links-notation 0.30.0 → 0.30.2
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 +34 -0
- package/README.ru.md +10 -0
- package/dist/index.js +12 -7
- package/index.d.ts +5 -1
- package/package.json +1 -1
- package/src/Link.js +18 -5
- package/src/Parser.js +13 -3
- package/tests/IndentedIdGroups.test.js +69 -0
- package/tests/SharedTextParity.test.js +53 -1
- package/tests/SyntaxMetadata.test.js +1 -1
package/README.md
CHANGED
|
@@ -215,6 +215,40 @@ console.log(group.children[0].children[0].element.getValuesString()); // Detail
|
|
|
215
215
|
`Parser.parse()` continues to return the existing list of links and expanded
|
|
216
216
|
paths. `parseGroups()` preserves indentation even for the `name:` syntax.
|
|
217
217
|
|
|
218
|
+
#### Indented ID Groups
|
|
219
|
+
|
|
220
|
+
`parse` defines the canonical link values for `x:\n a\n b`: `(x: a b)`.
|
|
221
|
+
`parseGroups` keeps the `x:` element as a self reference (`id: 'x'`,
|
|
222
|
+
`values: []`), with children separate from that empty value list. Leaf lines
|
|
223
|
+
containing a single reference inside an indented ID's value subtree become
|
|
224
|
+
references, so folding that subtree agrees with `parse`:
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
import { Link, Parser } from 'links-notation';
|
|
228
|
+
|
|
229
|
+
const parser = new Parser();
|
|
230
|
+
const unindent = ({ element, children }) =>
|
|
231
|
+
children.length
|
|
232
|
+
? new Link(element.id, [...element.values, ...children.map(unindent)])
|
|
233
|
+
: element;
|
|
234
|
+
const text = 'x:\n a\n b';
|
|
235
|
+
console.log(parser.parse(text)[0].format()); // (x: a b)
|
|
236
|
+
console.log(parser.parseGroups(text).map(unindent)[0].format()); // (x: a b)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Parentheses keep their meaning: `x:\n (a)` gives `(x: (a))` in both readings.
|
|
240
|
+
Named and multi-value children keep their structure too. Under an ordinary
|
|
241
|
+
head such as `x: kind`, the child `a` remains a one-value link `(a)`;
|
|
242
|
+
`parse` expands those lines into paths, so this folding function is only an
|
|
243
|
+
equivalence with `parse` for indented ID subtrees. A standalone `id:` or
|
|
244
|
+
`(id:)` has no values in either API. The Link model represents it with
|
|
245
|
+
`id: 'id', values: []`; its existing default formatter writes `(id)`.
|
|
246
|
+
|
|
247
|
+
Before 0.30.2, `parseGroups` kept the anonymous one-value wrapper around leaf
|
|
248
|
+
references under `x:`. Callers that accessed `child.element.values[0]` there
|
|
249
|
+
should now use `child.element`. C++ (`parse_groups`), Swift and Kotlin expose
|
|
250
|
+
the same group contract; the other six bindings do not expose this parser API.
|
|
251
|
+
|
|
218
252
|
`escapeReference` and `unescapeReference` are exported functions and static
|
|
219
253
|
methods on `Link`. Use minimal mode only for whole text lines; default escaping
|
|
220
254
|
still quotes spaces so references can be used inside tuples:
|
package/README.ru.md
CHANGED
|
@@ -213,6 +213,16 @@ stream.end('следующая связь');
|
|
|
213
213
|
`listenerCount` и другие). До версии 0.27.1 `StreamParser` импортировал модуль
|
|
214
214
|
`events`, и для браузерной сборки требовался пакет-полифил `events`.
|
|
215
215
|
|
|
216
|
+
### Группы отступных ID
|
|
217
|
+
|
|
218
|
+
В JavaScript, C++, Swift и Kotlin доступен `parseGroups`.
|
|
219
|
+
Для отступного ID, например `x:\n a\n b`, элемент `x:` имеет пустой
|
|
220
|
+
список значений, а его дети — ссылки `a` и `b`. Свёртка детей в значения
|
|
221
|
+
элемента даёт `(x: a b)`, как канонический `parse`. Явное `(a)` сохраняет
|
|
222
|
+
связь с одним значением; обычный заголовок `x: kind` сохраняет прежние
|
|
223
|
+
дочерние связи и развёртку путей. Начиная с 0.30.2, ссылка в листе доступна
|
|
224
|
+
как `child.element`, а не `child.element.values[0]`.
|
|
225
|
+
|
|
216
226
|
## Примеры синтаксиса
|
|
217
227
|
|
|
218
228
|
### Дуплеты (2-кортежи)
|
package/dist/index.js
CHANGED
|
@@ -348,7 +348,7 @@ class Link {
|
|
|
348
348
|
return this._formatWithOptions(lessParentheses, isCompoundValue);
|
|
349
349
|
}
|
|
350
350
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
351
|
-
return
|
|
351
|
+
return "()";
|
|
352
352
|
}
|
|
353
353
|
if (!this.values || this.values.length === 0) {
|
|
354
354
|
const escapedId = Link.escapeReference(this.id);
|
|
@@ -359,7 +359,7 @@ class Link {
|
|
|
359
359
|
}
|
|
360
360
|
const valuesStr = this.values.map((v) => this.formatValue(v)).join(" ");
|
|
361
361
|
if (this.id === null) {
|
|
362
|
-
if (lessParentheses) {
|
|
362
|
+
if (lessParentheses && !this._hasLoneNestedValue()) {
|
|
363
363
|
const allSimple = this.values.every((v) => !v.values || v.values.length === 0);
|
|
364
364
|
if (allSimple) {
|
|
365
365
|
const simpleValuesStr = this.values.map((v) => Link.escapeReference(v.id)).join(" ");
|
|
@@ -389,9 +389,12 @@ class Link {
|
|
|
389
389
|
needsParentheses(str) {
|
|
390
390
|
return str && (str.includes(" ") || str.includes(":") || str.includes("(") || str.includes(")"));
|
|
391
391
|
}
|
|
392
|
+
_hasLoneNestedValue() {
|
|
393
|
+
return this.values.length === 1 && this.values[0].values?.length > 0;
|
|
394
|
+
}
|
|
392
395
|
_formatWithOptions(options, isCompoundValue = false) {
|
|
393
396
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
394
|
-
return
|
|
397
|
+
return "()";
|
|
395
398
|
}
|
|
396
399
|
if (!this.values || this.values.length === 0) {
|
|
397
400
|
const escapedId = Link.escapeReference(this.id);
|
|
@@ -410,7 +413,7 @@ class Link {
|
|
|
410
413
|
const idStr = Link.escapeReference(this.id);
|
|
411
414
|
testLine = options.lessParentheses ? `${idStr}: ${valuesStr}` : `(${idStr}: ${valuesStr})`;
|
|
412
415
|
} else {
|
|
413
|
-
testLine = options.lessParentheses ? valuesStr : `(${valuesStr})`;
|
|
416
|
+
testLine = options.lessParentheses && !this._hasLoneNestedValue() ? valuesStr : `(${valuesStr})`;
|
|
414
417
|
}
|
|
415
418
|
if (options.shouldIndentByLength(testLine)) {
|
|
416
419
|
shouldIndent = true;
|
|
@@ -421,7 +424,7 @@ class Link {
|
|
|
421
424
|
}
|
|
422
425
|
const valuesStr = this.values.map((v) => this.formatValue(v)).join(" ");
|
|
423
426
|
if (this.id === null) {
|
|
424
|
-
if (options.lessParentheses) {
|
|
427
|
+
if (options.lessParentheses && !this._hasLoneNestedValue()) {
|
|
425
428
|
const allSimple = this.values.every((v) => !v.values || v.values.length === 0);
|
|
426
429
|
if (allSimple) {
|
|
427
430
|
return this.values.map((v) => Link.escapeReference(v.id)).join(" ");
|
|
@@ -2940,8 +2943,10 @@ class Parser {
|
|
|
2940
2943
|
this.parse(input);
|
|
2941
2944
|
return buildCst(input, { comments: this.comments });
|
|
2942
2945
|
}
|
|
2943
|
-
_transformGroup(item) {
|
|
2944
|
-
|
|
2946
|
+
_transformGroup(item, indentedValue = false) {
|
|
2947
|
+
const children = item.children || [];
|
|
2948
|
+
const childValues = indentedValue || item.id != null && !item.values?.length;
|
|
2949
|
+
return new LinksGroup(indentedValue && children.length === 0 ? this.transformIndentedValue(item) : this.transformLink(item), children.map((child) => this._transformGroup(child, childValues)));
|
|
2945
2950
|
}
|
|
2946
2951
|
_parse(input, transform) {
|
|
2947
2952
|
if (typeof input !== "string") {
|
package/index.d.ts
CHANGED
|
@@ -264,7 +264,11 @@ export class Parser {
|
|
|
264
264
|
*/
|
|
265
265
|
parse(input: string): Link[];
|
|
266
266
|
|
|
267
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* Read the indentation tree without expanding it into path combinations.
|
|
269
|
+
* Indented ID elements have no values; their single-reference leaf children
|
|
270
|
+
* are references, so folding an indented ID subtree agrees with parse().
|
|
271
|
+
*/
|
|
268
272
|
parseGroups(input: string): IndentedGroup[];
|
|
269
273
|
|
|
270
274
|
/**
|
package/package.json
CHANGED
package/src/Link.js
CHANGED
|
@@ -196,7 +196,7 @@ export class Link {
|
|
|
196
196
|
// Original implementation for backward compatibility
|
|
197
197
|
// Empty link
|
|
198
198
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
199
|
-
return
|
|
199
|
+
return '()';
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
// Link with only ID, no values
|
|
@@ -217,7 +217,7 @@ export class Link {
|
|
|
217
217
|
// Link with values only (null id)
|
|
218
218
|
if (this.id === null) {
|
|
219
219
|
// For lessParentheses mode with simple values, don't wrap the whole thing
|
|
220
|
-
if (lessParentheses) {
|
|
220
|
+
if (lessParentheses && !this._hasLoneNestedValue()) {
|
|
221
221
|
// Check if all values are simple (no nested values)
|
|
222
222
|
const allSimple = this.values.every(
|
|
223
223
|
(v) => !v.values || v.values.length === 0
|
|
@@ -288,6 +288,16 @@ export class Link {
|
|
|
288
288
|
);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Whether this link holds exactly one link that has values. The parentheses
|
|
293
|
+
* of an anonymous link are what nest such a value: `((b c))` without them is
|
|
294
|
+
* `(b c)`, a different link, so lessParentheses has to keep them.
|
|
295
|
+
* @returns {boolean} True if the only value has values of its own
|
|
296
|
+
*/
|
|
297
|
+
_hasLoneNestedValue() {
|
|
298
|
+
return this.values.length === 1 && this.values[0].values?.length > 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
291
301
|
/**
|
|
292
302
|
* Format the link using a FormatOptions object
|
|
293
303
|
* @param {FormatOptions} options - FormatOptions object with formatting options
|
|
@@ -297,7 +307,7 @@ export class Link {
|
|
|
297
307
|
_formatWithOptions(options, isCompoundValue = false) {
|
|
298
308
|
// Empty link
|
|
299
309
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
300
|
-
return
|
|
310
|
+
return '()';
|
|
301
311
|
}
|
|
302
312
|
|
|
303
313
|
// Link with only ID, no values
|
|
@@ -325,7 +335,10 @@ export class Link {
|
|
|
325
335
|
? `${idStr}: ${valuesStr}`
|
|
326
336
|
: `(${idStr}: ${valuesStr})`;
|
|
327
337
|
} else {
|
|
328
|
-
testLine =
|
|
338
|
+
testLine =
|
|
339
|
+
options.lessParentheses && !this._hasLoneNestedValue()
|
|
340
|
+
? valuesStr
|
|
341
|
+
: `(${valuesStr})`;
|
|
329
342
|
}
|
|
330
343
|
|
|
331
344
|
if (options.shouldIndentByLength(testLine)) {
|
|
@@ -343,7 +356,7 @@ export class Link {
|
|
|
343
356
|
|
|
344
357
|
// Link with values only (null id)
|
|
345
358
|
if (this.id === null) {
|
|
346
|
-
if (options.lessParentheses) {
|
|
359
|
+
if (options.lessParentheses && !this._hasLoneNestedValue()) {
|
|
347
360
|
const allSimple = this.values.every(
|
|
348
361
|
(v) => !v.values || v.values.length === 0
|
|
349
362
|
);
|
package/src/Parser.js
CHANGED
|
@@ -42,6 +42,8 @@ export class Parser {
|
|
|
42
42
|
* Read the indentation tree before it is expanded into path combinations.
|
|
43
43
|
* Each group's element is the Link on that line; its children are groups
|
|
44
44
|
* for the immediately indented lines. Parenthesized values remain Links.
|
|
45
|
+
* In an indented ID value subtree, single-reference leaves are references,
|
|
46
|
+
* as in parse(); the ID element itself keeps an empty value list.
|
|
45
47
|
* @param {string} input - The Lino document
|
|
46
48
|
* @returns {LinksGroup[]}
|
|
47
49
|
*/
|
|
@@ -65,10 +67,18 @@ export class Parser {
|
|
|
65
67
|
return buildCst(input, { comments: this.comments });
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
_transformGroup(item) {
|
|
70
|
+
_transformGroup(item, indentedValue = false) {
|
|
71
|
+
const children = item.children || [];
|
|
72
|
+
// Carry the value context through an indented ID subtree, just as
|
|
73
|
+
// transformIndentedValue does. Keep non-leaf containers: folding their
|
|
74
|
+
// children later must retain the reference written on the parent line.
|
|
75
|
+
const childValues =
|
|
76
|
+
indentedValue || (item.id != null && !item.values?.length);
|
|
69
77
|
return new LinksGroup(
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
indentedValue && children.length === 0
|
|
79
|
+
? this.transformIndentedValue(item)
|
|
80
|
+
: this.transformLink(item),
|
|
81
|
+
children.map((child) => this._transformGroup(child, childValues))
|
|
72
82
|
);
|
|
73
83
|
}
|
|
74
84
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
import { expect, test } from 'bun:test';
|
|
4
|
+
import { Link, Parser } from '../src/index.js';
|
|
5
|
+
|
|
6
|
+
const unindent = ({ element, children }) =>
|
|
7
|
+
children.length
|
|
8
|
+
? new Link(element.id, [...element.values, ...children.map(unindent)])
|
|
9
|
+
: element;
|
|
10
|
+
|
|
11
|
+
const fixtures = JSON.parse(
|
|
12
|
+
readFileSync(
|
|
13
|
+
new URL('../../parity/text-fixtures.json', import.meta.url),
|
|
14
|
+
'utf8'
|
|
15
|
+
)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
test('indented ID groups fold to the same links as parse (#381)', () => {
|
|
19
|
+
for (const input of [
|
|
20
|
+
'x:\n a\n b',
|
|
21
|
+
"'x':\n 'a'\n ~1{62}",
|
|
22
|
+
'x:\n (a)\n b c\n child: value',
|
|
23
|
+
'x:\n inner:\n a\n branch\n b',
|
|
24
|
+
'x:\n child: kind\n a\n (branch)\n b',
|
|
25
|
+
'x:\r\n a # comment\r\n\r\n b',
|
|
26
|
+
...fixtures
|
|
27
|
+
.filter((fixture) => fixture.id.startsWith('indented-id-'))
|
|
28
|
+
.map((fixture) => fixture.input),
|
|
29
|
+
]) {
|
|
30
|
+
const parser = new Parser();
|
|
31
|
+
expect(parser.parseGroups(input).map(unindent), input).toEqual(
|
|
32
|
+
parser.parse(input)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('a bare ID group is a self reference with an empty value list', () => {
|
|
38
|
+
const parser = new Parser();
|
|
39
|
+
for (const input of ['id:', '(id:)', 'id:\n a']) {
|
|
40
|
+
const [group] = parser.parseGroups(input);
|
|
41
|
+
expect(group.element.id).toBe('id');
|
|
42
|
+
expect(group.element.values).toEqual([]);
|
|
43
|
+
expect(group.children).toHaveLength(input.includes('\n') ? 1 : 0);
|
|
44
|
+
}
|
|
45
|
+
expect(parser.parse('id:')).toEqual([new Link('id', [])]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('reference children keep syntax metadata and explicit parentheses', () => {
|
|
49
|
+
const [group] = new Parser().parseGroups("'x':\n 'a'\n (b)");
|
|
50
|
+
expect(group.element.rawId).toBe("'x'");
|
|
51
|
+
expect(group.children[0].element.id).toBe('a');
|
|
52
|
+
expect(group.children[0].element.rawId).toBe("'a'");
|
|
53
|
+
expect(group.children[0].element.quote).toBe("'");
|
|
54
|
+
expect(group.children[1].element).toEqual(new Link(null, [new Link('b')]));
|
|
55
|
+
const [ast] = new Parser({ syntax: false }).parseGroups("x:\n 'a'");
|
|
56
|
+
expect(ast.children[0].element.rawId).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('ordinary heads retain one-value child links and path expansion', () => {
|
|
60
|
+
const parser = new Parser();
|
|
61
|
+
for (const input of ['x\n a\n b', 'x: kind\n a\n b']) {
|
|
62
|
+
const [group] = parser.parseGroups(input);
|
|
63
|
+
expect(group.children.map((child) => child.element)).toEqual([
|
|
64
|
+
new Link(null, [new Link('a')]),
|
|
65
|
+
new Link(null, [new Link('b')]),
|
|
66
|
+
]);
|
|
67
|
+
expect(parser.parse(input)).toHaveLength(3);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { URL } from 'node:url';
|
|
3
3
|
import { expect, test } from 'bun:test';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FormatOptions,
|
|
6
|
+
Link,
|
|
7
|
+
Parser,
|
|
8
|
+
StreamParser,
|
|
9
|
+
formatLinks,
|
|
10
|
+
} from '../src/index.js';
|
|
5
11
|
|
|
6
12
|
const fixtures = JSON.parse(
|
|
7
13
|
readFileSync(
|
|
@@ -25,3 +31,49 @@ test('shared text parity', () => {
|
|
|
25
31
|
expect(tree(stream.end()), fixture.id).toEqual(fixture.expected);
|
|
26
32
|
}
|
|
27
33
|
});
|
|
34
|
+
|
|
35
|
+
test('shared less parentheses round trip', () => {
|
|
36
|
+
const options = new FormatOptions({ lessParentheses: true });
|
|
37
|
+
for (const fixture of fixtures) {
|
|
38
|
+
const parsed = new Parser().parse(fixture.input);
|
|
39
|
+
expect(formatLinks(parsed, true), fixture.id).toBe(fixture.lessParentheses);
|
|
40
|
+
expect(formatLinks(parsed, options), fixture.id).toBe(
|
|
41
|
+
fixture.lessParentheses
|
|
42
|
+
);
|
|
43
|
+
expect(
|
|
44
|
+
tree(new Parser().parse(fixture.lessParentheses)),
|
|
45
|
+
fixture.id
|
|
46
|
+
).toEqual(fixture.expected);
|
|
47
|
+
for (const link of parsed) {
|
|
48
|
+
const [readBack] = new Parser().parse(link.format(true));
|
|
49
|
+
expect(link.equals(readBack), fixture.id).toBe(true);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('less parentheses keeps the parentheses of a lone nested value', () => {
|
|
55
|
+
const nested = new Link(null, [new Link('b'), new Link('c')]);
|
|
56
|
+
expect(new Link(null, [nested]).format(true)).toBe('((b c))');
|
|
57
|
+
expect(new Link(null, [new Link('b')]).format(true)).toBe('b');
|
|
58
|
+
expect(new Link(null, [new Link('a'), nested]).format(true)).toBe('a (b c)');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('less parentheses keeps the parentheses of an empty link', () => {
|
|
62
|
+
const options = new FormatOptions({ lessParentheses: true });
|
|
63
|
+
expect(new Link(null, []).format(true)).toBe('()');
|
|
64
|
+
expect(new Link(null, []).format(options)).toBe('()');
|
|
65
|
+
expect(formatLinks([], true)).toBe('');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('line length is measured on the parentheses a lone nested value keeps', () => {
|
|
69
|
+
const link = new Link(null, [new Link(null, [new Link('b'), new Link('c')])]);
|
|
70
|
+
const options = (maxLineLength) =>
|
|
71
|
+
new FormatOptions({
|
|
72
|
+
lessParentheses: true,
|
|
73
|
+
indentLongLines: true,
|
|
74
|
+
maxLineLength,
|
|
75
|
+
preferInline: false,
|
|
76
|
+
});
|
|
77
|
+
expect(link.format(options(7))).toBe('((b c))');
|
|
78
|
+
expect(link.format(options(6))).toBe(' (b c)');
|
|
79
|
+
});
|
|
@@ -69,7 +69,7 @@ describe('syntax metadata on parsed links (issue #364)', () => {
|
|
|
69
69
|
test('parseGroups and StreamParser keep the metadata', () => {
|
|
70
70
|
const groups = new Parser().parseGroups('\'root\':\n "child"');
|
|
71
71
|
expect(groups[0].element.rawId).toBe("'root'");
|
|
72
|
-
expect(groups[0].children[0].element.
|
|
72
|
+
expect(groups[0].children[0].element.rawId).toBe('"child"');
|
|
73
73
|
const stream = new StreamParser();
|
|
74
74
|
stream.write("(a '");
|
|
75
75
|
stream.write("b')");
|