links-notation 0.8.0

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.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "links-notation",
3
+ "version": "0.8.0",
4
+ "description": "Lino protocol parser for JavaScript",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "bun run build:grammar && bun build src/index.js --outdir=dist --target=node",
9
+ "build:grammar": "peggy --format es -o src/parser-generated.js src/grammar.pegjs",
10
+ "test": "bun test",
11
+ "test:watch": "bun test --watch"
12
+ },
13
+ "keywords": [
14
+ "lino",
15
+ "parser",
16
+ "protocol",
17
+ "links"
18
+ ],
19
+ "author": "",
20
+ "license": "Unlicense",
21
+ "devDependencies": {
22
+ "bun-types": "^1.2.19",
23
+ "peggy": "^5.0.6"
24
+ },
25
+ "dependencies": {}
26
+ }
package/src/Link.js ADDED
@@ -0,0 +1,155 @@
1
+ export class Link {
2
+ constructor(id = null, values = null) {
3
+ this.id = id;
4
+ this.values = values || [];
5
+ }
6
+
7
+ toString() {
8
+ return this.format(false);
9
+ }
10
+
11
+ getValuesString() {
12
+ return (!this.values || this.values.length === 0) ?
13
+ '' : this.values.map(v => Link.getValueString(v)).join(' ');
14
+ }
15
+
16
+ simplify() {
17
+ if (!this.values || this.values.length === 0) {
18
+ return this;
19
+ } else if (this.values.length === 1) {
20
+ return this.values[0];
21
+ } else {
22
+ const newValues = this.values.map(v => v.simplify());
23
+ return new Link(this.id, newValues);
24
+ }
25
+ }
26
+
27
+ combine(other) {
28
+ return new Link(null, [this, other]);
29
+ }
30
+
31
+ static getValueString(value) {
32
+ return value.toLinkOrIdString();
33
+ }
34
+
35
+ static escapeReference(reference) {
36
+ if (!reference || reference.trim() === '') {
37
+ return '';
38
+ }
39
+
40
+ const needsSingleQuotes =
41
+ reference.includes(':') ||
42
+ reference.includes('(') ||
43
+ reference.includes(')') ||
44
+ reference.includes(' ') ||
45
+ reference.includes('\t') ||
46
+ reference.includes('\n') ||
47
+ reference.includes('\r') ||
48
+ reference.includes('"');
49
+
50
+ if (needsSingleQuotes) {
51
+ return `'${reference}'`;
52
+ } else if (reference.includes("'")) {
53
+ return `"${reference}"`;
54
+ } else {
55
+ return reference;
56
+ }
57
+ }
58
+
59
+ toLinkOrIdString() {
60
+ if (!this.values || this.values.length === 0) {
61
+ return this.id === null ? '' : Link.escapeReference(this.id);
62
+ }
63
+ return this.toString();
64
+ }
65
+
66
+ equals(other) {
67
+ if (!(other instanceof Link)) return false;
68
+ if (this.id !== other.id) return false;
69
+ if (this.values.length !== other.values.length) return false;
70
+
71
+ for (let i = 0; i < this.values.length; i++) {
72
+ if (!this.values[i].equals(other.values[i])) {
73
+ return false;
74
+ }
75
+ }
76
+ return true;
77
+ }
78
+
79
+
80
+ format(lessParentheses = false, isCompoundValue = false) {
81
+ // Empty link
82
+ if (this.id === null && (!this.values || this.values.length === 0)) {
83
+ return lessParentheses ? '' : '()';
84
+ }
85
+
86
+ // Link with only ID, no values
87
+ if (!this.values || this.values.length === 0) {
88
+ const escapedId = Link.escapeReference(this.id);
89
+ // When used as a value in a compound link (created from combining links), wrap in parentheses
90
+ if (isCompoundValue) {
91
+ return `(${escapedId})`;
92
+ }
93
+ return lessParentheses && !this.needsParentheses(this.id) ? escapedId : `(${escapedId})`;
94
+ }
95
+
96
+ // Format values recursively
97
+ const valuesStr = this.values.map(v => this.formatValue(v)).join(' ');
98
+
99
+ // Link with values only (null id)
100
+ if (this.id === null) {
101
+ // For lessParentheses mode with simple values, don't wrap the whole thing
102
+ if (lessParentheses) {
103
+ // Check if all values are simple (no nested values)
104
+ const allSimple = this.values.every(v => !v.values || v.values.length === 0);
105
+ if (allSimple) {
106
+ // Format each value without extra wrapping
107
+ const simpleValuesStr = this.values.map(v => Link.escapeReference(v.id)).join(' ');
108
+ return simpleValuesStr;
109
+ }
110
+ // For mixed or complex values in lessParentheses mode, still avoid outer wrapper
111
+ // but keep the inner formatting
112
+ return valuesStr;
113
+ }
114
+ // For normal mode, wrap in parentheses
115
+ return `(${valuesStr})`;
116
+ }
117
+
118
+ // Link with ID and values
119
+ const idStr = Link.escapeReference(this.id);
120
+ const withColon = `${idStr}: ${valuesStr}`;
121
+ return lessParentheses && !this.needsParentheses(this.id) ? withColon : `(${withColon})`;
122
+ }
123
+
124
+ formatValue(value) {
125
+ if (!value.format) {
126
+ return Link.escapeReference(value.id || '');
127
+ }
128
+
129
+ // Check if we're in a compound link that was created from path combinations
130
+ // This is indicated by having a parent context passed through
131
+ const isCompoundFromPaths = this._isFromPathCombination === true;
132
+
133
+ // For compound links from paths, format values with parentheses
134
+ if (isCompoundFromPaths) {
135
+ return value.format(false, true);
136
+ }
137
+
138
+ // Simple link with just an ID - don't wrap in parentheses when used as a value
139
+ if (!value.values || value.values.length === 0) {
140
+ return Link.escapeReference(value.id);
141
+ }
142
+
143
+ // Complex value with its own structure - format it normally with parentheses
144
+ return value.format(false, false);
145
+ }
146
+
147
+ needsParentheses(str) {
148
+ return str && (str.includes(' ') || str.includes(':') || str.includes('(') || str.includes(')'));
149
+ }
150
+ }
151
+
152
+ export function formatLinks(links, lessParentheses = false) {
153
+ if (!links || links.length === 0) return '';
154
+ return links.map(link => link.format(lessParentheses)).join('\n');
155
+ }
@@ -0,0 +1,30 @@
1
+ export class LinksGroup {
2
+ constructor(element, children = []) {
3
+ this.element = element;
4
+ this.children = children;
5
+ }
6
+
7
+ toList() {
8
+ const result = [];
9
+ this._appendToList(result);
10
+ return result;
11
+ }
12
+
13
+ _appendToList(list) {
14
+ list.push(this.element);
15
+ if (this.children && this.children.length > 0) {
16
+ for (const child of this.children) {
17
+ if (child instanceof LinksGroup) {
18
+ child._appendToList(list);
19
+ } else {
20
+ list.push(child);
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ toString() {
27
+ const list = this.toList();
28
+ return list.map(item => `(${item.id || item})`).join(' ');
29
+ }
30
+ }
package/src/Parser.js ADDED
@@ -0,0 +1,132 @@
1
+ import { Link } from './Link.js';
2
+ import * as parserModule from './parser-generated.js';
3
+
4
+ export class Parser {
5
+ constructor() {
6
+ }
7
+
8
+ parse(input) {
9
+ try {
10
+ const rawResult = parserModule.parse(input);
11
+ return this.transformResult(rawResult);
12
+ } catch (error) {
13
+ throw new Error(`Parse error: ${error.message}`);
14
+ }
15
+ }
16
+
17
+ transformResult(rawResult) {
18
+ const links = [];
19
+ const items = Array.isArray(rawResult) ? rawResult : [rawResult];
20
+
21
+ for (const item of items) {
22
+ if (item) {
23
+ this.collectLinks(item, [], links);
24
+ }
25
+ }
26
+ return links;
27
+ }
28
+
29
+ collectLinks(item, parentPath, result) {
30
+ if (!item) return;
31
+
32
+ // For items with children (indented structure)
33
+ if (item.children && item.children.length > 0) {
34
+ // Special case: If this is an ID with empty values but has children,
35
+ // the children should become the values of the link (indented ID syntax)
36
+ if (item.id && (!item.values || item.values.length === 0)) {
37
+ const childValues = item.children.map((child) => {
38
+ // For indented children, extract the actual reference from the child's values
39
+ if (child.values && child.values.length === 1) {
40
+ return this.transformLink(child.values[0]);
41
+ }
42
+ return this.transformLink(child);
43
+ });
44
+ const linkWithChildren = {
45
+ id: item.id,
46
+ values: childValues
47
+ };
48
+ const currentLink = this.transformLink(linkWithChildren);
49
+
50
+ if (parentPath.length === 0) {
51
+ result.push(currentLink);
52
+ } else {
53
+ result.push(this.combinePathElements(parentPath, currentLink));
54
+ }
55
+ } else {
56
+ // Regular indented structure - process as before
57
+ const currentLink = this.transformLink(item);
58
+
59
+ // Add the link combined with parent path
60
+ if (parentPath.length === 0) {
61
+ result.push(currentLink);
62
+ } else {
63
+ result.push(this.combinePathElements(parentPath, currentLink));
64
+ }
65
+
66
+ // Process each child with this item in the path
67
+ const newPath = [...parentPath, currentLink];
68
+
69
+ for (const child of item.children) {
70
+ this.collectLinks(child, newPath, result);
71
+ }
72
+ }
73
+ } else {
74
+ // Leaf item or item with inline values
75
+ const currentLink = this.transformLink(item);
76
+
77
+ if (parentPath.length === 0) {
78
+ result.push(currentLink);
79
+ } else {
80
+ result.push(this.combinePathElements(parentPath, currentLink));
81
+ }
82
+ }
83
+ }
84
+
85
+ combinePathElements(pathElements, current) {
86
+ if (pathElements.length === 0) return current;
87
+ if (pathElements.length === 1) {
88
+ const combined = new Link(null, [pathElements[0], current]);
89
+ combined._isFromPathCombination = true;
90
+ return combined;
91
+ }
92
+
93
+ // For multiple path elements, we need to build proper nesting
94
+ // The last element in the path should be combined with its parent
95
+ const parentPath = pathElements.slice(0, -1);
96
+ const lastElement = pathElements[pathElements.length - 1];
97
+
98
+ // Build the parent structure
99
+ let parent = this.combinePathElements(parentPath, lastElement);
100
+
101
+ // Add current element to the built structure
102
+ const combined = new Link(null, [parent, current]);
103
+ combined._isFromPathCombination = true;
104
+ return combined;
105
+ }
106
+
107
+
108
+ transformLink(item) {
109
+ if (!item) return null;
110
+
111
+ if (item instanceof Link) {
112
+ return item;
113
+ }
114
+
115
+ // Handle simple reference objects like {id: 'a'}
116
+ if (item.id !== undefined && !item.values && !item.children) {
117
+ return new Link(item.id);
118
+ }
119
+
120
+ // For items with values, create a link with those values
121
+ if (item.values && Array.isArray(item.values)) {
122
+ // Create a link with id (if present) and transformed values
123
+ const link = new Link(item.id || null, []);
124
+ link.values = item.values.map(v => this.transformLink(v));
125
+ return link;
126
+ }
127
+
128
+ // Default case
129
+ return new Link(item.id || null, []);
130
+ }
131
+
132
+ }
@@ -0,0 +1,86 @@
1
+ {
2
+ let indentationStack = [0];
3
+
4
+ function pushIndentation(spaces) {
5
+ indentationStack.push(spaces.length);
6
+ }
7
+
8
+ function popIndentation() {
9
+ if (indentationStack.length > 1) {
10
+ indentationStack.pop();
11
+ }
12
+ }
13
+
14
+ function checkIndentation(spaces) {
15
+ return spaces.length >= indentationStack[indentationStack.length - 1];
16
+ }
17
+
18
+ function getCurrentIndentation() {
19
+ return indentationStack[indentationStack.length - 1];
20
+ }
21
+ }
22
+
23
+ document = _ links:links eof { return links; }
24
+ / _ eof { return []; }
25
+
26
+ links = fl:firstLine list:line* { popIndentation(); return [fl].concat(list || []); }
27
+
28
+ firstLine = l:element { return l; }
29
+
30
+ line = CHECK_INDENTATION l:element { return l; }
31
+
32
+ element = e:anyLink PUSH_INDENTATION l:links {
33
+ return { id: e.id, values: e.values, children: l };
34
+ }
35
+ / e:anyLink { return e; }
36
+
37
+ referenceOrLink = l:multiLineAnyLink { return l; } / i:reference { return { id: i }; }
38
+
39
+ anyLink = ml:multiLineAnyLink eol { return ml; } / il:indentedIdLink { return il; } / sl:singleLineAnyLink { return sl; }
40
+
41
+ multiLineAnyLink = multiLineValueLink / multiLineLink
42
+
43
+ singleLineAnyLink = fl:singleLineLink eol { return fl; }
44
+ / vl:singleLineValueLink eol { return vl; }
45
+
46
+ multiLineValueAndWhitespace = value:referenceOrLink _ { return value; }
47
+
48
+ multiLineValues = _ list:multiLineValueAndWhitespace* { return list; }
49
+
50
+ singleLineValueAndWhitespace = __ value:referenceOrLink { return value; }
51
+
52
+ singleLineValues = list:singleLineValueAndWhitespace+ { return list; }
53
+
54
+ singleLineLink = __ id:reference __ ":" v:singleLineValues { return { id: id, values: v }; }
55
+
56
+ multiLineLink = "(" _ id:reference _ ":" v:multiLineValues _ ")" { return { id: id, values: v }; }
57
+
58
+ singleLineValueLink = v:singleLineValues { return { values: v }; }
59
+
60
+ multiLineValueLink = "(" v:multiLineValues _ ")" { return { values: v }; }
61
+
62
+ indentedIdLink = id:reference __ ":" eol { return { id: id, values: [] }; }
63
+
64
+ reference = doubleQuotedReference / singleQuotedReference / simpleReference
65
+
66
+ simpleReference = chars:referenceSymbol+ { return chars.join(''); }
67
+
68
+ doubleQuotedReference = '"' r:[^"]+ '"' { return r.join(''); }
69
+
70
+ singleQuotedReference = "'" r:[^']+ "'" { return r.join(''); }
71
+
72
+ PUSH_INDENTATION = spaces:" "* &{ return spaces.length > getCurrentIndentation(); } { pushIndentation(spaces); }
73
+
74
+ CHECK_INDENTATION = spaces:" "* &{ return checkIndentation(spaces); }
75
+
76
+ eol = __ ([\r\n]+ / eof)
77
+
78
+ eof = !.
79
+
80
+ __ = [ \t]*
81
+
82
+ _ = whiteSpaceSymbol*
83
+
84
+ whiteSpaceSymbol = [ \t\n\r]
85
+
86
+ referenceSymbol = [^ \t\n\r(:)]
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { Link, formatLinks } from './Link.js';
2
+ export { LinksGroup } from './LinksGroup.js';
3
+ export { Parser } from './Parser.js';