wiki-formant 0.21.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 +72 -4
- package/dist/base.css +100 -0
- package/dist/block-views.d.ts +43 -18
- package/dist/block-views.js +36 -22
- package/dist/blocks.d.ts +26 -3
- package/dist/blocks.js +19 -5
- package/dist/conformance.d.ts +14 -2
- package/dist/conformance.js +41 -13
- package/dist/crawlers.d.ts +8 -1
- package/dist/crawlers.js +14 -1
- package/dist/editor.d.ts +38 -1
- package/dist/editor.js +82 -0
- package/dist/feed.d.ts +10 -0
- package/dist/feed.js +20 -2
- package/dist/freshness.d.ts +47 -0
- package/dist/freshness.js +66 -0
- package/dist/headings.d.ts +14 -0
- package/dist/headings.js +27 -11
- package/dist/http.d.ts +28 -1
- package/dist/http.js +29 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/license.d.ts +9 -0
- package/dist/license.js +8 -0
- package/dist/link-check.js +3 -1
- package/dist/metadata.d.ts +123 -0
- package/dist/metadata.js +109 -0
- package/dist/react-server.d.ts +85 -2
- package/dist/react-server.js +83 -3
- package/dist/react.d.ts +94 -2
- package/dist/react.js +159 -0
- package/dist/sanitize.d.ts +5 -7
- package/dist/sanitize.js +5 -9
- package/dist/search.d.ts +20 -0
- package/dist/search.js +22 -0
- package/dist/text.d.ts +2 -0
- package/dist/text.js +4 -0
- package/dist/tiptap.d.ts +14 -1
- package/dist/tiptap.js +41 -1
- package/dist/well-known.d.ts +47 -8
- package/dist/well-known.js +58 -2
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -78,7 +78,12 @@ const toc = headingsFrom(html); // [{ id, text, level }]
|
|
|
78
78
|
Two behaviours worth knowing:
|
|
79
79
|
|
|
80
80
|
- **The slug rule is a parameter.** A heading id is a live URL — readers link to `#the-shape-of-a-code`, and so does the page's own permalink anchor. The two wikis this was lifted from had drifted onto different rules, and unifying them would have silently moved every published anchor on whichever one lost. Pass `slug` to keep the rule you already ship.
|
|
81
|
-
- **Deduping is not a parameter.** Two headings with the same text otherwise mint the same id twice, and every link to the second lands on the first. The
|
|
81
|
+
- **Deduping is not a parameter, but its scope is.** Two headings with the same text otherwise mint the same id twice, and every link to the second lands on the first. The unit that must be unique is the page, so a block wiki that injects block by block passes one `used` set to every call — seeded with any id the template renders itself, such as the title's. All three consumers once passed none, and a heading repeated across two blocks shipped two identical ids.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const used = new Set([slug(page.title)]);
|
|
85
|
+
const blocks = page.blocks.map(b => ({ ...b, text: injectHeadingIds(b.text, { used }) }));
|
|
86
|
+
```
|
|
82
87
|
|
|
83
88
|
`headingsFrom` reads the string, not the rendered DOM — possible only where the body IS a string at render time. A wiki whose content streams in as blocks after mount has to query the DOM, and uses only the injector.
|
|
84
89
|
|
|
@@ -165,6 +170,8 @@ process.exit(t.summary());
|
|
|
165
170
|
|
|
166
171
|
What stays in your repo is fixtures: which tools you expect, what a good answer from each looks like, and which text surfaces you publish. `payloadBudget` weighs every listed call **and** every read-only tool that takes no required arguments, because the one tool nobody thought to list is the one that answers with 3.3 MB; pass a per-call `maxBytes` for a bulk-export tool that is deliberately large. It also asserts that a JSON answer arrived with `structuredContent`.
|
|
167
172
|
|
|
173
|
+
`descriptorChecks(t)` defaults to `S10_DESCRIPTORS`, the six JSON descriptors every surface here serves, and `distinctEtagChecks(t, ['llms.txt', 'llms-index.txt', 'llms-full.txt'])` asserts that depths projecting one corpus carry different ETags — every one present, which a bare Set-size check misses.
|
|
174
|
+
|
|
168
175
|
## Markdown twins
|
|
169
176
|
|
|
170
177
|
Pass an `etag` and answer `notModified` before rendering: a twin is the single most recrawled URL a page has, so a twin with no validator is a full render on every pass, forever — the same arithmetic that justifies the corpus ETag, applied per page.
|
|
@@ -192,6 +199,8 @@ Block trees stay in your app — every project owns its own type set. Give this
|
|
|
192
199
|
|
|
193
200
|
The `llms.txt` / `llms-index.txt` / `llms-full.txt` trio are the most-recrawled URLs a wiki serves and the most expensive to render. Without a corpus-derived ETag, every AI crawler pays full price on every pass, forever.
|
|
194
201
|
|
|
202
|
+
A URL that answers JSON or markdown by `Accept` asks `wantsMarkdown(request)` and sends `VARY_ACCEPT` on both branches. Without the Vary, a shared cache hands one client the other's format.
|
|
203
|
+
|
|
195
204
|
```ts
|
|
196
205
|
import { corpusEtag, notModified, textHeaders } from 'wiki-formant/http';
|
|
197
206
|
|
|
@@ -265,16 +274,23 @@ const { inputProps, listProps, optionProps } = combobox();
|
|
|
265
274
|
The arithmetic is `wiki-formant/combobox`, which imports nothing, so the rules
|
|
266
275
|
are unit-tested without a DOM.
|
|
267
276
|
|
|
277
|
+
`SortHeader` is the React half of `sortTables`: spread `useTableSort`'s `headerProps(key)` onto it and it renders a `<th aria-sort>` holding a `<button>`, the markup `sortTables` writes into stored tables, so one stylesheet rule draws both arrows. One of the copies it replaced put `onClick` on the cell, which no keyboard can reach.
|
|
278
|
+
|
|
279
|
+
`useBlockOperations(blocks, setBlocks, { duplicate })` is the five list edits every block editor makes, with ref-stable callbacks, and `BlockActions` is their button bar — every button named, the moves disabled at the ends, clicks kept from bubbling into a row that selects on click. `useRevisionRestore(restore, { onRestored })` confirms, restores and reports an error; `restoreViaPost(endpoint)` is the `restore` for a wiki whose route takes `{ revisionId }`. Of the three restores it replaced, one asked no confirmation and one dropped a failure silently.
|
|
280
|
+
|
|
268
281
|
## Server components
|
|
269
282
|
|
|
270
283
|
`wiki-formant/react` carries `'use client'`, and that is a module-level boundary: anything exported from it hydrates in the consumer's tree whether or not it uses a hook. `wiki-formant/react-server` is the same React, without the directive — for the parts of a wiki that are pure functions of their props and should ship no JavaScript at all.
|
|
271
284
|
|
|
272
|
-
`FacetBar` renders the rows `createTaxonomy` already produces. This is why the taxonomy exports a rows model rather than markup: the rows could always cross the boundary and, until this subpath existed, the markup could not, so all three wikis hand-rendered it and two put `aria-pressed` on an `<a>`. `Breadcrumbs` renders the trail and its `BreadcrumbList` JSON-LD together, because a trail whose structured data is written somewhere else is a trail that will one day disagree with its own markup — which is the case Google penalises. It takes a `base` origin: structured-data URLs must be absolute and a package cannot know the site.
|
|
285
|
+
`FacetBar` renders the rows `createTaxonomy` already produces. This is why the taxonomy exports a rows model rather than markup: the rows could always cross the boundary and, until this subpath existed, the markup could not, so all three wikis hand-rendered it and two put `aria-pressed` on an `<a>`. `Breadcrumbs` renders the trail and its `BreadcrumbList` JSON-LD together, because a trail whose structured data is written somewhere else is a trail that will one day disagree with its own markup — which is the case Google penalises. It takes a `base` origin: structured-data URLs must be absolute and a package cannot know the site.
|
|
286
|
+
`FacetBar` takes a `count` class to render each count as its own element, and `alphaFirst` to lead with the A–Z row. Its JSON-LD goes out through `JsonLd`, which is exported for every other payload a page emits: it re-encodes each `<` as `\u003c`, because these payloads carry authored titles and an authored `</script>` would otherwise close the tag.
|
|
273
287
|
|
|
274
288
|
`PageNav` is the previous/next pair at the foot of an article — the sequential read the infobox rail's lateral links do not cover. Ordering is the caller's, because it is the one part that is never portable: a wiki's sequence is its section's configured sort, a knowledge base's is a taxonomy walk. Pair it with `adjacentPages` from `wiki-formant/pagination` over a list you already hold — neither wiki needs a query for it, and the two indexed lookups the neighbours used to cost were the reason one of them dropped the control.
|
|
275
289
|
|
|
276
290
|
`RailShell` stays in `wiki-formant/react`, because it calls `useSidebar` and genuinely is a client component. It renders the rail's landmark, its scroll wrapper and the three collapse states — including the `--instant` class that keeps a remembered-closed rail from animating shut on first paint, and the close-on-tap that a mobile rail needs. Compose your own rail inside it and mark the active link with `isRailLinkActive`; all three wikis do, and the pre-composed component that used to sit here had one consumer and had already lost both of those behaviours.
|
|
277
291
|
|
|
292
|
+
`FacetSummary` is the line under the facet bar — how much of the section is showing, and a Clear link when anything is narrowed — with the filtered-empty and empty-section states. `RelatedPages` is the see-also list: a labelled `<aside>` over a `<ul>`, its heading linked to the set `rankRelated` found in common. `InfoboxAside` is the facts panel's named landmark with its "Part of a series on" line, and `InfoboxFacts` renders `metadataRows` as a row-headed table. That table was an HTML string in two wikis, injected as a fake content block; one re-escaped a `"` before each href by hand and the other did not. As React nodes there is nothing to escape. `formatFactValue` is its default cell: a date as its day, a URL or bare domain as an outbound link, `<br>`-separated values as lines.
|
|
293
|
+
|
|
278
294
|
All three take the router's link component as a prop:
|
|
279
295
|
|
|
280
296
|
```tsx
|
|
@@ -314,6 +330,25 @@ and a crawler obeys only its most-specific matching group, so an agent with no
|
|
|
314
330
|
group of its own falls through to `*`. Three wikis were measuring five crawlers
|
|
315
331
|
they had never addressed.
|
|
316
332
|
|
|
333
|
+
Every group also allows `AGENT_SURFACE_PATHS` — `/api/mcp`, the three `llms` exports, `/openapi.json`, `/.well-known/` — without being told. `aiAllow` is what an origin serves beyond that.
|
|
334
|
+
|
|
335
|
+
## Page metadata
|
|
336
|
+
|
|
337
|
+
`wiki-formant/metadata`'s `pageMetadata` builds a page's canonical, markdown-twin alternate, Open Graph and Twitter card from one input. Next replaces those objects per route segment rather than merging them, and fills a missing twitter card from `openGraph` only when no `twitter` object was inherited — so under a layout that sets its own card, a page that sets one and forgets the other shows the layout's. Two wikis wrote a helper around that, covering different halves.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
export const generateMetadata = () => ({
|
|
341
|
+
title,
|
|
342
|
+
...pageMetadata({ title, description, url, type: 'article', image: ogImageUrl(title), siteName, handle, markdownTwin: true }),
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The schema.org nodes sit beside it, because they state the same facts to a different reader. `articleLd` requires `image` — two of three wikis shipped articles without one while their og:image named a card — so pass it the card `pageMetadata` got. `collectionLd` is an index page over an `ItemList`, and `citationsFromReferences` turns a `references` block into `Article.citation`.
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
<JsonLd data={articleLd({ headline: title, url, image, published, modified, publisher: { '@id': `${SITE_URL}/#organization` }, citation: citationsFromReferences(refs) })} />
|
|
350
|
+
```
|
|
351
|
+
|
|
317
352
|
## Revisions
|
|
318
353
|
|
|
319
354
|
What changed between two versions of a page, and therefore which semver bump to
|
|
@@ -348,9 +383,11 @@ verbatim in two of them, the comment explaining why the apostrophe escapes
|
|
|
348
383
|
numerically. `renderFeed` is the union.
|
|
349
384
|
|
|
350
385
|
```ts
|
|
351
|
-
|
|
386
|
+
export const GET = async (request: Request) => feedResponse(request, channel, await items());
|
|
352
387
|
```
|
|
353
388
|
|
|
389
|
+
`feedResponse` renders the channel and answers a conditional GET: the ETag is the XML's own, Last-Modified the build date. All four feeds used to return the XML bare, so a poller could never be told the channel was unchanged.
|
|
390
|
+
|
|
354
391
|
**`lastBuildDate` comes from the newest item, not the clock.** One copy stamped
|
|
355
392
|
`new Date()` on every request, telling every poller the feed had changed when it
|
|
356
393
|
had not — the recrawl the conditional-GET helpers exist to prevent. An empty feed
|
|
@@ -358,6 +395,7 @@ carries no build date rather than a fictional one.
|
|
|
358
395
|
|
|
359
396
|
## Licence declarations
|
|
360
397
|
|
|
398
|
+
|
|
361
399
|
S10 wants a licence on every surface, and each repo satisfied that by writing the
|
|
362
400
|
same block again. What is genuinely per-project is the *scope* — which half of a
|
|
363
401
|
site the grant covers and what it excludes — so that is the parameter.
|
|
@@ -367,6 +405,8 @@ const license = ccBy40({ siteName: 'AcuiQ', siteUrl: SITE_URL });
|
|
|
367
405
|
const block = licenseBlock({ license, scope: 'The protocol compilation and prose', excludes });
|
|
368
406
|
```
|
|
369
407
|
|
|
408
|
+
The same `License` goes to `agentCard({ license, licenseScope })` and to `openApiLicense(license)` for a spec's `info.license`. The three cards and three specs here had each projected it differently, one from a hand-typed name.
|
|
409
|
+
|
|
370
410
|
## Rendered-article passes
|
|
371
411
|
|
|
372
412
|
`wiki-formant/dom` holds what runs against an article element after it is in the document. Not React, so not in `react.tsx`.
|
|
@@ -380,12 +420,18 @@ const off = onTweetResize(h => sizeTweetEmbeds(el, h));
|
|
|
380
420
|
|
|
381
421
|
`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.
|
|
382
422
|
|
|
423
|
+
In React, `useArticlePasses(ref, [blocks])` runs the three passes and `useTweetEmbeds(ref, [html])` the embed pair, from `wiki-formant/react`. 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.
|
|
424
|
+
|
|
383
425
|
`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.
|
|
384
426
|
|
|
385
427
|
`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.
|
|
386
428
|
|
|
387
429
|
`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.
|
|
388
430
|
|
|
431
|
+
## Search
|
|
432
|
+
|
|
433
|
+
`proseSql`, `searchTsvSql`, `HEADLINE_OPTIONS` and `FTS_RANK_NORMALIZATION` are what a literal tier and a full-text tier must agree on. `searchTsvDdl(table)` is the statements that build the generated `search_tsv` column and its index — dropped and re-added in one transaction every run, because a skip-if-present script is blind to a changed expression.
|
|
434
|
+
|
|
389
435
|
## Block trees
|
|
390
436
|
|
|
391
437
|
Every wiki here stores the same two containers the same way — `columns[i].blocks` and an infobox's `blocks` — and between them had hand-written that walk seven times. Bind it once and hand it to every walk:
|
|
@@ -410,9 +456,17 @@ if (issues.length) return badRequest(`Invalid content: ${describeBlockIssues(iss
|
|
|
410
456
|
// Invalid content: [2].columns[0].blocks[1]: malformed linkGrid block
|
|
411
457
|
```
|
|
412
458
|
|
|
459
|
+
A `codeTabs` tab's `code` is source text in every wiki here, and every view treats it that way: `CodeTabsView` escapes it into a `<pre><code>`, the sanitiser leaves it alone, the markdown twin fences it verbatim. A wiki that escapes and highlights on the server passes `highlighted` to the view. The package used to assume editors stored highlighted markup; none ever had, and every twin printed `Vec<u8>` as `Vec`.
|
|
460
|
+
|
|
461
|
+
`recentPages` and `pageList` resolve to `ResolvedPageRef`s — build each with `pageRef(page, now)`, which carries the ISO date a `<time>` needs — and `PageRefsView` renders them as a list, with a `renderItem` slot for a wiki that has its own row.
|
|
462
|
+
|
|
463
|
+
## Dates
|
|
464
|
+
|
|
465
|
+
`relativeTime(then, now, { style })` is `compact` (`3h`), `short` (`3h ago`) or `long` (`3 days ago`, day-grained for pages cached for hours), with `absoluteAfterDays` to hand over to the date itself. `now` is always passed in, never read, so a server render and its hydration agree. `formatDay` is always UTC: a stored day formatted in the server's zone, or the browser's, is the previous day for half the world. Both are in `wiki-formant/freshness`, with `isoDate`.
|
|
466
|
+
|
|
413
467
|
## Sanitising stored HTML
|
|
414
468
|
|
|
415
|
-
`wiki-formant/sanitize` is the allowlist between an author's saved HTML and a reader's browser. The block views render
|
|
469
|
+
`wiki-formant/sanitize` is the allowlist between an author's saved HTML and a reader's browser. The block views render two HTML fields themselves — `linkGrid` descriptions and `references` text — and a repo renders `content.text` beside them, so the guard ships with the renderer. `codeTabs` is not one of them: its code is source. `sanitize-html` is an optional peer; run it server-side, on the render path, so it covers rows written before it existed and no sanitiser ships to the client.
|
|
416
470
|
|
|
417
471
|
```ts
|
|
418
472
|
import { createHtmlSanitizer, sanitizeCoreLeaf } from 'wiki-formant/sanitize';
|
|
@@ -441,6 +495,10 @@ The ones that take config take it because that is exactly where the two copies d
|
|
|
441
495
|
|
|
442
496
|
`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.
|
|
443
497
|
|
|
498
|
+
`createHeadingIds({ slug })` decorates each heading in the editor with the id its published copy will carry, through `uniqueHeadingId` — the dedupe `injectHeadingIds` uses — so a rail listing headings mid-edit links to the anchors readers will get. `uploadImageTo('/api/upload')` is the `uploadImage` two editors had written identically.
|
|
499
|
+
|
|
500
|
+
`TOOLBAR_ACTIONS` are the formatting commands with the labels a screen reader announces — two toolbars titled buttons with the internal key, so one said "codeBlock". Pick yours in order with `toolbarActions([...])`, render each through `ToolbarButton` (named, and `aria-pressed`, since these are toggles), and run a table command with `runTableAction`. Upload and embed stay with the caller; they are the parts that differ.
|
|
501
|
+
|
|
444
502
|
The redirect resolves through the wiki's own route, because the editor cannot read it cross-origin. `resolveMapUrl` is the client half and `resolveMapHandler` the whole route: exact shortener hosts, one hop, an allowlisted landing host, a timeout, and your sign-in check as `authorize`. One of the two copies it replaced matched `goo.gl` as a substring and followed every redirect for anyone who asked.
|
|
445
503
|
|
|
446
504
|
```ts
|
|
@@ -471,6 +529,16 @@ leaves a wiki able to say what every crawler asked for and nothing about what
|
|
|
471
529
|
its readers asked for. The queries that return zero rows are the valuable ones —
|
|
472
530
|
they name a gap in the corpus in the reader's own words.
|
|
473
531
|
|
|
532
|
+
## Base stylesheet
|
|
533
|
+
|
|
534
|
+
`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.
|
|
535
|
+
|
|
536
|
+
```css
|
|
537
|
+
@import "wiki-formant/base.css" layer(components);
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
No view emits a Tailwind utility any more — `ColumnsView` states its gap and alignment as data attributes, and an inactive code tab is `hidden` — so a site no longer needs its Tailwind to scan this package for the markup to work.
|
|
541
|
+
|
|
474
542
|
## API
|
|
475
543
|
|
|
476
544
|
Every module has a subpath — `wiki-formant/taxonomy`, `wiki-formant/mcp`, and so on.
|
package/dist/base.css
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* wiki-formant/base.css — the layout the package's markup does not work without.
|
|
2
|
+
|
|
3
|
+
Structure only: no colour, no type scale, no design token. Every value that
|
|
4
|
+
is a look rather than a mechanism is left to the importing stylesheet, which
|
|
5
|
+
loads this first and overrides anything here with an equally specific rule.
|
|
6
|
+
|
|
7
|
+
@import "wiki-formant/base.css" layer(components);
|
|
8
|
+
|
|
9
|
+
Three wikis had each restated these rules, and each had one missing: one
|
|
10
|
+
emitted the stored-tab and copy-button markup with no CSS for it, so every
|
|
11
|
+
panel showed at once; one never received the utility classes ColumnsView
|
|
12
|
+
used to emit, so two of its four alignments did nothing. */
|
|
13
|
+
|
|
14
|
+
/* ---- ColumnsView ----------------------------------------------------------
|
|
15
|
+
Stacked until there is room for a row. `data-gap` and `data-align` replace
|
|
16
|
+
the Tailwind utilities the view used to emit, which only reached a site whose
|
|
17
|
+
Tailwind happened to scan node_modules. */
|
|
18
|
+
.columns-layout {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: var(--columns-gap, 1rem);
|
|
22
|
+
}
|
|
23
|
+
.columns-layout[data-gap='sm'] { --columns-gap: 0.5rem; }
|
|
24
|
+
.columns-layout[data-gap='lg'] { --columns-gap: 1.5rem; }
|
|
25
|
+
.columns-layout[data-align='start'] { align-items: flex-start; }
|
|
26
|
+
.columns-layout[data-align='center'] { align-items: center; }
|
|
27
|
+
.columns-layout[data-align='end'] { align-items: flex-end; }
|
|
28
|
+
.columns-layout[data-align='stretch'] { align-items: stretch; }
|
|
29
|
+
.column-view {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
min-width: 0;
|
|
33
|
+
}
|
|
34
|
+
@media (min-width: 48rem) {
|
|
35
|
+
.columns-layout { flex-direction: row; }
|
|
36
|
+
.column-view { flex: 1 1 0%; }
|
|
37
|
+
}
|
|
38
|
+
/* A stacked column is as wide as the row, whatever the alignment. */
|
|
39
|
+
@media (max-width: 47.99rem) {
|
|
40
|
+
.columns-layout > .column-view { align-self: stretch; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ---- stored tab groups (activateTabGroups) --------------------------------
|
|
44
|
+
Only the active panel shows. Before the script runs, the first tab's content
|
|
45
|
+
stands in for the group rather than every tab at once. */
|
|
46
|
+
.tab-panel:not(.active) { display: none; }
|
|
47
|
+
[data-tabs]:not([data-tabs-init]) [data-tab-item] ~ [data-tab-item] { display: none; }
|
|
48
|
+
|
|
49
|
+
/* ---- code copy buttons (addCopyButtons) -----------------------------------
|
|
50
|
+
The script positions the <pre>; this pins the button in its corner. Shown
|
|
51
|
+
on hover, on keyboard focus, and always where there is no hover to show it. */
|
|
52
|
+
.code-copy-btn {
|
|
53
|
+
position: absolute;
|
|
54
|
+
top: 0.5rem;
|
|
55
|
+
right: 0.5rem;
|
|
56
|
+
display: inline-flex;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
opacity: 0;
|
|
59
|
+
transition: opacity 120ms ease;
|
|
60
|
+
}
|
|
61
|
+
pre:hover > .code-copy-btn,
|
|
62
|
+
.code-copy-btn:focus-visible { opacity: 1; }
|
|
63
|
+
@media (hover: none) {
|
|
64
|
+
.code-copy-btn { opacity: 1; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ---- sortable headers (SortHeader, sortTables) ----------------------------
|
|
68
|
+
The arrow is read from `aria-sort` on the cell, so it cannot point one way
|
|
69
|
+
while the rows go the other. It is a mask over currentColor, so it takes the
|
|
70
|
+
header's colour, whatever that is. */
|
|
71
|
+
.sort-header {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 0.25em;
|
|
75
|
+
padding: 0;
|
|
76
|
+
border: 0;
|
|
77
|
+
border-radius: 0;
|
|
78
|
+
background: none;
|
|
79
|
+
color: inherit;
|
|
80
|
+
font: inherit;
|
|
81
|
+
text-align: inherit;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
.sort-header::after {
|
|
85
|
+
content: '';
|
|
86
|
+
flex: none;
|
|
87
|
+
width: 0.75em;
|
|
88
|
+
height: 0.75em;
|
|
89
|
+
background-color: currentColor;
|
|
90
|
+
opacity: 0.4;
|
|
91
|
+
--sort-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 15 5 5 5-5'/%3E%3Cpath d='m7 9 5-5 5 5'/%3E%3C/svg%3E");
|
|
92
|
+
mask: var(--sort-icon) center / contain no-repeat;
|
|
93
|
+
}
|
|
94
|
+
:is([aria-sort='ascending'], [aria-sort='descending']) > .sort-header::after { opacity: 1; }
|
|
95
|
+
[aria-sort='ascending'] > .sort-header::after {
|
|
96
|
+
--sort-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12 7-7 7 7'/%3E%3Cpath d='M12 19V5'/%3E%3C/svg%3E");
|
|
97
|
+
}
|
|
98
|
+
[aria-sort='descending'] > .sort-header::after {
|
|
99
|
+
--sort-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14'/%3E%3Cpath d='m19 12-7 7-7-7'/%3E%3C/svg%3E");
|
|
100
|
+
}
|
package/dist/block-views.d.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import type { CodeTab, LinkGridGroup, ReferenceItem, StatItem } from './blocks.js';
|
|
2
|
+
import type { CodeTab, LinkGridGroup, ReferenceItem, ResolvedPageRef, StatItem } from './blocks.js';
|
|
3
3
|
import type { WikiLinkComponent } from './react-server.js';
|
|
4
|
+
export interface CodeTabsViewProps {
|
|
5
|
+
tabs: readonly CodeTab[];
|
|
6
|
+
/**
|
|
7
|
+
* Every tab's `code` has already been escaped and highlighted into markup on
|
|
8
|
+
* the server, and is written as HTML. Only a wiki whose render path does that
|
|
9
|
+
* for every tree reaching this view may pass it: without it, `code` is source
|
|
10
|
+
* and renders escaped in a `<pre><code>`, which is the safe default.
|
|
11
|
+
*/
|
|
12
|
+
highlighted?: boolean;
|
|
13
|
+
}
|
|
4
14
|
/**
|
|
5
15
|
* Tabbed code samples. Every tab's body stays mounted and the inactive ones are
|
|
6
|
-
* hidden rather than unmounted:
|
|
7
|
-
*
|
|
8
|
-
* it on every tab press.
|
|
16
|
+
* hidden rather than unmounted: remounting a highlighted panel would re-run
|
|
17
|
+
* whatever the consumer's highlighter attached to it on every tab press.
|
|
9
18
|
*/
|
|
10
|
-
export declare function CodeTabsView({ tabs }:
|
|
11
|
-
tabs: readonly CodeTab[];
|
|
12
|
-
}): import("react").JSX.Element | null;
|
|
19
|
+
export declare function CodeTabsView({ tabs, highlighted }: CodeTabsViewProps): import("react").JSX.Element | null;
|
|
13
20
|
export interface ColumnsViewProps<B extends {
|
|
14
21
|
id: string;
|
|
15
22
|
}> {
|
|
@@ -29,6 +36,10 @@ export interface ColumnsViewProps<B extends {
|
|
|
29
36
|
* it was a layout bug rather than a style: `.column-view` is the flow context
|
|
30
37
|
* its stylesheet spaces children in, and an extra element between them means
|
|
31
38
|
* every gap rule inside a column silently matches nothing.
|
|
39
|
+
*
|
|
40
|
+
* Gap and alignment are data attributes that `wiki-formant/base.css` reads.
|
|
41
|
+
* They were Tailwind utilities, which reached a page only if the site's
|
|
42
|
+
* Tailwind scanned this package — and on one site `end` and `stretch` did not.
|
|
32
43
|
*/
|
|
33
44
|
export declare function ColumnsView<B extends {
|
|
34
45
|
id: string;
|
|
@@ -48,6 +59,23 @@ export declare function LinkGridView({ groups, intro, link: Link, }: {
|
|
|
48
59
|
intro?: string;
|
|
49
60
|
link?: WikiLinkComponent;
|
|
50
61
|
}): import("react").JSX.Element;
|
|
62
|
+
/**
|
|
63
|
+
* A list of resolved page references — the body of a `recentPages` or
|
|
64
|
+
* `pageList` block. A real list, with each age in a `<time>` carrying its ISO
|
|
65
|
+
* date: the copies this replaced were bare `<div>` rows, one emitted a
|
|
66
|
+
* `<time>` with no `dateTime`, and their empty states were three different
|
|
67
|
+
* things (a sentence, a different sentence, nothing).
|
|
68
|
+
*
|
|
69
|
+
* `renderItem` replaces the row's inner markup for a wiki with its own row
|
|
70
|
+
* component; the list, its semantics and the empty state stay here.
|
|
71
|
+
*/
|
|
72
|
+
export declare function PageRefsView({ refs, empty, link: Link, renderItem, }: {
|
|
73
|
+
refs: readonly ResolvedPageRef[];
|
|
74
|
+
/** Said when there are none. Omit to render nothing. */
|
|
75
|
+
empty?: string;
|
|
76
|
+
link?: WikiLinkComponent;
|
|
77
|
+
renderItem?: (ref: ResolvedPageRef) => ReactNode;
|
|
78
|
+
}): import("react").JSX.Element | null;
|
|
51
79
|
/**
|
|
52
80
|
* The numbered citation list, with the `#cite-n` back-links the inline markers
|
|
53
81
|
* point at. `processHtml` is a prop because only one wiki runs reference text
|
|
@@ -79,19 +107,16 @@ export declare function StatsView({ items, columns, }: {
|
|
|
79
107
|
/**
|
|
80
108
|
* A maintenance notice.
|
|
81
109
|
*
|
|
82
|
-
* The label
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* component library that hardcoded an icon set would make its
|
|
87
|
-
* that icon set.
|
|
110
|
+
* The label is `BANNER_LABELS`, looked up here: three renderers each did the
|
|
111
|
+
* unknown-variant fallback and the lookup, and one restated all six labels.
|
|
112
|
+
* The fallback `message` arrives as a prop because it is the wiki's editorial
|
|
113
|
+
* voice — "You can help RADIX Wiki by expanding it" has a name in it. `icon`
|
|
114
|
+
* is optional: a component library that hardcoded an icon set would make its
|
|
115
|
+
* consumers install that icon set.
|
|
88
116
|
*/
|
|
89
|
-
export declare function BannerView({ variant, text,
|
|
117
|
+
export declare function BannerView({ variant, text, message, icon, }: {
|
|
90
118
|
variant: string;
|
|
91
119
|
text?: string | null;
|
|
92
|
-
|
|
93
|
-
label: string;
|
|
94
|
-
message: string;
|
|
95
|
-
};
|
|
120
|
+
message: string;
|
|
96
121
|
icon?: ReactNode;
|
|
97
122
|
}): import("react").JSX.Element;
|
package/dist/block-views.js
CHANGED
|
@@ -22,18 +22,17 @@ import { Fragment, useState } from 'react';
|
|
|
22
22
|
import { Anchor } from './react-server.js';
|
|
23
23
|
import { safeLinkHref } from './validation.js';
|
|
24
24
|
import { cx } from './html.js';
|
|
25
|
-
|
|
25
|
+
import { BANNER_LABELS, bannerVariant } from './text.js';
|
|
26
26
|
/**
|
|
27
27
|
* Tabbed code samples. Every tab's body stays mounted and the inactive ones are
|
|
28
|
-
* hidden rather than unmounted:
|
|
29
|
-
*
|
|
30
|
-
* it on every tab press.
|
|
28
|
+
* hidden rather than unmounted: remounting a highlighted panel would re-run
|
|
29
|
+
* whatever the consumer's highlighter attached to it on every tab press.
|
|
31
30
|
*/
|
|
32
|
-
export function CodeTabsView({ tabs }) {
|
|
31
|
+
export function CodeTabsView({ tabs, highlighted = false }) {
|
|
33
32
|
const [activeTab, setActiveTab] = useState(0);
|
|
34
33
|
if (!tabs.length)
|
|
35
34
|
return null;
|
|
36
|
-
return (_jsxs("div", { className: "code-tabs", children: [_jsx("div", { className: "code-tabs-list", children: tabs.map((tab, i) => (_jsx("button", { type: "button", className: cx('code-tabs-btn', i === activeTab && 'code-tabs-btn-active'), onClick: () => setActiveTab(i), children: tab.label }, i))) }), tabs.map((tab, i) => (_jsx("div", { className: i
|
|
35
|
+
return (_jsxs("div", { className: "code-tabs", children: [_jsx("div", { className: "code-tabs-list", children: tabs.map((tab, i) => (_jsx("button", { type: "button", className: cx('code-tabs-btn', i === activeTab && 'code-tabs-btn-active'), onClick: () => setActiveTab(i), children: tab.label }, i))) }), tabs.map((tab, i) => highlighted ? (_jsx("div", { className: "code-tabs-panel", hidden: i !== activeTab, dangerouslySetInnerHTML: { __html: tab.code } }, i)) : (_jsx("div", { className: "code-tabs-panel", hidden: i !== activeTab, children: _jsx("pre", { children: _jsx("code", { className: tab.language ? `language-${tab.language}` : undefined, children: tab.code }) }) }, i)))] }));
|
|
37
36
|
}
|
|
38
37
|
/**
|
|
39
38
|
* A row of columns, each holding blocks the caller renders.
|
|
@@ -42,16 +41,13 @@ export function CodeTabsView({ tabs }) {
|
|
|
42
41
|
* it was a layout bug rather than a style: `.column-view` is the flow context
|
|
43
42
|
* its stylesheet spaces children in, and an extra element between them means
|
|
44
43
|
* every gap rule inside a column silently matches nothing.
|
|
44
|
+
*
|
|
45
|
+
* Gap and alignment are data attributes that `wiki-formant/base.css` reads.
|
|
46
|
+
* They were Tailwind utilities, which reached a page only if the site's
|
|
47
|
+
* Tailwind scanned this package — and on one site `end` and `stretch` did not.
|
|
45
48
|
*/
|
|
46
49
|
export function ColumnsView({ columns, gap = 'md', align = 'start', render, }) {
|
|
47
|
-
|
|
48
|
-
const alignClass = {
|
|
49
|
-
start: 'items-start',
|
|
50
|
-
center: 'items-center',
|
|
51
|
-
end: 'items-end',
|
|
52
|
-
stretch: 'items-stretch',
|
|
53
|
-
}[align];
|
|
54
|
-
return (_jsx("div", { className: cx('columns-layout', gapClass, alignClass), children: columns.map(col => (_jsx("div", { className: "column-view", children: (col.blocks ?? []).map(bl => (_jsx(Fragment, { children: render(bl) }, bl.id))) }, col.id))) }));
|
|
50
|
+
return (_jsx("div", { className: "columns-layout", "data-gap": gap, "data-align": align, children: columns.map(col => (_jsx("div", { className: "column-view", children: (col.blocks ?? []).map(bl => (_jsx(Fragment, { children: render(bl) }, bl.id))) }, col.id))) }));
|
|
55
51
|
}
|
|
56
52
|
// ---- linkGrid ---------------------------------------------------------------
|
|
57
53
|
/**
|
|
@@ -70,6 +66,23 @@ export function LinkGridView({ groups, intro, link: Link = Anchor, }) {
|
|
|
70
66
|
return /^https?:\/\//.test(href) ? (_jsx("a", { href: href, target: "_blank", rel: "noopener", children: item.label }, i)) : (_jsx(Link, { href: href, children: item.label }, i));
|
|
71
67
|
}) })] }, group.id ?? gi)))] }));
|
|
72
68
|
}
|
|
69
|
+
// ---- recentPages / pageList -------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* A list of resolved page references — the body of a `recentPages` or
|
|
72
|
+
* `pageList` block. A real list, with each age in a `<time>` carrying its ISO
|
|
73
|
+
* date: the copies this replaced were bare `<div>` rows, one emitted a
|
|
74
|
+
* `<time>` with no `dateTime`, and their empty states were three different
|
|
75
|
+
* things (a sentence, a different sentence, nothing).
|
|
76
|
+
*
|
|
77
|
+
* `renderItem` replaces the row's inner markup for a wiki with its own row
|
|
78
|
+
* component; the list, its semantics and the empty state stay here.
|
|
79
|
+
*/
|
|
80
|
+
export function PageRefsView({ refs, empty, link: Link = Anchor, renderItem, }) {
|
|
81
|
+
if (!refs.length)
|
|
82
|
+
return empty ? _jsx("p", { className: "page-refs-empty", children: empty }) : null;
|
|
83
|
+
return (_jsx("ul", { className: "page-refs", children: refs.map(ref => (_jsx("li", { children: renderItem ? (renderItem(ref)) : (_jsxs(Link, { href: ref.href, className: "page-ref", children: [_jsx("span", { className: "page-ref-title", children: ref.title }), ref.timeAgo &&
|
|
84
|
+
(ref.updated ? (_jsx("time", { className: "page-ref-time", dateTime: ref.updated, children: ref.timeAgo })) : (_jsx("span", { className: "page-ref-time", children: ref.timeAgo })))] })) }, ref.href))) }));
|
|
85
|
+
}
|
|
73
86
|
// ---- references -------------------------------------------------------------
|
|
74
87
|
/**
|
|
75
88
|
* The numbered citation list, with the `#cite-n` back-links the inline markers
|
|
@@ -106,13 +119,14 @@ export function StatsView({ items, columns = 4, }) {
|
|
|
106
119
|
/**
|
|
107
120
|
* A maintenance notice.
|
|
108
121
|
*
|
|
109
|
-
* The label
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* component library that hardcoded an icon set would make its
|
|
114
|
-
* that icon set.
|
|
122
|
+
* The label is `BANNER_LABELS`, looked up here: three renderers each did the
|
|
123
|
+
* unknown-variant fallback and the lookup, and one restated all six labels.
|
|
124
|
+
* The fallback `message` arrives as a prop because it is the wiki's editorial
|
|
125
|
+
* voice — "You can help RADIX Wiki by expanding it" has a name in it. `icon`
|
|
126
|
+
* is optional: a component library that hardcoded an icon set would make its
|
|
127
|
+
* consumers install that icon set.
|
|
115
128
|
*/
|
|
116
|
-
export function BannerView({ variant, text,
|
|
117
|
-
|
|
129
|
+
export function BannerView({ variant, text, message, icon, }) {
|
|
130
|
+
const known = bannerVariant(variant);
|
|
131
|
+
return (_jsxs("div", { className: cx('editorial-banner', `editorial-banner-${known}`), role: "note", children: [icon, _jsxs("p", { className: "editorial-banner-body", children: [_jsxs("strong", { children: [BANNER_LABELS[known], "."] }), " ", text?.trim() || message] })] }));
|
|
118
132
|
}
|
package/dist/blocks.d.ts
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
|
+
import { type RelativeTimeStyle } from './freshness.js';
|
|
1
2
|
import type { BlockGroup } from './revisions.js';
|
|
2
3
|
export interface CodeTab {
|
|
3
4
|
label: string;
|
|
4
5
|
language?: string;
|
|
6
|
+
/** Source text, never markup: every view escapes it unless told otherwise. */
|
|
5
7
|
code: string;
|
|
6
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* A page a `recentPages` or `pageList` block points at, resolved on the server
|
|
11
|
+
* so the list renders with no client fetch and no client clock. Build it with
|
|
12
|
+
* `pageRef`, which is what fills `updated`: a `<time>` with no `dateTime`
|
|
13
|
+
* states an age no machine can read.
|
|
14
|
+
*/
|
|
15
|
+
export interface ResolvedPageRef {
|
|
16
|
+
title: string;
|
|
17
|
+
href: string;
|
|
18
|
+
/** `relativeTime` of `updated`, as of the render. */
|
|
19
|
+
timeAgo?: string;
|
|
20
|
+
/** ISO timestamp. */
|
|
21
|
+
updated?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function pageRef(page: {
|
|
24
|
+
title: string;
|
|
25
|
+
href: string;
|
|
26
|
+
updatedAt: Date | string;
|
|
27
|
+
}, now: number, style?: RelativeTimeStyle): ResolvedPageRef;
|
|
7
28
|
export interface ReferenceItem {
|
|
8
29
|
text: string;
|
|
9
30
|
url?: string | null;
|
|
@@ -109,9 +130,11 @@ export declare function someBlock<B>(blocks: readonly B[], predicate: (block: B)
|
|
|
109
130
|
/**
|
|
110
131
|
* Fenced blocks, one per tab, each under its label.
|
|
111
132
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
133
|
+
* `code` is source, so it goes in verbatim. This used to strip tags on the
|
|
134
|
+
* belief that editors stored highlighted markup; none of the three wikis ever
|
|
135
|
+
* did, and the strip turned `Vec<u8>` into `Vec` in every markdown twin. The
|
|
136
|
+
* fence outgrows any backtick run in the code, so a sample that itself
|
|
137
|
+
* contains a fence cannot close this one early.
|
|
115
138
|
*/
|
|
116
139
|
export declare function codeTabsToMarkdown(tabs: readonly CodeTab[]): string;
|
|
117
140
|
/** A maintenance notice as a blockquote. `label` is the caller's display string. */
|
package/dist/blocks.js
CHANGED
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
// new block type is a compile error until it is handled) and the bodies come
|
|
11
11
|
// from here. Each renderer takes the block's DATA rather than the block, which
|
|
12
12
|
// keeps this file free of any one repo's type union.
|
|
13
|
-
import { decodeEntities } from './markdown.js';
|
|
14
13
|
import { htmlToMarkdown, inlineToMarkdown } from './markdown.js';
|
|
14
|
+
import { relativeTime } from './freshness.js';
|
|
15
|
+
export function pageRef(page, now, style = 'long') {
|
|
16
|
+
return {
|
|
17
|
+
title: page.title,
|
|
18
|
+
href: page.href,
|
|
19
|
+
timeAgo: relativeTime(page.updatedAt, now, { style }),
|
|
20
|
+
updated: new Date(page.updatedAt).toISOString(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
15
23
|
/**
|
|
16
24
|
* The whole tree as markdown. Containers flatten in document order, because a
|
|
17
25
|
* reader following prose does not have columns — and an agent quoting the page
|
|
@@ -139,13 +147,19 @@ export function someBlock(blocks, predicate, containers) {
|
|
|
139
147
|
/**
|
|
140
148
|
* Fenced blocks, one per tab, each under its label.
|
|
141
149
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
150
|
+
* `code` is source, so it goes in verbatim. This used to strip tags on the
|
|
151
|
+
* belief that editors stored highlighted markup; none of the three wikis ever
|
|
152
|
+
* did, and the strip turned `Vec<u8>` into `Vec` in every markdown twin. The
|
|
153
|
+
* fence outgrows any backtick run in the code, so a sample that itself
|
|
154
|
+
* contains a fence cannot close this one early.
|
|
145
155
|
*/
|
|
146
156
|
export function codeTabsToMarkdown(tabs) {
|
|
147
157
|
return tabs
|
|
148
|
-
.map(t =>
|
|
158
|
+
.map(t => {
|
|
159
|
+
const longest = Math.max(0, ...(t.code.match(/`+/g) ?? []).map(run => run.length));
|
|
160
|
+
const fence = '`'.repeat(Math.max(3, longest + 1));
|
|
161
|
+
return `**${t.label}**\n\n${fence}${t.language || ''}\n${t.code.trim()}\n${fence}`;
|
|
162
|
+
})
|
|
149
163
|
.join('\n\n');
|
|
150
164
|
}
|
|
151
165
|
/** A maintenance notice as a blockquote. `label` is the caller's display string. */
|
package/dist/conformance.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface TesterOptions {
|
|
|
66
66
|
export interface Tester {
|
|
67
67
|
base: string;
|
|
68
68
|
endpoint: string;
|
|
69
|
-
/** One JSON-RPC call.
|
|
69
|
+
/** One JSON-RPC call. Waits out a 429 as `Retry-After` asks, up to four tries. */
|
|
70
70
|
rpc(method: string, params?: unknown): Promise<Rpc>;
|
|
71
71
|
/** `tools/call` shorthand. */
|
|
72
72
|
call(name: string, args?: Record<string, unknown>): Promise<Rpc>;
|
|
@@ -134,7 +134,19 @@ export declare function conditionalGetChecks(t: Tester, paths: readonly string[]
|
|
|
134
134
|
* document moving, which is worse than none. The body-derived ETag is the whole
|
|
135
135
|
* validator, and it is enough.
|
|
136
136
|
*/
|
|
137
|
-
|
|
137
|
+
/** The JSON descriptors every S10 origin serves: the two agent-card paths, the OpenAPI pair, the MCP manifest and the server card. */
|
|
138
|
+
export declare const S10_DESCRIPTORS: readonly string[];
|
|
139
|
+
export declare function descriptorChecks(t: Tester, paths?: readonly string[]): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Documents that project one corpus at different depths carry different ETags.
|
|
142
|
+
*
|
|
143
|
+
* Depths sharing a validator pass every conditional-GET check and still never
|
|
144
|
+
* move when only one of them changes. Every one must also HAVE an ETag: a Set
|
|
145
|
+
* of three nulls is size one and fails, but one null among two tags would pass
|
|
146
|
+
* a size check alone — the copy of this check one repo carried had exactly
|
|
147
|
+
* that hole.
|
|
148
|
+
*/
|
|
149
|
+
export declare function distinctEtagChecks(t: Tester, paths: readonly string[], label?: string): Promise<void>;
|
|
138
150
|
/**
|
|
139
151
|
* Every tool carries behavioural hints, and the ones that write say so.
|
|
140
152
|
*
|