rapidoc 11.0.1 → 12.0.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.
Files changed (80) hide show
  1. package/README.md +46 -31
  2. package/dist/rapidoc-min.js +1424 -213
  3. package/dist/rapidoc-static-min.js +13542 -0
  4. package/package.json +47 -35
  5. package/src/components/api-request.ts +869 -0
  6. package/src/components/api-response.ts +509 -0
  7. package/src/components/json-tree.ts +171 -0
  8. package/src/components/schema-base-element.ts +315 -0
  9. package/src/components/schema-table.ts +430 -0
  10. package/src/components/schema-tree.ts +386 -0
  11. package/src/components/tag-input.ts +184 -0
  12. package/src/index.js +6 -0
  13. package/src/oauth-receiver.ts +29 -0
  14. package/src/rapidoc-mini.ts +582 -0
  15. package/src/rapidoc.ts +1808 -0
  16. package/src/static/index.d.ts +149 -0
  17. package/src/static/index.js +62 -0
  18. package/src/static/navigation.js +497 -0
  19. package/src/static/render.js +490 -0
  20. package/src/static/serializer.js +216 -0
  21. package/src/styles/auth-styles.js +318 -0
  22. package/src/styles/border-styles.js +24 -0
  23. package/src/styles/css-bundle.js +129 -0
  24. package/src/styles/custom-styles.js +8 -0
  25. package/src/styles/dialog-box-styles.js +102 -0
  26. package/src/styles/endpoint-styles.js +559 -0
  27. package/src/styles/flex-styles.js +16 -0
  28. package/src/styles/font-styles.js +432 -0
  29. package/src/styles/info-styles.js +36 -0
  30. package/src/styles/input-styles.js +555 -0
  31. package/src/styles/layout-styles.js +396 -0
  32. package/src/styles/microlighter-styles.js +99 -0
  33. package/src/styles/nav-styles.js +609 -0
  34. package/src/styles/param-list-styles.js +261 -0
  35. package/src/styles/schema-styles.js +288 -0
  36. package/src/styles/tab-styles.js +106 -0
  37. package/src/styles/theme-tokens.js +582 -0
  38. package/src/templates/advanced-search-dialog-template.js +94 -0
  39. package/src/templates/api-response-template.js +219 -0
  40. package/src/templates/callback-template.js +86 -0
  41. package/src/templates/code-samples-template.js +52 -0
  42. package/src/templates/components-template.js +86 -0
  43. package/src/templates/downloads-template.js +85 -0
  44. package/src/templates/endpoint-template.js +386 -0
  45. package/src/templates/expanded-endpoint-template.js +266 -0
  46. package/src/templates/focused-endpoint-template.js +145 -0
  47. package/src/templates/header-template.js +92 -0
  48. package/src/templates/logo-template.js +21 -0
  49. package/src/templates/main-body-template.js +102 -0
  50. package/src/templates/navbar-template.js +489 -0
  51. package/src/templates/overview-template.js +104 -0
  52. package/src/templates/request-body-template.js +651 -0
  53. package/src/templates/request-params-template.js +474 -0
  54. package/src/templates/security-scheme-template.js +1263 -0
  55. package/src/templates/server-template.js +152 -0
  56. package/src/utils/auto-color-scheme.js +99 -0
  57. package/src/utils/bind-navbar.js +175 -0
  58. package/src/utils/color-utils.js +59 -0
  59. package/src/utils/common-utils.js +191 -0
  60. package/src/utils/curl-utils.js +123 -0
  61. package/src/utils/highlighter.js +416 -0
  62. package/src/utils/markdown-utils.js +110 -0
  63. package/src/utils/mcp/common.js +467 -0
  64. package/src/utils/mcp/generators/csharp.js +242 -0
  65. package/src/utils/mcp/generators/go.js +298 -0
  66. package/src/utils/mcp/generators/java.js +330 -0
  67. package/src/utils/mcp/generators/python.js +248 -0
  68. package/src/utils/mcp/generators/rust.js +281 -0
  69. package/src/utils/mcp/generators/typescript.js +290 -0
  70. package/src/utils/mcp-generator.js +143 -0
  71. package/src/utils/mock-interceptor.js +508 -0
  72. package/src/utils/oauth-utils.js +87 -0
  73. package/src/utils/request-utils.js +203 -0
  74. package/src/utils/response-utils.js +203 -0
  75. package/src/utils/sanitize.js +57 -0
  76. package/src/utils/schema-utils.js +1927 -0
  77. package/src/utils/slugger.js +189 -0
  78. package/src/utils/spec-parser.js +915 -0
  79. package/src/utils/url-utils.js +211 -0
  80. package/src/utils/xml-utils.js +61 -0
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Normalizes header values by deduplicating comma-separated tokens (e.g. Accept headers).
3
+ *
4
+ * @param {string} value - Raw header value
5
+ * @returns {string} Deduplicated header value
6
+ */
7
+ export function deduplicateHeaderValues(value) {
8
+ if (!value || typeof value !== "string") {
9
+ return value || "";
10
+ }
11
+ const seenValues = [];
12
+ return value
13
+ .split(",")
14
+ .map((val) => {
15
+ const trimmed = val.trim();
16
+ const normalizedValue = trimmed.toLowerCase();
17
+ if (seenValues.includes(normalizedValue)) {
18
+ return null;
19
+ }
20
+ seenValues.push(normalizedValue);
21
+ return trimmed;
22
+ })
23
+ .filter((val) => val !== null)
24
+ .join(", ");
25
+ }
26
+
27
+ /**
28
+ * Generates a clean, copyable cURL command string from request parameters.
29
+ * Pure utility decoupled from the DOM.
30
+ *
31
+ * @param {object} options
32
+ * @param {string} [options.method='GET'] - HTTP method (GET, POST, etc.)
33
+ * @param {string} options.url - Request URL
34
+ * @param {Headers|Map|Array<[string, string]>|object} [options.headers] - Request headers
35
+ * @param {URLSearchParams|FormData|File|string|object} [options.body] - Request body payload
36
+ * @param {string} [options.contentType=''] - Content type of the payload
37
+ * @param {string} [options.baseLocation] - Optional base location to resolve relative URLs
38
+ * @returns {string} Formatted cURL snippet
39
+ */
40
+ export function generateCurlCommand({
41
+ method = "GET",
42
+ url = "",
43
+ headers,
44
+ body,
45
+ contentType = "",
46
+ baseLocation = typeof location !== "undefined" ? location.href : "",
47
+ } = {}) {
48
+ let curlUrl = url;
49
+ if (!url.startsWith("http://") && !url.startsWith("https://") && baseLocation) {
50
+ try {
51
+ curlUrl = new URL(url, baseLocation).href;
52
+ } catch {
53
+ curlUrl = url;
54
+ }
55
+ }
56
+
57
+ let curl = `curl -X ${method.toUpperCase()} "${curlUrl}" \\\n`;
58
+
59
+ // Process headers into key-value pairs with deduplicated comma values
60
+ const headerEntries = [];
61
+ if (headers instanceof Headers || (headers && typeof headers.forEach === "function")) {
62
+ headers.forEach((value, key) => {
63
+ headerEntries.push([key, deduplicateHeaderValues(value)]);
64
+ });
65
+ } else if (Array.isArray(headers)) {
66
+ headers.forEach(([key, value]) => {
67
+ headerEntries.push([key, deduplicateHeaderValues(value)]);
68
+ });
69
+ } else if (headers && typeof headers === "object") {
70
+ Object.entries(headers).forEach(([key, value]) => {
71
+ headerEntries.push([key, deduplicateHeaderValues(String(value))]);
72
+ });
73
+ }
74
+
75
+ let curlHeaders = headerEntries.map(([key, value]) => ` -H '${key}: ${value}'`).join("\\\n");
76
+ if (curlHeaders) {
77
+ curlHeaders = `${curlHeaders} \\\n`;
78
+ }
79
+
80
+ let curlData = "";
81
+ let curlForm = "";
82
+
83
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
84
+ curlData = ` -d ${body.toString()} \\\n`;
85
+ } else if (typeof File !== "undefined" && body instanceof File) {
86
+ curlData = ` --data-binary @${body.name} \\\n`;
87
+ } else if (typeof FormData !== "undefined" && body instanceof FormData) {
88
+ curlForm = Array.from(body)
89
+ .map(([key, value]) => {
90
+ if (typeof File !== "undefined" && value instanceof File) {
91
+ return ` -F "${key}=@${value.name}"`;
92
+ }
93
+ const sanitizedVal =
94
+ typeof value === "string" ? value.replace(/\\/g, "\\\\").replace(/"/g, '\\"') : value;
95
+ return ` -F "${key}=${sanitizedVal}"`;
96
+ })
97
+ .join("\\\n");
98
+ if (curlForm) {
99
+ curlForm = `${curlForm} \\\n`;
100
+ }
101
+ } else if (body !== undefined && body !== null && body !== "") {
102
+ const rawBody = typeof body === "object" ? JSON.stringify(body) : String(body);
103
+ const isJson =
104
+ contentType.includes("json") ||
105
+ (rawBody.trim().startsWith("{") && rawBody.trim().endsWith("}"));
106
+
107
+ if (isJson) {
108
+ try {
109
+ const compactJson = JSON.stringify(JSON.parse(rawBody));
110
+ curlData = ` -d '${compactJson.replace(/'/g, "'\\''")}' \\\n`;
111
+ } catch {
112
+ // If JSON.parse fails, fall through to regular string escape
113
+ }
114
+ }
115
+
116
+ if (!curlData) {
117
+ curlData = ` -d '${rawBody.replace(/'/g, "'\"'\"'")}' \\\n`;
118
+ }
119
+ }
120
+
121
+ const result = `${curl}${curlHeaders}${curlData}${curlForm}`;
122
+ return result.endsWith(" \\\n") ? result.slice(0, -3) : result;
123
+ }
@@ -0,0 +1,416 @@
1
+ // CSS Custom Highlight API-based Syntax Highlighter using bundled TextMate grammars
2
+
3
+ import astro from "microlighter/grammars/astro.js";
4
+ import bash from "microlighter/grammars/bash.js";
5
+ import c from "microlighter/grammars/c.js";
6
+ import cpp from "microlighter/grammars/cpp.js";
7
+ import csharp from "microlighter/grammars/csharp.js";
8
+ import cssGrammar from "microlighter/grammars/css.js";
9
+ import go from "microlighter/grammars/go.js";
10
+ import html from "microlighter/grammars/html.js";
11
+ import java from "microlighter/grammars/java.js";
12
+ import javascript from "microlighter/grammars/javascript.js";
13
+ import json from "microlighter/grammars/json.js";
14
+ import kotlin from "microlighter/grammars/kotlin.js";
15
+ import markdown from "microlighter/grammars/markdown.js";
16
+ import objectiveC from "microlighter/grammars/objective-c.js";
17
+ import perl from "microlighter/grammars/perl.js";
18
+ import php from "microlighter/grammars/php.js";
19
+ import powershell from "microlighter/grammars/powershell.js";
20
+ import python from "microlighter/grammars/python.js";
21
+ import r from "microlighter/grammars/r.js";
22
+ import ruby from "microlighter/grammars/ruby.js";
23
+ import rust from "microlighter/grammars/rust.js";
24
+ import svelte from "microlighter/grammars/svelte.js";
25
+ import toml from "microlighter/grammars/toml.js";
26
+ import tsx from "microlighter/grammars/tsx.js";
27
+ import typescript from "microlighter/grammars/typescript.js";
28
+ import vue from "microlighter/grammars/vue.js";
29
+ import yaml from "microlighter/grammars/yaml.js";
30
+
31
+ const httpGrammar = {
32
+ scopeName: "source.http",
33
+ patterns: [
34
+ {
35
+ match:
36
+ "^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|TRACE|CONNECT)\\s+([^\\s]+)\\s+(HTTP\\/[0-9.]+)",
37
+ captures: {
38
+ 1: { name: "keyword.control.http" },
39
+ 2: { name: "string.unquoted.http" },
40
+ 3: { name: "keyword.other.http" },
41
+ },
42
+ },
43
+ {
44
+ match: "^(HTTP\\/[0-9.]+)\\s+([0-9]{3})(?:\\s+(.*))?",
45
+ captures: {
46
+ 1: { name: "keyword.other.http" },
47
+ 2: { name: "constant.numeric.http" },
48
+ 3: { name: "string.unquoted.http" },
49
+ },
50
+ },
51
+ {
52
+ match: "^([a-zA-Z0-9_-]+):\\s*(.*)",
53
+ captures: {
54
+ 1: { name: "support.type.property-name.http" },
55
+ 2: { name: "string.unquoted.http" },
56
+ },
57
+ },
58
+ ],
59
+ };
60
+
61
+ const grammars = {
62
+ astro,
63
+ bash,
64
+ c,
65
+ cpp,
66
+ csharp,
67
+ css: cssGrammar,
68
+ go,
69
+ html,
70
+ http: httpGrammar,
71
+ java,
72
+ javascript,
73
+ json,
74
+ kotlin,
75
+ markdown,
76
+ "objective-c": objectiveC,
77
+ perl,
78
+ php,
79
+ powershell,
80
+ python,
81
+ r,
82
+ ruby,
83
+ rust,
84
+ svelte,
85
+ toml,
86
+ tsx,
87
+ typescript,
88
+ vue,
89
+ yaml,
90
+ };
91
+
92
+ const scopes = new Map();
93
+ Object.values(grammars).forEach((grammar) => {
94
+ if (grammar?.scopeName) {
95
+ scopes.set(grammar.scopeName, grammar);
96
+ }
97
+ });
98
+
99
+ const languageAliases = {
100
+ "c++": "cpp",
101
+ cs: "csharp",
102
+ curl: "bash",
103
+ js: "javascript",
104
+ jsx: "javascript",
105
+ markup: "html",
106
+ md: "markdown",
107
+ objc: "objective-c",
108
+ ps1: "powershell",
109
+ py: "python",
110
+ rb: "ruby",
111
+ sh: "bash",
112
+ shell: "bash",
113
+ svg: "html",
114
+ ts: "typescript",
115
+ xml: "html",
116
+ yml: "yaml",
117
+ zsh: "bash",
118
+ };
119
+
120
+ const noMatch = { indices: [[Infinity, Infinity]] };
121
+ const highlights = new Map();
122
+
123
+ const getCategory = (scope) => {
124
+ const parts = scope.split(".");
125
+ const [first, second, third] = parts;
126
+ const last = parts.at(-1);
127
+
128
+ if (first === "markup" && ["quote", "inserted", "deleted", "raw"].includes(second)) return second;
129
+ if (first === "entity" && second === "name") return third;
130
+ if (scope.startsWith("constant.character.entity")) return "character-entity";
131
+ if (parts.includes("numeric")) return "numeric";
132
+ if (scope.startsWith("support.type.property-name")) return "property";
133
+ if (parts.includes("attribute-value")) return "attribute-value";
134
+ if (scope.startsWith("string.other.link")) return "link";
135
+
136
+ if (
137
+ [
138
+ "doctype",
139
+ "at-rule",
140
+ "important",
141
+ "regexp",
142
+ "boolean",
143
+ "symbol",
144
+ "operator",
145
+ "attribute-name",
146
+ ].includes(last)
147
+ )
148
+ return last;
149
+
150
+ if (
151
+ [
152
+ "comment",
153
+ "string",
154
+ "constant",
155
+ "storage",
156
+ "keyword",
157
+ "variable",
158
+ "punctuation",
159
+ "entity",
160
+ "support",
161
+ ].includes(first)
162
+ )
163
+ return first;
164
+ };
165
+
166
+ const addRange = (node, start, end, scope) => {
167
+ const category = getCategory(scope);
168
+ if (!category || start === end) return;
169
+
170
+ const range = new Range();
171
+ range.setStart(node, start);
172
+ range.setEnd(node, end);
173
+
174
+ if (!highlights.has(category)) {
175
+ highlights.set(category, new Highlight());
176
+ }
177
+ highlights.get(category).add(range);
178
+ };
179
+
180
+ const addCaptures = (node, match, captures = {}) => {
181
+ Object.entries(captures).forEach(([index, capture]) => {
182
+ const offsets = match.indices[+index];
183
+ if (offsets) addRange(node, ...offsets, capture.name);
184
+ });
185
+ };
186
+
187
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
188
+
189
+ const expandEnd = (pattern, beginMatch) =>
190
+ pattern.replace(/\\(\d+)/g, (reference, index) =>
191
+ beginMatch[index] === undefined ? reference : escapeRegex(beginMatch[index]),
192
+ );
193
+
194
+ const getRules = (rule) => {
195
+ if (!rule) return [];
196
+ if (Array.isArray(rule)) return rule;
197
+ if (rule.match || rule.begin || rule.include) return [rule];
198
+ return rule.patterns || [];
199
+ };
200
+
201
+ const resolveInclude = (include, grammar, baseGrammar) => {
202
+ if (include === "$self") return { grammar, rules: grammar.patterns };
203
+ if (include === "$base") return { grammar: baseGrammar, rules: baseGrammar.patterns };
204
+
205
+ if (include[0] === "#") {
206
+ return { grammar, rules: getRules(grammar.repository?.[include.slice(1)]) };
207
+ }
208
+
209
+ const [scopeName, repositoryName] = include.split("#");
210
+ const includedGrammar = scopes.get(scopeName);
211
+ if (!includedGrammar) return null;
212
+
213
+ return {
214
+ grammar: includedGrammar,
215
+ rules: repositoryName
216
+ ? getRules(includedGrammar.repository?.[repositoryName])
217
+ : includedGrammar.patterns,
218
+ };
219
+ };
220
+
221
+ const expandRules = (rules, grammar, baseGrammar, activeIncludes = new Set()) => {
222
+ const expanded = [];
223
+ rules.forEach((rule) => {
224
+ if (rule.include) {
225
+ const includeKey = `${grammar.scopeName}:${rule.include}`;
226
+ if (activeIncludes.has(includeKey)) return;
227
+
228
+ const included = resolveInclude(rule.include, grammar, baseGrammar);
229
+ if (!included) return;
230
+
231
+ const nestedIncludes = new Set(activeIncludes);
232
+ nestedIncludes.add(includeKey);
233
+ expanded.push(...expandRules(included.rules, included.grammar, baseGrammar, nestedIncludes));
234
+ return;
235
+ }
236
+
237
+ if (rule.match || (rule.begin && rule.end)) expanded.push({ grammar, rule });
238
+ });
239
+
240
+ return expanded;
241
+ };
242
+
243
+ const regexes = new Map();
244
+ let matches = new Map();
245
+
246
+ const exec = (pattern, node, start, end) => {
247
+ let match = matches.get(pattern);
248
+ if (!match || match.indices[0][0] < start) {
249
+ if (!regexes.has(pattern)) regexes.set(pattern, new RegExp(pattern, "dgm"));
250
+ const regex = regexes.get(pattern);
251
+ regex.lastIndex = start;
252
+ match = regex.exec(node.data) || noMatch;
253
+ matches.set(pattern, match);
254
+ }
255
+
256
+ return match.indices[0][0] < end && match.indices[0][1] <= end ? match : null;
257
+ };
258
+
259
+ const nextRule = (node, contexts, start, end) => {
260
+ let winner = null;
261
+ contexts.forEach((context) => {
262
+ const pattern = context.rule.match || context.rule.begin;
263
+ const match = exec(pattern, node, start, end);
264
+ if (!match) return;
265
+
266
+ if (!winner || match.indices[0][0] < winner.match.indices[0][0]) {
267
+ winner = { ...context, match };
268
+ }
269
+ });
270
+
271
+ return winner;
272
+ };
273
+
274
+ const scanRegion = (node, rules, start, end, grammar, baseGrammar = grammar, closing = null) => {
275
+ const contexts = expandRules(rules, grammar, baseGrammar);
276
+ let cursor = start;
277
+
278
+ while (cursor < end) {
279
+ const candidate = nextRule(node, contexts, cursor, end);
280
+ const endMatch = closing ? exec(closing.pattern, node, cursor, end) : null;
281
+ const candidateStart = candidate?.match.indices[0][0] ?? Infinity;
282
+ const endStart = endMatch?.indices[0][0] ?? Infinity;
283
+
284
+ if (
285
+ endMatch &&
286
+ (endStart < candidateStart || (endStart === candidateStart && !closing?.applyEndPatternLast))
287
+ ) {
288
+ return { contentEnd: endStart, end: endMatch.indices[0][1], match: endMatch };
289
+ }
290
+
291
+ if (!candidate) return { contentEnd: end, end, match: null };
292
+
293
+ const { rule, match, grammar: ruleGrammar } = candidate;
294
+ if (rule.match) {
295
+ if (rule.name) addRange(node, ...match.indices[0], rule.name);
296
+ addCaptures(node, match, rule.captures);
297
+ cursor = match.indices[0][1] > cursor ? match.indices[0][1] : cursor + 1;
298
+ continue;
299
+ }
300
+
301
+ const nested = scanRegion(
302
+ node,
303
+ rule.patterns || [],
304
+ match.indices[0][1],
305
+ end,
306
+ ruleGrammar,
307
+ baseGrammar,
308
+ {
309
+ pattern: expandEnd(rule.end, match),
310
+ applyEndPatternLast: rule.applyEndPatternLast,
311
+ },
312
+ );
313
+
314
+ if (rule.name) addRange(node, match.indices[0][0], nested.end, rule.name);
315
+ if (rule.contentName) addRange(node, match.indices[0][1], nested.contentEnd, rule.contentName);
316
+ addCaptures(node, match, rule.beginCaptures || rule.captures);
317
+ if (nested.match) addCaptures(node, nested.match, rule.endCaptures || rule.captures);
318
+
319
+ cursor = nested.end > cursor ? nested.end : cursor + 1;
320
+ }
321
+
322
+ return { contentEnd: end, end, match: null };
323
+ };
324
+
325
+ export const getLanguage = (codeBlock) => {
326
+ const pre = codeBlock.parentElement;
327
+ const getLangClass = (el) => {
328
+ if (!el || !el.classList) return null;
329
+ return [...el.classList].find((c) => c.startsWith("language-"))?.slice("language-".length);
330
+ };
331
+ const lang =
332
+ getLangClass(codeBlock) ||
333
+ codeBlock.dataset?.language ||
334
+ getLangClass(pre) ||
335
+ pre?.dataset?.language ||
336
+ pre?.getAttribute?.("lang") ||
337
+ "";
338
+ return lang.toLowerCase();
339
+ };
340
+
341
+ export const normalizeLanguage = (lang) => languageAliases[lang] || lang;
342
+
343
+ export const getAllCodeBlocks = (rootNode = document) => {
344
+ const blocks = [];
345
+ const visitedRoots = new Set();
346
+
347
+ const traverse = (node) => {
348
+ if (!node || visitedRoots.has(node)) return;
349
+ visitedRoots.add(node);
350
+
351
+ if (node.querySelectorAll) {
352
+ const codeEls = node.querySelectorAll("pre > code");
353
+ blocks.push(...codeEls);
354
+
355
+ const allChildren = node.querySelectorAll("*");
356
+ for (let i = 0; i < allChildren.length; i++) {
357
+ const child = allChildren[i];
358
+ if (child.shadowRoot) {
359
+ traverse(child.shadowRoot);
360
+ }
361
+ }
362
+ }
363
+ };
364
+
365
+ traverse(rootNode);
366
+ return blocks;
367
+ };
368
+
369
+ let highlightScheduled = false;
370
+
371
+ export const highlightAllCode = (rootNode = document) => {
372
+ if (typeof window === "undefined" || typeof CSS === "undefined" || !CSS.highlights) {
373
+ return;
374
+ }
375
+
376
+ // Find all code blocks across the root and any nested shadow roots
377
+ const codeBlocks = getAllCodeBlocks(rootNode).filter(getLanguage);
378
+ if (codeBlocks.length === 0) return;
379
+
380
+ // Clear previous ranges
381
+ highlights.forEach((ranges, category) => {
382
+ if (CSS.highlights.get(category) === ranges) CSS.highlights.delete(category);
383
+ ranges.clear();
384
+ });
385
+
386
+ codeBlocks.forEach((codeBlock) => {
387
+ const rawLang = getLanguage(codeBlock);
388
+ const lang = normalizeLanguage(rawLang);
389
+ const grammar = grammars[lang];
390
+ if (!grammar) return;
391
+
392
+ // Scan all text nodes without mutating or removing Lit marker comment nodes
393
+ const textNodes = Array.from(codeBlock.childNodes).filter(
394
+ (child) => child.nodeType === Node.TEXT_NODE && child.data?.length > 0,
395
+ );
396
+ if (textNodes.length === 0) return;
397
+
398
+ matches = new Map();
399
+ textNodes.forEach((node) => {
400
+ scanRegion(node, grammar.patterns, 0, node.data.length, grammar);
401
+ });
402
+ });
403
+
404
+ highlights.forEach((ranges, category) => {
405
+ if (ranges.size) CSS.highlights.set(category, ranges);
406
+ });
407
+ };
408
+
409
+ export const scheduleHighlight = (rootNode = document) => {
410
+ if (highlightScheduled) return;
411
+ highlightScheduled = true;
412
+ requestAnimationFrame(() => {
413
+ highlightScheduled = false;
414
+ highlightAllCode(rootNode);
415
+ });
416
+ };
@@ -0,0 +1,110 @@
1
+ import { marked } from "marked";
2
+
3
+ let configured = false;
4
+
5
+ function escapeAttribute(value) {
6
+ return value
7
+ .replace(/&/g, "&amp;")
8
+ .replace(/"/g, "&quot;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;");
11
+ }
12
+
13
+ /**
14
+ * When an HTML `<base>` tag is present in the page, a markdown in-page anchor link (e.g. `#link-to-a-section`) would
15
+ * resolve against the base URL instead of the current page. This renders such links with an absolute URL
16
+ * built from the current document location, and tags them with the `anchor-link` CSS class.
17
+ *
18
+ * @param {import('marked').Renderer} renderer
19
+ * @param {import('marked').Tokens.Link} token
20
+ * @returns {string | false} the rendered link, or `false` when the link is not an in-page anchor (use the default renderer)
21
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base#in-page_anchors
22
+ */
23
+ function renderAnchorLink(renderer, { href, title, tokens }) {
24
+ if (!href?.startsWith("#") || typeof location === "undefined") {
25
+ return false;
26
+ }
27
+ const absoluteHref = `${location.href.replace(/#.*/, "")}${href}`;
28
+ const titleAttribute = title ? ` title="${escapeAttribute(title)}"` : "";
29
+ return `<a class="anchor-link" href="${escapeAttribute(absoluteHref)}"${titleAttribute}>${renderer.parser.parseInline(tokens)}</a>`;
30
+ }
31
+
32
+ /**
33
+ * Applies the in-page anchor fix to a custom renderer. A renderer passed per call, `marked(text, { renderer })`,
34
+ * replaces the global one configured by {@link configureMarkdown}, so custom renderers need it too.
35
+ *
36
+ * @param {import('marked').Renderer} renderer
37
+ */
38
+ export function fixRenderedAnchorLinks(renderer) {
39
+ const defaultLink = renderer.link;
40
+ renderer.link = function link(token) {
41
+ const anchorLink = renderAnchorLink(this, token);
42
+ return anchorLink === false ? defaultLink.call(this, token) : anchorLink;
43
+ };
44
+ }
45
+
46
+ /** Inline HTML for the first line of a paragraph-like token (paragraph, heading, list), with line breaks as spaces. */
47
+ export function firstLineHtml(token) {
48
+ // a list item's own text, without any nested list under it
49
+ const text =
50
+ token.type === "list"
51
+ ? token.items[0]?.tokens?.find((t) => t.type === "text" || t.type === "paragraph")?.text
52
+ : token.text;
53
+ return marked.parseInline((text || "").replace(/\s*\n\s*/g, " ").trim());
54
+ }
55
+
56
+ /**
57
+ * One-line HTML stand-in for a markdown block that cannot be shown on a single line: a short placeholder
58
+ * for tables and code blocks, the plain text of an HTML block, and the first line of a blockquote.
59
+ *
60
+ * @param {import('marked').Token} token - a block token from `marked.lexer`
61
+ * @returns {string} HTML for the summary line, or '' when the block can be shown inline as it is
62
+ */
63
+ export function blockSummary(token) {
64
+ switch (token?.type) {
65
+ case "table": {
66
+ const rows = token.rows.length;
67
+ return `<span class="descr-placeholder">Table · ${rows} ${rows === 1 ? "row" : "rows"}</span>`;
68
+ }
69
+ case "code":
70
+ return `<span class="descr-placeholder">Code block${token.lang ? ` · ${escapeAttribute(token.lang)}` : ""}</span>`;
71
+ case "html": {
72
+ const text = token.text
73
+ .replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, "")
74
+ .replace(/<[^>]*>/g, " ")
75
+ .replace(/[<>]/g, "")
76
+ .replace(/\s+/g, " ")
77
+ .trim();
78
+ return text || '<span class="descr-placeholder">HTML content</span>';
79
+ }
80
+ case "blockquote": {
81
+ const first = token.tokens.find((t) => t.type !== "space");
82
+ if (!first) return "";
83
+ return ["paragraph", "heading", "list", "text"].includes(first.type)
84
+ ? firstLineHtml(first)
85
+ : blockSummary(first);
86
+ }
87
+ default:
88
+ return "";
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Configures the global `marked` instance used by every `marked(...)` call that has no custom renderer.
94
+ * Idempotent: safe to import and call from several entry points.
95
+ */
96
+ export function configureMarkdown() {
97
+ if (configured) {
98
+ return;
99
+ }
100
+ configured = true;
101
+ marked.use({
102
+ renderer: {
103
+ link(token) {
104
+ return renderAnchorLink(this, token); // `false` falls back to marked's default link renderer
105
+ },
106
+ },
107
+ });
108
+ }
109
+
110
+ configureMarkdown();