// The formatter intentionally operates on partially written source. Its small // scanner values are dynamically shaped, while the public API below remains typed. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck export interface FormatWrnOptions { insertSpaces?: boolean; tabSize?: number; printWidth?: number; multilineAttributes?: boolean; } const VOID_ELEMENTS = new Set([ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", ]); function splitPropDeclarations(value) { const declarations = []; let start = 0; let index = 0; let quote = null; let escaped = false; let square = 0; let brace = 0; let paren = 0; let segmentHasColon = false; let segmentHasEquals = false; const isIdentifierStart = (character) => /[A-Za-z_]/.test(character || ""); const isIdentifierPart = (character) => /[A-Za-z0-9_]/.test(character || ""); const beginsDeclaration = (position) => { let cursor = position; while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1; if (value.slice(cursor).startsWith("@event")) { cursor += "@event".length; if (!/\s/.test(value[cursor] || "")) return false; while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1; if (!isIdentifierStart(value[cursor])) return false; cursor += 1; while (cursor < value.length && isIdentifierPart(value[cursor])) cursor += 1; while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1; return value[cursor] === "=" ? "=" : null; } if (!isIdentifierStart(value[cursor])) return false; cursor += 1; while (cursor < value.length && isIdentifierPart(value[cursor])) cursor += 1; if (value[cursor] === "?") cursor += 1; while (cursor < value.length && /[ \t]/.test(value[cursor])) cursor += 1; return value[cursor] === "=" || value[cursor] === ":" ? value[cursor] : null; }; while (index < value.length) { const character = value[index]; if (quote !== null) { if (escaped) escaped = false; else if (character === "\\") escaped = true; else if (character === quote) quote = null; index += 1; continue; } if (character === '"' || character === "'" || character === "`") { quote = character; index += 1; continue; } if (character === "[") square += 1; else if (character === "]" && square > 0) square -= 1; else if (character === "{") brace += 1; else if (character === "}" && brace > 0) brace -= 1; else if (character === "(") paren += 1; else if (character === ")" && paren > 0) paren -= 1; const topLevel = square === 0 && brace === 0 && paren === 0; if (topLevel && character === ":") segmentHasColon = true; if (topLevel && character === "=") segmentHasEquals = true; const candidateDelimiter = topLevel && /\s/.test(character) ? beginsDeclaration(index) : null; const beginsNext = candidateDelimiter === ":" || (candidateDelimiter === "=" && (segmentHasEquals || !segmentHasColon)); if ( topLevel && /\s/.test(character) && value.slice(start, index).trim() !== "@event" && beginsNext ) { const declaration = value.slice(start, index).trim(); if (declaration) declarations.push(declaration); while (index < value.length && /\s/.test(value[index])) index += 1; start = index; segmentHasColon = false; segmentHasEquals = false; continue; } index += 1; } const declaration = value.slice(start).trim(); if (declaration) declarations.push(declaration); return declarations; } function splitOutputDeclarations(value) { const declarations = []; let start = 0; let paren = 0; let angle = 0; let square = 0; let quote = null; let escaped = false; const startsOutput = (position) => { let cursor = position; while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1; if (!/[A-Za-z_$]/.test(value[cursor] || "")) return false; cursor += 1; while (cursor < value.length && /[A-Za-z0-9_$]/.test(value[cursor] || "")) cursor += 1; while (cursor < value.length && /\s/.test(value[cursor])) cursor += 1; return value[cursor] === "("; }; for (let index = 0; index < value.length; index += 1) { const character = value[index]; if (quote !== null) { if (escaped) escaped = false; else if (character === "\\") escaped = true; else if (character === quote) quote = null; continue; } if (character === '"' || character === "'" || character === "`") { quote = character; continue; } if (character === "(") paren += 1; else if (character === ")" && paren > 0) paren -= 1; else if (character === "[") square += 1; else if (character === "]" && square > 0) square -= 1; else if (character === "<") angle += 1; else if (character === ">" && angle > 0) angle -= 1; if (paren === 0 && square === 0 && angle === 0 && /\s/.test(character) && startsOutput(index)) { const declaration = value.slice(start, index).trim(); if (declaration) declarations.push(declaration); while (index < value.length && /\s/.test(value[index])) index += 1; start = index; index -= 1; } } const finalDeclaration = value.slice(start).trim(); if (finalDeclaration) declarations.push(finalDeclaration); return declarations; } function formatInlineDeclarationBlock(value, unit, depth) { const match = /^(props|state|computed|outputs)\s*\{([\s\S]*)\}$/.exec(value.trim()); if (!match) return null; const declarations = match[1] === "outputs" ? splitOutputDeclarations(match[2].trim()) : splitPropDeclarations(match[2].trim()); return [ `${unit.repeat(depth)}${match[1]} {`, ...declarations.map((declaration) => `${unit.repeat(depth + 1)}${declaration}`), `${unit.repeat(depth)}}`, ]; } function formatInlinePropsBlock(value, unit, depth) { const match = /^props\s*\{([\s\S]*)\}$/.exec(value.trim()); if (!match) return null; const declarations = splitPropDeclarations(match[1].trim()); if (declarations.length === 0) { return [`${unit.repeat(depth)}props {`, `${unit.repeat(depth)}}`]; } return [ `${unit.repeat(depth)}props {`, ...declarations.map((declaration) => `${unit.repeat(depth + 1)}${declaration}`), `${unit.repeat(depth)}}`, ]; } function findOpeningTagEnd(value) { let quote = null; let escaped = false; for (let index = 0; index < value.length; index += 1) { const character = value[index]; if (quote !== null) { if (escaped) { escaped = false; continue; } if (character === "\\") { escaped = true; continue; } if (character === quote) { quote = null; } continue; } if (character === '"' || character === "'") { quote = character; continue; } if (character === ">") { return index; } } return -1; } function parseAttributes(value) { const attributes = []; let index = 0; while (index < value.length) { while (index < value.length && /\s/.test(value[index])) index += 1; if (index >= value.length) break; const start = index; while (index < value.length && !/[\s=]/.test(value[index])) index += 1; while (index < value.length && /\s/.test(value[index])) index += 1; if (value[index] === "=") { index += 1; while (index < value.length && /\s/.test(value[index])) index += 1; const quote = value[index]; if (quote === '"' || quote === "'") { index += 1; let escaped = false; while (index < value.length) { const character = value[index++]; if (escaped) escaped = false; else if (character === "\\") escaped = true; else if (character === quote) break; } } else if (value[index] === "{") { let depth = 0; let expressionQuote = null; let escaped = false; while (index < value.length) { const character = value[index++]; if (expressionQuote !== null) { if (escaped) escaped = false; else if (character === "\\") escaped = true; else if (character === expressionQuote) expressionQuote = null; continue; } if (character === '"' || character === "'" || character === "`") { expressionQuote = character; } else if (character === "{") { depth += 1; } else if (character === "}" && --depth === 0) { break; } } } else { while (index < value.length && !/\s/.test(value[index])) index += 1; } } const attribute = value.slice(start, index).trim(); if (attribute) attributes.push(attribute); } return attributes; } function parseStructuredAttribute(attribute) { const match = /^([^\s=]+)\s*=\s*\{([\s\S]*)\}$/.exec(attribute); if (!match) return null; const expression = match[2].trim(); if (!expression.startsWith("[") && !expression.startsWith("{")) return null; try { return { name: match[1], value: JSON.parse(expression), }; } catch { return null; } } function formatAttribute(attribute, indentation, unit) { const structured = parseStructuredAttribute(attribute); if (!structured) return [`${indentation}${attribute}`]; const jsonLines = JSON.stringify(structured.value, null, unit).split("\n"); if (jsonLines.length === 1) { return [`${indentation}${structured.name}={${jsonLines[0]}}`]; } return [ `${indentation}${structured.name}={${jsonLines[0]}`, ...jsonLines.slice(1, -1).map((line) => `${indentation}${line}`), `${indentation}${jsonLines.at(-1)}}`, ]; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function parseOpeningTag(value) { const endIndex = findOpeningTagEnd(value); if (endIndex === -1) { return null; } const openingPart = value.slice(0, endIndex + 1); const remainder = value.slice(endIndex + 1).trim(); const match = /^<([A-Za-z][\w$:.-]*)([\s\S]*?)(\/?)>$/.exec(openingPart); if (!match) { return null; } const tagName = match[1]; const attributes = parseAttributes(match[2].trim()); const selfClosing = match[3] === "/"; const escapedTagName = escapeRegExp(tagName); const immediateClosing = new RegExp(`^<\\/${escapedTagName}\\s*>`, "i").test(remainder); const trailingClosingMatch = new RegExp(`^([\\s\\S]*?)<\\/${escapedTagName}\\s*>$`, "i").exec( remainder, ); const trailingClosing = trailingClosingMatch !== null; const inlineContent = trailingClosing ? trailingClosingMatch[1].trim() : ""; const inlineClosing = trailingClosing && inlineContent.length === 0; const closesInRemainder = immediateClosing || trailingClosing; return { tagName, attributes, selfClosing, inlineClosing, immediateClosing, trailingClosing, inlineContent, closesInRemainder, remainder, }; } function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttributes = true) { const parsed = parseOpeningTag(value); if (!parsed) { return { lines: [`${unit.repeat(depth)}${value.trim()}`], opensElement: false, }; } const baseIndent = unit.repeat(depth); const childIndent = unit.repeat(depth + 1); const normalizedOpening = `<${parsed.tagName}` + `${parsed.attributes.length ? ` ${parsed.attributes.join(" ")}` : ""}` + `${parsed.selfClosing ? " /" : ""}>`; const normalizedSingleLine = `${normalizedOpening}${parsed.remainder}`; const shouldBreak = value.includes("\n") || (multilineAttributes && parsed.attributes.length > 0) || baseIndent.length + normalizedSingleLine.length > printWidth; const opensElement = !parsed.selfClosing && !parsed.closesInRemainder && !VOID_ELEMENTS.has(parsed.tagName.toLowerCase()); if (!shouldBreak) { return { lines: [`${baseIndent}${normalizedSingleLine}`], opensElement, }; } if (parsed.attributes.length === 0 && !parsed.selfClosing) { const lines = [`${baseIndent}<${parsed.tagName}>`]; if (parsed.trailingClosing) { if (parsed.inlineContent) lines.push(`${childIndent}${parsed.inlineContent}`); lines.push(`${baseIndent}`); } else if (parsed.remainder) { lines.push(`${childIndent}${parsed.remainder}`); } return { lines, opensElement }; } const lines = [ `${baseIndent}<${parsed.tagName}`, ...parsed.attributes.flatMap((attribute) => formatAttribute(attribute, childIndent, unit)), ]; if (parsed.selfClosing) { lines.push(`${baseIndent}/>`); return { lines, opensElement, }; } lines.push(`${baseIndent}>`); if (parsed.trailingClosing) { if (parsed.inlineContent) { lines.push(`${childIndent}${parsed.inlineContent}`); } lines.push(`${baseIndent}`); } else if (parsed.remainder) { lines.push(`${parsed.immediateClosing ? baseIndent : childIndent}${parsed.remainder}`); } return { lines, opensElement, }; } function isMultilineOpeningTagStart(value) { if (!value.startsWith("<")) { return false; } if ( value.startsWith("/.test(value); } /** * Preserve compact, already-balanced HTML fragments as one line. * * A line such as `PageAPI` is valid and * intentionally compact. Expanding only its first opening tag makes later * formatter passes treat the remaining siblings as children, causing runaway * indentation and false closing-tag diagnostics. Balanced fragments are kept * intact while normal multiline opening tags continue through the formatter. */ function isBalancedInlineHtmlFragment(value) { if (!value.startsWith("<") || value.startsWith("", tagStart + 4); if (commentEnd === -1) return false; index = commentEnd + 3; continue; } const relativeEnd = findOpeningTagEnd(value.slice(tagStart)); if (relativeEnd === -1) return false; const tag = value.slice(tagStart, tagStart + relativeEnd + 1); const match = /^<\/?([A-Za-z][\w$:.-]*)[\s\S]*?>$/.exec(tag); if (!match) { index = tagStart + 1; continue; } tagCount += 1; const tagName = match[1]; const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName; const closing = tag.startsWith("$/.test(tag); const voidElement = VOID_ELEMENTS.has(tagName.toLowerCase()); if (closing) { if (stack.at(-1) !== normalizedName) return false; stack.pop(); } else { if (stack.length === 0) rootCount += 1; else hasNestedElement = true; if (!selfClosing && !voidElement) stack.push(normalizedName); } index = tagStart + relativeEnd + 1; } return ( tagCount >= 2 && stack.length === 0 && (rootCount > 1 || hasNestedElement || hasOutsideText) ); } function isControlBlockOpen(value) { return /^\{#(?:if|each)\b[\s\S]*\}$/.test(value); } function isControlBlockMiddle(value) { return /^\{:(?:else(?:\s+if\b[\s\S]*)?|empty)\}$/.test(value); } function isControlBlockClose(value) { return /^\{\/(?:if|each)\}$/.test(value); } function countLeadingClosingBraces(value) { let index = 0; let count = 0; while (index < value.length) { while (index < value.length && /\s/.test(value[index])) { index += 1; } if (value[index] !== "}" && value[index] !== "]") { break; } count += 1; index += 1; } return count; } /** * Count braces outside strings and HTML comments. * * This supports WRN blocks, function bodies, lifecycle hooks, * watcher bodies and multiline JavaScript object literals. */ function countStructuralBraces(value) { let openings = 0; let closings = 0; let quote = null; let escaped = false; let htmlComment = false; for (let index = 0; index < value.length; index += 1) { if (!quote && !htmlComment && value.startsWith("", index)) { htmlComment = false; index += 2; continue; } if (htmlComment) { continue; } const character = value[index]; if (quote !== null) { if (escaped) { escaped = false; continue; } if (character === "\\") { escaped = true; continue; } if (character === quote) { quote = null; } continue; } if (character === '"' || character === "'" || character === "`") { quote = character; continue; } if (character === "{") { openings += 1; } else if (character === "}") { closings += 1; } else if (character === "[") { openings += 1; } else if (character === "]") { closings += 1; } } return { openings, closings, }; } function collectOpeningTag(inputLines, startIndex) { const collected = [inputLines[startIndex].trim()]; let index = startIndex; while (index + 1 < inputLines.length) { const joined = collected.join(" "); if (findOpeningTagEnd(joined) !== -1) { break; } index += 1; collected.push(inputLines[index].trim()); } return { // Preserve the fact that the opening tag was already multiline so a // second formatter pass cannot collapse it back to one line. value: collected.join("\n"), endIndex: index, }; } function isPreservedRawBlockStart(value) { return ( /)/i.test(value) && !/<\/pre\s*>/i.test(value.slice(0, value.search(/)/i))) ); } function hasPreservedRawBlockEnd(value) { return /<\/pre\s*>/i.test(value); } function transformOutsidePreservedRawBlocks(lines, transformLine) { const output = []; let preserving = false; for (const line of lines) { if (preserving) { output.push(line); if (hasPreservedRawBlockEnd(line)) preserving = false; continue; } if (isPreservedRawBlockStart(line)) { output.push(line); preserving = !hasPreservedRawBlockEnd(line); continue; } output.push(...transformLine(line)); } return output; } function collectPreservedRawBlock(lines, startIndex) { const collected = [lines[startIndex]]; let index = startIndex; while (!hasPreservedRawBlockEnd(collected.at(-1) || "") && index + 1 < lines.length) { index += 1; collected.push(lines[index]); } return { lines: collected, endIndex: index }; } /** * Put WRN template control markers on their own lines before indentation. * * Authors commonly write compact fragments such as * `{#if loading}{/if}`. Treating that as one line prevents the * normal HTML and control-block formatters from seeing its structure. */ function expandInlineControlBlocks(lines) { const marker = /(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g; return transformOutsidePreservedRawBlocks(lines, (line) => { if (!marker.test(line)) return [line]; marker.lastIndex = 0; const indentation = line.match(/^\s*/)?.[0] ?? ""; const segments = line .split(marker) .map((segment) => segment.trim()) .filter(Boolean); return segments.map((segment) => `${indentation}${segment}`); }); } function expandStructuredStateDeclarations(lines, unit) { return transformOutsidePreservedRawBlocks(lines, (line) => { const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line); if (!match) return [line]; try { const parsed = JSON.parse(match[2].trim()); const jsonLines = JSON.stringify(parsed, null, unit).split("\n"); if (jsonLines.length === 1) return [`${match[1]}${jsonLines[0]}`]; const leading = match[1].match(/^\s*/)?.[0] ?? ""; return [ `${match[1]}${jsonLines[0]}`, ...jsonLines.slice(1).map((jsonLine) => `${leading}${jsonLine}`), ]; } catch { return [line]; } }); } function formatWrnPass(source: string, options: FormatWrnOptions = {}): string { const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4); const printWidth = options.printWidth ?? 100; const multilineAttributes = options.multilineAttributes !== false; let codeDepth = 0; let htmlDepth = 0; let controlDepth = 0; let index = 0; const sourceLines = source.replace(/\r\n/g, "\n").split("\n"); const inputLines = expandInlineControlBlocks( expandStructuredStateDeclarations(sourceLines, unit), ); const output = []; let previousWasBlank = false; while (index < inputLines.length) { const originalLine = inputLines[index]; let value = originalLine.trim(); if (value === "") { if (!previousWasBlank && output.length > 0) { output.push(""); } previousWasBlank = true; index += 1; continue; } previousWasBlank = false; if (/^import\b/.test(value)) { const importLines = [value]; while ( !/(?:\bfrom\s+)?["'][^"']+["']\s*;?$/.test(importLines[importLines.length - 1]) && index + 1 < inputLines.length ) { index += 1; importLines.push(inputLines[index].trim()); } output.push(importLines[0], ...importLines.slice(1).map((line) => `${unit}${line}`)); index += 1; continue; } if (isPreservedRawBlockStart(value)) { const collected = collectPreservedRawBlock(inputLines, index); const depth = codeDepth + htmlDepth + controlDepth; output.push(`${unit.repeat(depth)}${collected.lines[0].trimStart()}`); output.push(...collected.lines.slice(1)); index = collected.endIndex + 1; continue; } if (isMultilineOpeningTagStart(value)) { const collected = collectOpeningTag(inputLines, index); value = collected.value; index = collected.endIndex; } const inlineDeclaration = formatInlineDeclarationBlock(value, unit, codeDepth + htmlDepth); const inlineProps = inlineDeclaration ?? formatInlinePropsBlock(value, unit, codeDepth + htmlDepth); if (inlineProps) { output.push(...inlineProps); index += 1; continue; } const leadingClosingBraces = countLeadingClosingBraces(value); const closesControlBlock = isControlBlockClose(value); const continuesControlBlock = isControlBlockMiddle(value); const lineControlDepth = closesControlBlock || continuesControlBlock ? Math.max(0, controlDepth - 1) : controlDepth; const lineCodeDepth = Math.max(0, codeDepth - leadingClosingBraces); let lineHtmlDepth = htmlDepth; if (isClosingTag(value)) { lineHtmlDepth = Math.max(0, htmlDepth - 1); } const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth; const inlineFragmentFits = unit.repeat(depth).length + value.length <= printWidth; if (isBalancedInlineHtmlFragment(value) && inlineFragmentFits) { output.push(`${unit.repeat(depth)}${value}`); } else if ( value.startsWith("<") && !value.startsWith(" 0 && output[output.length - 1] === "") { output.pop(); } return `${output.join("\n")}\n`; } /** Format to a bounded fixed point so one call is always safe for editor-on-save and migrations. */ export function formatWrn(source: string, options: FormatWrnOptions = {}): string { let current = source; const seen = new Set(); for (let pass = 0; pass < 8; pass++) { const formatted = formatWrnPass(current, options); if (formatted === current) return formatted; if (seen.has(formatted)) return [...seen, formatted].sort()[0]!; seen.add(current); current = formatted; } return current; }