wiki-formant 0.79.0 → 0.80.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 +3 -3
- package/dist/base.css +11 -2
- package/dist/dom.d.ts +8 -6
- package/dist/dom.js +10 -7
- package/dist/react.d.ts +1 -1
- package/dist/react.js +3 -3
- package/dist/tiptap.d.ts +8 -0
- package/dist/tiptap.js +18 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -693,7 +693,7 @@ 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
|
-
const off = onTweetResize(h =>
|
|
696
|
+
const off = onTweetResize((h, source) => sizeTweetEmbed(el, h, source));
|
|
697
697
|
```
|
|
698
698
|
|
|
699
699
|
`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.
|
|
@@ -706,7 +706,7 @@ In React, `useArticlePasses(ref, [blocks])` runs the three passes and `useTweetE
|
|
|
706
706
|
|
|
707
707
|
`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. `articleHtml(html, { used, slug, anchor, links })` (`wiki-formant/headings`) is that chain: heading ids, the scroll boxes, and, given `links`, `normaliseLinks`.
|
|
708
708
|
|
|
709
|
-
The embed's 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.
|
|
709
|
+
The embed's 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. Every embed on a page posts to the same listener, so `onTweetResize` hands over the window that posted, and `sizeTweetEmbed` sizes only that one's iframe: two posts on a page had both been drawn at whichever height arrived last.
|
|
710
710
|
|
|
711
711
|
## Search
|
|
712
712
|
|
|
@@ -799,7 +799,7 @@ The bytes say what the file is, never the name or type, and SVG is refused. Ever
|
|
|
799
799
|
|
|
800
800
|
## Editor nodes
|
|
801
801
|
|
|
802
|
-
`wiki-formant/tiptap` carries the custom nodes both wiki editors had written twice: `Iframe`, `YouTube` (the stock extension plus the paste rule it does not ship with), `TwitterEmbed`, `createMapEmbed`, `createCodeBlock` and `createTabs`. `Iframe` and `createMapEmbed` draw a frame only for an https src on `DEFAULT_IFRAME_HOSTS` (`embedSrc`), since an editor draws stored HTML where its author is signed in, and a `javascript:` src runs there under any CSP that allows inline script.
|
|
802
|
+
`wiki-formant/tiptap` carries the custom nodes both wiki editors had written twice: `Iframe`, `YouTube` (the stock extension plus the paste rule it does not ship with), `TwitterEmbed`, `createMapEmbed`, `createCodeBlock` and `createTabs`. `embedNodes({ resolveMapUrl })` is the first four as one list, which an editor puts in its `nodes`: a pasted YouTube, X or map link becomes its player as it lands, and the toolbar's embed prompt has a node for every link it recognises. `resolveMapUrl` defaults to `wiki-formant/maps`', through `/api/resolve-map`. `Iframe` and `createMapEmbed` draw a frame only for an https src on `DEFAULT_IFRAME_HOSTS` (`embedSrc`), since an editor draws stored HTML where its author is signed in, and a `javascript:` src runs there under any CSP that allows inline script.
|
|
803
803
|
|
|
804
804
|
```ts
|
|
805
805
|
const CodeBlock = createCodeBlock({
|
package/dist/base.css
CHANGED
|
@@ -432,8 +432,8 @@ pre:hover > .code-copy-btn,
|
|
|
432
432
|
.stats-count { font-variant-numeric: tabular-nums; }
|
|
433
433
|
|
|
434
434
|
/* ---- embeds ------------------------------------------------------------------
|
|
435
|
-
The players the `wiki-formant/tiptap` nodes store,
|
|
436
|
-
screen's proportions. The node writes a width and height onto the iframe,
|
|
435
|
+
The players the `wiki-formant/tiptap` nodes store, a video or map filling its
|
|
436
|
+
column at a screen's proportions. The node writes a width and height onto the iframe,
|
|
437
437
|
which `aspect-ratio` with `height: auto` overrides. Spacing, corners,
|
|
438
438
|
borders and the editor's selection ring are the site's. */
|
|
439
439
|
:is(div[data-youtube-video], .map-embed) iframe {
|
|
@@ -442,3 +442,12 @@ pre:hover > .code-copy-btn,
|
|
|
442
442
|
height: auto;
|
|
443
443
|
aspect-ratio: 16 / 9;
|
|
444
444
|
}
|
|
445
|
+
|
|
446
|
+
/* A post on X at the width X lays it out for, as tall as it says it is
|
|
447
|
+
(`onTweetResize`), and a little before it has said. */
|
|
448
|
+
.twitter-embed iframe {
|
|
449
|
+
display: block;
|
|
450
|
+
width: 550px;
|
|
451
|
+
max-width: 100%;
|
|
452
|
+
min-height: 200px;
|
|
453
|
+
}
|
package/dist/dom.d.ts
CHANGED
|
@@ -20,13 +20,15 @@ export declare function addCopyButtons(root: ParentNode, options?: CopyButtonOpt
|
|
|
20
20
|
/** The embed iframe's src. `dnt=true` opts the embed out of Twitter's tracking. */
|
|
21
21
|
export declare const tweetEmbedSrc: (tweetId: string | number) => string;
|
|
22
22
|
/**
|
|
23
|
-
* Subscribe to the height
|
|
23
|
+
* Subscribe to the height an embed posts back once it has laid itself out,
|
|
24
|
+
* with the window that posted it.
|
|
24
25
|
*
|
|
25
26
|
* The iframe renders at an unknown height and there is no other way to learn
|
|
26
|
-
* it.
|
|
27
|
-
* the origin check and the
|
|
27
|
+
* it. Every embed on the page posts to the same listener, so the source is how
|
|
28
|
+
* a height finds its own iframe. This only owns the origin check and the
|
|
29
|
+
* message shape. Returns a disposer.
|
|
28
30
|
*/
|
|
29
|
-
export declare function onTweetResize(resize: (height: number) => void): () => void;
|
|
31
|
+
export declare function onTweetResize(resize: (height: number, source: MessageEventSource | null) => void): () => void;
|
|
30
32
|
/**
|
|
31
33
|
* Point every un-hydrated `[data-twitter-embed]` placeholder under `root` at
|
|
32
34
|
* the real embed URL.
|
|
@@ -36,8 +38,8 @@ export declare function onTweetResize(resize: (height: number) => void): () => v
|
|
|
36
38
|
* of the same content does not reload every embed on the page.
|
|
37
39
|
*/
|
|
38
40
|
export declare function hydrateTweetEmbeds(root: ParentNode): void;
|
|
39
|
-
/** Apply a
|
|
40
|
-
export declare function
|
|
41
|
+
/** Apply a posted height to the embed iframe under `root` that posted it, and to no other. */
|
|
42
|
+
export declare function sizeTweetEmbed(root: ParentNode, height: number, source: MessageEventSource | null): void;
|
|
41
43
|
export interface TabGroupClassNames {
|
|
42
44
|
list?: string;
|
|
43
45
|
panels?: string;
|
package/dist/dom.js
CHANGED
|
@@ -63,11 +63,13 @@ export const TWITTER_ORIGIN = 'https://platform.twitter.com';
|
|
|
63
63
|
/** The embed iframe's src. `dnt=true` opts the embed out of Twitter's tracking. */
|
|
64
64
|
export const tweetEmbedSrc = (tweetId) => `${TWITTER_ORIGIN}/embed/Tweet.html?id=${tweetId}&dnt=true`;
|
|
65
65
|
/**
|
|
66
|
-
* Subscribe to the height
|
|
66
|
+
* Subscribe to the height an embed posts back once it has laid itself out,
|
|
67
|
+
* with the window that posted it.
|
|
67
68
|
*
|
|
68
69
|
* The iframe renders at an unknown height and there is no other way to learn
|
|
69
|
-
* it.
|
|
70
|
-
* the origin check and the
|
|
70
|
+
* it. Every embed on the page posts to the same listener, so the source is how
|
|
71
|
+
* a height finds its own iframe. This only owns the origin check and the
|
|
72
|
+
* message shape. Returns a disposer.
|
|
71
73
|
*/
|
|
72
74
|
export function onTweetResize(resize) {
|
|
73
75
|
const handleMessage = (e) => {
|
|
@@ -78,7 +80,7 @@ export function onTweetResize(resize) {
|
|
|
78
80
|
return;
|
|
79
81
|
const height = data.params?.[0]?.height;
|
|
80
82
|
if (height)
|
|
81
|
-
resize(height);
|
|
83
|
+
resize(height, e.source);
|
|
82
84
|
};
|
|
83
85
|
window.addEventListener('message', handleMessage);
|
|
84
86
|
return () => window.removeEventListener('message', handleMessage);
|
|
@@ -104,10 +106,11 @@ export function hydrateTweetEmbeds(root) {
|
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
|
-
/** Apply a
|
|
108
|
-
export function
|
|
109
|
+
/** Apply a posted height to the embed iframe under `root` that posted it, and to no other. */
|
|
110
|
+
export function sizeTweetEmbed(root, height, source) {
|
|
109
111
|
for (const iframe of Array.from(root.querySelectorAll('[data-twitter-embed] iframe'))) {
|
|
110
|
-
iframe.
|
|
112
|
+
if (iframe.contentWindow === source)
|
|
113
|
+
iframe.style.height = `${height}px`;
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
/**
|
package/dist/react.d.ts
CHANGED
|
@@ -485,7 +485,7 @@ export type { WikiLinkComponent, WikiLinkProps } from './react-server.js';
|
|
|
485
485
|
* idempotent, so running on every `deps` change is safe.
|
|
486
486
|
*/
|
|
487
487
|
export declare function useArticlePasses(ref: RefObject<HTMLElement | null>, deps: DependencyList): void;
|
|
488
|
-
/** Tweet placeholders hydrated, and
|
|
488
|
+
/** Tweet placeholders hydrated, and each iframe kept at the height it posts. */
|
|
489
489
|
export declare function useTweetEmbeds(ref: RefObject<HTMLElement | null>, deps: DependencyList): void;
|
|
490
490
|
/**
|
|
491
491
|
* Counts the page views of the app it is mounted in: place it once, in the root
|
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,
|
|
29
|
+
import { activateTabGroups, addCopyButtons, hydrateTweetEmbeds, onTweetResize, 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) => {
|
|
@@ -899,14 +899,14 @@ export function useArticlePasses(ref, deps) {
|
|
|
899
899
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
900
900
|
}, deps);
|
|
901
901
|
}
|
|
902
|
-
/** Tweet placeholders hydrated, and
|
|
902
|
+
/** Tweet placeholders hydrated, and each iframe kept at the height it posts. */
|
|
903
903
|
export function useTweetEmbeds(ref, deps) {
|
|
904
904
|
useEffect(() => {
|
|
905
905
|
const root = ref.current;
|
|
906
906
|
if (!root)
|
|
907
907
|
return;
|
|
908
908
|
hydrateTweetEmbeds(root);
|
|
909
|
-
return onTweetResize(height =>
|
|
909
|
+
return onTweetResize((height, source) => sizeTweetEmbed(root, height, source));
|
|
910
910
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
911
911
|
}, deps);
|
|
912
912
|
}
|
package/dist/tiptap.d.ts
CHANGED
|
@@ -29,6 +29,14 @@ export interface MapEmbedOptions {
|
|
|
29
29
|
* node has to already exist for the reader to see anything happen.
|
|
30
30
|
*/
|
|
31
31
|
export declare function createMapEmbed({ resolveMapUrl }: MapEmbedOptions): TiptapNode<any, any>;
|
|
32
|
+
/**
|
|
33
|
+
* Every player a pasted link becomes – a YouTube video, a post on X, a Google
|
|
34
|
+
* or Apple map – and any other allowed iframe, as one list. Each app had
|
|
35
|
+
* picked its embeds by hand, and one that left `TwitterEmbed` out kept a
|
|
36
|
+
* pasted post as a bare link. `resolveMapUrl` follows a shortened map link,
|
|
37
|
+
* through `/api/resolve-map` unless an app routes it another way.
|
|
38
|
+
*/
|
|
39
|
+
export declare const embedNodes: ({ resolveMapUrl }?: Partial<MapEmbedOptions>) => TiptapNode<any, any>[];
|
|
32
40
|
export interface CodeBlockClassNames {
|
|
33
41
|
wrapper?: string;
|
|
34
42
|
/** The control cluster, positioned over the block. */
|
package/dist/tiptap.js
CHANGED
|
@@ -18,7 +18,7 @@ import TiptapYoutube from '@tiptap/extension-youtube';
|
|
|
18
18
|
import TiptapCodeBlock from '@tiptap/extension-code-block';
|
|
19
19
|
import { onTweetResize, tweetEmbedSrc } from './dom.js';
|
|
20
20
|
import { DEFAULT_IFRAME_HOSTS } from './headers.js';
|
|
21
|
-
import { toMapEmbedUrl } from './maps.js';
|
|
21
|
+
import { resolveMapUrl as resolveMapRoute, toMapEmbedUrl } from './maps.js';
|
|
22
22
|
import { useClickOutside } from './react.js';
|
|
23
23
|
import { cx } from './html.js';
|
|
24
24
|
import { slugifyHeading, uniqueHeadingId } from './headings.js';
|
|
@@ -78,10 +78,10 @@ export const YouTube = TiptapYoutube.extend({
|
|
|
78
78
|
function TwitterEmbedView({ node }) {
|
|
79
79
|
const containerRef = useRef(null);
|
|
80
80
|
// The embed posts its measured height back; `onTweetResize` owns the origin
|
|
81
|
-
// check.
|
|
82
|
-
useEffect(() => onTweetResize(height => {
|
|
81
|
+
// check, and the source keeps one tweet's height off every other.
|
|
82
|
+
useEffect(() => onTweetResize((height, source) => {
|
|
83
83
|
const iframe = containerRef.current?.querySelector('iframe');
|
|
84
|
-
if (iframe)
|
|
84
|
+
if (iframe && iframe.contentWindow === source)
|
|
85
85
|
iframe.style.height = `${height}px`;
|
|
86
86
|
}), []);
|
|
87
87
|
return (_jsx(NodeViewWrapper, { children: _jsx("div", { ref: containerRef, className: "twitter-embed", children: _jsx("iframe", { src: tweetEmbedSrc(node.attrs.tweetId), scrolling: "no", allowFullScreen: true }) }) }));
|
|
@@ -231,6 +231,20 @@ export function createMapEmbed({ resolveMapUrl }) {
|
|
|
231
231
|
},
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
|
+
// ---- every embed ------------------------------------------------------------
|
|
235
|
+
/**
|
|
236
|
+
* Every player a pasted link becomes – a YouTube video, a post on X, a Google
|
|
237
|
+
* or Apple map – and any other allowed iframe, as one list. Each app had
|
|
238
|
+
* picked its embeds by hand, and one that left `TwitterEmbed` out kept a
|
|
239
|
+
* pasted post as a bare link. `resolveMapUrl` follows a shortened map link,
|
|
240
|
+
* through `/api/resolve-map` unless an app routes it another way.
|
|
241
|
+
*/
|
|
242
|
+
export const embedNodes = ({ resolveMapUrl = resolveMapRoute } = {}) => [
|
|
243
|
+
YouTube,
|
|
244
|
+
Iframe,
|
|
245
|
+
TwitterEmbed,
|
|
246
|
+
createMapEmbed({ resolveMapUrl }),
|
|
247
|
+
];
|
|
234
248
|
/** A code block whose language is pickable from a dropdown. */
|
|
235
249
|
export function createCodeBlock({ langs = CODE_LANGS, defaultLang = DEFAULT_LANG, classNames = {}, icons = {}, } = {}) {
|
|
236
250
|
function CodeBlockView({ node, updateAttributes, }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.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",
|