links-notation 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- links_notation/__init__.py +14 -0
- links_notation/formatter.py +24 -0
- links_notation/link.py +178 -0
- links_notation/parser.py +350 -0
- links_notation-0.9.0.dist-info/METADATA +187 -0
- links_notation-0.9.0.dist-info/RECORD +8 -0
- links_notation-0.9.0.dist-info/WHEEL +5 -0
- links_notation-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Platform.Protocols.Lino - Python implementation
|
|
3
|
+
|
|
4
|
+
Lino (Links Notation) is a simple, intuitive format for representing
|
|
5
|
+
structured data as links between references.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .link import Link
|
|
9
|
+
from .parser import Parser
|
|
10
|
+
from .formatter import format_links
|
|
11
|
+
|
|
12
|
+
__version__ = "0.7.0"
|
|
13
|
+
|
|
14
|
+
__all__ = ["Link", "Parser", "format_links"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Formatter for Lino notation.
|
|
3
|
+
|
|
4
|
+
Provides utilities for formatting Link objects back into Lino notation strings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
from .link import Link
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_links(links: List[Link], less_parentheses: bool = False) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Format a list of links into Lino notation.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
links: List of Link objects to format
|
|
17
|
+
less_parentheses: If True, omit parentheses where safe
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Formatted string in Lino notation
|
|
21
|
+
"""
|
|
22
|
+
if not links:
|
|
23
|
+
return ''
|
|
24
|
+
return '\n'.join(link.format(less_parentheses) for link in links)
|
links_notation/link.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Link class representing a Lino link with optional ID and values.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Link:
|
|
9
|
+
"""
|
|
10
|
+
Represents a link in Lino notation.
|
|
11
|
+
|
|
12
|
+
A link can be:
|
|
13
|
+
- A simple reference (id only, no values)
|
|
14
|
+
- A link with id and values
|
|
15
|
+
- A link with only values (no id)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, link_id: Optional[str] = None, values: Optional[List['Link']] = None):
|
|
19
|
+
"""
|
|
20
|
+
Initialize a Link.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
link_id: Optional identifier for the link
|
|
24
|
+
values: Optional list of child links
|
|
25
|
+
"""
|
|
26
|
+
self.id = link_id
|
|
27
|
+
self.values = values if values is not None else []
|
|
28
|
+
self._is_from_path_combination = False
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
"""String representation using standard formatting."""
|
|
32
|
+
return self.format(False)
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
"""Developer-friendly representation."""
|
|
36
|
+
return f"Link(id={self.id!r}, values={self.values!r})"
|
|
37
|
+
|
|
38
|
+
def __eq__(self, other) -> bool:
|
|
39
|
+
"""Check equality with another Link."""
|
|
40
|
+
if not isinstance(other, Link):
|
|
41
|
+
return False
|
|
42
|
+
if self.id != other.id:
|
|
43
|
+
return False
|
|
44
|
+
if len(self.values) != len(other.values):
|
|
45
|
+
return False
|
|
46
|
+
return all(v1 == v2 for v1, v2 in zip(self.values, other.values))
|
|
47
|
+
|
|
48
|
+
def get_values_string(self) -> str:
|
|
49
|
+
"""Get formatted string of all values."""
|
|
50
|
+
if not self.values:
|
|
51
|
+
return ''
|
|
52
|
+
return ' '.join(Link.get_value_string(v) for v in self.values)
|
|
53
|
+
|
|
54
|
+
def simplify(self) -> 'Link':
|
|
55
|
+
"""
|
|
56
|
+
Simplify the link structure.
|
|
57
|
+
- If no values, return self
|
|
58
|
+
- If single value, return that value
|
|
59
|
+
- Otherwise return new Link with simplified values
|
|
60
|
+
"""
|
|
61
|
+
if not self.values:
|
|
62
|
+
return self
|
|
63
|
+
elif len(self.values) == 1:
|
|
64
|
+
return self.values[0]
|
|
65
|
+
else:
|
|
66
|
+
new_values = [v.simplify() for v in self.values]
|
|
67
|
+
return Link(self.id, new_values)
|
|
68
|
+
|
|
69
|
+
def combine(self, other: 'Link') -> 'Link':
|
|
70
|
+
"""Combine this link with another to create a compound link."""
|
|
71
|
+
return Link(None, [self, other])
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def get_value_string(value: 'Link') -> str:
|
|
75
|
+
"""Get string representation of a value."""
|
|
76
|
+
return value.to_link_or_id_string()
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def escape_reference(reference: Optional[str]) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Escape a reference string if it contains special characters.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
reference: The reference string to escape
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Escaped reference with quotes if needed
|
|
88
|
+
"""
|
|
89
|
+
if not reference or not reference.strip():
|
|
90
|
+
return ''
|
|
91
|
+
|
|
92
|
+
# Check if single quotes are needed
|
|
93
|
+
needs_single_quotes = any(c in reference for c in [':', '(', ')', ' ', '\t', '\n', '\r', '"'])
|
|
94
|
+
|
|
95
|
+
if needs_single_quotes:
|
|
96
|
+
return f"'{reference}'"
|
|
97
|
+
elif "'" in reference:
|
|
98
|
+
return f'"{reference}"'
|
|
99
|
+
else:
|
|
100
|
+
return reference
|
|
101
|
+
|
|
102
|
+
def to_link_or_id_string(self) -> str:
|
|
103
|
+
"""Convert to string, using just ID if no values, otherwise full format."""
|
|
104
|
+
if not self.values:
|
|
105
|
+
return Link.escape_reference(self.id) if self.id is not None else ''
|
|
106
|
+
return str(self)
|
|
107
|
+
|
|
108
|
+
def format(self, less_parentheses: bool = False, is_compound_value: bool = False) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Format the link as a string.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
less_parentheses: If True, omit parentheses when safe
|
|
114
|
+
is_compound_value: If True, this is a value in a compound link
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Formatted string representation
|
|
118
|
+
"""
|
|
119
|
+
# Empty link
|
|
120
|
+
if self.id is None and not self.values:
|
|
121
|
+
return '' if less_parentheses else '()'
|
|
122
|
+
|
|
123
|
+
# Link with only ID, no values
|
|
124
|
+
if not self.values:
|
|
125
|
+
escaped_id = Link.escape_reference(self.id)
|
|
126
|
+
# When used as a value in a compound link, wrap in parentheses
|
|
127
|
+
if is_compound_value:
|
|
128
|
+
return f'({escaped_id})'
|
|
129
|
+
return escaped_id if (less_parentheses and not self.needs_parentheses(self.id)) else f'({escaped_id})'
|
|
130
|
+
|
|
131
|
+
# Format values recursively
|
|
132
|
+
values_str = ' '.join(self.format_value(v) for v in self.values)
|
|
133
|
+
|
|
134
|
+
# Link with values only (null id)
|
|
135
|
+
if self.id is None:
|
|
136
|
+
if less_parentheses:
|
|
137
|
+
# Check if all values are simple (no nested values)
|
|
138
|
+
all_simple = all(not v.values for v in self.values)
|
|
139
|
+
if all_simple:
|
|
140
|
+
# Format each value without extra wrapping
|
|
141
|
+
return ' '.join(Link.escape_reference(v.id) for v in self.values)
|
|
142
|
+
# For mixed or complex values, return without outer wrapper
|
|
143
|
+
return values_str
|
|
144
|
+
# For normal mode, wrap in parentheses
|
|
145
|
+
return f'({values_str})'
|
|
146
|
+
|
|
147
|
+
# Link with ID and values
|
|
148
|
+
id_str = Link.escape_reference(self.id)
|
|
149
|
+
with_colon = f'{id_str}: {values_str}'
|
|
150
|
+
return with_colon if (less_parentheses and not self.needs_parentheses(self.id)) else f'({with_colon})'
|
|
151
|
+
|
|
152
|
+
def format_value(self, value: 'Link') -> str:
|
|
153
|
+
"""
|
|
154
|
+
Format a single value within this link.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
value: The value link to format
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Formatted string for the value
|
|
161
|
+
"""
|
|
162
|
+
# Check if we're in a compound link from path combinations
|
|
163
|
+
is_compound_from_paths = self._is_from_path_combination
|
|
164
|
+
|
|
165
|
+
# For compound links from paths, format values with parentheses
|
|
166
|
+
if is_compound_from_paths:
|
|
167
|
+
return value.format(False, True)
|
|
168
|
+
|
|
169
|
+
# Simple link with just an ID - don't wrap in parentheses when used as a value
|
|
170
|
+
if not value.values:
|
|
171
|
+
return Link.escape_reference(value.id)
|
|
172
|
+
|
|
173
|
+
# Complex value with its own structure - format it normally with parentheses
|
|
174
|
+
return value.format(False, False)
|
|
175
|
+
|
|
176
|
+
def needs_parentheses(self, s: Optional[str]) -> bool:
|
|
177
|
+
"""Check if a string needs to be wrapped in parentheses."""
|
|
178
|
+
return s and any(c in s for c in [' ', ':', '(', ')'])
|
links_notation/parser.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser for Lino notation.
|
|
3
|
+
|
|
4
|
+
This module provides parsing functionality for Links Notation (Lino),
|
|
5
|
+
converting text into structured Link objects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional, Dict, Any
|
|
9
|
+
from .link import Link
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ParseError(Exception):
|
|
13
|
+
"""Exception raised when parsing fails."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Parser:
|
|
17
|
+
"""
|
|
18
|
+
Parser for Lino notation.
|
|
19
|
+
|
|
20
|
+
Handles both inline and indented syntax for defining links.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize the parser."""
|
|
25
|
+
self.indentation_stack = [0]
|
|
26
|
+
self.pos = 0
|
|
27
|
+
self.text = ""
|
|
28
|
+
self.lines = []
|
|
29
|
+
|
|
30
|
+
def parse(self, input_text: str) -> List[Link]:
|
|
31
|
+
"""
|
|
32
|
+
Parse Lino notation text into a list of Link objects.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
input_text: Text in Lino notation
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of parsed Link objects
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ParseError: If parsing fails
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
if not input_text or not input_text.strip():
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
self.text = input_text
|
|
48
|
+
self.lines = input_text.split('\n')
|
|
49
|
+
self.pos = 0
|
|
50
|
+
self.indentation_stack = [0]
|
|
51
|
+
|
|
52
|
+
raw_result = self._parse_document()
|
|
53
|
+
return self._transform_result(raw_result)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise ParseError(f"Parse error: {str(e)}") from e
|
|
56
|
+
|
|
57
|
+
def _parse_document(self) -> List[Dict]:
|
|
58
|
+
"""Parse the entire document."""
|
|
59
|
+
self.pos = 0
|
|
60
|
+
links = []
|
|
61
|
+
|
|
62
|
+
while self.pos < len(self.lines):
|
|
63
|
+
line = self.lines[self.pos]
|
|
64
|
+
if line.strip(): # Skip empty lines
|
|
65
|
+
element = self._parse_element(0)
|
|
66
|
+
if element:
|
|
67
|
+
links.append(element)
|
|
68
|
+
else:
|
|
69
|
+
self.pos += 1
|
|
70
|
+
|
|
71
|
+
return links
|
|
72
|
+
|
|
73
|
+
def _parse_element(self, current_indent: int) -> Optional[Dict]:
|
|
74
|
+
"""Parse a single element (link or reference) at given indentation."""
|
|
75
|
+
if self.pos >= len(self.lines):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
line = self.lines[self.pos]
|
|
79
|
+
indent = len(line) - len(line.lstrip(' '))
|
|
80
|
+
|
|
81
|
+
if indent < current_indent:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
content = line.strip()
|
|
85
|
+
if not content:
|
|
86
|
+
self.pos += 1
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
self.pos += 1
|
|
90
|
+
|
|
91
|
+
# Try to parse the line
|
|
92
|
+
element = self._parse_line_content(content)
|
|
93
|
+
|
|
94
|
+
# Check for children (indented lines that follow)
|
|
95
|
+
children = []
|
|
96
|
+
child_indent = indent + 2 # Expect at least 2 spaces for child
|
|
97
|
+
|
|
98
|
+
while self.pos < len(self.lines):
|
|
99
|
+
next_line = self.lines[self.pos]
|
|
100
|
+
next_indent = len(next_line) - len(next_line.lstrip(' '))
|
|
101
|
+
|
|
102
|
+
if next_line.strip() and next_indent > indent:
|
|
103
|
+
# This is a child
|
|
104
|
+
child = self._parse_element(child_indent if not children else indent + 2)
|
|
105
|
+
if child:
|
|
106
|
+
children.append(child)
|
|
107
|
+
else:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if children:
|
|
111
|
+
element['children'] = children
|
|
112
|
+
|
|
113
|
+
return element
|
|
114
|
+
|
|
115
|
+
def _parse_line_content(self, content: str) -> Dict:
|
|
116
|
+
"""Parse the content of a single line."""
|
|
117
|
+
# Try multiline link format: (id: values) or (values)
|
|
118
|
+
if content.startswith('(') and content.endswith(')'):
|
|
119
|
+
inner = content[1:-1].strip()
|
|
120
|
+
return self._parse_parenthesized(inner)
|
|
121
|
+
|
|
122
|
+
# Try indented ID syntax: id:
|
|
123
|
+
if content.endswith(':'):
|
|
124
|
+
id_part = content[:-1].strip()
|
|
125
|
+
ref = self._extract_reference(id_part)
|
|
126
|
+
return {'id': ref, 'values': [], 'is_indented_id': True}
|
|
127
|
+
|
|
128
|
+
# Try single-line link: id: values
|
|
129
|
+
if ':' in content and not (content.startswith('"') or content.startswith("'")):
|
|
130
|
+
parts = content.split(':', 1)
|
|
131
|
+
if len(parts) == 2:
|
|
132
|
+
id_part = parts[0].strip()
|
|
133
|
+
values_part = parts[1].strip()
|
|
134
|
+
ref = self._extract_reference(id_part)
|
|
135
|
+
values = self._parse_values(values_part)
|
|
136
|
+
return {'id': ref, 'values': values}
|
|
137
|
+
|
|
138
|
+
# Simple value list
|
|
139
|
+
values = self._parse_values(content)
|
|
140
|
+
return {'values': values}
|
|
141
|
+
|
|
142
|
+
def _parse_parenthesized(self, inner: str) -> Dict:
|
|
143
|
+
"""Parse content within parentheses."""
|
|
144
|
+
# Check for id: values format
|
|
145
|
+
colon_pos = self._find_colon_outside_quotes(inner)
|
|
146
|
+
if colon_pos >= 0:
|
|
147
|
+
id_part = inner[:colon_pos].strip()
|
|
148
|
+
values_part = inner[colon_pos + 1:].strip()
|
|
149
|
+
ref = self._extract_reference(id_part)
|
|
150
|
+
values = self._parse_values(values_part)
|
|
151
|
+
return {'id': ref, 'values': values}
|
|
152
|
+
|
|
153
|
+
# Just values
|
|
154
|
+
values = self._parse_values(inner)
|
|
155
|
+
return {'values': values}
|
|
156
|
+
|
|
157
|
+
def _find_colon_outside_quotes(self, text: str) -> int:
|
|
158
|
+
"""Find the position of a colon that's not inside quotes."""
|
|
159
|
+
in_single = False
|
|
160
|
+
in_double = False
|
|
161
|
+
|
|
162
|
+
for i, char in enumerate(text):
|
|
163
|
+
if char == "'" and not in_double:
|
|
164
|
+
in_single = not in_single
|
|
165
|
+
elif char == '"' and not in_single:
|
|
166
|
+
in_double = not in_double
|
|
167
|
+
elif char == ':' and not in_single and not in_double:
|
|
168
|
+
return i
|
|
169
|
+
|
|
170
|
+
return -1
|
|
171
|
+
|
|
172
|
+
def _parse_values(self, text: str) -> List[Dict]:
|
|
173
|
+
"""Parse a space-separated list of values."""
|
|
174
|
+
if not text:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
values = []
|
|
178
|
+
current = ""
|
|
179
|
+
in_single = False
|
|
180
|
+
in_double = False
|
|
181
|
+
paren_depth = 0
|
|
182
|
+
|
|
183
|
+
i = 0
|
|
184
|
+
while i < len(text):
|
|
185
|
+
char = text[i]
|
|
186
|
+
|
|
187
|
+
if char == "'" and not in_double:
|
|
188
|
+
in_single = not in_single
|
|
189
|
+
current += char
|
|
190
|
+
elif char == '"' and not in_single:
|
|
191
|
+
in_double = not in_double
|
|
192
|
+
current += char
|
|
193
|
+
elif char == '(' and not in_single and not in_double:
|
|
194
|
+
paren_depth += 1
|
|
195
|
+
current += char
|
|
196
|
+
elif char == ')' and not in_single and not in_double:
|
|
197
|
+
paren_depth -= 1
|
|
198
|
+
current += char
|
|
199
|
+
elif char == ' ' and not in_single and not in_double and paren_depth == 0:
|
|
200
|
+
# End of current value
|
|
201
|
+
if current.strip():
|
|
202
|
+
values.append(self._parse_value(current.strip()))
|
|
203
|
+
current = ""
|
|
204
|
+
else:
|
|
205
|
+
current += char
|
|
206
|
+
|
|
207
|
+
i += 1
|
|
208
|
+
|
|
209
|
+
# Add last value
|
|
210
|
+
if current.strip():
|
|
211
|
+
values.append(self._parse_value(current.strip()))
|
|
212
|
+
|
|
213
|
+
return values
|
|
214
|
+
|
|
215
|
+
def _parse_value(self, value: str) -> Dict:
|
|
216
|
+
"""Parse a single value (could be a reference or nested link)."""
|
|
217
|
+
# Nested link in parentheses
|
|
218
|
+
if value.startswith('(') and value.endswith(')'):
|
|
219
|
+
inner = value[1:-1].strip()
|
|
220
|
+
return self._parse_parenthesized(inner)
|
|
221
|
+
|
|
222
|
+
# Simple reference
|
|
223
|
+
ref = self._extract_reference(value)
|
|
224
|
+
return {'id': ref}
|
|
225
|
+
|
|
226
|
+
def _extract_reference(self, text: str) -> str:
|
|
227
|
+
"""Extract reference, handling quoted strings."""
|
|
228
|
+
text = text.strip()
|
|
229
|
+
|
|
230
|
+
# Double quoted
|
|
231
|
+
if text.startswith('"') and text.endswith('"'):
|
|
232
|
+
return text[1:-1]
|
|
233
|
+
|
|
234
|
+
# Single quoted
|
|
235
|
+
if text.startswith("'") and text.endswith("'"):
|
|
236
|
+
return text[1:-1]
|
|
237
|
+
|
|
238
|
+
# Unquoted
|
|
239
|
+
return text
|
|
240
|
+
|
|
241
|
+
def _transform_result(self, raw_result: List[Dict]) -> List[Link]:
|
|
242
|
+
"""Transform raw parse result into Link objects."""
|
|
243
|
+
links = []
|
|
244
|
+
|
|
245
|
+
for item in raw_result:
|
|
246
|
+
if item:
|
|
247
|
+
self._collect_links(item, [], links)
|
|
248
|
+
|
|
249
|
+
return links
|
|
250
|
+
|
|
251
|
+
def _collect_links(self, item: Dict, parent_path: List[Link], result: List[Link]) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Recursively collect links from parse tree.
|
|
254
|
+
|
|
255
|
+
Handles both inline and indented syntax, flattening the hierarchy
|
|
256
|
+
appropriately.
|
|
257
|
+
"""
|
|
258
|
+
if not item:
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
children = item.get('children', [])
|
|
262
|
+
|
|
263
|
+
# Special case: indented ID syntax (id: followed by children)
|
|
264
|
+
if item.get('is_indented_id') and item.get('id') and not item.get('values') and children:
|
|
265
|
+
child_values = []
|
|
266
|
+
for child in children:
|
|
267
|
+
# Extract the reference from child's values
|
|
268
|
+
if child.get('values') and len(child['values']) == 1:
|
|
269
|
+
child_values.append(self._transform_link(child['values'][0]))
|
|
270
|
+
else:
|
|
271
|
+
child_values.append(self._transform_link(child))
|
|
272
|
+
|
|
273
|
+
link_with_children = {
|
|
274
|
+
'id': item['id'],
|
|
275
|
+
'values': child_values
|
|
276
|
+
}
|
|
277
|
+
current_link = self._transform_link(link_with_children)
|
|
278
|
+
|
|
279
|
+
if not parent_path:
|
|
280
|
+
result.append(current_link)
|
|
281
|
+
else:
|
|
282
|
+
result.append(self._combine_path_elements(parent_path, current_link))
|
|
283
|
+
|
|
284
|
+
# Regular indented structure
|
|
285
|
+
elif children:
|
|
286
|
+
current_link = self._transform_link(item)
|
|
287
|
+
|
|
288
|
+
# Add the link combined with parent path
|
|
289
|
+
if not parent_path:
|
|
290
|
+
result.append(current_link)
|
|
291
|
+
else:
|
|
292
|
+
result.append(self._combine_path_elements(parent_path, current_link))
|
|
293
|
+
|
|
294
|
+
# Process each child with this item in the path
|
|
295
|
+
new_path = parent_path + [current_link]
|
|
296
|
+
|
|
297
|
+
for child in children:
|
|
298
|
+
self._collect_links(child, new_path, result)
|
|
299
|
+
|
|
300
|
+
# Leaf item or item with inline values
|
|
301
|
+
else:
|
|
302
|
+
current_link = self._transform_link(item)
|
|
303
|
+
|
|
304
|
+
if not parent_path:
|
|
305
|
+
result.append(current_link)
|
|
306
|
+
else:
|
|
307
|
+
result.append(self._combine_path_elements(parent_path, current_link))
|
|
308
|
+
|
|
309
|
+
def _combine_path_elements(self, path_elements: List[Link], current: Link) -> Link:
|
|
310
|
+
"""Combine path elements into a single link."""
|
|
311
|
+
if not path_elements:
|
|
312
|
+
return current
|
|
313
|
+
|
|
314
|
+
if len(path_elements) == 1:
|
|
315
|
+
combined = Link(None, [path_elements[0], current])
|
|
316
|
+
combined._is_from_path_combination = True
|
|
317
|
+
return combined
|
|
318
|
+
|
|
319
|
+
# For multiple path elements, build proper nesting
|
|
320
|
+
parent_path = path_elements[:-1]
|
|
321
|
+
last_element = path_elements[-1]
|
|
322
|
+
|
|
323
|
+
# Build the parent structure
|
|
324
|
+
parent = self._combine_path_elements(parent_path, last_element)
|
|
325
|
+
|
|
326
|
+
# Add current element to the built structure
|
|
327
|
+
combined = Link(None, [parent, current])
|
|
328
|
+
combined._is_from_path_combination = True
|
|
329
|
+
return combined
|
|
330
|
+
|
|
331
|
+
def _transform_link(self, item: Any) -> Link:
|
|
332
|
+
"""Transform a parsed item into a Link object."""
|
|
333
|
+
if isinstance(item, Link):
|
|
334
|
+
return item
|
|
335
|
+
|
|
336
|
+
if not isinstance(item, dict):
|
|
337
|
+
return Link(str(item))
|
|
338
|
+
|
|
339
|
+
# Simple reference
|
|
340
|
+
if 'id' in item and 'values' not in item:
|
|
341
|
+
return Link(item['id'])
|
|
342
|
+
|
|
343
|
+
# Link with values
|
|
344
|
+
if 'values' in item:
|
|
345
|
+
link_id = item.get('id')
|
|
346
|
+
values = [self._transform_link(v) for v in item['values']]
|
|
347
|
+
return Link(link_id, values)
|
|
348
|
+
|
|
349
|
+
# Default
|
|
350
|
+
return Link(item.get('id'))
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: links-notation
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Python implementation of the Lino protocol parser
|
|
5
|
+
Author-email: LinksPlatform <noreply@linksplatform.com>
|
|
6
|
+
License: Unlicense
|
|
7
|
+
Project-URL: Homepage, https://github.com/link-foundation/links-notation
|
|
8
|
+
Project-URL: Repository, https://github.com/link-foundation/links-notation
|
|
9
|
+
Project-URL: Issues, https://github.com/link-foundation/links-notation/issues
|
|
10
|
+
Keywords: lino,parser,links,notation,protocol
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: Public Domain
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Text Processing
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Platform.Protocols.Lino - Python
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/platform-lino/)
|
|
28
|
+
[](https://pypi.org/project/platform-lino/)
|
|
29
|
+
[](../LICENSE)
|
|
30
|
+
|
|
31
|
+
Python implementation of the Lino (Links Notation) protocol parser.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install platform-lino
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from platform_lino import Parser
|
|
43
|
+
|
|
44
|
+
parser = Parser()
|
|
45
|
+
links = parser.parse("papa (lovesMama: loves mama)")
|
|
46
|
+
|
|
47
|
+
# Access parsed links
|
|
48
|
+
for link in links:
|
|
49
|
+
print(link)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Basic Parsing
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from platform_lino import Parser, format_links
|
|
58
|
+
|
|
59
|
+
parser = Parser()
|
|
60
|
+
|
|
61
|
+
# Parse simple links
|
|
62
|
+
links = parser.parse("(papa: loves mama)")
|
|
63
|
+
print(links[0].id) # 'papa'
|
|
64
|
+
print(len(links[0].values)) # 2
|
|
65
|
+
|
|
66
|
+
# Format links back to string
|
|
67
|
+
output = format_links(links)
|
|
68
|
+
print(output) # (papa: loves mama)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Working with Link Objects
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from platform_lino import Link
|
|
75
|
+
|
|
76
|
+
# Create links programmatically
|
|
77
|
+
link = Link('parent', [Link('child1'), Link('child2')])
|
|
78
|
+
print(str(link)) # (parent: child1 child2)
|
|
79
|
+
|
|
80
|
+
# Access link properties
|
|
81
|
+
print(link.id) # 'parent'
|
|
82
|
+
print(link.values[0].id) # 'child1'
|
|
83
|
+
|
|
84
|
+
# Combine links
|
|
85
|
+
combined = link.combine(Link('another'))
|
|
86
|
+
print(str(combined)) # ((parent: child1 child2) another)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Indented Syntax
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
parser = Parser()
|
|
93
|
+
|
|
94
|
+
# Parse indented notation
|
|
95
|
+
text = """3:
|
|
96
|
+
papa
|
|
97
|
+
loves
|
|
98
|
+
mama"""
|
|
99
|
+
|
|
100
|
+
links = parser.parse(text)
|
|
101
|
+
# Produces: (3: papa loves mama)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API Reference
|
|
105
|
+
|
|
106
|
+
### Parser
|
|
107
|
+
|
|
108
|
+
The main parser class for Lino notation.
|
|
109
|
+
|
|
110
|
+
- `parse(input_text: str) -> List[Link]`: Parse Lino text into Link objects
|
|
111
|
+
|
|
112
|
+
### Link
|
|
113
|
+
|
|
114
|
+
Represents a link in Lino notation.
|
|
115
|
+
|
|
116
|
+
- `__init__(id: Optional[str] = None, values: Optional[List[Link]] = None)`
|
|
117
|
+
- `format(less_parentheses: bool = False) -> str`: Format as string
|
|
118
|
+
- `simplify() -> Link`: Simplify link structure
|
|
119
|
+
- `combine(other: Link) -> Link`: Combine with another link
|
|
120
|
+
|
|
121
|
+
### format_links
|
|
122
|
+
|
|
123
|
+
Format a list of links into Lino notation.
|
|
124
|
+
|
|
125
|
+
- `format_links(links: List[Link], less_parentheses: bool = False) -> str`
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
### Doublets (2-tuple)
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
parser = Parser()
|
|
133
|
+
text = """
|
|
134
|
+
papa (lovesMama: loves mama)
|
|
135
|
+
son lovesMama
|
|
136
|
+
daughter lovesMama
|
|
137
|
+
"""
|
|
138
|
+
links = parser.parse(text)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Triplets (3-tuple)
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
text = """
|
|
145
|
+
papa has car
|
|
146
|
+
mama has house
|
|
147
|
+
(papa and mama) are happy
|
|
148
|
+
"""
|
|
149
|
+
links = parser.parse(text)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Quoted References
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# References with special characters need quotes
|
|
156
|
+
text = '("has space": "value with: colon")'
|
|
157
|
+
links = parser.parse(text)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
### Running Tests
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Install development dependencies
|
|
166
|
+
pip install pytest
|
|
167
|
+
|
|
168
|
+
# Run tests
|
|
169
|
+
pytest
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Building
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install build
|
|
176
|
+
python -m build
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
This project is released into the public domain under the [Unlicense](../LICENSE).
|
|
182
|
+
|
|
183
|
+
## Links
|
|
184
|
+
|
|
185
|
+
- [Main Repository](https://github.com/link-foundation/links-notation)
|
|
186
|
+
- [PyPI Package](https://pypi.org/project/platform-lino/)
|
|
187
|
+
- [Documentation](https://link-foundation.github.io/links-notation/)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
links_notation/__init__.py,sha256=PYY7mi-4h7kb-J7skq0WejhadC3-3oPTCy55_kMshTE,327
|
|
2
|
+
links_notation/formatter.py,sha256=yj_xgdaszHslJxTnuq2WSpKpsSruhez9zn6pO5QcZjo,589
|
|
3
|
+
links_notation/link.py,sha256=JjVDBEzOT8RRWXLbtl39DDMqqQCETWuHbPm_h5M3IiA,6193
|
|
4
|
+
links_notation/parser.py,sha256=O_mhTS9vjYP6CYfh2fuRDLSFFyBauMSHt4ujZbIfrcI,11252
|
|
5
|
+
links_notation-0.9.0.dist-info/METADATA,sha256=ep2_nwnU2mXU_pQGQQo_VzZHlVzs1Dtpi5CU559TTH8,4199
|
|
6
|
+
links_notation-0.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
links_notation-0.9.0.dist-info/top_level.txt,sha256=KyOejsXELNXBmuP4E4qBB8bRmuQWi4iwfpvprt2J8ZI,15
|
|
8
|
+
links_notation-0.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
links_notation
|