preserve empty lines between jinja elements

This commit is contained in:
davidodenwald 2023-05-07 14:30:29 +02:00
parent 65dd4f1aea
commit 60a2cc487e
6 changed files with 141 additions and 53 deletions

View file

@ -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 = [

View file

@ -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,

View file

@ -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),
]);
};

View file

@ -1,5 +1,3 @@
<div> <div>{{ user.name }}</div>
{{ user.name }}
</div>
<div>{{ user.age }}</div> <div>{{ user.age }}</div>

View 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 %}

View 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 %}