Search by

link-foundation / links-notation

konard

Links Notation (lino) parser and formatter for PHP

Package info

github.com/link-foundation/links-notation

Language:JavaScript

pkg:composer/link-foundation/links-notation

Fund package maintenance!

Patreon

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 5

Open Issues: 5


README

Actions Status npm Version and Downloads count JavaScript
Actions Status Crates.io Version and Downloads count Rust
Actions Status NuGet Version and Downloads count C#
Actions Status PyPI Version and Downloads count Python
Actions Status Go Reference Go
Actions Status Maven Central Version Java
Actions Status Packagist Version and Downloads count PHP
Actions Status GitHub release C++
Actions Status Swift Package Manager Swift
Actions Status Maven Central Version Kotlin

Gitpod Open in GitHub Codespaces

Codacy Badge CodeFactor

Link Foundation's Links Notation Class Library.

Development starts in JavaScript and is checked against all ten hand-written bindings. See contributing, the parity contract, and release setup. All language CI now uses the same entry workflow.

introduction

This library gives you an ability to convert any string that contains links notation into a list of links and back to the string after modifications are made.

Links notation is based on two concepts references and links. Each reference references other link. The notation supports links with any number of references to other links.

Quick Start

C#

var parser = new Link.Foundation.Links.Notation.Parser();
var links = parser.Parse("papa (lovesMama: loves mama)");

JavaScript

import { Parser } from "links-notation";
const parser = new Parser();
const links = parser.parse("papa (lovesMama: loves mama)");

Rust

use links_notation::parse_lino;
let links = parse_lino("papa (lovesMama: loves mama)").unwrap();

Python

from links_notation import Parser
parser = Parser()
links = parser.parse("papa (lovesMama: loves mama)")

Go

import lino "github.com/link-foundation/links-notation/go"
links, _ := lino.Parse("papa (lovesMama: loves mama)")

Java

import io.github.linkfoundation.linksnotation.Parser;
Parser parser = new Parser();
List<Link> links = parser.parse("papa (lovesMama: loves mama)");

PHP

use LinkFoundation\LinksNotation\Parser;
$parser = new Parser();
$links = $parser->parse("papa (lovesMama: loves mama)");

C++

#include "links_notation.hpp"
std::vector<links_notation::Link> links = links_notation::parse("papa (lovesMama: loves mama)");

Swift

import LinksNotation
let links = try Parser().parse("papa (lovesMama: loves mama)")

Kotlin

import io.github.linkfoundation.linksnotation.Parser
val links = Parser().parse("papa (lovesMama: loves mama)")

Examples

Links notation (lino)

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

Sequences (N-tuple)

