@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/index.d.cts
CHANGED
|
@@ -132,7 +132,7 @@ declare function getContentReadTime(content: string, options?: GetContentReadTim
|
|
|
132
132
|
*
|
|
133
133
|
* Idempotent: parseRichText(parseRichText(x, opts), opts) === parseRichText(x, opts).
|
|
134
134
|
*/
|
|
135
|
-
type RichTextClassKey = "p" | "br" | "hr" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code" | "inlineCode" | "a" | "strong" | "em" | "u" | "s" | "kbd" | "table" | "tableWrapper" | "thead" | "tbody" | "tr" | "th" | "td" | "figure" | "figcaption" | "img" | "span" | "callout" | "calloutTitle";
|
|
135
|
+
type RichTextClassKey = "p" | "br" | "hr" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code" | "inlineCode" | "a" | "strong" | "em" | "u" | "s" | "kbd" | "table" | "tableWrapper" | "thead" | "tbody" | "tr" | "th" | "td" | "figure" | "figcaption" | "img" | "span" | "callout" | "calloutTitle" | "collapsible" | "collapsibleTitle";
|
|
136
136
|
type RichTextClassMap = Partial<Record<RichTextClassKey, string>> & {
|
|
137
137
|
/** Variant overrides keyed as `${matchKey}:${variant}`, e.g. "callout:warning". */
|
|
138
138
|
variants?: Record<string, string>;
|
|
@@ -141,6 +141,12 @@ interface ParseRichTextOptions {
|
|
|
141
141
|
classMap?: RichTextClassMap;
|
|
142
142
|
/** Tag allowlist override. Defaults to a safe semantic set. */
|
|
143
143
|
allowlist?: ReadonlyArray<string>;
|
|
144
|
+
/**
|
|
145
|
+
* When `true` (default), inject slugified `id` attributes on `<h1>`–`<h6>`
|
|
146
|
+
* tags that don't already have one. Lets ToC anchors resolve from the
|
|
147
|
+
* server-rendered markup without a client-side mutation step.
|
|
148
|
+
*/
|
|
149
|
+
autoHeadingIds?: boolean;
|
|
144
150
|
}
|
|
145
151
|
declare function parseRichText(html: string, options?: ParseRichTextOptions): string;
|
|
146
152
|
/**
|
|
@@ -150,4 +156,41 @@ declare function parseRichText(html: string, options?: ParseRichTextOptions): st
|
|
|
150
156
|
*/
|
|
151
157
|
declare function removeEmptyParagraphs(html: string): string;
|
|
152
158
|
|
|
153
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Heading extraction helpers used to build tables of contents (ToC) from
|
|
161
|
+
* rich-text HTML. Server-safe — no React, no DOM dependency in the HTML
|
|
162
|
+
* variant. The DOM variant assigns missing `id`s in-place so anchor links
|
|
163
|
+
* resolve immediately.
|
|
164
|
+
*/
|
|
165
|
+
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
166
|
+
interface ExtractedHeading {
|
|
167
|
+
id: string;
|
|
168
|
+
text: string;
|
|
169
|
+
level: HeadingLevel;
|
|
170
|
+
}
|
|
171
|
+
interface ExtractHeadingsOptions {
|
|
172
|
+
/** Levels to include. Defaults to `[2, 3]` — typical doc page outline. */
|
|
173
|
+
levels?: ReadonlyArray<HeadingLevel>;
|
|
174
|
+
/** Custom slug function. Defaults to a lowercase/kebab/diacritic-safe slug. */
|
|
175
|
+
slugify?: (text: string, index: number) => string;
|
|
176
|
+
}
|
|
177
|
+
declare function slugify(text: string): string;
|
|
178
|
+
/**
|
|
179
|
+
* Parse headings out of a raw HTML string. Returns headings in document
|
|
180
|
+
* order with stable, de-duplicated IDs.
|
|
181
|
+
*
|
|
182
|
+
* If a heading already has an `id` attribute, it's preserved verbatim
|
|
183
|
+
* (and reserved so later slugs don't collide with it).
|
|
184
|
+
*/
|
|
185
|
+
declare function extractHeadingsFromHtml(html: string, options?: ExtractHeadingsOptions): ExtractedHeading[];
|
|
186
|
+
/**
|
|
187
|
+
* Walk a rendered DOM subtree, collect headings, and assign missing `id`s
|
|
188
|
+
* in-place so anchor links resolve immediately. Also sets `scrollMarginTop`
|
|
189
|
+
* on each heading when `scrollMarginTop` is provided so navigation lands
|
|
190
|
+
* cleanly below a sticky header.
|
|
191
|
+
*/
|
|
192
|
+
declare function extractHeadingsFromElement(root: HTMLElement, options?: ExtractHeadingsOptions & {
|
|
193
|
+
scrollMarginTop?: number;
|
|
194
|
+
}): ExtractedHeading[];
|
|
195
|
+
|
|
196
|
+
export { type AsteroidCMSConfig, type CmsSearchCondition, type ContentStatus, type ExtractHeadingsOptions, type ExtractedHeading, type FieldSelector, type HeadingLevel, type ParseRichTextOptions, type ReferenceExpansion, type ResolvedAsteroidCMSConfig, type RichTextClassKey, type RichTextClassMap, type UseCmsContentOptions, buildCmsQuery, cmsImage, createApolloClient, extractHeadingsFromElement, extractHeadingsFromHtml, fetchCmsContent, getContentReadTime, parseRichText, removeEmptyParagraphs, slugify };
|
package/dist/index.d.ts
CHANGED
|
@@ -132,7 +132,7 @@ declare function getContentReadTime(content: string, options?: GetContentReadTim
|
|
|
132
132
|
*
|
|
133
133
|
* Idempotent: parseRichText(parseRichText(x, opts), opts) === parseRichText(x, opts).
|
|
134
134
|
*/
|
|
135
|
-
type RichTextClassKey = "p" | "br" | "hr" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code" | "inlineCode" | "a" | "strong" | "em" | "u" | "s" | "kbd" | "table" | "tableWrapper" | "thead" | "tbody" | "tr" | "th" | "td" | "figure" | "figcaption" | "img" | "span" | "callout" | "calloutTitle";
|
|
135
|
+
type RichTextClassKey = "p" | "br" | "hr" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code" | "inlineCode" | "a" | "strong" | "em" | "u" | "s" | "kbd" | "table" | "tableWrapper" | "thead" | "tbody" | "tr" | "th" | "td" | "figure" | "figcaption" | "img" | "span" | "callout" | "calloutTitle" | "collapsible" | "collapsibleTitle";
|
|
136
136
|
type RichTextClassMap = Partial<Record<RichTextClassKey, string>> & {
|
|
137
137
|
/** Variant overrides keyed as `${matchKey}:${variant}`, e.g. "callout:warning". */
|
|
138
138
|
variants?: Record<string, string>;
|
|
@@ -141,6 +141,12 @@ interface ParseRichTextOptions {
|
|
|
141
141
|
classMap?: RichTextClassMap;
|
|
142
142
|
/** Tag allowlist override. Defaults to a safe semantic set. */
|
|
143
143
|
allowlist?: ReadonlyArray<string>;
|
|
144
|
+
/**
|
|
145
|
+
* When `true` (default), inject slugified `id` attributes on `<h1>`–`<h6>`
|
|
146
|
+
* tags that don't already have one. Lets ToC anchors resolve from the
|
|
147
|
+
* server-rendered markup without a client-side mutation step.
|
|
148
|
+
*/
|
|
149
|
+
autoHeadingIds?: boolean;
|
|
144
150
|
}
|
|
145
151
|
declare function parseRichText(html: string, options?: ParseRichTextOptions): string;
|
|
146
152
|
/**
|
|
@@ -150,4 +156,41 @@ declare function parseRichText(html: string, options?: ParseRichTextOptions): st
|
|
|
150
156
|
*/
|
|
151
157
|
declare function removeEmptyParagraphs(html: string): string;
|
|
152
158
|
|
|
153
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Heading extraction helpers used to build tables of contents (ToC) from
|
|
161
|
+
* rich-text HTML. Server-safe — no React, no DOM dependency in the HTML
|
|
162
|
+
* variant. The DOM variant assigns missing `id`s in-place so anchor links
|
|
163
|
+
* resolve immediately.
|
|
164
|
+
*/
|
|
165
|
+
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
166
|
+
interface ExtractedHeading {
|
|
167
|
+
id: string;
|
|
168
|
+
text: string;
|
|
169
|
+
level: HeadingLevel;
|
|
170
|
+
}
|
|
171
|
+
interface ExtractHeadingsOptions {
|
|
172
|
+
/** Levels to include. Defaults to `[2, 3]` — typical doc page outline. */
|
|
173
|
+
levels?: ReadonlyArray<HeadingLevel>;
|
|
174
|
+
/** Custom slug function. Defaults to a lowercase/kebab/diacritic-safe slug. */
|
|
175
|
+
slugify?: (text: string, index: number) => string;
|
|
176
|
+
}
|
|
177
|
+
declare function slugify(text: string): string;
|
|
178
|
+
/**
|
|
179
|
+
* Parse headings out of a raw HTML string. Returns headings in document
|
|
180
|
+
* order with stable, de-duplicated IDs.
|
|
181
|
+
*
|
|
182
|
+
* If a heading already has an `id` attribute, it's preserved verbatim
|
|
183
|
+
* (and reserved so later slugs don't collide with it).
|
|
184
|
+
*/
|
|
185
|
+
declare function extractHeadingsFromHtml(html: string, options?: ExtractHeadingsOptions): ExtractedHeading[];
|
|
186
|
+
/**
|
|
187
|
+
* Walk a rendered DOM subtree, collect headings, and assign missing `id`s
|
|
188
|
+
* in-place so anchor links resolve immediately. Also sets `scrollMarginTop`
|
|
189
|
+
* on each heading when `scrollMarginTop` is provided so navigation lands
|
|
190
|
+
* cleanly below a sticky header.
|
|
191
|
+
*/
|
|
192
|
+
declare function extractHeadingsFromElement(root: HTMLElement, options?: ExtractHeadingsOptions & {
|
|
193
|
+
scrollMarginTop?: number;
|
|
194
|
+
}): ExtractedHeading[];
|
|
195
|
+
|
|
196
|
+
export { type AsteroidCMSConfig, type CmsSearchCondition, type ContentStatus, type ExtractHeadingsOptions, type ExtractedHeading, type FieldSelector, type HeadingLevel, type ParseRichTextOptions, type ReferenceExpansion, type ResolvedAsteroidCMSConfig, type RichTextClassKey, type RichTextClassMap, type UseCmsContentOptions, buildCmsQuery, cmsImage, createApolloClient, extractHeadingsFromElement, extractHeadingsFromHtml, fetchCmsContent, getContentReadTime, parseRichText, removeEmptyParagraphs, slugify };
|
package/dist/index.js
CHANGED
|
@@ -270,7 +270,9 @@ var DEFAULT_ALLOWLIST = [
|
|
|
270
270
|
"section",
|
|
271
271
|
"article",
|
|
272
272
|
"div",
|
|
273
|
-
"span"
|
|
273
|
+
"span",
|
|
274
|
+
"details",
|
|
275
|
+
"summary"
|
|
274
276
|
];
|
|
275
277
|
var ALLOWED_ATTRS = {
|
|
276
278
|
a: ["href", "title", "target", "rel"],
|
|
@@ -280,7 +282,8 @@ var ALLOWED_ATTRS = {
|
|
|
280
282
|
col: ["span", "width"],
|
|
281
283
|
colgroup: ["span"],
|
|
282
284
|
table: ["border", "cellpadding", "cellspacing"],
|
|
283
|
-
span: ["style"]
|
|
285
|
+
span: ["style"],
|
|
286
|
+
details: ["open"]
|
|
284
287
|
};
|
|
285
288
|
var ALLOWED_STYLE_PROPS = {
|
|
286
289
|
span: ["font-size"]
|
|
@@ -314,7 +317,8 @@ var GLOBAL_ALLOWED_ATTRS = [
|
|
|
314
317
|
"data-title",
|
|
315
318
|
"data-callout-title",
|
|
316
319
|
"data-language",
|
|
317
|
-
"data-filename"
|
|
320
|
+
"data-filename",
|
|
321
|
+
"data-icon"
|
|
318
322
|
];
|
|
319
323
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
|
|
320
324
|
function parseRichText(html, options = {}) {
|
|
@@ -324,7 +328,36 @@ function parseRichText(html, options = {}) {
|
|
|
324
328
|
working = upgradeStandaloneImages(working);
|
|
325
329
|
working = upgradeAuthoredBlockquotes(working);
|
|
326
330
|
working = flattenTableCellParagraphs(working);
|
|
327
|
-
|
|
331
|
+
working = sanitizeAndStyle(working, options);
|
|
332
|
+
if (options.autoHeadingIds !== false) {
|
|
333
|
+
working = injectHeadingIds(working);
|
|
334
|
+
}
|
|
335
|
+
return working;
|
|
336
|
+
}
|
|
337
|
+
function injectHeadingIds(html) {
|
|
338
|
+
const used = /* @__PURE__ */ new Map();
|
|
339
|
+
const existingRe = /<h[1-6]\b[^>]*\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/gi;
|
|
340
|
+
let em;
|
|
341
|
+
while ((em = existingRe.exec(html)) !== null) {
|
|
342
|
+
const id = em[2] ?? em[3] ?? em[4] ?? "";
|
|
343
|
+
if (id) used.set(id, (used.get(id) ?? 0) + 1);
|
|
344
|
+
}
|
|
345
|
+
return html.replace(
|
|
346
|
+
/<(h[1-6])\b([^>]*)>([\s\S]*?)<\/\1>/gi,
|
|
347
|
+
(full, tag, attrs, inner) => {
|
|
348
|
+
if (/\bid\s*=/i.test(attrs)) return full;
|
|
349
|
+
const text = inner.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
350
|
+
if (!text) return full;
|
|
351
|
+
const base = slugifyHeading(text) || tag;
|
|
352
|
+
const n = used.get(base) ?? 0;
|
|
353
|
+
used.set(base, n + 1);
|
|
354
|
+
const id = n === 0 ? base : `${base}-${n}`;
|
|
355
|
+
return `<${tag}${attrs} id="${id}">${inner}</${tag}>`;
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
function slugifyHeading(text) {
|
|
360
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
328
361
|
}
|
|
329
362
|
function flattenTableCellParagraphs(html) {
|
|
330
363
|
return html.replace(
|
|
@@ -699,6 +732,8 @@ function classKeyForTag(tag, attrs, openStack) {
|
|
|
699
732
|
if (tag === "p" && "data-callout-title" in attrs) return "calloutTitle";
|
|
700
733
|
if (tag === "code" && openStack[openStack.length - 1] !== "pre")
|
|
701
734
|
return "inlineCode";
|
|
735
|
+
if (tag === "details") return "collapsible";
|
|
736
|
+
if (tag === "summary") return "collapsibleTitle";
|
|
702
737
|
const known = [
|
|
703
738
|
"p",
|
|
704
739
|
"br",
|
|
@@ -866,6 +901,79 @@ function sanitizeAndStyle(html, options) {
|
|
|
866
901
|
return out.join("");
|
|
867
902
|
}
|
|
868
903
|
|
|
869
|
-
|
|
904
|
+
// src/utils/extractHeadings.ts
|
|
905
|
+
var DEFAULT_LEVELS = [2, 3];
|
|
906
|
+
function slugify(text) {
|
|
907
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
908
|
+
}
|
|
909
|
+
function decodeBasicEntities(s) {
|
|
910
|
+
return s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
911
|
+
}
|
|
912
|
+
function stripTags(s) {
|
|
913
|
+
return decodeBasicEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
914
|
+
}
|
|
915
|
+
function uniqueId(base, used) {
|
|
916
|
+
const seed = base || "section";
|
|
917
|
+
const n = used.get(seed) ?? 0;
|
|
918
|
+
used.set(seed, n + 1);
|
|
919
|
+
return n === 0 ? seed : `${seed}-${n}`;
|
|
920
|
+
}
|
|
921
|
+
function extractHeadingsFromHtml(html, options = {}) {
|
|
922
|
+
if (!html) return [];
|
|
923
|
+
const levels = options.levels ?? DEFAULT_LEVELS;
|
|
924
|
+
const slug = options.slugify ?? slugify;
|
|
925
|
+
const used = /* @__PURE__ */ new Map();
|
|
926
|
+
const out = [];
|
|
927
|
+
const re = /<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi;
|
|
928
|
+
let m;
|
|
929
|
+
let i = 0;
|
|
930
|
+
while ((m = re.exec(html)) !== null) {
|
|
931
|
+
const level = Number(m[1]);
|
|
932
|
+
if (!levels.includes(level)) continue;
|
|
933
|
+
const attrs = m[2] ?? "";
|
|
934
|
+
const inner = m[3] ?? "";
|
|
935
|
+
const text = stripTags(inner);
|
|
936
|
+
if (!text) continue;
|
|
937
|
+
const explicitIdMatch = attrs.match(/\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/i);
|
|
938
|
+
let id;
|
|
939
|
+
if (explicitIdMatch) {
|
|
940
|
+
id = explicitIdMatch[2] ?? explicitIdMatch[3] ?? explicitIdMatch[4] ?? "";
|
|
941
|
+
if (id) used.set(id, (used.get(id) ?? 0) + 1);
|
|
942
|
+
} else {
|
|
943
|
+
id = uniqueId(slug(text, i), used);
|
|
944
|
+
}
|
|
945
|
+
out.push({ id, text, level });
|
|
946
|
+
i++;
|
|
947
|
+
}
|
|
948
|
+
return out;
|
|
949
|
+
}
|
|
950
|
+
function extractHeadingsFromElement(root, options = {}) {
|
|
951
|
+
const levels = options.levels ?? DEFAULT_LEVELS;
|
|
952
|
+
const slug = options.slugify ?? slugify;
|
|
953
|
+
const selector = levels.map((l) => `h${l}`).join(",");
|
|
954
|
+
const nodes = root.querySelectorAll(selector);
|
|
955
|
+
const used = /* @__PURE__ */ new Map();
|
|
956
|
+
nodes.forEach((n) => {
|
|
957
|
+
if (n.id) used.set(n.id, (used.get(n.id) ?? 0) + 1);
|
|
958
|
+
});
|
|
959
|
+
const out = [];
|
|
960
|
+
let i = 0;
|
|
961
|
+
nodes.forEach((node) => {
|
|
962
|
+
const level = Number(node.tagName.slice(1));
|
|
963
|
+
const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
964
|
+
if (!text) return;
|
|
965
|
+
if (!node.id) {
|
|
966
|
+
node.id = uniqueId(slug(text, i), used);
|
|
967
|
+
}
|
|
968
|
+
if (options.scrollMarginTop != null) {
|
|
969
|
+
node.style.scrollMarginTop = `${options.scrollMarginTop}px`;
|
|
970
|
+
}
|
|
971
|
+
out.push({ id: node.id, text, level });
|
|
972
|
+
i++;
|
|
973
|
+
});
|
|
974
|
+
return out;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export { buildCmsQuery, cmsImage, createApolloClient, extractHeadingsFromElement, extractHeadingsFromHtml, fetchCmsContent, getContentReadTime, parseRichText, removeEmptyParagraphs, slugify };
|
|
870
978
|
//# sourceMappingURL=index.js.map
|
|
871
979
|
//# sourceMappingURL=index.js.map
|