Links Notation Parser for C#
C# implementation of the Links Notation parser using Pegasus parser generator and Platform.Collections.
Installation
Package Manager
Install-Package Link.Foundation.Links.Notation
.NET CLI
dotnet add package Link.Foundation.Links.Notation
PackageReference
<PackageReference Include="Link.Foundation.Links.Notation" Version="0.9.0" />
Build from Source
Clone the repository and build:
git clone https://github.com/link-foundation/links-notation.git
cd links-notation/csharp
dotnet build Link.Foundation.Links.Notation.sln
Test
Run tests:
dotnet test
Usage
Basic Parsing
using Link.Foundation.Links.Notation;
// Create parser
var parser = new Parser();
// Parse Links Notation format string
string input = @"papa (lovesMama: loves mama)
son lovesMama
daughter lovesMama
all (love mama)";
var links = parser.Parse(input);
// Access parsed links
foreach (var link in links)
{
Console.WriteLine(link.ToString());
}
Converting Back to String
using Link.Foundation.Links.Notation;
// Format links back to string
string formatted = links.Format();
Console.WriteLine(formatted);
Working with Links
// Create link programmatically
var link = new Link<string>("id", new[] { "value1", "value2" });
// Access link properties
Console.WriteLine($"ID: {link.Id}");
foreach (var value in link.Values)
{
Console.WriteLine($"Value: {value}");
}
Advanced Usage with Generic Types
// Using numeric link addresses
var parser = new Parser<ulong>();
var numericLinks = parser.Parse("(1: 2 3)");
// Working with custom address types
var customParser = new Parser<Guid>();
Streaming Parsing
StreamParser accepts arbitrary chunks and raises LinkParsed only for
complete top-level records. Disable collection for bounded-memory event use.
var stream = new StreamParser { Collect = false };
stream.LinkParsed += Console.WriteLine;
stream.Write("profile:\n name Ada\n");
stream.Finish("next link");
ParseChunks and ParseChunksAsync expose native enumerable adapters. The
parser also provides Position, Drain, Reset, and MaxBufferSize.
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))
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.
var links = new Parser().Parse(@"value (
id ""1""
label ""one""
)");
Console.WriteLine(links[0]); // (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:
var links = new Parser().Parse("# the machines this deploys to\ndeploy: staging # only staging, for now\n");
Console.WriteLine(links[0]); // (deploy: staging)
var plain = new Parser(comments: false);
Console.WriteLine(plain.Parse("# a b\n")[0]); // (# a b)
API Reference
Classes
- Parser<TLinkAddress>: Main parser class for converting strings to links
(
new Parser(comments: false)reads#as an ordinary character) - Link<TLinkAddress>: Represents a single link with ID and values
- LinksGroup<TLinkAddress>: Container for grouping related links
- ParseException: Thrown when a document does not parse
Error Handling
Parse throws a ParseException whose message says where the document stopped
making sense and quotes the offending line with a caret under it:
try
{
new Parser().Parse("ci_gate x\nstage: rust: nextest\n");
}
catch (ParseException error)
{
Console.Error.WriteLine(error.Message);
Console.Error.WriteLine($"{error.Line}:{error.Column} (offset {error.Offset})");
}
Syntax error at line 2, column 12: unexpected ":"
2 | stage: rust: nextest
| ^
ParseException derives from FormatException, so callers that already catch
FormatException keep working, and carries Offset, Line, Column, Found,
LineText, Summary and Snippet for callers that report errors themselves.
Nesting Limit
Parser.MaxDepth is how deep links may nest (default: Parser.DefaultMaxDepth,
64, the same in every implementation). Every parenthesized group and every
indentation level is one level, and the lines of a document start at level 0,
so with new Parser(comments: true, maxDepth: 1) (a) is accepted while
((a)), (a (b)) and a group on an indented line are refused. A document
nested deeper is refused with a ParseException whose MaxDepth is set, rather
than recursed into until the stack runs out, and which points at the group or
the line that is one level too deep:
Nesting too deep at line 1, column 4: nesting depth exceeds the maximum of 3
1 | ((((a))))
| ^
MaxDepth is null for any other error. StreamParser reports the same error
as a StreamParseException whose ParseError is the ParseException and whose
Line, Column and Offset are counted from the start of the stream.
Extension Methods
- IListExtensions.Format(): Converts list of links back to string format
- ILinksGroupListExtensions: Additional operations for link groups
Maintenance
Linting and Formatting
Check code formatting:
dotnet format --verify-no-changes --verbosity diagnostic
Auto-fix formatting:
dotnet format
Pre-commit Hooks
This project uses pre-commit hooks. To set up pre-commit hooks locally:
# From repository root
pip install pre-commit
pre-commit install
Note: C# formatting checks are integrated into the CI pipeline using
dotnet format.
Dependencies
- .NET 10.0
- Microsoft.CSharp (4.7.0)
- Pegasus (4.1.0)
- Platform.Collections (0.3.2)
Maintenance
Code Formatting
This project uses dotnet format for code formatting.
Format all files
dotnet format
Check formatting (without modifying files)
dotnet format --verify-no-changes
These checks are also enforced in CI. Pull requests with formatting issues will fail the format check.
Documentation
For complete API documentation, visit: Link.Foundation.Links.Notation Documentation