Skip to content
File

Blob: src/sites/static-renderer/outline.ts

typescript86 lines
1import type { JSONContent } from "@tiptap/core";
2import {
3 createUniqueOutlineAnchorId,
4 normalizeOutlineText,
5 readOutlineLevel,
6 type OutlineItem,
7} from "@/shared/editor/model/outline";
8 
9export interface SitesOutline {
10 items: OutlineItem[];
11 headingAnchorIds: (string | null)[];
12}
13 
14export function collectSitesOutline(content: JSONContent): SitesOutline {
15 const used = new Set<string>();
16 const items: OutlineItem[] = [];
17 const headingAnchorIds: (string | null)[] = [];
18 
19 walk(content, (node, includeInOutline) => {
20 if (!includeInOutline) {
21 headingAnchorIds.push(null);
22 return;
23 }
24 
25 const text = readNodeText(node);
26 if (!text) {
27 headingAnchorIds.push(null);
28 return;
29 }
30 
31 const id = createUniqueOutlineAnchorId(text, used);
32 headingAnchorIds.push(id);
33 items.push({
34 id,
35 text,
36 level: readOutlineLevel(node.attrs?.level),
37 href: `#${id}`,
38 });
39 });
40 
41 return { items, headingAnchorIds };
42}
43 
44function walk(node: JSONContent, onHeading: (node: JSONContent, includeInOutline: boolean) => void): void {
45 walkNode(node, onHeading, false);
46}
47 
48function walkNode(
49 node: JSONContent,
50 onHeading: (node: JSONContent, includeInOutline: boolean) => void,
51 insideClosedDetails: boolean,
52): void {
53 if (!node || typeof node !== "object") return;
54 if (node.type === "heading") {
55 onHeading(node, !insideClosedDetails);
56 return;
57 }
58 
59 const nextInsideClosedDetails = insideClosedDetails || (node.type === "details" && !isDetailsOpen(node));
60 if (Array.isArray(node.content)) {
61 for (const child of node.content) {
62 walkNode(child, onHeading, nextInsideClosedDetails);
63 }
64 }
65}
66 
67function isDetailsOpen(node: JSONContent): boolean {
68 return node.attrs?.open === true || node.attrs?.open === "true";
69}
70 
71function readNodeText(node: JSONContent): string {
72 const parts: string[] = [];
73 collectText(node, parts);
74 return normalizeOutlineText(parts.join(" "));
75}
76 
77function collectText(node: JSONContent, parts: string[]): void {
78 if (typeof node.text === "string") {
79 parts.push(node.text);
80 }
81 if (!Array.isArray(node.content)) return;
82 for (const child of node.content) {
83 collectText(child, parts);
84 }
85}