From 60a2cc487efdde5291a1acdc68ffec6332d84cc7 Mon Sep 17 00:00:00 2001 From: davidodenwald Date: Sun, 7 May 2023 14:30:29 +0200 Subject: [PATCH] preserve empty lines between jinja elements --- src/jinja.ts | 9 +-- src/parser.ts | 27 +++++--- src/printer.ts | 85 +++++++++++++++--------- test/cases/expression/expected.html | 4 +- test/cases/newline_between/expected.html | 30 +++++++++ test/cases/newline_between/input.html | 39 +++++++++++ 6 files changed, 141 insertions(+), 53 deletions(-) create mode 100644 test/cases/newline_between/expected.html create mode 100644 test/cases/newline_between/input.html diff --git a/src/jinja.ts b/src/jinja.ts index 2cbf7f5..e3ef42a 100644 --- a/src/jinja.ts +++ b/src/jinja.ts @@ -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 = [ diff --git a/src/parser.ts b/src/parser.ts index 2d20b42..eb40075 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -12,7 +12,7 @@ import { const NOT_FOUND = -1; const regex = - /(?
(?\n)?(\s*?))(?{{(?[-+]?)\s*(?'([^']|\\')*'|"([^"]|\\")*"|[\S\s]*?)\s*(?[-+]?)}}|{%(?[-+]?)\s*(?(?\w+)('([^']|\\')*'|"([^"]|\\")*"|[\S\s])*?)\s*(?[-+]?)%}|(?{#[\S\s]*?#})|(?<(script)((?!<)[\s\S])*>((?!<\/script)[\s\S])*?{{[\s\S]*?<\/(script)>)|(?<(style)((?!<)[\s\S])*>((?!<\/style)[\s\S])*?{{[\s\S]*?<\/(style)>)|(?[\s\S]*))/;
+	/(?{{(?[-+]?)\s*(?'([^']|\\')*'|"([^"]|\\")*"|[\S\s]*?)\s*(?[-+]?)}}|{%(?[-+]?)\s*(?(?\w+)('([^']|\\')*'|"([^"]|\\")*"|[\S\s])*?)\s*(?[-+]?)%}|(?{#[\S\s]*?#})|(?<(script)((?!<)[\s\S])*>((?!<\/script)[\s\S])*?{{[\s\S]*?<\/(script)>)|(?<(style)((?!<)[\s\S])*>((?!<\/style)[\s\S])*?{{[\s\S]*?<\/(style)>)|(?[\s\S]*))/;
 
 export const parse: Parser["parse"] = (text) => {
 	const statementStack: Statement[] = [];
@@ -21,7 +21,7 @@ export const parse: Parser["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["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["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["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,
diff --git a/src/printer.ts b/src/printer.ts
index 20f7fd4..d13b3fe 100644
--- a/src/printer.ts
+++ b/src/printer.ts
@@ -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["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["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["embed"] = (
@@ -150,22 +159,11 @@ export const embed: Printer["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,
+	print: (path: AstPath) => 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),
+	]);
+};
diff --git a/test/cases/expression/expected.html b/test/cases/expression/expected.html
index d34eb4c..b73aa96 100644
--- a/test/cases/expression/expected.html
+++ b/test/cases/expression/expected.html
@@ -1,5 +1,3 @@
-
- {{ user.name }} -
+
{{ user.name }}
{{ user.age }}
diff --git a/test/cases/newline_between/expected.html b/test/cases/newline_between/expected.html new file mode 100644 index 0000000..ff17c82 --- /dev/null +++ b/test/cases/newline_between/expected.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block title %}Index{% endblock %} + +{% block head %} + {{ super() }} + +{% endblock %} + +{{ title }} + +{# content #} +{% block content %} +

Index

+

Welcome to my awesome homepage.

+{% endblock %} + +
+ +{{ name }} + +{% for user in users %} +
  • {{ user.username|e }}
  • +{% else %} +
  • no users found
  • +{% endfor %} diff --git a/test/cases/newline_between/input.html b/test/cases/newline_between/input.html new file mode 100644 index 0000000..2704213 --- /dev/null +++ b/test/cases/newline_between/input.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + + + {% block title %}Index{% endblock %} + + +{% block head %} + {{ super() }} + +{% endblock %} + + +{{title}} + + +{# content #} +{% block content %} +

    Index

    +

    + Welcome to my awesome homepage. +

    + +{% endblock %} + +
    + +{{name}} + + +{% for user in users %} +
  • {{ user.username|e }}
  • + + + {% else %} +
  • no users found
  • + + {% endfor %}