@erclx/canon 5.52.0 → 5.53.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -4
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +0 -3
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -1
  6. package/claude/skills/design-taste/references/kinds.md +0 -1
  7. package/claude/skills/draw-figure/REQUIREMENT.md +2 -3
  8. package/claude/skills/draw-figure/SKILL.md +4 -5
  9. package/claude/skills/draw-figure/references/figures.md +0 -1
  10. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  11. package/claude/skills/pick-by-eye/REQUIREMENT.md +0 -1
  12. package/claude/skills/pick-by-eye/SKILL.md +1 -1
  13. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  14. package/claude/skills/teach-craft/SKILL.md +39 -0
  15. package/claude/skills/teach-craft/references/lesson-craft.md +52 -0
  16. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +12 -4
  17. package/docs/agents/audits.md +0 -1
  18. package/docs/target/migrations.md +10 -1
  19. package/docs/workflow/parallel-features.md +1 -1
  20. package/docs/workflow/skill-map-on-request.md +10 -22
  21. package/docs/workflow/skill-map.md +12 -14
  22. package/package.json +1 -4
  23. package/scripts/core/regen-web-previews.ts +2 -67
  24. package/src/audits/catalog.ts +0 -1
  25. package/src/browser/engine.ts +3 -6
  26. package/src/capture/render.ts +1 -1
  27. package/src/claude/cases/authoring.ts +0 -12
  28. package/src/claude/cases/misc.ts +0 -17
  29. package/src/claude/cases/workflow.ts +2 -38
  30. package/src/claude/skills-audit.ts +0 -1
  31. package/src/cli/help.ts +0 -7
  32. package/src/cli.ts +0 -6
  33. package/src/commands/records.ts +0 -1
  34. package/src/commands/serve.ts +2 -2
  35. package/src/design/components.ts +8 -1514
  36. package/src/design/css.ts +5 -6
  37. package/src/design/fonts.ts +3 -5
  38. package/src/driver/drive.ts +3 -4
  39. package/src/driver/steps.ts +2 -5
  40. package/src/inventory/walk.ts +1 -1
  41. package/src/markdown/scope.ts +4 -31
  42. package/src/records/validate.ts +4 -176
  43. package/src/sandbox/equivalence.ts +0 -5
  44. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  45. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  46. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  47. package/claude/skills/draft-doc/SKILL.md +0 -11
  48. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  49. package/claude/skills/draft-figure/SKILL.md +0 -11
  50. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  51. package/claude/skills/draft-identity/SKILL.md +0 -11
  52. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  53. package/claude/skills/draft-ready/SKILL.md +0 -11
  54. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  55. package/claude/skills/draft-screencast/SKILL.md +0 -11
  56. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  57. package/claude/skills/draft-slides/SKILL.md +0 -11
  58. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  59. package/claude/skills/make-deck/SKILL.md +0 -80
  60. package/claude/skills/make-deck/references/deck.md +0 -183
  61. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  62. package/claude/skills/read-frames/SKILL.md +0 -55
  63. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  64. package/claude/skills/record-screencast/SKILL.md +0 -99
  65. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  66. package/claude/skills/script-screencast/SKILL.md +0 -70
  67. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  68. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  69. package/claude/skills/teach-workspace/SKILL.md +0 -196
  70. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  71. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  72. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  73. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  74. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  75. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  76. package/claude/skills/teach-workspace/references/teach.md +0 -211
  77. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  78. package/claude/skills/video-craft/SKILL.md +0 -96
  79. package/claude/skills/video-craft/references/adopted.md +0 -110
  80. package/src/commands/demo.ts +0 -542
  81. package/src/commands/slides.ts +0 -184
  82. package/src/commands/teach.ts +0 -1401
  83. package/src/demo/beats.ts +0 -135
  84. package/src/demo/compile.ts +0 -350
  85. package/src/demo/container.ts +0 -219
  86. package/src/demo/cursors.ts +0 -55
  87. package/src/demo/drive.ts +0 -442
  88. package/src/demo/pointer.ts +0 -178
  89. package/src/demo/theme.ts +0 -112
  90. package/src/demo/timeline.ts +0 -64
  91. package/src/slides/convert/chart.ts +0 -165
  92. package/src/slides/convert/deck.ts +0 -244
  93. package/src/slides/convert/export.ts +0 -567
  94. package/src/slides/convert/master.ts +0 -198
  95. package/src/slides/convert/shapes.ts +0 -804
  96. package/src/slides/convert/svg.ts +0 -38
  97. package/src/slides/convert/walk.ts +0 -671
  98. package/src/slides/decks.ts +0 -110
  99. package/src/slides/open.ts +0 -18
  100. package/src/slides/package/fonts.ts +0 -301
  101. package/src/slides/package/motion.ts +0 -316
  102. package/src/slides/package/test-face.ts +0 -113
  103. package/src/teach/browser/compile.ts +0 -44
  104. package/src/teach/browser/dismiss.ts +0 -76
  105. package/src/teach/browser/focus-line.ts +0 -19
  106. package/src/teach/browser/glossary-filter.ts +0 -44
  107. package/src/teach/browser/head.ts +0 -30
  108. package/src/teach/browser/quiz.ts +0 -19
  109. package/src/teach/browser/reload.ts +0 -9
  110. package/src/teach/browser/sidebar.ts +0 -233
  111. package/src/teach/browser/theme.ts +0 -18
  112. package/src/teach/components/code.tsx +0 -24
  113. package/src/teach/components/heading.tsx +0 -11
  114. package/src/teach/components/list.tsx +0 -17
  115. package/src/teach/components/paragraph.tsx +0 -29
  116. package/src/teach/components/refs.tsx +0 -28
  117. package/src/teach/cross-references.ts +0 -78
  118. package/src/teach/fonts.ts +0 -16
  119. package/src/teach/highlight.ts +0 -71
  120. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  121. package/src/teach/html/jsx-runtime.ts +0 -113
  122. package/src/teach/html/raw.ts +0 -28
  123. package/src/teach/lesson.ts +0 -178
  124. package/src/teach/nav.ts +0 -1337
  125. package/src/teach/render.ts +0 -240
  126. package/src/teach/workspace.ts +0 -1164
  127. package/src/teach/write-if-changed.ts +0 -17
