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 {
|
||||
id: string;
|
||||
type: "root" | "expression" | "statement" | "block" | "ignore";
|
||||
type: "root" | "expression" | "statement" | "block" | "comment" | "ignore";
|
||||
content: string;
|
||||
ownLine: boolean;
|
||||
preNewLines: number;
|
||||
originalText: string;
|
||||
index: number;
|
||||
length: number;
|
||||
|
@ -31,10 +31,7 @@ export interface Block extends Node {
|
|||
type: "block";
|
||||
start: Statement;
|
||||
end: Statement;
|
||||
}
|
||||
|
||||
export interface IgnoreBlock extends Node {
|
||||
type: "ignore";
|
||||
containsNewLines: boolean;
|
||||
}
|
||||
|
||||
export const nonClosingStatements = [
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
const NOT_FOUND = -1;
|
||||
|
||||
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) => {
|
||||
const statementStack: Statement[] = [];
|
||||
|
@ -21,7 +21,7 @@ export const parse: Parser<Node>["parse"] = (text) => {
|
|||
id: "0",
|
||||
type: "root" as const,
|
||||
content: text,
|
||||
ownLine: false,
|
||||
preNewLines: 0,
|
||||
originalText: text,
|
||||
index: 0,
|
||||
length: 0,
|
||||
|
@ -44,9 +44,6 @@ export const parse: Parser<Node>["parse"] = (text) => {
|
|||
continue;
|
||||
}
|
||||
|
||||
const pre = match.groups.pre || "";
|
||||
const newline = !!match.groups.newline;
|
||||
|
||||
const matchText = match.groups.node;
|
||||
const expression = match.groups.expression;
|
||||
const statement = match.groups.statement;
|
||||
|
@ -58,21 +55,28 @@ export const parse: Parser<Node>["parse"] = (text) => {
|
|||
}
|
||||
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 = {
|
||||
id: placeholder,
|
||||
ownLine: newline,
|
||||
preNewLines,
|
||||
originalText: matchText,
|
||||
index: match.index + i + pre.length,
|
||||
index: match.index + i,
|
||||
length: matchText.length,
|
||||
nodes: root.nodes,
|
||||
};
|
||||
|
||||
if (ignoreBlock || comment) {
|
||||
if (comment || ignoreBlock) {
|
||||
root.content = root.content.replace(matchText, placeholder);
|
||||
root.nodes[node.id] = {
|
||||
...node,
|
||||
type: "ignore",
|
||||
content: ignoreBlock || comment,
|
||||
type: comment ? "comment" : "ignore",
|
||||
content: comment || ignoreBlock,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -158,7 +162,8 @@ export const parse: Parser<Node>["parse"] = (text) => {
|
|||
start: start,
|
||||
end: end,
|
||||
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,
|
||||
index: 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 {
|
||||
Placeholder,
|
||||
Node,
|
||||
Expression,
|
||||
Statement,
|
||||
Block,
|
||||
IgnoreBlock,
|
||||
} from "./jinja";
|
||||
import { Placeholder, Node, Expression, Statement, Block } from "./jinja";
|
||||
|
||||
const NOT_FOUND = -1;
|
||||
|
||||
|
@ -24,8 +17,10 @@ export const print: Printer<Node>["print"] = (path) => {
|
|||
return printExpression(node as Expression);
|
||||
case "statement":
|
||||
return printStatement(node as Statement);
|
||||
case "comment":
|
||||
return printCommentBlock(node);
|
||||
case "ignore":
|
||||
return printIgnoreBlock(node as IgnoreBlock);
|
||||
return printIgnoreBlock(node);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
@ -33,20 +28,24 @@ export const print: Printer<Node>["print"] = (path) => {
|
|||
const printExpression = (node: Expression): builders.Doc => {
|
||||
const multiline = node.content.includes("\n");
|
||||
|
||||
return builders.group(
|
||||
const expression = builders.group(
|
||||
builders.join(" ", [
|
||||
["{{", node.delimiter],
|
||||
multiline
|
||||
? builders.indent([getMultilineGroup(node.content)])
|
||||
? builders.indent(getMultilineGroup(node.content))
|
||||
: node.content,
|
||||
multiline
|
||||
? [builders.hardline, 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 => {
|
||||
|
@ -62,20 +61,30 @@ const printStatement = (node: Statement): builders.Doc => {
|
|||
? [builders.hardline, node.delimiter, "%}"]
|
||||
: [node.delimiter, "%}"],
|
||||
]),
|
||||
{ shouldBreak: node.ownLine }
|
||||
{ shouldBreak: node.preNewLines > 0 }
|
||||
);
|
||||
|
||||
if (
|
||||
["else", "elif"].includes(node.keyword) &&
|
||||
surroundingBlock(node)?.ownLine
|
||||
surroundingBlock(node)?.containsNewLines
|
||||
) {
|
||||
return [builders.dedent(builders.hardline), statemnt, builders.hardline];
|
||||
}
|
||||
return statemnt;
|
||||
};
|
||||
|
||||
const printIgnoreBlock = (node: IgnoreBlock): builders.Doc => {
|
||||
return builders.group(node.content, { shouldBreak: node.ownLine });
|
||||
const printCommentBlock = (node: Node): builders.Doc => {
|
||||
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"] = (
|
||||
|
@ -150,22 +159,11 @@ export const embed: Printer<Node>["embed"] = (
|
|||
});
|
||||
|
||||
if (node.type === "block") {
|
||||
if (node.content.includes("\n")) {
|
||||
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 builders.group([
|
||||
path.call(print, "nodes", (node as Block).start.id),
|
||||
utils.stripTrailingHardline(mapped),
|
||||
path.call(print, "nodes", (node as Block).end.id),
|
||||
]);
|
||||
const block = buildBlock(path, print, node as Block, mapped);
|
||||
|
||||
return node.preNewLines > 1
|
||||
? builders.group([builders.hardline, builders.trim, block])
|
||||
: block;
|
||||
}
|
||||
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
|
||||
) 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>
|
||||
{{ user.name }}
|
||||
</div>
|
||||
<div>{{ user.name }}</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