@asteroidcms/core-utils 0.1.3 → 0.1.5
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 +64 -4
- package/dist/client.cjs +383 -17
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +116 -11
- package/dist/client.d.ts +116 -11
- package/dist/client.js +382 -20
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +115 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -2
- package/dist/index.d.ts +45 -2
- package/dist/index.js +113 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -356,18 +356,25 @@ getContentReadTime(article.body, {
|
|
|
356
356
|
|
|
357
357
|
## `<RichTextContent>`
|
|
358
358
|
|
|
359
|
-
Render Asteroid CMS rich-text
|
|
359
|
+
Render Asteroid CMS rich-text HTML with syntax-highlighted code blocks (via `highlight.js`), self-healing enhancements (copy buttons, blockquote decorations, callout chips), terminal/diff code-block variants, and slugified heading IDs out of the box.
|
|
360
360
|
|
|
361
361
|
```tsx
|
|
362
|
-
import { RichTextContent } from "@asteroidcms/core-utils";
|
|
362
|
+
import { RichTextContent } from "@asteroidcms/core-utils/client";
|
|
363
|
+
import { Info } from "lucide-react";
|
|
363
364
|
|
|
364
365
|
<RichTextContent
|
|
365
|
-
|
|
366
|
+
html={article.body}
|
|
367
|
+
as="article"
|
|
368
|
+
className="prose"
|
|
366
369
|
classMap={{ p: "my-2 leading-relaxed", h2: "text-2xl font-bold" }}
|
|
370
|
+
onReady={(root) => console.log("hydrated", root.querySelectorAll("h2").length)}
|
|
371
|
+
calloutIcons={{ info: <Info size={14} strokeWidth={2.4} /> }}
|
|
367
372
|
/>;
|
|
368
373
|
```
|
|
369
374
|
|
|
370
|
-
|
|
375
|
+
Additional props: `contentRef` (forwards the wrapper element, useful for `useTableOfContents`), `onReady` (fires once enhancements have run), and `calloutIcons` (per-variant icon override for `<aside data-callout data-icon>` blocks).
|
|
376
|
+
|
|
377
|
+
Or use the parser directly (server-safe, no `highlight.js`):
|
|
371
378
|
|
|
372
379
|
```ts
|
|
373
380
|
import { parseRichText } from "@asteroidcms/core-utils";
|
|
@@ -379,6 +386,59 @@ const html = parseRichText(article.body, {
|
|
|
379
386
|
});
|
|
380
387
|
```
|
|
381
388
|
|
|
389
|
+
### Table of contents
|
|
390
|
+
|
|
391
|
+
Build a live, scroll-tracked ToC from rendered content with `useTableOfContents`. Pair it with `RichTextContent`'s `contentRef` prop:
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
import { useRef } from "react";
|
|
395
|
+
import {
|
|
396
|
+
RichTextContent,
|
|
397
|
+
useTableOfContents,
|
|
398
|
+
} from "@asteroidcms/core-utils/client";
|
|
399
|
+
|
|
400
|
+
function Article({ slug, html }: { slug: string; html: string }) {
|
|
401
|
+
const contentRef = useRef<HTMLElement | null>(null);
|
|
402
|
+
const { items, activeId } = useTableOfContents(contentRef, {
|
|
403
|
+
levels: [2, 3],
|
|
404
|
+
contentKey: slug, // re-collect when content swaps
|
|
405
|
+
activationOffset: 96, // px from viewport top
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<div className="flex gap-8">
|
|
410
|
+
<RichTextContent
|
|
411
|
+
html={html}
|
|
412
|
+
as="article"
|
|
413
|
+
className="prose"
|
|
414
|
+
contentRef={contentRef}
|
|
415
|
+
/>
|
|
416
|
+
<nav>
|
|
417
|
+
{items.map((it) => (
|
|
418
|
+
<a
|
|
419
|
+
key={it.id}
|
|
420
|
+
href={`#${it.id}`}
|
|
421
|
+
className={it.id === activeId ? "font-semibold" : ""}
|
|
422
|
+
>
|
|
423
|
+
{it.text}
|
|
424
|
+
</a>
|
|
425
|
+
))}
|
|
426
|
+
</nav>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
For a static, server-side outline (RSC layouts, sitemaps, RSS), use `extractHeadingsFromHtml`:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import { extractHeadingsFromHtml } from "@asteroidcms/core-utils";
|
|
436
|
+
|
|
437
|
+
const toc = extractHeadingsFromHtml(article.body, { levels: [2, 3] });
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
See the [full rich-text docs](./docs/web-sdk-react/10-rich-text.md) for `classMap` variants, parser options, and `useTableOfContents` tuning.
|
|
441
|
+
|
|
382
442
|
---
|
|
383
443
|
|
|
384
444
|
## Advanced - bring your own Apollo client
|
package/dist/client.cjs
CHANGED
|
@@ -7,8 +7,8 @@ var client = require('@apollo/client');
|
|
|
7
7
|
var context = require('@apollo/client/link/context');
|
|
8
8
|
var error = require('@apollo/client/link/error');
|
|
9
9
|
var jsxRuntime = require('react/jsx-runtime');
|
|
10
|
+
var reactDom = require('react-dom');
|
|
10
11
|
var hljs = require('highlight.js/lib/common');
|
|
11
|
-
require('highlight.js/styles/tokyo-night-dark.css');
|
|
12
12
|
|
|
13
13
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
14
|
|
|
@@ -371,7 +371,9 @@ var DEFAULT_ALLOWLIST = [
|
|
|
371
371
|
"section",
|
|
372
372
|
"article",
|
|
373
373
|
"div",
|
|
374
|
-
"span"
|
|
374
|
+
"span",
|
|
375
|
+
"details",
|
|
376
|
+
"summary"
|
|
375
377
|
];
|
|
376
378
|
var ALLOWED_ATTRS = {
|
|
377
379
|
a: ["href", "title", "target", "rel"],
|
|
@@ -381,7 +383,8 @@ var ALLOWED_ATTRS = {
|
|
|
381
383
|
col: ["span", "width"],
|
|
382
384
|
colgroup: ["span"],
|
|
383
385
|
table: ["border", "cellpadding", "cellspacing"],
|
|
384
|
-
span: ["style"]
|
|
386
|
+
span: ["style"],
|
|
387
|
+
details: ["open"]
|
|
385
388
|
};
|
|
386
389
|
var ALLOWED_STYLE_PROPS = {
|
|
387
390
|
span: ["font-size"]
|
|
@@ -415,7 +418,8 @@ var GLOBAL_ALLOWED_ATTRS = [
|
|
|
415
418
|
"data-title",
|
|
416
419
|
"data-callout-title",
|
|
417
420
|
"data-language",
|
|
418
|
-
"data-filename"
|
|
421
|
+
"data-filename",
|
|
422
|
+
"data-icon"
|
|
419
423
|
];
|
|
420
424
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
|
|
421
425
|
function parseRichText(html, options = {}) {
|
|
@@ -425,7 +429,36 @@ function parseRichText(html, options = {}) {
|
|
|
425
429
|
working = upgradeStandaloneImages(working);
|
|
426
430
|
working = upgradeAuthoredBlockquotes(working);
|
|
427
431
|
working = flattenTableCellParagraphs(working);
|
|
428
|
-
|
|
432
|
+
working = sanitizeAndStyle(working, options);
|
|
433
|
+
if (options.autoHeadingIds !== false) {
|
|
434
|
+
working = injectHeadingIds(working);
|
|
435
|
+
}
|
|
436
|
+
return working;
|
|
437
|
+
}
|
|
438
|
+
function injectHeadingIds(html) {
|
|
439
|
+
const used = /* @__PURE__ */ new Map();
|
|
440
|
+
const existingRe = /<h[1-6]\b[^>]*\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/gi;
|
|
441
|
+
let em;
|
|
442
|
+
while ((em = existingRe.exec(html)) !== null) {
|
|
443
|
+
const id = em[2] ?? em[3] ?? em[4] ?? "";
|
|
444
|
+
if (id) used.set(id, (used.get(id) ?? 0) + 1);
|
|
445
|
+
}
|
|
446
|
+
return html.replace(
|
|
447
|
+
/<(h[1-6])\b([^>]*)>([\s\S]*?)<\/\1>/gi,
|
|
448
|
+
(full, tag, attrs, inner) => {
|
|
449
|
+
if (/\bid\s*=/i.test(attrs)) return full;
|
|
450
|
+
const text = inner.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
451
|
+
if (!text) return full;
|
|
452
|
+
const base = slugifyHeading(text) || tag;
|
|
453
|
+
const n = used.get(base) ?? 0;
|
|
454
|
+
used.set(base, n + 1);
|
|
455
|
+
const id = n === 0 ? base : `${base}-${n}`;
|
|
456
|
+
return `<${tag}${attrs} id="${id}">${inner}</${tag}>`;
|
|
457
|
+
}
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
function slugifyHeading(text) {
|
|
461
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
429
462
|
}
|
|
430
463
|
function flattenTableCellParagraphs(html) {
|
|
431
464
|
return html.replace(
|
|
@@ -800,6 +833,8 @@ function classKeyForTag(tag, attrs, openStack) {
|
|
|
800
833
|
if (tag === "p" && "data-callout-title" in attrs) return "calloutTitle";
|
|
801
834
|
if (tag === "code" && openStack[openStack.length - 1] !== "pre")
|
|
802
835
|
return "inlineCode";
|
|
836
|
+
if (tag === "details") return "collapsible";
|
|
837
|
+
if (tag === "summary") return "collapsibleTitle";
|
|
803
838
|
const known = [
|
|
804
839
|
"p",
|
|
805
840
|
"br",
|
|
@@ -966,8 +1001,6 @@ function sanitizeAndStyle(html, options) {
|
|
|
966
1001
|
}
|
|
967
1002
|
return out.join("");
|
|
968
1003
|
}
|
|
969
|
-
|
|
970
|
-
// src/components/RichTextContent.tsx
|
|
971
1004
|
var DEFAULT_CLASS_MAP = {
|
|
972
1005
|
variants: {
|
|
973
1006
|
"figure:pullquote": "relative my-8",
|
|
@@ -1002,6 +1035,46 @@ function findQuoteBody(bq) {
|
|
|
1002
1035
|
if (lastIdx < 0) return { first: null, last: null };
|
|
1003
1036
|
return { first: children[0], last: children[lastIdx] };
|
|
1004
1037
|
}
|
|
1038
|
+
var CALLOUT_ICON_SVG = {
|
|
1039
|
+
info: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 16v-5"/><path d="M12 8h.01"/></svg>`,
|
|
1040
|
+
warning: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>`,
|
|
1041
|
+
success: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>`,
|
|
1042
|
+
danger: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`,
|
|
1043
|
+
default: `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M12 2.5l1.6 5.4a4 4 0 0 0 2.5 2.5L21.5 12l-5.4 1.6a4 4 0 0 0-2.5 2.5L12 21.5l-1.6-5.4a4 4 0 0 0-2.5-2.5L2.5 12l5.4-1.6a4 4 0 0 0 2.5-2.5z"/></svg>`
|
|
1044
|
+
};
|
|
1045
|
+
function calloutVariantOf(el) {
|
|
1046
|
+
return el.getAttribute("data-variant") ?? "default";
|
|
1047
|
+
}
|
|
1048
|
+
function isIconEnabled(el) {
|
|
1049
|
+
const v = el.getAttribute("data-icon");
|
|
1050
|
+
if (v === null) return false;
|
|
1051
|
+
return v !== "false" && v !== "0";
|
|
1052
|
+
}
|
|
1053
|
+
function enhanceCallouts(root) {
|
|
1054
|
+
const callouts = root.querySelectorAll("aside[data-callout]");
|
|
1055
|
+
callouts.forEach((el) => {
|
|
1056
|
+
if (el.dataset.rtCalloutEnhanced === "1") return;
|
|
1057
|
+
el.dataset.rtCalloutEnhanced = "1";
|
|
1058
|
+
if (!isIconEnabled(el)) return;
|
|
1059
|
+
if (el.querySelector(":scope > .rt-callout-icon")) return;
|
|
1060
|
+
const variant = calloutVariantOf(el);
|
|
1061
|
+
const icon = document.createElement("span");
|
|
1062
|
+
icon.className = "rt-callout-icon";
|
|
1063
|
+
icon.dataset.variant = variant;
|
|
1064
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1065
|
+
el.prepend(icon);
|
|
1066
|
+
});
|
|
1067
|
+
const chips = [];
|
|
1068
|
+
root.querySelectorAll(
|
|
1069
|
+
"aside[data-callout] > .rt-callout-icon"
|
|
1070
|
+
).forEach((chip) => {
|
|
1071
|
+
chips.push({
|
|
1072
|
+
el: chip,
|
|
1073
|
+
variant: chip.dataset.variant || (chip.parentElement?.getAttribute("data-variant") ?? "default")
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
return chips;
|
|
1077
|
+
}
|
|
1005
1078
|
function enhanceBlockquotes(root) {
|
|
1006
1079
|
const quotes = root.querySelectorAll("blockquote");
|
|
1007
1080
|
quotes.forEach((bq) => {
|
|
@@ -1231,6 +1304,28 @@ function enhanceCodeBlocks(root) {
|
|
|
1231
1304
|
highlightCodeBlock(pre);
|
|
1232
1305
|
});
|
|
1233
1306
|
}
|
|
1307
|
+
var HLJS_THEME = `
|
|
1308
|
+
pre code.hljs { display: block; overflow-x: auto; padding: 1em; }
|
|
1309
|
+
code.hljs { padding: 3px 5px; }
|
|
1310
|
+
.hljs-meta, .hljs-comment { color: #565f89; }
|
|
1311
|
+
.hljs-tag, .hljs-doctag, .hljs-selector-id, .hljs-selector-class,
|
|
1312
|
+
.hljs-regexp, .hljs-template-tag, .hljs-selector-pseudo,
|
|
1313
|
+
.hljs-selector-attr, .hljs-variable.language_, .hljs-deletion { color: #f7768e; }
|
|
1314
|
+
.hljs-variable, .hljs-template-variable, .hljs-number, .hljs-literal,
|
|
1315
|
+
.hljs-type, .hljs-params, .hljs-link { color: #ff9e64; }
|
|
1316
|
+
.hljs-built_in, .hljs-attribute { color: #e0af68; }
|
|
1317
|
+
.hljs-selector-tag { color: #73daca; }
|
|
1318
|
+
.hljs-keyword, .hljs-title.function_, .hljs-title, .hljs-title.class_,
|
|
1319
|
+
.hljs-title.class_.inherited__, .hljs-subst, .hljs-property { color: #7dcfff; }
|
|
1320
|
+
.hljs-quote, .hljs-string, .hljs-symbol, .hljs-bullet,
|
|
1321
|
+
.hljs-addition { color: #9ece6a; }
|
|
1322
|
+
.hljs-code, .hljs-formula, .hljs-section { color: #7aa2f7; }
|
|
1323
|
+
.hljs-name, .hljs-operator, .hljs-char.escape_, .hljs-attr { color: #bb9af7; }
|
|
1324
|
+
.hljs-punctuation { color: #c0caf5; }
|
|
1325
|
+
.hljs { background: #1a1b26; color: #9aa5ce; }
|
|
1326
|
+
.hljs-emphasis { font-style: italic; }
|
|
1327
|
+
.hljs-strong { font-weight: bold; }
|
|
1328
|
+
`;
|
|
1234
1329
|
var CODEBLOCK_STYLE = `
|
|
1235
1330
|
.rt-codeblock { position: relative; }
|
|
1236
1331
|
.rt-codeblock[data-filename]:not([data-filename=""]) { padding-top: 2rem; }
|
|
@@ -1282,6 +1377,64 @@ var CODEBLOCK_STYLE = `
|
|
|
1282
1377
|
}
|
|
1283
1378
|
.rt-quote-open { margin-right: 0.15em; }
|
|
1284
1379
|
.rt-quote-close { margin-left: 0.15em; }
|
|
1380
|
+
|
|
1381
|
+
/* Callout chip ----------------------------------------------------------- */
|
|
1382
|
+
/* Layout: when a callout opts into an icon, use a 2-column grid so the chip
|
|
1383
|
+
sits left of the title + body. Consumers can override via classMap. */
|
|
1384
|
+
aside[data-callout][data-icon]:not([data-icon="false"]):not([data-icon="0"]) {
|
|
1385
|
+
display: grid;
|
|
1386
|
+
grid-template-columns: auto 1fr;
|
|
1387
|
+
column-gap: 0.85rem;
|
|
1388
|
+
align-items: start;
|
|
1389
|
+
}
|
|
1390
|
+
aside[data-callout][data-icon]:not([data-icon="false"]):not([data-icon="0"]) > .rt-callout-icon {
|
|
1391
|
+
grid-row: 1 / span 99;
|
|
1392
|
+
margin-top: 0.05rem;
|
|
1393
|
+
}
|
|
1394
|
+
.rt-callout-icon {
|
|
1395
|
+
display: inline-flex;
|
|
1396
|
+
align-items: center;
|
|
1397
|
+
justify-content: center;
|
|
1398
|
+
width: 1.6rem;
|
|
1399
|
+
height: 1.6rem;
|
|
1400
|
+
border-radius: 0.4rem;
|
|
1401
|
+
color: #fff;
|
|
1402
|
+
background: #475569;
|
|
1403
|
+
flex-shrink: 0;
|
|
1404
|
+
}
|
|
1405
|
+
.rt-callout-icon[data-variant="info"] { background: #2563eb; }
|
|
1406
|
+
.rt-callout-icon[data-variant="warning"] { background: #d97706; }
|
|
1407
|
+
.rt-callout-icon[data-variant="success"] { background: #16a34a; }
|
|
1408
|
+
.rt-callout-icon[data-variant="danger"] { background: #dc2626; }
|
|
1409
|
+
.rt-callout-icon[data-variant="default"] { background: #475569; }
|
|
1410
|
+
|
|
1411
|
+
/* Collapsible (FAQ accordion) ------------------------------------------ */
|
|
1412
|
+
/* Native <details>/<summary> with a rotating chevron. Only structural
|
|
1413
|
+
styling lives here \u2014 colors, padding, and typography belong to
|
|
1414
|
+
consumers via the \`collapsible\` and \`collapsibleTitle\` classMap keys. */
|
|
1415
|
+
details[data-collapsible] > summary {
|
|
1416
|
+
cursor: pointer;
|
|
1417
|
+
list-style: none;
|
|
1418
|
+
display: flex;
|
|
1419
|
+
align-items: center;
|
|
1420
|
+
justify-content: space-between;
|
|
1421
|
+
gap: 0.75rem;
|
|
1422
|
+
}
|
|
1423
|
+
details[data-collapsible] > summary::-webkit-details-marker { display: none; }
|
|
1424
|
+
details[data-collapsible] > summary::after {
|
|
1425
|
+
content: "";
|
|
1426
|
+
width: 0.55rem;
|
|
1427
|
+
height: 0.55rem;
|
|
1428
|
+
border-right: 1.5px solid currentColor;
|
|
1429
|
+
border-bottom: 1.5px solid currentColor;
|
|
1430
|
+
transform: rotate(-45deg) translate(-0.1rem, 0.05rem);
|
|
1431
|
+
transition: transform 0.18s ease;
|
|
1432
|
+
opacity: 0.55;
|
|
1433
|
+
flex-shrink: 0;
|
|
1434
|
+
}
|
|
1435
|
+
details[data-collapsible][open] > summary::after {
|
|
1436
|
+
transform: rotate(45deg) translate(-0.05rem, -0.05rem);
|
|
1437
|
+
}
|
|
1285
1438
|
/* highlight.js theme handles .hljs-* color classes; we only override the
|
|
1286
1439
|
default .hljs background so the per-block chrome (dark bg, terminal,
|
|
1287
1440
|
diff red/green rows) wins. */
|
|
@@ -1423,15 +1576,29 @@ function ensureCodeBlockStyles() {
|
|
|
1423
1576
|
}
|
|
1424
1577
|
const tag = document.createElement("style");
|
|
1425
1578
|
tag.id = "rt-codeblock-style";
|
|
1426
|
-
tag.textContent = CODEBLOCK_STYLE;
|
|
1579
|
+
tag.textContent = HLJS_THEME + "\n" + CODEBLOCK_STYLE;
|
|
1427
1580
|
document.head.appendChild(tag);
|
|
1428
1581
|
styleInjected = true;
|
|
1429
1582
|
}
|
|
1583
|
+
function calloutChipsEqual(a, b) {
|
|
1584
|
+
if (a.length !== b.length) return false;
|
|
1585
|
+
for (let i = 0; i < a.length; i++) {
|
|
1586
|
+
if (a[i].el !== b[i].el || a[i].variant !== b[i].variant) return false;
|
|
1587
|
+
}
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
function BuiltinCalloutIcon({ variant }) {
|
|
1591
|
+
const html = CALLOUT_ICON_SVG[variant] ?? CALLOUT_ICON_SVG.default;
|
|
1592
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { dangerouslySetInnerHTML: { __html: html } });
|
|
1593
|
+
}
|
|
1430
1594
|
function RichTextContent({
|
|
1431
1595
|
html,
|
|
1432
1596
|
classMap,
|
|
1433
1597
|
as = "div",
|
|
1434
|
-
className
|
|
1598
|
+
className,
|
|
1599
|
+
onReady,
|
|
1600
|
+
contentRef,
|
|
1601
|
+
calloutIcons
|
|
1435
1602
|
}) {
|
|
1436
1603
|
const merged = react.useMemo(
|
|
1437
1604
|
() => mergeClassMap(DEFAULT_CLASS_MAP, classMap),
|
|
@@ -1442,25 +1609,224 @@ function RichTextContent({
|
|
|
1442
1609
|
[html, merged]
|
|
1443
1610
|
);
|
|
1444
1611
|
const ref = react.useRef(null);
|
|
1612
|
+
const [chips, setChips] = react.useState([]);
|
|
1445
1613
|
react.useEffect(() => {
|
|
1446
1614
|
ensureCodeBlockStyles();
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1615
|
+
const root = ref.current;
|
|
1616
|
+
if (!root) return;
|
|
1617
|
+
if (contentRef) contentRef.current = root;
|
|
1618
|
+
const apply = () => {
|
|
1619
|
+
mo.disconnect();
|
|
1620
|
+
enhanceCodeBlocks(root);
|
|
1621
|
+
enhanceBlockquotes(root);
|
|
1622
|
+
const nextChips = enhanceCallouts(root);
|
|
1623
|
+
setChips((prev) => calloutChipsEqual(prev, nextChips) ? prev : nextChips);
|
|
1624
|
+
onReady?.(root);
|
|
1625
|
+
mo.observe(root, { childList: true, subtree: true });
|
|
1626
|
+
};
|
|
1627
|
+
let raf = 0;
|
|
1628
|
+
const mo = new MutationObserver(() => {
|
|
1629
|
+
if (raf) return;
|
|
1630
|
+
raf = requestAnimationFrame(() => {
|
|
1631
|
+
raf = 0;
|
|
1632
|
+
apply();
|
|
1633
|
+
});
|
|
1634
|
+
});
|
|
1635
|
+
apply();
|
|
1636
|
+
return () => {
|
|
1637
|
+
mo.disconnect();
|
|
1638
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1639
|
+
};
|
|
1640
|
+
}, [safe, onReady, contentRef]);
|
|
1641
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
1642
|
+
react.createElement(as, {
|
|
1643
|
+
ref,
|
|
1644
|
+
className,
|
|
1645
|
+
dangerouslySetInnerHTML: { __html: safe }
|
|
1646
|
+
}),
|
|
1647
|
+
chips.map(
|
|
1648
|
+
(chip, i) => reactDom.createPortal(
|
|
1649
|
+
calloutIcons && chip.variant in calloutIcons ? calloutIcons[chip.variant] : /* @__PURE__ */ jsxRuntime.jsx(BuiltinCalloutIcon, { variant: chip.variant }),
|
|
1650
|
+
chip.el,
|
|
1651
|
+
`${chip.variant}:${i}`
|
|
1652
|
+
)
|
|
1653
|
+
)
|
|
1654
|
+
] });
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/utils/extractHeadings.ts
|
|
1658
|
+
var DEFAULT_LEVELS = [2, 3];
|
|
1659
|
+
function slugify(text) {
|
|
1660
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1661
|
+
}
|
|
1662
|
+
function decodeBasicEntities(s) {
|
|
1663
|
+
return s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
1664
|
+
}
|
|
1665
|
+
function stripTags(s) {
|
|
1666
|
+
return decodeBasicEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
1667
|
+
}
|
|
1668
|
+
function uniqueId(base, used) {
|
|
1669
|
+
const seed = base || "section";
|
|
1670
|
+
const n = used.get(seed) ?? 0;
|
|
1671
|
+
used.set(seed, n + 1);
|
|
1672
|
+
return n === 0 ? seed : `${seed}-${n}`;
|
|
1673
|
+
}
|
|
1674
|
+
function extractHeadingsFromHtml(html, options = {}) {
|
|
1675
|
+
if (!html) return [];
|
|
1676
|
+
const levels = options.levels ?? DEFAULT_LEVELS;
|
|
1677
|
+
const slug = options.slugify ?? slugify;
|
|
1678
|
+
const used = /* @__PURE__ */ new Map();
|
|
1679
|
+
const out = [];
|
|
1680
|
+
const re = /<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi;
|
|
1681
|
+
let m;
|
|
1682
|
+
let i = 0;
|
|
1683
|
+
while ((m = re.exec(html)) !== null) {
|
|
1684
|
+
const level = Number(m[1]);
|
|
1685
|
+
if (!levels.includes(level)) continue;
|
|
1686
|
+
const attrs = m[2] ?? "";
|
|
1687
|
+
const inner = m[3] ?? "";
|
|
1688
|
+
const text = stripTags(inner);
|
|
1689
|
+
if (!text) continue;
|
|
1690
|
+
const explicitIdMatch = attrs.match(/\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/i);
|
|
1691
|
+
let id;
|
|
1692
|
+
if (explicitIdMatch) {
|
|
1693
|
+
id = explicitIdMatch[2] ?? explicitIdMatch[3] ?? explicitIdMatch[4] ?? "";
|
|
1694
|
+
if (id) used.set(id, (used.get(id) ?? 0) + 1);
|
|
1695
|
+
} else {
|
|
1696
|
+
id = uniqueId(slug(text, i), used);
|
|
1450
1697
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1698
|
+
out.push({ id, text, level });
|
|
1699
|
+
i++;
|
|
1700
|
+
}
|
|
1701
|
+
return out;
|
|
1702
|
+
}
|
|
1703
|
+
function extractHeadingsFromElement(root, options = {}) {
|
|
1704
|
+
const levels = options.levels ?? DEFAULT_LEVELS;
|
|
1705
|
+
const slug = options.slugify ?? slugify;
|
|
1706
|
+
const selector = levels.map((l) => `h${l}`).join(",");
|
|
1707
|
+
const nodes = root.querySelectorAll(selector);
|
|
1708
|
+
const used = /* @__PURE__ */ new Map();
|
|
1709
|
+
nodes.forEach((n) => {
|
|
1710
|
+
if (n.id) used.set(n.id, (used.get(n.id) ?? 0) + 1);
|
|
1456
1711
|
});
|
|
1712
|
+
const out = [];
|
|
1713
|
+
let i = 0;
|
|
1714
|
+
nodes.forEach((node) => {
|
|
1715
|
+
const level = Number(node.tagName.slice(1));
|
|
1716
|
+
const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1717
|
+
if (!text) return;
|
|
1718
|
+
if (!node.id) {
|
|
1719
|
+
node.id = uniqueId(slug(text, i), used);
|
|
1720
|
+
}
|
|
1721
|
+
if (options.scrollMarginTop != null) {
|
|
1722
|
+
node.style.scrollMarginTop = `${options.scrollMarginTop}px`;
|
|
1723
|
+
}
|
|
1724
|
+
out.push({ id: node.id, text, level });
|
|
1725
|
+
i++;
|
|
1726
|
+
});
|
|
1727
|
+
return out;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/hooks/useTableOfContents.tsx
|
|
1731
|
+
function useTableOfContents(ref, options = {}) {
|
|
1732
|
+
const {
|
|
1733
|
+
levels,
|
|
1734
|
+
contentKey = null,
|
|
1735
|
+
scrollMarginTop = 24,
|
|
1736
|
+
activationOffset = 96
|
|
1737
|
+
} = options;
|
|
1738
|
+
const [items, setItems] = react.useState([]);
|
|
1739
|
+
const [activeId, setActiveId] = react.useState("");
|
|
1740
|
+
react.useEffect(() => {
|
|
1741
|
+
const root = ref.current;
|
|
1742
|
+
if (!root) {
|
|
1743
|
+
setItems([]);
|
|
1744
|
+
setActiveId("");
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
let raf = 0;
|
|
1748
|
+
const collect = () => {
|
|
1749
|
+
const next = extractHeadingsFromElement(root, {
|
|
1750
|
+
levels,
|
|
1751
|
+
scrollMarginTop
|
|
1752
|
+
});
|
|
1753
|
+
setItems(next);
|
|
1754
|
+
setActiveId(
|
|
1755
|
+
(prev) => next.some((h) => h.id === prev) ? prev : next[0]?.id ?? ""
|
|
1756
|
+
);
|
|
1757
|
+
};
|
|
1758
|
+
raf = requestAnimationFrame(collect);
|
|
1759
|
+
const mo = new MutationObserver(() => {
|
|
1760
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1761
|
+
raf = requestAnimationFrame(collect);
|
|
1762
|
+
});
|
|
1763
|
+
mo.observe(root, { childList: true, subtree: true, characterData: true });
|
|
1764
|
+
return () => {
|
|
1765
|
+
mo.disconnect();
|
|
1766
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1767
|
+
};
|
|
1768
|
+
}, [ref, contentKey, levels, scrollMarginTop]);
|
|
1769
|
+
react.useEffect(() => {
|
|
1770
|
+
if (items.length === 0) return;
|
|
1771
|
+
const targets = items.map((it) => document.getElementById(it.id)).filter((el) => el !== null);
|
|
1772
|
+
if (targets.length === 0) return;
|
|
1773
|
+
let raf = 0;
|
|
1774
|
+
const compute = () => {
|
|
1775
|
+
raf = 0;
|
|
1776
|
+
let activeIdx = 0;
|
|
1777
|
+
for (let i = 0; i < items.length; i++) {
|
|
1778
|
+
const el = document.getElementById(items[i].id);
|
|
1779
|
+
if (!el) continue;
|
|
1780
|
+
if (el.getBoundingClientRect().top - activationOffset <= 0) {
|
|
1781
|
+
activeIdx = i;
|
|
1782
|
+
} else {
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const scroller = document.scrollingElement || document.documentElement;
|
|
1787
|
+
const scrollY = window.scrollY;
|
|
1788
|
+
const viewportH = window.innerHeight;
|
|
1789
|
+
const atBottom = scrollY + viewportH >= scroller.scrollHeight - 2;
|
|
1790
|
+
if (atBottom) {
|
|
1791
|
+
for (let i = items.length - 1; i > activeIdx; i--) {
|
|
1792
|
+
const el = document.getElementById(items[i].id);
|
|
1793
|
+
if (el && el.getBoundingClientRect().top < viewportH) {
|
|
1794
|
+
activeIdx = i;
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
setActiveId(items[activeIdx].id);
|
|
1800
|
+
};
|
|
1801
|
+
const schedule = () => {
|
|
1802
|
+
if (raf) return;
|
|
1803
|
+
raf = requestAnimationFrame(compute);
|
|
1804
|
+
};
|
|
1805
|
+
const io = new IntersectionObserver(schedule, {
|
|
1806
|
+
rootMargin: `-${activationOffset}px 0px -${Math.max(0, window.innerHeight - activationOffset - 1)}px 0px`,
|
|
1807
|
+
threshold: 0
|
|
1808
|
+
});
|
|
1809
|
+
targets.forEach((t) => io.observe(t));
|
|
1810
|
+
window.addEventListener("resize", schedule, { passive: true });
|
|
1811
|
+
compute();
|
|
1812
|
+
return () => {
|
|
1813
|
+
io.disconnect();
|
|
1814
|
+
window.removeEventListener("resize", schedule);
|
|
1815
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1816
|
+
};
|
|
1817
|
+
}, [items, activationOffset]);
|
|
1818
|
+
return { items, activeId };
|
|
1457
1819
|
}
|
|
1458
1820
|
|
|
1459
1821
|
exports.AsteroidCMSProvider = AsteroidCMSProvider;
|
|
1460
1822
|
exports.RichTextContent = RichTextContent;
|
|
1823
|
+
exports.extractHeadingsFromElement = extractHeadingsFromElement;
|
|
1824
|
+
exports.extractHeadingsFromHtml = extractHeadingsFromHtml;
|
|
1825
|
+
exports.slugify = slugify;
|
|
1461
1826
|
exports.useAsteroidCMSConfig = useAsteroidCMSConfig;
|
|
1462
1827
|
exports.useCmsContent = useCmsContent;
|
|
1463
1828
|
exports.useCmsImage = useCmsImage;
|
|
1464
1829
|
exports.useCmsMutate = useCmsMutate;
|
|
1830
|
+
exports.useTableOfContents = useTableOfContents;
|
|
1465
1831
|
//# sourceMappingURL=client.cjs.map
|
|
1466
1832
|
//# sourceMappingURL=client.cjs.map
|