wiki-formant 0.25.0 → 0.26.1
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 +3 -1
- package/dist/base.css +5 -0
- package/dist/dom.d.ts +14 -0
- package/dist/dom.js +24 -0
- package/dist/markdown.js +29 -13
- package/dist/react.d.ts +2 -1
- package/dist/react.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -437,6 +437,8 @@ In React, `useArticlePasses(ref, [blocks])` runs the three passes and `useTweetE
|
|
|
437
437
|
|
|
438
438
|
`sortTables` makes the tables stored in article HTML sortable by their headers. They arrive as a string a `dangerouslySetInnerHTML` wrote, so React never sees their rows and cannot sort them. A column is dates if every filled cell starts with one, numbers if every one does, and text otherwise — one stray value makes the whole column text, which beats sorting half of it by one rule and half by another. A third press restores the author's order, which is often chronological or ranked and otherwise needs a reload. Label/value tables and tables with merged cells are left alone. The markup it writes — `aria-sort` on the cell, a `.sort-header` button inside it — is the markup a React sortable header should write too, so both kinds of table draw their arrows from one stylesheet rule.
|
|
439
439
|
|
|
440
|
+
`scrollTables(html)` is the one table pass that runs on the string, before render: it wraps each stored table in a `.table-scroll` box, and `base.css` lets that box scroll sideways. A table box ignores `overflow`, so a table with an address or a URL in a cell is as wide as that token whatever its column, and a scroll box added by an effect would arrive after the table had already been laid out too wide. Call it on the render path beside `injectHeadingIds`. A table already in a box keeps it, so it is safe on HTML that has been through it once.
|
|
441
|
+
|
|
440
442
|
`TWITTER_ORIGIN` is written down once. It is both the embed host and the allow-list `onTweetResize` checks before believing a posted height, and it had been spelled out at four call sites across two repos. Any page can `postMessage`; only the embed host may size the embed.
|
|
441
443
|
|
|
442
444
|
## Search
|
|
@@ -542,7 +544,7 @@ they name a gap in the corpus in the reader's own words.
|
|
|
542
544
|
|
|
543
545
|
## Base stylesheet
|
|
544
546
|
|
|
545
|
-
`wiki-formant/base.css` is the layout the package's markup does not work without, and nothing else: columns that stack until there is room, stored tab panels that show one at a time, a copy button pinned to its block's corner and visible on focus and on touch, and the `aria-sort` arrow as a mask over `currentColor`. No colour and no scale, so a design system's own rules override it at equal specificity.
|
|
547
|
+
`wiki-formant/base.css` is the layout the package's markup does not work without, and nothing else: columns that stack until there is room, stored tab panels that show one at a time, a copy button pinned to its block's corner and visible on focus and on touch, a stored table's scroll box, and the `aria-sort` arrow as a mask over `currentColor`. No colour and no scale, so a design system's own rules override it at equal specificity.
|
|
546
548
|
|
|
547
549
|
```css
|
|
548
550
|
@import "wiki-formant/base.css" layer(components);
|
package/dist/base.css
CHANGED
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
.tab-panel:not(.active) { display: none; }
|
|
47
47
|
[data-tabs]:not([data-tabs-init]) [data-tab-item] ~ [data-tab-item] { display: none; }
|
|
48
48
|
|
|
49
|
+
/* ---- table scroll boxes (scrollTables) -----------------------------------
|
|
50
|
+
A stored table wider than its column scrolls inside its own box, at every
|
|
51
|
+
width, instead of painting past the column. */
|
|
52
|
+
.table-scroll { overflow-x: auto; }
|
|
53
|
+
|
|
49
54
|
/* ---- code copy buttons (addCopyButtons) -----------------------------------
|
|
50
55
|
The script positions the <pre>; this pins the button in its corner. Shown
|
|
51
56
|
on hover, on keyboard focus, and always where there is no hover to show it. */
|
package/dist/dom.d.ts
CHANGED
|
@@ -89,3 +89,17 @@ export interface SortTablesOptions {
|
|
|
89
89
|
* tables with fewer than two rows to sort; and rows that span several `<tbody>`s.
|
|
90
90
|
*/
|
|
91
91
|
export declare function sortTables(root: ParentNode, options?: SortTablesOptions): void;
|
|
92
|
+
/**
|
|
93
|
+
* Wrap every table in stored article HTML in a `.table-scroll` box, which
|
|
94
|
+
* `base.css` lets scroll sideways.
|
|
95
|
+
*
|
|
96
|
+
* A table box ignores `overflow`, and a stored table is as wide as its longest
|
|
97
|
+
* unbreakable token, an address or a URL, however narrow its column. radix-wiki
|
|
98
|
+
* gave it a scroll box only on phones, keyed to the viewport; at 1024px its
|
|
99
|
+
* column was 425px and a 1,068px ledger ran under the sticky infobox beside it.
|
|
100
|
+
* Unlike `sortTables` this runs on the string, before render: an effect would
|
|
101
|
+
* run after the table had already been laid out too wide.
|
|
102
|
+
*
|
|
103
|
+
* A table already in a box keeps it, so a second pass changes nothing.
|
|
104
|
+
*/
|
|
105
|
+
export declare function scrollTables(html: string): string;
|
package/dist/dom.js
CHANGED
|
@@ -298,3 +298,27 @@ export function sortTables(root, options = {}) {
|
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
|
+
// ---- scroll boxes -----------------------------------------------------------
|
|
302
|
+
const SCROLL_BOX = '<div class="table-scroll">';
|
|
303
|
+
/**
|
|
304
|
+
* Wrap every table in stored article HTML in a `.table-scroll` box, which
|
|
305
|
+
* `base.css` lets scroll sideways.
|
|
306
|
+
*
|
|
307
|
+
* A table box ignores `overflow`, and a stored table is as wide as its longest
|
|
308
|
+
* unbreakable token, an address or a URL, however narrow its column. radix-wiki
|
|
309
|
+
* gave it a scroll box only on phones, keyed to the viewport; at 1024px its
|
|
310
|
+
* column was 425px and a 1,068px ledger ran under the sticky infobox beside it.
|
|
311
|
+
* Unlike `sortTables` this runs on the string, before render: an effect would
|
|
312
|
+
* run after the table had already been laid out too wide.
|
|
313
|
+
*
|
|
314
|
+
* A table already in a box keeps it, so a second pass changes nothing.
|
|
315
|
+
*/
|
|
316
|
+
export function scrollTables(html) {
|
|
317
|
+
const boxed = [];
|
|
318
|
+
return html.replace(/(<div class="table-scroll">\s*)?<table\b|<\/table>/gi, (tag, hadBox) => {
|
|
319
|
+
if (tag[1] === '/')
|
|
320
|
+
return boxed.pop() ? `${tag}</div>` : tag;
|
|
321
|
+
boxed.push(!hadBox);
|
|
322
|
+
return hadBox ? tag : SCROLL_BOX + tag;
|
|
323
|
+
});
|
|
324
|
+
}
|
package/dist/markdown.js
CHANGED
|
@@ -81,35 +81,51 @@ export function tableToMarkdown(html) {
|
|
|
81
81
|
...body.map(r => `| ${r.join(' | ')} |`),
|
|
82
82
|
].join('\n');
|
|
83
83
|
}
|
|
84
|
+
// The spaces either side go with it: at a block boundary they are not text.
|
|
85
|
+
const BLOCK_TOKEN = /[ \t]*\u0000(\d+)\u0000[ \t]*/g;
|
|
84
86
|
/** Block-level HTML → markdown. */
|
|
85
87
|
export function htmlToMarkdown(html) {
|
|
86
88
|
let out = html;
|
|
89
|
+
// Each converted block is set aside behind a token and put back at the end.
|
|
90
|
+
// The `<div>` rule reads its body as inline text and collapses whitespace, so
|
|
91
|
+
// a block already converted inside a div lost every newline: a boxed table
|
|
92
|
+
// (`scrollTables`) came out as one line of pipes, and so did a list, a code
|
|
93
|
+
// listing or a callout's paragraphs.
|
|
94
|
+
const blocks = [];
|
|
95
|
+
const block = (md) => `\u0000${blocks.push(`\n\n${md}\n\n`) - 1}\u0000`;
|
|
96
|
+
const restore = (s) => s.replace(BLOCK_TOKEN, (_m, i) => restore(blocks[Number(i)]));
|
|
97
|
+
// A list inside an item is already a block by the time its parent converts;
|
|
98
|
+
// it goes under the item's line, indented, rather than onto it.
|
|
99
|
+
const listItem = (marker, body) => [
|
|
100
|
+
`${marker} ${inlineToMarkdown(body.replace(BLOCK_TOKEN, ''))}`,
|
|
101
|
+
...[...body.matchAll(BLOCK_TOKEN)].map(m => restore(m[0]).trim().replace(/^/gm, ' ')),
|
|
102
|
+
].join('\n');
|
|
87
103
|
// Tables first — their inner markup must not be eaten by the generic rules.
|
|
88
|
-
out = out.replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, m =>
|
|
104
|
+
out = out.replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, m => block(tableToMarkdown(m)));
|
|
89
105
|
// Lists. `<ol>` numbering is computed per-list, which is why this cannot be a
|
|
90
106
|
// single regex with a `$1` backreference — inside a replace callback `$1` is
|
|
91
107
|
// a literal, not a substitution. (That exact bug shipped once and rendered
|
|
92
108
|
// every ordered list as a column of `1. $1`.)
|
|
93
109
|
out = out.replace(/<ul\b[^>]*>([\s\S]*?)<\/ul>/gi, (_m, body) => {
|
|
94
|
-
const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map(i =>
|
|
95
|
-
return
|
|
110
|
+
const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map(i => listItem('-', i[1]));
|
|
111
|
+
return block(items.join('\n'));
|
|
96
112
|
});
|
|
97
113
|
out = out.replace(/<ol\b[^>]*>([\s\S]*?)<\/ol>/gi, (_m, body) => {
|
|
98
|
-
const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map((i, n) => `${n + 1}
|
|
99
|
-
return
|
|
114
|
+
const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map((i, n) => listItem(`${n + 1}.`, i[1]));
|
|
115
|
+
return block(items.join('\n'));
|
|
100
116
|
});
|
|
101
117
|
out = out
|
|
102
|
-
.replace(/<pre\b[^>]*>\s*<code\b[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, code) =>
|
|
103
|
-
.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_m, code) =>
|
|
104
|
-
.replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi, (_m, body) =>
|
|
118
|
+
.replace(/<pre\b[^>]*>\s*<code\b[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, code) => block(`\`\`\`\n${decodeEntities(code).trim()}\n\`\`\``))
|
|
119
|
+
.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_m, code) => block(`\`\`\`\n${decodeEntities(code).trim()}\n\`\`\``))
|
|
120
|
+
.replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi, (_m, body) => block(inlineToMarkdown(body)
|
|
105
121
|
.split('\n')
|
|
106
122
|
.map(l => `> ${l}`)
|
|
107
|
-
.join('\n')
|
|
108
|
-
.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, level, body) =>
|
|
109
|
-
.replace(/<hr\s*\/?>/gi, '
|
|
110
|
-
.replace(/<p\b[^>]*>([\s\S]*?)<\/p>/gi, (_m, body) =>
|
|
123
|
+
.join('\n')))
|
|
124
|
+
.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, level, body) => block(`${'#'.repeat(Number(level))} ${inlineToMarkdown(body)}`))
|
|
125
|
+
.replace(/<hr\s*\/?>/gi, () => block('---'))
|
|
126
|
+
.replace(/<p\b[^>]*>([\s\S]*?)<\/p>/gi, (_m, body) => block(inlineToMarkdown(body)))
|
|
111
127
|
.replace(/<div\b[^>]*>([\s\S]*?)<\/div>/gi, (_m, body) => `\n\n${inlineToMarkdown(body)}\n\n`);
|
|
112
|
-
return decodeEntities(out.replace(/<[^>]+>/g, ''))
|
|
128
|
+
return decodeEntities(restore(out).replace(/<[^>]+>/g, ''))
|
|
113
129
|
.replace(/[ \t]+$/gm, '')
|
|
114
130
|
.replace(/\n{3,}/g, '\n\n')
|
|
115
131
|
.trim();
|
package/dist/react.d.ts
CHANGED
|
@@ -83,7 +83,8 @@ export interface TableOfContentsProps {
|
|
|
83
83
|
offsetVar?: string;
|
|
84
84
|
/** Fallback offset in px when `offsetVar` is unset or unparseable. */
|
|
85
85
|
offsetFallback?: number;
|
|
86
|
-
/** Indent per heading level, in rem. Set 0 for a flat list
|
|
86
|
+
/** Indent per heading level, in rem. Set 0 for a flat list, whose items then
|
|
87
|
+
* take their padding from the stylesheet: an inline `padding-left: 0` beat it. */
|
|
87
88
|
indentRem?: number;
|
|
88
89
|
/** Rendered instead of nothing when the article has no headings. */
|
|
89
90
|
empty?: ReactNode;
|
package/dist/react.js
CHANGED
|
@@ -248,7 +248,7 @@ export function TableOfContents({ containerSelector, headings: providedHeadings,
|
|
|
248
248
|
document.getElementById(h.id)?.scrollIntoView({ behavior: 'smooth' });
|
|
249
249
|
}, className: currentId === h.id
|
|
250
250
|
? [classNames.item, classNames.itemActive].filter(Boolean).join(' ')
|
|
251
|
-
: classNames.item, "aria-current": currentId === h.id ? 'location' : undefined, style: { paddingLeft: `${(h.level - 1) * indentRem}rem` }, children: h.text }, h.id))) }))] }));
|
|
251
|
+
: classNames.item, "aria-current": currentId === h.id ? 'location' : undefined, style: (h.level - 1) * indentRem ? { paddingLeft: `${(h.level - 1) * indentRem}rem` } : undefined, children: h.text }, h.id))) }))] }));
|
|
252
252
|
}
|
|
253
253
|
// ---- outside clicks ---------------------------------------------------------
|
|
254
254
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
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, and conditional-GET plumbing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -177,8 +177,8 @@
|
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
179
|
"bin": {
|
|
180
|
-
"check-classes": "
|
|
181
|
-
"check-pages": "
|
|
180
|
+
"check-classes": "bin/check-classes.mjs",
|
|
181
|
+
"check-pages": "bin/check-pages.mjs"
|
|
182
182
|
},
|
|
183
183
|
"files": [
|
|
184
184
|
"dist",
|