@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 +21 -0
- package/README.md +46 -0
- package/package.json +42 -0
- package/src/index.d.ts +33 -0
- package/src/index.js +664 -0
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
|
+
};
|