prettier-plugin-askama-temp.../src/printer.ts
2022-11-18 20:05:06 +01:00

181 lines
3.9 KiB
TypeScript

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<Node>["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<Node>["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 === "<!-- prettier-ignore -->") {
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;
};