release: WRNexusJS 0.8.0
This commit is contained in:
@@ -416,6 +416,17 @@ function formatOpeningTag(value, unit, depth, printWidth = 100, multilineAttribu
|
||||
};
|
||||
}
|
||||
|
||||
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}</${parsed.tagName}>`);
|
||||
} 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)),
|
||||
@@ -468,6 +479,75 @@ function isClosingTag(value) {
|
||||
return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve compact, already-balanced HTML fragments as one line.
|
||||
*
|
||||
* A line such as `<span>Page</span><b>→</b><span>API</span>` 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("</") || value.startsWith("<!--")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stack = [];
|
||||
let tagCount = 0;
|
||||
let rootCount = 0;
|
||||
let hasNestedElement = false;
|
||||
let hasOutsideText = false;
|
||||
let index = 0;
|
||||
|
||||
while (index < value.length) {
|
||||
const tagStart = value.indexOf("<", index);
|
||||
if (tagStart === -1) {
|
||||
if (stack.length === 0 && value.slice(index).trim()) hasOutsideText = true;
|
||||
break;
|
||||
}
|
||||
if (stack.length === 0 && value.slice(index, tagStart).trim()) hasOutsideText = true;
|
||||
|
||||
if (value.startsWith("<!--", tagStart)) {
|
||||
const commentEnd = value.indexOf("-->", 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("</");
|
||||
const selfClosing = /\/\s*>$/.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);
|
||||
}
|
||||
@@ -597,6 +677,50 @@ function collectOpeningTag(inputLines, startIndex) {
|
||||
};
|
||||
}
|
||||
|
||||
function isPreservedRawBlockStart(value) {
|
||||
return (
|
||||
/<pre(?:\s|>)/i.test(value) &&
|
||||
!/<\/pre\s*>/i.test(value.slice(0, value.search(/<pre(?:\s|>)/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.
|
||||
*
|
||||
@@ -608,7 +732,7 @@ function expandInlineControlBlocks(lines) {
|
||||
const marker =
|
||||
/(\{#(?:if|each)\b[^}]*\}|\{:(?:else(?:\s+if\b[^}]*)?|empty)\}|\{\/(?:if|each)\})/g;
|
||||
|
||||
return lines.flatMap((line) => {
|
||||
return transformOutsidePreservedRawBlocks(lines, (line) => {
|
||||
if (!marker.test(line)) return [line];
|
||||
marker.lastIndex = 0;
|
||||
|
||||
@@ -623,7 +747,7 @@ function expandInlineControlBlocks(lines) {
|
||||
}
|
||||
|
||||
function expandStructuredStateDeclarations(lines, unit) {
|
||||
return lines.flatMap((line) => {
|
||||
return transformOutsidePreservedRawBlocks(lines, (line) => {
|
||||
const match = /^(\s*state\s+[A-Za-z_$][\w$]*\s*=\s*)([\\[{][\s\S]*)$/.exec(line);
|
||||
if (!match) return [line];
|
||||
|
||||
@@ -695,6 +819,15 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
|
||||
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);
|
||||
|
||||
@@ -730,7 +863,11 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
|
||||
|
||||
const depth = lineCodeDepth + lineHtmlDepth + lineControlDepth;
|
||||
|
||||
if (
|
||||
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("</") &&
|
||||
!value.startsWith("<!--") &&
|
||||
|
||||
@@ -6,9 +6,24 @@ export type RuntimeType =
|
||||
export function runtimeTypeOf(annotation: string | undefined): RuntimeType {
|
||||
if (!annotation) return "unknown";
|
||||
const type = annotation.trim().replace(/^readonly\s+/, "");
|
||||
if (/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "string";
|
||||
if (/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "number";
|
||||
if (/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "boolean";
|
||||
const unionParts = type.split("|").map((part) => part.trim());
|
||||
const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
|
||||
if (
|
||||
/^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
|
||||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
|
||||
)
|
||||
return "string";
|
||||
if (
|
||||
/^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
|
||||
(concreteParts.length > 0 &&
|
||||
concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
|
||||
)
|
||||
return "number";
|
||||
if (
|
||||
/^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(type) ||
|
||||
(concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
|
||||
)
|
||||
return "boolean";
|
||||
if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(type)) return "bigint";
|
||||
if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(type) || /^\[/.test(type)) return "array";
|
||||
if (/^(?:Record\s*<|object\b|\{)/.test(type)) return "object";
|
||||
|
||||
Reference in New Issue
Block a user