chore: update dependencies and full integrate markdown formatting with official prettier implementations

This commit is contained in:
Luke Hagar
2025-10-31 21:48:44 -05:00
parent 9302f1812d
commit d4f9d729d3
36 changed files with 4054 additions and 623 deletions

110
bun.lock
View File

@@ -4,20 +4,20 @@
"": {
"name": "prettier-plugin-openapi",
"dependencies": {
"@types/js-yaml": "^4.0.0",
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0",
},
"devDependencies": {
"@biomejs/biome": "^2.2.4",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.1",
"@types/bun": "latest",
"@types/node": "^20.0.0",
"prettier": "^3.0.0",
"rollup": "^4.52.2",
"@biomejs/biome": "^2.3.2",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@types/bun": "^1.3.1",
"@types/node": "^24.9.2",
"prettier": "^3.6.2",
"rollup": "^4.52.5",
"rollup-plugin-typescript": "^1.0.1",
"typescript": "^5.0.0",
"typescript": "^5.9.3",
},
"peerDependencies": {
"prettier": "^3.0.0",
@@ -25,93 +25,93 @@
},
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
"@biomejs/biome": ["@biomejs/biome@2.3.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.2", "@biomejs/cli-darwin-x64": "2.3.2", "@biomejs/cli-linux-arm64": "2.3.2", "@biomejs/cli-linux-arm64-musl": "2.3.2", "@biomejs/cli-linux-x64": "2.3.2", "@biomejs/cli-linux-x64-musl": "2.3.2", "@biomejs/cli-win32-arm64": "2.3.2", "@biomejs/cli-win32-x64": "2.3.2" }, "bin": { "biome": "bin/biome" } }, "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.1", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ=="],
"@rollup/plugin-alias": ["@rollup/plugin-alias@6.0.0", "", { "peerDependencies": { "rollup": ">=4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g=="],
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.6", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw=="],
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="],
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA=="],
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.2", "", { "os": "android", "cpu": "arm" }, "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.2", "", { "os": "linux", "cpu": "arm" }, "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.2", "", { "os": "linux", "cpu": "none" }, "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.2", "", { "os": "linux", "cpu": "none" }, "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.2", "", { "os": "linux", "cpu": "none" }, "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.2", "", { "os": "linux", "cpu": "x64" }, "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.2", "", { "os": "linux", "cpu": "x64" }, "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.2", "", { "os": "none", "cpu": "arm64" }, "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.5", "", { "os": "none", "cpu": "arm64" }, "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="],
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/node": ["@types/node@20.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ=="],
"@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
@@ -137,7 +137,7 @@
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
@@ -145,9 +145,9 @@
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"rollup": ["rollup@4.52.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.2", "@rollup/rollup-android-arm64": "4.52.2", "@rollup/rollup-darwin-arm64": "4.52.2", "@rollup/rollup-darwin-x64": "4.52.2", "@rollup/rollup-freebsd-arm64": "4.52.2", "@rollup/rollup-freebsd-x64": "4.52.2", "@rollup/rollup-linux-arm-gnueabihf": "4.52.2", "@rollup/rollup-linux-arm-musleabihf": "4.52.2", "@rollup/rollup-linux-arm64-gnu": "4.52.2", "@rollup/rollup-linux-arm64-musl": "4.52.2", "@rollup/rollup-linux-loong64-gnu": "4.52.2", "@rollup/rollup-linux-ppc64-gnu": "4.52.2", "@rollup/rollup-linux-riscv64-gnu": "4.52.2", "@rollup/rollup-linux-riscv64-musl": "4.52.2", "@rollup/rollup-linux-s390x-gnu": "4.52.2", "@rollup/rollup-linux-x64-gnu": "4.52.2", "@rollup/rollup-linux-x64-musl": "4.52.2", "@rollup/rollup-openharmony-arm64": "4.52.2", "@rollup/rollup-win32-arm64-msvc": "4.52.2", "@rollup/rollup-win32-ia32-msvc": "4.52.2", "@rollup/rollup-win32-x64-gnu": "4.52.2", "@rollup/rollup-win32-x64-msvc": "4.52.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA=="],
"rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="],
"rollup-plugin-typescript": ["rollup-plugin-typescript@1.0.1", "", { "dependencies": { "resolve": "^1.10.0", "rollup-pluginutils": "^2.5.0" }, "peerDependencies": { "tslib": "*", "typescript": ">=2.1.0" } }, "sha512-rwJDNn9jv/NsKZuyBb/h0jsclP4CJ58qbvZt2Q9zDIGILF2LtdtvCqMOL+Gq9IVq5MTrTlHZNrn8h7VjQgd8tw=="],
@@ -157,14 +157,10 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"bun-types/@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"bun-types/@types/node/undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
}
}

View File

@@ -71,19 +71,19 @@
"prettier": "^3.0.0"
},
"devDependencies": {
"@biomejs/biome": "^2.2.4",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.1",
"@types/bun": "latest",
"@types/node": "^20.0.0",
"prettier": "^3.0.0",
"rollup": "^4.52.2",
"@biomejs/biome": "^2.3.2",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@types/bun": "^1.3.1",
"@types/node": "^24.9.2",
"prettier": "^3.6.2",
"rollup": "^4.52.5",
"rollup-plugin-typescript": "^1.0.1",
"typescript": "^5.0.0"
"typescript": "^5.9.3"
},
"dependencies": {
"@types/js-yaml": "^4.0.0",
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0"
}
}

View File

