const fs = require("fs");
const path = require("path");
/**
* Escapes characters that have special meaning in HTML.
* @param {string} text The text to escape.
* @returns {string} The escaped text.
*/
function escapeHtml(text) {
return text.replace(//g, ">");
}
/**
* Parses a single line of the input text.
* @param {string} line The line to parse.
* @returns {{ level: number, text: string, link: string | undefined }}
*/
function parseLine(line) {
const linkRegex = /`([^`]+)`$/;
const linkMatch = line.match(linkRegex);
let link = undefined;
if (linkMatch) {
const filePath = linkMatch[1].replace(/\\/g, "/");
link = `https://github.com/GraphiteEditor/Graphite/blob/master/${filePath}`;
}
const textContent = line.replace(/^[\s│├└─]*/, "").replace(linkRegex, "").trim();
const indentation = line.indexOf(textContent);
// Each level of indentation is 4 characters.
const level = Math.floor(indentation / 4);
return { level, text: textContent, link };
}
/**
* Recursively builds the HTML list from the parsed nodes.
* @param {Array} nodes The array of parsed node objects.
* @param {number} currentIndex The current index in the nodes array.
* @param {number} currentLevel The current indentation level.
* @returns {{html: string, nextIndex: number}}
*/
function buildHtmlList(nodes, currentIndex, currentLevel) {
if (currentIndex >= nodes.length) {
return { html: "", nextIndex: currentIndex };
}
let html = "
\n";
let i = currentIndex;
while (i < nodes.length && nodes[i].level >= currentLevel) {
const node = nodes[i];
if (node.level > currentLevel) {
// This case handles malformed input, skip to next valid line
i++;
continue;
}
const hasDirectChildren = i + 1 < nodes.length && nodes[i + 1].level > node.level;
const hasDeeperChildren = hasDirectChildren && i + 2 < nodes.length && nodes[i + 2].level > nodes[i + 1].level;
const linkHtml = node.link ? `${path.basename(node.link)}` : "";
const fieldPieces = node.text.match(/([^:]*):(.*)/);
let escapedText;
if (fieldPieces && fieldPieces.length === 3) {
escapedText = [escapeHtml(fieldPieces[1].trim()), escapeHtml(fieldPieces[2].trim())];
} else {
escapedText = [escapeHtml(node.text)];
}
let role = "message";
if (node.link) role = "subsystem";
else if (hasDeeperChildren) role = "submessage";
else if (escapedText.length === 2) role = "field";
const partOfMessageFromNamingConvention = ["Message", "MessageHandler", "MessageContext"].some((suffix) => node.text.replace(/(.*)<.*>/g, "$1").endsWith(suffix));
const partOfMessageViolatesNamingConvention = node.link && !partOfMessageFromNamingConvention;
const violatesNamingConvention = partOfMessageViolatesNamingConvention ? "(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')" : "";
if (hasDirectChildren) {
html += `
${escapedText}${linkHtml}${violatesNamingConvention}`;
const childResult = buildHtmlList(nodes, i + 1, node.level + 1);
html += `
${childResult.html}
\n`;
i = childResult.nextIndex;
} else if (role === "field") {
html += `
\n";
return { html, nextIndex: i };
}
const inputFile = process.argv[2];
const outputFile = process.argv[3];
if (!inputFile || !outputFile) {
console.error("Error: Please provide the input text and output HTML file paths as arguments.");
console.log("Usage: node generate-editor-structure.js