wiki-formant 0.3.0 → 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/dist/blocks.d.ts +6 -0
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +3 -3
- package/dist/blocks.js.map +1 -1
- package/dist/conformance.d.ts +99 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +158 -0
- package/dist/conformance.js.map +1 -0
- package/dist/freshness.d.ts +20 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +42 -0
- package/dist/freshness.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +31 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +81 -0
- package/dist/links.js.map +1 -0
- package/dist/maps.d.ts +21 -0
- package/dist/maps.d.ts.map +1 -0
- package/dist/maps.js +77 -0
- package/dist/maps.js.map +1 -0
- package/dist/text.d.ts +41 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +112 -0
- package/dist/text.js.map +1 -0
- package/dist/validation.d.ts +47 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +124 -0
- package/dist/validation.js.map +1 -0
- package/dist/well-known.d.ts +83 -0
- package/dist/well-known.d.ts.map +1 -0
- package/dist/well-known.js +84 -0
- package/dist/well-known.js.map +1 -0
- package/package.json +33 -3
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
|
package/dist/blocks.d.ts.map
CHANGED
|
@@ -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;
|
|
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(
|
|
28
|
+
.map(group => group.map(atomic).filter(Boolean).join(groupSeparator))
|
|
29
29
|
.filter(Boolean)
|
|
30
|
-
.join(
|
|
30
|
+
.join(groupSeparator);
|
|
31
31
|
})
|
|
32
32
|
.filter(Boolean)
|
|
33
33
|
.join('\n\n')
|
package/dist/blocks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks.js","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,4EAA4E;AAC5E,+EAA+E;AAC/E,qEAAqE;AACrE,EAAE;AACF,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAC/E,qDAAqD;AAErD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;
|
|
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":"AAcA,MAAM,WAAW,GAAG;IAClB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACnC,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,KAAK,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAChC,SAAS,CAAC,EAAE,KAAK,CAAC;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACnC,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACpC,UAAU,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAClC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACvC,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE;YAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;SAAE,CAAA;KAAE,CAAC;CACjF;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACpD,8BAA8B;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACjE,8EAA8E;IAC9E,OAAO,CAAC,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC;IACzB,sDAAsD;IACtD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACtD,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,kCAAkC;IAClC,SAAS,IAAI,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB,wDAAwD;IACxD,OAAO,IAAI,MAAM,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAkDxD;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,CAAC,EAAE,MAAM,EACT,UAAU,EAAE,MAAM;AAClB;;;;GAIG;AACH,oBAAoB,GAAE,SAAS,MAAM,EAAc,GAClD,OAAO,CAAC,GAAG,CAAC,CAyDd;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,CAAC,EAAE,MAAM,EACT,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAA;CAAE,CAAC,EACxF,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9D;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7F"}
|
|
@@ -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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './taxonomy.js';
|
|
2
2
|
export * from './mcp.js';
|
|
3
3
|
export * from './headings.js';
|
|
4
|
+
export * from './links.js';
|
|
4
5
|
export * from './markdown.js';
|
|
5
6
|
export * from './entities.js';
|
|
6
7
|
export * from './http.js';
|
|
@@ -9,4 +10,7 @@ export * from './versioning.js';
|
|
|
9
10
|
export * from './rate-limit.js';
|
|
10
11
|
export * from './analytics.js';
|
|
11
12
|
export * from './blocks.js';
|
|
13
|
+
export * from './text.js';
|
|
14
|
+
export * from './maps.js';
|
|
15
|
+
export * from './freshness.js';
|
|
12
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,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
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export * from './taxonomy.js';
|
|
7
7
|
export * from './mcp.js';
|
|
8
8
|
export * from './headings.js';
|
|
9
|
+
export * from './links.js';
|
|
9
10
|
export * from './markdown.js';
|
|
10
11
|
export * from './entities.js';
|
|
11
12
|
export * from './http.js';
|
|
@@ -14,4 +15,7 @@ export * from './versioning.js';
|
|
|
14
15
|
export * from './rate-limit.js';
|
|
15
16
|
export * from './analytics.js';
|
|
16
17
|
export * from './blocks.js';
|
|
18
|
+
export * from './text.js';
|
|
19
|
+
export * from './maps.js';
|
|
20
|
+
export * from './freshness.js';
|
|
17
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,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"}
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A readable label for an anchor whose text is empty, derived from its href.
|
|
3
|
+
* Without one, crawlers and screen readers meet a link with nothing in it.
|
|
4
|
+
*/
|
|
5
|
+
export declare function fallbackAnchorText(href: string): string;
|
|
6
|
+
export interface NormaliseLinkOptions {
|
|
7
|
+
/**
|
|
8
|
+
* This wiki's own origin. An absolute link back to it is really internal, so
|
|
9
|
+
* it is folded to a relative path and navigates in place instead of opening a
|
|
10
|
+
* new tab. Matched case-insensitively, with or without a `www.` prefix.
|
|
11
|
+
* e.g. `'radix.wiki'` or `'caper.network'`.
|
|
12
|
+
*/
|
|
13
|
+
selfHost?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Shared across one document so a `#ref-n` citation can be given a
|
|
16
|
+
* `id="cite-n"` target exactly once — the reference list renders a `^`
|
|
17
|
+
* back-link to it. Pass the same Set for every block of a page.
|
|
18
|
+
*/
|
|
19
|
+
citedRefs?: Set<number>;
|
|
20
|
+
/**
|
|
21
|
+
* Give an empty anchor a synthesised label. On by default; pass `false` to
|
|
22
|
+
* keep an empty anchor empty.
|
|
23
|
+
*/
|
|
24
|
+
fallbackText?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Tag every anchor in `html` as internal or external. Attribute-order agnostic;
|
|
28
|
+
* nested `<a>` is not handled, being invalid HTML anyway.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normaliseLinks(html: string, options?: NormaliseLinkOptions): string;
|
|
31
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AA2BA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWvD;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAyCD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAMvF"}
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// links.ts — internal vs. external link normalisation for wiki content.
|
|
2
|
+
//
|
|
3
|
+
// The editor stores whatever href an author types. At render time a wiki has to
|
|
4
|
+
// draw the line between *internal* links (relative paths, in-page anchors, and
|
|
5
|
+
// absolute links back to its own host) and *external* ones: externals open in a
|
|
6
|
+
// new tab with a safe rel, internals navigate in place, and CSS keys off the
|
|
7
|
+
// same distinction to tint one and badge the other.
|
|
8
|
+
//
|
|
9
|
+
// Both wikis had written this out — caper's `normaliseLinks`, radix-wiki's
|
|
10
|
+
// anchor branch in `processHtml` — down to identical `getAttr` and `removeAttr`
|
|
11
|
+
// helpers and the same `#ref-n` -> `id="cite-n"` citation wiring. caper's file
|
|
12
|
+
// said "ported from radix-wiki" at the top, which is the drift admitting itself.
|
|
13
|
+
//
|
|
14
|
+
// What they drifted on: only radix-wiki synthesised a label for an anchor with
|
|
15
|
+
// no visible text. That is an accessibility fix, not a preference, so it is the
|
|
16
|
+
// default here and caper gains it by adopting this module.
|
|
17
|
+
/** Read one double-quoted attribute out of a tag's attribute string. */
|
|
18
|
+
const getAttr = (attrs, name) => attrs.match(new RegExp(`\\s${name}\\s*=\\s*"([^"]*)"`, 'i'))?.[1] ?? null;
|
|
19
|
+
/** Drop every occurrence of one attribute from a tag's attribute string. */
|
|
20
|
+
const removeAttr = (attrs, name) => attrs.replace(new RegExp(`\\s${name}\\s*=\\s*"[^"]*"`, 'gi'), '');
|
|
21
|
+
const stripTags = (s) => s.replace(/<[^>]*>/g, '').trim();
|
|
22
|
+
/**
|
|
23
|
+
* A readable label for an anchor whose text is empty, derived from its href.
|
|
24
|
+
* Without one, crawlers and screen readers meet a link with nothing in it.
|
|
25
|
+
*/
|
|
26
|
+
export function fallbackAnchorText(href) {
|
|
27
|
+
if (href.startsWith('#'))
|
|
28
|
+
return href.slice(1).replace(/-/g, ' ') || 'section';
|
|
29
|
+
if (href.startsWith('/')) {
|
|
30
|
+
const last = href.split('/').filter(Boolean).pop();
|
|
31
|
+
return last ? last.replace(/-/g, ' ') : 'page';
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return new URL(href).hostname.replace(/^www\./, '') || href;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return href;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const ANCHOR = /<a\b([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
41
|
+
function selfHostPattern(host) {
|
|
42
|
+
const escaped = host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
+
return new RegExp(`^https?://(?:www\\.)?${escaped}(/[^\\s"]*)?$`, 'i');
|
|
44
|
+
}
|
|
45
|
+
function normaliseAnchor(attrs, inner, opts, self) {
|
|
46
|
+
// Injected heading permalink anchors carry their glyph via CSS. Leave them
|
|
47
|
+
// alone — in particular the empty-text synthesis below must not fill them,
|
|
48
|
+
// or the label leaks into the heading's textContent and out into a TOC.
|
|
49
|
+
if (/\bheading-anchor\b/.test(attrs))
|
|
50
|
+
return `<a${attrs}>${inner}</a>`;
|
|
51
|
+
const rawHref = getAttr(attrs, 'href');
|
|
52
|
+
if (rawHref == null)
|
|
53
|
+
return `<a${attrs}>${inner}</a>`;
|
|
54
|
+
const href = self ? rawHref.replace(self, (_m, path) => path || '/') : rawHref;
|
|
55
|
+
const isInternal = href.startsWith('/') || href.startsWith('#');
|
|
56
|
+
// Drop any author-supplied target/rel; both are re-derived from the link kind.
|
|
57
|
+
let rest = removeAttr(removeAttr(removeAttr(attrs, 'href'), 'rel'), 'target');
|
|
58
|
+
const refMatch = opts.citedRefs ? href.match(/^#ref-(\d+)$/) : null;
|
|
59
|
+
if (refMatch && !/\bid\s*=/.test(rest)) {
|
|
60
|
+
const n = Number(refMatch[1]);
|
|
61
|
+
if (!opts.citedRefs.has(n)) {
|
|
62
|
+
opts.citedRefs.add(n);
|
|
63
|
+
rest = ` id="cite-${n}"${rest}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const safeInner = opts.fallbackText === false || stripTags(inner) ? inner : fallbackAnchorText(href);
|
|
67
|
+
return isInternal
|
|
68
|
+
? `<a href="${href}"${rest}>${safeInner}</a>`
|
|
69
|
+
: `<a href="${href}"${rest} target="_blank" rel="noopener">${safeInner}</a>`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Tag every anchor in `html` as internal or external. Attribute-order agnostic;
|
|
73
|
+
* nested `<a>` is not handled, being invalid HTML anyway.
|
|
74
|
+
*/
|
|
75
|
+
export function normaliseLinks(html, options = {}) {
|
|
76
|
+
if (!html.trim())
|
|
77
|
+
return html;
|
|
78
|
+
const self = options.selfHost ? selfHostPattern(options.selfHost) : null;
|
|
79
|
+
return html.replace(ANCHOR, (_m, attrs, inner) => normaliseAnchor(attrs, inner, options, self));
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"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"}
|
package/dist/maps.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface MapCoords {
|
|
2
|
+
lat: number;
|
|
3
|
+
lon: number;
|
|
4
|
+
zoom?: number | undefined;
|
|
5
|
+
}
|
|
6
|
+
/** A plain embed URL for a coordinate pair. */
|
|
7
|
+
export declare function mapsEmbedUrl(lat: number, lon: number, zoom?: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Dig a coordinate pair out of a maps URL. Four shapes in descending
|
|
10
|
+
* specificity: the `@lat,lon,zoom` path segment, a `/search/lat,lon`, the
|
|
11
|
+
* `!3d…!4d…` data blob, and finally an `ll`/`sll` query parameter.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractCoordsFromUrl(url: string): MapCoords | null;
|
|
14
|
+
/**
|
|
15
|
+
* An embeddable URL for `url`, or null when it is not a map link this
|
|
16
|
+
* understands. Already-embeddable URLs pass through untouched.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toMapEmbedUrl(url: string): string | null;
|
|
19
|
+
/** True for the shortener forms that only a redirect can resolve. */
|
|
20
|
+
export declare const isShortMapUrl: (url: string) => boolean;
|
|
21
|
+
//# sourceMappingURL=maps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"maps.d.ts","sourceRoot":"","sources":["../src/maps.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,+CAA+C;AAC/C,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,MAAM,CAExE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAoBlE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6BxD;AAED,qEAAqE;AACrE,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,KAAG,OACE,CAAC"}
|