I'm a friendly AI.
(I'm a friendly AI too.)
(linksNotation: links notation)
(This is a linksNotation as well)
(linksNotation supports (unlimited number (of references) in each link))
(sequence (of references) surrounded by parentheses is a link)
parentheses may be ommitted if the whole line is a single link

Indented Syntax

Links can also use indented syntax for better readability:

3:
  papa
  loves
  mama

This is equivalent to:

(3: papa loves mama)

For indented IDs, Parser.parse('x:\n a\n b') reads (x: a b). parse defines the canonical link values. In JavaScript, C++, Swift and Kotlin, parseGroups exposes the indentation separately: the x: element is a self reference with an empty value list, and its children are references a and b. Folding those children into the element's values gives the same link as parse. Explicit (a) remains a one-value link. Under an ordinary head such as x: kind, child lines remain links and parse expands indentation into paths. See the JavaScript folding example.

Multi-line Groups

A parenthesized group opens a nested context: its body starts fresh at indentation level zero and follows the same rules the root document does, so a line break inside parentheses is structure rather than decoration.

value (
  id "1"
  label "one"
)

This reads as (value ((id 1) (label one))) - two children, each a link of its own - and not as one flat list in which the boundary between id and label would be lost. Indentation works inside a group exactly as it does at the root, and a body that stays on a single line still collapses to a single link, so (a b c) is unchanged.

All ten implementations agree on this: each native test suite reads the document above as the shared context case of parity/text-fixtures.json, and experiments/issue-282/parity parses it with the eight earlier ones side by side and fails if any one reads it differently. The complete rules are in the grammar.

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

That is the single link (deploy: staging): both comments are gone by the time the document is read. 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 in every implementation, and every parser can be told to read # as an ordinary character again for documents written before comments existed - see the parser options in the language READMEs.

So that means that this text is also links notation. So most of the text in the world already may be parsed as links notation. That makes links notation the most easy an natural/intuitive/native one.

What is Links Notation?

Links Notation (Lino) is a simple, intuitive format for representing structured data as links between entities references to links. It's designed to be:

  • Natural: Most text can already be parsed as links notation
  • Flexible: Supports any number of references in each link
  • Universal: Can represent doublets, triplets, and N-tuples
  • Hierarchical: Supports nested structures with indentation

The notation uses two core concepts:

  • References: Points to other links (like variables or identifiers)
  • Links: Connect references together with optional identifiers

Streaming

Every maintained implementation can parse arbitrary chunks incrementally, including one-symbol and one-line chunks. A stream emits a top-level record only after its indented children, multiline quoted references, and parenthesized contexts are complete. The canonical parser validates each completed record, so streaming and whole-document parsing have the same syntax and output.

Language Push API Lazy/native adapter
C# LinkParsed event IEnumerable / IAsyncEnumerable
JavaScript link, error, and end events sync / async iterables
Rust link and error callbacks Iterator
Python link callback generator / async generator
Go callback and io.Writer iter.Seq2
Java Consumer<Link> Stream<Link>
PHP callable Generator
C++ on_link / on_error callbacks ChunkedLinks / parse_chunks
Swift onLink / onError handlers Sequence / AsyncSequence
Kotlin onLink, onError and onEnd listeners Sequence via StreamParser.parse

All variants expose write/feed and finish operations, absolute position tracking, reset/drain controls, and an unresolved-record size limit. Collection is enabled by default; disable it when callbacks or lazy iteration should keep memory bounded. See the cross-language parity experiment and each language README for runnable examples.

How a document was written

1 and '1' are the same reference, so equality and formatting treat (t 1 '1') and (t 1 1) alike. The parser still records how each ID was written: every parsed link with an ID carries its raw text ('1') and its quote ('). This metadata is on by default; an AST mode leaves it out. For the whole text, every implementation also returns a lossless concrete syntax tree (CST): each character, comments, whitespace, blank lines and line endings included, belongs to exactly one token, so the tree's text is the document.

Language Raw text and quote AST mode CST
C# RawId, Quote new Parser { Syntax = false } ParseCst
JavaScript rawId, quote new Parser({ syntax: false }) parseCst
Rust parse_lino_to_syntax_links → raw_id(), quote() parse_lino parse_lino_to_cst
Python raw_id, quote Parser(syntax=False) parse_cst
Go RawID, Quote() Parser.Syntax = false ParseCst
Java getRawId(), getQuote() new Parser(comments, false) parseCst
PHP getRawId(), getQuote() new Parser(syntax: false) parseCst
C++ raw_id, quote() ParserOptions::syntax = false parse_cst
Swift rawId, quote ParserOptions(syntax: false) parseCst
Kotlin rawId, quote Parser(ParserOptions(syntax = false)) parseCst

Every implementation checks the shared golden data in parity/syntax-fixtures.json and restores every shared fixture exactly from its CST. See the issue 364 case study.

Documentation

For detailed implementation guides and API references, see the language-specific documentation:

Additional resources:

Test Coverage & Implementation Parity

All ten language implementations (C#, JavaScript, Rust, Python, Go, Java, PHP, C++, Swift, Kotlin) maintain equivalent core functionality, and the overlap is verified test by test rather than asserted:

Language Tests Test categories
Python 279 27
JavaScript 351 34
Rust 430 32
C# 325 30
Go 167 20
Java 214 19
PHP 265 26
C++ 380 32
Swift 349 35
Kotlin 358 33

The table is written by scripts/create-test-case-comparison.mjs, which reads the test files themselves and also produces TEST_CASE_COMPARISON.md - the full matrix of which implementation has which test, each cell linking to the test. The docs workflow runs that script with --check on every pull request, so a test added in one language and forgotten in the others shows up as a gap in the matrix instead of as a README that quietly went stale. It used to be a hand-written list, and had drifted to six languages and six wrong counts.

Known Implementation Differences

Some language-specific features are intentional:

  • LinksGroup - a parsed group of links kept as one object - exists in JavaScript, C#, Java, C++, Swift and Kotlin. Python, Rust, Go and PHP expose the same structure as nested Link values instead.
  • Tuple conversion - building a link from a language tuple - is offered where the language has the syntax for it: C# via implicit operators and Rust via From implementations.

These differences are by design and do not affect core parsing/formatting functionality.

Binary links notation

The version 1 specification defines shared packets and a LiNo document mapping. JavaScript, Python, Rust, C#, Go, Java, PHP, C++, Swift and Kotlin provide codecs beside their text parsers; all check the same golden vectors. See the cross-language example for encoding options and byte-for-byte interoperability.

Exact Unicode identifiers and values round-trip with the reference literal contract introduced in 0.24.0, including mixed quotes, empty strings, controls and multiline text.