├── .husky ├── .gitignore └── pre-commit ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── formatting.yml ├── dependabot.yml └── workflows │ ├── main.yml │ └── auto-merge.yml ├── .npmignore ├── .gitignore ├── types └── plugin.d.ts ├── .prettierignore ├── test ├── parser.test.js ├── embed.test.js ├── format.test.js ├── fixture.xml └── __snapshots__ │ └── format.test.js.snap ├── eslint.config.mjs ├── bin ├── print.js └── languages.js ├── LICENSE ├── package.json ├── src ├── plugin.js ├── embed.js ├── parser.js └── printer.js ├── README.md └── CHANGELOG.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kddnewton 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | bin/ 3 | src/ 4 | test/ 5 | test.xml 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.eslintcache 2 | /coverage/ 3 | /src/languages.js 4 | /dist/ 5 | /node_modules/ 6 | /test.xml 7 | -------------------------------------------------------------------------------- /types/plugin.d.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "prettier"; 2 | 3 | declare const plugin: Plugin; 4 | export default plugin; 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /coverage/ 3 | /dist/ 4 | /.eslintcache 5 | /.*ignore 6 | /yarn.lock 7 | /LICENSE 8 | 9 | /.husky/ 10 | /test/fixture.xml 11 | /test/__snapshots__/* 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /test/parser.test.js: -------------------------------------------------------------------------------- 1 | import parser from "../src/parser.js"; 2 | 3 | test("parseError", () => { 4 | const expected = new SyntaxError( 5 | "Expecting: one of these possible Token sequences:\n" + 6 | " 1. [CLOSE]\n" + 7 | " 2. [SLASH_CLOSE]\n" + 8 | "but found: '/' (1:6)" 9 | ); 10 | 11 | expect(() => parser.parse(" 21 | console.log(formatted.formatted) 22 | ); 23 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --merge "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present Kevin Newton 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/languages.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { writeFileSync } from "node:fs"; 4 | import * as linguistLanguages from "linguist-languages"; 5 | import { format } from "prettier"; 6 | import packageJSON from "../package.json" with { type: "json" }; 7 | 8 | function getSupportLanguages() { 9 | const supportLanguages = []; 10 | 11 | for (const language of Object.values(linguistLanguages)) { 12 | if (language.aceMode === "xml") { 13 | const { type, color, aceMode, languageId, ...config } = language; 14 | 15 | // Before we used linguist to get the languages, we had a 16 | // manually-maintained list. These two had been added manually. So in the 17 | // interest of not breaking anything, we'll add them back in here. 18 | if (language.name === "XML") { 19 | let extensions = language.extensions; 20 | if (extensions) { 21 | extensions.push(".inx", ".runsettings"); 22 | config.extensions = extensions 23 | // https://github.com/github-linguist/linguist/pull/1842 24 | .filter((e) => ![".ts", ".tsx"].includes(e)) 25 | .sort(); 26 | } 27 | } 28 | 29 | supportLanguages.push({ 30 | ...config, 31 | since: "0.1.0", 32 | parsers: ["xml"], 33 | linguistLanguageId: languageId, 34 | vscodeLanguageIds: ["xml"] 35 | }); 36 | } 37 | } 38 | 39 | return supportLanguages; 40 | } 41 | 42 | const languages = JSON.stringify(getSupportLanguages()); 43 | const { plugins, ...prettierConfig } = packageJSON.prettier; 44 | 45 | const formatted = await format(`export default ${languages};`, { 46 | parser: "babel", 47 | ...prettierConfig 48 | }); 49 | writeFileSync("src/languages.js", formatted); 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prettier/plugin-xml", 3 | "version": "3.4.2", 4 | "description": "prettier plugin for XML", 5 | "type": "module", 6 | "main": "src/plugin.js", 7 | "exports": { 8 | ".": { 9 | "types": "./types/plugin.d.ts", 10 | "default": "./src/plugin.js" 11 | }, 12 | "./*": "./*" 13 | }, 14 | "scripts": { 15 | "lint": "eslint --cache .", 16 | "prepare": "node bin/languages.js && husky install", 17 | "print": "prettier --plugin=./src/plugin.js", 18 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/prettier/plugin-xml.git" 23 | }, 24 | "author": "Kevin Newton", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/prettier/plugin-xml/issues" 28 | }, 29 | "homepage": "https://github.com/prettier/plugin-xml#readme", 30 | "dependencies": { 31 | "@xml-tools/parser": "^1.0.11" 32 | }, 33 | "peerDependencies": { 34 | "prettier": "^3.0.0" 35 | }, 36 | "devDependencies": { 37 | "@eslint/js": "^9.21.0", 38 | "eslint": "^9.21.0", 39 | "eslint-config-prettier": "^10.0.2", 40 | "globals": "^16.0.0", 41 | "husky": "^9.0.6", 42 | "jest": "^30.0.0", 43 | "linguist-languages": "^9.0.0", 44 | "lint-staged": "^16.0.0", 45 | "prettier": "^3.6.2" 46 | }, 47 | "jest": { 48 | "testRegex": ".test.js$", 49 | "transform": {} 50 | }, 51 | "prettier": { 52 | "embeddedLanguageFormatting": "auto", 53 | "plugins": [ 54 | "./src/plugin.js" 55 | ], 56 | "trailingComma": "none", 57 | "xmlWhitespaceSensitivity": "ignore" 58 | }, 59 | "lint-staged": { 60 | "*.js": [ 61 | "eslint --cache --fix", 62 | "prettier --write" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/formatting.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Formatting 2 | description: Issues for ugly or incorrect code 3 | body: 4 | - type: dropdown 5 | attributes: 6 | label: bracketSameLine 7 | description: What value do you have the `bracketSameLine` option set to? (Defaults to `false`.) 8 | options: 9 | - "true" 10 | - "false" 11 | - type: input 12 | attributes: 13 | label: printWidth 14 | description: What value do you have the `printWidth` option set to? (Defaults to `80`.) 15 | placeholder: "80" 16 | - type: dropdown 17 | attributes: 18 | label: singleAttributePerLine 19 | description: What value do you have the `singleAttributePerLine` option set to? (Defaults to `false`.) 20 | options: 21 | - "true" 22 | - "false" 23 | default: 1 24 | - type: input 25 | attributes: 26 | label: tabWidth 27 | description: What value do you have the `tabWidth` option set to? (Defaults to `2`.) 28 | placeholder: "2" 29 | - type: dropdown 30 | attributes: 31 | label: xmlWhitespaceSensitivity 32 | description: What value do you have the `xmlWhitespaceSensitivity` option set to? (Defaults to `"strict"`.) Be 100% sure changing this to `"ignore"` doesn't fix your issue! 33 | options: 34 | - "strict" 35 | - "preserve" 36 | - "ignore" 37 | validations: 38 | required: true 39 | - type: dropdown 40 | attributes: 41 | label: xmlSelfClosingSpace 42 | description: What value do you have the `xmlSelfClosingSpace` option set to? (Defaults to `true`.) 43 | options: 44 | - "true" 45 | - "false" 46 | default: 0 47 | - type: textarea 48 | attributes: 49 | label: Input XML 50 | render: xml 51 | validations: 52 | required: true 53 | - type: textarea 54 | attributes: 55 | label: Current output XML 56 | render: xml 57 | validations: 58 | required: true 59 | - type: textarea 60 | attributes: 61 | label: Expected output XML 62 | render: xml 63 | validations: 64 | required: true 65 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import languages from "./languages.js"; 2 | import parser from "./parser.js"; 3 | import printer from "./printer.js"; 4 | 5 | const plugin = { 6 | languages, 7 | parsers: { 8 | xml: parser 9 | }, 10 | printers: { 11 | xml: printer 12 | }, 13 | options: { 14 | xmlSelfClosingSpace: { 15 | type: "boolean", 16 | category: "XML", 17 | default: true, 18 | description: "Adds a space before self-closing tags.", 19 | since: "1.1.0" 20 | }, 21 | xmlWhitespaceSensitivity: { 22 | type: "choice", 23 | category: "XML", 24 | default: "strict", 25 | description: "How to handle whitespaces in XML.", 26 | choices: [ 27 | { 28 | value: "strict", 29 | description: "Whitespaces are considered sensitive in all elements." 30 | }, 31 | { 32 | value: "preserve", 33 | description: 34 | "Whitespaces within text nodes in XML elements and attributes are considered sensitive." 35 | }, 36 | { 37 | value: "ignore", 38 | description: "Whitespaces are considered insensitive in all elements." 39 | } 40 | ], 41 | since: "0.6.0" 42 | }, 43 | xmlSortAttributesByKey: { 44 | type: "boolean", 45 | category: "XML", 46 | default: false, 47 | description: 48 | "Orders XML attributes by key alphabetically while prioritizing xmlns attributes." 49 | }, 50 | xmlQuoteAttributes: { 51 | type: "choice", 52 | category: "XML", 53 | default: "preserve", 54 | description: "How to handle whitespaces in XML.", 55 | choices: [ 56 | { 57 | value: "preserve", 58 | description: 59 | "Quotes in attribute values will be preserved as written." 60 | }, 61 | { 62 | value: "single", 63 | description: 64 | "Quotes in attribute values will be converted to consistent single quotes and other quotes in the string will be escaped." 65 | }, 66 | { 67 | value: "double", 68 | description: 69 | "Quotes in attribute values will be converted to consistent double quotes and other quotes in the string will be escaped." 70 | } 71 | ] 72 | } 73 | }, 74 | defaultOptions: { 75 | printWidth: 80, 76 | tabWidth: 2 77 | } 78 | }; 79 | 80 | export default plugin; 81 | -------------------------------------------------------------------------------- /test/embed.test.js: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import plugin from "../src/plugin.js"; 3 | 4 | function format(content, opts = {}) { 5 | const { plugins = [] } = opts; 6 | 7 | return prettier.format(content, { 8 | ...opts, 9 | parser: "xml", 10 | plugins: [...plugins, plugin] 11 | }); 12 | } 13 | 14 | test("embeds properly when the name of the tag matches", async () => { 15 | const formatted = await format("1+1;"); 16 | const expected = ` 17 | 1 + 1; 18 | 19 | `; 20 | 21 | expect(formatted).toEqual(expected); 22 | }); 23 | 24 | test("embeds properly when the type of the style tag matches", async () => { 25 | const formatted = await format( 26 | '' 27 | ); 28 | const expected = ` 33 | `; 34 | 35 | expect(formatted).toEqual(expected); 36 | }); 37 | 38 | test("embeds properly when the type of the script tag matches", async () => { 39 | const formatted = await format( 40 | '' 41 | ); 42 | const expected = ` 45 | `; 46 | 47 | expect(formatted).toEqual(expected); 48 | }); 49 | 50 | const customScriptPlugin = { 51 | parsers: { 52 | customScript: { 53 | astFormat: "customScript", 54 | parse(text) { 55 | return { type: "root", text }; 56 | }, 57 | locStart() { 58 | return -1; 59 | }, 60 | locEnd() { 61 | return -1; 62 | } 63 | } 64 | }, 65 | printers: { 66 | customScript: { 67 | print(path) { 68 | const { hardline } = prettier.doc.builders; 69 | return ["customScript!!!", hardline, hardline, path.getValue().text]; 70 | } 71 | } 72 | } 73 | }; 74 | 75 | test("embeds properly when the type of the script tag matches a custom parser", async () => { 76 | const formatted = await format( 77 | '', 78 | { 79 | plugins: [customScriptPlugin] 80 | } 81 | ); 82 | 83 | const expected = ` 88 | `; 89 | 90 | expect(formatted).toEqual(expected); 91 | }); 92 | 93 | test("does not embed when self-closing", async () => { 94 | const expected = `