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/.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 7b07a1915..3030054ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,6 @@ -.DS_Store -*~ - node_modules/ -npm-debug.log -yarn-error.log -build/ -lib/*.js +coverage/ -api/ +docs/api/index.html +docs/api/assets/ diff --git a/.jsdocrc b/.jsdocrc deleted file mode 100644 index f06a846f7..000000000 --- a/.jsdocrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "source": { - "includePattern": ".+\\.es6$" - }, - "opts": { - "template": "node_modules/docdash", - "destination": "./api/" - }, - "plugins": ["plugins/markdown"], - "templates": { - "default": { - "layoutFile": "node_modules/docdash/tmpl/layout.tmpl", - "includeDate": false - } - } -} diff --git a/.npmignore b/.npmignore index 4a55ebeac..9439f8b9d 100644 --- a/.npmignore +++ b/.npmignore @@ -1,16 +1,7 @@ -.gitignore - -node_modules/ -npm-debug.log -yarn-error.log -yarn.lock - -build/ - +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 1b3aa1e5b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: node_js -cache: yarn -node_js: - - node - - "8" - - "6" - - "4" -install: - - YARN_IGNORE_ENGINES=true yarn diff --git a/.yaspellerrc b/.yaspellerrc deleted file mode 100644 index 13c720c6f..000000000 --- a/.yaspellerrc +++ /dev/null @@ -1,213 +0,0 @@ -{ - "lang": "en", - "format": "markdown", - "fileExtensions": [".md"], - "excludeFiles": [ - ".git", "node_modules", "build", "api/*.es6.html", "README-cn.md" - ], - "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", - "ES5", - "ES6", - "ENB", - "Vapula", - "Zagan", - "SemVer", - "Sass", - "Sass’s", - "SCSS", - "Weibo", - "PreCSS", - "libsass", - "CSSOM", - "Popp", - "Jeet", - "Modernizr", - "npm", - "webpack", - "Amdusias", - "Anton", - "Alexey", - "Andres", - "Andromalius", - "Andrealphus", - "Andalusian", - "Andras", - "asan", - "Lind", - "Zuo", - "Jed", - "Neal", - "Valac", - "Vue", - "Briggs", - "Bogdan", - "Chadkin", - "ClojureWerkz’s", - "Dantalion", - "Montoro", - "Decarabia", - "Dvornov", - "Efremov", - "Flauros", - "Josiah", - "Lydell", - "Locati", - "Lolspeak", - "Matija", - "Marohnić", - "Maxime", - "Michele", - "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", - "CamelCase", - "rebases", - "resolver", - "minifier", - "isolatable", - "minifiers", - "mixins", - "mixin", - "multitool", - "io", - "partials", - "inlined", - "inlines", - "polyfill", - "stylesheet", - "stylesheets", - "stringifier", - "keyframes", - "BEM", - "CLI", - "CSS3", - "CSS4", - "SugarSS", - "SVG", - "SVGO", - "Markdown", - "JS", - "js", - "vs", - "pantone", - "YIQ", - "gitter", - "evilmartians", - "Less’s", - "visualizer", - "Rollup", - "AtRule", - "CssSyntaxError", - "LazyResult", - "RegExp", - "atRule", - "childIterator", - "cloneAfter", - "cloneBefore", - "docdash", - "withContent", - "walkRules", - "walkDecls", - "walkComments", - "unprefixed", - "toResult", - "showSourceCode", - "replaceWith", - "replaceValues", - "removeChild", - "removeAll", - "prepend", - "params", - "insertBefore", - "insertAfter", - "decl", - "es6", - "stringify", - "defaultType", - "newParent", - "otherNode", - "SourceMapGenerator", - "onFulfilled", - "onRejected", - "base64", - "stringifing", - "Browserify", - "CodePen", - "LTS", - "Garet", - "McKinley", - "Semigradsky", - "janschoenherr", - "bumbleblym", - "Packt", - "Libby", - "LTR", - "RTL", - "Orias", - "Oleh", - "Kuchuk", - "Kalani", - "Adamenko", - "Eppstein", - "Taskr", - "AST", - "EOL", - "Keller", - "Sylvain", - "Pollet", - "Villard" - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 328310865..45510c7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,549 +1,1441 @@ # Change Log -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. + +- Fix `Node#prev` and `Node#next` on missed parent. ## 6.0.21 -* Rename Chinese docs to fix `yarnpkg.com` issue. + +- Rename Chinese docs to fix `yarnpkg.com` issue. ## 6.0.20 -* Better error message on `null` as input CSS. + +- 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). + +- 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). + +- Use primitive object in TypeScript definitions (by Sylvain Pollet-Villard). ## 6.0.17 -* Fix parsing comment in selector between word tokens (by Oleh Kuchuk). + +- Fix parsing comment in selector between word tokens (by Oleh Kuchuk). ## 6.0.16 -* Fix warning text (by Michael Keller). + +- Fix warning text (by Michael Keller). ## 6.0.15 -* Add warning about missed `from` option on `process().then()` call. -* Add IE 10 support. + +- Add warning about missed `from` option on `process().then()` call. +- Add IE 10 support. ## 6.0.14 -* Fix TypeScript definitions (by Jed Mao). + +- Fix TypeScript definitions (by Jed Mao). ## 6.0.13 -* Fix TypeScript definitions for case of multiple PostCSS versions + +- Fix TypeScript definitions for case of multiple PostCSS versions in `node_modules` (by Chris Eppstein). -* Use `source-map` 0.6. +- Use `source-map` 0.6. ## 6.0.12 -* Don’t copy `*` hack to declaration indent. + +- Don’t copy `*` hack to declaration indent. ## 6.0.11 -* Add upper case `!IMPORTANT` support. + +- Add upper case `!IMPORTANT` support. ## 6.0.10 -* Reduce PostCSS size in webpack bundle. + +- Reduce PostCSS size in webpack bundle. ## 6.0.9 -* Improve error message for plugin with old PostCSS (by Igor Adamenko). + +- Improve error message for plugin with old PostCSS (by Igor Adamenko). ## 6.0.8 -* Fix Node.js 4.2.2 support. + +- Fix Node.js 4.2.2 support. ## 6.0.7 -* Fix base64 decoding for old Node.js and browser. + +- Fix base64 decoding for old Node.js and browser. ## 6.0.6 -* Fix `end` position in at-rule without semicolon (by Oleh Kuchuk). + +- 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. + +- Move Babel config from `package.json` for `node_modules` compiling cases. ## 6.0.4 -* Fix parsing `;;` after rules. -* Use Chalk 2.0. + +- 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. + +- 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`. + +- Keep `raws.before` on moving `Root` children to new `Root`. ## 6.0.1 -* Fix parser extensibility to use it in Safe Parser. + +- 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). + +- 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 + +- 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. + +- Add `postcss-sass` suggestion to syntax error on `.sass` input. ## 5.2.16 -* Better error on wrong argument in node constructor. + +- Better error on wrong argument in node constructor. ## 5.2.15 -* Fix TypeScript definitions (by bumbleblym). + +- Fix TypeScript definitions (by bumbleblym). ## 5.2.14 -* Fix browser bundle building in webpack (by janschoenherr). + +- Fix browser bundle building in webpack (by janschoenherr). ## 5.2.13 -* Do not add comment to important raws. -* Fix JSDoc (by Dmitry Semigradsky). + +- Do not add comment to important raws. +- Fix JSDoc (by Dmitry Semigradsky). ## 5.2.12 -* Fix typo in deprecation message (by Garet McKinley). + +- Fix typo in deprecation message (by Garet McKinley). ## 5.2.11 -* Fix TypeScript definitions (by Jed Mao). + +- Fix TypeScript definitions (by Jed Mao). ## 5.2.10 -* Fix TypeScript definitions (by Jed Mao). + +- Fix TypeScript definitions (by Jed Mao). ## 5.2.9 -* Update TypeScript definitions (by Jed Mao). + +- Update TypeScript definitions (by Jed Mao). ## 5.2.8 -* Fix error message (by Ben Briggs). + +- Fix error message (by Ben Briggs). ## 5.2.7 -* Better error message on syntax object in plugins list. + +- Better error message on syntax object in plugins list. ## 5.2.6 -* Fix `postcss.vendor` for values with spaces (by 刘祺). + +- Fix `postcss.vendor` for values with spaces (by 刘祺). ## 5.2.5 -* Better error message on unclosed string (by Ben Briggs). + +- Better error message on unclosed string (by Ben Briggs). ## 5.2.4 -* Improve terminal CSS syntax highlight (by Simon Lydell). + +- 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. + +- Better color highlight in syntax error code frame. +- Fix color highlight support in old systems. ## 5.2.2 -* Update `Processor#version`. + +- Update `Processor#version`. ## 5.2.1 -* Fix source map path for CSS without `from` option (by Michele Locati). + +- 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). + +- 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. + +- Suggests SCSS/Less parsers on parse errors depends on file extension. ## 5.1.1 -* Fix TypeScript definitions (by Efremov Alexey). + +- 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/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 227caa27d..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,78 +0,0 @@ -# Contributing Guide to PostCSS - -If you want contribute to PostCSS, there are few things that you should -be familiar with. - - -## In Case You Have Question About Using PostCSS - -* **Ask for help in [the chat]** - - If you stuck on something there is a big chance - that someone had similar problem before. - -[the chat]: https://gitter.im/postcss/postcss - - -## Adding Your Plugin to the List - -If you created or found a plugin and want to add it to PostCSS plugins list -follow this simple steps. - -PR should not change plugins defined in README it contains only favorite plugins -and moderated by PostCSS author. - -Plugins submitted by community located in [`docs/plugins`]. - -* **Keep plugins order** - - Be sure that plugin not presented yet and find suitable position - in alphabetic order for it. - But plugins with `postcss-` prefix should come first. - -* **Check spelling** - - Before submitting PR be sure that spelling check pass. - For that run command `npm test`. - If it fails with unknown word error, add it as word - to `.yaspellerrc` dictionary. - -* **Check PostCSS plugin guideline** - - Provided plugin should match plugin [guidelines]. - -- **Provide link to suggested plugin** - - Make sure your pull request description contains link to plugin - you are willing to add. - -[`docs/plugins`]: https://github.com/postcss/postcss/blob/master/docs/plugins.md -[guidelines]: https://github.com/postcss/postcss/blob/master/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 same subject. - * Interfaces used for defining custom types. - - Make sure you read through declaration file writing [best practices] - by 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 feature or fix existed issue - -- **Become familiar with PostCSS architecture** - - For gentle intro to PostCSS architecture look through our [guide]. - -[guide]: https://github.com/postcss/postcss/blob/master/docs/architecture.md 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-cn.md b/README-cn.md deleted file mode 100644 index 0a69cfa23..000000000 --- a/README-cn.md +++ /dev/null @@ -1,346 +0,0 @@ -# PostCSS [![Gitter][chat-img]][chat] - -哲学家的石头 - PostCSS 的 logo - -[chat-img]: https://img.shields.io/badge/Gitter-Join_the_PostCSS_chat-brightgreen.svg -[chat]: https://gitter.im/postcss/postcss - -PostCSS 是一个允许使用 JS 插件转换样式的工具。 -这些插件可以检查(lint)你的 CSS,支持 CSS Variables 和 Mixins, -编译尚未被浏览器广泛支持的先进的 CSS 语法,内联图片,以及其它很多优秀的功能。 - -PostCSS 在工业界被广泛地应用,其中不乏很多有名的行业领导者,如:维基百科,Twitter,阿里巴巴, -JetBrains。PostCSS 的 [Autoprefixer] 插件是最流行的 CSS 处理工具之一。 - -**Twitter 账号:** [@postcss](https://twitter.com/postcss)
-**支持 / 讨论:** [Gitter](https://gitter.im/postcss/postcss)
- -如果需要 PostCSS 商业支持(如咨询,提升公司的前端文化, -PostCSS 插件),请联系 [Evil Martians](https://evilmartians.com/?utm_source=postcss) -邮箱 。 - -[Autoprefixer]: https://github.com/postcss/autoprefixer - - - 由 Evil Martians 赞助 - - -## 插件 - -截止到目前,PostCSS 有 200 多个插件。你可以在 [插件列表] 或 [搜索目录] 找到它们。 -下方的列表是我们最喜欢的插件 - 它们很好地演示了我们可以用 PostCSS 做些什么。 - -如果你有任何新的想法,[开发 PostCSS 插件] 非常简单易上手。 - -[搜索目录]: http://postcss.parts -[插件列表]: https://github.com/postcss/postcss/blob/master/docs/plugins.md - -### 解决全局 CSS 的问题 - -* [`postcss-use`] 允许你在 CSS 里明确地设置 PostCSS 插件,并且只在当前文件执行它们。 -* [`postcss-modules`] 和 [`react-css-modules`] 可以自动以组件为单位隔绝 CSS 选择器。 -* [`postcss-autoreset`] 是全局样式重置的又一个选择,它更适用于分离的组件。 -* [`postcss-initial`] 添加了 `all: initial` 的支持,重置了所有继承的样式。 -* [`cq-prolyfill`] 添加了容器查询的支持,允许添加响应于父元素宽度的样式. - -### 提前使用先进的 CSS 特性 - -* [`autoprefixer`] 添加了 vendor 浏览器前缀,它使用 Can I Use 上面的数据。 -* [`postcss-preset-env`] 允许你使用未来的 CSS 特性。 -* [`postcss-image-set-polyfill`] 为所有浏览器模拟了 [`image-set`] 函数逻辑。 - -### 更佳的 CSS 可读性 - -* [`precss`] 囊括了许多插件来支持类似 Sass 的特性,比如 CSS 变量,套嵌,mixins 等。 -* [`postcss-sorting`] 给规则的内容以及@规则排序。 -* [`postcss-utilities`] 囊括了最常用的简写方式和书写帮助。 -* [`short`] 添加并拓展了大量的缩写属性。 - -### 图片和字体 - -* [`postcss-assets`] 可以插入图片尺寸和内联文件。 -* [`postcss-sprites`] 能生成雪碧图。 -* [`font-magician`] 生成所有在 CSS 里需要的 `@font-face` 规则。 -* [`postcss-inline-svg`] 允许你内联 SVG 并定制它的样式。 -* [`postcss-write-svg`] 允许你在 CSS 里写简单的 SVG。 - -### 提示器(Linters) - -* [`stylelint`] 是一个模块化的样式提示器。 -* [`stylefmt`] 是一个能根据 `stylelint` 规则自动优化 CSS 格式的工具。 -* [`doiuse`] 提示 CSS 的浏览器支持性,使用的数据来自于 Can I Use。 -* [`colorguard`] 帮助你保持一个始终如一的调色板。 - -### 其它 - -* [`postcss-rtl`] 在单个 CSS 文件里组合了两个方向(左到右,右到左)的样式。 -* [`cssnano`] 是一个模块化的 CSS 压缩器。 -* [`lost`] 是一个功能强大的 `calc()` 栅格系统。 -* [`rtlcss`] 镜像翻转 CSS 样式,适用于 right-to-left 的应用场景。 - -[`postcss-image-set-polyfill`]: https://github.com/SuperOl3g/postcss-image-set-polyfill -[`postcss-inline-svg`]: https://github.com/TrySound/postcss-inline-svg -[`postcss-preset-env`]: https://github.com/jonathantneal/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/jonathantneal/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-assets`]: https://github.com/assetsjs/postcss-assets -[开发 PostCSS 插件]: https://github.com/postcss/postcss/blob/master/docs/writing-a-plugin.md -[`font-magician`]: https://github.com/jonathantneal/postcss-font-magician -[`autoprefixer`]: https://github.com/postcss/autoprefixer -[`cq-prolyfill`]: https://github.com/ausi/cq-prolyfill -[`postcss-rtl`]: https://github.com/vkalinichev/postcss-rtl -[`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 -[`image-set`]: https://drafts.csswg.org/css-images-4/#image-set-notation -[`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 可以转化样式到任意语法,不仅仅是 CSS。 -如果还没有支持你最喜欢的语法,你可以编写一个解释器以及(或者)一个 stringifier 来拓展 PostCSS。 - -* [`sugarss`] 是一个以缩进为基础的语法,类似于 Sass 和 Stylus。 -* [`postcss-syntax`] 通过文件扩展名自动切换语法。 -* [`postcss-html`] 解析类 HTML 文件里`' + * ) + * 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 dfbba7b7c..000000000 --- a/lib/input.es6 +++ /dev/null @@ -1,169 +0,0 @@ -import CssSyntaxError from './css-syntax-error'; -import PreviousMap from './previous-map'; - -import path from 'path'; - -let sequence = 0; - -/** - * 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 = { }) { - if ( css === null || typeof css === 'object' && !css.toString ) { - throw new Error(`PostCSS received ${ css } instead of CSS string`); - } - - /** - * @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; - -/** - * @typedef {object} filePosition - * @property {string} file - path to file - * @property {number} line - source line in file - * @property {number} column - source column in file - */ 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 24e33efe4..000000000 --- a/lib/lazy-result.es6 +++ /dev/null @@ -1,370 +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'; -} - -/** - * 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 !== null && 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 executed on any error - * - * @return {Promise} Promise API to make queue - * - * @example - * postcss([cssnext]).process(css, { from: cssPath }).then(result => { - * console.log(result.css); - * }); - */ - then(onFulfilled, onRejected) { - 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); - } - - /** - * 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 executed 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]) ) { - 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) { - 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; - -/** - * @callback onFulfilled - * @param {Result} result - */ - -/** - * @callback onRejected - * @param {Error} error - */ 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 b205017a3..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[]} split 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[]} split 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 2a8af808c..000000000 --- a/lib/map-generator.es6 +++ /dev/null @@ -1,279 +0,0 @@ -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; - } - } - - toBase64(str) { - if ( Buffer ) { - if ( Buffer.from && Buffer.from !== Uint8Array.from ) { - return Buffer.from(str).toString('base64'); - } else { - return new Buffer(str).toString('base64'); - } - } else { - return window.btoa(unescape(encodeURIComponent(str))); - } - } - - 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 { - 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 ( file.indexOf('<') === 0 ) return 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 99e0fa79c..000000000 --- a/lib/node.es6 +++ /dev/null @@ -1,538 +0,0 @@ -import CssSyntaxError from './css-syntax-error'; -import Stringifier from './stringifier'; -import stringify from './stringify'; -import warnOnce from './warn-once'; - -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 ( type === 'object' && value !== null ) value = cloneNode(value); - cloned[i] = value; - } - } - - return cloned; -}; - -/** - * All node classes inherit the following common methods. - * - * @abstract - */ -class Node { - - /** - * @param {object} [defaults] - value for node properties - */ - constructor(defaults = { }) { - this.raws = { }; - if ( typeof defaults !== 'object' && typeof defaults !== 'undefined' ) { - throw new Error( - 'PostCSS nodes constructor accepts object, not ' + - JSON.stringify(defaults)); - } - 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 (root, result) => { - * root.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; - } - - moveTo(newParent) { - warnOnce('Node#moveTo was deprecated. Use Container#append.'); - this.cleanRaws(this.root() === newParent.root()); - this.remove(); - newParent.append(this); - return this; - } - - moveBefore(otherNode) { - warnOnce('Node#moveBefore was deprecated. Use Node#before.'); - this.cleanRaws(this.root() === otherNode.root()); - this.remove(); - otherNode.parent.insertBefore(otherNode, this); - return this; - } - - moveAfter(otherNode) { - warnOnce('Node#moveAfter was deprecated. Use Node#after.'); - 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() { - if ( !this.parent ) return undefined; - 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() { - if ( !this.parent ) return undefined; - let index = this.parent.index(this); - return this.parent.nodes[index - 1]; - } - - /** - * Insert new node before current node to current node’s parent. - * - * Just alias for `node.parent.insertBefore(node, add)`. - * - * @param {Node|object|string|Node[]} add - new node - * - * @return {Node} this node for methods chain. - * - * @example - * decl.before('content: ""'); - */ - before(add) { - this.parent.insertBefore(this, add); - return this; - } - - /** - * Insert new node after current node to current node’s parent. - * - * Just alias for `node.parent.insertAfter(node, add)`. - * - * @param {Node|object|string|Node[]} add - new node - * - * @return {Node} this node for methods chain. - * - * @example - * decl.after('color: black'); - */ - after(add) { - this.parent.insertAfter(this, add); - return this; - } - - 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; - } - - /** - * @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; - -/** - * @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 - */ 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 f696e2e03..000000000 --- a/lib/parse.es6 +++ /dev/null @@ -1,34 +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); - try { - parser.parse(); - } catch (e) { - 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; -} 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 9b4fbb350..000000000 --- a/lib/parser.es6 +++ /dev/null @@ -1,527 +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.root = new Root(); - this.current = this.root; - this.spaces = ''; - this.semicolon = false; - - this.createTokenizer(); - this.root.source = { input, start: { line: 1, column: 1 } }; - } - - createTokenizer() { - this.tokenizer = tokenizer(this.input); - } - - 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(); - } - - 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; - } - - other(start) { - let end = false; - let type = null; - let colon = false; - let bracket = null; - let brackets = []; - - 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 ( brackets.length === 0 ) { - if ( type === ';' ) { - if ( colon ) { - this.decl(tokens); - 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 ) { - while ( tokens.length ) { - token = tokens[tokens.length - 1][0]; - if ( token !== 'space' && token !== 'comment' ) break; - this.tokenizer.back(tokens.pop()); - } - this.decl(tokens); - return; - } else { - this.unknownWord(tokens); - } - } - - rule(tokens) { - tokens.pop(); - - let node = new Rule(); - this.init(node, tokens[0][2], tokens[0][3]); - - node.raws.between = this.spacesAndCommentsFromEnd(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' ) { - if ( tokens.length === 1 ) this.unknownWord(tokens); - 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.spacesAndCommentsFromStart(tokens); - 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().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 prev; - let shift; - let last = false; - let open = false; - let params = []; - - while ( !this.tokenizer.endOfFile() ) { - token = this.tokenizer.nextToken(); - - 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] === '}') { - if ( params.length > 0 ) { - shift = params.length - 1; - prev = params[shift]; - while ( prev && prev[0] === 'space' ) { - prev = params[--shift]; - } - if ( prev ) { - node.source.end = { line: prev[4], column: prev[5] }; - } - } - this.end(token); - break; - } 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 = { 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; - } - - 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 = ''; - } - } - } - - // 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; - let next, prev; - const pattern = /^([.|#])?([\w])+/i; - - for ( let i = 0; i < length; i += 1 ) { - token = tokens[i]; - type = token[0]; - - if ( type === 'comment' && node.type === 'rule' ) { - prev = tokens[i - 1]; - next = tokens[i + 1]; - - if ( - prev[0] !== 'space' && - next[0] !== 'space' && - pattern.test(prev[1]) && - pattern.test(next[1]) - ) { - value += token[1]; - } else { - clean = false; - } - - continue; - } - - 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; - } - - 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; - } - - 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; - } - - 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(tokens) { - throw this.input.error('Unknown word', tokens[0][2], tokens[0][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 index 8123e3fe0..41bf40f9b 100644 --- a/lib/postcss.d.ts +++ b/lib/postcss.d.ts @@ -1,1301 +1,464 @@ -import * as mozilla from 'source-map'; +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 +} -/** - * @param plugins Can also be included with the Processor#use method. - * @returns A processor that will apply plugins as CSS processors. - */ -declare function postcss(plugins?: postcss.AcceptedPlugin[]): postcss.Processor; -declare function postcss(...plugins: postcss.AcceptedPlugin[]): postcss.Processor; declare namespace postcss { - type AcceptedPlugin = Plugin | Transformer | { - postcss: TransformCallback | Processor; - } | Processor; - /** - * Creates a PostCSS plugin with a standard API. - * @param name Plugin name. Same as in name property in package.json. It will - * be saved in plugin.postcssPlugin property. - * @param initializer Will receive plugin options and should return functions - * to modify nodes in input CSS. - */ - function plugin(name: string, initializer: PluginInitializer): Plugin; - interface Plugin extends Transformer { - (opts?: T): Transformer; - postcss: Transformer; - process: (css: string | { - toString(): string; - } | Result, opts?: any) => LazyResult; - } - interface Transformer extends TransformCallback { - postcssPlugin?: string; - postcssVersion?: string; - } - interface TransformCallback { - /** - * @returns Asynchronous plugins should return a promise. - */ - (root: Root, result?: Result): void | Function | any; - } - interface PluginInitializer { - (pluginOptions?: T): Transformer; - } - /** - * Contains helpers for working with vendor prefixes. - */ - export namespace vendor { - /** - * @returns The vendor prefix extracted from the input string. - */ - function prefix(prop: string): string; - /** - * @returns The input string stripped of its vendor prefix. - */ - function unprefixed(prop: string): string; - } - export class Stringifier { - builder: Stringifier.Builder; - constructor(builder?: Stringifier.Builder); - stringify(node: Node, semicolon?: boolean): void; - root(node: any): void; - comment(node: any): void; - decl(node: any, semicolon: any): void; - rule(node: any): void; - atrule(node: any, semicolon: any): void; - body(node: any): void; - block(node: any, start: any): void; - raw(node: Node, own: string, detect?: string): any; - rawSemicolon(root: any): any; - rawEmptyBody(root: any): any; - rawIndent(root: any): any; - rawBeforeComment(root: any, node: any): any; - rawBeforeDecl(root: any, node: any): any; - rawBeforeRule(root: any): any; - rawBeforeClose(root: any): any; - rawBeforeOpen(root: any): any; - rawColon(root: any): any; - beforeAfter(node: any, detect: any): any; - rawValue(node: any, prop: any): any; - } - export namespace Stringifier { - interface Builder { - (str: string, node?: Node, str2?: string): void; - } - } - /** - * Default function to convert a node tree into a CSS string. - */ - function stringify(node: Node, builder: Stringifier.Builder): void; - /** - * Parses source CSS. - * @param css The CSS to parse. - * @param options - * @returns {} A new Root node, which contains the source CSS nodes. - */ - function parse(css: string | { - toString(): string; - } | LazyResult | Result, options?: { - from?: string; - map?: postcss.SourceMapOptions; - }): Root; - /** - * Contains helpers for safely splitting lists of CSS values, preserving - * parentheses and quotes. - */ - export namespace list { - /** - * Safely splits space-separated values (such as those for background, - * border-radius and other shorthand properties). - */ - function space(str: string): string[]; - /** - * Safely splits comma-separated values (such as those for transition-* and - * background properties). - */ - function comma(str: string): string[]; - } - /** - * Creates a new Comment node. - * @param defaults Properties for the new Comment node. - * @returns The new node. - */ - function comment(defaults?: CommentNewProps): Comment; + 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 { /** - * Creates a new AtRule node. - * @param defaults Properties for the new AtRule node. - * @returns The new node. + * Function to generate AST by string. */ - function atRule(defaults?: AtRuleNewProps): AtRule; + parse?: Parser + /** - * Creates a new Declaration node. - * @param defaults Properties for the new Declaration node. - * @returns The new node. + * Class to generate string by AST. */ - function decl(defaults?: DeclarationNewProps): Declaration; + stringify?: Stringifier + } + + export interface SourceMapOptions { /** - * Creates a new Rule node. - * @param defaults Properties for the new Rule node. - * @returns The new node. + * Use absolute path in generated source map. */ - function rule(defaults?: RuleNewProps): Rule; + absolute?: boolean + /** - * Creates a new Root node. - * @param defaults Properties for the new Root node. - * @returns The new node. + * 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. */ - function root(defaults?: object): Root; - interface SourceMapOptions { - /** - * 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 compilation). - * 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?: any; - /** - * 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; - /** - * 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?: boolean | string; - /** - * If true, PostCSS will try to correct any syntax errors that it finds in the CSS. - * This is useful for legacy code filled with hacks. Another use-case is interactive - * tools with live input — for example, the Autoprefixer demo. - */ - safe?: boolean; - } + annotation?: ((file: string, root: Root) => string) | boolean | string + /** - * A Processor instance contains plugins to process CSS. Create one - * Processor instance, initialize its plugins, and then use that instance - * on numerous CSS files. + * Override `from` in map’s sources. */ - interface Processor { - /** - * Adds a plugin to be used as a CSS processor. Plugins can also be - * added by passing them as arguments when creating a postcss instance. - */ - use(plugin: AcceptedPlugin): Processor; - /** - * Parses source CSS. Because some plugins can be asynchronous it doesn't - * make any transformations. Transformations will be applied in LazyResult's - * methods. - * @param css Input CSS or any object with toString() method, like a file - * stream. If a Result instance is passed the processor will take the - * existing Root parser from it. - */ - process(css: string | { - toString(): string; - } | Result, options?: ProcessOptions): LazyResult; - /** - * Contains plugins added to this processor. - */ - plugins: Plugin[]; - /** - * Contains the current version of PostCSS (e.g., "4.0.5"). - */ - version: string; - } - interface ProcessOptions extends Syntax { - /** - * The path of the CSS source file. You should always set from, because it is - * used in source map generation and syntax error messages. - */ - from?: string; - /** - * The path where you'll put the output CSS file. You should always set it - * to generate correct source maps. - */ - to?: string; - syntax?: Syntax; - /** - * Enable Safe Mode, in which PostCSS will try to fix CSS syntax errors. - */ - safe?: boolean; - map?: postcss.SourceMapOptions; - /** - * Function to generate AST by string. - */ - parser?: Parse | Syntax; - /** - * Class to generate string by AST. - */ - stringifier?: Stringify | Syntax; - } - interface Syntax { - /** - * Function to generate AST by string. - */ - parse?: Parse; - /** - * Class to generate string by AST. - */ - stringify?: Stringify; - } - interface Parse { - (css?: string, opts?: postcss.SourceMapOptions): Root; - } - interface Stringify { - (node?: postcss.Node, builder?: any): postcss.Result | void; - } + from?: string + /** - * A promise proxy for the result of PostCSS transformations. + * 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`. */ - interface LazyResult { - /** - * Processes input CSS through synchronous and asynchronous plugins. - * @param onRejected Called if any plugin throws an error. - */ - then(onFulfilled: (result: Result) => void, onRejected?: (error: Error) => void): Function | any; - /** - * Processes input CSS through synchronous and asynchronous plugins. - * @param onRejected Called if any plugin throws an error. - */ - catch(onRejected: (error: Error) => void): Function | any; - /** - * Alias for css property. - */ - toString(): string; - /** - * Processes input CSS through synchronous plugins and converts Root to - * CSS string. This property will only work with synchronous plugins. If - * the processor contains any asynchronous plugins it will throw an error. - * In this case, you should use LazyResult#then() instead. - * @returns Result#css. - */ - css: string; - /** - * Alias for css property to use when syntaxes generate non-CSS output. - */ - content: string; - /** - * Processes input CSS through synchronous plugins. This property will - * work only with synchronous plugins. If processor contains any - * asynchronous plugins it will throw an error. You should use - * LazyResult#then() instead. - */ - map: ResultMap; - /** - * Processes input CSS through synchronous plugins. This property will work - * only with synchronous plugins. If processor contains any asynchronous - * plugins it will throw an error. You should use LazyResult#then() instead. - */ - root: Root; - /** - * Processes input CSS through synchronous plugins and calls Result#warnings(). - * This property will only work with synchronous plugins. If the processor - * contains any asynchronous plugins it will throw an error. In this case, - * you should use LazyResult#then() instead. - */ - warnings(): ResultMessage[]; - /** - * Processes input CSS through synchronous plugins. This property will work - * only with synchronous plugins. If processor contains any asynchronous - * plugins it will throw an error. You should use LazyResult#then() instead. - */ - messages: ResultMessage[]; - /** - * @returns A processor used for CSS transformations. - */ - processor: Processor; - /** - * @returns Options from the Processor#process(css, opts) call that produced - * this Result instance. - */ - opts: ResultOptions; - } + inline?: boolean + /** - * Provides the result of the PostCSS transformations. + * 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`. */ - interface Result { - /** - * Alias for css property. - */ - toString(): string; - /** - * Creates an instance of Warning and adds it to messages. - * @param message Used in the text property of the message object. - * @param options Properties for Message object. - */ - warn(message: string, options?: WarningOptions): void; - /** - * @returns Warnings from plugins, filtered from messages. - */ - warnings(): ResultMessage[]; - /** - * A CSS string representing this Result's Root instance. - */ - css: string; - /** - * Alias for css property to use with syntaxes that generate non-CSS output. - */ - content: string; - /** - * An instance of the SourceMapGenerator class from the source-map library, - * representing changes to the Result's Root instance. - * This property will have a value only if the user does not want an inline - * source map. By default, PostCSS generates inline source maps, written - * directly into the processed CSS. The map property will be empty by default. - * An external source map will be generated — and assigned to map — only if - * the user has set the map.inline option to false, or if PostCSS was passed - * an external input source map. - */ - map: ResultMap; - /** - * Contains the Root node after all transformations. - */ - root?: Root; - /** - * Contains messages from plugins (e.g., warnings or custom messages). - * Add a warning using Result#warn() and get all warnings - * using the Result#warnings() method. - */ - messages: ResultMessage[]; - /** - * The Processor instance used for this transformation. - */ - processor?: Processor; - /** - * Options from the Processor#process(css, opts) or Root#toResult(opts) call - * that produced this Result instance. - */ - opts?: ResultOptions; - } - interface ResultOptions extends ProcessOptions { - /** - * The CSS node that was the source of the warning. - */ - node?: postcss.Node; - /** - * Name of plugin that created this warning. Result#warn() will fill it - * automatically with plugin.postcssPlugin value. - */ - plugin?: string; - } - interface ResultMap { - /** - * Add a single mapping from original source line and column to the generated - * source's line and column for this source map being created. The mapping - * object should have the following properties: - * @param mapping - * @returns {} - */ - addMapping(mapping: mozilla.Mapping): void; - /** - * Set the source content for an original source file. - * @param sourceFile The URL of the original source file. - * @param sourceContent The content of the source file. - */ - setSourceContent(sourceFile: string, sourceContent: string): void; - /** - * Applies a SourceMap for a source file to the SourceMap. Each mapping to - * the supplied source file is rewritten using the supplied SourceMap. - * Note: The resolution for the resulting mappings is the minimium of this - * map and the supplied map. - * @param sourceMapConsumer The SourceMap to be applied. - * @param sourceFile The filename of the source file. If omitted, sourceMapConsumer - * file will be used, if it exists. Otherwise an error will be thrown. - * @param sourceMapPath The dirname of the path to the SourceMap to be applied. - * If relative, it is relative to the SourceMap. This parameter is needed when - * the two SourceMaps aren't in the same directory, and the SourceMap to be - * applied contains relative source paths. If so, those relative source paths - * need to be rewritten relative to the SourceMap. - * If omitted, it is assumed that both SourceMaps are in the same directory; - * thus, not needing any rewriting (Supplying '.' has the same effect). - */ - applySourceMap( - sourceMapConsumer: mozilla.SourceMapConsumer, - sourceFile?: string, - sourceMapPath?: string - ): void; - /** - * Renders the source map being generated to JSON. - */ - toJSON: () => mozilla.RawSourceMap; - /** - * Renders the source map being generated to a string. - */ - toString: () => string; - } - interface ResultMessage { - type: string; - text?: string; - plugin?: string; - browsers?: string[]; - } + prev?: ((file: string) => string) | boolean | object | string + /** - * Represents a plugin warning. It can be created using Result#warn(). + * 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. */ - interface Warning { - /** - * @returns Error position, message. - */ - toString(): string; - /** - * Contains the warning message. - */ - text: string; - /** - * Contains the name of the plugin that created this warning. When you - * call Result#warn(), it will fill this property automatically. - */ - plugin: string; - /** - * The CSS node that caused the warning. - */ - node: Node; - /** - * The line in the input file with this warning's source. - */ - line: number; - /** - * Column in the input file with this warning's source. - */ - column: number; - } - interface WarningOptions extends ResultOptions { - /** - * A word inside a node's string that should be highlighted as source - * of warning. - */ - word?: string; - /** - * The index inside a node's string that should be highlighted as - * source of warning. - */ - index?: number; - } + sourcesContent?: boolean + } + + export interface ProcessOptions { /** - * The CSS parser throws this error for broken CSS. + * 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.test.js b/test/parse.test.js deleted file mode 100644 index a35408524..000000000 --- a/test/parse.test.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; - -const parse = require('../lib/parse'); -const Root = require('../lib/root'); - -const cases = require('postcss-parser-tests'); -const path = require('path'); -const fs = require('fs'); - -it('works with file reads', () => { - let stream = fs.readFileSync(cases.path('atrule-empty.css')); - expect(parse(stream) instanceof Root).toBeTruthy(); -}); - -cases.each( (name, css, json) => { - it('parses ' + name, () => { - let parsed = cases.jsonify(parse(css, { from: name })); - expect(parsed).toEqual(json); - }); -}); - -it('saves source file', () => { - let css = parse('a {}', { from: 'a.css' }); - expect(css.first.source.input.css).toEqual('a {}'); - expect(css.first.source.input.file).toEqual(path.resolve('a.css')); - expect(css.first.source.input.from).toEqual(path.resolve('a.css')); -}); - -it('keeps absolute path in source', () => { - let css = parse('a {}', { from: 'http://example.com/a.css' }); - expect(css.first.source.input.file).toEqual('http://example.com/a.css'); - expect(css.first.source.input.from).toEqual('http://example.com/a.css'); -}); - -it('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); - expect(root2.first.source.input.file).toEqual(path.resolve('to.css')); -}); - -it('sets unique ID for file without name', () => { - let css1 = parse('a {}'); - let css2 = parse('a {}'); - expect(css1.first.source.input.id).toMatch(/^$/); - expect(css1.first.source.input.from).toMatch(/^$/); - expect(css2.first.source.input.id).not.toEqual(css1.first.source.input.id); -}); - -it('sets parent node', () => { - 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; - - expect(decl.parent).toBe(from); - expect(from.parent).toBe(keyframes); - expect(support.parent).toBe(css); - expect(keyframes.parent).toBe(support); -}); - -it('ignores wrong close bracket', () => { - let root = parse('a { p: ()) }'); - expect(root.first.first.value).toEqual('())'); -}); - -it('ignores symbols before declaration', () => { - let root = parse('a { :one: 1 }'); - expect(root.first.first.raws.before).toEqual(' :'); -}); - -it('parses double semicolon after rule', () => { - expect(parse('a { };;').toString()).toEqual('a { };;'); -}); - -it('throws on unclosed blocks', () => { - expect(() => { - parse('\na {\n'); - }).toThrowError(/:2:1: Unclosed block/); -}); - -it('throws on unnecessary block close', () => { - expect(() => { - parse('a {\n} }'); - }).toThrowError(/:2:3: Unexpected }/); -}); - -it('throws on unclosed comment', () => { - expect(() => { - parse('\n/*\n '); - }).toThrowError(/:2:1: Unclosed comment/); -}); - -it('throws on unclosed quote', () => { - expect(() => { - parse('\n"\n\na '); - }).toThrowError(/:2:1: Unclosed string/); -}); - -it('throws on unclosed bracket', () => { - expect(() => { - parse(':not(one() { }'); - }).toThrowError(/:1:5: Unclosed bracket/); -}); - -it('throws on property without value', () => { - expect(() => { - parse('a { b;}'); - }).toThrowError(/:1:5: Unknown word/); - expect(() => { - parse('a { b b }'); - }).toThrowError(/:1:5: Unknown word/); -}); - -it('throws on nameless at-rule', () => { - expect(() => { - parse('@'); - }).toThrowError(/:1:1: At-rule without name/); -}); - -it('throws on property without semicolon', () => { - expect(() => { - parse('a { one: filter(a:"") two: 2 }'); - }).toThrowError(/:1:21: Missed semicolon/); -}); - -it('throws on double colon', () => { - expect(() => { - parse('a { one:: 1 }'); - }).toThrowError(/:1:9: Double colon/); -}); - -it('throws on just colon', () => { - expect(() => { - parse(':'); - }).toThrowError(/:1:1: Unknown word/); - expect(() => { - parse(' : '); - }).toThrowError(/:1:2: Unknown word/); -}); - -it('does not suggest different parsers for CSS', () => { - let error; - try { - parse('a { one:: 1 }', { from: 'app.css' }); - } catch (e) { - error = e; - } - expect(error.message).not.toMatch(/postcss-less|postcss-scss/); -}); - -it('suggests postcss-scss for SCSS sources', () => { - expect(() => { - parse('a { #{var}: 1 }', { from: 'app.scss' }); - }).toThrowError(/postcss-scss/); -}); - -it('suggests postcss-sass for Sass sources', () => { - expect(() => { - parse('a\n #{var}: 1', { from: 'app.sass' }); - }).toThrowError(/postcss-sass/); -}); - -it('suggests postcss-less for Less sources', () => { - expect(() => { - parse('.@{my-selector} { }', { from: 'app.less' }); - }).toThrowError(/postcss-less/); -}); 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.test.js b/test/postcss.test.js deleted file mode 100644 index ba599e326..000000000 --- a/test/postcss.test.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict'; - -const Processor = require('../lib/processor'); -const postcss = require('../lib/postcss'); - -it('creates plugins list', () => { - let processor = postcss(); - expect(processor instanceof Processor).toBeTruthy(); - expect(processor.plugins).toEqual([]); -}); - -it('saves plugins list', () => { - let a = () => 1; - let b = () => 2; - expect(postcss(a, b).plugins).toEqual([a, b]); -}); - -it('saves plugins list as array', () => { - let a = () => 1; - let b = () => 2; - expect(postcss([a, b]).plugins).toEqual([a, b]); -}); - -it('takes plugin from other processor', () => { - let a = () => 1; - let b = () => 2; - let c = () => 3; - let other = postcss([a, b]); - expect(postcss([other, c]).plugins).toEqual([a, b, c]); -}); - -it('supports injecting additional processors at runtime', () => { - 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}', { from: undefined }) - .then(result => { - expect(result.css).toEqual('a{hello: world}'); - }); -}); - -it('creates plugin', () => { - let plugin = postcss.plugin('test', filter => { - return root => { - root.walkDecls(filter || 'two', i => i.remove()); - }; - }); - - let func1 = postcss(plugin).plugins[0]; - expect(func1.postcssPlugin).toEqual('test'); - expect(func1.postcssVersion).toMatch(/\d+.\d+.\d+/); - - let func2 = postcss(plugin()).plugins[0]; - expect(func2.postcssPlugin).toEqual(func1.postcssPlugin); - expect(func2.postcssVersion).toEqual(func1.postcssVersion); - - let result1 = postcss(plugin('one')).process('a{ one: 1; two: 2 }'); - expect(result1.css).toEqual('a{ two: 2 }'); - - let result2 = postcss(plugin).process('a{ one: 1; two: 2 }'); - expect(result2.css).toEqual('a{ one: 1 }'); -}); - -it('does not call plugin constructor', () => { - let calls = 0; - let plugin = postcss.plugin('test', () => { - calls += 1; - return () => { }; - }); - expect(calls).toBe(0); - - postcss(plugin).process('a{}'); - expect(calls).toBe(1); - - postcss(plugin()).process('a{}'); - expect(calls).toBe(2); -}); - -it('creates a shortcut to process css', () => { - let plugin = postcss.plugin('test', str => { - return root => { - root.walkDecls(i => { - i.value = str || 'bar'; - }); - }; - }); - - let result1 = plugin.process('a{value:foo}'); - expect(result1.css).toEqual('a{value:bar}'); - - let result2 = plugin.process('a{value:foo}', { }, 'baz'); - expect(result2.css).toEqual('a{value:baz}'); - - plugin.process('a{value:foo}', { from: 'a' }, 'baz').then(result => { - expect(result.opts).toEqual({ from: 'a' }); - expect(result.css).toEqual('a{value:baz}'); - }); -}); - -it('contains parser', () => { - expect(postcss.parse('').type).toEqual('root'); -}); - -it('contains stringifier', () => { - expect(typeof postcss.stringify).toEqual('function'); -}); - -it('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); - - expect(root.toString()).toEqual('/* Example */\n' + - '@media screen {\n' + - ' a {\n' + - ' color: black\n' + - ' }\n' + - '}\n'); -}); - -it('contains vendor module', () => { - expect(postcss.vendor.prefix('-moz-tab')).toEqual('-moz-'); -}); - -it('contains list module', () => { - expect(postcss.list.space('a b')).toEqual(['a', 'b']); -}); - -it('works with null', () => { - expect(() => { - postcss().process(null).css; - }).toThrowError(/PostCSS received null instead of CSS string/); -}); 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.test.js b/test/previous-map.test.js deleted file mode 100644 index dcc189049..000000000 --- a/test/previous-map.test.js +++ /dev/null @@ -1,196 +0,0 @@ -'use strict'; - -const parse = require('../lib/parse'); - -const mozilla = require('source-map'); -const path = require('path'); -const fs = require('fs-extra'); - -let dir = path.join(__dirname, 'prevmap-fixtures'); -let mapObj = { - version: 3, - file: null, - sources: [], - names: [], - mappings: '' -}; -let map = JSON.stringify(mapObj); - -afterEach(() => { - if ( fs.existsSync(dir) ) fs.removeSync(dir); -}); - -it('misses property if no map', () => { - expect(parse('a{}').source.input.map).not.toBeDefined(); -}); - -it('creates property if map present', () => { - let root = parse('a{}', { map: { prev: map } }); - expect(root.source.input.map.text).toEqual(map); -}); - -it('returns consumer', () => { - let obj = parse('a{}', { map: { prev: map } }).source.input.map.consumer(); - expect(obj instanceof mozilla.SourceMapConsumer).toBeTruthy(); -}); - -it('sets annotation property', () => { - let mapOpts = { map: { prev: map } }; - - let root1 = parse('a{}', mapOpts); - expect(root1.source.input.map.annotation).not.toBeDefined(); - - let root2 = parse('a{}/*# sourceMappingURL=a.css.map */', mapOpts); - expect(root2.source.input.map.annotation).toEqual('a.css.map'); -}); - -it('checks previous sources content', () => { - let map2 = { - version: 3, - file: 'b', - sources: ['a'], - names: [], - mappings: '' - }; - - let opts = { map: { prev: map2 } }; - expect(parse('a{}', opts).source.input.map.withContent()).toBe(false); - - map2.sourcesContent = ['a{}']; - expect(parse('a{}', opts).source.input.map.withContent()).toBe(true); -}); - -it('decodes base64 maps', () => { - let b64 = new Buffer(map).toString('base64'); - let css = 'a{}\n' + - `/*# sourceMappingURL=data:application/json;base64,${b64} */`; - - expect(parse(css).source.input.map.text).toEqual(map); -}); - -it('decodes base64 UTF-8 maps', () => { - let b64 = new Buffer(map).toString('base64'); - let css = 'a{}\n/*# sourceMappingURL=data:application/json;' + - 'charset=utf-8;base64,' + b64 + ' */'; - - expect(parse(css).source.input.map.text).toEqual(map); -}); - -it('accepts different name for UTF-8 encoding', () => { - let b64 = new Buffer(map).toString('base64'); - let css = 'a{}\n/*# sourceMappingURL=data:application/json;' + - 'charset=utf8;base64,' + b64 + ' */'; - - expect(parse(css).source.input.map.text).toEqual(map); -}); - -it('decodes URI maps', () => { - let uri = 'data:application/json,' + decodeURI(map); - let css = `a{}\n/*# sourceMappingURL=${ uri } */`; - - expect(parse(css).source.input.map.text).toEqual(map); -}); - -it('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; - expect(input.map).not.toBeDefined(); -}); - -it('raises on unknown inline encoding', () => { - let css = 'a { }\n/*# sourceMappingURL=data:application/json;' + - 'md5,68b329da9893e34099c7d8ad5cb9c940*/'; - - expect(() => { - parse(css); - }).toThrowError('Unsupported source map encoding md5'); -}); - -it('raises on unknown map format', () => { - expect(() => { - parse('a{}', { map: { prev: 1 } }); - }).toThrowError('Unsupported previous source map format: 1'); -}); - -it('reads map from annotation', () => { - let file = path.join(dir, 'a.map'); - fs.outputFileSync(file, map); - let root = parse('a{}\n/*# sourceMappingURL=a.map */', { from: file }); - - expect(root.source.input.map.text).toEqual(map); - expect(root.source.input.map.root).toEqual(dir); -}); - -it('sets uniq name for inline map', () => { - 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; - - expect(file1).toMatch(/^$/); - expect(file1).not.toEqual(file2); -}); - -it('accepts an empty mappings string', () => { - let emptyMap = { - version: 3, - sources: [], - names: [], - mappings: '' - }; - parse('body{}', { map: { prev: emptyMap } } ); -}); - -it('accepts a function', () => { - let css = 'body{}\n/*# sourceMappingURL=a.map */'; - let file = path.join(dir, 'previous-sourcemap-function.map'); - fs.outputFileSync(file, map); - let opts = { - map: { - prev: () => file - } - }; - let root = parse(css, opts); - expect(root.source.input.map.text).toEqual(map); - expect(root.source.input.map.annotation).toEqual('a.map'); -}); - -it('calls function with opts.from', () => { - expect.assertions(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 => { - expect(from).toEqual('a.css'); - return file; - } - } - }; - parse(css, opts); -}); - -it('raises when function returns invalid path', () => { - 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 - } - }; - expect(() => { - parse(css, opts); - }).toThrowError('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.test.js b/test/processor.test.js deleted file mode 100644 index 4ff815746..000000000 --- a/test/processor.test.js +++ /dev/null @@ -1,469 +0,0 @@ -'use strict'; - -const LazyResult = require('../lib/lazy-result'); -const Processor = require('../lib/processor'); -const postcss = require('../lib/postcss'); -const Result = require('../lib/result'); -const parse = require('../lib/parse'); -const Root = require('../lib/root'); - -const path = require('path'); - -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: '""' }); - } - }); -}]); - -const originWarn = console.warn; -const originError = console.error; -afterAll(() => { - console.warn = originWarn; - console.error = originError; -}); - -it('adds new plugins', () => { - let a = () => 1; - let processor = new Processor(); - processor.use(a); - expect(processor.plugins).toEqual([a]); -}); - -it('adds new plugin by object', () => { - let a = () => 1; - let processor = new Processor(); - processor.use({ postcss: a }); - expect(processor.plugins).toEqual([a]); -}); - -it('adds new plugin by object-function', () => { - let a = () => 1; - let obj = () => 2; - obj.postcss = a; - let processor = new Processor(); - processor.use(obj); - expect(processor.plugins).toEqual([a]); -}); - -it('adds new processors of another postcss instance', () => { - let a = () => 1; - let processor = new Processor(); - let other = new Processor([a]); - processor.use(other); - expect(processor.plugins).toEqual([a]); -}); - -it('adds new processors from object', () => { - let a = () => 1; - let processor = new Processor(); - let other = new Processor([a]); - processor.use({ postcss: other }); - expect(processor.plugins).toEqual([a]); -}); - -it('returns itself', () => { - let a = () => 1; - let b = () => 2; - let processor = new Processor(); - expect(processor.use(a).use(b).plugins).toEqual([a, b]); -}); - -it('throws on wrong format', () => { - let pr = new Processor(); - expect(() => { - pr.use(1); - }).toThrowError(/1 is not a PostCSS plugin/); -}); - -it('processes CSS', () => { - let result = beforeFix.process('a::before{top:0}'); - expect(result.css).toEqual('a::before{content:"";top:0}'); -}); - -it('processes parsed AST', () => { - let root = parse('a::before{top:0}'); - let result = beforeFix.process(root); - expect(result.css).toEqual('a::before{content:"";top:0}'); -}); - -it('processes previous result', () => { - let result = (new Processor()).process('a::before{top:0}'); - result = beforeFix.process(result); - expect(result.css).toEqual('a::before{content:"";top:0}'); -}); - -it('takes maps from previous result', () => { - let one = (new Processor()).process('a{}', { - from: 'a.css', - to: 'b.css', - map: { inline: false } - }); - let two = (new Processor()).process(one, { to: 'c.css' }); - expect(two.map.toJSON().sources).toEqual(['a.css']); -}); - -it('inlines maps from previous result', () => { - 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 } - }); - expect(two.map).not.toBeDefined(); -}); - -it('throws with file name', () => { - let error; - try { - (new Processor()).process('a {', { from: 'a.css' }).css; - } catch (e) { - if ( e.name === 'CssSyntaxError' ) { - error = e; - } else { - throw e; - } - } - - expect(error.file).toEqual(path.resolve('a.css')); - expect(error.message).toMatch(/a.css:1:1: Unclosed block$/); -}); - -it('allows to replace Root', () => { - let plugin = (css, result) => { - result.root = new Root(); - }; - let processor = new Processor([plugin]); - expect(processor.process('a {}').css).toEqual(''); -}); - -it('returns LazyResult object', () => { - let result = (new Processor()).process('a{}'); - expect(result instanceof LazyResult).toBeTruthy(); - expect(result.css).toEqual('a{}'); - expect(result.toString()).toEqual('a{}'); -}); - -it('calls all plugins once', () => { - expect.assertions(1); - - let calls = ''; - let a = () => { - calls += 'a'; - }; - let b = () => { - calls += 'b'; - }; - - let result = new Processor([a, b]).process('', { from: undefined }); - result.css; - result.map; - result.root; - return result.then( () => { - expect(calls).toEqual('ab'); - }); -}); - -it('parses, converts and stringifies CSS', () => { - let a = css => expect(css instanceof Root).toBeTruthy(); - expect(typeof (new Processor([a])).process('a {}').css).toEqual('string'); -}); - -it('send result to plugins', () => { - expect.assertions(4); - let processor = new Processor(); - let a = (css, result) => { - expect(result instanceof Result).toBeTruthy(); - expect(result.processor).toEqual(processor); - expect(result.opts).toEqual({ map: true }); - expect(result.root).toEqual(css); - }; - return processor.use(a).process('a {}', { map: true, from: undefined }); -}); - -it('accepts source map from PostCSS', () => { - 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 } - }); - expect(two.map.toJSON().sources).toEqual(['a.css']); -}); - -it('supports async plugins', () => { - let starts = 0; - let finish = 0; - let async1 = css => { - return new Promise(resolve => { - starts += 1; - setTimeout(() => { - expect(starts).toEqual(1); - - css.append('a {}'); - finish += 1; - resolve(); - }, 1); - }); - }; - let async2 = css => { - return new Promise(resolve => { - expect(starts).toEqual(1); - expect(finish).toEqual(1); - - starts += 1; - setTimeout(() => { - css.append('b {}'); - finish += 1; - resolve(); - }, 1); - }); - }; - return (new Processor([async1, async2])).process('', { from: undefined }) - .then( result => { - expect(starts).toEqual(2); - expect(finish).toEqual(2); - expect(result.css).toEqual('a {}b {}'); - }); -}); - -it('works async without plugins', () => { - return (new Processor()).process('a {}', { from: undefined }) - .then( result => { - expect(result.css).toEqual('a {}'); - }); -}); - -it('runs async plugin only once', () => { - expect.assertions(1); - - let calls = 0; - let async = () => { - return new Promise( resolve => { - setTimeout(() => { - calls += 1; - resolve(); - }, 1); - }); - }; - - let result = (new Processor([async])).process('a {}', { from: undefined }); - result.then( () => { }); - return result.then( () => { - return result.then( () => { - expect(calls).toEqual(1); - }); - }); -}); - -it('supports async errors', done => { - let error = new Error('Async'); - let async = () => { - return new Promise( (resolve, reject) => { - reject(error); - }); - }; - let result = (new Processor([async])).process('', { from: undefined }); - result.then( () => { - done.fail(); - }).catch( err => { - expect(err).toEqual(error); - return result.catch( err2 => { - expect(err2).toEqual(error); - done(); - }); - }); -}); - -it('supports sync errors in async mode', done => { - let error = new Error('Async'); - let async = () => { - throw error; - }; - (new Processor([async])).process('', { from: undefined }).then( () => { - done.fail(); - }).catch( err => { - expect(err).toEqual(error); - done(); - }); -}); - -it('throws parse error in async', () => { - return (new Processor()).process('a{').catch( err => { - expect(err.message).toEqual(':1:1: Unclosed block'); - }); -}); - -it('throws error on sync method to async plugin', () => { - let async = () => { - return new Promise( resolve => resolve() ); - }; - expect(() => { - (new Processor([async])).process('a{}').css; - }).toThrowError(/async/); -}); - -it('throws a sync call in async running', () => { - let async = () => new Promise( done => setTimeout(done, 1) ); - - let processor = (new Processor([async])).process('a{}'); - processor.async(); - - expect(() => { - processor.sync(); - }).toThrowError(/then/); -}); - -it('checks plugin compatibility', () => { - 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; - }; - - console.error = jest.fn(); - - expect(() => { - processBy('1.0.0'); - }).toThrowError('Er'); - expect(console.error.mock.calls.length).toEqual(1); - expect(console.error.mock.calls[0][0]).toEqual( - '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.'); - - expect(() => { - processBy('3.0.0'); - }).toThrowError('Er'); - expect(console.error.mock.calls.length).toEqual(2); - - expect(() => { - processBy('2.0.0'); - }).toThrowError('Er'); - expect(console.error.mock.calls.length).toEqual(3); - - expect(() => { - processBy('2.1.0'); - }).toThrowError('Er'); - expect(console.error.mock.calls.length).toEqual(3); -}); - -it('sets last plugin to result', () => { - let plugin1 = function (css, result) { - expect(result.lastPlugin).toBe(plugin1); - }; - let plugin2 = function (css, result) { - expect(result.lastPlugin).toBe(plugin2); - }; - - let processor = new Processor([plugin1, plugin2]); - return processor.process('a{}', { from: undefined }).then( result => { - expect(result.lastPlugin).toBe(plugin2); - }); -}); - -it('uses custom parsers', () => { - let processor = new Processor([]); - return processor.process('a{}', { parser: prs, from: undefined }) - .then( result => { - expect(result.css).toEqual('ok'); - }); -}); - -it('uses custom parsers from object', () => { - let processor = new Processor([]); - let syntax = { parse: prs, stringify: str }; - return processor.process('a{}', { parser: syntax, from: undefined }) - .then( result => { - expect(result.css).toEqual('ok'); - }); -}); - -it('uses custom stringifier', () => { - let processor = new Processor([]); - return processor.process('a{}', { stringifier: str, from: undefined }) - .then( result => { - expect(result.css).toEqual('!'); - }); -}); - -it('uses custom stringifier from object', () => { - let processor = new Processor([]); - let syntax = { parse: prs, stringify: str }; - return processor.process('', { stringifier: syntax, from: undefined }) - .then( result => { - expect(result.css).toEqual('!'); - }); -}); - -it('uses custom stringifier with source maps', () => { - let processor = new Processor([]); - return processor.process('a{}', { - map: true, stringifier: str, from: undefined - }).then( result => { - expect(result.css).toMatch(/!\n\/\*# sourceMap/); - }); -}); - -it('uses custom syntax', () => { - let processor = new Processor([]); - return processor.process('a{}', { - syntax: { parse: prs, stringify: str }, from: undefined - }).then( result => { - expect(result.css).toEqual('ok!'); - }); -}); - -it('contains PostCSS version', () => { - expect((new Processor()).version).toMatch(/\d+.\d+.\d+/); -}); - -it('throws on syntax as plugin', () => { - let processor = new Processor(); - expect(() => { - processor.use({ - parse() { } - }); - }).toThrowError(/syntax/); -}); - -it('warns about missed from', () => { - console.warn = jest.fn(); - let processor = new Processor(); - - processor.process('a{}').css; - expect(console.warn).not.toBeCalled(); - - return processor.process('a{}').then(() => { - expect(console.warn).toBeCalledWith( - '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.' - ); - }); -}); 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.test.js b/test/result.test.js deleted file mode 100644 index 842757337..000000000 --- a/test/result.test.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const Warning = require('../lib/warning'); -const postcss = require('../lib/postcss'); -const Result = require('../lib/result'); - -it('stringifies', () => { - let result = new Result(); - result.css = 'a{}'; - expect('' + result).toEqual(result.css); -}); - -it('adds warning', () => { - 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(); - - expect(warning).toEqual(new Warning('test', { - plugin: 'test-plugin', - node: result.root.first - })); - - expect(result.messages).toEqual([warning]); -}); - -it('allows to override plugin', () => { - let plugin = postcss.plugin('test-plugin', () => { - return (css, res) => { - res.warn('test', { plugin: 'test-plugin#one' }); - }; - }); - let result = postcss([plugin]).process('a{}').sync(); - - expect(result.messages[0].plugin).toEqual('test-plugin#one'); -}); - -it('allows Root', () => { - let result = new Result(); - let root = postcss.parse('a{}'); - result.warn('TT', { node: root }); - - expect(result.messages[0].toString()).toEqual(':1:1: TT'); -}); - -it('returns only warnings', () => { - let result = new Result(); - result.messages = [ - { type: 'warning', text: 'a' }, - { type: 'custom' }, - { type: 'warning', text: 'b' } - ]; - expect(result.warnings()).toEqual([ - { 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.test.js b/test/root.test.js deleted file mode 100644 index 4889603bd..000000000 --- a/test/root.test.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -const Result = require('../lib/result'); -const parse = require('../lib/parse'); - -it('prepend() fixes spaces on insert before first', () => { - let css = parse('a {} b {}'); - css.prepend({ selector: 'em' }); - expect(css.toString()).toEqual('em {} a {} b {}'); -}); - -it('prepend() fixes spaces on multiple inserts before first', () => { - let css = parse('a {} b {}'); - css.prepend({ selector: 'em' }, { selector: 'strong' }); - expect(css.toString()).toEqual('em {} strong {} a {} b {}'); -}); - -it('prepend() uses default spaces on only first', () => { - let css = parse('a {}'); - css.prepend({ selector: 'em' }); - expect(css.toString()).toEqual('em {}\na {}'); -}); - -it('append() sets new line between rules in multiline files', () => { - let a = parse('a {}\n\na {}\n'); - let b = parse('b {}\n'); - expect(a.append(b).toString()).toEqual('a {}\n\na {}\n\nb {}\n'); -}); - -it('insertAfter() does not use before of first rule', () => { - let css = parse('a{} b{}'); - css.insertAfter(0, { selector: '.a' }); - css.insertAfter(2, { selector: '.b' }); - - expect(css.nodes[1].raws.before).not.toBeDefined(); - expect(css.nodes[3].raws.before).toEqual(' '); - expect(css.toString()).toEqual('a{} .a{} b{} .b{}'); -}); - -it('fixes spaces on removing first rule', () => { - let css = parse('a{}\nb{}\n'); - css.first.remove(); - expect(css.toString()).toEqual('b{}\n'); -}); - -it('keeps spaces on moving root', () => { - let css1 = parse('a{}\nb{}\n'); - - let css2 = parse(''); - css2.append(css1); - expect(css2.toString()).toEqual('a{}\nb{}'); - - let css3 = parse('\n'); - css3.append(css2.nodes); - expect(css3.toString()).toEqual('a{}\nb{}\n'); -}); - -it('generates result with map', () => { - let root = parse('a {}'); - let result = root.toResult({ map: true }); - - expect(result instanceof Result).toBeTruthy(); - expect(result.css).toMatch(/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.test.js b/test/rule.test.js deleted file mode 100644 index 068e2741c..000000000 --- a/test/rule.test.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; - -const parse = require('../lib/parse'); -const Rule = require('../lib/rule'); - -it('initializes with properties', () => { - let rule = new Rule({ selector: 'a' }); - expect(rule.selector).toEqual('a'); -}); - -it('returns array in selectors', () => { - let rule = new Rule({ selector: 'a,b' }); - expect(rule.selectors).toEqual(['a', 'b']); -}); - -it('trims selectors', () => { - let rule = new Rule({ selector: '.a\n, .b , .c' }); - expect(rule.selectors).toEqual(['.a', '.b', '.c']); -}); - -it('is smart about selectors commas', () => { - let rule = new Rule({ - selector: '[foo=\'a, b\'], a:-moz-any(:focus, [href*=\',\'])' - }); - expect(rule.selectors).toEqual([ - '[foo=\'a, b\']', - 'a:-moz-any(:focus, [href*=\',\'])' - ]); -}); - -it('receive array in selectors', () => { - let rule = new Rule({ selector: 'i, b' }); - rule.selectors = ['em', 'strong']; - expect(rule.selector).toEqual('em, strong'); -}); - -it('saves separator in selectors', () => { - let rule = new Rule({ selector: 'i,\nb' }); - rule.selectors = ['em', 'strong']; - expect(rule.selector).toEqual('em,\nstrong'); -}); - -it('uses between to detect separator in selectors', () => { - let rule = new Rule({ selector: 'b', raws: { between: '' } }); - rule.selectors = ['b', 'strong']; - expect(rule.selector).toEqual('b,strong'); -}); - -it('uses space in separator be default in selectors', () => { - let rule = new Rule({ selector: 'b' }); - rule.selectors = ['b', 'strong']; - expect(rule.selector).toEqual('b, strong'); -}); - -it('selectors works in constructor', () => { - let rule = new Rule({ selectors: ['a', 'b'] }); - expect(rule.selector).toEqual('a, b'); -}); - -it('inserts default spaces', () => { - let rule = new Rule({ selector: 'a' }); - expect(rule.toString()).toEqual('a {}'); - rule.append({ prop: 'color', value: 'black' }); - expect(rule.toString()).toEqual('a {\n color: black\n}'); -}); - -it('clones spaces from another rule', () => { - let root = parse('b{\n }'); - let rule = new Rule({ selector: 'em' }); - root.append(rule); - expect(root.toString()).toEqual('b{\n }\nem{\n }'); -}); - -it('uses different spaces for empty rules', () => { - let root = parse('a{}\nb{\n a:1\n}'); - let rule = new Rule({ selector: 'em' }); - root.append(rule); - expect(root.toString()).toEqual('a{}\nb{\n a:1\n}\nem{}'); - - rule.append({ prop: 'top', value: '0' }); - expect(root.toString()).toEqual('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.test.js b/test/stringifier.test.js old mode 100644 new mode 100755 index 47c0b7b94..f845650e2 --- a/test/stringifier.test.js +++ b/test/stringifier.test.js @@ -1,175 +1,480 @@ -'use strict'; - -const Stringifier = require('../lib/stringifier'); -const Declaration = require('../lib/declaration'); -const AtRule = require('../lib/at-rule'); -const parse = require('../lib/parse'); -const Node = require('../lib/node'); -const Root = require('../lib/root'); -const Rule = require('../lib/rule'); - -let str; -beforeAll(() => { - str = new Stringifier(); -}); - -it('creates trimmed/raw property', () => { - let b = new Node({ one: 'trim' }); - b.raws.one = { value: 'trim', raw: 'raw' }; - expect(str.rawValue(b, 'one')).toEqual('raw'); - - b.one = 'trim1'; - expect(str.rawValue(b, 'one')).toEqual('trim1'); -}); - -it('works without rawValue magic', () => { - let b = new Node(); - b.one = '1'; - expect(b.one).toEqual('1'); - expect(str.rawValue(b, 'one')).toEqual('1'); -}); - -it('uses node raw', () => { - let rule = new Rule({ selector: 'a', raws: { between: '\n' } }); - expect(str.raw(rule, 'between', 'beforeOpen')).toEqual('\n'); -}); - -it('hacks before for nodes without parent', () => { - let rule = new Rule({ selector: 'a' }); - expect(str.raw(rule, 'before')).toEqual(''); -}); - -it('hacks before for first node', () => { - let root = new Root(); - root.append(new Rule({ selector: 'a' })); - expect(str.raw(root.first, 'before')).toEqual(''); -}); - -it('hacks before for first decl', () => { - let decl = new Declaration({ prop: 'color', value: 'black' }); - expect(str.raw(decl, 'before')).toEqual(''); - - let rule = new Rule({ selector: 'a' }); - rule.append(decl); - expect(str.raw(decl, 'before')).toEqual('\n '); -}); - -it('detects after raw', () => { - let root = new Root(); - root.append({ selector: 'a', raws: { after: ' ' } }); - root.first.append({ prop: 'color', value: 'black' }); - root.append({ selector: 'a' }); - expect(str.raw(root.last, 'after')).toEqual(' '); -}); - -it('uses defaults without parent', () => { - let rule = new Rule({ selector: 'a' }); - expect(str.raw(rule, 'between', 'beforeOpen')).toEqual(' '); -}); - -it('uses defaults for unique node', () => { - let root = new Root(); - root.append(new Rule({ selector: 'a' })); - expect(str.raw(root.first, 'between', 'beforeOpen')).toEqual(' '); -}); - -it('clones raw from first node', () => { - let root = new Root(); - root.append( new Rule({ selector: 'a', raws: { between: '' } }) ); - root.append( new Rule({ selector: 'b' }) ); - - expect(str.raw(root.last, 'between', 'beforeOpen')).toEqual(''); -}); - -it('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' }); - - expect(root.toString()).toEqual('@page {\n' + - ' a {\n' + - ' color: black\n' + - ' }\n' + - '}'); -}); - -it('clones style', () => { - let compress = parse('@page{ a{ } }'); - let spaces = parse('@page {\n a {\n }\n}'); - - compress.first.first.append({ prop: 'color', value: 'black' }); - expect(compress.toString()).toEqual('@page{ a{ color: black } }'); - - spaces.first.first.append({ prop: 'color', value: 'black' }); - expect(spaces.toString()) - .toEqual('@page {\n a {\n color: black\n }\n}'); -}); - -it('clones indent', () => { - let root = parse('a{\n}'); - root.first.append({ text: 'a' }); - root.first.append({ text: 'b', raws: { before: '\n\n ' } }); - expect(root.toString()).toEqual('a{\n\n /* a */\n\n /* b */\n}'); -}); - -it('clones declaration before for comment', () => { - let root = parse('a{\n}'); - root.first.append({ text: 'a' }); - root.first.append({ - prop: 'a', - value: '1', - raws: { before: '\n\n ' } - }); - expect(root.toString()).toEqual('a{\n\n /* a */\n\n a: 1\n}'); -}); - -it('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' }); - expect(css.last.first.raw('before')).toEqual('\n '); -}); - -it('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' }); - - expect(css.last.raw('before')).toEqual('\n\n'); - expect(css.last.first.raw('before')).toEqual('\n '); -}); - -it('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' }); - - expect(css.first.last.raw('before')).toEqual('\n\n '); - expect(css.first.last.raw('after')).toEqual(''); -}); - -it('clones semicolon only from rules with children', () => { - let css = parse('a{}b{one:1;}'); - expect(str.raw(css.first, 'semicolon')).toBeTruthy(); -}); - -it('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' }); - expect(css.toString()).toEqual('a{*one:1;two:2}\n@keyframes a{\nfrom{}}'); -}); - -it('clones only spaces in between', () => { - let css = parse('a{one/**/:1}'); - css.first.append({ prop: 'two', value: '2' }); - expect(css.toString()).toEqual('a{one/**/:1;two:2}'); -}); - -it('uses optional raws.indent', () => { - let rule = new Rule({ selector: 'a', raws: { indent: ' ' } }); - rule.append({ prop: 'color', value: 'black' }); - expect(rule.toString()).toEqual('a {\n color: black\n}'); -}); +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: '