wiki-formant 0.2.1 → 0.4.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 CHANGED
@@ -47,6 +47,24 @@ 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
+ ## Headings
51
+
52
+ Heading ids, permalink anchors, and the list an "on this page" rail renders — one pass over the HTML you already have.
53
+
54
+ ```ts
55
+ import { injectHeadingIds, headingsFrom } from 'wiki-formant/headings';
56
+
57
+ const html = injectHeadingIds(page.body); // ids + anchors, idempotent
58
+ const toc = headingsFrom(html); // [{ id, text, level }]
59
+ ```
60
+
61
+ Two behaviours worth knowing:
62
+
63
+ - **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.
64
+ - **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 copy that lacked it had that bug.
65
+
66
+ `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.
67
+
50
68
  ## MCP
51
69
 
52
70
  A minimal [Model Context Protocol](https://modelcontextprotocol.io) server over Streamable HTTP, with the transport edges most implementations get wrong.
@@ -135,6 +153,7 @@ export async function GET(request: Request) {
135
153
  | Export | From |
136
154
  |---|---|
137
155
  | `createTaxonomy`, `defaultHref`, `firstLetter`, `toggleFilter` | `wiki-formant/taxonomy` |
156
+ | `injectHeadingIds`, `headingsFrom`, `slugifyHeading` | `wiki-formant/headings` |
138
157
  | `mcpResponse`, `mcpGet`, `mcpOptions`, `handleMcp`, `withMcpCors`, `McpToolError`, `MCP_CORS`, `MCP_PROTOCOL_VERSION` | `wiki-formant/mcp` |
139
158
  | `htmlToMarkdown`, `inlineToMarkdown`, `tableToMarkdown`, `frontmatter`, `markdownDocument`, `decodeEntities` | `wiki-formant/markdown` |
140
159
  | `corpusEtag`, `notModified`, `textHeaders`, `markdownHeaders`, `cleanSnippet`, `pageLine` | `wiki-formant/http` |
package/dist/blocks.d.ts CHANGED
@@ -29,6 +29,12 @@ export interface BlockTreeOptions<B> {
29
29
  * for a leaf. An infobox is one group; a columns block is one per column.
30
30
  */
31
31
  containers?: (block: B) => B[][] | null;
32
+ /**
33
+ * What joins blocks *inside* a container. Defaults to a blank line, which is
34
+ * what markdown needs between paragraphs. Plain-text extraction passes a
35
+ * single newline: it is flattening for a reader, not typesetting.
36
+ */
37
+ groupSeparator?: string;
32
38
  }
33
39
  /**
34
40
  * The whole tree as markdown. Containers flatten in document order, because a
@@ -1 +1 @@
1
- {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;CAChC;AAID,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC;CACzC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,CAe1F;AAID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAOnE;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE5E;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,KAAK,SAAe,GACnB,MAAM,CAMR;AAED,yDAAyD;AACzD,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAGlE;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACpB,MAAM,CAWR;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAE/D"}
1
+ {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;CAChC;AAID,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,uEAAuE;IACvE,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC;IACxC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,CAe1F;AAID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAOnE;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE5E;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,KAAK,SAAe,GACnB,MAAM,CAMR;AAED,yDAAyD;AACzD,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAGlE;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACpB,MAAM,CAWR;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAE/D"}
package/dist/blocks.js CHANGED
@@ -18,16 +18,16 @@ import { htmlToMarkdown, inlineToMarkdown } from './markdown.js';
18
18
  * needs the sentences adjacent, not interleaved.
19
19
  */
20
20
  export function renderBlockTree(blocks, opts) {
21
- const { atomic, containers } = opts;
21
+ const { atomic, containers, groupSeparator = '\n\n' } = opts;
22
22
  return blocks
23
23
  .map(block => {
24
24
  const groups = containers?.(block);
25
25
  if (!groups)
26
26
  return atomic(block);
27
27
  return groups
28
- .map(group => group.map(atomic).filter(Boolean).join('\n\n'))
28
+ .map(group => group.map(atomic).filter(Boolean).join(groupSeparator))
29
29
  .filter(Boolean)
30
- .join('\n\n');
30
+ .join(groupSeparator);
31
31
  })
32
32
  .filter(Boolean)
33
33
  .join('\n\n')
@@ -1 +1 @@
1
- {"version":3,"file":"blocks.js","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"blocks.js","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ export interface Rpc {
2
+ result?: {
3
+ content?: Array<{
4
+ text?: string;
5
+ }>;
6
+ isError?: boolean;
7
+ tools?: Array<{
8
+ name: string;
9
+ }>;
10
+ resources?: Array<{
11
+ uri: string;
12
+ }>;
13
+ contents?: Array<{
14
+ text?: string;
15
+ }>;
16
+ serverInfo?: {
17
+ version?: string;
18
+ };
19
+ capabilities?: Record<string, unknown>;
20
+ instructions?: string;
21
+ };
22
+ error?: {
23
+ code: number;
24
+ message: string;
25
+ data?: {
26
+ availableTools?: string[];
27
+ };
28
+ };
29
+ }
30
+ export interface CheckResult {
31
+ tool: string;
32
+ ok: boolean;
33
+ note: string;
34
+ }
35
+ export interface TesterOptions {
36
+ /** Origin under test, no trailing slash. */
37
+ base: string;
38
+ /** MCP endpoint. Defaults to `${base}/api/mcp`. */
39
+ endpoint?: string;
40
+ /** Sent as clientInfo.name on initialize, and as the User-Agent. */
41
+ clientName: string;
42
+ }
43
+ export interface Tester {
44
+ base: string;
45
+ endpoint: string;
46
+ /** One JSON-RPC call. Backs off once and retries on a 429. */
47
+ rpc(method: string, params?: unknown): Promise<Rpc>;
48
+ /** `tools/call` shorthand. */
49
+ call(name: string, args?: Record<string, unknown>): Promise<Rpc>;
50
+ /** The decoded body of a tool result: parsed JSON, raw text, or the error. */
51
+ payload(r: Rpc): unknown;
52
+ /** Record an assertion and print it as it happens. */
53
+ check(label: string, ok: boolean, note: string): void;
54
+ results: CheckResult[];
55
+ /** JSON-RPC calls made so far. */
56
+ callCount(): number;
57
+ /**
58
+ * Count a call this tester did not make. A surface with more than one MCP
59
+ * endpoint drives the others itself; without this the tally under-reports.
60
+ */
61
+ recordCall(): void;
62
+ /** Print the tally and return the process exit code. */
63
+ summary(): number;
64
+ }
65
+ export declare function createTester(opts: TesterOptions): Tester;
66
+ /**
67
+ * The transport edges every spec-correct MCP server over HTTP must get right,
68
+ * independent of what tools it exposes. Returns the `initialize` response, since
69
+ * callers go on to assert against its serverInfo.
70
+ */
71
+ export declare function transportChecks(t: Tester, clientName: string,
72
+ /**
73
+ * The capability keys this server actually implements. The assertion is that
74
+ * a server declares what it has — which keys those are is the server's own
75
+ * business, so it is a parameter rather than one surface's set hardcoded here.
76
+ */
77
+ expectedCapabilities?: readonly string[]): Promise<Rpc>;
78
+ /**
79
+ * One service, many descriptors — server.json, the two agent-card paths, the
80
+ * OpenAPI document, the MCP server card, and `initialize` — should never
81
+ * disagree about the version. Pass the URLs this origin actually serves.
82
+ */
83
+ export declare function versionCoherence(t: Tester, expected: string, sources: Record<string, {
84
+ url: string;
85
+ at: (json: Record<string, unknown>) => unknown;
86
+ }>, initVersion?: string): Promise<void>;
87
+ /**
88
+ * Both A2A well-known paths must serve the identical card. v0.3 renamed
89
+ * agent.json to agent-card.json and defined no fallback either way, so an origin
90
+ * that lets them drift answers two different questions to two halves of its
91
+ * callers.
92
+ */
93
+ export declare function agentCardParity(t: Tester): Promise<void>;
94
+ /**
95
+ * A corpus export that costs a full render should answer a conditional GET with
96
+ * a 304. Checks each path serves an ETag and honours it on the way back.
97
+ */
98
+ export declare function conditionalGetChecks(t: Tester, paths: readonly string[]): Promise<void>;
99
+ //# sourceMappingURL=conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,158 @@
1
+ // conformance.ts — the half of an MCP conformance run that is not about any one
2
+ // server's tools.
3
+ //
4
+ // All three apps here ship a `scripts/_mcp-test.ts` that hits its live endpoints
5
+ // over HTTP exactly as an agent would, and all three had written the same JSON-RPC
6
+ // client, the same pass/fail table, and the same transport assertions: the CORS
7
+ // preflight, GET→405, a notification answering 202-with-no-body, a parse error
8
+ // carrying -32700, the batch cap, and honest `capabilities`.
9
+ //
10
+ // Those assertions are about the transport, and the transport is `mcp.ts` in this
11
+ // same package — so the suite that checks it belongs next to it. What each app
12
+ // keeps is its own fixtures: which tools it expects, what a good answer from each
13
+ // looks like, and which text surfaces it publishes.
14
+ export function createTester(opts) {
15
+ const base = opts.base.replace(/\/$/, '');
16
+ const endpoint = opts.endpoint ?? `${base}/api/mcp`;
17
+ const results = [];
18
+ let calls = 0;
19
+ const rpc = async (method, params) => {
20
+ calls++;
21
+ const res = await fetch(endpoint, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json', 'User-Agent': opts.clientName },
24
+ body: JSON.stringify({ jsonrpc: '2.0', id: calls, method, params }),
25
+ });
26
+ // The server's own rate limit is part of what is under test elsewhere; here
27
+ // it is just noise, so wait it out once rather than failing the run.
28
+ if (res.status === 429) {
29
+ await new Promise(r => setTimeout(r, 5000));
30
+ return rpc(method, params);
31
+ }
32
+ return (await res.json());
33
+ };
34
+ return {
35
+ base,
36
+ endpoint,
37
+ rpc,
38
+ call: (name, args = {}) => rpc('tools/call', { name, arguments: args }),
39
+ payload(r) {
40
+ const text = r.result?.content?.[0]?.text;
41
+ if (text == null)
42
+ return r.error ? { _error: r.error } : r.result;
43
+ try {
44
+ return JSON.parse(text);
45
+ }
46
+ catch {
47
+ return text;
48
+ }
49
+ },
50
+ check(tool, ok, note) {
51
+ results.push({ tool, ok, note });
52
+ console.log(`${ok ? ' PASS' : ' FAIL'} ${tool.padEnd(24)} ${note}`);
53
+ },
54
+ results,
55
+ callCount: () => calls,
56
+ recordCall: () => { calls++; },
57
+ summary() {
58
+ const failed = results.filter(r => !r.ok);
59
+ console.log(`\n──────── ${results.length - failed.length}/${results.length} passed, ${calls} JSON-RPC calls ────────`);
60
+ for (const f of failed)
61
+ console.log(`FAILED: ${f.tool} — ${f.note}`);
62
+ return failed.length ? 1 : 0;
63
+ },
64
+ };
65
+ }
66
+ /**
67
+ * The transport edges every spec-correct MCP server over HTTP must get right,
68
+ * independent of what tools it exposes. Returns the `initialize` response, since
69
+ * callers go on to assert against its serverInfo.
70
+ */
71
+ export async function transportChecks(t, clientName,
72
+ /**
73
+ * The capability keys this server actually implements. The assertion is that
74
+ * a server declares what it has — which keys those are is the server's own
75
+ * business, so it is a parameter rather than one surface's set hardcoded here.
76
+ */
77
+ expectedCapabilities = ['tools']) {
78
+ console.log(`\n=== MCP server ${t.endpoint} ===`);
79
+ const init = await t.rpc('initialize', {
80
+ protocolVersion: '2024-11-05',
81
+ capabilities: {},
82
+ clientInfo: { name: clientName, version: '1' },
83
+ });
84
+ t.check('initialize', !!init.result?.serverInfo && !!init.result?.instructions, JSON.stringify(init.result?.serverInfo ?? init.error));
85
+ console.log(`\n=== transport ===`);
86
+ const opt = await fetch(t.endpoint, { method: 'OPTIONS' });
87
+ t.check('OPTIONS preflight', opt.status === 204 &&
88
+ opt.headers.get('access-control-allow-origin') === '*' &&
89
+ (opt.headers.get('access-control-allow-headers') ?? '').includes('Mcp-Protocol-Version'), `${opt.status} ACAO=${opt.headers.get('access-control-allow-origin')}`);
90
+ const get = await fetch(t.endpoint);
91
+ t.check('GET→405', get.status === 405 && get.headers.get('access-control-allow-origin') === '*', `${get.status} Allow=${get.headers.get('allow')} ACAO=${get.headers.get('access-control-allow-origin')}`);
92
+ // A notification has no id, so it must be acknowledged with no body at all —
93
+ // a JSON-RPC response to one is a protocol error.
94
+ const notif = await fetch(t.endpoint, {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
98
+ });
99
+ const notifBody = await notif.text();
100
+ t.check('notification→202', notif.status === 202 && notifBody === '', `${notif.status} body="${notifBody}"`);
101
+ const bad = await fetch(t.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: 'not json' });
102
+ const badJson = (await bad.json());
103
+ t.check('parse error→400', bad.status === 400 && badJson.error?.code === -32700, `${bad.status} code=${badJson.error?.code}`);
104
+ const over = await fetch(t.endpoint, {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify(Array.from({ length: 21 }, (_, i) => ({ jsonrpc: '2.0', id: i, method: 'ping' }))),
108
+ });
109
+ const overJson = (await over.json());
110
+ t.check('batch cap (21)', overJson.error?.code === -32600, `code=${overJson.error?.code}: ${(overJson.error?.message ?? '').slice(0, 80)}`);
111
+ const caps = init.result?.capabilities ?? {};
112
+ t.check('capabilities honest', expectedCapabilities.every(k => k in caps), `${JSON.stringify(caps)} expected=[${expectedCapabilities.join(', ')}]`);
113
+ return init;
114
+ }
115
+ /**
116
+ * One service, many descriptors — server.json, the two agent-card paths, the
117
+ * OpenAPI document, the MCP server card, and `initialize` — should never
118
+ * disagree about the version. Pass the URLs this origin actually serves.
119
+ */
120
+ export async function versionCoherence(t, expected, sources, initVersion) {
121
+ const found = { init: initVersion };
122
+ for (const [label, { url, at }] of Object.entries(sources)) {
123
+ try {
124
+ found[label] = at((await (await fetch(url)).json()));
125
+ }
126
+ catch {
127
+ found[label] = '<unreachable>';
128
+ }
129
+ }
130
+ const all = Object.values(found).filter(v => v !== undefined);
131
+ t.check('version coherence', all.every(v => v === expected), `expected=${expected} ` + Object.entries(found).map(([k, v]) => `${k}=${v}`).join(' '));
132
+ }
133
+ /**
134
+ * Both A2A well-known paths must serve the identical card. v0.3 renamed
135
+ * agent.json to agent-card.json and defined no fallback either way, so an origin
136
+ * that lets them drift answers two different questions to two halves of its
137
+ * callers.
138
+ */
139
+ export async function agentCardParity(t) {
140
+ const [a, b] = await Promise.all([
141
+ fetch(`${t.base}/.well-known/agent-card.json`).then(r => r.json()),
142
+ fetch(`${t.base}/.well-known/agent.json`).then(r => r.json()),
143
+ ]);
144
+ t.check('agent card parity', JSON.stringify(a) === JSON.stringify(b), 'agent.json === agent-card.json');
145
+ }
146
+ /**
147
+ * A corpus export that costs a full render should answer a conditional GET with
148
+ * a 304. Checks each path serves an ETag and honours it on the way back.
149
+ */
150
+ export async function conditionalGetChecks(t, paths) {
151
+ for (const path of paths) {
152
+ const fresh = await fetch(`${t.base}/${path}`);
153
+ const etag = fresh.headers.get('etag');
154
+ const revalidated = etag ? await fetch(`${t.base}/${path}`, { headers: { 'If-None-Match': etag } }) : null;
155
+ t.check(`${path} 304`, !!etag && revalidated?.status === 304, `etag=${etag} revalidate=${revalidated?.status}`);
156
+ }
157
+ }
158
+ //# sourceMappingURL=conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface FreshnessInput {
2
+ lastVerifiedAt?: Date | string | null;
3
+ updatedAt?: Date | string | null;
4
+ }
5
+ export declare const DEFAULT_MAX_AGE_DAYS = 180;
6
+ /** Whole days between `date` and `now`, or null when there is no usable date. */
7
+ export declare function daysSince(date: Date | string | null | undefined, now: number): number | null;
8
+ /**
9
+ * Stale when the page has never been verified, or was verified longer ago than
10
+ * `maxAgeDays`. Falls back to `updatedAt` when `lastVerifiedAt` is unset: an
11
+ * edit is a weaker signal than a verification, but it is a signal.
12
+ */
13
+ export declare function isStale(page: FreshnessInput, now: number, maxAgeDays?: number): boolean;
14
+ /**
15
+ * The sentence the notice carries. Both wikis shipped this string identically,
16
+ * down to the ISO date and the closing request, so it lives here rather than
17
+ * being re-typed either side of the extraction.
18
+ */
19
+ export declare function freshnessNotice(page: FreshnessInput): string;
20
+ //# sourceMappingURL=freshness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../src/freshness.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,cAAc;IAC7B,cAAc,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;IACtC,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAIxC,iFAAiF;AACjF,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5F;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,SAAuB,GAAG,OAAO,CAGrG;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAK5D"}
@@ -0,0 +1,42 @@
1
+ // freshness.ts — the on-chain-native extension of verifiability.
2
+ //
3
+ // A page is "fresh" if its facts were verified recently. Crypto facts decay
4
+ // fast, so a page not re-checked within `maxAgeDays` earns a synthetic notice
5
+ // at the top of the article until an editor (or a maintenance sweep) re-checks
6
+ // it and stamps `lastVerifiedAt`.
7
+ //
8
+ // `now` is a parameter rather than a `Date.now()` call inside the function.
9
+ // This runs during SSR, and a render that reads the clock is a render that can
10
+ // disagree with the one the server just sent.
11
+ export const DEFAULT_MAX_AGE_DAYS = 180;
12
+ const DAY_MS = 86_400_000;
13
+ /** Whole days between `date` and `now`, or null when there is no usable date. */
14
+ export function daysSince(date, now) {
15
+ if (!date)
16
+ return null;
17
+ const t = date instanceof Date ? date.getTime() : Date.parse(date);
18
+ if (Number.isNaN(t))
19
+ return null;
20
+ return Math.floor((now - t) / DAY_MS);
21
+ }
22
+ /**
23
+ * Stale when the page has never been verified, or was verified longer ago than
24
+ * `maxAgeDays`. Falls back to `updatedAt` when `lastVerifiedAt` is unset: an
25
+ * edit is a weaker signal than a verification, but it is a signal.
26
+ */
27
+ export function isStale(page, now, maxAgeDays = DEFAULT_MAX_AGE_DAYS) {
28
+ const age = daysSince(page.lastVerifiedAt ?? page.updatedAt, now);
29
+ return age === null || age > maxAgeDays;
30
+ }
31
+ /**
32
+ * The sentence the notice carries. Both wikis shipped this string identically,
33
+ * down to the ISO date and the closing request, so it lives here rather than
34
+ * being re-typed either side of the extraction.
35
+ */
36
+ export function freshnessNotice(page) {
37
+ const when = page.lastVerifiedAt
38
+ ? `last verified ${new Date(page.lastVerifiedAt).toISOString().slice(0, 10)}`
39
+ : 'not yet verified against sources';
40
+ return `This page was ${when} and may be out of date. Please help re-check its facts against current sources and the live ledger.`;
41
+ }
42
+ //# sourceMappingURL=freshness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freshness.js","sourceRoot":"","sources":["../src/freshness.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,+EAA+E;AAC/E,kCAAkC;AAClC,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,8CAA8C;AAO9C,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC,MAAM,MAAM,GAAG,UAAU,CAAC;AAE1B,iFAAiF;AACjF,MAAM,UAAU,SAAS,CAAC,IAAsC,EAAE,GAAW;IAC3E,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,CAAC,GAAG,IAAI,YAAY,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnE,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,IAAoB,EAAE,GAAW,EAAE,UAAU,GAAG,oBAAoB;IAC1F,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAClE,OAAO,GAAG,KAAK,IAAI,IAAI,GAAG,GAAG,UAAU,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAoB;IAClD,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc;QAC9B,CAAC,CAAC,iBAAiB,IAAI,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE;QAC7E,CAAC,CAAC,kCAAkC,CAAC;IACvC,OAAO,iBAAiB,IAAI,sGAAsG,CAAC;AACrI,CAAC"}
@@ -0,0 +1,42 @@
1
+ /** A heading found in a page's HTML, in document order. */
2
+ export interface Heading {
3
+ id: string;
4
+ text: string;
5
+ /** 1 for `<h1>`, 2 for `<h2>`, and so on. */
6
+ level: number;
7
+ }
8
+ /** The default slug rule: lowercase words joined by hyphens. */
9
+ export declare function slugifyHeading(text: string): string;
10
+ export interface HeadingIdOptions {
11
+ /**
12
+ * How heading text becomes an id. Defaults to `slugifyHeading`. Pass the rule
13
+ * your wiki has already published ids under — see the note at the top of this
14
+ * file about why this is not standardised.
15
+ */
16
+ slug?: (text: string) => string;
17
+ /**
18
+ * Emitted after the heading text so a reader can link to the section. Return
19
+ * `''` for no anchor. The default is deliberately empty of text — its glyph
20
+ * comes from CSS, so it never leaks into a heading's `textContent` and out
21
+ * into a TOC label.
22
+ */
23
+ anchor?: (id: string) => string;
24
+ }
25
+ /**
26
+ * Give every heading in `html` an id and a permalink anchor. Headings that
27
+ * already carry an id keep it, and a heading already carrying an anchor is left
28
+ * alone, so this is safe to run twice over the same string.
29
+ */
30
+ export declare function injectHeadingIds(html: string, options?: HeadingIdOptions): string;
31
+ /**
32
+ * The headings in `html` that carry an id, in document order — the list an "on
33
+ * this page" rail renders. Run it over the output of `injectHeadingIds` and
34
+ * every heading is in it.
35
+ *
36
+ * Reading the string rather than the rendered DOM is only possible where the
37
+ * body IS a string at render time. A wiki whose content streams in as blocks
38
+ * after mount has to query the DOM instead, which is why two of the three
39
+ * consumers use only the injector above.
40
+ */
41
+ export declare function headingsFrom(html: string): Heading[];
42
+ //# sourceMappingURL=headings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headings.d.ts","sourceRoot":"","sources":["../src/headings.ts"],"names":[],"mappings":"AAoBA,2DAA2D;AAC3D,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOnD;AAUD,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACjC;AAKD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAkBrF;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAQpD"}
@@ -0,0 +1,81 @@
1
+ // headings.ts — stable ids on a wiki page's headings, and the list a table of
2
+ // contents is built from.
3
+ //
4
+ // Two of the three wikis had already written this out (caper's `injectHeadingIds`,
5
+ // radix-wiki's heading branch in `processHtml`), down to byte-identical
6
+ // `stripTags` and `getAttr` helpers — caper's file says "ported in spirit from
7
+ // radix-wiki" at the top, which is the drift admitting itself. What they had
8
+ // drifted on is below.
9
+ //
10
+ // The slug rule is a parameter, not a decision this module makes. A heading id
11
+ // is a live URL: readers link to `#the-shape-of-a-code`, and so does the page's
12
+ // own permalink anchor. Unifying two slug rules would silently move every
13
+ // existing anchor on whichever wiki lost, so the rule each has shipped is the
14
+ // rule it keeps, stated at its call site instead of buried in a helper.
15
+ //
16
+ // Deduping, by contrast, is not a choice: two headings with the same text
17
+ // otherwise mint the same id twice and every link to the second one lands on
18
+ // the first. That was already a bug in the copy that lacked it, so this always
19
+ // dedupes.
20
+ /** The default slug rule: lowercase words joined by hyphens. */
21
+ export function slugifyHeading(text) {
22
+ return text
23
+ .toLowerCase()
24
+ .trim()
25
+ .replace(/[^\w\s-]/g, '')
26
+ .replace(/[\s_-]+/g, '-')
27
+ .replace(/^-+|-+$/g, '');
28
+ }
29
+ const HEADING = /<(h[1-6])([^>]*)>([\s\S]*?)<\/\1>/gi;
30
+ const stripTags = (s) => s.replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').trim();
31
+ const getAttr = (attrs, name) => attrs.match(new RegExp(`\\s${name}\\s*=\\s*"([^"]*)"`, 'i'))?.[1] ?? null;
32
+ const defaultAnchor = (id) => `<a class="heading-anchor" href="#${id}" aria-label="Permalink to this section" tabindex="-1"></a>`;
33
+ /**
34
+ * Give every heading in `html` an id and a permalink anchor. Headings that
35
+ * already carry an id keep it, and a heading already carrying an anchor is left
36
+ * alone, so this is safe to run twice over the same string.
37
+ */
38
+ export function injectHeadingIds(html, options = {}) {
39
+ if (!html.trim())
40
+ return html;
41
+ const slug = options.slug ?? slugifyHeading;
42
+ const anchor = options.anchor ?? defaultAnchor;
43
+ const used = new Set();
44
+ return html.replace(HEADING, (match, tag, attrs, content) => {
45
+ if (content.includes('heading-anchor'))
46
+ return match;
47
+ const existing = getAttr(attrs, 'id');
48
+ let id = existing || slug(stripTags(content));
49
+ if (!id)
50
+ return match;
51
+ if (!existing) {
52
+ const base = id;
53
+ let n = 2;
54
+ while (used.has(id))
55
+ id = `${base}-${n++}`;
56
+ }
57
+ used.add(id);
58
+ return `<${tag}${existing ? attrs : `${attrs} id="${id}"`}>${content}${anchor(id)}</${tag}>`;
59
+ });
60
+ }
61
+ /**
62
+ * The headings in `html` that carry an id, in document order — the list an "on
63
+ * this page" rail renders. Run it over the output of `injectHeadingIds` and
64
+ * every heading is in it.
65
+ *
66
+ * Reading the string rather than the rendered DOM is only possible where the
67
+ * body IS a string at render time. A wiki whose content streams in as blocks
68
+ * after mount has to query the DOM instead, which is why two of the three
69
+ * consumers use only the injector above.
70
+ */
71
+ export function headingsFrom(html) {
72
+ const out = [];
73
+ for (const [, tag, attrs, content] of html.matchAll(HEADING)) {
74
+ const id = getAttr(attrs ?? '', 'id');
75
+ const text = stripTags(content ?? '');
76
+ if (id && text)
77
+ out.push({ id, text, level: Number(tag[1]) });
78
+ }
79
+ return out;
80
+ }
81
+ //# sourceMappingURL=headings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headings.js","sourceRoot":"","sources":["../src/headings.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './taxonomy.js';
2
2
  export * from './mcp.js';
3
+ export * from './headings.js';
4
+ export * from './links.js';
3
5
  export * from './markdown.js';
4
6
  export * from './entities.js';
5
7
  export * from './http.js';
@@ -8,4 +10,7 @@ export * from './versioning.js';
8
10
  export * from './rate-limit.js';
9
11
  export * from './analytics.js';
10
12
  export * from './blocks.js';
13
+ export * from './text.js';
14
+ export * from './maps.js';
15
+ export * from './freshness.js';
11
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@
5
5
  // then lets drift.
6
6
  export * from './taxonomy.js';
7
7
  export * from './mcp.js';
8
+ export * from './headings.js';
9
+ export * from './links.js';
8
10
  export * from './markdown.js';
9
11
  export * from './entities.js';
10
12
  export * from './http.js';
@@ -13,4 +15,7 @@ export * from './versioning.js';
13
15
  export * from './rate-limit.js';
14
16
  export * from './analytics.js';
15
17
  export * from './blocks.js';
18
+ export * from './text.js';
19
+ export * from './maps.js';
20
+ export * from './freshness.js';
16
21
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,mBAAmB;AAEnB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,mBAAmB;AAEnB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}