boxwood 2.18.0 → 2.19.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 +26 -0
- package/index.js +28 -6
- package/package.json +1 -1
- package/ui/markdown/index.js +7 -0
- package/ui/prose/README.md +11 -0
- package/ui/prose/index.js +11 -1
- package/ui/prose/utilities/processConditionals.js +36 -9
- package/ui/prose/utilities/processLoops.js +8 -7
- package/ui/prose/utilities/validate.js +172 -22
- package/ui/utilities/convertNodes.js +99 -1
- package/ui/utilities/format.js +0 -0
- package/ui/utilities/params.js +9 -1
- package/ui/utilities/parseBlock.js +214 -4
- package/ui/utilities/parseCustomTag.js +112 -60
- package/ui/utilities/replaceVariables.js +320 -35
- package/ui/utilities/slugify.js +39 -0
|
@@ -26,10 +26,29 @@ const SAFE_METHODS = new Set([
|
|
|
26
26
|
"endsWith",
|
|
27
27
|
// Numbers
|
|
28
28
|
"toFixed",
|
|
29
|
+
// Dates and numbers - locale-aware formatting, e.g. toLocaleDateString('pl-PL')
|
|
30
|
+
"toLocaleDateString",
|
|
31
|
+
"toLocaleTimeString",
|
|
32
|
+
"toLocaleString",
|
|
33
|
+
"toISOString",
|
|
29
34
|
// Anything
|
|
30
35
|
"toString",
|
|
31
36
|
])
|
|
32
37
|
|
|
38
|
+
// Property names that expose the prototype chain - never resolved, so a
|
|
39
|
+
// path like "x.constructor.constructor" cannot reach Function or pollute
|
|
40
|
+
// prototypes. Blocked for both dot and bracket access, at any depth.
|
|
41
|
+
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"])
|
|
42
|
+
|
|
43
|
+
// Guard against pathological nesting like "{[[[[...]]]]}" - real expressions
|
|
44
|
+
// never nest this deeply, and unbounded recursion would exhaust the stack
|
|
45
|
+
const MAX_EXPRESSION_DEPTH = 50
|
|
46
|
+
|
|
47
|
+
// Real expressions are short (a path, a call, a small array literal); an
|
|
48
|
+
// oversized one is an attack or a typo. Cutting it off before the O(n)
|
|
49
|
+
// scanners run keeps a malicious "{[[[...]]]}" from allocating megabytes.
|
|
50
|
+
const MAX_EXPRESSION_LENGTH = 1000
|
|
51
|
+
|
|
33
52
|
/**
|
|
34
53
|
* Parse a single method argument literal
|
|
35
54
|
* Supports numbers, quoted strings, booleans and null
|
|
@@ -56,7 +75,13 @@ function parseArgument(raw) {
|
|
|
56
75
|
|
|
57
76
|
/**
|
|
58
77
|
* Parse a path into segments of property accesses and method calls
|
|
59
|
-
* e.g. "images.slice(0,
|
|
78
|
+
* e.g. "images.slice(0, limit)" -> [
|
|
79
|
+
* {type: "property", name: "images"},
|
|
80
|
+
* {type: "method", name: "slice", args: [
|
|
81
|
+
* {type: "literal", value: 0},
|
|
82
|
+
* {type: "path", path: "limit"},
|
|
83
|
+
* ]},
|
|
84
|
+
* ]
|
|
60
85
|
* @param {string} path - The path to parse
|
|
61
86
|
* @returns {Array|null} - Parsed segments or null if the path is malformed
|
|
62
87
|
*/
|
|
@@ -91,12 +116,14 @@ function parsePathSegments(path) {
|
|
|
91
116
|
if (!name) return null
|
|
92
117
|
|
|
93
118
|
if (path[i] === "(") {
|
|
94
|
-
// Method call - collect
|
|
119
|
+
// Method call - collect arguments, respecting quoted strings and
|
|
120
|
+
// nested brackets/parentheses (e.g. "slice(0, items.indexOf('x'))")
|
|
95
121
|
i++
|
|
96
122
|
const rawArgs = []
|
|
97
123
|
let current = ""
|
|
98
124
|
let quote = null
|
|
99
125
|
let closed = false
|
|
126
|
+
let depth = 0
|
|
100
127
|
while (i < path.length) {
|
|
101
128
|
const c = path[i]
|
|
102
129
|
if (quote) {
|
|
@@ -111,16 +138,34 @@ function parsePathSegments(path) {
|
|
|
111
138
|
i++
|
|
112
139
|
continue
|
|
113
140
|
}
|
|
114
|
-
if (c === "
|
|
115
|
-
|
|
116
|
-
current
|
|
141
|
+
if (c === "(" || c === "[") {
|
|
142
|
+
depth++
|
|
143
|
+
current += c
|
|
144
|
+
i++
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
if (c === "]") {
|
|
148
|
+
depth--
|
|
149
|
+
current += c
|
|
117
150
|
i++
|
|
118
151
|
continue
|
|
119
152
|
}
|
|
120
153
|
if (c === ")") {
|
|
121
|
-
|
|
154
|
+
if (depth === 0) {
|
|
155
|
+
closed = true
|
|
156
|
+
i++
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
depth--
|
|
160
|
+
current += c
|
|
161
|
+
i++
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
if (c === "," && depth === 0) {
|
|
165
|
+
rawArgs.push(current)
|
|
166
|
+
current = ""
|
|
122
167
|
i++
|
|
123
|
-
|
|
168
|
+
continue
|
|
124
169
|
}
|
|
125
170
|
current += c
|
|
126
171
|
i++
|
|
@@ -130,11 +175,19 @@ function parsePathSegments(path) {
|
|
|
130
175
|
rawArgs.push(current)
|
|
131
176
|
}
|
|
132
177
|
|
|
178
|
+
// Arguments are literals or data paths, e.g. "slice(0, limit)" -
|
|
179
|
+
// paths are stored unresolved and looked up against data at resolve time
|
|
133
180
|
const args = []
|
|
134
181
|
for (const raw of rawArgs) {
|
|
135
182
|
const parsed = parseArgument(raw)
|
|
136
|
-
if (
|
|
137
|
-
|
|
183
|
+
if (parsed) {
|
|
184
|
+
args.push({ type: "literal", value: parsed.value })
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
const argPath = raw.trim()
|
|
188
|
+
const argSegments = parsePathSegments(argPath)
|
|
189
|
+
if (!argSegments || argSegments.length === 0) return null
|
|
190
|
+
args.push({ type: "path", path: argPath })
|
|
138
191
|
}
|
|
139
192
|
segments.push({ type: "method", name, args })
|
|
140
193
|
} else {
|
|
@@ -147,9 +200,10 @@ function parsePathSegments(path) {
|
|
|
147
200
|
|
|
148
201
|
/**
|
|
149
202
|
* Resolve a path like "images[0].src", "user.name" or "images.slice(0, 2)" from a data object
|
|
150
|
-
* Method calls are restricted to a whitelist of non-mutating methods
|
|
203
|
+
* Method calls are restricted to a whitelist of non-mutating methods
|
|
204
|
+
* Arguments are literals or data paths, e.g. "images.slice(0, limit)"
|
|
151
205
|
* @param {Object} data - The data object to resolve the path from
|
|
152
|
-
* @param {string} path - The path to resolve (e.g., "images[0].src", "images.slice(0,
|
|
206
|
+
* @param {string} path - The path to resolve (e.g., "images[0].src", "images.slice(0, limit)")
|
|
153
207
|
* @returns {*} - The resolved value or undefined
|
|
154
208
|
*/
|
|
155
209
|
function resolvePath(data, path) {
|
|
@@ -160,6 +214,9 @@ function resolvePath(data, path) {
|
|
|
160
214
|
|
|
161
215
|
// Handle simple variable names (backwards compatibility)
|
|
162
216
|
if (!/[.\[(]/.test(path)) {
|
|
217
|
+
if (FORBIDDEN_KEYS.has(path)) {
|
|
218
|
+
return undefined
|
|
219
|
+
}
|
|
163
220
|
return data[path]
|
|
164
221
|
}
|
|
165
222
|
|
|
@@ -173,6 +230,10 @@ function resolvePath(data, path) {
|
|
|
173
230
|
if (current === null || current === undefined) {
|
|
174
231
|
return undefined
|
|
175
232
|
}
|
|
233
|
+
// Block prototype-chain access at every hop
|
|
234
|
+
if (FORBIDDEN_KEYS.has(segment.name)) {
|
|
235
|
+
return undefined
|
|
236
|
+
}
|
|
176
237
|
if (segment.type === "method") {
|
|
177
238
|
if (
|
|
178
239
|
!SAFE_METHODS.has(segment.name) ||
|
|
@@ -180,8 +241,12 @@ function resolvePath(data, path) {
|
|
|
180
241
|
) {
|
|
181
242
|
return undefined
|
|
182
243
|
}
|
|
244
|
+
// Path arguments are full expressions, e.g. "slice(0, n - 1)"
|
|
245
|
+
const args = segment.args.map((arg) =>
|
|
246
|
+
arg.type === "path" ? resolveExpression(data, arg.path) : arg.value,
|
|
247
|
+
)
|
|
183
248
|
try {
|
|
184
|
-
current = current[segment.name](...
|
|
249
|
+
current = current[segment.name](...args)
|
|
185
250
|
} catch (error) {
|
|
186
251
|
return undefined
|
|
187
252
|
}
|
|
@@ -272,46 +337,166 @@ function splitArrayElements(inner) {
|
|
|
272
337
|
}
|
|
273
338
|
|
|
274
339
|
/**
|
|
275
|
-
* Split an expression on top-level ??
|
|
340
|
+
* Split an expression on a top-level two-character operator (?? or ||)
|
|
276
341
|
* Respects quoted strings, brackets and parentheses
|
|
277
342
|
* @param {string} expression - The expression to split
|
|
343
|
+
* @param {string} char - The operator character, repeated twice ("?" or "|")
|
|
344
|
+
* @returns {Array<string>} - Operands (a single element when there is no operator)
|
|
345
|
+
*/
|
|
346
|
+
function splitDoubleOperator(expression, char) {
|
|
347
|
+
const operands = []
|
|
348
|
+
let current = ""
|
|
349
|
+
let depth = 0
|
|
350
|
+
let quote = null
|
|
351
|
+
let i = 0
|
|
352
|
+
|
|
353
|
+
while (i < expression.length) {
|
|
354
|
+
const c = expression[i]
|
|
355
|
+
if (quote) {
|
|
356
|
+
current += c
|
|
357
|
+
if (c === quote) quote = null
|
|
358
|
+
i++
|
|
359
|
+
continue
|
|
360
|
+
}
|
|
361
|
+
if (c === '"' || c === "'") {
|
|
362
|
+
quote = c
|
|
363
|
+
current += c
|
|
364
|
+
i++
|
|
365
|
+
continue
|
|
366
|
+
}
|
|
367
|
+
if (c === "[" || c === "(") depth++
|
|
368
|
+
if (c === "]" || c === ")") depth--
|
|
369
|
+
if (depth === 0 && c === char && expression[i + 1] === char) {
|
|
370
|
+
operands.push(current)
|
|
371
|
+
current = ""
|
|
372
|
+
i += 2
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
375
|
+
current += c
|
|
376
|
+
i++
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
operands.push(current)
|
|
380
|
+
return operands
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Split an expression on top-level ?? operators
|
|
385
|
+
* @param {string} expression - The expression to split
|
|
278
386
|
* @returns {Array<string>} - Operands (a single element when there is no ??)
|
|
279
387
|
*/
|
|
280
388
|
function splitNullishCoalescing(expression) {
|
|
389
|
+
return splitDoubleOperator(expression, "?")
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Split an expression on top-level || operators
|
|
394
|
+
* @param {string} expression - The expression to split
|
|
395
|
+
* @returns {Array<string>} - Operands (a single element when there is no ||)
|
|
396
|
+
*/
|
|
397
|
+
function splitLogicalOr(expression) {
|
|
398
|
+
return splitDoubleOperator(expression, "|")
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Split an expression on top-level && operators
|
|
403
|
+
* @param {string} expression - The expression to split
|
|
404
|
+
* @returns {Array<string>} - Operands (a single element when there is no &&)
|
|
405
|
+
*/
|
|
406
|
+
function splitLogicalAnd(expression) {
|
|
407
|
+
return splitDoubleOperator(expression, "&")
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Split an expression on top-level arithmetic operators from the given set
|
|
412
|
+
* Operators must be surrounded by spaces ("i + 1", not "i+1") to stay
|
|
413
|
+
* unambiguous with negative literals and identifiers
|
|
414
|
+
* Respects quoted strings, brackets and parentheses
|
|
415
|
+
* @param {string} expression - The expression to split
|
|
416
|
+
* @param {Array<string>} characters - Operator characters, e.g. ["+", "-"]
|
|
417
|
+
* @returns {{operands: Array<string>, operators: Array<string>}}
|
|
418
|
+
*/
|
|
419
|
+
function splitArithmetic(expression, characters) {
|
|
281
420
|
const operands = []
|
|
421
|
+
const operators = []
|
|
282
422
|
let current = ""
|
|
283
423
|
let depth = 0
|
|
284
424
|
let quote = null
|
|
285
425
|
let i = 0
|
|
286
426
|
|
|
287
427
|
while (i < expression.length) {
|
|
288
|
-
const
|
|
428
|
+
const c = expression[i]
|
|
289
429
|
if (quote) {
|
|
290
|
-
current +=
|
|
291
|
-
if (
|
|
430
|
+
current += c
|
|
431
|
+
if (c === quote) quote = null
|
|
292
432
|
i++
|
|
293
433
|
continue
|
|
294
434
|
}
|
|
295
|
-
if (
|
|
296
|
-
quote =
|
|
297
|
-
current +=
|
|
435
|
+
if (c === '"' || c === "'") {
|
|
436
|
+
quote = c
|
|
437
|
+
current += c
|
|
298
438
|
i++
|
|
299
439
|
continue
|
|
300
440
|
}
|
|
301
|
-
if (
|
|
302
|
-
if (
|
|
303
|
-
if (
|
|
441
|
+
if (c === "[" || c === "(") depth++
|
|
442
|
+
if (c === "]" || c === ")") depth--
|
|
443
|
+
if (
|
|
444
|
+
depth === 0 &&
|
|
445
|
+
characters.includes(c) &&
|
|
446
|
+
expression[i - 1] === " " &&
|
|
447
|
+
expression[i + 1] === " "
|
|
448
|
+
) {
|
|
304
449
|
operands.push(current)
|
|
450
|
+
operators.push(c)
|
|
305
451
|
current = ""
|
|
306
452
|
i += 2
|
|
307
453
|
continue
|
|
308
454
|
}
|
|
309
|
-
current +=
|
|
455
|
+
current += c
|
|
310
456
|
i++
|
|
311
457
|
}
|
|
312
458
|
|
|
313
459
|
operands.push(current)
|
|
314
|
-
return operands
|
|
460
|
+
return { operands, operators }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Apply a single arithmetic operator with JS semantics
|
|
465
|
+
* "+" works for numbers and strings (concatenation), the rest require numbers
|
|
466
|
+
* Nullish operands and NaN results resolve to undefined
|
|
467
|
+
*/
|
|
468
|
+
function applyArithmetic(left, operator, right) {
|
|
469
|
+
if (
|
|
470
|
+
left === undefined ||
|
|
471
|
+
left === null ||
|
|
472
|
+
right === undefined ||
|
|
473
|
+
right === null
|
|
474
|
+
) {
|
|
475
|
+
return undefined
|
|
476
|
+
}
|
|
477
|
+
if (operator === "+") {
|
|
478
|
+
const addable = (value) =>
|
|
479
|
+
typeof value === "number" || typeof value === "string"
|
|
480
|
+
if (!addable(left) || !addable(right)) {
|
|
481
|
+
return undefined
|
|
482
|
+
}
|
|
483
|
+
const result = left + right
|
|
484
|
+
return typeof result === "number" && Number.isNaN(result)
|
|
485
|
+
? undefined
|
|
486
|
+
: result
|
|
487
|
+
}
|
|
488
|
+
if (typeof left !== "number" || typeof right !== "number") {
|
|
489
|
+
return undefined
|
|
490
|
+
}
|
|
491
|
+
let result
|
|
492
|
+
if (operator === "-") {
|
|
493
|
+
result = left - right
|
|
494
|
+
} else if (operator === "*") {
|
|
495
|
+
result = left * right
|
|
496
|
+
} else {
|
|
497
|
+
result = left / right
|
|
498
|
+
}
|
|
499
|
+
return Number.isNaN(result) ? undefined : result
|
|
315
500
|
}
|
|
316
501
|
|
|
317
502
|
/**
|
|
@@ -319,22 +504,52 @@ function splitNullishCoalescing(expression) {
|
|
|
319
504
|
* Supports everything resolvePath does, plus:
|
|
320
505
|
* - array literals whose elements are paths or literals,
|
|
321
506
|
* e.g. "[images[0], images[2]]" or "['a', user.name]"
|
|
507
|
+
* - spread elements inside array literals,
|
|
508
|
+
* e.g. "[...images.slice(0, 2), ...images.slice(4)]"
|
|
322
509
|
* - nullish coalescing with JS semantics (fallback only for null/undefined),
|
|
323
510
|
* e.g. "name ?? 'Guest'" or "nickname ?? name ?? 'Guest'"
|
|
511
|
+
* - logical or with JS semantics (fallback for any falsy value),
|
|
512
|
+
* e.g. "title || 'Untitled'"
|
|
513
|
+
* - logical and with JS semantics (first falsy operand, or the last one),
|
|
514
|
+
* with JS precedence (&& binds tighter than ||)
|
|
515
|
+
* - arithmetic with space-separated + - * / operators and JS precedence,
|
|
516
|
+
* e.g. "i + 1", "items.length - 1", "price * quantity"
|
|
517
|
+
* Mixing ?? with || or && in one expression is a syntax error in JS
|
|
518
|
+
* (parentheses are required) - such expressions resolve to undefined
|
|
324
519
|
* @param {Object} data - The data object to resolve the expression from
|
|
325
520
|
* @param {string} expression - The expression to resolve
|
|
326
521
|
* @returns {*} - The resolved value or undefined
|
|
327
522
|
*/
|
|
328
|
-
function resolveExpression(data, expression) {
|
|
523
|
+
function resolveExpression(data, expression, depth = 0) {
|
|
524
|
+
// Bail out on pathological nesting before the stack is exhausted
|
|
525
|
+
if (depth > MAX_EXPRESSION_DEPTH) {
|
|
526
|
+
return undefined
|
|
527
|
+
}
|
|
528
|
+
// Oversized expressions are never legitimate - reject before scanning
|
|
529
|
+
if (expression.length > MAX_EXPRESSION_LENGTH) {
|
|
530
|
+
return undefined
|
|
531
|
+
}
|
|
329
532
|
const trimmed = expression.trim()
|
|
533
|
+
const recurse = (raw) => resolveExpression(data, raw, depth + 1)
|
|
330
534
|
|
|
331
|
-
const
|
|
332
|
-
|
|
535
|
+
const nullishOperands = splitNullishCoalescing(trimmed)
|
|
536
|
+
const orOperands = splitLogicalOr(trimmed)
|
|
537
|
+
const andOperands = splitLogicalAnd(trimmed)
|
|
538
|
+
|
|
539
|
+
// Mixing ?? with || or && without parentheses is invalid, same as in JS
|
|
540
|
+
if (
|
|
541
|
+
nullishOperands.length > 1 &&
|
|
542
|
+
(orOperands.length > 1 || andOperands.length > 1)
|
|
543
|
+
) {
|
|
544
|
+
return undefined
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (nullishOperands.length > 1) {
|
|
333
548
|
let value
|
|
334
|
-
for (const operand of
|
|
549
|
+
for (const operand of nullishOperands) {
|
|
335
550
|
const raw = operand.trim()
|
|
336
551
|
const literal = parseArgument(raw)
|
|
337
|
-
value = literal ? literal.value :
|
|
552
|
+
value = literal ? literal.value : recurse(raw)
|
|
338
553
|
if (value !== undefined && value !== null) {
|
|
339
554
|
return value
|
|
340
555
|
}
|
|
@@ -342,16 +557,81 @@ function resolveExpression(data, expression) {
|
|
|
342
557
|
return value
|
|
343
558
|
}
|
|
344
559
|
|
|
560
|
+
// || first so && binds tighter (JS precedence) - each || operand may
|
|
561
|
+
// contain && which the recursive call resolves
|
|
562
|
+
if (orOperands.length > 1) {
|
|
563
|
+
let value
|
|
564
|
+
for (const operand of orOperands) {
|
|
565
|
+
const raw = operand.trim()
|
|
566
|
+
const literal = parseArgument(raw)
|
|
567
|
+
value = literal ? literal.value : recurse(raw)
|
|
568
|
+
if (value) {
|
|
569
|
+
return value
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return value
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (andOperands.length > 1) {
|
|
576
|
+
let value
|
|
577
|
+
for (const operand of andOperands) {
|
|
578
|
+
const raw = operand.trim()
|
|
579
|
+
const literal = parseArgument(raw)
|
|
580
|
+
value = literal ? literal.value : recurse(raw)
|
|
581
|
+
if (!value) {
|
|
582
|
+
return value
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return value
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Arithmetic - operators must be surrounded by spaces
|
|
589
|
+
// Sums split first so * and / bind tighter (their operands resolve
|
|
590
|
+
// through the recursive call), chains evaluate left to right
|
|
591
|
+
const sums = splitArithmetic(trimmed, ["+", "-"])
|
|
592
|
+
const arithmetic =
|
|
593
|
+
sums.operators.length > 0 ? sums : splitArithmetic(trimmed, ["*", "/"])
|
|
594
|
+
if (arithmetic.operators.length > 0) {
|
|
595
|
+
const resolveOperand = (raw) => {
|
|
596
|
+
const operand = raw.trim()
|
|
597
|
+
const literal = parseArgument(operand)
|
|
598
|
+
return literal ? literal.value : recurse(operand)
|
|
599
|
+
}
|
|
600
|
+
let value = resolveOperand(arithmetic.operands[0])
|
|
601
|
+
for (let index = 0; index < arithmetic.operators.length; index++) {
|
|
602
|
+
value = applyArithmetic(
|
|
603
|
+
value,
|
|
604
|
+
arithmetic.operators[index],
|
|
605
|
+
resolveOperand(arithmetic.operands[index + 1]),
|
|
606
|
+
)
|
|
607
|
+
if (value === undefined) {
|
|
608
|
+
return undefined
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return value
|
|
612
|
+
}
|
|
613
|
+
|
|
345
614
|
if (isArrayLiteral(trimmed)) {
|
|
346
615
|
const inner = trimmed.substring(1, trimmed.length - 1)
|
|
347
|
-
|
|
616
|
+
const result = []
|
|
617
|
+
for (const element of splitArrayElements(inner)) {
|
|
348
618
|
const raw = element.trim()
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
619
|
+
if (raw.startsWith("...")) {
|
|
620
|
+
// Spread element - flatten the resolved array into the result,
|
|
621
|
+
// e.g. "[...images.slice(0, 2), ...images.slice(4)]"
|
|
622
|
+
// Nullish values disappear, other non-array values are kept as-is
|
|
623
|
+
const value = recurse(raw.substring(3))
|
|
624
|
+
if (Array.isArray(value)) {
|
|
625
|
+
result.push(...value)
|
|
626
|
+
} else if (value !== undefined && value !== null) {
|
|
627
|
+
result.push(value)
|
|
628
|
+
}
|
|
629
|
+
continue
|
|
352
630
|
}
|
|
353
|
-
|
|
354
|
-
|
|
631
|
+
const literal = parseArgument(raw)
|
|
632
|
+
result.push(literal ? literal.value : recurse(raw))
|
|
633
|
+
}
|
|
634
|
+
return result
|
|
355
635
|
}
|
|
356
636
|
|
|
357
637
|
return resolvePath(data, trimmed)
|
|
@@ -449,9 +729,14 @@ module.exports = {
|
|
|
449
729
|
resolveExpression,
|
|
450
730
|
// Parsing internals, exported for the Prose validator
|
|
451
731
|
SAFE_METHODS,
|
|
732
|
+
FORBIDDEN_KEYS,
|
|
733
|
+
MAX_EXPRESSION_LENGTH,
|
|
452
734
|
parseArgument,
|
|
453
735
|
parsePathSegments,
|
|
454
736
|
splitNullishCoalescing,
|
|
737
|
+
splitLogicalOr,
|
|
738
|
+
splitLogicalAnd,
|
|
739
|
+
splitArithmetic,
|
|
455
740
|
isArrayLiteral,
|
|
456
741
|
splitArrayElements,
|
|
457
742
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Characters that do not decompose to ASCII via NFD normalization
|
|
2
|
+
const CHARACTER_MAP = {
|
|
3
|
+
ł: "l",
|
|
4
|
+
Ł: "L",
|
|
5
|
+
ø: "o",
|
|
6
|
+
Ø: "O",
|
|
7
|
+
đ: "d",
|
|
8
|
+
Đ: "D",
|
|
9
|
+
ß: "ss",
|
|
10
|
+
æ: "ae",
|
|
11
|
+
Æ: "AE",
|
|
12
|
+
œ: "oe",
|
|
13
|
+
Œ: "OE",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Combining diacritical marks left over after NFD normalization
|
|
17
|
+
const DIACRITICS_REGEXP = /[̀-ͯ]/g
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert a heading text into a URL-friendly anchor slug
|
|
21
|
+
* "Mój tytuł" -> "moj-tytul"
|
|
22
|
+
* @param {string} text - The text to slugify
|
|
23
|
+
* @returns {string} - The slug (may be empty for non-alphanumeric input)
|
|
24
|
+
*/
|
|
25
|
+
function slugify(text) {
|
|
26
|
+
if (!text || typeof text !== "string") {
|
|
27
|
+
return ""
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return text
|
|
31
|
+
.replace(/[łŁøØđĐßæÆœŒ]/g, (character) => CHARACTER_MAP[character])
|
|
32
|
+
.normalize("NFD")
|
|
33
|
+
.replace(DIACRITICS_REGEXP, "")
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
36
|
+
.replace(/^-+|-+$/g, "")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { slugify }
|