@paroicms/site-generator-plugin
Version:
ParoiCMS Site Generator Plugin
156 lines (155 loc) • 5.64 kB
JavaScript
export function parseMarkdownBulletedList(markdown) {
const lines = markdown.split(/\r?\n/);
const parsedLines = lines.map(parseLine).filter((line) => !!line);
return { home: convertParsedLinesToTree(parsedLines) };
}
export function convertParsedLinesToTree(lines) {
if (lines.length === 0) {
throw new Error("Input array cannot be empty");
}
const firstLine = lines[0];
// Ensure first line is a routing document with indent 0
if (firstLine.indent !== 0 || firstLine.kind !== "routingDocument") {
throw new Error("First line must be a routing document with zero indentation");
}
// Create root node
const root = {
kind: "routingDocument",
typeName: firstLine.typeName,
};
// Stack to keep track of parent nodes at each level
const stack = [root];
let currentDepth = 0;
// Process remaining lines
for (let i = 1; i < lines.length; ++i) {
const line = lines[i];
const node = createNode(line);
// If indent is greater than current level, add as child to last node in stack
if (line.indent > currentDepth) {
if (line.indent !== currentDepth + 1) {
throw new Error(`Invalid indentation level at line ${line.lineNumber}`);
}
const lastNode = stack[stack.length - 1];
appendChildToParentNode(node, lastNode);
stack.push(node);
}
// If indent is less than current level, pop stack until we reach correct level
else if (line.indent < currentDepth) {
while (line.indent <= currentDepth) {
stack.pop();
--currentDepth;
}
const lastNode = stack[stack.length - 1];
appendChildToParentNode(node, lastNode);
stack.push(node);
}
// If same indent, add as sibling
else {
// Special case: if we're at root level (indent 0), treat subsequent items as children of root
// This handles cases where LLM outputs multiple root-level items instead of properly indenting them
if (line.indent === 0) {
appendChildToParentNode(node, root);
stack.push(node);
}
else {
stack.pop();
const lastNode = stack[stack.length - 1];
appendChildToParentNode(node, lastNode);
stack.push(node);
}
}
currentDepth = line.indent;
}
return root;
}
function appendChildToParentNode(child, parent) {
if (parent.kind === "routingDocument") {
parent.children ??= [];
parent.children.push(child);
}
else if (parent.kind === "regularDocument") {
if (child.kind === "routingDocument") {
throw new Error(`Regular document type "${parent.typeName}" cannot have ${child.kind} child "${child.typeName}"`);
}
parent.children ??= [];
parent.children.push(child);
}
else if (parent.kind === "part") {
if (child.kind !== "part") {
throw new Error(`Part type "${parent.typeName}" cannot have ${child.kind} child "${child.typeName}"`);
}
parent.children ??= [];
parent.children.push(child);
}
}
function createNode(line) {
switch (line.kind) {
case "routingDocument":
return {
kind: "routingDocument",
typeName: line.typeName,
};
case "regularDocument":
return {
kind: "regularDocument",
typeName: line.typeName,
};
case "part":
if (!line.listName) {
throw new Error("Part node must have a listName");
}
return {
kind: "part",
typeName: line.typeName,
listName: line.listName,
};
default:
throw new Error(`Unknown node kind: ${line.kind}`);
}
}
export function parseLine(input, index) {
if (!input.trim())
return;
const lineNumber = index + 1;
// Count indentation (2 spaces per level)
const indentSpaces = input.match(/^[ ]+/);
const bulletIndex = indentSpaces ? indentSpaces[0].length : 0;
const indent = bulletIndex / 2;
const bulletChar = input.charAt(bulletIndex);
if (bulletChar !== "*" && bulletChar !== "-") {
throw new Error(`Missing bullet at line ${lineNumber}`);
}
const cleanLine = input.substring(bulletIndex + 1).trim();
// list of `article` (regular documents)
const regularDocumentMatch = cleanLine.match(/^list of `([^`]+)` \(regular documents\)$/);
if (regularDocumentMatch) {
return {
kind: "regularDocument",
indent,
typeName: regularDocumentMatch[1],
lineNumber,
};
}
// list of `pageSection` (parts), list name: `partSections`
const partMatch = cleanLine.match(/^list of `([^`]+)` \(parts\),? list name: `([^`]+)`$/);
if (partMatch) {
return {
kind: "part",
indent,
typeName: partMatch[1],
listName: partMatch[2],
lineNumber,
};
}
// `pages` (routing document)
const routingDocumentMatch = cleanLine.match(/^`([^`]+)` \(routing document\)$/);
if (routingDocumentMatch) {
return {
kind: "routingDocument",
indent,
typeName: routingDocumentMatch[1],
lineNumber,
};
}
throw new Error(`Invalid line at line ${lineNumber}`);
}