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/README.md +219 -0
- package/README.ru.md +193 -0
- package/bun.lock +33 -0
- package/dist/index.js +1462 -0
- package/package.json +26 -0
- package/src/Link.js +155 -0
- package/src/LinksGroup.js +30 -0
- package/src/Parser.js +132 -0
- package/src/grammar.pegjs +86 -0
- package/src/index.js +3 -0
- package/src/parser-generated.js +1322 -0
- package/tests/ApiTests.test.js +74 -0
- package/tests/EdgeCaseParser.test.js +162 -0
- package/tests/IndentedIdSyntax.test.js +171 -0
- package/tests/Link.test.js +69 -0
- package/tests/LinksGroup.test.js +40 -0
- package/tests/MixedIndentationModes.test.js +188 -0
- package/tests/MultilineParser.test.js +118 -0
- package/tests/MultilineQuotedString.test.js +78 -0
- package/tests/NestedParser.test.js +166 -0
- package/tests/SingleLineParser.test.js +229 -0
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