wiki-formant 0.4.0 → 0.7.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 +102 -1
- package/dist/analytics.d.ts +22 -0
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +31 -1
- package/dist/analytics.js.map +1 -1
- package/dist/dom.d.ts +69 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +167 -0
- package/dist/dom.js.map +1 -0
- package/dist/react.d.ts +216 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +470 -0
- package/dist/react.js.map +1 -0
- package/dist/sidebar.d.ts +38 -0
- package/dist/sidebar.d.ts.map +1 -0
- package/dist/sidebar.js +43 -0
- package/dist/sidebar.js.map +1 -0
- package/dist/taxonomy.d.ts +52 -0
- package/dist/taxonomy.d.ts.map +1 -1
- package/dist/taxonomy.js +95 -42
- package/dist/taxonomy.js.map +1 -1
- package/dist/tiptap.d.ts +78 -0
- package/dist/tiptap.d.ts.map +1 -0
- package/dist/tiptap.js +344 -0
- package/dist/tiptap.js.map +1 -0
- package/package.json +54 -3
package/README.md
CHANGED
|
@@ -47,6 +47,23 @@ Four behaviours worth knowing, because each replaces a plausible wrong answer:
|
|
|
47
47
|
|
|
48
48
|
One `href` builder is passed in and used by every chip, letter and sort button. A sort button that drops the active filters is the tell that a project grew a second one.
|
|
49
49
|
|
|
50
|
+
### The controls, not just the counts
|
|
51
|
+
|
|
52
|
+
`buildFacets` and `alphaIndex` hand back numbers; something still has to turn each one into a pressable thing with a destination. That step was rebuilt in all three wikis, and by the time this was written they no longer agreed on what the row *contains*: one shipped no A–Z index at all, one had no way back to the unfiltered set, and two marked the active chip with `aria-pressed` on an `<a>` — which is not a toggle button and does not take that attribute.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const state = { sort, filters, letter: taxonomy.resolveLetter(all, filters, query.letter) };
|
|
56
|
+
const groups = taxonomy.facetControls('ecosystem', all, state); // [{key, label, options}]
|
|
57
|
+
const letters = taxonomy.alphaControls('ecosystem', all, state); // [] below the threshold
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Every control arrives as `{value, label, count, href, active}`, its `href` already built through your one builder with **every other axis carried along** — the arithmetic that, done by hand, drops the reader's letter the first time they press a chip.
|
|
61
|
+
|
|
62
|
+
- **The reset control leads the row**, flagged `reset`, so a consumer that maps the array cannot ship a narrowed view with nothing to press to widen it.
|
|
63
|
+
- **`alphaControls` returns `[]` below the index threshold**, folding in `needsAlphaIndex` — one call is either the index you needed or nothing, rather than a decision each caller makes and one caller forgets.
|
|
64
|
+
- **`resolveLetter` drops a letter no page starts with.** A stray `?letter=Q` would otherwise empty the grid with no control marked to explain why.
|
|
65
|
+
|
|
66
|
+
|
|
50
67
|
## Headings
|
|
51
68
|
|
|
52
69
|
Heading ids, permalink anchors, and the list an "on this page" rail renders — one pass over the HTML you already have.
|
|
@@ -148,6 +165,85 @@ export async function GET(request: Request) {
|
|
|
148
165
|
|
|
149
166
|
`parseVersion` / `bump` / `compareVersions` handle revision semver tolerantly — a page always has a version, even when the column holds `null` or junk.
|
|
150
167
|
|
|
168
|
+
## React
|
|
169
|
+
|
|
170
|
+
`wiki-formant/react` is the one part that needs React, so it is the one part behind its own subpath, and React is an *optional* peer. What is shared is behaviour; every class name and icon is passed in, because these wikis style their rails differently and always will.
|
|
171
|
+
|
|
172
|
+
`SidebarProvider` / `useSidebar` hold the rail's collapse state — remembered across loads, defaulted from the viewport only when the reader has never chosen. Pair it with `sidebarBootScript` from `wiki-formant/sidebar` (framework-free, so a server component can stamp it into `<head>`) or a remembered-closed rail paints open and animates shut on every load. `TableOfContents` is the "on this page" list, with scroll-spy, reading either headings you already know or the rendered article.
|
|
173
|
+
|
|
174
|
+
`useTypeahead` is the search field's state machine. Five surfaces across the three wikis had three implementations and no two agreed on what a search field does; this is their union, because each had a piece the others lacked:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const { query, setQuery, items, highlight, setHighlight, onKeyDown, reset } =
|
|
178
|
+
useTypeahead({ fetch: searchPages, onPick: page => router.push(page.href) });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- **A request-id guard**, so a slow response cannot paint over a newer one. Two of the five surfaces had none: type fast enough and you are reading results for a query you have already replaced.
|
|
182
|
+
- **Keyboard navigation.** Two surfaces had none — including the header dropdown of a wiki whose search *is* its primary navigation.
|
|
183
|
+
- **Abort and a per-query cache**, which only the third had. The cache is per mount, so a stale result cannot outlive the page.
|
|
184
|
+
|
|
185
|
+
`fetch` must be referentially stable; it is an effect dependency. Its `signal` is optional to implement — forward it from a REST fetcher, ignore it in a server action.
|
|
186
|
+
|
|
187
|
+
`useLinkPreview` is the Wikipedia-style hover card: one delegated listener rather than a component per link, which is what makes it viable over an article holding hundreds of anchors. Two wikis had written the same ninety lines — same intent delay, same grace period for the cursor to cross into the card, same clamp arithmetic, same cache. They differed in three things, and those three are the options: which anchors are eligible, what fetches a preview, and how tall the card is.
|
|
188
|
+
|
|
189
|
+
`useClickOutside` is fifteen lines and was in all three. Only one had the `offsetParent` check, and without it a container hidden at the current breakpoint still answers outside-clicks — so on a phone, a tap anywhere dismisses the popover the reader is looking at, because the hidden desktop copy got there first.
|
|
190
|
+
|
|
191
|
+
## Rendered-article passes
|
|
192
|
+
|
|
193
|
+
`wiki-formant/dom` holds what runs against an article element after it is in the document. Not React, so not in `react.tsx`.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
addCopyButtons(el); // every <pre> gets one, once
|
|
197
|
+
hydrateTweetEmbeds(el); // placeholders get a live src
|
|
198
|
+
const off = onTweetResize(h => sizeTweetEmbeds(el, h));
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`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.
|
|
202
|
+
|
|
203
|
+
`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.
|
|
204
|
+
|
|
205
|
+
`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.
|
|
206
|
+
|
|
207
|
+
## Editor nodes
|
|
208
|
+
|
|
209
|
+
`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`. The four `@tiptap/*` packages are optional peers, so a consumer taking only the taxonomy still installs a package with no runtime dependencies.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const CodeBlock = createCodeBlock({
|
|
213
|
+
langs: CODE_LANGS, defaultLang: DEFAULT_LANG,
|
|
214
|
+
classNames: { button: 'lang-btn', option: 'lang-option', optionActive: 'text-accent' },
|
|
215
|
+
icons: { chevron: open => <ChevronDown className={open ? 'rotate-180' : ''} /> },
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The ones that take config take it because that is exactly where the two copies differed — class tokens, the language list, and the API route a shortened map URL has to be resolved through. Injecting them is what lets one wiki keep `text-jupiter` and the other `text-accent` without either forking the node, and it keeps this file from dragging an icon library in behind it.
|
|
220
|
+
|
|
221
|
+
`createTabs` returns `TabGroup` and `TabItem` together: `tabGroup`'s content expression is `tabItem+`, so registering one without the other leaves a node type the schema cannot satisfy. A pasted short map link inserts immediately with `about:blank` and swaps its `src` when the redirect resolves — pasting must not block on a network hop, and the node has to exist for the reader to see anything happen.
|
|
222
|
+
|
|
223
|
+
## Analytics
|
|
224
|
+
|
|
225
|
+
Two lanes, one events helper. `mcpCallProps` reads a tool name out of a JSON-RPC
|
|
226
|
+
envelope that is untrusted and may be a batch; `searchQueryProps` normalises a
|
|
227
|
+
search box's free text so the same question aggregates as one row. Sending stays
|
|
228
|
+
with the caller — `plausibleEvent` builds the request, and the framework's own
|
|
229
|
+
deferral (`after()` in a route or server action, `event.waitUntil` in a proxy)
|
|
230
|
+
decides when it goes, so this package keeps its zero-dependency guarantee.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const props = searchQueryProps({ query, results: rows.length, surface: 'wiki' });
|
|
234
|
+
// null for an empty field, so a blank search cannot fire an event
|
|
235
|
+
if (props) after(() => plausibleEvent({ domain }, 'Search Query', url, props, headers, {
|
|
236
|
+
// a person triggered this, so it joins their session rather than landing
|
|
237
|
+
// as the fixed bot-tracker pseudo-visitor
|
|
238
|
+
userAgent: headers.get('user-agent') ?? undefined,
|
|
239
|
+
}));
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Instrumenting the agent lane and not the human one is the easy mistake: it
|
|
243
|
+
leaves a wiki able to say what every crawler asked for and nothing about what
|
|
244
|
+
its readers asked for. The queries that return zero rows are the valuable ones —
|
|
245
|
+
they name a gap in the corpus in the reader's own words.
|
|
246
|
+
|
|
151
247
|
## API
|
|
152
248
|
|
|
153
249
|
| Export | From |
|
|
@@ -159,8 +255,13 @@ export async function GET(request: Request) {
|
|
|
159
255
|
| `corpusEtag`, `notModified`, `textHeaders`, `markdownHeaders`, `cleanSnippet`, `pageLine` | `wiki-formant/http` |
|
|
160
256
|
| `parsePagination`, `paginatedResponse`, `toOffset` | `wiki-formant/pagination` |
|
|
161
257
|
| `parseVersion`, `formatVersion`, `incrementVersion`, `bump`, `compareVersions` | `wiki-formant/versioning` |
|
|
258
|
+
| `plausibleEvent`, `mcpCallProps`, `searchQueryProps`, `plausibleDomain` | `wiki-formant/analytics` |
|
|
259
|
+
| `useCollapsibleSidebar`, `SidebarProvider`, `useSidebar`, `TableOfContents`, `useTypeahead`, `useLinkPreview`, `useClickOutside` | `wiki-formant/react` |
|
|
260
|
+
| `resolveSidebarOpen`, `sidebarBootScript`, `SIDEBAR_ATTRIBUTE` | `wiki-formant/sidebar` |
|
|
261
|
+
| `addCopyButtons`, `activateTabGroups`, `tweetEmbedSrc`, `onTweetResize`, `hydrateTweetEmbeds`, `sizeTweetEmbeds`, `TWITTER_ORIGIN` | `wiki-formant/dom` |
|
|
262
|
+
| `Iframe`, `YouTube`, `TwitterEmbed`, `createMapEmbed`, `createCodeBlock`, `createTabs` | `wiki-formant/tiptap` |
|
|
162
263
|
|
|
163
|
-
|
|
264
|
+
Everything above `wiki-formant/react` is also re-exported from the package root. The React, sidebar, DOM and tiptap subpaths are not: they carry `'use client'` or reach for a browser global, and the root has to stay importable from a route handler.
|
|
164
265
|
|
|
165
266
|
## Licence
|
|
166
267
|
|
package/dist/analytics.d.ts
CHANGED
|
@@ -38,6 +38,28 @@ export declare function mcpCallProps(request: {
|
|
|
38
38
|
headers: HeaderReader;
|
|
39
39
|
url: string;
|
|
40
40
|
}, body: unknown, server?: string): Record<string, string>;
|
|
41
|
+
export interface SearchQueryInput {
|
|
42
|
+
/** The settled query the typeahead debounce actually dispatched. */
|
|
43
|
+
query: string;
|
|
44
|
+
/** How many rows came back. Zero is the interesting case. */
|
|
45
|
+
results: number;
|
|
46
|
+
/** Which search box, when a site has more than one. */
|
|
47
|
+
surface?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Props for a "Search Query" event — the human-side twin of `mcpCallProps`.
|
|
51
|
+
*
|
|
52
|
+
* A wiki instruments its agent surface and then counts every question an agent
|
|
53
|
+
* asks while recording none of the questions a person asks. The queries that
|
|
54
|
+
* return nothing are the valuable ones: they name a gap in the corpus in the
|
|
55
|
+
* reader's own words, which is otherwise only ever guessed at.
|
|
56
|
+
*
|
|
57
|
+
* Returns `null` for a query with no content, so an empty field cannot fire an
|
|
58
|
+
* event. The text is untrusted, so it is collapsed, lowercased for aggregation
|
|
59
|
+
* and truncated — the same 64-character bound search itself applies, which
|
|
60
|
+
* keeps the prop and the query that produced it the same string.
|
|
61
|
+
*/
|
|
62
|
+
export declare function searchQueryProps({ query, results, surface, }: SearchQueryInput): Record<string, string> | null;
|
|
41
63
|
/** The hostname of a site URL, for the `domain` a Plausible property is filed under. */
|
|
42
64
|
export declare function plausibleDomain(siteUrl: string | undefined, fallback: string): string;
|
|
43
65
|
//# sourceMappingURL=analytics.d.ts.map
|
package/dist/analytics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,OAAO,EAAE,YAAY,EACrB,KAAK,GAAE,cAAmB,GACzB,OAAO,CAAC,OAAO,CAAC,CAiBlB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE;IAAE,OAAO,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAC/C,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,OAAO,EACP,OAAO,GACR,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAUlD;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMrF"}
|
package/dist/analytics.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// analytics.ts — server-side Plausible events for agent
|
|
1
|
+
// analytics.ts — server-side Plausible events for a wiki's agent and human lanes.
|
|
2
2
|
//
|
|
3
3
|
// The proxy counts an "AI Bot Visit" by matching user agents, which cannot see
|
|
4
4
|
// inside a JSON-RPC envelope. So an MCP call is only countable by tool name at
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
// batch — is the part worth sharing. Every wiki in the workspace had its own
|
|
7
7
|
// copy of it, differing only in the default hostname.
|
|
8
8
|
//
|
|
9
|
+
// `searchQueryProps` is the human-side twin: the same shape problem, from the
|
|
10
|
+
// other direction. A search box is untrusted free text on every wiki, and the
|
|
11
|
+
// normalisation that makes it aggregatable — and keeps an empty field from
|
|
12
|
+
// firing an event — is the part worth sharing rather than re-deriving.
|
|
13
|
+
//
|
|
9
14
|
// Deferral stays with the caller: `after()` in a route handler,
|
|
10
15
|
// `event.waitUntil` in a proxy. Both are framework calls, and importing one
|
|
11
16
|
// here would cost this package its zero-dependency guarantee.
|
|
@@ -49,6 +54,31 @@ export function mcpCallProps(request, body, server) {
|
|
|
49
54
|
const ua = (request.headers.get('user-agent') || 'unknown').slice(0, 80);
|
|
50
55
|
return { ...(server ? { server } : {}), method, ...(tool ? { tool } : {}), ua };
|
|
51
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Props for a "Search Query" event — the human-side twin of `mcpCallProps`.
|
|
59
|
+
*
|
|
60
|
+
* A wiki instruments its agent surface and then counts every question an agent
|
|
61
|
+
* asks while recording none of the questions a person asks. The queries that
|
|
62
|
+
* return nothing are the valuable ones: they name a gap in the corpus in the
|
|
63
|
+
* reader's own words, which is otherwise only ever guessed at.
|
|
64
|
+
*
|
|
65
|
+
* Returns `null` for a query with no content, so an empty field cannot fire an
|
|
66
|
+
* event. The text is untrusted, so it is collapsed, lowercased for aggregation
|
|
67
|
+
* and truncated — the same 64-character bound search itself applies, which
|
|
68
|
+
* keeps the prop and the query that produced it the same string.
|
|
69
|
+
*/
|
|
70
|
+
export function searchQueryProps({ query, results, surface, }) {
|
|
71
|
+
const q = (query ?? '').replace(/\s+/g, ' ').trim().toLowerCase().slice(0, 64);
|
|
72
|
+
if (!q)
|
|
73
|
+
return null;
|
|
74
|
+
return {
|
|
75
|
+
q,
|
|
76
|
+
// A string because Plausible props are strings; `results == "0"` is the
|
|
77
|
+
// filter that yields the gap list.
|
|
78
|
+
results: String(Math.max(0, Math.trunc(results) || 0)),
|
|
79
|
+
...(surface ? { surface } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
52
82
|
/** The hostname of a site URL, for the `domain` a Plausible property is filed under. */
|
|
53
83
|
export function plausibleDomain(siteUrl, fallback) {
|
|
54
84
|
try {
|
package/dist/analytics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface CopyButtonOptions {
|
|
2
|
+
/** Class on the injected button. Style it in your own stylesheet. */
|
|
3
|
+
className?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
/** How long the tick shows before reverting to the copy glyph, in ms. */
|
|
6
|
+
revertAfter?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Give every `<pre>` under `root` a copy button, once.
|
|
10
|
+
*
|
|
11
|
+
* Idempotent by inspection rather than by selector: both wikis called this
|
|
12
|
+
* through `pre:not(:has(.code-copy-btn))`, which puts the guard at the call
|
|
13
|
+
* site — where it can be, and was, retyped. Checking inside means a caller
|
|
14
|
+
* that passes a plain `pre` selector still cannot double up, and it drops the
|
|
15
|
+
* `:has()` dependency along the way.
|
|
16
|
+
*
|
|
17
|
+
* Returns the number of buttons added, so a caller can skip work when zero.
|
|
18
|
+
*/
|
|
19
|
+
export declare function addCopyButtons(root: ParentNode, options?: CopyButtonOptions): number;
|
|
20
|
+
/**
|
|
21
|
+
* The one place this origin is written down. It is both the embed host and the
|
|
22
|
+
* allow-list the resize listener checks, and it was previously spelled out at
|
|
23
|
+
* four call sites across the two wikis.
|
|
24
|
+
*/
|
|
25
|
+
export declare const TWITTER_ORIGIN = "https://platform.twitter.com";
|
|
26
|
+
/** The embed iframe's src. `dnt=true` opts the embed out of Twitter's tracking. */
|
|
27
|
+
export declare const tweetEmbedSrc: (tweetId: string | number) => string;
|
|
28
|
+
/**
|
|
29
|
+
* Subscribe to the height the embed posts back once it has laid itself out.
|
|
30
|
+
*
|
|
31
|
+
* The iframe renders at an unknown height and there is no other way to learn
|
|
32
|
+
* it. The caller decides which iframes the height applies to; this only owns
|
|
33
|
+
* the origin check and the message shape. Returns a disposer.
|
|
34
|
+
*/
|
|
35
|
+
export declare function onTweetResize(resize: (height: number) => void): () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Point every un-hydrated `[data-twitter-embed]` placeholder under `root` at
|
|
38
|
+
* the real embed URL.
|
|
39
|
+
*
|
|
40
|
+
* Stored article HTML carries the placeholder markup; the iframe only gets a
|
|
41
|
+
* live `src` once it is in the document. Marked with `data-init` so a re-render
|
|
42
|
+
* of the same content does not reload every embed on the page.
|
|
43
|
+
*/
|
|
44
|
+
export declare function hydrateTweetEmbeds(root: ParentNode): void;
|
|
45
|
+
/** Apply a measured height to every embed iframe under `root`. */
|
|
46
|
+
export declare function sizeTweetEmbeds(root: ParentNode, height: number): void;
|
|
47
|
+
export interface TabGroupClassNames {
|
|
48
|
+
list?: string;
|
|
49
|
+
panels?: string;
|
|
50
|
+
button?: string;
|
|
51
|
+
panel?: string;
|
|
52
|
+
/** Added to the open button and its panel. */
|
|
53
|
+
active?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Turn stored `[data-tabs]` markup into a working tab group.
|
|
57
|
+
*
|
|
58
|
+
* The editor stores tabs as nested `[data-tab-item]` divs, which is the right
|
|
59
|
+
* thing to persist — it survives a markdown twin, a plain-HTML render and a
|
|
60
|
+
* reader with JavaScript off, which all show every tab's content in order.
|
|
61
|
+
* Making one of them pressable is a reader-side job, and it belongs beside the
|
|
62
|
+
* other passes rather than inside a component: it is the same shape as the copy
|
|
63
|
+
* buttons, and the second wiki to render tabs would otherwise write it again.
|
|
64
|
+
*
|
|
65
|
+
* Idempotent via `data-tabs-init`, so a re-render does not rebuild the group and
|
|
66
|
+
* silently reset it to the first tab.
|
|
67
|
+
*/
|
|
68
|
+
export declare function activateTabGroups(root: ParentNode, classNames?: TabGroupClassNames): void;
|
|
69
|
+
//# sourceMappingURL=dom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAgCxF;AAID;;;;GAIG;AACH,eAAO,MAAM,cAAc,iCAAiC,CAAC;AAE7D,mFAAmF;AACnF,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,GAAG,MAAM,KAAG,MACI,CAAC;AAE9D;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAU1E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAazD;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAItE;AAID,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,GAAE,kBAAuB,GAAG,IAAI,CAkD7F"}
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// dom.ts — the imperative passes a rendered article needs after it is in the DOM.
|
|
2
|
+
//
|
|
3
|
+
// Not in `react.tsx`, because none of this is React: it runs against an element
|
|
4
|
+
// a `dangerouslySetInnerHTML` just filled, and both wikis call it from an effect
|
|
5
|
+
// they already had. Keeping it out of the `'use client'` file also means a
|
|
6
|
+
// consumer can call it from anywhere it has an element.
|
|
7
|
+
//
|
|
8
|
+
// Both routines were duplicated. `addCopyButton` was BYTE-IDENTICAL in the two
|
|
9
|
+
// BlockRenderers, down to the SVG path data. The Twitter embed was worse: one
|
|
10
|
+
// wiki had extracted it to a module, the other wrote the origin allow-list and
|
|
11
|
+
// the embed URL out at three separate call sites — a duplicated origin check is
|
|
12
|
+
// the kind that goes stale quietly.
|
|
13
|
+
// ---- copy buttons -----------------------------------------------------------
|
|
14
|
+
const COPY_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
15
|
+
const CHECK_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
16
|
+
/**
|
|
17
|
+
* Give every `<pre>` under `root` a copy button, once.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent by inspection rather than by selector: both wikis called this
|
|
20
|
+
* through `pre:not(:has(.code-copy-btn))`, which puts the guard at the call
|
|
21
|
+
* site — where it can be, and was, retyped. Checking inside means a caller
|
|
22
|
+
* that passes a plain `pre` selector still cannot double up, and it drops the
|
|
23
|
+
* `:has()` dependency along the way.
|
|
24
|
+
*
|
|
25
|
+
* Returns the number of buttons added, so a caller can skip work when zero.
|
|
26
|
+
*/
|
|
27
|
+
export function addCopyButtons(root, options = {}) {
|
|
28
|
+
const { className = 'code-copy-btn', label = 'Copy code', revertAfter = 2000 } = options;
|
|
29
|
+
let added = 0;
|
|
30
|
+
for (const pre of Array.from(root.querySelectorAll('pre'))) {
|
|
31
|
+
if (pre.querySelector(`.${className}`))
|
|
32
|
+
continue;
|
|
33
|
+
const btn = document.createElement('button');
|
|
34
|
+
btn.type = 'button';
|
|
35
|
+
btn.className = className;
|
|
36
|
+
btn.setAttribute('aria-label', label);
|
|
37
|
+
btn.innerHTML = COPY_SVG;
|
|
38
|
+
btn.onclick = () => {
|
|
39
|
+
const code = pre.querySelector('code')?.textContent || pre.textContent || '';
|
|
40
|
+
// A rejected clipboard write (permissions, insecure origin) must not
|
|
41
|
+
// leave an unhandled rejection behind — the button simply does nothing.
|
|
42
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
43
|
+
btn.innerHTML = CHECK_SVG;
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
btn.innerHTML = COPY_SVG;
|
|
46
|
+
}, revertAfter);
|
|
47
|
+
}, () => { });
|
|
48
|
+
};
|
|
49
|
+
pre.style.position = 'relative';
|
|
50
|
+
pre.appendChild(btn);
|
|
51
|
+
added++;
|
|
52
|
+
}
|
|
53
|
+
return added;
|
|
54
|
+
}
|
|
55
|
+
// ---- twitter embeds ---------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* The one place this origin is written down. It is both the embed host and the
|
|
58
|
+
* allow-list the resize listener checks, and it was previously spelled out at
|
|
59
|
+
* four call sites across the two wikis.
|
|
60
|
+
*/
|
|
61
|
+
export const TWITTER_ORIGIN = 'https://platform.twitter.com';
|
|
62
|
+
/** The embed iframe's src. `dnt=true` opts the embed out of Twitter's tracking. */
|
|
63
|
+
export const tweetEmbedSrc = (tweetId) => `${TWITTER_ORIGIN}/embed/Tweet.html?id=${tweetId}&dnt=true`;
|
|
64
|
+
/**
|
|
65
|
+
* Subscribe to the height the embed posts back once it has laid itself out.
|
|
66
|
+
*
|
|
67
|
+
* The iframe renders at an unknown height and there is no other way to learn
|
|
68
|
+
* it. The caller decides which iframes the height applies to; this only owns
|
|
69
|
+
* the origin check and the message shape. Returns a disposer.
|
|
70
|
+
*/
|
|
71
|
+
export function onTweetResize(resize) {
|
|
72
|
+
const handleMessage = (e) => {
|
|
73
|
+
if (e.origin !== TWITTER_ORIGIN)
|
|
74
|
+
return;
|
|
75
|
+
const data = e.data?.['twttr.embed'];
|
|
76
|
+
if (data?.method !== 'twttr.private.resize')
|
|
77
|
+
return;
|
|
78
|
+
const height = data.params?.[0]?.height;
|
|
79
|
+
if (height)
|
|
80
|
+
resize(height);
|
|
81
|
+
};
|
|
82
|
+
window.addEventListener('message', handleMessage);
|
|
83
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Point every un-hydrated `[data-twitter-embed]` placeholder under `root` at
|
|
87
|
+
* the real embed URL.
|
|
88
|
+
*
|
|
89
|
+
* Stored article HTML carries the placeholder markup; the iframe only gets a
|
|
90
|
+
* live `src` once it is in the document. Marked with `data-init` so a re-render
|
|
91
|
+
* of the same content does not reload every embed on the page.
|
|
92
|
+
*/
|
|
93
|
+
export function hydrateTweetEmbeds(root) {
|
|
94
|
+
for (const container of Array.from(root.querySelectorAll('[data-twitter-embed]:not([data-init])'))) {
|
|
95
|
+
container.setAttribute('data-init', '');
|
|
96
|
+
const tweetId = container.getAttribute('data-tweet-id');
|
|
97
|
+
if (!tweetId)
|
|
98
|
+
continue;
|
|
99
|
+
const iframe = container.querySelector('iframe');
|
|
100
|
+
if (iframe) {
|
|
101
|
+
iframe.src = tweetEmbedSrc(tweetId);
|
|
102
|
+
iframe.setAttribute('scrolling', 'no');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Apply a measured height to every embed iframe under `root`. */
|
|
107
|
+
export function sizeTweetEmbeds(root, height) {
|
|
108
|
+
for (const iframe of Array.from(root.querySelectorAll('[data-twitter-embed] iframe'))) {
|
|
109
|
+
iframe.style.height = `${height}px`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Turn stored `[data-tabs]` markup into a working tab group.
|
|
114
|
+
*
|
|
115
|
+
* The editor stores tabs as nested `[data-tab-item]` divs, which is the right
|
|
116
|
+
* thing to persist — it survives a markdown twin, a plain-HTML render and a
|
|
117
|
+
* reader with JavaScript off, which all show every tab's content in order.
|
|
118
|
+
* Making one of them pressable is a reader-side job, and it belongs beside the
|
|
119
|
+
* other passes rather than inside a component: it is the same shape as the copy
|
|
120
|
+
* buttons, and the second wiki to render tabs would otherwise write it again.
|
|
121
|
+
*
|
|
122
|
+
* Idempotent via `data-tabs-init`, so a re-render does not rebuild the group and
|
|
123
|
+
* silently reset it to the first tab.
|
|
124
|
+
*/
|
|
125
|
+
export function activateTabGroups(root, classNames = {}) {
|
|
126
|
+
const { list = 'tabs-list', panels = 'tabs-panels', button = 'tab-button', panel = 'tab-panel', active = 'active', } = classNames;
|
|
127
|
+
for (const group of Array.from(root.querySelectorAll('[data-tabs]:not([data-tabs-init])'))) {
|
|
128
|
+
const items = group.querySelectorAll('[data-tab-item]');
|
|
129
|
+
if (!items.length)
|
|
130
|
+
continue;
|
|
131
|
+
group.setAttribute('data-tabs-init', '');
|
|
132
|
+
const tabList = document.createElement('div');
|
|
133
|
+
tabList.className = list;
|
|
134
|
+
tabList.setAttribute('role', 'tablist');
|
|
135
|
+
const tabPanels = document.createElement('div');
|
|
136
|
+
tabPanels.className = panels;
|
|
137
|
+
items.forEach((item, i) => {
|
|
138
|
+
const btn = document.createElement('button');
|
|
139
|
+
btn.type = 'button';
|
|
140
|
+
btn.className = i === 0 ? `${button} ${active}` : button;
|
|
141
|
+
btn.setAttribute('role', 'tab');
|
|
142
|
+
btn.setAttribute('aria-selected', String(i === 0));
|
|
143
|
+
btn.textContent = item.getAttribute('data-tab-title') || `Tab ${i + 1}`;
|
|
144
|
+
btn.onclick = () => {
|
|
145
|
+
for (const b of Array.from(tabList.children)) {
|
|
146
|
+
b.classList.remove(active);
|
|
147
|
+
b.setAttribute('aria-selected', 'false');
|
|
148
|
+
}
|
|
149
|
+
for (const p of Array.from(tabPanels.children))
|
|
150
|
+
p.classList.remove(active);
|
|
151
|
+
btn.classList.add(active);
|
|
152
|
+
btn.setAttribute('aria-selected', 'true');
|
|
153
|
+
tabPanels.children[i]?.classList.add(active);
|
|
154
|
+
};
|
|
155
|
+
tabList.appendChild(btn);
|
|
156
|
+
const body = document.createElement('div');
|
|
157
|
+
body.className = i === 0 ? `${panel} ${active}` : panel;
|
|
158
|
+
body.setAttribute('role', 'tabpanel');
|
|
159
|
+
body.innerHTML = item.innerHTML;
|
|
160
|
+
tabPanels.appendChild(body);
|
|
161
|
+
});
|
|
162
|
+
group.innerHTML = '';
|
|
163
|
+
group.appendChild(tabList);
|
|
164
|
+
group.appendChild(tabPanels);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=dom.js.map
|
package/dist/dom.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"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"}
|