Links Notation Parser for Rust
Rust implementation of the Links Notation parser using nom parser combinator library.
Installation
Add this to your Cargo.toml:
[]
= { = "." } # For local development
# Or from a registry:
# links-notation = "0.9.0"
From Source
Clone the repository and build:
Build
Build the project:
Build with optimizations:
Test
Run tests:
Run tests with output:
Usage
Using the lino! Macro (Recommended)
The lino! macro provides compile-time validation and a convenient way to work with Links Notation. It supports two syntax options:
Direct Syntax (Recommended for Simple Cases)
Write Links Notation directly without quotes for a cleaner, more native feel:
use lino;
String Literal Syntax (For Complex Cases)
Use string literals when you need special characters, newlines, or quoted strings:
use lino;
Benefits
The lino! macro:
- Direct syntax: Write Links Notation natively without quotes
- Compile-time validation: Syntax errors are caught at compile time
- Clear error messages: Descriptive errors for invalid syntax
- Type-safe: Returns fully typed
LiNo<String>structures - Zero overhead: Validation happens at compile time
When to Use Each Syntax
| Use Case | Syntax |
|---|---|
| Simple identifiers | lino!(papa has car) |
| Nested links | lino!(papa (loves mama)) |
| Links with IDs | lino!((myId: value)) |
| Multiline content | lino!("line1\nline2") |
| Quoted strings with spaces | lino!(r#"("my id": "my value")"#) |
| Indented syntax | lino!(r#"id:\n child"#) |
Basic Runtime Parsing
For dynamic content, use the runtime parser:
use ;
Working with Links
use LiNo;
// Create links programmatically
let reference = Ref;
let link = Link ;
// Check link types
if link.is_link
if reference.is_ref
Formatting Output
use parse_lino;
let input = "(parent: child1 child2)";
let parsed = parse_lino.unwrap;
// Regular formatting (parenthesized)
println!;
// Alternate formatting (line-based)
println!;
Handling Different Input Formats
use parse_lino;
// Single line format
let single_line = "id: value1 value2";
let parsed = parse_lino?;
// Parenthesized format
let parenthesized = "(id: value1 value2)";
let parsed = parse_lino?;
// Multi-line with indentation
let indented = r#"parent
child1
child2"#;
let parsed = parse_lino?;
// Quoted identifiers and values
let quoted = r#"("quoted id": "value with spaces")"#;
let parsed = parse_lino?;
Streaming Parsing
StreamParser accepts arbitrary chunks and invokes callbacks only for complete
top-level records. Disable collection for bounded-memory callback use.
use StreamParser;
let mut stream = new;
stream.set_collect.on_link;
stream.write?;
stream.write?;
stream.finish?;
StreamParser::parse_chunks(chunks) returns a lazy Iterator. The parser also
provides position, drain, reset, and maximum-buffer controls. See the
runnable example.
Tuple Conversion
The library supports ergonomic conversion from Rust tuples to Links Notation, similar to C#'s tuple conversion feature. This allows you to create links using native Rust tuple syntax.
Basic Usage
use LiNo;
// Convert a 2-tuple to a link
let link: = .into;
println!; // (papa: mama)
// Convert a 3-tuple to a link
let link: = .into;
println!; // (papa: loves mama)
// Convert a 4-tuple to a link
let link: = .into;
println!; // (id: val1 val2 val3)
Mixed Tuple Types
You can also mix strings and LiNo values in tuples:
use LiNo;
// Mix string and LiNo
let child = Ref;
let link: = .into;
println!; // (parent: child)
// Create anonymous links from multiple LiNo values
let a = Ref;
let b = Ref;
let link: = .into;
println!; // (a b)
Complex Nested Structures
Tuples can be nested to create complex link structures:
use ;
// Create nested links using tuples
let loves_mama: = .into;
let papa: = .into;
let son: = .into;
let daughter: = .into;
let links = vec!;
let result = format_links;
println!;
// Output:
// (papa: (lovesMama: loves mama))
// (son: lovesMama)
// (daughter: lovesMama)
Supported Tuple Conversions
Tuple conversions are supported for tuples of size 2 through 12 (following Rust's standard library convention). For each tuple size N, four conversion types are implemented:
-
All
&str- First element becomes ID, rest become values("id", "v1", ...)→(id: v1 ...)
-
All
String- Same as above but with owned strings(id.to_string(), v1.to_string(), ...)→(id: v1 ...)
-
&strID withLiNo<String>values - For nested links("id", lino1, lino2, ...)→(id: <lino1> <lino2> ...)
-
All
LiNo<String>- Creates anonymous link (no ID)(lino1, lino2, ...)→(<lino1> <lino2> ...)
Examples by Tuple Size
use LiNo;
// 2-tuple
let link: = .into; // (id: value)
// 5-tuple
let link: = .into; // (id: v1 v2 v3 v4)
// 8-tuple
let link: = .into;
// 12-tuple (maximum)
let link: = .into;
// Anonymous links from LiNo tuples
let refs: = .map.collect;
let link: = .into;
// Result: (v1 v2 v3 v4 v5 v6)
This macro-generated implementation reduces code duplication while providing compile-time type safety for all tuple sizes.
Alternative APIs for Arbitrary-Length Links
Since Rust doesn't support variadic generics, tuples are limited to 12 elements (following Rust's standard library convention). For links with more than 12 values or when the number of values is determined at runtime, use one of these alternative APIs:
Vec-based Conversions
Convert vectors directly to links:
use LiNo;
// Anonymous link from Vec<&str>
let values: = vec!;
let link: = values.into;
println!; // (a b c d e)
// Named link from (id, Vec) tuple
let values: = vec!;
let link: = .into;
println!; // (myLink: v1 v2 v3 v4 v5)
// Large links with more than 12 values
let values: = .map.collect;
let link: = .into;
LiNoBuilder (Fluent API)
Build links using a fluent API for maximum flexibility:
use ;
// Build a link with chained method calls
let link: = new
.id
.value
.value
.value
.build;
println!; // (myLink: v1 v2 v3)
// Build anonymous link (no ID)
let link: = new
.value
.value
.value
.build;
println!; // (a b c)
// Mix values and nested LiNo elements
let nested: = .into;
let link: = new
.id
.lino
.value
.build;
println!; // (outer: (inner: a b) c)
// Add multiple values at once
let link: = new
.id
.values
.build;
println!; // (batch: a b c d)
LiNo Static Methods
Create links directly using static methods:
use LiNo;
// Create a named link with LiNo::new()
let values: = vec!;
let link = new;
println!; // (myId: a b)
// Create an anonymous link with LiNo::anonymous()
let values: = vec!;
let link = anonymous;
println!; // (x y z)
// Create a reference with LiNo::reference()
let r: = reference;
println!; // hello
// Create links with arbitrary number of values
let values: =
.map
.collect;
let link = new;
API Summary
| API | Max Length | Use Case |
|---|---|---|
| Tuple conversion | 12 | Most common cases, ergonomic syntax |
| Vec conversion | Unlimited | Runtime-determined or large fixed sets |
| LiNoBuilder | Unlimited | Fluent construction, mixing types |
| LiNo::new() | Unlimited | Direct construction with Vec |
Syntax Examples
Doublets (2-tuple)
papa (lovesMama: loves mama)
son lovesMama
daughter lovesMama
all (love mama)
Triplets (3-tuple)
papa has car
mama has house
(papa and mama) are happy
N-tuples with References
(linksNotation: links notation)
(This is a linksNotation as well)
(linksNotation supports (unlimited number (of references) in each link))
Indented Structure
parent
child1
child2
grandchild1
grandchild2
Multi-line Groups
A parenthesized group opens a nested context: its body starts fresh at indentation level zero and follows the same rules as the root document, so a line break inside parentheses is structure rather than decoration.
value (
id "1"
label "one"
)
The document above parses to (value ((id 1) (label one))) - two children, each
a link of its own - rather than to one flat list in which the boundary between
id and label would be lost. A body that stays on a single line still
collapses to a single link, so (a b c) is unchanged.
use ;
let input = r#"value (
id "1"
label "one"
)"#;
let links = parse_lino_to_links?;
println!; // (value ((id 1) (label one)))
Comments
A # hides the rest of the line it stands on, so a document can carry prose
about itself:
# the machines this deploys to
deploy: staging # only staging, for now
Both comments are gone by the time the document is read, leaving the single
link (deploy: staging). A # only opens a comment where a reference could
begin, so a # inside a token (issue#1047) and a # inside a delimited
reference ("#") stay ordinary characters.
A formatter keeps the same rule from the other side: a reference that begins
with a # is written quoted ('#tag'), so a document it writes reads back as
itself.
Comments are on by default, and a parser can be told to read # as an ordinary
character again, for documents written before comments existed:
use ;
let document = "# the machines this deploys to\ndeploy: staging # only staging, for now\n";
let links = parse_lino_to_links?;
println!; // (deploy: staging)
let config = without_comments;
let links = parse_lino_to_links_with_config?;
println!; // (# a b)
API Reference
Enums
LiNo<T>
Represents either a Link or a Reference:
Link { id: Option<T>, values: Vec<Self> }- A link with optional ID and child valuesRef(T)- A reference to another link
Methods
Methods for LiNo<T>
is_ref() -> bool- Returns true if this is a referenceis_link() -> bool- Returns true if this is a link
Functions
parse_lino(document: &str) -> Result<LiNo<String>, ParseError>
Parses a Links Notation document string and returns the parsed structure or an error.
parse_lino_with_config(document: &str, config: &ParserConfig) -> Result<LiNo<String>, ParseError>
Parses the same way, with the parser configured. parse_lino_to_links and
parse_lino_to_links_with_config are the same pair, returning the top-level
links rather than one document link.
Configuration
ParserConfig
comments: bool- Whether a#opens a comment that runs to the end of its line (default:true)ParserConfig::new()- The defaultsParserConfig::without_comments()-#as an ordinary character
Formatting
The Display trait is implemented for LiNo<T> where T: ToString:
- Regular format:
format!("{}", lino)- Parenthesized output - Alternate format:
format!("{:#}", lino)- Line-based output
Maintenance
Linting and Formatting
Check code formatting:
Auto-fix formatting:
Run Clippy linter:
Pre-commit Hooks
This project uses pre-commit hooks that automatically run cargo fmt and
cargo check before commits. To set up pre-commit hooks locally:
# From repository root
Dependencies
- nom (8.0) - Parser combinator library
Error Handling
A parse error says where the document stopped making sense. Printing it gives the line and the column, what could have stood there, and the offending line with a caret under it:
match parse_lino
Syntax error at line 2, column 12: expected "(", a reference or end of line, found ":"
2 | stage: rust: nextest
| ^
The same position is available as fields, for callers that report errors themselves rather than printing them:
use ;
if let Err = parse_lino
ParseError::EmptyInput is returned for input that is empty or only
whitespace. cargo run --example parse_error_positions prints what several
broken documents report.
Maintenance
Code Formatting
This project uses rustfmt for code formatting and clippy for linting.
Format all files
Check formatting (without modifying files)
Run linter
These checks are also enforced in CI. Pull requests with formatting issues will fail the format check.