links-notation 0.11.2 → 0.13.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/.eslintrc.json +28 -0
- package/.prettierignore +4 -0
- package/.prettierrc +4 -0
- package/README.md +52 -7
- package/README.ru.md +3 -6
- package/bun.lock +200 -0
- package/dist/index.js +582 -119
- package/package.json +6 -3
- package/src/FormatConfig.js +19 -0
- package/src/FormatOptions.js +52 -0
- package/src/Link.js +340 -40
- package/src/LinksGroup.js +2 -2
- package/src/Parser.js +57 -22
- package/src/grammar.pegjs +105 -4
- package/src/index.js +3 -1
- package/src/parser-generated.js +401 -88
- package/tests/ApiTests.test.js +104 -7
- package/tests/EdgeCaseParser.test.js +12 -12
- package/tests/FormatConfig.test.js +148 -0
- package/tests/IndentationConsistency.test.js +1 -1
- package/tests/IndentedIdSyntax.test.js +39 -39
- package/tests/Link.test.js +3 -3
- package/tests/LinksGroup.test.js +35 -5
- package/tests/MixedIndentationModes.test.js +35 -35
- package/tests/MultiQuoteParser.test.js +463 -0
- package/tests/MultilineParser.test.js +17 -17
- package/tests/MultilineQuotedString.test.js +9 -9
- package/tests/NestedParser.test.js +36 -7
- package/tests/NestedSelfReference.test.js +104 -0
- package/tests/SingleLineParser.test.js +88 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "links-notation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Links Notation parser for JavaScript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"build": "bun run build:grammar && bun build src/index.js --outdir=dist --target=node",
|
|
9
9
|
"build:grammar": "peggy --format es -o src/parser-generated.js src/grammar.pegjs",
|
|
10
10
|
"test": "bun test",
|
|
11
|
-
"test:watch": "bun test --watch"
|
|
11
|
+
"test:watch": "bun test --watch",
|
|
12
|
+
"lint": "eslint src/ tests/",
|
|
13
|
+
"lint:fix": "eslint src/ tests/ --fix"
|
|
12
14
|
},
|
|
13
15
|
"keywords": [
|
|
14
16
|
"lino",
|
|
@@ -20,7 +22,8 @@
|
|
|
20
22
|
"license": "Unlicense",
|
|
21
23
|
"devDependencies": {
|
|
22
24
|
"bun-types": "^1.2.19",
|
|
25
|
+
"eslint": "^8.56.0",
|
|
23
26
|
"peggy": "^5.0.6"
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {}
|
|
26
|
-
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormatConfig is an alias for FormatOptions to maintain API consistency
|
|
3
|
+
* with the Python implementation.
|
|
4
|
+
*
|
|
5
|
+
* JavaScript uses 'FormatOptions' internally, but exports 'FormatConfig'
|
|
6
|
+
* as the primary name to match Python's naming convention.
|
|
7
|
+
*/
|
|
8
|
+
import { FormatOptions } from './FormatOptions.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* FormatConfig for Lino notation formatting.
|
|
12
|
+
*
|
|
13
|
+
* This is an alias for FormatOptions to maintain API consistency across languages.
|
|
14
|
+
* Python uses 'FormatConfig', so this export provides the same name for JavaScript.
|
|
15
|
+
*/
|
|
16
|
+
export class FormatConfig extends FormatOptions {
|
|
17
|
+
// FormatConfig inherits all functionality from FormatOptions
|
|
18
|
+
// No additional implementation needed - this is purely for naming consistency
|
|
19
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormatOptions for Lino notation formatting.
|
|
3
|
+
*
|
|
4
|
+
* Provides configuration options for controlling how Link objects are formatted.
|
|
5
|
+
*/
|
|
6
|
+
export class FormatOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Create a new FormatOptions instance
|
|
9
|
+
* @param {Object} options - Configuration options
|
|
10
|
+
* @param {boolean} [options.lessParentheses=false] - If true, omit parentheses where safe
|
|
11
|
+
* @param {number} [options.maxLineLength=80] - Maximum line length before auto-indenting
|
|
12
|
+
* @param {boolean} [options.indentLongLines=false] - If true, indent lines exceeding maxLineLength
|
|
13
|
+
* @param {number|null} [options.maxInlineRefs=null] - Maximum number of references before auto-indenting
|
|
14
|
+
* @param {boolean} [options.groupConsecutive=false] - If true, group consecutive links with same ID
|
|
15
|
+
* @param {string} [options.indentString=" "] - String to use for indentation
|
|
16
|
+
* @param {boolean} [options.preferInline=true] - If true, prefer inline format when under thresholds
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.lessParentheses = options.lessParentheses ?? false;
|
|
20
|
+
this.maxLineLength = options.maxLineLength ?? 80;
|
|
21
|
+
this.indentLongLines = options.indentLongLines ?? false;
|
|
22
|
+
this.maxInlineRefs = options.maxInlineRefs ?? null;
|
|
23
|
+
this.groupConsecutive = options.groupConsecutive ?? false;
|
|
24
|
+
this.indentString = options.indentString ?? ' ';
|
|
25
|
+
this.preferInline = options.preferInline ?? true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if line should be indented based on length
|
|
30
|
+
* @param {string} line - The line to check
|
|
31
|
+
* @returns {boolean} True if line should be indented based on length threshold
|
|
32
|
+
*/
|
|
33
|
+
shouldIndentByLength(line) {
|
|
34
|
+
if (!this.indentLongLines) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
// Count printable unicode characters
|
|
38
|
+
return line.length > this.maxLineLength;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if link should be indented based on reference count
|
|
43
|
+
* @param {number} refCount - Number of references in the link
|
|
44
|
+
* @returns {boolean} True if link should be indented based on reference count threshold
|
|
45
|
+
*/
|
|
46
|
+
shouldIndentByRefCount(refCount) {
|
|
47
|
+
if (this.maxInlineRefs === null) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return refCount > this.maxInlineRefs;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/Link.js
CHANGED
|
@@ -1,43 +1,95 @@
|
|
|
1
1
|
export class Link {
|
|
2
|
+
/**
|
|
3
|
+
* Create a new Link
|
|
4
|
+
* @param {string|null} id - Optional identifier for the link
|
|
5
|
+
* @param {Link[]|null} values - Optional array of nested links
|
|
6
|
+
* @throws {TypeError} If values is not an array or null
|
|
7
|
+
*/
|
|
2
8
|
constructor(id = null, values = null) {
|
|
3
9
|
this.id = id;
|
|
4
|
-
|
|
10
|
+
|
|
11
|
+
// Validate that values is an array if provided
|
|
12
|
+
if (values !== null && values !== undefined) {
|
|
13
|
+
if (!Array.isArray(values)) {
|
|
14
|
+
throw new TypeError('values must be an array or null');
|
|
15
|
+
}
|
|
16
|
+
this.values = values;
|
|
17
|
+
} else {
|
|
18
|
+
this.values = [];
|
|
19
|
+
}
|
|
5
20
|
}
|
|
6
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Convert link to string representation
|
|
24
|
+
* @returns {string} String representation of the link
|
|
25
|
+
*/
|
|
7
26
|
toString() {
|
|
8
27
|
return this.format(false);
|
|
9
28
|
}
|
|
10
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Get formatted string of all values
|
|
32
|
+
* @returns {string} Space-separated string of values
|
|
33
|
+
*/
|
|
11
34
|
getValuesString() {
|
|
12
|
-
return
|
|
13
|
-
|
|
35
|
+
return !this.values || this.values.length === 0
|
|
36
|
+
? ''
|
|
37
|
+
: this.values.map((v) => Link.getValueString(v)).join(' ');
|
|
14
38
|
}
|
|
15
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Simplify the link structure by unwrapping single-value containers
|
|
42
|
+
* @returns {Link} Simplified link
|
|
43
|
+
*/
|
|
16
44
|
simplify() {
|
|
17
45
|
if (!this.values || this.values.length === 0) {
|
|
18
46
|
return this;
|
|
19
47
|
} else if (this.values.length === 1) {
|
|
20
48
|
return this.values[0];
|
|
21
49
|
} else {
|
|
22
|
-
const newValues = this.values.map(v =>
|
|
50
|
+
const newValues = this.values.map((v) => {
|
|
51
|
+
// Check if value has simplify method (defensive programming)
|
|
52
|
+
return v && typeof v.simplify === 'function' ? v.simplify() : v;
|
|
53
|
+
});
|
|
23
54
|
return new Link(this.id, newValues);
|
|
24
55
|
}
|
|
25
56
|
}
|
|
26
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Combine this link with another link
|
|
60
|
+
* @param {Link} other - The link to combine with
|
|
61
|
+
* @returns {Link} Combined link
|
|
62
|
+
*/
|
|
27
63
|
combine(other) {
|
|
28
64
|
return new Link(null, [this, other]);
|
|
29
65
|
}
|
|
30
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Get string representation of a value
|
|
69
|
+
* @param {Link} value - The value to stringify
|
|
70
|
+
* @returns {string} String representation
|
|
71
|
+
*/
|
|
31
72
|
static getValueString(value) {
|
|
32
|
-
|
|
73
|
+
// Defensive check for method existence
|
|
74
|
+
return value && typeof value.toLinkOrIdString === 'function'
|
|
75
|
+
? value.toLinkOrIdString()
|
|
76
|
+
: String(value);
|
|
33
77
|
}
|
|
34
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Escape a reference string by adding quotes if necessary
|
|
81
|
+
* @param {string} reference - The reference to escape
|
|
82
|
+
* @returns {string} Escaped reference
|
|
83
|
+
*/
|
|
35
84
|
static escapeReference(reference) {
|
|
36
85
|
if (!reference || reference.trim() === '') {
|
|
37
86
|
return '';
|
|
38
87
|
}
|
|
39
|
-
|
|
40
|
-
const
|
|
88
|
+
|
|
89
|
+
const hasSingleQuote = reference.includes("'");
|
|
90
|
+
const hasDoubleQuote = reference.includes('"');
|
|
91
|
+
|
|
92
|
+
const needsQuoting =
|
|
41
93
|
reference.includes(':') ||
|
|
42
94
|
reference.includes('(') ||
|
|
43
95
|
reference.includes(')') ||
|
|
@@ -45,17 +97,38 @@ export class Link {
|
|
|
45
97
|
reference.includes('\t') ||
|
|
46
98
|
reference.includes('\n') ||
|
|
47
99
|
reference.includes('\r') ||
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
100
|
+
hasDoubleQuote ||
|
|
101
|
+
hasSingleQuote;
|
|
102
|
+
|
|
103
|
+
// Handle edge case: reference contains both single and double quotes
|
|
104
|
+
if (hasSingleQuote && hasDoubleQuote) {
|
|
105
|
+
// Escape single quotes and wrap in single quotes
|
|
106
|
+
return `'${reference.replace(/'/g, "\\'")}'`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Prefer single quotes if double quotes are present
|
|
110
|
+
if (hasDoubleQuote) {
|
|
51
111
|
return `'${reference}'`;
|
|
52
|
-
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Use double quotes if single quotes are present
|
|
115
|
+
if (hasSingleQuote) {
|
|
53
116
|
return `"${reference}"`;
|
|
54
|
-
} else {
|
|
55
|
-
return reference;
|
|
56
117
|
}
|
|
118
|
+
|
|
119
|
+
// Use single quotes for special characters
|
|
120
|
+
if (needsQuoting) {
|
|
121
|
+
return `'${reference}'`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// No quoting needed
|
|
125
|
+
return reference;
|
|
57
126
|
}
|
|
58
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Convert to string using either just ID or full format
|
|
130
|
+
* @returns {string} String representation
|
|
131
|
+
*/
|
|
59
132
|
toLinkOrIdString() {
|
|
60
133
|
if (!this.values || this.values.length === 0) {
|
|
61
134
|
return this.id === null ? '' : Link.escapeReference(this.id);
|
|
@@ -63,26 +136,61 @@ export class Link {
|
|
|
63
136
|
return this.toString();
|
|
64
137
|
}
|
|
65
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Check equality with another Link
|
|
141
|
+
* @param {*} other - Object to compare with
|
|
142
|
+
* @returns {boolean} True if links are equal
|
|
143
|
+
*/
|
|
66
144
|
equals(other) {
|
|
67
145
|
if (!(other instanceof Link)) return false;
|
|
68
146
|
if (this.id !== other.id) return false;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
147
|
+
|
|
148
|
+
// Handle null/undefined values arrays
|
|
149
|
+
const thisValues = this.values || [];
|
|
150
|
+
const otherValues = other.values || [];
|
|
151
|
+
|
|
152
|
+
if (thisValues.length !== otherValues.length) return false;
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < thisValues.length; i++) {
|
|
155
|
+
// Defensive check for equals method
|
|
156
|
+
if (thisValues[i] && typeof thisValues[i].equals === 'function') {
|
|
157
|
+
if (!thisValues[i].equals(otherValues[i])) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// Fallback to reference equality
|
|
162
|
+
if (thisValues[i] !== otherValues[i]) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
74
165
|
}
|
|
75
166
|
}
|
|
76
167
|
return true;
|
|
77
168
|
}
|
|
78
169
|
|
|
79
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Format the link as a string
|
|
172
|
+
* @param {boolean|FormatOptions} lessParentheses - If true, omit parentheses where safe; or a FormatOptions object
|
|
173
|
+
* @param {boolean} isCompoundValue - If true, this is a value in a compound link
|
|
174
|
+
* @returns {string} Formatted string
|
|
175
|
+
*/
|
|
80
176
|
format(lessParentheses = false, isCompoundValue = false) {
|
|
177
|
+
// Support FormatOptions/FormatConfig as first parameter
|
|
178
|
+
// Check if it's an object with formatting properties (FormatOptions or FormatConfig)
|
|
179
|
+
if (
|
|
180
|
+
lessParentheses &&
|
|
181
|
+
typeof lessParentheses === 'object' &&
|
|
182
|
+
(lessParentheses.constructor.name === 'FormatOptions' ||
|
|
183
|
+
lessParentheses.constructor.name === 'FormatConfig')
|
|
184
|
+
) {
|
|
185
|
+
return this._formatWithOptions(lessParentheses, isCompoundValue);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Original implementation for backward compatibility
|
|
81
189
|
// Empty link
|
|
82
190
|
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
83
191
|
return lessParentheses ? '' : '()';
|
|
84
192
|
}
|
|
85
|
-
|
|
193
|
+
|
|
86
194
|
// Link with only ID, no values
|
|
87
195
|
if (!this.values || this.values.length === 0) {
|
|
88
196
|
const escapedId = Link.escapeReference(this.id);
|
|
@@ -90,21 +198,27 @@ export class Link {
|
|
|
90
198
|
if (isCompoundValue) {
|
|
91
199
|
return `(${escapedId})`;
|
|
92
200
|
}
|
|
93
|
-
return lessParentheses && !this.needsParentheses(this.id)
|
|
201
|
+
return lessParentheses && !this.needsParentheses(this.id)
|
|
202
|
+
? escapedId
|
|
203
|
+
: `(${escapedId})`;
|
|
94
204
|
}
|
|
95
|
-
|
|
96
|
-
// Format values recursively
|
|
97
|
-
const valuesStr = this.values.map(v => this.formatValue(v)).join(' ');
|
|
98
|
-
|
|
205
|
+
|
|
206
|
+
// Format values recursively
|
|
207
|
+
const valuesStr = this.values.map((v) => this.formatValue(v)).join(' ');
|
|
208
|
+
|
|
99
209
|
// Link with values only (null id)
|
|
100
210
|
if (this.id === null) {
|
|
101
211
|
// For lessParentheses mode with simple values, don't wrap the whole thing
|
|
102
212
|
if (lessParentheses) {
|
|
103
213
|
// Check if all values are simple (no nested values)
|
|
104
|
-
const allSimple = this.values.every(
|
|
214
|
+
const allSimple = this.values.every(
|
|
215
|
+
(v) => !v.values || v.values.length === 0
|
|
216
|
+
);
|
|
105
217
|
if (allSimple) {
|
|
106
218
|
// Format each value without extra wrapping
|
|
107
|
-
const simpleValuesStr = this.values
|
|
219
|
+
const simpleValuesStr = this.values
|
|
220
|
+
.map((v) => Link.escapeReference(v.id))
|
|
221
|
+
.join(' ');
|
|
108
222
|
return simpleValuesStr;
|
|
109
223
|
}
|
|
110
224
|
// For mixed or complex values in lessParentheses mode, still avoid outer wrapper
|
|
@@ -114,42 +228,228 @@ export class Link {
|
|
|
114
228
|
// For normal mode, wrap in parentheses
|
|
115
229
|
return `(${valuesStr})`;
|
|
116
230
|
}
|
|
117
|
-
|
|
231
|
+
|
|
118
232
|
// Link with ID and values
|
|
119
233
|
const idStr = Link.escapeReference(this.id);
|
|
120
234
|
const withColon = `${idStr}: ${valuesStr}`;
|
|
121
|
-
return lessParentheses && !this.needsParentheses(this.id)
|
|
235
|
+
return lessParentheses && !this.needsParentheses(this.id)
|
|
236
|
+
? withColon
|
|
237
|
+
: `(${withColon})`;
|
|
122
238
|
}
|
|
123
|
-
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Format a value within this link
|
|
242
|
+
* @param {Link} value - The value to format
|
|
243
|
+
* @returns {string} Formatted value string
|
|
244
|
+
*/
|
|
124
245
|
formatValue(value) {
|
|
125
|
-
if (!value.format) {
|
|
126
|
-
return Link.escapeReference(value.id || '');
|
|
246
|
+
if (!value || !value.format) {
|
|
247
|
+
return Link.escapeReference((value && value.id) || '');
|
|
127
248
|
}
|
|
128
|
-
|
|
249
|
+
|
|
129
250
|
// Check if we're in a compound link that was created from path combinations
|
|
130
251
|
// This is indicated by having a parent context passed through
|
|
131
252
|
const isCompoundFromPaths = this._isFromPathCombination === true;
|
|
132
|
-
|
|
253
|
+
|
|
133
254
|
// For compound links from paths, format values with parentheses
|
|
134
255
|
if (isCompoundFromPaths) {
|
|
135
256
|
return value.format(false, true);
|
|
136
257
|
}
|
|
137
|
-
|
|
258
|
+
|
|
138
259
|
// Simple link with just an ID - don't wrap in parentheses when used as a value
|
|
139
260
|
if (!value.values || value.values.length === 0) {
|
|
140
261
|
return Link.escapeReference(value.id);
|
|
141
262
|
}
|
|
142
|
-
|
|
263
|
+
|
|
143
264
|
// Complex value with its own structure - format it normally with parentheses
|
|
144
265
|
return value.format(false, false);
|
|
145
266
|
}
|
|
146
|
-
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if a string needs to be wrapped in parentheses
|
|
270
|
+
* @param {string} str - The string to check
|
|
271
|
+
* @returns {boolean} True if parentheses are needed
|
|
272
|
+
*/
|
|
147
273
|
needsParentheses(str) {
|
|
148
|
-
return
|
|
274
|
+
return (
|
|
275
|
+
str &&
|
|
276
|
+
(str.includes(' ') ||
|
|
277
|
+
str.includes(':') ||
|
|
278
|
+
str.includes('(') ||
|
|
279
|
+
str.includes(')'))
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Format the link using a FormatOptions object
|
|
285
|
+
* @param {FormatOptions} options - FormatOptions object with formatting options
|
|
286
|
+
* @param {boolean} isCompoundValue - If true, this is a value in a compound link
|
|
287
|
+
* @returns {string} Formatted string
|
|
288
|
+
*/
|
|
289
|
+
_formatWithOptions(options, isCompoundValue = false) {
|
|
290
|
+
// Empty link
|
|
291
|
+
if (this.id === null && (!this.values || this.values.length === 0)) {
|
|
292
|
+
return options.lessParentheses ? '' : '()';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Link with only ID, no values
|
|
296
|
+
if (!this.values || this.values.length === 0) {
|
|
297
|
+
const escapedId = Link.escapeReference(this.id);
|
|
298
|
+
if (isCompoundValue) {
|
|
299
|
+
return `(${escapedId})`;
|
|
300
|
+
}
|
|
301
|
+
return options.lessParentheses && !this.needsParentheses(this.id)
|
|
302
|
+
? escapedId
|
|
303
|
+
: `(${escapedId})`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check if we should use indented format
|
|
307
|
+
let shouldIndent = false;
|
|
308
|
+
if (options.shouldIndentByRefCount(this.values.length)) {
|
|
309
|
+
shouldIndent = true;
|
|
310
|
+
} else {
|
|
311
|
+
// Try inline format first
|
|
312
|
+
const valuesStr = this.values.map((v) => this.formatValue(v)).join(' ');
|
|
313
|
+
let testLine;
|
|
314
|
+
if (this.id !== null) {
|
|
315
|
+
const idStr = Link.escapeReference(this.id);
|
|
316
|
+
testLine = options.lessParentheses
|
|
317
|
+
? `${idStr}: ${valuesStr}`
|
|
318
|
+
: `(${idStr}: ${valuesStr})`;
|
|
319
|
+
} else {
|
|
320
|
+
testLine = options.lessParentheses ? valuesStr : `(${valuesStr})`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (options.shouldIndentByLength(testLine)) {
|
|
324
|
+
shouldIndent = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Format with indentation if needed
|
|
329
|
+
if (shouldIndent && options.preferInline === false) {
|
|
330
|
+
return this._formatIndented(options);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Standard inline formatting
|
|
334
|
+
const valuesStr = this.values.map((v) => this.formatValue(v)).join(' ');
|
|
335
|
+
|
|
336
|
+
// Link with values only (null id)
|
|
337
|
+
if (this.id === null) {
|
|
338
|
+
if (options.lessParentheses) {
|
|
339
|
+
const allSimple = this.values.every(
|
|
340
|
+
(v) => !v.values || v.values.length === 0
|
|
341
|
+
);
|
|
342
|
+
if (allSimple) {
|
|
343
|
+
return this.values.map((v) => Link.escapeReference(v.id)).join(' ');
|
|
344
|
+
}
|
|
345
|
+
return valuesStr;
|
|
346
|
+
}
|
|
347
|
+
return `(${valuesStr})`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Link with ID and values
|
|
351
|
+
const idStr = Link.escapeReference(this.id);
|
|
352
|
+
const withColon = `${idStr}: ${valuesStr}`;
|
|
353
|
+
return options.lessParentheses && !this.needsParentheses(this.id)
|
|
354
|
+
? withColon
|
|
355
|
+
: `(${withColon})`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Format the link with indentation
|
|
360
|
+
* @param {FormatOptions} options - FormatOptions object with formatting options
|
|
361
|
+
* @returns {string} Indented formatted string
|
|
362
|
+
*/
|
|
363
|
+
_formatIndented(options) {
|
|
364
|
+
if (this.id === null) {
|
|
365
|
+
// Values only - format each on separate line
|
|
366
|
+
const lines = this.values.map(
|
|
367
|
+
(v) => options.indentString + this.formatValue(v)
|
|
368
|
+
);
|
|
369
|
+
return lines.join('\n');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Link with ID - format as id:\n value1\n value2
|
|
373
|
+
const idStr = Link.escapeReference(this.id);
|
|
374
|
+
const lines = [`${idStr}:`];
|
|
375
|
+
for (const v of this.values) {
|
|
376
|
+
lines.push(options.indentString + this.formatValue(v));
|
|
377
|
+
}
|
|
378
|
+
return lines.join('\n');
|
|
149
379
|
}
|
|
150
380
|
}
|
|
151
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Group consecutive links with the same ID
|
|
384
|
+
* @param {Link[]} links - List of links to group
|
|
385
|
+
* @returns {Link[]} New list with consecutive links grouped
|
|
386
|
+
*/
|
|
387
|
+
function _groupConsecutiveLinks(links) {
|
|
388
|
+
if (!links || links.length === 0) {
|
|
389
|
+
return links;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const grouped = [];
|
|
393
|
+
let i = 0;
|
|
394
|
+
|
|
395
|
+
while (i < links.length) {
|
|
396
|
+
const current = links[i];
|
|
397
|
+
|
|
398
|
+
// Look ahead for consecutive links with same ID
|
|
399
|
+
if (current.id !== null && current.values && current.values.length > 0) {
|
|
400
|
+
// Collect all values with same ID
|
|
401
|
+
const sameIdValues = [...current.values];
|
|
402
|
+
let j = i + 1;
|
|
403
|
+
|
|
404
|
+
while (j < links.length) {
|
|
405
|
+
const nextLink = links[j];
|
|
406
|
+
if (
|
|
407
|
+
nextLink.id === current.id &&
|
|
408
|
+
nextLink.values &&
|
|
409
|
+
nextLink.values.length > 0
|
|
410
|
+
) {
|
|
411
|
+
sameIdValues.push(...nextLink.values);
|
|
412
|
+
j++;
|
|
413
|
+
} else {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// If we found consecutive links, create grouped link
|
|
419
|
+
if (j > i + 1) {
|
|
420
|
+
const groupedLink = new Link(current.id, sameIdValues);
|
|
421
|
+
grouped.push(groupedLink);
|
|
422
|
+
i = j;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
grouped.push(current);
|
|
428
|
+
i++;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return grouped;
|
|
432
|
+
}
|
|
433
|
+
|
|
152
434
|
export function formatLinks(links, lessParentheses = false) {
|
|
153
435
|
if (!links || links.length === 0) return '';
|
|
154
|
-
|
|
155
|
-
|
|
436
|
+
|
|
437
|
+
// Support FormatOptions/FormatConfig as parameter
|
|
438
|
+
if (
|
|
439
|
+
lessParentheses &&
|
|
440
|
+
typeof lessParentheses === 'object' &&
|
|
441
|
+
(lessParentheses.constructor.name === 'FormatOptions' ||
|
|
442
|
+
lessParentheses.constructor.name === 'FormatConfig')
|
|
443
|
+
) {
|
|
444
|
+
const options = lessParentheses;
|
|
445
|
+
// Apply consecutive link grouping if enabled
|
|
446
|
+
let linksToFormat = links;
|
|
447
|
+
if (options.groupConsecutive) {
|
|
448
|
+
linksToFormat = _groupConsecutiveLinks(links);
|
|
449
|
+
}
|
|
450
|
+
return linksToFormat.map((link) => link.format(options)).join('\n');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Backward compatibility with boolean parameter
|
|
454
|
+
return links.map((link) => link.format(lessParentheses)).join('\n');
|
|
455
|
+
}
|
package/src/LinksGroup.js
CHANGED