preserve empty lines between jinja elements
This commit is contained in:
parent
65dd4f1aea
commit
60a2cc487e
6 changed files with 141 additions and 53 deletions
|
@ -5,9 +5,9 @@ export const Placeholder = {
|
||||||
|
|
||||||
export interface Node {
|
export interface Node {
|
||||||
id: string;
|
id: string;
|
||||||
type: "root" | "expression" | "statement" | "block" | "ignore";
|
type: "root" | "expression" | "statement" | "block" | "comment" | "ignore";
|
||||||
content: string;
|
content: string;
|
||||||
ownLine: boolean;
|
preNewLines: number;
|
||||||
originalText: string;
|
originalText: string;
|
||||||
index: number;
|
index: number;
|
||||||
length: number;
|
length: number;
|
||||||
|
@ -31,10 +31,7 @@ export interface Block extends Node {
|
||||||
type: "block";
|
type: "block";
|
||||||
start: Statement;
|
start: Statement;
|
||||||
end: Statement;
|
end: Statement;
|
||||||
}
|
containsNewLines: boolean;
|
||||||
|
|
||||||
export interface IgnoreBlock extends Node {
|
|
||||||
type: "ignore";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nonClosingStatements = [
|
export const nonClosingStatements = [
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
const NOT_FOUND = -1;
|
const NOT_FOUND = -1;
|
||||||
|
|
||||||
const regex =
|
const regex =
|
||||||
/(?<pre>(?<newline>\n)?(\s*?))(?<node>{{(?<startDelimiterEx>[-+]?)\s*(?<expression>'([^']|\\')*'|"([^"]|\\")*"|[\S\s]*?)\s*(?<endDelimiterEx>[-+]?)}}|{%(?<startDelimiter>[-+]?)\s*(?<statement>(?<keyword>\w+)('([^']|\\')*'|"([^"]|\\")*"|[\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 -->))/;
|
/(?<node>{{(?<startDelimiterEx>[-+]?)\s*(?<expression>'([^']|\\')*'|"([^"]|\\")*"|[\S\s]*?)\s*(?<endDelimiterEx>[-+]?)}}|{%(?<startDelimiter>[-+]?)\s*(?<statement>(?<keyword>\w+)('([^']|\\')*'|"([^"]|\\")*"|[\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) => {
|
export const parse: Parser<Node>["parse"] = (text) => {
|
||||||
const statementStack: Statement[] = [];
|
const statementStack: Statement[] = [];
|
||||||
|
@ -21,7 +21,7 @@ export const parse: Parser<Node>["parse"] = (text) => {
|
||||||
id: "0",
|
id: "0",
|
||||||
type: "root" as const,
|
type: "root" as const,
|
||||||
content: text,
|
content: text,
|
||||||
ownLine: false,
|
preNewLines: 0,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
index: 0,
|
index: 0,
|
||||||
length: 0,
|
length: 0,
|
||||||
|
@ -44,9 +44,6 @@ export const parse: Parser<Node>["parse"] = (text) => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pre = match.groups.pre || "";
|
|
||||||
const newline = !!match.groups.newline;
|
|
||||||
|
|
||||||
const matchText = match.groups.node;
|
const matchText = match.groups.node;
|
||||||
const expression = match.groups.expression;
|
const expression = match.groups.expression;
|
||||||
const statement = match.groups.statement;
|
const statement = match.groups.statement;
|
||||||
|
@ -58,21 +55,28 @@ export const parse: Parser<Node>["parse"] = (text) => {
|
||||||
}
|
}
|
||||||
const placeholder = generatePlaceholder();
|
const placeholder = generatePlaceholder();
|
||||||
|
|
||||||
|
const emptyLinesBetween = text.slice(i, i + match.index).match(/^\s+$/) || [
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
const preNewLines = emptyLinesBetween.length
|
||||||
|
? emptyLinesBetween[0].split("\n").length - 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
const node = {
|
const node = {
|
||||||
id: placeholder,
|
id: placeholder,
|
||||||
ownLine: newline,
|
preNewLines,
|
||||||
originalText: matchText,
|
originalText: matchText,
|
||||||
index: match.index + i + pre.length,
|
index: match.index + i,
|
||||||
length: matchText.length,
|
length: matchText.length,
|
||||||
nodes: root.nodes,
|
nodes: root.nodes,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ignoreBlock || comment) {
|
if (comment || ignoreBlock) {
|
||||||
root.content = root.content.replace(matchText, placeholder);
|
root.content = root.content.replace(matchText, placeholder);
|
||||||
root.nodes[node.id] = {
|
root.nodes[node.id] = {
|
||||||
...node,
|
...node,
|
||||||
type: "ignore",
|
type: comment ? "comment" : "ignore",
|
||||||
content: ignoreBlock || comment,
|
content: comment || ignoreBlock,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +162,8 @@ export const parse: Parser<Node>["parse"] = (text) => {
|
||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
content: blockText.slice(start.length, blockText.length - end.length),
|
content: blockText.slice(start.length, blockText.length - end.length),
|
||||||
ownLine: originalText.search("\n") !== NOT_FOUND,
|
preNewLines: start.preNewLines,
|
||||||
|
containsNewLines: originalText.search("\n") !== NOT_FOUND,
|
||||||
originalText,
|
originalText,
|
||||||
index: start.index,
|
index: start.index,
|
||||||
length: end.index + end.length - start.index,
|
length: end.index + end.length - start.index,
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import { Printer } from "prettier";
|
import { AstPath, Printer } from "prettier";
|
||||||
import { builders, utils } from "prettier/doc";
|
import { builders, utils } from "prettier/doc";
|
||||||
import {
|
import { Placeholder, Node, Expression, Statement, Block } from "./jinja";
|
||||||
Placeholder,
|
|
||||||
Node,
|
|
||||||
Expression,
|
|
||||||
Statement,
|
|
||||||
Block,
|
|
||||||
IgnoreBlock,
|
|
||||||
} from "./jinja";
|
|
||||||
|
|
||||||
const NOT_FOUND = -1;
|
const NOT_FOUND = -1;
|
||||||
|
|
||||||
|
@ -24,8 +17,10 @@ export const print: Printer<Node>["print"] = (path) => {
|
||||||
return printExpression(node as Expression);
|
return printExpression(node as Expression);
|
||||||
case "statement":
|
case "statement":
|
||||||
return printStatement(node as Statement);
|
return printStatement(node as Statement);
|
||||||
|
case "comment":
|
||||||
|
return printCommentBlock(node);
|
||||||
case "ignore":
|
case "ignore":
|
||||||
return printIgnoreBlock(node as IgnoreBlock);
|
return printIgnoreBlock(node);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
@ -33,20 +28,24 @@ export const print: Printer<Node>["print"] = (path) => {
|
||||||
const printExpression = (node: Expression): builders.Doc => {
|
const printExpression = (node: Expression): builders.Doc => {
|
||||||
const multiline = node.content.includes("\n");
|
const multiline = node.content.includes("\n");
|
||||||
|
|
||||||
return builders.group(
|
const expression = builders.group(
|
||||||
builders.join(" ", [
|
builders.join(" ", [
|
||||||
["{{", node.delimiter],
|
["{{", node.delimiter],
|
||||||
multiline
|
multiline
|
||||||
? builders.indent([getMultilineGroup(node.content)])
|
? builders.indent(getMultilineGroup(node.content))
|
||||||
: node.content,
|
: node.content,
|
||||||
multiline
|
multiline
|
||||||
? [builders.hardline, node.delimiter, "}}"]
|
? [builders.hardline, node.delimiter, "}}"]
|
||||||
: [node.delimiter, "}}"],
|
: [node.delimiter, "}}"],
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
shouldBreak: node.ownLine,
|
shouldBreak: node.preNewLines > 0,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return node.preNewLines > 1
|
||||||
|
? builders.group([builders.hardline, builders.trim, expression])
|
||||||
|
: expression;
|
||||||
};
|
};
|
||||||
|
|
||||||
const printStatement = (node: Statement): builders.Doc => {
|
const printStatement = (node: Statement): builders.Doc => {
|
||||||
|
@ -62,20 +61,30 @@ const printStatement = (node: Statement): builders.Doc => {
|
||||||
? [builders.hardline, node.delimiter, "%}"]
|
? [builders.hardline, node.delimiter, "%}"]
|
||||||
: [node.delimiter, "%}"],
|
: [node.delimiter, "%}"],
|
||||||
]),
|
]),
|
||||||
{ shouldBreak: node.ownLine }
|
{ shouldBreak: node.preNewLines > 0 }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
["else", "elif"].includes(node.keyword) &&
|
["else", "elif"].includes(node.keyword) &&
|
||||||
surroundingBlock(node)?.ownLine
|
surroundingBlock(node)?.containsNewLines
|
||||||
) {
|
) {
|
||||||
return [builders.dedent(builders.hardline), statemnt, builders.hardline];
|
return [builders.dedent(builders.hardline), statemnt, builders.hardline];
|
||||||
}
|
}
|
||||||
return statemnt;
|
return statemnt;
|
||||||
};
|
};
|
||||||
|
|
||||||
const printIgnoreBlock = (node: IgnoreBlock): builders.Doc => {
|
const printCommentBlock = (node: Node): builders.Doc => {
|
||||||
return builders.group(node.content, { shouldBreak: node.ownLine });
|
const comment = builders.group(node.content, {
|
||||||
|
shouldBreak: node.preNewLines > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return node.preNewLines > 1
|
||||||
|
? builders.group([builders.hardline, builders.trim, comment])
|
||||||
|
: comment;
|
||||||
|
};
|
||||||
|
|
||||||
|
const printIgnoreBlock = (node: Node): builders.Doc => {
|
||||||
|
return node.content;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const embed: Printer<Node>["embed"] = (
|
export const embed: Printer<Node>["embed"] = (
|
||||||
|
@ -150,22 +159,11 @@ export const embed: Printer<Node>["embed"] = (
|
||||||
});
|
});
|
||||||
|
|
||||||
if (node.type === "block") {
|
if (node.type === "block") {
|
||||||
if (node.content.includes("\n")) {
|
const block = buildBlock(path, print, node as Block, mapped);
|
||||||
return builders.group([
|
|
||||||
path.call(print, "nodes", (node as Block).start.id),
|
return node.preNewLines > 1
|
||||||
builders.indent([
|
? builders.group([builders.hardline, builders.trim, block])
|
||||||
builders.softline,
|
: block;
|
||||||
utils.stripTrailingHardline(mapped),
|
|
||||||
]),
|
|
||||||
builders.hardline,
|
|
||||||
path.call(print, "nodes", (node as Block).end.id),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return builders.group([
|
|
||||||
path.call(print, "nodes", (node as Block).start.id),
|
|
||||||
utils.stripTrailingHardline(mapped),
|
|
||||||
path.call(print, "nodes", (node as Block).end.id),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
return [...mapped, builders.hardline];
|
return [...mapped, builders.hardline];
|
||||||
};
|
};
|
||||||
|
@ -223,3 +221,24 @@ export const surroundingBlock = (node: Node): Block | undefined => {
|
||||||
(n) => n.type === "block" && n.content.search(node.id) !== NOT_FOUND
|
(n) => n.type === "block" && n.content.search(node.id) !== NOT_FOUND
|
||||||
) as Block;
|
) as Block;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildBlock = (
|
||||||
|
path: AstPath<Node>,
|
||||||
|
print: (path: AstPath<Node>) => builders.Doc,
|
||||||
|
block: Block,
|
||||||
|
mapped: (string | builders.Doc[] | builders.DocCommand)[]
|
||||||
|
): builders.Doc => {
|
||||||
|
if (block.containsNewLines) {
|
||||||
|
return builders.group([
|
||||||
|
path.call(print, "nodes", block.start.id),
|
||||||
|
builders.indent([builders.softline, utils.stripTrailingHardline(mapped)]),
|
||||||
|
builders.hardline,
|
||||||
|
path.call(print, "nodes", block.end.id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return builders.group([
|
||||||
|
path.call(print, "nodes", block.start.id),
|
||||||
|
utils.stripTrailingHardline(mapped),
|
||||||
|
path.call(print, "nodes", block.end.id),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
<div>
|
<div>{{ user.name }}</div>
|
||||||
{{ user.name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>{{ user.age }}</div>
|
<div>{{ user.age }}</div>
|
||||||
|
|
30
test/cases/newline_between/expected.html
Normal file
30
test/cases/newline_between/expected.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Index{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<style type="text/css">
|
||||||
|
.important {
|
||||||
|
color: #336699;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{{ title }}
|
||||||
|
|
||||||
|
{# content #}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Index</h1>
|
||||||
|
<p class="important">Welcome to my awesome homepage.</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
{{ name }}
|
||||||
|
|
||||||
|
{% for user in users %}
|
||||||
|
<li>{{ user.username|e }}</li>
|
||||||
|
{% else %}
|
||||||
|
<li><em>no users found</em></li>
|
||||||
|
{% endfor %}
|
39
test/cases/newline_between/input.html
Normal file
39
test/cases/newline_between/input.html
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}Index{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<style type="text/css">
|
||||||
|
.important { color: #336699; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{{title}}
|
||||||
|
|
||||||
|
|
||||||
|
{# content #}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Index</h1>
|
||||||
|
<p class="important">
|
||||||
|
Welcome to my awesome homepage.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
{{name}}
|
||||||
|
|
||||||
|
|
||||||
|
{% for user in users %}
|
||||||
|
<li>{{ user.username|e }}</li>
|
||||||
|
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<li><em>no users found</em></li>
|
||||||
|
|
||||||
|
{% endfor %}
|
Loading…
Reference in a new issue