diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 5ccc64aa2..000000000 --- a/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["es2015-loose"], - "plugins": ["add-module-exports", "precompile-charcodes"] -} diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 000000000..55d59b674 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,3 @@ +{ + "image": "ghcr.io/ai/devcontainer:latest" +} diff --git a/.editorconfig b/.editorconfig index e2cb9700e..c6c8b3621 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,11 +2,8 @@ root = true [*] indent_style = space -indent_size = 4 +indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true - -[*.{json,yml}] -indent_size = 2 diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index cefc9eeaf..000000000 --- a/.eslintrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "parser": "babel-eslint", - "extends": "eslint-config-postcss", - "rules": { - "consistent-return": [0], - "valid-jsdoc": [2], - "complexity": [0] - }, - "env": { - "mocha": true - } -} diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index fae8897c8..000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* eol=lf diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..0aaa2efe2 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contributing Guide to PostCSS + +If you want to contribute to PostCSS, there are a few things that you should +be familiar with. + +## Adding Your Plugin to the List + +If you created or found a plugin and want to add it to the PostCSS plugins list +follow these simple steps: + +PR should not change plugins defined in README — it only contains favorite plugins +moderated by the PostCSS author. + +Plugins submitted by the community are located in [`docs/plugins`]. + +- **Keep plugins ordered** + + Be sure that a plugin is not already present and find a suitable position + for it in alphabetical order. + However plugins with `postcss-` prefix should come first. + +- **Check spelling** + + Before submitting a PR make sure the spelling check is passing. + To run the check use `npm test`. + If it fails with an unknown word error, add it as a word + to `.yaspellerrc` dictionary. + +- **Check PostCSS plugin guidelines** + + The suggested plugin should match plugin [guidelines]. + +* **Provide link to suggested plugin** + + Make sure your pull request description contains a link to the plugin + you want to add. + +[`docs/plugins`]: https://github.com/postcss/postcss/blob/main/docs/plugins.md +[guidelines]: https://github.com/postcss/postcss/blob/main/docs/guidelines/plugin.md + +## TypeScript Declaration Improvements + +If you found a bug or want to add certain improvements to types declaration file: + +- **Check current TypeScript styling** + + Be sure that your changes match TypeScript styling rules defined in typings file. + - We use classes for existing JS classes like `Stringifier`. + - Namespaces used for separating functions related to the same subject. + - Interfaces used for defining custom types. + + Make sure you read through declaration file writing [best practices] + by the TypeScript team. + +[best practices]: https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html + +## Core Development + +If you want to add new features or fix existing issues + +- **Become familiar with PostCSS architecture** + + For a gentle intro to PostCSS architecture look through our [guide]. + +[guide]: https://github.com/postcss/postcss/blob/main/docs/architecture.md diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..91de44399 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +open_collective: postcss +tidelift: npm/postcss +github: ai diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..7787390f0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Release +on: + push: + tags: + - '*' +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Clean npm package + uses: ai/clean-npm-project@3f664d1a83e04a2be737b7ad4e0b5a4e1b7dbc08 # v0.3.1 + with: + clean-docs: true + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 26 + package-manager-cache: false + - name: Publish npm package + run: npm stage publish + working-directory: cleaned-project/ + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Copy CHANGELOG.md to Releases + uses: ai/copy-changelog-to-release@a6dc825c34575add2da2060796794f7b84894628 # v0.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..743c0db40 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,104 @@ +name: Test +on: + push: + branches: + - main + pull_request: +permissions: + contents: read +jobs: + full: + name: Node.js Latest Full + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install Node.js & pnpm + uses: pnpm/setup@84cb39b217b10273981911c288cd62326dc7c6d2 # v2.0.2 + with: + version: 11 + runtime: node@26 + - name: Install dependencies + run: pnpm ci + - name: Run tests + run: pnpm test + short: + runs-on: ubuntu-latest + strategy: + matrix: + node: + - 24 + - 22 + name: Node.js ${{ matrix.node }} Quick + steps: + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install Node.js & pnpm + uses: pnpm/setup@84cb39b217b10273981911c288cd62326dc7c6d2 # v2.0.2 + with: + version: 11 + runtime: node@${{ matrix.node }} + - name: Install dependencies + run: pnpm ci + - name: Install dependencies + run: pnpm ci --ignore-scripts + - name: Run unit tests + run: pnpm run unit + old: + runs-on: ubuntu-latest + strategy: + matrix: + node: + - 20 + - 18 + - 16 + - 14 + - 12 + - 10 + name: Node.js ${{ matrix.node }} Quick + steps: + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: 3 + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + - name: Install Node.js ${{ matrix.node }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ matrix.node }} + - name: Install dependencies + run: pnpm install --ignore-scripts + - name: Downgrade TypeScript + run: pnpm install typescript@4 --ignore-scripts + - name: Run unit tests + run: pnpm run old + windows: + runs-on: windows-latest + name: Windows Quick + steps: + - name: Checkout the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pnpm + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + with: + version: 11 + - name: Install Node.js LTS + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 26 + cache: pnpm + - name: Install dependencies + run: pnpm install --ignore-scripts + - name: Run unit tests + run: pnpm run unit diff --git a/.gitignore b/.gitignore index f05e09e41..3030054ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,6 @@ -.DS_Store -*~ - node_modules/ -npm-debug.log -build/ -lib/*.js -/postcss.js +coverage/ -api/ +docs/api/index.html +docs/api/assets/ diff --git a/.npmignore b/.npmignore index 42a3f7dfa..9439f8b9d 100644 --- a/.npmignore +++ b/.npmignore @@ -1,15 +1,7 @@ -.gitignore - -node_modules/ -npm-debug.log - -build/ -/postcss.js - +coverage/ test/ -.travis.yml -appveyor.yml - -gulpfile.babel.js -api/ +docs/ +tsconfig.json +eslint.config.mjs +pnpm-workspace.yaml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2e7fd109f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - "6" - - "4" - - "0.12" diff --git a/.yaspellerrc b/.yaspellerrc deleted file mode 100644 index 7657d068a..000000000 --- a/.yaspellerrc +++ /dev/null @@ -1,139 +0,0 @@ -{ - "lang": "en", - "format": "markdown", - "fileExtensions": [".md"], - "excludeFiles": [".git", "node_modules", "build"], - "ignoreCapitalization": true, - "dictionary": [ - "6to5", - "ASE", - "AtCSS", - "Autoprefixer", - "Base64", - "cssnext", - "cssnano", - "Browserhacks", - "CoffeeScript", - "GitHub", - "JetBrains", - "WebStorm", - "Traceur", - "PostCSS", - "PostCSS’s", - "postcss", - "Shopify", - "Instagram", - "Travis", - "poststylus", - "Gofmt", - "CI", - "MD5", - "W3C", - "WebP", - "VK", - "IE8", - "IE9", - "ES6", - "ENB", - "SemVer", - "Sass", - "Sass’s", - "SCSS", - "Weibo", - "PreCSS", - "libsass", - "CSSOM", - "Jeet", - "Modernizr", - "npm", - "webpack", - "Amdusias", - "Anton", - "Andres", - "Andromalius", - "Andrealphus", - "Andalusian", - "Andras", - "asan", - "Lind", - "Zuo", - "Jed", - "Neal", - "Valac", - "Briggs", - "Bogdan", - "Chadkin", - "ClojureWerkz’s", - "Dantalion", - "Decarabia", - "Dvornov", - "Flauros", - "Josiah", - "Lydell", - "Lolspeak", - "Matija", - "Marohnić", - "Maxime", - "Mohammad", - "Nikitenko", - "Savary", - "Seere", - "Suarez", - "Thirouin", - "Peterson", - "Yakushev", - "Younes", - "Cimeies", - "Autodetect", - "autodetect", - "codebases", - "flexbox", - "unprefixes", - "regexp", - "changelog", - "Changelog", - "Transpiler", - "transpiler", - "transpile", - "transpiles", - "transpiling", - "tokenize", - "tokenizer", - "linter", - "linters", - "rebases", - "resolver", - "minifier", - "isolatable", - "minifiers", - "mixins", - "mixin", - "multitool", - "io", - "partials", - "inlined", - "inlines", - "polyfill", - "stylesheet", - "stylesheets", - "stringifier", - "keyframes", - "BEM", - "CLI", - "CSS3", - "CSS4", - "SugarSS", - "SVG", - "SVGO", - "JS", - "js", - "vs", - "pantone", - "YIQ", - "gitter", - "evilmartians", - "Less’s", - "visualizer", - "Rollup" - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2a3aa7f..45510c7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,391 +1,1441 @@ # Change Log -All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](http://semver.org/). + +This project adheres to [Semantic Versioning](https://semver.org/). + +## 8.5.26 + +- Fixed `list.split()` regression (by @lazerg). +- Track symlinks in path protection in source map loading (by @drengir1). + +## 8.5.25 + +- Fixed 8.5.17 visitor regression. +- Fixed `list.split()` for non-string values (by @amir-rezaei). + +## 8.5.24 + +- Preserve the BOM after the processing (by @hdimer). + +## 8.5.23 + +- Do not load source map without `opts.from` for security reasons. + +## 8.5.22 + +- Fixed custom property losing semicolon before a comment (by @sarathfrancis90). + +## 8.5.21 + +- Fixed childless at-rule losing semicolon before comment (by @sarathfrancis90). +- Fixed docs (by @isker). + +## 8.5.20 + +- Fixed missing space if `AtRule#params` is set after (by @sarathfrancis90). +- Fixed mixing AST error on warnings (by @MahinAnowar). + +## 8.5.19 + +- Fixed cleaning `before` for new nodes inserted to `Root` (by @MahinAnowar). + +## 8.5.18 + +- Restricted loading previous source maps file to the `opts.from` folder for security reasons (use `unsafeMap: true` to disable the check). + +## 8.5.17 + +- Fixed `Maximum call stack size exceeded` error. +- Fixed Prototype hijacking for `postcss.fromJSON()`. +- Fixed `Input#origin()` for unmapped end position (by @chatman-media). + +## 8.5.16 + +- Fixed `Input#origin()` position (by @mizdra). +- Fixed `raws` after rehydrating a JSON AST (by @sarathfrancis90). +- Fixed putting parent-less node in `nodes` of new node (by @MahinAnowar). +- Fixed computing `offset` in `positionBy()` (by @greymoth-jp). +- Fixed `rangeBy()` on `index: 0` (by @sarathfrancis90). + +## 8.5.15 + +- Fixed declaration parsing performance (by @homanp). + +## 8.5.14 + +- Fixed custom syntax regression (by @43081j). + +## 8.5.13 + +- Fixed `postcss-scss` commend regression. + +## 8.5.12 + +- Fixed reading any file via user-generated CSS. +- Added `opts.unsafeMap` to disable checks. + +## 8.5.11 + +- Fixed nested brackets parsing performance (by @offset). + +## 8.5.10 + +- Fixed XSS via unescaped `` in non-bundler cases (by @TharVid). + +## 8.5.9 + +- Speed up source map encoding paring in case of the error. + +## 8.5.8 + +- Fixed `Processor#version`. + +## 8.5.7 + +- Improved source map annotation cleaning performance (by CodeAnt AI). + +## 8.5.6 + +- Fixed `ContainerWithChildren` type discriminating (by @Goodwine). + +## 8.5.5 + +- Fixed `package.json`→`exports` compatibility with some tools (by @JounQin). + +## 8.5.4 + +- Fixed Parcel compatibility issue (by @git-sumitchaudhary). + +## 8.5.3 + +- Added more details to `Unknown word` error (by @hiepxanh). +- Fixed types (by @romainmenke). +- Fixed docs (by @catnipan). + +## 8.5.2 + +- Fixed end position of rules with semicolon (by @romainmenke). + +## 8.5.1 + +- Fixed backwards compatibility for complex cases (by @romainmenke). + +## 8.5 “Duke Alloces” + +- Added `Input#document` for sources like CSS-in-JS or HTML (by @romainmenke). + +## 8.4.49 + +- Fixed custom syntax without `source.offset` (by @romainmenke). + +## 8.4.48 + +- Fixed position calculation in error/warnings methods (by @romainmenke). + +## 8.4.47 + +- Removed debug code. + +## 8.4.46 + +- Fixed `Cannot read properties of undefined (reading 'before')`. + +## 8.4.45 + +- Removed unnecessary fix which could lead to infinite loop. + +## 8.4.44 + +- Another way to fix `markClean is not a function` error. + +## 8.4.43 + +- Fixed `markClean is not a function` error. + +## 8.4.42 + +- Fixed CSS syntax error on long minified files (by @varpstar). + +## 8.4.41 + +- Fixed types (by @nex3 and @querkmachine). +- Cleaned up RegExps (by @bluwy). + +## 8.4.40 + +- Moved to getter/setter in nodes types to help Sass team (by @nex3). + +## 8.4.39 + +- Fixed `CssSyntaxError` types (by @romainmenke). + +## 8.4.38 + +- Fixed `endIndex: 0` in errors and warnings (by @romainmenke). + +## 8.4.37 + +- Fixed `original.column are not numbers` error in another case. + +## 8.4.36 + +- Fixed `original.column are not numbers` error on broken previous source map. + +## 8.4.35 + +- Avoid `!` in `node.parent.nodes` type. +- Allow to pass `undefined` to node adding method to simplify types. + +## 8.4.34 + +- Fixed `AtRule#nodes` type (by Tim Weißenfels). +- Cleaned up code (by Dmitry Kirillov). + +## 8.4.33 + +- Fixed `NoWorkResult` behavior difference with normal mode (by Romain Menke). +- Fixed `NoWorkResult` usage conditions (by @ahmdammarr). + +## 8.4.32 + +- Fixed `postcss().process()` types (by Andrew Ferreira). + +## 8.4.31 + +- Fixed `\r` parsing to fix CVE-2023-44270. + +## 8.4.30 + +- Improved source map performance (by Romain Menke). + +## 8.4.29 + +- Fixed `Node#source.offset` (by Ido Rosenthal). +- Fixed docs (by Christian Oliff). + +## 8.4.28 + +- Fixed `Root.source.end` for better source map (by Romain Menke). +- Fixed `Result.root` types when `process()` has no parser. + +## 8.4.27 + +- Fixed `Container` clone methods types. + +## 8.4.26 + +- Fixed clone methods types. + +## 8.4.25 + +- Improve stringify performance (by Romain Menke). +- Fixed docs (by @vikaskaliramna07). + +## 8.4.24 + +- Fixed `Plugin` types. + +## 8.4.23 + +- Fixed warnings in TypeDoc. + +## 8.4.22 + +- Fixed TypeScript support with `node16` (by Remco Haszing). + +## 8.4.21 + +- Fixed `Input#error` types (by Aleks Hudochenkov). + +## 8.4.20 + +- Fixed source map generation for childless at-rules like `@layer`. + +## 8.4.19 + +- Fixed whitespace preserving after AST transformations (by Romain Menke). + +## 8.4.18 + +- Fixed an error on `absolute: true` with empty `sourceContent` (by Rene Haas). + +## 8.4.17 + +- Fixed `Node.before()` unexpected behavior (by Romain Menke). +- Added TOC to docs (by Mikhail Dedov). + +## 8.4.16 + +- Fixed `Root` AST migration. + +## 8.4.15 + +- Fixed AST normalization after using custom parser with old PostCSS AST. + +## 8.4.14 + +- Print “old plugin API” warning only if plugin was used (by @zardoy). + +## 8.4.13 + +- Fixed `append()` error after using `.parent` (by Jordan Pittman). + +## 8.4.12 + +- Fixed `package.funding` to have same value between all PostCSS packages. + +## 8.4.11 + +- Fixed `Declaration#raws.value` type. + +## 8.4.10 + +- Fixed `package.funding` URL format. + +## 8.4.9 + +- Fixed `package.funding` (by Álvaro Mondéjar). + +## 8.4.8 + +- Fixed end position in empty Custom Properties. + +## 8.4.7 + +- Fixed `Node#warn()` type (by Masafumi Koba). +- Fixed comment removal in values after `,`. + +## 8.4.6 + +- Prevented comment removing when it change meaning of CSS. +- Fixed parsing space in last semicolon-less CSS Custom Properties. +- Fixed comment cleaning in CSS Custom Properties with space. +- Fixed throwing an error on `.root` access for plugin-less case. + +## 8.4.5 + +- Fixed `raws` types to make object extendable (by James Garbutt). +- Moved from Yarn 1 to pnpm. + +## 8.4.4 + +- Fixed absolute path in source map on zero plugins mode. + +## 8.4.3 + +- Fixed `this.css.replace is not a function` error. + +## 8.4.2 + +- Fixed previous source map support in zero plugins mode. + +## 8.4.1 + +- Fixed `Stringifier` types (by James Garbutt). + +## 8.4 “President Camio” + +- Added ranges for errors and warnings (by Adaline Valentina Simonian). +- Added `Stringifier` types (by James Garbutt). +- Added `Processor` types. +- Removed `PostCSS does nothing` warning by lazy parser (Bogdan Dolin). +- Fixed `Node#walkRules()` types (by Aleks Hudochenkov). +- Fixed types `Root` and `Document` in result values (by James Garbutt). +- Reduced npm install size by 0.5 MB. +- Moved tests from Jest to `uvu` (by Andrey Kim). +- Fixed docs (by Paul Shryock). + +## 8.3.11 + +- Remove debugging code. + +## 8.3.10 + +- Fixed `Maximum call stack` issue of some source maps (by Yeting Li). + +## 8.3.9 + +- Replaced `nanocolors` to `picocolors`. +- Reduced package size. + +## 8.3.8 + +- Update `nanocolors`. + +## 8.3.7 + +- Replaced `colorette` to `nanocolors`. +- Added bug field to `package.json` (by Christian Oliff). +- Improved docs (by Andrew Bruce and Paul Shryock). + +## 8.3.6 + +- Fixed column in `missed semicolon` error (by @Gusted). + +## 8.3.5 + +- Fixed broken AST detection. + +## 8.3.4 + +- Fixed broken AST detection. + +## 8.3.3 + +- Fixed broken AST on `postcss` dependency duplication in custom parsers. + +## 8.3.2 + +- Update changelog. + +## 8.3.1 + +- Fixed false positives `PostCSS does nothing` warning on `syntax` option. + +## 8.3 “Duke Murmur” + +- Added `Node#assign()` shortcut (by Jonathan Neal). +- Added experimental `Document` node to AST (by Aleks Hudochenkov). +- Moved to faster fork of `source-map` (by Valentin Semirulnik). + +## 8.2.15 + +- Fixed `list` type definitions (by @n19htz). + +## 8.2.14 + +- Removed `source-map` from client-side bundle (by Barak Igal). + +## 8.2.13 + +- Fixed ReDoS vulnerabilities in source map parsing (by Yeting Li). + +## 8.2.12 + +- Fixed `package.json` exports. + +## 8.2.11 + +- Fixed `DEP0148` warning in Node.js 16. +- Fixed docs (by @semiromid). + +## 8.2.10 + +- Fixed ReDoS vulnerabilities in source map parsing. +- Fixed webpack 5 support (by Barak Igal). +- Fixed docs (by Roeland Moors). + +## 8.2.9 + +- Exported `NodeErrorOptions` type (by Rouven Weßling). + +## 8.2.8 + +- Fixed browser builds in webpack 4 (by Matt Jones). + +## 8.2.7 + +- Fixed browser builds in webpack 5 (by Matt Jones). + +## 8.2.6 + +- Fixed `Maximum call stack size exceeded` in `Node#toJSON`. +- Fixed docs (by inokawa). + +## 8.2.5 + +- Fixed escaped characters handling in `list.split` (by Natalie Weizenbaum). + +## 8.2.4 + +- Added plugin name to `postcss.plugin()` warning (by Tom Williams). +- Fixed docs (by Bill Columbia). + +## 8.2.3 + +- Fixed `JSON.stringify(Node[])` support (by Niklas Mischkulnig). + +## 8.2.2 + +- Fixed CSS-in-JS support (by James Garbutt). +- Fixed plugin types (by Ludovico Fischer). +- Fixed `Result#warn()` types. + +## 8.2.1 + +- Fixed `Node#toJSON()` and `postcss.fromJSON()` (by Niklas Mischkulnig). + +## 8.2 “Prince Orobas” + +- Added `Node#toJSON()` and `postcss.fromJSON()` (by Niklas Mischkulnig). + +## 8.1.14 + +- Fixed parser performance regression. + +## 8.1.13 + +- Fixed broken AST after moving nodes in visitor API. + +## 8.1.12 + +- Fixed Autoprefixer regression. + +## 8.1.11 + +- Added PostCSS update suggestion on unknown event in plugin. + +## 8.1.10 + +- Fixed `LazyResult` type export (by Evan You). +- Fixed `LazyResult` type compatibility with `Promise` (by Anton Kastritskiy). + +## 8.1.9 + +- Reduced dependencies number (by Bogdan Chadkin). + +## 8.1.8 + +- Fixed `LazyResult` type compatibility with `Promise` (by Ludovico Fischer). +- Fixed HTTPS links in documentation. + +## 8.1.7 + +- Fixed `import` support in TypeScript (by Remco Haszing). + +## 8.1.6 + +- Reverted `package.exports` Node.js 15 fix. + +## 8.1.5 + +- Fixed Node.js 15 warning (by 沈鸿飞). + +## 8.1.4 + +- Fixed TypeScript definition (by Arthur Petrie). + +## 8.1.3 + +- Added `package.types`. + +## 8.1.2 + +- Fixed API docs (by Arthur Petrie). +- Improved plugin guide (by Yunus Gaziev). +- Prepared code base for Deno support (by Oscar Otero). + +## 8.1.1 + +- Updated funding link. + +## 8.1 “Duke Gemory” + +- Added `Once` and `OnceExit` events. +- Fixed `Root` and `RootExit` events re-visiting. +- Fixed node re-visiting on deep children changes. +- Added docs for visitor API events. + +## 8.0.9 + +- Replace prototype in PostCSS 7 nodes instead of recreating them. +- Added missed `Transformer` to exported types (by Pierre-Marie Dartus). + +## 8.0.8 + +- Fix `8.0.7` regression on PostCSS 7 nodes converting (by Adam Wathan). + +## 8.0.7 + +- Fixed compatibility issue with mixin AST with PostCSS 7 and 8 nodes. +- Added migration guide translation to Chinese to the warning. + +## 8.0.6 + +- Fixed child adding methods in `Container`. + +## 8.0.5 + +- Update changelog. + +## 8.0.4 + +- Fixed `Cannot read property 'line' of null` error. +- Fixed source map support for declarations. + +## 8.0.3 + +- Fixed client-side bundling support. + +## 8.0.2 + +- Fixed plugin packs support. + +## 8.0.1 + +- Updated `Processor#version`. + +## 8.0 “President Ose” + +- Removed support for Node.js 6.x, 8.x, 11.x, and 13.x versions. +- Removed `postcss.vendor` helpers. +- Deprecated `postcss.plugin()` API. +- Treats `sourceMap.sources` as URL instead of file path. +- Plugins and runners must have `postcss` in `peerDependencies`. +- Prohibited to extend PostCSS AST classes. +- Moved from JSDoc to TypeDoc. +- Moved unknown source from counter to random IDs. +- Added visitor API for plugins (by Alexey Bondarenko). +- Added ES modules support. +- Added named exports for public classes `const { Rule } = require('postcss)`. +- Added `position.url` to `Node#origin()` result. +- Added `opts.maps.absolute = true` option. +- Added `opts.maps.annotation = (file, root) => url` option support. +- Added `Node#source.offset` (by Ayaz Zaynutdinov). +- Added `Declaration#variable`. +- Added JSON source map support. +- Added index source map support. +- Added `Declaration#value` auto-converting to string. +- Fixed parsing `{}` in at-rule parameters. +- Fixed parsing empty Custom Properties. `--foo: ;` will have ` ` value. +- Fixed building PostCSS with Rollup (by MapGrid). +- Fixed TypeScript types. +- Fixed source map relative paths. +- Fixed calling `replaceWith` with input replaced node (by Joseph Kaptur). +- Improved “Writing a PostCSS Plugin” docs (by Alexey Bondarenko). +- Removed Babel from the project’s release process. +- Removed docs from npm package. +- Replaced `chalk` to `colorette`. + +## 7.0.38 + +- Update `Processor#version`. + +## 7.0.37 + +- Backport `chalk` to `nanocolors` migration. + +## 7.0.36 + +- Backport ReDoS vulnerabilities from PostCSS 8. + +## 7.0.35 + +- Add migration guide link to PostCSS 8 error text. + +## 7.0.34 + +- Fix compatibility with `postcss-scss` 2. + +## 7.0.33 + +- Add error message for PostCSS 8 plugins. + +## 7.0.32 + +- Fix error message (by @admosity). + +## 7.0.31 + +- Use only the latest source map annotation (by Emmanouil Zoumpoulakis). + +## 7.0.30 + +- Fix TypeScript definition (by Natalie Weizenbaum). + +## 7.0.29 + +- Update `Processor#version`. + +## 7.0.28 + +- Fix TypeScript definition (by Natalie Weizenbaum). + +## 7.0.27 + +- Fix TypeScript definition (by Natalie Weizenbaum). + +## 7.0.26 + +- Fix TypeScript definition (by Natalie Weizenbaum). + +## 7.0.25 + +- Fix absolute path support for Windows (by Tom Raviv). + +## 7.0.24 + +- Fix TypeScript definition (by Keith Cirkel). + +## 7.0.23 + +- Update `Processor#version`. + +## 7.0.22 + +- Add funding link for `npm fund`. + +## 7.0.21 + +- Revert passing `nodes` property to node constructor. + +## 7.0.20 + +- Allow to pass PostCSS’s nodes in `nodes` property to node constructor. + +## 7.0.19 + +- Fix passing `nodes` property to node constructor. + +## 7.0.18 + +- Fix TypeScript type definitions (by Jan Buschtöns). + +## 7.0.17 + +- Fix TypeScript type definitions (by Bob Matcuk and Jan Buschtöns). + +## 7.0.16 + +- Revert Custom Properties fix until PostCSS 8.0. + +## 7.0.15 + +- Fix Custom Properties support (by Ivan Solovev). + +## 7.0.14 + +- Fix tokenizer for `postcss-less` (by Matt Lyons). + +## 7.0.13 + +- Fix parsing regression in 7.0.12 for comments between property and value. + +## 7.0.12 + +- Fix parsing broken CSS with two words in declaration property. + +## 7.0.11 + +- Fix source maps on declaration semicolon (by Niklas Mischkulnig). + +## 7.0.10 + +- Fix source maps (by Niklas Mischkulnig). + +## 7.0.9 + +- Increase stringifing performance for non-raws AST. + +## 7.0.8 + +- Fix TypeScript definitions (by Ankur Oberoi). +- Use `support-colors` 6.0. + +## 7.0.7 + +- Extend `Error` in `CssSyntaxError`. + +## 7.0.6 + +- Fix parsing files with BOM (by Veniamin Krol). + +## 7.0.5 + +- Reduce npm package size (by Gilad Peleg). + +## 7.0.4 + +- Fix safe parser regression. + +## 7.0.3 + +- Fix tokenizer extendability (by Andrew Powell). +- Reduce npm package size. + +## 7.0.2 + +- Fix warning text (by Rui Pedro M Lima). + +## 7.0.1 + +- Fix JSDoc (by Steven Lambert). + +## 7.0 “President Amy” + +- Remove Node.js 9 and Node.js 4 support. +- Remove IE and “dead” browsers support for client-side Babel transpiling. +- Add CSS position on error happened inside `walk()` (by Nikhil Gaba). +- Add `LazyResult#finally` (by Igor Kamyshev). +- Add warning on calling PostCSS without plugins and syntax options. +- Reduce client-side size. + +## 6.0.23 + +- Fix parsing nested at-rules without semicolon, params, and spaces. +- Fix docs (by Kevin Schiffer and Pat Cavit). + +## 6.0.22 + +- Fix `Node#prev` and `Node#next` on missed parent. + +## 6.0.21 + +- Rename Chinese docs to fix `yarnpkg.com` issue. + +## 6.0.20 + +- Better error message on `null` as input CSS. + +## 6.0.19 + +- Fix TypeScript definitions for source maps (by Oleh Kuchuk). +- Fix `source` field in TypeScript definitions (by Sylvain Pollet-Villard). + +## 6.0.18 + +- Use primitive object in TypeScript definitions (by Sylvain Pollet-Villard). + +## 6.0.17 + +- Fix parsing comment in selector between word tokens (by Oleh Kuchuk). + +## 6.0.16 + +- Fix warning text (by Michael Keller). + +## 6.0.15 + +- Add warning about missed `from` option on `process().then()` call. +- Add IE 10 support. + +## 6.0.14 + +- Fix TypeScript definitions (by Jed Mao). + +## 6.0.13 + +- Fix TypeScript definitions for case of multiple PostCSS versions + in `node_modules` (by Chris Eppstein). +- Use `source-map` 0.6. + +## 6.0.12 + +- Don’t copy `*` hack to declaration indent. + +## 6.0.11 + +- Add upper case `!IMPORTANT` support. + +## 6.0.10 + +- Reduce PostCSS size in webpack bundle. + +## 6.0.9 + +- Improve error message for plugin with old PostCSS (by Igor Adamenko). + +## 6.0.8 + +- Fix Node.js 4.2.2 support. + +## 6.0.7 + +- Fix base64 decoding for old Node.js and browser. + +## 6.0.6 + +- Fix `end` position in at-rule without semicolon (by Oleh Kuchuk). + +## 6.0.5 + +- Move Babel config from `package.json` for `node_modules` compiling cases. + +## 6.0.4 + +- Fix parsing `;;` after rules. +- Use Chalk 2.0. + +## 6.0.3 + +- Fix escape sequences parsing (by Oleh Kuchuk). +- Added ability to force disable colors with an environment variable. +- Improved color detection of some terminal apps. + +## 6.0.2 + +- Keep `raws.before` on moving `Root` children to new `Root`. + +## 6.0.1 + +- Fix parser extensibility to use it in Safe Parser. + +## 6.0 “Marquis Orias” + +- Remove node.js 0.12 support. +- Remove deprecated method from PostCSS 4. +- Insert methods remove child from previous parent, instead of closing. +- Insert methods and cloning doesn’t clean `raws` anymore. +- Methods `moveTo`, `moveAfter`, `moveBefore` were deprecated. +- Options was changed in `Plugin#process(css, processOptions, pluginOptions)`. +- Add stream parser to reduce memory usage (by Oleh Kuchuk). +- Add `before()`/`after()` shortcuts for `node.parent.insertBefore(node, x)`. +- Add `Rule#raws.ownSemicolon` for semicolon after templates for `@apply`. +- Use `babel-preset-env` to compile npm package. +- Remove `js-base64` from dependencies (by Roman Dvornov). +- Fix error message on single `:` in CSS. +- Move tests to Jest. +- Clean up test (by Gabriel Kalani). + +## 5.2.18 + +- Fix TypeScript definitions for case of multiple PostCSS versions + in `node_modules` (by Chris Eppstein). + +## 5.2.17 + +- Add `postcss-sass` suggestion to syntax error on `.sass` input. + +## 5.2.16 + +- Better error on wrong argument in node constructor. + +## 5.2.15 + +- Fix TypeScript definitions (by bumbleblym). + +## 5.2.14 + +- Fix browser bundle building in webpack (by janschoenherr). + +## 5.2.13 + +- Do not add comment to important raws. +- Fix JSDoc (by Dmitry Semigradsky). + +## 5.2.12 + +- Fix typo in deprecation message (by Garet McKinley). + +## 5.2.11 + +- Fix TypeScript definitions (by Jed Mao). + +## 5.2.10 + +- Fix TypeScript definitions (by Jed Mao). + +## 5.2.9 + +- Update TypeScript definitions (by Jed Mao). + +## 5.2.8 + +- Fix error message (by Ben Briggs). + +## 5.2.7 + +- Better error message on syntax object in plugins list. + +## 5.2.6 + +- Fix `postcss.vendor` for values with spaces (by 刘祺). + +## 5.2.5 + +- Better error message on unclosed string (by Ben Briggs). + +## 5.2.4 + +- Improve terminal CSS syntax highlight (by Simon Lydell). + +## 5.2.3 + +- Better color highlight in syntax error code frame. +- Fix color highlight support in old systems. + +## 5.2.2 + +- Update `Processor#version`. + +## 5.2.1 + +- Fix source map path for CSS without `from` option (by Michele Locati). + +## 5.2 “Duke Vapula” + +- Add syntax highlight to code frame in syntax error (by Andrey Popp). +- Use Babel code frame style and size in syntax error. +- Add `[` and `]` tokens to parse `[attr=;] {}` correctly. +- Add `ignoreErrors` options to tokenizer (by Andrey Popp). +- Fix error position on tab indent (by Simon Lydell). + +## 5.1.2 + +- Suggests SCSS/Less parsers on parse errors depends on file extension. + +## 5.1.1 + +- Fix TypeScript definitions (by Efremov Alexey). ## 5.1 “King and President Zagan” -* Add URI in source map support (by Mark Finger). -* Add `map.from` option (by Mark Finger). -* Add `` mappings for nodes without source (by Bogdan Chadkin). -* Add function value support to `map.prev` option (by Chris Montoro). -* Add declaration value type check in shortcut creating (by 刘祺). -* `Result#warn` now returns new created warning. -* Don’t call plugin creator in `postcss.plugin` call. -* Add source maps to PostCSS ES5 build. -* Add JSDoc to PostCSS classes. -* Clean npm package from unnecessary docs. + +- Add URI in source map support (by Mark Finger). +- Add `map.from` option (by Mark Finger). +- Add `` mappings for nodes without source (by Bogdan Chadkin). +- Add function value support to `map.prev` option (by Chris Montoro). +- Add declaration value type check in shortcut creating (by 刘祺). +- `Result#warn` now returns new created warning. +- Don’t call plugin creator in `postcss.plugin` call. +- Add source maps to PostCSS ES5 build. +- Add JSDoc to PostCSS classes. +- Clean npm package from unnecessary docs. ## 5.0.21 -* Fix support with input source mao with `utf8` encoding name. + +- Fix support with input source mao with `utf8` encoding name. ## 5.0.20 -* Fix between raw value parsing (by David Clark). -* Update TypeScript definitions (by Jed Mao). -* Clean fake node.source after `append(string)`. + +- Fix between raw value parsing (by David Clark). +- Update TypeScript definitions (by Jed Mao). +- Clean fake node.source after `append(string)`. ## 5.0.19 -* Fix indent-based syntaxes support. + +- Fix indent-based syntaxes support. ## 5.0.18 -* Parse new lines according W3C CSS syntax specification. + +- Parse new lines according W3C CSS syntax specification. ## 5.0.17 -* Fix options argument in `Node#warn` (by Ben Briggs). -* Fix TypeScript definitions (by Jed Mao). + +- Fix options argument in `Node#warn` (by Ben Briggs). +- Fix TypeScript definitions (by Jed Mao). ## 5.0.16 -* Fix CSS syntax error position on unclosed quotes. + +- Fix CSS syntax error position on unclosed quotes. ## 5.0.15 -* Fix `Node#clone()` on `null` value somewhere in node. + +- Fix `Node#clone()` on `null` value somewhere in node. ## 5.0.14 -* Allow to use PostCSS in webpack bundle without JSON loader. + +- Allow to use PostCSS in webpack bundle without JSON loader. ## 5.0.13 -* Fix `index` and `word` options in `Warning#toString` (by Bogdan Chadkin). -* Fix input source content loading in errors. -* Fix map options on using `LazyResult` as input CSS. -* 100% test coverage. -* Use Babel 6. + +- Fix `index` and `word` options in `Warning#toString` (by Bogdan Chadkin). +- Fix input source content loading in errors. +- Fix map options on using `LazyResult` as input CSS. +- 100% test coverage. +- Use Babel 6. ## 5.0.12 -* Allow passing a previous map with no mappings (by Andreas Lind). + +- Allow passing a previous map with no mappings (by Andreas Lind). ## 5.0.11 -* Increase plugins performance by 1.5 times. + +- Increase plugins performance by 1.5 times. ## 5.0.10 -* Fix warning from nodes without source. + +- Fix warning from nodes without source. ## 5.0.9 -* Fix source map type detection (by @asan). + +- Fix source map type detection (by @asan). ## 5.0.8 -* Fixed a missed step in `5.0.7` that caused the module to be published as + +- Fixed a missed step in `5.0.7` that caused the module to be published as ES6 code. ## 5.0.7 -* PostCSS now requires that node 0.12 is installed via the engines property + +- PostCSS now requires that node 0.12 is installed via the engines property in package.json (by Howard Zuo). ## 5.0.6 -* Fix parsing nested at-rule without semicolon (by Matt Drake). -* Trim `Declaration#value` (by Bogdan Chadkin). + +- Fix parsing nested at-rule without semicolon (by Matt Drake). +- Trim `Declaration#value` (by Bogdan Chadkin). ## 5.0.5 -* Fix multi-tokens property parsing (by Matt Drake). + +- Fix multi-tokens property parsing (by Matt Drake). ## 5.0.4 -* Fix start position in `Root#source`. -* Fix source map annotation, when CSS uses `\r\n` (by Mohammad Younes). + +- Fix start position in `Root#source`. +- Fix source map annotation, when CSS uses `\r\n` (by Mohammad Younes). ## 5.0.3 -* Fix `url()` parsing. -* Fix using `selectors` in `Rule` constructor. -* Add start source to `Root` node. + +- Fix `url()` parsing. +- Fix using `selectors` in `Rule` constructor. +- Add start source to `Root` node. ## 5.0.2 -* Fix `remove(index)` to be compatible with 4.x plugin. + +- Fix `remove(index)` to be compatible with 4.x plugin. ## 5.0.1 -* Fix PostCSS 4.x plugins compatibility. -* Fix type definition loading (by Jed Mao). + +- Fix PostCSS 4.x plugins compatibility. +- Fix type definition loading (by Jed Mao). ## 5.0 “President Valac” -* Remove `safe` option. Move Safe Parser to separate project. -* `Node#toString` does not include `before` for root nodes. -* Remove plugin returning `Root` API. -* Remove Promise polyfill for node.js 0.10. -* Deprecate `eachInside`, `eachDecl`, `eachRule`, `eachAtRule` and `eachComment` + +- Remove `safe` option. Move Safe Parser to separate project. +- `Node#toString` does not include `before` for root nodes. +- Remove plugin returning `Root` API. +- Remove Promise polyfill for node.js 0.10. +- Deprecate `eachInside`, `eachDecl`, `eachRule`, `eachAtRule` and `eachComment` in favor of `walk`, `walkDecls`, `walkRules`, `walkAtRules` and `walkComments` (by Jed Mao). -* Deprecate `Container#remove` and `Node#removeSelf` +- Deprecate `Container#remove` and `Node#removeSelf` in favor of `Container#removeChild` and `Node#remove` (by Ben Briggs). -* Deprecate `Node#replace` in favor of `replaceWith` (by Ben Briggs). -* Deprecate raw properties in favor of `Node#raws` object. -* Deprecate `Node#style` in favor of `raw`. -* Deprecate `CssSyntaxError#generated` in favor of `input`. -* Deprecate `Node#cleanStyles` in favor of `cleanRaws`. -* Deprecate `Root#prevMap` in favor of `Root.source.input.map`. -* Add `syntax`, `parser` and `stringifier` options for Custom Syntaxes. -* Add stringifier option to `Node#toString`. -* Add `Result#content` alias for non-CSS syntaxes. -* Add `plugin.process(css)` shortcut to every plugin function (by Ben Briggs). -* Add multiple nodes support to insert methods (by Jonathan Neal). -* Add `Node#warn` shortcut (by Ben Briggs). -* Add `word` and `index` options to errors and warnings (by David Clark). -* Add `line`, `column` properties to `Warning`. -* Use `supports-color` library to detect color support in error output. -* Add type definitions for TypeScript plugin developers (by Jed Mao). -* `Rule#selectors` setter detects separators. -* Add `postcss.stringify` method. -* Throw descriptive errors for incorrectly formatted plugins. -* Add docs to npm release. -* Fix `url()` parsing. -* Fix Windows support (by Jed Mao). +- Deprecate `Node#replace` in favor of `replaceWith` (by Ben Briggs). +- Deprecate raw properties in favor of `Node#raws` object. +- Deprecate `Node#style` in favor of `raw`. +- Deprecate `CssSyntaxError#generated` in favor of `input`. +- Deprecate `Node#cleanStyles` in favor of `cleanRaws`. +- Deprecate `Root#prevMap` in favor of `Root.source.input.map`. +- Add `syntax`, `parser` and `stringifier` options for Custom Syntaxes. +- Add stringifier option to `Node#toString`. +- Add `Result#content` alias for non-CSS syntaxes. +- Add `plugin.process(css)` shortcut to every plugin function (by Ben Briggs). +- Add multiple nodes support to insert methods (by Jonathan Neal). +- Add `Node#warn` shortcut (by Ben Briggs). +- Add `word` and `index` options to errors and warnings (by David Clark). +- Add `line`, `column` properties to `Warning`. +- Use `supports-color` library to detect color support in error output. +- Add type definitions for TypeScript plugin developers (by Jed Mao). +- `Rule#selectors` setter detects separators. +- Add `postcss.stringify` method. +- Throw descriptive errors for incorrectly formatted plugins. +- Add docs to npm release. +- Fix `url()` parsing. +- Fix Windows support (by Jed Mao). ## 4.1.16 -* Fix errors without stack trace. + +- Fix errors without stack trace. ## 4.1.15 -* Allow asynchronous plugins to change processor plugins list (by Ben Briggs). + +- Allow asynchronous plugins to change processor plugins list (by Ben Briggs). ## 4.1.14 -* Fix for plugins packs defined by `postcss.plugin`. + +- Fix for plugins packs defined by `postcss.plugin`. ## 4.1.13 -* Fix input inlined source maps with UTF-8 encoding. + +- Fix input inlined source maps with UTF-8 encoding. ## 4.1.12 -* Update Promise polyfill. + +- Update Promise polyfill. ## 4.1.11 -* Fix error message on wrong plugin format. + +- Fix error message on wrong plugin format. ## 4.1.10 -* Fix Promise behavior on sync plugin errors. -* Automatically fill `plugin` field in `CssSyntaxError`. -* Fix warning message (by Ben Briggs). + +- Fix Promise behavior on sync plugin errors. +- Automatically fill `plugin` field in `CssSyntaxError`. +- Fix warning message (by Ben Briggs). ## 4.1.9 -* Speed up `node.clone()`. + +- Speed up `node.clone()`. ## 4.1.8 -* Accepts `Processor` instance in `postcss()` constructor too. + +- Accepts `Processor` instance in `postcss()` constructor too. ## 4.1.7 -* Speed up `postcss.list` (by Bogdan Chadkin). + +- Speed up `postcss.list` (by Bogdan Chadkin). ## 4.1.6 -* Fix Promise behavior on parsing error. + +- Fix Promise behavior on parsing error. ## 4.1.5 -* Parse at-words in declaration values. + +- Parse at-words in declaration values. ## 4.1.4 -* Fix Promise polyfill dependency (by Anton Yakushev and Matija Marohnić). + +- Fix Promise polyfill dependency (by Anton Yakushev and Matija Marohnić). ## 4.1.3 -* Add Promise polyfill for node.js 0.10 and IE. + +- Add Promise polyfill for node.js 0.10 and IE. ## 4.1.2 -* List helpers can be accessed independently `var space = postcss.list.space`. + +- List helpers can be accessed independently `var space = postcss.list.space`. ## 4.1.1 -* Show deprecated message only once. + +- Show deprecated message only once. ## 4.1 “Marquis Andras” -* Asynchronous plugin support. -* Add warnings from plugins and `Result#messages`. -* Add `postcss.plugin()` to create plugins with a standard API. -* Insert nodes by CSS string. -* Show version warning message on error from an outdated plugin. -* Send `Result` instance to plugins as the second argument. -* Add `CssSyntaxError#plugin`. -* Add `CssSyntaxError#showSourceCode()`. -* Add `postcss.list` and `postcss.vendor` aliases. -* Add `Processor#version`. -* Parse wrong closing bracket. -* Parse `!important` statement with spaces and comments inside (by Ben Briggs). -* Throw an error on declaration without `prop` or `value` (by Philip Peterson). -* Fix source map mappings position. -* Add indexed source map support. -* Always set `error.generated`. -* Clean all source map annotation comments. + +- Asynchronous plugin support. +- Add warnings from plugins and `Result#messages`. +- Add `postcss.plugin()` to create plugins with a standard API. +- Insert nodes by CSS string. +- Show version warning message on error from an outdated plugin. +- Send `Result` instance to plugins as the second argument. +- Add `CssSyntaxError#plugin`. +- Add `CssSyntaxError#showSourceCode()`. +- Add `postcss.list` and `postcss.vendor` aliases. +- Add `Processor#version`. +- Parse wrong closing bracket. +- Parse `!important` statement with spaces and comments inside (by Ben Briggs). +- Throw an error on declaration without `prop` or `value` (by Philip Peterson). +- Fix source map mappings position. +- Add indexed source map support. +- Always set `error.generated`. +- Clean all source map annotation comments. ## 4.0.6 -* Remove `babel` from released package dependencies (by Andres Suarez). + +- Remove `babel` from released package dependencies (by Andres Suarez). ## 4.0.5 -* Fix error message on double colon in declaration. + +- Fix error message on double colon in declaration. ## 4.0.4 -* Fix indent detection in some rare cases. + +- Fix indent detection in some rare cases. ## 4.0.3 -* Faster API with 6to5 Loose mode. -* Fix indexed source maps support. + +- Faster API with 6to5 Loose mode. +- Fix indexed source maps support. ## 4.0.2 -* Do not copy IE hacks to code style. + +- Do not copy IE hacks to code style. ## 4.0.1 -* Add `source.input` to `Root` too. + +- Add `source.input` to `Root` too. ## 4.0 “Duke Flauros” -* Rename `Container#childs` to `nodes`. -* Rename `PostCSS#processors` to `plugins`. -* Add `Node#replaceValues()` method. -* Add `Node#moveTo()`, `moveBefore()` and `moveAfter()` methods. -* Add `Node#cloneBefore()` and `cloneAfter()` shortcuts. -* Add `Node#next()`, `prev()` and `root()` shortcuts. -* Add `Node#replaceWith()` method. -* Add `Node#error()` method. -* Add `Container#removeAll()` method. -* Add filter argument to `eachDecl()` and `eachAtRule()`. -* Add `Node#source.input` and move `source.file` or `source.id` to `input`. -* Change code indent, when node was moved. -* Better fix code style on `Rule`, `AtRule` and `Comment` nodes changes. -* Allow to create rules and at-rules by hash shortcut in append methods. -* Add class name to CSS syntax error output. + +- Rename `Container#childs` to `nodes`. +- Rename `PostCSS#processors` to `plugins`. +- Add `Node#replaceValues()` method. +- Add `Node#moveTo()`, `moveBefore()` and `moveAfter()` methods. +- Add `Node#cloneBefore()` and `cloneAfter()` shortcuts. +- Add `Node#next()`, `prev()` and `root()` shortcuts. +- Add `Node#replaceWith()` method. +- Add `Node#error()` method. +- Add `Container#removeAll()` method. +- Add filter argument to `eachDecl()` and `eachAtRule()`. +- Add `Node#source.input` and move `source.file` or `source.id` to `input`. +- Change code indent, when node was moved. +- Better fix code style on `Rule`, `AtRule` and `Comment` nodes changes. +- Allow to create rules and at-rules by hash shortcut in append methods. +- Add class name to CSS syntax error output. ## 3.0.7 -* Fix IE filter parsing with multiple commands. -* Safer way to consume PostCSS object as plugin (by Maxime Thirouin). + +- Fix IE filter parsing with multiple commands. +- Safer way to consume PostCSS object as plugin (by Maxime Thirouin). ## 3.0.6 -* Fix missing semicolon when comment comes after last declaration. -* Fix Safe Mode declaration parsing on unclosed blocks. + +- Fix missing semicolon when comment comes after last declaration. +- Fix Safe Mode declaration parsing on unclosed blocks. ## 3.0.5 -* Fix parser to support difficult cases with backslash escape and brackets. -* Add `CssSyntaxError#stack` (by Maxime Thirouin). + +- Fix parser to support difficult cases with backslash escape and brackets. +- Add `CssSyntaxError#stack` (by Maxime Thirouin). ## 3.0.4 -* Fix Safe Mode on unknown word before declaration. + +- Fix Safe Mode on unknown word before declaration. ## 3.0.3 -* Increase tokenizer speed (by Roman Dvornov). + +- Increase tokenizer speed (by Roman Dvornov). ## 3.0.2 -* Fix empty comment parsing. -* Fix `Root#normalize` in some inserts. + +- Fix empty comment parsing. +- Fix `Root#normalize` in some inserts. ## 3.0.1 -* Fix Rhino JS runtime support. -* Typo in deprecated warning (by Maxime Thirouin). + +- Fix Rhino JS runtime support. +- Typo in deprecated warning (by Maxime Thirouin). ## 3.0 “Marquis Andrealphus” -* New parser, which become the fastest ever CSS parser written in JavaScript. -* Parser can now parse declarations and rules in one parent (like in `@page`) + +- New parser, which become the fastest ever CSS parser written in JavaScript. +- Parser can now parse declarations and rules in one parent (like in `@page`) and nested declarations for plugins like `postcss-nested`. -* Child nodes array is now in `childs` property, instead of `decls` and `rules`. -* `map.inline` and `map.sourcesContent` options are now `true` by default. -* Fix iterators (`each`, `insertAfter`) on children array changes. -* Use previous source map to show origin source of CSS syntax error. -* Use 6to5 ES6 compiler, instead of ES6 Transpiler. -* Use code style for manually added rules from existing rules. -* Use `from` option from previous source map `file` field. -* Set `to` value to `from` if `to` option is missing. -* Use better node source name when missing `from` option. -* Show a syntax error when `;` is missed between declarations. -* Allow to pass `PostCSS` instance or list of plugins to `use()` method. -* Allow to pass `Result` instance to `process()` method. -* Trim Unicode BOM on source maps parsing. -* Parse at-rules without spaces like `@import"file"`. -* Better previous `sourceMappingURL` annotation comment cleaning. -* Do not remove previous `sourceMappingURL` comment on `map.annotation: false`. -* Parse nameless at-rules in Safe Mode. -* Fix source map generation for nodes without source. -* Fix next child `before` if `Root` first child got removed. +- Child nodes array is now in `childs` property, instead of `decls` and `rules`. +- `map.inline` and `map.sourcesContent` options are now `true` by default. +- Fix iterators (`each`, `insertAfter`) on children array changes. +- Use previous source map to show origin source of CSS syntax error. +- Use 6to5 ES6 compiler, instead of ES6 Transpiler. +- Use code style for manually added rules from existing rules. +- Use `from` option from previous source map `file` field. +- Set `to` value to `from` if `to` option is missing. +- Use better node source name when missing `from` option. +- Show a syntax error when `;` is missed between declarations. +- Allow to pass `PostCSS` instance or list of plugins to `use()` method. +- Allow to pass `Result` instance to `process()` method. +- Trim Unicode BOM on source maps parsing. +- Parse at-rules without spaces like `@import"file"`. +- Better previous `sourceMappingURL` annotation comment cleaning. +- Do not remove previous `sourceMappingURL` comment on `map.annotation: false`. +- Parse nameless at-rules in Safe Mode. +- Fix source map generation for nodes without source. +- Fix next child `before` if `Root` first child got removed. ## 2.2.6 -* Fix map generation for nodes without source (by Josiah Savary). + +- Fix map generation for nodes without source (by Josiah Savary). ## 2.2.5 -* Fix source map with BOM marker support (by Mohammad Younes). -* Fix source map paths (by Mohammad Younes). + +- Fix source map with BOM marker support (by Mohammad Younes). +- Fix source map paths (by Mohammad Younes). ## 2.2.4 -* Fix `prepend()` on empty `Root`. + +- Fix `prepend()` on empty `Root`. ## 2.2.3 -* Allow to use object shortcut in `use()` with functions like `autoprefixer`. + +- Allow to use object shortcut in `use()` with functions like `autoprefixer`. ## 2.2.2 -* Add shortcut to set processors in `use()` via object with `.postcss` property. + +- Add shortcut to set processors in `use()` via object with `.postcss` property. ## 2.2.1 -* Send `opts` from `Processor#process(css, opts)` to processors. + +- Send `opts` from `Processor#process(css, opts)` to processors. ## 2.2 “Marquis Cimeies” -* Use GNU style syntax error messages. -* Add `Node#replace` method. -* Add `CssSyntaxError#reason` property. + +- Use GNU style syntax error messages. +- Add `Node#replace` method. +- Add `CssSyntaxError#reason` property. ## 2.1.2 -* Fix UTF-8 support in inline source map. -* Fix source map `sourcesContent` if there is no `from` and `to` options. + +- Fix UTF-8 support in inline source map. +- Fix source map `sourcesContent` if there is no `from` and `to` options. ## 2.1.1 -* Allow to miss `to` and `from` options for inline source maps. -* Add `Node#source.id` if file name is unknown. -* Better detect splitter between rules in CSS concatenation tools. -* Automatically clone node in insert methods. + +- Allow to miss `to` and `from` options for inline source maps. +- Add `Node#source.id` if file name is unknown. +- Better detect splitter between rules in CSS concatenation tools. +- Automatically clone node in insert methods. ## 2.1 “King Amdusias” -* Change Traceur ES6 compiler to ES6 Transpiler. -* Show broken CSS line in syntax error. + +- Change Traceur ES6 compiler to ES6 Transpiler. +- Show broken CSS line in syntax error. ## 2.0 “King Belial” -* Project was rewritten from CoffeeScript to ES6. -* Add Safe Mode to works with live input or with hacks from legacy code. -* More safer parser to pass all hacks from Browserhacks.com. -* Use real properties instead of magic getter/setter for raw properties. + +- Project was rewritten from CoffeeScript to ES6. +- Add Safe Mode to works with live input or with hacks from legacy code. +- More safer parser to pass all hacks from Browserhacks.com. +- Use real properties instead of magic getter/setter for raw properties. ## 1.0 “Marquis Decarabia” -* Save previous source map for each node to support CSS concatenation + +- Save previous source map for each node to support CSS concatenation with multiple previous maps. -* Add `map.sourcesContent` option to add origin content to `sourcesContent` +- Add `map.sourcesContent` option to add origin content to `sourcesContent` inside map. -* Allow to set different place of output map in annotation comment. -* Allow to use arrays and `Root` in `Container#append` and same methods. -* Add `Root#prevMap` with information about previous map. -* Allow to use latest PostCSS from GitHub by npm. -* `Result` now is lazy and it will generate output CSS only if you use `css` +- Allow to set different place of output map in annotation comment. +- Allow to use arrays and `Root` in `Container#append` and same methods. +- Add `Root#prevMap` with information about previous map. +- Allow to use latest PostCSS from GitHub by npm. +- `Result` now is lazy and it will generate output CSS only if you use `css` or `map` property. -* Use separated `map.prev` option to set previous map. -* Rename `inlineMap` option to `map.inline`. -* Rename `mapAnnotation` option to `map.annotation`. -* `Result#map` now return `SourceMapGenerator` object, instead of string. -* Run previous map autodetect only if input CSS contains annotation comment. -* Add `map: 'inline'` shortcut for `map: { inline: true }` option. -* `Node#source.file` now will contains absolute path. -* Clean `Declaration#between` style on node clone. +- Use separated `map.prev` option to set previous map. +- Rename `inlineMap` option to `map.inline`. +- Rename `mapAnnotation` option to `map.annotation`. +- `Result#map` now return `SourceMapGenerator` object, instead of string. +- Run previous map autodetect only if input CSS contains annotation comment. +- Add `map: 'inline'` shortcut for `map: { inline: true }` option. +- `Node#source.file` now will contains absolute path. +- Clean `Declaration#between` style on node clone. ## 0.3.5 -* Allow to use `Root` or `Result` as first argument in `process()`. -* Save parsed AST to `Result#root`. + +- Allow to use `Root` or `Result` as first argument in `process()`. +- Save parsed AST to `Result#root`. ## 0.3.4 -* Better space symbol detect to read UTF-8 BOM correctly. + +- Better space symbol detect to read UTF-8 BOM correctly. ## 0.3.3 -* Remove source map hacks by using new Mozilla’s `source-map` (by Simon Lydell). + +- Remove source map hacks by using new Mozilla’s `source-map` (by Simon Lydell). ## 0.3.2 -* Add URI encoding support for inline source maps. + +- Add URI encoding support for inline source maps. ## 0.3.1 -* Fix relative paths from previous source map. -* Safer space split in `Rule#selectors` (by Simon Lydell). + +- Fix relative paths from previous source map. +- Safer space split in `Rule#selectors` (by Simon Lydell). ## 0.3 “Prince Seere” -* Add `Comment` node for comments between declarations or rules. -* Add source map annotation comment to output CSS. -* Allow to inline source map to annotation comment by data:uri. -* Fix source maps on Windows. -* Fix source maps for subdirectory (by Dmitry Nikitenko and Simon Lydell). -* Autodetect previous source map. -* Add `first` and `last` shortcuts to container nodes. -* Parse `!important` to separated property in `Declaration`. -* Allow to break iteration by returning `false`. -* Copy code style to new nodes. -* Add `eachInside` method to recursively iterate all nodes. -* Add `selectors` shortcut to get selectors array. -* Add `toResult` method to `Rule` to simplify work with several input files. -* Clean declaration’s `value`, rule’s `selector` and at-rule’s `params` + +- Add `Comment` node for comments between declarations or rules. +- Add source map annotation comment to output CSS. +- Allow to inline source map to annotation comment by data:uri. +- Fix source maps on Windows. +- Fix source maps for subdirectory (by Dmitry Nikitenko and Simon Lydell). +- Autodetect previous source map. +- Add `first` and `last` shortcuts to container nodes. +- Parse `!important` to separated property in `Declaration`. +- Allow to break iteration by returning `false`. +- Copy code style to new nodes. +- Add `eachInside` method to recursively iterate all nodes. +- Add `selectors` shortcut to get selectors array. +- Add `toResult` method to `Rule` to simplify work with several input files. +- Clean declaration’s `value`, rule’s `selector` and at-rule’s `params` by storing spaces in `between` property. ## 0.2 “Duke Dantalion” -* Add source map support. -* Add shortcuts to create nodes. -* Method `process()` now returns object with `css` and `map` keys. -* Origin CSS file option was renamed from `file` to `from`. -* Rename `Node#remove()` method to `removeSelf()` to fix name conflict. -* Node source was moved to `source` property with origin file + +- Add source map support. +- Add shortcuts to create nodes. +- Method `process()` now returns object with `css` and `map` keys. +- Origin CSS file option was renamed from `file` to `from`. +- Rename `Node#remove()` method to `removeSelf()` to fix name conflict. +- Node source was moved to `source` property with origin file and node end position. -* You can set own CSS generate function. +- You can set own CSS generate function. ## 0.1 “Count Andromalius” -* Initial release. + +- Initial release. diff --git a/LICENSE b/LICENSE index da057b456..c2314d53d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright 2013 Andrey Sitnik +Copyright 2013 Andrey Sitnik Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index b0611a3e6..01f715fea 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,140 @@ -# PostCSS [![Travis Build Status][travis-img]][travis] [![AppVeyor Build Status][appveyor-img]][appveyor] [![Gitter][chat-img]][chat] +# PostCSS - -[appveyor-img]: https://img.shields.io/appveyor/ci/ai/postcss.svg?label=windows -[travis-img]: https://img.shields.io/travis/postcss/postcss.svg?label=unix -[chat-img]: https://img.shields.io/badge/Gitter-Join_the_PostCSS_chat-brightgreen.svg -[appveyor]: https://ci.appveyor.com/project/ai/postcss -[travis]: https://travis-ci.org/postcss/postcss -[chat]: https://gitter.im/postcss/postcss + alt="Philosopher’s stone, logo of PostCSS" + src="https://postcss.org/logo.svg"> PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more. PostCSS is used by industry leaders including Wikipedia, Twitter, Alibaba, -and JetBrains. The [Autoprefixer] PostCSS plugin is one of the most popular -CSS processors. +and JetBrains. The [Autoprefixer] and [Stylelint] PostCSS plugins are some of the most popular CSS tools. -Twitter account: [@postcss](https://twitter.com/postcss). -VK.com page: [postcss](https://vk.com/postcss). -Support / Discussion: [Gitter](https://gitter.im/postcss/postcss). +--- -For PostCSS commercial support (consulting, improving the front-end culture -of your company, PostCSS plugins), contact Evil Martians -at . +  PostCSS is built by Evil Martians, an American design and engineering consultancy for developer tools, AI, and cybersecurity startups. -[Autoprefixer]: https://github.com/postcss/autoprefixer +--- - - Sponsored by Evil Martians +[Abstract Syntax Tree]: https://en.wikipedia.org/wiki/Abstract_syntax_tree +[Evil Martians]: https://evilmartians.com/?utm_source=postcss +[Autoprefixer]: https://github.com/postcss/autoprefixer +[Stylelint]: https://stylelint.io/ +[plugins]: https://github.com/postcss/postcss#plugins + +## Sponsorship + +PostCSS needs your support. We are accepting donations +[at Open Collective](https://opencollective.com/postcss/). + +
+
+ +Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. + +Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access. ## Plugins +PostCSS takes a CSS file and provides an API to analyze and modify its rules +(by transforming them into an [Abstract Syntax Tree]). +This API can then be used by [plugins] to do a lot of useful things, +e.g., to find errors automatically, or to insert vendor prefixes. + Currently, PostCSS has more than 200 plugins. You can find all of the plugins -in the [plugins list] or in the [searchable catalog]. Below is a list -of our favorite plugins — the best demonstrations of what can be built -on top of PostCSS. +in the [plugins list]. Below is a list of our favorite plugins — +the best demonstrations of what can be built on top of PostCSS. If you have any new ideas, [PostCSS plugin development] is really easy. -[searchable catalog]: http://postcss.parts -[plugins list]: https://github.com/postcss/postcss/blob/master/docs/plugins.md +[plugins list]: https://github.com/postcss/postcss/blob/main/docs/plugins.md ### Solve Global CSS Problem -* [`postcss-use`] allows you to explicitly set PostCSS plugins within CSS +- [`postcss-use`] allows you to explicitly set PostCSS plugins within CSS and execute them only for the current file. -* [`postcss-modules`] and [`react-css-modules`] automatically isolate +- [`postcss-modules`] and [`react-css-modules`] automatically isolate selectors within components. -* [`postcss-autoreset`] is an alternative to using a global reset +- [`postcss-autoreset`] is an alternative to using a global reset that is better for isolatable components. -* [`postcss-initial`] adds `all: initial` support, which resets +- [`postcss-initial`] adds `all: initial` support, which resets all inherited styles. -* [`cq-prolyfill`] adds container query support, allowing styles that respond +- [`cq-prolyfill`] adds container query support, allowing styles that respond to the width of the parent. ### Use Future CSS, Today -* [`autoprefixer`] adds vendor prefixes, using data from Can I Use. -* [`postcss-cssnext`] allows you to use future CSS features today - (includes `autoprefixer`). +- [`autoprefixer`] adds vendor prefixes, using data from Can I Use. +- [`postcss-preset-env`] allows you to use future CSS features today. ### Better CSS Readability -* [`precss`] contains plugins for Sass-like features, like variables, nesting, - and mixins. -* [`postcss-sorting`] sorts the content of rules and at-rules. -* [`postcss-utilities`] includes the most commonly used shortcuts and helpers. -* [`short`] adds and extends numerous shorthand properties. +- [`postcss-nested`] unwraps nested rules the way Sass does. +- [`postcss-sorting`] sorts the content of rules and at-rules. +- [`postcss-utilities`] includes the most commonly used shortcuts and helpers. +- [`short`] adds and extends numerous shorthand properties. ### Images and Fonts -* [`postcss-assets`] inserts image dimensions and inlines files. -* [`postcss-sprites`] generates image sprites. -* [`font-magician`] generates all the `@font-face` rules needed in CSS. -* [`postcss-inline-svg`] allows you to inline SVG and customize its styles. -* [`postcss-write-svg`] allows you to write simple SVG directly in your CSS. +- [`postcss-url`] postcss plugin to rebase url(), inline or copy asset. +- [`postcss-sprites`] generates image sprites. +- [`font-magician`] generates all the `@font-face` rules needed in CSS. +- [`postcss-inline-svg`] allows you to inline SVG and customize its styles. +- [`postcss-write-svg`] allows you to write simple SVG directly in your CSS. +- [`webp-in-css`] to use WebP image format in CSS background. +- [`avif-in-css`] to use AVIF image format in CSS background. ### Linters -* [`stylelint`] is a modular stylesheet linter. -* [`stylefmt`] is a tool that automatically formats CSS +- [`stylelint`] is a modular stylesheet linter. +- [`stylefmt`] is a tool that automatically formats CSS according `stylelint` rules. -* [`doiuse`] lints CSS for browser support, using data from Can I Use. -* [`colorguard`] helps you maintain a consistent color palette. +- [`doiuse`] lints CSS for browser support, using data from Can I Use. +- [`colorguard`] helps you maintain a consistent color palette. ### Other -* [`lost`] is a feature-rich `calc()` grid system. -* [`cssnano`] is a modular CSS minifier. -* [`rtlcss`] mirrors styles for right-to-left locales. +- [`cssnano`] is a modular CSS minifier. +- [`lost`] is a feature-rich `calc()` grid system. +- [`rtlcss`] mirrors styles for right-to-left locales. -[PostCSS plugin development]: https://github.com/postcss/postcss/blob/master/docs/writing-a-plugin.md -[`postcss-inline-svg`]: https://github.com/TrySound/postcss-inline-svg -[`react-css-modules`]: https://github.com/gajus/react-css-modules -[`postcss-autoreset`]: https://github.com/maximkoretskiy/postcss-autoreset -[`postcss-write-svg`]: https://github.com/jonathantneal/postcss-write-svg +[PostCSS plugin development]: https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md +[`postcss-inline-svg`]: https://github.com/TrySound/postcss-inline-svg +[`postcss-preset-env`]: https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env +[`react-css-modules`]: https://github.com/gajus/react-css-modules +[`postcss-autoreset`]: https://github.com/maximkoretskiy/postcss-autoreset +[`postcss-write-svg`]: https://github.com/csstools/postcss-write-svg [`postcss-utilities`]: https://github.com/ismamz/postcss-utilities -[`postcss-initial`]: https://github.com/maximkoretskiy/postcss-initial -[`postcss-sprites`]: https://github.com/2createStudio/postcss-sprites -[`postcss-modules`]: https://github.com/outpunk/postcss-modules -[`postcss-sorting`]: https://github.com/hudochenkov/postcss-sorting -[`postcss-cssnext`]: http://cssnext.io -[`postcss-assets`]: https://github.com/assetsjs/postcss-assets -[`font-magician`]: https://github.com/jonathantneal/postcss-font-magician -[`autoprefixer`]: https://github.com/postcss/autoprefixer -[`cq-prolyfill`]: https://github.com/ausi/cq-prolyfill -[`postcss-use`]: https://github.com/postcss/postcss-use -[`css-modules`]: https://github.com/css-modules/css-modules -[`colorguard`]: https://github.com/SlexAxton/css-colorguard -[`stylelint`]: https://github.com/stylelint/stylelint -[`stylefmt`]: https://github.com/morishitter/stylefmt -[`cssnano`]: http://cssnano.co -[`precss`]: https://github.com/jonathantneal/precss -[`doiuse`]: https://github.com/anandthakker/doiuse -[`rtlcss`]: https://github.com/MohammadYounes/rtlcss -[`short`]: https://github.com/jonathantneal/postcss-short -[`lost`]: https://github.com/peterramsing/lost +[`postcss-initial`]: https://github.com/maximkoretskiy/postcss-initial +[`postcss-sprites`]: https://github.com/2createStudio/postcss-sprites +[`postcss-modules`]: https://github.com/outpunk/postcss-modules +[`postcss-sorting`]: https://github.com/hudochenkov/postcss-sorting +[`font-magician`]: https://github.com/csstools/postcss-font-magician +[`autoprefixer`]: https://github.com/postcss/autoprefixer +[`cq-prolyfill`]: https://github.com/ausi/cq-prolyfill +[`postcss-url`]: https://github.com/postcss/postcss-url +[`postcss-use`]: https://github.com/postcss/postcss-use +[`css-modules`]: https://github.com/css-modules/css-modules +[`webp-in-css`]: https://github.com/ai/webp-in-css +[`avif-in-css`]: https://github.com/nucliweb/avif-in-css +[`colorguard`]: https://github.com/SlexAxton/css-colorguard +[`stylelint`]: https://github.com/stylelint/stylelint +[`stylefmt`]: https://github.com/morishitter/stylefmt +[`cssnano`]: https://cssnano.github.io/cssnano/ +[`postcss-nested`]: https://github.com/postcss/postcss-nested +[`doiuse`]: https://github.com/anandthakker/doiuse +[`rtlcss`]: https://github.com/MohammadYounes/rtlcss +[`short`]: https://github.com/csstools/postcss-short +[`lost`]: https://github.com/peterramsing/lost ## Syntaxes @@ -128,34 +142,54 @@ PostCSS can transform styles in any syntax, not just CSS. If there is not yet support for your favorite syntax, you can write a parser and/or stringifier to extend PostCSS. -* [`sugarss`] is a indent-based syntax like Sass or Stylus. -* [`postcss-scss`] allows you to work with SCSS - *(but does not compile SCSS to CSS)*. -* [`postcss-less`] allows you to work with Less - *(but does not compile LESS to CSS)*. -* [`postcss-less-engine`] allows you to work with Less - *(and DOES compile LESS to CSS using true Less.js evaluation)*. -* [`postcss-js`] allows you to write styles in JS or transform +- [`sugarss`] is a indent-based syntax like Sass or Stylus. +- [`postcss-syntax`] switch syntax automatically by file extensions. +- [`postcss-html`] parsing styles in `' + * ) + * document.type //=> 'document' + * document.nodes.length //=> 2 + * ``` + */ +declare class Document_ extends Container { + nodes: Root[] + parent: undefined + type: 'document' + + constructor(defaults?: Document.DocumentProps) + + assign(overrides: Document.DocumentProps | object): this + clone(overrides?: Partial): this + cloneAfter(overrides?: Partial): this + cloneBefore(overrides?: Partial): this + + /** + * Returns a `Result` instance representing the document’s CSS roots. + * + * ```js + * const root1 = postcss.parse(css1, { from: 'a.css' }) + * const root2 = postcss.parse(css2, { from: 'b.css' }) + * const document = postcss.document() + * document.append(root1) + * document.append(root2) + * const result = document.toResult({ to: 'all.css', map: true }) + * ``` + * + * @param opts Options. + * @return Result with current document’s CSS. + */ + toResult(options?: ProcessOptions): Result +} + +declare class Document extends Document_ {} + +export = Document diff --git a/lib/document.js b/lib/document.js new file mode 100644 index 000000000..44689917f --- /dev/null +++ b/lib/document.js @@ -0,0 +1,33 @@ +'use strict' + +let Container = require('./container') + +let LazyResult, Processor + +class Document extends Container { + constructor(defaults) { + // type needs to be passed to super, otherwise child roots won't be normalized correctly + super({ type: 'document', ...defaults }) + + if (!this.nodes) { + this.nodes = [] + } + } + + toResult(opts = {}) { + let lazy = new LazyResult(new Processor(), this, opts) + + return lazy.stringify() + } +} + +Document.registerLazyResult = dependant => { + LazyResult = dependant +} + +Document.registerProcessor = dependant => { + Processor = dependant +} + +module.exports = Document +Document.default = Document diff --git a/lib/fromJSON.d.ts b/lib/fromJSON.d.ts new file mode 100644 index 000000000..3a0c5b85f --- /dev/null +++ b/lib/fromJSON.d.ts @@ -0,0 +1,9 @@ +import { JSONHydrator } from './postcss.js' + +interface FromJSON extends JSONHydrator { + default: FromJSON +} + +declare let fromJSON: FromJSON + +export = fromJSON diff --git a/lib/fromJSON.js b/lib/fromJSON.js new file mode 100644 index 000000000..c1a9509b4 --- /dev/null +++ b/lib/fromJSON.js @@ -0,0 +1,107 @@ +'use strict' + +let AtRule = require('./at-rule') +let Comment = require('./comment') +let Declaration = require('./declaration') +let Input = require('./input') +let PreviousMap = require('./previous-map') +let Root = require('./root') +let Rule = require('./rule') + +function hydrateInputs(json, inputs) { + if (!json.inputs) return inputs + return json.inputs.map(input => { + let inputHydrated = { ...input, __proto__: Input.prototype } + if (inputHydrated.map) { + inputHydrated.map = { + ...inputHydrated.map, + __proto__: PreviousMap.prototype + } + } + return inputHydrated + }) +} + +function constructNode(json, inputs, children) { + let defaults = { ...json } + delete defaults.inputs + delete defaults.nodes + if (defaults.source) { + let { inputId, ...source } = defaults.source + defaults.source = source + if (inputId != null) { + defaults.source.input = inputs[inputId] + } + } + + let node + if (defaults.type === 'root') { + node = new Root(defaults) + } else if (defaults.type === 'decl') { + node = new Declaration(defaults) + } else if (defaults.type === 'rule') { + node = new Rule(defaults) + } else if (defaults.type === 'comment') { + node = new Comment(defaults) + } else if (defaults.type === 'atrule') { + node = new AtRule(defaults) + } else { + throw new Error('Unknown node type: ' + json.type) + } + + // Rehydrated children are attached after construction. Passing them + // through the container constructor would re-run insertion spacing + // normalization and overwrite each child's own `raws.before`. + if (children) { + node.nodes = children + for (let child of children) child.parent = node + } + + return node +} + +function fromJSON(json, inputs) { + if (Array.isArray(json)) return json.map(n => fromJSON(n)) + + // An explicit stack instead of recursive calls to survive deeply + // nested trees. Children are rehydrated before their parent node + // is constructed. + let result + let stack = [ + { childIndex: 0, children: [], inputs: hydrateInputs(json, inputs), json } + ] + + while (stack.length > 0) { + let frame = stack[stack.length - 1] + let jsonNodes = frame.json.nodes + + if (jsonNodes && frame.childIndex < jsonNodes.length) { + let childJson = jsonNodes[frame.childIndex] + frame.childIndex += 1 + stack.push({ + childIndex: 0, + children: [], + inputs: hydrateInputs(childJson, frame.inputs), + json: childJson + }) + continue + } + + stack.pop() + let node = constructNode( + frame.json, + frame.inputs, + jsonNodes ? frame.children : undefined + ) + if (stack.length > 0) { + stack[stack.length - 1].children.push(node) + } else { + result = node + } + } + + return result +} + +module.exports = fromJSON +fromJSON.default = fromJSON diff --git a/lib/input.d.ts b/lib/input.d.ts new file mode 100644 index 000000000..ca2d26bf8 --- /dev/null +++ b/lib/input.d.ts @@ -0,0 +1,226 @@ +import { CssSyntaxError, ProcessOptions } from './postcss.js' +import PreviousMap from './previous-map.js' + +declare namespace Input { + export interface FilePosition { + /** + * Column of inclusive start position in source file. + */ + column: number + + /** + * Column of exclusive end position in source file. + */ + endColumn?: number + + /** + * Line of exclusive end position in source file. + */ + endLine?: number + + /** + * Offset of exclusive end position in source file. + */ + endOffset?: number + + /** + * Absolute path to the source file. + */ + file?: string + + /** + * Line of inclusive start position in source file. + */ + line: number + + /** + * Offset of inclusive start position in source file. + */ + offset: number + + /** + * Source code. + */ + source?: string + + /** + * URL for the source file. + */ + url: string + } + + export { Input_ as default } +} + +/** + * Represents the source CSS. + * + * ```js + * const root = postcss.parse(css, { from: file }) + * const input = root.source.input + * ``` + */ +declare class Input_ { + /** + * Input CSS source. + * + * ```js + * const input = postcss.parse('a{}', { from: file }).input + * input.css //=> "a{}" + * ``` + */ + css: string + + /** + * Input source with support for non-CSS documents. + * + * ```js + * const input = postcss.parse('a{}', { from: file, document: '' }).input + * input.document //=> "" + * input.css //=> "a{}" + * ``` + */ + document: string + + /** + * The absolute path to the CSS source file defined + * with the `from` option. + * + * ```js + * const root = postcss.parse(css, { from: 'a.css' }) + * root.source.input.file //=> '/home/ai/a.css' + * ``` + */ + file?: string + + /** + * The flag to indicate whether or not the source code has Unicode BOM. + */ + hasBOM: boolean + + /** + * The unique ID of the CSS source. It will be created if `from` option + * is not provided (because PostCSS does not know the file path). + * + * ```js + * const root = postcss.parse(css) + * root.source.input.file //=> undefined + * root.source.input.id //=> "" + * ``` + */ + id?: string + + /** + * The input source map passed from a compilation step before PostCSS + * (for example, from Sass compiler). + * + * ```js + * root.source.input.map.consumer().sources //=> ['a.sass'] + * ``` + */ + map: PreviousMap + + /** + * The CSS source identifier. Contains `Input#file` if the user + * set the `from` option, or `Input#id` if they did not. + * + * ```js + * const root = postcss.parse(css, { from: 'a.css' }) + * root.source.input.from //=> "/home/ai/a.css" + * + * const root = postcss.parse(css) + * root.source.input.from //=> "" + * ``` + */ + get from(): string + + /** + * @param css Input CSS source. + * @param opts Process options. + */ + constructor(css: string, opts?: ProcessOptions) + + /** + * Returns `CssSyntaxError` with information about the error and its position. + */ + error( + message: string, + start: + | { + column: number + line: number + } + | { + offset: number + }, + end: + | { + column: number + line: number + } + | { + offset: number + }, + opts?: { plugin?: CssSyntaxError['plugin'] } + ): CssSyntaxError + error( + message: string, + line: number, + column: number, + opts?: { plugin?: CssSyntaxError['plugin'] } + ): CssSyntaxError + error( + message: string, + offset: number, + opts?: { plugin?: CssSyntaxError['plugin'] } + ): CssSyntaxError + + /** + * Converts source line and column to offset. + * + * @param line Source line. + * @param column Source column. + * @return Source offset. + */ + fromLineAndColumn(line: number, column: number): number + + /** + * Converts source offset to line and column. + * + * @param offset Source offset. + */ + fromOffset(offset: number): { col: number; line: number } | null + + /** + * Reads the input source map and returns a symbol position + * in the input source (e.g., in a Sass file that was compiled + * to CSS before being passed to PostCSS). Optionally takes an + * end position, exclusive. + * + * ```js + * root.source.input.origin(1, 1) //=> { file: 'a.css', line: 3, column: 1 } + * root.source.input.origin(1, 1, 1, 4) + * //=> { file: 'a.css', line: 3, column: 1, endLine: 3, endColumn: 4 } + * ``` + * + * @param line Line for inclusive start position in input CSS. + * @param column Column for inclusive start position in input CSS. + * @param endLine Line for exclusive end position in input CSS. + * @param endColumn Column for exclusive end position in input CSS. + * + * @return Position in input source. + */ + origin( + line: number, + column: number, + endLine?: number, + endColumn?: number + ): false | Input.FilePosition + + /** Converts this to a JSON-friendly object representation. */ + toJSON(): object +} + +declare class Input extends Input_ {} + +export = Input diff --git a/lib/input.es6 b/lib/input.es6 deleted file mode 100644 index b7b6b0d93..000000000 --- a/lib/input.es6 +++ /dev/null @@ -1,165 +0,0 @@ -import CssSyntaxError from './css-syntax-error'; -import PreviousMap from './previous-map'; - -import path from 'path'; - -let sequence = 0; - -/** - * @typedef {object} filePosition - * @property {string} file - path to file - * @property {number} line - source line in file - * @property {number} column - source column in file - */ - -/** - * Represents the source CSS. - * - * @example - * const root = postcss.parse(css, { from: file }); - * const input = root.source.input; - */ -class Input { - - /** - * @param {string} css - input CSS source - * @param {object} [opts] - {@link Processor#process} options - */ - constructor(css, opts = { }) { - /** - * @member {string} - input CSS source - * - * @example - * const input = postcss.parse('a{}', { from: file }).input; - * input.css //=> "a{}"; - */ - this.css = css.toString(); - - if ( this.css[0] === '\uFEFF' || this.css[0] === '\uFFFE' ) { - this.css = this.css.slice(1); - } - - if ( opts.from ) { - if ( /^\w+:\/\//.test(opts.from) ) { - /** - * @member {string} - The absolute path to the CSS source file - * defined with the `from` option. - * - * @example - * const root = postcss.parse(css, { from: 'a.css' }); - * root.source.input.file //=> '/home/ai/a.css' - */ - this.file = opts.from; - } else { - this.file = path.resolve(opts.from); - } - } - - let map = new PreviousMap(this.css, opts); - if ( map.text ) { - /** - * @member {PreviousMap} - The input source map passed from - * a compilation step before PostCSS - * (for example, from Sass compiler). - * - * @example - * root.source.input.map.consumer().sources //=> ['a.sass'] - */ - this.map = map; - let file = map.consumer().file; - if ( !this.file && file ) this.file = this.mapResolve(file); - } - - if ( !this.file ) { - sequence += 1; - /** - * @member {string} - The unique ID of the CSS source. It will be - * created if `from` option is not provided - * (because PostCSS does not know the file path). - * - * @example - * const root = postcss.parse(css); - * root.source.input.file //=> undefined - * root.source.input.id //=> "" - */ - this.id = ''; - } - if ( this.map ) this.map.file = this.from; - } - - error(message, line, column, opts = { }) { - let result; - let origin = this.origin(line, column); - if ( origin ) { - result = new CssSyntaxError(message, origin.line, origin.column, - origin.source, origin.file, opts.plugin); - } else { - result = new CssSyntaxError(message, line, column, - this.css, this.file, opts.plugin); - } - - result.input = { line, column, source: this.css }; - if ( this.file ) result.input.file = this.file; - - return result; - } - - /** - * Reads the input source map and returns a symbol position - * in the input source (e.g., in a Sass file that was compiled - * to CSS before being passed to PostCSS). - * - * @param {number} line - line in input CSS - * @param {number} column - column in input CSS - * - * @return {filePosition} position in input source - * - * @example - * root.source.input.origin(1, 1) //=> { file: 'a.css', line: 3, column: 1 } - */ - origin(line, column) { - if ( !this.map ) return false; - let consumer = this.map.consumer(); - - let from = consumer.originalPositionFor({ line, column }); - if ( !from.source ) return false; - - let result = { - file: this.mapResolve(from.source), - line: from.line, - column: from.column - }; - - let source = consumer.sourceContentFor(from.source); - if ( source ) result.source = source; - - return result; - } - - mapResolve(file) { - if ( /^\w+:\/\//.test(file) ) { - return file; - } else { - return path.resolve(this.map.consumer().sourceRoot || '.', file); - } - } - - /** - * The CSS source identifier. Contains {@link Input#file} if the user - * set the `from` option, or {@link Input#id} if they did not. - * @type {string} - * - * @example - * const root = postcss.parse(css, { from: 'a.css' }); - * root.source.input.from //=> "/home/ai/a.css" - * - * const root = postcss.parse(css); - * root.source.input.from //=> "" - */ - get from() { - return this.file || this.id; - } - -} - -export default Input; diff --git a/lib/input.js b/lib/input.js new file mode 100644 index 000000000..defe395fc --- /dev/null +++ b/lib/input.js @@ -0,0 +1,282 @@ +'use strict' + +let { nanoid } = require('nanoid/non-secure') +let { isAbsolute, resolve } = require('path') +let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js') +let { fileURLToPath, pathToFileURL } = require('url') + +let CssSyntaxError = require('./css-syntax-error') +let PreviousMap = require('./previous-map') +let terminalHighlight = require('./terminal-highlight') + +let lineToIndexCache = Symbol('lineToIndexCache') + +let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator) +let pathAvailable = Boolean(resolve && isAbsolute) + +function getLineToIndex(input) { + if (input[lineToIndexCache]) return input[lineToIndexCache] + let lines = input.css.split('\n') + let lineToIndex = new Array(lines.length) + let prevIndex = 0 + + for (let i = 0, l = lines.length; i < l; i++) { + lineToIndex[i] = prevIndex + prevIndex += lines[i].length + 1 + } + + input[lineToIndexCache] = lineToIndex + return lineToIndex +} + +class Input { + get from() { + return this.file || this.id + } + + constructor(css, opts = {}) { + if ( + css === null || + typeof css === 'undefined' || + (typeof css === 'object' && !css.toString) + ) { + throw new Error(`PostCSS received ${css} instead of CSS string`) + } + + this.css = css.toString() + + if (this.css[0] === '\uFEFF' || this.css[0] === '\uFFFE') { + this.hasBOM = true + this.css = this.css.slice(1) + } else { + this.hasBOM = false + } + + this.document = this.css + if (opts.document) this.document = opts.document.toString() + + if (opts.from) { + if ( + !pathAvailable || + /^\w+:\/\//.test(opts.from) || + isAbsolute(opts.from) + ) { + this.file = opts.from + } else { + this.file = resolve(opts.from) + } + } + + if (pathAvailable && sourceMapAvailable) { + let map = new PreviousMap(this.css, opts) + if (map.text) { + this.map = map + let file = map.consumer().file + if (!this.file && file) this.file = this.mapResolve(file) + } + } + + if (!this.file) { + this.id = '' + } + if (this.map) this.map.file = this.from + } + + error(message, line, column, opts = {}) { + let endColumn, endLine, endOffset, offset, result + + if (line && typeof line === 'object') { + let start = line + let end = column + if (typeof start.offset === 'number') { + offset = start.offset + let pos = this.fromOffset(offset) + line = pos.line + column = pos.col + } else { + line = start.line + column = start.column + offset = this.fromLineAndColumn(line, column) + } + if (typeof end.offset === 'number') { + endOffset = end.offset + let pos = this.fromOffset(endOffset) + endLine = pos.line + endColumn = pos.col + } else { + endLine = end.line + endColumn = end.column + endOffset = this.fromLineAndColumn(end.line, end.column) + } + } else if (!column) { + offset = line + let pos = this.fromOffset(offset) + line = pos.line + column = pos.col + } else { + offset = this.fromLineAndColumn(line, column) + } + + let origin = this.origin(line, column, endLine, endColumn) + if (origin) { + result = new CssSyntaxError( + message, + origin.endLine === undefined + ? origin.line + : { column: origin.column, line: origin.line }, + origin.endLine === undefined + ? origin.column + : { column: origin.endColumn, line: origin.endLine }, + origin.source, + origin.file, + opts.plugin + ) + } else { + result = new CssSyntaxError( + message, + endLine === undefined ? line : { column, line }, + endLine === undefined ? column : { column: endColumn, line: endLine }, + this.css, + this.file, + opts.plugin + ) + } + + result.input = { + column, + endColumn, + endLine, + endOffset, + line, + offset, + source: this.css + } + if (this.file) { + if (pathToFileURL) { + result.input.url = pathToFileURL(this.file).toString() + } + result.input.file = this.file + } + + return result + } + + fromLineAndColumn(line, column) { + let lineToIndex = getLineToIndex(this) + let index = lineToIndex[line - 1] + return index + column - 1 + } + + fromOffset(offset) { + let lineToIndex = getLineToIndex(this) + let lastLine = lineToIndex[lineToIndex.length - 1] + + let min = 0 + if (offset >= lastLine) { + min = lineToIndex.length - 1 + } else { + let max = lineToIndex.length - 2 + let mid + while (min < max) { + mid = min + ((max - min) >> 1) + if (offset < lineToIndex[mid]) { + max = mid - 1 + } else if (offset >= lineToIndex[mid + 1]) { + min = mid + 1 + } else { + min = mid + break + } + } + } + return { + col: offset - lineToIndex[min] + 1, + line: min + 1 + } + } + + mapResolve(file) { + if (/^\w+:\/\//.test(file)) { + return file + } + return resolve(this.map.consumer().sourceRoot || this.map.root || '.', file) + } + + origin(line, column, endLine, endColumn) { + if (!this.map) return false + let consumer = this.map.consumer() + + let from = consumer.originalPositionFor({ column: column - 1, line }) + if (!from.source) return false + + let to + if (typeof endLine === 'number') { + let toPosition = consumer.originalPositionFor({ + column: endColumn - 1, + line: endLine + }) + // The source map may not have a mapping that covers the end position + // (`originalPositionFor()` then returns `null` for `line`/`column` + // instead of omitting them). Treat that the same as not requesting + // an end position at all, so `endLine`/`endColumn` stay a consistent + // `undefined` pair instead of a mix of `null` and a bogus number. + if (toPosition.source) to = toPosition + } + + let fromUrl + + if (isAbsolute(from.source)) { + fromUrl = pathToFileURL(from.source) + } else { + fromUrl = new URL( + from.source, + this.map.consumer().sourceRoot || pathToFileURL(this.map.mapFile) + ) + } + + let result = { + column: from.column + 1, + endColumn: to && to.column + 1, + endLine: to && to.line, + line: from.line, + url: fromUrl.toString() + } + + if (fromUrl.protocol === 'file:') { + if (fileURLToPath) { + result.file = fileURLToPath(fromUrl) + } else { + /* c8 ignore next 2 */ + throw new Error(`file: protocol is not available in this PostCSS build`) + } + } + + let source = consumer.sourceContentFor(from.source) + if (source) result.source = source + + return result + } + + toJSON() { + let json = {} + for (let name of ['hasBOM', 'css', 'file', 'id']) { + if (this[name] != null) { + json[name] = this[name] + } + } + if (this.map) { + json.map = { ...this.map } + if (json.map.consumerCache) { + json.map.consumerCache = undefined + } + } + return json + } +} + +module.exports = Input +Input.default = Input + +if (terminalHighlight && terminalHighlight.registerInput) { + terminalHighlight.registerInput(Input) +} diff --git a/lib/lazy-result.d.ts b/lib/lazy-result.d.ts new file mode 100644 index 000000000..599a614bf --- /dev/null +++ b/lib/lazy-result.d.ts @@ -0,0 +1,189 @@ +import Document from './document.js' +import { SourceMap } from './postcss.js' +import Processor from './processor.js' +import Result, { Message, ResultOptions } from './result.js' +import Root from './root.js' +import Warning from './warning.js' + +declare namespace LazyResult { + export { LazyResult_ as default } +} + +/** + * A Promise proxy for the result of PostCSS transformations. + * + * A `LazyResult` instance is returned by `Processor#process`. + * + * ```js + * const lazy = postcss([autoprefixer]).process(css) + * ``` + */ +declare class LazyResult_ implements PromiseLike< + Result +> { + /** + * Processes input CSS through synchronous and asynchronous plugins + * and calls onRejected for each error thrown in any plugin. + * + * It implements standard Promise API. + * + * ```js + * postcss([autoprefixer]).process(css).then(result => { + * console.log(result.css) + * }).catch(error => { + * console.error(error) + * }) + * ``` + */ + catch: Promise>['catch'] + + /** + * Processes input CSS through synchronous and asynchronous plugins + * and calls onFinally on any error or when all plugins will finish work. + * + * It implements standard Promise API. + * + * ```js + * postcss([autoprefixer]).process(css).finally(() => { + * console.log('processing ended') + * }) + * ``` + */ + finally: Promise>['finally'] + + /** + * Processes input CSS through synchronous and asynchronous plugins + * and calls `onFulfilled` with a Result instance. If a plugin throws + * an error, the `onRejected` callback will be executed. + * + * It implements standard Promise API. + * + * ```js + * postcss([autoprefixer]).process(css, { from: cssPath }).then(result => { + * console.log(result.css) + * }) + * ``` + */ + then: Promise>['then'] + + /** + * An alias for the `css` property. Use it with syntaxes + * that generate non-CSS output. + * + * This property will only work with synchronous plugins. + * If the processor contains any asynchronous plugins + * it will throw an error. + * + * PostCSS runners should always use `LazyResult#then`. + */ + get content(): string + + /** + * Processes input CSS through synchronous plugins, converts `Root` + * to a CSS string and returns `Result#css`. + * + * This property will only work with synchronous plugins. + * If the processor contains any asynchronous plugins + * it will throw an error. + * + * PostCSS runners should always use `LazyResult#then`. + */ + get css(): string + + /** + * Processes input CSS through synchronous plugins + * and returns `Result#map`. + * + * This property will only work with synchronous plugins. + * If the processor contains any asynchronous plugins + * it will throw an error. + * + * PostCSS runners should always use `LazyResult#then`. + */ + get map(): SourceMap + + /** + * Processes input CSS through synchronous plugins + * and returns `Result#messages`. + * + * This property will only work with synchronous plugins. If the processor + * contains any asynchronous plugins it will throw an error. + * + * PostCSS runners should always use `LazyResult#then`. + */ + get messages(): Message[] + + /** + * Options from the `Processor#process` call. + */ + get opts(): ResultOptions + + /** + * Returns a `Processor` instance, which will be used + * for CSS transformations. + */ + get processor(): Processor + + /** + * Processes input CSS through synchronous plugins + * and returns `Result#root`. + * + * This property will only work with synchronous plugins. If the processor + * contains any asynchronous plugins it will throw an error. + * + * PostCSS runners should always use `LazyResult#then`. + */ + get root(): RootNode + + /** + * Returns the default string description of an object. + * Required to implement the Promise interface. + */ + get [Symbol.toStringTag](): string + + /** + * @param processor Processor used for this transformation. + * @param css CSS to parse and transform. + * @param opts Options from the `Processor#process` or `Root#toResult`. + */ + constructor(processor: Processor, css: string, opts: ResultOptions) + + /** + * Run plugin in async way and return `Result`. + * + * @return Result with output content. + */ + async(): Promise> + + /** + * Run plugin in sync way and return `Result`. + * + * @return Result with output content. + */ + sync(): Result + + /** + * Alias for the `LazyResult#css` property. + * + * ```js + * lazy + '' === lazy.css + * ``` + * + * @return Output CSS. + */ + toString(): string + + /** + * Processes input CSS through synchronous plugins + * and calls `Result#warnings`. + * + * @return Warnings from plugins. + */ + warnings(): Warning[] +} + +declare class LazyResult< + RootNode = Document | Root +> extends LazyResult_ {} + +export = LazyResult diff --git a/lib/lazy-result.es6 b/lib/lazy-result.es6 deleted file mode 100644 index 1bb6eb110..000000000 --- a/lib/lazy-result.es6 +++ /dev/null @@ -1,360 +0,0 @@ -import MapGenerator from './map-generator'; -import stringify from './stringify'; -import warnOnce from './warn-once'; -import Result from './result'; -import parse from './parse'; - -function isPromise(obj) { - return typeof obj === 'object' && typeof obj.then === 'function'; -} - -/** - * @callback onFulfilled - * @param {Result} result - */ - -/** - * @callback onRejected - * @param {Error} error - */ - -/** - * A Promise proxy for the result of PostCSS transformations. - * - * A `LazyResult` instance is returned by {@link Processor#process}. - * - * @example - * const lazy = postcss([cssnext]).process(css); - */ -class LazyResult { - - constructor(processor, css, opts) { - this.stringified = false; - this.processed = false; - - let root; - if ( typeof css === 'object' && css.type === 'root' ) { - root = css; - } else if ( css instanceof LazyResult || css instanceof Result ) { - root = css.root; - if ( css.map ) { - if ( typeof opts.map === 'undefined' ) opts.map = { }; - if ( !opts.map.inline ) opts.map.inline = false; - opts.map.prev = css.map; - } - } else { - let parser = parse; - if ( opts.syntax ) parser = opts.syntax.parse; - if ( opts.parser ) parser = opts.parser; - if ( parser.parse ) parser = parser.parse; - - try { - root = parser(css, opts); - } catch (error) { - this.error = error; - } - } - - this.result = new Result(processor, root, opts); - } - - /** - * Returns a {@link Processor} instance, which will be used - * for CSS transformations. - * @type {Processor} - */ - get processor() { - return this.result.processor; - } - - /** - * Options from the {@link Processor#process} call. - * @type {processOptions} - */ - get opts() { - return this.result.opts; - } - - /** - * Processes input CSS through synchronous plugins, converts `Root` - * to a CSS string and returns {@link Result#css}. - * - * This property will only work with synchronous plugins. - * If the processor contains any asynchronous plugins - * it will throw an error. This is why this method is only - * for debug purpose, you should always use {@link LazyResult#then}. - * - * @type {string} - * @see Result#css - */ - get css() { - return this.stringify().css; - } - - /** - * An alias for the `css` property. Use it with syntaxes - * that generate non-CSS output. - * - * This property will only work with synchronous plugins. - * If the processor contains any asynchronous plugins - * it will throw an error. This is why this method is only - * for debug purpose, you should always use {@link LazyResult#then}. - * - * @type {string} - * @see Result#content - */ - get content() { - return this.stringify().content; - } - - /** - * Processes input CSS through synchronous plugins - * and returns {@link Result#map}. - * - * This property will only work with synchronous plugins. - * If the processor contains any asynchronous plugins - * it will throw an error. This is why this method is only - * for debug purpose, you should always use {@link LazyResult#then}. - * - * @type {SourceMapGenerator} - * @see Result#map - */ - get map() { - return this.stringify().map; - } - - /** - * Processes input CSS through synchronous plugins - * and returns {@link Result#root}. - * - * This property will only work with synchronous plugins. If the processor - * contains any asynchronous plugins it will throw an error. - * - * This is why this method is only for debug purpose, - * you should always use {@link LazyResult#then}. - * - * @type {Root} - * @see Result#root - */ - get root() { - return this.sync().root; - } - - /** - * Processes input CSS through synchronous plugins - * and returns {@link Result#messages}. - * - * This property will only work with synchronous plugins. If the processor - * contains any asynchronous plugins it will throw an error. - * - * This is why this method is only for debug purpose, - * you should always use {@link LazyResult#then}. - * - * @type {Message[]} - * @see Result#messages - */ - get messages() { - return this.sync().messages; - } - - /** - * Processes input CSS through synchronous plugins - * and calls {@link Result#warnings()}. - * - * @return {Warning[]} warnings from plugins - */ - warnings() { - return this.sync().warnings(); - } - - /** - * Alias for the {@link LazyResult#css} property. - * - * @example - * lazy + '' === lazy.css; - * - * @return {string} output CSS - */ - toString() { - return this.css; - } - - /** - * Processes input CSS through synchronous and asynchronous plugins - * and calls `onFulfilled` with a Result instance. If a plugin throws - * an error, the `onRejected` callback will be executed. - * - * It implements standard Promise API. - * - * @param {onFulfilled} onFulfilled - callback will be executed - * when all plugins will finish work - * @param {onRejected} onRejected - callback will be execited on any error - * - * @return {Promise} Promise API to make queue - * - * @example - * postcss([cssnext]).process(css).then(result => { - * console.log(result.css); - * }); - */ - then(onFulfilled, onRejected) { - return this.async().then(onFulfilled, onRejected); - } - - /** - * Processes input CSS through synchronous and asynchronous plugins - * and calls onRejected for each error thrown in any plugin. - * - * It implements standard Promise API. - * - * @param {onRejected} onRejected - callback will be execited on any error - * - * @return {Promise} Promise API to make queue - * - * @example - * postcss([cssnext]).process(css).then(result => { - * console.log(result.css); - * }).catch(error => { - * console.error(error); - * }); - */ - catch(onRejected) { - return this.async().catch(onRejected); - } - - handleError(error, plugin) { - try { - this.error = error; - if ( error.name === 'CssSyntaxError' && !error.plugin ) { - error.plugin = plugin.postcssPlugin; - error.setMessage(); - } else if ( plugin.postcssVersion ) { - let pluginName = plugin.postcssPlugin; - let pluginVer = plugin.postcssVersion; - let runtimeVer = this.result.processor.version; - let a = pluginVer.split('.'); - let b = runtimeVer.split('.'); - - if ( a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1]) ) { - warnOnce('Your current PostCSS version ' + - 'is ' + runtimeVer + ', but ' + pluginName + ' ' + - 'uses ' + pluginVer + '. Perhaps this is ' + - 'the source of the error below.'); - } - } - } catch (err) { - if ( console && console.error ) console.error(err); - } - } - - asyncTick(resolve, reject) { - if ( this.plugin >= this.processor.plugins.length ) { - this.processed = true; - return resolve(); - } - - try { - let plugin = this.processor.plugins[this.plugin]; - let promise = this.run(plugin); - this.plugin += 1; - - if ( isPromise(promise) ) { - promise.then( () => { - this.asyncTick(resolve, reject); - }).catch( error => { - this.handleError(error, plugin); - this.processed = true; - reject(error); - }); - } else { - this.asyncTick(resolve, reject); - } - - } catch (error) { - this.processed = true; - reject(error); - } - } - - async() { - if ( this.processed ) { - return new Promise( (resolve, reject) => { - if ( this.error ) { - reject(this.error); - } else { - resolve(this.stringify()); - } - }); - } - if ( this.processing ) { - return this.processing; - } - - this.processing = new Promise( (resolve, reject) => { - if ( this.error ) return reject(this.error); - this.plugin = 0; - this.asyncTick(resolve, reject); - }).then( () => { - this.processed = true; - return this.stringify(); - }); - - return this.processing; - } - - sync() { - if ( this.processed ) return this.result; - this.processed = true; - - if ( this.processing ) { - throw new Error( - 'Use process(css).then(cb) to work with async plugins'); - } - - if ( this.error ) throw this.error; - - for ( let plugin of this.result.processor.plugins ) { - let promise = this.run(plugin); - if ( isPromise(promise) ) { - throw new Error( - 'Use process(css).then(cb) to work with async plugins'); - } - } - - return this.result; - } - - run(plugin) { - this.result.lastPlugin = plugin; - - try { - return plugin(this.result.root, this.result); - } catch (error) { - this.handleError(error, plugin); - throw error; - } - } - - stringify() { - if ( this.stringified ) return this.result; - this.stringified = true; - - this.sync(); - - let opts = this.result.opts; - let str = stringify; - if ( opts.syntax ) str = opts.syntax.stringify; - if ( opts.stringifier ) str = opts.stringifier; - if ( str.stringify ) str = str.stringify; - - let map = new MapGenerator(str, this.result.root, this.result.opts); - let data = map.generate(); - this.result.css = data[0]; - this.result.map = data[1]; - - return this.result; - } - -} - -export default LazyResult; diff --git a/lib/lazy-result.js b/lib/lazy-result.js new file mode 100644 index 000000000..be695b299 --- /dev/null +++ b/lib/lazy-result.js @@ -0,0 +1,626 @@ +'use strict' + +let Container = require('./container') +let Document = require('./document') +let MapGenerator = require('./map-generator') +let parse = require('./parse') +let Result = require('./result') +let Root = require('./root') +let stringify = require('./stringify') +let { isClean, my } = require('./symbols') +let warnOnce = require('./warn-once') + +const TYPE_TO_CLASS_NAME = { + atrule: 'AtRule', + comment: 'Comment', + decl: 'Declaration', + document: 'Document', + root: 'Root', + rule: 'Rule' +} + +const PLUGIN_PROPS = { + AtRule: true, + AtRuleExit: true, + Comment: true, + CommentExit: true, + Declaration: true, + DeclarationExit: true, + Document: true, + DocumentExit: true, + Once: true, + OnceExit: true, + postcssPlugin: true, + prepare: true, + Root: true, + RootExit: true, + Rule: true, + RuleExit: true +} + +const NOT_VISITORS = { + Once: true, + postcssPlugin: true, + prepare: true +} + +const CHILDREN = 0 + +function isPromise(obj) { + return typeof obj === 'object' && typeof obj.then === 'function' +} + +function getEvents(node) { + let key = false + let type = TYPE_TO_CLASS_NAME[node.type] + if (node.type === 'decl') { + key = node.prop.toLowerCase() + } else if (node.type === 'atrule') { + key = node.name.toLowerCase() + } + + if (key && node.append) { + return [ + type, + type + '-' + key, + CHILDREN, + type + 'Exit', + type + 'Exit-' + key + ] + } else if (key) { + return [type, type + '-' + key, type + 'Exit', type + 'Exit-' + key] + } else if (node.append) { + return [type, CHILDREN, type + 'Exit'] + } else { + return [type, type + 'Exit'] + } +} + +function toStack(node) { + let events + if (node.type === 'document') { + events = ['Document', CHILDREN, 'DocumentExit'] + } else if (node.type === 'root') { + events = ['Root', CHILDREN, 'RootExit'] + } else { + events = getEvents(node) + } + + return { + eventIndex: 0, + events, + iterator: 0, + node, + visitorIndex: 0, + visitors: [] + } +} + +function cleanMarks(node) { + let stack = [node] + while (stack.length > 0) { + let next = stack.pop() + next[isClean] = false + if (next.nodes) { + for (let i of next.nodes) stack.push(i) + } + } + return node +} + +let postcss = {} + +class LazyResult { + get content() { + return this.stringify().content + } + + get css() { + return this.stringify().css + } + + get map() { + return this.stringify().map + } + + get messages() { + return this.sync().messages + } + + get opts() { + return this.result.opts + } + + get processor() { + return this.result.processor + } + + get root() { + return this.sync().root + } + + get [Symbol.toStringTag]() { + return 'LazyResult' + } + + constructor(processor, css, opts) { + this.stringified = false + this.processed = false + + let root + if ( + typeof css === 'object' && + css !== null && + (css.type === 'root' || css.type === 'document') + ) { + root = cleanMarks(css) + } else if (css instanceof LazyResult || css instanceof Result) { + root = cleanMarks(css.root) + if (css.map) { + if (typeof opts.map === 'undefined') opts.map = {} + if (!opts.map.inline) opts.map.inline = false + opts.map.prev = css.map + } + } else { + let parser = parse + if (opts.syntax) parser = opts.syntax.parse + if (opts.parser) parser = opts.parser + if (parser.parse) parser = parser.parse + + try { + root = parser(css, opts) + } catch (error) { + this.processed = true + this.error = error + } + + if (root && !root[my]) { + /* c8 ignore next 2 */ + Container.rebuild(root) + } + } + + this.result = new Result(processor, root, opts) + this.helpers = { ...postcss, postcss, result: this.result } + this.plugins = this.processor.plugins.map(plugin => { + if (typeof plugin === 'object' && plugin.prepare) { + return { ...plugin, ...plugin.prepare(this.result) } + } else { + return plugin + } + }) + } + + async() { + if (this.error) return Promise.reject(this.error) + if (this.processed) return Promise.resolve(this.result) + if (!this.processing) { + this.processing = this.runAsync() + } + return this.processing + } + + catch(onRejected) { + return this.async().catch(onRejected) + } + + finally(onFinally) { + return this.async().then(onFinally, onFinally) + } + + getAsyncError() { + throw new Error('Use process(css).then(cb) to work with async plugins') + } + + handleError(error, node) { + let plugin = this.result.lastPlugin + try { + if (node) node.addToError(error) + this.error = error + if (error.name === 'CssSyntaxError' && !error.plugin) { + error.plugin = plugin.postcssPlugin + error.setMessage() + } else if (plugin.postcssVersion) { + if (process.env.NODE_ENV !== 'production') { + let pluginName = plugin.postcssPlugin + let pluginVer = plugin.postcssVersion + let runtimeVer = this.result.processor.version + let a = pluginVer.split('.') + let b = runtimeVer.split('.') + + if (a[0] !== b[0] || parseInt(a[1]) > parseInt(b[1])) { + // eslint-disable-next-line no-console + console.error( + 'Unknown error from PostCSS plugin. Your current PostCSS ' + + 'version is ' + + runtimeVer + + ', but ' + + pluginName + + ' uses ' + + pluginVer + + '. Perhaps this is the source of the error below.' + ) + } + } + } + } catch (err) { + /* c8 ignore next 3 */ + // eslint-disable-next-line no-console + if (console && console.error) console.error(err) + } + return error + } + + prepareVisitors() { + this.listeners = {} + let add = (plugin, type, cb) => { + if (!this.listeners[type]) this.listeners[type] = [] + this.listeners[type].push([plugin, cb]) + } + for (let plugin of this.plugins) { + if (typeof plugin === 'object') { + for (let event in plugin) { + if (!PLUGIN_PROPS[event] && /^[A-Z]/.test(event)) { + throw new Error( + `Unknown event ${event} in ${plugin.postcssPlugin}. ` + + `Try to update PostCSS (${this.processor.version} now).` + ) + } + if (!NOT_VISITORS[event]) { + if (typeof plugin[event] === 'object') { + for (let filter in plugin[event]) { + if (filter === '*') { + add(plugin, event, plugin[event][filter]) + } else { + add( + plugin, + event + '-' + filter.toLowerCase(), + plugin[event][filter] + ) + } + } + } else if (typeof plugin[event] === 'function') { + add(plugin, event, plugin[event]) + } + } + } + } + } + this.hasListener = Object.keys(this.listeners).length > 0 + } + + async runAsync() { + this.plugin = 0 + for (let i = 0; i < this.plugins.length; i++) { + let plugin = this.plugins[i] + let promise = this.runOnRoot(plugin) + if (isPromise(promise)) { + try { + await promise + } catch (error) { + throw this.handleError(error) + } + } + } + + this.prepareVisitors() + if (this.hasListener) { + let root = this.result.root + while (!root[isClean]) { + root[isClean] = true + let stack = [toStack(root)] + while (stack.length > 0) { + let promise = this.visitTick(stack) + if (isPromise(promise)) { + try { + await promise + } catch (e) { + let node = stack[stack.length - 1].node + throw this.handleError(e, node) + } + } + } + } + + if (this.listeners.OnceExit) { + for (let [plugin, visitor] of this.listeners.OnceExit) { + this.result.lastPlugin = plugin + try { + if (root.type === 'document') { + let roots = root.nodes.map(subRoot => + visitor(subRoot, this.helpers) + ) + + await Promise.all(roots) + } else { + await visitor(root, this.helpers) + } + } catch (e) { + throw this.handleError(e) + } + } + } + } + + this.processed = true + return this.stringify() + } + + runOnRoot(plugin) { + this.result.lastPlugin = plugin + try { + if (typeof plugin === 'object' && plugin.Once) { + if (this.result.root.type === 'document') { + let roots = this.result.root.nodes.map(root => + plugin.Once(root, this.helpers) + ) + + if (isPromise(roots[0])) { + return Promise.all(roots) + } + + return roots + } + + return plugin.Once(this.result.root, this.helpers) + } else if (typeof plugin === 'function') { + return plugin(this.result.root, this.result) + } + } catch (error) { + throw this.handleError(error) + } + } + + stringify() { + if (this.error) throw this.error + if (this.stringified) return this.result + this.stringified = true + + this.sync() + + let opts = this.result.opts + let str = stringify + if (opts.syntax) str = opts.syntax.stringify + if (opts.stringifier) str = opts.stringifier + if (str.stringify) str = str.stringify + + let rootSource = this.result.root.source + if ( + opts.map === undefined && + !(rootSource && rootSource.input && rootSource.input.map) + ) { + let result = '' + str(this.result.root, i => { + result += i + }) + this.result.css = result + return this.result + } + + let map = new MapGenerator(str, this.result.root, this.result.opts) + let data = map.generate() + this.result.css = data[0] + this.result.map = data[1] + + return this.result + } + + sync() { + if (this.error) throw this.error + if (this.processed) return this.result + this.processed = true + + if (this.processing) { + throw this.getAsyncError() + } + + for (let plugin of this.plugins) { + let promise = this.runOnRoot(plugin) + if (isPromise(promise)) { + throw this.getAsyncError() + } + } + + this.prepareVisitors() + if (this.hasListener) { + let root = this.result.root + while (!root[isClean]) { + root[isClean] = true + this.walkSync(root) + } + if (this.listeners.OnceExit) { + if (root.type === 'document') { + for (let subRoot of root.nodes) { + this.visitSync(this.listeners.OnceExit, subRoot) + } + } else { + this.visitSync(this.listeners.OnceExit, root) + } + } + } + + return this.result + } + + then(onFulfilled, onRejected) { + if (process.env.NODE_ENV !== 'production') { + if (!('from' in this.opts)) { + warnOnce( + 'Without `from` option PostCSS could generate wrong source map ' + + 'and will not find Browserslist config. Set it to CSS file path ' + + 'or to `undefined` to prevent this warning.' + ) + } + } + return this.async().then(onFulfilled, onRejected) + } + + toString() { + return this.css + } + + visitSync(visitors, node) { + for (let [plugin, visitor] of visitors) { + this.result.lastPlugin = plugin + let promise + try { + promise = visitor(node, this.helpers) + } catch (e) { + throw this.handleError(e, node.proxyOf) + } + if (node.type !== 'root' && node.type !== 'document' && !node.parent) { + return true + } + if (isPromise(promise)) { + throw this.getAsyncError() + } + } + } + + visitTick(stack) { + let visit = stack[stack.length - 1] + let { node, visitors } = visit + + if (node.type !== 'root' && node.type !== 'document' && !node.parent) { + stack.pop() + return + } + + if (visitors.length > 0 && visit.visitorIndex < visitors.length) { + let [plugin, visitor] = visitors[visit.visitorIndex] + visit.visitorIndex += 1 + if (visit.visitorIndex === visitors.length) { + visit.visitors = [] + visit.visitorIndex = 0 + } + this.result.lastPlugin = plugin + try { + return visitor(node.toProxy(), this.helpers) + } catch (e) { + throw this.handleError(e, node) + } + } + + if (visit.iterator !== 0) { + let iterator = visit.iterator + // Advance past the child we just finished visiting. Like + // `Container#each`, the index is incremented only after a child has + // been fully processed, so a node inserted right after the current + // child is not skipped by the `existIndex < index` adjustment in + // `Container#insertAfter()` (which would fire exit events too early). + if (visit.descending) { + visit.descending = false + node.indexes[iterator] += 1 + } + let child + while ((child = node.nodes[node.indexes[iterator]])) { + if (!child[isClean]) { + child[isClean] = true + visit.descending = true + stack.push(toStack(child)) + return + } + node.indexes[iterator] += 1 + } + visit.iterator = 0 + delete node.indexes[iterator] + } + + let events = visit.events + while (visit.eventIndex < events.length) { + let event = events[visit.eventIndex] + visit.eventIndex += 1 + if (event === CHILDREN) { + if (node.nodes && node.nodes.length) { + node[isClean] = true + visit.iterator = node.getIterator() + } + return + } else if (this.listeners[event]) { + visit.visitors = this.listeners[event] + return + } + } + stack.pop() + } + + walkSync(node) { + // An explicit stack like in async `visitTick()` to survive deeply + // nested trees. Unlike `visitTick()`, nodes are marked clean only + // on entering, so a node dirtied by its own visitors is revisited + // on the next pass. + node[isClean] = true + let stack = [{ eventIndex: 0, events: getEvents(node), iterator: 0, node }] + + while (stack.length > 0) { + let visit = stack[stack.length - 1] + let visitNode = visit.node + + if (visit.iterator !== 0) { + let iterator = visit.iterator + // Advance past the child we just finished visiting. Like + // `Container#each`, the index is incremented only after a child has + // been fully processed. Incrementing before (as this loop used to) + // makes a node inserted right after the current child get skipped by + // the `existIndex < index` adjustment in `Container#insertAfter()`, + // which fires exit events before those new siblings are visited. + if (visit.descending) { + visit.descending = false + visitNode.indexes[iterator] += 1 + } + let child + let descended = false + while ((child = visitNode.nodes[visitNode.indexes[iterator]])) { + if (!child[isClean]) { + child[isClean] = true + visit.descending = true + stack.push({ + eventIndex: 0, + events: getEvents(child), + iterator: 0, + node: child + }) + descended = true + break + } + visitNode.indexes[iterator] += 1 + } + if (descended) continue + visit.iterator = 0 + delete visitNode.indexes[iterator] + } + + if (visit.eventIndex < visit.events.length) { + let event = visit.events[visit.eventIndex] + visit.eventIndex += 1 + if (event === CHILDREN) { + if (visitNode.nodes && visitNode.nodes.length) { + visit.iterator = visitNode.getIterator() + } + } else { + let visitors = this.listeners[event] + if (visitors) { + if (this.visitSync(visitors, visitNode.toProxy())) stack.pop() + } + } + continue + } + + stack.pop() + } + } + + warnings() { + return this.sync().warnings() + } +} + +LazyResult.registerPostcss = dependant => { + postcss = dependant +} + +module.exports = LazyResult +LazyResult.default = LazyResult + +Root.registerLazyResult(LazyResult) +Document.registerLazyResult(LazyResult) diff --git a/lib/list.d.ts b/lib/list.d.ts new file mode 100644 index 000000000..119624e87 --- /dev/null +++ b/lib/list.d.ts @@ -0,0 +1,60 @@ +declare namespace list { + type List = { + /** + * Safely splits comma-separated values (such as those for `transition-*` + * and `background` properties). + * + * ```js + * Once (root, { list }) { + * list.comma('black, linear-gradient(white, black)') + * //=> ['black', 'linear-gradient(white, black)'] + * } + * ``` + * + * @param str Comma-separated values. + * @return Split values. + */ + comma(str: string): string[] + + default: List + + /** + * Safely splits space-separated values (such as those for `background`, + * `border-radius`, and other shorthand properties). + * + * ```js + * Once (root, { list }) { + * list.space('1px calc(10% + 1px)') //=> ['1px', 'calc(10% + 1px)'] + * } + * ``` + * + * @param str Space-separated values. + * @return Split values. + */ + space(str: string): string[] + + /** + * Safely splits values. + * + * ```js + * Once (root, { list }) { + * list.split('1px calc(10% + 1px)', [' ', '\n', '\t']) //=> ['1px', 'calc(10% + 1px)'] + * } + * ``` + * + * @param string separated values. + * @param separators array of separators. + * @param last boolean indicator. + * @return Split values. + */ + split( + string: string, + separators: readonly string[], + last: boolean + ): string[] + } +} + +declare let list: list.List + +export = list diff --git a/lib/list.es6 b/lib/list.es6 deleted file mode 100644 index 48b1bcbbd..000000000 --- a/lib/list.es6 +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Contains helpers for safely splitting lists of CSS values, - * preserving parentheses and quotes. - * - * @example - * const list = postcss.list; - * - * @namespace list - */ -let list = { - - split(string, separators, last) { - let array = []; - let current = ''; - let split = false; - - let func = 0; - let quote = false; - let escape = false; - - for ( let i = 0; i < string.length; i++ ) { - let letter = string[i]; - - if ( quote ) { - if ( escape ) { - escape = false; - } else if ( letter === '\\' ) { - escape = true; - } else if ( letter === quote ) { - quote = false; - } - } else if ( letter === '"' || letter === '\'' ) { - quote = letter; - } else if ( letter === '(' ) { - func += 1; - } else if ( letter === ')' ) { - if ( func > 0 ) func -= 1; - } else if ( func === 0 ) { - if ( separators.indexOf(letter) !== -1 ) split = true; - } - - if ( split ) { - if ( current !== '' ) array.push(current.trim()); - current = ''; - split = false; - } else { - current += letter; - } - } - - if ( last || current !== '' ) array.push(current.trim()); - return array; - }, - - /** - * Safely splits space-separated values (such as those for `background`, - * `border-radius`, and other shorthand properties). - * - * @param {string} string - space-separated values - * - * @return {string[]} splitted values - * - * @example - * postcss.list.space('1px calc(10% + 1px)') //=> ['1px', 'calc(10% + 1px)'] - */ - space(string) { - let spaces = [' ', '\n', '\t']; - return list.split(string, spaces); - }, - - /** - * Safely splits comma-separated values (such as those for `transition-*` - * and `background` properties). - * - * @param {string} string - comma-separated values - * - * @return {string[]} splitted values - * - * @example - * postcss.list.comma('black, linear-gradient(white, black)') - * //=> ['black', 'linear-gradient(white, black)'] - */ - comma(string) { - let comma = ','; - return list.split(string, [comma], true); - } - -}; - -export default list; diff --git a/lib/list.js b/lib/list.js new file mode 100644 index 000000000..3e879d7f1 --- /dev/null +++ b/lib/list.js @@ -0,0 +1,59 @@ +'use strict' + +let list = { + comma(string) { + return list.split(string, [','], true) + }, + + space(string) { + let spaces = [' ', '\n', '\t'] + return list.split(string, spaces) + }, + + split(string, separators, last) { + if (typeof string !== 'string') return [] + let array = [] + let current = '' + let split = false + + let func = 0 + let inQuote = false + let prevQuote = '' + let escape = false + + for (let letter of string) { + if (escape) { + escape = false + } else if (letter === '\\') { + escape = true + } else if (inQuote) { + if (letter === prevQuote) { + inQuote = false + } + } else if (letter === '"' || letter === "'") { + inQuote = true + prevQuote = letter + } else if (letter === '(') { + func += 1 + } else if (letter === ')') { + if (func > 0) func -= 1 + } else if (func === 0) { + if (separators.includes(letter)) split = true + } + + if (split) { + if (last || current !== '') array.push(current.trim()) + current = '' + split = false + } else { + current += letter + } + } + + if (last || current !== '') array.push(current.trim()) + return array + } +} + +module.exports = list +list.default = list diff --git a/lib/map-generator.es6 b/lib/map-generator.es6 deleted file mode 100644 index 368f018af..000000000 --- a/lib/map-generator.es6 +++ /dev/null @@ -1,266 +0,0 @@ -import { Base64 } from 'js-base64'; -import mozilla from 'source-map'; -import path from 'path'; - -export default class MapGenerator { - - constructor(stringify, root, opts) { - this.stringify = stringify; - this.mapOpts = opts.map || { }; - this.root = root; - this.opts = opts; - } - - isMap() { - if ( typeof this.opts.map !== 'undefined' ) { - return !!this.opts.map; - } else { - return this.previous().length > 0; - } - } - - previous() { - if ( !this.previousMaps ) { - this.previousMaps = []; - this.root.walk( node => { - if ( node.source && node.source.input.map ) { - let map = node.source.input.map; - if ( this.previousMaps.indexOf(map) === -1 ) { - this.previousMaps.push(map); - } - } - }); - } - - return this.previousMaps; - } - - isInline() { - if ( typeof this.mapOpts.inline !== 'undefined' ) { - return this.mapOpts.inline; - } - - let annotation = this.mapOpts.annotation; - if ( typeof annotation !== 'undefined' && annotation !== true ) { - return false; - } - - if ( this.previous().length ) { - return this.previous().some( i => i.inline ); - } else { - return true; - } - } - - isSourcesContent() { - if ( typeof this.mapOpts.sourcesContent !== 'undefined' ) { - return this.mapOpts.sourcesContent; - } - if ( this.previous().length ) { - return this.previous().some( i => i.withContent() ); - } else { - return true; - } - } - - clearAnnotation() { - if ( this.mapOpts.annotation === false ) return; - - let node; - for ( let i = this.root.nodes.length - 1; i >= 0; i-- ) { - node = this.root.nodes[i]; - if ( node.type !== 'comment' ) continue; - if ( node.text.indexOf('# sourceMappingURL=') === 0 ) { - this.root.removeChild(i); - } - } - } - - setSourcesContent() { - let already = { }; - this.root.walk( node => { - if ( node.source ) { - let from = node.source.input.from; - if ( from && !already[from] ) { - already[from] = true; - let relative = this.relative(from); - this.map.setSourceContent(relative, node.source.input.css); - } - } - }); - } - - applyPrevMaps() { - for ( let prev of this.previous() ) { - let from = this.relative(prev.file); - let root = prev.root || path.dirname(prev.file); - let map; - - if ( this.mapOpts.sourcesContent === false ) { - map = new mozilla.SourceMapConsumer(prev.text); - if ( map.sourcesContent ) { - map.sourcesContent = map.sourcesContent.map( () => null ); - } - } else { - map = prev.consumer(); - } - - this.map.applySourceMap(map, from, this.relative(root)); - } - } - - isAnnotation() { - if ( this.isInline() ) { - return true; - } else if ( typeof this.mapOpts.annotation !== 'undefined' ) { - return this.mapOpts.annotation; - } else if ( this.previous().length ) { - return this.previous().some( i => i.annotation ); - } else { - return true; - } - } - - addAnnotation() { - let content; - - if ( this.isInline() ) { - content = 'data:application/json;base64,' + - Base64.encode( this.map.toString() ); - - } else if ( typeof this.mapOpts.annotation === 'string' ) { - content = this.mapOpts.annotation; - - } else { - content = this.outputFile() + '.map'; - } - - let eol = '\n'; - if ( this.css.indexOf('\r\n') !== -1 ) eol = '\r\n'; - - this.css += eol + '/*# sourceMappingURL=' + content + ' */'; - } - - outputFile() { - if ( this.opts.to ) { - return this.relative(this.opts.to); - } else if ( this.opts.from ) { - return this.relative(this.opts.from); - } else { - return 'to.css'; - } - } - - generateMap() { - this.generateString(); - if ( this.isSourcesContent() ) this.setSourcesContent(); - if ( this.previous().length > 0 ) this.applyPrevMaps(); - if ( this.isAnnotation() ) this.addAnnotation(); - - if ( this.isInline() ) { - return [this.css]; - } else { - return [this.css, this.map]; - } - } - - relative(file) { - if ( /^\w+:\/\//.test(file) ) return file; - - let from = this.opts.to ? path.dirname(this.opts.to) : '.'; - - if ( typeof this.mapOpts.annotation === 'string' ) { - from = path.dirname( path.resolve(from, this.mapOpts.annotation) ); - } - - file = path.relative(from, file); - if ( path.sep === '\\' ) { - return file.replace(/\\/g, '/'); - } else { - return file; - } - } - - sourcePath(node) { - if ( this.mapOpts.from ) { - return this.mapOpts.from; - } else { - return this.relative(node.source.input.from); - } - } - - generateString() { - this.css = ''; - this.map = new mozilla.SourceMapGenerator({ file: this.outputFile() }); - - let line = 1; - let column = 1; - - let lines, last; - this.stringify(this.root, (str, node, type) => { - this.css += str; - - if ( node && type !== 'end' ) { - if ( node.source && node.source.start ) { - this.map.addMapping({ - source: this.sourcePath(node), - generated: { line, column: column - 1 }, - original: { - line: node.source.start.line, - column: node.source.start.column - 1 - } - }); - } else { - this.map.addMapping({ - source: '', - original: { line: 1, column: 0 }, - generated: { line, column: column - 1 } - }); - } - } - - lines = str.match(/\n/g); - if ( lines ) { - line += lines.length; - last = str.lastIndexOf('\n'); - column = str.length - last; - } else { - column += str.length; - } - - if ( node && type !== 'start' ) { - if ( node.source && node.source.end ) { - this.map.addMapping({ - source: this.sourcePath(node), - generated: { line, column: column - 1 }, - original: { - line: node.source.end.line, - column: node.source.end.column - } - }); - } else { - this.map.addMapping({ - source: '', - original: { line: 1, column: 0 }, - generated: { line, column: column - 1 } - }); - } - } - }); - } - - generate() { - this.clearAnnotation(); - - if ( this.isMap() ) { - return this.generateMap(); - } else { - let result = ''; - this.stringify(this.root, i => { - result += i; - }); - return [result]; - } - } - -} diff --git a/lib/map-generator.js b/lib/map-generator.js new file mode 100644 index 000000000..df880ac99 --- /dev/null +++ b/lib/map-generator.js @@ -0,0 +1,376 @@ +'use strict' + +let { dirname, relative, resolve, sep } = require('path') +let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js') +let { pathToFileURL } = require('url') + +let Input = require('./input') + +let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator) +let pathAvailable = Boolean(dirname && resolve && relative && sep) + +class MapGenerator { + constructor(stringify, root, opts, cssString) { + this.stringify = stringify + this.mapOpts = opts.map || {} + this.root = root + this.opts = opts + this.css = cssString + this.originalCSS = cssString + this.usesFileUrls = !this.mapOpts.from && this.mapOpts.absolute + + this.memoizedFileURLs = new Map() + this.memoizedPaths = new Map() + this.memoizedURLs = new Map() + } + + addAnnotation() { + let content + + if (this.isInline()) { + content = + 'data:application/json;base64,' + this.toBase64(this.map.toString()) + } else if (typeof this.mapOpts.annotation === 'string') { + content = this.mapOpts.annotation + } else if (typeof this.mapOpts.annotation === 'function') { + content = this.mapOpts.annotation(this.opts.to, this.root) + } else { + content = this.outputFile() + '.map' + } + let eol = '\n' + if (this.css.includes('\r\n')) eol = '\r\n' + + this.css += eol + '/*# sourceMappingURL=' + content + ' */' + } + + applyPrevMaps() { + for (let prev of this.previous()) { + let from = this.toUrl(this.path(prev.file)) + let root = prev.root || dirname(prev.file) + let map + + if (this.mapOpts.sourcesContent === false) { + map = new SourceMapConsumer(prev.text) + if (map.sourcesContent) { + map.sourcesContent = null + } + } else { + map = prev.consumer() + } + + this.map.applySourceMap(map, from, this.toUrl(this.path(root))) + } + } + + clearAnnotation() { + if (this.mapOpts.annotation === false) return + + if (this.root) { + let node + for (let i = this.root.nodes.length - 1; i >= 0; i--) { + node = this.root.nodes[i] + if (node.type !== 'comment') continue + if (node.text.startsWith('# sourceMappingURL=')) { + this.root.removeChild(i) + } + } + } else if (this.css) { + let startIndex + while ((startIndex = this.css.lastIndexOf('/*#')) !== -1) { + let endIndex = this.css.indexOf('*/', startIndex + 3) + if (endIndex === -1) break + while (startIndex > 0 && this.css[startIndex - 1] === '\n') { + startIndex-- + } + this.css = this.css.slice(0, startIndex) + this.css.slice(endIndex + 2) + } + } + } + + generate() { + this.clearAnnotation() + if (pathAvailable && sourceMapAvailable && this.isMap()) { + return this.generateMap() + } else { + let result = '' + this.stringify(this.root, i => { + result += i + }) + return [result] + } + } + + generateMap() { + if (this.root) { + this.generateString() + } else if (this.previous().length === 1) { + let prev = this.previous()[0].consumer() + prev.file = this.outputFile() + this.map = SourceMapGenerator.fromSourceMap(prev, { + ignoreInvalidMapping: true + }) + } else { + this.map = new SourceMapGenerator({ + file: this.outputFile(), + ignoreInvalidMapping: true + }) + this.map.addMapping({ + generated: { column: 0, line: 1 }, + original: { column: 0, line: 1 }, + source: this.opts.from + ? this.toUrl(this.path(this.opts.from)) + : '' + }) + } + + if (this.isSourcesContent()) this.setSourcesContent() + if (this.root && this.previous().length > 0) this.applyPrevMaps() + if (this.isAnnotation()) this.addAnnotation() + + if (this.isInline()) { + return [this.css] + } else { + return [this.css, this.map] + } + } + + generateString() { + this.css = '' + this.map = new SourceMapGenerator({ + file: this.outputFile(), + ignoreInvalidMapping: true + }) + + let line = 1 + let column = 1 + + let noSource = '' + let mapping = { + generated: { column: 0, line: 0 }, + original: { column: 0, line: 0 }, + source: '' + } + + let last, lines + this.stringify(this.root, (str, node, type) => { + this.css += str + + if (node && type !== 'end') { + mapping.generated.line = line + mapping.generated.column = column - 1 + if (node.source && node.source.start) { + mapping.source = this.sourcePath(node) + mapping.original.line = node.source.start.line + mapping.original.column = node.source.start.column - 1 + this.map.addMapping(mapping) + } else { + mapping.source = noSource + mapping.original.line = 1 + mapping.original.column = 0 + this.map.addMapping(mapping) + } + } + + lines = str.match(/\n/g) + if (lines) { + line += lines.length + last = str.lastIndexOf('\n') + column = str.length - last + } else { + column += str.length + } + + if (node && type !== 'start') { + let p = node.parent || { raws: {} } + let childless = + node.type === 'decl' || (node.type === 'atrule' && !node.nodes) + if (!childless || node !== p.last || p.raws.semicolon) { + if (node.source && node.source.end) { + mapping.source = this.sourcePath(node) + mapping.original.line = node.source.end.line + mapping.original.column = node.source.end.column - 1 + mapping.generated.line = line + mapping.generated.column = column - 2 + this.map.addMapping(mapping) + } else { + mapping.source = noSource + mapping.original.line = 1 + mapping.original.column = 0 + mapping.generated.line = line + mapping.generated.column = column - 1 + this.map.addMapping(mapping) + } + } + } + }) + } + + isAnnotation() { + if (this.isInline()) { + return true + } + if (typeof this.mapOpts.annotation !== 'undefined') { + return this.mapOpts.annotation + } + if (this.previous().length) { + return this.previous().some(i => i.annotation) + } + return true + } + + isInline() { + if (typeof this.mapOpts.inline !== 'undefined') { + return this.mapOpts.inline + } + + let annotation = this.mapOpts.annotation + if (typeof annotation !== 'undefined' && annotation !== true) { + return false + } + + if (this.previous().length) { + return this.previous().some(i => i.inline) + } + return true + } + + isMap() { + if (typeof this.opts.map !== 'undefined') { + return !!this.opts.map + } + return this.previous().length > 0 + } + + isSourcesContent() { + if (typeof this.mapOpts.sourcesContent !== 'undefined') { + return this.mapOpts.sourcesContent + } + if (this.previous().length) { + return this.previous().some(i => i.withContent()) + } + return true + } + + outputFile() { + if (this.opts.to) { + return this.path(this.opts.to) + } else if (this.opts.from) { + return this.path(this.opts.from) + } else { + return 'to.css' + } + } + + path(file) { + if (this.mapOpts.absolute) return file + if (file.charCodeAt(0) === 60 /* `<` */) return file + if (/^\w+:\/\//.test(file)) return file + let cached = this.memoizedPaths.get(file) + if (cached) return cached + + let from = this.opts.to ? dirname(this.opts.to) : '.' + + if (typeof this.mapOpts.annotation === 'string') { + from = dirname(resolve(from, this.mapOpts.annotation)) + } + + let path = relative(from, file) + this.memoizedPaths.set(file, path) + + return path + } + + previous() { + if (!this.previousMaps) { + this.previousMaps = [] + if (this.root) { + this.root.walk(node => { + if (node.source && node.source.input.map) { + let map = node.source.input.map + if (!this.previousMaps.includes(map)) { + this.previousMaps.push(map) + } + } + }) + } else { + let input = new Input(this.originalCSS, this.opts) + if (input.map) this.previousMaps.push(input.map) + } + } + + return this.previousMaps + } + + setSourcesContent() { + let already = {} + if (this.root) { + this.root.walk(node => { + if (node.source) { + let from = node.source.input.from + if (from && !already[from]) { + already[from] = true + let fromUrl = this.usesFileUrls + ? this.toFileUrl(from) + : this.toUrl(this.path(from)) + this.map.setSourceContent(fromUrl, node.source.input.css) + } + } + }) + } else if (this.css) { + let from = this.opts.from + ? this.toUrl(this.path(this.opts.from)) + : '' + this.map.setSourceContent(from, this.css) + } + } + + sourcePath(node) { + if (this.mapOpts.from) { + return this.toUrl(this.mapOpts.from) + } else if (this.usesFileUrls) { + return this.toFileUrl(node.source.input.from) + } else { + return this.toUrl(this.path(node.source.input.from)) + } + } + + toBase64(str) { + if (Buffer) { + return Buffer.from(str).toString('base64') + } else { + return window.btoa(unescape(encodeURIComponent(str))) + } + } + + toFileUrl(path) { + let cached = this.memoizedFileURLs.get(path) + if (cached) return cached + + if (pathToFileURL) { + let fileURL = pathToFileURL(path).toString() + this.memoizedFileURLs.set(path, fileURL) + + return fileURL + } else { + throw new Error( + '`map.absolute` option is not available in this PostCSS build' + ) + } + } + + toUrl(path) { + let cached = this.memoizedURLs.get(path) + if (cached) return cached + + if (sep === '\\') { + path = path.replace(/\\/g, '/') + } + + let url = encodeURI(path).replace(/[#?]/g, encodeURIComponent) + this.memoizedURLs.set(path, url) + + return url + } +} + +module.exports = MapGenerator diff --git a/lib/no-work-result.d.ts b/lib/no-work-result.d.ts new file mode 100644 index 000000000..fa9d28484 --- /dev/null +++ b/lib/no-work-result.d.ts @@ -0,0 +1,45 @@ +import LazyResult from './lazy-result.js' +import { SourceMap } from './postcss.js' +import Processor from './processor.js' +import Result, { Message, ResultOptions } from './result.js' +import Root from './root.js' +import Warning from './warning.js' + +declare namespace NoWorkResult { + export { NoWorkResult_ as default } +} + +/** + * A Promise proxy for the result of PostCSS transformations. + * This lazy result instance doesn't parse css unless `NoWorkResult#root` or `Result#root` + * are accessed. See the example below for details. + * A `NoWork` instance is returned by `Processor#process` ONLY when no plugins defined. + * + * ```js + * const noWorkResult = postcss().process(css) // No plugins are defined. + * // CSS is not parsed + * let root = noWorkResult.root // now css is parsed because we accessed the root + * ``` + */ +declare class NoWorkResult_ implements LazyResult { + catch: Promise>['catch'] + finally: Promise>['finally'] + then: Promise>['then'] + get content(): string + get css(): string + get map(): SourceMap + get messages(): Message[] + get opts(): ResultOptions + get processor(): Processor + get root(): Root + get [Symbol.toStringTag](): string + constructor(processor: Processor, css: string, opts: ResultOptions) + async(): Promise> + sync(): Result + toString(): string + warnings(): Warning[] +} + +declare class NoWorkResult extends NoWorkResult_ {} + +export = NoWorkResult diff --git a/lib/no-work-result.js b/lib/no-work-result.js new file mode 100644 index 000000000..7ec1a7425 --- /dev/null +++ b/lib/no-work-result.js @@ -0,0 +1,137 @@ +'use strict' + +let MapGenerator = require('./map-generator') +let parse = require('./parse') +let Result = require('./result') +let stringify = require('./stringify') +let warnOnce = require('./warn-once') + +class NoWorkResult { + get content() { + return this.result.css + } + + get css() { + return this.result.css + } + + get map() { + return this.result.map + } + + get messages() { + return [] + } + + get opts() { + return this.result.opts + } + + get processor() { + return this.result.processor + } + + get root() { + if (this._root) { + return this._root + } + + let root + let parser = parse + + try { + root = parser(this._css, this._opts) + } catch (error) { + this.error = error + } + + if (this.error) { + throw this.error + } else { + this._root = root + return root + } + } + + get [Symbol.toStringTag]() { + return 'NoWorkResult' + } + + constructor(processor, css, opts) { + css = css.toString() + this.stringified = false + + this._processor = processor + this._css = css + this._opts = opts + this._map = undefined + + let str = stringify + this.result = new Result(this._processor, undefined, this._opts) + this.result.css = css + + let self = this + Object.defineProperty(this.result, 'root', { + get() { + return self.root + } + }) + + let map = new MapGenerator(str, undefined, this._opts, css) + if (map.isMap()) { + let [generatedCSS, generatedMap] = map.generate() + if (generatedCSS) { + this.result.css = generatedCSS + } + if (generatedMap) { + this.result.map = generatedMap + } + } else { + map.clearAnnotation() + this.result.css = map.css + } + } + + async() { + if (this.error) return Promise.reject(this.error) + return Promise.resolve(this.result) + } + + catch(onRejected) { + return this.async().catch(onRejected) + } + + finally(onFinally) { + return this.async().then(onFinally, onFinally) + } + + sync() { + if (this.error) throw this.error + return this.result + } + + then(onFulfilled, onRejected) { + if (process.env.NODE_ENV !== 'production') { + if (!('from' in this._opts)) { + warnOnce( + 'Without `from` option PostCSS could generate wrong source map ' + + 'and will not find Browserslist config. Set it to CSS file path ' + + 'or to `undefined` to prevent this warning.' + ) + } + } + + return this.async().then(onFulfilled, onRejected) + } + + toString() { + return this._css + } + + warnings() { + return [] + } +} + +module.exports = NoWorkResult +NoWorkResult.default = NoWorkResult diff --git a/lib/node.d.ts b/lib/node.d.ts new file mode 100644 index 000000000..e93ce5bae --- /dev/null +++ b/lib/node.d.ts @@ -0,0 +1,555 @@ +import AtRule = require('./at-rule.js') +import { AtRuleProps } from './at-rule.js' +import Comment, { CommentProps } from './comment.js' +import Container, { NewChild } from './container.js' +import CssSyntaxError from './css-syntax-error.js' +import Declaration, { DeclarationProps } from './declaration.js' +import Document from './document.js' +import Input from './input.js' +import { Stringifier, Syntax } from './postcss.js' +import Result from './result.js' +import Root from './root.js' +import Rule, { RuleProps } from './rule.js' +import Warning, { WarningOptions } from './warning.js' + +declare namespace Node { + export type ChildNode = AtRule.default | Comment | Declaration | Rule + + export type AnyNode = + | AtRule.default + | Comment + | Declaration + | Document + | Root + | Rule + + export type ChildProps = + | AtRuleProps + | CommentProps + | DeclarationProps + | RuleProps + + export interface Position { + /** + * Source column in file. It starts from 1. + */ + column: number + + /** + * Source line in file. It starts from 1. + */ + line: number + + /** + * Source offset in file. It starts from 0. + */ + offset: number + } + + export interface Range { + /** + * End position, exclusive. + */ + end: Position + + /** + * Start position, inclusive. + */ + start: Position + } + + /** + * Source represents an interface for the {@link Node.source} property. + */ + export interface Source { + /** + * The inclusive ending position for the source + * code of a node. + * + * However, `end.offset` of a non `Root` node is the exclusive position. + * See https://github.com/postcss/postcss/pull/1879 for details. + * + * ```js + * const root = postcss.parse('a { color: black }') + * const a = root.first + * const color = a.first + * + * // The offset of `Root` node is the inclusive position + * css.source.end // { line: 1, column: 19, offset: 18 } + * + * // The offset of non `Root` node is the exclusive position + * a.source.end // { line: 1, column: 18, offset: 18 } + * color.source.end // { line: 1, column: 16, offset: 16 } + * ``` + */ + end?: Position + + /** + * The source file from where a node has originated. + */ + input: Input + + /** + * The inclusive starting position for the source + * code of a node. + */ + start?: Position + } + + /** + * Interface represents an interface for an object received + * as parameter by Node class constructor. + */ + export interface NodeProps { + source?: Source + } + + export interface NodeErrorOptions { + /** + * An ending index inside a node's string that should be highlighted as + * source of error. + */ + endIndex?: number + /** + * An index inside a node's string that should be highlighted as source + * of error. + */ + index?: number + /** + * Plugin name that created this error. PostCSS will set it automatically. + */ + plugin?: string + /** + * A word inside a node's string, that should be highlighted as source + * of error. + */ + word?: string + } + + class Node extends Node_ {} + export { Node as default } +} + +/** + * It represents an abstract class that handles common + * methods for other CSS abstract syntax tree nodes. + * + * Any node that represents CSS selector or value should + * not extend the `Node` class. + */ +declare abstract class Node_ { + /** + * It represents parent of the current node. + * + * ```js + * root.nodes[0].parent === root //=> true + * ``` + */ + parent: Container | Document | undefined + + /** + * It represents unnecessary whitespace and characters present + * in the css source code. + * + * Information to generate byte-to-byte equal node string as it was + * in the origin input. + * + * The properties of the raws object are decided by parser, + * the default parser uses the following properties: + * + * * `before`: the space symbols before the node. It also stores `*` + * and `_` symbols before the declaration (IE hack). + * * `after`: the space symbols after the last child of the node + * to the end of the node. + * * `between`: the symbols between the property and value + * for declarations, selector and `{` for rules, or last parameter + * and `{` for at-rules. + * * `semicolon`: contains true if the last child has + * an (optional) semicolon. + * * `afterName`: the space between the at-rule name and its parameters. + * * `left`: the space symbols between `/*` and the comment’s text. + * * `right`: the space symbols between the comment’s text + * and */. + * - `important`: the content of the important statement, + * if it is not just `!important`. + * + * PostCSS filters out the comments inside selectors, declaration values + * and at-rule parameters but it stores the origin content in raws. + * + * ```js + * const root = postcss.parse('a {\n color:black\n}') + * root.first.first.raws //=> { before: '\n ', between: ':' } + * ``` + */ + raws: any + + /** + * It represents information related to origin of a node and is required + * for generating source maps. + * + * The nodes that are created manually using the public APIs + * provided by PostCSS will have `source` undefined and + * will be absent in the source map. + * + * For this reason, the plugin developer should consider + * duplicating nodes as the duplicate node will have the + * same source as the original node by default or assign + * source to a node created manually. + * + * ```js + * decl.source.input.from //=> '/home/ai/source.css' + * decl.source.start //=> { line: 10, column: 2 } + * decl.source.end //=> { line: 10, column: 12 } + * ``` + * + * ```js + * // Incorrect method, source not specified! + * const prefixed = postcss.decl({ + * prop: '-moz-' + decl.prop, + * value: decl.value + * }) + * + * // Correct method, source is inherited when duplicating. + * const prefixed = decl.clone({ + * prop: '-moz-' + decl.prop + * }) + * ``` + * + * ```js + * if (atrule.name === 'add-link') { + * const rule = postcss.rule({ + * selector: 'a', + * source: atrule.source + * }) + * + * atrule.parent.insertBefore(atrule, rule) + * } + * ``` + */ + source?: Node.Source + + /** + * It represents type of a node in + * an abstract syntax tree. + * + * A type of node helps in identification of a node + * and perform operation based on it's type. + * + * ```js + * const declaration = new Declaration({ + * prop: 'color', + * value: 'black' + * }) + * + * declaration.type //=> 'decl' + * ``` + */ + type: string + + constructor(defaults?: object) + + /** + * Insert new node after current node to current node’s parent. + * + * Just alias for `node.parent.insertAfter(node, add)`. + * + * ```js + * decl.after('color: black') + * ``` + * + * @param newNode New node. + * @return This node for methods chain. + */ + after( + newNode: Node | Node.ChildProps | readonly Node[] | string | undefined + ): this + + /** + * It assigns properties to an existing node instance. + * + * ```js + * decl.assign({ prop: 'word-wrap', value: 'break-word' }) + * ``` + * + * @param overrides New properties to override the node. + * + * @return `this` for method chaining. + */ + assign(overrides: object): this + + /** + * Insert new node before current node to current node’s parent. + * + * Just alias for `node.parent.insertBefore(node, add)`. + * + * ```js + * decl.before('content: ""') + * ``` + * + * @param newNode New node. + * @return This node for methods chain. + */ + before( + newNode: Node | Node.ChildProps | readonly Node[] | string | undefined + ): this + + /** + * Clear the code style properties for the node and its children. + * + * ```js + * node.raws.before //=> ' ' + * node.cleanRaws() + * node.raws.before //=> undefined + * ``` + * + * @param keepBetween Keep the `raws.between` symbols. + */ + cleanRaws(keepBetween?: boolean): void + + /** + * It creates clone of an existing node, which includes all the properties + * and their values, that includes `raws` but not `type`. + * + * ```js + * decl.raws.before //=> "\n " + * const cloned = decl.clone({ prop: '-moz-' + decl.prop }) + * cloned.raws.before //=> "\n " + * cloned.toString() //=> -moz-transform: scale(0) + * ``` + * + * @param overrides New properties to override in the clone. + * + * @return Duplicate of the node instance. + */ + clone(overrides?: object): this + + /** + * Shortcut to clone the node and insert the resulting cloned node + * after the current node. + * + * @param overrides New properties to override in the clone. + * @return New node. + */ + cloneAfter(overrides?: object): this + + /** + * Shortcut to clone the node and insert the resulting cloned node + * before the current node. + * + * ```js + * decl.cloneBefore({ prop: '-moz-' + decl.prop }) + * ``` + * + * @param overrides Mew properties to override in the clone. + * + * @return New node + */ + cloneBefore(overrides?: object): this + + /** + * It creates an instance of the class `CssSyntaxError` and parameters passed + * to this method are assigned to the error instance. + * + * The error instance will have description for the + * error, original position of the node in the + * source, showing line and column number. + * + * If any previous map is present, it would be used + * to get original position of the source. + * + * The Previous Map here is referred to the source map + * generated by previous compilation, example: Less, + * Stylus and Sass. + * + * This method returns the error instance instead of + * throwing it. + * + * ```js + * if (!variables[name]) { + * throw decl.error(`Unknown variable ${name}`, { word: name }) + * // CssSyntaxError: postcss-vars:a.sass:4:3: Unknown variable $black + * // color: $black + * // a + * // ^ + * // background: white + * } + * ``` + * + * @param message Description for the error instance. + * @param options Options for the error instance. + * + * @return Error instance is returned. + */ + error(message: string, options?: Node.NodeErrorOptions): CssSyntaxError + + /** + * Returns the next child of the node’s parent. + * Returns `undefined` if the current node is the last child. + * + * ```js + * if (comment.text === 'delete next') { + * const next = comment.next() + * if (next) { + * next.remove() + * } + * } + * ``` + * + * @return Next node. + */ + next(): Node.ChildNode | undefined + + /** + * Get the position for a word or an index inside the node. + * + * @param opts Options. + * @return Position. + */ + positionBy(opts?: Pick): Node.Position + + /** + * Convert string index to line/column. + * + * @param index The symbol number in the node’s string. + * @return Symbol position in file. + */ + positionInside(index: number): Node.Position + + /** + * Returns the previous child of the node’s parent. + * Returns `undefined` if the current node is the first child. + * + * ```js + * const annotation = decl.prev() + * if (annotation.type === 'comment') { + * readAnnotation(annotation.text) + * } + * ``` + * + * @return Previous node. + */ + prev(): Node.ChildNode | undefined + + /** + * Get the range for a word or start and end index inside the node. + * The start index is inclusive; the end index is exclusive. + * + * @param opts Options. + * @return Range. + */ + rangeBy( + opts?: Pick + ): Node.Range + + /** + * Returns a `raws` value. If the node is missing + * the code style property (because the node was manually built or cloned), + * PostCSS will try to autodetect the code style property by looking + * at other nodes in the tree. + * + * ```js + * const root = postcss.parse('a { background: white }') + * root.nodes[0].append({ prop: 'color', value: 'black' }) + * root.nodes[0].nodes[1].raws.before //=> undefined + * root.nodes[0].nodes[1].raw('before') //=> ' ' + * ``` + * + * @param prop Name of code style property. + * @param defaultType Name of default value, it can be missed + * if the value is the same as prop. + * @return {string} Code style value. + */ + raw(prop: string, defaultType?: string): string + + /** + * It removes the node from its parent and deletes its parent property. + * + * ```js + * if (decl.prop.match(/^-webkit-/)) { + * decl.remove() + * } + * ``` + * + * @return `this` for method chaining. + */ + remove(): this + + /** + * Inserts node(s) before the current node and removes the current node. + * + * ```js + * AtRule: { + * mixin: atrule => { + * atrule.replaceWith(mixinRules[atrule.params]) + * } + * } + * ``` + * + * @param nodes Mode(s) to replace current one. + * @return Current node to methods chain. + */ + replaceWith(...nodes: NewChild[]): this + + /** + * Finds the Root instance of the node’s tree. + * + * ```js + * root.nodes[0].nodes[0].root() === root + * ``` + * + * @return Root parent. + */ + root(): Root + + /** + * Fix circular links on `JSON.stringify()`. + * + * @return Cleaned object. + */ + toJSON(): object + + /** + * It compiles the node to browser readable cascading style sheets string + * depending on it's type. + * + * ```js + * new Rule({ selector: 'a' }).toString() //=> "a {}" + * ``` + * + * @param stringifier A syntax to use in string generation. + * @return CSS string of this node. + */ + toString(stringifier?: Stringifier | Syntax): string + + /** + * It is a wrapper for {@link Result#warn}, providing convenient + * way of generating warnings. + * + * ```js + * Declaration: { + * bad: (decl, { result }) => { + * decl.warn(result, 'Deprecated property: bad') + * } + * } + * ``` + * + * @param result The `Result` instance that will receive the warning. + * @param message Description for the warning. + * @param options Options for the warning. + * + * @return `Warning` instance is returned + */ + warn(result: Result, message: string, options?: WarningOptions): Warning + + /** + * If this node isn't already dirty, marks it and its ancestors as such. This + * indicates to the LazyResult processor that the {@link Root} has been + * modified by the current plugin and may need to be processed again by other + * plugins. + */ + protected markDirty(): void +} + +declare class Node extends Node_ {} + +export = Node diff --git a/lib/node.es6 b/lib/node.es6 deleted file mode 100644 index 5dac49075..000000000 --- a/lib/node.es6 +++ /dev/null @@ -1,575 +0,0 @@ -import CssSyntaxError from './css-syntax-error'; -import Stringifier from './stringifier'; -import stringify from './stringify'; -import warnOnce from './warn-once'; - -/** - * @typedef {object} position - * @property {number} line - source line in file - * @property {number} column - source column in file - */ - -/** - * @typedef {object} source - * @property {Input} input - {@link Input} with input file - * @property {position} start - The starting position of the node’s source - * @property {position} end - The ending position of the node’s source - */ - -let cloneNode = function (obj, parent) { - let cloned = new obj.constructor(); - - for ( let i in obj ) { - if ( !obj.hasOwnProperty(i) ) continue; - let value = obj[i]; - let type = typeof value; - - if ( i === 'parent' && type === 'object' ) { - if (parent) cloned[i] = parent; - } else if ( i === 'source' ) { - cloned[i] = value; - } else if ( value instanceof Array ) { - cloned[i] = value.map( j => cloneNode(j, cloned) ); - } else if ( i !== 'before' && i !== 'after' && - i !== 'between' && i !== 'semicolon' ) { - if ( type === 'object' && value !== null ) value = cloneNode(value); - cloned[i] = value; - } - } - - return cloned; -}; - -/** - * All node classes inherit the following common methods. - * - * @abstract - * @ignore - */ -class Node { - - /** - * @param {object} [defaults] - value for node properties - */ - constructor(defaults = { }) { - this.raws = { }; - for ( let name in defaults ) { - this[name] = defaults[name]; - } - } - - /** - * Returns a CssSyntaxError instance containing the original position - * of the node in the source, showing line and column numbers and also - * a small excerpt to facilitate debugging. - * - * If present, an input source map will be used to get the original position - * of the source, even from a previous compilation step - * (e.g., from Sass compilation). - * - * This method produces very useful error messages. - * - * @param {string} message - error description - * @param {object} [opts] - options - * @param {string} opts.plugin - plugin name that created this error. - * PostCSS will set it automatically. - * @param {string} opts.word - a word inside a node’s string that should - * be highlighted as the source of the error - * @param {number} opts.index - an index inside a node’s string that should - * be highlighted as the source of the error - * - * @return {CssSyntaxError} error object to throw it - * - * @example - * if ( !variables[name] ) { - * throw decl.error('Unknown variable ' + name, { word: name }); - * // CssSyntaxError: postcss-vars:a.sass:4:3: Unknown variable $black - * // color: $black - * // a - * // ^ - * // background: white - * } - */ - error(message, opts = { }) { - if ( this.source ) { - let pos = this.positionBy(opts); - return this.source.input.error(message, pos.line, pos.column, opts); - } else { - return new CssSyntaxError(message); - } - } - - /** - * This method is provided as a convenience wrapper for {@link Result#warn}. - * - * @param {Result} result - the {@link Result} instance - * that will receive the warning - * @param {string} text - warning message - * @param {object} [opts] - options - * @param {string} opts.plugin - plugin name that created this warning. - * PostCSS will set it automatically. - * @param {string} opts.word - a word inside a node’s string that should - * be highlighted as the source of the warning - * @param {number} opts.index - an index inside a node’s string that should - * be highlighted as the source of the warning - * - * @return {Warning} created warning object - * - * @example - * const plugin = postcss.plugin('postcss-deprecated', () => { - * return (css, result) => { - * css.walkDecls('bad', decl => { - * decl.warn(result, 'Deprecated property bad'); - * }); - * }; - * }); - */ - warn(result, text, opts) { - let data = { node: this }; - for ( let i in opts ) data[i] = opts[i]; - return result.warn(text, data); - } - - /** - * Removes the node from its parent and cleans the parent properties - * from the node and its children. - * - * @example - * if ( decl.prop.match(/^-webkit-/) ) { - * decl.remove(); - * } - * - * @return {Node} node to make calls chain - */ - remove() { - if ( this.parent ) { - this.parent.removeChild(this); - } - this.parent = undefined; - return this; - } - - /** - * Returns a CSS string representing the node. - * - * @param {stringifier|syntax} [stringifier] - a syntax to use - * in string generation - * - * @return {string} CSS string of this node - * - * @example - * postcss.rule({ selector: 'a' }).toString() //=> "a {}" - */ - toString(stringifier = stringify) { - if ( stringifier.stringify ) stringifier = stringifier.stringify; - let result = ''; - stringifier(this, i => { - result += i; - }); - return result; - } - - /** - * Returns a clone of the node. - * - * The resulting cloned node and its (cloned) children will have - * a clean parent and code style properties. - * - * @param {object} [overrides] - new properties to override in the clone. - * - * @example - * const cloned = decl.clone({ prop: '-moz-' + decl.prop }); - * cloned.raws.before //=> undefined - * cloned.parent //=> undefined - * cloned.toString() //=> -moz-transform: scale(0) - * - * @return {Node} clone of the node - */ - clone(overrides = { }) { - let cloned = cloneNode(this); - for ( let name in overrides ) { - cloned[name] = overrides[name]; - } - return cloned; - } - - /** - * Shortcut to clone the node and insert the resulting cloned node - * before the current node. - * - * @param {object} [overrides] - new properties to override in the clone. - * - * @example - * decl.cloneBefore({ prop: '-moz-' + decl.prop }); - * - * @return {Node} - new node - */ - cloneBefore(overrides = { }) { - let cloned = this.clone(overrides); - this.parent.insertBefore(this, cloned); - return cloned; - } - - /** - * Shortcut to clone the node and insert the resulting cloned node - * after the current node. - * - * @param {object} [overrides] - new properties to override in the clone. - * - * @return {Node} - new node - */ - cloneAfter(overrides = { }) { - let cloned = this.clone(overrides); - this.parent.insertAfter(this, cloned); - return cloned; - } - - /** - * Inserts node(s) before the current node and removes the current node. - * - * @param {...Node} nodes - node(s) to replace current one - * - * @example - * if ( atrule.name == 'mixin' ) { - * atrule.replaceWith(mixinRules[atrule.params]); - * } - * - * @return {Node} current node to methods chain - */ - replaceWith(...nodes) { - if (this.parent) { - for (let node of nodes) { - this.parent.insertBefore(this, node); - } - - this.remove(); - } - - return this; - } - - /** - * Removes the node from its current parent and inserts it - * at the end of `newParent`. - * - * This will clean the `before` and `after` code {@link Node#raws} data - * from the node and replace them with the indentation style of `newParent`. - * It will also clean the `between` property - * if `newParent` is in another {@link Root}. - * - * @param {Container} newParent - container node where the current node - * will be moved - * - * @example - * atrule.moveTo(atrule.root()); - * - * @return {Node} current node to methods chain - */ - moveTo(newParent) { - this.cleanRaws(this.root() === newParent.root()); - this.remove(); - newParent.append(this); - return this; - } - - /** - * Removes the node from its current parent and inserts it into - * a new parent before `otherNode`. - * - * This will also clean the node’s code style properties just as it would - * in {@link Node#moveTo}. - * - * @param {Node} otherNode - node that will be before current node - * - * @return {Node} current node to methods chain - */ - moveBefore(otherNode) { - this.cleanRaws(this.root() === otherNode.root()); - this.remove(); - otherNode.parent.insertBefore(otherNode, this); - return this; - } - - /** - * Removes the node from its current parent and inserts it into - * a new parent after `otherNode`. - * - * This will also clean the node’s code style properties just as it would - * in {@link Node#moveTo}. - * - * @param {Node} otherNode - node that will be after current node - * - * @return {Node} current node to methods chain - */ - moveAfter(otherNode) { - this.cleanRaws(this.root() === otherNode.root()); - this.remove(); - otherNode.parent.insertAfter(otherNode, this); - return this; - } - - /** - * Returns the next child of the node’s parent. - * Returns `undefined` if the current node is the last child. - * - * @return {Node|undefined} next node - * - * @example - * if ( comment.text === 'delete next' ) { - * const next = comment.next(); - * if ( next ) { - * next.remove(); - * } - * } - */ - next() { - let index = this.parent.index(this); - return this.parent.nodes[index + 1]; - } - - /** - * Returns the previous child of the node’s parent. - * Returns `undefined` if the current node is the first child. - * - * @return {Node|undefined} previous node - * - * @example - * const annotation = decl.prev(); - * if ( annotation.type == 'comment' ) { - * readAnnotation(annotation.text); - * } - */ - prev() { - let index = this.parent.index(this); - return this.parent.nodes[index - 1]; - } - - toJSON() { - let fixed = { }; - - for ( let name in this ) { - if ( !this.hasOwnProperty(name) ) continue; - if ( name === 'parent' ) continue; - let value = this[name]; - - if ( value instanceof Array ) { - fixed[name] = value.map( i => { - if ( typeof i === 'object' && i.toJSON ) { - return i.toJSON(); - } else { - return i; - } - }); - } else if ( typeof value === 'object' && value.toJSON ) { - fixed[name] = value.toJSON(); - } else { - fixed[name] = value; - } - } - - return fixed; - } - - /** - * Returns a {@link Node#raws} value. If the node is missing - * the code style property (because the node was manually built or cloned), - * PostCSS will try to autodetect the code style property by looking - * at other nodes in the tree. - * - * @param {string} prop - name of code style property - * @param {string} [defaultType] - name of default value, it can be missed - * if the value is the same as prop - * - * @example - * const root = postcss.parse('a { background: white }'); - * root.nodes[0].append({ prop: 'color', value: 'black' }); - * root.nodes[0].nodes[1].raws.before //=> undefined - * root.nodes[0].nodes[1].raw('before') //=> ' ' - * - * @return {string} code style value - */ - raw(prop, defaultType) { - let str = new Stringifier(); - return str.raw(this, prop, defaultType); - } - - /** - * Finds the Root instance of the node’s tree. - * - * @example - * root.nodes[0].nodes[0].root() === root - * - * @return {Root} root parent - */ - root() { - let result = this; - while ( result.parent ) result = result.parent; - return result; - } - - cleanRaws(keepBetween) { - delete this.raws.before; - delete this.raws.after; - if ( !keepBetween ) delete this.raws.between; - } - - positionInside(index) { - let string = this.toString(); - let column = this.source.start.column; - let line = this.source.start.line; - - for ( let i = 0; i < index; i++ ) { - if ( string[i] === '\n' ) { - column = 1; - line += 1; - } else { - column += 1; - } - } - - return { line, column }; - } - - positionBy(opts) { - let pos = this.source.start; - if ( opts.index ) { - pos = this.positionInside(opts.index); - } else if ( opts.word ) { - let index = this.toString().indexOf(opts.word); - if ( index !== -1 ) pos = this.positionInside(index); - } - return pos; - } - - removeSelf() { - warnOnce('Node#removeSelf is deprecated. Use Node#remove.'); - return this.remove(); - } - - replace(nodes) { - warnOnce('Node#replace is deprecated. Use Node#replaceWith'); - return this.replaceWith(nodes); - } - - style(own, detect) { - warnOnce('Node#style() is deprecated. Use Node#raw()'); - return this.raw(own, detect); - } - - cleanStyles(keepBetween) { - warnOnce('Node#cleanStyles() is deprecated. Use Node#cleanRaws()'); - return this.cleanRaws(keepBetween); - } - - get before() { - warnOnce('Node#before is deprecated. Use Node#raws.before'); - return this.raws.before; - } - - set before(val) { - warnOnce('Node#before is deprecated. Use Node#raws.before'); - this.raws.before = val; - } - - get between() { - warnOnce('Node#between is deprecated. Use Node#raws.between'); - return this.raws.between; - } - - set between(val) { - warnOnce('Node#between is deprecated. Use Node#raws.between'); - this.raws.between = val; - } - - /** - * @memberof Node# - * @member {string} type - String representing the node’s type. - * Possible values are `root`, `atrule`, `rule`, - * `decl`, or `comment`. - * - * @example - * postcss.decl({ prop: 'color', value: 'black' }).type //=> 'decl' - */ - - /** - * @memberof Node# - * @member {Container} parent - the node’s parent node. - * - * @example - * root.nodes[0].parent == root; - */ - - /** - * @memberof Node# - * @member {source} source - the input source of the node - * - * The property is used in source map generation. - * - * If you create a node manually (e.g., with `postcss.decl()`), - * that node will not have a `source` property and will be absent - * from the source map. For this reason, the plugin developer should - * consider cloning nodes to create new ones (in which case the new node’s - * source will reference the original, cloned node) or setting - * the `source` property manually. - * - * ```js - * // Bad - * const prefixed = postcss.decl({ - * prop: '-moz-' + decl.prop, - * value: decl.value - * }); - * - * // Good - * const prefixed = decl.clone({ prop: '-moz-' + decl.prop }); - * ``` - * - * ```js - * if ( atrule.name == 'add-link' ) { - * const rule = postcss.rule({ selector: 'a', source: atrule.source }); - * atrule.parent.insertBefore(atrule, rule); - * } - * ``` - * - * @example - * decl.source.input.from //=> '/home/ai/a.sass' - * decl.source.start //=> { line: 10, column: 2 } - * decl.source.end //=> { line: 10, column: 12 } - */ - - /** - * @memberof Node# - * @member {object} raws - Information to generate byte-to-byte equal - * node string as it was in the origin input. - * - * Every parser saves its own properties, - * but the default CSS parser uses: - * - * * `before`: the space symbols before the node. It also stores `*` - * and `_` symbols before the declaration (IE hack). - * * `after`: the space symbols after the last child of the node - * to the end of the node. - * * `between`: the symbols between the property and value - * for declarations, selector and `{` for rules, or last parameter - * and `{` for at-rules. - * * `semicolon`: contains true if the last child has - * an (optional) semicolon. - * * `afterName`: the space between the at-rule name and its parameters. - * * `left`: the space symbols between `/*` and the comment’s text. - * * `right`: the space symbols between the comment’s text - * and */. - * * `important`: the content of the important statement, - * if it is not just `!important`. - * - * PostCSS cleans selectors, declaration values and at-rule parameters - * from comments and extra spaces, but it stores origin content in raws - * properties. As such, if you don’t change a declaration’s value, - * PostCSS will use the raw value with comments. - * - * @example - * const root = postcss.parse('a {\n color:black\n}') - * root.first.first.raws //=> { before: '\n ', between: ':' } - */ - -} - -export default Node; diff --git a/lib/node.js b/lib/node.js new file mode 100644 index 000000000..a298d3656 --- /dev/null +++ b/lib/node.js @@ -0,0 +1,494 @@ +'use strict' + +let CssSyntaxError = require('./css-syntax-error') +let Stringifier = require('./stringifier') +let stringify = require('./stringify') +let { isClean, my } = require('./symbols') + +function cloneNode(obj, parent) { + let cloned = new obj.constructor() + // An explicit stack instead of recursive calls to survive deeply + // nested trees. Each entry is [source, its clone, clone's parent]. + let stack = [[obj, cloned, parent]] + + while (stack.length > 0) { + let [source, target, targetParent] = stack.pop() + for (let i in source) { + if (!Object.prototype.hasOwnProperty.call(source, i)) { + /* c8 ignore next 2 */ + continue + } + if (i === 'proxyCache') continue + let value = source[i] + let type = typeof value + + if (i === 'parent' && type === 'object') { + if (targetParent) target[i] = targetParent + } else if (i === 'source') { + target[i] = value + } else if (Array.isArray(value)) { + let children = [] + target[i] = children + for (let j of value) { + let childClone = new j.constructor() + children.push(childClone) + stack.push([j, childClone, target]) + } + } else { + if (type === 'object' && value !== null) { + let valueClone = new value.constructor() + stack.push([value, valueClone, undefined]) + value = valueClone + } + target[i] = value + } + } + } + + return cloned +} + +function sourceOffset(inputCSS, position) { + // Not all custom syntaxes support `offset` in `source.start` and `source.end` + if (position && typeof position.offset !== 'undefined') { + return position.offset + } + + let column = 1 + let line = 1 + let offset = 0 + + for (let i = 0; i < inputCSS.length; i++) { + if (line === position.line && column === position.column) { + offset = i + break + } + + if (inputCSS[i] === '\n') { + column = 1 + line += 1 + } else { + column += 1 + } + } + + return offset +} + +class Node { + get proxyOf() { + return this + } + + constructor(defaults = {}) { + this.raws = {} + this[isClean] = false + this[my] = true + + for (let name of Object.keys(defaults)) { + if (name === '__proto__') continue + if (name === 'nodes') { + this.nodes = [] + for (let node of defaults[name]) { + // Clone only nodes that already belong to another tree, so passing a + // freshly created (parent-less) node adopts that instance instead of + // a copy and keeps the caller's reference usable. See #1987. + if (typeof node.clone === 'function' && node.parent) { + this.append(node.clone()) + } else { + this.append(node) + } + } + } else { + this[name] = defaults[name] + } + } + } + + addToError(error) { + error.postcssNode = this + if (error.stack && this.source && /\n\s{4}at /.test(error.stack)) { + let s = this.source + error.stack = error.stack.replace( + /\n\s{4}at /, + `$&${s.input.from}:${s.start.line}:${s.start.column}$&` + ) + } + return error + } + + after(add) { + this.parent.insertAfter(this, add) + return this + } + + assign(overrides = {}) { + for (let name in overrides) { + this[name] = overrides[name] + } + return this + } + + before(add) { + this.parent.insertBefore(this, add) + return this + } + + cleanRaws(keepBetween) { + delete this.raws.before + delete this.raws.after + if (!keepBetween) delete this.raws.between + } + + clone(overrides = {}) { + let cloned = cloneNode(this) + for (let name in overrides) { + cloned[name] = overrides[name] + } + return cloned + } + + cloneAfter(overrides = {}) { + let cloned = this.clone(overrides) + this.parent.insertAfter(this, cloned) + return cloned + } + + cloneBefore(overrides = {}) { + let cloned = this.clone(overrides) + this.parent.insertBefore(this, cloned) + return cloned + } + + error(message, opts = {}) { + if (this.source) { + let { end, start } = this.rangeBy(opts) + return this.source.input.error( + message, + { column: start.column, line: start.line }, + { column: end.column, line: end.line }, + opts + ) + } + return new CssSyntaxError(message) + } + + getProxyProcessor() { + return { + get(node, prop) { + if (prop === 'proxyOf') { + return node + } else if (prop === 'root') { + return () => node.root().toProxy() + } else { + return node[prop] + } + }, + + set(node, prop, value) { + if (node[prop] === value) return true + node[prop] = value + if ( + prop === 'prop' || + prop === 'value' || + prop === 'name' || + prop === 'params' || + prop === 'important' || + /* c8 ignore next */ + prop === 'text' + ) { + node.markDirty() + } + return true + } + } + } + + /* c8 ignore next 3 */ + markClean() { + this[isClean] = true + } + + markDirty() { + if (this[isClean]) { + this[isClean] = false + let next = this + while ((next = next.parent)) { + next[isClean] = false + } + } + } + + next() { + if (!this.parent) return undefined + let index = this.parent.index(this) + return this.parent.nodes[index + 1] + } + + positionBy(opts = {}) { + let inputString = + 'document' in this.source.input + ? this.source.input.document + : this.source.input.css + let pos = { + column: this.source.start.column, + line: this.source.start.line, + offset: sourceOffset(inputString, this.source.start) + } + if (opts.index) { + pos = this.positionInside(opts.index) + } else if (opts.word) { + let stringRepresentation = inputString.slice( + sourceOffset(inputString, this.source.start), + sourceOffset(inputString, this.source.end) + ) + let index = stringRepresentation.indexOf(opts.word) + if (index !== -1) pos = this.positionInside(index) + } + return pos + } + + positionInside(index) { + let column = this.source.start.column + let line = this.source.start.line + let inputString = + 'document' in this.source.input + ? this.source.input.document + : this.source.input.css + let offset = sourceOffset(inputString, this.source.start) + let end = offset + index + + for (let i = offset; i < end; i++) { + if (inputString[i] === '\n') { + column = 1 + line += 1 + } else { + column += 1 + } + } + + return { column, line, offset: end } + } + + prev() { + if (!this.parent) return undefined + let index = this.parent.index(this) + return this.parent.nodes[index - 1] + } + + rangeBy(opts = {}) { + let inputString = + 'document' in this.source.input + ? this.source.input.document + : this.source.input.css + let start = { + column: this.source.start.column, + line: this.source.start.line, + offset: sourceOffset(inputString, this.source.start) + } + let end = this.source.end + ? { + column: this.source.end.column + 1, + line: this.source.end.line, + offset: + typeof this.source.end.offset === 'number' + ? // `source.end.offset` is exclusive, so we don't need to add 1 + this.source.end.offset + : // Since line/column in this.source.end is inclusive, + // the `sourceOffset(... , this.source.end)` returns an inclusive offset. + // So, we add 1 to convert it to exclusive. + sourceOffset(inputString, this.source.end) + 1 + } + : { + column: start.column + 1, + line: start.line, + offset: start.offset + 1 + } + + if (opts.word) { + let stringRepresentation = inputString.slice( + sourceOffset(inputString, this.source.start), + sourceOffset(inputString, this.source.end) + ) + let index = stringRepresentation.indexOf(opts.word) + if (index !== -1) { + start = this.positionInside(index) + end = this.positionInside(index + opts.word.length) + } + } else { + if (opts.start) { + start = { + column: opts.start.column, + line: opts.start.line, + offset: sourceOffset(inputString, opts.start) + } + } else if (typeof opts.index === 'number') { + start = this.positionInside(opts.index) + } + + if (opts.end) { + end = { + column: opts.end.column, + line: opts.end.line, + offset: sourceOffset(inputString, opts.end) + } + } else if (typeof opts.endIndex === 'number') { + end = this.positionInside(opts.endIndex) + } else if (typeof opts.index === 'number') { + end = this.positionInside(opts.index + 1) + } + } + + if ( + end.line < start.line || + (end.line === start.line && end.column <= start.column) + ) { + end = { + column: start.column + 1, + line: start.line, + offset: start.offset + 1 + } + } + + return { end, start } + } + + raw(prop, defaultType) { + let str = new Stringifier() + return str.raw(this, prop, defaultType) + } + + remove() { + if (this.parent) { + this.parent.removeChild(this) + } + this.parent = undefined + return this + } + + replaceWith(...nodes) { + if (this.parent) { + let bookmark = this + let foundSelf = false + for (let node of nodes) { + if (node === this) { + foundSelf = true + } else if (foundSelf) { + this.parent.insertAfter(bookmark, node) + bookmark = node + } else { + this.parent.insertBefore(bookmark, node) + } + } + + if (!foundSelf) { + this.remove() + } + } + + return this + } + + root() { + let result = this + while (result.parent && result.parent.type !== 'document') { + result = result.parent + } + return result + } + + toJSON(_, inputs) { + let emitInputs = inputs == null + inputs = inputs || new Map() + + // A worklist instead of recursive `toJSON()` calls to survive deeply + // nested trees. Each entry converts one node and writes the result + // into the already converted parent by [holder, key]. + let holderOfRoot = [] + let queue = [[this, holderOfRoot, 0]] + + for (let step = 0; step < queue.length; step++) { + let [node, holder, key] = queue[step] + let fixed = {} + holder[key] = fixed + + for (let name in node) { + if (!Object.prototype.hasOwnProperty.call(node, name)) { + /* c8 ignore next 2 */ + continue + } + if (name === 'parent' || name === 'proxyCache') continue + let value = node[name] + + if (Array.isArray(value)) { + let fixedArray = [] + fixed[name] = fixedArray + for (let i = 0; i < value.length; i++) { + let item = value[i] + if (typeof item === 'object' && item.toJSON) { + if (item.toJSON === Node.prototype.toJSON) { + queue.push([item, fixedArray, i]) + } else { + fixedArray[i] = item.toJSON(null, inputs) + } + } else { + fixedArray[i] = item + } + } + } else if (typeof value === 'object' && value.toJSON) { + if (value.toJSON === Node.prototype.toJSON) { + queue.push([value, fixed, name]) + } else { + fixed[name] = value.toJSON(null, inputs) + } + } else if (name === 'source') { + if (value == null) continue + let inputId = inputs.get(value.input) + if (inputId == null) { + inputId = inputs.size + inputs.set(value.input, inputId) + } + fixed[name] = { + end: value.end, + inputId, + start: value.start + } + } else { + fixed[name] = value + } + } + } + + let fixed = holderOfRoot[0] + if (emitInputs) { + fixed.inputs = [...inputs.keys()].map(input => input.toJSON()) + } + + return fixed + } + + toProxy() { + if (!this.proxyCache) { + this.proxyCache = new Proxy(this, this.getProxyProcessor()) + } + return this.proxyCache + } + + toString(stringifier = stringify) { + if (stringifier.stringify) stringifier = stringifier.stringify + let result = '' + stringifier(this, i => { + result += i + }) + return result + } + + warn(result, text, opts = {}) { + let data = { node: this } + for (let i in opts) data[i] = opts[i] + return result.warn(text, data) + } +} + +module.exports = Node +Node.default = Node diff --git a/lib/parse.d.ts b/lib/parse.d.ts new file mode 100644 index 000000000..ffe35b439 --- /dev/null +++ b/lib/parse.d.ts @@ -0,0 +1,9 @@ +import { Parser } from './postcss.js' + +interface Parse extends Parser { + default: Parse +} + +declare let parse: Parse + +export = parse diff --git a/lib/parse.es6 b/lib/parse.es6 deleted file mode 100644 index 6c78cb827..000000000 --- a/lib/parse.es6 +++ /dev/null @@ -1,17 +0,0 @@ -import Parser from './parser'; -import Input from './input'; - -export default function parse(css, opts) { - if ( opts && opts.safe ) { - throw new Error('Option safe was removed. ' + - 'Use parser: require("postcss-safe-parser")'); - } - - let input = new Input(css, opts); - - let parser = new Parser(input); - parser.tokenize(); - parser.loop(); - - return parser.root; -} diff --git a/lib/parse.js b/lib/parse.js new file mode 100644 index 000000000..00a1037aa --- /dev/null +++ b/lib/parse.js @@ -0,0 +1,42 @@ +'use strict' + +let Container = require('./container') +let Input = require('./input') +let Parser = require('./parser') + +function parse(css, opts) { + let input = new Input(css, opts) + let parser = new Parser(input) + try { + parser.parse() + } catch (e) { + if (process.env.NODE_ENV !== 'production') { + if (e.name === 'CssSyntaxError' && opts && opts.from) { + if (/\.scss$/i.test(opts.from)) { + e.message += + '\nYou tried to parse SCSS with ' + + 'the standard CSS parser; ' + + 'try again with the postcss-scss parser' + } else if (/\.sass/i.test(opts.from)) { + e.message += + '\nYou tried to parse Sass with ' + + 'the standard CSS parser; ' + + 'try again with the postcss-sass parser' + } else if (/\.less$/i.test(opts.from)) { + e.message += + '\nYou tried to parse Less with ' + + 'the standard CSS parser; ' + + 'try again with the postcss-less parser' + } + } + } + throw e + } + + return parser.root +} + +module.exports = parse +parse.default = parse + +Container.registerParse(parse) diff --git a/lib/parser.es6 b/lib/parser.es6 deleted file mode 100644 index 90fe9c686..000000000 --- a/lib/parser.es6 +++ /dev/null @@ -1,474 +0,0 @@ -import Declaration from './declaration'; -import tokenizer from './tokenize'; -import Comment from './comment'; -import AtRule from './at-rule'; -import Root from './root'; -import Rule from './rule'; - -export default class Parser { - - constructor(input) { - this.input = input; - - this.pos = 0; - this.root = new Root(); - this.current = this.root; - this.spaces = ''; - this.semicolon = false; - - this.root.source = { input, start: { line: 1, column: 1 } }; - } - - tokenize() { - this.tokens = tokenizer(this.input); - } - - loop() { - let token; - while ( this.pos < this.tokens.length ) { - token = this.tokens[this.pos]; - - switch ( token[0] ) { - case 'word': - case ':': - this.word(); - break; - - case '}': - this.end(token); - break; - - case 'comment': - this.comment(token); - break; - - case 'at-word': - this.atrule(token); - break; - - case '{': - this.emptyRule(token); - break; - - default: - this.spaces += token[1]; - break; - } - - this.pos += 1; - } - this.endFile(); - } - - comment(token) { - let node = new Comment(); - this.init(node, token[2], token[3]); - node.source.end = { line: token[4], column: token[5] }; - - let text = token[1].slice(2, -2); - if ( /^\s*$/.test(text) ) { - node.text = ''; - node.raws.left = text; - node.raws.right = ''; - } else { - let match = text.match(/^(\s*)([^]*[^\s])(\s*)$/); - node.text = match[2]; - node.raws.left = match[1]; - node.raws.right = match[3]; - } - } - - emptyRule(token) { - let node = new Rule(); - this.init(node, token[2], token[3]); - node.selector = ''; - node.raws.between = ''; - this.current = node; - } - - word() { - let token; - let end = false; - let type = null; - let colon = false; - let bracket = null; - let brackets = 0; - - let start = this.pos; - this.pos += 1; - while ( this.pos < this.tokens.length ) { - token = this.tokens[this.pos]; - type = token[0]; - - if ( type === '(' ) { - if ( !bracket ) bracket = token; - brackets += 1; - - } else if ( brackets === 0 ) { - if ( type === ';' ) { - if ( colon ) { - this.decl(this.tokens.slice(start, this.pos + 1)); - return; - } else { - break; - } - - } else if ( type === '{' ) { - this.rule(this.tokens.slice(start, this.pos + 1)); - return; - - } else if ( type === '}' ) { - this.pos -= 1; - end = true; - break; - - } else if ( type === ':' ) { - colon = true; - } - - } else if ( type === ')' ) { - brackets -= 1; - if ( brackets === 0 ) bracket = null; - } - - this.pos += 1; - } - if ( this.pos === this.tokens.length ) { - this.pos -= 1; - end = true; - } - - if ( brackets > 0 ) this.unclosedBracket(bracket); - - if ( end && colon ) { - while ( this.pos > start ) { - token = this.tokens[this.pos][0]; - if ( token !== 'space' && token !== 'comment' ) break; - this.pos -= 1; - } - this.decl(this.tokens.slice(start, this.pos + 1)); - return; - } - - this.unknownWord(start); - } - - rule(tokens) { - tokens.pop(); - - let node = new Rule(); - this.init(node, tokens[0][2], tokens[0][3]); - - node.raws.between = this.spacesFromEnd(tokens); - this.raw(node, 'selector', tokens); - this.current = node; - } - - decl(tokens) { - let node = new Declaration(); - this.init(node); - - let last = tokens[tokens.length - 1]; - if ( last[0] === ';' ) { - this.semicolon = true; - tokens.pop(); - } - if ( last[4] ) { - node.source.end = { line: last[4], column: last[5] }; - } else { - node.source.end = { line: last[2], column: last[3] }; - } - - while ( tokens[0][0] !== 'word' ) { - node.raws.before += tokens.shift()[1]; - } - node.source.start = { line: tokens[0][2], column: tokens[0][3] }; - - node.prop = ''; - while ( tokens.length ) { - let type = tokens[0][0]; - if ( type === ':' || type === 'space' || type === 'comment' ) { - break; - } - node.prop += tokens.shift()[1]; - } - - node.raws.between = ''; - - let token; - while ( tokens.length ) { - token = tokens.shift(); - - if ( token[0] === ':' ) { - node.raws.between += token[1]; - break; - } else { - node.raws.between += token[1]; - } - } - - if ( node.prop[0] === '_' || node.prop[0] === '*' ) { - node.raws.before += node.prop[0]; - node.prop = node.prop.slice(1); - } - node.raws.between += this.spacesFromStart(tokens); - this.precheckMissedSemicolon(tokens); - - for ( let i = tokens.length - 1; i > 0; i-- ) { - token = tokens[i]; - if ( token[1] === '!important' ) { - node.important = true; - let string = this.stringFrom(tokens, i); - string = this.spacesFromEnd(tokens) + string; - if ( string !== ' !important' ) node.raws.important = string; - break; - - } else if (token[1] === 'important') { - let cache = tokens.slice(0); - let str = ''; - for ( let j = i; j > 0; j-- ) { - let type = cache[j][0]; - if ( str.trim().indexOf('!') === 0 && type !== 'space' ) { - break; - } - str = cache.pop()[1] + str; - } - if ( str.trim().indexOf('!') === 0 ) { - node.important = true; - node.raws.important = str; - tokens = cache; - } - } - - if ( token[0] !== 'space' && token[0] !== 'comment' ) { - break; - } - } - - this.raw(node, 'value', tokens); - - if ( node.value.indexOf(':') !== -1 ) this.checkMissedSemicolon(tokens); - } - - atrule(token) { - let node = new AtRule(); - node.name = token[1].slice(1); - if ( node.name === '' ) { - this.unnamedAtrule(node, token); - } - this.init(node, token[2], token[3]); - - let last = false; - let open = false; - let params = []; - - this.pos += 1; - while ( this.pos < this.tokens.length ) { - token = this.tokens[this.pos]; - - if ( token[0] === ';' ) { - node.source.end = { line: token[2], column: token[3] }; - this.semicolon = true; - break; - } else if ( token[0] === '{' ) { - open = true; - break; - } else if ( token[0] === '}') { - this.end(token); - break; - } else { - params.push(token); - } - - this.pos += 1; - } - if ( this.pos === this.tokens.length ) { - last = true; - } - - node.raws.between = this.spacesFromEnd(params); - if ( params.length ) { - node.raws.afterName = this.spacesFromStart(params); - this.raw(node, 'params', params); - if ( last ) { - token = params[params.length - 1]; - node.source.end = { line: token[4], column: token[5] }; - this.spaces = node.raws.between; - node.raws.between = ''; - } - } else { - node.raws.afterName = ''; - node.params = ''; - } - - if ( open ) { - node.nodes = []; - this.current = node; - } - } - - end(token) { - if ( this.current.nodes && this.current.nodes.length ) { - this.current.raws.semicolon = this.semicolon; - } - this.semicolon = false; - - this.current.raws.after = (this.current.raws.after || '') + this.spaces; - this.spaces = ''; - - if ( this.current.parent ) { - this.current.source.end = { line: token[2], column: token[3] }; - this.current = this.current.parent; - } else { - this.unexpectedClose(token); - } - } - - endFile() { - if ( this.current.parent ) this.unclosedBlock(); - if ( this.current.nodes && this.current.nodes.length ) { - this.current.raws.semicolon = this.semicolon; - } - this.current.raws.after = (this.current.raws.after || '') + this.spaces; - } - - // Helpers - - init(node, line, column) { - this.current.push(node); - - node.source = { start: { line, column }, input: this.input }; - node.raws.before = this.spaces; - this.spaces = ''; - if ( node.type !== 'comment' ) this.semicolon = false; - } - - raw(node, prop, tokens) { - let token, type; - let length = tokens.length; - let value = ''; - let clean = true; - for ( let i = 0; i < length; i += 1 ) { - token = tokens[i]; - type = token[0]; - if ( type === 'comment' || type === 'space' && i === length - 1 ) { - clean = false; - } else { - value += token[1]; - } - } - if ( !clean ) { - let raw = tokens.reduce( (all, i) => all + i[1], ''); - node.raws[prop] = { value, raw }; - } - node[prop] = value; - } - - spacesFromEnd(tokens) { - let lastTokenType; - let spaces = ''; - while ( tokens.length ) { - lastTokenType = tokens[tokens.length - 1][0]; - if ( lastTokenType !== 'space' && - lastTokenType !== 'comment' ) break; - spaces = tokens.pop()[1] + spaces; - } - return spaces; - } - - spacesFromStart(tokens) { - let next; - let spaces = ''; - while ( tokens.length ) { - next = tokens[0][0]; - if ( next !== 'space' && next !== 'comment' ) break; - spaces += tokens.shift()[1]; - } - return spaces; - } - - stringFrom(tokens, from) { - let result = ''; - for ( let i = from; i < tokens.length; i++ ) { - result += tokens[i][1]; - } - tokens.splice(from, tokens.length - from); - return result; - } - - colon(tokens) { - let brackets = 0; - let token, type, prev; - for ( let i = 0; i < tokens.length; i++ ) { - token = tokens[i]; - type = token[0]; - - if ( type === '(' ) { - brackets += 1; - } else if ( type === ')' ) { - brackets -= 1; - } else if ( brackets === 0 && type === ':' ) { - if ( !prev ) { - this.doubleColon(token); - } else if ( prev[0] === 'word' && prev[1] === 'progid' ) { - continue; - } else { - return i; - } - } - - prev = token; - } - return false; - } - - // Errors - - unclosedBracket(bracket) { - throw this.input.error('Unclosed bracket', bracket[2], bracket[3]); - } - - unknownWord(start) { - let token = this.tokens[start]; - throw this.input.error('Unknown word', token[2], token[3]); - } - - unexpectedClose(token) { - throw this.input.error('Unexpected }', token[2], token[3]); - } - - unclosedBlock() { - let pos = this.current.source.start; - throw this.input.error('Unclosed block', pos.line, pos.column); - } - - doubleColon(token) { - throw this.input.error('Double colon', token[2], token[3]); - } - - unnamedAtrule(node, token) { - throw this.input.error('At-rule without name', token[2], token[3]); - } - - precheckMissedSemicolon(tokens) { - // Hook for Safe Parser - tokens; - } - - checkMissedSemicolon(tokens) { - let colon = this.colon(tokens); - if ( colon === false ) return; - - let founded = 0; - let token; - for ( let j = colon - 1; j >= 0; j-- ) { - token = tokens[j]; - if ( token[0] !== 'space' ) { - founded += 1; - if ( founded === 2 ) break; - } - } - throw this.input.error('Missed semicolon', token[2], token[3]); - } - -} diff --git a/lib/parser.js b/lib/parser.js new file mode 100644 index 000000000..f549f56d1 --- /dev/null +++ b/lib/parser.js @@ -0,0 +1,621 @@ +'use strict' + +let AtRule = require('./at-rule') +let Comment = require('./comment') +let Declaration = require('./declaration') +let Root = require('./root') +let Rule = require('./rule') +let tokenizer = require('./tokenize') + +const SAFE_COMMENT_NEIGHBOR = { + empty: true, + space: true +} + +function findLastWithPosition(tokens) { + for (let i = tokens.length - 1; i >= 0; i--) { + let token = tokens[i] + let pos = token[3] || token[2] + if (pos) return pos + } +} + +function tokensToString(tokens, from, to) { + let result = '' + for (let i = from; i < to; i++) result += tokens[i][1] + return result +} + +class Parser { + constructor(input) { + this.input = input + + this.root = new Root() + this.current = this.root + this.spaces = '' + this.semicolon = false + + this.createTokenizer() + this.root.source = { input, start: { column: 1, line: 1, offset: 0 } } + } + + atrule(token) { + let node = new AtRule() + node.name = token[1].slice(1) + if (node.name === '') { + this.unnamedAtrule(node, token) + } + this.init(node, token[2]) + + let type + let prev + let shift + let last = false + let open = false + let params = [] + let brackets = [] + + while (!this.tokenizer.endOfFile()) { + token = this.tokenizer.nextToken() + type = token[0] + + if (type === '(' || type === '[') { + brackets.push(type === '(' ? ')' : ']') + } else if (type === '{' && brackets.length > 0) { + brackets.push('}') + } else if (type === brackets[brackets.length - 1]) { + brackets.pop() + } + + if (brackets.length === 0) { + if (type === ';') { + node.source.end = this.getPosition(token[2]) + node.source.end.offset++ + this.semicolon = true + break + } else if (type === '{') { + open = true + break + } else if (type === '}') { + if (params.length > 0) { + shift = params.length - 1 + prev = params[shift] + while (prev && prev[0] === 'space') { + prev = params[--shift] + } + if (prev) { + node.source.end = this.getPosition(prev[3] || prev[2]) + node.source.end.offset++ + } + } + this.end(token) + break + } else { + params.push(token) + } + } else { + params.push(token) + } + + if (this.tokenizer.endOfFile()) { + last = true + break + } + } + + node.raws.between = this.spacesAndCommentsFromEnd(params) + if (params.length) { + node.raws.afterName = this.spacesAndCommentsFromStart(params) + this.raw(node, 'params', params) + if (last) { + token = params[params.length - 1] + node.source.end = this.getPosition(token[3] || token[2]) + node.source.end.offset++ + this.spaces = node.raws.between + node.raws.between = '' + } + } else { + node.raws.afterName = '' + node.params = '' + } + + if (open) { + node.nodes = [] + this.current = node + } + } + + checkMissedSemicolon(tokens) { + let colon = this.colon(tokens) + if (colon === false) return + + let founded = 0 + let token + for (let j = colon - 1; j >= 0; j--) { + token = tokens[j] + if (token[0] !== 'space') { + founded += 1 + if (founded === 2) break + } + } + // If the token is a word, e.g. `!important`, `red` or any other valid + // property's value. Then we need to return the colon after that word + // token. [3] is the "end" colon of that word. And because we need it + // after that one we do +1 to get the next one. + throw this.input.error( + 'Missed semicolon', + token[0] === 'word' ? token[3] + 1 : token[2] + ) + } + + colon(tokens) { + let brackets = 0 + let prev, token, type + for (let [i, element] of tokens.entries()) { + token = element + type = token[0] + + if (type === '(') { + brackets += 1 + } + if (type === ')') { + brackets -= 1 + } + if (brackets === 0 && type === ':') { + if (!prev) { + this.doubleColon(token) + } else if (prev[0] === 'word' && prev[1] === 'progid') { + continue + } else { + return i + } + } + + prev = token + } + return false + } + + comment(token) { + let node = new Comment() + this.init(node, token[2]) + node.source.end = this.getPosition(token[3] || token[2]) + node.source.end.offset++ + + let text = token[1].slice(2, -2) + if (!text.trim()) { + node.text = '' + node.raws.left = text + node.raws.right = '' + } else { + let match = text.match(/^(\s*)([^]*\S)(\s*)$/) + node.text = match[2] + node.raws.left = match[1] + node.raws.right = match[3] + } + } + + createTokenizer() { + this.tokenizer = tokenizer(this.input) + } + + decl(tokens, customProperty) { + let node = new Declaration() + this.init(node, tokens[0][2]) + + let last = tokens[tokens.length - 1] + if (last[0] === ';') { + this.semicolon = true + tokens.pop() + } + + node.source.end = this.getPosition( + last[3] || last[2] || findLastWithPosition(tokens) + ) + node.source.end.offset++ + + let start = 0 + while (tokens[start][0] !== 'word') { + if (start === tokens.length - 1) this.unknownWord([tokens[start]]) + start++ + } + node.raws.before += tokensToString(tokens, 0, start) + node.source.start = this.getPosition(tokens[start][2]) + + let propStart = start + while (start < tokens.length) { + let type = tokens[start][0] + if (type === ':' || type === 'space' || type === 'comment') { + break + } + start++ + } + node.prop = tokensToString(tokens, propStart, start) + + let betweenStart = start + let token + while (start < tokens.length) { + token = tokens[start] + start++ + if (token[0] === ':') break + if (token[0] === 'word' && /\w/.test(token[1])) { + this.unknownWord([token]) + } + } + node.raws.between = tokensToString(tokens, betweenStart, start) + + if (node.prop[0] === '_' || node.prop[0] === '*') { + node.raws.before += node.prop[0] + node.prop = node.prop.slice(1) + } + + let firstSpacesStart = start + while (start < tokens.length) { + let next = tokens[start][0] + if (next !== 'space' && next !== 'comment') break + start++ + } + let firstSpaces = tokens.slice(firstSpacesStart, start) + + tokens = tokens.slice(start) + + this.precheckMissedSemicolon(tokens) + + for (let i = tokens.length - 1; i >= 0; i--) { + token = tokens[i] + if (token[1].toLowerCase() === '!important') { + node.important = true + let string = this.stringFrom(tokens, i) + string = this.spacesFromEnd(tokens) + string + if (string !== ' !important') node.raws.important = string + break + } else if (token[1].toLowerCase() === 'important') { + let cache = tokens.slice(0) + let str = '' + for (let j = i; j > 0; j--) { + let type = cache[j][0] + if (str.trim().startsWith('!') && type !== 'space') { + break + } + str = cache.pop()[1] + str + } + if (str.trim().startsWith('!')) { + node.important = true + node.raws.important = str + tokens = cache + } + } + + if (token[0] !== 'space' && token[0] !== 'comment') { + break + } + } + + let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment') + + if (hasWord) { + node.raws.between += firstSpaces.map(i => i[1]).join('') + firstSpaces = [] + } + this.raw(node, 'value', firstSpaces.concat(tokens), customProperty) + + if (node.value.includes(':') && !customProperty) { + this.checkMissedSemicolon(tokens) + } + } + + doubleColon(token) { + throw this.input.error( + 'Double colon', + { offset: token[2] }, + { offset: token[2] + token[1].length } + ) + } + + emptyRule(token) { + let node = new Rule() + this.init(node, token[2]) + node.selector = '' + node.raws.between = '' + this.current = node + } + + end(token) { + if (this.current.nodes && this.current.nodes.length) { + this.current.raws.semicolon = this.semicolon + } + this.semicolon = false + + this.current.raws.after = (this.current.raws.after || '') + this.spaces + this.spaces = '' + + if (this.current.parent) { + this.current.source.end = this.getPosition(token[2]) + this.current.source.end.offset++ + this.current = this.current.parent + } else { + this.unexpectedClose(token) + } + } + + endFile() { + if (this.current.parent) this.unclosedBlock() + if (this.current.nodes && this.current.nodes.length) { + this.current.raws.semicolon = this.semicolon + } + this.current.raws.after = (this.current.raws.after || '') + this.spaces + this.root.source.end = this.getPosition(this.tokenizer.position()) + } + + freeSemicolon(token) { + this.spaces += token[1] + if (this.current.nodes) { + let prev = this.current.nodes[this.current.nodes.length - 1] + if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) { + prev.raws.ownSemicolon = this.spaces + this.spaces = '' + // `ownSemicolon` also holds the spaces before the semicolon, but + // the position above is the semicolon itself, so the node ends + // right after it. + prev.source.end = this.getPosition(token[2]) + prev.source.end.offset++ + } + } + } + + // Helpers + + getPosition(offset) { + let pos = this.input.fromOffset(offset) + return { + column: pos.col, + line: pos.line, + offset + } + } + + init(node, offset) { + this.current.push(node) + node.source = { + input: this.input, + start: this.getPosition(offset) + } + node.raws.before = this.spaces + this.spaces = '' + if (node.type !== 'comment') this.semicolon = false + } + + other(start) { + let end = false + let type = null + let colon = false + let bracket = null + let brackets = [] + let customProperty = start[1].startsWith('--') + + let tokens = [] + let token = start + while (token) { + type = token[0] + tokens.push(token) + + if (type === '(' || type === '[') { + if (!bracket) bracket = token + brackets.push(type === '(' ? ')' : ']') + } else if (customProperty && colon && type === '{') { + if (!bracket) bracket = token + brackets.push('}') + } else if (brackets.length === 0) { + if (type === ';') { + if (colon) { + this.decl(tokens, customProperty) + return + } else { + break + } + } else if (type === '{') { + this.rule(tokens) + return + } else if (type === '}') { + this.tokenizer.back(tokens.pop()) + end = true + break + } else if (type === ':') { + colon = true + } + } else if (type === brackets[brackets.length - 1]) { + brackets.pop() + if (brackets.length === 0) bracket = null + } + + token = this.tokenizer.nextToken() + } + + if (this.tokenizer.endOfFile()) end = true + if (brackets.length > 0) this.unclosedBracket(bracket) + + if (end && colon) { + if (!customProperty) { + while (tokens.length) { + token = tokens[tokens.length - 1][0] + if (token !== 'space' && token !== 'comment') break + this.tokenizer.back(tokens.pop()) + } + } + this.decl(tokens, customProperty) + } else { + this.unknownWord(tokens) + } + } + + parse() { + let token + while (!this.tokenizer.endOfFile()) { + token = this.tokenizer.nextToken() + + switch (token[0]) { + case 'space': + this.spaces += token[1] + break + + case ';': + this.freeSemicolon(token) + break + + case '}': + this.end(token) + break + + case 'comment': + this.comment(token) + break + + case 'at-word': + this.atrule(token) + break + + case '{': + this.emptyRule(token) + break + + default: + this.other(token) + break + } + } + this.endFile() + } + + precheckMissedSemicolon(/* tokens */) { + // Hook for Safe Parser + } + + raw(node, prop, tokens, customProperty) { + let token, type + let length = tokens.length + let value = '' + let clean = true + let next, prev + + for (let i = 0; i < length; i += 1) { + token = tokens[i] + type = token[0] + if (type === 'space' && i === length - 1 && !customProperty) { + clean = false + } else if (type === 'comment') { + prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty' + next = tokens[i + 1] ? tokens[i + 1][0] : 'empty' + if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) { + if (value.slice(-1) === ',') { + clean = false + } else { + value += token[1] + } + } else { + clean = false + } + } else { + value += token[1] + } + } + if (!clean) { + let raw = tokens.reduce((all, i) => all + i[1], '') + node.raws[prop] = { raw, value } + } + node[prop] = value + } + + rule(tokens) { + tokens.pop() + + let node = new Rule() + this.init(node, tokens[0][2]) + + node.raws.between = this.spacesAndCommentsFromEnd(tokens) + this.raw(node, 'selector', tokens) + this.current = node + } + + spacesAndCommentsFromEnd(tokens) { + let lastTokenType + let spaces = '' + while (tokens.length) { + lastTokenType = tokens[tokens.length - 1][0] + if (lastTokenType !== 'space' && lastTokenType !== 'comment') break + spaces = tokens.pop()[1] + spaces + } + return spaces + } + + // Errors + + spacesAndCommentsFromStart(tokens) { + let next + let spaces = '' + while (tokens.length) { + next = tokens[0][0] + if (next !== 'space' && next !== 'comment') break + spaces += tokens.shift()[1] + } + return spaces + } + + spacesFromEnd(tokens) { + let lastTokenType + let spaces = '' + while (tokens.length) { + lastTokenType = tokens[tokens.length - 1][0] + if (lastTokenType !== 'space') break + spaces = tokens.pop()[1] + spaces + } + return spaces + } + + stringFrom(tokens, from) { + let result = '' + for (let i = from; i < tokens.length; i++) { + result += tokens[i][1] + } + tokens.splice(from, tokens.length - from) + return result + } + + unclosedBlock() { + let pos = this.current.source.start + throw this.input.error('Unclosed block', pos.line, pos.column) + } + + unclosedBracket(bracket) { + throw this.input.error( + 'Unclosed bracket', + { offset: bracket[2] }, + { offset: bracket[2] + 1 } + ) + } + + unexpectedClose(token) { + throw this.input.error( + 'Unexpected }', + { offset: token[2] }, + { offset: token[2] + 1 } + ) + } + + unknownWord(tokens) { + throw this.input.error( + 'Unknown word ' + tokens[0][1], + { offset: tokens[0][2] }, + { offset: tokens[0][2] + tokens[0][1].length } + ) + } + + unnamedAtrule(node, token) { + throw this.input.error( + 'At-rule without name', + { offset: token[2] }, + { offset: token[2] + token[1].length } + ) + } +} + +module.exports = Parser diff --git a/lib/postcss.d.mts b/lib/postcss.d.mts new file mode 100644 index 000000000..eaec8681f --- /dev/null +++ b/lib/postcss.d.mts @@ -0,0 +1,66 @@ +export { + // Type-only exports + AcceptedPlugin, + AnyNode, + atRule, + AtRule, + AtRuleProps, + Builder, + ChildNode, + ChildProps, + comment, + Comment, + CommentProps, + Container, + ContainerProps, + CssSyntaxError, + decl, + Declaration, + DeclarationProps, + // postcss function / namespace + default, + document, + Document, + DocumentProps, + FilePosition, + fromJSON, + Helpers, + Input, + JSONHydrator, + // This is a class, but it’s not re-exported. That’s why it’s exported as type-only here. + type LazyResult, + list, + Message, + Node, + NodeErrorOptions, + NodeProps, + OldPlugin, + parse, + Parser, + // @ts-expect-error This value exists, but it’s untyped. + plugin, + Plugin, + PluginCreator, + Position, + Postcss, + ProcessOptions, + Processor, + Result, + root, + Root, + RootProps, + rule, + Rule, + RuleProps, + Source, + SourceMap, + SourceMapOptions, + Stringifier, + // Value exports from postcss.mjs + stringify, + Syntax, + TransformCallback, + Transformer, + Warning, + WarningOptions +} from './postcss.js' diff --git a/lib/postcss.d.ts b/lib/postcss.d.ts new file mode 100644 index 000000000..41bf40f9b --- /dev/null +++ b/lib/postcss.d.ts @@ -0,0 +1,464 @@ +import { RawSourceMap, SourceMapGenerator } from 'source-map-js' + +import AtRule, { AtRuleProps } from './at-rule.js' +import Comment, { CommentProps } from './comment.js' +import Container, { ContainerProps, NewChild } from './container.js' +import CssSyntaxError from './css-syntax-error.js' +import Declaration, { DeclarationProps } from './declaration.js' +import Document, { DocumentProps } from './document.js' +import Input, { FilePosition } from './input.js' +import LazyResult from './lazy-result.js' +import list from './list.js' +import Node, { + AnyNode, + ChildNode, + ChildProps, + NodeErrorOptions, + NodeProps, + Position, + Source +} from './node.js' +import Processor from './processor.js' +import Result, { Message } from './result.js' +import Root, { RootProps } from './root.js' +import Rule, { RuleProps } from './rule.js' +import Warning, { WarningOptions } from './warning.js' + +type DocumentProcessor = ( + document: Document, + helper: postcss.Helpers +) => Promise | void +type RootProcessor = ( + root: Root, + helper: postcss.Helpers +) => Promise | void +type DeclarationProcessor = ( + decl: Declaration, + helper: postcss.Helpers +) => Promise | void +type RuleProcessor = ( + rule: Rule, + helper: postcss.Helpers +) => Promise | void +type AtRuleProcessor = ( + atRule: AtRule, + helper: postcss.Helpers +) => Promise | void +type CommentProcessor = ( + comment: Comment, + helper: postcss.Helpers +) => Promise | void + +interface Processors { + /** + * Will be called on all`AtRule` nodes. + * + * Will be called again on node or children changes. + */ + AtRule?: { [name: string]: AtRuleProcessor } | AtRuleProcessor + + /** + * Will be called on all `AtRule` nodes, when all children will be processed. + * + * Will be called again on node or children changes. + */ + AtRuleExit?: { [name: string]: AtRuleProcessor } | AtRuleProcessor + + /** + * Will be called on all `Comment` nodes. + * + * Will be called again on node or children changes. + */ + Comment?: CommentProcessor + + /** + * Will be called on all `Comment` nodes after listeners + * for `Comment` event. + * + * Will be called again on node or children changes. + */ + CommentExit?: CommentProcessor + + /** + * Will be called on all `Declaration` nodes after listeners + * for `Declaration` event. + * + * Will be called again on node or children changes. + */ + Declaration?: { [prop: string]: DeclarationProcessor } | DeclarationProcessor + + /** + * Will be called on all `Declaration` nodes. + * + * Will be called again on node or children changes. + */ + DeclarationExit?: + | { [prop: string]: DeclarationProcessor } + | DeclarationProcessor + + /** + * Will be called on `Document` node. + * + * Will be called again on children changes. + */ + Document?: DocumentProcessor + + /** + * Will be called on `Document` node, when all children will be processed. + * + * Will be called again on children changes. + */ + DocumentExit?: DocumentProcessor + + /** + * Will be called on `Root` node once. + */ + Once?: RootProcessor + + /** + * Will be called on `Root` node once, when all children will be processed. + */ + OnceExit?: RootProcessor + + /** + * Will be called on `Root` node. + * + * Will be called again on children changes. + */ + Root?: RootProcessor + + /** + * Will be called on `Root` node, when all children will be processed. + * + * Will be called again on children changes. + */ + RootExit?: RootProcessor + + /** + * Will be called on all `Rule` nodes. + * + * Will be called again on node or children changes. + */ + Rule?: RuleProcessor + + /** + * Will be called on all `Rule` nodes, when all children will be processed. + * + * Will be called again on node or children changes. + */ + RuleExit?: RuleProcessor +} + +declare namespace postcss { + export { + AnyNode, + AtRule, + AtRuleProps, + ChildNode, + ChildProps, + Comment, + CommentProps, + Container, + ContainerProps, + CssSyntaxError, + Declaration, + DeclarationProps, + Document, + DocumentProps, + FilePosition, + Input, + LazyResult, + list, + Message, + NewChild, + Node, + NodeErrorOptions, + NodeProps, + Position, + Processor, + Result, + Root, + RootProps, + Rule, + RuleProps, + Source, + Warning, + WarningOptions + } + + export type SourceMap = { + toJSON(): RawSourceMap + } & SourceMapGenerator + + export type Helpers = { postcss: Postcss; result: Result } & Postcss + + export interface Plugin extends Processors { + postcssPlugin: string + prepare?: (result: Result) => Processors + } + + export interface PluginCreator { + (opts?: PluginOptions): Plugin | Processor + postcss: true + } + + export interface Transformer extends TransformCallback { + postcssPlugin: string + postcssVersion: string + } + + export interface TransformCallback { + (root: Root, result: Result): Promise | void + } + + export interface OldPlugin extends Transformer { + (opts?: T): Transformer + postcss: Transformer + } + + export type AcceptedPlugin = + | { + postcss: Processor | TransformCallback + } + | OldPlugin + | Plugin + | PluginCreator + | Processor + | TransformCallback + + export interface Parser { + ( + css: { toString(): string } | string, + opts?: Pick + ): RootNode + } + + export interface Builder { + (part: string, node?: AnyNode, type?: 'end' | 'start'): void + } + + export interface Stringifier { + (node: AnyNode, builder: Builder): void + } + + export interface JSONHydrator { + (data: object): Node + (data: object[]): Node[] + } + + export interface Syntax { + /** + * Function to generate AST by string. + */ + parse?: Parser + + /** + * Class to generate string by AST. + */ + stringify?: Stringifier + } + + export interface SourceMapOptions { + /** + * Use absolute path in generated source map. + */ + absolute?: boolean + + /** + * Indicates that PostCSS should add annotation comments to the CSS. + * By default, PostCSS will always add a comment with a path + * to the source map. PostCSS will not add annotations to CSS files + * that do not contain any comments. + * + * By default, PostCSS presumes that you want to save the source map as + * `opts.to + '.map'` and will use this path in the annotation comment. + * A different path can be set by providing a string value for annotation. + * + * If you have set `inline: true`, annotation cannot be disabled. + */ + annotation?: ((file: string, root: Root) => string) | boolean | string + + /** + * Override `from` in map’s sources. + */ + from?: string + + /** + * Indicates that the source map should be embedded in the output CSS + * as a Base64-encoded comment. By default, it is `true`. + * But if all previous maps are external, not inline, PostCSS will not embed + * the map even if you do not set this option. + * + * If you have an inline source map, the result.map property will be empty, + * as the source map will be contained within the text of `result.css`. + */ + inline?: boolean + + /** + * Source map content from a previous processing step (e.g., Sass). + * + * PostCSS will try to read the previous source map + * automatically (based on comments within the source CSS), but you can use + * this option to identify it manually. + * + * If desired, you can omit the previous map with prev: `false`. + */ + prev?: ((file: string) => string) | boolean | object | string + + /** + * Indicates that PostCSS should set the origin content (e.g., Sass source) + * of the source map. By default, it is true. But if all previous maps do not + * contain sources content, PostCSS will also leave it out even if you + * do not set this option. + */ + sourcesContent?: boolean + } + + export interface ProcessOptions { + /** + * Input file if it is not simple CSS file, but HTML with ' }) + is(root.source?.input.css, 'a {} b {}') + is(root.source?.input.document, '') + + let a = root.first as Rule + + // Offset the source location of `a` to mimic syntaxes like `postcss-html` + a.source = { + end: { + column: 12, + line: 1, + offset: 12 + }, + input: a.source!.input, + start: { + column: 8, + line: 1, + offset: 7 + } + } + + equal(a.positionInside(0), { column: 8, line: 1, offset: 7 }) + equal(a.positionInside(1), { column: 9, line: 1, offset: 8 }) +}) + +test('positionBy() returns position', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.positionBy(), { column: 6, line: 1, offset: 5 }) + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) +}) + +test('positionBy() returns position after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) + equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) + + one.remove() + + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) + equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) +}) + +test('positionBy() returns position', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.positionBy(), { column: 6, line: 1, offset: 5 }) + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) +}) + +test('positionBy() returns position after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) + equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) + + one.remove() + + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) + equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) +}) + +test('positionBy() returns position for word', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.positionBy({ word: 'one' }), { column: 6, line: 1, offset: 5 }) + equal(one.positionBy({ word: 'X' }), { column: 11, line: 1, offset: 10 }) + equal(a.positionBy({ word: '}' }), { column: 14, line: 1, offset: 13 }) +}) + +test('positionBy() returns position for word after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) + equal(two.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) + + one.remove() + + equal(a.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) + equal(two.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) +}) + +test('positionBy() returns position when offset is missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + + equal(one.positionBy(), { column: 6, line: 1, offset: 5 }) +}) + +test('positionBy() returns position for index', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.positionBy({ index: 1 }), { column: 7, line: 1, offset: 6 }) +}) + +test('positionBy() returns position for index after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.positionBy({ index: 15 }), { column: 3, line: 3, offset: 15 }) + equal(two.positionBy({ index: 1 }), { column: 3, line: 3, offset: 15 }) + + one.remove() + + equal(a.positionBy({ index: 15 }), { column: 3, line: 3, offset: 15 }) + equal(two.positionBy({ index: 1 }), { column: 3, line: 3, offset: 15 }) +}) + +test('positionBy() supports multi-root documents', () => { + let root = parse('a {} b {}', { document: '' }) + is(root.source?.input.css, 'a {} b {}') + is(root.source?.input.document, '') + + let a = root.first as Rule + + // Offset the source location of `a` to mimic syntaxes like `postcss-html` + a.source = { + end: { + column: 12, + line: 1, + offset: 12 + }, + input: a.source!.input, + start: { + column: 8, + line: 1, + offset: 7 + } + } + + // `offset` is present because the `0` index returns `source.start` + equal(a.positionBy({ index: 0 }), { column: 8, line: 1, offset: 7 }) + equal(a.positionBy({ index: 1 }), { column: 9, line: 1, offset: 8 }) + equal(a.positionBy({ word: 'a' }), { column: 8, line: 1, offset: 7 }) +}) + +test('rangeBy() returns range', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy(), { + end: { column: 12, line: 1, offset: 11 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal(one.rangeBy(), { + end: { column: 12, line: 1, offset: 11 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for empty object even after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.rangeBy(), { + end: { column: 10, line: 3, offset: 22 }, + start: { column: 1, line: 1, offset: 0 } + }) + equal(two.rangeBy(), { + end: { column: 9, line: 3, offset: 21 }, + start: { column: 2, line: 3, offset: 14 } + }) + + one.remove() + + equal(a.rangeBy(), { + end: { column: 10, line: 3, offset: 22 }, + start: { column: 1, line: 1, offset: 0 } + }) + equal(two.rangeBy(), { + end: { column: 9, line: 3, offset: 21 }, + start: { column: 2, line: 3, offset: 14 } + }) +}) + +test('rangeBy() returns range', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy(), { + end: { column: 12, line: 1, offset: 11 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal(one.rangeBy(), { + end: { column: 12, line: 1, offset: 11 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for empty object even after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.rangeBy(), { + end: { column: 10, line: 3, offset: 22 }, + start: { column: 1, line: 1, offset: 0 } + }) + equal(two.rangeBy(), { + end: { column: 9, line: 3, offset: 21 }, + start: { column: 2, line: 3, offset: 14 } + }) + + one.remove() + + equal(a.rangeBy(), { + end: { column: 10, line: 3, offset: 22 }, + start: { column: 1, line: 1, offset: 0 } + }) + equal(two.rangeBy(), { + end: { column: 9, line: 3, offset: 21 }, + start: { column: 2, line: 3, offset: 14 } + }) +}) + +test('rangeBy() returns range for word', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy({ word: 'one' }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for word when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal(one.rangeBy({ word: 'one' }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for word even after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + equal(two.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + + one.remove() + + equal(a.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + equal(two.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) +}) + +test('rangeBy() returns range for word even after AST mutations when offsets are missing', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + // @ts-expect-error Testing non-standard AST + if (a.source?.start) delete a.source.start.offset + // @ts-expect-error Testing non-standard AST + if (a.source?.end) delete a.source.end.offset + // @ts-expect-error Testing non-standard AST + if (two.source?.start) delete two.source.start.offset + // @ts-expect-error Testing non-standard AST + if (two.source?.end) delete two.source.end.offset + + equal(a.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + equal(two.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + + one.remove() + + equal(a.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + equal(two.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) +}) + +test('rangeBy() returns range for start and end', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal( + one.rangeBy({ end: { column: 9, line: 1 }, start: { column: 7, line: 1 } }), + { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 7, line: 1, offset: 6 } + } + ) +}) + +test('rangeBy() returns range for start and end when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal( + one.rangeBy({ end: { column: 9, line: 1 }, start: { column: 7, line: 1 } }), + { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 7, line: 1, offset: 6 } + } + ) +}) + +test('rangeBy() returns range for start and end after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal( + a.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), + { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + } + ) + equal( + two.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), + { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + } + ) + + one.remove() + + equal( + a.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), + { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + } + ) + equal( + two.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), + { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + } + ) +}) + +test('rangeBy() returns range for index and endIndex', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy({ endIndex: 3, index: 1 }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 7, line: 1, offset: 6 } + }) +}) + +test('rangeBy() returns range for index 0', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy({ index: 0 }), { + end: { column: 7, line: 1, offset: 6 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for index and endIndex when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal(one.rangeBy({ endIndex: 3, index: 1 }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 7, line: 1, offset: 6 } + }) +}) + +test('rangeBy() returns range for index and endIndex after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.rangeBy({ endIndex: 17, index: 15 }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + }) + equal(two.rangeBy({ endIndex: 3, index: 1 }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + }) + + one.remove() + + equal(a.rangeBy({ endIndex: 17, index: 15 }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + }) + equal(two.rangeBy({ endIndex: 3, index: 1 }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + }) +}) + +test('rangeBy() supports multi-root documents', () => { + let root = parse('a {} b {}', { document: '' }) + is(root.source?.input.css, 'a {} b {}') + is(root.source?.input.document, '') + + let a = root.first as Rule + + // Offset the source location of `a` to mimic syntaxes like `postcss-html` + a.source = { + end: { + column: 12, + line: 1, + offset: 12 + }, + input: a.source!.input, + start: { + column: 8, + line: 1, + offset: 7 + } + } + + equal(a.rangeBy({ endIndex: 1, index: 0 }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 8, line: 1, offset: 7 } + }) + + equal(a.rangeBy({ word: 'a' }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 8, line: 1, offset: 7 } + }) +}) + +test.run() diff --git a/test/old-node.js b/test/old-node.js new file mode 100644 index 000000000..9f674c4c1 --- /dev/null +++ b/test/old-node.js @@ -0,0 +1,27 @@ +// eslint-disable-next-line +globalThis = Function('return this')() + +let Module = require('module') +let path = require('path') +let originalRequire = Module.prototype.require + +Module.prototype.require = function (request) { + if (request.startsWith('node:')) { + request = request.slice(5) + } + return originalRequire.call(this, request) +} + +process.env.TS_NODE_COMPILER_OPTIONS = JSON.stringify({ + moduleResolution: 'node' +}) + +require( + path.join( + process.cwd(), + 'node_modules', + 'ts-node', + 'register', + 'transpile-only' + ) +) diff --git a/test/parse.js b/test/parse.js deleted file mode 100644 index 3e37d3a28..000000000 --- a/test/parse.js +++ /dev/null @@ -1,129 +0,0 @@ -import parse from '../lib/parse'; -import Root from '../lib/root'; - -import cases from 'postcss-parser-tests'; -import path from 'path'; -import test from 'ava'; -import fs from 'fs'; - -test('works with file reads', t => { - let stream = fs.readFileSync(cases.path('atrule-empty.css')); - t.truthy(parse(stream) instanceof Root); -}); - -cases.each( (name, css, json) => { - test('parses ' + name, t => { - let parsed = cases.jsonify(parse(css, { from: name })); - t.deepEqual(parsed, json); - }); -}); - -test('saves source file', t => { - let css = parse('a {}', { from: 'a.css' }); - t.deepEqual(css.first.source.input.css, 'a {}'); - t.deepEqual(css.first.source.input.file, path.resolve('a.css')); - t.deepEqual(css.first.source.input.from, path.resolve('a.css')); -}); - -test('keeps absolute path in source', t => { - let css = parse('a {}', { from: 'http://example.com/a.css' }); - t.deepEqual(css.first.source.input.file, 'http://example.com/a.css'); - t.deepEqual(css.first.source.input.from, 'http://example.com/a.css'); -}); - -test('saves source file on previous map', t => { - let root1 = parse('a {}', { map: { inline: true } }); - let css = root1.toResult({ map: { inline: true } }).css; - let root2 = parse(css); - t.deepEqual(root2.first.source.input.file, path.resolve('to.css')); -}); - -test('sets unique ID for file without name', t => { - let css1 = parse('a {}'); - let css2 = parse('a {}'); - t.regex(css1.first.source.input.id, /^$/); - t.regex(css1.first.source.input.from, /^$/); - t.notDeepEqual(css2.first.source.input.id, css1.first.source.input.id); -}); - -test('sets parent node', t => { - let file = cases.path('atrule-rules.css'); - let css = parse(fs.readFileSync(file)); - - let support = css.first; - let keyframes = support.first; - let from = keyframes.first; - let decl = from.first; - - t.is(decl.parent, from); - t.is(from.parent, keyframes); - t.is(support.parent, css); - t.is(keyframes.parent, support); -}); - -test('ignores wrong close bracket', t => { - let root = parse('a { p: ()) }'); - t.deepEqual(root.first.first.value, '())'); -}); - -test('ignores symbols before declaration', t => { - let root = parse('a { :one: 1 }'); - t.deepEqual(root.first.first.raws.before, ' :'); -}); - -test('throws on unclosed blocks', t => { - t.throws(() => { - parse('\na {\n'); - }, /:2:1: Unclosed block/); -}); - -test('throws on unnecessary block close', t => { - t.throws(() => { - parse('a {\n} }'); - }, /:2:3: Unexpected }/); -}); - -test('throws on unclosed comment', t => { - t.throws(() => { - parse('\n/*\n '); - }, /:2:1: Unclosed comment/); -}); - -test('throws on unclosed quote', t => { - t.throws(() => { - parse('\n"\n\na '); - }, /:2:1: Unclosed quote/); -}); - -test('throws on unclosed bracket', t => { - t.throws(() => { - parse(':not(one() { }'); - }, /:1:5: Unclosed bracket/); -}); - -test('throws on property without value', t => { - t.throws(() => { - parse('a { b;}'); - }, /:1:5: Unknown word/); - t.throws(() => { - parse('a { b b }'); - }, /:1:5: Unknown word/); -}); - -test('throws on nameless at-rule', t => { - t.throws(() => { - parse('@'); - }, /:1:1: At-rule without name/); -}); - -test('throws on property without semicolon', t => { - t.throws(() => { - parse('a { one: filter(a:"") two: 2 }'); - }, /:1:21: Missed semicolon/); -}); - -test('throws on double colon', t => { - t.throws(() => { - parse('a { one:: 1 }'); - }, /:1:9: Double colon/); -}); diff --git a/test/parse.test.ts b/test/parse.test.ts new file mode 100755 index 000000000..06bde8385 --- /dev/null +++ b/test/parse.test.ts @@ -0,0 +1,281 @@ +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { eachTest, jsonify, testPath } from 'postcss-parser-tests' +import { test } from 'uvu' +import { equal, is, match, not, throws } from 'uvu/assert' + +import { + AtRule, + Declaration, + fromJSON, + parse, + Root, + Rule +} from '../lib/postcss.js' + +test('works with file reads', () => { + let stream = readFileSync(testPath('atrule-empty.css')) + is(parse(stream) instanceof Root, true) +}) + +eachTest((name, css, json) => { + test(`parses ${name}`, () => { + css = css.replace(/\r\n/g, '\n') + let parsed = jsonify(parse(css, { from: name })) + equal(parsed, json) + }) +}) + +test('parses UTF-8 BOM', () => { + let css = parse('\uFEFF@host { a {\f} }') + equal(css.nodes[0].raws.before, '') +}) + +test('should has true at hasBOM property', () => { + let css = parse('\uFEFF@host { a {\f} }') + is(css.first?.source?.input.hasBOM, true) +}) + +test('should has false at hasBOM property', () => { + let css = parse('@host { a {\f} }') + is(css.first?.source?.input.hasBOM, false) +}) + +test('parses carrier return', () => { + throws(() => { + parse('@font-face{ font:(\r/*);} body { a: "a*/)} a{}"}') + }, /:1:46: Unclosed string/) +}) + +test('saves source file', () => { + let css = parse('a {}', { from: 'a.css' }) + is(css.first?.source?.input.css, 'a {}') + is(css.first?.source?.input.file, resolve('a.css')) + is(css.first?.source?.input.from, resolve('a.css')) +}) + +test('keeps absolute path in source', () => { + let css = parse('a {}', { from: 'http://example.com/a.css' }) + is(css.first?.source?.input.file, 'http://example.com/a.css') + is(css.first?.source?.input.from, 'http://example.com/a.css') +}) + +test('saves source file on previous map', () => { + let root1 = parse('a {}', { map: { inline: true } }) + let css = root1.toResult({ map: { inline: true } }).css + let root2 = parse(css) + is(root2.first?.source?.input.file, resolve('to.css')) +}) + +test('sets unique ID for file without name', () => { + let css1 = parse('a {}') + let css2 = parse('a {}') + match(String(css1.first?.source?.input.id), /^$/) + match(String(css1.first?.source?.input.from), /^$/) + is.not(css2.first?.source?.input.id, css1.first?.source?.input.id) +}) + +test('sets parent node', () => { + let file = testPath('atrule-rules.css') + let css = parse(readFileSync(file)) + + let support = css.first as AtRule + let keyframes = support.first as AtRule + let from = keyframes.first as Rule + let decl = from.first as Declaration + + is(decl.parent, from) + is(from.parent, keyframes) + is(support.parent, css) + is(keyframes.parent, support) +}) + +test('ignores wrong close bracket', () => { + let root = parse('a { p: ()) }') + let a = root.first as Rule + let decl = a.first as Declaration + is(decl.value, '())') +}) + +test('parses unofficial --mixins', () => { + let root = parse(':root { --x { color: pink; }; }') + let rule = root.first as Rule + let prop = rule.first as Rule + is(prop.selector, '--x') +}) + +test('ignores symbols before declaration', () => { + let root = parse('a { :one: 1 }') + let a = root.first as Rule + let prop = a.first as Declaration + is(prop.raws.before, ' :') +}) + +test('parses double semicolon after rule', () => { + is(parse('a { };;').toString(), 'a { };;') +}) + +test('parses a functional property', () => { + let root = parse('a { b(c): d }') + let a = root.first as Rule + let b = a.first as Declaration + + is(b.prop, 'b(c)') +}) + +test('parses a functional tagname', () => { + let root = parse('a { b(c): d {} }') + let a = root.first as Rule + let b = a.first as Rule + + is(b.selector, 'b(c): d') +}) + +test('throws on unclosed blocks', () => { + throws(() => { + parse('\na {\n') + }, /:2:1: Unclosed block/) +}) + +test('throws on unnecessary block close', () => { + throws(() => { + parse('a {\n} }') + }, /:2:3: Unexpected }/) +}) + +test('throws on unclosed comment', () => { + throws(() => { + parse('\n/*\n ') + }, /:2:1: Unclosed comment/) +}) + +test('throws on unclosed quote', () => { + throws(() => { + parse('\n"\n\na ') + }, /:2:1: Unclosed string/) +}) + +test('throws on unclosed bracket', () => { + throws(() => { + parse(':not(one() { }') + }, /:1:5: Unclosed bracket/) +}) + +test('throws on property without value', () => { + throws(() => { + parse('a { b;}') + }, /:1:5: Unknown word/) + throws(() => { + parse('a { b b }') + }, /:1:5: Unknown word/) + throws(() => { + parse('a { b(); }') + }, /:1:5: Unknown word/) +}) + +test('throws on nameless at-rule', () => { + throws(() => { + parse('@') + }, /:1:1: At-rule without name/) +}) + +test('throws on property without semicolon', () => { + throws(() => { + parse('a { one: filter(a:"") two: 2 }') + }, /:1:21: Missed semicolon/) +}) + +test('throws on double colon', () => { + throws(() => { + parse('a { one:: 1 }') + }, /:1:9: Double colon/) +}) + +test('do not throws on comment in between', () => { + parse('a { b/* c */: 1 }') +}) + +test('throws on two words in between', () => { + throws(() => { + parse('a { b c: 1 }') + }, /:1:7: Unknown word/) +}) + +test('throws on just colon', () => { + throws(() => { + parse(':') + }, /:1:1: Unknown word/) + throws(() => { + parse(' : ') + }, /:1:2: Unknown word/) +}) + +test('does not suggest different parsers for CSS', () => { + let error: any + try { + parse('a { one:: 1 }', { from: 'app.css' }) + } catch (e) { + error = e + } + not.match(error.message, /postcss-less|postcss-scss/) +}) + +test('suggests postcss-scss for SCSS sources', () => { + throws(() => { + parse('a { #{var}: 1 }', { from: 'app.scss' }) + }, /postcss-scss/) +}) + +test('suggests postcss-sass for Sass sources', () => { + throws(() => { + parse('a\n #{var}: 1', { from: 'app.sass' }) + }, /postcss-sass/) +}) + +test('suggests postcss-less for Less sources', () => { + throws(() => { + parse('.@{my-selector} { }', { from: 'app.less' }) + }, /postcss-less/) +}) + +test('should give the correct column of missed semicolon with !important', () => { + let error: any + try { + parse('a { \n color: red !important\n background-color: black;\n}') + } catch (e) { + error = e + } + match(error.message, /2:26: Missed semicolon/) +}) + +test('should give the correct column of missed semicolon without !important', () => { + let error: any + try { + parse('a { \n color: red\n background-color: black;\n}') + } catch (e) { + error = e + } + match(error.message, /2:15: Missed semicolon/) +}) + +test('does not overflow the stack on deeply nested nodes', () => { + let depth = 6000 + let css = 'a{'.repeat(depth) + 'color:red' + '}'.repeat(depth) + + let root = parse(css) + is(root.toString(), css) + + let clone = root.clone() + is(clone.toString(), css) + + let json = root.toJSON() + is(fromJSON(json).toString(), css) + + let count = 0 + root.walk(() => { + count += 1 + }) + is(count, depth + 1) +}) + +test.run() diff --git a/test/postcss.js b/test/postcss.js deleted file mode 100644 index a3a561cc9..000000000 --- a/test/postcss.js +++ /dev/null @@ -1,142 +0,0 @@ -import Processor from '../lib/processor'; -import postcss from '../lib/postcss'; - -import test from 'ava'; - -test('creates plugins list', t => { - let processor = postcss(); - t.truthy(processor instanceof Processor); - t.deepEqual(processor.plugins, []); -}); - -test('saves plugins list', t => { - let a = () => 1; - let b = () => 2; - t.deepEqual(postcss(a, b).plugins, [a, b]); -}); - -test('saves plugins list as array', t => { - let a = () => 1; - let b = () => 2; - t.deepEqual(postcss([a, b]).plugins, [a, b]); -}); - -test('takes plugin from other processor', t => { - let a = () => 1; - let b = () => 2; - let c = () => 3; - let other = postcss([a, b]); - t.deepEqual(postcss([other, c]).plugins, [a, b, c]); -}); - -test('supports injecting additional processors at runtime', t => { - let plugin1 = postcss.plugin('one', () => { - return css => { - css.walkDecls(decl => { - decl.value = 'world'; - }); - }; - }); - let plugin2 = postcss.plugin('two', () => { - return (css, result) => { - result.processor.use(plugin1()); - }; - }); - - return postcss([ plugin2 ]).process('a{hello: bob}').then(result => { - t.deepEqual(result.css, 'a{hello: world}'); - }); -}); - -test('creates plugin', t => { - let plugin = postcss.plugin('test', filter => { - return function (css) { - css.walkDecls(filter || 'two', i => i.remove() ); - }; - }); - - let func1 = postcss(plugin).plugins[0]; - t.deepEqual(func1.postcssPlugin, 'test'); - t.regex(func1.postcssVersion, /\d+.\d+.\d+/); - - let func2 = postcss(plugin()).plugins[0]; - t.deepEqual(func2.postcssPlugin, func1.postcssPlugin); - t.deepEqual(func2.postcssVersion, func1.postcssVersion); - - let result1 = postcss(plugin('one')).process('a{ one: 1; two: 2 }'); - t.deepEqual(result1.css, 'a{ two: 2 }'); - - let result2 = postcss(plugin).process('a{ one: 1; two: 2 }'); - t.deepEqual(result2.css, 'a{ one: 1 }'); -}); - -test('does not call plugin constructor', t => { - let calls = 0; - let plugin = postcss.plugin('test', () => { - calls += 1; - return function () { }; - }); - t.is(calls, 0); - - postcss(plugin).process('a{}'); - t.is(calls, 1); - - postcss(plugin()).process('a{}'); - t.is(calls, 2); -}); - -test('creates a shortcut to process css', t => { - let plugin = postcss.plugin('test', (str = 'bar') => { - return function (css) { - css.walkDecls(i => { - i.value = str; - }); - }; - }); - - let result1 = plugin.process('a{value:foo}'); - t.deepEqual(result1.css, 'a{value:bar}'); - - let result2 = plugin.process('a{value:foo}', 'baz'); - t.deepEqual(result2.css, 'a{value:baz}'); - - plugin.process('a{value:foo}').then( result => { - t.deepEqual(result.css, 'a{value:bar}'); - }); -}); - -test('contains parser', t => { - t.deepEqual(postcss.parse('').type, 'root'); -}); - -test('contains stringifier', t => { - t.deepEqual(typeof postcss.stringify, 'function'); -}); - -test('allows to build own CSS', t => { - let root = postcss.root({ raws: { after: '\n' } }); - let comment = postcss.comment({ text: 'Example' }); - let media = postcss.atRule({ name: 'media', params: 'screen' }); - let rule = postcss.rule({ selector: 'a' }); - let decl = postcss.decl({ prop: 'color', value: 'black' }); - - root.append(comment); - rule.append(decl); - media.append(rule); - root.append(media); - - t.deepEqual(root.toString(), '/* Example */\n' + - '@media screen {\n' + - ' a {\n' + - ' color: black\n' + - ' }\n' + - '}\n'); -}); - -test('contains vendor module', t => { - t.deepEqual(postcss.vendor.prefix('-moz-tab'), '-moz-'); -}); - -test('contains list module', t => { - t.deepEqual(postcss.list.space('a b'), ['a', 'b']); -}); diff --git a/test/postcss.test.ts b/test/postcss.test.ts new file mode 100755 index 000000000..fddd982fe --- /dev/null +++ b/test/postcss.test.ts @@ -0,0 +1,192 @@ +import { restoreAll, spyOn } from 'nanospy' +import { test } from 'uvu' +import { equal, is, match, throws, type } from 'uvu/assert' + +import postcss = require('../lib/postcss.js') +import postcssDefault, { PluginCreator, Root } from '../lib/postcss.js' +import Processor from '../lib/processor.js' + +test.after.each(() => { + restoreAll() +}) + +test('default matches module.exports', () => { + is(postcss, postcssDefault) +}) + +test('creates plugins list', () => { + let processor = postcss() + is(processor instanceof Processor, true) + equal(processor.plugins, []) +}) + +test('saves plugins list', () => { + let a = (): void => {} + let b = (): void => {} + equal(postcss(a, b).plugins, [a, b]) +}) + +test('saves plugins list as array', () => { + let a = (): void => {} + let b = (): void => {} + equal(postcss([a, b]).plugins, [a, b]) +}) + +test('takes plugin from other processor', () => { + let a = (): void => {} + let b = (): void => {} + let c = (): void => {} + let other = postcss([a, b]) + equal(postcss([other, c]).plugins, [a, b, c]) +}) + +test('takes plugins from a a plugin returning a processor', () => { + let a = (): void => {} + let b = (): void => {} + let c = (): void => {} + let other = postcss([a, b]) + let meta = (() => other) as PluginCreator + meta.postcss = true + equal(postcss([other, c]).plugins, [a, b, c]) +}) + +test('contains parser', () => { + is(postcss.parse('').type, 'root') +}) + +test('contains stringifier', () => { + type(postcss.stringify, 'function') +}) + +test('allows to build own CSS', () => { + let root = postcss.root({ raws: { after: '\n' } }) + let comment = postcss.comment({ text: 'Example' }) + let media = postcss.atRule({ name: 'media', params: 'screen' }) + let rule = postcss.rule({ selector: 'a' }) + let decl = postcss.decl({ prop: 'color', value: 'black' }) + + root.append(comment) + rule.append(decl) + media.append(rule) + root.append(media) + + is( + root.toString(), + '/* Example */\n' + + '@media screen {\n' + + ' a {\n' + + ' color: black\n' + + ' }\n' + + '}\n' + ) +}) + +test('allows to build own CSS with Document', () => { + let document = postcss.document() + let root = postcss.root({ raws: { after: '\n' } }) + let comment = postcss.comment({ text: 'Example' }) + let media = postcss.atRule({ name: 'media', params: 'screen' }) + let rule = postcss.rule({ selector: 'a' }) + let decl = postcss.decl({ prop: 'color', value: 'black' }) + + root.append(comment) + rule.append(decl) + media.append(rule) + root.append(media) + document.append(root) + + is( + document.toString(), + '/* Example */\n' + + '@media screen {\n' + + ' a {\n' + + ' color: black\n' + + ' }\n' + + '}\n' + ) +}) + +test('contains list module', () => { + equal(postcss.list.space('a b'), ['a', 'b']) +}) + +test('works with null', () => { + throws(() => { + // @ts-expect-error Testing invalid input + postcss([() => {}]).process(null).css + }, /PostCSS received null instead of CSS string/) +}) + +test('has deprecated method to create plugins', () => { + let warn = spyOn(console, 'warn', () => {}) + let plugin = (postcss as any).plugin('test', (filter?: string) => { + return (root: Root) => { + root.walkDecls(filter ?? 'two', i => { + i.remove() + }) + } + }) + + equal(warn.callCount, 0) + + let func1: any = postcss(plugin).plugins[0] + is(func1.postcssPlugin, 'test') + match(func1.postcssVersion, /\d+.\d+.\d+/) + equal(warn.callCount, 1) + + let func2: any = postcss(plugin()).plugins[0] + equal(func2.postcssPlugin, func1.postcssPlugin) + equal(func2.postcssVersion, func1.postcssVersion) + + let result1 = postcss(plugin('one')).process('a{ one: 1; two: 2 }') + is(result1.css, 'a{ two: 2 }') + + let result2 = postcss(plugin).process('a{ one: 1; two: 2 }') + is(result2.css, 'a{ one: 1 }') + + equal(warn.callCount, 1) + match(warn.calls[0][0], /postcss\.plugin was deprecated/) +}) + +test('creates a shortcut to process css', async () => { + let warn = spyOn(console, 'warn', () => {}) + let plugin = (postcss as any).plugin('test', (str?: string) => { + return (root: Root) => { + root.walkDecls(i => { + i.value = str ?? 'bar' + }) + } + }) + + let result1 = plugin.process('a{value:foo}') + is(result1.css, 'a{value:bar}') + + let result2 = plugin.process('a{value:foo}', {}, 'baz') + is(result2.css, 'a{value:baz}') + + let result = await plugin.process('a{value:foo}', { from: 'a' }, 'baz') + equal(result.opts, { from: 'a' }) + is(result.css, 'a{value:baz}') + + equal(warn.callCount, 1) +}) + +test('does not call plugin constructor', () => { + let warn = spyOn(console, 'warn', () => {}) + let calls = 0 + let plugin = (postcss as any).plugin('test', () => { + calls += 1 + return () => {} + }) + is(calls, 0) + + postcss(plugin).process('a{}') + is(calls, 1) + + postcss(plugin()).process('a{}') + is(calls, 2) + + equal(warn.callCount, 1) +}) + +test.run() diff --git a/test/previous-map.js b/test/previous-map.js deleted file mode 100644 index 56db07344..000000000 --- a/test/previous-map.js +++ /dev/null @@ -1,195 +0,0 @@ -import parse from '../lib/parse'; - -import mozilla from 'source-map'; -import path from 'path'; -import test from 'ava'; -import fs from 'fs-extra'; - -let dir = path.join(__dirname, 'prevmap-fixtures'); -let mapObj = { - version: 3, - file: null, - sources: [], - names: [], - mappings: '' -}; -let map = JSON.stringify(mapObj); - -test.afterEach( () => { - if ( fs.existsSync(dir) ) fs.removeSync(dir); -}); - -test('misses property if no map', t => { - t.deepEqual(typeof parse('a{}').source.input.map, 'undefined'); -}); - -test('creates property if map present', t => { - let root = parse('a{}', { map: { prev: map } }); - t.deepEqual(root.source.input.map.text, map); -}); - -test('returns consumer', t => { - let obj = parse('a{}', { map: { prev: map } }).source.input.map.consumer(); - t.truthy(obj instanceof mozilla.SourceMapConsumer); -}); - -test('sets annotation property', t => { - let mapOpts = { map: { prev: map } }; - - let root1 = parse('a{}', mapOpts); - t.deepEqual(typeof root1.source.input.map.annotation, 'undefined'); - - let root2 = parse('a{}/*# sourceMappingURL=a.css.map */', mapOpts); - t.deepEqual(root2.source.input.map.annotation, 'a.css.map'); -}); - -test('checks previous sources content', t => { - let map2 = { - version: 3, - file: 'b', - sources: ['a'], - names: [], - mappings: '' - }; - - let opts = { map: { prev: map2 } }; - t.false(parse('a{}', opts).source.input.map.withContent()); - - map2.sourcesContent = ['a{}']; - t.true(parse('a{}', opts).source.input.map.withContent()); -}); - -test('decodes base64 maps', t => { - let b64 = new Buffer(map).toString('base64'); - let css = 'a{}\n' + - `/*# sourceMappingURL=data:application/json;base64,${b64} */`; - - t.deepEqual(parse(css).source.input.map.text, map); -}); - -test('decodes base64 UTF-8 maps', t => { - let b64 = new Buffer(map).toString('base64'); - let css = 'a{}\n/*# sourceMappingURL=data:application/json;' + - 'charset=utf-8;base64,' + b64 + ' */'; - - t.deepEqual(parse(css).source.input.map.text, map); -}); - -test('accepts different name for UTF-8 encoding', t => { - let b64 = new Buffer(map).toString('base64'); - let css = 'a{}\n/*# sourceMappingURL=data:application/json;' + - 'charset=utf8;base64,' + b64 + ' */'; - - t.deepEqual(parse(css).source.input.map.text, map); -}); - -test('decodes URI maps', t => { - let uri = 'data:application/json,' + decodeURI(map); - let css = `a{}\n/*# sourceMappingURL=${ uri } */`; - - t.deepEqual(parse(css).source.input.map.text, map); -}); - -test('removes map on request', t => { - let uri = 'data:application/json,' + decodeURI(map); - let css = `a{}\n/*# sourceMappingURL=${ uri } */`; - - let input = parse(css, { map: { prev: false } }).source.input; - t.deepEqual(typeof input.map, 'undefined'); -}); - -test('raises on unknown inline encoding', t => { - let css = 'a { }\n/*# sourceMappingURL=data:application/json;' + - 'md5,68b329da9893e34099c7d8ad5cb9c940*/'; - - t.throws( () => { - parse(css); - }, 'Unsupported source map encoding md5'); -}); - -test('raises on unknown map format', t => { - t.throws( () => { - parse('a{}', { map: { prev: 1 } }); - }, 'Unsupported previous source map format: 1'); -}); - -test('reads map from annotation', t => { - let file = path.join(dir, 'a.map'); - fs.outputFileSync(file, map); - let root = parse('a{}\n/*# sourceMappingURL=a.map */', { from: file }); - - t.deepEqual(root.source.input.map.text, map); - t.deepEqual(root.source.input.map.root, dir); -}); - -test('sets uniq name for inline map', t => { - let map2 = { - version: 3, - sources: ['a'], - names: [], - mappings: '' - }; - - let opts = { map: { prev: map2 } }; - let file1 = parse('a{}', opts).source.input.map.file; - let file2 = parse('a{}', opts).source.input.map.file; - - t.regex(file1, /^$/); - t.notDeepEqual(file1, file2); -}); - -test('should accept an empty mappings string', () => { - let emptyMap = { - version: 3, - sources: [], - names: [], - mappings: '' - }; - parse('body{}', { map: { prev: emptyMap } } ); -}); - -test('should accept a function', t => { - let css = 'body{}\n/*# sourceMappingURL=a.map */'; - let file = path.join(dir, 'previous-sourcemap-function.map'); - fs.outputFileSync(file, map); - let opts = { - map: { - prev: (/* from */) => file - } - }; - let root = parse(css, opts); - t.deepEqual(root.source.input.map.text, map); - t.deepEqual(root.source.input.map.annotation, 'a.map'); -}); - -test('should call function with opts.from', t => { - t.plan(1); - - let css = 'body{}\n/*# sourceMappingURL=a.map */'; - let file = path.join(dir, 'previous-sourcemap-function.map'); - fs.outputFileSync(file, map); - let opts = { - from: 'a.css', - map: { - prev: from => { - t.deepEqual(from, 'a.css'); - return file; - } - } - }; - parse(css, opts); -}); - -test('should raise when function returns invalid path', t => { - let css = 'body{}\n/*# sourceMappingURL=a.map */'; - let fakeMap = Number.MAX_SAFE_INTEGER.toString() + '.map'; - let fakePath = path.join(dir, fakeMap); - let opts = { - map: { - prev: () => fakePath - } - }; - t.throws( () => { - parse(css, opts); - }, 'Unable to load previous source map: ' + fakePath); -}); diff --git a/test/previous-map.test.ts b/test/previous-map.test.ts new file mode 100755 index 000000000..5fa8b76bd --- /dev/null +++ b/test/previous-map.test.ts @@ -0,0 +1,411 @@ +import { + existsSync, + lstatSync, + mkdirSync, + readdirSync, + rmdirSync, + unlinkSync, + writeFileSync +} from 'fs' +import { join } from 'path' +import { SourceMapConsumer } from 'source-map-js' +import { pathToFileURL } from 'url' +import { test } from 'uvu' +import { equal, is, match, not, throws, type } from 'uvu/assert' + +import { parse } from '../lib/postcss.js' + +let dir = join(__dirname, 'prevmap-fixtures') +let mapObj = { + file: null, + mappings: '', + names: [], + sources: [], + version: 3 +} +let map = JSON.stringify(mapObj) + +function deleteDir(path: string): void { + if (existsSync(path)) { + readdirSync(path).forEach(i => { + let file = join(path, i) + if (lstatSync(file).isDirectory()) { + deleteDir(file) + } else { + unlinkSync(file) + } + }) + rmdirSync(path) + } +} + +test.after.each(() => { + deleteDir(dir) +}) + +test('misses property if no map', () => { + type(parse('a{}').source?.input.map, 'undefined') +}) + +test('creates property if map present', () => { + let root = parse('a{}', { map: { prev: map } }) + is(root.source?.input.map.text, map) +}) + +test('returns consumer', () => { + let obj = parse('a{}', { map: { prev: map } }).source?.input.map.consumer() + is(obj instanceof SourceMapConsumer, true) +}) + +test('sets annotation property', () => { + let mapOpts = { map: { prev: map } } + + let root1 = parse('a{}', mapOpts) + type(root1.source?.input.map.annotation, 'undefined') + + let root2 = parse('a{}/*# sourceMappingURL=a.css.map */', mapOpts) + is(root2.source?.input.map.annotation, 'a.css.map') +}) + +test('checks previous sources content', () => { + let map2: any = { + file: 'b', + mappings: '', + names: [], + sources: ['a'], + version: 3 + } + + let opts = { map: { prev: map2 } } + is(parse('a{}', opts).source?.input.map.withContent(), false) + + map2.sourcesContent = ['a{}'] + is(parse('a{}', opts).source?.input.map.withContent(), true) +}) + +test('decodes base64 maps', () => { + let b64 = Buffer.from(map).toString('base64') + let css = + 'a{}\n' + `/*# sourceMappingURL=data:application/json;base64,${b64} */` + + is(parse(css).source?.input.map.text, map) +}) + +test('decodes base64 UTF-8 maps', () => { + let b64 = Buffer.from(map).toString('base64') + let css = + 'a{}\n/*# sourceMappingURL=data:application/json;' + + 'charset=utf-8;base64,' + + b64 + + ' */' + + is(parse(css).source?.input.map.text, map) +}) + +test('accepts different name for base64 maps with UTF-8 encoding', () => { + let b64 = Buffer.from(map).toString('base64') + let css = + 'a{}\n/*# sourceMappingURL=data:application/json;' + + 'charset=utf8;base64,' + + b64 + + ' */' + + is(parse(css).source?.input.map.text, map) +}) + +test('decodes URI maps', () => { + let uri = 'data:application/json,' + decodeURI(map) + let css = `a{}\n/*# sourceMappingURL=${uri} */` + + is(parse(css).source?.input.map.text, map) +}) + +test('decodes URI UTF-8 maps', () => { + let uri = decodeURI(map) + let css = + 'a{}\n/*# sourceMappingURL=data:application/json;' + + 'charset=utf-8,' + + uri + + ' */' + + is(parse(css).source?.input.map.text, map) +}) + +test('accepts different name for URI maps with UTF-8 encoding', () => { + let uri = decodeURI(map) + let css = + 'a{}\n/*# sourceMappingURL=data:application/json;' + + 'charset=utf8,' + + uri + + ' */' + + is(parse(css).source?.input.map.text, map) +}) + +test('removes map on request', () => { + let uri = 'data:application/json,' + decodeURI(map) + let css = `a{}\n/*# sourceMappingURL=${uri} */` + + let input = parse(css, { map: { prev: false } }).source?.input + type(input?.map, 'undefined') +}) + +test('raises on unknown inline encoding', () => { + let css = + 'a { }\n/*# sourceMappingURL=data:application/json;' + + 'md5,68b329da9893e34099c7d8ad5cb9c940*/' + + throws(() => { + parse(css) + }, 'Unsupported source map encoding md5') +}) + +test('raises on unknown map format', () => { + throws(() => { + // @ts-expect-error Invalid input + parse('a{}', { map: { prev: 1 } }) + }, 'Unsupported previous source map format: 1') +}) + +test('reads map from annotation', () => { + let file = join(dir, 'a.map') + mkdirSync(dir) + writeFileSync(file, map) + let root = parse('a{}\n/*# sourceMappingURL=a.map */', { from: file }) + + is(root.source?.input.map.text, map) + is(root.source?.input.map.root, dir) +}) + +test('reads only the last map from annotation', () => { + let file = join(dir, 'c.map') + mkdirSync(dir) + writeFileSync(file, map) + let root = parse( + 'a{}' + + '\n/*# sourceMappingURL=a.map */' + + '\n/*# sourceMappingURL=b.map */' + + '\n/*# sourceMappingURL=c.map */', + { from: file } + ) + + is(root.source?.input.map.text, map) + is(root.source?.input.map.root, dir) +}) + +test('sets unique name for inline map', () => { + let map2 = { + mappings: '', + names: [], + sources: ['a'], + version: 3 + } + + let opts = { map: { prev: map2 } } + let file1 = parse('a{}', opts).source?.input.map.file + let file2 = parse('a{}', opts).source?.input.map.file + + match(String(file1), /^$/) + is.not(file1, file2) +}) + +test('accepts an empty mappings string', () => { + not.throws(() => { + let emptyMap = { + mappings: '', + names: [], + sources: [], + version: 3 + } + parse('body{}', { map: { prev: emptyMap } }) + }) +}) + +test('accepts a function', () => { + let css = 'body{}\n/*# sourceMappingURL=a.map */' + let file = join(dir, 'previous-sourcemap-function.map') + mkdirSync(dir) + writeFileSync(file, map) + let opts = { + map: { + prev: () => file + } + } + let root = parse(css, opts) + is(root.source?.input.map.text, map) + is(root.source?.input.map.annotation, 'a.map') +}) + +test('calls function with opts.from', () => { + let css = 'body{}\n/*# sourceMappingURL=a.map */' + let file = join(dir, 'previous-sourcemap-function.map') + mkdirSync(dir) + writeFileSync(file, map) + parse(css, { + from: 'a.css', + map: { + prev: from => { + is(from, 'a.css') + return file + } + } + }) +}) + +test('raises when function returns invalid path', () => { + let css = 'body{}\n/*# sourceMappingURL=a.map */' + let fakeMap = Number.MAX_SAFE_INTEGER.toString() + '.map' + let fakePath = join(dir, fakeMap) + let opts = { + map: { + prev: () => fakePath + } + } + throws(() => { + parse(css, opts) + }, 'Unable to load previous source map: ' + fakePath) +}) + +test('uses source map path as a root', () => { + let from = join(dir, 'a.css') + mkdirSync(dir) + mkdirSync(join(dir, 'maps')) + writeFileSync( + join(dir, 'maps', 'a.map'), + JSON.stringify({ + file: 'test.css', + mappings: 'AACA,CAAC,CACG,GAAG,CAAC;EACF,KAAK,EAAE,GAAI;CACZ', + names: [], + sources: ['../../test.scss'], + version: 3 + }) + ) + let root = parse( + '* div {\n color: red;\n }\n/*# sourceMappingURL=maps/a.map */', + { from } + ) + equal(root.source?.input.origin(1, 4, 1, 6), { + column: 5, + endColumn: 8, + endLine: 3, + file: join(dir, '..', 'test.scss'), + line: 3, + url: pathToFileURL(join(dir, '..', 'test.scss')).href + }) +}) + +test('does not load map from non-.map file', () => { + let from = join(dir, 'a.css') + mkdirSync(dir) + writeFileSync(join(dir, 'a.txt'), map) + let input = parse('a{}\n/*# sourceMappingURL=a.txt */', { from }).source + ?.input + type(input?.map, 'undefined') +}) + +test('does not load map from outside the from folder', () => { + let from = join(dir, 'subdir', 'a.css') + mkdirSync(dir) + mkdirSync(join(dir, 'subdir')) + writeFileSync(join(dir, 'outside.map'), map) + let input = parse('a{}\n/*# sourceMappingURL=../outside.map */', { from }) + .source?.input + type(input?.map, 'undefined') +}) + +test('does not load relative map without from', () => { + let cwd = join(dir, 'subdir') + mkdirSync(dir) + mkdirSync(cwd) + writeFileSync(join(cwd, 'previous.map'), map) + let previousCwd = process.cwd() + try { + process.chdir(cwd) + let input = parse('a{}\n/*# sourceMappingURL=previous.map */').source?.input + type(input?.map, 'undefined') + } finally { + process.chdir(previousCwd) + } +}) + +test('loads map from outside the from folder with unsafeMap', () => { + let from = join(dir, 'subdir', 'a.css') + mkdirSync(dir) + mkdirSync(join(dir, 'subdir')) + writeFileSync(join(dir, 'outside.map'), map) + let input = parse('a{}\n/*# sourceMappingURL=../outside.map */', { + from, + unsafeMap: true + }).source?.input + is(input?.map.text, map) +}) + +test('uses current file path for source map', () => { + let root = parse('a{b:1}', { + from: join(__dirname, 'dir', 'subdir', 'a.css'), + map: { + prev: { + file: 'test.css', + mappings: 'AAAA,CAAC;EAAC,CAAC,EAAC,CAAC', + names: [], + sources: ['../test.scss'], + version: 3 + } + } + }) + equal(root.source?.input.origin(1, 1), { + column: 1, + endColumn: undefined, + endLine: undefined, + file: join(__dirname, 'dir', 'test.scss'), + line: 1, + url: pathToFileURL(join(__dirname, 'dir', 'test.scss')).href + }) +}) + +test('works with non-file sources', () => { + let root = parse('a{b:1}', { + from: join(__dirname, 'dir', 'subdir', 'a.css'), + map: { + prev: { + file: 'test.css', + mappings: 'AAAA,CAAC;EAAC,CAAC,EAAC,CAAC', + names: [], + sources: ['http://example.com/test.scss'], + version: 3 + } + } + }) + equal(root.source?.input.origin(1, 1), { + column: 1, + endColumn: undefined, + endLine: undefined, + line: 1, + url: 'http://example.com/test.scss' + }) +}) + +test('works with index map', () => { + let root = parse('body {\nwidth:100%;\n}', { + from: join(__dirname, 'a.css'), + map: { + prev: { + sections: [ + { + map: { + mappings: 'AAAA;AACA;AACA;', + sources: ['b.css'], + sourcesContent: ['body {\nwidth:100%;\n}'], + version: 3 + }, + offset: { column: 0, line: 0 } + } + ], + version: 3 + } + } + }) + is((root as any).source.input.origin(1, 2).file, join(__dirname, 'b.css')) +}) + +test.run() diff --git a/test/processor.js b/test/processor.js deleted file mode 100644 index c828596e2..000000000 --- a/test/processor.js +++ /dev/null @@ -1,431 +0,0 @@ -import LazyResult from '../lib/lazy-result'; -import Processor from '../lib/processor'; -import postcss from '../lib/postcss'; -import Result from '../lib/result'; -import parse from '../lib/parse'; -import Root from '../lib/root'; - -import sinon from 'sinon'; -import path from 'path'; -import test from 'ava'; - -function prs() { - return new Root({ raws: { after: 'ok' } }); -} - -function str(node, builder) { - builder(node.raws.after + '!'); -} - -let beforeFix = new Processor([ css => { - css.walkRules( rule => { - if ( !rule.selector.match(/::(before|after)/) ) return; - if ( !rule.some( i => i.prop === 'content' ) ) { - rule.prepend({ prop: 'content', value: '""' }); - } - }); -}]); - -test.before( () => { - sinon.stub(console, 'warn'); -}); - -test.after( () => { - console.warn.restore(); -}); - -test('adds new plugins', t => { - let a = () => 1; - let processor = new Processor(); - processor.use(a); - t.deepEqual(processor.plugins, [a]); -}); - -test('adds new plugin by object', t => { - let a = () => 1; - let processor = new Processor(); - processor.use({ postcss: a }); - t.deepEqual(processor.plugins, [a]); -}); - -test('adds new plugin by object-function', t => { - let a = () => 1; - let obj = () => 2; - obj.postcss = a; - let processor = new Processor(); - processor.use(obj); - t.deepEqual(processor.plugins, [a]); -}); - -test('adds new processors of another postcss instance', t => { - let a = () => 1; - let processor = new Processor(); - let other = new Processor([a]); - processor.use(other); - t.deepEqual(processor.plugins, [a]); -}); - -test('adds new processors from object', t => { - let a = () => 1; - let processor = new Processor(); - let other = new Processor([a]); - processor.use({ postcss: other }); - t.deepEqual(processor.plugins, [a]); -}); - -test('returns itself', t => { - let a = () => 1; - let b = () => 2; - let processor = new Processor(); - t.deepEqual(processor.use(a).use(b).plugins, [a, b]); -}); - -test('throws on wrong format', t => { - let pr = new Processor(); - t.throws( () => { - pr.use(1); - }, /1 is not a PostCSS plugin/); -}); - -test('processes CSS', t => { - let result = beforeFix.process('a::before{top:0}'); - t.deepEqual(result.css, 'a::before{content:"";top:0}'); -}); - -test('processes parsed AST', t => { - let root = parse('a::before{top:0}'); - let result = beforeFix.process(root); - t.deepEqual(result.css, 'a::before{content:"";top:0}'); -}); - -test('processes previous result', t => { - let result = (new Processor()).process('a::before{top:0}'); - result = beforeFix.process(result); - t.deepEqual(result.css, 'a::before{content:"";top:0}'); -}); - -test('takes maps from previous result', t => { - let one = (new Processor()).process('a{}', { - from: 'a.css', - to: 'b.css', - map: { inline: false } - }); - let two = (new Processor()).process(one, { to: 'c.css' }); - t.deepEqual(two.map.toJSON().sources, ['a.css']); -}); - -test('inlines maps from previous result', t => { - let one = (new Processor()).process('a{}', { - from: 'a.css', - to: 'b.css', - map: { inline: false } - }); - let two = (new Processor()).process(one, { - to: 'c.css', - map: { inline: true } - }); - t.deepEqual(typeof two.map, 'undefined'); -}); - -test('throws with file name', t => { - let error; - try { - (new Processor()).process('a {', { from: 'a.css' }).css; - } catch (e) { - if ( e.name === 'CssSyntaxError' ) { - error = e; - } else { - throw e; - } - } - - t.deepEqual(error.file, path.resolve('a.css')); - t.regex(error.message, /a.css:1:1: Unclosed block$/); -}); - -test('allows to replace Root', t => { - let plugin = (css, result) => { - result.root = new Root(); - }; - let processor = new Processor([plugin]); - t.deepEqual(processor.process('a {}').css, ''); -}); - -test('returns LazyResult object', t => { - let result = (new Processor()).process('a{}'); - t.truthy(result instanceof LazyResult); - t.deepEqual(result.css, 'a{}'); - t.deepEqual(result.toString(), 'a{}'); -}); - -test('calls all plugins once', t => { - t.plan(1); - - let calls = ''; - let a = () => { - calls += 'a'; - }; - let b = () => { - calls += 'b'; - }; - - let result = new Processor([a, b]).process(''); - result.css; - result.map; - result.root; - return result.then( () => { - t.deepEqual(calls, 'ab'); - }); -}); - -test('parses, converts and stringifies CSS', t => { - let a = css => t.truthy(css instanceof Root); - t.deepEqual(typeof (new Processor([a])).process('a {}').css, 'string'); -}); - -test('send result to plugins', t => { - let processor = new Processor(); - let a = (css, result) => { - t.truthy(result instanceof Result); - t.deepEqual(result.processor, processor); - t.deepEqual(result.opts, { map: true }); - t.deepEqual(result.root, css); - }; - processor.use(a).process('a {}', { map: true }); -}); - -test('accepts source map from PostCSS', t => { - let one = (new Processor()).process('a{}', { - from: 'a.css', - to: 'b.css', - map: { inline: false } - }); - let two = (new Processor()).process(one.css, { - from: 'b.css', - to: 'c.css', - map: { prev: one.map, inline: false } - }); - t.deepEqual(two.map.toJSON().sources, ['a.css']); -}); - -test('supports async plugins', t => { - let starts = 0; - let finish = 0; - let async1 = css => { - return new Promise(resolve => { - starts += 1; - setTimeout(() => { - t.deepEqual(starts, 1); - - css.append('a {}'); - finish += 1; - resolve(); - }, 1); - }); - }; - let async2 = css => { - return new Promise(resolve => { - t.deepEqual(starts, 1); - t.deepEqual(finish, 1); - - starts += 1; - setTimeout(() => { - css.append('b {}'); - finish += 1; - resolve(); - }, 1); - }); - }; - return (new Processor([async1, async2])).process('').then( result => { - t.deepEqual(starts, 2); - t.deepEqual(finish, 2); - t.deepEqual(result.css, 'a {}\nb {}'); - }); -}); - -test('works async without plugins', t => { - return (new Processor()).process('a {}').then( result => { - t.deepEqual(result.css, 'a {}'); - }); -}); - -test('runs async plugin only once', t => { - t.plan(1); - - let calls = 0; - let async = () => { - return new Promise( resolve => { - setTimeout(() => { - calls += 1; - resolve(); - }, 1); - }); - }; - - let result = (new Processor([async])).process('a {}'); - result.then( () => { }); - return result.then( () => { - return result.then( () => { - t.deepEqual(calls, 1); - }); - }); -}); - -test('supports async errors', t => { - let error = new Error('Async'); - let async = () => { - return new Promise( (resolve, reject) => { - reject(error); - }); - }; - let result = (new Processor([async])).process(''); - return result.then( () => { - t.fail(); - }).catch( err => { - t.deepEqual(err, error); - return result.catch( err2 => { - t.deepEqual(err2, error); - }); - }); -}); - -test('supports sync errors in async mode', t => { - let error = new Error('Async'); - let async = () => { - throw error; - }; - return (new Processor([async])).process('').then( () => { - t.fail(); - }).catch( err => { - t.deepEqual(err, error); - }); -}); - -test('throws parse error in async', t => { - return (new Processor()).process('a{').catch( err => { - t.deepEqual(err.message, ':1:1: Unclosed block'); - }); -}); - -test('throws error on sync method to async plugin', t => { - let async = () => { - return new Promise( resolve => resolve() ); - }; - t.throws( () => { - (new Processor([async])).process('a{}').css; - }, /async/); -}); - -test('throws a sync call in async running', t => { - let async = () => new Promise( done => setTimeout(done, 1) ); - - let processor = (new Processor([async])).process('a{}'); - processor.async(); - - t.throws( () => { - processor.sync(); - }, /then/); -}); - -test('checks plugin compatibility', t => { - let plugin = postcss.plugin('test', () => { - return () => { - throw new Error('Er'); - }; - }); - let func = plugin(); - func.postcssVersion = '2.1.5'; - - let processBy = version => { - let processor = new Processor([func]); - processor.version = version; - processor.process('a{}').css; - }; - - t.throws( () => { - processBy('1.0.0'); - }, 'Er'); - t.deepEqual(console.warn.callCount, 1); - t.deepEqual(console.warn.args[0][0], - 'Your current PostCSS version is 1.0.0, but test uses 2.1.5. ' + - 'Perhaps this is the source of the error below.'); - - t.throws( () => { - processBy('3.0.0'); - }, 'Er'); - t.deepEqual(console.warn.callCount, 2); - - t.throws( () => { - processBy('2.0.0'); - }, 'Er'); - t.deepEqual(console.warn.callCount, 3); - - t.throws( () => { - processBy('2.1.0'); - }, 'Er'); - t.deepEqual(console.warn.callCount, 3); -}); - -test('sets last plugin to result', t => { - let plugin1 = function (css, result) { - t.is(result.lastPlugin, plugin1); - }; - let plugin2 = function (css, result) { - t.is(result.lastPlugin, plugin2); - }; - - let processor = new Processor([plugin1, plugin2]); - return processor.process('a{}').then( result => { - t.is(result.lastPlugin, plugin2); - }); -}); - -test('uses custom parsers', t => { - let processor = new Processor([]); - return processor.process('a{}', { parser: prs }).then( result => { - t.deepEqual(result.css, 'ok'); - }); -}); - -test('uses custom parsers from object', t => { - let processor = new Processor([]); - let syntax = { parse: prs, stringify: str }; - return processor.process('a{}', { parser: syntax }).then( result => { - t.deepEqual(result.css, 'ok'); - }); -}); - -test('uses custom stringifier', t => { - let processor = new Processor([]); - return processor.process('a{}', { stringifier: str }).then( result => { - t.deepEqual(result.css, '!'); - }); -}); - -test('uses custom stringifier from object', t => { - let processor = new Processor([]); - let syntax = { parse: prs, stringify: str }; - return processor.process('', { stringifier: syntax }).then( result => { - t.deepEqual(result.css, '!'); - }); -}); - -test('uses custom stringifier with source maps', t => { - let processor = new Processor([]); - return processor.process('a{}', { map: true, stringifier: str }) - .then( result => { - t.regex(result.css, /!\n\/\*# sourceMap/); - }); -}); - -test('uses custom syntax', t => { - let processor = new Processor([]); - let syntax = { parse: prs, stringify: str }; - return processor.process('a{}', { syntax }).then( result => { - t.deepEqual(result.css, 'ok!'); - }); -}); - -test('contains PostCSS version', t => { - t.regex((new Processor()).version, /\d+.\d+.\d+/); -}); diff --git a/test/processor.test.ts b/test/processor.test.ts new file mode 100755 index 000000000..74803073d --- /dev/null +++ b/test/processor.test.ts @@ -0,0 +1,639 @@ +import { delay } from 'nanodelay' +import { restoreAll, spyOn } from 'nanospy' +import { resolve as pathResolve } from 'path' +import { test } from 'uvu' +import { equal, instance, is, match, not, throws, type } from 'uvu/assert' + +import CssSyntaxError from '../lib/css-syntax-error.js' +import LazyResult from '../lib/lazy-result.js' +import NoWorkResult from '../lib/no-work-result.js' +import postcss, { + Document, + Node, + parse, + Parser, + Plugin, + PluginCreator, + Result, + Root, + Stringifier +} from '../lib/postcss.js' +import Processor from '../lib/processor.js' +import Rule from '../lib/rule.js' + +test.after.each(() => { + restoreAll() +}) + +function prs(): Root { + return new Root({ raws: { after: 'ok' } }) +} + +function str(node: Node, builder: (s: string) => void): void { + builder(`${node.raws.after}!`) +} + +async function catchError(cb: () => Promise): Promise { + try { + await cb() + } catch (e) { + if (e instanceof Error) return e + } + throw new Error('Error was not thrown') +} + +let beforeFix = new Processor([ + (root: Root) => { + root.walkRules(rule => { + if (!rule.selector.match(/::(before|after)/)) return + if (!rule.some(i => i.type === 'decl' && i.prop === 'content')) { + rule.prepend({ prop: 'content', value: '""' }) + } + }) + } +]) + +test('adds new plugins', () => { + let a = (): void => {} + let processor = new Processor() + processor.use(a) + equal(processor.plugins, [a]) +}) + +test('adds new plugin by object', () => { + let a = (): void => {} + let processor = new Processor() + processor.use({ postcss: a }) + equal(processor.plugins, [a]) +}) + +test('adds new plugin by object-function', () => { + let a = (): void => {} + let obj: any = () => {} + obj.postcss = a + let processor = new Processor() + processor.use(obj) + equal(processor.plugins, [a]) +}) + +test('adds new processors of another postcss instance', () => { + let a = (): void => {} + let processor = new Processor() + let other = new Processor([a]) + processor.use(other) + equal(processor.plugins, [a]) +}) + +test('adds new processors from object', () => { + let a = (): void => {} + let processor = new Processor() + let other = new Processor([a]) + processor.use({ postcss: other }) + equal(processor.plugins, [a]) +}) + +test('returns itself', () => { + let a = (): void => {} + let b = (): void => {} + let processor = new Processor() + equal(processor.use(a).use(b).plugins, [a, b]) +}) + +test('throws on wrong format', () => { + let pr = new Processor() + throws(() => { + // @ts-expect-error Testing invalid API + pr.use(1) + }, /1 is not a PostCSS plugin/) +}) + +test('processes CSS', () => { + let result = beforeFix.process('a::before{top:0}') + is(result.css, 'a::before{content:"";top:0}') +}) + +test('processes parsed AST', () => { + let root = parse('a::before{top:0}') + let result = beforeFix.process(root) + is(result.css, 'a::before{content:"";top:0}') +}) + +test('processes previous result', () => { + let result = new Processor([() => {}]).process('a::before{top:0}') + result = beforeFix.process(result) + is(result.css, 'a::before{content:"";top:0}') +}) + +test('takes maps from previous result', () => { + let one = new Processor([() => {}]).process('a{}', { + from: 'a.css', + map: { inline: false }, + to: 'b.css' + }) + let two = new Processor([() => {}]).process(one, { to: 'c.css' }) + equal(two.map.toJSON().sources, ['a.css']) +}) + +test('inlines maps from previous result', () => { + let one = new Processor([() => {}]).process('a{}', { + from: 'a.css', + map: { inline: false }, + to: 'b.css' + }) + let two = new Processor([() => {}]).process(one, { + map: { inline: true }, + to: 'c.css' + }) + type(two.map, 'undefined') +}) + +test('throws with file name', () => { + let error: CssSyntaxError | undefined + try { + new Processor([() => {}]).process('a {', { from: 'a.css' }).css + } catch (e) { + if (e instanceof CssSyntaxError) { + error = e + } else { + throw e + } + } + + is(error?.file, pathResolve('a.css')) + match(String(error?.message), /a.css:1:1: Unclosed block$/) +}) + +test('allows to replace Root', () => { + let processor = new Processor([ + (css, result) => { + result.root = new Root() + } + ]) + is(processor.process('a {}').css, '') +}) + +test('returns LazyResult object', () => { + let result = new Processor([() => {}]).process('a{}') + is(result instanceof LazyResult, true) + is(result.css, 'a{}') + is(result.toString(), 'a{}') +}) + +test('calls all plugins once', async () => { + let calls = '' + let a = (): void => { + calls += 'a' + } + let b = (): void => { + calls += 'b' + } + + let result = new Processor([a, b]).process('', { from: undefined }) + result.css + result.map + result.root + await result + is(calls, 'ab') +}) + +test('parses, converts and stringifies CSS', () => { + is( + typeof new Processor([ + (css: Root) => { + equal(css instanceof Root, true) + } + ]).process('a {}').css, + 'string' + ) +}) + +test('send result to plugins', () => { + let processor = new Processor([() => {}]) + processor + .use((css, result) => { + is(result instanceof Result, true) + equal(result.processor, processor) + equal(result.opts, { map: true }) + equal(result.root, css) + }) + .process('a {}', { from: undefined, map: true }) +}) + +test('accepts source map from PostCSS', () => { + let one = new Processor([() => {}]).process('a{}', { + from: 'a.css', + map: { inline: false }, + to: 'b.css' + }) + let two = new Processor([() => {}]).process(one.css, { + from: 'b.css', + map: { inline: false, prev: one.map }, + to: 'c.css' + }) + equal(two.map.toJSON().sources, ['a.css']) +}) + +test('supports async plugins', async () => { + let starts = 0 + let finish = 0 + let async1 = (css: Root): Promise => + new Promise(resolve => { + starts += 1 + setTimeout(() => { + equal(starts, 1) + + css.append('a {}') + finish += 1 + resolve() + }, 1) + }) + let async2 = (css: Root): Promise => + new Promise(resolve => { + equal(starts, 1) + equal(finish, 1) + + starts += 1 + setTimeout(() => { + css.append('b {}') + finish += 1 + resolve() + }, 1) + }) + let r = await new Processor([async1, async2]).process('', { from: 'a' }) + is(starts, 2) + is(finish, 2) + is(r.css, 'a {}b {}') +}) + +test('works async without plugins', async () => { + let r = await new Processor([() => {}]).process('a {}', { from: 'a' }) + is(r.css, 'a {}') +}) + +test('runs async plugin only once', async () => { + let calls = 0 + let async = (): Promise => { + return new Promise(resolve => { + setTimeout(() => { + calls += 1 + resolve() + }, 1) + }) + } + + let result = new Processor([async]).process('a {}', { from: undefined }) + result.then(() => {}) + await result + await result + is(calls, 1) +}) + +test('supports async errors', async () => { + let error = new Error('Async') + let async = (): Promise => { + return new Promise((resolve, reject) => { + reject(error) + }) + } + let result = new Processor([async]).process('', { from: undefined }) + let err1 = await catchError(async () => await result) + equal(err1, error) + + let err2: unknown + result.catch((catched: unknown) => { + err2 = catched + }) + await delay(10) + equal(err2, error) +}) + +test('supports sync errors in async mode', async () => { + let error = new Error('Async') + let async = (): void => { + throw error + } + let err = await catchError(() => + new Processor([async]).process('', { from: undefined }) + ) + equal(err, error) +}) + +test('throws parse error in async', async () => { + let err = await catchError(() => + new Processor([() => {}]).process('a{', { from: undefined }) + ) + is(err.message, ':1:1: Unclosed block') +}) + +test('throws error on sync method to async plugin', () => { + let async = (): Promise => { + return new Promise(resolve => { + resolve() + }) + } + throws(() => { + new Processor([async]).process('a{}').css + }, /async/) +}) + +test('throws a sync call in async running', () => { + let async = (): Promise => + new Promise(resolve => setTimeout(resolve, 1)) + + let processor = new Processor([async]).process('a{}', { from: 'a.css' }) + processor.async() + + throws(() => { + processor.sync() + }, /then/) +}) + +test('remembers errors', async () => { + let calls = 0 + let plugin: Plugin = { + Once() { + calls += 1 + throw new Error('test') + }, + postcssPlugin: 'plugin' + } + + let processing = postcss([plugin]).process('a{}', { from: undefined }) + + throws(() => { + processing.css + }, 'test') + throws(() => { + processing.css + }, 'test') + throws(() => { + processing.root + }, 'test') + + let asyncError: any + try { + await processing + } catch (e) { + asyncError = e + } + is(asyncError.message, 'test') + + is(calls, 1) +}) + +test('checks plugin compatibility', () => { + let error = spyOn(console, 'error', () => {}) + let warn = spyOn(console, 'warn', () => {}) + + let plugin = (postcss as any).plugin('test', () => { + return () => { + throw new Error('Er') + } + }) + let func = plugin() + equal(warn.callCount, 1) + func.postcssVersion = '2.1.5' + + function processBy(version: string): void { + let processor = new Processor([func]) + processor.version = version + processor.process('a{}').css + } + + throws(() => { + processBy('1.0.0') + }, 'Er') + equal(error.callCount, 1) + equal(error.calls, [ + [ + 'Unknown error from PostCSS plugin. ' + + 'Your current PostCSS version is 1.0.0, but test uses 2.1.5. ' + + 'Perhaps this is the source of the error below.' + ] + ]) + + throws(() => { + processBy('3.0.0') + }, 'Er') + equal(error.callCount, 2) + + throws(() => { + processBy('2.0.0') + }, 'Er') + equal(error.callCount, 3) + + throws(() => { + processBy('2.1.0') + }, 'Er') + equal(error.callCount, 3) +}) + +test('sets last plugin to result', async () => { + let plugin1 = (css: Root, result: Result): void => { + equal(result.lastPlugin, plugin1) + } + let plugin2 = (css: Root, result: Result): void => { + equal(result.lastPlugin, plugin2) + } + + let processor = new Processor([plugin1, plugin2]) + let result = await processor.process('a{}', { from: undefined }) + equal(result.lastPlugin, plugin2) +}) + +test('uses custom parsers', async () => { + let processor = new Processor([]) + let result = await processor.process('a{}', { from: undefined, parser: prs }) + is(result.css, 'ok') +}) + +test('uses custom parsers from object', async () => { + let processor = new Processor([]) + let syntax = { parse: prs, stringify: str } + let result = await processor.process('a{}', { from: 'a', parser: syntax }) + equal(result.css, 'ok') +}) + +test('uses custom stringifier', async () => { + let processor = new Processor([]) + let result = await processor.process('a{}', { from: 'a', stringifier: str }) + is(result.css, '!') +}) + +test('uses custom stringifier from object', async () => { + let processor = new Processor([]) + let syntax = { parse: prs, stringify: str } + let result = await processor.process('', { from: 'a', stringifier: syntax }) + is(result.css, '!') +}) + +test('uses custom stringifier with source maps', async () => { + let processor = new Processor([]) + let result = await processor.process('a{}', { + from: undefined, + map: true, + stringifier: str + }) + match(result.css, /!\n\/\*# sourceMap/) +}) + +test('uses custom syntax', async () => { + let processor = new Processor([() => {}]) + let result = await processor.process('a{}', { + from: undefined, + syntax: { parse: prs, stringify: str } + }) + is(result.css, 'ok!') +}) + +test('contains PostCSS version', () => { + match(new Processor().version, /\d+.\d+.\d+/) +}) + +test('throws on syntax as plugin', () => { + let processor = new Processor([() => {}]) + throws(() => { + processor.use({ + // @ts-expect-error Testing invalid API + parse() {} + }) + }, /syntax/) +}) + +test('warns about missed from', async () => { + let warn = spyOn(console, 'warn', () => {}) + let processor = new Processor([() => {}]) + + processor.process('a{}').css + equal(warn.calls, []) + + await processor.process('a{}') + equal(warn.calls, [ + [ + 'Without `from` option PostCSS could generate wrong source map ' + + 'and will not find Browserslist config. Set it to CSS file path ' + + 'or to `undefined` to prevent this warning.' + ] + ]) +}) + +test('returns NoWorkResult object', () => { + let result = new Processor().process('a{}') + instance(result, NoWorkResult) +}) + +test('without plugins parses CSS only on root access', async () => { + let noWorkResult = new Processor().process('a{}') + let result = await noWorkResult + // @ts-expect-error Testing private API + type(noWorkResult._root, 'undefined') + is(result.root.nodes.length, 1) + // @ts-expect-error Testing private API + not.type(noWorkResult._root, 'undefined') + is(noWorkResult.root.nodes.length, 1) +}) + +test('catches error with empty processor', async () => { + let noWorkResult = new Processor().process('a {') + + try { + noWorkResult.root + } catch {} + + let err = await catchError(async () => await noWorkResult) + + noWorkResult.catch((e: unknown) => { + instance(e, CssSyntaxError) + }) + + instance(err, CssSyntaxError) +}) + +test('throws an error on root access on no plugins mode', () => { + throws(() => { + postcss().process('// invalid', { from: 'a' }).root + }, 'Unknown word') +}) + +test('supports plugins returning processors', () => { + let warn = spyOn(console, 'warn', () => {}) + let a = (): void => {} + let processor = new Processor() + let other: any = (postcss as any).plugin('test', () => { + return new Processor([a]) + }) + processor.use(other) + equal(processor.plugins, [a]) + equal(warn.callCount, 1) +}) + +test('supports plugin creators returning processors', () => { + let a = (): void => {} + let processor = new Processor() + let other = (() => { + return new Processor([a]) + }) as PluginCreator + other.postcss = true + processor.use(other) + equal(processor.plugins, [a]) +}) + +test('uses custom syntax for document', async () => { + let customParser: Parser = () => { + return new Document({ + nodes: [ + new Root({ + nodes: [new Rule({ selector: 'a' })], + raws: { + after: '\n\n\n', + codeBefore: '\n\n\n', + codeBefore: '\n\n\n' + ) +}) + +test.run() diff --git a/test/result.js b/test/result.js deleted file mode 100644 index 436ea1112..000000000 --- a/test/result.js +++ /dev/null @@ -1,56 +0,0 @@ -import Warning from '../lib/warning'; -import postcss from '../lib/postcss'; -import Result from '../lib/result'; - -import test from 'ava'; - -test('stringifies', t => { - let result = new Result(); - result.css = 'a{}'; - t.deepEqual('' + result, result.css); -}); - -test('adds warning', t => { - let warning; - let plugin = postcss.plugin('test-plugin', () => { - return (css, res) => { - warning = res.warn('test', { node: css.first }); - }; - }); - let result = postcss([plugin]).process('a{}').sync(); - - t.deepEqual(warning, new Warning('test', { - plugin: 'test-plugin', - node: result.root.first - })); - - t.deepEqual(result.messages, [warning]); -}); - -test('allows to override plugin', t => { - let plugin = postcss.plugin('test-plugin', () => { - return (css, res) => { - res.warn('test', { plugin: 'test-plugin#one' }); - }; - }); - let result = postcss([plugin]).process('a{}').sync(); - - t.deepEqual(result.messages[0].plugin, 'test-plugin#one'); -}); - -test('allows Root', t => { - let result = new Result(); - let root = postcss.parse('a{}'); - result.warn('TT', { node: root }); - - t.deepEqual(result.messages[0].toString(), ':1:1: TT'); -}); - -test('returns only warnings', t => { - let result = new Result(); - result.messages = [{ type: 'warning', text: 'a' }, - { type: 'custom' }, - { type: 'warning', text: 'b' }]; - t.deepEqual(result.warnings(), [{ type: 'warning', text: 'a' }, - { type: 'warning', text: 'b' }]); -}); diff --git a/test/result.test.ts b/test/result.test.ts new file mode 100755 index 000000000..e163e1787 --- /dev/null +++ b/test/result.test.ts @@ -0,0 +1,70 @@ +import { test } from 'uvu' +import { equal, is } from 'uvu/assert' + +import postcss, { Plugin, Result, Root, Warning } from '../lib/postcss.js' +import Processor from '../lib/processor.js' + +let processor = new Processor() +let root = new Root() + +test('stringifies', () => { + let result = new Result(processor, root, {}) + result.css = 'a{}' + is(`${result}`, result.css) +}) + +test('adds warning', () => { + let warning + let plugin: Plugin = { + Once(css, { result }) { + warning = result.warn('test', { node: css.first }) + }, + postcssPlugin: 'test-plugin' + } + let result = postcss([plugin]).process('a{}').sync() + + equal( + warning, + new Warning('test', { + node: result.root.first, + plugin: 'test-plugin' + }) + ) + + equal(result.messages, [warning]) +}) + +test('allows to override plugin', () => { + let plugin: Plugin = { + Once(css, { result }) { + result.warn('test', { plugin: 'test-plugin#one' }) + }, + postcssPlugin: 'test-plugin' + } + let result = postcss([plugin]).process('a{}').sync() + + is(result.messages[0].plugin, 'test-plugin#one') +}) + +test('allows Root', () => { + let css = postcss.parse('a{}') + let result = new Result(processor, css, {}) + result.warn('TT', { node: css.first }) + + is(result.messages[0].toString(), ':1:1: TT') +}) + +test('returns only warnings', () => { + let result = new Result(processor, root, {}) + result.messages = [ + { text: 'a', type: 'warning' }, + { type: 'custom' }, + { text: 'b', type: 'warning' } + ] + equal(result.warnings(), [ + { text: 'a', type: 'warning' }, + { text: 'b', type: 'warning' } + ]) +}) + +test.run() diff --git a/test/root.js b/test/root.js deleted file mode 100644 index 1ae52c9b4..000000000 --- a/test/root.js +++ /dev/null @@ -1,71 +0,0 @@ -import Result from '../lib/result'; -import parse from '../lib/parse'; - -import test from 'ava'; - -test('prepend() fixes spaces on insert before first', t => { - let css = parse('a {} b {}'); - css.prepend({ selector: 'em' }); - t.deepEqual(css.toString(), 'em {} a {} b {}'); -}); - -test('prepend() fixes spaces on multiple inserts before first', t => { - let css = parse('a {} b {}'); - css.prepend({ selector: 'em' }, { selector: 'strong' }); - t.deepEqual(css.toString(), 'em {} strong {} a {} b {}'); -}); - -test('prepend() uses default spaces on only first', t => { - let css = parse('a {}'); - css.prepend({ selector: 'em' }); - t.deepEqual(css.toString(), 'em {}\na {}'); -}); - -test('append() sets new line between rules in multiline files', t => { - let a = parse('a {}\n\na {}\n'); - let b = parse('b {}\n'); - t.deepEqual(a.append(b).toString(), 'a {}\n\na {}\n\nb {}\n'); -}); - -test('append() sets new line between rules on last newline', t => { - let a = parse('a {}\n'); - let b = parse('b {}\n'); - t.deepEqual(a.append(b).toString(), 'a {}\nb {}\n'); -}); - -test('append() saves compressed style', t => { - let a = parse('a{}a{}'); - let b = parse('b {\n}\n'); - t.deepEqual(a.append(b).toString(), 'a{}a{}b{}'); -}); - -test('append() saves compressed style with multiple nodes', t => { - let a = parse('a{}a{}'); - let b = parse('b {\n}\n'); - let c = parse('c {\n}\n'); - t.deepEqual(a.append(b, c).toString(), 'a{}a{}b{}c{}'); -}); - -test('insertAfter() does not use before of first rule', t => { - let css = parse('a{} b{}'); - css.insertAfter(0, { selector: '.a' }); - css.insertAfter(2, { selector: '.b' }); - - t.deepEqual(typeof css.nodes[1].raws.before, 'undefined'); - t.deepEqual(css.nodes[3].raws.before, ' '); - t.deepEqual(css.toString(), 'a{} .a{} b{} .b{}'); -}); - -test('fixes spaces on removing first rule', t => { - let css = parse('a{}\nb{}\n'); - css.first.remove(); - t.deepEqual(css.toString(), 'b{}\n'); -}); - -test('generates result with map', t => { - let root = parse('a {}'); - let result = root.toResult({ map: true }); - - t.truthy(result instanceof Result); - t.regex(result.css, /a \{\}\n\/\*# sourceMappingURL=/); -}); diff --git a/test/root.test.ts b/test/root.test.ts new file mode 100755 index 000000000..16eabcb7a --- /dev/null +++ b/test/root.test.ts @@ -0,0 +1,84 @@ +import { test } from 'uvu' +import { is, match, type } from 'uvu/assert' + +import postcss, { parse, Result } from '../lib/postcss.js' + +test('prepend() fixes spaces on insert before first', () => { + let css = parse('a {} b {}') + css.prepend({ selector: 'em' }) + is(css.toString(), 'em {} a {} b {}') +}) + +test('prepend() fixes spaces on multiple inserts before first', () => { + let css = parse('a {} b {}') + css.prepend({ selector: 'em' }, { selector: 'strong' }) + is(css.toString(), 'em {} strong {} a {} b {}') +}) + +test('prepend() uses default spaces on only first', () => { + let css = parse('a {}') + css.prepend({ selector: 'em' }) + is(css.toString(), 'em {}\na {}') +}) + +test('append() sets new line between rules in multiline files', () => { + let a = parse('a {}\n\na {}\n') + let b = parse('b {}\n') + is(a.append(b).toString(), 'a {}\n\na {}\n\nb {}\n') +}) + +test('insertAfter() does not use before of first rule', () => { + let css = parse('a{} b{}') + css.insertAfter(0, { selector: '.a' }) + css.insertAfter(2, { selector: '.b' }) + + type(css.nodes[1].raws.before, 'undefined') + is(css.nodes[3].raws.before, ' ') + is(css.toString(), 'a{} .a{} b{} .b{}') +}) + +test('fixes spaces on removing first rule', () => { + let css = parse('a{}\nb{}\n') + if (!css.first) throw new Error('No nodes were parsed') + css.first.remove() + is(css.toString(), 'b{}\n') +}) + +test('keeps explicitly set raws.before on inserted node', () => { + let css = parse('/*a*/\n\n/*b*/') + let node = postcss.comment({ raws: { before: '' }, text: 'new' }) + if (!css.nodes[1]) throw new Error('No nodes were parsed') + css.nodes[1].before(node) + is(node.raws.before, '') + is(css.toString(), '/*a*//*new*/\n\n/*b*/') +}) + +test('updates raws.before on node moved from another root', () => { + let css1 = parse('a{}\nb{}') + let css2 = parse('em{}\n\n\nstrong{}') + if (!css1.nodes[1] || !css2.nodes[1]) throw new Error('No nodes were parsed') + css2.nodes[1].before(css1.nodes[1]) + is(css2.toString(), 'em{}\n\n\nb{}\n\n\nstrong{}') +}) + +test('keeps spaces on moving root', () => { + let css1 = parse('a{}\nb{}\n') + + let css2 = parse('') + css2.append(css1) + is(css2.toString(), 'a{}\nb{}') + + let css3 = parse('\n') + css3.append(css2.nodes) + is(css3.toString(), 'a{}\nb{}\n') +}) + +test('generates result with map', () => { + let root = parse('a {}') + let result = root.toResult({ map: true }) + + is(result instanceof Result, true) + match(result.css, /a {}\n\/\*# sourceMappingURL=/) +}) + +test.run() diff --git a/test/rule.js b/test/rule.js deleted file mode 100644 index a11581a8b..000000000 --- a/test/rule.js +++ /dev/null @@ -1,82 +0,0 @@ -import parse from '../lib/parse'; -import Rule from '../lib/rule'; - -import test from 'ava'; - -test('initializes with properties', t => { - let rule = new Rule({ selector: 'a' }); - t.deepEqual(rule.selector, 'a'); -}); - -test('returns array in selectors', t => { - let rule = new Rule({ selector: 'a,b' }); - t.deepEqual(rule.selectors, ['a', 'b']); -}); - -test('trims selectors', t => { - let rule = new Rule({ selector: '.a\n, .b , .c' }); - t.deepEqual(rule.selectors, ['.a', '.b', '.c']); -}); - -test('is smart about selectors commas', t => { - let rule = new Rule({ - selector: '[foo=\'a, b\'], a:-moz-any(:focus, [href*=\',\'])' - }); - t.deepEqual(rule.selectors, [ - '[foo=\'a, b\']', - 'a:-moz-any(:focus, [href*=\',\'])' - ]); -}); - -test('receive array in selectors', t => { - let rule = new Rule({ selector: 'i, b' }); - rule.selectors = ['em', 'strong']; - t.deepEqual(rule.selector, 'em, strong'); -}); - -test('saves separator in selectors', t => { - let rule = new Rule({ selector: 'i,\nb' }); - rule.selectors = ['em', 'strong']; - t.deepEqual(rule.selector, 'em,\nstrong'); -}); - -test('uses between to detect separator in selectors', t => { - let rule = new Rule({ selector: 'b', raws: { between: '' } }); - rule.selectors = ['b', 'strong']; - t.deepEqual(rule.selector, 'b,strong'); -}); - -test('uses space in separator be default in selectors', t => { - let rule = new Rule({ selector: 'b' }); - rule.selectors = ['b', 'strong']; - t.deepEqual(rule.selector, 'b, strong'); -}); - -test('selectors works in constructor', t => { - let rule = new Rule({ selectors: ['a', 'b'] }); - t.deepEqual(rule.selector, 'a, b'); -}); - -test('inserts default spaces', t => { - let rule = new Rule({ selector: 'a' }); - t.deepEqual(rule.toString(), 'a {}'); - rule.append({ prop: 'color', value: 'black' }); - t.deepEqual(rule.toString(), 'a {\n color: black\n}'); -}); - -test('clones spaces from another rule', t => { - let root = parse('b{\n }'); - let rule = new Rule({ selector: 'em' }); - root.append(rule); - t.deepEqual(root.toString(), 'b{\n }\nem{\n }'); -}); - -test('uses different spaces for empty rules', t => { - let root = parse('a{}\nb{\n a:1\n}'); - let rule = new Rule({ selector: 'em' }); - root.append(rule); - t.deepEqual(root.toString(), 'a{}\nb{\n a:1\n}\nem{}'); - - rule.append({ prop: 'top', value: '0' }); - t.deepEqual(root.toString(), 'a{}\nb{\n a:1\n}\nem{\n top:0\n}'); -}); diff --git a/test/rule.test.ts b/test/rule.test.ts new file mode 100755 index 000000000..795255d46 --- /dev/null +++ b/test/rule.test.ts @@ -0,0 +1,96 @@ +import { test } from 'uvu' +import { equal, is } from 'uvu/assert' + +import { parse, Rule } from '../lib/postcss.js' + +test('initializes with properties', () => { + let rule = new Rule({ selector: 'a' }) + is(rule.selector, 'a') +}) + +test('returns array in selectors', () => { + let rule = new Rule({ selector: 'a,b' }) + equal(rule.selectors, ['a', 'b']) +}) + +test('returns empty selector in selectors', () => { + let rule = new Rule({ selector: '' }) + equal(rule.selectors, ['']) +}) + +test('keeps empty selector between other selectors', () => { + let rule = new Rule({ selector: 'a,,b' }) + equal(rule.selectors, ['a', '', 'b']) +}) + +test('keeps empty selector before other selectors', () => { + let rule = new Rule({ selector: ',b' }) + equal(rule.selectors, ['', 'b']) +}) + +test('trims selectors', () => { + let rule = new Rule({ selector: '.a\n, .b , .c' }) + equal(rule.selectors, ['.a', '.b', '.c']) +}) + +test('is smart about selectors commas', () => { + let rule = new Rule({ + selector: "[foo='a, b'], a:-moz-any(:focus, [href*=','])" + }) + equal(rule.selectors, ["[foo='a, b']", "a:-moz-any(:focus, [href*=','])"]) +}) + +test('receive array in selectors', () => { + let rule = new Rule({ selector: 'i, b' }) + rule.selectors = ['em', 'strong'] + is(rule.selector, 'em, strong') +}) + +test('saves separator in selectors', () => { + let rule = new Rule({ selector: 'i,\nb' }) + rule.selectors = ['em', 'strong'] + is(rule.selector, 'em,\nstrong') +}) + +test('uses between to detect separator in selectors', () => { + let rule = new Rule({ raws: { between: '' }, selector: 'b' }) + rule.selectors = ['b', 'strong'] + is(rule.selector, 'b,strong') +}) + +test('uses space in separator be default in selectors', () => { + let rule = new Rule({ selector: 'b' }) + rule.selectors = ['b', 'strong'] + is(rule.selector, 'b, strong') +}) + +test('selectors works in constructor', () => { + let rule = new Rule({ selectors: ['a', 'b'] }) + is(rule.selector, 'a, b') +}) + +test('inserts default spaces', () => { + let rule = new Rule({ selector: 'a' }) + is(rule.toString(), 'a {}') + rule.append({ prop: 'color', value: 'black' }) + is(rule.toString(), 'a {\n color: black\n}') +}) + +test('clones spaces from another rule', () => { + let root = parse('b{\n }') + let rule = new Rule({ selector: 'em' }) + root.append(rule) + is(root.toString(), 'b{\n }\nem{\n }') +}) + +test('uses different spaces for empty rules', () => { + let root = parse('a{}\nb{\n a:1\n}') + let rule = new Rule({ selector: 'em' }) + root.append(rule) + is(root.toString(), 'a{}\nb{\n a:1\n}\nem{}') + + rule.append({ prop: 'top', value: '0' }) + is(root.toString(), 'a{}\nb{\n a:1\n}\nem{\n top:0\n}') +}) + +test.run() diff --git a/test/stringifier.js b/test/stringifier.js deleted file mode 100644 index 50a494d42..000000000 --- a/test/stringifier.js +++ /dev/null @@ -1,167 +0,0 @@ -import Stringifier from '../lib/stringifier'; -import Declaration from '../lib/declaration'; -import AtRule from '../lib/at-rule'; -import parse from '../lib/parse'; -import Node from '../lib/node'; -import Root from '../lib/root'; -import Rule from '../lib/rule'; - -import test from 'ava'; - -let str; -test.before( () => { - str = new Stringifier(); -}); - -test('creates trimmed/raw property', t => { - let b = new Node({ one: 'trim' }); - b.raws.one = { value: 'trim', raw: 'raw' }; - t.deepEqual(str.rawValue(b, 'one'), 'raw'); - - b.one = 'trim1'; - t.deepEqual(str.rawValue(b, 'one'), 'trim1'); -}); - -test('works without rawValue magic', t => { - let b = new Node(); - b.one = '1'; - t.deepEqual(b.one, '1'); - t.deepEqual(str.rawValue(b, 'one'), '1'); -}); - -test('uses node raw', t => { - let rule = new Rule({ selector: 'a', raws: { between: '\n' } }); - t.deepEqual(str.raw(rule, 'between', 'beforeOpen'), '\n'); -}); - -test('hacks before for nodes without parent', t => { - let rule = new Rule({ selector: 'a' }); - t.deepEqual(str.raw(rule, 'before'), ''); -}); - -test('hacks before for first node', t => { - let root = new Root(); - root.append(new Rule({ selector: 'a' })); - t.deepEqual(str.raw(root.first, 'before'), ''); -}); - -test('hacks before for first decl', t => { - let decl = new Declaration({ prop: 'color', value: 'black' }); - t.deepEqual(str.raw(decl, 'before'), ''); - - let rule = new Rule({ selector: 'a' }); - rule.append(decl); - t.deepEqual(str.raw(decl, 'before'), '\n '); -}); - -test('detects after raw', t => { - let root = new Root(); - root.append({ selector: 'a', raws: { after: ' ' } }); - root.first.append({ prop: 'color', value: 'black' }); - root.append({ selector: 'a' }); - t.deepEqual(str.raw(root.last, 'after'), ' '); -}); - -test('uses defaults without parent', t => { - let rule = new Rule({ selector: 'a' }); - t.deepEqual(str.raw(rule, 'between', 'beforeOpen'), ' '); -}); - -test('uses defaults for unique node', t => { - let root = new Root(); - root.append(new Rule({ selector: 'a' })); - t.deepEqual(str.raw(root.first, 'between', 'beforeOpen'), ' '); -}); - -test('clones raw from first node', t => { - let root = new Root(); - root.append( new Rule({ selector: 'a', raws: { between: '' } }) ); - root.append( new Rule({ selector: 'b' }) ); - - t.deepEqual(str.raw(root.last, 'between', 'beforeOpen'), ''); -}); - -test('indents by default', t => { - let root = new Root(); - root.append( new AtRule({ name: 'page' }) ); - root.first.append( new Rule({ selector: 'a' }) ); - root.first.first.append({ prop: 'color', value: 'black' }); - - t.deepEqual(root.toString(), '@page {\n' + - ' a {\n' + - ' color: black\n' + - ' }\n' + - '}'); -}); - -test('clones style', t => { - let compress = parse('@page{ a{ } }'); - let spaces = parse('@page {\n a {\n }\n}'); - - compress.first.first.append({ prop: 'color', value: 'black' }); - t.deepEqual(compress.toString(), '@page{ a{ color: black } }'); - - spaces.first.first.append({ prop: 'color', value: 'black' }); - t.deepEqual(spaces.toString(), '@page {\n a {\n color: black\n }\n}'); -}); - -test('clones indent', t => { - let root = parse('a{\n}'); - root.first.append({ text: 'a' }); - root.first.append({ text: 'b', raws: { before: '\n\n ' } }); - t.deepEqual(root.toString(), 'a{\n\n /* a */\n\n /* b */\n}'); -}); - -test('clones declaration before for comment', t => { - let root = parse('a{\n}'); - root.first.append({ text: 'a' }); - root.first.append({ - prop: 'a', - value: '1', - raws: { before: '\n\n ' } - }); - t.deepEqual(root.toString(), 'a{\n\n /* a */\n\n a: 1\n}'); -}); - -test('clones indent by types', t => { - let css = parse('a {\n color: black\n}\n\nb {\n}'); - css.append(new Rule({ selector: 'em' })); - css.last.append({ prop: 'z-index', value: '1' }); - - t.deepEqual(css.last.raw('before'), '\n\n'); - t.deepEqual(css.last.first.raw('before'), '\n '); -}); - -test('clones indent by before and after', t => { - let css = parse('@page{\n\n a{\n color: black}}'); - css.first.append(new Rule({ selector: 'b' })); - css.first.last.append({ prop: 'z-index', value: '1' }); - - t.deepEqual(css.first.last.raw('before'), '\n\n '); - t.deepEqual(css.first.last.raw('after'), ''); -}); - -test('clones semicolon only from rules with children', t => { - let css = parse('a{}b{one:1;}'); - t.truthy(str.raw(css.first, 'semicolon')); -}); - -test('clones only spaces in before', t => { - let css = parse('a{*one:1}'); - css.first.append({ prop: 'two', value: '2' }); - css.append({ name: 'keyframes', params: 'a' }); - css.last.append({ selector: 'from' }); - t.deepEqual(css.toString(), 'a{*one:1;two:2}\n@keyframes a{\nfrom{}}'); -}); - -test('clones only spaces in between', t => { - let css = parse('a{one/**/:1}'); - css.first.append({ prop: 'two', value: '2' }); - t.deepEqual(css.toString(), 'a{one/**/:1;two:2}'); -}); - -test('uses optional raws.indent', t => { - let rule = new Rule({ selector: 'a', raws: { indent: ' ' } }); - rule.append({ prop: 'color', value: 'black' }); - t.deepEqual(rule.toString(), 'a {\n color: black\n}'); -}); diff --git a/test/stringifier.test.js b/test/stringifier.test.js new file mode 100755 index 000000000..f845650e2 --- /dev/null +++ b/test/stringifier.test.js @@ -0,0 +1,480 @@ +let { test } = require('uvu') +let { is } = require('uvu/assert') + +let { + AtRule, + Comment, + Declaration, + Document, + Node, + parse, + Root, + Rule +} = require('../lib/postcss') +let Stringifier = require('../lib/stringifier') + +let str + +test.before.each(() => { + str = new Stringifier() +}) + +test('creates trimmed/raw property', () => { + let b = new Node({ one: 'trim' }) + b.raws.one = { raw: 'raw', value: 'trim' } + is(str.rawValue(b, 'one'), 'raw') + + b.one = 'trim1' + is(str.rawValue(b, 'one'), 'trim1') +}) + +test('works without rawValue magic', () => { + let b = new Node() + b.one = '1' + is(b.one, '1') + is(str.rawValue(b, 'one'), '1') +}) + +test('uses node raw', () => { + let rule = new Rule({ raws: { between: '\n' }, selector: 'a' }) + is(str.raw(rule, 'between', 'beforeOpen'), '\n') +}) + +test('hacks before for nodes without parent', () => { + let rule = new Rule({ selector: 'a' }) + is(str.raw(rule, 'before'), '') +}) + +test('hacks before for first node', () => { + let root = new Root() + root.append(new Rule({ selector: 'a' })) + is(str.raw(root.first, 'before'), '') +}) + +test('hacks before for first decl', () => { + let decl = new Declaration({ prop: 'color', value: 'black' }) + is(str.raw(decl, 'before'), '') + + let rule = new Rule({ selector: 'a' }) + rule.append(decl) + is(str.raw(decl, 'before'), '\n ') +}) + +test('detects after raw', () => { + let root = new Root() + root.append({ raws: { after: ' ' }, selector: 'a' }) + root.first.append({ prop: 'color', value: 'black' }) + root.append({ selector: 'a' }) + is(str.raw(root.last, 'after'), ' ') +}) + +test('uses defaults without parent', () => { + let rule = new Rule({ selector: 'a' }) + is(str.raw(rule, 'between', 'beforeOpen'), ' ') +}) + +test('uses defaults for unique node', () => { + let root = new Root() + root.append(new Rule({ selector: 'a' })) + is(str.raw(root.first, 'between', 'beforeOpen'), ' ') +}) + +test('clones raw from first node', () => { + let root = new Root() + root.append(new Rule({ raws: { between: '' }, selector: 'a' })) + root.append(new Rule({ selector: 'b' })) + + is(str.raw(root.last, 'between', 'beforeOpen'), '') +}) + +test('indents by default', () => { + let root = new Root() + root.append(new AtRule({ name: 'page' })) + root.first.append(new Rule({ selector: 'a' })) + root.first.first.append({ prop: 'color', value: 'black' }) + + is( + root.toString(), + '@page {\n' + ' a {\n' + ' color: black\n' + ' }\n' + '}' + ) +}) + +test('clones style', () => { + let compress = parse('@page{ a{ } }') + let spaces = parse('@page {\n a {\n }\n}') + + compress.first.first.append({ prop: 'color', value: 'black' }) + is(compress.toString(), '@page{ a{ color: black } }') + + spaces.first.first.append({ prop: 'color', value: 'black' }) + is(spaces.toString(), '@page {\n a {\n color: black\n }\n}') +}) + +test('clones indent', () => { + let root = parse('a{\n}') + root.first.append({ text: 'a' }) + root.first.append({ raws: { before: '\n\n ' }, text: 'b' }) + is(root.toString(), 'a{\n\n /* a */\n\n /* b */\n}') +}) + +test('clones declaration before for comment', () => { + let root = parse('a{\n}') + root.first.append({ text: 'a' }) + root.first.append({ + prop: 'a', + raws: { before: '\n\n ' }, + value: '1' + }) + is(root.toString(), 'a{\n\n /* a */\n\n a: 1\n}') +}) + +test('clones indent by types', () => { + let css = parse('a {\n *color: black\n}\n\nb {\n}') + css.append(new Rule({ selector: 'em' })) + css.last.append({ prop: 'z-index', value: '1' }) + is(css.last.first.raw('before'), '\n ') +}) + +test('ignores non-space symbols in indent cloning', () => { + let css = parse('a {\n color: black\n}\n\nb {\n}') + css.append(new Rule({ selector: 'em' })) + css.last.append({ prop: 'z-index', value: '1' }) + + is(css.last.raw('before'), '\n\n') + is(css.last.first.raw('before'), '\n ') +}) + +test('clones indent by before and after', () => { + let css = parse('@page{\n\n a{\n color: black}}') + css.first.append(new Rule({ selector: 'b' })) + css.first.last.append({ prop: 'z-index', value: '1' }) + + is(css.first.last.raw('before'), '\n\n ') + is(css.first.last.raw('after'), '') +}) + +test('clones semicolon only from rules with children', () => { + let css = parse('a{}b{one:1;}') + is(str.raw(css.first, 'semicolon'), true) +}) + +test('terminates childless at-rule followed by a comment', () => { + let css = parse('a {}\n/* comment */') + css.insertBefore(css.last, new AtRule({ name: 'import', params: '"x.css"' })) + + is(css.toString(), 'a {}\n@import "x.css";\n/* comment */') + is( + parse(css.toString()) + .nodes.map(i => i.type) + .join(','), + 'rule,atrule,comment' + ) +}) + +test('terminates nested childless at-rule followed by a comment', () => { + let css = parse('@media screen {\n a {}\n}') + css.first.append(new AtRule({ name: 'import', params: '"y.css"' })) + css.first.append(new Comment({ text: 'note' })) + + is( + css.toString(), + '@media screen {\n a {}\n @import "y.css";\n /* note */\n}' + ) + is( + parse(css.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'rule,atrule,comment' + ) +}) + +test('terminates custom property followed by a comment', () => { + let css = parse('a{--x:red}') + css.first.append(new Comment({ text: 'note' })) + + is(css.toString(), 'a{--x:red;/* note */}') + is( + parse(css.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'decl,comment' + ) +}) + +test('terminates custom property with !important before a comment', () => { + let css = parse('a{--x:red !important}') + css.first.first.after(new Comment({ text: 'note' })) + + is(css.toString(), 'a{--x:red !important;/* note */}') + is( + parse(css.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'decl,comment' + ) +}) + +test('keeps hack-prefixed property before a comment unchanged', () => { + for (let css of ['a{*--x:red/*c*/}', 'a{_--x:red/*c*/}']) { + let root = parse(css) + is(root.toString(), css) + is( + parse(root.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'decl,comment' + ) + } +}) + +test('terminates indented custom property followed by a comment', () => { + let css = parse('a{ --x:red}') + css.first.first.after(new Comment({ text: 'note' })) + + is(css.toString(), 'a{ --x:red; /* note */}') + is( + parse(css.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'decl,comment' + ) +}) + +test('clones only spaces in before', () => { + let css = parse('a{*one:1}') + css.first.append({ prop: 'two', value: '2' }) + css.append({ name: 'keyframes', params: 'a' }) + css.last.append({ selector: 'from' }) + is(css.toString(), 'a{*one:1;two:2}\n@keyframes a{\nfrom{}}') +}) + +test('clones only spaces in between', () => { + let css = parse('a{one/**/:1}') + css.first.append({ prop: 'two', value: '2' }) + is(css.toString(), 'a{one/**/:1;two:2}') +}) + +test('uses optional raws.indent', () => { + let rule = new Rule({ raws: { indent: ' ' }, selector: 'a' }) + rule.append({ prop: 'color', value: 'black' }) + is(rule.toString(), 'a {\n color: black\n}') +}) + +test('handles nested roots', () => { + let root = new Root() + let subRoot = new Root() + subRoot.append(new AtRule({ name: 'foo' })) + root.append(subRoot) + + is(root.toString(), '@foo') +}) + +test('handles root', () => { + let root = new Root() + root.append(new AtRule({ name: 'foo' })) + + let s = root.toString() + + is(s, '@foo') +}) + +test('handles root with after', () => { + let root = new Root({ raws: { after: ' ' } }) + root.append(new AtRule({ name: 'foo' })) + + let s = root.toString() + + is(s, '@foo ') +}) + +test('pass nodes to document', () => { + let root = new Root() + let document = new Document({ nodes: [root] }) + + is(document.toString(), '') +}) + +test('handles document with one root', () => { + let root = new Root() + root.append(new AtRule({ name: 'foo' })) + + let document = new Document() + document.append(root) + + let s = document.toString() + + is(s, '@foo') +}) + +test('handles document with one root and after raw', () => { + let document = new Document() + let root = new Root({ raws: { after: ' ' } }) + root.append(new AtRule({ name: 'foo' })) + document.append(root) + + let s = document.toString() + + is(s, '@foo ') +}) + +test('handles document with one root and before and after', () => { + let document = new Document() + let root = new Root({ raws: { after: 'AFTER' } }) + root.append(new AtRule({ name: 'foo' })) + document.append(root) + + let s = document.toString() + + is(s, '@fooAFTER') +}) + +test('handles document with three roots without raws', () => { + let root1 = new Root() + root1.append(new AtRule({ name: 'foo' })) + + let root2 = new Root() + root2.append(new Rule({ selector: 'a' })) + + let root3 = new Root() + root3.append(new Declaration({ prop: 'color', value: 'black' })) + + let document = new Document() + document.append(root1) + document.append(root2) + document.append(root3) + + let s = document.toString() + + is(s, '@fooa {}color: black') +}) + +test('handles document with three roots, with before and after raws', () => { + let root1 = new Root({ raws: { after: 'AFTER_ONE' } }) + root1.append(new Rule({ selector: 'a.one' })) + + let root2 = new Root({ raws: { after: 'AFTER_TWO' } }) + root2.append(new Rule({ selector: 'a.two' })) + + let root3 = new Root({ raws: { after: 'AFTER_THREE' } }) + root3.append(new Rule({ selector: 'a.three' })) + + let document = new Document() + document.append(root1) + document.append(root2) + document.append(root3) + + let s = document.toString() + + is(s, 'a.one {}AFTER_ONEa.two {}AFTER_TWOa.three {}AFTER_THREE') +}) + +test('escapes { + let root = new Root() + root.append(new Rule({ selector: '' })) + root.append(new AtRule({ name: 'media', params: '