UNPKG

prettier-plugin-marko

Version:

A prettier plugin for parsing and printing Marko files

1,384 lines (1,383 loc) 41.9 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); let prettier = require("prettier"); let htmljs_parser = require("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 = (0, htmljs_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 = htmljs_parser.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 htmljs_parser.TagType.statement; } else { bodyType = htmljs_parser.TagType.text; break; } } case "class": this.program.body.push(this.#staticNode = { type: NodeType.Class, parent: this.program, start: range.start, end: UNFINISHED }); return htmljs_parser.TagType.statement; case "export": this.program.body.push(this.#staticNode = { type: NodeType.Export, parent: this.program, start: range.start, end: UNFINISHED }); return htmljs_parser.TagType.statement; case "import": this.program.body.push(this.#staticNode = { type: NodeType.Import, parent: this.program, start: range.start, end: UNFINISHED }); return htmljs_parser.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 htmljs_parser.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 = htmljs_parser.TagType.void; break; case "html-comment": case "html-script": case "html-style": case "script": case "textarea": bodyType = htmljs_parser.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 === htmljs_parser.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 } = prettier.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 } = prettier.doc; function toValidAttrValue(doc, concise) { switch ((0, htmljs_parser.isValidAttrValue)(printDoc(doc).trim(), concise)) { case htmljs_parser.Validity.enclosed: return doc; case htmljs_parser.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, htmljs_parser.isValidScriptlet); } function toValidStatement(doc) { return toValidBlock(doc, htmljs_parser.isValidStatement); } function toValidBlock(doc, check) { if (check(printDoc(doc).trim()) === htmljs_parser.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 = prettier.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 = prettier.doc.builders; const traverseDoc = prettier.doc.utils.traverseDoc; const findInDoc = prettier.doc.utils.findInDoc; const mapDoc = prettier.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 === htmljs_parser.TagType.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 exports.languages = languages; exports.options = options; exports.parsers = parsers; exports.printers = printers;