@@ -1,6 +1,6 @@
import * as yaml from 'js-yaml';
import type { AstPath, Doc, Parser, ParserOptions, Printer, SupportLanguage } from 'prettier';
import { getVendorExtensions } from './extensions/vendor-loader.js';
import * as yaml from "js-yaml";
import type { AstPath, Doc, Parser, ParserOptions, Printer, SupportLanguage } from "prettier";
import { getVendorExtensions } from "./extensions/vendor-loader.js";
export type PrintFn = (path: AstPath) => Doc;
@@ -30,18 +30,19 @@ import {
TagKeys,
WebhookKeys,
XMLKeys,
} from './keys.js';
} from "./keys.js";
// Type definitions for better type safety
interface OpenAPINode {
isOpenAPI: boolean;
content?: any;
format?: 'json' | 'yaml';
format?: "json" | "yaml";
}
interface OpenAPIPluginOptions {
tabWidth?: number;
printWidth?: number;
formatMarkdown?: boolean; // Option to enable/disable markdown formatting (default: true)
}
// Load vendor extensions
@@ -51,15 +52,15 @@ const vendorExtensions = getVendorExtensions();
* Unified parser that can handle both JSON and YAML OpenAPI files
*/
function parseOpenAPIFile(text: string, options?: any): OpenAPINode {
let format: 'json' | 'yaml' | undefined;
let format: "json" | "yaml" | undefined;
if (options?.filepath) {
switch (true) {
case options?.filepath.endsWith('.yaml') || options?.filepath.endsWith('.yml'):
format = 'yaml';
case options?.filepath.endsWith(".yaml") || options?.filepath.endsWith(".yml"):
format = "yaml";
break;
case options?.filepath.endsWith('.json'):
format = 'json';
case options?.filepath.endsWith(".json"):
format = "json";
break;
}
}
@@ -67,10 +68,10 @@ function parseOpenAPIFile(text: string, options?: any): OpenAPINode {
if (!format) {
// Try to detect format from content
const trimmedText = text.trim();
if (trimmedText.startsWith('{') || trimmedText.startsWith('[')) {
format = 'json';
if (trimmedText.startsWith("{") || trimmedText.startsWith("[")) {
format = "json";
} else {
format = 'yaml';
format = "yaml";
}
}
@@ -78,19 +79,19 @@ function parseOpenAPIFile(text: string, options?: any): OpenAPINode {
try {
switch (format) {
case 'yaml':
case "yaml":
parsed = yaml.load(text, {
schema: yaml.DEFAULT_SCHEMA,
});
break;
case 'json':
case "json":
parsed = JSON.parse(text);
break;
}
} catch (error) {
return {
isOpenAPI: false,
}
};
}
let isOpenAPI: boolean;
@@ -100,7 +101,7 @@ function parseOpenAPIFile(text: string, options?: any): OpenAPINode {
} catch (error) {
return {
isOpenAPI: false,
}
};
}
switch (isOpenAPI) {
@@ -109,11 +110,11 @@ function parseOpenAPIFile(text: string, options?: any): OpenAPINode {
isOpenAPI: true,
content: parsed,
format: format,
}
};
case false:
return {
isOpenAPI: false,
}
};
}
}
@@ -125,7 +126,7 @@ function parseOpenAPIFile(text: string, options?: any): OpenAPINode {
* Detects if a file is an OpenAPI-related file based on content and structure
*/
function isOpenAPIFile(content: any, filePath?: string): boolean {
if (!content || typeof content !== 'object') {
if (!content || typeof content !== "object") {
return false;
}
@@ -140,24 +141,32 @@ function isOpenAPIFile(content: any, filePath?: string): boolean {
const path = filePath.toLowerCase();
// Check for component directory patterns
if (path.includes('/components/') ||
path.includes('/schemas/') ||
path.includes('/parameters/') ||
path.includes('/responses/') ||
path.includes('/requestbodies/') ||
path.includes('/headers/') ||
path.includes('/examples/') ||
path.includes('/securityschemes/') ||
path.includes('/links/') ||
path.includes('/callbacks/') ||
path.includes('/webhooks/') ||
path.includes('/paths/')) {
if (
path.includes("/components/") ||
path.includes("/schemas/") ||
path.includes("/parameters/") ||
path.includes("/responses/") ||
path.includes("/requestbodies/") ||
path.includes("/headers/") ||
path.includes("/examples/") ||
path.includes("/securityschemes/") ||
path.includes("/links/") ||
path.includes("/callbacks/") ||
path.includes("/webhooks/") ||
path.includes("/paths/")
) {
return true;
}
}
// Check for component-like structures (only if we have strong indicators)
if (content.components || content.definitions || content.parameters || content.responses || content.securityDefinitions) {
if (
content.components ||
content.definitions ||
content.parameters ||
content.responses ||
content.securityDefinitions
) {
return true;
}
@@ -168,7 +177,16 @@ function isOpenAPIFile(content: any, filePath?: string): boolean {
// Check for schema-like structures (but be more strict)
// Only accept if we have strong schema indicators
if (isSchemaObject(content) && (content.$ref || content.allOf || content.oneOf || content.anyOf || content.not || content.properties || content.items)) {
if (
isSchemaObject(content) &&
(content.$ref ||
content.allOf ||
content.oneOf ||
content.anyOf ||
content.not ||
content.properties ||
content.items)
) {
return true;
}
@@ -225,9 +243,23 @@ function isOpenAPIFile(content: any, filePath?: string): boolean {
// Additional strict check: reject objects that look like generic data
// If an object only has simple properties like name, age, etc. without any OpenAPI structure, reject it
const keys = Object.keys(content);
const hasOnlyGenericProperties = keys.every(key =>
!key.startsWith('x-') && // Not a custom extension
!['openapi', 'swagger', 'info', 'paths', 'components', 'definitions', 'parameters', 'responses', 'securityDefinitions', 'tags', 'servers', 'webhooks'].includes(key)
const hasOnlyGenericProperties = keys.every(
(key) =>
!key.startsWith("x-") && // Not a custom extension
![
"openapi",
"swagger",
"info",
"paths",
"components",
"definitions",
"parameters",
"responses",
"securityDefinitions",
"tags",
"servers",
"webhooks",
].includes(key)
);
if (hasOnlyGenericProperties) {
@@ -242,38 +274,40 @@ function isOpenAPIFile(content: any, filePath?: string): boolean {
* Detects if an object represents a path with operations
*/
function isPathObject(obj: any): boolean {
if (!obj || typeof obj !== 'object') {
if (!obj || typeof obj !== "object") {
return false;
}
const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
return Object.keys(obj).some(key => httpMethods.includes(key.toLowerCase()));
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
return Object.keys(obj).some((key) => httpMethods.includes(key.toLowerCase()));
}
export const languages: SupportLanguage[] = [
{
name: 'openapi',
name: "openapi",
extensions: [
// Accepting all JSON and YAML files so that component files used by $ref work
'.json', '.yaml', '.yml'
".json",
".yaml",
".yml",
],
parsers: ['openapi-parser'],
parsers: ["openapi-parser"],
},
] as const;
export const parsers: Record<string, Parser> = {
'openapi-parser': {
"openapi-parser": {
parse: (text: string, options?: any): OpenAPINode => {
return parseOpenAPIFile(text, options);
},
astFormat: 'openapi-ast',
astFormat: "openapi-ast",
locStart: (node: OpenAPINode) => 0,
locEnd: (node: OpenAPINode) => node.content?.length || 0,
},
}
};
export const printers: Record<string, Printer> = {
'openapi-ast': {
"openapi-ast": {
print: (path: AstPath, options: ParserOptions, print: PrintFn): string => {
const node = path.getNode();
if (!node.isOpenAPI || node.isOpenAPI === false) {
@@ -283,23 +317,105 @@ export const printers: Record<string, Printer> = {
return formatOpenAPI(node.content, node.format, options);
},
},
};
/**
* Formats markdown strings using Prettier's markdown parser and printer
*
* This uses Prettier's actual markdown formatting implementation, ensuring
* that markdown in OpenAPI descriptions is formatted exactly as Prettier would format it.
*/
function formatMarkdownSync(markdown: string, options?: OpenAPIPluginOptions): string {
if (!markdown || typeof markdown !== "string") {
return markdown;
}
// Skip formatting if disabled
if (options?.formatMarkdown === false) {
return markdown;
}
// Trim to avoid formatting whitespace-only strings
const trimmed = markdown.trim();
if (trimmed.length === 0) {
return markdown;
}
try {
// Use Prettier's markdown formatter
// Dynamic require to avoid issues during build
const formatModule = require("./prettier-markdown/format-markdown.js");
const formatted = formatModule.formatMarkdown(trimmed, {
printWidth: options?.printWidth || 80,
tabWidth: options?.tabWidth || 2,
proseWrap: "preserve",
singleQuote: false,
});
return formatted;
} catch (error) {
// If Prettier's formatter fails, fall back to basic normalization
// This ensures we always return valid markdown
return trimmed;
}
}
/**
* Recursively formats all description and summary fields that may contain markdown
*/
function formatMarkdownFields(obj: any, options?: OpenAPIPluginOptions): any {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => formatMarkdownFields(item, options));
}
const formatted: any = {};
for (const [key, value] of Object.entries(obj)) {
// Fields that commonly contain markdown in OpenAPI
const markdownFields = ["description", "summary"];
if (markdownFields.includes(key) && typeof value === "string" && value.trim().length > 0) {
// Format markdown synchronously
formatted[key] = formatMarkdownSync(value, options);
} else if (typeof value === "object" && value !== null) {
// Recursively process nested objects
formatted[key] = formatMarkdownFields(value, options);
} else {
// Keep other values as-is
formatted[key] = value;
}
}
return formatted;
}
/**
* Unified formatter that outputs in the detected format
*/
function formatOpenAPI(content: any, format: 'json' | 'yaml', options?: OpenAPIPluginOptions): string {
function formatOpenAPI(
content: any,
format: "json" | "yaml",
options?: OpenAPIPluginOptions
): string {
// Sort keys for better organization
const sortedContent = sortOpenAPIKeys(content);
// Format markdown in description and summary fields
const contentWithFormattedMarkdown = formatMarkdownFields(sortedContent, options);
switch (format) {
case 'json':
return JSON.stringify(sortedContent, null, options?.tabWidth || 2);
case 'yaml':
case "json":
return JSON.stringify(contentWithFormattedMarkdown, null, options?.tabWidth || 2);
case "yaml":
// Format YAML with proper indentation and line breaks
return yaml.dump(sortedContent, {
// Use lineWidth: -1 to disable automatic line wrapping for better markdown preservation
return yaml.dump(contentWithFormattedMarkdown, {
indent: options?.tabWidth || 2,
lineWidth: options?.printWidth || 80,
lineWidth: -1, // Disable line width to preserve markdown formatting
noRefs: true,
quotingType: '"',
forceQuotes: false,
@@ -310,28 +426,28 @@ function formatOpenAPI(content: any, format: 'json' | 'yaml', options?: OpenAPIP
function sortOpenAPIKeys(obj: any): any {
// Special handling: if root object is a referenced OpenAPI object (for referenced files)
// Check for ref-able object types before checking for root-level keys
let contextKey = 'top-level';
let contextKey = "top-level";
// Skip detection if it's a full OpenAPI spec (has openapi/swagger)
if (!('openapi' in obj) && !('swagger' in obj)) {
if (!("openapi" in obj) && !("swagger" in obj)) {
// Check for all ref-able object types in priority order
// Check more specific types first to avoid false positives
if (isLinkObject(obj)) {
contextKey = 'link';
contextKey = "link";
} else if (isOperationObject(obj)) {
contextKey = 'operation';
contextKey = "operation";
} else if (isSchemaObject(obj)) {
contextKey = 'schema';
contextKey = "schema";
} else if (isParameterObject(obj)) {
contextKey = 'parameter';
contextKey = "parameter";
} else if (isResponseObject(obj)) {
contextKey = 'response';
contextKey = "response";
} else if (isHeaderObject(obj)) {
contextKey = 'header';
contextKey = "header";
} else if (isPathItemObject(obj)) {
contextKey = 'pathItem';
contextKey = "pathItem";
} else if (isRequestBodyObject(obj)) {
contextKey = 'requestBody';
contextKey = "requestBody";
} else {
// Fall back to standard context detection
contextKey = getContextKey("", obj);
@@ -358,29 +474,29 @@ function sortOpenAPIKeys(obj: any): any {
}
// Enhanced sorting for nested OpenAPI structures
function sortOpenAPIKeysEnhanced(obj: any, path: string = ''): any {
if (typeof obj !== 'object' || obj === null) {
function sortOpenAPIKeysEnhanced(obj: any, path: string = ""): any {
if (typeof obj !== "object" || obj === null) {
return obj;
}
// Handle arrays by recursively sorting each element
if (Array.isArray(obj)) {
const sortedObjs = []
const sortedObjs = [];
for (let i = 0; i < obj.length; i++) {
sortedObjs.push(sortOpenAPIKeysEnhanced(obj[i], `${path}[${i}]`));
}
if (path === 'tags') {
if (path === "tags") {
return sortedObjs.sort((a, b) => sortTags(a, b));
}
// Sort parameter arrays so $ref items come first
// Check if this array is a parameters array (path ends with '.parameters' or is 'parameters')
if (path.endsWith('.parameters') || path === 'parameters') {
if (path.endsWith(".parameters") || path === "parameters") {
return sortedObjs.sort((a, b) => {
const aHasRef = a && typeof a === 'object' && '$ref' in a;
const bHasRef = b && typeof b === 'object' && '$ref' in b;
const aHasRef = a && typeof a === "object" && "$ref" in a;
const bHasRef = b && typeof b === "object" && "$ref" in b;
if (aHasRef && !bHasRef) return -1; // $ref comes first
if (!aHasRef && bHasRef) return 1; // $ref comes first
@@ -397,9 +513,9 @@ function sortOpenAPIKeysEnhanced(obj: any, path: string = ''): any {
const sortedKeys = Object.keys(obj).sort((a, b) => {
switch (path) {
case 'paths':
case "paths":
return sortPathKeys(a, b);
case 'responses':
case "responses":
return sortResponseCodes(a, b);
default:
return sortKeys(a, b, standardKeys, customExtensions);
@@ -429,7 +545,7 @@ function sortPathKeys(a: string, b: string): number {
type Tag = {
name: string;
}
};
function sortTags(a: Tag, b: Tag): number {
// Sort tags by name
@@ -451,7 +567,7 @@ function sortResponseCodes(a: string, b: string): number {
//#region Object type detection functions
function isOperationObject(obj: any): boolean {
if (!obj || typeof obj !== 'object') {
if (!obj || typeof obj !== "object") {
return false;
}
@@ -459,13 +575,15 @@ function isOperationObject(obj: any): boolean {
// HTTP methods indicate a path item, not an operation
// Strong indicators: operationId or responses (required fields in operations)
// Secondary indicators: requestBody, callbacks (operation-specific)
if ('operationId' in obj || 'responses' in obj) {
if ("operationId" in obj || "responses" in obj) {
return true;
}
// If it has both requestBody or callbacks (operation-specific) AND other operation keys
if (('requestBody' in obj || 'callbacks' in obj) &&
('parameters' in obj || 'security' in obj || 'servers' in obj)) {
if (
("requestBody" in obj || "callbacks" in obj) &&
("parameters" in obj || "security" in obj || "servers" in obj)
) {
return true;
}
@@ -473,108 +591,151 @@ function isOperationObject(obj: any): boolean {
}
function isParameterObject(obj: any): boolean {
return obj && typeof obj === 'object' && 'name' in obj && 'in' in obj;
return obj && typeof obj === "object" && "name" in obj && "in" in obj;
}
function isSchemaObject(obj: any): boolean {
if (!obj || typeof obj !== 'object') {
if (!obj || typeof obj !== "object") {
return false;
}
// Check for JSON Schema keywords - be very strict
const hasSchemaKeywords = '$ref' in obj || 'allOf' in obj || 'oneOf' in obj || 'anyOf' in obj || 'not' in obj;
const hasValidType = 'type' in obj && obj.type && ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'].includes(obj.type);
const hasSchemaKeywords =
"$ref" in obj || "allOf" in obj || "oneOf" in obj || "anyOf" in obj || "not" in obj;
const hasValidType =
"type" in obj &&
obj.type &&
["object", "array", "string", "number", "integer", "boolean", "null"].includes(obj.type);
// Only return true if we have clear schema indicators
// Must have either schema keywords OR valid type with schema properties
// Also require additional schema-specific properties to be more strict
return hasSchemaKeywords || (hasValidType && ('properties' in obj || 'items' in obj || 'enum' in obj || 'format' in obj || 'pattern' in obj));
return (
hasSchemaKeywords ||
(hasValidType &&
("properties" in obj ||
"items" in obj ||
"enum" in obj ||
"format" in obj ||
"pattern" in obj))
);
}
function isResponseObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('description' in obj || 'content' in obj);
return obj && typeof obj === "object" && ("description" in obj || "content" in obj);
}
function isSecuritySchemeObject(obj: any): boolean {
return obj && typeof obj === 'object' && 'type' in obj &&
['apiKey', 'http', 'oauth2', 'openIdConnect'].includes(obj.type);
return (
obj &&
typeof obj === "object" &&
"type" in obj &&
["apiKey", "http", "oauth2", "openIdConnect"].includes(obj.type)
);
}
function isServerObject(obj: any): boolean {
return obj && typeof obj === 'object' && 'url' in obj;
return obj && typeof obj === "object" && "url" in obj;
}
function isTagObject(obj: any): boolean {
return obj && typeof obj === 'object' && 'name' in obj && typeof obj.name === 'string' &&
return (
obj &&
typeof obj === "object" &&
"name" in obj &&
typeof obj.name === "string" &&
(Object.keys(obj).length === 1 || // Only name
'description' in obj || // name + description
'externalDocs' in obj); // name + externalDocs
"description" in obj || // name + description
"externalDocs" in obj)
); // name + externalDocs
}
function isExternalDocsObject(obj: any): boolean {
return obj && typeof obj === 'object' && 'url' in obj;
return obj && typeof obj === "object" && "url" in obj;
}
function isWebhookObject(obj: any): boolean {
const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
return obj && typeof obj === 'object' && Object.keys(obj).some(key => httpMethods.includes(key.toLowerCase()));
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
return (
obj &&
typeof obj === "object" &&
Object.keys(obj).some((key) => httpMethods.includes(key.toLowerCase()))
);
}
function isPathItemObject(obj: any): boolean {
const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
return obj && typeof obj === 'object' && Object.keys(obj).some(key => httpMethods.includes(key.toLowerCase()));
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
return (
obj &&
typeof obj === "object" &&
Object.keys(obj).some((key) => httpMethods.includes(key.toLowerCase()))
);
}
function isRequestBodyObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('content' in obj || 'description' in obj);
return obj && typeof obj === "object" && ("content" in obj || "description" in obj);
}
function isMediaTypeObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('schema' in obj || 'example' in obj || 'examples' in obj);
return (
obj && typeof obj === "object" && ("schema" in obj || "example" in obj || "examples" in obj)
);
}
function isEncodingObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('contentType' in obj || 'style' in obj || 'explode' in obj);
return (
obj && typeof obj === "object" && ("contentType" in obj || "style" in obj || "explode" in obj)
);
}
function isHeaderObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('description' in obj || 'schema' in obj || 'required' in obj);
return (
obj && typeof obj === "object" && ("description" in obj || "schema" in obj || "required" in obj)
);
}
function isLinkObject(obj: any): boolean {
if (!obj || typeof obj !== 'object') {
if (!obj || typeof obj !== "object") {
return false;
}
// Link objects have operationRef OR operationId, but NOT responses (which indicates an operation)
return ('operationRef' in obj || ('operationId' in obj && !('responses' in obj)));
return "operationRef" in obj || ("operationId" in obj && !("responses" in obj));
}
function isExampleObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('summary' in obj || 'value' in obj || 'externalValue' in obj);
return (
obj && typeof obj === "object" && ("summary" in obj || "value" in obj || "externalValue" in obj)
);
}
function isDiscriminatorObject(obj: any): boolean {
return obj && typeof obj === 'object' && 'propertyName' in obj;
return obj && typeof obj === "object" && "propertyName" in obj;
}
function isXMLObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('name' in obj || 'namespace' in obj || 'attribute' in obj);
return (
obj && typeof obj === "object" && ("name" in obj || "namespace" in obj || "attribute" in obj)
);
}
function isContactObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('name' in obj || 'url' in obj || 'email' in obj);
return obj && typeof obj === "object" && ("name" in obj || "url" in obj || "email" in obj);
}
function isLicenseObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('name' in obj || 'identifier' in obj || 'url' in obj);
return obj && typeof obj === "object" && ("name" in obj || "identifier" in obj || "url" in obj);
}
function isOAuthFlowObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('authorizationUrl' in obj || 'tokenUrl' in obj || 'scopes' in obj);
return (
obj &&
typeof obj === "object" &&
("authorizationUrl" in obj || "tokenUrl" in obj || "scopes" in obj)
);
}
function isServerVariableObject(obj: any): boolean {
return obj && typeof obj === 'object' && ('enum' in obj || 'default' in obj);
return obj && typeof obj === "object" && ("enum" in obj || "default" in obj);
}
//#endregion
@@ -588,7 +749,12 @@ function isServerVariableObject(obj: any): boolean {
* @param customExtensions Custom extension positions
* @returns Comparison result
*/
function sortKeys(a: string, b: string, standardKeys: readonly string[], customExtensions: Record<string, number> = {}): number {
function sortKeys(
a: string,
b: string,
standardKeys: readonly string[],
customExtensions: Record<string, number> = {}
): number {
const aCustomPos = customExtensions[a];
const bCustomPos = customExtensions[b];
@@ -646,117 +812,144 @@ function sortKeys(a: string, b: string, standardKeys: readonly string[], customE
function getContextKey(path: string, obj: any): string {
// Determine the context based on path and object properties
if (path === 'info') return 'info';
if (path === 'components') return 'components';
if (path === 'servers' || path.startsWith('servers[')) return 'server';
if (path === 'tags' || path.startsWith('tags[')) return 'tag';
if (path === 'externalDocs') return 'externalDocs';
if (path === 'webhooks') return 'webhook';
if (path === 'definitions') return 'definitions';
if (path === 'securityDefinitions') return 'securityDefinitions';
if (path === "info") return "info";
if (path === "components") return "components";
if (path === "servers" || path.startsWith("servers[")) return "server";
if (path === "tags" || path.startsWith("tags[")) return "tag";
if (path === "externalDocs") return "externalDocs";
if (path === "webhooks") return "webhook";
if (path === "definitions") return "definitions";
if (path === "securityDefinitions") return "securityDefinitions";
// Check if this is a path operation (e.g., "paths./users.get")
if (path.includes('.') && path.split('.').length >= 3) {
const pathParts = path.split('.');
if (path.includes(".") && path.split(".").length >= 3) {
const pathParts = path.split(".");
const lastPart = pathParts[pathParts.length - 1];
const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
if (httpMethods.includes(lastPart.toLowerCase())) {
return 'operation';
return "operation";
}
}
// Handle nested paths for components
if (path.startsWith('components.')) {
if (path.includes('schemas.')) return 'schema';
if (path.includes('parameters.')) return 'parameter';
if (path.includes('responses.')) return 'response';
if (path.includes('securitySchemes.')) return 'securityScheme';
if (path.includes('requestBodies.')) return 'requestBody';
if (path.includes('headers.')) return 'header';
if (path.includes('examples.')) return 'example';
if (path.includes('links.')) return 'link';
if (path.includes('callbacks.')) return 'callback';
if (path.includes('pathItems.')) return 'pathItem';
if (path.startsWith("components.")) {
if (path.includes("schemas.")) return "schema";
if (path.includes("parameters.")) return "parameter";
if (path.includes("responses.")) return "response";
if (path.includes("securitySchemes.")) return "securityScheme";
if (path.includes("requestBodies.")) return "requestBody";
if (path.includes("headers.")) return "header";
if (path.includes("examples.")) return "example";
if (path.includes("links.")) return "link";
if (path.includes("callbacks.")) return "callback";
if (path.includes("pathItems.")) return "pathItem";
}
// Handle nested paths for Swagger 2.0
if (path.startsWith('definitions.')) return 'definitions';
if (path.startsWith('securityDefinitions.')) return 'securityDefinitions';
if (path.startsWith("definitions.")) return "definitions";
if (path.startsWith("securityDefinitions.")) return "securityDefinitions";
// Handle nested paths for operations (parameters, responses, etc.)
if (path.includes('.parameters.') && path.split('.').length > 3) return 'parameter';
if (path.includes('.responses.') && path.split('.').length > 3) return 'response';
if (path.includes('.requestBody.')) return 'requestBody';
if (path.includes('.headers.')) return 'header';
if (path.includes('.examples.')) return 'example';
if (path.includes('.links.')) return 'link';
if (path.includes('.content.')) return 'mediaType';
if (path.includes('.encoding.')) return 'encoding';
if (path.includes('.discriminator.')) return 'discriminator';
if (path.includes('.xml.')) return 'xml';
if (path.includes('.contact.')) return 'contact';
if (path.includes('.license.')) return 'license';
if (path.includes('.flows.')) return 'oauthFlow';
if (path.includes('.variables.')) return 'serverVariable';
if (path.includes(".parameters.") && path.split(".").length > 3) return "parameter";
if (path.includes(".responses.") && path.split(".").length > 3) return "response";
if (path.includes(".requestBody.")) return "requestBody";
if (path.includes(".headers.")) return "header";
if (path.includes(".examples.")) return "example";
if (path.includes(".links.")) return "link";
if (path.includes(".content.")) return "mediaType";
if (path.includes(".encoding.")) return "encoding";
if (path.includes(".discriminator.")) return "discriminator";
if (path.includes(".xml.")) return "xml";
if (path.includes(".contact.")) return "contact";
if (path.includes(".license.")) return "license";
if (path.includes(".flows.")) return "oauthFlow";
if (path.includes(".variables.")) return "serverVariable";
// Check object types as fallback
// Only check for operation if path is not empty (not at root level)
// Root-level objects should not be detected as operations unless they're truly operations
// (handled separately in sortOpenAPIKeys for referenced operation files)
if (path && isOperationObject(obj)) return 'operation';
if (isParameterObject(obj)) return 'parameter';
if (isSchemaObject(obj)) return 'schema';
if (isResponseObject(obj)) return 'response';
if (isSecuritySchemeObject(obj)) return 'securityScheme';
if (isServerObject(obj)) return 'server';
if (isTagObject(obj)) return 'tag';
if (isExternalDocsObject(obj)) return 'externalDocs';
if (isWebhookObject(obj)) return 'webhook';
if (isPathItemObject(obj)) return 'pathItem';
if (isRequestBodyObject(obj)) return 'requestBody';
if (isMediaTypeObject(obj)) return 'mediaType';
if (isEncodingObject(obj)) return 'encoding';
if (isHeaderObject(obj)) return 'header';
if (isLinkObject(obj)) return 'link';
if (isExampleObject(obj)) return 'example';
if (isDiscriminatorObject(obj)) return 'discriminator';
if (isXMLObject(obj)) return 'xml';
if (isContactObject(obj)) return 'contact';
if (isLicenseObject(obj)) return 'license';
if (isOAuthFlowObject(obj)) return 'oauthFlow';
if (isServerVariableObject(obj)) return 'serverVariable';
if (path && isOperationObject(obj)) return "operation";
if (isParameterObject(obj)) return "parameter";
if (isSchemaObject(obj)) return "schema";
if (isResponseObject(obj)) return "response";
if (isSecuritySchemeObject(obj)) return "securityScheme";
if (isServerObject(obj)) return "server";
if (isTagObject(obj)) return "tag";
if (isExternalDocsObject(obj)) return "externalDocs";
if (isWebhookObject(obj)) return "webhook";
if (isPathItemObject(obj)) return "pathItem";
if (isRequestBodyObject(obj)) return "requestBody";
if (isMediaTypeObject(obj)) return "mediaType";
if (isEncodingObject(obj)) return "encoding";
if (isHeaderObject(obj)) return "header";
if (isLinkObject(obj)) return "link";
if (isExampleObject(obj)) return "example";
if (isDiscriminatorObject(obj)) return "discriminator";
if (isXMLObject(obj)) return "xml";
if (isContactObject(obj)) return "contact";
if (isLicenseObject(obj)) return "license";
if (isOAuthFlowObject(obj)) return "oauthFlow";
if (isServerVariableObject(obj)) return "serverVariable";
return 'top-level';
return "top-level";
}
function getStandardKeysForContext(contextKey: string): readonly string[] {
switch (contextKey) {
case 'info': return InfoKeys;
case 'components': return ComponentsKeys;
case 'operation': return OperationKeys;
case 'parameter': return ParameterKeys;
case 'schema': return SchemaKeys;
case 'response': return ResponseKeys;
case 'securityScheme': return SecuritySchemeKeys;
case 'server': return ServerKeys;
case 'tag': return TagKeys;
case 'externalDocs': return ExternalDocsKeys;
case 'webhook': return WebhookKeys;
case 'pathItem': return PathItemKeys;
case 'requestBody': return RequestBodyKeys;
case 'mediaType': return MediaTypeKeys;
case 'encoding': return EncodingKeys;
case 'header': return HeaderKeys;
case 'link': return LinkKeys;
case 'example': return ExampleKeys;
case 'discriminator': return DiscriminatorKeys;
case 'xml': return XMLKeys;
case 'contact': return ContactKeys;
case 'license': return LicenseKeys;
case 'oauthFlow': return OAuthFlowKeys;
case 'serverVariable': return ServerVariableKeys;
case 'definitions': return SchemaKeys; // Definitions use schema keys
case 'securityDefinitions': return SecuritySchemeKeys; // Security definitions use security scheme keys
default: return RootKeys;
case "info":
return InfoKeys;
case "components":
return ComponentsKeys;
case "operation":
return OperationKeys;
case "parameter":
return ParameterKeys;
case "schema":
return SchemaKeys;
case "response":
return ResponseKeys;
case "securityScheme":
return SecuritySchemeKeys;
case "server":
return ServerKeys;
case "tag":
return TagKeys;
case "externalDocs":
return ExternalDocsKeys;
case "webhook":
return WebhookKeys;
case "pathItem":
return PathItemKeys;
case "requestBody":
return RequestBodyKeys;
case "mediaType":
return MediaTypeKeys;
case "encoding":
return EncodingKeys;
case "header":
return HeaderKeys;
case "link":
return LinkKeys;
case "example":
return ExampleKeys;
case "discriminator":
return DiscriminatorKeys;
case "xml":
return XMLKeys;
case "contact":
return ContactKeys;
case "license":
return LicenseKeys;
case "oauthFlow":
return OAuthFlowKeys;
case "serverVariable":
return ServerVariableKeys;
case "definitions":
return SchemaKeys; // Definitions use schema keys
case "securityDefinitions":
return SecuritySchemeKeys; // Security definitions use security scheme keys
default:
return RootKeys;
}
}

View File

@@ -0,0 +1,54 @@
# Prettier Markdown Integration
This directory contains Prettier's markdown language implementation, adapted for use within this plugin.
## Structure
### Core Files (from Prettier)
These files are copied from Prettier's `src/language-markdown` directory:
- `parser-markdown.js` - Markdown parser
- `printer-markdown.js` - Markdown printer
- `clean.js` - AST cleaning utilities
- `utils.js` - Utility functions
- `constants.evaluate.js` - Constants
- `print/` - Printing utilities
- `unified-plugins/` - Unified/Remark plugins
- Other supporting files
### Adapter Files (created for this plugin)
These files adapt Prettier's internal dependencies:
- `adapter-document-builders.js` - Adapts Prettier's document builders
- `adapter-document-utils.js` - Adapts Prettier's document utilities
- `adapter-document-constants.js` - Adapts Prettier's document constants
- `adapter-prettier-utils.js` - Adapts Prettier's utility functions
- `adapter-pragma.js` - Adapts pragma handling (simplified)
### Integration Files
- `format-markdown.ts` - Type-safe wrapper for formatting markdown
- `options.js` - Markdown formatting options (adapted from Prettier)
## Updating from Prettier
When Prettier updates its markdown implementation:
1. **Copy updated files** from `prettier/src/language-markdown/` to this directory
2. **Update adapter files** if Prettier's internal structure changed:
- Check if document builders path changed → update `adapter-document-builders.js`
- Check if document utils path changed → update `adapter-document-utils.js`
- Check if utility functions changed → update `adapter-prettier-utils.js`
3. **Update imports** in copied files to use adapter files:
- `../document/builders.js``./adapter-document-builders.js`
- `../document/utils.js``./adapter-document-utils.js`
- `../document/constants.js``./adapter-document-constants.js`
- `../utils/*``./adapter-prettier-utils.js`
- `../common/common-options.evaluate.js` → update `options.js`
- `../main/front-matter/index.js` → update `adapter-pragma.js` or `clean.js`
- `../utils/pragma/pragma.evaluate.js` → update `adapter-pragma.js`
4. **Test** that markdown formatting still works correctly
## Dependencies
The adapter files attempt to access Prettier's internal APIs at runtime. If Prettier's internal structure changes significantly, you may need to update the adapter files to match.
The interfaces in the adapter files are designed to be similar to Prettier's actual structure, making updates easier.

View File

@@ -0,0 +1,55 @@
/**
* Adapter for Prettier's document builders
*
* This file attempts to import Prettier's document builders from internal APIs.
* Update this file when Prettier's internal structure changes.
*/
let builders: any = null;
try {
const prettier = require("prettier");
// Try multiple paths to access document builders
if (prettier.__internal?.document?.builders) {
builders = prettier.__internal.document.builders;
} else {
// Try to require directly (may work in plugin context)
try {
builders = require("prettier/internal/document/builders");
} catch {
// Fallback: try alternative paths
try {
const doc = require("prettier/doc");
if (doc) {
builders = doc;
}
} catch {
// Not accessible
}
}
}
} catch {
// Builders not accessible
}
// Export what we found, or throw if not available
if (!builders) {
throw new Error(
"Prettier document builders not accessible. " +
"Markdown formatting requires Prettier's internal document builders."
);
}
export const {
align,
fill,
group,
hardline,
indent,
line,
literalline,
markAsRoot,
softline,
} = builders;

View File

@@ -0,0 +1,33 @@
/**
* Adapter for Prettier's document constants
*
* This file attempts to import Prettier's document constants from internal APIs.
* Update this file when Prettier's internal structure changes.
*/
let constants: any = null;
try {
const prettier = require("prettier");
if (prettier.__internal?.document?.constants) {
constants = prettier.__internal.document.constants;
} else {
try {
constants = require("prettier/internal/document/constants");
} catch {
// Constants may not be accessible, provide a fallback
constants = {
DOC_TYPE_STRING: "doc-type-string",
};
}
}
} catch {
// Provide fallback
constants = {
DOC_TYPE_STRING: "doc-type-string",
};
}
export const { DOC_TYPE_STRING } = constants;

View File

@@ -0,0 +1,39 @@
/**
* Adapter for Prettier's document utils
*
* This file attempts to import Prettier's document utilities from internal APIs.
* Update this file when Prettier's internal structure changes.
*/
let utils: any = null;
try {
const prettier = require("prettier");
if (prettier.__internal?.document?.utils) {
utils = prettier.__internal.document.utils;
} else {
try {
utils = require("prettier/internal/document/utils");
} catch {
try {
const docUtils = require("prettier/doc");
utils = docUtils;
} catch {
// Not accessible
}
}
}
} catch {
// Utils not accessible
}
if (!utils) {
throw new Error(
"Prettier document utils not accessible. " +
"Markdown formatting requires Prettier's internal document utilities."
);
}
export const { getDocType, replaceEndOfLine } = utils;

View File

@@ -0,0 +1,48 @@
/**
* Adapter for Prettier's pragma utilities
*
* Simplified version that doesn't depend on Prettier's internal pragma system.
* Update this file when Prettier's pragma behavior changes.
*/
// Simplified pragma regexes based on Prettier's implementation
const MARKDOWN_HAS_PRAGMA_REGEXP = /^<!--\s*@(prettier|format)\s*-->$/m;
const MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP = /^<!--\s*prettier-ignore(?:-(start|end))?\s*-->$/m;
const FORMAT_PRAGMA_TO_INSERT = "format";
/**
* Simple front matter parser (minimal implementation)
*/
function parseFrontMatter(text: string) {
const yamlMatch = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
if (yamlMatch) {
return {
content: text.slice(yamlMatch[0].length),
frontMatter: {
raw: yamlMatch[0],
end: { index: yamlMatch[0].length },
},
};
}
return { content: text, frontMatter: null };
}
const hasPragma = (text: string) =>
parseFrontMatter(text).content.trimStart().match(MARKDOWN_HAS_PRAGMA_REGEXP)
?.index === 0;
const hasIgnorePragma = (text: string) =>
parseFrontMatter(text)
.content.trimStart()
.match(MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP)?.index === 0;
const insertPragma = (text: string) => {
const { frontMatter } = parseFrontMatter(text);
const pragma = `<!-- @${FORMAT_PRAGMA_TO_INSERT} -->`;
return frontMatter
? `${frontMatter.raw}\n\n${pragma}\n\n${text.slice(frontMatter.end.index)}`
: `${pragma}\n\n${text}`;
};
export { hasIgnorePragma, hasPragma, insertPragma };

View File

@@ -0,0 +1,130 @@
/**
* Adapter for Prettier's internal document builders and utilities
*
* This file attempts to access Prettier's internal APIs that are needed
* by the markdown parser and printer. These adapters provide a type-safe
* way to access Prettier internals when available.
*
* Note: This file can be updated when Prettier's internal structure changes.
* The interfaces should remain similar to allow easy updates.
*/
/**
* Attempts to import Prettier's document builders
* @returns {Promise<typeof import('prettier/internal/document/builders')> | null>}
*/
export function getDocumentBuilders() {
try {
// Try to require Prettier's document builders
// Prettier 3.x structure
const prettier = require("prettier");
// Path 1: Check if accessible via __internal
if (prettier.__internal?.document?.builders) {
return prettier.__internal.document.builders;
}
// Path 2: Try to require directly (may work in some contexts)
try {
return require("prettier/internal/document/builders");
} catch {
// Not accessible
}
return null;
} catch {
return null;
}
}
/**
* Attempts to import Prettier's document utils
*/
export function getDocumentUtils() {
try {
const prettier = require("prettier");
if (prettier.__internal?.document?.utils) {
return prettier.__internal.document.utils;
}
try {
return require("prettier/internal/document/utils");
} catch {
return null;
}
} catch {
return null;
}
}
/**
* Attempts to import Prettier's document constants
*/
export function getDocumentConstants() {
try {
const prettier = require("prettier");
if (prettier.__internal?.document?.constants) {
return prettier.__internal.document.constants;
}
try {
return require("prettier/internal/document/constants");
} catch {
return null;
}
} catch {
return null;
}
}
/**
* Attempts to import Prettier's utility functions
*/
export function getPrettierUtils() {
try {
const prettier = require("prettier");
if (prettier.__internal?.utils) {
return prettier.__internal.utils;
}
return null;
} catch {
return null;
}
}
/**
* Attempts to get Prettier's doc-to-string printer
* This is needed to convert Doc objects to strings
*/
export function getDocPrinter() {
try {
const prettier = require("prettier");
// Try multiple paths
if (prettier.__internal?.doc?.printDocToString) {
return prettier.__internal.doc.printDocToString;
}
if (prettier.__internal?.docPrinter?.formatDoc) {
return prettier.__internal.docPrinter.formatDoc;
}
try {
const docUtils = require("prettier/internal/doc");
if (docUtils?.printDocToString) {
return docUtils.printDocToString;
}
} catch {
// Not accessible
}
return null;
} catch {
return null;
}
}

View File

@@ -0,0 +1,72 @@
/**
* Adapter for Prettier's utility functions
*
* This file attempts to import Prettier's utility functions from internal APIs.
* Update this file when Prettier's internal structure changes.
*/
let utils: any = null;
try {
const prettier = require("prettier");
if (prettier.__internal?.utils) {
utils = prettier.__internal.utils;
} else {
try {
// Try alternative paths
utils = require("prettier/internal/utils");
} catch {
// Utils may not be accessible
}
}
} catch {
// Utils not accessible
}
// Provide fallback implementations if utils aren't accessible
if (!utils) {
// Minimal fallback implementations
utils = {
getMaxContinuousCount: (str: string, char: string) => {
let max = 0;
let current = 0;
for (const c of str) {
if (c === char) {
current++;
max = Math.max(max, current);
} else {
current = 0;
}
}
return max;
},
getMinNotPresentContinuousCount: (str: string, char: string) => {
let count = 1;
while (str.includes(char.repeat(count))) {
count++;
}
return count;
},
getPreferredQuote: (str: string, singleQuote: boolean) => {
if (singleQuote) return "'";
const hasSingle = str.includes("'");
const hasDouble = str.includes('"');
if (hasSingle && !hasDouble) return '"';
return "'";
},
};
}
export const getMaxContinuousCount = utils.getMaxContinuousCount;
export const getMinNotPresentContinuousCount = utils.getMinNotPresentContinuousCount;
export const getPreferredQuote = utils.getPreferredQuote;
// UnexpectedNodeError may be in utils or separate
export class UnexpectedNodeError extends Error {
constructor(node: any, language: string) {
super(`Unexpected node type: ${node.type} in ${language}`);
this.name = "UnexpectedNodeError";
}
}

View File

@@ -0,0 +1,91 @@
import collapseWhiteSpace from "collapse-white-space";
import { hasPragma } from "./adapter-pragma.js";
// Simplified front matter check
function isFrontMatter(node: any): boolean {
return node?.type === "yaml" || node?.type === "toml";
}
const ignoredProperties = new Set([
"position",
"raw", // front-matter
]);
function clean(original, cloned, parent) {
// for codeblock
if (
original.type === "code" ||
original.type === "yaml" ||
original.type === "import" ||
original.type === "export" ||
original.type === "jsx"
) {
delete cloned.value;
}
if (original.type === "list") {
delete cloned.isAligned;
}
if (original.type === "list" || original.type === "listItem") {
delete cloned.spread;
}
// texts can be splitted or merged
if (original.type === "text") {
return null;
}
if (original.type === "inlineCode") {
cloned.value = original.value.replaceAll("\n", " ");
}
if (original.type === "wikiLink") {
cloned.value = original.value.trim().replaceAll(/[\t\n]+/gu, " ");
}
if (
original.type === "definition" ||
original.type === "linkReference" ||
original.type === "imageReference"
) {
cloned.label = collapseWhiteSpace(original.label);
}
if (
(original.type === "link" || original.type === "image") &&
original.url &&
original.url.includes("(")
) {
for (const character of "<>") {
cloned.url = original.url.replaceAll(
character,
encodeURIComponent(character),
);
}
}
if (
(original.type === "definition" ||
original.type === "link" ||
original.type === "image") &&
original.title
) {
cloned.title = original.title.replaceAll(/\\(?=["')])/gu, "");
}
// for insert pragma
if (
parent?.type === "root" &&
parent.children.length > 0 &&
(parent.children[0] === original ||
(isFrontMatter(parent.children[0]) && parent.children[1] === original)) &&
original.type === "html" &&
hasPragma(original.value)
) {
return null;
}
}
clean.ignoredProperties = ignoredProperties;
export default clean;

View File

@@ -0,0 +1,86 @@
import { all as getCjkCharset } from "cjk-regex";
import { Charset } from "regexp-util";
import unicodeRegex from "unicode-regex";
const cjkCharset = new Charset(
getCjkCharset(),
unicodeRegex({
Script_Extensions: ["Han", "Katakana", "Hiragana", "Hangul", "Bopomofo"],
General_Category: [
"Other_Letter",
"Letter_Number",
"Other_Symbol",
"Modifier_Letter",
"Modifier_Symbol",
"Nonspacing_Mark",
],
}),
);
const variationSelectorsCharset = unicodeRegex({
Block: ["Variation_Selectors", "Variation_Selectors_Supplement"],
});
const CJK_REGEXP = new RegExp(
`(?:${cjkCharset.toString("u")})(?:${variationSelectorsCharset.toString("u")})?`,
"u",
);
const asciiPunctuationCharacters = [
"!",
'"',
"#",
"$",
"%",
"&",
"'",
"(",
")",
"*",
"+",
",",
"-",
".",
"/",
":",
";",
"<",
"=",
">",
"?",
"@",
"[",
"\\",
"]",
"^",
"_",
"`",
"{",
"|",
"}",
"~",
];
// https://spec.commonmark.org/0.25/#punctuation-character
// https://unicode.org/Public/5.1.0/ucd/UCD.html#General_Category_Values
const unicodePunctuationClasses = [
/* Pc */ "Connector_Punctuation",
/* Pd */ "Dash_Punctuation",
/* Pe */ "Close_Punctuation",
/* Pf */ "Final_Punctuation",
/* Pi */ "Initial_Punctuation",
/* Po */ "Other_Punctuation",
/* Ps */ "Open_Punctuation",
];
const PUNCTUATION_REGEXP = new RegExp(
`(?:${[
new Charset(...asciiPunctuationCharacters).toRegExp("u").source,
...unicodePunctuationClasses.map(
(charset) => String.raw`\p{General_Category=${charset}}`,
"\u{ff5e}", // Used as a substitute for U+301C in Windows
),
].join("|")})`,
"u",
);
export { CJK_REGEXP, PUNCTUATION_REGEXP };

View File

@@ -0,0 +1,87 @@
import { hardline, markAsRoot } from "../document/builders.js";
import { replaceEndOfLine } from "../document/utils.js";
import getMaxContinuousCount from "../utils/get-max-continuous-count.js";
import inferParser from "../utils/infer-parser.js";
import { getFencedCodeBlockValue } from "./utils.js";
function embed(path, options) {
const { node } = path;
if (node.type === "code" && node.lang !== null) {
const parser = inferParser(options, { language: node.lang });
if (parser) {
return async (textToDoc) => {
const styleUnit = options.__inJsTemplate ? "~" : "`";
const style = styleUnit.repeat(
Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1),
);
const newOptions = { parser };
// Override the filepath option.
// This is because whether the trailing comma of type parameters
// should be printed depends on whether it is `*.ts` or `*.tsx`.
// https://github.com/prettier/prettier/issues/15282
if (node.lang === "ts" || node.lang === "typescript") {
newOptions.filepath = "dummy.ts";
} else if (node.lang === "tsx") {
newOptions.filepath = "dummy.tsx";
}
const doc = await textToDoc(
getFencedCodeBlockValue(node, options.originalText),
newOptions,
);
return markAsRoot([
style,
node.lang,
node.meta ? " " + node.meta : "",
hardline,
replaceEndOfLine(doc),
hardline,
style,
]);
};
}
}
switch (node.type) {
// MDX
case "import":
case "export":
return (textToDoc) =>
textToDoc(node.value, {
// TODO: Rename this option since it's not used in HTML
__onHtmlBindingRoot: (ast) => validateImportExport(ast, node.type),
parser: "babel",
});
case "jsx":
return (textToDoc) =>
textToDoc(`<$>${node.value}</$>`, {
parser: "__js_expression",
rootMarker: "mdx",
});
}
return null;
}
function validateImportExport(ast, type) {
const {
program: { body },
} = ast;
// https://github.com/mdx-js/mdx/blob/3430138958c9c0344ecad9d59e0d6b5d72bedae3/packages/remark-mdx/extract-imports-and-exports.js#L16
if (
!body.every(
(node) =>
node.type === "ImportDeclaration" ||
node.type === "ExportDefaultDeclaration" ||
node.type === "ExportNamedDeclaration",
)
) {
throw new Error(`Unexpected '${type}' in MDX.`);
}
}
export default embed;

View File

@@ -0,0 +1,123 @@
/**
* Type-safe wrapper for Prettier's markdown formatting
*
* This module provides a synchronous interface to Prettier's markdown
* parser and printer, adapted to work within a Prettier plugin context.
*/
const { markdown: markdownParser } = require("./parser-markdown.js");
const printer = require("./printer-markdown.js");
const { getDocPrinter } = require("./adapter-prettier-internals.js");
/**
* Formats a markdown string using Prettier's markdown parser and printer
*
* @param {string} markdown - The markdown string to format
* @param {Object} options - Formatting options
* @param {number} [options.printWidth=80] - Maximum line width
* @param {number} [options.tabWidth=2] - Tab width
* @param {string} [options.proseWrap='preserve'] - Prose wrapping mode
* @param {boolean} [options.singleQuote=false] - Use single quotes
* @returns {string} The formatted markdown string, or the original if formatting fails
*/
function formatMarkdown(markdown, options = {}) {
if (!markdown || typeof markdown !== "string") {
return markdown;
}
const trimmed = markdown.trim();
if (trimmed.length === 0) {
return markdown;
}
try {
// Parse markdown to AST
const ast = markdownParser.parse(trimmed, {
originalText: trimmed,
filepath: "temp.md",
printWidth: options.printWidth || 80,
tabWidth: options.tabWidth || 2,
proseWrap: options.proseWrap || "preserve",
singleQuote: options.singleQuote || false,
});
// Create an AstPath-like object for the printer
const astPath = {
getNode: () => ast,
stack: [ast],
node: ast,
callParent: (fn) => fn(astPath),
each: (fn) => {
if (ast.children) {
ast.children.forEach((child, index) => {
const childPath = {
getNode: () => child,
stack: [...astPath.stack, child],
node: child,
index,
previous: index > 0 ? ast.children[index - 1] : null,
next: index < ast.children.length - 1 ? ast.children[index + 1] : null,
parent: ast,
isFirst: index === 0,
isLast: index === ast.children.length - 1,
callParent: (fn) => fn(childPath),
};
fn(childPath);
});
}
},
};
// Create a print function for recursive printing
const createPrintFn = (path) => {
return (printPath) => {
return printer.print(printPath, {
printWidth: options.printWidth || 80,
tabWidth: options.tabWidth || 2,
proseWrap: options.proseWrap || "preserve",
singleQuote: options.singleQuote || false,
originalText: trimmed,
}, createPrintFn);
};
};
// Print the AST to a Doc object
const doc = printer.print(astPath, {
printWidth: options.printWidth || 80,
tabWidth: options.tabWidth || 2,
proseWrap: options.proseWrap || "preserve",
singleQuote: options.singleQuote || false,
originalText: trimmed,
}, createPrintFn);
// Convert Doc to string
if (typeof doc === "string") {
return doc.trimEnd();
}
// Try to convert Doc object to string using Prettier's doc printer
const docPrinter = getDocPrinter();
if (docPrinter && typeof docPrinter === "function") {
try {
const formattedString = docPrinter(doc, {
printWidth: options.printWidth || 80,
tabWidth: options.tabWidth || 2,
useTabs: false,
});
return typeof formattedString === "string" ? formattedString.trimEnd() : markdown;
} catch {
// Doc printing failed
return markdown;
}
}
// If we can't convert Doc to string, return original
return markdown;
} catch (error) {
// Parsing or printing failed, return original
return markdown;
}
}
module.exports = { formatMarkdown };

View File

@@ -0,0 +1,110 @@
/**
* Type-safe wrapper for Prettier's markdown formatting
*
* This module provides a synchronous interface to Prettier's markdown
* parser and printer, adapted to work within a Prettier plugin context.
*/
import type { ParserOptions } from "prettier";
import {
getDocPrinter,
getDocumentBuilders,
getDocumentConstants,
getDocumentUtils,
} from "./adapter-prettier-internals.js";
import { markdown as markdownParser } from "./parser-markdown.js";
import printer from "./printer-markdown.js";
interface MarkdownFormatOptions {
printWidth?: number;
tabWidth?: number;
proseWrap?: "always" | "never" | "preserve";
singleQuote?: boolean;
}
/**
* Formats a markdown string using Prettier's markdown parser and printer
*
* @param markdown - The markdown string to format
* @param options - Formatting options
* @returns The formatted markdown string, or the original if formatting fails
*/
export function formatMarkdown(markdown: string, options: MarkdownFormatOptions = {}): string {
if (!markdown || typeof markdown !== "string") {
return markdown;
}
const trimmed = markdown.trim();
if (trimmed.length === 0) {
return markdown;
}
try {
// Parse markdown to AST
const ast = markdownParser.parse(trimmed, {
originalText: trimmed,
filepath: "temp.md",
} as ParserOptions);
// Create an AstPath-like object for the printer
const astPath = {
getNode: () => ast,
stack: [ast],
callParent: (fn: (path: any) => any) => fn(astPath),
each: (fn: (path: any) => void) => {
if (ast.children) {
ast.children.forEach((child: any, index: number) => {
const childPath = {
getNode: () => child,
stack: [...astPath.stack, child],
index,
previous: index > 0 ? ast.children[index - 1] : null,
next: index < ast.children.length - 1 ? ast.children[index + 1] : null,
parent: ast,
isFirst: index === 0,
isLast: index === ast.children.length - 1,
};
fn(childPath);
});
}
},
};
// Create a print function for recursive printing
const createPrintFn = (path: any): any => {
return (printPath: any) => {
return printer.print(printPath, options as ParserOptions, createPrintFn);
};
};
// Print the AST to a Doc object
const doc = printer.print(astPath, options as ParserOptions, createPrintFn);
// Convert Doc to string
if (typeof doc === "string") {
return doc.trimEnd();
}
// Try to convert Doc object to string using Prettier's doc printer
const docPrinter = getDocPrinter();
if (docPrinter && typeof docPrinter === "function") {
try {
const formattedString = docPrinter(doc, {
printWidth: options.printWidth || 80,
tabWidth: options.tabWidth || 2,
useTabs: false,
});
return typeof formattedString === "string" ? formattedString.trimEnd() : markdown;
} catch {
// Doc printing failed
return markdown;
}
}
// If we can't convert Doc to string, return original
return markdown;
} catch (error) {
// Parsing or printing failed, return original
return markdown;
}
}

View File

@@ -0,0 +1,6 @@
import createGetVisitorKeys from "../utils/create-get-visitor-keys.js";
import visitorKeys from "./visitor-keys.js";
const getVisitorKeys = createGetVisitorKeys(visitorKeys);
export default getVisitorKeys;

View File

@@ -0,0 +1,8 @@
import printer from "./printer-markdown.js";
export const printers = {
mdast: printer,
};
export { default as languages } from "./languages.evaluate.js";
export { default as options } from "./options.js";
export * as parsers from "./parser-markdown.js";

View File

@@ -0,0 +1,20 @@
import * as linguistLanguages from "linguist-languages";
import createLanguage from "../utils/create-language.js";
const languages = [
createLanguage(linguistLanguages.Markdown, (data) => ({
parsers: ["markdown"],
vscodeLanguageIds: ["markdown"],
filenames: [...data.filenames, "README"],
extensions: data.extensions.filter((extension) => extension !== ".mdx"),
})),
createLanguage(linguistLanguages.Markdown, () => ({
name: "MDX",
parsers: ["mdx"],
vscodeLanguageIds: ["mdx"],
filenames: [],
extensions: [".mdx"],
})),
];
export default languages;

View File

@@ -0,0 +1,4 @@
const locStart = (node) => node.position.start.offset;
const locEnd = (node) => node.position.end.offset;
export { locEnd, locStart };

View File

@@ -0,0 +1,83 @@
/**
* modified from https://github.com/mdx-js/mdx/blob/c91b00c673bcf3e7c28b861fd692b69016026c45/packages/remark-mdx/index.js
*
* The MIT License (MIT)
*
* Copyright (c) 2017-2018 Compositor and Zeit, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
const IMPORT_REGEX = /^import\s/u;
const EXPORT_REGEX = /^export\s/u;
const BLOCKS_REGEX = String.raw`[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*|`;
const COMMENT_REGEX = /<!---->|<!---?[^>-](?:-?[^-])*-->/u;
const ES_COMMENT_REGEX = /^\{\s*\/\*(.*)\*\/\s*\}/u;
const EMPTY_NEWLINE = "\n\n";
const isImport = (text) => IMPORT_REGEX.test(text);
const isExport = (text) => EXPORT_REGEX.test(text);
const isImportOrExport = (text) => isImport(text) || isExport(text);
const tokenizeEsSyntax = (eat, value) => {
const index = value.indexOf(EMPTY_NEWLINE);
const subvalue = index === -1 ? value : value.slice(0, index);
if (isImportOrExport(subvalue)) {
return eat(subvalue)({
type: isExport(subvalue) ? "export" : "import",
value: subvalue,
});
}
};
tokenizeEsSyntax.notInBlock = true;
tokenizeEsSyntax.locator = (value /* , fromIndex*/) =>
isImportOrExport(value) ? -1 : 1;
const tokenizeEsComment = (eat, value) => {
const match = ES_COMMENT_REGEX.exec(value);
if (match) {
return eat(match[0])({
type: "esComment",
value: match[1].trim(),
});
}
};
tokenizeEsComment.locator = (value, fromIndex) => value.indexOf("{", fromIndex);
/** @import {Plugin, Settings} from "unified" */
/**
* @type {Plugin<[], Settings>}
*/
const esSyntax = function () {
const { Parser } = this;
const { blockTokenizers, blockMethods, inlineTokenizers, inlineMethods } =
Parser.prototype;
blockTokenizers.esSyntax = tokenizeEsSyntax;
inlineTokenizers.esComment = tokenizeEsComment;
blockMethods.splice(blockMethods.indexOf("paragraph"), 0, "esSyntax");
inlineMethods.splice(inlineMethods.indexOf("text"), 0, "esComment");
};
export { BLOCKS_REGEX, COMMENT_REGEX, esSyntax };

View File

@@ -0,0 +1,20 @@
// Options for markdown formatting
// These match Prettier's default markdown options
// Update this file if Prettier's markdown options change
const options = {
proseWrap: {
type: "choice",
default: "preserve",
choices: [
{ value: "always", description: "Wrap prose if it exceeds the print width" },
{ value: "never", description: "Don't wrap prose" },
{ value: "preserve", description: "Preserve the original wrapping" },
],
},
singleQuote: {
type: "boolean",
default: false,
},
};
export default options;

View File

@@ -0,0 +1,57 @@
import footnotes from "remark-footnotes";
import remarkMath from "remark-math";
import remarkParse from "remark-parse";
import unified from "unified";
import { locEnd, locStart } from "./loc.js";
import { BLOCKS_REGEX, esSyntax } from "./mdx.js";
import { hasIgnorePragma, hasPragma } from "./pragma.js";
import frontMatter from "./unified-plugins/front-matter.js";
import htmlToJsx from "./unified-plugins/html-to-jsx.js";
import liquid from "./unified-plugins/liquid.js";
import wikiLink from "./unified-plugins/wiki-link.js";
/**
* based on [MDAST](https://github.com/syntax-tree/mdast) with following modifications:
*
* 1. restore unescaped character (Text)
* 2. merge continuous Texts
* 3. replace whitespaces in InlineCode#value with one whitespace
* reference: http://spec.commonmark.org/0.25/#example-605
* 4. split Text into Sentence
*
* interface Word { value: string }
* interface Whitespace { value: string }
* interface Sentence { children: Array<Word | Whitespace> }
* interface InlineCode { children: Array<Sentence> }
*/
function createParse({ isMDX }) {
return (text) => {
const processor = unified()
.use(remarkParse, {
commonmark: true,
...(isMDX && { blocks: [BLOCKS_REGEX] }),
})
.use(footnotes)
.use(frontMatter)
.use(remarkMath)
.use(isMDX ? esSyntax : noop)
.use(liquid)
.use(isMDX ? htmlToJsx : noop)
.use(wikiLink);
return processor.run(processor.parse(text));
};
}
function noop() {}
const baseParser = {
astFormat: "mdast",
hasPragma,
hasIgnorePragma,
locStart,
locEnd,
};
export const markdown = { ...baseParser, parse: createParse({ isMDX: false }) };
export const mdx = { ...baseParser, parse: createParse({ isMDX: true }) };
export { markdown as remark };

View File

@@ -0,0 +1,25 @@
import { parseFrontMatter } from "../main/front-matter/index.js";
import {
FORMAT_PRAGMA_TO_INSERT,
MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP,
MARKDOWN_HAS_PRAGMA_REGEXP,
} from "../utils/pragma/pragma.evaluate.js";
const hasPragma = (text) =>
parseFrontMatter(text).content.trimStart().match(MARKDOWN_HAS_PRAGMA_REGEXP)
?.index === 0;
const hasIgnorePragma = (text) =>
parseFrontMatter(text)
.content.trimStart()
.match(MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP)?.index === 0;
const insertPragma = (text) => {
const { frontMatter } = parseFrontMatter(text);
const pragma = `<!-- @${FORMAT_PRAGMA_TO_INSERT} -->`;
return frontMatter
? `${frontMatter.raw}\n\n${pragma}\n\n${text.slice(frontMatter.end.index)}`
: `${pragma}\n\n${text}`;
};
export { hasIgnorePragma, hasPragma, insertPragma };

View File

@@ -0,0 +1,55 @@
import { fill } from "../document/builders.js";
import { DOC_TYPE_ARRAY, DOC_TYPE_FILL } from "../document/constants.js";
import { getDocType } from "../document/utils.js";
/**
* @import AstPath from "../common/ast-path.js"
* @import {Doc} from "../document/builders.js"
*/
/**
* @param {AstPath} path
* @param {*} options
* @param {*} print
* @returns {Doc}
*/
function printParagraph(path, options, print) {
const parts = path.map(print, "children");
return flattenFill(parts);
}
/**
* @param {Doc[]} docs
* @returns {Doc}
*/
function flattenFill(docs) {
/*
* We assume parts always meet following conditions:
* - parts.length is odd
* - odd elements are line-like doc that comes from odd element off inner fill
*/
/** @type {Doc[]} */
const parts = [""];
(function rec(/** @type {*} */ docArray) {
for (const doc of docArray) {
const docType = getDocType(doc);
if (docType === DOC_TYPE_ARRAY) {
rec(doc);
continue;
}
let head = doc;
let rest = [];
if (docType === DOC_TYPE_FILL) {
[head, ...rest] = doc.parts;
}
parts.push([parts.pop(), head], ...rest);
}
})(docs);
return fill(parts);
}
export { printParagraph };

View File

@@ -0,0 +1,256 @@
import htmlWhitespaceUtils from "../utils/html-whitespace-utils.js";
import { getOrderedListItemInfo, mapAst, splitText } from "./utils.js";
// 0x0 ~ 0x10ffff
const isSingleCharRegex = /^\\?.$/su;
const isNewLineBlockquoteRegex = /^\n *>[ >]*$/u;
function preprocess(ast, options) {
ast = restoreUnescapedCharacter(ast, options);
ast = mergeContinuousTexts(ast);
ast = transformIndentedCodeblockAndMarkItsParentList(ast, options);
ast = markAlignedList(ast, options);
ast = splitTextIntoSentences(ast);
return ast;
}
function restoreUnescapedCharacter(ast, options) {
return mapAst(ast, (node) => {
if (node.type !== "text") {
return node;
}
const { value } = node;
if (
value === "*" ||
value === "_" || // handle these cases in printer
!isSingleCharRegex.test(value) ||
node.position.end.offset - node.position.start.offset === value.length
) {
return node;
}
const text = options.originalText.slice(
node.position.start.offset,
node.position.end.offset,
);
if (isNewLineBlockquoteRegex.test(text)) {
return node;
}
return { ...node, value: text };
});
}
function mergeChildren(ast, shouldMerge, mergeNode) {
return mapAst(ast, (node) => {
if (!node.children) {
return node;
}
const children = [];
let lastChild;
let changed;
for (let child of node.children) {
if (lastChild && shouldMerge(lastChild, child)) {
child = mergeNode(lastChild, child);
// Replace the previous node
children.splice(-1, 1, child);
changed ||= true;
} else {
children.push(child);
}
lastChild = child;
}
return changed ? { ...node, children } : node;
});
}
function mergeContinuousTexts(ast) {
return mergeChildren(
ast,
(prevNode, node) => prevNode.type === "text" && node.type === "text",
(prevNode, node) => ({
type: "text",
value: prevNode.value + node.value,
position: {
start: prevNode.position.start,
end: node.position.end,
},
}),
);
}
function splitTextIntoSentences(ast) {
return mapAst(ast, (node, index, [parentNode]) => {
if (node.type !== "text") {
return node;
}
let { value } = node;
if (parentNode.type === "paragraph") {
// CommonMark doesn't remove trailing/leading \f, but it should be
// removed in the HTML rendering process
if (index === 0) {
value = htmlWhitespaceUtils.trimStart(value);
}
if (index === parentNode.children.length - 1) {
value = htmlWhitespaceUtils.trimEnd(value);
}
}
return {
type: "sentence",
position: node.position,
children: splitText(value),
};
});
}
function transformIndentedCodeblockAndMarkItsParentList(ast, options) {
return mapAst(ast, (node, index, parentStack) => {
if (node.type === "code") {
// the first char may point to `\n`, e.g. `\n\t\tbar`, just ignore it
const isIndented = /^\n?(?: {4,}|\t)/u.test(
options.originalText.slice(
node.position.start.offset,
node.position.end.offset,
),
);
node.isIndented = isIndented;
if (isIndented) {
for (let i = 0; i < parentStack.length; i++) {
const parent = parentStack[i];
// no need to check checked items
if (parent.hasIndentedCodeblock) {
break;
}
if (parent.type === "list") {
parent.hasIndentedCodeblock = true;
}
}
}
}
return node;
});
}
function markAlignedList(ast, options) {
return mapAst(ast, (node, index, parentStack) => {
if (node.type === "list" && node.children.length > 0) {
// if one of its parents is not aligned, it's not possible to be aligned in sub-lists
for (let i = 0; i < parentStack.length; i++) {
const parent = parentStack[i];
if (parent.type === "list" && !parent.isAligned) {
node.isAligned = false;
return node;
}
}
node.isAligned = isAligned(node);
}
return node;
});
function getListItemStart(listItem) {
return listItem.children.length === 0
? -1
: listItem.children[0].position.start.column - 1;
}
function isAligned(list) {
if (!list.ordered) {
/**
* - 123
* - 123
*/
return true;
}
const [firstItem, secondItem] = list.children;
const firstInfo = getOrderedListItemInfo(firstItem, options);
if (firstInfo.leadingSpaces.length > 1) {
/**
* 1. 123
*
* 1. 123
* 1. 123
*/
return true;
}
const firstStart = getListItemStart(firstItem);
if (firstStart === -1) {
/**
* 1.
*
* 1.
* 1.
*/
return false;
}
if (list.children.length === 1) {
/**
* aligned:
*
* 11. 123
*
* not aligned:
*
* 1. 123
*/
return firstStart % options.tabWidth === 0;
}
const secondStart = getListItemStart(secondItem);
if (firstStart !== secondStart) {
/**
* 11. 123
* 1. 123
*
* 1. 123
* 11. 123
*/
return false;
}
if (firstStart % options.tabWidth === 0) {
/**
* 11. 123
* 12. 123
*/
return true;
}
/**
* aligned:
*
* 11. 123
* 1. 123
*
* not aligned:
*
* 1. 123
* 2. 123
*/
const secondInfo = getOrderedListItemInfo(secondItem, options);
return secondInfo.leadingSpaces.length > 1;
}
}
export default preprocess;

View File

@@ -0,0 +1,37 @@
/**
* @import AstPath from "../common/ast-path.js"
* @import {Doc} from "../document/builders.js"
*/
import { fill } from "../document/builders.js";
import { DOC_TYPE_STRING } from "../document/constants.js";
import { getDocType } from "../document/utils.js";
/**
* @param {AstPath} path
* @param {*} print
* @returns {Doc}
*/
function printSentence(path, print) {
/** @type {Doc[]} */
const parts = [""];
path.each(() => {
const { node } = path;
const doc = print();
switch (node.type) {
case "whitespace":
if (getDocType(doc) !== DOC_TYPE_STRING) {
parts.push(doc, "");
break;
}
// fallthrough
default:
parts.push([parts.pop(), doc]);
}
}, "children");
return fill(parts);
}
export { printSentence };

View File

@@ -0,0 +1,266 @@
import { hardline, line, softline } from "../document/builders.js";
import {
KIND_CJ_LETTER,
KIND_CJK_PUNCTUATION,
KIND_K_LETTER,
KIND_NON_CJK,
} from "./utils.js";
/**
* @import {WordNode, WhitespaceValue, WordKind} from "./utils.js"
* @import AstPath from "../common/ast-path.js"
* @typedef {"always" | "never" | "preserve"} ProseWrap
* @typedef {{ next?: WordNode | null, previous?: WordNode | null }}
* AdjacentNodes Nodes adjacent to a `whitespace` node. Are always of type
* `word`.
*/
const SINGLE_LINE_NODE_TYPES = new Set([
"heading",
"tableCell",
"link",
"wikiLink",
]);
/**
* A line break between a character from this set and CJ can be converted to a
* space. Includes only ASCII punctuation marks for now.
*/
const lineBreakBetweenTheseAndCJConvertsToSpace = new Set(
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
);
/**
* Determine the preferred style of spacing between Chinese or Japanese and non-CJK
* characters in the parent `sentence` node.
*
* @param {AstPath} path
* @returns {boolean} `true` if Space tends to be inserted between CJ and
* non-CJK, `false` otherwise.
*/
function isInSentenceWithCJSpaces({ parent: sentenceNode }) {
if (sentenceNode.usesCJSpaces === undefined) {
const stats = { " ": 0, "": 0 };
const { children } = sentenceNode;
for (let i = 1; i < children.length - 1; ++i) {
const node = children[i];
if (
node.type === "whitespace" &&
(node.value === " " || node.value === "")
) {
const previousKind = children[i - 1].kind;
const nextKind = children[i + 1].kind;
if (
(previousKind === KIND_CJ_LETTER && nextKind === KIND_NON_CJK) ||
(previousKind === KIND_NON_CJK && nextKind === KIND_CJ_LETTER)
) {
++stats[node.value];
}
}
}
// Inject a property to cache the result.
sentenceNode.usesCJSpaces = stats[" "] > stats[""];
}
return sentenceNode.usesCJSpaces;
}
/**
* Check whether the given `"\n"` node can be converted to a space.
*
* For example, if you would like to squash English text
*
* "You might want\nto use Prettier."
*
* into a single line, you would replace `"\n"` with `" "`:
*
* "You might want to use Prettier."
*
* However, Chinese and Japanese don't use U+0020 Space to divide words, so line
* breaks shouldn't be replaced with spaces for those languages.
*
* PRs are welcome to support line breaking rules for other languages.
*
* @param {AstPath} path
* @param {boolean} isLink
* @returns {boolean}
*/
function lineBreakCanBeConvertedToSpace(path, isLink) {
if (isLink) {
return true;
}
/** @type {AdjacentNodes} */
const { previous, next } = path;
// e.g. " \nletter"
if (!previous || !next) {
return true;
}
const previousKind = previous.kind;
const nextKind = next.kind;
if (
// "\n" between non-CJK or Korean characters always can be converted to a
// space. Korean Hangul simulates Latin words. See
// https://github.com/prettier/prettier/issues/6516
(isNonCJKOrKoreanLetter(previousKind) &&
isNonCJKOrKoreanLetter(nextKind)) ||
// Han & Hangul: same way preferred
(previousKind === KIND_K_LETTER && nextKind === KIND_CJ_LETTER) ||
(nextKind === KIND_K_LETTER && previousKind === KIND_CJ_LETTER)
) {
return true;
}
// Do not convert \n to a space:
if (
// around CJK punctuation
previousKind === KIND_CJK_PUNCTUATION ||
nextKind === KIND_CJK_PUNCTUATION ||
// between CJ
(previousKind === KIND_CJ_LETTER && nextKind === KIND_CJ_LETTER)
) {
return false;
}
// The rest of this function deals only with line breaks between CJ and
// non-CJK characters.
// Convert a line break between CJ and certain non-letter characters (e.g.
// ASCII punctuation) to a space.
//
// E.g. :::\n句子句子句子\n::: → ::: 句子句子句子 :::
//
// Note: line breaks like "(\n句子句子\n)" or "句子\n." are suppressed in
// `isBreakable(...)`.
if (
lineBreakBetweenTheseAndCJConvertsToSpace.has(next.value[0]) ||
lineBreakBetweenTheseAndCJConvertsToSpace.has(previous.value.at(-1))
) {
return true;
}
// Converting a line break between CJ and non-ASCII punctuation to a space is
// undesired in many cases. PRs are welcome to fine-tune this logic.
//
// Examples where \n must not be converted to a space:
//
// 1. "〜" (U+301C, belongs to Pd) in
//
// "ア〜\nエの中から1つ選べ。"
//
// 2. "…" (U+2026, belongs to Po) in
//
// "これはひどい……\nなんと汚いコミットログなんだ……"
if (previous.hasTrailingPunctuation || next.hasLeadingPunctuation) {
return false;
}
// If the sentence uses the style with spaces between CJ and non-CJK, "\n" can
// be converted to a space.
return isInSentenceWithCJSpaces(path);
}
/**
* @param {WordKind | undefined} kind
* @returns {boolean} `true` if `kind` is Korean letter or non-CJK
*/
function isNonCJKOrKoreanLetter(kind) {
return kind === KIND_NON_CJK || kind === KIND_K_LETTER;
}
/**
* Check whether whitespace can be printed as a line break.
*
* @param {AstPath} path
* @param {WhitespaceValue} value
* @param {ProseWrap} proseWrap
* @param {boolean} isLink
* @returns {boolean}
*/
function isBreakable(path, value, proseWrap, isLink) {
if (
proseWrap !== "always" ||
path.hasAncestor((node) => SINGLE_LINE_NODE_TYPES.has(node.type))
) {
return false;
}
if (isLink) {
return value !== "";
}
/** @type {AdjacentNodes} */
const { previous, next } = path;
// [1]: We will make a breaking change to the rule to convert spaces between
// a Chinese or Japanese character and another character in the future.
// Such a space must have been always interchangeable with a line break.
// https://wpt.fyi/results/css/css-text/line-breaking?label=master&label=experimental&aligned&q=segment-break-transformation-rules-
// [2]: we should not break lines even between Chinese/Japanese characters because Chrome & Safari replaces "\n" between such characters with " " now.
// [3]: Hangul (Korean) must simulate Latin words; see https://github.com/prettier/prettier/issues/6516
// [printable][""][Hangul] & vice versa => Don't break
// [printable][\n][Hangul] will be interchangeable to [printable][" "][Hangul] in the future
// (will be compatible with Firefox's behavior)
if (!previous || !next) {
// empty side is Latin ASCII symbol (e.g. *, [, ], or `)
// value is " " or "\n" (not "")
// [1] & [2]? No, it's the only exception because " " & "\n" have been always interchangeable only here
return true;
}
if (value === "") {
// [1] & [2] & [3]
// At least either of previous or next is non-Latin (=CJK)
return false;
}
if (
// See the same product terms as the following in lineBreakCanBeConvertedToSpace
// The behavior is consistent between browsers and Prettier in that line breaks between Korean and Chinese/Japanese letters are equivalent to spaces.
// Currently, [CJK punctuation][\n][Hangul] is interchangeable to [CJK punctuation][""][Hangul],
// but this is not compatible with Firefox's behavior.
// Will be changed to [CJK punctuation][" "][Hangul] in the future
(previous.kind === KIND_K_LETTER && next.kind === KIND_CJ_LETTER) ||
(next.kind === KIND_K_LETTER && previous.kind === KIND_CJ_LETTER)
) {
return true;
}
// [1] & [2]
if (previous.isCJ || next.isCJ) {
return false;
}
return true;
}
/**
* @param {AstPath} path
* @param {WhitespaceValue} value
* @param {ProseWrap} proseWrap
* @param {boolean} [isLink] Special mode of (un)wrapping that preserves the
* normalized form of link labels. https://spec.commonmark.org/0.30/#matches
*/
function printWhitespace(path, value, proseWrap, isLink) {
if (proseWrap === "preserve" && value === "\n") {
return hardline;
}
const canBeSpace =
value === " " ||
(value === "\n" && lineBreakCanBeConvertedToSpace(path, isLink));
if (isBreakable(path, value, proseWrap, isLink)) {
return canBeSpace ? line : softline;
}
return canBeSpace ? " " : "";
}
export { printWhitespace };

View File

@@ -0,0 +1,81 @@
import {
breakParent,
group,
hardlineWithoutBreakParent,
ifBreak,
join,
} from "../../document/builders.js";
import { printDocToString } from "../../document/printer.js";
import getStringWidth from "../../utils/get-string-width.js";
function printTable(path, options, print) {
const { node } = path;
const columnMaxWidths = [];
// { [rowIndex: number]: { [columnIndex: number]: {text: string, width: number} } }
const contents = path.map(
() =>
path.map(({ index: columnIndex }) => {
const text = printDocToString(print(), options).formatted;
const width = getStringWidth(text);
columnMaxWidths[columnIndex] = Math.max(
columnMaxWidths[columnIndex] ?? 3, // minimum width = 3 (---, :--, :-:, --:)
width,
);
return { text, width };
}, "children"),
"children",
);
const alignedTable = printTableContents(/* isCompact */ false);
if (options.proseWrap !== "never") {
return [breakParent, alignedTable];
}
// Only if the --prose-wrap never is set and it exceeds the print width.
const compactTable = printTableContents(/* isCompact */ true);
return [breakParent, group(ifBreak(compactTable, alignedTable))];
function printTableContents(isCompact) {
return join(
hardlineWithoutBreakParent,
[
printRow(contents[0], isCompact),
printAlign(isCompact),
...contents
.slice(1)
.map((rowContents) => printRow(rowContents, isCompact)),
].map((columns) => `| ${columns.join(" | ")} |`),
);
}
function printAlign(isCompact) {
return columnMaxWidths.map((width, index) => {
const align = node.align[index];
const first = align === "center" || align === "left" ? ":" : "-";
const last = align === "center" || align === "right" ? ":" : "-";
const middle = isCompact ? "-" : "-".repeat(width - 2);
return `${first}${middle}${last}`;
});
}
function printRow(columns, isCompact) {
return columns.map(({ text, width }, columnIndex) => {
if (isCompact) {
return text;
}
const spaces = columnMaxWidths[columnIndex] - width;
const align = node.align[columnIndex];
let before = 0;
if (align === "right") {
before = spaces;
} else if (align === "center") {
before = Math.floor(spaces / 2);
}
const after = spaces - before;
return `${" ".repeat(before)}${text}${" ".repeat(after)}`;
});
}
}
export { printTable };

View File

@@ -0,0 +1,810 @@
import collapseWhiteSpace from "collapse-white-space";
import escapeStringRegexp from "escape-string-regexp";
import {
align,
fill,
group,
hardline,
indent,
line,
literalline,
markAsRoot,
softline,
} from "./adapter-document-builders.js";
import { DOC_TYPE_STRING } from "./adapter-document-constants.js";
import { getDocType, replaceEndOfLine } from "./adapter-document-utils.js";
import {
getMaxContinuousCount,
getMinNotPresentContinuousCount,
getPreferredQuote,
UnexpectedNodeError,
} from "./adapter-prettier-utils.js";
import clean from "./clean.js";
import { PUNCTUATION_REGEXP } from "./constants.evaluate.js";
import embed from "./embed.js";
import getVisitorKeys from "./get-visitor-keys.js";
import { locEnd, locStart } from "./loc.js";
import { insertPragma } from "./pragma.js";
import { printTable } from "./print/table.js";
import { printParagraph } from "./print-paragraph.js";
import preprocess from "./print-preprocess.js";
import { printSentence } from "./print-sentence.js";
import { printWhitespace } from "./print-whitespace.js";
import {
getFencedCodeBlockValue,
hasGitDiffFriendlyOrderedList,
INLINE_NODE_TYPES,
INLINE_NODE_WRAPPER_TYPES,
isAutolink,
splitText,
} from "./utils.js";
/**
* @import {Doc} from "../document/builders.js"
*/
const SIBLING_NODE_TYPES = new Set(["listItem", "definition"]);
function prevOrNextWord(path) {
const { previous, next } = path;
const hasPrevOrNextWord =
(previous?.type === "sentence" &&
previous.children.at(-1)?.type === "word" &&
!previous.children.at(-1).hasTrailingPunctuation) ||
(next?.type === "sentence" &&
next.children[0]?.type === "word" &&
!next.children[0].hasLeadingPunctuation);
return hasPrevOrNextWord;
}
function genericPrint(path, options, print) {
const { node } = path;
if (shouldRemainTheSameContent(path)) {
/*
* We assume parts always meet following conditions:
* - parts.length is odd
* - odd (0-indexed) elements are line-like doc
*/
/** @type {Doc[]} */
const parts = [""];
const textsNodes = splitText(
options.originalText.slice(
node.position.start.offset,
node.position.end.offset,
),
);
for (const node of textsNodes) {
if (node.type === "word") {
parts.push([parts.pop(), node.value]);
continue;
}
const doc = printWhitespace(path, node.value, options.proseWrap, true);
if (getDocType(doc) === DOC_TYPE_STRING) {
parts.push([parts.pop(), doc]);
continue;
}
// In this path, doc is line. To meet the condition, we need additional element "".
parts.push(doc, "");
}
return fill(parts);
}
switch (node.type) {
case "root":
/* c8 ignore next 3 */
if (node.children.length === 0) {
return "";
}
return [printRoot(path, options, print), hardline];
case "paragraph":
return printParagraph(path, options, print);
case "sentence":
return printSentence(path, print);
case "word": {
let escapedValue = node.value
.replaceAll("*", String.raw`\*`) // escape all `*`
.replaceAll(
new RegExp(
[
`(^|${PUNCTUATION_REGEXP.source})(_+)`,
`(_+)(${PUNCTUATION_REGEXP.source}|$)`,
].join("|"),
"gu",
),
(_, text1, underscore1, underscore2, text2) =>
(underscore1
? `${text1}${underscore1}`
: `${underscore2}${text2}`
).replaceAll("_", String.raw`\_`),
); // escape all `_` except concating with non-punctuation, e.g. `1_2_3` is not considered emphasis
const isFirstSentence = (node, name, index) =>
node.type === "sentence" && index === 0;
const isLastChildAutolink = (node, name, index) =>
isAutolink(node.children[index - 1]);
if (
escapedValue !== node.value &&
(path.match(undefined, isFirstSentence, isLastChildAutolink) ||
path.match(
undefined,
isFirstSentence,
(node, name, index) => node.type === "emphasis" && index === 0,
isLastChildAutolink,
))
) {
// backslash is parsed as part of autolinks, so we need to remove it
escapedValue = escapedValue.replace(/^(\\?[*_])+/u, (prefix) =>
prefix.replaceAll("\\", ""),
);
}
return escapedValue;
}
case "whitespace": {
const { next } = path;
const proseWrap =
// leading char that may cause different syntax
next && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/u.test(next.value)
? "never"
: options.proseWrap;
return printWhitespace(path, node.value, proseWrap);
}
case "emphasis": {
let style;
if (isAutolink(node.children[0])) {
style = options.originalText[node.position.start.offset];
} else {
const hasPrevOrNextWord = prevOrNextWord(path); // `1*2*3` is considered emphasis but `1_2_3` is not
const inStrongAndHasPrevOrNextWord = // `1***2***3` is considered strong emphasis but `1**_2_**3` is not
path.callParent(
({ node }) => node.type === "strong" && prevOrNextWord(path),
);
style =
hasPrevOrNextWord ||
inStrongAndHasPrevOrNextWord ||
path.hasAncestor((node) => node.type === "emphasis")
? "*"
: "_";
}
return [style, printChildren(path, options, print), style];
}
case "strong":
return ["**", printChildren(path, options, print), "**"];
case "delete":
return ["~~", printChildren(path, options, print), "~~"];
case "inlineCode": {
const code =
options.proseWrap === "preserve"
? node.value
: node.value.replaceAll("\n", " ");
const backtickCount = getMinNotPresentContinuousCount(code, "`");
const backtickString = "`".repeat(backtickCount);
const padding =
code.startsWith("`") ||
code.endsWith("`") ||
(/^[\n ]/u.test(code) && /[\n ]$/u.test(code) && /[^\n ]/u.test(code))
? " "
: "";
return [backtickString, padding, code, padding, backtickString];
}
case "wikiLink": {
let contents = "";
if (options.proseWrap === "preserve") {
contents = node.value;
} else {
contents = node.value.replaceAll(/[\t\n]+/gu, " ");
}
return ["[[", contents, "]]"];
}
case "link":
switch (options.originalText[node.position.start.offset]) {
case "<": {
const mailto = "mailto:";
const url =
// <hello@example.com> is parsed as { url: "mailto:hello@example.com" }
node.url.startsWith(mailto) &&
options.originalText.slice(
node.position.start.offset + 1,
node.position.start.offset + 1 + mailto.length,
) !== mailto
? node.url.slice(mailto.length)
: node.url;
return ["<", url, ">"];
}
case "[":
return [
"[",
printChildren(path, options, print),
"](",
printUrl(node.url, ")"),
printTitle(node.title, options),
")",
];
default:
return options.originalText.slice(
node.position.start.offset,
node.position.end.offset,
);
}
case "image":
return [
"![",
node.alt || "",
"](",
printUrl(node.url, ")"),
printTitle(node.title, options),
")",
];
case "blockquote":
return ["> ", align("> ", printChildren(path, options, print))];
case "heading":
return [
"#".repeat(node.depth) + " ",
printChildren(path, options, print),
];
case "code": {
if (node.isIndented) {
// indented code block
const alignment = " ".repeat(4);
return align(alignment, [
alignment,
replaceEndOfLine(node.value, hardline),
]);
}
// fenced code block
const styleUnit = options.__inJsTemplate ? "~" : "`";
const style = styleUnit.repeat(
Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1),
);
return [
style,
node.lang || "",
node.meta ? " " + node.meta : "",
hardline,
replaceEndOfLine(
getFencedCodeBlockValue(node, options.originalText),
hardline,
),
hardline,
style,
];
}
case "html": {
const { parent, isLast } = path;
const value =
parent.type === "root" && isLast ? node.value.trimEnd() : node.value;
const isHtmlComment = /^<!--.*-->$/su.test(value);
return replaceEndOfLine(
value,
// @ts-expect-error
isHtmlComment ? hardline : markAsRoot(literalline),
);
}
case "list": {
const nthSiblingIndex = getNthListSiblingIndex(node, path.parent);
const isGitDiffFriendlyOrderedList = hasGitDiffFriendlyOrderedList(
node,
options,
);
return printChildren(path, options, print, {
processor() {
const prefix = getPrefix();
const { node: childNode } = path;
if (
childNode.children.length === 2 &&
childNode.children[1].type === "html" &&
childNode.children[0].position.start.column !==
childNode.children[1].position.start.column
) {
return [prefix, printListItem(path, options, print, prefix)];
}
return [
prefix,
align(
" ".repeat(prefix.length),
printListItem(path, options, print, prefix),
),
];
function getPrefix() {
const rawPrefix = node.ordered
? (path.isFirst
? node.start
: isGitDiffFriendlyOrderedList
? 1
: node.start + path.index) +
(nthSiblingIndex % 2 === 0 ? ". " : ") ")
: nthSiblingIndex % 2 === 0
? "- "
: "* ";
return (node.isAligned ||
/* workaround for https://github.com/remarkjs/remark/issues/315 */ node.hasIndentedCodeblock) &&
node.ordered
? alignListPrefix(rawPrefix, options)
: rawPrefix;
}
},
});
}
case "thematicBreak": {
const { ancestors } = path;
const counter = ancestors.findIndex((node) => node.type === "list");
if (counter === -1) {
return "---";
}
const nthSiblingIndex = getNthListSiblingIndex(
ancestors[counter],
ancestors[counter + 1],
);
return nthSiblingIndex % 2 === 0 ? "***" : "---";
}
case "linkReference":
return [
"[",
printChildren(path, options, print),
"]",
node.referenceType === "full"
? printLinkReference(node)
: node.referenceType === "collapsed"
? "[]"
: "",
];
case "imageReference":
switch (node.referenceType) {
case "full":
return ["![", node.alt || "", "]", printLinkReference(node)];
default:
return [
"![",
node.alt,
"]",
node.referenceType === "collapsed" ? "[]" : "",
];
}
case "definition": {
const lineOrSpace = options.proseWrap === "always" ? line : " ";
return group([
printLinkReference(node),
":",
indent([
lineOrSpace,
printUrl(node.url),
node.title === null
? ""
: [lineOrSpace, printTitle(node.title, options, false)],
]),
]);
}
// `footnote` requires `.use(footnotes, {inlineNotes: true})`, we are not using this option
// https://github.com/remarkjs/remark-footnotes#optionsinlinenotes
/* c8 ignore next 2 */
case "footnote":
return ["[^", printChildren(path, options, print), "]"];
case "footnoteReference":
return printFootnoteReference(node);
case "footnoteDefinition": {
const shouldInlineFootnote =
node.children.length === 1 &&
node.children[0].type === "paragraph" &&
(options.proseWrap === "never" ||
(options.proseWrap === "preserve" &&
node.children[0].position.start.line ===
node.children[0].position.end.line));
return [
printFootnoteReference(node),
": ",
shouldInlineFootnote
? printChildren(path, options, print)
: group([
align(
" ".repeat(4),
printChildren(path, options, print, {
processor: ({ isFirst }) =>
isFirst ? group([softline, print()]) : print(),
}),
),
]),
];
}
case "table":
return printTable(path, options, print);
case "tableCell":
return printChildren(path, options, print);
case "break":
return /\s/u.test(options.originalText[node.position.start.offset])
? [" ", markAsRoot(literalline)]
: ["\\", hardline];
case "liquidNode":
return replaceEndOfLine(node.value, hardline);
// MDX
// fallback to the original text if multiparser failed
// or `embeddedLanguageFormatting: "off"`
case "import":
case "export":
case "jsx":
return node.value.trimEnd();
case "esComment":
return ["{/* ", node.value, " */}"];
case "math":
return [
"$$",
hardline,
node.value ? [replaceEndOfLine(node.value, hardline), hardline] : "",
"$$",
];
case "inlineMath":
// remark-math trims content but we don't want to remove whitespaces
// since it's very possible that it's recognized as math accidentally
return options.originalText.slice(locStart(node), locEnd(node));
case "frontMatter": // Handled in core
case "tableRow": // handled in "table"
case "listItem": // handled in "list"
case "text": // handled in other types
default:
/* c8 ignore next */
throw new UnexpectedNodeError(node, "Markdown");
}
}
function printListItem(path, options, print, listPrefix) {
const { node } = path;
const prefix = node.checked === null ? "" : node.checked ? "[x] " : "[ ] ";
return [
prefix,
printChildren(path, options, print, {
processor({ node, isFirst }) {
if (isFirst && node.type !== "list") {
return align(" ".repeat(prefix.length), print());
}
const alignment = " ".repeat(
clamp(options.tabWidth - listPrefix.length, 0, 3), // 4+ will cause indented code block
);
return [alignment, align(alignment, print())];
},
}),
];
}
function alignListPrefix(prefix, options) {
const additionalSpaces = getAdditionalSpaces();
return (
prefix +
" ".repeat(
additionalSpaces >= 4 ? 0 : additionalSpaces, // 4+ will cause indented code block
)
);
function getAdditionalSpaces() {
const restSpaces = prefix.length % options.tabWidth;
return restSpaces === 0 ? 0 : options.tabWidth - restSpaces;
}
}
function getNthListSiblingIndex(node, parentNode) {
return getNthSiblingIndex(
node,
parentNode,
(siblingNode) => siblingNode.ordered === node.ordered,
);
}
function getNthSiblingIndex(node, parentNode, condition) {
let index = -1;
for (const childNode of parentNode.children) {
if (childNode.type === node.type && condition(childNode)) {
index++;
} else {
index = -1;
}
if (childNode === node) {
return index;
}
}
}
function printRoot(path, options, print) {
/** @typedef {{ index: number, offset: number }} IgnorePosition */
/** @type {Array<{start: IgnorePosition, end: IgnorePosition}>} */
const ignoreRanges = [];
/** @type {IgnorePosition | null} */
let ignoreStart = null;
const { children } = path.node;
for (const [index, childNode] of children.entries()) {
switch (isPrettierIgnore(childNode)) {
case "start":
if (ignoreStart === null) {
ignoreStart = { index, offset: childNode.position.end.offset };
}
break;
case "end":
if (ignoreStart !== null) {
ignoreRanges.push({
start: ignoreStart,
end: { index, offset: childNode.position.start.offset },
});
ignoreStart = null;
}
break;
default:
// do nothing
break;
}
}
return printChildren(path, options, print, {
processor({ index }) {
if (ignoreRanges.length > 0) {
const ignoreRange = ignoreRanges[0];
if (index === ignoreRange.start.index) {
return [
printIgnoreComment(children[ignoreRange.start.index]),
options.originalText.slice(
ignoreRange.start.offset,
ignoreRange.end.offset,
),
printIgnoreComment(children[ignoreRange.end.index]),
];
}
if (ignoreRange.start.index < index && index < ignoreRange.end.index) {
return false;
}
if (index === ignoreRange.end.index) {
ignoreRanges.shift();
return false;
}
}
return print();
},
});
}
function printChildren(path, options, print, events = {}) {
const { processor = print } = events;
const parts = [];
path.each(() => {
const result = processor(path);
if (result !== false) {
if (parts.length > 0 && shouldPrePrintHardline(path)) {
parts.push(hardline);
if (
shouldPrePrintDoubleHardline(path, options) ||
shouldPrePrintTripleHardline(path)
) {
parts.push(hardline);
}
if (shouldPrePrintTripleHardline(path)) {
parts.push(hardline);
}
}
parts.push(result);
}
}, "children");
return parts;
}
function printIgnoreComment(node) {
if (node.type === "html") {
return node.value;
}
if (
node.type === "paragraph" &&
Array.isArray(node.children) &&
node.children.length === 1 &&
node.children[0].type === "esComment"
) {
return ["{/* ", node.children[0].value, " */}"];
}
}
/** @return {false | 'next' | 'start' | 'end'} */
function isPrettierIgnore(node) {
let match;
if (node.type === "html") {
match = node.value.match(
/^<!--\s*prettier-ignore(?:-(start|end))?\s*-->$/u,
);
} else {
let comment;
if (node.type === "esComment") {
comment = node;
} else if (
node.type === "paragraph" &&
node.children.length === 1 &&
node.children[0].type === "esComment"
) {
comment = node.children[0];
}
if (comment) {
match = comment.value.match(/^prettier-ignore(?:-(start|end))?$/u);
}
}
return match ? match[1] || "next" : false;
}
function shouldPrePrintHardline({ node, parent }) {
const isInlineNode = INLINE_NODE_TYPES.has(node.type);
const isInlineHTML =
node.type === "html" && INLINE_NODE_WRAPPER_TYPES.has(parent.type);
return !isInlineNode && !isInlineHTML;
}
function isLooseListItem(node, options) {
return (
node.type === "listItem" &&
(node.spread ||
// Check if `listItem` ends with `\n`
// since it can't be empty, so we only need check the last character
options.originalText.charAt(node.position.end.offset - 1) === "\n")
);
}
function shouldPrePrintDoubleHardline({ node, previous, parent }, options) {
if (
isLooseListItem(previous, options) ||
(node.type === "list" &&
parent.type === "listItem" &&
previous.type === "code")
) {
return true;
}
const isSequence = previous.type === node.type;
const isSiblingNode = isSequence && SIBLING_NODE_TYPES.has(node.type);
const isInTightListItem =
parent.type === "listItem" &&
(node.type === "list" || !isLooseListItem(parent, options));
const isPrevNodePrettierIgnore = isPrettierIgnore(previous) === "next";
const isBlockHtmlWithoutBlankLineBetweenPrevHtml =
node.type === "html" &&
previous.type === "html" &&
previous.position.end.line + 1 === node.position.start.line;
const isHtmlDirectAfterListItem =
node.type === "html" &&
parent.type === "listItem" &&
previous.type === "paragraph" &&
previous.position.end.line + 1 === node.position.start.line;
return !(
isSiblingNode ||
isInTightListItem ||
isPrevNodePrettierIgnore ||
isBlockHtmlWithoutBlankLineBetweenPrevHtml ||
isHtmlDirectAfterListItem
);
}
function shouldPrePrintTripleHardline({ node, previous }) {
const isPrevNodeList = previous.type === "list";
const isIndentedCode = node.type === "code" && node.isIndented;
return isPrevNodeList && isIndentedCode;
}
function shouldRemainTheSameContent(path) {
const node = path.findAncestor(
(node) => node.type === "linkReference" || node.type === "imageReference",
);
return (
node && (node.type !== "linkReference" || node.referenceType !== "full")
);
}
const encodeUrl = (url, characters) => {
for (const character of characters) {
url = url.replaceAll(character, encodeURIComponent(character));
}
return url;
};
/**
* @param {string} url
* @param {string[] | string} [dangerousCharOrChars]
* @returns {string}
*/
function printUrl(url, dangerousCharOrChars = []) {
const dangerousChars = [
" ",
...(Array.isArray(dangerousCharOrChars)
? dangerousCharOrChars
: [dangerousCharOrChars]),
];
return new RegExp(
dangerousChars.map((x) => escapeStringRegexp(x)).join("|"),
"u",
).test(url)
? `<${encodeUrl(url, "<>")}>`
: url;
}
function printTitle(title, options, printSpace = true) {
if (!title) {
return "";
}
if (printSpace) {
return " " + printTitle(title, options, false);
}
// title is escaped after `remark-parse` v7
title = title.replaceAll(/\\(?=["')])/gu, "");
if (title.includes('"') && title.includes("'") && !title.includes(")")) {
return `(${title})`; // avoid escaped quotes
}
const quote = getPreferredQuote(title, options.singleQuote);
title = title.replaceAll("\\", "\\\\");
title = title.replaceAll(quote, `\\${quote}`);
return `${quote}${title}${quote}`;
}
function clamp(value, min, max) {
return Math.max(min, Math.min(value, max));
}
function hasPrettierIgnore(path) {
return path.index > 0 && isPrettierIgnore(path.previous) === "next";
}
// `remark-parse` lowercase the `label` as `identifier`, we don't want do that
// https://github.com/remarkjs/remark/blob/daddcb463af2d5b2115496c395d0571c0ff87d15/packages/remark-parse/lib/tokenize/reference.js
function printLinkReference(node) {
return `[${collapseWhiteSpace(node.label)}]`;
}
function printFootnoteReference(node) {
return `[^${node.label}]`;
}
const printer = {
features: {
experimental_frontMatterSupport: {
massageAstNode: true,
embed: true,
print: true,
},
},
preprocess,
print: genericPrint,
embed,
massageAstNode: clean,
hasPrettierIgnore,
insertPragma,
getVisitorKeys,
};
export default printer;

View File

@@ -0,0 +1,23 @@
import { parseFrontMatter } from "../../main/front-matter/index.js";
/** @import {Plugin, Settings} from "unified" */
/**
* @type {Plugin<[], Settings>}
*/
const frontMatter = function () {
const proto = this.Parser.prototype;
proto.blockMethods = ["frontMatter", ...proto.blockMethods];
proto.blockTokenizers.frontMatter = tokenizer;
function tokenizer(eat, value) {
const { frontMatter } = parseFrontMatter(value);
if (frontMatter) {
return eat(frontMatter.raw)({ ...frontMatter, type: "frontMatter" });
}
}
tokenizer.onlyAtStart = true;
};
export default frontMatter;

View File

@@ -0,0 +1,19 @@
import { COMMENT_REGEX } from "../mdx.js";
import { INLINE_NODE_WRAPPER_TYPES, mapAst } from "../utils.js";
function htmlToJsx() {
return (ast) =>
mapAst(ast, (node, _index, [parent]) => {
if (
node.type !== "html" ||
// Keep HTML-style comments (legacy MDX)
COMMENT_REGEX.test(node.value) ||
INLINE_NODE_WRAPPER_TYPES.has(parent.type)
) {
return node;
}
return { ...node, type: "jsx" };
});
}
export default htmlToJsx;

View File

@@ -0,0 +1,27 @@
/** @import {Plugin, Settings} from "unified" */
/**
* @type {Plugin<[], Settings>}
*/
const liquid = function () {
const proto = this.Parser.prototype;
const methods = proto.inlineMethods;
methods.splice(methods.indexOf("text"), 0, "liquid");
proto.inlineTokenizers.liquid = tokenizer;
function tokenizer(eat, value) {
const match = value.match(/^(\{%.*?%\}|\{\{.*?\}\})/su);
if (match) {
return eat(match[0])({
type: "liquidNode",
value: match[0],
});
}
}
tokenizer.locator = function (value, fromIndex) {
return value.indexOf("{", fromIndex);
};
};
export default liquid;

View File

@@ -0,0 +1,32 @@
/** @import {Plugin, Settings} from "unified" */
/**
* @type {Plugin<[], Settings>}
*/
const wikiLink = function () {
const entityType = "wikiLink";
const wikiLinkRegex = /^\[\[(?<linkContents>.+?)\]\]/su;
const proto = this.Parser.prototype;
const methods = proto.inlineMethods;
methods.splice(methods.indexOf("link"), 0, entityType);
proto.inlineTokenizers.wikiLink = tokenizer;
function tokenizer(eat, value) {
const match = wikiLinkRegex.exec(value);
if (match) {
const linkContents = match.groups.linkContents.trim();
return eat(match[0])({
type: entityType,
value: linkContents,
});
}
}
tokenizer.locator = function (value, fromIndex) {
return value.indexOf("[", fromIndex);
};
};
export default wikiLink;

View File

@@ -0,0 +1,268 @@
import * as assert from "#universal/assert";
import { CJK_REGEXP, PUNCTUATION_REGEXP } from "./constants.evaluate.js";
import { locEnd, locStart } from "./loc.js";
const INLINE_NODE_TYPES = new Set([
"liquidNode",
"inlineCode",
"emphasis",
"esComment",
"strong",
"delete",
"wikiLink",
"link",
"linkReference",
"image",
"imageReference",
"footnote",
"footnoteReference",
"sentence",
"whitespace",
"word",
"break",
"inlineMath",
]);
const INLINE_NODE_WRAPPER_TYPES = new Set([
...INLINE_NODE_TYPES,
"tableCell",
"paragraph",
"heading",
]);
const KIND_NON_CJK = "non-cjk";
const KIND_CJ_LETTER = "cj-letter";
const KIND_K_LETTER = "k-letter";
const KIND_CJK_PUNCTUATION = "cjk-punctuation";
const K_REGEXP = /\p{Script_Extensions=Hangul}/u;
/**
* @typedef {" " | "\n" | ""} WhitespaceValue
* @typedef { KIND_NON_CJK | KIND_CJ_LETTER | KIND_K_LETTER | KIND_CJK_PUNCTUATION } WordKind
* @typedef {{
* type: "whitespace",
* value: WhitespaceValue,
* kind?: never
* }} WhitespaceNode
* @typedef {{
* type: "word",
* value: string,
* kind: WordKind,
* isCJ: boolean,
* hasLeadingPunctuation: boolean,
* hasTrailingPunctuation: boolean,
* }} WordNode
* Node for a single CJK character or a sequence of non-CJK characters
* @typedef {WhitespaceNode | WordNode} TextNode
*/
/**
* split text into whitespaces and words
* @param {string} text
*/
function splitText(text) {
/** @type {Array<TextNode>} */
const nodes = [];
const tokens = text.split(/([\t\n ]+)/u);
for (const [index, token] of tokens.entries()) {
// whitespace
if (index % 2 === 1) {
nodes.push({
type: "whitespace",
value: /\n/u.test(token) ? "\n" : " ",
});
continue;
}
// word separated by whitespace
if ((index === 0 || index === tokens.length - 1) && token === "") {
continue;
}
const innerTokens = token.split(new RegExp(`(${CJK_REGEXP.source})`, "u"));
for (const [innerIndex, innerToken] of innerTokens.entries()) {
if (
(innerIndex === 0 || innerIndex === innerTokens.length - 1) &&
innerToken === ""
) {
continue;
}
// non-CJK word
if (innerIndex % 2 === 0) {
if (innerToken !== "") {
appendNode({
type: "word",
value: innerToken,
kind: KIND_NON_CJK,
isCJ: false,
hasLeadingPunctuation: PUNCTUATION_REGEXP.test(innerToken[0]),
hasTrailingPunctuation: PUNCTUATION_REGEXP.test(innerToken.at(-1)),
});
}
continue;
}
// CJK character
// punctuation for CJ(K)
// Korean doesn't use them in horizontal writing usually
if (PUNCTUATION_REGEXP.test(innerToken)) {
appendNode({
type: "word",
value: innerToken,
kind: KIND_CJK_PUNCTUATION,
isCJ: true,
hasLeadingPunctuation: true,
hasTrailingPunctuation: true,
});
continue;
}
// Korean uses space to divide words, but Chinese & Japanese do not
// This is why Korean should be treated like non-CJK
if (K_REGEXP.test(innerToken)) {
appendNode({
type: "word",
value: innerToken,
kind: KIND_K_LETTER,
isCJ: false,
hasLeadingPunctuation: false,
hasTrailingPunctuation: false,
});
continue;
}
appendNode({
type: "word",
value: innerToken,
kind: KIND_CJ_LETTER,
isCJ: true,
hasLeadingPunctuation: false,
hasTrailingPunctuation: false,
});
}
}
// Check for `canBeConvertedToSpace` in ./print-whitespace.js etc.
if (process.env.NODE_ENV !== "production") {
for (let i = 1; i < nodes.length; i++) {
assert.ok(
!(nodes[i - 1].type === "whitespace" && nodes[i].type === "whitespace"),
"splitText should not create consecutive whitespace nodes",
);
}
}
return nodes;
function appendNode(node) {
const lastNode = nodes.at(-1);
if (
lastNode?.type === "word" &&
!isBetween(KIND_NON_CJK, KIND_CJK_PUNCTUATION) &&
// disallow leading/trailing full-width whitespace
![lastNode.value, node.value].some((value) => /\u3000/u.test(value))
) {
nodes.push({ type: "whitespace", value: "" });
}
nodes.push(node);
function isBetween(kind1, kind2) {
return (
(lastNode.kind === kind1 && node.kind === kind2) ||
(lastNode.kind === kind2 && node.kind === kind1)
);
}
}
}
function getOrderedListItemInfo(orderListItem, options) {
const text = options.originalText.slice(
orderListItem.position.start.offset,
orderListItem.position.end.offset,
);
const { numberText, leadingSpaces } = text.match(
/^\s*(?<numberText>\d+)(\.|\))(?<leadingSpaces>\s*)/u,
).groups;
return { number: Number(numberText), leadingSpaces };
}
function hasGitDiffFriendlyOrderedList(node, options) {
if (!node.ordered || node.children.length < 2) {
return false;
}
const secondNumber = getOrderedListItemInfo(node.children[1], options).number;
if (secondNumber !== 1) {
return false;
}
const firstNumber = getOrderedListItemInfo(node.children[0], options).number;
if (firstNumber !== 0) {
return true;
}
return (
node.children.length > 2 &&
getOrderedListItemInfo(node.children[2], options).number === 1
);
}
// The final new line should not include in value
// https://github.com/remarkjs/remark/issues/512
function getFencedCodeBlockValue(node, originalText) {
const { value } = node;
if (
node.position.end.offset === originalText.length &&
value.endsWith("\n") &&
// Code block has no end mark
originalText.endsWith("\n")
) {
return value.slice(0, -1);
}
return value;
}
function mapAst(ast, handler) {
return (function preorder(node, index, parentStack) {
const newNode = { ...handler(node, index, parentStack) };
if (newNode.children) {
newNode.children = newNode.children.map((child, index) =>
preorder(child, index, [newNode, ...parentStack]),
);
}
return newNode;
})(ast, null, []);
}
function isAutolink(node) {
if (node?.type !== "link" || node.children.length !== 1) {
return false;
}
const [child] = node.children;
return locStart(node) === locStart(child) && locEnd(node) === locEnd(child);
}
export {
getFencedCodeBlockValue,
getOrderedListItemInfo,
hasGitDiffFriendlyOrderedList,
INLINE_NODE_TYPES,
INLINE_NODE_WRAPPER_TYPES,
isAutolink,
KIND_CJ_LETTER,
KIND_CJK_PUNCTUATION,
KIND_K_LETTER,
KIND_NON_CJK,
mapAst,
splitText,
};

View File

@@ -0,0 +1,41 @@
const visitorKeys = {
root: ["children"],
paragraph: ["children"],
sentence: ["children"],
word: [],
whitespace: [],
emphasis: ["children"],
strong: ["children"],
delete: ["children"],
inlineCode: [],
wikiLink: [],
link: ["children"],
image: [],
blockquote: ["children"],
heading: ["children"],
code: [],
html: [],
list: ["children"],
thematicBreak: [],
linkReference: ["children"],
imageReference: [],
definition: [],
footnote: ["children"],
footnoteReference: [],
footnoteDefinition: ["children"],
table: ["children"],
tableCell: ["children"],
break: [],
liquidNode: [],
import: [],
export: [],
esComment: [],
jsx: [],
math: [],
inlineMath: [],
tableRow: ["children"],
listItem: ["children"],
text: [],
};
export default visitorKeys;

View File

@@ -0,0 +1,176 @@
import { describe, expect, it } from "bun:test";
import { printers } from "../src/index.js";
describe("Markdown Formatting in Descriptions", () => {
const printer = printers?.["openapi-ast"];
describe("Basic markdown formatting", () => {
it("should format description fields with markdown", () => {
expect(printer).toBeDefined();
const testData = {
isOpenAPI: true,
format: "yaml",
content: {
openapi: "3.0.0",
info: {
title: "Test API",
version: "1.0.0",
description: "This is a test description\n\n\nWith multiple spaces ",
},
paths: {
"/test": {
get: {
summary: "Get endpoint ",
description: "Endpoint description\n\n\nwith extra spaces",
operationId: "getTest",
responses: {
"200": {
description: "Success response",
},
},
},
},
},
},
originalText: "",
};
// @ts-expect-error We are mocking things here
const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error("Result is undefined");
}
const resultString = result.toString();
// Check that multiple spaces are normalized in the original content
// Note: YAML may format this differently, but the content should be processed
// The description field should exist and be formatted
expect(resultString).toContain("description:");
// Check that multiple blank lines are normalized
expect(resultString).not.toMatch(/\n{4,}/);
});
it("should preserve code blocks in descriptions", () => {
const testData = {
isOpenAPI: true,
format: "yaml",
content: {
openapi: "3.0.0",
info: {
title: "Test API",
version: "1.0.0",
description:
"Here is some code:\n\n const x = 1;\n const y = 2;\n\nAnd more text.",
},
},
originalText: "",
};
// @ts-expect-error We are mocking things here
const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error("Result is undefined");
}
const resultString = result.toString();
// Code blocks (4+ spaces) should be preserved
expect(resultString).toContain(" const x = 1;");
expect(resultString).toContain(" const y = 2;");
});
it("should format markdown in nested objects", () => {
const testData = {
isOpenAPI: true,
format: "yaml",
content: {
openapi: "3.0.0",
info: {
title: "Test API",
version: "1.0.0",
},
paths: {
"/test": {
get: {
operationId: "test",
parameters: [
{
name: "filter",
in: "query",
description: "Filter parameter with spaces",
},
],
responses: {
"200": {
description: "Success response",
},
},
},
},
},
},
originalText: "",
};
// @ts-expect-error We are mocking things here
const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error("Result is undefined");
}
const resultString = result.toString();
// Both parameter and response descriptions should be formatted
expect(resultString).toContain("description:");
});
});
describe("Summary field formatting", () => {
it("should format summary fields", () => {
const testData = {
isOpenAPI: true,
format: "yaml",
content: {
openapi: "3.0.0",
info: {
title: "Test API",
version: "1.0.0",
summary: "API summary with spaces",
},
paths: {
"/test": {
get: {
summary: "Get endpoint summary",
operationId: "test",
responses: { "200": { description: "OK" } },
},
},
},
},
originalText: "",
};
// @ts-expect-error We are mocking things here
const result = printer?.print({ getNode: () => testData }, { tabWidth: 2 });
expect(result).toBeDefined();
if (!result) {
throw new Error("Result is undefined");
}
const resultString = result.toString();
// Summary fields should be processed
expect(resultString).toContain("summary:");
});
});
});