@pieceful/ravel-core 0.1.1 → 0.3.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/LICENSE +15 -3
- package/README.md +10 -4
- package/package.json +26 -7
- package/src/directive-syntax.js +354 -0
- package/src/directives.d.ts +2 -2
- package/src/directives.js +15 -2
- package/src/index.d.ts +115 -6
- package/src/index.js +417 -52
- package/src/live.js +406 -0
package/src/index.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
const componentPattern = /^[a-z][a-z0-9-]*$/;
|
|
2
|
+
const chunkPathPattern = /^[a-z][a-z0-9-]*(?:\/(?:[a-z][a-z0-9-]*)?)*$/;
|
|
3
|
+
const relativeChunkPathPattern = /^(?:\.\.?)(?:\/(?:\.\.?|[a-z][a-z0-9-]*))*$/;
|
|
2
4
|
|
|
3
5
|
export { directiveKinds, compose, append, newline, pipe, pass, createDirective, aliasDirective } from "./directives.js";
|
|
6
|
+
export { parseRavelDirectiveBlock } from "./directive-syntax.js";
|
|
7
|
+
export {
|
|
8
|
+
cloneRavelValue,
|
|
9
|
+
executeLiveProgram,
|
|
10
|
+
planLiveExecutions,
|
|
11
|
+
ravelValueIssue,
|
|
12
|
+
serializeRavelValue
|
|
13
|
+
} from "./live.js";
|
|
4
14
|
|
|
5
15
|
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
6
16
|
|
|
@@ -167,18 +177,32 @@ const replaceMappedOnce = (value, search, replacement) => {
|
|
|
167
177
|
);
|
|
168
178
|
};
|
|
169
179
|
|
|
180
|
+
// Parsing asks for many positions in the same chunk body, so keep the newline
|
|
181
|
+
// offsets of the most recent text and binary-search them instead of rescanning.
|
|
182
|
+
const lineIndexCache = new Map();
|
|
183
|
+
const lineStartsOf = (text) => {
|
|
184
|
+
let starts = lineIndexCache.get(text);
|
|
185
|
+
if (starts) return starts;
|
|
186
|
+
starts = [];
|
|
187
|
+
for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) starts.push(i + 1);
|
|
188
|
+
// A handful of entries covers a chunk body and the expressions inside it.
|
|
189
|
+
if (lineIndexCache.size >= 16) lineIndexCache.delete(lineIndexCache.keys().next().value);
|
|
190
|
+
lineIndexCache.set(text, starts);
|
|
191
|
+
return starts;
|
|
192
|
+
};
|
|
193
|
+
|
|
170
194
|
const advance = (start, text, index) => {
|
|
171
|
-
|
|
172
|
-
let
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
column += 1;
|
|
179
|
-
}
|
|
195
|
+
const starts = lineStartsOf(text);
|
|
196
|
+
let low = 0;
|
|
197
|
+
let high = starts.length;
|
|
198
|
+
while (low < high) {
|
|
199
|
+
const middle = (low + high) >> 1;
|
|
200
|
+
if (starts[middle] <= index) low = middle + 1;
|
|
201
|
+
else high = middle;
|
|
180
202
|
}
|
|
181
|
-
return
|
|
203
|
+
return low === 0
|
|
204
|
+
? { line: start.line, column: start.column + index, offset: start.offset + index }
|
|
205
|
+
: { line: start.line + low, column: index - starts[low - 1], offset: start.offset + index };
|
|
182
206
|
};
|
|
183
207
|
|
|
184
208
|
const span = (source, text, start, end) => ({
|
|
@@ -198,6 +222,7 @@ const diagnostic = (code, message, source, related = []) => ({
|
|
|
198
222
|
});
|
|
199
223
|
|
|
200
224
|
const validComponent = (value) => value === null || (typeof value === "string" && componentPattern.test(value));
|
|
225
|
+
const validChunkPath = (value) => value === null || (typeof value === "string" && chunkPathPattern.test(value));
|
|
201
226
|
|
|
202
227
|
export const formatChunkId = (identity) => {
|
|
203
228
|
const prefix = identity.document === null ? "" : identity.document + "::";
|
|
@@ -223,7 +248,8 @@ export const parseChunkId = (input, { reference = false } = {}) => {
|
|
|
223
248
|
|
|
224
249
|
let type = null;
|
|
225
250
|
const typeIndex = remainder.lastIndexOf(".");
|
|
226
|
-
|
|
251
|
+
const pathIndex = remainder.lastIndexOf("/");
|
|
252
|
+
if (typeIndex > pathIndex) {
|
|
227
253
|
type = remainder.slice(typeIndex + 1);
|
|
228
254
|
remainder = remainder.slice(0, typeIndex);
|
|
229
255
|
if (!componentPattern.test(type)) return null;
|
|
@@ -236,11 +262,16 @@ export const parseChunkId = (input, { reference = false } = {}) => {
|
|
|
236
262
|
chunk = remainder.slice(0, minorIndex);
|
|
237
263
|
minor = remainder.slice(minorIndex + 1);
|
|
238
264
|
if (!componentPattern.test(minor)) return null;
|
|
265
|
+
if (chunk.endsWith("/") && /^\.{1,2}(?:\/|$)/.test(chunk)) chunk = chunk.slice(0, -1);
|
|
239
266
|
}
|
|
240
267
|
|
|
241
268
|
if (chunk === "") chunk = null;
|
|
242
|
-
|
|
269
|
+
const relativePath = reference && typeof chunk === "string" && relativeChunkPathPattern.test(chunk)
|
|
270
|
+
? chunk
|
|
271
|
+
: null;
|
|
272
|
+
if (relativePath === null && !validChunkPath(chunk)) return null;
|
|
243
273
|
if (!validComponent(document) || !validComponent(minor) || !validComponent(type)) return null;
|
|
274
|
+
if (relativePath !== null && explicitDocument) return null;
|
|
244
275
|
|
|
245
276
|
if (!reference && document === null && chunk === null) return null;
|
|
246
277
|
if (!reference && document === null && chunk !== null && input.includes("::")) return null;
|
|
@@ -253,7 +284,14 @@ export const parseChunkId = (input, { reference = false } = {}) => {
|
|
|
253
284
|
}
|
|
254
285
|
if (reference && !explicitDocument && chunk === null && minor === null && type === null) return null;
|
|
255
286
|
|
|
256
|
-
return {
|
|
287
|
+
return {
|
|
288
|
+
document,
|
|
289
|
+
chunk,
|
|
290
|
+
minor,
|
|
291
|
+
type,
|
|
292
|
+
explicitDocument,
|
|
293
|
+
...(relativePath === null ? {} : { relativePath })
|
|
294
|
+
};
|
|
257
295
|
};
|
|
258
296
|
|
|
259
297
|
const validateIdentity = (identity) => {
|
|
@@ -267,7 +305,7 @@ const validateIdentity = (identity) => {
|
|
|
267
305
|
type: identity.type,
|
|
268
306
|
explicitDocument: identity.document !== null
|
|
269
307
|
};
|
|
270
|
-
if (!validComponent(result.document) || !
|
|
308
|
+
if (!validComponent(result.document) || !validChunkPath(result.chunk) ||
|
|
271
309
|
!validComponent(result.minor) || !validComponent(result.type)) {
|
|
272
310
|
return null;
|
|
273
311
|
}
|
|
@@ -358,9 +396,10 @@ const parseEmitSuffix = (value) => {
|
|
|
358
396
|
return { minor, type, inheritMinor: false };
|
|
359
397
|
};
|
|
360
398
|
|
|
361
|
-
|
|
399
|
+
// `text` is the string that `expressionOffset` and `part` offsets index into.
|
|
400
|
+
const parsePipeStep = (part, text, source, expressionOffset, diagnostics) => {
|
|
362
401
|
const match = /^([a-z][a-z0-9-]*)\s*(?:\((.*)\))?$/s.exec(part.value);
|
|
363
|
-
const location = span(source,
|
|
402
|
+
const location = span(source, text, expressionOffset + part.start, expressionOffset + part.end);
|
|
364
403
|
if (!match) {
|
|
365
404
|
diagnostics.push(diagnostic("RV110", "Malformed pipeline step: " + part.value, location));
|
|
366
405
|
return null;
|
|
@@ -406,7 +445,7 @@ const parsePipeStep = (part, expression, source, expressionOffset, diagnostics)
|
|
|
406
445
|
diagnostics.push(diagnostic("RV121", "ch requires one quoted chunk expression.", location));
|
|
407
446
|
return null;
|
|
408
447
|
}
|
|
409
|
-
const parsed = parseExpression(argumentsValue[0], source, expressionOffset + part.start, diagnostics, { allowDelay: false });
|
|
448
|
+
const parsed = parseExpression(argumentsValue[0], source, expressionOffset + part.start, diagnostics, { allowDelay: false, text });
|
|
410
449
|
if (!parsed) return null;
|
|
411
450
|
return { type: "chunk", expression: argumentsValue[0], value: parsed, source: location };
|
|
412
451
|
}
|
|
@@ -414,7 +453,7 @@ const parsePipeStep = (part, expression, source, expressionOffset, diagnostics)
|
|
|
414
453
|
const argumentsParsed = rawArguments.map((argument) => {
|
|
415
454
|
const value = parseValue(argument.value);
|
|
416
455
|
if (typeof value !== "undefined") return value;
|
|
417
|
-
const nested = parsePipeStep({ value: argument.value, start: part.start + argument.start, end: part.start + argument.end },
|
|
456
|
+
const nested = parsePipeStep({ value: argument.value, start: part.start + argument.start, end: part.start + argument.end }, text, source, expressionOffset, diagnostics);
|
|
418
457
|
return nested?.type === "text" || nested?.type === "chunk"
|
|
419
458
|
? { kind: "ravel-command-argument", command: nested }
|
|
420
459
|
: undefined;
|
|
@@ -426,12 +465,14 @@ const parsePipeStep = (part, expression, source, expressionOffset, diagnostics)
|
|
|
426
465
|
return { type: name === "delay" ? "delay" : "transform", name, arguments: argumentsParsed, source: location };
|
|
427
466
|
};
|
|
428
467
|
|
|
429
|
-
|
|
468
|
+
// `text` is the chunk body that `expressionOffset` indexes into; positions
|
|
469
|
+
// must be computed against it, not against the expression alone.
|
|
470
|
+
const parseExpression = (expression, source, expressionOffset, diagnostics, { allowDelay = true, text = expression } = {}) => {
|
|
430
471
|
const parts = splitTopLevel(expression, "|");
|
|
431
472
|
const reference = parts.shift();
|
|
432
473
|
const target = parseChunkId(reference?.value, { reference: true });
|
|
433
474
|
const pipeline = parts
|
|
434
|
-
.map((part) => parsePipeStep(part,
|
|
475
|
+
.map((part) => parsePipeStep(part, text, source, expressionOffset, diagnostics))
|
|
435
476
|
.filter(Boolean);
|
|
436
477
|
if (!reference || !target) {
|
|
437
478
|
const delay = pipeline.length === 1 && pipeline[0].type === "delay" ? pipeline[0] : null;
|
|
@@ -450,7 +491,7 @@ const parseExpression = (expression, source, expressionOffset, diagnostics, { al
|
|
|
450
491
|
expression: typeof value === "string" ? value : command.expression ?? command.value,
|
|
451
492
|
phase,
|
|
452
493
|
safeSymbol,
|
|
453
|
-
source: span(source,
|
|
494
|
+
source: span(source, text, expressionOffset, expressionOffset + expression.length)
|
|
454
495
|
};
|
|
455
496
|
}
|
|
456
497
|
if (reference?.value === "" && pipeline.length && pipeline[0].type !== "delay" &&
|
|
@@ -459,13 +500,13 @@ const parseExpression = (expression, source, expressionOffset, diagnostics, { al
|
|
|
459
500
|
diagnostics.push(diagnostic("RV121", "delay may only appear as the sole command in _\"|delay(...)\".", pipeline.find((step) => step.type === "delay").source));
|
|
460
501
|
return null;
|
|
461
502
|
}
|
|
462
|
-
return { type: "pipeline", pipeline, source: span(source,
|
|
503
|
+
return { type: "pipeline", pipeline, source: span(source, text, expressionOffset, expressionOffset + expression.length) };
|
|
463
504
|
}
|
|
464
505
|
if (pipeline.some((step) => step.type === "delay")) {
|
|
465
506
|
diagnostics.push(diagnostic("RV121", "delay may only appear as the sole command in _\"|delay(...)\".", pipeline.find((step) => step.type === "delay").source));
|
|
466
507
|
return null;
|
|
467
508
|
}
|
|
468
|
-
diagnostics.push(diagnostic("RV110", "Malformed chunk reference: " + (reference?.value ?? ""), span(source,
|
|
509
|
+
diagnostics.push(diagnostic("RV110", "Malformed chunk reference: " + (reference?.value ?? ""), span(source, text, expressionOffset, expressionOffset + expression.length)));
|
|
469
510
|
return null;
|
|
470
511
|
}
|
|
471
512
|
|
|
@@ -479,15 +520,39 @@ const parseExpression = (expression, source, expressionOffset, diagnostics, { al
|
|
|
479
520
|
reference: reference.value,
|
|
480
521
|
target,
|
|
481
522
|
pipeline,
|
|
482
|
-
source: span(source,
|
|
523
|
+
source: span(source, text, expressionOffset, expressionOffset + expression.length)
|
|
483
524
|
};
|
|
484
525
|
};
|
|
485
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Parse a definition-time transform pipeline without executing it.
|
|
529
|
+
* Adapters use this entry point so every markup dialect shares one grammar.
|
|
530
|
+
*/
|
|
531
|
+
export const parseDefinitionPipeline = (text, source) => {
|
|
532
|
+
const diagnostics = [];
|
|
533
|
+
if (typeof text !== "string" || !text.trim()) return { pipeline: [], diagnostics };
|
|
534
|
+
const expression = text.trim();
|
|
535
|
+
const pipeline = [];
|
|
536
|
+
for (const part of splitTopLevel(expression, "|")) {
|
|
537
|
+
const step = parsePipeStep(part, expression, source, 0, diagnostics);
|
|
538
|
+
if (!step) continue;
|
|
539
|
+
if (step.type !== "transform") {
|
|
540
|
+
diagnostics.push(diagnostic("RV120", "Definition pipelines accept transform calls only.", step.source ?? source));
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
pipeline.push({ name: step.name, arguments: step.arguments });
|
|
544
|
+
}
|
|
545
|
+
return { pipeline, diagnostics };
|
|
546
|
+
};
|
|
547
|
+
|
|
486
548
|
// An embedded reference inherits the indentation of its containing source line
|
|
487
549
|
// on continuation lines. The first line is already positioned by the literal
|
|
488
550
|
// text before the reference, so it deliberately remains untouched.
|
|
489
551
|
const continuationIndentAt = (body, index) => {
|
|
490
|
-
|
|
552
|
+
// Walk back only to the start of this line; lastIndexOf for an absent "\r"
|
|
553
|
+
// would rescan the whole body for every reference.
|
|
554
|
+
let lineStart = index;
|
|
555
|
+
while (lineStart > 0 && body[lineStart - 1] !== "\n" && body[lineStart - 1] !== "\r") lineStart -= 1;
|
|
491
556
|
return /^[\t ]*/.exec(body.slice(lineStart, index))[0];
|
|
492
557
|
};
|
|
493
558
|
|
|
@@ -513,18 +578,130 @@ const applyContinuationIndentMapped = (value, indentation, source, chunk) => {
|
|
|
513
578
|
return concatMapped(...parts);
|
|
514
579
|
};
|
|
515
580
|
|
|
516
|
-
|
|
581
|
+
const firstUnescapedPipe = (value) => {
|
|
582
|
+
let escaped = false;
|
|
583
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
584
|
+
if (escaped) escaped = false;
|
|
585
|
+
else if (value[index] === "\\") escaped = true;
|
|
586
|
+
else if (value[index] === "|") return index;
|
|
587
|
+
}
|
|
588
|
+
return -1;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const nowebExpression = (value, options) => {
|
|
592
|
+
const pipe = options.nowebPlus ? firstUnescapedPipe(value) : -1;
|
|
593
|
+
const authoredName = (pipe === -1 ? value : value.slice(0, pipe)).trim().replace(/\\\|/g, "|");
|
|
594
|
+
const canonicalName = options.referenceAliases?.[authoredName] ?? authoredName;
|
|
595
|
+
return pipe === -1
|
|
596
|
+
? canonicalName
|
|
597
|
+
: canonicalName + " | " + value.slice(pipe + 1).trim();
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Recognize a noweb-style `<<name>>` reference starting at `index`.
|
|
602
|
+
*
|
|
603
|
+
* A reference must close on the same line and its name must be non-empty
|
|
604
|
+
* without leading or trailing whitespace, so shift operators such as
|
|
605
|
+
* `std::cout << x` or `a << 2; b >> 1` remain ordinary code. Returns the
|
|
606
|
+
* authored text and the offset of the closing `>>`, or null.
|
|
607
|
+
*/
|
|
608
|
+
const countedReferencePattern = /\\([1-9][0-9]*)_(["'`])/y;
|
|
609
|
+
|
|
610
|
+
export const nowebReferenceAt = (body, index) => {
|
|
611
|
+
if (!body.startsWith("<<", index)) return null;
|
|
612
|
+
const lineEnd = body.indexOf("\n", index + 2);
|
|
613
|
+
const end = body.indexOf(">>", index + 2);
|
|
614
|
+
if (end === -1 || (lineEnd !== -1 && end > lineEnd)) return null;
|
|
615
|
+
const authored = body.slice(index + 2, end);
|
|
616
|
+
if (!authored || authored !== authored.trim() || authored.startsWith("<")) return null;
|
|
617
|
+
return { authored, end };
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export const parseChunk = (body, source, options = {}) => {
|
|
517
621
|
const nodes = [];
|
|
518
622
|
const diagnostics = [];
|
|
519
623
|
let literalStart = 0;
|
|
520
624
|
let index = 0;
|
|
521
625
|
|
|
522
626
|
while (index < body.length) {
|
|
627
|
+
if (options.nowebReferences && body.startsWith("@<<", index)) {
|
|
628
|
+
// `@<<` is noweb's escape for a literal `<<`; drop the `@`.
|
|
629
|
+
if (literalStart < index) {
|
|
630
|
+
nodes.push({ type: "literal", value: body.slice(literalStart, index), source: span(source, body, literalStart, index) });
|
|
631
|
+
}
|
|
632
|
+
literalStart = index + 1;
|
|
633
|
+
index += 3;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const noweb = options.nowebReferences ? nowebReferenceAt(body, index) : null;
|
|
637
|
+
if (noweb) {
|
|
638
|
+
const { end, authored } = noweb;
|
|
639
|
+
if (literalStart < index) {
|
|
640
|
+
nodes.push({ type: "literal", value: body.slice(literalStart, index), source: span(source, body, literalStart, index) });
|
|
641
|
+
}
|
|
642
|
+
const expression = nowebExpression(authored, options);
|
|
643
|
+
const parsed = parseExpression(expression, source, index + 2, diagnostics, { text: body });
|
|
644
|
+
if (parsed) {
|
|
645
|
+
nodes.push({
|
|
646
|
+
...parsed,
|
|
647
|
+
authoredReference: authored,
|
|
648
|
+
source: span(source, body, index + 2, end),
|
|
649
|
+
continuationIndent: continuationIndentAt(body, index)
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
index = end + 2;
|
|
653
|
+
literalStart = index;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
let counted = null;
|
|
657
|
+
if (options.underscoreReferences !== false && body[index] === "\\") {
|
|
658
|
+
countedReferencePattern.lastIndex = index;
|
|
659
|
+
counted = countedReferencePattern.exec(body);
|
|
660
|
+
}
|
|
661
|
+
if (counted) {
|
|
662
|
+
const quote = counted[2];
|
|
663
|
+
const expressionStart = index + counted[0].length;
|
|
664
|
+
let end = expressionStart;
|
|
665
|
+
let escaped = false;
|
|
666
|
+
while (end < body.length) {
|
|
667
|
+
if (escaped) escaped = false;
|
|
668
|
+
else if (body[end] === "\\") escaped = true;
|
|
669
|
+
else if (body[end] === quote) break;
|
|
670
|
+
end += 1;
|
|
671
|
+
}
|
|
672
|
+
if (end >= body.length) {
|
|
673
|
+
diagnostics.push(diagnostic("RV110", "Unterminated counted chunk reference.", span(source, body, index, body.length)));
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
if (literalStart < index) {
|
|
677
|
+
nodes.push({ type: "literal", value: body.slice(literalStart, index), source: span(source, body, literalStart, index) });
|
|
678
|
+
}
|
|
679
|
+
const expression = body.slice(expressionStart, end);
|
|
680
|
+
const parsed = parseExpression(expression, source, expressionStart, diagnostics, { allowDelay: false, text: body });
|
|
681
|
+
if (parsed) {
|
|
682
|
+
nodes.push({
|
|
683
|
+
type: "delay",
|
|
684
|
+
value: {
|
|
685
|
+
kind: "ravel-command-argument",
|
|
686
|
+
command: { type: "chunk", expression, value: parsed, source: span(source, body, expressionStart, end) }
|
|
687
|
+
},
|
|
688
|
+
expression,
|
|
689
|
+
phase: Number(counted[1]),
|
|
690
|
+
safeSymbol: undefined,
|
|
691
|
+
source: span(source, body, index, end + 1),
|
|
692
|
+
continuationIndent: continuationIndentAt(body, index)
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
index = end + 1;
|
|
696
|
+
literalStart = index;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
523
699
|
if (body[index] === "\\" && body[index + 1] === "_" && ["\"", "'", "`"].includes(body[index + 2])) {
|
|
524
700
|
index += 3;
|
|
525
701
|
continue;
|
|
526
702
|
}
|
|
527
|
-
if (
|
|
703
|
+
if (options.underscoreReferences === false ||
|
|
704
|
+
body[index] !== "_" || !["\"", "'", "`"].includes(body[index + 1])) {
|
|
528
705
|
index += 1;
|
|
529
706
|
continue;
|
|
530
707
|
}
|
|
@@ -552,7 +729,7 @@ export const parseChunk = (body, source) => {
|
|
|
552
729
|
nodes.push({ type: "literal", value: body.slice(literalStart, index), source: span(source, body, literalStart, index) });
|
|
553
730
|
}
|
|
554
731
|
const expression = body.slice(index + 2, end);
|
|
555
|
-
const parsed = parseExpression(expression, source, index + 2, diagnostics);
|
|
732
|
+
const parsed = parseExpression(expression, source, index + 2, diagnostics, { text: body });
|
|
556
733
|
if (parsed) nodes.push({ ...parsed, continuationIndent: continuationIndentAt(body, index) });
|
|
557
734
|
index = end + 1;
|
|
558
735
|
literalStart = index;
|
|
@@ -565,8 +742,16 @@ export const parseChunk = (body, source) => {
|
|
|
565
742
|
};
|
|
566
743
|
|
|
567
744
|
const parseChunkFragments = (raw) => {
|
|
568
|
-
const
|
|
569
|
-
|
|
745
|
+
const referenceSyntax = raw.metadata?.data?.ravel?.referenceSyntax;
|
|
746
|
+
const parsed = parseChunk(raw.body, raw.source, referenceSyntax && typeof referenceSyntax === "object"
|
|
747
|
+
? {
|
|
748
|
+
nowebReferences: referenceSyntax.noweb === true,
|
|
749
|
+
nowebPlus: referenceSyntax.dialect === "noweb-plus",
|
|
750
|
+
underscoreReferences: referenceSyntax.underscore !== false,
|
|
751
|
+
referenceAliases: referenceSyntax.aliases
|
|
752
|
+
}
|
|
753
|
+
: {});
|
|
754
|
+
if (!Array.isArray(raw.fragments) || raw.fragments.length === 0 ||
|
|
570
755
|
raw.fragments.some((fragment) => typeof fragment?.body !== "string" || !fragment.source) ||
|
|
571
756
|
raw.fragments.map((fragment) => fragment.body).join("") !== raw.body) {
|
|
572
757
|
return parsed;
|
|
@@ -583,16 +768,41 @@ const parseChunkFragments = (raw) => {
|
|
|
583
768
|
start: source.range.start.offset - syntheticStart,
|
|
584
769
|
end: source.range.end.offset - syntheticStart
|
|
585
770
|
});
|
|
771
|
+
const fragmentSlice = (fragment, start, end) =>
|
|
772
|
+
fragment.precision === "coarse"
|
|
773
|
+
? fragment.source
|
|
774
|
+
: span(
|
|
775
|
+
fragment.source,
|
|
776
|
+
fragment.body,
|
|
777
|
+
start - fragment.start,
|
|
778
|
+
end - fragment.start
|
|
779
|
+
);
|
|
586
780
|
const remapLocation = (source) => {
|
|
587
781
|
const range = relativeRange(source);
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
return span(
|
|
591
|
-
fragment.source,
|
|
592
|
-
fragment.body,
|
|
593
|
-
range.start - fragment.start,
|
|
594
|
-
range.end - fragment.start
|
|
782
|
+
const overlapping = fragments.filter((entry) =>
|
|
783
|
+
range.start < entry.end && range.end > entry.start
|
|
595
784
|
);
|
|
785
|
+
if (!overlapping.length) return source;
|
|
786
|
+
const first = overlapping[0];
|
|
787
|
+
const last = overlapping.at(-1);
|
|
788
|
+
const firstSlice = fragmentSlice(
|
|
789
|
+
first,
|
|
790
|
+
Math.max(range.start, first.start),
|
|
791
|
+
Math.min(range.end, first.end)
|
|
792
|
+
);
|
|
793
|
+
const lastSlice = fragmentSlice(
|
|
794
|
+
last,
|
|
795
|
+
Math.max(range.start, last.start),
|
|
796
|
+
Math.min(range.end, last.end)
|
|
797
|
+
);
|
|
798
|
+
if (firstSlice.uri !== lastSlice.uri) return firstSlice;
|
|
799
|
+
return {
|
|
800
|
+
uri: firstSlice.uri,
|
|
801
|
+
range: {
|
|
802
|
+
start: firstSlice.range.start,
|
|
803
|
+
end: lastSlice.range.end
|
|
804
|
+
}
|
|
805
|
+
};
|
|
596
806
|
};
|
|
597
807
|
const remapSources = (value) => {
|
|
598
808
|
if (!value || typeof value !== "object") return value;
|
|
@@ -617,7 +827,8 @@ const parseChunkFragments = (raw) => {
|
|
|
617
827
|
nodes.push({
|
|
618
828
|
type: "literal",
|
|
619
829
|
value: raw.body.slice(start, end),
|
|
620
|
-
source:
|
|
830
|
+
source: fragmentSlice(fragment, start, end),
|
|
831
|
+
precision: fragment.precision ?? "exact"
|
|
621
832
|
});
|
|
622
833
|
}
|
|
623
834
|
}
|
|
@@ -709,13 +920,40 @@ const applyTransform = (value, step, diagnostics, transforms, context = {}) => {
|
|
|
709
920
|
if (step.name === "quote-reference") {
|
|
710
921
|
return value.replace(/_(["'\x60])/g, "\\_$1");
|
|
711
922
|
}
|
|
923
|
+
if (step.name === "jsontext") {
|
|
924
|
+
if (step.arguments.length > 1 ||
|
|
925
|
+
(step.arguments.length === 1 && typeof step.arguments[0] !== "string")) {
|
|
926
|
+
diagnostics.push(diagnostic("RV120", "jsontext accepts no arguments or one string key.", step.source));
|
|
927
|
+
return value;
|
|
928
|
+
}
|
|
929
|
+
let parsed;
|
|
930
|
+
try {
|
|
931
|
+
parsed = JSON.parse(value);
|
|
932
|
+
} catch {
|
|
933
|
+
diagnostics.push(diagnostic("RV120", "jsontext requires a live-result JSON value.", step.source));
|
|
934
|
+
return value;
|
|
935
|
+
}
|
|
936
|
+
if (step.arguments.length === 0) return JSON.stringify(parsed);
|
|
937
|
+
|
|
938
|
+
const key = step.arguments[0];
|
|
939
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) ||
|
|
940
|
+
!Object.hasOwn(parsed, key)) {
|
|
941
|
+
diagnostics.push(diagnostic("RV120", "jsontext could not find object key " + JSON.stringify(key) + ".", step.source));
|
|
942
|
+
return value;
|
|
943
|
+
}
|
|
944
|
+
const selected = parsed[key];
|
|
945
|
+
return typeof selected === "string" ? selected : JSON.stringify(selected);
|
|
946
|
+
}
|
|
712
947
|
|
|
713
948
|
const transform = customTransform(transforms, step.name);
|
|
714
949
|
if (typeof transform === "function") {
|
|
715
950
|
try {
|
|
716
|
-
|
|
951
|
+
// `parts: true` invites a transform to describe which output came from
|
|
952
|
+
// its input or arguments; a plain string result is always accepted.
|
|
953
|
+
const result = transform(value, { ...context, arguments: clone(step.arguments), transform: step.name, parts: true });
|
|
717
954
|
if (typeof result === "string") return result;
|
|
718
|
-
|
|
955
|
+
if (Array.isArray(result?.parts)) return result;
|
|
956
|
+
diagnostics.push(diagnostic("RV121", "Transform " + step.name + " must return a string or { parts }.", step.source));
|
|
719
957
|
} catch (error) {
|
|
720
958
|
diagnostics.push(diagnostic("RV121", "Transform " + step.name + " failed: " + (error?.message ?? String(error)), step.source));
|
|
721
959
|
}
|
|
@@ -797,9 +1035,22 @@ const definitionFromComposeEmission = (owner, capture, emit) => {
|
|
|
797
1035
|
};
|
|
798
1036
|
|
|
799
1037
|
const resolveTarget = (target, owner, definitions) => {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1038
|
+
let withOwnerChunk;
|
|
1039
|
+
if (target.relativePath) {
|
|
1040
|
+
const segments = (owner.identity.chunk ?? "").split("/").filter((segment, index, entries) =>
|
|
1041
|
+
segment || (index > 0 && index < entries.length - 1)
|
|
1042
|
+
);
|
|
1043
|
+
for (const segment of target.relativePath.split("/")) {
|
|
1044
|
+
if (segment === ".") continue;
|
|
1045
|
+
if (segment === "..") segments.pop();
|
|
1046
|
+
else segments.push(segment);
|
|
1047
|
+
}
|
|
1048
|
+
withOwnerChunk = segments.join("/");
|
|
1049
|
+
} else {
|
|
1050
|
+
withOwnerChunk = target.chunk === null && !target.explicitDocument
|
|
1051
|
+
? owner.identity.chunk
|
|
1052
|
+
: target.chunk;
|
|
1053
|
+
}
|
|
803
1054
|
const parts = {
|
|
804
1055
|
chunk: withOwnerChunk,
|
|
805
1056
|
minor: target.minor,
|
|
@@ -829,6 +1080,11 @@ export const transformGraph = (pretransform, options = {}) => {
|
|
|
829
1080
|
const delayTokenCorpus = JSON.stringify(pretransform);
|
|
830
1081
|
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
831
1082
|
const orderedEntries = (value) => Object.entries(value).sort(([left], [right]) => compareText(left, right));
|
|
1083
|
+
const liveExecutions = options.liveResults?.executions ?? options.liveResults;
|
|
1084
|
+
const liveExecution = (id) => liveExecutions instanceof Map
|
|
1085
|
+
? liveExecutions.get(id)
|
|
1086
|
+
: liveExecutions?.[id];
|
|
1087
|
+
const isExecutable = (definition) => definition?.metadata?.data?.ravel?.run === true;
|
|
832
1088
|
|
|
833
1089
|
const normalizeDefinitionPipeline = (steps, source) => {
|
|
834
1090
|
if (steps === undefined) return [];
|
|
@@ -1029,14 +1285,52 @@ export const transformGraph = (pretransform, options = {}) => {
|
|
|
1029
1285
|
return (hash >>> 0).toString(36).toUpperCase();
|
|
1030
1286
|
};
|
|
1031
1287
|
|
|
1288
|
+
// Assemble a `{ parts }` transform result: input and argument ranges keep
|
|
1289
|
+
// their provenance; literal parts are attributed to the transform step.
|
|
1290
|
+
const mappedTransformParts = (input, parts, step, definition, argumentValues = []) => {
|
|
1291
|
+
const derivation = { kind: "transform", name: step.name, source: clone(step.source) };
|
|
1292
|
+
const argumentMapped = (index) => argumentValues[index] ??
|
|
1293
|
+
(typeof step.arguments[index] === "string" ? coarseMapped(step.arguments[index], step.source, definition.id, "transform", [derivation]) : null);
|
|
1294
|
+
const validRange = (text, start, end) => Number.isInteger(start) && Number.isInteger(end) && start >= 0 && start <= end && end <= text.length;
|
|
1295
|
+
let value = mappedValue();
|
|
1296
|
+
for (const part of parts) {
|
|
1297
|
+
let piece = null;
|
|
1298
|
+
if (typeof part === "string") {
|
|
1299
|
+
piece = coarseMapped(part, step.source, definition.id, "transform", [derivation]);
|
|
1300
|
+
} else if (Array.isArray(part?.input) && validRange(input.text, part.input[0], part.input[1])) {
|
|
1301
|
+
piece = withMappedDerivation(sliceMapped(input, part.input[0], part.input[1]), derivation);
|
|
1302
|
+
} else if (Number.isInteger(part?.argument) && typeof part.text === "string") {
|
|
1303
|
+
const source = argumentMapped(part.argument);
|
|
1304
|
+
piece = source
|
|
1305
|
+
? coarseDerivedMapped(part.text, step.source, definition.id, "transform", [derivation], source)
|
|
1306
|
+
: coarseMapped(part.text, step.source, definition.id, "transform", [derivation]);
|
|
1307
|
+
} else if (Number.isInteger(part?.argument)) {
|
|
1308
|
+
const source = argumentMapped(part.argument);
|
|
1309
|
+
if (source && validRange(source.text, part.start, part.end)) {
|
|
1310
|
+
piece = withMappedDerivation(sliceMapped(source, part.start, part.end), derivation);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (!piece) {
|
|
1314
|
+
diagnostics.push(diagnostic("RV121", "Transform " + step.name + " returned an invalid part.", step.source));
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
value = concatMapped(value, piece);
|
|
1318
|
+
}
|
|
1319
|
+
return value;
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1032
1322
|
const evaluateTransformMapped = (input, step, definition, context = {}) => {
|
|
1323
|
+
const { argumentValues, ...transformContext } = context;
|
|
1033
1324
|
const output = applyTransform(
|
|
1034
1325
|
input.text,
|
|
1035
1326
|
step,
|
|
1036
1327
|
diagnostics,
|
|
1037
1328
|
options.transforms,
|
|
1038
|
-
{ chunk: definition, ...
|
|
1329
|
+
{ chunk: definition, ...transformContext }
|
|
1039
1330
|
);
|
|
1331
|
+
if (typeof output !== "string") {
|
|
1332
|
+
return mappedTransformParts(input, output.parts, step, definition, argumentValues) ?? input;
|
|
1333
|
+
}
|
|
1040
1334
|
return mapBuiltinTransform(input, output, step, definition.id) ?? coarseDerivedMapped(
|
|
1041
1335
|
output,
|
|
1042
1336
|
step.source,
|
|
@@ -1067,10 +1361,47 @@ export const transformGraph = (pretransform, options = {}) => {
|
|
|
1067
1361
|
to: resolved,
|
|
1068
1362
|
source: clone(node.source)
|
|
1069
1363
|
};
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1364
|
+
const resolvedDefinition = definitions.get(resolved);
|
|
1365
|
+
const ownerIsExecutable = isExecutable(owner.definition);
|
|
1366
|
+
const shouldUseLiveResult = isExecutable(resolvedDefinition) && !ownerIsExecutable;
|
|
1367
|
+
if (shouldUseLiveResult && options.deferLiveResults && !liveExecutions) {
|
|
1368
|
+
return mappedValue();
|
|
1369
|
+
}
|
|
1370
|
+
let value;
|
|
1371
|
+
if (shouldUseLiveResult && liveExecutions) {
|
|
1372
|
+
const execution = liveExecution(resolved);
|
|
1373
|
+
if (execution?.status !== "succeeded") {
|
|
1374
|
+
diagnostics.push(diagnostic("RV140", "Live result is unavailable for reference " + node.reference + ".", node.source));
|
|
1375
|
+
return mappedValue();
|
|
1376
|
+
}
|
|
1377
|
+
const first = node.pipeline[0];
|
|
1378
|
+
const hasJsonTextBoundary = first?.type === "transform" && first.name === "jsontext";
|
|
1379
|
+
const serializeWholeValue = hasJsonTextBoundary && first.arguments.length === 0;
|
|
1380
|
+
if (typeof execution.value === "string" && !serializeWholeValue) {
|
|
1381
|
+
value = coarseMapped(execution.value, resolvedDefinition.source, resolved, "live-result", [reference]);
|
|
1382
|
+
} else if (hasJsonTextBoundary) {
|
|
1383
|
+
value = coarseMapped(
|
|
1384
|
+
execution.serialized ?? JSON.stringify(execution.value),
|
|
1385
|
+
resolvedDefinition.source,
|
|
1386
|
+
resolved,
|
|
1387
|
+
"live-result",
|
|
1388
|
+
[reference]
|
|
1389
|
+
);
|
|
1390
|
+
} else {
|
|
1391
|
+
diagnostics.push(diagnostic(
|
|
1392
|
+
"RV140",
|
|
1393
|
+
"Non-string live result " + resolved +
|
|
1394
|
+
" requires jsontext() or jsontext(\"key\") before ordinary Ravel processing.",
|
|
1395
|
+
node.source
|
|
1396
|
+
));
|
|
1397
|
+
return mappedValue();
|
|
1398
|
+
}
|
|
1399
|
+
} else {
|
|
1400
|
+
value = mappedValue(dependency.value, dependency.segments.map((segment) => ({
|
|
1401
|
+
...clone(segment),
|
|
1402
|
+
via: [...(segment.via ?? []), reference]
|
|
1403
|
+
})));
|
|
1404
|
+
}
|
|
1074
1405
|
return evaluatePipeline(value, node.pipeline, owner);
|
|
1075
1406
|
};
|
|
1076
1407
|
|
|
@@ -1115,8 +1446,16 @@ export const transformGraph = (pretransform, options = {}) => {
|
|
|
1115
1446
|
} else if (step.type === "chunk") {
|
|
1116
1447
|
value = evaluateExpression(step.value, owner);
|
|
1117
1448
|
} else if (step.type === "transform") {
|
|
1118
|
-
|
|
1119
|
-
|
|
1449
|
+
// Evaluate each ch()/text() argument once, keeping its mapped value so
|
|
1450
|
+
// a parts-aware transform can carry its provenance through.
|
|
1451
|
+
const argumentValues = step.arguments.map((argument) => {
|
|
1452
|
+
const command = argument?.kind === "ravel-command-argument" ? argument.command : null;
|
|
1453
|
+
if (command?.type === "chunk") return evaluateExpression(command.value, owner);
|
|
1454
|
+
if (command?.type === "text") return coarseMapped(command.value, command.source ?? step.source, owner.definition.id, "text");
|
|
1455
|
+
return null;
|
|
1456
|
+
});
|
|
1457
|
+
const argumentsValue = step.arguments.map((argument, index) => argumentValues[index]?.text ?? evaluateArgument(argument, owner));
|
|
1458
|
+
value = evaluateTransformMapped(value, { ...step, arguments: argumentsValue }, owner.definition, { argumentValues });
|
|
1120
1459
|
}
|
|
1121
1460
|
}
|
|
1122
1461
|
return value;
|
|
@@ -1136,7 +1475,13 @@ export const transformGraph = (pretransform, options = {}) => {
|
|
|
1136
1475
|
|
|
1137
1476
|
const evaluateNode = (node, owner) => {
|
|
1138
1477
|
if (node.type === "literal") {
|
|
1139
|
-
return sourceSegment(
|
|
1478
|
+
return sourceSegment(
|
|
1479
|
+
node.value,
|
|
1480
|
+
node.source,
|
|
1481
|
+
owner.definition.id,
|
|
1482
|
+
"literal",
|
|
1483
|
+
node.precision ?? "exact"
|
|
1484
|
+
);
|
|
1140
1485
|
}
|
|
1141
1486
|
if (node.type === "delay") return evaluateDelay(node, owner);
|
|
1142
1487
|
const value = evaluateExpression(node, owner);
|
|
@@ -1295,6 +1640,7 @@ export const transformGraph = (pretransform, options = {}) => {
|
|
|
1295
1640
|
value: value.text,
|
|
1296
1641
|
segments: value.segments,
|
|
1297
1642
|
metadata: definition.metadata,
|
|
1643
|
+
source: definition.source,
|
|
1298
1644
|
dependencies: [...owner.dependencies].sort(),
|
|
1299
1645
|
// References retain authored source order, which is deterministic and is
|
|
1300
1646
|
// more useful for explaining a chunk than lexical target order.
|
|
@@ -1327,7 +1673,26 @@ export const transformGraph = (pretransform, options = {}) => {
|
|
|
1327
1673
|
diagnostics.push(diagnostic("RV130", "out requires a fully qualified existing source chunk ID.", directive.source));
|
|
1328
1674
|
continue;
|
|
1329
1675
|
}
|
|
1330
|
-
|
|
1676
|
+
let chunk = evaluate(id, directive.source);
|
|
1677
|
+
const definition = definitions.get(id);
|
|
1678
|
+
if (isExecutable(definition) && liveExecutions) {
|
|
1679
|
+
const execution = liveExecution(id);
|
|
1680
|
+
if (execution?.status !== "succeeded") {
|
|
1681
|
+
diagnostics.push(diagnostic("RV140", "Live result is unavailable for output " + name + ".", directive.source));
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
if (typeof execution.value !== "string") {
|
|
1685
|
+
diagnostics.push(diagnostic(
|
|
1686
|
+
"RV140",
|
|
1687
|
+
"Output " + name +
|
|
1688
|
+
" requires a live string; use jsontext() or jsontext(\"key\") in a derived chunk.",
|
|
1689
|
+
directive.source
|
|
1690
|
+
));
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
const mapped = coarseMapped(execution.value, definition.source, id, "live-result");
|
|
1694
|
+
chunk = { ...chunk, value: mapped.text, segments: mapped.segments };
|
|
1695
|
+
}
|
|
1331
1696
|
if (deliverables[name]) {
|
|
1332
1697
|
diagnostics.push(diagnostic("RV101", "Duplicate out deliverable: " + name, directive.source));
|
|
1333
1698
|
continue;
|