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']
+ ])
+})