├── test ├── fixtures │ ├── errors │ │ ├── expression.astro │ │ ├── attribute.astro │ │ ├── frontmatter.astro │ │ └── style.astro │ ├── other │ │ ├── autocloses-open-tags │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── binary-expression │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── ignore-self-close │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── embedded-expr-options │ │ │ ├── output.astro │ │ │ ├── input.astro │ │ │ ├── options.js │ │ │ └── custom-plugin.js │ │ ├── slots │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── expr-and-html-comment │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── directive │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── prettier-ignore-html │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── spread-attributes │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── typescript-expression │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── hugging │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── attribute-with-embedded-expr │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── jsx-comments │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── expression-in-inline-tag │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── prettier-ignore-js │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── spread-operator │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── script-types │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── preserve-tag-case │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── with-script │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── shorthand-in-expression │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── expression-multiple-roots │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── frontmatter │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── non-jsx-compatible-characters │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── clean-self-closing │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── nested-comment │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── fragment │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── expression-with-inline-comments │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── doctype-with-embedded-expr │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── embedded-expr │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── doctype-with-extra-attributes │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── expression-multiple-roots-stress │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── unclosed-tag │ │ │ ├── output.astro │ │ │ └── input.astro │ │ └── format-with-cursor-position │ │ │ ├── input.astro │ │ │ └── output.astro │ ├── options │ │ ├── option-tab-width │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-semicolon-true │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-print-width │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-semicolon-false │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-use-tabs-false │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-use-tabs-true │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-single-quote-true │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-arrow-parens-always │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-arrow-parens-avoid │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-bracket-spacing-true │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-single-quote-false │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-trailing-comma-es5 │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-trailing-comma-none │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-bracket-same-line-false │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-bracket-same-line-true │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-bracket-spacing-false │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-jsx-single-quote-true │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-quote-props-as-needed │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-quote-props-preserve │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-quote-props-consistent │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-astro-allow-shorthand-false │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-astro-allow-shorthand-true │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── single-attribute-per-line-false │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── single-attribute-per-line-true │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-prose-wrap-always │ │ │ ├── options.json │ │ │ ├── input.md │ │ │ └── output.md │ │ ├── option-prose-wrap-never │ │ │ ├── options.json │ │ │ ├── input.md │ │ │ └── output.md │ │ ├── option-astro-allow-skip-frontmatter-true │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-astro-sort-order-markup-styles │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-astro-sort-order-styles-markup │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-html-whitespace-sensitivity-css │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-prose-wrap-preserve │ │ │ ├── options.json │ │ │ ├── input.md │ │ │ └── output.md │ │ ├── option-html-whitespace-sensitivity-ignore │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-html-whitespace-sensitivity-strict │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── option-jsx-single-quote-false │ │ │ ├── options.json │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── option-html-whitespace-sensitivity-ignore-component │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ │ └── option-bracket-same-line-html-true-whitespace-sensitivity-ignore │ │ │ ├── options.json │ │ │ ├── input.astro │ │ │ └── output.astro │ ├── basic │ │ ├── html-class-attribute │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── html-custom-elements │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── html-class-attribute-with-line-breaks │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── html-comment │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── single-style-element │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── self-closing │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── basic-html │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── inline-whitespace │ │ │ ├── input.astro │ │ │ └── output.astro │ │ └── simple-text │ │ │ ├── input.astro │ │ │ └── output.astro │ ├── styles │ │ ├── with-styles-and-body-tag-complex │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── with-less │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── with-styles │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── style-tag-attributes │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── with-unknown │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── with-styles-and-body-tag │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── with-sass │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── with-scss │ │ │ ├── output.astro │ │ │ └── input.astro │ │ ├── format-nested-sass-style-tag-content │ │ │ ├── input.astro │ │ │ └── output.astro │ │ ├── format-nested-style-tag-content │ │ │ ├── input.astro │ │ │ └── output.astro │ │ └── with-indented-sass │ │ │ ├── input.astro │ │ │ └── output.astro │ └── return │ │ ├── return-basic │ │ ├── options.json │ │ ├── output.astro │ │ └── input.astro │ │ ├── return-semicolon-false │ │ ├── options.json │ │ ├── output.astro │ │ └── input.astro │ │ ├── return-single-quote-true │ │ ├── options.json │ │ ├── output.astro │ │ └── input.astro │ │ ├── return-arrow-parens-avoid │ │ ├── options.json │ │ ├── output.astro │ │ └── input.astro │ │ ├── return-trailing-comma-all │ │ ├── options.json │ │ ├── output.astro │ │ └── input.astro │ │ ├── return-trailing-comma-none │ │ ├── options.json │ │ ├── output.astro │ │ └── input.astro │ │ └── return-bracket-spacing-false │ │ ├── options.json │ │ ├── output.astro │ │ └── input.astro ├── tests │ ├── markdown.test.ts │ ├── basic.test.ts │ ├── errors.test.ts │ ├── return.test.ts │ ├── styles.test.ts │ ├── other.test.ts │ └── options.test.ts └── test-utils.ts ├── .gitignore ├── .git-blame-ignore-revs ├── .vscode ├── extensions.json └── settings.json ├── tsconfig.eslint.json ├── vitest.config.ts ├── .changeset ├── config.json └── README.md ├── .editorconfig ├── .github ├── workflows │ ├── format.yml │ ├── congrats.yml │ ├── prerelease.yml │ ├── ci.yml │ └── release.yml ├── PULL_REQUEST_TEMPLATE.md ├── actions │ └── install │ │ └── action.yml └── ISSUE_TEMPLATE │ ├── config.yml │ └── ---01-bug-report.yml ├── tsconfig.json ├── src ├── get-visitor-keys.ts ├── options.ts ├── printer │ ├── nodes.ts │ ├── elements.ts │ ├── index.ts │ └── utils.ts └── index.ts ├── rollup.config.mjs ├── CONTRIBUTING.md ├── biome.json ├── .gitpod.yml ├── package.json ├── LICENSE ├── eslint.config.js ├── README.md └── CHANGELOG.md /test/fixtures/errors/expression.astro: -------------------------------------------------------------------------------- 1 | { 2 | [.] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/other/autocloses-open-tags/input.astro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | *.log 5 | -------------------------------------------------------------------------------- /test/fixtures/errors/attribute.astro: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /test/fixtures/other/autocloses-open-tags/output.astro: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/errors/frontmatter.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const hello = 3 | --- 4 | -------------------------------------------------------------------------------- /test/fixtures/errors/style.astro: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /test/fixtures/options/option-tab-width/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 3 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/other/binary-expression/output.astro: -------------------------------------------------------------------------------- 1 |

2 | {1 + 2} 3 |

4 | -------------------------------------------------------------------------------- /test/fixtures/basic/html-class-attribute/output.astro: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /test/fixtures/options/option-semicolon-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/other/ignore-self-close/input.astro: -------------------------------------------------------------------------------- 1 | 2 |
  • 3 | -------------------------------------------------------------------------------- /test/fixtures/basic/html-class-attribute/input.astro: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /test/fixtures/basic/html-custom-elements/output.astro: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/options/option-print-width/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-semicolon-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-use-tabs-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-use-tabs-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/other/ignore-self-close/output.astro: -------------------------------------------------------------------------------- 1 | 2 |
  • 3 | -------------------------------------------------------------------------------- /test/fixtures/options/option-single-quote-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-arrow-parens-always/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-arrow-parens-avoid/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-spacing-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-single-quote-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-trailing-comma-es5/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-trailing-comma-none/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-spacing-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-jsx-single-quote-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-quote-props-as-needed/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "quoteProps": "as-needed" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-quote-props-preserve/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "quoteProps": "preserve" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-quote-props-consistent/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "quoteProps": "consistent" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/other/binary-expression/input.astro: -------------------------------------------------------------------------------- 1 | 2 |

    3 | { 1 + 2}

    4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/basic/html-custom-elements/input.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-shorthand-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "astroAllowShorthand": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-shorthand-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "astroAllowShorthand": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/single-attribute-per-line-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleAttributePerLine": false 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/single-attribute-per-line-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleAttributePerLine": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/other/embedded-expr-options/output.astro: -------------------------------------------------------------------------------- 1 |
    2 | {(
    )} 3 |
    4 | -------------------------------------------------------------------------------- /test/fixtures/other/slots/input.astro: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    -------------------------------------------------------------------------------- /test/fixtures/other/slots/output.astro: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 | -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-always/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "always", 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-never/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "never", 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-skip-frontmatter-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "astroAllowSkipFrontmatter": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-sort-order-markup-styles/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "astroSortOrder": "markup | styles" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-sort-order-styles-markup/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "astroSortOrder": "styles | markup" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-html-whitespace-sensitivity-css/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "htmlWhitespaceSensitivity": "css" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-preserve/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "preserve", 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/other/embedded-expr-options/input.astro: -------------------------------------------------------------------------------- 1 |
    2 | { 3 |
    4 | } 5 |
    6 | -------------------------------------------------------------------------------- /test/fixtures/options/option-html-whitespace-sensitivity-ignore/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "htmlWhitespaceSensitivity": "ignore" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-html-whitespace-sensitivity-strict/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "htmlWhitespaceSensitivity": "strict" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/options/option-jsx-single-quote-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/basic/html-class-attribute-with-line-breaks/output.astro: -------------------------------------------------------------------------------- 1 |
    4 | -------------------------------------------------------------------------------- /test/fixtures/basic/html-class-attribute-with-line-breaks/input.astro: -------------------------------------------------------------------------------- 1 |
    4 | -------------------------------------------------------------------------------- /test/fixtures/basic/html-comment/input.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-spacing-false/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const obj = {prop: false}; 3 | --- 4 | 5 |

    6 | {obj.prop} 7 |

    8 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-spacing-true/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const obj = { prop: false }; 3 | --- 4 | 5 |

    6 | {obj.prop} 7 |

    8 | -------------------------------------------------------------------------------- /test/fixtures/options/option-tab-width/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | function hello() { 3 | return "hello" 4 | } 5 | --- 6 |

    7 | {hello} 8 |

    -------------------------------------------------------------------------------- /test/fixtures/options/option-use-tabs-true/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | function hello() { 3 | return "hello" 4 | } 5 | --- 6 |

    7 | {hello} 8 |

    -------------------------------------------------------------------------------- /test/fixtures/other/expr-and-html-comment/output.astro: -------------------------------------------------------------------------------- 1 |
    {`testing 1 2 3`}
    2 | 3 | 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/html-comment/output.astro: -------------------------------------------------------------------------------- 1 | 6 |
    lorem
    7 | -------------------------------------------------------------------------------- /test/fixtures/options/option-use-tabs-false/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | function hello() { 3 | return "hello" 4 | } 5 | --- 6 |

    7 | {hello} 8 |

    -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-spacing-false/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const obj = { prop: false } 3 | 4 | --- 5 |

    6 | {obj.prop} 7 |

    -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-spacing-true/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const obj = { prop: false } 3 | 4 | --- 5 |

    6 | {obj.prop} 7 |

    -------------------------------------------------------------------------------- /test/fixtures/options/option-tab-width/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | function hello() { 3 | return "hello"; 4 | } 5 | --- 6 | 7 |

    8 | {hello} 9 |

    10 | -------------------------------------------------------------------------------- /test/fixtures/options/option-use-tabs-true/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | function hello() { 3 | return "hello"; 4 | } 5 | --- 6 | 7 |

    8 | {hello} 9 |

    10 | -------------------------------------------------------------------------------- /test/fixtures/other/directive/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const content = "lorem"; 3 | --- 4 | 5 |

    6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/other/prettier-ignore-html/output.astro: -------------------------------------------------------------------------------- 1 |
    One
    2 | 3 | 4 |
    Two
    5 | 6 |
    Three
    7 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-styles-and-body-tag-complex/input.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/fixtures/options/option-html-whitespace-sensitivity-ignore-component/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": true, 3 | "htmlWhitespaceSensitivity": "ignore" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/options/option-use-tabs-false/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | function hello() { 3 | return "hello"; 4 | } 5 | --- 6 | 7 |

    8 | {hello} 9 |

    10 | -------------------------------------------------------------------------------- /test/fixtures/other/spread-attributes/input.astro: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |
    6 | -------------------------------------------------------------------------------- /test/fixtures/other/spread-attributes/output.astro: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |
    6 | -------------------------------------------------------------------------------- /test/fixtures/other/typescript-expression/output.astro: -------------------------------------------------------------------------------- 1 | {[].map((item: string) =>
    {item}
    )} 2 | 3 |
    item).join()}>
    4 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Update Prettier configuration 2 | e0b6f19c6f75c12997143be739be81f6b4898357 3 | # Update formatting setup 4 | 03b874611bbff16936e6c1887c428213e028beef 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "EditorConfig.EditorConfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/options/option-arrow-parens-always/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const foo = ()=> false; 3 | const foo2 = (a)=> a; 4 | const foo3 = a=> { 5 | return a 6 | }; 7 | --- -------------------------------------------------------------------------------- /test/fixtures/options/option-arrow-parens-avoid/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const foo = ()=> false; 3 | const foo2 = (a)=> a; 4 | const foo3 = a=> { 5 | return a 6 | }; 7 | --- -------------------------------------------------------------------------------- /test/fixtures/options/option-arrow-parens-avoid/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const foo = () => false; 3 | const foo2 = a => a; 4 | const foo3 = a => { 5 | return a; 6 | }; 7 | --- 8 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-shorthand-false/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Comp from "./comp.astro" 3 | --- 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/other/hugging/input.astro: -------------------------------------------------------------------------------- 1 |

    2 | If the text is longer than the line length for Prettier to start wrapping, this is... not fine.

    -------------------------------------------------------------------------------- /test/fixtures/options/option-arrow-parens-always/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const foo = () => false; 3 | const foo2 = (a) => a; 4 | const foo3 = (a) => { 5 | return a; 6 | }; 7 | --- 8 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-styles-and-body-tag-complex/output.astro: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "checkJs": true 5 | }, 6 | "include": ["src/**/*.ts", "test/**/*.ts", "*"] 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-html-true-whitespace-sensitivity-ignore/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": true, 3 | "htmlWhitespaceSensitivity": "ignore" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-shorthand-false/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Comp from "./comp.astro"; 3 | --- 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/other/attribute-with-embedded-expr/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export let post 3 | export let author 4 | --- 5 | 6 | {author.name} -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | threads: false, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-shorthand-true/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Comp from "./comp.astro"; 3 | --- 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-sort-order-markup-styles/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const number = 10 3 | --- 4 | 5 | 10 | 11 |

    lorem

    -------------------------------------------------------------------------------- /test/fixtures/other/attribute-with-embedded-expr/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export let post; 3 | export let author; 4 | --- 5 | 6 | {author.name} 7 | -------------------------------------------------------------------------------- /test/fixtures/other/directive/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const content = "lorem" 3 | --- 4 | 5 |

    6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-sort-order-styles-markup/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const number = 10 3 | --- 4 | 5 |

    lorem

    6 | 11 | -------------------------------------------------------------------------------- /test/fixtures/other/jsx-comments/input.astro: -------------------------------------------------------------------------------- 1 | { // Hello 2 | } 3 | 4 | { 5 | // Hey 6 | // Hello 7 | 8 | 9 | 10 | } 11 | 12 | {/* Hello */ } 13 | 14 | {/* 15 | Hello 16 | */} 17 | -------------------------------------------------------------------------------- /test/fixtures/other/jsx-comments/output.astro: -------------------------------------------------------------------------------- 1 | { 2 | // Hello 3 | } 4 | 5 | { 6 | // Hey 7 | // Hello 8 | } 9 | 10 | {/* Hello */} 11 | 12 | { 13 | /* 14 | Hello 15 | */ 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/options/option-quote-props-as-needed/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const myObject = { 3 | "prop-1": "hello", 4 | prop2: 10, 5 | prop3: { 6 | prop4: false, 7 | }, 8 | }; 9 | --- 10 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-less/input.astro: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-styles/output.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-shorthand-true/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Comp from "./comp.astro" 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-sort-order-markup-styles/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const number = 10; 3 | --- 4 | 5 |

    lorem

    6 | 7 | 12 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-sort-order-styles-markup/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const number = 10; 3 | --- 4 | 5 | 10 | 11 |

    lorem

    12 | -------------------------------------------------------------------------------- /test/fixtures/options/option-quote-props-consistent/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const myObject = { 3 | "prop-1": "hello", 4 | "prop2": 10, 5 | "prop3": { 6 | prop4: false, 7 | }, 8 | }; 9 | --- 10 | -------------------------------------------------------------------------------- /test/fixtures/options/option-quote-props-preserve/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const myObject = { 3 | "prop-1": "hello", 4 | "prop2": 10, 5 | prop3: { 6 | "prop4": false, 7 | }, 8 | }; 9 | --- 10 | -------------------------------------------------------------------------------- /test/fixtures/other/expression-in-inline-tag/input.astro: -------------------------------------------------------------------------------- 1 | {['long', 'long', 'long', 'long', 'long', 'long', 'long', 'long', 'long', 'long', 'long', 'expression'].join('') 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/other/hugging/output.astro: -------------------------------------------------------------------------------- 1 |

    2 | If the text is longer than the line length for Prettier to start wrapping, 4 | this is... not fine. 6 |

    7 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-less/output.astro: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-styles/input.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    2 | 3 | 4 | 9 | 10 | 10 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-skip-frontmatter-true/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Comp from "./comp.astro" 3 | --- 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/other/typescript-expression/input.astro: -------------------------------------------------------------------------------- 1 | { 2 | [].map((item: string) =>
    3 | {item} 4 |
    ) 5 | } 6 | 7 |
    8 | 9 | 10 | 11 | item).join()}>
    12 | -------------------------------------------------------------------------------- /test/fixtures/return/return-basic/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "jsxSingleQuote": false, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/options/option-astro-allow-skip-frontmatter-true/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Comp from "./comp.astro" 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/return/return-semicolon-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": false, 4 | "jsxSingleQuote": false, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/return/return-single-quote-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "jsxSingleQuote": true, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-html-true-whitespace-sensitivity-ignore/input.astro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-html-true-whitespace-sensitivity-ignore/output.astro: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /test/fixtures/return/return-arrow-parens-avoid/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "jsxSingleQuote": false, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/return/return-trailing-comma-all/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "jsxSingleQuote": false, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/return/return-trailing-comma-none/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "jsxSingleQuote": false, 5 | "trailingComma": "none", 6 | "bracketSpacing": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/basic/self-closing/input.astro: -------------------------------------------------------------------------------- 1 | A random image 2 | 3 | A random image 8 | -------------------------------------------------------------------------------- /test/fixtures/return/return-bracket-spacing-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "jsxSingleQuote": false, 5 | "trailingComma": "es5", 6 | "bracketSpacing": false, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/styles/style-tag-attributes/input.astro: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /test/fixtures/other/prettier-ignore-js/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { matrix } from "math"; 3 | 4 | matrix(1, 0, 0, 0, 1, 0, 0, 0, 1); 5 | 6 | // prettier-ignore 7 | matrix( 8 | 1, 0, 0, 9 | 0, 1, 0, 10 | 0, 0, 1 11 | ) 12 | --- 13 | -------------------------------------------------------------------------------- /test/fixtures/other/spread-operator/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const meta = { title: "My Title", lang: "en" } 3 | --- 4 | Foo 5 | 6 | 7 | {meta && Bar} 8 | -------------------------------------------------------------------------------- /test/fixtures/other/spread-operator/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const meta = { title: "My Title", lang: "en" }; 3 | --- 4 | 5 | Foo 6 | 7 | 8 | {meta && Bar} 9 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-unknown/input.astro: -------------------------------------------------------------------------------- 1 | ❤️ 2 | 3 | 12 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-unknown/output.astro: -------------------------------------------------------------------------------- 1 | ❤️ 2 | 3 | 12 | -------------------------------------------------------------------------------- /test/fixtures/options/option-single-quote-false/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const myVar = `lorem "quoted"` 3 | const myVar2 = 'lorem' 4 | function hello() { 5 | return "hello" + 'lorem' 6 | } 7 | --- 8 |

    9 | {hello + "hello"} 10 |

    -------------------------------------------------------------------------------- /test/fixtures/options/option-single-quote-false/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const myVar = `lorem "quoted"`; 3 | const myVar2 = "lorem"; 4 | function hello() { 5 | return "hello" + "lorem"; 6 | } 7 | --- 8 | 9 |

    10 | {hello + "hello"} 11 |

    12 | -------------------------------------------------------------------------------- /test/fixtures/options/option-single-quote-true/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const myVar = `lorem "quoted"` 3 | const myVar2 = "lorem" 4 | function hello() { 5 | return "hello" + 'lorem' 6 | } 7 | --- 8 |

    9 | {hello + "hello"} 10 |

    -------------------------------------------------------------------------------- /test/fixtures/options/option-single-quote-true/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const myVar = `lorem "quoted"`; 3 | const myVar2 = 'lorem'; 4 | function hello() { 5 | return 'hello' + 'lorem'; 6 | } 7 | --- 8 | 9 |

    10 | {hello + 'hello'} 11 |

    12 | -------------------------------------------------------------------------------- /test/fixtures/basic/single-style-element/input.astro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/basic/self-closing/output.astro: -------------------------------------------------------------------------------- 1 | A random image 6 | 7 | A random image 12 | -------------------------------------------------------------------------------- /test/fixtures/other/prettier-ignore-js/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { matrix } from "math"; 3 | 4 | matrix( 5 | 1, 0, 0, 6 | 0, 1, 0, 7 | 0, 0, 1 8 | ) 9 | 10 | // prettier-ignore 11 | matrix( 12 | 1, 0, 0, 13 | 0, 1, 0, 14 | 0, 0, 1 15 | ) 16 | 17 | --- 18 | -------------------------------------------------------------------------------- /test/fixtures/other/script-types/input.astro: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/other/expr-and-html-comment/input.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    {`testing 1 2 3`}
    7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/fixtures/other/preserve-tag-case/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Footer from "./Footer.astro"; 3 | import { Body } from "lib"; 4 | --- 5 | 6 |
    7 |
    this is a footer component
    8 |
    this is a regular footer element
    9 | 10 |
    11 | -------------------------------------------------------------------------------- /test/fixtures/other/script-types/output.astro: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/other/preserve-tag-case/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Footer from "./Footer.astro"; 3 | import {Body} from "lib"; 4 | --- 5 | 6 |
    7 |
    this is a footer component
    8 |
    this is a regular footer element
    9 | 10 |
    -------------------------------------------------------------------------------- /test/fixtures/styles/style-tag-attributes/output.astro: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /test/fixtures/options/option-semicolon-false/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 3 | function hello() { 4 | return "hello" 5 | } 6 | --- 7 |

    8 | {hello} 9 | {numbers.map((number) => { 10 | return

    {number}

    11 | } 12 | )} 13 |

    -------------------------------------------------------------------------------- /test/fixtures/options/option-semicolon-true/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 3 | function hello() { 4 | return "hello" 5 | } 6 | --- 7 |

    8 | {hello} 9 | {numbers.map((number) => { 10 | return

    {number}

    11 | } 12 | )} 13 |

    -------------------------------------------------------------------------------- /test/fixtures/options/option-semicolon-false/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 3 | function hello() { 4 | return "hello" 5 | } 6 | --- 7 | 8 |

    9 | {hello} 10 | { 11 | numbers.map((number) => { 12 | return

    {number}

    13 | }) 14 | } 15 |

    16 | -------------------------------------------------------------------------------- /test/fixtures/options/option-semicolon-true/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 3 | function hello() { 4 | return "hello"; 5 | } 6 | --- 7 | 8 |

    9 | {hello} 10 | { 11 | numbers.map((number) => { 12 | return

    {number}

    ; 13 | }) 14 | } 15 |

    16 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/other/with-script/output.astro: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/other/shorthand-in-expression/input.astro: -------------------------------------------------------------------------------- 1 | {enabled &&
    } 6 | 7 | {enabled &&
    } 12 | 13 | {[].map((item) => { 14 | return <> 15 |
    16 |
    17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = tab 11 | insert_final_newline = true 12 | trim_trailing_whitespace = false 13 | 14 | [{.*,*.md,*.json,*.toml,*.yml,}] 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /test/fixtures/other/embedded-expr-options/options.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url'; 2 | 3 | export default { 4 | plugins: [ 5 | fileURLToPath(new URL('../../../../dist/index.js', import.meta.url)), 6 | fileURLToPath(new URL('./custom-plugin.js', import.meta.url)), 7 | ], 8 | customPluginClass: 'my-custom-class', 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/other/expression-multiple-roots/input.astro: -------------------------------------------------------------------------------- 1 | {hello ??
    } 2 | 3 | {hello ?
    : } 4 | 5 | {[].map((value) =>
    {value}

    {value}

    )} 6 | 7 | {[].map((value) => { 8 | console.log(value); 9 | 10 | return

    11 | })} 12 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-styles-and-body-tag/output.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | Title of the document 4 | 5 | 6 | 7 |

    This is a heading

    8 |

    This is a paragraph.

    9 | 10 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /test/fixtures/other/shorthand-in-expression/output.astro: -------------------------------------------------------------------------------- 1 | {enabled &&
    } 2 | 3 | {enabled &&
    } 4 | 5 | { 6 | [].map((item) => { 7 | return ( 8 | <> 9 |
    10 |
    11 | 12 | ); 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-styles-and-body-tag/input.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | Title of the document 4 | 5 | 6 | 7 |

    This is a heading

    8 |

    This is a paragraph.

    9 | 10 | 11 | 12 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /test/fixtures/other/frontmatter/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Color from "../components/Color.jsx"; 3 | 4 | let title = "My Site"; 5 | 6 | const colors = ["red", "yellow", "blue"]; 7 | --- 8 | 9 | 10 | 11 | My site 12 | 13 | 14 |

    {title}

    15 | 16 | 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Make sure fixtures don't get formatted by accident while working on them 3 | "[astro]": { 4 | "editor.formatOnSave": false, 5 | "editor.formatOnPaste": false, 6 | "editor.formatOnType": false, 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports": "never" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/other/with-script/input.astro: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /test/tests/markdown.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '../test-utils'; 2 | 3 | const files = import.meta.glob('/test/fixtures/markdown/*/*', { 4 | eager: true, 5 | as: 'raw', 6 | }); 7 | 8 | test( 9 | 'can format an Astro file containing an Astro file embedded in a codeblock', 10 | files, 11 | 'markdown/embedded-in-markdown', 12 | true, 13 | ); 14 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | prettier: 11 | if: github.repository_owner == 'withastro' 12 | uses: withastro/automation/.github/workflows/format.yml@main 13 | with: 14 | command: "format" 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /test/fixtures/other/expression-in-inline-tag/output.astro: -------------------------------------------------------------------------------- 1 | { 3 | [ 4 | "long", 5 | "long", 6 | "long", 7 | "long", 8 | "long", 9 | "long", 10 | "long", 11 | "long", 12 | "long", 13 | "long", 14 | "long", 15 | "expression", 16 | ].join("") 17 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/other/non-jsx-compatible-characters/output.astro: -------------------------------------------------------------------------------- 1 | {(
    Astro!
    )} 2 | 3 | {(
    Astro!
    )} 4 | 5 | {true &&
    blah
    } 6 | 7 | {arr.map(() => } 30 | 31 | 32 | { 33 |
    34 | } 35 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-true/output.astro: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-false/output.astro: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-false/input.astro: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /test/fixtures/options/option-bracket-same-line-true/input.astro: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-sass/output.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    2 | 3 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/fixtures/other/fragment/input.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | <> 11 | Hello world! 12 | 13 | lorem 14 | <>Hello world! 15 | 16 | lorem 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-scss/output.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    2 | 3 | 33 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import { defineConfig } from 'rollup'; 4 | 5 | export default defineConfig({ 6 | input: 'src/index.ts', 7 | plugins: [commonjs(), typescript()], 8 | external: [ 9 | 'prettier', 10 | 'prettier/doc', 11 | '@astrojs/compiler', 12 | '@astrojs/compiler/utils', 13 | '@astrojs/compiler/sync', 14 | 'sass-formatter', 15 | 'node:module', 16 | 'node:buffer', 17 | ], 18 | output: { 19 | dir: 'dist', 20 | format: 'esm', 21 | sourcemap: true, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /test/fixtures/other/expression-with-inline-comments/input.astro: -------------------------------------------------------------------------------- 1 | { 2 | // For best cross-browser support of sticky or fixed elements, they must not be nested 3 | // inside elements that hide any overflow axis. The article content hides `overflow-x`, 4 | // so we must place the mobile TOC here. 5 | headers && ( 6 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-sass/input.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    2 | 3 | 34 | 35 | 39 | -------------------------------------------------------------------------------- /test/fixtures/options/single-attribute-per-line-false/input.astro: -------------------------------------------------------------------------------- 1 |
    2 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 |
    4 |
    5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 6 |
    7 |
    8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 9 |
    10 |
    11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 12 |
    13 | {[1,2,3].map((num) => { 14 | return
    {num}
    15 | } 16 | ) 17 | } 18 | s 19 | -------------------------------------------------------------------------------- /test/fixtures/options/single-attribute-per-line-true/input.astro: -------------------------------------------------------------------------------- 1 |
    2 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 |
    4 |
    5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 6 |
    7 |
    8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 9 |
    10 |
    11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 12 |
    13 | {[1,2,3].map((num) => { 14 | return
    {num}
    15 | } 16 | ) 17 | } 18 | s 19 | -------------------------------------------------------------------------------- /test/fixtures/other/doctype-with-embedded-expr/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Color from "../components/Color.jsx"; 3 | 4 | let title = "My Site"; 5 | 6 | const colors = ["red", "yellow", "blue"]; 7 | --- 8 | 9 | 10 | 11 | 12 | My site 13 | 14 | 15 |

    {title}

    16 | 17 | { 18 | "I'm some super long text and oh boy I sure do hope this formatter doesn't break me!" 19 | } 20 | 21 | { 22 | colors.map((color) => ( 23 |
    24 | 25 |
    26 | )) 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/fixtures/other/embedded-expr/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Color from "../components/Color.jsx"; 3 | 4 | let title = "My Site"; 5 | 6 | const colors = ["red", "yellow", "blue"]; 7 | --- 8 | 9 | 10 | 11 | My site 12 | 13 | 14 |

    {title}

    15 | 16 | { 17 | "I'm some super long text and oh boy I sure do hope this formatter doesn't break me!" 18 | } 19 | 20 | { 21 | colors.map((color) => ( 22 |
    23 | 24 |
    25 | )) 26 | } 27 | 28 | {/* JSX Comment */} 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/fixtures/options/option-print-width/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const arrayTest = [{ 3 | content: "Princesseuh", 4 | attributes: { x: "55", y: "105", "font-size": "70px", fill: "#fefffe" }, 5 | }, 6 | { 7 | content: "Introducing Astro: Ship Less JavaScript", 8 | attributes: { x: "50", y: "325", "font-size": "40px", fill: "#fefffe" }, 9 | }, 10 | ] 11 | --- 12 | 15 | -------------------------------------------------------------------------------- /test/fixtures/styles/format-nested-sass-style-tag-content/input.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 35 | 36 | -------------------------------------------------------------------------------- /test/fixtures/other/expression-multiple-roots/output.astro: -------------------------------------------------------------------------------- 1 | { 2 | hello ?? ( 3 | <> 4 |
    5 |
    6 | 7 | ) 8 | } 9 | 10 | { 11 | hello ? ( 12 | <> 13 |
    14 |
    15 | 16 | ) : ( 17 | <> 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | { 25 | [].map((value) => ( 26 | <> 27 |
    {value}
    28 |

    {value}

    29 | 30 | )) 31 | } 32 | 33 | { 34 | [].map((value) => { 35 | console.log(value); 36 | 37 | return ( 38 | <> 39 |
    40 |

    41 | 42 | ); 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /test/fixtures/other/doctype-with-extra-attributes/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Color from "../components/Color.jsx"; 3 | 4 | let title = "My Site"; 5 | 6 | const colors = ["red", "yellow", "blue"]; 7 | --- 8 | 9 | 10 | 11 | 12 | My site 13 | 14 | 15 |

    {title}

    16 | 17 | {"I'm some super long text and oh boy I sure do hope this formatter doesn't break me!"} 18 | 19 | {colors.map((color) =>
    20 | 21 |
    )} 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/fixtures/other/expression-with-inline-comments/output.astro: -------------------------------------------------------------------------------- 1 | { 2 | // For best cross-browser support of sticky or fixed elements, they must not be nested 3 | // inside elements that hide any overflow axis. The article content hides `overflow-x`, 4 | // so we must place the mobile TOC here. 5 | headers && ( 6 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/other/doctype-with-embedded-expr/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Color from '../components/Color.jsx'; 3 | 4 | let title = 5 | 'My Site'; 6 | 7 | const colors = ['red', 'yellow', 'blue',]; 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | My site 17 | 18 | 19 |

    {title}

    20 | 21 | {"I'm some super long text and oh boy I sure do hope this formatter doesn't break me!"} 22 | 23 | {colors.map(color => ( 24 |
    ))} 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Tools & Dependencies 2 | description: Installs pnpm, Node.js & package dependencies 3 | 4 | inputs: 5 | node-version: 6 | description: 'Node version' 7 | required: false 8 | type: number 9 | default: latest 10 | 11 | runs: 12 | using: composite 13 | steps: 14 | - name: Setup pnpm 15 | uses: pnpm/action-setup@v3 16 | 17 | - name: Setup Node ${{ inputs.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ inputs.node-version }} 21 | cache: pnpm 22 | 23 | - name: Install dependencies 24 | run: pnpm install 25 | shell: bash 26 | -------------------------------------------------------------------------------- /test/fixtures/options/single-attribute-per-line-false/output.astro: -------------------------------------------------------------------------------- 1 |
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    2 |
    3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 4 |
    5 |
    6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 7 |
    8 |
    9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 10 |
    11 | { 12 | [1, 2, 3].map((num) => { 13 | return ( 14 |
    15 | {num} 16 |
    17 | ); 18 | }) 19 | } 20 | s 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💁 Support 4 | url: https://astro.build/chat 5 | about: 'This issue tracker is not for support questions. Join us on Discord for assistance!' 6 | - name: 📘 Documentation 7 | url: https://github.com/withastro/docs 8 | about: File an issue or make an improvement to the docs website. 9 | - name: 💡 Ideas for New Features, Improvements and RFCs 10 | url: https://github.com/withastro/roadmap/discussions 11 | about: Propose and discuss future improvements to Astro 12 | - name: 👾 Chat 13 | url: https://astro.build/chat 14 | about: Our Discord server is active, come join us! 15 | -------------------------------------------------------------------------------- /test/fixtures/other/embedded-expr/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Color from '../components/Color.jsx'; 3 | 4 | let title = 5 | 'My Site'; 6 | 7 | const colors = ['red', 'yellow', 'blue',]; 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | My site 17 | 18 | 19 |

    {title}

    20 | 21 | {"I'm some super long text and oh boy I sure do hope this formatter doesn't break me!"} 22 | 23 | {colors.map(color => ( 24 |
    ))} 25 | 26 | { /* JSX Comment */ } 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/fixtures/styles/format-nested-sass-style-tag-content/output.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-scss/input.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/options/option-print-width/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const arrayTest = [ 3 | { 4 | content: "Princesseuh", 5 | attributes: { x: "55", y: "105", "font-size": "70px", fill: "#fefffe" }, 6 | }, 7 | { 8 | content: "Introducing Astro: Ship Less JavaScript", 9 | attributes: { x: "50", y: "325", "font-size": "40px", fill: "#fefffe" }, 10 | }, 11 | ]; 12 | --- 13 | 14 | 26 | -------------------------------------------------------------------------------- /test/fixtures/other/expression-multiple-roots-stress/input.astro: -------------------------------------------------------------------------------- 1 | { 2 | Promise.all([ 3 | [].map((something) => { 4 | return [ 5 |
    , 6 |
    , 7 |
    , 8 |
    9 | ] 10 | }), 11 | [].map((something) => { 12 | return
    13 | }), 14 | [].map((something) => { 15 | return
    +
    16 | }), 17 | [].map((something) => { 18 | return
    + {something:
    } 19 | }), 20 | 21 |
    22 | ]) 23 | } 24 | 25 | { 26 |
    27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/other/doctype-with-extra-attributes/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Color from '../components/Color.jsx'; 3 | 4 | let title = 5 | 'My Site'; 6 | 7 | const colors = ['red', 'yellow', 'blue',]; 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | My site 17 | 18 | 19 |

    {title}

    20 | 21 | {"I'm some super long text and oh boy I sure do hope this formatter doesn't break me!"} 22 | 23 | {colors.map(color => ( 24 |
    ))} 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/fixtures/return/return-basic/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from "return"; 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | return "return"; 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return "return"; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return`; 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const; 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-never/input.md: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 2 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 3 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 4 | 5 | ``` 6 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 7 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 8 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 9 | ``` -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-never/output.md: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 2 | 3 | ``` 4 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 5 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 6 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 7 | ``` 8 | -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-preserve/input.md: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 2 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 3 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 4 | ``` 5 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 6 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 7 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 8 | ``` -------------------------------------------------------------------------------- /test/fixtures/return/return-semicolon-false/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from "return" 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | return "return" 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return "return" 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return` 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/") 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-always/input.md: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 2 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 3 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 4 | 5 | ``` 6 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 7 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 8 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 9 | ``` -------------------------------------------------------------------------------- /test/fixtures/return/return-arrow-parens-avoid/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from "return"; 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | return "return"; 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return "return"; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return`; 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const; 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/return/return-single-quote-true/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from 'return'; 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | return 'return'; 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return 'return'; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return`; 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const; 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get('token').value) return Astro.redirect('/'); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/return/return-trailing-comma-all/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from "return"; 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | return "return"; 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return "return"; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return`; 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const; 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/options/single-attribute-per-line-true/output.astro: -------------------------------------------------------------------------------- 1 |
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    2 |
    6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 7 |
    8 |
    12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 13 |
    14 |
    19 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 20 |
    21 | { 22 | [1, 2, 3].map((num) => { 23 | return ( 24 |
    28 | {num} 29 |
    30 | ); 31 | }) 32 | } 33 | s 38 | -------------------------------------------------------------------------------- /test/fixtures/return/return-bracket-spacing-false/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from "return"; 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | return "return"; 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return "return"; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return`; 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const; 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/return/return-trailing-comma-none/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from "return"; 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | return "return"; 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return "return"; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return`; 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const; 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/return/return-basic/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from 'return' 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | ;return "return" 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return 'return'; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return` 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-preserve/output.md: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 2 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 3 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 4 | 5 | ``` 6 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 7 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 8 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 9 | ``` 10 | -------------------------------------------------------------------------------- /test/fixtures/return/return-arrow-parens-avoid/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from 'return' 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | ;return "return" 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return 'return'; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return` 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/return/return-semicolon-false/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from 'return' 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | ;return "return" 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return 'return'; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return` 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/return/return-single-quote-true/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from 'return' 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | ;return "return" 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return 'return'; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return` 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/return/return-trailing-comma-all/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from 'return' 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | ;return "return" 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return 'return'; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return` 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/return/return-trailing-comma-none/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from 'return' 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | ;return "return" 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return 'return'; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return` 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /test/fixtures/options/option-prose-wrap-always/output.md: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi 2 | saepe odit sed repellendus voluptatum sunt, quia dolorem quam quos aliquid 3 | dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis 4 | recusandae, animi odit quis cupiditate non culpa voluptatem, officiis magnam 5 | quod aperiam perferendis obcaecati temporibus, iure natus hic? 6 | 7 | ``` 8 | Lorem ipsum dolor sit amet consectetur adipisicing elit. At dignissimos quasi saepe odit sed repellendus voluptatum sunt, quia 9 | dolorem quam quos aliquid dolorum, iste suscipit nisi aliquam. Illum eius velit distinctio corporis recusandae, animi odit quis cupiditate 10 | non culpa voluptatem, officiis magnam quod aperiam perferendis obcaecati temporibus, iure natus hic? 11 | ``` 12 | -------------------------------------------------------------------------------- /test/fixtures/return/return-bracket-spacing-false/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Test 1: we can use the word return in a comment without problems 3 | // return "return" 'return' ;return 4 | 5 | // Test 2: we can use return in an import 6 | import _return from 'return' 7 | 8 | // Test 3: we can use return in a function 9 | function t() { 10 | ;return "return" 11 | } 12 | 13 | // Test 4: we can use return in a block 14 | if (false) { 15 | return 'return'; 16 | } 17 | 18 | // Test 5: we can use return outside a block 19 | return `return` 20 | 21 | // Test 6: we can return multiple things 22 | return 1, 2 as const 23 | 24 | // Test 7: we can return inline 25 | if (Astro.cookies.get("token").value) return Astro.redirect("/"); 26 | --- 27 | 28 | 29 |
    return
    30 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import type { SupportOption } from 'prettier'; 2 | 3 | interface PluginOptions { 4 | astroAllowShorthand: boolean; 5 | astroSkipFrontmatter: boolean; 6 | } 7 | 8 | declare module 'prettier' { 9 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 10 | interface RequiredOptions extends PluginOptions {} 11 | } 12 | 13 | // https://prettier.io/docs/en/plugins.html#options 14 | export const options: Record = { 15 | astroAllowShorthand: { 16 | category: 'Astro', 17 | type: 'boolean', 18 | default: false, 19 | description: 'Enable/disable attribute shorthand if attribute name and expression are the same', 20 | }, 21 | astroSkipFrontmatter: { 22 | category: 'Astro', 23 | type: 'boolean', 24 | default: false, 25 | description: 'Skips the formatting of the frontmatter.', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/printer/nodes.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttributeNode, 3 | CommentNode, 4 | ComponentNode, 5 | CustomElementNode, 6 | DoctypeNode, 7 | ElementNode, 8 | ExpressionNode, 9 | FragmentNode, 10 | FrontmatterNode, 11 | Node, 12 | ParentLikeNode, 13 | RootNode, 14 | TagLikeNode, 15 | TextNode, 16 | } from '@astrojs/compiler/types'; 17 | 18 | export type anyNode = 19 | | RootNode 20 | | AttributeNode 21 | | ElementNode 22 | | ComponentNode 23 | | CustomElementNode 24 | | ExpressionNode 25 | | TextNode 26 | | DoctypeNode 27 | | CommentNode 28 | | FragmentNode 29 | | FrontmatterNode; 30 | 31 | export type { 32 | AttributeNode, 33 | CommentNode, 34 | ComponentNode, 35 | CustomElementNode, 36 | DoctypeNode, 37 | ElementNode, 38 | ExpressionNode, 39 | FragmentNode, 40 | FrontmatterNode, 41 | Node, 42 | ParentLikeNode, 43 | RootNode, 44 | TagLikeNode, 45 | TextNode, 46 | }; 47 | -------------------------------------------------------------------------------- /test/fixtures/other/unclosed-tag/output.astro: -------------------------------------------------------------------------------- 1 |
    2 | 6 | This Site is in no way affiliated or attempting to represent itself as the 8 | Distractible podcast. 10 |
    11 | 12 | 35 | -------------------------------------------------------------------------------- /test/fixtures/other/unclosed-tag/input.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 | 14 | This Site is in no way affiliated or attempting to represent itself as the 16 | Distractible podcast. 18 |
    19 | 20 | 43 | -------------------------------------------------------------------------------- /test/fixtures/styles/format-nested-style-tag-content/input.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: PreRelease 2 | 3 | on: 4 | push: 5 | branches: 6 | - next 7 | 8 | jobs: 9 | release: 10 | name: PreRelease 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out branch 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | 18 | - name: Install Tools & Dependencies 19 | uses: ./.github/actions/install 20 | 21 | - name: Create Release Pull Request or Publish to npm 22 | id: changesets 23 | uses: changesets/action@master 24 | with: 25 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 26 | publish: pnpm release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | -------------------------------------------------------------------------------- /test/tests/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '../test-utils'; 2 | 3 | const files = import.meta.glob('/test/fixtures/basic/*/*', { 4 | eager: true, 5 | as: 'raw', 6 | }); 7 | 8 | test('Can format a basic astro file', files, 'basic/basic-html'); 9 | 10 | test('Can format an Astro file with a single style element', files, 'basic/single-style-element'); 11 | 12 | test('Can format a basic astro only text', files, 'basic/simple-text'); 13 | 14 | test('Can format html comments', files, 'basic/html-comment'); 15 | 16 | test('Can format HTML custom elements', files, 'basic/html-custom-elements'); 17 | 18 | test('Can properly format the class attribute', files, 'basic/html-class-attribute'); 19 | 20 | test( 21 | 'Can properly format the class attribute with line breaks', 22 | files, 23 | 'basic/html-class-attribute-with-line-breaks', 24 | ); 25 | 26 | test('Can format long self-closing tags with multiple attributes', files, 'basic/self-closing'); 27 | 28 | test('Can properly format inline tags and respect whitespace', files, 'basic/inline-whitespace'); 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---01-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: Report an issue or possible bug 3 | title: "\U0001F41B BUG:" 4 | labels: [] 5 | assignees: [] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## Quick Checklist 11 | Thank you for taking the time to file a bug report! Please fill out this form as completely as possible. 12 | 13 | ✅ I am using the **latest version of the Astro Prettier Plugin**. 14 | - type: textarea 15 | attributes: 16 | label: Describe the Bug 17 | description: A clear and concise description of what the bug is. 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Steps to Reproduce 23 | description: Describe the bug in steps that we can reproduce ourselves. 24 | value: | 25 | 1. `npm init astro` using template 26 | 2. ... 27 | 3. ... 28 | 4. ... 29 | 5. Error! Describe what went wrong (and what was expected instead)... 30 | validations: 31 | required: true 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## To get set up 4 | 5 | 1. `git clone git@github.com:withastro/prettier-plugin-astro.git` 6 | 1. `pnpm install` 7 | 1. `pnpm build` 8 | 1. Run [tests](https://vitest.dev/guide/) with `pnpm test` or `pnpm test:watch` for watch mode 9 | 1. Lint code with `pnpm lint` 10 | 1. Format code with `pnpm format` 11 | 1. Run `pnpm changeset` to add your changes to the changelog on version bump. 12 | Most changes to the plugin should be `patch` changes while we're before `1.0.0`. 13 | 14 | ## Notes 15 | 16 | 1. A single test file can be run with `pnpm test *file-name*` 17 | 1. To skip one or more tests in a file, add comments to them individually 18 | 1. Watch mode won't rerun tests when changing an input/output file 19 | 20 | ## Resources for contributing 21 | 22 | - [Prettier rationale](https://prettier.io/docs/en/rationale.html) 23 | - [Prettier plugin docs](https://prettier.io/docs/en/plugins.html) 24 | - [Svelte Prettier plugin](https://github.com/sveltejs/prettier-plugin-svelte) 25 | - [Prettier HTML formatter](https://github.com/prettier/prettier/tree/main/src/language-html) 26 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.1/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "vendor", 6 | "**/dist/**", 7 | "**/smoke/**", 8 | "**/fixtures/**", 9 | "**/vendor/**", 10 | "**/.vercel/**" 11 | ], 12 | "include": ["test/**", "src/**", "*"] 13 | }, 14 | "formatter": { 15 | "indentStyle": "tab", 16 | "indentWidth": 2, 17 | "lineWidth": 100, 18 | "ignore": [".changeset", "pnpm-lock.yaml", "*.astro"] 19 | }, 20 | "organizeImports": { 21 | "enabled": true 22 | }, 23 | "linter": { "enabled": false }, 24 | "javascript": { 25 | "formatter": { 26 | "trailingCommas": "all", 27 | "quoteStyle": "single", 28 | "semicolons": "always" 29 | } 30 | }, 31 | "json": { 32 | "parser": { 33 | "allowComments": true, 34 | "allowTrailingCommas": true 35 | }, 36 | "formatter": { 37 | "indentStyle": "space", 38 | "trailingCommas": "none" 39 | } 40 | }, 41 | "overrides": [ 42 | { 43 | "include": ["package.json"], 44 | "json": { 45 | "formatter": { 46 | "lineWidth": 1 47 | } 48 | } 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /test/fixtures/options/option-html-whitespace-sensitivity-css/input.astro: -------------------------------------------------------------------------------- 1 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? 2 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 3 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 4 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 5 |
    Architecto rerum architecto incidunt sint.
    6 |
    Architecto rerum architecto incidunt sint.
    7 |
    Architecto rerum architecto incidunt sint.
    8 |
    Architecto rerum architecto incidunt sint.
    -------------------------------------------------------------------------------- /test/fixtures/options/option-html-whitespace-sensitivity-ignore/input.astro: -------------------------------------------------------------------------------- 1 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? 2 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 3 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 4 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 5 |
    Architecto rerum architecto incidunt sint.
    6 |
    Architecto rerum architecto incidunt sint.
    7 |
    Architecto rerum architecto incidunt sint.
    8 |
    Architecto rerum architecto incidunt sint.
    -------------------------------------------------------------------------------- /test/fixtures/options/option-html-whitespace-sensitivity-strict/input.astro: -------------------------------------------------------------------------------- 1 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? 2 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 3 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 4 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa architecto 5 |
    Architecto rerum architecto incidunt sint.
    6 |
    Architecto rerum architecto incidunt sint.
    7 |
    Architecto rerum architecto incidunt sint.
    8 |
    Architecto rerum architecto incidunt sint.
    -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Commands to start on workspace startup 3 | tasks: 4 | - init: pnpm install 5 | command: pnpm build 6 | vscode: 7 | extensions: 8 | # TODO Once astro is on [vsx](https://open-vsx.org/), we should be able to specify it as an extension as well! 9 | # https://www.gitpod.io/docs/vscode-extensions 10 | - https://marketplace.visualstudio.com/_apis/public/gallery/publishers/astro-build/vsextensions/astro-vscode/0.7.13/vspackage 11 | - esbenp.prettier-vscode 12 | - dbaeumer.vscode-eslint 13 | github: 14 | prebuilds: 15 | # enable for the master/default branch (defaults to true) 16 | master: true 17 | # enable for all branches in this repo (defaults to false) 18 | branches: true 19 | # enable for pull requests coming from this repo (defaults to true) 20 | pullRequests: true 21 | # enable for pull requests coming from forks (defaults to false) 22 | pullRequestsFromForks: true 23 | # add a "Review in Gitpod" button as a comment to pull requests (defaults to true) 24 | addComment: true 25 | # add a "Review in Gitpod" button to pull requests (defaults to false) 26 | addBadge: false 27 | # add a label once the prebuild is ready to pull requests (defaults to false) 28 | addLabel: prebuilt-in-gitpod 29 | -------------------------------------------------------------------------------- /test/tests/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | import { format } from '../test-utils'; 3 | 4 | const files = import.meta.glob('/test/fixtures/errors/**/*', { 5 | eager: true, 6 | as: 'raw', 7 | }); 8 | 9 | function getFile(allFiles: any, path: string): string { 10 | return allFiles[path]; 11 | } 12 | 13 | it('Correctly errors when parsing faulty frontmatter', async () => { 14 | const content = getFile(files, '/test/fixtures/errors/frontmatter.astro'); 15 | await expect(format(content, {})).rejects.toThrow('Unexpected token (3:1)'); 16 | }); 17 | 18 | it('Correctly errors when parsing faulty expressions', async () => { 19 | const content = getFile(files, '/test/fixtures/errors/expression.astro'); 20 | await expect(format(content, {})).rejects.toThrow('Unexpected token'); 21 | }); 22 | 23 | it('Correctly errors when parsing faulty attributes with expression', async () => { 24 | const content = getFile(files, '/test/fixtures/errors/attribute.astro'); 25 | await expect(format(content, {})).rejects.toThrow('Unexpected token'); 26 | }); 27 | 28 | it('Correctly errors when parsing faulty style tag', async () => { 29 | const content = getFile(files, '/test/fixtures/errors/style.astro'); 30 | await expect(format(content, {})).rejects.toThrow('CssSyntaxError'); 31 | }); 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'CI' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | node_version: [18, 20] 17 | include: 18 | - os: windows-latest 19 | node_version: 18 20 | - os: macos-latest 21 | node_version: 18 22 | fail-fast: false 23 | env: 24 | LANG: en-us 25 | name: 'Test: node-${{ matrix.node_version }}, ${{ matrix.os }}' 26 | steps: 27 | - name: Check out code using Git 28 | uses: actions/checkout@v4 29 | 30 | - name: Install Tools & Dependencies 31 | uses: ./.github/actions/install 32 | 33 | - name: Build prettier-plugin-astro 34 | run: pnpm build 35 | 36 | - name: Test 37 | run: pnpm test 38 | 39 | lint: 40 | runs-on: ubuntu-latest 41 | name: 'Lint: node-16, ubuntu-latest' 42 | steps: 43 | - uses: actions/checkout@v4 44 | 45 | - name: Install Tools & Dependencies 46 | uses: ./.github/actions/install 47 | 48 | - name: Build prettier-plugin-astro 49 | run: pnpm build 50 | 51 | - name: Lint 52 | run: pnpm lint 53 | -------------------------------------------------------------------------------- /test/tests/return.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '../test-utils'; 2 | 3 | const files = import.meta.glob('/test/fixtures/return/*/*', { 4 | eager: true, 5 | as: 'raw', 6 | }); 7 | 8 | test('Can format an Astro file with top-level return', files, 'return/return-basic'); 9 | 10 | test( 11 | 'Can format an Astro file with top-level return with prettier "semi: false" option', 12 | files, 13 | 'return/return-semicolon-false', 14 | ); 15 | 16 | test( 17 | 'Can format an Astro file with top-level return with prettier "singleQuote: true" option', 18 | files, 19 | 'return/return-single-quote-true', 20 | ); 21 | 22 | test( 23 | 'Can format an Astro file with top-level return with prettier "trailingComma: all" option', 24 | files, 25 | 'return/return-trailing-comma-all', 26 | ); 27 | 28 | test( 29 | 'Can format an Astro file with top-level return with prettier "trailingComma: none" option', 30 | files, 31 | 'return/return-trailing-comma-none', 32 | ); 33 | 34 | test( 35 | 'Can format an Astro file with top-level return with prettier "bracketSpacing: false" option', 36 | files, 37 | 'return/return-bracket-spacing-false', 38 | ); 39 | 40 | test( 41 | 'Can format an Astro file with top-level return with prettier "arrowParens: avoid" option', 42 | files, 43 | 'return/return-arrow-parens-avoid', 44 | ); 45 | -------------------------------------------------------------------------------- /test/fixtures/options/option-html-whitespace-sensitivity-css/output.astro: -------------------------------------------------------------------------------- 1 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, 3 | dolor sit amet consectetur adipisicing elit. Odio, sint? 4 | 5 | 6 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor 7 | sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa 8 | architecto 10 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, 12 | dolor sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa 13 | architecto 15 | 16 | Est molestiae sunt facilis quiasa architecto incidunt sint. Lorem ipsum, dolor 17 | sit amet consectetur adipisicing elit. Odio, sint? sunt facilis quiasa 18 | architecto 19 | 20 |
    21 | Architecto rerum architecto incidunt sint. 22 |
    23 |
    24 | Architecto rerum architecto incidunt sint. 25 |
    26 |
    27 | Architecto rerum architecto incidunt sint. 28 |
    29 |
    30 | Architecto rerum architecto incidunt sint. 31 |
    32 | -------------------------------------------------------------------------------- /test/tests/styles.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '../test-utils'; 2 | 3 | const files = import.meta.glob('/test/fixtures/styles/*/*', { 4 | eager: true, 5 | as: 'raw', 6 | }); 7 | 8 | test('Can format a basic Astro file with styles', files, 'styles/with-styles'); 9 | 10 | test( 11 | 'Can format an Astro file with attributes in the 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/printer/elements.ts: -------------------------------------------------------------------------------- 1 | export type TagName = keyof HTMLElementTagNameMap | 'svg'; 2 | 3 | // https://github.com/prettier/prettier/blob/b77d912c0c1a5df85e3e9b5b192fc92523e411ee/vendors/html-void-elements.json 4 | export const selfClosingTags = [ 5 | 'area', 6 | 'base', 7 | 'basefont', 8 | 'bgsound', 9 | 'br', 10 | 'col', 11 | 'command', 12 | 'embed', 13 | 'frame', 14 | 'hr', 15 | 'image', 16 | 'img', 17 | 'input', 18 | 'isindex', 19 | 'keygen', 20 | 'link', 21 | 'menuitem', 22 | 'meta', 23 | 'nextid', 24 | 'param', 25 | 'slot', 26 | 'source', 27 | 'track', 28 | 'wbr', 29 | ]; 30 | 31 | // https://web.archive.org/web/20230108213516/https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements#elements 32 | export const blockElements: TagName[] = [ 33 | 'address', 34 | 'article', 35 | 'aside', 36 | 'blockquote', 37 | 'details', 38 | 'dialog', 39 | 'dd', 40 | 'div', 41 | 'dl', 42 | 'dt', 43 | 'fieldset', 44 | 'figcaption', 45 | 'figure', 46 | 'footer', 47 | 'form', 48 | 'h1', 49 | 'h2', 50 | 'h3', 51 | 'h4', 52 | 'h5', 53 | 'h6', 54 | 'header', 55 | 'hgroup', 56 | 'hr', 57 | 'li', 58 | 'main', 59 | 'nav', 60 | 'ol', 61 | 'p', 62 | 'pre', 63 | 'section', 64 | 'table', 65 | 'ul', 66 | // TODO: WIP 67 | 'title', 68 | 'html', 69 | ]; 70 | 71 | /** 72 | * HTML attributes that we may safely reformat (trim whitespace, add or remove newlines) 73 | */ 74 | export const formattableAttributes: string[] = [ 75 | // None at the moment 76 | // Prettier HTML does not format attributes at all 77 | // and to be consistent we leave this array empty for now 78 | ]; 79 | -------------------------------------------------------------------------------- /test/fixtures/other/embedded-expr-options/custom-plugin.js: -------------------------------------------------------------------------------- 1 | import * as astro from '../../../../dist/index.js'; 2 | 3 | let original = astro.parsers.astroExpressionParser; 4 | 5 | export const options = { 6 | customPluginClass: { 7 | since: '1.0.0', 8 | category: 'foo', 9 | type: 'string', 10 | default: 'my-default-class', 11 | description: 'Replace all classes with this one.', 12 | }, 13 | }; 14 | 15 | export const parsers = { 16 | astroExpressionParser: { 17 | ...original, 18 | parse(text, options) { 19 | let ast = original.parse(text, options); 20 | 21 | let nodes = [ast.program]; 22 | while (nodes.length) { 23 | let node = nodes.shift(); 24 | switch (node.type) { 25 | case 'Program': 26 | nodes.push(...node.body); 27 | break; 28 | case 'ExpressionStatement': 29 | nodes.push(node.expression); 30 | break; 31 | case 'JSXExpressionContainer': 32 | nodes.push(node.expression); 33 | break; 34 | case 'JSXFragment': 35 | nodes.push(...node.children); 36 | break; 37 | case 'JSXElement': 38 | nodes.push(node.openingElement); 39 | nodes.push(...node.children); 40 | break; 41 | case 'JSXOpeningElement': 42 | nodes.push(...node.attributes); 43 | break; 44 | case 'JSXAttribute': 45 | if (node.name && node.name.type === 'JSXIdentifier' && node.name.name === 'class') { 46 | node.value.value = `${options.customPluginClass}`; 47 | node.value.extra = { 48 | rawValue: node.value.value, 49 | raw: `"${node.value.value}"`, 50 | }; 51 | } 52 | break; 53 | } 54 | } 55 | 56 | return ast; 57 | }, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@astrojs/compiler/sync'; 2 | import type { Parser, Printer, SupportLanguage } from 'prettier'; 3 | import * as prettierPluginBabel from 'prettier/plugins/babel'; 4 | import { options } from './options'; 5 | import { print } from './printer'; 6 | import { embed } from './printer/embed'; 7 | import { getVisitorKeys } from './get-visitor-keys'; 8 | 9 | const babelParser = prettierPluginBabel.parsers['babel-ts']; 10 | 11 | // https://prettier.io/docs/en/plugins.html#languages 12 | export const languages: Partial[] = [ 13 | { 14 | name: 'astro', 15 | parsers: ['astro'], 16 | extensions: ['.astro'], 17 | vscodeLanguageIds: ['astro'], 18 | }, 19 | ]; 20 | 21 | // https://prettier.io/docs/en/plugins.html#parsers 22 | export const parsers: Record = { 23 | astro: { 24 | parse: (source) => parse(source, { position: true }).ast, 25 | astFormat: 'astro', 26 | locStart: (node) => node.position.start.offset, 27 | locEnd: (node) => node.position.end?.offset, 28 | }, 29 | astroExpressionParser: { 30 | ...babelParser, 31 | preprocess(text) { 32 | // note the trailing newline: if the statement ends in a // comment, 33 | // we can't add the closing bracket right afterwards 34 | return `<>{${text}\n}`; 35 | }, 36 | parse(text, opts) { 37 | const ast = babelParser.parse(text, opts); 38 | 39 | return { 40 | ...ast, 41 | program: ast.program.body[0].expression.children[0].expression, 42 | }; 43 | }, 44 | }, 45 | }; 46 | 47 | // https://prettier.io/docs/en/plugins.html#printers 48 | export const printers: Record = { 49 | astro: { 50 | print, 51 | embed, 52 | getVisitorKeys, 53 | }, 54 | }; 55 | 56 | const defaultOptions = { 57 | tabWidth: 2, 58 | }; 59 | 60 | export { defaultOptions, options }; 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Changelog 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Changelog 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out branch 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | 18 | - name: Install Tools & Dependencies 19 | uses: ./.github/actions/install 20 | 21 | - name: Create Release Pull Request or Publish to npm 22 | id: changesets 23 | uses: changesets/action@master 24 | with: 25 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 26 | publish: pnpm release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | 31 | # - name: Send a Discord notification if a publish happens 32 | # if: steps.changesets.outputs.published == 'true' 33 | # id: discord-notification 34 | # env: 35 | # DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 36 | # uses: Ilshidur/action-discord@0.3.2 37 | # with: 38 | # args: 'A new release just went out! [Release notes →]()' 39 | 40 | - name: push main branch to latest branch 41 | if: steps.changesets.outputs.published == 'true' 42 | id: git-push-latest 43 | # Note: this will fail if "latest" and "main" have different commit history, 44 | # which is a good thing! Also, don't push if in pre-release mode. 45 | run: '(test -f .changeset/pre.json && echo "prerelease: skip pushing to latest branch.") || git push origin main:latest' 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier-plugin-astro", 3 | "version": "0.14.2", 4 | "type": "module", 5 | "description": "A Prettier Plugin for formatting Astro files", 6 | "main": "dist/index.js", 7 | "files": [ 8 | "dist/**", 9 | "workers/*" 10 | ], 11 | "engines": { 12 | "node": "^14.15.0 || >=16.0.0" 13 | }, 14 | "packageManager": "pnpm@8.6.2", 15 | "homepage": "https://github.com/withastro/prettier-plugin-astro/", 16 | "issues": { 17 | "url": "https://github.com/withastro/prettier-plugin-astro/issues" 18 | }, 19 | "license": "MIT", 20 | "keywords": [ 21 | "prettier-plugin", 22 | "prettier", 23 | "astro", 24 | "formatter" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/withastro/prettier-plugin-astro.git" 29 | }, 30 | "scripts": { 31 | "build": "rollup -c", 32 | "dev": "rollup -c -w", 33 | "test": "vitest run", 34 | "test:watch": "vitest -w", 35 | "test:ui": "vitest --ui", 36 | "lint": "eslint .", 37 | "lint:fix": "pnpm lint --fix", 38 | "format": "biome format --write", 39 | "release": "pnpm build && changeset publish" 40 | }, 41 | "dependencies": { 42 | "@astrojs/compiler": "^2.9.1", 43 | "prettier": "^3.5.3", 44 | "sass-formatter": "^0.7.6" 45 | }, 46 | "devDependencies": { 47 | "@biomejs/biome": "1.8.1", 48 | "@changesets/cli": "^2.26.1", 49 | "@rollup/plugin-commonjs": "^25.0.0", 50 | "@rollup/plugin-typescript": "^11.1.1", 51 | "@types/node": "^20.2.5", 52 | "@vitest/ui": "^0.31.3", 53 | "eslint": "^9.8.0", 54 | "eslint-plugin-regexp": "^2.6.0", 55 | "eslint-plugin-prettier-doc": "^1.1.0", 56 | "rollup": "^3.23.0", 57 | "tslib": "^2.5.2", 58 | "typescript": "^5.5.4", 59 | "vite": "^4.4.3", 60 | "typescript-eslint": "^8.0.1", 61 | "vitest": "^0.31.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/fixtures/other/format-with-cursor-position/input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import astroLogo from "../assets/astro.svg"; 3 | import background from "../assets/background.svg"; 4 | --- 5 |
    6 | 7 |
    8 |
    9 | Astro Homepage 10 |

    To get started, open the
    src/pages
    directory in your 11 | project.

    12 | 17 |
    18 |
    19 | 20 | 21 |

    What's New in Astro 5.0?

    22 |

    From content layers to server islands, click to learn more about the new features and improvements in Astro 5.0

    23 |
    24 |
    25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nate Moore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --- 24 | 25 | Substantial portions of this code are adopted from https://github.com/sveltejs/prettier-plugin-svelte 26 | under the following license: 27 | 28 | MIT License 29 | 30 | Copyright (c) 2019 [Contributors](https://github.com/sveltejs/prettier-plugin-svelte/graphs/contributors) 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining a copy 33 | of this software and associated documentation files (the "Software"), to deal 34 | in the Software without restriction, including without limitation the rights 35 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 36 | copies of the Software, and to permit persons to whom the Software is 37 | furnished to do so, subject to the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be included in all 40 | copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 43 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 44 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 45 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 46 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 47 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 48 | SOFTWARE. 49 | -------------------------------------------------------------------------------- /test/fixtures/other/format-with-cursor-position/output.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import astroLogo from "../assets/astro.svg"; 3 | import background from "../assets/background.svg"; 4 | --- 5 | 6 |
    7 | 8 |
    9 |
    10 | Astro Homepage 18 |

    19 | To get started, open the
    src/pages
    directory in your 20 | project. 21 |

    22 | 34 |
    35 |
    36 | 37 | 42 |

    What's New in Astro 5.0?

    43 |

    44 | From content layers to server islands, click to learn more about the new 45 | features and improvements in Astro 5.0 46 |

    47 |
    48 |
    49 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-indented-sass/input.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    2 | 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/styles/with-indented-sass/output.astro: -------------------------------------------------------------------------------- 1 |
    lorem
    2 | 3 | 132 | -------------------------------------------------------------------------------- /test/test-utils.ts: -------------------------------------------------------------------------------- 1 | import prettier from 'prettier'; 2 | import { expect, it } from 'vitest'; 3 | 4 | const plugins = [new URL('../dist/index.js', import.meta.url).href]; 5 | 6 | /** 7 | * format the contents of an astro file 8 | */ 9 | export async function format( 10 | contents: string, 11 | options: Partial, 12 | ): Promise { 13 | try { 14 | return await prettier.formatWithCursor(contents, { 15 | parser: 'astro', 16 | plugins, 17 | cursorOffset: -1, 18 | ...options, 19 | }); 20 | } catch (e) { 21 | if (e instanceof Error) { 22 | throw e; 23 | } 24 | if (typeof e === 'string') { 25 | throw new Error(e); 26 | } 27 | } 28 | return { 29 | formatted: '', 30 | cursorOffset: -1, 31 | }; 32 | } 33 | 34 | async function markdownFormat( 35 | contents: string, 36 | options: Partial, 37 | ): Promise { 38 | try { 39 | return await prettier.formatWithCursor(contents, { 40 | parser: 'markdown', 41 | plugins, 42 | cursorOffset: -1, 43 | ...options, 44 | }); 45 | } catch (e) { 46 | if (e instanceof Error) { 47 | throw e; 48 | } 49 | if (typeof e === 'string') { 50 | throw new Error(e); 51 | } 52 | } 53 | return { 54 | formatted: '', 55 | cursorOffset: -1, 56 | }; 57 | } 58 | 59 | /** 60 | * Utility to get `[input, output]` files 61 | */ 62 | function getFiles(file: any, path: string, isMarkdown = false) { 63 | const ext = isMarkdown ? 'md' : 'astro'; 64 | let input: string = file[`/test/fixtures/${path}/input.${ext}`]; 65 | let output: string = file[`/test/fixtures/${path}/output.${ext}`]; 66 | // workaround: normalize end of lines to pass windows ci 67 | if (input) input = input.replace(/\r\n|\r/g, '\n'); 68 | if (output) output = output.replace(/\r\n|\r/g, '\n'); 69 | return { input, output }; 70 | } 71 | 72 | function getOptions(files: any, path: string) { 73 | if (files[`/test/fixtures/${path}/options.js`] !== undefined) { 74 | return files[`/test/fixtures/${path}/options.js`].default; 75 | } 76 | 77 | let opts: object; 78 | try { 79 | opts = JSON.parse(files[`/test/fixtures/${path}/options.json`]); 80 | } catch { 81 | opts = {}; 82 | } 83 | return opts; 84 | } 85 | 86 | /** 87 | * @param {string} name Test name. 88 | * @param {any} files Files from import.meta.glob. 89 | * @param {string} path Fixture path. 90 | * @param {boolean} isMarkdown For markdown files 91 | * @param {number} cursorOffset Specify where the cursor is. 92 | */ 93 | export function test( 94 | name: string, 95 | files: any, 96 | path: string, 97 | isMarkdown = false, 98 | cursorOffset = -1, 99 | ) { 100 | it(`${path}\n${name}`, async () => { 101 | const { input, output } = getFiles(files, path, isMarkdown); 102 | 103 | expect(input, 'Missing input file').to.not.be.undefined; 104 | expect(output, 'Missing output file').to.not.be.undefined; 105 | 106 | const formatFile = isMarkdown ? markdownFormat : format; 107 | 108 | const opts = { 109 | ...getOptions(files, path), 110 | cursorOffset, 111 | }; 112 | 113 | const firstPass = await formatFile(input, opts); 114 | expect(firstPass.formatted, 'Incorrect formatting').toBe(output); 115 | 116 | // test that our formatting is idempotent 117 | const secondPass = await formatFile(firstPass.formatted, opts); 118 | expect(firstPass.formatted === secondPass.formatted, 'Formatting is not idempotent').toBe(true); 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | import tseslint from 'typescript-eslint'; 5 | 6 | // plugins 7 | import regexpEslint from 'eslint-plugin-regexp'; 8 | const typescriptEslint = tseslint.plugin; 9 | 10 | // parsers 11 | const typescriptParser = tseslint.parser; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | 16 | export default [ 17 | // If ignores is used without any other keys in the configuration object, then the patterns act as global ignores. 18 | // ref: https://eslint.org/docs/latest/use/configure/configuration-files#globally-ignoring-files-with-ignores 19 | { 20 | ignores: ['**/.*', 'dist/**', 'test/**/fixtures/', '.github/', '.changeset/'], 21 | }, 22 | 23 | ...tseslint.configs.recommendedTypeChecked, 24 | ...tseslint.configs.stylisticTypeChecked, 25 | regexpEslint.configs['flat/recommended'], 26 | { 27 | languageOptions: { 28 | parser: typescriptParser, 29 | parserOptions: { 30 | project: ['./tsconfig.eslint.json'], 31 | tsconfigRootDir: __dirname, 32 | }, 33 | }, 34 | plugins: { 35 | '@typescript-eslint': typescriptEslint, 36 | regexp: regexpEslint, 37 | }, 38 | rules: { 39 | // These off/configured-differently-by-default rules fit well for us 40 | '@typescript-eslint/switch-exhaustiveness-check': 'error', 41 | '@typescript-eslint/no-unused-vars': [ 42 | 'error', 43 | { 44 | argsIgnorePattern: '^_', 45 | varsIgnorePattern: '^_', 46 | caughtErrorsIgnorePattern: '^_', 47 | ignoreRestSiblings: true, 48 | }, 49 | ], 50 | '@typescript-eslint/no-shadow': 'error', 51 | 'no-console': 'warn', 52 | 53 | // Todo: do we want these? 54 | '@typescript-eslint/array-type': 'off', 55 | '@typescript-eslint/ban-ts-comment': 'off', 56 | '@typescript-eslint/class-literal-property-style': 'off', 57 | '@typescript-eslint/consistent-indexed-object-style': 'off', 58 | '@typescript-eslint/consistent-type-definitions': 'off', 59 | '@typescript-eslint/dot-notation': 'off', 60 | '@typescript-eslint/no-base-to-string': 'off', 61 | '@typescript-eslint/no-empty-function': 'off', 62 | '@typescript-eslint/no-floating-promises': 'off', 63 | '@typescript-eslint/no-misused-promises': 'off', 64 | '@typescript-eslint/no-redundant-type-constituents': 'off', 65 | '@typescript-eslint/no-this-alias': 'off', 66 | '@typescript-eslint/no-unsafe-argument': 'off', 67 | '@typescript-eslint/no-unsafe-assignment': 'off', 68 | '@typescript-eslint/no-unsafe-call': 'off', 69 | '@typescript-eslint/no-unsafe-member-access': 'off', 70 | '@typescript-eslint/no-unused-expressions': 'off', 71 | '@typescript-eslint/only-throw-error': 'off', 72 | '@typescript-eslint/no-unsafe-return': 'off', 73 | '@typescript-eslint/no-unnecessary-type-assertion': 'off', 74 | '@typescript-eslint/prefer-nullish-coalescing': 'off', 75 | '@typescript-eslint/prefer-optional-chain': 'off', 76 | '@typescript-eslint/prefer-promise-reject-errors': 'off', 77 | '@typescript-eslint/prefer-string-starts-ends-with': 'off', 78 | '@typescript-eslint/require-await': 'off', 79 | '@typescript-eslint/restrict-plus-operands': 'off', 80 | '@typescript-eslint/restrict-template-expressions': 'off', 81 | '@typescript-eslint/sort-type-constituents': 'off', 82 | '@typescript-eslint/unbound-method': 'off', 83 | '@typescript-eslint/no-explicit-any': 'off', 84 | 85 | // Enforce separate type imports for type-only imports to avoid bundling unneeded code 86 | '@typescript-eslint/consistent-type-imports': [ 87 | 'error', 88 | { 89 | prefer: 'type-imports', 90 | fixStyle: 'separate-type-imports', 91 | disallowTypeAnnotations: false, 92 | }, 93 | ], 94 | 95 | // These rules enabled by the preset configs don't work well for us 96 | '@typescript-eslint/await-thenable': 'off', 97 | 'prefer-const': 'off', 98 | 99 | // In some cases, using explicit letter-casing is more performant than the `i` flag 100 | 'regexp/use-ignore-case': 'off', 101 | 'regexp/prefer-regexp-exec': 'warn', 102 | 'regexp/prefer-regexp-test': 'warn', 103 | }, 104 | }, 105 | ]; 106 | -------------------------------------------------------------------------------- /test/tests/other.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '../test-utils'; 2 | 3 | const files = { 4 | ...import.meta.glob('/test/fixtures/other/*/*', { 5 | eager: true, 6 | as: 'raw', 7 | }), 8 | ...import.meta.glob('/test/fixtures/other/*/*.js', { 9 | eager: true, 10 | }), 11 | }; 12 | 13 | test('Can format an Astro file with frontmatter', files, 'other/frontmatter'); 14 | 15 | test('Can format an Astro file with embedded JSX expressions', files, 'other/embedded-expr'); 16 | 17 | test( 18 | 'Options are passed to other Prettier Plugins when parsing embedded JSX expressions', 19 | files, 20 | 'other/embedded-expr-options', 21 | ); 22 | 23 | test( 24 | 'Can format an Astro file with a `` + embedded JSX expressions', 25 | files, 26 | 'other/doctype-with-embedded-expr', 27 | ); 28 | 29 | // // note(drew): this should be fixed in new Parser. And as this is an HTML4 / deprecated / extreme edge case, probably fine to ignore? 30 | // test.failing('Can format an Astro file with `` with extraneous attributes', Prettier, 'doctype-with-extra-attributes'); 31 | 32 | test('Can format an Astro file with fragments', files, 'other/fragment'); 33 | 34 | test( 35 | 'Can format an Astro file with a JSX expression in an attribute', 36 | files, 37 | 'other/attribute-with-embedded-expr', 38 | ); 39 | 40 | test( 41 | 'Can format an Astro file with a JSX expression and an HTML Comment', 42 | files, 43 | 'other/expr-and-html-comment', 44 | ); 45 | 46 | // test.failing('an Astro file with an invalidly unclosed tag is still formatted', Prettier, 'unclosed-tag'); 47 | 48 | test( 49 | 'Can format an Astro file with components that are the uppercase version of html elements', 50 | files, 51 | 'other/preserve-tag-case', 52 | ); 53 | 54 | test('Autocloses open tags.', files, 'other/autocloses-open-tags'); 55 | 56 | test('Can format an Astro file with a script tag inside it', files, 'other/with-script'); 57 | 58 | test('Can format an Astro file with scripts in different languages', files, 'other/script-types'); 59 | 60 | test( 61 | 'Can format an Astro file with a HTML style prettier ignore comment: https://prettier.io/docs/en/ignore.html', 62 | files, 63 | 'other/prettier-ignore-html', 64 | ); 65 | 66 | test( 67 | 'Can format an Astro file with a JS style prettier ignore comment: https://prettier.io/docs/en/ignore.html', 68 | files, 69 | 'other/prettier-ignore-js', 70 | ); 71 | 72 | // // note(drew): this _may_ be covered under the 'prettier-ignore-html' test. But if any bugs arise, let’s add more tests! 73 | // test.todo("properly follow prettier' advice on formatting comments"); 74 | 75 | // // note(drew): I think this is a function of Astro’s parser, not Prettier. We’ll have to handle helpful error messages there! 76 | // test.todo('test whether invalid files provide helpful support messages / still try to be parsed by prettier?'); 77 | 78 | test('Format spread operator', files, 'other/spread-operator'); 79 | 80 | test('Can format nested comment', files, 'other/nested-comment'); 81 | 82 | test('Format binary expressions', files, 'other/binary-expression'); 83 | 84 | test('Format self-closing tags without additional content', files, 'other/clean-self-closing'); 85 | 86 | test('Format directives', files, 'other/directive'); 87 | 88 | test('Format slots', files, 'other/slots'); 89 | 90 | test( 91 | 'Can format expressions with shorthands props in them', 92 | files, 93 | 'other/shorthand-in-expression', 94 | ); 95 | 96 | test( 97 | 'Can format expression with inline comments in it', 98 | files, 99 | 'other/expression-with-inline-comments', 100 | ); 101 | 102 | test('Can format JSX comments properly', files, 'other/jsx-comments'); 103 | 104 | test( 105 | 'Can format expression with TypeScript in them properly', 106 | files, 107 | 'other/typescript-expression', 108 | ); 109 | 110 | test( 111 | 'Can format expressions with characters not compatible with JSX', 112 | files, 113 | 'other/non-jsx-compatible-characters', 114 | ); 115 | 116 | test( 117 | 'Can format expressions inside inline tags without adding a newline', 118 | files, 119 | 'other/expression-in-inline-tag', 120 | ); 121 | 122 | test( 123 | 'Can format expressions who have multi-roots returns', 124 | files, 125 | 'other/expression-multiple-roots', 126 | ); 127 | 128 | test( 129 | 'Can format expressions who have multi-roots returns - extreme cases', 130 | files, 131 | 'other/expression-multiple-roots-stress', 132 | ); 133 | 134 | test('Can ignore self-closing elements', files, 'other/ignore-self-close'); 135 | 136 | test('can format spread attributes', files, 'other/spread-attributes'); 137 | 138 | test('can format with cursor position', files, 'other/format-with-cursor-position', false, 313); 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Prettier](https://prettier.io/) Plugin for [Astro](https://astro.build/) 2 | 3 | Official Prettier plugin adding support for formatting `.astro` files. 4 | 5 | ## Installation 6 | 7 | First install Prettier and the plugin: 8 | 9 | ```shell 10 | npm i --save-dev prettier prettier-plugin-astro 11 | ``` 12 | 13 |
    14 | Installation with pnpm 15 | 16 | ```shell 17 | pnpm add --save-dev prettier prettier-plugin-astro 18 | ``` 19 |
    20 | 21 | Then add the plugin to your Prettier configuration: 22 | 23 | ```js 24 | // .prettierrc.mjs 25 | /** @type {import("prettier").Config} */ 26 | export default { 27 | plugins: ['prettier-plugin-astro'], 28 | }; 29 | ``` 30 | 31 | ### Recommended configuration 32 | 33 | For optimal compatibility with the different package managers and Prettier plugins, we recommend manually specifying the parser to use for Astro files in your Prettier config as shown in the example below: 34 | 35 | ```js 36 | // .prettierrc.mjs 37 | /** @type {import("prettier").Config} */ 38 | export default { 39 | plugins: ['prettier-plugin-astro'], 40 | overrides: [ 41 | { 42 | files: '*.astro', 43 | options: { 44 | parser: 'astro', 45 | }, 46 | }, 47 | ], 48 | }; 49 | ``` 50 | 51 | To customize formatting behavior, see the [Configuration](#configuration) section below. 52 | 53 | ## Formatting with the VS Code Prettier extension directly 54 | 55 | > **Note** > [The Astro VS Code extension](https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode) uses Prettier and this plugin (`prettier-plugin-astro`) to format your code. You will only need to install this plugin separately for formatting if: 56 | 57 | - You are not using Astro's VS Code extension. 58 | - You want to use features of the Prettier extension that not supported by Astro's own VS Code extension, such as the toolbar panel showing Prettier's status. 59 | 60 | Install the [VS Code Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and add the following settings to your VS Code configuration: 61 | 62 | ```json 63 | { 64 | "prettier.documentSelectors": ["**/*.astro"], 65 | "[astro]": { 66 | "editor.defaultFormatter": "esbenp.prettier-vscode" 67 | } 68 | } 69 | ``` 70 | 71 | The settings above ensure that VS Code is aware that Prettier can be used for Astro files, and sets Prettier as the default formatter for Astro files. 72 | 73 | ### Reporting issues 74 | 75 | When submitting issues about formatting your `.astro` files in VS Code, please specify which extension you are using to format your files: Astro's own extension or the Prettier extension. 76 | 77 | ## Configuration 78 | 79 | Most [options from Prettier](https://prettier.io/docs/en/options.html) will work with the plugin and can be set in a [configuration file](https://prettier.io/docs/en/configuration.html) or through [CLI flags](https://prettier.io/docs/en/cli.html). 80 | 81 | ### Astro Allow Shorthand 82 | 83 | Set if attributes with the same name as their expression should be formatted to the short form automatically (for example, if enabled `` will become simply ``) 84 | 85 | | Default | CLI Override | API Override | 86 | | ------- | -------------------------------- | ----------------------------- | 87 | | `false` | `--astro-allow-shorthand ` | `astroAllowShorthand: ` | 88 | 89 | ### Astro Skip Frontmatter 90 | 91 | If you are using another tool to format your JavaScript code, like Biome for example, it is possible to skip formatting the frontmatter. 92 | 93 | | Default | CLI Override | API Override | 94 | | ------- | --------------------------------- | ------------------------------ | 95 | | `false` | `--astro-skip-frontmatter ` | `astroSkipFrontmatter: ` | 96 | 97 | ### Example `.prettierrc.cjs` 98 | 99 | ```js 100 | { 101 | astroAllowShorthand: false; 102 | astroSkipFrontmatter: false; 103 | } 104 | ``` 105 | 106 | ## Contributing 107 | 108 | Pull requests of any size and any skill level are welcome, no contribution is too small. Changes to the Astro Prettier Plugin are subject to [Astro Governance](https://github.com/withastro/.github/blob/main/GOVERNANCE.md) and should adhere to the [Astro Style Guide](https://github.com/withastro/astro/blob/main/STYLE_GUIDE.md). 109 | 110 | See [CONTRIBUTING.md](./CONTRIBUTING.md) for instructions on how to set up your development environment. 111 | 112 | ## Sponsors 113 | 114 | Astro is free, open source software made possible by these wonderful sponsors. 115 | 116 | [❤️ Sponsor Astro! ❤️](https://github.com/withastro/.github/blob/main/FUNDING.md) 117 | 118 |

    119 | 120 | sponsors 121 | 122 |

    123 | -------------------------------------------------------------------------------- /test/tests/options.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '../test-utils'; 2 | 3 | const files = import.meta.glob('/test/fixtures/options/*/*', { 4 | eager: true, 5 | as: 'raw', 6 | }); 7 | 8 | // https://prettier.io/docs/en/options.html#print-width 9 | // TODO: MAYBE NOT WORKING? 10 | test( 11 | 'Can format an Astro file with prettier "printWidth" option', 12 | files, 13 | 'options/option-print-width', 14 | ); 15 | 16 | // https://prettier.io/docs/en/options.html#tab-width 17 | test('Can format an Astro file with prettier "tabWidth" option', files, 'options/option-tab-width'); 18 | 19 | // https://prettier.io/docs/en/options.html#tabs 20 | test( 21 | 'Can format an Astro file with prettier "useTabs: true" option', 22 | files, 23 | 'options/option-use-tabs-true', 24 | ); 25 | 26 | // https://prettier.io/docs/en/options.html#tabs 27 | test( 28 | 'Can format an Astro file with prettier "useTabs: false" option', 29 | files, 30 | 'options/option-use-tabs-false', 31 | ); 32 | 33 | // https://prettier.io/docs/en/options.html#semicolons 34 | test( 35 | 'Can format an Astro file with prettier "semi: true" option', 36 | files, 37 | 'options/option-semicolon-true', 38 | ); 39 | 40 | // https://prettier.io/docs/en/options.html#semicolons 41 | test( 42 | 'Can format an Astro file with prettier "semi: false" option', 43 | files, 44 | 'options/option-semicolon-false', 45 | ); 46 | 47 | // https://prettier.io/docs/en/options.html#quotes 48 | test( 49 | 'Can format an Astro file with prettier "singleQuote: false" option', 50 | files, 51 | 'options/option-single-quote-false', 52 | ); 53 | 54 | // https://prettier.io/docs/en/options.html#quotes 55 | test( 56 | 'Can format an Astro file with prettier "singleQuote: true" option', 57 | files, 58 | 'options/option-single-quote-true', 59 | ); 60 | 61 | // https://prettier.io/docs/en/options.html#quote-props 62 | test( 63 | 'Can format an Astro file with prettier "quoteProps: as-needed" option', 64 | files, 65 | 'options/option-quote-props-as-needed', 66 | ); 67 | 68 | // https://prettier.io/docs/en/options.html#quote-props 69 | test( 70 | 'Can format an Astro file with prettier "quoteProps: consistent" option', 71 | files, 72 | 'options/option-quote-props-consistent', 73 | ); 74 | 75 | // https://prettier.io/docs/en/options.html#quote-props 76 | test( 77 | 'Can format an Astro file with prettier "quoteProps: preserve" option', 78 | files, 79 | 'options/option-quote-props-preserve', 80 | ); 81 | 82 | // https://prettier.io/docs/en/options.html#jsx-quotes 83 | test( 84 | 'Can format an Astro file with prettier "jsxSingleQuote: false" option', 85 | files, 86 | 'options/option-jsx-single-quote-false', 87 | ); 88 | 89 | // https://prettier.io/docs/en/options.html#jsx-quotes 90 | test( 91 | 'Can format an Astro file with prettier "jsxSingleQuote: true" option', 92 | files, 93 | 'options/option-jsx-single-quote-true', 94 | ); 95 | 96 | // https://prettier.io/docs/en/options.html#trailing-commas 97 | test( 98 | 'Can format an Astro file with prettier "trailingComma: es5" option', 99 | files, 100 | 'options/option-trailing-comma-es5', 101 | ); 102 | 103 | // https://prettier.io/docs/en/options.html#trailing-commas 104 | test( 105 | 'Can format an Astro file with prettier "trailingComma: none" option', 106 | files, 107 | 'options/option-trailing-comma-none', 108 | ); 109 | 110 | // https://prettier.io/docs/en/options.html#bracket-spacing 111 | test( 112 | 'Can format an Astro file with prettier "bracketSpacing: true" option', 113 | files, 114 | 'options/option-bracket-spacing-true', 115 | ); 116 | 117 | // https://prettier.io/docs/en/options.html#bracket-spacing 118 | test( 119 | 'Can format an Astro file with prettier "bracketSpacing: false" option', 120 | files, 121 | 'options/option-bracket-spacing-false', 122 | ); 123 | 124 | // https://prettier.io/docs/en/options.html#bracket-line 125 | test( 126 | 'Can format an Astro file with prettier "bracketSameLine: false" option', 127 | files, 128 | 'options/option-bracket-same-line-false', 129 | ); 130 | 131 | // https://prettier.io/docs/en/options.html#bracket-line 132 | test( 133 | 'Can format an Astro file with prettier "bracketSameLine: true" option', 134 | files, 135 | 'options/option-bracket-same-line-true', 136 | ); 137 | 138 | // https://prettier.io/docs/en/options.html#arrow-function-parentheses 139 | test( 140 | 'Can format an Astro file with prettier "arrowParens: always" option', 141 | files, 142 | 'options/option-arrow-parens-always', 143 | ); 144 | 145 | // https://prettier.io/docs/en/options.html#arrow-function-parentheses 146 | test( 147 | 'Can format an Astro file with prettier "arrowParens: avoid" option', 148 | files, 149 | 'options/option-arrow-parens-avoid', 150 | ); 151 | 152 | // https://prettier.io/docs/en/options.html#prose-wrap 153 | test( 154 | 'Can format an Astro file with prettier "proseWrap: preserve" option', 155 | files, 156 | 'options/option-prose-wrap-preserve', 157 | true, 158 | ); 159 | 160 | // https://prettier.io/docs/en/options.html#prose-wrap 161 | test( 162 | 'Can format an Astro file with prettier "proseWrap: always" option', 163 | files, 164 | 'options/option-prose-wrap-always', 165 | true, 166 | ); 167 | 168 | // https://prettier.io/docs/en/options.html#prose-wrap 169 | test( 170 | 'Can format an Astro file with prettier "proseWrap: never" option', 171 | files, 172 | 'options/option-prose-wrap-never', 173 | true, 174 | ); 175 | 176 | // // https://prettier.io/docs/en/options.html#html-whitespace-sensitivity 177 | // test('Can format an Astro file with prettier "htmlWhitespaceSensitivity: css" option', 'option-html-whitespace-sensitivity-css'); 178 | 179 | // // https://prettier.io/docs/en/options.html#html-whitespace-sensitivity 180 | // test('Can format an Astro file with prettier "htmlWhitespaceSensitivity: strict" option', 'option-html-whitespace-sensitivity-strict'); 181 | 182 | // https://prettier.io/docs/en/options.html#html-whitespace-sensitivity 183 | test( 184 | 'Can format an Astro file with prettier "htmlWhitespaceSensitivity: ignore" option', 185 | files, 186 | 'options/option-html-whitespace-sensitivity-ignore', 187 | ); 188 | test( 189 | 'Can format components with prettier "htmlWhitespaceSensitivity: ignore" option', 190 | files, 191 | 'options/option-html-whitespace-sensitivity-ignore-component', 192 | ); 193 | 194 | // https://prettier.io/docs/en/options.html#single-attribute-per-line 195 | test( 196 | 'Can format an Astro file with prettier "singleAttributePerLine: true" option', 197 | files, 198 | 'options/single-attribute-per-line-true', 199 | ); 200 | 201 | // https://prettier.io/docs/en/options.html#single-attribute-per-line 202 | test( 203 | 'Can format an Astro file with prettier "singleAttributePerLine: false" option', 204 | files, 205 | 'options/single-attribute-per-line-false', 206 | ); 207 | 208 | // https://prettier.io/docs/options.html#bracket-line 209 | // https://prettier.io/docs/en/options.html#html-whitespace-sensitivity 210 | test( 211 | 'Can format an Astro file with prettier "bracketSameLine: true, htmlWhitespaceSensitivity: ignore" options', 212 | files, 213 | 'options/option-bracket-same-line-html-true-whitespace-sensitivity-ignore', 214 | ); 215 | 216 | // // astro option: astroSortOrder 217 | // test('Can format an Astro file with prettier "astroSortOrder: markup | styles" option', 'option-astro-sort-order-markup-styles'); 218 | 219 | // // astro option: astroSortOrder 220 | // test('Can format an Astro file with prettier "astroSortOrder: styles | markup" option', 'option-astro-sort-order-styles-markup'); 221 | 222 | // // astro option: astroAllowShorthand 223 | // test('Can format an Astro file with prettier "astroAllowShorthand: true" option', 'option-astro-allow-shorthand-true'); 224 | 225 | // // astro option: astroAllowShorthand 226 | // test('Can format an Astro file with prettier "astroAllowShorthand: false" option', 'option-astro-allow-shorthand-false'); 227 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # prettier-plugin-astro 2 | 3 | ## 0.14.2 4 | 5 | ### Patch Changes 6 | 7 | - de79d60: Fixes a bug with formatting empty elements with bracketSameLine: true and htmlWhitespaceSensitivity: ignore 8 | - 49456da: Handle potential undefined position.end in locEnd, and add a getVisitorKeys method to avoid unnecessary traversal. 9 | - c380d32: Format custom elements & components as block elements with htmlWhitespaceSensitivity: ignore 10 | 11 | ## 0.14.1 12 | 13 | ### Patch Changes 14 | 15 | - 7282bcb: Fixes an issue where style and script tags would sometimes get moved to other tags 16 | 17 | ## 0.14.0 18 | 19 | ### Minor Changes 20 | 21 | - bb756df: Adds a new option called `astroSkipFrontmatter` to disable formatting the frontmatter. This can be useful when using other tools to format the frontmatter, such as Biome or dprint. 22 | 23 | ## 0.13.0 24 | 25 | ### Minor Changes 26 | 27 | - e97406a: Fix plugin sometimes including significant whitespace inside components, fragments and custom elements 28 | 29 | ## 0.12.3 30 | 31 | ### Patch Changes 32 | 33 | - e75f9c7: Fix `
    ` tags sometimes causing additional spaces to appear 34 | - b4b0918: Fix not being able to format expressions with more than 2 roots 35 | 36 | ## 0.12.2 37 | 38 | ### Patch Changes 39 | 40 | - 11b0dc7: Fix attributes using optional chaining not formatting correctly 41 | 42 | ## 0.12.1 43 | 44 | ### Patch Changes 45 | 46 | - 0188f04: Fix attributes with multiple invalid JSX characters in their key inside expressions causing the plugin to throw an error 47 | 48 | ## 0.12.0 49 | 50 | ### Minor Changes 51 | 52 | - fa1a6e3: Do not delete line breaks and indentation of lines in class attribute 53 | 54 | ### Patch Changes 55 | 56 | - b806845: Format doctype as lowercase to match Prettier 3.0 57 | 58 | ## 0.11.1 59 | 60 | ### Patch Changes 61 | 62 | - 62fe714: removes pnpm from engines 63 | 64 | ## 0.11.0 65 | 66 | ### Minor Changes 67 | 68 | - 94ed904: Migrated the plugin to Prettier 3's new APIs. It's unfortunately not possible for a plugin to support both version 2 and 3 of Prettier. As such, from this version on, only Prettier 3 is supported. 69 | 70 | ## 0.10.0 71 | 72 | ### Minor Changes 73 | 74 | - af9324e: Use the sync entrypoint of the Astro compiler instead of `synckit`, improving performance and reducing the dependency count of the plugin 75 | 76 | ## 0.9.1 77 | 78 | ### Patch Changes 79 | 80 | - 97c4b07: fix: prevent parsing empty script tags 81 | 82 | ## 0.9.0 83 | 84 | ### Minor Changes 85 | 86 | - abecea0: Use the babel-ts parser to parse the frontmatter. This parser was already used for expressions inside the template, so the experience should now be more homogenous all throughout the file. 87 | - d2a2c26: Add support for formatting script tags containing JSON, Markdown and other content 88 | 89 | ## 0.8.1 90 | 91 | ### Patch Changes 92 | 93 | - 9cb4c4f: Add compatibility for other plugins parsing top-level returns in Astro frontmatter 94 | - 88b0d84: Correctly pass options to embedded parsers 95 | - e5cf99d: Fix style tags using Stylus being truncated under certain circumstances 96 | 97 | ## 0.8.0 98 | 99 | ### Minor Changes 100 | 101 | - c724082: Add support for LESS style tags, fixed crash on style tags with unknown languages 102 | - 18cd321: Add support for formatting spread attributes 103 | 104 | ## 0.7.2 105 | 106 | ### Patch Changes 107 | 108 | - a97750b: Fix packaging error causing the plugin to only be installable using pnpm 109 | 110 | ## 0.7.1 111 | 112 | ### Patch Changes 113 | 114 | - 9fa788f: Upgrade `@astrojs/compiler` 115 | - a3ff2ef: Fix inline tags not hugging the end of their content if the last child was a tag 116 | 117 | ## 0.7.0 118 | 119 | ### Minor Changes 120 | 121 | - 485fb91: Fixed custom-elements being allowed to self close despite the HTML spec saying otherwise 122 | 123 | ### Patch Changes 124 | 125 | - b99b461: Add support for formatting expressions with multiple root elements 126 | - ca48060: Add support for prettier-ignore comments 127 | 128 | ## 0.6.0 129 | 130 | ### Minor Changes 131 | 132 | - 699e02c: Allow elements with set:\* directives to self-close 133 | 134 | ### Patch Changes 135 | 136 | - 163ffec: Fix `jsxSingleQuote` not considering if there was any incompatible characters inside the attribute value 137 | - 17af6ef: Fix style tags getting moved inside body tags 138 | Fix fragments with expressions inside being moved to before the expressions in certain cases 139 | 140 | ## 0.5.5 141 | 142 | ### Patch Changes 143 | 144 | - fe68b94: Fix missing newline after attributes on inline elements when using singleAttributePerLine 145 | - 96e2b28: Fix expressions not hugging the end of the tag in cases where they should 146 | - 4e6fde8: Fix newlines being added to style tags even if they were empty 147 | 148 | ## 0.5.4 149 | 150 | ### Patch Changes 151 | 152 | - 4115a8e: Support formatting expressions with elements with attributes not compatible with JSX 153 | 154 | ## 0.5.3 155 | 156 | ### Patch Changes 157 | 158 | - 1da195e: Update to latest version of the compiler 159 | 160 | ## 0.5.2 161 | 162 | ### Patch Changes 163 | 164 | - 13810b7: End tag hugs text nodes not ending in whitespace 165 | - 5b2177e: Correctly break self-closing tags 166 | 167 | ## 0.5.1 168 | 169 | ### Patch Changes 170 | 171 | - d4afe5b: Fix TypeScript not working inside expressions 172 | 173 | ## 0.5.0 174 | 175 | ### Minor Changes 176 | 177 | - 2bc2f38: Properly format multi-lines expressions, fixes expression with leading comments not being formatted properly 178 | 179 | ## 0.4.1 180 | 181 | ### Patch Changes 182 | 183 | - babd8c3: Properly trim the class attribute on HTML elements 184 | 185 | ## 0.4.0 186 | 187 | ### Minor Changes 188 | 189 | - 2d78f06: Fix loading workers not working when parser is used from an external module 190 | 191 | ### Patch Changes 192 | 193 | - 285360e: Fixed error when trying to format custom elements 194 | - 1b6622e: Properly handle errors inside style tags 195 | 196 | ## 0.3.0 197 | 198 | ### Minor Changes 199 | 200 | - 6ebc4d4: Update error handling to give better error messages when we fail to parse an expression, fixed shorthands props not working inside expressions 201 | - bfe22e3: Improve error handling for frontmatter and script tags 202 | 203 | ## 0.2.0 204 | 205 | ### Minor Changes 206 | 207 | - f9aa07e: Remove support for the Markdown component 208 | 209 | ## 0.1.3 210 | 211 | ### Patch Changes 212 | 213 | - 4c8d8dc: Use Prettier option: 'singleAttributePerLine' 214 | 215 | ## 0.1.2 216 | 217 | ### Patch Changes 218 | 219 | - 410dfb4: Use prettier option 'semi' for jsx semi colons 220 | - 410dfb4: HTML attribute quotes now depend on `jsxSingleQuote` option 221 | - 410dfb4: Self-close slots 222 | - 410dfb4: Remove dedup utility 223 | 224 | ## 0.1.1 225 | 226 | ### Patch Changes 227 | 228 | - d8b666b: fix for jsx empty expression 229 | 230 | ## 0.1.0 231 | 232 | ### Minor Changes 233 | 234 | - 054d055: Migrate to new compiler. This took months of work, and some things might be broken for the time being. However, it should be a major improvement in most cases over the previous version. We hope you'll like it! 235 | 236 | ## 0.0.12 237 | 238 | ### Patch Changes 239 | 240 | - ab70152: Fix a bug in "allow shorthand" option (#87) 241 | - 08c5fc6: Bump ava to v4.0.1 242 | - 4eebc6c: Fix comment error when nested 243 | - e28d1cf: Format html using only the plugin itself 244 | 245 | ## 0.0.11 246 | 247 | ### Patch Changes 248 | 249 | - 4a7d602: Bump ava 250 | - d07451c: Bump @astrojs/parser from 0.20.2 to 0.20.3 251 | - 2c164f7: Format spread operator 252 | - f7cf7c1: Format markdown component content 253 | 254 | ## 0.0.10 255 | 256 | ### Patch Changes 257 | 258 | - a7ca7bc: Format nested style tag content 259 | - 2995e7c: Add Astro option: 'allow shorthand' 260 | - 7ec632f: Typescript refactor 261 | - 85f7f93: Add support for prettier options 262 | 263 | ## 0.0.9 264 | 265 | ### Patch Changes 266 | 267 | - 8820423: Fix test macro 'PrettierMarkdown' 268 | - a30ddcd: Bump @astrojs/parser from 0.15.0 to 0.20.2 269 | - 695fc07: Add formatting for components 270 | - 1bf9f7c: Support arbitrary attributes in style tags 271 | - 395b3bd: Add basic support for indented sass 272 | - 672afef: Add new line at the end of the file 273 | - 20a298e: Add support for .sass formatting 274 | - 915a6e2: Add support for prettier-ignore comments 275 | 276 | ## 0.0.8 277 | 278 | ### Patch Changes 279 | 280 | - 87c3564: Bump mixme from 0.5.1 to 0.5.4 (#12) 281 | - 87c3564: Bump @changesets/cli from 2.16.0 to 2.17.0 (#15) 282 | - 87c3564: Bump eslint-plugin-ava from 12.0.0 to 13.0.0 (#13) 283 | - 87c3564: Preserve tag case (#19) 284 | 285 | ## 0.0.7 286 | 287 | ### Patch Changes 288 | 289 | - 80df170: Upgrade prettier to ^2.4.1 290 | 291 | ## 0.0.6 292 | 293 | ### Patch Changes 294 | 295 | - Updated dependencies [47ac2cc] 296 | - @astrojs/parser@0.15.0 297 | 298 | ## 0.0.5 299 | 300 | ### Patch Changes 301 | 302 | - ff7ec2f: Add @types/prettier for type support 303 | 304 | ## 0.0.4 305 | 306 | ### Patch Changes 307 | 308 | - Updated dependencies [ab2972b] 309 | - @astrojs/parser@0.13.3 310 | 311 | ## 0.0.3 312 | 313 | ### Patch Changes 314 | 315 | - Updated dependencies [9cdada0] 316 | - astro-parser@0.11.0 317 | 318 | ## 0.0.2 319 | 320 | ### Patch Changes 321 | 322 | - Updated dependencies [b3886c2] 323 | - astro-parser@0.1.0 324 | -------------------------------------------------------------------------------- /src/printer/index.ts: -------------------------------------------------------------------------------- 1 | import { type Doc } from 'prettier'; 2 | import { selfClosingTags } from './elements'; 3 | import { type TextNode } from './nodes'; 4 | import { 5 | canOmitSoftlineBeforeClosingTag, 6 | endsWithLinebreak, 7 | getNextNode, 8 | getPreferredQuote, 9 | getUnencodedText, 10 | hasSetDirectives, 11 | isEmptyTextNode, 12 | isIgnoreDirective, 13 | isInlineElement, 14 | isPreTagContent, 15 | isTagLikeNode, 16 | isTextNode, 17 | isTextNodeEndingWithWhitespace, 18 | isTextNodeStartingWithLinebreak, 19 | isTextNodeStartingWithWhitespace, 20 | printClassNames, 21 | printRaw, 22 | shouldHugEnd, 23 | shouldHugStart, 24 | startsWithLinebreak, 25 | trimTextNodeLeft, 26 | trimTextNodeRight, 27 | type AstPath, 28 | type ParserOptions, 29 | type printFn, 30 | } from './utils'; 31 | 32 | import _doc from 'prettier/doc'; 33 | const { 34 | builders: { 35 | breakParent, 36 | dedent, 37 | fill, 38 | group, 39 | indent, 40 | join, 41 | line, 42 | softline, 43 | hardline, 44 | literalline, 45 | }, 46 | utils: { stripTrailingHardline }, 47 | } = _doc; 48 | 49 | let ignoreNext = false; 50 | 51 | // https://prettier.io/docs/en/plugins.html#print 52 | // eslint-disable-next-line @typescript-eslint/no-shadow 53 | export function print(path: AstPath, opts: ParserOptions, print: printFn): Doc { 54 | const node = path.node; 55 | 56 | // 1. handle special node types 57 | if (!node) { 58 | return ''; 59 | } 60 | 61 | if (ignoreNext && !isEmptyTextNode(node)) { 62 | ignoreNext = false; 63 | return [ 64 | opts.originalText 65 | .slice(opts.locStart(node), opts.locEnd(node)) 66 | .split('\n') 67 | .map((lineContent, i) => (i == 0 ? [lineContent] : [literalline, lineContent])) 68 | .flat(), 69 | ]; 70 | } 71 | 72 | if (typeof node === 'string') { 73 | return node; 74 | } 75 | 76 | // 2. handle printing 77 | switch (node.type) { 78 | case 'root': { 79 | return [stripTrailingHardline(path.map(print, 'children')), hardline]; 80 | } 81 | 82 | case 'text': { 83 | const rawText = getUnencodedText(node); 84 | 85 | // TODO: TEST PRE TAGS 86 | // if (isPreTagContent(path)) { 87 | // if (path.getParentNode()?.type === 'Attribute') { 88 | // // Direct child of attribute value -> add literallines at end of lines 89 | // // so that other things don't break in unexpected places 90 | // return replaceEndOfLineWith(rawText, literalline); 91 | // } 92 | // return rawText; 93 | // } 94 | 95 | if (isEmptyTextNode(node)) { 96 | const hasWhiteSpace = rawText.trim().length < getUnencodedText(node).length; 97 | const hasOneOrMoreNewlines = getUnencodedText(node).includes('\n'); 98 | const hasTwoOrMoreNewlines = /\n\s*\n\r?/.test(getUnencodedText(node)); 99 | if (hasTwoOrMoreNewlines) { 100 | return [hardline, hardline]; 101 | } 102 | if (hasOneOrMoreNewlines) { 103 | return hardline; 104 | } 105 | if (hasWhiteSpace) { 106 | return line; 107 | } 108 | return ''; 109 | } 110 | 111 | /** 112 | * For non-empty text nodes each sequence of non-whitespace characters (effectively, 113 | * each "word") is joined by a single `line`, which will be rendered as a single space 114 | * until this node's current line is out of room, at which `fill` will break at the 115 | * most convenient instance of `line`. 116 | */ 117 | return fill(splitTextToDocs(node)); 118 | } 119 | 120 | case 'component': 121 | case 'fragment': 122 | case 'custom-element': 123 | case 'element': { 124 | let isEmpty: boolean; 125 | if (!node.children) { 126 | isEmpty = true; 127 | } else { 128 | isEmpty = node.children.every((child) => isEmptyTextNode(child)); 129 | } 130 | 131 | /** 132 | * An element is allowed to self close only if: 133 | * It is empty AND 134 | * It's a component OR 135 | * It's in the HTML spec as a void element OR 136 | * It has a `set:*` directive 137 | */ 138 | const isSelfClosingTag = 139 | isEmpty && 140 | (node.type === 'component' || 141 | selfClosingTags.includes(node.name) || 142 | hasSetDirectives(node)); 143 | 144 | const isSingleLinePerAttribute = opts.singleAttributePerLine && node.attributes.length > 1; 145 | const attributeLine = isSingleLinePerAttribute ? breakParent : ''; 146 | const attributes = join(attributeLine, path.map(print, 'attributes')); 147 | 148 | if (isSelfClosingTag) { 149 | return group(['<', node.name, indent(attributes), line, `/>`]); 150 | } 151 | 152 | if (node.children) { 153 | const children = node.children; 154 | const firstChild = children[0]; 155 | const lastChild = children[children.length - 1]; 156 | 157 | // No hugging of content means it's either a block element and/or there's whitespace at the start/end 158 | let noHugSeparatorStart: 159 | | _doc.builders.Line 160 | | _doc.builders.Softline 161 | | _doc.builders.Hardline 162 | | string = softline; 163 | let noHugSeparatorEnd: 164 | | _doc.builders.Line 165 | | _doc.builders.Softline 166 | | _doc.builders.Hardline 167 | | string = softline; 168 | const hugStart = shouldHugStart(node, opts); 169 | const hugEnd = shouldHugEnd(node, opts); 170 | 171 | let body; 172 | 173 | if (isEmpty) { 174 | body = 175 | isInlineElement(path, opts, node) && 176 | node.children.length && 177 | isTextNodeStartingWithWhitespace(node.children[0]) && 178 | !isPreTagContent(path) 179 | ? () => line 180 | : () => (node.children.length > 0 ? softline : ''); 181 | } else if (isPreTagContent(path)) { 182 | body = () => printRaw(node); 183 | } else if (isInlineElement(path, opts, node) && !isPreTagContent(path)) { 184 | body = () => path.map(print, 'children'); 185 | } else { 186 | body = () => path.map(print, 'children'); 187 | } 188 | 189 | const openingTag = [ 190 | '<', 191 | node.name, 192 | indent( 193 | group([ 194 | attributes, 195 | hugStart 196 | ? '' 197 | : !isPreTagContent(path) && !opts.bracketSameLine 198 | ? dedent(softline) 199 | : '', 200 | ]), 201 | ), 202 | ]; 203 | 204 | if (hugStart && hugEnd) { 205 | const huggedContent = [ 206 | isSingleLinePerAttribute ? hardline : softline, 207 | group(['>', body(), `', 217 | ]); 218 | } 219 | 220 | if (isPreTagContent(path)) { 221 | noHugSeparatorStart = ''; 222 | noHugSeparatorEnd = ''; 223 | } else { 224 | let didSetEndSeparator = false; 225 | 226 | if (!hugStart && firstChild && isTextNode(firstChild)) { 227 | if ( 228 | isTextNodeStartingWithLinebreak(firstChild) && 229 | firstChild !== lastChild && 230 | (!isInlineElement(path, opts, node) || isTextNodeEndingWithWhitespace(lastChild)) 231 | ) { 232 | noHugSeparatorStart = hardline; 233 | noHugSeparatorEnd = hardline; 234 | didSetEndSeparator = true; 235 | } else if (isInlineElement(path, opts, node)) { 236 | noHugSeparatorStart = line; 237 | } 238 | trimTextNodeLeft(firstChild); 239 | } 240 | if (!hugEnd && lastChild && isTextNode(lastChild)) { 241 | if (isInlineElement(path, opts, node) && !didSetEndSeparator) { 242 | noHugSeparatorEnd = line; 243 | } 244 | trimTextNodeRight(lastChild); 245 | } 246 | } 247 | 248 | if (hugStart) { 249 | return group([ 250 | ...openingTag, 251 | indent([softline, group(['>', body()])]), 252 | noHugSeparatorEnd, 253 | ``, 254 | ]); 255 | } 256 | 257 | if (hugEnd) { 258 | return group([ 259 | ...openingTag, 260 | '>', 261 | indent([noHugSeparatorStart, group([body(), `', 264 | ]); 265 | } 266 | 267 | if (isEmpty) { 268 | return group([...openingTag, '>', body(), ``]); 269 | } 270 | 271 | return group([ 272 | ...openingTag, 273 | '>', 274 | indent([noHugSeparatorStart, body()]), 275 | noHugSeparatorEnd, 276 | ``, 277 | ]); 278 | } 279 | 280 | // TODO: WIP 281 | return ''; 282 | } 283 | 284 | case 'attribute': { 285 | const name = node.name.trim(); 286 | switch (node.kind) { 287 | case 'empty': 288 | return [line, name]; 289 | case 'expression': 290 | // Handled in the `embed` function 291 | // See embed.ts 292 | return ''; 293 | case 'quoted': 294 | let value = node.value; 295 | 296 | if (node.name === 'class') { 297 | value = printClassNames(value); 298 | } 299 | 300 | const unescapedValue = value.replace(/'/g, "'").replace(/"/g, '"'); 301 | const { escaped, quote, regex } = getPreferredQuote( 302 | unescapedValue, 303 | opts.jsxSingleQuote ? "'" : '"', 304 | ); 305 | 306 | const result = unescapedValue.replace(regex, escaped); 307 | return [line, name, '=', quote, result, quote]; 308 | case 'shorthand': 309 | return [line, '{', name, '}']; 310 | case 'spread': 311 | return [line, '{...', name, '}']; 312 | case 'template-literal': 313 | return [line, name, '=', '`', node.value, '`']; 314 | default: 315 | break; 316 | } 317 | return ''; 318 | } 319 | 320 | case 'doctype': { 321 | // https://www.w3.org/wiki/Doctypes_and_markup_styles 322 | return ['', hardline]; 323 | } 324 | 325 | case 'comment': 326 | if (isIgnoreDirective(node)) { 327 | ignoreNext = true; 328 | } 329 | 330 | const nextNode = getNextNode(path); 331 | let trailingLine: string | _doc.builders.Hardline = ''; 332 | if (nextNode && isTagLikeNode(nextNode)) { 333 | trailingLine = hardline; 334 | } 335 | return ['', trailingLine]; 336 | 337 | default: { 338 | throw new Error(`Unhandled node type "${node.type}"!`); 339 | } 340 | } 341 | } 342 | 343 | /** 344 | * Split the text into words separated by whitespace. Replace the whitespaces by lines, 345 | * collapsing multiple whitespaces into a single line. 346 | * 347 | * If the text starts or ends with multiple newlines, two of those should be kept. 348 | */ 349 | function splitTextToDocs(node: TextNode): Doc[] { 350 | const text = getUnencodedText(node); 351 | 352 | const textLines = text.split(/[\t\n\f\r ]+/); 353 | 354 | let docs = join(line, textLines).filter((doc) => doc !== ''); 355 | 356 | if (startsWithLinebreak(text)) { 357 | docs[0] = hardline; 358 | } 359 | if (startsWithLinebreak(text, 2)) { 360 | docs = [hardline, ...docs]; 361 | } 362 | 363 | if (endsWithLinebreak(text)) { 364 | docs[docs.length - 1] = hardline; 365 | } 366 | if (endsWithLinebreak(text, 2)) { 367 | docs = [...docs, hardline]; 368 | } 369 | 370 | return docs; 371 | } 372 | -------------------------------------------------------------------------------- /src/printer/utils.ts: -------------------------------------------------------------------------------- 1 | import { serialize } from '@astrojs/compiler/utils'; 2 | import { 3 | type AstPath as AstP, 4 | type BuiltInParserName, 5 | type Doc, 6 | type ParserOptions as ParserOpts, 7 | } from 'prettier'; 8 | import { blockElements, formattableAttributes, type TagName } from './elements'; 9 | import type { 10 | CommentNode, 11 | ExpressionNode, 12 | Node, 13 | ParentLikeNode, 14 | TagLikeNode, 15 | TextNode, 16 | anyNode, 17 | } from './nodes'; 18 | 19 | export type printFn = (path: AstPath) => Doc; 20 | export type ParserOptions = ParserOpts; 21 | export type AstPath = AstP; 22 | 23 | export const openingBracketReplace = '_Pé'; 24 | export const closingBracketReplace = 'èP_'; 25 | export const atSignReplace = 'ΩP_'; 26 | export const dotReplace = 'ωP_'; 27 | export const interrogationReplace = 'ΔP_'; 28 | 29 | export function isInlineElement(path: AstPath, opts: ParserOptions, node: anyNode): boolean { 30 | return node && isTagLikeNode(node) && !isBlockElement(node, opts) && !isPreTagContent(path); 31 | } 32 | 33 | export function isBlockElement(node: anyNode, opts: ParserOptions): boolean { 34 | if (!node) { 35 | return false; 36 | } 37 | 38 | // All tags (element, custom-element, component, fragment) are considered 39 | // block elements when htmlWhitespaceSensitivity is set to "ignore". 40 | if (opts.htmlWhitespaceSensitivity === 'ignore') { 41 | return true; 42 | } 43 | 44 | return ( 45 | node.type === 'element' && 46 | opts.htmlWhitespaceSensitivity !== 'strict' && 47 | blockElements.includes(node.name as TagName) 48 | ); 49 | } 50 | 51 | export function isIgnoreDirective(node: Node): boolean { 52 | return node.type === 'comment' && node.value.trim() === 'prettier-ignore'; 53 | } 54 | 55 | /** 56 | * Returns the content of the node 57 | */ 58 | export function printRaw(node: anyNode, stripLeadingAndTrailingNewline = false): string { 59 | if (!isNodeWithChildren(node)) { 60 | return ''; 61 | } 62 | 63 | if (node.children.length === 0) { 64 | return ''; 65 | } 66 | 67 | let raw = node.children.reduce((prev: string, curr: Node) => prev + serialize(curr), ''); 68 | 69 | if (!stripLeadingAndTrailingNewline) { 70 | return raw; 71 | } 72 | 73 | if (startsWithLinebreak(raw)) { 74 | raw = raw.substring(raw.indexOf('\n') + 1); 75 | } 76 | if (endsWithLinebreak(raw)) { 77 | raw = raw.substring(0, raw.lastIndexOf('\n')); 78 | if (raw.charAt(raw.length - 1) === '\r') { 79 | raw = raw.substring(0, raw.length - 1); 80 | } 81 | } 82 | 83 | return raw; 84 | } 85 | 86 | export function isNodeWithChildren(node: anyNode): node is anyNode & ParentLikeNode { 87 | return node && 'children' in node && Array.isArray(node.children); 88 | } 89 | 90 | export const isEmptyTextNode = (node: anyNode): boolean => { 91 | return !!node && node.type === 'text' && getUnencodedText(node).trim() === ''; 92 | }; 93 | 94 | export function getUnencodedText(node: TextNode | CommentNode): string { 95 | return node.value; 96 | } 97 | 98 | export function isTextNodeStartingWithLinebreak(node: TextNode, nrLines = 1): node is TextNode { 99 | return startsWithLinebreak(getUnencodedText(node), nrLines); 100 | } 101 | 102 | export function startsWithLinebreak(text: string, nrLines = 1): boolean { 103 | return new RegExp(`^([\\t\\f\\r ]*\\n){${nrLines}}`).test(text); 104 | } 105 | 106 | export function endsWithLinebreak(text: string, nrLines = 1): boolean { 107 | return new RegExp(`(\\n[\\t\\f\\r ]*){${nrLines}}$`).test(text); 108 | } 109 | 110 | export function isTextNodeStartingWithWhitespace(node: Node): node is TextNode { 111 | return isTextNode(node) && /^\s/.test(getUnencodedText(node)); 112 | } 113 | 114 | function endsWithWhitespace(text: string) { 115 | return /\s$/.test(text); 116 | } 117 | 118 | export function isTextNodeEndingWithWhitespace(node: Node): node is TextNode { 119 | return isTextNode(node) && endsWithWhitespace(getUnencodedText(node)); 120 | } 121 | 122 | export function hasSetDirectives(node: TagLikeNode) { 123 | const attributes = Array.from(node.attributes, (attr) => attr.name); 124 | return attributes.some((attr) => ['set:html', 'set:text'].includes(attr)); 125 | } 126 | 127 | /** 128 | * Check if given node's start tag should hug its first child. This is the case for inline elements when there's 129 | * no whitespace between the `>` and the first child. 130 | */ 131 | export function shouldHugStart(node: anyNode, opts: ParserOptions): boolean { 132 | if (isBlockElement(node, opts)) { 133 | return false; 134 | } 135 | 136 | if (!isNodeWithChildren(node)) { 137 | return false; 138 | } 139 | 140 | const children = node.children; 141 | if (children.length === 0) { 142 | return true; 143 | } 144 | 145 | const firstChild = children[0]; 146 | return !isTextNodeStartingWithWhitespace(firstChild); 147 | } 148 | 149 | /** 150 | * Check if given node's end tag should hug its last child. This is the case for inline elements when there's 151 | * no whitespace between the last child and the `` can be omitted. 175 | */ 176 | export function canOmitSoftlineBeforeClosingTag(path: AstPath, opts: ParserOptions): boolean { 177 | return isLastChildWithinParentBlockElement(path, opts); 178 | } 179 | 180 | function getChildren(node: anyNode): Node[] { 181 | return isNodeWithChildren(node) ? node.children : []; 182 | } 183 | 184 | function isLastChildWithinParentBlockElement(path: AstPath, opts: ParserOptions): boolean { 185 | const parent = path.getParentNode(); 186 | if (!parent || !isBlockElement(parent, opts)) { 187 | return false; 188 | } 189 | 190 | const children = getChildren(parent); 191 | const lastChild = children[children.length - 1]; 192 | return lastChild === path.getNode(); 193 | } 194 | 195 | export function trimTextNodeLeft(node: TextNode): void { 196 | node.value = node.value && node.value.trimStart(); 197 | } 198 | 199 | export function trimTextNodeRight(node: TextNode): void { 200 | node.value = node.value && node.value.trimEnd(); 201 | } 202 | 203 | export function printClassNames(value: string) { 204 | const lines = value.trim().split(/[\r\n]+/); 205 | const formattedLines = lines.map((line) => { 206 | const spaces = /^\s+/.exec(line); 207 | return (spaces ? spaces[0] : '') + line.trim().split(/\s+/).join(' '); 208 | }); 209 | return formattedLines.join('\n'); 210 | } 211 | 212 | /** dedent string & return tabSize (the last part is what we need) */ 213 | export function manualDedent(input: string): { 214 | tabSize: number; 215 | char: string; 216 | result: string; 217 | } { 218 | let minTabSize = Infinity; 219 | let result = input; 220 | // 1. normalize 221 | result = result.replace(/\r\n/g, '\n'); 222 | 223 | // 2. count tabSize 224 | let char = ''; 225 | for (const line of result.split('\n')) { 226 | if (!line) continue; 227 | // if any line begins with a non-whitespace char, minTabSize is 0 228 | if (line[0] && /^\S/.test(line[0])) { 229 | minTabSize = 0; 230 | break; 231 | } 232 | const match = /^(\s+)\S+/.exec(line); // \S ensures we don’t count lines of pure whitespace 233 | if (match) { 234 | if (match[1] && !char) char = match[1][0]; 235 | if (match[1].length < minTabSize) minTabSize = match[1].length; 236 | } 237 | } 238 | 239 | // 3. reformat string 240 | if (minTabSize > 0 && Number.isFinite(minTabSize)) { 241 | result = result.replace(new RegExp(`^${new Array(minTabSize + 1).join(char)}`, 'gm'), ''); 242 | } 243 | 244 | return { 245 | tabSize: minTabSize === Infinity ? 0 : minTabSize, 246 | char, 247 | result, 248 | }; 249 | } 250 | 251 | /** True if the node is of type text */ 252 | export function isTextNode(node: anyNode): node is TextNode { 253 | return node.type === 'text'; 254 | } 255 | 256 | export function isExpressionNode(node: anyNode): node is ExpressionNode { 257 | return node.type === 'expression'; 258 | } 259 | 260 | /** True if the node is TagLikeNode: 261 | * 262 | * ElementNode | ComponentNode | CustomElementNode | FragmentNode */ 263 | export function isTagLikeNode(node: anyNode): node is TagLikeNode { 264 | return ( 265 | node.type === 'element' || 266 | node.type === 'component' || 267 | node.type === 'custom-element' || 268 | node.type === 'fragment' 269 | ); 270 | } 271 | 272 | /** 273 | * Returns siblings, that is, the children of the parent. 274 | */ 275 | export function getSiblings(path: AstPath): anyNode[] { 276 | const parent = path.getParentNode(); 277 | if (!parent) return []; 278 | 279 | return getChildren(parent); 280 | } 281 | 282 | export function getNextNode(path: AstPath): anyNode | null { 283 | const node = path.getNode(); 284 | if (node) { 285 | const siblings = getSiblings(path); 286 | if (node.position?.start === siblings[siblings.length - 1].position?.start) return null; 287 | for (let i = 0; i < siblings.length; i++) { 288 | const sibling = siblings[i]; 289 | if (sibling.position?.start === node.position?.start && i !== siblings.length - 1) { 290 | return siblings[i + 1]; 291 | } 292 | } 293 | } 294 | return null; 295 | } 296 | 297 | export const isPreTagContent = (path: AstPath): boolean => { 298 | if (!path || !path.stack || !Array.isArray(path.stack)) return false; 299 | return path.stack.some( 300 | (node: anyNode) => 301 | (node.type === 'element' && node.name.toLowerCase() === 'pre') || 302 | (node.type === 'attribute' && !formattableAttributes.includes(node.name)), 303 | ); 304 | }; 305 | 306 | interface QuoteResult { 307 | quote: '"' | "'"; 308 | regex: RegExp; 309 | escaped: string; 310 | } 311 | 312 | // Adapted from Prettier's source code as it's unfortunately not exported 313 | // https://github.com/prettier/prettier/blob/237e681936fc533c27d7ce8577d3fc98838a3314/src/common/util.js#L238 314 | export function getPreferredQuote(rawContent: string, preferredQuote: string): QuoteResult { 315 | const double: QuoteResult = { quote: '"', regex: /"/g, escaped: '"' }; 316 | const single: QuoteResult = { quote: "'", regex: /'/g, escaped: ''' }; 317 | 318 | const preferred = preferredQuote === "'" ? single : double; 319 | const alternate = preferred === single ? double : single; 320 | 321 | let result = preferred; 322 | 323 | // If `rawContent` contains at least one of the quote preferred for enclosing 324 | // the string, we might want to enclose with the alternate quote instead, to 325 | // minimize the number of escaped quotes. 326 | if (rawContent.includes(preferred.quote) || rawContent.includes(alternate.quote)) { 327 | const numPreferredQuotes = (preferred.regex.exec(rawContent) || []).length; 328 | const numAlternateQuotes = (alternate.regex.exec(rawContent) || []).length; 329 | 330 | result = numPreferredQuotes > numAlternateQuotes ? alternate : preferred; 331 | } 332 | 333 | return result; 334 | } 335 | 336 | // Adapted from: https://github.com/prettier/prettier/blob/20ab6d6f1c5bd774621230b493a3b71d39383a2c/src/language-html/utils/index.js#LL336C1-L369C2 337 | export function inferParserByTypeAttribute(type: string): BuiltInParserName { 338 | if (!type) { 339 | return 'babel-ts'; 340 | } 341 | 342 | switch (type) { 343 | case 'module': 344 | case 'text/javascript': 345 | case 'text/babel': 346 | case 'application/javascript': 347 | return 'babel'; 348 | 349 | case 'application/x-typescript': 350 | return 'babel-ts'; 351 | 352 | case 'text/markdown': 353 | return 'markdown'; 354 | 355 | case 'text/html': 356 | return 'html'; 357 | 358 | case 'text/x-handlebars-template': 359 | return 'glimmer'; 360 | 361 | default: 362 | if (type.endsWith('json') || type.endsWith('importmap') || type === 'speculationrules') { 363 | return 'json'; 364 | } 365 | return 'babel-ts'; 366 | } 367 | } 368 | --------------------------------------------------------------------------------