muddle/core/format-source.ts
2026-10-23 09:12:49 -04:00

119 lines
3.6 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;
}