explico 0.3.0 → 0.5.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 (151) hide show
  1. package/CHANGELOG.md +344 -1
  2. package/README.md +117 -3
  3. package/dist/compiler/astro.d.ts +38 -0
  4. package/dist/compiler/astro.js +258 -0
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +32 -12
  7. package/dist/compiler/collections.d.ts +3 -2
  8. package/dist/compiler/collections.js +28 -1
  9. package/dist/compiler/course-config.d.ts +86 -4
  10. package/dist/compiler/course-config.js +39 -0
  11. package/dist/compiler/exports/standalone-html.d.ts +34 -0
  12. package/dist/compiler/exports/standalone-html.js +803 -0
  13. package/dist/compiler/lesson-derivation.d.ts +1 -1
  14. package/dist/compiler/lesson-derivation.js +2 -2
  15. package/dist/compiler/manifest.d.ts +112 -7
  16. package/dist/compiler/manifest.js +205 -40
  17. package/dist/compiler/resolution-report.d.ts +4 -0
  18. package/dist/reference/base-abbreviations.d.mts +30 -0
  19. package/dist/reference/base-abbreviations.mjs +60 -0
  20. package/dist/reference/base-operators.d.mts +15 -0
  21. package/dist/reference/base-operators.mjs +40 -0
  22. package/dist/reference/base-roman-labels.d.mts +35 -0
  23. package/dist/reference/base-roman-labels.mjs +43 -0
  24. package/dist/reference/consistency.d.ts +8 -2
  25. package/dist/reference/consistency.js +50 -3
  26. package/dist/reference/diagrams-validate.d.ts +21 -0
  27. package/dist/reference/diagrams-validate.js +13 -0
  28. package/dist/reference/equations-validate.d.ts +8 -3
  29. package/dist/reference/equations-validate.js +1 -1
  30. package/dist/reference/equations.d.mts +66 -35
  31. package/dist/reference/equations.mjs +142 -40
  32. package/dist/reference/gate-abbreviations.d.ts +17 -0
  33. package/dist/reference/gate-abbreviations.js +64 -0
  34. package/dist/reference/gate-math.d.ts +28 -5
  35. package/dist/reference/gate-math.js +5 -2
  36. package/dist/reference/gate-prose-notation.d.ts +60 -0
  37. package/dist/reference/gate-prose-notation.js +125 -0
  38. package/dist/reference/keyed-float-validate.d.ts +14 -8
  39. package/dist/reference/keyed-float-validate.js +1 -1
  40. package/dist/reference/keyed-jsx-floats.d.mts +24 -21
  41. package/dist/reference/keyed-jsx-floats.mjs +90 -41
  42. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  43. package/dist/reference/knowledge-checks-validate.js +78 -0
  44. package/dist/reference/knowledge-checks.d.mts +25 -0
  45. package/dist/reference/knowledge-checks.mjs +45 -0
  46. package/dist/reference/lab-math-scope.d.ts +9 -1
  47. package/dist/reference/lab-math-scope.js +32 -4
  48. package/dist/reference/math-glyphs.d.mts +30 -0
  49. package/dist/reference/math-glyphs.mjs +367 -0
  50. package/dist/reference/mdx-attributes.d.mts +13 -0
  51. package/dist/reference/mdx-attributes.mjs +38 -0
  52. package/dist/reference/mixed-content.d.ts +17 -0
  53. package/dist/reference/mixed-content.js +29 -0
  54. package/dist/reference/page-notation.d.ts +52 -0
  55. package/dist/reference/page-notation.js +171 -0
  56. package/dist/reference/raw-floats-validate.d.ts +18 -0
  57. package/dist/reference/raw-floats-validate.js +53 -0
  58. package/dist/reference/raw-floats.d.mts +18 -0
  59. package/dist/reference/raw-floats.mjs +129 -0
  60. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  61. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  62. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  63. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  64. package/dist/reference/remark-abbreviations.d.mts +7 -0
  65. package/dist/reference/remark-abbreviations.mjs +174 -0
  66. package/dist/reference/remark-memo.d.mts +16 -0
  67. package/dist/reference/remark-memo.mjs +339 -0
  68. package/dist/reference/remark-notation.d.mts +102 -5
  69. package/dist/reference/remark-notation.mjs +506 -50
  70. package/dist/reference/remark-short-note.d.mts +4 -0
  71. package/dist/reference/remark-short-note.mjs +237 -0
  72. package/dist/reference/types.d.ts +16 -1
  73. package/dist/reference/units-vocab.js +4 -0
  74. package/docs/VERSIONING.md +33 -4
  75. package/docs/authoring/README.md +12 -0
  76. package/docs/authoring/agents.md +17 -2
  77. package/docs/authoring/components.md +135 -0
  78. package/docs/authoring/migrating.md +154 -7
  79. package/docs/authoring/notation.md +568 -8
  80. package/docs/authoring/structure.md +251 -0
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +332 -0
  84. package/src/compiler/cli.ts +33 -10
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +131 -3
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/lesson-derivation.ts +2 -2
  89. package/src/compiler/manifest.ts +367 -49
  90. package/src/components/HoverPanel.astro +45 -4
  91. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  92. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  93. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  94. package/src/components/assessments/AssessmentSet.astro +98 -14
  95. package/src/components/assessments/RenderedContent.tsx +55 -0
  96. package/src/components/assessments/assessment-randomization.ts +16 -0
  97. package/src/components/diagram/Diagram.astro +444 -83
  98. package/src/components/disclosure/hover-preview.ts +202 -0
  99. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  100. package/src/components/examples/CompactExamples.astro +2 -0
  101. package/src/components/figure/Figure.astro +279 -10
  102. package/src/components/memo/Memo.astro +453 -0
  103. package/src/components/memo/ShortNote.astro +223 -0
  104. package/src/components/notation/NotationFormula.astro +18 -1
  105. package/src/components/notation/NotationLayer.astro +116 -8
  106. package/src/components/notation/render-notation-math.ts +39 -2
  107. package/src/components/notation/safe-math-to-react.ts +50 -0
  108. package/src/components/reference/FloatRefLayer.astro +233 -15
  109. package/src/components/starlight/CollapsibleSections.astro +461 -0
  110. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  111. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  112. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  113. package/src/components/starlight/LayoutHeader.astro +50 -0
  114. package/src/components/starlight/LayoutSidebar.astro +62 -1
  115. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  116. package/src/components/starlight/LessonExportActions.astro +436 -0
  117. package/src/components/starlight/LessonFooter.astro +38 -2
  118. package/src/components/starlight/section-parts.ts +31 -0
  119. package/src/components/table/Table.astro +296 -9
  120. package/src/components/table/TableOverflowExpander.astro +489 -0
  121. package/src/content-config.ts +20 -0
  122. package/src/reference/base-abbreviations.mjs +60 -0
  123. package/src/reference/base-operators.mjs +40 -0
  124. package/src/reference/base-roman-labels.mjs +43 -0
  125. package/src/reference/consistency.ts +73 -2
  126. package/src/reference/diagrams-validate.ts +40 -0
  127. package/src/reference/equations-validate.ts +6 -4
  128. package/src/reference/equations.mjs +142 -40
  129. package/src/reference/gate-abbreviations.ts +128 -0
  130. package/src/reference/gate-math.ts +27 -7
  131. package/src/reference/gate-prose-notation.ts +224 -0
  132. package/src/reference/keyed-float-validate.ts +12 -9
  133. package/src/reference/keyed-jsx-floats.mjs +90 -41
  134. package/src/reference/knowledge-checks-validate.ts +130 -0
  135. package/src/reference/knowledge-checks.mjs +45 -0
  136. package/src/reference/lab-math-scope.ts +45 -3
  137. package/src/reference/math-glyphs.mjs +367 -0
  138. package/src/reference/mdx-attributes.mjs +38 -0
  139. package/src/reference/mixed-content.ts +69 -0
  140. package/src/reference/page-notation.ts +252 -0
  141. package/src/reference/raw-floats-validate.ts +103 -0
  142. package/src/reference/raw-floats.mjs +129 -0
  143. package/src/reference/rehype-heading-anchors.mjs +107 -1
  144. package/src/reference/rehype-table-header-labels.mjs +152 -0
  145. package/src/reference/remark-abbreviations.mjs +174 -0
  146. package/src/reference/remark-memo.mjs +339 -0
  147. package/src/reference/remark-notation.mjs +506 -50
  148. package/src/reference/remark-short-note.mjs +237 -0
  149. package/src/reference/types.ts +16 -0
  150. package/src/reference/units-vocab.ts +4 -0
  151. package/src/styles/global.css +1125 -50
