@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/dist/client.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { createContext, useContext, useMemo, useRef, useEffect, createElement } from 'react';
|
|
2
|
+
import { createContext, useContext, useMemo, useRef, useState, useEffect, Fragment, createElement } from 'react';
|
|
3
3
|
import { ApolloProvider, useQuery, useMutation } from '@apollo/client/react';
|
|
4
4
|
import { gql, HttpLink, ApolloClient, InMemoryCache, ApolloLink, CombinedGraphQLErrors, CombinedProtocolErrors } from '@apollo/client';
|
|
5
5
|
import { SetContextLink } from '@apollo/client/link/context';
|
|
6
6
|
import { ErrorLink } from '@apollo/client/link/error';
|
|
7
|
-
import { jsx } from 'react/jsx-runtime';
|
|
7
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
8
9
|
import hljs from 'highlight.js/lib/common';
|
|
9
|
-
import 'highlight.js/styles/tokyo-night-dark.css';
|
|
10
10
|
|
|
11
11
|
var AsteroidCMSContext = createContext(null);
|
|
12
12
|
function useAsteroidCMSConfig() {
|
|
@@ -365,7 +365,9 @@ var DEFAULT_ALLOWLIST = [
|
|
|
365
365
|
"section",
|
|
366
366
|
"article",
|
|
367
367
|
"div",
|
|
368
|
-
"span"
|
|
368
|
+
"span",
|
|
369
|
+
"details",
|
|
370
|
+
"summary"
|
|
369
371
|
];
|
|
370
372
|
var ALLOWED_ATTRS = {
|
|
371
373
|
a: ["href", "title", "target", "rel"],
|
|
@@ -375,7 +377,8 @@ var ALLOWED_ATTRS = {
|
|
|
375
377
|
col: ["span", "width"],
|
|
376
378
|
colgroup: ["span"],
|
|
377
379
|
table: ["border", "cellpadding", "cellspacing"],
|
|
378
|
-
span: ["style"]
|
|
380
|
+
span: ["style"],
|
|
381
|
+
details: ["open"]
|
|
379
382
|
};
|
|
380
383
|
var ALLOWED_STYLE_PROPS = {
|
|
381
384
|
span: ["font-size"]
|
|
@@ -409,7 +412,8 @@ var GLOBAL_ALLOWED_ATTRS = [
|
|
|
409
412
|
"data-title",
|
|
410
413
|
"data-callout-title",
|
|
411
414
|
"data-language",
|
|
412
|
-
"data-filename"
|
|
415
|
+
"data-filename",
|
|
416
|
+
"data-icon"
|
|
413
417
|
];
|
|
414
418
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
|
|
415
419
|
function parseRichText(html, options = {}) {
|
|
@@ -419,7 +423,36 @@ function parseRichText(html, options = {}) {
|
|
|
419
423
|
working = upgradeStandaloneImages(working);
|
|
420
424
|
working = upgradeAuthoredBlockquotes(working);
|
|
421
425
|
working = flattenTableCellParagraphs(working);
|
|
422
|
-
|
|
426
|
+
working = sanitizeAndStyle(working, options);
|
|
427
|
+
if (options.autoHeadingIds !== false) {
|
|
428
|
+
working = injectHeadingIds(working);
|
|
429
|
+
}
|
|
430
|
+
return working;
|
|
431
|
+
}
|
|
432
|
+
function injectHeadingIds(html) {
|
|
433
|
+
const used = /* @__PURE__ */ new Map();
|
|
434
|
+
const existingRe = /<h[1-6]\b[^>]*\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/gi;
|
|
435
|
+
let em;
|
|
436
|
+
while ((em = existingRe.exec(html)) !== null) {
|
|
437
|
+
const id = em[2] ?? em[3] ?? em[4] ?? "";
|
|
438
|
+
if (id) used.set(id, (used.get(id) ?? 0) + 1);
|
|
439
|
+
}
|
|
440
|
+
return html.replace(
|
|
441
|
+
/<(h[1-6])\b([^>]*)>([\s\S]*?)<\/\1>/gi,
|
|
442
|
+
(full, tag, attrs, inner) => {
|
|
443
|
+
if (/\bid\s*=/i.test(attrs)) return full;
|
|
444
|
+
const text = inner.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
445
|
+
if (!text) return full;
|
|
446
|
+
const base = slugifyHeading(text) || tag;
|
|
447
|
+
const n = used.get(base) ?? 0;
|
|
448
|
+
used.set(base, n + 1);
|
|
449
|
+
const id = n === 0 ? base : `${base}-${n}`;
|
|
450
|
+
return `<${tag}${attrs} id="${id}">${inner}</${tag}>`;
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
function slugifyHeading(text) {
|
|
455
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
423
456
|
}
|
|
424
457
|
function flattenTableCellParagraphs(html) {
|
|
425
458
|
return html.replace(
|
|
@@ -794,6 +827,8 @@ function classKeyForTag(tag, attrs, openStack) {
|
|
|
794
827
|
if (tag === "p" && "data-callout-title" in attrs) return "calloutTitle";
|
|
795
828
|
if (tag === "code" && openStack[openStack.length - 1] !== "pre")
|
|
796
829
|
return "inlineCode";
|
|
830
|
+
if (tag === "details") return "collapsible";
|
|
831
|
+
if (tag === "summary") return "collapsibleTitle";
|
|
797
832
|
const known = [
|
|
798
833
|
"p",
|
|
799
834
|
"br",
|
|
@@ -960,8 +995,6 @@ function sanitizeAndStyle(html, options) {
|
|
|
960
995
|
}
|
|
961
996
|
return out.join("");
|
|
962
997
|
}
|
|
963
|
-
|
|
964
|
-
// src/components/RichTextContent.tsx
|
|
965
998
|
var DEFAULT_CLASS_MAP = {
|
|
966
999
|
variants: {
|
|
967
1000
|
"figure:pullquote": "relative my-8",
|
|
@@ -996,6 +1029,46 @@ function findQuoteBody(bq) {
|
|
|
996
1029
|
if (lastIdx < 0) return { first: null, last: null };
|
|
997
1030
|
return { first: children[0], last: children[lastIdx] };
|
|
998
1031
|
}
|
|
1032
|
+
var CALLOUT_ICON_SVG = {
|
|
1033
|
+
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>`,
|
|
1034
|
+
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>`,
|
|
1035
|
+
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>`,
|
|
1036
|
+
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>`,
|
|
1037
|
+
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>`
|
|
1038
|
+
};
|
|
1039
|
+
function calloutVariantOf(el) {
|
|
1040
|
+
return el.getAttribute("data-variant") ?? "default";
|
|
1041
|
+
}
|
|
1042
|
+
function isIconEnabled(el) {
|
|
1043
|
+
const v = el.getAttribute("data-icon");
|
|
1044
|
+
if (v === null) return false;
|
|
1045
|
+
return v !== "false" && v !== "0";
|
|
1046
|
+
}
|
|
1047
|
+
function enhanceCallouts(root) {
|
|
1048
|
+
const callouts = root.querySelectorAll("aside[data-callout]");
|
|
1049
|
+
callouts.forEach((el) => {
|
|
1050
|
+
if (el.dataset.rtCalloutEnhanced === "1") return;
|
|
1051
|
+
el.dataset.rtCalloutEnhanced = "1";
|
|
1052
|
+
if (!isIconEnabled(el)) return;
|
|
1053
|
+
if (el.querySelector(":scope > .rt-callout-icon")) return;
|
|
1054
|
+
const variant = calloutVariantOf(el);
|
|
1055
|
+
const icon = document.createElement("span");
|
|
1056
|
+
icon.className = "rt-callout-icon";
|
|
1057
|
+
icon.dataset.variant = variant;
|
|
1058
|
+
icon.setAttribute("aria-hidden", "true");
|
|
1059
|
+
el.prepend(icon);
|
|
1060
|
+
});
|
|
1061
|
+
const chips = [];
|
|
1062
|
+
root.querySelectorAll(
|
|
1063
|
+
"aside[data-callout] > .rt-callout-icon"
|
|
1064
|
+
).forEach((chip) => {
|
|
1065
|
+
chips.push({
|
|
1066
|
+
el: chip,
|
|
1067
|
+
variant: chip.dataset.variant || (chip.parentElement?.getAttribute("data-variant") ?? "default")
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
return chips;
|
|
1071
|
+
}
|
|
999
1072
|
function enhanceBlockquotes(root) {
|
|
1000
1073
|
const quotes = root.querySelectorAll("blockquote");
|
|
1001
1074
|
quotes.forEach((bq) => {
|
|
@@ -1225,6 +1298,28 @@ function enhanceCodeBlocks(root) {
|
|
|
1225
1298
|
highlightCodeBlock(pre);
|
|
1226
1299
|
});
|
|
1227
1300
|
}
|
|
1301
|
+
var HLJS_THEME = `
|
|
1302
|
+
pre code.hljs { display: block; overflow-x: auto; padding: 1em; }
|
|
1303
|
+
code.hljs { padding: 3px 5px; }
|
|
1304
|
+
.hljs-meta, .hljs-comment { color: #565f89; }
|
|
1305
|
+
.hljs-tag, .hljs-doctag, .hljs-selector-id, .hljs-selector-class,
|
|
1306
|
+
.hljs-regexp, .hljs-template-tag, .hljs-selector-pseudo,
|
|
1307
|
+
.hljs-selector-attr, .hljs-variable.language_, .hljs-deletion { color: #f7768e; }
|
|
1308
|
+
.hljs-variable, .hljs-template-variable, .hljs-number, .hljs-literal,
|
|
1309
|
+
.hljs-type, .hljs-params, .hljs-link { color: #ff9e64; }
|
|
1310
|
+
.hljs-built_in, .hljs-attribute { color: #e0af68; }
|
|
1311
|
+
.hljs-selector-tag { color: #73daca; }
|
|
1312
|
+
.hljs-keyword, .hljs-title.function_, .hljs-title, .hljs-title.class_,
|
|
1313
|
+
.hljs-title.class_.inherited__, .hljs-subst, .hljs-property { color: #7dcfff; }
|
|
1314
|
+
.hljs-quote, .hljs-string, .hljs-symbol, .hljs-bullet,
|
|
1315
|
+
.hljs-addition { color: #9ece6a; }
|
|
1316
|
+
.hljs-code, .hljs-formula, .hljs-section { color: #7aa2f7; }
|
|
1317
|
+
.hljs-name, .hljs-operator, .hljs-char.escape_, .hljs-attr { color: #bb9af7; }
|
|
1318
|
+
.hljs-punctuation { color: #c0caf5; }
|
|
1319
|
+
.hljs { background: #1a1b26; color: #9aa5ce; }
|
|
1320
|
+
.hljs-emphasis { font-style: italic; }
|
|
1321
|
+
.hljs-strong { font-weight: bold; }
|
|
1322
|
+
`;
|
|
1228
1323
|
var CODEBLOCK_STYLE = `
|
|
1229
1324
|
.rt-codeblock { position: relative; }
|
|
1230
1325
|
.rt-codeblock[data-filename]:not([data-filename=""]) { padding-top: 2rem; }
|
|
@@ -1276,6 +1371,64 @@ var CODEBLOCK_STYLE = `
|
|
|
1276
1371
|
}
|
|
1277
1372
|
.rt-quote-open { margin-right: 0.15em; }
|
|
1278
1373
|
.rt-quote-close { margin-left: 0.15em; }
|
|
1374
|
+
|
|
1375
|
+
/* Callout chip ----------------------------------------------------------- */
|
|
1376
|
+
/* Layout: when a callout opts into an icon, use a 2-column grid so the chip
|
|
1377
|
+
sits left of the title + body. Consumers can override via classMap. */
|
|
1378
|
+
aside[data-callout][data-icon]:not([data-icon="false"]):not([data-icon="0"]) {
|
|
1379
|
+
display: grid;
|
|
1380
|
+
grid-template-columns: auto 1fr;
|
|
1381
|
+
column-gap: 0.85rem;
|
|
1382
|
+
align-items: start;
|
|
1383
|
+
}
|
|
1384
|
+
aside[data-callout][data-icon]:not([data-icon="false"]):not([data-icon="0"]) > .rt-callout-icon {
|
|
1385
|
+
grid-row: 1 / span 99;
|
|
1386
|
+
margin-top: 0.05rem;
|
|
1387
|
+
}
|
|
1388
|
+
.rt-callout-icon {
|
|
1389
|
+
display: inline-flex;
|
|
1390
|
+
align-items: center;
|
|
1391
|
+
justify-content: center;
|
|
1392
|
+
width: 1.6rem;
|
|
1393
|
+
height: 1.6rem;
|
|
1394
|
+
border-radius: 0.4rem;
|
|
1395
|
+
color: #fff;
|
|
1396
|
+
background: #475569;
|
|
1397
|
+
flex-shrink: 0;
|
|
1398
|
+
}
|
|
1399
|
+
.rt-callout-icon[data-variant="info"] { background: #2563eb; }
|
|
1400
|
+
.rt-callout-icon[data-variant="warning"] { background: #d97706; }
|
|
1401
|
+
.rt-callout-icon[data-variant="success"] { background: #16a34a; }
|
|
1402
|
+
.rt-callout-icon[data-variant="danger"] { background: #dc2626; }
|
|
1403
|
+
.rt-callout-icon[data-variant="default"] { background: #475569; }
|
|
1404
|
+
|
|
1405
|
+
/* Collapsible (FAQ accordion) ------------------------------------------ */
|
|
1406
|
+
/* Native <details>/<summary> with a rotating chevron. Only structural
|
|
1407
|
+
styling lives here \u2014 colors, padding, and typography belong to
|
|
1408
|
+
consumers via the \`collapsible\` and \`collapsibleTitle\` classMap keys. */
|
|
1409
|
+
details[data-collapsible] > summary {
|
|
1410
|
+
cursor: pointer;
|
|
1411
|
+
list-style: none;
|
|
1412
|
+
display: flex;
|
|
1413
|
+
align-items: center;
|
|
1414
|
+
justify-content: space-between;
|
|
1415
|
+
gap: 0.75rem;
|
|
1416
|
+
}
|
|
1417
|
+
details[data-collapsible] > summary::-webkit-details-marker { display: none; }
|
|
1418
|
+
details[data-collapsible] > summary::after {
|
|
1419
|
+
content: "";
|
|
1420
|
+
width: 0.55rem;
|
|
1421
|
+
height: 0.55rem;
|
|
1422
|
+
border-right: 1.5px solid currentColor;
|
|
1423
|
+
border-bottom: 1.5px solid currentColor;
|
|
1424
|
+
transform: rotate(-45deg) translate(-0.1rem, 0.05rem);
|
|
1425
|
+
transition: transform 0.18s ease;
|
|
1426
|
+
opacity: 0.55;
|
|
1427
|
+
flex-shrink: 0;
|
|
1428
|
+
}
|
|
1429
|
+
details[data-collapsible][open] > summary::after {
|
|
1430
|
+
transform: rotate(45deg) translate(-0.05rem, -0.05rem);
|
|
1431
|
+
}
|
|
1279
1432
|
/* highlight.js theme handles .hljs-* color classes; we only override the
|
|
1280
1433
|
default .hljs background so the per-block chrome (dark bg, terminal,
|
|
1281
1434
|
diff red/green rows) wins. */
|
|
@@ -1417,15 +1570,29 @@ function ensureCodeBlockStyles() {
|
|
|
1417
1570
|
}
|
|
1418
1571
|
const tag = document.createElement("style");
|
|
1419
1572
|
tag.id = "rt-codeblock-style";
|
|
1420
|
-
tag.textContent = CODEBLOCK_STYLE;
|
|
1573
|
+
tag.textContent = HLJS_THEME + "\n" + CODEBLOCK_STYLE;
|
|
1421
1574
|
document.head.appendChild(tag);
|
|
1422
1575
|
styleInjected = true;
|
|
1423
1576
|
}
|
|
1577
|
+
function calloutChipsEqual(a, b) {
|
|
1578
|
+
if (a.length !== b.length) return false;
|
|
1579
|
+
for (let i = 0; i < a.length; i++) {
|
|
1580
|
+
if (a[i].el !== b[i].el || a[i].variant !== b[i].variant) return false;
|
|
1581
|
+
}
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
function BuiltinCalloutIcon({ variant }) {
|
|
1585
|
+
const html = CALLOUT_ICON_SVG[variant] ?? CALLOUT_ICON_SVG.default;
|
|
1586
|
+
return /* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: html } });
|
|
1587
|
+
}
|
|
1424
1588
|
function RichTextContent({
|
|
1425
1589
|
html,
|
|
1426
1590
|
classMap,
|
|
1427
1591
|
as = "div",
|
|
1428
|
-
className
|
|
1592
|
+
className,
|
|
1593
|
+
onReady,
|
|
1594
|
+
contentRef,
|
|
1595
|
+
calloutIcons
|
|
1429
1596
|
}) {
|
|
1430
1597
|
const merged = useMemo(
|
|
1431
1598
|
() => mergeClassMap(DEFAULT_CLASS_MAP, classMap),
|
|
@@ -1436,20 +1603,215 @@ function RichTextContent({
|
|
|
1436
1603
|
[html, merged]
|
|
1437
1604
|
);
|
|
1438
1605
|
const ref = useRef(null);
|
|
1606
|
+
const [chips, setChips] = useState([]);
|
|
1439
1607
|
useEffect(() => {
|
|
1440
1608
|
ensureCodeBlockStyles();
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1609
|
+
const root = ref.current;
|
|
1610
|
+
if (!root) return;
|
|
1611
|
+
if (contentRef) contentRef.current = root;
|
|
1612
|
+
const apply = () => {
|
|
1613
|
+
mo.disconnect();
|
|
1614
|
+
enhanceCodeBlocks(root);
|
|
1615
|
+
enhanceBlockquotes(root);
|
|
1616
|
+
const nextChips = enhanceCallouts(root);
|
|
1617
|
+
setChips((prev) => calloutChipsEqual(prev, nextChips) ? prev : nextChips);
|
|
1618
|
+
onReady?.(root);
|
|
1619
|
+
mo.observe(root, { childList: true, subtree: true });
|
|
1620
|
+
};
|
|
1621
|
+
let raf = 0;
|
|
1622
|
+
const mo = new MutationObserver(() => {
|
|
1623
|
+
if (raf) return;
|
|
1624
|
+
raf = requestAnimationFrame(() => {
|
|
1625
|
+
raf = 0;
|
|
1626
|
+
apply();
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
apply();
|
|
1630
|
+
return () => {
|
|
1631
|
+
mo.disconnect();
|
|
1632
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1633
|
+
};
|
|
1634
|
+
}, [safe, onReady, contentRef]);
|
|
1635
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1636
|
+
createElement(as, {
|
|
1637
|
+
ref,
|
|
1638
|
+
className,
|
|
1639
|
+
dangerouslySetInnerHTML: { __html: safe }
|
|
1640
|
+
}),
|
|
1641
|
+
chips.map(
|
|
1642
|
+
(chip, i) => createPortal(
|
|
1643
|
+
calloutIcons && chip.variant in calloutIcons ? calloutIcons[chip.variant] : /* @__PURE__ */ jsx(BuiltinCalloutIcon, { variant: chip.variant }),
|
|
1644
|
+
chip.el,
|
|
1645
|
+
`${chip.variant}:${i}`
|
|
1646
|
+
)
|
|
1647
|
+
)
|
|
1648
|
+
] });
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// src/utils/extractHeadings.ts
|
|
1652
|
+
var DEFAULT_LEVELS = [2, 3];
|
|
1653
|
+
function slugify(text) {
|
|
1654
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1655
|
+
}
|
|
1656
|
+
function decodeBasicEntities(s) {
|
|
1657
|
+
return s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
1658
|
+
}
|
|
1659
|
+
function stripTags(s) {
|
|
1660
|
+
return decodeBasicEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
1661
|
+
}
|
|
1662
|
+
function uniqueId(base, used) {
|
|
1663
|
+
const seed = base || "section";
|
|
1664
|
+
const n = used.get(seed) ?? 0;
|
|
1665
|
+
used.set(seed, n + 1);
|
|
1666
|
+
return n === 0 ? seed : `${seed}-${n}`;
|
|
1667
|
+
}
|
|
1668
|
+
function extractHeadingsFromHtml(html, options = {}) {
|
|
1669
|
+
if (!html) return [];
|
|
1670
|
+
const levels = options.levels ?? DEFAULT_LEVELS;
|
|
1671
|
+
const slug = options.slugify ?? slugify;
|
|
1672
|
+
const used = /* @__PURE__ */ new Map();
|
|
1673
|
+
const out = [];
|
|
1674
|
+
const re = /<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi;
|
|
1675
|
+
let m;
|
|
1676
|
+
let i = 0;
|
|
1677
|
+
while ((m = re.exec(html)) !== null) {
|
|
1678
|
+
const level = Number(m[1]);
|
|
1679
|
+
if (!levels.includes(level)) continue;
|
|
1680
|
+
const attrs = m[2] ?? "";
|
|
1681
|
+
const inner = m[3] ?? "";
|
|
1682
|
+
const text = stripTags(inner);
|
|
1683
|
+
if (!text) continue;
|
|
1684
|
+
const explicitIdMatch = attrs.match(/\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/i);
|
|
1685
|
+
let id;
|
|
1686
|
+
if (explicitIdMatch) {
|
|
1687
|
+
id = explicitIdMatch[2] ?? explicitIdMatch[3] ?? explicitIdMatch[4] ?? "";
|
|
1688
|
+
if (id) used.set(id, (used.get(id) ?? 0) + 1);
|
|
1689
|
+
} else {
|
|
1690
|
+
id = uniqueId(slug(text, i), used);
|
|
1444
1691
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1692
|
+
out.push({ id, text, level });
|
|
1693
|
+
i++;
|
|
1694
|
+
}
|
|
1695
|
+
return out;
|
|
1696
|
+
}
|
|
1697
|
+
function extractHeadingsFromElement(root, options = {}) {
|
|
1698
|
+
const levels = options.levels ?? DEFAULT_LEVELS;
|
|
1699
|
+
const slug = options.slugify ?? slugify;
|
|
1700
|
+
const selector = levels.map((l) => `h${l}`).join(",");
|
|
1701
|
+
const nodes = root.querySelectorAll(selector);
|
|
1702
|
+
const used = /* @__PURE__ */ new Map();
|
|
1703
|
+
nodes.forEach((n) => {
|
|
1704
|
+
if (n.id) used.set(n.id, (used.get(n.id) ?? 0) + 1);
|
|
1450
1705
|
});
|
|
1706
|
+
const out = [];
|
|
1707
|
+
let i = 0;
|
|
1708
|
+
nodes.forEach((node) => {
|
|
1709
|
+
const level = Number(node.tagName.slice(1));
|
|
1710
|
+
const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1711
|
+
if (!text) return;
|
|
1712
|
+
if (!node.id) {
|
|
1713
|
+
node.id = uniqueId(slug(text, i), used);
|
|
1714
|
+
}
|
|
1715
|
+
if (options.scrollMarginTop != null) {
|
|
1716
|
+
node.style.scrollMarginTop = `${options.scrollMarginTop}px`;
|
|
1717
|
+
}
|
|
1718
|
+
out.push({ id: node.id, text, level });
|
|
1719
|
+
i++;
|
|
1720
|
+
});
|
|
1721
|
+
return out;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/hooks/useTableOfContents.tsx
|
|
1725
|
+
function useTableOfContents(ref, options = {}) {
|
|
1726
|
+
const {
|
|
1727
|
+
levels,
|
|
1728
|
+
contentKey = null,
|
|
1729
|
+
scrollMarginTop = 24,
|
|
1730
|
+
activationOffset = 96
|
|
1731
|
+
} = options;
|
|
1732
|
+
const [items, setItems] = useState([]);
|
|
1733
|
+
const [activeId, setActiveId] = useState("");
|
|
1734
|
+
useEffect(() => {
|
|
1735
|
+
const root = ref.current;
|
|
1736
|
+
if (!root) {
|
|
1737
|
+
setItems([]);
|
|
1738
|
+
setActiveId("");
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
let raf = 0;
|
|
1742
|
+
const collect = () => {
|
|
1743
|
+
const next = extractHeadingsFromElement(root, {
|
|
1744
|
+
levels,
|
|
1745
|
+
scrollMarginTop
|
|
1746
|
+
});
|
|
1747
|
+
setItems(next);
|
|
1748
|
+
setActiveId(
|
|
1749
|
+
(prev) => next.some((h) => h.id === prev) ? prev : next[0]?.id ?? ""
|
|
1750
|
+
);
|
|
1751
|
+
};
|
|
1752
|
+
raf = requestAnimationFrame(collect);
|
|
1753
|
+
const mo = new MutationObserver(() => {
|
|
1754
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1755
|
+
raf = requestAnimationFrame(collect);
|
|
1756
|
+
});
|
|
1757
|
+
mo.observe(root, { childList: true, subtree: true, characterData: true });
|
|
1758
|
+
return () => {
|
|
1759
|
+
mo.disconnect();
|
|
1760
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1761
|
+
};
|
|
1762
|
+
}, [ref, contentKey, levels, scrollMarginTop]);
|
|
1763
|
+
useEffect(() => {
|
|
1764
|
+
if (items.length === 0) return;
|
|
1765
|
+
const targets = items.map((it) => document.getElementById(it.id)).filter((el) => el !== null);
|
|
1766
|
+
if (targets.length === 0) return;
|
|
1767
|
+
let raf = 0;
|
|
1768
|
+
const compute = () => {
|
|
1769
|
+
raf = 0;
|
|
1770
|
+
let activeIdx = 0;
|
|
1771
|
+
for (let i = 0; i < items.length; i++) {
|
|
1772
|
+
const el = document.getElementById(items[i].id);
|
|
1773
|
+
if (!el) continue;
|
|
1774
|
+
if (el.getBoundingClientRect().top - activationOffset <= 0) {
|
|
1775
|
+
activeIdx = i;
|
|
1776
|
+
} else {
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const scroller = document.scrollingElement || document.documentElement;
|
|
1781
|
+
const scrollY = window.scrollY;
|
|
1782
|
+
const viewportH = window.innerHeight;
|
|
1783
|
+
const atBottom = scrollY + viewportH >= scroller.scrollHeight - 2;
|
|
1784
|
+
if (atBottom) {
|
|
1785
|
+
for (let i = items.length - 1; i > activeIdx; i--) {
|
|
1786
|
+
const el = document.getElementById(items[i].id);
|
|
1787
|
+
if (el && el.getBoundingClientRect().top < viewportH) {
|
|
1788
|
+
activeIdx = i;
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
setActiveId(items[activeIdx].id);
|
|
1794
|
+
};
|
|
1795
|
+
const schedule = () => {
|
|
1796
|
+
if (raf) return;
|
|
1797
|
+
raf = requestAnimationFrame(compute);
|
|
1798
|
+
};
|
|
1799
|
+
const io = new IntersectionObserver(schedule, {
|
|
1800
|
+
rootMargin: `-${activationOffset}px 0px -${Math.max(0, window.innerHeight - activationOffset - 1)}px 0px`,
|
|
1801
|
+
threshold: 0
|
|
1802
|
+
});
|
|
1803
|
+
targets.forEach((t) => io.observe(t));
|
|
1804
|
+
window.addEventListener("resize", schedule, { passive: true });
|
|
1805
|
+
compute();
|
|
1806
|
+
return () => {
|
|
1807
|
+
io.disconnect();
|
|
1808
|
+
window.removeEventListener("resize", schedule);
|
|
1809
|
+
if (raf) cancelAnimationFrame(raf);
|
|
1810
|
+
};
|
|
1811
|
+
}, [items, activationOffset]);
|
|
1812
|
+
return { items, activeId };
|
|
1451
1813
|
}
|
|
1452
1814
|
|
|
1453
|
-
export { AsteroidCMSProvider, RichTextContent, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
|
|
1815
|
+
export { AsteroidCMSProvider, RichTextContent, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate, useTableOfContents };
|
|
1454
1816
|
//# sourceMappingURL=client.js.map
|
|
1455
1817
|
//# sourceMappingURL=client.js.map
|