explico 0.12.0 → 0.13.1
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/CHANGELOG.md +1137 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +4 -3
- package/dist/compiler/astro.js +45 -15
- package/dist/compiler/cli.d.ts +26 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +9 -1
- package/dist/compiler/collections.js +55 -16
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +14 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +87 -6
- package/dist/compiler/manifest.js +174 -31
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +2 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +2 -2
- package/dist/reference/base-operators.mjs +2 -2
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +34 -4
- package/dist/reference/page-notation.js +54 -2
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +104 -10
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +84 -20
- package/dist/reference/render-lab-math.d.ts +2 -0
- package/dist/reference/render-lab-math.js +17 -5
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +191 -30
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +79 -24
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +32 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +111 -6
- package/docs/authoring/migrating.md +707 -0
- package/docs/authoring/notation.md +281 -29
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +10 -41
- package/docs/authoring/structure.md +53 -2
- package/docs/authoring/visualizations.md +144 -18
- package/package.json +12 -7
- package/src/compiler/astro.ts +57 -17
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +92 -19
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +299 -28
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +12 -9
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/figure/Figure.astro +11 -0
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +321 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +47 -9
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +13 -21
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +2 -2
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +100 -3
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +154 -8
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +84 -20
- package/src/reference/render-lab-math.ts +29 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +205 -29
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +291 -75
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MDX-expression gate (invariant `prop-text`, with the prop gate). MDX
|
|
3
|
+
* reads `{…}` in a lesson's text as JavaScript: `The set {a} has one element` renders nothing
|
|
4
|
+
* and fails the build with "a is not defined", in a compiled chunk, with no
|
|
5
|
+
* file or line. `validate:content` and `content check` parse no expression, so
|
|
6
|
+
* the build was the first to notice. This gate finds a brace pair in the prose
|
|
7
|
+
* (code, math, JSX tags, comments, references and import/export lines blanked;
|
|
8
|
+
* a directive's attributes, `:::note{icon=""}`, kept out) and says what to
|
|
9
|
+
* write instead. See docs/authoring/components.md#braces-in-prose.
|
|
10
|
+
*/
|
|
11
|
+
import type { MathGateDiagnostic } from './gate-math';
|
|
12
|
+
import { lessonLine, proseOnly } from './gate-prose-notation';
|
|
13
|
+
import type { NotationLessonInput } from './types';
|
|
14
|
+
|
|
15
|
+
/** `export const …` lines: MDX's own JavaScript, not prose. */
|
|
16
|
+
const EXPORT_LINE = /^[ \t]*export\b[^\n]*$/gm;
|
|
17
|
+
/** A directive and its attributes, `:::note{icon=""}` or `:::note[Title]{…}`. */
|
|
18
|
+
const DIRECTIVE = /:{1,3}[A-Za-z][\w-]*(?:\[[^\]\n]*\])?\{[^{}\n]*\}/g;
|
|
19
|
+
|
|
20
|
+
export function gateMdxExpressions(lessons: readonly NotationLessonInput[]): MathGateDiagnostic[] {
|
|
21
|
+
const blank = (match: string) => match.replace(/[^\n]/g, ' ');
|
|
22
|
+
const diagnostics: MathGateDiagnostic[] = [];
|
|
23
|
+
for (const lesson of lessons) {
|
|
24
|
+
if (!lesson.body.includes('{')) continue;
|
|
25
|
+
const prose = proseOnly(lesson.body).replace(EXPORT_LINE, blank).replace(DIRECTIVE, blank);
|
|
26
|
+
for (const match of prose.matchAll(/\{([^{}\n]*)\}/g)) {
|
|
27
|
+
const index = match.index ?? 0;
|
|
28
|
+
if (lesson.body[index - 1] === '\\') continue;
|
|
29
|
+
const inner = lesson.body.slice(index + 1, index + match[0].length - 1);
|
|
30
|
+
diagnostics.push({
|
|
31
|
+
context: 'prose',
|
|
32
|
+
code: 'mdx-expression',
|
|
33
|
+
severity: 'error',
|
|
34
|
+
file: lesson.source.file,
|
|
35
|
+
line: lessonLine(lesson, index),
|
|
36
|
+
token: `{${inner}}`,
|
|
37
|
+
message:
|
|
38
|
+
`{${inner}} in the prose is a JavaScript expression to MDX, and the build fails on it ` +
|
|
39
|
+
`("${inner.trim() || '…'} is not defined"). Write \\{${inner}\\} for braces in the text, ` +
|
|
40
|
+
`or $\\{${inner}\\}$ for a set.`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return diagnostics;
|
|
45
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prop-text gate (invariant `prop-text`). A component's quoted prop —
|
|
3
|
+
* `caption="…"`, `short="…"`, `title="…"`, `label="…"` — reaches the page as
|
|
4
|
+
* plain text: KaTeX, the reference plugin and the citation plugin never run
|
|
5
|
+
* inside one. A `$…$`, `[[…]]` or `[@…]` there would print exactly as typed,
|
|
6
|
+
* dollars and brackets included, so it fails the build at its line with what
|
|
7
|
+
* to write instead. See docs/authoring/components.md#props-are-plain-text.
|
|
8
|
+
*/
|
|
9
|
+
import type { CurriculumIssue } from '../curriculum/validation';
|
|
10
|
+
import type { ContentRecord } from './gate-retired-keys';
|
|
11
|
+
|
|
12
|
+
/** One quoted prop of a JSX opening tag: `name="value"`. */
|
|
13
|
+
export interface PropString {
|
|
14
|
+
readonly tag: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly value: string;
|
|
17
|
+
/** Index of `value` in the text it was read from. */
|
|
18
|
+
readonly offset: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const blank = (text: string) => text.replace(/[^\n]/g, ' ');
|
|
22
|
+
|
|
23
|
+
/** Code, MDX comments and a leading frontmatter, blanked so offsets hold. */
|
|
24
|
+
function blankNonJsx(text: string): string {
|
|
25
|
+
return text
|
|
26
|
+
.replace(/^---\r?\n[\s\S]*?\r?\n---(?=\r?\n|$)/, blank)
|
|
27
|
+
.replace(/\{\/\*[\s\S]*?\*\/\}/g, blank)
|
|
28
|
+
.replace(/```[\s\S]*?```/g, blank)
|
|
29
|
+
.replace(/(^|[^`])`[^`\n]*`/g, (match, lead: string) => lead + blank(match.slice(lead.length)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The tag that opens at `start` (on its `<`), with every `{…}` expression
|
|
34
|
+
* blanked so a quoted string inside one is not read as a prop: its end and
|
|
35
|
+
* that copy, or `undefined` when the tag never closes.
|
|
36
|
+
*/
|
|
37
|
+
function readTag(text: string, start: number): { readonly end: number; readonly props: string } | undefined {
|
|
38
|
+
let quote: string | undefined;
|
|
39
|
+
let depth = 0;
|
|
40
|
+
let props = '';
|
|
41
|
+
for (let index = start; index < text.length; index += 1) {
|
|
42
|
+
const char = text[index]!;
|
|
43
|
+
const kept = depth === 0 || char === '\n' ? char : ' ';
|
|
44
|
+
if (quote !== undefined) {
|
|
45
|
+
if (char === quote) quote = undefined;
|
|
46
|
+
props += kept;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === '"' || char === "'" || (char === '`' && depth > 0)) {
|
|
50
|
+
quote = char;
|
|
51
|
+
} else if (char === '{') {
|
|
52
|
+
depth += 1;
|
|
53
|
+
} else if (char === '}') {
|
|
54
|
+
depth = Math.max(0, depth - 1);
|
|
55
|
+
props += ' ';
|
|
56
|
+
continue;
|
|
57
|
+
} else if (char === '>' && depth === 0) {
|
|
58
|
+
return { end: index + 1, props: props + char };
|
|
59
|
+
}
|
|
60
|
+
props += depth === 0 || char === '\n' ? char : ' ';
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Every quoted prop of every JSX opening tag in `text`, outside code, comments and frontmatter. */
|
|
66
|
+
export function propStrings(text: string): PropString[] {
|
|
67
|
+
const searchable = blankNonJsx(text);
|
|
68
|
+
const found: PropString[] = [];
|
|
69
|
+
const opening = /<([A-Za-z][\w.:-]*)/g;
|
|
70
|
+
let match: RegExpExecArray | null;
|
|
71
|
+
while ((match = opening.exec(searchable))) {
|
|
72
|
+
const tag = readTag(searchable, match.index);
|
|
73
|
+
if (tag === undefined) break;
|
|
74
|
+
for (const prop of tag.props.matchAll(/([A-Za-z_][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
|
75
|
+
const value = prop[2] ?? prop[3] ?? '';
|
|
76
|
+
const valueAt = match.index + (prop.index ?? 0) + prop[0].length - value.length - 1;
|
|
77
|
+
found.push({ tag: match[1]!, name: prop[1]!, value: text.slice(valueAt, valueAt + value.length), offset: valueAt });
|
|
78
|
+
}
|
|
79
|
+
opening.lastIndex = tag.end;
|
|
80
|
+
}
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `text` with every quoted JSX prop value blanked: the scans for math and prose skip what never renders as either. */
|
|
85
|
+
export function blankPropStrings(text: string): string {
|
|
86
|
+
let out = text;
|
|
87
|
+
for (const { value, offset } of propStrings(text)) {
|
|
88
|
+
out = out.slice(0, offset) + blank(value) + out.slice(offset + value.length);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/;
|
|
94
|
+
const REFERENCE = /\[\[[^\]\n]*\]\]/;
|
|
95
|
+
const CITATION = /\[@[^\]\n]*\]/;
|
|
96
|
+
|
|
97
|
+
/** What a prop holds that only renders in prose, and what to write instead. */
|
|
98
|
+
function markupIn(value: string): { readonly found: string; readonly advice: string } | undefined {
|
|
99
|
+
const math = INLINE_MATH.exec(value);
|
|
100
|
+
if (math) {
|
|
101
|
+
return {
|
|
102
|
+
found: `math (\`${math[0]}\`)`,
|
|
103
|
+
advice:
|
|
104
|
+
'Write the symbol in Unicode or words ("v₀", "the launch speed"); the prose next to it typesets the symbol.',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const reference = REFERENCE.exec(value);
|
|
108
|
+
if (reference) {
|
|
109
|
+
return {
|
|
110
|
+
found: `a reference (\`${reference[0]}\`)`,
|
|
111
|
+
advice: 'Name the target in words, and reference it from the prose next to the component.',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const citation = CITATION.exec(value);
|
|
115
|
+
if (citation) {
|
|
116
|
+
return {
|
|
117
|
+
found: `a citation (\`${citation[0]}\`)`,
|
|
118
|
+
advice: 'Cite the source in the prose next to the component.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One `markup-in-prop` issue per quoted prop of a page's JSX that holds math, a reference or a citation. */
|
|
125
|
+
export function findMarkupInProps(records: readonly ContentRecord[]): CurriculumIssue[] {
|
|
126
|
+
const issues: CurriculumIssue[] = [];
|
|
127
|
+
for (const { file, text } of records) {
|
|
128
|
+
const path = file.replace(/\\/g, '/');
|
|
129
|
+
if (!path.startsWith('content/docs/') || !path.endsWith('.mdx')) continue;
|
|
130
|
+
for (const prop of propStrings(text)) {
|
|
131
|
+
const markup = markupIn(prop.value);
|
|
132
|
+
if (markup === undefined) continue;
|
|
133
|
+
const line = text.slice(0, prop.offset).split('\n').length;
|
|
134
|
+
issues.push({
|
|
135
|
+
kind: 'markup-in-prop',
|
|
136
|
+
message:
|
|
137
|
+
`${file}:${line}: <${prop.tag}> \`${prop.name}\` holds ${markup.found}, but a prop ` +
|
|
138
|
+
`is plain text: it would print as typed. ${markup.advice}`,
|
|
139
|
+
file,
|
|
140
|
+
line,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return issues;
|
|
145
|
+
}
|
|
@@ -49,6 +49,12 @@ const KEY_REFERENCE = /\[\[[^\]\n]*\]\]/g;
|
|
|
49
49
|
// does not apply inside one — enforcing "use $…$" somewhere it can't render
|
|
50
50
|
// would just break the caption instead of fixing it.
|
|
51
51
|
const JSX_TAG = /<\/?[A-Za-z][^<>]*>/g;
|
|
52
|
+
// A link's destination, `[text](https://…/basically/ "title")`: an address,
|
|
53
|
+
// not prose a reader sees; its text stays. A definition line,
|
|
54
|
+
// `[text]: https://…`, is all address. An autolink `<https://…>` is a JSX-tag
|
|
55
|
+
// shape and goes with them.
|
|
56
|
+
const LINK_DESTINATION = /\]\((?:[^()\n]|\([^()\n]*\))*\)/g;
|
|
57
|
+
const LINK_DEFINITION = /^[ \t]{0,3}\[[^\]\n]+\]:[ \t]*\S[^\n]*$/gm;
|
|
52
58
|
|
|
53
59
|
/**
|
|
54
60
|
* Blank code, math, reference, and JSX-tag spans so offsets don't shift.
|
|
@@ -62,9 +68,13 @@ const MDX_COMMENT = /\{\/\*[\s\S]*?\*\/\}/g;
|
|
|
62
68
|
/**
|
|
63
69
|
* `keepMath` leaves `$…$` / `$$…$$` in place (everything else still blanked,
|
|
64
70
|
* offsets unchanged) — for a check that needs to see what a math span sits
|
|
65
|
-
* next to, such as `$x = 3$ meters`.
|
|
71
|
+
* next to, such as `$x = 3$ meters`. `keepReferences` leaves `[[…]]` in
|
|
72
|
+
* place, for the check that a reference is well formed.
|
|
66
73
|
*/
|
|
67
|
-
export function proseOnly(
|
|
74
|
+
export function proseOnly(
|
|
75
|
+
body: string,
|
|
76
|
+
options: { readonly keepMath?: boolean; readonly keepReferences?: boolean } = {},
|
|
77
|
+
): string {
|
|
68
78
|
const blank = (match: string) => match.replace(/[^\n]/g, ' ');
|
|
69
79
|
const code = body
|
|
70
80
|
.replace(MDX_COMMENT, blank)
|
|
@@ -73,7 +83,9 @@ export function proseOnly(body: string, options: { readonly keepMath?: boolean }
|
|
|
73
83
|
const math = options.keepMath ? code : code.replace(DISPLAY_MATH, blank).replace(INLINE_MATH, blank);
|
|
74
84
|
return math
|
|
75
85
|
.replace(IMPORT_LINE, blank)
|
|
76
|
-
.replace(KEY_REFERENCE, blank)
|
|
86
|
+
.replace(KEY_REFERENCE, (match) => (options.keepReferences ? match : blank(match)))
|
|
87
|
+
.replace(LINK_DESTINATION, (match) => `]${blank(match.slice(1))}`)
|
|
88
|
+
.replace(LINK_DEFINITION, blank)
|
|
77
89
|
.replace(JSX_TAG, blank);
|
|
78
90
|
}
|
|
79
91
|
|
|
@@ -130,8 +142,10 @@ const SLASH_IDIOMS = new Set([
|
|
|
130
142
|
const SLASH_TOKENS = /\b([A-Za-z]{1,6})\/([A-Za-z]{1,6})\b/g;
|
|
131
143
|
|
|
132
144
|
// A `_` or `^` sitting directly between two alphanumerics reads as a
|
|
133
|
-
// hand-typed sub/superscript that was never wrapped in `$…$`.
|
|
134
|
-
|
|
145
|
+
// hand-typed sub/superscript that was never wrapped in `$…$`. The match takes
|
|
146
|
+
// the whole runs on both sides (or a braced group), so the message quotes
|
|
147
|
+
// `2^10`, not `2^1`.
|
|
148
|
+
const FAKE_SCRIPT = /[A-Za-z0-9]+[_^](?:\{[^{}\n]{0,24}\}?|[A-Za-z0-9]+)/g;
|
|
135
149
|
|
|
136
150
|
/** Every N9 finding that is not a spelled-out unit. */
|
|
137
151
|
const PROSE_NOTATION_CODE = 'prose-notation' as const;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retired-key gate (invariant `retired-keys`). A content file that still
|
|
3
|
+
* sets a key the engine no longer reads fails the build, once per key and
|
|
4
|
+
* file, so a course stops maintaining a field that does nothing. The finding
|
|
5
|
+
* points at the key's line, and deleting that line clears it. See
|
|
6
|
+
* docs/authoring/structure.md#retired-keys.
|
|
7
|
+
*/
|
|
8
|
+
import type { CurriculumIssue } from '../curriculum/validation';
|
|
9
|
+
|
|
10
|
+
/** One content file as `loadContentRecords` (`compiler/collections.ts`) reads it. */
|
|
11
|
+
export interface ContentRecord {
|
|
12
|
+
/** Repository-relative path, e.g. `content/sources/hull.json`. */
|
|
13
|
+
readonly file: string;
|
|
14
|
+
/** Top-level fields: a page's frontmatter, or a JSON record's object. */
|
|
15
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
16
|
+
readonly text: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Each retired key, with what an author does about it. Keys are plain identifiers. */
|
|
20
|
+
export const RETIRED_KEYS: Readonly<Record<string, string>> = {
|
|
21
|
+
editorialStatus:
|
|
22
|
+
'explico no longer tracks a draft / in-review / reviewed status, because lessons are reviewed continuously. Delete the line.',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** One `retired-key` issue per retired key a record sets, in record order. */
|
|
26
|
+
export function findRetiredKeys(
|
|
27
|
+
records: readonly ContentRecord[],
|
|
28
|
+
): CurriculumIssue[] {
|
|
29
|
+
const issues: CurriculumIssue[] = [];
|
|
30
|
+
for (const { file, data, text } of records) {
|
|
31
|
+
for (const [key, advice] of Object.entries(RETIRED_KEYS)) {
|
|
32
|
+
if (!(key in data)) continue;
|
|
33
|
+
const line = keyLine(text, key);
|
|
34
|
+
const at = line === undefined ? file : `${file}:${line}`;
|
|
35
|
+
issues.push({
|
|
36
|
+
kind: 'retired-key',
|
|
37
|
+
message: `${at}: \`${key}\` is retired. ${advice}`,
|
|
38
|
+
file,
|
|
39
|
+
...(line === undefined ? {} : { line }),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return issues;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The 1-based line a `key:` (YAML) or `"key":` (JSON) first starts on. */
|
|
47
|
+
export function keyLine(text: string, key: string): number | undefined {
|
|
48
|
+
const match = new RegExp(`^[ \\t]*(?:"${key}"|${key})[ \\t]*:`, 'm').exec(text);
|
|
49
|
+
return match === null
|
|
50
|
+
? undefined
|
|
51
|
+
: text.slice(0, match.index).split('\n').length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Starlight frontmatter a lesson may set but explico cannot honour: the
|
|
56
|
+
* manifest, not Starlight, decides which pages a course lists and in what
|
|
57
|
+
* order (invariant `retired-keys`). `draft: true` is an error — Starlight
|
|
58
|
+
* drops the page from a production build while the sidebar still names it, so
|
|
59
|
+
* the build fails — and `sidebar.order` a warning, since chapters follow the
|
|
60
|
+
* tracks and the key does nothing.
|
|
61
|
+
*/
|
|
62
|
+
export function findUnsupportedKeys(
|
|
63
|
+
records: readonly ContentRecord[],
|
|
64
|
+
): CurriculumIssue[] {
|
|
65
|
+
const issues: CurriculumIssue[] = [];
|
|
66
|
+
for (const { file, data, text } of records) {
|
|
67
|
+
if (!file.replace(/\\/g, '/').startsWith('content/docs/')) continue;
|
|
68
|
+
if (data.draft === true) {
|
|
69
|
+
const line = keyLine(text, 'draft');
|
|
70
|
+
issues.push({
|
|
71
|
+
kind: 'unsupported-key',
|
|
72
|
+
message:
|
|
73
|
+
`${line === undefined ? file : `${file}:${line}`}: \`draft: true\` is not supported in a ` +
|
|
74
|
+
'course: Starlight leaves a draft out of a production build, but explico still ' +
|
|
75
|
+
'lists and numbers it, so the build fails ("The slug … specified in the Starlight ' +
|
|
76
|
+
'sidebar config does not exist"). Keep an unfinished lesson outside content/docs/ ' +
|
|
77
|
+
'until it is ready.',
|
|
78
|
+
file,
|
|
79
|
+
...(line === undefined ? {} : { line }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// A chapter's order: a page outside a part (the glossary, the curriculum
|
|
83
|
+
// map) is placed by the engine and has no tracks to point to.
|
|
84
|
+
const sidebar = data.sidebar;
|
|
85
|
+
const chapter = /^content\/docs\/[^/]+\/(?!index\.mdx?$)[^/]+\.mdx?$/.test(file.replace(/\\/g, '/'));
|
|
86
|
+
if (chapter && sidebar !== null && typeof sidebar === 'object' && 'order' in sidebar) {
|
|
87
|
+
const line = keyLine(text, 'order');
|
|
88
|
+
issues.push({
|
|
89
|
+
kind: 'ignored-key',
|
|
90
|
+
severity: 'warning',
|
|
91
|
+
message:
|
|
92
|
+
`${line === undefined ? file : `${file}:${line}`}: \`sidebar.order\` does nothing in a ` +
|
|
93
|
+
'course: chapters follow the order of the tracks (content/tracks/*.json), then their ' +
|
|
94
|
+
'slugs. Order them there and delete the line.',
|
|
95
|
+
file,
|
|
96
|
+
...(line === undefined ? {} : { line }),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return issues;
|
|
101
|
+
}
|
package/src/reference/gloss.ts
CHANGED