├── .gitattributes ├── d.ts ├── tokenize.d.ts ├── warn-once.d.ts ├── stringify.d.ts ├── vendor.d.ts ├── previous-map.d.ts ├── list.d.ts ├── parse.d.ts ├── map-generator.d.ts ├── comment.d.ts ├── stringifier.d.ts ├── warning.d.ts ├── parser.d.ts ├── processor.d.ts ├── rule.d.ts ├── at-rule.d.ts ├── declaration.d.ts ├── input.d.ts ├── root.d.ts ├── result.d.ts ├── lazy-result.d.ts ├── css-syntax-error.d.ts └── node.d.ts ├── .travis.yml ├── .babelrc ├── .gitignore ├── .npmignore ├── lib ├── stringify.es6 ├── warn-once.es6 ├── parse.es6 ├── vendor.es6 ├── comment.es6 ├── list.es6 ├── root.es6 ├── declaration.es6 ├── rule.es6 ├── warning.es6 ├── at-rule.es6 ├── previous-map.es6 ├── input.es6 ├── result.es6 ├── postcss.es6 ├── css-syntax-error.es6 ├── processor.es6 ├── map-generator.es6 ├── tokenize.es6 └── stringifier.es6 ├── .editorconfig ├── .eslintrc ├── appveyor.yml ├── test ├── vendor.js ├── stringify.js ├── comment.js ├── declaration.js ├── list.js ├── at-rule.js ├── lazy-result.js ├── result.js ├── root.js ├── rule.js ├── warning.js ├── parse.js ├── postcss.js ├── css-syntax-error.js ├── stringifier.js ├── previous-map.js └── tokenize.js ├── docs ├── api.md ├── writing-a-plugin.md ├── source-maps.md ├── guidelines │ ├── runner.md │ └── plugin.md └── syntax.md ├── LICENSE ├── package.json ├── .yaspellerrc └── gulpfile.babel.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * eol=lf 2 | -------------------------------------------------------------------------------- /d.ts/tokenize.d.ts: -------------------------------------------------------------------------------- 1 | export default function tokenize(input: any): any[]; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "4" 5 | - "0.12" 6 | -------------------------------------------------------------------------------- /d.ts/warn-once.d.ts: -------------------------------------------------------------------------------- 1 | declare var _default: (message: string) => void; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-loose"], 3 | "plugins": ["add-module-exports", "precompile-charcodes"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | 4 | node_modules/ 5 | npm-debug.log 6 | 7 | build/ 8 | lib/*.js 9 | /postcss.js 10 | 11 | api/ 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | 3 | node_modules/ 4 | npm-debug.log 5 | 6 | build/ 7 | /postcss.js 8 | 9 | test/ 10 | .travis.yml 11 | appveyor.yml 12 | 13 | gulpfile.babel.js 14 | 15 | api/ 16 | -------------------------------------------------------------------------------- /lib/stringify.es6: -------------------------------------------------------------------------------- 1 | import Stringifier from './stringifier'; 2 | 3 | export default function stringify(node, builder) { 4 | let str = new Stringifier(builder); 5 | str.stringify(node); 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /lib/warn-once.es6: -------------------------------------------------------------------------------- 1 | let printed = { }; 2 | 3 | export default function warnOnce(message) { 4 | if ( printed[message] ) return; 5 | printed[message] = true; 6 | 7 | if ( typeof console !== 'undefined' && console.warn ) console.warn(message); 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint-config-postcss", 4 | "rules": { 5 | "consistent-return": [0], 6 | "valid-jsdoc": [2], 7 | "complexity": [0] 8 | }, 9 | "env": { 10 | "mocha": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "5" 4 | - nodejs_version: "4" 5 | - nodejs_version: "0.12" 6 | 7 | version: "{build}" 8 | build: off 9 | deploy: off 10 | 11 | install: 12 | - ps: Install-Product node $env:nodejs_version 13 | - npm install 14 | 15 | test_script: 16 | - node --version 17 | - npm --version 18 | - npm test 19 | -------------------------------------------------------------------------------- /d.ts/stringify.d.ts: -------------------------------------------------------------------------------- 1 | import Stringifier from './stringifier'; 2 | import * as postcss from './postcss'; 3 | import Node from './node'; 4 | /** 5 | * Default function to convert a node tree into a CSS string. 6 | */ 7 | declare function stringify(node: Node, builder: Stringifier.Builder): void; 8 | declare module stringify { 9 | var stringify: postcss.Syntax | postcss.Stringify; 10 | } 11 | export default stringify; 12 | -------------------------------------------------------------------------------- /test/vendor.js: -------------------------------------------------------------------------------- 1 | import vendor from '../lib/vendor'; 2 | 3 | import test from 'ava'; 4 | 5 | test('returns prefix', t => { 6 | t.deepEqual(vendor.prefix('-moz-color'), '-moz-'); 7 | t.deepEqual(vendor.prefix('color' ), ''); 8 | }); 9 | 10 | test('returns unprefixed version', t => { 11 | t.deepEqual(vendor.unprefixed('-moz-color'), 'color'); 12 | t.deepEqual(vendor.unprefixed('color' ), 'color'); 13 | }); 14 | -------------------------------------------------------------------------------- /d.ts/vendor.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains helpers for working with vendor prefixes. 3 | */ 4 | declare module Vendor { 5 | /** 6 | * @returns The vendor prefix extracted from the input string. 7 | */ 8 | function prefix(prop: string): string; 9 | /** 10 | * @returns The input string stripped of its vendor prefix. 11 | */ 12 | function unprefixed(prop: string): string; 13 | } 14 | export default Vendor; 15 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API docs has been moved to [api.postcss.org](http://api.postcss.org/) 2 | 3 | We have found a new home for the PostCSS API documentation that you can find at [api.postcss.org](http://api.postcss.org/). We hope you find it more easy to use and more accessibility friendly. If you find any bugs or have any other thoughts regarding the API documnetation, feel free to submit an [issue](https://github.com/postcss/postcss/issues). 4 | 5 | Thank you, 6 | -------------------------------------------------------------------------------- /lib/parse.es6: -------------------------------------------------------------------------------- 1 | import Parser from './parser'; 2 | import Input from './input'; 3 | 4 | export default function parse(css, opts) { 5 | if ( opts && opts.safe ) { 6 | throw new Error('Option safe was removed. ' + 7 | 'Use parser: require("postcss-safe-parser")'); 8 | } 9 | 10 | let input = new Input(css, opts); 11 | 12 | let parser = new Parser(input); 13 | parser.tokenize(); 14 | parser.loop(); 15 | 16 | return parser.root; 17 | } 18 | -------------------------------------------------------------------------------- /test/stringify.js: -------------------------------------------------------------------------------- 1 | import stringify from '../lib/stringify'; 2 | import parse from '../lib/parse'; 3 | 4 | import cases from 'postcss-parser-tests'; 5 | import test from 'ava'; 6 | 7 | cases.each( (name, css) => { 8 | if ( name === 'bom.css' ) return; 9 | 10 | test('stringifies ' + name, t => { 11 | let root = parse(css); 12 | let result = ''; 13 | stringify(root, i => { 14 | result += i; 15 | }); 16 | t.deepEqual(result, css); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /d.ts/previous-map.d.ts: -------------------------------------------------------------------------------- 1 | export default class PreviousMap { 2 | private inline; 3 | annotation: string; 4 | root: string; 5 | private consumerCache; 6 | text: string; 7 | file: string; 8 | constructor(css: any, opts: any); 9 | consumer(): any; 10 | withContent(): boolean; 11 | startWith(string: any, start: any): boolean; 12 | loadAnnotation(css: any): void; 13 | decodeInline(text: any): any; 14 | loadMap(file: any, prev: any): any; 15 | isMap(map: any): boolean; 16 | } 17 | -------------------------------------------------------------------------------- /test/comment.js: -------------------------------------------------------------------------------- 1 | import Comment from '../lib/comment'; 2 | import parse from '../lib/parse'; 3 | 4 | import test from 'ava'; 5 | 6 | test('toString() inserts default spaces', t => { 7 | let comment = new Comment({ text: 'hi' }); 8 | t.deepEqual(comment.toString(), '/* hi */'); 9 | }); 10 | 11 | test('toString() clones spaces from another comment', t => { 12 | let root = parse('a{} /*hello*/'); 13 | let comment = new Comment({ text: 'world' }); 14 | root.append(comment); 15 | 16 | t.deepEqual(root.toString(), 'a{} /*hello*/ /*world*/'); 17 | }); 18 | -------------------------------------------------------------------------------- /d.ts/list.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains helpers for safely splitting lists of CSS values, preserving 3 | * parentheses and quotes. 4 | */ 5 | declare module List { 6 | /** 7 | * Safely splits space-separated values (such as those for background, 8 | * border-radius and other shorthand properties). 9 | */ 10 | function space(str: string): string[]; 11 | /** 12 | * Safely splits comma-separated values (such as those for transition-* and 13 | * background properties). 14 | */ 15 | function comma(str: string): string[]; 16 | } 17 | export default List; 18 | -------------------------------------------------------------------------------- /d.ts/parse.d.ts: -------------------------------------------------------------------------------- 1 | import LazyResult from './lazy-result'; 2 | import * as postcss from './postcss'; 3 | import Result from './result'; 4 | import Root from './root'; 5 | /** 6 | * Parses source CSS. 7 | * @param css The CSS to parse. 8 | * @param options 9 | * @returns {} A new Root node, which contains the source CSS nodes. 10 | */ 11 | declare function parse(css: string | { 12 | toString(): string; 13 | } | LazyResult | Result, options?: { 14 | from?: string; 15 | map?: postcss.SourceMapOptions; 16 | }): Root; 17 | declare module parse { 18 | var parse: postcss.Syntax | postcss.Parse; 19 | } 20 | export default parse; 21 | -------------------------------------------------------------------------------- /d.ts/map-generator.d.ts: -------------------------------------------------------------------------------- 1 | import Root from './root'; 2 | export default class MapGenerator { 3 | private stringify; 4 | private root; 5 | private opts; 6 | private mapOpts; 7 | private previousMaps; 8 | private map; 9 | private css; 10 | constructor(stringify: any, root: Root, opts: any); 11 | isMap(): boolean; 12 | previous(): any; 13 | isInline(): any; 14 | isSourcesContent(): any; 15 | clearAnnotation(): void; 16 | setSourcesContent(): void; 17 | applyPrevMaps(): void; 18 | isAnnotation(): any; 19 | addAnnotation(): void; 20 | outputFile(): any; 21 | generateMap(): any[]; 22 | relative(file: any): any; 23 | sourcePath(node: any): any; 24 | generateString(): void; 25 | generate(): any[]; 26 | } 27 | -------------------------------------------------------------------------------- /d.ts/comment.d.ts: -------------------------------------------------------------------------------- 1 | import * as postcss from './postcss'; 2 | import Node from './node'; 3 | export default class Comment extends Node implements postcss.Comment { 4 | /** 5 | * Returns a string representing the node's type. Possible values are 6 | * root, atrule, rule, decl or comment. 7 | */ 8 | type: string; 9 | /** 10 | * The comment's text. 11 | */ 12 | text: string; 13 | /** 14 | * Represents a comment between declarations or statements (rule and at-rules). 15 | * Comments inside selectors, at-rule parameters, or declaration values will 16 | * be stored in the Node#raws properties. 17 | */ 18 | constructor(defaults?: postcss.CommentNewProps); 19 | /** 20 | * @param overrides New properties to override in the clone. 21 | * @returns A clone of this node. The node and its (cloned) children will 22 | * have a clean parent and code style properties. 23 | */ 24 | clone(overrides?: Object): any; 25 | toJSON(): postcss.JsonComment; 26 | left: string; 27 | right: string; 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2013 Andrey Sitnik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/declaration.js: -------------------------------------------------------------------------------- 1 | import Declaration from '../lib/declaration'; 2 | import parse from '../lib/parse'; 3 | import Rule from '../lib/rule'; 4 | 5 | import test from 'ava'; 6 | 7 | test('initializes with properties', t => { 8 | let decl = new Declaration({ prop: 'color', value: 'black' }); 9 | t.deepEqual(decl.prop, 'color'); 10 | t.deepEqual(decl.value, 'black'); 11 | }); 12 | 13 | test('returns boolean important', t => { 14 | let decl = new Declaration({ prop: 'color', value: 'black' }); 15 | decl.important = true; 16 | t.deepEqual(decl.toString(), 'color: black !important'); 17 | }); 18 | 19 | test('inserts default spaces', t => { 20 | let decl = new Declaration({ prop: 'color', value: 'black' }); 21 | let rule = new Rule({ selector: 'a' }); 22 | rule.append(decl); 23 | t.deepEqual(rule.toString(), 'a {\n color: black\n}'); 24 | }); 25 | 26 | test('clones spaces from another declaration', t => { 27 | let root = parse('a{color:black}'); 28 | let decl = new Declaration({ prop: 'margin', value: '0' }); 29 | root.first.append(decl); 30 | t.deepEqual(root.toString(), 'a{color:black;margin:0}'); 31 | }); 32 | -------------------------------------------------------------------------------- /d.ts/stringifier.d.ts: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | declare class Stringifier { 3 | builder: Stringifier.Builder; 4 | constructor(builder?: Stringifier.Builder); 5 | stringify(node: Node, semicolon?: boolean): void; 6 | root(node: any): void; 7 | comment(node: any): void; 8 | decl(node: any, semicolon: any): void; 9 | rule(node: any): void; 10 | atrule(node: any, semicolon: any): void; 11 | body(node: any): void; 12 | block(node: any, start: any): void; 13 | raw(node: Node, own: string, detect?: string): any; 14 | rawSemicolon(root: any): any; 15 | rawEmptyBody(root: any): any; 16 | rawIndent(root: any): any; 17 | rawBeforeComment(root: any, node: any): any; 18 | rawBeforeDecl(root: any, node: any): any; 19 | rawBeforeRule(root: any): any; 20 | rawBeforeClose(root: any): any; 21 | rawBeforeOpen(root: any): any; 22 | rawColon(root: any): any; 23 | beforeAfter(node: any, detect: any): any; 24 | rawValue(node: any, prop: any): any; 25 | } 26 | declare module Stringifier { 27 | interface Builder { 28 | (str: string, node?: Node, str2?: string): void; 29 | } 30 | } 31 | export default Stringifier; 32 | -------------------------------------------------------------------------------- /d.ts/warning.d.ts: -------------------------------------------------------------------------------- 1 | import * as postcss from './postcss'; 2 | import Node from './node'; 3 | export default class Warning implements postcss.Warning { 4 | /** 5 | * Contains the warning message. 6 | */ 7 | text: string; 8 | /** 9 | * Returns a string representing the node's type. Possible values are 10 | * root, atrule, rule, decl or comment. 11 | */ 12 | type: string; 13 | /** 14 | * Contains the name of the plugin that created this warning. When you 15 | * call Node#warn(), it will fill this property automatically. 16 | */ 17 | plugin: string; 18 | /** 19 | * The CSS node that caused the warning. 20 | */ 21 | node: Node; 22 | /** 23 | * The line in the input file with this warning's source. 24 | */ 25 | line: number; 26 | /** 27 | * Column in the input file with this warning's source. 28 | */ 29 | column: number; 30 | /** 31 | * Represents a plugin warning. It can be created using Node#warn(). 32 | */ 33 | constructor( 34 | /** 35 | * Contains the warning message. 36 | */ 37 | text: string, options?: postcss.WarningOptions); 38 | /** 39 | * @returns Error position, message. 40 | */ 41 | toString(): string; 42 | } 43 | -------------------------------------------------------------------------------- /d.ts/parser.d.ts: -------------------------------------------------------------------------------- 1 | import Input from './input'; 2 | import Node from './node'; 3 | import Root from './root'; 4 | export default class Parser { 5 | input: Input; 6 | pos: number; 7 | root: Root; 8 | spaces: string; 9 | semicolon: boolean; 10 | private current; 11 | private tokens; 12 | constructor(input: Input); 13 | tokenize(): void; 14 | loop(): void; 15 | comment(token: any): void; 16 | emptyRule(token: any): void; 17 | word(): void; 18 | rule(tokens: any): void; 19 | decl(tokens: any): void; 20 | atrule(token: any): void; 21 | end(token: any): void; 22 | endFile(): void; 23 | init(node: Node, line?: number, column?: number): void; 24 | raw(node: any, prop: any, tokens: any): void; 25 | spacesFromEnd(tokens: any): string; 26 | spacesFromStart(tokens: any): string; 27 | stringFrom(tokens: any, from: any): string; 28 | colon(tokens: any): number | boolean; 29 | unclosedBracket(bracket: any): void; 30 | unknownWord(start: any): void; 31 | unexpectedClose(token: any): void; 32 | unclosedBlock(): void; 33 | doubleColon(token: any): void; 34 | unnamedAtrule(node: any, token: any): void; 35 | precheckMissedSemicolon(tokens: any): void; 36 | checkMissedSemicolon(tokens: any): void; 37 | } 38 | -------------------------------------------------------------------------------- /d.ts/processor.d.ts: -------------------------------------------------------------------------------- 1 | import LazyResult from './lazy-result'; 2 | import * as postcss from './postcss'; 3 | import Result from './result'; 4 | export default class Processor implements postcss.Processor { 5 | /** 6 | * Contains the current version of PostCSS (e.g., "5.0.19"). 7 | */ 8 | version: '5.0.19'; 9 | /** 10 | * Contains plugins added to this processor. 11 | */ 12 | plugins: postcss.Plugin[]; 13 | constructor(plugins?: (typeof postcss.acceptedPlugin)[]); 14 | /** 15 | * Adds a plugin to be used as a CSS processor. Plugins can also be 16 | * added by passing them as arguments when creating a postcss instance. 17 | */ 18 | use(plugin: typeof postcss.acceptedPlugin): this; 19 | /** 20 | * Parses source CSS. Because some plugins can be asynchronous it doesn't 21 | * make any transformations. Transformations will be applied in LazyResult's 22 | * methods. 23 | * @param css Input CSS or any object with toString() method, like a file 24 | * stream. If a Result instance is passed the processor will take the 25 | * existing Root parser from it. 26 | */ 27 | process(css: string | { 28 | toString(): string; 29 | } | Result, options?: postcss.ProcessOptions): LazyResult; 30 | private normalize(plugins); 31 | } 32 | -------------------------------------------------------------------------------- /test/list.js: -------------------------------------------------------------------------------- 1 | import list from '../lib/list'; 2 | 3 | import test from 'ava'; 4 | 5 | test('space() splits list by spaces', t => { 6 | t.deepEqual(list.space('a b'), ['a', 'b']); 7 | }); 8 | 9 | test('space() trims values', t => { 10 | t.deepEqual(list.space(' a b '), ['a', 'b']); 11 | }); 12 | 13 | test('space() checks quotes', t => { 14 | t.deepEqual(list.space('"a b\\"" \'\''), ['"a b\\""', '\'\'']); 15 | }); 16 | 17 | test('space() checks functions', t => { 18 | t.deepEqual(list.space('f( )) a( () )'), ['f( ))', 'a( () )']); 19 | }); 20 | 21 | test('space() works from variable', t => { 22 | let space = list.space; 23 | t.deepEqual(space('a b'), ['a', 'b']); 24 | }); 25 | 26 | test('comma() splits list by spaces', t => { 27 | t.deepEqual(list.comma('a, b'), ['a', 'b']); 28 | }); 29 | 30 | test('comma() adds last empty', t => { 31 | t.deepEqual(list.comma('a, b,'), ['a', 'b', '']); 32 | }); 33 | 34 | test('comma() checks quotes', t => { 35 | t.deepEqual(list.comma('"a,b\\"", \'\''), ['"a,b\\""', '\'\'']); 36 | }); 37 | 38 | test('comma() checks functions', t => { 39 | t.deepEqual(list.comma('f(,)), a(,(),)'), ['f(,))', 'a(,(),)']); 40 | }); 41 | 42 | test('comma() works from variable', t => { 43 | let comma = list.comma; 44 | t.deepEqual(comma('a, b'), ['a', 'b']); 45 | }); 46 | -------------------------------------------------------------------------------- /d.ts/rule.d.ts: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import * as postcss from './postcss'; 3 | export default class Rule extends Container implements postcss.Rule { 4 | /** 5 | * Returns a string representing the node's type. Possible values are 6 | * root, atrule, rule, decl or comment. 7 | */ 8 | type: string; 9 | /** 10 | * Contains information to generate byte-to-byte equal node string as it 11 | * was in origin input. 12 | */ 13 | raws: postcss.RuleRaws; 14 | /** 15 | * The rule's full selector. If there are multiple comma-separated selectors, 16 | * the entire group will be included. 17 | */ 18 | selector: string; 19 | /** 20 | * Represents a CSS rule: a selector followed by a declaration block. 21 | */ 22 | constructor(defaults?: postcss.RuleNewProps); 23 | /** 24 | * @param overrides New properties to override in the clone. 25 | * @returns A clone of this node. The node and its (cloned) children will 26 | * have a clean parent and code style properties. 27 | */ 28 | clone(overrides?: Object): Rule; 29 | toJSON(): postcss.JsonRule; 30 | /** 31 | * @returns An array containing the rule's individual selectors. 32 | * Groups of selectors are split at commas. 33 | */ 34 | selectors: string[]; 35 | _selector: string; 36 | } 37 | -------------------------------------------------------------------------------- /docs/writing-a-plugin.md: -------------------------------------------------------------------------------- 1 | # Writing a PostCSS Plugin 2 | 3 | ## Getting Started 4 | 5 | * [“Create Your Own Plugin” tutorial](http://webdesign.tutsplus.com/tutorials/postcss-deep-dive-create-your-own-plugin--cms-24605) 6 | * [Plugin Boilerplate](https://github.com/postcss/postcss-plugin-boilerplate) 7 | * [Plugin Guidelines](https://github.com/postcss/postcss/blob/master/docs/guidelines/plugin.md) 8 | * [AST explorer with playground](http://astexplorer.net/#/np0DfVT78g/1) 9 | 10 | ## Documentation and Support 11 | 12 | * [PostCSS API](http://api.postcss.org/) 13 | * [Ask questions](https://gitter.im/postcss/postcss) 14 | * [PostCSS twitter](https://twitter.com/postcss) with latest updates. 15 | 16 | ## Tools 17 | 18 | * [Selector parser](https://github.com/postcss/postcss-selector-parser) 19 | * [Value parser](https://github.com/TrySound/postcss-value-parser) 20 | * [Property resolver](https://github.com/jedmao/postcss-resolve-prop) 21 | * [Function resolver](https://github.com/andyjansson/postcss-functions) 22 | * [Font parser](https://github.com/jedmao/parse-css-font) 23 | * [Dimension parser](https://github.com/jedmao/parse-css-dimension) 24 | for `number`, `length` and `percentage`. 25 | * [Sides parser](https://github.com/jedmao/parse-css-sides) 26 | for `margin`, `padding` and `border` properties. 27 | * [Font helpers](https://github.com/jedmao/postcss-font-helpers) 28 | * [Margin helpers](https://github.com/jedmao/postcss-margin-helpers) 29 | -------------------------------------------------------------------------------- /lib/vendor.es6: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains helpers for working with vendor prefixes. 3 | * 4 | * @example 5 | * const vendor = postcss.vendor; 6 | * 7 | * @namespace vendor 8 | */ 9 | let vendor = { 10 | 11 | /** 12 | * Returns the vendor prefix extracted from an input string. 13 | * 14 | * @param {string} prop - string with or without vendor prefix 15 | * 16 | * @return {string} vendor prefix or empty string 17 | * 18 | * @example 19 | * postcss.vendor.prefix('-moz-tab-size') //=> '-moz-' 20 | * postcss.vendor.prefix('tab-size') //=> '' 21 | */ 22 | prefix(prop) { 23 | if ( prop[0] === '-' ) { 24 | let sep = prop.indexOf('-', 1); 25 | return prop.substr(0, sep + 1); 26 | } else { 27 | return ''; 28 | } 29 | }, 30 | 31 | /** 32 | * Returns the input string stripped of its vendor prefix. 33 | * 34 | * @param {string} prop - string with or without vendor prefix 35 | * 36 | * @return {string} string name without vendor prefixes 37 | * 38 | * @example 39 | * postcss.vendor.unprefixed('-moz-tab-size') //=> 'tab-size' 40 | */ 41 | unprefixed(prop) { 42 | if ( prop[0] === '-' ) { 43 | let sep = prop.indexOf('-', 1); 44 | return prop.substr(sep + 1); 45 | } else { 46 | return prop; 47 | } 48 | } 49 | 50 | }; 51 | 52 | export default vendor; 53 | -------------------------------------------------------------------------------- /test/at-rule.js: -------------------------------------------------------------------------------- 1 | import AtRule from '../lib/at-rule'; 2 | import parse from '../lib/parse'; 3 | 4 | import test from 'ava'; 5 | 6 | test('initializes with properties', t => { 7 | let rule = new AtRule({ name: 'encoding', params: '"utf-8"' }); 8 | 9 | t.deepEqual(rule.name, 'encoding'); 10 | t.deepEqual(rule.params, '"utf-8"'); 11 | 12 | t.deepEqual(rule.toString(), '@encoding "utf-8"'); 13 | }); 14 | 15 | test('does not fall on childless at-rule', t => { 16 | let rule = new AtRule(); 17 | t.deepEqual(typeof rule.each( i => i ), 'undefined'); 18 | }); 19 | 20 | test('creates nodes property on prepend()', t => { 21 | let rule = new AtRule(); 22 | t.deepEqual(typeof rule.nodes, 'undefined'); 23 | 24 | rule.prepend('color: black'); 25 | t.deepEqual(rule.nodes.length, 1); 26 | }); 27 | 28 | test('creates nodes property on append()', t => { 29 | let rule = new AtRule(); 30 | t.deepEqual(typeof rule.nodes, 'undefined'); 31 | 32 | rule.append('color: black'); 33 | t.deepEqual(rule.nodes.length, 1); 34 | }); 35 | 36 | test('inserts default spaces', t => { 37 | let rule = new AtRule({ name: 'page', params: 1, nodes: [] }); 38 | t.deepEqual(rule.toString(), '@page 1 {}'); 39 | }); 40 | 41 | test('clone spaces from another at-rule', t => { 42 | let root = parse('@page{}a{}'); 43 | let rule = new AtRule({ name: 'page', params: 1, nodes: [] }); 44 | root.append(rule); 45 | 46 | t.deepEqual(rule.toString(), '@page 1{}'); 47 | }); 48 | -------------------------------------------------------------------------------- /d.ts/at-rule.d.ts: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import * as postcss from './postcss'; 3 | export default class AtRule extends Container implements postcss.AtRule { 4 | /** 5 | * Returns a string representing the node's type. Possible values are 6 | * root, atrule, rule, decl or comment. 7 | */ 8 | type: string; 9 | /** 10 | * Contains information to generate byte-to-byte equal node string as it 11 | * was in origin input. 12 | */ 13 | raws: postcss.AtRuleRaws; 14 | /** 15 | * The identifier that immediately follows the @. 16 | */ 17 | name: string; 18 | /** 19 | * These are the values that follow the at-rule's name, but precede any {} 20 | * block. The spec refers to this area as the at-rule's "prelude". 21 | */ 22 | params: string; 23 | /** 24 | * Represents an at-rule. If it's followed in the CSS by a {} block, this 25 | * node will have a nodes property representing its children. 26 | */ 27 | constructor(defaults?: postcss.AtRuleNewProps); 28 | /** 29 | * @param overrides New properties to override in the clone. 30 | * @returns A clone of this node. The node and its (cloned) children will 31 | * have a clean parent and code style properties. 32 | */ 33 | clone(overrides?: Object): AtRule; 34 | toJSON(): postcss.JsonAtRule; 35 | append(...children: any[]): this; 36 | prepend(...children: any[]): this; 37 | afterName: string; 38 | _params: string; 39 | } 40 | -------------------------------------------------------------------------------- /d.ts/declaration.d.ts: -------------------------------------------------------------------------------- 1 | import * as postcss from './postcss'; 2 | import Node from './node'; 3 | export default class Declaration extends Node implements postcss.Declaration { 4 | /** 5 | * Returns a string representing the node's type. Possible values are 6 | * root, atrule, rule, decl or comment. 7 | */ 8 | type: string; 9 | /** 10 | * Contains information to generate byte-to-byte equal node string as it 11 | * was in origin input. 12 | */ 13 | raws: postcss.DeclarationRaws; 14 | /** 15 | * The declaration's property name. 16 | */ 17 | prop: string; 18 | /** 19 | * The declaration's value. This value will be cleaned of comments. If the 20 | * source value contained comments, those comments will be available in the 21 | * _value.raws property. If you have not changed the value, the result of 22 | * decl.toString() will include the original raws value (comments and all). 23 | */ 24 | value: string; 25 | /** 26 | * True if the declaration has an !important annotation. 27 | */ 28 | important: boolean; 29 | /** 30 | * Represents a CSS declaration. 31 | */ 32 | constructor(defaults?: postcss.DeclarationNewProps); 33 | /** 34 | * @param overrides New properties to override in the clone. 35 | * @returns A clone of this node. The node and its (cloned) children will 36 | * have a clean parent and code style properties. 37 | */ 38 | clone(overrides?: Object): any; 39 | toJSON(): postcss.JsonDeclaration; 40 | _value: string; 41 | _important: string; 42 | } 43 | -------------------------------------------------------------------------------- /d.ts/input.d.ts: -------------------------------------------------------------------------------- 1 | import CssSyntaxError from './css-syntax-error'; 2 | import PreviousMap from './previous-map'; 3 | import LazyResult from './lazy-result'; 4 | import * as postcss from './postcss'; 5 | import Result from './result'; 6 | export default class Input implements postcss.Input { 7 | /** 8 | * The absolute path to the CSS source file defined with the "from" option. 9 | */ 10 | file: string; 11 | /** 12 | * The unique ID of the CSS source. Used if "from" option is not provided 13 | * (because PostCSS does not know the file path). 14 | */ 15 | id: string; 16 | /** 17 | * Represents the input source map passed from a compilation step before 18 | * PostCSS (e.g., from the Sass compiler). 19 | */ 20 | map: PreviousMap; 21 | css: string; 22 | /** 23 | * Represents the source CSS. 24 | */ 25 | constructor(css: string | { 26 | toString(): string; 27 | } | LazyResult | Result, opts?: { 28 | safe?: boolean | any; 29 | from?: string; 30 | }); 31 | /** 32 | * The CSS source identifier. Contains input.file if the user set the "from" 33 | * option, or input.id if they did not. 34 | */ 35 | from: string; 36 | error(message: string, line: number, column: number, opts?: { 37 | plugin?: string; 38 | }): CssSyntaxError; 39 | /** 40 | * Reads the input source map. 41 | * @returns A symbol position in the input source (e.g., in a Sass file 42 | * that was compiled to CSS before being passed to PostCSS): 43 | */ 44 | origin(line: number, column: number): postcss.InputOrigin; 45 | private mapResolve(file); 46 | } 47 | -------------------------------------------------------------------------------- /test/lazy-result.js: -------------------------------------------------------------------------------- 1 | import LazyResult from '../lib/lazy-result'; 2 | import Processor from '../lib/processor'; 3 | 4 | import mozilla from 'source-map'; 5 | import test from 'ava'; 6 | 7 | let processor = new Processor(); 8 | 9 | test('contains AST', t => { 10 | let result = new LazyResult(processor, 'a {}', { }); 11 | t.deepEqual(result.root.type, 'root'); 12 | }); 13 | 14 | test('will stringify css', t => { 15 | let result = new LazyResult(processor, 'a {}', { }); 16 | t.deepEqual(result.css, 'a {}'); 17 | }); 18 | 19 | test('stringifies css', t => { 20 | let result = new LazyResult(processor, 'a {}', { }); 21 | t.deepEqual('' + result, result.css); 22 | }); 23 | 24 | test('has content alias for css', t => { 25 | let result = new LazyResult(processor, 'a {}', { }); 26 | t.deepEqual(result.content, 'a {}'); 27 | }); 28 | 29 | test('has map only if necessary', t => { 30 | let result = new LazyResult(processor, '', { }); 31 | t.deepEqual(typeof result.map, 'undefined'); 32 | 33 | result = new LazyResult(processor, '', { }); 34 | t.deepEqual(typeof result.map, 'undefined'); 35 | 36 | result = new LazyResult(processor, '', { map: { inline: false } }); 37 | t.truthy(result.map instanceof mozilla.SourceMapGenerator); 38 | }); 39 | 40 | test('contains options', t => { 41 | let result = new LazyResult(processor, 'a {}', { to: 'a.css' }); 42 | t.deepEqual(result.opts, { to: 'a.css' }); 43 | }); 44 | 45 | test('contains warnings', t => { 46 | let result = new LazyResult(processor, 'a {}', { }); 47 | t.deepEqual(result.warnings(), []); 48 | }); 49 | 50 | test('contains messages', t => { 51 | let result = new LazyResult(processor, 'a {}', { }); 52 | t.deepEqual(result.messages, []); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/comment.es6: -------------------------------------------------------------------------------- 1 | import warnOnce from './warn-once'; 2 | import Node from './node'; 3 | 4 | /** 5 | * Represents a comment between declarations or statements (rule and at-rules). 6 | * 7 | * Comments inside selectors, at-rule parameters, or declaration values 8 | * will be stored in the `raws` properties explained above. 9 | * 10 | * @extends Node 11 | */ 12 | class Comment extends Node { 13 | 14 | constructor(defaults) { 15 | super(defaults); 16 | this.type = 'comment'; 17 | } 18 | 19 | get left() { 20 | warnOnce('Comment#left was deprecated. Use Comment#raws.left'); 21 | return this.raws.left; 22 | } 23 | 24 | set left(val) { 25 | warnOnce('Comment#left was deprecated. Use Comment#raws.left'); 26 | this.raws.left = val; 27 | } 28 | 29 | get right() { 30 | warnOnce('Comment#right was deprecated. Use Comment#raws.right'); 31 | return this.raws.right; 32 | } 33 | 34 | set right(val) { 35 | warnOnce('Comment#right was deprecated. Use Comment#raws.right'); 36 | this.raws.right = val; 37 | } 38 | 39 | /** 40 | * @memberof Comment# 41 | * @member {string} text - the comment’s text 42 | */ 43 | 44 | /** 45 | * @memberof Comment# 46 | * @member {object} raws - Information to generate byte-to-byte equal 47 | * node string as it was in the origin input. 48 | * 49 | * Every parser saves its own properties, 50 | * but the default CSS parser uses: 51 | * 52 | * * `before`: the space symbols before the node. 53 | * * `left`: the space symbols between `/*` and the comment’s text. 54 | * * `right`: the space symbols between the comment’s text. 55 | */ 56 | } 57 | 58 | export default Comment; 59 | -------------------------------------------------------------------------------- /test/result.js: -------------------------------------------------------------------------------- 1 | import Warning from '../lib/warning'; 2 | import postcss from '../lib/postcss'; 3 | import Result from '../lib/result'; 4 | 5 | import test from 'ava'; 6 | 7 | test('stringifies', t => { 8 | let result = new Result(); 9 | result.css = 'a{}'; 10 | t.deepEqual('' + result, result.css); 11 | }); 12 | 13 | test('adds warning', t => { 14 | let warning; 15 | let plugin = postcss.plugin('test-plugin', () => { 16 | return (css, res) => { 17 | warning = res.warn('test', { node: css.first }); 18 | }; 19 | }); 20 | let result = postcss([plugin]).process('a{}').sync(); 21 | 22 | t.deepEqual(warning, new Warning('test', { 23 | plugin: 'test-plugin', 24 | node: result.root.first 25 | })); 26 | 27 | t.deepEqual(result.messages, [warning]); 28 | }); 29 | 30 | test('allows to override plugin', t => { 31 | let plugin = postcss.plugin('test-plugin', () => { 32 | return (css, res) => { 33 | res.warn('test', { plugin: 'test-plugin#one' }); 34 | }; 35 | }); 36 | let result = postcss([plugin]).process('a{}').sync(); 37 | 38 | t.deepEqual(result.messages[0].plugin, 'test-plugin#one'); 39 | }); 40 | 41 | test('allows Root', t => { 42 | let result = new Result(); 43 | let root = postcss.parse('a{}'); 44 | result.warn('TT', { node: root }); 45 | 46 | t.deepEqual(result.messages[0].toString(), ':1:1: TT'); 47 | }); 48 | 49 | test('returns only warnings', t => { 50 | let result = new Result(); 51 | result.messages = [{ type: 'warning', text: 'a' }, 52 | { type: 'custom' }, 53 | { type: 'warning', text: 'b' }]; 54 | t.deepEqual(result.warnings(), [{ type: 'warning', text: 'a' }, 55 | { type: 'warning', text: 'b' }]); 56 | }); 57 | -------------------------------------------------------------------------------- /d.ts/root.d.ts: -------------------------------------------------------------------------------- 1 | import PreviousMap from './previous-map'; 2 | import Container from './container'; 3 | import * as postcss from './postcss'; 4 | import Result from './result'; 5 | import Node from './node'; 6 | export default class Root extends Container implements postcss.Root { 7 | /** 8 | * Returns a string representing the node's type. Possible values are 9 | * root, atrule, rule, decl or comment. 10 | */ 11 | type: string; 12 | rawCache: { 13 | [key: string]: any; 14 | }; 15 | /** 16 | * Represents a CSS file and contains all its parsed nodes. 17 | */ 18 | constructor(defaults?: postcss.RootNewProps); 19 | /** 20 | * @param overrides New properties to override in the clone. 21 | * @returns A clone of this node. The node and its (cloned) children will 22 | * have a clean parent and code style properties. 23 | */ 24 | clone(overrides?: Object): Root; 25 | toJSON(): postcss.JsonRoot; 26 | /** 27 | * Removes child from the root node, and the parent properties of node and 28 | * its children. 29 | * @param child Child or child's index. 30 | * @returns This root node for chaining. 31 | */ 32 | removeChild(child: Node | number): this; 33 | protected normalize(node: Node | string, sample: Node, type?: string): Node[]; 34 | protected normalize(props: postcss.AtRuleNewProps | postcss.RuleNewProps | postcss.DeclarationNewProps | postcss.CommentNewProps, sample: Node, type?: string): Node[]; 35 | /** 36 | * @returns A Result instance representing the root's CSS. 37 | */ 38 | toResult(options?: { 39 | /** 40 | * The path where you'll put the output CSS file. You should always 41 | * set "to" to generate correct source maps. 42 | */ 43 | to?: string; 44 | map?: postcss.SourceMapOptions; 45 | }): Result; 46 | /** 47 | * Deprecated. Use Root#removeChild. 48 | */ 49 | remove(child?: Node | number): Root; 50 | /** 51 | * Deprecated. Use Root#source.input.map. 52 | */ 53 | prevMap(): PreviousMap; 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss", 3 | "version": "5.1.0", 4 | "description": "Tool for transforming styles with JS plugins", 5 | "typings": "d.ts/postcss.d.ts", 6 | "engines": { 7 | "node": ">=0.12" 8 | }, 9 | "keywords": [ 10 | "css", 11 | "postcss", 12 | "rework", 13 | "preprocessor", 14 | "parser", 15 | "source map", 16 | "transform", 17 | "manipulation", 18 | "transpiler" 19 | ], 20 | "author": "Andrey Sitnik ", 21 | "license": "MIT", 22 | "homepage": "http://postcss.org/", 23 | "repository": "postcss/postcss", 24 | "dependencies": { 25 | "supports-color": "^3.1.2", 26 | "source-map": "^0.5.6", 27 | "js-base64": "^2.1.9" 28 | }, 29 | "devDependencies": { 30 | "babel-plugin-precompile-charcodes": "1.0.0", 31 | "babel-plugin-add-module-exports": "0.2.1", 32 | "babel-preset-es2015-loose": "7.0.0", 33 | "concat-with-sourcemaps": "1.0.4", 34 | "eslint-config-postcss": "2.0.2", 35 | "postcss-parser-tests": "5.0.9", 36 | "babel-preset-es2015": "6.9.0", 37 | "gulp-sourcemaps": "1.6.0", 38 | "babel-eslint": "6.1.2", 39 | "gulp-eslint": "3.0.1", 40 | "gulp-jsdoc3": "0.3.0", 41 | "babel-core": "6.10.4", 42 | "gulp-babel": "6.1.2", 43 | "strip-ansi": "3.0.1", 44 | "yaspeller": "2.8.1", 45 | "gulp-run": "1.7.1", 46 | "gulp-ava": "0.12.1", 47 | "fs-extra": "0.30.0", 48 | "docdash": "0.4.0", 49 | "eslint": "3.0.1", 50 | "sinon": "1.17.4", 51 | "gulp": "3.9.1", 52 | "ava": "0.15.2", 53 | "del": "2.2.1" 54 | }, 55 | "scripts": { 56 | "test": "gulp" 57 | }, 58 | "main": "lib/postcss" 59 | } 60 | -------------------------------------------------------------------------------- /test/root.js: -------------------------------------------------------------------------------- 1 | import Result from '../lib/result'; 2 | import parse from '../lib/parse'; 3 | 4 | import test from 'ava'; 5 | 6 | test('prepend() fixes spaces on insert before first', t => { 7 | let css = parse('a {} b {}'); 8 | css.prepend({ selector: 'em' }); 9 | t.deepEqual(css.toString(), 'em {} a {} b {}'); 10 | }); 11 | 12 | test('prepend() fixes spaces on multiple inserts before first', t => { 13 | let css = parse('a {} b {}'); 14 | css.prepend({ selector: 'em' }, { selector: 'strong' }); 15 | t.deepEqual(css.toString(), 'em {} strong {} a {} b {}'); 16 | }); 17 | 18 | test('prepend() uses default spaces on only first', t => { 19 | let css = parse('a {}'); 20 | css.prepend({ selector: 'em' }); 21 | t.deepEqual(css.toString(), 'em {}\na {}'); 22 | }); 23 | 24 | test('append() sets new line between rules in multiline files', t => { 25 | let a = parse('a {}\n\na {}\n'); 26 | let b = parse('b {}\n'); 27 | t.deepEqual(a.append(b).toString(), 'a {}\n\na {}\n\nb {}\n'); 28 | }); 29 | 30 | test('append() sets new line between rules on last newline', t => { 31 | let a = parse('a {}\n'); 32 | let b = parse('b {}\n'); 33 | t.deepEqual(a.append(b).toString(), 'a {}\nb {}\n'); 34 | }); 35 | 36 | test('append() saves compressed style', t => { 37 | let a = parse('a{}a{}'); 38 | let b = parse('b {\n}\n'); 39 | t.deepEqual(a.append(b).toString(), 'a{}a{}b{}'); 40 | }); 41 | 42 | test('append() saves compressed style with multiple nodes', t => { 43 | let a = parse('a{}a{}'); 44 | let b = parse('b {\n}\n'); 45 | let c = parse('c {\n}\n'); 46 | t.deepEqual(a.append(b, c).toString(), 'a{}a{}b{}c{}'); 47 | }); 48 | 49 | test('insertAfter() does not use before of first rule', t => { 50 | let css = parse('a{} b{}'); 51 | css.insertAfter(0, { selector: '.a' }); 52 | css.insertAfter(2, { selector: '.b' }); 53 | 54 | t.deepEqual(typeof css.nodes[1].raws.before, 'undefined'); 55 | t.deepEqual(css.nodes[3].raws.before, ' '); 56 | t.deepEqual(css.toString(), 'a{} .a{} b{} .b{}'); 57 | }); 58 | 59 | test('fixes spaces on removing first rule', t => { 60 | let css = parse('a{}\nb{}\n'); 61 | css.first.remove(); 62 | t.deepEqual(css.toString(), 'b{}\n'); 63 | }); 64 | 65 | test('generates result with map', t => { 66 | let root = parse('a {}'); 67 | let result = root.toResult({ map: true }); 68 | 69 | t.truthy(result instanceof Result); 70 | t.regex(result.css, /a \{\}\n\/\*# sourceMappingURL=/); 71 | }); 72 | -------------------------------------------------------------------------------- /test/rule.js: -------------------------------------------------------------------------------- 1 | import parse from '../lib/parse'; 2 | import Rule from '../lib/rule'; 3 | 4 | import test from 'ava'; 5 | 6 | test('initializes with properties', t => { 7 | let rule = new Rule({ selector: 'a' }); 8 | t.deepEqual(rule.selector, 'a'); 9 | }); 10 | 11 | test('returns array in selectors', t => { 12 | let rule = new Rule({ selector: 'a,b' }); 13 | t.deepEqual(rule.selectors, ['a', 'b']); 14 | }); 15 | 16 | test('trims selectors', t => { 17 | let rule = new Rule({ selector: '.a\n, .b , .c' }); 18 | t.deepEqual(rule.selectors, ['.a', '.b', '.c']); 19 | }); 20 | 21 | test('is smart about selectors commas', t => { 22 | let rule = new Rule({ 23 | selector: '[foo=\'a, b\'], a:-moz-any(:focus, [href*=\',\'])' 24 | }); 25 | t.deepEqual(rule.selectors, [ 26 | '[foo=\'a, b\']', 27 | 'a:-moz-any(:focus, [href*=\',\'])' 28 | ]); 29 | }); 30 | 31 | test('receive array in selectors', t => { 32 | let rule = new Rule({ selector: 'i, b' }); 33 | rule.selectors = ['em', 'strong']; 34 | t.deepEqual(rule.selector, 'em, strong'); 35 | }); 36 | 37 | test('saves separator in selectors', t => { 38 | let rule = new Rule({ selector: 'i,\nb' }); 39 | rule.selectors = ['em', 'strong']; 40 | t.deepEqual(rule.selector, 'em,\nstrong'); 41 | }); 42 | 43 | test('uses between to detect separator in selectors', t => { 44 | let rule = new Rule({ selector: 'b', raws: { between: '' } }); 45 | rule.selectors = ['b', 'strong']; 46 | t.deepEqual(rule.selector, 'b,strong'); 47 | }); 48 | 49 | test('uses space in separator be default in selectors', t => { 50 | let rule = new Rule({ selector: 'b' }); 51 | rule.selectors = ['b', 'strong']; 52 | t.deepEqual(rule.selector, 'b, strong'); 53 | }); 54 | 55 | test('selectors works in constructor', t => { 56 | let rule = new Rule({ selectors: ['a', 'b'] }); 57 | t.deepEqual(rule.selector, 'a, b'); 58 | }); 59 | 60 | test('inserts default spaces', t => { 61 | let rule = new Rule({ selector: 'a' }); 62 | t.deepEqual(rule.toString(), 'a {}'); 63 | rule.append({ prop: 'color', value: 'black' }); 64 | t.deepEqual(rule.toString(), 'a {\n color: black\n}'); 65 | }); 66 | 67 | test('clones spaces from another rule', t => { 68 | let root = parse('b{\n }'); 69 | let rule = new Rule({ selector: 'em' }); 70 | root.append(rule); 71 | t.deepEqual(root.toString(), 'b{\n }\nem{\n }'); 72 | }); 73 | 74 | test('uses different spaces for empty rules', t => { 75 | let root = parse('a{}\nb{\n a:1\n}'); 76 | let rule = new Rule({ selector: 'em' }); 77 | root.append(rule); 78 | t.deepEqual(root.toString(), 'a{}\nb{\n a:1\n}\nem{}'); 79 | 80 | rule.append({ prop: 'top', value: '0' }); 81 | t.deepEqual(root.toString(), 'a{}\nb{\n a:1\n}\nem{\n top:0\n}'); 82 | }); 83 | -------------------------------------------------------------------------------- /lib/list.es6: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains helpers for safely splitting lists of CSS values, 3 | * preserving parentheses and quotes. 4 | * 5 | * @example 6 | * const list = postcss.list; 7 | * 8 | * @namespace list 9 | */ 10 | let list = { 11 | 12 | split(string, separators, last) { 13 | let array = []; 14 | let current = ''; 15 | let split = false; 16 | 17 | let func = 0; 18 | let quote = false; 19 | let escape = false; 20 | 21 | for ( let i = 0; i < string.length; i++ ) { 22 | let letter = string[i]; 23 | 24 | if ( quote ) { 25 | if ( escape ) { 26 | escape = false; 27 | } else if ( letter === '\\' ) { 28 | escape = true; 29 | } else if ( letter === quote ) { 30 | quote = false; 31 | } 32 | } else if ( letter === '"' || letter === '\'' ) { 33 | quote = letter; 34 | } else if ( letter === '(' ) { 35 | func += 1; 36 | } else if ( letter === ')' ) { 37 | if ( func > 0 ) func -= 1; 38 | } else if ( func === 0 ) { 39 | if ( separators.indexOf(letter) !== -1 ) split = true; 40 | } 41 | 42 | if ( split ) { 43 | if ( current !== '' ) array.push(current.trim()); 44 | current = ''; 45 | split = false; 46 | } else { 47 | current += letter; 48 | } 49 | } 50 | 51 | if ( last || current !== '' ) array.push(current.trim()); 52 | return array; 53 | }, 54 | 55 | /** 56 | * Safely splits space-separated values (such as those for `background`, 57 | * `border-radius`, and other shorthand properties). 58 | * 59 | * @param {string} string - space-separated values 60 | * 61 | * @return {string[]} splitted values 62 | * 63 | * @example 64 | * postcss.list.space('1px calc(10% + 1px)') //=> ['1px', 'calc(10% + 1px)'] 65 | */ 66 | space(string) { 67 | let spaces = [' ', '\n', '\t']; 68 | return list.split(string, spaces); 69 | }, 70 | 71 | /** 72 | * Safely splits comma-separated values (such as those for `transition-*` 73 | * and `background` properties). 74 | * 75 | * @param {string} string - comma-separated values 76 | * 77 | * @return {string[]} splitted values 78 | * 79 | * @example 80 | * postcss.list.comma('black, linear-gradient(white, black)') 81 | * //=> ['black', 'linear-gradient(white, black)'] 82 | */ 83 | comma(string) { 84 | let comma = ','; 85 | return list.split(string, [comma], true); 86 | } 87 | 88 | }; 89 | 90 | export default list; 91 | -------------------------------------------------------------------------------- /.yaspellerrc: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "en", 3 | "format": "markdown", 4 | "fileExtensions": [".md"], 5 | "excludeFiles": [".git", "node_modules", "build"], 6 | "ignoreCapitalization": true, 7 | "dictionary": [ 8 | "6to5", 9 | "ASE", 10 | "AtCSS", 11 | "Autoprefixer", 12 | "Base64", 13 | "cssnext", 14 | "cssnano", 15 | "Browserhacks", 16 | "CoffeeScript", 17 | "GitHub", 18 | "JetBrains", 19 | "WebStorm", 20 | "Traceur", 21 | "PostCSS", 22 | "PostCSS’s", 23 | "postcss", 24 | "Shopify", 25 | "Instagram", 26 | "Travis", 27 | "poststylus", 28 | "Gofmt", 29 | "CI", 30 | "MD5", 31 | "W3C", 32 | "WebP", 33 | "VK", 34 | "IE8", 35 | "IE9", 36 | "ES6", 37 | "ENB", 38 | "SemVer", 39 | "Sass", 40 | "Sass’s", 41 | "SCSS", 42 | "Weibo", 43 | "PreCSS", 44 | "libsass", 45 | "CSSOM", 46 | "Jeet", 47 | "Modernizr", 48 | "npm", 49 | "webpack", 50 | "Amdusias", 51 | "Anton", 52 | "Andres", 53 | "Andromalius", 54 | "Andrealphus", 55 | "Andalusian", 56 | "Andras", 57 | "asan", 58 | "Lind", 59 | "Zuo", 60 | "Jed", 61 | "Neal", 62 | "Valac", 63 | "Briggs", 64 | "Bogdan", 65 | "Chadkin", 66 | "ClojureWerkz’s", 67 | "Dantalion", 68 | "Decarabia", 69 | "Dvornov", 70 | "Flauros", 71 | "Josiah", 72 | "Lydell", 73 | "Lolspeak", 74 | "Matija", 75 | "Marohnić", 76 | "Maxime", 77 | "Mohammad", 78 | "Nikitenko", 79 | "Savary", 80 | "Seere", 81 | "Suarez", 82 | "Thirouin", 83 | "Peterson", 84 | "Yakushev", 85 | "Younes", 86 | "Cimeies", 87 | "Autodetect", 88 | "autodetect", 89 | "codebases", 90 | "flexbox", 91 | "unprefixes", 92 | "regexp", 93 | "changelog", 94 | "Changelog", 95 | "Transpiler", 96 | "transpiler", 97 | "transpile", 98 | "transpiles", 99 | "transpiling", 100 | "tokenize", 101 | "tokenizer", 102 | "linter", 103 | "linters", 104 | "rebases", 105 | "resolver", 106 | "minifier", 107 | "isolatable", 108 | "minifiers", 109 | "mixins", 110 | "mixin", 111 | "multitool", 112 | "io", 113 | "partials", 114 | "inlined", 115 | "inlines", 116 | "polyfill", 117 | "stylesheet", 118 | "stylesheets", 119 | "stringifier", 120 | "keyframes", 121 | "BEM", 122 | "CLI", 123 | "CSS3", 124 | "CSS4", 125 | "SugarSS", 126 | "SVG", 127 | "SVGO", 128 | "JS", 129 | "js", 130 | "vs", 131 | "pantone", 132 | "YIQ", 133 | "gitter", 134 | "evilmartians", 135 | "Less’s", 136 | "visualizer", 137 | "Rollup" 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /d.ts/result.d.ts: -------------------------------------------------------------------------------- 1 | import Processor from './processor'; 2 | import * as postcss from './postcss'; 3 | import Root from './root'; 4 | export default class Result implements postcss.Result { 5 | /** 6 | * The Processor instance used for this transformation. 7 | */ 8 | processor: Processor; 9 | /** 10 | * Contains the Root node after all transformations. 11 | */ 12 | root: Root; 13 | /** 14 | * Options from the Processor#process(css, opts) or Root#toResult(opts) call 15 | * that produced this Result instance. 16 | */ 17 | opts: postcss.ResultOptions; 18 | /** 19 | * A CSS string representing this Result's Root instance. 20 | */ 21 | css: string; 22 | /** 23 | * An instance of the SourceMapGenerator class from the source-map library, 24 | * representing changes to the Result's Root instance. 25 | * This property will have a value only if the user does not want an inline 26 | * source map. By default, PostCSS generates inline source maps, written 27 | * directly into the processed CSS. The map property will be empty by default. 28 | * An external source map will be generated — and assigned to map — only if 29 | * the user has set the map.inline option to false, or if PostCSS was passed 30 | * an external input source map. 31 | */ 32 | map: postcss.ResultMap; 33 | /** 34 | * Contains messages from plugins (e.g., warnings or custom messages). 35 | * Add a warning using Result#warn() and get all warnings 36 | * using the Result#warnings() method. 37 | */ 38 | messages: postcss.ResultMessage[]; 39 | lastPlugin: postcss.Transformer; 40 | /** 41 | * Provides the result of the PostCSS transformations. 42 | */ 43 | constructor( 44 | /** 45 | * The Processor instance used for this transformation. 46 | */ 47 | processor?: Processor, 48 | /** 49 | * Contains the Root node after all transformations. 50 | */ 51 | root?: Root, 52 | /** 53 | * Options from the Processor#process(css, opts) or Root#toResult(opts) call 54 | * that produced this Result instance. 55 | */ 56 | opts?: postcss.ResultOptions); 57 | /** 58 | * Alias for css property. 59 | */ 60 | toString(): string; 61 | /** 62 | * Creates an instance of Warning and adds it to messages. 63 | * @param message Used in the text property of the message object. 64 | * @param options Properties for Message object. 65 | */ 66 | warn(message: string, options?: postcss.WarningOptions): void; 67 | /** 68 | * @returns Warnings from plugins, filtered from messages. 69 | */ 70 | warnings(): postcss.ResultMessage[]; 71 | /** 72 | * Alias for css property to use with syntaxes that generate non-CSS output. 73 | */ 74 | content: string; 75 | } 76 | -------------------------------------------------------------------------------- /test/warning.js: -------------------------------------------------------------------------------- 1 | import Declaration from '../lib/declaration'; 2 | import Warning from '../lib/warning'; 3 | import parse from '../lib/parse'; 4 | 5 | import path from 'path'; 6 | import test from 'ava'; 7 | 8 | test('outputs simple warning', t => { 9 | let warning = new Warning('text'); 10 | t.deepEqual(warning.toString(), 'text'); 11 | }); 12 | 13 | test('outputs warning with plugin', t => { 14 | let warning = new Warning('text', { plugin: 'plugin' }); 15 | t.deepEqual(warning.toString(), 'plugin: text'); 16 | }); 17 | 18 | test('outputs warning with position', t => { 19 | let root = parse('a{}'); 20 | let warning = new Warning('text', { node: root.first }); 21 | t.deepEqual(warning.toString(), ':1:1: text'); 22 | }); 23 | 24 | test('outputs warning with plugin and node', t => { 25 | let file = path.resolve('a.css'); 26 | let root = parse('a{}', { from: file }); 27 | let warning = new Warning('text', { 28 | plugin: 'plugin', 29 | node: root.first 30 | }); 31 | t.deepEqual(warning.toString(), `plugin: ${ file }:1:1: text`); 32 | }); 33 | 34 | test('outputs warning with index', t => { 35 | let file = path.resolve('a.css'); 36 | let root = parse('@rule param {}', { from: file }); 37 | let warning = new Warning('text', { 38 | plugin: 'plugin', 39 | node: root.first, 40 | index: 7 41 | }); 42 | t.deepEqual(warning.toString(), `plugin: ${ file }:1:8: text`); 43 | }); 44 | 45 | test('outputs warning with word', t => { 46 | let file = path.resolve('a.css'); 47 | let root = parse('@rule param {}', { from: file }); 48 | let warning = new Warning('text', { 49 | plugin: 'plugin', 50 | node: root.first, 51 | word: 'am' 52 | }); 53 | t.deepEqual(warning.toString(), `plugin: ${ file }:1:10: text`); 54 | }); 55 | 56 | test('generates warning without source', t => { 57 | let decl = new Declaration({ prop: 'color', value: 'black' }); 58 | let warning = new Warning('text', { node: decl }); 59 | t.deepEqual(warning.toString(), ': text'); 60 | }); 61 | 62 | test('has line and column is undefined by default', t => { 63 | let warning = new Warning('text'); 64 | t.deepEqual(typeof warning.line, 'undefined'); 65 | t.deepEqual(typeof warning.column, 'undefined'); 66 | }); 67 | 68 | test('gets position from node', t => { 69 | let root = parse('a{}'); 70 | let warning = new Warning('text', { node: root.first }); 71 | t.deepEqual(warning.line, 1); 72 | t.deepEqual(warning.column, 1); 73 | }); 74 | 75 | test('gets position from word', t => { 76 | let root = parse('a b{}'); 77 | let warning = new Warning('text', { node: root.first, word: 'b' }); 78 | t.deepEqual(warning.line, 1); 79 | t.deepEqual(warning.column, 3); 80 | }); 81 | 82 | test('gets position from index', t => { 83 | let root = parse('a b{}'); 84 | let warning = new Warning('text', { node: root.first, index: 2 }); 85 | t.deepEqual(warning.line, 1); 86 | t.deepEqual(warning.column, 3); 87 | }); 88 | -------------------------------------------------------------------------------- /lib/root.es6: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import warnOnce from './warn-once'; 3 | 4 | /** 5 | * Represents a CSS file and contains all its parsed nodes. 6 | * 7 | * @extends Container 8 | * 9 | * @example 10 | * const root = postcss.parse('a{color:black} b{z-index:2}'); 11 | * root.type //=> 'root' 12 | * root.nodes.length //=> 2 13 | */ 14 | class Root extends Container { 15 | 16 | constructor(defaults) { 17 | super(defaults); 18 | this.type = 'root'; 19 | if ( !this.nodes ) this.nodes = []; 20 | } 21 | 22 | removeChild(child) { 23 | child = this.index(child); 24 | 25 | if ( child === 0 && this.nodes.length > 1 ) { 26 | this.nodes[1].raws.before = this.nodes[child].raws.before; 27 | } 28 | 29 | return super.removeChild(child); 30 | } 31 | 32 | normalize(child, sample, type) { 33 | let nodes = super.normalize(child); 34 | 35 | if ( sample ) { 36 | if ( type === 'prepend' ) { 37 | if ( this.nodes.length > 1 ) { 38 | sample.raws.before = this.nodes[1].raws.before; 39 | } else { 40 | delete sample.raws.before; 41 | } 42 | } else if ( this.first !== sample ) { 43 | for ( let node of nodes ) { 44 | node.raws.before = sample.raws.before; 45 | } 46 | } 47 | } 48 | 49 | return nodes; 50 | } 51 | 52 | /** 53 | * Returns a {@link Result} instance representing the root’s CSS. 54 | * 55 | * @param {processOptions} [opts] - options with only `to` and `map` keys 56 | * 57 | * @return {Result} result with current root’s CSS 58 | * 59 | * @example 60 | * const root1 = postcss.parse(css1, { from: 'a.css' }); 61 | * const root2 = postcss.parse(css2, { from: 'b.css' }); 62 | * root1.append(root2); 63 | * const result = root1.toResult({ to: 'all.css', map: true }); 64 | */ 65 | toResult(opts = { }) { 66 | let LazyResult = require('./lazy-result'); 67 | let Processor = require('./processor'); 68 | 69 | let lazy = new LazyResult(new Processor(), this, opts); 70 | return lazy.stringify(); 71 | } 72 | 73 | remove(child) { 74 | warnOnce('Root#remove is deprecated. Use Root#removeChild'); 75 | this.removeChild(child); 76 | } 77 | 78 | prevMap() { 79 | warnOnce('Root#prevMap is deprecated. Use Root#source.input.map'); 80 | return this.source.input.map; 81 | } 82 | 83 | /** 84 | * @memberof Root# 85 | * @member {object} raws - Information to generate byte-to-byte equal 86 | * node string as it was in the origin input. 87 | * 88 | * Every parser saves its own properties, 89 | * but the default CSS parser uses: 90 | * 91 | * * `after`: the space symbols after the last child to the end of file. 92 | * * `semicolon`: is the last child has an (optional) semicolon. 93 | * 94 | * @example 95 | * postcss.parse('a {}\n').raws //=> { after: '\n' } 96 | * postcss.parse('a {}').raws //=> { after: '' } 97 | */ 98 | 99 | } 100 | 101 | export default Root; 102 | -------------------------------------------------------------------------------- /docs/source-maps.md: -------------------------------------------------------------------------------- 1 | # PostCSS and Source Maps 2 | 3 | PostCSS has great [source maps] support. It can read and interpret maps 4 | from previous transformation steps, autodetect the format that you expect, 5 | and output both external and inline maps. 6 | 7 | To ensure that you generate an accurate source map, you must indicate the input 8 | and output CSS file paths — using the options `from` and `to`, respectively. 9 | 10 | To generate a new source map with the default options, simply set `map: true`. 11 | This will generate an inline source map that contains the source content. 12 | If you don’t want the map inlined, you can set `map.inline: false`. 13 | 14 | ```js 15 | processor 16 | .process(css, { 17 | from: 'app.sass.css', 18 | to: 'app.css', 19 | map: { inline: false }, 20 | }) 21 | .then(function (result) { 22 | result.map //=> '{ "version":3, 23 | // "file":"app.css", 24 | // "sources":["app.sass"], 25 | // "mappings":"AAAA,KAAI" }' 26 | }); 27 | ``` 28 | 29 | If PostCSS finds source maps from a previous transformation, 30 | it will automatically update that source map with the same options. 31 | 32 | ## Options 33 | 34 | If you want more control over source map generation, you can define the `map` 35 | option as an object with the following parameters: 36 | 37 | * `inline` boolean: indicates that the source map should be embedded 38 | in the output CSS as a Base64-encoded comment. By default, it is `true`. 39 | But if all previous maps are external, not inline, PostCSS will not embed 40 | the map even if you do not set this option. 41 | 42 | If you have an inline source map, the `result.map` property will be empty, 43 | as the source map will be contained within the text of `result.css`. 44 | 45 | * `prev` string, object, boolean or function: source map content from 46 | a previous processing step (for example, Sass compilation). 47 | PostCSS will try to read the previous source map automatically 48 | (based on comments within the source CSS), but you can use this option 49 | to identify it manually. If desired, you can omit the previous map 50 | with `prev: false`. 51 | 52 | * `sourcesContent` boolean: indicates that PostCSS should set the origin 53 | content (for example, Sass source) of the source map. By default, 54 | it is `true`. But if all previous maps do not contain sources content, 55 | PostCSS will also leave it out even if you do not set this option. 56 | 57 | * `annotation` boolean or string: indicates that PostCSS should add annotation 58 | comments to the CSS. By default, PostCSS will always add a comment with a path 59 | to the source map. PostCSS will not add annotations to CSS files that 60 | do not contain any comments. 61 | 62 | By default, PostCSS presumes that you want to save the source map as 63 | `opts.to + '.map'` and will use this path in the annotation comment. 64 | A different path can be set by providing a string value for `annotation`. 65 | 66 | If you have set `inline: true`, annotation cannot be disabled. 67 | 68 | * `from` string: by default, PostCSS will set the `sources` property of the map 69 | to the value of the `from` option. If you want to override this behaviour, you 70 | can use `map.from` to explicitly set the source map's `sources` property. 71 | 72 | [source maps]: http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/ 73 | -------------------------------------------------------------------------------- /lib/declaration.es6: -------------------------------------------------------------------------------- 1 | import warnOnce from './warn-once'; 2 | import Node from './node'; 3 | 4 | /** 5 | * Represents a CSS declaration. 6 | * 7 | * @extends Node 8 | * 9 | * @example 10 | * const root = postcss.parse('a { color: black }'); 11 | * const decl = root.first.first; 12 | * decl.type //=> 'decl' 13 | * decl.toString() //=> ' color: black' 14 | */ 15 | class Declaration extends Node { 16 | 17 | constructor(defaults) { 18 | super(defaults); 19 | this.type = 'decl'; 20 | } 21 | 22 | get _value() { 23 | warnOnce('Node#_value was deprecated. Use Node#raws.value'); 24 | return this.raws.value; 25 | } 26 | 27 | set _value(val) { 28 | warnOnce('Node#_value was deprecated. Use Node#raws.value'); 29 | this.raws.value = val; 30 | } 31 | 32 | get _important() { 33 | warnOnce('Node#_important was deprecated. Use Node#raws.important'); 34 | return this.raws.important; 35 | } 36 | 37 | set _important(val) { 38 | warnOnce('Node#_important was deprecated. Use Node#raws.important'); 39 | this.raws.important = val; 40 | } 41 | 42 | /** 43 | * @memberof Declaration# 44 | * @member {string} prop - the declaration’s property name 45 | * 46 | * @example 47 | * const root = postcss.parse('a { color: black }'); 48 | * const decl = root.first.first; 49 | * decl.prop //=> 'color' 50 | */ 51 | 52 | /** 53 | * @memberof Declaration# 54 | * @member {string} value - the declaration’s value 55 | * 56 | * @example 57 | * const root = postcss.parse('a { color: black }'); 58 | * const decl = root.first.first; 59 | * decl.value //=> 'black' 60 | */ 61 | 62 | /** 63 | * @memberof Declaration# 64 | * @member {boolean} important - `true` if the declaration 65 | * has an !important annotation. 66 | * 67 | * @example 68 | * const root = postcss.parse('a { color: black !important; color: red }'); 69 | * root.first.first.important //=> true 70 | * root.first.last.important //=> undefined 71 | */ 72 | 73 | /** 74 | * @memberof Declaration# 75 | * @member {object} raws - Information to generate byte-to-byte equal 76 | * node string as it was in the origin input. 77 | * 78 | * Every parser saves its own properties, 79 | * but the default CSS parser uses: 80 | * 81 | * * `before`: the space symbols before the node. It also stores `*` 82 | * and `_` symbols before the declaration (IE hack). 83 | * * `between`: the symbols between the property and value 84 | * for declarations, selector and `{` for rules, or last parameter 85 | * and `{` for at-rules. 86 | * * `important`: the content of the important statement, 87 | * if it is not just `!important`. 88 | * 89 | * PostCSS cleans declaration from comments and extra spaces, 90 | * but it stores origin content in raws properties. 91 | * As such, if you don’t change a declaration’s value, 92 | * PostCSS will use the raw value with comments. 93 | * 94 | * @example 95 | * const root = postcss.parse('a {\n color:black\n}') 96 | * root.first.first.raws //=> { before: '\n ', between: ':' } 97 | */ 98 | 99 | } 100 | 101 | export default Declaration; 102 | -------------------------------------------------------------------------------- /lib/rule.es6: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import warnOnce from './warn-once'; 3 | import list from './list'; 4 | 5 | /** 6 | * Represents a CSS rule: a selector followed by a declaration block. 7 | * 8 | * @extends Container 9 | * 10 | * @example 11 | * const root = postcss.parse('a{}'); 12 | * const rule = root.first; 13 | * rule.type //=> 'rule' 14 | * rule.toString() //=> 'a{}' 15 | */ 16 | class Rule extends Container { 17 | 18 | constructor(defaults) { 19 | super(defaults); 20 | this.type = 'rule'; 21 | if ( !this.nodes ) this.nodes = []; 22 | } 23 | 24 | /** 25 | * An array containing the rule’s individual selectors. 26 | * Groups of selectors are split at commas. 27 | * 28 | * @type {string[]} 29 | * 30 | * @example 31 | * const root = postcss.parse('a, b { }'); 32 | * const rule = root.first; 33 | * 34 | * rule.selector //=> 'a, b' 35 | * rule.selectors //=> ['a', 'b'] 36 | * 37 | * rule.selectors = ['a', 'strong']; 38 | * rule.selector //=> 'a, strong' 39 | */ 40 | get selectors() { 41 | return list.comma(this.selector); 42 | } 43 | 44 | set selectors(values) { 45 | let match = this.selector ? this.selector.match(/,\s*/) : null; 46 | let sep = match ? match[0] : ',' + this.raw('between', 'beforeOpen'); 47 | this.selector = values.join(sep); 48 | } 49 | 50 | get _selector() { 51 | warnOnce('Rule#_selector is deprecated. Use Rule#raws.selector'); 52 | return this.raws.selector; 53 | } 54 | 55 | set _selector(val) { 56 | warnOnce('Rule#_selector is deprecated. Use Rule#raws.selector'); 57 | this.raws.selector = val; 58 | } 59 | 60 | /** 61 | * @memberof Rule# 62 | * @member {string} selector - the rule’s full selector represented 63 | * as a string 64 | * 65 | * @example 66 | * const root = postcss.parse('a, b { }'); 67 | * const rule = root.first; 68 | * rule.selector //=> 'a, b' 69 | */ 70 | 71 | /** 72 | * @memberof Rule# 73 | * @member {object} raws - Information to generate byte-to-byte equal 74 | * node string as it was in the origin input. 75 | * 76 | * Every parser saves its own properties, 77 | * but the default CSS parser uses: 78 | * 79 | * * `before`: the space symbols before the node. It also stores `*` 80 | * and `_` symbols before the declaration (IE hack). 81 | * * `after`: the space symbols after the last child of the node 82 | * to the end of the node. 83 | * * `between`: the symbols between the property and value 84 | * for declarations, selector and `{` for rules, or last parameter 85 | * and `{` for at-rules. 86 | * * `semicolon`: contains true if the last child has 87 | * an (optional) semicolon. 88 | * 89 | * PostCSS cleans selectors from comments and extra spaces, 90 | * but it stores origin content in raws properties. 91 | * As such, if you don’t change a declaration’s value, 92 | * PostCSS will use the raw value with comments. 93 | * 94 | * @example 95 | * const root = postcss.parse('a {\n color:black\n}') 96 | * root.first.first.raws //=> { before: '', between: ' ', after: '\n' } 97 | */ 98 | 99 | } 100 | 101 | export default Rule; 102 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | 3 | gulp.task('clean', () => { 4 | let del = require('del'); 5 | return del(['lib/*.js', 'postcss.js', 'build/', 'api/']); 6 | }); 7 | 8 | // Build 9 | 10 | gulp.task('compile', ['clean'], () => { 11 | let sourcemaps = require('gulp-sourcemaps'); 12 | let babel = require('gulp-babel'); 13 | return gulp.src('lib/*.es6') 14 | .pipe(sourcemaps.init()) 15 | .pipe(babel()) 16 | .pipe(sourcemaps.write()) 17 | .pipe(gulp.dest('lib')); 18 | }); 19 | 20 | gulp.task('build:lib', ['compile'], () => { 21 | return gulp.src('lib/*.js').pipe(gulp.dest('build/lib')); 22 | }); 23 | 24 | gulp.task('build:docs', ['clean'], () => { 25 | let ignore = require('fs').readFileSync('.npmignore').toString() 26 | .trim().split(/\n+/) 27 | .concat(['.npmignore', 'index.js', 'lib/*', 'test/*', 28 | 'node_modules/**/*', 'docs/api.md', 'docs/plugins.md', 29 | 'docs/writing-a-plugin.md']) 30 | .map( i => '!' + i ); 31 | return gulp.src(['**/*'].concat(ignore)) 32 | .pipe(gulp.dest('build')); 33 | }); 34 | 35 | gulp.task('build', ['build:lib', 'build:docs']); 36 | 37 | // Lint 38 | 39 | gulp.task('lint', () => { 40 | if ( parseInt(process.versions.node) < 4 ) { 41 | return; 42 | } 43 | let eslint = require('gulp-eslint'); 44 | return gulp.src(['*.js', 'lib/*.es6', 'test/*.js']) 45 | .pipe(eslint()) 46 | .pipe(eslint.format()) 47 | .pipe(eslint.failAfterError()); 48 | }); 49 | 50 | gulp.task('spellcheck', () => { 51 | let run = require('gulp-run'); 52 | return gulp.src(['*.md', 'docs/**/*.md'], { read: false }) 53 | .pipe(run('yaspeller <%= file.path %>')); 54 | }); 55 | 56 | // Tests 57 | 58 | gulp.task('test', ['compile'], () => { 59 | let ava = require('gulp-ava'); 60 | return gulp.src('test/*.js', { read: false }).pipe(ava()); 61 | }); 62 | 63 | gulp.task('integration', ['build:lib'], done => { 64 | let postcss = require('./build/lib/postcss'); 65 | let real = require('postcss-parser-tests/real'); 66 | real(done, css => { 67 | return postcss.parse(css).toResult({ map: { annotation: false } }); 68 | }); 69 | }); 70 | 71 | gulp.task('version', ['build:lib'], () => { 72 | let Processor = require('./lib/processor'); 73 | let instance = new Processor(); 74 | let pkg = require('./package'); 75 | if ( pkg.version !== instance.version ) { 76 | throw new Error('Version in Processor is not equal to package.json'); 77 | } 78 | }); 79 | 80 | // Docs 81 | 82 | gulp.task('api', ['clean'], done => { 83 | let jsdoc = require('gulp-jsdoc3'); 84 | gulp.src('lib/*.es6', { read: false }) 85 | .pipe(jsdoc({ 86 | source: { 87 | includePattern: '.+\\.es6$' 88 | }, 89 | opts: { 90 | template: 'node_modules/docdash', 91 | destination: './api/' 92 | }, 93 | plugins: ['plugins/markdown'], 94 | templates: { 95 | default: { 96 | layoutFile: 'node_modules/docdash/tmpl/layout.tmpl' 97 | } 98 | } 99 | }, done)); 100 | }); 101 | 102 | // Common 103 | 104 | gulp.task('offline', ['version', 'lint', 'test', 'api']); 105 | 106 | gulp.task('default', ['offline', 'spellcheck', 'integration']); 107 | -------------------------------------------------------------------------------- /lib/warning.es6: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a plugin’s warning. It can be created using {@link Node#warn}. 3 | * 4 | * @example 5 | * if ( decl.important ) { 6 | * decl.warn(result, 'Avoid !important', { word: '!important' }); 7 | * } 8 | */ 9 | class Warning { 10 | 11 | /** 12 | * @param {string} text - warning message 13 | * @param {Object} [opts] - warning options 14 | * @param {Node} opts.node - CSS node that caused the warning 15 | * @param {string} opts.word - word in CSS source that caused the warning 16 | * @param {number} opts.index - index in CSS node string that caused 17 | * the warning 18 | * @param {string} opts.plugin - name of the plugin that created 19 | * this warning. {@link Result#warn} fills 20 | * this property automatically. 21 | */ 22 | constructor(text, opts = { }) { 23 | /** 24 | * @member {string} - Type to filter warnings from 25 | * {@link Result#messages}. Always equal 26 | * to `"warning"`. 27 | * 28 | * @example 29 | * const nonWarning = result.messages.filter(i => i.type !== 'warning') 30 | */ 31 | this.type = 'warning'; 32 | /** 33 | * @member {string} - The warning message. 34 | * 35 | * @example 36 | * warning.text //=> 'Try to avoid !important' 37 | */ 38 | this.text = text; 39 | 40 | if ( opts.node && opts.node.source ) { 41 | let pos = opts.node.positionBy(opts); 42 | /** 43 | * @member {number} - Line in the input file 44 | * with this warning’s source 45 | * 46 | * @example 47 | * warning.line //=> 5 48 | */ 49 | this.line = pos.line; 50 | /** 51 | * @member {number} - Column in the input file 52 | * with this warning’s source. 53 | * 54 | * @example 55 | * warning.column //=> 6 56 | */ 57 | this.column = pos.column; 58 | } 59 | 60 | for ( let opt in opts ) this[opt] = opts[opt]; 61 | } 62 | 63 | /** 64 | * Returns a warning position and message. 65 | * 66 | * @example 67 | * warning.toString() //=> 'postcss-lint:a.css:10:14: Avoid !important' 68 | * 69 | * @return {string} warning position and message 70 | */ 71 | toString() { 72 | if ( this.node ) { 73 | return this.node.error(this.text, { 74 | plugin: this.plugin, 75 | index: this.index, 76 | word: this.word 77 | }).message; 78 | } else if ( this.plugin ) { 79 | return this.plugin + ': ' + this.text; 80 | } else { 81 | return this.text; 82 | } 83 | } 84 | 85 | /** 86 | * @memberof Warning# 87 | * @member {string} plugin - The name of the plugin that created 88 | * it will fill this property automatically. 89 | * this warning. When you call {@link Node#warn} 90 | * 91 | * @example 92 | * warning.plugin //=> 'postcss-important' 93 | */ 94 | 95 | /** 96 | * @memberof Warning# 97 | * @member {Node} node - Contains the CSS node that caused the warning. 98 | * 99 | * @example 100 | * warning.node.toString() //=> 'color: white !important' 101 | */ 102 | 103 | } 104 | 105 | export default Warning; 106 | -------------------------------------------------------------------------------- /d.ts/lazy-result.d.ts: -------------------------------------------------------------------------------- 1 | import Processor from './processor'; 2 | import * as postcss from './postcss'; 3 | import Result from './result'; 4 | import Root from './root'; 5 | export default class LazyResult implements postcss.LazyResult { 6 | private stringified; 7 | private processed; 8 | private result; 9 | private error; 10 | private plugin; 11 | private processing; 12 | /** 13 | * A promise proxy for the result of PostCSS transformations. 14 | */ 15 | constructor(processor: Processor, 16 | /** 17 | * String with input CSS or any object with toString() method, like a Buffer. 18 | * Optionally, send Result instance and the processor will take the existing 19 | * [Root] parser from it. 20 | */ 21 | css: string | { 22 | toString(): string; 23 | } | LazyResult | Result, opts?: postcss.ProcessOptions); 24 | /** 25 | * @returns A processor used for CSS transformations. 26 | */ 27 | processor: Processor; 28 | /** 29 | * @returns Options from the Processor#process(css, opts) call that produced 30 | * this Result instance. 31 | */ 32 | opts: postcss.ResultOptions; 33 | /** 34 | * Processes input CSS through synchronous plugins and converts Root to a 35 | * CSS string. This property will only work with synchronous plugins. If 36 | * the processor contains any asynchronous plugins it will throw an error. 37 | * In this case, you should use LazyResult#then() instead. 38 | */ 39 | css: string; 40 | /** 41 | * Alias for css property to use when syntaxes generate non-CSS output. 42 | */ 43 | content: string; 44 | /** 45 | * Processes input CSS through synchronous plugins. This property will 46 | * only work with synchronous plugins. If the processor contains any 47 | * asynchronous plugins it will throw an error. In this case, you should 48 | * use LazyResult#then() instead. 49 | */ 50 | map: postcss.ResultMap; 51 | /** 52 | * Processes input CSS through synchronous plugins. This property will only 53 | * work with synchronous plugins. If the processor contains any asynchronous 54 | * plugins it will throw an error. In this case, you should use 55 | * LazyResult#then() instead. 56 | */ 57 | root: Root; 58 | /** 59 | * Processes input CSS through synchronous plugins. This property will only 60 | * work with synchronous plugins. If the processor contains any asynchronous 61 | * plugins it will throw an error. In this case, you should use 62 | * LazyResult#then() instead. 63 | */ 64 | messages: postcss.ResultMessage[]; 65 | /** 66 | * Processes input CSS through synchronous plugins and calls Result#warnings(). 67 | * This property will only work with synchronous plugins. If the processor 68 | * contains any asynchronous plugins it will throw an error. In this case, you 69 | * You should use LazyResult#then() instead. 70 | */ 71 | warnings(): postcss.ResultMessage[]; 72 | /** 73 | * Alias for css property. 74 | */ 75 | toString(): string; 76 | /** 77 | * Processes input CSS through synchronous and asynchronous plugins. 78 | * @param onRejected Called if any plugin throws an error. 79 | */ 80 | then(onFulfilled: (result: Result) => void, onRejected?: (error: Error) => void): Function | any; 81 | /** 82 | * Processes input CSS through synchronous and asynchronous plugins. 83 | * @param onRejected Called if any plugin throws an error. 84 | */ 85 | catch(onRejected: (error: Error) => void): Function | any; 86 | private handleError(error, plugin); 87 | private asyncTick(resolve, reject); 88 | private async(); 89 | sync(): Result; 90 | private run(plugin); 91 | stringify(): Result; 92 | } 93 | -------------------------------------------------------------------------------- /lib/at-rule.es6: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import warnOnce from './warn-once'; 3 | 4 | /** 5 | * Represents an at-rule. 6 | * 7 | * If it’s followed in the CSS by a {} block, this node will have 8 | * a nodes property representing its children. 9 | * 10 | * @extends Container 11 | * 12 | * @example 13 | * const root = postcss.parse('@charset "UTF-8"; @media print {}'); 14 | * 15 | * const charset = root.first; 16 | * charset.type //=> 'atrule' 17 | * charset.nodes //=> undefined 18 | * 19 | * const media = root.last; 20 | * media.nodes //=> [] 21 | */ 22 | class AtRule extends Container { 23 | 24 | constructor(defaults) { 25 | super(defaults); 26 | this.type = 'atrule'; 27 | } 28 | 29 | append(...children) { 30 | if ( !this.nodes ) this.nodes = []; 31 | return super.append(...children); 32 | } 33 | 34 | prepend(...children) { 35 | if ( !this.nodes ) this.nodes = []; 36 | return super.prepend(...children); 37 | } 38 | 39 | get afterName() { 40 | warnOnce('AtRule#afterName was deprecated. Use AtRule#raws.afterName'); 41 | return this.raws.afterName; 42 | } 43 | 44 | set afterName(val) { 45 | warnOnce('AtRule#afterName was deprecated. Use AtRule#raws.afterName'); 46 | this.raws.afterName = val; 47 | } 48 | 49 | get _params() { 50 | warnOnce('AtRule#_params was deprecated. Use AtRule#raws.params'); 51 | return this.raws.params; 52 | } 53 | 54 | set _params(val) { 55 | warnOnce('AtRule#_params was deprecated. Use AtRule#raws.params'); 56 | this.raws.params = val; 57 | } 58 | 59 | /** 60 | * @memberof AtRule# 61 | * @member {string} name - the at-rule’s name immediately follows the `@` 62 | * 63 | * @example 64 | * const root = postcss.parse('@media print {}'); 65 | * media.name //=> 'media' 66 | * const media = root.first; 67 | */ 68 | 69 | /** 70 | * @memberof AtRule# 71 | * @member {string} params - the at-rule’s parameters, the values 72 | * that follow the at-rule’s name but precede 73 | * any {} block 74 | * 75 | * @example 76 | * const root = postcss.parse('@media print, screen {}'); 77 | * const media = root.first; 78 | * media.params //=> 'print, screen' 79 | */ 80 | 81 | /** 82 | * @memberof AtRule# 83 | * @member {object} raws - Information to generate byte-to-byte equal 84 | * node string as it was in the origin input. 85 | * 86 | * Every parser saves its own properties, 87 | * but the default CSS parser uses: 88 | * 89 | * * `before`: the space symbols before the node. It also stores `*` 90 | * and `_` symbols before the declaration (IE hack). 91 | * * `after`: the space symbols after the last child of the node 92 | * to the end of the node. 93 | * * `between`: the symbols between the property and value 94 | * for declarations, selector and `{` for rules, or last parameter 95 | * and `{` for at-rules. 96 | * * `semicolon`: contains true if the last child has 97 | * an (optional) semicolon. 98 | * * `afterName`: the space between the at-rule name and its parameters. 99 | * 100 | * PostCSS cleans at-rule parameters from comments and extra spaces, 101 | * but it stores origin content in raws properties. 102 | * As such, if you don’t change a declaration’s value, 103 | * PostCSS will use the raw value with comments. 104 | * 105 | * @example 106 | * const root = postcss.parse(' @media\nprint {\n}') 107 | * root.first.first.raws //=> { before: ' ', 108 | * // between: ' ', 109 | * // afterName: '\n', 110 | * // after: '\n' } 111 | */ 112 | } 113 | 114 | export default AtRule; 115 | -------------------------------------------------------------------------------- /test/parse.js: -------------------------------------------------------------------------------- 1 | import parse from '../lib/parse'; 2 | import Root from '../lib/root'; 3 | 4 | import cases from 'postcss-parser-tests'; 5 | import path from 'path'; 6 | import test from 'ava'; 7 | import fs from 'fs'; 8 | 9 | test('works with file reads', t => { 10 | let stream = fs.readFileSync(cases.path('atrule-empty.css')); 11 | t.truthy(parse(stream) instanceof Root); 12 | }); 13 | 14 | cases.each( (name, css, json) => { 15 | test('parses ' + name, t => { 16 | let parsed = cases.jsonify(parse(css, { from: name })); 17 | t.deepEqual(parsed, json); 18 | }); 19 | }); 20 | 21 | test('saves source file', t => { 22 | let css = parse('a {}', { from: 'a.css' }); 23 | t.deepEqual(css.first.source.input.css, 'a {}'); 24 | t.deepEqual(css.first.source.input.file, path.resolve('a.css')); 25 | t.deepEqual(css.first.source.input.from, path.resolve('a.css')); 26 | }); 27 | 28 | test('keeps absolute path in source', t => { 29 | let css = parse('a {}', { from: 'http://example.com/a.css' }); 30 | t.deepEqual(css.first.source.input.file, 'http://example.com/a.css'); 31 | t.deepEqual(css.first.source.input.from, 'http://example.com/a.css'); 32 | }); 33 | 34 | test('saves source file on previous map', t => { 35 | let root1 = parse('a {}', { map: { inline: true } }); 36 | let css = root1.toResult({ map: { inline: true } }).css; 37 | let root2 = parse(css); 38 | t.deepEqual(root2.first.source.input.file, path.resolve('to.css')); 39 | }); 40 | 41 | test('sets unique ID for file without name', t => { 42 | let css1 = parse('a {}'); 43 | let css2 = parse('a {}'); 44 | t.regex(css1.first.source.input.id, /^$/); 45 | t.regex(css1.first.source.input.from, /^$/); 46 | t.notDeepEqual(css2.first.source.input.id, css1.first.source.input.id); 47 | }); 48 | 49 | test('sets parent node', t => { 50 | let file = cases.path('atrule-rules.css'); 51 | let css = parse(fs.readFileSync(file)); 52 | 53 | let support = css.first; 54 | let keyframes = support.first; 55 | let from = keyframes.first; 56 | let decl = from.first; 57 | 58 | t.is(decl.parent, from); 59 | t.is(from.parent, keyframes); 60 | t.is(support.parent, css); 61 | t.is(keyframes.parent, support); 62 | }); 63 | 64 | test('ignores wrong close bracket', t => { 65 | let root = parse('a { p: ()) }'); 66 | t.deepEqual(root.first.first.value, '())'); 67 | }); 68 | 69 | test('ignores symbols before declaration', t => { 70 | let root = parse('a { :one: 1 }'); 71 | t.deepEqual(root.first.first.raws.before, ' :'); 72 | }); 73 | 74 | test('throws on unclosed blocks', t => { 75 | t.throws(() => { 76 | parse('\na {\n'); 77 | }, /:2:1: Unclosed block/); 78 | }); 79 | 80 | test('throws on unnecessary block close', t => { 81 | t.throws(() => { 82 | parse('a {\n} }'); 83 | }, /:2:3: Unexpected }/); 84 | }); 85 | 86 | test('throws on unclosed comment', t => { 87 | t.throws(() => { 88 | parse('\n/*\n '); 89 | }, /:2:1: Unclosed comment/); 90 | }); 91 | 92 | test('throws on unclosed quote', t => { 93 | t.throws(() => { 94 | parse('\n"\n\na '); 95 | }, /:2:1: Unclosed quote/); 96 | }); 97 | 98 | test('throws on unclosed bracket', t => { 99 | t.throws(() => { 100 | parse(':not(one() { }'); 101 | }, /:1:5: Unclosed bracket/); 102 | }); 103 | 104 | test('throws on property without value', t => { 105 | t.throws(() => { 106 | parse('a { b;}'); 107 | }, /:1:5: Unknown word/); 108 | t.throws(() => { 109 | parse('a { b b }'); 110 | }, /:1:5: Unknown word/); 111 | }); 112 | 113 | test('throws on nameless at-rule', t => { 114 | t.throws(() => { 115 | parse('@'); 116 | }, /:1:1: At-rule without name/); 117 | }); 118 | 119 | test('throws on property without semicolon', t => { 120 | t.throws(() => { 121 | parse('a { one: filter(a:"") two: 2 }'); 122 | }, /:1:21: Missed semicolon/); 123 | }); 124 | 125 | test('throws on double colon', t => { 126 | t.throws(() => { 127 | parse('a { one:: 1 }'); 128 | }, /:1:9: Double colon/); 129 | }); 130 | -------------------------------------------------------------------------------- /test/postcss.js: -------------------------------------------------------------------------------- 1 | import Processor from '../lib/processor'; 2 | import postcss from '../lib/postcss'; 3 | 4 | import test from 'ava'; 5 | 6 | test('creates plugins list', t => { 7 | let processor = postcss(); 8 | t.truthy(processor instanceof Processor); 9 | t.deepEqual(processor.plugins, []); 10 | }); 11 | 12 | test('saves plugins list', t => { 13 | let a = () => 1; 14 | let b = () => 2; 15 | t.deepEqual(postcss(a, b).plugins, [a, b]); 16 | }); 17 | 18 | test('saves plugins list as array', t => { 19 | let a = () => 1; 20 | let b = () => 2; 21 | t.deepEqual(postcss([a, b]).plugins, [a, b]); 22 | }); 23 | 24 | test('takes plugin from other processor', t => { 25 | let a = () => 1; 26 | let b = () => 2; 27 | let c = () => 3; 28 | let other = postcss([a, b]); 29 | t.deepEqual(postcss([other, c]).plugins, [a, b, c]); 30 | }); 31 | 32 | test('supports injecting additional processors at runtime', t => { 33 | let plugin1 = postcss.plugin('one', () => { 34 | return css => { 35 | css.walkDecls(decl => { 36 | decl.value = 'world'; 37 | }); 38 | }; 39 | }); 40 | let plugin2 = postcss.plugin('two', () => { 41 | return (css, result) => { 42 | result.processor.use(plugin1()); 43 | }; 44 | }); 45 | 46 | return postcss([ plugin2 ]).process('a{hello: bob}').then(result => { 47 | t.deepEqual(result.css, 'a{hello: world}'); 48 | }); 49 | }); 50 | 51 | test('creates plugin', t => { 52 | let plugin = postcss.plugin('test', filter => { 53 | return function (css) { 54 | css.walkDecls(filter || 'two', i => i.remove() ); 55 | }; 56 | }); 57 | 58 | let func1 = postcss(plugin).plugins[0]; 59 | t.deepEqual(func1.postcssPlugin, 'test'); 60 | t.regex(func1.postcssVersion, /\d+.\d+.\d+/); 61 | 62 | let func2 = postcss(plugin()).plugins[0]; 63 | t.deepEqual(func2.postcssPlugin, func1.postcssPlugin); 64 | t.deepEqual(func2.postcssVersion, func1.postcssVersion); 65 | 66 | let result1 = postcss(plugin('one')).process('a{ one: 1; two: 2 }'); 67 | t.deepEqual(result1.css, 'a{ two: 2 }'); 68 | 69 | let result2 = postcss(plugin).process('a{ one: 1; two: 2 }'); 70 | t.deepEqual(result2.css, 'a{ one: 1 }'); 71 | }); 72 | 73 | test('does not call plugin constructor', t => { 74 | let calls = 0; 75 | let plugin = postcss.plugin('test', () => { 76 | calls += 1; 77 | return function () { }; 78 | }); 79 | t.is(calls, 0); 80 | 81 | postcss(plugin).process('a{}'); 82 | t.is(calls, 1); 83 | 84 | postcss(plugin()).process('a{}'); 85 | t.is(calls, 2); 86 | }); 87 | 88 | test('creates a shortcut to process css', t => { 89 | let plugin = postcss.plugin('test', (str = 'bar') => { 90 | return function (css) { 91 | css.walkDecls(i => { 92 | i.value = str; 93 | }); 94 | }; 95 | }); 96 | 97 | let result1 = plugin.process('a{value:foo}'); 98 | t.deepEqual(result1.css, 'a{value:bar}'); 99 | 100 | let result2 = plugin.process('a{value:foo}', 'baz'); 101 | t.deepEqual(result2.css, 'a{value:baz}'); 102 | 103 | plugin.process('a{value:foo}').then( result => { 104 | t.deepEqual(result.css, 'a{value:bar}'); 105 | }); 106 | }); 107 | 108 | test('contains parser', t => { 109 | t.deepEqual(postcss.parse('').type, 'root'); 110 | }); 111 | 112 | test('contains stringifier', t => { 113 | t.deepEqual(typeof postcss.stringify, 'function'); 114 | }); 115 | 116 | test('allows to build own CSS', t => { 117 | let root = postcss.root({ raws: { after: '\n' } }); 118 | let comment = postcss.comment({ text: 'Example' }); 119 | let media = postcss.atRule({ name: 'media', params: 'screen' }); 120 | let rule = postcss.rule({ selector: 'a' }); 121 | let decl = postcss.decl({ prop: 'color', value: 'black' }); 122 | 123 | root.append(comment); 124 | rule.append(decl); 125 | media.append(rule); 126 | root.append(media); 127 | 128 | t.deepEqual(root.toString(), '/* Example */\n' + 129 | '@media screen {\n' + 130 | ' a {\n' + 131 | ' color: black\n' + 132 | ' }\n' + 133 | '}\n'); 134 | }); 135 | 136 | test('contains vendor module', t => { 137 | t.deepEqual(postcss.vendor.prefix('-moz-tab'), '-moz-'); 138 | }); 139 | 140 | test('contains list module', t => { 141 | t.deepEqual(postcss.list.space('a b'), ['a', 'b']); 142 | }); 143 | -------------------------------------------------------------------------------- /docs/guidelines/runner.md: -------------------------------------------------------------------------------- 1 | # PostCSS Runner Guidelines 2 | 3 | A PostCSS runner is a tool that processes CSS through a user-defined list 4 | of plugins; for example, [`postcss-cli`] or [`gulp‑postcss`]. 5 | These rules are mandatory for any such runners. 6 | 7 | For single-plugin tools, like [`gulp-autoprefixer`], 8 | these rules are not mandatory but are highly recommended. 9 | 10 | See also [ClojureWerkz’s recommendations] for open source projects. 11 | 12 | [ClojureWerkz’s recommendations]: http://blog.clojurewerkz.org/blog/2013/04/20/how-to-make-your-open-source-project-really-awesome/ 13 | [`gulp-autoprefixer`]: https://github.com/sindresorhus/gulp-autoprefixer 14 | [`gulp‑postcss`]: https://github.com/w0rm/gulp-postcss 15 | [`postcss-cli`]: https://github.com/postcss/postcss-cli 16 | 17 | ## 1. API 18 | 19 | ### 1.1. Accept functions in plugin parameters 20 | 21 | If your runner uses a config file, it must be written in JavaScript, so that 22 | it can support plugins which accept a function, such as [`postcss-assets`]: 23 | 24 | ```js 25 | module.exports = [ 26 | require('postcss-assets')({ 27 | cachebuster: function (file) { 28 | return fs.statSync(file).mtime.getTime().toString(16); 29 | } 30 | }) 31 | ]; 32 | ``` 33 | 34 | [`postcss-assets`]: https://github.com/borodean/postcss-assets 35 | 36 | ## 2. Processing 37 | 38 | ### 2.1. Set `from` and `to` processing options 39 | 40 | To ensure that PostCSS generates source maps and displays better syntax errors, 41 | runners must specify the `from` and `to` options. If your runner does not handle 42 | writing to disk (for example, a gulp transform), you should set both options 43 | to point to the same file: 44 | 45 | ```js 46 | processor.process({ from: file.path, to: file.path }); 47 | ``` 48 | 49 | ### 2.2. Use only the asynchronous API 50 | 51 | PostCSS runners must use only the asynchronous API. 52 | The synchronous API is provided only for debugging, is slower, 53 | and can’t work with asynchronous plugins. 54 | 55 | ```js 56 | processor.process(opts).then(function (result) { 57 | // processing is finished 58 | }); 59 | ``` 60 | 61 | ### 2.3. Use only the public PostCSS API 62 | 63 | PostCSS runners must not rely on undocumented properties or methods, 64 | which may be subject to change in any minor release. The public API 65 | is described in [API docs]. 66 | 67 | [API docs]: http://api.postcss.org/ 68 | 69 | ## 3. Output 70 | 71 | ### 3.1. Don’t show JS stack for `CssSyntaxError` 72 | 73 | PostCSS runners must not show a stack trace for CSS syntax errors, 74 | as the runner can be used by developers who are not familiar with JavaScript. 75 | Instead, handle such errors gracefully: 76 | 77 | ```js 78 | processor.process(opts).catch(function (error) { 79 | if ( error.name === 'CssSyntaxError' ) { 80 | process.stderr.write(error.message + error.showSourceCode()); 81 | } else { 82 | throw error; 83 | } 84 | }); 85 | ``` 86 | 87 | ### 3.2. Display `result.warnings()` 88 | 89 | PostCSS runners must output warnings from `result.warnings()`: 90 | 91 | ```js 92 | result.warnings().forEach(function (warn) { 93 | process.stderr.write(warn.toString()); 94 | }); 95 | ``` 96 | 97 | See also [postcss-log-warnings] and [postcss-messages] plugins. 98 | 99 | [postcss-log-warnings]: https://github.com/davidtheclark/postcss-log-warnings 100 | [postcss-messages]: https://github.com/postcss/postcss-messages 101 | 102 | ### 3.3. Allow the user to write source maps to different files 103 | 104 | PostCSS by default will inline source maps in the generated file; however, 105 | PostCSS runners must provide an option to save the source map in a different 106 | file: 107 | 108 | ```js 109 | if ( result.map ) { 110 | fs.writeFile(opts.to + '.map', result.map.toString()); 111 | } 112 | ``` 113 | 114 | ## 4. Documentation 115 | 116 | ### 4.1. Document your runner in English 117 | 118 | PostCSS runners must have their `README.md` written in English. Do not be afraid 119 | of your English skills, as the open source community will fix your errors. 120 | 121 | Of course, you are welcome to write documentation in other languages; 122 | just name them appropriately (e.g. `README.ja.md`). 123 | 124 | ### 4.2. Maintain a changelog 125 | 126 | PostCSS runners must describe changes of all releases in a separate file, 127 | such as `ChangeLog.md`, `History.md`, or with [GitHub Releases]. 128 | Visit [Keep A Changelog] for more information on how to write one of these. 129 | 130 | Of course you should use [SemVer]. 131 | 132 | [Keep A Changelog]: http://keepachangelog.com/ 133 | [GitHub Releases]: https://help.github.com/articles/creating-releases/ 134 | [SemVer]: http://semver.org/ 135 | 136 | ### 4.3. `postcss-runner` keyword in `package.json` 137 | 138 | PostCSS runners written for npm must have the `postcss-runner` keyword 139 | in their `package.json`. This special keyword will be useful for feedback about 140 | the PostCSS ecosystem. 141 | 142 | For packages not published to npm, this is not mandatory, but recommended 143 | if the package format is allowed to contain keywords. 144 | -------------------------------------------------------------------------------- /test/css-syntax-error.js: -------------------------------------------------------------------------------- 1 | import CssSyntaxError from '../lib/css-syntax-error'; 2 | import postcss from '../lib/postcss'; 3 | 4 | import stripAnsi from 'strip-ansi'; 5 | import Concat from 'concat-with-sourcemaps'; 6 | import path from 'path'; 7 | import test from 'ava'; 8 | 9 | function parseError(css, opts) { 10 | let error; 11 | try { 12 | postcss.parse(css, opts); 13 | } catch (e) { 14 | if ( e.name === 'CssSyntaxError' ) { 15 | error = e; 16 | } else { 17 | throw e; 18 | } 19 | } 20 | return error; 21 | } 22 | 23 | test('saves source', t => { 24 | let error = parseError('a {\n content: "\n}'); 25 | 26 | t.truthy(error instanceof CssSyntaxError); 27 | t.deepEqual(error.name, 'CssSyntaxError'); 28 | t.deepEqual(error.message, ':2:12: Unclosed quote'); 29 | t.deepEqual(error.reason, 'Unclosed quote'); 30 | t.deepEqual(error.line, 2); 31 | t.deepEqual(error.column, 12); 32 | t.deepEqual(error.source, 'a {\n content: "\n}'); 33 | 34 | t.deepEqual(error.input, { 35 | line: error.line, 36 | column: error.column, 37 | source: error.source 38 | }); 39 | }); 40 | 41 | test('has stack trace', t => { 42 | t.regex(parseError('a {\n content: "\n}').stack, /css-syntax-error\.js/); 43 | }); 44 | 45 | test('highlights broken line', t => { 46 | t.deepEqual(parseError('a {\n content: "\n}').showSourceCode(true), 47 | '\n' + 48 | 'a {\n' + 49 | ' content: "\n' + 50 | ' \u001b[1;31m^\u001b[0m\n' + 51 | '}'); 52 | }); 53 | 54 | test('highlights without colors on request', t => { 55 | t.deepEqual(parseError('a {').showSourceCode(false), 56 | '\n' + 57 | 'a {\n' + 58 | '^'); 59 | }); 60 | 61 | test('prints with highlight', t => { 62 | t.deepEqual(stripAnsi(parseError('a {').toString()), 63 | 'CssSyntaxError: :1:1: Unclosed block\n' + 64 | 'a {\n' + 65 | '^'); 66 | }); 67 | 68 | test('misses highlights without source content', t => { 69 | let error = parseError('a {'); 70 | error.source = null; 71 | t.deepEqual(error.toString(), 72 | 'CssSyntaxError: :1:1: Unclosed block'); 73 | }); 74 | 75 | test('misses position without source', t => { 76 | let decl = postcss.decl({ prop: 'color', value: 'black' }); 77 | let error = decl.error('Test'); 78 | t.deepEqual(error.toString(), 'CssSyntaxError: : Test'); 79 | }); 80 | 81 | test('uses source map', t => { 82 | let concat = new Concat(true, 'all.css'); 83 | concat.add('a.css', 'a { }\n'); 84 | concat.add('b.css', '\nb {\n'); 85 | 86 | let error = parseError(concat.content, { 87 | from: 'build/all.css', 88 | map: { prev: concat.sourceMap } 89 | }); 90 | 91 | t.deepEqual(error.file, path.resolve('b.css')); 92 | t.deepEqual(error.line, 2); 93 | t.deepEqual(typeof error.source, 'undefined'); 94 | 95 | t.deepEqual(error.input, { 96 | file: path.resolve('build/all.css'), 97 | line: 3, 98 | column: 1, 99 | source: 'a { }\n\nb {\n' 100 | }); 101 | }); 102 | 103 | test('shows origin source', t => { 104 | let input = postcss().process('a{}', { 105 | from: '/a.css', 106 | to: '/b.css', 107 | map: { inline: false } 108 | }); 109 | let error = parseError('a{', { 110 | from: '/b.css', 111 | to: '/c.css', 112 | map: { prev: input.map } 113 | }); 114 | t.deepEqual(error.source, 'a{}'); 115 | }); 116 | 117 | test('does not uses wrong source map', t => { 118 | let error = parseError('a { }\nb {', { 119 | from: 'build/all.css', 120 | map: { 121 | prev: { 122 | version: 3, 123 | file: 'build/all.css', 124 | sources: ['a.css', 'b.css'], 125 | mappings: 'A' 126 | } 127 | } 128 | }); 129 | t.deepEqual(error.file, path.resolve('build/all.css')); 130 | }); 131 | 132 | test('set source plugin', t => { 133 | let error = postcss.parse('a{}').first.error('Error', { plugin: 'PL' }); 134 | t.deepEqual(error.plugin, 'PL'); 135 | t.regex(error.toString(), /^CssSyntaxError: PL: :1:1: Error/); 136 | }); 137 | 138 | test('set source plugin automatically', t => { 139 | let plugin = postcss.plugin('test-plugin', () => { 140 | return css => { 141 | throw css.first.error('Error'); 142 | }; 143 | }); 144 | 145 | return postcss([plugin]).process('a{}').catch( error => { 146 | if ( error.name !== 'CssSyntaxError' ) throw error; 147 | t.deepEqual(error.plugin, 'test-plugin'); 148 | t.regex(error.toString(), /test-plugin/); 149 | }); 150 | }); 151 | 152 | test('set plugin automatically in async', t => { 153 | let plugin = postcss.plugin('async-plugin', () => { 154 | return css => { 155 | return new Promise( (resolve, reject) => { 156 | reject(css.first.error('Error')); 157 | }); 158 | }; 159 | }); 160 | 161 | return postcss([plugin]).process('a{}').catch( error => { 162 | if ( error.name !== 'CssSyntaxError' ) throw error; 163 | t.deepEqual(error.plugin, 'async-plugin'); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /lib/previous-map.es6: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | import mozilla from 'source-map'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | 6 | /** 7 | * Source map information from input CSS. 8 | * For example, source map after Sass compiler. 9 | * 10 | * This class will automatically find source map in input CSS or in file system 11 | * near input file (according `from` option). 12 | * 13 | * @example 14 | * const root = postcss.parse(css, { from: 'a.sass.css' }); 15 | * root.input.map //=> PreviousMap 16 | */ 17 | class PreviousMap { 18 | 19 | /** 20 | * @param {string} css - input CSS source 21 | * @param {processOptions} [opts] - {@link Processor#process} options 22 | */ 23 | constructor(css, opts) { 24 | this.loadAnnotation(css); 25 | /** 26 | * @member {boolean} - Was source map inlined by data-uri to input CSS. 27 | */ 28 | this.inline = this.startWith(this.annotation, 'data:'); 29 | 30 | let prev = opts.map ? opts.map.prev : undefined; 31 | let text = this.loadMap(opts.from, prev); 32 | if ( text ) this.text = text; 33 | } 34 | 35 | /** 36 | * Create a instance of `SourceMapGenerator` class 37 | * from the `source-map` library to work with source map information. 38 | * 39 | * It is lazy method, so it will create object only on first call 40 | * and then it will use cache. 41 | * 42 | * @return {SourceMapGenerator} object woth source map information 43 | */ 44 | consumer() { 45 | if ( !this.consumerCache ) { 46 | this.consumerCache = new mozilla.SourceMapConsumer(this.text); 47 | } 48 | return this.consumerCache; 49 | } 50 | 51 | /** 52 | * Does source map contains `sourcesContent` with input source text. 53 | * 54 | * @return {boolean} Is `sourcesContent` present 55 | */ 56 | withContent() { 57 | return !!(this.consumer().sourcesContent && 58 | this.consumer().sourcesContent.length > 0); 59 | } 60 | 61 | startWith(string, start) { 62 | if ( !string ) return false; 63 | return string.substr(0, start.length) === start; 64 | } 65 | 66 | loadAnnotation(css) { 67 | let match = css.match(/\/\*\s*# sourceMappingURL=(.*)\s*\*\//); 68 | if ( match ) this.annotation = match[1].trim(); 69 | } 70 | 71 | decodeInline(text) { 72 | let utfd64 = 'data:application/json;charset=utf-8;base64,'; 73 | let utf64 = 'data:application/json;charset=utf8;base64,'; 74 | let b64 = 'data:application/json;base64,'; 75 | let uri = 'data:application/json,'; 76 | 77 | if ( this.startWith(text, uri) ) { 78 | return decodeURIComponent( text.substr(uri.length) ); 79 | 80 | } else if ( this.startWith(text, b64) ) { 81 | return Base64.decode( text.substr(b64.length) ); 82 | 83 | } else if ( this.startWith(text, utf64) ) { 84 | return Base64.decode( text.substr(utf64.length) ); 85 | 86 | } else if ( this.startWith(text, utfd64) ) { 87 | return Base64.decode( text.substr(utfd64.length) ); 88 | 89 | } else { 90 | let encoding = text.match(/data:application\/json;([^,]+),/)[1]; 91 | throw new Error('Unsupported source map encoding ' + encoding); 92 | } 93 | } 94 | 95 | loadMap(file, prev) { 96 | if ( prev === false ) return false; 97 | 98 | if ( prev ) { 99 | if ( typeof prev === 'string' ) { 100 | return prev; 101 | } else if ( typeof prev === 'function' ) { 102 | let prevPath = prev(file); 103 | if ( prevPath && fs.existsSync && fs.existsSync(prevPath) ) { 104 | return fs.readFileSync(prevPath, 'utf-8').toString().trim(); 105 | } else { 106 | throw new Error('Unable to load previous source map: ' + 107 | prevPath.toString()); 108 | } 109 | } else if ( prev instanceof mozilla.SourceMapConsumer ) { 110 | return mozilla.SourceMapGenerator 111 | .fromSourceMap(prev).toString(); 112 | } else if ( prev instanceof mozilla.SourceMapGenerator ) { 113 | return prev.toString(); 114 | } else if ( this.isMap(prev) ) { 115 | return JSON.stringify(prev); 116 | } else { 117 | throw new Error('Unsupported previous source map format: ' + 118 | prev.toString()); 119 | } 120 | 121 | } else if ( this.inline ) { 122 | return this.decodeInline(this.annotation); 123 | 124 | } else if ( this.annotation ) { 125 | let map = this.annotation; 126 | if ( file ) map = path.join(path.dirname(file), map); 127 | 128 | this.root = path.dirname(map); 129 | if ( fs.existsSync && fs.existsSync(map) ) { 130 | return fs.readFileSync(map, 'utf-8').toString().trim(); 131 | } else { 132 | return false; 133 | } 134 | } 135 | } 136 | 137 | isMap(map) { 138 | if ( typeof map !== 'object' ) return false; 139 | return typeof map.mappings === 'string' || 140 | typeof map._mappings === 'string'; 141 | } 142 | } 143 | 144 | export default PreviousMap; 145 | -------------------------------------------------------------------------------- /d.ts/css-syntax-error.d.ts: -------------------------------------------------------------------------------- 1 | import * as postcss from './postcss'; 2 | export default class CssSyntaxError implements postcss.CssSyntaxError, SyntaxError { 3 | /** 4 | * Contains full error text in the GNU error format. 5 | */ 6 | message: string; 7 | /** 8 | * Contains the source line of the error. PostCSS will use the input 9 | * source map to detect the original error location. If you wrote a 10 | * Sass file, compiled it to CSS and then parsed it with PostCSS, 11 | * PostCSS will show the original position in the Sass file. If you need 12 | * position in PostCSS input (e.g., to debug previous compiler), use 13 | * error.generated.line. 14 | */ 15 | line: number; 16 | /** 17 | * Contains the source column of the error. PostCSS will use the input 18 | * source map to detect the original error location. If you wrote a 19 | * Sass file, compiled it to CSS and then parsed it with PostCSS, 20 | * PostCSS will show the original position in the Sass file. If you 21 | * need position in PostCSS input (e.g., to debug previous compiler), 22 | * use error.generated.column. 23 | */ 24 | column: number; 25 | /** 26 | * Contains the source code of the broken file. PostCSS will use the 27 | * input source map to detect the original error location. If you wrote 28 | * a Sass file, compiled it to CSS and then parsed it with PostCSS, 29 | * PostCSS will show the original position in the Sass file. If you 30 | * need position in PostCSS input (e.g., to debug previous compiler), 31 | * use error.generated.source. 32 | */ 33 | source: string; 34 | /** 35 | * If parser's from option is set, contains the absolute path to the 36 | * broken file. PostCSS will use the input source map to detect the 37 | * original error location. If you wrote a Sass file, compiled it 38 | * to CSS and then parsed it with PostCSS, PostCSS will show the 39 | * original position in the Sass file. If you need the position in 40 | * PostCSS input (e.g., to debug previous compiler), use 41 | * error.generated.file. 42 | */ 43 | file: string; 44 | /** 45 | * Contains the PostCSS plugin name if the error didn't come from the 46 | * CSS parser. 47 | */ 48 | plugin: string; 49 | name: string; 50 | /** 51 | * Contains only the error description. 52 | */ 53 | reason: string; 54 | private columnNumber; 55 | private description; 56 | private lineNumber; 57 | private fileName; 58 | input: postcss.InputOrigin; 59 | /** 60 | * The CSS parser throws this error for broken CSS. 61 | */ 62 | constructor( 63 | /** 64 | * Contains full error text in the GNU error format. 65 | */ 66 | message: string, 67 | /** 68 | * Contains the source line of the error. PostCSS will use the input 69 | * source map to detect the original error location. If you wrote a 70 | * Sass file, compiled it to CSS and then parsed it with PostCSS, 71 | * PostCSS will show the original position in the Sass file. If you need 72 | * position in PostCSS input (e.g., to debug previous compiler), use 73 | * error.generated.line. 74 | */ 75 | line?: number, 76 | /** 77 | * Contains the source column of the error. PostCSS will use the input 78 | * source map to detect the original error location. If you wrote a 79 | * Sass file, compiled it to CSS and then parsed it with PostCSS, 80 | * PostCSS will show the original position in the Sass file. If you 81 | * need position in PostCSS input (e.g., to debug previous compiler), 82 | * use error.generated.column. 83 | */ 84 | column?: number, 85 | /** 86 | * Contains the source code of the broken file. PostCSS will use the 87 | * input source map to detect the original error location. If you wrote 88 | * a Sass file, compiled it to CSS and then parsed it with PostCSS, 89 | * PostCSS will show the original position in the Sass file. If you 90 | * need position in PostCSS input (e.g., to debug previous compiler), 91 | * use error.generated.source. 92 | */ 93 | source?: string, 94 | /** 95 | * If parser's from option is set, contains the absolute path to the 96 | * broken file. PostCSS will use the input source map to detect the 97 | * original error location. If you wrote a Sass file, compiled it 98 | * to CSS and then parsed it with PostCSS, PostCSS will show the 99 | * original position in the Sass file. If you need the position in 100 | * PostCSS input (e.g., to debug previous compiler), use 101 | * error.generated.file. 102 | */ 103 | file?: string, 104 | /** 105 | * Contains the PostCSS plugin name if the error didn't come from the 106 | * CSS parser. 107 | */ 108 | plugin?: string); 109 | private setMessage(); 110 | /** 111 | * @param color Whether arrow should be colored red by terminal color codes. 112 | * By default, PostCSS will use process.stdout.isTTY and 113 | * process.env.NODE_DISABLE_COLORS. 114 | * @returns A few lines of CSS source that caused the error. If CSS has 115 | * input source map without sourceContent this method will return an empty 116 | * string. 117 | */ 118 | showSourceCode(color?: boolean): string; 119 | /** 120 | * 121 | * @returns Error position, message and source code of broken part. 122 | */ 123 | toString(): string; 124 | generated: postcss.InputOrigin; 125 | } 126 | -------------------------------------------------------------------------------- /lib/input.es6: -------------------------------------------------------------------------------- 1 | import CssSyntaxError from './css-syntax-error'; 2 | import PreviousMap from './previous-map'; 3 | 4 | import path from 'path'; 5 | 6 | let sequence = 0; 7 | 8 | /** 9 | * @typedef {object} filePosition 10 | * @property {string} file - path to file 11 | * @property {number} line - source line in file 12 | * @property {number} column - source column in file 13 | */ 14 | 15 | /** 16 | * Represents the source CSS. 17 | * 18 | * @example 19 | * const root = postcss.parse(css, { from: file }); 20 | * const input = root.source.input; 21 | */ 22 | class Input { 23 | 24 | /** 25 | * @param {string} css - input CSS source 26 | * @param {object} [opts] - {@link Processor#process} options 27 | */ 28 | constructor(css, opts = { }) { 29 | /** 30 | * @member {string} - input CSS source 31 | * 32 | * @example 33 | * const input = postcss.parse('a{}', { from: file }).input; 34 | * input.css //=> "a{}"; 35 | */ 36 | this.css = css.toString(); 37 | 38 | if ( this.css[0] === '\uFEFF' || this.css[0] === '\uFFFE' ) { 39 | this.css = this.css.slice(1); 40 | } 41 | 42 | if ( opts.from ) { 43 | if ( /^\w+:\/\//.test(opts.from) ) { 44 | /** 45 | * @member {string} - The absolute path to the CSS source file 46 | * defined with the `from` option. 47 | * 48 | * @example 49 | * const root = postcss.parse(css, { from: 'a.css' }); 50 | * root.source.input.file //=> '/home/ai/a.css' 51 | */ 52 | this.file = opts.from; 53 | } else { 54 | this.file = path.resolve(opts.from); 55 | } 56 | } 57 | 58 | let map = new PreviousMap(this.css, opts); 59 | if ( map.text ) { 60 | /** 61 | * @member {PreviousMap} - The input source map passed from 62 | * a compilation step before PostCSS 63 | * (for example, from Sass compiler). 64 | * 65 | * @example 66 | * root.source.input.map.consumer().sources //=> ['a.sass'] 67 | */ 68 | this.map = map; 69 | let file = map.consumer().file; 70 | if ( !this.file && file ) this.file = this.mapResolve(file); 71 | } 72 | 73 | if ( !this.file ) { 74 | sequence += 1; 75 | /** 76 | * @member {string} - The unique ID of the CSS source. It will be 77 | * created if `from` option is not provided 78 | * (because PostCSS does not know the file path). 79 | * 80 | * @example 81 | * const root = postcss.parse(css); 82 | * root.source.input.file //=> undefined 83 | * root.source.input.id //=> "" 84 | */ 85 | this.id = ''; 86 | } 87 | if ( this.map ) this.map.file = this.from; 88 | } 89 | 90 | error(message, line, column, opts = { }) { 91 | let result; 92 | let origin = this.origin(line, column); 93 | if ( origin ) { 94 | result = new CssSyntaxError(message, origin.line, origin.column, 95 | origin.source, origin.file, opts.plugin); 96 | } else { 97 | result = new CssSyntaxError(message, line, column, 98 | this.css, this.file, opts.plugin); 99 | } 100 | 101 | result.input = { line, column, source: this.css }; 102 | if ( this.file ) result.input.file = this.file; 103 | 104 | return result; 105 | } 106 | 107 | /** 108 | * Reads the input source map and returns a symbol position 109 | * in the input source (e.g., in a Sass file that was compiled 110 | * to CSS before being passed to PostCSS). 111 | * 112 | * @param {number} line - line in input CSS 113 | * @param {number} column - column in input CSS 114 | * 115 | * @return {filePosition} position in input source 116 | * 117 | * @example 118 | * root.source.input.origin(1, 1) //=> { file: 'a.css', line: 3, column: 1 } 119 | */ 120 | origin(line, column) { 121 | if ( !this.map ) return false; 122 | let consumer = this.map.consumer(); 123 | 124 | let from = consumer.originalPositionFor({ line, column }); 125 | if ( !from.source ) return false; 126 | 127 | let result = { 128 | file: this.mapResolve(from.source), 129 | line: from.line, 130 | column: from.column 131 | }; 132 | 133 | let source = consumer.sourceContentFor(from.source); 134 | if ( source ) result.source = source; 135 | 136 | return result; 137 | } 138 | 139 | mapResolve(file) { 140 | if ( /^\w+:\/\//.test(file) ) { 141 | return file; 142 | } else { 143 | return path.resolve(this.map.consumer().sourceRoot || '.', file); 144 | } 145 | } 146 | 147 | /** 148 | * The CSS source identifier. Contains {@link Input#file} if the user 149 | * set the `from` option, or {@link Input#id} if they did not. 150 | * @type {string} 151 | * 152 | * @example 153 | * const root = postcss.parse(css, { from: 'a.css' }); 154 | * root.source.input.from //=> "/home/ai/a.css" 155 | * 156 | * const root = postcss.parse(css); 157 | * root.source.input.from //=> "" 158 | */ 159 | get from() { 160 | return this.file || this.id; 161 | } 162 | 163 | } 164 | 165 | export default Input; 166 | -------------------------------------------------------------------------------- /test/stringifier.js: -------------------------------------------------------------------------------- 1 | import Stringifier from '../lib/stringifier'; 2 | import Declaration from '../lib/declaration'; 3 | import AtRule from '../lib/at-rule'; 4 | import parse from '../lib/parse'; 5 | import Node from '../lib/node'; 6 | import Root from '../lib/root'; 7 | import Rule from '../lib/rule'; 8 | 9 | import test from 'ava'; 10 | 11 | let str; 12 | test.before( () => { 13 | str = new Stringifier(); 14 | }); 15 | 16 | test('creates trimmed/raw property', t => { 17 | let b = new Node({ one: 'trim' }); 18 | b.raws.one = { value: 'trim', raw: 'raw' }; 19 | t.deepEqual(str.rawValue(b, 'one'), 'raw'); 20 | 21 | b.one = 'trim1'; 22 | t.deepEqual(str.rawValue(b, 'one'), 'trim1'); 23 | }); 24 | 25 | test('works without rawValue magic', t => { 26 | let b = new Node(); 27 | b.one = '1'; 28 | t.deepEqual(b.one, '1'); 29 | t.deepEqual(str.rawValue(b, 'one'), '1'); 30 | }); 31 | 32 | test('uses node raw', t => { 33 | let rule = new Rule({ selector: 'a', raws: { between: '\n' } }); 34 | t.deepEqual(str.raw(rule, 'between', 'beforeOpen'), '\n'); 35 | }); 36 | 37 | test('hacks before for nodes without parent', t => { 38 | let rule = new Rule({ selector: 'a' }); 39 | t.deepEqual(str.raw(rule, 'before'), ''); 40 | }); 41 | 42 | test('hacks before for first node', t => { 43 | let root = new Root(); 44 | root.append(new Rule({ selector: 'a' })); 45 | t.deepEqual(str.raw(root.first, 'before'), ''); 46 | }); 47 | 48 | test('hacks before for first decl', t => { 49 | let decl = new Declaration({ prop: 'color', value: 'black' }); 50 | t.deepEqual(str.raw(decl, 'before'), ''); 51 | 52 | let rule = new Rule({ selector: 'a' }); 53 | rule.append(decl); 54 | t.deepEqual(str.raw(decl, 'before'), '\n '); 55 | }); 56 | 57 | test('detects after raw', t => { 58 | let root = new Root(); 59 | root.append({ selector: 'a', raws: { after: ' ' } }); 60 | root.first.append({ prop: 'color', value: 'black' }); 61 | root.append({ selector: 'a' }); 62 | t.deepEqual(str.raw(root.last, 'after'), ' '); 63 | }); 64 | 65 | test('uses defaults without parent', t => { 66 | let rule = new Rule({ selector: 'a' }); 67 | t.deepEqual(str.raw(rule, 'between', 'beforeOpen'), ' '); 68 | }); 69 | 70 | test('uses defaults for unique node', t => { 71 | let root = new Root(); 72 | root.append(new Rule({ selector: 'a' })); 73 | t.deepEqual(str.raw(root.first, 'between', 'beforeOpen'), ' '); 74 | }); 75 | 76 | test('clones raw from first node', t => { 77 | let root = new Root(); 78 | root.append( new Rule({ selector: 'a', raws: { between: '' } }) ); 79 | root.append( new Rule({ selector: 'b' }) ); 80 | 81 | t.deepEqual(str.raw(root.last, 'between', 'beforeOpen'), ''); 82 | }); 83 | 84 | test('indents by default', t => { 85 | let root = new Root(); 86 | root.append( new AtRule({ name: 'page' }) ); 87 | root.first.append( new Rule({ selector: 'a' }) ); 88 | root.first.first.append({ prop: 'color', value: 'black' }); 89 | 90 | t.deepEqual(root.toString(), '@page {\n' + 91 | ' a {\n' + 92 | ' color: black\n' + 93 | ' }\n' + 94 | '}'); 95 | }); 96 | 97 | test('clones style', t => { 98 | let compress = parse('@page{ a{ } }'); 99 | let spaces = parse('@page {\n a {\n }\n}'); 100 | 101 | compress.first.first.append({ prop: 'color', value: 'black' }); 102 | t.deepEqual(compress.toString(), '@page{ a{ color: black } }'); 103 | 104 | spaces.first.first.append({ prop: 'color', value: 'black' }); 105 | t.deepEqual(spaces.toString(), '@page {\n a {\n color: black\n }\n}'); 106 | }); 107 | 108 | test('clones indent', t => { 109 | let root = parse('a{\n}'); 110 | root.first.append({ text: 'a' }); 111 | root.first.append({ text: 'b', raws: { before: '\n\n ' } }); 112 | t.deepEqual(root.toString(), 'a{\n\n /* a */\n\n /* b */\n}'); 113 | }); 114 | 115 | test('clones declaration before for comment', t => { 116 | let root = parse('a{\n}'); 117 | root.first.append({ text: 'a' }); 118 | root.first.append({ 119 | prop: 'a', 120 | value: '1', 121 | raws: { before: '\n\n ' } 122 | }); 123 | t.deepEqual(root.toString(), 'a{\n\n /* a */\n\n a: 1\n}'); 124 | }); 125 | 126 | test('clones indent by types', t => { 127 | let css = parse('a {\n color: black\n}\n\nb {\n}'); 128 | css.append(new Rule({ selector: 'em' })); 129 | css.last.append({ prop: 'z-index', value: '1' }); 130 | 131 | t.deepEqual(css.last.raw('before'), '\n\n'); 132 | t.deepEqual(css.last.first.raw('before'), '\n '); 133 | }); 134 | 135 | test('clones indent by before and after', t => { 136 | let css = parse('@page{\n\n a{\n color: black}}'); 137 | css.first.append(new Rule({ selector: 'b' })); 138 | css.first.last.append({ prop: 'z-index', value: '1' }); 139 | 140 | t.deepEqual(css.first.last.raw('before'), '\n\n '); 141 | t.deepEqual(css.first.last.raw('after'), ''); 142 | }); 143 | 144 | test('clones semicolon only from rules with children', t => { 145 | let css = parse('a{}b{one:1;}'); 146 | t.truthy(str.raw(css.first, 'semicolon')); 147 | }); 148 | 149 | test('clones only spaces in before', t => { 150 | let css = parse('a{*one:1}'); 151 | css.first.append({ prop: 'two', value: '2' }); 152 | css.append({ name: 'keyframes', params: 'a' }); 153 | css.last.append({ selector: 'from' }); 154 | t.deepEqual(css.toString(), 'a{*one:1;two:2}\n@keyframes a{\nfrom{}}'); 155 | }); 156 | 157 | test('clones only spaces in between', t => { 158 | let css = parse('a{one/**/:1}'); 159 | css.first.append({ prop: 'two', value: '2' }); 160 | t.deepEqual(css.toString(), 'a{one/**/:1;two:2}'); 161 | }); 162 | 163 | test('uses optional raws.indent', t => { 164 | let rule = new Rule({ selector: 'a', raws: { indent: ' ' } }); 165 | rule.append({ prop: 'color', value: 'black' }); 166 | t.deepEqual(rule.toString(), 'a {\n color: black\n}'); 167 | }); 168 | -------------------------------------------------------------------------------- /lib/result.es6: -------------------------------------------------------------------------------- 1 | import Warning from './warning'; 2 | 3 | /** 4 | * @typedef {object} Message 5 | * @property {string} type - message type 6 | * @property {string} plugin - source PostCSS plugin name 7 | */ 8 | 9 | /** 10 | * Provides the result of the PostCSS transformations. 11 | * 12 | * A Result instance is returned by {@link LazyResult#then} 13 | * or {@link Root#toResult} methods. 14 | * 15 | * @example 16 | * postcss([cssnext]).process(css).then(function (result) { 17 | * console.log(result.css); 18 | * }); 19 | * 20 | * @example 21 | * var result2 = postcss.parse(css).toResult(); 22 | */ 23 | class Result { 24 | 25 | /** 26 | * @param {Processor} processor - processor used for this transformation. 27 | * @param {Root} root - Root node after all transformations. 28 | * @param {processOptions} opts - options from the {@link Processor#process} 29 | * or {@link Root#toResult} 30 | */ 31 | constructor(processor, root, opts) { 32 | /** 33 | * @member {Processor} - The Processor instance used 34 | * for this transformation. 35 | * 36 | * @example 37 | * for ( let plugin of result.processor.plugins) { 38 | * if ( plugin.postcssPlugin === 'postcss-bad' ) { 39 | * throw 'postcss-good is incompatible with postcss-bad'; 40 | * } 41 | * }); 42 | */ 43 | this.processor = processor; 44 | /** 45 | * @member {Message[]} - Contains messages from plugins 46 | * (e.g., warnings or custom messages). 47 | * Each message should have type 48 | * and plugin properties. 49 | * 50 | * @example 51 | * postcss.plugin('postcss-min-browser', () => { 52 | * return (css, result) => { 53 | * var browsers = detectMinBrowsersByCanIUse(css); 54 | * result.messages.push({ 55 | * type: 'min-browser', 56 | * plugin: 'postcss-min-browser', 57 | * browsers: browsers 58 | * }); 59 | * }; 60 | * }); 61 | */ 62 | this.messages = []; 63 | /** 64 | * @member {Root} - Root node after all transformations. 65 | * 66 | * @example 67 | * root.toResult().root == root; 68 | */ 69 | this.root = root; 70 | /** 71 | * @member {processOptions} - Options from the {@link Processor#process} 72 | * or {@link Root#toResult} call 73 | * that produced this Result instance. 74 | * 75 | * @example 76 | * root.toResult(opts).opts == opts; 77 | */ 78 | this.opts = opts; 79 | /** 80 | * @member {string} - A CSS string representing of {@link Result#root}. 81 | * 82 | * @example 83 | * postcss.parse('a{}').toResult().css //=> "a{}" 84 | */ 85 | this.css = undefined; 86 | /** 87 | * @member {SourceMapGenerator} - An instance of `SourceMapGenerator` 88 | * class from the `source-map` library, 89 | * representing changes 90 | * to the {@link Result#root} instance. 91 | * 92 | * @example 93 | * result.map.toJSON() //=> { version: 3, file: 'a.css', … } 94 | * 95 | * @example 96 | * if ( result.map ) { 97 | * fs.writeFileSync(result.opts.to + '.map', result.map.toString()); 98 | * } 99 | */ 100 | this.map = undefined; 101 | } 102 | 103 | /** 104 | * Returns for @{link Result#css} content. 105 | * 106 | * @example 107 | * result + '' === result.css 108 | * 109 | * @return {string} string representing of {@link Result#root} 110 | */ 111 | toString() { 112 | return this.css; 113 | } 114 | 115 | /** 116 | * Creates an instance of {@link Warning} and adds it 117 | * to {@link Result#messages}. 118 | * 119 | * @param {string} text - warning message 120 | * @param {Object} [opts] - warning options 121 | * @param {Node} opts.node - CSS node that caused the warning 122 | * @param {string} opts.word - word in CSS source that caused the warning 123 | * @param {number} opts.index - index in CSS node string that caused 124 | * the warning 125 | * @param {string} opts.plugin - name of the plugin that created 126 | * this warning. {@link Result#warn} fills 127 | * this property automatically. 128 | * 129 | * @return {Warning} created warning 130 | */ 131 | warn(text, opts = { }) { 132 | if ( !opts.plugin ) { 133 | if ( this.lastPlugin && this.lastPlugin.postcssPlugin ) { 134 | opts.plugin = this.lastPlugin.postcssPlugin; 135 | } 136 | } 137 | 138 | let warning = new Warning(text, opts); 139 | this.messages.push(warning); 140 | 141 | return warning; 142 | } 143 | 144 | /** 145 | * Returns warnings from plugins. Filters {@link Warning} instances 146 | * from {@link Result#messages}. 147 | * 148 | * @example 149 | * result.warnings().forEach(warn => { 150 | * console.warn(warn.toString()); 151 | * }); 152 | * 153 | * @return {Warning[]} warnings from plugins 154 | */ 155 | warnings() { 156 | return this.messages.filter( i => i.type === 'warning' ); 157 | } 158 | 159 | /** 160 | * An alias for the {@link Result#css} property. 161 | * Use it with syntaxes that generate non-CSS output. 162 | * @type {string} 163 | * 164 | * @example 165 | * result.css === result.content; 166 | */ 167 | get content() { 168 | return this.css; 169 | } 170 | 171 | } 172 | 173 | export default Result; 174 | -------------------------------------------------------------------------------- /test/previous-map.js: -------------------------------------------------------------------------------- 1 | import parse from '../lib/parse'; 2 | 3 | import mozilla from 'source-map'; 4 | import path from 'path'; 5 | import test from 'ava'; 6 | import fs from 'fs-extra'; 7 | 8 | let dir = path.join(__dirname, 'prevmap-fixtures'); 9 | let mapObj = { 10 | version: 3, 11 | file: null, 12 | sources: [], 13 | names: [], 14 | mappings: '' 15 | }; 16 | let map = JSON.stringify(mapObj); 17 | 18 | test.afterEach( () => { 19 | if ( fs.existsSync(dir) ) fs.removeSync(dir); 20 | }); 21 | 22 | test('misses property if no map', t => { 23 | t.deepEqual(typeof parse('a{}').source.input.map, 'undefined'); 24 | }); 25 | 26 | test('creates property if map present', t => { 27 | let root = parse('a{}', { map: { prev: map } }); 28 | t.deepEqual(root.source.input.map.text, map); 29 | }); 30 | 31 | test('returns consumer', t => { 32 | let obj = parse('a{}', { map: { prev: map } }).source.input.map.consumer(); 33 | t.truthy(obj instanceof mozilla.SourceMapConsumer); 34 | }); 35 | 36 | test('sets annotation property', t => { 37 | let mapOpts = { map: { prev: map } }; 38 | 39 | let root1 = parse('a{}', mapOpts); 40 | t.deepEqual(typeof root1.source.input.map.annotation, 'undefined'); 41 | 42 | let root2 = parse('a{}/*# sourceMappingURL=a.css.map */', mapOpts); 43 | t.deepEqual(root2.source.input.map.annotation, 'a.css.map'); 44 | }); 45 | 46 | test('checks previous sources content', t => { 47 | let map2 = { 48 | version: 3, 49 | file: 'b', 50 | sources: ['a'], 51 | names: [], 52 | mappings: '' 53 | }; 54 | 55 | let opts = { map: { prev: map2 } }; 56 | t.false(parse('a{}', opts).source.input.map.withContent()); 57 | 58 | map2.sourcesContent = ['a{}']; 59 | t.true(parse('a{}', opts).source.input.map.withContent()); 60 | }); 61 | 62 | test('decodes base64 maps', t => { 63 | let b64 = new Buffer(map).toString('base64'); 64 | let css = 'a{}\n' + 65 | `/*# sourceMappingURL=data:application/json;base64,${b64} */`; 66 | 67 | t.deepEqual(parse(css).source.input.map.text, map); 68 | }); 69 | 70 | test('decodes base64 UTF-8 maps', t => { 71 | let b64 = new Buffer(map).toString('base64'); 72 | let css = 'a{}\n/*# sourceMappingURL=data:application/json;' + 73 | 'charset=utf-8;base64,' + b64 + ' */'; 74 | 75 | t.deepEqual(parse(css).source.input.map.text, map); 76 | }); 77 | 78 | test('accepts different name for UTF-8 encoding', t => { 79 | let b64 = new Buffer(map).toString('base64'); 80 | let css = 'a{}\n/*# sourceMappingURL=data:application/json;' + 81 | 'charset=utf8;base64,' + b64 + ' */'; 82 | 83 | t.deepEqual(parse(css).source.input.map.text, map); 84 | }); 85 | 86 | test('decodes URI maps', t => { 87 | let uri = 'data:application/json,' + decodeURI(map); 88 | let css = `a{}\n/*# sourceMappingURL=${ uri } */`; 89 | 90 | t.deepEqual(parse(css).source.input.map.text, map); 91 | }); 92 | 93 | test('removes map on request', t => { 94 | let uri = 'data:application/json,' + decodeURI(map); 95 | let css = `a{}\n/*# sourceMappingURL=${ uri } */`; 96 | 97 | let input = parse(css, { map: { prev: false } }).source.input; 98 | t.deepEqual(typeof input.map, 'undefined'); 99 | }); 100 | 101 | test('raises on unknown inline encoding', t => { 102 | let css = 'a { }\n/*# sourceMappingURL=data:application/json;' + 103 | 'md5,68b329da9893e34099c7d8ad5cb9c940*/'; 104 | 105 | t.throws( () => { 106 | parse(css); 107 | }, 'Unsupported source map encoding md5'); 108 | }); 109 | 110 | test('raises on unknown map format', t => { 111 | t.throws( () => { 112 | parse('a{}', { map: { prev: 1 } }); 113 | }, 'Unsupported previous source map format: 1'); 114 | }); 115 | 116 | test('reads map from annotation', t => { 117 | let file = path.join(dir, 'a.map'); 118 | fs.outputFileSync(file, map); 119 | let root = parse('a{}\n/*# sourceMappingURL=a.map */', { from: file }); 120 | 121 | t.deepEqual(root.source.input.map.text, map); 122 | t.deepEqual(root.source.input.map.root, dir); 123 | }); 124 | 125 | test('sets uniq name for inline map', t => { 126 | let map2 = { 127 | version: 3, 128 | sources: ['a'], 129 | names: [], 130 | mappings: '' 131 | }; 132 | 133 | let opts = { map: { prev: map2 } }; 134 | let file1 = parse('a{}', opts).source.input.map.file; 135 | let file2 = parse('a{}', opts).source.input.map.file; 136 | 137 | t.regex(file1, /^$/); 138 | t.notDeepEqual(file1, file2); 139 | }); 140 | 141 | test('should accept an empty mappings string', () => { 142 | let emptyMap = { 143 | version: 3, 144 | sources: [], 145 | names: [], 146 | mappings: '' 147 | }; 148 | parse('body{}', { map: { prev: emptyMap } } ); 149 | }); 150 | 151 | test('should accept a function', t => { 152 | let css = 'body{}\n/*# sourceMappingURL=a.map */'; 153 | let file = path.join(dir, 'previous-sourcemap-function.map'); 154 | fs.outputFileSync(file, map); 155 | let opts = { 156 | map: { 157 | prev: (/* from */) => file 158 | } 159 | }; 160 | let root = parse(css, opts); 161 | t.deepEqual(root.source.input.map.text, map); 162 | t.deepEqual(root.source.input.map.annotation, 'a.map'); 163 | }); 164 | 165 | test('should call function with opts.from', t => { 166 | t.plan(1); 167 | 168 | let css = 'body{}\n/*# sourceMappingURL=a.map */'; 169 | let file = path.join(dir, 'previous-sourcemap-function.map'); 170 | fs.outputFileSync(file, map); 171 | let opts = { 172 | from: 'a.css', 173 | map: { 174 | prev: from => { 175 | t.deepEqual(from, 'a.css'); 176 | return file; 177 | } 178 | } 179 | }; 180 | parse(css, opts); 181 | }); 182 | 183 | test('should raise when function returns invalid path', t => { 184 | let css = 'body{}\n/*# sourceMappingURL=a.map */'; 185 | let fakeMap = Number.MAX_SAFE_INTEGER.toString() + '.map'; 186 | let fakePath = path.join(dir, fakeMap); 187 | let opts = { 188 | map: { 189 | prev: () => fakePath 190 | } 191 | }; 192 | t.throws( () => { 193 | parse(css, opts); 194 | }, 'Unable to load previous source map: ' + fakePath); 195 | }); 196 | -------------------------------------------------------------------------------- /d.ts/node.d.ts: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import CssSyntaxError from './css-syntax-error'; 3 | import * as postcss from './postcss'; 4 | import Result from './result'; 5 | export default class Node implements postcss.Node { 6 | /** 7 | * Returns a string representing the node's type. Possible values are 8 | * root, atrule, rule, decl or comment. 9 | */ 10 | type: string; 11 | /** 12 | * Unique node ID 13 | */ 14 | id: string; 15 | /** 16 | * Contains information to generate byte-to-byte equal node string as it 17 | * was in origin input. 18 | */ 19 | raws: postcss.NodeRaws; 20 | /** 21 | * Returns the node's parent node. 22 | */ 23 | parent: Container; 24 | /** 25 | * Returns the input source of the node. The property is used in source map 26 | * generation. If you create a node manually (e.g., with postcss.decl() ), 27 | * that node will not have a source property and will be absent from the 28 | * source map. For this reason, the plugin developer should consider cloning 29 | * nodes to create new ones (in which case the new node's source will 30 | * reference the original, cloned node) or setting the source property 31 | * manually. 32 | */ 33 | source: postcss.NodeSource; 34 | constructor(defaults?: Object); 35 | /** 36 | * This method produces very useful error messages. If present, an input 37 | * source map will be used to get the original position of the source, even 38 | * from a previous compilation step (e.g., from Sass compilation). 39 | * @returns The original position of the node in the source, showing line 40 | * and column numbers and also a small excerpt to facilitate debugging. 41 | */ 42 | error( 43 | /** 44 | * Error description. 45 | */ 46 | message: string, options?: postcss.NodeErrorOptions): CssSyntaxError; 47 | /** 48 | * Creates an instance of Warning and adds it to messages. This method is 49 | * provided as a convenience wrapper for Result#warn. 50 | * Note that `opts.node` is automatically passed to Result#warn for you. 51 | * @param result The result that will receive the warning. 52 | * @param text Warning message. It will be used in the `text` property of 53 | * the message object. 54 | * @param opts Properties to assign to the message object. 55 | */ 56 | warn(result: Result, text: string, opts?: postcss.WarningOptions): void; 57 | /** 58 | * Removes the node from its parent and cleans the parent property in the 59 | * node and its children. 60 | * @returns This node for chaining. 61 | */ 62 | remove(): this; 63 | /** 64 | * @returns A CSS string representing the node. 65 | */ 66 | toString(stringifier?: any): string; 67 | /** 68 | * @param overrides New properties to override in the clone. 69 | * @returns A clone of this node. The node and its (cloned) children will 70 | * have a clean parent and code style properties. 71 | */ 72 | clone(overrides?: Object): Node; 73 | /** 74 | * Shortcut to clone the node and insert the resulting cloned node before 75 | * the current node. 76 | * @param overrides New Properties to override in the clone. 77 | * @returns The cloned node. 78 | */ 79 | cloneBefore(overrides?: Object): Node; 80 | /** 81 | * Shortcut to clone the node and insert the resulting cloned node after 82 | * the current node. 83 | * @param overrides New Properties to override in the clone. 84 | * @returns The cloned node. 85 | */ 86 | cloneAfter(overrides?: Object): Node; 87 | /** 88 | * Inserts node(s) before the current node and removes the current node. 89 | * @returns This node for chaining. 90 | */ 91 | replaceWith(...nodes: (Node | Object)[]): this; 92 | /** 93 | * Removes the node from its current parent and inserts it at the end of 94 | * newParent. This will clean the before and after code style properties 95 | * from the node and replace them with the indentation style of newParent. 96 | * It will also clean the between property if newParent is in another Root. 97 | * @param newParent Where the current node will be moved. 98 | * @returns This node for chaining. 99 | */ 100 | moveTo(newParent: Container): this; 101 | /** 102 | * Removes the node from its current parent and inserts it into a new 103 | * parent before otherNode. This will also clean the node's code style 104 | * properties just as it would in node.moveTo(newParent). 105 | * @param otherNode Will be after the current node after moving. 106 | * @returns This node for chaining. 107 | */ 108 | moveBefore(otherNode: Node): this; 109 | /** 110 | * Removes the node from its current parent and inserts it into a new 111 | * parent after otherNode. This will also clean the node's code style 112 | * properties just as it would in node.moveTo(newParent). 113 | * @param otherNode Will be before the current node after moving. 114 | * @returns This node for chaining. 115 | */ 116 | moveAfter(otherNode: Node): this; 117 | /** 118 | * @returns The next child of the node's parent; or, returns undefined if 119 | * the current node is the last child. 120 | */ 121 | next(): Node; 122 | /** 123 | * @returns The previous child of the node's parent; or, returns undefined 124 | * if the current node is the first child. 125 | */ 126 | prev(): Node; 127 | toJSON(): postcss.JsonNode; 128 | /** 129 | * @param prop Name or code style property. 130 | * @param defaultType Name of default value. It can be easily missed if the 131 | * value is the same as prop. 132 | * @returns A code style property value. If the node is missing the code 133 | * style property (because the node was manually built or cloned), PostCSS 134 | * will try to autodetect the code style property by looking at other nodes 135 | * in the tree. 136 | */ 137 | raw(prop: string, defaultType?: string): any; 138 | /** 139 | * @returns The Root instance of the node's tree. 140 | */ 141 | root(): any; 142 | cleanRaws(keepBetween?: boolean): void; 143 | positionInside(index: number): { 144 | line: number; 145 | column: number; 146 | }; 147 | positionBy(options: any): { 148 | column: number; 149 | line: number; 150 | }; 151 | /** 152 | * Deprecated. Use Node#remove. 153 | */ 154 | removeSelf(): void; 155 | replace(nodes: any): this; 156 | style(prop: string, defaultType?: string): any; 157 | cleanStyles(keepBetween?: boolean): void; 158 | before: string; 159 | between: string; 160 | } 161 | -------------------------------------------------------------------------------- /docs/guidelines/plugin.md: -------------------------------------------------------------------------------- 1 | # PostCSS Plugin Guidelines 2 | 3 | A PostCSS plugin is a function that receives and, usually, 4 | transforms a CSS AST from the PostCSS parser. 5 | 6 | The rules below are *mandatory* for all PostCSS plugins. 7 | 8 | See also [ClojureWerkz’s recommendations] for open source projects. 9 | 10 | [ClojureWerkz’s recommendations]: http://blog.clojurewerkz.org/blog/2013/04/20/how-to-make-your-open-source-project-really-awesome/ 11 | 12 | ## 1. API 13 | 14 | ### 1.1 Clear name with `postcss-` prefix 15 | 16 | The plugin’s purpose should be clear just by reading its name. 17 | If you wrote a transpiler for CSS 4 Custom Media, `postcss-custom-media` 18 | would be a good name. If you wrote a plugin to support mixins, 19 | `postcss-mixins` would be a good name. 20 | 21 | The prefix `postcss-` shows that the plugin is part of the PostCSS ecosystem. 22 | 23 | This rule is not mandatory for plugins that can run as independent tools, 24 | without the user necessarily knowing that it is powered by 25 | PostCSS — for example, [cssnext] and [Autoprefixer]. 26 | 27 | [Autoprefixer]: https://github.com/postcss/autoprefixer 28 | [cssnext]: http://cssnext.io/ 29 | 30 | ### 1.2. Do one thing, and do it well 31 | 32 | Do not create multitool plugins. Several small, one-purpose plugins bundled into 33 | a plugin pack is usually a better solution. 34 | 35 | For example, [cssnext] contains many small plugins, 36 | one for each W3C specification. And [cssnano] contains a separate plugin 37 | for each of its optimization. 38 | 39 | [cssnext]: http://cssnext.io/ 40 | [cssnano]: https://github.com/ben-eb/cssnano 41 | 42 | ### 1.3. Do not use mixins 43 | 44 | Preprocessors libraries like Compass provide an API with mixins. 45 | 46 | PostCSS plugins are different. 47 | A plugin cannot be just a set of mixins for [postcss-mixins]. 48 | 49 | To achieve your goal, consider transforming valid CSS 50 | or using custom at-rules and custom properties. 51 | 52 | [postcss-mixins]: https://github.com/postcss/postcss-mixins 53 | 54 | ### 1.4. Create plugin by `postcss.plugin` 55 | 56 | By wrapping your function in this method, 57 | you are hooking into a common plugin API: 58 | 59 | ```js 60 | module.exports = postcss.plugin('plugin-name', function (opts) { 61 | return function (css, result) { 62 | // Plugin code 63 | }; 64 | }); 65 | ``` 66 | 67 | ## 2. Processing 68 | 69 | ### 2.1. Plugin must be tested 70 | 71 | A CI service like [Travis] is also recommended for testing code in 72 | different environments. You should test in (at least) Node.js 0.12 and stable. 73 | 74 | [Travis]: https://travis-ci.org/ 75 | 76 | ### 2.2. Use asynchronous methods whenever possible 77 | 78 | For example, use `fs.writeFile` instead of `fs.writeFileSync`: 79 | 80 | ```js 81 | postcss.plugin('plugin-sprite', function (opts) { 82 | return function (css, result) { 83 | 84 | return new Promise(function (resolve, reject) { 85 | var sprite = makeSprite(); 86 | fs.writeFile(opts.file, function (err) { 87 | if ( err ) return reject(err); 88 | resolve(); 89 | }) 90 | }); 91 | 92 | }; 93 | }); 94 | ``` 95 | 96 | ### 2.3. Set `node.source` for new nodes 97 | 98 | Every node must have a relevant `source` so PostCSS can generate 99 | an accurate source map. 100 | 101 | So if you add new declaration based on some existing declaration, you should 102 | clone the existing declaration in order to save that original `source`. 103 | 104 | ```js 105 | if ( needPrefix(decl.prop) ) { 106 | decl.cloneBefore({ prop: '-webkit-' + decl.prop }); 107 | } 108 | ``` 109 | 110 | You can also set `source` directly, copying from some existing node: 111 | 112 | ```js 113 | if ( decl.prop === 'animation' ) { 114 | var keyframe = createAnimationByName(decl.value); 115 | keyframes.source = decl.source; 116 | decl.root().append(keyframes); 117 | } 118 | ``` 119 | 120 | ### 2.4. Use only the public PostCSS API 121 | 122 | PostCSS plugins must not rely on undocumented properties or methods, 123 | which may be subject to change in any minor release. The public API 124 | is described in [API docs]. 125 | 126 | [API docs]: http://api.postcss.org/ 127 | 128 | ## 3. Errors 129 | 130 | ### 3.1. Use `node.error` on CSS relevant errors 131 | 132 | If you have an error because of input CSS (like an unknown name 133 | in a mixin plugin) you should use `node.error` to create an error 134 | that includes source position: 135 | 136 | ```js 137 | if ( typeof mixins[name] === 'undefined' ) { 138 | throw decl.error('Unknown mixin ' + name, { plugin: 'postcss-mixins' }); 139 | } 140 | ``` 141 | 142 | ### 3.2. Use `result.warn` for warnings 143 | 144 | Do not print warnings with `console.log` or `console.warn`, 145 | because some PostCSS runner may not allow console output. 146 | 147 | ```js 148 | if ( outdated(decl.prop) ) { 149 | result.warn(decl.prop + ' is outdated', { node: decl }); 150 | } 151 | ``` 152 | 153 | If CSS input is a source of the warning, the plugin must set the `node` option. 154 | 155 | ## 4. Documentation 156 | 157 | ### 4.1. Document your plugin in English 158 | 159 | PostCSS plugins must have their `README.md` written in English. Do not be afraid 160 | of your English skills, as the open source community will fix your errors. 161 | 162 | Of course, you are welcome to write documentation in other languages; 163 | just name them appropriately (e.g. `README.ja.md`). 164 | 165 | ### 4.2. Include input and output examples 166 | 167 | The plugin's `README.md` must contain example input and output CSS. 168 | A clear example is the best way to describe how your plugin works. 169 | 170 | The first section of the `README.md` is a good place to put examples. 171 | See [postcss-opacity](https://github.com/iamvdo/postcss-opacity) for an example. 172 | 173 | Of course, this guideline does not apply if your plugin does not 174 | transform the CSS. 175 | 176 | ### 4.3. Maintain a changelog 177 | 178 | PostCSS plugins must describe the changes of all their releases 179 | in a separate file, such as `CHANGELOG.md`, `History.md`, or [GitHub Releases]. 180 | Visit [Keep A Changelog] for more information about how to write one of these. 181 | 182 | Of course, you should be using [SemVer]. 183 | 184 | [Keep A Changelog]: http://keepachangelog.com/ 185 | [GitHub Releases]: https://help.github.com/articles/creating-releases/ 186 | [SemVer]: http://semver.org/ 187 | 188 | ### 4.4. Include `postcss-plugin` keyword in `package.json` 189 | 190 | PostCSS plugins written for npm must have the `postcss-plugin` keyword 191 | in their `package.json`. This special keyword will be useful for feedback about 192 | the PostCSS ecosystem. 193 | 194 | For packages not published to npm, this is not mandatory, but is recommended 195 | if the package format can contain keywords. 196 | -------------------------------------------------------------------------------- /test/tokenize.js: -------------------------------------------------------------------------------- 1 | import tokenize from '../lib/tokenize'; 2 | import Input from '../lib/input'; 3 | 4 | import test from 'ava'; 5 | 6 | function run(t, css, opts, tokens) { 7 | if ( typeof tokens === 'undefined' ) [tokens, opts] = [opts, tokens]; 8 | t.deepEqual(tokenize(new Input(css, opts)), tokens); 9 | } 10 | 11 | test('tokenizes empty file', t => { 12 | run(t, '', []); 13 | }); 14 | 15 | test('tokenizes space', t => { 16 | run(t, '\r\n \f\t', [ ['space', '\r\n \f\t'] ]); 17 | }); 18 | 19 | test('tokenizes word', t => { 20 | run(t, 'ab', [ ['word', 'ab', 1, 1, 1, 2] ]); 21 | }); 22 | 23 | test('splits word by !', t => { 24 | run(t, 'aa!bb', [ 25 | ['word', 'aa', 1, 1, 1, 2], 26 | ['word', '!bb', 1, 3, 1, 5] 27 | ]); 28 | }); 29 | 30 | test('changes lines in spaces', t => { 31 | run(t, 'a \n b', [ 32 | ['word', 'a', 1, 1, 1, 1], 33 | ['space', ' \n '], 34 | ['word', 'b', 2, 2, 2, 2] 35 | ]); 36 | }); 37 | 38 | test('tokenizes control chars', t => { 39 | run(t, '{:;}', [ 40 | ['{', '{', 1, 1], 41 | [':', ':', 1, 2], 42 | [';', ';', 1, 3], 43 | ['}', '}', 1, 4] 44 | ]); 45 | }); 46 | 47 | test('escapes control symbols', t => { 48 | run(t, '\\(\\{\\"\\@\\\\""', [ 49 | ['word', '\\(', 1, 1, 1, 2], 50 | ['word', '\\{', 1, 3, 1, 4], 51 | ['word', '\\"', 1, 5, 1, 6], 52 | ['word', '\\@', 1, 7, 1, 8], 53 | ['word', '\\\\', 1, 9, 1, 10], 54 | ['string', '""', 1, 11, 1, 12] 55 | ]); 56 | }); 57 | 58 | test('escapes backslash', t => { 59 | run(t, '\\\\\\\\{', [ 60 | ['word', '\\\\\\\\', 1, 1, 1, 4], 61 | ['{', '{', 1, 5] 62 | ]); 63 | }); 64 | 65 | test('tokenizes simple brackets', t => { 66 | run(t, '(ab)', [ ['brackets', '(ab)', 1, 1, 1, 4] ]); 67 | }); 68 | 69 | test('tokenizes complicated brackets', t => { 70 | run(t, '(())("")(/**/)(\\\\)(\n)(', [ 71 | ['(', '(', 1, 1], 72 | ['brackets', '()', 1, 2, 1, 3], 73 | [')', ')', 1, 4], 74 | ['(', '(', 1, 5], 75 | ['string', '""', 1, 6, 1, 7], 76 | [')', ')', 1, 8], 77 | ['(', '(', 1, 9], 78 | ['comment', '/**/', 1, 10, 1, 13], 79 | [')', ')', 1, 14], 80 | ['(', '(', 1, 15], 81 | ['word', '\\\\', 1, 16, 1, 17], 82 | [')', ')', 1, 18], 83 | ['(', '(', 1, 19], 84 | ['space', '\n'], 85 | [')', ')', 2, 1], 86 | ['(', '(', 2, 2] 87 | ]); 88 | }); 89 | 90 | test('tokenizes string', t => { 91 | run(t, '\'"\'"\\""', [ 92 | ['string', '\'"\'', 1, 1, 1, 3], 93 | ['string', '"\\""', 1, 4, 1, 7] 94 | ]); 95 | }); 96 | 97 | test('tokenizes escaped string', t => { 98 | run(t, '"\\\\"', [ ['string', '"\\\\"', 1, 1, 1, 4] ]); 99 | }); 100 | 101 | test('changes lines in strings', t => { 102 | run(t, '"\n\n""\n\n"', [ 103 | ['string', '"\n\n"', 1, 1, 3, 1], 104 | ['string', '"\n\n"', 3, 2, 5, 1] 105 | ]); 106 | }); 107 | 108 | test('tokenizes at-word', t => { 109 | run(t, '@word ', [ ['at-word', '@word', 1, 1, 1, 5], ['space', ' '] ]); 110 | }); 111 | 112 | test('tokenizes at-word end', t => { 113 | run(t, '@one{@two()@three""@four;', [ 114 | ['at-word', '@one', 1, 1, 1, 4], 115 | ['{', '{', 1, 5], 116 | ['at-word', '@two', 1, 6, 1, 9], 117 | ['brackets', '()', 1, 10, 1, 11], 118 | ['at-word', '@three', 1, 12, 1, 17], 119 | ['string', '""', 1, 18, 1, 19], 120 | ['at-word', '@four', 1, 20, 1, 24], 121 | [';', ';', 1, 25] 122 | ]); 123 | }); 124 | 125 | test('tokenizes urls', t => { 126 | run(t, 'url(/*\\))', [ ['word', 'url', 1, 1, 1, 3], 127 | ['brackets', '(/*\\))', 1, 4, 1, 9] ]); 128 | }); 129 | 130 | test('tokenizes quoted urls', t => { 131 | run(t, 'url(")")', [ ['word', 'url', 1, 1, 1, 3], 132 | ['(', '(', 1, 4], 133 | ['string', '")"', 1, 5, 1, 7], 134 | [')', ')', 1, 8] ]); 135 | }); 136 | 137 | test('tokenizes at-symbol', t => { 138 | run(t, '@', [ ['at-word', '@', 1, 1, 1, 1] ]); 139 | }); 140 | 141 | test('tokenizes comment', t => { 142 | run(t, '/* a\nb */', [ ['comment', '/* a\nb */', 1, 1, 2, 4] ]); 143 | }); 144 | 145 | test('changes lines in comments', t => { 146 | run(t, 'a/* \n */b', [ 147 | ['word', 'a', 1, 1, 1, 1], 148 | ['comment', '/* \n */', 1, 2, 2, 3], 149 | ['word', 'b', 2, 4, 2, 4] 150 | ]); 151 | }); 152 | 153 | test('supports line feed', t => { 154 | run(t, 'a\fb', [ 155 | ['word', 'a', 1, 1, 1, 1], 156 | ['space', '\f'], 157 | ['word', 'b', 2, 1, 2, 1] 158 | ]); 159 | }); 160 | 161 | test('supports carriage return', t => { 162 | run(t, 'a\rb\r\nc', [ 163 | ['word', 'a', 1, 1, 1, 1], 164 | ['space', '\r'], 165 | ['word', 'b', 2, 1, 2, 1], 166 | ['space', '\r\n'], 167 | ['word', 'c', 3, 1, 3, 1] 168 | ]); 169 | }); 170 | 171 | test('tokenizes CSS', t => { 172 | let css = 'a {\n' + 173 | ' content: "a";\n' + 174 | ' width: calc(1px;)\n' + 175 | ' }\n' + 176 | '/* small screen */\n' + 177 | '@media screen {}'; 178 | run(t, css, [ 179 | ['word', 'a', 1, 1, 1, 1], 180 | ['space', ' '], 181 | ['{', '{', 1, 3], 182 | ['space', '\n '], 183 | ['word', 'content', 2, 3, 2, 9], 184 | [':', ':', 2, 10], 185 | ['space', ' '], 186 | ['string', '"a"', 2, 12, 2, 14], 187 | [';', ';', 2, 15], 188 | ['space', '\n '], 189 | ['word', 'width', 3, 3, 3, 7], 190 | [':', ':', 3, 8], 191 | ['space', ' '], 192 | ['word', 'calc', 3, 10, 3, 13], 193 | ['brackets', '(1px;)', 3, 14, 3, 19], 194 | ['space', '\n '], 195 | ['}', '}', 4, 3], 196 | ['space', '\n'], 197 | ['comment', '/* small screen */', 5, 1, 5, 18], 198 | ['space', '\n'], 199 | ['at-word', '@media', 6, 1, 6, 6], 200 | ['space', ' '], 201 | ['word', 'screen', 6, 8, 6, 13], 202 | ['space', ' '], 203 | ['{', '{', 6, 15], 204 | ['}', '}', 6, 16] 205 | ]); 206 | }); 207 | 208 | test('throws error on unclosed string', t => { 209 | t.throws(() => { 210 | tokenize(new Input(' "')); 211 | }, /:1:2: Unclosed quote/); 212 | }); 213 | 214 | test('throws error on unclosed comment', t => { 215 | t.throws(() => { 216 | tokenize(new Input(' /*')); 217 | }, /:1:2: Unclosed comment/); 218 | }); 219 | 220 | test('throws error on unclosed url', t => { 221 | t.throws(() => { 222 | tokenize(new Input('url(')); 223 | }, /:1:4: Unclosed bracket/); 224 | }); 225 | -------------------------------------------------------------------------------- /lib/postcss.es6: -------------------------------------------------------------------------------- 1 | import Declaration from './declaration'; 2 | import Processor from './processor'; 3 | import stringify from './stringify'; 4 | import Comment from './comment'; 5 | import AtRule from './at-rule'; 6 | import vendor from './vendor'; 7 | import parse from './parse'; 8 | import list from './list'; 9 | import Rule from './rule'; 10 | import Root from './root'; 11 | 12 | /** 13 | * Create a new {@link Processor} instance that will apply `plugins` 14 | * as CSS processors. 15 | * 16 | * @param {Array.|Processor} plugins - PostCSS 17 | * plugins. See {@link Processor#use} for plugin format. 18 | * 19 | * @return {Processor} Processor to process multiple CSS 20 | * 21 | * @example 22 | * import postcss from 'postcss'; 23 | * 24 | * postcss(plugins).process(css, { from, to }).then(result => { 25 | * console.log(result.css); 26 | * }); 27 | * 28 | * @namespace postcss 29 | */ 30 | function postcss(...plugins) { 31 | if ( plugins.length === 1 && Array.isArray(plugins[0]) ) { 32 | plugins = plugins[0]; 33 | } 34 | return new Processor(plugins); 35 | } 36 | 37 | /** 38 | * Creates a PostCSS plugin with a standard API. 39 | * 40 | * The newly-wrapped function will provide both the name and PostCSS 41 | * version of the plugin. 42 | * 43 | * ```js 44 | * const processor = postcss([replace]); 45 | * processor.plugins[0].postcssPlugin //=> 'postcss-replace' 46 | * processor.plugins[0].postcssVersion //=> '5.1.0' 47 | * ``` 48 | * 49 | * The plugin function receives 2 arguments: {@link Root} 50 | * and {@link Result} instance. The function should mutate the provided 51 | * `Root` node. Alternatively, you can create a new `Root` node 52 | * and override the `result.root` property. 53 | * 54 | * ```js 55 | * const cleaner = postcss.plugin('postcss-cleaner', () => { 56 | * return (css, result) => { 57 | * result.root = postcss.root(); 58 | * }; 59 | * }); 60 | * ``` 61 | * 62 | * As a convenience, plugins also expose a `process` method so that you can use 63 | * them as standalone tools. 64 | * 65 | * ```js 66 | * cleaner.process(css, options); 67 | * // This is equivalent to: 68 | * postcss([ cleaner(options) ]).process(css); 69 | * ``` 70 | * 71 | * Asynchronous plugins should return a `Promise` instance. 72 | * 73 | * ```js 74 | * postcss.plugin('postcss-import', () => { 75 | * return (css, result) => { 76 | * return new Promise( (resolve, reject) => { 77 | * fs.readFile('base.css', (base) => { 78 | * css.prepend(base); 79 | * resolve(); 80 | * }); 81 | * }); 82 | * }; 83 | * }); 84 | * ``` 85 | * 86 | * Add warnings using the {@link Node#warn} method. 87 | * Send data to other plugins using the {@link Result#messages} array. 88 | * 89 | * ```js 90 | * postcss.plugin('postcss-caniuse-test', () => { 91 | * return (css, result) => { 92 | * css.walkDecls(decl => { 93 | * if ( !caniuse.support(decl.prop) ) { 94 | * decl.warn(result, 'Some browsers do not support ' + decl.prop); 95 | * } 96 | * }); 97 | * }; 98 | * }); 99 | * ``` 100 | * 101 | * @param {string} name - PostCSS plugin name. Same as in `name` 102 | * property in `package.json`. It will be saved 103 | * in `plugin.postcssPlugin` property. 104 | * @param {function} initializer - will receive plugin options 105 | * and should return {@link pluginFunction} 106 | * 107 | * @return {Plugin} PostCSS plugin 108 | */ 109 | postcss.plugin = function plugin(name, initializer) { 110 | let creator = function (...args) { 111 | let transformer = initializer(...args); 112 | transformer.postcssPlugin = name; 113 | transformer.postcssVersion = (new Processor()).version; 114 | return transformer; 115 | }; 116 | 117 | let cache; 118 | Object.defineProperty(creator, 'postcss', { 119 | get() { 120 | if ( !cache ) cache = creator(); 121 | return cache; 122 | } 123 | }); 124 | 125 | creator.process = function (css, opts) { 126 | return postcss([ creator(opts) ]).process(css, opts); 127 | }; 128 | 129 | return creator; 130 | }; 131 | 132 | /** 133 | * Default function to convert a node tree into a CSS string. 134 | * 135 | * @param {Node} node - start node for stringifing. Usually {@link Root}. 136 | * @param {builder} builder - function to concatenate CSS from node’s parts 137 | * or generate string and source map 138 | * 139 | * @return {void} 140 | * 141 | * @function 142 | */ 143 | postcss.stringify = stringify; 144 | 145 | /** 146 | * Parses source css and returns a new {@link Root} node, 147 | * which contains the source CSS nodes. 148 | * 149 | * @param {string|toString} css - string with input CSS or any object 150 | * with toString() method, like a Buffer 151 | * @param {processOptions} [opts] - options with only `from` and `map` keys 152 | * 153 | * @return {Root} PostCSS AST 154 | * 155 | * @example 156 | * // Simple CSS concatenation with source map support 157 | * const root1 = postcss.parse(css1, { from: file1 }); 158 | * const root2 = postcss.parse(css2, { from: file2 }); 159 | * root1.append(root2).toResult().css; 160 | * 161 | * @function 162 | */ 163 | postcss.parse = parse; 164 | 165 | /** 166 | * @member {vendor} - Contains the {@link vendor} module. 167 | * 168 | * @example 169 | * postcss.vendor.unprefixed('-moz-tab') //=> ['tab'] 170 | */ 171 | postcss.vendor = vendor; 172 | 173 | /** 174 | * @member {list} - Contains the {@link list} module. 175 | * 176 | * @example 177 | * postcss.list.space('5px calc(10% + 5px)') //=> ['5px', 'calc(10% + 5px)'] 178 | */ 179 | postcss.list = list; 180 | 181 | /** 182 | * Creates a new {@link Comment} node. 183 | * 184 | * @param {object} [defaults] - properties for the new node. 185 | * 186 | * @return {Comment} new Comment node 187 | * 188 | * @example 189 | * postcss.comment({ text: 'test' }) 190 | */ 191 | postcss.comment = defaults => new Comment(defaults); 192 | 193 | /** 194 | * Creates a new {@link AtRule} node. 195 | * 196 | * @param {object} [defaults] - properties for the new node. 197 | * 198 | * @return {AtRule} new AtRule node 199 | * 200 | * @example 201 | * postcss.atRule({ name: 'charset' }).toString() //=> "@charset" 202 | */ 203 | postcss.atRule = defaults => new AtRule(defaults); 204 | 205 | /** 206 | * Creates a new {@link Declaration} node. 207 | * 208 | * @param {object} [defaults] - properties for the new node. 209 | * 210 | * @return {Declaration} new Declaration node 211 | * 212 | * @example 213 | * postcss.decl({ prop: 'color', value: 'red' }).toString() //=> "color: red" 214 | */ 215 | postcss.decl = defaults => new Declaration(defaults); 216 | 217 | /** 218 | * Creates a new {@link Rule} node. 219 | * 220 | * @param {object} [defaults] - properties for the new node. 221 | * 222 | * @return {AtRule} new Rule node 223 | * 224 | * @example 225 | * postcss.rule({ selector: 'a' }).toString() //=> "a {\n}" 226 | */ 227 | postcss.rule = defaults => new Rule(defaults); 228 | 229 | /** 230 | * Creates a new {@link Root} node. 231 | * 232 | * @param {object} [defaults] - properties for the new node. 233 | * 234 | * @return {Root} new Root node 235 | * 236 | * @example 237 | * postcss.root({ after: '\n' }).toString() //=> "\n" 238 | */ 239 | postcss.root = defaults => new Root(defaults); 240 | 241 | export default postcss; 242 | -------------------------------------------------------------------------------- /lib/css-syntax-error.es6: -------------------------------------------------------------------------------- 1 | import supportsColor from 'supports-color'; 2 | 3 | import warnOnce from './warn-once'; 4 | 5 | /** 6 | * The CSS parser throws this error for broken CSS. 7 | * 8 | * Custom parsers can throw this error for broken custom syntax using 9 | * the {@link Node#error} method. 10 | * 11 | * PostCSS will use the input source map to detect the original error location. 12 | * If you wrote a Sass file, compiled it to CSS and then parsed it with PostCSS, 13 | * PostCSS will show the original position in the Sass file. 14 | * 15 | * If you need the position in the PostCSS input 16 | * (e.g., to debug the previous compiler), use `error.input.file`. 17 | * 18 | * @example 19 | * // Catching and checking syntax error 20 | * try { 21 | * postcss.parse('a{') 22 | * } catch (error) { 23 | * if ( error.name === 'CssSyntaxError' ) { 24 | * error //=> CssSyntaxError 25 | * } 26 | * } 27 | * 28 | * @example 29 | * // Raising error from plugin 30 | * throw node.error('Unknown variable', { plugin: 'postcss-vars' }); 31 | */ 32 | class CssSyntaxError { 33 | 34 | /** 35 | * @param {string} message - error message 36 | * @param {number} [line] - source line of the error 37 | * @param {number} [column] - source column of the error 38 | * @param {string} [source] - source code of the broken file 39 | * @param {string} [file] - absolute path to the broken file 40 | * @param {string} [plugin] - PostCSS plugin name, if error came from plugin 41 | */ 42 | constructor(message, line, column, source, file, plugin) { 43 | /** 44 | * @member {string} - Always equal to `'CssSyntaxError'`. You should 45 | * always check error type 46 | * by `error.name === 'CssSyntaxError'` instead of 47 | * `error instanceof CssSyntaxError`, because 48 | * npm could have several PostCSS versions. 49 | * 50 | * @example 51 | * if ( error.name === 'CssSyntaxError' ) { 52 | * error //=> CssSyntaxError 53 | * } 54 | */ 55 | this.name = 'CssSyntaxError'; 56 | /** 57 | * @member {string} - Error message. 58 | * 59 | * @example 60 | * error.message //=> 'Unclosed block' 61 | */ 62 | this.reason = message; 63 | 64 | if ( file ) { 65 | /** 66 | * @member {string} - Absolute path to the broken file. 67 | * 68 | * @example 69 | * error.file //=> 'a.sass' 70 | * error.input.file //=> 'a.css' 71 | */ 72 | this.file = file; 73 | } 74 | if ( source ) { 75 | /** 76 | * @member {string} - Source code of the broken file. 77 | * 78 | * @example 79 | * error.source //=> 'a { b {} }' 80 | * error.input.column //=> 'a b { }' 81 | */ 82 | this.source = source; 83 | } 84 | if ( plugin ) { 85 | /** 86 | * @member {string} - Plugin name, if error came from plugin. 87 | * 88 | * @example 89 | * error.plugin //=> 'postcss-vars' 90 | */ 91 | this.plugin = plugin; 92 | } 93 | if ( typeof line !== 'undefined' && typeof column !== 'undefined' ) { 94 | /** 95 | * @member {number} - Source line of the error. 96 | * 97 | * @example 98 | * error.line //=> 2 99 | * error.input.line //=> 4 100 | */ 101 | this.line = line; 102 | /** 103 | * @member {number} - Source column of the error. 104 | * 105 | * @example 106 | * error.column //=> 1 107 | * error.input.column //=> 4 108 | */ 109 | this.column = column; 110 | } 111 | 112 | this.setMessage(); 113 | 114 | if ( Error.captureStackTrace ) { 115 | Error.captureStackTrace(this, CssSyntaxError); 116 | } 117 | } 118 | 119 | setMessage() { 120 | /** 121 | * @member {string} - Full error text in the GNU error format 122 | * with plugin, file, line and column. 123 | * 124 | * @example 125 | * error.message //=> 'a.css:1:1: Unclosed block' 126 | */ 127 | this.message = this.plugin ? this.plugin + ': ' : ''; 128 | this.message += this.file ? this.file : ''; 129 | if ( typeof this.line !== 'undefined' ) { 130 | this.message += ':' + this.line + ':' + this.column; 131 | } 132 | this.message += ': ' + this.reason; 133 | } 134 | 135 | /** 136 | * Returns a few lines of CSS source that caused the error. 137 | * 138 | * If the CSS has an input source map without `sourceContent`, 139 | * this method will return an empty string. 140 | * 141 | * @param {boolean} [color] whether arrow will be colored red by terminal 142 | * color codes. By default, PostCSS will detect 143 | * color support by `process.stdout.isTTY` 144 | * and `process.env.NODE_DISABLE_COLORS`. 145 | * 146 | * @example 147 | * error.showSourceCode() //=> "a { 148 | * // bad 149 | * // ^ 150 | * // }" 151 | * 152 | * @return {string} few lines of CSS source that caused the error 153 | */ 154 | showSourceCode(color) { 155 | if ( !this.source ) return ''; 156 | 157 | let num = this.line - 1; 158 | let lines = this.source.split('\n'); 159 | 160 | let prev = num > 0 ? lines[num - 1] + '\n' : ''; 161 | let broken = lines[num]; 162 | let next = num < lines.length - 1 ? '\n' + lines[num + 1] : ''; 163 | 164 | let mark = '\n'; 165 | for ( let i = 0; i < this.column - 1; i++ ) { 166 | mark += ' '; 167 | } 168 | 169 | if ( typeof color === 'undefined' ) color = supportsColor; 170 | if ( color ) { 171 | mark += '\x1B[1;31m^\x1B[0m'; 172 | } else { 173 | mark += '^'; 174 | } 175 | 176 | return '\n' + prev + broken + mark + next; 177 | } 178 | 179 | /** 180 | * Returns error position, message and source code of the broken part. 181 | * 182 | * @example 183 | * error.toString() //=> "CssSyntaxError: app.css:1:1: Unclosed block 184 | * // a { 185 | * // ^" 186 | * 187 | * @return {string} error position, message and source code 188 | */ 189 | toString() { 190 | return this.name + ': ' + this.message + this.showSourceCode(); 191 | } 192 | 193 | get generated() { 194 | warnOnce('CssSyntaxError#generated is depreacted. Use input instead.'); 195 | return this.input; 196 | } 197 | 198 | /** 199 | * @memberof CssSyntaxError# 200 | * @member {Input} input - Input object with PostCSS internal information 201 | * about input file. If input has source map 202 | * from previous tool, PostCSS will use origin 203 | * (for example, Sass) source. You can use this 204 | * object to get PostCSS input source. 205 | * 206 | * @example 207 | * error.input.file //=> 'a.css' 208 | * error.file //=> 'a.sass' 209 | */ 210 | 211 | } 212 | 213 | export default CssSyntaxError; 214 | -------------------------------------------------------------------------------- /lib/processor.es6: -------------------------------------------------------------------------------- 1 | import LazyResult from './lazy-result'; 2 | 3 | /** 4 | * @callback builder 5 | * @param {string} part - part of generated CSS connected to this node 6 | * @param {Node} node - AST node 7 | * @param {"start"|"end"} [type] - node’s part type 8 | */ 9 | 10 | /** 11 | * @callback parser 12 | * 13 | * @param {string|toString} css - string with input CSS or any object 14 | * with toString() method, like a Buffer 15 | * @param {processOptions} [opts] - options with only `from` and `map` keys 16 | * 17 | * @return {Root} PostCSS AST 18 | */ 19 | 20 | /** 21 | * @callback stringifier 22 | * 23 | * @param {Node} node - start node for stringifing. Usually {@link Root}. 24 | * @param {builder} builder - function to concatenate CSS from node’s parts 25 | * or generate string and source map 26 | * 27 | * @return {void} 28 | */ 29 | 30 | /** 31 | * @typedef {object} syntax 32 | * @property {parser} parse - function to generate AST by string 33 | * @property {stringifier} stringify - function to generate string by AST 34 | */ 35 | 36 | /** 37 | * @typedef {object} toString 38 | * @property {function} toString 39 | */ 40 | 41 | /** 42 | * @callback pluginFunction 43 | * @param {Root} root - parsed input CSS 44 | * @param {Result} result - result to set warnings or check other plugins 45 | */ 46 | 47 | /** 48 | * @typedef {object} Plugin 49 | * @property {function} postcss - PostCSS plugin function 50 | */ 51 | 52 | /** 53 | * @typedef {object} processOptions 54 | * @property {string} from - the path of the CSS source file. 55 | * You should always set `from`, 56 | * because it is used in source map 57 | * generation and syntax error messages. 58 | * @property {string} to - the path where you’ll put the output 59 | * CSS file. You should always set `to` 60 | * to generate correct source maps. 61 | * @property {parser} parser - function to generate AST by string 62 | * @property {stringifier} stringifier - class to generate string by AST 63 | * @property {syntax} syntax - object with `parse` and `stringify` 64 | * @property {object} map - source map options 65 | * @property {boolean} map.inline - does source map should 66 | * be embedded in the output 67 | * CSS as a base64-encoded 68 | * comment 69 | * @property {string|object|false|function} map.prev - source map content 70 | * from a previous 71 | * processing step 72 | * (for example, Sass). 73 | * PostCSS will try to find 74 | * previous map 75 | * automatically, so you 76 | * could disable it by 77 | * `false` value. 78 | * @property {boolean} map.sourcesContent - does PostCSS should set 79 | * the origin content to map 80 | * @property {string|false} map.annotation - does PostCSS should set 81 | * annotation comment to map 82 | * @property {string} map.from - override `from` in map’s 83 | * `sources` 84 | */ 85 | 86 | /** 87 | * Contains plugins to process CSS. Create one `Processor` instance, 88 | * initialize its plugins, and then use that instance on numerous CSS files. 89 | * 90 | * @example 91 | * const processor = postcss([autoprefixer, precss]); 92 | * processor.process(css1).then(result => console.log(result.css)); 93 | * processor.process(css2).then(result => console.log(result.css)); 94 | */ 95 | class Processor { 96 | 97 | /** 98 | * @param {Array.|Processor} plugins - PostCSS 99 | * plugins. See {@link Processor#use} for plugin format. 100 | */ 101 | constructor(plugins = []) { 102 | /** 103 | * @member {string} - Current PostCSS version. 104 | * 105 | * @example 106 | * if ( result.processor.version.split('.')[0] !== '5' ) { 107 | * throw new Error('This plugin works only with PostCSS 5'); 108 | * } 109 | */ 110 | this.version = '5.1.0'; 111 | /** 112 | * @member {pluginFunction[]} - Plugins added to this processor. 113 | * 114 | * @example 115 | * const processor = postcss([autoprefixer, precss]); 116 | * processor.plugins.length //=> 2 117 | */ 118 | this.plugins = this.normalize(plugins); 119 | } 120 | 121 | /** 122 | * Adds a plugin to be used as a CSS processor. 123 | * 124 | * PostCSS plugin can be in 4 formats: 125 | * * A plugin created by {@link postcss.plugin} method. 126 | * * A function. PostCSS will pass the function a @{link Root} 127 | * as the first argument and current {@link Result} instance 128 | * as the second. 129 | * * An object with a `postcss` method. PostCSS will use that method 130 | * as described in #2. 131 | * * Another {@link Processor} instance. PostCSS will copy plugins 132 | * from that instance into this one. 133 | * 134 | * Plugins can also be added by passing them as arguments when creating 135 | * a `postcss` instance (see [`postcss(plugins)`]). 136 | * 137 | * Asynchronous plugins should return a `Promise` instance. 138 | * 139 | * @param {Plugin|pluginFunction|Processor} plugin - PostCSS plugin 140 | * or {@link Processor} 141 | * with plugins 142 | * 143 | * @example 144 | * const processor = postcss() 145 | * .use(autoprefixer) 146 | * .use(precss); 147 | * 148 | * @return {Processes} current processor to make methods chain 149 | */ 150 | use(plugin) { 151 | this.plugins = this.plugins.concat(this.normalize([plugin])); 152 | return this; 153 | } 154 | 155 | /** 156 | * Parses source CSS and returns a {@link LazyResult} Promise proxy. 157 | * Because some plugins can be asynchronous it doesn’t make 158 | * any transformations. Transformations will be applied 159 | * in the {@link LazyResult} methods. 160 | * 161 | * @param {string|toString|Result} css - String with input CSS or 162 | * any object with a `toString()` 163 | * method, like a Buffer. 164 | * Optionally, send a {@link Result} 165 | * instance and the processor will 166 | * take the {@link Root} from it. 167 | * @param {processOptions} [opts] - options 168 | * 169 | * @return {LazyResult} Promise proxy 170 | * 171 | * @example 172 | * processor.process(css, { from: 'a.css', to: 'a.out.css' }) 173 | * .then(result => { 174 | * console.log(result.css); 175 | * }); 176 | */ 177 | process(css, opts = { }) { 178 | return new LazyResult(this, css, opts); 179 | } 180 | 181 | normalize(plugins) { 182 | let normalized = []; 183 | for ( let i of plugins ) { 184 | if ( i.postcss ) i = i.postcss; 185 | 186 | if ( typeof i === 'object' && Array.isArray(i.plugins) ) { 187 | normalized = normalized.concat(i.plugins); 188 | } else if ( typeof i === 'function' ) { 189 | normalized.push(i); 190 | } else { 191 | throw new Error(i + ' is not a PostCSS plugin'); 192 | } 193 | } 194 | return normalized; 195 | } 196 | 197 | } 198 | 199 | export default Processor; 200 | -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | # How to Write Custom Syntax 2 | 3 | PostCSS can transform styles in any syntax, and is not limited to just CSS. 4 | By writing a custom syntax, you can transform styles in any desired format. 5 | 6 | Writing a custom syntax is much harder than writing a PostCSS plugin, but 7 | it is an awesome adventure. 8 | 9 | There are 3 types of PostCSS syntax packages: 10 | 11 | * **Parser** to parse input string to node’s tree. 12 | * **Stringifier** to generate output string by node’s tree. 13 | * **Syntax** contains both parser and stringifier. 14 | 15 | ## Syntax 16 | 17 | A good example of a custom syntax is [SCSS]. Some users may want to transform 18 | SCSS sources with PostCSS plugins, for example if they need to add vendor 19 | prefixes or change the property order. So this syntax should output SCSS from 20 | an SCSS input. 21 | 22 | The syntax API is a very simple plain object, with `parse` & `stringify` 23 | functions: 24 | 25 | ```js 26 | module.exports = { 27 | parse: require('./parse'), 28 | stringify: require('./stringify') 29 | }; 30 | ``` 31 | 32 | [SCSS]: https://github.com/postcss/postcss-scss 33 | 34 | ## Parser 35 | 36 | A good example of a parser is [Safe Parser], which parses malformed/broken CSS. 37 | Because there is no point to generate broken output, this package only provides 38 | a parser. 39 | 40 | The parser API is a function which receives a string & returns a [`Root`] node. 41 | The second argument is a function which receives an object with PostCSS options. 42 | 43 | ```js 44 | var postcss = require('postcss'); 45 | 46 | module.exports = function (css, opts) { 47 | var root = postcss.root(); 48 | // Add other nodes to root 49 | return root; 50 | }; 51 | ``` 52 | 53 | [Safe Parser]: https://github.com/postcss/postcss-safe-parser 54 | [`Root`]: http://api.postcss.org/Root.html 55 | 56 | ### Main Theory 57 | 58 | There are many books about parsers; but do not worry because CSS syntax is 59 | very easy, and so the parser will be much simpler than a programming language 60 | parser. 61 | 62 | The default PostCSS parser contains two steps: 63 | 64 | 1. [Tokenizer] which reads input string character by character and builds a 65 | tokens array. For example, it joins space symbols to a `['space', '\n ']` 66 | token, and detects strings to a `['string', '"\"{"']` token. 67 | 2. [Parser] which reads the tokens array, creates node instances and 68 | builds a tree. 69 | 70 | [Tokenizer]: https://github.com/postcss/postcss/blob/master/lib/tokenize.es6 71 | [Parser]: https://github.com/postcss/postcss/blob/master/lib/parser.es6 72 | 73 | ### Performance 74 | 75 | Parsing input is often the most time consuming task in CSS processors. So it 76 | is very important to have a fast parser. 77 | 78 | The main rule of optimization is that there is no performance without a 79 | benchmark. You can look at [PostCSS benchmarks] to build your own. 80 | 81 | Of parsing tasks, the tokenize step will often take the most time, so its 82 | performance should be prioritized. Unfortunately, classes, functions and 83 | high level structures can slow down your tokenizer. Be ready to write dirty 84 | code with repeated statements. This is why it is difficult to extend the 85 | default [PostCSS tokenizer]; copy & paste will be a necessary evil. 86 | 87 | Second optimization is using character codes instead of strings. 88 | 89 | ```js 90 | // Slow 91 | string[i] === '{'; 92 | 93 | // Fast 94 | const OPEN_CURLY = 123; // `{' 95 | string.charCodeAt(i) === OPEN_CURLY; 96 | ``` 97 | 98 | Third optimization is “fast jumps”. If you find open quotes, you can find 99 | next closing quote much faster by `indexOf`: 100 | 101 | ```js 102 | // Simple jump 103 | next = string.indexOf('"', currentPosition + 1); 104 | 105 | // Jump by RegExp 106 | regexp.lastIndex = currentPosion + 1; 107 | regexp.text(string); 108 | next = regexp.lastIndex; 109 | ``` 110 | 111 | The parser can be a well written class. There is no need in copy-paste and 112 | hardcore optimization there. You can extend the default [PostCSS parser]. 113 | 114 | [PostCSS benchmarks]: https://github.com/postcss/benchmark 115 | [PostCSS tokenizer]: https://github.com/postcss/postcss/blob/master/lib/tokenize.es6 116 | [PostCSS parser]: https://github.com/postcss/postcss/blob/master/lib/parser.es6 117 | 118 | ### Node Source 119 | 120 | Every node should have `source` property to generate correct source map. 121 | This property contains `start` and `end` properties with `{ line, column }`, 122 | and `input` property with an [`Input`] instance. 123 | 124 | Your tokenizer should save the original position so that you can propagate 125 | the values to the parser, to ensure that the source map is correctly updated. 126 | 127 | [`Input`]: https://github.com/postcss/postcss/blob/master/lib/input.es6 128 | 129 | ### Raw Values 130 | 131 | A good PostCSS parser should provide all information (including spaces symbols) 132 | to generate byte-to-byte equal output. It is not so difficult, but respectful 133 | for user input and allow integration smoke tests. 134 | 135 | A parser should save all additional symbols to `node.raws` object. 136 | It is an open structure for you, you can add additional keys. 137 | For example, [SCSS parser] saves comment types (`/* */` or `//`) 138 | in `node.raws.inline`. 139 | 140 | The default parser cleans CSS values from comments and spaces. 141 | It saves the original value with comments to `node.raws.value.raw` and uses it, 142 | if the node value was not changed. 143 | 144 | [SCSS parser]: https://github.com/postcss/postcss-scss 145 | 146 | ### Tests 147 | 148 | Of course, all parsers in the PostCSS ecosystem must have tests. 149 | 150 | If your parser just extends CSS syntax (like [SCSS] or [Safe Parser]), 151 | you can use the [PostCSS Parser Tests]. It contains unit & integration tests. 152 | 153 | [PostCSS Parser Tests]: https://github.com/postcss/postcss-parser-tests 154 | 155 | ## Stringifier 156 | 157 | A style guide generator is a good example of a stringifier. It generates output 158 | HTML which contains CSS components. For this use case, a parser isn't necessary, 159 | so the package should just contain a stringifier. 160 | 161 | The Stringifier API is little bit more complicated, than the parser API. 162 | PostCSS generates a source map, so a stringifier can’t just return a string. 163 | It must link every substring with its source node. 164 | 165 | A Stringifier is a function which receives [`Root`] node and builder callback. 166 | Then it calls builder with every node’s string and node instance. 167 | 168 | ```js 169 | module.exports = function (root, builder) { 170 | // Some magic 171 | var string = decl.prop + ':' + decl.value + ';'; 172 | builder(string, decl); 173 | // Some science 174 | }; 175 | ``` 176 | 177 | ### Main Theory 178 | 179 | PostCSS [default stringifier] is just a class with a method for each node type 180 | and many methods to detect raw properties. 181 | 182 | In most cases it will be enough just to extend this class, 183 | like in [SCSS stringifier]. 184 | 185 | [default stringifier]: https://github.com/postcss/postcss/blob/master/lib/stringifier.es6 186 | [SCSS stringifier]: https://github.com/postcss/postcss-scss/blob/master/lib/scss-stringifier.es6 187 | 188 | ### Builder Function 189 | 190 | A builder function will be passed to `stringify` function as second argument. 191 | For example, the default PostCSS stringifier class saves it 192 | to `this.builder` property. 193 | 194 | Builder receives output substring and source node to append this substring 195 | to the final output. 196 | 197 | Some nodes contain other nodes in the middle. For example, a rule has a `{` 198 | at the beginning, many declarations inside and a closing `}`. 199 | 200 | For these cases, you should pass a third argument to builder function: 201 | `'start'` or `'end'` string: 202 | 203 | ```js 204 | this.builder(rule.selector + '{', rule, 'start'); 205 | // Stringify declarations inside 206 | this.builder('}', rule, 'end'); 207 | ``` 208 | 209 | ### Raw Values 210 | 211 | A good PostCSS custom syntax saves all symbols and provide byte-to-byte equal 212 | output if there were no changes. 213 | 214 | This is why every node has `node.raws` object to store space symbol, etc. 215 | 216 | Be careful, because sometimes these raw properties will not be present; some 217 | nodes may be built manually, or may lose their indentation when they are moved 218 | to another parent node. 219 | 220 | This is why the default stringifier has a `raw()` method to autodetect raw 221 | properties by other nodes. For example, it will look at other nodes to detect 222 | indent size and them multiply it with the current node depth. 223 | 224 | ### Tests 225 | 226 | A stringifier must have tests too. 227 | 228 | You can use unit and integration test cases from [PostCSS Parser Tests]. 229 | Just compare input CSS with CSS after your parser and stringifier. 230 | 231 | [PostCSS Parser Tests]: https://github.com/postcss/postcss-parser-tests 232 | -------------------------------------------------------------------------------- /lib/map-generator.es6: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | import mozilla from 'source-map'; 3 | import path from 'path'; 4 | 5 | export default class MapGenerator { 6 | 7 | constructor(stringify, root, opts) { 8 | this.stringify = stringify; 9 | this.mapOpts = opts.map || { }; 10 | this.root = root; 11 | this.opts = opts; 12 | } 13 | 14 | isMap() { 15 | if ( typeof this.opts.map !== 'undefined' ) { 16 | return !!this.opts.map; 17 | } else { 18 | return this.previous().length > 0; 19 | } 20 | } 21 | 22 | previous() { 23 | if ( !this.previousMaps ) { 24 | this.previousMaps = []; 25 | this.root.walk( node => { 26 | if ( node.source && node.source.input.map ) { 27 | let map = node.source.input.map; 28 | if ( this.previousMaps.indexOf(map) === -1 ) { 29 | this.previousMaps.push(map); 30 | } 31 | } 32 | }); 33 | } 34 | 35 | return this.previousMaps; 36 | } 37 | 38 | isInline() { 39 | if ( typeof this.mapOpts.inline !== 'undefined' ) { 40 | return this.mapOpts.inline; 41 | } 42 | 43 | let annotation = this.mapOpts.annotation; 44 | if ( typeof annotation !== 'undefined' && annotation !== true ) { 45 | return false; 46 | } 47 | 48 | if ( this.previous().length ) { 49 | return this.previous().some( i => i.inline ); 50 | } else { 51 | return true; 52 | } 53 | } 54 | 55 | isSourcesContent() { 56 | if ( typeof this.mapOpts.sourcesContent !== 'undefined' ) { 57 | return this.mapOpts.sourcesContent; 58 | } 59 | if ( this.previous().length ) { 60 | return this.previous().some( i => i.withContent() ); 61 | } else { 62 | return true; 63 | } 64 | } 65 | 66 | clearAnnotation() { 67 | if ( this.mapOpts.annotation === false ) return; 68 | 69 | let node; 70 | for ( let i = this.root.nodes.length - 1; i >= 0; i-- ) { 71 | node = this.root.nodes[i]; 72 | if ( node.type !== 'comment' ) continue; 73 | if ( node.text.indexOf('# sourceMappingURL=') === 0 ) { 74 | this.root.removeChild(i); 75 | } 76 | } 77 | } 78 | 79 | setSourcesContent() { 80 | let already = { }; 81 | this.root.walk( node => { 82 | if ( node.source ) { 83 | let from = node.source.input.from; 84 | if ( from && !already[from] ) { 85 | already[from] = true; 86 | let relative = this.relative(from); 87 | this.map.setSourceContent(relative, node.source.input.css); 88 | } 89 | } 90 | }); 91 | } 92 | 93 | applyPrevMaps() { 94 | for ( let prev of this.previous() ) { 95 | let from = this.relative(prev.file); 96 | let root = prev.root || path.dirname(prev.file); 97 | let map; 98 | 99 | if ( this.mapOpts.sourcesContent === false ) { 100 | map = new mozilla.SourceMapConsumer(prev.text); 101 | if ( map.sourcesContent ) { 102 | map.sourcesContent = map.sourcesContent.map( () => null ); 103 | } 104 | } else { 105 | map = prev.consumer(); 106 | } 107 | 108 | this.map.applySourceMap(map, from, this.relative(root)); 109 | } 110 | } 111 | 112 | isAnnotation() { 113 | if ( this.isInline() ) { 114 | return true; 115 | } else if ( typeof this.mapOpts.annotation !== 'undefined' ) { 116 | return this.mapOpts.annotation; 117 | } else if ( this.previous().length ) { 118 | return this.previous().some( i => i.annotation ); 119 | } else { 120 | return true; 121 | } 122 | } 123 | 124 | addAnnotation() { 125 | let content; 126 | 127 | if ( this.isInline() ) { 128 | content = 'data:application/json;base64,' + 129 | Base64.encode( this.map.toString() ); 130 | 131 | } else if ( typeof this.mapOpts.annotation === 'string' ) { 132 | content = this.mapOpts.annotation; 133 | 134 | } else { 135 | content = this.outputFile() + '.map'; 136 | } 137 | 138 | let eol = '\n'; 139 | if ( this.css.indexOf('\r\n') !== -1 ) eol = '\r\n'; 140 | 141 | this.css += eol + '/*# sourceMappingURL=' + content + ' */'; 142 | } 143 | 144 | outputFile() { 145 | if ( this.opts.to ) { 146 | return this.relative(this.opts.to); 147 | } else if ( this.opts.from ) { 148 | return this.relative(this.opts.from); 149 | } else { 150 | return 'to.css'; 151 | } 152 | } 153 | 154 | generateMap() { 155 | this.generateString(); 156 | if ( this.isSourcesContent() ) this.setSourcesContent(); 157 | if ( this.previous().length > 0 ) this.applyPrevMaps(); 158 | if ( this.isAnnotation() ) this.addAnnotation(); 159 | 160 | if ( this.isInline() ) { 161 | return [this.css]; 162 | } else { 163 | return [this.css, this.map]; 164 | } 165 | } 166 | 167 | relative(file) { 168 | if ( /^\w+:\/\//.test(file) ) return file; 169 | 170 | let from = this.opts.to ? path.dirname(this.opts.to) : '.'; 171 | 172 | if ( typeof this.mapOpts.annotation === 'string' ) { 173 | from = path.dirname( path.resolve(from, this.mapOpts.annotation) ); 174 | } 175 | 176 | file = path.relative(from, file); 177 | if ( path.sep === '\\' ) { 178 | return file.replace(/\\/g, '/'); 179 | } else { 180 | return file; 181 | } 182 | } 183 | 184 | sourcePath(node) { 185 | if ( this.mapOpts.from ) { 186 | return this.mapOpts.from; 187 | } else { 188 | return this.relative(node.source.input.from); 189 | } 190 | } 191 | 192 | generateString() { 193 | this.css = ''; 194 | this.map = new mozilla.SourceMapGenerator({ file: this.outputFile() }); 195 | 196 | let line = 1; 197 | let column = 1; 198 | 199 | let lines, last; 200 | this.stringify(this.root, (str, node, type) => { 201 | this.css += str; 202 | 203 | if ( node && type !== 'end' ) { 204 | if ( node.source && node.source.start ) { 205 | this.map.addMapping({ 206 | source: this.sourcePath(node), 207 | generated: { line, column: column - 1 }, 208 | original: { 209 | line: node.source.start.line, 210 | column: node.source.start.column - 1 211 | } 212 | }); 213 | } else { 214 | this.map.addMapping({ 215 | source: '', 216 | original: { line: 1, column: 0 }, 217 | generated: { line, column: column - 1 } 218 | }); 219 | } 220 | } 221 | 222 | lines = str.match(/\n/g); 223 | if ( lines ) { 224 | line += lines.length; 225 | last = str.lastIndexOf('\n'); 226 | column = str.length - last; 227 | } else { 228 | column += str.length; 229 | } 230 | 231 | if ( node && type !== 'start' ) { 232 | if ( node.source && node.source.end ) { 233 | this.map.addMapping({ 234 | source: this.sourcePath(node), 235 | generated: { line, column: column - 1 }, 236 | original: { 237 | line: node.source.end.line, 238 | column: node.source.end.column 239 | } 240 | }); 241 | } else { 242 | this.map.addMapping({ 243 | source: '', 244 | original: { line: 1, column: 0 }, 245 | generated: { line, column: column - 1 } 246 | }); 247 | } 248 | } 249 | }); 250 | } 251 | 252 | generate() { 253 | this.clearAnnotation(); 254 | 255 | if ( this.isMap() ) { 256 | return this.generateMap(); 257 | } else { 258 | let result = ''; 259 | this.stringify(this.root, i => { 260 | result += i; 261 | }); 262 | return [result]; 263 | } 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /lib/tokenize.es6: -------------------------------------------------------------------------------- 1 | const SINGLE_QUOTE = '\''.charCodeAt(0); 2 | const DOUBLE_QUOTE = '"'.charCodeAt(0); 3 | const BACKSLASH = '\\'.charCodeAt(0); 4 | const SLASH = '/'.charCodeAt(0); 5 | const NEWLINE = '\n'.charCodeAt(0); 6 | const SPACE = ' '.charCodeAt(0); 7 | const FEED = '\f'.charCodeAt(0); 8 | const TAB = '\t'.charCodeAt(0); 9 | const CR = '\r'.charCodeAt(0); 10 | const OPEN_PARENTHESES = '('.charCodeAt(0); 11 | const CLOSE_PARENTHESES = ')'.charCodeAt(0); 12 | const OPEN_CURLY = '{'.charCodeAt(0); 13 | const CLOSE_CURLY = '}'.charCodeAt(0); 14 | const SEMICOLON = ';'.charCodeAt(0); 15 | const ASTERICK = '*'.charCodeAt(0); 16 | const COLON = ':'.charCodeAt(0); 17 | const AT = '@'.charCodeAt(0); 18 | 19 | const RE_AT_END = /[ \n\t\r\f\{\(\)'"\\;/]/g; 20 | const RE_WORD_END = /[ \n\t\r\f\(\)\{\}:;@!'"\\]|\/(?=\*)/g; 21 | const RE_BAD_BRACKET = /.[\\\/\("'\n]/; 22 | 23 | export default function tokenize(input) { 24 | let tokens = []; 25 | let css = input.css.valueOf(); 26 | 27 | let code, next, quote, lines, last, content, escape, 28 | nextLine, nextOffset, escaped, escapePos, prev, n; 29 | 30 | let length = css.length; 31 | let offset = -1; 32 | let line = 1; 33 | let pos = 0; 34 | 35 | function unclosed(what) { 36 | throw input.error('Unclosed ' + what, line, pos - offset); 37 | } 38 | 39 | while ( pos < length ) { 40 | code = css.charCodeAt(pos); 41 | 42 | if ( code === NEWLINE || code === FEED || 43 | code === CR && css.charCodeAt(pos + 1) !== NEWLINE ) { 44 | offset = pos; 45 | line += 1; 46 | } 47 | 48 | switch ( code ) { 49 | case NEWLINE: 50 | case SPACE: 51 | case TAB: 52 | case CR: 53 | case FEED: 54 | next = pos; 55 | do { 56 | next += 1; 57 | code = css.charCodeAt(next); 58 | if ( code === NEWLINE ) { 59 | offset = next; 60 | line += 1; 61 | } 62 | } while ( code === SPACE || 63 | code === NEWLINE || 64 | code === TAB || 65 | code === CR || 66 | code === FEED ); 67 | 68 | tokens.push(['space', css.slice(pos, next)]); 69 | pos = next - 1; 70 | break; 71 | 72 | case OPEN_CURLY: 73 | tokens.push(['{', '{', line, pos - offset]); 74 | break; 75 | 76 | case CLOSE_CURLY: 77 | tokens.push(['}', '}', line, pos - offset]); 78 | break; 79 | 80 | case COLON: 81 | tokens.push([':', ':', line, pos - offset]); 82 | break; 83 | 84 | case SEMICOLON: 85 | tokens.push([';', ';', line, pos - offset]); 86 | break; 87 | 88 | case OPEN_PARENTHESES: 89 | prev = tokens.length ? tokens[tokens.length - 1][1] : ''; 90 | n = css.charCodeAt(pos + 1); 91 | if ( prev === 'url' && n !== SINGLE_QUOTE && n !== DOUBLE_QUOTE && 92 | n !== SPACE && n !== NEWLINE && n !== TAB && 93 | n !== FEED && n !== CR ) { 94 | next = pos; 95 | do { 96 | escaped = false; 97 | next = css.indexOf(')', next + 1); 98 | if ( next === -1 ) unclosed('bracket'); 99 | escapePos = next; 100 | while ( css.charCodeAt(escapePos - 1) === BACKSLASH ) { 101 | escapePos -= 1; 102 | escaped = !escaped; 103 | } 104 | } while ( escaped ); 105 | 106 | tokens.push(['brackets', css.slice(pos, next + 1), 107 | line, pos - offset, 108 | line, next - offset 109 | ]); 110 | pos = next; 111 | 112 | } else { 113 | next = css.indexOf(')', pos + 1); 114 | content = css.slice(pos, next + 1); 115 | 116 | if ( next === -1 || RE_BAD_BRACKET.test(content) ) { 117 | tokens.push(['(', '(', line, pos - offset]); 118 | } else { 119 | tokens.push(['brackets', content, 120 | line, pos - offset, 121 | line, next - offset 122 | ]); 123 | pos = next; 124 | } 125 | } 126 | 127 | break; 128 | 129 | case CLOSE_PARENTHESES: 130 | tokens.push([')', ')', line, pos - offset]); 131 | break; 132 | 133 | case SINGLE_QUOTE: 134 | case DOUBLE_QUOTE: 135 | quote = code === SINGLE_QUOTE ? '\'' : '"'; 136 | next = pos; 137 | do { 138 | escaped = false; 139 | next = css.indexOf(quote, next + 1); 140 | if ( next === -1 ) unclosed('quote'); 141 | escapePos = next; 142 | while ( css.charCodeAt(escapePos - 1) === BACKSLASH ) { 143 | escapePos -= 1; 144 | escaped = !escaped; 145 | } 146 | } while ( escaped ); 147 | 148 | content = css.slice(pos, next + 1); 149 | lines = content.split('\n'); 150 | last = lines.length - 1; 151 | 152 | if ( last > 0 ) { 153 | nextLine = line + last; 154 | nextOffset = next - lines[last].length; 155 | } else { 156 | nextLine = line; 157 | nextOffset = offset; 158 | } 159 | 160 | tokens.push(['string', css.slice(pos, next + 1), 161 | line, pos - offset, 162 | nextLine, next - nextOffset 163 | ]); 164 | 165 | offset = nextOffset; 166 | line = nextLine; 167 | pos = next; 168 | break; 169 | 170 | case AT: 171 | RE_AT_END.lastIndex = pos + 1; 172 | RE_AT_END.test(css); 173 | if ( RE_AT_END.lastIndex === 0 ) { 174 | next = css.length - 1; 175 | } else { 176 | next = RE_AT_END.lastIndex - 2; 177 | } 178 | tokens.push(['at-word', css.slice(pos, next + 1), 179 | line, pos - offset, 180 | line, next - offset 181 | ]); 182 | pos = next; 183 | break; 184 | 185 | case BACKSLASH: 186 | next = pos; 187 | escape = true; 188 | while ( css.charCodeAt(next + 1) === BACKSLASH ) { 189 | next += 1; 190 | escape = !escape; 191 | } 192 | code = css.charCodeAt(next + 1); 193 | if ( escape && (code !== SLASH && 194 | code !== SPACE && 195 | code !== NEWLINE && 196 | code !== TAB && 197 | code !== CR && 198 | code !== FEED ) ) { 199 | next += 1; 200 | } 201 | tokens.push(['word', css.slice(pos, next + 1), 202 | line, pos - offset, 203 | line, next - offset 204 | ]); 205 | pos = next; 206 | break; 207 | 208 | default: 209 | if ( code === SLASH && css.charCodeAt(pos + 1) === ASTERICK ) { 210 | next = css.indexOf('*/', pos + 2) + 1; 211 | if ( next === 0 ) unclosed('comment'); 212 | 213 | content = css.slice(pos, next + 1); 214 | lines = content.split('\n'); 215 | last = lines.length - 1; 216 | 217 | if ( last > 0 ) { 218 | nextLine = line + last; 219 | nextOffset = next - lines[last].length; 220 | } else { 221 | nextLine = line; 222 | nextOffset = offset; 223 | } 224 | 225 | tokens.push(['comment', content, 226 | line, pos - offset, 227 | nextLine, next - nextOffset 228 | ]); 229 | 230 | offset = nextOffset; 231 | line = nextLine; 232 | pos = next; 233 | 234 | } else { 235 | RE_WORD_END.lastIndex = pos + 1; 236 | RE_WORD_END.test(css); 237 | if ( RE_WORD_END.lastIndex === 0 ) { 238 | next = css.length - 1; 239 | } else { 240 | next = RE_WORD_END.lastIndex - 2; 241 | } 242 | 243 | tokens.push(['word', css.slice(pos, next + 1), 244 | line, pos - offset, 245 | line, next - offset 246 | ]); 247 | pos = next; 248 | } 249 | 250 | break; 251 | } 252 | 253 | pos++; 254 | } 255 | 256 | return tokens; 257 | } 258 | -------------------------------------------------------------------------------- /lib/stringifier.es6: -------------------------------------------------------------------------------- 1 | /* eslint-disable valid-jsdoc */ 2 | 3 | const defaultRaw = { 4 | colon: ': ', 5 | indent: ' ', 6 | beforeDecl: '\n', 7 | beforeRule: '\n', 8 | beforeOpen: ' ', 9 | beforeClose: '\n', 10 | beforeComment: '\n', 11 | after: '\n', 12 | emptyBody: '', 13 | commentLeft: ' ', 14 | commentRight: ' ' 15 | }; 16 | 17 | function capitalize(str) { 18 | return str[0].toUpperCase() + str.slice(1); 19 | } 20 | 21 | class Stringifier { 22 | 23 | constructor(builder) { 24 | this.builder = builder; 25 | } 26 | 27 | stringify(node, semicolon) { 28 | this[node.type](node, semicolon); 29 | } 30 | 31 | root(node) { 32 | this.body(node); 33 | if ( node.raws.after ) this.builder(node.raws.after); 34 | } 35 | 36 | comment(node) { 37 | let left = this.raw(node, 'left', 'commentLeft'); 38 | let right = this.raw(node, 'right', 'commentRight'); 39 | this.builder('/*' + left + node.text + right + '*/', node); 40 | } 41 | 42 | decl(node, semicolon) { 43 | let between = this.raw(node, 'between', 'colon'); 44 | let string = node.prop + between + this.rawValue(node, 'value'); 45 | 46 | if ( node.important ) { 47 | string += node.raws.important || ' !important'; 48 | } 49 | 50 | if ( semicolon ) string += ';'; 51 | this.builder(string, node); 52 | } 53 | 54 | rule(node) { 55 | this.block(node, this.rawValue(node, 'selector')); 56 | } 57 | 58 | atrule(node, semicolon) { 59 | let name = '@' + node.name; 60 | let params = node.params ? this.rawValue(node, 'params') : ''; 61 | 62 | if ( typeof node.raws.afterName !== 'undefined' ) { 63 | name += node.raws.afterName; 64 | } else if ( params ) { 65 | name += ' '; 66 | } 67 | 68 | if ( node.nodes ) { 69 | this.block(node, name + params); 70 | } else { 71 | let end = (node.raws.between || '') + (semicolon ? ';' : ''); 72 | this.builder(name + params + end, node); 73 | } 74 | } 75 | 76 | body(node) { 77 | let last = node.nodes.length - 1; 78 | while ( last > 0 ) { 79 | if ( node.nodes[last].type !== 'comment' ) break; 80 | last -= 1; 81 | } 82 | 83 | let semicolon = this.raw(node, 'semicolon'); 84 | for ( let i = 0; i < node.nodes.length; i++ ) { 85 | let child = node.nodes[i]; 86 | let before = this.raw(child, 'before'); 87 | if ( before ) this.builder(before); 88 | this.stringify(child, last !== i || semicolon); 89 | } 90 | } 91 | 92 | block(node, start) { 93 | let between = this.raw(node, 'between', 'beforeOpen'); 94 | this.builder(start + between + '{', node, 'start'); 95 | 96 | let after; 97 | if ( node.nodes && node.nodes.length ) { 98 | this.body(node); 99 | after = this.raw(node, 'after'); 100 | } else { 101 | after = this.raw(node, 'after', 'emptyBody'); 102 | } 103 | 104 | if ( after ) this.builder(after); 105 | this.builder('}', node, 'end'); 106 | } 107 | 108 | raw(node, own, detect) { 109 | let value; 110 | if ( !detect ) detect = own; 111 | 112 | // Already had 113 | if ( own ) { 114 | value = node.raws[own]; 115 | if ( typeof value !== 'undefined' ) return value; 116 | } 117 | 118 | let parent = node.parent; 119 | 120 | // Hack for first rule in CSS 121 | if ( detect === 'before' ) { 122 | if ( !parent || parent.type === 'root' && parent.first === node ) { 123 | return ''; 124 | } 125 | } 126 | 127 | // Floating child without parent 128 | if ( !parent ) return defaultRaw[detect]; 129 | 130 | // Detect style by other nodes 131 | let root = node.root(); 132 | if ( !root.rawCache ) root.rawCache = { }; 133 | if ( typeof root.rawCache[detect] !== 'undefined' ) { 134 | return root.rawCache[detect]; 135 | } 136 | 137 | if ( detect === 'before' || detect === 'after' ) { 138 | return this.beforeAfter(node, detect); 139 | } else { 140 | let method = 'raw' + capitalize(detect); 141 | if ( this[method] ) { 142 | value = this[method](root, node); 143 | } else { 144 | root.walk( i => { 145 | value = i.raws[own]; 146 | if ( typeof value !== 'undefined' ) return false; 147 | }); 148 | } 149 | } 150 | 151 | if ( typeof value === 'undefined' ) value = defaultRaw[detect]; 152 | 153 | root.rawCache[detect] = value; 154 | return value; 155 | } 156 | 157 | rawSemicolon(root) { 158 | let value; 159 | root.walk( i => { 160 | if ( i.nodes && i.nodes.length && i.last.type === 'decl' ) { 161 | value = i.raws.semicolon; 162 | if ( typeof value !== 'undefined' ) return false; 163 | } 164 | }); 165 | return value; 166 | } 167 | 168 | rawEmptyBody(root) { 169 | let value; 170 | root.walk( i => { 171 | if ( i.nodes && i.nodes.length === 0 ) { 172 | value = i.raws.after; 173 | if ( typeof value !== 'undefined' ) return false; 174 | } 175 | }); 176 | return value; 177 | } 178 | 179 | rawIndent(root) { 180 | if ( root.raws.indent ) return root.raws.indent; 181 | let value; 182 | root.walk( i => { 183 | let p = i.parent; 184 | if ( p && p !== root && p.parent && p.parent === root ) { 185 | if ( typeof i.raws.before !== 'undefined' ) { 186 | let parts = i.raws.before.split('\n'); 187 | value = parts[parts.length - 1]; 188 | value = value.replace(/[^\s]/g, ''); 189 | return false; 190 | } 191 | } 192 | }); 193 | return value; 194 | } 195 | 196 | rawBeforeComment(root, node) { 197 | let value; 198 | root.walkComments( i => { 199 | if ( typeof i.raws.before !== 'undefined' ) { 200 | value = i.raws.before; 201 | if ( value.indexOf('\n') !== -1 ) { 202 | value = value.replace(/[^\n]+$/, ''); 203 | } 204 | return false; 205 | } 206 | }); 207 | if ( typeof value === 'undefined' ) { 208 | value = this.raw(node, null, 'beforeDecl'); 209 | } 210 | return value; 211 | } 212 | 213 | rawBeforeDecl(root, node) { 214 | let value; 215 | root.walkDecls( i => { 216 | if ( typeof i.raws.before !== 'undefined' ) { 217 | value = i.raws.before; 218 | if ( value.indexOf('\n') !== -1 ) { 219 | value = value.replace(/[^\n]+$/, ''); 220 | } 221 | return false; 222 | } 223 | }); 224 | if ( typeof value === 'undefined' ) { 225 | value = this.raw(node, null, 'beforeRule'); 226 | } 227 | return value; 228 | } 229 | 230 | rawBeforeRule(root) { 231 | let value; 232 | root.walk( i => { 233 | if ( i.nodes && (i.parent !== root || root.first !== i) ) { 234 | if ( typeof i.raws.before !== 'undefined' ) { 235 | value = i.raws.before; 236 | if ( value.indexOf('\n') !== -1 ) { 237 | value = value.replace(/[^\n]+$/, ''); 238 | } 239 | return false; 240 | } 241 | } 242 | }); 243 | return value; 244 | } 245 | 246 | rawBeforeClose(root) { 247 | let value; 248 | root.walk( i => { 249 | if ( i.nodes && i.nodes.length > 0 ) { 250 | if ( typeof i.raws.after !== 'undefined' ) { 251 | value = i.raws.after; 252 | if ( value.indexOf('\n') !== -1 ) { 253 | value = value.replace(/[^\n]+$/, ''); 254 | } 255 | return false; 256 | } 257 | } 258 | }); 259 | return value; 260 | } 261 | 262 | rawBeforeOpen(root) { 263 | let value; 264 | root.walk( i => { 265 | if ( i.type !== 'decl' ) { 266 | value = i.raws.between; 267 | if ( typeof value !== 'undefined' ) return false; 268 | } 269 | }); 270 | return value; 271 | } 272 | 273 | rawColon(root) { 274 | let value; 275 | root.walkDecls( i => { 276 | if ( typeof i.raws.between !== 'undefined' ) { 277 | value = i.raws.between.replace(/[^\s:]/g, ''); 278 | return false; 279 | } 280 | }); 281 | return value; 282 | } 283 | 284 | beforeAfter(node, detect) { 285 | let value; 286 | if ( node.type === 'decl' ) { 287 | value = this.raw(node, null, 'beforeDecl'); 288 | } else if ( node.type === 'comment' ) { 289 | value = this.raw(node, null, 'beforeComment'); 290 | } else if ( detect === 'before' ) { 291 | value = this.raw(node, null, 'beforeRule'); 292 | } else { 293 | value = this.raw(node, null, 'beforeClose'); 294 | } 295 | 296 | let buf = node.parent; 297 | let depth = 0; 298 | while ( buf && buf.type !== 'root' ) { 299 | depth += 1; 300 | buf = buf.parent; 301 | } 302 | 303 | if ( value.indexOf('\n') !== -1 ) { 304 | let indent = this.raw(node, null, 'indent'); 305 | if ( indent.length ) { 306 | for ( let step = 0; step < depth; step++ ) value += indent; 307 | } 308 | } 309 | 310 | return value; 311 | } 312 | 313 | rawValue(node, prop) { 314 | let value = node[prop]; 315 | let raw = node.raws[prop]; 316 | if ( raw && raw.value === value ) { 317 | return raw.raw; 318 | } else { 319 | return value; 320 | } 321 | } 322 | 323 | } 324 | 325 | export default Stringifier; 326 | --------------------------------------------------------------------------------