explico 0.7.0 → 0.11.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 +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- 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 +14 -1
- package/dist/compiler/logging.js +6 -2
- 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 +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- 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 +69 -34
- package/dist/reference/equations.mjs +90 -44
- 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 +3 -2
- 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 +47 -23
- 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 +44 -6
- package/dist/reference/gate-math.js +63 -10
- 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 +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +5 -5
- 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 +24 -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 +2 -2
- 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 +1324 -2
- package/docs/authoring/notation.md +616 -344
- 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/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- 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 +21 -4
- 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 +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- 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 +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- 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 +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +17 -32
- 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 +24 -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 +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Units written out as English words — "2 meters per second squared",
|
|
3
|
+
* "30 degrees", "$x = 3$ meters" — for rule N9's `spelled-unit` check
|
|
4
|
+
* (`gate-prose-notation.ts`). A unit is an upright symbol inside `$…$`
|
|
5
|
+
* (`$2\ \text{m/s}^2$`, `$30^{\circ}$`, `$x = 3\ \text{m}$`), so a spelled-out
|
|
6
|
+
* unit that quantifies something is the same mistake as `45 m/s` in plain
|
|
7
|
+
* text.
|
|
8
|
+
*
|
|
9
|
+
* A unit word on its own is not flagged — "measured in meters" names the
|
|
10
|
+
* unit, it does not state a quantity. What is flagged is a unit word
|
|
11
|
+
*
|
|
12
|
+
* - after a number, written in digits or as a word, in prose or typeset
|
|
13
|
+
* (`2 meters`, `two-meter`, `$x = 3$ meters`);
|
|
14
|
+
* - after `per`, or before it (`rotations per second`, `meters per unit`);
|
|
15
|
+
* - with `square` / `cubic` before it or `squared` / `cubed` after it.
|
|
16
|
+
*
|
|
17
|
+
* "Degrees of freedom" is not a unit. Calendar time (day, week, month, year)
|
|
18
|
+
* and currency are left out on purpose: they are ordinary words in a finance
|
|
19
|
+
* course's prose, not SI units. `notation` `units:` strings are a different
|
|
20
|
+
* vocabulary (`units-vocab.ts`, rule N19) and are never scanned here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One unit: the regex stem of its English name (singular), and its symbol. */
|
|
24
|
+
interface UnitWord {
|
|
25
|
+
readonly stem: string;
|
|
26
|
+
/** The symbol inside `\text{…}`, or `°` for an angle in degrees. */
|
|
27
|
+
readonly symbol: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Prefixed forms first, so a suggestion names the right symbol; `\b` on both
|
|
31
|
+
// sides keeps `gram` from matching inside `kilogram` or `program` anyway.
|
|
32
|
+
const UNIT_WORDS: readonly UnitWord[] = [
|
|
33
|
+
{ stem: 'kilomet(?:er|re)', symbol: 'km' },
|
|
34
|
+
{ stem: 'centimet(?:er|re)', symbol: 'cm' },
|
|
35
|
+
{ stem: 'millimet(?:er|re)', symbol: 'mm' },
|
|
36
|
+
{ stem: 'met(?:er|re)', symbol: 'm' },
|
|
37
|
+
{ stem: 'millisecond', symbol: 'ms' },
|
|
38
|
+
{ stem: 'second', symbol: 's' },
|
|
39
|
+
{ stem: 'minute', symbol: 'min' },
|
|
40
|
+
{ stem: 'hour', symbol: 'h' },
|
|
41
|
+
{ stem: 'kilogram', symbol: 'kg' },
|
|
42
|
+
{ stem: 'milligram', symbol: 'mg' },
|
|
43
|
+
{ stem: 'gram', symbol: 'g' },
|
|
44
|
+
{ stem: 'newton', symbol: 'N' },
|
|
45
|
+
{ stem: 'kilojoule', symbol: 'kJ' },
|
|
46
|
+
{ stem: 'joule', symbol: 'J' },
|
|
47
|
+
{ stem: 'kilowatt', symbol: 'kW' },
|
|
48
|
+
{ stem: 'watt', symbol: 'W' },
|
|
49
|
+
{ stem: 'hertz', symbol: 'Hz' },
|
|
50
|
+
{ stem: 'kilopascal', symbol: 'kPa' },
|
|
51
|
+
{ stem: 'pascal', symbol: 'Pa' },
|
|
52
|
+
{ stem: 'volt', symbol: 'V' },
|
|
53
|
+
{ stem: 'amp(?:ere)?', symbol: 'A' },
|
|
54
|
+
{ stem: 'coulomb', symbol: 'C' },
|
|
55
|
+
{ stem: 'kelvin', symbol: 'K' },
|
|
56
|
+
{ stem: 'mole', symbol: 'mol' },
|
|
57
|
+
{ stem: 'lit(?:er|re)', symbol: 'L' },
|
|
58
|
+
{ stem: 'radian', symbol: 'rad' },
|
|
59
|
+
{ stem: 'degree', symbol: '°' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const NUMBER_WORDS: Readonly<Record<string, number>> = {
|
|
63
|
+
zero: 0, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7,
|
|
64
|
+
eight: 8, nine: 9, ten: 10, eleven: 11, twelve: 12, twenty: 20, thirty: 30,
|
|
65
|
+
forty: 40, fifty: 50, sixty: 60, seventy: 70, eighty: 80, ninety: 90,
|
|
66
|
+
hundred: 100, thousand: 1000,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const UNIT = `(?:${UNIT_WORDS.map((u) => `${u.stem}s?`).join('|')})`;
|
|
70
|
+
// A unit word not glued to a hyphenated compound (`second-order`) and not the
|
|
71
|
+
// start of "degrees of freedom".
|
|
72
|
+
const UNIT_WORD = String.raw`\b${UNIT}\b(?!-)(?!\s+of\s+freedom\b)`;
|
|
73
|
+
const POWER_BEFORE = String.raw`(?:(?:square|cubic)\s+)`;
|
|
74
|
+
const POWER_AFTER = String.raw`(?:\s+(?:squared|cubed)\b)`;
|
|
75
|
+
const TERM = String.raw`${POWER_BEFORE}?${UNIT_WORD}${POWER_AFTER}?`;
|
|
76
|
+
const COMPOUND = String.raw`${TERM}(?:\s+per\s+${TERM})*`;
|
|
77
|
+
const NUMBER = String.raw`(?:\d+(?:\.\d+)?|${Object.keys(NUMBER_WORDS).join('|')})`;
|
|
78
|
+
|
|
79
|
+
/** Each shape that makes a unit word a stated quantity (or rate). */
|
|
80
|
+
const PATTERNS: readonly RegExp[] = [
|
|
81
|
+
// 2 meters per second squared · two-meter · 4 seconds
|
|
82
|
+
new RegExp(String.raw`(?<![\w.])${NUMBER}(?:\s+|-)${COMPOUND}`, 'gi'),
|
|
83
|
+
// meters per second · rotations per second
|
|
84
|
+
new RegExp(String.raw`(?:${TERM}\s+)?\bper\s+${COMPOUND}`, 'gi'),
|
|
85
|
+
// meters per unit time
|
|
86
|
+
new RegExp(String.raw`${TERM}\s+per\b`, 'gi'),
|
|
87
|
+
// square meters · seconds squared
|
|
88
|
+
new RegExp(String.raw`${POWER_BEFORE}${UNIT_WORD}${POWER_AFTER}?|${UNIT_WORD}${POWER_AFTER}`, 'gi'),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// An inline `$…$` whose last visible character is a digit, then a unit word
|
|
92
|
+
// in the prose after it: `$x = 3$ meters`.
|
|
93
|
+
const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/g;
|
|
94
|
+
const AFTER_MATH = new RegExp(String.raw`^(?:[ \t]+|-)(${COMPOUND})`, 'i');
|
|
95
|
+
|
|
96
|
+
export interface SpelledUnit {
|
|
97
|
+
readonly index: number;
|
|
98
|
+
readonly token: string;
|
|
99
|
+
/** What to write instead, as `$…$`. */
|
|
100
|
+
readonly suggestion: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function unitOf(word: string): UnitWord | undefined {
|
|
104
|
+
return UNIT_WORDS.find((u) => new RegExp(`^${u.stem}s?$`, 'i').test(word));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The unit words a phrase contains, lowercased. */
|
|
108
|
+
function unitWordsIn(phrase: string): string[] {
|
|
109
|
+
return [...phrase.matchAll(new RegExp(UNIT_WORD, 'gi'))].map((m) => m[0].toLowerCase());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `{2 meters per second squared}` → `\text{m/s}^{2}`, or undefined when it has no clean form. */
|
|
113
|
+
function unitLatex(compound: string): string | undefined {
|
|
114
|
+
const groups = compound.trim().split(/\s+per\s+/i);
|
|
115
|
+
const segments: { text: string; power?: string }[] = [];
|
|
116
|
+
for (const [i, group] of groups.entries()) {
|
|
117
|
+
const words = group.toLowerCase().split(/\s+/);
|
|
118
|
+
const power =
|
|
119
|
+
words.includes('square') || words.includes('squared')
|
|
120
|
+
? '2'
|
|
121
|
+
: words.includes('cubic') || words.includes('cubed')
|
|
122
|
+
? '3'
|
|
123
|
+
: undefined;
|
|
124
|
+
const unit = words.map(unitOf).find(Boolean);
|
|
125
|
+
if (unit === undefined) return undefined;
|
|
126
|
+
if (unit.symbol === '°') {
|
|
127
|
+
// An angle alone is `^{\circ}`; degrees inside a rate has no tidy form.
|
|
128
|
+
return groups.length === 1 && power === undefined ? '^{\\circ}' : undefined;
|
|
129
|
+
}
|
|
130
|
+
segments.push({ text: `${i > 0 ? '/' : ''}${unit.symbol}`, ...(power ? { power } : {}) });
|
|
131
|
+
}
|
|
132
|
+
let out = '';
|
|
133
|
+
let pending = '';
|
|
134
|
+
for (const segment of segments) {
|
|
135
|
+
pending += segment.text;
|
|
136
|
+
if (segment.power !== undefined) {
|
|
137
|
+
out += `\\text{${pending}}^{${segment.power}}`;
|
|
138
|
+
pending = '';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return pending === '' ? out : `${out}\\text{${pending}}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function numberOf(word: string): string {
|
|
145
|
+
return /^\d/.test(word) ? word : String(NUMBER_WORDS[word.toLowerCase()] ?? word);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const GENERIC = 'a unit symbol in `$…$` (`$2\\ \\text{m/s}^2$`, `$30^{\\circ}$`)';
|
|
149
|
+
const LEADING_NUMBER = new RegExp(String.raw`^(${NUMBER})(?:\s+|-)([\s\S]*)$`, 'i');
|
|
150
|
+
const STARTS_WITH_UNIT = new RegExp(String.raw`^${TERM}`, 'i');
|
|
151
|
+
|
|
152
|
+
/** What to write instead of `phrase`, as a backticked `$…$`. */
|
|
153
|
+
function suggest(phrase: string): string {
|
|
154
|
+
const numbered = LEADING_NUMBER.exec(phrase.trim());
|
|
155
|
+
const number = numbered ? numberOf(numbered[1]!) : undefined;
|
|
156
|
+
const rest = numbered ? numbered[2]! : phrase.trim();
|
|
157
|
+
|
|
158
|
+
// `rotations per second` / `per second`: only the part after `per` is a unit.
|
|
159
|
+
if (!STARTS_WITH_UNIT.test(rest)) {
|
|
160
|
+
const after = /\bper\s+([\s\S]*)$/i.exec(rest)?.[1];
|
|
161
|
+
const latex = after === undefined ? undefined : unitLatex(after);
|
|
162
|
+
return latex === undefined ? GENERIC : `the rate in \`$…$\`, with \`${latex}\` after the slash (\`$…/${latex}$\`)`;
|
|
163
|
+
}
|
|
164
|
+
// `meters per unit time`: the unit before a `per` that no unit follows.
|
|
165
|
+
const unit = rest.replace(/\s+per$/i, '');
|
|
166
|
+
const latex = unitLatex(unit);
|
|
167
|
+
if (latex === undefined) return GENERIC;
|
|
168
|
+
const trailingPer = unit !== rest ? '/…' : '';
|
|
169
|
+
if (number === undefined) return `\`$${latex}${trailingPer}$\``;
|
|
170
|
+
return `\`$${number}${latex.startsWith('^') ? '' : '\\ '}${latex}${trailingPer}$\``;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Overlapping matches collapse to the earliest, then longest. */
|
|
174
|
+
function merge(found: SpelledUnit[]): SpelledUnit[] {
|
|
175
|
+
const sorted = [...found].sort((a, b) => a.index - b.index || b.token.length - a.token.length);
|
|
176
|
+
const out: SpelledUnit[] = [];
|
|
177
|
+
let end = -1;
|
|
178
|
+
for (const item of sorted) {
|
|
179
|
+
if (item.index < end) continue;
|
|
180
|
+
out.push(item);
|
|
181
|
+
end = item.index + item.token.length;
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Every spelled-out unit in a lesson or assessment text. `prose` has code,
|
|
188
|
+
* math, references, JSX tags and comments blanked (`proseOnly`); `withMath`
|
|
189
|
+
* is the same text with math left in, for the `$x = 3$ meters` shape. Both
|
|
190
|
+
* must share offsets. A phrase whose unit words are all in `allow`
|
|
191
|
+
* (`rules.N9.allow`, singular or plural) is skipped.
|
|
192
|
+
*/
|
|
193
|
+
export function findSpelledUnits(
|
|
194
|
+
prose: string,
|
|
195
|
+
withMath: string,
|
|
196
|
+
allow: readonly string[] = [],
|
|
197
|
+
): SpelledUnit[] {
|
|
198
|
+
const allowed = new Set(allow.map((word) => word.toLowerCase()));
|
|
199
|
+
const isAllowed = (phrase: string) => {
|
|
200
|
+
const words = unitWordsIn(phrase);
|
|
201
|
+
return words.length > 0 && words.every((w) => allowed.has(w) || allowed.has(w.replace(/s$/, '')));
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const found: SpelledUnit[] = [];
|
|
205
|
+
for (const pattern of PATTERNS) {
|
|
206
|
+
for (const m of prose.matchAll(new RegExp(pattern.source, pattern.flags))) {
|
|
207
|
+
if (isAllowed(m[0])) continue;
|
|
208
|
+
found.push({ index: m.index, token: m[0], suggestion: suggest(m[0]) });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const m of withMath.matchAll(INLINE_MATH)) {
|
|
213
|
+
const content = m[1]!;
|
|
214
|
+
if (!/\d\s*$/.test(content.replace(/\\[,;:! ]|~|\\quad/g, ''))) continue;
|
|
215
|
+
const after = withMath.slice(m.index + m[0].length);
|
|
216
|
+
const unit = AFTER_MATH.exec(after);
|
|
217
|
+
if (unit === null || isAllowed(unit[1]!)) continue;
|
|
218
|
+
// The unit must sit in prose, not in a code span or tag that withMath kept.
|
|
219
|
+
const at = m.index + m[0].length + unit[0].indexOf(unit[1]!);
|
|
220
|
+
if (prose.slice(at, at + unit[1]!.length) !== unit[1]) continue;
|
|
221
|
+
const latex = unitLatex(unit[1]!);
|
|
222
|
+
const typeset = latex === undefined
|
|
223
|
+
? 'the unit inside the same `$…$` as its number'
|
|
224
|
+
: `\`$${content.trim()}${latex.startsWith('^') ? '' : '\\ '}${latex}$\``;
|
|
225
|
+
found.push({
|
|
226
|
+
index: m.index,
|
|
227
|
+
token: `${m[0]}${unit[0]}`,
|
|
228
|
+
suggestion: typeset,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return merge(found);
|
|
233
|
+
}
|