initial commit
This commit is contained in:
commit
417b3d96ec
51 changed files with 7285 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.vscode
|
||||||
|
node_modules
|
||||||
|
lib
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/test/cases
|
3
.prettierrc
Normal file
3
.prettierrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"useTabs": true
|
||||||
|
}
|
26
README.md
Normal file
26
README.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# prettier-plugin-jinja-template
|
||||||
|
|
||||||
|
Formatter plugin for jinja2 template files.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev prettier-plugin-jinja-template
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
To use it with basic .html files, you'll have to override the used parser inside your prettier config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.html"],
|
||||||
|
"options": {
|
||||||
|
"parser": "jinja-template"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
3
babel.config.js
Normal file
3
babel.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: ["@babel/preset-typescript"],
|
||||||
|
};
|
4
jest.config.js
Normal file
4
jest.config.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||||
|
module.exports = () => ({
|
||||||
|
preset: "ts-jest",
|
||||||
|
});
|
6347
package-lock.json
generated
Normal file
6347
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
41
package.json
Normal file
41
package.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "prettier-plugin-jinja-template",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Prettier plugin for formatting jinja templates.",
|
||||||
|
"author": "David Odenwald",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/davidodenwald/prettier-plugin-jinja-template.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/davidodenwald/prettier-plugin-jinja-template/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/davidodenwald/prettier-plugin-jinja-template#readme",
|
||||||
|
"keywords": [
|
||||||
|
"prettier",
|
||||||
|
"plugin",
|
||||||
|
"template",
|
||||||
|
"html",
|
||||||
|
"jinja",
|
||||||
|
"jinja2",
|
||||||
|
"flask"
|
||||||
|
],
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "npm run build -- --watch",
|
||||||
|
"test": "jest --verbose",
|
||||||
|
"test:watch": "jest --watch --verbose",
|
||||||
|
"publish": "npm run test && npm run build && npm publish",
|
||||||
|
"publish:beta": "npm run test && npm run build && npm publish --tag beta"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
|
"@types/jest": "^29.2.2",
|
||||||
|
"jest": "^29.3.1",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
|
"typescript": "^4.8.4"
|
||||||
|
}
|
||||||
|
}
|
31
src/index.ts
Normal file
31
src/index.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Node } from "./jinja";
|
||||||
|
import { parse } from "./parser";
|
||||||
|
import { print, embed } from "./printer";
|
||||||
|
import { Parser, Printer, SupportLanguage } from "prettier";
|
||||||
|
|
||||||
|
const PLUGIN_KEY = "jinja-template";
|
||||||
|
|
||||||
|
export const languages: SupportLanguage[] = [
|
||||||
|
{
|
||||||
|
name: "JinjaTemplate",
|
||||||
|
parsers: [PLUGIN_KEY],
|
||||||
|
extensions: [".jinja", ".jinja2", ".j2", ".html"],
|
||||||
|
vscodeLanguageIds: ["jinja"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const parsers = {
|
||||||
|
[PLUGIN_KEY]: <Parser<Node>>{
|
||||||
|
astFormat: PLUGIN_KEY,
|
||||||
|
parse,
|
||||||
|
locStart: (node) => node.index,
|
||||||
|
locEnd: (node) => node.index + node.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const printers = {
|
||||||
|
[PLUGIN_KEY]: <Printer<Node>>{
|
||||||
|
print,
|
||||||
|
embed,
|
||||||
|
},
|
||||||
|
};
|
68
src/jinja.ts
Normal file
68
src/jinja.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
export const Placeholder = {
|
||||||
|
startToken: "#~",
|
||||||
|
endToken: "~#",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
id: string;
|
||||||
|
type: "root" | "expression" | "statement" | "block" | "ignore";
|
||||||
|
content: string;
|
||||||
|
originalText: string;
|
||||||
|
index: number;
|
||||||
|
length: number;
|
||||||
|
nodes: { [id: string]: Node };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Expression extends Node {
|
||||||
|
type: "expression";
|
||||||
|
ownLine: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Delimiter = "" | "-" | "+";
|
||||||
|
|
||||||
|
export interface Statement extends Node {
|
||||||
|
type: "statement";
|
||||||
|
keyword: Keyword;
|
||||||
|
startDelimiter: Delimiter;
|
||||||
|
endDelimiter: Delimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Block extends Node {
|
||||||
|
type: "block";
|
||||||
|
start: Statement;
|
||||||
|
end: Statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IgnoreBlock extends Node {
|
||||||
|
type: "ignore";
|
||||||
|
ownLine: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Keyword =
|
||||||
|
| "for"
|
||||||
|
| "endfor"
|
||||||
|
| "if"
|
||||||
|
| "else"
|
||||||
|
| "endif"
|
||||||
|
| "macro"
|
||||||
|
| "endmacro"
|
||||||
|
| "call"
|
||||||
|
| "endcall"
|
||||||
|
| "filter"
|
||||||
|
| "endfilter"
|
||||||
|
| "set"
|
||||||
|
| "endset"
|
||||||
|
| "include"
|
||||||
|
| "import"
|
||||||
|
| "from"
|
||||||
|
| "extends"
|
||||||
|
| "block"
|
||||||
|
| "endblock";
|
||||||
|
|
||||||
|
export const nonClosingStatements = [
|
||||||
|
"else",
|
||||||
|
"include",
|
||||||
|
"import",
|
||||||
|
"from",
|
||||||
|
"extends",
|
||||||
|
];
|
222
src/parser.ts
Normal file
222
src/parser.ts
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
import { Parser } from "prettier";
|
||||||
|
import {
|
||||||
|
Delimiter,
|
||||||
|
Keyword,
|
||||||
|
Node,
|
||||||
|
Placeholder,
|
||||||
|
Statement,
|
||||||
|
Block,
|
||||||
|
nonClosingStatements,
|
||||||
|
Expression,
|
||||||
|
IgnoreBlock,
|
||||||
|
} from "./jinja";
|
||||||
|
|
||||||
|
const regex =
|
||||||
|
/(?<pre>(?<newline>\n)?(\s*?))(?<node>{{\s*(?<expression>'([^']|\\')*'|"([^"]|\\")*"|[\S\s]*?)\s*}}|{%(?<startDelimiter>[-\+]?)\s*(?<statement>(?<keyword>for|endfor|if|else|endif|macro|endmacro|call|endcall|filter|endfilter|set|endset|include|import|from|extends|block|endblock)('([^']|\\')*'|"([^"]|\\")*"|[\S\s])*?)\s*(?<endDelimiter>[-\+]?)%}|(?<comment>{#[\S\s]*?#})|(?<scriptBlock><(script)((?!<)[\s\S])*>((?!<\/script)[\s\S])*?{{[\s\S]*?<\/(script)>)|(?<styleBlock><(style)((?!<)[\s\S])*>((?!<\/style)[\s\S])*?{{[\s\S]*?<\/(style)>)|(?<ignoreBlock><!-- prettier-ignore-start -->[\s\S]*<!-- prettier-ignore-end -->))/;
|
||||||
|
|
||||||
|
export const parse: Parser<Node>["parse"] = (text) => {
|
||||||
|
const statementStack: Statement[] = [];
|
||||||
|
|
||||||
|
const root: Node = {
|
||||||
|
id: "0",
|
||||||
|
type: "root" as const,
|
||||||
|
content: text,
|
||||||
|
originalText: text,
|
||||||
|
index: 0,
|
||||||
|
length: 0,
|
||||||
|
nodes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePlaceholder = placeholderGenerator(text);
|
||||||
|
|
||||||
|
let match;
|
||||||
|
let i = 0;
|
||||||
|
while ((match = root.content.slice(i).match(regex)) !== null) {
|
||||||
|
if (!match.groups || match.index === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (match.groups.scriptBlock || match.groups.styleBlock) {
|
||||||
|
i += match.index + match[0].length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pre = match.groups.pre || "";
|
||||||
|
const newline = !!match.groups.newline;
|
||||||
|
|
||||||
|
const node = match.groups.node;
|
||||||
|
const expression = match.groups.expression;
|
||||||
|
const statement = match.groups.statement;
|
||||||
|
const ignoreBlock = match.groups.ignoreBlock;
|
||||||
|
const comment = match.groups.comment;
|
||||||
|
|
||||||
|
if (!node && !expression && !statement && !ignoreBlock && !comment) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const matchText = node;
|
||||||
|
|
||||||
|
if (ignoreBlock || comment) {
|
||||||
|
const placeholder = generatePlaceholder();
|
||||||
|
root.content = root.content.replace(matchText, placeholder);
|
||||||
|
|
||||||
|
root.nodes[placeholder] = {
|
||||||
|
id: placeholder,
|
||||||
|
type: "ignore",
|
||||||
|
content: ignoreBlock || comment,
|
||||||
|
ownLine: newline,
|
||||||
|
originalText: matchText,
|
||||||
|
index: match.index + i + pre.length,
|
||||||
|
length: matchText.length,
|
||||||
|
nodes: root.nodes,
|
||||||
|
} as IgnoreBlock;
|
||||||
|
i += match.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expression) {
|
||||||
|
const placeholder = generatePlaceholder();
|
||||||
|
root.content = root.content.replace(matchText, placeholder);
|
||||||
|
|
||||||
|
root.nodes[placeholder] = {
|
||||||
|
id: placeholder,
|
||||||
|
type: "expression",
|
||||||
|
content: expression,
|
||||||
|
ownLine: newline,
|
||||||
|
originalText: matchText,
|
||||||
|
index: match.index + i + pre.length,
|
||||||
|
length: matchText.length,
|
||||||
|
nodes: root.nodes,
|
||||||
|
} as Expression;
|
||||||
|
|
||||||
|
i += match.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statement) {
|
||||||
|
const keyword = match.groups.keyword as Keyword;
|
||||||
|
const startDelimiter = match.groups.startDelimiter as Delimiter;
|
||||||
|
const endDelimiter = match.groups.endDelimiter as Delimiter;
|
||||||
|
|
||||||
|
if (nonClosingStatements.includes(keyword)) {
|
||||||
|
const placeholder = generatePlaceholder();
|
||||||
|
root.content = root.content.replace(matchText, placeholder);
|
||||||
|
root.nodes[placeholder] = {
|
||||||
|
id: placeholder,
|
||||||
|
type: "statement",
|
||||||
|
content: statement,
|
||||||
|
originalText: matchText,
|
||||||
|
index: match.index + i + pre.length,
|
||||||
|
length: matchText.length,
|
||||||
|
keyword,
|
||||||
|
startDelimiter,
|
||||||
|
endDelimiter,
|
||||||
|
nodes: root.nodes,
|
||||||
|
} as Statement;
|
||||||
|
|
||||||
|
i += match.index;
|
||||||
|
} else if (!keyword.startsWith("end")) {
|
||||||
|
statementStack.push({
|
||||||
|
id: generatePlaceholder(),
|
||||||
|
type: "statement" as const,
|
||||||
|
content: statement,
|
||||||
|
originalText: matchText,
|
||||||
|
index: match.index + i + pre.length,
|
||||||
|
length: matchText.length,
|
||||||
|
keyword,
|
||||||
|
startDelimiter,
|
||||||
|
endDelimiter,
|
||||||
|
nodes: root.nodes,
|
||||||
|
});
|
||||||
|
|
||||||
|
i += match.index + matchText.length;
|
||||||
|
} else {
|
||||||
|
let start: Statement | undefined;
|
||||||
|
while (!start) {
|
||||||
|
start = statementStack.pop();
|
||||||
|
|
||||||
|
if (!start) {
|
||||||
|
throw new Error(
|
||||||
|
`No opening statement found for closing statement "${statement}".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startKeyword = keyword.replace("end", "");
|
||||||
|
if (startKeyword !== start.keyword) {
|
||||||
|
if (start.keyword === "set") {
|
||||||
|
start = undefined;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Closung statement "${statement}" doesn't match Opening Statement "${start.content}".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = {
|
||||||
|
id: generatePlaceholder(),
|
||||||
|
type: "statement" as const,
|
||||||
|
content: statement,
|
||||||
|
originalText: matchText,
|
||||||
|
index: match.index + i + pre.length,
|
||||||
|
length: matchText.length,
|
||||||
|
keyword,
|
||||||
|
startDelimiter,
|
||||||
|
endDelimiter,
|
||||||
|
nodes: root.nodes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const placeholder = generatePlaceholder();
|
||||||
|
const content = root.content.slice(
|
||||||
|
start.index + start.length,
|
||||||
|
end.index
|
||||||
|
);
|
||||||
|
|
||||||
|
root.nodes[placeholder] = {
|
||||||
|
id: placeholder,
|
||||||
|
type: "block",
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
content,
|
||||||
|
originalText: matchText,
|
||||||
|
index: start.index,
|
||||||
|
length: end.index + end.length - start.index,
|
||||||
|
nodes: root.nodes,
|
||||||
|
} as Block;
|
||||||
|
|
||||||
|
root.nodes[start.id] = start;
|
||||||
|
root.nodes[end.id] = end;
|
||||||
|
|
||||||
|
root.content =
|
||||||
|
root.content.slice(0, start.index) +
|
||||||
|
placeholder +
|
||||||
|
root.content.slice(end.index + end.length, root.content.length);
|
||||||
|
|
||||||
|
i = start.index + placeholder.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingStatement = statementStack.find(
|
||||||
|
(stmt) => stmt.keyword !== "set"
|
||||||
|
);
|
||||||
|
if (remainingStatement) {
|
||||||
|
throw new Error(
|
||||||
|
`No closing statement found for opening statement "${remainingStatement.content}".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
|
||||||
|
const placeholderGenerator = (text: string) => {
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
return (): string => {
|
||||||
|
while (true) {
|
||||||
|
id++;
|
||||||
|
|
||||||
|
const placeholder = Placeholder.startToken + id + Placeholder.endToken;
|
||||||
|
if (!text.includes(placeholder)) {
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
181
src/printer.ts
Normal file
181
src/printer.ts
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
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;
|
||||||
|
};
|
1
test/cases/collition/expected.html
Normal file
1
test/cases/collition/expected.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#~1~# {{ i }}
|
1
test/cases/collition/input.html
Normal file
1
test/cases/collition/input.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#~1~# {{i}}
|
14
test/cases/comment/expected.html
Normal file
14
test/cases/comment/expected.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{#
|
||||||
|
Language will be used as ab_variant shortcut
|
||||||
|
tracking callback is our own patched callback
|
||||||
|
|
||||||
|
Integration note:
|
||||||
|
|
||||||
|
window.emos3 = {
|
||||||
|
stored: [],
|
||||||
|
send: function(p){this.stored.push(p);}
|
||||||
|
};
|
||||||
|
|
||||||
|
This script tag must be moved to the head tag so
|
||||||
|
that onclick events can already use the send method.
|
||||||
|
#}
|
14
test/cases/comment/input.html
Normal file
14
test/cases/comment/input.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{#
|
||||||
|
Language will be used as ab_variant shortcut
|
||||||
|
tracking callback is our own patched callback
|
||||||
|
|
||||||
|
Integration note:
|
||||||
|
|
||||||
|
window.emos3 = {
|
||||||
|
stored: [],
|
||||||
|
send: function(p){this.stored.push(p);}
|
||||||
|
};
|
||||||
|
|
||||||
|
This script tag must be moved to the head tag so
|
||||||
|
that onclick events can already use the send method.
|
||||||
|
#}
|
5
test/cases/expression/expected.html
Normal file
5
test/cases/expression/expected.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<div>
|
||||||
|
{{ user.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>{{ user.age }}</div>
|
5
test/cases/expression/input.html
Normal file
5
test/cases/expression/input.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<div>
|
||||||
|
{{user.name}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>{{user.age}}</div>
|
1
test/cases/expression_2/expected.html
Normal file
1
test/cases/expression_2/expected.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div>~{{ user.id }}~{{ user.name }}~</div>
|
3
test/cases/expression_2/input.html
Normal file
3
test/cases/expression_2/input.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
~{{user.id}}~{{user.name}}~
|
||||||
|
</div>
|
1
test/cases/expression_as_attr/expected.html
Normal file
1
test/cases/expression_as_attr/expected.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div id="test" {{ attr }}></div>
|
1
test/cases/expression_as_attr/input.html
Normal file
1
test/cases/expression_as_attr/input.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div id="test" {{attr}}></div>
|
2
test/cases/expression_escaped/expected.html
Normal file
2
test/cases/expression_escaped/expected.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{{ '{{' }}
|
||||||
|
{{ '}}' }}
|
2
test/cases/expression_escaped/input.html
Normal file
2
test/cases/expression_escaped/input.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{{'{{'}}
|
||||||
|
{{'}}'}}
|
3
test/cases/expression_long/expected.html
Normal file
3
test/cases/expression_long/expected.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
{{ really really looooooooooooooooooooooooooooooooooooooooong expression that should get a own line }}
|
||||||
|
</div>
|
1
test/cases/expression_long/input.html
Normal file
1
test/cases/expression_long/input.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div>{{really really looooooooooooooooooooooooooooooooooooooooong expression that should get a own line}}</div>
|
1
test/cases/expression_multi/expected.html
Normal file
1
test/cases/expression_multi/expected.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div>{{ user.id }}{{ user.name }}</div>
|
1
test/cases/expression_multi/input.html
Normal file
1
test/cases/expression_multi/input.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div>{{ user.id }}{{user.name}}</div>
|
7
test/cases/expression_multiline/expected.html
Normal file
7
test/cases/expression_multiline/expected.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<div>
|
||||||
|
{{ {
|
||||||
|
'dict': 'of',
|
||||||
|
'key': 'and',
|
||||||
|
'value': 'pairs'
|
||||||
|
} }}
|
||||||
|
</div>
|
9
test/cases/expression_multiline/input.html
Normal file
9
test/cases/expression_multiline/input.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
{
|
||||||
|
'dict': 'of',
|
||||||
|
'key': 'and',
|
||||||
|
'value': 'pairs'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</div>
|
34
test/cases/ignore/expected.html
Normal file
34
test/cases/ignore/expected.html
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<html>
|
||||||
|
<script>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
js;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* {{ css }} */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<ul>
|
||||||
|
{%for item in seq%}
|
||||||
|
<li>
|
||||||
|
{{item}}
|
||||||
|
</li>
|
||||||
|
{%endfor%}
|
||||||
|
</ul>
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<div class="{{class}}" >hello world</div >
|
||||||
|
<div class="{{ class }}">hello world</div>
|
||||||
|
|
||||||
|
{% if foo %}
|
||||||
|
<p>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
{{item}}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</html>
|
30
test/cases/ignore/input.html
Normal file
30
test/cases/ignore/input.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<html>
|
||||||
|
<script>
|
||||||
|
{{js}}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* {{ css }} */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<ul>
|
||||||
|
{%for item in seq%}
|
||||||
|
<li>
|
||||||
|
{{item}}
|
||||||
|
</li>
|
||||||
|
{%endfor%}
|
||||||
|
</ul>
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<div class="{{class}}" >hello world</div >
|
||||||
|
<div class="{{class}}" >hello world</div >
|
||||||
|
|
||||||
|
{%if foo%}
|
||||||
|
<p>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
{{item}}
|
||||||
|
</p>
|
||||||
|
{%endif%}
|
||||||
|
</html>
|
5
test/cases/statement/expected.html
Normal file
5
test/cases/statement/expected.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<ul>
|
||||||
|
{% for item in seq %}
|
||||||
|
<li>{{ item }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
5
test/cases/statement/input.html
Normal file
5
test/cases/statement/input.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<ul>
|
||||||
|
{% for item in seq %}
|
||||||
|
<li> {{ item }} </li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
1
test/cases/statement_broken/expected.html
Normal file
1
test/cases/statement_broken/expected.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Error('Error: No opening statement found for closing statement "endif"')
|
8
test/cases/statement_broken/input.html
Normal file
8
test/cases/statement_broken/input.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<body>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
{%else if href == "random.html" %}
|
||||||
|
<h2>{{title}}</h2>
|
||||||
|
{%else%}
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
1
test/cases/statement_broken_2/expected.html
Normal file
1
test/cases/statement_broken_2/expected.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Error('Error: No closing statement found for opening statement "if title".')
|
4
test/cases/statement_broken_2/input.html
Normal file
4
test/cases/statement_broken_2/input.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<body>
|
||||||
|
{% if title %}
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
</body>
|
9
test/cases/statement_if_else/expected.html
Normal file
9
test/cases/statement_if_else/expected.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<body>
|
||||||
|
{% if href in ['layout.html', 'index.html, 'about.html', 'user.html'] %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
{% else if href == "random.html" %}
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
{% else %}
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
9
test/cases/statement_if_else/input.html
Normal file
9
test/cases/statement_if_else/input.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<body>
|
||||||
|
{% if href in ['layout.html', 'index.html, 'about.html', 'user.html'] %}
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
{%else if href == "random.html" %}
|
||||||
|
<h2>{{title}}</h2>
|
||||||
|
{%else%}
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
19
test/cases/statement_multiple/expected.html
Normal file
19
test/cases/statement_multiple/expected.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<div>
|
||||||
|
{% for row in database %}
|
||||||
|
<ul>
|
||||||
|
{% for element in row %}
|
||||||
|
<li>
|
||||||
|
{% if element.active %}
|
||||||
|
{{ element.description }}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
{% for row in res %}
|
||||||
|
<p>{{ row[0] }}</p>
|
||||||
|
<p>{{ row[1] }}</p>
|
||||||
|
<p>{{ row[2] }}</p>
|
||||||
|
<p>{{ row[3] }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
14
test/cases/statement_multiple/input.html
Normal file
14
test/cases/statement_multiple/input.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<div>
|
||||||
|
{% for row in database %}
|
||||||
|
<ul>
|
||||||
|
{% for element in row %}
|
||||||
|
<li>
|
||||||
|
{%if element.active%}
|
||||||
|
{{ element.description }}
|
||||||
|
{% endif%}
|
||||||
|
</li>
|
||||||
|
{% endfor%}</ul>{%endfor %}
|
||||||
|
{% for row in res %}
|
||||||
|
<p>{{row[0]}}</p><p>{{row[1]}}</p><p>{{row[2]}}</p><p>{{row[3]}}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
7
test/cases/statement_non_closing/expected.html
Normal file
7
test/cases/statement_non_closing/expected.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<html>
|
||||||
|
{% for item in seq %}
|
||||||
|
{% include 'header.html' %}
|
||||||
|
{% include 'body.html' %}
|
||||||
|
{% include 'footer.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
</html>
|
7
test/cases/statement_non_closing/input.html
Normal file
7
test/cases/statement_non_closing/input.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<html>
|
||||||
|
{% for item in seq %}
|
||||||
|
{% include 'header.html' %}
|
||||||
|
{% include 'body.html' %}
|
||||||
|
{% include 'footer.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
</html>
|
14
test/cases/statement_set/expected.html
Normal file
14
test/cases/statement_set/expected.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<head>
|
||||||
|
{% for item in seq %}
|
||||||
|
<!-- meta for {{ item }} -->
|
||||||
|
{% set key, value = call(item) %}
|
||||||
|
<meta name="{{ key }}" content="{{ value }}" />
|
||||||
|
{% endfor %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %}
|
||||||
|
{% set navigation %}
|
||||||
|
<li><a href="/">Index</a></li>
|
||||||
|
<li><a href="/downloads">Downloads</a></li>
|
||||||
|
{% endset %}
|
||||||
|
</body>
|
14
test/cases/statement_set/input.html
Normal file
14
test/cases/statement_set/input.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<head>
|
||||||
|
{% for item in seq %}
|
||||||
|
<!-- meta for {{item}} -->
|
||||||
|
{% set key, value = call(item) %}
|
||||||
|
<meta name="{{key}}" content="{{value}}">
|
||||||
|
{% endfor %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %}
|
||||||
|
{% set navigation %}
|
||||||
|
<li><a href="/">Index</a></li>
|
||||||
|
<li><a href="/downloads">Downloads</a></li>
|
||||||
|
{% endset %}
|
||||||
|
</body>
|
5
test/cases/statement_whitespace/expected.html
Normal file
5
test/cases/statement_whitespace/expected.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<ul>
|
||||||
|
{%- for item in seq -%}
|
||||||
|
<li>{{ item }}</li>
|
||||||
|
{%- endfor %}
|
||||||
|
</ul>
|
5
test/cases/statement_whitespace/input.html
Normal file
5
test/cases/statement_whitespace/input.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<ul>
|
||||||
|
{%-for item in seq -%}
|
||||||
|
<li>{{ item}}</li>
|
||||||
|
{%- endfor%}
|
||||||
|
</ul>
|
44
test/plugin.test.ts
Normal file
44
test/plugin.test.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { format, Options } from "prettier";
|
||||||
|
import * as jinjaPlugin from "../src/index";
|
||||||
|
|
||||||
|
const prettify = (code: string, options: Options) =>
|
||||||
|
format(code, {
|
||||||
|
parser: "jinja-template",
|
||||||
|
plugins: [jinjaPlugin],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFolder = join(__dirname, "cases");
|
||||||
|
const tests = readdirSync(testFolder);
|
||||||
|
|
||||||
|
tests.forEach((test) => {
|
||||||
|
if (test.startsWith("_")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return it(test, () => {
|
||||||
|
const path = join(testFolder, test);
|
||||||
|
const input = readFileSync(join(path, "input.html")).toString();
|
||||||
|
const expected = readFileSync(join(path, "expected.html")).toString();
|
||||||
|
|
||||||
|
const configPath = join(path, "config.json");
|
||||||
|
const configString =
|
||||||
|
existsSync(configPath) && readFileSync(configPath)?.toString();
|
||||||
|
const configObject = configString ? JSON.parse(configString) : {};
|
||||||
|
|
||||||
|
const format = () => prettify(input, configObject);
|
||||||
|
|
||||||
|
const expectedError = expected.match(/Error\(["'`](?<message>.*)["'`]\)/)
|
||||||
|
?.groups?.message;
|
||||||
|
|
||||||
|
if (expectedError) {
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
expect(format).toThrow(expectedError);
|
||||||
|
} else {
|
||||||
|
const result = format();
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(prettify(result, configObject)).toEqual(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
34
test/printer.test.ts
Normal file
34
test/printer.test.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { findPlaceholders } from "../src/printer";
|
||||||
|
|
||||||
|
test("findPlaceholders should find placeholder", () => {
|
||||||
|
expect(findPlaceholders("#~1~#")).toEqual([[0, 4]]);
|
||||||
|
expect(findPlaceholders("XX#~1~#XX")).toEqual([[2, 6]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findPlaceholders should find multiple placeholders", () => {
|
||||||
|
expect(findPlaceholders("#~1~##~99~#")).toEqual([
|
||||||
|
[0, 4],
|
||||||
|
[5, 10],
|
||||||
|
]);
|
||||||
|
expect(findPlaceholders("#~1~#X#~99~#")).toEqual([
|
||||||
|
[0, 4],
|
||||||
|
[6, 11],
|
||||||
|
]);
|
||||||
|
expect(findPlaceholders("#~1~##~X#~99~#")).toEqual([
|
||||||
|
[0, 4],
|
||||||
|
[5, 13],
|
||||||
|
[8, 13],
|
||||||
|
]);
|
||||||
|
expect(findPlaceholders("#~1~##~99~#~#")).toEqual([
|
||||||
|
[0, 4],
|
||||||
|
[5, 10],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findPlaceholders should find no placeholders", () => {
|
||||||
|
expect(findPlaceholders("")).toEqual([]);
|
||||||
|
expect(findPlaceholders("#~#")).toEqual([]);
|
||||||
|
expect(findPlaceholders("#~ #")).toEqual([]);
|
||||||
|
expect(findPlaceholders("# ~#")).toEqual([]);
|
||||||
|
expect(findPlaceholders("#~#~")).toEqual([]);
|
||||||
|
});
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./lib",
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["esnext"],
|
||||||
|
"declaration": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "test"]
|
||||||
|
}
|
Loading…
Reference in a new issue