# fyaml

A safe Rust wrapper around the `libfyaml` C library for parsing and
manipulating YAML documents.

## Overview

`fyaml` provides an idiomatic Rust interface to the high-performance
`libfyaml` YAML parsing library. It supports DOM-style navigation,
zero-copy scalar access, node type introspection, multi-document
parsing, document mutation, and a serde-compatible `Value` type.

## Status

**Early development** - This library is functional but has not yet been
widely used or audited. The API may change, and edge cases may exist. If
you need a mature, battle-tested YAML library, consider `serde_yml` or
`serde-yaml-ng` instead.

## Why libfyaml?

`fyaml` is built on [libfyaml](https://github.com/pantoniou/libfyaml), a
modern C library that offers several advantages over the traditional
`libyaml`:

- **Full YAML 1.2 compliance** with YAML 1.3 preparation
- **Zero-copy architecture** for efficient large document handling
- **No artificial limits** (libyaml has a [1024-char implicit key
  limit](https://github.com/yaml/libyaml/blob/master/src/scanner.c#L1065-L1074))
- **[Up to 24x
  faster](https://github.com/pantoniou/libfyaml/blob/master/CHANGELOG.md#091---2025-12-28)**
  on large files in streaming mode (vs document mode)
- **Rich manipulation APIs** including YPATH expressions for path
  queries
- **MIT licensed** (as of v0.9.1)

This makes `fyaml` suitable for use cases requiring DOM manipulation,
YAML transformation tools, or configuration inspection utilities where
path-based queries are convenient.

## Features

- Parse YAML strings into document objects
- **Zero-copy scalar access** via lifetime-bound `NodeRef` and
  `ValueRef`
- Navigate nodes using path-based queries (e.g., `/foo/bar`, `/list/0`)
- Support for all YAML node types: scalars, sequences, and mappings
- Iterate over mapping key-value pairs and sequence items
- Convert nodes back to YAML strings
- Multi-document stream parsing via `FyParser`
- Read YAML from stdin (single document or stream)
- **Document mutation** via `Editor` with compile-time safety
- **Style and comment preservation** during edits: comments, quote
  styles, block/flow structure
- **`ValueRef` type**: Zero-copy typed access with YAML type
  interpretation
  - `as_str()`, `as_bool()`, `as_i64()`, `as_f64()`, `is_null()`
  - Non-plain scalars (quoted, literal, folded) preserved as strings
- **`Value` type**: Pure Rust enum with serde support
  - Serialize/deserialize with any serde-compatible format (JSON, TOML,
    etc.)
  - Emit YAML using libfyaml for standards-compliant output
  - Convenient indexing: `value["key"]`, `value[0]`

## Error Handling

Parse errors include detailed location information (line and column
numbers), making it easy to report errors to users or integrate with
IDEs and linters.

``` rust
use fyaml::Document;

let result = Document::parse_str("[unclosed bracket");
if let Err(e) = result {
    // Access structured error info
    if let fyaml::Error::ParseError(parse_err) = &e {
        println!("Error: {}", parse_err.message());
        if let Some((line, col)) = parse_err.location() {
            println!("At line {}, column {}", line, col);
        }
    }
    // Or just display it nicely
    println!("{}", e);  // "Parse error at 2:1: flow sequence without a closing bracket"
}
```

The `ParseError` type provides:

- `message()` - The error message from libfyaml
- `line()` - Line number (1-based), if available
- `column()` - Column number (1-based), if available
- `location()` - Tuple of (line, column) if both available

All parsing methods (`Document::parse_str`, `Document::from_string`,
`Document::from_bytes`, `Editor::build_from_yaml`) capture errors
silently without printing to stderr, making the library suitable for use
in GUI applications and test suites.

## Zero-Copy Architecture

`fyaml` leverages libfyaml's zero-copy design for efficient memory
usage. When you access scalar values through `NodeRef` or `ValueRef`,
you get references directly into the parsed document buffer - no string
copying or allocation occurs.

``` rust
use fyaml::Document;

let doc = Document::parse_str("message: Hello, World!").unwrap();
let root = doc.root().unwrap();
let node = root.at_path("/message").unwrap();

// Zero-copy: this &str points directly into the document's memory
let s: &str = node.scalar_str().unwrap();
assert_eq!(s, "Hello, World!");

// The reference is tied to the document's lifetime -
// this prevents use-after-free at compile time
```

This is particularly beneficial for:

- **Large documents**: Read gigabytes of YAML without doubling memory
  usage
- **Config parsing**: Extract only the values you need without copying
  everything
- **High-throughput processing**: Minimize allocations in hot paths

## Style and Comment Preservation

When modifying documents with `Editor`, `fyaml` preserves formatting and
comments. This is essential for configuration files where maintaining
the original style improves readability and diff-friendliness.

### What IS preserved:

- **Comments**: Top-level, inline, and end-of-line comments
- **Quote styles**: Single-quoted (`'value'`), double-quoted
  (`"value"`), and plain scalars
- **Block scalar styles**: Literal (`|`) and folded (`>`) blocks
- **Collection styles**: Flow (`[a, b]`, `{a: 1}`) vs block (indented)
  sequences/mappings

``` rust
use fyaml::Document;

// Comments and quote styles are preserved through edits
let yaml = "# Database configuration
database:
  host: 'localhost'  # local dev server
  port: 5432
";

let mut doc = Document::parse_str(yaml).unwrap();
{
    let mut ed = doc.edit();
    ed.set_yaml_at("/database/port", "5433").unwrap();
}

let output = doc.emit().unwrap();
// Comments preserved
assert!(output.contains("# Database configuration"));
assert!(output.contains("# local dev server"));
// Quote style preserved
assert!(output.contains("'localhost'"));
```

Block scalars are also preserved:

``` rust
use fyaml::Document;

let yaml = "script: |
  echo hello
  echo world
name: test
";

let mut doc = Document::parse_str(yaml).unwrap();
{
    let mut ed = doc.edit();
    ed.set_yaml_at("/name", "modified").unwrap();
}

let output = doc.emit().unwrap();
// Literal block style (|) is preserved for the script
assert!(output.contains("script: |"));
```

### Formatting notes:

- Flow collections (`[a, b]`, `{a: 1}`) are preserved as flow style but
  may be reformatted across multiple lines by libfyaml's emitter

## Path Syntax

Paths use `/` as the separator (YPATH/JSON Pointer style):

- `/key` - access a mapping key
- `/0` - access a sequence index
- `/parent/child/0` - nested access

``` rust
use fyaml::Document;

let yaml = "
database:
  host: localhost
  ports:
    - 5432
    - 5433
";

let doc = Document::parse_str(yaml).unwrap();
let root = doc.root().unwrap();

// Access mapping key
let db = root.at_path("/database").unwrap();
assert!(db.is_mapping());

// Nested access
let host = root.at_path("/database/host").unwrap();
assert_eq!(host.scalar_str().unwrap(), "localhost");

// Sequence index
let first_port = root.at_path("/database/ports/0").unwrap();
assert_eq!(first_port.scalar_str().unwrap(), "5432");
```

## Usage

### Working with Value (high-level, owned)

The `Value` type provides a convenient, serde-compatible way to work
with YAML:

``` rust
use fyaml::Value;

// Parse YAML
let value: Value = "name: Alice\nage: 30".parse().unwrap();

// Access values with indexing
assert_eq!(value["name"].as_str(), Some("Alice"));
assert_eq!(value["age"].as_i64(), Some(30));

// Emit back to YAML
let yaml = value.to_yaml_string().unwrap();
```

### Zero-copy with Document and NodeRef

For more control and zero-copy scalar access, use the `Document` API:

``` rust
use fyaml::Document;

let doc = Document::parse_str("database:\n  host: localhost\n  port: 5432").unwrap();
let root = doc.root().unwrap();

// Zero-copy: returns &str pointing into document memory
let host = root.at_path("/database/host").unwrap();
assert_eq!(host.scalar_str().unwrap(), "localhost");

// Navigation by path
let port = root.at_path("/database/port").unwrap();
assert_eq!(port.scalar_str().unwrap(), "5432");
```

### Zero-copy typed access with ValueRef

`ValueRef` wraps `NodeRef` and provides typed accessors that interpret
YAML scalars on demand without allocation:

``` rust
use fyaml::Document;

let doc = Document::parse_str("name: Alice\nage: 30\nactive: yes").unwrap();
let root = doc.root_value().unwrap();

// Zero-copy typed access
assert_eq!(root.get("name").unwrap().as_str(), Some("Alice"));
assert_eq!(root.get("age").unwrap().as_i64(), Some(30));
assert_eq!(root.get("active").unwrap().as_bool(), Some(true));  // yes -> true
```

Non-plain scalars (quoted, literal `|`, folded `>`) are NOT
type-interpreted:

``` rust
use fyaml::Document;

let doc = Document::parse_str("quoted: 'true'\nunquoted: true").unwrap();
let root = doc.root_value().unwrap();

// Quoted: string, not bool
assert_eq!(root.get("quoted").unwrap().as_bool(), None);
assert_eq!(root.get("quoted").unwrap().as_str(), Some("true"));

// Unquoted: interpreted as bool
assert_eq!(root.get("unquoted").unwrap().as_bool(), Some(true));
```

### Mutation with Editor

Use `Document::edit()` to get an exclusive `Editor` for modifications:

``` rust
use fyaml::Document;

let mut doc = Document::parse_str("name: Alice").unwrap();

// Mutation phase - NodeRef cannot exist during this
{
    let mut ed = doc.edit();
    ed.set_yaml_at("/name", "'Bob'").unwrap();   // Preserve quotes
    ed.set_yaml_at("/age", "30").unwrap();       // Add new key
    ed.delete_at("/name").unwrap();              // Delete key
    ed.set_yaml_at("/name", "\"Charlie\"").unwrap();  // Re-add
}

// Read phase - safe to access nodes again
let root = doc.root().unwrap();
assert_eq!(root.at_path("/name").unwrap().scalar_str().unwrap(), "Charlie");
assert_eq!(root.at_path("/age").unwrap().scalar_str().unwrap(), "30");
```

Building complex structures:

``` rust
use fyaml::Document;

let mut doc = Document::new().unwrap();
{
    let mut ed = doc.edit();
    let root = ed.build_from_yaml("users:\n  - name: Alice\n  - name: Bob").unwrap();
    ed.set_root(root).unwrap();
}
assert!(doc.root().is_some());
```

Modifying sequence elements:

``` rust
use fyaml::Document;

let mut doc = Document::parse_str("items:\n  - a\n  - b\n  - c").unwrap();
{
    let mut ed = doc.edit();
    // Replace by positive index
    ed.set_yaml_at("/items/0", "first").unwrap();
    // Replace by negative index (Python-style: -1 = last element)
    ed.set_yaml_at("/items/-1", "last").unwrap();
}
assert_eq!(doc.at_path("/items/0").unwrap().scalar_str().unwrap(), "first");
assert_eq!(doc.at_path("/items/1").unwrap().scalar_str().unwrap(), "b");
assert_eq!(doc.at_path("/items/2").unwrap().scalar_str().unwrap(), "last");
```

Building structures programmatically with handle-level operations:

``` rust
use fyaml::Document;

let mut doc = Document::new().unwrap();
{
    let mut ed = doc.edit();

    // Build a sequence of servers
    let mut servers = ed.build_sequence().unwrap();
    let s1 = ed.build_scalar("web1").unwrap();
    let s2 = ed.build_scalar("web2").unwrap();
    ed.seq_append(&mut servers, s1).unwrap();
    ed.seq_append(&mut servers, s2).unwrap();

    // Build the root mapping
    let mut root = ed.build_mapping().unwrap();
    let k1 = ed.build_scalar("host").unwrap();
    let v1 = ed.build_scalar("localhost").unwrap();
    ed.map_insert(&mut root, k1, v1).unwrap();
    let k2 = ed.build_scalar("port").unwrap();
    let v2 = ed.build_null().unwrap();
    ed.map_insert(&mut root, k2, v2).unwrap();
    let k3 = ed.build_scalar("servers").unwrap();
    ed.map_insert(&mut root, k3, servers).unwrap();

    // Tag the root
    ed.set_tag(&mut root, "!config").unwrap();
    ed.set_root(root).unwrap();
}

let root = doc.root().unwrap();
assert_eq!(root.at_path("/host").unwrap().scalar_str().unwrap(), "localhost");
assert_eq!(root.tag_str().unwrap().unwrap(), "!config");
assert_eq!(root.at_path("/servers/0").unwrap().scalar_str().unwrap(), "web1");
```

### Multi-document parsing with FyParser

Use `FyParser` for parsing YAML streams with multiple documents:

``` rust
use fyaml::FyParser;

let yaml = "---\ndoc1: value1\n---\ndoc2: value2";
let parser = FyParser::from_string(yaml).unwrap();

let docs: Vec<_> = parser.doc_iter().filter_map(|r| r.ok()).collect();
assert_eq!(docs.len(), 2);

// Each document is independent
assert_eq!(docs[0].at_path("/doc1").unwrap().scalar_str().unwrap(), "value1");
assert_eq!(docs[1].at_path("/doc2").unwrap().scalar_str().unwrap(), "value2");
```

### Reading from stdin

For CLI tools that read YAML from stdin:

```rust,no_run
use fyaml::Document;

// Single document from stdin
let doc = Document::from_stdin().unwrap();
println!("{}", doc.emit().unwrap());
```

For multi-document streams:

```rust,no_run
use fyaml::FyParser;

// Default: line-buffered mode for interactive/streaming use
let parser = FyParser::from_stdin().unwrap();

for doc_result in parser.doc_iter() {
    let doc = doc_result.unwrap();
    println!("{}", doc.emit().unwrap());
}
```

For batch processing where efficiency matters more than interactivity:

```rust,no_run
use fyaml::FyParser;

// Block-buffered mode: more efficient for large inputs
let parser = FyParser::from_stdin_with_line_buffer(false).unwrap();

for doc_result in parser.doc_iter() {
    // Process each document
}
```

### Serde integration

`Value` works with any serde-compatible format:

``` rust
use fyaml::Value;

let value: Value = "key: value".parse().unwrap();

// Convert to JSON
let json = serde_json::to_string(&value).unwrap();
assert_eq!(json, r#"{"key":"value"}"#);

// Parse from JSON
let from_json: Value = serde_json::from_str(&json).unwrap();
```

### Iterating over mappings

``` rust
use fyaml::Document;

let doc = Document::parse_str("a: 1\nb: 2\nc: 3").unwrap();
let root = doc.root().unwrap();

for (key, value) in root.map_iter() {
    println!("{}: {}", key.scalar_str().unwrap(), value.scalar_str().unwrap());
}
```

### Iterating over sequences

``` rust
use fyaml::Document;

let doc = Document::parse_str("- apple\n- banana\n- cherry").unwrap();
let root = doc.root().unwrap();

for item in root.seq_iter() {
    println!("{}", item.scalar_str().unwrap());
}
```

### Checking node types

``` rust
use fyaml::{Document, NodeType};

let doc = Document::parse_str("key: value").unwrap();
let root = doc.root().unwrap();

assert!(root.is_mapping());
assert_eq!(root.kind(), NodeType::Mapping);

let value = root.at_path("/key").unwrap();
assert!(value.is_scalar());
```

## API Reference

### Main Types

<table>
<thead>
<tr class="header">
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>Document</code></td>
<td>Parsed YAML document (owns the data)</td>
</tr>
<tr class="even">
<td><code>NodeRef&lt;'doc&gt;</code></td>
<td>Zero-copy reference to a node (borrows document)</td>
</tr>
<tr class="odd">
<td><code>ValueRef&lt;'doc&gt;</code></td>
<td>Zero-copy typed access (wraps NodeRef)</td>
</tr>
<tr class="even">
<td><code>Editor&lt;'doc&gt;</code></td>
<td>Exclusive mutable access to document</td>
</tr>
<tr class="odd">
<td><code>FyParser</code></td>
<td>Multi-document stream parser</td>
</tr>
<tr class="even">
<td><code>Value</code></td>
<td>Owned serde-compatible YAML value</td>
</tr>
<tr class="odd">
<td><code>Number</code></td>
<td>Numeric value: <code>Int(i64)</code>, <code>UInt(u64)</code>,
<code>Float(f64)</code></td>
</tr>
<tr class="even">
<td><code>TaggedValue</code></td>
<td>Value with an associated YAML tag</td>
</tr>
<tr class="odd">
<td><code>ParseError</code></td>
<td>Rich parse error with line/column location</td>
</tr>
</tbody>
</table>

### Enums

<table>
<thead>
<tr class="header">
<th>Type</th>
<th>Variants</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>NodeType</code></td>
<td><code>Scalar</code>, <code>Sequence</code>,
<code>Mapping</code></td>
</tr>
<tr class="even">
<td><code>NodeStyle</code></td>
<td><code>Plain</code>, <code>SingleQuoted</code>,
<code>DoubleQuoted</code>, <code>Literal</code>, <code>Folded</code>,
etc.</td>
</tr>
</tbody>
</table>

### Document Methods

<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>Document::parse_str(yaml)</code></td>
<td>Parse YAML string into Document</td>
</tr>
<tr class="even">
<td><code>Document::new()</code></td>
<td>Create empty document</td>
</tr>
<tr class="odd">
<td><code>Document::from_stdin()</code></td>
<td>Parse single document from stdin</td>
</tr>
<tr class="even">
<td><code>doc.root()</code></td>
<td>Get root node as <code>Option&lt;NodeRef&gt;</code></td>
</tr>
<tr class="odd">
<td><code>doc.root_value()</code></td>
<td>Get root node as <code>Option&lt;ValueRef&gt;</code></td>
</tr>
<tr class="even">
<td><code>doc.at_path(path)</code></td>
<td>Navigate to node by path</td>
</tr>
<tr class="odd">
<td><code>doc.edit()</code></td>
<td>Get exclusive <code>Editor</code> for mutations</td>
</tr>
<tr class="even">
<td><code>doc.emit()</code></td>
<td>Emit document as YAML string</td>
</tr>
</tbody>
</table>

### NodeRef Methods (zero-copy)

<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>node.kind()</code></td>
<td>Get node type (<code>NodeType</code>)</td>
</tr>
<tr class="even">
<td><code>node.style()</code></td>
<td>Get node style (<code>NodeStyle</code>)</td>
</tr>
<tr class="odd">
<td><code>node.is_scalar()</code></td>
<td>Check if node is a scalar</td>
</tr>
<tr class="even">
<td><code>node.is_mapping()</code></td>
<td>Check if node is a mapping</td>
</tr>
<tr class="odd">
<td><code>node.is_sequence()</code></td>
<td>Check if node is a sequence</td>
</tr>
<tr class="even">
<td><code>node.is_quoted()</code></td>
<td>Check if scalar is quoted</td>
</tr>
<tr class="odd">
<td><code>node.is_non_plain()</code></td>
<td>Check if scalar has non-plain style</td>
</tr>
<tr class="even">
<td><code>node.scalar_str()</code></td>
<td>Get scalar as <code>&amp;str</code> (zero-copy)</td>
</tr>
<tr class="odd">
<td><code>node.scalar_bytes()</code></td>
<td>Get scalar as <code>&amp;[u8]</code> (zero-copy)</td>
</tr>
<tr class="even">
<td><code>node.at_path(path)</code></td>
<td>Navigate to child by path</td>
</tr>
<tr class="odd">
<td><code>node.seq_iter()</code></td>
<td>Iterate over sequence items</td>
</tr>
<tr class="even">
<td><code>node.map_iter()</code></td>
<td>Iterate over mapping key-value pairs</td>
</tr>
<tr class="odd">
<td><code>node.seq_len()</code></td>
<td>Get sequence length</td>
</tr>
<tr class="even">
<td><code>node.map_len()</code></td>
<td>Get mapping length</td>
</tr>
<tr class="odd">
<td><code>node.seq_get(i)</code></td>
<td>Get sequence item by index</td>
</tr>
<tr class="even">
<td><code>node.map_get(key)</code></td>
<td>Get mapping value by string key</td>
</tr>
<tr class="odd">
<td><code>node.tag_str()</code></td>
<td>Get YAML tag (zero-copy)</td>
</tr>
<tr class="even">
<td><code>node.emit()</code></td>
<td>Emit node as YAML string</td>
</tr>
</tbody>
</table>

### ValueRef Methods (zero-copy typed access)

<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>value.as_str()</code></td>
<td>Get string (zero-copy) <code>Option&lt;&amp;str&gt;</code></td>
</tr>
<tr class="even">
<td><code>value.as_bool()</code></td>
<td>Interpret as boolean (yes/no/on/off/true/false)</td>
</tr>
<tr class="odd">
<td><code>value.as_i64()</code></td>
<td>Interpret as signed integer (hex/octal/binary)</td>
</tr>
<tr class="even">
<td><code>value.as_u64()</code></td>
<td>Interpret as unsigned integer</td>
</tr>
<tr class="odd">
<td><code>value.as_f64()</code></td>
<td>Interpret as float (.inf, .nan support)</td>
</tr>
<tr class="even">
<td><code>value.is_null()</code></td>
<td>Check for null/~/empty</td>
</tr>
<tr class="odd">
<td><code>value.is_scalar()</code></td>
<td>Check if scalar</td>
</tr>
<tr class="even">
<td><code>value.is_sequence()</code></td>
<td>Check if sequence</td>
</tr>
<tr class="odd">
<td><code>value.is_mapping()</code></td>
<td>Check if mapping</td>
</tr>
<tr class="even">
<td><code>value.get(key)</code></td>
<td>Get mapping value by key</td>
</tr>
<tr class="odd">
<td><code>value.index(i)</code></td>
<td>Get sequence item by index</td>
</tr>
<tr class="even">
<td><code>value.at_path(path)</code></td>
<td>Navigate by path</td>
</tr>
<tr class="odd">
<td><code>value.seq_iter()</code></td>
<td>Iterate over sequence as <code>ValueRef</code></td>
</tr>
<tr class="even">
<td><code>value.map_iter()</code></td>
<td>Iterate over mapping as <code>(ValueRef, ValueRef)</code></td>
</tr>
<tr class="odd">
<td><code>value.tag()</code></td>
<td>Get YAML tag (zero-copy)</td>
</tr>
</tbody>
</table>

### Editor Methods

<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>ed.set_yaml_at(path, yaml)</code></td>
<td>Set/replace value at path (mappings and sequences)</td>
</tr>
<tr class="even">
<td><code>ed.delete_at(path)</code></td>
<td>Delete value at path, returns <code>bool</code></td>
</tr>
<tr class="odd">
<td><code>ed.build_from_yaml(yaml)</code></td>
<td>Build detached node from YAML</td>
</tr>
<tr class="even">
<td><code>ed.build_scalar(value)</code></td>
<td>Build plain scalar node</td>
</tr>
<tr class="odd">
<td><code>ed.build_sequence()</code></td>
<td>Build empty sequence node</td>
</tr>
<tr class="even">
<td><code>ed.build_mapping()</code></td>
<td>Build empty mapping node</td>
</tr>
<tr class="odd">
<td><code>ed.build_null()</code></td>
<td>Build null scalar node</td>
</tr>
<tr class="even">
<td><code>ed.set_root(handle)</code></td>
<td>Set document root</td>
</tr>
<tr class="odd">
<td><code>ed.copy_node(node)</code></td>
<td>Copy node from any document</td>
</tr>
<tr class="even">
<td><code>ed.seq_append(seq, item)</code></td>
<td>Append item to detached sequence handle</td>
</tr>
<tr class="odd">
<td><code>ed.map_insert(map, key, val)</code></td>
<td>Insert key-value pair into detached mapping</td>
</tr>
<tr class="even">
<td><code>ed.set_tag(node, tag)</code></td>
<td>Set YAML tag on detached node</td>
</tr>
<tr class="odd">
<td><code>ed.set_style(node, style)</code></td>
<td>Set YAML style on detached node, returns actual style</td>
</tr>
<tr class="even">
<td><code>ed.seq_append_at(path, item)</code></td>
<td>Append item to sequence at path</td>
</tr>
<tr class="odd">
<td><code>ed.root()</code></td>
<td>Read root during edit session</td>
</tr>
<tr class="even">
<td><code>ed.at_path(path)</code></td>
<td>Navigate during edit session</td>
</tr>
</tbody>
</table>

### FyParser Methods

<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>FyParser::from_string(yaml)</code></td>
<td>Create parser from YAML string</td>
</tr>
<tr class="even">
<td><code>FyParser::from_stdin()</code></td>
<td>Create parser from stdin (line-buffered)</td>
</tr>
<tr class="odd">
<td><code>FyParser::from_stdin_with_line_buffer(b)</code></td>
<td>Configurable buffering</td>
</tr>
<tr class="even">
<td><code>parser.doc_iter()</code></td>
<td>Iterate over documents (yields
<code>Result&lt;Document&gt;</code>)</td>
</tr>
</tbody>
</table>

### Value Methods

<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>parse()</code></td>
<td>Parse YAML string into Value</td>
</tr>
<tr class="even">
<td><code>to_yaml_string()</code></td>
<td>Emit as YAML string via libfyaml</td>
</tr>
<tr class="odd">
<td><code>is_null()</code></td>
<td>Check if value is null</td>
</tr>
<tr class="even">
<td><code>is_bool()</code></td>
<td>Check if value is boolean</td>
</tr>
<tr class="odd">
<td><code>is_number()</code></td>
<td>Check if value is numeric</td>
</tr>
<tr class="even">
<td><code>is_string()</code></td>
<td>Check if value is a string</td>
</tr>
<tr class="odd">
<td><code>is_sequence()</code></td>
<td>Check if value is a sequence</td>
</tr>
<tr class="even">
<td><code>is_mapping()</code></td>
<td>Check if value is a mapping</td>
</tr>
<tr class="odd">
<td><code>is_tagged()</code></td>
<td>Check if value has a tag</td>
</tr>
<tr class="even">
<td><code>as_str()</code></td>
<td>Get as <code>&amp;str</code> if string</td>
</tr>
<tr class="odd">
<td><code>as_i64()</code></td>
<td>Get as <code>i64</code> if numeric</td>
</tr>
<tr class="even">
<td><code>as_u64()</code></td>
<td>Get as <code>u64</code> if numeric</td>
</tr>
<tr class="odd">
<td><code>as_f64()</code></td>
<td>Get as <code>f64</code> if numeric</td>
</tr>
<tr class="even">
<td><code>as_bool()</code></td>
<td>Get as <code>bool</code> if boolean</td>
</tr>
<tr class="odd">
<td><code>as_sequence()</code></td>
<td>Get as <code>&amp;[Value]</code> if sequence</td>
</tr>
<tr class="even">
<td><code>as_mapping()</code></td>
<td>Get as <code>&amp;IndexMap</code> if mapping</td>
</tr>
<tr class="odd">
<td><code>as_tagged()</code></td>
<td>Get as <code>&amp;TaggedValue</code> if tagged</td>
</tr>
<tr class="even">
<td><code>get(key)</code></td>
<td>Get value by key from mapping</td>
</tr>
<tr class="odd">
<td><code>[key]</code> / <code>[idx]</code></td>
<td>Index into mapping or sequence</td>
</tr>
</tbody>
</table>

### Iterators

<table>
<thead>
<tr class="header">
<th>Type</th>
<th>Yields</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>SeqIter&lt;'doc&gt;</code></td>
<td><code>NodeRef&lt;'doc&gt;</code></td>
<td>Sequence items</td>
</tr>
<tr class="even">
<td><code>MapIter&lt;'doc&gt;</code></td>
<td><code>(NodeRef&lt;'doc&gt;, NodeRef&lt;'doc&gt;)</code></td>
<td>Mapping key-value pairs</td>
</tr>
<tr class="odd">
<td><code>DocumentIterator</code></td>
<td><code>Result&lt;Document&gt;</code></td>
<td>Documents in stream</td>
</tr>
</tbody>
</table>

## Dependencies

- `libc` - C library bindings
- `fyaml-sys` - FFI bindings to libfyaml
- `log` - Logging framework
- `serde` - Serialization framework
- `indexmap` - Order-preserving map for YAML mappings

## Test Coverage

<table>
<thead>
<tr class="header">
<th>Metric</th>
<th>Coverage</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Lines</td>
<td>89.05%</td>
</tr>
<tr class="even">
<td>Functions</td>
<td>91.83%</td>
</tr>
<tr class="odd">
<td>Regions</td>
<td>91.22%</td>
</tr>
</tbody>
</table>

## Other Rust YAML Libraries

<table>
<thead>
<tr class="header">
<th>Library</th>
<th>Engine</th>
<th>Serde</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><a href="https://github.com/dtolnay/serde-yaml">serde_yaml</a></td>
<td><a
href="https://github.com/dtolnay/unsafe-libyaml">unsafe-libyaml</a>
(libyaml transpiled to Rust)</td>
<td>Yes</td>
<td>Deprecated (2024-03)</td>
</tr>
<tr class="even">
<td><a
href="https://github.com/sebastienrousseau/serde_yml">serde_yml</a></td>
<td><a
href="https://github.com/dtolnay/unsafe-libyaml">unsafe-libyaml</a></td>
<td>Yes</td>
<td>Maintained (fork of serde_yaml)</td>
</tr>
<tr class="odd">
<td><a
href="https://github.com/acatton/serde-yaml-ng">serde-yaml-ng</a></td>
<td><a
href="https://github.com/dtolnay/unsafe-libyaml">unsafe-libyaml</a></td>
<td>Yes</td>
<td>Active (migrating to libyaml-safer)</td>
</tr>
<tr class="even">
<td><a href="https://github.com/saphyr-rs/saphyr">saphyr</a></td>
<td>Pure Rust (fork of yaml-rust)</td>
<td>Soon</td>
<td>Active</td>
</tr>
<tr class="odd">
<td><a
href="https://github.com/Ethiraric/yaml-rust2">yaml-rust2</a></td>
<td>Pure Rust (fork of yaml-rust)</td>
<td>No</td>
<td>Active (high MSRV)</td>
</tr>
<tr class="even">
<td><a href="https://github.com/chyh1990/yaml-rust">yaml-rust</a></td>
<td>Pure Rust</td>
<td>No</td>
<td>Unmaintained</td>
</tr>
<tr class="odd">
<td>fyaml</td>
<td><a href="https://github.com/pantoniou/libfyaml">libfyaml</a> (C
library via FFI)</td>
<td>Yes</td>
<td>Development</td>
</tr>
</tbody>
</table>

### Choosing a Library

- **For serde integration**: `fyaml` provides a serde-compatible `Value`
  type with libfyaml-powered parsing and emission. Alternatives include
  `serde_yml` or `serde-yaml-ng` (based on unsafe-libyaml).

- **For pure Rust**: Use `saphyr` or `yaml-rust2` (no C dependencies,
  easier to audit).

- **For DOM manipulation and path queries**: `fyaml` provides convenient
  path-based navigation (`/foo/0/bar`) via libfyaml's YPATH support,
  plus a `Value` type for programmatic manipulation.

- **For maximum performance on large files**: `fyaml` benefits from
  libfyaml's zero-copy architecture and streaming optimizations.

## License

MIT License (c) 2024-2026 Valentin Lab. The LICENSE file is available
with the source.


# Changelog


## 0.5.1 (2026-06-26)

### Fix

* Upgrade to ``fyaml-sys`` 0.1.1-alpha.2 (libfyaml v1.0.0-alpha7) [Valentin Lab]

  Bump the ``fyaml-sys`` dependency to ``=0.1.1-alpha.2``, which vendors
  libfyaml ``v1.0.0-alpha7``. This fixes a re-emit bug where a block
  literal/folded scalar followed by a comment had the comment fused onto
  the scalar's last content line (e.g. ``fi## comment``).

  libfyaml v1.0 renamed the parse-config flag ``FYPCF_PARSE_COMMENTS`` to
  ``FYPCF_KEEP_COMMENTS`` (keeping the old name only as a ``#define`` alias,
  which bindgen does not surface). Update ``config.rs`` accordingly.

  Add a regression test asserting a block scalar followed by a comment no
  longer fuses the comment into the scalar on a parse -> emit round-trip.


## 0.5.0 (2026-02-02)

### New

* Add ``set_style`` method to ``Editor`` [Valentin Lab]

  Allows setting the YAML style (plain, single-quoted, double-quoted,
  literal, folded, flow, block) on detached node handles. libfyaml
  validates the requested style against node content and may keep the
  current style if the request is invalid; the actually-applied style
  is returned.

* Add handle-level ``Editor`` methods for programmatic node assembly. [Valentin Lab]

  Add ``seq_append()``, ``map_insert()``, ``set_tag()``, and
  ``build_null()`` to ``Editor`` for building YAML structures without
  parsing YAML snippets. These complement the existing path-based API
  (``set_yaml_at``, ``seq_append_at``) with lower-level handle operations.

  Update ``README.org`` with API table entries and a usage example.

### Fix

* Quote ambiguous strings during ``Value`` YAML emission. [Valentin Lab]

  Strings like "true", "null", "42" were emitted as plain scalars,
  causing them to be reinterpreted as bool/null/number on re-parse.
  Now ``needs_quoting()`` detects such values and the emitter wraps
  them in single quotes so they roundtrip correctly as ``Value::String``.

* ``build_null()`` now produces a proper YAML null node. [Valentin Lab]

  ``fy_node_create_scalar_copy(doc, NULL, 0)`` creates an empty scalar
  without libfyaml's internal ``is_null`` flag, making it indistinguishable
  from ``build_scalar("")``. Switch to ``build_from_yaml("null")`` which
  goes through the parser and sets ``is_null = true``.

  Tighten null emission tests: ``Value::Null`` must emit exactly ``"null"``,
  and must differ from ``Value::String("")``.

* ``Value::to_yaml_string()`` no longer adds trailing newline. [Valentin Lab]

  ``to_yaml_string()`` was using document-level emission
  (``fy_emit_document_to_string``) which appends ``\n``. Rewrite to use
  node-level emission via ``Editor`` API, matching ``NodeRef::emit()``
  behavior.

  Tighten existing test assertions that masked this with ``.trim()``.


## 0.4.0 (2026-01-24)

### New

* Add sequence element support to ``set_yaml_at`` [Valentin Lab]

  - Support positive and negative (Python-style) indices for sequences
  - Validate index bounds and return error for out-of-bounds access
  - Update documentation with sequence examples and supported parent types
  - Add comprehensive tests for sequence manipulation edge cases


## 0.3.0 (2026-01-22)

### New

* Add rich parse errors with line/column location info. [Valentin Lab]

  - Add ``ParseError`` type with ``line()``, ``column()``, ``location()`` accessors
  - Add ``diag`` module to capture libfyaml errors via ``fy_diag`` callbacks
  - Enable ``FYPCF_QUIET`` on all parse configs to suppress stderr output
  - Update ``Document::parse_str``, ``from_string``, ``from_bytes`` to return
    ``Error::ParseError`` with location info
  - Update ``Editor::build_from_yaml`` with RAII ``DiagGuard`` for diag restoration
  - Update ``FyParser`` stream iterator to capture errors with location

  This makes the library suitable for GUI applications and IDEs that need
  structured error information without stderr pollution.

* Refactor to enforce zero-copy aspects wherever possible. [Valentin Lab]


## 0.2.0 (2026-01-20)

### New

* Add ``from_stdin_with_line_buffer()`` for configurable stdin buffering. [Valentin Lab]

  Allows callers to control whether stdin uses line-buffered or block-buffered
  mode. Line-buffered is useful for streaming/interactive use (process documents
  as lines arrive), while block-buffered is more efficient for batch processing.

  The existing ``from_stdin()`` method now delegates to this with line_buffered=true
  to preserve backward compatibility.


