Python implementation of the Links Notation parser.
pip install links-notationfrom links_notation import Parser
parser = Parser()
links = parser.parse("papa (lovesMama: loves mama)")
# Access parsed links
for link in links:
print(link)from links_notation import Parser, format_links
parser = Parser()
# Parse simple links
links = parser.parse("(papa: loves mama)")
print(links[0].id) # 'papa'
print(len(links[0].values)) # 2
# Format links back to string
output = format_links(links)
print(output) # (papa: loves mama)from links_notation import Link
# Create links programmatically
link = Link('parent', [Link('child1'), Link('child2')])
print(str(link)) # (parent: child1 child2)
# Access link properties
print(link.id) # 'parent'
print(link.values[0].id) # 'child1'
# Combine links
combined = link.combine(Link('another'))
print(str(combined)) # ((parent: child1 child2) another)parser = Parser()
# Parse indented notation
text = """3:
papa
loves
mama"""
links = parser.parse(text)
# Produces: (3: papa loves mama)StreamParser accepts arbitrary chunks and emits only complete top-level
records. Set collect=False for bounded-memory callback or generator use.
from links_notation import StreamParser, parse_chunks
stream = StreamParser(on_link=print, collect=False)
stream.write("profile:\n name Ada\n")
stream.finish("next link")
for link in parse_chunks(["one li", "nk\ntwo link"]):
print(link)parse_async_chunks provides the equivalent native async generator. The
stream also exposes position, drain(), reset(), and a buffer-size limit.
The main parser class for Links Notation.
__init__(max_input_size: int = 10 * 1024 * 1024, max_depth: int = DEFAULT_MAX_DEPTH, comments: bool = True): Create a parser; withcomments=Falsea#is an ordinary character instead of the start of a commentparse(input_text: str) -> List[Link]: Parse Links Notation text into Link objects; raisesParseErrorwhen the text does not parse or nests links deeper thanmax_depthDEFAULT_MAX_DEPTH: The defaultmax_depth, 64, the same in every implementation
max_depth is how deep links may nest (default: 64). Every parenthesized group
and every indentation level is one level, and the lines of a document start at
level 0, so with max_depth=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 ParseError rather than recursed into until Python's recursion limit is hit.
Raised when parsing fails. When a document nests links deeper than
max_depth, it 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))))
| ^
max_depth: The deepest nesting the parser accepts, when the document nests deeper;Nonefor any other errorline,column: Where the offending group or line starts, counted from 1offset: The same position as a character offset from the start of the documentline_text: The offending line, as written
StreamParser reports the same error as a StreamParseError whose error is
the ParseError and whose line, column and offset are counted from the
start of the stream.
Represents a link in Links Notation.
__init__(id: Optional[str] = None, values: Optional[List[Link]] = None)format(less_parentheses: bool = False) -> str: Format as stringsimplify() -> Link: Simplify link structurecombine(other: Link) -> Link: Combine with another link
Format a list of links into Links Notation.
format_links(links: List[Link], less_parentheses: bool = False) -> str
parser = Parser()
text = """
papa (lovesMama: loves mama)
son lovesMama
daughter lovesMama
"""
links = parser.parse(text)text = """
papa has car
mama has house
(papa and mama) are happy
"""
links = parser.parse(text)# References with special characters need quotes
text = '("has space": "value with: colon")'
links = parser.parse(text)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.
text = """value (
id "1"
label "one"
)"""
print(format_links(parser.parse(text)))
# (value ((id 1) (label one)))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:
document = "# the machines this deploys to\ndeploy: staging # only staging, for now\n"
print(format_links(Parser().parse(document))) # (deploy: staging)
print(format_links(Parser(comments=False).parse("# a b\n"))) # (# a b)# Install development dependencies
pip install pytest
# Run tests
pytestpip install build
python -m buildThis project uses Black for code formatting, isort for import sorting, and flake8 for linting.
Install linting tools:
pip install ".[lint]"
# Or install all dev dependencies
pip install ".[dev]"black .
isort .black --check --diff .
isort --check-only --diff .
flake8 --max-line-length=120These checks are enforced in CI. Pull requests with unformatted code will fail the lint check.
This project uses pre-commit hooks that automatically run Black, isort, and flake8 before commits. To set up pre-commit hooks locally:
# From repository root
pip install pre-commit
pre-commit installThis project is released into the public domain under the Unlicense.
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.