wiki-formant 0.82.0 → 0.83.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/README.md CHANGED
@@ -693,12 +693,13 @@ export const X = xAccount({ siteName: 'AcuiQ', handle: '@acu_iq', posts: "a name
693
693
  addCopyButtons(el); // every <pre> gets one, once
694
694
  sortTables(el); // every column-headed table sorts by its headers
695
695
  hydrateTweetEmbeds(el); // placeholders get a live src
696
+ await renderMath(el); // $$…$$, \\[…\\] and \\(…\\) typeset by KaTeX, loaded only when there is maths
696
697
  const off = onTweetResize((h, source) => sizeTweetEmbed(el, h, source));
697
698
  ```
698
699
 
699
700
  `addCopyButton` was **byte-identical** in two BlockRenderers, down to the SVG path data. Its idempotence guard now lives inside the function rather than in a `pre:not(:has(…))` at the call site, where it can be — and was — retyped.
700
701
 
701
- In React, `useArticlePasses(ref, [blocks])` (`wiki-formant/react`) runs them all, the embed pair included. Three renderers had the effect written out and disagreed on its dependencies: two ran once on mount, so a page swapped in without a remount kept its old tables unsortable.
702
+ In React, `useArticlePasses(ref, [blocks])` (`wiki-formant/react`) runs them all, the embed pair and the maths included. KaTeX is an optional peer: an app installs `katex`, and a page with no delimiter never fetches it. There is no single-`$` delimiter, since prose writes $XRD. Three renderers had the effect written out and disagreed on its dependencies: two ran once on mount, so a page swapped in without a remount kept its old tables unsortable.
702
703
 
703
704
  `activateTabGroups` turns stored `[data-tabs]` markup into a working tab group. The editor persists tabs as nested divs, which is the right thing to store — it survives a markdown twin, a plain HTML render and a reader with JavaScript off, all of which show every tab in order. Making one of them pressable is a reader-side job, and it sits beside the other passes rather than inside a component.
704
705
 
package/dist/base.css CHANGED
@@ -200,7 +200,6 @@ html[data-sidebar='closed'] .wiki-sidebar { width: 0; }
200
200
  .tabs-editor .tab-button { display: inline-flex; align-items: center; padding: 0.15em 0.3em; border-bottom: 0; border-radius: 0.3rem; }
201
201
  .tabs-editor .tab-button.active { background: color-mix(in srgb, currentColor 10%, transparent); }
202
202
  .tab-title { width: 7em; outline: none; }
203
- .tab-remove, .tab-add { opacity: 0.7; }
204
203
  .tabs-content { padding: 1rem; }
205
204
  .tabs-editor [data-tab-item] { display: none; }
206
205
  [data-tabs][data-active-tab='0'] .tabs-editor [data-tab-item]:nth-of-type(1),
@@ -212,6 +211,9 @@ html[data-sidebar='closed'] .wiki-sidebar { width: 0; }
212
211
  [data-tabs][data-active-tab='6'] .tabs-editor [data-tab-item]:nth-of-type(7),
213
212
  [data-tabs][data-active-tab='7'] .tabs-editor [data-tab-item]:nth-of-type(8) { display: block; }
214
213
 
214
+ /* ---- maths (renderMath): a display wider than its column scrolls rather than spilling. */
215
+ .katex-display { overflow-x: auto; overflow-y: hidden; }
216
+
215
217
  /* ---- one-line fields (WikiLine) --------------------------------------------- */
216
218
  [data-wiki-line] { outline: none; cursor: text; white-space: pre-wrap; }
217
219
  [data-wiki-line]:is(:empty, :has(> .ProseMirror-trailingBreak))::before { content: attr(data-placeholder); opacity: 0.6; }
package/dist/dom.d.ts CHANGED
@@ -40,6 +40,14 @@ export declare function onTweetResize(resize: (height: number, source: MessageEv
40
40
  export declare function hydrateTweetEmbeds(root: ParentNode): void;
41
41
  /** Apply a posted height to the embed iframe under `root` that posted it, and to no other. */
42
42
  export declare function sizeTweetEmbed(root: ParentNode, height: number, source: MessageEventSource | null): void;
43
+ /**
44
+ * Typeset the maths under `root` with KaTeX: `$$…$$` and `\[…\]` as a display,
45
+ * `\(…\)` inline. KaTeX and its stylesheet load only when the text holds a
46
+ * delimiter, so a page with no maths fetches nothing. KaTeX is an optional peer
47
+ * the app installs. Typeset maths carries no delimiter, so a second pass finds
48
+ * nothing to do.
49
+ */
50
+ export declare function renderMath(root: HTMLElement): Promise<void>;
43
51
  export interface TabGroupClassNames {
44
52
  list?: string;
45
53
  panels?: string;
package/dist/dom.js CHANGED
@@ -113,6 +113,29 @@ export function sizeTweetEmbed(root, height, source) {
113
113
  iframe.style.height = `${height}px`;
114
114
  }
115
115
  }
116
+ // ---- maths ------------------------------------------------------------------
117
+ /** What marks maths in stored prose. No single `$`: prose writes $XRD, and two cashtags in one block would read as a formula. */
118
+ const MATH = /\$\$|\\\(|\\\[/;
119
+ /**
120
+ * Typeset the maths under `root` with KaTeX: `$$…$$` and `\[…\]` as a display,
121
+ * `\(…\)` inline. KaTeX and its stylesheet load only when the text holds a
122
+ * delimiter, so a page with no maths fetches nothing. KaTeX is an optional peer
123
+ * the app installs. Typeset maths carries no delimiter, so a second pass finds
124
+ * nothing to do.
125
+ */
126
+ export async function renderMath(root) {
127
+ if (!MATH.test(root.textContent ?? ''))
128
+ return;
129
+ const [{ default: render }] = await Promise.all([import('katex/contrib/auto-render'), import('katex/dist/katex.min.css')]);
130
+ render(root, {
131
+ delimiters: [
132
+ { left: '$$', right: '$$', display: true },
133
+ { left: '\\(', right: '\\)', display: false },
134
+ { left: '\\[', right: '\\]', display: true },
135
+ ],
136
+ throwOnError: false,
137
+ });
138
+ }
116
139
  /**
117
140
  * Turn stored `[data-tabs]` markup into a working tab group.
118
141
  *
package/dist/react.d.ts CHANGED
@@ -479,8 +479,8 @@ export declare function isRailLinkActive(activePath: string, href: string, tree?
479
479
  export type { WikiLinkComponent, WikiLinkProps } from './react-server.js';
480
480
  /**
481
481
  * The `wiki-formant/dom` passes over a rendered article: tab groups, copy
482
- * buttons, sortable tables, and each post on X hydrated and kept at the height
483
- * it posts. All three renderers ran the same passes in an effect, and they
482
+ * buttons, sortable tables, maths typeset, and each post on X hydrated and kept
483
+ * at the height it posts. All three renderers ran the same passes in an effect, and they
484
484
  * disagreed on when: two ran it once on mount, so a page swapped in without a
485
485
  * remount kept its old tables unsortable. Each pass is idempotent, so running
486
486
  * on every `deps` change is safe.
package/dist/react.js CHANGED
@@ -26,7 +26,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
26
26
  // state on <html> before first paint. The hook keeps that attribute in
27
27
  // sync afterwards, so CSS has one source of truth either side of hydration.
28
28
  import { railBreakpointMismatch, readRailFloating, resolveSidebarOpen, SIDEBAR_ATTRIBUTE, } from './sidebar.js';
29
- import { activateTabGroups, addCopyButtons, hydrateTweetEmbeds, onTweetResize, sizeTweetEmbed, sortTables, startBeacon } from './dom.js';
29
+ import { activateTabGroups, addCopyButtons, hydrateTweetEmbeds, onTweetResize, renderMath, sizeTweetEmbed, sortTables, startBeacon } from './dom.js';
30
30
  import { comboboxAria } from './combobox.js';
31
31
  import { Component, createContext, createElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
32
32
  const readStored = (key) => {
@@ -883,8 +883,8 @@ const decodePath = (path) => {
883
883
  // ---- rendered-article passes --------------------------------------------------
884
884
  /**
885
885
  * The `wiki-formant/dom` passes over a rendered article: tab groups, copy
886
- * buttons, sortable tables, and each post on X hydrated and kept at the height
887
- * it posts. All three renderers ran the same passes in an effect, and they
886
+ * buttons, sortable tables, maths typeset, and each post on X hydrated and kept
887
+ * at the height it posts. All three renderers ran the same passes in an effect, and they
888
888
  * disagreed on when: two ran it once on mount, so a page swapped in without a
889
889
  * remount kept its old tables unsortable. Each pass is idempotent, so running
890
890
  * on every `deps` change is safe.
@@ -897,6 +897,7 @@ export function useArticlePasses(ref, deps) {
897
897
  activateTabGroups(root);
898
898
  addCopyButtons(root);
899
899
  sortTables(root);
900
+ void renderMath(root);
900
901
  hydrateTweetEmbeds(root);
901
902
  return onTweetResize((height, source) => sizeTweetEmbed(root, height, source));
902
903
  // eslint-disable-next-line react-hooks/exhaustive-deps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.82.0",
3
+ "version": "0.83.0",
4
4
  "description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -328,12 +328,14 @@
328
328
  "devDependencies": {
329
329
  "@electric-sql/pglite": "^0.3.9",
330
330
  "@radixdlt/rola": "^2.1.0",
331
+ "@types/katex": "^0.16.8",
331
332
  "@types/node": "^22.0.0",
332
333
  "@types/react": "^19.0.0",
333
334
  "@types/sanitize-html": "^2.16.1",
334
335
  "@vercel/blob": "^2.8.2",
335
336
  "jose": "^6.2.12",
336
337
  "jsdom": "^30.1.0",
338
+ "katex": "^0.16.47",
337
339
  "react": "^19.2.4",
338
340
  "sharp": "^0.35.5",
339
341
  "typescript": "^5.9.0"
@@ -344,7 +346,8 @@
344
346
  "jose": ">=6",
345
347
  "pg": ">=8",
346
348
  "react": ">=18",
347
- "sharp": ">=0.33"
349
+ "sharp": ">=0.33",
350
+ "katex": "^0.16.0"
348
351
  },
349
352
  "peerDependenciesMeta": {
350
353
  "react": {
@@ -364,6 +367,9 @@
364
367
  },
365
368
  "@vercel/blob": {
366
369
  "optional": true
370
+ },
371
+ "katex": {
372
+ "optional": true
367
373
  }
368
374
  }
369
375
  }