lens-content-processor 0.40.0 → 0.42.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/dist/content-schema.js +6 -0
- package/dist/content-schema.js.map +1 -1
- package/dist/flattener/index.js +94 -10
- package/dist/flattener/index.js.map +1 -1
- package/dist/flattener/resolve-text-links.d.ts +2 -1
- package/dist/flattener/resolve-text-links.js +7 -26
- package/dist/flattener/resolve-text-links.js.map +1 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/lens.d.ts +6 -1
- package/dist/parser/lens.js +56 -1
- package/dist/parser/lens.js.map +1 -1
- package/dist/parser/sections.js +1 -0
- package/dist/parser/sections.js.map +1 -1
- package/dist/parser/widget.d.ts +25 -0
- package/dist/parser/widget.js +367 -0
- package/dist/parser/widget.js.map +1 -0
- package/dist/same-page-links.d.ts +11 -0
- package/dist/same-page-links.js +109 -0
- package/dist/same-page-links.js.map +1 -0
- package/dist/validator/article-structure.js +6 -51
- package/dist/validator/article-structure.js.map +1 -1
- package/dist/validator/criticmarkup.js +1 -0
- package/dist/validator/criticmarkup.js.map +1 -1
- package/dist/validator/same-lens-links.d.ts +2 -0
- package/dist/validator/same-lens-links.js +129 -72
- package/dist/validator/same-lens-links.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// src/parser/widget.ts
|
|
2
|
+
//
|
|
3
|
+
// A widget is a self-contained interactive HTML page authored in the
|
|
4
|
+
// `widgets/` folder. The file is Markdown only by extension: YAML frontmatter
|
|
5
|
+
// carries the metadata, the body is the full HTML document that the platform
|
|
6
|
+
// renders for learners inside a sandboxed iframe. Keeping the `.md` extension
|
|
7
|
+
// means widgets flow through the editor, relay-git-sync, promotion and CI
|
|
8
|
+
// exactly like every other content file; only this parser treats the body
|
|
9
|
+
// as HTML.
|
|
10
|
+
import { parse as parseHtml, defaultTreeAdapter, Tokenizer, TokenizerMode, } from "parse5";
|
|
11
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
12
|
+
import { validateFrontmatter } from "../validator/validate-frontmatter.js";
|
|
13
|
+
import { stripAuthoringMarkup } from "../authoring-markup.js";
|
|
14
|
+
/** Widgets are inlined into module JSON; above this the page payload suffers. */
|
|
15
|
+
export const MAX_WIDGET_HTML_BYTES = 400 * 1024;
|
|
16
|
+
// parse5 reports every HTML5 parse error, including ones browsers recover from
|
|
17
|
+
// identically to the author's intent. These never change what renders.
|
|
18
|
+
const BENIGN_PARSE_ERRORS = new Set([
|
|
19
|
+
"missing-doctype",
|
|
20
|
+
"open-elements-left-after-eof",
|
|
21
|
+
"duplicate-attribute",
|
|
22
|
+
]);
|
|
23
|
+
// Elements whose text is not learner-visible prose.
|
|
24
|
+
const SKIPPED_TEXT_ELEMENTS = new Set([
|
|
25
|
+
"script",
|
|
26
|
+
"style",
|
|
27
|
+
"svg",
|
|
28
|
+
"noscript",
|
|
29
|
+
"template",
|
|
30
|
+
"head",
|
|
31
|
+
]);
|
|
32
|
+
const BLOCK_ELEMENTS = new Set([
|
|
33
|
+
"p",
|
|
34
|
+
"div",
|
|
35
|
+
"section",
|
|
36
|
+
"article",
|
|
37
|
+
"header",
|
|
38
|
+
"footer",
|
|
39
|
+
"main",
|
|
40
|
+
"aside",
|
|
41
|
+
"nav",
|
|
42
|
+
"h1",
|
|
43
|
+
"h2",
|
|
44
|
+
"h3",
|
|
45
|
+
"h4",
|
|
46
|
+
"h5",
|
|
47
|
+
"h6",
|
|
48
|
+
"li",
|
|
49
|
+
"ul",
|
|
50
|
+
"ol",
|
|
51
|
+
"tr",
|
|
52
|
+
"td",
|
|
53
|
+
"th",
|
|
54
|
+
"table",
|
|
55
|
+
"blockquote",
|
|
56
|
+
"pre",
|
|
57
|
+
"figure",
|
|
58
|
+
"figcaption",
|
|
59
|
+
"button",
|
|
60
|
+
"label",
|
|
61
|
+
"br",
|
|
62
|
+
"hr",
|
|
63
|
+
"summary",
|
|
64
|
+
"details",
|
|
65
|
+
"dt",
|
|
66
|
+
"dd",
|
|
67
|
+
]);
|
|
68
|
+
export function isWidgetPath(path) {
|
|
69
|
+
return path.startsWith("widgets/") || path.includes("/widgets/");
|
|
70
|
+
}
|
|
71
|
+
export function parseWidget(content, file) {
|
|
72
|
+
const errors = [];
|
|
73
|
+
const fm = parseFrontmatter(stripAuthoringMarkup(content), file);
|
|
74
|
+
if (fm.error) {
|
|
75
|
+
errors.push(fm.error);
|
|
76
|
+
return { widget: null, errors };
|
|
77
|
+
}
|
|
78
|
+
errors.push(...validateFrontmatter(fm.frontmatter, "widget", file));
|
|
79
|
+
const html = fm.body.trim();
|
|
80
|
+
if (!html) {
|
|
81
|
+
errors.push({
|
|
82
|
+
file,
|
|
83
|
+
line: fm.bodyStartLine,
|
|
84
|
+
message: "Widget body is empty",
|
|
85
|
+
suggestion: "Write the widget's HTML document below the frontmatter (a complete <!doctype html> page with its own CSS and JS)",
|
|
86
|
+
severity: "error",
|
|
87
|
+
});
|
|
88
|
+
return { widget: null, errors };
|
|
89
|
+
}
|
|
90
|
+
if (Buffer.byteLength(html, "utf8") > MAX_WIDGET_HTML_BYTES) {
|
|
91
|
+
errors.push({
|
|
92
|
+
file,
|
|
93
|
+
line: fm.bodyStartLine,
|
|
94
|
+
message: `Widget HTML is larger than ${MAX_WIDGET_HTML_BYTES / 1024}KB`,
|
|
95
|
+
suggestion: "Widgets are inlined into every module that uses them; move large data or media to an external URL",
|
|
96
|
+
severity: "error",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const { document, parseErrors } = parseHtmlDocument(html);
|
|
100
|
+
// Tag-balance findings first: they name the tag and line an author can act
|
|
101
|
+
// on, where a tokenizer error code often only says where parsing went wrong.
|
|
102
|
+
const htmlErrors = [...checkTagBalance(html), ...parseErrors];
|
|
103
|
+
for (const err of htmlErrors.slice(0, MAX_REPORTED_HTML_ERRORS)) {
|
|
104
|
+
errors.push({
|
|
105
|
+
file,
|
|
106
|
+
line: fm.bodyStartLine + err.line - 1,
|
|
107
|
+
message: `Invalid HTML: ${err.code}`,
|
|
108
|
+
suggestion: "Fix the markup so the page parses cleanly; a broken widget shows learners a 'could not be loaded' notice",
|
|
109
|
+
severity: "error",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (htmlErrors.length > MAX_REPORTED_HTML_ERRORS) {
|
|
113
|
+
errors.push({
|
|
114
|
+
file,
|
|
115
|
+
line: fm.bodyStartLine,
|
|
116
|
+
message: `Invalid HTML: ${htmlErrors.length - MAX_REPORTED_HTML_ERRORS} more problem(s) not shown`,
|
|
117
|
+
suggestion: "Fix the ones above and re-validate",
|
|
118
|
+
severity: "error",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const height = frontmatterString(fm.frontmatter.height);
|
|
122
|
+
if (height !== undefined && !isValidHeight(height)) {
|
|
123
|
+
errors.push({
|
|
124
|
+
file,
|
|
125
|
+
line: 2,
|
|
126
|
+
message: `Invalid widget height: ${height}`,
|
|
127
|
+
suggestion: 'Use "auto" (default) or a CSS length such as 480px',
|
|
128
|
+
severity: "error",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const widget = {
|
|
132
|
+
html,
|
|
133
|
+
text: extractVisibleText(document),
|
|
134
|
+
};
|
|
135
|
+
const title = frontmatterString(fm.frontmatter.title);
|
|
136
|
+
if (title)
|
|
137
|
+
widget.title = title;
|
|
138
|
+
const summary = frontmatterString(fm.frontmatter.summary_for_tutor);
|
|
139
|
+
if (summary)
|
|
140
|
+
widget.summaryForTutor = summary;
|
|
141
|
+
if (!widget.text && !summary) {
|
|
142
|
+
// A widget that builds its UI in JavaScript has no static text to give
|
|
143
|
+
// the tutor; the summary is then the tutor's only view of it.
|
|
144
|
+
errors.push({
|
|
145
|
+
file,
|
|
146
|
+
line: 2,
|
|
147
|
+
message: "Widget has no visible text in its HTML and no summary_for_tutor; the tutor cannot see what the learner does here",
|
|
148
|
+
suggestion: "Add summary_for_tutor to the frontmatter describing what the widget shows and what the learner does with it",
|
|
149
|
+
severity: "warning",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (height && isValidHeight(height))
|
|
153
|
+
widget.height = height;
|
|
154
|
+
return { widget, errors };
|
|
155
|
+
}
|
|
156
|
+
function frontmatterString(value) {
|
|
157
|
+
if (value === undefined || value === null)
|
|
158
|
+
return undefined;
|
|
159
|
+
const str = String(value).trim();
|
|
160
|
+
return str === "" ? undefined : str;
|
|
161
|
+
}
|
|
162
|
+
function isValidHeight(height) {
|
|
163
|
+
return height === "auto" || /^\d+(\.\d+)?(px|rem|em|vh)$/.test(height);
|
|
164
|
+
}
|
|
165
|
+
const MAX_REPORTED_HTML_ERRORS = 5;
|
|
166
|
+
// Browsers (and parse5) silently recover from stray or mis-nested end tags,
|
|
167
|
+
// so the HTML5 parser reports nothing for the mistakes authors actually make.
|
|
168
|
+
// This pass walks the token stream with an open-element stack for the tags
|
|
169
|
+
// that have no implicit-close rules and reports what the recovery hides.
|
|
170
|
+
const STRICT_TAGS = new Set([
|
|
171
|
+
"div",
|
|
172
|
+
"section",
|
|
173
|
+
"article",
|
|
174
|
+
"main",
|
|
175
|
+
"header",
|
|
176
|
+
"footer",
|
|
177
|
+
"aside",
|
|
178
|
+
"nav",
|
|
179
|
+
"span",
|
|
180
|
+
"a",
|
|
181
|
+
"button",
|
|
182
|
+
"label",
|
|
183
|
+
"table",
|
|
184
|
+
"ul",
|
|
185
|
+
"ol",
|
|
186
|
+
"form",
|
|
187
|
+
"select",
|
|
188
|
+
"textarea",
|
|
189
|
+
"details",
|
|
190
|
+
"summary",
|
|
191
|
+
"figure",
|
|
192
|
+
"figcaption",
|
|
193
|
+
"blockquote",
|
|
194
|
+
"pre",
|
|
195
|
+
"code",
|
|
196
|
+
"em",
|
|
197
|
+
"strong",
|
|
198
|
+
"b",
|
|
199
|
+
"i",
|
|
200
|
+
"small",
|
|
201
|
+
"sup",
|
|
202
|
+
"sub",
|
|
203
|
+
"svg",
|
|
204
|
+
"h1",
|
|
205
|
+
"h2",
|
|
206
|
+
"h3",
|
|
207
|
+
"h4",
|
|
208
|
+
"h5",
|
|
209
|
+
"h6",
|
|
210
|
+
"script",
|
|
211
|
+
"style",
|
|
212
|
+
"template",
|
|
213
|
+
"iframe",
|
|
214
|
+
"canvas",
|
|
215
|
+
"video",
|
|
216
|
+
"audio",
|
|
217
|
+
]);
|
|
218
|
+
const VOID_TAGS = new Set([
|
|
219
|
+
"area",
|
|
220
|
+
"base",
|
|
221
|
+
"br",
|
|
222
|
+
"col",
|
|
223
|
+
"embed",
|
|
224
|
+
"hr",
|
|
225
|
+
"img",
|
|
226
|
+
"input",
|
|
227
|
+
"link",
|
|
228
|
+
"meta",
|
|
229
|
+
"source",
|
|
230
|
+
"track",
|
|
231
|
+
"wbr",
|
|
232
|
+
]);
|
|
233
|
+
// Unclosed at EOF, these swallow the rest of the document.
|
|
234
|
+
const MUST_CLOSE_TAGS = new Set(["script", "style", "template", "textarea"]);
|
|
235
|
+
const RAWTEXT_TAGS = new Set(["style", "xmp", "iframe", "noembed", "noframes"]);
|
|
236
|
+
const RCDATA_TAGS = new Set(["textarea", "title"]);
|
|
237
|
+
function checkTagBalance(html) {
|
|
238
|
+
const errors = [];
|
|
239
|
+
// Open strict elements (and every element inside <svg>, where nesting is XML-strict).
|
|
240
|
+
const stack = [];
|
|
241
|
+
let svgDepth = 0;
|
|
242
|
+
const line = (token) => token.location?.startLine ?? 1;
|
|
243
|
+
const tokenizer = new Tokenizer({ sourceCodeLocationInfo: true }, {
|
|
244
|
+
onStartTag(token) {
|
|
245
|
+
const tag = token.tagName;
|
|
246
|
+
if (tag === "script")
|
|
247
|
+
tokenizer.state = TokenizerMode.SCRIPT_DATA;
|
|
248
|
+
else if (RAWTEXT_TAGS.has(tag))
|
|
249
|
+
tokenizer.state = TokenizerMode.RAWTEXT;
|
|
250
|
+
else if (RCDATA_TAGS.has(tag))
|
|
251
|
+
tokenizer.state = TokenizerMode.RCDATA;
|
|
252
|
+
if (svgDepth > 0 || tag === "svg") {
|
|
253
|
+
if (tag === "svg")
|
|
254
|
+
svgDepth += 1;
|
|
255
|
+
if (token.selfClosing) {
|
|
256
|
+
if (tag === "svg")
|
|
257
|
+
svgDepth -= 1;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
stack.push({ tag, line: line(token) });
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (VOID_TAGS.has(tag))
|
|
265
|
+
return;
|
|
266
|
+
if (token.selfClosing) {
|
|
267
|
+
errors.push({
|
|
268
|
+
code: `self-closing <${tag}/> is not allowed in HTML (the element stays open)`,
|
|
269
|
+
line: line(token),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (STRICT_TAGS.has(tag))
|
|
273
|
+
stack.push({ tag, line: line(token) });
|
|
274
|
+
},
|
|
275
|
+
onEndTag(token) {
|
|
276
|
+
const tag = token.tagName;
|
|
277
|
+
if (svgDepth === 0 && !STRICT_TAGS.has(tag))
|
|
278
|
+
return;
|
|
279
|
+
const index = stack.map((e) => e.tag).lastIndexOf(tag);
|
|
280
|
+
if (index === -1) {
|
|
281
|
+
errors.push({
|
|
282
|
+
code: `stray end tag </${tag}> (no open <${tag}>)`,
|
|
283
|
+
line: line(token),
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
for (let i = stack.length - 1; i > index; i--) {
|
|
288
|
+
errors.push({
|
|
289
|
+
code: `</${tag}> closes while <${stack[i].tag}> (line ${stack[i].line}) is still open`,
|
|
290
|
+
line: line(token),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const closed = stack.splice(index);
|
|
294
|
+
svgDepth -= closed.filter((e) => e.tag === "svg").length;
|
|
295
|
+
},
|
|
296
|
+
onEof() {
|
|
297
|
+
for (const open of stack) {
|
|
298
|
+
if (MUST_CLOSE_TAGS.has(open.tag)) {
|
|
299
|
+
errors.push({
|
|
300
|
+
code: `<${open.tag}> is never closed; everything after it is swallowed`,
|
|
301
|
+
line: open.line,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
onComment() { },
|
|
307
|
+
onDoctype() { },
|
|
308
|
+
onCharacter() { },
|
|
309
|
+
onWhitespaceCharacter() { },
|
|
310
|
+
onNullCharacter() { },
|
|
311
|
+
onParseError() { },
|
|
312
|
+
});
|
|
313
|
+
tokenizer.write(html, true);
|
|
314
|
+
return errors;
|
|
315
|
+
}
|
|
316
|
+
function parseHtmlDocument(html) {
|
|
317
|
+
const parseErrors = [];
|
|
318
|
+
const document = parseHtml(html, {
|
|
319
|
+
sourceCodeLocationInfo: true,
|
|
320
|
+
onParseError: (err) => {
|
|
321
|
+
if (BENIGN_PARSE_ERRORS.has(err.code))
|
|
322
|
+
return;
|
|
323
|
+
parseErrors.push({ code: err.code, line: err.startLine });
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
return { document, parseErrors };
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Learner-visible text of the document, block elements separated by blank
|
|
330
|
+
* lines, whitespace collapsed. Scripts, styles and SVG geometry are left out.
|
|
331
|
+
*/
|
|
332
|
+
export function extractVisibleText(document) {
|
|
333
|
+
const parts = [];
|
|
334
|
+
const visit = (node) => {
|
|
335
|
+
if (defaultTreeAdapter.isTextNode(node)) {
|
|
336
|
+
parts.push(node.value);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (defaultTreeAdapter.isCommentNode(node))
|
|
340
|
+
return;
|
|
341
|
+
if (defaultTreeAdapter.isElementNode(node)) {
|
|
342
|
+
const tag = node.tagName.toLowerCase();
|
|
343
|
+
if (SKIPPED_TEXT_ELEMENTS.has(tag))
|
|
344
|
+
return;
|
|
345
|
+
const block = BLOCK_ELEMENTS.has(tag);
|
|
346
|
+
if (block)
|
|
347
|
+
parts.push("\n\n");
|
|
348
|
+
for (const child of node.childNodes)
|
|
349
|
+
visit(child);
|
|
350
|
+
if (block)
|
|
351
|
+
parts.push("\n\n");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if ("childNodes" in node) {
|
|
355
|
+
for (const child of node.childNodes)
|
|
356
|
+
visit(child);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
visit(document);
|
|
360
|
+
return parts
|
|
361
|
+
.join("")
|
|
362
|
+
.split(/\n\s*\n/)
|
|
363
|
+
.map((para) => para.replace(/\s+/g, " ").trim())
|
|
364
|
+
.filter((para) => para.length > 0)
|
|
365
|
+
.join("\n\n");
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=widget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget.js","sourceRoot":"","sources":["../../src/parser/widget.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface BlockTarget {
|
|
2
|
+
kind: "heading" | "block";
|
|
3
|
+
heading?: string;
|
|
4
|
+
}
|
|
5
|
+
/** Find every valid block marker and the Markdown block it labels. */
|
|
6
|
+
export declare function collectBlockTargets(source: string): Map<string, BlockTarget>;
|
|
7
|
+
export declare function collectBlockTargetsFrom(sources: Iterable<string>): Map<string, BlockTarget>;
|
|
8
|
+
/** Mask code and math examples without changing source offsets. */
|
|
9
|
+
export declare function maskCode(value: string): string;
|
|
10
|
+
/** Convert canonical same-page wikilinks to the renderer-private block scheme. */
|
|
11
|
+
export declare function resolveSamePageLinks(content: string, targets: ReadonlyMap<string, BlockTarget>): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { toString } from "mdast-util-to-string";
|
|
2
|
+
import remarkDirective from "remark-directive";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkMath from "remark-math";
|
|
5
|
+
import remarkParse from "remark-parse";
|
|
6
|
+
import { unified } from "unified";
|
|
7
|
+
import { visit } from "unist-util-visit";
|
|
8
|
+
const BLOCK_MARKER_RE = /(?:^|\s)\^([A-Za-z0-9-]+)\s*$/;
|
|
9
|
+
function parseMarkdown(source) {
|
|
10
|
+
return unified()
|
|
11
|
+
.use(remarkParse)
|
|
12
|
+
.use(remarkGfm)
|
|
13
|
+
.use(remarkDirective)
|
|
14
|
+
// Single dollars are currency in Lens content; the renderer parses them
|
|
15
|
+
// the same way (LensMarkdown.tsx). With the default, "$5 ... $10" would
|
|
16
|
+
// become inline math and mask any [[#^id]] link between the amounts.
|
|
17
|
+
.use(remarkMath, { singleDollarTextMath: false })
|
|
18
|
+
.parse(source);
|
|
19
|
+
}
|
|
20
|
+
function targetFor(node, id) {
|
|
21
|
+
if (node.type !== "heading")
|
|
22
|
+
return { kind: "block" };
|
|
23
|
+
return {
|
|
24
|
+
kind: "heading",
|
|
25
|
+
heading: toString(node)
|
|
26
|
+
.replace(new RegExp(`(?:^|\\s+)\\^${id}\\s*$`), "")
|
|
27
|
+
.trim(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Find every valid block marker and the Markdown block it labels. */
|
|
31
|
+
export function collectBlockTargets(source) {
|
|
32
|
+
const tree = parseMarkdown(source);
|
|
33
|
+
const targets = new Map();
|
|
34
|
+
const walk = (parent) => {
|
|
35
|
+
for (let index = 0; index < parent.children.length; index += 1) {
|
|
36
|
+
const node = parent.children[index];
|
|
37
|
+
if (node.type === "paragraph" || node.type === "heading") {
|
|
38
|
+
const last = node.children.at(-1);
|
|
39
|
+
const match = last?.type === "text" && last.value.match(BLOCK_MARKER_RE);
|
|
40
|
+
if (match) {
|
|
41
|
+
const markerOnly = node.type === "paragraph" &&
|
|
42
|
+
node.children.length === 1 &&
|
|
43
|
+
last.value.trim() === `^${match[1]}`;
|
|
44
|
+
const target = markerOnly && index > 0 ? parent.children[index - 1] : node;
|
|
45
|
+
if ("children" in target) {
|
|
46
|
+
targets.set(match[1], targetFor(target, match[1]));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if ("children" in node && Array.isArray(node.children))
|
|
51
|
+
walk(node);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
walk(tree);
|
|
55
|
+
return targets;
|
|
56
|
+
}
|
|
57
|
+
export function collectBlockTargetsFrom(sources) {
|
|
58
|
+
const targets = new Map();
|
|
59
|
+
for (const source of sources) {
|
|
60
|
+
for (const [id, target] of collectBlockTargets(source))
|
|
61
|
+
targets.set(id, target);
|
|
62
|
+
}
|
|
63
|
+
return targets;
|
|
64
|
+
}
|
|
65
|
+
function markdownLabel(value) {
|
|
66
|
+
return value.replace(/([\\[\]])/g, "\\$1");
|
|
67
|
+
}
|
|
68
|
+
/** Mask code and math examples without changing source offsets. */
|
|
69
|
+
export function maskCode(value) {
|
|
70
|
+
const chars = value.split("");
|
|
71
|
+
visit(parseMarkdown(value), (node) => {
|
|
72
|
+
if (!["code", "inlineCode", "math", "inlineMath"].includes(node.type))
|
|
73
|
+
return;
|
|
74
|
+
const start = node.position?.start.offset;
|
|
75
|
+
const end = node.position?.end.offset;
|
|
76
|
+
if (start === undefined || end === undefined)
|
|
77
|
+
return;
|
|
78
|
+
for (let index = start; index < end; index += 1) {
|
|
79
|
+
if (chars[index] !== "\n")
|
|
80
|
+
chars[index] = " ";
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return chars.join("");
|
|
84
|
+
}
|
|
85
|
+
/** Convert canonical same-page wikilinks to the renderer-private block scheme. */
|
|
86
|
+
export function resolveSamePageLinks(content, targets) {
|
|
87
|
+
const matches = Array.from(maskCode(content).matchAll(/(?<!\\)(?<!!)\[\[#\^([A-Za-z0-9-]+)(?:\|([^\]]+))?\]\]/g));
|
|
88
|
+
let resolved = content;
|
|
89
|
+
for (const match of matches.reverse()) {
|
|
90
|
+
const [fullMatch, id, authoredLabel] = match;
|
|
91
|
+
if (match.index === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
// maskCode blanks inline code, so take the label from the unmasked text;
|
|
94
|
+
// masking keeps offsets, and the label always ends just before "]]".
|
|
95
|
+
const labelEnd = match.index + fullMatch.length - 2;
|
|
96
|
+
const label = (authoredLabel === undefined
|
|
97
|
+
? ""
|
|
98
|
+
: content.slice(labelEnd - authoredLabel.length, labelEnd)).trim() || targets.get(id)?.heading;
|
|
99
|
+
if (!label)
|
|
100
|
+
continue;
|
|
101
|
+
const replacement = `[${markdownLabel(label)}](anchor:block:${encodeURIComponent(id)})`;
|
|
102
|
+
resolved =
|
|
103
|
+
resolved.slice(0, match.index) +
|
|
104
|
+
replacement +
|
|
105
|
+
resolved.slice(match.index + fullMatch.length);
|
|
106
|
+
}
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=same-page-links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"same-page-links.js","sourceRoot":"","sources":["../src/same-page-links.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import { parseMarkdown } from "./markdown-parse.js";
|
|
|
4
4
|
import { stripAuthoringMarkupForValidation } from "../authoring-markup.js";
|
|
5
5
|
import { parseFrontmatter } from "../parser/frontmatter.js";
|
|
6
6
|
import { applyNextLineValidatorSuppressions, formatValidatorSuppression, VALIDATOR_SUPPRESSIONS, validatorSuppressionsForCode, } from "./suppressions.js";
|
|
7
|
+
import { validateArticleSamePageLinks } from "./same-lens-links.js";
|
|
7
8
|
const lowResolutionSuppression = VALIDATOR_SUPPRESSIONS.imageLowResolutionAiSearchExhausted;
|
|
8
9
|
const lowResolutionSuppressionGuidance = validatorSuppressionsForCode(lowResolutionSuppression.code).map((definition) => `${definition.explanation} Use: ${formatValidatorSuppression(definition)}`).join(" ");
|
|
9
10
|
const repeatedBlockSuppressionGuidance = validatorSuppressionsForCode("article.block-repeated-nearby").map((definition) => `${definition.explanation} Use: ${formatValidatorSuppression(definition)}`).join(" ");
|
|
@@ -18,14 +19,6 @@ function issue(file, bodyStartLine, code, severity, message, line, suggestion) {
|
|
|
18
19
|
...(suggestion ? { suggestion } : {}),
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
function renderedHeadingId(value) {
|
|
22
|
-
return value
|
|
23
|
-
.toLowerCase()
|
|
24
|
-
.replace(/[^a-z0-9\s-]/g, "")
|
|
25
|
-
.replace(/\s+/g, "-")
|
|
26
|
-
.replace(/-+/g, "-")
|
|
27
|
-
.slice(0, 50);
|
|
28
|
-
}
|
|
29
22
|
function lineAt(source, offset) {
|
|
30
23
|
return source.slice(0, offset).split("\n").length;
|
|
31
24
|
}
|
|
@@ -273,14 +266,16 @@ export function validateArticleStructure(content, file) {
|
|
|
273
266
|
const body = stripAuthoringMarkupForValidation(fm.body);
|
|
274
267
|
const bodyStartLine = fm.bodyStartLine;
|
|
275
268
|
const sourceUrl = fm.frontmatter.source_url;
|
|
276
|
-
const errors = [
|
|
269
|
+
const errors = [
|
|
270
|
+
...scanFences(body, file, bodyStartLine),
|
|
271
|
+
...validateArticleSamePageLinks(body, file, bodyStartLine - 1),
|
|
272
|
+
];
|
|
277
273
|
const tree = parseMarkdown(body, "gfm-directive-math");
|
|
278
274
|
const prose = proseMask(body);
|
|
279
275
|
const inlineMathScanSource = maskNodes(body, tree, new Set(["code", "inlineCode", "math", "image", "imageReference"]));
|
|
280
276
|
const definitions = new Map();
|
|
281
277
|
const referencedFootnotes = new Map();
|
|
282
278
|
const definedFootnotes = new Map();
|
|
283
|
-
const headingIds = new Set();
|
|
284
279
|
const imageOccurrences = new Map();
|
|
285
280
|
const externalSelfFragments = new Set();
|
|
286
281
|
const blocks = [];
|
|
@@ -298,35 +293,6 @@ export function validateArticleStructure(content, file) {
|
|
|
298
293
|
visit(tree, "definition", (node) => {
|
|
299
294
|
definitions.set(node.identifier.toLowerCase(), node.url);
|
|
300
295
|
});
|
|
301
|
-
// ArticleSectionWrapper pre-registers H1-H3 IDs for the table of contents.
|
|
302
|
-
// Duplicate lower-level headings fall back to their unsuffixed slug unless
|
|
303
|
-
// their exact text is also registered. Mirror that behavior here rather
|
|
304
|
-
// than applying GitHub-style suffixes to every heading.
|
|
305
|
-
const registeredHeadingIds = new Map();
|
|
306
|
-
const registeredBaseCounts = new Map();
|
|
307
|
-
visit(tree, "heading", (node) => {
|
|
308
|
-
if (node.depth > 3)
|
|
309
|
-
return;
|
|
310
|
-
const text = toString(node);
|
|
311
|
-
const base = renderedHeadingId(text);
|
|
312
|
-
const count = registeredBaseCounts.get(base) ?? 0;
|
|
313
|
-
const ids = registeredHeadingIds.get(text) ?? [];
|
|
314
|
-
ids.push(count === 0 ? base : `${base}-${count}`);
|
|
315
|
-
registeredHeadingIds.set(text, ids);
|
|
316
|
-
registeredBaseCounts.set(base, count + 1);
|
|
317
|
-
});
|
|
318
|
-
const renderedTextCounts = new Map();
|
|
319
|
-
visit(tree, "heading", (node) => {
|
|
320
|
-
const text = toString(node);
|
|
321
|
-
const registered = registeredHeadingIds.get(text);
|
|
322
|
-
if (!registered?.length) {
|
|
323
|
-
headingIds.add(renderedHeadingId(text));
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
const count = renderedTextCounts.get(text) ?? 0;
|
|
327
|
-
headingIds.add(registered[count] ?? registered[registered.length - 1]);
|
|
328
|
-
renderedTextCounts.set(text, count + 1);
|
|
329
|
-
});
|
|
330
296
|
visit(tree, "footnoteReference", (node) => {
|
|
331
297
|
referencedFootnotes.set(node.identifier.toLowerCase(), node.position?.start.line ?? 1);
|
|
332
298
|
});
|
|
@@ -381,7 +347,7 @@ export function validateArticleStructure(content, file) {
|
|
|
381
347
|
if (sourceFragment &&
|
|
382
348
|
!externalSelfFragments.has(sourceFragment)) {
|
|
383
349
|
externalSelfFragments.add(sourceFragment);
|
|
384
|
-
errors.push(issue(file, bodyStartLine, "article.external-self-fragment", "warning", `Link '#${sourceFragment}' points back to this article's source page`, line, `Use a native Markdown footnote for a note, or a
|
|
350
|
+
errors.push(issue(file, bodyStartLine, "article.external-self-fragment", "warning", `Link '#${sourceFragment}' points back to this article's source page`, line, `Use a native Markdown footnote for a note, or add a ^block-id to imported content and link to [[#^block-id|label]]. If this deliberately targets source-only content that was not imported, place this exemption immediately above the link: ${formatValidatorSuppression(externalSelfFragmentSuppression)}`));
|
|
385
351
|
}
|
|
386
352
|
}
|
|
387
353
|
const start = positioned.position?.start.offset;
|
|
@@ -595,17 +561,6 @@ export function validateArticleStructure(content, file) {
|
|
|
595
561
|
const code = raw.startsWith("![") ? "article.image-destination-empty" : "article.link-destination-empty";
|
|
596
562
|
errors.push(issue(file, bodyStartLine, code, "error", "Incomplete Markdown link or image", lineAt(prose, match.index ?? 0)));
|
|
597
563
|
}
|
|
598
|
-
for (const match of prose.matchAll(/\]\(#([^\s)]+)\)/g)) {
|
|
599
|
-
let fragment;
|
|
600
|
-
try {
|
|
601
|
-
fragment = decodeURIComponent(match[1]).toLowerCase();
|
|
602
|
-
}
|
|
603
|
-
catch {
|
|
604
|
-
fragment = match[1].toLowerCase();
|
|
605
|
-
}
|
|
606
|
-
if (!headingIds.has(fragment))
|
|
607
|
-
errors.push(issue(file, bodyStartLine, "article.fragment-target-missing", "error", `Fragment target '#${match[1]}' does not resolve to a rendered heading`, lineAt(prose, match.index ?? 0)));
|
|
608
|
-
}
|
|
609
564
|
for (const match of prose.matchAll(/^#{1,6}\s+(Image|Figure|Photo)\s*$/gim)) {
|
|
610
565
|
const line = lineAt(prose, match.index ?? 0);
|
|
611
566
|
const nearby = body.split("\n").slice(line, line + 4).join("\n");
|