├── .babelrc ├── .eslintrc.yml ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.eslint.config.js ├── jest.test.config.js ├── out ├── index-esm.js ├── index-esm.js.map ├── index.js └── index.js.map ├── package.json ├── plugins.md ├── src ├── embed.js ├── index.js ├── melody-extension-core │ ├── LICENSE │ ├── package.json │ └── src │ │ ├── index.js │ │ ├── operators.js │ │ ├── parser │ │ ├── autoescape.js │ │ ├── block.js │ │ ├── do.js │ │ ├── embed.js │ │ ├── extends.js │ │ ├── filter.js │ │ ├── flush.js │ │ ├── for.js │ │ ├── from.js │ │ ├── if.js │ │ ├── import.js │ │ ├── include.js │ │ ├── macro.js │ │ ├── mount.js │ │ ├── set.js │ │ ├── spaceless.js │ │ ├── url.js │ │ ├── use.js │ │ └── with.js │ │ ├── types.js │ │ └── visitors │ │ ├── filters.js │ │ ├── for.js │ │ ├── functions.js │ │ └── tests.js ├── melody-parser │ ├── LICENSE │ ├── README.md │ ├── package.json │ └── src │ │ ├── Associativity.js │ │ ├── CharStream.js │ │ ├── GenericMultiTagParser.js │ │ ├── GenericTagParser.js │ │ ├── Lexer.js │ │ ├── Parser.js │ │ ├── TokenStream.js │ │ ├── TokenTypes.js │ │ ├── elementInfo.js │ │ ├── index.js │ │ └── util.js ├── parser.js ├── print │ ├── AliasExpression.js │ ├── ArrayExpression.js │ ├── Attribute.js │ ├── AutoescapeBlock.js │ ├── BinaryExpression.js │ ├── BlockStatement.js │ ├── CallExpression.js │ ├── ConditionalExpression.js │ ├── Declaration.js │ ├── DoStatement.js │ ├── Element.js │ ├── EmbedStatement.js │ ├── ExpressionStatement.js │ ├── ExtendsStatement.js │ ├── FilterBlockStatement.js │ ├── FilterExpression.js │ ├── FlushStatement.js │ ├── ForStatement.js │ ├── FromStatement.js │ ├── GenericToken.js │ ├── GenericTwigTag.js │ ├── HtmlComment.js │ ├── Identifier.js │ ├── IfStatement.js │ ├── ImportDeclaration.js │ ├── IncludeStatement.js │ ├── MacroDeclarationStatement.js │ ├── MemberExpression.js │ ├── MountStatement.js │ ├── NamedArgumentExpression.js │ ├── ObjectExpression.js │ ├── ObjectProperty.js │ ├── SequenceExpression.js │ ├── SetStatement.js │ ├── SliceExpression.js │ ├── SpacelessBlock.js │ ├── StringLiteral.js │ ├── TestExpression.js │ ├── TextStatement.js │ ├── TwigComment.js │ ├── UnaryExpression.js │ ├── UnarySubclass.js │ ├── UrlStatement.js │ ├── UseStatement.js │ ├── VariableDeclarationStatement.js │ └── WithStatement.js ├── printer.js └── util │ ├── index.js │ ├── pluginUtil.js │ ├── prettier-doc-builders.js │ ├── printFunctions.js │ ├── publicFunctions.js │ └── publicSymbols.js ├── tests ├── Comments │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── htmlComments.melody.twig │ ├── jsfmt.spec.js │ └── twigComments.melody.twig ├── ConstantValue │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── constant-value-int.melody.twig │ ├── constant-value-string.melody.twig │ ├── jsfmt.spec.js │ └── special-cases.melody.twig ├── ControlStructures │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── for.melody.twig │ ├── forIfElse.melody.twig │ ├── forInclude.melody.twig │ ├── forWithBlock.melody.twig │ ├── if.melody.twig │ └── jsfmt.spec.js ├── Declaration │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── doctype.melody.twig │ └── jsfmt.spec.js ├── Element │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── attributes.melody.twig │ ├── breakingSiblings.melody.twig │ ├── children.melody.twig │ ├── emptyLines.melody.twig │ ├── extraSpaces.melody.twig │ ├── jsfmt.spec.js │ ├── manyAttributes.melody.twig │ ├── oneLine.melody.twig │ ├── selfClosing.melody.twig │ ├── siblings.melody.twig │ └── whitespace.melody.twig ├── Expressions │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── arrayExpression.melody.twig │ ├── binaryExpressions.melody.twig │ ├── callExpression.melody.twig │ ├── conditionalExpression.melody.twig │ ├── filterExpression.melody.twig │ ├── jsfmt.spec.js │ ├── memberExpression.melody.twig │ ├── objectExpression.melody.twig │ ├── operators.melody.twig │ ├── stringConcat.melody.twig │ ├── stringLiteral.melody.twig │ └── unaryNot.melody.twig ├── Failing │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── controversial.melody.twig │ ├── failing.melody.twig │ └── jsfmt.spec.js ├── GenericTagCustomPrint │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── jsfmt.spec.js │ └── switch.melody.twig ├── GenericTags │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── cache.melody.twig │ ├── header.melody.twig │ ├── includeCssFile.melody.twig │ ├── jsfmt.spec.js │ ├── nav.melody.twig │ ├── paginate.melody.twig │ ├── redirect.melody.twig │ └── switch.melody.twig ├── IncludeEmbed │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── block.melody.twig │ ├── embed.melody.twig │ ├── extendsEmbed.melody.twig │ ├── import.melody.twig │ ├── include.melody.twig │ ├── jsfmt.spec.js │ ├── mount.melody.twig │ └── useStatement.melody.twig ├── Options │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── alwaysBreakObjects.melody.twig │ ├── endblockName.melody.twig │ ├── jsfmt.spec.js │ └── printWidth.melody.twig ├── PrettierIgnore │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── jsfmt.spec.js │ ├── prettierIgnore.melody.twig │ └── prettierIgnoreStartEnd.melody.twig ├── Statements │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── autoescape.melody.twig │ ├── do.melody.twig │ ├── filter.melody.twig │ ├── flush.melody.twig │ ├── jsfmt.spec.js │ ├── macro.melody.twig │ ├── set.melody.twig │ └── spaceless.melody.twig ├── TwigCodingStandards │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── jsfmt.spec.js │ └── twigCodingStandards.melody.twig ├── Whitespace │ ├── __snapshots__ │ │ └── jsfmt.spec.js.snap │ ├── element.melody.twig │ └── jsfmt.spec.js └── switch-plugin │ └── index.js ├── tests_config ├── raw-serializer.js └── run_spec.js └── whitespace.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - eslint:recommended 4 | - plugin:prettier/recommended 5 | - plugin:jest/recommended 6 | plugins: 7 | - import 8 | root: true 9 | env: 10 | es6: true 11 | node: true 12 | jest: true 13 | rules: 14 | curly: error 15 | import/no-extraneous-dependencies: 16 | - error 17 | - devDependencies: ["tests*/**", "scripts/**"] 18 | no-else-return: error 19 | no-inner-declarations: error 20 | no-unneeded-ternary: error 21 | no-debugger: off 22 | no-unused-vars: off 23 | no-console: off 24 | no-useless-return: error 25 | no-var: error 26 | one-var: 27 | - error 28 | - never 29 | prefer-arrow-callback: error 30 | prefer-const: error 31 | react/no-deprecated: off 32 | strict: off 33 | symbol-description: error 34 | yoda: 35 | - error 36 | - never 37 | - exceptRange: true 38 | overrides: 39 | - files: "tests/**/*.js" 40 | rules: 41 | strict: off 42 | globals: 43 | run_spec: true 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: ['master'] 4 | 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '20' 13 | - run: npm install 14 | - uses: JS-DevTools/npm-publish@v3 15 | with: 16 | token: ${{ secrets.NPM_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npmrc 3 | *.log 4 | /test.py 5 | /.vscode 6 | .DS_Store 7 | coverage 8 | .idea 9 | __pycache__/ 10 | *.pyc 11 | test_files 12 | tests/prettier_* 13 | yarn.lock 14 | test.js 15 | yarn.lock 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 5000, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /jest.eslint.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | runner: "jest-runner-eslint", 5 | displayName: "lint", 6 | testMatch: ["/**/*.js"], 7 | testPathIgnorePatterns: ["node_modules/"] 8 | }; 9 | -------------------------------------------------------------------------------- /jest.test.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ENABLE_COVERAGE = false; // !!process.env.CI; 4 | 5 | module.exports = { 6 | displayName: "test", 7 | setupFiles: ["/tests_config/run_spec.js"], 8 | snapshotSerializers: ["/tests_config/raw-serializer.js"], 9 | testRegex: "jsfmt\\.spec\\.js$|__tests__/.*\\.js$", 10 | collectCoverage: ENABLE_COVERAGE, 11 | collectCoverageFrom: ["src/**/*.js", "!/node_modules/"], 12 | transform: {} 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier-plugin-django", 3 | "version": "0.5.18", 4 | "description": "Prettier Plugin for Django/Twig/Melody, Django template", 5 | "repository": "https://github.com/junstyle/prettier-plugin-django", 6 | "author": "junstyle", 7 | "license": "Apache-2.0", 8 | "files": [ 9 | "out/" 10 | ], 11 | "engines": { 12 | "node": ">=6" 13 | }, 14 | "main": "./out/index.js", 15 | "jsnext:main": "./out/index.esm.js", 16 | "module": "./out/index.esm.js", 17 | "scripts": { 18 | "prepublishOnly": "npm run build", 19 | "prettier": "prettier --plugin=. --parser=melody", 20 | "build": "npm run esbuild && npm run esbuild-es", 21 | "esbuild": "esbuild ./src/index.js --bundle --minify --sourcemap --keep-names --outfile=out/index.js --format=cjs --platform=node", 22 | "esbuild-es": "esbuild ./src/index.js --bundle --minify --sourcemap --keep-names --outfile=out/index-esm.js --format=esm --platform=node", 23 | "watch": "npm run -S esbuild -- --sourcemap --watch" 24 | }, 25 | "dependencies": { 26 | "babel-template": "^6.26.0", 27 | "entities": "^4.5.0", 28 | "melody-code-frame": "^1.7.5", 29 | "melody-idom": "^1.7.5", 30 | "melody-traverse": "^1.7.5", 31 | "melody-types": "^1.7.5", 32 | "resolve": "^1.22.8" 33 | }, 34 | "devDependencies": { 35 | "esbuild": "^0.21.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins for prettier-plugin-django 2 | 3 | Since the Melody parser might be extended through custom Twig syntax, it can become necessary to extend the Prettier plugin, as well. Such plugins have to fulfill certain requirements. 4 | 5 | ## Finding plugins 6 | 7 | In order for `prettier-plugin-django` to find a plugin, there are the following ways: 8 | 9 | ### Prettier option 10 | 11 | Use the option `prettier-plugin-django-plugins` to pass a list of directories that hold the plugins you want to have loaded. 12 | 13 | In `.prettierrc.json`: 14 | 15 | ```json 16 | { 17 | "printWidth": 80, 18 | "tabWidth": 4, 19 | "prettier-plugin-django-plugins": [ 20 | "src/@my-namespace/plugin1", 21 | "src/@my-namespace/plugin2" 22 | ] 23 | } 24 | ``` 25 | 26 | ### Naming convention 27 | 28 | tbd 29 | 30 | ## Plugin structure 31 | 32 | A plugin has to export an object of the following shape: 33 | 34 | ``` 35 | module.exports = { 36 | melodyExtensions: [ 37 | ext1, 38 | ext2, 39 | ... 40 | ], 41 | printers: { 42 | nodeTypeName1: printNodeType1, 43 | nodeTypeName2: printNodeType2, 44 | ... 45 | } 46 | }; 47 | ``` 48 | -------------------------------------------------------------------------------- /src/embed.js: -------------------------------------------------------------------------------- 1 | import { Element } from 'melody-types' 2 | import { printOpeningTag } from './print/Element' 3 | import { concat, dedent, group, hardline, indent, softline } from './util/prettier-doc-builders' 4 | 5 | export function embed(path, print, textToDoc, options) { 6 | const node = path.getValue() 7 | if (options.embeddedLanguageFormatting == 'auto' && node instanceof Element) { 8 | let tagName = node.name.toLowerCase() 9 | if (tagName == 'script' || tagName == 'style') { 10 | let parser = tagName == 'script' ? 'babel' : 'css' 11 | let { value } = node.children?.[0].value 12 | if (value) { 13 | let opening = group(printOpeningTag(node, path, print)) 14 | let children = indent([softline, textToDoc(value, { ...options, parser }, { stripTrailingHardline: true })]) 15 | return [opening, children, hardline, concat([''])] 16 | } 17 | } 18 | } 19 | return false 20 | } 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { embed } from './embed.js' 4 | import { parse } from './parser.js' 5 | import { print } from './printer.js' 6 | export * from './util/publicFunctions.js' 7 | export * from './util/publicSymbols.js' 8 | 9 | const languages = [ 10 | { 11 | name: 'melody', 12 | parsers: ['melody'], 13 | group: 'Melody', 14 | tmScope: 'melody.twig', 15 | aceMode: 'html', 16 | codemirrorMode: 'clike', 17 | codemirrorMimeType: 'text/melody-twig', 18 | extensions: ['.melody.twig', '.html.twig', '.twig', '.django', '.jinja'], 19 | linguistLanguageId: 0, 20 | vscodeLanguageIds: ['twig', 'django', 'django-html'] 21 | } 22 | ] 23 | 24 | function hasPragma(/* text */) { 25 | return false 26 | } 27 | 28 | function locStart(/* node */) { 29 | return -1 30 | } 31 | 32 | function locEnd(/* node */) { 33 | return -1 34 | } 35 | 36 | const parsers = { 37 | melody: { 38 | parse, 39 | astFormat: 'melody', 40 | hasPragma, 41 | locStart, 42 | locEnd 43 | } 44 | } 45 | 46 | function canAttachComment(node) { 47 | return node.ast_type && node.ast_type !== 'comment' 48 | } 49 | 50 | function printComment(commentPath) { 51 | const comment = commentPath.getValue() 52 | 53 | switch (comment.ast_type) { 54 | case 'comment': 55 | return comment.value 56 | default: 57 | throw new Error('Not a comment: ' + JSON.stringify(comment)) 58 | } 59 | } 60 | 61 | function clean(ast, newObj) { 62 | delete newObj.lineno 63 | delete newObj.col_offset 64 | } 65 | 66 | const printers = { 67 | melody: { 68 | print, 69 | // hasPrettierIgnore, 70 | embed, 71 | printComment, 72 | canAttachComment, 73 | massageAstNode: clean, 74 | willPrintOwnComments: () => true 75 | } 76 | } 77 | 78 | const options = { 79 | twigMelodyPlugins: { 80 | type: 'path', 81 | category: 'Global', 82 | array: true, 83 | default: [{ value: [] }], 84 | description: 'Provide additional plugins for Melody. Relative file path from the project root.' 85 | }, 86 | twigMultiTags: { 87 | type: 'path', 88 | category: 'Global', 89 | array: true, 90 | default: [{ value: [] }], 91 | description: 'Make custom Twig tags known to the parser.' 92 | }, 93 | twigSingleQuote: { 94 | type: 'boolean', 95 | category: 'Global', 96 | default: true, 97 | description: 'Use single quotes in Twig files?' 98 | }, 99 | twigAlwaysBreakObjects: { 100 | type: 'boolean', 101 | category: 'Global', 102 | default: true, 103 | description: 'Should objects always break in Twig files?' 104 | }, 105 | twigPrintWidth: { 106 | type: 'int', 107 | category: 'Global', 108 | default: 80, 109 | description: 'Print width for Twig files' 110 | }, 111 | twigFollowOfficialCodingStandards: { 112 | type: 'boolean', 113 | category: 'Global', 114 | default: true, 115 | description: 'See https://twig.symfony.com/doc/2.x/coding_standards.html' 116 | }, 117 | twigOutputEndblockName: { 118 | type: 'boolean', 119 | category: 'Global', 120 | default: false, 121 | description: "Output the Twig block name in the 'endblock' tag" 122 | }, 123 | templateType: { 124 | type: 'string', 125 | category: 'Global', 126 | default: 'twig', 127 | description: "template type, such as django, twig" 128 | }, 129 | } 130 | 131 | // This exports defines the Prettier plugin 132 | // See https://github.com/prettier/prettier/blob/master/docs/plugins.md 133 | export { languages, options, parsers, printers } 134 | 135 | -------------------------------------------------------------------------------- /src/melody-extension-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "melody-extension-core", 3 | "version": "1.7.5", 4 | "description": "", 5 | "main": "./lib/index.js", 6 | "jsnext:main": "./lib/index.esm.js", 7 | "scripts": { 8 | "build": "mkdir lib; SUPPORT_CJS=true rollup -c ../../rollup.config.js -i src/index.js" 9 | }, 10 | "author": "", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "babel-template": "^6.8.0", 14 | "babel-types": "^6.8.1", 15 | "lodash": "^4.12.0", 16 | "shortid": "^2.2.6" 17 | }, 18 | "peerDependencies": { 19 | "melody-idom": "^1.1.0", 20 | "melody-parser": "^1.1.0", 21 | "melody-runtime": "^1.1.0", 22 | "melody-traverse": "^1.1.0", 23 | "melody-types": "^1.1.0" 24 | }, 25 | "gitHead": "623a490f999fdad0c11e76d676ae7faa14f569e5" 26 | } 27 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { binaryOperators, tests, unaryOperators } from './operators'; 17 | import { AutoescapeParser } from './parser/autoescape'; 18 | import { BlockParser } from './parser/block'; 19 | import { DoParser } from './parser/do'; 20 | import { EmbedParser } from './parser/embed'; 21 | import { ExtendsParser } from './parser/extends'; 22 | import { FilterParser } from './parser/filter'; 23 | import { FlushParser } from './parser/flush'; 24 | import { ForParser } from './parser/for'; 25 | import { FromParser } from './parser/from'; 26 | import { IfParser } from './parser/if'; 27 | import { ImportParser } from './parser/import'; 28 | import { IncludeParser } from './parser/include'; 29 | import { MacroParser } from './parser/macro'; 30 | import { MountParser } from './parser/mount'; 31 | import { SetParser } from './parser/set'; 32 | import { SpacelessParser } from './parser/spaceless'; 33 | import { UrlParser } from './parser/url'; 34 | import { UseParser } from './parser/use'; 35 | import { WithParser } from './parser/with'; 36 | 37 | // import forVisitor from './visitors/for'; 38 | // import testVisitor from './visitors/tests'; 39 | // import filters from './visitors/filters'; 40 | // import functions from './visitors/functions'; 41 | 42 | // const filterMap = [ 43 | // 'attrs', 44 | // 'classes', 45 | // 'styles', 46 | // 'batch', 47 | // 'escape', 48 | // 'format', 49 | // 'merge', 50 | // 'nl2br', 51 | // 'number_format', 52 | // 'raw', 53 | // 'replace', 54 | // 'reverse', 55 | // 'round', 56 | // 'striptags', 57 | // 'title', 58 | // 'url_encode', 59 | // 'trim', 60 | // ].reduce((map, filterName) => { 61 | // map[filterName] = 'melody-runtime'; 62 | // return map; 63 | // }, Object.create(null)); 64 | 65 | // Object.assign(filterMap, filters); 66 | 67 | // const functionMap = [ 68 | // 'attribute', 69 | // 'constant', 70 | // 'cycle', 71 | // 'date', 72 | // 'max', 73 | // 'min', 74 | // 'random', 75 | // 'range', 76 | // 'source', 77 | // 'template_from_string', 78 | // ].reduce((map, functionName) => { 79 | // map[functionName] = 'melody-runtime'; 80 | // return map; 81 | // }, Object.create(null)); 82 | // Object.assign(functionMap, functions); 83 | 84 | export const extension = { 85 | tags: [ 86 | AutoescapeParser, 87 | BlockParser, 88 | DoParser, 89 | EmbedParser, 90 | ExtendsParser, 91 | FilterParser, 92 | FlushParser, 93 | ForParser, 94 | FromParser, 95 | IfParser, 96 | ImportParser, 97 | IncludeParser, 98 | UrlParser, 99 | WithParser, 100 | MacroParser, 101 | SetParser, 102 | SpacelessParser, 103 | UseParser, 104 | MountParser, 105 | ], 106 | unaryOperators, 107 | binaryOperators, 108 | tests, 109 | // visitors: [forVisitor, testVisitor], 110 | // filterMap, 111 | // functionMap, 112 | }; 113 | 114 | export { 115 | AliasExpression, AutoescapeBlock, BinaryAddExpression, BinaryAndExpression, BinaryDivExpression, BinaryEndsWithExpression, BinaryEqualsExpression, BinaryFloorDivExpression, BinaryGreaterThanExpression, BinaryGreaterThanOrEqualExpression, BinaryInExpression, BinaryLessThanExpression, BinaryLessThanOrEqualExpression, BinaryMatchesExpression, BinaryModExpression, BinaryMulExpression, BinaryNotEqualsExpression, BinaryNotInExpression, BinaryNullCoalesceExpression, BinaryOrExpression, BinaryPowerExpression, BinaryRangeExpression, BinaryStartsWithExpression, BitwiseAndExpression, BitwiseOrExpression, 116 | BitwiseXorExpression, BlockCallExpression, BlockStatement, DoStatement, 117 | EmbedStatement, 118 | ExtendsStatement, 119 | FilterBlockStatement, 120 | FlushStatement, 121 | ForStatement, FromStatement, 122 | IfStatement, ImportDeclaration, IncludeStatement, MacroDeclarationStatement, MountStatement, SetStatement, 123 | SpacelessBlock, TestConstantExpression, TestDefinedExpression, TestDivisibleByExpression, TestEmptyExpression, TestEvenExpression, TestIterableExpression, TestNullExpression, TestOddExpression, TestSameAsExpression, UnaryNeqExpression, UnaryNotExpression, UnaryPosExpression, UrlStatement, UseStatement, VariableDeclarationStatement, WithStatement 124 | } from './types'; 125 | 126 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/autoescape.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Types, hasTagEndTokenTrimRight, hasTagStartTokenTrimLeft, setEndFromToken, setStartFromToken } from '../../../melody-parser/src/' 17 | import { AutoescapeBlock } from './../types' 18 | 19 | export const AutoescapeParser = { 20 | name: 'autoescape', 21 | parse(parser, token) { 22 | const tokens = parser.tokens 23 | const tagStartToken = tokens.la(-2) 24 | 25 | let escapeType = null, 26 | stringStartToken, 27 | openingTagEndToken, 28 | closingTagStartToken 29 | if (tokens.nextIf(Types.TAG_END)) { 30 | openingTagEndToken = tokens.la(-1) 31 | escapeType = null 32 | } else if ((stringStartToken = tokens.nextIf(Types.STRING_START))) { 33 | escapeType = tokens.expect(Types.STRING).text 34 | if (!tokens.nextIf(Types.STRING_END)) { 35 | parser.error({ 36 | title: 'autoescape type declaration must be a simple string', 37 | pos: tokens.la(0).pos, 38 | advice: `The type declaration for autoescape must be a simple string such as 'html' or 'js'. 39 | I expected the current string to end with a ${stringStartToken.text} but instead found ${Types.ERROR_TABLE[tokens.lat(0)] || tokens.lat(0)}.` 40 | }) 41 | } 42 | openingTagEndToken = tokens.la(0) 43 | } else if (tokens.nextIf(Types.FALSE)) { 44 | escapeType = false 45 | openingTagEndToken = tokens.la(0) 46 | } else if (tokens.nextIf(Types.TRUE)) { 47 | escapeType = true 48 | openingTagEndToken = tokens.la(0) 49 | } else if (tokens.nextIf(Types.SYMBOL, "on")) { 50 | escapeType = 'on' 51 | openingTagEndToken = tokens.la(0) 52 | } else if (tokens.nextIf(Types.SYMBOL, "off")) { 53 | escapeType = 'off' 54 | openingTagEndToken = tokens.la(0) 55 | } else { 56 | parser.error({ 57 | title: 'Invalid autoescape type declaration', 58 | pos: tokens.la(0).pos, 59 | advice: `Expected type of autoescape to be a string, boolean or not specified. Found ${tokens.la(0).type} instead.` 60 | }) 61 | } 62 | 63 | const autoescape = new AutoescapeBlock(escapeType) 64 | setStartFromToken(autoescape, token) 65 | let tagEndToken 66 | autoescape.expressions = parser.parse((_, token, tokens) => { 67 | if (token.type === Types.TAG_START && tokens.nextIf(Types.SYMBOL, 'endautoescape')) { 68 | closingTagStartToken = token 69 | tagEndToken = tokens.expect(Types.TAG_END, '', tagStartToken) 70 | return true 71 | } 72 | return false 73 | }).expressions 74 | setEndFromToken(autoescape, tagEndToken) 75 | 76 | autoescape.trimRightAutoescape = hasTagEndTokenTrimRight(openingTagEndToken) 77 | autoescape.trimLeftEndautoescape = hasTagStartTokenTrimLeft(closingTagStartToken) 78 | 79 | return autoescape 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/block.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier, PrintExpressionStatement } from 'melody-types' 17 | import { Types, setStartFromToken, setEndFromToken, createNode, hasTagStartTokenTrimLeft, hasTagEndTokenTrimRight } from '../../../melody-parser/src/' 18 | import { BlockStatement } from './../types' 19 | 20 | export const BlockParser = { 21 | name: 'block', 22 | parse(parser, token) { 23 | const tokens = parser.tokens, 24 | tagStartToken = tokens.la(-2), 25 | nameToken = tokens.expect(Types.SYMBOL) 26 | 27 | let blockStatement, openingTagEndToken, closingTagStartToken 28 | if ((openingTagEndToken = tokens.nextIf(Types.TAG_END))) { 29 | blockStatement = new BlockStatement( 30 | createNode(Identifier, nameToken, nameToken.text), 31 | parser.parse((tokenText, token, tokens) => { 32 | const result = !!(token.type === Types.TAG_START && tokens.nextIf(Types.SYMBOL, 'endblock')) 33 | if (result) { 34 | closingTagStartToken = token 35 | } 36 | return result 37 | }).expressions 38 | ) 39 | 40 | if (tokens.nextIf(Types.SYMBOL, nameToken.text)) { 41 | if (tokens.lat(0) !== Types.TAG_END) { 42 | const unexpectedToken = tokens.next() 43 | parser.error({ 44 | title: 'Block name mismatch', 45 | pos: unexpectedToken.pos, 46 | advice: unexpectedToken.type == Types.SYMBOL ? `Expected end of block ${nameToken.text} but instead found end of block ${tokens.la(0).text}.` : `endblock must be followed by either '%}' or the name of the open block. Found a token of type ${Types.ERROR_TABLE[unexpectedToken.type] || unexpectedToken.type} instead.` 47 | }) 48 | } 49 | } 50 | } else { 51 | blockStatement = new BlockStatement(createNode(Identifier, nameToken, nameToken.text), new PrintExpressionStatement(parser.matchExpression())) 52 | } 53 | 54 | setStartFromToken(blockStatement, token) 55 | setEndFromToken(blockStatement, tokens.expect(Types.TAG_END, null, tagStartToken)) 56 | 57 | blockStatement.trimRightBlock = openingTagEndToken && hasTagEndTokenTrimRight(openingTagEndToken) 58 | blockStatement.trimLeftEndblock = !!(closingTagStartToken && hasTagStartTokenTrimLeft(closingTagStartToken)) 59 | 60 | return blockStatement 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/do.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Types, setStartFromToken, setEndFromToken } from '../../../melody-parser/src/' 17 | import { DoStatement } from './../types' 18 | 19 | export const DoParser = { 20 | name: 'do', 21 | parse(parser, token) { 22 | const tokens = parser.tokens, 23 | tagStartToken = tokens.la(-2), 24 | doStatement = new DoStatement(parser.matchExpression()) 25 | setStartFromToken(doStatement, token) 26 | setEndFromToken(doStatement, tokens.expect(Types.TAG_END, '', tagStartToken)) 27 | return doStatement 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/embed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Node } from 'melody-types' 17 | import { Types, setStartFromToken, setEndFromToken, hasTagStartTokenTrimLeft, hasTagEndTokenTrimRight } from '../../../melody-parser/src/' 18 | import filter from 'lodash/filter' 19 | import { EmbedStatement } from './../types' 20 | 21 | export const EmbedParser = { 22 | name: 'embed', 23 | parse(parser, token) { 24 | const tokens = parser.tokens 25 | const tagStartToken = tokens.la(-2) 26 | 27 | const embedStatement = new EmbedStatement(parser.matchExpression()) 28 | 29 | if (tokens.nextIf(Types.SYMBOL, 'ignore')) { 30 | tokens.expect(Types.SYMBOL, 'missing') 31 | embedStatement.ignoreMissing = true 32 | } 33 | 34 | if (tokens.nextIf(Types.SYMBOL, 'with')) { 35 | embedStatement.argument = parser.matchExpression() 36 | } 37 | 38 | if (tokens.nextIf(Types.SYMBOL, 'only')) { 39 | embedStatement.contextFree = true 40 | } 41 | 42 | tokens.expect(Types.TAG_END) 43 | const openingTagEndToken = tokens.la(-1) 44 | let closingTagStartToken 45 | 46 | embedStatement.blocks = filter( 47 | parser.parse((tokenText, token, tokens) => { 48 | const result = !!(token.type === Types.TAG_START && tokens.nextIf(Types.SYMBOL, 'endembed')) 49 | if (result) { 50 | closingTagStartToken = token 51 | } 52 | return result 53 | }).expressions, 54 | Node.isBlockStatement 55 | ) 56 | 57 | setStartFromToken(embedStatement, token) 58 | setEndFromToken(embedStatement, tokens.expect(Types.TAG_END, '', tagStartToken)) 59 | 60 | embedStatement.trimRightEmbed = hasTagEndTokenTrimRight(openingTagEndToken) 61 | embedStatement.trimLeftEndembed = closingTagStartToken && hasTagStartTokenTrimLeft(closingTagStartToken) 62 | 63 | return embedStatement 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/extends.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Types, setStartFromToken, setEndFromToken } from '../../../melody-parser/src/' 17 | import { ExtendsStatement } from './../types' 18 | 19 | export const ExtendsParser = { 20 | name: 'extends', 21 | parse(parser, token) { 22 | const tokens = parser.tokens 23 | 24 | const extendsStatement = new ExtendsStatement(parser.matchExpression()) 25 | 26 | setStartFromToken(extendsStatement, token) 27 | setEndFromToken(extendsStatement, tokens.expect(Types.TAG_END)) 28 | 29 | return extendsStatement 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier } from 'melody-types' 17 | import { Types, setStartFromToken, setEndFromToken, createNode, hasTagStartTokenTrimLeft, hasTagEndTokenTrimRight } from '../../../melody-parser/src/' 18 | import { FilterBlockStatement } from './../types' 19 | 20 | export const FilterParser = { 21 | name: 'filter', 22 | parse(parser, token) { 23 | const tokens = parser.tokens, 24 | tagStartToken = tokens.la(-2), 25 | ref = createNode(Identifier, token, 'filter'), 26 | filterExpression = parser.matchFilterExpression(ref) 27 | tokens.expect(Types.TAG_END) 28 | const openingTagEndToken = tokens.la(-1) 29 | let closingTagStartToken 30 | 31 | const body = parser.parse((text, token, tokens) => { 32 | const result = token.type === Types.TAG_START && tokens.nextIf(Types.SYMBOL, 'endfilter') 33 | 34 | if (result) { 35 | closingTagStartToken = token 36 | } 37 | return result 38 | }).expressions 39 | 40 | const filterBlockStatement = new FilterBlockStatement(filterExpression, body) 41 | setStartFromToken(filterBlockStatement, token) 42 | setEndFromToken(filterBlockStatement, tokens.expect(Types.TAG_END, '', tagStartToken)) 43 | 44 | filterBlockStatement.trimRightFilter = hasTagEndTokenTrimRight(openingTagEndToken) 45 | filterBlockStatement.trimLeftEndfilter = closingTagStartToken && hasTagStartTokenTrimLeft(closingTagStartToken) 46 | 47 | return filterBlockStatement 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/flush.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Types, setStartFromToken, setEndFromToken } from '../../../melody-parser/src/' 17 | import { FlushStatement } from './../types' 18 | 19 | export const FlushParser = { 20 | name: 'flush', 21 | parse(parser, token) { 22 | const tokens = parser.tokens, 23 | flushStatement = new FlushStatement() 24 | 25 | setStartFromToken(flushStatement, token) 26 | setEndFromToken(flushStatement, tokens.expect(Types.TAG_END)) 27 | return flushStatement 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/for.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier } from 'melody-types' 17 | import { Types, createNode, hasTagEndTokenTrimRight, hasTagStartTokenTrimLeft, setEndFromToken, setStartFromToken } from '../../../melody-parser/src/' 18 | import { ForStatement } from './../types' 19 | 20 | export const ForParser = { 21 | name: 'for', 22 | parse(parser, token) { 23 | const tokens = parser.tokens, 24 | forStatement = new ForStatement(), 25 | tagStartToken = tokens.la(-2) 26 | 27 | const keyTarget = tokens.expect(Types.SYMBOL) 28 | if (tokens.nextIf(Types.COMMA)) { 29 | forStatement.keyTarget = createNode(Identifier, keyTarget, keyTarget.text) 30 | const valueTarget = tokens.expect(Types.SYMBOL) 31 | forStatement.valueTarget = createNode(Identifier, valueTarget, valueTarget.text) 32 | } else { 33 | forStatement.keyTarget = null 34 | forStatement.valueTarget = createNode(Identifier, keyTarget, keyTarget.text) 35 | } 36 | 37 | tokens.expect(Types.OPERATOR, 'in') 38 | 39 | forStatement.sequence = parser.matchExpression() 40 | 41 | if (tokens.nextIf(Types.SYMBOL, 'if')) { 42 | forStatement.condition = parser.matchExpression() 43 | } 44 | 45 | if (tokens.nextIf(Types.SYMBOL, 'reversed')) { 46 | forStatement.reversed = true 47 | } 48 | 49 | if (tokens.nextIf(Types.SYMBOL, 'sorted')) { 50 | forStatement.sorted = true 51 | } 52 | 53 | tokens.expect(Types.TAG_END) 54 | 55 | const openingTagEndToken = tokens.la(-1) 56 | let elseTagStartToken, elseTagEndToken 57 | 58 | forStatement.body = parser.parse((tokenText, token, tokens) => { 59 | const result = token.type === Types.TAG_START && (tokens.test(Types.SYMBOL, 'else') || tokens.test(Types.SYMBOL, 'empty') || tokens.test(Types.SYMBOL, 'endfor')) 60 | if (result && (tokens.test(Types.SYMBOL, 'else') || tokens.test(Types.SYMBOL, 'empty'))) { 61 | elseTagStartToken = token 62 | } 63 | return result 64 | }) 65 | 66 | if (tokens.nextIf(Types.SYMBOL, 'else') || tokens.nextIf(Types.SYMBOL, 'empty')) { 67 | forStatement.otherwiseText = tokens.la(-1).text 68 | tokens.expect(Types.TAG_END) 69 | elseTagEndToken = tokens.la(-1) 70 | forStatement.otherwise = parser.parse((tokenText, token, tokens) => { 71 | return token.type === Types.TAG_START && tokens.test(Types.SYMBOL, 'endfor') 72 | }) 73 | } 74 | const endforTagStartToken = tokens.la(-1) 75 | tokens.expect(Types.SYMBOL, 'endfor', tagStartToken) 76 | 77 | setStartFromToken(forStatement, token) 78 | setEndFromToken(forStatement, tokens.expect(Types.TAG_END)) 79 | 80 | forStatement.trimRightFor = hasTagEndTokenTrimRight(openingTagEndToken) 81 | forStatement.trimLeftElse = !!(elseTagStartToken && hasTagStartTokenTrimLeft(elseTagStartToken)) 82 | forStatement.trimRightElse = !!(elseTagEndToken && hasTagEndTokenTrimRight(elseTagEndToken)) 83 | forStatement.trimLeftEndfor = hasTagStartTokenTrimLeft(endforTagStartToken) 84 | 85 | return forStatement 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/from.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier } from 'melody-types' 17 | import { Types, setStartFromToken, setEndFromToken, createNode } from '../../../melody-parser/src/' 18 | import { ImportDeclaration, FromStatement } from './../types' 19 | 20 | export const FromParser = { 21 | name: 'from', 22 | parse(parser, token) { 23 | const tokens = parser.tokens, 24 | source = parser.matchExpression(), 25 | imports = [] 26 | 27 | tokens.expect(Types.SYMBOL, 'import') 28 | 29 | do { 30 | const name = tokens.expect(Types.SYMBOL) 31 | 32 | let alias = name 33 | if (tokens.nextIf(Types.SYMBOL, 'as')) { 34 | alias = tokens.expect(Types.SYMBOL) 35 | } 36 | 37 | const importDeclaration = new ImportDeclaration(createNode(Identifier, name, name.text), createNode(Identifier, alias, alias.text)) 38 | setStartFromToken(importDeclaration, name) 39 | setEndFromToken(importDeclaration, alias) 40 | 41 | imports.push(importDeclaration) 42 | 43 | if (!tokens.nextIf(Types.COMMA)) { 44 | break 45 | } 46 | } while (!tokens.test(Types.EOF)) 47 | 48 | const fromStatement = new FromStatement(source, imports) 49 | 50 | setStartFromToken(fromStatement, token) 51 | setEndFromToken(fromStatement, tokens.expect(Types.TAG_END)) 52 | 53 | return fromStatement 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/if.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { hasTagEndTokenTrimRight, hasTagStartTokenTrimLeft, setEndFromToken, setStartFromToken, Types } from '../../../melody-parser/src/' 17 | import { IfStatement } from './../types' 18 | 19 | export const IfParser = { 20 | name: 'if', 21 | parse(parser, token) { 22 | const tokens = parser.tokens 23 | const tagStartToken = tokens.la(-2) 24 | 25 | let test = parser.matchExpression(), 26 | alternate = null 27 | 28 | tokens.expect(Types.TAG_END) 29 | const ifTagEndToken = tokens.la(-1) 30 | 31 | const ifStatement = new IfStatement(test, parser.parse(matchConsequent).expressions) 32 | 33 | let elseTagStartToken, elseTagEndToken, elseifTagStartToken, elseifTagEndToken 34 | 35 | do { 36 | if (tokens.nextIf(Types.SYMBOL, 'else')) { 37 | elseTagStartToken = tokens.la(-2) 38 | tokens.expect(Types.TAG_END) 39 | elseTagEndToken = tokens.la(-1) 40 | ; (alternate || ifStatement).alternate = parser.parse(matchAlternate).expressions 41 | } else if (tokens.nextIf(Types.SYMBOL, 'elseif') || tokens.nextIf(Types.SYMBOL, 'elif')) { 42 | const elseifText = tokens.la(-1).text 43 | elseifTagStartToken = tokens.la(-2) 44 | test = parser.matchExpression() 45 | tokens.expect(Types.TAG_END) 46 | elseifTagEndToken = tokens.la(-1) 47 | const consequent = parser.parse(matchConsequent).expressions 48 | alternate = (alternate || ifStatement).alternate = new IfStatement(test, consequent) 49 | alternate.elseifText = elseifText 50 | alternate.trimLeft = hasTagStartTokenTrimLeft(elseifTagStartToken) 51 | alternate.trimRightIf = hasTagEndTokenTrimRight(elseifTagEndToken) 52 | } 53 | 54 | if (tokens.nextIf(Types.SYMBOL, 'endif')) { 55 | break 56 | } 57 | } while (!tokens.test(Types.EOF)) 58 | 59 | const endifTagStartToken = tokens.la(-2) 60 | 61 | setStartFromToken(ifStatement, token) 62 | setEndFromToken(ifStatement, tokens.expect(Types.TAG_END, '', tagStartToken)) 63 | 64 | ifStatement.trimRightIf = hasTagEndTokenTrimRight(ifTagEndToken) 65 | ifStatement.trimLeftElse = !!(elseTagStartToken && hasTagStartTokenTrimLeft(elseTagStartToken)) 66 | ifStatement.trimRightElse = !!(elseTagEndToken && hasTagEndTokenTrimRight(elseTagEndToken)) 67 | ifStatement.trimLeftEndif = hasTagStartTokenTrimLeft(endifTagStartToken) 68 | 69 | return ifStatement 70 | } 71 | } 72 | 73 | function matchConsequent(tokenText, token, tokens) { 74 | if (token.type === Types.TAG_START) { 75 | const next = tokens.la(0).text 76 | return next === 'else' || next === 'endif' || next === 'elseif' || next == 'elif' 77 | } 78 | return false 79 | } 80 | 81 | function matchAlternate(tokenText, token, tokens) { 82 | return token.type === Types.TAG_START && tokens.test(Types.SYMBOL, 'endif') 83 | } 84 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/import.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier } from 'melody-types' 17 | import { Types, setStartFromToken, setEndFromToken, createNode } from '../../../melody-parser/src/' 18 | import { ImportDeclaration } from './../types' 19 | 20 | export const ImportParser = { 21 | name: 'import', 22 | parse(parser, token) { 23 | const tokens = parser.tokens, 24 | source = parser.matchExpression() 25 | 26 | tokens.expect(Types.SYMBOL, 'as') 27 | const alias = tokens.expect(Types.SYMBOL) 28 | 29 | const importStatement = new ImportDeclaration(source, createNode(Identifier, alias, alias.text)) 30 | 31 | setStartFromToken(importStatement, token) 32 | setEndFromToken(importStatement, tokens.expect(Types.TAG_END)) 33 | 34 | return importStatement 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/include.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as n from 'melody-types' 17 | import { Types, createNode, setEndFromToken, setStartFromToken } from '../../../melody-parser/src/' 18 | import { copyEnd } from '../../../melody-parser/src/util' 19 | import { IncludeStatement } from './../types' 20 | 21 | 22 | export const IncludeParser = { 23 | name: 'include', 24 | parse(parser, token) { 25 | const tokens = parser.tokens 26 | 27 | const includeStatement = new IncludeStatement(parser.matchExpression()) 28 | 29 | if (tokens.nextIf(Types.SYMBOL, 'ignore')) { 30 | tokens.expect(Types.SYMBOL, 'missing') 31 | includeStatement.ignoreMissing = true 32 | } 33 | 34 | if (tokens.nextIf(Types.SYMBOL, 'with')) { 35 | let args = [] 36 | while (!tokens.test(Types.EOF) && !tokens.test(Types.TAG_END)) { 37 | if (tokens.test(Types.SYMBOL) && tokens.lat(1) === Types.ASSIGNMENT) { 38 | const name = tokens.next() 39 | tokens.next() 40 | const value = parser.matchExpression() 41 | const arg = new n.NamedArgumentExpression(createNode(n.Identifier, name, name.text), value) 42 | copyEnd(arg, value) 43 | args.push(arg) 44 | } else if (tokens.test(Types.SYMBOL, 'only')) { 45 | includeStatement.contextFree = true 46 | tokens.next() 47 | break 48 | } else { 49 | // args.push(parser.matchExpression()) 50 | const unexpectedToken = tokens.next() 51 | parser.error({ 52 | title: 'include arguments mismatch', 53 | pos: unexpectedToken.pos, 54 | advice: 'eg: {% include "/path/file.dj" with alpha=1 beta=2 %}' 55 | }) 56 | } 57 | } 58 | if (args.length == 0) { 59 | const unexpectedToken = tokens.next() 60 | parser.error({ 61 | title: 'include arguments mismatch', 62 | pos: unexpectedToken.pos, 63 | advice: 'eg: {% include "/path/file.dj" with alpha=1 beta=2 %}' 64 | }) 65 | } 66 | includeStatement.argument = args 67 | } else if (tokens.nextIf(Types.SYMBOL, 'only')) { 68 | includeStatement.contextFree = true 69 | } 70 | 71 | setStartFromToken(includeStatement, token) 72 | setEndFromToken(includeStatement, tokens.expect(Types.TAG_END)) 73 | 74 | return includeStatement 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/macro.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier } from 'melody-types' 17 | import { Types, setStartFromToken, setEndFromToken, createNode, hasTagStartTokenTrimLeft, hasTagEndTokenTrimRight } from '../../../melody-parser/src/' 18 | import { MacroDeclarationStatement } from './../types' 19 | 20 | export const MacroParser = { 21 | name: 'macro', 22 | parse(parser, token) { 23 | const tokens = parser.tokens 24 | const tagStartToken = tokens.la(-2) 25 | 26 | const nameToken = tokens.expect(Types.SYMBOL) 27 | const args = [] 28 | 29 | tokens.expect(Types.LPAREN) 30 | while (!tokens.test(Types.RPAREN) && !tokens.test(Types.EOF)) { 31 | const arg = tokens.expect(Types.SYMBOL) 32 | args.push(createNode(Identifier, arg, arg.text)) 33 | 34 | if (!tokens.nextIf(Types.COMMA) && !tokens.test(Types.RPAREN)) { 35 | // not followed by comma or rparen 36 | parser.error({ 37 | title: 'Expected comma or ")"', 38 | pos: tokens.la(0).pos, 39 | advice: 'The argument list of a macro can only consist of parameter names separated by commas.' 40 | }) 41 | } 42 | } 43 | tokens.expect(Types.RPAREN) 44 | 45 | const openingTagEndToken = tokens.la(0) 46 | let closingTagStartToken 47 | 48 | const body = parser.parse((tokenText, token, tokens) => { 49 | const result = !!(token.type === Types.TAG_START && tokens.nextIf(Types.SYMBOL, 'endmacro')) 50 | if (result) { 51 | closingTagStartToken = token 52 | } 53 | return result 54 | }) 55 | 56 | if (tokens.test(Types.SYMBOL)) { 57 | var nameEndToken = tokens.next() 58 | if (nameToken.text !== nameEndToken.text) { 59 | parser.error({ 60 | title: `Macro name mismatch, expected "${nameToken.text}" but found "${nameEndToken.text}"`, 61 | pos: nameEndToken.pos 62 | }) 63 | } 64 | } 65 | 66 | const macroDeclarationStatement = new MacroDeclarationStatement(createNode(Identifier, nameToken, nameToken.text), args, body) 67 | 68 | setStartFromToken(macroDeclarationStatement, token) 69 | setEndFromToken(macroDeclarationStatement, tokens.expect(Types.TAG_END, '', tagStartToken)) 70 | 71 | macroDeclarationStatement.trimRightMacro = hasTagEndTokenTrimRight(openingTagEndToken) 72 | macroDeclarationStatement.trimLeftEndmacro = hasTagStartTokenTrimLeft(closingTagStartToken) 73 | 74 | return macroDeclarationStatement 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/mount.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier } from 'melody-types' 17 | import { MountStatement } from '../types' 18 | import { Types, setStartFromToken, setEndFromToken, createNode, hasTagStartTokenTrimLeft, hasTagEndTokenTrimRight } from '../../../melody-parser/src/' 19 | 20 | export const MountParser = { 21 | name: 'mount', 22 | parse(parser, token) { 23 | const tokens = parser.tokens 24 | const tagStartToken = tokens.la(-2) 25 | 26 | let name = null, 27 | source = null, 28 | key = null, 29 | async = false, 30 | delayBy = 0, 31 | argument = null 32 | 33 | if (tokens.test(Types.SYMBOL, 'async')) { 34 | // we might be looking at an async mount 35 | const nextToken = tokens.la(1) 36 | if (nextToken.type === Types.STRING_START) { 37 | async = true 38 | tokens.next() 39 | } 40 | } 41 | 42 | if (tokens.test(Types.STRING_START)) { 43 | source = parser.matchStringExpression() 44 | } else { 45 | const nameToken = tokens.expect(Types.SYMBOL) 46 | name = createNode(Identifier, nameToken, nameToken.text) 47 | if (tokens.nextIf(Types.SYMBOL, 'from')) { 48 | source = parser.matchStringExpression() 49 | } 50 | } 51 | 52 | if (tokens.nextIf(Types.SYMBOL, 'as')) { 53 | key = parser.matchExpression() 54 | } 55 | 56 | if (tokens.nextIf(Types.SYMBOL, 'with')) { 57 | argument = parser.matchExpression() 58 | } 59 | 60 | if (async) { 61 | if (tokens.nextIf(Types.SYMBOL, 'delay')) { 62 | tokens.expect(Types.SYMBOL, 'placeholder') 63 | tokens.expect(Types.SYMBOL, 'by') 64 | delayBy = Number.parseInt(tokens.expect(Types.NUMBER).text, 10) 65 | if (tokens.nextIf(Types.SYMBOL, 's')) { 66 | delayBy *= 1000 67 | } else { 68 | tokens.expect(Types.SYMBOL, 'ms') 69 | } 70 | } 71 | } 72 | 73 | const mountStatement = new MountStatement(name, source, key, argument, async, delayBy) 74 | 75 | let openingTagEndToken, catchTagStartToken, catchTagEndToken, endmountTagStartToken 76 | 77 | if (async) { 78 | tokens.expect(Types.TAG_END) 79 | openingTagEndToken = tokens.la(-1) 80 | 81 | mountStatement.body = parser.parse((tokenText, token, tokens) => { 82 | return token.type === Types.TAG_START && (tokens.test(Types.SYMBOL, 'catch') || tokens.test(Types.SYMBOL, 'endmount')) 83 | }) 84 | 85 | if (tokens.nextIf(Types.SYMBOL, 'catch')) { 86 | catchTagStartToken = tokens.la(-2) 87 | const errorVariableName = tokens.expect(Types.SYMBOL) 88 | mountStatement.errorVariableName = createNode(Identifier, errorVariableName, errorVariableName.text) 89 | tokens.expect(Types.TAG_END) 90 | catchTagEndToken = tokens.la(-1) 91 | mountStatement.otherwise = parser.parse((tokenText, token, tokens) => { 92 | return token.type === Types.TAG_START && tokens.test(Types.SYMBOL, 'endmount') 93 | }) 94 | } 95 | tokens.expect(Types.SYMBOL, 'endmount') 96 | endmountTagStartToken = tokens.la(-2) 97 | } 98 | 99 | setStartFromToken(mountStatement, token) 100 | setEndFromToken(mountStatement, tokens.expect(Types.TAG_END, '', tagStartToken)) 101 | 102 | mountStatement.trimRightMount = !!(openingTagEndToken && hasTagEndTokenTrimRight(openingTagEndToken)) 103 | mountStatement.trimLeftCatch = !!(catchTagStartToken && hasTagStartTokenTrimLeft(catchTagStartToken)) 104 | mountStatement.trimRightCatch = !!(catchTagEndToken && hasTagEndTokenTrimRight(catchTagEndToken)) 105 | mountStatement.trimLeftEndmount = !!(endmountTagStartToken && hasTagStartTokenTrimLeft(endmountTagStartToken)) 106 | 107 | return mountStatement 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/set.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier } from 'melody-types' 17 | import { Types, setStartFromToken, setEndFromToken, createNode, hasTagStartTokenTrimLeft, hasTagEndTokenTrimRight } from '../../../melody-parser/src/' 18 | import { VariableDeclarationStatement, SetStatement } from './../types' 19 | 20 | export const SetParser = { 21 | name: 'set', 22 | parse(parser, token) { 23 | const tokens = parser.tokens, 24 | names = [], 25 | values = [] 26 | 27 | let openingTagEndToken, closingTagStartToken 28 | 29 | do { 30 | const name = tokens.expect(Types.SYMBOL) 31 | names.push(createNode(Identifier, name, name.text)) 32 | } while (tokens.nextIf(Types.COMMA)) 33 | 34 | if (tokens.nextIf(Types.ASSIGNMENT)) { 35 | do { 36 | values.push(parser.matchExpression()) 37 | } while (tokens.nextIf(Types.COMMA)) 38 | } else { 39 | if (names.length !== 1) { 40 | parser.error({ 41 | title: 'Illegal multi-set', 42 | pos: tokens.la(0).pos, 43 | advice: 'When using set with a block, you cannot have multiple targets.' 44 | }) 45 | } 46 | tokens.expect(Types.TAG_END) 47 | openingTagEndToken = tokens.la(-1) 48 | 49 | values[0] = parser.parse((tokenText, token, tokens) => { 50 | const result = !!(token.type === Types.TAG_START && tokens.nextIf(Types.SYMBOL, 'endset')) 51 | if (result) { 52 | closingTagStartToken = token 53 | } 54 | return result 55 | }).expressions 56 | } 57 | 58 | if (names.length !== values.length) { 59 | parser.error({ 60 | title: 'Mismatch of set names and values', 61 | pos: token.pos, 62 | advice: `When using set, you must ensure that the number of 63 | assigned variable names is identical to the supplied values. However, here I've found 64 | ${names.length} variable names and ${values.length} values.` 65 | }) 66 | } 67 | 68 | // now join names and values 69 | const assignments = [] 70 | for (let i = 0, len = names.length; i < len; i++) { 71 | assignments[i] = new VariableDeclarationStatement(names[i], values[i]) 72 | } 73 | 74 | const setStatement = new SetStatement(assignments) 75 | 76 | setStartFromToken(setStatement, token) 77 | setEndFromToken(setStatement, tokens.expect(Types.TAG_END)) 78 | 79 | setStatement.trimRightSet = !!(openingTagEndToken && hasTagEndTokenTrimRight(openingTagEndToken)) 80 | setStatement.trimLeftEndset = !!(closingTagStartToken && hasTagStartTokenTrimLeft(closingTagStartToken)) 81 | 82 | return setStatement 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/spaceless.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Types, setStartFromToken, setEndFromToken, hasTagStartTokenTrimLeft, hasTagEndTokenTrimRight } from '../../../melody-parser/src/' 17 | import { SpacelessBlock } from './../types' 18 | 19 | export const SpacelessParser = { 20 | name: 'spaceless', 21 | parse(parser, token) { 22 | const tokens = parser.tokens 23 | const tagStartToken = tokens.la(-2) 24 | 25 | tokens.expect(Types.TAG_END) 26 | const openingTagEndToken = tokens.la(-1) 27 | let closingTagStartToken 28 | 29 | const body = parser.parse((tokenText, token, tokens) => { 30 | const result = !!(token.type === Types.TAG_START && tokens.nextIf(Types.SYMBOL, 'endspaceless')) 31 | closingTagStartToken = token 32 | return result 33 | }).expressions 34 | 35 | const spacelessBlock = new SpacelessBlock(body) 36 | setStartFromToken(spacelessBlock, token) 37 | setEndFromToken(spacelessBlock, tokens.expect(Types.TAG_END, '', tagStartToken)) 38 | 39 | tokens.expect(Types.SYMBOL, 'endfor', tagStartToken) 40 | 41 | spacelessBlock.trimRightSpaceless = hasTagEndTokenTrimRight(openingTagEndToken) 42 | spacelessBlock.trimLeftEndspaceless = !!(closingTagStartToken && hasTagStartTokenTrimLeft(closingTagStartToken)) 43 | 44 | return spacelessBlock 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as n from 'melody-types' 17 | import { Types, setEndFromToken, setStartFromToken } from '../../../melody-parser/src/' 18 | import { copyEnd, createNode } from '../../../melody-parser/src/util' 19 | import { UrlStatement } from './../types' 20 | 21 | export const UrlParser = { 22 | name: 'url', 23 | parse(parser, token) { 24 | const tokens = parser.tokens 25 | 26 | const urlStatement = new UrlStatement(parser.matchExpression()) 27 | 28 | let args = [] 29 | while (!tokens.test(Types.EOF) && !tokens.test(Types.TAG_END)) { 30 | if (tokens.test(Types.SYMBOL) && tokens.lat(1) === Types.ASSIGNMENT) { 31 | const name = tokens.next() 32 | tokens.next() 33 | const value = parser.matchExpression() 34 | const arg = new n.NamedArgumentExpression(createNode(n.Identifier, name, name.text), value) 35 | copyEnd(arg, value) 36 | args.push(arg) 37 | } else { 38 | args.push(parser.matchExpression()) 39 | } 40 | 41 | if (tokens.test(Types.SYMBOL) && tokens.lat(0) === 'as') { 42 | tokens.next() 43 | urlStatement.as = this.matchExpression() 44 | } 45 | } 46 | 47 | urlStatement.arguments = args 48 | 49 | setStartFromToken(urlStatement, token) 50 | setEndFromToken(urlStatement, tokens.expect(Types.TAG_END)) 51 | 52 | return urlStatement 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/use.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Identifier } from 'melody-types' 17 | import { Types, setStartFromToken, setEndFromToken, copyStart, copyEnd, createNode } from '../../../melody-parser/src/' 18 | import { AliasExpression, UseStatement } from './../types' 19 | 20 | export const UseParser = { 21 | name: 'use', 22 | parse(parser, token) { 23 | const tokens = parser.tokens 24 | 25 | const source = parser.matchExpression(), 26 | aliases = [] 27 | 28 | if (tokens.nextIf(Types.SYMBOL, 'with')) { 29 | do { 30 | const nameToken = tokens.expect(Types.SYMBOL), 31 | name = createNode(Identifier, nameToken, nameToken.text) 32 | let alias = name 33 | if (tokens.nextIf(Types.SYMBOL, 'as')) { 34 | const aliasToken = tokens.expect(Types.SYMBOL) 35 | alias = createNode(Identifier, aliasToken, aliasToken.text) 36 | } 37 | const aliasExpression = new AliasExpression(name, alias) 38 | copyStart(aliasExpression, name) 39 | copyEnd(aliasExpression, alias) 40 | aliases.push(aliasExpression) 41 | } while (tokens.nextIf(Types.COMMA)) 42 | } 43 | 44 | const useStatement = new UseStatement(source, aliases) 45 | 46 | setStartFromToken(useStatement, token) 47 | setEndFromToken(useStatement, tokens.expect(Types.TAG_END)) 48 | 49 | return useStatement 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/parser/with.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as n from 'melody-types' 17 | import { Types, createNode, hasTagEndTokenTrimRight, hasTagStartTokenTrimLeft, setEndFromToken, setStartFromToken } from '../../../melody-parser/src/' 18 | import { copyEnd } from '../../../melody-parser/src/util' 19 | import { WithStatement } from './../types' 20 | 21 | export const WithParser = { 22 | name: 'with', 23 | parse(parser, token) { 24 | const tokens = parser.tokens, 25 | tagStartToken = tokens.la(-2) 26 | 27 | let withStatement, openingTagEndToken, closingTagStartToken 28 | 29 | let args = [] 30 | while (!tokens.test(Types.EOF) && !tokens.test(Types.TAG_END)) { 31 | if (tokens.test(Types.SYMBOL) && tokens.lat(1) === Types.ASSIGNMENT) { 32 | const name = tokens.next() 33 | tokens.next() 34 | const value = parser.matchExpression() 35 | const arg = new n.NamedArgumentExpression(createNode(n.Identifier, name, name.text), value) 36 | copyEnd(arg, value) 37 | args.push(arg) 38 | } else { 39 | const value = parser.matchExpression() 40 | if (tokens.nextIf(Types.SYMBOL, 'as')) { 41 | const name = tokens.next() 42 | const arg = new n.NamedArgumentExpression(createNode(n.Identifier, name, name.text), value) 43 | copyEnd(arg, value) 44 | args.push(arg) 45 | } else { 46 | const unexpectedToken = tokens.next() 47 | parser.error({ 48 | title: 'with arguments mismatch', 49 | pos: unexpectedToken.pos, 50 | advice: 'eg: {% with alpha=1 beta=2 %}' 51 | }) 52 | } 53 | } 54 | } 55 | 56 | if ((openingTagEndToken = tokens.nextIf(Types.TAG_END))) { 57 | withStatement = new WithStatement( 58 | parser.parse((tokenText, token, tokens) => { 59 | const result = !!(token.type === Types.TAG_START && tokens.nextIf(Types.SYMBOL, 'endwith')) 60 | if (result) { 61 | closingTagStartToken = token 62 | } 63 | return result 64 | }).expressions 65 | ) 66 | } else { 67 | withStatement = new WithStatement(new PrintExpressionStatement(parser.matchExpression())) 68 | } 69 | 70 | withStatement.arguments = args 71 | 72 | setStartFromToken(withStatement, token) 73 | setEndFromToken(withStatement, tokens.expect(Types.TAG_END, null, tagStartToken)) 74 | 75 | withStatement.trimRightBlock = openingTagEndToken && hasTagEndTokenTrimRight(openingTagEndToken) 76 | withStatement.trimLeftEndblock = !!(closingTagStartToken && hasTagStartTokenTrimLeft(closingTagStartToken)) 77 | 78 | return withStatement 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/visitors/functions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as t from 'babel-types'; 17 | 18 | function addOne(expr) { 19 | return t.binaryExpression('+', expr, t.numericLiteral(1)); 20 | } 21 | 22 | export default { 23 | range(path) { 24 | const args = path.node.arguments; 25 | const callArgs = []; 26 | if (args.length === 1) { 27 | callArgs.push(addOne(args[0])); 28 | } else if (args.length === 3) { 29 | callArgs.push(args[0]); 30 | callArgs.push(addOne(args[1])); 31 | callArgs.push(args[2]); 32 | } else if (args.length === 2) { 33 | callArgs.push(args[0], addOne(args[1])); 34 | } else { 35 | path.state.error( 36 | 'Invalid range call', 37 | path.node.pos, 38 | `The range function accepts 1 to 3 arguments but you have specified ${ 39 | args.length 40 | } arguments instead.` 41 | ); 42 | } 43 | 44 | path.replaceWithJS( 45 | t.callExpression( 46 | t.identifier(path.state.addImportFrom('lodash', 'range')), 47 | callArgs 48 | ) 49 | ); 50 | }, 51 | // range: 'lodash', 52 | dump(path) { 53 | if (!path.parentPath.is('PrintExpressionStatement')) { 54 | path.state.error( 55 | 'dump must be used in a lone expression', 56 | path.node.pos, 57 | 'The dump function does not have a return value. Thus it must be used as the only expression.' 58 | ); 59 | } 60 | path.parentPath.replaceWithJS( 61 | t.expressionStatement( 62 | t.callExpression( 63 | t.memberExpression( 64 | t.identifier('console'), 65 | t.identifier('log') 66 | ), 67 | path.node.arguments 68 | ) 69 | ) 70 | ); 71 | }, 72 | include(path) { 73 | if (!path.parentPath.is('PrintExpressionStatement')) { 74 | path.state.error({ 75 | title: 'Include function does not return value', 76 | pos: path.node.loc.start, 77 | advice: `The include function currently does not return a value. 78 | Thus you must use it like a regular include tag.`, 79 | }); 80 | } 81 | const includeName = path.scope.generateUid('include'); 82 | const importDecl = t.importDeclaration( 83 | [t.importDefaultSpecifier(t.identifier(includeName))], 84 | path.node.arguments[0] 85 | ); 86 | path.state.program.body.splice(0, 0, importDecl); 87 | path.scope.registerBinding(includeName); 88 | 89 | const argument = path.node.arguments[1]; 90 | 91 | const includeCall = t.expressionStatement( 92 | t.callExpression( 93 | t.memberExpression( 94 | t.identifier(includeName), 95 | t.identifier('render') 96 | ), 97 | argument ? [argument] : [] 98 | ) 99 | ); 100 | path.replaceWithJS(includeCall); 101 | }, 102 | }; 103 | -------------------------------------------------------------------------------- /src/melody-extension-core/src/visitors/tests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as t from 'babel-types'; 17 | 18 | export default { 19 | convert: { 20 | TestEvenExpression: { 21 | exit(path) { 22 | const expr = t.unaryExpression( 23 | '!', 24 | t.binaryExpression( 25 | '%', 26 | path.get('expression').node, 27 | t.numericLiteral(2) 28 | ) 29 | ); 30 | expr.extra = { parenthesizedArgument: true }; 31 | path.replaceWithJS(expr); 32 | }, 33 | }, 34 | TestOddExpression: { 35 | exit(path) { 36 | const expr = t.unaryExpression( 37 | '!', 38 | t.unaryExpression( 39 | '!', 40 | t.binaryExpression( 41 | '%', 42 | path.get('expression').node, 43 | t.numericLiteral(2) 44 | ) 45 | ) 46 | ); 47 | expr.extra = { parenthesizedArgument: true }; 48 | path.replaceWithJS(expr); 49 | }, 50 | }, 51 | TestDefinedExpression: { 52 | exit(path) { 53 | path.replaceWithJS( 54 | t.binaryExpression( 55 | '!==', 56 | t.unaryExpression( 57 | 'typeof', 58 | path.get('expression').node 59 | ), 60 | t.stringLiteral('undefined') 61 | ) 62 | ); 63 | }, 64 | }, 65 | TestEmptyExpression: { 66 | exit(path) { 67 | path.replaceWithJS( 68 | t.callExpression( 69 | t.identifier( 70 | this.addImportFrom('melody-runtime', 'isEmpty') 71 | ), 72 | [path.get('expression').node] 73 | ) 74 | ); 75 | }, 76 | }, 77 | TestSameAsExpression: { 78 | exit(path) { 79 | path.replaceWithJS( 80 | t.binaryExpression( 81 | '===', 82 | path.get('expression').node, 83 | path.get('arguments')[0].node 84 | ) 85 | ); 86 | }, 87 | }, 88 | TestNullExpression: { 89 | exit(path) { 90 | path.replaceWithJS( 91 | t.binaryExpression( 92 | '===', 93 | path.get('expression').node, 94 | t.nullLiteral() 95 | ) 96 | ); 97 | }, 98 | }, 99 | TestDivisibleByExpression: { 100 | exit(path) { 101 | path.replaceWithJS( 102 | t.unaryExpression( 103 | '!', 104 | t.binaryExpression( 105 | '%', 106 | path.get('expression').node, 107 | path.node.arguments[0] 108 | ) 109 | ) 110 | ); 111 | }, 112 | }, 113 | TestIterableExpression: { 114 | exit(path) { 115 | path.replaceWithJS( 116 | t.callExpression( 117 | t.memberExpression( 118 | t.identifier('Array'), 119 | t.identifier('isArray') 120 | ), 121 | [path.node.expression] 122 | ) 123 | ); 124 | }, 125 | }, 126 | }, 127 | }; 128 | -------------------------------------------------------------------------------- /src/melody-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "melody-parser", 3 | "version": "1.7.5", 4 | "description": "", 5 | "main": "./src/index.js", 6 | "jsnext:main": "./lib/index.esm.js", 7 | "scripts": { 8 | "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js" 9 | }, 10 | "author": "", 11 | "license": "Apache-2.0", 12 | "dependencies": {}, 13 | "peerDependencies": { 14 | "he": "^1.1.0", 15 | "lodash": "^4.12.0", 16 | "melody-code-frame": "^1.7.5", 17 | "melody-types": "^1.1.0" 18 | }, 19 | "gitHead": "623a490f999fdad0c11e76d676ae7faa14f569e5" 20 | } 21 | -------------------------------------------------------------------------------- /src/melody-parser/src/Associativity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export var LEFT = Symbol(); 17 | export var RIGHT = Symbol(); 18 | -------------------------------------------------------------------------------- /src/melody-parser/src/CharStream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const EOF = Symbol(); 18 | 19 | export class CharStream { 20 | constructor(input) { 21 | this.input = String(input); 22 | this.length = this.input.length; 23 | this.index = 0; 24 | this.position = { line: 1, column: 0 }; 25 | } 26 | 27 | get source() { 28 | return this.input; 29 | } 30 | 31 | reset() { 32 | this.rewind({ line: 1, column: 0, index: 0 }); 33 | } 34 | 35 | mark() { 36 | let { line, column } = this.position, 37 | index = this.index; 38 | return { line, column, index }; 39 | } 40 | 41 | rewind(marker) { 42 | this.position.line = marker.line; 43 | this.position.column = marker.column; 44 | this.index = marker.index; 45 | } 46 | 47 | la(offset) { 48 | var index = this.index + offset; 49 | return index < this.length ? this.input.charAt(index) : EOF; 50 | } 51 | 52 | lac(offset) { 53 | var index = this.index + offset; 54 | return index < this.length ? this.input.charCodeAt(index) : EOF; 55 | } 56 | 57 | next() { 58 | if (this.index === this.length) { 59 | return EOF; 60 | } 61 | var ch = this.input.charAt(this.index); 62 | this.index++; 63 | this.position.column++; 64 | if (ch === '\n') { 65 | this.position.line += 1; 66 | this.position.column = 0; 67 | } 68 | return ch; 69 | } 70 | 71 | match(str) { 72 | const start = this.mark(); 73 | for (let i = 0, len = str.length; i < len; i++) { 74 | const ch = this.next(); 75 | if (ch !== str.charAt(i) || ch === EOF) { 76 | this.rewind(start); 77 | return false; 78 | } 79 | } 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/melody-parser/src/GenericMultiTagParser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { setStartFromToken, setEndFromToken } from './util'; 17 | import { GenericTagParser } from './GenericTagParser'; 18 | import * as Types from './TokenTypes'; 19 | 20 | const tagMatchesOneOf = (tokenStream, tagNames) => { 21 | for (let i = 0; i < tagNames.length; i++) { 22 | if (tokenStream.test(Types.SYMBOL, tagNames[i])) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | }; 28 | 29 | export const createMultiTagParser = (tagName, subTags = []) => ({ 30 | name: 'genericTwigMultiTag', 31 | parse(parser, token) { 32 | const tokens = parser.tokens, 33 | tagStartToken = tokens.la(-1); 34 | 35 | if (subTags.length === 0) { 36 | subTags.push('end' + tagName); 37 | } 38 | 39 | const twigTag = GenericTagParser.parse(parser, token); 40 | let currentTagName = tagName; 41 | const endTagName = subTags[subTags.length - 1]; 42 | 43 | while (currentTagName !== endTagName) { 44 | // Parse next section 45 | twigTag.sections.push( 46 | parser.parse((tokenText, token, tokens) => { 47 | const hasReachedNextTag = 48 | token.type === Types.TAG_START && 49 | tagMatchesOneOf(tokens, subTags); 50 | return hasReachedNextTag; 51 | }) 52 | ); 53 | tokens.next(); // Get past "{%" 54 | 55 | // Parse next tag 56 | const childTag = GenericTagParser.parse(parser); 57 | twigTag.sections.push(childTag); 58 | currentTagName = childTag.tagName; 59 | } 60 | 61 | setStartFromToken(twigTag, tagStartToken); 62 | setEndFromToken(twigTag, tokens.la(0)); 63 | 64 | return twigTag; 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /src/melody-parser/src/GenericTagParser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { hasTagStartTokenTrimLeft, hasTagEndTokenTrimRight } from './util'; 17 | import * as Types from './TokenTypes'; 18 | import * as n from 'melody-types'; 19 | 20 | export const GenericTagParser = { 21 | name: 'genericTwigTag', 22 | parse(parser) { 23 | const tokens = parser.tokens, 24 | tagStartToken = tokens.la(-2); 25 | let currentToken; 26 | 27 | const twigTag = new n.GenericTwigTag(tokens.la(-1).text); 28 | while ((currentToken = tokens.la(0))) { 29 | if (currentToken.type === Types.TAG_END) { 30 | break; 31 | } else { 32 | try { 33 | twigTag.parts.push(parser.matchExpression()); 34 | } catch (e) { 35 | if (e.errorType === 'UNEXPECTED_TOKEN') { 36 | twigTag.parts.push( 37 | new n.GenericToken(e.tokenType, e.tokenText) 38 | ); 39 | tokens.next(); 40 | } else { 41 | throw e; 42 | } 43 | } 44 | } 45 | } 46 | tokens.expect(Types.TAG_END); 47 | 48 | twigTag.trimLeft = hasTagStartTokenTrimLeft(tagStartToken); 49 | twigTag.trimRight = hasTagEndTokenTrimRight(currentToken); 50 | 51 | return twigTag; 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/melody-parser/src/TokenTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export const EXPRESSION_START = 'expressionStart'; 17 | export const EXPRESSION_END = 'expressionEnd'; 18 | export const TAG_START = 'tagStart'; 19 | export const TAG_END = 'tagEnd'; 20 | export const INTERPOLATION_START = 'interpolationStart'; 21 | export const INTERPOLATION_END = 'interpolationEnd'; 22 | export const STRING_START = 'stringStart'; 23 | export const STRING_END = 'stringEnd'; 24 | export const DECLARATION_START = 'declarationStart'; 25 | export const COMMENT = 'comment'; 26 | export const WHITESPACE = 'whitespace'; 27 | export const HTML_COMMENT = 'htmlComment'; 28 | export const TEXT = 'text'; 29 | export const ENTITY = 'entity'; 30 | export const SYMBOL = 'symbol'; 31 | export const STRING = 'string'; 32 | export const OPERATOR = 'operator'; 33 | export const TRUE = 'true'; 34 | export const FALSE = 'false'; 35 | export const NULL = 'null'; 36 | export const LBRACE = '['; 37 | export const RBRACE = ']'; 38 | export const LPAREN = '('; 39 | export const RPAREN = ')'; 40 | export const LBRACKET = '{'; 41 | export const RBRACKET = '}'; 42 | export const COLON = ':'; 43 | export const COMMA = ','; 44 | export const DOT = '.'; 45 | export const PIPE = '|'; 46 | export const QUESTION_MARK = '?'; 47 | export const ASSIGNMENT = '='; 48 | export const ELEMENT_START = '<'; 49 | export const SLASH = '/'; 50 | export const ELEMENT_END = '>'; 51 | export const NUMBER = 'number'; 52 | export const EOF = 'EOF'; 53 | export const ERROR = 'ERROR'; 54 | export const EOF_TOKEN = { 55 | type: EOF, 56 | pos: { 57 | index: -1, 58 | line: -1, 59 | pos: -1, 60 | }, 61 | end: -1, 62 | length: 0, 63 | source: null, 64 | text: '', 65 | }; 66 | 67 | export const ERROR_TABLE = { 68 | [EXPRESSION_END]: 'expression end "}}"', 69 | [EXPRESSION_START]: 'expression start "{{"', 70 | [TAG_START]: 'tag start "{%"', 71 | [TAG_END]: 'tag end "%}"', 72 | [INTERPOLATION_START]: 'interpolation start "#{"', 73 | [INTERPOLATION_END]: 'interpolation end "}"', 74 | }; 75 | -------------------------------------------------------------------------------- /src/melody-parser/src/elementInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // https://www.w3.org/TR/html5/syntax.html#void-elements 17 | export const voidElements = { 18 | area: true, 19 | base: true, 20 | br: true, 21 | col: true, 22 | embed: true, 23 | hr: true, 24 | img: true, 25 | input: true, 26 | keygen: true, 27 | link: true, 28 | meta: true, 29 | param: true, 30 | source: true, 31 | track: true, 32 | wbr: true, 33 | }; 34 | 35 | export const rawTextElements = { 36 | script: true, 37 | style: true, 38 | }; 39 | 40 | export const escapableRawTextElements = { 41 | textarea: true, 42 | title: true, 43 | }; 44 | -------------------------------------------------------------------------------- /src/melody-parser/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import Parser from './Parser'; 17 | import TokenStream from './TokenStream'; 18 | import * as Types from './TokenTypes'; 19 | import Lexer from './Lexer'; 20 | import { EOF, CharStream } from './CharStream'; 21 | import { LEFT, RIGHT } from './Associativity'; 22 | import { 23 | setStartFromToken, 24 | setEndFromToken, 25 | copyStart, 26 | copyEnd, 27 | copyLoc, 28 | getNodeSource, 29 | createNode, 30 | hasTagStartTokenTrimLeft, 31 | hasTagEndTokenTrimRight, 32 | isMelodyExtension, 33 | } from './util'; 34 | 35 | function parse(code, options, ...extensions) { 36 | return createExtendedParser(code, options, ...extensions).parse(); 37 | } 38 | 39 | function createExtendedParser(code, options, ...extensions) { 40 | let passedOptions = options; 41 | const passedExtensions = extensions; 42 | if (isMelodyExtension(options)) { 43 | // Variant without options parameter: createExtendedParser(code, ...extensions) 44 | passedOptions = undefined; 45 | passedExtensions.unshift(options); 46 | } 47 | const lexer = createExtendedLexer(code, options, ...passedExtensions); 48 | const parser = new Parser( 49 | new TokenStream(lexer, passedOptions), 50 | passedOptions 51 | ); 52 | for (const ext of passedExtensions) { 53 | parser.applyExtension(ext); 54 | } 55 | return parser; 56 | } 57 | 58 | function createExtendedLexer(code, options, ...extensions) { 59 | let passedOptions = options; 60 | const passedExtensions = extensions; 61 | if (isMelodyExtension(options)) { 62 | // Variant without options parameter: createExtendedLexer(code, ...extensions) 63 | passedOptions = undefined; 64 | passedExtensions.unshift(options); 65 | } 66 | const lexer = new Lexer(new CharStream(code), passedOptions); 67 | for (const ext of passedExtensions) { 68 | lexer.applyExtension(ext); 69 | } 70 | return lexer; 71 | } 72 | 73 | export { 74 | Parser, 75 | TokenStream, 76 | Lexer, 77 | EOF, 78 | CharStream, 79 | LEFT, 80 | RIGHT, 81 | parse, 82 | createExtendedLexer, 83 | createExtendedParser, 84 | setStartFromToken, 85 | setEndFromToken, 86 | copyStart, 87 | copyEnd, 88 | copyLoc, 89 | getNodeSource, 90 | createNode, 91 | hasTagStartTokenTrimLeft, 92 | hasTagEndTokenTrimRight, 93 | Types, 94 | }; 95 | -------------------------------------------------------------------------------- /src/melody-parser/src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 trivago N.V. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export function setStartFromToken(node, { pos: { index, line, column } }) { 17 | node.loc.start = { line, column, index }; 18 | return node; 19 | } 20 | 21 | export function setEndFromToken(node, { pos: { line, column }, end }) { 22 | node.loc.end = { line, column, index: end }; 23 | return node; 24 | } 25 | 26 | export function setMarkFromToken( 27 | node, 28 | propertyName, 29 | { pos: { index, line, column } } 30 | ) { 31 | node[propertyName] = { line, column, index }; 32 | return node; 33 | } 34 | 35 | export function copyStart( 36 | node, 37 | { 38 | loc: { 39 | start: { line, column, index }, 40 | }, 41 | } 42 | ) { 43 | node.loc.start.line = line; 44 | node.loc.start.column = column; 45 | node.loc.start.index = index; 46 | return node; 47 | } 48 | 49 | export function copyEnd(node, end) { 50 | node.loc.end.line = end.loc.end.line; 51 | node.loc.end.column = end.loc.end.column; 52 | node.loc.end.index = end.loc.end.index; 53 | return node; 54 | } 55 | 56 | export function getNodeSource(node, entireSource) { 57 | if (entireSource && node.loc.start && node.loc.end) { 58 | return entireSource.substring(node.loc.start.index, node.loc.end.index); 59 | } 60 | return ''; 61 | } 62 | 63 | export function copyLoc(node, { loc: { start, end } }) { 64 | node.loc.start.line = start.line; 65 | node.loc.start.column = start.column; 66 | node.loc.start.index = start.index; 67 | node.loc.end.line = end.line; 68 | node.loc.end.column = end.column; 69 | node.loc.end.index = end.index; 70 | return node; 71 | } 72 | 73 | export function createNode(Type, token, ...args) { 74 | return setEndFromToken(setStartFromToken(new Type(...args), token), token); 75 | } 76 | 77 | export function startNode(Type, token, ...args) { 78 | return setStartFromToken(new Type(...args), token); 79 | } 80 | 81 | export function hasTagStartTokenTrimLeft(token) { 82 | return token.text.endsWith('-'); 83 | } 84 | 85 | export function hasTagEndTokenTrimRight(token) { 86 | return token.text.startsWith('-'); 87 | } 88 | 89 | export function isMelodyExtension(obj) { 90 | return ( 91 | obj && 92 | (Array.isArray(obj.binaryOperators) || 93 | typeof obj.filterMap === 'object' || 94 | typeof obj.functionMap === 'object' || 95 | Array.isArray(obj.tags) || 96 | Array.isArray(obj.tests) || 97 | Array.isArray(obj.unaryOperators) || 98 | Array.isArray(obj.visitors)) 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | import { CharStream, Lexer, TokenStream, Parser } from './melody-parser/src' 2 | import { extension as coreExtension } from './melody-extension-core/src' 3 | import { getAdditionalMelodyExtensions, getPluginPathsFromOptions } from './util' 4 | 5 | // const ORIGINAL_SOURCE = Symbol('ORIGINAL_SOURCE') 6 | const ORIGINAL_SOURCE = 'ORIGINAL_SOURCE' 7 | 8 | const createConfiguredLexer = (code, ...extensions) => { 9 | const lexer = new Lexer(new CharStream(code)) 10 | for (const extension of extensions) { 11 | if (extension.unaryOperators) { 12 | lexer.addOperators(...extension.unaryOperators.map((op) => op.text)) 13 | } 14 | if (extension.binaryOperators) { 15 | lexer.addOperators(...extension.binaryOperators.map((op) => op.text)) 16 | } 17 | } 18 | return lexer 19 | } 20 | 21 | const applyParserExtensions = (parser, ...extensions) => { 22 | for (const extension of extensions) { 23 | if (extension.tags) { 24 | for (const tag of extension.tags) { 25 | parser.addTag(tag) 26 | } 27 | } 28 | if (extension.unaryOperators) { 29 | for (const op of extension.unaryOperators) { 30 | parser.addUnaryOperator(op) 31 | } 32 | } 33 | if (extension.binaryOperators) { 34 | for (const op of extension.binaryOperators) { 35 | parser.addBinaryOperator(op) 36 | } 37 | } 38 | if (extension.tests) { 39 | for (const test of extension.tests) { 40 | parser.addTest(test) 41 | } 42 | } 43 | } 44 | } 45 | 46 | const createConfiguredParser = (code, multiTagConfig, ...extensions) => { 47 | const parser = new Parser( 48 | new TokenStream(createConfiguredLexer(code, ...extensions), { 49 | ignoreWhitespace: true, 50 | ignoreComments: false, 51 | ignoreHtmlComments: false, 52 | applyWhitespaceTrimming: false 53 | }), 54 | { 55 | ignoreComments: false, 56 | ignoreHtmlComments: false, 57 | ignoreDeclarations: false, 58 | decodeEntities: false, 59 | multiTags: multiTagConfig, 60 | allowUnknownTags: true 61 | } 62 | ) 63 | applyParserExtensions(parser, ...extensions) 64 | return parser 65 | } 66 | 67 | const getMultiTagConfig = (tagsCsvs = []) => 68 | tagsCsvs.reduce((acc, curr) => { 69 | const tagNames = curr.split(',') 70 | acc[tagNames[0].trim()] = tagNames.slice(1).map((s) => s.trim()) 71 | return acc 72 | }, {}) 73 | 74 | const parse = (text, parsers, options) => { 75 | const pluginPaths = getPluginPathsFromOptions(options) 76 | const multiTagConfig = getMultiTagConfig(options.twigMultiTags || []) 77 | const extensions = [coreExtension, ...getAdditionalMelodyExtensions(pluginPaths)] 78 | const parser = createConfiguredParser(text, multiTagConfig, ...extensions) 79 | const ast = parser.parse() 80 | ast[ORIGINAL_SOURCE] = text 81 | return ast 82 | } 83 | 84 | export { parse, ORIGINAL_SOURCE } 85 | -------------------------------------------------------------------------------- /src/print/AliasExpression.js: -------------------------------------------------------------------------------- 1 | import { concat } from './../util/prettier-doc-builders' 2 | 3 | export const printAliasExpression = (node, path, print) => { 4 | return concat([path.call(print, 'name'), ' as ', path.call(print, 'alias')]) 5 | } 6 | -------------------------------------------------------------------------------- /src/print/ArrayExpression.js: -------------------------------------------------------------------------------- 1 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES, wrapExpressionIfNeeded } from '../util'; 2 | import { concat, group, indent, join, line, softline } from './../util/prettier-doc-builders'; 3 | 4 | export const printArrayExpression = (node, path, print) => { 5 | node[STRING_NEEDS_QUOTES] = true 6 | node[EXPRESSION_NEEDED] = false 7 | const mappedElements = path.map(print, 'elements') 8 | const indentedContent = concat([softline, join(concat([',', line]), mappedElements)]) 9 | 10 | let parts = ['[', indent(indentedContent), softline, ']'] 11 | wrapExpressionIfNeeded(path, parts, node) 12 | return group(concat(parts)) 13 | } 14 | -------------------------------------------------------------------------------- /src/print/Attribute.js: -------------------------------------------------------------------------------- 1 | import { concat } from './../util/prettier-doc-builders' 2 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES } from '../util' 3 | import { Node } from 'melody-types' 4 | 5 | const mayCorrectWhitespace = attrName => ['id', 'class', 'type'].indexOf(attrName) > -1 6 | 7 | const sanitizeWhitespace = s => s.replace(/\s+/g, ' ').trim() 8 | 9 | const printConcatenatedString = (valueNode, path, print, ...initialPath) => { 10 | const printedFragments = [] 11 | let currentNode = valueNode 12 | const currentPath = initialPath 13 | while (Node.isBinaryConcatExpression(currentNode)) { 14 | printedFragments.unshift(path.call(print, ...currentPath, 'right')) 15 | currentPath.push('left') 16 | currentNode = currentNode.left 17 | } 18 | printedFragments.unshift(path.call(print, ...currentPath)) 19 | return concat(printedFragments) 20 | } 21 | 22 | export const printAttribute = (node, path, print = print) => { 23 | node[EXPRESSION_NEEDED] = false 24 | const docs = [path.call(print, 'name')] 25 | node[EXPRESSION_NEEDED] = true 26 | node[STRING_NEEDS_QUOTES] = false 27 | if (node.value) { 28 | docs.push('="') 29 | if (Node.isBinaryConcatExpression(node.value) && node.value.wasImplicitConcatenation) { 30 | // Special handling for concatenated string values 31 | docs.push(printConcatenatedString(node.value, path, print, 'value')) 32 | } else { 33 | const isStringValue = Node.isStringLiteral(node.value) 34 | if (mayCorrectWhitespace(node.name.name) && isStringValue) { 35 | node.value.value = sanitizeWhitespace(node.value.value) 36 | } 37 | docs.push(path.call(print, 'value')) 38 | } 39 | docs.push('"') 40 | } 41 | 42 | return concat(docs) 43 | } 44 | -------------------------------------------------------------------------------- /src/print/AutoescapeBlock.js: -------------------------------------------------------------------------------- 1 | import { printChildBlock, quoteChar } from '../util' 2 | import { concat, hardline } from './../util/prettier-doc-builders' 3 | 4 | const createOpener = (node, options) => { 5 | let escapeType = node.escapeType, escapeTypeParts 6 | if (escapeType == 'on' || escapeType == 'off') { 7 | escapeTypeParts = [escapeType] 8 | } else { 9 | escapeTypeParts = [quoteChar(options), node.escapeType || 'html', quoteChar(options)] 10 | } 11 | return concat([node.trimLeft ? '{%-' : '{%', ' autoescape ', ...escapeTypeParts, ' ', node.trimRightAutoescape ? '-%}' : '%}']) 12 | } 13 | 14 | export const printAutoescapeBlock = (node, path, print, options) => { 15 | const parts = [createOpener(node, options)] 16 | parts.push(printChildBlock(node, path, print, 'expressions')) 17 | parts.push(hardline, node.trimLeftEndautoescape ? '{%-' : '{%', ' endautoescape ', node.trimRight ? '-%}' : '%}') 18 | 19 | return concat(parts) 20 | } 21 | -------------------------------------------------------------------------------- /src/print/BinaryExpression.js: -------------------------------------------------------------------------------- 1 | import { Node } from 'melody-types' 2 | import { EXPRESSION_NEEDED, GROUP_TOP_LEVEL_LOGICAL, INSIDE_OF_STRING, IS_ROOT_LOGICAL_EXPRESSION, STRING_NEEDS_QUOTES, findParentNode, firstValueInAncestorChain, wrapExpressionIfNeeded } from '../util' 3 | import { extension as coreExtension } from './../melody-extension-core/src' 4 | import { concat, group, indent, line, softline } from './../util/prettier-doc-builders' 5 | const ALREADY_INDENTED = Symbol('ALREADY_INDENTED') 6 | const OPERATOR_PRECEDENCE = Symbol('OPERATOR_PRECEDENCE') 7 | const NO_WHITESPACE_AROUND = ['..'] 8 | 9 | const operatorPrecedence = coreExtension.binaryOperators.reduce((acc, curr) => { 10 | acc[curr.text] = curr.precedence 11 | return acc 12 | }, {}) 13 | 14 | const printInterpolatedString = (node, path, print, options) => { 15 | node[STRING_NEEDS_QUOTES] = false 16 | node[INSIDE_OF_STRING] = true 17 | 18 | const printedFragments = ['"'] // For interpolated strings, we HAVE to use double quotes 19 | let currentNode = node 20 | const currentPath = [] 21 | while (Node.isBinaryConcatExpression(currentNode)) { 22 | printedFragments.unshift(path.call(print, ...currentPath, 'right')) 23 | currentPath.push('left') 24 | currentNode = currentNode.left 25 | } 26 | printedFragments.unshift(path.call(print, ...currentPath)) 27 | printedFragments.unshift('"') 28 | return concat(printedFragments) 29 | } 30 | 31 | const operatorNeedsSpaces = operator => { 32 | return NO_WHITESPACE_AROUND.indexOf(operator) < 0 33 | } 34 | 35 | const hasLogicalOperator = node => { 36 | return node.operator === 'or' || node.operator === 'and' 37 | } 38 | 39 | const otherNeedsParentheses = (node, otherProp) => { 40 | const other = node[otherProp] 41 | const isBinaryOther = Node.isBinaryExpression(other) 42 | const ownPrecedence = operatorPrecedence[node.operator] 43 | const otherPrecedence = isBinaryOther ? operatorPrecedence[node[otherProp].operator] : Number.MAX_SAFE_INTEGER 44 | return otherPrecedence < ownPrecedence || (otherPrecedence > ownPrecedence && isBinaryOther && hasLogicalOperator(other)) || Node.isFilterExpression(other) || (Node.isBinaryConcatExpression(node) && Node.isConditionalExpression(other)) 45 | } 46 | 47 | const _printBinaryExpression = (node, path, print, options) => { 48 | node[EXPRESSION_NEEDED] = false 49 | node[STRING_NEEDS_QUOTES] = true 50 | 51 | const isBinaryRight = Node.isBinaryExpression(node.right) 52 | const isLogicalOperator = ['and', 'or'].indexOf(node.operator) > -1 53 | const whitespaceAroundOperator = operatorNeedsSpaces(node.operator) 54 | 55 | const alreadyIndented = firstValueInAncestorChain(path, ALREADY_INDENTED, false) 56 | if (!alreadyIndented && isBinaryRight) { 57 | node.right[ALREADY_INDENTED] = true 58 | } 59 | const foundRootAbove = firstValueInAncestorChain(path, IS_ROOT_LOGICAL_EXPRESSION, false) 60 | 61 | const parentNode = findParentNode(path) 62 | const shouldGroupOnTopLevel = parentNode[GROUP_TOP_LEVEL_LOGICAL] !== false 63 | 64 | if (!foundRootAbove) { 65 | node[IS_ROOT_LOGICAL_EXPRESSION] = true 66 | } 67 | const parentOperator = foundRootAbove ? firstValueInAncestorChain(path, 'operator') : '' 68 | 69 | node[OPERATOR_PRECEDENCE] = operatorPrecedence[node.operator] 70 | 71 | const printedLeft = path.call(print, 'left') 72 | const printedRight = path.call(print, 'right') 73 | 74 | const parts = [] 75 | const leftNeedsParens = otherNeedsParentheses(node, 'left') && options['templateType'] != 'django' 76 | const rightNeedsParens = otherNeedsParentheses(node, 'right') && options['templateType'] != 'django' 77 | 78 | if (leftNeedsParens) { 79 | parts.push('(') 80 | } 81 | parts.push(printedLeft) 82 | if (leftNeedsParens) { 83 | parts.push(')') 84 | } 85 | const potentiallyIndented = [whitespaceAroundOperator ? line : softline, node.operator, whitespaceAroundOperator ? ' ' : ''] 86 | if (rightNeedsParens) { 87 | potentiallyIndented.push('(') 88 | } 89 | potentiallyIndented.push(printedRight) 90 | if (rightNeedsParens) { 91 | potentiallyIndented.push(')') 92 | } 93 | const rightHandSide = alreadyIndented ? concat(potentiallyIndented) : indent(concat(potentiallyIndented)) 94 | const result = concat(wrapExpressionIfNeeded(path, [...parts, rightHandSide], node)) 95 | 96 | const shouldCreateTopLevelGroup = !foundRootAbove && shouldGroupOnTopLevel 97 | const isDifferentLogicalOperator = isLogicalOperator && node.operator !== parentOperator 98 | 99 | const shouldGroupResult = shouldCreateTopLevelGroup || !isLogicalOperator || (foundRootAbove && isDifferentLogicalOperator) 100 | 101 | return shouldGroupResult ? group(result) : result 102 | } 103 | 104 | export const printBinaryExpression = (node, path, print, options) => { 105 | if (Node.isBinaryConcatExpression(node) && node.wasImplicitConcatenation) { 106 | return printInterpolatedString(node, path, print, options) 107 | } 108 | return _printBinaryExpression(node, path, print, options) 109 | } 110 | -------------------------------------------------------------------------------- /src/print/BlockStatement.js: -------------------------------------------------------------------------------- 1 | import { concat, hardline, group } from './../util/prettier-doc-builders' 2 | import { Node } from 'melody-types' 3 | import { EXPRESSION_NEEDED, printChildBlock } from '../util' 4 | 5 | export const printBlockStatement = (node, path, print, options) => { 6 | node[EXPRESSION_NEEDED] = false 7 | const hasChildren = Array.isArray(node.body) 8 | const printEndblockName = options.twigOutputEndblockName === true 9 | 10 | if (hasChildren) { 11 | const blockName = path.call(print, 'name') 12 | const opener = concat([node.trimLeft ? '{%-' : '{%', ' block ', blockName, node.trimRightBlock ? ' -%}' : ' %}']) 13 | const parts = [opener] 14 | if (node.body.length > 0) { 15 | const indentedBody = printChildBlock(node, path, print, 'body') 16 | parts.push(indentedBody) 17 | } 18 | parts.push(hardline) 19 | parts.push(node.trimLeftEndblock ? '{%-' : '{%', ' endblock', printEndblockName ? concat([' ', blockName]) : '', node.trimRight ? ' -%}' : ' %}') 20 | 21 | const result = group(concat(parts)) 22 | return result 23 | } else if (Node.isPrintExpressionStatement(node.body)) { 24 | const parts = [node.trimLeft ? '{%-' : '{%', ' block ', path.call(print, 'name'), ' ', path.call(print, 'body', 'value'), node.trimRight ? ' -%}' : ' %}'] 25 | return concat(parts) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/print/CallExpression.js: -------------------------------------------------------------------------------- 1 | import { group, concat, softline, line, indent, join } from './../util/prettier-doc-builders' 2 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES, wrapExpressionIfNeeded } from '../util' 3 | import { Node } from 'melody-types' 4 | 5 | export const printCallExpression = (node, path, print) => { 6 | node[EXPRESSION_NEEDED] = false 7 | node[STRING_NEEDS_QUOTES] = true 8 | const mappedArguments = path.map(print, 'arguments') 9 | const parts = [path.call(print, 'callee'), '('] 10 | if (node.arguments.length === 0) { 11 | parts.push(')') 12 | } else if (node.arguments.length === 1 && Node.isObjectExpression(node.arguments[0])) { 13 | // Optimization: No line break between "(" and "{" if 14 | // there is exactly one object parameter 15 | parts.push(mappedArguments[0], ')') 16 | } else { 17 | parts.push(indent(concat([softline, join(concat([',', line]), mappedArguments)])), softline, ')') 18 | } 19 | 20 | wrapExpressionIfNeeded(path, parts, node) 21 | 22 | return group(concat(parts)) 23 | } 24 | -------------------------------------------------------------------------------- /src/print/ConditionalExpression.js: -------------------------------------------------------------------------------- 1 | import { concat, line, indent, group } from './../util/prettier-doc-builders' 2 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES, wrapExpressionIfNeeded } from '../util' 3 | 4 | export const printConditionalExpression = (node, path, print) => { 5 | node[EXPRESSION_NEEDED] = false 6 | node[STRING_NEEDS_QUOTES] = true 7 | 8 | const rest = [line, '?'] 9 | if (node.consequent) { 10 | rest.push(concat([' ', path.call(print, 'consequent')])) 11 | } 12 | if (node.alternate) { 13 | rest.push(line, ': ', path.call(print, 'alternate')) 14 | } 15 | const parts = [path.call(print, 'test'), indent(concat(rest))] 16 | wrapExpressionIfNeeded(path, parts, node) 17 | 18 | return group(concat(parts)) 19 | } 20 | -------------------------------------------------------------------------------- /src/print/Declaration.js: -------------------------------------------------------------------------------- 1 | import { fill, join } from './../util/prettier-doc-builders' 2 | import { STRING_NEEDS_QUOTES, OVERRIDE_QUOTE_CHAR } from '../util' 3 | 4 | export const printDeclaration = (node, path, print) => { 5 | node[STRING_NEEDS_QUOTES] = true 6 | node[OVERRIDE_QUOTE_CHAR] = '"' 7 | const start = '']) 11 | } 12 | -------------------------------------------------------------------------------- /src/print/DoStatement.js: -------------------------------------------------------------------------------- 1 | import { concat } from './../util/prettier-doc-builders' 2 | 3 | export const printDoStatement = (node, path, print) => { 4 | return concat([node.trimLeft ? '{%-' : '{%', ' do ', path.call(print, 'value'), node.trimRight ? ' -%}' : ' %}']) 5 | } 6 | -------------------------------------------------------------------------------- /src/print/Element.js: -------------------------------------------------------------------------------- 1 | import { concat, group, line, hardline, softline, indent, join } from './../util/prettier-doc-builders' 2 | import { removeSurroundingWhitespace, isInlineElement, printChildGroups, EXPRESSION_NEEDED, STRING_NEEDS_QUOTES } from '../util' 3 | 4 | export const printOpeningTag = (node, path, print) => { 5 | const opener = '<' + node.name 6 | const printedAttributes = printSeparatedList(path, print, '', 'attributes') 7 | const openingTagEnd = node.selfClosing ? ' />' : '>' 8 | const hasAttributes = node.attributes && node.attributes.length > 0 9 | 10 | if (hasAttributes) { 11 | return concat([opener, indent(concat([' ', printedAttributes])), openingTagEnd]) 12 | } 13 | return concat([opener, openingTagEnd]) 14 | } 15 | 16 | const printSeparatedList = (path, print, separator, attrName) => { 17 | return join(concat([separator, line]), path.map(print, attrName)) 18 | } 19 | 20 | export const printElement = (node, path, print) => { 21 | // Set a flag in case attributes contain, e.g., a FilterExpression 22 | node[EXPRESSION_NEEDED] = true 23 | const openingGroup = group(printOpeningTag(node, path, print)) 24 | node[EXPRESSION_NEEDED] = false 25 | node[STRING_NEEDS_QUOTES] = false 26 | 27 | const tagName = node.name.toLowerCase() 28 | if (tagName == 'script' || tagName == 'style') { 29 | let { value } = node.children?.[0]?.value || {} 30 | if (value) { 31 | return [openingGroup, value, concat([''])] 32 | } else { 33 | return [openingGroup, concat([''])] 34 | } 35 | } 36 | 37 | if (!node.selfClosing) { 38 | node.children = removeSurroundingWhitespace(node.children) 39 | 40 | const childGroups = printChildGroups(node, path, print, 'children') 41 | const closingTag = concat(['']) 42 | const result = [openingGroup] 43 | const joinedChildren = concat(childGroups) 44 | if (isInlineElement(node)) { 45 | result.push(indent(concat([softline, joinedChildren])), softline) 46 | } else { 47 | const childBlock = [] 48 | 49 | var onlyTextChildren = node.children.findIndex((c) => c.type != 'PrintExpressionStatement' && c.type != 'PrintTextStatement') == -1 50 | 51 | if (childGroups.length > 0) { 52 | if (!onlyTextChildren) { 53 | childBlock.push(hardline) 54 | } 55 | } 56 | childBlock.push(joinedChildren) 57 | result.push(indent(concat(childBlock))) 58 | if (childGroups.length > 0) { 59 | if (!onlyTextChildren) { 60 | result.push(hardline) 61 | } 62 | } 63 | } 64 | result.push(closingTag) 65 | 66 | return isInlineElement(node) ? group(concat(result)) : concat(result) 67 | } 68 | 69 | return openingGroup 70 | } 71 | -------------------------------------------------------------------------------- /src/print/EmbedStatement.js: -------------------------------------------------------------------------------- 1 | import { concat, indent, hardline, line, group } from './../util/prettier-doc-builders.js' 2 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES, printChildBlock } from '../util' 3 | 4 | const printOpener = (node, path, print) => { 5 | node[EXPRESSION_NEEDED] = false 6 | node[STRING_NEEDS_QUOTES] = true 7 | const parts = [node.trimLeft ? '{%-' : '{%', ' embed ', path.call(print, 'parent')] 8 | if (node.argument) { 9 | parts.push(indent(concat([line, 'with ', path.call(print, 'argument')]))) 10 | } 11 | parts.push(concat([line, node.trimRightEmbed ? '-%}' : '%}'])) 12 | return group(concat(parts)) 13 | } 14 | 15 | export const printEmbedStatement = (node, path, print) => { 16 | const children = printChildBlock(node, path, print, 'blocks') 17 | const printedOpener = printOpener(node, path, print) 18 | const closing = concat([hardline, node.trimLeftEndembed ? '{%-' : '{%', ' endembed ', node.trimRight ? '-%}' : '%}']) 19 | 20 | return concat([printedOpener, children, closing]) 21 | } 22 | -------------------------------------------------------------------------------- /src/print/ExpressionStatement.js: -------------------------------------------------------------------------------- 1 | import { concat, group, indent, line } from './../util/prettier-doc-builders.js' 2 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES, isContractableNodeType } from '../util' 3 | import { Node } from 'melody-types' 4 | 5 | export const printExpressionStatement = (node, path, print) => { 6 | node[EXPRESSION_NEEDED] = false 7 | node[STRING_NEEDS_QUOTES] = true 8 | const opener = node.trimLeft ? '{{-' : '{{' 9 | const closing = node.trimRight ? '-}}' : '}}' 10 | const shouldContractValue = isContractableNodeType(node.value) && !Node.isObjectExpression(node.value) 11 | const padding = shouldContractValue ? ' ' : line 12 | const printedValue = concat([padding, path.call(print, 'value')]) 13 | const value = shouldContractValue ? printedValue : indent(printedValue) 14 | return group(concat([opener, value, padding, closing])) 15 | } 16 | -------------------------------------------------------------------------------- /src/print/ExtendsStatement.js: -------------------------------------------------------------------------------- 1 | import { concat } from './../util/prettier-doc-builders.js' 2 | import { STRING_NEEDS_QUOTES } from '../util' 3 | 4 | export const printExtendsStatement = (node, path, print) => { 5 | node[STRING_NEEDS_QUOTES] = true 6 | return concat([node.trimLeft ? '{%-' : '{%', ' extends ', path.call(print, 'parentName'), node.trimRight ? ' -%}' : ' %}']) 7 | } 8 | -------------------------------------------------------------------------------- /src/print/FilterBlockStatement.js: -------------------------------------------------------------------------------- 1 | import { concat, group, line, hardline } from './../util/prettier-doc-builders.js' 2 | import { FILTER_BLOCK, printChildBlock } from '../util' 3 | 4 | const printOpeningGroup = (node, path, print) => { 5 | const parts = [node.trimLeft ? '{%- ' : '{% '] 6 | const printedExpression = path.call(print, 'filterExpression') 7 | parts.push(printedExpression, line, node.trimRightFilter ? '-%}' : '%}') 8 | return group(concat(parts)) 9 | } 10 | 11 | export const printFilterBlockStatement = (node, path, print) => { 12 | node[FILTER_BLOCK] = true 13 | const openingGroup = printOpeningGroup(node, path, print) 14 | const body = printChildBlock(node, path, print, 'body') 15 | const closingStatement = concat([hardline, node.trimLeftEndfilter ? '{%-' : '{%', ' endfilter ', node.trimRight ? '-%}' : '%}']) 16 | 17 | return concat([openingGroup, body, closingStatement]) 18 | } 19 | -------------------------------------------------------------------------------- /src/print/FilterExpression.js: -------------------------------------------------------------------------------- 1 | import { group, concat, indent, line, softline, join } from './../util/prettier-doc-builders.js' 2 | import { Node } from 'melody-types' 3 | import { EXPRESSION_NEEDED, INSIDE_OF_STRING, STRING_NEEDS_QUOTES, FILTER_BLOCK, shouldExpressionsBeWrapped, wrapInStringInterpolation, someParentNode, isMultipartExpression, getDeepProperty } from '../util' 4 | 5 | const isInFilterBlock = path => someParentNode(path, node => node[FILTER_BLOCK] === true) 6 | 7 | const printArguments = (node, path, print, nodePath) => { 8 | const hasArguments = node.arguments && node.arguments.length > 0 9 | if (!hasArguments) { 10 | return '' 11 | } 12 | 13 | const printedArguments = path.map(print, ...nodePath, 'arguments') 14 | if (node.arguments.length === 1 && Node.isObjectExpression(node.arguments[0])) { 15 | // Optimization: Avoid additional indentation level 16 | if (node.isDjango) { 17 | return group(concat([':', printedArguments[0]])) 18 | } else { 19 | return group(concat(['(', printedArguments[0], ')'])) 20 | } 21 | } 22 | 23 | if (node.isDjango) { 24 | return group(concat([':', indent(concat([softline, join(concat([',', line]), printedArguments)])), softline])) 25 | } else { 26 | return group(concat(['(', indent(concat([softline, join(concat([',', line]), printedArguments)])), softline, ')'])) 27 | } 28 | } 29 | 30 | const printOneFilterExpression = (node, path, print, nodePath) => { 31 | const args = printArguments(node, path, print, nodePath) 32 | const filterName = path.call(print, ...nodePath, 'name') 33 | return concat([filterName, args]) 34 | } 35 | 36 | const joinFilters = (filterExpressions, space = '') => { 37 | return join(concat([space === '' ? softline : line, '|', space]), filterExpressions) 38 | } 39 | 40 | export const printFilterExpression = (node, path, print, options) => { 41 | let currentNode = node 42 | node[EXPRESSION_NEEDED] = false 43 | node[STRING_NEEDS_QUOTES] = true 44 | const spaceAroundPipe = options.twigFollowOfficialCodingStandards === false 45 | const space = spaceAroundPipe ? ' ' : '' 46 | const pathToFinalTarget = ['target'] 47 | let filterExpressions = [printOneFilterExpression(node, path, print, [])] 48 | 49 | // Here, we do not do the usual recursion using path.call(), but 50 | // instead traverse the chain of FilterExpressions ourselves (in 51 | // case there are multiple chained FilterExpressions, that is). 52 | // Reason: For a proper layout like this 53 | // "Some text" 54 | // | filter1 55 | // | filter2(arg) 56 | // | filter3 57 | // we need all the individual filter expressions in one group. This 58 | // can only be achieved by collecting them manually in the top-level 59 | // FilterExpression node. 60 | while (Node.isFilterExpression(currentNode.target)) { 61 | filterExpressions.unshift(printOneFilterExpression(currentNode.target, path, print, pathToFinalTarget)) 62 | pathToFinalTarget.push('target') // Go one level deeper 63 | currentNode = currentNode.target 64 | } 65 | 66 | const finalTarget = path.call(print, ...pathToFinalTarget) 67 | const isFilterBlock = isInFilterBlock(path) // Special case of FilterBlockStatement 68 | const targetNeedsParentheses = isMultipartExpression(getDeepProperty(node, ...pathToFinalTarget)) 69 | const parts = [] 70 | if (targetNeedsParentheses) { 71 | parts.push('(') 72 | } 73 | parts.push(finalTarget) 74 | if (targetNeedsParentheses) { 75 | parts.push(')') 76 | } 77 | if (isFilterBlock) { 78 | parts.push(concat([' ', filterExpressions[0]])) 79 | filterExpressions = filterExpressions.slice(1) 80 | } 81 | if (filterExpressions.length === 1) { 82 | // No breaks and indentation for just one expression 83 | parts.push(`${space}|${space}`, filterExpressions[0]) 84 | } else if (filterExpressions.length > 1) { 85 | const indentedFilters = concat([spaceAroundPipe ? line : softline, `|${space}`, joinFilters(filterExpressions, space)]) 86 | parts.push(indent(indentedFilters)) 87 | } 88 | 89 | const kindOfWrap = shouldExpressionsBeWrapped(path) 90 | if (kindOfWrap === EXPRESSION_NEEDED) { 91 | // Instead of using wrapExpressionIfNeeded(), we manually 92 | // wrap here, to avoid a line break between the curly braces 93 | parts.push(' }}') 94 | parts.unshift('{{ ') 95 | } else if (kindOfWrap === INSIDE_OF_STRING) { 96 | wrapInStringInterpolation(parts) 97 | } 98 | 99 | return group(concat(parts)) 100 | } 101 | -------------------------------------------------------------------------------- /src/print/FlushStatement.js: -------------------------------------------------------------------------------- 1 | export const printFlushStatement = (node, path, print) => { 2 | const dashLeft = node.trimLeft ? '-' : '' 3 | const dashRight = node.trimRight ? '-' : '' 4 | return `{%${dashLeft} flush ${dashRight}%}` 5 | } 6 | -------------------------------------------------------------------------------- /src/print/ForStatement.js: -------------------------------------------------------------------------------- 1 | import { EXPRESSION_NEEDED, indentWithHardline, isWhitespaceNode } from '../util' 2 | import { concat, group, hardline, indent, line } from './../util/prettier-doc-builders.js' 3 | 4 | const printFor = (node, path, print) => { 5 | const parts = [node.trimLeft ? '{%-' : '{%', ' for '] 6 | if (node.keyTarget) { 7 | parts.push(path.call(print, 'keyTarget'), ', ') 8 | } 9 | parts.push(path.call(print, 'valueTarget'), ' in ', path.call(print, 'sequence')) 10 | if (node.condition) { 11 | parts.push(indent(concat([line, 'if ', path.call(print, 'condition')]))) 12 | } 13 | if (node.reversed) { 14 | parts.push(' reversed') 15 | } 16 | if (node.sorted) { 17 | parts.push(' sorted') 18 | } 19 | parts.push(concat([' ', node.trimRightFor ? '-%}' : '%}'])) 20 | return group(concat(parts)) 21 | } 22 | 23 | export const printForStatement = (node, path, print) => { 24 | node[EXPRESSION_NEEDED] = false 25 | const parts = [printFor(node, path, print)] 26 | const isBodyEmpty = node.body.expressions.length === 0 || (node.body.expressions.length === 1 && isWhitespaceNode(node.body.expressions[0])) 27 | const printedChildren = path.call(print, 'body') 28 | if (!isBodyEmpty || node.otherwise) { 29 | parts.push(indentWithHardline(printedChildren)) 30 | } 31 | if (node.otherwise) { 32 | parts.push(hardline, node.trimLeftElse ? '{%-' : '{%', ' ' + node.otherwiseText + ' ', node.trimRightElse ? '-%}' : '%}') 33 | const printedOtherwise = path.call(print, 'otherwise') 34 | parts.push(indentWithHardline(printedOtherwise)) 35 | } 36 | parts.push(isBodyEmpty ? '' : hardline, node.trimLeftEndfor ? '{%-' : '{%', ' endfor ', node.trimRight ? '-%}' : '%}') 37 | 38 | return concat(parts) 39 | } 40 | -------------------------------------------------------------------------------- /src/print/FromStatement.js: -------------------------------------------------------------------------------- 1 | import { group, concat, join, line, indent } from './../util/prettier-doc-builders.js' 2 | import { STRING_NEEDS_QUOTES } from '../util' 3 | 4 | const printImportDeclaration = node => { 5 | const parts = [node.key.name] 6 | if (node.key.name !== node.alias.name) { 7 | parts.push(' as ', node.alias.name) 8 | } 9 | return concat(parts) 10 | } 11 | 12 | export const printFromStatement = (node, path, print) => { 13 | node[STRING_NEEDS_QUOTES] = true 14 | // Unfortunately, ImportDeclaration has different 15 | // formatting needs here compared to when used 16 | // standalone. Therefore, we collect them manually. 17 | const mappedImports = node.imports.map(printImportDeclaration) 18 | const indentedParts = indent(concat([line, join(concat([',', line]), mappedImports)])) 19 | return group(concat([node.trimLeft ? '{%-' : '{%', ' from ', path.call(print, 'source'), ' import', indentedParts, line, node.trimRight ? '-%}' : '%}'])) 20 | } 21 | -------------------------------------------------------------------------------- /src/print/GenericToken.js: -------------------------------------------------------------------------------- 1 | export const printGenericToken = (node, path, print) => { 2 | return node.tokenText 3 | } 4 | -------------------------------------------------------------------------------- /src/print/GenericTwigTag.js: -------------------------------------------------------------------------------- 1 | import { concat, hardline } from './../util/prettier-doc-builders.js' 2 | import { Node } from 'melody-types' 3 | import { STRING_NEEDS_QUOTES, indentWithHardline, printSingleTwigTag, isEmptySequence } from '../util' 4 | 5 | export const printGenericTwigTag = (node, path, print) => { 6 | node[STRING_NEEDS_QUOTES] = true 7 | const openingTag = printSingleTwigTag(node, path, print) 8 | const parts = [openingTag] 9 | const printedSections = path.map(print, 'sections') 10 | node.sections.forEach((section, i) => { 11 | if (Node.isGenericTwigTag(section)) { 12 | parts.push(concat([hardline, printedSections[i]])) 13 | } else { 14 | if (!isEmptySequence(section)) { 15 | // Indent 16 | parts.push(indentWithHardline(printedSections[i])) 17 | } 18 | } 19 | }) 20 | return concat(parts) 21 | } 22 | -------------------------------------------------------------------------------- /src/print/HtmlComment.js: -------------------------------------------------------------------------------- 1 | import { concat, join, indent, hardline } from './../util/prettier-doc-builders.js' 2 | import { createTextGroups, stripHtmlCommentChars, normalizeHtmlComment, countNewlines } from '../util' 3 | 4 | export const printHtmlComment = (node, path, print) => { 5 | const commentText = stripHtmlCommentChars(node.value.value || '') 6 | 7 | const numNewlines = countNewlines(commentText) 8 | if (numNewlines === 0) { 9 | return normalizeHtmlComment(commentText) 10 | } 11 | 12 | return concat(['']) 13 | } 14 | -------------------------------------------------------------------------------- /src/print/Identifier.js: -------------------------------------------------------------------------------- 1 | import { group, concat } from './../util/prettier-doc-builders.js' 2 | import { EXPRESSION_NEEDED, wrapExpressionIfNeeded } from '../util' 3 | 4 | export const printIdentifier = (node, path) => { 5 | node[EXPRESSION_NEEDED] = false 6 | 7 | const parts = [node.name] 8 | wrapExpressionIfNeeded(path, parts, node) 9 | const result = concat(parts) 10 | return parts.length === 1 ? result : group(result) 11 | } 12 | -------------------------------------------------------------------------------- /src/print/IfStatement.js: -------------------------------------------------------------------------------- 1 | import { Node } from 'melody-types' 2 | import { EXPRESSION_NEEDED, hasNoNewlines, PRESERVE_LEADING_WHITESPACE, PRESERVE_TRAILING_WHITESPACE, printChildBlock } from '../util' 3 | import { concat, group, hardline, indent, line } from './../util/prettier-doc-builders.js' 4 | 5 | const IS_ELSEIF = Symbol('IS_ELSEIF') 6 | 7 | export const printIfStatement = (node, path, print) => { 8 | node[EXPRESSION_NEEDED] = false 9 | const hasElseBranch = Array.isArray(node.alternate) && node.alternate.length > 0 10 | const hasElseIfBranch = Node.isIfStatement(node.alternate) 11 | const isElseIf = node[IS_ELSEIF] === true 12 | const isEmptyIf = node.consequent.length === 0 13 | const hasOneChild = node.consequent.length === 1 14 | const firstChild = node.consequent[0] 15 | const printInline = !isElseIf && !node.alternate && (isEmptyIf || (hasOneChild && !Node.isElement(firstChild) && (!Node.isPrintTextStatement(firstChild) || hasNoNewlines(firstChild.value.value)))) 16 | 17 | // Preserve no-newline white space in single text node child 18 | if (hasOneChild && Node.isPrintTextStatement(firstChild) && hasNoNewlines(firstChild.value.value)) { 19 | firstChild[PRESERVE_LEADING_WHITESPACE] = true 20 | firstChild[PRESERVE_TRAILING_WHITESPACE] = true 21 | } 22 | 23 | const ifClause = group(concat([node.trimLeft ? '{%- ' : '{% ', isElseIf ? node.elseifText : 'if', indent(concat([line, path.call(print, 'test')])), ' ', node.trimRightIf ? '-%}' : '%}'])) 24 | const ifBody = printInline ? (isEmptyIf ? '' : path.call(print, 'consequent', '0')) : printChildBlock(node, path, print, 'consequent') 25 | const parts = [ifClause, ifBody] 26 | if (hasElseBranch) { 27 | parts.push(hardline, node.trimLeftElse ? '{%-' : '{%', ' else ', node.trimRightElse ? '-%}' : '%}') 28 | parts.push(printChildBlock(node, path, print, 'alternate')) 29 | } else if (hasElseIfBranch) { 30 | node.alternate[IS_ELSEIF] = true 31 | parts.push(hardline) 32 | parts.push(path.call(print, 'alternate')) 33 | } 34 | // The {% endif %} will be taken care of by the "root" if statement 35 | if (!isElseIf) { 36 | parts.push(printInline ? '' : hardline, node.trimLeftEndif ? '{%-' : '{%', ' endif ', node.trimRight ? '-%}' : '%}') 37 | } 38 | return concat(parts) 39 | } 40 | -------------------------------------------------------------------------------- /src/print/ImportDeclaration.js: -------------------------------------------------------------------------------- 1 | import { group, concat, line, indent } from './../util/prettier-doc-builders.js' 2 | import { STRING_NEEDS_QUOTES } from '../util' 3 | 4 | export const printImportDeclaration = (node, path, print) => { 5 | node[STRING_NEEDS_QUOTES] = true 6 | return group(concat([node.trimLeft ? '{%-' : '{%', ' import ', path.call(print, 'key'), indent(concat([line, 'as ', path.call(print, 'alias')])), line, node.trimRight ? '-%}' : '%}'])) 7 | } 8 | -------------------------------------------------------------------------------- /src/print/IncludeStatement.js: -------------------------------------------------------------------------------- 1 | import { STRING_NEEDS_QUOTES } from '../util' 2 | import { concat, group, join, softline } from './../util/prettier-doc-builders.js' 3 | 4 | export const printIncludeStatement = (node, path, print) => { 5 | node[STRING_NEEDS_QUOTES] = true 6 | const parts = [node.trimLeft ? '{%-' : '{%', ' include ', path.call(print, 'source')] 7 | if (node.argument) { 8 | const printedArguments = path.map(print, 'argument') 9 | parts.push(' with ') 10 | 11 | let gpargs = group(concat([concat([softline, join(' ', printedArguments)]), softline])) 12 | 13 | parts.push(gpargs) 14 | } 15 | 16 | if (node.contextFree) { 17 | parts.push(' only') 18 | } 19 | parts.push(node.trimRight ? ' -%}' : ' %}') 20 | return group(concat(parts)) 21 | } 22 | -------------------------------------------------------------------------------- /src/print/MacroDeclarationStatement.js: -------------------------------------------------------------------------------- 1 | import { group, join, concat, line, softline, hardline, indent } from './../util/prettier-doc-builders.js' 2 | 3 | const printOpener = (node, path, print) => { 4 | const parts = [node.trimLeft ? '{%-' : '{%', ' macro ', path.call(print, 'name'), '('] 5 | const mappedArguments = path.map(print, 'arguments') 6 | const joinedArguments = join(concat([',', line]), mappedArguments) 7 | parts.push(indent(concat([softline, joinedArguments]))) 8 | parts.push(')', line, node.trimRightMacro ? '-%}' : '%}') 9 | return group(concat(parts)) 10 | } 11 | 12 | export const printMacroDeclarationStatement = (node, path, print) => { 13 | const parts = [printOpener(node, path, print)] 14 | parts.push(indent(concat([hardline, path.call(print, 'body')]))) 15 | parts.push(hardline, node.trimLeftEndmacro ? '{%-' : '{%', ' endmacro ', node.trimRight ? '-%}' : '%}') 16 | return concat(parts) 17 | } 18 | -------------------------------------------------------------------------------- /src/print/MemberExpression.js: -------------------------------------------------------------------------------- 1 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES, wrapExpressionIfNeeded } from '../util' 2 | import { concat, group } from './../util/prettier-doc-builders.js' 3 | 4 | export const printMemberExpression = (node, path, print, options) => { 5 | node[EXPRESSION_NEEDED] = false 6 | node[STRING_NEEDS_QUOTES] = true 7 | const parts = [path.call(print, 'object')] 8 | const squareBrackets = node.computed && options['templateType'] != 'django' 9 | if (!squareBrackets) { 10 | node[STRING_NEEDS_QUOTES] = false 11 | } 12 | parts.push(squareBrackets ? '[' : '.') 13 | parts.push(path.call(print, 'property')) 14 | if (squareBrackets) { 15 | parts.push(']') 16 | } 17 | wrapExpressionIfNeeded(path, parts, node) 18 | return group(concat(parts)) 19 | } 20 | -------------------------------------------------------------------------------- /src/print/MountStatement.js: -------------------------------------------------------------------------------- 1 | import { group, concat, indent, line, hardline } from './../util/prettier-doc-builders.js' 2 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES } from '../util' 3 | 4 | const formatDelay = delay => { 5 | return '' + delay / 1000 + 's' 6 | } 7 | 8 | const buildOpener = (node, path, print) => { 9 | const result = [] 10 | const firstGroup = [node.trimLeft ? '{%-' : '{%', ' mount'] 11 | if (node.async === true) { 12 | firstGroup.push(' async') 13 | } 14 | 15 | if (node.name) { 16 | firstGroup.push(' ', path.call(print, 'name')) 17 | } 18 | 19 | if (node.name && node.source) { 20 | firstGroup.push(' from') 21 | } 22 | 23 | if (node.source) { 24 | firstGroup.push(' ', path.call(print, 'source')) 25 | } 26 | 27 | if (node.key) { 28 | firstGroup.push(indent(concat([line, 'as ', path.call(print, 'key')]))) 29 | } 30 | result.push(group(concat(firstGroup))) 31 | if (node.argument) { 32 | result.push(indent(concat([' with ', path.call(print, 'argument')]))) 33 | } 34 | if (node.delayBy) { 35 | result.push(indent(concat([line, 'delay placeholder by ', formatDelay(node.delayBy)]))) 36 | } 37 | const trimRightMount = node.body || node.otherwise ? node.trimRightMount : node.trimRight 38 | result.push(concat([line, trimRightMount ? '-%}' : '%}'])) 39 | return group(concat(result)) 40 | } 41 | 42 | const buildBody = (path, print) => { 43 | return indent(concat([hardline, path.call(print, 'body')])) 44 | } 45 | 46 | const buildErrorHandling = (node, path, print) => { 47 | const parts = [] 48 | parts.push(concat([hardline, node.trimLeftCatch ? '{%-' : '{%', ' catch '])) 49 | if (node.errorVariableName) { 50 | parts.push(path.call(print, 'errorVariableName'), ' ') 51 | } 52 | parts.push(node.trimRightCatch ? '-%}' : '%}') 53 | parts.push(indent(concat([hardline, path.call(print, 'otherwise')]))) 54 | return concat(parts) 55 | } 56 | 57 | export const printMountStatement = (node, path, print) => { 58 | node[EXPRESSION_NEEDED] = false 59 | node[STRING_NEEDS_QUOTES] = true 60 | const parts = [buildOpener(node, path, print)] 61 | if (node.body) { 62 | parts.push(buildBody(path, print)) 63 | } 64 | if (node.otherwise) { 65 | parts.push(buildErrorHandling(node, path, print)) 66 | } 67 | if (node.body || node.otherwise) { 68 | parts.push(concat([hardline, node.trimLeftEndmount ? '{%-' : '{%', ' endmount ', node.trimRight ? '-%}' : '%}'])) 69 | } 70 | 71 | return concat(parts) 72 | } 73 | -------------------------------------------------------------------------------- /src/print/NamedArgumentExpression.js: -------------------------------------------------------------------------------- 1 | import { STRING_NEEDS_QUOTES } from '../util' 2 | import { concat } from './../util/prettier-doc-builders.js' 3 | 4 | export const printNamedArgumentExpression = (node, path, print) => { 5 | node[STRING_NEEDS_QUOTES] = true 6 | const printedName = path.call(print, 'name') 7 | const printedValue = path.call(print, 'value') 8 | return concat([printedName, '=', printedValue]) 9 | } 10 | -------------------------------------------------------------------------------- /src/print/ObjectExpression.js: -------------------------------------------------------------------------------- 1 | import { group, concat, line, hardline, indent, join } from './../util/prettier-doc-builders.js' 2 | import { EXPRESSION_NEEDED, wrapExpressionIfNeeded } from '../util' 3 | 4 | export const printObjectExpression = (node, path, print, options) => { 5 | if (node.properties.length === 0) { 6 | return '{}' 7 | } 8 | node[EXPRESSION_NEEDED] = false 9 | const mappedElements = path.map(print, 'properties') 10 | const separator = options.twigAlwaysBreakObjects ? hardline : line 11 | const indentedContent = concat([line, join(concat([',', separator]), mappedElements)]) 12 | 13 | const parts = ['{', indent(indentedContent), separator, '}'] 14 | wrapExpressionIfNeeded(path, parts, node) 15 | 16 | return group(concat(parts)) 17 | } 18 | -------------------------------------------------------------------------------- /src/print/ObjectProperty.js: -------------------------------------------------------------------------------- 1 | import { concat } from './../util/prettier-doc-builders.js' 2 | import { isValidIdentifierName, STRING_NEEDS_QUOTES } from '../util' 3 | import { Node } from 'melody-types' 4 | 5 | export const printObjectProperty = (node, path, print, options) => { 6 | node[STRING_NEEDS_QUOTES] = !node.computed && Node.isStringLiteral(node.key) && !isValidIdentifierName(node.key.value) 7 | const shouldPrintKeyAsString = node.key.wasImplicitConcatenation 8 | const needsParentheses = node.computed && !shouldPrintKeyAsString 9 | const parts = [] 10 | if (needsParentheses) { 11 | parts.push('(') 12 | } 13 | parts.push(path.call(print, 'key')) 14 | if (needsParentheses) { 15 | parts.push(')') 16 | } 17 | parts.push(': ') 18 | node[STRING_NEEDS_QUOTES] = true 19 | parts.push(path.call(print, 'value')) 20 | return concat(parts) 21 | } 22 | -------------------------------------------------------------------------------- /src/print/SequenceExpression.js: -------------------------------------------------------------------------------- 1 | import { concat, hardline } from './../util/prettier-doc-builders.js' 2 | import { removeSurroundingWhitespace, printChildGroups, isRootNode, STRING_NEEDS_QUOTES } from '../util' 3 | 4 | export const printSequenceExpression = (node, path, print) => { 5 | node[STRING_NEEDS_QUOTES] = false 6 | node.expressions = removeSurroundingWhitespace(node.expressions) 7 | const items = printChildGroups(node, path, print, 'expressions') 8 | if (isRootNode(path)) { 9 | return concat([...items, hardline]) 10 | } 11 | return concat(items) 12 | } 13 | -------------------------------------------------------------------------------- /src/print/SetStatement.js: -------------------------------------------------------------------------------- 1 | import { group, concat, line, hardline } from './../util/prettier-doc-builders.js' 2 | import { printChildBlock, isNotExpression, STRING_NEEDS_QUOTES, GROUP_TOP_LEVEL_LOGICAL } from '../util' 3 | import { Node } from 'melody-types' 4 | 5 | const shouldAvoidBreakBeforeClosing = valueNode => Node.isObjectExpression(valueNode) || isNotExpression(valueNode) || Node.isArrayExpression(valueNode) 6 | 7 | const buildSetStatement = (node, path, print, assignmentIndex) => { 8 | const varDeclaration = node.assignments[assignmentIndex] 9 | varDeclaration[GROUP_TOP_LEVEL_LOGICAL] = false 10 | const avoidBreakBeforeClosing = shouldAvoidBreakBeforeClosing(varDeclaration.value) 11 | 12 | return group(concat([node.trimLeft ? '{%-' : '{%', ' set ', path.call(print, 'assignments', assignmentIndex), avoidBreakBeforeClosing ? ' ' : line, node.trimRight ? '-%}' : '%}'])) 13 | } 14 | 15 | const isEmbracingSet = node => { 16 | return Array.isArray(node.assignments) && node.assignments.length === 1 && Array.isArray(node.assignments[0].value) 17 | } 18 | 19 | const printRegularSet = (node, path, print) => { 20 | const parts = [] 21 | const hasAssignments = Array.isArray(node.assignments) && node.assignments.length > 0 22 | if (hasAssignments) { 23 | node.assignments.forEach((_, index) => { 24 | if (parts.length > 0) { 25 | parts.push(hardline) 26 | } 27 | parts.push(buildSetStatement(node, path, print, index)) 28 | }) 29 | } 30 | return concat(parts) 31 | } 32 | 33 | const printEmbracingSet = (node, path, print) => { 34 | const parts = [node.trimLeft ? '{%-' : '{%', ' set ', path.call(print, 'assignments', '0', 'name'), node.trimRightSet ? ' -%}' : ' %}'] 35 | node[STRING_NEEDS_QUOTES] = false 36 | const printedContents = printChildBlock(node, path, print, 'assignments', '0', 'value') 37 | // const printedContents = path.map(print, "assignments", "0", "value"); 38 | parts.push(printedContents) 39 | parts.push(hardline, node.trimLeftEndset ? '{%-' : '{%', ' endset ', node.trimRight ? '-%}' : '%}') 40 | return concat(parts) 41 | } 42 | 43 | export const printSetStatement = (node, path, print) => { 44 | node[STRING_NEEDS_QUOTES] = true 45 | if (isEmbracingSet(node)) { 46 | return printEmbracingSet(node, path, print) 47 | } 48 | return printRegularSet(node, path, print) 49 | } 50 | -------------------------------------------------------------------------------- /src/print/SliceExpression.js: -------------------------------------------------------------------------------- 1 | import { concat } from './../util/prettier-doc-builders.js' 2 | 3 | export const printSliceExpression = (node, path, print) => { 4 | const printedTarget = path.call(print, 'target') 5 | const printedStart = node.start ? path.call(print, 'start') : '' 6 | const printedEnd = node.end ? path.call(print, 'end') : '' 7 | return concat([printedTarget, '[', printedStart, ':', printedEnd, ']']) 8 | } 9 | -------------------------------------------------------------------------------- /src/print/SpacelessBlock.js: -------------------------------------------------------------------------------- 1 | import { concat, hardline, group } from './../util/prettier-doc-builders.js' 2 | import { printChildBlock } from '../util' 3 | 4 | export const printSpacelessBlock = (node, path, print) => { 5 | const parts = [node.trimLeft ? '{%-' : '{%', ' spaceless ', node.trimRightSpaceless ? '-%}' : '%}'] 6 | parts.push(printChildBlock(node, path, print, 'body')) 7 | parts.push(hardline) 8 | parts.push(node.trimLeftEndspaceless ? '{%-' : '{%', ' endspaceless ', node.trimRight ? '-%}' : '%}') 9 | const result = group(concat(parts)) 10 | return result 11 | } 12 | -------------------------------------------------------------------------------- /src/print/StringLiteral.js: -------------------------------------------------------------------------------- 1 | import { firstValueInAncestorChain, quoteChar, STRING_NEEDS_QUOTES, OVERRIDE_QUOTE_CHAR } from '../util' 2 | 3 | const isUnmaskedOccurrence = (s, pos) => { 4 | return pos === 0 || s[pos - 1] !== '\\' 5 | } 6 | 7 | const containsUnmasked = char => s => { 8 | let pos = s.indexOf(char) 9 | while (pos >= 0) { 10 | if (isUnmaskedOccurrence(s, pos)) { 11 | return true 12 | } 13 | pos = s.indexOf(char, pos + 1) 14 | } 15 | return false 16 | } 17 | 18 | const containsUnmaskedSingleQuote = containsUnmasked("'") 19 | const containsUnmaskedDoubleQuote = containsUnmasked('"') 20 | 21 | const getQuoteChar = (s, options) => { 22 | if (containsUnmaskedSingleQuote(s)) { 23 | return '"' 24 | } 25 | if (containsUnmaskedDoubleQuote(s)) { 26 | return "'" 27 | } 28 | return quoteChar(options) 29 | } 30 | 31 | export const printStringLiteral = (node, path, print, options) => { 32 | // The structure this string literal is part of 33 | // determines if we need quotes or not 34 | const needsQuotes = firstValueInAncestorChain(path, STRING_NEEDS_QUOTES, false) 35 | // In case of a string with interpolations, only double quotes 36 | // are allowed. This is then indicated by OVERRIDE_QUOTE_CHAR 37 | // in an ancestor. 38 | const overridingQuoteChar = firstValueInAncestorChain(path, OVERRIDE_QUOTE_CHAR, null) 39 | 40 | if (needsQuotes) { 41 | const quote = overridingQuoteChar ? overridingQuoteChar : getQuoteChar(node.value, options) 42 | return quote + node.value + quote 43 | } 44 | 45 | return node.value 46 | } 47 | -------------------------------------------------------------------------------- /src/print/TestExpression.js: -------------------------------------------------------------------------------- 1 | import { findParentNode } from '../util' 2 | import { concat, group, indent, join, line, softline } from './../util/prettier-doc-builders.js' 3 | 4 | const textMap = { 5 | TestNullExpression: 'null', 6 | TestDivisibleByExpression: 'divisible by', 7 | TestDefinedExpression: 'defined', 8 | TestEmptyExpression: 'empty', 9 | TestEvenExpression: 'even', 10 | TestOddExpression: 'odd', 11 | TestIterableExpression: 'iterable', 12 | TestSameAsExpression: 'same as', 13 | TestTrueExpression: "True", 14 | TestFalseExpression: "False", 15 | TestNoneExpression: "None" 16 | } 17 | 18 | const isNegator = node => node.constructor.name === 'UnarySubclass' && node.operator === 'not' 19 | 20 | export const printTestExpression = (node, path, print) => { 21 | const expressionType = node.__proto__.type 22 | const parts = [path.call(print, 'expression'), ' is '] 23 | const parent = findParentNode(path) 24 | const hasArguments = Array.isArray(node.arguments) && node.arguments.length > 0 25 | if (isNegator(parent)) { 26 | parts.push('not ') 27 | } 28 | if (!textMap[expressionType]) { 29 | console.error('TestExpression: No text for ' + expressionType + ' defined') 30 | } else { 31 | parts.push(textMap[expressionType]) 32 | } 33 | if (hasArguments) { 34 | const printedArguments = path.map(print, 'arguments') 35 | const joinedArguments = join(concat([',', line]), printedArguments) 36 | parts.push(group(concat(['(', indent(concat([softline, joinedArguments])), softline, ')']))) 37 | } 38 | 39 | return concat(parts) 40 | } 41 | -------------------------------------------------------------------------------- /src/print/TextStatement.js: -------------------------------------------------------------------------------- 1 | import { concat, line, join, hardline } from './../util/prettier-doc-builders' 2 | import { isWhitespaceOnly, countNewlines, createTextGroups, PRESERVE_LEADING_WHITESPACE, PRESERVE_TRAILING_WHITESPACE, NEWLINES_ONLY } from '../util' 3 | 4 | const newlinesOnly = (s, preserveWhitespace = true) => { 5 | const numNewlines = countNewlines(s) 6 | if (numNewlines === 0) { 7 | return preserveWhitespace ? line : '' 8 | } else if (numNewlines === 1) { 9 | return hardline 10 | } 11 | return concat([hardline, hardline]) 12 | } 13 | 14 | export const printTextStatement = (node, path, print) => { 15 | // Check for special values that might have been 16 | // computed during preprocessing 17 | const preserveLeadingWhitespace = node[PRESERVE_LEADING_WHITESPACE] === true 18 | const preserveTrailingWhitespace = node[PRESERVE_TRAILING_WHITESPACE] === true 19 | 20 | const rawString = path.call(print, 'value') 21 | if (isWhitespaceOnly(rawString) && node[NEWLINES_ONLY]) { 22 | return newlinesOnly(rawString) 23 | } 24 | 25 | const textGroups = createTextGroups(rawString, preserveLeadingWhitespace, preserveTrailingWhitespace) 26 | 27 | return join(concat([hardline, hardline]), textGroups) 28 | } 29 | -------------------------------------------------------------------------------- /src/print/TwigComment.js: -------------------------------------------------------------------------------- 1 | import { concat } from '../util/prettier-doc-builders.js' 2 | import { createTextGroups, stripTwigCommentChars, normalizeTwigComment, countNewlines } from '../util' 3 | 4 | export const printTwigComment = node => { 5 | const originalText = node.value.value || '' 6 | const commentText = stripTwigCommentChars(originalText) 7 | const trimLeft = originalText.length >= 3 ? originalText[2] === '-' : false 8 | const trimRight = originalText.length >= 3 ? originalText.slice(-3, -2) === '-' : false 9 | 10 | const numNewlines = countNewlines(commentText) 11 | if (numNewlines === 0) { 12 | return normalizeTwigComment(commentText, trimLeft, trimRight) 13 | } 14 | 15 | return concat([trimLeft ? '{#-' : '{#', commentText, trimRight ? '-#}' : '#}']) 16 | } 17 | -------------------------------------------------------------------------------- /src/print/UnaryExpression.js: -------------------------------------------------------------------------------- 1 | import { concat, group } from './../util/prettier-doc-builders.js' 2 | import { EXPRESSION_NEEDED, STRING_NEEDS_QUOTES, wrapExpressionIfNeeded } from '../util' 3 | 4 | export const printUnaryExpression = (node, path, print) => { 5 | node[EXPRESSION_NEEDED] = false 6 | node[STRING_NEEDS_QUOTES] = true 7 | const parts = [node.operator, path.call(print, 'argument')] 8 | wrapExpressionIfNeeded(path, parts, node) 9 | return group(concat(parts)) 10 | } 11 | -------------------------------------------------------------------------------- /src/print/UnarySubclass.js: -------------------------------------------------------------------------------- 1 | import { concat, softline, indent, group } from './../util/prettier-doc-builders.js' 2 | import { Node } from 'melody-types' 3 | import { firstValueInAncestorChain, findParentNode, isMultipartExpression, IS_ROOT_LOGICAL_EXPRESSION, GROUP_TOP_LEVEL_LOGICAL } from '../util' 4 | 5 | const argumentNeedsParentheses = node => isMultipartExpression(node) 6 | 7 | const isLogicalOperator = operator => operator === 'not' 8 | 9 | const printLogicalExpression = (node, path, print) => { 10 | const foundRootAbove = firstValueInAncestorChain(path, IS_ROOT_LOGICAL_EXPRESSION, false) 11 | if (!foundRootAbove) { 12 | node[IS_ROOT_LOGICAL_EXPRESSION] = true 13 | } 14 | const parentNode = findParentNode(path) 15 | const shouldGroupOnTopLevel = parentNode[GROUP_TOP_LEVEL_LOGICAL] !== false 16 | 17 | const parts = [node.operator, ' '] 18 | const needsParentheses = argumentNeedsParentheses(node.argument) 19 | const printedArgument = path.call(print, 'argument') 20 | if (needsParentheses) { 21 | parts.push('(', indent(concat([softline, printedArgument])), concat([softline, ')'])) 22 | } else { 23 | parts.push(printedArgument) 24 | } 25 | const result = concat(parts) 26 | const shouldCreateTopLevelGroup = !foundRootAbove && shouldGroupOnTopLevel 27 | 28 | return shouldCreateTopLevelGroup ? group(result) : result 29 | } 30 | 31 | export const printUnarySubclass = (node, path, print) => { 32 | const parts = [] 33 | // Example: a is not same as ... => Here, the "not" is printed "inline" 34 | // Therefore, we do not output it here 35 | const hasTestExpressionArgument = Node.isTestExpression(node.argument) 36 | if (isLogicalOperator(node.operator) && !hasTestExpressionArgument) { 37 | return printLogicalExpression(node, path, print) 38 | } 39 | if (!hasTestExpressionArgument) { 40 | parts.push(node.operator, ' ') 41 | } 42 | parts.push(path.call(print, 'argument')) 43 | return concat(parts) 44 | } 45 | -------------------------------------------------------------------------------- /src/print/UrlStatement.js: -------------------------------------------------------------------------------- 1 | import { STRING_NEEDS_QUOTES } from '../util' 2 | import { concat, group, join, softline } from './../util/prettier-doc-builders.js' 3 | 4 | export const printUrlStatement = (node, path, print) => { 5 | node[STRING_NEEDS_QUOTES] = true 6 | const parts = [node.trimLeft ? '{%-' : '{%', ' url ', path.call(print, 'name')] 7 | if (node.arguments && node.arguments.length > 0) { 8 | // const printedArguments = path.call(print, 'arguments') 9 | 10 | parts.push(' ') 11 | const printedArguments = path.map(print, 'arguments') 12 | 13 | let gpargs = group(concat([concat([softline, join(' ', printedArguments)]), softline])) 14 | 15 | parts.push(gpargs) 16 | } 17 | 18 | parts.push(node.trimRight ? ' -%}' : ' %}') 19 | return concat(parts) 20 | } 21 | -------------------------------------------------------------------------------- /src/print/UseStatement.js: -------------------------------------------------------------------------------- 1 | import { concat, group, indent, join, line } from './../util/prettier-doc-builders.js' 2 | 3 | export const printUseStatement = (node, path, print) => { 4 | const docs = [node.trimLeft ? '{%-' : '{%', ' use "', path.call(print, 'source'), '"'] 5 | const hasAliases = node.aliases && node.aliases.length > 0 6 | if (hasAliases) { 7 | docs.push(' with') 8 | const mappedAliases = path.map(print, 'aliases') 9 | docs.push(indent(concat([line, join(concat([',', line]), mappedAliases)]))) 10 | docs.push(line) 11 | } else { 12 | docs.push(' ') 13 | } 14 | docs.push(node.trimRight ? '-%}' : '%}') 15 | return group(concat(docs)) 16 | } 17 | -------------------------------------------------------------------------------- /src/print/VariableDeclarationStatement.js: -------------------------------------------------------------------------------- 1 | import { concat, line, indent } from './../util/prettier-doc-builders.js' 2 | import { STRING_NEEDS_QUOTES, isContractableNodeType } from '../util' 3 | 4 | export const printVariableDeclarationStatement = (node, path, print) => { 5 | const printedName = path.call(print, 'name') 6 | node[STRING_NEEDS_QUOTES] = true 7 | const printedValue = path.call(print, 'value') 8 | const shouldCondenseLayout = isContractableNodeType(node.value) 9 | const rightHandSide = shouldCondenseLayout ? concat([' ', printedValue]) : indent(concat([line, printedValue])) 10 | 11 | // We are explicitly not returning a group here, because 12 | // a VariableDeclarationStatement is - currently - always 13 | // embedded in a group created by SetStatement. 14 | return concat([printedName, ' =', rightHandSide]) 15 | } 16 | -------------------------------------------------------------------------------- /src/print/WithStatement.js: -------------------------------------------------------------------------------- 1 | import { EXPRESSION_NEEDED, printChildBlock } from '../util' 2 | import { concat, group, hardline, join, softline } from './../util/prettier-doc-builders.js' 3 | 4 | export const printWithStatement = (node, path, print) => { 5 | node[EXPRESSION_NEEDED] = false 6 | 7 | let gpargs = '' 8 | if (node.arguments && node.arguments.length > 0) { 9 | const printedArguments = path.map(print, 'arguments') 10 | 11 | gpargs = group(concat([concat([softline, join(' ', printedArguments)]), softline])) 12 | } 13 | 14 | const hasChildren = Array.isArray(node.body) 15 | 16 | if (hasChildren) { 17 | const opener = concat([node.trimLeft ? '{%-' : '{%', ' with', gpargs === '' ? '' : ' ', gpargs, node.trimRightBlock ? ' -%}' : ' %}']) 18 | const parts = [opener] 19 | if (node.body.length > 0) { 20 | const indentedBody = printChildBlock(node, path, print, 'body') 21 | parts.push(indentedBody) 22 | } 23 | parts.push(hardline) 24 | parts.push(node.trimLeftEndblock ? '{%-' : '{%', ' endwith', node.trimRight ? ' -%}' : ' %}') 25 | 26 | const result = group(concat(parts)) 27 | return result 28 | } else if (Node.isPrintExpressionStatement(node.body)) { 29 | const parts = [node.trimLeft ? '{%-' : '{%', ' with', gpargs === '' ? '' : ' ', gpargs, ' ', path.call(print, 'body', 'value'), node.trimRight ? ' -%}' : ' %}'] 30 | return concat(parts) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | export * from './pluginUtil.js' 2 | export * from './publicSymbols.js' 3 | export * from './publicFunctions.js' 4 | export * from './printFunctions.js' 5 | -------------------------------------------------------------------------------- /src/util/pluginUtil.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import resolve from 'resolve' 3 | 4 | const getPluginPathsFromOptions = options => { 5 | if (options.twigMelodyPlugins && Array.isArray(options.twigMelodyPlugins)) { 6 | return options.twigMelodyPlugins.map(s => s.trim()) 7 | } 8 | return [] 9 | } 10 | 11 | const getProjectRoot = () => { 12 | const parts = __dirname.split(path.sep) 13 | let index = parts.length - 1 14 | let dirName = parts[index] 15 | while (dirName !== 'node_modules' && index > 0) { 16 | index-- 17 | dirName = parts[index] 18 | } 19 | // If we are not inside a "node_modules" folder, just 20 | // strip away "src" and "util" 21 | if (index === 0) { 22 | index = parts.length - 2 23 | } 24 | const subPath = parts.slice(0, index) 25 | const joined = path.join(...subPath) 26 | 27 | // This might contain something like 28 | // Users/jdoe/project 29 | // => leading slash missing, which can cause 30 | // problems. To stay OS independent, let's 31 | // re-add everything that came before the result 32 | // we have so far. 33 | const foundIndex = __dirname.indexOf(joined) 34 | return __dirname.slice(0, foundIndex) + joined 35 | } 36 | 37 | const tryLoadPlugin = pluginPath => { 38 | try { 39 | const projectRoot = getProjectRoot() 40 | const requirePath = resolve.sync(path.resolve(projectRoot, pluginPath)) 41 | // return eval('require')(requirePath) 42 | return require(requirePath) 43 | } catch (e) { 44 | console.error('Could not load plugin path ' + pluginPath) 45 | return undefined 46 | } 47 | } 48 | 49 | const loadPlugins = pluginPaths => { 50 | const result = [] 51 | if (pluginPaths && Array.isArray(pluginPaths)) { 52 | pluginPaths.forEach(pluginPath => { 53 | const loadedPlugin = tryLoadPlugin(pluginPath) 54 | if (loadedPlugin) { 55 | result.push(loadedPlugin) 56 | } 57 | }) 58 | } 59 | return result 60 | } 61 | 62 | const getAdditionalMelodyExtensions = pluginPaths => { 63 | let result = [] 64 | const loadedPlugins = loadPlugins(pluginPaths) 65 | loadedPlugins.forEach(loadedPlugin => { 66 | result = result.concat(loadedPlugin.melodyExtensions) 67 | }) 68 | // Filter out potential "undefined" values 69 | return result.filter(elem => !!elem) 70 | } 71 | 72 | export { getAdditionalMelodyExtensions, getPluginPathsFromOptions, loadPlugins, tryLoadPlugin } 73 | 74 | -------------------------------------------------------------------------------- /src/util/printFunctions.js: -------------------------------------------------------------------------------- 1 | import { Node } from 'melody-types' 2 | import { concat, group, indent, line } from './prettier-doc-builders.js' 3 | 4 | const noSpaceBeforeToken = { 5 | ',': true, 6 | '=': true, 7 | } 8 | const noSpaceAfterToken = { 9 | '=': true 10 | } 11 | 12 | export const printSingleTwigTag = (node, path, print) => { 13 | const opener = node.trimLeft ? '{%-' : '{%' 14 | const parts = [opener, ' ', node.tagName] 15 | const printedParts = path.map(print, 'parts') 16 | if (printedParts.length > 0) { 17 | parts.push(' ', printedParts[0]) 18 | } 19 | const indentedParts = [] 20 | let beforeTokenText = '' 21 | for (let i = 1; i < node.parts.length; i++) { 22 | const part = node.parts[i] 23 | const isToken = Node.isGenericToken(part) 24 | const separator = ((isToken && noSpaceBeforeToken[part.tokenText]) || noSpaceAfterToken[beforeTokenText]) ? '' : line 25 | indentedParts.push(separator, printedParts[i]) 26 | beforeTokenText = part.tokenText 27 | } 28 | if (node.parts.length > 1) { 29 | parts.push(indent(concat(indentedParts))) 30 | } 31 | const closing = node.trimRight ? '-%}' : '%}' 32 | parts.push(line, closing) 33 | return group(concat(parts)) 34 | } 35 | -------------------------------------------------------------------------------- /src/util/publicSymbols.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These symbols are visible to outside users of 3 | * the package. For example, they might be useful 4 | * for plugins. 5 | */ 6 | 7 | /** 8 | * Set this to true on an AST node that might be the 9 | * parent of a StringLiteral node. The StringLiteral 10 | * will be enclosed in quotes when this attribute is 11 | * set to true on the parent. 12 | */ 13 | const STRING_NEEDS_QUOTES = Symbol('STRING_NEEDS_QUOTES') 14 | 15 | /** 16 | * Set to " or ' 17 | * Allows a node type to determine the quote char string 18 | * literals must use. 19 | */ 20 | const OVERRIDE_QUOTE_CHAR = Symbol('OVERRIDE_QUOTE_CHAR') 21 | 22 | /** 23 | * This signals to child nodes that an expression environment 24 | * {{ ... }} has not yet been opened, so they might have 25 | * to open one. Example: An Element node, in its attributes 26 | * array, can directly contain a FilterExpression. Usually, 27 | * a FilterExpression does not open an {{...}} environment, 28 | * but here, it has to. 29 | */ 30 | const EXPRESSION_NEEDED = Symbol('EXPRESSION_NEEDED') 31 | 32 | /** 33 | * Signals to child nodes that they are part of a string, 34 | * which means that expressions have to be interpolated. 35 | * Example: 36 | * "Part #{ partNr } of #{ partCount }" 37 | */ 38 | const INSIDE_OF_STRING = Symbol('INSIDE_OF_STRING') 39 | 40 | /** 41 | * Signals to FilterStatement nodes that they are part of 42 | * a filter block 43 | */ 44 | const FILTER_BLOCK = Symbol('FILTER_BLOCK') 45 | 46 | /** 47 | * Signals to text nodes that they should preserve leading 48 | * whitespace (whitespace at the beginning) 49 | */ 50 | const PRESERVE_LEADING_WHITESPACE = Symbol('PRESERVE_LEADING_WHITESPACE') 51 | 52 | /** 53 | * Signals to text nodes that they should preserve trailing 54 | * whitespace (whitespace at the end) 55 | */ 56 | const PRESERVE_TRAILING_WHITESPACE = Symbol('PRESERVE_TRAILING_WHITESPACE') 57 | 58 | /** 59 | * Signals to text statements that only newlines should be 60 | * preserved when hitting a whitespace-only node 61 | */ 62 | const NEWLINES_ONLY = Symbol('NEWLINES_ONLY') 63 | 64 | /** 65 | * This defaults to TRUE. Only if it is explicitly set to FALSE, 66 | * a logical expression will not create a wrapping group on the 67 | * top level 68 | */ 69 | const GROUP_TOP_LEVEL_LOGICAL = Symbol('GROUP_TOP_LEVEL_LOGICAL') 70 | 71 | /** 72 | * Used to mark the root of a logical expression. Can be important 73 | * for grouping and parenthesis placement. 74 | */ 75 | const IS_ROOT_LOGICAL_EXPRESSION = Symbol('IS_ROOT_LOGICAL_EXPRESSION') 76 | 77 | export { STRING_NEEDS_QUOTES, OVERRIDE_QUOTE_CHAR, INSIDE_OF_STRING, EXPRESSION_NEEDED, FILTER_BLOCK, PRESERVE_TRAILING_WHITESPACE, PRESERVE_LEADING_WHITESPACE, NEWLINES_ONLY, GROUP_TOP_LEVEL_LOGICAL, IS_ROOT_LOGICAL_EXPRESSION } 78 | -------------------------------------------------------------------------------- /tests/Comments/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`htmlComments.melody.twig 1`] = ` 4 | 5 | This is a paragraph 6 | 7 | 8 | 9 | Another paragraph 10 | 11 | 12 | 13 | 14 | A third paragraph 15 | 16 | 19 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 20 | 21 | This is a paragraph 22 | 23 | 24 | 25 | Another paragraph 26 | 27 | 28 | 29 | A third paragraph 30 | 31 | 34 | 35 | `; 36 | 37 | exports[`twigComments.melody.twig 1`] = ` 38 | {# One #} 39 | 40 | {# Two #} 41 | 42 | {#comment #} 43 | 44 | {# comment#} 45 | 46 | {# comment 47 | #} 48 | 49 | {% if searchResultFailing %} 50 | {# This is a Twig comment #} 51 |
  • No results found
  • 52 | {% endif %} 53 | 54 | {#- comment -#} 55 | 56 | {#- 57 | comment 58 | with multiple lines 59 | -#} 60 | 61 | {## 62 | # Illustration Hotel Interaction 63 | # 64 | # This image is just of decorative nature and doesn't contain relevant information 65 | # for visually impaired users. 66 | #} 67 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 68 | {# One #} 69 | 70 | {# Two #} 71 | 72 | {# comment #} 73 | 74 | {# comment #} 75 | 76 | {# comment 77 | #} 78 | 79 | {% if searchResultFailing %} 80 | {# This is a Twig comment #} 81 |
  • No results found
  • 82 | {% endif %} 83 | 84 | {#- comment -#} 85 | 86 | {#- 87 | comment 88 | with multiple lines 89 | -#} 90 | 91 | {## 92 | # Illustration Hotel Interaction 93 | # 94 | # This image is just of decorative nature and doesn't contain relevant information 95 | # for visually impaired users. 96 | #} 97 | 98 | `; 99 | -------------------------------------------------------------------------------- /tests/Comments/htmlComments.melody.twig: -------------------------------------------------------------------------------- 1 | 2 | This is a paragraph 3 | 4 | 5 | 6 | Another paragraph 7 | 8 | 9 | 10 | 11 | A third paragraph 12 | 13 | 16 | -------------------------------------------------------------------------------- /tests/Comments/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/Comments/twigComments.melody.twig: -------------------------------------------------------------------------------- 1 | {# One #} 2 | 3 | {# Two #} 4 | 5 | {#comment #} 6 | 7 | {# comment#} 8 | 9 | {# comment 10 | #} 11 | 12 | {% if searchResultFailing %} 13 | {# This is a Twig comment #} 14 |
  • No results found
  • 15 | {% endif %} 16 | 17 | {#- comment -#} 18 | 19 | {#- 20 | comment 21 | with multiple lines 22 | -#} 23 | 24 | {## 25 | # Illustration Hotel Interaction 26 | # 27 | # This image is just of decorative nature and doesn't contain relevant information 28 | # for visually impaired users. 29 | #} 30 | -------------------------------------------------------------------------------- /tests/ConstantValue/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`constant-value-int.melody.twig 1`] = ` 4 | 123 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 123 7 | 8 | `; 9 | 10 | exports[`constant-value-string.melody.twig 1`] = ` 11 | Test string 12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | Test string 14 | 15 | `; 16 | 17 | exports[`special-cases.melody.twig 1`] = ` 18 | {% if isRTL %}‎{% endif %} 19 | 20 | {% if searchResultFailing %} 21 |
  • No results found
  • 22 | {% endif %} 23 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 24 | {% if isRTL %}‎{% endif %} 25 | 26 | {% if searchResultFailing %} 27 |
  • No results found
  • 28 | {% endif %} 29 | 30 | `; 31 | -------------------------------------------------------------------------------- /tests/ConstantValue/constant-value-int.melody.twig: -------------------------------------------------------------------------------- 1 | 123 2 | -------------------------------------------------------------------------------- /tests/ConstantValue/constant-value-string.melody.twig: -------------------------------------------------------------------------------- 1 | Test string 2 | -------------------------------------------------------------------------------- /tests/ConstantValue/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/ConstantValue/special-cases.melody.twig: -------------------------------------------------------------------------------- 1 | {% if isRTL %}‎{% endif %} 2 | 3 | {% if searchResultFailing %} 4 |
  • No results found
  • 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /tests/ControlStructures/for.melody.twig: -------------------------------------------------------------------------------- 1 |
      2 | {% for item in items %} 3 |
    • 4 | {{ loop.index0 // 2 }} {{ item.name }} {{ loop.index }} 5 |
    • 6 | {% endfor %} 7 |
    8 | 9 |
      10 | {%- for item in items -%} 11 |
    • 12 | {{ loop.index0 // 2 }} {{ item.name }} {{ loop.index }} 13 |
    • 14 | {%- endfor -%} 15 |
    16 | -------------------------------------------------------------------------------- /tests/ControlStructures/forIfElse.melody.twig: -------------------------------------------------------------------------------- 1 |
      2 | {% for a,b in c | slice(3, c.length) if b is even -%} 3 |
    • {{ a }} - {{ b }}
    • 4 | {%- else %} 5 |
    • No results found
    • 6 | {%- endfor %} 7 |
    8 | 9 |
      10 | {% for key,value in c[:c.length - 1] if value is defined and not value is even %} 11 |
    • {{ key }} - {{ value }}
    • 12 | {% else -%} 13 | {% if regionName is empty %} 14 |
    • No results found
    • 15 | {% endif %} 16 | {% endfor -%} 17 |
    18 | -------------------------------------------------------------------------------- /tests/ControlStructures/forInclude.melody.twig: -------------------------------------------------------------------------------- 1 | {% for foo in range(1, category) %} 2 | 3 | {% include './Star.twig' only %} 4 | 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /tests/ControlStructures/forWithBlock.melody.twig: -------------------------------------------------------------------------------- 1 |

    {{ title | title }}

    2 |
      3 | {% for item in items %} 4 |
    • 5 | {% block title %} 6 | {{ loop.index0 }} {{ item.name | title }} {{ loop.index }} 7 | {% endblock %} 8 |
    • 9 | {% endfor %} 10 |
    11 | -------------------------------------------------------------------------------- /tests/ControlStructures/if.melody.twig: -------------------------------------------------------------------------------- 1 |
    2 | {%- if foo %} 3 |
    4 | {% else -%} 5 |
    6 | {%- endif %} 7 |
    8 | 9 | {% if partner -%} 10 | {{ partner.name }} 14 | {%- elseif partnerName %} 15 | {{ partnerName }} 16 | {% elseif partnerImg -%} 17 | {{ partnerImg }} 18 | {%- endif -%} 19 | 20 | 21 | {% if isLeftToRight %}Hund{% endif %} 22 | {% if isRTL %}{{ '‎' | raw }}{% endif %} 23 | 24 | 25 | {% if unitAfter | length > 0 and not withoutDisplayPattern %}{{ unitAfter }}{% endif %} 26 | 27 | {%- if isCTestActive('WEB-50808') %} web50808{% endif -%} 28 | -------------------------------------------------------------------------------- /tests/ControlStructures/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/Declaration/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`doctype.melody.twig 1`] = ` 4 | 5 | 6 | 7 | 8 | 9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | 12 | 13 | 14 | 15 | 16 | `; 17 | -------------------------------------------------------------------------------- /tests/Declaration/doctype.melody.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/Declaration/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/Element/attributes.melody.twig: -------------------------------------------------------------------------------- 1 | Link 2 | 3 | Test 9 | 10 | 11 |
    abcd
    12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/Element/breakingSiblings.melody.twig: -------------------------------------------------------------------------------- 1 | OneTwoThreeFourFiveSixSeven 2 | 3 | One 4 | Two 5 | Three 6 | Four 7 | Five 8 | Six 9 | Seven 10 | -------------------------------------------------------------------------------- /tests/Element/children.melody.twig: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 | {{ 'checking_deals' }} 6 |
    7 | -------------------------------------------------------------------------------- /tests/Element/emptyLines.melody.twig: -------------------------------------------------------------------------------- 1 |
    2 | DNS explained 3 | 4 | 5 | DNS explained 6 |
    7 | 8 |
    9 | DNS explained 10 | 11 | DNS explained 12 |
    13 | -------------------------------------------------------------------------------- /tests/Element/extraSpaces.melody.twig: -------------------------------------------------------------------------------- 1 | Text 2 | -------------------------------------------------------------------------------- /tests/Element/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/Element/manyAttributes.melody.twig: -------------------------------------------------------------------------------- 1 | Text 2 | -------------------------------------------------------------------------------- /tests/Element/oneLine.melody.twig: -------------------------------------------------------------------------------- 1 | 2 | Next 3 | 4 | -------------------------------------------------------------------------------- /tests/Element/selfClosing.melody.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/Element/siblings.melody.twig: -------------------------------------------------------------------------------- 1 | OneTwoThree 2 | -------------------------------------------------------------------------------- /tests/Element/whitespace.melody.twig: -------------------------------------------------------------------------------- 1 | {{ price }} {{ currencySymbol }} 2 | 3 | Price: {{ price }} {{ currencySymbol }} per night 4 | 5 | This accommodation is {{ price }} {{ currencySymbol }} per night 6 | 7 |
    8 | Gallia est omnis divisa in {{ "partes tres" }}, quarum unam incolunt Belgae, aliam Aquitani, tertiam, qui ipsorum lingua Celtae, nostra Galli appellantur. 9 | 10 | Gallien in seiner Gesamtheit zerfällt in drei Teile. Den einen bewohnen die Belger, einen anderen die Aquitaner und den dritten die, die sich selbst Kelten nennen, in unserer Sprache aber Gallier heißen. 11 | 12 | All Gaul is divided into three 13 | parts, one of which the Belgae inhabit, 14 | the Aquitani another, those who in their 15 | own language are called Celts, 16 | in our Gauls, the third. 17 |
    18 | -------------------------------------------------------------------------------- /tests/Expressions/arrayExpression.melody.twig: -------------------------------------------------------------------------------- 1 | {{ [2, 3, "cat"] }} 2 | 3 | {{ [ 2, 3, "cat","dog", "mouse"] }} 4 | 5 | {{ [ 2, 3, "cat","dog", "mouse", "cake", "elephant", "zebra", 3.1415, translate('translation_key')] }} 6 | 7 | {{ numbers[:1] }} 8 | 9 | {{ numbers[1:4] }} 10 | 11 | {{ numbers[1:endIndex] }} 12 | 13 | {{ numbers[1:] }} 14 | -------------------------------------------------------------------------------- /tests/Expressions/binaryExpressions.melody.twig: -------------------------------------------------------------------------------- 1 | {% set highlightValueForMoney = isFeatureEnabled('vFMV5') or isCTestActive('WEB-48935') or isCTestActive('WEB-48956') or isCTestActive('WEB-48955')%} 2 | 3 | {% set name = condition1 or condition2 and condition3 or condition4 or condition5 and condition6 %} 4 | 5 | {% set name = condition1 and condition2 or condition3 and condition4 and condition5 or condition6 %} 6 | 7 | 8 | {% set replacement = { 9 | '$address': '' ~ address_attributes.address ~ '', 10 | } %} 11 | 12 | {% set renderLoadingBar = showNewLoadingAnimation and isCTestActive('WEB-47697') and showLoadingBar and (isABCD or isLoading) %} 13 | 14 | {% set result = (conditionAlpha or conditionBeta) 15 | and (conditionGamma or conditionDelta) %} 16 | 17 | -------------------------------------------------------------------------------- /tests/Expressions/callExpression.melody.twig: -------------------------------------------------------------------------------- 1 | {{ range(3) }} 2 | 3 | {{ date('d/m/Y H:i', timezone="Europe/Paris") }} 4 | 5 | 6 |
    7 |
    8 | {{ 9 | helpers.partner(cheapestPrice.group.groupId, cheapestPrice.name.value) 10 | }} 11 |
    12 |
    13 |
    14 | 15 | {{ date({ 16 | index: 5, 17 | isOverview: true, 18 | isLongPropertyName: true, 19 | hasBeenWaiting: true 20 | }) }} 21 | 22 | {{ craft.someCoolObject.someMethodToUse({ foo: 'bar', bar: 'baz', baz: 'foo' }).all() }} 23 | -------------------------------------------------------------------------------- /tests/Expressions/conditionalExpression.melody.twig: -------------------------------------------------------------------------------- 1 | {{ test ? "One" : "Two" }} 2 | 3 | {{ test ? "This is a slightly longer string to overflow the line" : "and here is its counterpart" }} 4 | 5 | {{- ratingValue == 10 ? ratingValue : ratingValue -}} 6 | -------------------------------------------------------------------------------- /tests/Expressions/filterExpression.melody.twig: -------------------------------------------------------------------------------- 1 | {{ 'test.foo' | split('.') }} 2 | {{ range(3) | sort | join(',') }} 3 | {{ 'SHOUTING' | lower|escape('html') | upper | escape('markdown') | lower | upper | escape('markdown') }} 4 | 5 | {% include './usefulDeal.melody.twig' with deal | merge( 6 | { 7 | 'index': loop.index0, 8 | 'isOverview': isOverview, 9 | 'isRTL': isRTL, 10 | 'useWiderItems': useWiderItems 11 | } 12 | ) only %} 13 | 14 | {{ (hasAdvertiserRatings 15 | ? 'tri_based_on' | translate({ 'iBasedOn': '' ~ reviewCount ~ '' }) 16 | : 'tri_based_on_no_partners' | translate({ 'iBasedOn': '' ~ reviewCount ~ '' }) 17 | ) | raw 18 | }} 19 | -------------------------------------------------------------------------------- /tests/Expressions/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/Expressions/memberExpression.melody.twig: -------------------------------------------------------------------------------- 1 | {{ alternativeMarriottRewardRates[deal.dealId].short }} 2 | -------------------------------------------------------------------------------- /tests/Expressions/objectExpression.melody.twig: -------------------------------------------------------------------------------- 1 | {{ { 2 | a: "foo", 3 | "b#{ar}": "bar", 4 | 2: 4, 5 | (a): foo, 6 | } }} 7 | 8 |

    Heading

    11 | -------------------------------------------------------------------------------- /tests/Expressions/operators.melody.twig: -------------------------------------------------------------------------------- 1 | {{ a b-and b }} 2 | {{ a b-or b }} 3 | {{ a b-xor b }} 4 | {{ a or b }} 5 | {{ a and b }} 6 | {{ a == b }} 7 | {{ a != b }} 8 | {{ a < b }} 9 | {{ a > b }} 10 | {{ a >= b }} 11 | {{ a <= b }} 12 | {{ a in b }} 13 | {{ a not in b }} 14 | {{ a matches b }} 15 | {{ a matches '^foo' }} 16 | {{ a starts with b }} 17 | {{ a ends with b }} 18 | {{ a..b }} 19 | {{ a+b }} 20 | {{ a-b }} 21 | {{ a~b }} 22 | {{ a*b }} 23 | {{ a/b }} 24 | {{ a%b }} 25 | {{ a ** b }} 26 | {{ a ? b }} 27 | {{ a ?: b }} 28 | {{ a ?? b }} 29 | 30 | {{ a is divisible by(b) }} 31 | {{ a is not divisible by(b) }} 32 | {{ a is defined }} 33 | {{ a is not defined }} 34 | {{ isEmpty is empty }} 35 | {{ a is not empty }} 36 | {{ a is even }} 37 | {{ a is not even }} 38 | {{ a is iterable }} 39 | {{ a is not iterable }} 40 | {{ a is null }} 41 | {{ a is not null }} 42 | {{ a is odd }} 43 | {{ a is not odd }} 44 | {{ a is same as(b) }} 45 | {{ a is not same as(b) }} 46 | {{ a is not same as(banana, apple, orange, lemonade, kiwi, coconut, pineapple, pomegrenade) }} 47 | 48 | {{ dump(test) }} 49 | {{ range(2, 3) | sort | join(',') }} 50 | {{ range(3) | sort | join(',') }} 51 | {{ range(2, 3, 2) | sort | join(',') }} 52 | {{ test | raw }} 53 | {{ 2.4 | abs }} 54 | {{ { a: 'b' } | json_encode | trim }} 55 | {{ [2, 3] | length }} 56 | {{ 'test.foo' | split('.') }} 57 | -------------------------------------------------------------------------------- /tests/Expressions/stringConcat.melody.twig: -------------------------------------------------------------------------------- 1 |
    2 | {{ first ~ second }} 3 |
    4 | 5 | Test 6 | 7 | {% set calendarIcon = isNewGuestSelector ? 'icn_#{type | lower}_line_dark' : type | lower %} 8 | 9 | {% icon 'name' with { classList: 'classA' ~ (not needsB ? ' classB') } %} 10 | -------------------------------------------------------------------------------- /tests/Expressions/stringLiteral.melody.twig: -------------------------------------------------------------------------------- 1 | {{ 'zzz\\bar\\baz' }} 2 | 3 | {{ "College - Women's" }} 4 | 5 | {{ "test ' with \\"both\\" kinds" }} 6 | 7 | {{ 'test \\' with "both" kinds' }} 8 | 9 | {{ "Quoted \\'' unquoted" }} 10 | -------------------------------------------------------------------------------- /tests/Expressions/unaryNot.melody.twig: -------------------------------------------------------------------------------- 1 | {% if not invalid %} 2 |

    All's well.

    3 | {% endif %} 4 | -------------------------------------------------------------------------------- /tests/Failing/controversial.melody.twig: -------------------------------------------------------------------------------- 1 | {% set isRewardRate = isMarriottRewardRate or (rewardRateAltIds and deal.dealId in rewardRateAltIds[accommodation.id.id]) %} 2 | 3 | 4 | {% set altIds = rewardRateAltIds[accommodation.id.id] %} 5 | {% set isRewardRate = isMarriottRewardRate or (rewardRateAltIds and deal.dealId in altIds) %} 6 | 7 | 8 |
    11 |
    12 | 13 | 16 |
    17 | {% mount '@hotelsearch/accommodation-list/src/Slideout/index' 18 | as 'accommodation-slideout-' ~ accommodation.id.id with { 19 | key: 'accommodation-slideout-' ~ accommodation.id.id, 20 | itemId: accommodation.id.id, 21 | item: accommodation, 22 | isSearchedItem: isSearchedItem, 23 | bestPrice: accommodation.deals.bestPrice, 24 | insights: hasInsights ? insightsData.accommodation.id.id, 25 | clickedAltDealPartnerId: clickedAltDealPartnerId, 26 | entirePlace: isEntirePlace ? entirePlaceData.accommodation.id.id 27 | } %} 28 |
    29 | 30 | 31 | {% include './partials/arrowBtn.melody.twig' with { 32 | ref: prev | default(), 33 | } only %} 34 | 35 | 36 |

    37 | {{ 'results_for' | translate({ 'searchedterm': '' ~ semKeyword ~ ''}) | raw }} 38 |

    39 | -------------------------------------------------------------------------------- /tests/Failing/failing.melody.twig: -------------------------------------------------------------------------------- 1 | {# IF tag in element not allowed 2 | 10 | #} 11 | 12 | {% icon 'general/arrow-36x36' with { 13 | classList: { 14 | base: "#{css['arrowIcon#{action}']} icon-rtl", 15 | 'icon-flip': flipIcon ?? false 16 | } | classes 17 | } 18 | %} 19 | 20 | {# "only" dropped, comment dropped #} 21 | {% embed '@hotelsearch/common/tooltip/tooltip.melody.twig' with { 22 | position: 'bottomTrailing', 23 | id: 'most-popular-badge', 24 | classList: tooltipOpen ? 'show-tooltip', 25 | isAriaHidden: not tooltipOpen ?? false 26 | } only %} 27 | {# Necessary block to inject html into the tooltip #} 28 | {% block text %} 29 |

    Hey>

    30 | {% endblock %} 31 | {% endembed %} 32 | 33 | {# Inserts a newline where it shouldn't #} 34 | 35 | 36 | 37 | 38 | 39 | 40 | {# Parentheses dropped. Might be valid, but could be better, cosmetically #} 41 | {% for feature in (showAAAmenities ? entirePlaceFeatures : topFeatures) %} 42 | abcd 43 | {% endfor %} 44 | 45 | {# There should be no whitespace changes in textarea #} 46 | 54 | 55 | {# Before the closing -->, whitespace keeps being added #} 56 | 59 | -------------------------------------------------------------------------------- /tests/Failing/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"], { 2 | twigPrintWidth: 120, 3 | twigAlwaysBreakObjects: true, 4 | twigFollowOfficialCodingStandards: false 5 | }); 6 | -------------------------------------------------------------------------------- /tests/GenericTagCustomPrint/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`switch.melody.twig 1`] = ` 4 | {% switch matrixBlock.type %} 5 | {% case "text" %} 6 | 7 | {{ matrixBlock.textField | markdown }} 8 | 9 | {% case "image" %} 10 | 11 | {{ matrixBlock.image[0].getImg() }} 12 | 13 | {% default %} 14 | 15 |

    A font walks into a bar.

    16 |

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

    17 | 18 | {% endswitch %} 19 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 20 | {% switch matrixBlock.type %} 21 | {% case 'text' %} 22 | {{ matrixBlock.textField|markdown }} 23 | {% case 'image' %} 24 | {{ matrixBlock.image[0].getImg() }} 25 | {% default %} 26 |

    27 | A font walks into a bar. 28 |

    29 |

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

    32 | {% endswitch %} 33 | 34 | `; 35 | -------------------------------------------------------------------------------- /tests/GenericTagCustomPrint/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"], { 2 | twigMultiTags: ["switch,case,default,endswitch"], 3 | twigMelodyPlugins: ["tests/switch-plugin"] 4 | }); 5 | -------------------------------------------------------------------------------- /tests/GenericTagCustomPrint/switch.melody.twig: -------------------------------------------------------------------------------- 1 | {% switch matrixBlock.type %} 2 | {% case "text" %} 3 | 4 | {{ matrixBlock.textField | markdown }} 5 | 6 | {% case "image" %} 7 | 8 | {{ matrixBlock.image[0].getImg() }} 9 | 10 | {% default %} 11 | 12 |

    A font walks into a bar.

    13 |

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

    14 | 15 | {% endswitch %} 16 | -------------------------------------------------------------------------------- /tests/GenericTags/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`cache.melody.twig 1`] = ` 4 | {% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} 5 | {% for block in entry.myMatrixField %} 6 |

    {{ block.text }}

    7 | {% endfor %} 8 | {% endcache %} 9 | 10 | {# prettier-ignore #} 11 | {% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} 12 | {% for block in entry.myMatrixField %} 13 |

    {{ block.text }}

    14 | {% endfor %} 15 | {% endcache %} 16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 17 | {% cache globally 18 | using 19 | key 20 | craft.some.rather.long.property.chain.request.path 21 | for 22 | 3 23 | weeks 24 | %} 25 | {% for block in entry.myMatrixField %} 26 |

    27 | {{ block.text }} 28 |

    29 | {% endfor %} 30 | {% endcache %} 31 | 32 | {# prettier-ignore #} 33 | {% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} 34 | {% for block in entry.myMatrixField %} 35 |

    {{ block.text }}

    36 | {% endfor %} 37 | {% endcache %} 38 | 39 | `; 40 | 41 | exports[`header.melody.twig 1`] = ` 42 | {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} 43 | 44 | {# prettier-ignore #} 45 | {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} 46 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 47 | {% header 'Cache-Control: max-age=' ~ (expiry.timestamp - now.timestamp) %} 48 | 49 | {# prettier-ignore #} 50 | {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} 51 | 52 | `; 53 | 54 | exports[`includeCssFile.melody.twig 1`] = ` 55 | {% includeCssFile "/assets/css/layouts/" ~ entry.layout ~ ".css" %} 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | {% includeCssFile '/assets/css/layouts/' ~ entry.layout ~ '.css' %} 58 | 59 | `; 60 | 61 | exports[`nav.melody.twig 1`] = ` 62 | {% nav entry in entries %} 63 |
  • 64 | {{ entry.title }} 65 | {% ifchildren %} 66 |
      67 | {% children %} 68 |
    69 | {% endifchildren %} 70 |
  • 71 | {% endnav %} 72 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 73 | {% nav entry in entries %} 74 |
  • 75 | {{ entry.title }} 76 | {% ifchildren %} 77 |
      78 | {% children %} 79 |
    80 | {% endifchildren %} 81 |
  • 82 | {% endnav %} 83 | 84 | `; 85 | 86 | exports[`paginate.melody.twig 1`] = ` 87 | {% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries %} 88 | 89 | {% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries, pageProperties %} 90 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 91 | {% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries %} 92 | 93 | {% paginate craft.entries.section('blog').limit(10) 94 | as 95 | pageInfo, 96 | pageEntries, 97 | pageProperties 98 | %} 99 | 100 | `; 101 | 102 | exports[`redirect.melody.twig 1`] = ` 103 | {% redirect "pricing" 301 %} 104 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 105 | {% redirect 'pricing' 301 %} 106 | 107 | `; 108 | 109 | exports[`switch.melody.twig 1`] = ` 110 | {% switch matrixBlock.type %} 111 | 112 | 113 | 114 | {% case "text" %} 115 | 116 | {{ matrixBlock.textField | markdown }} 117 | 118 | {% case "image" %} 119 | 120 | {{ matrixBlock.image[0].getImg() }} 121 | 122 | {% default %} 123 | 124 |

    A font walks into a bar.

    125 |

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

    126 | 127 | {% endswitch %} 128 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 129 | {% switch matrixBlock.type %} 130 | {% case 'text' %} 131 | {{ matrixBlock.textField|markdown }} 132 | {% case 'image' %} 133 | {{ matrixBlock.image[0].getImg() }} 134 | {% default %} 135 |

    136 | A font walks into a bar. 137 |

    138 |

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

    141 | {% endswitch %} 142 | 143 | `; 144 | -------------------------------------------------------------------------------- /tests/GenericTags/cache.melody.twig: -------------------------------------------------------------------------------- 1 | {% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} 2 | {% for block in entry.myMatrixField %} 3 |

    {{ block.text }}

    4 | {% endfor %} 5 | {% endcache %} 6 | 7 | {# prettier-ignore #} 8 | {% cache globally using key craft.some.rather.long.property.chain.request.path for 3 weeks %} 9 | {% for block in entry.myMatrixField %} 10 |

    {{ block.text }}

    11 | {% endfor %} 12 | {% endcache %} 13 | -------------------------------------------------------------------------------- /tests/GenericTags/header.melody.twig: -------------------------------------------------------------------------------- 1 | {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} 2 | 3 | {# prettier-ignore #} 4 | {% header "Cache-Control: max-age=" ~ (expiry.timestamp - now.timestamp) %} 5 | -------------------------------------------------------------------------------- /tests/GenericTags/includeCssFile.melody.twig: -------------------------------------------------------------------------------- 1 | {% includeCssFile "/assets/css/layouts/" ~ entry.layout ~ ".css" %} 2 | -------------------------------------------------------------------------------- /tests/GenericTags/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"], { 2 | twigMultiTags: [ 3 | "nav,endnav", 4 | "switch,case,default,endswitch", 5 | "ifchildren,endifchildren", 6 | "cache,endcache" 7 | ] 8 | }); 9 | -------------------------------------------------------------------------------- /tests/GenericTags/nav.melody.twig: -------------------------------------------------------------------------------- 1 | {% nav entry in entries %} 2 |
  • 3 | {{ entry.title }} 4 | {% ifchildren %} 5 |
      6 | {% children %} 7 |
    8 | {% endifchildren %} 9 |
  • 10 | {% endnav %} 11 | -------------------------------------------------------------------------------- /tests/GenericTags/paginate.melody.twig: -------------------------------------------------------------------------------- 1 | {% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries %} 2 | 3 | {% paginate craft.entries.section('blog').limit(10) as pageInfo, pageEntries, pageProperties %} 4 | -------------------------------------------------------------------------------- /tests/GenericTags/redirect.melody.twig: -------------------------------------------------------------------------------- 1 | {% redirect "pricing" 301 %} 2 | -------------------------------------------------------------------------------- /tests/GenericTags/switch.melody.twig: -------------------------------------------------------------------------------- 1 | {% switch matrixBlock.type %} 2 | 3 | 4 | 5 | {% case "text" %} 6 | 7 | {{ matrixBlock.textField | markdown }} 8 | 9 | {% case "image" %} 10 | 11 | {{ matrixBlock.image[0].getImg() }} 12 | 13 | {% default %} 14 | 15 |

    A font walks into a bar.

    16 |

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

    17 | 18 | {% endswitch %} 19 | -------------------------------------------------------------------------------- /tests/IncludeEmbed/block.melody.twig: -------------------------------------------------------------------------------- 1 |
    2 | {% block hello %} 3 |
    4 | Hello 5 |
    6 | {% endblock %} 7 |
    8 | 9 | {% block bar foo %} 10 | 11 | {{ block('hello') }} 12 | 13 | {% block content %}{% endblock %} 14 | 15 | {%- block hello -%} 16 | Hello 17 | {%- endblock -%} 18 | 19 | {%- block bar foo -%} 20 | -------------------------------------------------------------------------------- /tests/IncludeEmbed/embed.melody.twig: -------------------------------------------------------------------------------- 1 | {% extends "parent.twig" %} 2 | 3 | {% block hello %} 4 |
    5 | {%- embed "foo.twig" with { foo: 'bar' } %} 6 | {% block hello %} 7 | {{ fun }} 8 | {% embed "bar.twig" -%} 9 | {% block hello %} 10 | {{ message }} 11 | {% endblock %} 12 | {% block test %} 13 | {% endblock %} 14 | {%- endembed %} 15 | {% endblock hello %} 16 | {% endembed -%} 17 |
    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /tests/IncludeEmbed/extendsEmbed.melody.twig: -------------------------------------------------------------------------------- 1 | {%- extends "parent.twig" %} 2 | {% extends someVar -%} 3 | -------------------------------------------------------------------------------- /tests/IncludeEmbed/import.melody.twig: -------------------------------------------------------------------------------- 1 | {%- import "forms.html" as forms %} 2 | {% import "aVeryLongAndConvolutedAndIntertwinedFilename.html" as someQuiteEccentricLocalVariableName -%} 3 | 4 | {% from "macros.twig" import hello %} 5 | {%- from 'forms.html' import input as input_field,textarea -%} 6 | {% from 'aVeryLongAndConvolutedAndIntertwinedFilename.html' import input as input_field, textarea, select, password as pw_field, radioButton %} 7 | -------------------------------------------------------------------------------- /tests/IncludeEmbed/include.melody.twig: -------------------------------------------------------------------------------- 1 |
    2 | {{ message | lower | upper }}{% flush %} 3 | {{ _context.name[1:] }} 4 | {{ block('test') }} 5 | {{ include('test.twig') }} 6 | {% include 'test.twig' %} 7 |
    8 | 9 | {% include './Star.twig' only %} 10 | 11 | {%- include "./Flag.twig" with { 12 | "styleModifier": flagModifiers, 13 | "dataVariables": dataVariables, 14 | "text": "ie_topdeal" 15 | } only %} 16 | 17 |
    18 | {% include "./Flag.twig" with { 19 | "styleModifier": flagModifiers, 20 | "dataVariables": dataVariables, 21 | "text": "ie_topdeal" 22 | } only -%} 23 |
    24 | 25 | {% include 'foo/' ~ BRT %} 26 | {% include "#{filename}" %} 27 | -------------------------------------------------------------------------------- /tests/IncludeEmbed/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"], { twigFollowOfficialCodingStandards: false }); 2 | -------------------------------------------------------------------------------- /tests/IncludeEmbed/mount.melody.twig: -------------------------------------------------------------------------------- 1 | {%- mount './component' as 'bar' -%} 2 | 3 | {% mount async './parts/#{ part }.twig' as 'bar-#{part}' with {foo: 'bar'} delay placeholder by 1s -%} 4 | Loading... 5 | {%- catch err -%} 6 | Failed to load with {{ err }} 7 | {%- endmount -%} 8 | 9 | 21 | 22 |
  • 23 |
    24 | {% mount "@hotelsearch/accommodation-list/src/Slideout/index" 25 | as "accommodation-slideout-" ~ accommodation.id.id with { 26 | "key": "accommodation-slideout-" ~ accommodation.id.id, 27 | "itemId": accommodation.id.id, 28 | "item": accommodation, 29 | "isSearchedItem": isSearchedItem, 30 | "bestPrice": accommodation.deals.bestPrice, 31 | "insights": hasInsights ? insightsData.accommodation.id.id, 32 | "clickedAltDealPartnerId": clickedAltDealPartnerId, 33 | "entirePlace": isEntirePlace 34 | ? entirePlaceData.accommodation.id.id 35 | } 36 | %} 37 |
    38 |
  • 39 | 40 |
    41 | {% mount ContentComponent as 'popover-component' ~ activeView with _context %} 42 |
    43 | 44 |
    45 | {% mount Tabs from '@trivago/components' as seotabs with { 46 | tabs: tabs, 47 | classList: 'tabs--homepage', 48 | } %} 49 |
    50 | -------------------------------------------------------------------------------- /tests/IncludeEmbed/useStatement.melody.twig: -------------------------------------------------------------------------------- 1 | {% use "foo.twig" %} 2 | 3 | {%- use "blocks.html" with sidebar as base_sidebar, title as base_title %} 4 | 5 | {% use "extraLongNameBlocks.html" with sidebar as base_sidebar, title as base_title -%} 6 | -------------------------------------------------------------------------------- /tests/Options/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`alwaysBreakObjects.melody.twig 1`] = ` 4 |
    7 |
    8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 |
    10 | 11 | `; 12 | 13 | exports[`endblockName.melody.twig 1`] = ` 14 | {% block title %} 15 | {{ item.name|title }} 16 | {% endblock %} 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | {% block title %} 19 | {{ item.name|title }} 20 | {% endblock title %} 21 | 22 | `; 23 | 24 | exports[`printWidth.melody.twig 1`] = ` 25 | Text 26 | 27 | Text 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | Text 30 | 31 | 32 | Text 33 | 34 | 35 | `; 36 | -------------------------------------------------------------------------------- /tests/Options/alwaysBreakObjects.melody.twig: -------------------------------------------------------------------------------- 1 |
    4 |
    5 | -------------------------------------------------------------------------------- /tests/Options/endblockName.melody.twig: -------------------------------------------------------------------------------- 1 | {% block title %} 2 | {{ item.name|title }} 3 | {% endblock %} 4 | -------------------------------------------------------------------------------- /tests/Options/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"], { 2 | twigAlwaysBreakObjects: false, 3 | twigPrintWidth: 120, 4 | twigOutputEndblockName: true 5 | }); 6 | -------------------------------------------------------------------------------- /tests/Options/printWidth.melody.twig: -------------------------------------------------------------------------------- 1 | Text 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /tests/PrettierIgnore/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`prettierIgnore.melody.twig 1`] = ` 4 | 5 |
    Should not re-format
    6 |
    Should re-format
    7 | 8 | {#prettier-ignore#} 9 |
    Should not re-format
    10 |
    Should re-format
    11 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12 | 13 |
    Should not re-format
    14 |
    15 | Should re-format 16 |
    17 | 18 | {# prettier-ignore #} 19 |
    Should not re-format
    20 |
    21 | Should re-format 22 |
    23 | 24 | `; 25 | 26 | exports[`prettierIgnoreStartEnd.melody.twig 1`] = ` 27 | 28 |
    Should not re-format
    29 |
    Should not re-format
    30 |
    Should not re-format
    31 | 32 | 33 |
    Should re-format
    34 | 35 | {# prettier-ignore-start #} 36 |
    Should not re-format
    37 |
    Should not re-format
    38 |
    Should not re-format
    39 | {# prettier-ignore-end #} 40 | 41 |
    Should re-format
    42 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 |
    Should not re-format
    45 |
    Should not re-format
    46 |
    Should not re-format
    47 | 48 | 49 |
    50 | Should re-format 51 |
    52 | 53 | {# prettier-ignore-start #} 54 |
    Should not re-format
    55 |
    Should not re-format
    56 |
    Should not re-format
    57 | {# prettier-ignore-end #} 58 | 59 |
    60 | Should re-format 61 |
    62 | 63 | `; 64 | -------------------------------------------------------------------------------- /tests/PrettierIgnore/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/PrettierIgnore/prettierIgnore.melody.twig: -------------------------------------------------------------------------------- 1 | 2 |
    Should not re-format
    3 |
    Should re-format
    4 | 5 | {#prettier-ignore#} 6 |
    Should not re-format
    7 |
    Should re-format
    8 | -------------------------------------------------------------------------------- /tests/PrettierIgnore/prettierIgnoreStartEnd.melody.twig: -------------------------------------------------------------------------------- 1 | 2 |
    Should not re-format
    3 |
    Should not re-format
    4 |
    Should not re-format
    5 | 6 | 7 |
    Should re-format
    8 | 9 | {# prettier-ignore-start #} 10 |
    Should not re-format
    11 |
    Should not re-format
    12 |
    Should not re-format
    13 | {# prettier-ignore-end #} 14 | 15 |
    Should re-format
    16 | -------------------------------------------------------------------------------- /tests/Statements/autoescape.melody.twig: -------------------------------------------------------------------------------- 1 | {% autoescape 'html' %} 2 | 3 | 4 | 5 | Yes 6 | 7 | {% endautoescape %} 8 | 9 | {%- autoescape 'html' -%} 10 | 11 | {%- endautoescape -%} 12 | -------------------------------------------------------------------------------- /tests/Statements/do.melody.twig: -------------------------------------------------------------------------------- 1 | {% do 1 + 2 %} 2 | 3 | {%- do 1 + 2 -%} 4 | -------------------------------------------------------------------------------- /tests/Statements/filter.melody.twig: -------------------------------------------------------------------------------- 1 | {% filter upper %} 2 | This text becomes uppercase 3 | {% endfilter %} 4 | 5 | {% filter upper -%} 6 | This text becomes uppercase 7 | {%- endfilter %} 8 | 9 | {%- filter lower|escape('html') | upper | escape('markdown') | lower | upper | escape('markdown') %} 10 | SOME TEXT 11 | 12 |

    The cat is taking a nap in the sunshine.

    13 | 14 | 15 | {% endfilter -%} 16 | -------------------------------------------------------------------------------- /tests/Statements/flush.melody.twig: -------------------------------------------------------------------------------- 1 | {%- flush %} 2 | {% flush -%} 3 | -------------------------------------------------------------------------------- /tests/Statements/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/Statements/macro.melody.twig: -------------------------------------------------------------------------------- 1 | {% macro input(name, value, type, size, shape, colour, taste, flash, broom, lawn, cloud, sky, hedgehog) %} 2 | 3 | {% endmacro %} 4 | 5 | {%- macro wrapped_input(name, value, type, size) %} 6 | {% import _self as forms %} 7 | 8 |
    9 | {{ forms.input(name, value, type, size) }} 10 |
    11 | {% endmacro -%} 12 | 13 | {% macro whitespaceRemoval(name, value, type, size) -%} 14 | 15 |
    16 | {{ forms.input(name, value, type, size) }} 17 |
    18 | 19 | 20 | {%- endmacro %} 21 | 22 | {% macro partner(groupId, value) %} 23 | {# 80 is the groupid of specific hotel websites that may not have a correct partner name #} 24 | {{ groupId == 80 ? 'book_hotel_website_test' | translate : value }} 25 | {% endmacro %} 26 | 27 | -------------------------------------------------------------------------------- /tests/Statements/set.melody.twig: -------------------------------------------------------------------------------- 1 | {% set list = [1, 2] %} 2 | {%- set foo = 0 -%} 3 | {% set foo = 'foo' ~ 'bar' %} 4 | {% set foo = {'fruit': 'apple', 'shape': 'round', 'taste': 'sweet', 'region': 'Europe' } %} 5 | {% set foo = {'fruit': 'apple', 'shape': 'round', 'taste': 'sweet', 'region': 'Europe', 'colour': 'reddish' } %} 6 | {% set foo, bar = 'foo', 'bar' %} 7 | {%- set foo -%} 8 | 9 | 13 | 14 | 15 |

    Some more text

    16 | {%- endset -%} 17 | 18 | {% set showArrows = hideArrowWhenDisabled | default(false) 19 | ? shouldShowArrows | default(false) and scrollEnabled | default(false) 20 | : shouldShowArrows | default(false) 21 | %} 22 | 23 | {% set recommendedClickoutAttributes = hasRecommendedPrice ? clickoutAttributes | merge({ 24 | 'data-id': recommendedPrice.dealId, 25 | 'data-co_params': recommendedPrice.clcklB | json_encode(), 26 | 'data-co_li_lo': 1 27 | }) : {} %} 28 | 29 | {% set showAAScoreRating = isAAScoreActive and isAAAccommodation and aaScoreRatingData and aaScoreRatingData.score > 0 %} 30 | 31 | {% set displayLegalPaymentInfo = not (isFrance and isAtLeastScreenTabletWide) %} 32 | 33 | {% set displayLegalPaymentInfo = not (isAAScoreActive and isAAAccommodation and aaScoreRatingData and aaScoreRatingDataABC > 0 and aaScoreRatingDataABC < 5) %} 34 | 35 | {% set flavours = ['banana', 'strawberry', 'pineapple', 'lemon', 'raspberry', 'vanilla'] %} 36 | -------------------------------------------------------------------------------- /tests/Statements/spaceless.melody.twig: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 |
    3 | Receive {{ formattedIncentive }} cash back for testing this hotel. 4 | Or just be happy! 5 |
    6 | {% endspaceless %} 7 | 8 | {%- spaceless -%} 9 | The quick brown fox 10 | {%- endspaceless -%} 11 | -------------------------------------------------------------------------------- /tests/TwigCodingStandards/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`twigCodingStandards.melody.twig 1`] = ` 4 | {{ foo }} 5 | {# comment #} 6 | {% if foo %}{% endif %} 7 | 8 | {{- foo -}} 9 | {#- comment -#} 10 | {%- if foo -%}{%- endif -%} 11 | 12 | {{ 1 + 2 }} 13 | {{ foo ~ bar }} 14 | {{ true ? true : false }} 15 | 16 | {{ [1, 2, 3] }} 17 | {{ {'foo': 'bar'} }} 18 | 19 | {{ 1 + (2 * 3) }} 20 | 21 | {{ foo|upper|lower }} 22 | {{ user.name }} 23 | {{ user[name] }} 24 | {% for i in 1..12 %}{% endfor %} 25 | 26 | {{ foo|default('foo') }} 27 | {{ range(1..10) }} 28 | 29 | {% block foo %} 30 | {% if true %} 31 | true 32 | {% endif %} 33 | {% endblock %} 34 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | {{ foo }} 36 | {# comment #} 37 | {% if foo %}{% endif %} 38 | 39 | {{- foo -}} 40 | {#- comment -#} 41 | {%- if foo -%}{%- endif -%} 42 | 43 | {{ 1 + 2 }} 44 | {{ foo ~ bar }} 45 | {{ true ? true : false }} 46 | 47 | {{ [1, 2, 3] }} 48 | {{ { foo: 'bar' } }} 49 | 50 | {{ 1 + 2 * 3 }} 51 | 52 | {{ foo|upper|lower }} 53 | {{ user.name }} 54 | {{ user[name] }} 55 | {% for i in 1..12 %}{% endfor %} 56 | 57 | {{ foo|default('foo') }} 58 | {{ range(1..10) }} 59 | 60 | {% block foo %} 61 | {% if true %} 62 | true 63 | {% endif %} 64 | {% endblock %} 65 | 66 | `; 67 | -------------------------------------------------------------------------------- /tests/TwigCodingStandards/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"], { twigAlwaysBreakObjects: false }); 2 | -------------------------------------------------------------------------------- /tests/TwigCodingStandards/twigCodingStandards.melody.twig: -------------------------------------------------------------------------------- 1 | {{ foo }} 2 | {# comment #} 3 | {% if foo %}{% endif %} 4 | 5 | {{- foo -}} 6 | {#- comment -#} 7 | {%- if foo -%}{%- endif -%} 8 | 9 | {{ 1 + 2 }} 10 | {{ foo ~ bar }} 11 | {{ true ? true : false }} 12 | 13 | {{ [1, 2, 3] }} 14 | {{ {'foo': 'bar'} }} 15 | 16 | {{ 1 + (2 * 3) }} 17 | 18 | {{ foo|upper|lower }} 19 | {{ user.name }} 20 | {{ user[name] }} 21 | {% for i in 1..12 %}{% endfor %} 22 | 23 | {{ foo|default('foo') }} 24 | {{ range(1..10) }} 25 | 26 | {% block foo %} 27 | {% if true %} 28 | true 29 | {% endif %} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /tests/Whitespace/__snapshots__/jsfmt.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`element.melody.twig 1`] = ` 4 |
    5 | 6 |
    7 | 8 |

    9 | This is some text This is some text This is some text This is some text This is some text This is some text This is some text This is some text This is some text 10 |

    11 | 12 |
    13 | 14 | {% include 'pages/formularios/sub/pregunta-simple.html' %} 15 | {% set index = index + 1 %} 16 |
    17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 |
    19 | 20 |
    21 | 22 |

    23 | This is some text This is some text This is some text This is some text This 24 | is some text This is some text This is some text This is some text This is 25 | some text 26 |

    27 | 28 |
    29 | 30 | {% include 'pages/formularios/sub/pregunta-simple.html' %} 31 | {% set index = index + 1 %} 32 |
    33 | 34 | `; 35 | -------------------------------------------------------------------------------- /tests/Whitespace/element.melody.twig: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |

    6 | This is some text This is some text This is some text This is some text This is some text This is some text This is some text This is some text This is some text 7 |

    8 | 9 |
    10 | 11 | {% include 'pages/formularios/sub/pregunta-simple.html' %} 12 | {% set index = index + 1 %} 13 |
    14 | -------------------------------------------------------------------------------- /tests/Whitespace/jsfmt.spec.js: -------------------------------------------------------------------------------- 1 | run_spec(__dirname, ["melody"]); 2 | -------------------------------------------------------------------------------- /tests/switch-plugin/index.js: -------------------------------------------------------------------------------- 1 | const prettier = require("prettier"); 2 | const { concat, indent, hardline } = prettier.doc.builders; 3 | const { 4 | STRING_NEEDS_QUOTES, 5 | printSingleTwigTag, 6 | indentWithHardline, 7 | isEmptySequence 8 | } = require("../../src/util"); 9 | const { Node } = require("melody-types"); 10 | 11 | const printSwitch = (node, path, print) => { 12 | node[STRING_NEEDS_QUOTES] = true; 13 | const openingTag = printSingleTwigTag(node, path, print); 14 | const parts = [openingTag]; 15 | const printedSections = path.map(print, "sections"); 16 | node.sections.forEach((section, i) => { 17 | if (Node.isGenericTwigTag(section)) { 18 | if (section.tagName === "endswitch") { 19 | parts.push(concat([hardline, printedSections[i]])); 20 | } else { 21 | parts.push(indentWithHardline(printedSections[i])); 22 | } 23 | } else { 24 | if (!isEmptySequence(section)) { 25 | // Indent twice 26 | parts.push(indent(indentWithHardline(printedSections[i]))); 27 | } 28 | } 29 | }); 30 | return concat(parts); 31 | }; 32 | 33 | module.exports = { 34 | printers: { 35 | switchTag: printSwitch 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /tests_config/raw-serializer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const RAW = Symbol.for("raw"); 4 | 5 | module.exports = { 6 | print(val) { 7 | return val[RAW]; 8 | }, 9 | test(val) { 10 | return ( 11 | val && 12 | Object.prototype.hasOwnProperty.call(val, RAW) && 13 | typeof val[RAW] === "string" 14 | ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /tests_config/run_spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const extname = require("path").extname; 5 | const prettier = require("prettier"); 6 | 7 | function run_spec(dirname, parsers, options) { 8 | options = Object.assign( 9 | { 10 | plugins: ["."], 11 | tabWidth: 4 12 | }, 13 | options 14 | ); 15 | 16 | /* instabul ignore if */ 17 | if (!parsers || !parsers.length) { 18 | throw new Error(`No parsers were specified for ${dirname}`); 19 | } 20 | 21 | fs.readdirSync(dirname).forEach(filename => { 22 | const path = dirname + "/" + filename; 23 | if ( 24 | extname(filename) !== ".snap" && 25 | fs.lstatSync(path).isFile() && 26 | filename[0] !== "." && 27 | filename !== "jsfmt.spec.js" 28 | ) { 29 | const source = read(path).replace(/\r\n/g, "\n"); 30 | 31 | const mergedOptions = Object.assign({}, options, { 32 | parser: parsers[0] 33 | }); 34 | const output = prettyprint(source, path, mergedOptions); 35 | test(`${filename} - ${mergedOptions.parser}-verify`, () => { 36 | expect( 37 | raw(source + "~".repeat(80) + "\n" + output) 38 | ).toMatchSnapshot(filename); 39 | }); 40 | 41 | parsers.slice(1).forEach(parserName => { 42 | test(`${filename} - ${parserName}-verify`, () => { 43 | const verifyOptions = Object.assign(mergedOptions, { 44 | parser: parserName 45 | }); 46 | const verifyOutput = prettyprint( 47 | source, 48 | path, 49 | verifyOptions 50 | ); 51 | expect(output).toEqual(verifyOutput); 52 | }); 53 | }); 54 | } 55 | }); 56 | } 57 | global.run_spec = run_spec; 58 | 59 | function stripLocation(ast) { 60 | if (Array.isArray(ast)) { 61 | return ast.map(e => stripLocation(e)); 62 | } 63 | if (typeof ast === "object") { 64 | const newObj = {}; 65 | for (const key in ast) { 66 | if ( 67 | key === "loc" || 68 | key === "range" || 69 | key === "raw" || 70 | key === "comments" || 71 | key === "parent" || 72 | key === "prev" 73 | ) { 74 | continue; 75 | } 76 | newObj[key] = stripLocation(ast[key]); 77 | } 78 | return newObj; 79 | } 80 | return ast; 81 | } 82 | 83 | function parse(string, opts) { 84 | return stripLocation(prettier.__debug.parse(string, opts)); 85 | } 86 | 87 | function prettyprint(src, filename, options) { 88 | return prettier.format( 89 | src, 90 | Object.assign( 91 | { 92 | filepath: filename 93 | }, 94 | options 95 | ) 96 | ); 97 | } 98 | 99 | function read(filename) { 100 | return fs.readFileSync(filename, "utf8"); 101 | } 102 | 103 | /** 104 | * Wraps a string in a marker object that is used by `./raw-serializer.js` to 105 | * directly print that string in a snapshot without escaping all double quotes. 106 | * Backticks will still be escaped. 107 | */ 108 | function raw(string) { 109 | if (typeof string !== "string") { 110 | throw new Error("Raw snapshots have to be strings."); 111 | } 112 | return { [Symbol.for("raw")]: string }; 113 | } 114 | -------------------------------------------------------------------------------- /whitespace.md: -------------------------------------------------------------------------------- 1 | ```html 2 | 7 | ``` 8 | 9 | The whitespace between the opening `
    ` tag and the opening `` tag 10 | can be removed. Criteria: 11 | 12 | * Whitespace separates two opening tags 13 | 14 | The whitespace between the closing `` tag and the closing `
    ` tag 15 | can also be removed. 16 | 17 | ```HTML 18 | abc 19 | E 20 | fg 21 | ``` 22 | 23 | Here, neither the leading whitespace nor the trailing whitespace in the `span` tag 24 | can be removed, because it would change the appearance. What are the criteria? 25 | 26 | * The element is an inline element... 27 | * ...AND the node before the current element (left sibling) is a `PrintTextStatement` ... 28 | * ...AND there is no whitespace before the current element 29 | * THEN leading whitespace in the current element must be preserved 30 | * OTHERWISE it can be removed 31 | 32 | IDEA: This kind of information can be passed on from parent to child by adding additional properties to the children AST nodes: 33 | 34 | * `SequenceExpression` could pass on information on whitespace to children. 35 | * Then, `Element` would know if it is preceded by whitespace or something else. 36 | * And so on... 37 | 38 | Strictly speaking, you may also not insert whitespace between "abc" and the opening ``, even though this might be an edge case that can be ignored for now. 39 | 40 | ## SequenceExpression 41 | 42 | * Whitespace only: Only count newline characters, and output those (as hardlines?) 43 | * If no newline, but whitespace only: Normalize whitespace, respect surrounding inline elements 44 | * If PrintTextStatement: Leading/trailing whitespace will be taken care of by TextStatement. Just pass the information as PRESERVE_LEADING_WHITESPACE and PRESERVE_TRAILING_WHITESPACE, respectively 45 | 46 | ```html 47 |
    48 |
    49 | 50 |
    51 | {{ 'checking_deals' }} 52 |
    53 | ``` 54 | 55 | ## Algorithm for Element 56 | 57 | 1. Remove surrounding whitespace 58 | 2. Add "preserve whitespace" info to PrintTextStatement nodes 59 | 3. Iterate over children, start with empty result 60 | 4. If inline element, put in temporary group. Eventually, use fill() on it. 61 | 5. If block element => add to result 62 | --------------------------------------------------------------------------------