explico 0.8.0 → 0.11.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 +1257 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +46 -2
- package/dist/compiler/cli.js +70 -4
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +68 -5
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +4 -1
- package/dist/compiler/logging.js +4 -1
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +3 -2
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +60 -31
- package/dist/reference/equations.mjs +83 -43
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +1 -0
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +45 -21
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +22 -6
- package/dist/reference/gate-math.js +42 -8
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +21 -11
- package/dist/reference/gate-prose-notation.js +25 -7
- package/dist/reference/katex-delimiters.mjs +20 -7
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +572 -37
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +4 -4
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +17 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +5 -3
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1152 -2
- package/docs/authoring/notation.md +576 -322
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/package.json +2 -2
- package/src/compiler/cli.ts +116 -6
- package/src/compiler/collections.ts +111 -7
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +8 -2
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +3 -4
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +5 -4
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +3 -2
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +83 -43
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +1 -0
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +66 -35
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +69 -10
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +59 -12
- package/src/reference/katex-delimiters.mjs +20 -7
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +572 -37
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +16 -31
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +17 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1467 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The heading/
|
|
3
|
-
* through `######`)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The heading/short-caption-length gate (rule S1): no Markdown heading (`##`
|
|
3
|
+
* through `######`), no `short` caption (`<Figure>`, `<Diagram>`, `<Table>`,
|
|
4
|
+
* `<Memo>`, `<CompactExamples>`), no display equation's `\caption{…}` and no
|
|
5
|
+
* `<ShortNote title>` may run past `HEADING_LENGTH_LIMIT` characters. A long
|
|
6
|
+
* heading or short caption is usually a full sentence that belongs in the
|
|
7
|
+
* body paragraph (for a heading) or the long `caption` (for a float) instead
|
|
8
|
+
* — see docs/authoring/structure-rules.md#s1.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `title` (the text on the collapsible title bar) is in scope.
|
|
10
|
+
* The long `caption` is deliberately never measured: it is the LaTeX
|
|
11
|
+
* convention's full sentence, shown with the float's number, and capping it
|
|
12
|
+
* would fight the rule that puts it there. Only the short name a float goes
|
|
13
|
+
* by away from itself is in scope — the `\caption[short]{…}` half.
|
|
14
14
|
*
|
|
15
15
|
* A lesson's own `#` (its `frontmatter.title`, rendered as the page H1 by
|
|
16
16
|
* Starlight) is Review-only — it never appears in `lesson.body`, so this
|
|
@@ -19,26 +19,33 @@
|
|
|
19
19
|
* Ships straight as an error, same as N9/N14: no partial corpus to absorb,
|
|
20
20
|
* no `lint-ignore.yml` entry.
|
|
21
21
|
*/
|
|
22
|
-
import { stripFencedCode } from './equations.mjs';
|
|
22
|
+
import { EQ_CAPTION_PATTERN, stripFencedCode } from './equations.mjs';
|
|
23
|
+
import { stripHeadingMarker } from './rehype-heading-anchors.mjs';
|
|
23
24
|
import type { MathGateDiagnostic } from './gate-math';
|
|
24
25
|
import { lessonLine } from './gate-prose-notation';
|
|
25
26
|
import type { NotationLessonInput } from './types';
|
|
26
27
|
|
|
28
|
+
/** Rule S1's default; a course sets its own with `rules.S1.maxLength`. */
|
|
27
29
|
export const HEADING_LENGTH_LIMIT = 65;
|
|
28
30
|
|
|
29
31
|
// ATX heading: 1-6 `#`, then the text, with any trailing closing `#`s
|
|
30
32
|
// trimmed off (the `# text #` form CommonMark also allows).
|
|
31
33
|
const HEADING_LINE = /^[ \t]{0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/gm;
|
|
32
34
|
|
|
33
|
-
// The JSX attribute that
|
|
34
|
-
// `
|
|
35
|
-
const
|
|
36
|
-
Figure: '
|
|
37
|
-
Diagram: '
|
|
38
|
-
|
|
35
|
+
// The JSX attribute that holds a component's short name, per component: the
|
|
36
|
+
// `short` caption of the caption pair, and a short note's inline link text.
|
|
37
|
+
const SHORT_ATTRIBUTE_BY_TAG: Record<string, string> = {
|
|
38
|
+
Figure: 'short',
|
|
39
|
+
Diagram: 'short',
|
|
40
|
+
Table: 'short',
|
|
41
|
+
Memo: 'short',
|
|
42
|
+
CompactExamples: 'short',
|
|
39
43
|
ShortNote: 'title',
|
|
40
44
|
};
|
|
41
45
|
|
|
46
|
+
// A display equation's `\caption{…}` (its short caption).
|
|
47
|
+
const DISPLAY_MATH = /\$\$([\s\S]+?)\$\$/g;
|
|
48
|
+
|
|
42
49
|
interface Finding {
|
|
43
50
|
readonly index: number;
|
|
44
51
|
readonly token: string;
|
|
@@ -51,44 +58,63 @@ function readAttribute(attributesSource: string, name: string): string | undefin
|
|
|
51
58
|
return match ? match[2] : undefined;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
function overLimitMessage(kind: string, text: string): string {
|
|
61
|
+
function overLimitMessage(kind: string, text: string, limit: number): string {
|
|
55
62
|
return (
|
|
56
63
|
`${kind} ${JSON.stringify(text)} is ${text.length} characters, over the ` +
|
|
57
|
-
`${
|
|
58
|
-
'the rest into the paragraph or caption
|
|
64
|
+
`${limit}-character limit (rule S1, rules.S1.maxLength in the course ` +
|
|
65
|
+
'settings). Shorten it and move the rest into the paragraph or caption ' +
|
|
66
|
+
'that follows.'
|
|
59
67
|
);
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
function headingFindings(text: string): Finding[] {
|
|
70
|
+
function headingFindings(text: string, limit: number): Finding[] {
|
|
63
71
|
const out: Finding[] = [];
|
|
64
72
|
const re = new RegExp(HEADING_LINE.source, HEADING_LINE.flags);
|
|
65
73
|
let match: RegExpExecArray | null;
|
|
66
74
|
while ((match = re.exec(text))) {
|
|
67
75
|
const level = match[1]?.length ?? 1;
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
// What the reader sees: `[collapsed]`, `[card]` and the like never render.
|
|
77
|
+
const title = stripHeadingMarker(match[2] ?? '').trim();
|
|
78
|
+
if (title.length > limit) {
|
|
70
79
|
out.push({
|
|
71
80
|
index: match.index,
|
|
72
81
|
token: title,
|
|
73
|
-
message: overLimitMessage(`${'#'.repeat(level)} heading`, title),
|
|
82
|
+
message: overLimitMessage(`${'#'.repeat(level)} heading`, title, limit),
|
|
74
83
|
});
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
return out;
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
function floatTitleFindings(text: string): Finding[] {
|
|
89
|
+
function floatTitleFindings(text: string, limit: number): Finding[] {
|
|
81
90
|
const out: Finding[] = [];
|
|
82
|
-
for (const [tagName, attribute] of Object.entries(
|
|
91
|
+
for (const [tagName, attribute] of Object.entries(SHORT_ATTRIBUTE_BY_TAG)) {
|
|
83
92
|
const opening = new RegExp(`<${tagName}\\b([^>]*)>`, 'g');
|
|
84
93
|
let match: RegExpExecArray | null;
|
|
85
94
|
while ((match = opening.exec(text))) {
|
|
86
95
|
const value = readAttribute(match[1] ?? '', attribute)?.trim();
|
|
87
|
-
if (value && value.length >
|
|
96
|
+
if (value && value.length > limit) {
|
|
88
97
|
out.push({
|
|
89
98
|
index: match.index,
|
|
90
99
|
token: value,
|
|
91
|
-
message: overLimitMessage(`<${tagName} ${attribute}>`, value),
|
|
100
|
+
message: overLimitMessage(`<${tagName} ${attribute}>`, value, limit),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function equationCaptionFindings(text: string, limit: number): Finding[] {
|
|
109
|
+
const out: Finding[] = [];
|
|
110
|
+
for (const display of text.matchAll(DISPLAY_MATH)) {
|
|
111
|
+
for (const caption of (display[1] ?? '').matchAll(EQ_CAPTION_PATTERN)) {
|
|
112
|
+
const value = (caption[1] ?? '').replace(/\s+/g, ' ').trim();
|
|
113
|
+
if (value.length > limit) {
|
|
114
|
+
out.push({
|
|
115
|
+
index: (display.index ?? 0) + 2 + (caption.index ?? 0),
|
|
116
|
+
token: value,
|
|
117
|
+
message: overLimitMessage('Equation \\caption{}', value, limit),
|
|
92
118
|
});
|
|
93
119
|
}
|
|
94
120
|
}
|
|
@@ -97,27 +123,32 @@ function floatTitleFindings(text: string): Finding[] {
|
|
|
97
123
|
}
|
|
98
124
|
|
|
99
125
|
/** Every S1 finding in one lesson body, in source order. */
|
|
100
|
-
function findingsIn(text: string): Finding[] {
|
|
101
|
-
return [
|
|
102
|
-
(
|
|
103
|
-
|
|
126
|
+
function findingsIn(text: string, limit: number): Finding[] {
|
|
127
|
+
return [
|
|
128
|
+
...headingFindings(text, limit),
|
|
129
|
+
...floatTitleFindings(text, limit),
|
|
130
|
+
...equationCaptionFindings(text, limit),
|
|
131
|
+
].sort((a, b) => a.index - b.index);
|
|
104
132
|
}
|
|
105
133
|
|
|
106
134
|
/**
|
|
107
|
-
* Scan every lesson body for a heading or a
|
|
135
|
+
* Scan every lesson body for a heading or a short caption running past
|
|
108
136
|
* `HEADING_LENGTH_LIMIT` characters. Returns one `MathGateDiagnostic`
|
|
109
137
|
* (context `'structure'`, always an error) per finding.
|
|
110
138
|
*/
|
|
111
139
|
export function gateHeadingLength(
|
|
112
140
|
lessons: readonly NotationLessonInput[],
|
|
141
|
+
options: { readonly maxLength?: number } = {},
|
|
113
142
|
): MathGateDiagnostic[] {
|
|
114
143
|
const diagnostics: MathGateDiagnostic[] = [];
|
|
144
|
+
const limit = options.maxLength ?? HEADING_LENGTH_LIMIT;
|
|
115
145
|
|
|
116
146
|
for (const lesson of lessons) {
|
|
117
147
|
const text = stripFencedCode(lesson.body);
|
|
118
|
-
for (const finding of findingsIn(text)) {
|
|
148
|
+
for (const finding of findingsIn(text, limit)) {
|
|
119
149
|
diagnostics.push({
|
|
120
150
|
context: 'structure',
|
|
151
|
+
code: 'title-length',
|
|
121
152
|
severity: 'error',
|
|
122
153
|
file: lesson.source.file,
|
|
123
154
|
line: lessonLine(lesson, finding.index),
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The heading-structure gate: rules S6 and S7, over a lesson's outline — the
|
|
3
|
+
* top-level headings `sections.mjs` numbers, read by the same
|
|
4
|
+
* `scanOutlineBlocks`, so the rules and the section numbers can never
|
|
5
|
+
* disagree about which headings a page has. A heading inside a component
|
|
6
|
+
* (`<Memo>`, `<CompactExample>`, …) belongs to that component and is in
|
|
7
|
+
* neither. See docs/authoring/structure-rules.md#s6 and #s7.
|
|
8
|
+
*
|
|
9
|
+
* - **S6 (`heading-skip`)** — headings descend one level at a time. The page
|
|
10
|
+
* title (`frontmatter.title`) is the lesson's one `#`, so the body starts at
|
|
11
|
+
* `##`, a `#` in the body is an error, and a heading is at most one level
|
|
12
|
+
* deeper than the one before it. Going back up any number of levels is fine.
|
|
13
|
+
* - **S7 (`heading-adjacent`)** — a heading is followed by body content,
|
|
14
|
+
* never directly by another heading, whatever the two levels.
|
|
15
|
+
*
|
|
16
|
+
* Both also see the headings a component draws where its tag stands
|
|
17
|
+
* ({@link TITLED_COMPONENTS}): a knowledge check's and a worked-example set's
|
|
18
|
+
* `<h2>` (`BlockHeading.astro`) read as a `##`, so a heading written directly
|
|
19
|
+
* above either is two headings in a row, and the next heading steps from
|
|
20
|
+
* `##`.
|
|
21
|
+
*
|
|
22
|
+
* Ships straight as an error, same as S1: every playground violation was fixed
|
|
23
|
+
* or waived in the same change.
|
|
24
|
+
*/
|
|
25
|
+
import type { MathGateDiagnostic } from './gate-math';
|
|
26
|
+
import { lessonLine } from './gate-prose-notation';
|
|
27
|
+
import { scanOutlineBlocks } from './sections.mjs';
|
|
28
|
+
import type { NotationLessonInput } from './types';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Components that open with a title of their own, drawn where the tag stands.
|
|
32
|
+
* `level` is the `<hN>` a component renders, when it renders a real heading
|
|
33
|
+
* (rule S6 steps from it); `what` finishes a rule S7 message.
|
|
34
|
+
*/
|
|
35
|
+
const TITLED_COMPONENTS: Readonly<Record<string, { readonly level?: number; readonly what: string }>> = {
|
|
36
|
+
AssessmentSet: {
|
|
37
|
+
level: 2,
|
|
38
|
+
what: 'which renders its own `<h2>`: the “Knowledge check” badge, its number and the check’s title',
|
|
39
|
+
},
|
|
40
|
+
CompactExamples: {
|
|
41
|
+
level: 2,
|
|
42
|
+
what: 'which renders its own `<h2>`: the “Example” badge, its number and the set’s `short` title',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type Block =
|
|
47
|
+
| {
|
|
48
|
+
readonly kind: 'heading';
|
|
49
|
+
readonly level: number;
|
|
50
|
+
/** How a message names it: `` `## Title` ``. */
|
|
51
|
+
readonly label: string;
|
|
52
|
+
readonly token: string;
|
|
53
|
+
readonly index: number;
|
|
54
|
+
}
|
|
55
|
+
| { readonly kind: 'titled'; readonly name: string; readonly index: number; readonly level?: number }
|
|
56
|
+
| { readonly kind: 'content' };
|
|
57
|
+
|
|
58
|
+
function blocksOf(body: string): Block[] {
|
|
59
|
+
return scanOutlineBlocks(body).map((block): Block => {
|
|
60
|
+
if (block.kind === 'heading') {
|
|
61
|
+
const hashes = '#'.repeat(block.level);
|
|
62
|
+
return {
|
|
63
|
+
kind: 'heading',
|
|
64
|
+
level: block.level,
|
|
65
|
+
label: `\`${hashes} ${block.text}\``,
|
|
66
|
+
token: block.text,
|
|
67
|
+
index: block.index,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (block.kind === 'element') {
|
|
71
|
+
const titled = TITLED_COMPONENTS[block.name];
|
|
72
|
+
if (titled) {
|
|
73
|
+
return {
|
|
74
|
+
kind: 'titled',
|
|
75
|
+
name: block.name,
|
|
76
|
+
index: block.index,
|
|
77
|
+
...(titled.level === undefined ? {} : { level: titled.level }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { kind: 'content' };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface Finding {
|
|
86
|
+
readonly code: 'heading-skip' | 'heading-adjacent';
|
|
87
|
+
readonly index: number;
|
|
88
|
+
readonly token: string;
|
|
89
|
+
readonly message: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Rule S6: every heading steps down at most one level from the one before. */
|
|
93
|
+
function skipFindings(blocks: readonly Block[]): Finding[] {
|
|
94
|
+
const out: Finding[] = [];
|
|
95
|
+
// The page title is the `<h1>` above the body.
|
|
96
|
+
let previous: { level: number; label: string } = { level: 1, label: 'the page title (`#`)' };
|
|
97
|
+
for (const block of blocks) {
|
|
98
|
+
if (block.kind === 'content') continue;
|
|
99
|
+
if (block.kind === 'titled') {
|
|
100
|
+
if (block.level !== undefined) {
|
|
101
|
+
previous = { level: block.level, label: `the \`<h${block.level}>\` of \`<${block.name} />\`` };
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (block.level === 1) {
|
|
106
|
+
out.push({
|
|
107
|
+
code: 'heading-skip',
|
|
108
|
+
index: block.index,
|
|
109
|
+
token: block.token,
|
|
110
|
+
message:
|
|
111
|
+
`${block.label} is a \`#\` in the lesson body, but the page title ` +
|
|
112
|
+
'(`frontmatter.title`) is the lesson’s one `#`: the body’s headings start ' +
|
|
113
|
+
'at `##` (rule S6). Make it `##`, or move the text into the title.',
|
|
114
|
+
});
|
|
115
|
+
} else if (block.level > previous.level + 1) {
|
|
116
|
+
const expected = '#'.repeat(previous.level + 1);
|
|
117
|
+
out.push({
|
|
118
|
+
code: 'heading-skip',
|
|
119
|
+
index: block.index,
|
|
120
|
+
token: block.token,
|
|
121
|
+
message:
|
|
122
|
+
`${block.label} follows ${previous.label}, skipping a level: headings ` +
|
|
123
|
+
`descend one level at a time (rule S6). Make it \`${expected}\`, or add ` +
|
|
124
|
+
`the \`${expected}\` section it belongs under.`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
previous = { level: block.level, label: block.label };
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Rule S7: a heading is followed by body content before the next heading. */
|
|
133
|
+
function adjacentFindings(blocks: readonly Block[], lesson: NotationLessonInput): Finding[] {
|
|
134
|
+
const out: Finding[] = [];
|
|
135
|
+
blocks.forEach((block, position) => {
|
|
136
|
+
if (block.kind !== 'heading') return;
|
|
137
|
+
const next = blocks[position + 1];
|
|
138
|
+
if (next === undefined || next.kind === 'content') return;
|
|
139
|
+
const line = lessonLine(lesson, next.index);
|
|
140
|
+
const followedBy =
|
|
141
|
+
next.kind === 'heading'
|
|
142
|
+
? `${next.label} (line ${line})`
|
|
143
|
+
: `\`<${next.name}>\` (line ${line}), ${TITLED_COMPONENTS[next.name]!.what}`;
|
|
144
|
+
out.push({
|
|
145
|
+
code: 'heading-adjacent',
|
|
146
|
+
index: block.index,
|
|
147
|
+
token: block.token,
|
|
148
|
+
message:
|
|
149
|
+
`${block.label} is followed directly by ${followedBy}: a heading is ` +
|
|
150
|
+
'followed by body content, never by another heading (rule S7). Write ' +
|
|
151
|
+
'what the section is about before it goes on, or drop the heading that ' +
|
|
152
|
+
'says nothing of its own.',
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Scan every lesson body's outline for rule S6 (`heading-skip`) and rule S7
|
|
160
|
+
* (`heading-adjacent`) findings. `levels` / `adjacency` switch each rule's
|
|
161
|
+
* half off, the way `rules.S6.enabled` / `rules.S7.enabled` do. Returns one
|
|
162
|
+
* `MathGateDiagnostic` (context `'structure'`, always an error) per finding,
|
|
163
|
+
* located at the heading that breaks the rule.
|
|
164
|
+
*/
|
|
165
|
+
export function gateHeadingStructure(
|
|
166
|
+
lessons: readonly NotationLessonInput[],
|
|
167
|
+
options: { readonly levels?: boolean; readonly adjacency?: boolean } = {},
|
|
168
|
+
): MathGateDiagnostic[] {
|
|
169
|
+
const { levels = true, adjacency = true } = options;
|
|
170
|
+
const diagnostics: MathGateDiagnostic[] = [];
|
|
171
|
+
if (!levels && !adjacency) return diagnostics;
|
|
172
|
+
|
|
173
|
+
for (const lesson of lessons) {
|
|
174
|
+
const blocks = blocksOf(lesson.body);
|
|
175
|
+
const findings = [
|
|
176
|
+
...(levels ? skipFindings(blocks) : []),
|
|
177
|
+
...(adjacency ? adjacentFindings(blocks, lesson) : []),
|
|
178
|
+
].sort((a, b) => a.index - b.index);
|
|
179
|
+
for (const finding of findings) {
|
|
180
|
+
diagnostics.push({
|
|
181
|
+
context: 'structure',
|
|
182
|
+
code: finding.code,
|
|
183
|
+
severity: 'error',
|
|
184
|
+
file: lesson.source.file,
|
|
185
|
+
line: lessonLine(lesson, finding.index),
|
|
186
|
+
token: finding.token,
|
|
187
|
+
message: finding.message,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return diagnostics;
|
|
193
|
+
}
|