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 { 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; }