prettier-plugin-marko
Version:
A prettier plugin for parsing and printing Marko files
1,380 lines (1,379 loc) • 41.5 kB
JavaScript
import { doc } from "prettier";
import { TagType, TagType as TagType$1, Validity, createParser, isValidAttrValue, isValidScriptlet, isValidStatement } from "htmljs-parser";
//#region src/parser.ts
const styleBlockReg = /((?:\.[^\s\\/:*?"<>|({]+)*)\s*\{/y;
const UNFINISHED = Number.MAX_SAFE_INTEGER;
let NodeType = /* @__PURE__ */ function(NodeType) {
NodeType[NodeType["Program"] = 0] = "Program";
NodeType[NodeType["Tag"] = 1] = "Tag";
NodeType[NodeType["OpenTagName"] = 2] = "OpenTagName";
NodeType[NodeType["ShorthandId"] = 3] = "ShorthandId";
NodeType[NodeType["ShorthandClassName"] = 4] = "ShorthandClassName";
NodeType[NodeType["TagTypeArgs"] = 5] = "TagTypeArgs";
NodeType[NodeType["TagTypeParams"] = 6] = "TagTypeParams";
NodeType[NodeType["TagVar"] = 7] = "TagVar";
NodeType[NodeType["TagArgs"] = 8] = "TagArgs";
NodeType[NodeType["TagParams"] = 9] = "TagParams";
NodeType[NodeType["AttrNamed"] = 10] = "AttrNamed";
NodeType[NodeType["AttrName"] = 11] = "AttrName";
NodeType[NodeType["AttrArgs"] = 12] = "AttrArgs";
NodeType[NodeType["AttrValue"] = 13] = "AttrValue";
NodeType[NodeType["AttrMethod"] = 14] = "AttrMethod";
NodeType[NodeType["AttrSpread"] = 15] = "AttrSpread";
NodeType[NodeType["AttrTag"] = 16] = "AttrTag";
NodeType[NodeType["Text"] = 17] = "Text";
NodeType[NodeType["CDATA"] = 18] = "CDATA";
NodeType[NodeType["Doctype"] = 19] = "Doctype";
NodeType[NodeType["Declaration"] = 20] = "Declaration";
NodeType[NodeType["Comment"] = 21] = "Comment";
NodeType[NodeType["Placeholder"] = 22] = "Placeholder";
NodeType[NodeType["Scriptlet"] = 23] = "Scriptlet";
NodeType[NodeType["Import"] = 24] = "Import";
NodeType[NodeType["Export"] = 25] = "Export";
NodeType[NodeType["Class"] = 26] = "Class";
NodeType[NodeType["Style"] = 27] = "Style";
NodeType[NodeType["Static"] = 28] = "Static";
return NodeType;
}({});
let CommentType = /* @__PURE__ */ function(CommentType) {
CommentType[CommentType["line"] = 0] = "line";
CommentType[CommentType["block"] = 1] = "block";
CommentType[CommentType["html"] = 2] = "html";
return CommentType;
}({});
function parse(code, filename = "index.marko") {
const builder = new Builder(code);
const parser = createParser(builder);
const { program } = builder;
parser.parse(code);
return {
read: parser.read,
locationAt: parser.locationAt,
positionAt: parser.positionAt,
filename,
program,
code
};
}
var Builder = class {
program;
#code;
#openTagStart;
#parentNode;
#staticNode;
#attrNode;
constructor(code) {
this.#code = code;
this.program = this.#parentNode = {
type: NodeType.Program,
parent: void 0,
body: [],
start: 0,
end: code.length
};
}
onText(range) {
pushBody(this.#parentNode, {
type: NodeType.Text,
parent: this.#parentNode,
start: range.start,
end: range.end
});
}
onCDATA(range) {
pushBody(this.#parentNode, {
type: NodeType.CDATA,
parent: this.#parentNode,
value: range.value,
start: range.start,
end: range.end
});
}
onDoctype(range) {
pushBody(this.#parentNode, {
type: NodeType.Doctype,
parent: this.#parentNode,
value: range.value,
start: range.start,
end: range.end
});
}
onDeclaration(range) {
pushBody(this.#parentNode, {
type: NodeType.Declaration,
parent: this.#parentNode,
value: range.value,
start: range.start,
end: range.end
});
}
onComment(range) {
let commentType = CommentType.html;
switch (this.#code.charCodeAt(range.start + 1)) {
case 47:
commentType = CommentType.line;
break;
case 42:
commentType = CommentType.block;
break;
}
pushBody(this.#parentNode, {
type: NodeType.Comment,
parent: this.#parentNode,
commentType,
value: range.value,
start: range.start,
end: range.end
});
}
onPlaceholder(range) {
pushBody(this.#parentNode, {
type: NodeType.Placeholder,
parent: this.#parentNode,
value: range.value,
escape: range.escape,
start: range.start,
end: range.end
});
}
onScriptlet(range) {
pushBody(this.#parentNode, {
type: NodeType.Scriptlet,
parent: this.#parentNode,
value: range.value,
block: range.block,
start: range.start,
end: range.end
});
}
onOpenTagStart(range) {
this.#openTagStart = range;
}
onOpenTagName(range) {
let concise = true;
let start = range.start;
let type = NodeType.Tag;
let bodyType = TagType.html;
let nameText = void 0;
if (this.#openTagStart) {
concise = false;
start = this.#openTagStart.start;
this.#openTagStart = void 0;
}
if (!range.expressions.length) switch (nameText = this.#code.slice(range.start, range.end) || "div") {
case "style": {
styleBlockReg.lastIndex = range.end;
const styleBlockMatch = styleBlockReg.exec(this.#code);
if (styleBlockMatch) {
const [{ length }, ext] = styleBlockMatch;
this.program.body.push(this.#staticNode = {
type: NodeType.Style,
parent: this.program,
ext: ext || void 0,
value: {
start: range.end + length,
end: UNFINISHED
},
start: range.start,
end: UNFINISHED
});
return TagType.statement;
} else {
bodyType = TagType.text;
break;
}
}
case "class":
this.program.body.push(this.#staticNode = {
type: NodeType.Class,
parent: this.program,
start: range.start,
end: UNFINISHED
});
return TagType.statement;
case "export":
this.program.body.push(this.#staticNode = {
type: NodeType.Export,
parent: this.program,
start: range.start,
end: UNFINISHED
});
return TagType.statement;
case "import":
this.program.body.push(this.#staticNode = {
type: NodeType.Import,
parent: this.program,
start: range.start,
end: UNFINISHED
});
return TagType.statement;
case "server":
case "client":
case "static":
this.program.body.push(this.#staticNode = {
type: NodeType.Static,
parent: this.program,
target: nameText,
start: range.start,
end: UNFINISHED
});
return TagType.statement;
case "area":
case "base":
case "br":
case "col":
case "embed":
case "hr":
case "img":
case "input":
case "link":
case "meta":
case "param":
case "source":
case "track":
case "wbr":
case "const":
case "debug":
case "id":
case "let":
case "lifecycle":
case "log":
case "return":
bodyType = TagType.void;
break;
case "html-comment":
case "html-script":
case "html-style":
case "script":
case "textarea":
bodyType = TagType.text;
break;
default:
if (nameText[0] === "@") type = NodeType.AttrTag;
break;
}
const parent = this.#parentNode;
const end = UNFINISHED;
const name = {
type: NodeType.OpenTagName,
parent: void 0,
quasis: range.quasis,
expressions: range.expressions,
start: range.start,
end: range.end
};
const tag = this.#parentNode = name.parent = {
type,
parent,
owner: void 0,
concise,
selfClosed: false,
hasAttrTags: false,
open: {
start,
end
},
nameText,
name,
var: void 0,
args: void 0,
params: void 0,
shorthandId: void 0,
shorthandClassNames: void 0,
typeArgs: void 0,
typeParams: void 0,
attrs: void 0,
bodyType,
body: void 0,
close: void 0,
start,
end
};
if (tag.type === NodeType.AttrTag) {
let parentTag = parent;
let nameText = tag.nameText.slice(1);
while (parentTag.type === NodeType.Tag && isControlFlowTag(parentTag)) {
parentTag.hasAttrTags = true;
parentTag = parentTag.parent;
}
switch (parentTag.type) {
case NodeType.AttrTag:
tag.owner = parentTag.owner;
parentTag.hasAttrTags = true;
nameText = `${parentTag.nameText}:${nameText}`;
break;
case NodeType.Tag:
tag.owner = parentTag;
parentTag.hasAttrTags = true;
nameText = `${parentTag.nameText || "*"}:${nameText}`;
break;
}
tag.nameText = nameText;
}
pushBody(parent, tag);
this.#openTagStart = void 0;
return bodyType;
}
onTagShorthandId(range) {
const parent = this.#parentNode;
parent.shorthandId = {
type: NodeType.ShorthandId,
parent,
quasis: range.quasis,
expressions: range.expressions,
start: range.start,
end: range.end
};
}
onTagShorthandClass(range) {
const parent = this.#parentNode;
const shorthandClassName = {
type: NodeType.ShorthandClassName,
parent,
quasis: range.quasis,
expressions: range.expressions,
start: range.start,
end: range.end
};
if (parent.shorthandClassNames) parent.shorthandClassNames.push(shorthandClassName);
else parent.shorthandClassNames = [shorthandClassName];
}
onTagTypeArgs(range) {
const parent = this.#parentNode;
parent.typeArgs = {
type: NodeType.TagTypeArgs,
parent,
value: range.value,
start: range.start,
end: range.end
};
}
onTagTypeParams(range) {
const parent = this.#parentNode;
parent.typeParams = {
type: NodeType.TagTypeParams,
parent,
value: range.value,
start: range.start,
end: range.end
};
}
onTagVar(range) {
const parent = this.#parentNode;
parent.var = {
type: NodeType.TagVar,
parent,
value: range.value,
start: range.start,
end: range.end
};
}
onTagParams(range) {
const parent = this.#parentNode;
parent.params = {
type: NodeType.TagParams,
parent,
value: range.value,
start: range.start,
end: range.end
};
}
onTagArgs(range) {
const parent = this.#parentNode;
parent.args = {
type: NodeType.TagArgs,
parent,
value: range.value,
start: range.start,
end: range.end
};
}
onAttrName(range) {
const parent = this.#parentNode;
const name = {
type: NodeType.AttrName,
parent: void 0,
start: range.start,
end: range.end
};
pushAttr(parent, this.#attrNode = name.parent = {
type: NodeType.AttrNamed,
parent,
name,
value: void 0,
args: void 0,
start: range.start,
end: range.end
});
}
onAttrArgs(range) {
const parent = this.#attrNode;
parent.args = {
type: NodeType.AttrArgs,
parent,
value: range.value,
start: range.start,
end: range.end
};
parent.end = range.end;
}
onAttrValue(range) {
const parent = this.#attrNode;
parent.value = {
type: NodeType.AttrValue,
parent,
value: range.value,
bound: range.bound,
start: range.start,
end: range.end
};
parent.end = range.end;
}
onAttrMethod(range) {
const parent = this.#attrNode;
parent.value = {
type: NodeType.AttrMethod,
parent,
typeParams: range.typeParams,
params: range.params,
body: range.body,
start: range.start,
end: range.end
};
parent.end = range.end;
}
onAttrSpread(range) {
const parent = this.#parentNode;
pushAttr(parent, {
type: NodeType.AttrSpread,
parent,
value: range.value,
start: range.start,
end: range.end
});
}
onOpenTagEnd(range) {
if (this.#staticNode) {
if (this.#staticNode.type === NodeType.Style) this.#staticNode.value.end = range.end - 1;
this.#staticNode.end = range.end;
this.#staticNode = void 0;
} else {
this.#attrNode = void 0;
const tag = this.#parentNode;
tag.open.end = range.end;
if (range.selfClosed || tag.bodyType === TagType.void) {
this.#parentNode = tag.parent;
tag.end = range.end;
tag.selfClosed = range.selfClosed;
}
}
}
onCloseTagStart(range) {
this.#parentNode.close = {
start: range.start,
end: UNFINISHED
};
}
onCloseTagEnd(range) {
const parent = this.#parentNode;
if (hasCloseTag(parent)) parent.close.end = range.end;
parent.end = range.end;
this.#parentNode = parent.parent;
}
};
function pushBody(parent, node) {
if (parent.body) parent.body.push(node);
else parent.body = [node];
}
function pushAttr(parent, node) {
if (parent.attrs) parent.attrs.push(node);
else parent.attrs = [node];
}
function hasCloseTag(parent) {
return parent.close !== void 0;
}
/**
* Used to check if a node should be ignored as the parent of an attribute tag.
* When control flow is the parent of an attribute tag, we add the attribute tag to
* the closest non control flow ancestor attrs instead.
*/
function isControlFlowTag(node) {
switch (node.nameText) {
case "if":
case "else":
case "else-if":
case "for":
case "while": return true;
default: return false;
}
}
//#endregion
//#region src/utils/read.ts
function read(range, opts) {
return opts._markoParsed.read(range);
}
//#endregion
//#region src/utils/get-formatted-body.ts
const placeholderReg = /MARKO_(\d+)_/g;
const { mapDoc: mapDoc$1 } = doc.utils;
async function getFormattedBody(tag, parser, toDoc, print, opts) {
let pid = 0;
let code = "";
let placeholders;
tag.each((child) => {
if (child.node.type === NodeType.Placeholder) {
code += `MARKO_${pid++}_`;
(placeholders ||= []).push(print(child));
} else code += read(child.node, opts);
}, "body");
const doc = parser ? await toDoc(code, { parser }) : code;
return !placeholders ? doc : mapDoc$1(doc, (cur) => {
if (typeof cur === "string") {
let match = placeholderReg.exec(cur);
if (match) {
const replacementDocs = [];
let index = 0;
do {
const placeholderIndex = +match[1];
if (index !== match.index) replacementDocs.push(cur.slice(index, match.index));
replacementDocs.push(placeholders[placeholderIndex]);
index = match.index + match[0].length;
} while (match = placeholderReg.exec(cur));
if (index !== cur.length) replacementDocs.push(cur.slice(index));
if (replacementDocs.length === 1) return replacementDocs[0];
return replacementDocs;
}
}
return cur;
});
}
//#endregion
//#region src/utils/get-parser-name.ts
function getParserFromExt(ext) {
switch (ext) {
case ".css": return "css";
case ".less": return "less";
case ".scss": return "scss";
case ".js":
case ".mjs":
case ".cjs": return "babel";
case ".ts":
case ".mts":
case ".cts": return "babel-ts";
default: return false;
}
}
function hasTagParser(tag) {
switch (tag.nameText) {
case "script":
case "html-script":
case "style":
case "html-style": return true;
default: return false;
}
}
function getTagParser(tag, opts) {
switch (tag.body && tag.nameText) {
case "script":
case "html-script": return getScriptTagParser(tag, opts);
case "style": return getStyleTagParser(tag, opts);
case "html-style": return "css";
}
}
function getScriptTagParser(tag, opts) {
if (tag.attrs?.length) {
for (const attr of tag.attrs) if (attr.type === NodeType.AttrNamed && attr.value?.type === NodeType.AttrValue && read(attr.name, opts) === "type") {
const { code } = opts._markoParsed;
const value = attr.value.value;
const start = code.charAt(value.start);
switch ((start === "\"" || start === "'") && start === code.charAt(value.end - 1) ? code.slice(value.start + 1, value.end - 1) : "") {
case "module":
case "text/javascript":
case "application/javascript": return "babel-ts";
case "importmap":
case "speculationrules":
case "application/json": return "json";
default: return false;
}
}
}
return "babel-ts";
}
function getStyleTagParser(tag, opts) {
return getParserFromExt(tag.shorthandClassNames && read(tag.shorthandClassNames[tag.shorthandClassNames.length - 1], opts) || ".css");
}
//#endregion
//#region src/utils/print-doc.ts
const DocCache = /* @__PURE__ */ new WeakMap();
function printDoc(doc) {
switch (typeof doc) {
case "string": return doc;
case "object": if (doc !== null) {
let cached = DocCache.get(doc);
if (cached !== void 0) return cached;
if (Array.isArray(doc)) {
cached = "";
for (const item of doc) cached += printDoc(item);
} else switch (doc.type) {
case "align":
cached = `\n${printDoc(doc.contents)}\n`;
break;
case "indent":
cached = ` ${printDoc(doc.contents)} `;
break;
case "break-parent":
case "cursor":
case "line-suffix-boundary":
case "trim":
cached = "";
break;
case "fill":
cached = ` ${printDoc(doc.parts)} `;
break;
case "group":
cached = printDoc(doc.contents) + printDoc(doc.expandedStates);
break;
case "if-break":
cached = printDoc(doc.flatContents) + printDoc(doc.breakContents);
break;
case "indent-if-break":
cached = " " + printDoc(doc.contents) + printDoc(doc.negate);
break;
case "label":
case "line-suffix":
cached = printDoc(doc.contents);
break;
default:
cached = "\n";
break;
}
DocCache.set(doc, cached);
return cached;
}
}
return "";
}
//#endregion
//#region src/utils/to-valid-doc.ts
const { builders: b$2 } = doc;
function toValidAttrValue(doc, concise) {
switch (isValidAttrValue(printDoc(doc).trim(), concise)) {
case Validity.enclosed: return doc;
case Validity.valid: return b$2.group([
b$2.ifBreak("("),
b$2.indent([b$2.softline, doc]),
b$2.softline,
b$2.ifBreak(")")
]);
default: return b$2.group([
"(",
b$2.indent([b$2.softline, doc]),
b$2.softline,
")"
]);
}
}
function toValidScriptlet(doc) {
return toValidBlock(doc, isValidScriptlet);
}
function toValidStatement(doc) {
return toValidBlock(doc, isValidStatement);
}
function toValidBlock(doc, check) {
if (check(printDoc(doc).trim()) === Validity.enclosed) return doc;
return b$2.group([
b$2.ifBreak("{"),
b$2.indent([b$2.softline, doc]),
b$2.softline,
b$2.ifBreak("}")
]);
}
//#endregion
//#region src/utils/visible-space.ts
const b$1 = doc.builders;
const singleQuoteSpace = "${' '}";
const doubleQuoteSpace = "${\" \"}";
const singleQuoteSpaceIfBreak = b$1.ifBreak([singleQuoteSpace, b$1.line], " ");
const doubleQuoteSpaceIfBreak = b$1.ifBreak([doubleQuoteSpace, b$1.line], " ");
function ensureVisibleSpace(parts, opts) {
if (parts[0] === b$1.line) parts[0] = getVisibleSpace(opts);
if (parts[parts.length - 1] === b$1.line) parts[parts.length - 1] = getVisibleSpace(opts);
}
function ensureVisibleTrailingSpace(parts, opts) {
const last = parts.length - 1;
if (typeof parts[last] === "string" && /[ \t]$/.test(parts[last])) parts[last] = parts[last].slice(0, -1) + getVisibleSpace(opts);
}
function ensureVisibleSpaceBetweenTags(parts, opts) {
const last = parts.length - 1;
if (last > 0 && parts[last] === b$1.line) {
if (typeof parts[last - 1] !== "string") parts[last] = opts.singleQuote ? singleQuoteSpaceIfBreak : doubleQuoteSpaceIfBreak;
}
}
function getVisibleSpace(opts) {
return opts.singleQuote ? singleQuoteSpace : doubleQuoteSpace;
}
function isVisibleSpace(code) {
switch (code) {
case singleQuoteSpace:
case doubleQuoteSpace: return true;
default: return false;
}
}
//#endregion
//#region src/index.ts
const b = doc.builders;
const traverseDoc = doc.utils.traverseDoc;
const findInDoc = doc.utils.findInDoc;
const mapDoc = doc.utils.mapDoc;
const stmtParse = { parser: "babel-ts" };
const exprParse = { parser: "__ts_expression" };
const noVisitorKeys = [];
const tagVisitorKeys = [
"name",
"shorthandId",
"shorthandClassNames",
"var",
"args",
"typeArgs",
"params",
"typeParams",
"attrs",
"body"
];
const visitorKeys = {
[NodeType.Tag]: tagVisitorKeys,
[NodeType.AttrTag]: tagVisitorKeys,
[NodeType.Program]: ["body"],
[NodeType.AttrNamed]: ["args", "value"]
};
const languages = [{
name: "marko",
aceMode: "text",
parsers: ["marko"],
aliases: ["markojs"],
tmScope: "text.marko",
codemirrorMode: "htmlmixed",
vscodeLanguageIds: ["marko"],
linguistLanguageId: 932782397,
codemirrorMimeType: "text/html",
extensions: [".marko"]
}];
const options = { markoSyntax: {
type: "choice",
default: "auto",
category: "Marko",
description: "Change output syntax between HTML mode, concise mode and auto.",
choices: [
{
value: "auto",
description: "Determine output syntax by the input syntax used."
},
{
value: "html",
description: "Force the output to use the HTML syntax."
},
{
value: "concise",
description: "Force the output to use the concise syntax."
}
]
} };
const parsers = { marko: {
astFormat: "marko-ast",
parse(text, opts) {
const { program } = opts._markoParsed = parse(text, opts.filepath);
if (opts.markoSyntax === "auto") {
opts.markoSyntax = "html";
for (const child of program.body) if (child.type === NodeType.Tag) {
if (child.concise) opts.markoSyntax = "concise";
break;
}
}
return program;
},
locStart(node) {
return node.start;
},
locEnd(node) {
return node.end;
}
} };
const printers = { "marko-ast": {
print(path, opts, print) {
const { type } = path.node;
const handler = printHandlers[type];
if (handler) return handler(path, opts, print);
/* c8 ignore next */
throw new Error(`Unknown node type in Marko template: ${NodeType[type] || type}`);
},
embed(path) {
return embedHandlers[path.node.type] ?? null;
},
getVisitorKeys(node) {
return visitorKeys[node.type] || noVisitorKeys;
}
} };
const printHandlers = {
[NodeType.AttrArgs]: printExact,
[NodeType.AttrMethod]: printExact,
[NodeType.AttrNamed]: printExact,
[NodeType.AttrSpread]: printExact,
[NodeType.Class]: printExact,
[NodeType.Export]: printExact,
[NodeType.Import]: printExact,
[NodeType.OpenTagName]: printExact,
[NodeType.Placeholder]: printExact,
[NodeType.Scriptlet]: printExact,
[NodeType.ShorthandClassName]: printExact,
[NodeType.ShorthandId]: printExact,
[NodeType.Static]: printExact,
[NodeType.Style]: printExact,
[NodeType.TagArgs]: printExact,
[NodeType.TagParams]: printExact,
[NodeType.TagTypeArgs]: printExact,
[NodeType.TagTypeParams]: printExact,
[NodeType.TagVar]: printExact,
[NodeType.Tag]: printTag,
[NodeType.AttrTag]: printTag,
[NodeType.CDATA]: (path, opts) => `<![CDATA[${read(path.node.value, opts)}]]>`,
[NodeType.Comment]: (path, opts) => {
const { node } = path;
const code = read(node, opts);
if (node.commentType !== CommentType.line) {
if (code.includes("\n")) {
const lines = code.split("\n");
const len = lines.length;
let indent = Infinity;
for (let i = 1; i < len; i++) {
const match = lines[i].match(/^(\s+)/);
if (match) indent = Math.min(indent, match[1].length);
else {
indent = 0;
break;
}
}
const parts = [lines[0]];
for (let i = 1; i < len; i++) parts.push(b.hardline, indent ? lines[i].slice(indent) : lines[i]);
return parts;
}
return code;
}
return b.lineSuffix(code);
},
[NodeType.Doctype]: (path, opts) => `<!${read(path.node.value, opts).replace(/\s+/g, " ").trim()}>`,
[NodeType.Declaration]: (path, opts) => `<?${read(path.node.value, opts).trim()}?>`,
[NodeType.Program]: (path, opts, print) => {
const body = printBody(path, opts, print);
if (!body) return [b.hardline];
return [body.inline ? wrapConciseText(body.content) : b.join(b.hardline, body.content), b.hardline];
},
[NodeType.Text]: (path, opts) => {
const text = read(path.node, opts).replace(/\\/g, "\\\\");
return /^\$!?{/.test(text) ? "\\" + text : text;
}
};
const embedHandlers = {
[NodeType.Class]: (toDoc, _print, path, opts) => toDoc(read(path.node, opts), exprParse),
[NodeType.Import]: (toDoc, _print, path, opts) => toDoc(read(path.node, opts), stmtParse),
[NodeType.Export]: (toDoc, _print, path, opts) => toDoc(read(path.node, opts), stmtParse),
[NodeType.Style]: async (toDoc, _print, path, opts) => {
const { node } = path;
const code = read(node.value, opts).trim();
const parser = getParserFromExt(node.ext?.slice(node.ext.lastIndexOf(".")) || ".css");
if (parser) return b.group([
`style${node.ext || ""} {`,
b.indent([b.line, await toDoc(code, { parser })]),
b.line,
"}"
]);
},
[NodeType.Static]: async (toDoc, _print, path, opts) => {
const { node } = path;
const code = opts._markoParsed.code.slice(node.start + node.target.length + 1, node.end).replace(/^\s*\{([\s\S]*)\}\s*$/, "$1").trim();
return code ? [`${node.target} `, toValidStatement(await toDoc(code, stmtParse))] : [];
},
[NodeType.Scriptlet]: async (toDoc, _print, path, opts) => {
const code = read(path.node.value, opts).replace(/^\s*\{([\s\S]*)\}\s*$/, "$1").trim();
return code ? [
b.breakParent,
"$ ",
toValidScriptlet(await toDoc(code, stmtParse))
] : [];
},
[NodeType.OpenTagName]: (toDoc, _print, path, opts) => templateToDoc(toDoc, path, opts),
[NodeType.Placeholder]: async (toDoc, _print, path, opts) => {
const { node } = path;
const code = read(node.value, opts);
if (code === "\" \"" || code === "' '") return getVisibleSpace(opts);
return b.group([
node.escape ? "${" : "$!{",
b.indent([b.softline, await toDoc(code, exprParse)]),
b.softline,
"}"
]);
},
[NodeType.TagArgs]: (toDoc, _print, path, opts) => argsToDoc(path.node, opts, toDoc),
[NodeType.AttrNamed]: async (toDoc, _print, path, opts) => {
const { node } = path;
const name = read(node.name, opts);
if (!(node.args || node.value)) return name;
const attrDoc = [name];
if (node.args && !isEmpty(node.args.value, opts)) {
const argsDoc = await argsToDoc(node.args, opts, toDoc);
if (argsDoc) attrDoc.push(argsDoc);
else return unexpectedDoc(opts, node);
}
if (node.value) if (node.value.type === NodeType.AttrMethod) {
const attrMethodDoc = await toDoc(`function${read(node.value, opts)}`, exprParse);
if (Array.isArray(attrMethodDoc) && attrMethodDoc.length && typeof attrMethodDoc[0] === "string") {
attrMethodDoc[0] = attrMethodDoc[0].replace(/^function\s*/, "");
attrDoc.push(attrMethodDoc);
} else return unexpectedDoc(opts, node);
} else attrDoc.push(node.value.bound ? ":=" : "=", toValidAttrValue(await toDoc(read(node.value.value, opts), exprParse), isConcise(opts)));
return b.group(attrDoc);
},
[NodeType.AttrSpread]: async (toDoc, _print, path, opts) => {
return b.group(["...", toValidAttrValue(await toDoc(read(path.node.value, opts), exprParse), isConcise(opts))]);
},
[NodeType.ShorthandId]: async (toDoc, _print, path, opts) => ["#", await templateToDoc(toDoc, path, opts)],
[NodeType.ShorthandClassName]: async (toDoc, _print, path, opts) => [".", await templateToDoc(toDoc, path, opts)],
[NodeType.Tag]: async (toDoc, print, path, opts) => {
const parser = getTagParser(path.node, opts);
if (parser === void 0) return void 0;
return printTag(path, opts, print, {
inline: true,
preserve: parser === false,
content: await getFormattedBody(path, parser, toDoc, print, opts)
});
},
[NodeType.TagVar]: async (toDoc, _print, path, opts) => {
const { node } = path;
let doc = await toDoc(`var ${read(node.value, opts).trim()}=_`, stmtParse);
if (Array.isArray(doc) && doc.length === 1) doc = doc[0];
if (typeof doc === "object" && !Array.isArray(doc) && doc.type === "group") doc = doc.contents;
if (Array.isArray(doc) && doc.length > 1) {
const varPart = doc[1];
if (typeof varPart === "object" && "type" in varPart && varPart.type === "group" && Array.isArray(varPart.contents)) {
const varContents = varPart.contents;
for (let i = varContents.length; i--;) {
const item = varContents[i];
if (typeof item === "string") {
const match = /\s*=\s*$/.exec(item);
if (match) {
varContents[i] = item.slice(0, -match[0].length);
varContents.length = i + 1;
return ["/", varContents];
}
}
}
}
}
return unexpectedDoc(opts, node);
/* c8 ignore stop */
},
[NodeType.TagTypeArgs]: async (toDoc, _print, path, opts) => {
const { node } = path;
if (isEmpty(node.value, opts)) return "";
const doc = await toDoc(`_<${read(node.value, opts).trim()}>`, exprParse);
if (typeof doc === "string") return doc.replace(/^_/, "");
if (Array.isArray(doc) && typeof doc[0] === "string") {
doc[0] = doc[0].replace(/^_/, "");
return doc;
}
/* c8 ignore next */
return unexpectedDoc(opts, node);
},
[NodeType.TagParams]: async (toDoc, _print, path, opts) => {
const { node } = path;
if (isEmpty(node.value, opts)) return "";
const doc = await toDoc(`function _(${read(node.value, opts).trim()}){}`, stmtParse);
if (Array.isArray(doc) && doc.length > 1) {
const paramsGroup = doc[1];
if (paramsGroup && typeof paramsGroup === "object" && "type" in paramsGroup && paramsGroup.type === "group" && Array.isArray(paramsGroup.contents)) {
let paramsContents = [...paramsGroup.contents];
const first = paramsContents[0];
const last = paramsContents[paramsContents.length - 1];
if (typeof first === "string" && typeof last === "string") {
paramsContents[0] = first.replace(/^\(/, "|");
paramsContents[paramsContents.length - 1] = last.replace(/\)$/, "|");
}
if (docContainsPipe(paramsContents.slice(1, -1))) paramsContents = wrapPipedTypesInParens(paramsContents);
return b.group(paramsContents);
}
}
/* c8 ignore next */
return unexpectedDoc(opts, node);
},
[NodeType.TagTypeParams]: async (toDoc, _print, path, opts) => {
const { node } = path;
if (isEmpty(node.parent.params?.value, opts) || isEmpty(node.value, opts)) return "";
const doc = await toDoc(`function _<${read(node.value, opts).trim()}>(){}`, stmtParse);
if (Array.isArray(doc) && doc.length > 1) return doc[1];
/* c8 ignore next */
return unexpectedDoc(opts, node);
}
};
function printTag(path, opts, print, body = printBody(path, opts, print)) {
return (isConcise(opts) ? printConciseTag : printHTMLTag)(path, opts, print, body);
}
function printHTMLTag(path, opts, print, body) {
const { node } = path;
const openTagDoc = ["<", printTagBeforeAttrs(path, opts, print)];
if (node.attrs) {
const hasDefault = isDefaultAttr(node.attrs[0]);
let attrsDocs = path.map(print, "attrs");
if (hasDefault) {
openTagDoc.push(attrsDocs[0]);
attrsDocs = attrsDocs.slice(1);
}
if (attrsDocs.length) if (attrsDocs.length === 1 && !(hasDefault || node.params || node.args)) openTagDoc.push(" ", attrsDocs[0]);
else openTagDoc.push(b.indent([b.line, b.join(b.line, attrsDocs)]), b.softline);
}
openTagDoc.push(body || node.bodyType === TagType$1.void ? ">" : "/>");
if (body) {
const bodyLine = body.inline ? b.softline : b.hardline;
const closeTagDoc = `</${node.name.expressions.length ? "" : read(node.name, opts)}>`;
if (body.preserve) return b.group([
b.group(openTagDoc),
body.content,
closeTagDoc
]);
return b.group([
b.group(openTagDoc),
b.indent([bodyLine, body.inline ? body.content : b.join(bodyLine, body.content)]),
bodyLine,
closeTagDoc
]);
}
return b.group(openTagDoc);
}
function printConciseTag(path, opts, print, body) {
const { node } = path;
const tagDoc = [printTagBeforeAttrs(path, opts, print)];
if (node.attrs) {
const hasDefault = isDefaultAttr(node.attrs[0]);
let attrsDocs = path.map(print, "attrs");
if (hasDefault) {
tagDoc.push(attrsDocs[0]);
attrsDocs = attrsDocs.slice(1);
}
if (attrsDocs.length) if (attrsDocs.length === 1 && !(hasDefault || node.params || node.args)) tagDoc.push(" ", attrsDocs[0]);
else {
const attrsDoc = [];
for (const attrDoc of attrsDocs) attrsDoc.push(b.line, b.ifBreak(","), attrDoc);
tagDoc.push(b.group(b.indent(attrsDoc)));
}
}
if (body) tagDoc.push(b.group(body.inline ? body.preserve ? b.indent([b.line, wrapConciseText(body.content)]) : [" --", b.indent([b.line, body.content])] : b.indent([b.hardline, b.join(b.hardline, body.content)])));
return b.group(tagDoc);
}
function printTagBeforeAttrs(path, opts, print) {
const { node } = path;
const name = path.call(print, "name");
const doc = [name];
if (pathHas(path, "typeArgs") && !isEmpty(path.node.typeArgs.value, opts)) doc.push(path.call(print, "typeArgs"));
if (pathHas(path, "shorthandId")) doc.push(path.call(print, "shorthandId"));
if (pathHas(path, "shorthandClassNames")) doc.push(path.map(print, "shorthandClassNames"));
if (pathHas(path, "args") && !isEmpty(path.node.args.value, opts)) doc.push(path.call(print, "args"));
if (pathHas(path, "var")) doc.push(path.call(print, "var"));
if (pathHas(path, "params") && !isEmpty(path.node.params.value, opts)) {
if (pathHas(path, "typeParams") && !isEmpty(path.node.typeParams.value, opts)) {
if (!(node.typeArgs || node.args || node.var)) doc.push(" ");
doc.push(path.call(print, "typeParams"));
}
doc.push(path.call(print, "params"));
}
return doc.length === 1 ? name : doc;
}
function printBody(path, opts, print) {
const { node } = path;
if (!node.body) return;
const concise = !node.parent || isConcise(opts);
const isInline = concise ? isTextLike : isInlineHTML;
const preserve = hasPreservedText(node);
let content;
let inline;
let inlineIndex = -1;
if (preserve) {
let inlineChild = false;
path.each((child) => {
const childDoc = child.call(print);
inlineChild = isInline(child.node) || inlineChild && child.node.type === NodeType.Comment && child.node.commentType !== CommentType.line;
content ||= [];
if (inlineChild) {
if (!inline) {
inline = [];
inlineIndex = content.push(inline) - 1;
}
if (child.node.type === NodeType.Text && typeof childDoc === "string") {
const nl = isConcise(opts) ? b.hardline : b.literalline;
let lineStart = 0;
for (let i = 0; i < childDoc.length; i++) if (childDoc.charAt(i) === "\n") {
if (lineStart !== i) inline.push(childDoc.slice(lineStart, i));
inline.push(nl);
lineStart = i + 1;
}
if (!lineStart) inline.push(childDoc);
else if (lineStart !== childDoc.length) inline.push(childDoc.slice(lineStart));
} else inline.push(childDoc);
} else {
if (inline) {
if (concise) {
ensureVisibleTrailingSpace(inline, opts);
content[inlineIndex] = wrapConciseText(content[inlineIndex]);
}
inline = void 0;
}
content.push(childDoc);
}
}, "body");
if (inline && concise) ensureVisibleTrailingSpace(inline, opts);
} else {
let inlineChild = false;
let isInlineTag = false;
let isExplicitLine = false;
path.each((child) => {
const wasInlineTag = isInlineTag;
let childDoc = child.call(print);
if (child.node.type === NodeType.Text && typeof childDoc === "string") childDoc = trimText(childDoc, child);
if (!childDoc) return;
content ||= [];
isInlineTag = false;
inlineChild = isInline(child.node) || inlineChild && child.node.type === NodeType.Comment && child.node.commentType !== CommentType.line;
if (isExplicitLine) {
const last = content.length - 1;
isExplicitLine = false;
content[last] = [content[last], b.hardline];
}
if (inlineChild) {
if (!inline) {
inline = [];
inlineIndex = content.push(b.fill(inline)) - 1;
}
switch (child.node.type) {
case NodeType.Text:
if (typeof childDoc === "string") {
const len = childDoc.length;
let start = 0;
for (let i = 0; i < len; i++) if (childDoc.charAt(i) === " ") {
if (start !== i) inline.push(childDoc.slice(start, i));
if (i || !endsWithLine(inline)) inline.push(b.line);
start = i + 1;
}
if (start === len) return;
if (start) childDoc = childDoc.slice(start);
}
break;
case NodeType.Placeholder:
if (typeof childDoc === "string" && isVisibleSpace(childDoc)) {
if (endsWithLine(inline)) return;
childDoc = b.line;
}
break;
case NodeType.Tag:
isInlineTag = true;
ensureVisibleSpaceBetweenTags(inline, opts);
if (wasInlineTag) inline.push(b.softline);
break;
}
inline.push(childDoc);
} else {
isExplicitLine = !child.isLast && hasExplicitLine(child, opts);
if (inline) {
ensureVisibleSpace(inline, opts);
inline = void 0;
if (concise) content[inlineIndex] = wrapConciseText(content[inlineIndex]);
}
content.push(childDoc);
}
}, "body");
}
if (content) {
if (inline) {
ensureVisibleSpace(inline, opts);
if (inlineIndex === 0) return {
inline: true,
preserve,
content: content[inlineIndex]
};
if (concise) content[inlineIndex] = wrapConciseText(content[inlineIndex]);
}
return {
inline: false,
preserve,
content
};
}
}
function printExact(path, opts) {
return read(path.node, opts);
}
async function templateToDoc(toDoc, path, opts) {
const { expressions, quasis } = path.node;
const first = read(quasis[0], opts);
const len = expressions.length;
if (!len) return first;
const shorthandDoc = [first];
for (let i = 0; i < len; i++) {
const quasi = read(quasis[i + 1], opts);
const expr = read(expressions[i].value, opts);
shorthandDoc.push(b.group([
"${",
b.indent([b.softline, await toDoc(expr, exprParse)]),
b.softline,
"}"
]));
if (quasi) shorthandDoc.push(quasi);
}
return shorthandDoc;
}
async function argsToDoc(node, opts, toDoc) {
if (isEmpty(node.value, opts)) return "";
const doc = await toDoc(`_(${read(node.value, opts).trim()})`, exprParse);
if (Array.isArray(doc) && doc.length && typeof doc[0] === "string") {
doc[0] = doc[0].replace(/^_/, "");
return doc;
}
/* c8 ignore next */
return unexpectedDoc(opts, node);
}
function wrapConciseText(doc) {
let maxDashes = 0;
traverseDoc(doc, (child) => {
if (typeof child === "string") {
let current = 0;
for (const char of child) if (char === "-") {
current++;
if (current > maxDashes) maxDashes = current;
} else current = 0;
}
});
const breakDashes = maxDashes > 1 ? "-".repeat(maxDashes + 1) : "--";
return b.group([
b.ifBreak(breakDashes, "--"),
b.line,
doc,
b.ifBreak([b.line, breakDashes])
]);
}
function trimText(text, path) {
if (/^(?:\n\s*)?(?:\n\s*)?$/.test(text)) return "";
const siblings = path.siblings;
let trimmed = text;
let prev;
let next;
for (let i = path.index; --i >= 0;) {
const sibling = siblings[i];
if (sibling.type !== NodeType.Scriptlet && sibling.type !== NodeType.Comment) {
prev = sibling;
break;
}
}
for (let i = path.index; ++i < siblings.length;) {
const sibling = siblings[i];
if (sibling.type !== NodeType.Scriptlet && sibling.type !== NodeType.Comment) {
next = sibling;
break;
}
}
const parent = path.node.parent;
const isInline = !parent.parent || parent.concise ? isTextLike : isInlineHTML;
const trimStart = !(prev && isInline(prev));
const trimEnd = !(next && isInline(next));
if (trimStart) trimmed = trimmed.replace(/^\n\s*/, "");
if (trimEnd) trimmed = trimmed.replace(/\n\s*$/, "");
return trimmed.replace(/\s+/g, " ");
}
function isTextLike(node) {
switch (node.type) {
case NodeType.Text:
case NodeType.Placeholder: return true;
default: return false;
}
}
function isInlineHTML(node) {
switch (node.type) {
case NodeType.Text:
case NodeType.Placeholder: return true;
case NodeType.Tag: return !!node.nameText && /^(?:a(?:bbr|cronym)?|b(?:do|ig|r)?|cite|code|dfn|em|i(?:mg)?|kbd|label|map|object|output|q|samp|small|span|strong|sub|sup|time|tt|var)$/.test(node.nameText);
default: return false;
}
}
function hasPreservedText(node) {
if (node.type === NodeType.Tag && hasTagParser(node)) return true;
let cur = node;
while (cur.type === NodeType.Tag) {
if (cur.nameText && /^(?:textarea|pre)$/.test(cur.nameText)) return true;
cur = cur.parent;
}
return false;
}
function isDefaultAttr(node) {
if (node.type === NodeType.AttrNamed && node.value && node.name.start === node.name.end) return true;
return false;
}
function isConcise(opts) {
return opts.markoSyntax === "concise";
}
const explicitLineReg = /\S?\n\n/y;
function hasExplicitLine(path, opts) {
explicitLineReg.lastIndex = path.node.end - 1;
return explicitLineReg.test(opts._markoParsed.code);
}
function endsWithLine(doc) {
switch (doc.length && doc[doc.length - 1]) {
case b.line:
case b.hardline:
case b.literalline:
case b.softline:
case b.hardlineWithoutBreakParent: return true;
default: return false;
}
}
const nonWhitespaceReg = /\S/g;
function isEmpty(range, opts) {
if (!range || range.start === range.end) return true;
nonWhitespaceReg.lastIndex = range.start;
return !(nonWhitespaceReg.test(opts._markoParsed.code) && nonWhitespaceReg.lastIndex <= range.end);
}
function pathHas(path, key) {
return !!path.node[key];
}
function docContainsPipe(doc) {
return !!findInDoc(doc, (child) => {
if (typeof child === "string" && child.includes("|")) return true;
}, false);
}
function wrapPipedTypesInParens(doc) {
return mapDoc(doc, (node) => {
if (Array.isArray(node)) {
let changed = false;
const result = [...node];
for (let i = 1; i < result.length; i++) {
const prev = result[i - 1];
const cur = result[i];
if (typeof cur === "string" || !docContainsPipe(cur)) continue;
if (typeof prev === "string" && prev.endsWith(": ") || i >= 2 && result[i - 2] === ":" && prev === " ") {
result[i] = b.group([
"(",
cur,
b.softline,
")"
]);
changed = true;
}
}
return changed ? result : node;
}
return node;
});
}
/* c8 ignore start */
function unexpectedDoc(opts, node) {
const parsed = opts._markoParsed;
const pos = parsed.positionAt(node.start);
console.warn(`Unable to format "${NodeType[node.type]}", please open an issue https://github.com/marko-js/prettier/issues.${opts.filepath ? `:\n at ${opts.filepath}:${pos.line + 1}:${pos.character + 1}` : ""}\n${parsed.read(node).replace(/(?:^|\n)(?!\n])/, opts.filepath ? "$& " : "$& ")}\n`);
}
/* c8 ignore stop */
//#endregion
export { languages, options, parsers, printers };