@@ -15,8 +15,15 @@
15
15
  */
16
16
  import NotationMath from './NotationMath.astro';
17
17
  import SafeMathNode from './SafeMathNode.astro';
18
- import { renderNotationFormula } from './render-notation-math';
18
+ import {
19
+ renderNotationFormula,
20
+ stripNotationMarkers,
21
+ } from './render-notation-math';
19
22
  import type { GlyphScopeEntry, ResolvedGloss } from '../../reference/gloss';
23
+ import {
24
+ notationKeysInMathNodes,
25
+ registerPageNotationDependencies,
26
+ } from '../../reference/page-notation';
20
27
 
21
28
  export interface Props {
22
29
  formula: string;
@@ -36,12 +43,18 @@ const {
36
43
  entryLabel,
37
44
  } = Astro.props;
38
45
  const { nodes } = renderNotationFormula(formula, scope);
46
+ registerPageNotationDependencies(
47
+ Astro.locals,
48
+ notationKeysInMathNodes(nodes),
49
+ `NotationFormula formula "${formula}"`,
50
+ );
39
51
  ---
40
52
 
41
53
  <div class="notation-formula-block">
42
54
  <div
43
55
  class="notation-formula-math"
44
56
  data-notation-formula
57
+ data-latex={stripNotationMarkers(formula)}
45
58
  role="group"
46
59
  aria-label={entryLabel ? `${entryLabel} formula` : 'Formula'}
47
60
  tabindex="0"
@@ -88,6 +101,10 @@ const { nodes } = renderNotationFormula(formula, scope);
88
101
  container is the fix; this is its focus ring. */
89
102
  .notation-formula-math {
90
103
  overflow-x: auto;
104
+ /* `overflow-x: auto` alone makes the block axis compute to `auto` too
105
+ (CSS Overflow §3) — equations must never scroll vertically on their
106
+ own, only sideways. */
107
+ overflow-y: hidden;
91
108
  padding-block: 0.15rem;
92
109
  color: var(--sl-color-text);
93
110
  }
@@ -46,6 +46,7 @@ export interface Props {
46
46
 
47
47
  import NotationMath from './NotationMath.astro';
48
48
  import NotationFormula from './NotationFormula.astro';
49
+ import { stripNotationMarkers } from './render-notation-math';
49
50
  import type { GlyphScopeEntry, ResolvedGloss } from '../../reference/gloss';
50
51
 
51
52
  const {
@@ -169,7 +170,11 @@ for (const gloss of glossItems) {
169
170
  entryLabel={definition.label}
170
171
  />
171
172
  ) : (
172
- <span class="notation-formula" data-notation-formula>
173
+ <span
174
+ class="notation-formula"
175
+ data-notation-formula
176
+ data-latex={stripNotationMarkers(definition.formula)}
177
+ >
173
178
  <NotationMath latex={definition.formula} />
174
179
  </span>
175
180
  ))}
@@ -244,6 +249,14 @@ for (const gloss of glossItems) {
244
249
  </header>
245
250
  <p data-slot-summary hidden />
246
251
  <div data-slot-formula hidden />
252
+ <button
253
+ class="notation-popup-copy"
254
+ type="button"
255
+ data-popup-copy
256
+ hidden
257
+ >
258
+ Copy LaTeX
259
+ </button>
247
260
  <p class="notation-units" data-slot-units hidden />
248
261
  <a data-slot-link hidden>
249
262
  Open the full definition
@@ -369,7 +382,18 @@ for (const gloss of glossItems) {
369
382
  const units = fragment.querySelector<HTMLElement>('[data-slot-units]');
370
383
  const link =
371
384
  fragment.querySelector<HTMLAnchorElement>('[data-slot-link]');
372
- if (!el || !symbolHost || !title || !summary || !formulaHost || !units) {
385
+ const copyButton = fragment.querySelector<HTMLButtonElement>(
386
+ '[data-popup-copy]',
387
+ );
388
+ if (
389
+ !el ||
390
+ !symbolHost ||
391
+ !title ||
392
+ !summary ||
393
+ !formulaHost ||
394
+ !units ||
395
+ !copyButton
396
+ ) {
373
397
  return null;
374
398
  }
375
399
 
@@ -395,6 +419,35 @@ for (const gloss of glossItems) {
395
419
  ...(sourceFormula ? [sourceFormula.cloneNode(true)] : []),
396
420
  );
397
421
  formulaHost.hidden = sourceFormula === null;
422
+
423
+ // The formula's own `data-latex` carries the plain, authored LaTeX —
424
+ // not the `\explain{key}{…}`-wrapped source KaTeX actually rendered —
425
+ // so what a reader copies is what they'd write themselves, not an
426
+ // internal marker macro. See `render-notation-math.ts:renderNotationFormula`.
427
+ const formulaLatex = sourceFormula?.dataset.latex ?? '';
428
+ copyButton.hidden = formulaLatex.length === 0;
429
+ if (formulaLatex.length > 0) {
430
+ copyButton.addEventListener('click', (event) => {
431
+ // Otherwise this bubbles to the document click handler in
432
+ // `popup-stack.ts`, which treats any click inside a popup that
433
+ // isn't a trigger or a link as "close it" — that would remove the
434
+ // card before the "Copied!" flash below ever paints.
435
+ event.stopPropagation();
436
+ if (!navigator.clipboard) return;
437
+ navigator.clipboard.writeText(formulaLatex).then(
438
+ () => {
439
+ copyButton.dataset.copied = 'true';
440
+ window.setTimeout(() => {
441
+ delete copyButton.dataset.copied;
442
+ }, 1200);
443
+ },
444
+ () => {
445
+ /* Clipboard blocked: nothing more we can do here. */
446
+ },
447
+ );
448
+ });
449
+ }
450
+
398
451
  setOptionalText(
399
452
  units,
400
453
  entry
@@ -573,6 +626,10 @@ for (const gloss of glossItems) {
573
626
 
574
627
  .notation-formula {
575
628
  overflow-x: auto;
629
+ /* `overflow-x: auto` alone makes the block axis compute to `auto` too
630
+ (CSS Overflow §3) — equations must never scroll vertically on their
631
+ own, only sideways. */
632
+ overflow-y: hidden;
576
633
  padding-block: 0.15rem;
577
634
  color: var(--sl-color-text);
578
635
  }
@@ -612,15 +669,19 @@ for (const gloss of glossItems) {
612
669
  margin: 0;
613
670
  width: min(23rem, calc(100vw - 1.5rem));
614
671
  overflow: auto;
615
- padding: 1rem;
616
- padding-inline-end: 2.4rem;
672
+ padding-block: 0.75rem;
673
+ padding-inline: 1rem 2.4rem;
617
674
  border: 1px solid var(--sl-color-gray-5);
618
675
  border-top: 0.25rem solid var(--sl-color-accent);
619
676
  border-radius: 0.65rem;
620
677
  color: var(--sl-color-text);
621
678
  background: var(--sl-color-bg);
622
679
  box-shadow: var(--sl-shadow-lg);
623
- font-size: var(--sl-text-sm);
680
+ /* Relative to the surrounding prose's own font-size, not a fixed
681
+ Starlight step — the card is mounted on `<body>`, alongside the
682
+ article rather than nested inside it, so `%` still resolves against
683
+ the same base the article's paragraphs use. */
684
+ font-size: 90%;
624
685
  }
625
686
 
626
687
  .notation-popup-close {
@@ -677,15 +738,51 @@ for (const gloss of glossItems) {
677
738
  }
678
739
 
679
740
  .notation-panel-body p {
680
- margin-block: 0.65rem;
741
+ margin-block: 0.5rem;
681
742
  color: var(--sl-color-gray-2);
682
- font-size: var(--sl-text-sm);
683
743
  line-height: 1.5;
684
744
  }
685
745
 
746
+ .notation-popup-copy {
747
+ position: relative;
748
+ margin-block: 0.4rem 0.6rem;
749
+ padding: 0.2rem 0.55rem;
750
+ border: 1px solid var(--sl-color-gray-5);
751
+ border-radius: 0.35rem;
752
+ color: var(--sl-color-text-accent);
753
+ background: var(--sl-color-bg);
754
+ font: inherit;
755
+ font-size: var(--sl-text-xs);
756
+ cursor: pointer;
757
+ }
758
+
759
+ .notation-popup-copy:hover {
760
+ border-color: var(--sl-color-accent);
761
+ background: var(--sl-color-accent-low);
762
+ }
763
+
764
+ .notation-popup-copy[data-copied]::after {
765
+ content: 'Copied!';
766
+ position: absolute;
767
+ inset-block-end: calc(100% + 0.35rem);
768
+ inset-inline-start: 50%;
769
+ translate: -50% 0;
770
+ padding: 0.15rem 0.45rem;
771
+ border-radius: 0.35rem;
772
+ background: var(--sl-color-accent-low);
773
+ color: var(--sl-color-text-accent);
774
+ font-size: var(--sl-text-xs);
775
+ white-space: nowrap;
776
+ pointer-events: none;
777
+ }
778
+
686
779
  .notation-panel-body [data-slot-formula] {
687
780
  overflow-x: auto;
688
- margin-block: 0.65rem;
781
+ /* See `.notation-formula` above: pairs with `overflow-x` to stop the
782
+ auto-both-axes quirk from giving the formula its own vertical
783
+ scrollbar. */
784
+ overflow-y: hidden;
785
+ margin-block: 0.5rem;
689
786
  color: var(--sl-color-text);
690
787
  }
691
788
 
@@ -711,6 +808,17 @@ for (const gloss of glossItems) {
711
808
  pointer-events: none;
712
809
  }
713
810
 
811
+ /* KaTeX draws a radical sign, an extensible arrow (`\xrightarrow`), and
812
+ other stretchy glyphs as an absolutely-positioned SVG layer that is
813
+ painted after — and on top of — the content it decorates. Without this,
814
+ a pointer over a symbol inside `\sqrt{…}` or under an arrow hits that
815
+ overlay instead of the notation marker beneath it. The radicand lives in
816
+ the sibling `.svg-align` layer, which stays interactive. */
817
+ :global(.katex-html .sqrt .vlist > span:not(.svg-align)),
818
+ :global(.katex-html .hide-tail) {
819
+ pointer-events: none;
820
+ }
821
+
714
822
  :global(.katex-html [data-notation-key]) {
715
823
  cursor: help;
716
824
  }
@@ -1,7 +1,11 @@
1
1
  import rehypeKatex from 'rehype-katex';
2
2
  import { createNotationKatexOptions } from '../../reference/katex-options.mjs';
3
3
  import { NOTATION_KEY_PATTERN } from '../../reference/remark-notation.mjs';
4
- import { resolveMathGlyphs } from '../../reference/math-glyphs.mjs';
4
+ import {
5
+ GlyphResolutionError,
6
+ resolveMathGlyphs,
7
+ stripNotationMarkers,
8
+ } from '../../reference/math-glyphs.mjs';
5
9
  import type { GlyphScopeEntry } from '../../reference/gloss';
6
10
 
7
11
  interface HastText {
@@ -32,6 +36,19 @@ export type SafeMathNode =
32
36
  children: readonly SafeMathNode[];
33
37
  };
34
38
 
39
+ /**
40
+ * A string that mixes prose and math, already split and rendered — text runs
41
+ * verbatim, a math run as the same safe, structured tree `SafeMathNode` is.
42
+ * Produced by `AssessmentSet.astro` from `manifest.assessmentContent`
43
+ * (`ContentSegment[]`, unrendered LaTeX) via {@link renderNotationMath}, and
44
+ * consumed by `AssessmentRunner`'s React-side recursive renderer
45
+ * (`RenderedContent.tsx`) — the React equivalent of `SafeMathNode.astro`,
46
+ * since a React island can't render an `.astro` component.
47
+ */
48
+ export type MixedContentNode =
49
+ | { type: 'text'; value: string }
50
+ | { type: 'math'; nodes: readonly SafeMathNode[] };
51
+
35
52
  const allowedTags = new Set([
36
53
  'annotation',
37
54
  'math',
@@ -203,6 +220,14 @@ function sanitizeNode(node: HastNode): SafeMathNode {
203
220
  });
204
221
  }
205
222
 
223
+ // Re-exported so existing imports of `stripNotationMarkers` from this module
224
+ // (the notation popup card's "Copy LaTeX" button) keep working. The
225
+ // implementation lives in `math-glyphs.mjs` because `remark-notation.mjs`
226
+ // (a numbered display equation's own copy button, at build time) needs it
227
+ // too, and importing it from here would make that a circular dependency —
228
+ // this module already imports from `remark-notation.mjs`.
229
+ export { stripNotationMarkers };
230
+
206
231
  /**
207
232
  * Render repository-authored LaTeX through the same build-time KaTeX adapter
208
233
  * as lesson math, then expose a constrained structured tree to Astro. No HTML
@@ -274,7 +299,19 @@ export function renderNotationFormula(
274
299
  const result = resolveMathGlyphs(formula, [...scope]);
275
300
  bound = result.latex;
276
301
  unresolved = result.unresolved.map(({ token }: { token: string }) => token);
277
- } catch {
302
+ } catch (error) {
303
+ // N13 (a nonlinear function with no parenthesized argument) is a hard,
304
+ // no-exceptions rule like N9 — never degrade it to inert math, so both
305
+ // `Diagram` (which turns `degraded` into a build error anyway, but with
306
+ // a misleading "notation scope" message) and `NotationFormula` (which
307
+ // otherwise keeps a degraded formula's inert-but-correct fallback) fail
308
+ // loudly with the actual fix instead.
309
+ if (
310
+ error instanceof GlyphResolutionError &&
311
+ error.code === 'unparenthesized-function'
312
+ ) {
313
+ throw error;
314
+ }
278
315
  bound = formula;
279
316
  degraded = true;
280
317
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Adapt a `SafeMathNode.properties` object — HTML attribute names and a raw
3
+ * CSS-text `style` string, exactly what `SafeMathNode.astro` spreads onto a
4
+ * dynamic element — into React's prop shape, for the React-side renderer
5
+ * (`RenderedContent.tsx`) `SafeMathNode.astro` doesn't need: React requires
6
+ * `className` (not `class`) and an object for `style` (not a CSS-text
7
+ * string) — "The `style` prop expects a mapping from style properties to
8
+ * values, not a string" is a hard React error, not a warning, so this isn't
9
+ * optional. Split into its own `.ts` module (rather than living inline in
10
+ * the `.tsx` component) so the parsing logic is unit-testable without a DOM.
11
+ */
12
+
13
+ /** kebab-case CSS property -> camelCase, e.g. `margin-right` -> `marginRight`. */
14
+ export function camelCaseCssProperty(name: string): string {
15
+ return name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
16
+ }
17
+
18
+ /**
19
+ * Parse a CSS-text `style` string into a React style object. `safeStyle`'s
20
+ * own grammar in `render-notation-math.ts`
21
+ * (`/^(?:[a-z-]+:[ A-Za-z0-9+.,()\-/%]*;)*$/`) guarantees every declaration
22
+ * is `key: value;` with no nested colons/semicolons — a straight split is
23
+ * exact for that input; this is not a general CSS parser.
24
+ */
25
+ export function parseStyle(style: string): Record<string, string> {
26
+ const result: Record<string, string> = {};
27
+ for (const declaration of style.split(';')) {
28
+ if (declaration.trim() === '') continue;
29
+ const [property, value] = declaration.split(':');
30
+ if (property === undefined || value === undefined) continue;
31
+ result[camelCaseCssProperty(property.trim())] = value.trim();
32
+ }
33
+ return result;
34
+ }
35
+
36
+ /**
37
+ * `class` -> `className`, a `style` string -> a style object, everything
38
+ * else (`aria-hidden`, `data-notation-key`, the MathML attribute names)
39
+ * passed through unchanged — they're spelled the same in HTML and React.
40
+ */
41
+ export function toReactProps(
42
+ properties: Readonly<Record<string, string>>,
43
+ ): Record<string, unknown> {
44
+ const { class: className, style, ...rest } = properties;
45
+ return {
46
+ ...rest,
47
+ ...(className !== undefined ? { className } : {}),
48
+ ...(style !== undefined ? { style: parseStyle(style) } : {}),
49
+ };
50
+ }