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.
- package/README.md +46 -31
- package/dist/rapidoc-min.js +1424 -213
- package/dist/rapidoc-static-min.js +13542 -0
- package/package.json +47 -35
- package/src/components/api-request.ts +869 -0
- package/src/components/api-response.ts +509 -0
- package/src/components/json-tree.ts +171 -0
- package/src/components/schema-base-element.ts +315 -0
- package/src/components/schema-table.ts +430 -0
- package/src/components/schema-tree.ts +386 -0
- package/src/components/tag-input.ts +184 -0
- package/src/index.js +6 -0
- package/src/oauth-receiver.ts +29 -0
- package/src/rapidoc-mini.ts +582 -0
- package/src/rapidoc.ts +1808 -0
- package/src/static/index.d.ts +149 -0
- package/src/static/index.js +62 -0
- package/src/static/navigation.js +497 -0
- package/src/static/render.js +490 -0
- package/src/static/serializer.js +216 -0
- package/src/styles/auth-styles.js +318 -0
- package/src/styles/border-styles.js +24 -0
- package/src/styles/css-bundle.js +129 -0
- package/src/styles/custom-styles.js +8 -0
- package/src/styles/dialog-box-styles.js +102 -0
- package/src/styles/endpoint-styles.js +559 -0
- package/src/styles/flex-styles.js +16 -0
- package/src/styles/font-styles.js +432 -0
- package/src/styles/info-styles.js +36 -0
- package/src/styles/input-styles.js +555 -0
- package/src/styles/layout-styles.js +396 -0
- package/src/styles/microlighter-styles.js +99 -0
- package/src/styles/nav-styles.js +609 -0
- package/src/styles/param-list-styles.js +261 -0
- package/src/styles/schema-styles.js +288 -0
- package/src/styles/tab-styles.js +106 -0
- package/src/styles/theme-tokens.js +582 -0
- package/src/templates/advanced-search-dialog-template.js +94 -0
- package/src/templates/api-response-template.js +219 -0
- package/src/templates/callback-template.js +86 -0
- package/src/templates/code-samples-template.js +52 -0
- package/src/templates/components-template.js +86 -0
- package/src/templates/downloads-template.js +85 -0
- package/src/templates/endpoint-template.js +386 -0
- package/src/templates/expanded-endpoint-template.js +266 -0
- package/src/templates/focused-endpoint-template.js +145 -0
- package/src/templates/header-template.js +92 -0
- package/src/templates/logo-template.js +21 -0
- package/src/templates/main-body-template.js +102 -0
- package/src/templates/navbar-template.js +489 -0
- package/src/templates/overview-template.js +104 -0
- package/src/templates/request-body-template.js +651 -0
- package/src/templates/request-params-template.js +474 -0
- package/src/templates/security-scheme-template.js +1263 -0
- package/src/templates/server-template.js +152 -0
- package/src/utils/auto-color-scheme.js +99 -0
- package/src/utils/bind-navbar.js +175 -0
- package/src/utils/color-utils.js +59 -0
- package/src/utils/common-utils.js +191 -0
- package/src/utils/curl-utils.js +123 -0
- package/src/utils/highlighter.js +416 -0
- package/src/utils/markdown-utils.js +110 -0
- package/src/utils/mcp/common.js +467 -0
- package/src/utils/mcp/generators/csharp.js +242 -0
- package/src/utils/mcp/generators/go.js +298 -0
- package/src/utils/mcp/generators/java.js +330 -0
- package/src/utils/mcp/generators/python.js +248 -0
- package/src/utils/mcp/generators/rust.js +281 -0
- package/src/utils/mcp/generators/typescript.js +290 -0
- package/src/utils/mcp-generator.js +143 -0
- package/src/utils/mock-interceptor.js +508 -0
- package/src/utils/oauth-utils.js +87 -0
- package/src/utils/request-utils.js +203 -0
- package/src/utils/response-utils.js +203 -0
- package/src/utils/sanitize.js +57 -0
- package/src/utils/schema-utils.js +1927 -0
- package/src/utils/slugger.js +189 -0
- package/src/utils/spec-parser.js +915 -0
- package/src/utils/url-utils.js +211 -0
- 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, "&")
|
|
8
|
+
.replace(/"/g, """)
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">");
|
|
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();
|