@pieceful/ravel-myst 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Taylor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # `@pieceful/ravel-myst`
2
+
3
+ Lossless MyST Markdown source adapter for [Ravel](https://github.com/jostylr/ravel). It recognizes canonical
4
+ `{ravel:piece}` directives, the short `{piece}` alias, `{ravel}` graph
5
+ directives, and native MyST `{code}`, `{code-block}`, and `{code-cell}`
6
+ fallbacks carrying an `lp-*` label.
7
+
8
+ Install `@pieceful/ravel-myst-plugin` alongside this adapter when MyST itself
9
+ should render the custom directives. The plugin owns presentation;
10
+ this package continues to own Ravel semantics and exact source mapping.
11
+
12
+ ```js
13
+ import { mystToMap } from "@pieceful/ravel-myst";
14
+
15
+ const { map, diagnostics, surface } = mystToMap(source, {
16
+ uri: "program.myst.md",
17
+ document: "program",
18
+ });
19
+ ```
20
+
21
+ The `{ravel:piece}` argument accepts the shared name-and-pipeline grammar. Its
22
+ `:language:`, `:caption:`, and `:label:` options map to language, visible name,
23
+ and stable MyST anchor:
24
+
25
+ ````markdown
26
+ ```{ravel:piece} main | normalize-eol() | trim()
27
+ :language: javascript
28
+ :caption: Main program
29
+ :label: lp-main
30
+
31
+ console.log(_"helper");
32
+ ```
33
+ ````
34
+
35
+ A native fallback uses `{code}` or `{code-block}` with `:label: lp-main` and
36
+ `:caption:`. `{code-cell}` blocks with an `lp-*` label additionally preserve
37
+ notebook tags, page front matter, and an inert execution plan. Parsing never
38
+ invokes MyST or Jupyter.
39
+
40
+ MyST links, `{ref}`/`{numref}` roles, and `@label` shorthand are recorded as
41
+ navigation to rendered piece anchors. They do not become code-composition
42
+ references. Piece bodies use Ravel's underscore-quote syntax for composition.
43
+
44
+ MyST owns notebook execution by default. To run a piece through Ravel's live
45
+ provider instead, select `executionOwner: "ravel"` and explicitly request
46
+ `run`; this prevents both engines from claiming the same cell.
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pieceful/ravel-myst",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Lossless MyST directive and notebook-cell source adapter for Ravel.",
6
+ "license": "MIT",
7
+ "author": "James Taylor",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/jostylr/ravel.git",
11
+ "directory": "packages/myst"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/jostylr/ravel/issues"
15
+ },
16
+ "homepage": "https://ravel.jostylr.com/",
17
+ "engines": {
18
+ "node": ">=22"
19
+ },
20
+ "keywords": [
21
+ "ravel",
22
+ "myst",
23
+ "jupyter",
24
+ "literate-programming"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "exports": {
30
+ ".": "./src/index.js"
31
+ },
32
+ "types": "./src/index.d.ts",
33
+ "files": [
34
+ "src",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "dependencies": {
39
+ "@pieceful/ravel-core": "0.2.0",
40
+ "yaml": "^2.8.2"
41
+ }
42
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ export interface SourcePosition { line: number; column: number; offset: number; }
2
+ export interface SourceRange { start: SourcePosition; end: SourcePosition; }
3
+ export interface SourceLocation { uri: string; range: SourceRange; }
4
+ export interface Diagnostic {
5
+ code: string;
6
+ severity: "error" | "warning" | "info";
7
+ message: string;
8
+ source: SourceLocation;
9
+ }
10
+ export interface MystOptions {
11
+ uri?: string;
12
+ document?: string;
13
+ executionOwner?: "myst" | "ravel";
14
+ run?: boolean | string[];
15
+ provider?: string;
16
+ }
17
+ export interface MystAdapterResult {
18
+ map: {
19
+ version: 1;
20
+ document: { id: string; uri: string; format: string };
21
+ chunks: Array<Record<string, unknown>>;
22
+ directives: Array<Record<string, unknown>>;
23
+ metadata: Record<string, unknown>;
24
+ };
25
+ diagnostics: Diagnostic[];
26
+ surface: {
27
+ definitions: Array<Record<string, unknown>>;
28
+ references: Array<Record<string, unknown>>;
29
+ directives: Array<Record<string, unknown>>;
30
+ navigation: Array<Record<string, unknown>>;
31
+ };
32
+ }
33
+ export function mystToMap(text: string, options?: MystOptions): MystAdapterResult;
package/src/index.js ADDED
@@ -0,0 +1,664 @@
1
+ import {
2
+ parseDefinitionPipeline,
3
+ parseRavelDirectiveBlock
4
+ } from "@pieceful/ravel-core";
5
+ import { parse as parseYaml } from "yaml";
6
+
7
+ const componentPattern = /^[a-z][a-z0-9-]*$/;
8
+ const pieceDirectives = new Set(["ravel:piece", "piece"]);
9
+ const supportedDirectives = new Set([
10
+ ...pieceDirectives,
11
+ "ravel",
12
+ "code",
13
+ "code-block",
14
+ "code-cell"
15
+ ]);
16
+ const executionOwners = new Set(["myst", "ravel"]);
17
+ const ravelPresentationOptions = new Set([
18
+ "caption",
19
+ "label",
20
+ "name",
21
+ "class",
22
+ "enumerated",
23
+ "numbered",
24
+ "enumerator",
25
+ "number"
26
+ ]);
27
+
28
+ const diagnostic = (code, message, source, severity = "error") => ({
29
+ code, severity, message, source
30
+ });
31
+
32
+ const lineStarts = (text) => {
33
+ const starts = [0];
34
+ for (let index = 0; index < text.length; index += 1) {
35
+ if (text[index] === "\n") starts.push(index + 1);
36
+ }
37
+ return starts;
38
+ };
39
+
40
+ const positionAt = (starts, offset) => {
41
+ let low = 0;
42
+ let high = starts.length;
43
+ while (low + 1 < high) {
44
+ const middle = Math.floor((low + high) / 2);
45
+ if (starts[middle] <= offset) low = middle;
46
+ else high = middle;
47
+ }
48
+ return { line: low, column: offset - starts[low], offset };
49
+ };
50
+
51
+ const rangeAt = (uri, starts, start, end) => ({
52
+ uri,
53
+ range: { start: positionAt(starts, start), end: positionAt(starts, end) }
54
+ });
55
+
56
+ const advanceRange = (source, text, start, end) => {
57
+ const starts = lineStarts(text);
58
+ const base = source.range.start;
59
+ const position = (offset) => {
60
+ const local = positionAt(starts, offset);
61
+ return {
62
+ line: base.line + local.line,
63
+ column: local.line === 0 ? base.column + local.column : local.column,
64
+ offset: base.offset + offset
65
+ };
66
+ };
67
+ return { uri: source.uri, range: { start: position(start), end: position(end) } };
68
+ };
69
+
70
+ const linesOf = (text) => {
71
+ const lines = [];
72
+ let start = 0;
73
+ while (start < text.length) {
74
+ let end = text.indexOf("\n", start);
75
+ if (end === -1) end = text.length;
76
+ else end += 1;
77
+ const contentEnd = end > start && text[end - 1] === "\n"
78
+ ? end - (end > start + 1 && text[end - 2] === "\r" ? 2 : 1)
79
+ : end;
80
+ lines.push({ start, end, contentEnd, value: text.slice(start, contentEnd) });
81
+ start = end;
82
+ }
83
+ return lines;
84
+ };
85
+
86
+ const defaultDocumentId = (uri) => {
87
+ const base = uri.split(/[\\/]/).at(-1)?.replace(/(?:\.myst)?\.[^.]+$/, "") ?? "";
88
+ const id = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
89
+ return componentPattern.test(id) ? id : null;
90
+ };
91
+
92
+ const semanticComponent = (value) => {
93
+ const normalized = value.trim().toLowerCase()
94
+ .normalize("NFKD")
95
+ .replace(/[\u0300-\u036f]/g, "")
96
+ .replace(/[^a-z0-9]+/g, "-")
97
+ .replace(/^-|-$/g, "");
98
+ return componentPattern.test(normalized) ? normalized : null;
99
+ };
100
+
101
+ const firstUnescapedPipe = (value) => {
102
+ let escaped = false;
103
+ for (let index = 0; index < value.length; index += 1) {
104
+ if (escaped) escaped = false;
105
+ else if (value[index] === "\\") escaped = true;
106
+ else if (value[index] === "|") return index;
107
+ }
108
+ return -1;
109
+ };
110
+
111
+ const splitNamePipeline = (value) => {
112
+ const pipeIndex = firstUnescapedPipe(value);
113
+ return {
114
+ name: (pipeIndex === -1 ? value : value.slice(0, pipeIndex)).trim().replace(/\\\|/g, "|"),
115
+ pipe: pipeIndex === -1 ? null : value.slice(pipeIndex + 1).trim(),
116
+ pipeIndex
117
+ };
118
+ };
119
+
120
+ const frontMatterFrom = (text) => {
121
+ if (!text.startsWith("---\n") && !text.startsWith("---\r\n")) {
122
+ return { data: undefined, source: null, end: 0 };
123
+ }
124
+ const firstEnd = text.indexOf("\n");
125
+ const close = /^---[ \t]*\r?$/m.exec(text.slice(firstEnd + 1));
126
+ if (!close) return { data: undefined, source: null, end: 0 };
127
+ const contentStart = firstEnd + 1;
128
+ const contentEnd = contentStart + close.index;
129
+ const end = contentEnd + close[0].length;
130
+ try {
131
+ return { data: parseYaml(text.slice(contentStart, contentEnd)), start: 0, end };
132
+ } catch {
133
+ return { data: undefined, start: 0, end };
134
+ }
135
+ };
136
+
137
+ const scalar = (value) => {
138
+ const trimmed = value.trim();
139
+ if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
140
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
141
+ return trimmed.slice(1, -1);
142
+ }
143
+ return trimmed;
144
+ };
145
+
146
+ const booleanValue = (value) => {
147
+ if (value.trim() === "") return true;
148
+ if (/^(?:true|yes|on|1)$/i.test(value.trim())) return true;
149
+ if (/^(?:false|no|off|0)$/i.test(value.trim())) return false;
150
+ return null;
151
+ };
152
+
153
+ const tagsFrom = (value) => {
154
+ const trimmed = value.trim();
155
+ const inner = trimmed.startsWith("[") && trimmed.endsWith("]")
156
+ ? trimmed.slice(1, -1)
157
+ : trimmed;
158
+ return inner.split(",").flatMap((entry) =>
159
+ entry.trim().split(/\s+/).filter(Boolean)
160
+ ).map(scalar);
161
+ };
162
+
163
+ const optionValue = (options, name) =>
164
+ options.findLast((option) => option.name === name)?.value;
165
+
166
+ const scanFences = (text, uri, starts, frontMatterEnd) => {
167
+ const lines = linesOf(text);
168
+ const fences = [];
169
+ for (let index = 0; index < lines.length; index += 1) {
170
+ const line = lines[index];
171
+ if (line.start < frontMatterEnd) continue;
172
+ const opening = /^([ \t]{0,3})(:{3,}|`{3,})\{([A-Za-z][A-Za-z0-9_-]*(?::[A-Za-z][A-Za-z0-9_-]*)*)\}(?:[ \t]+(.*?))?[ \t]*$/.exec(line.value);
173
+ if (!opening) continue;
174
+ const marker = opening[2][0];
175
+ const minimumLength = opening[2].length;
176
+ let endIndex = index + 1;
177
+ while (endIndex < lines.length) {
178
+ const close = /^([ \t]{0,3})(:{3,}|`{3,})[ \t]*$/.exec(lines[endIndex].value);
179
+ if (close && close[2][0] === marker && close[2].length >= minimumLength) break;
180
+ endIndex += 1;
181
+ }
182
+ const endLine = lines[endIndex] ?? null;
183
+ const contentEnd = endLine?.start ?? text.length;
184
+ const options = [];
185
+ let bodyIndex = index + 1;
186
+ while (bodyIndex < endIndex) {
187
+ const optionLine = lines[bodyIndex];
188
+ const option = /^[ \t]{0,3}:([A-Za-z][A-Za-z0-9_-]*):(?:[ \t]*(.*))?$/.exec(optionLine.value);
189
+ if (!option) break;
190
+ const rawValue = option[2] ?? "";
191
+ const valueStart = optionLine.contentEnd - rawValue.length;
192
+ options.push({
193
+ name: option[1].toLowerCase(),
194
+ value: scalar(rawValue),
195
+ rawValue,
196
+ source: rangeAt(uri, starts, optionLine.start, optionLine.end),
197
+ valueSource: rangeAt(uri, starts, valueStart, optionLine.contentEnd)
198
+ });
199
+ bodyIndex += 1;
200
+ }
201
+ if (bodyIndex < endIndex && lines[bodyIndex].value.trim() === "") bodyIndex += 1;
202
+ const bodyStart = lines[bodyIndex]?.start ?? contentEnd;
203
+ fences.push({
204
+ directive: opening[3].toLowerCase(),
205
+ argument: opening[4] ?? "",
206
+ marker: opening[2],
207
+ declaration: rangeAt(uri, starts, line.start, line.end),
208
+ body: text.slice(bodyStart, contentEnd),
209
+ bodySource: rangeAt(uri, starts, bodyStart, contentEnd),
210
+ options,
211
+ end: endLine ? rangeAt(uri, starts, endLine.start, endLine.end) : null,
212
+ fullStart: line.start,
213
+ fullEnd: endLine?.end ?? text.length,
214
+ _endIndex: endIndex
215
+ });
216
+ if (endLine) index = endIndex;
217
+ else break;
218
+ }
219
+ return fences;
220
+ };
221
+
222
+ const precedingTarget = (text, fence, uri, starts) => {
223
+ const before = text.slice(0, fence.fullStart);
224
+ const lineEnd = before.endsWith("\n") ? before.length - 1 : before.length;
225
+ const lineStart = before.lastIndexOf("\n", Math.max(0, lineEnd - 1)) + 1;
226
+ const value = before.slice(lineStart, lineEnd).replace(/\r$/, "");
227
+ const match = /^[ \t]{0,3}\(([^()\s]+)\)=[ \t]*$/.exec(value);
228
+ if (!match) return null;
229
+ return {
230
+ label: match[1],
231
+ source: rangeAt(uri, starts, lineStart, before.length)
232
+ };
233
+ };
234
+
235
+ const underscoreReferencesFrom = (body, source) => {
236
+ const references = [];
237
+ const pattern = /(?:\\[1-9][0-9]*)?_(["'`])([\s\S]*?)\1/g;
238
+ for (const match of body.matchAll(pattern)) {
239
+ const prefix = match[0].indexOf("_");
240
+ references.push({
241
+ targetText: match[2],
242
+ source: advanceRange(source, body, match.index + prefix, match.index + match[0].length)
243
+ });
244
+ }
245
+ return references;
246
+ };
247
+
248
+ const narrativeCrossReferences = (text, excluded, labelToPiece, uri, starts) => {
249
+ const references = [];
250
+ const patterns = [
251
+ {
252
+ pattern: /\[[^\]]*\]\(#([^) \t]+)\)/g,
253
+ target: (match) => match[1],
254
+ syntax: "link"
255
+ },
256
+ {
257
+ pattern: /\{(?:ref|numref)\}`([^`]+)`/g,
258
+ target: (match) => /<([^<>]+)>[ \t]*$/.exec(match[1])?.[1] ?? match[1].trim(),
259
+ syntax: "role"
260
+ },
261
+ {
262
+ pattern: /(^|[^\w@])@([A-Za-z][A-Za-z0-9:._/-]*)/gm,
263
+ target: (match) => match[2],
264
+ start: (match) => match.index + match[1].length,
265
+ syntax: "shorthand"
266
+ }
267
+ ];
268
+ const included = (offset) => !excluded.some((range) => offset >= range.start && offset < range.end);
269
+ for (const entry of patterns) {
270
+ for (const match of text.matchAll(entry.pattern)) {
271
+ const start = entry.start?.(match) ?? match.index;
272
+ const targetLabel = entry.target(match).replace(/^#/, "")
273
+ .replace(entry.syntax === "shorthand" ? /[.,;:!?]+$/ : /$^/, "");
274
+ const targetPieceId = labelToPiece.get(targetLabel);
275
+ if (!targetPieceId || !included(start)) continue;
276
+ const length = entry.syntax === "shorthand" ? match[0].length - match[1].length : match[0].length;
277
+ references.push({
278
+ targetPieceId,
279
+ targetLabel,
280
+ syntax: entry.syntax,
281
+ source: rangeAt(uri, starts, start, start + length)
282
+ });
283
+ }
284
+ }
285
+ return references.sort((left, right) =>
286
+ left.source.range.start.offset - right.source.range.start.offset
287
+ );
288
+ };
289
+
290
+ const pipelineKey = (pipeline) =>
291
+ JSON.stringify(pipeline.map(({ name, arguments: argumentsValue }) => [name, argumentsValue ?? []]));
292
+
293
+ const runSelected = (options, names) =>
294
+ options.run === true ||
295
+ (Array.isArray(options.run) && names.some((name) => options.run.includes(name)));
296
+
297
+ export const mystToMap = (text, options = {}) => {
298
+ const uri = options.uri ?? "document.myst.md";
299
+ const starts = lineStarts(text);
300
+ const frontMatter = frontMatterFrom(text);
301
+ const configuredDocument = frontMatter.data?.ravel?.document ?? frontMatter.data?.lp?.document;
302
+ const documentId = options.document ?? configuredDocument ?? defaultDocumentId(uri);
303
+ if (!componentPattern.test(documentId ?? "")) {
304
+ throw new Error("MyST document identity must be a lowercase identifier: " + String(documentId));
305
+ }
306
+ const frontMatterOwner = frontMatter.data?.ravel?.execution_owner ??
307
+ frontMatter.data?.ravel?.executionOwner;
308
+ const executionOwner = options.executionOwner ?? frontMatterOwner ?? null;
309
+ if (executionOwner !== null && !executionOwners.has(executionOwner)) {
310
+ throw new Error("MyST execution owner must be myst or ravel: " + executionOwner);
311
+ }
312
+
313
+ const diagnostics = [];
314
+ const fences = scanFences(text, uri, starts, frontMatter.end);
315
+ const candidates = [];
316
+ const ravelDirectives = [];
317
+ const ignoredDirectives = [];
318
+ for (const fence of fences) {
319
+ if (!fence.end) {
320
+ diagnostics.push(diagnostic("LPA111", "MyST directive is missing its closing fence.", fence.declaration));
321
+ }
322
+ if (!supportedDirectives.has(fence.directive)) {
323
+ ignoredDirectives.push({
324
+ directive: fence.directive,
325
+ declaration: fence.declaration,
326
+ body: fence.bodySource,
327
+ reason: "unrecognized-directive"
328
+ });
329
+ continue;
330
+ }
331
+
332
+ if (fence.directive === "ravel") {
333
+ const semanticOptions = fence.options.filter((option) =>
334
+ !ravelPresentationOptions.has(option.name)
335
+ );
336
+ if (fence.argument || semanticOptions.length) {
337
+ diagnostics.push(diagnostic(
338
+ "LPA101",
339
+ "The MyST {ravel} directive accepts no argument or semantic options.",
340
+ fence.declaration
341
+ ));
342
+ }
343
+ const parsed = parseRavelDirectiveBlock(fence.body, {
344
+ document: documentId,
345
+ sourceAt: (start, end) =>
346
+ advanceRange(fence.bodySource, fence.body, start, end)
347
+ });
348
+ diagnostics.push(...parsed.diagnostics);
349
+ ravelDirectives.push(...parsed.directives);
350
+ continue;
351
+ }
352
+
353
+ const optionLabel = optionValue(fence.options, "label") || null;
354
+ const target = precedingTarget(text, fence, uri, starts);
355
+ if (target && optionLabel && target.label !== optionLabel) {
356
+ diagnostics.push(diagnostic(
357
+ "LPA113",
358
+ "A MyST target and directive :label: must agree when both label one piece.",
359
+ fence.declaration
360
+ ));
361
+ }
362
+ const label = optionLabel ?? target?.label ?? null;
363
+ const pieceDirective = pieceDirectives.has(fence.directive);
364
+ const fallback = !pieceDirective;
365
+ if (fallback && !label?.startsWith("lp-")) {
366
+ ignoredDirectives.push({
367
+ directive: fence.directive,
368
+ declaration: fence.declaration,
369
+ body: fence.bodySource,
370
+ reason: "non-piece-label"
371
+ });
372
+ continue;
373
+ }
374
+
375
+ const split = pieceDirective
376
+ ? splitNamePipeline(fence.argument)
377
+ : { name: label.slice(3), pipe: null, pipeIndex: -1 };
378
+ const caption = optionValue(fence.options, "caption") || null;
379
+ const authoredName = split.name || (label?.replace(/^lp-/, "") ?? "");
380
+ const explicitSemantic = label ? label.replace(/^lp-/, "") : null;
381
+ const canonical = semanticComponent(explicitSemantic ?? authoredName);
382
+ if (!canonical) {
383
+ diagnostics.push(diagnostic(
384
+ "LPA101",
385
+ "MyST piece name or label does not produce a usable Ravel ID.",
386
+ fence.declaration
387
+ ));
388
+ continue;
389
+ }
390
+ if (!label) {
391
+ diagnostics.push(diagnostic(
392
+ "LPA102",
393
+ "MyST piece ID was inferred from its mutable directive argument.",
394
+ fence.declaration,
395
+ "info"
396
+ ));
397
+ }
398
+ const displayName = caption ?? authoredName;
399
+ if (!displayName) {
400
+ diagnostics.push(diagnostic(
401
+ "LPA101",
402
+ "MyST piece has no visible caption or directive name.",
403
+ fence.declaration
404
+ ));
405
+ }
406
+ if (fallback && !caption) {
407
+ diagnostics.push(diagnostic(
408
+ "LPA101",
409
+ "A native MyST fallback block needs :caption: for a visible piece name.",
410
+ fence.declaration,
411
+ "warning"
412
+ ));
413
+ }
414
+
415
+ const language = pieceDirective
416
+ ? optionValue(fence.options, "language") || null
417
+ : scalar(fence.argument).split(/\s+/)[0] || null;
418
+ const cellOption = booleanValue(optionValue(fence.options, "cell") ?? "false");
419
+ const notebookCell = fence.directive === "code-cell" || cellOption === true;
420
+ const directiveOwner = optionValue(fence.options, "execution-owner") || null;
421
+ if (directiveOwner !== null && !executionOwners.has(directiveOwner)) {
422
+ diagnostics.push(diagnostic(
423
+ "LPA141",
424
+ "MyST directive execution-owner must be myst or ravel.",
425
+ fence.declaration
426
+ ));
427
+ }
428
+ if (directiveOwner && executionOwner && directiveOwner !== executionOwner) {
429
+ diagnostics.push(diagnostic(
430
+ "LPA141",
431
+ "MyST directive and adapter configuration select different execution owners.",
432
+ fence.declaration
433
+ ));
434
+ }
435
+ const selectedOwner = executionOwners.has(directiveOwner)
436
+ ? directiveOwner
437
+ : executionOwner;
438
+ const runOption = booleanValue(optionValue(fence.options, "run") ?? "false");
439
+ const ravelRequested = runSelected(options, [authoredName, canonical, label].filter(Boolean)) ||
440
+ runOption === true;
441
+ if (ravelRequested && selectedOwner !== "ravel") {
442
+ diagnostics.push(diagnostic(
443
+ "LPA141",
444
+ "Ravel execution requires executionOwner: ravel; MyST remains the default notebook owner.",
445
+ fence.declaration
446
+ ));
447
+ }
448
+ if (notebookCell && selectedOwner === "ravel" && !ravelRequested) {
449
+ diagnostics.push(diagnostic(
450
+ "LPA141",
451
+ "A MyST notebook cell assigned to Ravel must explicitly request run.",
452
+ fence.declaration
453
+ ));
454
+ }
455
+
456
+ const declarationText = text.slice(
457
+ fence.declaration.range.start.offset,
458
+ fence.declaration.range.end.offset
459
+ );
460
+ const pipeWhitespace = split.pipe !== null
461
+ ? fence.argument.slice(split.pipeIndex + 1).search(/\S|$/)
462
+ : 0;
463
+ const pipeStart = declarationText.indexOf(fence.argument) + split.pipeIndex + 1 + pipeWhitespace;
464
+ const pipeSource = split.pipe !== null
465
+ ? advanceRange(fence.declaration, declarationText, pipeStart, pipeStart + split.pipe.length)
466
+ : null;
467
+ const parsedPipeline = split.pipe
468
+ ? parseDefinitionPipeline(split.pipe, pipeSource ?? fence.declaration)
469
+ : { pipeline: [], diagnostics: [] };
470
+ diagnostics.push(...parsedPipeline.diagnostics);
471
+ candidates.push({
472
+ ...fence,
473
+ label,
474
+ target,
475
+ canonical,
476
+ authoredName,
477
+ displayName,
478
+ caption,
479
+ language,
480
+ pipeline: parsedPipeline.pipeline,
481
+ pipelineSource: pipeSource,
482
+ notebookCell,
483
+ executionOwner: selectedOwner,
484
+ ravelRun: ravelRequested && selectedOwner === "ravel",
485
+ provider: optionValue(fence.options, "provider") || options.provider || null,
486
+ tags: tagsFrom(optionValue(fence.options, "tags") ?? "")
487
+ });
488
+ }
489
+
490
+ const aliases = {};
491
+ const chunks = [];
492
+ const chunksById = new Map();
493
+ const labelToPiece = new Map();
494
+ const labelOwners = new Map();
495
+ const surface = {
496
+ definitions: [],
497
+ references: [],
498
+ directives: [...ravelDirectives],
499
+ navigation: []
500
+ };
501
+ const plannedEffects = [];
502
+ for (const candidate of candidates) {
503
+ const id = documentId + "::" + candidate.canonical;
504
+ for (const alias of [candidate.authoredName, candidate.label, candidate.canonical].filter(Boolean)) {
505
+ aliases[alias] = candidate.canonical;
506
+ }
507
+ if (candidate.label) {
508
+ const priorOwner = labelOwners.get(candidate.label);
509
+ if (priorOwner && priorOwner !== id) {
510
+ diagnostics.push(diagnostic(
511
+ "LPA103",
512
+ "MyST rendered labels must be unique: " + candidate.label + ".",
513
+ candidate.declaration
514
+ ));
515
+ } else {
516
+ labelOwners.set(candidate.label, id);
517
+ labelToPiece.set(candidate.label, id);
518
+ }
519
+ }
520
+ let chunk = chunksById.get(id);
521
+ if (!chunk) {
522
+ chunk = {
523
+ id,
524
+ identity: { document: documentId, chunk: candidate.canonical, minor: null, type: null },
525
+ name: candidate.displayName,
526
+ body: "",
527
+ definitionPipeline: candidate.pipeline,
528
+ metadata: {
529
+ ...(candidate.language ? { language: candidate.language } : {}),
530
+ tags: [...candidate.tags],
531
+ data: {
532
+ ravel: {
533
+ adapter: "myst",
534
+ displayName: candidate.displayName,
535
+ ...(candidate.label ? { renderedAnchor: candidate.label } : {}),
536
+ referenceSyntax: {
537
+ noweb: false,
538
+ underscore: true,
539
+ aliases
540
+ },
541
+ ...(candidate.ravelRun ? { run: true } : {}),
542
+ ...(candidate.ravelRun && candidate.provider ? { provider: candidate.provider } : {})
543
+ },
544
+ myst: {
545
+ label: candidate.label,
546
+ caption: candidate.caption,
547
+ notebookCell: candidate.notebookCell,
548
+ executionOwner: candidate.notebookCell
549
+ ? (candidate.executionOwner ?? "myst")
550
+ : candidate.executionOwner,
551
+ fragments: []
552
+ }
553
+ }
554
+ },
555
+ source: candidate.bodySource,
556
+ fragments: [],
557
+ _pipelineKey: pipelineKey(candidate.pipeline)
558
+ };
559
+ chunks.push(chunk);
560
+ chunksById.set(id, chunk);
561
+ } else {
562
+ if (candidate.pipeline.length) {
563
+ const key = pipelineKey(candidate.pipeline);
564
+ if (chunk.definitionPipeline.length === 0) {
565
+ chunk.definitionPipeline = candidate.pipeline;
566
+ chunk._pipelineKey = key;
567
+ } else if (chunk._pipelineKey !== key) {
568
+ diagnostics.push(diagnostic(
569
+ "LPA113",
570
+ "Repeated MyST fragments have conflicting pipelines for " + candidate.authoredName + ".",
571
+ candidate.declaration
572
+ ));
573
+ }
574
+ }
575
+ if (candidate.language && chunk.metadata.language && candidate.language !== chunk.metadata.language) {
576
+ diagnostics.push(diagnostic(
577
+ "LPA113",
578
+ "Repeated MyST fragments have conflicting languages for " + candidate.authoredName + ".",
579
+ candidate.declaration
580
+ ));
581
+ } else if (candidate.language && !chunk.metadata.language) {
582
+ chunk.metadata.language = candidate.language;
583
+ }
584
+ if (candidate.label && chunk.metadata.data.myst.label &&
585
+ candidate.label === chunk.metadata.data.myst.label) {
586
+ diagnostics.push(diagnostic(
587
+ "LPA103",
588
+ "Repeated MyST fragments cannot reuse the same rendered label.",
589
+ candidate.declaration,
590
+ "warning"
591
+ ));
592
+ }
593
+ chunk.metadata.tags = [...new Set([...chunk.metadata.tags, ...candidate.tags])];
594
+ }
595
+
596
+ chunk.body += candidate.body;
597
+ chunk.fragments.push({ body: candidate.body, source: candidate.bodySource });
598
+ chunk.metadata.data.myst.fragments.push({
599
+ directive: candidate.directive,
600
+ marker: candidate.marker,
601
+ declaration: candidate.declaration,
602
+ end: candidate.end,
603
+ label: candidate.label,
604
+ target: candidate.target,
605
+ caption: candidate.caption,
606
+ language: candidate.language,
607
+ options: candidate.options,
608
+ pipeline: candidate.pipelineSource,
609
+ notebookCell: candidate.notebookCell,
610
+ tags: candidate.tags
611
+ });
612
+ surface.definitions.push({
613
+ pieceId: id,
614
+ declaration: candidate.declaration,
615
+ fragments: [candidate.bodySource],
616
+ displayName: candidate.displayName,
617
+ ...(candidate.label ? {
618
+ sourceAnchor: candidate.label,
619
+ renderedAnchor: candidate.label
620
+ } : {}),
621
+ ...(candidate.pipelineSource ? { pipeline: candidate.pipelineSource } : {})
622
+ });
623
+ for (const reference of underscoreReferencesFrom(candidate.body, candidate.bodySource)) {
624
+ surface.references.push({ ownerPieceId: id, ...reference });
625
+ }
626
+ if (candidate.notebookCell) {
627
+ plannedEffects.push({
628
+ kind: "myst-code-cell",
629
+ owner: candidate.executionOwner ?? "myst",
630
+ pieceId: id,
631
+ language: candidate.language,
632
+ label: candidate.label,
633
+ tags: candidate.tags,
634
+ source: candidate.declaration
635
+ });
636
+ }
637
+ }
638
+
639
+ surface.navigation = narrativeCrossReferences(
640
+ text,
641
+ fences.map((fence) => ({ start: fence.fullStart, end: fence.fullEnd })),
642
+ labelToPiece,
643
+ uri,
644
+ starts
645
+ );
646
+
647
+ return {
648
+ map: {
649
+ version: 1,
650
+ document: { id: documentId, uri, format: "myst+ravel-v1" },
651
+ chunks: chunks.map(({ _pipelineKey, ...chunk }) => chunk),
652
+ directives: ravelDirectives,
653
+ metadata: {
654
+ adapter: "myst",
655
+ frontMatter: frontMatter.data ?? null,
656
+ crossReferences: surface.navigation,
657
+ plannedEffects,
658
+ ignoredDirectives
659
+ }
660
+ },
661
+ diagnostics,
662
+ surface
663
+ };
664
+ };