From eb896ce2f429dbf8304546ffc152c99fd8bdd467 Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Sun, 16 May 2021 19:33:59 +0200 Subject: [PATCH 01/12] Add Document node and related API (#1498) --- lib/container.js | 2 +- lib/document.d.ts | 264 ++++++++++++++++++++ lib/document.js | 33 +++ lib/lazy-result.js | 51 +++- lib/node.d.ts | 12 + lib/node.js | 13 +- lib/postcss.d.ts | 26 +- lib/postcss.js | 3 + lib/postcss.mjs | 2 + lib/processor.js | 2 + lib/root.d.ts | 7 +- lib/stringifier.js | 11 +- test/document.test.ts | 26 ++ test/node.test.ts | 46 +++- test/postcss.test.ts | 24 ++ test/stringifier.test.js | 114 ++++++++- test/visitor.test.ts | 508 ++++++++++++++++++++++++++++++++++++++- 17 files changed, 1121 insertions(+), 23 deletions(-) create mode 100644 lib/document.d.ts create mode 100644 lib/document.js create mode 100644 test/document.test.ts diff --git a/lib/container.js b/lib/container.js index 25ab486b3..42b1b417a 100644 --- a/lib/container.js +++ b/lib/container.js @@ -310,7 +310,7 @@ class Container extends Node { for (let i of nodes) { if (i.parent) i.parent.removeChild(i, 'ignore') } - } else if (nodes.type === 'root') { + } else if (nodes.type === 'root' && this.type !== 'document') { nodes = nodes.nodes.slice(0) for (let i of nodes) { if (i.parent) i.parent.removeChild(i, 'ignore') diff --git a/lib/document.d.ts b/lib/document.d.ts new file mode 100644 index 000000000..7b05fc8a8 --- /dev/null +++ b/lib/document.d.ts @@ -0,0 +1,264 @@ +import Container, { ContainerProps } from './container.js' +import { ProcessOptions } from './postcss.js' +import Result from './result.js' +import Root, { RootProps } from './root.js' + +export interface DocumentProps extends ContainerProps { + nodes?: Root[] +} + +type ChildNode = Root +type ChildProps = RootProps + +/** + * Represents a file and contains all its parsed nodes. + * + * ```js + * const document = postcss.parse('') + * document.type //=> 'document' + * document.nodes.length //=> 2 + * ``` + */ +export default class Document extends Container { + type: 'document' + parent: undefined + + constructor(defaults?: DocumentProps) + + /** + * 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 + + /** + * An array containing the container’s children. + * + * ```js + * const root = postcss.parse('a { color: black }') + * root.nodes.length //=> 1 + * root.nodes[0].selector //=> 'a' + * root.nodes[0].nodes[0].prop //=> 'color' + * ``` + */ + // @ts-expect-error + nodes: Root[] + + /** + * The container’s first child. + * + * ```js + * rule.first === rules.nodes[0] + * ``` + */ + // @ts-expect-error + get first(): ChildNode | undefined + + /** + * The container’s last child. + * + * ```js + * rule.last === rule.nodes[rule.nodes.length - 1] + * ``` + */ + // @ts-expect-error + get last(): ChildNode | undefined + + /** + * Iterates through the container’s immediate children, + * calling `callback` for each child. + * + * Returning `false` in the callback will break iteration. + * + * This method only iterates through the container’s immediate children. + * If you need to recursively iterate through all the container’s descendant + * nodes, use `Container#walk`. + * + * 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 + * const root = postcss.parse('a { color: black; z-index: 1 }') + * const rule = root.first + * + * for (const decl of rule.nodes) { + * decl.cloneBefore({ prop: '-webkit-' + decl.prop }) + * // Cycle will be infinite, because cloneBefore moves the current node + * // to the next index + * } + * + * rule.each(decl => { + * decl.cloneBefore({ prop: '-webkit-' + decl.prop }) + * // Will be executed only for color and z-index + * }) + * ``` + * + * @param callback Iterator receives each node and index. + * @return Returns `false` if iteration was broke. + */ + // @ts-expect-error + each( + callback: (node: ChildNode, index: number) => false | void + ): false | undefined + + /** + * Inserts new nodes to the end of the container. + * + * ```js + * const decl1 = new Declaration({ prop: 'color', value: 'black' }) + * const decl2 = new Declaration({ prop: 'background-color', value: 'white' }) + * rule.append(decl1, decl2) + * + * 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 + * + * root.append('a {}') + * root.first.append('color: black; z-index: 1') + * ``` + * + * @param nodes New nodes. + * @return This node for methods chain. + */ + append(...nodes: (ChildProps | ChildProps[])[]): this + + /** + * Inserts new nodes to the start of the container. + * + * ```js + * const decl1 = new Declaration({ prop: 'color', value: 'black' }) + * const decl2 = new Declaration({ prop: 'background-color', value: 'white' }) + * rule.prepend(decl1, decl2) + * + * 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 + * + * root.append('a {}') + * root.first.append('color: black; z-index: 1') + * ``` + * + * @param nodes New nodes. + * @return This node for methods chain. + */ + prepend(...nodes: (ChildProps | ChildProps[])[]): this + + /** + * Add child to the end of the node. + * + * ```js + * rule.push(new Declaration({ prop: 'color', value: 'black' })) + * ``` + * + * @param child New node. + * @return This node for methods chain. + */ + // @ts-expect-error + push(child: ChildNode): this + + /** + * Insert new node before old node within the container. + * + * ```js + * rule.insertBefore(decl, decl.clone({ prop: '-webkit-' + decl.prop })) + * ``` + * + * @param oldNode Child or child’s index. + * @param newNode New node. + * @return This node for methods chain. + */ + // @ts-expect-error + insertBefore( + oldNode: ChildNode | number, + newNode: ChildNode | ChildProps | ChildNode[] | ChildProps[] + ): this + + /** + * Insert new node after old node within the container. + * + * @param oldNode Child or child’s index. + * @param newNode New node. + * @return This node for methods chain. + */ + // @ts-expect-error + insertAfter( + oldNode: ChildNode | number, + newNode: ChildNode | ChildProps | ChildNode[] | ChildProps[] + ): this + + /** + * Removes node from the container and cleans the parent properties + * from the node and its children. + * + * ```js + * rule.nodes.length //=> 5 + * rule.removeChild(decl) + * rule.nodes.length //=> 4 + * decl.parent //=> undefined + * ``` + * + * @param child Child or child’s index. + * @return This node for methods chain. + */ + // @ts-expect-error + removeChild(child: ChildNode | number): this + + /** + * Returns `true` if callback returns `true` + * for all of the container’s children. + * + * ```js + * const noPrefixes = rule.every(i => i.prop[0] !== '-') + * ``` + * + * @param condition Iterator returns true or false. + * @return Is every child pass condition. + */ + // @ts-expect-error + every( + condition: (node: ChildNode, index: number, nodes: ChildNode[]) => boolean + ): boolean + + /** + * Returns `true` if callback returns `true` for (at least) one + * of the container’s children. + * + * ```js + * const hasPrefix = rule.some(i => i.prop[0] === '-') + * ``` + * + * @param condition Iterator returns true or false. + * @return Is some child pass condition. + */ + // @ts-expect-error + some( + condition: (node: ChildNode, index: number, nodes: ChildNode[]) => boolean + ): boolean + + /** + * Returns a `child`’s index within the `Container#nodes` array. + * + * ```js + * rule.index( rule.nodes[2] ) //=> 2 + * ``` + * + * @param child Child of the current container. + * @return Child index. + */ + // @ts-expect-error + index(child: ChildNode | number): number +} 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/lazy-result.js b/lib/lazy-result.js index 6d9674969..d5503f335 100644 --- a/lib/lazy-result.js +++ b/lib/lazy-result.js @@ -7,8 +7,10 @@ let warnOnce = require('./warn-once') let Result = require('./result') let parse = require('./parse') let Root = require('./root') +let Document = require('./document') const TYPE_TO_CLASS_NAME = { + document: 'Document', root: 'Root', atrule: 'AtRule', rule: 'Rule', @@ -20,6 +22,7 @@ const PLUGIN_PROPS = { postcssPlugin: true, prepare: true, Once: true, + Document: true, Root: true, Declaration: true, Rule: true, @@ -30,6 +33,7 @@ const PLUGIN_PROPS = { AtRuleExit: true, CommentExit: true, RootExit: true, + DocumentExit: true, OnceExit: true } @@ -73,7 +77,9 @@ function getEvents(node) { function toStack(node) { let events - if (node.type === 'root') { + if (node.type === 'document') { + events = ['Document', CHILDREN, 'DocumentExit'] + } else if (node.type === 'root') { events = ['Root', CHILDREN, 'RootExit'] } else { events = getEvents(node) @@ -103,7 +109,11 @@ class LazyResult { this.processed = false let root - if (typeof css === 'object' && css !== null && css.type === '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) @@ -231,7 +241,13 @@ class LazyResult { this.walkSync(root) } if (this.listeners.OnceExit) { - this.visitSync(this.listeners.OnceExit, root) + if (root.type === 'document') { + for (let subRoot of root.nodes) { + this.visitSync(this.listeners.OnceExit, subRoot) + } + } else { + this.visitSync(this.listeners.OnceExit, root) + } } } @@ -287,7 +303,9 @@ class LazyResult { } catch (e) { throw this.handleError(e, node.proxyOf) } - if (node.type !== 'root' && !node.parent) return true + if (node.type !== 'root' && node.type !== 'document' && !node.parent) { + return true + } if (isPromise(promise)) { throw this.getAsyncError() } @@ -298,6 +316,18 @@ class LazyResult { 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) @@ -385,7 +415,15 @@ class LazyResult { for (let [plugin, visitor] of this.listeners.OnceExit) { this.result.lastPlugin = plugin try { - await visitor(root, this.helpers) + 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) } @@ -439,7 +477,7 @@ class LazyResult { let visit = stack[stack.length - 1] let { node, visitors } = visit - if (node.type !== 'root' && !node.parent) { + if (node.type !== 'root' && node.type !== 'document' && !node.parent) { stack.pop() return } @@ -501,3 +539,4 @@ module.exports = LazyResult LazyResult.default = LazyResult Root.registerLazyResult(LazyResult) +Document.registerLazyResult(LazyResult) diff --git a/lib/node.d.ts b/lib/node.d.ts index 466058506..e9773913d 100644 --- a/lib/node.d.ts +++ b/lib/node.d.ts @@ -9,6 +9,7 @@ import Container from './container.js' import Result from './result.js' import Input from './input.js' import Root from './root.js' +import Document from './document.js' export type ChildNode = AtRule | Rule | Declaration | Comment @@ -381,6 +382,17 @@ export default abstract class Node { */ root(): Root + /** + * Finds the Document instance of the node’s tree. + * + * ```js + * document.nodes[0].nodes[0].document() === document + * ``` + * + * @return Document parent. + */ + document(): Document + /** * Returns a `Node#raws` value. If the node is missing * the code style property (because the node was manually built or cloned), diff --git a/lib/node.js b/lib/node.js index 5ea7de8e1..443281932 100644 --- a/lib/node.js +++ b/lib/node.js @@ -151,7 +151,18 @@ class Node { root() { let result = this - while (result.parent) result = result.parent + while (result.parent && result.parent.type !== 'document') { + result = result.parent + } + return result + } + + document() { + let result = this + while (result.parent) { + result = result.parent + } + return result } diff --git a/lib/postcss.d.ts b/lib/postcss.d.ts index dd1f116a9..980707ecb 100644 --- a/lib/postcss.d.ts +++ b/lib/postcss.d.ts @@ -11,6 +11,7 @@ import Node, { } from './node.js' import Declaration, { DeclarationProps } from './declaration.js' import Root, { RootProps } from './root.js' +import Document, { DocumentProps } from './document.js' import Comment, { CommentProps } from './comment.js' import AtRule, { AtRuleProps } from './at-rule.js' import Result, { Message } from './result.js' @@ -50,6 +51,7 @@ export { AtRule, Rule, Root, + Document, Result, LazyResult, Input @@ -74,6 +76,20 @@ type CommentProcessor = ( ) => Promise | void interface Processors { + /** + * Will be called on `Document` node. + * + * Will be called again on children changes. + */ + Document?: RootProcessor + + /** + * Will be called on `Document` node, when all children will be processed. + * + * Will be called again on children changes. + */ + DocumentExit?: RootProcessor + /** * Will be called on `Root` node once. */ @@ -347,7 +363,7 @@ export interface Postcss { stringify: Stringifier /** - * Parses source css and returns a new `Root` node, + * Parses source css and returns a new `Root` or `Document` node, * which contains the source CSS nodes. * * ```js @@ -415,6 +431,14 @@ export interface Postcss { */ root(defaults?: RootProps): Root + /** + * Creates a new `Document` node. + * + * @param defaults Properties for the new node. + * @return New document node. + */ + document(defaults?: DocumentProps): Document + CssSyntaxError: typeof CssSyntaxError Declaration: typeof Declaration Container: typeof Container diff --git a/lib/postcss.js b/lib/postcss.js index ce177a814..b03bb1803 100644 --- a/lib/postcss.js +++ b/lib/postcss.js @@ -7,6 +7,7 @@ let Container = require('./container') let Processor = require('./processor') let stringify = require('./stringify') let fromJSON = require('./fromJSON') +let Document = require('./document') let Warning = require('./warning') let Comment = require('./comment') let AtRule = require('./at-rule') @@ -73,10 +74,12 @@ postcss.atRule = defaults => new AtRule(defaults) postcss.decl = defaults => new Declaration(defaults) postcss.rule = defaults => new Rule(defaults) postcss.root = defaults => new Root(defaults) +postcss.document = defaults => new Document(defaults) postcss.CssSyntaxError = CssSyntaxError postcss.Declaration = Declaration postcss.Container = Container +postcss.Document = Document postcss.Comment = Comment postcss.Warning = Warning postcss.AtRule = AtRule diff --git a/lib/postcss.mjs b/lib/postcss.mjs index e6360c495..a8b633830 100644 --- a/lib/postcss.mjs +++ b/lib/postcss.mjs @@ -13,10 +13,12 @@ export const atRule = postcss.atRule export const rule = postcss.rule export const decl = postcss.decl export const root = postcss.root +export const document = postcss.document export const CssSyntaxError = postcss.CssSyntaxError export const Declaration = postcss.Declaration export const Container = postcss.Container +export const Document = postcss.Document export const Comment = postcss.Comment export const Warning = postcss.Warning export const AtRule = postcss.AtRule diff --git a/lib/processor.js b/lib/processor.js index 7fea35703..5d08513d6 100644 --- a/lib/processor.js +++ b/lib/processor.js @@ -2,6 +2,7 @@ let LazyResult = require('./lazy-result') let Root = require('./root') +let Document = require('./document') class Processor { constructor(plugins = []) { @@ -68,3 +69,4 @@ module.exports = Processor Processor.default = Processor Root.registerProcessor(Processor) +Document.registerProcessor(Processor) diff --git a/lib/root.d.ts b/lib/root.d.ts index 37d948f71..d8ee39531 100644 --- a/lib/root.d.ts +++ b/lib/root.d.ts @@ -3,6 +3,11 @@ import { ProcessOptions } from './postcss.js' import Result from './result.js' interface RootRaws { + /** + * The symbols before the first child to the start of file. + */ + before?: string + /** * The space symbols after the last child to the end of file. */ @@ -29,7 +34,7 @@ export interface RootProps extends ContainerProps { */ export default class Root extends Container { type: 'root' - parent: undefined + // parent: Document | undefined raws: RootRaws constructor(defaults?: RootProps) diff --git a/lib/stringifier.js b/lib/stringifier.js index a34394640..81e125182 100644 --- a/lib/stringifier.js +++ b/lib/stringifier.js @@ -37,6 +37,10 @@ class Stringifier { this[node.type](node, semicolon) } + document(node) { + this.body(node) + } + root(node) { this.body(node) if (node.raws.after) this.builder(node.raws.after) @@ -129,11 +133,16 @@ class Stringifier { let parent = node.parent - // Hack for first rule in CSS if (detect === 'before') { + // Hack for first rule in CSS if (!parent || (parent.type === 'root' && parent.first === node)) { return '' } + + // `root` nodes in `document` should use only their own raws + if (parent && parent.type === 'document') { + return '' + } } // Floating child without parent diff --git a/test/document.test.ts b/test/document.test.ts new file mode 100644 index 000000000..0d1253043 --- /dev/null +++ b/test/document.test.ts @@ -0,0 +1,26 @@ +import { Result, parse } from '../lib/postcss.js' +import Document from '../lib/document.js' + +it('generates result without map', () => { + let root = parse('a {}') + let document = new Document() + + document.append(root) + + let result = document.toResult() + + expect(result instanceof Result).toBe(true) + expect(result.css).toBe('a {}') +}) + +it('generates result with map', () => { + let root = parse('a {}') + let document = new Document() + + document.append(root) + + let result = document.toResult({ map: true }) + + expect(result instanceof Result).toBe(true) + expect(result.css).toMatch(/a {}\n\/\*# sourceMappingURL=/) +}) diff --git a/test/node.test.ts b/test/node.test.ts index d60141408..780c175a1 100644 --- a/test/node.test.ts +++ b/test/node.test.ts @@ -9,7 +9,8 @@ import postcss, { Declaration, parse, Result, - Plugin + Plugin, + Document } from '../lib/postcss.js' function stringify(node: AnyNode, builder: (str: string) => void): void { @@ -321,6 +322,49 @@ it('root() returns self on root', () => { expect(rule.root()).toBe(rule) }) +it('root() returns root in document', () => { + let css = new Document({ nodes: [parse('@page{a{color:black}}')] }) + + let root = css.first as Root + let page = root.first as AtRule + let a = page.first as Rule + let color = a.first as Declaration + expect(color.root()).toBe(root) +}) + +it('root() on root in document returns same root', () => { + let document = new Document() + let root = new Root() + document.append(root) + + expect(document.first?.root()).toBe(root) +}) + +it('root() returns self on document', () => { + let document = new Document() + expect(document.root()).toBe(document) +}) + +it('document() returns document', () => { + let css = new Document({ nodes: [parse('@page{a{color:black}}')] }) + + let root = css.first as Root + let page = root.first as AtRule + let a = page.first as Rule + let color = a.first as Declaration + expect(color.document()).toBe(css) +}) + +it('document() returns self on root', () => { + let rule = new Rule({ selector: 'a' }) + expect(rule.document()).toBe(rule) +}) + +it('document() returns self on document', () => { + let document = new Document() + expect(document.document()).toBe(document) +}) + it('cleanRaws() cleans style recursivelly', () => { let css = parse('@page{a{color:black}}') css.cleanRaws() diff --git a/test/postcss.test.ts b/test/postcss.test.ts index 7dd35e2df..8d3dd4a2e 100644 --- a/test/postcss.test.ts +++ b/test/postcss.test.ts @@ -135,6 +135,30 @@ it('allows to build own CSS', () => { ) }) +it('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) + + expect(document.toString()).toEqual( + '/* Example */\n' + + '@media screen {\n' + + ' a {\n' + + ' color: black\n' + + ' }\n' + + '}\n' + ) +}) + it('contains list module', () => { expect(postcss.list.space('a b')).toEqual(['a', 'b']) }) diff --git a/test/stringifier.test.js b/test/stringifier.test.js index f293bf2bf..c199ded1b 100644 --- a/test/stringifier.test.js +++ b/test/stringifier.test.js @@ -1,4 +1,4 @@ -let { Declaration, AtRule, Node, Root, Rule, parse } = require('..') +let { Declaration, AtRule, Node, Root, Rule, Document, parse } = require('..') let Stringifier = require('../lib/stringifier') let str @@ -172,3 +172,115 @@ it('handles nested roots', () => { expect(root.toString()).toEqual('@foo') }) + +it('handles root', () => { + let root = new Root() + root.append(new AtRule({ name: 'foo' })) + + let s = root.toString() + + expect(s).toEqual('@foo') +}) + +it('handles root with after', () => { + let root = new Root({ raws: { after: ' ' } }) + root.append(new AtRule({ name: 'foo' })) + + let s = root.toString() + + expect(s).toEqual('@foo ') +}) + +it('pass nodes to document', () => { + let root = new Root() + let document = new Document({ nodes: [root] }) + + expect(document.toString()).toEqual('') +}) + +it('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() + + expect(s).toEqual('@foo') +}) + +it('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() + + expect(s).toEqual('@foo ') +}) + +it('handles document with one root and before raw', () => { + let document = new Document() + let root = new Root({ raws: { before: ' ' } }) + root.append(new AtRule({ name: 'foo' })) + document.append(root) + + let s = document.toString() + + expect(s).toEqual(' @foo') +}) + +it('handles document with one root and before and after', () => { + let document = new Document() + let root = new Root({ raws: { before: 'BEFORE', after: 'AFTER' } }) + root.append(new AtRule({ name: 'foo' })) + document.append(root) + + let s = document.toString() + + expect(s).toEqual('BEFORE@fooAFTER') +}) + +it('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() + + expect(s).toEqual('@fooa {}color: black') +}) + +it('handles document with three roots, with before and after raws', () => { + let root1 = new Root({ raws: { before: 'BEFORE_ONE', 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() + + expect(s).toEqual( + 'BEFORE_ONEa.one {}AFTER_ONEa.two {}AFTER_TWOa.three {}AFTER_THREE' + ) +}) diff --git a/test/visitor.test.ts b/test/visitor.test.ts index 76102847c..5a45992ac 100644 --- a/test/visitor.test.ts +++ b/test/visitor.test.ts @@ -29,6 +29,9 @@ function buildVisitor(): [[string, string][], Plugin] { let visits: [string, string][] = [] let visitor: Plugin = { postcssPlugin: 'visitor', + Document(i) { + visits.push(['Document', `${i.nodes.length}`]) + }, Once(i) { visits.push(['Once', `${i.nodes.length}`]) }, @@ -64,6 +67,9 @@ function buildVisitor(): [[string, string][], Plugin] { }, OnceExit(i) { visits.push(['OnceExit', `${i.nodes.length}`]) + }, + DocumentExit(i) { + visits.push(['DocumentExit', `${i.nodes.length}`]) } } return [visits, visitor] @@ -255,9 +261,10 @@ it('works classic plugin replace-color', async () => { }) it('works visitor plugin will-change', async () => { - let { css } = postcss([ - willChangeVisitor - ]).process('.foo { will-change: transform; }', { from: 'a.css' }) + let { css } = postcss([willChangeVisitor]).process( + '.foo { will-change: transform; }', + { from: 'a.css' } + ) expect(css).toEqual( '.foo { backface-visibility: hidden; will-change: transform; }' ) @@ -276,11 +283,39 @@ it('works visitor plugin add-prop', async () => { ) }) +it('works visitor plugin add-prop in document with single root', async () => { + let document = postcss.document({ + nodes: [postcss.parse('.a{ color: red; } .b{ will-change: transform; }')] + }) + + let { css } = await postcss([addPropsVisitor]).process(document, { + from: 'a.css' + }) + expect(css).toEqual( + '.a{ will-change: transform; color: red; } ' + + '.b{ will-change: transform; }' + ) +}) + +it('works visitor plugin add-prop in document with two roots', async () => { + let document = postcss.document({ + nodes: [ + postcss.parse('.a{ color: red; }'), + postcss.parse('.b{ will-change: transform; }') + ] + }) + + let { css } = await postcss([addPropsVisitor]).process(document, { + from: 'a.css' + }) + expect(css).toEqual('.a{ color: red; }' + '.b{ will-change: transform; }') +}) + it('works with at-rule params', () => { - let { css } = postcss([ - replacePrintToMobile, - replaceScreenToPrint - ]).process('@media (screen) {}', { from: 'a.css' }) + let { css } = postcss([replacePrintToMobile, replaceScreenToPrint]).process( + '@media (screen) {}', + { from: 'a.css' } + ) expect(css).toEqual('@media (mobile) {}') }) @@ -338,6 +373,35 @@ it('work of three plug-ins; sequence 2', async () => { expect(css).toEqual(expectedThree) }) +const cssThreeDocument = postcss.document({ + nodes: [ + postcss.parse('.a{ color: red; }'), + postcss.parse('.b{ will-change: transform; }') + ] +}) + +const expectedThreeDocument = + '.a{ color: green; }' + + '.b{ backface-visibility: hidden; will-change: transform; }' + +it('work of three plug-ins in a document; sequence 1', async () => { + let { css } = await postcss([ + replaceColorGreenClassic, + willChangeVisitor, + addPropsVisitor + ]).process(cssThreeDocument, { from: 'a.css' }) + expect(css).toEqual(expectedThreeDocument) +}) + +it('work of three plug-ins in a document; sequence 2', async () => { + let { css } = await postcss([ + addPropsVisitor, + replaceColorGreenClassic, + willChangeVisitor + ]).process(cssThreeDocument, { from: 'a.css' }) + expect(css).toEqual(expectedThreeDocument) +}) + const cssThroughProps = '.a{color: yellow;}' const expectedThroughProps = '.a{color: red;}' @@ -586,6 +650,49 @@ it('passes helpers', async () => { await postcss([asyncPlugin]).process('a{}', { from: 'a.css' }) }) +it('passes helpers in a document', async () => { + function check(node: AnyNode, helpers: Helpers): void { + expect(helpers.result.messages).toEqual([]) + expect(typeof helpers.postcss).toEqual('function') + expect(helpers.comment().type).toEqual('comment') + expect(new helpers.Comment().type).toEqual('comment') + expect(helpers.list).toBe(postcss.list) + } + + let syncPlugin: Plugin = { + postcssPlugin: 'syncPlugin', + Once: check, + Rule: check, + RuleExit: check, + OnceExit: check + } + + let asyncPlugin: Plugin = { + postcssPlugin: 'syncPlugin', + async Once(node, helpers) { + await delay(1) + check(node, helpers) + }, + async Rule(node, helpers) { + await delay(1) + check(node, helpers) + }, + async OnceExit(node, helpers) { + await delay(1) + check(node, helpers) + } + } + + postcss([syncPlugin]).process( + postcss.document({ nodes: [postcss.parse('a{}')] }), + { from: 'a.css' } + ).css + await postcss([asyncPlugin]).process( + postcss.document({ nodes: [postcss.parse('a{}')] }), + { from: 'a.css' } + ) +}) + it('detects non-changed values', () => { let plugin: Plugin = { postcssPlugin: 'test', @@ -766,6 +873,56 @@ for (let type of ['sync', 'async']) { ) }) + it(`walks ${type} through tree in a document`, async () => { + let document = postcss.document({ + nodes: [ + postcss.parse(`@media screen { + body { + /* comment */ + background: white; + padding: 10px; + } + a { + color: blue; + } + }`) + ] + }) + + let [visits, visitor] = buildVisitor() + let processor = postcss([visitor]).process(document, { from: 'a.css' }) + if (type === 'sync') { + processor.css + } else { + await processor + } + + expect(addIndex(visits)).toEqual( + addIndex([ + ['Once', '1'], + ['Document', '1'], + ['Root', '1'], + ['AtRule', 'media'], + ['Rule', 'body'], + ['Comment', 'comment'], + ['CommentExit', 'comment'], + ['Declaration', 'background: white'], + ['DeclarationExit', 'background: white'], + ['Declaration', 'padding: 10px'], + ['DeclarationExit', 'padding: 10px'], + ['RuleExit', 'body'], + ['Rule', 'a'], + ['Declaration', 'color: blue'], + ['DeclarationExit', 'color: blue'], + ['RuleExit', 'a'], + ['AtRuleExit', 'media'], + ['RootExit', '1'], + ['DocumentExit', '1'], + ['OnceExit', '1'] + ]) + ) + }) + it(`walks ${type} during transformations`, async () => { let [visits, visitor] = buildVisitor() let result = postcss([ @@ -882,6 +1039,136 @@ for (let type of ['sync', 'async']) { ) }) + it(`walks ${type} during transformations in a document`, async () => { + let document = postcss.document({ + nodes: [ + postcss.parse( + `.first { + color: red; + } + @define-mixin { + b { + color: red; + } + } + a { + color: red; + } + @media (screen) { + @insert-first; + } + .foo { + background: red; + } + @apply-mixin;` + ) + ] + }) + + let [visits, visitor] = buildVisitor() + let result = postcss([ + visitor, + redToGreen, + greenToBlue, + mixins, + fooToBar, + insertFirst + ]).process(document, { from: 'a.css' }) + let output + if (type === 'sync') { + output = result.css + } else { + output = (await result).css + } + + expect(output).toEqual( + `a { + color: blue; + } + @media (screen) {.first { + color: blue; + } + } + .bar { + background: red; + } + b { + color: blue; + }` + ) + expect(addIndex(visits)).toEqual( + addIndex([ + ['Once', '6'], + ['Document', '1'], + ['Root', '6'], + ['Rule', '.first'], + ['Declaration', 'color: red'], + ['DeclarationExit', 'color: green'], + ['RuleExit', '.first'], + ['AtRule', 'define-mixin'], + ['Rule', 'a'], + ['Declaration', 'color: red'], + ['DeclarationExit', 'color: green'], + ['RuleExit', 'a'], + ['AtRule', 'media'], + ['AtRule', 'insert-first'], + ['AtRuleExit', 'media'], + ['Rule', '.foo'], + ['Declaration', 'background: red'], + ['DeclarationExit', 'background: red'], + ['RuleExit', '.bar'], + ['AtRule', 'apply-mixin'], + ['RootExit', '4'], + ['DocumentExit', '1'], + ['Document', '1'], + ['Root', '4'], + ['Rule', 'a'], + ['Declaration', 'color: green'], + ['DeclarationExit', 'color: blue'], + ['RuleExit', 'a'], + ['AtRule', 'media'], + ['Rule', '.first'], + ['Declaration', 'color: green'], + ['DeclarationExit', 'color: blue'], + ['RuleExit', '.first'], + ['AtRuleExit', 'media'], + ['Rule', 'b'], + ['Declaration', 'color: red'], + ['DeclarationExit', 'color: green'], + ['RuleExit', 'b'], + ['RootExit', '4'], + ['DocumentExit', '1'], + ['Document', '1'], + ['Root', '4'], + ['Rule', 'a'], + ['Declaration', 'color: blue'], + ['DeclarationExit', 'color: blue'], + ['RuleExit', 'a'], + ['AtRule', 'media'], + ['Rule', '.first'], + ['Declaration', 'color: blue'], + ['DeclarationExit', 'color: blue'], + ['RuleExit', '.first'], + ['AtRuleExit', 'media'], + ['Rule', 'b'], + ['Declaration', 'color: green'], + ['DeclarationExit', 'color: blue'], + ['RuleExit', 'b'], + ['RootExit', '4'], + ['DocumentExit', '1'], + ['Document', '1'], + ['Root', '4'], + ['Rule', 'b'], + ['Declaration', 'color: blue'], + ['DeclarationExit', 'color: blue'], + ['RuleExit', 'b'], + ['RootExit', '4'], + ['DocumentExit', '1'], + ['OnceExit', '4'] + ]) + ) + }) + it(`has ${type} property and at-rule name filters`, async () => { let filteredDecls: string[] = [] let allDecls: string[] = [] @@ -918,9 +1205,7 @@ for (let type of ['sync', 'async']) { } } - let result = postcss([ - scanner - ]).process( + let result = postcss([scanner]).process( `@charset "UTF-8"; @media (screen) { COLOR: black; z-index: 1 }`, { from: 'a.css' } ) @@ -938,6 +1223,65 @@ for (let type of ['sync', 'async']) { expect(allAtRules).toEqual(['charset', 'media']) }) + it(`has ${type} property and at-rule name filters in a document`, async () => { + let filteredDecls: string[] = [] + let allDecls: string[] = [] + let filteredAtRules: string[] = [] + let allAtRules: string[] = [] + let allExits: string[] = [] + let filteredExits: string[] = [] + + let scanner: Plugin = { + postcssPlugin: 'test', + Declaration: { + 'color': decl => { + filteredDecls.push(decl.prop) + }, + '*': decl => { + allDecls.push(decl.prop) + } + }, + DeclarationExit: { + 'color': decl => { + filteredExits.push(decl.prop) + }, + '*': decl => { + allExits.push(decl.prop) + } + }, + AtRule: { + 'media': atRule => { + filteredAtRules.push(atRule.name) + }, + '*': atRule => { + allAtRules.push(atRule.name) + } + } + } + + let document = postcss.document({ + nodes: [ + postcss.parse( + `@charset "UTF-8"; @media (screen) { COLOR: black; z-index: 1 }` + ) + ] + }) + + let result = postcss([scanner]).process(document, { from: 'a.css' }) + if (type === 'sync') { + result.css + } else { + await result + } + + expect(filteredDecls).toEqual(['COLOR']) + expect(allDecls).toEqual(['COLOR', 'z-index']) + expect(filteredExits).toEqual(['COLOR']) + expect(allExits).toEqual(['COLOR', 'z-index']) + expect(filteredAtRules).toEqual(['media']) + expect(allAtRules).toEqual(['charset', 'media']) + }) + it(`has ${type} OnceExit listener`, async () => { let rootExit = 0 let OnceExit = 0 @@ -966,6 +1310,82 @@ for (let type of ['sync', 'async']) { expect(rootExit).toBe(2) expect(OnceExit).toBe(1) }) + + it(`has ${type} OnceExit listener in a document with one root`, async () => { + let RootExit = 0 + let OnceExit = 0 + let DocumentExit = 0 + + let plugin: Plugin = { + postcssPlugin: 'test', + Rule(rule) { + rule.remove() + }, + RootExit() { + RootExit += 1 + }, + DocumentExit() { + DocumentExit += 1 + }, + OnceExit() { + OnceExit += 1 + } + } + + let document = postcss.document({ + nodes: [postcss.parse('a{}')] + }) + + let result = postcss([plugin]).process(document, { from: 'a.css' }) + + if (type === 'sync') { + result.css + } else { + await result + } + + expect(RootExit).toBe(2) + expect(DocumentExit).toBe(2) + expect(OnceExit).toBe(1) + }) + + it(`has ${type} OnceExit listener in a document with two roots`, async () => { + let RootExit = 0 + let OnceExit = 0 + let DocumentExit = 0 + + let plugin: Plugin = { + postcssPlugin: 'test', + Rule(rule) { + rule.remove() + }, + RootExit() { + RootExit += 1 + }, + DocumentExit() { + DocumentExit += 1 + }, + OnceExit() { + OnceExit += 1 + } + } + + let document = postcss.document({ + nodes: [postcss.parse('a{}'), postcss.parse('b{}')] + }) + + let result = postcss([plugin]).process(document, { from: 'a.css' }) + + if (type === 'sync') { + result.css + } else { + await result + } + + expect(RootExit).toBe(4) + expect(DocumentExit).toBe(2) + expect(OnceExit).toBe(2) // 2 roots === 2 OnceExit + }) } it('throws error from async OnceExit', async () => { @@ -1009,6 +1429,28 @@ it('rescan Root in another processor', () => { ]) }) +it('rescan Root in another processor in a document', () => { + let [visits, visitor] = buildVisitor() + let root = postcss([visitor]).process('a{z-index:1}', { from: 'a.css' }).root + let document = postcss.document({ nodes: [root] }) + + visits.splice(0, visits.length) + postcss([visitor]).process(document, { from: 'a.css' }).root + + expect(visits).toEqual([ + ['Once', '1'], + ['Document', '1'], + ['Root', '1'], + ['Rule', 'a'], + ['Declaration', 'z-index: 1'], + ['DeclarationExit', 'z-index: 1'], + ['RuleExit', 'a'], + ['RootExit', '1'], + ['DocumentExit', '1'], + ['OnceExit', '1'] + ]) +}) + it('marks cleaned nodes as dirty on moving', () => { let mover: Plugin = { postcssPlugin: 'mover', @@ -1046,3 +1488,49 @@ it('marks cleaned nodes as dirty on moving', () => { ['OnceExit', '1'] ]) }) + +it('marks cleaned nodes as dirty on moving in a document', () => { + let mover: Plugin = { + postcssPlugin: 'mover', + Rule(rule) { + if (rule.selector === 'b') { + let a = rule.prev() + if (a) rule.append(a) + } + } + } + let [visits, visitor] = buildVisitor() + + let document = postcss.document({ + nodes: [postcss.parse('a { color: black } b { }')] + }) + + postcss([mover, visitor]).process(document, { + from: 'a.css' + }).root + + expect(visits).toEqual([ + ['Once', '2'], + ['Document', '1'], + ['Root', '2'], + ['Rule', 'a'], + ['Declaration', 'color: black'], + ['DeclarationExit', 'color: black'], + ['RuleExit', 'a'], + ['Rule', 'b'], + ['Rule', 'a'], + ['Declaration', 'color: black'], + ['DeclarationExit', 'color: black'], + ['RuleExit', 'a'], + ['RuleExit', 'b'], + ['RootExit', '1'], + ['DocumentExit', '1'], + ['Document', '1'], + ['Root', '1'], + ['Rule', 'b'], + ['RuleExit', 'b'], + ['RootExit', '1'], + ['DocumentExit', '1'], + ['OnceExit', '1'] + ]) +}) From aa25ce040ac18e9286e438c0edd513acb2d5870a Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Mon, 17 May 2021 01:58:03 +0200 Subject: [PATCH 02/12] Narrow Document nodes type --- lib/at-rule.d.ts | 2 ++ lib/container.d.ts | 3 ++- lib/document.d.ts | 14 +------------- lib/root.d.ts | 2 ++ lib/rule.d.ts | 2 ++ 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/at-rule.d.ts b/lib/at-rule.d.ts index 74ecfb430..bbbbe630b 100644 --- a/lib/at-rule.d.ts +++ b/lib/at-rule.d.ts @@ -1,4 +1,5 @@ import Container, { ContainerProps } from './container.js' +import { ChildNode } from './node.js' interface AtRuleRaws { /** @@ -70,6 +71,7 @@ export interface AtRuleProps extends ContainerProps { export default class AtRule extends Container { type: 'atrule' raws: AtRuleRaws + nodes: ChildNode[] /** * The at-rule’s name immediately follows the `@`. diff --git a/lib/container.d.ts b/lib/container.d.ts index c69f84a15..5958249a6 100644 --- a/lib/container.d.ts +++ b/lib/container.d.ts @@ -3,6 +3,7 @@ import Declaration from './declaration.js' import Comment from './comment.js' import AtRule from './at-rule.js' import Rule from './rule.js' +import Root from './root.js' interface ValueOptions { /** @@ -38,7 +39,7 @@ export default abstract class Container extends Node { * root.nodes[0].nodes[0].prop //=> 'color' * ``` */ - nodes: ChildNode[] + nodes: (ChildNode | Root)[] /** * The container’s first child. diff --git a/lib/document.d.ts b/lib/document.d.ts index 7b05fc8a8..9cbfb5859 100644 --- a/lib/document.d.ts +++ b/lib/document.d.ts @@ -22,6 +22,7 @@ type ChildProps = RootProps export default class Document extends Container { type: 'document' parent: undefined + nodes: Root[] constructor(defaults?: DocumentProps) @@ -42,19 +43,6 @@ export default class Document extends Container { */ toResult(options?: ProcessOptions): Result - /** - * An array containing the container’s children. - * - * ```js - * const root = postcss.parse('a { color: black }') - * root.nodes.length //=> 1 - * root.nodes[0].selector //=> 'a' - * root.nodes[0].nodes[0].prop //=> 'color' - * ``` - */ - // @ts-expect-error - nodes: Root[] - /** * The container’s first child. * diff --git a/lib/root.d.ts b/lib/root.d.ts index d8ee39531..69f26dc32 100644 --- a/lib/root.d.ts +++ b/lib/root.d.ts @@ -1,4 +1,5 @@ import Container, { ContainerProps } from './container.js' +import { ChildNode } from './node.js' import { ProcessOptions } from './postcss.js' import Result from './result.js' @@ -36,6 +37,7 @@ export default class Root extends Container { type: 'root' // parent: Document | undefined raws: RootRaws + nodes: ChildNode[] constructor(defaults?: RootProps) diff --git a/lib/rule.d.ts b/lib/rule.d.ts index 8243c3666..d34a8a9f8 100644 --- a/lib/rule.d.ts +++ b/lib/rule.d.ts @@ -1,4 +1,5 @@ import Container, { ContainerProps } from './container.js' +import { ChildNode } from './node.js' interface RuleRaws { /** @@ -63,6 +64,7 @@ export interface RuleProps extends ContainerProps { export default class Rule extends Container { type: 'rule' raws: RuleRaws + nodes: ChildNode[] /** * The rule’s full selector represented as a string. From 39d2a84150cf3855ee38d908f6d39d938692b089 Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Mon, 17 May 2021 02:12:28 +0200 Subject: [PATCH 03/12] Specify parent type on a Root node --- lib/at-rule.d.ts | 1 + lib/comment.d.ts | 2 ++ lib/declaration.d.ts | 2 ++ lib/node.d.ts | 3 +-- lib/root.d.ts | 3 ++- lib/rule.d.ts | 1 + 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/at-rule.d.ts b/lib/at-rule.d.ts index bbbbe630b..40880c848 100644 --- a/lib/at-rule.d.ts +++ b/lib/at-rule.d.ts @@ -70,6 +70,7 @@ export interface AtRuleProps extends ContainerProps { */ export default class AtRule extends Container { type: 'atrule' + parent: Container | undefined raws: AtRuleRaws nodes: ChildNode[] diff --git a/lib/comment.d.ts b/lib/comment.d.ts index d1b6a4f91..c4ae906cc 100644 --- a/lib/comment.d.ts +++ b/lib/comment.d.ts @@ -1,3 +1,4 @@ +import Container from './container.js' import Node, { NodeProps } from './node.js' interface CommentRaws { @@ -37,6 +38,7 @@ export interface CommentProps extends NodeProps { */ export default class Comment extends Node { type: 'comment' + parent: Container | undefined raws: CommentRaws /** diff --git a/lib/declaration.d.ts b/lib/declaration.d.ts index 0be0d2822..a3e3d8f33 100644 --- a/lib/declaration.d.ts +++ b/lib/declaration.d.ts @@ -1,3 +1,4 @@ +import Container from './container.js' import Node from './node.js' interface DeclarationRaws { @@ -51,6 +52,7 @@ export interface DeclarationProps { */ export default class Declaration extends Node { type: 'decl' + parent: Container | undefined raws: DeclarationRaws /** diff --git a/lib/node.d.ts b/lib/node.d.ts index e9773913d..aa38d7bff 100644 --- a/lib/node.d.ts +++ b/lib/node.d.ts @@ -5,7 +5,6 @@ import AtRule, { AtRuleProps } from './at-rule.js' import Rule, { RuleProps } from './rule.js' import { WarningOptions } from './warning.js' import CssSyntaxError from './css-syntax-error.js' -import Container from './container.js' import Result from './result.js' import Input from './input.js' import Root from './root.js' @@ -98,7 +97,7 @@ export default abstract class Node { * root.nodes[0].parent === root * ``` */ - parent: Container | undefined + parent: any /** * The input source of the node. diff --git a/lib/root.d.ts b/lib/root.d.ts index 69f26dc32..4df44efde 100644 --- a/lib/root.d.ts +++ b/lib/root.d.ts @@ -1,4 +1,5 @@ import Container, { ContainerProps } from './container.js' +import Document from './document.js' import { ChildNode } from './node.js' import { ProcessOptions } from './postcss.js' import Result from './result.js' @@ -35,7 +36,7 @@ export interface RootProps extends ContainerProps { */ export default class Root extends Container { type: 'root' - // parent: Document | undefined + parent: Document | undefined raws: RootRaws nodes: ChildNode[] diff --git a/lib/rule.d.ts b/lib/rule.d.ts index d34a8a9f8..897829337 100644 --- a/lib/rule.d.ts +++ b/lib/rule.d.ts @@ -63,6 +63,7 @@ export interface RuleProps extends ContainerProps { */ export default class Rule extends Container { type: 'rule' + parent: Container | undefined raws: RuleRaws nodes: ChildNode[] From fca648b5196ff06c7a85cb6f76a52c4b00111cb8 Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Mon, 17 May 2021 10:56:37 +0200 Subject: [PATCH 04/12] Fix types --- lib/container.d.ts | 43 ++++------ lib/document.d.ts | 210 +-------------------------------------------- lib/node.d.ts | 3 +- 3 files changed, 19 insertions(+), 237 deletions(-) diff --git a/lib/container.d.ts b/lib/container.d.ts index 5958249a6..3005a1614 100644 --- a/lib/container.d.ts +++ b/lib/container.d.ts @@ -3,7 +3,6 @@ import Declaration from './declaration.js' import Comment from './comment.js' import AtRule from './at-rule.js' import Rule from './rule.js' -import Root from './root.js' interface ValueOptions { /** @@ -28,7 +27,9 @@ export interface ContainerProps extends NodeProps { * Note that all containers can store any content. If you write a rule inside * a rule, PostCSS will parse it. */ -export default abstract class Container extends Node { +export default abstract class Container< + Child extends Node = ChildNode +> extends Node { /** * An array containing the container’s children. * @@ -39,7 +40,7 @@ export default abstract class Container extends Node { * root.nodes[0].nodes[0].prop //=> 'color' * ``` */ - nodes: (ChildNode | Root)[] + nodes: Child[] /** * The container’s first child. @@ -48,7 +49,7 @@ export default abstract class Container extends Node { * rule.first === rules.nodes[0] * ``` */ - get first(): ChildNode | undefined + get first(): Child | undefined /** * The container’s last child. @@ -57,7 +58,7 @@ export default abstract class Container extends Node { * rule.last === rule.nodes[rule.nodes.length - 1] * ``` */ - get last(): ChildNode | undefined + get last(): Child | undefined /** * Iterates through the container’s immediate children, @@ -93,7 +94,7 @@ export default abstract class Container extends Node { * @return Returns `false` if iteration was broke. */ each( - callback: (node: ChildNode, index: number) => false | void + callback: (node: Child, index: number) => false | void ): false | undefined /** @@ -305,7 +306,7 @@ export default abstract class Container extends Node { * @param child New node. * @return This node for methods chain. */ - push(child: ChildNode): this + push(child: Child): this /** * Insert new node before old node within the container. @@ -319,14 +320,8 @@ export default abstract class Container extends Node { * @return This node for methods chain. */ insertBefore( - oldNode: ChildNode | number, - newNode: - | ChildNode - | ChildProps - | string - | ChildNode[] - | ChildProps[] - | string[] + oldNode: Child | number, + newNode: Child | ChildProps | string | Child[] | ChildProps[] | string[] ): this /** @@ -337,14 +332,8 @@ export default abstract class Container extends Node { * @return This node for methods chain. */ insertAfter( - oldNode: ChildNode | number, - newNode: - | ChildNode - | ChildProps - | string - | ChildNode[] - | ChildProps[] - | string[] + oldNode: Child | number, + newNode: Child | ChildProps | string | Child[] | ChildProps[] | string[] ): this /** @@ -361,7 +350,7 @@ export default abstract class Container extends Node { * @param child Child or child’s index. * @return This node for methods chain. */ - removeChild(child: ChildNode | number): this + removeChild(child: Child | number): this /** * Removes all children from the container @@ -421,7 +410,7 @@ export default abstract class Container extends Node { * @return Is every child pass condition. */ every( - condition: (node: ChildNode, index: number, nodes: ChildNode[]) => boolean + condition: (node: Child, index: number, nodes: Child[]) => boolean ): boolean /** @@ -436,7 +425,7 @@ export default abstract class Container extends Node { * @return Is some child pass condition. */ some( - condition: (node: ChildNode, index: number, nodes: ChildNode[]) => boolean + condition: (node: Child, index: number, nodes: Child[]) => boolean ): boolean /** @@ -449,5 +438,5 @@ export default abstract class Container extends Node { * @param child Child of the current container. * @return Child index. */ - index(child: ChildNode | number): number + index(child: Child | number): number } diff --git a/lib/document.d.ts b/lib/document.d.ts index 9cbfb5859..7274b7eba 100644 --- a/lib/document.d.ts +++ b/lib/document.d.ts @@ -19,10 +19,9 @@ type ChildProps = RootProps * document.nodes.length //=> 2 * ``` */ -export default class Document extends Container { +export default class Document extends Container { type: 'document' parent: undefined - nodes: Root[] constructor(defaults?: DocumentProps) @@ -42,211 +41,4 @@ export default class Document extends Container { * @return Result with current document’s CSS. */ toResult(options?: ProcessOptions): Result - - /** - * The container’s first child. - * - * ```js - * rule.first === rules.nodes[0] - * ``` - */ - // @ts-expect-error - get first(): ChildNode | undefined - - /** - * The container’s last child. - * - * ```js - * rule.last === rule.nodes[rule.nodes.length - 1] - * ``` - */ - // @ts-expect-error - get last(): ChildNode | undefined - - /** - * Iterates through the container’s immediate children, - * calling `callback` for each child. - * - * Returning `false` in the callback will break iteration. - * - * This method only iterates through the container’s immediate children. - * If you need to recursively iterate through all the container’s descendant - * nodes, use `Container#walk`. - * - * 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 - * const root = postcss.parse('a { color: black; z-index: 1 }') - * const rule = root.first - * - * for (const decl of rule.nodes) { - * decl.cloneBefore({ prop: '-webkit-' + decl.prop }) - * // Cycle will be infinite, because cloneBefore moves the current node - * // to the next index - * } - * - * rule.each(decl => { - * decl.cloneBefore({ prop: '-webkit-' + decl.prop }) - * // Will be executed only for color and z-index - * }) - * ``` - * - * @param callback Iterator receives each node and index. - * @return Returns `false` if iteration was broke. - */ - // @ts-expect-error - each( - callback: (node: ChildNode, index: number) => false | void - ): false | undefined - - /** - * Inserts new nodes to the end of the container. - * - * ```js - * const decl1 = new Declaration({ prop: 'color', value: 'black' }) - * const decl2 = new Declaration({ prop: 'background-color', value: 'white' }) - * rule.append(decl1, decl2) - * - * 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 - * - * root.append('a {}') - * root.first.append('color: black; z-index: 1') - * ``` - * - * @param nodes New nodes. - * @return This node for methods chain. - */ - append(...nodes: (ChildProps | ChildProps[])[]): this - - /** - * Inserts new nodes to the start of the container. - * - * ```js - * const decl1 = new Declaration({ prop: 'color', value: 'black' }) - * const decl2 = new Declaration({ prop: 'background-color', value: 'white' }) - * rule.prepend(decl1, decl2) - * - * 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 - * - * root.append('a {}') - * root.first.append('color: black; z-index: 1') - * ``` - * - * @param nodes New nodes. - * @return This node for methods chain. - */ - prepend(...nodes: (ChildProps | ChildProps[])[]): this - - /** - * Add child to the end of the node. - * - * ```js - * rule.push(new Declaration({ prop: 'color', value: 'black' })) - * ``` - * - * @param child New node. - * @return This node for methods chain. - */ - // @ts-expect-error - push(child: ChildNode): this - - /** - * Insert new node before old node within the container. - * - * ```js - * rule.insertBefore(decl, decl.clone({ prop: '-webkit-' + decl.prop })) - * ``` - * - * @param oldNode Child or child’s index. - * @param newNode New node. - * @return This node for methods chain. - */ - // @ts-expect-error - insertBefore( - oldNode: ChildNode | number, - newNode: ChildNode | ChildProps | ChildNode[] | ChildProps[] - ): this - - /** - * Insert new node after old node within the container. - * - * @param oldNode Child or child’s index. - * @param newNode New node. - * @return This node for methods chain. - */ - // @ts-expect-error - insertAfter( - oldNode: ChildNode | number, - newNode: ChildNode | ChildProps | ChildNode[] | ChildProps[] - ): this - - /** - * Removes node from the container and cleans the parent properties - * from the node and its children. - * - * ```js - * rule.nodes.length //=> 5 - * rule.removeChild(decl) - * rule.nodes.length //=> 4 - * decl.parent //=> undefined - * ``` - * - * @param child Child or child’s index. - * @return This node for methods chain. - */ - // @ts-expect-error - removeChild(child: ChildNode | number): this - - /** - * Returns `true` if callback returns `true` - * for all of the container’s children. - * - * ```js - * const noPrefixes = rule.every(i => i.prop[0] !== '-') - * ``` - * - * @param condition Iterator returns true or false. - * @return Is every child pass condition. - */ - // @ts-expect-error - every( - condition: (node: ChildNode, index: number, nodes: ChildNode[]) => boolean - ): boolean - - /** - * Returns `true` if callback returns `true` for (at least) one - * of the container’s children. - * - * ```js - * const hasPrefix = rule.some(i => i.prop[0] === '-') - * ``` - * - * @param condition Iterator returns true or false. - * @return Is some child pass condition. - */ - // @ts-expect-error - some( - condition: (node: ChildNode, index: number, nodes: ChildNode[]) => boolean - ): boolean - - /** - * Returns a `child`’s index within the `Container#nodes` array. - * - * ```js - * rule.index( rule.nodes[2] ) //=> 2 - * ``` - * - * @param child Child of the current container. - * @return Child index. - */ - // @ts-expect-error - index(child: ChildNode | number): number } diff --git a/lib/node.d.ts b/lib/node.d.ts index aa38d7bff..8718c94d8 100644 --- a/lib/node.d.ts +++ b/lib/node.d.ts @@ -9,6 +9,7 @@ import Result from './result.js' import Input from './input.js' import Root from './root.js' import Document from './document.js' +import Container from './container.js' export type ChildNode = AtRule | Rule | Declaration | Comment @@ -97,7 +98,7 @@ export default abstract class Node { * root.nodes[0].parent === root * ``` */ - parent: any + parent: Document | Container | undefined /** * The input source of the node. From da90932a9c9f930ec9c68cd9023e25b4f8c54b71 Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Mon, 17 May 2021 10:58:54 +0200 Subject: [PATCH 05/12] Add experimental warning --- lib/document.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/document.d.ts b/lib/document.d.ts index 7274b7eba..a81acdfe3 100644 --- a/lib/document.d.ts +++ b/lib/document.d.ts @@ -13,6 +13,8 @@ type ChildProps = RootProps /** * Represents a file and contains all its parsed nodes. * + * **Experimental!** Some aspects of this node could change within minor or patch version releases. + * * ```js * const document = postcss.parse('') * document.type //=> 'document' From a5ac723f1f63c59db0a6e4aa7747c400419d755d Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Mon, 17 May 2021 18:23:00 +0200 Subject: [PATCH 06/12] Fix types --- lib/document.d.ts | 4 ++-- lib/postcss.d.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/document.d.ts b/lib/document.d.ts index a81acdfe3..246fcd1c4 100644 --- a/lib/document.d.ts +++ b/lib/document.d.ts @@ -13,10 +13,10 @@ type ChildProps = RootProps /** * Represents a file and contains all its parsed nodes. * - * **Experimental!** Some aspects of this node could change within minor or patch version releases. + * **Experimental:** some aspects of this node could change within minor or patch version releases. * * ```js - * const document = postcss.parse('') + * const document = htmlParser('') * document.type //=> 'document' * document.nodes.length //=> 2 * ``` diff --git a/lib/postcss.d.ts b/lib/postcss.d.ts index 980707ecb..08a0574d8 100644 --- a/lib/postcss.d.ts +++ b/lib/postcss.d.ts @@ -41,6 +41,7 @@ export { ChildProps, AtRuleProps, RootProps, + DocumentProps, Warning, CssSyntaxError, Node, @@ -63,6 +64,10 @@ export type SourceMap = SourceMapGenerator & { export type Helpers = { result: Result; postcss: Postcss } & Postcss +type DocumentProcessor = ( + document: Document, + helper: Helpers +) => Promise | void type RootProcessor = (root: Root, helper: Helpers) => Promise | void type DeclarationProcessor = ( decl: Declaration, @@ -81,14 +86,14 @@ interface Processors { * * Will be called again on children changes. */ - Document?: RootProcessor + Document?: DocumentProcessor /** * Will be called on `Document` node, when all children will be processed. * * Will be called again on children changes. */ - DocumentExit?: RootProcessor + DocumentExit?: DocumentProcessor /** * Will be called on `Root` node once. From 69c5d81279f86ec53046079207341f6832947970 Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Tue, 18 May 2021 01:22:21 +0200 Subject: [PATCH 07/12] Remove unneeded parent type --- lib/at-rule.d.ts | 2 -- lib/root.d.ts | 2 -- lib/rule.d.ts | 2 -- 3 files changed, 6 deletions(-) diff --git a/lib/at-rule.d.ts b/lib/at-rule.d.ts index 40880c848..7a12b068f 100644 --- a/lib/at-rule.d.ts +++ b/lib/at-rule.d.ts @@ -1,5 +1,4 @@ import Container, { ContainerProps } from './container.js' -import { ChildNode } from './node.js' interface AtRuleRaws { /** @@ -72,7 +71,6 @@ export default class AtRule extends Container { type: 'atrule' parent: Container | undefined raws: AtRuleRaws - nodes: ChildNode[] /** * The at-rule’s name immediately follows the `@`. diff --git a/lib/root.d.ts b/lib/root.d.ts index 4df44efde..9fc4cf83d 100644 --- a/lib/root.d.ts +++ b/lib/root.d.ts @@ -1,6 +1,5 @@ import Container, { ContainerProps } from './container.js' import Document from './document.js' -import { ChildNode } from './node.js' import { ProcessOptions } from './postcss.js' import Result from './result.js' @@ -38,7 +37,6 @@ export default class Root extends Container { type: 'root' parent: Document | undefined raws: RootRaws - nodes: ChildNode[] constructor(defaults?: RootProps) diff --git a/lib/rule.d.ts b/lib/rule.d.ts index 897829337..ea549acc3 100644 --- a/lib/rule.d.ts +++ b/lib/rule.d.ts @@ -1,5 +1,4 @@ import Container, { ContainerProps } from './container.js' -import { ChildNode } from './node.js' interface RuleRaws { /** @@ -65,7 +64,6 @@ export default class Rule extends Container { type: 'rule' parent: Container | undefined raws: RuleRaws - nodes: ChildNode[] /** * The rule’s full selector represented as a string. From 50e07d1c4bcb306102381b59c16e5be6872be35f Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Tue, 18 May 2021 02:01:20 +0200 Subject: [PATCH 08/12] Remove `Node#document()` --- lib/node.d.ts | 11 ----------- lib/node.js | 9 --------- test/node.test.ts | 20 -------------------- 3 files changed, 40 deletions(-) diff --git a/lib/node.d.ts b/lib/node.d.ts index 8718c94d8..f4150a87e 100644 --- a/lib/node.d.ts +++ b/lib/node.d.ts @@ -382,17 +382,6 @@ export default abstract class Node { */ root(): Root - /** - * Finds the Document instance of the node’s tree. - * - * ```js - * document.nodes[0].nodes[0].document() === document - * ``` - * - * @return Document parent. - */ - document(): Document - /** * Returns a `Node#raws` value. If the node is missing * the code style property (because the node was manually built or cloned), diff --git a/lib/node.js b/lib/node.js index 443281932..2cfe1557e 100644 --- a/lib/node.js +++ b/lib/node.js @@ -157,15 +157,6 @@ class Node { return result } - document() { - let result = this - while (result.parent) { - result = result.parent - } - - return result - } - raw(prop, defaultType) { let str = new Stringifier() return str.raw(this, prop, defaultType) diff --git a/test/node.test.ts b/test/node.test.ts index 780c175a1..d8271cb59 100644 --- a/test/node.test.ts +++ b/test/node.test.ts @@ -345,26 +345,6 @@ it('root() returns self on document', () => { expect(document.root()).toBe(document) }) -it('document() returns document', () => { - let css = new Document({ nodes: [parse('@page{a{color:black}}')] }) - - let root = css.first as Root - let page = root.first as AtRule - let a = page.first as Rule - let color = a.first as Declaration - expect(color.document()).toBe(css) -}) - -it('document() returns self on root', () => { - let rule = new Rule({ selector: 'a' }) - expect(rule.document()).toBe(rule) -}) - -it('document() returns self on document', () => { - let document = new Document() - expect(document.document()).toBe(document) -}) - it('cleanRaws() cleans style recursivelly', () => { let css = parse('@page{a{color:black}}') css.cleanRaws() From 49eec3a27d9bb0ac014b2b5065028d40e8d1b805 Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Tue, 18 May 2021 21:40:56 +0200 Subject: [PATCH 09/12] Remove Root.raws.before --- lib/root.d.ts | 5 ----- test/stringifier.test.js | 21 ++++----------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/lib/root.d.ts b/lib/root.d.ts index 9fc4cf83d..d7b98fd04 100644 --- a/lib/root.d.ts +++ b/lib/root.d.ts @@ -4,11 +4,6 @@ import { ProcessOptions } from './postcss.js' import Result from './result.js' interface RootRaws { - /** - * The symbols before the first child to the start of file. - */ - before?: string - /** * The space symbols after the last child to the end of file. */ diff --git a/test/stringifier.test.js b/test/stringifier.test.js index c199ded1b..e929e9c6a 100644 --- a/test/stringifier.test.js +++ b/test/stringifier.test.js @@ -221,26 +221,15 @@ it('handles document with one root and after raw', () => { expect(s).toEqual('@foo ') }) -it('handles document with one root and before raw', () => { - let document = new Document() - let root = new Root({ raws: { before: ' ' } }) - root.append(new AtRule({ name: 'foo' })) - document.append(root) - - let s = document.toString() - - expect(s).toEqual(' @foo') -}) - it('handles document with one root and before and after', () => { let document = new Document() - let root = new Root({ raws: { before: 'BEFORE', after: 'AFTER' } }) + let root = new Root({ raws: { after: 'AFTER' } }) root.append(new AtRule({ name: 'foo' })) document.append(root) let s = document.toString() - expect(s).toEqual('BEFORE@fooAFTER') + expect(s).toEqual('@fooAFTER') }) it('handles document with three roots without raws', () => { @@ -264,7 +253,7 @@ it('handles document with three roots without raws', () => { }) it('handles document with three roots, with before and after raws', () => { - let root1 = new Root({ raws: { before: 'BEFORE_ONE', after: 'AFTER_ONE' } }) + let root1 = new Root({ raws: { after: 'AFTER_ONE' } }) root1.append(new Rule({ selector: 'a.one' })) let root2 = new Root({ raws: { after: 'AFTER_TWO' } }) @@ -280,7 +269,5 @@ it('handles document with three roots, with before and after raws', () => { let s = document.toString() - expect(s).toEqual( - 'BEFORE_ONEa.one {}AFTER_ONEa.two {}AFTER_TWOa.three {}AFTER_THREE' - ) + expect(s).toEqual('a.one {}AFTER_ONEa.two {}AFTER_TWOa.three {}AFTER_THREE') }) From 54e2e52d08b4a69e0a6c3f8767233803ce1160bb Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Tue, 18 May 2021 23:41:23 +0200 Subject: [PATCH 10/12] Test custom parser --- lib/document.d.ts | 8 ++++++ lib/node.d.ts | 2 +- lib/root.d.ts | 2 +- test/processor.test.ts | 64 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/lib/document.d.ts b/lib/document.d.ts index 246fcd1c4..d2adc0482 100644 --- a/lib/document.d.ts +++ b/lib/document.d.ts @@ -5,6 +5,14 @@ import Root, { RootProps } from './root.js' export interface DocumentProps extends ContainerProps { nodes?: Root[] + + /** + * Information to generate byte-to-byte equal node string as it was + * in the origin input. + * + * Every parser saves its own properties. + */ + raws?: Record } type ChildNode = Root diff --git a/lib/node.d.ts b/lib/node.d.ts index f4150a87e..fc4e38299 100644 --- a/lib/node.d.ts +++ b/lib/node.d.ts @@ -13,7 +13,7 @@ import Container from './container.js' export type ChildNode = AtRule | Rule | Declaration | Comment -export type AnyNode = AtRule | Rule | Declaration | Comment | Root +export type AnyNode = AtRule | Rule | Declaration | Comment | Root | Document export type ChildProps = | AtRuleProps diff --git a/lib/root.d.ts b/lib/root.d.ts index d7b98fd04..49e4d6113 100644 --- a/lib/root.d.ts +++ b/lib/root.d.ts @@ -3,7 +3,7 @@ import Document from './document.js' import { ProcessOptions } from './postcss.js' import Result from './result.js' -interface RootRaws { +interface RootRaws extends Record { /** * The space symbols after the last child to the end of file. */ diff --git a/test/processor.test.ts b/test/processor.test.ts index 9a73f8a0c..eedb09539 100644 --- a/test/processor.test.ts +++ b/test/processor.test.ts @@ -7,10 +7,14 @@ import postcss, { Node, Root, parse, - PluginCreator + PluginCreator, + Document, + Parser, + Stringifier } from '../lib/postcss.js' import LazyResult from '../lib/lazy-result.js' import Processor from '../lib/processor.js' +import Rule from '../lib/rule.js' afterEach(() => { jest.resetAllMocks() @@ -547,3 +551,61 @@ it('supports plugin creators returning processors', () => { processor.use(other) expect(processor.plugins).toEqual([a]) }) + +it('uses custom syntax for document', async () => { + // @ts-expect-error + let customParser: Parser = () => { + return new Document({ + nodes: [ + new Root({ + raws: { + markupBefore: '\n\n\n\n' + }, + nodes: [new Rule({ selector: 'b' })] + }) + ] + }) + } + + let customStringifier: Stringifier = (doc, builder) => { + if (doc.type === 'document') { + for (let root of doc.nodes) { + if (root.raws.markupBefore) { + builder(root.raws.markupBefore, root) + } + + builder(root.toString(), root) + + if (root.raws.markupAfter) { + builder(root.raws.markupAfter, root) + } + } + } + } + + let processor = new Processor([() => {}]) + let result = await processor.process('a{}', { + syntax: { + parse: customParser, + stringify: customStringifier + }, + from: undefined + }) + + expect(result.css).toEqual( + '\n\n\n\n' + ) +}) From 3eb068b01f0486e48bcffe0dd1c0f579b2ecfaed Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Tue, 18 May 2021 23:48:26 +0200 Subject: [PATCH 11/12] Fix Parser type --- lib/postcss.d.ts | 6 +++--- test/processor.test.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/postcss.d.ts b/lib/postcss.d.ts index 08a0574d8..5c3ff62e3 100644 --- a/lib/postcss.d.ts +++ b/lib/postcss.d.ts @@ -221,11 +221,11 @@ export type AcceptedPlugin = } | Processor -export interface Parser { +export interface Parser { ( css: string | { toString(): string }, opts?: Pick - ): Root + ): RootNode } export interface Builder { @@ -245,7 +245,7 @@ export interface Syntax { /** * Function to generate AST by string. */ - parse?: Parser + parse?: Parser /** * Class to generate string by AST. diff --git a/test/processor.test.ts b/test/processor.test.ts index eedb09539..d4f83cc57 100644 --- a/test/processor.test.ts +++ b/test/processor.test.ts @@ -553,8 +553,7 @@ it('supports plugin creators returning processors', () => { }) it('uses custom syntax for document', async () => { - // @ts-expect-error - let customParser: Parser = () => { + let customParser: Parser = () => { return new Document({ nodes: [ new Root({ From 8af26ab8251c0e380aecba186afa8987a7169b73 Mon Sep 17 00:00:00 2001 From: Aleks Hudochenkov Date: Tue, 18 May 2021 23:56:29 +0200 Subject: [PATCH 12/12] Update documentation --- docs/syntax.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index 88716022b..01b81ef7c 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -39,7 +39,7 @@ A good example of a parser is [Safe Parser], which parses malformed/broken CSS. Because there is no point to generate broken output, this package only provides a parser. -The parser API is a function which receives a string & returns a [`Root`] node. +The parser API is a function which receives a string & returns a [`Root`] or [`Document`] node. The second argument is a function which receives an object with PostCSS options. ```js @@ -54,6 +54,7 @@ module.exports = function parse (css, opts) { [Safe Parser]: https://github.com/postcss/postcss-safe-parser [`Root`]: https://postcss.org/api/#root +[`Document`]: https://postcss.org/api/#document ### Main Theory @@ -170,7 +171,7 @@ The Stringifier API is little bit more complicated, than the parser API. PostCSS generates a source map, so a stringifier can’t just return a string. It must link every substring with its source node. -A Stringifier is a function which receives [`Root`] node and builder callback. +A Stringifier is a function which receives [`Root`] or [`Document`] node and builder callback. Then it calls builder with every node’s string and node instance. ```js