From e77a7b1dfdd9b576c1ad98ef2c9d705ab1d50020 Mon Sep 17 00:00:00 2001 From: tbartel Date: Wed, 19 Feb 2020 06:55:57 +0100 Subject: [PATCH 01/10] Use Melody parser configuration for custom Twig tags Introduce printing for GenericTwigTag and GenericToken Small refactoring --- src/index.js | 7 ++++++ src/parser.js | 20 +++++++++++---- src/print/ForStatement.js | 8 +++--- src/print/GenericToken.js | 7 ++++++ src/print/GenericTwigTag.js | 50 +++++++++++++++++++++++++++++++++++++ src/printer.js | 12 +++++++++ src/util/publicFunctions.js | 5 +++- 7 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 src/print/GenericToken.js create mode 100644 src/print/GenericTwigTag.js diff --git a/src/index.js b/src/index.js index d3c98821..7e5ba9d3 100644 --- a/src/index.js +++ b/src/index.js @@ -82,6 +82,13 @@ const options = { description: "Provide additional plugins for Melody. Relative file path from the project root." }, + twigMultiTags: { + type: "path", + category: "Global", + array: true, + default: [{ value: [] }], + description: "Make custom Twig tags known to the parser." + }, twigSingleQuote: { type: "boolean", category: "Global", diff --git a/src/parser.js b/src/parser.js index 60f3879f..dd456b8d 100644 --- a/src/parser.js +++ b/src/parser.js @@ -20,7 +20,7 @@ const createConfiguredLexer = (code, ...extensions) => { return lexer; }; -const configureParser = (parser, ...extensions) => { +const applyParserExtensions = (parser, ...extensions) => { for (const extension of extensions) { if (extension.tags) { for (const tag of extension.tags) { @@ -45,7 +45,7 @@ const configureParser = (parser, ...extensions) => { } }; -const createConfiguredParser = (code, ...extensions) => { +const createConfiguredParser = (code, multiTagConfig, ...extensions) => { const parser = new Parser( new TokenStream(createConfiguredLexer(code, ...extensions), { ignoreWhitespace: true, @@ -57,20 +57,30 @@ const createConfiguredParser = (code, ...extensions) => { ignoreComments: false, ignoreHtmlComments: false, ignoreDeclarations: false, - decodeEntities: false + decodeEntities: false, + multiTags: multiTagConfig, + allowUnknownTags: true } ); - configureParser(parser, ...extensions); + applyParserExtensions(parser, ...extensions); return parser; }; +const getMultiTagConfig = (tagsCsvs = []) => + tagsCsvs.reduce((acc, curr) => { + const tagNames = curr.split(","); + acc[tagNames[0].trim()] = tagNames.slice(1).map(s => s.trim()); + return acc; + }, {}); + const parse = (text, parsers, options) => { const pluginPaths = getPluginPathsFromOptions(options); + const multiTagConfig = getMultiTagConfig(options.twigMultiTags || []); const extensions = [ coreExtension, ...getAdditionalMelodyExtensions(pluginPaths) ]; - const parser = createConfiguredParser(text, ...extensions); + const parser = createConfiguredParser(text, multiTagConfig, ...extensions); const ast = parser.parse(); ast[ORIGINAL_SOURCE] = text; return ast; diff --git a/src/print/ForStatement.js b/src/print/ForStatement.js index 0c184dc1..20e54a39 100644 --- a/src/print/ForStatement.js +++ b/src/print/ForStatement.js @@ -1,6 +1,10 @@ const prettier = require("prettier"); const { group, indent, line, hardline, concat } = prettier.doc.builders; -const { EXPRESSION_NEEDED, isWhitespaceNode } = require("../util"); +const { + EXPRESSION_NEEDED, + isWhitespaceNode, + indentWithHardline +} = require("../util"); const printFor = (node, path, print) => { const parts = [node.trimLeft ? "{%-" : "{%", " for "]; @@ -21,8 +25,6 @@ const printFor = (node, path, print) => { return group(concat(parts)); }; -const indentWithHardline = contents => indent(concat([hardline, contents])); - const p = (node, path, print) => { node[EXPRESSION_NEEDED] = false; const parts = [printFor(node, path, print)]; diff --git a/src/print/GenericToken.js b/src/print/GenericToken.js new file mode 100644 index 00000000..7510d680 --- /dev/null +++ b/src/print/GenericToken.js @@ -0,0 +1,7 @@ +const p = (node, path, print) => { + return node.tokenText; +}; + +module.exports = { + printGenericToken: p +}; diff --git a/src/print/GenericTwigTag.js b/src/print/GenericTwigTag.js new file mode 100644 index 00000000..89dbdb66 --- /dev/null +++ b/src/print/GenericTwigTag.js @@ -0,0 +1,50 @@ +const prettier = require("prettier"); +const { concat, indent, line, hardline, join, group } = prettier.doc.builders; +const { Node } = require("melody-types"); +const { indentWithHardline } = require("../util"); + +const noSpaceBeforeToken = { + ",": true +}; + +const buildOpeningTag = (node, path, print) => { + const opener = node.trimLeft ? "{%-" : "{%"; + const parts = [opener, " ", node.tagName]; + const printedParts = path.map(print, "parts"); + if (printedParts.length > 0) { + parts.push(" ", printedParts[0]); + } + const indentedParts = [line]; + for (let i = 1; i < parts.length; i++) { + const part = parts[i]; + const isToken = Node.isGenericToken(part); + const separator = + isToken && noSpaceBeforeToken[part.tokenText] ? "" : line; + indentedParts.push(separator, printedParts[i]); + } + if (indentedParts.length > 1) { + parts.push(indent(concat(indentedParts))); + } + const closing = node.trimRight ? "-%}" : "%}"; + parts.push(line, closing); + return group(concat(parts)); +}; + +const p = (node, path, print) => { + const openingTag = buildOpeningTag(node, path, print); + const parts = [openingTag]; + const printedSections = path.map(print, "sections"); + node.sections.forEach((section, i) => { + if (Node.isGenericTwigTag(section)) { + parts.push(concat([hardline, printedSections[i]])); + } else { + // Indent + parts.push(indentWithHardline(printedSections[i])); + } + }); + return concat(parts); +}; + +module.exports = { + printGenericTwigTag: p +}; diff --git a/src/printer.js b/src/printer.js index dfdbe1b3..32287b31 100644 --- a/src/printer.js +++ b/src/printer.js @@ -40,6 +40,8 @@ const { printFromStatement } = require("./print/FromStatement.js"); const { printTwigComment } = require("./print/TwigComment.js"); const { printHtmlComment } = require("./print/HtmlComment.js"); const { printDeclaration } = require("./print/Declaration.js"); +const { printGenericTwigTag } = require("./print/GenericTwigTag.js"); +const { printGenericToken } = require("./print/GenericToken.js"); const { printMacroDeclarationStatement } = require("./print/MacroDeclarationStatement.js"); @@ -264,6 +266,16 @@ printFunctions["MacroDeclarationStatement"] = printMacroDeclarationStatement; printFunctions["TwigComment"] = printTwigComment; printFunctions["HtmlComment"] = printHtmlComment; printFunctions["Declaration"] = printDeclaration; +printFunctions["GenericTwigTag"] = (node, path, print, options) => { + const tagName = node.tagName; + if (printFunctions[tagName + "Tag"]) { + // Give the user the chance to implement a custom + // print function for certain generic Twig tags + return printFunctions[tagName + "Tag"](node, path, print, options); + } + return printGenericTwigTag(node, path, print, options); +}; +printFunctions["GenericToken"] = printGenericToken; // Fallbacks printFunctions["String"] = s => s; diff --git a/src/util/publicFunctions.js b/src/util/publicFunctions.js index 1a9adec8..3f3985f5 100644 --- a/src/util/publicFunctions.js +++ b/src/util/publicFunctions.js @@ -487,6 +487,8 @@ const addPreserveWhitespaceInfo = (inlineMap, nodes) => { }); }; +const indentWithHardline = contents => indent(concat([hardline, contents])); + const printChildGroups = (node, path, print, ...childPath) => { // For the preprocessed children, get a map showing which elements can // be printed inline @@ -559,5 +561,6 @@ module.exports = { setDeepProperty, isInlineElement, printChildBlock, - printChildGroups + printChildGroups, + indentWithHardline }; From 21d773609cd00294d3ddd52b6578bbaf168aef2c Mon Sep 17 00:00:00 2001 From: tbartel Date: Wed, 19 Feb 2020 06:56:45 +0100 Subject: [PATCH 02/10] Add test cases for custom Twig tags --- .../Failing/__snapshots__/jsfmt.spec.js.snap | 17 ++++ tests/Failing/failing.melody.twig | 9 ++ .../__snapshots__/jsfmt.spec.js.snap | 96 +++++++++++++++++++ tests/GenericTags/cache.melody.twig | 12 +++ tests/GenericTags/header.melody.twig | 4 + tests/GenericTags/includeCssFile.melody.twig | 1 + tests/GenericTags/jsfmt.spec.js | 8 ++ tests/GenericTags/nav.melody.twig | 10 ++ tests/GenericTags/paginate.melody.twig | 1 + tests/GenericTags/redirect.melody.twig | 1 + tests/GenericTags/switch.melody.twig | 16 ++++ 11 files changed, 175 insertions(+) create mode 100644 tests/GenericTags/__snapshots__/jsfmt.spec.js.snap create mode 100644 tests/GenericTags/cache.melody.twig create mode 100644 tests/GenericTags/header.melody.twig create mode 100644 tests/GenericTags/includeCssFile.melody.twig create mode 100644 tests/GenericTags/jsfmt.spec.js create mode 100644 tests/GenericTags/nav.melody.twig create mode 100644 tests/GenericTags/paginate.melody.twig create mode 100644 tests/GenericTags/redirect.melody.twig create mode 100644 tests/GenericTags/switch.melody.twig diff --git a/tests/Failing/__snapshots__/jsfmt.spec.js.snap b/tests/Failing/__snapshots__/jsfmt.spec.js.snap index a3c677b5..c61d38ae 100644 --- a/tests/Failing/__snapshots__/jsfmt.spec.js.snap +++ b/tests/Failing/__snapshots__/jsfmt.spec.js.snap @@ -84,6 +84,23 @@ exports[`controversial.melody.twig 1`] = ` `; exports[`failing.melody.twig 1`] = ` +{% mount '@trivago/popover-component' as 'toolbar-popover' with { + ContentComponent: PopoverComponent, + + position: 'bottom-leading' + } +%} + +{# Parentheses dropped #} +{% header 'Cache-Control: max-age=' ~ (expiry.timestamp - now.timestamp) %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% mount '@trivago/popover-component' as 'toolbar-popover' with { + ContentComponent: PopoverComponent, + position: 'bottom-leading' + } +%} + +{# Parentheses dropped #} +{% header 'Cache-Control: max-age=' ~ expiry.timestamp - now.timestamp %} `; diff --git a/tests/Failing/failing.melody.twig b/tests/Failing/failing.melody.twig index e69de29b..0d516fa3 100644 --- a/tests/Failing/failing.melody.twig +++ b/tests/Failing/failing.melody.twig @@ -0,0 +1,9 @@ +{% mount '@trivago/popover-component' as 'toolbar-popover' with { + ContentComponent: PopoverComponent, + + position: 'bottom-leading' + } +%} + +{# Parentheses dropped #} +{% header 'Cache-Control: max-age=' ~ (expiry.timestamp - now.timestamp) %} diff --git a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..2e5c7a29 --- /dev/null +++ b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`cache.melody.twig 1`] = ` +{% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} + {% for block in entry.myMatrixField %} +

{{ block.text }}

+ {% endfor %} +{% endcache %} + +{# prettier-ignore #} +{% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} + {% for block in entry.myMatrixField %} +

{{ block.text }}

+ {% endfor %} +{% endcache %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% cache globally + + using + key + craft.some.rather.long.property.chain.request.path + for +%} + {% for block in entry.myMatrixField %} +

+ {{ block.text }} +

+ {% endfor %} +{% endcache %} + +{# prettier-ignore #} +{% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} + {% for block in entry.myMatrixField %} +

{{ block.text }}

+ {% endfor %} +{% endcache %} + +`; + +exports[`header.melody.twig 1`] = ` +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} + +{# prettier-ignore #} +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% header 'Cache-Control: max-age=' ~ expiry.timestamp - now.timestamp %} + +{# prettier-ignore #} +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} + +`; + +exports[`includeCssFile.melody.twig 1`] = ` +{% includeCssFile "/assets/css/layouts/" ~ entry.layout ~ ".css" %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% includeCssFile '/assets/css/layouts/' ~ entry.layout ~ '.css' %} + +`; + +exports[`nav.melody.twig 1`] = ` +{% nav entry in entries %} +
  • + {{ entry.title }} + {% ifchildren %} +
      + {% children %} +
    + {% endifchildren %} +
  • +{% endnav %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% nav entry in entries %} +
  • + {{ entry.title }} + {% ifchildren %} +
      + {% children %} +
    + {% endifchildren %} +
  • +{% endnav %} + +`; + +exports[`paginate.melody.twig 1`] = ` +{% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% paginate craft.entries.section('blog').limit(10) + + as + pageInfo + , + pageEntries +%} + +`; diff --git a/tests/GenericTags/cache.melody.twig b/tests/GenericTags/cache.melody.twig new file mode 100644 index 00000000..4345fdbb --- /dev/null +++ b/tests/GenericTags/cache.melody.twig @@ -0,0 +1,12 @@ +{% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} + {% for block in entry.myMatrixField %} +

    {{ block.text }}

    + {% endfor %} +{% endcache %} + +{# prettier-ignore #} +{% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} + {% for block in entry.myMatrixField %} +

    {{ block.text }}

    + {% endfor %} +{% endcache %} diff --git a/tests/GenericTags/header.melody.twig b/tests/GenericTags/header.melody.twig new file mode 100644 index 00000000..183bdc0b --- /dev/null +++ b/tests/GenericTags/header.melody.twig @@ -0,0 +1,4 @@ +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} + +{# prettier-ignore #} +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} diff --git a/tests/GenericTags/includeCssFile.melody.twig b/tests/GenericTags/includeCssFile.melody.twig new file mode 100644 index 00000000..1e2ca1fb --- /dev/null +++ b/tests/GenericTags/includeCssFile.melody.twig @@ -0,0 +1 @@ +{% includeCssFile "/assets/css/layouts/" ~ entry.layout ~ ".css" %} diff --git a/tests/GenericTags/jsfmt.spec.js b/tests/GenericTags/jsfmt.spec.js new file mode 100644 index 00000000..555a2c8d --- /dev/null +++ b/tests/GenericTags/jsfmt.spec.js @@ -0,0 +1,8 @@ +run_spec(__dirname, ["melody"], { + twigMultiTags: [ + "nav,endnav", + "switch,case,endswitch", + "ifchildren,endifchildren", + "cache,endcache" + ] +}); diff --git a/tests/GenericTags/nav.melody.twig b/tests/GenericTags/nav.melody.twig new file mode 100644 index 00000000..49cd790c --- /dev/null +++ b/tests/GenericTags/nav.melody.twig @@ -0,0 +1,10 @@ +{% nav entry in entries %} +
  • + {{ entry.title }} + {% ifchildren %} +
      + {% children %} +
    + {% endifchildren %} +
  • +{% endnav %} diff --git a/tests/GenericTags/paginate.melody.twig b/tests/GenericTags/paginate.melody.twig new file mode 100644 index 00000000..f38ca117 --- /dev/null +++ b/tests/GenericTags/paginate.melody.twig @@ -0,0 +1 @@ +{% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries %} diff --git a/tests/GenericTags/redirect.melody.twig b/tests/GenericTags/redirect.melody.twig new file mode 100644 index 00000000..85224823 --- /dev/null +++ b/tests/GenericTags/redirect.melody.twig @@ -0,0 +1 @@ +{% redirect "pricing" 301 %} diff --git a/tests/GenericTags/switch.melody.twig b/tests/GenericTags/switch.melody.twig new file mode 100644 index 00000000..5f6cd3f0 --- /dev/null +++ b/tests/GenericTags/switch.melody.twig @@ -0,0 +1,16 @@ +{% switch matrixBlock.type %} + + {% case "text" %} + + {{ matrixBlock.textField | markdown }} + + {% case "image" %} + + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    A font walks into a bar.

    +

    The bartender says, “Hey, we don’t serve your type in here!”

    + +{% endswitch %} From 35d8c21e1b5664d624d3fb3ccf2cc52da985c242 Mon Sep 17 00:00:00 2001 From: tbartel Date: Thu, 20 Feb 2020 08:39:05 +0100 Subject: [PATCH 03/10] Fix bug with indented sections of GenericTwigTag Add test cases --- src/print/GenericTwigTag.js | 8 +-- .../__snapshots__/jsfmt.spec.js.snap | 58 +++++++++++++++++-- tests/GenericTags/paginate.melody.twig | 2 + 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/print/GenericTwigTag.js b/src/print/GenericTwigTag.js index 89dbdb66..e9ec873a 100644 --- a/src/print/GenericTwigTag.js +++ b/src/print/GenericTwigTag.js @@ -14,15 +14,15 @@ const buildOpeningTag = (node, path, print) => { if (printedParts.length > 0) { parts.push(" ", printedParts[0]); } - const indentedParts = [line]; - for (let i = 1; i < parts.length; i++) { - const part = parts[i]; + const indentedParts = []; + for (let i = 1; i < node.parts.length; i++) { + const part = node.parts[i]; const isToken = Node.isGenericToken(part); const separator = isToken && noSpaceBeforeToken[part.tokenText] ? "" : line; indentedParts.push(separator, printedParts[i]); } - if (indentedParts.length > 1) { + if (node.parts.length > 1) { parts.push(indent(concat(indentedParts))); } const closing = node.trimRight ? "-%}" : "%}"; diff --git a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap index 2e5c7a29..b8141bef 100644 --- a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap +++ b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap @@ -15,11 +15,12 @@ exports[`cache.melody.twig 1`] = ` {% endcache %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {% cache globally - using key craft.some.rather.long.property.chain.request.path for + 3 + weeks %} {% for block in entry.myMatrixField %}

    @@ -84,13 +85,60 @@ exports[`nav.melody.twig 1`] = ` exports[`paginate.melody.twig 1`] = ` {% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries %} + +{% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries, pageProperties %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -{% paginate craft.entries.section('blog').limit(10) +{% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries %} +{% paginate craft.entries.section('blog').limit(10) as - pageInfo - , - pageEntries + pageInfo, + pageEntries, + pageProperties %} `; + +exports[`redirect.melody.twig 1`] = ` +{% redirect "pricing" 301 %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% redirect pricing 301 %} + +`; + +exports[`switch.melody.twig 1`] = ` +{% switch matrixBlock.type %} + + {% case "text" %} + + {{ matrixBlock.textField | markdown }} + + {% case "image" %} + + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    A font walks into a bar.

    +

    The bartender says, “Hey, we don’t serve your type in here!”

    + +{% endswitch %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% switch matrixBlock.type %} + +{% case text %} + {{ matrixBlock.textField|markdown }} +{% case image %} + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    + A font walks into a bar. +

    +

    + The bartender says, “Hey, we don’t serve your type in here!” +

    +{% endswitch %} + +`; diff --git a/tests/GenericTags/paginate.melody.twig b/tests/GenericTags/paginate.melody.twig index f38ca117..d6f90e58 100644 --- a/tests/GenericTags/paginate.melody.twig +++ b/tests/GenericTags/paginate.melody.twig @@ -1 +1,3 @@ {% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries %} + +{% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries, pageProperties %} From 8c3524122bcb93c786e56f06574aaa58bbeb643f Mon Sep 17 00:00:00 2001 From: tbartel Date: Fri, 21 Feb 2020 07:06:37 +0100 Subject: [PATCH 04/10] Fix bug with isMelodyNode(): Improve detection --- src/print/GenericTwigTag.js | 3 ++- src/util/publicFunctions.js | 6 +++++- tests/GenericTags/__snapshots__/jsfmt.spec.js.snap | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/print/GenericTwigTag.js b/src/print/GenericTwigTag.js index e9ec873a..1dffed91 100644 --- a/src/print/GenericTwigTag.js +++ b/src/print/GenericTwigTag.js @@ -1,7 +1,7 @@ const prettier = require("prettier"); const { concat, indent, line, hardline, join, group } = prettier.doc.builders; const { Node } = require("melody-types"); -const { indentWithHardline } = require("../util"); +const { STRING_NEEDS_QUOTES, indentWithHardline } = require("../util"); const noSpaceBeforeToken = { ",": true @@ -31,6 +31,7 @@ const buildOpeningTag = (node, path, print) => { }; const p = (node, path, print) => { + node[STRING_NEEDS_QUOTES] = true; const openingTag = buildOpeningTag(node, path, print); const parts = [openingTag]; const printedSections = path.map(print, "sections"); diff --git a/src/util/publicFunctions.js b/src/util/publicFunctions.js index 3f3985f5..99d916b4 100644 --- a/src/util/publicFunctions.js +++ b/src/util/publicFunctions.js @@ -132,7 +132,11 @@ const isValidIdentifierName = s => { const isMelodyNode = n => { const proto = n.__proto__; - return typeof n === "object" && proto.type && proto.visitorKeys; + return ( + typeof n === "object" && + proto.type && + typeof Node["is" + proto.type] === "function" + ); }; const findParentNode = path => { diff --git a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap index b8141bef..4438300b 100644 --- a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap +++ b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap @@ -102,7 +102,7 @@ exports[`paginate.melody.twig 1`] = ` exports[`redirect.melody.twig 1`] = ` {% redirect "pricing" 301 %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -{% redirect pricing 301 %} +{% redirect 'pricing' 301 %} `; @@ -126,9 +126,9 @@ exports[`switch.melody.twig 1`] = ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {% switch matrixBlock.type %} -{% case text %} +{% case 'text' %} {{ matrixBlock.textField|markdown }} -{% case image %} +{% case 'image' %} {{ matrixBlock.image[0].getImg() }} {% default %} From 677524e1edc219b33af26f94b4791828f482b913 Mon Sep 17 00:00:00 2001 From: tbartel Date: Fri, 21 Feb 2020 07:20:42 +0100 Subject: [PATCH 05/10] Re-arrange test cases --- .../__snapshots__/jsfmt.spec.js.snap | 74 ++++++++++--------- tests/GenericTags/failing.melody.twig | 18 +++++ tests/GenericTags/header.melody.twig | 2 - tests/GenericTags/switch.melody.twig | 15 ---- 4 files changed, 58 insertions(+), 51 deletions(-) create mode 100644 tests/GenericTags/failing.melody.twig diff --git a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap index 4438300b..c985a291 100644 --- a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap +++ b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap @@ -38,14 +38,51 @@ exports[`cache.melody.twig 1`] = ` `; -exports[`header.melody.twig 1`] = ` +exports[`failing.melody.twig 1`] = ` {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} -{# prettier-ignore #} -{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} +{% switch matrixBlock.type %} + + {% case "text" %} + + {{ matrixBlock.textField | markdown }} + + {% case "image" %} + + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    A font walks into a bar.

    +

    The bartender says, “Hey, we don’t serve your type in here!”

    + +{% endswitch %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {% header 'Cache-Control: max-age=' ~ expiry.timestamp - now.timestamp %} +{% switch matrixBlock.type %} + +{% case 'text' %} + {{ matrixBlock.textField|markdown }} +{% case 'image' %} + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    + A font walks into a bar. +

    +

    + The bartender says, “Hey, we don’t serve your type in here!” +

    +{% endswitch %} + +`; + +exports[`header.melody.twig 1`] = ` +{# prettier-ignore #} +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {# prettier-ignore #} {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} @@ -107,38 +144,7 @@ exports[`redirect.melody.twig 1`] = ` `; exports[`switch.melody.twig 1`] = ` -{% switch matrixBlock.type %} - - {% case "text" %} - - {{ matrixBlock.textField | markdown }} - - {% case "image" %} - - {{ matrixBlock.image[0].getImg() }} - - {% default %} - -

    A font walks into a bar.

    -

    The bartender says, “Hey, we don’t serve your type in here!”

    -{% endswitch %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -{% switch matrixBlock.type %} - -{% case 'text' %} - {{ matrixBlock.textField|markdown }} -{% case 'image' %} - {{ matrixBlock.image[0].getImg() }} - - {% default %} - -

    - A font walks into a bar. -

    -

    - The bartender says, “Hey, we don’t serve your type in here!” -

    -{% endswitch %} `; diff --git a/tests/GenericTags/failing.melody.twig b/tests/GenericTags/failing.melody.twig new file mode 100644 index 00000000..4cc50d0f --- /dev/null +++ b/tests/GenericTags/failing.melody.twig @@ -0,0 +1,18 @@ +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} + +{% switch matrixBlock.type %} + + {% case "text" %} + + {{ matrixBlock.textField | markdown }} + + {% case "image" %} + + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    A font walks into a bar.

    +

    The bartender says, “Hey, we don’t serve your type in here!”

    + +{% endswitch %} diff --git a/tests/GenericTags/header.melody.twig b/tests/GenericTags/header.melody.twig index 183bdc0b..ebad273d 100644 --- a/tests/GenericTags/header.melody.twig +++ b/tests/GenericTags/header.melody.twig @@ -1,4 +1,2 @@ -{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} - {# prettier-ignore #} {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} diff --git a/tests/GenericTags/switch.melody.twig b/tests/GenericTags/switch.melody.twig index 5f6cd3f0..8b137891 100644 --- a/tests/GenericTags/switch.melody.twig +++ b/tests/GenericTags/switch.melody.twig @@ -1,16 +1 @@ -{% switch matrixBlock.type %} - {% case "text" %} - - {{ matrixBlock.textField | markdown }} - - {% case "image" %} - - {{ matrixBlock.image[0].getImg() }} - - {% default %} - -

    A font walks into a bar.

    -

    The bartender says, “Hey, we don’t serve your type in here!”

    - -{% endswitch %} From 8453b614137733e9387ea07cb824e3cb02e94dbc Mon Sep 17 00:00:00 2001 From: tbartel Date: Mon, 24 Feb 2020 07:22:52 +0100 Subject: [PATCH 06/10] Fix parenthesis bug in BinaryExpression --- src/print/BinaryExpression.js | 39 ++++++++++--------- .../Failing/__snapshots__/jsfmt.spec.js.snap | 6 --- tests/Failing/failing.melody.twig | 3 -- .../__snapshots__/jsfmt.spec.js.snap | 12 +++--- tests/GenericTags/failing.melody.twig | 2 - tests/GenericTags/header.melody.twig | 4 +- 6 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/print/BinaryExpression.js b/src/print/BinaryExpression.js index 7df04409..c2b23704 100644 --- a/src/print/BinaryExpression.js +++ b/src/print/BinaryExpression.js @@ -46,11 +46,26 @@ const hasLogicalOperator = node => { return node.operator === "or" || node.operator === "and"; }; +const otherNeedsParentheses = (node, otherProp) => { + const other = node[otherProp]; + const isBinaryOther = Node.isBinaryExpression(other); + const ownPrecedence = operatorPrecedence[node.operator]; + const otherPrecedence = isBinaryOther + ? operatorPrecedence[node[otherProp].operator] + : Number.MAX_SAFE_INTEGER; + return ( + otherPrecedence < ownPrecedence || + (otherPrecedence > ownPrecedence && + isBinaryOther && + hasLogicalOperator(other)) || + Node.isFilterExpression(other) + ); +}; + const printBinaryExpression = (node, path, print) => { node[EXPRESSION_NEEDED] = false; node[STRING_NEEDS_QUOTES] = true; - const isBinaryLeft = Node.isBinaryExpression(node.left); const isBinaryRight = Node.isBinaryExpression(node.right); const isLogicalOperator = ["and", "or"].indexOf(node.operator) > -1; const whitespaceAroundOperator = operatorNeedsSpaces(node.operator); @@ -79,29 +94,15 @@ const printBinaryExpression = (node, path, print) => { ? firstValueInAncestorChain(path, "operator") : ""; - const ownPrecedence = operatorPrecedence[node.operator]; - node[OPERATOR_PRECEDENCE] = ownPrecedence; + node[OPERATOR_PRECEDENCE] = operatorPrecedence[node.operator]; - const leftPrecedence = isBinaryLeft - ? operatorPrecedence[node.left.operator] - : Number.MAX_SAFE_INTEGER; - const rightPrecedence = isBinaryRight - ? operatorPrecedence[node.right.operator] - : Number.MAX_SAFE_INTEGER; const printedLeft = path.call(print, "left"); const printedRight = path.call(print, "right"); const parts = []; - const leftNeedsParens = - (leftPrecedence != ownPrecedence && - Node.isBinaryExpression(node.left) && - hasLogicalOperator(node.left)) || - Node.isFilterExpression(node.left); - const rightNeedsParens = - (rightPrecedence != ownPrecedence && - Node.isBinaryExpression(node.right) && - hasLogicalOperator(node.right)) || - Node.isFilterExpression(node.right); + const leftNeedsParens = otherNeedsParentheses(node, "left"); + const rightNeedsParens = otherNeedsParentheses(node, "right"); + if (leftNeedsParens) { parts.push("("); } diff --git a/tests/Failing/__snapshots__/jsfmt.spec.js.snap b/tests/Failing/__snapshots__/jsfmt.spec.js.snap index c61d38ae..add6e1ce 100644 --- a/tests/Failing/__snapshots__/jsfmt.spec.js.snap +++ b/tests/Failing/__snapshots__/jsfmt.spec.js.snap @@ -90,9 +90,6 @@ exports[`failing.melody.twig 1`] = ` position: 'bottom-leading' } %} - -{# Parentheses dropped #} -{% header 'Cache-Control: max-age=' ~ (expiry.timestamp - now.timestamp) %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {% mount '@trivago/popover-component' as 'toolbar-popover' with { ContentComponent: PopoverComponent, @@ -100,7 +97,4 @@ exports[`failing.melody.twig 1`] = ` } %} -{# Parentheses dropped #} -{% header 'Cache-Control: max-age=' ~ expiry.timestamp - now.timestamp %} - `; diff --git a/tests/Failing/failing.melody.twig b/tests/Failing/failing.melody.twig index 0d516fa3..52aab75c 100644 --- a/tests/Failing/failing.melody.twig +++ b/tests/Failing/failing.melody.twig @@ -4,6 +4,3 @@ position: 'bottom-leading' } %} - -{# Parentheses dropped #} -{% header 'Cache-Control: max-age=' ~ (expiry.timestamp - now.timestamp) %} diff --git a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap index c985a291..60ca6463 100644 --- a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap +++ b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap @@ -39,8 +39,6 @@ exports[`cache.melody.twig 1`] = ` `; exports[`failing.melody.twig 1`] = ` -{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} - {% switch matrixBlock.type %} {% case "text" %} @@ -58,8 +56,6 @@ exports[`failing.melody.twig 1`] = ` {% endswitch %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -{% header 'Cache-Control: max-age=' ~ expiry.timestamp - now.timestamp %} - {% switch matrixBlock.type %} {% case 'text' %} @@ -80,11 +76,15 @@ exports[`failing.melody.twig 1`] = ` `; exports[`header.melody.twig 1`] = ` +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} + {# prettier-ignore #} -{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% header 'Cache-Control: max-age=' ~ (expiry.timestamp - now.timestamp) %} + {# prettier-ignore #} -{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} `; diff --git a/tests/GenericTags/failing.melody.twig b/tests/GenericTags/failing.melody.twig index 4cc50d0f..5f6cd3f0 100644 --- a/tests/GenericTags/failing.melody.twig +++ b/tests/GenericTags/failing.melody.twig @@ -1,5 +1,3 @@ -{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} - {% switch matrixBlock.type %} {% case "text" %} diff --git a/tests/GenericTags/header.melody.twig b/tests/GenericTags/header.melody.twig index ebad273d..d5b458f4 100644 --- a/tests/GenericTags/header.melody.twig +++ b/tests/GenericTags/header.melody.twig @@ -1,2 +1,4 @@ +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} + {# prettier-ignore #} -{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} +{% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} From 25c1cd539647307942e9031f66b650e48f5af97b Mon Sep 17 00:00:00 2001 From: tbartel Date: Mon, 24 Feb 2020 07:29:18 +0100 Subject: [PATCH 07/10] Fix "switch" test case --- .../__snapshots__/jsfmt.spec.js.snap | 68 +++++++++---------- tests/GenericTags/failing.melody.twig | 16 ----- tests/GenericTags/jsfmt.spec.js | 2 +- tests/GenericTags/switch.melody.twig | 17 +++++ 4 files changed, 49 insertions(+), 54 deletions(-) delete mode 100644 tests/GenericTags/failing.melody.twig diff --git a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap index 60ca6463..22ae3ea9 100644 --- a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap +++ b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap @@ -38,43 +38,6 @@ exports[`cache.melody.twig 1`] = ` `; -exports[`failing.melody.twig 1`] = ` -{% switch matrixBlock.type %} - - {% case "text" %} - - {{ matrixBlock.textField | markdown }} - - {% case "image" %} - - {{ matrixBlock.image[0].getImg() }} - - {% default %} - -

    A font walks into a bar.

    -

    The bartender says, “Hey, we don’t serve your type in here!”

    - -{% endswitch %} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -{% switch matrixBlock.type %} - -{% case 'text' %} - {{ matrixBlock.textField|markdown }} -{% case 'image' %} - {{ matrixBlock.image[0].getImg() }} - - {% default %} - -

    - A font walks into a bar. -

    -

    - The bartender says, “Hey, we don’t serve your type in here!” -

    -{% endswitch %} - -`; - exports[`header.melody.twig 1`] = ` {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} @@ -144,7 +107,38 @@ exports[`redirect.melody.twig 1`] = ` `; exports[`switch.melody.twig 1`] = ` +{% switch matrixBlock.type %} + + + {% case "text" %} + + {{ matrixBlock.textField | markdown }} + + {% case "image" %} + + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    A font walks into a bar.

    +

    The bartender says, “Hey, we don’t serve your type in here!”

    + +{% endswitch %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% switch matrixBlock.type %} + +{% case 'text' %} + {{ matrixBlock.textField|markdown }} +{% case 'image' %} + {{ matrixBlock.image[0].getImg() }} +{% default %} +

    + A font walks into a bar. +

    +

    + The bartender says, “Hey, we don’t serve your type in here!” +

    +{% endswitch %} `; diff --git a/tests/GenericTags/failing.melody.twig b/tests/GenericTags/failing.melody.twig deleted file mode 100644 index 5f6cd3f0..00000000 --- a/tests/GenericTags/failing.melody.twig +++ /dev/null @@ -1,16 +0,0 @@ -{% switch matrixBlock.type %} - - {% case "text" %} - - {{ matrixBlock.textField | markdown }} - - {% case "image" %} - - {{ matrixBlock.image[0].getImg() }} - - {% default %} - -

    A font walks into a bar.

    -

    The bartender says, “Hey, we don’t serve your type in here!”

    - -{% endswitch %} diff --git a/tests/GenericTags/jsfmt.spec.js b/tests/GenericTags/jsfmt.spec.js index 555a2c8d..244df108 100644 --- a/tests/GenericTags/jsfmt.spec.js +++ b/tests/GenericTags/jsfmt.spec.js @@ -1,7 +1,7 @@ run_spec(__dirname, ["melody"], { twigMultiTags: [ "nav,endnav", - "switch,case,endswitch", + "switch,case,default,endswitch", "ifchildren,endifchildren", "cache,endcache" ] diff --git a/tests/GenericTags/switch.melody.twig b/tests/GenericTags/switch.melody.twig index 8b137891..93407201 100644 --- a/tests/GenericTags/switch.melody.twig +++ b/tests/GenericTags/switch.melody.twig @@ -1 +1,18 @@ +{% switch matrixBlock.type %} + + + {% case "text" %} + + {{ matrixBlock.textField | markdown }} + + {% case "image" %} + + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    A font walks into a bar.

    +

    The bartender says, “Hey, we don’t serve your type in here!”

    + +{% endswitch %} From 00250a4f76f462358413327fda12ed3ac5468522 Mon Sep 17 00:00:00 2001 From: tbartel Date: Mon, 24 Feb 2020 08:24:27 +0100 Subject: [PATCH 08/10] Extract print functionality for single Twig tag Enable plugin loading in test environment Add test for generic parsing, but custom printing Improve whitespace handling for generic sub-tags --- src/print/GenericTwigTag.js | 44 +++++-------------- src/util/index.js | 4 +- src/util/pluginUtil.js | 7 ++- src/util/printFunctions.js | 34 ++++++++++++++ src/util/publicFunctions.js | 4 ++ .../__snapshots__/jsfmt.spec.js.snap | 34 ++++++++++++++ tests/GenericTagCustomPrint/jsfmt.spec.js | 4 ++ .../GenericTagCustomPrint/switch.melody.twig | 15 +++++++ .../__snapshots__/jsfmt.spec.js.snap | 1 - tests/switch-plugin/index.js | 37 ++++++++++++++++ 10 files changed, 149 insertions(+), 35 deletions(-) create mode 100644 src/util/printFunctions.js create mode 100644 tests/GenericTagCustomPrint/__snapshots__/jsfmt.spec.js.snap create mode 100644 tests/GenericTagCustomPrint/jsfmt.spec.js create mode 100644 tests/GenericTagCustomPrint/switch.melody.twig create mode 100644 tests/switch-plugin/index.js diff --git a/src/print/GenericTwigTag.js b/src/print/GenericTwigTag.js index 1dffed91..52f0bd58 100644 --- a/src/print/GenericTwigTag.js +++ b/src/print/GenericTwigTag.js @@ -1,46 +1,26 @@ const prettier = require("prettier"); -const { concat, indent, line, hardline, join, group } = prettier.doc.builders; +const { concat, hardline } = prettier.doc.builders; const { Node } = require("melody-types"); -const { STRING_NEEDS_QUOTES, indentWithHardline } = require("../util"); - -const noSpaceBeforeToken = { - ",": true -}; - -const buildOpeningTag = (node, path, print) => { - const opener = node.trimLeft ? "{%-" : "{%"; - const parts = [opener, " ", node.tagName]; - const printedParts = path.map(print, "parts"); - if (printedParts.length > 0) { - parts.push(" ", printedParts[0]); - } - const indentedParts = []; - for (let i = 1; i < node.parts.length; i++) { - const part = node.parts[i]; - const isToken = Node.isGenericToken(part); - const separator = - isToken && noSpaceBeforeToken[part.tokenText] ? "" : line; - indentedParts.push(separator, printedParts[i]); - } - if (node.parts.length > 1) { - parts.push(indent(concat(indentedParts))); - } - const closing = node.trimRight ? "-%}" : "%}"; - parts.push(line, closing); - return group(concat(parts)); -}; +const { + STRING_NEEDS_QUOTES, + indentWithHardline, + printSingleTwigTag, + isEmptySequence +} = require("../util"); const p = (node, path, print) => { node[STRING_NEEDS_QUOTES] = true; - const openingTag = buildOpeningTag(node, path, print); + const openingTag = printSingleTwigTag(node, path, print); const parts = [openingTag]; const printedSections = path.map(print, "sections"); node.sections.forEach((section, i) => { if (Node.isGenericTwigTag(section)) { parts.push(concat([hardline, printedSections[i]])); } else { - // Indent - parts.push(indentWithHardline(printedSections[i])); + if (!isEmptySequence(section)) { + // Indent + parts.push(indentWithHardline(printedSections[i])); + } } }); return concat(parts); diff --git a/src/util/index.js b/src/util/index.js index 135f6d38..039e1a2b 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1,12 +1,14 @@ const pluginUtil = require("./pluginUtil.js"); const publicSymbols = require("./publicSymbols.js"); const publicFunctions = require("./publicFunctions.js"); +const printFunctions = require("./printFunctions.js"); const combinedExports = Object.assign( {}, pluginUtil, publicSymbols, - publicFunctions + publicFunctions, + printFunctions ); module.exports = combinedExports; diff --git a/src/util/pluginUtil.js b/src/util/pluginUtil.js index 5dcc6933..1f8bb925 100644 --- a/src/util/pluginUtil.js +++ b/src/util/pluginUtil.js @@ -12,10 +12,15 @@ const getProjectRoot = () => { const parts = __dirname.split(path.sep); let index = parts.length - 1; let dirName = parts[index]; - while (dirName !== "node_modules") { + while (dirName !== "node_modules" && index > 0) { index--; dirName = parts[index]; } + // If we are not inside a "node_modules" folder, just + // strip away "src" and "util" + if (index === 0) { + index = parts.length - 2; + } const subPath = parts.slice(0, index); const joined = path.join(...subPath); diff --git a/src/util/printFunctions.js b/src/util/printFunctions.js new file mode 100644 index 00000000..1a1aeafb --- /dev/null +++ b/src/util/printFunctions.js @@ -0,0 +1,34 @@ +const prettier = require("prettier"); +const { line, indent, concat, group } = prettier.doc.builders; +const { Node } = require("melody-types"); + +const noSpaceBeforeToken = { + ",": true +}; + +const printSingleTwigTag = (node, path, print) => { + const opener = node.trimLeft ? "{%-" : "{%"; + const parts = [opener, " ", node.tagName]; + const printedParts = path.map(print, "parts"); + if (printedParts.length > 0) { + parts.push(" ", printedParts[0]); + } + const indentedParts = []; + for (let i = 1; i < node.parts.length; i++) { + const part = node.parts[i]; + const isToken = Node.isGenericToken(part); + const separator = + isToken && noSpaceBeforeToken[part.tokenText] ? "" : line; + indentedParts.push(separator, printedParts[i]); + } + if (node.parts.length > 1) { + parts.push(indent(concat(indentedParts))); + } + const closing = node.trimRight ? "-%}" : "%}"; + parts.push(line, closing); + return group(concat(parts)); +}; + +module.exports = { + printSingleTwigTag +}; diff --git a/src/util/publicFunctions.js b/src/util/publicFunctions.js index 99d916b4..d7adbc47 100644 --- a/src/util/publicFunctions.js +++ b/src/util/publicFunctions.js @@ -326,6 +326,9 @@ const isWhitespaceNode = node => { ); }; +const isEmptySequence = node => + Node.isSequenceExpression(node) && node.expressions.length === 0; + const removeSurroundingWhitespace = children => { if (!Array.isArray(children)) { return children; @@ -550,6 +553,7 @@ module.exports = { testCurrentAndParentNodes, isWhitespaceOnly, isWhitespaceNode, + isEmptySequence, hasNoNewlines, countNewlines, hasAtLeastTwoNewlines, diff --git a/tests/GenericTagCustomPrint/__snapshots__/jsfmt.spec.js.snap b/tests/GenericTagCustomPrint/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..7297bccb --- /dev/null +++ b/tests/GenericTagCustomPrint/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`switch.melody.twig 1`] = ` +{% switch matrixBlock.type %} + {% case "text" %} + + {{ matrixBlock.textField | markdown }} + + {% case "image" %} + + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    A font walks into a bar.

    +

    The bartender says, “Hey, we don’t serve your type in here!”

    + +{% endswitch %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{% switch matrixBlock.type %} + {% case 'text' %} + {{ matrixBlock.textField|markdown }} + {% case 'image' %} + {{ matrixBlock.image[0].getImg() }} + {% default %} +

    + A font walks into a bar. +

    +

    + The bartender says, “Hey, we don’t serve your type in here!” +

    +{% endswitch %} + +`; diff --git a/tests/GenericTagCustomPrint/jsfmt.spec.js b/tests/GenericTagCustomPrint/jsfmt.spec.js new file mode 100644 index 00000000..d277ffdf --- /dev/null +++ b/tests/GenericTagCustomPrint/jsfmt.spec.js @@ -0,0 +1,4 @@ +run_spec(__dirname, ["melody"], { + twigMultiTags: ["switch,case,default,endswitch"], + twigMelodyPlugins: ["tests/switch-plugin"] +}); diff --git a/tests/GenericTagCustomPrint/switch.melody.twig b/tests/GenericTagCustomPrint/switch.melody.twig new file mode 100644 index 00000000..26df34d8 --- /dev/null +++ b/tests/GenericTagCustomPrint/switch.melody.twig @@ -0,0 +1,15 @@ +{% switch matrixBlock.type %} + {% case "text" %} + + {{ matrixBlock.textField | markdown }} + + {% case "image" %} + + {{ matrixBlock.image[0].getImg() }} + + {% default %} + +

    A font walks into a bar.

    +

    The bartender says, “Hey, we don’t serve your type in here!”

    + +{% endswitch %} diff --git a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap index 22ae3ea9..94eb0e97 100644 --- a/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap +++ b/tests/GenericTags/__snapshots__/jsfmt.spec.js.snap @@ -127,7 +127,6 @@ exports[`switch.melody.twig 1`] = ` {% endswitch %} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {% switch matrixBlock.type %} - {% case 'text' %} {{ matrixBlock.textField|markdown }} {% case 'image' %} diff --git a/tests/switch-plugin/index.js b/tests/switch-plugin/index.js new file mode 100644 index 00000000..1c980960 --- /dev/null +++ b/tests/switch-plugin/index.js @@ -0,0 +1,37 @@ +const prettier = require("prettier"); +const { concat, indent, hardline } = prettier.doc.builders; +const { + STRING_NEEDS_QUOTES, + printSingleTwigTag, + indentWithHardline, + isEmptySequence +} = require("../../src/util"); +const { Node } = require("melody-types"); + +const printSwitch = (node, path, print) => { + node[STRING_NEEDS_QUOTES] = true; + const openingTag = printSingleTwigTag(node, path, print); + const parts = [openingTag]; + const printedSections = path.map(print, "sections"); + node.sections.forEach((section, i) => { + if (Node.isGenericTwigTag(section)) { + if (section.tagName === "endswitch") { + parts.push(concat([hardline, printedSections[i]])); + } else { + parts.push(indentWithHardline(printedSections[i])); + } + } else { + if (!isEmptySequence(section)) { + // Indent twice + parts.push(indent(indentWithHardline(printedSections[i]))); + } + } + }); + return concat(parts); +}; + +module.exports = { + printers: { + switchTag: printSwitch + } +}; From 9b892f6fb5b8dc356b0a58e6258afc41c997c630 Mon Sep 17 00:00:00 2001 From: tbartel Date: Wed, 26 Feb 2020 18:11:54 +0100 Subject: [PATCH 09/10] Add documentation for twigMultiTags --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4b98121f..22785b05 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ An array containing file paths to plugin directories. This can be used to add yo The paths are relative paths, seen from the project root. Example: -``` +```json "twigMelodyPlugins": ["src-js/some-melody-plugin", "src-js/some-other-plugin"] ``` @@ -54,16 +54,17 @@ Because Twig files might have a lot of nesting, it can be useful to define a sep If set to `true`, objects will always be wrapped/broken, even if they would fit on one line: -``` -
    -
    +} | classes }}" +> ``` If set to `false` (default value), this would be printed as: -``` +```html
    ``` @@ -75,6 +76,41 @@ Follow the standards described in [https://twig.symfony.com/doc/2.x/coding_stand Choose whether to output the block name in `{% endblock %}` tags (e.g., `{% endblock content %}`) or not. The default is not to output it. +### twigMultiTags (default: `[]`) + +An array of coherent sequences of non-standard Twig tags that should be treated as belonging together. Example (inspired by [Craft CMS](https://docs.craftcms.com/v2/templating/nav.html)): + +```json +twigMultiTags: [ + "nav,endnav", + "switch,case,default,endswitch", + "ifchildren,endifchildren", + "cache,endcache" +] +``` + +Looking at the case of `nav,endnav`, this means that the Twig tags `{% nav %}` and `{% endnav %}` will be treated as a pair, and everything in between will be indented: + +```twig +{% nav entry in entries %} +
  • + {{ entry.title }} +
  • +{% endnav %} +``` + +If we did not list the `"nav,endnav"` entry in `twigMultiTags`, this code example would be printed without indentation, because `{% nav %}` and `{% endnav %}` would be treated as unrelated, individual Twig tags: + +```twig +{% nav entry in entries %} +
  • + {{ entry.title }} +
  • +{% endnav %} +``` + +Note that the order matters: It has to be `"nav,endnav"`, and it must not be `"endnav,nav"`. In general, the first and the last tag name matter. In the case of `"switch,case,default,endswitch"`, the order of `case` and `default` does not matter. However, `switch` has to come first, and `endswitch` has to come last. + ## Features ### `prettier-ignore` and `prettier-ignore-start` From 57a22ec2316ae8bb4da8ad65cd4b828bd253c5a4 Mon Sep 17 00:00:00 2001 From: tbartel Date: Sun, 1 Mar 2020 07:37:53 +0100 Subject: [PATCH 10/10] Use latest publicly available Melody version 1.7.1 --- package.json | 8 ++++---- yarn.lock | 42 +++++++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 8715ebfd..ab33b3a3 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ }, "dependencies": { "babel-types": "^6.26.0", - "melody-extension-core": "^1.6.0", - "melody-parser": "^1.6.0", - "melody-traverse": "^1.6.0", - "melody-types": "^1.6.0", + "melody-extension-core": "^1.7.1", + "melody-parser": "^1.7.1", + "melody-traverse": "^1.7.1", + "melody-types": "^1.7.1", "prettier": "^1.8.2", "resolve": "^1.12.0" }, diff --git a/yarn.lock b/yarn.lock index 429c1b8d..1a021c76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2484,41 +2484,41 @@ math-random@^1.0.1: resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== -melody-code-frame@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/melody-code-frame/-/melody-code-frame-1.6.0.tgz#b707269bf4cd252ec7e80849753135c73ec4ea73" - integrity sha512-aKMRTazQ6kJgJiJ6SpJ3h1oE6vLu7gCsAHwdEyqaD19GK+hbHGH/9AA3MY+YkDdH+70fT2+QV6SFxkaYXCg2eg== +melody-code-frame@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/melody-code-frame/-/melody-code-frame-1.7.1.tgz#b982c6ee71b095e46a66467271bdbc5f17b6968f" + integrity sha512-DiGFCEkcRkWNYIW/z/mLB9J9b1s3DyXtOU9hPex3xJQ8d2vULQ0nNYty/Rok7GFDG8oATnFYiz99BIcclVHhIg== dependencies: lodash "^4.15.0" -melody-extension-core@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/melody-extension-core/-/melody-extension-core-1.6.0.tgz#5c3a7d65f701ce916c0a17f87772c1a31e06babe" - integrity sha512-hUJSK3dRkwA6JJCTaeW6mzkPj8MZKiAnThq/qbheiNHtFRZ7UiAZL8aCVDTDuuDsYaevqV6Y2TEy6r1zEpikFQ== +melody-extension-core@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/melody-extension-core/-/melody-extension-core-1.7.1.tgz#a8c05967a67a1da970929cd3e7ad4437a7db6c98" + integrity sha512-kfFav/qLJt0EQMuImv4M+NoODBmRl8VS7Vexw7jYotOdV0gh/JvECxyCZ2mNMMYbDtsD0Az7R9Zi4q//N07eyw== dependencies: babel-template "^6.8.0" babel-types "^6.8.1" lodash "^4.12.0" shortid "^2.2.6" -melody-parser@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/melody-parser/-/melody-parser-1.6.0.tgz#f33b163ddad236068f6170182da1db8adc085a72" - integrity sha512-DojSKRcylqhz8sQ0oUn7ZS40CwM0dHtWcw/9Fd2BA4grhQa93QiKEfZSxE3kkvE6PzMRJ8fakXITrzv2wF7V7w== +melody-parser@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/melody-parser/-/melody-parser-1.7.1.tgz#7a163958b0e0f0d58f654634c4a4e6f86d7d9390" + integrity sha512-7+ci6QIDyPRCBUmaBism9f/S93Y9aZQODmn1l4uTa8eIHk/Xl6VlkaceOwExl/7ObYvY08tBZSCV6WxbiG4Eug== dependencies: he "^1.1.0" lodash "^4.12.0" - melody-code-frame "^1.6.0" + melody-code-frame "1.7.1" -melody-traverse@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/melody-traverse/-/melody-traverse-1.6.0.tgz#fc249c7282ed93533e6a4224fdd98fdf8f4b3e9c" - integrity sha512-fq3pBA1n44YOEFLgKQ1kuVlj7DfbuLo6EyhtPF23To6McyePSJg1nmzfmAuQK2AZLgQBb4JhpwrPXX81TxafCA== +melody-traverse@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/melody-traverse/-/melody-traverse-1.7.1.tgz#a764bfb1b49bf32aba62811e1196532d11756612" + integrity sha512-Gz58x8O1TD7Qn7tEjzHXTuQywWxPXUrQRRUtZNRPfDsKPDyLCDJw+UF8pmd818D+d3so1/M9miinYuMueBM0rA== -melody-types@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/melody-types/-/melody-types-1.6.0.tgz#d5fd1aedfcab723363505d87213b564eedeb8198" - integrity sha512-79cjEO5uJAHIC8n73EDZeNvdzNnqypsDBpo6WvSB7l4ENGIeYjjACeciur/o1TIz16Io064RsNyxyxRMbAwSYQ== +melody-types@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/melody-types/-/melody-types-1.7.1.tgz#eb3d109a6f6a53bf3cbd6d73d1365553f287c1c6" + integrity sha512-gxVut1tEDSTdJV6n+x2HIlWnZlXnwf6RgbWPtaCdvyuX/tKI6LVRayjbT9L9U0u01UPqXXF2Iu6j3O5Zmqj8ig== dependencies: babel-types "^6.8.1"