wiki-formant 0.22.0 → 0.23.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 +42 -3
- package/dist/base.css +100 -0
- package/dist/block-views.d.ts +35 -7
- package/dist/block-views.js +26 -14
- package/dist/blocks.d.ts +26 -3
- package/dist/blocks.js +19 -5
- package/dist/conformance.d.ts +1 -1
- package/dist/conformance.js +18 -12
- package/dist/editor.d.ts +33 -1
- package/dist/editor.js +58 -0
- package/dist/feed.d.ts +10 -0
- package/dist/feed.js +20 -2
- package/dist/freshness.d.ts +33 -0
- package/dist/freshness.js +53 -0
- package/dist/headings.d.ts +7 -0
- package/dist/headings.js +12 -4
- package/dist/metadata.d.ts +54 -0
- package/dist/metadata.js +64 -0
- package/dist/react-server.d.ts +81 -2
- package/dist/react-server.js +71 -2
- package/dist/react.d.ts +83 -1
- package/dist/react.js +128 -0
- package/dist/sanitize.d.ts +5 -7
- package/dist/sanitize.js +5 -9
- package/package.json +4 -3
package/dist/react.js
CHANGED
|
@@ -518,6 +518,134 @@ export function useTableSort(rows, config) {
|
|
|
518
518
|
}, [sortKey, direction, toggle]);
|
|
519
519
|
return { sorted, sortKey, direction, headerProps, toggle };
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* The header cell `useTableSort` drives: spread `headerProps(key)` onto it.
|
|
523
|
+
*
|
|
524
|
+
* The press target is a `<button>` inside the `<th>`, never the cell itself: a
|
|
525
|
+
* cell with an `onClick` cannot be reached or pressed from the keyboard, which
|
|
526
|
+
* is the state one of the two copies this replaced was in. The markup is the
|
|
527
|
+
* one `sortTables` in `wiki-formant/dom` writes for tables stored in articles,
|
|
528
|
+
* so one stylesheet rule keyed on `aria-sort` draws both kinds of arrow.
|
|
529
|
+
*/
|
|
530
|
+
export function SortHeader({ sortKey, onSort, active: _active, direction: _direction, 'aria-sort': ariaSort, label, icon, children, ...th }) {
|
|
531
|
+
return (_jsx("th", { scope: "col", ...th, "aria-sort": ariaSort, children: _jsxs("button", { type: "button", className: "sort-header", onClick: () => onSort(sortKey), title: label, "aria-label": label, children: [children, icon] }) }));
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* The five edits every block editor makes to its list, with stable callbacks.
|
|
535
|
+
*
|
|
536
|
+
* The list and its setter are read through refs written after render, so a row
|
|
537
|
+
* memoised on these callbacks does not re-render on every keystroke elsewhere
|
|
538
|
+
* — and the setter may be a `useState` setter or a parent's `onChange`, which
|
|
539
|
+
* is what the three copies this replaced were split between. Creating and
|
|
540
|
+
* duplicating a block stay with the caller, whose union it is.
|
|
541
|
+
*/
|
|
542
|
+
export function useBlockOperations(blocks, setBlocks, options) {
|
|
543
|
+
const [selectedIndex, setSelectedIndex] = useState(null);
|
|
544
|
+
const latest = useRef({ blocks, setBlocks, clone: options.duplicate });
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
latest.current = { blocks, setBlocks, clone: options.duplicate };
|
|
547
|
+
});
|
|
548
|
+
const update = useCallback((i, block) => {
|
|
549
|
+
const { blocks, setBlocks } = latest.current;
|
|
550
|
+
setBlocks(blocks.map((b, j) => (j === i ? block : b)));
|
|
551
|
+
}, []);
|
|
552
|
+
const remove = useCallback((i) => {
|
|
553
|
+
const { blocks, setBlocks } = latest.current;
|
|
554
|
+
setBlocks(blocks.filter((_, j) => j !== i));
|
|
555
|
+
setSelectedIndex(null);
|
|
556
|
+
}, []);
|
|
557
|
+
const duplicate = useCallback((i) => {
|
|
558
|
+
const { blocks, setBlocks, clone } = latest.current;
|
|
559
|
+
const source = blocks[i];
|
|
560
|
+
if (source === undefined)
|
|
561
|
+
return;
|
|
562
|
+
setBlocks([...blocks.slice(0, i + 1), clone(source), ...blocks.slice(i + 1)]);
|
|
563
|
+
setSelectedIndex(i + 1);
|
|
564
|
+
}, []);
|
|
565
|
+
const move = useCallback((from, to) => {
|
|
566
|
+
const { blocks, setBlocks } = latest.current;
|
|
567
|
+
if (to < 0 || to >= blocks.length || from === to)
|
|
568
|
+
return;
|
|
569
|
+
const next = [...blocks];
|
|
570
|
+
const [moved] = next.splice(from, 1);
|
|
571
|
+
next.splice(to, 0, moved);
|
|
572
|
+
setBlocks(next);
|
|
573
|
+
setSelectedIndex(to);
|
|
574
|
+
}, []);
|
|
575
|
+
const insert = useCallback((block, at) => {
|
|
576
|
+
const { blocks, setBlocks } = latest.current;
|
|
577
|
+
const i = at ?? blocks.length;
|
|
578
|
+
setBlocks([...blocks.slice(0, i), block, ...blocks.slice(i)]);
|
|
579
|
+
setSelectedIndex(i);
|
|
580
|
+
}, []);
|
|
581
|
+
return { selectedIndex, setSelectedIndex, update, remove, duplicate, move, insert };
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Move up, move down, duplicate, delete. Every button has an accessible name
|
|
585
|
+
* and the moves are disabled at the ends — the three bars this replaced had
|
|
586
|
+
* each of those only sometimes. Clicks do not bubble, so a row that selects on
|
|
587
|
+
* click is not also selected by pressing delete in it. Omit `icons.duplicate`
|
|
588
|
+
* for a bar without it.
|
|
589
|
+
*/
|
|
590
|
+
export function BlockActions({ index, total, ops, icons, className, buttonClassName, blockLabel }) {
|
|
591
|
+
const name = (verb, rest = '') => (blockLabel ? `${verb} ${blockLabel}${rest}` : `${verb}${rest}`);
|
|
592
|
+
const button = (label, icon, run, disabled = false) => (_jsx("button", { type: "button", className: buttonClassName, title: label, "aria-label": label, disabled: disabled, onClick: e => {
|
|
593
|
+
e.stopPropagation();
|
|
594
|
+
run();
|
|
595
|
+
}, children: icon }));
|
|
596
|
+
return (_jsxs("span", { className: className, children: [button(name('Move', ' up'), icons.up, () => ops.move(index, index - 1), index === 0), button(name('Move', ' down'), icons.down, () => ops.move(index, index + 1), index === total - 1), icons.duplicate !== undefined && button(name('Duplicate'), icons.duplicate, () => ops.duplicate(index)), button(name('Delete'), icons.remove, () => ops.remove(index))] }));
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* POST `{ revisionId }` to `endpoint`, the transport two of the wikis use; the
|
|
600
|
+
* third passes its server action instead.
|
|
601
|
+
*/
|
|
602
|
+
export function restoreViaPost(endpoint) {
|
|
603
|
+
return async (id) => {
|
|
604
|
+
try {
|
|
605
|
+
const res = await fetch(endpoint, {
|
|
606
|
+
method: 'POST',
|
|
607
|
+
headers: { 'Content-Type': 'application/json' },
|
|
608
|
+
body: JSON.stringify({ revisionId: id }),
|
|
609
|
+
});
|
|
610
|
+
if (res.ok)
|
|
611
|
+
return null;
|
|
612
|
+
const body = (await res.json().catch(() => null));
|
|
613
|
+
return body?.error ?? 'Restore failed.';
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
return 'Restore failed.';
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Confirm, restore, report. Of the three copies one asked no confirmation, one
|
|
622
|
+
* dropped a failure silently, and one reported it through `alert`. A restore
|
|
623
|
+
* writes the old state forward as a new revision in all three, which is what
|
|
624
|
+
* the default question says.
|
|
625
|
+
*/
|
|
626
|
+
export function useRevisionRestore(restore, options) {
|
|
627
|
+
const [busyId, setBusyId] = useState(null);
|
|
628
|
+
const [error, setError] = useState(null);
|
|
629
|
+
const { onRestored, confirm = (label) => `Restore ${label}? The current state stays in the history.` } = options;
|
|
630
|
+
const run = useCallback(async (id, label) => {
|
|
631
|
+
const question = confirm(label);
|
|
632
|
+
if (question && !window.confirm(question))
|
|
633
|
+
return;
|
|
634
|
+
setBusyId(id);
|
|
635
|
+
setError(null);
|
|
636
|
+
try {
|
|
637
|
+
const failure = await restore(id);
|
|
638
|
+
if (failure)
|
|
639
|
+
setError(failure);
|
|
640
|
+
else
|
|
641
|
+
onRestored();
|
|
642
|
+
}
|
|
643
|
+
finally {
|
|
644
|
+
setBusyId(null);
|
|
645
|
+
}
|
|
646
|
+
}, [restore, onRestored, confirm]);
|
|
647
|
+
return { restore: run, busyId, error };
|
|
648
|
+
}
|
|
521
649
|
// ---- copy to clipboard ------------------------------------------------------
|
|
522
650
|
/**
|
|
523
651
|
* "Copied", then not, after a beat. Six call sites across three repos wrote the
|
package/dist/sanitize.d.ts
CHANGED
|
@@ -27,17 +27,15 @@ export interface HtmlSanitizerOptions {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function createHtmlSanitizer(options?: HtmlSanitizerOptions): (html: string) => string;
|
|
29
29
|
/**
|
|
30
|
-
* The core leaf types' HTML fields, cleaned: `content.text`, `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* The core leaf types' HTML fields, cleaned: `content.text`, `linkGrid` group
|
|
31
|
+
* descriptions and `references` item text. Every other field those blocks
|
|
32
|
+
* carry renders as a React text node and is escaped already.
|
|
33
33
|
*
|
|
34
34
|
* Returns any other block untouched, so it drops straight into a
|
|
35
35
|
* `mapBlockTree` pass — a repo whose own types render HTML cleans those itself.
|
|
36
36
|
*
|
|
37
|
-
* `codeTabs` code is
|
|
38
|
-
*
|
|
39
|
-
* must not pass it through here — every `<T>` in a signature would go — and
|
|
40
|
-
* cleans its other three fields with its own switch.
|
|
37
|
+
* `codeTabs` passes through: its `code` is source text, which `CodeTabsView`
|
|
38
|
+
* escapes. Cleaning it as HTML was a bug — every `<T>` in a signature went.
|
|
41
39
|
*/
|
|
42
40
|
export declare function sanitizeCoreLeaf<B extends {
|
|
43
41
|
type: string;
|
package/dist/sanitize.js
CHANGED
|
@@ -159,25 +159,21 @@ export function createHtmlSanitizer(options = {}) {
|
|
|
159
159
|
const each = (v, map) => Array.isArray(v) ? v.map(item => (item && typeof item === 'object' ? map(item) : item)) : v;
|
|
160
160
|
const str = (v) => typeof v === 'string';
|
|
161
161
|
/**
|
|
162
|
-
* The core leaf types' HTML fields, cleaned: `content.text`, `
|
|
163
|
-
*
|
|
164
|
-
*
|
|
162
|
+
* The core leaf types' HTML fields, cleaned: `content.text`, `linkGrid` group
|
|
163
|
+
* descriptions and `references` item text. Every other field those blocks
|
|
164
|
+
* carry renders as a React text node and is escaped already.
|
|
165
165
|
*
|
|
166
166
|
* Returns any other block untouched, so it drops straight into a
|
|
167
167
|
* `mapBlockTree` pass — a repo whose own types render HTML cleans those itself.
|
|
168
168
|
*
|
|
169
|
-
* `codeTabs` code is
|
|
170
|
-
*
|
|
171
|
-
* must not pass it through here — every `<T>` in a signature would go — and
|
|
172
|
-
* cleans its other three fields with its own switch.
|
|
169
|
+
* `codeTabs` passes through: its `code` is source text, which `CodeTabsView`
|
|
170
|
+
* escapes. Cleaning it as HTML was a bug — every `<T>` in a signature went.
|
|
173
171
|
*/
|
|
174
172
|
export function sanitizeCoreLeaf(block, clean) {
|
|
175
173
|
const b = block;
|
|
176
174
|
switch (b.type) {
|
|
177
175
|
case 'content':
|
|
178
176
|
return str(b.text) ? { ...b, text: clean(b.text) } : block;
|
|
179
|
-
case 'codeTabs':
|
|
180
|
-
return { ...b, tabs: each(b.tabs, t => (str(t.code) ? { ...t, code: clean(t.code) } : t)) };
|
|
181
177
|
case 'linkGrid':
|
|
182
178
|
return {
|
|
183
179
|
...b,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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, and conditional-GET plumbing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"markdown",
|
|
19
19
|
"agent"
|
|
20
20
|
],
|
|
21
|
-
"sideEffects":
|
|
21
|
+
"sideEffects": ["*.css"],
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
@@ -144,6 +144,7 @@
|
|
|
144
144
|
"types": "./dist/x402.d.ts",
|
|
145
145
|
"import": "./dist/x402.js"
|
|
146
146
|
},
|
|
147
|
+
"./base.css": "./dist/base.css",
|
|
147
148
|
"./block-views": {
|
|
148
149
|
"types": "./dist/block-views.d.ts",
|
|
149
150
|
"import": "./dist/block-views.js"
|
|
@@ -184,7 +185,7 @@
|
|
|
184
185
|
"node": ">=20"
|
|
185
186
|
},
|
|
186
187
|
"scripts": {
|
|
187
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
188
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/base.css dist/base.css",
|
|
188
189
|
"test": "npm run build && node --test 'test/*.test.js'"
|
|
189
190
|
},
|
|
190
191
|
"devDependencies": {
|