@pieceful/ravel-html 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 +41 -0
- package/package.json +42 -0
- package/src/index.d.ts +39 -0
- package/src/index.js +868 -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,41 @@
|
|
|
1
|
+
# `@pieceful/ravel-html`
|
|
2
|
+
|
|
3
|
+
Lossless, script-free HTML source adapter for [Ravel](https://github.com/jostylr/ravel). Semantic `section` and
|
|
4
|
+
`figure` elements declare visible pieces; `pre > code` elements provide exact
|
|
5
|
+
source fragments; ordinary links can declare conspicuous graph directives.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { htmlToMap } from "@pieceful/ravel-html";
|
|
9
|
+
|
|
10
|
+
const { map, diagnostics, surface } = htmlToMap(source, {
|
|
11
|
+
uri: "program.html",
|
|
12
|
+
document: "program",
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The adapter parses source HTML with scripting disabled. It never constructs a
|
|
17
|
+
browser DOM, evaluates scripts, follows links, mutates the document, executes
|
|
18
|
+
code, or writes outputs.
|
|
19
|
+
|
|
20
|
+
Canonical section form:
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<meta name="ravel-document" content="program" />
|
|
24
|
+
<section id="lp-main" data-ravel-piece="main | trim()">
|
|
25
|
+
<h2>Main program <code>main</code></h2>
|
|
26
|
+
<pre><code class="language-javascript">console.log(_"helper");</code></pre>
|
|
27
|
+
</section>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Canonical block form:
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<figure id="lp-helper" data-ravel-piece="helper">
|
|
34
|
+
<figcaption>Helper <code>helper</code></figcaption>
|
|
35
|
+
<pre><code class="language-javascript">export const helper = true;</code></pre>
|
|
36
|
+
</figure>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Visible `<a>` and `<data>` elements with `data-ravel-effect` declare `read`,
|
|
40
|
+
`derive`, and `write` graph operations. `data-lp-*` remains a compatibility
|
|
41
|
+
alias for the earlier design spelling.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pieceful/ravel-html",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Lossless, script-free semantic HTML 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/html"
|
|
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
|
+
"html",
|
|
23
|
+
"literate-programming"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.js"
|
|
30
|
+
},
|
|
31
|
+
"types": "./src/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@pieceful/ravel-core": "0.2.0",
|
|
39
|
+
"character-entities": "^2.0.2",
|
|
40
|
+
"parse5": "^8.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface SourcePosition {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
offset: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SourceLocation {
|
|
8
|
+
uri: string;
|
|
9
|
+
range: { start: SourcePosition; end: SourcePosition };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HtmlAdapterOptions {
|
|
13
|
+
uri?: string;
|
|
14
|
+
document?: string;
|
|
15
|
+
run?: boolean | string[];
|
|
16
|
+
provider?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HtmlAdapterResult {
|
|
20
|
+
map: Record<string, unknown>;
|
|
21
|
+
diagnostics: Array<{
|
|
22
|
+
code: string;
|
|
23
|
+
severity: "error" | "warning" | "info";
|
|
24
|
+
message: string;
|
|
25
|
+
source: SourceLocation;
|
|
26
|
+
}>;
|
|
27
|
+
surface: {
|
|
28
|
+
definitions: unknown[];
|
|
29
|
+
references: unknown[];
|
|
30
|
+
directives: unknown[];
|
|
31
|
+
navigation: unknown[];
|
|
32
|
+
entities: unknown[];
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function htmlToMap(
|
|
37
|
+
text: string,
|
|
38
|
+
options?: HtmlAdapterOptions
|
|
39
|
+
): HtmlAdapterResult;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import { characterEntities } from "character-entities";
|
|
2
|
+
import { parse } from "parse5";
|
|
3
|
+
import { parseDefinitionPipeline } from "@pieceful/ravel-core";
|
|
4
|
+
|
|
5
|
+
const componentPattern = /^[a-z][a-z0-9-]*$/;
|
|
6
|
+
const pieceTags = new Set(["section", "figure"]);
|
|
7
|
+
|
|
8
|
+
const diagnostic = (code, message, source, severity = "error") => ({
|
|
9
|
+
code,
|
|
10
|
+
severity,
|
|
11
|
+
message,
|
|
12
|
+
source
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const lineStarts = (text) => {
|
|
16
|
+
const starts = [0];
|
|
17
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
18
|
+
if (text[index] === "\n") starts.push(index + 1);
|
|
19
|
+
}
|
|
20
|
+
return starts;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const positionAt = (starts, offset) => {
|
|
24
|
+
let low = 0;
|
|
25
|
+
let high = starts.length;
|
|
26
|
+
while (low + 1 < high) {
|
|
27
|
+
const middle = Math.floor((low + high) / 2);
|
|
28
|
+
if (starts[middle] <= offset) low = middle;
|
|
29
|
+
else high = middle;
|
|
30
|
+
}
|
|
31
|
+
return { line: low, column: offset - starts[low], offset };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const rangeAt = (uri, starts, start, end) => ({
|
|
35
|
+
uri,
|
|
36
|
+
range: {
|
|
37
|
+
start: positionAt(starts, start),
|
|
38
|
+
end: positionAt(starts, end)
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const locationRange = (location, uri, starts) => rangeAt(
|
|
43
|
+
uri,
|
|
44
|
+
starts,
|
|
45
|
+
location?.startOffset ?? 0,
|
|
46
|
+
location?.endOffset ?? location?.startOffset ?? 0
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const defaultDocumentId = (uri) => {
|
|
50
|
+
const base = uri.split(/[\\/]/).at(-1)
|
|
51
|
+
?.replace(/\.html?$/i, "") ?? "";
|
|
52
|
+
const id = base.toLowerCase()
|
|
53
|
+
.normalize("NFKD")
|
|
54
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
55
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
56
|
+
.replace(/^-|-$/g, "");
|
|
57
|
+
return componentPattern.test(id) ? id : null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const semanticComponent = (value) => {
|
|
61
|
+
const normalized = String(value ?? "").trim().toLowerCase()
|
|
62
|
+
.normalize("NFKD")
|
|
63
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
64
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
65
|
+
.replace(/^-|-$/g, "");
|
|
66
|
+
return componentPattern.test(normalized) ? normalized : null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const firstUnescapedPipe = (value) => {
|
|
70
|
+
let escaped = false;
|
|
71
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
72
|
+
if (escaped) escaped = false;
|
|
73
|
+
else if (value[index] === "\\") escaped = true;
|
|
74
|
+
else if (value[index] === "|") return index;
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const splitNamePipeline = (value) => {
|
|
80
|
+
const pipeIndex = firstUnescapedPipe(value);
|
|
81
|
+
return {
|
|
82
|
+
name: (pipeIndex === -1 ? value : value.slice(0, pipeIndex))
|
|
83
|
+
.trim()
|
|
84
|
+
.replace(/\\\|/g, "|"),
|
|
85
|
+
pipe: pipeIndex === -1 ? null : value.slice(pipeIndex + 1).trim(),
|
|
86
|
+
pipeIndex
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const attributesOf = (node) => Object.fromEntries(
|
|
91
|
+
(node.attrs ?? []).map((attribute) => [attribute.name, attribute.value])
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const attributeValue = (node, name) =>
|
|
95
|
+
node?.attrs?.find((attribute) => attribute.name === name)?.value;
|
|
96
|
+
|
|
97
|
+
const ravelAttribute = (node, suffix) =>
|
|
98
|
+
attributeValue(node, "data-ravel-" + suffix) ??
|
|
99
|
+
attributeValue(node, "data-lp-" + suffix);
|
|
100
|
+
|
|
101
|
+
const attributeSource = (node, name, text, uri, starts) => {
|
|
102
|
+
const location = node.sourceCodeLocation?.attrs?.[name];
|
|
103
|
+
if (!location) return locationRange(node.sourceCodeLocation, uri, starts);
|
|
104
|
+
const raw = text.slice(location.startOffset, location.endOffset);
|
|
105
|
+
const equals = raw.indexOf("=");
|
|
106
|
+
if (equals === -1) return locationRange(location, uri, starts);
|
|
107
|
+
let start = equals + 1;
|
|
108
|
+
while (/\s/.test(raw[start] ?? "")) start += 1;
|
|
109
|
+
let end = raw.length;
|
|
110
|
+
while (end > start && /\s/.test(raw[end - 1])) end -= 1;
|
|
111
|
+
if ((raw[start] === "\"" || raw[start] === "'") &&
|
|
112
|
+
raw[end - 1] === raw[start]) {
|
|
113
|
+
start += 1;
|
|
114
|
+
end -= 1;
|
|
115
|
+
}
|
|
116
|
+
return rangeAt(
|
|
117
|
+
uri,
|
|
118
|
+
starts,
|
|
119
|
+
location.startOffset + start,
|
|
120
|
+
location.startOffset + end
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const ravelAttributeSource = (node, suffix, text, uri, starts) => {
|
|
125
|
+
const canonical = "data-ravel-" + suffix;
|
|
126
|
+
return attributeValue(node, canonical) !== undefined
|
|
127
|
+
? attributeSource(node, canonical, text, uri, starts)
|
|
128
|
+
: attributeSource(node, "data-lp-" + suffix, text, uri, starts);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const childNodesOf = (node) =>
|
|
132
|
+
node.tagName === "template" ? [] : node.childNodes ?? [];
|
|
133
|
+
|
|
134
|
+
const walk = (node, visit) => {
|
|
135
|
+
visit(node);
|
|
136
|
+
for (const child of childNodesOf(node)) walk(child, visit);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const descendants = (node, predicate, stop = () => false) => {
|
|
140
|
+
const result = [];
|
|
141
|
+
const visit = (current) => {
|
|
142
|
+
for (const child of childNodesOf(current)) {
|
|
143
|
+
if (stop(child)) continue;
|
|
144
|
+
if (predicate(child)) result.push(child);
|
|
145
|
+
visit(child);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
visit(node);
|
|
149
|
+
return result;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const normalizedText = (node, excludedTags = new Set()) => {
|
|
153
|
+
let value = "";
|
|
154
|
+
const collect = (current) => {
|
|
155
|
+
if (current.nodeName === "#text") {
|
|
156
|
+
value += current.value;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (excludedTags.has(current.tagName)) return;
|
|
160
|
+
for (const child of childNodesOf(current)) collect(child);
|
|
161
|
+
};
|
|
162
|
+
collect(node);
|
|
163
|
+
return value.replace(/\s+/g, " ").trim();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const mergeSegment = (segments, next) => {
|
|
167
|
+
const prior = segments.at(-1);
|
|
168
|
+
if (prior &&
|
|
169
|
+
prior.precision === next.precision &&
|
|
170
|
+
prior.source.range.end.offset === next.source.range.start.offset) {
|
|
171
|
+
prior.body += next.body;
|
|
172
|
+
prior.source.range.end = next.source.range.end;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
segments.push(next);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const namedEntityAt = (raw, rawIndex, decoded, decodedIndex) => {
|
|
179
|
+
const match = /^&([A-Za-z0-9]+)/.exec(raw.slice(rawIndex));
|
|
180
|
+
if (!match) return null;
|
|
181
|
+
for (let length = match[1].length; length > 0; length -= 1) {
|
|
182
|
+
const name = match[1].slice(0, length);
|
|
183
|
+
const value = characterEntities[name];
|
|
184
|
+
if (value === undefined || !decoded.startsWith(value, decodedIndex)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const semicolon = raw[rawIndex + 1 + length] === ";" ? 1 : 0;
|
|
188
|
+
return {
|
|
189
|
+
rawLength: 1 + length + semicolon,
|
|
190
|
+
value
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const entityAt = (raw, rawIndex, decoded, decodedIndex) => {
|
|
197
|
+
const numeric = /^&#(?:[xX][0-9A-Fa-f]+|[0-9]+);?/
|
|
198
|
+
.exec(raw.slice(rawIndex));
|
|
199
|
+
if (numeric) {
|
|
200
|
+
const codePoint = decoded.codePointAt(decodedIndex);
|
|
201
|
+
const value = codePoint === undefined ? "" : String.fromCodePoint(codePoint);
|
|
202
|
+
return value
|
|
203
|
+
? { rawLength: numeric[0].length, value }
|
|
204
|
+
: null;
|
|
205
|
+
}
|
|
206
|
+
return namedEntityAt(raw, rawIndex, decoded, decodedIndex);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const fallbackBoundary = (raw, rawIndex, decoded, decodedIndex) => {
|
|
210
|
+
const limit = 64;
|
|
211
|
+
for (let distance = 2; distance <= limit; distance += 1) {
|
|
212
|
+
for (let rawDistance = 1; rawDistance < distance; rawDistance += 1) {
|
|
213
|
+
const decodedDistance = distance - rawDistance;
|
|
214
|
+
const rawAnchor = raw.slice(
|
|
215
|
+
rawIndex + rawDistance,
|
|
216
|
+
rawIndex + rawDistance + 4
|
|
217
|
+
);
|
|
218
|
+
if (rawAnchor.length &&
|
|
219
|
+
decoded.slice(
|
|
220
|
+
decodedIndex + decodedDistance,
|
|
221
|
+
decodedIndex + decodedDistance + rawAnchor.length
|
|
222
|
+
) === rawAnchor) {
|
|
223
|
+
return {
|
|
224
|
+
rawEnd: rawIndex + rawDistance,
|
|
225
|
+
decodedEnd: decodedIndex + decodedDistance
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { rawEnd: raw.length, decodedEnd: decoded.length };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const decodedTextSegments = (node, text, uri, starts) => {
|
|
234
|
+
const location = node.sourceCodeLocation;
|
|
235
|
+
if (!location) return [];
|
|
236
|
+
const raw = text.slice(location.startOffset, location.endOffset);
|
|
237
|
+
const decoded = node.value;
|
|
238
|
+
const segments = [];
|
|
239
|
+
let rawIndex = 0;
|
|
240
|
+
let decodedIndex = 0;
|
|
241
|
+
const append = (rawEnd, decodedEnd, precision) => {
|
|
242
|
+
mergeSegment(segments, {
|
|
243
|
+
body: decoded.slice(decodedIndex, decodedEnd),
|
|
244
|
+
source: rangeAt(
|
|
245
|
+
uri,
|
|
246
|
+
starts,
|
|
247
|
+
location.startOffset + rawIndex,
|
|
248
|
+
location.startOffset + rawEnd
|
|
249
|
+
),
|
|
250
|
+
precision
|
|
251
|
+
});
|
|
252
|
+
rawIndex = rawEnd;
|
|
253
|
+
decodedIndex = decodedEnd;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
while (rawIndex < raw.length || decodedIndex < decoded.length) {
|
|
257
|
+
if (raw[rawIndex] === "&") {
|
|
258
|
+
const entity = entityAt(raw, rawIndex, decoded, decodedIndex);
|
|
259
|
+
if (entity) {
|
|
260
|
+
append(
|
|
261
|
+
rawIndex + entity.rawLength,
|
|
262
|
+
decodedIndex + entity.value.length,
|
|
263
|
+
"coarse"
|
|
264
|
+
);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (raw[rawIndex] === decoded[decodedIndex]) {
|
|
269
|
+
let end = rawIndex + 1;
|
|
270
|
+
while (end < raw.length &&
|
|
271
|
+
raw[end] !== "&" &&
|
|
272
|
+
raw[end] !== "\r" &&
|
|
273
|
+
raw[end] === decoded[decodedIndex + end - rawIndex]) {
|
|
274
|
+
end += 1;
|
|
275
|
+
}
|
|
276
|
+
append(end, decodedIndex + end - rawIndex, "exact");
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (raw.startsWith("\r\n", rawIndex) &&
|
|
280
|
+
decoded[decodedIndex] === "\n") {
|
|
281
|
+
append(rawIndex + 2, decodedIndex + 1, "coarse");
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (raw[rawIndex] === "\r" && decoded[decodedIndex] === "\n") {
|
|
285
|
+
append(rawIndex + 1, decodedIndex + 1, "coarse");
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const boundary = fallbackBoundary(
|
|
289
|
+
raw,
|
|
290
|
+
rawIndex,
|
|
291
|
+
decoded,
|
|
292
|
+
decodedIndex
|
|
293
|
+
);
|
|
294
|
+
append(boundary.rawEnd, boundary.decodedEnd, "coarse");
|
|
295
|
+
}
|
|
296
|
+
return segments;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const segmentsForCode = (node, text, uri, starts) => {
|
|
300
|
+
const textNodes = descendants(node, (child) => child.nodeName === "#text");
|
|
301
|
+
const segments = textNodes.flatMap((child) =>
|
|
302
|
+
decodedTextSegments(child, text, uri, starts)
|
|
303
|
+
);
|
|
304
|
+
if (segments.length) return segments;
|
|
305
|
+
const start = node.sourceCodeLocation?.startTag?.endOffset ??
|
|
306
|
+
node.sourceCodeLocation?.startOffset ?? 0;
|
|
307
|
+
const end = node.sourceCodeLocation?.endTag?.startOffset ?? start;
|
|
308
|
+
return [{
|
|
309
|
+
body: "",
|
|
310
|
+
source: rangeAt(uri, starts, start, end),
|
|
311
|
+
precision: "exact"
|
|
312
|
+
}];
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const languageFrom = (code, candidate) => {
|
|
316
|
+
const explicit = ravelAttribute(code, "language") ??
|
|
317
|
+
ravelAttribute(candidate.node, "language");
|
|
318
|
+
if (explicit) return explicit;
|
|
319
|
+
const classes = (attributeValue(code, "class") ?? "").split(/\s+/);
|
|
320
|
+
return classes.find((entry) => entry.startsWith("language-"))
|
|
321
|
+
?.slice("language-".length) || null;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const runSelected = (options, names) =>
|
|
325
|
+
options.run === true ||
|
|
326
|
+
(Array.isArray(options.run) &&
|
|
327
|
+
names.some((name) => options.run.includes(name)));
|
|
328
|
+
|
|
329
|
+
const truthyAttribute = (value) =>
|
|
330
|
+
value === "" || /^(?:1|yes|true|on)$/i.test(value ?? "");
|
|
331
|
+
|
|
332
|
+
const virtualRangeSource = (segments, start, end) => {
|
|
333
|
+
const overlapping = segments.filter((segment) =>
|
|
334
|
+
start < segment.generated.end && end > segment.generated.start
|
|
335
|
+
);
|
|
336
|
+
if (!overlapping.length) return null;
|
|
337
|
+
return {
|
|
338
|
+
uri: overlapping[0].source.uri,
|
|
339
|
+
range: {
|
|
340
|
+
start: overlapping[0].source.range.start,
|
|
341
|
+
end: overlapping.at(-1).source.range.end
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const underscoreReferencesFrom = (body, segments) => {
|
|
347
|
+
const references = [];
|
|
348
|
+
const pattern = /(?:\\[1-9][0-9]*)?_(["'`])([\s\S]*?)\1/g;
|
|
349
|
+
for (const match of body.matchAll(pattern)) {
|
|
350
|
+
const prefix = match[0].indexOf("_");
|
|
351
|
+
const start = match.index + prefix;
|
|
352
|
+
const source = virtualRangeSource(
|
|
353
|
+
segments,
|
|
354
|
+
start,
|
|
355
|
+
match.index + match[0].length
|
|
356
|
+
);
|
|
357
|
+
if (source) references.push({ targetText: match[2], source });
|
|
358
|
+
}
|
|
359
|
+
return references;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const directiveFrom = (
|
|
363
|
+
node,
|
|
364
|
+
document,
|
|
365
|
+
text,
|
|
366
|
+
uri,
|
|
367
|
+
starts,
|
|
368
|
+
diagnostics
|
|
369
|
+
) => {
|
|
370
|
+
const effect = ravelAttribute(node, "effect");
|
|
371
|
+
if (!effect) return null;
|
|
372
|
+
const source = locationRange(node.sourceCodeLocation, uri, starts);
|
|
373
|
+
const target = ravelAttribute(node, "target") ??
|
|
374
|
+
attributeValue(node, "href") ??
|
|
375
|
+
attributeValue(node, "value");
|
|
376
|
+
const from = ravelAttribute(node, "from");
|
|
377
|
+
if (effect === "write") {
|
|
378
|
+
if (!target || !from) {
|
|
379
|
+
diagnostics.push(diagnostic(
|
|
380
|
+
"LPH101",
|
|
381
|
+
"HTML write links require a target and data-ravel-from.",
|
|
382
|
+
source
|
|
383
|
+
));
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
kind: "out",
|
|
388
|
+
name: target,
|
|
389
|
+
from: from.includes("::") ? from : document + "::" + from,
|
|
390
|
+
source
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (effect === "read") {
|
|
394
|
+
if (!target) {
|
|
395
|
+
diagnostics.push(diagnostic(
|
|
396
|
+
"LPH101",
|
|
397
|
+
"HTML read links require an href or data-ravel-target.",
|
|
398
|
+
source
|
|
399
|
+
));
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const alias = ravelAttribute(node, "as");
|
|
403
|
+
return {
|
|
404
|
+
kind: "in",
|
|
405
|
+
target,
|
|
406
|
+
source,
|
|
407
|
+
metadata: {
|
|
408
|
+
adapter: "html",
|
|
409
|
+
...(alias ? { legacy: { alias } } : {})
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (effect === "derive") {
|
|
414
|
+
const using = ravelAttribute(node, "using") ??
|
|
415
|
+
ravelAttribute(node, "pipe");
|
|
416
|
+
if (!target || !from || !using) {
|
|
417
|
+
diagnostics.push(diagnostic(
|
|
418
|
+
"LPH101",
|
|
419
|
+
"HTML derive elements require target, data-ravel-from, and data-ravel-using.",
|
|
420
|
+
source
|
|
421
|
+
));
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const usingSource = ravelAttributeSource(
|
|
425
|
+
node,
|
|
426
|
+
ravelAttribute(node, "using") !== undefined ? "using" : "pipe",
|
|
427
|
+
text,
|
|
428
|
+
uri,
|
|
429
|
+
starts
|
|
430
|
+
);
|
|
431
|
+
const parsed = parseDefinitionPipeline(using, usingSource);
|
|
432
|
+
diagnostics.push(...parsed.diagnostics);
|
|
433
|
+
return {
|
|
434
|
+
kind: "create",
|
|
435
|
+
document,
|
|
436
|
+
name: target,
|
|
437
|
+
compose: [
|
|
438
|
+
{
|
|
439
|
+
kind: "append",
|
|
440
|
+
reference: from,
|
|
441
|
+
source: ravelAttributeSource(node, "from", text, uri, starts)
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
kind: "pipe",
|
|
445
|
+
steps: parsed.pipeline.map((step) => ({
|
|
446
|
+
type: "transform",
|
|
447
|
+
name: step.name,
|
|
448
|
+
arguments: step.arguments ?? [],
|
|
449
|
+
source: usingSource
|
|
450
|
+
})),
|
|
451
|
+
source: usingSource
|
|
452
|
+
}
|
|
453
|
+
],
|
|
454
|
+
source
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
diagnostics.push(diagnostic(
|
|
458
|
+
"LPH101",
|
|
459
|
+
"Unknown HTML Ravel effect: " + effect + ".",
|
|
460
|
+
source
|
|
461
|
+
));
|
|
462
|
+
return null;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export const htmlToMap = (text, options = {}) => {
|
|
466
|
+
const uri = options.uri ?? "document.html";
|
|
467
|
+
const starts = lineStarts(text);
|
|
468
|
+
const diagnostics = [];
|
|
469
|
+
const parseErrors = [];
|
|
470
|
+
const documentNode = parse(text, {
|
|
471
|
+
scriptingEnabled: false,
|
|
472
|
+
sourceCodeLocationInfo: true,
|
|
473
|
+
onParseError: (error) => parseErrors.push(error)
|
|
474
|
+
});
|
|
475
|
+
for (const error of parseErrors) {
|
|
476
|
+
if (error.code === "missing-doctype") continue;
|
|
477
|
+
diagnostics.push(diagnostic(
|
|
478
|
+
"LPH110",
|
|
479
|
+
"HTML parse error: " + error.code + ".",
|
|
480
|
+
rangeAt(
|
|
481
|
+
uri,
|
|
482
|
+
starts,
|
|
483
|
+
error.startOffset ?? 0,
|
|
484
|
+
error.endOffset ?? error.startOffset ?? 0
|
|
485
|
+
),
|
|
486
|
+
"warning"
|
|
487
|
+
));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const nodes = [];
|
|
491
|
+
walk(documentNode, (node) => nodes.push(node));
|
|
492
|
+
const configuredDocument = nodes
|
|
493
|
+
.filter((node) => node.tagName === "meta")
|
|
494
|
+
.find((node) => ["ravel-document", "lp-document"].includes(
|
|
495
|
+
attributeValue(node, "name")
|
|
496
|
+
));
|
|
497
|
+
const documentId = options.document ??
|
|
498
|
+
attributeValue(configuredDocument, "content") ??
|
|
499
|
+
defaultDocumentId(uri);
|
|
500
|
+
if (!componentPattern.test(documentId ?? "")) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
"HTML document identity must be a lowercase identifier: " +
|
|
503
|
+
String(documentId)
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const ids = new Map();
|
|
508
|
+
for (const node of nodes) {
|
|
509
|
+
const id = attributeValue(node, "id");
|
|
510
|
+
if (!id || !node.sourceCodeLocation) continue;
|
|
511
|
+
if (ids.has(id)) {
|
|
512
|
+
diagnostics.push(diagnostic(
|
|
513
|
+
"LPH103",
|
|
514
|
+
"HTML element IDs must be unique: " + id + ".",
|
|
515
|
+
attributeSource(node, "id", text, uri, starts)
|
|
516
|
+
));
|
|
517
|
+
} else {
|
|
518
|
+
ids.set(id, node);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const candidateNodes = nodes.filter((node) =>
|
|
523
|
+
pieceTags.has(node.tagName) &&
|
|
524
|
+
ravelAttribute(node, "piece") !== undefined &&
|
|
525
|
+
node.sourceCodeLocation
|
|
526
|
+
);
|
|
527
|
+
const candidates = [];
|
|
528
|
+
const candidateByNode = new Map();
|
|
529
|
+
for (const node of candidateNodes) {
|
|
530
|
+
const declaration = locationRange(node.sourceCodeLocation, uri, starts);
|
|
531
|
+
const authored = ravelAttribute(node, "piece");
|
|
532
|
+
const split = splitNamePipeline(authored);
|
|
533
|
+
const explicitPipeline = ravelAttribute(node, "pipe");
|
|
534
|
+
if (split.pipe !== null && explicitPipeline !== undefined &&
|
|
535
|
+
split.pipe !== explicitPipeline) {
|
|
536
|
+
diagnostics.push(diagnostic(
|
|
537
|
+
"LPH113",
|
|
538
|
+
"HTML piece name and data-ravel-pipe declare conflicting pipelines.",
|
|
539
|
+
declaration
|
|
540
|
+
));
|
|
541
|
+
}
|
|
542
|
+
const canonical = semanticComponent(split.name);
|
|
543
|
+
if (!canonical) {
|
|
544
|
+
diagnostics.push(diagnostic(
|
|
545
|
+
"LPH101",
|
|
546
|
+
"HTML data-ravel-piece does not produce a usable Ravel ID.",
|
|
547
|
+
declaration
|
|
548
|
+
));
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const titleTag = node.tagName === "figure"
|
|
552
|
+
? "figcaption"
|
|
553
|
+
: /^(?:h[1-6])$/;
|
|
554
|
+
const titleNode = descendants(
|
|
555
|
+
node,
|
|
556
|
+
(child) => typeof titleTag === "string"
|
|
557
|
+
? child.tagName === titleTag
|
|
558
|
+
: titleTag.test(child.tagName ?? ""),
|
|
559
|
+
(child) => candidateNodes.includes(child)
|
|
560
|
+
)[0];
|
|
561
|
+
const displayName = titleNode
|
|
562
|
+
? normalizedText(titleNode, new Set(["code"]))
|
|
563
|
+
: "";
|
|
564
|
+
if (!displayName) {
|
|
565
|
+
diagnostics.push(diagnostic(
|
|
566
|
+
"LPH102",
|
|
567
|
+
"HTML pieces should have a visible heading or figcaption.",
|
|
568
|
+
declaration,
|
|
569
|
+
"warning"
|
|
570
|
+
));
|
|
571
|
+
}
|
|
572
|
+
const pipelineText = explicitPipeline ?? split.pipe;
|
|
573
|
+
const pipelineSource = explicitPipeline !== undefined
|
|
574
|
+
? ravelAttributeSource(node, "pipe", text, uri, starts)
|
|
575
|
+
: (() => {
|
|
576
|
+
const pieceSource = ravelAttributeSource(
|
|
577
|
+
node,
|
|
578
|
+
"piece",
|
|
579
|
+
text,
|
|
580
|
+
uri,
|
|
581
|
+
starts
|
|
582
|
+
);
|
|
583
|
+
const afterPipe = authored.slice(split.pipeIndex + 1);
|
|
584
|
+
const whitespace = afterPipe.search(/\S|$/);
|
|
585
|
+
const start = pieceSource.range.start.offset +
|
|
586
|
+
split.pipeIndex + 1 + whitespace;
|
|
587
|
+
return rangeAt(
|
|
588
|
+
uri,
|
|
589
|
+
starts,
|
|
590
|
+
start,
|
|
591
|
+
start + (split.pipe?.length ?? 0)
|
|
592
|
+
);
|
|
593
|
+
})();
|
|
594
|
+
const parsed = pipelineText
|
|
595
|
+
? parseDefinitionPipeline(pipelineText, pipelineSource)
|
|
596
|
+
: { pipeline: [], diagnostics: [] };
|
|
597
|
+
diagnostics.push(...parsed.diagnostics);
|
|
598
|
+
const label = attributeValue(node, "id") ?? null;
|
|
599
|
+
const requestedRun = runSelected(
|
|
600
|
+
options,
|
|
601
|
+
[split.name, canonical, label].filter(Boolean)
|
|
602
|
+
) || truthyAttribute(ravelAttribute(node, "run"));
|
|
603
|
+
const candidate = {
|
|
604
|
+
node,
|
|
605
|
+
authoredName: split.name,
|
|
606
|
+
canonical,
|
|
607
|
+
displayName: displayName || split.name,
|
|
608
|
+
label,
|
|
609
|
+
pipeline: parsed.pipeline,
|
|
610
|
+
pipelineSource: pipelineText ? pipelineSource : null,
|
|
611
|
+
declaration,
|
|
612
|
+
fragments: [],
|
|
613
|
+
codeBlocks: [],
|
|
614
|
+
run: requestedRun,
|
|
615
|
+
provider: ravelAttribute(node, "provider") ?? options.provider ?? null,
|
|
616
|
+
order: node.sourceCodeLocation.startOffset
|
|
617
|
+
};
|
|
618
|
+
candidates.push(candidate);
|
|
619
|
+
candidateByNode.set(node, candidate);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const nearestCandidate = (node) => {
|
|
623
|
+
let current = node.parentNode;
|
|
624
|
+
while (current) {
|
|
625
|
+
if (candidateByNode.has(current)) return candidateByNode.get(current);
|
|
626
|
+
current = current.parentNode;
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
};
|
|
630
|
+
for (const code of nodes.filter((node) =>
|
|
631
|
+
node.tagName === "code" && node.parentNode?.tagName === "pre"
|
|
632
|
+
)) {
|
|
633
|
+
const owner = nearestCandidate(code);
|
|
634
|
+
if (!owner) continue;
|
|
635
|
+
const language = languageFrom(code, owner);
|
|
636
|
+
const segments = segmentsForCode(code, text, uri, starts);
|
|
637
|
+
const body = segments.map((segment) => segment.body).join("");
|
|
638
|
+
const generated = [];
|
|
639
|
+
let cursor = 0;
|
|
640
|
+
for (const segment of segments) {
|
|
641
|
+
generated.push({
|
|
642
|
+
...segment,
|
|
643
|
+
generated: {
|
|
644
|
+
start: cursor,
|
|
645
|
+
end: cursor + segment.body.length
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
cursor += segment.body.length;
|
|
649
|
+
}
|
|
650
|
+
const broadSource = rangeAt(
|
|
651
|
+
uri,
|
|
652
|
+
starts,
|
|
653
|
+
code.sourceCodeLocation?.startTag?.endOffset ??
|
|
654
|
+
code.sourceCodeLocation.startOffset,
|
|
655
|
+
code.sourceCodeLocation?.endTag?.startOffset ??
|
|
656
|
+
code.sourceCodeLocation.endOffset
|
|
657
|
+
);
|
|
658
|
+
owner.fragments.push(...generated.map((segment) => ({
|
|
659
|
+
body: segment.body,
|
|
660
|
+
source: segment.source,
|
|
661
|
+
precision: segment.precision
|
|
662
|
+
})));
|
|
663
|
+
owner.codeBlocks.push({
|
|
664
|
+
body,
|
|
665
|
+
language,
|
|
666
|
+
source: broadSource,
|
|
667
|
+
declaration: locationRange(code.sourceCodeLocation, uri, starts),
|
|
668
|
+
segments: generated
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
candidates.sort((left, right) => left.order - right.order);
|
|
673
|
+
const aliases = {};
|
|
674
|
+
const chunks = [];
|
|
675
|
+
const definitions = [];
|
|
676
|
+
const references = [];
|
|
677
|
+
const semanticOwners = new Map();
|
|
678
|
+
const labelToPiece = new Map();
|
|
679
|
+
for (const candidate of candidates) {
|
|
680
|
+
const id = documentId + "::" + candidate.canonical;
|
|
681
|
+
if (semanticOwners.has(id)) {
|
|
682
|
+
diagnostics.push(diagnostic(
|
|
683
|
+
"LPH103",
|
|
684
|
+
"HTML semantic piece IDs must be unique: " + id + ".",
|
|
685
|
+
candidate.declaration
|
|
686
|
+
));
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
semanticOwners.set(id, candidate);
|
|
690
|
+
if (candidate.label && !labelToPiece.has(candidate.label)) {
|
|
691
|
+
labelToPiece.set(candidate.label, id);
|
|
692
|
+
}
|
|
693
|
+
for (const alias of [
|
|
694
|
+
candidate.authoredName,
|
|
695
|
+
candidate.canonical,
|
|
696
|
+
candidate.label
|
|
697
|
+
].filter(Boolean)) {
|
|
698
|
+
aliases[alias] = candidate.canonical;
|
|
699
|
+
}
|
|
700
|
+
const languages = candidate.codeBlocks
|
|
701
|
+
.map((block) => block.language)
|
|
702
|
+
.filter(Boolean);
|
|
703
|
+
const language = languages.length &&
|
|
704
|
+
languages.every((entry) => entry === languages[0])
|
|
705
|
+
? languages[0]
|
|
706
|
+
: null;
|
|
707
|
+
if (new Set(languages).size > 1) {
|
|
708
|
+
diagnostics.push(diagnostic(
|
|
709
|
+
"LPH113",
|
|
710
|
+
"HTML piece fragments have conflicting source languages.",
|
|
711
|
+
candidate.declaration
|
|
712
|
+
));
|
|
713
|
+
}
|
|
714
|
+
const body = candidate.fragments.map((fragment) => fragment.body).join("");
|
|
715
|
+
const chunkSegments = [];
|
|
716
|
+
let bodyOffset = 0;
|
|
717
|
+
for (const fragment of candidate.fragments) {
|
|
718
|
+
chunkSegments.push({
|
|
719
|
+
...fragment,
|
|
720
|
+
generated: {
|
|
721
|
+
start: bodyOffset,
|
|
722
|
+
end: bodyOffset + fragment.body.length
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
bodyOffset += fragment.body.length;
|
|
726
|
+
}
|
|
727
|
+
for (const reference of underscoreReferencesFrom(body, chunkSegments)) {
|
|
728
|
+
references.push({ ownerPieceId: id, ...reference });
|
|
729
|
+
}
|
|
730
|
+
const chunk = {
|
|
731
|
+
id,
|
|
732
|
+
identity: {
|
|
733
|
+
document: documentId,
|
|
734
|
+
chunk: candidate.canonical,
|
|
735
|
+
minor: null,
|
|
736
|
+
type: null
|
|
737
|
+
},
|
|
738
|
+
name: candidate.displayName,
|
|
739
|
+
body,
|
|
740
|
+
definitionPipeline: candidate.pipeline,
|
|
741
|
+
metadata: {
|
|
742
|
+
...(language ? { language } : {}),
|
|
743
|
+
tags: [],
|
|
744
|
+
data: {
|
|
745
|
+
ravel: {
|
|
746
|
+
adapter: "html",
|
|
747
|
+
displayName: candidate.displayName,
|
|
748
|
+
...(candidate.label
|
|
749
|
+
? { renderedAnchor: candidate.label }
|
|
750
|
+
: {}),
|
|
751
|
+
referenceSyntax: {
|
|
752
|
+
noweb: false,
|
|
753
|
+
underscore: true,
|
|
754
|
+
aliases
|
|
755
|
+
},
|
|
756
|
+
...(candidate.run ? { run: true } : {}),
|
|
757
|
+
...(candidate.run && candidate.provider
|
|
758
|
+
? { provider: candidate.provider }
|
|
759
|
+
: {})
|
|
760
|
+
},
|
|
761
|
+
html: {
|
|
762
|
+
element: candidate.node.tagName,
|
|
763
|
+
label: candidate.label,
|
|
764
|
+
attributes: attributesOf(candidate.node),
|
|
765
|
+
fragments: candidate.codeBlocks.map((block) => ({
|
|
766
|
+
source: block.source,
|
|
767
|
+
declaration: block.declaration,
|
|
768
|
+
language: block.language,
|
|
769
|
+
entities: block.segments
|
|
770
|
+
.filter((segment) => segment.precision === "coarse")
|
|
771
|
+
.map((segment) => ({
|
|
772
|
+
generated: segment.generated,
|
|
773
|
+
source: segment.source,
|
|
774
|
+
value: segment.body
|
|
775
|
+
}))
|
|
776
|
+
}))
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
source: candidate.fragments[0]?.source ?? candidate.declaration,
|
|
781
|
+
fragments: candidate.fragments
|
|
782
|
+
};
|
|
783
|
+
chunks.push(chunk);
|
|
784
|
+
definitions.push({
|
|
785
|
+
pieceId: id,
|
|
786
|
+
declaration: candidate.declaration,
|
|
787
|
+
fragments: candidate.codeBlocks.map((block) => block.source),
|
|
788
|
+
displayName: candidate.displayName,
|
|
789
|
+
...(candidate.label
|
|
790
|
+
? {
|
|
791
|
+
sourceAnchor: candidate.label,
|
|
792
|
+
renderedAnchor: candidate.label
|
|
793
|
+
}
|
|
794
|
+
: {}),
|
|
795
|
+
...(candidate.pipelineSource
|
|
796
|
+
? { pipeline: candidate.pipelineSource }
|
|
797
|
+
: {})
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const directives = nodes
|
|
802
|
+
.filter((node) =>
|
|
803
|
+
(node.tagName === "a" || node.tagName === "data") &&
|
|
804
|
+
ravelAttribute(node, "effect") !== undefined
|
|
805
|
+
)
|
|
806
|
+
.map((node) => directiveFrom(
|
|
807
|
+
node,
|
|
808
|
+
documentId,
|
|
809
|
+
text,
|
|
810
|
+
uri,
|
|
811
|
+
starts,
|
|
812
|
+
diagnostics
|
|
813
|
+
))
|
|
814
|
+
.filter(Boolean);
|
|
815
|
+
const navigation = nodes
|
|
816
|
+
.filter((node) =>
|
|
817
|
+
node.tagName === "a" &&
|
|
818
|
+
attributeValue(node, "href")?.startsWith("#") &&
|
|
819
|
+
ravelAttribute(node, "effect") === undefined
|
|
820
|
+
)
|
|
821
|
+
.map((node) => {
|
|
822
|
+
const targetLabel = attributeValue(node, "href").slice(1);
|
|
823
|
+
const targetPieceId = labelToPiece.get(targetLabel);
|
|
824
|
+
return targetPieceId
|
|
825
|
+
? {
|
|
826
|
+
targetPieceId,
|
|
827
|
+
targetLabel,
|
|
828
|
+
syntax: "anchor",
|
|
829
|
+
source: locationRange(node.sourceCodeLocation, uri, starts)
|
|
830
|
+
}
|
|
831
|
+
: null;
|
|
832
|
+
})
|
|
833
|
+
.filter(Boolean);
|
|
834
|
+
const entities = chunks.flatMap((chunk) =>
|
|
835
|
+
chunk.metadata.data.html.fragments.flatMap((fragment) =>
|
|
836
|
+
fragment.entities.map((entity) => ({
|
|
837
|
+
pieceId: chunk.id,
|
|
838
|
+
...entity
|
|
839
|
+
}))
|
|
840
|
+
)
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
map: {
|
|
845
|
+
version: 1,
|
|
846
|
+
document: {
|
|
847
|
+
id: documentId,
|
|
848
|
+
uri,
|
|
849
|
+
format: "html+ravel-v1"
|
|
850
|
+
},
|
|
851
|
+
chunks,
|
|
852
|
+
directives,
|
|
853
|
+
metadata: {
|
|
854
|
+
adapter: "html",
|
|
855
|
+
crossReferences: navigation,
|
|
856
|
+
sourceBoundary: "authored-html"
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
diagnostics,
|
|
860
|
+
surface: {
|
|
861
|
+
definitions,
|
|
862
|
+
references,
|
|
863
|
+
directives: [...directives],
|
|
864
|
+
navigation,
|
|
865
|
+
entities
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
};
|