@asteroidcms/core-utils 0.1.2 → 0.1.4
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 +59 -4
- package/dist/client.cjs +1684 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +316 -0
- package/dist/client.d.ts +316 -0
- package/dist/client.js +1669 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +99 -740
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -180
- package/dist/index.d.ts +42 -180
- package/dist/index.js +97 -731
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/dist/client.js
ADDED
|
@@ -0,0 +1,1669 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext, useMemo, useRef, useEffect, createElement, useState } from 'react';
|
|
3
|
+
import { ApolloProvider, useQuery, useMutation } from '@apollo/client/react';
|
|
4
|
+
import { gql, HttpLink, ApolloClient, InMemoryCache, ApolloLink, CombinedGraphQLErrors, CombinedProtocolErrors } from '@apollo/client';
|
|
5
|
+
import { SetContextLink } from '@apollo/client/link/context';
|
|
6
|
+
import { ErrorLink } from '@apollo/client/link/error';
|
|
7
|
+
import { jsx } from 'react/jsx-runtime';
|
|
8
|
+
import hljs from 'highlight.js/lib/common';
|
|
9
|
+
import 'highlight.js/styles/tokyo-night-dark.css';
|
|
10
|
+
|
|
11
|
+
var AsteroidCMSContext = createContext(null);
|
|
12
|
+
function useAsteroidCMSConfig() {
|
|
13
|
+
const ctx = useContext(AsteroidCMSContext);
|
|
14
|
+
if (!ctx) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"useAsteroidCMSConfig must be used inside <AsteroidCMSProvider>. Wrap your app with <AsteroidCMSProvider cmsUrl=... apiKey=... />."
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return ctx;
|
|
20
|
+
}
|
|
21
|
+
function createErrorLink(onError) {
|
|
22
|
+
return new ErrorLink(({ error }) => {
|
|
23
|
+
if (!onError) return;
|
|
24
|
+
if (CombinedGraphQLErrors.is(error)) {
|
|
25
|
+
error.errors.forEach((e) => onError(e));
|
|
26
|
+
} else if (CombinedProtocolErrors.is(error)) {
|
|
27
|
+
error.errors.forEach((e) => onError(e));
|
|
28
|
+
} else {
|
|
29
|
+
onError(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/apollo/createApolloClient.ts
|
|
35
|
+
var trimTrailingSlash = (s) => s.replace(/\/+$/, "");
|
|
36
|
+
var ensureLeadingSlash = (s) => s.startsWith("/") ? s : `/${s}`;
|
|
37
|
+
function resolveConfig(config) {
|
|
38
|
+
if (!config.cmsUrl) throw new Error("AsteroidCMSProvider: `cmsUrl` is required.");
|
|
39
|
+
if (!config.apiKey) throw new Error("AsteroidCMSProvider: `apiKey` is required.");
|
|
40
|
+
return {
|
|
41
|
+
cmsUrl: trimTrailingSlash(config.cmsUrl),
|
|
42
|
+
apiKey: config.apiKey,
|
|
43
|
+
graphqlPath: ensureLeadingSlash(config.graphqlPath ?? "/graphql"),
|
|
44
|
+
mediaPath: ensureLeadingSlash(config.mediaPath ?? "/media/canonical"),
|
|
45
|
+
headers: config.headers ?? {},
|
|
46
|
+
onError: config.onError
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createApolloClient(config) {
|
|
50
|
+
if (config.client) return config.client;
|
|
51
|
+
const resolved = resolveConfig(config);
|
|
52
|
+
const uri = `${resolved.cmsUrl}${resolved.graphqlPath}`;
|
|
53
|
+
const authLink = new SetContextLink((_op, prevContext) => {
|
|
54
|
+
const prevHeaders = prevContext.headers ?? {};
|
|
55
|
+
return {
|
|
56
|
+
...prevContext,
|
|
57
|
+
headers: {
|
|
58
|
+
...prevHeaders,
|
|
59
|
+
"x-api-key": resolved.apiKey,
|
|
60
|
+
...resolved.headers
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
const httpLink = new HttpLink({ uri });
|
|
65
|
+
return new ApolloClient({
|
|
66
|
+
link: ApolloLink.from([createErrorLink(resolved.onError), authLink, httpLink]),
|
|
67
|
+
cache: new InMemoryCache(config.cacheConfig),
|
|
68
|
+
defaultOptions: {
|
|
69
|
+
watchQuery: {
|
|
70
|
+
fetchPolicy: "cache-and-network",
|
|
71
|
+
errorPolicy: "all",
|
|
72
|
+
returnPartialData: false
|
|
73
|
+
},
|
|
74
|
+
query: { fetchPolicy: "network-only", errorPolicy: "all" },
|
|
75
|
+
mutate: { errorPolicy: "none" }
|
|
76
|
+
},
|
|
77
|
+
...config.apolloOptions
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function AsteroidCMSProvider({ children, ...config }) {
|
|
81
|
+
const resolved = useMemo(() => resolveConfig(config), [
|
|
82
|
+
config.cmsUrl,
|
|
83
|
+
config.apiKey,
|
|
84
|
+
config.graphqlPath,
|
|
85
|
+
config.mediaPath,
|
|
86
|
+
config.headers,
|
|
87
|
+
config.onError
|
|
88
|
+
]);
|
|
89
|
+
const client = useMemo(
|
|
90
|
+
() => createApolloClient(config),
|
|
91
|
+
// The client is intentionally rebuilt only when the identity-shaping props change.
|
|
92
|
+
[
|
|
93
|
+
config.client,
|
|
94
|
+
resolved.cmsUrl,
|
|
95
|
+
resolved.apiKey,
|
|
96
|
+
resolved.graphqlPath,
|
|
97
|
+
config.cacheConfig,
|
|
98
|
+
config.apolloOptions
|
|
99
|
+
]
|
|
100
|
+
);
|
|
101
|
+
return /* @__PURE__ */ jsx(AsteroidCMSContext.Provider, { value: resolved, children: /* @__PURE__ */ jsx(ApolloProvider, { client, children }) });
|
|
102
|
+
}
|
|
103
|
+
function useCmsContent({
|
|
104
|
+
schema_slug,
|
|
105
|
+
entrySlug,
|
|
106
|
+
select = [],
|
|
107
|
+
fullData = false,
|
|
108
|
+
limit,
|
|
109
|
+
offset,
|
|
110
|
+
status = "PUBLISHED",
|
|
111
|
+
filter,
|
|
112
|
+
search,
|
|
113
|
+
variables = {}
|
|
114
|
+
}) {
|
|
115
|
+
const isSingle = !!entrySlug;
|
|
116
|
+
const buildSelection = (items = []) => {
|
|
117
|
+
const lines = [];
|
|
118
|
+
items.forEach((item) => {
|
|
119
|
+
if (typeof item === "string") {
|
|
120
|
+
lines.push(`${item}: dataField(slug: "${item}")`);
|
|
121
|
+
} else if ("field" in item && typeof item.field === "string") {
|
|
122
|
+
if (!("select" in item)) {
|
|
123
|
+
const alias2 = item.as || item.field;
|
|
124
|
+
lines.push(`${alias2}: dataField(slug: "${item.field}")`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const alias = item.as || item.field;
|
|
128
|
+
const resolver = item.single ? "expandedReferenceObject" : "expandedReference";
|
|
129
|
+
const subSelection = item.select?.length ? buildSelection(item.select) : "data { ... }";
|
|
130
|
+
lines.push(`
|
|
131
|
+
${alias}: ${resolver}(slug: "${item.field}") {
|
|
132
|
+
${subSelection}
|
|
133
|
+
}
|
|
134
|
+
`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return lines.join("\n ").trim();
|
|
138
|
+
};
|
|
139
|
+
const selectionParts = [];
|
|
140
|
+
if (fullData) selectionParts.push("data");
|
|
141
|
+
if (select.length > 0) {
|
|
142
|
+
const userSelection = buildSelection(select);
|
|
143
|
+
if (userSelection) selectionParts.push(userSelection);
|
|
144
|
+
}
|
|
145
|
+
const selection = selectionParts.join("\n ").trim() || "id";
|
|
146
|
+
const queryVariables = {
|
|
147
|
+
schema_slug,
|
|
148
|
+
...entrySlug && { slug: entrySlug },
|
|
149
|
+
...variables
|
|
150
|
+
};
|
|
151
|
+
const fieldArgParts = ["schema_slug: $schema_slug"];
|
|
152
|
+
if (!isSingle) {
|
|
153
|
+
if (typeof limit === "number" && limit >= 0) {
|
|
154
|
+
fieldArgParts.push("limit: $limit");
|
|
155
|
+
queryVariables.limit = limit;
|
|
156
|
+
}
|
|
157
|
+
if (typeof offset === "number" && offset >= 0) {
|
|
158
|
+
fieldArgParts.push("offset: $offset");
|
|
159
|
+
queryVariables.offset = offset;
|
|
160
|
+
}
|
|
161
|
+
if (status) {
|
|
162
|
+
fieldArgParts.push("status: $status");
|
|
163
|
+
queryVariables.status = status;
|
|
164
|
+
}
|
|
165
|
+
const hasSearch = Array.isArray(search) && search.length > 0;
|
|
166
|
+
const hasFilter = filter && typeof filter === "object" && Object.keys(filter).length > 0;
|
|
167
|
+
if (hasSearch || hasFilter) {
|
|
168
|
+
const mergedFilter = hasFilter ? { ...filter } : {};
|
|
169
|
+
if (hasSearch) {
|
|
170
|
+
for (const condition of search) {
|
|
171
|
+
mergedFilter[condition.field] = {
|
|
172
|
+
regex: true,
|
|
173
|
+
value: condition.value,
|
|
174
|
+
mode: condition.mode ?? "i"
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
fieldArgParts.push("data: $filter");
|
|
179
|
+
queryVariables.filter = mergedFilter;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const varDecls = ["$schema_slug: String!"];
|
|
183
|
+
if ("limit" in queryVariables) varDecls.push("$limit: Float");
|
|
184
|
+
if ("offset" in queryVariables) varDecls.push("$offset: Float");
|
|
185
|
+
if ("status" in queryVariables) varDecls.push("$status: ContentStatus");
|
|
186
|
+
if ("filter" in queryVariables) varDecls.push("$filter: JSONObject");
|
|
187
|
+
const varBlock = varDecls.length > 0 ? `(
|
|
188
|
+
${varDecls.join("\n ")}
|
|
189
|
+
)` : "";
|
|
190
|
+
const contentFieldArgs = fieldArgParts.join(", ");
|
|
191
|
+
const queryStr = isSingle ? `
|
|
192
|
+
query Get${schema_slug}Entry($schema_slug: String!, $slug: String!) {
|
|
193
|
+
entry: contentEntry(schema_slug: $schema_slug, slug: $slug) {
|
|
194
|
+
${selection}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
` : `
|
|
198
|
+
query Get${schema_slug}Entries${varBlock} {
|
|
199
|
+
entries: contentEntries(${contentFieldArgs}) {
|
|
200
|
+
${selection}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
const GET_CONTENT = gql(queryStr);
|
|
205
|
+
const { loading, error, data, ...rest } = useQuery(GET_CONTENT, {
|
|
206
|
+
variables: queryVariables,
|
|
207
|
+
skip: !schema_slug || isSingle && !entrySlug
|
|
208
|
+
});
|
|
209
|
+
const typed = data;
|
|
210
|
+
const result = isSingle ? typed?.entry : typed?.entries;
|
|
211
|
+
return {
|
|
212
|
+
loading,
|
|
213
|
+
error,
|
|
214
|
+
data: result,
|
|
215
|
+
...rest
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function useCmsMutate({
|
|
219
|
+
schema_slug,
|
|
220
|
+
select = [],
|
|
221
|
+
fullData = false,
|
|
222
|
+
mutationType = "create",
|
|
223
|
+
entryId,
|
|
224
|
+
variables: inputVariables = {}
|
|
225
|
+
}) {
|
|
226
|
+
const buildSelection = (items = []) => {
|
|
227
|
+
const lines = [];
|
|
228
|
+
for (const item of items) {
|
|
229
|
+
if (typeof item === "string") {
|
|
230
|
+
lines.push(`${item}: dataField(slug: "${item}")`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const { field, as, single, select: subSelect } = item;
|
|
234
|
+
const alias = as || field;
|
|
235
|
+
if (!subSelect?.length) {
|
|
236
|
+
lines.push(`${alias}: dataField(slug: "${field}")`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const resolver = single ? "expandedReferenceObject" : "expandedReference";
|
|
240
|
+
const sub = buildSelection(subSelect) || "id slug";
|
|
241
|
+
lines.push(`
|
|
242
|
+
${alias}: ${resolver}(slug: "${field}") {
|
|
243
|
+
id
|
|
244
|
+
slug
|
|
245
|
+
${sub}
|
|
246
|
+
}
|
|
247
|
+
`);
|
|
248
|
+
}
|
|
249
|
+
return lines.join("\n ").trim();
|
|
250
|
+
};
|
|
251
|
+
const userSelection = buildSelection(select);
|
|
252
|
+
let selection = fullData ? "data" : "";
|
|
253
|
+
if (userSelection) {
|
|
254
|
+
selection = selection ? `${selection}
|
|
255
|
+
${userSelection}` : userSelection;
|
|
256
|
+
}
|
|
257
|
+
if (!selection) {
|
|
258
|
+
selection = "id slug status version created_at updated_at";
|
|
259
|
+
}
|
|
260
|
+
const operation = mutationType;
|
|
261
|
+
const mutationName = `${operation}ContentEntry`;
|
|
262
|
+
const varDecls = ["$schema_slug: String!"];
|
|
263
|
+
const callArgs = ["schema_slug: $schema_slug"];
|
|
264
|
+
if (operation === "create" || operation === "update") {
|
|
265
|
+
varDecls.unshift("$data: JSONObject!");
|
|
266
|
+
callArgs.unshift("data: $data");
|
|
267
|
+
}
|
|
268
|
+
if (operation === "update" || operation === "delete") {
|
|
269
|
+
varDecls.push("$id: ID!");
|
|
270
|
+
callArgs.push("id: $id");
|
|
271
|
+
}
|
|
272
|
+
const returnFields = selection.includes("data") ? selection : `${selection}
|
|
273
|
+
data`;
|
|
274
|
+
const MUTATION = gql`
|
|
275
|
+
mutation ${mutationName}(${varDecls.join(", ")}) {
|
|
276
|
+
result: ${mutationName}(${callArgs.join(", ")}) {
|
|
277
|
+
id
|
|
278
|
+
status
|
|
279
|
+
version
|
|
280
|
+
created_at
|
|
281
|
+
updated_at
|
|
282
|
+
schema {
|
|
283
|
+
id
|
|
284
|
+
slug
|
|
285
|
+
}
|
|
286
|
+
${returnFields}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
`;
|
|
290
|
+
const mutationVariables = {
|
|
291
|
+
schema_slug,
|
|
292
|
+
...operation === "update" || operation === "delete" ? { id: entryId } : {},
|
|
293
|
+
...inputVariables
|
|
294
|
+
};
|
|
295
|
+
if ((operation === "update" || operation === "delete") && !entryId) {
|
|
296
|
+
console.warn(`useCmsMutate: entryId is required for ${operation}`);
|
|
297
|
+
}
|
|
298
|
+
const [mutateFn, mutationResult] = useMutation(MUTATION, {
|
|
299
|
+
variables: mutationVariables
|
|
300
|
+
});
|
|
301
|
+
const resultData = mutationResult.data?.result;
|
|
302
|
+
return {
|
|
303
|
+
mutate: mutateFn,
|
|
304
|
+
...mutationResult,
|
|
305
|
+
data: resultData
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/utils/cmsImage.ts
|
|
310
|
+
function cmsImage(id, options) {
|
|
311
|
+
if (!id) return "";
|
|
312
|
+
const base = options.cmsUrl.replace(/\/+$/, "");
|
|
313
|
+
const path = (options.mediaPath ?? "/media/canonical").replace(/^\/?/, "/");
|
|
314
|
+
return `${base}${path}/${id}`;
|
|
315
|
+
}
|
|
316
|
+
function useCmsImage() {
|
|
317
|
+
const { cmsUrl, mediaPath } = useAsteroidCMSConfig();
|
|
318
|
+
return (id) => cmsImage(id, { cmsUrl, mediaPath });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/components/richTextParser.ts
|
|
322
|
+
var DEFAULT_ALLOWLIST = [
|
|
323
|
+
"p",
|
|
324
|
+
"br",
|
|
325
|
+
"hr",
|
|
326
|
+
"h1",
|
|
327
|
+
"h2",
|
|
328
|
+
"h3",
|
|
329
|
+
"h4",
|
|
330
|
+
"h5",
|
|
331
|
+
"h6",
|
|
332
|
+
"ul",
|
|
333
|
+
"ol",
|
|
334
|
+
"li",
|
|
335
|
+
"blockquote",
|
|
336
|
+
"pre",
|
|
337
|
+
"code",
|
|
338
|
+
"kbd",
|
|
339
|
+
"a",
|
|
340
|
+
"strong",
|
|
341
|
+
"em",
|
|
342
|
+
"u",
|
|
343
|
+
"b",
|
|
344
|
+
"i",
|
|
345
|
+
"s",
|
|
346
|
+
"strike",
|
|
347
|
+
"del",
|
|
348
|
+
"mark",
|
|
349
|
+
"small",
|
|
350
|
+
"sub",
|
|
351
|
+
"sup",
|
|
352
|
+
"table",
|
|
353
|
+
"thead",
|
|
354
|
+
"tbody",
|
|
355
|
+
"tfoot",
|
|
356
|
+
"tr",
|
|
357
|
+
"th",
|
|
358
|
+
"td",
|
|
359
|
+
"colgroup",
|
|
360
|
+
"col",
|
|
361
|
+
"figure",
|
|
362
|
+
"figcaption",
|
|
363
|
+
"img",
|
|
364
|
+
"aside",
|
|
365
|
+
"section",
|
|
366
|
+
"article",
|
|
367
|
+
"div",
|
|
368
|
+
"span"
|
|
369
|
+
];
|
|
370
|
+
var ALLOWED_ATTRS = {
|
|
371
|
+
a: ["href", "title", "target", "rel"],
|
|
372
|
+
img: ["src", "alt", "title", "width", "height", "loading"],
|
|
373
|
+
th: ["colspan", "rowspan", "scope"],
|
|
374
|
+
td: ["colspan", "rowspan"],
|
|
375
|
+
col: ["span", "width"],
|
|
376
|
+
colgroup: ["span"],
|
|
377
|
+
table: ["border", "cellpadding", "cellspacing"],
|
|
378
|
+
span: ["style"]
|
|
379
|
+
};
|
|
380
|
+
var ALLOWED_STYLE_PROPS = {
|
|
381
|
+
span: ["font-size"]
|
|
382
|
+
};
|
|
383
|
+
var STYLE_VALUE_VALIDATORS = {
|
|
384
|
+
"font-size": (v) => /^\d{1,3}(?:\.\d+)?(?:px|rem|em|%)$/.test(v.trim())
|
|
385
|
+
};
|
|
386
|
+
function sanitizeStyle(tag, style) {
|
|
387
|
+
const allowed = ALLOWED_STYLE_PROPS[tag];
|
|
388
|
+
if (!allowed || !style) return "";
|
|
389
|
+
const out = [];
|
|
390
|
+
for (const decl of style.split(";")) {
|
|
391
|
+
const idx = decl.indexOf(":");
|
|
392
|
+
if (idx === -1) continue;
|
|
393
|
+
const name = decl.slice(0, idx).trim().toLowerCase();
|
|
394
|
+
const value = decl.slice(idx + 1).trim();
|
|
395
|
+
if (!allowed.includes(name)) continue;
|
|
396
|
+
const validate = STYLE_VALUE_VALIDATORS[name];
|
|
397
|
+
if (!validate || !validate(value)) continue;
|
|
398
|
+
out.push(`${name}: ${value}`);
|
|
399
|
+
}
|
|
400
|
+
return out.join("; ");
|
|
401
|
+
}
|
|
402
|
+
var GLOBAL_ALLOWED_ATTRS = [
|
|
403
|
+
"id",
|
|
404
|
+
"class",
|
|
405
|
+
"lang",
|
|
406
|
+
"dir",
|
|
407
|
+
"data-variant",
|
|
408
|
+
"data-callout",
|
|
409
|
+
"data-title",
|
|
410
|
+
"data-callout-title",
|
|
411
|
+
"data-language",
|
|
412
|
+
"data-filename"
|
|
413
|
+
];
|
|
414
|
+
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
|
|
415
|
+
function parseRichText(html, options = {}) {
|
|
416
|
+
if (!html || typeof html !== "string") return "";
|
|
417
|
+
let working = isLegacyFormat(html) ? migrateLegacyToHtml(html) : html;
|
|
418
|
+
working = removeEmptyParagraphs(working);
|
|
419
|
+
working = upgradeStandaloneImages(working);
|
|
420
|
+
working = upgradeAuthoredBlockquotes(working);
|
|
421
|
+
working = flattenTableCellParagraphs(working);
|
|
422
|
+
working = sanitizeAndStyle(working, options);
|
|
423
|
+
if (options.autoHeadingIds !== false) {
|
|
424
|
+
working = injectHeadingIds(working);
|
|
425
|
+
}
|
|
426
|
+
return working;
|
|
427
|
+
}
|
|
428
|
+
function injectHeadingIds(html) {
|
|
429
|
+
const used = /* @__PURE__ */ new Map();
|
|
430
|
+
const existingRe = /<h[1-6]\b[^>]*\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/gi;
|
|
431
|
+
let em;
|
|
432
|
+
while ((em = existingRe.exec(html)) !== null) {
|
|
433
|
+
const id = em[2] ?? em[3] ?? em[4] ?? "";
|
|
434
|
+
if (id) used.set(id, (used.get(id) ?? 0) + 1);
|
|
435
|
+
}
|
|
436
|
+
return html.replace(
|
|
437
|
+
/<(h[1-6])\b([^>]*)>([\s\S]*?)<\/\1>/gi,
|
|
438
|
+
(full, tag, attrs, inner) => {
|
|
439
|
+
if (/\bid\s*=/i.test(attrs)) return full;
|
|
440
|
+
const text = inner.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
441
|
+
if (!text) return full;
|
|
442
|
+
const base = slugifyHeading(text) || tag;
|
|
443
|
+
const n = used.get(base) ?? 0;
|
|
444
|
+
used.set(base, n + 1);
|
|
445
|
+
const id = n === 0 ? base : `${base}-${n}`;
|
|
446
|
+
return `<${tag}${attrs} id="${id}">${inner}</${tag}>`;
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
function slugifyHeading(text) {
|
|
451
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
452
|
+
}
|
|
453
|
+
function flattenTableCellParagraphs(html) {
|
|
454
|
+
return html.replace(
|
|
455
|
+
/<(th|td)\b([^>]*)>([\s\S]*?)<\/\1>/gi,
|
|
456
|
+
(_full, tag, attrs, inner) => {
|
|
457
|
+
let flat = inner;
|
|
458
|
+
flat = flat.replace(/<p\b[^>]*>/gi, "");
|
|
459
|
+
flat = flat.replace(/<\/p>(\s*)(?=\S)/gi, "<br />$1");
|
|
460
|
+
flat = flat.replace(/<\/p>\s*$/i, "");
|
|
461
|
+
return `<${tag}${attrs}>${flat.trim()}</${tag}>`;
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
function removeEmptyParagraphs(html) {
|
|
466
|
+
return html.replace(/<p\b[^>]*>(?:\s| |<br\s*\/?>)*<\/p>/gi, "");
|
|
467
|
+
}
|
|
468
|
+
function upgradeStandaloneImages(html) {
|
|
469
|
+
return html.replace(
|
|
470
|
+
/<p\b[^>]*>\s*(<img\b[^>]*?>)\s*<\/p>/gi,
|
|
471
|
+
(_full, img) => {
|
|
472
|
+
const altMatch = img.match(/\balt\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
|
|
473
|
+
const titleMatch = img.match(/\btitle\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
|
|
474
|
+
const alt = altMatch ? altMatch[1] ?? altMatch[2] ?? "" : "";
|
|
475
|
+
const title = titleMatch ? titleMatch[1] ?? titleMatch[2] ?? "" : "";
|
|
476
|
+
const caption = title || alt;
|
|
477
|
+
const captionHtml = caption ? `<figcaption data-variant="image">${escapeHtml(caption)}</figcaption>` : "";
|
|
478
|
+
return `<figure data-variant="image">${img}${captionHtml}</figure>`;
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
function injectQuoteSpans(inner) {
|
|
483
|
+
const trimmed = inner.trim();
|
|
484
|
+
if (!trimmed) return trimmed;
|
|
485
|
+
const openSpan = `<span data-variant="quote-open">\u201C</span>`;
|
|
486
|
+
const closeSpan = `<span data-variant="quote-close">\u201D</span>`;
|
|
487
|
+
let result = trimmed.replace(
|
|
488
|
+
/^(\s*<p\b[^>]*>)/,
|
|
489
|
+
(_m, tag) => `${tag}${openSpan}`
|
|
490
|
+
);
|
|
491
|
+
if (result === trimmed) {
|
|
492
|
+
result = `${openSpan}${result}`;
|
|
493
|
+
}
|
|
494
|
+
const replaced = result.replace(
|
|
495
|
+
/(<\/p>\s*)$/,
|
|
496
|
+
(_m, tag) => `${closeSpan}${tag}`
|
|
497
|
+
);
|
|
498
|
+
if (replaced === result) {
|
|
499
|
+
result = `${result}${closeSpan}`;
|
|
500
|
+
} else {
|
|
501
|
+
result = replaced;
|
|
502
|
+
}
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
function upgradeAuthoredBlockquotes(html) {
|
|
506
|
+
const figureRanges = [];
|
|
507
|
+
const figRe = /<figure\b[\s\S]*?<\/figure>/gi;
|
|
508
|
+
let fm;
|
|
509
|
+
while ((fm = figRe.exec(html)) !== null) {
|
|
510
|
+
figureRanges.push([fm.index, fm.index + fm[0].length]);
|
|
511
|
+
}
|
|
512
|
+
const insideFigure = (pos) => figureRanges.some(([s, e]) => pos >= s && pos < e);
|
|
513
|
+
return html.replace(
|
|
514
|
+
/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi,
|
|
515
|
+
(full, inner, offset) => {
|
|
516
|
+
if (typeof offset === "number" && insideFigure(offset)) return full;
|
|
517
|
+
const lastP = inner.match(/<p\b[^>]*>([\s\S]*?)<\/p>\s*$/i);
|
|
518
|
+
if (!lastP) return full;
|
|
519
|
+
const text = stripInlineTags(decodeEntities(lastP[1])).trim();
|
|
520
|
+
const authorMatch = text.match(/^(?:[—–]+|--)\s+(.+)$/);
|
|
521
|
+
if (!authorMatch) return full;
|
|
522
|
+
const author = authorMatch[1].trim();
|
|
523
|
+
const rest = inner.slice(0, inner.length - lastP[0].length).trim();
|
|
524
|
+
if (!rest) return full;
|
|
525
|
+
const wrapped = injectQuoteSpans(rest);
|
|
526
|
+
return `<figure data-variant="pullquote"><blockquote data-variant="pullquote">${wrapped}</blockquote><figcaption data-variant="pullquote">\u2014 <span data-variant="author">${escapeHtml(author)}</span></figcaption></figure>`;
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
function isLegacyFormat(html) {
|
|
531
|
+
if (!/<div\b/i.test(html)) return false;
|
|
532
|
+
return /<div[^>]*>\s*(?:>|```|\|)/i.test(html);
|
|
533
|
+
}
|
|
534
|
+
function decodeEntities(input) {
|
|
535
|
+
return input.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
536
|
+
}
|
|
537
|
+
function escapeHtml(input) {
|
|
538
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
539
|
+
}
|
|
540
|
+
function extractLegacyLines(html) {
|
|
541
|
+
const normalized = html.replace(/\r\n?/g, "\n").replace(/<\/div>\s*<div[^>]*>/gi, "\n").replace(/<div[^>]*>/gi, "").replace(/<\/div>/gi, "\n").replace(/<br\s*\/?>(\s*)/gi, "\n").replace(/<\/p>\s*<p[^>]*>/gi, "\n\n").replace(/<p[^>]*>/gi, "").replace(/<\/p>/gi, "\n");
|
|
542
|
+
return normalized.split("\n").map((line) => line.replace(/\s+$/g, ""));
|
|
543
|
+
}
|
|
544
|
+
function stripInlineTags(input) {
|
|
545
|
+
return input.replace(/<[^>]+>/g, "");
|
|
546
|
+
}
|
|
547
|
+
function renderInline(raw) {
|
|
548
|
+
let out = decodeEntities(raw);
|
|
549
|
+
out = out.replace(
|
|
550
|
+
/`([^`]+)`/g,
|
|
551
|
+
(_m, code) => `<code>${escapeHtml(decodeEntities(code))}</code>`
|
|
552
|
+
);
|
|
553
|
+
out = out.replace(
|
|
554
|
+
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
|
555
|
+
(_m, alt, src) => `<img src="${escapeHtml(String(src).trim())}" alt="${escapeHtml(String(alt))}" />`
|
|
556
|
+
);
|
|
557
|
+
out = out.replace(
|
|
558
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
559
|
+
(_m, text, href) => `<a href="${escapeHtml(String(href).trim())}" target="_blank" rel="noreferrer noopener">${text}</a>`
|
|
560
|
+
);
|
|
561
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
562
|
+
out = out.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
563
|
+
out = out.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
564
|
+
out = out.replace(/(^|[^_])_([^_]+)_(?!_)/g, "$1<em>$2</em>");
|
|
565
|
+
return out;
|
|
566
|
+
}
|
|
567
|
+
function isCodeFence(line) {
|
|
568
|
+
return /^\s*```/.test(stripInlineTags(decodeEntities(line)).trim());
|
|
569
|
+
}
|
|
570
|
+
function isBlockquoteLine(line) {
|
|
571
|
+
return stripInlineTags(decodeEntities(line)).trim().startsWith(">");
|
|
572
|
+
}
|
|
573
|
+
function calloutHeaderOf(line) {
|
|
574
|
+
const text = stripInlineTags(decodeEntities(line)).trim();
|
|
575
|
+
const m = text.match(/^>\s*\[!\s*([^\]]+?)\s*\]\s*$/);
|
|
576
|
+
if (!m) return null;
|
|
577
|
+
const raw = m[1];
|
|
578
|
+
const lower = raw.toLowerCase();
|
|
579
|
+
const knownVariants = [
|
|
580
|
+
"info",
|
|
581
|
+
"warning",
|
|
582
|
+
"success",
|
|
583
|
+
"danger",
|
|
584
|
+
"note",
|
|
585
|
+
"tip",
|
|
586
|
+
"caution",
|
|
587
|
+
"important"
|
|
588
|
+
];
|
|
589
|
+
const variant = knownVariants.includes(lower) ? lower === "note" ? "info" : lower === "tip" ? "success" : lower === "caution" || lower === "important" ? "warning" : lower : "info";
|
|
590
|
+
return { title: raw, variant };
|
|
591
|
+
}
|
|
592
|
+
function stripBlockquoteMarker(line) {
|
|
593
|
+
return stripInlineTags(decodeEntities(line)).replace(/^\s*>\s?/, "");
|
|
594
|
+
}
|
|
595
|
+
function isTableLine(line) {
|
|
596
|
+
return /^\|.*\|$/.test(stripInlineTags(decodeEntities(line)).trim());
|
|
597
|
+
}
|
|
598
|
+
function isTableDivider(line) {
|
|
599
|
+
return /^\|\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?$/.test(
|
|
600
|
+
stripInlineTags(decodeEntities(line)).trim()
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
function splitTableRow(line) {
|
|
604
|
+
return stripInlineTags(decodeEntities(line)).trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
605
|
+
}
|
|
606
|
+
function renderTable(rows) {
|
|
607
|
+
if (rows.length === 0) return "";
|
|
608
|
+
let headerCells = [];
|
|
609
|
+
let bodyStart = 0;
|
|
610
|
+
if (rows.length >= 2 && isTableDivider(rows[1])) {
|
|
611
|
+
headerCells = splitTableRow(rows[0]);
|
|
612
|
+
bodyStart = 2;
|
|
613
|
+
}
|
|
614
|
+
const bodyRows = rows.slice(bodyStart).map(splitTableRow);
|
|
615
|
+
const head = headerCells.length ? `<thead><tr>${headerCells.map((c) => `<th>${renderInline(c)}</th>`).join("")}</tr></thead>` : "";
|
|
616
|
+
const body = bodyRows.length ? `<tbody>${bodyRows.map((cells) => `<tr>${cells.map((c) => `<td>${renderInline(c)}</td>`).join("")}</tr>`).join("")}</tbody>` : "";
|
|
617
|
+
return `<table>${head}${body}</table>`;
|
|
618
|
+
}
|
|
619
|
+
function renderCallout(title, variant, bodyLines) {
|
|
620
|
+
const body = bodyLines.map((l) => renderInline(l)).join("<br />");
|
|
621
|
+
return `<aside data-callout data-variant="${escapeHtml(variant)}" data-title="${escapeHtml(title)}">${body ? `<p>${body}</p>` : ""}</aside>`;
|
|
622
|
+
}
|
|
623
|
+
function renderBlockquote(lines) {
|
|
624
|
+
if (lines.length > 0) {
|
|
625
|
+
const last = stripInlineTags(
|
|
626
|
+
decodeEntities(lines[lines.length - 1])
|
|
627
|
+
).trim();
|
|
628
|
+
const authorMatch = last.match(/^(?:[—–]+|--)\s+(.+)$/);
|
|
629
|
+
if (authorMatch && lines.length > 1) {
|
|
630
|
+
const author = authorMatch[1].trim();
|
|
631
|
+
const quoteLines = lines.slice(0, -1);
|
|
632
|
+
const body = quoteLines.map((l) => renderInline(l)).join("<br />");
|
|
633
|
+
return `<figure data-variant="pullquote"><blockquote data-variant="pullquote"><p><span data-variant="quote-open">\u201C</span>${body}<span data-variant="quote-close">\u201D</span></p></blockquote><figcaption data-variant="pullquote">\u2014 <span data-variant="author">${escapeHtml(author)}</span></figcaption></figure>`;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return `<blockquote><p>${lines.map((l) => renderInline(l)).join("<br />")}</p></blockquote>`;
|
|
637
|
+
}
|
|
638
|
+
function isImageOnlyLine(line) {
|
|
639
|
+
const text = stripInlineTags(decodeEntities(line)).trim();
|
|
640
|
+
return /^!\[[^\]]*\]\([^)]+\)$/.test(text);
|
|
641
|
+
}
|
|
642
|
+
function renderImageFigure(line) {
|
|
643
|
+
const m = stripInlineTags(decodeEntities(line)).trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
644
|
+
if (!m) return "";
|
|
645
|
+
const alt = m[1];
|
|
646
|
+
const src = m[2].trim();
|
|
647
|
+
const img = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" />`;
|
|
648
|
+
const cap = alt ? `<figcaption data-variant="image">${escapeHtml(alt)}</figcaption>` : "";
|
|
649
|
+
return `<figure data-variant="image">${img}${cap}</figure>`;
|
|
650
|
+
}
|
|
651
|
+
function renderParagraph(lines) {
|
|
652
|
+
const body = lines.map((l) => renderInline(l)).join("<br />");
|
|
653
|
+
return body.length === 0 ? "" : `<p>${body}</p>`;
|
|
654
|
+
}
|
|
655
|
+
function migrateLegacyToHtml(html) {
|
|
656
|
+
const lines = extractLegacyLines(html);
|
|
657
|
+
const blocks = [];
|
|
658
|
+
let i = 0;
|
|
659
|
+
while (i < lines.length) {
|
|
660
|
+
const line = lines[i];
|
|
661
|
+
const trimmed = stripInlineTags(decodeEntities(line)).trim();
|
|
662
|
+
if (trimmed.length === 0) {
|
|
663
|
+
i++;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (isCodeFence(line)) {
|
|
667
|
+
const codeLines = [];
|
|
668
|
+
i++;
|
|
669
|
+
while (i < lines.length && !isCodeFence(lines[i])) {
|
|
670
|
+
codeLines.push(stripInlineTags(decodeEntities(lines[i])));
|
|
671
|
+
i++;
|
|
672
|
+
}
|
|
673
|
+
if (i < lines.length) i++;
|
|
674
|
+
blocks.push(
|
|
675
|
+
`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`
|
|
676
|
+
);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const callout = calloutHeaderOf(line);
|
|
680
|
+
if (callout) {
|
|
681
|
+
const bodyLines = [];
|
|
682
|
+
i++;
|
|
683
|
+
while (i < lines.length && isBlockquoteLine(lines[i])) {
|
|
684
|
+
bodyLines.push(stripBlockquoteMarker(lines[i]));
|
|
685
|
+
i++;
|
|
686
|
+
}
|
|
687
|
+
blocks.push(renderCallout(callout.title, callout.variant, bodyLines));
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (isBlockquoteLine(line)) {
|
|
691
|
+
const quoteLines = [];
|
|
692
|
+
while (i < lines.length && isBlockquoteLine(lines[i])) {
|
|
693
|
+
quoteLines.push(stripBlockquoteMarker(lines[i]));
|
|
694
|
+
i++;
|
|
695
|
+
}
|
|
696
|
+
blocks.push(renderBlockquote(quoteLines));
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (isTableLine(line)) {
|
|
700
|
+
const tableLines = [];
|
|
701
|
+
while (i < lines.length && isTableLine(lines[i])) {
|
|
702
|
+
tableLines.push(lines[i]);
|
|
703
|
+
i++;
|
|
704
|
+
}
|
|
705
|
+
blocks.push(renderTable(tableLines));
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (isImageOnlyLine(line)) {
|
|
709
|
+
blocks.push(renderImageFigure(line));
|
|
710
|
+
i++;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const paragraphLines = [];
|
|
714
|
+
while (i < lines.length && stripInlineTags(decodeEntities(lines[i])).trim().length > 0 && !isCodeFence(lines[i]) && !isBlockquoteLine(lines[i]) && !isTableLine(lines[i]) && !isImageOnlyLine(lines[i])) {
|
|
715
|
+
paragraphLines.push(lines[i]);
|
|
716
|
+
i++;
|
|
717
|
+
}
|
|
718
|
+
const p = renderParagraph(paragraphLines);
|
|
719
|
+
if (p) blocks.push(p);
|
|
720
|
+
}
|
|
721
|
+
return blocks.join("");
|
|
722
|
+
}
|
|
723
|
+
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
724
|
+
"br",
|
|
725
|
+
"hr",
|
|
726
|
+
"img",
|
|
727
|
+
"input",
|
|
728
|
+
"meta",
|
|
729
|
+
"link",
|
|
730
|
+
"source",
|
|
731
|
+
"wbr",
|
|
732
|
+
"col"
|
|
733
|
+
]);
|
|
734
|
+
function tokenize(html) {
|
|
735
|
+
const tokens = [];
|
|
736
|
+
let i = 0;
|
|
737
|
+
while (i < html.length) {
|
|
738
|
+
const lt = html.indexOf("<", i);
|
|
739
|
+
if (lt === -1) {
|
|
740
|
+
tokens.push({ type: "text", value: html.slice(i) });
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
if (lt > i) tokens.push({ type: "text", value: html.slice(i, lt) });
|
|
744
|
+
if (html.startsWith("<!--", lt)) {
|
|
745
|
+
const end2 = html.indexOf("-->", lt + 4);
|
|
746
|
+
if (end2 === -1) {
|
|
747
|
+
i = html.length;
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
tokens.push({ type: "comment" });
|
|
751
|
+
i = end2 + 3;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
let end = lt + 1;
|
|
755
|
+
let inQuote = null;
|
|
756
|
+
while (end < html.length) {
|
|
757
|
+
const ch = html[end];
|
|
758
|
+
if (inQuote) {
|
|
759
|
+
if (ch === inQuote) inQuote = null;
|
|
760
|
+
} else {
|
|
761
|
+
if (ch === '"' || ch === "'") inQuote = ch;
|
|
762
|
+
else if (ch === ">") break;
|
|
763
|
+
}
|
|
764
|
+
end++;
|
|
765
|
+
}
|
|
766
|
+
if (end >= html.length) {
|
|
767
|
+
tokens.push({ type: "text", value: html.slice(lt) });
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
const tagContent = html.slice(lt + 1, end).trim();
|
|
771
|
+
if (tagContent.startsWith("/")) {
|
|
772
|
+
tokens.push({
|
|
773
|
+
type: "close",
|
|
774
|
+
tag: tagContent.slice(1).trim().toLowerCase()
|
|
775
|
+
});
|
|
776
|
+
} else {
|
|
777
|
+
const selfClosingMark = tagContent.endsWith("/");
|
|
778
|
+
const inner = selfClosingMark ? tagContent.slice(0, -1).trim() : tagContent;
|
|
779
|
+
const spaceIdx = inner.search(/\s/);
|
|
780
|
+
const tag = (spaceIdx === -1 ? inner : inner.slice(0, spaceIdx)).toLowerCase();
|
|
781
|
+
const attrStr = spaceIdx === -1 ? "" : inner.slice(spaceIdx + 1);
|
|
782
|
+
const attrs = parseAttrs(attrStr);
|
|
783
|
+
tokens.push({
|
|
784
|
+
type: "open",
|
|
785
|
+
tag,
|
|
786
|
+
attrs,
|
|
787
|
+
selfClosing: selfClosingMark || VOID_TAGS.has(tag)
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
i = end + 1;
|
|
791
|
+
}
|
|
792
|
+
return tokens;
|
|
793
|
+
}
|
|
794
|
+
function parseAttrs(str) {
|
|
795
|
+
const out = {};
|
|
796
|
+
const re = /([a-zA-Z_][a-zA-Z0-9_:-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
797
|
+
let m;
|
|
798
|
+
while ((m = re.exec(str)) !== null) {
|
|
799
|
+
const name = m[1].toLowerCase();
|
|
800
|
+
const value = m[2] ?? m[3] ?? m[4] ?? "";
|
|
801
|
+
out[name] = decodeEntities(value);
|
|
802
|
+
}
|
|
803
|
+
return out;
|
|
804
|
+
}
|
|
805
|
+
function isAttrAllowed(tag, attr) {
|
|
806
|
+
if (attr.startsWith("on")) return false;
|
|
807
|
+
if (GLOBAL_ALLOWED_ATTRS.includes(attr)) return true;
|
|
808
|
+
const list = ALLOWED_ATTRS[tag];
|
|
809
|
+
return Boolean(list && list.includes(attr));
|
|
810
|
+
}
|
|
811
|
+
function isSafeUrl(value) {
|
|
812
|
+
const v = value.trim().toLowerCase();
|
|
813
|
+
if (v.startsWith("javascript:") || v.startsWith("data:") && !v.startsWith("data:image/"))
|
|
814
|
+
return false;
|
|
815
|
+
if (v.startsWith("vbscript:") || v.startsWith("file:")) return false;
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
function escapeAttr(value) {
|
|
819
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
820
|
+
}
|
|
821
|
+
function classKeyForTag(tag, attrs, openStack) {
|
|
822
|
+
if (tag === "aside" && "data-callout" in attrs) return "callout";
|
|
823
|
+
if (tag === "p" && "data-callout-title" in attrs) return "calloutTitle";
|
|
824
|
+
if (tag === "code" && openStack[openStack.length - 1] !== "pre")
|
|
825
|
+
return "inlineCode";
|
|
826
|
+
const known = [
|
|
827
|
+
"p",
|
|
828
|
+
"br",
|
|
829
|
+
"hr",
|
|
830
|
+
"h1",
|
|
831
|
+
"h2",
|
|
832
|
+
"h3",
|
|
833
|
+
"h4",
|
|
834
|
+
"h5",
|
|
835
|
+
"h6",
|
|
836
|
+
"ul",
|
|
837
|
+
"ol",
|
|
838
|
+
"li",
|
|
839
|
+
"blockquote",
|
|
840
|
+
"pre",
|
|
841
|
+
"code",
|
|
842
|
+
"kbd",
|
|
843
|
+
"a",
|
|
844
|
+
"strong",
|
|
845
|
+
"em",
|
|
846
|
+
"u",
|
|
847
|
+
"s",
|
|
848
|
+
"table",
|
|
849
|
+
"thead",
|
|
850
|
+
"tbody",
|
|
851
|
+
"tr",
|
|
852
|
+
"th",
|
|
853
|
+
"td",
|
|
854
|
+
"figure",
|
|
855
|
+
"figcaption",
|
|
856
|
+
"img",
|
|
857
|
+
"span"
|
|
858
|
+
];
|
|
859
|
+
return known.includes(tag) ? tag : null;
|
|
860
|
+
}
|
|
861
|
+
function variantClass(classMap, matchKey, variant) {
|
|
862
|
+
if (!classMap?.variants || !matchKey || !variant) return void 0;
|
|
863
|
+
return classMap.variants[`${matchKey}:${variant}`];
|
|
864
|
+
}
|
|
865
|
+
function sanitizeAndStyle(html, options) {
|
|
866
|
+
const allowlist = options.allowlist ?? DEFAULT_ALLOWLIST;
|
|
867
|
+
const classMap = options.classMap;
|
|
868
|
+
const tokens = tokenize(html);
|
|
869
|
+
const out = [];
|
|
870
|
+
const openStack = [];
|
|
871
|
+
const skipStack = [];
|
|
872
|
+
const tableWrapperStack = [];
|
|
873
|
+
for (let tokenIdx = 0; tokenIdx < tokens.length; tokenIdx++) {
|
|
874
|
+
const token = tokens[tokenIdx];
|
|
875
|
+
if (token.type === "comment") continue;
|
|
876
|
+
if (token.type === "text") {
|
|
877
|
+
if (skipStack.length > 0 && skipStack[skipStack.length - 1].dropChildren)
|
|
878
|
+
continue;
|
|
879
|
+
out.push(token.value);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (token.type === "open") {
|
|
883
|
+
const allowed = allowlist.includes(token.tag);
|
|
884
|
+
const dropChildren = token.tag === "script" || token.tag === "style";
|
|
885
|
+
if (!allowed) {
|
|
886
|
+
if (!token.selfClosing)
|
|
887
|
+
skipStack.push({ tag: token.tag, dropChildren });
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const safeAttrs = {};
|
|
891
|
+
for (const [name, value] of Object.entries(token.attrs)) {
|
|
892
|
+
if (!isAttrAllowed(token.tag, name)) continue;
|
|
893
|
+
if (URL_ATTRS.has(name) && !isSafeUrl(value)) continue;
|
|
894
|
+
if (name === "style") {
|
|
895
|
+
const cleaned = sanitizeStyle(token.tag, value);
|
|
896
|
+
if (cleaned) safeAttrs.style = cleaned;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
safeAttrs[name] = value;
|
|
900
|
+
}
|
|
901
|
+
const matchKey = classKeyForTag(token.tag, token.attrs, openStack);
|
|
902
|
+
const variant = token.attrs["data-variant"];
|
|
903
|
+
const classes = [];
|
|
904
|
+
if (safeAttrs.class) classes.push(safeAttrs.class);
|
|
905
|
+
if (matchKey && classMap?.[matchKey])
|
|
906
|
+
classes.push(classMap[matchKey]);
|
|
907
|
+
const variantCls = variantClass(classMap, matchKey, variant);
|
|
908
|
+
if (variantCls) classes.push(variantCls);
|
|
909
|
+
if (token.tag === "a") {
|
|
910
|
+
if (safeAttrs.target === "_blank" && !safeAttrs.rel)
|
|
911
|
+
safeAttrs.rel = "noreferrer noopener";
|
|
912
|
+
}
|
|
913
|
+
const merged = classes.join(" ").trim();
|
|
914
|
+
if (merged) safeAttrs.class = merged;
|
|
915
|
+
else delete safeAttrs.class;
|
|
916
|
+
const attrStr = Object.entries(safeAttrs).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ");
|
|
917
|
+
if (token.tag === "table") {
|
|
918
|
+
const wrapperClass = classMap?.tableWrapper;
|
|
919
|
+
if (wrapperClass) {
|
|
920
|
+
out.push(`<div class="${escapeAttr(wrapperClass)}">`);
|
|
921
|
+
tableWrapperStack.push(true);
|
|
922
|
+
} else {
|
|
923
|
+
tableWrapperStack.push(false);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
out.push(
|
|
927
|
+
`<${token.tag}${attrStr ? " " + attrStr : ""}${token.selfClosing ? " /" : ""}>`
|
|
928
|
+
);
|
|
929
|
+
if (!token.selfClosing) openStack.push(token.tag);
|
|
930
|
+
if (token.tag === "aside" && "data-callout" in token.attrs && token.attrs["data-title"]) {
|
|
931
|
+
const title = token.attrs["data-title"];
|
|
932
|
+
let lookahead = tokenIdx + 1;
|
|
933
|
+
while (lookahead < tokens.length) {
|
|
934
|
+
const next2 = tokens[lookahead];
|
|
935
|
+
if (next2.type === "comment") {
|
|
936
|
+
lookahead++;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (next2.type === "text" && next2.value.trim() === "") {
|
|
940
|
+
lookahead++;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
const next = tokens[lookahead];
|
|
946
|
+
const alreadyHasTitle = next && next.type === "open" && next.tag === "p" && "data-callout-title" in next.attrs;
|
|
947
|
+
if (!alreadyHasTitle) {
|
|
948
|
+
const titleClasses = [];
|
|
949
|
+
if (classMap?.calloutTitle) titleClasses.push(classMap.calloutTitle);
|
|
950
|
+
const titleVariantCls = variantClass(
|
|
951
|
+
classMap,
|
|
952
|
+
"calloutTitle",
|
|
953
|
+
token.attrs["data-variant"]
|
|
954
|
+
);
|
|
955
|
+
if (titleVariantCls) titleClasses.push(titleVariantCls);
|
|
956
|
+
const titleClassAttr = titleClasses.length ? ` class="${escapeAttr(titleClasses.join(" "))}"` : "";
|
|
957
|
+
out.push(
|
|
958
|
+
`<p data-callout-title="true"${titleClassAttr}>${escapeHtml(title)}</p>`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (token.type === "close") {
|
|
965
|
+
if (skipStack.length > 0 && skipStack[skipStack.length - 1].tag === token.tag) {
|
|
966
|
+
skipStack.pop();
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (!allowlist.includes(token.tag)) continue;
|
|
970
|
+
const idx = openStack.lastIndexOf(token.tag);
|
|
971
|
+
if (idx === -1) continue;
|
|
972
|
+
while (openStack.length > idx + 1) {
|
|
973
|
+
const orphan = openStack.pop();
|
|
974
|
+
out.push(`</${orphan}>`);
|
|
975
|
+
}
|
|
976
|
+
openStack.pop();
|
|
977
|
+
out.push(`</${token.tag}>`);
|
|
978
|
+
if (token.tag === "table" && tableWrapperStack.pop()) {
|
|
979
|
+
out.push(`</div>`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
while (openStack.length > 0) {
|
|
984
|
+
const tag = openStack.pop();
|
|
985
|
+
out.push(`</${tag}>`);
|
|
986
|
+
if (tag === "table" && tableWrapperStack.pop()) {
|
|
987
|
+
out.push(`</div>`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return out.join("");
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/components/RichTextContent.tsx
|
|
994
|
+
var DEFAULT_CLASS_MAP = {
|
|
995
|
+
variants: {
|
|
996
|
+
"figure:pullquote": "relative my-8",
|
|
997
|
+
"blockquote:pullquote": "relative italic text-lg leading-snug tracking-[-0.01em] sm:text-xl",
|
|
998
|
+
"figcaption:pullquote": "mt-4 pt-3 border-t border-current/15 not-italic text-xs uppercase tracking-[0.14em] opacity-80",
|
|
999
|
+
"span:quote-open": "mr-1 font-serif text-[1.4em] leading-none align-[-0.15em] opacity-60 select-none",
|
|
1000
|
+
"span:quote-close": "ml-1 font-serif text-[1.4em] leading-none align-[-0.15em] opacity-60 select-none",
|
|
1001
|
+
"span:author": "ml-1 font-semibold not-italic tracking-[0.16em] text-current"
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
function mergeClassMap(defaults, user) {
|
|
1005
|
+
if (!user) return defaults;
|
|
1006
|
+
return {
|
|
1007
|
+
...defaults,
|
|
1008
|
+
...user,
|
|
1009
|
+
variants: { ...defaults.variants, ...user.variants }
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
var COPY_ICON_SVG = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
|
|
1013
|
+
var CHECK_ICON_SVG = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
|
1014
|
+
var ATTRIBUTION_RE = /^(?:[—–-]+|--)\s+\S/;
|
|
1015
|
+
function isAttributionEl(el) {
|
|
1016
|
+
if (!el) return false;
|
|
1017
|
+
if (el.getAttribute("data-variant") === "attribution") return true;
|
|
1018
|
+
return ATTRIBUTION_RE.test((el.textContent ?? "").trim());
|
|
1019
|
+
}
|
|
1020
|
+
function findQuoteBody(bq) {
|
|
1021
|
+
const children = Array.from(bq.children);
|
|
1022
|
+
if (children.length === 0) return { first: null, last: null };
|
|
1023
|
+
let lastIdx = children.length - 1;
|
|
1024
|
+
while (lastIdx >= 0 && isAttributionEl(children[lastIdx])) lastIdx--;
|
|
1025
|
+
if (lastIdx < 0) return { first: null, last: null };
|
|
1026
|
+
return { first: children[0], last: children[lastIdx] };
|
|
1027
|
+
}
|
|
1028
|
+
function enhanceBlockquotes(root) {
|
|
1029
|
+
const quotes = root.querySelectorAll("blockquote");
|
|
1030
|
+
quotes.forEach((bq) => {
|
|
1031
|
+
if (bq.dataset.rtQuoted === "1") return;
|
|
1032
|
+
if (bq.closest('figure[data-variant="pullquote"]')) return;
|
|
1033
|
+
bq.dataset.rtQuoted = "1";
|
|
1034
|
+
const { first, last } = findQuoteBody(bq);
|
|
1035
|
+
if (!first || !last) return;
|
|
1036
|
+
const open = document.createElement("span");
|
|
1037
|
+
open.className = "rt-quote-open";
|
|
1038
|
+
open.setAttribute("aria-hidden", "true");
|
|
1039
|
+
open.textContent = "\u201C";
|
|
1040
|
+
const close = document.createElement("span");
|
|
1041
|
+
close.className = "rt-quote-close";
|
|
1042
|
+
close.setAttribute("aria-hidden", "true");
|
|
1043
|
+
close.textContent = "\u201D";
|
|
1044
|
+
first.prepend(open);
|
|
1045
|
+
last.append(close);
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function escapeHtml2(s) {
|
|
1049
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1050
|
+
}
|
|
1051
|
+
function preserveSpaces(html) {
|
|
1052
|
+
return html.replace(/ {2,}/g, (m) => " ".repeat(m.length));
|
|
1053
|
+
}
|
|
1054
|
+
function highlightSource(src, lang) {
|
|
1055
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
1056
|
+
try {
|
|
1057
|
+
return hljs.highlight(src, { language: lang, ignoreIllegals: true }).value;
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return escapeHtml2(src);
|
|
1062
|
+
}
|
|
1063
|
+
function highlightLine(line, lang) {
|
|
1064
|
+
if (!line) return "";
|
|
1065
|
+
return preserveSpaces(highlightSource(line, lang));
|
|
1066
|
+
}
|
|
1067
|
+
function highlightCodeBlock(pre) {
|
|
1068
|
+
const lang = pre.dataset.language;
|
|
1069
|
+
if (!lang) return;
|
|
1070
|
+
const code = pre.querySelector("code");
|
|
1071
|
+
if (!code) return;
|
|
1072
|
+
if (code.dataset.rtHighlighted === "1") return;
|
|
1073
|
+
const source = code.textContent ?? "";
|
|
1074
|
+
code.innerHTML = highlightSource(source, lang);
|
|
1075
|
+
code.classList.add("hljs");
|
|
1076
|
+
code.dataset.rtHighlighted = "1";
|
|
1077
|
+
}
|
|
1078
|
+
var DIFF_SEPARATOR_RE = /\n?@@---@@\n?/;
|
|
1079
|
+
function diffLines(a, b) {
|
|
1080
|
+
const n = a.length;
|
|
1081
|
+
const m = b.length;
|
|
1082
|
+
const dp = Array.from(
|
|
1083
|
+
{ length: n + 1 },
|
|
1084
|
+
() => new Array(m + 1).fill(0)
|
|
1085
|
+
);
|
|
1086
|
+
for (let i2 = 1; i2 <= n; i2++) {
|
|
1087
|
+
for (let j2 = 1; j2 <= m; j2++) {
|
|
1088
|
+
dp[i2][j2] = a[i2 - 1] === b[j2 - 1] ? dp[i2 - 1][j2 - 1] + 1 : Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const ops = [];
|
|
1092
|
+
let i = n;
|
|
1093
|
+
let j = m;
|
|
1094
|
+
while (i > 0 && j > 0) {
|
|
1095
|
+
if (a[i - 1] === b[j - 1]) {
|
|
1096
|
+
ops.unshift({ t: "eq", line: a[i - 1] });
|
|
1097
|
+
i--;
|
|
1098
|
+
j--;
|
|
1099
|
+
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
|
|
1100
|
+
ops.unshift({ t: "rem", line: a[i - 1] });
|
|
1101
|
+
i--;
|
|
1102
|
+
} else {
|
|
1103
|
+
ops.unshift({ t: "add", line: b[j - 1] });
|
|
1104
|
+
j--;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
while (i > 0) ops.unshift({ t: "rem", line: a[--i] });
|
|
1108
|
+
while (j > 0) ops.unshift({ t: "add", line: b[--j] });
|
|
1109
|
+
return ops;
|
|
1110
|
+
}
|
|
1111
|
+
function renderDiff(before, after, lang) {
|
|
1112
|
+
const ops = diffLines(before.split("\n"), after.split("\n"));
|
|
1113
|
+
const rows = [];
|
|
1114
|
+
let aNo = 0;
|
|
1115
|
+
let bNo = 0;
|
|
1116
|
+
let k = 0;
|
|
1117
|
+
while (k < ops.length) {
|
|
1118
|
+
const op = ops[k];
|
|
1119
|
+
if (op.t === "eq") {
|
|
1120
|
+
aNo++;
|
|
1121
|
+
bNo++;
|
|
1122
|
+
rows.push({
|
|
1123
|
+
left: { n: aNo, line: op.line, cls: "" },
|
|
1124
|
+
right: { n: bNo, line: op.line, cls: "" }
|
|
1125
|
+
});
|
|
1126
|
+
k++;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const rems = [];
|
|
1130
|
+
while (k < ops.length && ops[k].t === "rem") {
|
|
1131
|
+
rems.push(ops[k].line);
|
|
1132
|
+
k++;
|
|
1133
|
+
}
|
|
1134
|
+
const adds = [];
|
|
1135
|
+
while (k < ops.length && ops[k].t === "add") {
|
|
1136
|
+
adds.push(ops[k].line);
|
|
1137
|
+
k++;
|
|
1138
|
+
}
|
|
1139
|
+
const len = Math.max(rems.length, adds.length);
|
|
1140
|
+
for (let x = 0; x < len; x++) {
|
|
1141
|
+
const remLine = x < rems.length ? rems[x] : null;
|
|
1142
|
+
const addLine = x < adds.length ? adds[x] : null;
|
|
1143
|
+
rows.push({
|
|
1144
|
+
left: {
|
|
1145
|
+
n: remLine !== null ? ++aNo : null,
|
|
1146
|
+
line: remLine,
|
|
1147
|
+
cls: remLine !== null ? "rt-diff-rem" : "rt-diff-empty"
|
|
1148
|
+
},
|
|
1149
|
+
right: {
|
|
1150
|
+
n: addLine !== null ? ++bNo : null,
|
|
1151
|
+
line: addLine,
|
|
1152
|
+
cls: addLine !== null ? "rt-diff-add" : "rt-diff-empty"
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const cell = (c, side) => {
|
|
1158
|
+
const sign = side === "l" ? "-" : "+";
|
|
1159
|
+
const showSign = c.cls === "rt-diff-rem" || c.cls === "rt-diff-add";
|
|
1160
|
+
return `<td class="rt-diff-num">${c.n ?? ""}</td><td class="rt-diff-sign">${showSign ? sign : ""}</td><td class="rt-diff-line ${c.cls}">${c.line === null ? "" : highlightLine(c.line, lang) || " "}</td>`;
|
|
1161
|
+
};
|
|
1162
|
+
const body = rows.map((r) => `<tr>${cell(r.left, "l")}${cell(r.right, "r")}</tr>`).join("");
|
|
1163
|
+
return `<table class="rt-diff-table"><tbody>${body}</tbody></table>`;
|
|
1164
|
+
}
|
|
1165
|
+
function makeCopyButton(getText, opts = {}) {
|
|
1166
|
+
const btn = document.createElement("button");
|
|
1167
|
+
btn.type = "button";
|
|
1168
|
+
btn.className = opts.className ?? "rt-codeblock__copy";
|
|
1169
|
+
btn.setAttribute("aria-label", opts.label ?? "Copy code");
|
|
1170
|
+
btn.innerHTML = COPY_ICON_SVG;
|
|
1171
|
+
btn.addEventListener("click", async () => {
|
|
1172
|
+
try {
|
|
1173
|
+
await navigator.clipboard.writeText(getText());
|
|
1174
|
+
btn.innerHTML = CHECK_ICON_SVG;
|
|
1175
|
+
btn.classList.add("is-copied");
|
|
1176
|
+
window.setTimeout(() => {
|
|
1177
|
+
btn.innerHTML = COPY_ICON_SVG;
|
|
1178
|
+
btn.classList.remove("is-copied");
|
|
1179
|
+
}, 1500);
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
return btn;
|
|
1184
|
+
}
|
|
1185
|
+
function buildCodeBlockLabel(pre) {
|
|
1186
|
+
const filename = pre.dataset.filename;
|
|
1187
|
+
if (!filename) return null;
|
|
1188
|
+
const tag = document.createElement("span");
|
|
1189
|
+
tag.className = "rt-codeblock__label";
|
|
1190
|
+
const file = document.createElement("span");
|
|
1191
|
+
file.className = "rt-codeblock__filename rt-codeblock__language";
|
|
1192
|
+
file.textContent = filename;
|
|
1193
|
+
tag.appendChild(file);
|
|
1194
|
+
return tag;
|
|
1195
|
+
}
|
|
1196
|
+
function enhanceCodeBlocks(root) {
|
|
1197
|
+
const blocks = root.querySelectorAll("pre");
|
|
1198
|
+
blocks.forEach((pre) => {
|
|
1199
|
+
if (pre.dataset.rtEnhanced === "1") return;
|
|
1200
|
+
pre.dataset.rtEnhanced = "1";
|
|
1201
|
+
pre.classList.add("rt-codeblock");
|
|
1202
|
+
const variant = pre.dataset.variant;
|
|
1203
|
+
if (variant === "diff") {
|
|
1204
|
+
const codeEl = pre.querySelector("code");
|
|
1205
|
+
const src = codeEl?.textContent ?? "";
|
|
1206
|
+
const [beforeSrc = "", afterSrc = ""] = src.split(DIFF_SEPARATOR_RE);
|
|
1207
|
+
pre.innerHTML = renderDiff(beforeSrc, afterSrc, pre.dataset.language);
|
|
1208
|
+
const bar = document.createElement("div");
|
|
1209
|
+
bar.className = "rt-diff-copy-bar";
|
|
1210
|
+
const leftHalf = document.createElement("div");
|
|
1211
|
+
leftHalf.className = "rt-diff-copy-half";
|
|
1212
|
+
leftHalf.appendChild(
|
|
1213
|
+
makeCopyButton(() => beforeSrc, {
|
|
1214
|
+
className: "rt-diff-copy",
|
|
1215
|
+
label: "Copy before"
|
|
1216
|
+
})
|
|
1217
|
+
);
|
|
1218
|
+
const rightHalf = document.createElement("div");
|
|
1219
|
+
rightHalf.className = "rt-diff-copy-half";
|
|
1220
|
+
rightHalf.appendChild(
|
|
1221
|
+
makeCopyButton(() => afterSrc, {
|
|
1222
|
+
className: "rt-diff-copy",
|
|
1223
|
+
label: "Copy after"
|
|
1224
|
+
})
|
|
1225
|
+
);
|
|
1226
|
+
bar.appendChild(leftHalf);
|
|
1227
|
+
bar.appendChild(rightHalf);
|
|
1228
|
+
pre.prepend(bar);
|
|
1229
|
+
const label2 = buildCodeBlockLabel(pre);
|
|
1230
|
+
if (label2) pre.prepend(label2);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (variant === "terminal") {
|
|
1234
|
+
pre.classList.add("rt-terminal");
|
|
1235
|
+
const codeEl = pre.querySelector("code");
|
|
1236
|
+
const source = codeEl?.textContent ?? "";
|
|
1237
|
+
if (codeEl) {
|
|
1238
|
+
const lang = pre.dataset.language || "sh";
|
|
1239
|
+
const lines = source.split("\n");
|
|
1240
|
+
codeEl.innerHTML = lines.map(
|
|
1241
|
+
(line) => `<span class="rt-term-line"><span class="rt-term-prompt" aria-hidden="true">$</span> ${highlightLine(line, lang) || " "}</span>`
|
|
1242
|
+
).join("");
|
|
1243
|
+
}
|
|
1244
|
+
pre.appendChild(makeCopyButton(() => source, { label: "Copy commands" }));
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const label = buildCodeBlockLabel(pre);
|
|
1248
|
+
if (label) pre.prepend(label);
|
|
1249
|
+
pre.appendChild(
|
|
1250
|
+
makeCopyButton(
|
|
1251
|
+
() => pre.querySelector("code")?.textContent ?? pre.innerText
|
|
1252
|
+
)
|
|
1253
|
+
);
|
|
1254
|
+
highlightCodeBlock(pre);
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
var CODEBLOCK_STYLE = `
|
|
1258
|
+
.rt-codeblock { position: relative; }
|
|
1259
|
+
.rt-codeblock[data-filename]:not([data-filename=""]) { padding-top: 2rem; }
|
|
1260
|
+
.rt-codeblock__label {
|
|
1261
|
+
position: absolute; top: 0.45rem; left: 0.85rem;
|
|
1262
|
+
display: inline-flex; align-items: center; gap: 0.5rem;
|
|
1263
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1264
|
+
pointer-events: none;
|
|
1265
|
+
}
|
|
1266
|
+
.rt-codeblock__filename {
|
|
1267
|
+
font-size: 0.72rem; letter-spacing: 0.02em;
|
|
1268
|
+
color: #d1d5db;
|
|
1269
|
+
}
|
|
1270
|
+
.rt-codeblock__sep {
|
|
1271
|
+
color: #6b7280;
|
|
1272
|
+
font-size: 0.72rem;
|
|
1273
|
+
}
|
|
1274
|
+
.rt-codeblock__language {
|
|
1275
|
+
font-size: 0.6rem; letter-spacing: 0.08em;
|
|
1276
|
+
text-transform: lowercase;
|
|
1277
|
+
color: #9ca3af;
|
|
1278
|
+
padding: 0.05rem 0.35rem;
|
|
1279
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
1280
|
+
border-radius: 0.25rem;
|
|
1281
|
+
}
|
|
1282
|
+
.rt-codeblock__copy {
|
|
1283
|
+
position: absolute; top: 0.4rem; right: 0.4rem;
|
|
1284
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1285
|
+
width: 1.75rem; height: 1.75rem; padding: 0;
|
|
1286
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
1287
|
+
border-radius: 0.375rem;
|
|
1288
|
+
background: rgba(255,255,255,0.04);
|
|
1289
|
+
color: #d1d5db;
|
|
1290
|
+
cursor: pointer;
|
|
1291
|
+
opacity: 0; transition: opacity 120ms ease, background 120ms ease, color 120ms ease;
|
|
1292
|
+
}
|
|
1293
|
+
.rt-codeblock:hover .rt-codeblock__copy,
|
|
1294
|
+
.rt-codeblock:focus-within .rt-codeblock__copy { opacity: 1; }
|
|
1295
|
+
.rt-codeblock__copy:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
|
1296
|
+
.rt-codeblock__copy.is-copied { color: #34d399; opacity: 1; }
|
|
1297
|
+
@media (hover: none) { .rt-codeblock__copy { opacity: 1; } }
|
|
1298
|
+
.rt-quote-open, .rt-quote-close {
|
|
1299
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
1300
|
+
font-size: 1.4em;
|
|
1301
|
+
line-height: 0;
|
|
1302
|
+
vertical-align: -0.15em;
|
|
1303
|
+
opacity: 0.6;
|
|
1304
|
+
user-select: none;
|
|
1305
|
+
}
|
|
1306
|
+
.rt-quote-open { margin-right: 0.15em; }
|
|
1307
|
+
.rt-quote-close { margin-left: 0.15em; }
|
|
1308
|
+
/* highlight.js theme handles .hljs-* color classes; we only override the
|
|
1309
|
+
default .hljs background so the per-block chrome (dark bg, terminal,
|
|
1310
|
+
diff red/green rows) wins. */
|
|
1311
|
+
.rt-codeblock .hljs,
|
|
1312
|
+
.rt-codeblock code.hljs { background: transparent; padding: 0; }
|
|
1313
|
+
|
|
1314
|
+
/* Terminal variant ------------------------------------------------------- */
|
|
1315
|
+
.rt-codeblock.rt-terminal,
|
|
1316
|
+
.rt-codeblock[data-variant="terminal"] {
|
|
1317
|
+
position: relative;
|
|
1318
|
+
padding-top: 2.25rem;
|
|
1319
|
+
background: #0b0b0d;
|
|
1320
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
1321
|
+
border-radius: 0.65rem;
|
|
1322
|
+
box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset,
|
|
1323
|
+
0 10px 30px -10px rgba(0,0,0,0.6);
|
|
1324
|
+
}
|
|
1325
|
+
.rt-codeblock.rt-terminal::before,
|
|
1326
|
+
.rt-codeblock[data-variant="terminal"]::before {
|
|
1327
|
+
content: "";
|
|
1328
|
+
position: absolute; top: 0; left: 0; right: 0; height: 1.75rem;
|
|
1329
|
+
background: linear-gradient(#1a1a1d, #141417);
|
|
1330
|
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
1331
|
+
border-radius: 0.65rem 0.65rem 0 0;
|
|
1332
|
+
}
|
|
1333
|
+
.rt-codeblock.rt-terminal::after,
|
|
1334
|
+
.rt-codeblock[data-variant="terminal"]::after {
|
|
1335
|
+
content: "";
|
|
1336
|
+
position: absolute; top: 0.55rem; left: 0.75rem;
|
|
1337
|
+
width: 0.65rem; height: 0.65rem; border-radius: 50%;
|
|
1338
|
+
background: #ff5f57;
|
|
1339
|
+
box-shadow:
|
|
1340
|
+
1.1rem 0 0 0 #febc2e,
|
|
1341
|
+
2.2rem 0 0 0 #28c840;
|
|
1342
|
+
}
|
|
1343
|
+
.rt-codeblock.rt-terminal code,
|
|
1344
|
+
.rt-codeblock[data-variant="terminal"] code {
|
|
1345
|
+
color: #d4d4d8;
|
|
1346
|
+
display: block;
|
|
1347
|
+
}
|
|
1348
|
+
.rt-term-line { display: block; white-space: pre-wrap; }
|
|
1349
|
+
.rt-term-prompt {
|
|
1350
|
+
color: #28c840;
|
|
1351
|
+
font-weight: 600;
|
|
1352
|
+
margin-right: 0.35em;
|
|
1353
|
+
user-select: none;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/* Diff variant ----------------------------------------------------------- */
|
|
1357
|
+
.rt-codeblock[data-variant="diff"] {
|
|
1358
|
+
padding: 0;
|
|
1359
|
+
overflow: hidden;
|
|
1360
|
+
background: #0b0b0d;
|
|
1361
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
1362
|
+
}
|
|
1363
|
+
.rt-codeblock[data-variant="diff"][data-filename]:not([data-filename=""]) {
|
|
1364
|
+
padding-top: 2rem;
|
|
1365
|
+
}
|
|
1366
|
+
.rt-diff-table {
|
|
1367
|
+
width: 100%;
|
|
1368
|
+
border-collapse: collapse;
|
|
1369
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1370
|
+
font-size: 0.78rem;
|
|
1371
|
+
line-height: 1.55;
|
|
1372
|
+
color: #e5e7eb;
|
|
1373
|
+
table-layout: fixed;
|
|
1374
|
+
}
|
|
1375
|
+
.rt-diff-table colgroup { display: none; }
|
|
1376
|
+
.rt-diff-table td {
|
|
1377
|
+
padding: 0 0.5rem;
|
|
1378
|
+
vertical-align: top;
|
|
1379
|
+
white-space: pre-wrap;
|
|
1380
|
+
word-break: break-word;
|
|
1381
|
+
}
|
|
1382
|
+
.rt-diff-table td.rt-diff-num {
|
|
1383
|
+
width: 2.25rem;
|
|
1384
|
+
text-align: right;
|
|
1385
|
+
color: rgba(156,163,175,0.55);
|
|
1386
|
+
user-select: none;
|
|
1387
|
+
background: rgba(255,255,255,0.02);
|
|
1388
|
+
}
|
|
1389
|
+
.rt-diff-table td.rt-diff-sign {
|
|
1390
|
+
width: 0.85rem;
|
|
1391
|
+
text-align: center;
|
|
1392
|
+
user-select: none;
|
|
1393
|
+
color: rgba(255,255,255,0.45);
|
|
1394
|
+
}
|
|
1395
|
+
.rt-diff-table td.rt-diff-line.rt-diff-rem {
|
|
1396
|
+
background: rgba(248,113,113,0.12);
|
|
1397
|
+
color: #fecaca;
|
|
1398
|
+
}
|
|
1399
|
+
.rt-diff-table tr:has(td.rt-diff-rem) td.rt-diff-sign:first-of-type {
|
|
1400
|
+
color: #f87171;
|
|
1401
|
+
}
|
|
1402
|
+
.rt-diff-table td.rt-diff-line.rt-diff-add {
|
|
1403
|
+
background: rgba(74,222,128,0.12);
|
|
1404
|
+
color: #bbf7d0;
|
|
1405
|
+
}
|
|
1406
|
+
.rt-diff-table tr:has(td.rt-diff-add) td.rt-diff-sign + td + td + td.rt-diff-sign {
|
|
1407
|
+
color: #4ade80;
|
|
1408
|
+
}
|
|
1409
|
+
.rt-diff-table td.rt-diff-line.rt-diff-empty {
|
|
1410
|
+
background: rgba(255,255,255,0.025);
|
|
1411
|
+
}
|
|
1412
|
+
.rt-diff-table tr td:nth-child(4) { border-left: 1px solid rgba(255,255,255,0.06); }
|
|
1413
|
+
.rt-diff-copy-bar {
|
|
1414
|
+
position: absolute; top: 0.4rem; left: 0; right: 0; z-index: 2;
|
|
1415
|
+
display: grid; grid-template-columns: 1fr 1fr;
|
|
1416
|
+
pointer-events: none;
|
|
1417
|
+
}
|
|
1418
|
+
.rt-diff-copy-half {
|
|
1419
|
+
display: flex;
|
|
1420
|
+
justify-content: flex-end;
|
|
1421
|
+
padding-right: 0.4rem;
|
|
1422
|
+
}
|
|
1423
|
+
.rt-diff-copy {
|
|
1424
|
+
pointer-events: auto;
|
|
1425
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1426
|
+
width: 1.6rem; height: 1.6rem; padding: 0;
|
|
1427
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
1428
|
+
border-radius: 0.375rem;
|
|
1429
|
+
background: rgba(20,20,23,0.85);
|
|
1430
|
+
color: #d1d5db;
|
|
1431
|
+
cursor: pointer;
|
|
1432
|
+
opacity: 0; transition: opacity 120ms ease, background 120ms ease, color 120ms ease;
|
|
1433
|
+
}
|
|
1434
|
+
.rt-codeblock[data-variant="diff"]:hover .rt-diff-copy,
|
|
1435
|
+
.rt-codeblock[data-variant="diff"]:focus-within .rt-diff-copy { opacity: 1; }
|
|
1436
|
+
.rt-diff-copy:hover { background: rgba(0,0,0,0.92); color: #fff; }
|
|
1437
|
+
.rt-diff-copy.is-copied { color: #34d399; opacity: 1; }
|
|
1438
|
+
@media (hover: none) { .rt-diff-copy { opacity: 1; } }
|
|
1439
|
+
`;
|
|
1440
|
+
var styleInjected = false;
|
|
1441
|
+
function ensureCodeBlockStyles() {
|
|
1442
|
+
if (styleInjected || typeof document === "undefined") return;
|
|
1443
|
+
if (document.getElementById("rt-codeblock-style")) {
|
|
1444
|
+
styleInjected = true;
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const tag = document.createElement("style");
|
|
1448
|
+
tag.id = "rt-codeblock-style";
|
|
1449
|
+
tag.textContent = CODEBLOCK_STYLE;
|
|
1450
|
+
document.head.appendChild(tag);
|
|
1451
|
+
styleInjected = true;
|
|
1452
|
+
}
|
|
1453
|
+
function RichTextContent({
|
|
1454
|
+
html,
|
|
1455
|
+
classMap,
|
|
1456
|
+
as = "div",
|
|
1457
|
+
className,
|
|
1458
|
+
onReady,
|
|
1459
|
+
contentRef
|
|
1460
|
+
}) {
|
|
1461
|
+
const merged = useMemo(
|
|
1462
|
+
() => mergeClassMap(DEFAULT_CLASS_MAP, classMap),
|
|
1463
|
+
[classMap]
|
|
1464
|
+
);
|
|
1465
|
+
const safe = useMemo(
|
|
1466
|
+
() => parseRichText(html, { classMap: merged }),
|
|
1467
|
+
[html, merged]
|
|
1468
|
+
);
|
|
1469
|
+
const ref = useRef(null);
|
|
1470
|
+
useEffect(() => {
|
|
1471
|
+
ensureCodeBlockStyles();
|
|
1472
|
+
const root = ref.current;
|
|
1473
|
+
if (!root) return;
|
|
1474
|
+
if (contentRef) contentRef.current = root;
|
|
1475
|
+
const apply = () => {
|
|
1476
|
+
mo.disconnect();
|
|
1477
|
+
enhanceCodeBlocks(root);
|
|
1478
|
+
enhanceBlockquotes(root);
|
|
1479
|
+
onReady?.(root);
|
|
1480
|
+
mo.observe(root, { childList: true, subtree: true });
|
|
1481
|
+
};
|
|
1482
|
+
let raf = 0;
|
|
1483
|
+
const mo = new MutationObserver(() => {
|
|
1484
|
+
if (raf) return;
|
|
1485
|
+
raf = requestAnimationFrame(() => {
|
|
1486
|
+
raf = 0;
|
|
1487
|
+
apply();
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1490
|
+
apply();
|
|
1491
|
+
return () => {
|
|
1492
|
+
mo.disconnect();
|
|
1493
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1494
|
+
};
|
|
1495
|
+
}, [safe, onReady, contentRef]);
|
|
1496
|
+
return createElement(as, {
|
|
1497
|
+
ref,
|
|
1498
|
+
className,
|
|
1499
|
+
dangerouslySetInnerHTML: { __html: safe }
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// src/utils/extractHeadings.ts
|
|
1504
|
+
var DEFAULT_LEVELS = [2, 3];
|
|
1505
|
+
function slugify(text) {
|
|
1506
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1507
|
+
}
|
|
1508
|
+
function decodeBasicEntities(s) {
|
|
1509
|
+
return s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
1510
|
+
}
|
|
1511
|
+
function stripTags(s) {
|
|
1512
|
+
return decodeBasicEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
1513
|
+
}
|
|
1514
|
+
function uniqueId(base, used) {
|
|
1515
|
+
const seed = base || "section";
|
|
1516
|
+
const n = used.get(seed) ?? 0;
|
|
1517
|
+
used.set(seed, n + 1);
|
|
1518
|
+
return n === 0 ? seed : `${seed}-${n}`;
|
|
1519
|
+
}
|
|
1520
|
+
function extractHeadingsFromHtml(html, options = {}) {
|
|
1521
|
+
if (!html) return [];
|
|
1522
|
+
const levels = options.levels ?? DEFAULT_LEVELS;
|
|
1523
|
+
const slug = options.slugify ?? slugify;
|
|
1524
|
+
const used = /* @__PURE__ */ new Map();
|
|
1525
|
+
const out = [];
|
|
1526
|
+
const re = /<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi;
|
|
1527
|
+
let m;
|
|
1528
|
+
let i = 0;
|
|
1529
|
+
while ((m = re.exec(html)) !== null) {
|
|
1530
|
+
const level = Number(m[1]);
|
|
1531
|
+
if (!levels.includes(level)) continue;
|
|
1532
|
+
const attrs = m[2] ?? "";
|
|
1533
|
+
const inner = m[3] ?? "";
|
|
1534
|
+
const text = stripTags(inner);
|
|
1535
|
+
if (!text) continue;
|
|
1536
|
+
const explicitIdMatch = attrs.match(/\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/i);
|
|
1537
|
+
let id;
|
|
1538
|
+
if (explicitIdMatch) {
|
|
1539
|
+
id = explicitIdMatch[2] ?? explicitIdMatch[3] ?? explicitIdMatch[4] ?? "";
|
|
1540
|
+
if (id) used.set(id, (used.get(id) ?? 0) + 1);
|
|
1541
|
+
} else {
|
|
1542
|
+
id = uniqueId(slug(text, i), used);
|
|
1543
|
+
}
|
|
1544
|
+
out.push({ id, text, level });
|
|
1545
|
+
i++;
|
|
1546
|
+
}
|
|
1547
|
+
return out;
|
|
1548
|
+
}
|
|
1549
|
+
function extractHeadingsFromElement(root, options = {}) {
|
|
1550
|
+
const levels = options.levels ?? DEFAULT_LEVELS;
|
|
1551
|
+
const slug = options.slugify ?? slugify;
|
|
1552
|
+
const selector = levels.map((l) => `h${l}`).join(",");
|
|
1553
|
+
const nodes = root.querySelectorAll(selector);
|
|
1554
|
+
const used = /* @__PURE__ */ new Map();
|
|
1555
|
+
nodes.forEach((n) => {
|
|
1556
|
+
if (n.id) used.set(n.id, (used.get(n.id) ?? 0) + 1);
|
|
1557
|
+
});
|
|
1558
|
+
const out = [];
|
|
1559
|
+
let i = 0;
|
|
1560
|
+
nodes.forEach((node) => {
|
|
1561
|
+
const level = Number(node.tagName.slice(1));
|
|
1562
|
+
const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1563
|
+
if (!text) return;
|
|
1564
|
+
if (!node.id) {
|
|
1565
|
+
node.id = uniqueId(slug(text, i), used);
|
|
1566
|
+
}
|
|
1567
|
+
if (options.scrollMarginTop != null) {
|
|
1568
|
+
node.style.scrollMarginTop = `${options.scrollMarginTop}px`;
|
|
1569
|
+
}
|
|
1570
|
+
out.push({ id: node.id, text, level });
|
|
1571
|
+
i++;
|
|
1572
|
+
});
|
|
1573
|
+
return out;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/hooks/useTableOfContents.tsx
|
|
1577
|
+
function useTableOfContents(ref, options = {}) {
|
|
1578
|
+
const {
|
|
1579
|
+
levels,
|
|
1580
|
+
contentKey = null,
|
|
1581
|
+
scrollMarginTop = 24,
|
|
1582
|
+
activationOffset = 96
|
|
1583
|
+
} = options;
|
|
1584
|
+
const [items, setItems] = useState([]);
|
|
1585
|
+
const [activeId, setActiveId] = useState("");
|
|
1586
|
+
useEffect(() => {
|
|
1587
|
+
const root = ref.current;
|
|
1588
|
+
if (!root) {
|
|
1589
|
+
setItems([]);
|
|
1590
|
+
setActiveId("");
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
let raf = 0;
|
|
1594
|
+
const collect = () => {
|
|
1595
|
+
const next = extractHeadingsFromElement(root, {
|
|
1596
|
+
levels,
|
|
1597
|
+
scrollMarginTop
|
|
1598
|
+
});
|
|
1599
|
+
setItems(next);
|
|
1600
|
+
setActiveId(
|
|
1601
|
+
(prev) => next.some((h) => h.id === prev) ? prev : next[0]?.id ?? ""
|
|
1602
|
+
);
|
|
1603
|
+
};
|
|
1604
|
+
raf = requestAnimationFrame(collect);
|
|
1605
|
+
const mo = new MutationObserver(() => {
|
|
1606
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1607
|
+
raf = requestAnimationFrame(collect);
|
|
1608
|
+
});
|
|
1609
|
+
mo.observe(root, { childList: true, subtree: true, characterData: true });
|
|
1610
|
+
return () => {
|
|
1611
|
+
mo.disconnect();
|
|
1612
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1613
|
+
};
|
|
1614
|
+
}, [ref, contentKey, levels, scrollMarginTop]);
|
|
1615
|
+
useEffect(() => {
|
|
1616
|
+
if (items.length === 0) return;
|
|
1617
|
+
const targets = items.map((it) => document.getElementById(it.id)).filter((el) => el !== null);
|
|
1618
|
+
if (targets.length === 0) return;
|
|
1619
|
+
let raf = 0;
|
|
1620
|
+
const compute = () => {
|
|
1621
|
+
raf = 0;
|
|
1622
|
+
let activeIdx = 0;
|
|
1623
|
+
for (let i = 0; i < items.length; i++) {
|
|
1624
|
+
const el = document.getElementById(items[i].id);
|
|
1625
|
+
if (!el) continue;
|
|
1626
|
+
if (el.getBoundingClientRect().top - activationOffset <= 0) {
|
|
1627
|
+
activeIdx = i;
|
|
1628
|
+
} else {
|
|
1629
|
+
break;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
const scroller = document.scrollingElement || document.documentElement;
|
|
1633
|
+
const scrollY = window.scrollY;
|
|
1634
|
+
const viewportH = window.innerHeight;
|
|
1635
|
+
const atBottom = scrollY + viewportH >= scroller.scrollHeight - 2;
|
|
1636
|
+
if (atBottom) {
|
|
1637
|
+
for (let i = items.length - 1; i > activeIdx; i--) {
|
|
1638
|
+
const el = document.getElementById(items[i].id);
|
|
1639
|
+
if (el && el.getBoundingClientRect().top < viewportH) {
|
|
1640
|
+
activeIdx = i;
|
|
1641
|
+
break;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
setActiveId(items[activeIdx].id);
|
|
1646
|
+
};
|
|
1647
|
+
const schedule = () => {
|
|
1648
|
+
if (raf) return;
|
|
1649
|
+
raf = requestAnimationFrame(compute);
|
|
1650
|
+
};
|
|
1651
|
+
const io = new IntersectionObserver(schedule, {
|
|
1652
|
+
rootMargin: `-${activationOffset}px 0px -${Math.max(0, window.innerHeight - activationOffset - 1)}px 0px`,
|
|
1653
|
+
threshold: 0
|
|
1654
|
+
});
|
|
1655
|
+
targets.forEach((t) => io.observe(t));
|
|
1656
|
+
window.addEventListener("resize", schedule, { passive: true });
|
|
1657
|
+
compute();
|
|
1658
|
+
return () => {
|
|
1659
|
+
io.disconnect();
|
|
1660
|
+
window.removeEventListener("resize", schedule);
|
|
1661
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1662
|
+
};
|
|
1663
|
+
}, [items, activationOffset]);
|
|
1664
|
+
return { items, activeId };
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
export { AsteroidCMSProvider, RichTextContent, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate, useTableOfContents };
|
|
1668
|
+
//# sourceMappingURL=client.js.map
|
|
1669
|
+
//# sourceMappingURL=client.js.map
|