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 new file mode 100644 index 000000000..c6c8b3621 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true 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 d2dc35e82..3030054ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,6 @@ -.DS_Store -*~ - node_modules/ -.bundle/ - -benchmark/results/ -benchmark/cache/ -build/ -origin.css -fail.css +coverage/ -postcss.js +docs/api/index.html +docs/api/assets/ diff --git a/.npmignore b/.npmignore index 21e306af4..9439f8b9d 100644 --- a/.npmignore +++ b/.npmignore @@ -1,16 +1,7 @@ -.gitignore - -node_modules/ - -benchmark/ -build/ -tasks/ - +coverage/ test/ -.travis.yml -origin.css -fail.css -postcss.js -gulpfile.js -logo.svg +docs/ +tsconfig.json +eslint.config.mjs +pnpm-workspace.yaml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ffb8b3996..000000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -sudo: false -language: node_js -node_js: - - iojs - - "0.12" - - "0.10" diff --git a/API.md b/API.md deleted file mode 100644 index 57a9644d2..000000000 --- a/API.md +++ /dev/null @@ -1,1296 +0,0 @@ -# PostCSS API - -* [postcss function](#postcss-function) -* [PostCSS class](#postcss-class) -* [Result class](#result-class) -* [Vendor module](#vendor-module) -* [List module](#list-module) -* [Input class](#input-class) -* [Nodes common methods](#nodes-common-methods) -* [Containers common methods](#containers-common-methods) -* [Root node](#root-node) -* [AtRule node](#atrule-node) -* [Rule node](#rule-node) -* [Declaration node](#declaration-node) -* [Comment node](#comment-node) - -## `postcss` function - -The `postcss` function is the main entry point for PostCSS. - -```js -var postcss = require('postcss'); -``` - -### `postcss(plugins)` - -Returns a new `PostCSS` instance that will apply `plugins` -as CSS processors. - -```js -postcss([autoprefixer, cssnext, cssgrace]).process(css).css; -``` - -You can also set plugins with the [`PostCSS#use`] method. - -See [`PostCSS#use`] below for details about plugin formats. - -### `postcss.parse(css, opts)` - -Parses source `css` and returns a new `Root` node, which contains -the source CSS nodes. - -```js -// Simple CSS concatenation with source map support -var root1 = postcss.parse(css1, { from: file1 }); -var root2 = postcss.parse(css2, { from: file2 }); -root1.append(root2).toResult().css; -``` - -Options: - -* `from`: the path to the source CSS file. You should always set `from`, - because it is used in map generation and in syntax error messages. -* `safe`: enable [Safe Mode], in which PostCSS will try - to fix CSS syntax errors. -* `map`: an object of [source map options]. - Only `map.prev` is used in `parse`. - -### `postcss.root(props)` - -Creates a new [`Root` node]. - -```js -postcss.root({ after: '\n' }).toString() //=> "\n" -``` - -### `postcss.atRule(props)` - -Creates a new [`AtRule` node]. - -```js -postcss.atRule({ name: 'charset' }).toString() //=> "@charset" -``` - -### `postcss.rule(props)` - -Creates a new [`Rule` node]. - -```js -postcss.rule({ selector: 'a' }).toString() //=> "a {\n}" -``` - -### `postcss.decl(props)` - -Creates a new [`Declaration` node]. - -```js -postcss.decl({ prop: 'color', value: 'black' }).toString() //=> "color: black" -``` - -### `postcss.comment(props)` - -Creates a new [`Comment` node]. - -```js -postcss.comment({ text: 'test' }).toString() //=> "/* test */" -``` - -## `PostCSS` class - -A `PostCSS` instance contains plugins to process CSS. You can create -one `PostCSS` instance, initialize its plugins, and then use that instance -on many CSS files. - -```js -var processor = postcss([autoprefixer, cssnext, cssgrace]); -processor.process(css1).css; -processor.process(css2).css; -``` - -### `p.use(plugin)` - -Adds a plugin to be used as a CSS processor. - -```js -var processor = postcss(); -processor.use(autoprefixer).use(cssnext).use(cssgrace); -``` - -Plugins can also be added by passing them as arguments when creating -a `postcss` instance (cf. [`postcss(plugins)`]). - -Plugins can come in three formats: - -1. A function. PostCSS will pass the function a [`Root` node] - as the first argument. -2. An object with a `postcss` method. PostCSS will use that method - as described in #1. -3. Another `PostCSS` instance. PostCSS will copy plugins - from that instance to this one. - -Plugin functions should mutate the passed `Root` node and return nothing, -or return a new `Root` node. - -```js -processor.use(function (css) { - css.prepend({ name: 'charset', params: '"UTF-8"' }); -}); -processor.use(function (css) { - return postcss.root(); -}); -``` - -### `p.process(css, opts)` - -This is the main method of PostCSS. It will parse the source CSS -and create a [`Root` node]; send this `Root` to each plugin successively, -for transformations; and then return a `Result` instance created -from the transformed `Root`. - -```js -var result = processor.process(css, { from: 'a.css', to: 'a.out.css' }); -``` - -Input CSS formats are: - -* A string of CSS. -* A `Result` instance from another PostCSS processor. PostCSS will accept - the already parsed `Root` from it. -* Any object with a `toString()` method — for example, a file stream. - -Options: - -* `from`: the path of the CSS source file. You should always set `from`, - because it is used in source map generation and syntax error messages. -* `to`: the path where you’ll put the output CSS file. You should always set - `to` to generate correct source maps. -* `safe`: enable [Safe Mode], in which PostCSS will try - to fix CSS syntax errors. -* `map`: an object of [source map options]. - -## `Result` class - -Provides result of PostCSS transformations. - -A `Result` instance is returned -by [`PostCSS#process(css, opts)`] and [`Root#toResult(opts)`]. - -```js -var result1 = postcss().process(css); -var result2 = postcss.parse(css).toResult(); -``` - -### `result.root` - -The source `Root` instance. - -```js -root.toResult().root == root; -``` - -### `result.opts` - -Options from the [`PostCSS#process(css, opts)`] or -[`Root#toResult(opts)`] call that produced -this `Result` instance. - -```js -postcss().process(css, opts).opts == opts; -``` - -### `result.css` - -A CSS string representing this `Result`’s '`Root` instance. - -```js -postcss().process('a{}').css //=> "a{}" -``` - -This property is generated *lazily*: `Root` is not stringified until -the first request for the `css` property (or the [`result.map`] property). -That initial request for `css` will also generate a source map. -Source map will inlined into CSS or assigned to the [`result.map`] property, -if user ask to save map to separated file. - -### `result.map` - -An instance of the `SourceMapGenerator` class from the [`source-map`] library, -representing changes to the `Result`’s `Root` instance. - -```js -result.map.toJSON() //=> { version: 3, file: 'a.css', sources: ['a.css'], … } -``` - -This property is generated *lazily*: the source map for `Root` is not generated -until the first request for the `map` property (or the [`result.css`] property). -That initial request will also stringify `Root` and assign the generated -CSS string to the [`result.css`] property. - -Additionally, *this property will receive a value only if the user does not wan -an inline source map*. By default, PostCSS generates inline source maps, -written directly into the processed CSS; so by default the `map` property -will be empty. - -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. - -```js -if ( result.map ) { - fs.writeFileSync(to + '.map', result.map.toString()); -} -``` - -## Vendor module - -Contains helpers for working with vendor prefixes. - -### `vendor.prefix(string)` - -Returns the vendor prefix extracted from an input string. - -```js -vendor.prefix('-moz-tab-size') //=> '-moz-' -``` - -### `vendor.unprefixed(string)` - -Returns the input string stripped of its vendor prefix. - -```js -vendor.unprefixed('-moz-tab-size') //=> 'tab-size' -``` - -## List module - -Contains helpers for safely splitting lists of CSS values, preserving parentheses -and quotes. - -```js -var list = require('postcss/lib/list'); -``` - -### `list.space(string)` - -Safely splits space-separated values (such as those for `background`, -`border-radius`, and other shorthand properties). - -```js -list.space('1px calc(10% + 1px)') -//=> ['1px', 'calc(10% + 1px)'] -``` - -### `list.comma(string)` - -Safely splits comma-separated values (such as those -for `transition-*` and `background` properties). - -```js -list.comma('black, linear-gradient(white, black)') -//=> ['black', 'linear-gradient(white, black)'] -``` - -## `Input` class - -Represents the source CSS. - -```js -var root = postcss.parse(css, { from: file }); -var input = root.source.input; -``` - -### `input.file` - -The absolute path to the CSS source file defined with -the [`from` option](#pprocesscss-opts). - -```js -var root = postcss.parse(css, { from: 'a.css' }); -root.source.input.file //=> '/home/ai/a.css' -``` - -### `input.id` - -The unique ID of the CSS source. This is used if the user did not enter a `from` -options so PostCSS does not know about a file path. - -```js -var root = postcss.parse(css); -root.source.input.file //=> undefined -root.source.input.id //=> -``` - -### `input.from` - -The CSS source identifier. Contains [`input.file`](#inputfile) if the user set the -[`from` option](#pprocesscss-opts), or [`input.id`](#inputid) if she did not. - -```js -var root = postcss.parse(css, { from: 'a.css' }); -root.source.input.from //=> '/home/ai/a.css' - -var root = postcss.parse(css); -root.source.input.from //=> -``` - -### `input.map` - -Represents the input source map passed from a compilation step before PostCSS -(for example, from the Sass compiler). - -`map.consumer()` returns an instance of the `SourceMapConsumer` class -from the [`source-map`] library. - -```js -root.source.input.map.consumer().sources //=> ['a.sass'] -``` - -### `input.origin(line, column)` - -Reads the input source map and returns a symbol position in the input source -(for example, in a Sass file that was compiled to CSS before being passed -to PostCSS): - -```js -root.source.input.origin(1, 1) //=> { source: 'a.css', line: 3, column: 1 } -``` - -## Nodes: common methods - -All node classes have many common methods. - -### `node.type` - -Returns a string representing the node’s type. - -Possible values are `root`, `atrule`, `rule`, `decl`, or `comment`. - -```js -postcss.decl({ prop: 'color', value: 'black' }).type //=> 'decl' -``` - -### `node.parent` - -Returns the node’s parent node. - -```js -root.nodes[0].parent == root; -``` - -### `node.source` - -Returns the input source of the node, with the following properties: - -- `node.source.input`: An [`Input`] instance. -- `node.source.start`: The starting position of the node’s source — - line and column. -- `node.source.end`: The ending position of the node’s source — line and column. - -```js -decl.source.input.from //=> '/home/ai/a.sass' -decl.source.start //=> { line: 10, column: 2 } -decl.source.end //=> { line: 10, column: 12 } -``` - -The property is used in source map generation. - -If you create a node manually (for example, with `postcss.decl()`), -that node will not have a `source` property and will be absent -from the source map. For this reason, 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 -var prefixed = postcss.decl({ prop: '-moz-' + decl.prop, value: decl.value }); - -// Good -var prefixed = decl.clone({ prop: '-moz-' + decl.prop }); -``` - -```js -if ( atrule.name == 'add-link' ) { - var rule = postcss.rule({ selector: 'a' }); // Rule has no source - atrule.parent.insertBefore(atrule, rule); // We add it because of atrule - rule.source = atrule.source; // So we copy source from atrule -} -``` - -### `node.toString()` - -Returns a CSS string representing the node. - -```js -postcss.rule({ selector: 'a' }).toString() //=> 'a {}'' -``` - -### `node.error(message)` - -Returns a `CssSyntaxError` instance that presents the original position -of the node in the source, showing line and column numbers and also -a small excerpt to facilitate debugging. - -It will use an input source map, if present, to get the original position -of the source, even from a previous compilation step -(for example, from Sass compilation). - -This method produces very useful error messages. - -```js -if ( !variables[name] ) { - throw decl.error('Unknown variable ' + name); - // CssSyntaxError: a.sass:4:3: Unknown variable $black - // a - // color: $black - // ^ - // background: white -} - -if ( oldSyntax.check(decl) ) { - console.warn( decl.error('Old syntax for variables').message ); - // a.sass:4:3: Old syntax for variables -} -``` - -### `node.next()` and `node.prev()` - -Returns the next/previous child of the node’s parent; or returns `undefined` -if the current node is the last/first child. - -```js -var annotation = decl.prev(); -if ( annotation.type == 'comment' ) { - readAnnotation( annotation.text ); -} -``` - -### `node.root()` - -Returns the `Root` instance of the node’s tree. - -```js -root.nodes[0].nodes[0].root() == root -``` - -### `node.removeSelf()` - -Removes the node from its parent, and cleans the `parent` property in the node -and its children. - -```js -if ( decl.prop.match(/^-webkit-/) ) { - decl.removeSelf(); -} -``` - -### `node.replaceWith(otherNode)` - -Inserts another node before the current node, and removes the current node. - -```js -if ( atrule.name == 'mixin' ) { - atrule.replaceWith(mixinRules[atrule.params]); -} -``` - -### `node.clone(props)` - -Returns a clones of the node. - -The resultant clone node and its (clone) children will have clean `parent` -and code style properties. You can override properties in the clone node -by passing a `props` argument. - -```js -var clonded = decl.clone({ prop: '-moz-' + decl.prop }); -cloned.before //=> undefined -cloned.parent //=> undefined -cloned.toString() //=> -moz-transform: scale(0) -``` - -### `node.cloneBefore(props)` and `node.cloneAfter(props)` - -Shortcuts to clone the node and insert the resultant clone node before/after -the current node. - -```js -decl.cloneBefore({ prop: '-moz-' + decl.prop }); -``` - -### `node.moveTo(newParent)` - -Removes the node from its current parent and inserts it -at the end of `newParent`. - -This will clean the `before` and `after` code style properties from the node, -and replace them with the indentation style of `newParent`. It will also clean -the `between` property if `newParent` is in another `Root`. - -```js -atrule.moveTo(atrule.parent.parent); -``` - -### `node.moveBefore(otherNode)` and `node.moveAfter(otherNode)` - -Removes the node from its current parent and inserts it into a new parent -before/after `otherNode`. - -This will also clean the node’s code style properties just -as `node.moveTo(newParent)` does. - -### `node.style(prop, defaultType)` - -Returns a code style property 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 -var root = postcss.parse('a { background: white }'); -root.nodes[0].append({ prop: 'color', value: 'black' }); -root.nodes[0].nodes[1].style('before') //=> ' ' -``` - -## Containers: common methods - -The `Root`, `AtRule`, and `Rule` container nodes have some common methods -to help work with their children. - -Note that all containers can store *any* content. If you write a rule inside -a rule, PostCSS will parse it. - -### `container.nodes` - -An array containing the container’s children. - -```js -var root = postcss.parse('a { color: black }'); -root.nodes.length //=> 1 -root.nodes[0].selector //=> 'a' -root.nodes[0].nodes[0].prop //=> 'color' -``` - -### `container.first` - -The container’s first child. - -```js -rule.first == rules.nodes[0]; -``` - -### `container.last` - -The container’s last child. - -```js -rule.last == rule.nodes[rule.nodes.length - 1]; -``` - -### `container.index(child)` - -Returns `child`’s index within the container’s `nodes` array. - -```js -rule.index( rule.nodes[2] ) //=> 2 -``` - -### `container.every(callback)` - -Returns `true` if `callback` returns a truthy value for all -of the container’s children. - -```js -var noPrefixes = rule.every(function (decl) { - return decl.prop[0] != '-'; -}); -``` - -### `container.some(callback)` - -Return `true` if `callback` returns a truthy value -for (at least) one of the container’s children. - -```js -var hasPrefix = rule.some(function (decl) { - return decl.prop[0] == '-'; -}); -``` - -### `container.each(callback)` - -Iterates through the container’s immediate children, calling `callback` -for each child. - -`callback` receives 2 arguments: the node itself and an index. - -Returning `false` within `callback` will break iteration. - -```js -var color; -rule.each(function (decl) { - if ( decl.prop == 'color' ) { - color = decl.value; - return false; - } -}); -``` - -Unlike the `for {}`-cycle or `Array#forEach()` this iterator is safe -if you are mutating the array of child nodes during iteration. -PostCSS will adjust the current index to match the mutations. - -```js -var root = postcss.parse('a { color: black; z-index: 1 }'); -var rule = root.first; - -for ( var i = 0; i < rule.nodes.length; i++ ) { - var decl = rule.nodes[i]; - decl.cloneBefore({ prop: '-webkit-' + decl.prop }); - // Cycle will be infinite, because cloneBefore move current node - // to next index -} - -rule.each(function (decl) { - decl.cloneBefore({ prop: '-webkit-' + decl.prop }); - // Will be executed only for color and z-index -}); -``` - -`container.each()` only iterates through the container’s immediate children. -If you need to recursively iterate through all the container’s descendents, -use `container.eachInside()`. - -### `container.eachInside(callback)` - -Recursively iterates through the container’s children, -those children’s children, etc., calling `callback` for each. - -`callback` receives 2 arguments: the node itself and an index. - -```js -root.eachInside(function (node) { - // Will be iterate through all nodes -}); -``` - -Like `container.each()`, this method is safe to use -if you are mutating arrays during iteration. - -If you only need to iterate through the container’s immediate children, -use `container.each()`. - -### `container.eachDecl([propFilter,] callback)` - -Recursively iterates through all declaration nodes within the container, -calling `callback` for each. - -`callback` receives 2 arguments: the node itself and an index. - -```js -root.eachDecl(function (decl) { - if ( decl.prop.match(/^-webkit-/) ) { - decl.removeSelf(); - } -}); -``` - -If you pass a string or regular expression as `filter`, only those declarations whose -property matches`filter` will be iterated over. - -```js -// Make flat design -root.eachDecl('border-radius', function (decl) { - decl.removeSelf(); -}); -root.eachDecl(/^background/, function (decl) { - decl.value = takeFirstColorFromGradient(decl.value); -}); -``` - -Like `container.each()`, this method is safe to use if you are mutating -arrays during iteration. - -### `container.eachAtRule([nameFilter,] callback)` - -Recursively iterates through all at-rule nodes within the container, -calling `callback` for each. - -`callback` receives 2 arguments: the node itself and an index. - - -```js -root.eachAtRule(function (rule) { - if ( rule.name.match(/^-webkit-/) ) rule.removeSelf(); -}); -``` - -If you pass a string or regular expression as `filter`, only those at-rules whose name -matches `filter` will be iterated over. - -```js -var first = false; -root.eachAtRule('charset', function (rule) { - if ( !first ) { - first = true; - } else { - rule.removeSelf(); - } -}); -``` - -Like `container.each()`, this method is safe to use if you are mutating arrays -during iteration. - -### `container.eachRule(callback)` - -Recursively iterates through all rule nodes within the container, calling -`callback` for each. - -`callback` receives 2 arguments: the node itself and an index. - -```js -var selectors = []; -root.eachRule(function (rule) { - selectors.push(rule.selector); -}); -console.log('You CSS uses ' + selectors.length + ' selectors'); -``` - -Like `container.each()`, this method is safe to use if you are mutating arrays -during iteration. - -### `container.eachComment(callback)` - -Recursively iterates through all comment nodes within the container, calling -`callback` for each. - -```js -root.eachComment(function (comment) { - comment.removeSelf(); -}) -``` - -Like `container.each()`, this method is safe to use if you are mutating arrays -during iteration. - -### `container.replaceValues(regexp, opts, callback)` - -Passes all declaration values within the container that match `regexp` through -`callback`, replacing those values with the returned result of `callback`. - -`callback` will receive the same arguments as those passed to a function -parameter of [`String#replace`]. - -You can speed up the search by passing `opts`: - -- `props`: An array of property names. The method will only search for values - that match `regexp` within declarations of listed properties. -- `fast`: A string that will be used to narrow down values and speed up - the regexp search. Searching every single value with a regexp can be slow; - so if you pass a `fast` string, PostCSS will first check whether the value - contains the `fast` string; and only if it does will PostCSS check that value - against `regexp`. For example, instead of just checking for `/\d+rem/` on - all values, you can set `fast: 'rem'` to first check whether a value has - the `rem` unit, and only if it does perform the regexp check. - -This method is useful if you are using a custom unit or function, -so need to iterate through all values. - -```js -root.replaceValues(/\d+rem/, { fast: 'rem' }, function (string) { - return 15 * parseInt(string) + 'px'; -}); -``` - -[`String#replace`]: (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_function_as_a_parameter) - -### `container.prepend(node)` and `container.append(node)` - -Insert a new node to the start/end of the container. - -```js -var decl = postcss.decl({ prop: 'color', value: 'black' }); -rule.append(decl); -``` - -Because each node class is identifiable by unique properties, you can use -the following shortcuts to create nodes to prepend/append: - -```js -root.append({ name: '@charset', params: '"UTF-8"' }); // at-rule -root.append({ selector: 'a' }); // rule -rule.append({ prop: 'color', value: 'black' }); // declaration -rule.append({ text: 'Comment' }) // comment -``` - -### `container.insertBefore(oldNode, newNew)` and `container.insertAftr(oldNode, newNew)` - -Insert `newNode` before/after `oldNode` within the container. - -`oldNode` can be a node or a node’s index. - -```js -rule.insertBefore(decl, decl.clone({ prop: '-webkit-' + decl.prop })); -``` - -You can also use the same shorcuts available to `container.append()`. - -```js -rule.insertBefore(decl, { prop: 'color', value: 'black' }); -``` - -### `container.remove(node)` - -Removes `node` from the container, and the `parent` properties of `node` -and its children. - -`node` can be a node or a node’s index. - -```js -rule.nodes.length //=> 5 -rule.remove(decl); -rule.nodes.length //=> 4 -decl.parent //=> undefined -``` - -### `container.removeAll()` - -Removes all children from the container, and cleans their `parent` properties. - -```js -rule.removeAll(); -rule.nodes.length //=> 0 -``` - -## `Root` node - -Represents a CSS file and contains all its parsed nodes. - -```js -var root = postcss.parse('a{color:black} b{z-index:2}'); -root.type //=> 'root' -root.nodes.length //=> 2 -``` - -### `root.toResult(opts)` - -Returns a [`Result`] instance representing the root's CSS. - -```js -var root1 = postcss.parse(css1, { from: 'a.css' }); -var root2 = postcss.parse(css2, { from: 'b.css' }); - -root1.append(root2); -var result = root1.toResult({ to: 'all.css', map: true }); -``` - -Options: - -* `to`: the path where you’ll put the output CSS file. You should always set - `to` to generate correct source maps. -* `map`: an object of [source map options]. - -### `root.after` - -The space symbols after the last child of `root`, -such as `\n` at the end of a file. - -```js -var root = parse('a {}\nb { color: black }\n'); -root.after //=> '\n' -``` - -This is a code style property. - -## `AtRule` node - -Represents an at-rule. - -This node will have a `nodes` property, representing its children, -if it is followed in the CSS by a `{}` block. - - -```js -var root = postcss.parse('@charset "UTF-8"; @media print {}'); - -var charset = root.first; -charset.type //=> 'atrule' -charset.nodes //=> undefined - -var media = root.last; -media.nodes //=> [] -``` - -### `atrule.name` - -The at-rule’s name. This is the identifier that immediately follows the `@`. - -```js -var root = postcss.parse('@media print {}'); -var media = root.first; -media.name //=> 'media' -``` - -### `atrule.params` - -The at-rule’s parameters. These are the values that follow the at-rule’s name -but precede any `{}` block. The spec refers to this area -as the at-rule’s “prelude”. - -```js -var root = postcss.parse('@media print, screen {}'); -var media = root.first; -media.params //=> '[print, screen]' -``` - -This value will be cleaned of comments. If the source at-rule’s prelude -contained comments, those comments will be available -in the `_params.raw` property. - -If you have not changed the parameters, calling `atrule.toString()` -will stringify the original raw value (comments and all). - -```js -var root = postcss.parse('@media print, /**/ screen {}'); -var media = root.first; -media.params //=> '[print, screen]' -media._params.raw //=> 'print, /**/ screen' -media.toString() //=> '@media print, /**/ screen {}' -``` - -### `atrule.before` - -The space symbols before the at-rule. - -The default value is `\n`, except for the first rule in a `Root`, -whose `before` property is empty. - -```js -var root = postcss.parse('@charset "UTF-8";\n@media print {}\n'); -var media = root.last; -media.before //=> '\n' -``` - -This is a code style property. - -### `atrule.afterName` - -The space symbols between the at-rule’s name and its parameters. - -The default value is ` `. - -```js -var root = postcss.parse('@media\n print,\n screen {}\n'); -var media = root.first; -media.afterName //=> '\n ' -``` - -This is a code style property. - -### `atrule.between` - -The space symbols between the at-rule’s parameters -and `{`, the block-opening curly brace. - -The default value is ` `. - -```js -var root = postcss.parse('@media print, screen\n{}\n'); -var media = root.first; -media.before //=> '\n' -``` - -This is a code style property. - -### `atrule.after` - -The space symbols between the at-rule’s last child and `}`, -the block-closing curly brace. - -The default value is `\n` if the at-rule has children, -and an empty string (`''`) if it does not. - -```js -var root = postcss.parse('@media print {\n a {}\n }\n'); -var media = root.first; -media.after //=> '\n ' -``` - -This is a code style property. - -### `atrule.semicolon` - -`true` if at-rule’s last child declaration -is followed by an (optional) semicolon. - -`undefined` if the semicolon is omitted. - -```js -postcss.parse('@page{color:black}').first.semicolon //=> undefined -postcss.parse('@page{color:black;}').first.semicolon //=> true -``` - -This is a code style property. - -## `Rule` node - -Represents a CSS rule: a selector followed by a declaration block. - -```js -var root = postcss.parse('a{}'); -var rule = root.first; -rule.type //=> 'rule' -rule.toString() //=> 'a{}' -``` - -### `rule.selector` - -The rule’s full selector represented as a string. If there are multiple -comma-separated selectors, the entire group will be included. - -```js -var root = postcss.parse('a, b { }'); -var rule = root.first; -rule.selector //=> 'a, b' -``` - -This value will be cleaned of comments. If the source selector contained -comments, those comments will be available in the `_selector.raw` property. - -If you have not changed the selector, the result of `rule.toString()` -will include the original raw selector value (comments and all). - -```js -var root = postcss.parse('a /**/ b {}'); -var rule = root.first; -rule.selector //=> 'a b' -rule._selector.raw //=> 'a /**/ b' -rule.toString() //=> 'a /**/ b {}' -``` - -### `rule.selectors` - -An array containing the rule’s individual selectors. -Groups of selectors are split at commas. - -```js -var root = postcss.parse('a, b { }'); -var rule = root.first; - -rule.selector //=> 'a, b' -rule.selectors //=> ['a', 'b'] - -rule.selectors = ['a', 'strong']; -rule.selector //=> 'a, strong' -``` - -### `rule.before` - -The space symbols before the rule. - -The default value is `\n`, except for first rule in root, -whose `before` property is empty. - -```js -var root = postcss.parse('a {}\nb {}\n'); -var rule = root.last; -rule.before //=> '\n' -``` - -This is a code style property. - -### `rule.after` - -The space symbols between the rule’s last child and `}`, -the block-closing curly brace. - -The default value is `\n` if rule has children and an empty string (`''`) -if it does not. - -```js -var root = postcss.parse('@a {\n color: black\n }\n'); -var rule = root.first; -root.after //=> '\n ' -``` - -This is a code style property. - -### `rule.semicolon` - -`true` if rule’s last child declaration is followed by an (optional) semicolon. - -`undefined` if the semicolon is omitted. - -```js -postcss.parse('a{color:black}').first.semicolon //=> undefined -postcss.parse('a{color:black;}').first.semicolon //=> true -``` - -This is a code style property. - -## `Declaration` node - -Represents a CSS declaration. - -```js -var root = postcss.parse('a { color: black }'); -var decl = root.first.first; -decl.type //=> 'decl' -decl.toString() //=> ' color: black' -``` - -### `declaration.prop` - -The declaration’s property name. - -```js -var root = postcss.parse('a { color: black }'); -var decl = root.first.first; -decl.prop //=> 'color' -``` - -### `declaration.value` - -The declaration’s value. - -```js -var root = postcss.parse('a { color: black }'); -var decl = root.first.first; -decl.value //=> 'black' -``` - -This value will be cleaned of comments. If the source value contained comments, -those comments will be available in the `_value.raw` property. - -If you have not changed the value, the result of `decl.toString()` will include -the original raw value (comments and all). - -```js -var root = postcss.parse('a { border-radius: 3px /**/ 0 }'); -var decl = root.first.first; -decl.value //=> '3px 0' -decl._value.raw //=> '3px /**/ 0' -decl.toString() //=> ' border-radius: 3px /**/ 0' -``` - -### `declaration.before` - -The space symbols before the declaration. - -Default value is `\n `. - -```js -var root = postcss.parse('a {\n color: black\n}\n'); -var decl = root.first.first; -decl.before //=> '\n ' -``` - -This is a code style property. - -### `declaration.between` - -The symbols between the declaration’s property and its value. - -Default value is `: `. - -```js -var root = postcss.parse('a { color/**/: black }'); -var decl = root.first.first; -decl.between //=> '/**/: ' -``` - -This is a code style property. - -### `declaration.important` - -`true` if the declaration has an `!important` annotation. - -```js -var root = postcss.parse('a { color: black !important; color: white }'); -root.first.first.important //=> true -root.first.last.important //=> undefined -``` - -If there are comments between the declaration’s value and its -`!important` annotation, they will be available in the `_important` property. - -```js -var root = postcss.parse('a { color: black /**/ !important }'); -root.first.first._important //=> ' /**/ !important' -``` - -## `Comment` node - -Represents a comment between declarations or statements (rule and at-rules). -Comments inside selectors, at-rules parameters, or declaration values -will be stored in the raw properties explained above. - -```js -var root = postcss.parse('a { color: /* inner */ black; /* outer */ }'); -var decl = root.first.first; -var comment = root.first.last; - -comment.type //=> 'comment' -decl.between //=> ': /* inner */' -``` - -### `comment.text` - -The comment’s text. - -```js -var root = postcss.parse('/* Empty file */'); -var comment = root.first; -var comment.text //=> 'Empty file' -``` - -### `comment.left` and `comment.right` - -The space symbols before/after the comment’s text. - -Default value is ` `. - -```js -var root = postcss.parse('/* long */ /*short*/'); -var long = root.first; -var short = root.last; - -long.left //=> ' ' -short.left //=> '' -``` - -This is a code style property. - -### `comment.before` - -The space symbols before the comment. - -Default value is `\n`. - -```js -var root = postcss.parse('a {\n /**/}\n'); -var comment = root.first.first; -comment.before //=> '\n ' -``` - -This is a code style property. - -[`source-map`]: https://github.com/mozilla/source-map - -[source map options]: https://github.com/postcss/postcss#source-map -[Safe Mode]: https://github.com/postcss/postcss#safe-mode - -[`PostCSS#process(css, opts)`]: #pprocesscss-opts -[`Root#toResult(opts)`]: #roottoresult-opts -[`postcss(plugins)`]: #postcssplugins -[`Declaration` node]: #declaration-node -[`Comment` node]: #comment-node -[`PostCSS#use`]: #puseplugin -[`AtRule` node]: #atrule-node -[`result.map`]: #resultmap -[`result.css`]: #resultcss -[`Root` node]: #root-node -[`Rule` node]: #rule-node -[`Input`]: #inputclass -[`Result`]: #result-class diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..45510c7e5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1441 @@ +# Change Log + +This project adheres to [Semantic Versioning](https://semver.org/). + +## 8.5.26 + +- Fixed `list.split()` regression (by @lazerg). +- Track symlinks in path protection in source map loading (by @drengir1). + +## 8.5.25 + +- Fixed 8.5.17 visitor regression. +- Fixed `list.split()` for non-string values (by @amir-rezaei). + +## 8.5.24 + +- Preserve the BOM after the processing (by @hdimer). + +## 8.5.23 + +- Do not load source map without `opts.from` for security reasons. + +## 8.5.22 + +- Fixed custom property losing semicolon before a comment (by @sarathfrancis90). + +## 8.5.21 + +- Fixed childless at-rule losing semicolon before comment (by @sarathfrancis90). +- Fixed docs (by @isker). + +## 8.5.20 + +- Fixed missing space if `AtRule#params` is set after (by @sarathfrancis90). +- Fixed mixing AST error on warnings (by @MahinAnowar). + +## 8.5.19 + +- Fixed cleaning `before` for new nodes inserted to `Root` (by @MahinAnowar). + +## 8.5.18 + +- Restricted loading previous source maps file to the `opts.from` folder for security reasons (use `unsafeMap: true` to disable the check). + +## 8.5.17 + +- Fixed `Maximum call stack size exceeded` error. +- Fixed Prototype hijacking for `postcss.fromJSON()`. +- Fixed `Input#origin()` for unmapped end position (by @chatman-media). + +## 8.5.16 + +- Fixed `Input#origin()` position (by @mizdra). +- Fixed `raws` after rehydrating a JSON AST (by @sarathfrancis90). +- Fixed putting parent-less node in `nodes` of new node (by @MahinAnowar). +- Fixed computing `offset` in `positionBy()` (by @greymoth-jp). +- Fixed `rangeBy()` on `index: 0` (by @sarathfrancis90). + +## 8.5.15 + +- Fixed declaration parsing performance (by @homanp). + +## 8.5.14 + +- Fixed custom syntax regression (by @43081j). + +## 8.5.13 + +- Fixed `postcss-scss` commend regression. + +## 8.5.12 + +- Fixed reading any file via user-generated CSS. +- Added `opts.unsafeMap` to disable checks. + +## 8.5.11 + +- Fixed nested brackets parsing performance (by @offset). + +## 8.5.10 + +- Fixed XSS via unescaped `` in non-bundler cases (by @TharVid). + +## 8.5.9 + +- Speed up source map encoding paring in case of the error. + +## 8.5.8 + +- Fixed `Processor#version`. + +## 8.5.7 + +- Improved source map annotation cleaning performance (by CodeAnt AI). + +## 8.5.6 + +- Fixed `ContainerWithChildren` type discriminating (by @Goodwine). + +## 8.5.5 + +- Fixed `package.json`→`exports` compatibility with some tools (by @JounQin). + +## 8.5.4 + +- Fixed Parcel compatibility issue (by @git-sumitchaudhary). + +## 8.5.3 + +- Added more details to `Unknown word` error (by @hiepxanh). +- Fixed types (by @romainmenke). +- Fixed docs (by @catnipan). + +## 8.5.2 + +- Fixed end position of rules with semicolon (by @romainmenke). + +## 8.5.1 + +- Fixed backwards compatibility for complex cases (by @romainmenke). + +## 8.5 “Duke Alloces” + +- Added `Input#document` for sources like CSS-in-JS or HTML (by @romainmenke). + +## 8.4.49 + +- Fixed custom syntax without `source.offset` (by @romainmenke). + +## 8.4.48 + +- Fixed position calculation in error/warnings methods (by @romainmenke). + +## 8.4.47 + +- Removed debug code. + +## 8.4.46 + +- Fixed `Cannot read properties of undefined (reading 'before')`. + +## 8.4.45 + +- Removed unnecessary fix which could lead to infinite loop. + +## 8.4.44 + +- Another way to fix `markClean is not a function` error. + +## 8.4.43 + +- Fixed `markClean is not a function` error. + +## 8.4.42 + +- Fixed CSS syntax error on long minified files (by @varpstar). + +## 8.4.41 + +- Fixed types (by @nex3 and @querkmachine). +- Cleaned up RegExps (by @bluwy). + +## 8.4.40 + +- Moved to getter/setter in nodes types to help Sass team (by @nex3). + +## 8.4.39 + +- Fixed `CssSyntaxError` types (by @romainmenke). + +## 8.4.38 + +- Fixed `endIndex: 0` in errors and warnings (by @romainmenke). + +## 8.4.37 + +- Fixed `original.column are not numbers` error in another case. + +## 8.4.36 + +- Fixed `original.column are not numbers` error on broken previous source map. + +## 8.4.35 + +- Avoid `!` in `node.parent.nodes` type. +- Allow to pass `undefined` to node adding method to simplify types. + +## 8.4.34 + +- Fixed `AtRule#nodes` type (by Tim Weißenfels). +- Cleaned up code (by Dmitry Kirillov). + +## 8.4.33 + +- Fixed `NoWorkResult` behavior difference with normal mode (by Romain Menke). +- Fixed `NoWorkResult` usage conditions (by @ahmdammarr). + +## 8.4.32 + +- Fixed `postcss().process()` types (by Andrew Ferreira). + +## 8.4.31 + +- Fixed `\r` parsing to fix CVE-2023-44270. + +## 8.4.30 + +- Improved source map performance (by Romain Menke). + +## 8.4.29 + +- Fixed `Node#source.offset` (by Ido Rosenthal). +- Fixed docs (by Christian Oliff). + +## 8.4.28 + +- Fixed `Root.source.end` for better source map (by Romain Menke). +- Fixed `Result.root` types when `process()` has no parser. + +## 8.4.27 + +- Fixed `Container` clone methods types. + +## 8.4.26 + +- Fixed clone methods types. + +## 8.4.25 + +- Improve stringify performance (by Romain Menke). +- Fixed docs (by @vikaskaliramna07). + +## 8.4.24 + +- Fixed `Plugin` types. + +## 8.4.23 + +- Fixed warnings in TypeDoc. + +## 8.4.22 + +- Fixed TypeScript support with `node16` (by Remco Haszing). + +## 8.4.21 + +- Fixed `Input#error` types (by Aleks Hudochenkov). + +## 8.4.20 + +- Fixed source map generation for childless at-rules like `@layer`. + +## 8.4.19 + +- Fixed whitespace preserving after AST transformations (by Romain Menke). + +## 8.4.18 + +- Fixed an error on `absolute: true` with empty `sourceContent` (by Rene Haas). + +## 8.4.17 + +- Fixed `Node.before()` unexpected behavior (by Romain Menke). +- Added TOC to docs (by Mikhail Dedov). + +## 8.4.16 + +- Fixed `Root` AST migration. + +## 8.4.15 + +- Fixed AST normalization after using custom parser with old PostCSS AST. + +## 8.4.14 + +- Print “old plugin API” warning only if plugin was used (by @zardoy). + +## 8.4.13 + +- Fixed `append()` error after using `.parent` (by Jordan Pittman). + +## 8.4.12 + +- Fixed `package.funding` to have same value between all PostCSS packages. + +## 8.4.11 + +- Fixed `Declaration#raws.value` type. + +## 8.4.10 + +- Fixed `package.funding` URL format. + +## 8.4.9 + +- Fixed `package.funding` (by Álvaro Mondéjar). + +## 8.4.8 + +- Fixed end position in empty Custom Properties. + +## 8.4.7 + +- Fixed `Node#warn()` type (by Masafumi Koba). +- Fixed comment removal in values after `,`. + +## 8.4.6 + +- Prevented comment removing when it change meaning of CSS. +- Fixed parsing space in last semicolon-less CSS Custom Properties. +- Fixed comment cleaning in CSS Custom Properties with space. +- Fixed throwing an error on `.root` access for plugin-less case. + +## 8.4.5 + +- Fixed `raws` types to make object extendable (by James Garbutt). +- Moved from Yarn 1 to pnpm. + +## 8.4.4 + +- Fixed absolute path in source map on zero plugins mode. + +## 8.4.3 + +- Fixed `this.css.replace is not a function` error. + +## 8.4.2 + +- Fixed previous source map support in zero plugins mode. + +## 8.4.1 + +- Fixed `Stringifier` types (by James Garbutt). + +## 8.4 “President Camio” + +- Added ranges for errors and warnings (by Adaline Valentina Simonian). +- Added `Stringifier` types (by James Garbutt). +- Added `Processor` types. +- Removed `PostCSS does nothing` warning by lazy parser (Bogdan Dolin). +- Fixed `Node#walkRules()` types (by Aleks Hudochenkov). +- Fixed types `Root` and `Document` in result values (by James Garbutt). +- Reduced npm install size by 0.5 MB. +- Moved tests from Jest to `uvu` (by Andrey Kim). +- Fixed docs (by Paul Shryock). + +## 8.3.11 + +- Remove debugging code. + +## 8.3.10 + +- Fixed `Maximum call stack` issue of some source maps (by Yeting Li). + +## 8.3.9 + +- Replaced `nanocolors` to `picocolors`. +- Reduced package size. + +## 8.3.8 + +- Update `nanocolors`. + +## 8.3.7 + +- Replaced `colorette` to `nanocolors`. +- Added bug field to `package.json` (by Christian Oliff). +- Improved docs (by Andrew Bruce and Paul Shryock). + +## 8.3.6 + +- Fixed column in `missed semicolon` error (by @Gusted). + +## 8.3.5 + +- Fixed broken AST detection. + +## 8.3.4 + +- Fixed broken AST detection. + +## 8.3.3 + +- Fixed broken AST on `postcss` dependency duplication in custom parsers. + +## 8.3.2 + +- Update changelog. + +## 8.3.1 + +- Fixed false positives `PostCSS does nothing` warning on `syntax` option. + +## 8.3 “Duke Murmur” + +- Added `Node#assign()` shortcut (by Jonathan Neal). +- Added experimental `Document` node to AST (by Aleks Hudochenkov). +- Moved to faster fork of `source-map` (by Valentin Semirulnik). + +## 8.2.15 + +- Fixed `list` type definitions (by @n19htz). + +## 8.2.14 + +- Removed `source-map` from client-side bundle (by Barak Igal). + +## 8.2.13 + +- Fixed ReDoS vulnerabilities in source map parsing (by Yeting Li). + +## 8.2.12 + +- Fixed `package.json` exports. + +## 8.2.11 + +- Fixed `DEP0148` warning in Node.js 16. +- Fixed docs (by @semiromid). + +## 8.2.10 + +- Fixed ReDoS vulnerabilities in source map parsing. +- Fixed webpack 5 support (by Barak Igal). +- Fixed docs (by Roeland Moors). + +## 8.2.9 + +- Exported `NodeErrorOptions` type (by Rouven Weßling). + +## 8.2.8 + +- Fixed browser builds in webpack 4 (by Matt Jones). + +## 8.2.7 + +- Fixed browser builds in webpack 5 (by Matt Jones). + +## 8.2.6 + +- Fixed `Maximum call stack size exceeded` in `Node#toJSON`. +- Fixed docs (by inokawa). + +## 8.2.5 + +- Fixed escaped characters handling in `list.split` (by Natalie Weizenbaum). + +## 8.2.4 + +- Added plugin name to `postcss.plugin()` warning (by Tom Williams). +- Fixed docs (by Bill Columbia). + +## 8.2.3 + +- Fixed `JSON.stringify(Node[])` support (by Niklas Mischkulnig). + +## 8.2.2 + +- Fixed CSS-in-JS support (by James Garbutt). +- Fixed plugin types (by Ludovico Fischer). +- Fixed `Result#warn()` types. + +## 8.2.1 + +- Fixed `Node#toJSON()` and `postcss.fromJSON()` (by Niklas Mischkulnig). + +## 8.2 “Prince Orobas” + +- Added `Node#toJSON()` and `postcss.fromJSON()` (by Niklas Mischkulnig). + +## 8.1.14 + +- Fixed parser performance regression. + +## 8.1.13 + +- Fixed broken AST after moving nodes in visitor API. + +## 8.1.12 + +- Fixed Autoprefixer regression. + +## 8.1.11 + +- Added PostCSS update suggestion on unknown event in plugin. + +## 8.1.10 + +- Fixed `LazyResult` type export (by Evan You). +- Fixed `LazyResult` type compatibility with `Promise` (by Anton Kastritskiy). + +## 8.1.9 + +- Reduced dependencies number (by Bogdan Chadkin). + +## 8.1.8 + +- Fixed `LazyResult` type compatibility with `Promise` (by Ludovico Fischer). +- Fixed HTTPS links in documentation. + +## 8.1.7 + +- Fixed `import` support in TypeScript (by Remco Haszing). + +## 8.1.6 + +- Reverted `package.exports` Node.js 15 fix. + +## 8.1.5 + +- Fixed Node.js 15 warning (by 沈鸿飞). + +## 8.1.4 + +- Fixed TypeScript definition (by Arthur Petrie). + +## 8.1.3 + +- Added `package.types`. + +## 8.1.2 + +- Fixed API docs (by Arthur Petrie). +- Improved plugin guide (by Yunus Gaziev). +- Prepared code base for Deno support (by Oscar Otero). + +## 8.1.1 + +- Updated funding link. + +## 8.1 “Duke Gemory” + +- Added `Once` and `OnceExit` events. +- Fixed `Root` and `RootExit` events re-visiting. +- Fixed node re-visiting on deep children changes. +- Added docs for visitor API events. + +## 8.0.9 + +- Replace prototype in PostCSS 7 nodes instead of recreating them. +- Added missed `Transformer` to exported types (by Pierre-Marie Dartus). + +## 8.0.8 + +- Fix `8.0.7` regression on PostCSS 7 nodes converting (by Adam Wathan). + +## 8.0.7 + +- Fixed compatibility issue with mixin AST with PostCSS 7 and 8 nodes. +- Added migration guide translation to Chinese to the warning. + +## 8.0.6 + +- Fixed child adding methods in `Container`. + +## 8.0.5 + +- Update changelog. + +## 8.0.4 + +- Fixed `Cannot read property 'line' of null` error. +- Fixed source map support for declarations. + +## 8.0.3 + +- Fixed client-side bundling support. + +## 8.0.2 + +- Fixed plugin packs support. + +## 8.0.1 + +- Updated `Processor#version`. + +## 8.0 “President Ose” + +- Removed support for Node.js 6.x, 8.x, 11.x, and 13.x versions. +- Removed `postcss.vendor` helpers. +- Deprecated `postcss.plugin()` API. +- Treats `sourceMap.sources` as URL instead of file path. +- Plugins and runners must have `postcss` in `peerDependencies`. +- Prohibited to extend PostCSS AST classes. +- Moved from JSDoc to TypeDoc. +- Moved unknown source from counter to random IDs. +- Added visitor API for plugins (by Alexey Bondarenko). +- Added ES modules support. +- Added named exports for public classes `const { Rule } = require('postcss)`. +- Added `position.url` to `Node#origin()` result. +- Added `opts.maps.absolute = true` option. +- Added `opts.maps.annotation = (file, root) => url` option support. +- Added `Node#source.offset` (by Ayaz Zaynutdinov). +- Added `Declaration#variable`. +- Added JSON source map support. +- Added index source map support. +- Added `Declaration#value` auto-converting to string. +- Fixed parsing `{}` in at-rule parameters. +- Fixed parsing empty Custom Properties. `--foo: ;` will have ` ` value. +- Fixed building PostCSS with Rollup (by MapGrid). +- Fixed TypeScript types. +- Fixed source map relative paths. +- Fixed calling `replaceWith` with input replaced node (by Joseph Kaptur). +- Improved “Writing a PostCSS Plugin” docs (by Alexey Bondarenko). +- Removed Babel from the project’s release process. +- Removed docs from npm package. +- Replaced `chalk` to `colorette`. + +## 7.0.38 + +- Update `Processor#version`. + +## 7.0.37 + +- Backport `chalk` to `nanocolors` migration. + +## 7.0.36 + +- Backport ReDoS vulnerabilities from PostCSS 8. + +## 7.0.35 + +- Add migration guide link to PostCSS 8 error text. + +## 7.0.34 + +- Fix compatibility with `postcss-scss` 2. + +## 7.0.33 + +- Add error message for PostCSS 8 plugins. + +## 7.0.32 + +- Fix error message (by @admosity). + +## 7.0.31 + +- Use only the latest source map annotation (by Emmanouil Zoumpoulakis). + +## 7.0.30 + +- Fix TypeScript definition (by Natalie Weizenbaum). + +## 7.0.29 + +- Update `Processor#version`. + +## 7.0.28 + +- Fix TypeScript definition (by Natalie Weizenbaum). + +## 7.0.27 + +- Fix TypeScript definition (by Natalie Weizenbaum). + +## 7.0.26 + +- Fix TypeScript definition (by Natalie Weizenbaum). + +## 7.0.25 + +- Fix absolute path support for Windows (by Tom Raviv). + +## 7.0.24 + +- Fix TypeScript definition (by Keith Cirkel). + +## 7.0.23 + +- Update `Processor#version`. + +## 7.0.22 + +- Add funding link for `npm fund`. + +## 7.0.21 + +- Revert passing `nodes` property to node constructor. + +## 7.0.20 + +- Allow to pass PostCSS’s nodes in `nodes` property to node constructor. + +## 7.0.19 + +- Fix passing `nodes` property to node constructor. + +## 7.0.18 + +- Fix TypeScript type definitions (by Jan Buschtöns). + +## 7.0.17 + +- Fix TypeScript type definitions (by Bob Matcuk and Jan Buschtöns). + +## 7.0.16 + +- Revert Custom Properties fix until PostCSS 8.0. + +## 7.0.15 + +- Fix Custom Properties support (by Ivan Solovev). + +## 7.0.14 + +- Fix tokenizer for `postcss-less` (by Matt Lyons). + +## 7.0.13 + +- Fix parsing regression in 7.0.12 for comments between property and value. + +## 7.0.12 + +- Fix parsing broken CSS with two words in declaration property. + +## 7.0.11 + +- Fix source maps on declaration semicolon (by Niklas Mischkulnig). + +## 7.0.10 + +- Fix source maps (by Niklas Mischkulnig). + +## 7.0.9 + +- Increase stringifing performance for non-raws AST. + +## 7.0.8 + +- Fix TypeScript definitions (by Ankur Oberoi). +- Use `support-colors` 6.0. + +## 7.0.7 + +- Extend `Error` in `CssSyntaxError`. + +## 7.0.6 + +- Fix parsing files with BOM (by Veniamin Krol). + +## 7.0.5 + +- Reduce npm package size (by Gilad Peleg). + +## 7.0.4 + +- Fix safe parser regression. + +## 7.0.3 + +- Fix tokenizer extendability (by Andrew Powell). +- Reduce npm package size. + +## 7.0.2 + +- Fix warning text (by Rui Pedro M Lima). + +## 7.0.1 + +- Fix JSDoc (by Steven Lambert). + +## 7.0 “President Amy” + +- Remove Node.js 9 and Node.js 4 support. +- Remove IE and “dead” browsers support for client-side Babel transpiling. +- Add CSS position on error happened inside `walk()` (by Nikhil Gaba). +- Add `LazyResult#finally` (by Igor Kamyshev). +- Add warning on calling PostCSS without plugins and syntax options. +- Reduce client-side size. + +## 6.0.23 + +- Fix parsing nested at-rules without semicolon, params, and spaces. +- Fix docs (by Kevin Schiffer and Pat Cavit). + +## 6.0.22 + +- Fix `Node#prev` and `Node#next` on missed parent. + +## 6.0.21 + +- Rename Chinese docs to fix `yarnpkg.com` issue. + +## 6.0.20 + +- Better error message on `null` as input CSS. + +## 6.0.19 + +- Fix TypeScript definitions for source maps (by Oleh Kuchuk). +- Fix `source` field in TypeScript definitions (by Sylvain Pollet-Villard). + +## 6.0.18 + +- Use primitive object in TypeScript definitions (by Sylvain Pollet-Villard). + +## 6.0.17 + +- Fix parsing comment in selector between word tokens (by Oleh Kuchuk). + +## 6.0.16 + +- Fix warning text (by Michael Keller). + +## 6.0.15 + +- Add warning about missed `from` option on `process().then()` call. +- Add IE 10 support. + +## 6.0.14 + +- Fix TypeScript definitions (by Jed Mao). + +## 6.0.13 + +- Fix TypeScript definitions for case of multiple PostCSS versions + in `node_modules` (by Chris Eppstein). +- Use `source-map` 0.6. + +## 6.0.12 + +- Don’t copy `*` hack to declaration indent. + +## 6.0.11 + +- Add upper case `!IMPORTANT` support. + +## 6.0.10 + +- Reduce PostCSS size in webpack bundle. + +## 6.0.9 + +- Improve error message for plugin with old PostCSS (by Igor Adamenko). + +## 6.0.8 + +- Fix Node.js 4.2.2 support. + +## 6.0.7 + +- Fix base64 decoding for old Node.js and browser. + +## 6.0.6 + +- Fix `end` position in at-rule without semicolon (by Oleh Kuchuk). + +## 6.0.5 + +- Move Babel config from `package.json` for `node_modules` compiling cases. + +## 6.0.4 + +- Fix parsing `;;` after rules. +- Use Chalk 2.0. + +## 6.0.3 + +- Fix escape sequences parsing (by Oleh Kuchuk). +- Added ability to force disable colors with an environment variable. +- Improved color detection of some terminal apps. + +## 6.0.2 + +- Keep `raws.before` on moving `Root` children to new `Root`. + +## 6.0.1 + +- Fix parser extensibility to use it in Safe Parser. + +## 6.0 “Marquis Orias” + +- Remove node.js 0.12 support. +- Remove deprecated method from PostCSS 4. +- Insert methods remove child from previous parent, instead of closing. +- Insert methods and cloning doesn’t clean `raws` anymore. +- Methods `moveTo`, `moveAfter`, `moveBefore` were deprecated. +- Options was changed in `Plugin#process(css, processOptions, pluginOptions)`. +- Add stream parser to reduce memory usage (by Oleh Kuchuk). +- Add `before()`/`after()` shortcuts for `node.parent.insertBefore(node, x)`. +- Add `Rule#raws.ownSemicolon` for semicolon after templates for `@apply`. +- Use `babel-preset-env` to compile npm package. +- Remove `js-base64` from dependencies (by Roman Dvornov). +- Fix error message on single `:` in CSS. +- Move tests to Jest. +- Clean up test (by Gabriel Kalani). + +## 5.2.18 + +- Fix TypeScript definitions for case of multiple PostCSS versions + in `node_modules` (by Chris Eppstein). + +## 5.2.17 + +- Add `postcss-sass` suggestion to syntax error on `.sass` input. + +## 5.2.16 + +- Better error on wrong argument in node constructor. + +## 5.2.15 + +- Fix TypeScript definitions (by bumbleblym). + +## 5.2.14 + +- Fix browser bundle building in webpack (by janschoenherr). + +## 5.2.13 + +- Do not add comment to important raws. +- Fix JSDoc (by Dmitry Semigradsky). + +## 5.2.12 + +- Fix typo in deprecation message (by Garet McKinley). + +## 5.2.11 + +- Fix TypeScript definitions (by Jed Mao). + +## 5.2.10 + +- Fix TypeScript definitions (by Jed Mao). + +## 5.2.9 + +- Update TypeScript definitions (by Jed Mao). + +## 5.2.8 + +- Fix error message (by Ben Briggs). + +## 5.2.7 + +- Better error message on syntax object in plugins list. + +## 5.2.6 + +- Fix `postcss.vendor` for values with spaces (by 刘祺). + +## 5.2.5 + +- Better error message on unclosed string (by Ben Briggs). + +## 5.2.4 + +- Improve terminal CSS syntax highlight (by Simon Lydell). + +## 5.2.3 + +- Better color highlight in syntax error code frame. +- Fix color highlight support in old systems. + +## 5.2.2 + +- Update `Processor#version`. + +## 5.2.1 + +- Fix source map path for CSS without `from` option (by Michele Locati). + +## 5.2 “Duke Vapula” + +- Add syntax highlight to code frame in syntax error (by Andrey Popp). +- Use Babel code frame style and size in syntax error. +- Add `[` and `]` tokens to parse `[attr=;] {}` correctly. +- Add `ignoreErrors` options to tokenizer (by Andrey Popp). +- Fix error position on tab indent (by Simon Lydell). + +## 5.1.2 + +- Suggests SCSS/Less parsers on parse errors depends on file extension. + +## 5.1.1 + +- Fix TypeScript definitions (by Efremov Alexey). + +## 5.1 “King and President Zagan” + +- Add URI in source map support (by Mark Finger). +- Add `map.from` option (by Mark Finger). +- Add `` mappings for nodes without source (by Bogdan Chadkin). +- Add function value support to `map.prev` option (by Chris Montoro). +- Add declaration value type check in shortcut creating (by 刘祺). +- `Result#warn` now returns new created warning. +- Don’t call plugin creator in `postcss.plugin` call. +- Add source maps to PostCSS ES5 build. +- Add JSDoc to PostCSS classes. +- Clean npm package from unnecessary docs. + +## 5.0.21 + +- 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)`. + +## 5.0.19 + +- Fix indent-based syntaxes support. + +## 5.0.18 + +- 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). + +## 5.0.16 + +- Fix CSS syntax error position on unclosed quotes. + +## 5.0.15 + +- Fix `Node#clone()` on `null` value somewhere in node. + +## 5.0.14 + +- 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. + +## 5.0.12 + +- Allow passing a previous map with no mappings (by Andreas Lind). + +## 5.0.11 + +- Increase plugins performance by 1.5 times. + +## 5.0.10 + +- Fix warning from nodes without source. + +## 5.0.9 + +- 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 + ES6 code. + +## 5.0.7 + +- 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). + +## 5.0.5 + +- 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). + +## 5.0.3 + +- 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. + +## 5.0.1 + +- 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` + in favor of `walk`, `walkDecls`, `walkRules`, `walkAtRules` and `walkComments` + (by Jed Mao). +- 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). + +## 4.1.16 + +- Fix errors without stack trace. + +## 4.1.15 + +- Allow asynchronous plugins to change processor plugins list (by Ben Briggs). + +## 4.1.14 + +- Fix for plugins packs defined by `postcss.plugin`. + +## 4.1.13 + +- Fix input inlined source maps with UTF-8 encoding. + +## 4.1.12 + +- Update Promise polyfill. + +## 4.1.11 + +- 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). + +## 4.1.9 + +- Speed up `node.clone()`. + +## 4.1.8 + +- Accepts `Processor` instance in `postcss()` constructor too. + +## 4.1.7 + +- Speed up `postcss.list` (by Bogdan Chadkin). + +## 4.1.6 + +- Fix Promise behavior on parsing error. + +## 4.1.5 + +- Parse at-words in declaration values. + +## 4.1.4 + +- Fix Promise polyfill dependency (by Anton Yakushev and Matija Marohnić). + +## 4.1.3 + +- Add Promise polyfill for node.js 0.10 and IE. + +## 4.1.2 + +- List helpers can be accessed independently `var space = postcss.list.space`. + +## 4.1.1 + +- 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. + +## 4.0.6 + +- Remove `babel` from released package dependencies (by Andres Suarez). + +## 4.0.5 + +- Fix error message on double colon in declaration. + +## 4.0.4 + +- Fix indent detection in some rare cases. + +## 4.0.3 + +- Faster API with 6to5 Loose mode. +- Fix indexed source maps support. + +## 4.0.2 + +- Do not copy IE hacks to code style. + +## 4.0.1 + +- 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. + +## 3.0.7 + +- 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. + +## 3.0.5 + +- 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. + +## 3.0.3 + +- Increase tokenizer speed (by Roman Dvornov). + +## 3.0.2 + +- 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). + +## 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`) + 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. + +## 2.2.6 + +- 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). + +## 2.2.4 + +- Fix `prepend()` on empty `Root`. + +## 2.2.3 + +- 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. + +## 2.2.1 + +- 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. + +## 2.1.2 + +- 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. + +## 2.1 “King Amdusias” + +- 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. + +## 1.0 “Marquis Decarabia” + +- 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` + 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` + 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. + +## 0.3.5 + +- 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. + +## 0.3.3 + +- 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. + +## 0.3.1 + +- 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` + 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 + and node end position. +- You can set own CSS generate function. + +## 0.1 “Count Andromalius” + +- Initial release. diff --git a/ChangeLog.md b/ChangeLog.md deleted file mode 100644 index 1e89eab0f..000000000 --- a/ChangeLog.md +++ /dev/null @@ -1,190 +0,0 @@ -## 4.0.4 -* Fix indent detection in some rare cases. - -## 4.0.3 -* Faster API with 6to5 Loose mode. -* Fix indexed source maps support. - -## 4.0.2 -* Do not copy IE hacks to code style. - -## 4.0.1 -* 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()` shorcuts. -* 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). - -## 3.0.6 -* 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). - -## 3.0.4 -* Fix Safe Mode on unknown word before declaration. - -## 3.0.3 -* Increase tokenizer speed (by Roman Dvornov). - -## 3.0.2 -* 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). - -## 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`) - 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. - -## 2.2.6 -* 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). - -## 2.2.4 -* Fix `prepend()` on empty `Root`. - -## 2.2.3 -* 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. - -## 2.2.1 -* 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. - -## 2.1.2 -* 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. - -## 2.1 “King Amdusias” -* 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 propeties. - -## 1.0 “Marquis Decarabia” -* 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` - 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 stringify 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. - -## 0.3.5 -* 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. - -## 0.3.3 -* 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. - -## 0.3.1 -* 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 styles in subdirectory (by @nDmitry and @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 recursivelly 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 - and node end position. -* You can set own stringify function. - -## 0.1 “Count Andromalius” -* Initial release. diff --git a/LICENSE b/LICENSE index da057b456..c2314d53d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright 2013 Andrey Sitnik +Copyright 2013 Andrey Sitnik Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 7b1cd8100..01f715fea 100644 --- a/README.md +++ b/README.md @@ -1,440 +1,476 @@ -# PostCSS [![Build Status](https://travis-ci.org/postcss/postcss.svg)](https://travis-ci.org/postcss/postcss) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/postcss/postcss?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# PostCSS - +Philosopher’s stone, logo of PostCSS -PostCSS is a tool for transforming CSS with JS plugins. The growing ecosystem -of PostCSS plugins can add vendor prefixes, support variables and mixins, -transpile future CSS syntax, inline images, and more. +PostCSS is a tool for transforming styles with JS plugins. +These plugins can lint your CSS, support variables and mixins, +transpile future CSS syntax, inline images, and more. -PostCSS is used by Google, Twitter, Alibaba, and Shopify. -Its most popular plugin, [Autoprefixer], is one of the most universally -praised CSS processors available. +PostCSS is used by industry leaders including Wikipedia, Twitter, Alibaba, +and JetBrains. The [Autoprefixer] and [Stylelint] PostCSS plugins are some of the most popular CSS tools. -PostCSS can do the same work as preprocessors like Sass, Less, and Stylus. -But PostCSS is modular, 4-40x faster, and much more powerful. +--- -PostCSS itself is very small. It includes only a CSS parser, -a CSS node tree API, a source map generator, and a node tree stringifier. -All CSS transformations are encapsulated in modular plugins. And these plugins -are themselves small plain JS functions, which receive a CSS node tree, -apply transformations to it, and return a modified tree. +  PostCSS is built by Evil Martians, an American design and engineering consultancy for developer tools, AI, and cybersecurity startups. -You can use the [cssnext] plugin pack and write future CSS code right now: +--- -```css -:root { - --row: 1rem; - --mainColor: #ffbbaaff; -} +[Abstract Syntax Tree]: https://en.wikipedia.org/wiki/Abstract_syntax_tree +[Evil Martians]: https://evilmartians.com/?utm_source=postcss +[Autoprefixer]: https://github.com/postcss/autoprefixer +[Stylelint]: https://stylelint.io/ +[plugins]: https://github.com/postcss/postcss#plugins + +## Sponsorship + +PostCSS needs your support. We are accepting donations +[at Open Collective](https://opencollective.com/postcss/). + +
+
+ + + + Sponsored by Atlas Cloud + + +
+ +Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. + +Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access. + +## Plugins + +PostCSS takes a CSS file and provides an API to analyze and modify its rules +(by transforming them into an [Abstract Syntax Tree]). +This API can then be used by [plugins] to do a lot of useful things, +e.g., to find errors automatically, or to insert vendor prefixes. + +Currently, PostCSS has more than 200 plugins. You can find all of the plugins +in the [plugins list]. Below is a list of our favorite plugins — +the best demonstrations of what can be built on top of PostCSS. + +If you have any new ideas, [PostCSS plugin development] is really easy. + +[plugins list]: https://github.com/postcss/postcss/blob/main/docs/plugins.md + +### Solve Global CSS Problem + +- [`postcss-use`] allows you to explicitly set PostCSS plugins within CSS + and execute them only for the current file. +- [`postcss-modules`] and [`react-css-modules`] automatically isolate + selectors within components. +- [`postcss-autoreset`] is an alternative to using a global reset + that is better for isolatable components. +- [`postcss-initial`] adds `all: initial` support, which resets + all inherited styles. +- [`cq-prolyfill`] adds container query support, allowing styles that respond + to the width of the parent. + +### Use Future CSS, Today + +- [`autoprefixer`] adds vendor prefixes, using data from Can I Use. +- [`postcss-preset-env`] allows you to use future CSS features today. + +### Better CSS Readability + +- [`postcss-nested`] unwraps nested rules the way Sass does. +- [`postcss-sorting`] sorts the content of rules and at-rules. +- [`postcss-utilities`] includes the most commonly used shortcuts and helpers. +- [`short`] adds and extends numerous shorthand properties. + +### Images and Fonts + +- [`postcss-url`] postcss plugin to rebase url(), inline or copy asset. +- [`postcss-sprites`] generates image sprites. +- [`font-magician`] generates all the `@font-face` rules needed in CSS. +- [`postcss-inline-svg`] allows you to inline SVG and customize its styles. +- [`postcss-write-svg`] allows you to write simple SVG directly in your CSS. +- [`webp-in-css`] to use WebP image format in CSS background. +- [`avif-in-css`] to use AVIF image format in CSS background. + +### Linters + +- [`stylelint`] is a modular stylesheet linter. +- [`stylefmt`] is a tool that automatically formats CSS + according `stylelint` rules. +- [`doiuse`] lints CSS for browser support, using data from Can I Use. +- [`colorguard`] helps you maintain a consistent color palette. + +### Other + +- [`cssnano`] is a modular CSS minifier. +- [`lost`] is a feature-rich `calc()` grid system. +- [`rtlcss`] mirrors styles for right-to-left locales. + +[PostCSS plugin development]: https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md +[`postcss-inline-svg`]: https://github.com/TrySound/postcss-inline-svg +[`postcss-preset-env`]: https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env +[`react-css-modules`]: https://github.com/gajus/react-css-modules +[`postcss-autoreset`]: https://github.com/maximkoretskiy/postcss-autoreset +[`postcss-write-svg`]: https://github.com/csstools/postcss-write-svg +[`postcss-utilities`]: https://github.com/ismamz/postcss-utilities +[`postcss-initial`]: https://github.com/maximkoretskiy/postcss-initial +[`postcss-sprites`]: https://github.com/2createStudio/postcss-sprites +[`postcss-modules`]: https://github.com/outpunk/postcss-modules +[`postcss-sorting`]: https://github.com/hudochenkov/postcss-sorting +[`font-magician`]: https://github.com/csstools/postcss-font-magician +[`autoprefixer`]: https://github.com/postcss/autoprefixer +[`cq-prolyfill`]: https://github.com/ausi/cq-prolyfill +[`postcss-url`]: https://github.com/postcss/postcss-url +[`postcss-use`]: https://github.com/postcss/postcss-use +[`css-modules`]: https://github.com/css-modules/css-modules +[`webp-in-css`]: https://github.com/ai/webp-in-css +[`avif-in-css`]: https://github.com/nucliweb/avif-in-css +[`colorguard`]: https://github.com/SlexAxton/css-colorguard +[`stylelint`]: https://github.com/stylelint/stylelint +[`stylefmt`]: https://github.com/morishitter/stylefmt +[`cssnano`]: https://cssnano.github.io/cssnano/ +[`postcss-nested`]: https://github.com/postcss/postcss-nested +[`doiuse`]: https://github.com/anandthakker/doiuse +[`rtlcss`]: https://github.com/MohammadYounes/rtlcss +[`short`]: https://github.com/csstools/postcss-short +[`lost`]: https://github.com/peterramsing/lost + +## Syntaxes + +PostCSS can transform styles in any syntax, not just CSS. +If there is not yet support for your favorite syntax, +you can write a parser and/or stringifier to extend PostCSS. + +- [`sugarss`] is a indent-based syntax like Sass or Stylus. +- [`postcss-syntax`] switch syntax automatically by file extensions. +- [`postcss-html`] parsing styles in `' + * ) + * document.type //=> 'document' + * document.nodes.length //=> 2 + * ``` + */ +declare class Document_ extends Container { + nodes: Root[] + parent: undefined + type: 'document' + + constructor(defaults?: Document.DocumentProps) + + assign(overrides: Document.DocumentProps | object): this + clone(overrides?: Partial): this + cloneAfter(overrides?: Partial): this + cloneBefore(overrides?: Partial): this + + /** + * Returns a `Result` instance representing the document’s CSS roots. + * + * ```js + * const root1 = postcss.parse(css1, { from: 'a.css' }) + * const root2 = postcss.parse(css2, { from: 'b.css' }) + * const document = postcss.document() + * document.append(root1) + * document.append(root2) + * const result = document.toResult({ to: 'all.css', map: true }) + * ``` + * + * @param opts Options. + * @return Result with current document’s CSS. + */ + toResult(options?: ProcessOptions): Result +} + +declare class Document extends Document_ {} + +export = Document diff --git a/lib/document.js b/lib/document.js new file mode 100644 index 000000000..44689917f --- /dev/null +++ b/lib/document.js @@ -0,0 +1,33 @@ +'use strict' + +let Container = require('./container') + +let LazyResult, Processor + +class Document extends Container { + constructor(defaults) { + // type needs to be passed to super, otherwise child roots won't be normalized correctly + super({ type: 'document', ...defaults }) + + if (!this.nodes) { + this.nodes = [] + } + } + + toResult(opts = {}) { + let lazy = new LazyResult(new Processor(), this, opts) + + return lazy.stringify() + } +} + +Document.registerLazyResult = dependant => { + LazyResult = dependant +} + +Document.registerProcessor = dependant => { + Processor = dependant +} + +module.exports = Document +Document.default = Document diff --git a/lib/fromJSON.d.ts b/lib/fromJSON.d.ts new file mode 100644 index 000000000..3a0c5b85f --- /dev/null +++ b/lib/fromJSON.d.ts @@ -0,0 +1,9 @@ +import { JSONHydrator } from './postcss.js' + +interface FromJSON extends JSONHydrator { + default: FromJSON +} + +declare let fromJSON: FromJSON + +export = fromJSON diff --git a/lib/fromJSON.js b/lib/fromJSON.js new file mode 100644 index 000000000..c1a9509b4 --- /dev/null +++ b/lib/fromJSON.js @@ -0,0 +1,107 @@ +'use strict' + +let AtRule = require('./at-rule') +let Comment = require('./comment') +let Declaration = require('./declaration') +let Input = require('./input') +let PreviousMap = require('./previous-map') +let Root = require('./root') +let Rule = require('./rule') + +function hydrateInputs(json, inputs) { + if (!json.inputs) return inputs + return json.inputs.map(input => { + let inputHydrated = { ...input, __proto__: Input.prototype } + if (inputHydrated.map) { + inputHydrated.map = { + ...inputHydrated.map, + __proto__: PreviousMap.prototype + } + } + return inputHydrated + }) +} + +function constructNode(json, inputs, children) { + let defaults = { ...json } + delete defaults.inputs + delete defaults.nodes + if (defaults.source) { + let { inputId, ...source } = defaults.source + defaults.source = source + if (inputId != null) { + defaults.source.input = inputs[inputId] + } + } + + let node + if (defaults.type === 'root') { + node = new Root(defaults) + } else if (defaults.type === 'decl') { + node = new Declaration(defaults) + } else if (defaults.type === 'rule') { + node = new Rule(defaults) + } else if (defaults.type === 'comment') { + node = new Comment(defaults) + } else if (defaults.type === 'atrule') { + node = new AtRule(defaults) + } else { + throw new Error('Unknown node type: ' + json.type) + } + + // Rehydrated children are attached after construction. Passing them + // through the container constructor would re-run insertion spacing + // normalization and overwrite each child's own `raws.before`. + if (children) { + node.nodes = children + for (let child of children) child.parent = node + } + + return node +} + +function fromJSON(json, inputs) { + if (Array.isArray(json)) return json.map(n => fromJSON(n)) + + // An explicit stack instead of recursive calls to survive deeply + // nested trees. Children are rehydrated before their parent node + // is constructed. + let result + let stack = [ + { childIndex: 0, children: [], inputs: hydrateInputs(json, inputs), json } + ] + + while (stack.length > 0) { + let frame = stack[stack.length - 1] + let jsonNodes = frame.json.nodes + + if (jsonNodes && frame.childIndex < jsonNodes.length) { + let childJson = jsonNodes[frame.childIndex] + frame.childIndex += 1 + stack.push({ + childIndex: 0, + children: [], + inputs: hydrateInputs(childJson, frame.inputs), + json: childJson + }) + continue + } + + stack.pop() + let node = constructNode( + frame.json, + frame.inputs, + jsonNodes ? frame.children : undefined + ) + if (stack.length > 0) { + stack[stack.length - 1].children.push(node) + } else { + result = node + } + } + + return result +} + +module.exports = fromJSON +fromJSON.default = fromJSON diff --git a/lib/input.d.ts b/lib/input.d.ts new file mode 100644 index 000000000..ca2d26bf8 --- /dev/null +++ b/lib/input.d.ts @@ -0,0 +1,226 @@ +import { CssSyntaxError, ProcessOptions } from './postcss.js' +import PreviousMap from './previous-map.js' + +declare namespace Input { + export interface FilePosition { + /** + * Column of inclusive start position in source file. + */ + column: number + + /** + * Column of exclusive end position in source file. + */ + endColumn?: number + + /** + * Line of exclusive end position in source file. + */ + endLine?: number + + /** + * Offset of exclusive end position in source file. + */ + endOffset?: number + + /** + * Absolute path to the source file. + */ + file?: string + + /** + * Line of inclusive start position in source file. + */ + line: number + + /** + * Offset of inclusive start position in source file. + */ + offset: number + + /** + * Source code. + */ + source?: string + + /** + * URL for the source file. + */ + url: string + } + + export { Input_ as default } +} + +/** + * Represents the source CSS. + * + * ```js + * const root = postcss.parse(css, { from: file }) + * const input = root.source.input + * ``` + */ +declare class Input_ { + /** + * Input CSS source. + * + * ```js + * const input = postcss.parse('a{}', { from: file }).input + * input.css //=> "a{}" + * ``` + */ + css: string + + /** + * Input source with support for non-CSS documents. + * + * ```js + * const input = postcss.parse('a{}', { from: file, document: '' }).input + * input.document //=> "" + * input.css //=> "a{}" + * ``` + */ + document: string + + /** + * The absolute path to the CSS source file defined + * with the `from` option. + * + * ```js + * const root = postcss.parse(css, { from: 'a.css' }) + * root.source.input.file //=> '/home/ai/a.css' + * ``` + */ + file?: string + + /** + * The flag to indicate whether or not the source code has Unicode BOM. + */ + hasBOM: boolean + + /** + * The unique ID of the CSS source. It will be created if `from` option + * is not provided (because PostCSS does not know the file path). + * + * ```js + * const root = postcss.parse(css) + * root.source.input.file //=> undefined + * root.source.input.id //=> "" + * ``` + */ + id?: string + + /** + * The input source map passed from a compilation step before PostCSS + * (for example, from Sass compiler). + * + * ```js + * root.source.input.map.consumer().sources //=> ['a.sass'] + * ``` + */ + map: PreviousMap + + /** + * The CSS source identifier. Contains `Input#file` if the user + * set the `from` option, or `Input#id` if they did not. + * + * ```js + * const root = postcss.parse(css, { from: 'a.css' }) + * root.source.input.from //=> "/home/ai/a.css" + * + * const root = postcss.parse(css) + * root.source.input.from //=> "" + * ``` + */ + get from(): string + + /** + * @param css Input CSS source. + * @param opts Process options. + */ + constructor(css: string, opts?: ProcessOptions) + + /** + * Returns `CssSyntaxError` with information about the error and its position. + */ + error( + message: string, + start: + | { + column: number + line: number + } + | { + offset: number + }, + end: + | { + column: number + line: number + } + | { + offset: number + }, + opts?: { plugin?: CssSyntaxError['plugin'] } + ): CssSyntaxError + error( + message: string, + line: number, + column: number, + opts?: { plugin?: CssSyntaxError['plugin'] } + ): CssSyntaxError + error( + message: string, + offset: number, + opts?: { plugin?: CssSyntaxError['plugin'] } + ): CssSyntaxError + + /** + * Converts source line and column to offset. + * + * @param line Source line. + * @param column Source column. + * @return Source offset. + */ + fromLineAndColumn(line: number, column: number): number + + /** + * Converts source offset to line and column. + * + * @param offset Source offset. + */ + fromOffset(offset: number): { col: number; line: number } | null + + /** + * Reads the input source map and returns a symbol position + * in the input source (e.g., in a Sass file that was compiled + * to CSS before being passed to PostCSS). Optionally takes an + * end position, exclusive. + * + * ```js + * root.source.input.origin(1, 1) //=> { file: 'a.css', line: 3, column: 1 } + * root.source.input.origin(1, 1, 1, 4) + * //=> { file: 'a.css', line: 3, column: 1, endLine: 3, endColumn: 4 } + * ``` + * + * @param line Line for inclusive start position in input CSS. + * @param column Column for inclusive start position in input CSS. + * @param endLine Line for exclusive end position in input CSS. + * @param endColumn Column for exclusive end position in input CSS. + * + * @return Position in input source. + */ + origin( + line: number, + column: number, + endLine?: number, + endColumn?: number + ): false | Input.FilePosition + + /** Converts this to a JSON-friendly object representation. */ + toJSON(): object +} + +declare class Input extends Input_ {} + +export = Input diff --git a/lib/input.js b/lib/input.js index f1b9eae6c..defe395fc 100644 --- a/lib/input.js +++ b/lib/input.js @@ -1,85 +1,282 @@ -import CssSyntaxError from './css-syntax-error'; -import PreviousMap from './previous-map'; -import Parser from './parser'; +'use strict' -import path from 'path'; +let { nanoid } = require('nanoid/non-secure') +let { isAbsolute, resolve } = require('path') +let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js') +let { fileURLToPath, pathToFileURL } = require('url') -var sequence = 0; +let CssSyntaxError = require('./css-syntax-error') +let PreviousMap = require('./previous-map') +let terminalHighlight = require('./terminal-highlight') -export default class Input { - constructor(css, opts = { }) { - this.css = css.toString(); +let lineToIndexCache = Symbol('lineToIndexCache') - if ( this.css[0] == '\uFEFF' || this.css[0] == '\uFFFE' ) { - this.css = this.css.slice(1); - } +let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator) +let pathAvailable = Boolean(resolve && isAbsolute) - this.safe = !!opts.safe; +function getLineToIndex(input) { + if (input[lineToIndexCache]) return input[lineToIndexCache] + let lines = input.css.split('\n') + let lineToIndex = new Array(lines.length) + let prevIndex = 0 - if ( opts.from ) this.file = path.resolve(opts.from); + for (let i = 0, l = lines.length; i < l; i++) { + lineToIndex[i] = prevIndex + prevIndex += lines[i].length + 1 + } - var map = new PreviousMap(this.css, opts, this.id); - if ( map.text ) { - this.map = map; - var file = map.consumer().file; - if ( !this.file && file ) this.file = this.mapResolve(file); - } + input[lineToIndexCache] = lineToIndex + return lineToIndex +} - if ( this.file ) { - this.from = this.file; - } else { - sequence += 1; - this.id = ''; - this.from = this.id; - } - if ( this.map ) this.map.file = this.from; +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) } - // Throw syntax error from this input - error(message, line, column) { - var error = new CssSyntaxError(message); + 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 + } - var origin = this.origin(line, column); - if ( origin ) { - error = new CssSyntaxError(message, origin.line, origin.column, - origin.source, origin.file); + fromLineAndColumn(line, column) { + let lineToIndex = getLineToIndex(this) + let index = lineToIndex[line - 1] + return index + column - 1 + } - error.generated = { - line: line, - column: column, - source: this.css - }; - if ( this.file ) error.generated.file = this.file; + 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 { - error = new CssSyntaxError(message, line, column, - this.css, this.file); + min = mid + break } + } + } + return { + col: offset - lineToIndex[min] + 1, + line: min + 1 + } + } - return error; + mapResolve(file) { + if (/^\w+:\/\//.test(file)) { + return file } + return resolve(this.map.consumer().sourceRoot || this.map.root || '.', file) + } - // Get origin position of code if source map was given - origin(line, column) { - if ( !this.map ) return false; - var consumer = this.map.consumer(); + origin(line, column, endLine, endColumn) { + if (!this.map) return false + let consumer = this.map.consumer() - var from = consumer.originalPositionFor({ line, column }); - if ( !from.source ) return false; + let from = consumer.originalPositionFor({ column: column - 1, line }) + if (!from.source) return false - var result = { - file: this.mapResolve(from.source), - line: from.line, - column: from.column - }; + 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 + } - var source = consumer.sourceContentFor(result.file); - if ( source ) result.source = source; + let fromUrl - return result; + if (isAbsolute(from.source)) { + fromUrl = pathToFileURL(from.source) + } else { + fromUrl = new URL( + from.source, + this.map.consumer().sourceRoot || pathToFileURL(this.map.mapFile) + ) } - // Return path relative from source map root - mapResolve(file) { - return path.resolve(this.map.consumer().sourceRoot || '.', file); + 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.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.js b/lib/list.js index 8270c93e9..3e879d7f1 100644 --- a/lib/list.js +++ b/lib/list.js @@ -1,73 +1,59 @@ -// Methods to parse list and split it to array -export default { - - // Split string to array by separator symbols with function and inside strings - // cheching - split(string, separators, last) { - var array = []; - var current = ''; - var split = false; - - var func = 0; - var quote = false; - var escape = false; - - for ( var i = 0; i < string.length; i++ ) { - var 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 ) { - for ( var j = 0; j < separators.length; j++ ) { - if ( letter == separators[j] ) split = true; - } - } - - if ( split ) { - if ( current !== '' ) array.push(current.trim()); - current = ''; - split = false; - } else { - current += letter; - } +'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 } - - if ( last || current !== '' ) array.push(current.trim()); - return array; - }, - - // Split list devided by space: - // - // list.space('a b') #=> ['a', 'b'] - // - // It check for fuction and strings: - // - // list.space('calc(1px + 1em) "b c"') #=> ['calc(1px + 1em)', '"b c"'] - space(string) { - return this.split(string, [' ', "\n", "\t"]); - }, - - // Split list devided by comma - // - // list.comma('a, b') #=> ['a', 'b'] - // - // It check for fuction and strings: - // - // list.comma('rgba(0, 0, 0, 0) white') #=> ['rgba(0, 0, 0, 0)', '"white"'] - comma(string) { - return this.split(string, [','], true); + } 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.js b/lib/map-generator.js index 9c699b9a2..df880ac99 100644 --- a/lib/map-generator.js +++ b/lib/map-generator.js @@ -1,259 +1,376 @@ -import Result from './result'; - -import { Base64 } from 'js-base64'; -import mozilla from 'source-map'; -import path from 'path'; - -// All tools to generate source maps -export default class MapGenerator { - constructor(root, opts) { - this.root = root; - this.opts = opts; - this.mapOpts = opts.map || { }; +'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() + } - // Should map be generated - isMap() { - if ( typeof(this.opts.map) != 'undefined' ) { - return !!this.opts.map; - } else { - return this.previous().length > 0; + 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)) + : '' + }) } - // Return source map arrays from previous compilation step (like Sass) - previous() { - if ( !this.previousMaps ) { - this.previousMaps = []; - this.root.eachInside( (node) => { - if ( node.source && node.source.input.map ) { - var map = node.source.input.map; - if ( this.previousMaps.indexOf(map) == -1 ) { - this.previousMaps.push(map); - } - } - }); - } + if (this.isSourcesContent()) this.setSourcesContent() + if (this.root && this.previous().length > 0) this.applyPrevMaps() + if (this.isAnnotation()) this.addAnnotation() - return this.previousMaps; + 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: '' } - // Should we inline source map to annotation comment - isInline() { - if ( typeof(this.mapOpts.inline) != 'undefined' ) { - return this.mapOpts.inline; + 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) } - - var annotation = this.mapOpts.annotation; - if ( typeof(annotation) != 'undefined' && annotation !== true ) { - return false; + } + + 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) + } } + } + }) + } - if ( this.previous().length ) { - return this.previous().some( i => i.inline ); - } else { - return true; - } + isAnnotation() { + if (this.isInline()) { + return true } - - // Should we set sourcesContent - 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; - } + if (typeof this.mapOpts.annotation !== 'undefined') { + return this.mapOpts.annotation } - - // Clear source map annotation comment - clearAnnotation() { - if ( this.mapOpts.annotation === false ) return; - - var node; - for ( var i = this.root.nodes.length - 1; i >= 0; i-- ) { - node = this.root.nodes[i]; - if ( node.type != 'comment' ) continue; - if ( node.text.match(/^# sourceMappingURL=/) ) { - this.root.remove(i); - return; - } - } + if (this.previous().length) { + return this.previous().some(i => i.annotation) } + return true + } - // Set origin CSS content - setSourcesContent() { - var already = { }; - this.root.eachInside( (node) => { - if ( node.source ) { - var from = node.source.input.from; - if ( from && !already[from] ) { - already[from] = true; - var relative = this.relative(from); - this.map.setSourceContent(relative, node.source.input.css); - } - } - }); + isInline() { + if (typeof this.mapOpts.inline !== 'undefined') { + return this.mapOpts.inline } - // Apply source map from previous compilation step (like Sass) - applyPrevMaps() { - for ( var prev of this.previous() ) { - var from = this.relative(prev.file); - var root = prev.root || path.dirname(prev.file); - var map; - - if ( this.mapOpts.sourcesContent === false ) { - map = new mozilla.SourceMapConsumer(prev.text); - map.sourcesContent = map.sourcesContent.map( i => null ); - } else { - map = prev.consumer(); - } - - this.map.applySourceMap(map, from, this.relative(root)); - } + let annotation = this.mapOpts.annotation + if (typeof annotation !== 'undefined' && annotation !== true) { + return false } - // Should we add annotation comment - 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; - } + if (this.previous().length) { + return this.previous().some(i => i.inline) } + return true + } - // Add source map annotation comment if it is needed - addAnnotation() { - var content; + isMap() { + if (typeof this.opts.map !== 'undefined') { + return !!this.opts.map + } + return this.previous().length > 0 + } - if ( this.isInline() ) { - content = "data:application/json;base64," + - Base64.encode( this.map.toString() ); + 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' + } + } - } else if ( typeof(this.mapOpts.annotation) == 'string' ) { - content = this.mapOpts.annotation; + 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 - } else { - content = this.outputFile() + '.map'; - } + let from = this.opts.to ? dirname(this.opts.to) : '.' - this.css += "\n/*# sourceMappingURL=" + content + " */"; + if (typeof this.mapOpts.annotation === 'string') { + from = dirname(resolve(from, this.mapOpts.annotation)) } - // Return output CSS file path - 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'; - } + 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 Result object with map - generateMap() { - this.stringify(); - 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]; + 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) } - - // Return path relative from output CSS file - relative(file) { - var 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.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)) } + } - // Return path of node source for map - sourcePath(node) { - return this.relative(node.source.input.from); + toBase64(str) { + if (Buffer) { + return Buffer.from(str).toString('base64') + } else { + return window.btoa(unescape(encodeURIComponent(str))) } + } - // Return CSS string and source map - stringify() { - this.css = ''; - this.map = new mozilla.SourceMapGenerator({ file: this.outputFile() }); - - var line = 1; - var column = 1; - - var lines, last; - var builder = (str, node, type) => { - this.css += str; - - if ( node && node.source && node.source.start && type != 'end' ) { - this.map.addMapping({ - source: this.sourcePath(node), - original: { - line: node.source.start.line, - column: node.source.start.column - 1 - }, - generated: { - line: line, - column: column - 1 - } - }); - } - - lines = str.match(/\n/g); - if ( lines ) { - line += lines.length; - last = str.lastIndexOf("\n"); - column = str.length - last; - } else { - column = column + str.length; - } + toFileUrl(path) { + let cached = this.memoizedFileURLs.get(path) + if (cached) return cached - if ( node && node.source && node.source.end && type != 'start' ) { - this.map.addMapping({ - source: this.sourcePath(node), - original: { - line: node.source.end.line, - column: node.source.end.column - }, - generated: { - line: line, - column: column - } - }); - } - }; + if (pathToFileURL) { + let fileURL = pathToFileURL(path).toString() + this.memoizedFileURLs.set(path, fileURL) - this.root.stringify(builder); + return fileURL + } else { + throw new Error( + '`map.absolute` option is not available in this PostCSS build' + ) } + } - // Return Result object with or without map - generate() { - this.clearAnnotation(); + toUrl(path) { + let cached = this.memoizedURLs.get(path) + if (cached) return cached - if ( this.isMap() ) { - return this.generateMap(); - } else { - return [this.root.toString()]; - } + 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.js b/lib/node.js index affb56710..a298d3656 100644 --- a/lib/node.js +++ b/lib/node.js @@ -1,419 +1,494 @@ -import CssSyntaxError from './css-syntax-error'; - -// Default code style -const defaultStyle = { - colon: ': ', - indent: ' ', - beforeDecl: '\n', - beforeRule: '\n', - beforeOpen: ' ', - beforeClose: '\n', - beforeComment: '\n', - after: '\n', - emptyBody: '', - commentLeft: ' ', - commentRight: ' ' -}; - -// Recursivly clone objects -var cloneNode = function (obj, parent) { - if ( typeof(obj) != 'object' ) return obj; - var cloned = new obj.constructor(); - - for ( var i in obj ) { - if ( !obj.hasOwnProperty(i) ) continue; - var value = obj[i]; - - if ( i == 'parent' && typeof(value) == 'object' ) { - if (parent) cloned[i] = parent; - } else if ( i == 'source' ) { - cloned[i] = value; - } else if ( value instanceof Array ) { - cloned[i] = value.map( i => cloneNode(i, cloned) ); - } else if ( i != 'before' && i != 'after' && - i != 'between' && i != 'semicolon' ) { - cloned[i] = cloneNode(value, cloned); +'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; -}; + return cloned +} -// Some common methods for all CSS nodes -export default class Node { - constructor(defaults = { }) { - for ( var name in defaults ) { - this[name] = defaults[name]; - } - } +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 + } - // Return error to mark error in your plugin syntax: - // - // if ( wrongVariable ) { - // throw decl.error('Wrong variable'); - // } - // - // You can also get origin line and column from previous source map: - // - // if ( deprecatedSyntax ) { - // var error = decl.error('Deprecated syntax'); - // console.warn(error.toString()); - // } - error(message) { - if ( this.source ) { - var pos = this.source.start; - return this.source.input.error(message, pos.line, pos.column); - } else { - return new CssSyntaxError(message); - } - } + let column = 1 + let line = 1 + let offset = 0 - // Remove this node from parent - // - // decl.removeSelf(); - // - // Note, that removing by index is faster: - // - // rule.each( (decl, i) => rule.remove(i) ); - removeSelf() { - if ( this.parent ) { - this.parent.remove(this); - } - this.parent = undefined; - return this; + for (let i = 0; i < inputCSS.length; i++) { + if (line === position.line && column === position.column) { + offset = i + break } - // Shortcut to insert nodes before and remove self. - // - // importNode.replace( loadedRoot ); - replace(nodes) { - this.parent.insertBefore(this, nodes); - this.parent.remove(this); - return this; + if (inputCSS[i] === '\n') { + column = 1 + line += 1 + } else { + column += 1 } + } - // Return CSS string of current node - // - // decl.toString(); //=> " color: black" - toString() { - var result = ''; - var builder = (str) => result += str; - this.stringify(builder); - return result; - } + return offset +} - // Clone current node - // - // rule.append( decl.clone() ); - // - // You can override properties while cloning: - // - // rule.append( decl.clone({ value: '0' }) ); - clone(overrides = { }) { - var cloned = cloneNode(this); - for ( var name in overrides ) { - cloned[name] = overrides[name]; +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) + } } - return cloned; + } else { + this[name] = defaults[name] + } } - - // Clone node and insert clone before current one. - // It accept properties to change in clone and return new node. - // - // decl.cloneBefore({ prop: '-webkit-' + del.prop }); - cloneBefore(overrides = { }) { - var cloned = this.clone(overrides); - this.parent.insertBefore(this, cloned); - return cloned; + } + + 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 + } - // Clone node and insert clone after current one. - // It accept properties to change in clone and return new node. - // - // decl.cloneAfter({ value: convertToRem(decl.value) }); - cloneAfter(overrides = { }) { - var cloned = this.clone(overrides); - this.parent.insertAfter(this, cloned); - return cloned; - } + after(add) { + this.parent.insertAfter(this, add) + return this + } - // Replace with node by another one. - // - // decl.replaceWith(fixedDecl); - replaceWith(node) { - this.parent.insertBefore(this, node); - this.removeSelf(); - return this; + assign(overrides = {}) { + for (let name in overrides) { + this[name] = overrides[name] } - - // Remove node from current place and put to end of new one. - // It will also clean node code styles, but will keep `between` if old - // parent and new parent has same root. - // - // rule.moveTo(atRule); - moveTo(container) { - this.cleanStyles(this.root() == container.root()); - this.removeSelf(); - container.append(this); - return this; + 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] } - - // Remove node from current place and put to before other node. - // It will also clean node code styles, but will keep `between` if old - // parent and new parent has same root. - // - // rule.moveBefore(rule.parent); - moveBefore(node) { - this.cleanStyles(this.root() == node.root()); - this.removeSelf(); - node.parent.insertBefore(node, this); - return this; + 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 + ) } - - // Remove node from current place and put to after other node. - // It will also clean node code styles, but will keep `between` if old - // parent and new parent has same root. - // - // rule.moveAfter(rule.parent); - moveAfter(node) { - this.cleanStyles(this.root() == node.root()); - this.removeSelf(); - node.parent.insertAfter(node, this); - return this; + 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 + } } - - // Return next node in parent. If current node is last one, - // method will return `undefined`. - // - // var next = decl.next(); - // if ( next && next.prop == removePrefix(decl.prop) ) { - // decl.removeSelf(); - // } - next() { - var index = this.parent.index(this); - return this.parent.nodes[index + 1]; + } + + /* 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 + } } - - // Return previous node in parent. If current node is first one, - // method will return `undefined`. - // - // var prev = decl.prev(); - // if ( prev && removePrefix(prev.prop) == decl.prop) ) { - // prev.removeSelf(); - // } - prev() { - var index = this.parent.index(this); - return this.parent.nodes[index - 1]; + } + + 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 + } } - // Remove `parent` node on cloning to fix circular structures - toJSON() { - var fixed = { }; - - for ( var name in this ) { - if ( !this.hasOwnProperty(name) ) continue; - if ( name == 'parent' ) continue; - var value = this[name]; - - if ( value instanceof Array ) { - fixed[name] = value.map( (i) => { - return (typeof(i) == 'object' && i.toJSON) ? i.toJSON() : i; - }); - } else if ( typeof(value) == 'object' && value.toJSON ) { - fixed[name] = value.toJSON(); - } else { - fixed[name] = value; - } + 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 } - return fixed; + 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) + } } - // Copy code style from first node with same type - style(own, detect) { - var value; - if ( !detect ) detect = own; + 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 + } + } - // Already had - if ( own ) { - value = this[own]; - if ( typeof(value) != 'undefined' ) return value; - } + return { end, start } + } - var parent = this.parent; + raw(prop, defaultType) { + let str = new Stringifier() + return str.raw(this, prop, defaultType) + } - // Hack for first rule in CSS - if ( detect == 'before' ) { - if ( !parent || (parent.type == 'root' && parent.first == this) ) { - return ''; - } + 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) } + } - // Floating child without parent - if ( !parent ) return defaultStyle[detect]; + if (!foundSelf) { + this.remove() + } + } - // Detect style by other nodes - var root = this.root(); - if ( !root.styleCache ) root.styleCache = { }; - if ( typeof(root.styleCache[detect]) != 'undefined' ) { - return root.styleCache[detect]; - } + return this + } - if ( detect == 'semicolon' ) { - root.eachInside( (i) => { - if ( i.nodes && i.nodes.length && i.last.type == 'decl' ) { - value = i.semicolon; - if ( typeof(value) != 'undefined' ) return false; - } - }); - } else if ( detect == 'emptyBody' ) { - root.eachInside( (i) => { - if ( i.nodes && i.nodes.length === 0 ) { - value = i.after; - if ( typeof(value) != 'undefined' ) return false; - } - }); - } else if ( detect == 'indent' ) { - root.eachInside( (i) => { - var p = i.parent; - if ( p && p != root && p.parent && p.parent == root ) { - if ( typeof(i.before) != 'undefined' ) { - var parts = i.before.split('\n'); - value = parts[parts.length - 1]; - value = value.replace(/[^\s]/g, ''); - return false; - } - } - }); - } else if ( detect == 'beforeComment' ) { - root.eachComment( (i) => { - if ( typeof(i.before) != 'undefined' ) { - value = i.before; - if ( value.indexOf('\n') != -1 ) { - value = value.replace(/[^\n]+$/, ''); - } - return false; - } - }); - if ( typeof(value) == 'undefined' ) { - value = this.style(null, 'beforeDecl'); - } - } else if ( detect == 'beforeDecl' ) { - root.eachDecl( (i) => { - if ( typeof(i.before) != 'undefined' ) { - value = i.before; - if ( value.indexOf('\n') != -1 ) { - value = value.replace(/[^\n]+$/, ''); - } - return false; - } - }); - if ( typeof(value) == 'undefined' ) { - value = this.style(null, 'beforeRule'); - } - } else if ( detect == 'beforeRule' ) { - root.eachInside( (i) => { - if ( i.nodes && (i.parent != root || root.first != i) ) { - if ( typeof(i.before) != 'undefined' ) { - value = i.before; - if ( value.indexOf('\n') != -1 ) { - value = value.replace(/[^\n]+$/, ''); - } - return false; - } - } - }); - } else if ( detect == 'beforeClose' ) { - root.eachInside( (i) => { - if ( i.nodes && i.nodes.length > 0 ) { - if ( typeof(i.after) != 'undefined' ) { - value = i.after; - if ( value.indexOf('\n') != -1 ) { - value = value.replace(/[^\n]+$/, ''); - } - return false; - } - } - }); - } else if ( detect == 'before' || detect == 'after' ) { - if ( this.type == 'decl' ) { - value = this.style(null, 'beforeDecl'); - } else if ( this.type == 'comment' ) { - value = this.style(null, 'beforeComment'); - } else if ( detect == 'before' ) { - value = this.style(null, 'beforeRule'); + 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 { - value = this.style(null, 'beforeClose'); + fixedArray[i] = item } - - var node = this.parent; - var depth = 0; - while ( node && node.type != 'root' ) { - depth += 1; - node = node.parent; - } - - if ( value.indexOf('\n') != -1 ) { - var indent = this.style(null, 'indent'); - if ( indent.length ) { - for ( var step = 0; step < depth; step++ ) value += indent; - } - } - - return value; - } else if ( detect == 'colon' ) { - root.eachDecl( (i) => { - if ( typeof(i.between) != 'undefined' ) { - value = i.between.replace(/[^\s:]/g, ''); - return false; - } - }); - } else if ( detect == 'beforeOpen' ) { - root.eachInside( (i) => { - if ( i.type != 'decl' ) { - value = i.between; - if ( typeof(value) != 'undefined' ) return false; - } - }); + } + } 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 { - root.eachInside( (i) => { - value = i[own]; - if ( typeof(value) != 'undefined' ) return false; - }); + fixed[name] = value } - - if ( typeof(value) == 'undefined' ) value = defaultStyle[detect]; - - root.styleCache[detect] = value; - return value; + } } - // Return top parent , parent of parents. - root() { - var result = this; - while ( result.parent ) result = result.parent; - return result; + let fixed = holderOfRoot[0] + if (emitInputs) { + fixed.inputs = [...inputs.keys()].map(input => input.toJSON()) } - // Recursivelly remove all code style properties (`before` and `between`). - cleanStyles(keepBetween) { - delete this.before; - delete this.after; - if ( !keepBetween ) delete this.between; + return fixed + } - if ( this.nodes ) { - for ( var node of this.nodes ) node.cleanStyles(keepBetween); - } - } - - // Use raw value if origin was not changed - stringifyRaw(prop) { - var value = this[prop]; - var raw = this['_' + prop]; - if ( raw && raw.value === value ) { - return raw.raw; - } else { - return value; - } + 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.js b/lib/parse.js index 4e57c1635..00a1037aa 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1,12 +1,42 @@ -import Parser from './parser'; -import Input from './input'; +'use strict' -export default function (css, opts) { - var input = new Input(css, opts); +let Container = require('./container') +let Input = require('./input') +let Parser = require('./parser') - var parser = new Parser(input); - parser.tokenize(); - parser.loop(); +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; + return parser.root } + +module.exports = parse +parse.default = parse + +Container.registerParse(parse) diff --git a/lib/parser.js b/lib/parser.js index 4a788eae1..f549f56d1 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,463 +1,621 @@ -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'; - -// CSS parser -export default class Parser { - constructor(input) { - this.input = input; - - this.pos = 0; - this.root = new Root(); - this.current = this.root; - this.spaces = ''; - this.semicolon = false; - - this.root.source = { input: input }; - if ( input.map ) this.root.prevMap = input.map; - } - - tokenize() { - this.tokens = tokenizer(this.input); - } - - loop() { - var token; - while ( this.pos < this.tokens.length ) { - token = this.tokens[this.pos]; - - switch ( token[0] ) { - case 'word': - case ':': - this.word(token); - break; +'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 +} - case '}': - this.end(token); - break; +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 + } +} - case 'comment': - this.comment(token); - break; +function tokensToString(tokens, from, to) { + let result = '' + for (let i = from; i < to; i++) result += tokens[i][1] + return result +} - case 'at-word': - this.atrule(token); - break; +class Parser { + constructor(input) { + this.input = input - case '{': - this.emptyRule(token); - break; + this.root = new Root() + this.current = this.root + this.spaces = '' + this.semicolon = false - default: - this.spaces += token[1]; - break; - } + this.createTokenizer() + this.root.source = { input, start: { column: 1, line: 1, offset: 0 } } + } - this.pos += 1; - } - this.endFile(); + atrule(token) { + let node = new AtRule() + node.name = token[1].slice(1) + if (node.name === '') { + this.unnamedAtrule(node, token) } - - comment(token) { - var node = new Comment(); - this.init(node, token[2], token[3]); - node.source.end = { line: token[4], column: token[5] }; - - var text = token[1].slice(2, -2); - if ( text.match(/^\s*$/) ) { - node.left = text; - node.text = ''; - node.right = ''; + 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 { - var match = text.match(/^(\s*)([^]*[^\s])(\s*)$/); - node.left = match[1]; - node.text = match[2]; - node.right = match[3]; + params.push(token) } + } else { + params.push(token) + } + + if (this.tokenizer.endOfFile()) { + last = true + break + } } - emptyRule(token) { - var node = new Rule(); - this.init(node, token[2], token[3]); - node.between = ''; - node.selector = ''; - this.current = node; + 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 = '' } - word() { - var token; - var end = false; - var type = null; - var colon = false; - var bracket = null; - var brackets = 0; - - var start = this.pos; - this.pos += 1; - while ( true ) { - token = this.tokens[this.pos]; - if ( !token ) { - this.pos -= 1; - end = true; - break; - } - - type = token[0]; - if ( type == '(' ) { - if ( !bracket ) bracket = token; - brackets += 1; - - } else if ( type == ')' ) { - brackets -= 1; - if ( brackets === 0 ) bracket = null; - - } else if ( brackets === 0 ) { - if ( type == ';' ) { - if ( colon ) { - this.decl(this.tokens.slice(start, this.pos + 1)); - return; - } else { - break; - } - - } else if ( type == '{' ) { - this.rule(this.tokens.slice(start, this.pos + 1)); - return; - - } else if ( type == '}' ) { - this.pos -= 1; - end = true; - break; - - } else if ( type == 'at-word' ) { - this.pos -= 1; - break; - - } else { - if ( type == ':' ) colon = true; - } - } - - this.pos += 1; - } - - if ( brackets > 0 && !this.input.safe ) { - throw this.input.error('Unclosed bracket', bracket[2], bracket[3]); - } - - if ( end && colon ) { - while ( this.pos > start ) { - token = this.tokens[this.pos][0]; - if ( token != 'space' && token != 'comment' ) break; - this.pos -= 1; - } - this.decl(this.tokens.slice(start, this.pos + 1)); - return; - } - - if ( this.input.safe ) { - var buffer = this.tokens.slice(start, this.pos + 1); - this.spaces += buffer.map(i => i[1]).join(''); + 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 { - token = this.tokens[start]; - throw this.input.error('Unknown word', token[2], token[3]); + 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] + } + } - rule(tokens) { - tokens.pop(); + createTokenizer() { + this.tokenizer = tokenizer(this.input) + } - var node = new Rule(); - this.init(node, tokens[0][2], tokens[0][3]); + decl(tokens, customProperty) { + let node = new Declaration() + this.init(node, tokens[0][2]) - node.between = this.spacesFromEnd(tokens); - this.raw(node, 'selector', tokens); - this.current = node; + let last = tokens[tokens.length - 1] + if (last[0] === ';') { + this.semicolon = true + tokens.pop() } - decl(tokens) { - var node = new Declaration(); - this.init(node); + node.source.end = this.getPosition( + last[3] || last[2] || findLastWithPosition(tokens) + ) + node.source.end.offset++ - var 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] }; - } + 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) - while ( tokens[0][0] != 'word' ) { - node.before += tokens.shift()[1]; - } - node.source.start = { line: tokens[0][2], column: tokens[0][3] }; - - node.prop = tokens.shift()[1]; - node.between = ''; - - var token; - while ( tokens.length ) { - token = tokens.shift(); - - if ( token[0] == ':' ) { - node.between += token[1]; - break; - } else if ( token[0] != 'space' && token[0] != 'comment' ) { - this.unknownWord(node, token, tokens); - } else { - node.between += token[1]; - } - } + if (node.prop[0] === '_' || node.prop[0] === '*') { + node.raws.before += node.prop[0] + node.prop = node.prop.slice(1) + } - if ( node.prop[0] == '_' || node.prop[0] == '*' ) { - node.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 } - node.between += this.spacesFromStart(tokens); - - if ( this.input.safe ) this.checkMissedSemicolon(tokens); - - for ( var i = tokens.length - 1; i > 0; i-- ) { - token = tokens[i]; - if ( token[1] == '!important' ) { - node.important = true; - var string = this.stringFrom(tokens, i); - string = this.spacesFromEnd(tokens) + string; - if ( string != ' !important' ) node._important = string; - break; - } else if ( token[0] != 'space' && token[0] != 'comment' ) { - break; - } + if (str.trim().startsWith('!')) { + node.important = true + node.raws.important = str + tokens = cache } + } - this.raw(node, 'value', tokens); - - if ( node.value.indexOf(':') != -1 && !this.input.safe ) { - this.checkMissedSemicolon(tokens); - } + if (token[0] !== 'space' && token[0] !== 'comment') { + break + } } - atrule(token) { - var node = new AtRule(); - node.name = token[1].slice(1); - if ( node.name === '' ) { - if ( this.input.safe ) { - node.name = ''; - } else { - throw this.input.error( - 'At-rule without name', token[2], token[3]); - } - } - this.init(node, token[2], token[3]); - - var next; - var last = false; - var open = false; - var params = []; - while ( true ) { - this.pos += 1; - token = this.tokens[this.pos]; - - if ( !token ) { - last = true; - break; - } else if ( token[0] == ';' ) { - node.source.end = { line: token[2], column: token[3] }; - this.semicolon = true; - break; - } else if ( token[0] == '{' ) { - open = true; - break; - } else { - params.push(token); - } - } + let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment') - node.between = this.spacesFromEnd(params); - if ( params.length ) { - node.afterName = this.spacesFromStart(params); - this.raw(node, 'params', params); - if ( last ) { - token = params[ params.length - 1 ]; - node.source.end = { line: token[4], column: token[5] }; - this.spaces = node.between; - node.between = ''; - } - } else { - node.afterName = ''; - node.params = ''; - } - - if ( open ) { - node.nodes = []; - this.current = node; - } + if (hasWord) { + node.raws.between += firstSpaces.map(i => i[1]).join('') + firstSpaces = [] } + this.raw(node, 'value', firstSpaces.concat(tokens), customProperty) - end(token) { - if ( this.current.nodes && this.current.nodes.length ) { - this.current.semicolon = this.semicolon; - } - this.semicolon = false; + 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.after = (this.current.after || '') + this.spaces; - this.spaces = ''; + 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 if ( !this.input.safe ) { - throw this.input.error('Unexpected }', token[2], token[3]); - } else { - this.current.after += '}'; - } + 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.input.safe ) { - var pos = this.current.source.start; - throw this.input.error('Unclosed block', pos.line, pos.column); - } + 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++ + } + } + } - if ( this.current.nodes && this.current.nodes.length ) { - this.current.semicolon = this.semicolon; - } - this.current.after = (this.current.after || '') + this.spaces; + // Helpers - while ( this.current.parent ) { - this.current = this.current.parent; - this.current.after = ''; - } + getPosition(offset) { + let pos = this.input.fromOffset(offset) + return { + column: pos.col, + line: pos.line, + offset } + } - unknownWord(node, token) { - if ( this.input.safe ) { - node.source.start = { line: token[2], column: token[3] }; - node.before += node.prop + node.between; - node.prop = token[1]; - node.between = ''; - } else { - throw this.input.error('Unknown word', token[2], token[3]); + 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() } - checkMissedSemicolon(tokens) { - var prev = null; - var colon = false; - var brackets = 0; - var type, token; - for ( var i = 0; i < tokens.length; i++ ) { - token = tokens[i]; - type = token[0]; - - if ( type == '(' ) { - brackets += 1; - } else if ( type == ')' ) { - brackets -= 0; - } else if ( brackets === 0 && type == ':' ) { - if ( prev[0] == 'word' && prev[1] == 'progid' ) { - continue; - } else { - colon = i; - break; - } - } + if (this.tokenizer.endOfFile()) end = true + if (brackets.length > 0) this.unclosedBracket(bracket) - prev = token; + 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()) } - - if ( colon === false ) return; - - if ( this.input.safe ) { - var split; - for ( split = colon - 1; split >= 0; split-- ) { - if ( tokens[split][0] == 'word' ) break; - } - for ( split -= 1; split >= 0; split-- ) { - if ( tokens[split][0] != 'space' ) { - split += 1; - break; - } - } - var other = tokens.splice(split, tokens.length - split); - this.decl(other); + } + 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 { - var founded = 0; - for ( var 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[4], token[5]); + clean = false } + } else { + value += token[1] + } } - - // Helpers - - init(node, line, column) { - this.current.push(node); - - node.source = { start: { line, column }, input: this.input }; - node.before = this.spaces; - this.spaces = ''; - if ( node.type != 'comment' ) this.semicolon = false; + if (!clean) { + let raw = tokens.reduce((all, i) => all + i[1], '') + node.raws[prop] = { raw, value } } - - raw(node, prop, tokens) { - var token; - var value = ''; - var clean = true; - for ( token of tokens ) { - if ( token[0] == 'comment' ) { - clean = false; - } else { - value += token[1]; - } - } - if ( !clean ) { - var origin = ''; - for ( token of tokens ) origin += token[1]; - node['_' + prop] = { value: value, raw: origin }; - } - node[prop] = 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 } - - spacesFromEnd(tokens) { - var next; - var spaces = ''; - while ( tokens.length ) { - next = tokens[ tokens.length - 1 ][0]; - if ( next != 'space' && next != 'comment' ) break; - spaces += tokens.pop()[1]; - } - return 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] } - - spacesFromStart(tokens) { - var next; - var spaces = ''; - while ( tokens.length ) { - next = tokens[0][0]; - if ( next != 'space' && next != 'comment' ) break; - spaces += tokens.shift()[1]; - } - return spaces; + 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) { - var result = ''; - for ( var i = from; i < tokens.length; i++ ) { - result += tokens[i][1]; - } - tokens.splice(from, tokens.length - from); - return result; + stringFrom(tokens, from) { + let result = '' + for (let i = from; i < tokens.length; i++) { + result += tokens[i][1] } + tokens.splice(from, tokens.length - from) + return result + } + + unclosedBlock() { + let pos = this.current.source.start + throw this.input.error('Unclosed block', pos.line, pos.column) + } + + unclosedBracket(bracket) { + throw this.input.error( + 'Unclosed bracket', + { offset: bracket[2] }, + { offset: bracket[2] + 1 } + ) + } + + unexpectedClose(token) { + throw this.input.error( + 'Unexpected }', + { offset: token[2] }, + { offset: token[2] + 1 } + ) + } + + unknownWord(tokens) { + throw this.input.error( + 'Unknown word ' + tokens[0][1], + { offset: tokens[0][2] }, + { offset: tokens[0][2] + tokens[0][1].length } + ) + } + + unnamedAtrule(node, token) { + throw this.input.error( + 'At-rule without name', + { offset: token[2] }, + { offset: token[2] + token[1].length } + ) + } } + +module.exports = Parser diff --git a/lib/postcss.d.mts b/lib/postcss.d.mts new file mode 100644 index 000000000..eaec8681f --- /dev/null +++ b/lib/postcss.d.mts @@ -0,0 +1,66 @@ +export { + // Type-only exports + AcceptedPlugin, + AnyNode, + atRule, + AtRule, + AtRuleProps, + Builder, + ChildNode, + ChildProps, + comment, + Comment, + CommentProps, + Container, + ContainerProps, + CssSyntaxError, + decl, + Declaration, + DeclarationProps, + // postcss function / namespace + default, + document, + Document, + DocumentProps, + FilePosition, + fromJSON, + Helpers, + Input, + JSONHydrator, + // This is a class, but it’s not re-exported. That’s why it’s exported as type-only here. + type LazyResult, + list, + Message, + Node, + NodeErrorOptions, + NodeProps, + OldPlugin, + parse, + Parser, + // @ts-expect-error This value exists, but it’s untyped. + plugin, + Plugin, + PluginCreator, + Position, + Postcss, + ProcessOptions, + Processor, + Result, + root, + Root, + RootProps, + rule, + Rule, + RuleProps, + Source, + SourceMap, + SourceMapOptions, + Stringifier, + // Value exports from postcss.mjs + stringify, + Syntax, + TransformCallback, + Transformer, + Warning, + WarningOptions +} from './postcss.js' diff --git a/lib/postcss.d.ts b/lib/postcss.d.ts new file mode 100644 index 000000000..41bf40f9b --- /dev/null +++ b/lib/postcss.d.ts @@ -0,0 +1,464 @@ +import { RawSourceMap, SourceMapGenerator } from 'source-map-js' + +import AtRule, { AtRuleProps } from './at-rule.js' +import Comment, { CommentProps } from './comment.js' +import Container, { ContainerProps, NewChild } from './container.js' +import CssSyntaxError from './css-syntax-error.js' +import Declaration, { DeclarationProps } from './declaration.js' +import Document, { DocumentProps } from './document.js' +import Input, { FilePosition } from './input.js' +import LazyResult from './lazy-result.js' +import list from './list.js' +import Node, { + AnyNode, + ChildNode, + ChildProps, + NodeErrorOptions, + NodeProps, + Position, + Source +} from './node.js' +import Processor from './processor.js' +import Result, { Message } from './result.js' +import Root, { RootProps } from './root.js' +import Rule, { RuleProps } from './rule.js' +import Warning, { WarningOptions } from './warning.js' + +type DocumentProcessor = ( + document: Document, + helper: postcss.Helpers +) => Promise | void +type RootProcessor = ( + root: Root, + helper: postcss.Helpers +) => Promise | void +type DeclarationProcessor = ( + decl: Declaration, + helper: postcss.Helpers +) => Promise | void +type RuleProcessor = ( + rule: Rule, + helper: postcss.Helpers +) => Promise | void +type AtRuleProcessor = ( + atRule: AtRule, + helper: postcss.Helpers +) => Promise | void +type CommentProcessor = ( + comment: Comment, + helper: postcss.Helpers +) => Promise | void + +interface Processors { + /** + * Will be called on all`AtRule` nodes. + * + * Will be called again on node or children changes. + */ + AtRule?: { [name: string]: AtRuleProcessor } | AtRuleProcessor + + /** + * Will be called on all `AtRule` nodes, when all children will be processed. + * + * Will be called again on node or children changes. + */ + AtRuleExit?: { [name: string]: AtRuleProcessor } | AtRuleProcessor + + /** + * Will be called on all `Comment` nodes. + * + * Will be called again on node or children changes. + */ + Comment?: CommentProcessor + + /** + * Will be called on all `Comment` nodes after listeners + * for `Comment` event. + * + * Will be called again on node or children changes. + */ + CommentExit?: CommentProcessor + + /** + * Will be called on all `Declaration` nodes after listeners + * for `Declaration` event. + * + * Will be called again on node or children changes. + */ + Declaration?: { [prop: string]: DeclarationProcessor } | DeclarationProcessor + + /** + * Will be called on all `Declaration` nodes. + * + * Will be called again on node or children changes. + */ + DeclarationExit?: + | { [prop: string]: DeclarationProcessor } + | DeclarationProcessor + + /** + * Will be called on `Document` node. + * + * Will be called again on children changes. + */ + Document?: DocumentProcessor + + /** + * Will be called on `Document` node, when all children will be processed. + * + * Will be called again on children changes. + */ + DocumentExit?: DocumentProcessor + + /** + * Will be called on `Root` node once. + */ + Once?: RootProcessor + + /** + * Will be called on `Root` node once, when all children will be processed. + */ + OnceExit?: RootProcessor + + /** + * Will be called on `Root` node. + * + * Will be called again on children changes. + */ + Root?: RootProcessor + + /** + * Will be called on `Root` node, when all children will be processed. + * + * Will be called again on children changes. + */ + RootExit?: RootProcessor + + /** + * Will be called on all `Rule` nodes. + * + * Will be called again on node or children changes. + */ + Rule?: RuleProcessor + + /** + * Will be called on all `Rule` nodes, when all children will be processed. + * + * Will be called again on node or children changes. + */ + RuleExit?: RuleProcessor +} + +declare namespace postcss { + export { + AnyNode, + AtRule, + AtRuleProps, + ChildNode, + ChildProps, + Comment, + CommentProps, + Container, + ContainerProps, + CssSyntaxError, + Declaration, + DeclarationProps, + Document, + DocumentProps, + FilePosition, + Input, + LazyResult, + list, + Message, + NewChild, + Node, + NodeErrorOptions, + NodeProps, + Position, + Processor, + Result, + Root, + RootProps, + Rule, + RuleProps, + Source, + Warning, + WarningOptions + } + + export type SourceMap = { + toJSON(): RawSourceMap + } & SourceMapGenerator + + export type Helpers = { postcss: Postcss; result: Result } & Postcss + + export interface Plugin extends Processors { + postcssPlugin: string + prepare?: (result: Result) => Processors + } + + export interface PluginCreator { + (opts?: PluginOptions): Plugin | Processor + postcss: true + } + + export interface Transformer extends TransformCallback { + postcssPlugin: string + postcssVersion: string + } + + export interface TransformCallback { + (root: Root, result: Result): Promise | void + } + + export interface OldPlugin extends Transformer { + (opts?: T): Transformer + postcss: Transformer + } + + export type AcceptedPlugin = + | { + postcss: Processor | TransformCallback + } + | OldPlugin + | Plugin + | PluginCreator + | Processor + | TransformCallback + + export interface Parser { + ( + css: { toString(): string } | string, + opts?: Pick + ): RootNode + } + + export interface Builder { + (part: string, node?: AnyNode, type?: 'end' | 'start'): void + } + + export interface Stringifier { + (node: AnyNode, builder: Builder): void + } + + export interface JSONHydrator { + (data: object): Node + (data: object[]): Node[] + } + + export interface Syntax { + /** + * Function to generate AST by string. + */ + parse?: Parser + + /** + * Class to generate string by AST. + */ + stringify?: Stringifier + } + + export interface SourceMapOptions { + /** + * Use absolute path in generated source map. + */ + absolute?: boolean + + /** + * Indicates that PostCSS should add annotation comments to the CSS. + * By default, PostCSS will always add a comment with a path + * to the source map. PostCSS will not add annotations to CSS files + * that do not contain any comments. + * + * By default, PostCSS presumes that you want to save the source map as + * `opts.to + '.map'` and will use this path in the annotation comment. + * A different path can be set by providing a string value for annotation. + * + * If you have set `inline: true`, annotation cannot be disabled. + */ + annotation?: ((file: string, root: Root) => string) | boolean | string + + /** + * Override `from` in map’s sources. + */ + from?: string + + /** + * Indicates that the source map should be embedded in the output CSS + * as a Base64-encoded comment. By default, it is `true`. + * But if all previous maps are external, not inline, PostCSS will not embed + * the map even if you do not set this option. + * + * If you have an inline source map, the result.map property will be empty, + * as the source map will be contained within the text of `result.css`. + */ + inline?: boolean + + /** + * Source map content from a previous processing step (e.g., Sass). + * + * PostCSS will try to read the previous source map + * automatically (based on comments within the source CSS), but you can use + * this option to identify it manually. + * + * If desired, you can omit the previous map with prev: `false`. + */ + prev?: ((file: string) => string) | boolean | object | string + + /** + * Indicates that PostCSS should set the origin content (e.g., Sass source) + * of the source map. By default, it is true. But if all previous maps do not + * contain sources content, PostCSS will also leave it out even if you + * do not set this option. + */ + sourcesContent?: boolean + } + + export interface ProcessOptions { + /** + * Input file if it is not simple CSS file, but HTML with ' }) + is(root.source?.input.css, 'a {} b {}') + is(root.source?.input.document, '') + + let a = root.first as Rule + + // Offset the source location of `a` to mimic syntaxes like `postcss-html` + a.source = { + end: { + column: 12, + line: 1, + offset: 12 + }, + input: a.source!.input, + start: { + column: 8, + line: 1, + offset: 7 + } + } + + equal(a.positionInside(0), { column: 8, line: 1, offset: 7 }) + equal(a.positionInside(1), { column: 9, line: 1, offset: 8 }) +}) + +test('positionBy() returns position', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.positionBy(), { column: 6, line: 1, offset: 5 }) + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) +}) + +test('positionBy() returns position after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) + equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) + + one.remove() + + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) + equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) +}) + +test('positionBy() returns position', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.positionBy(), { column: 6, line: 1, offset: 5 }) + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) +}) + +test('positionBy() returns position after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) + equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) + + one.remove() + + equal(a.positionBy(), { column: 1, line: 1, offset: 0 }) + equal(two.positionBy(), { column: 2, line: 3, offset: 14 }) +}) + +test('positionBy() returns position for word', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.positionBy({ word: 'one' }), { column: 6, line: 1, offset: 5 }) + equal(one.positionBy({ word: 'X' }), { column: 11, line: 1, offset: 10 }) + equal(a.positionBy({ word: '}' }), { column: 14, line: 1, offset: 13 }) +}) + +test('positionBy() returns position for word after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) + equal(two.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) + + one.remove() + + equal(a.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) + equal(two.positionBy({ word: 'two' }), { column: 2, line: 3, offset: 14 }) +}) + +test('positionBy() returns position when offset is missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + + equal(one.positionBy(), { column: 6, line: 1, offset: 5 }) +}) + +test('positionBy() returns position for index', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.positionBy({ index: 1 }), { column: 7, line: 1, offset: 6 }) +}) + +test('positionBy() returns position for index after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.positionBy({ index: 15 }), { column: 3, line: 3, offset: 15 }) + equal(two.positionBy({ index: 1 }), { column: 3, line: 3, offset: 15 }) + + one.remove() + + equal(a.positionBy({ index: 15 }), { column: 3, line: 3, offset: 15 }) + equal(two.positionBy({ index: 1 }), { column: 3, line: 3, offset: 15 }) +}) + +test('positionBy() supports multi-root documents', () => { + let root = parse('a {} b {}', { document: '' }) + is(root.source?.input.css, 'a {} b {}') + is(root.source?.input.document, '') + + let a = root.first as Rule + + // Offset the source location of `a` to mimic syntaxes like `postcss-html` + a.source = { + end: { + column: 12, + line: 1, + offset: 12 + }, + input: a.source!.input, + start: { + column: 8, + line: 1, + offset: 7 + } + } + + // `offset` is present because the `0` index returns `source.start` + equal(a.positionBy({ index: 0 }), { column: 8, line: 1, offset: 7 }) + equal(a.positionBy({ index: 1 }), { column: 9, line: 1, offset: 8 }) + equal(a.positionBy({ word: 'a' }), { column: 8, line: 1, offset: 7 }) +}) + +test('rangeBy() returns range', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy(), { + end: { column: 12, line: 1, offset: 11 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal(one.rangeBy(), { + end: { column: 12, line: 1, offset: 11 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for empty object even after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.rangeBy(), { + end: { column: 10, line: 3, offset: 22 }, + start: { column: 1, line: 1, offset: 0 } + }) + equal(two.rangeBy(), { + end: { column: 9, line: 3, offset: 21 }, + start: { column: 2, line: 3, offset: 14 } + }) + + one.remove() + + equal(a.rangeBy(), { + end: { column: 10, line: 3, offset: 22 }, + start: { column: 1, line: 1, offset: 0 } + }) + equal(two.rangeBy(), { + end: { column: 9, line: 3, offset: 21 }, + start: { column: 2, line: 3, offset: 14 } + }) +}) + +test('rangeBy() returns range', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy(), { + end: { column: 12, line: 1, offset: 11 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal(one.rangeBy(), { + end: { column: 12, line: 1, offset: 11 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for empty object even after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.rangeBy(), { + end: { column: 10, line: 3, offset: 22 }, + start: { column: 1, line: 1, offset: 0 } + }) + equal(two.rangeBy(), { + end: { column: 9, line: 3, offset: 21 }, + start: { column: 2, line: 3, offset: 14 } + }) + + one.remove() + + equal(a.rangeBy(), { + end: { column: 10, line: 3, offset: 22 }, + start: { column: 1, line: 1, offset: 0 } + }) + equal(two.rangeBy(), { + end: { column: 9, line: 3, offset: 21 }, + start: { column: 2, line: 3, offset: 14 } + }) +}) + +test('rangeBy() returns range for word', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy({ word: 'one' }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for word when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal(one.rangeBy({ word: 'one' }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for word even after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + equal(two.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + + one.remove() + + equal(a.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + equal(two.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) +}) + +test('rangeBy() returns range for word even after AST mutations when offsets are missing', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + // @ts-expect-error Testing non-standard AST + if (a.source?.start) delete a.source.start.offset + // @ts-expect-error Testing non-standard AST + if (a.source?.end) delete a.source.end.offset + // @ts-expect-error Testing non-standard AST + if (two.source?.start) delete two.source.start.offset + // @ts-expect-error Testing non-standard AST + if (two.source?.end) delete two.source.end.offset + + equal(a.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + equal(two.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + + one.remove() + + equal(a.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) + equal(two.rangeBy({ word: 'two' }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 2, line: 3, offset: 14 } + }) +}) + +test('rangeBy() returns range for start and end', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal( + one.rangeBy({ end: { column: 9, line: 1 }, start: { column: 7, line: 1 } }), + { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 7, line: 1, offset: 6 } + } + ) +}) + +test('rangeBy() returns range for start and end when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal( + one.rangeBy({ end: { column: 9, line: 1 }, start: { column: 7, line: 1 } }), + { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 7, line: 1, offset: 6 } + } + ) +}) + +test('rangeBy() returns range for start and end after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal( + a.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), + { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + } + ) + equal( + two.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), + { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + } + ) + + one.remove() + + equal( + a.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), + { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + } + ) + equal( + two.rangeBy({ end: { column: 5, line: 3 }, start: { column: 3, line: 3 } }), + { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + } + ) +}) + +test('rangeBy() returns range for index and endIndex', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy({ endIndex: 3, index: 1 }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 7, line: 1, offset: 6 } + }) +}) + +test('rangeBy() returns range for index 0', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + equal(one.rangeBy({ index: 0 }), { + end: { column: 7, line: 1, offset: 6 }, + start: { column: 6, line: 1, offset: 5 } + }) +}) + +test('rangeBy() returns range for index and endIndex when offsets are missing', () => { + let css = parse('a { one: X }') + let a = css.first as Rule + let one = a.first as Declaration + + // @ts-expect-error Testing non-standard AST + if (one.source?.start) delete one.source.start.offset + // @ts-expect-error Testing non-standard AST + if (one.source?.end) delete one.source.end.offset + + equal(one.rangeBy({ endIndex: 3, index: 1 }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 7, line: 1, offset: 6 } + }) +}) + +test('rangeBy() returns range for index and endIndex after AST mutations', () => { + let css = parse('a {\n\tone: 1;\n\ttwo: 2;}') + let a = css.first as Rule + let one = a.first as Declaration + let two = one.next() as Declaration + + equal(a.rangeBy({ endIndex: 17, index: 15 }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + }) + equal(two.rangeBy({ endIndex: 3, index: 1 }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + }) + + one.remove() + + equal(a.rangeBy({ endIndex: 17, index: 15 }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + }) + equal(two.rangeBy({ endIndex: 3, index: 1 }), { + end: { column: 5, line: 3, offset: 17 }, + start: { column: 3, line: 3, offset: 15 } + }) +}) + +test('rangeBy() supports multi-root documents', () => { + let root = parse('a {} b {}', { document: '' }) + is(root.source?.input.css, 'a {} b {}') + is(root.source?.input.document, '') + + let a = root.first as Rule + + // Offset the source location of `a` to mimic syntaxes like `postcss-html` + a.source = { + end: { + column: 12, + line: 1, + offset: 12 + }, + input: a.source!.input, + start: { + column: 8, + line: 1, + offset: 7 + } + } + + equal(a.rangeBy({ endIndex: 1, index: 0 }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 8, line: 1, offset: 7 } + }) + + equal(a.rangeBy({ word: 'a' }), { + end: { column: 9, line: 1, offset: 8 }, + start: { column: 8, line: 1, offset: 7 } + }) +}) + +test.run() diff --git a/test/old-node.js b/test/old-node.js new file mode 100644 index 000000000..9f674c4c1 --- /dev/null +++ b/test/old-node.js @@ -0,0 +1,27 @@ +// eslint-disable-next-line +globalThis = Function('return this')() + +let Module = require('module') +let path = require('path') +let originalRequire = Module.prototype.require + +Module.prototype.require = function (request) { + if (request.startsWith('node:')) { + request = request.slice(5) + } + return originalRequire.call(this, request) +} + +process.env.TS_NODE_COMPILER_OPTIONS = JSON.stringify({ + moduleResolution: 'node' +}) + +require( + path.join( + process.cwd(), + 'node_modules', + 'ts-node', + 'register', + 'transpile-only' + ) +) diff --git a/test/parse.js b/test/parse.js deleted file mode 100644 index 8f681bcec..000000000 --- a/test/parse.js +++ /dev/null @@ -1,189 +0,0 @@ -import Input from '../lib/input'; -import parse from '../lib/parse'; -import Root from '../lib/root'; - -import { expect } from 'chai'; -import path from 'path'; -import fs from 'fs'; - -var read = file => fs.readFileSync(__dirname + '/cases/' + file); - -describe('postcss.parse()', () => { - - it('works with file reads', () => { - var css = fs.readFileSync(__dirname + '/cases/atrule-empty.css'); - expect(parse(css)).to.be.instanceOf(Root); - }); - - describe('empty file', () => { - - it('parses UTF-8 BOM', () => { - var css = parse('\uFEFF@host { a {\f} }'); - expect(css.first.before).to.eql(''); - }); - - it('parses empty file', () => { - expect(parse('', { from: 'a.css' })).to.eql(new Root({ - after: '', - source: { - input: new Input('', { from: 'a.css' }) - } - })); - }); - - it('parses spaces', () => { - expect(parse(' \n', { from: 'a.css' })).to.eql(new Root({ - after: ' \n', - source: { - input: new Input(' \n', { from: 'a.css' }) - } - })); - }); - - }); - - fs.readdirSync(__dirname + '/cases/').forEach( (file) => { - if ( !file.match(/\.css$/) ) return; - - it('parses ' + file, () => { - var css = parse(read(file), { from: '/' + file }); - var json = read(file.replace(/\.css$/, '.json')).toString().trim(); - expect(JSON.stringify(css, null, 4)).to.eql(json); - }); - }); - - it('saves source file', () => { - var css = parse('a {}', { from: 'a.css' }); - expect(css.first.source.input.file).to.eql(path.resolve('a.css')); - expect(css.first.source.input.from).to.eql(path.resolve('a.css')); - }); - - it('saves source file on previous map', () => { - var root1 = parse('a {}', { map: { inline: true } }); - var css = root1.toResult({ map: { inline: true } }).css; - var root2 = parse(css); - expect(root2.first.source.input.file).to.eql(path.resolve('to.css')); - }); - - it('sets unique ID for file without name', () => { - var css1 = parse('a {}'); - var css2 = parse('a {}'); - expect(css1.first.source.input.id).to.match(/^$/); - expect(css1.first.source.input.from).to.match(/^$/); - expect(css2.first.source.input.id) - .to.not.eql(css1.first.source.input.id); - }); - - it('sets parent node', () => { - var css = parse(read('atrule-rules.css')); - - var support = css.first; - var keyframes = support.first; - var from = keyframes.first; - var decl = from.first; - - expect(decl.parent).to.equal(from); - expect(from.parent).to.equal(keyframes); - expect(keyframes.parent).to.equal(support); - expect(support.parent).to.equal(css); - }); - - describe('errors', () => { - - it('throws on unclosed blocks', () => { - expect( () => parse('\na {\n') ).to.throw(/:2:1: Unclosed block/); - }); - - it('fixes unclosed blocks in safe mode', () => { - expect(parse('@media (screen) { a {\n', { safe: true }).toString()) - .to.eql('@media (screen) { a {\n}}'); - - expect(parse('a { color', { safe: true }).toString()) - .to.eql('a { color}'); - - expect(parse('a { color: black', { safe: true }).first.first.prop) - .to.eql('color'); - }); - - it('throws on unnecessary block close', () => { - expect( () => parse('a {\n} }') ).to.throw(/:2:3: Unexpected }/); - }); - - it('fixes unnecessary block close in safe mode', () => { - var root = parse('a {\n} }', { safe: true }); - expect(root.first.toString()).to.eql('a {\n}'); - expect(root.after).to.eql(' }'); - }); - - it('throws on unclosed comment', () => { - expect( () => parse('\n/*\n ') ).to.throw(/:2:1: Unclosed comment/); - }); - - it('fixes unclosed comment in safe mode', () => { - var root = parse('a { /* b ', { safe: true }); - expect(root.toString()).to.eql('a { /* b */}'); - expect(root.first.first.text).to.eql('b'); - }); - - it('throws on unclosed quote', () => { - expect( () => parse('\n"\n\na ') ).to.throw(/:2:1: Unclosed quote/); - }); - - it('fixes unclosed quote in safe mode', () => { - expect(parse('a { content: "b', { safe: true }).toString()) - .to.eql('a { content: "b"}'); - }); - - it('throws on unclosed bracket', () => { - expect( () => parse(':not(one() { }') ) - .to.throw(/:1:5: Unclosed bracket/); - }); - - it('fixes unclosed bracket', () => { - expect(parse(':not(one() { }', { safe: true }).after) - .to.eql(':not(one() { }'); - }); - - it('throws on property without value', () => { - expect( () => parse("a { b;}") ).to.throw(/:1:5: Unknown word/); - expect( () => parse("a { b b }") ).to.throw(/:1:5: Unknown word/); - }); - - it('fixes property without value in safe mode', () => { - var root = parse('a { color: white; one }', { safe: true }); - expect(root.first.nodes.length).to.eql(1); - expect(root.first.semicolon).to.be.true; - expect(root.first.after).to.eql(' one '); - }); - - it('fixes 2 properties in safe mode', () => { - var root = parse('a { one color: white; one }', { safe: true }); - expect(root.first.nodes.length).to.eql(1); - expect(root.first.first.prop).to.eql('color'); - expect(root.first.first.before).to.eql(' one '); - }); - - it('throws on nameless at-rule', () => { - expect( () => parse('@') ).to.throw(/:1:1: At-rule without name/); - }); - - it('fixes nameless at-rule in safe mode', () => { - var root = parse('@', { safe: true }); - expect(root.first.type).to.eql('atrule'); - expect(root.first.name).to.eql(''); - }); - - it('throws on property without semicolon', () => { - expect( () => parse('a { one: 1 two: 2 }') ) - .to.throw(/:1:10: Missed semicolon/); - }); - - it('fixes property without semicolon in safe mode', () => { - var root = parse('a { one: 1 two: 2 }', { safe: true }); - expect(root.first.nodes.length).to.eql(2); - expect(root.toString()).to.eql('a { one: 1; two: 2 }'); - }); - - }); - -}); diff --git a/test/parse.test.ts b/test/parse.test.ts new file mode 100755 index 000000000..06bde8385 --- /dev/null +++ b/test/parse.test.ts @@ -0,0 +1,281 @@ +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { eachTest, jsonify, testPath } from 'postcss-parser-tests' +import { test } from 'uvu' +import { equal, is, match, not, throws } from 'uvu/assert' + +import { + AtRule, + Declaration, + fromJSON, + parse, + Root, + Rule +} from '../lib/postcss.js' + +test('works with file reads', () => { + let stream = readFileSync(testPath('atrule-empty.css')) + is(parse(stream) instanceof Root, true) +}) + +eachTest((name, css, json) => { + test(`parses ${name}`, () => { + css = css.replace(/\r\n/g, '\n') + let parsed = jsonify(parse(css, { from: name })) + equal(parsed, json) + }) +}) + +test('parses UTF-8 BOM', () => { + let css = parse('\uFEFF@host { a {\f} }') + equal(css.nodes[0].raws.before, '') +}) + +test('should has true at hasBOM property', () => { + let css = parse('\uFEFF@host { a {\f} }') + is(css.first?.source?.input.hasBOM, true) +}) + +test('should has false at hasBOM property', () => { + let css = parse('@host { a {\f} }') + is(css.first?.source?.input.hasBOM, false) +}) + +test('parses carrier return', () => { + throws(() => { + parse('@font-face{ font:(\r/*);} body { a: "a*/)} a{}"}') + }, /:1:46: Unclosed string/) +}) + +test('saves source file', () => { + let css = parse('a {}', { from: 'a.css' }) + is(css.first?.source?.input.css, 'a {}') + is(css.first?.source?.input.file, resolve('a.css')) + is(css.first?.source?.input.from, resolve('a.css')) +}) + +test('keeps absolute path in source', () => { + let css = parse('a {}', { from: 'http://example.com/a.css' }) + is(css.first?.source?.input.file, 'http://example.com/a.css') + is(css.first?.source?.input.from, 'http://example.com/a.css') +}) + +test('saves source file on previous map', () => { + let root1 = parse('a {}', { map: { inline: true } }) + let css = root1.toResult({ map: { inline: true } }).css + let root2 = parse(css) + is(root2.first?.source?.input.file, resolve('to.css')) +}) + +test('sets unique ID for file without name', () => { + let css1 = parse('a {}') + let css2 = parse('a {}') + match(String(css1.first?.source?.input.id), /^$/) + match(String(css1.first?.source?.input.from), /^$/) + is.not(css2.first?.source?.input.id, css1.first?.source?.input.id) +}) + +test('sets parent node', () => { + let file = testPath('atrule-rules.css') + let css = parse(readFileSync(file)) + + let support = css.first as AtRule + let keyframes = support.first as AtRule + let from = keyframes.first as Rule + let decl = from.first as Declaration + + is(decl.parent, from) + is(from.parent, keyframes) + is(support.parent, css) + is(keyframes.parent, support) +}) + +test('ignores wrong close bracket', () => { + let root = parse('a { p: ()) }') + let a = root.first as Rule + let decl = a.first as Declaration + is(decl.value, '())') +}) + +test('parses unofficial --mixins', () => { + let root = parse(':root { --x { color: pink; }; }') + let rule = root.first as Rule + let prop = rule.first as Rule + is(prop.selector, '--x') +}) + +test('ignores symbols before declaration', () => { + let root = parse('a { :one: 1 }') + let a = root.first as Rule + let prop = a.first as Declaration + is(prop.raws.before, ' :') +}) + +test('parses double semicolon after rule', () => { + is(parse('a { };;').toString(), 'a { };;') +}) + +test('parses a functional property', () => { + let root = parse('a { b(c): d }') + let a = root.first as Rule + let b = a.first as Declaration + + is(b.prop, 'b(c)') +}) + +test('parses a functional tagname', () => { + let root = parse('a { b(c): d {} }') + let a = root.first as Rule + let b = a.first as Rule + + is(b.selector, 'b(c): d') +}) + +test('throws on unclosed blocks', () => { + throws(() => { + parse('\na {\n') + }, /:2:1: Unclosed block/) +}) + +test('throws on unnecessary block close', () => { + throws(() => { + parse('a {\n} }') + }, /:2:3: Unexpected }/) +}) + +test('throws on unclosed comment', () => { + throws(() => { + parse('\n/*\n ') + }, /:2:1: Unclosed comment/) +}) + +test('throws on unclosed quote', () => { + throws(() => { + parse('\n"\n\na ') + }, /:2:1: Unclosed string/) +}) + +test('throws on unclosed bracket', () => { + throws(() => { + parse(':not(one() { }') + }, /:1:5: Unclosed bracket/) +}) + +test('throws on property without value', () => { + throws(() => { + parse('a { b;}') + }, /:1:5: Unknown word/) + throws(() => { + parse('a { b b }') + }, /:1:5: Unknown word/) + throws(() => { + parse('a { b(); }') + }, /:1:5: Unknown word/) +}) + +test('throws on nameless at-rule', () => { + throws(() => { + parse('@') + }, /:1:1: At-rule without name/) +}) + +test('throws on property without semicolon', () => { + throws(() => { + parse('a { one: filter(a:"") two: 2 }') + }, /:1:21: Missed semicolon/) +}) + +test('throws on double colon', () => { + throws(() => { + parse('a { one:: 1 }') + }, /:1:9: Double colon/) +}) + +test('do not throws on comment in between', () => { + parse('a { b/* c */: 1 }') +}) + +test('throws on two words in between', () => { + throws(() => { + parse('a { b c: 1 }') + }, /:1:7: Unknown word/) +}) + +test('throws on just colon', () => { + throws(() => { + parse(':') + }, /:1:1: Unknown word/) + throws(() => { + parse(' : ') + }, /:1:2: Unknown word/) +}) + +test('does not suggest different parsers for CSS', () => { + let error: any + try { + parse('a { one:: 1 }', { from: 'app.css' }) + } catch (e) { + error = e + } + not.match(error.message, /postcss-less|postcss-scss/) +}) + +test('suggests postcss-scss for SCSS sources', () => { + throws(() => { + parse('a { #{var}: 1 }', { from: 'app.scss' }) + }, /postcss-scss/) +}) + +test('suggests postcss-sass for Sass sources', () => { + throws(() => { + parse('a\n #{var}: 1', { from: 'app.sass' }) + }, /postcss-sass/) +}) + +test('suggests postcss-less for Less sources', () => { + throws(() => { + parse('.@{my-selector} { }', { from: 'app.less' }) + }, /postcss-less/) +}) + +test('should give the correct column of missed semicolon with !important', () => { + let error: any + try { + parse('a { \n color: red !important\n background-color: black;\n}') + } catch (e) { + error = e + } + match(error.message, /2:26: Missed semicolon/) +}) + +test('should give the correct column of missed semicolon without !important', () => { + let error: any + try { + parse('a { \n color: red\n background-color: black;\n}') + } catch (e) { + error = e + } + match(error.message, /2:15: Missed semicolon/) +}) + +test('does not overflow the stack on deeply nested nodes', () => { + let depth = 6000 + let css = 'a{'.repeat(depth) + 'color:red' + '}'.repeat(depth) + + let root = parse(css) + is(root.toString(), css) + + let clone = root.clone() + is(clone.toString(), css) + + let json = root.toJSON() + is(fromJSON(json).toString(), css) + + let count = 0 + root.walk(() => { + count += 1 + }) + is(count, depth + 1) +}) + +test.run() diff --git a/test/postcss.js b/test/postcss.js deleted file mode 100644 index 3b1fc1bb4..000000000 --- a/test/postcss.js +++ /dev/null @@ -1,189 +0,0 @@ -import CssSyntaxError from '../lib/css-syntax-error'; -import postcss from '../lib/postcss'; -import Result from '../lib/result'; -import Root from '../lib/root'; - -import { expect } from 'chai'; -import path from 'path'; - -describe('postcss()', () => { - - it('creates plugins list', () => { - expect(postcss().plugins).to.eql([]); - }); - - it('saves plugins list', () => { - var a = () => 1; - var b = () => 2; - expect(postcss(a, b).plugins).to.eql([a, b]); - }); - - it('saves plugins list as array', () => { - var a = () => 1; - var b = () => 2; - expect(postcss([a, b]).plugins).to.eql([a, b]); - }); - - it('saves plugins object list', () => { - var a = () => 1; - expect(postcss({ postcss: a }).plugins).to.eql([a]); - }); - - describe('use()', () => { - - it('adds new plugins', () => { - var a = () => 1; - var processor = postcss(); - processor.use(a); - expect(processor.plugins).to.eql([a]); - }); - - it('adds new plugin by object', () => { - var a = () => 1; - var processor = postcss(); - processor.use({ postcss: a }); - expect(processor.plugins).to.eql([a]); - }); - - it('adds new plugin by object-function', () => { - var a = () => 1; - var obj = () => 2; - obj.postcss = a; - var processor = postcss(); - processor.use(obj); - expect(processor.plugins).to.eql([a]); - }); - - it('adds new processors of another postcss instance', () => { - var a = () => 1; - var processor = postcss(); - var other = postcss(a); - processor.use(other); - expect(processor.plugins).to.eql([a]); - }); - - it('returns itself', () => { - var a = () => 1; - var b = () => 2; - expect(postcss().use(a).use(b).plugins).to.eql([a, b]); - }); - - }); - - describe('process()', () => { - var processor = postcss( (css) => { - css.eachRule( (rule) => { - if ( !rule.selector.match(/::(before|after)/) ) return; - if ( !rule.some( i => i.prop == 'content' ) ) { - rule.prepend({ prop: 'content', value: '""' }); - } - }); - }); - - it('processes CSS', () => { - var result = processor.process('a::before{top:0}'); - expect(result.css).to.eql('a::before{content:"";top:0}'); - }); - - it('processes parsed AST', () => { - var root = postcss.parse('a::before{top:0}'); - var result = processor.process(root); - expect(result.css).to.eql('a::before{content:"";top:0}'); - }); - - it('processes previous result', () => { - var result = postcss().process('a::before{top:0}'); - result = processor.process(result); - expect(result.css).to.eql('a::before{content:"";top:0}'); - }); - - it('takes maps from previous result', () => { - var one = postcss().process('a{}', { - from: 'a.css', - to: 'b.css', - map: { inline: false } - }); - var two = postcss().process(one, { - to: 'c.css', - map: { inline: false } - }); - expect(two.map.toJSON().sources).to.eql(['a.css']); - }); - - it('throws with file name', () => { - var error; - try { - postcss().process('a {', { from: 'a.css' }); - } catch (e) { - if ( e instanceof CssSyntaxError ) { - error = e; - } else { - throw e; - } - } - - expect(error.file).to.eql(path.resolve('a.css')); - expect(error.message).to.match(/a.css:1:1: Unclosed block$/); - }); - - it('allows to replace Root', () => { - var processor = postcss( () => new Root() ); - expect(processor.process('a {}').css).to.eql(''); - }); - - it('returns Result object', () => { - var result = postcss().process('a{}'); - expect(result).to.be.an.instanceOf(Result); - expect(result.css).to.eql( 'a{}'); - expect(result.toString()).to.eql('a{}'); - }); - - it('calls all plugins', () => { - var calls = ''; - var a = () => calls += 'a'; - var b = () => calls += 'b'; - - postcss(a, b).process(''); - expect(calls).to.eql('ab'); - }); - - it('parses, convert and stringify CSS', () => { - var a = (css) => expect(css).to.be.an.instanceof(Root); - expect(postcss(a).process('a {}').css).to.be.a('string'); - }); - - it('send options to plugins', () => { - var a = (css, opts) => expect(opts).to.eql({ from: 'a.css' }); - postcss(a).process('a {}', { from: 'a.css' }); - }); - - it('accepts source map from PostCSS', () => { - var one = postcss().process('a{}', { - from: 'a.css', - to: 'b.css', - map: { inline: false } - }); - var two = postcss().process(one.css, { - from: 'b.css', - to: 'c.css', - map: { prev: one.map, inline: false } - }); - expect(two.map.toJSON().sources).to.eql(['a.css']); - }); - - }); - - describe('.root()', () => { - - it('allows to build own CSS', () => { - var root = postcss.root(); - var rule = postcss.rule({ selector: 'a' }); - rule.append( postcss.decl({ prop: 'color', value: 'black' }) ); - root.append( rule ); - - expect(root.toString()).to.eql("a {\n color: black\n}"); - }); - - }); - -}); diff --git a/test/postcss.test.ts b/test/postcss.test.ts new file mode 100755 index 000000000..fddd982fe --- /dev/null +++ b/test/postcss.test.ts @@ -0,0 +1,192 @@ +import { restoreAll, spyOn } from 'nanospy' +import { test } from 'uvu' +import { equal, is, match, throws, type } from 'uvu/assert' + +import postcss = require('../lib/postcss.js') +import postcssDefault, { PluginCreator, Root } from '../lib/postcss.js' +import Processor from '../lib/processor.js' + +test.after.each(() => { + restoreAll() +}) + +test('default matches module.exports', () => { + is(postcss, postcssDefault) +}) + +test('creates plugins list', () => { + let processor = postcss() + is(processor instanceof Processor, true) + equal(processor.plugins, []) +}) + +test('saves plugins list', () => { + let a = (): void => {} + let b = (): void => {} + equal(postcss(a, b).plugins, [a, b]) +}) + +test('saves plugins list as array', () => { + let a = (): void => {} + let b = (): void => {} + equal(postcss([a, b]).plugins, [a, b]) +}) + +test('takes plugin from other processor', () => { + let a = (): void => {} + let b = (): void => {} + let c = (): void => {} + let other = postcss([a, b]) + equal(postcss([other, c]).plugins, [a, b, c]) +}) + +test('takes plugins from a a plugin returning a processor', () => { + let a = (): void => {} + let b = (): void => {} + let c = (): void => {} + let other = postcss([a, b]) + let meta = (() => other) as PluginCreator + meta.postcss = true + equal(postcss([other, c]).plugins, [a, b, c]) +}) + +test('contains parser', () => { + is(postcss.parse('').type, 'root') +}) + +test('contains stringifier', () => { + type(postcss.stringify, 'function') +}) + +test('allows to build own CSS', () => { + let root = postcss.root({ raws: { after: '\n' } }) + let comment = postcss.comment({ text: 'Example' }) + let media = postcss.atRule({ name: 'media', params: 'screen' }) + let rule = postcss.rule({ selector: 'a' }) + let decl = postcss.decl({ prop: 'color', value: 'black' }) + + root.append(comment) + rule.append(decl) + media.append(rule) + root.append(media) + + is( + root.toString(), + '/* Example */\n' + + '@media screen {\n' + + ' a {\n' + + ' color: black\n' + + ' }\n' + + '}\n' + ) +}) + +test('allows to build own CSS with Document', () => { + let document = postcss.document() + let root = postcss.root({ raws: { after: '\n' } }) + let comment = postcss.comment({ text: 'Example' }) + let media = postcss.atRule({ name: 'media', params: 'screen' }) + let rule = postcss.rule({ selector: 'a' }) + let decl = postcss.decl({ prop: 'color', value: 'black' }) + + root.append(comment) + rule.append(decl) + media.append(rule) + root.append(media) + document.append(root) + + is( + document.toString(), + '/* Example */\n' + + '@media screen {\n' + + ' a {\n' + + ' color: black\n' + + ' }\n' + + '}\n' + ) +}) + +test('contains list module', () => { + equal(postcss.list.space('a b'), ['a', 'b']) +}) + +test('works with null', () => { + throws(() => { + // @ts-expect-error Testing invalid input + postcss([() => {}]).process(null).css + }, /PostCSS received null instead of CSS string/) +}) + +test('has deprecated method to create plugins', () => { + let warn = spyOn(console, 'warn', () => {}) + let plugin = (postcss as any).plugin('test', (filter?: string) => { + return (root: Root) => { + root.walkDecls(filter ?? 'two', i => { + i.remove() + }) + } + }) + + equal(warn.callCount, 0) + + let func1: any = postcss(plugin).plugins[0] + is(func1.postcssPlugin, 'test') + match(func1.postcssVersion, /\d+.\d+.\d+/) + equal(warn.callCount, 1) + + let func2: any = postcss(plugin()).plugins[0] + equal(func2.postcssPlugin, func1.postcssPlugin) + equal(func2.postcssVersion, func1.postcssVersion) + + let result1 = postcss(plugin('one')).process('a{ one: 1; two: 2 }') + is(result1.css, 'a{ two: 2 }') + + let result2 = postcss(plugin).process('a{ one: 1; two: 2 }') + is(result2.css, 'a{ one: 1 }') + + equal(warn.callCount, 1) + match(warn.calls[0][0], /postcss\.plugin was deprecated/) +}) + +test('creates a shortcut to process css', async () => { + let warn = spyOn(console, 'warn', () => {}) + let plugin = (postcss as any).plugin('test', (str?: string) => { + return (root: Root) => { + root.walkDecls(i => { + i.value = str ?? 'bar' + }) + } + }) + + let result1 = plugin.process('a{value:foo}') + is(result1.css, 'a{value:bar}') + + let result2 = plugin.process('a{value:foo}', {}, 'baz') + is(result2.css, 'a{value:baz}') + + let result = await plugin.process('a{value:foo}', { from: 'a' }, 'baz') + equal(result.opts, { from: 'a' }) + is(result.css, 'a{value:baz}') + + equal(warn.callCount, 1) +}) + +test('does not call plugin constructor', () => { + let warn = spyOn(console, 'warn', () => {}) + let calls = 0 + let plugin = (postcss as any).plugin('test', () => { + calls += 1 + return () => {} + }) + is(calls, 0) + + postcss(plugin).process('a{}') + is(calls, 1) + + postcss(plugin()).process('a{}') + is(calls, 2) + + equal(warn.callCount, 1) +}) + +test.run() diff --git a/test/previous-map.js b/test/previous-map.js deleted file mode 100644 index 33921a9b3..000000000 --- a/test/previous-map.js +++ /dev/null @@ -1,119 +0,0 @@ -import parse from '../lib/parse'; - -import mozilla from 'source-map'; -import fs from 'fs-extra'; -import { expect } from 'chai'; - -var dir = __dirname + '/fixtures'; -var map = JSON.stringify({ - version: 3, - file: null, - sources: [], - names: [], - mappings: [] -}); - -describe('PreviousMap', () => { - afterEach( () => { - if ( fs.existsSync(dir) ) fs.removeSync(dir); - }); - - it('misses property if no map', () => { - expect(parse('a{}')).to.not.have.property('prevMap'); - }); - - it('creates property if map present', () => { - expect(parse('a{}', { map: { prev: map } }).prevMap.text).to.eql(map); - }); - - it('returns consumer', () => { - expect(parse('a{}', { map: { prev: map } }).prevMap.consumer()) - .to.be.a.instanceOf(mozilla.SourceMapConsumer); - }); - - it('sets annotation property', () => { - var map2 = { map: { prev: map } }; - expect(parse('a{}', map2).prevMap).to.not.have.property('annotation'); - var root = parse('a{}/*# sourceMappingURL=a.css.map */', map2); - expect(root.prevMap.annotation).to.eql('a.css.map'); - }); - - it('checks previous sources content', () => { - var map2 = { - version: 3, - file: 'b', - sources: ['a'], - names: [], - mappings: [] - }; - - var opts = { map: { prev: map2 } }; - expect(parse('a{}', opts).prevMap.withContent()).to.be.false; - - map2.sourcesContent = ['a{}']; - expect(parse('a{}', opts).prevMap.withContent()).to.be.true; - }); - - it('decodes base64 maps', () => { - var b64 = new Buffer(map).toString('base64'); - var css = "a{}\n" + - `/*# sourceMappingURL=data:application/json;base64,${b64} */`; - - expect(parse(css).prevMap.text).to.eql(map); - }); - - it('decodes URI maps', () => { - var uri = 'data:application/json,' + decodeURI(map); - var css = "a{}\n/*# sourceMappingURL=" + uri + " */"; - - expect(parse(css).prevMap.text).to.eql(map); - }); - - it('removes map on request', () => { - var uri = 'data:application/json,' + decodeURI(map); - var css = "a{}\n/*# sourceMappingURL=" + uri + " */"; - - expect(parse(css, { map: { prev: false } })) - .to.not.have.property('prevMap'); - }); - - it('raises on unknown inline encoding', () => { - var css = "a { }\n" + - "/*# sourceMappingURL=data:application/json;" + - "md5,68b329da9893e34099c7d8ad5cb9c940*/"; - - expect( () => parse(css) ) - .to.throw('Unsupported source map encoding md5'); - }); - - it('raises on unknown map format', () => { - expect( () => parse('a{}', { map: { prev: 1 } }) ) - .to.throw('Unsupported previous source map format: 1'); - }); - - it('reads map from annotation', () => { - fs.outputFileSync(dir + '/a.map', map); - root = parse("a{}\n/*# sourceMappingURL=a.map */", { - from: dir + '/a.css' - }); - - expect(root.prevMap.text).to.eql(map); - expect(root.prevMap.root).to.eql(dir); - }); - - it('sets uniq name for inline map', () => { - var map2 = { - version: 3, - sources: ['a'], - names: [], - mappings: [] - }; - - var opts = { map: { prev: map2 } }; - var prev = parse('a{}', opts).prevMap; - - expect(prev.file).to.match(/^$/); - expect(prev.file).to.not.eql( parse('a{}', opts).prevMap.file ); - }); - -}); 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.ts b/test/processor.test.ts new file mode 100755 index 000000000..74803073d --- /dev/null +++ b/test/processor.test.ts @@ -0,0 +1,639 @@ +import { delay } from 'nanodelay' +import { restoreAll, spyOn } from 'nanospy' +import { resolve as pathResolve } from 'path' +import { test } from 'uvu' +import { equal, instance, is, match, not, throws, type } from 'uvu/assert' + +import CssSyntaxError from '../lib/css-syntax-error.js' +import LazyResult from '../lib/lazy-result.js' +import NoWorkResult from '../lib/no-work-result.js' +import postcss, { + Document, + Node, + parse, + Parser, + Plugin, + PluginCreator, + Result, + Root, + Stringifier +} from '../lib/postcss.js' +import Processor from '../lib/processor.js' +import Rule from '../lib/rule.js' + +test.after.each(() => { + restoreAll() +}) + +function prs(): Root { + return new Root({ raws: { after: 'ok' } }) +} + +function str(node: Node, builder: (s: string) => void): void { + builder(`${node.raws.after}!`) +} + +async function catchError(cb: () => Promise): Promise { + try { + await cb() + } catch (e) { + if (e instanceof Error) return e + } + throw new Error('Error was not thrown') +} + +let beforeFix = new Processor([ + (root: Root) => { + root.walkRules(rule => { + if (!rule.selector.match(/::(before|after)/)) return + if (!rule.some(i => i.type === 'decl' && i.prop === 'content')) { + rule.prepend({ prop: 'content', value: '""' }) + } + }) + } +]) + +test('adds new plugins', () => { + let a = (): void => {} + let processor = new Processor() + processor.use(a) + equal(processor.plugins, [a]) +}) + +test('adds new plugin by object', () => { + let a = (): void => {} + let processor = new Processor() + processor.use({ postcss: a }) + equal(processor.plugins, [a]) +}) + +test('adds new plugin by object-function', () => { + let a = (): void => {} + let obj: any = () => {} + obj.postcss = a + let processor = new Processor() + processor.use(obj) + equal(processor.plugins, [a]) +}) + +test('adds new processors of another postcss instance', () => { + let a = (): void => {} + let processor = new Processor() + let other = new Processor([a]) + processor.use(other) + equal(processor.plugins, [a]) +}) + +test('adds new processors from object', () => { + let a = (): void => {} + let processor = new Processor() + let other = new Processor([a]) + processor.use({ postcss: other }) + equal(processor.plugins, [a]) +}) + +test('returns itself', () => { + let a = (): void => {} + let b = (): void => {} + let processor = new Processor() + equal(processor.use(a).use(b).plugins, [a, b]) +}) + +test('throws on wrong format', () => { + let pr = new Processor() + throws(() => { + // @ts-expect-error Testing invalid API + pr.use(1) + }, /1 is not a PostCSS plugin/) +}) + +test('processes CSS', () => { + let result = beforeFix.process('a::before{top:0}') + is(result.css, 'a::before{content:"";top:0}') +}) + +test('processes parsed AST', () => { + let root = parse('a::before{top:0}') + let result = beforeFix.process(root) + is(result.css, 'a::before{content:"";top:0}') +}) + +test('processes previous result', () => { + let result = new Processor([() => {}]).process('a::before{top:0}') + result = beforeFix.process(result) + is(result.css, 'a::before{content:"";top:0}') +}) + +test('takes maps from previous result', () => { + let one = new Processor([() => {}]).process('a{}', { + from: 'a.css', + map: { inline: false }, + to: 'b.css' + }) + let two = new Processor([() => {}]).process(one, { to: 'c.css' }) + equal(two.map.toJSON().sources, ['a.css']) +}) + +test('inlines maps from previous result', () => { + let one = new Processor([() => {}]).process('a{}', { + from: 'a.css', + map: { inline: false }, + to: 'b.css' + }) + let two = new Processor([() => {}]).process(one, { + map: { inline: true }, + to: 'c.css' + }) + type(two.map, 'undefined') +}) + +test('throws with file name', () => { + let error: CssSyntaxError | undefined + try { + new Processor([() => {}]).process('a {', { from: 'a.css' }).css + } catch (e) { + if (e instanceof CssSyntaxError) { + error = e + } else { + throw e + } + } + + is(error?.file, pathResolve('a.css')) + match(String(error?.message), /a.css:1:1: Unclosed block$/) +}) + +test('allows to replace Root', () => { + let processor = new Processor([ + (css, result) => { + result.root = new Root() + } + ]) + is(processor.process('a {}').css, '') +}) + +test('returns LazyResult object', () => { + let result = new Processor([() => {}]).process('a{}') + is(result instanceof LazyResult, true) + is(result.css, 'a{}') + is(result.toString(), 'a{}') +}) + +test('calls all plugins once', async () => { + let calls = '' + let a = (): void => { + calls += 'a' + } + let b = (): void => { + calls += 'b' + } + + let result = new Processor([a, b]).process('', { from: undefined }) + result.css + result.map + result.root + await result + is(calls, 'ab') +}) + +test('parses, converts and stringifies CSS', () => { + is( + typeof new Processor([ + (css: Root) => { + equal(css instanceof Root, true) + } + ]).process('a {}').css, + 'string' + ) +}) + +test('send result to plugins', () => { + let processor = new Processor([() => {}]) + processor + .use((css, result) => { + is(result instanceof Result, true) + equal(result.processor, processor) + equal(result.opts, { map: true }) + equal(result.root, css) + }) + .process('a {}', { from: undefined, map: true }) +}) + +test('accepts source map from PostCSS', () => { + let one = new Processor([() => {}]).process('a{}', { + from: 'a.css', + map: { inline: false }, + to: 'b.css' + }) + let two = new Processor([() => {}]).process(one.css, { + from: 'b.css', + map: { inline: false, prev: one.map }, + to: 'c.css' + }) + equal(two.map.toJSON().sources, ['a.css']) +}) + +test('supports async plugins', async () => { + let starts = 0 + let finish = 0 + let async1 = (css: Root): Promise => + new Promise(resolve => { + starts += 1 + setTimeout(() => { + equal(starts, 1) + + css.append('a {}') + finish += 1 + resolve() + }, 1) + }) + let async2 = (css: Root): Promise => + new Promise(resolve => { + equal(starts, 1) + equal(finish, 1) + + starts += 1 + setTimeout(() => { + css.append('b {}') + finish += 1 + resolve() + }, 1) + }) + let r = await new Processor([async1, async2]).process('', { from: 'a' }) + is(starts, 2) + is(finish, 2) + is(r.css, 'a {}b {}') +}) + +test('works async without plugins', async () => { + let r = await new Processor([() => {}]).process('a {}', { from: 'a' }) + is(r.css, 'a {}') +}) + +test('runs async plugin only once', async () => { + let calls = 0 + let async = (): Promise => { + return new Promise(resolve => { + setTimeout(() => { + calls += 1 + resolve() + }, 1) + }) + } + + let result = new Processor([async]).process('a {}', { from: undefined }) + result.then(() => {}) + await result + await result + is(calls, 1) +}) + +test('supports async errors', async () => { + let error = new Error('Async') + let async = (): Promise => { + return new Promise((resolve, reject) => { + reject(error) + }) + } + let result = new Processor([async]).process('', { from: undefined }) + let err1 = await catchError(async () => await result) + equal(err1, error) + + let err2: unknown + result.catch((catched: unknown) => { + err2 = catched + }) + await delay(10) + equal(err2, error) +}) + +test('supports sync errors in async mode', async () => { + let error = new Error('Async') + let async = (): void => { + throw error + } + let err = await catchError(() => + new Processor([async]).process('', { from: undefined }) + ) + equal(err, error) +}) + +test('throws parse error in async', async () => { + let err = await catchError(() => + new Processor([() => {}]).process('a{', { from: undefined }) + ) + is(err.message, ':1:1: Unclosed block') +}) + +test('throws error on sync method to async plugin', () => { + let async = (): Promise => { + return new Promise(resolve => { + resolve() + }) + } + throws(() => { + new Processor([async]).process('a{}').css + }, /async/) +}) + +test('throws a sync call in async running', () => { + let async = (): Promise => + new Promise(resolve => setTimeout(resolve, 1)) + + let processor = new Processor([async]).process('a{}', { from: 'a.css' }) + processor.async() + + throws(() => { + processor.sync() + }, /then/) +}) + +test('remembers errors', async () => { + let calls = 0 + let plugin: Plugin = { + Once() { + calls += 1 + throw new Error('test') + }, + postcssPlugin: 'plugin' + } + + let processing = postcss([plugin]).process('a{}', { from: undefined }) + + throws(() => { + processing.css + }, 'test') + throws(() => { + processing.css + }, 'test') + throws(() => { + processing.root + }, 'test') + + let asyncError: any + try { + await processing + } catch (e) { + asyncError = e + } + is(asyncError.message, 'test') + + is(calls, 1) +}) + +test('checks plugin compatibility', () => { + let error = spyOn(console, 'error', () => {}) + let warn = spyOn(console, 'warn', () => {}) + + let plugin = (postcss as any).plugin('test', () => { + return () => { + throw new Error('Er') + } + }) + let func = plugin() + equal(warn.callCount, 1) + func.postcssVersion = '2.1.5' + + function processBy(version: string): void { + let processor = new Processor([func]) + processor.version = version + processor.process('a{}').css + } + + throws(() => { + processBy('1.0.0') + }, 'Er') + equal(error.callCount, 1) + equal(error.calls, [ + [ + 'Unknown error from PostCSS plugin. ' + + 'Your current PostCSS version is 1.0.0, but test uses 2.1.5. ' + + 'Perhaps this is the source of the error below.' + ] + ]) + + throws(() => { + processBy('3.0.0') + }, 'Er') + equal(error.callCount, 2) + + throws(() => { + processBy('2.0.0') + }, 'Er') + equal(error.callCount, 3) + + throws(() => { + processBy('2.1.0') + }, 'Er') + equal(error.callCount, 3) +}) + +test('sets last plugin to result', async () => { + let plugin1 = (css: Root, result: Result): void => { + equal(result.lastPlugin, plugin1) + } + let plugin2 = (css: Root, result: Result): void => { + equal(result.lastPlugin, plugin2) + } + + let processor = new Processor([plugin1, plugin2]) + let result = await processor.process('a{}', { from: undefined }) + equal(result.lastPlugin, plugin2) +}) + +test('uses custom parsers', async () => { + let processor = new Processor([]) + let result = await processor.process('a{}', { from: undefined, parser: prs }) + is(result.css, 'ok') +}) + +test('uses custom parsers from object', async () => { + let processor = new Processor([]) + let syntax = { parse: prs, stringify: str } + let result = await processor.process('a{}', { from: 'a', parser: syntax }) + equal(result.css, 'ok') +}) + +test('uses custom stringifier', async () => { + let processor = new Processor([]) + let result = await processor.process('a{}', { from: 'a', stringifier: str }) + is(result.css, '!') +}) + +test('uses custom stringifier from object', async () => { + let processor = new Processor([]) + let syntax = { parse: prs, stringify: str } + let result = await processor.process('', { from: 'a', stringifier: syntax }) + is(result.css, '!') +}) + +test('uses custom stringifier with source maps', async () => { + let processor = new Processor([]) + let result = await processor.process('a{}', { + from: undefined, + map: true, + stringifier: str + }) + match(result.css, /!\n\/\*# sourceMap/) +}) + +test('uses custom syntax', async () => { + let processor = new Processor([() => {}]) + let result = await processor.process('a{}', { + from: undefined, + syntax: { parse: prs, stringify: str } + }) + is(result.css, 'ok!') +}) + +test('contains PostCSS version', () => { + match(new Processor().version, /\d+.\d+.\d+/) +}) + +test('throws on syntax as plugin', () => { + let processor = new Processor([() => {}]) + throws(() => { + processor.use({ + // @ts-expect-error Testing invalid API + parse() {} + }) + }, /syntax/) +}) + +test('warns about missed from', async () => { + let warn = spyOn(console, 'warn', () => {}) + let processor = new Processor([() => {}]) + + processor.process('a{}').css + equal(warn.calls, []) + + await processor.process('a{}') + equal(warn.calls, [ + [ + 'Without `from` option PostCSS could generate wrong source map ' + + 'and will not find Browserslist config. Set it to CSS file path ' + + 'or to `undefined` to prevent this warning.' + ] + ]) +}) + +test('returns NoWorkResult object', () => { + let result = new Processor().process('a{}') + instance(result, NoWorkResult) +}) + +test('without plugins parses CSS only on root access', async () => { + let noWorkResult = new Processor().process('a{}') + let result = await noWorkResult + // @ts-expect-error Testing private API + type(noWorkResult._root, 'undefined') + is(result.root.nodes.length, 1) + // @ts-expect-error Testing private API + not.type(noWorkResult._root, 'undefined') + is(noWorkResult.root.nodes.length, 1) +}) + +test('catches error with empty processor', async () => { + let noWorkResult = new Processor().process('a {') + + try { + noWorkResult.root + } catch {} + + let err = await catchError(async () => await noWorkResult) + + noWorkResult.catch((e: unknown) => { + instance(e, CssSyntaxError) + }) + + instance(err, CssSyntaxError) +}) + +test('throws an error on root access on no plugins mode', () => { + throws(() => { + postcss().process('// invalid', { from: 'a' }).root + }, 'Unknown word') +}) + +test('supports plugins returning processors', () => { + let warn = spyOn(console, 'warn', () => {}) + let a = (): void => {} + let processor = new Processor() + let other: any = (postcss as any).plugin('test', () => { + return new Processor([a]) + }) + processor.use(other) + equal(processor.plugins, [a]) + equal(warn.callCount, 1) +}) + +test('supports plugin creators returning processors', () => { + let a = (): void => {} + let processor = new Processor() + let other = (() => { + return new Processor([a]) + }) as PluginCreator + other.postcss = true + processor.use(other) + equal(processor.plugins, [a]) +}) + +test('uses custom syntax for document', async () => { + let customParser: Parser = () => { + return new Document({ + nodes: [ + new Root({ + nodes: [new Rule({ selector: 'a' })], + raws: { + after: '\n\n\n', + codeBefore: '\n\n\n', + codeBefore: '\n\n\n' + ) +}) + +test.run() diff --git a/test/result.js b/test/result.js deleted file mode 100644 index bc2d1293a..000000000 --- a/test/result.js +++ /dev/null @@ -1,52 +0,0 @@ -import Result from '../lib/result'; -import parse from '../lib/parse'; - -import mozilla from 'source-map'; -import { expect } from 'chai'; - -var root; - -describe('Result', () => { - beforeEach( () => { - root = parse('a {}'); - }); - - describe('root', () => { - - it('contains AST', () => { - var result = new Result(root); - expect(result.root).to.eql(root); - }); - - }); - - describe('css', () => { - - it('will be stringified', () => { - var result = new Result(root); - expect(result.css).to.eql('a {}'); - }); - - it('stringifies', () => { - var result = new Result(root, 'a {}'); - expect('' + result).to.eql(result.css); - }); - - }); - - describe('map', () => { - - it('exists only if necessary', () => { - var result = new Result(root); - expect(result.map).to.not.exist; - - result = new Result(root, { map: true }); - expect(result.map).to.not.exist; - - result = new Result(root, { map: { inline: false } }); - expect(result.map).to.be.a.instanceOf(mozilla.SourceMapGenerator); - }); - - }); - -}); diff --git a/test/result.test.ts b/test/result.test.ts new file mode 100755 index 000000000..e163e1787 --- /dev/null +++ b/test/result.test.ts @@ -0,0 +1,70 @@ +import { test } from 'uvu' +import { equal, is } from 'uvu/assert' + +import postcss, { Plugin, Result, Root, Warning } from '../lib/postcss.js' +import Processor from '../lib/processor.js' + +let processor = new Processor() +let root = new Root() + +test('stringifies', () => { + let result = new Result(processor, root, {}) + result.css = 'a{}' + is(`${result}`, result.css) +}) + +test('adds warning', () => { + let warning + let plugin: Plugin = { + Once(css, { result }) { + warning = result.warn('test', { node: css.first }) + }, + postcssPlugin: 'test-plugin' + } + let result = postcss([plugin]).process('a{}').sync() + + equal( + warning, + new Warning('test', { + node: result.root.first, + plugin: 'test-plugin' + }) + ) + + equal(result.messages, [warning]) +}) + +test('allows to override plugin', () => { + let plugin: Plugin = { + Once(css, { result }) { + result.warn('test', { plugin: 'test-plugin#one' }) + }, + postcssPlugin: 'test-plugin' + } + let result = postcss([plugin]).process('a{}').sync() + + is(result.messages[0].plugin, 'test-plugin#one') +}) + +test('allows Root', () => { + let css = postcss.parse('a{}') + let result = new Result(processor, css, {}) + result.warn('TT', { node: css.first }) + + is(result.messages[0].toString(), ':1:1: TT') +}) + +test('returns only warnings', () => { + let result = new Result(processor, root, {}) + result.messages = [ + { text: 'a', type: 'warning' }, + { type: 'custom' }, + { text: 'b', type: 'warning' } + ] + equal(result.warnings(), [ + { text: 'a', type: 'warning' }, + { text: 'b', type: 'warning' } + ]) +}) + +test.run() diff --git a/test/root.js b/test/root.js deleted file mode 100644 index 5d82b8dfb..000000000 --- a/test/root.js +++ /dev/null @@ -1,101 +0,0 @@ -import Result from '../lib/result'; -import parse from '../lib/parse'; -import Root from '../lib/root'; -import Rule from '../lib/rule'; - -import { expect } from 'chai'; -import fs from 'fs'; - -describe('Root', () => { - - describe('toString()', () => { - - fs.readdirSync(__dirname + '/cases/').forEach( file => { - if ( !file.match(/\.css$/) ) return; - - it('stringify ' + file, () => { - var path = __dirname + '/cases/' + file; - var css = fs.readFileSync(path).toString(); - expect(parse(css).toString()).to.eql(css); - }); - }); - - }); - - describe('prepend()', () => { - - it('fixes spaces on insert before first', () => { - var css = parse("a {} b {}"); - css.prepend({ selector: 'em' }); - expect(css.toString()).to.eql("em {} a {} b {}"); - }); - - it('uses default spaces on only first', () => { - var css = parse("a {}"); - css.prepend({ selector: 'em' }); - expect(css.toString()).to.eql("em {}\na {}"); - }); - - }); - - describe('append()', () => { - - it('sets new line between rules in multiline files', () => { - var a = parse('a {}\n\na {}\n'); - var b = parse('b {}\n'); - - expect(a.append(b).toString()).to.eql('a {}\n\na {}\n\nb {}\n'); - }); - - it('sets new line between rules on last newline', () => { - var a = parse('a {}\n'); - var b = parse('b {}\n'); - - expect(a.append(b).toString()).to.eql('a {}\nb {}\n'); - }); - - it('saves compressed style', () => { - var a = parse('a{}a{}'); - var b = parse('b {\n}\n'); - expect(a.append(b).toString()).to.eql('a{}a{}b{}'); - }); - - }); - - describe('insertAfter()', () => { - - it('does not use before of first rule', () => { - var css = parse('a{} b{}'); - css.insertAfter(0, { selector: '.a' }); - css.insertAfter(2, { selector: '.b' }); - - expect(css.nodes[1].before).to.not.exist; - expect(css.nodes[3].before).to.eql(' '); - expect(css.toString()).to.eql('a{} .a{} b{} .b{}'); - }); - - }); - - describe('remove()', () => { - - it('fixes spaces on removing first rule', () => { - var css = parse('a{}\nb{}\n'); - css.first.removeSelf(); - expect(css.toString()).to.eql('b{}\n'); - }); - - }); - - describe('toResult()', () => { - - it('generates result with map', () => { - var root = parse('a {}'); - var result = root.toResult({ map: true }); - - expect(result).to.be.a.instanceOf(Result); - expect(result.css).to.match(/a \{\}\n\/\*# sourceMappingURL=/); - }); - - }); - -}); diff --git a/test/root.test.ts b/test/root.test.ts new file mode 100755 index 000000000..16eabcb7a --- /dev/null +++ b/test/root.test.ts @@ -0,0 +1,84 @@ +import { test } from 'uvu' +import { is, match, type } from 'uvu/assert' + +import postcss, { parse, Result } from '../lib/postcss.js' + +test('prepend() fixes spaces on insert before first', () => { + let css = parse('a {} b {}') + css.prepend({ selector: 'em' }) + is(css.toString(), 'em {} a {} b {}') +}) + +test('prepend() fixes spaces on multiple inserts before first', () => { + let css = parse('a {} b {}') + css.prepend({ selector: 'em' }, { selector: 'strong' }) + is(css.toString(), 'em {} strong {} a {} b {}') +}) + +test('prepend() uses default spaces on only first', () => { + let css = parse('a {}') + css.prepend({ selector: 'em' }) + is(css.toString(), 'em {}\na {}') +}) + +test('append() sets new line between rules in multiline files', () => { + let a = parse('a {}\n\na {}\n') + let b = parse('b {}\n') + is(a.append(b).toString(), 'a {}\n\na {}\n\nb {}\n') +}) + +test('insertAfter() does not use before of first rule', () => { + let css = parse('a{} b{}') + css.insertAfter(0, { selector: '.a' }) + css.insertAfter(2, { selector: '.b' }) + + type(css.nodes[1].raws.before, 'undefined') + is(css.nodes[3].raws.before, ' ') + is(css.toString(), 'a{} .a{} b{} .b{}') +}) + +test('fixes spaces on removing first rule', () => { + let css = parse('a{}\nb{}\n') + if (!css.first) throw new Error('No nodes were parsed') + css.first.remove() + is(css.toString(), 'b{}\n') +}) + +test('keeps explicitly set raws.before on inserted node', () => { + let css = parse('/*a*/\n\n/*b*/') + let node = postcss.comment({ raws: { before: '' }, text: 'new' }) + if (!css.nodes[1]) throw new Error('No nodes were parsed') + css.nodes[1].before(node) + is(node.raws.before, '') + is(css.toString(), '/*a*//*new*/\n\n/*b*/') +}) + +test('updates raws.before on node moved from another root', () => { + let css1 = parse('a{}\nb{}') + let css2 = parse('em{}\n\n\nstrong{}') + if (!css1.nodes[1] || !css2.nodes[1]) throw new Error('No nodes were parsed') + css2.nodes[1].before(css1.nodes[1]) + is(css2.toString(), 'em{}\n\n\nb{}\n\n\nstrong{}') +}) + +test('keeps spaces on moving root', () => { + let css1 = parse('a{}\nb{}\n') + + let css2 = parse('') + css2.append(css1) + is(css2.toString(), 'a{}\nb{}') + + let css3 = parse('\n') + css3.append(css2.nodes) + is(css3.toString(), 'a{}\nb{}\n') +}) + +test('generates result with map', () => { + let root = parse('a {}') + let result = root.toResult({ map: true }) + + is(result instanceof Result, true) + match(result.css, /a {}\n\/\*# sourceMappingURL=/) +}) + +test.run() diff --git a/test/rule.js b/test/rule.js deleted file mode 100644 index 6dc6c632c..000000000 --- a/test/rule.js +++ /dev/null @@ -1,74 +0,0 @@ -import parse from '../lib/parse'; -import Rule from '../lib/rule'; - -import { expect } from 'chai'; - -describe('Rule', () => { - - it('initializes with properties', () => { - var rule = new Rule({ selector: 'a' }); - expect(rule.selector).to.eql('a'); - }); - - describe('selectors', () => { - - it('returns array', () => { - var rule = new Rule({ selector: 'a,b' }); - expect(rule.selectors).to.eql(['a', 'b']); - }); - - it('trims selectors', () => { - var rule = new Rule({ selector: ".a\n, .b , .c" }); - expect(rule.selectors).to.eql(['.a', '.b', '.c']); - }); - - it('is smart about commas', () => { - // Note: We don’t have to care about unquoted attribute values - // (such as `[foo=a,b]`), because that is invalid CSS. - var rule = new Rule({ - selector: "[foo='a, b'], a:-moz-any(:focus, [href*=','])" - }); - expect(rule.selectors).to.eql([ - "[foo='a, b']", - "a:-moz-any(:focus, [href*=','])"]); - }); - - it('receive array', () => { - var rule = new Rule({ selector: 'a,b' }); - rule.selectors = ['em', 'strong']; - expect(rule.selector).to.eql('em, strong'); - }); - - }); - - describe('toString()', () => { - - it('inserts default spaces', () => { - var rule = new Rule({ selector: 'a' }); - expect(rule.toString()).to.eql('a {}'); - rule.append({ prop: 'color', value: 'black' }); - expect(rule.toString()).to.eql('a {\n color: black\n}'); - }); - - it('clones spaces from another rule', () => { - var root = parse("a{\n }b{\n }"); - var rule = new Rule({ selector: 'em' }); - root.append(rule); - - expect(rule.toString()).to.eql("em{\n }"); - }); - - it('uses different spaces for empty rules', () => { - var root = parse("a { }\nb {\n color: black\n}"); - var rule = new Rule({ selector: 'em' }); - root.append(rule); - - expect(rule.toString()).to.eql("\nem { }"); - - rule.append({ prop: 'top', value: '0' }); - expect(rule.toString()).to.eql("\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 new file mode 100755 index 000000000..f845650e2 --- /dev/null +++ b/test/stringifier.test.js @@ -0,0 +1,480 @@ +let { test } = require('uvu') +let { is } = require('uvu/assert') + +let { + AtRule, + Comment, + Declaration, + Document, + Node, + parse, + Root, + Rule +} = require('../lib/postcss') +let Stringifier = require('../lib/stringifier') + +let str + +test.before.each(() => { + str = new Stringifier() +}) + +test('creates trimmed/raw property', () => { + let b = new Node({ one: 'trim' }) + b.raws.one = { raw: 'raw', value: 'trim' } + is(str.rawValue(b, 'one'), 'raw') + + b.one = 'trim1' + is(str.rawValue(b, 'one'), 'trim1') +}) + +test('works without rawValue magic', () => { + let b = new Node() + b.one = '1' + is(b.one, '1') + is(str.rawValue(b, 'one'), '1') +}) + +test('uses node raw', () => { + let rule = new Rule({ raws: { between: '\n' }, selector: 'a' }) + is(str.raw(rule, 'between', 'beforeOpen'), '\n') +}) + +test('hacks before for nodes without parent', () => { + let rule = new Rule({ selector: 'a' }) + is(str.raw(rule, 'before'), '') +}) + +test('hacks before for first node', () => { + let root = new Root() + root.append(new Rule({ selector: 'a' })) + is(str.raw(root.first, 'before'), '') +}) + +test('hacks before for first decl', () => { + let decl = new Declaration({ prop: 'color', value: 'black' }) + is(str.raw(decl, 'before'), '') + + let rule = new Rule({ selector: 'a' }) + rule.append(decl) + is(str.raw(decl, 'before'), '\n ') +}) + +test('detects after raw', () => { + let root = new Root() + root.append({ raws: { after: ' ' }, selector: 'a' }) + root.first.append({ prop: 'color', value: 'black' }) + root.append({ selector: 'a' }) + is(str.raw(root.last, 'after'), ' ') +}) + +test('uses defaults without parent', () => { + let rule = new Rule({ selector: 'a' }) + is(str.raw(rule, 'between', 'beforeOpen'), ' ') +}) + +test('uses defaults for unique node', () => { + let root = new Root() + root.append(new Rule({ selector: 'a' })) + is(str.raw(root.first, 'between', 'beforeOpen'), ' ') +}) + +test('clones raw from first node', () => { + let root = new Root() + root.append(new Rule({ raws: { between: '' }, selector: 'a' })) + root.append(new Rule({ selector: 'b' })) + + is(str.raw(root.last, 'between', 'beforeOpen'), '') +}) + +test('indents by default', () => { + let root = new Root() + root.append(new AtRule({ name: 'page' })) + root.first.append(new Rule({ selector: 'a' })) + root.first.first.append({ prop: 'color', value: 'black' }) + + is( + root.toString(), + '@page {\n' + ' a {\n' + ' color: black\n' + ' }\n' + '}' + ) +}) + +test('clones style', () => { + let compress = parse('@page{ a{ } }') + let spaces = parse('@page {\n a {\n }\n}') + + compress.first.first.append({ prop: 'color', value: 'black' }) + is(compress.toString(), '@page{ a{ color: black } }') + + spaces.first.first.append({ prop: 'color', value: 'black' }) + is(spaces.toString(), '@page {\n a {\n color: black\n }\n}') +}) + +test('clones indent', () => { + let root = parse('a{\n}') + root.first.append({ text: 'a' }) + root.first.append({ raws: { before: '\n\n ' }, text: 'b' }) + is(root.toString(), 'a{\n\n /* a */\n\n /* b */\n}') +}) + +test('clones declaration before for comment', () => { + let root = parse('a{\n}') + root.first.append({ text: 'a' }) + root.first.append({ + prop: 'a', + raws: { before: '\n\n ' }, + value: '1' + }) + is(root.toString(), 'a{\n\n /* a */\n\n a: 1\n}') +}) + +test('clones indent by types', () => { + let css = parse('a {\n *color: black\n}\n\nb {\n}') + css.append(new Rule({ selector: 'em' })) + css.last.append({ prop: 'z-index', value: '1' }) + is(css.last.first.raw('before'), '\n ') +}) + +test('ignores non-space symbols in indent cloning', () => { + let css = parse('a {\n color: black\n}\n\nb {\n}') + css.append(new Rule({ selector: 'em' })) + css.last.append({ prop: 'z-index', value: '1' }) + + is(css.last.raw('before'), '\n\n') + is(css.last.first.raw('before'), '\n ') +}) + +test('clones indent by before and after', () => { + let css = parse('@page{\n\n a{\n color: black}}') + css.first.append(new Rule({ selector: 'b' })) + css.first.last.append({ prop: 'z-index', value: '1' }) + + is(css.first.last.raw('before'), '\n\n ') + is(css.first.last.raw('after'), '') +}) + +test('clones semicolon only from rules with children', () => { + let css = parse('a{}b{one:1;}') + is(str.raw(css.first, 'semicolon'), true) +}) + +test('terminates childless at-rule followed by a comment', () => { + let css = parse('a {}\n/* comment */') + css.insertBefore(css.last, new AtRule({ name: 'import', params: '"x.css"' })) + + is(css.toString(), 'a {}\n@import "x.css";\n/* comment */') + is( + parse(css.toString()) + .nodes.map(i => i.type) + .join(','), + 'rule,atrule,comment' + ) +}) + +test('terminates nested childless at-rule followed by a comment', () => { + let css = parse('@media screen {\n a {}\n}') + css.first.append(new AtRule({ name: 'import', params: '"y.css"' })) + css.first.append(new Comment({ text: 'note' })) + + is( + css.toString(), + '@media screen {\n a {}\n @import "y.css";\n /* note */\n}' + ) + is( + parse(css.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'rule,atrule,comment' + ) +}) + +test('terminates custom property followed by a comment', () => { + let css = parse('a{--x:red}') + css.first.append(new Comment({ text: 'note' })) + + is(css.toString(), 'a{--x:red;/* note */}') + is( + parse(css.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'decl,comment' + ) +}) + +test('terminates custom property with !important before a comment', () => { + let css = parse('a{--x:red !important}') + css.first.first.after(new Comment({ text: 'note' })) + + is(css.toString(), 'a{--x:red !important;/* note */}') + is( + parse(css.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'decl,comment' + ) +}) + +test('keeps hack-prefixed property before a comment unchanged', () => { + for (let css of ['a{*--x:red/*c*/}', 'a{_--x:red/*c*/}']) { + let root = parse(css) + is(root.toString(), css) + is( + parse(root.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'decl,comment' + ) + } +}) + +test('terminates indented custom property followed by a comment', () => { + let css = parse('a{ --x:red}') + css.first.first.after(new Comment({ text: 'note' })) + + is(css.toString(), 'a{ --x:red; /* note */}') + is( + parse(css.toString()) + .first.nodes.map(i => i.type) + .join(','), + 'decl,comment' + ) +}) + +test('clones only spaces in before', () => { + let css = parse('a{*one:1}') + css.first.append({ prop: 'two', value: '2' }) + css.append({ name: 'keyframes', params: 'a' }) + css.last.append({ selector: 'from' }) + is(css.toString(), 'a{*one:1;two:2}\n@keyframes a{\nfrom{}}') +}) + +test('clones only spaces in between', () => { + let css = parse('a{one/**/:1}') + css.first.append({ prop: 'two', value: '2' }) + is(css.toString(), 'a{one/**/:1;two:2}') +}) + +test('uses optional raws.indent', () => { + let rule = new Rule({ raws: { indent: ' ' }, selector: 'a' }) + rule.append({ prop: 'color', value: 'black' }) + is(rule.toString(), 'a {\n color: black\n}') +}) + +test('handles nested roots', () => { + let root = new Root() + let subRoot = new Root() + subRoot.append(new AtRule({ name: 'foo' })) + root.append(subRoot) + + is(root.toString(), '@foo') +}) + +test('handles root', () => { + let root = new Root() + root.append(new AtRule({ name: 'foo' })) + + let s = root.toString() + + is(s, '@foo') +}) + +test('handles root with after', () => { + let root = new Root({ raws: { after: ' ' } }) + root.append(new AtRule({ name: 'foo' })) + + let s = root.toString() + + is(s, '@foo ') +}) + +test('pass nodes to document', () => { + let root = new Root() + let document = new Document({ nodes: [root] }) + + is(document.toString(), '') +}) + +test('handles document with one root', () => { + let root = new Root() + root.append(new AtRule({ name: 'foo' })) + + let document = new Document() + document.append(root) + + let s = document.toString() + + is(s, '@foo') +}) + +test('handles document with one root and after raw', () => { + let document = new Document() + let root = new Root({ raws: { after: ' ' } }) + root.append(new AtRule({ name: 'foo' })) + document.append(root) + + let s = document.toString() + + is(s, '@foo ') +}) + +test('handles document with one root and before and after', () => { + let document = new Document() + let root = new Root({ raws: { after: 'AFTER' } }) + root.append(new AtRule({ name: 'foo' })) + document.append(root) + + let s = document.toString() + + is(s, '@fooAFTER') +}) + +test('handles document with three roots without raws', () => { + let root1 = new Root() + root1.append(new AtRule({ name: 'foo' })) + + let root2 = new Root() + root2.append(new Rule({ selector: 'a' })) + + let root3 = new Root() + root3.append(new Declaration({ prop: 'color', value: 'black' })) + + let document = new Document() + document.append(root1) + document.append(root2) + document.append(root3) + + let s = document.toString() + + is(s, '@fooa {}color: black') +}) + +test('handles document with three roots, with before and after raws', () => { + let root1 = new Root({ raws: { after: 'AFTER_ONE' } }) + root1.append(new Rule({ selector: 'a.one' })) + + let root2 = new Root({ raws: { after: 'AFTER_TWO' } }) + root2.append(new Rule({ selector: 'a.two' })) + + let root3 = new Root({ raws: { after: 'AFTER_THREE' } }) + root3.append(new Rule({ selector: 'a.three' })) + + let document = new Document() + document.append(root1) + document.append(root2) + document.append(root3) + + let s = document.toString() + + is(s, 'a.one {}AFTER_ONEa.two {}AFTER_TWOa.three {}AFTER_THREE') +}) + +test('escapes { + let root = new Root() + root.append(new Rule({ selector: '' })) + root.append(new AtRule({ name: 'media', params: '