@asteroidcms/core-utils 0.1.4 → 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 +6 -1
- package/dist/client.cjs +161 -13
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +28 -10
- package/dist/client.d.ts +28 -10
- package/dist/client.js +163 -15
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +9 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -356,19 +356,24 @@ getContentReadTime(article.body, {
|
|
|
356
356
|
|
|
357
357
|
## `<RichTextContent>`
|
|
358
358
|
|
|
359
|
-
Render Asteroid CMS rich-text HTML with syntax-highlighted code blocks (via `highlight.js`), self-healing enhancements (copy buttons, blockquote decorations), and slugified heading IDs out of the box.
|
|
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
362
|
import { RichTextContent } from "@asteroidcms/core-utils/client";
|
|
363
|
+
import { Info } from "lucide-react";
|
|
363
364
|
|
|
364
365
|
<RichTextContent
|
|
365
366
|
html={article.body}
|
|
366
367
|
as="article"
|
|
367
368
|
className="prose"
|
|
368
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} /> }}
|
|
369
372
|
/>;
|
|
370
373
|
```
|
|
371
374
|
|
|
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
|
+
|
|
372
377
|
Or use the parser directly (server-safe, no `highlight.js`):
|
|
373
378
|
|
|
374
379
|
```ts
|
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 = {}) {
|
|
@@ -829,6 +833,8 @@ function classKeyForTag(tag, attrs, openStack) {
|
|
|
829
833
|
if (tag === "p" && "data-callout-title" in attrs) return "calloutTitle";
|
|
830
834
|
if (tag === "code" && openStack[openStack.length - 1] !== "pre")
|
|
831
835
|
return "inlineCode";
|
|
836
|
+
if (tag === "details") return "collapsible";
|
|
837
|
+
if (tag === "summary") return "collapsibleTitle";
|
|
832
838
|
const known = [
|
|
833
839
|
"p",
|
|
834
840
|
"br",
|
|
@@ -995,8 +1001,6 @@ function sanitizeAndStyle(html, options) {
|
|
|
995
1001
|
}
|
|
996
1002
|
return out.join("");
|
|
997
1003
|
}
|
|
998
|
-
|
|
999
|
-
// src/components/RichTextContent.tsx
|
|
1000
1004
|
var DEFAULT_CLASS_MAP = {
|
|
1001
1005
|
variants: {
|
|
1002
1006
|
"figure:pullquote": "relative my-8",
|
|
@@ -1031,6 +1035,46 @@ function findQuoteBody(bq) {
|
|
|
1031
1035
|
if (lastIdx < 0) return { first: null, last: null };
|
|
1032
1036
|
return { first: children[0], last: children[lastIdx] };
|
|
1033
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
|
+
}
|
|
1034
1078
|
function enhanceBlockquotes(root) {
|
|
1035
1079
|
const quotes = root.querySelectorAll("blockquote");
|
|
1036
1080
|
quotes.forEach((bq) => {
|
|
@@ -1260,6 +1304,28 @@ function enhanceCodeBlocks(root) {
|
|
|
1260
1304
|
highlightCodeBlock(pre);
|
|
1261
1305
|
});
|
|
1262
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
|
+
`;
|
|
1263
1329
|
var CODEBLOCK_STYLE = `
|
|
1264
1330
|
.rt-codeblock { position: relative; }
|
|
1265
1331
|
.rt-codeblock[data-filename]:not([data-filename=""]) { padding-top: 2rem; }
|
|
@@ -1311,6 +1377,64 @@ var CODEBLOCK_STYLE = `
|
|
|
1311
1377
|
}
|
|
1312
1378
|
.rt-quote-open { margin-right: 0.15em; }
|
|
1313
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
|
+
}
|
|
1314
1438
|
/* highlight.js theme handles .hljs-* color classes; we only override the
|
|
1315
1439
|
default .hljs background so the per-block chrome (dark bg, terminal,
|
|
1316
1440
|
diff red/green rows) wins. */
|
|
@@ -1452,17 +1576,29 @@ function ensureCodeBlockStyles() {
|
|
|
1452
1576
|
}
|
|
1453
1577
|
const tag = document.createElement("style");
|
|
1454
1578
|
tag.id = "rt-codeblock-style";
|
|
1455
|
-
tag.textContent = CODEBLOCK_STYLE;
|
|
1579
|
+
tag.textContent = HLJS_THEME + "\n" + CODEBLOCK_STYLE;
|
|
1456
1580
|
document.head.appendChild(tag);
|
|
1457
1581
|
styleInjected = true;
|
|
1458
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
|
+
}
|
|
1459
1594
|
function RichTextContent({
|
|
1460
1595
|
html,
|
|
1461
1596
|
classMap,
|
|
1462
1597
|
as = "div",
|
|
1463
1598
|
className,
|
|
1464
1599
|
onReady,
|
|
1465
|
-
contentRef
|
|
1600
|
+
contentRef,
|
|
1601
|
+
calloutIcons
|
|
1466
1602
|
}) {
|
|
1467
1603
|
const merged = react.useMemo(
|
|
1468
1604
|
() => mergeClassMap(DEFAULT_CLASS_MAP, classMap),
|
|
@@ -1473,6 +1609,7 @@ function RichTextContent({
|
|
|
1473
1609
|
[html, merged]
|
|
1474
1610
|
);
|
|
1475
1611
|
const ref = react.useRef(null);
|
|
1612
|
+
const [chips, setChips] = react.useState([]);
|
|
1476
1613
|
react.useEffect(() => {
|
|
1477
1614
|
ensureCodeBlockStyles();
|
|
1478
1615
|
const root = ref.current;
|
|
@@ -1482,6 +1619,8 @@ function RichTextContent({
|
|
|
1482
1619
|
mo.disconnect();
|
|
1483
1620
|
enhanceCodeBlocks(root);
|
|
1484
1621
|
enhanceBlockquotes(root);
|
|
1622
|
+
const nextChips = enhanceCallouts(root);
|
|
1623
|
+
setChips((prev) => calloutChipsEqual(prev, nextChips) ? prev : nextChips);
|
|
1485
1624
|
onReady?.(root);
|
|
1486
1625
|
mo.observe(root, { childList: true, subtree: true });
|
|
1487
1626
|
};
|
|
@@ -1499,11 +1638,20 @@ function RichTextContent({
|
|
|
1499
1638
|
if (raf) cancelAnimationFrame(raf);
|
|
1500
1639
|
};
|
|
1501
1640
|
}, [safe, onReady, contentRef]);
|
|
1502
|
-
return react.
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
+
] });
|
|
1507
1655
|
}
|
|
1508
1656
|
|
|
1509
1657
|
// src/utils/extractHeadings.ts
|