mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-26 13:07:33 +09:00
144 lines
4.4 KiB
TypeScript
144 lines
4.4 KiB
TypeScript
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
import { toString } from "mdast-util-to-string";
|
|
import { directive } from "micromark-extension-directive";
|
|
import {
|
|
directiveFromMarkdown,
|
|
type LeafDirective,
|
|
} from "mdast-util-directive";
|
|
import type { ListItem, RootContent } from "mdast";
|
|
|
|
const docSectionDirectiveName = "doc-section";
|
|
|
|
const parseMarkdown = (markdown: string) =>
|
|
fromMarkdown(markdown, {
|
|
extensions: [directive()],
|
|
mdastExtensions: [directiveFromMarkdown()],
|
|
});
|
|
|
|
const getStartOffset = (node: RootContent) => node.position?.start.offset ?? 0;
|
|
|
|
const getNodeText = (node: RootContent | ListItem) => toString(node);
|
|
|
|
const getSectionEndIndex = (
|
|
children: readonly RootContent[],
|
|
startIndex: number,
|
|
maxHeadingDepth: number | undefined
|
|
) => {
|
|
for (let index = startIndex; index < children.length; index += 1) {
|
|
const node = children[index];
|
|
if (node?.type !== "heading") {
|
|
continue;
|
|
}
|
|
if (maxHeadingDepth === undefined || (node.depth ?? 0) <= maxHeadingDepth) {
|
|
return index;
|
|
}
|
|
}
|
|
return children.length;
|
|
};
|
|
|
|
const getMarkedSectionNodes = (
|
|
children: readonly RootContent[],
|
|
directiveIndex: number
|
|
) => {
|
|
const firstNode = children[directiveIndex + 1];
|
|
if (firstNode === undefined) {
|
|
return [];
|
|
}
|
|
const startsWithHeading = firstNode.type === "heading";
|
|
const contentStartIndex = startsWithHeading
|
|
? directiveIndex + 2
|
|
: directiveIndex + 1;
|
|
const endIndex = getSectionEndIndex(
|
|
children,
|
|
contentStartIndex,
|
|
startsWithHeading ? (firstNode.depth ?? 0) : undefined
|
|
);
|
|
return children.slice(contentStartIndex, endIndex);
|
|
};
|
|
|
|
const isDocSectionDirective = (node: RootContent): node is LeafDirective =>
|
|
node.type === "leafDirective" && node.name === docSectionDirectiveName;
|
|
|
|
const getDirectiveRemovalEnd = (markdown: string, node: LeafDirective) => {
|
|
let end = node.position?.end.offset ?? 0;
|
|
while (markdown[end] === "\n") {
|
|
end += 1;
|
|
}
|
|
return end;
|
|
};
|
|
|
|
export const stripDocMeta = (markdown: string) => {
|
|
const tree = parseMarkdown(markdown);
|
|
const ranges = tree.children
|
|
.filter(isDocSectionDirective)
|
|
.map((node) => ({
|
|
start: getStartOffset(node),
|
|
end: getDirectiveRemovalEnd(markdown, node),
|
|
}))
|
|
.sort((left, right) => right.start - left.start);
|
|
let strippedMarkdown = markdown;
|
|
for (const range of ranges) {
|
|
strippedMarkdown =
|
|
strippedMarkdown.slice(0, range.start) +
|
|
strippedMarkdown.slice(range.end);
|
|
}
|
|
return strippedMarkdown;
|
|
};
|
|
|
|
const getListItemText = (node: ListItem) =>
|
|
getNodeText(node).replace(/\s+/g, " ").trim();
|
|
|
|
const getListItems = (nodes: readonly RootContent[]) =>
|
|
nodes.flatMap((node) =>
|
|
node.type === "list" ? node.children.map(getListItemText) : []
|
|
);
|
|
|
|
export const getTitle = (markdown: string) => {
|
|
const heading = parseMarkdown(markdown).children.find(
|
|
(node) => node.type === "heading" && node.depth === 1
|
|
);
|
|
return heading === undefined ? "" : getNodeText(heading).trim();
|
|
};
|
|
|
|
export const buildDocSections = (docs: Record<string, string>) =>
|
|
Object.fromEntries(
|
|
Object.entries(docs).map(([docName, markdown]) => {
|
|
const tree = parseMarkdown(markdown);
|
|
const docSections: Record<string, string[]> = {};
|
|
tree.children.forEach((node, index) => {
|
|
if (isDocSectionDirective(node) === false) {
|
|
return;
|
|
}
|
|
const fieldName = node.attributes?.field;
|
|
if (typeof fieldName !== "string" || fieldName.length === 0) {
|
|
throw new Error(`Missing doc-section field ${docName}`);
|
|
}
|
|
const items = getListItems(getMarkedSectionNodes(tree.children, index));
|
|
if (items.length === 0) {
|
|
throw new Error(
|
|
`Missing generated doc section ${docName}:${fieldName}`
|
|
);
|
|
}
|
|
docSections[fieldName] = [...(docSections[fieldName] ?? []), ...items];
|
|
});
|
|
if (
|
|
docName.startsWith("manual-") &&
|
|
Object.keys(docSections).length === 0
|
|
) {
|
|
throw new Error(`Missing generated doc sections ${docName}`);
|
|
}
|
|
return [docName, docSections];
|
|
})
|
|
);
|
|
|
|
export const buildDocTitles = (docs: Record<string, string>) =>
|
|
Object.fromEntries(
|
|
Object.entries(docs).map(([docName, markdown]) => {
|
|
const title = getTitle(markdown);
|
|
if (title === "" && docName.startsWith("manual-")) {
|
|
throw new Error(`Missing generated doc title ${docName}`);
|
|
}
|
|
return [docName, title === "" ? docName : title];
|
|
})
|
|
);
|