512 lines
11 KiB
JavaScript
512 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
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;
|
|
|
|
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 (!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] === "=" || value[cursor] === ":";
|
|
};
|
|
|
|
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;
|
|
|
|
if (
|
|
square === 0 &&
|
|
brace === 0 &&
|
|
paren === 0 &&
|
|
/\s/.test(character) &&
|
|
beginsDeclaration(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;
|
|
continue;
|
|
}
|
|
|
|
index += 1;
|
|
}
|
|
|
|
const declaration = value.slice(start).trim();
|
|
if (declaration) declarations.push(declaration);
|
|
return declarations;
|
|
}
|
|
|
|
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 = [];
|
|
|
|
const pattern = /[^\s"'=<>`]+(?:\s*=\s*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s"'=<>`]+))?/g;
|
|
|
|
let match;
|
|
|
|
while ((match = pattern.exec(value)) !== null) {
|
|
attributes.push(match[0].trim());
|
|
}
|
|
|
|
return attributes;
|
|
}
|
|
|
|
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 inlineClosing = remainder === `</${tagName}>`;
|
|
|
|
const closesInRemainder = remainder.startsWith(`</${tagName}>`);
|
|
|
|
return {
|
|
tagName,
|
|
attributes,
|
|
selfClosing,
|
|
inlineClosing,
|
|
closesInRemainder,
|
|
remainder,
|
|
};
|
|
}
|
|
|
|
function formatOpeningTag(value, unit, depth, printWidth = 100) {
|
|
const parsed = parseOpeningTag(value);
|
|
|
|
if (!parsed) {
|
|
return {
|
|
lines: [`${unit.repeat(depth)}${value.trim()}`],
|
|
opensElement: false,
|
|
};
|
|
}
|
|
|
|
const baseIndent = unit.repeat(depth);
|
|
|
|
const attributeIndent = unit.repeat(depth + 1);
|
|
|
|
const normalizedOpening = `<${parsed.tagName}${
|
|
parsed.attributes.length ? ` ${parsed.attributes.join(" ")}` : ""
|
|
}${parsed.selfClosing ? " /" : ""}>`;
|
|
|
|
const normalizedSingleLine = `${normalizedOpening}${parsed.remainder}`;
|
|
|
|
const shouldBreak =
|
|
parsed.attributes.length > 1 ||
|
|
normalizedSingleLine.length > printWidth ||
|
|
value.includes("\n");
|
|
|
|
const opensElement =
|
|
!parsed.selfClosing &&
|
|
!parsed.inlineClosing &&
|
|
!parsed.closesInRemainder &&
|
|
!VOID_ELEMENTS.has(parsed.tagName.toLowerCase());
|
|
|
|
if (!shouldBreak) {
|
|
return {
|
|
lines: [`${baseIndent}${normalizedSingleLine}`],
|
|
opensElement,
|
|
};
|
|
}
|
|
|
|
const lines = [
|
|
`${baseIndent}<${parsed.tagName}`,
|
|
...parsed.attributes.map((attribute) => `${attributeIndent}${attribute}`),
|
|
];
|
|
|
|
if (parsed.inlineClosing) {
|
|
lines.push(`${baseIndent}></${parsed.tagName}>`);
|
|
} else if (parsed.selfClosing) {
|
|
lines.push(`${baseIndent}/>`);
|
|
} else {
|
|
lines.push(`${baseIndent}>`);
|
|
|
|
if (parsed.remainder) {
|
|
lines.push(`${baseIndent}${parsed.remainder}`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
lines,
|
|
opensElement,
|
|
};
|
|
}
|
|
|
|
function isMultilineOpeningTagStart(value) {
|
|
if (!value.startsWith("<")) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
value.startsWith("</") ||
|
|
value.startsWith("<!--") ||
|
|
value.startsWith("<!") ||
|
|
value.startsWith("<?")
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return findOpeningTagEnd(value) === -1;
|
|
}
|
|
|
|
function isClosingTag(value) {
|
|
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
|
|
}
|
|
|
|
function isInlineElement(value) {
|
|
return /^<([A-Za-z][\w$:.-]*)\b[^>]*>[\s\S]*<\/\1\s*>$/.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] !== "}") {
|
|
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 = true;
|
|
index += 3;
|
|
continue;
|
|
}
|
|
|
|
if (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;
|
|
}
|
|
}
|
|
|
|
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 formatWrn(source, options = {}) {
|
|
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize || 4);
|
|
|
|
const printWidth = options.printWidth || 100;
|
|
|
|
const inputLines = source.replace(/\r\n/g, "\n").split("\n");
|
|
|
|
const output = [];
|
|
|
|
let codeDepth = 0;
|
|
let htmlDepth = 0;
|
|
let index = 0;
|
|
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 (isMultilineOpeningTagStart(value)) {
|
|
const collected = collectOpeningTag(inputLines, index);
|
|
|
|
value = collected.value;
|
|
index = collected.endIndex;
|
|
}
|
|
|
|
const inlineProps = formatInlinePropsBlock(value, unit, codeDepth + htmlDepth);
|
|
if (inlineProps) {
|
|
output.push(...inlineProps);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
const leadingClosingBraces = countLeadingClosingBraces(value);
|
|
|
|
const lineCodeDepth = Math.max(0, codeDepth - leadingClosingBraces);
|
|
|
|
let lineHtmlDepth = htmlDepth;
|
|
|
|
if (isClosingTag(value)) {
|
|
lineHtmlDepth = Math.max(0, htmlDepth - 1);
|
|
}
|
|
|
|
const depth = lineCodeDepth + lineHtmlDepth;
|
|
|
|
if (
|
|
value.startsWith("<") &&
|
|
!value.startsWith("</") &&
|
|
!value.startsWith("<!--") &&
|
|
!value.startsWith("<!") &&
|
|
!value.startsWith("<?") &&
|
|
!isInlineElement(value)
|
|
) {
|
|
const formattedTag = formatOpeningTag(value, unit, depth, printWidth);
|
|
|
|
output.push(...formattedTag.lines);
|
|
|
|
if (formattedTag.opensElement) {
|
|
htmlDepth += 1;
|
|
}
|
|
} else {
|
|
output.push(`${unit.repeat(depth)}${value}`);
|
|
}
|
|
|
|
if (isClosingTag(value)) {
|
|
htmlDepth = lineHtmlDepth;
|
|
}
|
|
|
|
const braces = countStructuralBraces(value);
|
|
|
|
codeDepth = Math.max(
|
|
0,
|
|
lineCodeDepth + braces.openings - Math.max(0, braces.closings - leadingClosingBraces),
|
|
);
|
|
|
|
index += 1;
|
|
}
|
|
|
|
while (output.length > 0 && output[output.length - 1] === "") {
|
|
output.pop();
|
|
}
|
|
|
|
return `${output.join("\n")}\n`;
|
|
}
|
|
|
|
module.exports = {
|
|
countStructuralBraces,
|
|
formatOpeningTag,
|
|
formatWrn,
|
|
parseAttributes,
|
|
parseOpeningTag,
|
|
splitPropDeclarations,
|
|
};
|