import { Printer } from "prettier"; import { builders, utils } from "prettier/doc"; import { Placeholder, Node, Expression, Statement, Block, IgnoreBlock, } from "./jinja"; const NOT_FOUND = -1; process.env.PRETTIER_DEBUG = "true"; export const print: Printer["print"] = (path) => { const node = path.getNode(); if (!node) { return []; } switch (node.type) { case "expression": return printExpression(node as Expression); case "statement": return printStatement(node as Statement); case "ignore": return printIgnoreBlock(node as IgnoreBlock); } return []; }; const printExpression = (node: Expression): builders.Doc => { return builders.group(["{{", " ", node.content, " ", "}}"], { shouldBreak: node.ownLine || node.content.includes("\n"), }); }; const printStatement = (node: Statement): builders.Doc => { const statemnt = builders.group( [ "{%", node.startDelimiter, " ", node.content, " ", node.endDelimiter, "%}", ], { shouldBreak: true } ); return node.keyword === "else" ? [builders.dedent(builders.hardline), statemnt, builders.hardline] : statemnt; }; const printIgnoreBlock = (node: IgnoreBlock): builders.Doc => { return builders.group(node.content, { shouldBreak: node.ownLine }); }; export const embed: Printer["embed"] = ( path, print, textToDoc, options ) => { const node = path.getNode(); if (!node || !["root", "block"].includes(node.type)) { return null; } const mapped = splitAtElse(node).map((content) => { let doc; if (content in node.nodes) { doc = content; } else { doc = utils.stripTrailingHardline( textToDoc(content, { ...options, parser: "html", }) ); } let ignoreDoc = false; return utils.mapDoc(doc, (currentDoc) => { if (typeof currentDoc !== "string") { return currentDoc; } if (currentDoc === "") { ignoreDoc = true; return currentDoc; } const idxs = findPlaceholders(currentDoc).filter( ([start, end]) => currentDoc.slice(start, end + 1) in node.nodes ); if (!idxs.length) { return currentDoc; } const res: builders.Doc = []; let lastEnd = 0; for (const [start, end] of idxs) { if (lastEnd < start) { res.push(currentDoc.slice(lastEnd, start)); } const p = currentDoc.slice(start, end + 1) as string; if (ignoreDoc) { res.push(node.nodes[p].originalText); } else { res.push(path.call(print, "nodes", p)); } lastEnd = end + 1; } if (lastEnd > 0 && currentDoc.length > lastEnd) { res.push(currentDoc.slice(lastEnd)); } ignoreDoc = false; return res; }); }); if (node.type === "block") { return builders.group([ path.call(print, "nodes", (node as Block).start.id), builders.indent([builders.softline, utils.stripTrailingHardline(mapped)]), builders.hardline, path.call(print, "nodes", (node as Block).end.id), ]); } return [...mapped, builders.hardline]; }; const splitAtElse = (node: Node): string[] => { const elseNodes = Object.values(node.nodes).filter( (n) => n.type === "statement" && (n as Statement).keyword === "else" && node.content.search(n.id) !== NOT_FOUND ); if (!elseNodes.length) { return [node.content]; } const re = new RegExp(`(${elseNodes.map((e) => e.id).join(")|(")})`); return node.content.split(re).filter(Boolean); }; /** * Returns the indexs of the first and the last character of any placeholder * occuring in a string. */ export const findPlaceholders = (text: string): [number, number][] => { const res = []; let i = 0; while (true) { const start = text.slice(i).search(Placeholder.startToken); if (start === NOT_FOUND) break; const end = text .slice(start + i + Placeholder.startToken.length) .search(Placeholder.endToken); if (end === NOT_FOUND) break; res.push([ start + i, end + start + i + Placeholder.startToken.length + 1, ] as [number, number]); i += start + Placeholder.startToken.length; } return res; };