From c8cbbb0a79642c03605f95481ec32e1ec0b1fa27 Mon Sep 17 00:00:00 2001 From: Cyrille David Date: Mon, 18 Jan 2021 14:49:39 +0100 Subject: [PATCH] handlebars: Support `htmlWhitespaceSensitivity=strict` option (2/3) (#9885) * hbs: Add a strict mode * Fix lint (after concat removal) * Another lint error --- src/language-handlebars/printer-glimmer.js | 170 +++++++-- src/language-handlebars/utils.js | 22 ++ .../__snapshots__/jsfmt.spec.js.snap | 326 ++++++++++++------ .../html-whitespace-sensitivity/jsfmt.spec.js | 85 ++++- 4 files changed, 466 insertions(+), 137 deletions(-) diff --git a/src/language-handlebars/printer-glimmer.js b/src/language-handlebars/printer-glimmer.js index c0919657f..50092ce7f 100644 --- a/src/language-handlebars/printer-glimmer.js +++ b/src/language-handlebars/printer-glimmer.js @@ -1,7 +1,7 @@ "use strict"; const { - builders: { group, hardline, ifBreak, indent, join, line, softline }, + builders: { dedent, group, hardline, ifBreak, indent, join, line, softline }, utils: { getDocParts }, } = require("../document"); const { isNonEmptyArray } = require("../common/util"); @@ -12,6 +12,7 @@ const { getNextNode, getPreviousNode, hasPrettierIgnore, + isLastNodeOfSiblings, isNextNodeOfSomeType, isNodeOfSomeType, isParentOfSomeType, @@ -41,28 +42,44 @@ function print(path, options, print) { case "Template": { return group(path.map(print, "body")); } + case "ElementNode": { - // TODO: make it whitespace sensitive - const bim = isNextNodeOfSomeType(path, ["ElementNode"]) ? hardline : ""; + const startingTag = group(printStartingTag(path, print)); + + const escapeNextElementNode = + options.htmlWhitespaceSensitivity !== "strict" && + isNextNodeOfSomeType(path, ["ElementNode"]) + ? softline + : ""; if (isVoid(n)) { - return [group(printStartingTag(path, print)), bim]; + return [startingTag, escapeNextElementNode]; } - const isWhitespaceOnly = n.children.every((n) => isWhitespaceNode(n)); + const endingTag = [""]; + + if (n.children.length === 0) { + return [startingTag, indent(endingTag), escapeNextElementNode]; + } + + if (options.htmlWhitespaceSensitivity !== "strict") { + return [ + startingTag, + indent(printChildren(path, options, print)), + hardline, + indent(endingTag), + escapeNextElementNode, + ]; + } return [ - group(printStartingTag(path, print)), - group([ - isWhitespaceOnly ? "" : indent(printChildren(path, options, print)), - n.children.length > 0 ? hardline : "", - "", - ]), - bim, + startingTag, + indent(group(printChildren(path, options, print))), + indent(endingTag), + escapeNextElementNode, ]; } + case "BlockStatement": { const pp = path.getParentNode(1); @@ -76,20 +93,21 @@ function print(path, options, print) { if (isElseIf) { return [ printElseIfBlock(path, print), - printProgram(path, print), - printInverse(path, print), + printProgram(path, print, options), + printInverse(path, print, options), ]; } return [ printOpenBlock(path, print), group([ - printProgram(path, print), - printInverse(path, print), - printCloseBlock(path, print), + printProgram(path, print, options), + printInverse(path, print, options), + printCloseBlock(path, print, options), ]), ]; } + case "ElementModifierStatement": { return group(["{{", printPathAndParams(path, print), softline, "}}"]); } @@ -160,6 +178,65 @@ function print(path, options, print) { return [n.key, "=", path.call(print, "value")]; } case "TextNode": { + const inAttrNode = path.stack.includes("attributes"); + + if (options.htmlWhitespaceSensitivity === "strict" && !inAttrNode) { + // https://infra.spec.whatwg.org/#ascii-whitespace + const leadingWhitespacesRE = /^[\t\n\f\r ]*/; + const trailingWhitespacesRE = /[\t\n\f\r ]*$/; + const whitespacesOnlyRE = /^[\t\n\f\r ]*$/; + + if (whitespacesOnlyRE.test(n.chars)) { + let breaks = [line]; + + const newlines = countNewLines(n.chars); + if (newlines) { + breaks = generateHardlines(newlines, 2); + } + + if (isLastNodeOfSiblings(path)) { + breaks = breaks.map((newline) => dedent(newline)); + } + + return breaks; + } + + const [lead] = n.chars.match(leadingWhitespacesRE); + const [tail] = n.chars.match(trailingWhitespacesRE); + + let text = n.chars; + + let leadBreaks = []; + if (lead) { + leadBreaks = [line]; + + const leadingNewlines = countNewLines(lead); + if (leadingNewlines) { + leadBreaks = generateHardlines(countNewLines(lead) || 1, 2); + } + + text = text.replace(leadingWhitespacesRE, ""); + } + + let trailBreaks = []; + if (tail) { + trailBreaks = [line]; + + const trailingNewlines = countNewLines(tail); + if (trailingNewlines) { + trailBreaks = generateHardlines(trailingNewlines || 1, 2); + + if (isLastNodeOfSiblings(path)) { + trailBreaks = trailBreaks.map((hardline) => dedent(hardline)); + } + } + + text = text.replace(trailingWhitespacesRE, ""); + } + + return [...leadBreaks, text, ...trailBreaks]; + } + const maxLineBreaksToPreserve = 2; const isFirstElement = !getPreviousNode(path); const isLastElement = !getNextNode(path); @@ -193,7 +270,6 @@ function print(path, options, print) { } } - const inAttrNode = path.stack.includes("attributes"); if (inAttrNode) { // TODO: format style and srcset attributes if (!isInAttributeOfName(path, "class")) { @@ -349,12 +425,20 @@ function printStartingTag(path, print) { } function printChildren(path, options, print) { + const node = path.getValue(); + const isEmpty = node.children.every((n) => isWhitespaceNode(n)); + if (options.htmlWhitespaceSensitivity !== "strict" && isEmpty) { + return ""; + } + return path.map((childPath, childIndex) => { - if (childIndex === 0) { - return [softline, print(childPath, options, print)]; + const printedChild = print(childPath, options, print); + + if (childIndex === 0 && options.htmlWhitespaceSensitivity !== "strict") { + return [softline, printedChild]; } - return print(childPath, options, print); + return printedChild; }, "children"); } @@ -444,9 +528,9 @@ function printOpenBlock(path, print) { ]); } -function printElseBlock(node) { +function printElseBlock(node, options) { return [ - hardline, + options.htmlWhitespaceSensitivity !== "strict" ? hardline : "", printInverseBlockOpeningMustache(node), "else", printInverseBlockClosingMustache(node), @@ -464,11 +548,23 @@ function printElseIfBlock(path, print) { ]; } -function printCloseBlock(path, print) { +function printCloseBlock(path, print, options) { const node = path.getValue(); + if (options.htmlWhitespaceSensitivity !== "strict") { + const escape = blockStatementHasOnlyWhitespaceInProgram(node) + ? softline + : hardline; + + return [ + escape, + printClosingBlockOpeningMustache(node), + path.call(print, "path"), + printClosingBlockClosingMustache(node), + ]; + } + return [ - blockStatementHasOnlyWhitespaceInProgram(node) ? softline : hardline, printClosingBlockOpeningMustache(node), path.call(print, "path"), printClosingBlockClosingMustache(node), @@ -495,7 +591,7 @@ function blockStatementHasElse(node) { return isNodeOfSomeType(node, ["BlockStatement"]) && node.inverse; } -function printProgram(path, print) { +function printProgram(path, print, options) { const node = path.getValue(); if (blockStatementHasOnlyWhitespaceInProgram(node)) { @@ -503,21 +599,29 @@ function printProgram(path, print) { } const program = path.call(print, "program"); - return indent([hardline, program]); + + if (options.htmlWhitespaceSensitivity !== "strict") { + return indent([hardline, program]); + } + + return indent(program); } -function printInverse(path, print) { +function printInverse(path, print, options) { const node = path.getValue(); const inverse = path.call(print, "inverse"); - const parts = [hardline, inverse]; + const printed = + options.htmlWhitespaceSensitivity !== "strict" + ? [hardline, inverse] + : inverse; if (blockStatementHasElseIf(node)) { - return parts; + return printed; } if (blockStatementHasElse(node)) { - return [printElseBlock(node), indent(parts)]; + return [printElseBlock(node, options), indent(printed)]; } return ""; diff --git a/src/language-handlebars/utils.js b/src/language-handlebars/utils.js index c7b20c3d4..7b41711cb 100644 --- a/src/language-handlebars/utils.js +++ b/src/language-handlebars/utils.js @@ -2,6 +2,27 @@ const htmlVoidElements = require("html-void-elements"); +function isLastNodeOfSiblings(path) { + const node = path.getValue(); + const parentNode = path.getParentNode(0); + + if ( + isParentOfSomeType(path, ["ElementNode"]) && + parentNode.children[parentNode.children.length - 1] === node + ) { + return true; + } + + if ( + isParentOfSomeType(path, ["Block"]) && + parentNode.body[parentNode.body.length - 1] === node + ) { + return true; + } + + return false; +} + function isUppercase(string) { return string.toUpperCase() === string; } @@ -83,6 +104,7 @@ module.exports = { getNextNode, getPreviousNode, hasPrettierIgnore, + isLastNodeOfSiblings, isNextNodeOfSomeType, isNodeOfSomeType, isParentOfSomeType, diff --git a/tests/handlebars/html-whitespace-sensitivity/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/html-whitespace-sensitivity/__snapshots__/jsfmt.spec.js.snap index 5168494e5..5cb12c015 100644 --- a/tests/handlebars/html-whitespace-sensitivity/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/html-whitespace-sensitivity/__snapshots__/jsfmt.spec.js.snap @@ -433,125 +433,118 @@ sometimes{{nogaps}}areimportant =====================================output===================================== -

- Hi {{firstName}} {{ - lastName - }} , welcome! -

+

Hi + {{firstName}} + {{lastName}} + , welcome!

{{#component propA}} - for {{propB}} do {{propC}} f + for + {{propB}} + do + {{propC}} + f {{/component}} {{#component propA}} - for {{propB}} - - name - - do {{propC}} f + for + {{propB}} + namedo + {{propC}} + f {{/component}} -{{propA}} {{propB}} +{{propA}} +{{propB}} {{propC}}{{propD}} - - {{propE}} {{propF}} - - - {{propG}}{{propH}} - +{{propE}} {{propF}} +{{propG}}{{propH}} hey - +
- - +
- - + +
{{name}} -Some sentence with {{dynamic -}} expressions. +Some sentence with +{{dynamic}} +expressions. sometimes{{nogaps -}}areimportant - +}}areimportant -{{name}} is your name +{{name}} +is your name {{#block}}{{/block}} {{#block}} - some {{text}} + some + {{text}} {{/block}} {{#block}} - some {{text}} + + some + {{text}} {{/block}} - some {{text}} + some + {{text}} - some {{text}} + + some + {{text}}
+
- - This is your name: {{name}}. - - - This is your name: {{name}} (employee) - - - This is your name: {{name}} ({{role}}) - +This is your name: + {{name}}. +This is your name: + {{name}} + (employee) +This is your name: + {{name}} + ({{role}}) +123 - 123 - - - 123 - - - 123 + 123 +123 123 +
123
- 123 -
-
- 123 -
-
- 123 + 123
+
123
123 @@ -560,49 +553,23 @@ sometimes{{nogaps - - - + + +
- A - - B - - C - ABC
- - - - - - - - -
- A - - B - - C -
+
ABC
- - - - - + + +
- A - - B - - C - A B C
@@ -613,5 +580,164 @@ sometimes{{nogaps + +================================================================================ +`; + +exports[`snippet: #12 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +
 
+=====================================output===================================== +
 
+================================================================================ +`; + +exports[`snippet: #13 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +
 
+=====================================output===================================== +
 
+================================================================================ +`; + +exports[`snippet: #14 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +
  
+=====================================output===================================== +
  
+================================================================================ +`; + +exports[`snippet: #15 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +
   
+=====================================output===================================== +
   
+================================================================================ +`; + +exports[`snippet: #16 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +  +=====================================output===================================== +  +================================================================================ +`; + +exports[`snippet: #17 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +   +=====================================output===================================== +   +================================================================================ +`; + +exports[`snippet: #18 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +    +=====================================output===================================== +    +================================================================================ +`; + +exports[`snippet: #19 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +     +=====================================output===================================== +     +================================================================================ +`; + +exports[`snippet: #20 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +   |   +=====================================output===================================== +   |   +================================================================================ +`; + +exports[`snippet: #21 - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== +

X   or   Y

X   or   Y

+=====================================output===================================== +

X +   or   + Y

X   or   Y

+================================================================================ +`; + +exports[`snippet: \`U+2005\` should format like \`U+005F\` not like \`U+0020\` - {"htmlWhitespaceSensitivity":"strict","printWidth":40} format 1`] = ` +====================================options===================================== +htmlWhitespaceSensitivity: "strict" +parsers: ["glimmer"] +printWidth: 40 + | printWidth +=====================================input====================================== + +
before afterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
+ +
before_afterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
+ +
before afterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
+=====================================output===================================== + +
before afterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
+ +
before_afterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
+ +
before + afterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
================================================================================ `; diff --git a/tests/handlebars/html-whitespace-sensitivity/jsfmt.spec.js b/tests/handlebars/html-whitespace-sensitivity/jsfmt.spec.js index eecdb7268..07299457f 100644 --- a/tests/handlebars/html-whitespace-sensitivity/jsfmt.spec.js +++ b/tests/handlebars/html-whitespace-sensitivity/jsfmt.spec.js @@ -1,8 +1,85 @@ +const { outdent } = require("outdent"); + run_spec(__dirname, ["glimmer"], { htmlWhitespaceSensitivity: "ignore", printWidth: 40, }); -run_spec(__dirname, ["glimmer"], { - htmlWhitespaceSensitivity: "strict", - printWidth: 40, -}); +run_spec( + { + dirname: __dirname, + snippets: [ + // single + // https://developer.mozilla.org/en-US/docs/Glossary/Whitespace#In_HTML + ...["\u0009", "\u000C", "\u0020"].map((textContent) => ({ + code: `
${textContent}
`, + name: "content", + output: "
", + })), + ...["\u000A", "\u000D"].map((textContent) => ({ + code: `
${textContent}
`, + name: "content", + output: "
\n
", + })), + + // many + { + code: "
\u0009\u000A\u000C\u000D\u0020
", + output: "
\n\n
", + }, + + // single + ...["\u0009", "\u000C", "\u0020"].map((textContent) => ({ + code: `${textContent}`, + name: "between", + output: " ", + })), + ...["\u000A", "\u000D"].map((textContent) => ({ + code: `${textContent}`, + name: "between", + output: "\n", + })), + + // many + { + code: "\u0009\u000A\u000C\u000D\u0020", + output: "\n\n", + }, + + // non-space + ...[ + "\u2005", + " \u2005 ", + " \u2005\u2005 ", + " \u2005 \u2005 ", + ].map((textContent) => `
${textContent}
`), + + ...[ + "\u2005", + " \u2005 ", + " \u2005\u2005 ", + " \u2005 \u2005 ", + ].map((textContent) => `${textContent}`), + + " \u2005 | \u2005 ", + + "

X \u2005 or \u2005 Y

X \u2005 or \u2005 Y

", + + { + name: "`U+2005` should format like `U+005F` not like `U+0020`", + code: outdent` + +
before\u2005afterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
+ +
before\u005Fafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
+ +
before\u0020afterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafterafter
+ `, + }, + ], + }, + ["glimmer"], + { + htmlWhitespaceSensitivity: "strict", + printWidth: 40, + } +);