also looking at this
Merge @stacksjs/ts-css into this repo as packages/toolkit
#27
128 files
+9797
-201
| @@ -12,7 +12,7 @@ | ||
| 12 | 12 | # The release workflow uses this file only when both it and that secret exist, |
| 13 | 13 | # and otherwise falls back to a plain NPM_TOKEN secret. |
| 14 | 14 | |
| 15 | # npm token used to publish `ts-css`. Publishing a package that does not exist | |
| 15 | # npm token used to publish `@ts-css/core`. Publishing a package that does not exist | |
| 16 | 16 | # yet needs a token that can CREATE packages — an Automation token, or a |
| 17 | 17 | # granular token with read/write across all packages. A granular token scoped |
| 18 | 18 | # to existing packages returns 404 on the first publish. |
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | ## About |
| 4 | 4 | |
| 5 | `ts-css` (published unscoped on npm; CLI binary `cssx`, config file `css.config.ts`, class prefix `tc`) is an on-demand CSS engine built with Bun, with two front ends over one atomic output: | |
| 5 | `ts-css` (published on npm as `@ts-css/core`; CLI binary `cssx`, config file `css.config.ts`, class prefix `tc`) is an on-demand CSS engine built with Bun, with two front ends over one atomic output: | |
| 6 | 6 | |
| 7 | 7 | - **Utility classes** — Tailwind v4-compatible, with full variant support (responsive, state, dark mode, pseudo-elements), arbitrary values, shortcut aliases, and compile-class HTML optimization. |
| 8 | 8 | - **Style objects** — a StyleX-shaped typed API (`css.create`, `css.props`, `css.defineVars`, `css.createTheme`, `css.keyframes`) in `src/style/`, collected at build time by evaluating the modules that declare them. |
| @@ -11,7 +11,7 @@ A CSS engine with two front ends and one atomic output. | ||
| 11 | 11 | Write **utility classes** when you are laying out markup, and Tailwind v4 semantics apply unchanged. Write **typed style objects** when a style has to be computed, tokenised, or merged across a component boundary, and you get StyleX's authoring model. Both compile through the same pipeline into the same deduplicated atomic CSS, in one stylesheet, with nothing left at runtime. |
| 12 | 12 | |
| 13 | 13 | ```bash |
| 14 | bun add ts-css | |
| 14 | bun add @ts-css/core | |
| 15 | 15 | ``` |
| 16 | 16 | |
| 17 | 17 | ## Why two APIs |
| @@ -43,7 +43,7 @@ bunx cssx watch # rebuilds on change | ||
| 43 | 43 | |
| 44 | 44 | ```ts |
| 45 | 45 | // css.config.ts |
| 46 | import type { TsCssOptions } from 'ts-css' | |
| 46 | import type { TsCssOptions } from '@ts-css/core' | |
| 47 | 47 | |
| 48 | 48 | export default { |
| 49 | 49 | content: ['./src/**/*.{html,ts,tsx,stx}'], |
| @@ -57,7 +57,7 @@ export default { | ||
| 57 | 57 | ## Style objects |
| 58 | 58 | |
| 59 | 59 | ```ts |
| 60 | import { css } from 'ts-css' | |
| 60 | import { css } from '@ts-css/core' | |
| 61 | 61 | |
| 62 | 62 | const theme = css.defineVars({ |
| 63 | 63 | accent: '#0b7', |
| @@ -223,7 +223,7 @@ cssx clean # remove the output file | ||
| 223 | 223 | ## Configuration |
| 224 | 224 | |
| 225 | 225 | ```ts |
| 226 | import type { TsCssOptions } from 'ts-css' | |
| 226 | import type { TsCssOptions } from '@ts-css/core' | |
| 227 | 227 | |
| 228 | 228 | export default { |
| 229 | 229 | content: ['./src/**/*.{html,ts,tsx}'], |
| @@ -296,7 +296,7 @@ The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information. | ||
| 296 | 296 | Made with �� |
| 297 | 297 | |
| 298 | 298 | <!-- Badges --> |
| 299 | [npm-version-src]: https://img.shields.io/npm/v/ts-css?style=flat-square | |
| 300 | [npm-version-href]: https://npmjs.com/package/ts-css | |
| 299 | [npm-version-src]: https://img.shields.io/npm/v/@ts-css/core?style=flat-square | |
| 300 | [npm-version-href]: https://npmjs.com/package/@ts-css/core | |
| 301 | 301 | [github-actions-src]: https://img.shields.io/github/actions/workflow/status/cwcss/crosswind/ci.yml?style=flat-square&branch=main |
| 302 | 302 | [github-actions-href]: https://github.com/cwcss/crosswind/actions?query=workflow%3Aci |
| @@ -157,7 +157,7 @@ const transpiler = new Bun.Transpiler({ loader: 'ts' }) | ||
| 157 | 157 | |
| 158 | 158 | const tsCssSources = new Map(WORKLOADS.map(w => [ |
| 159 | 159 | w.name, |
| 160 | `import { css } from 'ts-css'\nexport const styles = css.create(${w.literal})\n`, | |
| 160 | `import { css } from '@ts-css/core'\nexport const styles = css.create(${w.literal})\n`, | |
| 161 | 161 | ])) |
| 162 | 162 | |
| 163 | 163 | /** |
bun.lockmodified+76-16
| @@ -3,24 +3,46 @@ | ||
| 3 | 3 | "configVersion": 1, |
| 4 | 4 | "workspaces": { |
| 5 | 5 | "": { |
| 6 | "name": "ts-css", | |
| 6 | "name": "@ts-css/core", | |
| 7 | 7 | "devDependencies": { |
| 8 | 8 | "better-dx": "^0.2.15", |
| 9 | 9 | }, |
| 10 | 10 | }, |
| 11 | "packages/toolkit": { | |
| 12 | "name": "@stacksjs/ts-css", | |
| 13 | "version": "0.1.4", | |
| 14 | "bin": { | |
| 15 | "ts-css": "./dist/bin/cli.js", | |
| 16 | }, | |
| 17 | "dependencies": { | |
| 18 | "bunfig": "^0.15.6", | |
| 19 | }, | |
| 20 | "devDependencies": { | |
| 21 | "@types/bun": "latest", | |
| 22 | "@types/css-tree": "^2.3.11", | |
| 23 | "@types/csso": "^5.0.4", | |
| 24 | "better-dx": "^0.2.15", | |
| 25 | "css-select": "^7.0.0", | |
| 26 | "css-tree": "^3.2.1", | |
| 27 | "css-what": "^8.0.0", | |
| 28 | "csso": "^5.0.5", | |
| 29 | "mitata": "^1.0.34", | |
| 30 | }, | |
| 31 | }, | |
| 11 | 32 | "packages/ts-css": { |
| 12 | "name": "ts-css", | |
| 33 | "name": "@ts-css/core", | |
| 13 | 34 | "version": "0.3.0", |
| 14 | 35 | "bin": { |
| 15 | 36 | "cssx": "./dist/cli.js", |
| 16 | 37 | }, |
| 17 | 38 | "dependencies": { |
| 18 | 39 | "@stacksjs/clapp": "^0.2.0", |
| 40 | "@stacksjs/ts-css": "workspace:*", | |
| 19 | 41 | "bunfig": "^0.15.17", |
| 20 | 42 | }, |
| 21 | 43 | }, |
| 22 | 44 | "packages/vscode": { |
| 23 | "name": "ts-css-vscode", | |
| 45 | "name": "@ts-css/vscode", | |
| 24 | 46 | "version": "0.3.0", |
| 25 | 47 | }, |
| 26 | 48 | }, |
| @@ -35,11 +57,11 @@ | ||
| 35 | 57 | |
| 36 | 58 | "@stacksjs/bunpress": ["@stacksjs/bunpress@0.2.14", "", { "dependencies": { "@cwcss/crosswind": "^0.2.16", "@stacksjs/clapp": "^0.2.0", "@stacksjs/stx": "^0.2.171", "@stacksjs/ts-i18n": "^0.1.11", "bunfig": "^0.15.17", "ts-syntax-highlighter": "^0.2.14" }, "peerDependencies": { "@stacksjs/ts-cloud": "^0.7.51" }, "optionalPeers": ["@stacksjs/ts-cloud"], "bin": { "bunpress": "./dist/bin/cli.js" } }, "sha512-o0Bf8b7kXKSyH5xXrVrdA46gPzMVssLBnbP82t8WzF17gIveFLNW+CzEZe3n9o+Ui2wiy2+6IF5SwGfrdTtWcQ=="], |
| 37 | 59 | |
| 38 | "@stacksjs/clapp": ["@stacksjs/clapp@0.2.13", "", {}, "sha512-wh6Ybgga1S/3P+GRDb2kBiOK97+Ay7TKTiaLKfhpgd+V04+BAHg23xDlZUNkpllhoRdFimeHpWahuwghoNYbUg=="], | |
| 60 | "@stacksjs/clapp": ["@stacksjs/clapp@0.2.15", "", {}, "sha512-EM2fWrFZaU+paHZ4X0RGNZTnBYVgwdkbTnYdLD72yoc8JXUH5Wl88IZcn/JZoFPrViNgXlhYgI3xEK4Fryd3Xw=="], | |
| 39 | 61 | |
| 40 | 62 | "@stacksjs/clarity": ["@stacksjs/clarity@0.3.33", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "clarity": "./dist/bin/cli.js" } }, "sha512-+5GLKnAI/M/z64UfNLLfQqx/kp+frmRY+NZXYwwGwbrkud/xlsEGDWEExdhqOhqEDMWdb1oafw/ubGAPg1PaFw=="], |
| 41 | 63 | |
| 42 | "@stacksjs/desktop": ["@stacksjs/desktop@0.2.237", "", { "dependencies": { "craft-native": ">=0.0.76" } }, "sha512-hpC2xTRn6uxgNPMomJBURQG7TYJPxWFoLzpzJePzKVpi0o2lEt4vZRxAGdZRQgc0ZTm9QCP3pTqRR+k3t0bEbw=="], | |
| 64 | "@stacksjs/desktop": ["@stacksjs/desktop@0.2.249", "", { "dependencies": { "craft-native": ">=0.0.76" } }, "sha512-mQfxzJW2wGzTFffa1NIYVA0qSbJH7QSHocYJbKZxBSQR5qD8u3SXfpxDNg3YcgoHCj6CfH/BsuSg5jqaiyRTpw=="], | |
| 43 | 65 | |
| 44 | 66 | "@stacksjs/dtsx": ["@stacksjs/dtsx@0.11.10", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0" }, "bin": { "dtsx": "./dist/bin/cli.js" } }, "sha512-NOSSGv7mdxzmijmKHxTZDmgZp7vCYrRKWpDwNqwTc97MTv5csjo3uukf7A/c+s7pSzJd+HxqsTCZ7IznjfEtmw=="], |
| 45 | 67 | |
| @@ -47,15 +69,15 @@ | ||
| 47 | 69 | |
| 48 | 70 | "@stacksjs/logsmith": ["@stacksjs/logsmith@0.2.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.6", "pickier": "^0.1.9" }, "bin": { "@stacksjs/logsmith": "./dist/bin/cli.js", "logsmith": "./dist/bin/cli.js" } }, "sha512-wqqAxewpsHVDzpC+IRy+yYyGXGlv1j5GCqYTh8fL7d3LLhnRPN72PEQywM357burtORvdkFHfOzk3Ih1SMvuiw=="], |
| 49 | 71 | |
| 50 | "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.237", "", {}, "sha512-hx51XqFdkoeVEKGXj083SGyY57BnWFRu8yWTrPk/FZlVqwup8XQGyiF0FTcf/u2z4pyRzpQ/pDCgsTQsh+d6tQ=="], | |
| 72 | "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.249", "", {}, "sha512-u7xHcDDeyj+IfDsqAEGpuFoZeRWuKfpnpexfyjPCOvBhJlFWIYKvFVY9QjuKN/kFQxDv5hYK9vkROpZAIVSzNQ=="], | |
| 51 | 73 | |
| 52 | "@stacksjs/stx": ["@stacksjs/stx@0.2.237", "", { "dependencies": { "@cwcss/crosswind": "^0.2.19", "@stacksjs/clapp": "^0.2.0", "@stacksjs/desktop": "0.2.237", "@stacksjs/sanitizer": "0.2.237", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.237", "bunfig": "^0.15.17", "stx-router": "0.2.237", "ts-broadcasting": "^0.0.9", "ts-images": "^0.2.11", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-trsm4du5m4doOJwZztrVgYlClUxwi5wzfb79V/o5Vu5iw9WIdFbfwiOz/zcxDeoqBp6+eAVqD0bY1Jvq7/wABQ=="], | |
| 74 | "@stacksjs/stx": ["@stacksjs/stx@0.2.249", "", { "dependencies": { "@cwcss/crosswind": "^0.2.19", "@stacksjs/clapp": "^0.2.14", "@stacksjs/desktop": "0.2.249", "@stacksjs/sanitizer": "0.2.249", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.249", "bunfig": "^0.15.17", "stx-router": "0.2.249", "ts-broadcasting": "^0.0.9", "ts-images": "^0.2.11", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-3r2RgvcrnJZ4FKwsjFMyOPwpyKtBV8RWnTVGby5C9XCKe0d/DsF125+A/8LAXTjB4z32NUYRDVnSpvhd1vRxPA=="], | |
| 53 | 75 | |
| 54 | 76 | "@stacksjs/ts-avif": ["@stacksjs/ts-avif@0.1.4", "", {}, "sha512-+ri7xxHLxaFGRJPihI7irDZpdLEIrEFpR7Em+ojfDbTjXQmXtQBqN/9WKNhBdQ1BZ2M1ckjdgnxmfege86QQ5Q=="], |
| 55 | 77 | |
| 56 | 78 | "@stacksjs/ts-bmp": ["@stacksjs/ts-bmp@0.1.1", "", {}, "sha512-TIgm4rbCoYOijlg2gQuAyJErXeeoVzra4snxNvZ2fe6QBGIA0gm5147oo3t/cIeTMUzbmnyJ1g0QbT4kP4Ck9Q=="], |
| 57 | 79 | |
| 58 | "@stacksjs/ts-css": ["@stacksjs/ts-css@0.1.4", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "ts-css": "./dist/bin/cli.js" } }, "sha512-QvHvgagJ6kQY3+zY37andUEJFKq/v4v2JpDCrvVdbX3TYCLuRQExZl8Qf3ai+Fl9BN2oWikH+DfKSs5HuEZrGQ=="], | |
| 80 | "@stacksjs/ts-css": ["@stacksjs/ts-css@workspace:packages/toolkit"], | |
| 59 | 81 | |
| 60 | 82 | "@stacksjs/ts-i18n": ["@stacksjs/ts-i18n@0.1.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.9", "bunfig": "^0.15.6" }, "bin": { "i18n": "./dist/cli.js" } }, "sha512-33FM0gKdxBy6EUsUZLGAMe0D7QUZ4f/BQXRsQIyzkZgs2+juruUTevPuJGCjWSZsgJuiaBfEJn8Oq8ctuo1OWQ=="], |
| 61 | 83 | |
| @@ -67,8 +89,16 @@ | ||
| 67 | 89 | |
| 68 | 90 | "@stacksjs/ts-webp": ["@stacksjs/ts-webp@0.1.3", "", {}, "sha512-0PFmmBLKCxIc+8pNEwfvoXfS5uH47y7rXpVaGEfj6VQGPoQCAWUmheppCeJ66YCdSVR1dezR/4MGTZCnT+uZow=="], |
| 69 | 91 | |
| 92 | "@ts-css/core": ["@ts-css/core@workspace:packages/ts-css"], | |
| 93 | ||
| 94 | "@ts-css/vscode": ["@ts-css/vscode@workspace:packages/vscode"], | |
| 95 | ||
| 70 | 96 | "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], |
| 71 | 97 | |
| 98 | "@types/css-tree": ["@types/css-tree@2.3.11", "", {}, "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg=="], | |
| 99 | ||
| 100 | "@types/csso": ["@types/csso@5.0.5", "", { "dependencies": { "@types/css-tree": "^2" } }, "sha512-Et/B3W1/1XMjG9SxwitiokWDabA9N8otVoTBJF2RmZ8itU0c5h7qmuZcTpp17BqfAnsdfkGQ3hwizff3nJvWSQ=="], | |
| 101 | ||
| 72 | 102 | "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], |
| 73 | 103 | |
| 74 | 104 | "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], |
| @@ -113,37 +143,61 @@ | ||
| 113 | 143 | |
| 114 | 144 | "better-dx": ["better-dx@0.2.25", "", { "dependencies": { "@stacksjs/bumpx": "^0.2.14", "@stacksjs/bunpress": "^0.2.8", "@stacksjs/clapp": "^0.2.12", "@stacksjs/clarity": "^0.3.31", "@stacksjs/gitlint": "^0.1.7", "@stacksjs/logsmith": "^0.2.6", "@types/bun": "^1.3.14", "buddy-bot": "^0.10.5", "bun-git-hooks": "^0.3.2", "bun-plugin-dtsx": "^0.11.1", "bunfig": "^0.15.15", "pickier": "^0.1.40", "ts-error-handling": "^0.1.3", "typescript": "^7.0.2" } }, "sha512-7GQ3EDXDwdZbpzwqWrOC2kzKkfw2Ptd30Bl1VlLS1SV82MvU5VIYXtGsRcJ5+6M17n9hQwGuT2TGEZEaWgZQaw=="], |
| 115 | 145 | |
| 146 | "boolbase": ["boolbase@2.0.0", "", {}, "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA=="], | |
| 147 | ||
| 116 | 148 | "buddy-bot": ["buddy-bot@0.10.5", "", { "dependencies": { "prompts": "^2.4.2", "ts-pantry": "^0.11.32" }, "peerDependencies": { "@anthropic-ai/sdk": "^0.117.1" }, "optionalPeers": ["@anthropic-ai/sdk"], "bin": { "buddy-bot": "./dist/bin/cli.js" } }, "sha512-pGyjHSi91Nix76u1VIVLEYEV6vkTVKvKS9NvRYbyGR6X6CdZQrvHcmJLfGqjN/4xuUWc4h6rlVpkiwG6MOo/6Q=="], |
| 117 | 149 | |
| 118 | 150 | "bun-git-hooks": ["bun-git-hooks@0.3.3", "", { "bin": { "git-hooks": "./dist/bin/cli.js", "bun-git-hooks": "./dist/bin/cli.js" } }, "sha512-kcg8teRh/cMs8uZyt/+A/bBuJnmc6EvjnBJ12pWEJ61L9z3uSuqm0mvjrYX4Iu7SZPAwpvAK5fFKmqevS4WBkA=="], |
| 119 | 151 | |
| 120 | 152 | "bun-plugin-dtsx": ["bun-plugin-dtsx@0.11.10", "", { "dependencies": { "@stacksjs/dtsx": "0.11.10" } }, "sha512-qQrEBIV1pzhDmrxmhTHhgVK5gqE5CfpO8C9NCp7zUaN48c4IJVrpqXbeNWch7sAT/Hf4GhVMWau7T65xySm59Q=="], |
| 121 | 153 | |
| 122 | "bun-plugin-stx": ["bun-plugin-stx@0.2.237", "", { "dependencies": { "@stacksjs/stx": "^0.2.227", "stx-router": "^0.2.227" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-LYBUiSNKtdkTHVeiUAYFPrrNGJWu4o/jLjOi2K1MiWDuOCqaQ6VHZItuw52nR4suVHOL5hbVJWRS93sQ+7dXxA=="], | |
| 154 | "bun-plugin-stx": ["bun-plugin-stx@0.2.249", "", { "dependencies": { "@stacksjs/stx": "^0.2.227", "stx-router": "^0.2.227" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-TQ6UEPX54pATWLq27VnkQCAiOBbiM1roOPye6nbSjgmRj54mqz9we0HPfepfj+ahYvkniEiwIVw9Ym3xCXNjzQ=="], | |
| 123 | 155 | |
| 124 | 156 | "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], |
| 125 | 157 | |
| 126 | 158 | "bunfig": ["bunfig@0.15.17", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./bin/cli.js" } }, "sha512-nJAYGUq29lMmKindrIQIU5MD0JFFRoVFt+PBoNEvvVO9gKS3WgsYQVQLzN0gzHNnZtOZSR9p7jTBiRCwjQpr1g=="], |
| 127 | 159 | |
| 128 | "craft-native": ["craft-native@0.0.77", "", { "bin": { "craft-sdk": "./dist/cli.js" } }, "sha512-hBqFxZC22GVt8wv3srd/rsDTKykSzM/aFJDDM8+W/Mk8CuZDFjlOIkRSnyJqZwyVf4bvkRzdegd9fiOfnwUNOg=="], | |
| 160 | "craft-native": ["craft-native@0.0.83", "", { "bin": { "craft-sdk": "./dist/cli.js" } }, "sha512-6FWJmaTG8TYpxKvj7gCYhkylq8jeY6anE75zNcxGbZDRt5AeaKpLBIdCo2AlCHkw1amuFUdahrJ6vPZdInGbQQ=="], | |
| 161 | ||
| 162 | "css-select": ["css-select@7.0.0", "", { "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", "domhandler": "^6.0.1", "domutils": "^4.0.2", "nth-check": "^3.0.1" } }, "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g=="], | |
| 163 | ||
| 164 | "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], | |
| 165 | ||
| 166 | "css-what": ["css-what@8.0.0", "", {}, "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw=="], | |
| 167 | ||
| 168 | "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], | |
| 129 | 169 | |
| 130 | 170 | "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], |
| 131 | 171 | |
| 172 | "dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="], | |
| 173 | ||
| 174 | "domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="], | |
| 175 | ||
| 176 | "domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="], | |
| 177 | ||
| 178 | "domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="], | |
| 179 | ||
| 180 | "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], | |
| 181 | ||
| 132 | 182 | "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], |
| 133 | 183 | |
| 184 | "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], | |
| 185 | ||
| 186 | "mitata": ["mitata@1.0.34", "", {}, "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA=="], | |
| 187 | ||
| 188 | "nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="], | |
| 189 | ||
| 134 | 190 | "pickier": ["pickier@0.1.65", "", { "dependencies": { "@stacksjs/clapp": "^0.2.3", "@stacksjs/clarity": "^0.3.24" }, "optionalDependencies": { "@stacksjs/ts-spell-check": "^0.1.0" }, "bin": { "pickier": "./dist/bin/cli.js" } }, "sha512-hpGKvrWwzzHeV3hMErtnZoGRD/5Wmvy9sKxxHAWvMMdmdzr2DMtuk2T8Vkr8ZoUCNeTKRRPgHTMHg0oCNxDnDA=="], |
| 135 | 191 | |
| 136 | 192 | "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], |
| 137 | 193 | |
| 138 | 194 | "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], |
| 139 | 195 | |
| 140 | "stx-router": ["stx-router@0.2.237", "", {}, "sha512-sEuFmBW4YIgaF79AkBrqhBmkN6t6sdFISCHlalxoLb+QK4xyesc7CXnk6UOcLTkfEP89ojHyvM/8yvK2bqYdFg=="], | |
| 141 | ||
| 142 | "ts-broadcasting": ["ts-broadcasting@0.0.9", "", { "bin": { "broadcast": "./dist/bin/cli.js" } }, "sha512-OA95bHPe3M/24Gx8FpbUV0+NzGwvemG2LP64s1p/zATagX8xJckeziru03ccjW0ExWaD+W5tC1GRIqS6lfbB4A=="], | |
| 196 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | |
| 143 | 197 | |
| 144 | "ts-css": ["ts-css@workspace:packages/ts-css"], | |
| 198 | "stx-router": ["stx-router@0.2.249", "", {}, "sha512-zKCQH2S/qj2FbxYhreS9UdCw0vKGiD1lx//ZohPaXqmIIKdha4gL219DyV7gfdQKrZbjOGEf6N7dN+tvu42gbQ=="], | |
| 145 | 199 | |
| 146 | "ts-css-vscode": ["ts-css-vscode@workspace:packages/vscode"], | |
| 200 | "ts-broadcasting": ["ts-broadcasting@0.0.9", "", { "bin": { "broadcast": "./dist/bin/cli.js" } }, "sha512-OA95bHPe3M/24Gx8FpbUV0+NzGwvemG2LP64s1p/zATagX8xJckeziru03ccjW0ExWaD+W5tC1GRIqS6lfbB4A=="], | |
| 147 | 201 | |
| 148 | 202 | "ts-error-handling": ["ts-error-handling@0.1.3", "", {}, "sha512-Agm8wbQL1YVvnffVDSUKMgImZBoR4UVFjzPaIwPxa0z7YwsowKV8R57pCDIjLP/dTHRBetg7ZfdTeuNVb21OfA=="], |
| 149 | 203 | |
| @@ -153,12 +207,18 @@ | ||
| 153 | 207 | |
| 154 | 208 | "ts-jpeg": ["ts-jpeg@0.3.5", "", {}, "sha512-etdIfMDRDYDF44kDu++z0M5GbR7ph9IKmubSXQop1O8NTUc+xjxE8dPt/niosHRUhMiQ4gaLtuFh0DH6QvfopA=="], |
| 155 | 209 | |
| 156 | "ts-pantry": ["ts-pantry@0.11.44", "", { "bin": { "ts-pantry": "./dist/bin/cli.js" } }, "sha512-xnvzhbhwdRk75aUyyXiSkSICRJNcUYR+xXtLUmRRufZiEBf4T1zrIPiEdgPWDhN0PYLCeA7GNi098yHxMcVvqg=="], | |
| 210 | "ts-pantry": ["ts-pantry@0.11.45", "", { "bin": { "ts-pantry": "./dist/bin/cli.js" } }, "sha512-wh8pGg7EYrK0bR2rz71SFMniJSq9OFNMZTVkxvk04ywedSP8ECjYLpEg/MK1VRGJjW2/FfIZskkxVQq211jjSw=="], | |
| 157 | 211 | |
| 158 | 212 | "ts-syntax-highlighter": ["ts-syntax-highlighter@0.2.17", "", { "bin": { "ts-syntax-highlighter": "./dist/bin/cli.js" } }, "sha512-cHxkTRvqYabO9axIrw6KUkMweIHzXmVmcVmO+Yi2NEa9nxQa15zoISvvlKYPzK4OLsUS5I4uVaLz0Oivc3oOSw=="], |
| 159 | 213 | |
| 160 | 214 | "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], |
| 161 | 215 | |
| 162 | 216 | "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], |
| 217 | ||
| 218 | "@stacksjs/ts-svg/@stacksjs/ts-css": ["@stacksjs/ts-css@0.1.4", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "ts-css": "./dist/bin/cli.js" } }, "sha512-QvHvgagJ6kQY3+zY37andUEJFKq/v4v2JpDCrvVdbX3TYCLuRQExZl8Qf3ai+Fl9BN2oWikH+DfKSs5HuEZrGQ=="], | |
| 219 | ||
| 220 | "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], | |
| 221 | ||
| 222 | "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], | |
| 163 | 223 | } |
| 164 | 224 | } |
| @@ -16,7 +16,7 @@ While ts-css includes comprehensive built-in utilities, you can add custom rules | ||
| 16 | 16 | Define custom rules in your configuration: |
| 17 | 17 | |
| 18 | 18 | ```typescript |
| 19 | import type { TsCssOptions } from 'ts-css' | |
| 19 | import type { TsCssOptions } from '@ts-css/core' | |
| 20 | 20 | |
| 21 | 21 | const config = { |
| 22 | 22 | rules: [ |
| @@ -234,7 +234,7 @@ const config = { | ||
| 234 | 234 | ```### Using Theme Values |
| 235 | 235 | |
| 236 | 236 | Access theme values in custom rules:```typescript |
| 237 | import type { TsCssOptions } from 'ts-css' | |
| 237 | import type { TsCssOptions } from '@ts-css/core' | |
| 238 | 238 | |
| 239 | 239 | const config = { |
| 240 | 240 | theme: { |
| @@ -15,7 +15,7 @@ cd my-app | ||
| 15 | 15 | |
| 16 | 16 | # Install ts-css |
| 17 | 17 | |
| 18 | bun add ts-css | |
| 18 | bun add @ts-css/core | |
| 19 | 19 | |
| 20 | 20 | # Initialize ts-css |
| 21 | 21 | |
| @@ -23,7 +23,7 @@ bunx cssx init | ||
| 23 | 23 | ```**Configuration:**```typescript |
| 24 | 24 | |
| 25 | 25 | // css.config.ts |
| 26 | import type { TsCssOptions } from 'ts-css' | |
| 26 | import type { TsCssOptions } from '@ts-css/core' | |
| 27 | 27 | |
| 28 | 28 | const config = { |
| 29 | 29 | content: ['./src/**/*.{js,jsx,ts,tsx}'], |
| @@ -90,7 +90,7 @@ bun install | ||
| 90 | 90 | |
| 91 | 91 | # Install ts-css |
| 92 | 92 | |
| 93 | bun add ts-css | |
| 93 | bun add @ts-css/core | |
| 94 | 94 | bunx cssx init |
| 95 | 95 | |
| 96 | 96 | ```**Configuration:**```typescript |
| @@ -118,7 +118,7 @@ cd my-app | ||
| 118 | 118 | |
| 119 | 119 | # Install ts-css |
| 120 | 120 | |
| 121 | bun add ts-css | |
| 121 | bun add @ts-css/core | |
| 122 | 122 | bunx cssx init |
| 123 | 123 | ```**Configuration:**```typescript |
| 124 | 124 | |
| @@ -191,7 +191,7 @@ bun install | ||
| 191 | 191 | |
| 192 | 192 | # Install ts-css |
| 193 | 193 | |
| 194 | bun add ts-css | |
| 194 | bun add @ts-css/core | |
| 195 | 195 | bunx cssx init |
| 196 | 196 | |
| 197 | 197 | ```**Configuration:**```typescript |
| @@ -263,7 +263,7 @@ bun install | ||
| 263 | 263 | |
| 264 | 264 | # Install ts-css |
| 265 | 265 | |
| 266 | bun add ts-css | |
| 266 | bun add @ts-css/core | |
| 267 | 267 | bunx cssx init |
| 268 | 268 | ```**Configuration:**```typescript |
| 269 | 269 | |
| @@ -307,7 +307,7 @@ bun install | ||
| 307 | 307 | |
| 308 | 308 | # Install ts-css |
| 309 | 309 | |
| 310 | bun add ts-css | |
| 310 | bun add @ts-css/core | |
| 311 | 311 | bunx cssx init |
| 312 | 312 | ```**Configuration:**```typescript |
| 313 | 313 | |
| @@ -358,7 +358,7 @@ const config = { | ||
| 358 | 358 | bun create vite my-app --template svelte-ts |
| 359 | 359 | cd my-app |
| 360 | 360 | bun install |
| 361 | bun add ts-css | |
| 361 | bun add @ts-css/core | |
| 362 | 362 | bunx cssx init |
| 363 | 363 | |
| 364 | 364 | ```**Configuration:**```typescript |
| @@ -377,7 +377,7 @@ bun install | ||
| 377 | 377 | |
| 378 | 378 | # Install ts-css |
| 379 | 379 | |
| 380 | bun add ts-css | |
| 380 | bun add @ts-css/core | |
| 381 | 381 | bunx cssx init |
| 382 | 382 | |
| 383 | 383 | ```**Configuration:**```typescript |
| @@ -444,7 +444,7 @@ bun install | ||
| 444 | 444 | |
| 445 | 445 | # Install ts-css |
| 446 | 446 | |
| 447 | bun add ts-css | |
| 447 | bun add @ts-css/core | |
| 448 | 448 | bunx cssx init |
| 449 | 449 | ```**Configuration:**```typescript |
| 450 | 450 | |
| @@ -471,7 +471,7 @@ bun install | ||
| 471 | 471 | |
| 472 | 472 | # Install ts-css |
| 473 | 473 | |
| 474 | bun add ts-css | |
| 474 | bun add @ts-css/core | |
| 475 | 475 | bunx cssx init |
| 476 | 476 | |
| 477 | 477 | ```**Configuration:**```typescript |
| @@ -491,7 +491,7 @@ import './styles.css' | ||
| 491 | 491 | |
| 492 | 492 | # Install ts-css |
| 493 | 493 | |
| 494 | bun add ts-css | |
| 494 | bun add @ts-css/core | |
| 495 | 495 | bunx cssx init |
| 496 | 496 | ```**Configuration:**```typescript |
| 497 | 497 | |
| @@ -525,7 +525,7 @@ const config = { | ||
| 525 | 525 | |
| 526 | 526 | # In your Hugo project |
| 527 | 527 | |
| 528 | bun add ts-css | |
| 528 | bun add @ts-css/core | |
| 529 | 529 | bunx cssx init |
| 530 | 530 | ```**Configuration:**```typescript |
| 531 | 531 | |
| @@ -31,7 +31,7 @@ const config = { | ||
| 31 | 31 | ### Basic Preset Structure |
| 32 | 32 | |
| 33 | 33 | A preset is an object that implements the`Preset`interface:```typescript |
| 34 | import type { Preset } from 'ts-css' | |
| 34 | import type { Preset } from '@ts-css/core' | |
| 35 | 35 | |
| 36 | 36 | export const myPreset: Preset = { |
| 37 | 37 | name: 'my-preset', |
| @@ -54,7 +54,7 @@ export const myPreset: Preset = { | ||
| 54 | 54 | |
| 55 | 55 | ```### Simple Example```typescript |
| 56 | 56 | // presets/minimal.ts |
| 57 | import type { Preset } from 'ts-css' | |
| 57 | import type { Preset } from '@ts-css/core' | |
| 58 | 58 | |
| 59 | 59 | export const minimalPreset: Preset = { |
| 60 | 60 | name: 'minimal', |
| @@ -82,7 +82,7 @@ export const minimalPreset: Preset = { | ||
| 82 | 82 | ### Single Preset```typescript |
| 83 | 83 | |
| 84 | 84 | // css.config.ts |
| 85 | import type { TsCssOptions } from 'ts-css' | |
| 85 | import type { TsCssOptions } from '@ts-css/core' | |
| 86 | 86 | import { minimalPreset } from './presets/minimal' |
| 87 | 87 | |
| 88 | 88 | const config = { |
| @@ -133,7 +133,7 @@ const config = { | ||
| 133 | 133 | ### Design System Preset```typescript |
| 134 | 134 | |
| 135 | 135 | // presets/design-system.ts |
| 136 | import type { Preset } from 'ts-css' | |
| 136 | import type { Preset } from '@ts-css/core' | |
| 137 | 137 | |
| 138 | 138 | export const designSystemPreset: Preset = { |
| 139 | 139 | name: 'design-system', |
| @@ -212,7 +212,7 @@ export const designSystemPreset: Preset = { | ||
| 212 | 212 | ```### Component Library Preset```typescript |
| 213 | 213 | |
| 214 | 214 | // presets/components.ts |
| 215 | import type { Preset } from 'ts-css' | |
| 215 | import type { Preset } from '@ts-css/core' | |
| 216 | 216 | |
| 217 | 217 | export const componentsPreset: Preset = { |
| 218 | 218 | name: 'components', |
| @@ -254,7 +254,7 @@ export const componentsPreset: Preset = { | ||
| 254 | 254 | |
| 255 | 255 | ```### Utility Preset```typescript |
| 256 | 256 | // presets/utilities.ts |
| 257 | import type { Preset } from 'ts-css' | |
| 257 | import type { Preset } from '@ts-css/core' | |
| 258 | 258 | |
| 259 | 259 | export const utilitiesPreset: Preset = { |
| 260 | 260 | name: 'utilities', |
| @@ -307,7 +307,7 @@ export const utilitiesPreset: Preset = { | ||
| 307 | 307 | ```### Framework-Specific Preset```typescript |
| 308 | 308 | |
| 309 | 309 | // presets/react.ts |
| 310 | import type { Preset } from 'ts-css' | |
| 310 | import type { Preset } from '@ts-css/core' | |
| 311 | 311 | |
| 312 | 312 | export const reactPreset: Preset = { |
| 313 | 313 | name: 'react', |
| @@ -360,7 +360,7 @@ my-ts-css-preset/ | ||
| 360 | 360 | |
| 361 | 361 | ```typescript |
| 362 | 362 | // src/index.ts |
| 363 | import type { Preset } from 'ts-css' | |
| 363 | import type { Preset } from '@ts-css/core' | |
| 364 | 364 | |
| 365 | 365 | export const myPreset: Preset = { |
| 366 | 366 | name: 'my-preset', |
| @@ -389,10 +389,10 @@ export default myPreset | ||
| 389 | 389 | "prepublishOnly": "bun run build" |
| 390 | 390 | }, |
| 391 | 391 | "peerDependencies": { |
| 392 | "ts-css": ">=1.0.0" | |
| 392 | "@ts-css/core": ">=1.0.0" | |
| 393 | 393 | }, |
| 394 | 394 | "devDependencies": { |
| 395 | "ts-css": "latest", | |
| 395 | "@ts-css/core": "latest", | |
| 396 | 396 | "typescript": "latest" |
| 397 | 397 | } |
| 398 | 398 | } |
| @@ -459,7 +459,7 @@ const config = { | ||
| 459 | 459 | |
| 460 | 460 | ### Preset with Custom Preflight```typescript |
| 461 | 461 | |
| 462 | import type { Preflight, Preset } from 'ts-css' | |
| 462 | import type { Preflight, Preset } from '@ts-css/core' | |
| 463 | 463 | |
| 464 | 464 | const customPreflight: Preflight = { |
| 465 | 465 | getCSS: () => `/*Custom reset*/*{ |
| @@ -593,7 +593,7 @@ const config = { | ||
| 593 | 593 | } |
| 594 | 594 | ```### Type Errors**Problem:**TypeScript errors when creating preset**Solution:**Import and use the`Preset`type:```typescript |
| 595 | 595 | |
| 596 | import type { Preset } from 'ts-css' | |
| 596 | import type { Preset } from '@ts-css/core' | |
| 597 | 597 | |
| 598 | 598 | export const myPreset: Preset = { |
| 599 | 599 | name: 'my-preset', |
| @@ -8,7 +8,7 @@ The theme configuration defines the design tokens that utilities are generated f | ||
| 8 | 8 | |
| 9 | 9 | ```typescript |
| 10 | 10 | // css.config.ts |
| 11 | import type { TsCssOptions } from 'ts-css' | |
| 11 | import type { TsCssOptions } from '@ts-css/core' | |
| 12 | 12 | |
| 13 | 13 | const config = { |
| 14 | 14 | theme: { |
| @@ -339,7 +339,7 @@ const config = { | ||
| 339 | 339 | </div> |
| 340 | 340 | ```## Complete Theme Example```typescript |
| 341 | 341 | |
| 342 | import type { TsCssOptions } from 'ts-css' | |
| 342 | import type { TsCssOptions } from '@ts-css/core' | |
| 343 | 343 | |
| 344 | 344 | const config = { |
| 345 | 345 | theme: { |
| @@ -530,7 +530,7 @@ const config = { | ||
| 530 | 530 | ```### Extending Default Theme |
| 531 | 531 | |
| 532 | 532 | Merge with the default theme:```typescript |
| 533 | import { defaultConfig } from 'ts-css' | |
| 533 | import { defaultConfig } from '@ts-css/core' | |
| 534 | 534 | |
| 535 | 535 | const config = { |
| 536 | 536 | theme: { |
| @@ -5,12 +5,12 @@ This document provides a complete reference for ts-css's programmatic API. Use t | ||
| 5 | 5 | ## Installation |
| 6 | 6 | |
| 7 | 7 | ```bash |
| 8 | bun add ts-css | |
| 8 | bun add @ts-css/core | |
| 9 | 9 | ```## Core Functions |
| 10 | 10 | |
| 11 | 11 | ### `build(config: TsCssConfig): Promise<BuildResult>`Build CSS from content files and return the result.**Parameters:**-`config` - Complete ts-css configuration object**Returns:**`Promise<BuildResult>`**Example:**```typescript |
| 12 | 12 | |
| 13 | import { build } from 'ts-css' | |
| 13 | import { build } from '@ts-css/core' | |
| 14 | 14 | |
| 15 | 15 | const result = await build({ |
| 16 | 16 | content: ['./src/**/*.{html,js,ts,jsx,tsx}'], |
| @@ -39,7 +39,7 @@ console.log(`Built ${result.classes.size} classes in ${result.duration}ms`) | ||
| 39 | 39 | console.log(`Generated CSS:\n${result.css}`) |
| 40 | 40 | |
| 41 | 41 | ```###`buildAndWrite(config: TsCssConfig): Promise<BuildResult>`Build CSS and write it to the output file.**Parameters:**-`config` - Complete ts-css configuration object**Returns:**`Promise<BuildResult>`**Example:**```typescript |
| 42 | import { buildAndWrite } from 'ts-css' | |
| 42 | import { buildAndWrite } from '@ts-css/core' | |
| 43 | 43 | |
| 44 | 44 | const result = await buildAndWrite({ |
| 45 | 45 | content: ['./src/**/*.tsx'], |
| @@ -53,13 +53,13 @@ console.log(`File size: ${result.css.length} bytes`) | ||
| 53 | 53 | ```###`writeCSS(css: string, outputPath: string): Promise<void>`Write CSS string to a file.**Parameters:**-`css`- CSS content to write |
| 54 | 54 | |
| 55 | 55 | -`outputPath` - Path to output file**Returns:**`Promise<void>`**Example:**```typescript |
| 56 | import { build, writeCSS } from 'ts-css' | |
| 56 | import { build, writeCSS } from '@ts-css/core' | |
| 57 | 57 | |
| 58 | 58 | const result = await build(config) |
| 59 | 59 | await writeCSS(result.css, './custom/path/output.css') |
| 60 | 60 | |
| 61 | 61 | ```###`writeTransformedFiles(transformedFiles: Map<string, string>): Promise<void>`Write transformed files to disk (used with compile class transformer).**Parameters:**-`transformedFiles` - Map of file paths to transformed content**Returns:**`Promise<void>`**Example:**```typescript |
| 62 | import { build, writeTransformedFiles } from 'ts-css' | |
| 62 | import { build, writeTransformedFiles } from '@ts-css/core' | |
| 63 | 63 | |
| 64 | 64 | const result = await build({ |
| 65 | 65 | // ... config with compileClass enabled |
| @@ -79,7 +79,7 @@ Class responsible for generating CSS from utility classes.**Constructor:**```typ | ||
| 79 | 79 | new CSSGenerator(config: TsCssConfig) |
| 80 | 80 | |
| 81 | 81 | ```**Methods:**#### `generate(className: string): void`Generate CSS for a single utility class.```typescript |
| 82 | import { CSSGenerator } from 'ts-css' | |
| 82 | import { CSSGenerator } from '@ts-css/core' | |
| 83 | 83 | |
| 84 | 84 | const generator = new CSSGenerator(config) |
| 85 | 85 | generator.generate('bg-blue-500') |
| @@ -96,7 +96,7 @@ const prettyCSS = generator.toCSS(false) // Formatted | ||
| 96 | 96 | generator.reset() |
| 97 | 97 | ```**Complete example:**```typescript |
| 98 | 98 | |
| 99 | import { CSSGenerator, defaultConfig } from 'ts-css' | |
| 99 | import { CSSGenerator, defaultConfig } from '@ts-css/core' | |
| 100 | 100 | |
| 101 | 101 | const config = { |
| 102 | 102 | ...defaultConfig, |
| @@ -133,7 +133,7 @@ new Scanner( | ||
| 133 | 133 | |
| 134 | 134 | -`classes`- Set of found utility classes |
| 135 | 135 | -`transformedFiles` - Map of transformed files (if transformer provided)**Example:**```typescript |
| 136 | import { Scanner } from 'ts-css' | |
| 136 | import { Scanner } from '@ts-css/core' | |
| 137 | 137 | |
| 138 | 138 | const scanner = new Scanner(['./src/**/*.tsx']) |
| 139 | 139 | const { classes, transformedFiles } = await scanner.scan() |
| @@ -147,7 +147,7 @@ for (const className of classes) { | ||
| 147 | 147 | |
| 148 | 148 | ### `parseClass(className: string): ParsedClass`Parse a utility class string into its components.**Parameters:**-`className` - Class name to parse**Returns:**`ParsedClass` object**Example:**```typescript |
| 149 | 149 | |
| 150 | import { parseClass } from 'ts-css' | |
| 150 | import { parseClass } from '@ts-css/core' | |
| 151 | 151 | |
| 152 | 152 | const parsed = parseClass('md:hover:bg-blue-500') |
| 153 | 153 | |
| @@ -172,7 +172,7 @@ console.log(arbitrary.value) // '500px' | ||
| 172 | 172 | ### `defaultConfig` |
| 173 | 173 | |
| 174 | 174 | The default ts-css configuration object.**Example:**```typescript |
| 175 | import { defaultConfig } from 'ts-css' | |
| 175 | import { defaultConfig } from '@ts-css/core' | |
| 176 | 176 | |
| 177 | 177 | // Extend default config |
| 178 | 178 | const config = { |
| @@ -188,7 +188,7 @@ const config = { | ||
| 188 | 188 | |
| 189 | 189 | ```###`loadConfig(options): Promise<TsCssConfig>`Load configuration from file (used internally by CLI).**Parameters:**-`options.name`- Config name (default: 'css') |
| 190 | 190 | -`options.defaultConfig` - Default configuration**Returns:**`Promise<TsCssConfig>`**Example:**```typescript |
| 191 | import { defaultConfig, loadConfig } from 'ts-css' | |
| 191 | import { defaultConfig, loadConfig } from '@ts-css/core' | |
| 192 | 192 | |
| 193 | 193 | const config = await loadConfig({ |
| 194 | 194 | name: 'css', |
| @@ -208,7 +208,7 @@ new CompileClassTransformer(config?: { | ||
| 208 | 208 | ```**Methods:**#### `getCompiledClasses(): Map<string, { className: string, utilities: string[] }>` |
| 209 | 209 | |
| 210 | 210 | Get all compiled classes.**Example:**```typescript |
| 211 | import { CompileClassTransformer } from 'ts-css' | |
| 211 | import { CompileClassTransformer } from '@ts-css/core' | |
| 212 | 212 | |
| 213 | 213 | const transformer = new CompileClassTransformer({ |
| 214 | 214 | trigger: ':tc:', |
| @@ -324,7 +324,7 @@ interface VariantConfig { | ||
| 324 | 324 | |
| 325 | 325 | ### Custom Build Pipeline```typescript |
| 326 | 326 | |
| 327 | import { CSSGenerator, Scanner, writeCSS } from 'ts-css' | |
| 327 | import { CSSGenerator, Scanner, writeCSS } from '@ts-css/core' | |
| 328 | 328 | |
| 329 | 329 | async function customBuild() { |
| 330 | 330 | // 1. Configure |
| @@ -361,7 +361,7 @@ async function customBuild() { | ||
| 361 | 361 | await customBuild() |
| 362 | 362 | |
| 363 | 363 | ```### Analyze Utility Usage```typescript |
| 364 | import { parseClass, Scanner } from 'ts-css' | |
| 364 | import { parseClass, Scanner } from '@ts-css/core' | |
| 365 | 365 | |
| 366 | 366 | async function analyzeUtilities() { |
| 367 | 367 | const scanner = new Scanner(['./src/**/*.tsx']) |
| @@ -389,7 +389,7 @@ await analyzeUtilities() | ||
| 389 | 389 | ```### Watch Mode Implementation```typescript |
| 390 | 390 | |
| 391 | 391 | import { watch } from 'node:fs' |
| 392 | import { buildAndWrite } from 'ts-css' | |
| 392 | import { buildAndWrite } from '@ts-css/core' | |
| 393 | 393 | |
| 394 | 394 | async function watchMode(config: TsCssConfig) { |
| 395 | 395 | console.log('Watching for changes...') |
| @@ -419,7 +419,7 @@ async function watchMode(config: TsCssConfig) { | ||
| 419 | 419 | await watchMode(config) |
| 420 | 420 | |
| 421 | 421 | ```### Framework Plugin```typescript |
| 422 | import { buildAndWrite } from 'ts-css' | |
| 422 | import { buildAndWrite } from '@ts-css/core' | |
| 423 | 423 | |
| 424 | 424 | // Usage in vite.config.ts |
| 425 | 425 | import { defineConfig } from 'vite' |
| @@ -457,7 +457,7 @@ export default defineConfig({ | ||
| 457 | 457 | }) |
| 458 | 458 | ```### Generate Multiple Themes```typescript |
| 459 | 459 | |
| 460 | import { build, writeCSS } from 'ts-css' | |
| 460 | import { build, writeCSS } from '@ts-css/core' | |
| 461 | 461 | |
| 462 | 462 | const baseConfig = { |
| 463 | 463 | content: ['./src/**/*.tsx'], |
| @@ -494,7 +494,7 @@ for (const [name, theme] of Object.entries(themes)) { | ||
| 494 | 494 | |
| 495 | 495 | ```### Testing Utilities```typescript |
| 496 | 496 | import { expect, test } from 'bun:test' |
| 497 | import { CSSGenerator, defaultConfig, parseClass } from 'ts-css' | |
| 497 | import { CSSGenerator, defaultConfig, parseClass } from '@ts-css/core' | |
| 498 | 498 | |
| 499 | 499 | test('parses utility class', () => { |
| 500 | 500 | const parsed = parseClass('md:hover:bg-blue-500') |
| @@ -519,7 +519,7 @@ test('handles important modifier', () => { | ||
| 519 | 519 | }) |
| 520 | 520 | ```## Error Handling```typescript |
| 521 | 521 | |
| 522 | import { buildAndWrite } from 'ts-css' | |
| 522 | import { buildAndWrite } from '@ts-css/core' | |
| 523 | 523 | |
| 524 | 524 | try { |
| 525 | 525 | const result = await buildAndWrite(config) |
| @@ -543,7 +543,7 @@ catch (error) { | ||
| 543 | 543 | |
| 544 | 544 | ### Caching Results```typescript |
| 545 | 545 | |
| 546 | import { build } from 'ts-css' | |
| 546 | import { build } from '@ts-css/core' | |
| 547 | 547 | |
| 548 | 548 | const cache = new Map<string, BuildResult>() |
| 549 | 549 | |
| @@ -562,7 +562,7 @@ async function buildWithCache(config: TsCssConfig) { | ||
| 562 | 562 | } |
| 563 | 563 | ```### Parallel Builds```typescript |
| 564 | 564 | |
| 565 | import { build, writeCSS } from 'ts-css' | |
| 565 | import { build, writeCSS } from '@ts-css/core' | |
| 566 | 566 | |
| 567 | 567 | const configs = [ |
| 568 | 568 | { output: './dist/app.css', content: ['./src/app/**/*.tsx'] }, |
| @@ -10,7 +10,7 @@ Create a configuration file using the init command: | ||
| 10 | 10 | cssx init |
| 11 | 11 | ```This creates a basic`css.config.ts`file:```typescript |
| 12 | 12 | |
| 13 | import type { TsCssOptions } from 'ts-css' | |
| 13 | import type { TsCssOptions } from '@ts-css/core' | |
| 14 | 14 | |
| 15 | 15 | const config = { |
| 16 | 16 | content: ['./src/**/*.{html,js,ts,jsx,tsx}'], |
| @@ -238,7 +238,7 @@ const config = { | ||
| 238 | 238 | |
| 239 | 239 | ```**Creating a Preset:**```typescript |
| 240 | 240 | // presets/custom.ts |
| 241 | import type { TsCssOptions } from 'ts-css' | |
| 241 | import type { TsCssOptions } from '@ts-css/core' | |
| 242 | 242 | |
| 243 | 243 | export const myCustomPreset: Partial<TsCssConfig> = { |
| 244 | 244 | theme: { |
| @@ -252,7 +252,7 @@ export const myCustomPreset: Partial<TsCssConfig> = { | ||
| 252 | 252 | } |
| 253 | 253 | ```## Complete Configuration Example```typescript |
| 254 | 254 | |
| 255 | import type { TsCssOptions } from 'ts-css' | |
| 255 | import type { TsCssOptions } from '@ts-css/core' | |
| 256 | 256 | |
| 257 | 257 | const config = { |
| 258 | 258 | // Required |
| @@ -306,7 +306,7 @@ export default config | ||
| 306 | 306 | ```## Environment-Specific Configuration |
| 307 | 307 | |
| 308 | 308 | You can create different configurations for different environments:```typescript |
| 309 | import type { TsCssOptions } from 'ts-css' | |
| 309 | import type { TsCssOptions } from '@ts-css/core' | |
| 310 | 310 | |
| 311 | 311 | const isDev = process.env.NODE_ENV === 'development' |
| 312 | 312 | const isProd = process.env.NODE_ENV === 'production' |
| @@ -327,7 +327,7 @@ export default config | ||
| 327 | 327 | ```## TypeScript Support |
| 328 | 328 | |
| 329 | 329 | ts-css provides full TypeScript support with type checking and autocomplete:```typescript |
| 330 | import type { TsCssOptions } from 'ts-css' | |
| 330 | import type { TsCssOptions } from '@ts-css/core' | |
| 331 | 331 | |
| 332 | 332 | // Use satisfies for type checking while preserving literal types |
| 333 | 333 | const config = { |
| @@ -10,11 +10,11 @@ Install ts-css globally or locally: | ||
| 10 | 10 | |
| 11 | 11 | # Global (use anywhere) |
| 12 | 12 | |
| 13 | bun add --global ts-css | |
| 13 | bun add --global @ts-css/core | |
| 14 | 14 | |
| 15 | 15 | # Local (project-specific) |
| 16 | 16 | |
| 17 | bun add ts-css | |
| 17 | bun add @ts-css/core | |
| 18 | 18 | ```## Commands |
| 19 | 19 | |
| 20 | 20 | ### `build`Build CSS from your content files.```bash |
| @@ -125,7 +125,7 @@ Next steps: | ||
| 125 | 125 | |
| 126 | 126 | ```**Generated file:**```typescript |
| 127 | 127 | |
| 128 | import type { TsCssOptions } from 'ts-css' | |
| 128 | import type { TsCssOptions } from '@ts-css/core' | |
| 129 | 129 | |
| 130 | 130 | const config = { |
| 131 | 131 | content: ['./src/**/*.{html,js,ts,jsx,tsx}'], |
| @@ -403,7 +403,7 @@ cssx build --config ./css.test.config.ts | ||
| 403 | 403 | ```### Programmatic Usage |
| 404 | 404 | |
| 405 | 405 | While the CLI is convenient, you can also use ts-css programmatically:```typescript |
| 406 | import { build, buildAndWrite } from 'ts-css' | |
| 406 | import { build, buildAndWrite } from '@ts-css/core' | |
| 407 | 407 | |
| 408 | 408 | // Build only (get result) |
| 409 | 409 | const result = await build({ |
| @@ -459,7 +459,7 @@ const config = { | ||
| 459 | 459 | ### Command Not Found**Problem:**`command not found: cssx`**Solutions:**1. Install globally |
| 460 | 460 | |
| 461 | 461 | ```bash |
| 462 | bun add --global ts-css | |
| 462 | bun add --global @ts-css/core | |
| 463 | 463 | ```2. Or use with package runner:```bash |
| 464 | 464 | bunx cssx build |
| 465 | 465 | |
| @@ -494,7 +494,7 @@ const config = { | ||
| 494 | 494 | ls -la ./src/**/*.tsx |
| 495 | 495 | ```3. Validate config:```typescript |
| 496 | 496 | // Use type checking |
| 497 | import type { TsCssOptions } from 'ts-css' | |
| 497 | import type { TsCssOptions } from '@ts-css/core' | |
| 498 | 498 | |
| 499 | 499 | const config = { |
| 500 | 500 | content: ['./src/**/*.tsx'], |
| @@ -30,7 +30,7 @@ Add the`:tc:`trigger to mark utility groups for compilation:```html | ||
| 30 | 30 | |
| 31 | 31 | Configure the transformer in your config:```typescript |
| 32 | 32 | // css.config.ts |
| 33 | import type { TsCssOptions } from 'ts-css' | |
| 33 | import type { TsCssOptions } from '@ts-css/core' | |
| 34 | 34 | |
| 35 | 35 | const config = { |
| 36 | 36 | content: ['./src/**/*.{html,js,ts,jsx,tsx}'], |
| @@ -8,7 +8,7 @@ Use TypeScript for your configuration file to get autocompletion and type checki | ||
| 8 | 8 | |
| 9 | 9 | ```typescript |
| 10 | 10 | // css.config.ts |
| 11 | import type { TsCssOptions } from 'ts-css' | |
| 11 | import type { TsCssOptions } from '@ts-css/core' | |
| 12 | 12 | |
| 13 | 13 | const config = { |
| 14 | 14 | content: ['./src/**/*.{html,js,ts,jsx,tsx}'], |
| @@ -31,7 +31,7 @@ export default config | ||
| 31 | 31 | ```## Type Checking |
| 32 | 32 | |
| 33 | 33 | The`satisfies`keyword provides type checking while preserving literal types:```typescript |
| 34 | import type { TsCssOptions } from 'ts-css' | |
| 34 | import type { TsCssOptions } from '@ts-css/core' | |
| 35 | 35 | |
| 36 | 36 | // ✅ Type-safe with autocompletion |
| 37 | 37 | const config = { |
| @@ -107,7 +107,7 @@ Get IntelliSense and autocompletion in your IDE: | ||
| 107 | 107 | 1. Install the TypeScript extension (comes pre-installed) |
| 108 | 108 | 2. Create`css.config.ts`with the type import:```typescript |
| 109 | 109 | |
| 110 | import type { TsCssOptions } from 'ts-css' | |
| 110 | import type { TsCssOptions } from '@ts-css/core' | |
| 111 | 111 | |
| 112 | 112 | const config = { |
| 113 | 113 | // Press Ctrl+Space here for autocompletion |
| @@ -161,8 +161,8 @@ const config = { | ||
| 161 | 161 | |
| 162 | 162 | ### Build Function```typescript |
| 163 | 163 | |
| 164 | import type { BuildResult, TsCssConfig } from 'ts-css' | |
| 165 | import { build } from 'ts-css' | |
| 164 | import type { BuildResult, TsCssConfig } from '@ts-css/core' | |
| 165 | import { build } from '@ts-css/core' | |
| 166 | 166 | |
| 167 | 167 | const result: BuildResult = await build({ |
| 168 | 168 | content: ['./src/**/*.tsx'], |
| @@ -185,7 +185,7 @@ interface BuildResult { | ||
| 185 | 185 | } |
| 186 | 186 | |
| 187 | 187 | ```### Using with async/await```typescript |
| 188 | import { build, buildAndWrite } from 'ts-css' | |
| 188 | import { build, buildAndWrite } from '@ts-css/core' | |
| 189 | 189 | |
| 190 | 190 | async function buildStyles() { |
| 191 | 191 | // With build |
| @@ -207,7 +207,7 @@ async function buildStyles() { | ||
| 207 | 207 | ```## Custom Rules with Types |
| 208 | 208 | |
| 209 | 209 | Define type-safe custom rules:```typescript |
| 210 | import type { CustomRule, TsCssConfig } from 'ts-css' | |
| 210 | import type { CustomRule, TsCssConfig } from '@ts-css/core' | |
| 211 | 211 | |
| 212 | 212 | const customRules: CustomRule[] = [ |
| 213 | 213 | // Pattern and handler with types |
| @@ -229,7 +229,7 @@ const config = { | ||
| 229 | 229 | ```## Preset Types |
| 230 | 230 | |
| 231 | 231 | Create type-safe presets:```typescript |
| 232 | import type { Preset, Theme } from 'ts-css' | |
| 232 | import type { Preset, Theme } from '@ts-css/core' | |
| 233 | 233 | |
| 234 | 234 | const myPreset: Preset = { |
| 235 | 235 | name: 'my-design-system', |
| @@ -264,7 +264,7 @@ const config = { | ||
| 264 | 264 | ### Custom Theme Values |
| 265 | 265 | |
| 266 | 266 | Extend the theme with custom properties:```typescript |
| 267 | import type { TsCssOptions, Theme } from 'ts-css' | |
| 267 | import type { TsCssOptions, Theme } from '@ts-css/core' | |
| 268 | 268 | |
| 269 | 269 | interface CustomTheme extends Theme { |
| 270 | 270 | customSpacing: Record<string, string> |
| @@ -286,7 +286,7 @@ const config = { | ||
| 286 | 286 | ```### Custom Config |
| 287 | 287 | |
| 288 | 288 | Create a custom config interface:```typescript |
| 289 | import type { TsCssOptions } from 'ts-css' | |
| 289 | import type { TsCssOptions } from '@ts-css/core' | |
| 290 | 290 | |
| 291 | 291 | interface MyConfig extends Partial<TsCssConfig> { |
| 292 | 292 | // Add custom properties |
| @@ -310,7 +310,7 @@ const config: MyConfig = { | ||
| 310 | 310 | ```## Type Guards |
| 311 | 311 | |
| 312 | 312 | Use type guards for runtime type checking:```typescript |
| 313 | import type { TsCssOptions } from 'ts-css' | |
| 313 | import type { TsCssOptions } from '@ts-css/core' | |
| 314 | 314 | |
| 315 | 315 | function isValidConfig(config: unknown): config is TsCssConfig { |
| 316 | 316 | return ( |
| @@ -333,7 +333,7 @@ if (isValidConfig(config)) { | ||
| 333 | 333 | ```## Generics |
| 334 | 334 | |
| 335 | 335 | Use generics for flexible, type-safe functions:```typescript |
| 336 | import type { TsCssOptions } from 'ts-css' | |
| 336 | import type { TsCssOptions } from '@ts-css/core' | |
| 337 | 337 | |
| 338 | 338 | function createConfig<T extends Partial<TsCssConfig>>(config: T): T { |
| 339 | 339 | return { |
| @@ -356,12 +356,12 @@ const config = createConfig({ | ||
| 356 | 356 | |
| 357 | 357 | Extend ts-css types globally:```typescript |
| 358 | 358 | // Now use in config |
| 359 | import type { TsCssOptions } from 'ts-css' | |
| 359 | import type { TsCssOptions } from '@ts-css/core' | |
| 360 | 360 | |
| 361 | 361 | // types/ts-css.d.ts |
| 362 | import 'ts-css' | |
| 362 | import '@ts-css/core' | |
| 363 | 363 | |
| 364 | declare module 'ts-css' { | |
| 364 | declare module '@ts-css/core' { | |
| 365 | 365 | interface TsCssConfig { |
| 366 | 366 | // Add your custom properties |
| 367 | 367 | customFeature?: { |
| @@ -392,8 +392,8 @@ const config = { | ||
| 392 | 392 | |
| 393 | 393 | ### Vite```typescript |
| 394 | 394 | |
| 395 | import type { TsCssOptions } from 'ts-css' | |
| 396 | import { build } from 'ts-css' | |
| 395 | import type { TsCssOptions } from '@ts-css/core' | |
| 396 | import { build } from '@ts-css/core' | |
| 397 | 397 | // vite.config.ts |
| 398 | 398 | import { defineConfig } from 'vite' |
| 399 | 399 | |
| @@ -415,10 +415,10 @@ export default defineConfig({ | ||
| 415 | 415 | }) |
| 416 | 416 | ```### Next.js```typescript |
| 417 | 417 | |
| 418 | import type { TsCssOptions } from 'ts-css' | |
| 418 | import type { TsCssOptions } from '@ts-css/core' | |
| 419 | 419 | // next.config.ts |
| 420 | 420 | import type { NextConfig } from 'next' |
| 421 | import { buildAndWrite } from 'ts-css' | |
| 421 | import { buildAndWrite } from '@ts-css/core' | |
| 422 | 422 | |
| 423 | 423 | const tsCssConfig: TsCssConfig = { |
| 424 | 424 | content: ['./src/**/*.tsx', './app/**/*.tsx'], |
| @@ -446,7 +446,7 @@ import type { | ||
| 446 | 446 | ParsedClass, |
| 447 | 447 | Preset, |
| 448 | 448 | Theme, |
| 449 | } from 'ts-css' | |
| 449 | } from '@ts-css/core' | |
| 450 | 450 | |
| 451 | 451 | // Extract specific types |
| 452 | 452 | type ConfigColors = Theme['colors'] |
| @@ -460,10 +460,10 @@ type PartialTheme = Partial<Theme> | ||
| 460 | 460 | ### 1. Always Use Type Imports```typescript |
| 461 | 461 | |
| 462 | 462 | // ✅ Type-only import |
| 463 | import type { TsCssOptions } from 'ts-css' | |
| 463 | import type { TsCssOptions } from '@ts-css/core' | |
| 464 | 464 | |
| 465 | 465 | // ❌ Value import (unnecessary) |
| 466 | import { TsCssConfig } from 'ts-css' | |
| 466 | import { TsCssConfig } from '@ts-css/core' | |
| 467 | 467 | |
| 468 | 468 | ```### 2. Use`satisfies`for Validation```typescript |
| 469 | 469 | // ✅ Validates types while preserving literals |
| @@ -478,7 +478,7 @@ const config: Partial<TsCssConfig> = { | ||
| 478 | 478 | ```### 3. Separate Type Definitions```typescript |
| 479 | 479 | |
| 480 | 480 | // types/ts-css.ts |
| 481 | import type { TsCssOptions, Preset } from 'ts-css' | |
| 481 | import type { TsCssOptions, Preset } from '@ts-css/core' | |
| 482 | 482 | |
| 483 | 483 | // css.config.ts |
| 484 | 484 | import type { MyConfig } from './types/ts-css' |
| @@ -528,14 +528,14 @@ bun add --dev typescript | ||
| 528 | 528 | |
| 529 | 529 | # Ensure types are imported |
| 530 | 530 | |
| 531 | import type { TsCssOptions } from 'ts-css' | |
| 531 | import type { TsCssOptions } from '@ts-css/core' | |
| 532 | 532 | |
| 533 | 533 | ```### Autocomplete Not Working**Problem:**No autocompletion in IDE**Solutions:**1. Restart TypeScript server |
| 534 | 534 | |
| 535 | 535 | 2. Check tsconfig.json includes the config file |
| 536 | 536 | 3. Ensure proper imports:```typescript |
| 537 | 537 | |
| 538 | import type { TsCssOptions } from 'ts-css' | |
| 538 | import type { TsCssOptions } from '@ts-css/core' | |
| 539 | 539 | ``` |
| 540 | 540 | |
| 541 | 541 | ## Related*[Configuration](../config.md) - Full configuration guide*[Custom Rules](../advanced/custom-rules.md) - Creating custom utilities*[Presets](../advanced/presets.md) - Reusable configurations*[API Reference](../api-reference.md) - Programmatic API |
| @@ -33,7 +33,7 @@ When you start watch mode, ts-css: | ||
| 33 | 33 | |
| 34 | 34 | Watch mode uses the content patterns from your configuration file:```typescript |
| 35 | 35 | // css.config.ts |
| 36 | import type { TsCssOptions } from 'ts-css' | |
| 36 | import type { TsCssOptions } from '@ts-css/core' | |
| 37 | 37 | |
| 38 | 38 | const config = { |
| 39 | 39 | content: [ |
| @@ -9,31 +9,31 @@ Install ts-css as a development dependency in your project: | ||
| 9 | 9 | ::: code-group |
| 10 | 10 | |
| 11 | 11 | ```sh [bun] |
| 12 | bun add ts-css | |
| 12 | bun add @ts-css/core | |
| 13 | 13 | |
| 14 | 14 | # or |
| 15 | 15 | |
| 16 | bun install --dev ts-css | |
| 16 | bun install --dev @ts-css/core | |
| 17 | 17 | ``` |
| 18 | 18 | |
| 19 | 19 | ```sh [npm] |
| 20 | npm install ts-css | |
| 20 | npm install @ts-css/core | |
| 21 | 21 | |
| 22 | 22 | # or |
| 23 | 23 | |
| 24 | npm i -D ts-css | |
| 24 | npm i -D @ts-css/core | |
| 25 | 25 | ``` |
| 26 | 26 | |
| 27 | 27 | ```sh [pnpm] |
| 28 | pnpm add ts-css | |
| 28 | pnpm add @ts-css/core | |
| 29 | 29 | |
| 30 | 30 | # or |
| 31 | 31 | |
| 32 | pnpm add -D ts-css | |
| 32 | pnpm add -D @ts-css/core | |
| 33 | 33 | ``` |
| 34 | 34 | |
| 35 | 35 | ```sh [yarn] |
| 36 | yarn add ts-css | |
| 36 | yarn add @ts-css/core | |
| 37 | 37 | ```::: |
| 38 | 38 | |
| 39 | 39 | ### Global Installation |
| @@ -41,29 +41,29 @@ yarn add ts-css | ||
| 41 | 41 | For global installation (to use the CLI anywhere): |
| 42 | 42 | |
| 43 | 43 | ::: code-group```sh [bun] |
| 44 | bun add --global ts-css | |
| 44 | bun add --global @ts-css/core | |
| 45 | 45 | |
| 46 | 46 | ``` |
| 47 | 47 | |
| 48 | 48 | ```sh [npm] |
| 49 | 49 | |
| 50 | npm install --global ts-css | |
| 50 | npm install --global @ts-css/core | |
| 51 | 51 | |
| 52 | 52 | # or |
| 53 | 53 | |
| 54 | npm i -g ts-css | |
| 54 | npm i -g @ts-css/core | |
| 55 | 55 | |
| 56 | 56 | ``` |
| 57 | 57 | |
| 58 | 58 | ```sh [pnpm] |
| 59 | 59 | |
| 60 | pnpm add --global ts-css | |
| 60 | pnpm add --global @ts-css/core | |
| 61 | 61 | |
| 62 | 62 | ``` |
| 63 | 63 | |
| 64 | 64 | ```sh [yarn] |
| 65 | 65 | |
| 66 | yarn global add ts-css | |
| 66 | yarn global add @ts-css/core | |
| 67 | 67 | |
| 68 | 68 | ```::: |
| 69 | 69 | |
| @@ -85,7 +85,7 @@ cssx watch | ||
| 85 | 85 | ```## Configuration |
| 86 | 86 | |
| 87 | 87 | The`cssx init`command creates a basic`css.config.ts`file:```typescript |
| 88 | import type { TsCssOptions } from 'ts-css' | |
| 88 | import type { TsCssOptions } from '@ts-css/core' | |
| 89 | 89 | |
| 90 | 90 | const config = { |
| 91 | 91 | content: ['./src/**/*.{html,js,ts,jsx,tsx}'], |
| @@ -102,7 +102,7 @@ export default config | ||
| 102 | 102 | |
| 103 | 103 | # Install ts-css |
| 104 | 104 | |
| 105 | bun add ts-css | |
| 105 | bun add @ts-css/core | |
| 106 | 106 | |
| 107 | 107 | # Create config |
| 108 | 108 | |
| @@ -110,7 +110,7 @@ cssx init | ||
| 110 | 110 | ```Update your`css.config.ts`: |
| 111 | 111 | |
| 112 | 112 | ```typescript |
| 113 | import type { TsCssOptions } from 'ts-css' | |
| 113 | import type { TsCssOptions } from '@ts-css/core' | |
| 114 | 114 | |
| 115 | 115 | const config = { |
| 116 | 116 | content: [ |
| @@ -143,7 +143,7 @@ import './styles/styles.css' | ||
| 143 | 143 | |
| 144 | 144 | # Install ts-css |
| 145 | 145 | |
| 146 | bun add ts-css | |
| 146 | bun add @ts-css/core | |
| 147 | 147 | |
| 148 | 148 | # Create config |
| 149 | 149 | |
| @@ -151,7 +151,7 @@ cssx init | ||
| 151 | 151 | ```Update your`css.config.ts`: |
| 152 | 152 | |
| 153 | 153 | ```typescript |
| 154 | import type { TsCssOptions } from 'ts-css' | |
| 154 | import type { TsCssOptions } from '@ts-css/core' | |
| 155 | 155 | |
| 156 | 156 | const config = { |
| 157 | 157 | content: [ |
| @@ -174,7 +174,7 @@ export default config | ||
| 174 | 174 | |
| 175 | 175 | # Install ts-css |
| 176 | 176 | |
| 177 | bun add ts-css | |
| 177 | bun add @ts-css/core | |
| 178 | 178 | |
| 179 | 179 | # Create config |
| 180 | 180 | |
| @@ -182,7 +182,7 @@ cssx init | ||
| 182 | 182 | ```Update your`css.config.ts`: |
| 183 | 183 | |
| 184 | 184 | ```typescript |
| 185 | import type { TsCssOptions } from 'ts-css' | |
| 185 | import type { TsCssOptions } from '@ts-css/core' | |
| 186 | 186 | |
| 187 | 187 | const config = { |
| 188 | 188 | content: [ |
| @@ -203,7 +203,7 @@ export default config | ||
| 203 | 203 | |
| 204 | 204 | # Install ts-css |
| 205 | 205 | |
| 206 | bun add ts-css | |
| 206 | bun add @ts-css/core | |
| 207 | 207 | |
| 208 | 208 | # Create config |
| 209 | 209 | |
| @@ -211,7 +211,7 @@ cssx init | ||
| 211 | 211 | ```Update your`css.config.ts`: |
| 212 | 212 | |
| 213 | 213 | ```typescript |
| 214 | import type { TsCssOptions } from 'ts-css' | |
| 214 | import type { TsCssOptions } from '@ts-css/core' | |
| 215 | 215 | |
| 216 | 216 | const config = { |
| 217 | 217 | content: [ |
| @@ -235,7 +235,7 @@ import '/styles.css' | ||
| 235 | 235 | |
| 236 | 236 | # Install ts-css globally |
| 237 | 237 | |
| 238 | bun add --global ts-css | |
| 238 | bun add --global @ts-css/core | |
| 239 | 239 | |
| 240 | 240 | # Create config |
| 241 | 241 | |
| @@ -243,7 +243,7 @@ cssx init | ||
| 243 | 243 | ```Update your`css.config.ts`: |
| 244 | 244 | |
| 245 | 245 | ```typescript |
| 246 | import type { TsCssOptions } from 'ts-css' | |
| 246 | import type { TsCssOptions } from '@ts-css/core' | |
| 247 | 247 | |
| 248 | 248 | const config = { |
| 249 | 249 | content: ['./src/**/*.html'], |
| @@ -397,11 +397,11 @@ If you get permission errors when installing globally:```bash | ||
| 397 | 397 | |
| 398 | 398 | # Use sudo on macOS/Linux |
| 399 | 399 | |
| 400 | sudo bun add --global ts-css | |
| 400 | sudo bun add --global @ts-css/core | |
| 401 | 401 | |
| 402 | 402 | # Or install locally and use npx |
| 403 | 403 | |
| 404 | bun add ts-css | |
| 404 | bun add @ts-css/core | |
| 405 | 405 | bunx cssx build |
| 406 | 406 | |
| 407 | 407 | ```### TypeScript Errors |
| @@ -412,7 +412,7 @@ If you encounter TypeScript errors in your config file: | ||
| 412 | 412 | |
| 413 | 413 | bun add --dev typescript |
| 414 | 414 | ```2. Use the`satisfies`keyword for type checking:```typescript |
| 415 | import type { TsCssOptions } from 'ts-css' | |
| 415 | import type { TsCssOptions } from '@ts-css/core' | |
| 416 | 416 | |
| 417 | 417 | const config = { |
| 418 | 418 | content: ['./src/**/*.tsx'], |
| @@ -130,15 +130,15 @@ Install ts-css via your package manager:```bash | ||
| 130 | 130 | |
| 131 | 131 | # Using Bun (recommended) |
| 132 | 132 | |
| 133 | bun add ts-css | |
| 133 | bun add @ts-css/core | |
| 134 | 134 | |
| 135 | 135 | # Using npm |
| 136 | 136 | |
| 137 | npm install ts-css | |
| 137 | npm install @ts-css/core | |
| 138 | 138 | |
| 139 | 139 | # Using pnpm |
| 140 | 140 | |
| 141 | pnpm add ts-css | |
| 141 | pnpm add @ts-css/core | |
| 142 | 142 | |
| 143 | 143 | ```### Configuration |
| 144 | 144 | |
| @@ -146,7 +146,7 @@ Create a configuration file:```bash | ||
| 146 | 146 | cssx init |
| 147 | 147 | ```This creates a`css.config.ts`file:```typescript |
| 148 | 148 | |
| 149 | import type { TsCssOptions } from 'ts-css' | |
| 149 | import type { TsCssOptions } from '@ts-css/core' | |
| 150 | 150 | |
| 151 | 151 | const config = { |
| 152 | 152 | content: ['./src/**/*.{html,js,ts,jsx,tsx}'], |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | { |
| 2 | "name": "ts-css", | |
| 2 | "name": "@ts-css/core", | |
| 3 | 3 | "type": "module", |
| 4 | 4 | "sideEffects": false, |
| 5 | 5 | "version": "0.3.0", |
| @@ -29,7 +29,7 @@ | ||
| 29 | 29 | "zero-runtime" |
| 30 | 30 | ], |
| 31 | 31 | "scripts": { |
| 32 | "build": "cd packages/ts-css && bun run build", | |
| 32 | "build": "bun run build:toolkit && cd packages/ts-css && bun run build", | |
| 33 | 33 | "fresh": "bunx rimraf node_modules/ bun.lock && bun i", |
| 34 | 34 | "test": "bun test", |
| 35 | 35 | "lint": "bunx --bun pickier .", |
| @@ -48,7 +48,9 @@ | ||
| 48 | 48 | "typecheck": "bun --bun tsc --noEmit", |
| 49 | 49 | "benchmark": "bun run benchmark/framework-comparison.bench.ts", |
| 50 | 50 | "benchmark:binary": "bun run benchmark/binary-vs-api.ts", |
| 51 | "benchmark:style": "bun run benchmark/style-api.bench.ts" | |
| 51 | "benchmark:style": "bun run benchmark/style-api.bench.ts", | |
| 52 | "build:toolkit": "cd packages/toolkit && bun run build", | |
| 53 | "prepare": "bun run build:toolkit" | |
| 52 | 54 | }, |
| 53 | 55 | "devDependencies": { |
| 54 | 56 | "better-dx": "^0.2.15" |
| @@ -3,9 +3,9 @@ import type { BuddyBotConfig } from 'buddy-bot' | ||
| 3 | 3 | const config: BuddyBotConfig = { |
| 4 | 4 | repository: { |
| 5 | 5 | owner: 'stacksjs', |
| 6 | name: 'ts-css', | |
| 6 | name: 'ts-starter', | |
| 7 | 7 | provider: 'github', |
| 8 | // Uses GITHUB_TOKEN by default | |
| 8 | // token: process.env.BUDDY_BOT_TOKEN, | |
| 9 | 9 | }, |
| 10 | 10 | dashboard: { |
| 11 | 11 | enabled: true, |
| @@ -0,0 +1,62 @@ | ||
| 1 | import type { PickierConfig } from 'pickier' | |
| 2 | ||
| 3 | const config: PickierConfig = { | |
| 4 | verbose: false, | |
| 5 | ignores: [ | |
| 6 | '**/node_modules/**', | |
| 7 | '**/dist/**', | |
| 8 | '**/build/**', | |
| 9 | '**/bin/**', | |
| 10 | '**/.git/**', | |
| 11 | '**/coverage/**', | |
| 12 | '**/*.min.js', | |
| 13 | '**/bun.lock', | |
| 14 | '**/benchmarks/**', | |
| 15 | '**/.claude/**', | |
| 16 | '**/.zed/**', | |
| 17 | ], | |
| 18 | ||
| 19 | lint: { | |
| 20 | extensions: ['ts', 'js'], | |
| 21 | reporter: 'stylish', | |
| 22 | cache: false, | |
| 23 | maxWarnings: -1, | |
| 24 | }, | |
| 25 | ||
| 26 | format: { | |
| 27 | extensions: ['ts', 'js', 'json', 'md', 'yaml', 'yml'], | |
| 28 | trimTrailingWhitespace: true, | |
| 29 | maxConsecutiveBlankLines: 1, | |
| 30 | finalNewline: 'one', | |
| 31 | indent: 2, | |
| 32 | quotes: 'single', | |
| 33 | semi: false, | |
| 34 | }, | |
| 35 | ||
| 36 | rules: { | |
| 37 | noDebugger: 'error', | |
| 38 | noConsole: 'off', | |
| 39 | }, | |
| 40 | ||
| 41 | pluginRules: { | |
| 42 | 'ts/no-explicit-any': 'off', | |
| 43 | 'ts/no-unused-vars': 'warn', | |
| 44 | 'ts/no-top-level-await': 'off', | |
| 45 | 'regexp/no-unused-capturing-group': 'off', | |
| 46 | 'regexp/no-super-linear-backtracking': 'off', | |
| 47 | 'style/brace-style': 'off', | |
| 48 | 'style/max-statements-per-line': 'off', | |
| 49 | 'markdown/heading-increment': 'error', | |
| 50 | 'markdown/no-trailing-spaces': 'error', | |
| 51 | 'markdown/fenced-code-language': 'warn', | |
| 52 | 'markdown/no-inline-html': 'off', | |
| 53 | 'markdown/reference-links-images': 'off', | |
| 54 | 'markdown/single-title': 'off', | |
| 55 | 'markdown/blanks-around-fences': 'off', | |
| 56 | 'markdown/no-duplicate-heading': 'off', | |
| 57 | 'markdown/single-trailing-newline': 'off', | |
| 58 | 'markdown/link-image-style': 'off', | |
| 59 | }, | |
| 60 | } | |
| 61 | ||
| 62 | export default config | |
| @@ -0,0 +1,1 @@ | ||
| 1 | * text=auto | |
| @@ -0,0 +1,47 @@ | ||
| 1 | antfu | |
| 2 | biomejs | |
| 3 | booleanish | |
| 4 | bumpp | |
| 5 | bunfig | |
| 6 | bunx | |
| 7 | changelogen | |
| 8 | changelogithub | |
| 9 | chrisbbreuer | |
| 10 | clapp | |
| 11 | codecov | |
| 12 | commitlint | |
| 13 | commitlintrc | |
| 14 | composables | |
| 15 | davidanson | |
| 16 | degit | |
| 17 | deps | |
| 18 | destructurable | |
| 19 | dtsx | |
| 20 | entrypoints | |
| 21 | heroicons | |
| 22 | iconify | |
| 23 | lockb | |
| 24 | openweb | |
| 25 | outdir | |
| 26 | pausable | |
| 27 | Postcardware | |
| 28 | prefetch | |
| 29 | preinstall | |
| 30 | quickfix | |
| 31 | shikijs | |
| 32 | socio | |
| 33 | Solana | |
| 34 | Spatie | |
| 35 | stacksjs | |
| 36 | twoslash | |
| 37 | typecheck | |
| 38 | unconfig | |
| 39 | unocss | |
| 40 | unplugin | |
| 41 | unref | |
| 42 | upath | |
| 43 | vite | |
| 44 | vitebook | |
| 45 | vitejs | |
| 46 | vue-demi | |
| 47 | vueus | |
| @@ -0,0 +1,153 @@ | ||
| 1 | { | |
| 2 | // Disable the default formatter, use eslint instead | |
| 3 | "prettier.enable": false, | |
| 4 | "biome.enabled": false, | |
| 5 | "editor.formatOnSave": false, | |
| 6 | // Auto fix | |
| 7 | "editor.codeActionsOnSave": { | |
| 8 | "source.fixAll.eslint": "explicit", | |
| 9 | "source.organizeImports": "never" | |
| 10 | }, | |
| 11 | // Silent the stylistic rules in you IDE, but still auto fix them | |
| 12 | "eslint.rules.customizations": [ | |
| 13 | { | |
| 14 | "rule": "style/*", | |
| 15 | "severity": "off", | |
| 16 | "fixable": true | |
| 17 | }, | |
| 18 | { | |
| 19 | "rule": "format/*", | |
| 20 | "severity": "off", | |
| 21 | "fixable": true | |
| 22 | }, | |
| 23 | { | |
| 24 | "rule": "*-indent", | |
| 25 | "severity": "off", | |
| 26 | "fixable": true | |
| 27 | }, | |
| 28 | { | |
| 29 | "rule": "*-spacing", | |
| 30 | "severity": "off", | |
| 31 | "fixable": true | |
| 32 | }, | |
| 33 | { | |
| 34 | "rule": "*-spaces", | |
| 35 | "severity": "off", | |
| 36 | "fixable": true | |
| 37 | }, | |
| 38 | { | |
| 39 | "rule": "*-order", | |
| 40 | "severity": "off", | |
| 41 | "fixable": true | |
| 42 | }, | |
| 43 | { | |
| 44 | "rule": "*-dangle", | |
| 45 | "severity": "off", | |
| 46 | "fixable": true | |
| 47 | }, | |
| 48 | { | |
| 49 | "rule": "*-newline", | |
| 50 | "severity": "off", | |
| 51 | "fixable": true | |
| 52 | }, | |
| 53 | { | |
| 54 | "rule": "*quotes", | |
| 55 | "severity": "off", | |
| 56 | "fixable": true | |
| 57 | }, | |
| 58 | { | |
| 59 | "rule": "*semi", | |
| 60 | "severity": "off", | |
| 61 | "fixable": true | |
| 62 | } | |
| 63 | ], | |
| 64 | // Enable eslint for all supported languages | |
| 65 | "eslint.validate": [ | |
| 66 | "javascript", | |
| 67 | "javascriptreact", | |
| 68 | "typescript", | |
| 69 | "typescriptreact", | |
| 70 | "vue", | |
| 71 | "html", | |
| 72 | "markdown", | |
| 73 | "json", | |
| 74 | "jsonc", | |
| 75 | "yaml", | |
| 76 | "toml", | |
| 77 | "xml", | |
| 78 | "gql", | |
| 79 | "graphql", | |
| 80 | "astro", | |
| 81 | "svelte", | |
| 82 | "css", | |
| 83 | "less", | |
| 84 | "scss", | |
| 85 | "pcss", | |
| 86 | "postcss" | |
| 87 | ], | |
| 88 | "[stx]": { | |
| 89 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" | |
| 90 | }, | |
| 91 | // Shell | |
| 92 | "[shellscript]": { | |
| 93 | "editor.defaultFormatter": "foxundermoon.shell-format" | |
| 94 | }, | |
| 95 | // Markdown | |
| 96 | "[markdown]": { | |
| 97 | "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", | |
| 98 | "editor.formatOnSave": true | |
| 99 | }, | |
| 100 | "[dockerfile]": { | |
| 101 | "editor.defaultFormatter": "foxundermoon.shell-format" | |
| 102 | }, | |
| 103 | "markdownlint.config": { | |
| 104 | "default": true, | |
| 105 | "MD033": { | |
| 106 | "allowed_elements": [ | |
| 107 | "details", | |
| 108 | "summary", | |
| 109 | "VPTeamPage", | |
| 110 | "VPTeamPageTitle", | |
| 111 | "VPTeamMembers", | |
| 112 | "script" | |
| 113 | ] | |
| 114 | }, | |
| 115 | "MD041": false | |
| 116 | }, | |
| 117 | "typescript.preferGoToSourceDefinition": true, | |
| 118 | "editor.quickSuggestions": { | |
| 119 | "strings": true | |
| 120 | }, | |
| 121 | "git.enableSmartCommit": true, | |
| 122 | "npm.enableRunFromFolder": true, | |
| 123 | "npm.packageManager": "bun", | |
| 124 | "editor.gotoLocation.multipleDefinitions": "goto", | |
| 125 | "search.exclude": { | |
| 126 | "**/node_modules": true, | |
| 127 | "**/cdk.out": true, | |
| 128 | "**/dist": true, | |
| 129 | "CHANGELOG.md": true | |
| 130 | }, | |
| 131 | ||
| 132 | "cSpell.ignorePaths": ["node_modules"], | |
| 133 | "cSpell.dictionaries": ["custom-dictionary"], | |
| 134 | "cSpell.diagnosticLevel": "Hint", | |
| 135 | "cSpell.customDictionaries": { | |
| 136 | "bun-plugin-env": { | |
| 137 | "name": "custom-dictionary", | |
| 138 | "path": "./.vscode/dictionary.txt", | |
| 139 | "scope": "user", | |
| 140 | "addWords": true | |
| 141 | }, | |
| 142 | "custom": true // enable the `custom` dictionary | |
| 143 | }, | |
| 144 | "terminal.integrated.scrollback": 10000, | |
| 145 | "grammarly.files.include": [ | |
| 146 | "**/README.md", | |
| 147 | "**/readme.md", | |
| 148 | "**/*.txt" | |
| 149 | ], | |
| 150 | "grammarly.files.exclude": [ | |
| 151 | "**/dictionary.txt" | |
| 152 | ] | |
| 153 | } | |
| @@ -0,0 +1,252 @@ | ||
| 1 | [Compare changes](https://github.com/stacksjs/ts-css/compare/v0.1.3...v0.1.4) | |
| 2 | ||
| 3 | ## 🐛 Bug Fixes | |
| 4 | ||
| 5 | - **build**: build the CLI the bin field points at ([61d4584](https://github.com/stacksjs/ts-css/commit/61d4584)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 6 | ||
| 7 | ## 🧹 Chores | |
| 8 | ||
| 9 | - release v0.1.4 ([5c4ca3d](https://github.com/stacksjs/ts-css/commit/5c4ca3d)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 10 | ||
| 11 | ## Contributors | |
| 12 | ||
| 13 | - _Chris <chrisbreuer93@gmail.com>_ | |
| 14 | ||
| 15 | [Compare changes](https://github.com/stacksjs/ts-css/compare/v0.1.2...v0.1.3) | |
| 16 | ||
| 17 | ## ⚡ Performance Improvements | |
| 18 | ||
| 19 | - **build**: cut the waste out of the published output ([222ad06](https://github.com/stacksjs/ts-css/commit/222ad06)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 20 | ||
| 21 | ## 🧹 Chores | |
| 22 | ||
| 23 | - release v0.1.3 ([1b59c44](https://github.com/stacksjs/ts-css/commit/1b59c44)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 24 | ||
| 25 | ## Contributors | |
| 26 | ||
| 27 | - _Chris <chrisbreuer93@gmail.com>_ | |
| 28 | ||
| 29 | [Compare changes](https://github.com/stacksjs/ts-css/compare/v0.1.1...v0.1.2) | |
| 30 | ||
| 31 | ## ⚡ Performance Improvements | |
| 32 | ||
| 33 | - **build**: minify the published bundles ([ab79be3](https://github.com/stacksjs/ts-css/commit/ab79be3)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 34 | ||
| 35 | ## 🧹 Chores | |
| 36 | ||
| 37 | - release v0.1.2 ([8e3ce9a](https://github.com/stacksjs/ts-css/commit/8e3ce9a)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 38 | ||
| 39 | ## Contributors | |
| 40 | ||
| 41 | - _Chris <chrisbreuer93@gmail.com>_ | |
| 42 | ||
| 43 | [Compare changes](https://github.com/stacksjs/ts-css/compare/v0.1.0...v0.1.1) | |
| 44 | ||
| 45 | ## 🐛 Bug Fixes | |
| 46 | ||
| 47 | - remove consumer postinstall hook ([44d1ee3](https://github.com/stacksjs/ts-css/commit/44d1ee3)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 48 | - **scripts**: stop double-generating CHANGELOG on release ([7fe2506](https://github.com/stacksjs/ts-css/commit/7fe2506)) _(by Glenn Michael Torregosa <gtorregosa@gmail.com>)_ | |
| 49 | ||
| 50 | ## 🤖 Continuous Integration | |
| 51 | ||
| 52 | - drop redundant setup-bun (pantry installs bun via deps.yaml) ([7fafadf](https://github.com/stacksjs/ts-css/commit/7fafadf)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 53 | ||
| 54 | ## 🧹 Chores | |
| 55 | ||
| 56 | - release v0.1.1 ([5639aff](https://github.com/stacksjs/ts-css/commit/5639aff)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 57 | - **deps**: declare bun ^1.3.14 in deps.yaml ([83f0002](https://github.com/stacksjs/ts-css/commit/83f0002)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 58 | - **deps**: refresh bun.lock to pick up pickier 0.1.37 ([6b8d86c](https://github.com/stacksjs/ts-css/commit/6b8d86c)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 59 | - **deps**: refresh bun.lock to pick up pickier 0.1.35 ([2dc313e](https://github.com/stacksjs/ts-css/commit/2dc313e)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 60 | - **deps**: refresh bun.lock to pick up pickier 0.1.33 ([a89a742](https://github.com/stacksjs/ts-css/commit/a89a742)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 61 | - **deps**: refresh bun.lock to pick up @stacksjs/logsmith 0.2.3 ([0b2e754](https://github.com/stacksjs/ts-css/commit/0b2e754)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 62 | - **deps**: refresh bun.lock to pick up buddy-bot 0.9.20 ([66d94f2](https://github.com/stacksjs/ts-css/commit/66d94f2)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 63 | - **deps**: bump better-dx to ^0.2.15 ([cbe57f9](https://github.com/stacksjs/ts-css/commit/cbe57f9)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 64 | - **ci**: bump actions/checkout to v6, actions/cache to v5 ([7ed4b33](https://github.com/stacksjs/ts-css/commit/7ed4b33)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 65 | - refresh bun.lock to pick up bun-plugin-dtsx@0.9.18 ([12aa00f](https://github.com/stacksjs/ts-css/commit/12aa00f)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 66 | - refresh bun.lock and apply pickier --fix ([ceb799c](https://github.com/stacksjs/ts-css/commit/ceb799c)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 67 | - refresh bun.lock ([9a27cc0](https://github.com/stacksjs/ts-css/commit/9a27cc0)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 68 | - refresh bun.lock to pick up latest pickier ([5c717ee](https://github.com/stacksjs/ts-css/commit/5c717ee)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 69 | ||
| 70 | ## ⏪ Reverts | |
| 71 | ||
| 72 | - keep staged-lint kebab + bunx gitlint shorthand ([cb5bcc0](https://github.com/stacksjs/ts-css/commit/cb5bcc0)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 73 | ||
| 74 | ## Contributors | |
| 75 | ||
| 76 | - _Chris <chrisbreuer93@gmail.com>_ | |
| 77 | - _Glenn Michael Torregosa <gtorregosa@gmail.com>_ | |
| 78 | - _glennmichael123 <gtorregosa@gmail.com>_ | |
| 79 | ||
| 80 | ### 🐛 Bug Fixes | |
| 81 | ||
| 82 | - add setup-bun to publish-commit job ([ba0a364](https://github.com/stacksjs/ts-css/commit/ba0a364)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 83 | ||
| 84 | ### 🧹 Chores | |
| 85 | ||
| 86 | - add --bun flag to changelog scripts ([fd558f1](https://github.com/stacksjs/ts-css/commit/fd558f1)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 87 | - normalize CI action versions to match other repos ([f2cfd71](https://github.com/stacksjs/ts-css/commit/f2cfd71)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 88 | - fix lint and typecheck errors ([9b61350](https://github.com/stacksjs/ts-css/commit/9b61350)) _(by glennmichael123 <gtorregosa@gmail.com>)_ | |
| 89 | - wip ([7e8a9b5](https://github.com/stacksjs/ts-css/commit/7e8a9b5)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 90 | - wip ([5624fd4](https://github.com/stacksjs/ts-css/commit/5624fd4)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 91 | - wip ([e793c83](https://github.com/stacksjs/ts-css/commit/e793c83)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 92 | - wip ([57b551c](https://github.com/stacksjs/ts-css/commit/57b551c)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 93 | - wip ([10a6e56](https://github.com/stacksjs/ts-css/commit/10a6e56)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 94 | - wip ([19a0620](https://github.com/stacksjs/ts-css/commit/19a0620)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 95 | - wip ([8649287](https://github.com/stacksjs/ts-css/commit/8649287)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 96 | - initial commit ([ee6a5e0](https://github.com/stacksjs/ts-css/commit/ee6a5e0)) _(by Chris <chrisbreuer93@gmail.com>)_ | |
| 97 | ||
| 98 | ### Contributors | |
| 99 | ||
| 100 | - _Chris <chrisbreuer93@gmail.com>_ | |
| 101 | - _glennmichael123 <gtorregosa@gmail.com>_ | |
| 102 | ||
| 103 | ## v0.3.1...main | |
| 104 | ||
| 105 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.3.1...main) | |
| 106 | ||
| 107 | ### 🚀 Enhancements | |
| 108 | ||
| 109 | - Add `bun-plugin-dts-auto` ([c0c487b](https://github.com/stacksjs/ts-starter/commit/c0c487b)) | |
| 110 | ||
| 111 | ### ❤️ Contributors | |
| 112 | ||
| 113 | - Chris <chrisbreuer93@gmail.com> | |
| 114 | ||
| 115 | ## v0.3.0...main | |
| 116 | ||
| 117 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.3.0...main) | |
| 118 | ||
| 119 | ### 🏡 Chore | |
| 120 | ||
| 121 | - Fix isolatedDeclarations setting ([b87b6b1](https://github.com/stacksjs/ts-starter/commit/b87b6b1)) | |
| 122 | - Adjust urls ([0a40b72](https://github.com/stacksjs/ts-starter/commit/0a40b72)) | |
| 123 | ||
| 124 | ### ❤️ Contributors | |
| 125 | ||
| 126 | - Chris <chrisbreuer93@gmail.com> | |
| 127 | ||
| 128 | ## v0.2.1...main | |
| 129 | ||
| 130 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.2.1...main) | |
| 131 | ||
| 132 | ### 🚀 Enhancements | |
| 133 | ||
| 134 | - Add `noFallthroughCasesInSwitch` ([b9cfa30](https://github.com/stacksjs/ts-starter/commit/b9cfa30)) | |
| 135 | - Add `verbatimModuleSyntax` ([c495d17](https://github.com/stacksjs/ts-starter/commit/c495d17)) | |
| 136 | - Several updates ([f703179](https://github.com/stacksjs/ts-starter/commit/f703179)) | |
| 137 | ||
| 138 | ### 🩹 Fixes | |
| 139 | ||
| 140 | - Properly use bun types ([7144221](https://github.com/stacksjs/ts-starter/commit/7144221)) | |
| 141 | ||
| 142 | ### 🏡 Chore | |
| 143 | ||
| 144 | - Adjust badge links ([432aff7](https://github.com/stacksjs/ts-starter/commit/432aff7)) | |
| 145 | - Add `runs-on` options ([9a5b97f](https://github.com/stacksjs/ts-starter/commit/9a5b97f)) | |
| 146 | - Cache node_modules ([ba2f6ce](https://github.com/stacksjs/ts-starter/commit/ba2f6ce)) | |
| 147 | - Use `ubuntu-latest` for now ([1add684](https://github.com/stacksjs/ts-starter/commit/1add684)) | |
| 148 | - Minor updates ([1007cff](https://github.com/stacksjs/ts-starter/commit/1007cff)) | |
| 149 | - Lint ([d531bdc](https://github.com/stacksjs/ts-starter/commit/d531bdc)) | |
| 150 | - Remove bunx usage ([e1a5575](https://github.com/stacksjs/ts-starter/commit/e1a5575)) | |
| 151 | - Pass bun flag ([960976f](https://github.com/stacksjs/ts-starter/commit/960976f)) | |
| 152 | - Use defaults ([157455b](https://github.com/stacksjs/ts-starter/commit/157455b)) | |
| 153 | - Run typecheck using bun flag ([f22f3b1](https://github.com/stacksjs/ts-starter/commit/f22f3b1)) | |
| 154 | - Test ([0b3c3a1](https://github.com/stacksjs/ts-starter/commit/0b3c3a1)) | |
| 155 | - Use modern js for commitlint ([4bd6978](https://github.com/stacksjs/ts-starter/commit/4bd6978)) | |
| 156 | - Update worklows readme ([f54aae9](https://github.com/stacksjs/ts-starter/commit/f54aae9)) | |
| 157 | - Adjust readme ([92d7ff1](https://github.com/stacksjs/ts-starter/commit/92d7ff1)) | |
| 158 | - More updates ([0225587](https://github.com/stacksjs/ts-starter/commit/0225587)) | |
| 159 | - Add .zed settings for biome ([1688024](https://github.com/stacksjs/ts-starter/commit/1688024)) | |
| 160 | - Extend via alias ([b108d30](https://github.com/stacksjs/ts-starter/commit/b108d30)) | |
| 161 | - Lint ([d961b2a](https://github.com/stacksjs/ts-starter/commit/d961b2a)) | |
| 162 | - Minor updates ([e66d44a](https://github.com/stacksjs/ts-starter/commit/e66d44a)) | |
| 163 | ||
| 164 | ### ❤️ Contributors | |
| 165 | ||
| 166 | - Chris <chrisbreuer93@gmail.com> | |
| 167 | ||
| 168 | ## v0.2.0...main | |
| 169 | ||
| 170 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.2.0...main) | |
| 171 | ||
| 172 | ### 🏡 Chore | |
| 173 | ||
| 174 | - Remove unused action ([066f85a](https://github.com/stacksjs/ts-starter/commit/066f85a)) | |
| 175 | - Housekeeping ([fc4e24d](https://github.com/stacksjs/ts-starter/commit/fc4e24d)) | |
| 176 | ||
| 177 | ### ❤️ Contributors | |
| 178 | ||
| 179 | - Chris <chrisbreuer93@gmail.com> | |
| 180 | ||
| 181 | ## v0.1.1...main | |
| 182 | ||
| 183 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.1.1...main) | |
| 184 | ||
| 185 | ### 🏡 Chore | |
| 186 | ||
| 187 | - Adjust eslint config name ([53c2aa6](https://github.com/stacksjs/ts-starter/commit/53c2aa6)) | |
| 188 | - Set type module ([22dde14](https://github.com/stacksjs/ts-starter/commit/22dde14)) | |
| 189 | ||
| 190 | ### ❤️ Contributors | |
| 191 | ||
| 192 | - Chris <chrisbreuer93@gmail.com> | |
| 193 | ||
| 194 | ## v0.1.0...main | |
| 195 | ||
| 196 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.1.0...main) | |
| 197 | ||
| 198 | ### 🏡 Chore | |
| 199 | ||
| 200 | - Use correct cover image ([75bd3ae](https://github.com/stacksjs/ts-starter/commit/75bd3ae)) | |
| 201 | ||
| 202 | ### ❤️ Contributors | |
| 203 | ||
| 204 | - Chris <chrisbreuer93@gmail.com> | |
| 205 | ||
| 206 | ## v0.0.5...main | |
| 207 | ||
| 208 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.0.5...main) | |
| 209 | ||
| 210 | ### 🚀 Enhancements | |
| 211 | ||
| 212 | - Add pkgx deps ([319c066](https://github.com/stacksjs/ts-starter/commit/319c066)) | |
| 213 | - Use flat eslint config ([cdb0093](https://github.com/stacksjs/ts-starter/commit/cdb0093)) | |
| 214 | ||
| 215 | ### 🏡 Chore | |
| 216 | ||
| 217 | - Fix badge ([bc3b000](https://github.com/stacksjs/ts-starter/commit/bc3b000)) | |
| 218 | - Minor updates ([78dc522](https://github.com/stacksjs/ts-starter/commit/78dc522)) | |
| 219 | - Housekeeping ([e1cba3b](https://github.com/stacksjs/ts-starter/commit/e1cba3b)) | |
| 220 | - Additional housekeeping ([f5dc625](https://github.com/stacksjs/ts-starter/commit/f5dc625)) | |
| 221 | - Add `.gitattributes` ([7080f8c](https://github.com/stacksjs/ts-starter/commit/7080f8c)) | |
| 222 | - Adjust deps ([cc71b42](https://github.com/stacksjs/ts-starter/commit/cc71b42)) | |
| 223 | - Adjust wording ([3bc54b3](https://github.com/stacksjs/ts-starter/commit/3bc54b3)) | |
| 224 | - Adjust readme cover ([e6acbb2](https://github.com/stacksjs/ts-starter/commit/e6acbb2)) | |
| 225 | ||
| 226 | ### ❤️ Contributors | |
| 227 | ||
| 228 | - Chris <chrisbreuer93@gmail.com> | |
| 229 | ||
| 230 | ## v0.0.5...main | |
| 231 | ||
| 232 | [compare changes](https://github.com/stacksjs/ts-starter/compare/v0.0.5...main) | |
| 233 | ||
| 234 | ### 🚀 Enhancements | |
| 235 | ||
| 236 | - Add pkgx deps ([319c066](https://github.com/stacksjs/ts-starter/commit/319c066)) | |
| 237 | - Use flat eslint config ([cdb0093](https://github.com/stacksjs/ts-starter/commit/cdb0093)) | |
| 238 | ||
| 239 | ### 🏡 Chore | |
| 240 | ||
| 241 | - Fix badge ([bc3b000](https://github.com/stacksjs/ts-starter/commit/bc3b000)) | |
| 242 | - Minor updates ([78dc522](https://github.com/stacksjs/ts-starter/commit/78dc522)) | |
| 243 | - Housekeeping ([e1cba3b](https://github.com/stacksjs/ts-starter/commit/e1cba3b)) | |
| 244 | - Additional housekeeping ([f5dc625](https://github.com/stacksjs/ts-starter/commit/f5dc625)) | |
| 245 | - Add `.gitattributes` ([7080f8c](https://github.com/stacksjs/ts-starter/commit/7080f8c)) | |
| 246 | - Adjust deps ([cc71b42](https://github.com/stacksjs/ts-starter/commit/cc71b42)) | |
| 247 | - Adjust wording ([3bc54b3](https://github.com/stacksjs/ts-starter/commit/3bc54b3)) | |
| 248 | - Adjust readme cover ([e6acbb2](https://github.com/stacksjs/ts-starter/commit/e6acbb2)) | |
| 249 | ||
| 250 | ### ❤️ Contributors | |
| 251 | ||
| 252 | - Chris <chrisbreuer93@gmail.com> | |
| @@ -0,0 +1,23 @@ | ||
| 1 | # Claude Code Guidelines | |
| 2 | ||
| 3 | ## Linting | |
| 4 | ||
| 5 | - Use **pickier** for linting — never use eslint directly | |
| 6 | - Run `bunx --bun pickier .` to lint, `bunx --bun pickier . --fix` to auto-fix | |
| 7 | - When fixing unused variable warnings, prefer `// eslint-disable-next-line` comments over prefixing with `_` | |
| 8 | ||
| 9 | ## Frontend | |
| 10 | ||
| 11 | - Use **stx** for templating — never write vanilla JS (`var`, `document.*`, `window.*`) in stx templates | |
| 12 | - Use **crosswind** as the default CSS framework which enables standard Tailwind-like utility classes | |
| 13 | - stx `<script>` tags should only contain stx-compatible code (signals, composables, directives) | |
| 14 | ||
| 15 | ## Dependencies | |
| 16 | ||
| 17 | - **buddy-bot** handles dependency updates — not renovatebot | |
| 18 | - **better-dx** provides shared dev tooling as peer dependencies — do not install its peers (e.g., `typescript`, `pickier`, `bun-plugin-dtsx`) separately if `better-dx` is already in `package.json` | |
| 19 | - If `better-dx` is in `package.json`, ensure `bunfig.toml` includes `linker = "hoisted"` | |
| 20 | ||
| 21 | ## Commits | |
| 22 | ||
| 23 | - Use conventional commit messages (e.g., `fix:`, `feat:`, `chore:`) | |
| @@ -0,0 +1,21 @@ | ||
| 1 | # MIT License | |
| 2 | ||
| 3 | Copyright (c) 2024 Open Web Foundation | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. | |
| @@ -0,0 +1,213 @@ | ||
| 1 | # @stacksjs/ts-css | |
| 2 | ||
| 3 | > Pure-TypeScript CSS toolkit for Bun & Node. Zero runtime dependencies. | |
| 4 | ||
| 5 | `ts-css` is a CSS toolkit for build tools, linters, design-system pipelines, | |
| 6 | SSR templating, runtime style transforms — anything that needs to read, | |
| 7 | walk, query, transform, or minify CSS in a JS/TS environment. Written from | |
| 8 | scratch in TypeScript with strict types and the CSS Syntax Module Level 3 | |
| 9 | tokenizer. | |
| 10 | ||
| 11 | It bundles every common CSS pipeline primitive — tokenizer, AST parser, | |
| 12 | visitor walker, generator, selector parser, selector matcher, and minifier — | |
| 13 | into a single zero-dep package. The shape of each sub-API mirrors the | |
| 14 | de-facto community standard for that capability (`css-tree`-style AST and | |
| 15 | walker, `css-what`-style selector tokens, the `css-select` adapter contract, | |
| 16 | `csso`-style minify result), so adopting `ts-css` rarely requires call-site | |
| 17 | changes — but the project stands on its own and isn't pitched as "the | |
| 18 | replacement for X." | |
| 19 | ||
| 20 | ## Highlights | |
| 21 | ||
| 22 | - **Zero runtime deps.** Only ts-css's own code. Ships ~4.5k lines of TS. | |
| 23 | - **One install for the whole CSS pipeline.** Tokenizer, parser, walker, | |
| 24 | generator, selector parser/matcher, and minifier — all behind sub-module | |
| 25 | imports at `@stacksjs/ts-css/parse`, `/what`, `/select`, `/optimize`. | |
| 26 | - **Familiar API shapes.** Visitor/walker callbacks, AST node types, and | |
| 27 | the `css-select` adapter contract follow the conventions established by | |
| 28 | the popular community libraries, so most existing call sites work | |
| 29 | unchanged. | |
| 30 | - **Bun-first.** Built and tested on Bun, works on Node ≥18. | |
| 31 | - **Strict TypeScript.** Strict mode, isolatedDeclarations, verbatimModuleSyntax. | |
| 32 | ||
| 33 | ## Install | |
| 34 | ||
| 35 | ```bash | |
| 36 | bun add @stacksjs/ts-css | |
| 37 | # or | |
| 38 | npm i @stacksjs/ts-css | |
| 39 | ``` | |
| 40 | ||
| 41 | ## Quick start | |
| 42 | ||
| 43 | ### Parse, walk, generate | |
| 44 | ||
| 45 | ```ts | |
| 46 | import { generate, parse, walk } from '@stacksjs/ts-css' | |
| 47 | ||
| 48 | const ast = parse(` | |
| 49 | .foo, .bar > .baz { | |
| 50 | color: red; | |
| 51 | background: url("/x.png") no-repeat; | |
| 52 | } | |
| 53 | `) | |
| 54 | ||
| 55 | walk(ast, { | |
| 56 | visit: 'Declaration', | |
| 57 | enter(decl) { | |
| 58 | console.log(decl.property, '→', generate(decl.value)) | |
| 59 | }, | |
| 60 | }) | |
| 61 | // color → red | |
| 62 | // background → url("/x.png") no-repeat | |
| 63 | ||
| 64 | console.log(generate(ast)) | |
| 65 | // .foo,.bar>.baz{color:red;background:url("/x.png") no-repeat} | |
| 66 | ``` | |
| 67 | ||
| 68 | ### Match selectors against any tree | |
| 69 | ||
| 70 | ```ts | |
| 71 | import { selectAll } from '@stacksjs/ts-css' | |
| 72 | ||
| 73 | const adapter = { | |
| 74 | isTag: (n) => n?.type === 'el', | |
| 75 | getName: (e) => e.name, | |
| 76 | getAttributeValue: (e, n) => e.attrs[n], | |
| 77 | // …other adapter methods (see docs/api/select.md) | |
| 78 | } | |
| 79 | ||
| 80 | selectAll('p > span.foo:not(.disabled)', root, { adapter, xmlMode: true }) | |
| 81 | ``` | |
| 82 | ||
| 83 | The matcher is decoupled from any DOM implementation: pass an `Adapter` | |
| 84 | describing your tree (HTML, XML, an SVG AST, your design-system component | |
| 85 | graph, anything with `parent`/`children`/`name`/`attrs`-like access) and | |
| 86 | ts-css will match standard CSS selectors against it. | |
| 87 | ||
| 88 | ### Minify CSS | |
| 89 | ||
| 90 | ```ts | |
| 91 | import { minify } from '@stacksjs/ts-css' | |
| 92 | ||
| 93 | minify(` | |
| 94 | .a { | |
| 95 | margin: 0px 0.5em 10.000px; | |
| 96 | color: #aabbcc; | |
| 97 | color: blue; /* later wins */ | |
| 98 | } | |
| 99 | `).css | |
| 100 | // → ".a{margin:0 .5em 10px;color:blue}" | |
| 101 | ``` | |
| 102 | ||
| 103 | ### Compute selector specificity | |
| 104 | ||
| 105 | ```ts | |
| 106 | import { parse, syntax } from '@stacksjs/ts-css' | |
| 107 | ||
| 108 | const sel = parse('#a.b div', { context: 'selector' }) | |
| 109 | syntax.specificity(sel) // [1, 1, 1] | |
| 110 | ``` | |
| 111 | ||
| 112 | ## API surface | |
| 113 | ||
| 114 | `ts-css` exposes a complete CSS pipeline. The named exports below cover the | |
| 115 | endpoints most build/lint/transform tools reach for: | |
| 116 | ||
| 117 | | Capability | Export | | |
| 118 | | -------------------------------- | ---------------------------- | | |
| 119 | | Parse a stylesheet to an AST (`stylesheet`/`rule`/`declaration`/`selectorList`/etc. contexts) | `parse(source, opts?)` | | |
| 120 | | Walk the AST with `enter`/`leave` callbacks, `visit` filtering, and a `walk.skip` sentinel | `walk(ast, fn \| visitor)` | | |
| 121 | | Stringify any AST node back to CSS source | `generate(node)` | | |
| 122 | | Deep-clone an AST subtree | `clone(node)` | | |
| 123 | | Doubly-linked children container with cursor-safe `forEach(data, item, list)` mid-walk mutation | `List` / `ListItem` | | |
| 124 | | Tokenize a selector string into segments | `cssWhat.parse(selector)` | | |
| 125 | | Detect combinator/traversal segments | `cssWhat.isTraversal(seg)` | | |
| 126 | | Match a selector against an arbitrary tree via the `Adapter` contract | `selectAll`, `selectOne`, `is`, `compile` | | |
| 127 | | Minify with declaration/value compression and whitespace collapse | `minify`, `minifyBlock` | | |
| 128 | | Compute selector specificity tuple | `syntax.specificity(node)` | | |
| 129 | ||
| 130 | A namespace export (`csstree`, `cssWhat`, `cssSelect`, `csso`) is also | |
| 131 | available so existing code that imports from those packages can typically | |
| 132 | switch by just changing the import path. | |
| 133 | ||
| 134 | ## CLI | |
| 135 | ||
| 136 | ```bash | |
| 137 | ts-css minify input.css > output.css | |
| 138 | ts-css parse input.css # AST as JSON on stdout | |
| 139 | ts-css format input.css # round-trip through parser/generator | |
| 140 | ``` | |
| 141 | ||
| 142 | ## What's not (yet) in scope | |
| 143 | ||
| 144 | This is a pragmatic toolkit — full surface area where it's load-bearing, | |
| 145 | trimmed where edge cases would force in spec data or rarely-used passes: | |
| 146 | ||
| 147 | - ❌ **No CSS-spec-aware value lexer.** Validating that `red` is a valid | |
| 148 | `<color>`, or that `1px` is the right syntax for a given property, is | |
| 149 | thousands of lines of spec grammar that most pipelines don't need. The | |
| 150 | AST preserves enough information to layer that on top if you do. | |
| 151 | - ❌ **No cross-rule restructuring pass.** Reordering rules across the | |
| 152 | whole stylesheet to fold compatible declarations is complex and gives | |
| 153 | marginal gains beyond gzip. Declaration-level minification (numbers, | |
| 154 | colors, dedup, longhand collapse) ships fully. | |
| 155 | ||
| 156 | Everything else round-trips byte-equivalent. | |
| 157 | ||
| 158 | ## Project layout | |
| 159 | ||
| 160 | ``` | |
| 161 | src/ | |
| 162 | parse/ # tokenizer, parser, AST types, walker, generator, clone, List | |
| 163 | what/ # selector parser/stringifier | |
| 164 | select/ # selectAll/selectOne/is engine + pseudo-class compilers | |
| 165 | optimize/ # minifier, value compressors, specificity | |
| 166 | ``` | |
| 167 | ||
| 168 | Each sub-module is exported as a separate entry point so you can import | |
| 169 | just what you need: | |
| 170 | ||
| 171 | ```ts | |
| 172 | import { parse, walk } from '@stacksjs/ts-css/parse' | |
| 173 | import { selectAll } from '@stacksjs/ts-css/select' | |
| 174 | import { minify } from '@stacksjs/ts-css/optimize' | |
| 175 | ``` | |
| 176 | ||
| 177 | ## Performance | |
| 178 | ||
| 179 | End-to-end (parse → minify → generate) on a 6 KB representative fixture | |
| 180 | (Apple M3 Pro, Bun 1.3): | |
| 181 | ||
| 182 | | Pipeline | Time/iter | | |
| 183 | | --------------------------- | --------- | | |
| 184 | | `ts-css.minify(source)` | **~270 µs** | | |
| 185 | | `csso.minify(source)` | ~890 µs | | |
| 186 | ||
| 187 | → **~3.3× faster end-to-end** with zero runtime deps. Per-API breakdown | |
| 188 | in [`docs/benchmarks.md`](./docs/benchmarks.md). Run them yourself with | |
| 189 | `bun run bench`. | |
| 190 | ||
| 191 | ## Documentation | |
| 192 | ||
| 193 | Full docs: see [`./docs`](./docs) or run `bun run dev:docs` to start the | |
| 194 | local docs site. | |
| 195 | ||
| 196 | - [Getting started](./docs/intro.md) | |
| 197 | - [Parser API](./docs/api/parse.md) | |
| 198 | - [Selector parser API](./docs/api/what.md) | |
| 199 | - [Selector matcher API](./docs/api/select.md) | |
| 200 | - [Optimizer API](./docs/api/optimize.md) | |
| 201 | - [Migration guide](./docs/migration.md) | |
| 202 | - [Benchmarks](./docs/benchmarks.md) | |
| 203 | ||
| 204 | ## License | |
| 205 | ||
| 206 | MIT — see [`LICENSE.md`](./LICENSE.md). | |
| 207 | ||
| 208 | `ts-css` is an independent implementation. The visitor / linked-list shape | |
| 209 | in `src/parse`, the selector tokenizer model in `src/what`, the adapter | |
| 210 | pattern in `src/select`, and the minification plan in `src/optimize` all | |
| 211 | follow conventions established by the long-standing community libraries in | |
| 212 | each area, so call sites that already use those conventions tend to drop in | |
| 213 | without changes. All such projects are MIT-licensed. | |
| @@ -0,0 +1,185 @@ | ||
| 1 | /** | |
| 2 | * Mitata bench: ts-css vs the original css-tree / css-select / css-what / csso | |
| 3 | * libraries. Run with: | |
| 4 | * | |
| 5 | * bun bench/index.bench.ts | |
| 6 | * | |
| 7 | * Prints per-operation timings and ratios. The fixtures under | |
| 8 | * `test/fixtures/` are concatenated into one ~6 KB stylesheet (small but | |
| 9 | * representative of the SVGO/optimize use case). | |
| 10 | */ | |
| 11 | ||
| 12 | import { readFileSync } from 'node:fs' | |
| 13 | import { join } from 'node:path' | |
| 14 | import * as cssTree from 'css-tree' | |
| 15 | import * as cssSelect from 'css-select' | |
| 16 | import * as cssWhat from 'css-what' | |
| 17 | import * as csso from 'csso' | |
| 18 | import { barplot, bench, group, run, summary } from 'mitata' | |
| 19 | ||
| 20 | import * as ourCss from '../src' | |
| 21 | import * as ourCsso from '../src/optimize' | |
| 22 | import * as ourSelect from '../src/select' | |
| 23 | import * as ourWhat from '../src/what' | |
| 24 | ||
| 25 | const FIX = join(import.meta.dir, '..', 'test', 'fixtures') | |
| 26 | const samples = ['reset.css', 'utilities.css', 'component.css'].map(f => readFileSync(join(FIX, f), 'utf8')) | |
| 27 | const sample = samples.join('\n') | |
| 28 | ||
| 29 | console.log(`Fixture: ${sample.length} bytes (concatenation of reset+utilities+component).`) | |
| 30 | console.log(`Bun ${Bun.version}`) | |
| 31 | console.log() | |
| 32 | ||
| 33 | // ---- Parse ---- | |
| 34 | group('parse stylesheet', () => { | |
| 35 | barplot(() => { | |
| 36 | bench('ts-css', () => ourCss.parse(sample)) | |
| 37 | bench('css-tree', () => cssTree.parse(sample)) | |
| 38 | }) | |
| 39 | }) | |
| 40 | ||
| 41 | // ---- Generate ---- | |
| 42 | { | |
| 43 | const ourAst = ourCss.parse(sample) | |
| 44 | const treeAst = cssTree.parse(sample) | |
| 45 | group('generate stylesheet', () => { | |
| 46 | barplot(() => { | |
| 47 | bench('ts-css', () => ourCss.generate(ourAst)) | |
| 48 | bench('css-tree', () => cssTree.generate(treeAst)) | |
| 49 | }) | |
| 50 | }) | |
| 51 | } | |
| 52 | ||
| 53 | // ---- Walk ---- | |
| 54 | { | |
| 55 | const ourAst = ourCss.parse(sample) | |
| 56 | const treeAst = cssTree.parse(sample) | |
| 57 | group('walk all nodes', () => { | |
| 58 | barplot(() => { | |
| 59 | bench('ts-css', () => { | |
| 60 | let n = 0 | |
| 61 | ourCss.walk(ourAst, () => { n++ }) | |
| 62 | return n | |
| 63 | }) | |
| 64 | bench('css-tree', () => { | |
| 65 | let n = 0 | |
| 66 | cssTree.walk(treeAst, () => { n++ }) | |
| 67 | return n | |
| 68 | }) | |
| 69 | }) | |
| 70 | }) | |
| 71 | } | |
| 72 | ||
| 73 | // ---- Selector parse ---- | |
| 74 | const SELECTORS = [ | |
| 75 | 'div', | |
| 76 | '.foo', | |
| 77 | '#bar', | |
| 78 | 'p > span.foo', | |
| 79 | 'a:hover[data-x="y" i]', | |
| 80 | 'div + .x ~ p:nth-child(2n+1):not(.disabled)', | |
| 81 | '*[data-foo],[data-bar]:where(.a, .b)', | |
| 82 | ] | |
| 83 | group('parse 7 selectors', () => { | |
| 84 | barplot(() => { | |
| 85 | bench('ts-css', () => { | |
| 86 | for (const s of SELECTORS) ourWhat.parse(s) | |
| 87 | }) | |
| 88 | bench('css-what', () => { | |
| 89 | for (const s of SELECTORS) cssWhat.parse(s) | |
| 90 | }) | |
| 91 | }) | |
| 92 | }) | |
| 93 | ||
| 94 | // ---- selectAll on a synthetic tree ---- | |
| 95 | { | |
| 96 | interface El { type: 'el', name: string, attrs: Record<string, string>, children: El[], parent: El | null } | |
| 97 | function el(name: string, attrs: Record<string, string> = {}, ...children: El[]): El { | |
| 98 | const e: El = { type: 'el', name, attrs, children, parent: null } | |
| 99 | for (const c of children) c.parent = e | |
| 100 | return e | |
| 101 | } | |
| 102 | // 5-level deep tree, ~250 nodes | |
| 103 | const root = el('html', {}, | |
| 104 | el('body', { class: 'app' }, | |
| 105 | ...Array.from({ length: 10 }, (_, i) => | |
| 106 | el('section', { id: `s${i}`, class: i % 2 ? 'even' : 'odd' }, | |
| 107 | ...Array.from({ length: 4 }, (_, j) => | |
| 108 | el('article', { 'data-idx': String(j) }, | |
| 109 | el('h2', { class: 'title' }, el('span', { class: 'inner' })), | |
| 110 | el('p', { class: i === 5 ? 'highlight' : 'normal' }, el('a', { href: '#x' })), | |
| 111 | el('p', { class: 'tail' }), | |
| 112 | ), | |
| 113 | ), | |
| 114 | ), | |
| 115 | ), | |
| 116 | ), | |
| 117 | ) | |
| 118 | const adapter = { | |
| 119 | isTag: (n: any): n is El => n?.type === 'el', | |
| 120 | existsOne: (test: any, xs: any[]): boolean => xs.some((x: any) => adapter.isTag(x) && (test(x) || adapter.existsOne(test, x.children))), | |
| 121 | getAttributeValue: (e: El, n: string) => e.attrs[n], | |
| 122 | getChildren: (e: any) => e.children ?? [], | |
| 123 | getName: (e: El) => e.name, | |
| 124 | getParent: (e: any) => e.parent ?? null, | |
| 125 | getSiblings: (e: any) => e.parent ? e.parent.children : [e], | |
| 126 | getText: () => '', | |
| 127 | hasAttrib: (e: El, n: string) => n in e.attrs, | |
| 128 | removeSubsets: (n: any[]) => n, | |
| 129 | findAll: (test: any, xs: any[]): El[] => { | |
| 130 | const out: El[] = [] | |
| 131 | for (const x of xs) { | |
| 132 | if (adapter.isTag(x)) { | |
| 133 | if (test(x)) out.push(x) | |
| 134 | out.push(...adapter.findAll(test, x.children)) | |
| 135 | } | |
| 136 | } | |
| 137 | return out | |
| 138 | }, | |
| 139 | findOne: (test: any, xs: any[]): El | null => { | |
| 140 | for (const x of xs) { | |
| 141 | if (adapter.isTag(x)) { | |
| 142 | if (test(x)) return x | |
| 143 | const r = adapter.findOne(test, x.children) | |
| 144 | if (r) return r | |
| 145 | } | |
| 146 | } | |
| 147 | return null | |
| 148 | }, | |
| 149 | } | |
| 150 | const opts = { adapter, xmlMode: true } as any | |
| 151 | ||
| 152 | const targets = ['p', 'p.highlight', 'section.odd > article > h2 .inner', '[data-idx="2"]'] | |
| 153 | group('selectAll on a 250-node tree', () => { | |
| 154 | barplot(() => { | |
| 155 | bench('ts-css', () => { | |
| 156 | for (const sel of targets) ourSelect.selectAll(sel, root, opts) | |
| 157 | }) | |
| 158 | bench('css-select', () => { | |
| 159 | for (const sel of targets) cssSelect.selectAll(sel, root, opts) | |
| 160 | }) | |
| 161 | }) | |
| 162 | }) | |
| 163 | } | |
| 164 | ||
| 165 | // ---- minify ---- | |
| 166 | group('minify stylesheet', () => { | |
| 167 | barplot(() => { | |
| 168 | bench('ts-css', () => ourCsso.minify(sample)) | |
| 169 | bench('csso', () => csso.minify(sample)) | |
| 170 | }) | |
| 171 | }) | |
| 172 | ||
| 173 | // ---- summary ---- | |
| 174 | summary(() => { | |
| 175 | bench('ts-css combined parse→minify→generate', () => { | |
| 176 | const { css } = ourCsso.minify(sample) | |
| 177 | return css.length | |
| 178 | }) | |
| 179 | bench('original combined parse→minify→generate', () => { | |
| 180 | const { css } = csso.minify(sample) | |
| 181 | return css.length | |
| 182 | }) | |
| 183 | }) | |
| 184 | ||
| 185 | await run() | |
| @@ -0,0 +1,58 @@ | ||
| 1 | #!/usr/bin/env bun | |
| 2 | /** | |
| 3 | * ts-css CLI — minify, parse, or round-trip CSS files. | |
| 4 | */ | |
| 5 | ||
| 6 | import { CLI } from '@stacksjs/clapp' | |
| 7 | import { existsSync, readFileSync } from 'node:fs' | |
| 8 | import { minify, minifyBlock } from '../src/optimize' | |
| 9 | import { generate, parse } from '../src/parse' | |
| 10 | import { version } from '../package.json' | |
| 11 | ||
| 12 | function readSourceOrExit(file: string): string { | |
| 13 | if (!existsSync(file)) { | |
| 14 | process.stderr.write(`ts-css: file not found: ${file}\n`) | |
| 15 | process.exit(1) | |
| 16 | } | |
| 17 | return readFileSync(file, 'utf8') | |
| 18 | } | |
| 19 | ||
| 20 | const cli = new CLI('ts-css') | |
| 21 | ||
| 22 | cli | |
| 23 | .command('minify <file>', 'Minify a CSS file') | |
| 24 | .option('--no-comments', 'Strip /*!*/ comments too') | |
| 25 | .option('--block', 'Treat input as a declarationList (style="…" body)') | |
| 26 | .action(async (file: string, opts: { comments: boolean, block: boolean }) => { | |
| 27 | const css = readSourceOrExit(file) | |
| 28 | const minOptions = { comments: opts.comments === false ? false : ('exclamation' as const) } | |
| 29 | const result = opts.block | |
| 30 | ? minifyBlock(css, minOptions) | |
| 31 | : minify(css, minOptions) | |
| 32 | process.stdout.write(result.css) | |
| 33 | }) | |
| 34 | ||
| 35 | cli | |
| 36 | .command('parse <file>', 'Parse a CSS file and print its AST as JSON') | |
| 37 | .option('--positions', 'Include source locations on every node') | |
| 38 | .action(async (file: string, opts: { positions: boolean }) => { | |
| 39 | const css = readSourceOrExit(file) | |
| 40 | const ast = parse(css, { positions: opts.positions }) | |
| 41 | process.stdout.write(JSON.stringify(ast, null, 2)) | |
| 42 | }) | |
| 43 | ||
| 44 | cli | |
| 45 | .command('format <file>', 'Round-trip CSS through parser/generator (deterministic single-line output)') | |
| 46 | .action(async (file: string) => { | |
| 47 | const css = readSourceOrExit(file) | |
| 48 | const ast = parse(css) | |
| 49 | process.stdout.write(generate(ast)) | |
| 50 | }) | |
| 51 | ||
| 52 | cli.command('version', 'Show the version of the CLI').action(() => { | |
| 53 | console.log(version) | |
| 54 | }) | |
| 55 | ||
| 56 | cli.version(version) | |
| 57 | cli.help() | |
| 58 | cli.parse() | |
| @@ -0,0 +1,27 @@ | ||
| 1 | import { dts } from 'bun-plugin-dtsx' | |
| 2 | ||
| 3 | await Bun.build({ | |
| 4 | splitting: true, | |
| 5 | minify: true, | |
| 6 | entrypoints: [ | |
| 7 | 'src/index.ts', | |
| 8 | 'src/parse/index.ts', | |
| 9 | 'src/what/index.ts', | |
| 10 | 'src/select/index.ts', | |
| 11 | 'src/optimize/index.ts', | |
| 12 | ], | |
| 13 | outdir: './dist', | |
| 14 | target: 'bun', | |
| 15 | plugins: [dts()], | |
| 16 | }) | |
| 17 | ||
| 18 | // The CLI the `bin` field points at. Nothing built it, so `dist/bin/cli.js` | |
| 19 | // has never existed and every install logged a failed bin link — the command | |
| 20 | // was declared and unavailable. Built separately so it lands under dist/bin/, | |
| 21 | // which is where the manifest already looks for it. | |
| 22 | await Bun.build({ | |
| 23 | minify: true, | |
| 24 | entrypoints: ['bin/cli.ts'], | |
| 25 | outdir: './dist/bin', | |
| 26 | target: 'bun', | |
| 27 | }) | |
packages/toolkit/bun.lockadded+338-0
| @@ -0,0 +1,338 @@ | ||
| 1 | { | |
| 2 | "lockfileVersion": 1, | |
| 3 | "configVersion": 1, | |
| 4 | "workspaces": { | |
| 5 | "": { | |
| 6 | "name": "@stacksjs/ts-css", | |
| 7 | "dependencies": { | |
| 8 | "bunfig": "^0.15.6", | |
| 9 | }, | |
| 10 | "devDependencies": { | |
| 11 | "@types/bun": "latest", | |
| 12 | "@types/css-tree": "^2.3.11", | |
| 13 | "@types/csso": "^5.0.4", | |
| 14 | "better-dx": "^0.2.15", | |
| 15 | "css-select": "^7.0.0", | |
| 16 | "css-tree": "^3.2.1", | |
| 17 | "css-what": "^8.0.0", | |
| 18 | "csso": "^5.0.5", | |
| 19 | "mitata": "^1.0.34", | |
| 20 | }, | |
| 21 | }, | |
| 22 | }, | |
| 23 | "packages": { | |
| 24 | "@cwcss/crosswind": ["@cwcss/crosswind@0.2.6", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.0" }, "bin": { "crosswind": "./dist/cli.js" } }, "sha512-8T1oJD8UMrpImgODg5tZO8soUySt5U8gEeHCIcC1yRNgyiEDIRIgQuKallaKs21/ab2pPOwCZdHyDXuWkq2ygg=="], | |
| 25 | ||
| 26 | "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], | |
| 27 | ||
| 28 | "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], | |
| 29 | ||
| 30 | "@stacksjs/bumpx": ["@stacksjs/bumpx@0.2.6", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/logsmith": "^0.2.1", "bunfig": "^0.15.6" }, "bin": { "bumpx": "./dist/bin/cli.js" } }, "sha512-y4Jz8WKTJtes6/o1vsIbOeuIoqKItfnbeel94qrJKNKsY2CBzU3FT3YwKjs8K52JFfyACCdQa1tGx+lQWtpyTA=="], | |
| 31 | ||
| 32 | "@stacksjs/bunpress": ["@stacksjs/bunpress@0.1.11", "", { "dependencies": { "@stacksjs/stx": "^0.2.72", "@stacksjs/ts-cloud": "^0.5.17", "@stacksjs/ts-md": "^0.1.1", "bunfig": "^0.15.6" }, "bin": { "bunpress": "./dist/bin/cli.js" } }, "sha512-nXWQxlBcEU9RmjZC1Wn+DSiQyNyHWUPsxAl+xuf2F/Vqk/FCfpCBueZCO1sP87dYFguWDIig8TQVB1DEBk+CTA=="], | |
| 33 | ||
| 34 | "@stacksjs/clapp": ["@stacksjs/clapp@0.2.10", "", {}, "sha512-l/2F0u5ihpae8R4kJ3Pdh6tV7aEpSxu6hCyafhw38Fl9+x2YKxZfUK+fF/G5AVwSzJar3dkKuMxbMcQ0PXKGRg=="], | |
| 35 | ||
| 36 | "@stacksjs/clarity": ["@stacksjs/clarity@0.3.28", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "clarity": "./dist/bin/cli.js" } }, "sha512-SElzGeDewEbp0c4C8PTfdvi/Xtr1HGAjG6b0prHvkVxyYGM+2R0BFU0otUmy+DbR7wsfZ4rNU3YN1+JK/CzoTw=="], | |
| 37 | ||
| 38 | "@stacksjs/desktop": ["@stacksjs/desktop@0.2.82", "", { "peerDependencies": { "craft": "*" }, "optionalPeers": ["craft"] }, "sha512-ymawT8XY6lT6hHdfOGgdOlKB/pUu2NB1eSG+zxmXs+vn/PuIO6rt1nibOuFyDdulkS0zTmeXo0uYiNHvY7mhoQ=="], | |
| 39 | ||
| 40 | "@stacksjs/dtsx": ["@stacksjs/dtsx@0.9.26", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0" }, "peerDependencies": { "typescript": ">=5.9.3" }, "bin": { "dtsx": "./dist/bin/cli.js" } }, "sha512-MOUxZn8BhXpFYM+vh7cnQLUOLjZ4G6q76V+4OypkRiu/Gvpek9ywBQLbmv8iWLjT6gXlFMpEdw33o3GKKLpMCw=="], | |
| 41 | ||
| 42 | "@stacksjs/gitlint": ["@stacksjs/gitlint@0.1.6", "", { "bin": { "gitlint": "./dist/bin/cli.js" } }, "sha512-A4MTsddeWHEhz0mLKq+P/CqHxmWv4SWhrmWotP1UMtVbGlpYs/q+bwqrFyThT6L9xMR8iWKGVTV908oS6QDeNg=="], | |
| 43 | ||
| 44 | "@stacksjs/logsmith": ["@stacksjs/logsmith@0.2.3", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.6", "pickier": "^0.1.9" }, "bin": { "@stacksjs/logsmith": "./dist/bin/cli.js", "logsmith": "./dist/bin/cli.js" } }, "sha512-Jy2AHVdEQwDtjlOVSG4xOmIMSy/D2iZcPK7KaoDOmKfZbMmfmbhveK16w1TWKCY9duhMZigdpeFx39ZzXGARVg=="], | |
| 45 | ||
| 46 | "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.82", "", {}, "sha512-NOqW3bcvqRutURpaIORdiLg/4RCZvkCzQEHhyfzlfuAWDwdoPVbGBKdNjyLL4yoKLj1h7ntHrah+Tc8OuDYn6w=="], | |
| 47 | ||
| 48 | "@stacksjs/stx": ["@stacksjs/stx@0.2.82", "", { "dependencies": { "@cwcss/crosswind": "^0.2.6", "@stacksjs/clapp": "^0.2.0", "@stacksjs/desktop": "0.2.82", "@stacksjs/sanitizer": "0.2.82", "@stacksjs/ts-craft": "^0.0.2", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.82", "bunfig": "^0.15.6", "stx-router": "0.2.82", "ts-broadcasting": "^0.0.4", "ts-images": "^0.1.8", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-k8pgFx3uBW8+QwdBzMQWMgC5RtLpmEvrlLW6NMzkop34jZppz8L5L/1vfs73mVvnKvnh6IqAT0Hizgp1R8umSg=="], | |
| 49 | ||
| 50 | "@stacksjs/ts-avif": ["@stacksjs/ts-avif@0.1.0", "", {}, "sha512-ytcta6ZhZeZwXJTcWlTraNLBsqL4imOfruSkJ2PwEp5RjHb4eSoqb21W4u7HLILRcILr7B1nOkIMeQct4Pozbw=="], | |
| 51 | ||
| 52 | "@stacksjs/ts-bmp": ["@stacksjs/ts-bmp@0.1.0", "", {}, "sha512-O02r5q35OaoLRkTLOcFCylQzY3Oeji6QebdzLBfuWt77MoS/gY/4igY53N++pUjA4uS34cHHd/P651LlCKoBeA=="], | |
| 53 | ||
| 54 | "@stacksjs/ts-cloud": ["@stacksjs/ts-cloud@0.5.35", "", { "dependencies": { "@stacksjs/ts-xml": "^0.1.0", "@ts-cloud/aws-types": "0.5.35", "@ts-cloud/core": "0.5.35" }, "bin": { "cloud": "./dist/bin/cli.js" } }, "sha512-yzUGTCCSDBr5AD5FEmQqcLz6RyYj68LLitE4HvmKFmom4nVVNn4L2ll9HI8Wdjs9CG6mu8NoFd3gTnryP0EXWQ=="], | |
| 55 | ||
| 56 | "@stacksjs/ts-craft": ["@stacksjs/ts-craft@0.0.2", "", { "dependencies": { "archiver": "^7.0.1" }, "bin": { "craft": "./bin/cli.ts" } }, "sha512-09vAI4BJuXXNClFoOE6f1acrKqV77U7lObmOjnRteSqDoX7WZJU0HnMhrjaX96lF9EdPIozfIp59kPq7EZwLYQ=="], | |
| 57 | ||
| 58 | "@stacksjs/ts-css": ["@stacksjs/ts-css@0.1.0", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "ts-css": "./dist/bin/cli.js" } }, "sha512-373WvzZeXdqwi6lRwyfr05r3KOtbo4QXWPhmoGFI6NSa5ZcUEiyYJGL2KsV9mfQNTsBp+0q3wPnj1EQ/1zuieg=="], | |
| 59 | ||
| 60 | "@stacksjs/ts-i18n": ["@stacksjs/ts-i18n@0.1.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.9", "bunfig": "^0.15.6" }, "bin": { "i18n": "./dist/cli.js" } }, "sha512-33FM0gKdxBy6EUsUZLGAMe0D7QUZ4f/BQXRsQIyzkZgs2+juruUTevPuJGCjWSZsgJuiaBfEJn8Oq8ctuo1OWQ=="], | |
| 61 | ||
| 62 | "@stacksjs/ts-md": ["@stacksjs/ts-md@0.1.1", "", { "bin": { "md": "./bin/cli.ts" } }, "sha512-kvsSg7S3vHq65fnHIRggTLudN4n1sjkos86xLB62V6PVRa8iosYAmFKVDTGaJk+0MdsIoRwQbJ2vLWVcNQsgGw=="], | |
| 63 | ||
| 64 | "@stacksjs/ts-png": ["@stacksjs/ts-png@0.1.2", "", {}, "sha512-NT9AVCAlZiOMzcBH+OlAAUAbfTzakm/6Ibp5S31JInrpTDd+Qe3arEfatPLuMasFR9tyO+NMAFsF84MT6TK1vA=="], | |
| 65 | ||
| 66 | "@stacksjs/ts-spell-check": ["@stacksjs/ts-spell-check@0.1.0", "", {}, "sha512-GnrxhHh606U6e0uPoUfbyh3sWI2VCtj6IwtSS4ik8Jz7vF11WFZ4qeCnXlUSZXLLlk0ukcdpilZr4girmx7PbA=="], | |
| 67 | ||
| 68 | "@stacksjs/ts-svg": ["@stacksjs/ts-svg@0.1.1", "", { "dependencies": { "@stacksjs/ts-css": "^0.1.0", "@stacksjs/ts-png": "^0.1.2" }, "bin": { "svg": "dist/cli.js" } }, "sha512-jG/DC3JaWEvblsrl3X1mVFPZY+1+DEtt9ypWXuXHyWyud+CaD/MQdGRypJbRyLrT03HWx/x6TG9UOzh7GDaing=="], | |
| 69 | ||
| 70 | "@stacksjs/ts-webp": ["@stacksjs/ts-webp@0.1.0", "", {}, "sha512-wWA+HEmRszZg4av8jzSa1l2eDPEu06Cdy/c9gjlP/7rqUb7K5ETzK5rZt9DgDe28treKGy/vGo/1YS4yiUvtUA=="], | |
| 71 | ||
| 72 | "@stacksjs/ts-xml": ["@stacksjs/ts-xml@0.1.0", "", { "bin": { "bin-name": "./dist/bin/cli.js" } }, "sha512-YB7FSlFcPqopUOmjK1Qqr5E6QaarerIFJzWfUXOO/EuR1qX+IRRKxOKbWRmCvN72Mnchz8EfXhlvxEK+Zz/mQQ=="], | |
| 73 | ||
| 74 | "@ts-cloud/aws-types": ["@ts-cloud/aws-types@0.5.35", "", {}, "sha512-yZP8SzM3T3Lpm6o6ohEe52vjq8He41Ml+p+keoJcTL+V2TAOxvlWu6QVw7A+XVNXl39eQrDulygIxfcORoeBAw=="], | |
| 75 | ||
| 76 | "@ts-cloud/core": ["@ts-cloud/core@0.5.35", "", { "dependencies": { "@ts-cloud/aws-types": "0.5.35" } }, "sha512-3W5Bx2ql4ImF7fSw9lRjZxPchUbOKAFvnQGo5pirtUzCdehMVrzL+H5cU9YszDWUMFhdkyQsYIuRg6a0GeSFMw=="], | |
| 77 | ||
| 78 | "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], | |
| 79 | ||
| 80 | "@types/css-tree": ["@types/css-tree@2.3.11", "", {}, "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg=="], | |
| 81 | ||
| 82 | "@types/csso": ["@types/csso@5.0.4", "", { "dependencies": { "@types/css-tree": "*" } }, "sha512-W/FsRkm/9c04x9ON+bj+HQ0cSgNkG1LvcfuBCpkP7cpikM7+RkrNFLGtiofb++xBG6KGMUycLoDbi9/K621ZCw=="], | |
| 83 | ||
| 84 | "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], | |
| 85 | ||
| 86 | "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], | |
| 87 | ||
| 88 | "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], | |
| 89 | ||
| 90 | "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], | |
| 91 | ||
| 92 | "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="], | |
| 93 | ||
| 94 | "archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="], | |
| 95 | ||
| 96 | "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], | |
| 97 | ||
| 98 | "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="], | |
| 99 | ||
| 100 | "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], | |
| 101 | ||
| 102 | "bare-events": ["bare-events@2.9.1", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg=="], | |
| 103 | ||
| 104 | "bare-fs": ["bare-fs@4.7.4", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ=="], | |
| 105 | ||
| 106 | "bare-path": ["bare-path@3.1.1", "", {}, "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ=="], | |
| 107 | ||
| 108 | "bare-stream": ["bare-stream@2.13.3", "", { "dependencies": { "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ=="], | |
| 109 | ||
| 110 | "bare-url": ["bare-url@2.4.5", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ=="], | |
| 111 | ||
| 112 | "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], | |
| 113 | ||
| 114 | "better-dx": ["better-dx@0.2.15", "", { "dependencies": { "@stacksjs/bumpx": "^0.2.5", "@stacksjs/bunpress": "^0.1.4", "@stacksjs/clapp": "^0.2.6", "@stacksjs/clarity": "^0.3.28", "@stacksjs/gitlint": "^0.1.5", "@stacksjs/logsmith": "^0.2.1", "@types/bun": "^1.3.13", "buddy-bot": "^0.9.17", "bun-git-hooks": "^0.3.2", "bun-plugin-dtsx": "^0.9.25", "bunfig": "^0.15.11", "pickier": "^0.1.28", "ts-error-handling": "^0.1.1", "typescript": "^6.0.3" } }, "sha512-MYTbINOS8ISmLO5zdjSotRlqiIt6PO7Ap9qOfXU1MD2dUB+V2WHyStAzgHG9QUnmC/vj+8AUfGWYjV3lBgXGXw=="], | |
| 115 | ||
| 116 | "boolbase": ["boolbase@2.0.0", "", {}, "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA=="], | |
| 117 | ||
| 118 | "brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], | |
| 119 | ||
| 120 | "buddy-bot": ["buddy-bot@0.9.20", "", { "dependencies": { "prompts": "^2.4.2", "ts-pantry": "^0.9.30" }, "bin": { "buddy-bot": "./dist/bin/cli.js" } }, "sha512-5OZxlumHdKBHHpiUhvcl5xC9WmL4F8vcEKVgbFBqrqpvwRR9ejyry4uQgq1NvAgZHLtKRhQgUkOK/ltARxoy2A=="], | |
| 121 | ||
| 122 | "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], | |
| 123 | ||
| 124 | "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], | |
| 125 | ||
| 126 | "bun-git-hooks": ["bun-git-hooks@0.3.2", "", { "bin": { "git-hooks": "./dist/bin/cli.js", "bun-git-hooks": "./dist/bin/cli.js" } }, "sha512-cHkmB7sxmtPNekzG0nGXqbN8Q+dZf4muvV4eCB8dBCduW1DHR+4YulKinPUk/C6Rogy+JMyL1ysVVVWwZobwZw=="], | |
| 127 | ||
| 128 | "bun-plugin-dtsx": ["bun-plugin-dtsx@0.9.26", "", { "dependencies": { "@stacksjs/dtsx": "0.9.26" } }, "sha512-dOlA+gx3mju+Be0uoZqsuQ3U+GPu08wv7QQwA1k569tw8Bv9GwVWJz0jjQw6tzJi9Rvwfl31MVXu9rHYQXSKow=="], | |
| 129 | ||
| 130 | "bun-plugin-stx": ["bun-plugin-stx@0.2.82", "", { "dependencies": { "@stacksjs/stx": "^0.2.72", "stx-router": "^0.2.72" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-1H6J1C4hg0fFdw2Xs5qqyBSEsVBiCo5jjuASXqRPAS5WSCT7AY6D8MaUGHYCRSYQ7L+ZdA2/eTG0XwDgrN776A=="], | |
| 131 | ||
| 132 | "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], | |
| 133 | ||
| 134 | "bunfig": ["bunfig@0.15.14", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./bin/cli.js" } }, "sha512-9yzw5/MkTBF3X8Dc96DJUPpZDl/jIAYVlY6XxGrP8+o5fEf8sGgoIc53GkSFucyMoqC5jF1qM++3bD1LL7/N1Q=="], | |
| 135 | ||
| 136 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], | |
| 137 | ||
| 138 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], | |
| 139 | ||
| 140 | "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], | |
| 141 | ||
| 142 | "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], | |
| 143 | ||
| 144 | "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], | |
| 145 | ||
| 146 | "crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="], | |
| 147 | ||
| 148 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], | |
| 149 | ||
| 150 | "css-select": ["css-select@7.0.0", "", { "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", "domhandler": "^6.0.1", "domutils": "^4.0.2", "nth-check": "^3.0.1" } }, "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g=="], | |
| 151 | ||
| 152 | "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], | |
| 153 | ||
| 154 | "css-what": ["css-what@8.0.0", "", {}, "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw=="], | |
| 155 | ||
| 156 | "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], | |
| 157 | ||
| 158 | "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], | |
| 159 | ||
| 160 | "dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="], | |
| 161 | ||
| 162 | "domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="], | |
| 163 | ||
| 164 | "domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="], | |
| 165 | ||
| 166 | "domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="], | |
| 167 | ||
| 168 | "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], | |
| 169 | ||
| 170 | "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], | |
| 171 | ||
| 172 | "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], | |
| 173 | ||
| 174 | "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], | |
| 175 | ||
| 176 | "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], | |
| 177 | ||
| 178 | "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], | |
| 179 | ||
| 180 | "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], | |
| 181 | ||
| 182 | "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], | |
| 183 | ||
| 184 | "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], | |
| 185 | ||
| 186 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], | |
| 187 | ||
| 188 | "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], | |
| 189 | ||
| 190 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], | |
| 191 | ||
| 192 | "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], | |
| 193 | ||
| 194 | "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], | |
| 195 | ||
| 196 | "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], | |
| 197 | ||
| 198 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], | |
| 199 | ||
| 200 | "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], | |
| 201 | ||
| 202 | "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], | |
| 203 | ||
| 204 | "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], | |
| 205 | ||
| 206 | "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], | |
| 207 | ||
| 208 | "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], | |
| 209 | ||
| 210 | "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], | |
| 211 | ||
| 212 | "minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], | |
| 213 | ||
| 214 | "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], | |
| 215 | ||
| 216 | "mitata": ["mitata@1.0.34", "", {}, "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA=="], | |
| 217 | ||
| 218 | "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], | |
| 219 | ||
| 220 | "nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="], | |
| 221 | ||
| 222 | "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], | |
| 223 | ||
| 224 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], | |
| 225 | ||
| 226 | "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], | |
| 227 | ||
| 228 | "pickier": ["pickier@0.1.37", "", { "dependencies": { "@stacksjs/clapp": "^0.2.3", "@stacksjs/clarity": "^0.3.24" }, "optionalDependencies": { "@stacksjs/ts-spell-check": "^0.1.0" }, "bin": { "pickier": "./dist/bin/cli.js" } }, "sha512-wna4oJxKm59QuTCwGEMuxowaEeXmj0L1tzHtAkgJ593KM6eKGurp7fDclMdnnnCk1eHlCuxDBI20qvK3DUC4KQ=="], | |
| 229 | ||
| 230 | "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], | |
| 231 | ||
| 232 | "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], | |
| 233 | ||
| 234 | "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], | |
| 235 | ||
| 236 | "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], | |
| 237 | ||
| 238 | "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], | |
| 239 | ||
| 240 | "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], | |
| 241 | ||
| 242 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], | |
| 243 | ||
| 244 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], | |
| 245 | ||
| 246 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], | |
| 247 | ||
| 248 | "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], | |
| 249 | ||
| 250 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | |
| 251 | ||
| 252 | "streamx": ["streamx@2.28.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw=="], | |
| 253 | ||
| 254 | "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], | |
| 255 | ||
| 256 | "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], | |
| 257 | ||
| 258 | "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], | |
| 259 | ||
| 260 | "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], | |
| 261 | ||
| 262 | "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], | |
| 263 | ||
| 264 | "stx-router": ["stx-router@0.2.82", "", {}, "sha512-IAEow8sBL0en/EBgNDCOvz5cUrJcauaFV+NflP/BQXDhJsmi1FIRTSxwAfv14q7NZ0AjzGu49zGvwcYBuPO9Hw=="], | |
| 265 | ||
| 266 | "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], | |
| 267 | ||
| 268 | "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], | |
| 269 | ||
| 270 | "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], | |
| 271 | ||
| 272 | "ts-broadcasting": ["ts-broadcasting@0.0.4", "", { "bin": { "broadcast": "./dist/bin/cli.js" } }, "sha512-dnPtNM6mTCCnJcf8S9ffnWMOLCr5LRpxz87haYw7AtqyqCkM5B3kT6tXr1jJnxjDlWPfSfhpY+n6Y6N2l9CGPQ=="], | |
| 273 | ||
| 274 | "ts-error-handling": ["ts-error-handling@0.1.1", "", {}, "sha512-d4VqdX0bJ+jvOYNsY58isJgrEr806Q9cjef5+1UO/kIn1M/+YYSwpR6fqdRhn9XXVASJLSzNmcnBSdl0fRCjDg=="], | |
| 275 | ||
| 276 | "ts-gif": ["ts-gif@0.1.2", "", { "dependencies": { "data-uri-to-buffer": "^6.0.2" } }, "sha512-jbraplO1qqJtrtkFeepNPUGPad7w8FQHWIQFsn1OJbHeWKUbdqdkrX18XzNXI5Fw7+s6A3nbNTdj+Ukn4d3f8g=="], | |
| 277 | ||
| 278 | "ts-images": ["ts-images@0.1.8", "", { "dependencies": { "@stacksjs/ts-avif": "^0.1.0", "@stacksjs/ts-bmp": "^0.1.0", "@stacksjs/ts-png": "^0.1.2", "@stacksjs/ts-svg": "^0.1.1", "@stacksjs/ts-webp": "^0.1.0", "ts-gif": "^0.1.2", "ts-jpeg": "^0.3.1" }, "bin": { "imgx": "./dist/bin/cli.js" } }, "sha512-z1S9gRanhORW6/N6QOtRwx2E/jTwIsJMh1KIRm31MEVulq5nsoNI3YFVWdkYnrLnRc3s2A83ptY4DeEPtY7Sqg=="], | |
| 279 | ||
| 280 | "ts-jpeg": ["ts-jpeg@0.3.1", "", {}, "sha512-KIGTJj4UIlkGoCFEofx8jHHQEkJfSLFJY0MHz60Mu4Scjrcj7K/0UJGCbL9QvfIaFGAOGdf0xNFpYSuzUpimwA=="], | |
| 281 | ||
| 282 | "ts-pantry": ["ts-pantry@0.9.37", "", { "dependencies": { "@stacksjs/ts-cloud": "^0.2.19", "bunfig": "^0.15.6", "ts-web-scraper": "^0.1.1" }, "bin": { "ts-pantry": "./dist/bin/cli.js" } }, "sha512-mEm/uxcxroKGZTRwXRpvFBs47wLR2djN3zO627Yy/42cmjtY74OuUewhgh1Th9qXhPPHS3zEkE+dMK6RK0rXfw=="], | |
| 283 | ||
| 284 | "ts-syntax-highlighter": ["ts-syntax-highlighter@0.2.1", "", { "bin": { "ts-syntax-highlighter": "dist/bin/cli.js" } }, "sha512-HEfxaVKifwvnX0NaOE8nfs4VFdZo6WdREj1KRehelfYsO1N+m9vOIj5exhlOqFpxOPWd4BlalU8y0WWS7yPWjw=="], | |
| 285 | ||
| 286 | "ts-web-scraper": ["ts-web-scraper@0.1.1", "", { "bin": { "scraper": "dist/bin/cli.js" } }, "sha512-js+gSRe95yDD9U3+GmjvIKBYLAN9WoZKV84x7HcSI7dqdWEIoZSeSGR/Jbr8f5hCeV9FD70KdTPIS2LQwviQXQ=="], | |
| 287 | ||
| 288 | "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], | |
| 289 | ||
| 290 | "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], | |
| 291 | ||
| 292 | "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], | |
| 293 | ||
| 294 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], | |
| 295 | ||
| 296 | "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], | |
| 297 | ||
| 298 | "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], | |
| 299 | ||
| 300 | "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], | |
| 301 | ||
| 302 | "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], | |
| 303 | ||
| 304 | "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], | |
| 305 | ||
| 306 | "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], | |
| 307 | ||
| 308 | "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], | |
| 309 | ||
| 310 | "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], | |
| 311 | ||
| 312 | "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], | |
| 313 | ||
| 314 | "ts-pantry/@stacksjs/ts-cloud": ["@stacksjs/ts-cloud@0.2.27", "", { "dependencies": { "@stacksjs/ts-xml": "^0.1.0", "@ts-cloud/aws-types": "0.2.27", "@ts-cloud/core": "0.2.27" }, "bin": { "cloud": "./dist/bin/cli.js" } }, "sha512-81+ORovneY4muI4CoM4bTrq3LORqvzbwVVEo5ifRzNglhc5SQSZWA3/S/nDnMZt7VNhFy0mn8VkqrSyZ9Emwcg=="], | |
| 315 | ||
| 316 | "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], | |
| 317 | ||
| 318 | "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], | |
| 319 | ||
| 320 | "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], | |
| 321 | ||
| 322 | "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], | |
| 323 | ||
| 324 | "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], | |
| 325 | ||
| 326 | "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], | |
| 327 | ||
| 328 | "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], | |
| 329 | ||
| 330 | "ts-pantry/@stacksjs/ts-cloud/@ts-cloud/aws-types": ["@ts-cloud/aws-types@0.2.27", "", {}, "sha512-9SStgWrllUtIA9xedrMjq0AlzAXt5xjdcVl9Q4xX9UVTp3tFIvYbaoxpCq2WmoaNqYVXTQDdArsbQvkLdpJmiw=="], | |
| 331 | ||
| 332 | "ts-pantry/@stacksjs/ts-cloud/@ts-cloud/core": ["@ts-cloud/core@0.2.27", "", { "dependencies": { "@ts-cloud/aws-types": "0.2.27" } }, "sha512-ePte26pVqgo/v5cAfinM9eEApENNr0x0uICnqjXeShDGCli1Fyh0MMxuMD60alIP7d/1qLyTESlUTrfhD9uKPQ=="], | |
| 333 | ||
| 334 | "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], | |
| 335 | ||
| 336 | "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], | |
| 337 | } | |
| 338 | } | |
| @@ -0,0 +1,102 @@ | ||
| 1 | export const core = [ | |
| 2 | { | |
| 3 | avatar: 'https://ca.slack-edge.com/TAFCQEYEP-UAFFN6YSE-fb28a6b5d278-512', | |
| 4 | name: 'Chris Breuer', | |
| 5 | title: 'Creator', | |
| 6 | org: 'Stacks', | |
| 7 | orgLink: 'https://stacksjs.org/', | |
| 8 | desc: 'Independent open source developer, builder in the Stacks ecosystem.', | |
| 9 | links: [ | |
| 10 | { icon: 'github', link: 'https://github.com/chrisbbreuer' }, | |
| 11 | { icon: 'bluesky', link: 'https://bsky.app/profile/chrisbreuer.dev' }, | |
| 12 | { icon: 'twitter', link: 'https://twitter.com/chrisbbreuer' }, | |
| 13 | ], | |
| 14 | sponsor: 'https://github.com/sponsors/chrisbbreuer', | |
| 15 | }, | |
| 16 | { | |
| 17 | avatar: 'https://avatars.githubusercontent.com/u/72235211', | |
| 18 | name: 'Blake Ayer', | |
| 19 | title: 'Cloud Genius', | |
| 20 | org: 'Stacks', | |
| 21 | orgLink: 'https://stacksjs.org/', | |
| 22 | desc: 'Core team member of Stacks.', | |
| 23 | links: [{ icon: 'github', link: 'https://github.com/blakeayer' }], | |
| 24 | sponsor: 'https://github.com/sponsors/blakeayer', | |
| 25 | }, | |
| 26 | { | |
| 27 | avatar: 'https://avatars.githubusercontent.com/u/19656966', | |
| 28 | name: 'Zoltan', | |
| 29 | title: 'Desktop Wizard', | |
| 30 | org: 'Stacks', | |
| 31 | orgLink: 'https://stacksjs.org/', | |
| 32 | desc: 'Core team member of Stacks.', | |
| 33 | links: [{ icon: 'github', link: 'https://github.com/konkonam' }], | |
| 34 | sponsor: 'https://github.com/sponsors/konkonam', | |
| 35 | }, | |
| 36 | { | |
| 37 | avatar: 'https://www.averyahill.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Favatar.626e0c07.jpg&w=128&q=75', | |
| 38 | name: 'Avery Hill', | |
| 39 | title: 'Merchandise & Event Planning', | |
| 40 | org: 'Stacks', | |
| 41 | orgLink: 'https://stacksjs.org/', | |
| 42 | desc: 'Core team member of Stacks.', | |
| 43 | links: [{ icon: 'github', link: 'https://www.averyahill.com/' }], | |
| 44 | }, | |
| 45 | { | |
| 46 | avatar: 'https://avatars.githubusercontent.com/u/10015302', | |
| 47 | name: 'Harlequin Doyon', | |
| 48 | title: 'A collaborative being', | |
| 49 | org: 'Stacks', | |
| 50 | orgLink: 'https://stacksjs.org/', | |
| 51 | desc: 'Core team member of Stacks.', | |
| 52 | links: [{ icon: 'github', link: 'https://github.com/harlekoy' }], | |
| 53 | sponsor: 'https://github.com/sponsors/harlekoy', | |
| 54 | }, | |
| 55 | { | |
| 56 | avatar: 'https://ca.slack-edge.com/TAFCQEYEP-UCX5LQ2NP-g16e383ecf66-512', | |
| 57 | name: 'Germaine Abellanosa', | |
| 58 | title: 'Social Tech Genius', | |
| 59 | org: 'Stacks', | |
| 60 | orgLink: 'https://stacksjs.org/', | |
| 61 | desc: 'Core team member of Stacks.', | |
| 62 | links: [{ icon: 'github', link: 'https://github.com/germikee' }], | |
| 63 | sponsor: 'https://github.com/sponsors/germikee', | |
| 64 | }, | |
| 65 | { | |
| 66 | avatar: 'https://avatars.githubusercontent.com/u/58994540', | |
| 67 | name: 'Frederik Bußmann', | |
| 68 | title: 'A collaborative being', | |
| 69 | org: 'Stacks', | |
| 70 | orgLink: 'https://stacksjs.org/', | |
| 71 | desc: 'Core team member of Stacks.', | |
| 72 | links: [{ icon: 'github', link: 'https://github.com/freb97' }], | |
| 73 | sponsor: 'https://github.com/sponsors/freb97', | |
| 74 | }, | |
| 75 | { | |
| 76 | avatar: 'https://avatars.githubusercontent.com/u/977413', | |
| 77 | name: 'Dorell James', | |
| 78 | title: 'A collaborative being', | |
| 79 | org: 'Stacks', | |
| 80 | orgLink: 'https://stacksjs.org/', | |
| 81 | desc: 'Core team member of Stacks.', | |
| 82 | links: [ | |
| 83 | { icon: 'github', link: 'https://github.com/dorelljames' }, | |
| 84 | { icon: 'twitter', link: 'https://twitter.com/dorelljames' }, | |
| 85 | ], | |
| 86 | sponsor: 'https://github.com/sponsors/dorelljames', | |
| 87 | }, | |
| 88 | { | |
| 89 | avatar: 'https://avatars.githubusercontent.com/u/29087513', | |
| 90 | name: 'Glenn Michael', | |
| 91 | title: 'Desktop, Mobile, Web', | |
| 92 | org: 'Stacks', | |
| 93 | orgLink: 'https://stacksjs.org/', | |
| 94 | desc: 'Core team member of Stacks.', | |
| 95 | links: [{ icon: 'github', link: 'https://github.com/glennmichael123' }], | |
| 96 | sponsor: 'https://github.com/sponsors/glennmichael123', | |
| 97 | }, | |
| 98 | ] | |
| 99 | ||
| 100 | export const emeriti = [ | |
| 101 | // | |
| 102 | ] | |
| @@ -0,0 +1,107 @@ | ||
| 1 | # Optimizer API | |
| 2 | ||
| 3 | Replaces `csso`. Compact a CSS string, remove duplicate declarations, | |
| 4 | shorten colors, and compute selector specificity. | |
| 5 | ||
| 6 | ```ts | |
| 7 | import { minify, minifyBlock, specificity, syntax } from '@stacksjs/ts-css' | |
| 8 | // or: | |
| 9 | import { /* … */ } from '@stacksjs/ts-css/optimize' | |
| 10 | ``` | |
| 11 | ||
| 12 | ## `minify(source, options?)` | |
| 13 | ||
| 14 | Parse, compress, and re-stringify a full stylesheet. | |
| 15 | ||
| 16 | ```ts | |
| 17 | minify(source: string, options?: MinifyOptions): { css: string, ast: CssNode } | |
| 18 | ``` | |
| 19 | ||
| 20 | ```ts | |
| 21 | minify('.a { margin: 0px 0.5em 10.000px; color: #aabbcc }').css | |
| 22 | // → ".a{margin:0 .5em 10px;color:#abc}" | |
| 23 | ``` | |
| 24 | ||
| 25 | ## `minifyBlock(source, options?)` | |
| 26 | ||
| 27 | Same, but parse `source` as a declaration list (the body of a `style="…"` | |
| 28 | attribute). | |
| 29 | ||
| 30 | ```ts | |
| 31 | minifyBlock('color: red; color: blue').css | |
| 32 | // → "color:blue" | |
| 33 | ``` | |
| 34 | ||
| 35 | ## `MinifyOptions` | |
| 36 | ||
| 37 | | Option | Default | Description | | |
| 38 | | ------------------- | ---------------- | ---------------------------------------------------------- | | |
| 39 | | `comments` | `'exclamation'` | `false` to strip all, `true` / `'exclamation'` to keep `/*!*/` comments, `'first-exclamation'` to keep just the first. | | |
| 40 | | `restructure` | `true` | (Reserved — currently a no-op; declaration-level minification only.) | | |
| 41 | | `forceMediaMerge` | `false` | (Reserved.) | | |
| 42 | | `usage` | `null` | Usage hints (`{tags, ids, classes, force}`) — accepted for API parity. | | |
| 43 | ||
| 44 | ## What gets compressed | |
| 45 | ||
| 46 | Inside any `Value`, ts-css compresses: | |
| 47 | ||
| 48 | | Input | Output | | |
| 49 | | ------------------ | ---------- | | |
| 50 | | `0.5em` | `.5em` | | |
| 51 | | `10.000px` | `10px` | | |
| 52 | | `0px` / `0em` / … | `0` | | |
| 53 | | `#aabbcc` | `#abc` | | |
| 54 | | `#aabbccdd` | `#abcd` | | |
| 55 | | `rgb(255,0,0)` | `#f00` | | |
| 56 | | `url("/x.png")` | `url(/x.png)` (when safe) | | |
| 57 | ||
| 58 | In each `Block`, duplicate declarations are folded with **last wins** | |
| 59 | semantics (respecting `!important`): | |
| 60 | ||
| 61 | ```css | |
| 62 | .a { color: red; color: blue } → .a { color: blue } | |
| 63 | .a { color: red !important; | |
| 64 | color: blue } → .a { color: red !important } | |
| 65 | ``` | |
| 66 | ||
| 67 | Comments matching `options.comments` rule are stripped. | |
| 68 | ||
| 69 | ## `specificity(node)` | |
| 70 | ||
| 71 | Compute the specificity of a `Selector` AST. | |
| 72 | ||
| 73 | ```ts | |
| 74 | import { parse, syntax } from '@stacksjs/ts-css' | |
| 75 | ||
| 76 | const sel = parse('#a.b div', { context: 'selector' }) | |
| 77 | syntax.specificity(sel) // [1, 1, 1] | |
| 78 | ``` | |
| 79 | ||
| 80 | Returns a `[a, b, c]` tuple per | |
| 81 | [selectors level 4](https://www.w3.org/TR/selectors-4/#specificity-rules): | |
| 82 | ||
| 83 | - `a` — id selectors | |
| 84 | - `b` — class / attribute / pseudo-class selectors | |
| 85 | - `c` — type / pseudo-element selectors | |
| 86 | ||
| 87 | `:is()`, `:not()`, `:has()`, `:matches()`, `:-moz-any()`, `:-webkit-any()` | |
| 88 | take the **maximum** specificity of their arguments. | |
| 89 | `:where()` always contributes `[0,0,0]`. | |
| 90 | ||
| 91 | The `syntax.specificity` re-export mirrors `csso.syntax.specificity` so | |
| 92 | existing call sites work without changes. | |
| 93 | ||
| 94 | ## Compose your own pipeline | |
| 95 | ||
| 96 | If you want finer control, all the building blocks are exported: | |
| 97 | ||
| 98 | ```ts | |
| 99 | import { compressTree, dedupeDeclarations, removeComments } from '@stacksjs/ts-css/optimize' | |
| 100 | import { generate, parse } from '@stacksjs/ts-css/parse' | |
| 101 | ||
| 102 | const ast = parse(input) | |
| 103 | removeComments(ast, { exclamation: true }) | |
| 104 | compressTree(ast) | |
| 105 | dedupeDeclarations(ast) | |
| 106 | const out = generate(ast) | |
| 107 | ``` | |
| @@ -0,0 +1,166 @@ | ||
| 1 | # Parser API | |
| 2 | ||
| 3 | Replaces `css-tree`. Tokenizer, recursive-descent parser, walker, | |
| 4 | generator, deep clone, and a doubly-linked `List`. | |
| 5 | ||
| 6 | ```ts | |
| 7 | import { clone, generate, List, parse, walk } from '@stacksjs/ts-css' | |
| 8 | // or, when you only need the parser: | |
| 9 | import { /* … */ } from '@stacksjs/ts-css/parse' | |
| 10 | ``` | |
| 11 | ||
| 12 | ## `parse(source, options?)` | |
| 13 | ||
| 14 | Parse a CSS source string into an AST. | |
| 15 | ||
| 16 | ```ts | |
| 17 | parse(source: string, options?: ParseOptions): CssNode | |
| 18 | ``` | |
| 19 | ||
| 20 | ### Options | |
| 21 | ||
| 22 | | Option | Default | Description | | |
| 23 | | ----------------------- | -------------- | ------------------------------------------------------------------------ | | |
| 24 | | `context` | `'stylesheet'` | What grammar to parse against (see below). | | |
| 25 | | `parseValue` | `true` | When `false`, declaration values are stored as `Raw` nodes. | | |
| 26 | | `parseAtrulePrelude` | `true` | When `false`, at-rule preludes are stored as `Raw`. | | |
| 27 | | `parseRulePrelude` | `true` | When `false`, rule preludes (selector lists) are stored as `Raw`. | | |
| 28 | | `parseCustomProperty` | `false` | When `true`, custom property values are parsed (otherwise `Raw`). | | |
| 29 | | `positions` | `false` | Track source `loc` info on every node. | | |
| 30 | | `filename` | — | Source filename, used in `loc.source`. | | |
| 31 | | `onParseError` | — | Called when the parser substitutes a `Raw` node for an error. | | |
| 32 | ||
| 33 | ### Parse contexts | |
| 34 | ||
| 35 | | `context` | Returns | Use for | | |
| 36 | | ------------------- | ------------------------------------ | ---------------------------------------------------- | | |
| 37 | | `'stylesheet'` | `StyleSheet` | Top-level CSS files (default) | | |
| 38 | | `'atrule'` | `Atrule \| StyleSheet` | A single `@rule …;` or `@rule { … }` | | |
| 39 | | `'atrulePrelude'` | `AtrulePrelude` | The `(min-width: 800px)` part of a media query | | |
| 40 | | `'rule'` | `Rule` | A single `selector { … }` block | | |
| 41 | | `'block'` | `Block` | Just the `{ decl; decl; }` body | | |
| 42 | | `'declarationList'` | `DeclarationList` | A `style="…"` attribute body | | |
| 43 | | `'declaration'` | `Declaration` | A single `prop: value;` line | | |
| 44 | | `'value'` | `Value` | Just the value side of a declaration | | |
| 45 | | `'selector'` | `Selector` | A single selector segment (`a > b`) | | |
| 46 | | `'selectorList'` | `SelectorList` | A comma-separated list (`a, b > c`) | | |
| 47 | ||
| 48 | ## `walk(ast, visitor)` | |
| 49 | ||
| 50 | Recursively traverse the AST. | |
| 51 | ||
| 52 | ```ts | |
| 53 | walk(ast: CssNode, visitor: WalkVisitor): symbol | undefined | |
| 54 | ``` | |
| 55 | ||
| 56 | `visitor` is either: | |
| 57 | - A function: `(node, item, list) => void | symbol` | |
| 58 | - An object: `{ visit?, enter?, leave?, reverse? }` | |
| 59 | ||
| 60 | Inside callbacks, `this` carries traversal context: `this.atrule`, | |
| 61 | `this.rule`, `this.selector`, `this.declaration`, etc. | |
| 62 | ||
| 63 | ```ts | |
| 64 | walk(ast, { | |
| 65 | visit: 'Declaration', | |
| 66 | enter(decl) { | |
| 67 | if (decl.property === 'color') | |
| 68 | decl.value = parse('blue', { context: 'value' }) | |
| 69 | }, | |
| 70 | }) | |
| 71 | ``` | |
| 72 | ||
| 73 | ### Sentinels | |
| 74 | ||
| 75 | | Constant | What it does | | |
| 76 | | ------------- | --------------------------------------------------------- | | |
| 77 | | `walk.skip` | Return from `enter` to skip the node's children. | | |
| 78 | | `walk.stop` | Return to halt the entire traversal. | | |
| 79 | ||
| 80 | ## `generate(node)` | |
| 81 | ||
| 82 | Serialize an AST node back to a CSS string. Output is compact (no | |
| 83 | unnecessary whitespace) — comparable to `csstree.generate(node)` with | |
| 84 | no formatting options. | |
| 85 | ||
| 86 | ```ts | |
| 87 | generate(node: CssNode): string | |
| 88 | ``` | |
| 89 | ||
| 90 | ## `clone(node)` | |
| 91 | ||
| 92 | Deep-clone an AST node, preserving the `List` structure of children. | |
| 93 | ||
| 94 | ```ts | |
| 95 | clone<T extends CssNode>(node: T): T | |
| 96 | ``` | |
| 97 | ||
| 98 | ## `List<T>` | |
| 99 | ||
| 100 | Doubly linked list — every container node uses one for its `children`. | |
| 101 | ||
| 102 | ```ts | |
| 103 | import { List } from '@stacksjs/ts-css' | |
| 104 | ||
| 105 | const list = new List<number>() | |
| 106 | list.appendData(1) | |
| 107 | list.appendData(2) | |
| 108 | list.toArray() // [1, 2] | |
| 109 | ``` | |
| 110 | ||
| 111 | ### Mutation methods | |
| 112 | ||
| 113 | | Method | Description | | |
| 114 | | --------------------------- | ------------------------------------------------- | | |
| 115 | | `appendData(data)` | Append a new item built from `data`. | | |
| 116 | | `prependData(data)` | Prepend a new item. | | |
| 117 | | `insertData(data, before?)` | Insert before `before` (a `ListItem`), or at tail. | | |
| 118 | | `insert(item, before?)` | Insert an existing `ListItem`. | | |
| 119 | | `remove(item)` | Detach an item from the list. | | |
| 120 | | `replace(old, newItem)` | Replace `old` with `newItem` (or another `List`). | | |
| 121 | | `clear()` | Detach all items. | | |
| 122 | ||
| 123 | ### Iteration | |
| 124 | ||
| 125 | | Method | Callback signature | | |
| 126 | | ------------------------------- | ----------------------------------------------- | | |
| 127 | | `forEach(fn)` | `(data, item, list)` — matches `css-tree`. | | |
| 128 | | `forEachRight(fn)` | Same, in reverse. | | |
| 129 | | `reduce(fn, init)` | `(acc, data, index, list)` | | |
| 130 | | `some(fn)` | `(data, index, list) => boolean` | | |
| 131 | | `map(fn)` | Returns a new `List`. | | |
| 132 | | `filter(fn)` | Returns a new `List`. | | |
| 133 | ||
| 134 | The `forEach` / `forEachRight` callbacks honor a cursor protocol so | |
| 135 | callbacks can safely insert/remove items mid-walk without breaking the | |
| 136 | iterator. | |
| 137 | ||
| 138 | ## AST node reference | |
| 139 | ||
| 140 | The full discriminated union is `CssNode`. The most common variants: | |
| 141 | ||
| 142 | ```ts | |
| 143 | interface StyleSheet { type: 'StyleSheet', children: List<CssNode> } | |
| 144 | interface Rule { type: 'Rule', prelude: SelectorList | Raw, block: Block } | |
| 145 | interface Atrule { type: 'Atrule', name: string, prelude: AtrulePrelude | Raw | null, block: Block | null } | |
| 146 | interface Block { type: 'Block', children: List<CssNode> } | |
| 147 | interface Declaration { type: 'Declaration', property: string, value: Value | Raw, important: boolean | string } | |
| 148 | interface Value { type: 'Value', children: List<CssNode> } | |
| 149 | interface Selector { type: 'Selector', children: List<CssNode> } | |
| 150 | // + identifiers, numbers, dimensions, hashes, urls, functions, … | |
| 151 | ``` | |
| 152 | ||
| 153 | See `src/parse/types.ts` for the complete type definitions. | |
| 154 | ||
| 155 | ## Tokenizer (advanced) | |
| 156 | ||
| 157 | If you need raw token output, import `Tokenizer` directly: | |
| 158 | ||
| 159 | ```ts | |
| 160 | import { Tokenizer, TokenType } from '@stacksjs/ts-css/parse' | |
| 161 | ||
| 162 | const t = new Tokenizer('.foo { color: red }') | |
| 163 | for (const tok of t.tokens) { | |
| 164 | console.log(TokenType[tok.type], tok.start, tok.end) | |
| 165 | } | |
| 166 | ``` | |
| @@ -0,0 +1,123 @@ | ||
| 1 | # Selector matcher API | |
| 2 | ||
| 3 | Replaces `css-select`. Run any CSS selector against any tree-like data | |
| 4 | structure via an `Adapter`. | |
| 5 | ||
| 6 | ```ts | |
| 7 | import { compile, is, selectAll, selectOne } from '@stacksjs/ts-css' | |
| 8 | // or: | |
| 9 | import { /* … */ } from '@stacksjs/ts-css/select' | |
| 10 | ``` | |
| 11 | ||
| 12 | ## `selectAll(selector, root, options)` | |
| 13 | ||
| 14 | Return every element under `root` that matches `selector`. | |
| 15 | ||
| 16 | ```ts | |
| 17 | selectAll<Node, ElementNode extends Node>( | |
| 18 | selector: string | Selector[][] | CompiledQuery<ElementNode>, | |
| 19 | root: Node | Node[], | |
| 20 | options: Options<Node, ElementNode>, | |
| 21 | ): ElementNode[] | |
| 22 | ``` | |
| 23 | ||
| 24 | ## `selectOne(selector, root, options)` | |
| 25 | ||
| 26 | Return the first match (in document order), or `null`. | |
| 27 | ||
| 28 | ```ts | |
| 29 | selectOne(...): ElementNode | null | |
| 30 | ``` | |
| 31 | ||
| 32 | ## `is(node, selector, options)` | |
| 33 | ||
| 34 | Test whether `node` itself matches `selector`. | |
| 35 | ||
| 36 | ```ts | |
| 37 | is(node, selector, options): boolean | |
| 38 | ``` | |
| 39 | ||
| 40 | ## `compile(selector, options)` | |
| 41 | ||
| 42 | Compile a selector once and reuse the predicate against many trees. | |
| 43 | ||
| 44 | ```ts | |
| 45 | const test = compile('p > span.foo', options) | |
| 46 | adapter.findAll(test, [root]) | |
| 47 | ``` | |
| 48 | ||
| 49 | ## `Adapter<Node, ElementNode>` | |
| 50 | ||
| 51 | The adapter is the tree-access contract. Every method has the same | |
| 52 | signature as `css-select`'s — if you already have one, it works as-is. | |
| 53 | ||
| 54 | | Method | Returns | Purpose | | |
| 55 | | ------------------------------------------------- | -------------------------------- | ----------------------------------------------- | | |
| 56 | | `isTag(node)` | `node is ElementNode` | Type guard for element-vs-text/comment. | | |
| 57 | | `getName(elem)` | `string` | Element tag name. | | |
| 58 | | `getAttributeValue(elem, name)` | `string \| undefined` | Attribute value (case-sensitive on `name`). | | |
| 59 | | `hasAttrib(elem, name)` | `boolean` | Whether `elem[name]` is set. | | |
| 60 | | `getChildren(node)` | `Node[]` | Direct children of `node`. | | |
| 61 | | `getParent(elem)` | `ElementNode \| null` | Parent element (or null for the root). | | |
| 62 | | `getSiblings(elem)` | `Node[]` | All siblings, including `elem`. | | |
| 63 | | `getText(node)` | `string` | Text content (used by some pseudos). | | |
| 64 | | `findAll(test, elems)` | `ElementNode[]` | DFS traversal. | | |
| 65 | | `findOne(test, elems)` | `ElementNode \| null` | DFS, first match. | | |
| 66 | | `existsOne(test, elems)` | `boolean` | DFS, just a check. | | |
| 67 | | `removeSubsets(nodes)` | `Node[]` | Remove descendants when their ancestor is also in `nodes`. | | |
| 68 | ||
| 69 | Optional state-aware hooks (always called for `:hover` / `:active` / | |
| 70 | `:visited`): | |
| 71 | ||
| 72 | | Method | Default | When you'd implement it | | |
| 73 | | --------------------- | ---------------- | --------------------------------------------- | | |
| 74 | | `isActive(elem)` | always `false` | DOM-aware match for `:active` | | |
| 75 | | `isHovered(elem)` | always `false` | DOM-aware match for `:hover` | | |
| 76 | | `isVisited(elem)` | always `false` | DOM-aware match for `:visited` | | |
| 77 | | `equals(a, b)` | `===` | Custom node equality (rare). | | |
| 78 | ||
| 79 | If your tree has no notion of focus/hover, leave these undefined — the | |
| 80 | matcher treats stateful pseudos as never-matching. | |
| 81 | ||
| 82 | ## `Options` | |
| 83 | ||
| 84 | | Option | Default | Description | | |
| 85 | | ---------------------------- | ------- | -------------------------------------------------------------------- | | |
| 86 | | `adapter` | — | **Required.** The `Adapter` for your tree. | | |
| 87 | | `xmlMode` | `false` | Disable HTML quirks (case-insensitive tags / attrs). | | |
| 88 | | `lowerCaseAttributeNames` | `true` | Lowercase attribute names before matching. | | |
| 89 | | `lowerCaseTags` | `true` | Lowercase tag names. | | |
| 90 | | `cacheResults` | `true` | Cache compiled selectors keyed by selector string. | | |
| 91 | | `context` | — | Reference node(s) for `:scope`. | | |
| 92 | | `pseudos` | — | Map of custom pseudo-class implementations. | | |
| 93 | | `relativeSelector` | — | Match `:has` lazily. | | |
| 94 | | `quirksMode` | `false` | Match HTML quirks-mode attribute case rules. | | |
| 95 | ||
| 96 | ## Supported selectors | |
| 97 | ||
| 98 | | Category | Examples | | |
| 99 | | --------------------- | --------------------------------------------------- | | |
| 100 | | Type / universal | `div`, `*`, `svg\|circle` (namespaces in xmlMode) | | |
| 101 | | Class / id | `.foo`, `#bar` | | |
| 102 | | Attribute | `[a]`, `[a=x]`, `[a~=x]`, `[a\|=x]`, `[a^=x]`, `[a$=x]`, `[a*=x]` (with `i` / `s` case flags) | | |
| 103 | | Combinators | `a b`, `a > b`, `a + b`, `a ~ b`, `a \|\| b` | | |
| 104 | | Structural pseudos | `:first-child`, `:last-child`, `:only-child`, `:first-of-type`, `:last-of-type`, `:only-of-type`, `:nth-child(...)`, `:nth-last-child(...)`, `:nth-of-type(...)`, `:nth-last-of-type(...)` | | |
| 105 | | Logical pseudos | `:is(...)`, `:not(...)`, `:where(...)`, `:has(...)`, `:matches(...)` | | |
| 106 | | Other pseudos | `:empty`, `:root`, `:scope` | | |
| 107 | | Stateful pseudos | `:hover`, `:active`, `:visited`, `:focus`, etc. — gated on adapter `is*` hooks. | | |
| 108 | ||
| 109 | ## Custom pseudo-classes | |
| 110 | ||
| 111 | Add your own via `options.pseudos`: | |
| 112 | ||
| 113 | ```ts | |
| 114 | selectAll(':my-thing', root, { | |
| 115 | adapter, | |
| 116 | pseudos: { | |
| 117 | 'my-thing': elem => adapter.getName(elem).startsWith('x-'), | |
| 118 | }, | |
| 119 | }) | |
| 120 | ``` | |
| 121 | ||
| 122 | The value is either a predicate `(elem, arg?) => boolean` or a string | |
| 123 | selector that the matcher will compile. | |
| @@ -0,0 +1,104 @@ | ||
| 1 | # Selector parser API | |
| 2 | ||
| 3 | Replaces `css-what`. Pure-string-to-selector-AST parsing — separate from | |
| 4 | the matcher, so you can analyse selectors (specificity, rewrites, lint | |
| 5 | rules) without running them. | |
| 6 | ||
| 7 | ```ts | |
| 8 | import { isTraversal, parseSelector, stringifySelector } from '@stacksjs/ts-css' | |
| 9 | // or: | |
| 10 | import { parse, stringify, isTraversal } from '@stacksjs/ts-css/what' | |
| 11 | ``` | |
| 12 | ||
| 13 | ## `parse(selector, options?)` | |
| 14 | ||
| 15 | Parse a CSS selector string into a list-of-lists of selector segments. | |
| 16 | ||
| 17 | ```ts | |
| 18 | parse(selector: string, options?: ParseOptions): Selector[][] | |
| 19 | ``` | |
| 20 | ||
| 21 | The outer array is **comma-separated selectors**; the inner arrays are | |
| 22 | **simple selectors and combinators** in source order. | |
| 23 | ||
| 24 | ```ts | |
| 25 | import { parse } from '@stacksjs/ts-css/what' | |
| 26 | ||
| 27 | parse('div > p, .foo') | |
| 28 | // [ | |
| 29 | // [{type:'tag',name:'div'}, {type:'child'}, {type:'tag',name:'p'}], | |
| 30 | // [{type:'attribute',name:'class',action:'element',value:'foo'}], | |
| 31 | // ] | |
| 32 | ``` | |
| 33 | ||
| 34 | ### Options | |
| 35 | ||
| 36 | | Option | Default | Description | | |
| 37 | | ---------------------------- | ------- | -------------------------------------------------------- | | |
| 38 | | `xmlMode` | `false` | Disable HTML quirks (case-insensitive tags / attrs). | | |
| 39 | | `lowerCaseAttributeNames` | `true` | Lowercase attribute names (HTML default). | | |
| 40 | | `lowerCaseTags` | `true` | Lowercase tag names. | | |
| 41 | ||
| 42 | ## `stringify(ast)` | |
| 43 | ||
| 44 | Inverse of `parse` — turn a selector AST back into a string. | |
| 45 | ||
| 46 | ```ts | |
| 47 | stringify(ast: Selector[][]): string | |
| 48 | ``` | |
| 49 | ||
| 50 | ```ts | |
| 51 | import { parse, stringify } from '@stacksjs/ts-css/what' | |
| 52 | ||
| 53 | const ast = parse('div > p:not(.x)') | |
| 54 | stringify(ast) // 'div > p:not(.x)' | |
| 55 | ``` | |
| 56 | ||
| 57 | ## `isTraversal(token)` | |
| 58 | ||
| 59 | Returns `true` if `token` is a combinator (descendant / child / sibling / | |
| 60 | adjacent / parent / column-combinator) and not a simple selector. | |
| 61 | ||
| 62 | ```ts | |
| 63 | isTraversal(token: Selector): boolean | |
| 64 | ``` | |
| 65 | ||
| 66 | ## Segment types | |
| 67 | ||
| 68 | The `Selector` union contains: | |
| 69 | ||
| 70 | ```ts | |
| 71 | type Selector = | |
| 72 | | { type: 'tag', name: string, namespace: string | null } | |
| 73 | | { type: 'universal', namespace: string | null } | |
| 74 | | { type: 'attribute', name: string, action: AttributeAction, value: string, namespace: string | null, ignoreCase: boolean | 'quirks' | null } | |
| 75 | | { type: 'pseudo', name: string, data: string | Selector[][] | null } | |
| 76 | | { type: 'pseudo-element', name: string, data: string | null } | |
| 77 | | { type: 'descendant' } | |
| 78 | | { type: 'child' } | |
| 79 | | { type: 'parent' } | |
| 80 | | { type: 'sibling' } | |
| 81 | | { type: 'adjacent' } | |
| 82 | | { type: 'column-combinator' } | |
| 83 | ``` | |
| 84 | ||
| 85 | `AttributeAction` is one of: | |
| 86 | `'any'`, `'element'`, `'end'`, `'equals'`, `'exists'`, `'hyphen'`, `'not'`, `'start'`. | |
| 87 | ||
| 88 | | `[a]` form | `action` | | |
| 89 | | -------------------- | ------------ | | |
| 90 | | `[a]` | `'exists'` | | |
| 91 | | `[a="x"]` | `'equals'` | | |
| 92 | | `[a~="x"]` | `'element'` | | |
| 93 | | `[a\|="x"]` | `'hyphen'` | | |
| 94 | | `[a^="x"]` | `'start'` | | |
| 95 | | `[a$="x"]` | `'end'` | | |
| 96 | | `[a*="x"]` | `'any'` | | |
| 97 | | `[a!="x"]` | `'not'` | | |
| 98 | ||
| 99 | ## Pseudo-class shape | |
| 100 | ||
| 101 | For pseudo-classes that take a selector list (`:is`, `:not`, `:where`, | |
| 102 | `:has`, `:matches`, `:-moz-any`, `:-webkit-any`), the `data` field is | |
| 103 | **already parsed** as a `Selector[][]`. For other pseudo-classes that take | |
| 104 | a string (e.g. `:nth-child(2n+1)`), `data` is the raw inner string. | |
| @@ -0,0 +1,91 @@ | ||
| 1 | # Benchmarks | |
| 2 | ||
| 3 | Numbers below are from a representative `~6 KB` CSS fixture | |
| 4 | (reset + utilities + component CSS), measured with | |
| 5 | [`mitata`](https://github.com/evanwashere/mitata) on Bun. | |
| 6 | ||
| 7 | ## Run them yourself | |
| 8 | ||
| 9 | ```bash | |
| 10 | bun run bench | |
| 11 | ``` | |
| 12 | ||
| 13 | The bench script lives at `bench/index.bench.ts` and benchmarks every | |
| 14 | public API against the closest community equivalent: | |
| 15 | ||
| 16 | | ts-css module | compared with | | |
| 17 | |---|---| | |
| 18 | | `parse` | `css-tree.parse` | | |
| 19 | | `walk` | `css-tree.walk` | | |
| 20 | | `generate` | `css-tree.generate` | | |
| 21 | | `what.parse`| `css-what.parse` | | |
| 22 | | `selectAll` | `css-select.selectAll` | | |
| 23 | | `minify` | `csso.minify` | | |
| 24 | | **end-to-end**: `minify(source)` | `csso.minify(source)` | | |
| 25 | ||
| 26 | ## Latest numbers (Apple M3 Pro, Bun 1.3) | |
| 27 | ||
| 28 | ``` | |
| 29 | Fixture: ~6 KB stylesheet | |
| 30 | ||
| 31 | parse stylesheet | |
| 32 | ts-css 83 µs/iter (ts-css 1.10× faster) | |
| 33 | css-tree 92 µs/iter | |
| 34 | ||
| 35 | generate stylesheet | |
| 36 | ts-css 19 µs/iter (ts-css 4.0× faster) | |
| 37 | css-tree 77 µs/iter | |
| 38 | ||
| 39 | walk all nodes | |
| 40 | ts-css 32 µs/iter (ts-css 1.34× faster) | |
| 41 | css-tree 43 µs/iter | |
| 42 | ||
| 43 | parse 7 selectors | |
| 44 | ts-css 1.6 µs/iter (ts-css 1.65× faster) | |
| 45 | css-what 2.7 µs/iter | |
| 46 | ||
| 47 | selectAll on a 250-node tree | |
| 48 | ts-css 40 µs/iter (ts-css 1.13× faster) | |
| 49 | css-select 45 µs/iter | |
| 50 | ||
| 51 | minify stylesheet | |
| 52 | ts-css 277 µs/iter (ts-css 3.3× faster) | |
| 53 | csso 907 µs/iter | |
| 54 | ||
| 55 | end-to-end (parse → minify → generate) | |
| 56 | ts-css 271 µs/iter (ts-css 3.28× faster) | |
| 57 | csso 890 µs/iter | |
| 58 | ``` | |
| 59 | ||
| 60 | ## Why we win on `generate` and `minify` | |
| 61 | ||
| 62 | - **`generate`** is a tight switch over the AST that walks the doubly | |
| 63 | linked list directly — no allocations beyond string concatenation, no | |
| 64 | array materialization to support lookahead, no formatter options. | |
| 65 | - **`minify`** does a single AST walk that combines value compression and | |
| 66 | whitespace compaction (no second tree traversal), short-circuits | |
| 67 | declaration dedup on blocks with fewer than two declarations, and uses | |
| 68 | manual scans where css-select / csso fall back to per-call regex. | |
| 69 | ||
| 70 | ## Why we win on `parse` and `selectAll` | |
| 71 | ||
| 72 | - **`parse`** — declaration values and at-rule preludes are parsed | |
| 73 | directly off the existing token stream by temporarily lowering an | |
| 74 | in-state `end` index. Re-tokenizing each declaration value text (the | |
| 75 | obvious approach) is what makes other parsers slower; we never do it. | |
| 76 | - **`selectAll`** — we ship our own iterative `findAll` so the consumer | |
| 77 | adapter's `findAll` (often a recursive `[].push(...spread)` shape that | |
| 78 | allocates per node) doesn't gate selector matching. The compile chain | |
| 79 | uses fail-fast leaves (`ALWAYS_TRUE` short-circuits `&& next(e)` calls) | |
| 80 | and procedure-cost sorting so the cheapest test in a compound runs | |
| 81 | first. Compile results are cached two-level (adapter + flags → | |
| 82 | selector) so per-call key allocation is gone. | |
| 83 | - **`what.parse`** — sticky regex (`/.../y`) lets us match identifier | |
| 84 | tokens without slicing. `charCodeAt` dispatch instead of `charAt` | |
| 85 | string allocation. | |
| 86 | ||
| 87 | ## End-to-end is what matters | |
| 88 | ||
| 89 | For build-tool pipelines that run parse → walk → mutate → generate, the | |
| 90 | combined pipeline completes in **roughly a third of the time** of the | |
| 91 | csso default — and ships **zero runtime dependencies**. | |
| @@ -0,0 +1,67 @@ | ||
| 1 | # Configuration | |
| 2 | ||
| 3 | _This is just an example of the ts-starter docs._ | |
| 4 | ||
| 5 | The Reverse Proxy can be configured using a `reverse-proxy.config.ts` _(or `reverse-proxy.config.js`)_ file and it will be automatically loaded when running the `reverse-proxy` command. | |
| 6 | ||
| 7 | ```ts | |
| 8 | // reverse-proxy.config.{ts,js} | |
| 9 | import type { ReverseProxyOptions } from '@stacksjs/rpx' | |
| 10 | import os from 'node:os' | |
| 11 | import path from 'node:path' | |
| 12 | ||
| 13 | const config: ReverseProxyOptions = { | |
| 14 | /** | |
| 15 | * The from URL to proxy from. | |
| 16 | * Default: localhost:5173 | |
| 17 | */ | |
| 18 | from: 'localhost:5173', | |
| 19 | ||
| 20 | /** | |
| 21 | * The to URL to proxy to. | |
| 22 | * Default: stacks.localhost | |
| 23 | */ | |
| 24 | to: 'stacks.localhost', | |
| 25 | ||
| 26 | /** | |
| 27 | * The HTTPS settings. | |
| 28 | * Default: true | |
| 29 | * If set to false, the proxy will use HTTP. | |
| 30 | * If set to true, the proxy will use HTTPS. | |
| 31 | * If set to an object, the proxy will use HTTPS with the provided settings. | |
| 32 | */ | |
| 33 | https: { | |
| 34 | domain: 'stacks.localhost', | |
| 35 | hostCertCN: 'stacks.localhost', | |
| 36 | caCertPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`), | |
| 37 | certPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt`), | |
| 38 | keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`), | |
| 39 | altNameIPs: ['127.0.0.1'], | |
| 40 | altNameURIs: ['localhost'], | |
| 41 | organizationName: 'stacksjs.org', | |
| 42 | countryName: 'US', | |
| 43 | stateName: 'California', | |
| 44 | localityName: 'Playa Vista', | |
| 45 | commonName: 'stacks.localhost', | |
| 46 | validityDays: 180, | |
| 47 | verbose: false, | |
| 48 | }, | |
| 49 | ||
| 50 | /** | |
| 51 | * The verbose setting. | |
| 52 | * Default: false | |
| 53 | * If set to true, the proxy will log more information. | |
| 54 | */ | |
| 55 | verbose: false, | |
| 56 | } | |
| 57 | ||
| 58 | export default config | |
| 59 | ``` | |
| 60 | ||
| 61 | _Then run:_ | |
| 62 | ||
| 63 | ```bash | |
| 64 | ./rpx start | |
| 65 | ``` | |
| 66 | ||
| 67 | To learn more, head over to the [documentation](https://reverse-proxy.sh/). | |
| @@ -0,0 +1,38 @@ | ||
| 1 | --- | |
| 2 | layout: home | |
| 3 | ||
| 4 | hero: | |
| 5 | name: "@stacksjs/ts-css" | |
| 6 | text: "Pure-TypeScript CSS toolkit." | |
| 7 | tagline: "Parser, walker, generator, selector engine, minifier — zero runtime deps." | |
| 8 | actions: | |
| 9 | - theme: brand | |
| 10 | text: Get started | |
| 11 | link: /intro | |
| 12 | - theme: alt | |
| 13 | text: Migration guide | |
| 14 | link: /migration | |
| 15 | - theme: alt | |
| 16 | text: View on GitHub | |
| 17 | link: https://github.com/stacksjs/ts-css | |
| 18 | ||
| 19 | features: | |
| 20 | - title: "All-in-one CSS pipeline" | |
| 21 | icon: "🧩" | |
| 22 | details: "Replaces css-tree + css-select + css-what + csso. One install, no transitive deps." | |
| 23 | - title: "Drop-in compatible" | |
| 24 | icon: "🔁" | |
| 25 | details: "Namespace re-exports keep existing call sites unchanged. Migrate by editing imports." | |
| 26 | - title: "Selector engine" | |
| 27 | icon: "🎯" | |
| 28 | details: "Run any CSS selector against any tree. Adapter pattern preserved verbatim." | |
| 29 | - title: "Minifier" | |
| 30 | icon: "📦" | |
| 31 | details: "Number / color / unit compression, declaration dedup, comment cleanup, specificity calc." | |
| 32 | - title: "Bun-first" | |
| 33 | icon: "🥟" | |
| 34 | details: "Built on Bun, runs on Node ≥18. Strict TypeScript with isolatedDeclarations." | |
| 35 | - title: "Small surface, full coverage" | |
| 36 | icon: "✂️" | |
| 37 | details: "Pragmatic — every API real consumers (CSSO, SVGO, lint tools) actually use." | |
| 38 | --- | |
| @@ -0,0 +1,76 @@ | ||
| 1 | # Install | |
| 2 | ||
| 3 | `@stacksjs/ts-css` is a single package. There are no transitive dependencies. | |
| 4 | ||
| 5 | ## Package managers | |
| 6 | ||
| 7 | ::: code-group | |
| 8 | ||
| 9 | ```sh [bun] | |
| 10 | bun add @stacksjs/ts-css | |
| 11 | # or, dev dependency | |
| 12 | bun add --dev @stacksjs/ts-css | |
| 13 | ``` | |
| 14 | ||
| 15 | ```sh [npm] | |
| 16 | npm install @stacksjs/ts-css | |
| 17 | # or, dev dependency | |
| 18 | npm install --save-dev @stacksjs/ts-css | |
| 19 | ``` | |
| 20 | ||
| 21 | ```sh [pnpm] | |
| 22 | pnpm add @stacksjs/ts-css | |
| 23 | # or, dev dependency | |
| 24 | pnpm add --save-dev @stacksjs/ts-css | |
| 25 | ``` | |
| 26 | ||
| 27 | ```sh [yarn] | |
| 28 | yarn add @stacksjs/ts-css | |
| 29 | ``` | |
| 30 | ||
| 31 | ::: | |
| 32 | ||
| 33 | ## Local development with `bun link` | |
| 34 | ||
| 35 | If you're contributing to ts-css and want to test the changes against | |
| 36 | another project locally, register it once with `bun link`: | |
| 37 | ||
| 38 | ```sh | |
| 39 | # inside the ts-css repo | |
| 40 | bun link | |
| 41 | ||
| 42 | # inside the consumer project | |
| 43 | bun link @stacksjs/ts-css | |
| 44 | ``` | |
| 45 | ||
| 46 | That's how [`ts-svg`](https://github.com/stacksjs/ts-svg) consumes ts-css | |
| 47 | during development — its `package.json` carries | |
| 48 | `"@stacksjs/ts-css": "link:@stacksjs/ts-css"`. | |
| 49 | ||
| 50 | ## CLI binary (optional) | |
| 51 | ||
| 52 | The `ts-css` CLI ships with the package and is exposed via Bun's `bin` | |
| 53 | field. After installing, you can run: | |
| 54 | ||
| 55 | ```sh | |
| 56 | bunx ts-css minify input.css | |
| 57 | ``` | |
| 58 | ||
| 59 | For a standalone binary, build from source: | |
| 60 | ||
| 61 | ```sh | |
| 62 | bun run compile # builds bin/ts-css for your platform | |
| 63 | bun run compile:all # cross-compile for darwin/linux/windows | |
| 64 | ``` | |
| 65 | ||
| 66 | ## Verifying the install | |
| 67 | ||
| 68 | ```ts | |
| 69 | import { minify, parse } from '@stacksjs/ts-css' | |
| 70 | ||
| 71 | console.log(minify('.a { color: #aabbcc }').css) | |
| 72 | // → ".a{color:#abc}" | |
| 73 | ``` | |
| 74 | ||
| 75 | If that prints the minified output, you're set. Continue to | |
| 76 | [**Getting started**](./intro.md). | |
| @@ -0,0 +1,99 @@ | ||
| 1 | # Getting started | |
| 2 | ||
| 3 | `@stacksjs/ts-css` is a pure-TypeScript CSS toolkit — a parser, walker, | |
| 4 | generator, selector engine, and minifier — all in one **zero-dependency** | |
| 5 | package. It's a drop-in replacement for the four-library quartet most CSS | |
| 6 | pipelines pull in: | |
| 7 | ||
| 8 | | Replaces | Provides | | |
| 9 | | ------------- | ----------------------------------------------------- | | |
| 10 | | `css-tree` | Tokenizer, parser, walker, generator, clone, `List` | | |
| 11 | | `css-what` | Selector parser & stringifier | | |
| 12 | | `css-select` | `selectAll` / `selectOne` / `is` against any tree | | |
| 13 | | `csso` | Minifier + `syntax.specificity` | | |
| 14 | ||
| 15 | If you've worked with any of those four libraries before, the migration | |
| 16 | is one block of `import` lines (see the [migration guide](./migration.md)). | |
| 17 | ||
| 18 | ## Install | |
| 19 | ||
| 20 | ```bash | |
| 21 | bun add @stacksjs/ts-css | |
| 22 | # or | |
| 23 | npm i @stacksjs/ts-css | |
| 24 | ``` | |
| 25 | ||
| 26 | ## Your first parse | |
| 27 | ||
| 28 | ```ts | |
| 29 | import { generate, parse, walk } from '@stacksjs/ts-css' | |
| 30 | ||
| 31 | const ast = parse('.foo { color: red }') | |
| 32 | ||
| 33 | walk(ast, (node) => { | |
| 34 | if (node.type === 'Declaration') | |
| 35 | console.log(node.property) | |
| 36 | }) | |
| 37 | // → "color" | |
| 38 | ||
| 39 | console.log(generate(ast)) | |
| 40 | // → ".foo{color:red}" | |
| 41 | ``` | |
| 42 | ||
| 43 | That's the full pipeline: parse → walk/transform → generate. | |
| 44 | ||
| 45 | ## Exploring the AST | |
| 46 | ||
| 47 | Pass a `context` to parse a fragment instead of a full stylesheet: | |
| 48 | ||
| 49 | ```ts | |
| 50 | parse('color: red', { context: 'declaration' }) | |
| 51 | parse('color: red; font-size: 12px', { context: 'declarationList' }) | |
| 52 | parse('.a > .b', { context: 'selector' }) | |
| 53 | parse('@media (min-width: 800px)', { context: 'atrulePrelude' }) | |
| 54 | ``` | |
| 55 | ||
| 56 | The full set of contexts is in the [parser API reference](./api/parse.md). | |
| 57 | ||
| 58 | ## Selecting nodes | |
| 59 | ||
| 60 | `ts-css` ships a tree-agnostic selector engine. Provide an `Adapter` for | |
| 61 | your tree and you can run any CSS selector against it: | |
| 62 | ||
| 63 | ```ts | |
| 64 | import { selectAll } from '@stacksjs/ts-css' | |
| 65 | ||
| 66 | const matches = selectAll('p > span.foo:not(.disabled)', root, { | |
| 67 | adapter: myAdapter, | |
| 68 | xmlMode: true, | |
| 69 | }) | |
| 70 | ``` | |
| 71 | ||
| 72 | [Full selector matcher API →](./api/select.md) | |
| 73 | ||
| 74 | ## Minifying | |
| 75 | ||
| 76 | ```ts | |
| 77 | import { minify } from '@stacksjs/ts-css' | |
| 78 | ||
| 79 | minify('.a { margin: 0px; color: #aabbcc }').css | |
| 80 | // → ".a{margin:0;color:#abc}" | |
| 81 | ``` | |
| 82 | ||
| 83 | [Full minifier API →](./api/optimize.md) | |
| 84 | ||
| 85 | ## CLI | |
| 86 | ||
| 87 | ```bash | |
| 88 | ts-css minify input.css > output.css | |
| 89 | ts-css parse input.css | jq | |
| 90 | ts-css format input.css | |
| 91 | ``` | |
| 92 | ||
| 93 | ## Where to next | |
| 94 | ||
| 95 | - [**Parser** API](./api/parse.md) — `parse`, `walk`, `generate`, `clone`, `List` | |
| 96 | - [**Selector parser** API](./api/what.md) — `parse`, `stringify`, `isTraversal` | |
| 97 | - [**Selector matcher** API](./api/select.md) — `selectAll`, `selectOne`, `is` | |
| 98 | - [**Optimizer** API](./api/optimize.md) — `minify`, `minifyBlock`, `specificity` | |
| 99 | - [**Migration** guide](./migration.md) — moving off the four-library setup | |
| @@ -0,0 +1,21 @@ | ||
| 1 | # MIT License | |
| 2 | ||
| 3 | Copyright (c) 2025 Stacks.js | |
| 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | of this software and associated documentation files (the "Software"), to deal | |
| 7 | in the Software without restriction, including without limitation the rights | |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | copies of the Software, and to permit persons to whom the Software is | |
| 10 | furnished to do so, subject to the following conditions: | |
| 11 | ||
| 12 | The above copyright notice and this permission notice shall be included in all | |
| 13 | copies or substantial portions of the Software. | |
| 14 | ||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | SOFTWARE. | |
| @@ -0,0 +1,125 @@ | ||
| 1 | # Migration guide | |
| 2 | ||
| 3 | Migrating from `css-tree` / `css-select` / `css-what` / `csso` to | |
| 4 | `@stacksjs/ts-css` is one block of `import` lines. | |
| 5 | ||
| 6 | ## The diff | |
| 7 | ||
| 8 | ```diff | |
| 9 | - import * as csstree from 'css-tree' | |
| 10 | - import * as csswhat from 'css-what' | |
| 11 | - import { is, selectAll, selectOne } from 'css-select' | |
| 12 | - import * as csso from 'csso' | |
| 13 | ||
| 14 | + import { | |
| 15 | + csstree, // namespace mirror of css-tree | |
| 16 | + cssWhat as csswhat, // namespace mirror of css-what | |
| 17 | + csso, // namespace mirror of csso | |
| 18 | + is, selectAll, selectOne, // top-level css-select APIs | |
| 19 | + } from '@stacksjs/ts-css' | |
| 20 | ``` | |
| 21 | ||
| 22 | That's it. Every method real consumers use is preserved. | |
| 23 | ||
| 24 | ## Per-library compatibility | |
| 25 | ||
| 26 | ### `css-tree` | |
| 27 | ||
| 28 | | Surface | Compat | | |
| 29 | | -------------------------------------------- | ------ | | |
| 30 | | `parse(source, opts)` — all `context` modes | ✅ | | |
| 31 | | `walk(ast, cb)` & `walk(ast, { visit, enter, leave })` | ✅ | | |
| 32 | | `walk.skip` / `walk.stop` | ✅ | | |
| 33 | | `this.atrule` / `this.rule` context inside callbacks | ✅ | | |
| 34 | | `generate(node)` | ✅ | | |
| 35 | | `clone(node)` | ✅ | | |
| 36 | | `List` class — `forEach(data, item, list)`, `insert`, `remove`, `replace`, `prepend`, `append`, `prependList`, `appendList`, `insertList`, `createItem`, `first`, `last`, `isEmpty` | ✅ | | |
| 37 | | Lexer (`syntax.lexer.match*`, `syntax.match*`) | ❌ — see below | | |
| 38 | ||
| 39 | ### `css-what` | |
| 40 | ||
| 41 | | Surface | Compat | | |
| 42 | | -------------------- | ------ | | |
| 43 | | `parse(selector)` | ✅ | | |
| 44 | | `stringify(ast)` | ✅ | | |
| 45 | | `isTraversal(token)` | ✅ | | |
| 46 | | Selector segment shape (incl. `ignoreCase: 'quirks'`) | ✅ | | |
| 47 | ||
| 48 | ### `css-select` | |
| 49 | ||
| 50 | | Surface | Compat | | |
| 51 | | ------------------------------------------- | ------ | | |
| 52 | | `selectAll(sel, root, opts)` | ✅ | | |
| 53 | | `selectOne(sel, root, opts)` | ✅ | | |
| 54 | | `is(node, sel, opts)` | ✅ | | |
| 55 | | `compile(sel, opts)` | ✅ | | |
| 56 | | `Adapter` interface | ✅ — same shape, including state hooks (`isActive`, etc.) | | |
| 57 | | `pseudos` extension | ✅ | | |
| 58 | | `:has`, `:is`, `:not`, `:where`, `:matches` | ✅ | | |
| 59 | | `:nth-child`, `:nth-last-child`, `:nth-of-type`, `:nth-last-of-type`, `:first-child`, `:last-child`, `:only-child`, `:first-of-type`, `:last-of-type`, `:only-of-type` | ✅ | | |
| 60 | | `:empty`, `:root`, `:scope` | ✅ | | |
| 61 | | Stateful pseudos via adapter hooks | ✅ | | |
| 62 | ||
| 63 | ### `csso` | |
| 64 | ||
| 65 | | Surface | Compat | | |
| 66 | | -------------------------------------------- | ------ | | |
| 67 | | `minify(source, opts)` | ✅ | | |
| 68 | | `minifyBlock(source, opts)` | ✅ | | |
| 69 | | `syntax.specificity(node)` | ✅ | | |
| 70 | | Restructuring across rules / selectors | ❌ — declaration-level only | | |
| 71 | ||
| 72 | ## What's intentionally not ported | |
| 73 | ||
| 74 | We dropped two large chunks of the original libs that real consumers | |
| 75 | rarely need: | |
| 76 | ||
| 77 | ### `css-tree`'s lexer (`syntax.lexer`) | |
| 78 | ||
| 79 | `css-tree` ships ~3000 lines of CSS spec data that lets you ask "is the | |
| 80 | string `red 1px solid` a valid `<border>` shorthand?". This is great for | |
| 81 | linters and editors, but downstream consumers like CSSO, SVGO, and most | |
| 82 | formatters never touch it. | |
| 83 | ||
| 84 | The AST that ts-css produces is **rich enough** for you to add lexer | |
| 85 | behaviour back on top — every node carries its source slice and a typed | |
| 86 | discriminated union, so a value-type checker is a straightforward | |
| 87 | external library. | |
| 88 | ||
| 89 | ### CSSO's restructuring pass | |
| 90 | ||
| 91 | `csso` does heroic work to merge declarations across rules: | |
| 92 | ||
| 93 | ```css | |
| 94 | .a { color: red; padding: 4px } | |
| 95 | .b { color: red } | |
| 96 | .c { padding: 4px } | |
| 97 | ``` | |
| 98 | ||
| 99 | → rewritten to share declarations between rules. Beyond gzip's reach | |
| 100 | this saves **single-digit percent**, costs ~1000 lines, and breaks subtly | |
| 101 | in edge cases (specificity, source-order dependencies, vendor-prefix | |
| 102 | collisions). ts-css does declaration-level minification (numbers, | |
| 103 | colors, dedup-within-rule) which catches the bulk of the bytes. | |
| 104 | ||
| 105 | If you have a use case where restructuring matters, file an issue — | |
| 106 | adding it back is straightforward, but we'd rather not ship dead code. | |
| 107 | ||
| 108 | ## Real-world swap: `ts-svg` | |
| 109 | ||
| 110 | [`ts-svg`](https://github.com/stacksjs/ts-svg)'s SVGO port uses ts-css. | |
| 111 | The migration was 7 files and looked like: | |
| 112 | ||
| 113 | ```diff | |
| 114 | - import * as csstree from 'css-tree' | |
| 115 | - import * as csswhat from 'css-what' | |
| 116 | - import { is, selectAll, selectOne } from 'css-select' | |
| 117 | - import { syntax } from 'csso' | |
| 118 | - import * as csso from 'csso' | |
| 119 | ||
| 120 | + import { csstree, cssWhat as csswhat, csso, is, selectAll, selectOne } from '@stacksjs/ts-css' | |
| 121 | + const { syntax } = csso | |
| 122 | ``` | |
| 123 | ||
| 124 | The full SVGO test suite (82 tests) passes after the swap with no | |
| 125 | behaviour changes, and removes 4 transitive dep trees from `node_modules`. | |
| @@ -0,0 +1,8 @@ | ||
| 1 | # Partners | |
| 2 | ||
| 3 | The following companies and organizations are supporting Stacks development through partnerships: | |
| 4 | ||
| 5 | - [JetBrains](https://www.jetbrains.com/) | |
| 6 | - [The Solana Foundation](https://solana.com/) | |
| 7 | ||
| 8 | If you are interested in becoming a partner, please reach out to us. | |
| @@ -0,0 +1,11 @@ | ||
| 1 | # Postcardware | |
| 2 | ||
| 3 | Two things are true: Stacks OSS will always stay open-source, and we do love to receive postcards from wherever Stacks is used! �� | |
| 4 | ||
| 5 | _We also publish them on our website._ | |
| 6 | ||
| 7 | ## Address | |
| 8 | ||
| 9 | Our address in the US is: | |
| 10 | ||
| 11 | Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States �� | |
| @@ -0,0 +1,12 @@ | ||
| 1 | <svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 2 | <g clip-path="url(#clip0_1142_2)"> | |
| 3 | <path fill-rule="evenodd" clip-rule="evenodd" d="M-0.5 -0.5C132.833 -0.5 266.167 -0.5 399.5 -0.5C399.5 132.833 399.5 266.167 399.5 399.5C266.167 399.5 132.833 399.5 -0.5 399.5C-0.5 266.167 -0.5 132.833 -0.5 -0.5Z" fill="#F4F5F7"/> | |
| 4 | <path d="M279.057 114.88L314.118 79.96C320.024 74.0775 315.852 64 307.532 64H204.085C204.085 64 148.635 64 112.145 107.943C110.903 109.443 109.684 110.943 108.419 112.396C106.896 114.177 104.271 117.388 101.247 121.887C80.3893 152.823 76.5224 192.008 89.4591 226.998C95.3182 242.794 104.763 261.614 119.223 274.175L135.605 257.559C146.034 246.989 148.143 231.029 141.44 217.788C137.222 209.445 133.918 198.031 134.457 183.079C134.996 168.127 139.87 153.104 149.385 141.316C159.463 128.824 176.712 116.356 205.421 116.684L279.057 114.927V114.88Z" fill="#0A0ABC"/> | |
| 5 | <path d="M120.606 273.12L85.5453 308.039C79.6394 313.922 83.8112 324 92.131 324H195.578C195.578 324 251.028 324 287.518 280.057C288.76 278.557 289.979 277.057 291.244 275.604C292.768 273.823 295.393 270.612 298.416 266.112C319.274 235.177 323.141 195.991 310.204 161.001C304.345 145.205 294.901 126.386 280.44 113.824L264.059 130.44C253.63 141.01 251.52 156.97 258.223 170.212C262.442 178.555 265.746 189.968 265.207 204.921C264.668 219.873 259.793 234.895 250.278 246.684C240.201 259.175 222.951 271.643 194.242 271.315L120.606 273.073V273.12Z" fill="#0A0ABC"/> | |
| 6 | </g> | |
| 7 | <defs> | |
| 8 | <clipPath id="clip0_1142_2"> | |
| 9 | <rect width="400" height="400" fill="white"/> | |
| 10 | </clipPath> | |
| 11 | </defs> | |
| 12 | </svg> | |
| @@ -0,0 +1,12 @@ | ||
| 1 | <svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 2 | <g clip-path="url(#clip0_1142_2)"> | |
| 3 | <path fill-rule="evenodd" clip-rule="evenodd" d="M-0.5 -0.5C132.833 -0.5 266.167 -0.5 399.5 -0.5C399.5 132.833 399.5 266.167 399.5 399.5C266.167 399.5 132.833 399.5 -0.5 399.5C-0.5 266.167 -0.5 132.833 -0.5 -0.5Z" fill="#F4F5F7"/> | |
| 4 | <path d="M279.057 114.88L314.118 79.96C320.024 74.0775 315.852 64 307.532 64H204.085C204.085 64 148.635 64 112.145 107.943C110.903 109.443 109.684 110.943 108.419 112.396C106.896 114.177 104.271 117.388 101.247 121.887C80.3893 152.823 76.5224 192.008 89.4591 226.998C95.3182 242.794 104.763 261.614 119.223 274.175L135.605 257.559C146.034 246.989 148.143 231.029 141.44 217.788C137.222 209.445 133.918 198.031 134.457 183.079C134.996 168.127 139.87 153.104 149.385 141.316C159.463 128.824 176.712 116.356 205.421 116.684L279.057 114.927V114.88Z" fill="#0A0ABC"/> | |
| 5 | <path d="M120.606 273.12L85.5453 308.039C79.6394 313.922 83.8112 324 92.131 324H195.578C195.578 324 251.028 324 287.518 280.057C288.76 278.557 289.979 277.057 291.244 275.604C292.768 273.823 295.393 270.612 298.416 266.112C319.274 235.177 323.141 195.991 310.204 161.001C304.345 145.205 294.901 126.386 280.44 113.824L264.059 130.44C253.63 141.01 251.52 156.97 258.223 170.212C262.442 178.555 265.746 189.968 265.207 204.921C264.668 219.873 259.793 234.895 250.278 246.684C240.201 259.175 222.951 271.643 194.242 271.315L120.606 273.073V273.12Z" fill="#0A0ABC"/> | |
| 6 | </g> | |
| 7 | <defs> | |
| 8 | <clipPath id="clip0_1142_2"> | |
| 9 | <rect width="400" height="400" fill="white"/> | |
| 10 | </clipPath> | |
| 11 | </defs> | |
| 12 | </svg> | |
| @@ -0,0 +1,4 @@ | ||
| 1 | <svg width="236" height="260" viewBox="0 0 236 260" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 2 | <path d="M197.057 50.8799L232.118 15.96C238.024 10.0775 233.852 0 225.532 0H122.085C122.085 0 66.6354 0 30.1453 43.9428C28.9032 45.4427 27.6844 46.9426 26.4189 48.3956C24.8955 50.1768 22.2707 53.3876 19.2474 57.8873C-1.61074 88.823 -5.47763 128.008 7.45912 162.998C13.3182 178.794 22.763 197.614 37.2231 210.175L53.6049 193.559C64.034 182.989 66.1432 167.029 59.4405 153.788C55.222 145.445 51.9175 134.031 52.4566 119.079C52.9956 104.127 57.8703 89.1042 67.3854 77.3158C77.4629 64.8244 94.7119 52.3564 123.421 52.6845L197.057 50.9268V50.8799Z" fill="#0A0ABC"/> | |
| 3 | <path d="M38.6059 209.12L3.54534 244.039C-2.36057 249.922 1.8112 260 10.131 260H113.578C113.578 260 169.028 260 205.518 216.057C206.76 214.557 207.979 213.057 209.244 211.604C210.768 209.823 213.393 206.612 216.416 202.112C237.274 171.177 241.141 131.991 228.204 97.0012C222.345 81.2052 212.901 62.386 198.44 49.8242L182.059 66.4404C171.63 77.0101 169.52 92.9702 176.223 106.212C180.442 114.555 183.746 125.968 183.207 140.921C182.668 155.873 177.793 170.895 168.278 182.684C158.201 195.175 140.951 207.643 112.242 207.315L38.6059 209.073V209.12Z" fill="#0A0ABC"/> | |
| 4 | </svg> | |
| @@ -0,0 +1,4 @@ | ||
| 1 | <svg width="236" height="260" viewBox="0 0 236 260" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 2 | <path d="M197.057 50.8799L232.118 15.96C238.024 10.0775 233.852 0 225.532 0H122.085C122.085 0 66.6354 0 30.1453 43.9428C28.9032 45.4427 27.6844 46.9426 26.4189 48.3956C24.8955 50.1768 22.2707 53.3876 19.2474 57.8873C-1.61074 88.823 -5.47763 128.008 7.45912 162.998C13.3182 178.794 22.763 197.614 37.2231 210.175L53.6049 193.559C64.034 182.989 66.1432 167.029 59.4405 153.788C55.222 145.445 51.9175 134.031 52.4566 119.079C52.9956 104.127 57.8703 89.1042 67.3854 77.3158C77.4629 64.8244 94.7119 52.3564 123.421 52.6845L197.057 50.9268V50.8799Z" fill="#0A0ABC"/> | |
| 3 | <path d="M38.6059 209.12L3.54534 244.039C-2.36057 249.922 1.8112 260 10.131 260H113.578C113.578 260 169.028 260 205.518 216.057C206.76 214.557 207.979 213.057 209.244 211.604C210.768 209.823 213.393 206.612 216.416 202.112C237.274 171.177 241.141 131.991 228.204 97.0012C222.345 81.2052 212.901 62.386 198.44 49.8242L182.059 66.4404C171.63 77.0101 169.52 92.9702 176.223 106.212C180.442 114.555 183.746 125.968 183.207 140.921C182.668 155.873 177.793 170.895 168.278 182.684C158.201 195.175 140.951 207.643 112.242 207.315L38.6059 209.073V209.12Z" fill="#0A0ABC"/> | |
| 4 | </svg> | |
| @@ -0,0 +1,4 @@ | ||
| 1 | <svg width="236" height="260" viewBox="0 0 236 260" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 2 | <path d="M197.057 50.8799L232.118 15.96C238.024 10.0775 233.852 0 225.532 0H122.085C122.085 0 66.6354 0 30.1453 43.9428C28.9032 45.4427 27.6844 46.9426 26.4189 48.3956C24.8955 50.1768 22.2707 53.3876 19.2474 57.8873C-1.61074 88.823 -5.47763 128.008 7.45912 162.998C13.3182 178.794 22.763 197.614 37.2231 210.175L53.6049 193.559C64.034 182.989 66.1432 167.029 59.4405 153.788C55.222 145.445 51.9175 134.031 52.4566 119.079C52.9956 104.127 57.8703 89.1042 67.3854 77.3158C77.4629 64.8244 94.7119 52.3564 123.421 52.6845L197.057 50.9268V50.8799Z" fill="#fff"/> | |
| 3 | <path d="M38.6059 209.12L3.54534 244.039C-2.36057 249.922 1.8112 260 10.131 260H113.578C113.578 260 169.028 260 205.518 216.057C206.76 214.557 207.979 213.057 209.244 211.604C210.768 209.823 213.393 206.612 216.416 202.112C237.274 171.177 241.141 131.991 228.204 97.0012C222.345 81.2052 212.901 62.386 198.44 49.8242L182.059 66.4404C171.63 77.0101 169.52 92.9702 176.223 106.212C180.442 114.555 183.746 125.968 183.207 140.921C182.668 155.873 177.793 170.895 168.278 182.684C158.201 195.175 140.951 207.643 112.242 207.315L38.6059 209.073V209.12Z" fill="#fff"/> | |
| 4 | </svg> | |
Binary file. Nothing to show line by line.
Binary file. Nothing to show line by line.
| @@ -0,0 +1,7 @@ | ||
| 1 | <svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 2 | <g clip-path="url(#clip0_1142_2)"> | |
| 3 | <path fill-rule="evenodd" clip-rule="evenodd" d="M-0.5 -0.5C132.833 -0.5 266.167 -0.5 399.5 -0.5C399.5 132.833 399.5 266.167 399.5 399.5C266.167 399.5 132.833 399.5 -0.5 399.5C-0.5 266.167 -0.5 132.833 -0.5 -0.5Z" fill="#F4F5F7"/> | |
| 4 | <path d="M279.057 114.88L314.118 79.96C320.024 74.0775 315.852 64 307.532 64H204.085C204.085 64 148.635 64 112.145 107.943C110.903 109.443 109.684 110.943 108.419 112.396C106.896 114.177 104.271 117.388 101.247 121.887C80.3893 152.823 76.5224 192.008 89.4591 226.998C95.3182 242.794 104.763 261.614 119.223 274.175L135.605 257.559C146.034 246.989 148.143 231.029 141.44 217.788C137.222 209.445 133.918 198.031 134.457 183.079C134.996 168.127 139.87 153.104 149.385 141.316C159.463 128.824 176.712 116.356 205.421 116.684L279.057 114.927V114.88Z" fill="#0A0ABC"/> | |
| 5 | <path d="M120.606 273.12L85.5453 308.039C79.6394 313.922 83.8112 324 92.131 324H195.578C195.578 324 251.028 324 287.518 280.057C288.76 278.557 289.979 277.057 291.244 275.604C292.768 273.823 295.393 270.612 298.416 266.112C319.274 235.177 323.141 195.991 310.204 161.001C304.345 145.205 294.901 126.386 280.44 113.824L264.059 130.44C253.63 141.01 251.52 156.97 258.223 170.212C262.442 178.555 265.746 189.968 265.207 204.921C264.668 219.873 259.793 234.895 250.278 246.684C240.201 259.175 222.951 271.643 194.242 271.315L120.606 273.073V273.12Z" fill="#0A0ABC"/> | |
| 6 | </g> | |
| 7 | </svg> | |
Binary file. Nothing to show line by line.
| @@ -0,0 +1,101 @@ | ||
| 1 | # Showcase | |
| 2 | ||
| 3 | Here are all the cool projects that people have built using `rpx`. | |
| 4 | ||
| 5 | ## Community Projects | |
| 6 | ||
| 7 | - _Yours could be here!_ | |
| 8 | ||
| 9 | If you authored a project you’d like to showcase, please share it with us in any way _(on Discord, Social Media, or via a PR, etc.)_, and we’ll add it here. | |
| 10 | ||
| 11 | ## First-Party | |
| 12 | ||
| 13 | The whole Stacks ecosystem is built using `rpx`. Here are some of the projects utilizing it: | |
| 14 | ||
| 15 | - [`rpx`](https://github.com/stacksjs/rpx) _(A modern, fast reverse proxy)_ | |
| 16 | - [`tlsx`](https://github.com/stacksjs/tlsx) _(A TLS library with automation. HTTPS by default through a light-weight library. Similar to mkcert.)_ | |
| 17 | - [`dtsx`](https://github.com/stacksjs/dtsx) _(Extremely fast & configurable DTS emitter)_ | |
| 18 | - [`bunfig`](https://github.com/stacksjs/bunfig) _(Lightweight & smart Bun configuration loader)_ | |
| 19 | - [`bun-plugin-auto-imports`](https://github.com/stacksjs/bun-plugin-auto-imports) _(Auto Imports support for Bun)_ | |
| 20 | - [`ts-collect`](https://github.com/stacksjs/ts-collect) _(Lightweight & powerful Laravel-like Collections written for TypeScript)_ | |
| 21 | - [`ts-spreadsheets`](https://github.com/stacksjs/ts-spreadsheets) _(Easily generate spreadsheets, like CSVs and Excel files)_ | |
| 22 | - [`localtunnels`](https://github.com/stacksjs/localtunnels) _(A simple and smart tunneling alternative. Without or with self-hosting)_ | |
| 23 | - [`vite-plugin-local`](https://github.com/stacksjs/vite-plugin-local) _(A Vite plugin for pretty development URLs and HTTPS. Zero config, zero setup)_ | |
| 24 | ||
| 25 | ### Stacks Framework | |
| 26 | ||
| 27 | As mentioned before, the whole core of the Stacks Framework is built using `dtsx`. Here are some of those Stacks core packages: | |
| 28 | ||
| 29 | - [`@stacksjs/actions`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/actions) | |
| 30 | - [`@stacksjs/ai`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/ai) | |
| 31 | - [`@stacksjs/alias`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/alias) | |
| 32 | - [`@stacksjs/analytics`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/analytics) | |
| 33 | - [`@stacksjs/api`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/api) | |
| 34 | - [`@stacksjs/arrays`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/arrays) | |
| 35 | - [`@stacksjs/auth`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/auth) | |
| 36 | - [`@stacksjs/browser`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/browser) | |
| 37 | - [`@stacksjs/buddy`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/buddy) | |
| 38 | - [`@stacksjs/build`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/build) | |
| 39 | - [`@stacksjs/cache`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cache) | |
| 40 | - [`@stacksjs/calendar`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/calendar) | |
| 41 | - [`@stacksjs/chat`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/chat) | |
| 42 | - [`@stacksjs/cli`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cli) | |
| 43 | - [`@stacksjs/cloud`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cloud) | |
| 44 | - [`@stacksjs/collections`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/collections) | |
| 45 | - [`@stacksjs/commerce`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/commerce) | |
| 46 | - [`@stacksjs/components`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/components) | |
| 47 | - [`@stacksjs/config`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/config) | |
| 48 | - [`@stacksjs/cron`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/cron) | |
| 49 | - [`@stacksjs/database`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/database) | |
| 50 | - [`@stacksjs/datetime`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/datetime) | |
| 51 | - [`@stacksjs/desktop`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/desktop) | |
| 52 | - [`@stacksjs/development`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/development) | |
| 53 | - [`@stacksjs/dns`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/dns) | |
| 54 | - [`@stacksjs/docs`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/docs) | |
| 55 | - [`@stacksjs/email`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/email) | |
| 56 | - [`@stacksjs/enums`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/enums) | |
| 57 | - [`@stacksjs/env`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/env) | |
| 58 | - [`@stacksjs/error-handling`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/error-handling) | |
| 59 | - [`@stacksjs/events`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/events) | |
| 60 | - [`@stacksjs/faker`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/faker) | |
| 61 | - [`@stacksjs/git`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/git) | |
| 62 | - [`@stacksjs/health`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/health) | |
| 63 | - [`@stacksjs/http`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/http) | |
| 64 | - [`@stacksjs/lint`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/lint) | |
| 65 | - [`@stacksjs/logging`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/logging) | |
| 66 | - [`@stacksjs/notifications`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/notifications) | |
| 67 | - [`@stacksjs/objects`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/objects) | |
| 68 | - [`@stacksjs/orm`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/orm) | |
| 69 | - [`@stacksjs/path`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/path) | |
| 70 | - [`@stacksjs/payments`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/payments) | |
| 71 | - [`@stacksjs/plugins`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/plugins) | |
| 72 | - [`@stacksjs/push`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/push) | |
| 73 | - [`@stacksjs/query-builder`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/query-builder) | |
| 74 | - [`@stacksjs/queue`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/queue) | |
| 75 | - [`@stacksjs/raycast`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/raycast) | |
| 76 | - [`@stacksjs/realtime`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/realtime) | |
| 77 | - [`@stacksjs/registry`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/registry) | |
| 78 | - [`@stacksjs/repl`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/repl) | |
| 79 | - [`@stacksjs/router`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/router) | |
| 80 | - [`@stacksjs/scheduler`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/scheduler) | |
| 81 | - [`@stacksjs/search-engine`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/search-engine) | |
| 82 | - [`@stacksjs/security`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/security) | |
| 83 | - [`@stacksjs/server`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/server) | |
| 84 | - [`@stacksjs/shell`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/shell) | |
| 85 | - [`@stacksjs/slug`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/slug) | |
| 86 | - [`@stacksjs/sms`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/sms) | |
| 87 | - [`@stacksjs/storage`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/storage) | |
| 88 | - [`@stacksjs/strings`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/strings) | |
| 89 | - [`@stacksjs/testing`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/testing) | |
| 90 | - [`@stacksjs/tinker`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/tinker) | |
| 91 | - [`@stacksjs/tunnel`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/tunnel) | |
| 92 | - [`@stacksjs/types`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/types) | |
| 93 | - [`@stacksjs/ui`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/ui) | |
| 94 | - [`@stacksjs/utils`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/utils) | |
| 95 | - [`@stacksjs/validation`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/validation) | |
| 96 | - [`@stacksjs/vite-config`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/vite-config) | |
| 97 | - [`@stacksjs/vite-plugin`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/vite-plugin) | |
| 98 | - [`@stacksjs/whois`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/whois) | |
| 99 | - [`@stacksjs/x-ray`](https://github.com/stacksjs/stacks/tree/main/storage/framework/core/x-ray) | |
| 100 | ||
| 101 | We try to keep the list up-to-date, but it's possible that some packages are missing. If you find any, please let us know! | |
| @@ -0,0 +1,131 @@ | ||
| 1 | # Become a Stacks.js Sponsor | |
| 2 | ||
| 3 | Stacks.js is an MIT licensed open source project and completely free to use. The tremendous amount of effort needed to maintain such a large ecosystem and develop new features for the project is only made sustainable thanks to the generous financial backing of our sponsors. | |
| 4 | ||
| 5 | ## How to Sponsor | |
| 6 | ||
| 7 | Sponsorships can be done via [GitHub Sponsors](https://github.com/sponsors/chrisbbreuer) or [OpenCollective](https://opencollective.com/stacksjs). Invoices can be obtained via GitHub's payment system. Both monthly-recurring sponsorships and one-time donations are accepted. Recurring sponsorships are entitled to logo placements as specified in Sponsorship Tiers. | |
| 8 | ||
| 9 | If you have questions regarding tiers, payment logistics, or sponsor exposure data, please reach out to <sponsor@stacksjs.org>. | |
| 10 | ||
| 11 | The following companies and organizations are supporting Stacks development through sponsorships: | |
| 12 | ||
| 13 | ## Sponsoring Stacks as a Business | |
| 14 | ||
| 15 | Sponsoring Stacks gives you developer exposure around the world through our website and GitHub project READMEs. This not only directly generates leads, but also improves your brand recognition as a business that cares about Open Source. This is an intangible but extremely important asset for companies building products for developers, as it improves your conversion rate. | |
| 16 | ||
| 17 | If you are using Stacks to build a revenue-generating product, it makes business sense to sponsor Stacks's development: it ensures the project that your product relies on stays healthy and actively maintained. The exposure and positive brand image in the Stacks community also makes it easier to attract and recruit Stacks developers. | |
| 18 | ||
| 19 | If you are building a product where your target customers are developers, you will gain high quality traffic through the sponsorship exposure, since all our visitors are developers. The sponsorship also builds brand recognition and improves conversion. | |
| 20 | ||
| 21 | ## Sponsoring Stacks as an Individual | |
| 22 | ||
| 23 | If you are an individual user and have enjoyed the productivity of using Stacks, consider donating as a sign of appreciation - like buying us coffee once in a while. Many of our team members accept sponsorships and donations via GitHub Sponsors. Look for the "Sponsor" button on each team member's profile on our [team page](https://stacksjs-docs.netlify.app/team). | |
| 24 | ||
| 25 | You can also try to convince your employer to sponsor Stacks as a business. This may not be easy, but business sponsorships typically make a much larger impact on the sustainability of OSS projects than individual donations, so you will help us much more if you succeed. | |
| 26 | ||
| 27 | ## Tier Benefits | |
| 28 | ||
| 29 | - ***Global Special Sponsor*** | |
| 30 | - Limited to one sponsor globally. Currently vacant. [Get in touch!](mailto:sponsors@stacksjs.org) | |
| 31 | - *(Exclusive)* Above the fold logo placement on the front page of [stacksjs.org](https://stacksjs.org). | |
| 32 | - *(Exclusive)* Special shoutout and regular retweets of major product launches via Stacks's official social accounts. | |
| 33 | - Most prominent logo placement in all locations from tiers below. | |
| 34 | ||
| 35 | ___ | |
| 36 | ||
| 37 | - **Diamond (USD$2,500/mo):** | |
| 38 | - Prominent logo placement on the front page of [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). | |
| 39 | - Prominent logo placement in sidebar of all content pages. | |
| 40 | - Prominent logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 41 | ||
| 42 | ___ | |
| 43 | ||
| 44 | - **Platinum (USD$1,000/mo):** | |
| 45 | - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). | |
| 46 | - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 47 | ||
| 48 | ___ | |
| 49 | ||
| 50 | - **Double Gold (USD$500/mo):** | |
| 51 | - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). | |
| 52 | - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 53 | ||
| 54 | ___ | |
| 55 | ||
| 56 | - **Gold (USD$250/mo):** | |
| 57 | - Large logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). | |
| 58 | - Large logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 59 | ||
| 60 | ___ | |
| 61 | ||
| 62 | - **Silver (USD$100/mo):** | |
| 63 | - Medium logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). | |
| 64 | - Medium logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 65 | - Medium logo placement in the `BACKERS.md` file of>70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 66 | ||
| 67 | ___ | |
| 68 | ||
| 69 | - **Bronze (50/mo):** | |
| 70 | - Small logo placement on the front page of [stacksjs.org](https://stacksjs.org), [stacksjs.org](https://stacksjs.org), [aax](https://ts-aax.netlify.app/), [audiox](https://stacks-audiox.netlify.app/), [buddy.sh](https://buddy.sh/), [bun-git-hooks](https://bun-git-hooks.netlify.app/), [bun-plugin-auto-imports](https://github.com/stacksjs/bun-plugin-auto-imports), [bun-plugin-dotenvx](https://bun-plugin-dotenvx.netlify.app/), [bun-plugin-dtsx](https://dtsx.netlify.app/bun-plugin), [bun-plugin-unocss](https://github.com/stacksjs/bun-plugin-unocss), [bunfig](https://bunfig.netlify.app/), [clarity](https://stacks-clarity.netlify.app/), [dnsx](https://dnsx.netlify.app/), [docs.stacksjs.org](https://docs.stacksjs.org), [dtsx](https://dtsx.netlify.app/), [httx](https://httx.netflix.app/), [imgx](https://jpgx.netlify.app/), [jpgx](https://jpgx.netlify.app/), [localtunnels.sh](https://localtunnel.sh/), [pngx](https://pngx.netlify.app/), [post](https://the-post.netlify.app/), [qrx](https://ts-quick-reaction.netlify.app/), [reverse-proxy.sh](https://reverse-proxy.sh/), [tlsx.sh](https://tlsx.sh/), [ts-avif](https://github.com/stacksjs/ts-avif), [ts-cache](https://ts-cache.netlify.app/), [ts-clone](https://github.com/stacksjs/ts-clone), [ts-collect](https://ts-collect.netlify.app/), [ts-countries](https://ts-countries.netlify.app/), [ts-gif](https://github.com/stacksjs/ts-gif), [ts-maps](https://ts-maps.netlify.app/), [ts-ndarray](https://github.com/stacksjs/ts-ndarray), [ts-punycode](https://ts-punycode.netlify.app/), [ts-rate-limiter](https://ts-rate-limiter.netlify.app/), [ts-security](https://ts-security.netlify.app/), [ts-spreadsheets](https://ts-spreadsheets.netlify.app/), [ts-svg](https://github.com/stacksjs/ts-svg), [ts-vat](https://ts-vat.netlify.app/), [ts-webp](https://github.com/stacksjs/ts-webp), [vidx](https://stacks-vidx.netlify.app/), [vite-plugin-dotenvx](https://vite-plugin-dotenvx.netlify.app/), [vite-plugin-layouts](https://vite-plugin-layouts.netlify.app/), [vite-plugin-local](https://vite-plugin-local.netlify.app/), [vite-plugin-tauri](https://github.com/stacksjs/vite-plugin-tauri). | |
| 71 | - Small logo placement in the README of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 72 | - Small logo placement in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 73 | ||
| 74 | ___ | |
| 75 | ||
| 76 | - **Generous Backer (USD$25/mo):** | |
| 77 | - Private Discord channel invite | |
| 78 | - Free Personal Account for Stacks Dashboard | |
| 79 | - Social media follow | |
| 80 | - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 81 | ||
| 82 | ___ | |
| 83 | ||
| 84 | - **Sponsorware Tier (USD$10/mo):** | |
| 85 | - Get access to all current sponsorware projects *(i.e. the Stacks DynamoDB ORM driver)* | |
| 86 | - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 87 | ||
| 88 | ___ | |
| 89 | ||
| 90 | - **Individual Backer (USD$5/mo):** | |
| 91 | - Name listed in the `BACKERS.md` file of >70x `stacksjs/core` repos, `stacksjs/stacks`, `aax`, `audiox`, `buddy.sh`, `bun-git-hooks`, `bun-plugin-auto-imports`, `bun-plugin-dotenvx`, `bun-plugin-dtsx`, `bun-plugin-unocss`, `bunfig`, `clarity`, `dnsx`, `docs.stacksjs.org`, `dtsx`, `httx`, `imgx`, `jpgx`, `localtunnels.sh`, `pngx`, `post`, `qrx`, `reverse-proxy.sh`, `tlsx.sh`, `ts-avif`, `ts-cache`, `ts-clone`, `ts-collect`, `ts-countries`, `ts-gif`, `ts-maps`, `ts-ndarray`, `ts-punycode`, `ts-rate-limiter`, `ts-security`, `ts-spreadsheets`, `ts-svg`, `ts-vat`, `ts-webp`, `vidx`, `vite-plugin-dotenvx`, `vite-plugin-layouts`, `vite-plugin-local`, `vite-plugin-tauri` | |
| 92 | ||
| 93 | ## Current Sponsors | |
| 94 | ||
| 95 | ### Global Special Sponsor | |
| 96 | ||
| 97 | - Vacant | |
| 98 | ||
| 99 | ### Diamond Sponsors | |
| 100 | ||
| 101 | - Vacant | |
| 102 | ||
| 103 | ### Platinum Sponsors | |
| 104 | ||
| 105 | - Vacant | |
| 106 | ||
| 107 | ### Gold Sponsors | |
| 108 | ||
| 109 | - Vacant | |
| 110 | ||
| 111 | ### Silver Sponsors | |
| 112 | ||
| 113 | - Vacant | |
| 114 | ||
| 115 | ### Bronze Sponsors | |
| 116 | ||
| 117 | - Vacant | |
| 118 | ||
| 119 | ### Generous Backers | |
| 120 | ||
| 121 | - Vacant | |
| 122 | ||
| 123 | ### Individual Backers | |
| 124 | ||
| 125 | - Vacant | |
| 126 | ||
| 127 | ___ | |
| 128 | ||
| 129 | ##### Thanks to Vue.js for the inspiration of this sponsorship page | |
| 130 | ||
| 131 | *You can find their sponsorship page [here](https://vuejs.org/sponsor/).* | |
| @@ -0,0 +1,36 @@ | ||
| 1 | --- | |
| 2 | layout: page | |
| 3 | title: Meet the Team | |
| 4 | description: A team of incredible people. | |
| 5 | sidebar: false | |
| 6 | --- | |
| 7 | ||
| 8 | <script setup> | |
| 9 | import { | |
| 10 | VPTeamPage, | |
| 11 | VPTeamPageTitle, | |
| 12 | VPTeamPageSection, | |
| 13 | VPTeamMembers | |
| 14 | } from 'vitepress/theme' | |
| 15 | import { core, emeriti } from './_data/team' | |
| 16 | </script> | |
| 17 | ||
| 18 | <VPTeamPage> | |
| 19 | <VPTeamPageTitle> | |
| 20 | <template #title>Meet the Team</template> | |
| 21 | <template #lead> | |
| 22 | A team of incredible talent & ambition. | |
| 23 | </template> | |
| 24 | </VPTeamPageTitle> | |
| 25 | <VPTeamMembers :members="core" /> | |
| 26 | <!-- <VPTeamPageSection> | |
| 27 | <template #title>Team Emeriti</template> | |
| 28 | <template #lead> | |
| 29 | Here we honor some no-longer-active team members who have made valuable | |
| 30 | contributions in the past. | |
| 31 | </template> | |
| 32 | <template #members> | |
| 33 | <VPTeamMembers size="small" :members="emeriti" /> | |
| 34 | </template> | |
| 35 | </VPTeamPageSection> --> | |
| 36 | </VPTeamPage> | |
| @@ -0,0 +1,162 @@ | ||
| 1 | # Usage | |
| 2 | ||
| 3 | `@stacksjs/ts-css` is a library first, with a small CLI on top. Most | |
| 4 | consumers import it as a library; the CLI is a convenience wrapper for | |
| 5 | ad-hoc minification and AST inspection. | |
| 6 | ||
| 7 | ## Library | |
| 8 | ||
| 9 | ### Full pipeline: parse → walk → generate | |
| 10 | ||
| 11 | ```ts | |
| 12 | import { generate, parse, walk } from '@stacksjs/ts-css' | |
| 13 | ||
| 14 | const ast = parse(` | |
| 15 | .foo { | |
| 16 | color: red; | |
| 17 | background: url("/assets/bg.png"); | |
| 18 | } | |
| 19 | `) | |
| 20 | ||
| 21 | walk(ast, { | |
| 22 | visit: 'Url', | |
| 23 | enter(url) { | |
| 24 | console.log('asset:', url.value) | |
| 25 | }, | |
| 26 | }) | |
| 27 | ||
| 28 | console.log(generate(ast)) | |
| 29 | ``` | |
| 30 | ||
| 31 | ### Selector matching against your tree | |
| 32 | ||
| 33 | ```ts | |
| 34 | import { selectAll } from '@stacksjs/ts-css' | |
| 35 | ||
| 36 | interface MyEl { | |
| 37 | type: 'el' | |
| 38 | name: string | |
| 39 | attrs: Record<string, string> | |
| 40 | children: MyEl[] | |
| 41 | parent: MyEl | null | |
| 42 | } | |
| 43 | ||
| 44 | const adapter = { | |
| 45 | isTag: (n: any): n is MyEl => n?.type === 'el', | |
| 46 | getName: (e: MyEl) => e.name, | |
| 47 | getParent: (e: MyEl) => e.parent, | |
| 48 | getChildren: (e: MyEl) => e.children, | |
| 49 | getSiblings: (e: MyEl) => e.parent?.children ?? [e], | |
| 50 | getAttributeValue: (e: MyEl, n: string) => e.attrs[n], | |
| 51 | hasAttrib: (e: MyEl, n: string) => n in e.attrs, | |
| 52 | getText: () => '', | |
| 53 | removeSubsets: (xs: any[]) => xs, | |
| 54 | existsOne: (test: (e: MyEl) => boolean, xs: any[]): boolean => | |
| 55 | xs.some(x => adapter.isTag(x) && (test(x) || adapter.existsOne(test, x.children))), | |
| 56 | findAll: (test: (e: MyEl) => boolean, xs: any[]): MyEl[] => { | |
| 57 | const out: MyEl[] = [] | |
| 58 | for (const x of xs) { | |
| 59 | if (adapter.isTag(x)) { | |
| 60 | if (test(x)) out.push(x) | |
| 61 | out.push(...adapter.findAll(test, x.children)) | |
| 62 | } | |
| 63 | } | |
| 64 | return out | |
| 65 | }, | |
| 66 | findOne: (test: (e: MyEl) => boolean, xs: any[]): MyEl | null => { | |
| 67 | for (const x of xs) { | |
| 68 | if (adapter.isTag(x)) { | |
| 69 | if (test(x)) return x | |
| 70 | const r = adapter.findOne(test, x.children) | |
| 71 | if (r) return r | |
| 72 | } | |
| 73 | } | |
| 74 | return null | |
| 75 | }, | |
| 76 | } | |
| 77 | ||
| 78 | const matches = selectAll('p > .foo:not(.disabled)', root, { | |
| 79 | adapter, | |
| 80 | xmlMode: true, | |
| 81 | }) | |
| 82 | ``` | |
| 83 | ||
| 84 | ### Minify a stylesheet | |
| 85 | ||
| 86 | ```ts | |
| 87 | import { minify } from '@stacksjs/ts-css' | |
| 88 | ||
| 89 | const { css } = minify(input, { | |
| 90 | comments: 'exclamation', // keep `/*!*/` comments | |
| 91 | }) | |
| 92 | ``` | |
| 93 | ||
| 94 | ### Compute selector specificity | |
| 95 | ||
| 96 | ```ts | |
| 97 | import { parse, syntax } from '@stacksjs/ts-css' | |
| 98 | ||
| 99 | const sel = parse('#a.b div', { context: 'selector' }) | |
| 100 | syntax.specificity(sel) // [1, 1, 1] | |
| 101 | ``` | |
| 102 | ||
| 103 | ## CLI | |
| 104 | ||
| 105 | ```sh | |
| 106 | ts-css <command> <file> | |
| 107 | ``` | |
| 108 | ||
| 109 | | Command | Output | | |
| 110 | | --------------- | ---------------------------------------------------- | | |
| 111 | | `minify <file>` | Minified CSS on stdout. | | |
| 112 | | `parse <file>` | The parsed AST as JSON on stdout. | | |
| 113 | | `format <file>` | Round-trip through parser/generator (deterministic). | | |
| 114 | | `version` | Print the installed CLI version. | | |
| 115 | ||
| 116 | ### Examples | |
| 117 | ||
| 118 | ```sh | |
| 119 | # Minify a stylesheet, write to disk | |
| 120 | ts-css minify src/app.css > dist/app.css | |
| 121 | ||
| 122 | # Inspect the AST of a snippet | |
| 123 | echo '.a { color: red }' > /tmp/snip.css | |
| 124 | ts-css parse /tmp/snip.css | jq '.children[0]' | |
| 125 | ||
| 126 | # Format-only round trip | |
| 127 | ts-css format src/app.css | |
| 128 | ``` | |
| 129 | ||
| 130 | ### Flags | |
| 131 | ||
| 132 | `ts-css minify`: | |
| 133 | ||
| 134 | | Flag | Description | | |
| 135 | | ---------------- | ------------------------------------ | | |
| 136 | | `--no-comments` | Strip `/*!*/` comments too. | | |
| 137 | | `--block` | Treat input as a `style="…"` body. | | |
| 138 | ||
| 139 | `ts-css parse`: | |
| 140 | ||
| 141 | | Flag | Description | | |
| 142 | | -------------- | --------------------------------------------------- | | |
| 143 | | `--positions` | Track source-location info on every AST node. | | |
| 144 | ||
| 145 | ## Configuration | |
| 146 | ||
| 147 | You can drop a `ts-css.config.ts` at the project root for defaults: | |
| 148 | ||
| 149 | ```ts | |
| 150 | // ts-css.config.ts | |
| 151 | import type { CSSOptions } from '@stacksjs/ts-css' | |
| 152 | ||
| 153 | const config: CSSOptions = { | |
| 154 | floatPrecision: 3, | |
| 155 | verbose: false, | |
| 156 | } | |
| 157 | ||
| 158 | export default config | |
| 159 | ``` | |
| 160 | ||
| 161 | `ts-css` loads it via [`bunfig`](https://github.com/stacksjs/bunfig) — the | |
| 162 | same loader used across the Stacks toolchain. | |
| @@ -0,0 +1,123 @@ | ||
| 1 | { | |
| 2 | "name": "@stacksjs/ts-css", | |
| 3 | "type": "module", | |
| 4 | "version": "0.1.4", | |
| 5 | "description": "Pure-TypeScript CSS toolkit for Bun/Node \u2014 parser, walker, generator, selector engine, and minifier. Zero runtime deps.", | |
| 6 | "author": "Chris Breuer <chris@stacksjs.org>", | |
| 7 | "license": "MIT", | |
| 8 | "homepage": "https://github.com/cwcss/crosswind/tree/main/packages/toolkit#readme", | |
| 9 | "repository": { | |
| 10 | "type": "git", | |
| 11 | "url": "git+https://github.com/cwcss/crosswind.git", | |
| 12 | "directory": "packages/toolkit" | |
| 13 | }, | |
| 14 | "bugs": { | |
| 15 | "url": "https://github.com/cwcss/crosswind/issues" | |
| 16 | }, | |
| 17 | "keywords": [ | |
| 18 | "typescript", | |
| 19 | "bun", | |
| 20 | "css", | |
| 21 | "parser", | |
| 22 | "minifier", | |
| 23 | "selector", | |
| 24 | "ast", | |
| 25 | "walker", | |
| 26 | "generator", | |
| 27 | "csso", | |
| 28 | "css-tree", | |
| 29 | "css-select", | |
| 30 | "css-what" | |
| 31 | ], | |
| 32 | "exports": { | |
| 33 | ".": { | |
| 34 | "types": "./dist/index.d.ts", | |
| 35 | "import": "./dist/index.js" | |
| 36 | }, | |
| 37 | "./parse": { | |
| 38 | "types": "./dist/parse/index.d.ts", | |
| 39 | "import": "./dist/parse/index.js" | |
| 40 | }, | |
| 41 | "./what": { | |
| 42 | "types": "./dist/what/index.d.ts", | |
| 43 | "import": "./dist/what/index.js" | |
| 44 | }, | |
| 45 | "./select": { | |
| 46 | "types": "./dist/select/index.d.ts", | |
| 47 | "import": "./dist/select/index.js" | |
| 48 | }, | |
| 49 | "./optimize": { | |
| 50 | "types": "./dist/optimize/index.d.ts", | |
| 51 | "import": "./dist/optimize/index.js" | |
| 52 | }, | |
| 53 | "./*": { | |
| 54 | "import": "./dist/*" | |
| 55 | } | |
| 56 | }, | |
| 57 | "module": "./dist/index.js", | |
| 58 | "types": "./dist/index.d.ts", | |
| 59 | "bin": { | |
| 60 | "ts-css": "./dist/bin/cli.js" | |
| 61 | }, | |
| 62 | "files": [ | |
| 63 | "README.md", | |
| 64 | "dist" | |
| 65 | ], | |
| 66 | "scripts": { | |
| 67 | "build": "bun --bun build.ts && bun run compile", | |
| 68 | "compile": "bun build ./bin/cli.ts --compile --minify --outfile bin/ts-css", | |
| 69 | "compile:all": "bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64", | |
| 70 | "compile:linux-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-x64 --outfile bin/ts-css-linux-x64", | |
| 71 | "compile:linux-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-arm64 --outfile bin/ts-css-linux-arm64", | |
| 72 | "compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/ts-css-windows-x64.exe", | |
| 73 | "compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/ts-css-darwin-x64", | |
| 74 | "compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/ts-css-darwin-arm64", | |
| 75 | "zip": "bun run zip:all", | |
| 76 | "zip:all": "bun run zip:linux-x64 && bun run zip:linux-arm64 && bun run zip:windows-x64 && bun run zip:darwin-x64 && bun run zip:darwin-arm64", | |
| 77 | "zip:linux-x64": "zip -j bin/ts-css-linux-x64.zip bin/ts-css-linux-x64", | |
| 78 | "zip:linux-arm64": "zip -j bin/ts-css-linux-arm64.zip bin/ts-css-linux-arm64", | |
| 79 | "zip:windows-x64": "zip -j bin/ts-css-windows-x64.zip bin/ts-css-windows-x64.exe", | |
| 80 | "zip:darwin-x64": "zip -j bin/ts-css-darwin-x64.zip bin/ts-css-darwin-x64", | |
| 81 | "zip:darwin-arm64": "zip -j bin/ts-css-darwin-arm64.zip bin/ts-css-darwin-arm64", | |
| 82 | "fresh": "bunx rimraf node_modules/ bun.lock && bun i", | |
| 83 | "prepublishOnly": "bun --bun run build && bun run compile:all && bun run zip", | |
| 84 | "test": "bun test", | |
| 85 | "bench": "bun bench/index.bench.ts", | |
| 86 | "lint": "bunx --bun pickier .", | |
| 87 | "lint:fix": "bunx --bun pickier . --fix", | |
| 88 | "changelog": "bunx --bun logsmith --verbose", | |
| 89 | "changelog:generate": "bunx --bun logsmith --output CHANGELOG.md", | |
| 90 | "release": "bunx --bun bumpx prompt --recursive", | |
| 91 | "release:patch": "bunx --bun bumpx patch --yes --recursive", | |
| 92 | "release:minor": "bunx --bun bumpx minor --yes --recursive", | |
| 93 | "dev:docs": "bun --bun bunpress dev docs", | |
| 94 | "build:docs": "bun --bun bunpress build docs", | |
| 95 | "preview:docs": "bun --bun bunpress preview docs", | |
| 96 | "typecheck": "bun --bun tsc --noEmit", | |
| 97 | "format": "bunx --bun pickier . --format", | |
| 98 | "format:fix": "bunx --bun pickier . --format --write" | |
| 99 | }, | |
| 100 | "dependencies": { | |
| 101 | "bunfig": "^0.15.6" | |
| 102 | }, | |
| 103 | "devDependencies": { | |
| 104 | "@types/bun": "latest", | |
| 105 | "@types/css-tree": "^2.3.11", | |
| 106 | "@types/csso": "^5.0.4", | |
| 107 | "better-dx": "^0.2.15", | |
| 108 | "css-select": "^7.0.0", | |
| 109 | "css-tree": "^3.2.1", | |
| 110 | "css-what": "^8.0.0", | |
| 111 | "csso": "^5.0.5", | |
| 112 | "mitata": "^1.0.34" | |
| 113 | }, | |
| 114 | "git-hooks": { | |
| 115 | "pre-commit": { | |
| 116 | "staged-lint": { | |
| 117 | "*.{js,ts,json,yaml,yml,md}": "bunx --bun pickier lint --fix" | |
| 118 | }, | |
| 119 | "autoRestage": true | |
| 120 | }, | |
| 121 | "commit-msg": "bunx gitlint --edit .git/COMMIT_EDITMSG" | |
| 122 | } | |
| 123 | } | |
| @@ -0,0 +1,15 @@ | ||
| 1 | { | |
| 2 | "version": "1.0", | |
| 3 | "packages": { | |
| 4 | "bun.sh@0.0.0": { | |
| 5 | "name": "bun.sh", | |
| 6 | "version": "0.0.0", | |
| 7 | "resolved": "registry:bun.sh@0.0.0" | |
| 8 | }, | |
| 9 | "bun.sh@1.3.9": { | |
| 10 | "name": "bun.sh", | |
| 11 | "version": "1.3.9", | |
| 12 | "resolved": "registry:bun.sh@1.3.9" | |
| 13 | } | |
| 14 | } | |
| 15 | } | |
| @@ -0,0 +1,21 @@ | ||
| 1 | import type { CSSConfig } from './types' | |
| 2 | import { loadConfig } from 'bunfig' | |
| 3 | ||
| 4 | export const defaultConfig: CSSConfig = { | |
| 5 | floatPrecision: 3, | |
| 6 | verbose: false, | |
| 7 | } | |
| 8 | ||
| 9 | let _config: CSSConfig | null = null | |
| 10 | ||
| 11 | export async function getConfig(): Promise<CSSConfig> { | |
| 12 | if (!_config) { | |
| 13 | _config = await loadConfig({ | |
| 14 | name: 'ts-css', | |
| 15 | defaultConfig, | |
| 16 | }) | |
| 17 | } | |
| 18 | return _config | |
| 19 | } | |
| 20 | ||
| 21 | export const config: CSSConfig = defaultConfig | |
| @@ -0,0 +1,29 @@ | ||
| 1 | /** | |
| 2 | * @stacksjs/ts-css — pure-TypeScript CSS toolkit. | |
| 3 | * | |
| 4 | * Top-level surface re-exports the four sub-modules: | |
| 5 | * - parse — CSS parser/walker/generator (replaces css-tree) | |
| 6 | * - what — selector parser (replaces css-what) | |
| 7 | * - select — selector matching (replaces css-select) | |
| 8 | * - optimize — CSS minifier + specificity (replaces csso) | |
| 9 | */ | |
| 10 | ||
| 11 | export * from './config' | |
| 12 | ||
| 13 | // Sub-module namespaces — `import * as csstree from '@stacksjs/ts-css'` etc. | |
| 14 | export * as csstree from './parse' | |
| 15 | export * as cssWhat from './what' | |
| 16 | export * as cssSelect from './select' | |
| 17 | export * as csso from './optimize' | |
| 18 | ||
| 19 | // Top-level convenience re-exports | |
| 20 | export { clone, generate, List, parse, walk } from './parse' | |
| 21 | export type { CssNode, CssNodeType, ListItem, ParseOptions } from './parse' | |
| 22 | export { is, selectAll, selectOne } from './select' | |
| 23 | export type { Adapter, CompiledQuery, Options } from './select' | |
| 24 | export { isTraversal, parse as parseSelector, stringify as stringifySelector } from './what' | |
| 25 | export type { Selector, SelectorType } from './what' | |
| 26 | export { minify, minifyBlock, specificity, syntax } from './optimize' | |
| 27 | export type { MinifyOptions, MinifyResult, Specificity } from './optimize' | |
| 28 | ||
| 29 | export * from './types' | |
| @@ -0,0 +1,24 @@ | ||
| 1 | /** | |
| 2 | * Strip CSS comments (except `/*!*\/` license-style comments by default). | |
| 3 | */ | |
| 4 | ||
| 5 | import type { CssNode } from '../../parse' | |
| 6 | import { walk } from '../../parse' | |
| 7 | ||
| 8 | export function removeComments(ast: CssNode, options: { exclamation?: boolean | 'first-exclamation' } = {}): void { | |
| 9 | const exclamation = options.exclamation ?? false | |
| 10 | let firstSeen = false | |
| 11 | walk(ast, (node, item, list) => { | |
| 12 | if (node.type !== 'Comment') | |
| 13 | return | |
| 14 | const isExclamation = node.value.startsWith('!') | |
| 15 | if (exclamation === true && isExclamation) | |
| 16 | return | |
| 17 | if (exclamation === 'first-exclamation' && isExclamation && !firstSeen) { | |
| 18 | firstSeen = true | |
| 19 | return | |
| 20 | } | |
| 21 | if (item && list) | |
| 22 | list.remove(item) | |
| 23 | }) | |
| 24 | } | |
| @@ -0,0 +1,61 @@ | ||
| 1 | /** | |
| 2 | * Within a declaration list, drop duplicates earlier in the list when the | |
| 3 | * later occurrence has equal or higher importance. Preserves the LAST | |
| 4 | * occurrence per property. | |
| 5 | */ | |
| 6 | ||
| 7 | import type { CssNode, Declaration } from '../../parse' | |
| 8 | import { walk } from '../../parse' | |
| 9 | ||
| 10 | export function dedupeDeclarations(ast: CssNode): void { | |
| 11 | walk(ast, (node) => { | |
| 12 | if (node.type !== 'Block' && node.type !== 'DeclarationList') | |
| 13 | return | |
| 14 | if (!('children' in node) || !node.children) | |
| 15 | return | |
| 16 | const list = node.children as any | |
| 17 | // Quick check: at least one Declaration AND a possibly-duplicating | |
| 18 | // sibling. Single-decl (or no-decl) blocks are the common case for | |
| 19 | // nested rules and skipping the Map allocation here is measurable. | |
| 20 | let head = list.head | |
| 21 | if (head == null || head.next == null) | |
| 22 | return | |
| 23 | // Pessimistic-but-cheap pre-scan that also confirms ≥2 declarations. | |
| 24 | let declCount = 0 | |
| 25 | for (let it = head; it != null; it = it.next) { | |
| 26 | if ((it.data as CssNode).type === 'Declaration') { | |
| 27 | declCount++ | |
| 28 | if (declCount >= 2) | |
| 29 | break | |
| 30 | } | |
| 31 | } | |
| 32 | if (declCount < 2) | |
| 33 | return | |
| 34 | ||
| 35 | const seen = new Map<string, { item: any, decl: Declaration }>() | |
| 36 | const toRemove: any[] = [] | |
| 37 | for (let item = head; item != null; item = item.next) { | |
| 38 | const data = item.data as CssNode | |
| 39 | if (data.type !== 'Declaration') | |
| 40 | continue | |
| 41 | const key = data.property | |
| 42 | const prev = seen.get(key) | |
| 43 | if (prev) { | |
| 44 | const prevImp = !!prev.decl.important | |
| 45 | const curImp = !!data.important | |
| 46 | if (curImp || prevImp === curImp) { | |
| 47 | toRemove.push(prev.item) | |
| 48 | seen.set(key, { item, decl: data }) | |
| 49 | } | |
| 50 | else { | |
| 51 | toRemove.push(item) | |
| 52 | } | |
| 53 | } | |
| 54 | else { | |
| 55 | seen.set(key, { item, decl: data }) | |
| 56 | } | |
| 57 | } | |
| 58 | for (const it of toRemove) | |
| 59 | list.remove(it) | |
| 60 | }) | |
| 61 | } | |
| @@ -0,0 +1,94 @@ | ||
| 1 | /** | |
| 2 | * Color shorthand compressor. | |
| 3 | * - `#aabbcc` → `#abc` when each pair is a doubled hex digit | |
| 4 | * - `rgb(255, 0, 0)` → `red` | |
| 5 | * - `rgb(0, 0, 0)` → `#000` (handled by named-name table later) | |
| 6 | * | |
| 7 | * Lookup tables here are conservative — only the substitutions that | |
| 8 | * unambiguously shorten the bytes. | |
| 9 | */ | |
| 10 | ||
| 11 | const NAMED_TO_HEX: Record<string, string> = { | |
| 12 | black: '#000', | |
| 13 | fuchsia: '#f0f', | |
| 14 | white: '#fff', | |
| 15 | red: '#f00', | |
| 16 | cyan: '#0ff', | |
| 17 | blue: '#00f', | |
| 18 | yellow: '#ff0', | |
| 19 | magenta: '#f0f', | |
| 20 | lime: '#0f0', | |
| 21 | silver: '#c0c0c0', | |
| 22 | gray: '#808080', | |
| 23 | maroon: '#800000', | |
| 24 | olive: '#808000', | |
| 25 | green: '#008000', | |
| 26 | purple: '#800080', | |
| 27 | teal: '#008080', | |
| 28 | navy: '#000080', | |
| 29 | } | |
| 30 | ||
| 31 | const HEX_TO_SHORT_NAME: Record<string, string> = { | |
| 32 | '#f00': 'red', | |
| 33 | '#ff0000': 'red', | |
| 34 | '#000080': 'navy', | |
| 35 | '#008080': 'teal', | |
| 36 | } | |
| 37 | ||
| 38 | export function shortenHex(hex: string): string { | |
| 39 | if (!hex.startsWith('#')) | |
| 40 | return hex | |
| 41 | const body = hex.slice(1) | |
| 42 | if (body.length === 6) { | |
| 43 | if (body[0] === body[1] && body[2] === body[3] && body[4] === body[5]) | |
| 44 | return `#${body[0]}${body[2]}${body[4]}` | |
| 45 | } | |
| 46 | if (body.length === 8) { | |
| 47 | if (body[0] === body[1] && body[2] === body[3] && body[4] === body[5] && body[6] === body[7]) | |
| 48 | return `#${body[0]}${body[2]}${body[4]}${body[6]}` | |
| 49 | } | |
| 50 | return hex | |
| 51 | } | |
| 52 | ||
| 53 | export function colorNameToHex(name: string): string | null { | |
| 54 | const lower = name.toLowerCase() | |
| 55 | return NAMED_TO_HEX[lower] ?? null | |
| 56 | } | |
| 57 | ||
| 58 | export function hexToShortName(hex: string): string | null { | |
| 59 | const lower = hex.toLowerCase() | |
| 60 | return HEX_TO_SHORT_NAME[lower] ?? null | |
| 61 | } | |
| 62 | ||
| 63 | export function rgbToHex(r: number, g: number, b: number): string { | |
| 64 | const hex = ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0') | |
| 65 | return shortenHex(`#${hex}`) | |
| 66 | } | |
| 67 | ||
| 68 | const RGB_RE = /^rgba?\(\s*([+-]?\d*\.?\d+%?)\s*[,\s]\s*([+-]?\d*\.?\d+%?)\s*[,\s]\s*([+-]?\d*\.?\d+%?)\s*(?:[,/]\s*([+-]?\d*\.?\d+%?)\s*)?\)$/ | |
| 69 | ||
| 70 | export function compressRgbToHex(value: string): string { | |
| 71 | const m = RGB_RE.exec(value.trim()) | |
| 72 | if (!m) | |
| 73 | return value | |
| 74 | const r = parseChannel(m[1]!) | |
| 75 | const g = parseChannel(m[2]!) | |
| 76 | const b = parseChannel(m[3]!) | |
| 77 | if (m[4] !== undefined && !isAlphaOne(m[4])) { | |
| 78 | // alpha != 1 — keep as rgba | |
| 79 | return value | |
| 80 | } | |
| 81 | return rgbToHex(r, g, b) | |
| 82 | } | |
| 83 | ||
| 84 | function isAlphaOne(s: string): boolean { | |
| 85 | if (s.endsWith('%')) | |
| 86 | return Number.parseFloat(s) >= 100 | |
| 87 | return Number.parseFloat(s) >= 1 | |
| 88 | } | |
| 89 | ||
| 90 | function parseChannel(s: string): number { | |
| 91 | if (s.endsWith('%')) | |
| 92 | return Math.max(0, Math.min(255, Math.round(Number.parseFloat(s) * 2.55))) | |
| 93 | return Math.max(0, Math.min(255, Math.round(Number.parseFloat(s)))) | |
| 94 | } | |
| @@ -0,0 +1,189 @@ | ||
| 1 | /** | |
| 2 | * Run all compressors over the AST in place. | |
| 3 | * | |
| 4 | * Identifier ↔ Hash conversions happen only when the surrounding | |
| 5 | * declaration's property is known to accept a color (so `font-family: red` | |
| 6 | * isn't treated as a color, and `color: red` is). The list+item args | |
| 7 | * from the walker callback let us swap nodes in place without rebuilding | |
| 8 | * the parent list. | |
| 9 | */ | |
| 10 | ||
| 11 | import type { CssNode, HashNode, Identifier } from '../../parse' | |
| 12 | import { generate, walk } from '../../parse' | |
| 13 | import { colorNameToHex, compressRgbToHex, hexToShortName, shortenHex } from './color' | |
| 14 | import { compressDimension, compressNumber, compressPercentage, roundNumberString } from './number' | |
| 15 | ||
| 16 | export interface CompressOptions { | |
| 17 | /** Round numeric values to this many decimal places. `null` = no rounding. */ | |
| 18 | floatPrecision?: number | null | |
| 19 | } | |
| 20 | ||
| 21 | const COLOR_PROPERTIES: ReadonlySet<string> = new Set([ | |
| 22 | 'color', | |
| 23 | 'background', | |
| 24 | 'background-color', | |
| 25 | 'border', | |
| 26 | 'border-color', | |
| 27 | 'border-top', | |
| 28 | 'border-top-color', | |
| 29 | 'border-right', | |
| 30 | 'border-right-color', | |
| 31 | 'border-bottom', | |
| 32 | 'border-bottom-color', | |
| 33 | 'border-left', | |
| 34 | 'border-left-color', | |
| 35 | 'border-block', | |
| 36 | 'border-block-color', | |
| 37 | 'border-block-start-color', | |
| 38 | 'border-block-end-color', | |
| 39 | 'border-inline', | |
| 40 | 'border-inline-color', | |
| 41 | 'border-inline-start-color', | |
| 42 | 'border-inline-end-color', | |
| 43 | 'outline', | |
| 44 | 'outline-color', | |
| 45 | 'caret-color', | |
| 46 | 'fill', | |
| 47 | 'stroke', | |
| 48 | 'flood-color', | |
| 49 | 'lighting-color', | |
| 50 | 'stop-color', | |
| 51 | 'column-rule', | |
| 52 | 'column-rule-color', | |
| 53 | 'text-decoration', | |
| 54 | 'text-decoration-color', | |
| 55 | 'text-emphasis', | |
| 56 | 'text-emphasis-color', | |
| 57 | 'text-shadow', | |
| 58 | 'box-shadow', | |
| 59 | 'accent-color', | |
| 60 | 'scrollbar-color', | |
| 61 | ]) | |
| 62 | ||
| 63 | /** | |
| 64 | * Drop redundant WhiteSpace nodes from a `Parentheses` / `Function` / | |
| 65 | * `Value` container — adjacent to `:` `,` `/` operators and at the | |
| 66 | * boundaries. Run inline as a `leave` callback so the compressor only | |
| 67 | * traverses the AST once instead of twice. | |
| 68 | * | |
| 69 | * Whitespace around `+` and `-` is preserved: in `calc()` (and friends) | |
| 70 | * the spec requires whitespace on both sides of those operators, and a | |
| 71 | * compactor that strips it produces unparseable output (e.g. | |
| 72 | * `calc(100%-1rem)` parses as two adjacent dimensions, not a subtraction). | |
| 73 | */ | |
| 74 | function isCompactableOperator(node: any): boolean { | |
| 75 | if (!node || node.type !== 'Operator') | |
| 76 | return false | |
| 77 | const v = node.value | |
| 78 | return v === ':' || v === ',' || v === '/' | |
| 79 | } | |
| 80 | ||
| 81 | function compactContainerWhitespace(node: { children: any }): void { | |
| 82 | const list = node.children as any | |
| 83 | if (!list) | |
| 84 | return | |
| 85 | // 1. trim leading | |
| 86 | while (list.head && list.head.data.type === 'WhiteSpace') | |
| 87 | list.remove(list.head) | |
| 88 | // 2. trim trailing | |
| 89 | while (list.tail && list.tail.data.type === 'WhiteSpace') | |
| 90 | list.remove(list.tail) | |
| 91 | // 3. drop WhiteSpace adjacent to `: , /` operators only. | |
| 92 | let cur = list.head | |
| 93 | while (cur) { | |
| 94 | const nxt = cur.next | |
| 95 | if (cur.data.type === 'WhiteSpace') { | |
| 96 | const prevIsOp = cur.prev && isCompactableOperator(cur.prev.data) | |
| 97 | const nextIsOp = nxt && isCompactableOperator(nxt.data) | |
| 98 | if (prevIsOp || nextIsOp) | |
| 99 | list.remove(cur) | |
| 100 | } | |
| 101 | cur = nxt | |
| 102 | } | |
| 103 | } | |
| 104 | ||
| 105 | export function compressTree(ast: CssNode, options: CompressOptions = {}): void { | |
| 106 | const fp = options.floatPrecision ?? null | |
| 107 | const round = fp === null ? null : (s: string) => roundNumberString(s, fp) | |
| 108 | walk(ast, { | |
| 109 | enter(node, item, list) { | |
| 110 | switch (node.type) { | |
| 111 | case 'Number': { | |
| 112 | const v = round ? round(node.value) : node.value | |
| 113 | node.value = compressNumber(v) | |
| 114 | return | |
| 115 | } | |
| 116 | case 'Percentage': { | |
| 117 | const v = round ? round(node.value) : node.value | |
| 118 | node.value = compressPercentage(v) | |
| 119 | return | |
| 120 | } | |
| 121 | case 'Dimension': { | |
| 122 | const v = round ? round(node.value) : node.value | |
| 123 | const c = compressDimension(v, node.unit) | |
| 124 | node.value = c.value | |
| 125 | node.unit = c.unit | |
| 126 | return | |
| 127 | } | |
| 128 | case 'Hash': { | |
| 129 | const newName = shortenHex(`#${node.name}`).slice(1) | |
| 130 | node.name = newName | |
| 131 | if (item && list && this.declaration && COLOR_PROPERTIES.has(this.declaration.property.toLowerCase())) { | |
| 132 | const short = hexToShortName(`#${newName}`) | |
| 133 | if (short && short.length < newName.length + 1) { | |
| 134 | const ident: Identifier = { type: 'Identifier', name: short, loc: node.loc } | |
| 135 | list.replace(item, list.createItem(ident)) | |
| 136 | } | |
| 137 | } | |
| 138 | return | |
| 139 | } | |
| 140 | case 'Identifier': { | |
| 141 | if (!item || !list || !this.declaration) | |
| 142 | return | |
| 143 | if (!COLOR_PROPERTIES.has(this.declaration.property.toLowerCase())) | |
| 144 | return | |
| 145 | const hex = colorNameToHex(node.name) | |
| 146 | if (hex && hex.length < node.name.length) { | |
| 147 | const hashNode: HashNode = { type: 'Hash', name: hex.slice(1), loc: node.loc } | |
| 148 | list.replace(item, list.createItem(hashNode)) | |
| 149 | } | |
| 150 | return | |
| 151 | } | |
| 152 | case 'Url': | |
| 153 | // Generator decides whether to quote based on the value's chars | |
| 154 | // (`/[\s"'()\\]/`). Storing a normalized inner string here is a | |
| 155 | // no-op pass — the previous double-trip through `compressUrl` | |
| 156 | // followed by `.replace(/^"|"$/g, '')` was equivalent to leaving | |
| 157 | // the value alone. Kept the case branch for future shrinkers | |
| 158 | // (e.g. dropping a leading `./`). | |
| 159 | return | |
| 160 | case 'Function': { | |
| 161 | // `rgb(...)` / `rgba(...)` → `#rrggbb` (or `#rgb`) when the alpha | |
| 162 | // is opaque. We let the walker process the children first (so any | |
| 163 | // contained Number/Percentage was already shortened) and then | |
| 164 | // replace the whole Function with a Hash on `leave`. Mark the | |
| 165 | // node so the leave handler knows to do the substitution. | |
| 166 | const lname = node.name.toLowerCase() | |
| 167 | if ((lname === 'rgb' || lname === 'rgba') && item && list) | |
| 168 | (node as any).__rgbReplaceCandidate = true | |
| 169 | } | |
| 170 | } | |
| 171 | }, | |
| 172 | leave(node, item, list) { | |
| 173 | if (node.type === 'Parentheses' || node.type === 'Function' || node.type === 'Value') | |
| 174 | compactContainerWhitespace(node as any) | |
| 175 | if (node.type === 'Function' && (node as any).__rgbReplaceCandidate && item && list) { | |
| 176 | // Re-stringify the function with its (now-compressed) children and | |
| 177 | // run the rgb→hex compressor. If the result is a `#rrggbb` token, | |
| 178 | // splice it in as a Hash so further passes can shorten it more | |
| 179 | // and the property-aware Identifier swap can apply. | |
| 180 | const text = generate(node as CssNode) | |
| 181 | const replacement = compressRgbToHex(text) | |
| 182 | if (replacement !== text && replacement.startsWith('#')) { | |
| 183 | const hashNode: HashNode = { type: 'Hash', name: replacement.slice(1), loc: node.loc } | |
| 184 | list.replace(item, list.createItem(hashNode)) | |
| 185 | } | |
| 186 | } | |
| 187 | }, | |
| 188 | }) | |
| 189 | } | |
| @@ -0,0 +1,84 @@ | ||
| 1 | /** | |
| 2 | * Number / dimension / percentage compressors. | |
| 3 | * | |
| 4 | * - `0.5px` → `.5px` | |
| 5 | * - `0.5em` → `.5em` | |
| 6 | * - `0px` / `0%` / `0em` (etc.) → `0` in length context | |
| 7 | * - trailing zeros removed: `2.5000` → `2.5` | |
| 8 | * - exponential / scientific stays as-is — already shortest form | |
| 9 | */ | |
| 10 | ||
| 11 | // Length units only — angle/time/frequency/resolution units are NOT here: | |
| 12 | // `0deg`, `0s`, `0Hz`, `0dpi` are NOT equivalent to `0` in CSS, and stripping | |
| 13 | // the unit there breaks the value type. Modern viewport variants (`svh`, | |
| 14 | // `lvh`, `dvh`, `svw`, `lvw`, `dvw`, `svi`, `svb`, …) and container-query | |
| 15 | // units (`cqw`, `cqh`, `cqi`, `cqb`, `cqmin`, `cqmax`) are length units too. | |
| 16 | const ZERO_UNITS = new Set([ | |
| 17 | // absolute / classic | |
| 18 | 'px', 'pt', 'pc', 'in', 'cm', 'mm', 'q', | |
| 19 | // font-relative | |
| 20 | 'em', 'rem', 'ex', 'ch', 'cap', 'ic', 'lh', 'rlh', | |
| 21 | // viewport-relative | |
| 22 | 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', | |
| 23 | // small/large/dynamic viewport | |
| 24 | 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax', | |
| 25 | 'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax', | |
| 26 | 'dvw', 'dvh', 'dvi', 'dvb', 'dvmin', 'dvmax', | |
| 27 | // container-query units | |
| 28 | 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', | |
| 29 | ]) | |
| 30 | ||
| 31 | export function compressNumber(value: string): string { | |
| 32 | // Drop a leading `+` sign (CSS numbers can't be `+`-prefixed except | |
| 33 | // immediately after an operator; the parser already handles that). | |
| 34 | if (value.charCodeAt(0) === 43 /* + */) | |
| 35 | value = value.slice(1) | |
| 36 | ||
| 37 | // strip trailing zeros after decimal point: `2.5000` → `2.5`, | |
| 38 | // `2.500e5` → `2.5e5`. The `($|e|E)` boundary keeps an exponent intact. | |
| 39 | if (value.includes('.')) { | |
| 40 | let v = value.replace(/(\.\d*?)0+($|[eE])/, '$1$2') | |
| 41 | // bare `.` left over — strip it (`5.` → `5`, `5.e2` → `5e2`) | |
| 42 | v = v.replace(/\.($|[eE])/, '$1') | |
| 43 | value = v | |
| 44 | } | |
| 45 | ||
| 46 | // 0.5 → .5 / -0.5 → -.5 | |
| 47 | if (value.startsWith('0.') && value.length > 2) | |
| 48 | value = value.slice(1) | |
| 49 | else if (value.startsWith('-0.') && value.length > 3) | |
| 50 | value = `-${value.slice(2)}` | |
| 51 | ||
| 52 | // Negative zero `-0` (and `-0.0` which has already collapsed) is just 0. | |
| 53 | if (value === '-0' || value === '-.0' || value === '-0.0') | |
| 54 | value = '0' | |
| 55 | ||
| 56 | return value | |
| 57 | } | |
| 58 | ||
| 59 | export function compressDimension(value: string, unit: string): { value: string, unit: string } { | |
| 60 | const compressed = compressNumber(value) | |
| 61 | if ((compressed === '0' || compressed === '-0') && ZERO_UNITS.has(unit.toLowerCase())) | |
| 62 | return { value: '0', unit: '' } | |
| 63 | return { value: compressed, unit } | |
| 64 | } | |
| 65 | ||
| 66 | export function compressPercentage(value: string): string { | |
| 67 | const compressed = compressNumber(value) | |
| 68 | return compressed | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Round a numeric string to `precision` decimal places. Returns the input | |
| 73 | * unchanged when it's not a finite number — calling sites should still | |
| 74 | * follow up with `compressNumber` to strip leading zeros etc. | |
| 75 | */ | |
| 76 | export function roundNumberString(value: string, precision: number): string { | |
| 77 | const n = Number.parseFloat(value) | |
| 78 | if (!Number.isFinite(n)) | |
| 79 | return value | |
| 80 | // toFixed with negative precision is invalid in older runtimes — clamp. | |
| 81 | const p = precision < 0 ? 0 : precision | |
| 82 | // toFixed rounds; trim trailing zeros and stray dot. | |
| 83 | return n.toFixed(p) | |
| 84 | } | |
| @@ -0,0 +1,23 @@ | ||
| 1 | /** | |
| 2 | * String compressor — picks the quote style with fewer escapes. | |
| 3 | * | |
| 4 | * `String.split(q).length - 1` is the conventional "count occurrences" | |
| 5 | * idiom but it allocates the intermediate array; for typical CSS strings | |
| 6 | * the value is short, but a tight indexOf-based count is cheaper and | |
| 7 | * doesn't churn the GC when the same string flows through repeatedly. | |
| 8 | */ | |
| 9 | ||
| 10 | // eslint-disable-next-line pickier/no-unused-vars | |
| 11 | export function compressString(value: string): string { | |
| 12 | let dq = 0 | |
| 13 | let sq = 0 | |
| 14 | for (let i = 0; i < value.length; i++) { | |
| 15 | const c = value.charCodeAt(i) | |
| 16 | if (c === 34 /* " */) dq++ | |
| 17 | else if (c === 39 /* ' */) sq++ | |
| 18 | } | |
| 19 | if (dq <= sq) { | |
| 20 | return `"${value.replace(/"/g, '\\"')}"` | |
| 21 | } | |
| 22 | return `'${value.replace(/'/g, '\\\'')}'` | |
| 23 | } | |
| @@ -0,0 +1,20 @@ | ||
| 1 | /** | |
| 2 | * URL compressor: drop quotes when the inner string is "URL-safe" (no | |
| 3 | * whitespace, no parens, no quotes, no backslash). Manual char scan is a | |
| 4 | * smidge faster than `RegExp.test` and avoids the per-call regex | |
| 5 | * lastIndex bookkeeping the runtime does for `g`-style patterns. | |
| 6 | */ | |
| 7 | ||
| 8 | // eslint-disable-next-line pickier/no-unused-vars | |
| 9 | export function compressUrl(value: string): string { | |
| 10 | for (let i = 0; i < value.length; i++) { | |
| 11 | const c = value.charCodeAt(i) | |
| 12 | if ( | |
| 13 | c === 34 /* " */ || c === 39 /* ' */ || c === 40 /* ( */ || c === 41 /* ) */ | |
| 14 | || c === 92 /* \ */ || c === 32 || c === 9 || c === 10 || c === 12 || c === 13 | |
| 15 | ) { | |
| 16 | return `"${value.replace(/"/g, '\\"')}"` | |
| 17 | } | |
| 18 | } | |
| 19 | return value | |
| 20 | } | |
| @@ -0,0 +1,18 @@ | ||
| 1 | /** | |
| 2 | * CSS minifier — the API surface that csso users (notably SVGO's | |
| 3 | * `minifyStyles` and `inlineStyles`) depend on. | |
| 4 | */ | |
| 5 | ||
| 6 | export { dedupeDeclarations, removeComments } from './clean' | |
| 7 | export { compressTree } from './compress' | |
| 8 | export { minify, minifyBlock } from './minify' | |
| 9 | export type { MinifyOptions, MinifyResult } from './minify' | |
| 10 | export { specificity, specificityToString } from './specificity' | |
| 11 | export type { Specificity } from './specificity' | |
| 12 | ||
| 13 | import { specificity as _specificity } from './specificity' | |
| 14 | ||
| 15 | /** Mirror csso's `syntax` namespace export. */ | |
| 16 | export const syntax: { specificity: typeof _specificity } = { | |
| 17 | specificity: _specificity, | |
| 18 | } | |
| @@ -0,0 +1,67 @@ | ||
| 1 | /** | |
| 2 | * Public minifier entry. Accepts a CSS string, returns | |
| 3 | * `{ css, ast }` matching csso's `minify` shape (the `map` field for | |
| 4 | * source maps is intentionally omitted — we don't generate source maps). | |
| 5 | */ | |
| 6 | ||
| 7 | import type { CssNode, ParseOptions } from '../parse' | |
| 8 | import { generate, parse } from '../parse' | |
| 9 | import { removeComments } from './clean/comment' | |
| 10 | import { dedupeDeclarations } from './clean/declaration' | |
| 11 | import { compressTree } from './compress' | |
| 12 | ||
| 13 | export interface MinifyOptions { | |
| 14 | /** When false, comments starting with `!` are preserved. */ | |
| 15 | comments?: boolean | 'exclamation' | 'first-exclamation' | |
| 16 | /** | |
| 17 | * Maximum decimal places to keep on numeric values (rounded). Omit or | |
| 18 | * set to `null` to keep input precision. Matches csso's option name. | |
| 19 | */ | |
| 20 | floatPrecision?: number | null | |
| 21 | /** Restructure rules across the document — currently a no-op (we | |
| 22 | * keep declarations stable; csso's restructuring is the bulk of its | |
| 23 | * code and gives marginal real-world benefit beyond gzip). */ | |
| 24 | restructure?: boolean | |
| 25 | /** Merge `@media` rules with the same query. */ | |
| 26 | forceMediaMerge?: boolean | |
| 27 | /** Usage hints for selector pruning (matches csso shape). */ | |
| 28 | usage?: { tags?: string[], ids?: string[], classes?: string[], force?: boolean } | null | |
| 29 | } | |
| 30 | ||
| 31 | export interface MinifyResult { | |
| 32 | css: string | |
| 33 | ast: CssNode | |
| 34 | } | |
| 35 | ||
| 36 | export function minify(source: string, options: MinifyOptions = {}): MinifyResult { | |
| 37 | const ast = parse(source, { context: 'stylesheet' } as ParseOptions) | |
| 38 | return runPipeline(ast, options) | |
| 39 | } | |
| 40 | ||
| 41 | export function minifyBlock(source: string, options: MinifyOptions = {}): MinifyResult { | |
| 42 | const ast = parse(source, { context: 'declarationList' } as ParseOptions) | |
| 43 | return runPipeline(ast, options) | |
| 44 | } | |
| 45 | ||
| 46 | function runPipeline(ast: CssNode, options: MinifyOptions): MinifyResult { | |
| 47 | // 1. comment cleanup — default preserves `/*!*\/` comments (csso behaviour) | |
| 48 | if (options.comments === false) { | |
| 49 | removeComments(ast, { exclamation: false }) | |
| 50 | } | |
| 51 | else if (options.comments === 'first-exclamation') { | |
| 52 | removeComments(ast, { exclamation: 'first-exclamation' }) | |
| 53 | } | |
| 54 | else { | |
| 55 | // undefined | true | 'exclamation' → preserve `!` comments | |
| 56 | removeComments(ast, { exclamation: true }) | |
| 57 | } | |
| 58 | ||
| 59 | // 2. value-level compression (numbers, dimensions, colors, urls) | |
| 60 | compressTree(ast, { floatPrecision: options.floatPrecision ?? null }) | |
| 61 | ||
| 62 | // 3. declaration dedup | |
| 63 | dedupeDeclarations(ast) | |
| 64 | ||
| 65 | // 4. stringify | |
| 66 | return { css: generate(ast), ast } | |
| 67 | } | |
| @@ -0,0 +1,181 @@ | ||
| 1 | /** | |
| 2 | * Compute CSS selector specificity per https://www.w3.org/TR/selectors-4/#specificity-rules. | |
| 3 | * | |
| 4 | * Returns an `[a, b, c]` tuple where: | |
| 5 | * a = id selectors | |
| 6 | * b = class / attribute / pseudo-class selectors | |
| 7 | * c = type / pseudo-element selectors | |
| 8 | * | |
| 9 | * Accepts either: | |
| 10 | * - a css-tree-style `Selector` / `SelectorList` AST node (csso shape), OR | |
| 11 | * - a `Selector[][]` returned by `cssWhat.parse(string)` — so callers | |
| 12 | * can pipe `cssWhat.parse(...)` straight into `syntax.specificity(...)`. | |
| 13 | */ | |
| 14 | ||
| 15 | import type { CssNode } from '../parse' | |
| 16 | import type { Selector as WhatSelector } from '../what' | |
| 17 | import { parse as parseSelector } from '../what' | |
| 18 | ||
| 19 | export type Specificity = [number, number, number] | |
| 20 | ||
| 21 | export function specificity(input: CssNode | WhatSelector[][] | WhatSelector[] | string): Specificity { | |
| 22 | const result: Specificity = [0, 0, 0] | |
| 23 | ||
| 24 | if (typeof input === 'string') { | |
| 25 | const ast = parseSelector(input) | |
| 26 | visitWhat(ast, result) | |
| 27 | return result | |
| 28 | } | |
| 29 | ||
| 30 | if (Array.isArray(input)) { | |
| 31 | if (input.length === 0) | |
| 32 | return result | |
| 33 | // distinguish Selector[][] from Selector[] | |
| 34 | if (Array.isArray(input[0])) | |
| 35 | visitWhat(input as WhatSelector[][], result) | |
| 36 | else | |
| 37 | visitWhatGroup(input as WhatSelector[], result) | |
| 38 | return result | |
| 39 | } | |
| 40 | ||
| 41 | visit(input, result) | |
| 42 | return result | |
| 43 | } | |
| 44 | ||
| 45 | // ---- css-tree shape ---- | |
| 46 | ||
| 47 | function visit(node: CssNode, result: Specificity): void { | |
| 48 | switch (node.type) { | |
| 49 | case 'IdSelector': | |
| 50 | result[0]++ | |
| 51 | return | |
| 52 | case 'ClassSelector': | |
| 53 | case 'AttributeSelector': | |
| 54 | result[1]++ | |
| 55 | return | |
| 56 | case 'TypeSelector': | |
| 57 | if (node.name !== '*') | |
| 58 | result[2]++ | |
| 59 | return | |
| 60 | case 'PseudoClassSelector': { | |
| 61 | const name = node.name.toLowerCase() | |
| 62 | if (name === 'is' || name === 'matches' || name === '-moz-any' || name === '-webkit-any' || name === 'not' || name === 'has') { | |
| 63 | if (node.children) { | |
| 64 | const selectors: CssNode[] = [] | |
| 65 | for (const inner of node.children as Iterable<CssNode>) { | |
| 66 | if (inner.type === 'SelectorList' && 'children' in inner && inner.children) { | |
| 67 | for (const s of inner.children as Iterable<CssNode>) | |
| 68 | selectors.push(s) | |
| 69 | } | |
| 70 | else if (inner.type === 'Selector') { | |
| 71 | selectors.push(inner) | |
| 72 | } | |
| 73 | } | |
| 74 | let max: Specificity = [0, 0, 0] | |
| 75 | for (const sel of selectors) { | |
| 76 | const s = specificity(sel) | |
| 77 | if (compareSpec(s, max) > 0) | |
| 78 | max = s | |
| 79 | } | |
| 80 | result[0] += max[0] | |
| 81 | result[1] += max[1] | |
| 82 | result[2] += max[2] | |
| 83 | } | |
| 84 | return | |
| 85 | } | |
| 86 | if (name === 'where') | |
| 87 | return | |
| 88 | result[1]++ | |
| 89 | return | |
| 90 | } | |
| 91 | case 'PseudoElementSelector': | |
| 92 | result[2]++ | |
| 93 | return | |
| 94 | case 'Selector': | |
| 95 | case 'SelectorList': | |
| 96 | case 'AtrulePrelude': | |
| 97 | if ('children' in node && node.children) { | |
| 98 | for (const child of node.children as Iterable<CssNode>) | |
| 99 | visit(child, result) | |
| 100 | } | |
| 101 | return | |
| 102 | case 'NestingSelector': | |
| 103 | return | |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | // ---- css-what shape (Selector[][] or Selector[]) ---- | |
| 108 | // | |
| 109 | // css-what's `Selector` is one of: | |
| 110 | // { type: 'tag' | 'universal' | 'attribute' | 'pseudo' | 'pseudo-element' | |
| 111 | // | 'descendant' | 'child' | 'parent' | 'sibling' | 'adjacent' | 'column-combinator', | |
| 112 | // … } | |
| 113 | // | |
| 114 | // For specificity, *only* attribute/pseudo/tag matter — combinators are | |
| 115 | // ignored. `:is`/`:not`/`:has`/`:matches` take the max of their args. | |
| 116 | ||
| 117 | function visitWhat(groups: WhatSelector[][], result: Specificity): void { | |
| 118 | // Each top-level group is one comma-separated selector. css-tree-style | |
| 119 | // specificity is computed per-selector; for SelectorList (`a, b`) we | |
| 120 | // sum across all groups (matches csso behaviour). | |
| 121 | for (const g of groups) | |
| 122 | visitWhatGroup(g, result) | |
| 123 | } | |
| 124 | ||
| 125 | function visitWhatGroup(tokens: WhatSelector[], result: Specificity): void { | |
| 126 | for (const t of tokens) { | |
| 127 | switch (t.type) { | |
| 128 | case 'tag': | |
| 129 | if (t.name !== '*') | |
| 130 | result[2]++ | |
| 131 | break | |
| 132 | case 'attribute': | |
| 133 | // `.foo` and `#bar` are emitted as attribute selectors with name | |
| 134 | // 'class'/'id' — count them like ClassSelector / IdSelector. | |
| 135 | if (t.name === 'id' && t.action === 'equals') | |
| 136 | result[0]++ | |
| 137 | else | |
| 138 | result[1]++ | |
| 139 | break | |
| 140 | case 'pseudo': { | |
| 141 | const name = t.name.toLowerCase() | |
| 142 | if (name === 'is' || name === 'matches' || name === 'not' || name === 'has' || name === '-moz-any' || name === '-webkit-any') { | |
| 143 | // data is Selector[][] when parsed by css-what's selector-list pseudos | |
| 144 | if (Array.isArray(t.data)) { | |
| 145 | let max: Specificity = [0, 0, 0] | |
| 146 | for (const inner of t.data as WhatSelector[][]) { | |
| 147 | const s: Specificity = [0, 0, 0] | |
| 148 | visitWhatGroup(inner, s) | |
| 149 | if (compareSpec(s, max) > 0) | |
| 150 | max = s | |
| 151 | } | |
| 152 | result[0] += max[0] | |
| 153 | result[1] += max[1] | |
| 154 | result[2] += max[2] | |
| 155 | } | |
| 156 | break | |
| 157 | } | |
| 158 | if (name === 'where') | |
| 159 | break | |
| 160 | result[1]++ | |
| 161 | break | |
| 162 | } | |
| 163 | case 'pseudo-element': | |
| 164 | result[2]++ | |
| 165 | break | |
| 166 | // combinators don't contribute to specificity | |
| 167 | } | |
| 168 | } | |
| 169 | } | |
| 170 | ||
| 171 | function compareSpec(a: Specificity, b: Specificity): number { | |
| 172 | for (let i = 0; i < 3; i++) { | |
| 173 | if (a[i] !== b[i]) | |
| 174 | return a[i]! - b[i]! | |
| 175 | } | |
| 176 | return 0 | |
| 177 | } | |
| 178 | ||
| 179 | export function specificityToString(s: Specificity): string { | |
| 180 | return s.join(',') | |
| 181 | } | |
| @@ -0,0 +1,43 @@ | ||
| 1 | /** | |
| 2 | * Deep clone a CSS AST node. List children are cloned via list iteration | |
| 3 | * so the result is structurally independent from the input. | |
| 4 | */ | |
| 5 | ||
| 6 | import type { CssNode } from './types' | |
| 7 | import { CssList } from './list' | |
| 8 | ||
| 9 | export function clone<T extends CssNode>(node: T): T { | |
| 10 | return cloneAny(node) as T | |
| 11 | } | |
| 12 | ||
| 13 | function cloneAny(value: any): any { | |
| 14 | if (value === null || typeof value !== 'object') | |
| 15 | return value | |
| 16 | if (value instanceof CssList) { | |
| 17 | const out = new CssList<any>() | |
| 18 | for (const child of value) | |
| 19 | out.appendData(cloneAny(child)) | |
| 20 | return out | |
| 21 | } | |
| 22 | if (Array.isArray(value)) | |
| 23 | return value.map(cloneAny) | |
| 24 | // CssLocation and other plain objects; defer detection of AST nodes to | |
| 25 | // the `'type' in value` check below. | |
| 26 | // typed AST node | |
| 27 | if ('type' in value && typeof value.type === 'string') { | |
| 28 | const out: Record<string, any> = { type: value.type } | |
| 29 | for (const k of Object.keys(value)) { | |
| 30 | if (k === 'type') | |
| 31 | continue | |
| 32 | out[k] = cloneAny(value[k]) | |
| 33 | } | |
| 34 | return out | |
| 35 | } | |
| 36 | // plain object — including CssLocation which we want to preserve | |
| 37 | // by-value rather than share by reference (matters when consumers | |
| 38 | // mutate `loc` after cloning). | |
| 39 | const out: Record<string, any> = {} | |
| 40 | for (const k of Object.keys(value)) | |
| 41 | out[k] = cloneAny(value[k]) | |
| 42 | return out | |
| 43 | } | |
Binary file. Nothing to show line by line.
| @@ -0,0 +1,59 @@ | ||
| 1 | /** | |
| 2 | * Public surface for the CSS parser/generator/walker. Drop-in compatible | |
| 3 | * with css-tree v3 for the API surface used by csso/SVGO. | |
| 4 | */ | |
| 5 | ||
| 6 | export { clone } from './clone' | |
| 7 | export { generate } from './generator' | |
| 8 | export { CssList as List, makeList } from './list' | |
| 9 | export type { ListItem } from './list' | |
| 10 | export { parse, TokenType } from './parser' | |
| 11 | export type { Token } from './tokenizer' | |
| 12 | export { decodeName, Tokenizer } from './tokenizer' | |
| 13 | export type { | |
| 14 | Atrule, | |
| 15 | AtrulePrelude, | |
| 16 | AttributeSelector, | |
| 17 | Block, | |
| 18 | ClassSelector, | |
| 19 | Combinator, | |
| 20 | CommentNode, | |
| 21 | CssLocation, | |
| 22 | CssNode, | |
| 23 | CssNodeType, | |
| 24 | Declaration, | |
| 25 | DeclarationList, | |
| 26 | DimensionNode, | |
| 27 | FunctionNode, | |
| 28 | HashNode, | |
| 29 | Identifier, | |
| 30 | IdSelector, | |
| 31 | MediaFeatureNode, | |
| 32 | MediaQueryListNode, | |
| 33 | MediaQueryNode, | |
| 34 | NestingSelector, | |
| 35 | NthNode, | |
| 36 | NumberNode, | |
| 37 | OperatorNode, | |
| 38 | ParenthesesNode, | |
| 39 | ParseContext, | |
| 40 | ParseOptions, | |
| 41 | PercentageNode, | |
| 42 | PseudoClassSelector, | |
| 43 | PseudoElementSelector, | |
| 44 | RatioNode, | |
| 45 | Raw, | |
| 46 | RawNode, | |
| 47 | Rule, | |
| 48 | Selector, | |
| 49 | SelectorList, | |
| 50 | StringNode, | |
| 51 | StyleSheet, | |
| 52 | TypeSelector, | |
| 53 | UnicodeRangeNode, | |
| 54 | UrlNode, | |
| 55 | Value, | |
| 56 | WhiteSpaceNode, | |
| 57 | } from './types' | |
| 58 | export { walk } from './walker' | |
| 59 | export type { WalkContext, WalkVisitor } from './walker' | |
| @@ -0,0 +1,375 @@ | ||
| 1 | /** | |
| 2 | * Doubly linked list used as the children container for every CSS AST node. | |
| 3 | * | |
| 4 | * Mirrors css-tree's `List` so `csstree.List` import sites can switch to | |
| 5 | * `import { CssList as List } from '@stacksjs/ts-css/parse'` without changing | |
| 6 | * call sites. The list owns its `ListItem<T>` cells; items can be moved | |
| 7 | * between lists by detach/insert pairs. | |
| 8 | */ | |
| 9 | ||
| 10 | export interface ListItem<T> { | |
| 11 | prev: ListItem<T> | null | |
| 12 | next: ListItem<T> | null | |
| 13 | data: T | |
| 14 | } | |
| 15 | ||
| 16 | function createItem<T>(data: T): ListItem<T> { | |
| 17 | return { prev: null, next: null, data } | |
| 18 | } | |
| 19 | ||
| 20 | interface ListCursor<T> { | |
| 21 | prev: ListItem<T> | null | |
| 22 | next: ListItem<T> | null | |
| 23 | } | |
| 24 | ||
| 25 | export class CssList<T> { | |
| 26 | head: ListItem<T> | null = null | |
| 27 | tail: ListItem<T> | null = null | |
| 28 | ||
| 29 | // ----- mutation cursors ----- | |
| 30 | // Each list owns its own cursor stack. Callbacks may insert/remove items | |
| 31 | // mid-walk; the topmost active cursor records the in-progress prev/next | |
| 32 | // pointers so a removal that lands on the cursor's `next` slot can be | |
| 33 | // repointed onto the survivor. Per-list (not module-global) so nested | |
| 34 | // walks of the same list don't corrupt each other. | |
| 35 | private cursors: ListCursor<T>[] = [] | |
| 36 | ||
| 37 | private allocateCursor(prev: ListItem<T> | null, next: ListItem<T> | null): ListCursor<T> { | |
| 38 | const cursor: ListCursor<T> = { prev, next } | |
| 39 | this.cursors.push(cursor) | |
| 40 | return cursor | |
| 41 | } | |
| 42 | ||
| 43 | private releaseCursor(): void { | |
| 44 | this.cursors.pop() | |
| 45 | } | |
| 46 | ||
| 47 | private updateCursors(prevOld: ListItem<T> | null, prevNew: ListItem<T> | null, nextOld: ListItem<T> | null, nextNew: ListItem<T> | null): void { | |
| 48 | for (const c of this.cursors) { | |
| 49 | if (c.prev === prevOld) | |
| 50 | c.prev = prevNew | |
| 51 | if (c.next === nextOld) | |
| 52 | c.next = nextNew | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | static createItem<U>(data: U): ListItem<U> { | |
| 57 | return createItem(data) | |
| 58 | } | |
| 59 | ||
| 60 | createItem(data: T): ListItem<T> { | |
| 61 | return createItem(data) | |
| 62 | } | |
| 63 | ||
| 64 | get isEmpty(): boolean { | |
| 65 | return this.head === null | |
| 66 | } | |
| 67 | ||
| 68 | get first(): T | null { | |
| 69 | return this.head?.data ?? null | |
| 70 | } | |
| 71 | ||
| 72 | get last(): T | null { | |
| 73 | return this.tail?.data ?? null | |
| 74 | } | |
| 75 | ||
| 76 | *[Symbol.iterator](): Iterator<T> { | |
| 77 | for (let cur = this.head; cur != null; cur = cur.next) | |
| 78 | yield cur.data | |
| 79 | } | |
| 80 | ||
| 81 | fromArray(items: ReadonlyArray<T>): this { | |
| 82 | let prev: ListItem<T> | null = null | |
| 83 | this.head = null | |
| 84 | for (const data of items) { | |
| 85 | const item = createItem(data) | |
| 86 | item.prev = prev | |
| 87 | if (prev) | |
| 88 | prev.next = item | |
| 89 | else | |
| 90 | this.head = item | |
| 91 | prev = item | |
| 92 | } | |
| 93 | this.tail = prev | |
| 94 | return this | |
| 95 | } | |
| 96 | ||
| 97 | toArray(): T[] { | |
| 98 | const out: T[] = [] | |
| 99 | for (let cur = this.head; cur != null; cur = cur.next) | |
| 100 | out.push(cur.data) | |
| 101 | return out | |
| 102 | } | |
| 103 | ||
| 104 | toJSON(): T[] { | |
| 105 | return this.toArray() | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * css-tree-style forEach: callback receives `(data, item, list)`. | |
| 110 | * The `item` is the underlying `ListItem<T>`, not the index — call sites | |
| 111 | * use it with `list.remove(item)` / `list.replace(item, ...)`. | |
| 112 | */ | |
| 113 | // eslint-disable-next-line pickier/no-unused-vars | |
| 114 | forEach(fn: (data: T, item: ListItem<T>, list: CssList<T>) => void, thisArg?: any): void { | |
| 115 | const cursor = this.allocateCursor(null, this.head) | |
| 116 | while (cursor.next !== null) { | |
| 117 | const item = cursor.next as ListItem<T> | |
| 118 | cursor.prev = item | |
| 119 | cursor.next = item.next | |
| 120 | fn.call(thisArg, item.data, item, this) | |
| 121 | } | |
| 122 | this.releaseCursor() | |
| 123 | } | |
| 124 | ||
| 125 | // eslint-disable-next-line pickier/no-unused-vars | |
| 126 | forEachRight(fn: (data: T, item: ListItem<T>, list: CssList<T>) => void, thisArg?: any): void { | |
| 127 | const cursor = this.allocateCursor(this.tail, null) | |
| 128 | while (cursor.prev !== null) { | |
| 129 | const item = cursor.prev as ListItem<T> | |
| 130 | cursor.next = item | |
| 131 | cursor.prev = item.prev | |
| 132 | fn.call(thisArg, item.data, item, this) | |
| 133 | } | |
| 134 | this.releaseCursor() | |
| 135 | } | |
| 136 | ||
| 137 | // eslint-disable-next-line pickier/no-unused-vars | |
| 138 | reduce<U>(fn: (acc: U, data: T, index: number, list: CssList<T>) => U, initial: U): U { | |
| 139 | let acc = initial | |
| 140 | let i = 0 | |
| 141 | for (let cur = this.head; cur != null; cur = cur.next) | |
| 142 | acc = fn(acc, cur.data, i++, this) | |
| 143 | return acc | |
| 144 | } | |
| 145 | ||
| 146 | // eslint-disable-next-line pickier/no-unused-vars | |
| 147 | some(fn: (item: T, index: number, list: CssList<T>) => boolean): boolean { | |
| 148 | let i = 0 | |
| 149 | for (let cur = this.head; cur != null; cur = cur.next) { | |
| 150 | if (fn(cur.data, i++, this)) | |
| 151 | return true | |
| 152 | } | |
| 153 | return false | |
| 154 | } | |
| 155 | ||
| 156 | // eslint-disable-next-line pickier/no-unused-vars | |
| 157 | map<U>(fn: (item: T, index: number, list: CssList<T>) => U): CssList<U> { | |
| 158 | const result = new CssList<U>() | |
| 159 | let prev: ListItem<U> | null = null | |
| 160 | let i = 0 | |
| 161 | for (let cur = this.head; cur != null; cur = cur.next) { | |
| 162 | const item = createItem(fn(cur.data, i++, this)) | |
| 163 | item.prev = prev | |
| 164 | if (prev) | |
| 165 | prev.next = item | |
| 166 | else | |
| 167 | result.head = item | |
| 168 | prev = item | |
| 169 | } | |
| 170 | result.tail = prev | |
| 171 | return result | |
| 172 | } | |
| 173 | ||
| 174 | // eslint-disable-next-line pickier/no-unused-vars | |
| 175 | filter(fn: (item: T, index: number, list: CssList<T>) => boolean): CssList<T> { | |
| 176 | const result = new CssList<T>() | |
| 177 | let prev: ListItem<T> | null = null | |
| 178 | let i = 0 | |
| 179 | for (let cur = this.head; cur != null; cur = cur.next) { | |
| 180 | if (fn(cur.data, i++, this)) { | |
| 181 | const item = createItem(cur.data) | |
| 182 | item.prev = prev | |
| 183 | if (prev) | |
| 184 | prev.next = item | |
| 185 | else | |
| 186 | result.head = item | |
| 187 | prev = item | |
| 188 | } | |
| 189 | } | |
| 190 | result.tail = prev | |
| 191 | return result | |
| 192 | } | |
| 193 | ||
| 194 | clear(): void { | |
| 195 | // Unlink every item so consumers that retained a reference via | |
| 196 | // `forEach`/`map` callbacks don't keep the rest of the chain alive | |
| 197 | // through the prev/next pointers (which would defeat GC and let | |
| 198 | // mutations on a "cleared" list surprise other code holding handles). | |
| 199 | let cur = this.head | |
| 200 | while (cur) { | |
| 201 | const nxt = cur.next | |
| 202 | cur.prev = null | |
| 203 | cur.next = null | |
| 204 | cur = nxt | |
| 205 | } | |
| 206 | this.head = null | |
| 207 | this.tail = null | |
| 208 | } | |
| 209 | ||
| 210 | copy(): CssList<T> { | |
| 211 | const result = new CssList<T>() | |
| 212 | let prev: ListItem<T> | null = null | |
| 213 | for (let cur = this.head; cur != null; cur = cur.next) { | |
| 214 | const item = createItem(cur.data) | |
| 215 | item.prev = prev | |
| 216 | if (prev) | |
| 217 | prev.next = item | |
| 218 | else | |
| 219 | result.head = item | |
| 220 | prev = item | |
| 221 | } | |
| 222 | result.tail = prev | |
| 223 | return result | |
| 224 | } | |
| 225 | ||
| 226 | prepend(item: ListItem<T>): this { | |
| 227 | return this.insert(item, this.head) | |
| 228 | } | |
| 229 | ||
| 230 | prependData(data: T): this { | |
| 231 | return this.insert(createItem(data), this.head) | |
| 232 | } | |
| 233 | ||
| 234 | append(item: ListItem<T>): this { | |
| 235 | return this.insert(item, null) | |
| 236 | } | |
| 237 | ||
| 238 | appendData(data: T): this { | |
| 239 | return this.insert(createItem(data), null) | |
| 240 | } | |
| 241 | ||
| 242 | /** Insert `item` before `before`, or at tail if `before` is null/undefined. */ | |
| 243 | insert(item: ListItem<T>, before: ListItem<T> | null | undefined = null): this { | |
| 244 | if (before != null) { | |
| 245 | this.updateCursors(before.prev, item, before, item) | |
| 246 | if (before.prev === null) { | |
| 247 | if (this.head !== before) | |
| 248 | throw new Error('before doesn\'t belong to list') | |
| 249 | this.head = item | |
| 250 | before.prev = item | |
| 251 | item.next = before | |
| 252 | this.updateCursors(null, item, null, null) | |
| 253 | } | |
| 254 | else { | |
| 255 | before.prev.next = item | |
| 256 | item.prev = before.prev | |
| 257 | before.prev = item | |
| 258 | item.next = before | |
| 259 | } | |
| 260 | } | |
| 261 | else { | |
| 262 | this.updateCursors(this.tail, item, null, item) | |
| 263 | if (this.tail !== null) { | |
| 264 | this.tail.next = item | |
| 265 | item.prev = this.tail | |
| 266 | this.tail = item | |
| 267 | } | |
| 268 | else { | |
| 269 | this.head = item | |
| 270 | this.tail = item | |
| 271 | } | |
| 272 | } | |
| 273 | return this | |
| 274 | } | |
| 275 | ||
| 276 | insertData(data: T, before: ListItem<T> | null = null): this { | |
| 277 | return this.insert(createItem(data), before) | |
| 278 | } | |
| 279 | ||
| 280 | remove(item: ListItem<T>): ListItem<T> { | |
| 281 | this.updateCursors(item, item.prev, item, item.next) | |
| 282 | if (item.prev !== null) | |
| 283 | item.prev.next = item.next | |
| 284 | else if (this.head === item) | |
| 285 | this.head = item.next | |
| 286 | else | |
| 287 | throw new Error('item doesn\'t belong to list') | |
| 288 | if (item.next !== null) | |
| 289 | item.next.prev = item.prev | |
| 290 | else if (this.tail === item) | |
| 291 | this.tail = item.prev | |
| 292 | else | |
| 293 | throw new Error('item doesn\'t belong to list') | |
| 294 | item.prev = null | |
| 295 | item.next = null | |
| 296 | return item | |
| 297 | } | |
| 298 | ||
| 299 | push(data: T): void { | |
| 300 | this.insert(createItem(data), null) | |
| 301 | } | |
| 302 | ||
| 303 | pop(): ListItem<T> | null { | |
| 304 | if (this.tail === null) | |
| 305 | return null | |
| 306 | return this.remove(this.tail) | |
| 307 | } | |
| 308 | ||
| 309 | unshift(data: T): void { | |
| 310 | this.prependData(data) | |
| 311 | } | |
| 312 | ||
| 313 | shift(): ListItem<T> | null { | |
| 314 | if (this.head === null) | |
| 315 | return null | |
| 316 | return this.remove(this.head) | |
| 317 | } | |
| 318 | ||
| 319 | prependList(list: CssList<T>): this { | |
| 320 | return this.insertList(list, this.head) | |
| 321 | } | |
| 322 | ||
| 323 | appendList(list: CssList<T>): this { | |
| 324 | return this.insertList(list, null) | |
| 325 | } | |
| 326 | ||
| 327 | insertList(list: CssList<T>, before: ListItem<T> | null = null): this { | |
| 328 | if (list.head === null) | |
| 329 | return this | |
| 330 | if (before !== null) { | |
| 331 | this.updateCursors(before.prev, list.tail, before, list.head) | |
| 332 | if (before.prev !== null) { | |
| 333 | before.prev.next = list.head | |
| 334 | list.head.prev = before.prev | |
| 335 | } | |
| 336 | else { | |
| 337 | this.head = list.head | |
| 338 | } | |
| 339 | before.prev = list.tail | |
| 340 | list.tail!.next = before | |
| 341 | } | |
| 342 | else { | |
| 343 | this.updateCursors(this.tail, list.tail, null, list.head) | |
| 344 | if (this.tail !== null) { | |
| 345 | this.tail.next = list.head | |
| 346 | list.head.prev = this.tail | |
| 347 | } | |
| 348 | else { | |
| 349 | this.head = list.head | |
| 350 | } | |
| 351 | this.tail = list.tail | |
| 352 | } | |
| 353 | list.head = null | |
| 354 | list.tail = null | |
| 355 | return this | |
| 356 | } | |
| 357 | ||
| 358 | replace(oldItem: ListItem<T>, newItemOrList: ListItem<T> | CssList<T>): void { | |
| 359 | if (newItemOrList instanceof CssList) { | |
| 360 | this.insertList(newItemOrList, oldItem) | |
| 361 | this.remove(oldItem) | |
| 362 | } | |
| 363 | else { | |
| 364 | this.insert(newItemOrList, oldItem) | |
| 365 | this.remove(oldItem) | |
| 366 | } | |
| 367 | } | |
| 368 | } | |
| 369 | ||
| 370 | /** | |
| 371 | * Convenience constructor matching `csstree.List()` (callable without `new`). | |
| 372 | */ | |
| 373 | export function makeList<T>(): CssList<T> { | |
| 374 | return new CssList<T>() | |
| 375 | } | |
packages/toolkit/src/parse/parser/index.tsadded+1340-0
| @@ -0,0 +1,1340 @@ | ||
| 1 | /** | |
| 2 | * CSS parser. Recursive-descent over the tokenizer output. Produces an | |
| 3 | * AST shape compatible with css-tree v3 (so consumer walker callbacks | |
| 4 | * remain unchanged). | |
| 5 | * | |
| 6 | * Implemented contexts: | |
| 7 | * stylesheet | declarationList | declaration | atrule | atrulePrelude | |
| 8 | * selectorList | selector | rule | block | value | mediaQuery | |
| 9 | * mediaQueryList | raw | |
| 10 | * | |
| 11 | * Options recognised (all match css-tree): | |
| 12 | * parseValue, parseAtrulePrelude, parseCustomProperty, parseRulePrelude, | |
| 13 | * filename, positions, context, onParseError | |
| 14 | */ | |
| 15 | ||
| 16 | import type { | |
| 17 | Atrule, | |
| 18 | AtrulePrelude, | |
| 19 | AttributeSelector, | |
| 20 | Block, | |
| 21 | ClassSelector, | |
| 22 | Combinator, | |
| 23 | CommentNode, | |
| 24 | CssLocation, | |
| 25 | CssNode, | |
| 26 | Declaration, | |
| 27 | DeclarationList, | |
| 28 | Identifier, | |
| 29 | IdSelector, | |
| 30 | ParenthesesNode, | |
| 31 | ParseContext, | |
| 32 | ParseOptions, | |
| 33 | PseudoClassSelector, | |
| 34 | PseudoElementSelector, | |
| 35 | Raw, | |
| 36 | Rule, | |
| 37 | Selector, | |
| 38 | SelectorList, | |
| 39 | StringNode, | |
| 40 | StyleSheet, | |
| 41 | TypeSelector, | |
| 42 | UrlNode, | |
| 43 | Value, | |
| 44 | } from '../types' | |
| 45 | import { CssList } from '../list' | |
| 46 | import type { Token } from '../tokenizer' | |
| 47 | import { decodeName, decodeString, Tokenizer, TokenType } from '../tokenizer' | |
| 48 | ||
| 49 | interface ParserState { | |
| 50 | source: string | |
| 51 | // Parallel typed-array storage from the tokenizer. Direct reads avoid | |
| 52 | // per-token Object allocation in the hot loop. | |
| 53 | types: Uint8Array | |
| 54 | starts: Uint32Array | |
| 55 | ends: Uint32Array | |
| 56 | count: number | |
| 57 | /** | |
| 58 | * Effective end of token stream visible to the parser. Equals `count` for | |
| 59 | * normal parsing; gets temporarily lowered when we recurse into a | |
| 60 | * sub-range (e.g. a declaration's value tokens) so the parser sees | |
| 61 | * "virtual EOF" at the right boundary without re-tokenizing. | |
| 62 | */ | |
| 63 | end: number | |
| 64 | pos: number | |
| 65 | positions: boolean | |
| 66 | filename: string | undefined | |
| 67 | parseValue: boolean | |
| 68 | parseAtrulePrelude: boolean | |
| 69 | parseCustomProperty: boolean | |
| 70 | parseRulePrelude: boolean | |
| 71 | // eslint-disable-next-line pickier/no-unused-vars | |
| 72 | onParseError?: (error: SyntaxError, fallbackNode: CssNode) => void | |
| 73 | tokenizer: Tokenizer | |
| 74 | } | |
| 75 | ||
| 76 | function makeState(source: string, options: ParseOptions): ParserState { | |
| 77 | const tokenizer = new Tokenizer(source) | |
| 78 | return { | |
| 79 | source, | |
| 80 | types: tokenizer.types, | |
| 81 | starts: tokenizer.starts, | |
| 82 | ends: tokenizer.ends, | |
| 83 | count: tokenizer.count, | |
| 84 | end: tokenizer.count, | |
| 85 | pos: 0, | |
| 86 | positions: options.positions ?? false, | |
| 87 | filename: options.filename, | |
| 88 | parseValue: options.parseValue ?? true, | |
| 89 | parseAtrulePrelude: options.parseAtrulePrelude ?? true, | |
| 90 | parseCustomProperty: options.parseCustomProperty ?? false, | |
| 91 | parseRulePrelude: options.parseRulePrelude ?? true, | |
| 92 | onParseError: options.onParseError, | |
| 93 | tokenizer, | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | // ----- token-stream helpers ----- | |
| 98 | // Read directly from the parallel typed arrays. `peek(s)` materialises a | |
| 99 | // `Token` view object lazily — most parser code only needs `.type` / | |
| 100 | // `.start` / `.end` which the helpers below expose without an alloc. | |
| 101 | ||
| 102 | function peekType(s: ParserState): TokenType { | |
| 103 | return s.pos < s.end ? (s.types[s.pos]! as TokenType) : TokenType.EOF | |
| 104 | } | |
| 105 | ||
| 106 | function peek(s: ParserState): Token { | |
| 107 | const i = s.pos | |
| 108 | if (i >= s.end) | |
| 109 | return { type: TokenType.EOF, start: s.starts[i]! ?? s.source.length, end: s.ends[i]! ?? s.source.length } | |
| 110 | return { type: s.types[i]! as TokenType, start: s.starts[i]!, end: s.ends[i]! } | |
| 111 | } | |
| 112 | ||
| 113 | function consume(s: ParserState): Token { | |
| 114 | const i = s.pos++ | |
| 115 | if (i >= s.end) | |
| 116 | return { type: TokenType.EOF, start: s.starts[i]! ?? s.source.length, end: s.ends[i]! ?? s.source.length } | |
| 117 | return { type: s.types[i]! as TokenType, start: s.starts[i]!, end: s.ends[i]! } | |
| 118 | } | |
| 119 | ||
| 120 | function tokenSlice(s: ParserState, t: Token): string { | |
| 121 | return s.source.slice(t.start, t.end) | |
| 122 | } | |
| 123 | ||
| 124 | function skipWhitespace(s: ParserState): void { | |
| 125 | const types = s.types | |
| 126 | const end = s.end | |
| 127 | while (s.pos < end) { | |
| 128 | const t = types[s.pos]! | |
| 129 | if (t === TokenType.WhiteSpace || t === TokenType.Comment) | |
| 130 | s.pos++ | |
| 131 | else | |
| 132 | break | |
| 133 | } | |
| 134 | } | |
| 135 | ||
| 136 | function skipWhitespaceOnly(s: ParserState): void { | |
| 137 | const types = s.types | |
| 138 | const end = s.end | |
| 139 | while (s.pos < end && types[s.pos]! === TokenType.WhiteSpace) | |
| 140 | s.pos++ | |
| 141 | } | |
| 142 | ||
| 143 | function loc(s: ParserState, startTok: Token, endTok: Token): CssLocation | null { | |
| 144 | if (!s.positions) | |
| 145 | return null | |
| 146 | const start = s.tokenizer.locate(startTok.start) | |
| 147 | const end = s.tokenizer.locate(endTok.end) | |
| 148 | return { | |
| 149 | source: s.filename ?? '<unknown>', | |
| 150 | start: { offset: startTok.start, line: start.line, column: start.column }, | |
| 151 | end: { offset: endTok.end, line: end.line, column: end.column }, | |
| 152 | } | |
| 153 | } | |
| 154 | ||
| 155 | function emptyLoc(s: ParserState): CssLocation | null { | |
| 156 | if (!s.positions) | |
| 157 | return null | |
| 158 | const t = peek(s) | |
| 159 | const p = s.tokenizer.locate(t.start) | |
| 160 | return { | |
| 161 | source: s.filename ?? '<unknown>', | |
| 162 | start: { offset: t.start, line: p.line, column: p.column }, | |
| 163 | end: { offset: t.start, line: p.line, column: p.column }, | |
| 164 | } | |
| 165 | } | |
| 166 | ||
| 167 | // ----- node constructors ----- | |
| 168 | ||
| 169 | function rawNode(value: string, start: Token, end: Token, s: ParserState): Raw { | |
| 170 | return { type: 'Raw', value, loc: loc(s, start, end) } | |
| 171 | } | |
| 172 | ||
| 173 | function newList<T>(): CssList<T> { | |
| 174 | return new CssList<T>() | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Report a parse problem to the consumer. Always returns the supplied | |
| 179 | * fallback node so call sites can `return reportError(…, raw)`. The | |
| 180 | * `onParseError` hook lets consumers (linters, IDEs) surface | |
| 181 | * the parser's recovery substitutions. | |
| 182 | */ | |
| 183 | function reportError(s: ParserState, message: string, at: Token, fallback: CssNode): CssNode { | |
| 184 | if (s.onParseError) { | |
| 185 | const { line, column } = s.tokenizer.locate(at.start) | |
| 186 | const err = new SyntaxError(`${s.filename ?? '<input>'}:${line}:${column}: ${message}`) as SyntaxError & { line: number, column: number, offset: number } | |
| 187 | err.line = line | |
| 188 | err.column = column | |
| 189 | err.offset = at.start | |
| 190 | s.onParseError(err, fallback) | |
| 191 | } | |
| 192 | return fallback | |
| 193 | } | |
| 194 | ||
| 195 | // ----- top-level dispatch ----- | |
| 196 | ||
| 197 | export function parse(source: string, options: ParseOptions = {}): CssNode { | |
| 198 | const s = makeState(source, options) | |
| 199 | const ctx: ParseContext = options.context ?? 'stylesheet' | |
| 200 | ||
| 201 | switch (ctx) { | |
| 202 | case 'stylesheet': return parseStyleSheet(s) | |
| 203 | case 'atrule': return parseAtrule(s) ?? makeEmptyStyleSheet(s) | |
| 204 | case 'atrulePrelude': return parseAtrulePreludeContext(s) | |
| 205 | case 'mediaQuery': | |
| 206 | case 'mediaQueryList': return parseRawAsValue(s) | |
| 207 | case 'rule': return parseRule(s) | |
| 208 | case 'selectorList': return parseSelectorList(s) | |
| 209 | case 'selector': return parseSelector(s) | |
| 210 | case 'block': return parseBlock(s) | |
| 211 | case 'declarationList': return parseDeclarationList(s) | |
| 212 | case 'declaration': return parseDeclaration(s) ?? makeEmptyStyleSheet(s) | |
| 213 | case 'value': return parseValue(s) | |
| 214 | case 'raw': return parseRawAsValue(s) | |
| 215 | default: return parseStyleSheet(s) | |
| 216 | } | |
| 217 | } | |
| 218 | ||
| 219 | function makeEmptyStyleSheet(s: ParserState): StyleSheet { | |
| 220 | return { type: 'StyleSheet', children: newList<CssNode>(), loc: emptyLoc(s) } | |
| 221 | } | |
| 222 | ||
| 223 | function parseRawAsValue(s: ParserState): Raw { | |
| 224 | const start = peek(s) | |
| 225 | let end = start | |
| 226 | while (peekType(s) !== TokenType.EOF) { | |
| 227 | end = consume(s) | |
| 228 | } | |
| 229 | return { type: 'Raw', value: s.source.slice(start.start, end.end), loc: loc(s, start, end) } | |
| 230 | } | |
| 231 | ||
| 232 | // ----- stylesheet ----- | |
| 233 | ||
| 234 | function parseStyleSheet(s: ParserState): StyleSheet { | |
| 235 | const startTok = peek(s) | |
| 236 | const children = newList<CssNode>() | |
| 237 | const types = s.types | |
| 238 | const end = s.end | |
| 239 | // Hot loop: read type from the typed array directly to avoid the | |
| 240 | // per-iteration Token-object alloc that `peek()` would do. | |
| 241 | while (s.pos < end) { | |
| 242 | const t = types[s.pos]! | |
| 243 | if (t === TokenType.WhiteSpace) { | |
| 244 | s.pos++ | |
| 245 | continue | |
| 246 | } | |
| 247 | if (t === TokenType.Comment) { | |
| 248 | const tok: Token = { type: TokenType.Comment, start: s.starts[s.pos]!, end: s.ends[s.pos]! } | |
| 249 | const node = makeComment(s, tok) | |
| 250 | s.pos++ | |
| 251 | children.appendData(node) | |
| 252 | continue | |
| 253 | } | |
| 254 | if (t === TokenType.EOF) | |
| 255 | break | |
| 256 | if (t === TokenType.CDO || t === TokenType.CDC) { | |
| 257 | s.pos++ | |
| 258 | continue | |
| 259 | } | |
| 260 | if (t === TokenType.AtKeyword) { | |
| 261 | const at = parseAtrule(s) | |
| 262 | if (at) | |
| 263 | children.appendData(at) | |
| 264 | continue | |
| 265 | } | |
| 266 | // Top-level safety: a stray `}` (mismatched braces in malformed input) | |
| 267 | // would otherwise let parseRule return without advancing — guard against | |
| 268 | // the infinite loop here rather than embedding the check inside parseRule. | |
| 269 | if (t === TokenType.RightCurlyBracket) { | |
| 270 | s.pos++ | |
| 271 | continue | |
| 272 | } | |
| 273 | const before = s.pos | |
| 274 | const r = parseRule(s) | |
| 275 | children.appendData(r) | |
| 276 | if (s.pos === before) | |
| 277 | s.pos++ | |
| 278 | } | |
| 279 | const lastIdx = s.end - 1 | |
| 280 | const endTok: Token = lastIdx >= 0 | |
| 281 | ? { type: s.types[lastIdx]! as TokenType, start: s.starts[lastIdx]!, end: s.ends[lastIdx]! } | |
| 282 | : { type: TokenType.EOF, start: 0, end: 0 } | |
| 283 | return { type: 'StyleSheet', children, loc: loc(s, startTok, endTok) } | |
| 284 | } | |
| 285 | ||
| 286 | function makeComment(s: ParserState, t: Token): CommentNode { | |
| 287 | return { type: 'Comment', value: s.source.slice(t.start + 2, t.end - 2), loc: loc(s, t, t) } | |
| 288 | } | |
| 289 | ||
| 290 | // ----- at-rule ----- | |
| 291 | ||
| 292 | function parseAtrule(s: ParserState): Atrule | null { | |
| 293 | const startTok = peek(s) | |
| 294 | if (startTok.type !== TokenType.AtKeyword) | |
| 295 | return null | |
| 296 | s.pos++ | |
| 297 | const name = lowerIfNeeded(s.source, startTok.start + 1, startTok.end) | |
| 298 | ||
| 299 | // Collect prelude tokens until ; or { — pre-scan for the boundary. | |
| 300 | skipWhitespace(s) | |
| 301 | const preludeStartPos = s.pos | |
| 302 | const preludeStartTok = peek(s) | |
| 303 | const types = s.types | |
| 304 | const tend = s.end | |
| 305 | let scanPos = preludeStartPos | |
| 306 | let preludeEndPos = preludeStartPos | |
| 307 | while (scanPos < tend) { | |
| 308 | const t = types[scanPos]! | |
| 309 | if (t === TokenType.EOF || t === TokenType.Semicolon || t === TokenType.LeftCurlyBracket) | |
| 310 | break | |
| 311 | if (t !== TokenType.WhiteSpace && t !== TokenType.Comment) | |
| 312 | preludeEndPos = scanPos + 1 | |
| 313 | scanPos++ | |
| 314 | } | |
| 315 | ||
| 316 | let prelude: AtrulePrelude | Raw | null = null | |
| 317 | if (preludeEndPos > preludeStartPos) { | |
| 318 | const preludeStartByte = s.starts[preludeStartPos]! | |
| 319 | const preludeEndByte = s.ends[preludeEndPos - 1]! | |
| 320 | const preludeEndTok: Token = { type: types[preludeEndPos - 1]! as TokenType, start: s.starts[preludeEndPos - 1]!, end: preludeEndByte } | |
| 321 | const raw = s.source.slice(preludeStartByte, preludeEndByte).trim() | |
| 322 | if (raw.length > 0) { | |
| 323 | if (s.parseAtrulePrelude) { | |
| 324 | try { | |
| 325 | // Run the prelude parser on the existing token stream — temporarily | |
| 326 | // restrict `s.end` to the prelude boundary so the loop sees EOF. | |
| 327 | const savedEnd = s.end | |
| 328 | s.end = preludeEndPos | |
| 329 | s.pos = preludeStartPos | |
| 330 | const children = newList<CssNode>() | |
| 331 | while (peekType(s) !== TokenType.EOF) { | |
| 332 | const node = parseValueChild(s) | |
| 333 | if (node) | |
| 334 | children.appendData(node) | |
| 335 | } | |
| 336 | promoteRatiosDeep(children) | |
| 337 | s.end = savedEnd | |
| 338 | // pos lands on the ; or { boundary so parseAtrule's caller can | |
| 339 | // dispatch on the next token without re-walking. | |
| 340 | s.pos = scanPos | |
| 341 | if (children.isEmpty) | |
| 342 | prelude = rawNode(raw, preludeStartTok, preludeEndTok, s) | |
| 343 | else | |
| 344 | prelude = { type: 'AtrulePrelude', children, loc: null } | |
| 345 | } | |
| 346 | catch (err) { | |
| 347 | const fallback = rawNode(raw, preludeStartTok, preludeEndTok, s) | |
| 348 | reportError(s, `Failed to parse @${name} prelude: ${(err as Error).message}`, preludeStartTok, fallback) | |
| 349 | prelude = fallback | |
| 350 | s.pos = scanPos | |
| 351 | } | |
| 352 | } | |
| 353 | else { | |
| 354 | prelude = rawNode(raw, preludeStartTok, preludeEndTok, s) | |
| 355 | s.pos = scanPos | |
| 356 | } | |
| 357 | } | |
| 358 | else { | |
| 359 | s.pos = scanPos | |
| 360 | } | |
| 361 | } | |
| 362 | else { | |
| 363 | s.pos = scanPos | |
| 364 | } | |
| 365 | ||
| 366 | let block: Block | null = null | |
| 367 | if (peekType(s) === TokenType.Semicolon) { | |
| 368 | consume(s) | |
| 369 | } | |
| 370 | else if (peekType(s) === TokenType.LeftCurlyBracket) { | |
| 371 | block = parseBlock(s, AT_RULES_WITH_NESTED_RULES.has(name)) | |
| 372 | } | |
| 373 | ||
| 374 | return { type: 'Atrule', name, prelude, block, loc: loc(s, startTok, peek(s)) } | |
| 375 | } | |
| 376 | ||
| 377 | /** | |
| 378 | * Pseudo-class names whose argument is a selector list and needs to be | |
| 379 | * re-parsed by `parseSelectorList` (rather than collected as a value). | |
| 380 | */ | |
| 381 | const SELECTOR_LIST_PSEUDOS_PARSE: ReadonlySet<string> = new Set([ | |
| 382 | 'is', 'not', 'where', 'has', 'matches', '-moz-any', '-webkit-any', | |
| 383 | ]) | |
| 384 | ||
| 385 | /** | |
| 386 | * At-rules whose block contains nested *rules* (selector { … }), as | |
| 387 | * opposed to direct declarations. Used to switch parser mode inside the | |
| 388 | * block. Lives here (not on _collections) because it's a parser concern. | |
| 389 | */ | |
| 390 | // eslint-disable-next-line pickier/no-unused-vars | |
| 391 | const AT_RULES_WITH_NESTED_RULES: ReadonlySet<string> = new Set([ | |
| 392 | 'media', | |
| 393 | 'supports', | |
| 394 | 'document', | |
| 395 | 'layer', | |
| 396 | 'container', | |
| 397 | 'scope', | |
| 398 | 'starting-style', | |
| 399 | '-moz-document', | |
| 400 | // keyframes — children are rules with `from` / `to` / `50%` preludes | |
| 401 | 'keyframes', | |
| 402 | '-webkit-keyframes', | |
| 403 | '-moz-keyframes', | |
| 404 | '-o-keyframes', | |
| 405 | ]) | |
| 406 | ||
| 407 | function parseAtrulePreludeContext(s: ParserState): AtrulePrelude { | |
| 408 | const startTok = peek(s) | |
| 409 | const children = newList<CssNode>() | |
| 410 | while (peekType(s) !== TokenType.EOF) { | |
| 411 | const t = peek(s) | |
| 412 | if (t.type === TokenType.WhiteSpace) { | |
| 413 | s.pos++ | |
| 414 | continue | |
| 415 | } | |
| 416 | const node = parseValueChild(s) | |
| 417 | if (node) | |
| 418 | children.appendData(node) | |
| 419 | } | |
| 420 | return { type: 'AtrulePrelude', children, loc: loc(s, startTok, peek(s)) } | |
| 421 | } | |
| 422 | ||
| 423 | // ----- rule ----- | |
| 424 | ||
| 425 | function parseRule(s: ParserState): Rule { | |
| 426 | const startTok = peek(s) | |
| 427 | ||
| 428 | let prelude: SelectorList | Raw | |
| 429 | if (s.parseRulePrelude) { | |
| 430 | // Parse the selector list straight from the existing token stream — | |
| 431 | // `parseSelectorList` stops at `,`-terminated boundaries internally and | |
| 432 | // at the leading `{` of the block, so no re-tokenization is needed. | |
| 433 | prelude = parseSelectorList(s) | |
| 434 | } | |
| 435 | else { | |
| 436 | // Raw fallback: collect tokens up to `{` and slice the text. | |
| 437 | const preludeStart = peek(s) | |
| 438 | let preludeEnd = preludeStart | |
| 439 | while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.LeftCurlyBracket) | |
| 440 | preludeEnd = consume(s) | |
| 441 | const preludeText = s.source.slice(preludeStart.start, preludeEnd.end).trim() | |
| 442 | prelude = rawNode(preludeText, preludeStart, preludeEnd, s) | |
| 443 | } | |
| 444 | ||
| 445 | // Rules support CSS Nesting (`.foo { color: red; & .bar { color: blue } }`) | |
| 446 | // — `allowNested` flips on the lookahead that distinguishes a nested | |
| 447 | // rule from a declaration with the same opening tokens. | |
| 448 | const block = parseBlock(s, true) | |
| 449 | return { type: 'Rule', prelude, block, loc: loc(s, startTok, peek(s)) } | |
| 450 | } | |
| 451 | ||
| 452 | // ----- block ----- | |
| 453 | ||
| 454 | function parseBlock(s: ParserState, allowNested: boolean = false): Block { | |
| 455 | const startTok = peek(s) | |
| 456 | if (peekType(s) !== TokenType.LeftCurlyBracket) | |
| 457 | return { type: 'Block', children: newList<CssNode>(), loc: emptyLoc(s) } | |
| 458 | s.pos++ // { | |
| 459 | const children = newList<CssNode>() | |
| 460 | const types = s.types | |
| 461 | const end = s.end | |
| 462 | while (s.pos < end) { | |
| 463 | const t = types[s.pos]! | |
| 464 | if (t === TokenType.RightCurlyBracket || t === TokenType.EOF) | |
| 465 | break | |
| 466 | if (t === TokenType.WhiteSpace) { | |
| 467 | s.pos++ | |
| 468 | continue | |
| 469 | } | |
| 470 | if (t === TokenType.Comment) { | |
| 471 | const tok: Token = { type: TokenType.Comment, start: s.starts[s.pos]!, end: s.ends[s.pos]! } | |
| 472 | s.pos++ | |
| 473 | children.appendData(makeComment(s, tok)) | |
| 474 | continue | |
| 475 | } | |
| 476 | if (t === TokenType.AtKeyword) { | |
| 477 | const at = parseAtrule(s) | |
| 478 | if (at) | |
| 479 | children.appendData(at) | |
| 480 | continue | |
| 481 | } | |
| 482 | if (allowNested && lookahead(s, isCurlyAheadBeforeSemicolon)) { | |
| 483 | const r = parseRule(s) | |
| 484 | children.appendData(r) | |
| 485 | continue | |
| 486 | } | |
| 487 | const decl = parseDeclaration(s) | |
| 488 | if (decl) | |
| 489 | children.appendData(decl) | |
| 490 | if (s.pos < end && types[s.pos]! === TokenType.Semicolon) | |
| 491 | s.pos++ | |
| 492 | } | |
| 493 | if (peekType(s) === TokenType.RightCurlyBracket) | |
| 494 | s.pos++ | |
| 495 | return { type: 'Block', children, loc: loc(s, startTok, peek(s)) } | |
| 496 | } | |
| 497 | ||
| 498 | function lookahead(s: ParserState, predicate: (s: ParserState) => boolean): boolean { | |
| 499 | const saved = s.pos | |
| 500 | const r = predicate(s) | |
| 501 | s.pos = saved | |
| 502 | return r | |
| 503 | } | |
| 504 | ||
| 505 | function isCurlyAheadBeforeSemicolon(s: ParserState): boolean { | |
| 506 | const types = s.types | |
| 507 | const end = s.end | |
| 508 | while (s.pos < end) { | |
| 509 | const t = types[s.pos]! | |
| 510 | if (t === TokenType.LeftCurlyBracket) | |
| 511 | return true | |
| 512 | if (t === TokenType.Semicolon || t === TokenType.RightCurlyBracket || t === TokenType.EOF) | |
| 513 | return false | |
| 514 | s.pos++ | |
| 515 | } | |
| 516 | return false | |
| 517 | } | |
| 518 | ||
| 519 | // ----- declarationList ----- | |
| 520 | ||
| 521 | function parseDeclarationList(s: ParserState): DeclarationList { | |
| 522 | const startTok = peek(s) | |
| 523 | const children = newList<CssNode>() | |
| 524 | while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightCurlyBracket) { | |
| 525 | const t = peek(s) | |
| 526 | if (t.type === TokenType.WhiteSpace) { | |
| 527 | s.pos++ | |
| 528 | continue | |
| 529 | } | |
| 530 | if (t.type === TokenType.Comment) { | |
| 531 | children.appendData(makeComment(s, consume(s))) | |
| 532 | continue | |
| 533 | } | |
| 534 | if (t.type === TokenType.Semicolon) { | |
| 535 | s.pos++ | |
| 536 | continue | |
| 537 | } | |
| 538 | if (t.type === TokenType.AtKeyword) { | |
| 539 | const at = parseAtrule(s) | |
| 540 | if (at) | |
| 541 | children.appendData(at) | |
| 542 | continue | |
| 543 | } | |
| 544 | const decl = parseDeclaration(s) | |
| 545 | if (decl) | |
| 546 | children.appendData(decl) | |
| 547 | if (peekType(s) === TokenType.Semicolon) | |
| 548 | consume(s) | |
| 549 | } | |
| 550 | return { type: 'DeclarationList', children, loc: loc(s, startTok, peek(s)) } | |
| 551 | } | |
| 552 | ||
| 553 | // ----- declaration ----- | |
| 554 | ||
| 555 | function parseDeclaration(s: ParserState): Declaration | null { | |
| 556 | skipWhitespace(s) | |
| 557 | const startTok = peek(s) | |
| 558 | if (startTok.type !== TokenType.Ident && startTok.type !== TokenType.Hash && !(startTok.type === TokenType.Delim && s.source[startTok.start] === '*' && s.source[startTok.start + 1] === ' ')) { | |
| 559 | if (startTok.type === TokenType.Delim && s.source[startTok.start] === '*') { | |
| 560 | // ie *property | |
| 561 | s.pos++ | |
| 562 | } | |
| 563 | else { | |
| 564 | reportError(s, `Expected property name`, startTok, { type: 'Raw', value: '', loc: null }) | |
| 565 | const types = s.types | |
| 566 | const end = s.end | |
| 567 | while (s.pos < end) { | |
| 568 | const t = types[s.pos]! | |
| 569 | if (t === TokenType.EOF || t === TokenType.Semicolon || t === TokenType.RightCurlyBracket) | |
| 570 | break | |
| 571 | s.pos++ | |
| 572 | } | |
| 573 | return null | |
| 574 | } | |
| 575 | } | |
| 576 | const propStart = s.starts[s.pos]! | |
| 577 | const propEnd = s.ends[s.pos]! | |
| 578 | s.pos++ | |
| 579 | const property = s.source.slice(propStart, propEnd) | |
| 580 | ||
| 581 | skipWhitespaceOnly(s) | |
| 582 | if (peekType(s) !== TokenType.Colon) { | |
| 583 | reportError(s, `Expected ':' after property "${property}"`, peek(s), { type: 'Raw', value: '', loc: null }) | |
| 584 | const types = s.types | |
| 585 | const end = s.end | |
| 586 | while (s.pos < end) { | |
| 587 | const t = types[s.pos]! | |
| 588 | if (t === TokenType.EOF || t === TokenType.Semicolon || t === TokenType.RightCurlyBracket) | |
| 589 | break | |
| 590 | s.pos++ | |
| 591 | } | |
| 592 | return null | |
| 593 | } | |
| 594 | s.pos++ // : | |
| 595 | ||
| 596 | skipWhitespaceOnly(s) | |
| 597 | // Pre-scan the value token range. We track: | |
| 598 | // - lastNonWsPos: index after the last non-whitespace value token | |
| 599 | // (where the parsable value ends — excludes trailing whitespace/!important) | |
| 600 | // - importantPos: position of the `important` ident if !important detected | |
| 601 | // - endPos: stop position (Semicolon/RightCurly/EOF) | |
| 602 | // This avoids re-tokenizing the value text — the parser walks the | |
| 603 | // already-tokenised stream directly. | |
| 604 | const types = s.types | |
| 605 | const starts = s.starts | |
| 606 | const tend = s.end | |
| 607 | const valueStartPos = s.pos | |
| 608 | let scanPos = valueStartPos | |
| 609 | let lastNonWsPos = valueStartPos | |
| 610 | let importantIdentPos = -1 | |
| 611 | let bangPos = -1 | |
| 612 | while (scanPos < tend) { | |
| 613 | const t = types[scanPos]! | |
| 614 | if (t === TokenType.EOF || t === TokenType.Semicolon || t === TokenType.RightCurlyBracket) | |
| 615 | break | |
| 616 | if (t === TokenType.Delim && s.source.charCodeAt(starts[scanPos]!) === 33 /* ! */) { | |
| 617 | // potential !important — peek next non-ws | |
| 618 | let look = scanPos + 1 | |
| 619 | while (look < tend && types[look]! === TokenType.WhiteSpace) look++ | |
| 620 | if (look < tend && types[look]! === TokenType.Ident) { | |
| 621 | const ts = starts[look]! | |
| 622 | const te = s.ends[look]! | |
| 623 | // Inline lowercase compare for "important" (9 chars) | |
| 624 | if (te - ts === 9 | |
| 625 | && (s.source.charCodeAt(ts) | 32) === 105 | |
| 626 | && (s.source.charCodeAt(ts + 1) | 32) === 109 | |
| 627 | && (s.source.charCodeAt(ts + 2) | 32) === 112 | |
| 628 | && (s.source.charCodeAt(ts + 3) | 32) === 111 | |
| 629 | && (s.source.charCodeAt(ts + 4) | 32) === 114 | |
| 630 | && (s.source.charCodeAt(ts + 5) | 32) === 116 | |
| 631 | && (s.source.charCodeAt(ts + 6) | 32) === 97 | |
| 632 | && (s.source.charCodeAt(ts + 7) | 32) === 110 | |
| 633 | && (s.source.charCodeAt(ts + 8) | 32) === 116) { | |
| 634 | bangPos = scanPos | |
| 635 | importantIdentPos = look | |
| 636 | scanPos = look + 1 | |
| 637 | continue | |
| 638 | } | |
| 639 | } | |
| 640 | } | |
| 641 | if (t !== TokenType.WhiteSpace && t !== TokenType.Comment) | |
| 642 | lastNonWsPos = scanPos + 1 | |
| 643 | scanPos++ | |
| 644 | } | |
| 645 | ||
| 646 | // The value range, parser-relative: [valueStartPos, valueEndPos). | |
| 647 | // `lastNonWsPos` already excludes both the trailing whitespace AND the | |
| 648 | // !important suffix because we don't update it inside the bang branch. | |
| 649 | void bangPos | |
| 650 | const valueEndPos = lastNonWsPos | |
| 651 | const importantTok: Token | null = importantIdentPos >= 0 | |
| 652 | ? { type: TokenType.Ident, start: starts[importantIdentPos]!, end: s.ends[importantIdentPos]! } | |
| 653 | : null | |
| 654 | ||
| 655 | // `property` came directly from a single Ident token's source slice, so | |
| 656 | // it can't have leading/trailing whitespace — the previous `.trim()` | |
| 657 | // here was load-bearing only when the slice path included whitespace | |
| 658 | // chars, which doesn't happen with the tokenizer. | |
| 659 | const isCustom = property.startsWith('--') | |
| 660 | let value: Value | Raw | |
| 661 | const valueStartByte = starts[valueStartPos]! | |
| 662 | const valueEndByte = valueEndPos > valueStartPos ? s.ends[valueEndPos - 1]! : valueStartByte | |
| 663 | const valueStartTok: Token = { type: types[valueStartPos]! as TokenType, start: valueStartByte, end: s.ends[valueStartPos]! } | |
| 664 | const valueEndTok: Token = valueEndPos > valueStartPos | |
| 665 | ? { type: types[valueEndPos - 1]! as TokenType, start: starts[valueEndPos - 1]!, end: valueEndByte } | |
| 666 | : valueStartTok | |
| 667 | ||
| 668 | if ((isCustom && !s.parseCustomProperty) || !s.parseValue) { | |
| 669 | const valueText = s.source.slice(valueStartByte, valueEndByte).trim() | |
| 670 | value = { type: 'Raw', value: valueText, loc: loc(s, valueStartTok, valueEndTok) } | |
| 671 | } | |
| 672 | else if (valueEndPos === valueStartPos) { | |
| 673 | value = { type: 'Value', children: newList<CssNode>(), loc: loc(s, valueStartTok, valueEndTok) } | |
| 674 | } | |
| 675 | else { | |
| 676 | // Run parseValueChildren on the existing token stream by temporarily | |
| 677 | // shrinking `s.end` to the value boundary so the parser sees a virtual | |
| 678 | // EOF at the right place. No re-tokenization required. | |
| 679 | const savedEnd = s.end | |
| 680 | s.end = valueEndPos | |
| 681 | s.pos = valueStartPos | |
| 682 | value = parseValueChildren(s) | |
| 683 | s.end = savedEnd | |
| 684 | } | |
| 685 | ||
| 686 | // Position the parser at the boundary token (Semicolon / RightCurly / EOF). | |
| 687 | // `scanPos` already advanced past !important when it was detected. | |
| 688 | s.pos = scanPos | |
| 689 | ||
| 690 | return { | |
| 691 | type: 'Declaration', | |
| 692 | property, | |
| 693 | important: importantTok ? true : false, | |
| 694 | value, | |
| 695 | loc: loc(s, startTok, peek(s)), | |
| 696 | } | |
| 697 | } | |
| 698 | ||
| 699 | // ----- value ----- | |
| 700 | ||
| 701 | function parseValue(s: ParserState): Value { | |
| 702 | return parseValueChildren(s) | |
| 703 | } | |
| 704 | ||
| 705 | function parseValueChildren(s: ParserState): Value { | |
| 706 | const startTok = peek(s) | |
| 707 | const children = newList<CssNode>() | |
| 708 | while (peekType(s) !== TokenType.EOF | |
| 709 | && peekType(s) !== TokenType.Semicolon | |
| 710 | && peekType(s) !== TokenType.RightCurlyBracket | |
| 711 | && peekType(s) !== TokenType.RightParenthesis | |
| 712 | && peekType(s) !== TokenType.RightSquareBracket) { | |
| 713 | const node = parseValueChild(s) | |
| 714 | if (node) | |
| 715 | children.appendData(node) | |
| 716 | } | |
| 717 | promoteRatios(children) | |
| 718 | return { type: 'Value', children, loc: loc(s, startTok, peek(s)) } | |
| 719 | } | |
| 720 | ||
| 721 | /** | |
| 722 | * Recursive ratio promotion — walk every nested children list (Function, | |
| 723 | * Parentheses, AtrulePrelude, etc.) and run `promoteRatios` on each. | |
| 724 | */ | |
| 725 | function promoteRatiosDeep(list: CssList<CssNode>): void { | |
| 726 | for (const child of list) { | |
| 727 | if ('children' in child && (child as any).children instanceof CssList) | |
| 728 | promoteRatiosDeep((child as any).children) | |
| 729 | } | |
| 730 | promoteRatios(list) | |
| 731 | } | |
| 732 | ||
| 733 | /** | |
| 734 | * Walk a children list and promote `Number / Number` sequences (with | |
| 735 | * optional WhiteSpace around the slash) into a single `Ratio` node. | |
| 736 | * Mirrors css-tree's handling of `aspect-ratio: 16/9`. | |
| 737 | */ | |
| 738 | function promoteRatios(list: CssList<CssNode>): void { | |
| 739 | let cur = list.head | |
| 740 | while (cur) { | |
| 741 | if (cur.data.type === 'Number') { | |
| 742 | // skip whitespace | |
| 743 | let slashItem = cur.next | |
| 744 | while (slashItem && slashItem.data.type === 'WhiteSpace') | |
| 745 | slashItem = slashItem.next | |
| 746 | if (slashItem && slashItem.data.type === 'Operator' && (slashItem.data as { value: string }).value === '/') { | |
| 747 | let rightItem = slashItem.next | |
| 748 | while (rightItem && rightItem.data.type === 'WhiteSpace') | |
| 749 | rightItem = rightItem.next | |
| 750 | if (rightItem && rightItem.data.type === 'Number') { | |
| 751 | // Build Ratio and splice it in. | |
| 752 | const ratio: CssNode = { | |
| 753 | type: 'Ratio', | |
| 754 | left: cur.data as any, | |
| 755 | right: rightItem.data as any, | |
| 756 | loc: null, | |
| 757 | } | |
| 758 | // Remove items between cur (exclusive end) and rightItem (inclusive) | |
| 759 | let toRemove = cur.next | |
| 760 | while (toRemove && toRemove !== rightItem.next) { | |
| 761 | const nxt = toRemove.next | |
| 762 | list.remove(toRemove) | |
| 763 | toRemove = nxt | |
| 764 | } | |
| 765 | list.replace(cur, list.createItem(ratio)) | |
| 766 | cur = list.head // restart — list pointer changed | |
| 767 | continue | |
| 768 | } | |
| 769 | } | |
| 770 | } | |
| 771 | cur = cur.next | |
| 772 | } | |
| 773 | } | |
| 774 | ||
| 775 | function parseValueChild(s: ParserState): CssNode | null { | |
| 776 | const i = s.pos | |
| 777 | if (i >= s.end) | |
| 778 | return null | |
| 779 | const ttype = s.types[i]! as TokenType | |
| 780 | const tstart = s.starts[i]! | |
| 781 | const tend = s.ends[i]! | |
| 782 | const src = s.source | |
| 783 | switch (ttype) { | |
| 784 | case TokenType.WhiteSpace: { | |
| 785 | s.pos++ | |
| 786 | return s.positions | |
| 787 | ? { type: 'WhiteSpace', value: ' ', loc: locRange(s, tstart, tend) } | |
| 788 | : { type: 'WhiteSpace', value: ' ', loc: null } | |
| 789 | } | |
| 790 | case TokenType.Comment: { | |
| 791 | s.pos++ | |
| 792 | return { type: 'Comment', value: src.slice(tstart + 2, tend - 2), loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 793 | } | |
| 794 | case TokenType.Number: { | |
| 795 | s.pos++ | |
| 796 | return { type: 'Number', value: src.slice(tstart, tend), loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 797 | } | |
| 798 | case TokenType.Percentage: { | |
| 799 | s.pos++ | |
| 800 | return { type: 'Percentage', value: src.slice(tstart, tend - 1), loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 801 | } | |
| 802 | case TokenType.Dimension: { | |
| 803 | s.pos++ | |
| 804 | // Manual scan: number portion ends where the unit (alpha/`-`/escape) | |
| 805 | // begins. Faster than the previous regex. | |
| 806 | let unitStart = tstart | |
| 807 | // optional sign | |
| 808 | const c0 = src.charCodeAt(unitStart) | |
| 809 | if (c0 === 43 /* + */ || c0 === 45 /* - */) | |
| 810 | unitStart++ | |
| 811 | // integer/fractional digits and a single dot | |
| 812 | let sawDot = false | |
| 813 | while (unitStart < tend) { | |
| 814 | const c = src.charCodeAt(unitStart) | |
| 815 | if (c >= 48 && c <= 57) { | |
| 816 | unitStart++ | |
| 817 | continue | |
| 818 | } | |
| 819 | if (c === 46 && !sawDot) { | |
| 820 | sawDot = true | |
| 821 | unitStart++ | |
| 822 | continue | |
| 823 | } | |
| 824 | break | |
| 825 | } | |
| 826 | // optional scientific notation | |
| 827 | const eC = src.charCodeAt(unitStart) | |
| 828 | if ((eC === 69 || eC === 101) /* e/E */) { | |
| 829 | let look = unitStart + 1 | |
| 830 | const sgn = src.charCodeAt(look) | |
| 831 | if (sgn === 43 || sgn === 45) look++ | |
| 832 | let any = false | |
| 833 | while (look < tend) { | |
| 834 | const c = src.charCodeAt(look) | |
| 835 | if (c >= 48 && c <= 57) { look++; any = true; continue } | |
| 836 | break | |
| 837 | } | |
| 838 | if (any) unitStart = look | |
| 839 | } | |
| 840 | return { | |
| 841 | type: 'Dimension', | |
| 842 | value: src.slice(tstart, unitStart), | |
| 843 | unit: src.slice(unitStart, tend), | |
| 844 | loc: s.positions ? locRange(s, tstart, tend) : null, | |
| 845 | } | |
| 846 | } | |
| 847 | case TokenType.Ident: { | |
| 848 | s.pos++ | |
| 849 | return { type: 'Identifier', name: decodeName(src, tstart, tend), loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 850 | } | |
| 851 | case TokenType.String: { | |
| 852 | s.pos++ | |
| 853 | const innerStart = tstart + 1 | |
| 854 | const closesProperly = tend - tstart >= 2 && src.charCodeAt(tend - 1) === src.charCodeAt(tstart) | |
| 855 | const innerEnd = closesProperly ? tend - 1 : tend | |
| 856 | const value = decodeString(src, innerStart, innerEnd) | |
| 857 | return { type: 'String', value, loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 858 | } | |
| 859 | case TokenType.Url: { | |
| 860 | s.pos++ | |
| 861 | // url(<inner>) — strip `url(` and the matching `)` (and any quotes | |
| 862 | // on the inner if present), avoiding a regex+slice chain. | |
| 863 | let a = tstart + 4 // past `url(` | |
| 864 | while (a < tend && isWhitespaceCode(src.charCodeAt(a))) a++ | |
| 865 | let b = tend | |
| 866 | if (b > a && src.charCodeAt(b - 1) === 41 /* ) */) b-- | |
| 867 | while (b > a && isWhitespaceCode(src.charCodeAt(b - 1))) b-- | |
| 868 | let value: string | |
| 869 | if (b - a >= 2) { | |
| 870 | const q0 = src.charCodeAt(a) | |
| 871 | const q1 = src.charCodeAt(b - 1) | |
| 872 | if ((q0 === 34 || q0 === 39) && q0 === q1) | |
| 873 | value = src.slice(a + 1, b - 1) | |
| 874 | else | |
| 875 | value = src.slice(a, b) | |
| 876 | } | |
| 877 | else { | |
| 878 | value = src.slice(a, b) | |
| 879 | } | |
| 880 | return { type: 'Url', value, loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 881 | } | |
| 882 | case TokenType.Hash: { | |
| 883 | s.pos++ | |
| 884 | return { type: 'Hash', name: src.slice(tstart + 1, tend), loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 885 | } | |
| 886 | case TokenType.Function: { | |
| 887 | return parseFunction(s) | |
| 888 | } | |
| 889 | case TokenType.LeftParenthesis: { | |
| 890 | return parseParentheses(s) | |
| 891 | } | |
| 892 | case TokenType.Comma: { | |
| 893 | s.pos++ | |
| 894 | return { type: 'Operator', value: ',', loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 895 | } | |
| 896 | case TokenType.Colon: { | |
| 897 | s.pos++ | |
| 898 | return { type: 'Operator', value: ':', loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 899 | } | |
| 900 | case TokenType.Delim: { | |
| 901 | s.pos++ | |
| 902 | const code = src.charCodeAt(tstart) | |
| 903 | const ch = src[tstart]! | |
| 904 | // ASCII bitmap: `/ + - * = > < ~ | $ ^ ! &` — Operator semantics in | |
| 905 | // value contexts. Anything else falls through as an Identifier | |
| 906 | // (e.g. `&` mid-value, `!` outside !important, etc.). | |
| 907 | if (isValueOperatorChar(code)) | |
| 908 | return { type: 'Operator', value: ch, loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 909 | return { type: 'Identifier', name: ch, loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 910 | } | |
| 911 | case TokenType.LeftSquareBracket: { | |
| 912 | // `[…]` inside a value (e.g. `grid-template-columns: [start] 1fr`) | |
| 913 | // becomes a `Brackets` node — matches css-tree's shape so consumers | |
| 914 | // walking nested values don't see a Raw blob here. | |
| 915 | s.pos++ | |
| 916 | const children = newList<CssNode>() | |
| 917 | while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightSquareBracket) { | |
| 918 | const inner = parseValueChild(s) | |
| 919 | if (inner) | |
| 920 | children.appendData(inner) | |
| 921 | } | |
| 922 | const endByte = s.pos < s.end ? s.ends[s.pos]! : tend | |
| 923 | if (peekType(s) === TokenType.RightSquareBracket) | |
| 924 | s.pos++ | |
| 925 | return { type: 'Brackets', children, loc: s.positions ? locRange(s, tstart, endByte) : null } | |
| 926 | } | |
| 927 | case TokenType.AtKeyword: | |
| 928 | case TokenType.LeftCurlyBracket: | |
| 929 | case TokenType.RightCurlyBracket: | |
| 930 | case TokenType.RightParenthesis: | |
| 931 | case TokenType.RightSquareBracket: | |
| 932 | case TokenType.Semicolon: | |
| 933 | case TokenType.EOF: | |
| 934 | case TokenType.CDO: | |
| 935 | case TokenType.CDC: | |
| 936 | case TokenType.BadString: | |
| 937 | case TokenType.BadUrl: { | |
| 938 | s.pos++ | |
| 939 | return { type: 'Raw', value: src.slice(tstart, tend), loc: s.positions ? locRange(s, tstart, tend) : null } | |
| 940 | } | |
| 941 | } | |
| 942 | s.pos++ | |
| 943 | return null | |
| 944 | } | |
| 945 | ||
| 946 | function isWhitespaceCode(c: number): boolean { | |
| 947 | return c === 32 || c === 9 || c === 10 || c === 12 || c === 13 | |
| 948 | } | |
| 949 | ||
| 950 | function isValueOperatorChar(code: number): boolean { | |
| 951 | // / + - * = > < ~ | $ ^ ! & | |
| 952 | return code === 47 || code === 43 || code === 45 || code === 42 | |
| 953 | || code === 61 || code === 62 || code === 60 || code === 126 | |
| 954 | || code === 124 || code === 36 || code === 94 || code === 33 | |
| 955 | || code === 38 | |
| 956 | } | |
| 957 | ||
| 958 | /** | |
| 959 | * Slice `source[start:end]` and lowercase it, but only allocate a new | |
| 960 | * string when the slice actually contains uppercase ASCII. Pure-ASCII | |
| 961 | * lowercase identifiers (the common case) fast-path through `slice`. | |
| 962 | */ | |
| 963 | function lowerIfNeeded(source: string, start: number, end: number): string { | |
| 964 | for (let i = start; i < end; i++) { | |
| 965 | const c = source.charCodeAt(i) | |
| 966 | if (c >= 65 && c <= 90) | |
| 967 | return source.slice(start, end).toLowerCase() | |
| 968 | } | |
| 969 | return source.slice(start, end) | |
| 970 | } | |
| 971 | ||
| 972 | function locRange(s: ParserState, startByte: number, endByte: number): CssLocation { | |
| 973 | const start = s.tokenizer.locate(startByte) | |
| 974 | const end = s.tokenizer.locate(endByte) | |
| 975 | return { | |
| 976 | source: s.filename ?? '<unknown>', | |
| 977 | start: { offset: startByte, line: start.line, column: start.column }, | |
| 978 | end: { offset: endByte, line: end.line, column: end.column }, | |
| 979 | } | |
| 980 | } | |
| 981 | ||
| 982 | function parseFunction(s: ParserState): CssNode { | |
| 983 | const startTok = consume(s) // function token (including `(`) | |
| 984 | // Fast path: scan for any uppercase ASCII before allocating a lower-case | |
| 985 | // copy. Most function names in real stylesheets (`rgb`, `var`, `calc`, | |
| 986 | // `linear-gradient`, …) are already lowercase, so the scan-with-no-upper | |
| 987 | // path saves the `.toLowerCase()` allocation per Function token. | |
| 988 | const name = lowerIfNeeded(s.source, startTok.start, startTok.end - 1) | |
| 989 | const children = newList<CssNode>() | |
| 990 | while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightParenthesis) { | |
| 991 | const node = parseValueChild(s) | |
| 992 | if (node) | |
| 993 | children.appendData(node) | |
| 994 | } | |
| 995 | if (peekType(s) === TokenType.RightParenthesis) | |
| 996 | consume(s) | |
| 997 | ||
| 998 | // Normalise url("…") / url('…') into a Url node (matches css-tree shape). | |
| 999 | // Walk the linked list directly — avoids the `toArray()` allocation. | |
| 1000 | if (name === 'url') { | |
| 1001 | for (let cur: any = children.head; cur != null; cur = cur.next) { | |
| 1002 | if (cur.data.type === 'String') | |
| 1003 | return { type: 'Url', value: (cur.data as any).value, loc: loc(s, startTok, peek(s)) } as UrlNode | |
| 1004 | } | |
| 1005 | } | |
| 1006 | ||
| 1007 | return { type: 'Function', name, children, loc: loc(s, startTok, peek(s)) } | |
| 1008 | } | |
| 1009 | ||
| 1010 | function parseParentheses(s: ParserState): ParenthesesNode { | |
| 1011 | const startTok = consume(s) // ( | |
| 1012 | const children = newList<CssNode>() | |
| 1013 | while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightParenthesis) { | |
| 1014 | const node = parseValueChild(s) | |
| 1015 | if (node) | |
| 1016 | children.appendData(node) | |
| 1017 | } | |
| 1018 | if (peekType(s) === TokenType.RightParenthesis) | |
| 1019 | consume(s) | |
| 1020 | return { type: 'Parentheses', children, loc: loc(s, startTok, peek(s)) } | |
| 1021 | } | |
| 1022 | ||
| 1023 | // ----- selectorList / selector ----- | |
| 1024 | ||
| 1025 | function parseSelectorList(s: ParserState): SelectorList { | |
| 1026 | const startTok = peek(s) | |
| 1027 | const children = newList<CssNode>() | |
| 1028 | while (peekType(s) !== TokenType.EOF) { | |
| 1029 | skipWhitespace(s) | |
| 1030 | if (peekType(s) === TokenType.EOF || peekType(s) === TokenType.LeftCurlyBracket) | |
| 1031 | break | |
| 1032 | const sel = parseSelector(s) | |
| 1033 | // Skip empty selectors entirely — these arise from malformed input | |
| 1034 | // like `,, .a {}` where parseSelector hits a `,` without finding any | |
| 1035 | // segment. Adding them would surface as `Rule.prelude.children: [{},…]` | |
| 1036 | // and break round-trips downstream. | |
| 1037 | if ((sel.children as any).head !== null) | |
| 1038 | children.appendData(sel) | |
| 1039 | skipWhitespace(s) | |
| 1040 | if (peekType(s) === TokenType.Comma) { | |
| 1041 | consume(s) | |
| 1042 | continue | |
| 1043 | } | |
| 1044 | break | |
| 1045 | } | |
| 1046 | return { type: 'SelectorList', children, loc: loc(s, startTok, peek(s)) } | |
| 1047 | } | |
| 1048 | ||
| 1049 | function parseSelector(s: ParserState): Selector { | |
| 1050 | const startTok = peek(s) | |
| 1051 | const children = newList<CssNode>() | |
| 1052 | let lastWasCombinator = true | |
| 1053 | while (peekType(s) !== TokenType.EOF | |
| 1054 | && peekType(s) !== TokenType.Comma | |
| 1055 | && peekType(s) !== TokenType.LeftCurlyBracket) { | |
| 1056 | const t = peek(s) | |
| 1057 | if (t.type === TokenType.WhiteSpace) { | |
| 1058 | consume(s) | |
| 1059 | // descendant combinator only between segments — and only when the | |
| 1060 | // next token isn't itself a combinator (`>`/`+`/`~` or `||`). | |
| 1061 | if (!lastWasCombinator | |
| 1062 | && peekType(s) !== TokenType.EOF | |
| 1063 | && peekType(s) !== TokenType.Comma | |
| 1064 | && peekType(s) !== TokenType.LeftCurlyBracket | |
| 1065 | && !isExplicitCombinator(s) | |
| 1066 | && !isColumnCombinator(s)) { | |
| 1067 | const c: Combinator = { type: 'Combinator', name: ' ', loc: loc(s, t, t) } | |
| 1068 | children.appendData(c) | |
| 1069 | lastWasCombinator = true | |
| 1070 | } | |
| 1071 | continue | |
| 1072 | } | |
| 1073 | // Column combinator `||` — two consecutive `|` Delim tokens (CSS | |
| 1074 | // Selectors 4). Detect before the single-char combinator path. | |
| 1075 | if ( | |
| 1076 | t.type === TokenType.Delim | |
| 1077 | && s.source.charCodeAt(t.start) === 124 /* | */ | |
| 1078 | && s.types[s.pos + 1] === TokenType.Delim | |
| 1079 | && s.source.charCodeAt(s.starts[s.pos + 1]!) === 124 | |
| 1080 | ) { | |
| 1081 | const startCol = peek(s) | |
| 1082 | consume(s) | |
| 1083 | const endCol = consume(s) | |
| 1084 | const c: Combinator = { type: 'Combinator', name: '||', loc: loc(s, startCol, endCol) } | |
| 1085 | children.appendData(c) | |
| 1086 | lastWasCombinator = true | |
| 1087 | skipWhitespace(s) | |
| 1088 | continue | |
| 1089 | } | |
| 1090 | if (isExplicitCombinator(s)) { | |
| 1091 | const ct = consume(s) | |
| 1092 | const ch = s.source[ct.start]! | |
| 1093 | const c: Combinator = { type: 'Combinator', name: ch, loc: loc(s, ct, ct) } | |
| 1094 | children.appendData(c) | |
| 1095 | lastWasCombinator = true | |
| 1096 | skipWhitespace(s) | |
| 1097 | continue | |
| 1098 | } | |
| 1099 | const seg = parseSelectorSegment(s) | |
| 1100 | if (!seg) | |
| 1101 | break | |
| 1102 | children.appendData(seg) | |
| 1103 | lastWasCombinator = false | |
| 1104 | } | |
| 1105 | // A trailing combinator (descendant or otherwise) is meaningless — the | |
| 1106 | // selector ended before another segment arrived. Strip it so the AST | |
| 1107 | // doesn't carry a phantom `Combinator` that round-trips to extra | |
| 1108 | // whitespace at the end of the rule prelude. | |
| 1109 | while (children.tail && (children.tail.data as CssNode).type === 'Combinator') | |
| 1110 | children.remove(children.tail) | |
| 1111 | return { type: 'Selector', children, loc: loc(s, startTok, peek(s)) } | |
| 1112 | } | |
| 1113 | ||
| 1114 | function isExplicitCombinator(s: ParserState): boolean { | |
| 1115 | const t = peek(s) | |
| 1116 | if (t.type !== TokenType.Delim) | |
| 1117 | return false | |
| 1118 | const ch = s.source[t.start]! | |
| 1119 | return ch === '>' || ch === '+' || ch === '~' | |
| 1120 | } | |
| 1121 | ||
| 1122 | function isColumnCombinator(s: ParserState): boolean { | |
| 1123 | // `||` — two consecutive `|` Delim tokens. | |
| 1124 | if (s.types[s.pos] !== TokenType.Delim) | |
| 1125 | return false | |
| 1126 | if (s.source.charCodeAt(s.starts[s.pos]!) !== 124 /* | */) | |
| 1127 | return false | |
| 1128 | if (s.types[s.pos + 1] !== TokenType.Delim) | |
| 1129 | return false | |
| 1130 | return s.source.charCodeAt(s.starts[s.pos + 1]!) === 124 | |
| 1131 | } | |
| 1132 | ||
| 1133 | function parseSelectorSegment(s: ParserState): CssNode | null { | |
| 1134 | const t = peek(s) | |
| 1135 | switch (t.type) { | |
| 1136 | case TokenType.Ident: { | |
| 1137 | consume(s) | |
| 1138 | // Namespace prefix? `svg|circle` → TypeSelector(name="svg|circle"). | |
| 1139 | // We keep the prefix as part of the name (matches css-tree shape). | |
| 1140 | // Skip when the trailing `|` is part of `||` (column combinator) or | |
| 1141 | // `|=` (attribute hyphen-match) — neither is a namespace separator. | |
| 1142 | if (peekType(s) === TokenType.Delim && s.source[peek(s).start] === '|' | |
| 1143 | && s.source[peek(s).start + 1] !== '=' | |
| 1144 | && !(s.types[s.pos + 1] === TokenType.Delim && s.source.charCodeAt(s.starts[s.pos + 1]!) === 124)) { | |
| 1145 | consume(s) | |
| 1146 | if (peekType(s) === TokenType.Ident) { | |
| 1147 | const local = consume(s) | |
| 1148 | const node: TypeSelector = { | |
| 1149 | type: 'TypeSelector', | |
| 1150 | name: `${tokenSlice(s, t)}|${tokenSlice(s, local)}`, | |
| 1151 | loc: loc(s, t, local), | |
| 1152 | } | |
| 1153 | return node | |
| 1154 | } | |
| 1155 | if (peekType(s) === TokenType.Delim && s.source[peek(s).start] === '*') { | |
| 1156 | const star = consume(s) | |
| 1157 | const node: TypeSelector = { | |
| 1158 | type: 'TypeSelector', | |
| 1159 | name: `${tokenSlice(s, t)}|*`, | |
| 1160 | loc: loc(s, t, star), | |
| 1161 | } | |
| 1162 | return node | |
| 1163 | } | |
| 1164 | } | |
| 1165 | const node: TypeSelector = { type: 'TypeSelector', name: tokenSlice(s, t), loc: loc(s, t, t) } | |
| 1166 | return node | |
| 1167 | } | |
| 1168 | // `@keyframes` rule preludes (`0%`, `100%`) parse as Percentage tokens. | |
| 1169 | // Treat them as a TypeSelector so the round-trip preserves the literal. | |
| 1170 | case TokenType.Percentage: { | |
| 1171 | consume(s) | |
| 1172 | const node: TypeSelector = { type: 'TypeSelector', name: tokenSlice(s, t), loc: loc(s, t, t) } | |
| 1173 | return node | |
| 1174 | } | |
| 1175 | case TokenType.Number: { | |
| 1176 | consume(s) | |
| 1177 | const node: TypeSelector = { type: 'TypeSelector', name: tokenSlice(s, t), loc: loc(s, t, t) } | |
| 1178 | return node | |
| 1179 | } | |
| 1180 | case TokenType.Delim: { | |
| 1181 | const ch = s.source[t.start]! | |
| 1182 | if (ch === '*') { | |
| 1183 | consume(s) | |
| 1184 | const node: TypeSelector = { type: 'TypeSelector', name: '*', loc: loc(s, t, t) } | |
| 1185 | return node | |
| 1186 | } | |
| 1187 | if (ch === '.') { | |
| 1188 | consume(s) | |
| 1189 | if (peekType(s) === TokenType.Ident) { | |
| 1190 | const id = consume(s) | |
| 1191 | const node: ClassSelector = { type: 'ClassSelector', name: tokenSlice(s, id), loc: loc(s, t, id) } | |
| 1192 | return node | |
| 1193 | } | |
| 1194 | return null | |
| 1195 | } | |
| 1196 | if (ch === '&') { | |
| 1197 | consume(s) | |
| 1198 | return { type: 'NestingSelector', loc: loc(s, t, t) } | |
| 1199 | } | |
| 1200 | consume(s) | |
| 1201 | return null | |
| 1202 | } | |
| 1203 | case TokenType.Hash: { | |
| 1204 | consume(s) | |
| 1205 | const node: IdSelector = { type: 'IdSelector', name: s.source.slice(t.start + 1, t.end), loc: loc(s, t, t) } | |
| 1206 | return node | |
| 1207 | } | |
| 1208 | case TokenType.Colon: { | |
| 1209 | consume(s) | |
| 1210 | const isElement = peekType(s) === TokenType.Colon | |
| 1211 | if (isElement) | |
| 1212 | consume(s) | |
| 1213 | return parsePseudo(s, isElement, t) | |
| 1214 | } | |
| 1215 | case TokenType.LeftSquareBracket: { | |
| 1216 | return parseAttribute(s) | |
| 1217 | } | |
| 1218 | } | |
| 1219 | return null | |
| 1220 | } | |
| 1221 | ||
| 1222 | function parsePseudo(s: ParserState, isElement: boolean, startTok: Token): PseudoClassSelector | PseudoElementSelector | null { | |
| 1223 | const t = peek(s) | |
| 1224 | if (t.type === TokenType.Ident) { | |
| 1225 | consume(s) | |
| 1226 | const name = lowerIfNeeded(s.source, t.start, t.end) | |
| 1227 | const node = isElement | |
| 1228 | ? { type: 'PseudoElementSelector' as const, name, children: null, loc: loc(s, startTok, t) } | |
| 1229 | : { type: 'PseudoClassSelector' as const, name, children: null, loc: loc(s, startTok, t) } | |
| 1230 | return node | |
| 1231 | } | |
| 1232 | if (t.type === TokenType.Function) { | |
| 1233 | consume(s) | |
| 1234 | const name = lowerIfNeeded(s.source, t.start, t.end - 1) | |
| 1235 | const children = newList<CssNode>() | |
| 1236 | ||
| 1237 | // Pseudo-classes that take a selector list re-parse their argument. | |
| 1238 | const isSelectorListPseudo = !isElement && SELECTOR_LIST_PSEUDOS_PARSE.has(name) | |
| 1239 | if (isSelectorListPseudo) { | |
| 1240 | // Slurp the argument tokens up to the matching `)` and re-parse them. | |
| 1241 | const argStart = peek(s) | |
| 1242 | let argEnd = argStart | |
| 1243 | let depth = 1 | |
| 1244 | while (peekType(s) !== TokenType.EOF && depth > 0) { | |
| 1245 | if (peekType(s) === TokenType.LeftParenthesis || peekType(s) === TokenType.Function) | |
| 1246 | depth++ | |
| 1247 | else if (peekType(s) === TokenType.RightParenthesis) { | |
| 1248 | depth-- | |
| 1249 | if (depth === 0) | |
| 1250 | break | |
| 1251 | } | |
| 1252 | argEnd = consume(s) | |
| 1253 | } | |
| 1254 | if (peekType(s) === TokenType.RightParenthesis) | |
| 1255 | consume(s) | |
| 1256 | const inner = s.source.slice(argStart.start, argEnd.end).trim() | |
| 1257 | if (inner.length > 0) { | |
| 1258 | const sub = makeState(inner, { positions: false }) | |
| 1259 | const list = parseSelectorList(sub) | |
| 1260 | children.appendData(list) | |
| 1261 | } | |
| 1262 | } | |
| 1263 | else { | |
| 1264 | while (peekType(s) !== TokenType.EOF && peekType(s) !== TokenType.RightParenthesis) { | |
| 1265 | const inner = parseValueChild(s) | |
| 1266 | if (inner) | |
| 1267 | children.appendData(inner) | |
| 1268 | } | |
| 1269 | if (peekType(s) === TokenType.RightParenthesis) | |
| 1270 | consume(s) | |
| 1271 | } | |
| 1272 | ||
| 1273 | if (isElement) { | |
| 1274 | const node: PseudoElementSelector = { type: 'PseudoElementSelector', name, children, loc: loc(s, startTok, peek(s)) } | |
| 1275 | return node | |
| 1276 | } | |
| 1277 | const node: PseudoClassSelector = { type: 'PseudoClassSelector', name, children, loc: loc(s, startTok, peek(s)) } | |
| 1278 | return node | |
| 1279 | } | |
| 1280 | return null | |
| 1281 | } | |
| 1282 | ||
| 1283 | function parseAttribute(s: ParserState): AttributeSelector | null { | |
| 1284 | const startTok = consume(s) // [ | |
| 1285 | skipWhitespace(s) | |
| 1286 | const nameTok = peek(s) | |
| 1287 | if (nameTok.type !== TokenType.Ident) | |
| 1288 | return null | |
| 1289 | consume(s) | |
| 1290 | const name: Identifier = { type: 'Identifier', name: tokenSlice(s, nameTok), loc: loc(s, nameTok, nameTok) } | |
| 1291 | skipWhitespace(s) | |
| 1292 | let matcher: string | null = null | |
| 1293 | let value: StringNode | Identifier | null = null | |
| 1294 | let flags: string | null = null | |
| 1295 | const next = peek(s) | |
| 1296 | if (next.type === TokenType.Delim) { | |
| 1297 | const ch = s.source[next.start]! | |
| 1298 | const ch2 = s.source[next.start + 1] | |
| 1299 | if (ch === '=') { | |
| 1300 | matcher = '=' | |
| 1301 | consume(s) | |
| 1302 | } | |
| 1303 | else if ((ch === '~' || ch === '|' || ch === '^' || ch === '$' || ch === '*') && ch2 === '=') { | |
| 1304 | // Two consecutive delim tokens | |
| 1305 | consume(s) | |
| 1306 | const eq = peek(s) | |
| 1307 | if (eq.type === TokenType.Delim && s.source[eq.start] === '=') { | |
| 1308 | consume(s) | |
| 1309 | matcher = `${ch}=` | |
| 1310 | } | |
| 1311 | } | |
| 1312 | skipWhitespace(s) | |
| 1313 | const v = peek(s) | |
| 1314 | if (v.type === TokenType.String) { | |
| 1315 | consume(s) | |
| 1316 | const closesProperly = v.end - v.start >= 2 && s.source.charCodeAt(v.end - 1) === s.source.charCodeAt(v.start) | |
| 1317 | const text = decodeString(s.source, v.start + 1, closesProperly ? v.end - 1 : v.end) | |
| 1318 | value = { type: 'String', value: text, loc: loc(s, v, v) } | |
| 1319 | } | |
| 1320 | else if (v.type === TokenType.Ident) { | |
| 1321 | consume(s) | |
| 1322 | value = { type: 'Identifier', name: tokenSlice(s, v), loc: loc(s, v, v) } | |
| 1323 | } | |
| 1324 | skipWhitespace(s) | |
| 1325 | const flagTok = peek(s) | |
| 1326 | if (flagTok.type === TokenType.Ident) { | |
| 1327 | consume(s) | |
| 1328 | flags = tokenSlice(s, flagTok) | |
| 1329 | } | |
| 1330 | } | |
| 1331 | skipWhitespace(s) | |
| 1332 | let endTok = peek(s) | |
| 1333 | if (peekType(s) === TokenType.RightSquareBracket) { | |
| 1334 | endTok = consume(s) | |
| 1335 | } | |
| 1336 | return { type: 'AttributeSelector', name, matcher, value, flags, loc: loc(s, startTok, endTok) } | |
| 1337 | } | |
| 1338 | ||
| 1339 | // expose some helpers for the public surface | |
| 1340 | export { TokenType } | |
packages/toolkit/src/parse/tokenizer.tsadded+708-0
| @@ -0,0 +1,708 @@ | ||
| 1 | /** | |
| 2 | * CSS Syntax Module Level 3 tokenizer. | |
| 3 | * | |
| 4 | * https://www.w3.org/TR/css-syntax-3/#tokenization | |
| 5 | * | |
| 6 | * Produces a flat sequence of token offsets that the parser walks. Tokens | |
| 7 | * are recorded as parallel arrays (type + offset) rather than allocations | |
| 8 | * — every other CSS lib does the same for performance. | |
| 9 | */ | |
| 10 | ||
| 11 | export const enum TokenType { | |
| 12 | EOF = 0, | |
| 13 | Ident = 1, | |
| 14 | Function = 2, | |
| 15 | AtKeyword = 3, | |
| 16 | Hash = 4, | |
| 17 | String = 5, | |
| 18 | BadString = 6, | |
| 19 | Url = 7, | |
| 20 | BadUrl = 8, | |
| 21 | Delim = 9, | |
| 22 | Number = 10, | |
| 23 | Percentage = 11, | |
| 24 | Dimension = 12, | |
| 25 | WhiteSpace = 13, | |
| 26 | CDO = 14, | |
| 27 | CDC = 15, | |
| 28 | Colon = 16, | |
| 29 | Semicolon = 17, | |
| 30 | Comma = 18, | |
| 31 | LeftSquareBracket = 19, | |
| 32 | RightSquareBracket = 20, | |
| 33 | LeftParenthesis = 21, | |
| 34 | RightParenthesis = 22, | |
| 35 | LeftCurlyBracket = 23, | |
| 36 | RightCurlyBracket = 24, | |
| 37 | Comment = 25, | |
| 38 | } | |
| 39 | ||
| 40 | export interface Token { | |
| 41 | type: TokenType | |
| 42 | start: number | |
| 43 | end: number | |
| 44 | } | |
| 45 | ||
| 46 | const REPLACEMENT = 0xFFFD | |
| 47 | ||
| 48 | // ----- pre-built character-class lookup ----- | |
| 49 | // One byte per char code (0-127). Each bit is a class: | |
| 50 | // bit 0 — whitespace (space/tab/CR/LF/FF) | |
| 51 | // bit 1 — digit (0-9) | |
| 52 | // bit 2 — hex digit (0-9 a-f A-F) | |
| 53 | // bit 3 — name-start (letter / underscore — non-ASCII handled separately) | |
| 54 | // bit 4 — newline (CR / LF / FF) | |
| 55 | const CHAR_CLASS = (() => { | |
| 56 | const t = new Uint8Array(128) | |
| 57 | for (let c = 0; c < 128; c++) { | |
| 58 | let bits = 0 | |
| 59 | if (c === 32 || c === 9 || c === 10 || c === 12 || c === 13) | |
| 60 | bits |= 1 | |
| 61 | if (c >= 48 && c <= 57) | |
| 62 | bits |= 2 | |
| 63 | if ((c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102)) | |
| 64 | bits |= 4 | |
| 65 | if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95) | |
| 66 | bits |= 8 | |
| 67 | if (c === 10 || c === 12 || c === 13) | |
| 68 | bits |= 16 | |
| 69 | t[c] = bits | |
| 70 | } | |
| 71 | return t | |
| 72 | })() | |
| 73 | ||
| 74 | function isDigit(code: number): boolean { | |
| 75 | return code < 128 && (CHAR_CLASS[code]! & 2) !== 0 | |
| 76 | } | |
| 77 | ||
| 78 | function isHexDigit(code: number): boolean { | |
| 79 | return code < 128 && (CHAR_CLASS[code]! & 4) !== 0 | |
| 80 | } | |
| 81 | ||
| 82 | function isNameStart(code: number): boolean { | |
| 83 | return code >= 0x80 || (code < 128 && (CHAR_CLASS[code]! & 8) !== 0) | |
| 84 | } | |
| 85 | ||
| 86 | function isName(code: number): boolean { | |
| 87 | return code === 45 /* - */ || code >= 0x80 || (code < 128 && (CHAR_CLASS[code]! & (2 | 8)) !== 0) | |
| 88 | } | |
| 89 | ||
| 90 | function isNonPrintable(code: number): boolean { | |
| 91 | return (code >= 0 && code <= 8) || code === 11 || (code >= 14 && code <= 31) || code === 127 | |
| 92 | } | |
| 93 | ||
| 94 | function isNewline(code: number): boolean { | |
| 95 | return code < 128 && (CHAR_CLASS[code]! & 16) !== 0 | |
| 96 | } | |
| 97 | ||
| 98 | function isWhitespace(code: number): boolean { | |
| 99 | return code < 128 && (CHAR_CLASS[code]! & 1) !== 0 | |
| 100 | } | |
| 101 | ||
| 102 | function isValidEscape(c1: number, c2: number): boolean { | |
| 103 | if (c1 !== 92 /* \ */) | |
| 104 | return false | |
| 105 | if (isNewline(c2)) | |
| 106 | return false | |
| 107 | return true | |
| 108 | } | |
| 109 | ||
| 110 | function startsIdentifier(c1: number, c2: number, c3: number): boolean { | |
| 111 | if (c1 === 45 /* - */) { | |
| 112 | return isNameStart(c2) || c2 === 45 || isValidEscape(c2, c3) | |
| 113 | } | |
| 114 | if (isNameStart(c1)) | |
| 115 | return true | |
| 116 | if (c1 === 92 /* \ */) | |
| 117 | return isValidEscape(c1, c2) | |
| 118 | return false | |
| 119 | } | |
| 120 | ||
| 121 | function startsNumber(c1: number, c2: number, c3: number): boolean { | |
| 122 | if (c1 === 43 /* + */ || c1 === 45 /* - */) { | |
| 123 | if (isDigit(c2)) | |
| 124 | return true | |
| 125 | if (c2 === 46 /* . */ && isDigit(c3)) | |
| 126 | return true | |
| 127 | return false | |
| 128 | } | |
| 129 | if (c1 === 46 /* . */) | |
| 130 | return isDigit(c2) | |
| 131 | return isDigit(c1) | |
| 132 | } | |
| 133 | ||
| 134 | export class Tokenizer { | |
| 135 | source: string | |
| 136 | offset = 0 | |
| 137 | /** | |
| 138 | * Parallel typed-array storage. The parser walks `count` tokens by | |
| 139 | * index; `types[i]` / `starts[i]` / `ends[i]` are the three fields of | |
| 140 | * the i-th token. This avoids one `Object` allocation per token, which | |
| 141 | * matters a lot on parse — for a 6 KB stylesheet there are ~1500 tokens. | |
| 142 | */ | |
| 143 | types: Uint8Array | |
| 144 | starts: Uint32Array | |
| 145 | ends: Uint32Array | |
| 146 | count = 0 | |
| 147 | /** Lazy-built `Token`-shaped array — only filled if a consumer | |
| 148 | * reads `.tokens`. Internally the parser uses the typed arrays. */ | |
| 149 | private _tokens: Token[] | null = null | |
| 150 | /** | |
| 151 | * Offset where each line starts; used for line/col reporting. Built | |
| 152 | * lazily by `locate()` — almost every parse runs with `positions: false` | |
| 153 | * and never asks for source locations, so paying the per-newline | |
| 154 | * `lineStarts.push()` during tokenization is pure overhead in the | |
| 155 | * common case. The closures the original loops captured for that push | |
| 156 | * are also gone. | |
| 157 | */ | |
| 158 | private _lineStarts: number[] | null = null | |
| 159 | ||
| 160 | constructor(source: string) { | |
| 161 | this.source = source | |
| 162 | // CSS averages ~3.5 chars/token across realistic stylesheets (selectors, | |
| 163 | // declarations, values, whitespace). Allocating `len/3` covers nearly | |
| 164 | // all inputs without needing `grow()`. Floor at 64 for tiny snippets. | |
| 165 | const cap = Math.max(64, Math.ceil(source.length / 3)) | |
| 166 | this.types = new Uint8Array(cap) | |
| 167 | this.starts = new Uint32Array(cap) | |
| 168 | this.ends = new Uint32Array(cap) | |
| 169 | this.tokenize() | |
| 170 | } | |
| 171 | ||
| 172 | /** Public alias kept for back-compat — built lazily on first read. */ | |
| 173 | get lineStarts(): number[] { | |
| 174 | if (this._lineStarts == null) | |
| 175 | this._lineStarts = buildLineStarts(this.source) | |
| 176 | return this._lineStarts | |
| 177 | } | |
| 178 | ||
| 179 | /** Lazy `Token[]` view for backwards-compat. */ | |
| 180 | get tokens(): Token[] { | |
| 181 | if (this._tokens != null) | |
| 182 | return this._tokens | |
| 183 | const out: Token[] = Array.from({ length: this.count }) | |
| 184 | for (let i = 0; i < this.count; i++) | |
| 185 | out[i] = { type: this.types[i]! as TokenType, start: this.starts[i]!, end: this.ends[i]! } | |
| 186 | this._tokens = out | |
| 187 | return out | |
| 188 | } | |
| 189 | ||
| 190 | private addToken(type: TokenType, start: number, end: number): void { | |
| 191 | if (this.count >= this.types.length) | |
| 192 | this.grow() | |
| 193 | this.types[this.count] = type | |
| 194 | this.starts[this.count] = start | |
| 195 | this.ends[this.count] = end | |
| 196 | this.count++ | |
| 197 | } | |
| 198 | ||
| 199 | private grow(): void { | |
| 200 | const oldLen = this.types.length | |
| 201 | const newLen = oldLen * 2 | |
| 202 | // eslint-disable-next-line pickier/no-unused-vars | |
| 203 | const t = new Uint8Array(newLen); t.set(this.types); this.types = t | |
| 204 | // eslint-disable-next-line pickier/no-unused-vars | |
| 205 | const s = new Uint32Array(newLen); s.set(this.starts); this.starts = s | |
| 206 | // eslint-disable-next-line pickier/no-unused-vars | |
| 207 | const e = new Uint32Array(newLen); e.set(this.ends); this.ends = e | |
| 208 | } | |
| 209 | ||
| 210 | private tokenize(): void { | |
| 211 | const src = this.source | |
| 212 | let i = 0 | |
| 213 | const len = src.length | |
| 214 | ||
| 215 | while (i < len) { | |
| 216 | const c1 = src.charCodeAt(i) | |
| 217 | ||
| 218 | // comment /* ... */ | |
| 219 | if (c1 === 47 /* / */ && src.charCodeAt(i + 1) === 42 /* * */) { | |
| 220 | const start = i | |
| 221 | i += 2 | |
| 222 | while (i < len && !(src.charCodeAt(i) === 42 && src.charCodeAt(i + 1) === 47)) | |
| 223 | i++ | |
| 224 | i = i < len ? i + 2 : len | |
| 225 | this.addToken(TokenType.Comment, start, i) | |
| 226 | continue | |
| 227 | } | |
| 228 | ||
| 229 | // whitespace | |
| 230 | if (isWhitespace(c1)) { | |
| 231 | const start = i | |
| 232 | while (i < len && isWhitespace(src.charCodeAt(i))) | |
| 233 | i++ | |
| 234 | this.addToken(TokenType.WhiteSpace, start, i) | |
| 235 | continue | |
| 236 | } | |
| 237 | ||
| 238 | // string | |
| 239 | if (c1 === 34 /* " */ || c1 === 39 /* ' */) { | |
| 240 | i = this.consumeString(c1, i) | |
| 241 | continue | |
| 242 | } | |
| 243 | ||
| 244 | // hash | |
| 245 | if (c1 === 35 /* # */) { | |
| 246 | if (i + 1 < len && (isName(src.charCodeAt(i + 1)) || isValidEscape(src.charCodeAt(i + 1), src.charCodeAt(i + 2)))) { | |
| 247 | const start = i | |
| 248 | i++ | |
| 249 | i = this.consumeName(i) | |
| 250 | this.addToken(TokenType.Hash, start, i) | |
| 251 | continue | |
| 252 | } | |
| 253 | this.addToken(TokenType.Delim, i, i + 1) | |
| 254 | i++ | |
| 255 | continue | |
| 256 | } | |
| 257 | ||
| 258 | // ( ) [ ] { } , : ; | |
| 259 | switch (c1) { | |
| 260 | case 40: this.addToken(TokenType.LeftParenthesis, i, i + 1); i++; continue | |
| 261 | case 41: this.addToken(TokenType.RightParenthesis, i, i + 1); i++; continue | |
| 262 | case 91: this.addToken(TokenType.LeftSquareBracket, i, i + 1); i++; continue | |
| 263 | case 93: this.addToken(TokenType.RightSquareBracket, i, i + 1); i++; continue | |
| 264 | case 123: this.addToken(TokenType.LeftCurlyBracket, i, i + 1); i++; continue | |
| 265 | case 125: this.addToken(TokenType.RightCurlyBracket, i, i + 1); i++; continue | |
| 266 | case 44: this.addToken(TokenType.Comma, i, i + 1); i++; continue | |
| 267 | case 58: this.addToken(TokenType.Colon, i, i + 1); i++; continue | |
| 268 | case 59: this.addToken(TokenType.Semicolon, i, i + 1); i++; continue | |
| 269 | } | |
| 270 | ||
| 271 | // CDO/CDC | |
| 272 | if (c1 === 60 /* < */ && src.startsWith('!--', i + 1)) { | |
| 273 | this.addToken(TokenType.CDO, i, i + 4) | |
| 274 | i += 4 | |
| 275 | continue | |
| 276 | } | |
| 277 | if (c1 === 45 /* - */ && src.startsWith('->', i + 1)) { | |
| 278 | this.addToken(TokenType.CDC, i, i + 3) | |
| 279 | i += 3 | |
| 280 | continue | |
| 281 | } | |
| 282 | ||
| 283 | // @ keyword | |
| 284 | if (c1 === 64 /* @ */) { | |
| 285 | const c2 = src.charCodeAt(i + 1) | |
| 286 | const c3 = src.charCodeAt(i + 2) | |
| 287 | const c4 = src.charCodeAt(i + 3) | |
| 288 | if (startsIdentifier(c2, c3, c4)) { | |
| 289 | const start = i | |
| 290 | i++ | |
| 291 | i = this.consumeName(i) | |
| 292 | this.addToken(TokenType.AtKeyword, start, i) | |
| 293 | continue | |
| 294 | } | |
| 295 | this.addToken(TokenType.Delim, i, i + 1) | |
| 296 | i++ | |
| 297 | continue | |
| 298 | } | |
| 299 | ||
| 300 | // number / percentage / dimension | |
| 301 | const c2 = src.charCodeAt(i + 1) | |
| 302 | const c3 = src.charCodeAt(i + 2) | |
| 303 | if (startsNumber(c1, c2, c3)) { | |
| 304 | i = this.consumeNumeric(i) | |
| 305 | continue | |
| 306 | } | |
| 307 | ||
| 308 | // ident-like (function, url, ident) | |
| 309 | if (startsIdentifier(c1, c2, c3)) { | |
| 310 | i = this.consumeIdentLike(i) | |
| 311 | continue | |
| 312 | } | |
| 313 | ||
| 314 | // \\ escape | |
| 315 | if (c1 === 92 /* \ */) { | |
| 316 | if (isValidEscape(c1, c2)) { | |
| 317 | i = this.consumeIdentLike(i) | |
| 318 | continue | |
| 319 | } | |
| 320 | this.addToken(TokenType.Delim, i, i + 1) | |
| 321 | i++ | |
| 322 | continue | |
| 323 | } | |
| 324 | ||
| 325 | // anything else: delim | |
| 326 | this.addToken(TokenType.Delim, i, i + 1) | |
| 327 | i++ | |
| 328 | } | |
| 329 | this.addToken(TokenType.EOF, len, len) | |
| 330 | } | |
| 331 | ||
| 332 | private consumeString(quote: number, start: number): number { | |
| 333 | const src = this.source | |
| 334 | const len = src.length | |
| 335 | let i = start + 1 | |
| 336 | while (i < len) { | |
| 337 | const c = src.charCodeAt(i) | |
| 338 | if (c === quote) { | |
| 339 | i++ | |
| 340 | this.addToken(TokenType.String, start, i) | |
| 341 | return i | |
| 342 | } | |
| 343 | if (isNewline(c)) { | |
| 344 | this.addToken(TokenType.BadString, start, i) | |
| 345 | return i | |
| 346 | } | |
| 347 | if (c === 92 /* \ */) { | |
| 348 | const c2 = src.charCodeAt(i + 1) | |
| 349 | if (isNewline(c2)) { | |
| 350 | i += 2 | |
| 351 | continue | |
| 352 | } | |
| 353 | if (i + 1 < len) { | |
| 354 | i = this.consumeEscapeSkip(i + 1) | |
| 355 | continue | |
| 356 | } | |
| 357 | } | |
| 358 | i++ | |
| 359 | } | |
| 360 | this.addToken(TokenType.String, start, i) | |
| 361 | return i | |
| 362 | } | |
| 363 | ||
| 364 | private consumeEscapeSkip(i: number): number { | |
| 365 | const src = this.source | |
| 366 | if (i >= src.length) | |
| 367 | return i | |
| 368 | const c = src.charCodeAt(i) | |
| 369 | if (isHexDigit(c)) { | |
| 370 | let n = 1 | |
| 371 | i++ | |
| 372 | while (n < 6 && i < src.length && isHexDigit(src.charCodeAt(i))) { | |
| 373 | i++ | |
| 374 | n++ | |
| 375 | } | |
| 376 | if (i < src.length && isWhitespace(src.charCodeAt(i))) | |
| 377 | i++ | |
| 378 | return i | |
| 379 | } | |
| 380 | return i + 1 | |
| 381 | } | |
| 382 | ||
| 383 | private consumeName(start: number): number { | |
| 384 | const src = this.source | |
| 385 | const len = src.length | |
| 386 | let i = start | |
| 387 | // Fast path: pure ASCII name chars — no escape, no non-ASCII. Inline | |
| 388 | // `isName` so the JIT keeps the loop body in a single branch path. | |
| 389 | while (i < len) { | |
| 390 | const c = src.charCodeAt(i) | |
| 391 | if (c < 128 && (CHAR_CLASS[c]! & (2 | 8)) !== 0) { | |
| 392 | i++ | |
| 393 | continue | |
| 394 | } | |
| 395 | if (c === 45 /* - */) { | |
| 396 | i++ | |
| 397 | continue | |
| 398 | } | |
| 399 | // Slow path: non-ASCII or escape — fall back to the general check. | |
| 400 | if (c >= 0x80) { | |
| 401 | i++ | |
| 402 | continue | |
| 403 | } | |
| 404 | if (c === 92 /* \ */ && isValidEscape(c, src.charCodeAt(i + 1))) { | |
| 405 | i = this.consumeEscapeSkip(i + 1) | |
| 406 | continue | |
| 407 | } | |
| 408 | break | |
| 409 | } | |
| 410 | return i | |
| 411 | } | |
| 412 | ||
| 413 | private consumeNumber(start: number): number { | |
| 414 | const src = this.source | |
| 415 | let i = start | |
| 416 | if (src.charCodeAt(i) === 43 || src.charCodeAt(i) === 45) | |
| 417 | i++ | |
| 418 | while (i < src.length && isDigit(src.charCodeAt(i))) | |
| 419 | i++ | |
| 420 | if (src.charCodeAt(i) === 46 && isDigit(src.charCodeAt(i + 1))) { | |
| 421 | i += 2 | |
| 422 | while (i < src.length && isDigit(src.charCodeAt(i))) | |
| 423 | i++ | |
| 424 | } | |
| 425 | const eC = src.charCodeAt(i) | |
| 426 | if (eC === 69 || eC === 101) { | |
| 427 | const next = src.charCodeAt(i + 1) | |
| 428 | const next2 = src.charCodeAt(i + 2) | |
| 429 | if (isDigit(next)) { | |
| 430 | i += 2 | |
| 431 | while (i < src.length && isDigit(src.charCodeAt(i))) | |
| 432 | i++ | |
| 433 | } | |
| 434 | else if ((next === 43 || next === 45) && isDigit(next2)) { | |
| 435 | i += 3 | |
| 436 | while (i < src.length && isDigit(src.charCodeAt(i))) | |
| 437 | i++ | |
| 438 | } | |
| 439 | } | |
| 440 | return i | |
| 441 | } | |
| 442 | ||
| 443 | private consumeNumeric(start: number): number { | |
| 444 | const src = this.source | |
| 445 | const numEnd = this.consumeNumber(start) | |
| 446 | const c1 = src.charCodeAt(numEnd) | |
| 447 | const c2 = src.charCodeAt(numEnd + 1) | |
| 448 | const c3 = src.charCodeAt(numEnd + 2) | |
| 449 | ||
| 450 | if (startsIdentifier(c1, c2, c3)) { | |
| 451 | const end = this.consumeName(numEnd) | |
| 452 | this.addToken(TokenType.Dimension, start, end) | |
| 453 | return end | |
| 454 | } | |
| 455 | if (c1 === 37 /* % */) { | |
| 456 | this.addToken(TokenType.Percentage, start, numEnd + 1) | |
| 457 | return numEnd + 1 | |
| 458 | } | |
| 459 | this.addToken(TokenType.Number, start, numEnd) | |
| 460 | return numEnd | |
| 461 | } | |
| 462 | ||
| 463 | private consumeIdentLike(start: number): number { | |
| 464 | const src = this.source | |
| 465 | const nameEnd = this.consumeName(start) | |
| 466 | const c = src.charCodeAt(nameEnd) | |
| 467 | // Only `url` (3 chars) needs special handling; avoid the | |
| 468 | // `slice(...).toLowerCase()` allocation per ident in the hot path. | |
| 469 | if (c === 40 /* ( */ && nameEnd - start === 3) { | |
| 470 | const c0 = src.charCodeAt(start) | 32 | |
| 471 | const c1 = src.charCodeAt(start + 1) | 32 | |
| 472 | const c2 = src.charCodeAt(start + 2) | 32 | |
| 473 | if (c0 === 117 && c1 === 114 && c2 === 108) { | |
| 474 | let i = nameEnd + 1 | |
| 475 | while (i < src.length && isWhitespace(src.charCodeAt(i))) | |
| 476 | i++ | |
| 477 | const next = src.charCodeAt(i) | |
| 478 | if (next === 34 || next === 39) { | |
| 479 | // function url("...") | |
| 480 | this.addToken(TokenType.Function, start, nameEnd + 1) | |
| 481 | return nameEnd + 1 | |
| 482 | } | |
| 483 | return this.consumeUrl(start, nameEnd) | |
| 484 | } | |
| 485 | } | |
| 486 | if (c === 40 /* ( */) { | |
| 487 | this.addToken(TokenType.Function, start, nameEnd + 1) | |
| 488 | return nameEnd + 1 | |
| 489 | } | |
| 490 | this.addToken(TokenType.Ident, start, nameEnd) | |
| 491 | return nameEnd | |
| 492 | } | |
| 493 | ||
| 494 | private consumeUrl(start: number, nameEnd: number): number { | |
| 495 | const src = this.source | |
| 496 | const len = src.length | |
| 497 | let i = nameEnd + 1 | |
| 498 | while (i < len && isWhitespace(src.charCodeAt(i))) | |
| 499 | i++ | |
| 500 | while (i < len) { | |
| 501 | const c = src.charCodeAt(i) | |
| 502 | if (c === 41 /* ) */) { | |
| 503 | i++ | |
| 504 | this.addToken(TokenType.Url, start, i) | |
| 505 | return i | |
| 506 | } | |
| 507 | if (isWhitespace(c)) { | |
| 508 | const wsStart = i | |
| 509 | while (i < len && isWhitespace(src.charCodeAt(i))) | |
| 510 | i++ | |
| 511 | if (i >= len) { | |
| 512 | this.addToken(TokenType.Url, start, i) | |
| 513 | return i | |
| 514 | } | |
| 515 | if (src.charCodeAt(i) === 41) { | |
| 516 | i++ | |
| 517 | this.addToken(TokenType.Url, start, i) | |
| 518 | return i | |
| 519 | } | |
| 520 | // bad url — recover by consuming to next ) or eof | |
| 521 | return this.consumeBadUrl(start, wsStart) | |
| 522 | } | |
| 523 | if (c === 34 || c === 39 || c === 40 || isNonPrintable(c)) { | |
| 524 | return this.consumeBadUrl(start, i) | |
| 525 | } | |
| 526 | if (c === 92 /* \ */) { | |
| 527 | if (isValidEscape(c, src.charCodeAt(i + 1))) { | |
| 528 | i = this.consumeEscapeSkip(i + 1) | |
| 529 | continue | |
| 530 | } | |
| 531 | return this.consumeBadUrl(start, i) | |
| 532 | } | |
| 533 | i++ | |
| 534 | } | |
| 535 | this.addToken(TokenType.Url, start, i) | |
| 536 | return i | |
| 537 | } | |
| 538 | ||
| 539 | private consumeBadUrl(start: number, from: number): number { | |
| 540 | const src = this.source | |
| 541 | let i = from | |
| 542 | while (i < src.length) { | |
| 543 | const c = src.charCodeAt(i) | |
| 544 | if (c === 41) { | |
| 545 | i++ | |
| 546 | break | |
| 547 | } | |
| 548 | if (c === 92 && isValidEscape(c, src.charCodeAt(i + 1))) { | |
| 549 | i = this.consumeEscapeSkip(i + 1) | |
| 550 | continue | |
| 551 | } | |
| 552 | i++ | |
| 553 | } | |
| 554 | this.addToken(TokenType.BadUrl, start, i) | |
| 555 | return i | |
| 556 | } | |
| 557 | ||
| 558 | /** Resolve `offset` to (line, column) for error messages. */ | |
| 559 | locate(offset: number): { line: number, column: number } { | |
| 560 | const ls = this.lineStarts | |
| 561 | let lo = 0 | |
| 562 | let hi = ls.length - 1 | |
| 563 | while (lo < hi) { | |
| 564 | const mid = (lo + hi + 1) >> 1 | |
| 565 | if (ls[mid]! <= offset) | |
| 566 | lo = mid | |
| 567 | else | |
| 568 | hi = mid - 1 | |
| 569 | } | |
| 570 | const lineStart = ls[lo]! | |
| 571 | return { line: lo + 1, column: offset - lineStart + 1 } | |
| 572 | } | |
| 573 | } | |
| 574 | ||
| 575 | function buildLineStarts(source: string): number[] { | |
| 576 | const out = [0] | |
| 577 | const len = source.length | |
| 578 | for (let i = 0; i < len; i++) { | |
| 579 | const c = source.charCodeAt(i) | |
| 580 | if (c === 10) { | |
| 581 | out.push(i + 1) | |
| 582 | } | |
| 583 | else if (c === 13) { | |
| 584 | // \r without \n, OR \r\n (treat the LF position consistently) | |
| 585 | if (source.charCodeAt(i + 1) !== 10) | |
| 586 | out.push(i + 1) | |
| 587 | } | |
| 588 | } | |
| 589 | return out | |
| 590 | } | |
| 591 | ||
| 592 | /** Decode a CSS-escaped name slice (e.g. `\\26 B` → `&B`). */ | |
| 593 | /** | |
| 594 | * Decode a CSS string literal slice to its actual character value. | |
| 595 | * `start` / `end` exclude the surrounding quote characters. | |
| 596 | * | |
| 597 | * Handles: | |
| 598 | * - hex escapes (`\26 ` → `&`) | |
| 599 | * - `\"` / `\'` / `\\` | |
| 600 | * - escaped newlines (CSS: backslash followed by newline is removed) | |
| 601 | * - any other `\<char>` → `<char>` | |
| 602 | */ | |
| 603 | export function decodeString(source: string, start: number, end: number): string { | |
| 604 | // Fast path: no escapes → return the slice directly. The vast majority of | |
| 605 | // CSS strings/identifiers have no backslash escapes, so a one-pass scan | |
| 606 | // saves us the per-char concat allocation that the slow path does. | |
| 607 | for (let k = start; k < end; k++) { | |
| 608 | if (source.charCodeAt(k) === 92 /* \\ */) | |
| 609 | return decodeStringSlow(source, start, end, k) | |
| 610 | } | |
| 611 | return source.slice(start, end) | |
| 612 | } | |
| 613 | ||
| 614 | function decodeStringSlow(source: string, start: number, end: number, firstEscape: number): string { | |
| 615 | let out = source.slice(start, firstEscape) | |
| 616 | let i = firstEscape | |
| 617 | while (i < end) { | |
| 618 | const c = source.charCodeAt(i) | |
| 619 | if (c !== 92 /* \\ */) { | |
| 620 | out += source[i]! | |
| 621 | i++ | |
| 622 | continue | |
| 623 | } | |
| 624 | // backslash at end-of-input | |
| 625 | if (i + 1 >= end) { | |
| 626 | i++ | |
| 627 | continue | |
| 628 | } | |
| 629 | // backslash + newline = continuation (removed entirely) | |
| 630 | const next = source.charCodeAt(i + 1) | |
| 631 | if (next === 10) { | |
| 632 | i += 2 | |
| 633 | continue | |
| 634 | } | |
| 635 | if (next === 13) { | |
| 636 | i += source.charCodeAt(i + 2) === 10 ? 3 : 2 | |
| 637 | continue | |
| 638 | } | |
| 639 | // hex escape | |
| 640 | if (isHexDigit(next)) { | |
| 641 | let j = i + 1 | |
| 642 | let hex = '' | |
| 643 | while (hex.length < 6 && j < end && isHexDigit(source.charCodeAt(j))) { | |
| 644 | hex += source[j] | |
| 645 | j++ | |
| 646 | } | |
| 647 | const code = Number.parseInt(hex, 16) | |
| 648 | if (j < end && isWhitespace(source.charCodeAt(j))) | |
| 649 | j++ | |
| 650 | out += code === 0 || (code >= 0xD800 && code <= 0xDFFF) || code > 0x10FFFF | |
| 651 | ? String.fromCodePoint(REPLACEMENT) | |
| 652 | : String.fromCodePoint(code) | |
| 653 | i = j | |
| 654 | continue | |
| 655 | } | |
| 656 | // escape of any other char (including \" \' \\) — keep the char | |
| 657 | out += source[i + 1] | |
| 658 | i += 2 | |
| 659 | } | |
| 660 | return out | |
| 661 | } | |
| 662 | ||
| 663 | export function decodeName(source: string, start: number, end: number): string { | |
| 664 | // Fast path: no escapes → return the slice directly. | |
| 665 | for (let k = start; k < end; k++) { | |
| 666 | if (source.charCodeAt(k) === 92 /* \\ */) | |
| 667 | return decodeNameSlow(source, start, end, k) | |
| 668 | } | |
| 669 | return source.slice(start, end) | |
| 670 | } | |
| 671 | ||
| 672 | function decodeNameSlow(source: string, start: number, end: number, firstEscape: number): string { | |
| 673 | let out = source.slice(start, firstEscape) | |
| 674 | let i = firstEscape | |
| 675 | while (i < end) { | |
| 676 | const c = source.charCodeAt(i) | |
| 677 | if (c !== 92 /* \\ */) { | |
| 678 | out += source[i]! | |
| 679 | i++ | |
| 680 | continue | |
| 681 | } | |
| 682 | // hex escape | |
| 683 | let j = i + 1 | |
| 684 | let hex = '' | |
| 685 | while (hex.length < 6 && j < end && isHexDigit(source.charCodeAt(j))) { | |
| 686 | hex += source[j] | |
| 687 | j++ | |
| 688 | } | |
| 689 | if (hex.length > 0) { | |
| 690 | const code = Number.parseInt(hex, 16) | |
| 691 | if (j < end && isWhitespace(source.charCodeAt(j))) | |
| 692 | j++ | |
| 693 | out += code === 0 || (code >= 0xD800 && code <= 0xDFFF) || code > 0x10FFFF | |
| 694 | ? String.fromCodePoint(REPLACEMENT) | |
| 695 | : String.fromCodePoint(code) | |
| 696 | i = j | |
| 697 | continue | |
| 698 | } | |
| 699 | if (j < end) { | |
| 700 | out += source[j] | |
| 701 | i = j + 1 | |
| 702 | continue | |
| 703 | } | |
| 704 | out += String.fromCodePoint(REPLACEMENT) | |
| 705 | i = j | |
| 706 | } | |
| 707 | return out | |
| 708 | } | |
| @@ -0,0 +1,344 @@ | ||
| 1 | /** | |
| 2 | * AST node types for the CSS parser. | |
| 3 | * | |
| 4 | * Mirrors the css-tree v3 node taxonomy so existing consumers (CSSO, | |
| 5 | * SVGO-style optimisers, etc.) can swap to ts-css without touching their | |
| 6 | * walker callbacks. Node shapes are intentionally narrow — we don't | |
| 7 | * model spec-aware lexer types (no `<color>` / `<length>` validation), | |
| 8 | * because the SVGO/csso pipelines that we target don't use them. | |
| 9 | */ | |
| 10 | ||
| 11 | import type { CssList } from './list' | |
| 12 | ||
| 13 | export type CssNodeType | |
| 14 | = | 'AnPlusB' | |
| 15 | | 'Atrule' | |
| 16 | | 'AtrulePrelude' | |
| 17 | | 'AttributeSelector' | |
| 18 | | 'Block' | |
| 19 | | 'Brackets' | |
| 20 | | 'CDC' | |
| 21 | | 'CDO' | |
| 22 | | 'ClassSelector' | |
| 23 | | 'Combinator' | |
| 24 | | 'Comment' | |
| 25 | | 'Declaration' | |
| 26 | | 'DeclarationList' | |
| 27 | | 'Dimension' | |
| 28 | | 'Function' | |
| 29 | | 'Hash' | |
| 30 | | 'IdSelector' | |
| 31 | | 'Identifier' | |
| 32 | | 'MediaFeature' | |
| 33 | | 'MediaQuery' | |
| 34 | | 'MediaQueryList' | |
| 35 | | 'NestingSelector' | |
| 36 | | 'Nth' | |
| 37 | | 'Number' | |
| 38 | | 'Operator' | |
| 39 | | 'Parentheses' | |
| 40 | | 'Percentage' | |
| 41 | | 'PseudoClassSelector' | |
| 42 | | 'PseudoElementSelector' | |
| 43 | | 'Ratio' | |
| 44 | | 'Raw' | |
| 45 | | 'Rule' | |
| 46 | | 'Selector' | |
| 47 | | 'SelectorList' | |
| 48 | | 'StyleSheet' | |
| 49 | | 'TypeSelector' | |
| 50 | | 'UnicodeRange' | |
| 51 | | 'Url' | |
| 52 | | 'Value' | |
| 53 | | 'WhiteSpace' | |
| 54 | | 'String' | |
| 55 | ||
| 56 | export interface BaseNode { | |
| 57 | type: CssNodeType | |
| 58 | loc?: CssLocation | null | |
| 59 | } | |
| 60 | ||
| 61 | export interface CssLocation { | |
| 62 | source: string | |
| 63 | start: { offset: number, line: number, column: number } | |
| 64 | end: { offset: number, line: number, column: number } | |
| 65 | } | |
| 66 | ||
| 67 | // ----- Containers ----- | |
| 68 | ||
| 69 | export interface StyleSheet extends BaseNode { | |
| 70 | type: 'StyleSheet' | |
| 71 | children: CssList<CssNode> | |
| 72 | } | |
| 73 | ||
| 74 | export interface Rule extends BaseNode { | |
| 75 | type: 'Rule' | |
| 76 | prelude: SelectorList | Raw | |
| 77 | block: Block | |
| 78 | } | |
| 79 | ||
| 80 | export interface Atrule extends BaseNode { | |
| 81 | type: 'Atrule' | |
| 82 | name: string | |
| 83 | prelude: AtrulePrelude | Raw | null | |
| 84 | block: Block | null | |
| 85 | } | |
| 86 | ||
| 87 | export interface AtrulePrelude extends BaseNode { | |
| 88 | type: 'AtrulePrelude' | |
| 89 | children: CssList<CssNode> | |
| 90 | } | |
| 91 | ||
| 92 | export interface Block extends BaseNode { | |
| 93 | type: 'Block' | |
| 94 | children: CssList<CssNode> | |
| 95 | } | |
| 96 | ||
| 97 | // ----- Declarations ----- | |
| 98 | ||
| 99 | export interface Declaration extends BaseNode { | |
| 100 | type: 'Declaration' | |
| 101 | /** | |
| 102 | * `true` when the declaration ends with `!important`. The string-form | |
| 103 | * (e.g. `'!ie'` for IE-prefix hacks) is occasionally produced by other | |
| 104 | * libraries, so the type stays permissive — but ts-css's parser always | |
| 105 | * sets `boolean`. | |
| 106 | */ | |
| 107 | important: boolean | string | |
| 108 | property: string | |
| 109 | value: Value | Raw | |
| 110 | } | |
| 111 | ||
| 112 | export interface DeclarationList extends BaseNode { | |
| 113 | type: 'DeclarationList' | |
| 114 | children: CssList<CssNode> | |
| 115 | } | |
| 116 | ||
| 117 | export interface Value extends BaseNode { | |
| 118 | type: 'Value' | |
| 119 | children: CssList<CssNode> | |
| 120 | } | |
| 121 | ||
| 122 | // ----- Selectors ----- | |
| 123 | ||
| 124 | export interface SelectorList extends BaseNode { | |
| 125 | type: 'SelectorList' | |
| 126 | children: CssList<CssNode> | |
| 127 | } | |
| 128 | ||
| 129 | export interface Selector extends BaseNode { | |
| 130 | type: 'Selector' | |
| 131 | children: CssList<CssNode> | |
| 132 | } | |
| 133 | ||
| 134 | export interface TypeSelector extends BaseNode { | |
| 135 | type: 'TypeSelector' | |
| 136 | name: string | |
| 137 | } | |
| 138 | ||
| 139 | export interface IdSelector extends BaseNode { | |
| 140 | type: 'IdSelector' | |
| 141 | name: string | |
| 142 | } | |
| 143 | ||
| 144 | export interface ClassSelector extends BaseNode { | |
| 145 | type: 'ClassSelector' | |
| 146 | name: string | |
| 147 | } | |
| 148 | ||
| 149 | export interface AttributeSelector extends BaseNode { | |
| 150 | type: 'AttributeSelector' | |
| 151 | name: Identifier | |
| 152 | matcher: string | null | |
| 153 | value: StringNode | Identifier | null | |
| 154 | flags: string | null | |
| 155 | } | |
| 156 | ||
| 157 | export interface PseudoClassSelector extends BaseNode { | |
| 158 | type: 'PseudoClassSelector' | |
| 159 | name: string | |
| 160 | children: CssList<CssNode> | null | |
| 161 | } | |
| 162 | ||
| 163 | export interface PseudoElementSelector extends BaseNode { | |
| 164 | type: 'PseudoElementSelector' | |
| 165 | name: string | |
| 166 | children: CssList<CssNode> | null | |
| 167 | } | |
| 168 | ||
| 169 | export interface Combinator extends BaseNode { | |
| 170 | type: 'Combinator' | |
| 171 | name: string | |
| 172 | } | |
| 173 | ||
| 174 | export interface NestingSelector extends BaseNode { | |
| 175 | type: 'NestingSelector' | |
| 176 | } | |
| 177 | ||
| 178 | // ----- Atomic values ----- | |
| 179 | ||
| 180 | export interface Identifier extends BaseNode { | |
| 181 | type: 'Identifier' | |
| 182 | name: string | |
| 183 | } | |
| 184 | ||
| 185 | export interface NumberNode extends BaseNode { | |
| 186 | type: 'Number' | |
| 187 | value: string | |
| 188 | } | |
| 189 | ||
| 190 | export interface PercentageNode extends BaseNode { | |
| 191 | type: 'Percentage' | |
| 192 | value: string | |
| 193 | } | |
| 194 | ||
| 195 | export interface DimensionNode extends BaseNode { | |
| 196 | type: 'Dimension' | |
| 197 | value: string | |
| 198 | unit: string | |
| 199 | } | |
| 200 | ||
| 201 | export interface StringNode extends BaseNode { | |
| 202 | type: 'String' | |
| 203 | value: string | |
| 204 | } | |
| 205 | ||
| 206 | export interface UrlNode extends BaseNode { | |
| 207 | type: 'Url' | |
| 208 | value: string | |
| 209 | } | |
| 210 | ||
| 211 | export interface HashNode extends BaseNode { | |
| 212 | type: 'Hash' | |
| 213 | name: string | |
| 214 | } | |
| 215 | ||
| 216 | export interface OperatorNode extends BaseNode { | |
| 217 | type: 'Operator' | |
| 218 | value: string | |
| 219 | } | |
| 220 | ||
| 221 | export interface FunctionNode extends BaseNode { | |
| 222 | type: 'Function' | |
| 223 | name: string | |
| 224 | children: CssList<CssNode> | |
| 225 | } | |
| 226 | ||
| 227 | export interface ParenthesesNode extends BaseNode { | |
| 228 | type: 'Parentheses' | |
| 229 | children: CssList<CssNode> | |
| 230 | } | |
| 231 | ||
| 232 | export interface BracketsNode extends BaseNode { | |
| 233 | type: 'Brackets' | |
| 234 | children: CssList<CssNode> | |
| 235 | } | |
| 236 | ||
| 237 | export interface RawNode extends BaseNode { | |
| 238 | type: 'Raw' | |
| 239 | value: string | |
| 240 | } | |
| 241 | ||
| 242 | export interface CommentNode extends BaseNode { | |
| 243 | type: 'Comment' | |
| 244 | value: string | |
| 245 | } | |
| 246 | ||
| 247 | export interface WhiteSpaceNode extends BaseNode { | |
| 248 | type: 'WhiteSpace' | |
| 249 | value: string | |
| 250 | } | |
| 251 | ||
| 252 | export interface CDONode extends BaseNode { type: 'CDO' } | |
| 253 | export interface CDCNode extends BaseNode { type: 'CDC' } | |
| 254 | ||
| 255 | export interface NthNode extends BaseNode { | |
| 256 | type: 'Nth' | |
| 257 | nth: AnPlusBNode | Identifier | |
| 258 | selector: SelectorList | null | |
| 259 | } | |
| 260 | ||
| 261 | export interface AnPlusBNode extends BaseNode { | |
| 262 | type: 'AnPlusB' | |
| 263 | a: string | null | |
| 264 | b: string | null | |
| 265 | } | |
| 266 | ||
| 267 | export interface RatioNode extends BaseNode { | |
| 268 | type: 'Ratio' | |
| 269 | left: NumberNode | |
| 270 | right: NumberNode | |
| 271 | } | |
| 272 | ||
| 273 | export interface UnicodeRangeNode extends BaseNode { | |
| 274 | type: 'UnicodeRange' | |
| 275 | value: string | |
| 276 | } | |
| 277 | ||
| 278 | export interface MediaQueryListNode extends BaseNode { | |
| 279 | type: 'MediaQueryList' | |
| 280 | children: CssList<CssNode> | |
| 281 | } | |
| 282 | ||
| 283 | export interface MediaQueryNode extends BaseNode { | |
| 284 | type: 'MediaQuery' | |
| 285 | children: CssList<CssNode> | |
| 286 | } | |
| 287 | ||
| 288 | export interface MediaFeatureNode extends BaseNode { | |
| 289 | type: 'MediaFeature' | |
| 290 | name: string | |
| 291 | value: NumberNode | DimensionNode | RatioNode | Identifier | null | |
| 292 | } | |
| 293 | ||
| 294 | // ----- Aliases ----- | |
| 295 | ||
| 296 | export type Raw = RawNode | |
| 297 | ||
| 298 | export type CssNode | |
| 299 | = | StyleSheet | Rule | Atrule | AtrulePrelude | Block | |
| 300 | | Declaration | DeclarationList | Value | |
| 301 | | SelectorList | Selector | TypeSelector | IdSelector | ClassSelector | |
| 302 | | AttributeSelector | PseudoClassSelector | PseudoElementSelector | |
| 303 | | Combinator | NestingSelector | |
| 304 | | Identifier | NumberNode | PercentageNode | DimensionNode | |
| 305 | | StringNode | UrlNode | HashNode | OperatorNode | |
| 306 | | FunctionNode | ParenthesesNode | BracketsNode | |
| 307 | | RawNode | CommentNode | WhiteSpaceNode | CDONode | CDCNode | |
| 308 | | NthNode | AnPlusBNode | RatioNode | UnicodeRangeNode | |
| 309 | | MediaQueryListNode | MediaQueryNode | MediaFeatureNode | |
| 310 | ||
| 311 | export type ParseContext | |
| 312 | = | 'stylesheet' | |
| 313 | | 'atrule' | |
| 314 | | 'atrulePrelude' | |
| 315 | | 'mediaQuery' | |
| 316 | | 'mediaQueryList' | |
| 317 | | 'rule' | |
| 318 | | 'selectorList' | |
| 319 | | 'selector' | |
| 320 | | 'block' | |
| 321 | | 'declarationList' | |
| 322 | | 'declaration' | |
| 323 | | 'value' | |
| 324 | | 'raw' | |
| 325 | ||
| 326 | export interface ParseOptions { | |
| 327 | /** Source filename used in error/loc reports. */ | |
| 328 | filename?: string | |
| 329 | /** Track source locations on every node. */ | |
| 330 | positions?: boolean | |
| 331 | /** Grammar context to parse `source` as (default `'stylesheet'`). */ | |
| 332 | context?: ParseContext | |
| 333 | /** When false, declaration values are stored as `Raw` rather than parsed. */ | |
| 334 | parseValue?: boolean | |
| 335 | /** When false, at-rule preludes are stored as `Raw` rather than parsed. */ | |
| 336 | parseAtrulePrelude?: boolean | |
| 337 | /** When false, custom-property values are stored as `Raw`. */ | |
| 338 | parseCustomProperty?: boolean | |
| 339 | /** When false, rule preludes (selector lists) are stored as `Raw`. */ | |
| 340 | parseRulePrelude?: boolean | |
| 341 | /** Forwarded to consumer's error handler. */ | |
| 342 | // eslint-disable-next-line pickier/no-unused-vars | |
| 343 | onParseError?: (error: SyntaxError, fallbackNode: CssNode) => void | |
| 344 | } | |
| @@ -0,0 +1,252 @@ | ||
| 1 | /** | |
| 2 | * Tree-walker for the CSS AST. Mirrors css-tree's `walk(ast, callback)` and | |
| 3 | * `walk(ast, { visit, enter, leave })` shapes — including the `walk.skip` | |
| 4 | * sentinel and the `this.atrule` / `this.rule` context exposed inside | |
| 5 | * callbacks. | |
| 6 | */ | |
| 7 | ||
| 8 | import type { Atrule, CssNode, Declaration, Rule } from './types' | |
| 9 | import { CssList, type ListItem } from './list' | |
| 10 | ||
| 11 | const SKIP: symbol = Symbol('walkSkip') | |
| 12 | const STOP: symbol = Symbol('walkStop') | |
| 13 | ||
| 14 | export interface WalkContext { | |
| 15 | root: CssNode | |
| 16 | stylesheet: CssNode | null | |
| 17 | atrule: Atrule | null | |
| 18 | atrulePrelude: CssNode | null | |
| 19 | rule: Rule | null | |
| 20 | selector: CssNode | null | |
| 21 | block: CssNode | null | |
| 22 | declaration: Declaration | null | |
| 23 | function: CssNode | null | |
| 24 | } | |
| 25 | ||
| 26 | export type WalkVisit = CssNode['type'] | |
| 27 | ||
| 28 | // Narrow CssNode by its `type` discriminant — used to type the | |
| 29 | // `visit: 'Rule'` filter so consumer callbacks receive a `Rule`, not the | |
| 30 | // full union. | |
| 31 | export type CssNodeOfType<T extends WalkVisit> = Extract<CssNode, { type: T }> | |
| 32 | ||
| 33 | // eslint-disable-next-line pickier/no-unused-vars | |
| 34 | type VisitorCallback<N> = (this: WalkContext, node: N, item: ListItem<CssNode> | null, list: CssList<CssNode> | null) => void | symbol | |
| 35 | ||
| 36 | /** | |
| 37 | * Object-form visitor. The `T extends WalkVisit` generic is inferred from | |
| 38 | * the literal value of `visit`, so writing | |
| 39 | * | |
| 40 | * walk(ast, { visit: 'Rule', enter(node) { … } }) | |
| 41 | * | |
| 42 | * narrows `node` to `Rule`. Without `visit`, callbacks receive the full | |
| 43 | * `CssNode` union as `enter`/`leave` arguments. | |
| 44 | */ | |
| 45 | export interface WalkVisitorObject<T extends WalkVisit | undefined = undefined> { | |
| 46 | visit?: T | |
| 47 | reverse?: boolean | |
| 48 | enter?: VisitorCallback<T extends WalkVisit ? CssNodeOfType<T> : CssNode> | |
| 49 | leave?: VisitorCallback<T extends WalkVisit ? CssNodeOfType<T> : CssNode> | |
| 50 | } | |
| 51 | ||
| 52 | /** Plain-function visitor — runs against every node. */ | |
| 53 | export type WalkVisitorFunction = VisitorCallback<CssNode> | |
| 54 | ||
| 55 | export type WalkVisitor = WalkVisitorFunction | WalkVisitorObject<WalkVisit | undefined> | |
| 56 | ||
| 57 | function newContext(root: CssNode): WalkContext { | |
| 58 | return { | |
| 59 | root, | |
| 60 | stylesheet: null, | |
| 61 | atrule: null, | |
| 62 | atrulePrelude: null, | |
| 63 | rule: null, | |
| 64 | selector: null, | |
| 65 | block: null, | |
| 66 | declaration: null, | |
| 67 | function: null, | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Walk every node of `root`, calling `visitor` (or visitor.enter / .leave). | |
| 73 | * Returns: | |
| 74 | * - undefined when traversal completes, | |
| 75 | * - the SKIP / STOP sentinel propagated from a visitor. | |
| 76 | * | |
| 77 | * `walk.skip` / `walk.stop` are exposed as static-symbol sentinels. | |
| 78 | */ | |
| 79 | export interface WalkFn { | |
| 80 | // 1. visitor function — every node (no narrowing) | |
| 81 | // eslint-disable-next-line pickier/no-unused-vars | |
| 82 | (root: CssNode, visitor: WalkVisitorFunction): symbol | undefined | |
| 83 | // 2. visitor object with `visit: '<NodeType>'` — narrowed callbacks | |
| 84 | // eslint-disable-next-line pickier/no-unused-vars | |
| 85 | <T extends WalkVisit>(root: CssNode, visitor: WalkVisitorObject<T>): symbol | undefined | |
| 86 | skip: symbol | |
| 87 | stop: symbol | |
| 88 | } | |
| 89 | ||
| 90 | function walkImpl(root: CssNode, visitor: WalkVisitor): symbol | undefined { | |
| 91 | const ctx = newContext(root) | |
| 92 | const enter = typeof visitor === 'function' ? visitor : visitor.enter | |
| 93 | const leave = typeof visitor === 'function' ? null : (visitor.leave ?? null) | |
| 94 | const filter = typeof visitor === 'function' ? null : (visitor.visit ?? null) | |
| 95 | const reverse = typeof visitor === 'function' ? false : (visitor.reverse ?? false) | |
| 96 | ||
| 97 | function visitNode(node: CssNode, item: ListItem<CssNode> | null, list: CssList<CssNode> | null): symbol | undefined { | |
| 98 | let pushed: keyof WalkContext | null = null | |
| 99 | let prev: any | |
| 100 | switch (node.type) { | |
| 101 | case 'StyleSheet': | |
| 102 | pushed = 'stylesheet' | |
| 103 | prev = ctx.stylesheet | |
| 104 | ctx.stylesheet = node | |
| 105 | break | |
| 106 | case 'Atrule': | |
| 107 | pushed = 'atrule' | |
| 108 | prev = ctx.atrule | |
| 109 | ctx.atrule = node | |
| 110 | break | |
| 111 | case 'AtrulePrelude': | |
| 112 | pushed = 'atrulePrelude' | |
| 113 | prev = ctx.atrulePrelude | |
| 114 | ctx.atrulePrelude = node | |
| 115 | break | |
| 116 | case 'Rule': | |
| 117 | pushed = 'rule' | |
| 118 | prev = ctx.rule | |
| 119 | ctx.rule = node | |
| 120 | break | |
| 121 | case 'Selector': | |
| 122 | pushed = 'selector' | |
| 123 | prev = ctx.selector | |
| 124 | ctx.selector = node | |
| 125 | break | |
| 126 | case 'Block': | |
| 127 | pushed = 'block' | |
| 128 | prev = ctx.block | |
| 129 | ctx.block = node | |
| 130 | break | |
| 131 | case 'Declaration': | |
| 132 | pushed = 'declaration' | |
| 133 | prev = ctx.declaration | |
| 134 | ctx.declaration = node | |
| 135 | break | |
| 136 | case 'Function': | |
| 137 | pushed = 'function' | |
| 138 | prev = ctx.function | |
| 139 | ctx.function = node | |
| 140 | break | |
| 141 | } | |
| 142 | ||
| 143 | let result: symbol | undefined | |
| 144 | if ((filter === null || filter === node.type) && enter) { | |
| 145 | const r = enter.call(ctx, node, item, list) | |
| 146 | if (r === SKIP) { | |
| 147 | if (pushed) (ctx as any)[pushed] = prev | |
| 148 | return undefined | |
| 149 | } | |
| 150 | if (r === STOP) { | |
| 151 | if (pushed) (ctx as any)[pushed] = prev | |
| 152 | return STOP | |
| 153 | } | |
| 154 | } | |
| 155 | ||
| 156 | // descend | |
| 157 | result = walkChildren(node, reverse, visitNode) | |
| 158 | if (result === STOP) { | |
| 159 | if (pushed) (ctx as any)[pushed] = prev | |
| 160 | return STOP | |
| 161 | } | |
| 162 | ||
| 163 | if ((filter === null || filter === node.type) && leave) { | |
| 164 | const r = leave.call(ctx, node, item, list) | |
| 165 | if (r === STOP) { | |
| 166 | if (pushed) (ctx as any)[pushed] = prev | |
| 167 | return STOP | |
| 168 | } | |
| 169 | } | |
| 170 | if (pushed) (ctx as any)[pushed] = prev | |
| 171 | return undefined | |
| 172 | } | |
| 173 | ||
| 174 | return visitNode(root, null, null) | |
| 175 | } | |
| 176 | ||
| 177 | export const walk: WalkFn = Object.assign(walkImpl, { skip: SKIP, stop: STOP }) as WalkFn | |
| 178 | ||
| 179 | export const walkSkip: symbol = SKIP | |
| 180 | export const walkStop: symbol = STOP | |
| 181 | ||
| 182 | function walkChildren( | |
| 183 | node: CssNode, | |
| 184 | reverse: boolean, | |
| 185 | visitNode: (n: CssNode, item: ListItem<CssNode> | null, list: CssList<CssNode> | null) => symbol | undefined, | |
| 186 | ): symbol | undefined { | |
| 187 | // Composite children | |
| 188 | if ('children' in node && node.children instanceof CssList) { | |
| 189 | return walkList(node.children, reverse, visitNode) | |
| 190 | } | |
| 191 | // Rule prelude/block | |
| 192 | if (node.type === 'Rule') { | |
| 193 | const r1 = visitNode(node.prelude as CssNode, null, null) | |
| 194 | if (r1 === STOP) return STOP | |
| 195 | const r2 = visitNode(node.block as CssNode, null, null) | |
| 196 | if (r2 === STOP) return STOP | |
| 197 | return undefined | |
| 198 | } | |
| 199 | if (node.type === 'Atrule') { | |
| 200 | if (node.prelude) { | |
| 201 | const r = visitNode(node.prelude, null, null) | |
| 202 | if (r === STOP) return STOP | |
| 203 | } | |
| 204 | if (node.block) { | |
| 205 | const r = visitNode(node.block, null, null) | |
| 206 | if (r === STOP) return STOP | |
| 207 | } | |
| 208 | return undefined | |
| 209 | } | |
| 210 | if (node.type === 'Declaration') { | |
| 211 | return visitNode(node.value as CssNode, null, null) | |
| 212 | } | |
| 213 | if (node.type === 'AttributeSelector') { | |
| 214 | const r = visitNode(node.name, null, null) | |
| 215 | if (r === STOP) return STOP | |
| 216 | if (node.value) | |
| 217 | return visitNode(node.value, null, null) | |
| 218 | return undefined | |
| 219 | } | |
| 220 | return undefined | |
| 221 | } | |
| 222 | ||
| 223 | function walkList( | |
| 224 | list: CssList<CssNode>, | |
| 225 | reverse: boolean, | |
| 226 | visitNode: (n: CssNode, item: ListItem<CssNode> | null, list: CssList<CssNode> | null) => symbol | undefined, | |
| 227 | ): symbol | undefined { | |
| 228 | // The list's `forEach` callback already receives the current `ListItem` | |
| 229 | // as its second argument — passing it straight to `visitNode` avoids the | |
| 230 | // O(N) `findItem` linear scan per child that the previous implementation | |
| 231 | // performed (turning every walk into O(N²)). | |
| 232 | let stopped: symbol | undefined | |
| 233 | if (reverse) { | |
| 234 | list.forEachRight((data, item, l) => { | |
| 235 | if (stopped) | |
| 236 | return | |
| 237 | const r = visitNode(data, item, l) | |
| 238 | if (r === STOP) | |
| 239 | stopped = STOP | |
| 240 | }) | |
| 241 | } | |
| 242 | else { | |
| 243 | list.forEach((data, item, l) => { | |
| 244 | if (stopped) | |
| 245 | return | |
| 246 | const r = visitNode(data, item, l) | |
| 247 | if (r === STOP) | |
| 248 | stopped = STOP | |
| 249 | }) | |
| 250 | } | |
| 251 | return stopped | |
| 252 | } | |
| @@ -0,0 +1,243 @@ | ||
| 1 | /** | |
| 2 | * Attribute-selector matchers. One factory per `AttributeAction` returning | |
| 3 | * a predicate over the consumer's element type (via the adapter). | |
| 4 | */ | |
| 5 | ||
| 6 | import type { AttributeSelectorNode } from '../what' | |
| 7 | import type { Adapter, CompiledQuery, Options } from './types' | |
| 8 | import { ALWAYS_TRUE } from './helpers/always-true' | |
| 9 | ||
| 10 | function isWsCode(c: number): boolean { | |
| 11 | return c === 32 || c === 9 || c === 10 || c === 12 || c === 13 | |
| 12 | } | |
| 13 | ||
| 14 | /** | |
| 15 | * Whitespace-separated token containment — equivalent to the `~=` operator | |
| 16 | * (and the `.class` shorthand), but without the regex compile + RegExp.test | |
| 17 | * overhead of a per-element check. | |
| 18 | */ | |
| 19 | function containsWsToken(haystack: string, needle: string): boolean { | |
| 20 | const nlen = needle.length | |
| 21 | const hlen = haystack.length | |
| 22 | if (nlen === 0 || nlen > hlen) | |
| 23 | return false | |
| 24 | let idx = haystack.indexOf(needle) | |
| 25 | while (idx >= 0) { | |
| 26 | const before = idx === 0 || isWsCode(haystack.charCodeAt(idx - 1)) | |
| 27 | const afterIdx = idx + nlen | |
| 28 | const after = afterIdx === hlen || isWsCode(haystack.charCodeAt(afterIdx)) | |
| 29 | if (before && after) | |
| 30 | return true | |
| 31 | idx = haystack.indexOf(needle, idx + 1) | |
| 32 | } | |
| 33 | return false | |
| 34 | } | |
| 35 | ||
| 36 | // eslint-disable-next-line pickier/no-unused-vars | |
| 37 | function caseInsensitive(value: string, attr: AttributeSelectorNode, options: Options<unknown, any>): boolean { | |
| 38 | if (attr.ignoreCase === true) | |
| 39 | return true | |
| 40 | if (attr.ignoreCase === false) | |
| 41 | return false | |
| 42 | if (attr.ignoreCase === 'quirks') | |
| 43 | return !options.xmlMode | |
| 44 | return false | |
| 45 | } | |
| 46 | ||
| 47 | export function compileAttribute<Node, ElementNode extends Node>( | |
| 48 | selector: AttributeSelectorNode, | |
| 49 | options: Options<Node, ElementNode>, | |
| 50 | next: CompiledQuery<ElementNode>, | |
| 51 | ): CompiledQuery<ElementNode> { | |
| 52 | const adapter = options.adapter | |
| 53 | const action = selector.action | |
| 54 | const name = selector.name | |
| 55 | let value = selector.value | |
| 56 | const ci = caseInsensitive(value, selector, options as any) | |
| 57 | if (ci) | |
| 58 | value = value.toLowerCase() | |
| 59 | return wrap(action, name, value, ci, adapter, next) | |
| 60 | } | |
| 61 | ||
| 62 | function wrap<Node, ElementNode extends Node>( | |
| 63 | action: string, | |
| 64 | name: string, | |
| 65 | value: string, | |
| 66 | ci: boolean, | |
| 67 | adapter: Adapter<Node, ElementNode>, | |
| 68 | next: CompiledQuery<ElementNode>, | |
| 69 | ): CompiledQuery<ElementNode> { | |
| 70 | const isLeaf = next === ALWAYS_TRUE | |
| 71 | switch (action) { | |
| 72 | case 'exists': | |
| 73 | if (isLeaf) | |
| 74 | return e => adapter.hasAttrib(e, name) | |
| 75 | return e => adapter.hasAttrib(e, name) && next(e) | |
| 76 | ||
| 77 | case 'equals': | |
| 78 | if (!ci) { | |
| 79 | // String equality with `null`/`undefined` already short-circuits to | |
| 80 | // false, so we can skip the explicit null guard — matches | |
| 81 | // css-select's equality fast path. | |
| 82 | if (isLeaf) | |
| 83 | return e => adapter.getAttributeValue(e, name) === value | |
| 84 | return e => adapter.getAttributeValue(e, name) === value && next(e) | |
| 85 | } | |
| 86 | if (isLeaf) { | |
| 87 | return (e) => { | |
| 88 | const v = adapter.getAttributeValue(e, name) | |
| 89 | if (v == null) | |
| 90 | return false | |
| 91 | return v.toLowerCase() === value | |
| 92 | } | |
| 93 | } | |
| 94 | return (e) => { | |
| 95 | const v = adapter.getAttributeValue(e, name) | |
| 96 | if (v == null) | |
| 97 | return false | |
| 98 | return v.toLowerCase() === value && next(e) | |
| 99 | } | |
| 100 | ||
| 101 | case 'not': | |
| 102 | return (e) => { | |
| 103 | const v = adapter.getAttributeValue(e, name) | |
| 104 | if (v == null) | |
| 105 | return next(e) | |
| 106 | return (ci ? v.toLowerCase() : v) !== value && next(e) | |
| 107 | } | |
| 108 | ||
| 109 | case 'start': | |
| 110 | if (value === '') | |
| 111 | return () => false | |
| 112 | if (isLeaf) { | |
| 113 | return (e) => { | |
| 114 | const v = adapter.getAttributeValue(e, name) | |
| 115 | if (v == null) | |
| 116 | return false | |
| 117 | return (ci ? v.toLowerCase() : v).startsWith(value) | |
| 118 | } | |
| 119 | } | |
| 120 | return (e) => { | |
| 121 | const v = adapter.getAttributeValue(e, name) | |
| 122 | if (v == null) | |
| 123 | return false | |
| 124 | return (ci ? v.toLowerCase() : v).startsWith(value) && next(e) | |
| 125 | } | |
| 126 | ||
| 127 | case 'end': | |
| 128 | if (value === '') | |
| 129 | return () => false | |
| 130 | if (isLeaf) { | |
| 131 | return (e) => { | |
| 132 | const v = adapter.getAttributeValue(e, name) | |
| 133 | if (v == null) | |
| 134 | return false | |
| 135 | return (ci ? v.toLowerCase() : v).endsWith(value) | |
| 136 | } | |
| 137 | } | |
| 138 | return (e) => { | |
| 139 | const v = adapter.getAttributeValue(e, name) | |
| 140 | if (v == null) | |
| 141 | return false | |
| 142 | return (ci ? v.toLowerCase() : v).endsWith(value) && next(e) | |
| 143 | } | |
| 144 | ||
| 145 | case 'any': { | |
| 146 | if (value === '') | |
| 147 | return () => false | |
| 148 | if (ci) { | |
| 149 | if (isLeaf) { | |
| 150 | return (e) => { | |
| 151 | const v = adapter.getAttributeValue(e, name) | |
| 152 | if (v == null) | |
| 153 | return false | |
| 154 | return v.toLowerCase().indexOf(value) >= 0 | |
| 155 | } | |
| 156 | } | |
| 157 | return (e) => { | |
| 158 | const v = adapter.getAttributeValue(e, name) | |
| 159 | if (v == null) | |
| 160 | return false | |
| 161 | return v.toLowerCase().indexOf(value) >= 0 && next(e) | |
| 162 | } | |
| 163 | } | |
| 164 | if (isLeaf) { | |
| 165 | return (e) => { | |
| 166 | const v = adapter.getAttributeValue(e, name) | |
| 167 | if (v == null) | |
| 168 | return false | |
| 169 | return v.indexOf(value) >= 0 | |
| 170 | } | |
| 171 | } | |
| 172 | return (e) => { | |
| 173 | const v = adapter.getAttributeValue(e, name) | |
| 174 | if (v == null) | |
| 175 | return false | |
| 176 | return v.indexOf(value) >= 0 && next(e) | |
| 177 | } | |
| 178 | } | |
| 179 | ||
| 180 | case 'element': { | |
| 181 | if (value === '' || /\s/.test(value)) | |
| 182 | return () => false | |
| 183 | if (ci) { | |
| 184 | if (isLeaf) { | |
| 185 | return (e) => { | |
| 186 | const v = adapter.getAttributeValue(e, name) | |
| 187 | if (v == null) | |
| 188 | return false | |
| 189 | return containsWsToken(v.toLowerCase(), value) | |
| 190 | } | |
| 191 | } | |
| 192 | return (e) => { | |
| 193 | const v = adapter.getAttributeValue(e, name) | |
| 194 | if (v == null) | |
| 195 | return false | |
| 196 | return containsWsToken(v.toLowerCase(), value) && next(e) | |
| 197 | } | |
| 198 | } | |
| 199 | if (isLeaf) { | |
| 200 | return (e) => { | |
| 201 | const v = adapter.getAttributeValue(e, name) | |
| 202 | if (v == null) | |
| 203 | return false | |
| 204 | return containsWsToken(v, value) | |
| 205 | } | |
| 206 | } | |
| 207 | return (e) => { | |
| 208 | const v = adapter.getAttributeValue(e, name) | |
| 209 | if (v == null) | |
| 210 | return false | |
| 211 | return containsWsToken(v, value) && next(e) | |
| 212 | } | |
| 213 | } | |
| 214 | ||
| 215 | case 'hyphen': | |
| 216 | if (isLeaf) { | |
| 217 | return (e) => { | |
| 218 | const v = adapter.getAttributeValue(e, name) | |
| 219 | if (v == null) | |
| 220 | return false | |
| 221 | const lower = ci ? v.toLowerCase() : v | |
| 222 | if (lower === value) | |
| 223 | return true | |
| 224 | if (lower.startsWith(value) && lower.charAt(value.length) === '-') | |
| 225 | return true | |
| 226 | return false | |
| 227 | } | |
| 228 | } | |
| 229 | return (e) => { | |
| 230 | const v = adapter.getAttributeValue(e, name) | |
| 231 | if (v == null) | |
| 232 | return false | |
| 233 | const lower = ci ? v.toLowerCase() : v | |
| 234 | if (lower === value) | |
| 235 | return next(e) | |
| 236 | if (lower.startsWith(value) && lower.charAt(value.length) === '-') | |
| 237 | return next(e) | |
| 238 | return false | |
| 239 | } | |
| 240 | } | |
| 241 | return _ => { void _; return false } | |
| 242 | } | |
| 243 | ||
| @@ -0,0 +1,55 @@ | ||
| 1 | /** | |
| 2 | * Compile a *simple* selector token (tag/universal/id/class/attr/pseudo) | |
| 3 | * into a predicate. Combinators (descendant/child/sibling/adjacent/parent) | |
| 4 | * are handled at a higher level — see pseudo-selectors/selectors.ts. | |
| 5 | * | |
| 6 | * `next` is the predicate for the rest of the compound chain to the right | |
| 7 | * of this token. When the caller has nothing further to test, it passes the | |
| 8 | * shared `ALWAYS_TRUE` sentinel — each branch detects that and returns a | |
| 9 | * leaf predicate without the trailing `&& next(e)` indirection. Skipping | |
| 10 | * the no-op call shaves ~10 % off `selectAll` on tight loops. | |
| 11 | */ | |
| 12 | ||
| 13 | import type { Selector } from '../what' | |
| 14 | import type { CompiledQuery, Options } from './types' | |
| 15 | import { compileAttribute } from './attributes' | |
| 16 | import { ALWAYS_TRUE } from './helpers/always-true' | |
| 17 | import { compilePseudo } from './pseudo-selectors' | |
| 18 | ||
| 19 | type Compiled<E> = CompiledQuery<E> | |
| 20 | ||
| 21 | export { ALWAYS_TRUE } | |
| 22 | ||
| 23 | export function compileToken<Node, ElementNode extends Node>( | |
| 24 | token: Selector, | |
| 25 | options: Options<Node, ElementNode>, | |
| 26 | next: Compiled<ElementNode>, | |
| 27 | ): Compiled<ElementNode> { | |
| 28 | const adapter = options.adapter | |
| 29 | const isLeaf = next === ALWAYS_TRUE | |
| 30 | switch (token.type) { | |
| 31 | case 'tag': { | |
| 32 | const name = options.lowerCaseTags !== false && !options.xmlMode ? token.name.toLowerCase() : token.name | |
| 33 | if (isLeaf) | |
| 34 | return e => adapter.getName(e) === name | |
| 35 | return e => adapter.getName(e) === name && next(e) | |
| 36 | } | |
| 37 | case 'universal': | |
| 38 | return next | |
| 39 | case 'attribute': | |
| 40 | return compileAttribute(token, options, next) | |
| 41 | case 'pseudo': | |
| 42 | case 'pseudo-element': | |
| 43 | return compilePseudo(token as any, options, next) as any | |
| 44 | case 'descendant': | |
| 45 | case 'child': | |
| 46 | case 'parent': | |
| 47 | case 'sibling': | |
| 48 | case 'adjacent': | |
| 49 | case 'column-combinator': | |
| 50 | // Should never reach here — combinators are handled by selectors.ts | |
| 51 | // before we get a chance to dispatch on them. | |
| 52 | return next | |
| 53 | } | |
| 54 | return _ => { void _; return false } | |
| 55 | } | |
| @@ -0,0 +1,7 @@ | ||
| 1 | /** | |
| 2 | * Singleton "always-matches" predicate. Used as the leaf sentinel in | |
| 3 | * compound-selector chains so each compileX factory can detect the leaf | |
| 4 | * case and skip the trailing `&& next(e)` indirection. | |
| 5 | */ | |
| 6 | ||
| 7 | export const ALWAYS_TRUE: (_: any) => boolean = (_: any): boolean => { void _; return true } | |
| @@ -0,0 +1,91 @@ | ||
| 1 | /** | |
| 2 | * Iterative tree search for selectAll / selectOne. Mirrors `css-select`'s | |
| 3 | * approach: use a stack of `(children, index)` pairs to walk the tree | |
| 4 | * depth-first without recursion, avoiding the per-recursion frame and the | |
| 5 | * `out.push(...spread)` allocations a naive recursive findAll incurs. | |
| 6 | * | |
| 7 | * We deliberately ignore `adapter.findAll` even when the consumer supplies | |
| 8 | * one, because forcing every selectAll through the adapter's recursion is | |
| 9 | * the single biggest cost on tree walks. Adapters are still consulted for | |
| 10 | * `isTag` / `getChildren` / `getName`. | |
| 11 | */ | |
| 12 | ||
| 13 | import type { Adapter, CompiledQuery } from '../types' | |
| 14 | ||
| 15 | export function findAll<Node, ElementNode extends Node>( | |
| 16 | query: CompiledQuery<ElementNode>, | |
| 17 | nodes: ReadonlyArray<Node>, | |
| 18 | adapter: Adapter<Node, ElementNode>, | |
| 19 | xmlMode: boolean, | |
| 20 | ): ElementNode[] { | |
| 21 | const result: ElementNode[] = [] | |
| 22 | const nodeStack: ReadonlyArray<Node>[] = [nodes] | |
| 23 | const indexStack: number[] = [0] | |
| 24 | for (; ;) { | |
| 25 | if (indexStack[0]! >= nodeStack[0]!.length) { | |
| 26 | if (nodeStack.length === 1) | |
| 27 | return result | |
| 28 | nodeStack.shift() | |
| 29 | indexStack.shift() | |
| 30 | continue | |
| 31 | } | |
| 32 | const element = nodeStack[0]![indexStack[0]!++]! | |
| 33 | if (!adapter.isTag(element)) | |
| 34 | continue | |
| 35 | if (query(element as unknown as ElementNode)) | |
| 36 | result.push(element as unknown as ElementNode) | |
| 37 | if (xmlMode || adapter.getName(element as unknown as ElementNode) !== 'template') { | |
| 38 | const children = adapter.getChildren(element as unknown as ElementNode) as Node[] | |
| 39 | if (children.length > 0) { | |
| 40 | nodeStack.unshift(children) | |
| 41 | indexStack.unshift(0) | |
| 42 | } | |
| 43 | } | |
| 44 | } | |
| 45 | } | |
| 46 | ||
| 47 | export function findOne<Node, ElementNode extends Node>( | |
| 48 | query: CompiledQuery<ElementNode>, | |
| 49 | nodes: ReadonlyArray<Node>, | |
| 50 | adapter: Adapter<Node, ElementNode>, | |
| 51 | xmlMode: boolean, | |
| 52 | ): ElementNode | null { | |
| 53 | const nodeStack: ReadonlyArray<Node>[] = [nodes] | |
| 54 | const indexStack: number[] = [0] | |
| 55 | for (; ;) { | |
| 56 | if (indexStack[0]! >= nodeStack[0]!.length) { | |
| 57 | if (nodeStack.length === 1) | |
| 58 | return null | |
| 59 | nodeStack.shift() | |
| 60 | indexStack.shift() | |
| 61 | continue | |
| 62 | } | |
| 63 | const element = nodeStack[0]![indexStack[0]!++]! | |
| 64 | if (!adapter.isTag(element)) | |
| 65 | continue | |
| 66 | if (query(element as unknown as ElementNode)) | |
| 67 | return element as unknown as ElementNode | |
| 68 | if (xmlMode || adapter.getName(element as unknown as ElementNode) !== 'template') { | |
| 69 | const children = adapter.getChildren(element as unknown as ElementNode) as Node[] | |
| 70 | if (children.length > 0) { | |
| 71 | nodeStack.unshift(children) | |
| 72 | indexStack.unshift(0) | |
| 73 | } | |
| 74 | } | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * Convert the user-supplied root into a search context. Mirrors | |
| 80 | * `css-select`'s `prepareContext` — a single root node is replaced by its | |
| 81 | * children (so the root itself isn't a candidate); arrays go through | |
| 82 | * `removeSubsets` to avoid testing nested selections twice. | |
| 83 | */ | |
| 84 | export function prepareContext<Node, ElementNode extends Node>( | |
| 85 | elements: Node | Node[], | |
| 86 | adapter: Adapter<Node, ElementNode>, | |
| 87 | ): ReadonlyArray<Node> { | |
| 88 | if (Array.isArray(elements)) | |
| 89 | return adapter.removeSubsets(elements) | |
| 90 | return adapter.getChildren(elements as unknown as ElementNode) as Node[] | |
| 91 | } | |
| @@ -0,0 +1,141 @@ | ||
| 1 | /** | |
| 2 | * Public surface — drop-in compatible with css-select v5. | |
| 3 | * | |
| 4 | * selectAll(selector, root, options) → ElementNode[] | |
| 5 | * selectOne(selector, root, options) → ElementNode | null | |
| 6 | * is(node, selector, options) → boolean | |
| 7 | * compile(selector, options) → CompiledQuery | |
| 8 | * | |
| 9 | * Compiled-selector cache: the same selector string compiled with the | |
| 10 | * same adapter + options is reused. Disable with `cacheResults: false`. | |
| 11 | */ | |
| 12 | ||
| 13 | import type { Selector } from '../what' | |
| 14 | import type { Adapter, CompiledQuery, Options } from './types' | |
| 15 | import { parse } from '../what' | |
| 16 | import { findAll as ourFindAll, findOne as ourFindOne, prepareContext } from './helpers/querying' | |
| 17 | import { compileGeneric, parseAndCompile } from './pseudo-selectors/selectors' | |
| 18 | ||
| 19 | function getAdapter<Node, ElementNode extends Node>(options: Options<Node, ElementNode>): Adapter<Node, ElementNode> { | |
| 20 | if (!options || !options.adapter) | |
| 21 | throw new Error('css-select: adapter is required') | |
| 22 | return options.adapter | |
| 23 | } | |
| 24 | ||
| 25 | // ----- compiled-selector cache ----- | |
| 26 | // Two-level lookup: outer keyed by Adapter (object identity), inner keyed | |
| 27 | // by a 3-bit flag-signature integer encoding the options that change | |
| 28 | // compile output. The innermost map is keyed by selector text. This avoids | |
| 29 | // the per-call `${flags}|${selector}` string concat the previous | |
| 30 | // single-level cache did — meaningful when consumers call selectAll in a | |
| 31 | // hot loop. | |
| 32 | const adapterCaches = new WeakMap<object, (Map<string, CompiledQuery<any>> | undefined)[]>() | |
| 33 | ||
| 34 | function flagSig(options: Options<any, any>): number { | |
| 35 | return ((options.xmlMode ? 1 : 0) << 0) | |
| 36 | | ((options.lowerCaseAttributeNames === false ? 1 : 0) << 1) | |
| 37 | | ((options.lowerCaseTags === false ? 1 : 0) << 2) | |
| 38 | } | |
| 39 | ||
| 40 | function getCache(adapter: object, sig: number): Map<string, CompiledQuery<any>> { | |
| 41 | let bucket = adapterCaches.get(adapter) | |
| 42 | if (!bucket) { | |
| 43 | bucket = [] | |
| 44 | adapterCaches.set(adapter, bucket) | |
| 45 | } | |
| 46 | let m = bucket[sig] | |
| 47 | if (!m) { | |
| 48 | m = new Map() | |
| 49 | bucket[sig] = m | |
| 50 | } | |
| 51 | return m | |
| 52 | } | |
| 53 | ||
| 54 | function compileSelectorString<Node, ElementNode extends Node>( | |
| 55 | selector: string, | |
| 56 | options: Options<Node, ElementNode>, | |
| 57 | ): CompiledQuery<ElementNode> { | |
| 58 | const ast = parse(selector, { | |
| 59 | xmlMode: options.xmlMode, | |
| 60 | lowerCaseAttributeNames: options.lowerCaseAttributeNames, | |
| 61 | lowerCaseTags: options.lowerCaseTags, | |
| 62 | }) | |
| 63 | return compileGeneric(ast, options) | |
| 64 | } | |
| 65 | ||
| 66 | export function compile<Node, ElementNode extends Node>( | |
| 67 | selector: string | Selector[][], | |
| 68 | options: Options<Node, ElementNode>, | |
| 69 | ): CompiledQuery<ElementNode> { | |
| 70 | if (typeof selector !== 'string') | |
| 71 | return compileGeneric(selector, options) | |
| 72 | ||
| 73 | // Fast path: use the per-adapter cache unless the consumer opted out. | |
| 74 | if (options.cacheResults === false || !options.adapter) | |
| 75 | return compileSelectorString(selector, options) | |
| 76 | ||
| 77 | const cache = getCache(options.adapter as unknown as object, flagSig(options)) | |
| 78 | let compiled = cache.get(selector) | |
| 79 | if (compiled !== undefined) | |
| 80 | return compiled as CompiledQuery<ElementNode> | |
| 81 | compiled = compileSelectorString(selector, options) | |
| 82 | cache.set(selector, compiled) | |
| 83 | return compiled as CompiledQuery<ElementNode> | |
| 84 | } | |
| 85 | ||
| 86 | /** Drop the cached compile of `selector` for `adapter`, or all entries | |
| 87 | * if `selector` is omitted. */ | |
| 88 | export function clearSelectorCache(adapter: object, selector?: string): void { | |
| 89 | if (selector === undefined) { | |
| 90 | adapterCaches.delete(adapter) | |
| 91 | return | |
| 92 | } | |
| 93 | const bucket = adapterCaches.get(adapter) | |
| 94 | if (!bucket) | |
| 95 | return | |
| 96 | for (const m of bucket) { | |
| 97 | if (m) | |
| 98 | m.delete(selector) | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | export function selectAll<Node, ElementNode extends Node>( | |
| 103 | selector: string | Selector[][] | CompiledQuery<ElementNode>, | |
| 104 | root: Node | Node[], | |
| 105 | options: Options<Node, ElementNode>, | |
| 106 | ): ElementNode[] { | |
| 107 | const adapter = getAdapter(options) | |
| 108 | const test = typeof selector === 'function' ? selector : compile(selector, options) | |
| 109 | const ctx = prepareContext(root, adapter) | |
| 110 | return ourFindAll(test, ctx, adapter, options.xmlMode === true) | |
| 111 | } | |
| 112 | ||
| 113 | export function selectOne<Node, ElementNode extends Node>( | |
| 114 | selector: string | Selector[][] | CompiledQuery<ElementNode>, | |
| 115 | root: Node | Node[], | |
| 116 | options: Options<Node, ElementNode>, | |
| 117 | ): ElementNode | null { | |
| 118 | const adapter = getAdapter(options) | |
| 119 | const test = typeof selector === 'function' ? selector : compile(selector, options) | |
| 120 | const ctx = prepareContext(root, adapter) | |
| 121 | return ourFindOne(test, ctx, adapter, options.xmlMode === true) | |
| 122 | } | |
| 123 | ||
| 124 | export function is<Node, ElementNode extends Node>( | |
| 125 | node: ElementNode, | |
| 126 | selector: string | Selector[][] | CompiledQuery<ElementNode>, | |
| 127 | options: Options<Node, ElementNode>, | |
| 128 | ): boolean { | |
| 129 | const test = typeof selector === 'function' ? selector : compile(selector, options) | |
| 130 | return test(node) | |
| 131 | } | |
| 132 | ||
| 133 | export type { | |
| 134 | Adapter, | |
| 135 | CompiledQuery, | |
| 136 | Options, | |
| 137 | Predicate, | |
| 138 | } from './types' | |
| 139 | ||
| 140 | // expose internals for advanced use | |
| 141 | export { parseAndCompile } | |
| @@ -0,0 +1,333 @@ | ||
| 1 | /** | |
| 2 | * Pseudo-selector dispatcher. Three families: | |
| 3 | * - filters: take a string or selector list, modify selection (`:nth-child`, `:not`, `:is`, `:where`, `:has`) | |
| 4 | * - pseudos: simple boolean predicates (`:empty`, `:root`, `:checked`, etc.) | |
| 5 | * - aliases: rewrite to attribute / sub-selectors | |
| 6 | */ | |
| 7 | ||
| 8 | import type { Selector } from '../../what' | |
| 9 | import type { CompiledQuery, Options } from '../types' | |
| 10 | import { parse as parseSelectorString } from '../../what' | |
| 11 | import { compileGeneric, parseAndCompile } from './selectors' | |
| 12 | ||
| 13 | interface PseudoToken { | |
| 14 | type: 'pseudo' | 'pseudo-element' | |
| 15 | name: string | |
| 16 | data: string | Selector[][] | null | |
| 17 | } | |
| 18 | ||
| 19 | // Pseudo-classes whose match depends on document state (focus, hover, form | |
| 20 | // validity, etc.) that ts-css doesn't model. They fail closed unless the | |
| 21 | // caller supplies an `options.pseudos[name]` predicate. | |
| 22 | const STATEFUL_PSEUDOS: ReadonlySet<string> = new Set([ | |
| 23 | 'link', 'any-link', 'visited', 'hover', 'active', 'focus', | |
| 24 | 'focus-visible', 'focus-within', 'target', 'target-within', 'enabled', | |
| 25 | 'disabled', 'checked', 'required', 'optional', 'valid', 'invalid', | |
| 26 | 'selected', 'placeholder-shown', 'read-only', 'read-write', 'in-range', | |
| 27 | 'out-of-range', 'default', 'indeterminate', | |
| 28 | ]) | |
| 29 | ||
| 30 | const SELECTOR_LIST_PSEUDOS: ReadonlySet<string> = new Set([ | |
| 31 | 'is', 'where', 'matches', '-moz-any', '-webkit-any', | |
| 32 | ]) | |
| 33 | ||
| 34 | const NTH_PSEUDOS: ReadonlySet<string> = new Set([ | |
| 35 | 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type', | |
| 36 | ]) | |
| 37 | ||
| 38 | export function compilePseudo<Node, ElementNode extends Node>( | |
| 39 | token: PseudoToken, | |
| 40 | options: Options<Node, ElementNode>, | |
| 41 | next: CompiledQuery<ElementNode>, | |
| 42 | ): CompiledQuery<ElementNode> { | |
| 43 | const adapter = options.adapter | |
| 44 | const name = token.name | |
| 45 | const data = token.data | |
| 46 | ||
| 47 | // -------- combinatorial pseudos: take selector lists -------- | |
| 48 | if (token.type === 'pseudo' && SELECTOR_LIST_PSEUDOS.has(name)) { | |
| 49 | if (!Array.isArray(data)) | |
| 50 | return next | |
| 51 | const tests = (data as Selector[][]).map(seg => parseAndCompile(seg, options)) | |
| 52 | // For a single sub-selector, skip the array iteration cost. | |
| 53 | if (tests.length === 1) { | |
| 54 | const t0 = tests[0]! | |
| 55 | return e => t0(e) && next(e) | |
| 56 | } | |
| 57 | return (e) => { | |
| 58 | for (const t of tests) { | |
| 59 | if (t(e)) | |
| 60 | return next(e) | |
| 61 | } | |
| 62 | return false | |
| 63 | } | |
| 64 | } | |
| 65 | if (token.type === 'pseudo' && name === 'not') { | |
| 66 | if (!Array.isArray(data)) | |
| 67 | return next | |
| 68 | const tests = (data as Selector[][]).map(seg => parseAndCompile(seg, options)) | |
| 69 | if (tests.length === 1) { | |
| 70 | const t0 = tests[0]! | |
| 71 | return e => !t0(e) && next(e) | |
| 72 | } | |
| 73 | return (e) => { | |
| 74 | for (const t of tests) { | |
| 75 | if (t(e)) | |
| 76 | return false | |
| 77 | } | |
| 78 | return next(e) | |
| 79 | } | |
| 80 | } | |
| 81 | if (token.type === 'pseudo' && name === 'has') { | |
| 82 | if (!Array.isArray(data)) | |
| 83 | return next | |
| 84 | const tests = (data as Selector[][]).map(seg => parseAndCompile(seg, options)) | |
| 85 | // Collapse the sub-test list into a single predicate so the inner | |
| 86 | // descendant loop only invokes one function per node. | |
| 87 | const subjectTest: CompiledQuery<ElementNode> = tests.length === 1 | |
| 88 | ? tests[0]! | |
| 89 | : ((node: any) => { | |
| 90 | for (const t of tests) { | |
| 91 | if (t(node)) | |
| 92 | return true | |
| 93 | } | |
| 94 | return false | |
| 95 | }) as any | |
| 96 | return (e) => { | |
| 97 | // Iterative DFS using `pop` (O(1)) instead of `shift` (O(n)). The | |
| 98 | // iteration order is reversed but `:has()` doesn't care about order | |
| 99 | // — it stops at the first descendant that satisfies the test. | |
| 100 | const stack: any[] = adapter.getChildren(e).slice() | |
| 101 | while (stack.length > 0) { | |
| 102 | const cur = stack.pop() | |
| 103 | if (!adapter.isTag(cur)) | |
| 104 | continue | |
| 105 | if (subjectTest(cur as any)) | |
| 106 | return next(e) | |
| 107 | const kids = adapter.getChildren(cur as any) | |
| 108 | for (let i = 0; i < kids.length; i++) | |
| 109 | stack.push(kids[i]) | |
| 110 | } | |
| 111 | return false | |
| 112 | } | |
| 113 | } | |
| 114 | ||
| 115 | // -------- nth-* pseudos -------- | |
| 116 | if (token.type === 'pseudo' && NTH_PSEUDOS.has(name)) { | |
| 117 | const fn = compileNth(typeof data === 'string' ? data : '', name, options) | |
| 118 | return e => fn(e) && next(e) | |
| 119 | } | |
| 120 | ||
| 121 | // -------- structural pseudos -------- | |
| 122 | // Each variant scans `getSiblings(e)` once without allocating the | |
| 123 | // filtered intermediate array `[].filter()` would (the previous shape | |
| 124 | // produced one allocation per element-test, which the matcher hits | |
| 125 | // 250+ times per selector even for tiny trees). | |
| 126 | if (token.type === 'pseudo' && name === 'first-child') { | |
| 127 | return (e) => { | |
| 128 | const sibs = adapter.getSiblings(e) | |
| 129 | for (const s of sibs) { | |
| 130 | if (!adapter.isTag(s)) | |
| 131 | continue | |
| 132 | return s === e && next(e) | |
| 133 | } | |
| 134 | return false | |
| 135 | } | |
| 136 | } | |
| 137 | if (token.type === 'pseudo' && name === 'last-child') { | |
| 138 | return (e) => { | |
| 139 | const sibs = adapter.getSiblings(e) | |
| 140 | for (let i = sibs.length - 1; i >= 0; i--) { | |
| 141 | const s = sibs[i]! | |
| 142 | if (!adapter.isTag(s)) | |
| 143 | continue | |
| 144 | return s === e && next(e) | |
| 145 | } | |
| 146 | return false | |
| 147 | } | |
| 148 | } | |
| 149 | if (token.type === 'pseudo' && name === 'first-of-type') { | |
| 150 | return (e) => { | |
| 151 | const tag = adapter.getName(e) | |
| 152 | const sibs = adapter.getSiblings(e) | |
| 153 | for (const s of sibs) { | |
| 154 | if (!adapter.isTag(s) || adapter.getName(s as any) !== tag) | |
| 155 | continue | |
| 156 | return s === e && next(e) | |
| 157 | } | |
| 158 | return false | |
| 159 | } | |
| 160 | } | |
| 161 | if (token.type === 'pseudo' && name === 'last-of-type') { | |
| 162 | return (e) => { | |
| 163 | const tag = adapter.getName(e) | |
| 164 | const sibs = adapter.getSiblings(e) | |
| 165 | for (let i = sibs.length - 1; i >= 0; i--) { | |
| 166 | const s = sibs[i]! | |
| 167 | if (!adapter.isTag(s) || adapter.getName(s as any) !== tag) | |
| 168 | continue | |
| 169 | return s === e && next(e) | |
| 170 | } | |
| 171 | return false | |
| 172 | } | |
| 173 | } | |
| 174 | if (token.type === 'pseudo' && name === 'only-child') { | |
| 175 | return (e) => { | |
| 176 | const sibs = adapter.getSiblings(e) | |
| 177 | let count = 0 | |
| 178 | let found = false | |
| 179 | for (const s of sibs) { | |
| 180 | if (!adapter.isTag(s)) | |
| 181 | continue | |
| 182 | count++ | |
| 183 | if (count > 1) | |
| 184 | return false | |
| 185 | if (s === e) | |
| 186 | found = true | |
| 187 | } | |
| 188 | return count === 1 && found && next(e) | |
| 189 | } | |
| 190 | } | |
| 191 | if (token.type === 'pseudo' && name === 'only-of-type') { | |
| 192 | return (e) => { | |
| 193 | const tag = adapter.getName(e) | |
| 194 | const sibs = adapter.getSiblings(e) | |
| 195 | let count = 0 | |
| 196 | let found = false | |
| 197 | for (const s of sibs) { | |
| 198 | if (!adapter.isTag(s) || adapter.getName(s as any) !== tag) | |
| 199 | continue | |
| 200 | count++ | |
| 201 | if (count > 1) | |
| 202 | return false | |
| 203 | if (s === e) | |
| 204 | found = true | |
| 205 | } | |
| 206 | return count === 1 && found && next(e) | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | // -------- simple pseudos -------- | |
| 211 | if (token.type === 'pseudo' && name === 'empty') { | |
| 212 | return e => adapter.getChildren(e).length === 0 && next(e) | |
| 213 | } | |
| 214 | if (token.type === 'pseudo' && name === 'root') { | |
| 215 | return (e) => { | |
| 216 | const p = adapter.getParent(e) | |
| 217 | return (p == null || !adapter.isTag(p as any)) && next(e) | |
| 218 | } | |
| 219 | } | |
| 220 | if (token.type === 'pseudo' && name === 'scope') { | |
| 221 | if (options.context && Array.isArray(options.context)) { | |
| 222 | // Materialize a Set once at compile time — `Array.includes` is O(n) | |
| 223 | // per element check and `:scope` runs against every candidate. | |
| 224 | const ctx = new Set<any>(options.context as any[]) | |
| 225 | return e => ctx.has(e) && next(e) | |
| 226 | } | |
| 227 | if (options.context) { | |
| 228 | const ctx = options.context as any | |
| 229 | return e => (e as any) === ctx && next(e) | |
| 230 | } | |
| 231 | return (e) => { | |
| 232 | const p = adapter.getParent(e) | |
| 233 | return (p == null || !adapter.isTag(p as any)) && next(e) | |
| 234 | } | |
| 235 | } | |
| 236 | if (token.type === 'pseudo' && STATEFUL_PSEUDOS.has(name)) { | |
| 237 | // Stateful pseudos — without a real DOM we treat them as always-false | |
| 238 | // (matches css-select's `xmlMode` defaults). Consumers who need real | |
| 239 | // matching can pass `options.pseudos[name]` callbacks. | |
| 240 | if (options.pseudos && options.pseudos[name]) { | |
| 241 | const ext = options.pseudos[name] | |
| 242 | if (typeof ext === 'function') | |
| 243 | return e => ext(e, undefined) && next(e) | |
| 244 | } | |
| 245 | return _ => { void _; return false } | |
| 246 | } | |
| 247 | ||
| 248 | // -------- pseudo-elements -------- | |
| 249 | if (token.type === 'pseudo-element') { | |
| 250 | // We don't model pseudo-elements; treat as no-op pass-through filter | |
| 251 | // (matches css-select's behaviour for ::before/::after/etc.). | |
| 252 | return next | |
| 253 | } | |
| 254 | ||
| 255 | // -------- caller-provided extensions -------- | |
| 256 | if (options.pseudos && options.pseudos[name]) { | |
| 257 | const ext = options.pseudos[name] | |
| 258 | if (typeof ext === 'function') { | |
| 259 | return e => ext(e, typeof data === 'string' ? data : undefined) && next(e) | |
| 260 | } | |
| 261 | if (typeof ext === 'string') { | |
| 262 | // Compile the alias selector string with `compileGeneric` so commas | |
| 263 | // (selector lists) are handled correctly. Previously this routed | |
| 264 | // through a stubbed `parseRaw` that always returned `[]`, which | |
| 265 | // silently caused string-aliased pseudos to never match. | |
| 266 | const aliasGroups = parseSelectorString(ext) | |
| 267 | const re = compileGeneric(aliasGroups, options) | |
| 268 | return e => re(e) && next(e) | |
| 269 | } | |
| 270 | } | |
| 271 | ||
| 272 | // unknown — fail closed | |
| 273 | return _ => { void _; return false } | |
| 274 | } | |
| 275 | ||
| 276 | ||
| 277 | function parseAnPlusB(s: string): { a: number, b: number } | null { | |
| 278 | const trimmed = s.trim() | |
| 279 | if (trimmed === 'odd') | |
| 280 | return { a: 2, b: 1 } | |
| 281 | if (trimmed === 'even') | |
| 282 | return { a: 2, b: 0 } | |
| 283 | const m = /^([+-]?\d*)n([+-]\d+)?$/.exec(trimmed.replace(/\s+/g, '')) | |
| 284 | if (m) { | |
| 285 | const a = m[1] === '' || m[1] === '+' ? 1 : m[1] === '-' ? -1 : Number(m[1]) | |
| 286 | const b = m[2] ? Number(m[2]) : 0 | |
| 287 | return { a, b } | |
| 288 | } | |
| 289 | const num = Number(trimmed) | |
| 290 | if (!Number.isNaN(num)) | |
| 291 | return { a: 0, b: num } | |
| 292 | return null | |
| 293 | } | |
| 294 | ||
| 295 | function compileNth<Node, ElementNode extends Node>( | |
| 296 | arg: string, | |
| 297 | kind: string, | |
| 298 | options: Options<Node, ElementNode>, | |
| 299 | ): (e: ElementNode) => boolean { | |
| 300 | const ab = parseAnPlusB(arg) | |
| 301 | if (!ab) | |
| 302 | return () => false | |
| 303 | const { a, b } = ab | |
| 304 | const adapter = options.adapter | |
| 305 | const ofType = kind === 'nth-of-type' || kind === 'nth-last-of-type' | |
| 306 | const reverse = kind === 'nth-last-child' || kind === 'nth-last-of-type' | |
| 307 | return (e) => { | |
| 308 | // Compute idx in one pass over siblings — no `filter()` allocs and no | |
| 309 | // `reverse()` mutation. For `nth-last-*` we count total matching | |
| 310 | // siblings then take `total - rawIdx + 1`. | |
| 311 | const sibs = adapter.getSiblings(e) | |
| 312 | const tag = ofType ? adapter.getName(e) : '' | |
| 313 | let raw = 0 // 1-based index of `e` among matching siblings | |
| 314 | let total = 0 | |
| 315 | for (const s of sibs) { | |
| 316 | if (!adapter.isTag(s)) | |
| 317 | continue | |
| 318 | if (ofType && adapter.getName(s as any) !== tag) | |
| 319 | continue | |
| 320 | total++ | |
| 321 | if (s === e) | |
| 322 | raw = total | |
| 323 | } | |
| 324 | if (raw === 0) | |
| 325 | return false | |
| 326 | const idx = reverse ? total - raw + 1 : raw | |
| 327 | if (a === 0) | |
| 328 | return idx === b | |
| 329 | return ((idx - b) / a) >= 0 && ((idx - b) % a) === 0 | |
| 330 | } | |
| 331 | } | |
| 332 | ||
| 333 | export { compileGeneric } | |
| @@ -0,0 +1,138 @@ | ||
| 1 | /** | |
| 2 | * Compose a token list into a test function. | |
| 3 | * | |
| 4 | * Tokens carry simple selectors (tag/class/attr/pseudo) interleaved with | |
| 5 | * combinators (descendant/child/sibling/adjacent). Each token becomes one | |
| 6 | * link in a chain — combinators wrap the running chain so it tests the | |
| 7 | * left-hand chain on a new pivot. We iterate left-to-right (matching | |
| 8 | * `css-select`'s compile model) but sort tokens *within each compound* by | |
| 9 | * procedure cost so cheap tests (tag) wind up outermost and fail-fast. | |
| 10 | */ | |
| 11 | ||
| 12 | import type { AttributeSelectorNode, Selector } from '../../what' | |
| 13 | import type { Adapter, CompiledQuery, Options } from '../types' | |
| 14 | import { compileToken } from '../general' | |
| 15 | import { ALWAYS_TRUE } from '../helpers/always-true' | |
| 16 | ||
| 17 | // Lower = more expensive → wrapped innermost → runs last. | |
| 18 | // Higher = cheaper → wrapped outermost → runs first (lets us fail-fast). | |
| 19 | const PROCEDURE: Record<string, number> = { | |
| 20 | universal: 50, | |
| 21 | tag: 30, | |
| 22 | attribute: 1, | |
| 23 | pseudo: 0, | |
| 24 | 'pseudo-element': 0, | |
| 25 | } | |
| 26 | ||
| 27 | // Within `attribute`, equality is cheaper than substring/regex. | |
| 28 | const ATTRIBUTE_PROCEDURE: Record<string, number> = { | |
| 29 | exists: 10, | |
| 30 | equals: 8, | |
| 31 | not: 7, | |
| 32 | start: 6, | |
| 33 | end: 6, | |
| 34 | hyphen: 5, | |
| 35 | any: 4, | |
| 36 | element: 3, | |
| 37 | } | |
| 38 | ||
| 39 | function tokenProcedure(t: Selector): number { | |
| 40 | const base = PROCEDURE[t.type] ?? 0 | |
| 41 | if (t.type === 'attribute') | |
| 42 | return base + (ATTRIBUTE_PROCEDURE[(t as AttributeSelectorNode).action] ?? 0) | |
| 43 | return base | |
| 44 | } | |
| 45 | ||
| 46 | export function compileGeneric<Node, ElementNode extends Node>( | |
| 47 | segments: Selector[][], | |
| 48 | options: Options<Node, ElementNode>, | |
| 49 | ): CompiledQuery<ElementNode> { | |
| 50 | if (segments.length === 1) | |
| 51 | return parseAndCompile(segments[0]!, options) | |
| 52 | const compiled = segments.map(seg => parseAndCompile(seg, options)) | |
| 53 | return e => compiled.some(c => c(e)) | |
| 54 | } | |
| 55 | ||
| 56 | export function parseAndCompile<Node, ElementNode extends Node>( | |
| 57 | tokens: Selector[], | |
| 58 | options: Options<Node, ElementNode>, | |
| 59 | ): CompiledQuery<ElementNode> { | |
| 60 | const adapter = options.adapter | |
| 61 | // Split into compound buckets; combinator markers stay in `combs`. | |
| 62 | const compounds: Selector[][] = [[]] | |
| 63 | const combs: Selector[] = [] | |
| 64 | for (const t of tokens) { | |
| 65 | if ( | |
| 66 | t.type === 'descendant' || t.type === 'child' || t.type === 'sibling' | |
| 67 | || t.type === 'adjacent' || t.type === 'parent' || t.type === 'column-combinator' | |
| 68 | ) { | |
| 69 | combs.push(t) | |
| 70 | compounds.push([]) | |
| 71 | } | |
| 72 | else { | |
| 73 | compounds[compounds.length - 1]!.push(t) | |
| 74 | } | |
| 75 | } | |
| 76 | ||
| 77 | // Build left-to-right. Within each compound, sort by procedure ascending | |
| 78 | // so the cheapest test ends up outermost (runs first). | |
| 79 | let func: CompiledQuery<ElementNode> = ALWAYS_TRUE | |
| 80 | for (let ci = 0; ci < compounds.length; ci++) { | |
| 81 | const compound = compounds[ci]!.slice().sort((a, b) => tokenProcedure(a) - tokenProcedure(b)) | |
| 82 | for (const t of compound) | |
| 83 | func = compileToken(t, options, func) | |
| 84 | if (ci < combs.length) | |
| 85 | func = wrapCombinator(combs[ci]!, func, adapter) | |
| 86 | } | |
| 87 | return func | |
| 88 | } | |
| 89 | ||
| 90 | function wrapCombinator<Node, ElementNode extends Node>( | |
| 91 | comb: Selector, | |
| 92 | after: CompiledQuery<ElementNode>, | |
| 93 | adapter: Adapter<Node, ElementNode>, | |
| 94 | ): CompiledQuery<ElementNode> { | |
| 95 | switch (comb.type) { | |
| 96 | case 'descendant': | |
| 97 | case 'column-combinator': | |
| 98 | return (elem: any) => { | |
| 99 | let p: any = adapter.getParent(elem) | |
| 100 | while (p) { | |
| 101 | if (adapter.isTag(p) && after(p)) | |
| 102 | return true | |
| 103 | p = adapter.getParent(p) | |
| 104 | } | |
| 105 | return false | |
| 106 | } | |
| 107 | case 'child': | |
| 108 | return (elem: any) => { | |
| 109 | const p: any = adapter.getParent(elem) | |
| 110 | return p != null && adapter.isTag(p) && after(p) | |
| 111 | } | |
| 112 | case 'sibling': | |
| 113 | return (elem: any) => { | |
| 114 | const sibs: any[] = adapter.getSiblings(elem) as any | |
| 115 | for (const s of sibs) { | |
| 116 | if (s === elem) | |
| 117 | return false | |
| 118 | if (adapter.isTag(s) && after(s)) | |
| 119 | return true | |
| 120 | } | |
| 121 | return false | |
| 122 | } | |
| 123 | case 'adjacent': | |
| 124 | return (elem: any) => { | |
| 125 | const sibs: any[] = adapter.getSiblings(elem) as any | |
| 126 | let prev: any = null | |
| 127 | for (const s of sibs) { | |
| 128 | if (s === elem) | |
| 129 | return prev != null && adapter.isTag(prev) && after(prev) | |
| 130 | prev = s | |
| 131 | } | |
| 132 | return false | |
| 133 | } | |
| 134 | case 'parent': | |
| 135 | return (elem: any) => adapter.existsOne(after, adapter.getChildren(elem)) | |
| 136 | } | |
| 137 | return _ => { void _; return false } | |
| 138 | } | |
| @@ -0,0 +1,77 @@ | ||
| 1 | /** | |
| 2 | * css-select adapter / option types. Drop-in compatible with css-select v5. | |
| 3 | * | |
| 4 | * The library is generic over the consumer's tree (`Node`/`ElementNode`), | |
| 5 | * communicated via the `Adapter` interface — exactly the shape SVGO etc. | |
| 6 | * already implement, so callers don't need to change a thing. | |
| 7 | */ | |
| 8 | ||
| 9 | export interface Adapter<Node, ElementNode extends Node> { | |
| 10 | // eslint-disable-next-line pickier/no-unused-vars | |
| 11 | isTag: (node: Node) => node is ElementNode | |
| 12 | // eslint-disable-next-line pickier/no-unused-vars | |
| 13 | existsOne: (test: (e: ElementNode) => boolean, elems: Node[]) => boolean | |
| 14 | // eslint-disable-next-line pickier/no-unused-vars | |
| 15 | getAttributeValue: (elem: ElementNode, name: string) => string | undefined | |
| 16 | // eslint-disable-next-line pickier/no-unused-vars | |
| 17 | getChildren: (node: Node) => Node[] | |
| 18 | // eslint-disable-next-line pickier/no-unused-vars | |
| 19 | getName: (elem: ElementNode) => string | |
| 20 | // eslint-disable-next-line pickier/no-unused-vars | |
| 21 | getParent: (elem: ElementNode | Node) => ElementNode | null | |
| 22 | // eslint-disable-next-line pickier/no-unused-vars | |
| 23 | getSiblings: (elem: Node) => Node[] | |
| 24 | // eslint-disable-next-line pickier/no-unused-vars | |
| 25 | getText: (node: Node) => string | |
| 26 | // eslint-disable-next-line pickier/no-unused-vars | |
| 27 | hasAttrib: (elem: ElementNode, name: string) => boolean | |
| 28 | // eslint-disable-next-line pickier/no-unused-vars | |
| 29 | removeSubsets: (nodes: Node[]) => Node[] | |
| 30 | /** | |
| 31 | * `findAll` / `findOne` are accepted for adapter-shape compatibility | |
| 32 | * with css-select, but ts-css implements its own iterative tree walker | |
| 33 | * (`select/helpers/querying.ts`) and does NOT call these. They're | |
| 34 | * declared optional so adapters that don't provide them still type-check. | |
| 35 | */ | |
| 36 | // eslint-disable-next-line pickier/no-unused-vars | |
| 37 | findAll?: (test: (e: ElementNode) => boolean, elems: Node[]) => ElementNode[] | |
| 38 | // eslint-disable-next-line pickier/no-unused-vars | |
| 39 | findOne?: (test: (e: ElementNode) => boolean, elems: Node[]) => ElementNode | null | |
| 40 | // eslint-disable-next-line pickier/no-unused-vars | |
| 41 | equals?: (a: Node, b: Node) => boolean | |
| 42 | // eslint-disable-next-line pickier/no-unused-vars | |
| 43 | isActive?: (elem: ElementNode) => boolean | |
| 44 | // eslint-disable-next-line pickier/no-unused-vars | |
| 45 | isVisited?: (elem: ElementNode) => boolean | |
| 46 | // eslint-disable-next-line pickier/no-unused-vars | |
| 47 | isHovered?: (elem: ElementNode) => boolean | |
| 48 | } | |
| 49 | ||
| 50 | export interface Options<Node, ElementNode extends Node> { | |
| 51 | /** XML mode disables HTML quirks (case-insensitive tags etc.). */ | |
| 52 | xmlMode?: boolean | |
| 53 | /** When false, attribute names are not lowercased before lookup. */ | |
| 54 | lowerCaseAttributeNames?: boolean | |
| 55 | /** When false, tags are not lowercased. */ | |
| 56 | lowerCaseTags?: boolean | |
| 57 | /** Cache compiled selectors when true. */ | |
| 58 | cacheResults?: boolean | |
| 59 | /** Adapter providing tree access. */ | |
| 60 | adapter: Adapter<Node, ElementNode> | |
| 61 | /** Optional context node for relative selectors (`:has`, `:not`). */ | |
| 62 | context?: Node | Node[] | |
| 63 | /** Optional pseudo-class/element extension hooks. */ | |
| 64 | // eslint-disable-next-line pickier/no-unused-vars | |
| 65 | pseudos?: Record<string, string | ((elem: ElementNode, value?: string) => boolean)> | |
| 66 | /** Set, treated as already-visited siblings root. */ | |
| 67 | rootFunc?: (elem: ElementNode) => boolean | |
| 68 | /** Whether to match `:has` lazily. */ | |
| 69 | relativeSelector?: boolean | |
| 70 | /** Whether the parsed selector itself is the document. */ | |
| 71 | quirksMode?: boolean | |
| 72 | } | |
| 73 | ||
| 74 | // eslint-disable-next-line pickier/no-unused-vars | |
| 75 | export type CompiledQuery<ElementNode> = (node: ElementNode) => boolean | |
| 76 | // eslint-disable-next-line pickier/no-unused-vars | |
| 77 | export type Predicate<Value> = (v: Value) => boolean | |
| @@ -0,0 +1,12 @@ | ||
| 1 | /** | |
| 2 | * Public configuration shape for ts-css. Loaded by `bunfig` from | |
| 3 | * `css.config.ts` (or `.json` / `.toml`) when present. | |
| 4 | */ | |
| 5 | export interface CSSConfig { | |
| 6 | /** Default float precision used by the minifier when rounding numeric values. */ | |
| 7 | floatPrecision: number | |
| 8 | /** Verbose logging from CLI commands. */ | |
| 9 | verbose: boolean | |
| 10 | } | |
| 11 | ||
| 12 | export type CSSOptions = Partial<CSSConfig> | |
| @@ -0,0 +1,21 @@ | ||
| 1 | /** | |
| 2 | * Public surface — drop-in compatible with css-what v6. | |
| 3 | */ | |
| 4 | ||
| 5 | export { parse } from './parse' | |
| 6 | export { stringify } from './stringify' | |
| 7 | export { isTraversal } from './traversal' | |
| 8 | export type { | |
| 9 | AttributeAction, | |
| 10 | AttributeSelectorNode, | |
| 11 | IgnoreCase, | |
| 12 | ParseOptions, | |
| 13 | PseudoElementNode, | |
| 14 | PseudoSelectorNode, | |
| 15 | Selector, | |
| 16 | SelectorType, | |
| 17 | TagSelectorNode, | |
| 18 | TraversalNode, | |
| 19 | UniversalSelectorNode, | |
| 20 | } from './types' | |
| 21 | export { IgnoreCaseMode } from './types' | |
| @@ -0,0 +1,388 @@ | ||
| 1 | /** | |
| 2 | * Selector parser — string → list-of-lists of selector segments. | |
| 3 | * | |
| 4 | * Returns one inner list per top-level (comma-separated) selector, with | |
| 5 | * traversal segments (descendant/child/sibling/adjacent/column) interleaved | |
| 6 | * between simple selector segments. Mirrors `css-what` v6. | |
| 7 | */ | |
| 8 | ||
| 9 | import type { ParseOptions, Selector } from './types' | |
| 10 | ||
| 11 | // Sticky regexes: `y` lets us match from a specific index without slicing. | |
| 12 | // Using `lastIndex` directly keeps the parser allocation-light per name. | |
| 13 | const RE_NAME_STICKY = /(?:\\(?:[\dA-Fa-f]{1,6} ?|[^])|[\w\-°-])+/y | |
| 14 | const RE_ESCAPE = /\\([\dA-Fa-f]{1,6} ?|[^])/g | |
| 15 | ||
| 16 | function unescape(name: string): string { | |
| 17 | return name.replace(RE_ESCAPE, (_m, escape: string) => { | |
| 18 | if (escape.length > 1 && /^[\dA-Fa-f]/.test(escape)) { | |
| 19 | const code = Number.parseInt(escape, 16) | |
| 20 | if (code >= 0xD800 && code <= 0xDFFF) | |
| 21 | return '�' | |
| 22 | return String.fromCodePoint(code) | |
| 23 | } | |
| 24 | return escape | |
| 25 | }) | |
| 26 | } | |
| 27 | ||
| 28 | function unescapeIfNeeded(name: string): string { | |
| 29 | // Vast majority of CSS selector identifiers have no `\\` escapes — skip | |
| 30 | // the regex replace pipeline and return the string directly. | |
| 31 | return name.indexOf('\\') < 0 ? name : unescape(name) | |
| 32 | } | |
| 33 | ||
| 34 | const ATTRIBUTES_QUIRKS = new Set([ | |
| 35 | 'accept', 'accept-charset', 'align', 'alink', 'axis', 'bgcolor', 'charset', | |
| 36 | 'checked', 'clear', 'codetype', 'color', 'compact', 'declare', 'defer', | |
| 37 | 'dir', 'direction', 'disabled', 'enctype', 'face', 'frame', 'hreflang', | |
| 38 | 'http-equiv', 'lang', 'language', 'link', 'media', 'method', 'multiple', | |
| 39 | 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'rel', 'rev', | |
| 40 | 'rules', 'scope', 'scrolling', 'selected', 'shape', 'target', 'text', | |
| 41 | 'type', 'valign', 'valuetype', 'vlink', | |
| 42 | ]) | |
| 43 | ||
| 44 | // Action-string lookup as a plain function (no per-parse Map). | |
| 45 | function actionFromChar(ch: number): string | null { | |
| 46 | switch (ch) { | |
| 47 | case 126 /* ~ */: return 'element' | |
| 48 | case 94 /* ^ */: return 'start' | |
| 49 | case 36 /* $ */: return 'end' | |
| 50 | case 42 /* * */: return 'any' | |
| 51 | case 33 /* ! */: return 'not' | |
| 52 | case 124 /* | */: return 'hyphen' | |
| 53 | default: return null | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | function isWsCode(c: number): boolean { | |
| 58 | return c === 32 || c === 9 || c === 10 || c === 13 || c === 12 | |
| 59 | } | |
| 60 | ||
| 61 | export function parse(selector: string, options: ParseOptions = {}): Selector[][] { | |
| 62 | const subselects: Selector[][] = [] | |
| 63 | const endIndex = parseSelectorImpl(subselects, selector, options, 0) | |
| 64 | if (endIndex < selector.length) | |
| 65 | throw new Error(`Unmatched selector: ${selector.slice(endIndex)}`) | |
| 66 | return subselects | |
| 67 | } | |
| 68 | ||
| 69 | function readName(selector: string, from: number): { value: string, end: number } { | |
| 70 | RE_NAME_STICKY.lastIndex = from | |
| 71 | const m = RE_NAME_STICKY.exec(selector) | |
| 72 | if (!m) | |
| 73 | throw new Error(`Expected name, found ${selector.slice(from)}`) | |
| 74 | return { value: unescapeIfNeeded(m[0]), end: from + m[0].length } | |
| 75 | } | |
| 76 | ||
| 77 | function stripWS(selector: string, from: number): number { | |
| 78 | while (from < selector.length && isWsCode(selector.charCodeAt(from))) | |
| 79 | from++ | |
| 80 | return from | |
| 81 | } | |
| 82 | ||
| 83 | function parseSelectorImpl(subselects: Selector[][], selector: string, options: ParseOptions, startIndex: number): number { | |
| 84 | let tokens: Selector[] = [] | |
| 85 | let i = stripWS(selector, startIndex) | |
| 86 | const len = selector.length | |
| 87 | const xmlMode = options.xmlMode === true | |
| 88 | const lowerCaseAttrs = options.lowerCaseAttributeNames !== false && !xmlMode | |
| 89 | const lowerCaseTagsFlag = options.lowerCaseTags !== false | |
| 90 | ||
| 91 | while (i < len) { | |
| 92 | const code = selector.charCodeAt(i) | |
| 93 | ||
| 94 | // whitespace → descendant combinator (or just trim if at start) | |
| 95 | if (isWsCode(code)) { | |
| 96 | let trimmed = i + 1 | |
| 97 | while (trimmed < len && isWsCode(selector.charCodeAt(trimmed))) | |
| 98 | trimmed++ | |
| 99 | if (tokens.length === 0) | |
| 100 | return trimmed | |
| 101 | i = trimmed | |
| 102 | addTraversal(tokens, 'descendant') | |
| 103 | continue | |
| 104 | } | |
| 105 | ||
| 106 | // explicit combinators | |
| 107 | if (code === 62 /* > */ || code === 60 /* < */ || code === 126 /* ~ */ || code === 43 /* + */ || code === 124 /* | */) { | |
| 108 | let j = i + 1 | |
| 109 | while (j < len && isWsCode(selector.charCodeAt(j))) j++ | |
| 110 | i = j | |
| 111 | switch (code) { | |
| 112 | case 62: addTraversal(tokens, 'child'); break | |
| 113 | case 60: addTraversal(tokens, 'parent'); break | |
| 114 | case 126: addTraversal(tokens, 'sibling'); break | |
| 115 | case 43: addTraversal(tokens, 'adjacent'); break | |
| 116 | case 124: | |
| 117 | if (i < len && selector.charCodeAt(i) === 124) { | |
| 118 | i++ | |
| 119 | i = stripWS(selector, i) | |
| 120 | addTraversal(tokens, 'column-combinator') | |
| 121 | } | |
| 122 | else { | |
| 123 | tokens.push({ type: 'tag', name: '', namespace: '' }) | |
| 124 | } | |
| 125 | break | |
| 126 | } | |
| 127 | continue | |
| 128 | } | |
| 129 | ||
| 130 | if (code === 44 /* , */) { | |
| 131 | if (tokens.length === 0) | |
| 132 | throw new Error('Empty sub-selector') | |
| 133 | subselects.push(tokens) | |
| 134 | tokens = [] | |
| 135 | i = stripWS(selector, i + 1) | |
| 136 | continue | |
| 137 | } | |
| 138 | ||
| 139 | // comment /* ... */ | |
| 140 | if (code === 47 /* / */ && selector.charCodeAt(i + 1) === 42 /* * */) { | |
| 141 | const end = selector.indexOf('*/', i + 2) | |
| 142 | if (end < 0) | |
| 143 | throw new Error('Unmatched comment') | |
| 144 | i = stripWS(selector, end + 2) | |
| 145 | continue | |
| 146 | } | |
| 147 | ||
| 148 | if (code === 42 /* * */) { | |
| 149 | i++ | |
| 150 | tokens.push({ type: 'universal', namespace: null }) | |
| 151 | continue | |
| 152 | } | |
| 153 | ||
| 154 | if (code === 35 /* # */) { | |
| 155 | const r = readName(selector, i + 1) | |
| 156 | i = r.end | |
| 157 | tokens.push({ | |
| 158 | type: 'attribute', | |
| 159 | name: 'id', | |
| 160 | action: 'equals', | |
| 161 | value: r.value, | |
| 162 | namespace: null, | |
| 163 | ignoreCase: false, | |
| 164 | }) | |
| 165 | continue | |
| 166 | } | |
| 167 | ||
| 168 | if (code === 46 /* . */) { | |
| 169 | const r = readName(selector, i + 1) | |
| 170 | i = r.end | |
| 171 | tokens.push({ | |
| 172 | type: 'attribute', | |
| 173 | name: 'class', | |
| 174 | action: 'element', | |
| 175 | value: r.value, | |
| 176 | namespace: null, | |
| 177 | ignoreCase: false, | |
| 178 | }) | |
| 179 | continue | |
| 180 | } | |
| 181 | ||
| 182 | if (code === 91 /* [ */) { | |
| 183 | i = parseAttribute(selector, i, tokens, options, xmlMode, lowerCaseAttrs) | |
| 184 | continue | |
| 185 | } | |
| 186 | ||
| 187 | if (code === 58 /* : */) { | |
| 188 | i = parsePseudo(selector, i, tokens, options) | |
| 189 | continue | |
| 190 | } | |
| 191 | ||
| 192 | // tag selector — readName then optional `|tag` | |
| 193 | if (code === 124 /* | */) { | |
| 194 | i++ | |
| 195 | const r = readName(selector, i) | |
| 196 | i = r.end | |
| 197 | tokens.push({ type: 'tag', name: lowerCaseTagsFlag ? r.value.toLowerCase() : r.value, namespace: '' }) | |
| 198 | continue | |
| 199 | } | |
| 200 | ||
| 201 | { | |
| 202 | const r1 = readName(selector, i) | |
| 203 | i = r1.end | |
| 204 | if (i < len && selector.charCodeAt(i) === 124 /* | */ && selector.charCodeAt(i + 1) !== 61 /* = */) { | |
| 205 | i++ | |
| 206 | const r2 = readName(selector, i) | |
| 207 | i = r2.end | |
| 208 | tokens.push({ type: 'tag', name: lowerCaseTagsFlag ? r2.value.toLowerCase() : r2.value, namespace: r1.value }) | |
| 209 | } | |
| 210 | else { | |
| 211 | tokens.push({ type: 'tag', name: lowerCaseTagsFlag ? r1.value.toLowerCase() : r1.value, namespace: null }) | |
| 212 | } | |
| 213 | } | |
| 214 | } | |
| 215 | if (tokens.length > 0) | |
| 216 | subselects.push(tokens) | |
| 217 | return i | |
| 218 | } | |
| 219 | ||
| 220 | // eslint-disable-next-line pickier/no-unused-vars | |
| 221 | function parseAttribute(selector: string, idx: number, tokens: Selector[], options: ParseOptions, xmlMode: boolean, lowerCaseAttrs: boolean): number { | |
| 222 | let i = idx + 1 | |
| 223 | const len = selector.length | |
| 224 | let attribute: string | |
| 225 | if (selector.charCodeAt(i) === 124 /* | */) | |
| 226 | throw new Error('Empty namespace not supported') | |
| 227 | if (selector.charCodeAt(i) === 42 /* * */ && selector.charCodeAt(i + 1) === 124 /* | */) { | |
| 228 | i += 2 | |
| 229 | const r = readName(selector, i) | |
| 230 | i = r.end | |
| 231 | attribute = r.value | |
| 232 | } | |
| 233 | else { | |
| 234 | const r = readName(selector, i) | |
| 235 | i = r.end | |
| 236 | attribute = r.value | |
| 237 | if (selector.charCodeAt(i) === 124 /* | */ && selector.charCodeAt(i + 1) !== 61 /* = */) { | |
| 238 | i++ | |
| 239 | const r2 = readName(selector, i) | |
| 240 | i = r2.end | |
| 241 | attribute = r2.value | |
| 242 | } | |
| 243 | } | |
| 244 | i = stripWS(selector, i) | |
| 245 | let action: 'exists' | 'equals' | 'element' | 'start' | 'end' | 'any' | 'not' | 'hyphen' = 'exists' | |
| 246 | let value = '' | |
| 247 | let ignoreCase: boolean | null = null | |
| 248 | const opCode = selector.charCodeAt(i) | |
| 249 | if (opCode === 61 /* = */) { | |
| 250 | action = 'equals' | |
| 251 | i++ | |
| 252 | } | |
| 253 | else if (opCode === 33 /* ! */ && selector.charCodeAt(i + 1) === 61) { | |
| 254 | action = 'not' | |
| 255 | i += 2 | |
| 256 | } | |
| 257 | else { | |
| 258 | const a = actionFromChar(opCode) | |
| 259 | if (a !== null && selector.charCodeAt(i + 1) === 61) { | |
| 260 | action = a as any | |
| 261 | i += 2 | |
| 262 | } | |
| 263 | } | |
| 264 | if (action !== 'exists') { | |
| 265 | i = stripWS(selector, i) | |
| 266 | const q = selector.charCodeAt(i) | |
| 267 | if (q === 34 /* " */ || q === 39 /* ' */) { | |
| 268 | const end = findEndOfString(selector, i + 1, q) | |
| 269 | value = unescapeIfNeeded(selector.slice(i + 1, end)) | |
| 270 | i = end + 1 | |
| 271 | } | |
| 272 | else { | |
| 273 | const r = readName(selector, i) | |
| 274 | value = r.value | |
| 275 | i = r.end | |
| 276 | } | |
| 277 | i = stripWS(selector, i) | |
| 278 | const flag = selector.charCodeAt(i) | |
| 279 | if (flag === 105 /* i */ || flag === 73 /* I */) { | |
| 280 | ignoreCase = true | |
| 281 | i++ | |
| 282 | } | |
| 283 | else if (flag === 115 /* s */ || flag === 83 /* S */) { | |
| 284 | ignoreCase = false | |
| 285 | i++ | |
| 286 | } | |
| 287 | } | |
| 288 | if (selector.charCodeAt(i) !== 93 /* ] */) | |
| 289 | throw new Error('Expected ]') | |
| 290 | i++ | |
| 291 | if (ignoreCase === null && !xmlMode && ATTRIBUTES_QUIRKS.has(attribute.toLowerCase())) | |
| 292 | ignoreCase = 'quirks' as any | |
| 293 | void options | |
| 294 | void len | |
| 295 | tokens.push({ | |
| 296 | type: 'attribute', | |
| 297 | name: lowerCaseAttrs ? attribute.toLowerCase() : attribute, | |
| 298 | action, | |
| 299 | value, | |
| 300 | namespace: null, | |
| 301 | ignoreCase, | |
| 302 | }) | |
| 303 | return i | |
| 304 | } | |
| 305 | ||
| 306 | function parsePseudo(selector: string, idx: number, tokens: Selector[], options: ParseOptions): number { | |
| 307 | if (selector.charCodeAt(idx + 1) === 58 /* : */) { | |
| 308 | let i = idx + 2 | |
| 309 | const r = readName(selector, i) | |
| 310 | i = r.end | |
| 311 | const name = r.value.toLowerCase() | |
| 312 | let data: string | null = null | |
| 313 | if (selector.charCodeAt(i) === 40 /* ( */) { | |
| 314 | const end = findClose(selector, i) | |
| 315 | data = selector.slice(i + 1, end).trim() | |
| 316 | i = end + 1 | |
| 317 | } | |
| 318 | tokens.push({ type: 'pseudo-element', name, data }) | |
| 319 | return i | |
| 320 | } | |
| 321 | let i = idx + 1 | |
| 322 | const r = readName(selector, i) | |
| 323 | i = r.end | |
| 324 | const name = r.value.toLowerCase() | |
| 325 | if (selector.charCodeAt(i) === 40 /* ( */) { | |
| 326 | const end = findClose(selector, i) | |
| 327 | const inner = selector.slice(i + 1, end) | |
| 328 | i = end + 1 | |
| 329 | if (name === 'is' || name === 'not' || name === 'where' || name === 'has' || name === 'matches' || name === '-moz-any' || name === '-webkit-any') { | |
| 330 | const sub: Selector[][] = [] | |
| 331 | parseSelectorImpl(sub, inner.trim(), options, 0) | |
| 332 | tokens.push({ type: 'pseudo', name, data: sub }) | |
| 333 | } | |
| 334 | else { | |
| 335 | tokens.push({ type: 'pseudo', name, data: inner.trim() }) | |
| 336 | } | |
| 337 | } | |
| 338 | else { | |
| 339 | tokens.push({ type: 'pseudo', name, data: null }) | |
| 340 | } | |
| 341 | return i | |
| 342 | } | |
| 343 | ||
| 344 | function addTraversal(tokens: Selector[], type: 'adjacent' | 'child' | 'descendant' | 'parent' | 'sibling' | 'column-combinator'): void { | |
| 345 | if (tokens.length > 0 && tokens[tokens.length - 1]!.type === 'descendant' && type !== 'descendant') | |
| 346 | tokens.pop() | |
| 347 | if (tokens.length > 0 && (tokens[tokens.length - 1]!.type === type)) | |
| 348 | return | |
| 349 | tokens.push({ type } as any) | |
| 350 | } | |
| 351 | ||
| 352 | function findEndOfString(selector: string, start: number, qCode: number): number { | |
| 353 | let i = start | |
| 354 | const len = selector.length | |
| 355 | while (i < len) { | |
| 356 | const c = selector.charCodeAt(i) | |
| 357 | if (c === 92 /* \ */) { | |
| 358 | i += 2 | |
| 359 | continue | |
| 360 | } | |
| 361 | if (c === qCode) | |
| 362 | return i | |
| 363 | i++ | |
| 364 | } | |
| 365 | throw new Error('Unterminated string') | |
| 366 | } | |
| 367 | ||
| 368 | // eslint-disable-next-line pickier/no-unused-vars | |
| 369 | function findClose(selector: string, openParen: number): number { | |
| 370 | let depth = 1 | |
| 371 | let i = openParen + 1 | |
| 372 | const len = selector.length | |
| 373 | while (i < len) { | |
| 374 | const c = selector.charCodeAt(i) | |
| 375 | if (c === 92 /* \ */) { i += 2; continue } | |
| 376 | if (c === 34 /* " */ || c === 39 /* ' */) { | |
| 377 | i = findEndOfString(selector, i + 1, c) + 1 | |
| 378 | continue | |
| 379 | } | |
| 380 | if (c === 40 /* ( */) depth++ | |
| 381 | else if (c === 41 /* ) */) { | |
| 382 | depth-- | |
| 383 | if (depth === 0) return i | |
| 384 | } | |
| 385 | i++ | |
| 386 | } | |
| 387 | throw new Error('Unterminated parenthesis') | |
| 388 | } | |
| @@ -0,0 +1,94 @@ | ||
| 1 | /** | |
| 2 | * Selector AST → string. Inverse of `parse`. | |
| 3 | */ | |
| 4 | ||
| 5 | import type { Selector } from './types' | |
| 6 | ||
| 7 | const COMBINATORS: Record<string, string> = { | |
| 8 | child: ' > ', | |
| 9 | parent: ' < ', | |
| 10 | sibling: ' ~ ', | |
| 11 | adjacent: ' + ', | |
| 12 | descendant: ' ', | |
| 13 | 'column-combinator': ' || ', | |
| 14 | } | |
| 15 | ||
| 16 | export function stringify(selector: Selector[][]): string { | |
| 17 | return selector.map(stringifySegments).join(', ') | |
| 18 | } | |
| 19 | ||
| 20 | function stringifySegments(tokens: Selector[]): string { | |
| 21 | return tokens.map((t, i) => stringifyOne(t, tokens[i - 1])).join('') | |
| 22 | } | |
| 23 | ||
| 24 | // eslint-disable-next-line pickier/no-unused-vars | |
| 25 | function stringifyOne(token: Selector, _prev?: Selector): string { | |
| 26 | switch (token.type) { | |
| 27 | case 'tag': return `${nsPrefix(token.namespace)}${escapeIdent(token.name)}` | |
| 28 | case 'universal': return `${nsPrefix(token.namespace)}*` | |
| 29 | case 'attribute': { | |
| 30 | if (token.name === 'id' && token.action === 'equals' && !token.ignoreCase && !token.namespace) | |
| 31 | return `#${escapeIdent(token.value)}` | |
| 32 | if (token.name === 'class' && token.action === 'element' && !token.ignoreCase && !token.namespace) | |
| 33 | return `.${escapeIdent(token.value)}` | |
| 34 | let out = `[${nsPrefix(token.namespace)}${escapeIdent(token.name)}` | |
| 35 | if (token.action !== 'exists') { | |
| 36 | const op = ACTION_OP[token.action] ?? '=' | |
| 37 | out += op | |
| 38 | out += `"${token.value.replace(/"/g, '\\"')}"` | |
| 39 | if (token.ignoreCase === true) | |
| 40 | out += ' i' | |
| 41 | else if (token.ignoreCase === false) | |
| 42 | out += ' s' | |
| 43 | } | |
| 44 | out += ']' | |
| 45 | return out | |
| 46 | } | |
| 47 | case 'pseudo': | |
| 48 | if (token.data === null) | |
| 49 | return `:${token.name}` | |
| 50 | if (typeof token.data === 'string') | |
| 51 | return `:${token.name}(${token.data})` | |
| 52 | return `:${token.name}(${stringify(token.data as Selector[][])})` | |
| 53 | case 'pseudo-element': | |
| 54 | return token.data === null ? `::${token.name}` : `::${token.name}(${token.data})` | |
| 55 | case 'descendant': | |
| 56 | case 'child': | |
| 57 | case 'parent': | |
| 58 | case 'sibling': | |
| 59 | case 'adjacent': | |
| 60 | case 'column-combinator': | |
| 61 | return COMBINATORS[token.type] ?? ' ' | |
| 62 | } | |
| 63 | return '' | |
| 64 | } | |
| 65 | ||
| 66 | // eslint-disable-next-line pickier/no-unused-vars | |
| 67 | const ACTION_OP: Record<string, string> = { | |
| 68 | equals: '=', | |
| 69 | element: '~=', | |
| 70 | start: '^=', | |
| 71 | end: '$=', | |
| 72 | any: '*=', | |
| 73 | not: '!=', | |
| 74 | hyphen: '|=', | |
| 75 | } | |
| 76 | ||
| 77 | function nsPrefix(ns: string | null): string { | |
| 78 | if (ns === null) | |
| 79 | return '' | |
| 80 | if (ns === '') | |
| 81 | return '|' | |
| 82 | return `${escapeIdent(ns)}|` | |
| 83 | } | |
| 84 | ||
| 85 | // Valid CSS identifier chars: ASCII letters, digits, `-`, `_`, and any | |
| 86 | // non-ASCII (>= 0x80). Anything else needs an escape. `\W` rejects | |
| 87 | // non-ASCII, so we use an explicit positive set. | |
| 88 | const RE_INVALID_ID_CHAR = /[^\w°--]/g | |
| 89 | ||
| 90 | function escapeIdent(name: string): string { | |
| 91 | if (name === '') | |
| 92 | return '' | |
| 93 | return name.replace(RE_INVALID_ID_CHAR, m => `\\${m}`) | |
| 94 | } | |
| @@ -0,0 +1,8 @@ | ||
| 1 | import type { Selector } from './types' | |
| 2 | ||
| 3 | const TRAVERSAL_TYPES = new Set(['adjacent', 'child', 'descendant', 'parent', 'sibling', 'column-combinator']) | |
| 4 | ||
| 5 | /** True when `token` is a traversal/combinator segment (vs. simple selector). */ | |
| 6 | export function isTraversal(token: Selector): boolean { | |
| 7 | return TRAVERSAL_TYPES.has(token.type) | |
| 8 | } | |
| @@ -0,0 +1,93 @@ | ||
| 1 | /** | |
| 2 | * Selector AST mirroring css-what v6 — the segment-list-of-lists shape | |
| 3 | * `parse(selector)` returns. Compatible with css-select consumers. | |
| 4 | */ | |
| 5 | ||
| 6 | export type SelectorType | |
| 7 | = | 'attribute' | |
| 8 | | 'pseudo' | |
| 9 | | 'pseudo-element' | |
| 10 | | 'tag' | |
| 11 | | 'universal' | |
| 12 | | 'adjacent' | |
| 13 | | 'child' | |
| 14 | | 'descendant' | |
| 15 | | 'parent' | |
| 16 | | 'sibling' | |
| 17 | | 'column-combinator' | |
| 18 | ||
| 19 | export type AttributeAction | |
| 20 | = | 'any' | |
| 21 | | 'element' | |
| 22 | | 'end' | |
| 23 | | 'equals' | |
| 24 | | 'exists' | |
| 25 | | 'hyphen' | |
| 26 | | 'not' | |
| 27 | | 'start' | |
| 28 | ||
| 29 | export const IgnoreCaseMode: Readonly<Record<string, true | false | 'quirks' | null>> = { | |
| 30 | Unknown: null, | |
| 31 | QuirksMode: 'quirks', | |
| 32 | IgnoreCase: true, | |
| 33 | CaseSensitive: false, | |
| 34 | } as const | |
| 35 | ||
| 36 | export type IgnoreCase = boolean | 'quirks' | null | |
| 37 | ||
| 38 | export interface AttributeSelectorNode { | |
| 39 | type: 'attribute' | |
| 40 | name: string | |
| 41 | action: AttributeAction | |
| 42 | value: string | |
| 43 | namespace: string | null | |
| 44 | ignoreCase: IgnoreCase | |
| 45 | } | |
| 46 | ||
| 47 | export interface TagSelectorNode { | |
| 48 | type: 'tag' | |
| 49 | name: string | |
| 50 | namespace: string | null | |
| 51 | } | |
| 52 | ||
| 53 | export interface UniversalSelectorNode { | |
| 54 | type: 'universal' | |
| 55 | namespace: string | null | |
| 56 | } | |
| 57 | ||
| 58 | export interface PseudoSelectorNode { | |
| 59 | type: 'pseudo' | |
| 60 | name: string | |
| 61 | data: string | Selector[][] | null | |
| 62 | } | |
| 63 | ||
| 64 | export interface PseudoElementNode { | |
| 65 | type: 'pseudo-element' | |
| 66 | name: string | |
| 67 | data: string | null | |
| 68 | } | |
| 69 | ||
| 70 | export interface TraversalNode { | |
| 71 | type: 'adjacent' | 'child' | 'descendant' | 'parent' | 'sibling' | 'column-combinator' | |
| 72 | } | |
| 73 | ||
| 74 | export type Selector | |
| 75 | = | AttributeSelectorNode | |
| 76 | | TagSelectorNode | |
| 77 | | UniversalSelectorNode | |
| 78 | | PseudoSelectorNode | |
| 79 | | PseudoElementNode | |
| 80 | | TraversalNode | |
| 81 | ||
| 82 | export interface ParseOptions { | |
| 83 | /** | |
| 84 | * When true, attribute names retain their original case. | |
| 85 | * css-what default mirrors HTML quirks-mode where attributes are | |
| 86 | * case-insensitive unless explicitly flagged. | |
| 87 | */ | |
| 88 | xmlMode?: boolean | |
| 89 | /** Lowercase attribute names. */ | |
| 90 | lowerCaseAttributeNames?: boolean | |
| 91 | /** Lowercase tag names. */ | |
| 92 | lowerCaseTags?: boolean | |
| 93 | } | |
| @@ -0,0 +1,52 @@ | ||
| 1 | /** | |
| 2 | * Real-world CSS fixtures. | |
| 3 | * | |
| 4 | * For each fixture we verify three properties: | |
| 5 | * 1. parse + generate round-trip is stable (re-parsing the output | |
| 6 | * produces the same string). | |
| 7 | * 2. minify produces output strictly smaller than input. | |
| 8 | * 3. minify is itself a fixed point (re-minifying doesn't change anything). | |
| 9 | */ | |
| 10 | ||
| 11 | import { describe, expect, it } from 'bun:test' | |
| 12 | import { readFileSync } from 'node:fs' | |
| 13 | import { join } from 'node:path' | |
| 14 | import { minify } from '../src/optimize' | |
| 15 | import { generate, parse } from '../src/parse' | |
| 16 | ||
| 17 | const FIXTURES = ['reset.css', 'utilities.css', 'component.css'] | |
| 18 | ||
| 19 | describe('fixtures', () => { | |
| 20 | for (const name of FIXTURES) { | |
| 21 | describe(name, () => { | |
| 22 | const source = readFileSync(join(import.meta.dir, 'fixtures', name), 'utf8') | |
| 23 | ||
| 24 | it('parse + generate round-trips stably', () => { | |
| 25 | const out1 = generate(parse(source)) | |
| 26 | const out2 = generate(parse(out1)) | |
| 27 | expect(out1).toBe(out2) | |
| 28 | }) | |
| 29 | ||
| 30 | it('minify shrinks the source meaningfully', () => { | |
| 31 | const { css } = minify(source) | |
| 32 | expect(css.length).toBeLessThan(source.length) | |
| 33 | // sanity floor — the minified output should keep the basic structure | |
| 34 | expect(css).toContain('{') | |
| 35 | expect(css).toContain('}') | |
| 36 | }) | |
| 37 | ||
| 38 | it('minify is idempotent', () => { | |
| 39 | const a = minify(source).css | |
| 40 | const b = minify(a).css | |
| 41 | expect(b).toBe(a) | |
| 42 | }) | |
| 43 | ||
| 44 | it('minify keeps `/*!*/` comments by default', () => { | |
| 45 | if (source.includes('/*!')) { | |
| 46 | const { css } = minify(source) | |
| 47 | expect(css).toContain('/*!') | |
| 48 | } | |
| 49 | }) | |
| 50 | }) | |
| 51 | } | |
| 52 | }) | |
| @@ -0,0 +1,88 @@ | ||
| 1 | /*! design-system-style component fixture */ | |
| 2 | ||
| 3 | :root { | |
| 4 | --color-primary: #3b82f6; | |
| 5 | --color-primary-hover: #2563eb; | |
| 6 | --color-text: #111827; | |
| 7 | --color-text-muted: #6b7280; | |
| 8 | --color-bg: #ffffff; | |
| 9 | --color-border: #e5e7eb; | |
| 10 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| 11 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| 12 | --radius-sm: 0.25rem; | |
| 13 | --radius-md: 0.5rem; | |
| 14 | --space-1: 0.25rem; | |
| 15 | --space-2: 0.5rem; | |
| 16 | --space-4: 1rem; | |
| 17 | } | |
| 18 | ||
| 19 | @media (prefers-color-scheme: dark) { | |
| 20 | :root { | |
| 21 | --color-text: #f9fafb; | |
| 22 | --color-text-muted: #9ca3af; | |
| 23 | --color-bg: #111827; | |
| 24 | --color-border: #374151; | |
| 25 | } | |
| 26 | } | |
| 27 | ||
| 28 | .btn { | |
| 29 | display: inline-flex; | |
| 30 | align-items: center; | |
| 31 | justify-content: center; | |
| 32 | padding: 0.5rem 1rem; | |
| 33 | border: 1px solid var(--color-border); | |
| 34 | border-radius: var(--radius-md); | |
| 35 | background-color: var(--color-bg); | |
| 36 | color: var(--color-text); | |
| 37 | font-weight: 500; | |
| 38 | font-size: 0.875rem; | |
| 39 | line-height: 1.25rem; | |
| 40 | cursor: pointer; | |
| 41 | transition: background-color 150ms ease-out, border-color 150ms ease-out; | |
| 42 | } | |
| 43 | .btn:hover { background-color: var(--color-primary); border-color: var(--color-primary); color: #fff; } | |
| 44 | .btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } | |
| 45 | .btn:disabled { opacity: 0.5; cursor: not-allowed; } | |
| 46 | ||
| 47 | .btn--primary { background-color: var(--color-primary); color: #fff; border-color: transparent; } | |
| 48 | .btn--primary:hover { background-color: var(--color-primary-hover); } | |
| 49 | .btn--small { padding: 0.25rem 0.5rem; font-size: 0.75rem; } | |
| 50 | .btn--icon-only { padding: 0.5rem; aspect-ratio: 1 / 1; } | |
| 51 | ||
| 52 | .card { | |
| 53 | background: var(--color-bg); | |
| 54 | border: 1px solid var(--color-border); | |
| 55 | border-radius: var(--radius-md); | |
| 56 | box-shadow: var(--shadow-sm); | |
| 57 | overflow: hidden; | |
| 58 | } | |
| 59 | .card > .card__header { padding: var(--space-4); border-bottom: 1px solid var(--color-border); font-weight: 600; } | |
| 60 | .card > .card__body { padding: var(--space-4); } | |
| 61 | .card > .card__footer { padding: var(--space-4); border-top: 1px solid var(--color-border); display: flex; justify-content: flex-end; gap: var(--space-2); } | |
| 62 | ||
| 63 | @keyframes fade-in { | |
| 64 | from { opacity: 0; transform: translateY(8px); } | |
| 65 | to { opacity: 1; transform: translateY(0); } | |
| 66 | } | |
| 67 | .fade-in { animation: fade-in 200ms ease-out; } | |
| 68 | ||
| 69 | @supports (backdrop-filter: blur(8px)) { | |
| 70 | .glass { backdrop-filter: blur(8px); background-color: rgba(255, 255, 255, 0.7); } | |
| 71 | } | |
| 72 | ||
| 73 | @container (min-width: 400px) { | |
| 74 | .card { padding: var(--space-4); } | |
| 75 | } | |
| 76 | ||
| 77 | .icon { width: 1em; height: 1em; flex-shrink: 0; fill: currentColor; } | |
| 78 | .icon--sm { width: 0.75em; height: 0.75em; } | |
| 79 | .icon--lg { width: 1.5em; height: 1.5em; } | |
| 80 | ||
| 81 | .list { list-style: none; margin: 0; padding: 0; } | |
| 82 | .list > li + li { margin-top: var(--space-2); } | |
| 83 | .list a:hover { text-decoration: underline; text-decoration-thickness: 2px; } | |
| 84 | ||
| 85 | @media (max-width: 600px) { | |
| 86 | .card { border-radius: 0; box-shadow: none; } | |
| 87 | .btn { width: 100%; } | |
| 88 | } | |
| @@ -0,0 +1,50 @@ | ||
| 1 | /*! normalize-style reset (excerpt). */ | |
| 2 | *, *::before, *::after { box-sizing: border-box; } | |
| 3 | html { | |
| 4 | -webkit-text-size-adjust: 100%; | |
| 5 | -moz-tab-size: 4; | |
| 6 | tab-size: 4; | |
| 7 | line-height: 1.15; | |
| 8 | } | |
| 9 | body { | |
| 10 | margin: 0; | |
| 11 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; | |
| 12 | } | |
| 13 | hr { box-sizing: content-box; height: 0; overflow: visible; } | |
| 14 | abbr[title] { text-decoration: underline dotted; } | |
| 15 | b, strong { font-weight: bolder; } | |
| 16 | code, kbd, samp, pre { | |
| 17 | font-family: ui-monospace, SFMono-Regular, "SF Mono", monospace; | |
| 18 | font-size: 1em; | |
| 19 | } | |
| 20 | small { font-size: 80%; } | |
| 21 | sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } | |
| 22 | sub { bottom: -0.25em; } | |
| 23 | sup { top: -0.5em; } | |
| 24 | table { border-color: inherit; text-indent: 0; } | |
| 25 | button, input, optgroup, select, textarea { | |
| 26 | font-family: inherit; | |
| 27 | font-size: 100%; | |
| 28 | line-height: 1.15; | |
| 29 | margin: 0; | |
| 30 | } | |
| 31 | button, [type='button'], [type='reset'], [type='submit'] { | |
| 32 | -webkit-appearance: button; | |
| 33 | } | |
| 34 | ::-moz-focus-inner { border-style: none; padding: 0; } | |
| 35 | :-moz-focusring { outline: 1px dotted ButtonText; } | |
| 36 | :-moz-ui-invalid { box-shadow: none; } | |
| 37 | legend { | |
| 38 | box-sizing: border-box; | |
| 39 | color: inherit; | |
| 40 | display: table; | |
| 41 | max-width: 100%; | |
| 42 | padding: 0; | |
| 43 | white-space: normal; | |
| 44 | } | |
| 45 | progress { vertical-align: baseline; } | |
| 46 | ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } | |
| 47 | [type='search'] { -webkit-appearance: textfield; outline-offset: -2px; } | |
| 48 | ::-webkit-search-decoration { -webkit-appearance: none; } | |
| 49 | ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } | |
| 50 | summary { display: list-item; } | |
| @@ -0,0 +1,64 @@ | ||
| 1 | /*! tailwind-style utility excerpt */ | |
| 2 | .flex { display: flex; } | |
| 3 | .inline-flex { display: inline-flex; } | |
| 4 | .grid { display: grid; } | |
| 5 | .hidden { display: none; } | |
| 6 | ||
| 7 | .flex-col { flex-direction: column; } | |
| 8 | .flex-row { flex-direction: row; } | |
| 9 | .flex-wrap { flex-wrap: wrap; } | |
| 10 | .items-center { align-items: center; } | |
| 11 | .justify-center { justify-content: center; } | |
| 12 | .justify-between { justify-content: space-between; } | |
| 13 | ||
| 14 | .gap-1 { gap: 0.25rem; } | |
| 15 | .gap-2 { gap: 0.5rem; } | |
| 16 | .gap-4 { gap: 1rem; } | |
| 17 | .gap-8 { gap: 2rem; } | |
| 18 | ||
| 19 | .p-0 { padding: 0; } | |
| 20 | .p-1 { padding: 0.25rem; } | |
| 21 | .p-2 { padding: 0.5rem; } | |
| 22 | .p-4 { padding: 1rem; } | |
| 23 | .p-8 { padding: 2rem; } | |
| 24 | ||
| 25 | .text-xs { font-size: 0.75rem; line-height: 1rem; } | |
| 26 | .text-sm { font-size: 0.875rem; line-height: 1.25rem; } | |
| 27 | .text-base { font-size: 1rem; line-height: 1.5rem; } | |
| 28 | .text-lg { font-size: 1.125rem; line-height: 1.75rem; } | |
| 29 | .text-xl { font-size: 1.25rem; line-height: 1.75rem; } | |
| 30 | ||
| 31 | .bg-white { background-color: #ffffff; } | |
| 32 | .bg-black { background-color: #000000; } | |
| 33 | .bg-red-500 { background-color: #ef4444; } | |
| 34 | .bg-blue-500 { background-color: #3b82f6; } | |
| 35 | .bg-green-500 { background-color: #22c55e; } | |
| 36 | ||
| 37 | .text-white { color: #ffffff; } | |
| 38 | .text-black { color: #000000; } | |
| 39 | .text-gray-500 { color: #6b7280; } | |
| 40 | ||
| 41 | .rounded { border-radius: 0.25rem; } | |
| 42 | .rounded-md { border-radius: 0.375rem; } | |
| 43 | .rounded-lg { border-radius: 0.5rem; } | |
| 44 | .rounded-full { border-radius: 9999px; } | |
| 45 | ||
| 46 | .shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); } | |
| 47 | .shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } | |
| 48 | .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } | |
| 49 | ||
| 50 | .transition { transition-property: all; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } | |
| 51 | .duration-300 { transition-duration: 300ms; } | |
| 52 | ||
| 53 | @media (min-width: 640px) { | |
| 54 | .sm\:flex { display: flex; } | |
| 55 | .sm\:hidden { display: none; } | |
| 56 | .sm\:p-4 { padding: 1rem; } | |
| 57 | } | |
| 58 | @media (min-width: 768px) { | |
| 59 | .md\:grid { display: grid; } | |
| 60 | .md\:hidden { display: none; } | |
| 61 | } | |
| 62 | @media (min-width: 1024px) { | |
| 63 | .lg\:flex { display: flex; } | |
| 64 | } | |
| @@ -0,0 +1,92 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { minify, minifyBlock, syntax } from '../src/optimize' | |
| 3 | import { parse } from '../src/parse' | |
| 4 | ||
| 5 | describe('minify', () => { | |
| 6 | it('strips comments', () => { | |
| 7 | expect(minify('/* hi */ .a{color:red}').css).toBe('.a{color:red}') | |
| 8 | }) | |
| 9 | it('preserves /*!*/ comments by default', () => { | |
| 10 | expect(minify('/*! keep */.a{color:red}').css).toContain('keep') | |
| 11 | }) | |
| 12 | it('drops `/*!*/` when comments:false', () => { | |
| 13 | expect(minify('/*! keep */.a{color:red}', { comments: false }).css).toBe('.a{color:red}') | |
| 14 | }) | |
| 15 | it('shortens decimals', () => { | |
| 16 | expect(minify('.a{margin:0.5em 10.000px}').css).toBe('.a{margin:.5em 10px}') | |
| 17 | }) | |
| 18 | it('drops zero units', () => { | |
| 19 | expect(minify('.a{margin:0px 0em}').css).toBe('.a{margin:0 0}') | |
| 20 | }) | |
| 21 | it('dedupes declarations keeping the later one', () => { | |
| 22 | expect(minify('.a{color:red;color:blue}').css).toBe('.a{color:blue}') | |
| 23 | }) | |
| 24 | it('respects !important when deduping', () => { | |
| 25 | expect(minify('.a{color:red!important;color:blue}').css).toBe('.a{color:red!important}') | |
| 26 | }) | |
| 27 | it('shortens hex colors', () => { | |
| 28 | expect(minify('.a{color:#aabbcc}').css).toBe('.a{color:#abc}') | |
| 29 | }) | |
| 30 | ||
| 31 | it('shortens named colors when hex is strictly shorter', () => { | |
| 32 | // black (5) → #000 (4): replace | |
| 33 | expect(minify('.a{color:black}').css).toBe('.a{color:#000}') | |
| 34 | // white (5) → #fff (4): replace | |
| 35 | expect(minify('.a{color:white}').css).toBe('.a{color:#fff}') | |
| 36 | }) | |
| 37 | ||
| 38 | it('keeps named colors that tie or beat hex', () => { | |
| 39 | // blue (4) ties #00f (4) — keep keyword | |
| 40 | expect(minify('.a{color:blue}').css).toBe('.a{color:blue}') | |
| 41 | // red (3) beats #f00 (4) — keep keyword | |
| 42 | expect(minify('.a{color:red}').css).toBe('.a{color:red}') | |
| 43 | }) | |
| 44 | ||
| 45 | it('only swaps colors inside color properties', () => { | |
| 46 | // font-family is not a color property — `red` stays | |
| 47 | expect(minify('.a{font-family:red}').css).toBe('.a{font-family:red}') | |
| 48 | }) | |
| 49 | ||
| 50 | it('reverses hex → name when name is strictly shorter', () => { | |
| 51 | // #ff0000 (7) → red (3): replace | |
| 52 | expect(minify('.a{color:#ff0000}').css).toBe('.a{color:red}') | |
| 53 | }) | |
| 54 | }) | |
| 55 | ||
| 56 | describe('minifyBlock', () => { | |
| 57 | it('minifies declaration lists in isolation', () => { | |
| 58 | expect(minifyBlock('color:red;color:blue').css).toBe('color:blue') | |
| 59 | }) | |
| 60 | }) | |
| 61 | ||
| 62 | describe('syntax.specificity', () => { | |
| 63 | it('counts ids', () => { | |
| 64 | const sel = parse('#a', { context: 'selector' }) | |
| 65 | expect(syntax.specificity(sel)).toEqual([1, 0, 0]) | |
| 66 | }) | |
| 67 | it('counts classes and attributes', () => { | |
| 68 | const sel = parse('.a[type=button]', { context: 'selector' }) | |
| 69 | expect(syntax.specificity(sel)).toEqual([0, 2, 0]) | |
| 70 | }) | |
| 71 | it('counts type selectors', () => { | |
| 72 | const sel = parse('div p', { context: 'selector' }) | |
| 73 | expect(syntax.specificity(sel)).toEqual([0, 0, 2]) | |
| 74 | }) | |
| 75 | it(':where() contributes nothing', () => { | |
| 76 | const sel = parse(':where(#a) p', { context: 'selector' }) | |
| 77 | expect(syntax.specificity(sel)).toEqual([0, 0, 1]) | |
| 78 | }) | |
| 79 | it(':is() takes max of args', () => { | |
| 80 | const sel = parse(':is(#a, .b)', { context: 'selector' }) | |
| 81 | expect(syntax.specificity(sel)).toEqual([1, 0, 0]) | |
| 82 | }) | |
| 83 | it('accepts a raw selector string', () => { | |
| 84 | expect(syntax.specificity('#a.b div')).toEqual([1, 1, 1]) | |
| 85 | expect(syntax.specificity('p > span:not(#x)')).toEqual([1, 0, 2]) | |
| 86 | }) | |
| 87 | it('accepts cssWhat-style Selector[][] arrays', async () => { | |
| 88 | const { parse: parseWhat } = await import('../src/what') | |
| 89 | const ast = parseWhat('#a.b div') | |
| 90 | expect(syntax.specificity(ast)).toEqual([1, 1, 1]) | |
| 91 | }) | |
| 92 | }) | |
| @@ -0,0 +1,203 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { generate, List, parse, walk } from '../src/parse' | |
| 3 | ||
| 4 | describe('parse + generate round-trip', () => { | |
| 5 | it('parses a basic stylesheet', () => { | |
| 6 | const ast = parse('.foo { color: red }') as any | |
| 7 | expect(ast.type).toBe('StyleSheet') | |
| 8 | expect(ast.children.first.type).toBe('Rule') | |
| 9 | }) | |
| 10 | ||
| 11 | it('keeps `/*!*/` comments at generate time but strips others', () => { | |
| 12 | // matches css-tree behaviour: regular comments are parsed (so plugins | |
| 13 | // can see them) but NOT regenerated by default. `!`-comments survive. | |
| 14 | expect(generate(parse('/* hi */.a{color:red}'))).toBe('.a{color:red}') | |
| 15 | expect(generate(parse('/*! keep */.a{color:red}'))).toBe('/*! keep */.a{color:red}') | |
| 16 | }) | |
| 17 | ||
| 18 | it('preserves !important', () => { | |
| 19 | const css = '.a{color:red!important}' | |
| 20 | expect(generate(parse(css))).toBe(css) | |
| 21 | }) | |
| 22 | ||
| 23 | it('parses comma-separated selectors', () => { | |
| 24 | const ast = parse('.a, .b, .c {}') as any | |
| 25 | const list = ast.children.first.prelude | |
| 26 | expect(list.type).toBe('SelectorList') | |
| 27 | expect(list.children.toArray().length).toBe(3) | |
| 28 | }) | |
| 29 | ||
| 30 | it('parses combinators', () => { | |
| 31 | const ast = parse('.a > .b + .c ~ .d {}') as any | |
| 32 | const sel = ast.children.first.prelude.children.first | |
| 33 | const types = sel.children.toArray().map((n: any) => n.type) | |
| 34 | expect(types).toContain('Combinator') | |
| 35 | }) | |
| 36 | ||
| 37 | it('parses an at-rule with block', () => { | |
| 38 | const ast = parse('@media (min-width: 800px) { .a { color: red } }') as any | |
| 39 | expect(ast.children.first.type).toBe('Atrule') | |
| 40 | expect(ast.children.first.name).toBe('media') | |
| 41 | }) | |
| 42 | ||
| 43 | it('parses CSS Nesting (& mid-selector)', () => { | |
| 44 | const ast = parse('.foo { color: red; &.bar { color: blue } }') as any | |
| 45 | const blockChildren = ast.children.first.block.children.toArray().map((n: any) => n.type) | |
| 46 | expect(blockChildren).toEqual(['Declaration', 'Rule']) | |
| 47 | }) | |
| 48 | ||
| 49 | it('parses namespace selectors svg|circle', () => { | |
| 50 | const ast = parse('svg|circle{}') as any | |
| 51 | const tag = ast.children.first.prelude.children.first.children.first | |
| 52 | expect(tag.type).toBe('TypeSelector') | |
| 53 | expect(tag.name).toBe('svg|circle') | |
| 54 | }) | |
| 55 | ||
| 56 | it('promotes Number/Number to Ratio in media queries', () => { | |
| 57 | const ast = parse('@media (aspect-ratio: 16/9) {}') as any | |
| 58 | let foundRatio: any = null | |
| 59 | walk(ast, (n) => { | |
| 60 | if (n.type === 'Ratio') foundRatio = n | |
| 61 | }) | |
| 62 | expect(foundRatio).not.toBeNull() | |
| 63 | expect(foundRatio.left.value).toBe('16') | |
| 64 | expect(foundRatio.right.value).toBe('9') | |
| 65 | }) | |
| 66 | ||
| 67 | it('parses unknown at-rule preludes (no hardcoded whitelist)', () => { | |
| 68 | const ast = parse('@my-custom rule (foo) { .a { color: red } }') as any | |
| 69 | const at = ast.children.first | |
| 70 | expect(at.type).toBe('Atrule') | |
| 71 | expect(at.name).toBe('my-custom') | |
| 72 | // Prelude should be parsed structurally (AtrulePrelude with children), | |
| 73 | // not stored as a Raw blob. | |
| 74 | expect(at.prelude.type).toBe('AtrulePrelude') | |
| 75 | expect(at.prelude.children.toArray().length).toBeGreaterThan(0) | |
| 76 | }) | |
| 77 | ||
| 78 | it('parses @scope, @starting-style, and @container with nested rules', () => { | |
| 79 | for (const name of ['scope', 'starting-style', 'container']) { | |
| 80 | const ast = parse(`@${name} (foo) { .x { color: blue } }`) as any | |
| 81 | const at = ast.children.first | |
| 82 | expect(at.type).toBe('Atrule') | |
| 83 | expect(at.block.children.first.type).toBe('Rule') | |
| 84 | } | |
| 85 | }) | |
| 86 | ||
| 87 | it('calls onParseError for malformed declarations', () => { | |
| 88 | const errors: string[] = [] | |
| 89 | parse('.a { invalid-no-colon; color: red; }', { | |
| 90 | onParseError: (err) => { errors.push(err.message) }, | |
| 91 | }) | |
| 92 | expect(errors.length).toBeGreaterThan(0) | |
| 93 | }) | |
| 94 | ||
| 95 | it('parses url() function', () => { | |
| 96 | const ast = parse('.a{background:url("x.png")}') as any | |
| 97 | let foundUrl = false | |
| 98 | walk(ast, (n) => { | |
| 99 | if (n.type === 'Url') foundUrl = true | |
| 100 | }) | |
| 101 | expect(foundUrl).toBe(true) | |
| 102 | }) | |
| 103 | ||
| 104 | it('walk.skip stops traversal into a node', () => { | |
| 105 | const ast = parse('.a{color:red} .b{color:blue}') | |
| 106 | const seen: string[] = [] | |
| 107 | walk(ast, function (n) { | |
| 108 | if (n.type === 'Rule') | |
| 109 | seen.push('rule') | |
| 110 | if (n.type === 'Block') | |
| 111 | return walk.skip | |
| 112 | }) | |
| 113 | expect(seen.length).toBe(2) | |
| 114 | }) | |
| 115 | ||
| 116 | it('parses declarationList context', () => { | |
| 117 | const ast = parse('color:red;font-size:12px', { context: 'declarationList' }) as any | |
| 118 | expect(ast.type).toBe('DeclarationList') | |
| 119 | expect(ast.children.toArray().length).toBe(2) | |
| 120 | }) | |
| 121 | ||
| 122 | it('narrows visitor.enter via visit filter', () => { | |
| 123 | const ast = parse('.a{color:red} .b{color:blue}') | |
| 124 | const propsByRule: string[][] = [] | |
| 125 | walk(ast, { | |
| 126 | visit: 'Rule', | |
| 127 | enter(rule) { | |
| 128 | // `rule` is narrowed to Rule here — TS should NOT see the wider union. | |
| 129 | const props: string[] = [] | |
| 130 | walk(rule.block, { | |
| 131 | visit: 'Declaration', | |
| 132 | enter(decl) { | |
| 133 | // `decl` narrowed to Declaration | |
| 134 | props.push(decl.property) | |
| 135 | }, | |
| 136 | }) | |
| 137 | propsByRule.push(props) | |
| 138 | }, | |
| 139 | }) | |
| 140 | expect(propsByRule).toEqual([['color'], ['color']]) | |
| 141 | }) | |
| 142 | ||
| 143 | it('exposes a doubly linked List', () => { | |
| 144 | const list = new List<number>() | |
| 145 | list.appendData(1) | |
| 146 | list.appendData(2) | |
| 147 | list.appendData(3) | |
| 148 | expect(list.toArray()).toEqual([1, 2, 3]) | |
| 149 | list.shift() | |
| 150 | expect(list.toArray()).toEqual([2, 3]) | |
| 151 | }) | |
| 152 | ||
| 153 | it('round-trips strings with escape sequences', () => { | |
| 154 | const cases = [ | |
| 155 | '.a{content:"with \\"quote\\" inside"}', | |
| 156 | '.a{content:"line1\\a line2"}', | |
| 157 | 'a[data-x="\\"x\\""]{color:red}', | |
| 158 | ] | |
| 159 | for (const c of cases) { | |
| 160 | const out1 = generate(parse(c)) | |
| 161 | const out2 = generate(parse(out1)) | |
| 162 | expect(out1).toBe(out2) | |
| 163 | } | |
| 164 | }) | |
| 165 | ||
| 166 | it('decodes hex escapes in string values', () => { | |
| 167 | const ast = parse('.a{content:"\\26 B"}') as any | |
| 168 | const decl = ast.children.first.block.children.first | |
| 169 | const string = decl.value.children.first | |
| 170 | expect(string.type).toBe('String') | |
| 171 | expect(string.value).toBe('&B') | |
| 172 | }) | |
| 173 | ||
| 174 | it('round-trips selector identifier escapes', () => { | |
| 175 | const cases = [ | |
| 176 | '.\\26 B{color:red}', | |
| 177 | '.foo\\30 bar{color:red}', | |
| 178 | '.foo\\!bar{color:red}', | |
| 179 | ] | |
| 180 | for (const c of cases) { | |
| 181 | const out1 = generate(parse(c)) | |
| 182 | const out2 = generate(parse(out1)) | |
| 183 | expect(out1).toBe(out2) | |
| 184 | } | |
| 185 | }) | |
| 186 | ||
| 187 | it('survives nested forEach with mid-walk removal', () => { | |
| 188 | const list = new List<number>() | |
| 189 | list.fromArray([1, 2, 3, 4, 5]) | |
| 190 | const seen: number[] = [] | |
| 191 | list.forEach((data, item) => { | |
| 192 | seen.push(data) | |
| 193 | // a nested walk over the SAME list — must not corrupt the outer cursor. | |
| 194 | list.forEach((d) => { void d }) | |
| 195 | if (data === 2) { | |
| 196 | // remove the current node mid-iteration | |
| 197 | list.remove(item) | |
| 198 | } | |
| 199 | }) | |
| 200 | expect(seen).toEqual([1, 2, 3, 4, 5]) | |
| 201 | expect(list.toArray()).toEqual([1, 3, 4, 5]) | |
| 202 | }) | |
| 203 | }) | |
| @@ -0,0 +1,131 @@ | ||
| 1 | import type { Adapter, Options } from '../src/select' | |
| 2 | import { describe, expect, it } from 'bun:test' | |
| 3 | import { is, selectAll, selectOne } from '../src/select' | |
| 4 | ||
| 5 | interface El { type: 'el', name: string, attrs: Record<string, string>, children: El[], parent: El | null } | |
| 6 | ||
| 7 | function el(name: string, attrs: Record<string, string> = {}, ...children: El[]): El { | |
| 8 | const e: El = { type: 'el', name, attrs, children, parent: null } | |
| 9 | for (const c of children) c.parent = e | |
| 10 | return e | |
| 11 | } | |
| 12 | ||
| 13 | const adapter: Adapter<El, El> = { | |
| 14 | isTag: (n: any): n is El => n && n.type === 'el', | |
| 15 | existsOne: (test, elems) => elems.some(e => adapter.isTag(e) && (test(e) || adapter.existsOne(test, e.children))), | |
| 16 | getAttributeValue: (e, n) => e.attrs[n], | |
| 17 | getChildren: (e: any) => e.children ?? [], | |
| 18 | getName: e => e.name, | |
| 19 | getParent: (e: any) => e.parent ?? null, | |
| 20 | getSiblings: (e: any) => e.parent ? e.parent.children : [e], | |
| 21 | getText: () => '', | |
| 22 | hasAttrib: (e, n) => n in e.attrs, | |
| 23 | removeSubsets: n => n, | |
| 24 | findAll: (test, elems) => { | |
| 25 | const out: El[] = [] | |
| 26 | for (const e of elems) { | |
| 27 | if (adapter.isTag(e)) { | |
| 28 | if (test(e)) out.push(e) | |
| 29 | out.push(...adapter.findAll!(test, e.children)) | |
| 30 | } | |
| 31 | } | |
| 32 | return out | |
| 33 | }, | |
| 34 | findOne: (test, elems) => { | |
| 35 | for (const e of elems) { | |
| 36 | if (adapter.isTag(e)) { | |
| 37 | if (test(e)) return e | |
| 38 | const r = adapter.findOne!(test, e.children) | |
| 39 | if (r) return r | |
| 40 | } | |
| 41 | } | |
| 42 | return null | |
| 43 | }, | |
| 44 | } | |
| 45 | ||
| 46 | const opts: Options<El, El> = { adapter, xmlMode: true } | |
| 47 | ||
| 48 | const tree = el('div', { class: 'root' }, | |
| 49 | el('p', { id: 'a' }, el('span', { class: 'foo bar' })), | |
| 50 | el('p', { id: 'b' }, | |
| 51 | el('span', { class: 'foo' }), | |
| 52 | el('span', { class: 'bar' }), | |
| 53 | ), | |
| 54 | ) | |
| 55 | ||
| 56 | describe('selectAll', () => { | |
| 57 | it('matches by tag name', () => { | |
| 58 | expect(selectAll('span', tree, opts).length).toBe(3) | |
| 59 | }) | |
| 60 | it('matches by class', () => { | |
| 61 | expect(selectAll('.foo', tree, opts).length).toBe(2) | |
| 62 | }) | |
| 63 | it('matches by id', () => { | |
| 64 | expect(selectAll('#a', tree, opts).length).toBe(1) | |
| 65 | }) | |
| 66 | it('matches child combinator', () => { | |
| 67 | expect(selectAll('p > span.foo', tree, opts).length).toBe(2) | |
| 68 | }) | |
| 69 | it('matches descendant combinator', () => { | |
| 70 | expect(selectAll('p#a span', tree, opts).length).toBe(1) | |
| 71 | }) | |
| 72 | it('matches attribute ~=', () => { | |
| 73 | expect(selectAll('[class~="bar"]', tree, opts).length).toBe(2) | |
| 74 | }) | |
| 75 | it('matches :not()', () => { | |
| 76 | expect(selectAll('p:not(#a)', tree, opts).length).toBe(1) | |
| 77 | }) | |
| 78 | it('matches :has()', () => { | |
| 79 | // querySelector-style: passing a single root excludes the root itself | |
| 80 | // (matches css-select). Wrap in `[tree]` to opt into root-as-candidate. | |
| 81 | expect(selectAll('div:has(span.bar)', [tree], opts).length).toBe(1) | |
| 82 | }) | |
| 83 | it('matches :nth-child', () => { | |
| 84 | expect(selectAll('p:nth-child(1)', tree, opts).length).toBe(1) | |
| 85 | }) | |
| 86 | it('matches :first-child', () => { | |
| 87 | expect(selectAll('span:first-child', tree, opts).length).toBe(2) | |
| 88 | }) | |
| 89 | it('matches adjacent sibling', () => { | |
| 90 | expect(selectAll('p + p', tree, opts).length).toBe(1) | |
| 91 | }) | |
| 92 | it('matches general sibling', () => { | |
| 93 | expect(selectAll('p ~ p', tree, opts).length).toBe(1) | |
| 94 | }) | |
| 95 | }) | |
| 96 | ||
| 97 | describe('selectOne / is', () => { | |
| 98 | it('selectOne returns the first match', () => { | |
| 99 | expect(selectOne('.bar', tree, opts)).not.toBeNull() | |
| 100 | }) | |
| 101 | it('is() tests a node directly', () => { | |
| 102 | const span = tree.children[0]!.children[0]! | |
| 103 | expect(is(span, '.foo', opts)).toBe(true) | |
| 104 | expect(is(span, '.missing', opts)).toBe(false) | |
| 105 | }) | |
| 106 | }) | |
| 107 | ||
| 108 | describe('compile cache', () => { | |
| 109 | it('reuses compiled selectors with default options', async () => { | |
| 110 | const { compile, clearSelectorCache } = await import('../src/select') | |
| 111 | clearSelectorCache(adapter as any) | |
| 112 | const a = compile('p > span.foo', opts) | |
| 113 | const b = compile('p > span.foo', opts) | |
| 114 | expect(a).toBe(b) | |
| 115 | }) | |
| 116 | ||
| 117 | it('skips the cache when cacheResults is false', async () => { | |
| 118 | const { compile } = await import('../src/select') | |
| 119 | const noCache: typeof opts = { ...opts, cacheResults: false } | |
| 120 | const a = compile('p > span', noCache) | |
| 121 | const b = compile('p > span', noCache) | |
| 122 | expect(a).not.toBe(b) | |
| 123 | }) | |
| 124 | ||
| 125 | it('keys cache by xmlMode flag', async () => { | |
| 126 | const { compile } = await import('../src/select') | |
| 127 | const a = compile('p', opts) | |
| 128 | const b = compile('p', { ...opts, xmlMode: false }) | |
| 129 | expect(a).not.toBe(b) | |
| 130 | }) | |
| 131 | }) | |
| @@ -0,0 +1,48 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { isTraversal, parse, stringify } from '../src/what' | |
| 3 | ||
| 4 | describe('what.parse', () => { | |
| 5 | it('parses a tag selector', () => { | |
| 6 | expect(parse('div')).toEqual([[{ type: 'tag', name: 'div', namespace: null }]]) | |
| 7 | }) | |
| 8 | it('parses a class selector as attribute selector', () => { | |
| 9 | const ast = parse('.foo') | |
| 10 | expect(ast[0]![0]).toMatchObject({ type: 'attribute', name: 'class', action: 'element', value: 'foo' }) | |
| 11 | }) | |
| 12 | it('parses combinators', () => { | |
| 13 | const ast = parse('a > b + c ~ d') | |
| 14 | const types = ast[0]!.map(x => x.type) | |
| 15 | expect(types).toEqual(['tag', 'child', 'tag', 'adjacent', 'tag', 'sibling', 'tag']) | |
| 16 | }) | |
| 17 | it('parses descendant combinator', () => { | |
| 18 | const ast = parse('a b') | |
| 19 | const types = ast[0]!.map(x => x.type) | |
| 20 | expect(types).toEqual(['tag', 'descendant', 'tag']) | |
| 21 | }) | |
| 22 | it('parses :not(...) with sub-selectors', () => { | |
| 23 | const ast = parse(':not(.x, .y)') | |
| 24 | expect(ast[0]![0]).toMatchObject({ type: 'pseudo', name: 'not' }) | |
| 25 | expect(Array.isArray((ast[0]![0] as any).data)).toBe(true) | |
| 26 | }) | |
| 27 | it('parses pseudo-elements', () => { | |
| 28 | expect(parse('::before')[0]![0]).toMatchObject({ type: 'pseudo-element', name: 'before' }) | |
| 29 | }) | |
| 30 | }) | |
| 31 | ||
| 32 | describe('what.stringify', () => { | |
| 33 | it('round-trips simple selectors', () => { | |
| 34 | const cases = ['div', '.a', '#x', 'a > b', '[type="button"]', ':hover', '::before'] | |
| 35 | for (const c of cases) { | |
| 36 | const re = stringify(parse(c)) | |
| 37 | // Round-trip should produce something that re-parses to the same shape | |
| 38 | expect(parse(re)).toEqual(parse(c)) | |
| 39 | } | |
| 40 | }) | |
| 41 | }) | |
| 42 | ||
| 43 | describe('what.isTraversal', () => { | |
| 44 | it('detects combinator types', () => { | |
| 45 | expect(isTraversal({ type: 'descendant' } as any)).toBe(true) | |
| 46 | expect(isTraversal({ type: 'tag', name: 'a', namespace: null })).toBe(false) | |
| 47 | }) | |
| 48 | }) | |
| @@ -5,9 +5,6 @@ | ||
| 5 | 5 | "moduleDetection": "force", |
| 6 | 6 | "module": "esnext", |
| 7 | 7 | "moduleResolution": "bundler", |
| 8 | "paths": { | |
| 9 | "ts-css": ["./packages/ts-css/src/index.ts"] | |
| 10 | }, | |
| 11 | 8 | "resolveJsonModule": true, |
| 12 | 9 | "types": ["bun"], |
| 13 | 10 | "allowImportingTsExtensions": true, |
| @@ -23,10 +20,5 @@ | ||
| 23 | 20 | "verbatimModuleSyntax": true, |
| 24 | 21 | "skipDefaultLibCheck": true, |
| 25 | 22 | "skipLibCheck": true |
| 26 | }, | |
| 27 | "exclude": [ | |
| 28 | "pantry", | |
| 29 | "packages/ts-css/examples/**/*", | |
| 30 | "benchmark" | |
| 31 | ] | |
| 23 | } | |
| 32 | 24 | } |
| @@ -5,7 +5,7 @@ A Bun plugin that automatically generates and injects Crosswind CSS into your HT | ||
| 5 | 5 | ## Installation |
| 6 | 6 | |
| 7 | 7 | ```bash |
| 8 | bun add ts-css | |
| 8 | bun add @ts-css/core | |
| 9 | 9 | ``` |
| 10 | 10 | |
| 11 | 11 | ## Quick Start**1. Create your HTML file**(`src/template.html`) |
| @@ -32,7 +32,7 @@ document.body.innerHTML = template | ||
| 32 | 32 | ```**3. Build with the plugin**: |
| 33 | 33 | |
| 34 | 34 | ```typescript |
| 35 | import { plugin } from 'ts-css' | |
| 35 | import { plugin } from '@ts-css/core' | |
| 36 | 36 | |
| 37 | 37 | await Bun.build({ |
| 38 | 38 | entrypoints: ['./src/index.ts'], |
| @@ -49,7 +49,7 @@ await Bun.build({ | ||
| 49 | 49 | |
| 50 | 50 | ### Basic Configuration```typescript |
| 51 | 51 | |
| 52 | import { plugin } from 'ts-css' | |
| 52 | import { plugin } from '@ts-css/core' | |
| 53 | 53 | |
| 54 | 54 | await Bun.build({ |
| 55 | 55 | entrypoints: ['./src/index.ts'], |
| @@ -62,7 +62,7 @@ await Bun.build({ | ||
| 62 | 62 | }) |
| 63 | 63 | |
| 64 | 64 | ```### Custom Theme```typescript |
| 65 | import { plugin } from 'ts-css' | |
| 65 | import { plugin } from '@ts-css/core' | |
| 66 | 66 | |
| 67 | 67 | await Bun.build({ |
| 68 | 68 | entrypoints: ['./src/index.ts'], |
| @@ -92,7 +92,7 @@ await Bun.build({ | ||
| 92 | 92 | }) |
| 93 | 93 | ```### Advanced Configuration```typescript |
| 94 | 94 | |
| 95 | import { plugin } from 'ts-css' | |
| 95 | import { plugin } from '@ts-css/core' | |
| 96 | 96 | |
| 97 | 97 | await Bun.build({ |
| 98 | 98 | entrypoints: ['./src/index.ts'], |
| @@ -186,7 +186,7 @@ The plugin is highly performant: | ||
| 186 | 186 | ## TypeScript Support |
| 187 | 187 | |
| 188 | 188 | The plugin is fully typed. Import the types:```typescript |
| 189 | import type { TsCssPluginOptions } from 'ts-css' | |
| 189 | import type { TsCssPluginOptions } from '@ts-css/core' | |
| 190 | 190 | |
| 191 | 191 | const options: TsCssPluginOptions = { |
| 192 | 192 | config: { |
| @@ -197,7 +197,7 @@ const options: TsCssPluginOptions = { | ||
| 197 | 197 | |
| 198 | 198 | The plugin also respects`crosswind.config.ts`in your project root:```typescript |
| 199 | 199 | // crosswind.config.ts |
| 200 | import type { TsCssOptions } from 'ts-css' | |
| 200 | import type { TsCssOptions } from '@ts-css/core' | |
| 201 | 201 | |
| 202 | 202 | export default { |
| 203 | 203 | minify: true, |
| @@ -5,7 +5,7 @@ A CSS engine with two front ends and one atomic output: Tailwind-compatible **ut | ||
| 5 | 5 | ## Installation |
| 6 | 6 | |
| 7 | 7 | ```bash |
| 8 | bun add ts-css | |
| 8 | bun add @ts-css/core | |
| 9 | 9 | ``` |
| 10 | 10 | |
| 11 | 11 | ## Quick Start |
| @@ -13,7 +13,7 @@ bun add ts-css | ||
| 13 | 13 | ### Programmatic API |
| 14 | 14 | |
| 15 | 15 | ```typescript |
| 16 | import { CSSGenerator, defaultConfig } from 'ts-css' | |
| 16 | import { CSSGenerator, defaultConfig } from '@ts-css/core' | |
| 17 | 17 | |
| 18 | 18 | const gen = new CSSGenerator(defaultConfig) |
| 19 | 19 | gen.generate('flex') |
| @@ -32,7 +32,7 @@ const css = gen.toCSS(true) // true = include preflight | ||
| 32 | 32 | ### Build API |
| 33 | 33 | |
| 34 | 34 | ```typescript |
| 35 | import { build } from 'ts-css' | |
| 35 | import { build } from '@ts-css/core' | |
| 36 | 36 | |
| 37 | 37 | const result = await build({ |
| 38 | 38 | content: ['./src/**/*.html', './src/**/*.tsx'], |
| @@ -44,7 +44,7 @@ const result = await build({ | ||
| 44 | 44 | ### Style API |
| 45 | 45 | |
| 46 | 46 | ```typescript |
| 47 | import { css } from 'ts-css' | |
| 47 | import { css } from '@ts-css/core' | |
| 48 | 48 | |
| 49 | 49 | const theme = css.defineVars({ |
| 50 | 50 | accent: '#0b7', |
| @@ -101,7 +101,7 @@ cssx build --minify | ||
| 101 | 101 | Create a `css.config.ts` in your project root: |
| 102 | 102 | |
| 103 | 103 | ```typescript |
| 104 | import type { TsCssConfig } from 'ts-css' | |
| 104 | import type { TsCssConfig } from '@ts-css/core' | |
| 105 | 105 | |
| 106 | 106 | export default { |
| 107 | 107 | content: ['./src/**/*.{html,tsx,stx}'], |
| @@ -588,7 +588,7 @@ Prefix with `!` to apply `!important`: | ||
| 588 | 588 | ## Presets |
| 589 | 589 | |
| 590 | 590 | ```typescript |
| 591 | import type { Preset } from 'ts-css' | |
| 591 | import type { Preset } from '@ts-css/core' | |
| 592 | 592 | |
| 593 | 593 | const myPreset: Preset = { |
| 594 | 594 | name: 'my-preset', |
| @@ -309,7 +309,7 @@ cli | ||
| 309 | 309 | process.exit(1) |
| 310 | 310 | } |
| 311 | 311 | |
| 312 | const defaultConfig = `import type { TsCssOptions } from 'ts-css' | |
| 312 | const defaultConfig = `import type { TsCssOptions } from '@ts-css/core' | |
| 313 | 313 | |
| 314 | 314 | const config = { |
| 315 | 315 | content: ['./src/**/*.{html,js,ts,jsx,tsx,stx}'], |
| @@ -20,7 +20,7 @@ The plugin works by intercepting HTML imports in your TypeScript/JavaScript code | ||
| 20 | 20 | |
| 21 | 21 | ### Basic Setup```typescript |
| 22 | 22 | |
| 23 | import { plugin } from 'ts-css' | |
| 23 | import { plugin } from '@ts-css/core' | |
| 24 | 24 | |
| 25 | 25 | await Bun.build({ |
| 26 | 26 | entrypoints: ['./src/index.ts'], |
| @@ -36,7 +36,7 @@ import html from './index.html' | ||
| 36 | 36 | document.body.innerHTML = html |
| 37 | 37 | ```### With Custom Configuration```typescript |
| 38 | 38 | |
| 39 | import { plugin } from 'ts-css' | |
| 39 | import { plugin } from '@ts-css/core' | |
| 40 | 40 | |
| 41 | 41 | await Bun.build({ |
| 42 | 42 | entrypoints: ['./src/index.ts'], |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | { |
| 2 | "name": "ts-css", | |
| 2 | "name": "@ts-css/core", | |
| 3 | 3 | "type": "module", |
| 4 | 4 | "version": "0.3.0", |
| 5 | 5 | "description": "A blazingly fast utility-first CSS engine with a StyleX-style typed API. Tailwind-compatible utilities, zero runtime.", |
| @@ -73,6 +73,7 @@ | ||
| 73 | 73 | }, |
| 74 | 74 | "dependencies": { |
| 75 | 75 | "@stacksjs/clapp": "^0.2.0", |
| 76 | "@stacksjs/ts-css": "workspace:*", | |
| 76 | 77 | "bunfig": "^0.15.17" |
| 77 | 78 | }, |
| 78 | 79 | "lint-staged": { |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import type { CSSRule, TsCssConfig, ParsedClass } from './types' |
| 2 | 2 | import type { UtilityRule } from './rules' |
| 3 | import { minifyBlock } from '@stacksjs/ts-css/optimize' | |
| 3 | 4 | import { parseClass } from './parser' |
| 4 | 5 | import { defaultConfig } from './config' |
| 5 | 6 | import { builtInRules } from './rules' |
| @@ -1436,17 +1437,13 @@ const NOT_VARIANT_SELECTORS: Record<string, string> = { | ||
| 1436 | 1437 | // around syntax characters. Used for preflight and keyframes, which were |
| 1437 | 1438 | // previously only whitespace-collapsed (comments and per-declaration |
| 1438 | 1439 | // spacing survived into "minified" output). |
| 1440 | /** | |
| 1441 | * Minification is delegated to the toolkit's AST-based optimiser rather than | |
| 1442 | * squeezed with regexes. The old inline version could not tell a space inside | |
| 1443 | * a string or `url()` from a space between tokens. | |
| 1444 | */ | |
| 1439 | 1445 | function minifyCSSBlock(css: string): string { |
| 1440 | return css | |
| 1441 | .replace(/\/\*[\s\S]*?\*\//g, '') | |
| 1442 | .replace(/\s+/g, ' ') | |
| 1443 | .replace(/\s*\{\s*/g, '{') | |
| 1444 | .replace(/\s*\}\s*/g, '}') | |
| 1445 | .replace(/;\s*/g, ';') | |
| 1446 | .replace(/:\s+/g, ':') | |
| 1447 | .replace(/,\s+/g, ',') | |
| 1448 | .replace(/;\}/g, '}') | |
| 1449 | .trim() | |
| 1446 | return minifyBlock(css).css | |
| 1450 | 1447 | } |
| 1451 | 1448 | |
| 1452 | 1449 | // Separator between stacked at-rule wrappers in a rule-group key |
| @@ -51,7 +51,7 @@ import { renderStyles, resetStyles } from './registry' | ||
| 51 | 51 | * |
| 52 | 52 | * @example |
| 53 | 53 | * ```ts |
| 54 | * import { css } from 'ts-css' | |
| 54 | * import { css } from '@ts-css/core' | |
| 55 | 55 | * |
| 56 | 56 | * const styles = css.create({ card: { padding: 16 } }) |
| 57 | 57 | * <div {...css.props(styles.card)} /> |
| @@ -28,11 +28,11 @@ TAG="${1:-$(git tag --list 'v*' --sort=-v:refname | head -1)}" | ||
| 28 | 28 | VERSION="${TAG#v}" |
| 29 | 29 | |
| 30 | 30 | echo "Repository : $REPO" |
| 31 | echo "Release tag: $TAG (ts-css@$VERSION)" | |
| 31 | echo "Release tag: $TAG (@ts-css/core@$VERSION)" | |
| 32 | 32 | echo |
| 33 | 33 | echo "Paste the npm token, then press Enter. It will not be displayed." |
| 34 | 34 | echo "It needs permission to CREATE packages (Automation, or granular with" |
| 35 | echo "read/write on all packages) — 'ts-css' does not exist on npm yet." | |
| 35 | echo "read/write on all packages) — '@ts-css/core' does not exist on npm yet." | |
| 36 | 36 | printf 'npm token: ' |
| 37 | 37 | read -rs NPM_TOKEN_VALUE |
| 38 | 38 | echo |
| @@ -62,7 +62,7 @@ if [[ "$WHOAMI_STATUS" != "200" ]]; then | ||
| 62 | 62 | echo "npm rejected the token (HTTP $WHOAMI_STATUS)." >&2 |
| 63 | 63 | echo "Create a fresh one at https://www.npmjs.com/settings/~/tokens" >&2 |
| 64 | 64 | echo "It must be an Automation token, or granular with read/write on ALL" >&2 |
| 65 | echo "packages — 'ts-css' does not exist yet, so a token scoped to existing" >&2 | |
| 65 | echo "packages — '@ts-css/core' does not exist yet, so a token scoped to existing" >&2 | |
| 66 | 66 | echo "packages cannot create it." >&2 |
| 67 | 67 | exit 1 |
| 68 | 68 | fi |
| @@ -129,11 +129,11 @@ fi | ||
| 129 | 129 | # publish step has previously reported errors while the run itself succeeded, |
| 130 | 130 | # and the registry is the only authority on whether the version exists. |
| 131 | 131 | echo |
| 132 | echo "Confirming ts-css@$VERSION on the registry ..." | |
| 132 | echo "Confirming @ts-css/core@$VERSION on the registry ..." | |
| 133 | 133 | for attempt in 1 2 3 4 5 6; do |
| 134 | STATUS="$(curl -s -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/ts-css/$VERSION" || echo 000)" | |
| 134 | STATUS="$(curl -s -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/@ts-css%2Fcore/$VERSION" || echo 000)" | |
| 135 | 135 | if [[ "$STATUS" == "200" ]]; then |
| 136 | echo " ts-css@$VERSION is live: https://www.npmjs.com/package/ts-css/v/$VERSION" | |
| 136 | echo " @ts-css/core@$VERSION is live: https://www.npmjs.com/package/@ts-css/core/v/$VERSION" | |
| 137 | 137 | PUBLISHED=1 |
| 138 | 138 | break |
| 139 | 139 | fi |
| @@ -143,7 +143,7 @@ done | ||
| 143 | 143 | |
| 144 | 144 | if [[ "${PUBLISHED:-0}" != "1" ]]; then |
| 145 | 145 | echo >&2 |
| 146 | echo "The workflow finished but ts-css@$VERSION is not on the registry." >&2 | |
| 146 | echo "The workflow finished but @ts-css/core@$VERSION is not on the registry." >&2 | |
| 147 | 147 | echo "Inspect the publish step with:" >&2 |
| 148 | 148 | echo " gh run view $RUN_ID --log --repo $REPO | grep -A5 'Publish to npm'" >&2 |
| 149 | 149 | exit 1 |