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