explico 0.5.0 → 0.6.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/CHANGELOG.md +116 -7
- package/dist/compiler/course-config.d.ts +31 -1
- package/dist/compiler/course-config.js +7 -0
- package/dist/compiler/manifest.d.ts +10 -2
- package/dist/compiler/manifest.js +7 -3
- package/dist/reference/gate-heading-length.d.ts +12 -0
- package/dist/reference/gate-heading-length.js +79 -0
- package/dist/reference/gate-math.d.ts +6 -2
- package/dist/reference/gate-math.js +2 -1
- package/dist/reference/math-glyphs.d.mts +12 -0
- package/dist/reference/math-glyphs.mjs +183 -42
- package/dist/reference/remark-citation.d.mts +5 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/docs/VERSIONING.md +22 -2
- package/docs/authoring/README.md +2 -0
- package/docs/authoring/components.md +4 -1
- package/docs/authoring/migrating.md +191 -8
- package/docs/authoring/notation.md +55 -0
- package/docs/authoring/structure-rules.md +69 -0
- package/docs/authoring/visualizations.md +2 -2
- package/package.json +1 -1
- package/src/compiler/course-config.ts +40 -0
- package/src/compiler/manifest.ts +14 -1
- package/src/components/diagram/Diagram.astro +0 -55
- package/src/components/notation/NotationLayer.astro +155 -75
- package/src/components/starlight/LessonFooter.astro +3 -1
- package/src/content-config.ts +16 -1
- package/src/reference/gate-heading-length.ts +131 -0
- package/src/reference/gate-math.ts +7 -2
- package/src/reference/math-glyphs.mjs +183 -42
- package/src/reference/remark-citation.mjs +0 -0
- package/src/styles/global.css +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,97 @@ under **Breaking** below; a PATCH never breaks.
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.6.0] - 2026-09-16
|
|
18
|
+
|
|
19
|
+
### Breaking
|
|
20
|
+
|
|
21
|
+
- **New build-enforced authoring rule S1: no Markdown heading (`##` through
|
|
22
|
+
`######`) and no `<Figure title>` / `<Diagram label>` / `<Memo title>` /
|
|
23
|
+
`<ShortNote title>` may run past 65 characters.** A `<Figure>` with a long
|
|
24
|
+
`caption` and no explicit `title` was silently using the whole caption as
|
|
25
|
+
its always-visible collapsible title-bar text — the reader saw the full
|
|
26
|
+
sentence twice. `gate-heading-length.ts` scans every lesson body; a course
|
|
27
|
+
with an over-limit heading or float title anywhere will now fail
|
|
28
|
+
`pnpm validate:content` / `astro build`. `<Table>` is exempt — it has only
|
|
29
|
+
a `caption`, not a separate `title`. A lesson's own `frontmatter.title`
|
|
30
|
+
(the page `<h1>`) is Review-only, not build-enforced — the gate scans body
|
|
31
|
+
text and cannot see frontmatter. See
|
|
32
|
+
`docs/authoring/structure-rules.md#s1`.
|
|
33
|
+
|
|
34
|
+
- **Removed the "Pin explanation" button from the "Page notation" panel**
|
|
35
|
+
(`.notation-pin`, `[data-notation-control]` — both gone from the DOM). Pin
|
|
36
|
+
a card from the list the same way an inline prose symbol always has: click
|
|
37
|
+
its glyph, now also Tab + Enter/Space-operable (`role="button"`,
|
|
38
|
+
`tabindex="0"`, `aria-pressed`) since it was the button's only remaining
|
|
39
|
+
job. A course's own CSS or tests targeting either selector need updating.
|
|
40
|
+
|
|
41
|
+
- **`NotationLayer.astro`'s separate "Page notation" `<h2>` is gone** —
|
|
42
|
+
the panel had two headings back to back ("Page notation" over "Notation
|
|
43
|
+
used on this page (N)"), saying the same thing twice. The panel's
|
|
44
|
+
`<summary>` is now the only heading, and `.notation-layer-heading` no
|
|
45
|
+
longer exists.
|
|
46
|
+
|
|
47
|
+
- **`Diagram`'s `description` prop and its collapsed "Diagram description"
|
|
48
|
+
fallback block are gone** (`.diagram-fallback` no longer exists in the
|
|
49
|
+
DOM). The caption (`label`) is the diagram's one authored piece of text,
|
|
50
|
+
matching how a LaTeX figure/table caption already tells the whole story —
|
|
51
|
+
a separate always-present description block was inventing a second text
|
|
52
|
+
category most courses never wanted and that only added visible chrome
|
|
53
|
+
under every diagram. Drop any `description="…"` passed to `<Diagram>`;
|
|
54
|
+
the build no longer accepts it (`astro check` fails on an excess prop).
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`NotationLayer.astro`'s "Page notation" panel and `remark-citation.mjs`'s
|
|
59
|
+
generated "References" section are both properly collapsible now** — same
|
|
60
|
+
native, persisted, deep-linkable disclosure `Figure`/`CollapsibleSections`
|
|
61
|
+
already use, not the always-native-default `<details>` "Page notation" had
|
|
62
|
+
before (no persistence, no course-configurable default, no anchor).
|
|
63
|
+
- `NotationLayer` gains `defaultOpen`/`rememberState` props (mirroring
|
|
64
|
+
`Figure`'s) and a hover-reveal permalink anchor next to its title bar
|
|
65
|
+
(`#page-notation` by default).
|
|
66
|
+
- `remark-citation.mjs`'s generated References heading now carries the
|
|
67
|
+
same `[collapsible]`/`[collapsed]` marker an authored `##` heading uses,
|
|
68
|
+
via a new `defaultOpen` plugin option — so it rides the *same*
|
|
69
|
+
`rehypeHeadingAnchors` / `CollapsibleSections` machinery every other
|
|
70
|
+
heading does, with no engine changes needed on that side. This only
|
|
71
|
+
takes visible effect on a course whose `astro.config.mjs` wires
|
|
72
|
+
`rehypeHeadingAnchors` into its rehype pipeline.
|
|
73
|
+
- New `CourseConfig.disclosureDefaults` (the manifest schema is now v14,
|
|
74
|
+
additive):
|
|
75
|
+
`{ notation?, references? }`, each `true` = open, closed (the engine's
|
|
76
|
+
own default) when omitted or the whole block is absent. Both are
|
|
77
|
+
engine-authored sections, not written by a lesson, so they get their own
|
|
78
|
+
default independent of `defaultCollapsible.sections`/`.subsections` (an
|
|
79
|
+
author's own `##` headings). See `docs/authoring`,
|
|
80
|
+
`playground/astro.config.mjs` for the wiring convention.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **A symbol explained once via `\explain{key}{…}` can now auto-resolve at
|
|
85
|
+
every later bare occurrence of the same declared `latex` on the page — this
|
|
86
|
+
previously failed silently (no tooltip, no build error) for several common
|
|
87
|
+
shapes.** A definition whose whole canonical form is a roman label
|
|
88
|
+
(`\mathrm{SSR}`, `\mathrm{SST}`) never entered the auto-annotator's
|
|
89
|
+
signature index at all, so a bare occurrence of such a symbol could never
|
|
90
|
+
resolve, anywhere on the page, independent of position. Also fixed: a bare
|
|
91
|
+
symbol sitting immediately next to an `\explain{…}{…}` call, a canonical
|
|
92
|
+
match found by seeing through an already-explicit call's own wrapping, and
|
|
93
|
+
a partial `\explain{key}{…}` wrap covering only part of a declared
|
|
94
|
+
multi-token form — all three previously either dropped the symbol silently
|
|
95
|
+
or produced malformed, unparseable output. All of these now resolve
|
|
96
|
+
correctly, or fail loudly as "Unresolved notation" (`resolveMathGlyphs`,
|
|
97
|
+
shared by lesson-body rendering and `gate-math.ts`'s `pnpm validate:content`
|
|
98
|
+
check — so a regression here fails the build immediately rather than
|
|
99
|
+
passing silently through to a render-time surprise). One side effect: the
|
|
100
|
+
fix also restores the `ambiguous-canonical-notation` check for
|
|
101
|
+
`\operatorname{…}`/`\mathrm{…}`-only glyphs, which the same bug had
|
|
102
|
+
silently disabled — a course with two different notation keys declaring
|
|
103
|
+
structurally identical LaTeX (e.g. two keys both `\operatorname{Var}`) will
|
|
104
|
+
now correctly fail `pnpm validate:content` where it previously built with a
|
|
105
|
+
silent, unresolvable ambiguity. See
|
|
106
|
+
[`docs/authoring/notation.md`](docs/authoring/notation.md).
|
|
107
|
+
|
|
17
108
|
## [0.5.0] - 2026-09-15
|
|
18
109
|
|
|
19
110
|
### Added
|
|
@@ -137,13 +228,16 @@ under **Breaking** below; a PATCH never breaks.
|
|
|
137
228
|
UI. The change is additive, but consumers that validate
|
|
138
229
|
`MANIFEST_SCHEMA_VERSION` must accept the new version.
|
|
139
230
|
|
|
140
|
-
- **The compiler manifest schema is now v10.**
|
|
231
|
+
- **The compiler manifest schema is now v10.** Numbered knowledge checks add
|
|
232
|
+
`manifest.checks`, `lessons[].knowledgeChecks`, `lessons[].numbering.checks`,
|
|
233
|
+
and `diagnostics.checks`. The change is additive, but consumers that
|
|
234
|
+
validate `MANIFEST_SCHEMA_VERSION` must accept the new version.
|
|
235
|
+
|
|
236
|
+
- **The compiler manifest schema is now v9.** Diagrams are first-class keyed
|
|
141
237
|
floats with their own `manifest.diagrams`, `lessons[].diagrams`,
|
|
142
|
-
`lessons[].numbering.diagrams`, and `diagnostics.diagrams` fields.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
additive, but consumers that validate `MANIFEST_SCHEMA_VERSION` must accept
|
|
146
|
-
the new version.
|
|
238
|
+
`lessons[].numbering.diagrams`, and `diagnostics.diagrams` fields. The
|
|
239
|
+
change is additive, but consumers that validate `MANIFEST_SCHEMA_VERSION`
|
|
240
|
+
must accept the new version.
|
|
147
241
|
|
|
148
242
|
- **New build-enforced authoring rule N9: a physical quantity, unit, or math
|
|
149
243
|
operator must be typeset in `$…$`, never as plain text, anywhere on a
|
|
@@ -217,6 +311,20 @@ under **Breaking** below; a PATCH never breaks.
|
|
|
217
311
|
with `\sin\theta`, `\log x`, `\exp t`, etc. anywhere in rendered math will
|
|
218
312
|
now fail the build; see `docs/authoring/notation.md#n13`.
|
|
219
313
|
|
|
314
|
+
- **New build-enforced authoring rule N15: a nested visible delimiter — `(`,
|
|
315
|
+
`[`, `\{`, or `|` inside another one of them — must use its `\left`/`\right`
|
|
316
|
+
sizing form, not the bare character.** The moment a second one of these four
|
|
317
|
+
glyphs sits inside the first — same type or mixed, e.g. `(a + [b])` — every
|
|
318
|
+
level of that nest switches to the sizing form: `\sin(2(90°-\theta))` is now
|
|
319
|
+
a hard error, `\sin\left(2\left(90°-\theta\right)\right)` is required.
|
|
320
|
+
`findNestedBareDelimiters` (`math-glyphs.mjs`) walks the same parsed KaTeX
|
|
321
|
+
tree `resolveMathGlyphs` and N13 already build; a violation fails
|
|
322
|
+
`pnpm validate:content` / `astro build` with no `lint-ignore.yml` escape
|
|
323
|
+
hatch, like N9/N13. A single, non-nested bare delimiter (`(x)`) is
|
|
324
|
+
untouched, and a delimiter inside `\text{…}` (a unit per N9) is unaffected.
|
|
325
|
+
A downstream course with a nested bare delimiter anywhere in rendered math
|
|
326
|
+
will now fail the build; see `docs/authoring/notation.md#n15`.
|
|
327
|
+
|
|
220
328
|
### Added
|
|
221
329
|
|
|
222
330
|
- **Numbered, referenceable, collapsible knowledge checks.** Assessment ids in
|
|
@@ -436,7 +544,8 @@ under **Breaking** below; a PATCH never breaks.
|
|
|
436
544
|
system with its completeness gate, the curriculum-graph validator, the
|
|
437
545
|
Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
|
|
438
546
|
|
|
439
|
-
[Unreleased]: https://github.com/danieltuzes/explico/compare/v0.
|
|
547
|
+
[Unreleased]: https://github.com/danieltuzes/explico/compare/v0.6.0...HEAD
|
|
548
|
+
[0.6.0]: https://github.com/danieltuzes/explico/compare/v0.5.0...v0.6.0
|
|
440
549
|
[0.5.0]: https://github.com/danieltuzes/explico/compare/v0.4.0...v0.5.0
|
|
441
550
|
[0.4.0]: https://github.com/danieltuzes/explico/compare/v0.3.0...v0.4.0
|
|
442
551
|
[0.3.0]: https://github.com/danieltuzes/explico/compare/v0.2.0...v0.3.0
|
|
@@ -107,6 +107,33 @@ interface CourseAbout {
|
|
|
107
107
|
/** Link to the published content changelog (repo blob, docs page, …). */
|
|
108
108
|
readonly changelogHref?: string;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* A course's chosen initial open/closed state for the engine's own built-in
|
|
112
|
+
* collapsible sections — the "Page notation" panel (`NotationLayer.astro`)
|
|
113
|
+
* and the generated "References" section (`remark-citation.mjs`). Each is
|
|
114
|
+
* independent of `defaultCollapsible.sections`/`.subsections` (an author's
|
|
115
|
+
* own `##` headings): both of these are engine-authored, not written by a
|
|
116
|
+
* lesson, so they need their own default rather than inheriting the site's
|
|
117
|
+
* general heading default.
|
|
118
|
+
*
|
|
119
|
+
* `true` starts open, `false` (or omitted) starts closed — the engine's own
|
|
120
|
+
* default is closed either way, matching a long panel a reader usually
|
|
121
|
+
* doesn't need open on arrival.
|
|
122
|
+
*/
|
|
123
|
+
interface CourseDisclosureDefaults {
|
|
124
|
+
/** Initial state of the "Page notation" panel. Default: closed. */
|
|
125
|
+
readonly notation?: boolean;
|
|
126
|
+
/** Initial state of the generated "References" section. Default: closed. */
|
|
127
|
+
readonly references?: boolean;
|
|
128
|
+
}
|
|
129
|
+
/** The stable, non-optional form `NormalizedLessonExports` mirrors. */
|
|
130
|
+
interface NormalizedDisclosureDefaults {
|
|
131
|
+
readonly notation: boolean;
|
|
132
|
+
readonly references: boolean;
|
|
133
|
+
}
|
|
134
|
+
/** Validate and canonicalize `CourseConfig.disclosureDefaults`. Absent (or
|
|
135
|
+
* an absent sub-field) resolves to closed, the engine's own default. */
|
|
136
|
+
declare function normalizeDisclosureDefaults(config: CourseConfig): NormalizedDisclosureDefaults;
|
|
110
137
|
/** Optional, build-time lesson artifacts exposed to readers. */
|
|
111
138
|
interface LessonExportsConfig {
|
|
112
139
|
/** Emit one self-contained, offline-capable HTML file per lesson. */
|
|
@@ -151,6 +178,9 @@ interface CourseConfig {
|
|
|
151
178
|
readonly abbreviations?: CourseAbbreviations;
|
|
152
179
|
/** Per-lesson export actions and downloadable artifacts. Disabled when omitted. */
|
|
153
180
|
readonly lessonExports?: LessonExportsConfig;
|
|
181
|
+
/** Initial open/closed state for the engine's own built-in collapsible
|
|
182
|
+
* sections ("Page notation", "References"). Closed when omitted. */
|
|
183
|
+
readonly disclosureDefaults?: CourseDisclosureDefaults;
|
|
154
184
|
}
|
|
155
185
|
/** Validate and canonicalize the build-facing lesson-export configuration. */
|
|
156
186
|
declare function normalizeLessonExports(config: CourseConfig): NormalizedLessonExports;
|
|
@@ -161,4 +191,4 @@ declare function normalizeLessonExports(config: CourseConfig): NormalizedLessonE
|
|
|
161
191
|
*/
|
|
162
192
|
declare function coursePartsOf(config: CourseConfig): readonly CoursePart[];
|
|
163
193
|
|
|
164
|
-
export { type CourseAbbreviations, type CourseAbout, type CourseAuthor, type CourseConfig, type CourseConventions, type CourseOperators, type CoursePart, type CourseRomanLabels, type CourseSection, type LessonExportsConfig, type NormalizedLessonExports, coursePartsOf, normalizeLessonExports };
|
|
194
|
+
export { type CourseAbbreviations, type CourseAbout, type CourseAuthor, type CourseConfig, type CourseConventions, type CourseDisclosureDefaults, type CourseOperators, type CoursePart, type CourseRomanLabels, type CourseSection, type LessonExportsConfig, type NormalizedDisclosureDefaults, type NormalizedLessonExports, coursePartsOf, normalizeDisclosureDefaults, normalizeLessonExports };
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
function normalizeDisclosureDefaults(config) {
|
|
2
|
+
return {
|
|
3
|
+
notation: config.disclosureDefaults?.notation === true,
|
|
4
|
+
references: config.disclosureDefaults?.references === true
|
|
5
|
+
};
|
|
6
|
+
}
|
|
1
7
|
function normalizeLessonExports(config) {
|
|
2
8
|
const standaloneHtml = config.lessonExports?.standaloneHtml === true;
|
|
3
9
|
const print = config.lessonExports?.print === true;
|
|
@@ -35,5 +41,6 @@ function coursePartsOf(config) {
|
|
|
35
41
|
}
|
|
36
42
|
export {
|
|
37
43
|
coursePartsOf,
|
|
44
|
+
normalizeDisclosureDefaults,
|
|
38
45
|
normalizeLessonExports
|
|
39
46
|
};
|
|
@@ -10,7 +10,7 @@ import { MathGateDiagnostic } from '../reference/gate-math.js';
|
|
|
10
10
|
import { ContentSegment } from '../reference/mixed-content.js';
|
|
11
11
|
import { GlyphScopeEntry } from '../reference/gloss.js';
|
|
12
12
|
import { NotationDefinitionRecord, NotationPageBundle, NotationBacklink, NotationDiagnostic, EditorialStatus, NotationGlossEntry } from '../reference/types.js';
|
|
13
|
-
import { NormalizedLessonExports, CourseConfig } from './course-config.js';
|
|
13
|
+
import { NormalizedLessonExports, NormalizedDisclosureDefaults, CourseConfig } from './course-config.js';
|
|
14
14
|
import { KnowledgeVersionDiagnostic } from './knowledge-version.js';
|
|
15
15
|
import { SidebarGroup } from './lesson-derivation.js';
|
|
16
16
|
import './collections.js';
|
|
@@ -90,8 +90,14 @@ import '../reference/keyed-float-validate.js';
|
|
|
90
90
|
* validation and can never get a stable number or `[[…]]` reference; this
|
|
91
91
|
* flags it (warning-only — the manifest shape a client already reads is
|
|
92
92
|
* unchanged).
|
|
93
|
+
* v14 (disclosure defaults, additive): `disclosureDefaults` — the resolved
|
|
94
|
+
* initial open/closed state for the "Page notation" panel and the generated
|
|
95
|
+
* "References" section, from `CourseConfig.disclosureDefaults`. Both are
|
|
96
|
+
* engine-authored sections, not written by a lesson, so they get their own
|
|
97
|
+
* default independent of a course's `##` heading default; closed unless the
|
|
98
|
+
* course sets one.
|
|
93
99
|
*/
|
|
94
|
-
declare const MANIFEST_SCHEMA_VERSION =
|
|
100
|
+
declare const MANIFEST_SCHEMA_VERSION = 14;
|
|
95
101
|
declare const MANIFEST_PATH: string;
|
|
96
102
|
interface ManifestCitation {
|
|
97
103
|
readonly id: string;
|
|
@@ -256,6 +262,8 @@ interface Manifest {
|
|
|
256
262
|
readonly schemaVersion: number;
|
|
257
263
|
/** Normalized build/UI settings for opt-in per-lesson exports. */
|
|
258
264
|
readonly lessonExports: NormalizedLessonExports;
|
|
265
|
+
/** Initial open/closed state for "Page notation" and "References". */
|
|
266
|
+
readonly disclosureDefaults: NormalizedDisclosureDefaults;
|
|
259
267
|
/** Reader-facing identity + versions (the `[?]` About panel). */
|
|
260
268
|
readonly about: ManifestAbout;
|
|
261
269
|
readonly notation: {
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
gateProseNotationInAssessments
|
|
46
46
|
} from "../reference/gate-prose-notation.js";
|
|
47
47
|
import { gateAbbreviations } from "../reference/gate-abbreviations.js";
|
|
48
|
+
import { gateHeadingLength } from "../reference/gate-heading-length.js";
|
|
48
49
|
import {
|
|
49
50
|
splitMixedContent
|
|
50
51
|
} from "../reference/mixed-content.js";
|
|
@@ -54,7 +55,8 @@ import { formulaGlyphScope } from "../reference/gloss.js";
|
|
|
54
55
|
import { resolveReferenceId } from "../reference/references.js";
|
|
55
56
|
import {
|
|
56
57
|
coursePartsOf,
|
|
57
|
-
normalizeLessonExports
|
|
58
|
+
normalizeLessonExports,
|
|
59
|
+
normalizeDisclosureDefaults
|
|
58
60
|
} from "./course-config.js";
|
|
59
61
|
import {
|
|
60
62
|
resolveKnowledgeVersion
|
|
@@ -73,7 +75,7 @@ import {
|
|
|
73
75
|
isLessonSlug,
|
|
74
76
|
lessonIdFromSlug
|
|
75
77
|
} from "./lesson-derivation.js";
|
|
76
|
-
const MANIFEST_SCHEMA_VERSION =
|
|
78
|
+
const MANIFEST_SCHEMA_VERSION = 14;
|
|
77
79
|
const REPOSITORY_ROOT = process.cwd();
|
|
78
80
|
const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
|
|
79
81
|
const MANIFEST_PATH = join(REPOSITORY_ROOT, "build", "manifest.json");
|
|
@@ -243,7 +245,8 @@ async function compileManifest(courseConfig) {
|
|
|
243
245
|
notationInput,
|
|
244
246
|
abbreviations: abbreviationsInput,
|
|
245
247
|
courseAbbreviations: courseConfig.abbreviations
|
|
246
|
-
})
|
|
248
|
+
}),
|
|
249
|
+
...gateHeadingLength(notationInput.lessons)
|
|
247
250
|
];
|
|
248
251
|
const notationLessonById = new Map(
|
|
249
252
|
notationInput.lessons.map((lesson) => [lesson.lessonId, lesson])
|
|
@@ -517,6 +520,7 @@ async function compileManifest(courseConfig) {
|
|
|
517
520
|
return {
|
|
518
521
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
519
522
|
lessonExports: normalizeLessonExports(courseConfig),
|
|
523
|
+
disclosureDefaults: normalizeDisclosureDefaults(courseConfig),
|
|
520
524
|
about,
|
|
521
525
|
notation: {
|
|
522
526
|
definitions: registry.definitions,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
|
+
import { NotationLessonInput } from './types.js';
|
|
3
|
+
|
|
4
|
+
declare const HEADING_LENGTH_LIMIT = 65;
|
|
5
|
+
/**
|
|
6
|
+
* Scan every lesson body for a heading or a keyed float's title running past
|
|
7
|
+
* `HEADING_LENGTH_LIMIT` characters. Returns one `MathGateDiagnostic`
|
|
8
|
+
* (context `'structure'`, always an error) per finding.
|
|
9
|
+
*/
|
|
10
|
+
declare function gateHeadingLength(lessons: readonly NotationLessonInput[]): MathGateDiagnostic[];
|
|
11
|
+
|
|
12
|
+
export { HEADING_LENGTH_LIMIT, gateHeadingLength };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { stripFencedCode } from "./equations.mjs";
|
|
2
|
+
import { lineOf } from "./gate-prose-notation.js";
|
|
3
|
+
const HEADING_LENGTH_LIMIT = 65;
|
|
4
|
+
const HEADING_LINE = /^[ \t]{0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/gm;
|
|
5
|
+
const TITLE_ATTRIBUTE_BY_TAG = {
|
|
6
|
+
Figure: "title",
|
|
7
|
+
Diagram: "label",
|
|
8
|
+
Memo: "title",
|
|
9
|
+
ShortNote: "title"
|
|
10
|
+
};
|
|
11
|
+
function readAttribute(attributesSource, name) {
|
|
12
|
+
const pattern = new RegExp(`\\b${name}\\s*=\\s*(["'])([^"']*)\\1`);
|
|
13
|
+
const match = pattern.exec(attributesSource);
|
|
14
|
+
return match ? match[2] : void 0;
|
|
15
|
+
}
|
|
16
|
+
function overLimitMessage(kind, text) {
|
|
17
|
+
return `${kind} ${JSON.stringify(text)} is ${text.length} characters, over the ${HEADING_LENGTH_LIMIT}-character limit (rule S1). Shorten it and move the rest into the paragraph or caption that follows.`;
|
|
18
|
+
}
|
|
19
|
+
function headingFindings(text) {
|
|
20
|
+
const out = [];
|
|
21
|
+
const re = new RegExp(HEADING_LINE.source, HEADING_LINE.flags);
|
|
22
|
+
let match;
|
|
23
|
+
while (match = re.exec(text)) {
|
|
24
|
+
const level = match[1]?.length ?? 1;
|
|
25
|
+
const title = (match[2] ?? "").trim();
|
|
26
|
+
if (title.length > HEADING_LENGTH_LIMIT) {
|
|
27
|
+
out.push({
|
|
28
|
+
index: match.index,
|
|
29
|
+
token: title,
|
|
30
|
+
message: overLimitMessage(`${"#".repeat(level)} heading`, title)
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function floatTitleFindings(text) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const [tagName, attribute] of Object.entries(TITLE_ATTRIBUTE_BY_TAG)) {
|
|
39
|
+
const opening = new RegExp(`<${tagName}\\b([^>]*)>`, "g");
|
|
40
|
+
let match;
|
|
41
|
+
while (match = opening.exec(text)) {
|
|
42
|
+
const value = readAttribute(match[1] ?? "", attribute)?.trim();
|
|
43
|
+
if (value && value.length > HEADING_LENGTH_LIMIT) {
|
|
44
|
+
out.push({
|
|
45
|
+
index: match.index,
|
|
46
|
+
token: value,
|
|
47
|
+
message: overLimitMessage(`<${tagName} ${attribute}>`, value)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function findingsIn(text) {
|
|
55
|
+
return [...headingFindings(text), ...floatTitleFindings(text)].sort(
|
|
56
|
+
(a, b) => a.index - b.index
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
function gateHeadingLength(lessons) {
|
|
60
|
+
const diagnostics = [];
|
|
61
|
+
for (const lesson of lessons) {
|
|
62
|
+
const text = stripFencedCode(lesson.body);
|
|
63
|
+
for (const finding of findingsIn(text)) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
context: "structure",
|
|
66
|
+
severity: "error",
|
|
67
|
+
file: lesson.source.file,
|
|
68
|
+
line: lineOf(lesson.body, finding.index),
|
|
69
|
+
token: finding.token,
|
|
70
|
+
message: finding.message
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return diagnostics;
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
HEADING_LENGTH_LIMIT,
|
|
78
|
+
gateHeadingLength
|
|
79
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NotationRegistryInput, NotationLessonInput } from './types.js';
|
|
2
2
|
|
|
3
3
|
interface MathGateDiagnostic {
|
|
4
|
-
readonly context: 'body' | 'formula' | 'assessment' | 'prose' | 'abbreviation';
|
|
4
|
+
readonly context: 'body' | 'formula' | 'assessment' | 'prose' | 'abbreviation' | 'structure';
|
|
5
5
|
/**
|
|
6
6
|
* Body / component-slot math blocks the build (the render path needs every
|
|
7
7
|
* glyph resolved for the KaTeX trust callback). `formula` joined it at D15:
|
|
@@ -19,7 +19,11 @@ interface MathGateDiagnostic {
|
|
|
19
19
|
* same change that introduced the check. `abbreviation` (N14,
|
|
20
20
|
* `gate-abbreviations.ts`) is the same "ships straight as an error" story
|
|
21
21
|
* for a different shape of plain-text token — an unexplained acronym or
|
|
22
|
-
* dotted abbreviation, rather than a mis-typed quantity/unit.
|
|
22
|
+
* dotted abbreviation, rather than a mis-typed quantity/unit. `structure`
|
|
23
|
+
* (S1, `gate-heading-length.ts`) is unrelated to notation — it caps how
|
|
24
|
+
* long a heading or a keyed float's title may run — but reuses this same
|
|
25
|
+
* diagnostic shape and the one `manifest.diagnostics.math` array rather
|
|
26
|
+
* than adding a second array the CLI has to know about.
|
|
23
27
|
*/
|
|
24
28
|
readonly severity: 'error' | 'warning';
|
|
25
29
|
readonly file: string;
|
|
@@ -6,7 +6,8 @@ const SEVERITY = {
|
|
|
6
6
|
formula: "error",
|
|
7
7
|
assessment: "error",
|
|
8
8
|
prose: "error",
|
|
9
|
-
abbreviation: "error"
|
|
9
|
+
abbreviation: "error",
|
|
10
|
+
structure: "error"
|
|
10
11
|
};
|
|
11
12
|
const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
|
|
12
13
|
const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
|
|
@@ -42,6 +42,14 @@ export function resolveMathGlyphs(latex: string, definitions: Array<{
|
|
|
42
42
|
* Used by the `operator-explained` consistency check (rule N8); the glyph
|
|
43
43
|
* resolver still skips operator bodies whole.
|
|
44
44
|
*
|
|
45
|
+
* The span handed in is raw lesson source, so it may still carry
|
|
46
|
+
* `\explain{key}{…}` / `\slot{key}{…}` markers around other symbols in the
|
|
47
|
+
* same expression — `\explain` is not valid KaTeX, so parsing it unstripped
|
|
48
|
+
* throws and this function used to swallow that as "no operators here" for
|
|
49
|
+
* the whole span, silently hiding any `\operatorname{…}` alongside the
|
|
50
|
+
* marker. `stripNotationMarkers` removes just the marker wrapper first so
|
|
51
|
+
* the scan sees plain, parseable math.
|
|
52
|
+
*
|
|
45
53
|
* @param {string} latex
|
|
46
54
|
* @returns {Array<{ name: string, source: 'named' | 'operatorname' }>}
|
|
47
55
|
*/
|
|
@@ -60,6 +68,10 @@ export function collectOperators(latex: string): Array<{
|
|
|
60
68
|
* `roman-label-explained` consistency check (rule N10); the glyph resolver
|
|
61
69
|
* still skips `\mathrm{…}` bodies whole ([N3](../../docs/authoring/notation.md#n3)).
|
|
62
70
|
*
|
|
71
|
+
* Same `stripNotationMarkers` preprocessing as {@link collectOperators}, and
|
|
72
|
+
* for the same reason: an unstripped `\explain{…}{…}` elsewhere in the span
|
|
73
|
+
* fails the raw KaTeX parse and used to silently drop every label in it.
|
|
74
|
+
*
|
|
63
75
|
* @param {string} latex
|
|
64
76
|
* @returns {Array<{ name: string }>}
|
|
65
77
|
*/
|