muddle/core/format-source.ts
2026-10-23 08:53:07 -04:00

63 lines
3.5 KiB
TypeScript

import { foldLines } from "../views/engine/lexer.ts";
export interface FormatOptions { tabWidth?: number; useTabs?: boolean; printWidth?: number }
export type FormatLanguage = "spug" | "stpug" | "stug" | "stcss";
/** Shared by the CLI and Prettier plugin, with no subprocesses or write side effects. */
export async function formatSource(source: string, language: FormatLanguage, options: FormatOptions = {}): Promise<string> {
const prettier = await import("prettier");
const scripts: string[] = [];
let protectedSource = source.replace(/(^[\t ]*)(javascript!|typescript!|---)\s*\n([\s\S]*?)^\s*(end|---)\s*$/gm, (_match, indentation: string, directive: string, code: string, ending: string) => {
const index = scripts.length;
scripts.push(JSON.stringify({ directive, code, ending }));
return language === "stcss" ? `/* MUDDLE_SCRIPT_${index} */` : `${indentation}MUDDLE_SCRIPT_${index}`;
});
const rawBlocks: { marker: string; body: string[]; prefix: string }[] = [];
if (language !== "stcss") {
const sourceLines = protectedSource.split("\n");
const output: string[] = [];
for (let index = 0; index < sourceLines.length; index++) {
const line = sourceLines[index]!;
const match = /^(\s*)(?:[\w.#-]+(?:\([^]*\))?\.|\/\/-?|\$\$\$)\s*$/.exec(line);
if (!match) { output.push(line); continue; }
const prefix = match[1]!;
const body: string[] = [line.slice(prefix.length)];
while (index + 1 < sourceLines.length) {
const next = sourceLines[index + 1]!;
if (next.trim() && (next.match(/^\s*/)?.[0].length ?? 0) <= prefix.length) break;
body.push(next.startsWith(prefix) ? next.slice(prefix.length) : next);
index++;
}
const marker = `MUDDLE_RAW_${rawBlocks.length}`;
rawBlocks.push({ marker, body, prefix });
output.push(prefix + marker);
}
protectedSource = output.join("\n");
}
const unit = options.useTabs ? "\t" : " ".repeat(options.tabWidth ?? 2);
let formatted: string;
if (language === "stcss") {
formatted = await prettier.format(protectedSource, { parser: "scss", plugins: [], tabWidth: options.tabWidth ?? 2, useTabs: options.useTabs ?? false, printWidth: options.printWidth ?? 80 });
} else {
const stack = [0];
const lines: string[] = [];
for (const line of foldLines(protectedSource)) {
while (stack.length > 1 && line.indent < stack[stack.length - 1]!) stack.pop();
if (line.indent > stack[stack.length - 1]!) stack.push(line.indent);
const indent = unit.repeat(stack.length - 1);
lines.push(`${indent}${line.text.trimEnd()}`);
}
formatted = `${lines.join("\n")}\n`;
}
for (let index = 0; index < scripts.length; index++) {
const script = JSON.parse(scripts[index]!) as { directive: string; code: string; ending: string };
const code = await prettier.format(script.code, { parser: "typescript", plugins: [], tabWidth: options.tabWidth ?? 2, useTabs: options.useTabs ?? false });
const marker = language === "stcss" ? new RegExp(`^[\\t ]*/\\* MUDDLE_SCRIPT_${index} \\*/`, "m") : new RegExp(`^([\\t ]*)MUDDLE_SCRIPT_${index}`, "m");
formatted = formatted.replace(marker, (match) => {
const indentation = match.match(/^[\t ]*/)?.[0] ?? "";
return `${indentation}${script.directive}\n${code.trimEnd().split("\n").map((line) => `${indentation}${line}`).join("\n")}\n${indentation}${script.ending}`;
});
}
for (const block of rawBlocks) formatted = formatted.replace(new RegExp(`^([\\t ]*)${block.marker}$`, "m"), (_match, prefix: string) => block.body.map((line) => prefix + line).join("\n"));
return formatted;
}