@@ -1,233 +0,0 @@
1
- import { focusLine } from '@/teach/browser/focus-line'
2
-
3
- /**
4
- * `focusLine` is declared in the page ahead of this function by `compile`, so
5
- * the call below reaches the one source the unit test exercises.
6
- */
7
- export function sidebar(breakpoint: number): void {
8
- var root = document.documentElement
9
- var found = document.querySelector<HTMLElement>('.sb')
10
- if (!found) return
11
- var panel = found
12
-
13
- var fold = document.querySelector<HTMLElement>('.sb-fold')
14
- var narrow = matchMedia('(max-width: ' + breakpoint + 'px)')
15
-
16
- var FOCUSABLE =
17
- "a[href], button:not([disabled]), summary, input, [tabindex]:not([tabindex='-1'])"
18
-
19
- function panelStops() {
20
- return Array.prototype.slice
21
- .call(panel.querySelectorAll(FOCUSABLE))
22
- .filter(function (el: HTMLElement) {
23
- return el.offsetParent !== null || el === document.activeElement
24
- }) as HTMLElement[]
25
- }
26
-
27
- /* The focus return belongs here rather than on the close control, because all
28
- three routes out land here and only one of them used to move focus. The
29
- shut panel takes visibility: hidden 180ms later, so a reader who pressed
30
- Escape kept focus on a control that then disappeared under them and the
31
- browser dropped them at the top of the document. */
32
- function shut() {
33
- var inside = panel.contains(document.activeElement)
34
- root.classList.add('sb-shut')
35
- try {
36
- localStorage.setItem('teach-sb', 'shut')
37
- } catch {}
38
- if (inside && fold) fold.focus()
39
- }
40
-
41
- if (fold)
42
- fold.addEventListener('click', function () {
43
- root.classList.toggle('sb-shut')
44
- try {
45
- localStorage.setItem(
46
- 'teach-sb',
47
- root.classList.contains('sb-shut') ? 'shut' : 'open',
48
- )
49
- } catch {}
50
- /* An overlay takes focus with it. Without this a keyboard reader opens the
51
- panel and goes on tabbing through the lesson behind the scrim. */
52
- if (!narrow.matches || root.classList.contains('sb-shut')) return
53
- var first = panel.querySelector<HTMLElement>('a, button, summary, input')
54
- if (first) first.focus()
55
- })
56
-
57
- var scrim = document.createElement('div')
58
- scrim.className = 'sb-scrim'
59
- document.body.appendChild(scrim)
60
- scrim.addEventListener('click', shut)
61
-
62
- document.addEventListener('keydown', function (e) {
63
- if (e.key === 'Escape' && narrow.matches) {
64
- shut()
65
- return
66
- }
67
-
68
- /* The scrim says the lesson is unavailable, so Tab must not reach it.
69
- Moving focus into the panel on open only defers this by however many
70
- controls the panel holds. */
71
- if (e.key !== 'Tab') return
72
- if (!narrow.matches || root.classList.contains('sb-shut')) return
73
-
74
- var stops = panelStops()
75
- if (!stops.length) return
76
-
77
- var first = stops[0]
78
- var last = stops[stops.length - 1]
79
-
80
- if (!panel.contains(document.activeElement)) {
81
- ;(e.shiftKey ? last : first).focus()
82
- e.preventDefault()
83
- } else if (e.shiftKey && document.activeElement === first) {
84
- last.focus()
85
- e.preventDefault()
86
- } else if (!e.shiftKey && document.activeElement === last) {
87
- first.focus()
88
- e.preventDefault()
89
- }
90
- })
91
-
92
- /* The panel covers the masthead and the toggle that opened it, so it carries
93
- its own way out rather than leaving the scrim as the only route. */
94
- var close = document.createElement('button')
95
- close.type = 'button'
96
- close.className = 'sb-close'
97
- close.setAttribute('aria-label', 'Close the course panel')
98
- close.textContent = '×'
99
- close.addEventListener('click', shut)
100
- panel.appendChild(close)
101
-
102
- var MIN = 208,
103
- MAX = 296,
104
- DEF = 256
105
- var grip = document.querySelector<HTMLElement>('.sb-grip')
106
- var lastWidth = parseInt(root.style.getPropertyValue('--sb-w'), 10) || DEF
107
-
108
- function setWidth(px: number, save: boolean) {
109
- var w = Math.max(MIN, Math.min(MAX, Math.round(px)))
110
- root.style.setProperty('--sb-w', w + 'px')
111
- lastWidth = w
112
- if (save) {
113
- try {
114
- localStorage.setItem('teach-sb-w', String(w))
115
- } catch {}
116
- }
117
- }
118
-
119
- if (grip) {
120
- var handle = grip
121
- var dragging = false
122
- handle.addEventListener('pointerdown', function (e) {
123
- dragging = true
124
- handle.setPointerCapture(e.pointerId)
125
- root.classList.add('sb-drag')
126
- e.preventDefault()
127
- })
128
- handle.addEventListener('pointermove', function (e) {
129
- if (dragging) setWidth(e.clientX, false)
130
- })
131
- handle.addEventListener('pointerup', function (e) {
132
- if (!dragging) return
133
- dragging = false
134
- root.classList.remove('sb-drag')
135
- handle.releasePointerCapture(e.pointerId)
136
- setWidth(lastWidth, true)
137
- })
138
- handle.addEventListener('dblclick', function () {
139
- setWidth(DEF, true)
140
- })
141
- handle.addEventListener('keydown', function (e) {
142
- var step = e.shiftKey ? 32 : 8
143
- if (e.key === 'ArrowLeft') {
144
- setWidth(lastWidth - step, true)
145
- e.preventDefault()
146
- } else if (e.key === 'ArrowRight') {
147
- setWidth(lastWidth + step, true)
148
- e.preventDefault()
149
- } else if (e.key === 'Home') {
150
- setWidth(DEF, true)
151
- e.preventDefault()
152
- }
153
- })
154
- }
155
-
156
- var filter = document.querySelector<HTMLInputElement>('.sb-filter input')
157
- if (filter) {
158
- var box = filter
159
- box.addEventListener('input', function () {
160
- var q = box.value.trim().toLowerCase()
161
- document
162
- .querySelectorAll<HTMLElement>('.sb-list > li')
163
- .forEach(function (li) {
164
- li.classList.toggle(
165
- 'hide',
166
- q !== '' &&
167
- (li.textContent as string).toLowerCase().indexOf(q) === -1,
168
- )
169
- })
170
- })
171
- }
172
-
173
- var slot = document.querySelector('.sb-out-slot')
174
- var hs = Array.prototype.slice.call(
175
- document.querySelectorAll('main h2'),
176
- ) as HTMLElement[]
177
- var links: HTMLElement[] = []
178
-
179
- if (slot && hs.length) {
180
- var list = document.createElement('ul')
181
- list.className = 'sb-out'
182
- hs.forEach(function (h, i) {
183
- if (!h.id) h.id = 's' + i
184
- var li = document.createElement('li')
185
- var a = document.createElement('a')
186
- a.href = '#' + h.id
187
- a.textContent = (h.textContent as string).trim()
188
- li.appendChild(a)
189
- list.appendChild(li)
190
- })
191
- ;(slot.parentNode as Node).replaceChild(list, slot)
192
- links = Array.prototype.slice.call(list.querySelectorAll('a'))
193
- }
194
-
195
- /* The bar reports position inside the lesson, which nothing reported before
196
- the segmented track retired. The breadcrumb's last segment carries the
197
- lesson title once the real one has scrolled off. */
198
- var bar = document.querySelector<HTMLElement>('.bar')
199
- var here = document.querySelector('.crumb-here')
200
- var h1 = document.querySelector('main h1')
201
- var counter = here ? (here.textContent as string).trim() : ''
202
- var title = h1 ? (h1.textContent as string).trim() : ''
203
-
204
- function sync() {
205
- var max = document.documentElement.scrollHeight - innerHeight
206
- if (bar) {
207
- var pct = max > 0 ? Math.min(100, Math.max(0, (scrollY / max) * 100)) : 0
208
- bar.style.setProperty('--read', pct.toFixed(1) + '%')
209
- }
210
-
211
- if (here && h1 && title && counter) {
212
- var want = h1.getBoundingClientRect().bottom < 56 ? title : counter
213
- if (here.textContent !== want) here.textContent = want
214
- }
215
-
216
- if (links.length) {
217
- /* Nothing is marked until a heading has actually passed the line, so the
218
- first section is not reported as current while the title is on screen. */
219
- var best = -1
220
- var line = focusLine(scrollY, max, innerHeight)
221
- for (var i = 0; i < hs.length; i++) {
222
- if (hs[i].getBoundingClientRect().top <= line) best = i
223
- }
224
- links.forEach(function (l, j) {
225
- l.classList.toggle('on', j === best)
226
- })
227
- }
228
- }
229
-
230
- addEventListener('scroll', sync, { passive: true })
231
- addEventListener('resize', sync)
232
- sync()
233
- }
@@ -1,18 +0,0 @@
1
- export function theme(): void {
2
- var r = document.documentElement
3
- try {
4
- var s = localStorage.getItem('course-theme')
5
- if (s) r.dataset.theme = s
6
- } catch {}
7
- document.addEventListener('click', function (e) {
8
- var b = (e.target as Element).closest('.theme')
9
- if (!b) return
10
- var d =
11
- r.dataset.theme === 'dark' ||
12
- (!r.dataset.theme && matchMedia('(prefers-color-scheme: dark)').matches)
13
- r.dataset.theme = d ? 'light' : 'dark'
14
- try {
15
- localStorage.setItem('course-theme', r.dataset.theme)
16
- } catch {}
17
- })
18
- }
@@ -1,24 +0,0 @@
1
- /** @jsxImportSource ../html */
2
- import { highlight } from '@/teach/highlight'
3
- import type { Element } from '@/teach/html/jsx-runtime'
4
- import { raw } from '@/teach/html/raw'
5
-
6
- export interface CodeProps {
7
- /** A highlight.js language name or alias. Unregistered ones render plain. */
8
- readonly lang?: string
9
- readonly text: string
10
- }
11
-
12
- /**
13
- * The one way code reaches a lesson body. The highlighter escapes every source
14
- * character before wrapping tokens in spans, which is what makes passing its
15
- * output through `raw` safe.
16
- */
17
- export function Code({ lang, text }: CodeProps): Element {
18
- const className = lang === undefined ? 'hljs' : `hljs language-${lang}`
19
- return (
20
- <pre>
21
- <code class={className}>{raw(highlight(text, lang).html)}</code>
22
- </pre>
23
- )
24
- }
@@ -1,11 +0,0 @@
1
- /** @jsxImportSource ../html */
2
- import type { Child, Element } from '@/teach/html/jsx-runtime'
3
-
4
- export interface HeadingProps {
5
- readonly level: 1 | 2
6
- readonly children: Child
7
- }
8
-
9
- export function Heading({ level, children }: HeadingProps): Element {
10
- return level === 1 ? <h1>{children}</h1> : <h2>{children}</h2>
11
- }
@@ -1,17 +0,0 @@
1
- /** @jsxImportSource ../html */
2
- import type { Child, Element } from '@/teach/html/jsx-runtime'
3
-
4
- export interface ListProps {
5
- readonly ordered?: boolean
6
- readonly items: readonly Child[]
7
- }
8
-
9
- /**
10
- * Composes an item per entry rather than taking pre-built `<li>` children, so
11
- * every list in a lesson escapes the same way regardless of what the caller
12
- * hands in.
13
- */
14
- export function List({ ordered, items }: ListProps): Element {
15
- const rendered = items.map((item) => <li>{item}</li>)
16
- return ordered ? <ol>{rendered}</ol> : <ul>{rendered}</ul>
17
- }
@@ -1,29 +0,0 @@
1
- /** @jsxImportSource ../html */
2
- import type { Child, Element } from '@/teach/html/jsx-runtime'
3
-
4
- export interface ParagraphProps {
5
- /** The lesson's dek, read back by `extractLessonMeta` in `@/teach/nav`. */
6
- readonly lede?: boolean
7
- /** Reference numbers, each linking to the `Refs` item carrying that id. */
8
- readonly cites?: readonly number[]
9
- readonly children: Child
10
- }
11
-
12
- export function Paragraph({ lede, cites, children }: ParagraphProps): Element {
13
- const markers = (cites ?? []).map((cite) => (
14
- <sup class="cite">
15
- <a href={`#r${cite}`}>{cite}</a>
16
- </sup>
17
- ))
18
- return lede ? (
19
- <p class="lede">
20
- {children}
21
- {markers}
22
- </p>
23
- ) : (
24
- <p>
25
- {children}
26
- {markers}
27
- </p>
28
- )
29
- }
@@ -1,28 +0,0 @@
1
- /** @jsxImportSource ../html */
2
- import type { Element } from '@/teach/html/jsx-runtime'
3
-
4
- export interface Reference {
5
- readonly title: string
6
- readonly url?: string
7
- readonly note?: string
8
- }
9
-
10
- export interface RefsProps {
11
- readonly items: readonly Reference[]
12
- }
13
-
14
- /**
15
- * Numbers each item from 1 so its `id` matches the `#r<n>` a cited
16
- * `Paragraph` links to. The caller has already checked every `url` parses as
17
- * http or https, since attribute escaping does not stop a `javascript:` href.
18
- */
19
- export function Refs({ items }: RefsProps): Element {
20
- const rendered = items.map(({ title, url, note }, index) => (
21
- <li id={`r${index + 1}`}>
22
- <cite>{title}</cite>
23
- {note === undefined ? null : `. ${note}`}
24
- {url === undefined ? null : [' ', <a href={url}>{new URL(url).host}</a>]}
25
- </li>
26
- ))
27
- return <ol class="refs">{rendered}</ol>
28
- }
@@ -1,78 +0,0 @@
1
- export interface LessonReferencesLinked {
2
- readonly html: string
3
- /** Four-digit numbers naming no lesson, each once, in first-seen order. */
4
- readonly unresolved: readonly string[]
5
- }
6
-
7
- /**
8
- * A mention a learner would read as pointing at another lesson. The whitespace
9
- * is captured rather than normalized, since a prettier-wrapped lesson can break
10
- * a mention across a line.
11
- */
12
- const MENTION = /\b(lesson|Lesson)(\s+)(\d{4})\b/g
13
-
14
- /**
15
- * A link nav wrote on an earlier run, and the only kind it may rewrite. The
16
- * close tag allows whitespace before its `>`, which is how prettier wraps it.
17
- */
18
- const OWNED_LINK = /<a\s[^>]*\bdata-lesson-ref\b[^>]*>([\s\S]*?)<\/a\s*>/g
19
-
20
- /**
21
- * A comment, a raw-text element taken whole so a `<` inside a script never
22
- * reads as a tag, any other tag, or the text between tags.
23
- */
24
- const TOKEN =
25
- /<!--[\s\S]*?-->|<(script|style)\b[\s\S]*?<\/\1\s*>|<\/?[a-zA-Z][^>]*>|[^<]+|</g
26
-
27
- const TAG_NAME = /^<(\/?)([a-zA-Z][a-zA-Z0-9-]*)/
28
-
29
- /**
30
- * Elements whose text stays plain. A link inside a quiz `label` navigates on
31
- * the click meant to pick an option, and one inside `summary` fights the
32
- * toggle.
33
- */
34
- const SKIPPED = new Set(['a', 'code', 'pre', 'label', 'summary'])
35
-
36
- /**
37
- * Links every "lesson NNNN" mention in a lesson's authored region to the
38
- * sibling file carrying that number, and reports the numbers naming no lesson.
39
- * A link carrying `data-lesson-ref` is nav's own, so it is unwrapped and
40
- * resolved again from its text, which re-points it after a slug rename and
41
- * drops it once its target is gone. An author's own `<a>` is never touched.
42
- */
43
- export function linkLessonReferences(
44
- html: string,
45
- targets: ReadonlyMap<string, string>,
46
- current: string,
47
- ): LessonReferencesLinked {
48
- const unresolved: string[] = []
49
- let depth = 0
50
-
51
- const linkText = (text: string): string =>
52
- text.replace(MENTION, (mention, word, space, number: string) => {
53
- const target = targets.get(number)
54
- if (target === current) return mention
55
- if (target === undefined) {
56
- if (!unresolved.includes(number)) unresolved.push(number)
57
- return mention
58
- }
59
- return `<a href="${target}" data-lesson-ref>${word}${space}${number}</a>`
60
- })
61
-
62
- const unwrapped = html.replace(OWNED_LINK, (_link, text: string) => text)
63
-
64
- const rewritten = unwrapped.replace(TOKEN, (token) => {
65
- if (!token.startsWith('<') || token === '<') {
66
- return depth === 0 ? linkText(token) : token
67
- }
68
-
69
- const tag = TAG_NAME.exec(token)
70
- if (tag === null || !SKIPPED.has(tag[2].toLowerCase())) return token
71
-
72
- if (tag[1] === '/') depth = Math.max(0, depth - 1)
73
- else if (!token.endsWith('/>')) depth += 1
74
- return token
75
- })
76
-
77
- return { html: rewritten, unresolved }
78
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * The faces a teach page embeds: the design module's own `FONT_FACES` for
3
- * everything a lesson reads and for code, plus the hand-drawn pair teach
4
- * needs for its figures, both from `@/design/fonts`.
5
- */
6
-
7
- import {
8
- FONT_FACES,
9
- HAND_DRAWN_FONT_FACES,
10
- type FontFace,
11
- } from '@/design/fonts'
12
-
13
- export const TEACH_FONT_FACES: readonly FontFace[] = [
14
- ...FONT_FACES,
15
- ...HAND_DRAWN_FONT_FACES,
16
- ]
@@ -1,71 +0,0 @@
1
- import hljs from 'highlight.js/lib/core'
2
- import bash from 'highlight.js/lib/languages/bash'
3
- import css from 'highlight.js/lib/languages/css'
4
- import diff from 'highlight.js/lib/languages/diff'
5
- import dockerfile from 'highlight.js/lib/languages/dockerfile'
6
- import go from 'highlight.js/lib/languages/go'
7
- import javascript from 'highlight.js/lib/languages/javascript'
8
- import json from 'highlight.js/lib/languages/json'
9
- import markdown from 'highlight.js/lib/languages/markdown'
10
- import python from 'highlight.js/lib/languages/python'
11
- import shell from 'highlight.js/lib/languages/shell'
12
- import sql from 'highlight.js/lib/languages/sql'
13
- import typescript from 'highlight.js/lib/languages/typescript'
14
- import xml from 'highlight.js/lib/languages/xml'
15
- import yaml from 'highlight.js/lib/languages/yaml'
16
-
17
- /**
18
- * A fixed set rather than all of highlight.js, since each grammar costs load
19
- * time and a workspace rarely reaches past these. An unlisted language renders
20
- * plain rather than refusing, so adding one here is the whole change.
21
- */
22
- const LANGUAGES = {
23
- bash,
24
- css,
25
- diff,
26
- dockerfile,
27
- go,
28
- javascript,
29
- json,
30
- markdown,
31
- python,
32
- shell,
33
- sql,
34
- typescript,
35
- xml,
36
- yaml,
37
- }
38
-
39
- for (const [name, grammar] of Object.entries(LANGUAGES)) {
40
- hljs.registerLanguage(name, grammar)
41
- }
42
-
43
- export interface Highlighted {
44
- /** Markup safe to embed inside `<code>`: every source character escaped. */
45
- readonly html: string
46
- /** The language as asked for, or `undefined` when it rendered plain. */
47
- readonly language: string | undefined
48
- }
49
-
50
- function escapeCode(text: string): string {
51
- return text
52
- .replaceAll('&', '&amp;')
53
- .replaceAll('<', '&lt;')
54
- .replaceAll('>', '&gt;')
55
- }
56
-
57
- /**
58
- * Highlights code at build time into `hljs-*` class spans the teach
59
- * stylesheet colors, so an emitted page needs no script. An absent or
60
- * unregistered language falls back to escaped plain text rather than
61
- * throwing or guessing.
62
- */
63
- export function highlight(text: string, lang?: string): Highlighted {
64
- if (lang === undefined || hljs.getLanguage(lang) === undefined) {
65
- return { html: escapeCode(text), language: undefined }
66
- }
67
- return {
68
- html: hljs.highlight(text, { language: lang, ignoreIllegals: true }).value,
69
- language: lang,
70
- }
71
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Bun resolves this module rather than `jsx-runtime` at run time (spike 1,
3
- * `.canon/groundwork/56-teach-render-layer/08-spikes.md`), so every export a
4
- * caller might reach through either entry point has to exist here too.
5
- */
6
- export {
7
- Fragment,
8
- jsx,
9
- jsx as jsxDEV,
10
- jsxs,
11
- render,
12
- type JSX,
13
- } from './jsx-runtime'
@@ -1,113 +0,0 @@
1
- import { escape, isRaw, raw, type RawMarkup } from './raw'
2
-
3
- export type Child =
4
- | string
5
- | number
6
- | boolean
7
- | null
8
- | undefined
9
- | RawMarkup
10
- | readonly Child[]
11
-
12
- export interface Props {
13
- readonly children?: Child
14
- readonly [key: string]: unknown
15
- }
16
-
17
- /**
18
- * What a component returns and what `jsx` produces: markup already rendered,
19
- * carrying the same brand `raw` gives an author's own escape hatch. One brand
20
- * for both is what lets a parent's `{children}` compose a child's output
21
- * without escaping it a second time, since a plain string cannot say whether
22
- * it is author text or already-rendered markup.
23
- */
24
- export type Element = RawMarkup
25
-
26
- export type Component<P extends Props = Props> = (props: P) => Element
27
-
28
- export const Fragment = Symbol('Fragment')
29
-
30
- const ARIA_PREFIX = 'aria-'
31
-
32
- const VOID_ELEMENTS = new Set([
33
- 'area',
34
- 'base',
35
- 'br',
36
- 'col',
37
- 'embed',
38
- 'hr',
39
- 'img',
40
- 'input',
41
- 'link',
42
- 'meta',
43
- 'source',
44
- 'track',
45
- 'wbr',
46
- ])
47
-
48
- function renderChild(child: Child): string {
49
- if (child === null || child === undefined || typeof child === 'boolean') {
50
- return ''
51
- }
52
- if (typeof child === 'number') return String(child)
53
- if (typeof child === 'string') return escape(child)
54
- if (isRaw(child)) return child.html
55
- return child.map(renderChild).join('')
56
- }
57
-
58
- /**
59
- * ARIA takes the literal word for a boolean, where HTML boolean attributes
60
- * take presence or absence. Spike 1's first draft rendered a bare
61
- * `aria-disabled` for `true` and dropped `aria-disabled={false}` entirely,
62
- * silently removing state a screen reader reads.
63
- */
64
- function renderAttribute(key: string, value: unknown): string {
65
- if (key.startsWith(ARIA_PREFIX) && typeof value === 'boolean') {
66
- return ` ${key}="${value}"`
67
- }
68
- if (value === undefined || value === null || value === false) return ''
69
- if (value === true) return ` ${key}`
70
- if (isRaw(value)) return ` ${key}="${escape(value.html)}"`
71
- return ` ${key}="${escape(String(value))}"`
72
- }
73
-
74
- function renderAttributes(props: Props): string {
75
- return Object.entries(props)
76
- .filter(([key]) => key !== 'children')
77
- .map(([key, value]) => renderAttribute(key, value))
78
- .join('')
79
- }
80
-
81
- export function jsx(
82
- type: string | Component | typeof Fragment,
83
- props: Props,
84
- ): Element {
85
- if (type === Fragment) return raw(renderChild(props.children ?? null))
86
- if (typeof type === 'function') return type(props)
87
-
88
- const attrs = renderAttributes(props)
89
- if (VOID_ELEMENTS.has(type)) return raw(`<${type}${attrs}>`)
90
-
91
- return raw(
92
- `<${type}${attrs}>${renderChild(props.children ?? null)}</${type}>`,
93
- )
94
- }
95
-
96
- export const jsxs = jsx
97
-
98
- /** Unwraps a rendered element to the plain string a file writes to disk. */
99
- export function render(element: Element): string {
100
- return element.html
101
- }
102
-
103
- export namespace JSX {
104
- export type Element = RawMarkup
105
-
106
- export interface ElementChildrenAttribute {
107
- children: Record<string, never>
108
- }
109
-
110
- export interface IntrinsicElements {
111
- [name: string]: Props
112
- }
113
- }
@@ -1,28 +0,0 @@
1
- const RAW = Symbol('raw')
2
-
3
- /**
4
- * Markup already rendered, exempt from the escaping every other string
5
- * receives. `raw` is the only way to produce one, so a lesson has to opt in
6
- * explicitly rather than an author text string accidentally passing through
7
- * unescaped.
8
- */
9
- export interface RawMarkup {
10
- readonly [RAW]: true
11
- readonly html: string
12
- }
13
-
14
- export function raw(html: string): RawMarkup {
15
- return { [RAW]: true, html }
16
- }
17
-
18
- export function isRaw(value: unknown): value is RawMarkup {
19
- return typeof value === 'object' && value !== null && RAW in value
20
- }
21
-
22
- export function escape(value: string): string {
23
- return value
24
- .replace(/&/g, '&amp;')
25
- .replace(/</g, '&lt;')
26
- .replace(/>/g, '&gt;')
27
- .replace(/"/g, '&quot;')
28
- }