Repository: postcss/postcss-selector-parser
Pull request #120 (2017-10-21 00:13:17)

TypeScript support. Adds definition file and type guards.


Patch:
diff --git a/API.md b/API.md
index 7813a35..194e4c7 100644
--- a/API.md
+++ b/API.md
@@ -11,7 +11,7 @@ This is the module's main entry point.
 const parser = require('postcss-selector-parser');
 ```
 
-### `parser([transform])`
+### `parser([transform], [options])`
 
 Creates a new `processor` instance
 
@@ -37,6 +37,7 @@ const result = processor.processSync('*.class');
 Arguments:
 
 * `transform (function)`: Provide a function to work with the parsed AST.
+* `options (object)`: Provide default options for all calls on the returned `Processor`.
 
 ### `parser.attribute([props])`
 
@@ -597,37 +598,167 @@ around it.
 [id=Bar   i  ] /* "   i  " */
 ```
 
-## `processor`
+## `Processor`
 
-### `process|processSync(css, [options])`
+### `ProcessorOptions`
 
-Processes the `css`, returning the parsed output. An async method is exposed
-as `process`.
+* `lossless` - When `true`, whitespace is preserved. Defaults to `true`.
+* `updateSelector` - When `true`, if any processor methods are passed a postcss
+  `Rule` node instead of a string, then that Rule's selector is updated
+  with the results of the processing. Defaults to `true`.
+
+### `process|processSync(selectors, [options])`
+
+Processes the `selectors`, returning a string from the result of processing.
+
+Note: when the `updateSelector` option is set, the rule's selector
+will be updated with the resulting string.
+
+**Example:**
 
 ```js
+const parser = require("postcss-selector-parser");
 const processor = parser();
 
-const result = processor.processSync(' .class');
+let result = processor.processSync(' .class');
+console.log(result);
 // =>  .class
 
 // Asynchronous operation
-processor.process(' .class').then(result => /* ... */);
+let promise = processor.process(' .class').then(result => {
+    console.log(result)
+    // => .class
+});
 
 // To have the parser normalize whitespace values, utilize the options
-const result = processor.processSync('  .class  ', {lossless: false});
+result = processor.processSync('  .class  ', {lossless: false});
+console.log(result);
 // => .class
 
 // For better syntax errors, pass a PostCSS Rule node.
 const postcss = require('postcss');
-const rule = postcss.rule({selector: 'a'});
-const result = process.process(rule);
+rule = postcss.rule({selector: ' #foo    > a,  .class  '});
+processor.process(rule, {lossless: false, updateSelector: true}).then(result => {
+    console.log(result);
+    // => #foo>a,.class
+    console.log("rule:", rule.selector);
+    // => rule: #foo>a,.class
+})
 ```
 
 Arguments:
 
-* `css (string|object)`: Either a selector string or a PostCSS Rule node.
+* `selectors (string|postcss.Rule)`: Either a selector string or a PostCSS Rule
+  node.
 * `[options] (object)`: Process options
 
-Options:
 
-* `lossless (boolean)`: false to normalize the selector whitespace, defaults to true
+### `ast|astSync(selectors, [options])`
+
+Like `process()` and `processSync()` but after
+processing the `selectors` these methods return the `Root` node of the result
+instead of a string.
+
+Note: when the `updateSelector` option is set, the rule's selector
+will be updated with the resulting string.
+
+### `transform|transformSync(selectors, [options])`
+
+Like `process()` and `processSync()` but after
+processing the `selectors` these methods return the value returned by the
+processor callback.
+
+Note: when the `updateSelector` option is set, the rule's selector
+will be updated with the resulting string.
+
+### Error Handling Within Selector Processors
+
+The root node passed to the selector processor callback
+has a method `error(message, options)` that returns an
+error object. This method should always be used to raise
+errors relating to the syntax of selectors. The options
+to this method are passed to postcss's error constructor
+([documentation](http://api.postcss.org/Container.html#error)).
+
+#### Async Error Example
+
+```js
+let processor = (root) => {
+    return new Promise((resolve, reject) => {
+        root.walkClasses((classNode) => {
+            if (/^(.*)[-_]/.test(classNode.value)) {
+                let msg = "classes may not have underscores or dashes in them";
+                reject(root.error(msg, {
+                    index: classNode.sourceIndex + RegExp.$1.length + 1,
+                    word: classNode.value
+                }));
+            }
+        });
+        resolve();
+    });
+};
+
+const postcss = require("postcss");
+const parser = require("postcss-selector-parser");
+const selectorProcessor = parser(processor);
+const plugin = postcss.plugin('classValidator', (options) => {
+    return (root) => {
+        let promises = [];
+        root.walkRules(rule => {
+            promises.push(selectorProcessor.process(rule));
+        });
+        return Promise.all(promises);
+    };
+});
+postcss(plugin()).process(`
+.foo-bar {
+  color: red;
+}
+`.trim(), {from: 'test.css'}).catch((e) => console.error(e.toString()));
+
+// CssSyntaxError: classValidator: ./test.css:1:5: classes may not have underscores or dashes in them
+// 
+// > 1 | .foo-bar {
+//     |     ^
+//   2 |   color: red;
+//   3 | }
+```
+
+#### Synchronous Error Example
+
+```js
+let processor = (root) => {
+    root.walkClasses((classNode) => {
+        if (/.*[-_]/.test(classNode.value)) {
+            let msg = "classes may not have underscores or dashes in them";
+            throw root.error(msg, {
+                index: classNode.sourceIndex,
+                word: classNode.value
+            });
+        }
+    });
+};
+
+const postcss = require("postcss");
+const parser = require("postcss-selector-parser");
+const selectorProcessor = parser(processor);
+const plugin = postcss.plugin('classValidator', (options) => {
+    return (root) => {
+        root.walkRules(rule => {
+            selectorProcessor.processSync(rule);
+        });
+    };
+});
+postcss(plugin()).process(`
+.foo-bar {
+  color: red;
+}
+`.trim(), {from: 'test.css'}).catch((e) => console.error(e.toString()));
+
+// CssSyntaxError: classValidator: ./test.css:1:5: classes may not have underscores or dashes in them
+// 
+// > 1 | .foo-bar {
+//     |     ^
+//   2 |   color: red;
+//   3 | }
+```
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1717e9..78553b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,17 +8,36 @@
   no longer uses regular expressions to parse attribute selectors.
 * Added an async API (thanks to @jacobp100); the default `process` API is now
   async, and the sync API is now accessed through `processSync` instead.
+* `process()` and `processSync()` now return a string instead of the Processor
+  instance.
 * Tweaks handling of Less interpolation (thanks to @jwilsson).
 * Removes support for Node 0.12.
 
 ## Other changes
 
+* `ast()` and `astSync()` methods have been added to the `Processor`. These
+  return the `Root` node of the selectors after processing them.
+* `transform()` and `transformSync()` methods have been added to the
+  `Processor`. These return the value returned by the processor callback
+  after processing the selectors.
 * Set the parent when inserting a node (thanks to @chriseppstein).
 * Correctly adjust indices when using insertBefore/insertAfter (thanks to @tivac).
 * Fixes handling of namespaces with qualified tag selectors.
-* Now accepts a PostCSS rule node for better syntax errors.
+* `process`, `ast` and `transform` (and their sync variants) now accept a
+  `postcss` rule node. When provided, better errors are generated and selector
+  processing is automatically set back to the rule selector (unless the `updateSelector` option is set to `false`.)
 * Now more memory efficient when tokenizing selectors.
 
+### Upgrade hints
+
+The pattern of:
+
+`rule.selector = processor.process(rule.selector).result.toString();`
+
+is now:
+
+`processor.processSync(rule)`
+
 # 2.2.3
 
 * Resolves an issue where the parser would not reduce multiple spaces between an
diff --git a/package.json b/package.json
index 4560cd0..0631e07 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
     "postcss": "^6.0.6"
   },
   "main": "dist/index.js",
+  "types": "postcss-selector-parser.d.ts",
   "files": [
     "API.md",
     "CHANGELOG.md",
diff --git a/postcss-selector-parser.d.ts b/postcss-selector-parser.d.ts
new file mode 100644
index 0000000..c3b65a1
--- /dev/null
+++ b/postcss-selector-parser.d.ts
@@ -0,0 +1,321 @@
+// Type definitions for postcss-selector-parser 2.2.3
+// Definitions by: Chris Eppstein <chris@eppsteins.net>
+
+/*~ Note that ES6 modules cannot directly export callable functions.
+ *~ This file should be imported using the CommonJS-style:
+ *~   import x = require('someLibrary');
+ *~
+ *~ Refer to the documentation to understand common
+ *~ workarounds for this limitation of ES6 modules.
+ */
+
+/*~ This declaration specifies that the function
+ *~ is the exported object from the file
+ */
+export = parser;
+
+declare function parser(): parser.Processor<never>;
+declare function parser<Transform extends any>(processor: parser.AsyncProcessor<Transform>): parser.Processor<Transform, never>;
+declare function parser(processor: parser.AsyncProcessor): parser.Processor<never>;
+declare function parser<Transform extends any>(processor: parser.SyncProcessor<Transform>): parser.Processor<Transform, never>;
+declare function parser(processor: parser.SyncProcessor): parser.Processor<never>;
+declare function parser<Transform>(processor?: parser.SyncProcessor<Transform> | parser.AsyncProcessor<Transform>): parser.Processor<Transform>;
+
+/*~ If you want to expose types from your module as well, you can
+ *~ place them in this block. Often you will want to describe the
+ *~ shape of the return type of the function; that type should
+ *~ be declared in here, as this example shows.
+ */
+declare namespace parser {
+    /* copied from postcss -- so we don't need to add a dependency */
+    type ErrorOptions = {
+        plugin?: string;
+        word?: string;
+        index?: number
+    };
+    /* the bits we use of postcss.Rule, copied from postcss -- so we don't need to add a dependency */
+    type PostCSSRuleNode = {
+        selector: string
+        /**
+         * @returns postcss.CssSyntaxError but it's a complex object, caller
+         *   should cast to it if they have a dependency on postcss.
+         */
+        error(message: string, options?: ErrorOptions): Error;
+    };
+    /** Accepts a string  */
+    type Selectors = string | PostCSSRuleNode
+    type SyncProcessor<Transform = void> = (root: parser.Root) => Transform
+    type AsyncProcessor<Transform = void> = (root: parser.Root) => Transform | PromiseLike<Transform>
+
+    const TAG: "tag";
+    const STRING: "string";
+    const SELECTOR: "selector";
+    const ROOT: "root";
+    const PSEUDO: "pseudo";
+    const NESTING: "nesting";
+    const ID: "id";
+    const COMMENT: "comment";
+    const COMBINATOR: "combinator";
+    const CLASS: "class";
+    const ATTRIBUTE: "attribute";
+    const UNIVERSAL: "universal";
+
+    interface NodeTypes {
+        tag: Tag,
+        string: String,
+        selector: Selector,
+        root: Root,
+        pseudo: Pseudo,
+        nesting: Nesting,
+        id: Identifier,
+        comment: Comment,
+        combinator: Combinator,
+        class: ClassName,
+        attribute: Attribute,
+        universal: Universal
+    }
+
+    type Node = NodeTypes[keyof NodeTypes];
+
+    function isNode(node: any): node is Node;
+
+    interface Options {
+        /**
+         * Preserve whitespace when true. Default: false;
+         */
+        lossless: boolean;
+        /**
+         * When true and a postcss.Rule is passed, set the result of
+         * processing back onto the rule when done. Default: false.
+         */
+        updateSelector: boolean;
+    }
+    class Processor<
+        TransformType = never,
+        SyncSelectorsType extends Selectors | never = Selectors
+    > {
+        res: Root;
+        readonly result: String;
+        ast(selectors: Selectors, options?: Partial<Options>): Promise<Root>;
+        astSync(selectors: SyncSelectorsType, options?: Partial<Options>): Root;
+        transform(selectors: Selectors, options?: Partial<Options>): Promise<TransformType>;
+        transformSync(selectors: SyncSelectorsType, options?: Partial<Options>): TransformType;
+        process(selectors: Selectors, options?: Partial<Options>): Promise<string>;
+        processSync(selectors: SyncSelectorsType, options?: Partial<Options>): string;
+    }
+    interface ParserOptions {
+        css: string;
+        error: (message: string, options: ErrorOptions) => Error;
+        options: Options;
+    }
+    class Parser {
+        input: ParserOptions;
+        lossy: boolean;
+        position: number;
+        root: Root;
+        selectors: string;
+        current: Selector;
+        constructor(input: ParserOptions);
+        /**
+         * Raises an error, if the processor is invoked on
+         * a postcss Rule node, a better error message is raised.
+         */
+        error(message: string, options?: ErrorOptions): void;
+    }
+    interface NodeSource {
+        start?: {
+            line: number,
+            column: number
+        },
+        end?: {
+            line: number,
+            column: number
+        }
+    }
+    interface NodeOptions<Value = string> {
+        value: Value;
+        spaces?: {
+            before: string;
+            after: string;
+        }
+        source?: NodeSource;
+    }
+    interface Base<Value extends string | undefined = string> {
+        type: keyof NodeTypes;
+        parent?: Selector;
+        value: Value;
+        spaces?: {
+            before: string;
+            after: string;
+        }
+        source?: NodeSource;
+        remove(): Node;
+        replaceWith(...nodes: Node[]): Node;
+        next(): Node;
+        prev(): Node;
+        clone(opts: {[override: string]:any}): Node;
+        toString(): string;
+
+    }
+    interface ContainerOptions extends NodeOptions {
+        nodes?: Array<Node>;
+    }
+    interface Container<Value extends string | undefined = string> extends Base<Value> {
+        nodes: Array<Node>;
+        append(selector: Selector): Container;
+        prepend(selector: Selector): Container;
+        at(index: number): Node;
+        index(child: Node): number;
+        readonly first: Node;
+        readonly last: Node;
+        readonly length: number;
+        removeChild(child: Node): Container;
+        removeAll(): Container;
+        empty(): Container;
+        insertAfter(oldNode: Node, newNode: Node): Container;
+        insertBefore(oldNode: Node, newNode: Node): Container;
+        each(callback: (node: Node) => boolean | void): boolean | undefined;
+        walk(callback: (node: Node) => boolean | void): boolean | undefined;
+        walkAttributes(callback: (node: Node) => boolean | void): boolean | undefined;
+        walkClasses(callback: (node: Node) => boolean | void): boolean | undefined;
+        walkCombinators(callback: (node: Node) => boolean | void): boolean | undefined;
+        walkComments(callback: (node: Node) => boolean | void): boolean | undefined;
+        walkIds(callback: (node: Node) => boolean | void): boolean | undefined;
+        walkNesting(callback: (node: Node) => boolean | void): boolean | undefined;
+        walkPseudos(callback: (node: Node) => boolean | void): boolean | undefined;
+        walkTags(callback: (node: Node) => boolean | void): boolean | undefined;
+        split(callback: (node: Node) => boolean): [Node[], Node[]];
+        map(callback: (node: Node) => Node): Node[];
+        reduce<T>(callback: (node: Node) => Node, memo: T): T;
+        every(callback: (node: Node) => boolean): boolean;
+        some(callback: (node: Node) => boolean): boolean;
+        filter(callback: (node: Node) => boolean): Node[];
+        sort(callback: (nodeA: Node, nodeB: Node) => number): Node[];
+        toString(): string;
+    }
+    function isContainer(node: any): node is Root | Selector | Pseudo;
+
+    interface NamespaceOptions extends NodeOptions {
+        /** Namespace prefix including the | */
+        ns?: string;
+        /** Namespace prefix excluding the | */
+        namespace?: string;
+    }
+    interface Namespace<Value extends string | undefined = string> extends Base<Value> {
+        /** Namespace prefix including the | */
+        readonly ns: string;
+        /** Namespace prefix excluding the | */
+        readonly namespace: string;
+    }
+    function isNamespace(node: any): node is ClassName | Attribute | Tag;
+
+    interface Root extends Container<undefined> {
+        type: "root";
+        /**
+         * Raises an error, if the processor is invoked on
+         * a postcss Rule node, a better error message is raised.
+         */
+        error(message: string, options?: ErrorOptions): Error;
+    }
+    function root(opts: ContainerOptions): Root;
+    function isRoot(node: any): node is Root;
+
+    interface Selector extends Container {
+        type: "selector";
+    }
+    function selector(opts: ContainerOptions): Selector;
+    function isSelector(node: any): node is Selector;
+
+    interface Combinator extends Base {
+        type: "combinator"
+    }
+    function combinator(opts: NodeOptions): Combinator;
+    function isCombinator(node: any): node is Combinator;
+
+    interface ClassName extends Namespace {
+        type: "class";
+    }
+    function className(opts: NamespaceOptions): ClassName;
+    function isClassName(node: any): node is ClassName;
+
+    interface AttributeOptions extends NodeOptions {
+        attribute: string;
+        operator: string;
+        insensitive?: boolean;
+        ns?: string;
+        raws?: {
+          insensitive?: boolean;
+        };
+    }
+    type AttributeOperator = "=" | "~=" | "|=" | "^=" | "$=" | "*=";
+    interface Attribute extends Namespace<string | undefined> {
+        type: "attribute";
+        attribute: string;
+        operator: AttributeOperator | undefined;
+        insensitive?: boolean;
+        raws: {
+          insensitive?: boolean;
+        };
+        toString(): string;
+    }
+    function attribute(opts: AttributeOptions): Attribute;
+    function isAttribute(node: any): node is Attribute;
+
+    interface Pseudo extends Container {
+        type: "pseudo";
+    }
+    function pseudo(opts: ContainerOptions): Pseudo;
+    /**
+     * Checks wether the node is the Psuedo subtype of node.
+     */
+    function isPseudo(node: any): node is Pseudo;
+
+    /**
+     * Checks wether the node is, specifically, a pseudo element instead of
+     * pseudo class.
+     */
+    function isPseudoElement(node: any): node is Pseudo;
+
+    /**
+     * Checks wether the node is, specifically, a pseudo class instead of
+     * pseudo element.
+     */
+    function isPseudoClass(node: any): node is Pseudo;
+
+
+    interface Tag extends Namespace {
+        type: "tag";
+    }
+    function tag(opts: NamespaceOptions): Tag;
+    function isTag(node: any): node is Tag;
+
+    interface Comment extends Base {
+        type: "comment";
+    }
+    function comment(opts: NodeOptions): Comment;
+    function isComment(node: any): node is Comment;
+
+    interface Identifier extends Base {
+        type: "id";
+    }
+    function id(opts: any): any;
+    function isIdentifier(node: any): node is Identifier;
+
+    interface Nesting extends Base {
+        type: "nesting";
+    }
+    function nesting(opts: any): any;
+    function isNesting(node: any): node is Nesting;
+
+    interface String extends Base {
+        type: "string";
+    }
+    function string(opts: NodeOptions): String;
+    function isString(node: any): node is String;
+
+    interface Universal extends Base {
+        type: "universal";
+    }
+    function universal(opts?: NamespaceOptions): any;
+    function isUniversal(node: any): node is Universal;
+}
diff --git a/src/index.js b/src/index.js
index 3198f24..4f49594 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,38 +1,10 @@
 import Processor  from './processor';
-import Attribute  from './selectors/attribute';
-import ClassName  from './selectors/className';
-import Combinator from './selectors/combinator';
-import Comment    from './selectors/comment';
-import Id         from './selectors/id';
-import Nesting    from './selectors/nesting';
-import Pseudo     from './selectors/pseudo';
-import Root       from './selectors/root';
-import Selector   from './selectors/selector';
-import Str        from './selectors/string';
-import Tag        from './selectors/tag';
-import Universal  from './selectors/universal';
-import * as types from './selectors/types';
+import * as selectors from './selectors';
 
 const parser = processor => new Processor(processor);
 
-parser.attribute = opts => new Attribute(opts);
-parser.className = opts => new ClassName(opts);
-parser.combinator = opts => new Combinator(opts);
-parser.comment = opts => new Comment(opts);
-parser.id = opts => new Id(opts);
-parser.nesting = opts => new Nesting(opts);
-parser.pseudo = opts => new Pseudo(opts);
-parser.root = opts => new Root(opts);
-parser.selector = opts => new Selector(opts);
-parser.string = opts => new Str(opts);
-parser.tag = opts => new Tag(opts);
-parser.universal = opts => new Universal(opts);
+Object.assign(parser, selectors);
 
-Object.keys(types).forEach(type => {
-    if (type === '__esModule') {
-        return;
-    }
-    parser[type] = types[type]; // eslint-disable-line
-});
+delete parser.__esModule;
 
 export default parser;
diff --git a/src/parser.js b/src/parser.js
index f275282..0044523 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -35,26 +35,39 @@ function getSource (startLine, startColumn, endLine, endColumn) {
 }
 
 export default class Parser {
-    constructor (input) {
-        this.input = input;
-        this.lossy = input.options.lossless === false;
+    constructor (rule, options = {}) {
+        this.rule = rule;
+        this.options = Object.assign({lossy: false, safe: false}, options);
         this.position = 0;
         this.root = new Root();
+        this.root.errorGenerator = this._errorGenerator();
+
 
         const selector = new Selector();
         this.root.append(selector);
         this.current = selector;
 
-        this.css = typeof input.css === 'string' ? input.css : input.css.selector;
+        this.css = typeof this.rule === 'string' ? this.rule : this.rule.selector;
 
-        if (this.lossy) {
+        if (this.options.lossy) {
             this.css = this.css.trim();
-            this.tokens = tokenize({safe: input.safe, css: this.css});
-        } else {
-            this.tokens = tokenize(Object.assign({}, input, {css: this.css}));
         }
+        this.tokens = tokenize({
+            css: this.css,
+            error: this._errorGenerator(),
+            safe: this.options.safe,
+        });
 
-        return this.loop();
+        this.loop();
+    }
+
+    _errorGenerator () {
+        return (message, errorOptions) => {
+            if (typeof this.rule === 'string') {
+                return new Error(message);
+            }
+            return this.rule.error(message, errorOptions);
+        };
     }
 
     attribute () {
@@ -105,7 +118,7 @@ export default class Parser {
                 ) {
                     return this.expected('attribute', token[5], content);
                 }
-                if (this.lossy) {
+                if (this.options.lossy) {
                     break;
                 }
                 if (!lastAdded || this.content(next) === 'i') {
@@ -167,7 +180,7 @@ export default class Parser {
                     dotProp.set(node, 'raws.unquoted', content);
                 } else if (content === 'i') {
                     node.insensitive = true;
-                    if (!this.lossy) {
+                    if (!this.options.lossy) {
                         lastAdded = 'raws.insensitive';
                         dotProp.set(node, lastAdded, `${spaceBefore}${content}`);
                         spaceBefore = '';
@@ -280,7 +293,7 @@ export default class Parser {
     }
 
     error (message, opts) {
-        throw new this.input.error(message, opts); // eslint-disable-line new-cap
+        throw this.root.error(message, opts);
     }
 
     missingBackslash () {
@@ -619,7 +632,7 @@ export default class Parser {
     }
 
     parseNamespace (namespace) {
-        if (this.lossy && typeof namespace === 'string') {
+        if (this.options.lossy && typeof namespace === 'string') {
             const trimmed = namespace.trim();
             if (!trimmed.length) {
                 return true;
@@ -632,11 +645,11 @@ export default class Parser {
     }
 
     parseSpace (space, replacement = '') {
-        return this.lossy ? replacement : space;
+        return this.options.lossy ? replacement : space;
     }
 
     parseValue (value) {
-        if (!this.lossy || !value || typeof value !== 'string') {
+        if (!this.options.lossy || !value || typeof value !== 'string') {
             return value;
         }
         return value.trim();
@@ -644,7 +657,7 @@ export default class Parser {
 
     parseParenthesisToken (token) {
         const content = this.content(token);
-        if (!this.lossy) {
+        if (!this.options.lossy) {
             return content;
         }
 
diff --git a/src/processor.js b/src/processor.js
index eacd794..0bd53d9 100644
--- a/src/processor.js
+++ b/src/processor.js
@@ -1,38 +1,139 @@
 import Parser from './parser';
 
 export default class Processor {
-    constructor (func) {
+    constructor (func, options) {
         this.func = func || function noop () {};
         this.funcRes = null;
-        return this;
-    }
-
-    _input (rule, options = {}) {
-        return new Parser({
-            css: rule,
-            error: (e, opts) => {
-                if (typeof rule === 'string') {
-                    throw new Error(e);
-                }
-                throw rule.error(e, opts); // eslint-disable-line new-cap
-            },
-            options,
+        this.options = options;
+    }
+
+    _shouldUpdateSelector (rule, options = {}) {
+        let merged = Object.assign({}, this.options, options);
+        if (merged.updateSelector === false) {
+            return false;
+        } else {
+            return typeof rule !== "string";
+        }
+    }
+
+    _isLossy (options = {}) {
+        let merged = Object.assign({}, this.options, options);
+        if (merged.lossless === false) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    _root (rule, options = {}) {
+        let parser = new Parser(rule, this._parseOptions(options));
+        return parser.root;
+    }
+
+    _parseOptions (options) {
+        return {
+            lossy: this._isLossy(options),
+        };
+    }
+
+    _run (rule, options = {}) {
+        return new Promise((resolve, reject) => {
+            try {
+                let root = this._root(rule, options);
+                Promise.resolve(this.func(root)).then(transform => {
+                    let string = undefined;
+                    if (this._shouldUpdateSelector(rule, options)) {
+                        string = root.toString();
+                        rule.selector = string;
+                    }
+                    return {transform, root, string};
+                }).then(resolve, reject);
+            } catch (e) {
+                reject(e);
+                return;
+            }
         });
     }
 
-    process (rule, options) {
-        let input;
-        try {
-            input = this._input(rule, options);
-        } catch (e) {
-            return Promise.reject(e);
+    _runSync (rule, options = {}) {
+        let root = this._root(rule, options);
+        let transform = this.func(root);
+        if (transform && typeof transform.then === "function") {
+            throw new Error("Selector processor returned a promise to a synchronous call.");
         }
-        return Promise.resolve(this.func(input)).then(() => String(input));
+        let string = undefined;
+        if (options.updateSelector && typeof rule !== "string") {
+            string = root.toString();
+            rule.selector = string;
+        }
+        return {transform, root, string};
+    }
+
+    /**
+     * Process rule into a selector AST.
+     * 
+     * @param rule {postcss.Rule | string} The css selector to be processed
+     * @param options The options for processing
+     * @returns {Promise<parser.Root>} The AST of the selector after processing it.
+     */
+    ast (rule, options) {
+        return this._run(rule, options).then(result => result.root);
+    }
+
+    /**
+     * Process rule into a selector AST synchronously.
+     * 
+     * @param rule {postcss.Rule | string} The css selector to be processed
+     * @param options The options for processing
+     * @returns {parser.Root} The AST of the selector after processing it.
+     */
+    astSync (rule, options) {
+        return this._runSync(rule, options).root;
+    }
+
+    /**
+     * Process a selector into a transformed value asynchronously
+     * 
+     * @param rule {postcss.Rule | string} The css selector to be processed
+     * @param options The options for processing
+     * @returns {Promise<any>} The value returned by the processor.
+     */
+    transform (rule, options) {
+        return this._run(rule, options).then(result => result.transform);
+    }
+
+    /**
+     * Process a selector into a transformed value synchronously.
+     * 
+     * @param rule {postcss.Rule | string} The css selector to be processed
+     * @param options The options for processing
+     * @returns {any} The value returned by the processor.
+     */
+    transformSync (rule, options) {
+        return this._runSync(rule, options).transform;
+    }
+
+    /**
+     * Process a selector into a new selector string asynchronously.
+     * 
+     * @param rule {postcss.Rule | string} The css selector to be processed
+     * @param options The options for processing
+     * @returns {string} the selector after processing.
+     */
+    process (rule, options) {
+        return this._run(rule, options)
+            .then((result) => result.string || result.root.toString());
     }
 
-    processSync (rule, options = {}) {
-        const input = this._input(rule, options);
-        this.func(input);
-        return String(input);
+    /**
+     * Process a selector into a new selector string synchronously.
+     * 
+     * @param rule {postcss.Rule | string} The css selector to be processed
+     * @param options The options for processing
+     * @returns {string} the selector after processing.
+     */
+    processSync (rule, options) {
+        let result = this._runSync(rule, options);
+        return result.string || result.root.toString();
     }
 }
diff --git a/src/selectors/constructors.js b/src/selectors/constructors.js
new file mode 100644
index 0000000..7a55257
--- /dev/null
+++ b/src/selectors/constructors.js
@@ -0,0 +1,25 @@
+import Attribute  from './attribute';
+import ClassName  from './className';
+import Combinator from './combinator';
+import Comment    from './comment';
+import Id         from './id';
+import Nesting    from './nesting';
+import Pseudo     from './pseudo';
+import Root       from './root';
+import Selector   from './selector';
+import Str        from './string';
+import Tag        from './tag';
+import Universal  from './universal';
+
+export const attribute = opts => new Attribute(opts);
+export const className = opts => new ClassName(opts);
+export const combinator = opts => new Combinator(opts);
+export const comment = opts => new Comment(opts);
+export const id = opts => new Id(opts);
+export const nesting = opts => new Nesting(opts);
+export const pseudo = opts => new Pseudo(opts);
+export const root = opts => new Root(opts);
+export const selector = opts => new Selector(opts);
+export const string = opts => new Str(opts);
+export const tag = opts => new Tag(opts);
+export const universal = opts => new Universal(opts);
diff --git a/src/selectors/guards.js b/src/selectors/guards.js
new file mode 100644
index 0000000..049944f
--- /dev/null
+++ b/src/selectors/guards.js
@@ -0,0 +1,71 @@
+import {
+    ATTRIBUTE,
+    CLASS,
+    COMBINATOR,
+    COMMENT,
+    ID,
+    NESTING,
+    PSEUDO,
+    ROOT,
+    SELECTOR,
+    STRING,
+    TAG,
+    UNIVERSAL,
+} from "./types";
+
+const IS_TYPE = {
+    [ATTRIBUTE]: true,
+    [CLASS]: true,
+    [COMBINATOR]: true,
+    [COMMENT]: true,
+    [ID]: true,
+    [NESTING]: true,
+    [PSEUDO]: true,
+    [ROOT]: true,
+    [SELECTOR]: true,
+    [STRING]: true,
+    [TAG]: true,
+    [UNIVERSAL]: true,
+};
+
+export function isNode (node) {
+    return (typeof node === "object" && IS_TYPE[node.type]);
+}
+
+function isNodeType (type, node) {
+    return isNode(node) && node.type === type;
+}
+
+export const isAttribute = isNodeType.bind(null, ATTRIBUTE);
+export const isClassName = isNodeType.bind(null, CLASS);
+export const isCombinator = isNodeType.bind(null, COMBINATOR);
+export const isComment = isNodeType.bind(null, COMMENT);
+export const isIdentifier = isNodeType.bind(null, ID);
+export const isNesting = isNodeType.bind(null, NESTING);
+export const isPseudo = isNodeType.bind(null, PSEUDO);
+export const isRoot = isNodeType.bind(null, ROOT);
+export const isSelector = isNodeType.bind(null, SELECTOR);
+export const isString = isNodeType.bind(null, STRING);
+export const isTag = isNodeType.bind(null, TAG);
+export const isUniversal = isNodeType.bind(null, UNIVERSAL);
+
+export function isPseudoElement (node) {
+    return isPseudo(node)
+           && node.value
+           && (
+             node.value.startsWith("::")
+             || node.value === ":before"
+             || node.value === ":after"
+           );
+}
+export function isPseudoClass (node) {
+    return isPseudo(node) && !isPseudoElement(node);
+}
+
+export function isContainer (node) {
+    return !!(isNode(node) && node.walk);
+}
+
+export function isNamespace (node) {
+    return isClassName(node) || isAttribute(node) || isTag(node);
+}
diff --git a/src/selectors/index.js b/src/selectors/index.js
new file mode 100644
index 0000000..3dcfa8e
--- /dev/null
+++ b/src/selectors/index.js
@@ -0,0 +1,3 @@
+export * from "./types";
+export * from "./constructors";
+export * from "./guards";
diff --git a/src/selectors/root.js b/src/selectors/root.js
index 6f8574f..fc9de0a 100644
--- a/src/selectors/root.js
+++ b/src/selectors/root.js
@@ -14,4 +14,16 @@ export default class Root extends Container {
         }, '').slice(0, -1);
         return this.trailingComma ? str + ',' : str;
     }
+
+    error (message, options) {
+        if (this._error) {
+            return this._error(message, options);
+        } else {
+            return new Error(message);
+        }
+    }
+
+    set errorGenerator (handler) {
+        this._error = handler;
+    }
 }
diff --git a/src/tokenize.js b/src/tokenize.js
index 20aa10b..a723f57 100644
--- a/src/tokenize.js
+++ b/src/tokenize.js
@@ -26,7 +26,7 @@ export default function tokenize (input) {
         tokenType;
 
     function unclosed (what, fix) {
-        if ( input.safe ) {
+        if ( input.safe ) { // fyi: this is never set to true.
             css += fix;
             next = css.length - 1;
         } else {


Test patch:
diff --git a/src/__tests__/guards.js b/src/__tests__/guards.js
new file mode 100644
index 0000000..d454b96
--- /dev/null
+++ b/src/__tests__/guards.js
@@ -0,0 +1,118 @@
+import parser from '../';
+import {test} from './util/helpers';
+
+const node = (tree, n = 0) => tree.nodes[0].nodes[n];
+
+test('attribute guard', '[foo]', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isAttribute(undefined));
+    t.true(parser.isAttribute(n));
+    t.false(parser.isContainer(n));
+    t.true(parser.isNamespace(n));
+});
+
+test('className guard', '.foo', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isClassName(undefined));
+    t.true(parser.isClassName(n));
+    t.false(parser.isContainer(n));
+    t.true(parser.isNamespace(n));
+});
+
+test('combinator guard', '.foo > .bar', (t, tree) => {
+    let n = node(tree, 1);
+    t.true(parser.isNode(n));
+    t.false(parser.isCombinator(undefined));
+    t.true(parser.isCombinator(n));
+    t.false(parser.isContainer(n));
+    t.false(parser.isNamespace(n));
+});
+
+test('comment guard', '/* foo */.foo > .bar', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isComment(undefined));
+    t.true(parser.isComment(n));
+    t.false(parser.isContainer(n));
+    t.false(parser.isNamespace(n));
+});
+
+test('id guard', '#ident', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isIdentifier(undefined));
+    t.true(parser.isIdentifier(n));
+    t.false(parser.isContainer(n));
+    t.false(parser.isNamespace(n));
+});
+
+test('nesting guard', '&.foo', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isNesting(undefined));
+    t.true(parser.isNesting(n));
+    t.false(parser.isContainer(n));
+    t.false(parser.isNamespace(n));
+});
+
+test('pseudo class guard', ':hover', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isPseudo(undefined));
+    t.true(parser.isPseudo(n));
+    t.true(parser.isPseudoClass(n));
+    t.false(parser.isPseudoElement(n));
+    t.true(parser.isContainer(n));
+    t.false(parser.isNamespace(n));
+});
+
+test('pseudo element guard', '::first-line', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isPseudo(undefined));
+    t.true(parser.isPseudo(n));
+    t.false(parser.isPseudoClass(n));
+    t.true(parser.isPseudoElement(n));
+    t.true(parser.isContainer(n));
+    t.false(parser.isNamespace(n));
+});
+
+test('special pseudo element guard', ':before:after', (t, tree) => {
+    [node(tree), node(tree, 1)].forEach((n) => {
+        t.true(parser.isPseudo(n));
+        t.false(parser.isPseudoClass(n));
+        t.true(parser.isPseudoElement(n));
+        t.true(parser.isContainer(n));
+        t.false(parser.isNamespace(n));
+    });
+});
+
+test('string guard', '"string"', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isString(undefined));
+    t.true(parser.isString(n));
+    t.false(parser.isContainer(n));
+    t.false(parser.isNamespace(n));
+});
+
+test('tag guard', 'h1', (t, tree) => {
+    let n = node(tree);
+    t.false(parser.isNode(undefined));
+    t.true(parser.isNode(n));
+    t.false(parser.isTag(undefined));
+    t.true(parser.isTag(n));
+    t.false(parser.isContainer(n));
+    t.true(parser.isNamespace(n));
+});
+
+test('universal guard', '*', (t, tree) => {
+    let n = node(tree);
+    t.true(parser.isNode(n));
+    t.false(parser.isUniversal(undefined));
+    t.true(parser.isUniversal(n));
+    t.false(parser.isContainer(n));
+    t.false(parser.isNamespace(n));
+});
diff --git a/src/__tests__/parser.js b/src/__tests__/parser.js
index 53adce0..55d391d 100644
--- a/src/__tests__/parser.js
+++ b/src/__tests__/parser.js
@@ -78,3 +78,132 @@ test('parse errors with the async parser', (t) => {
         }, 1);
     })).process('a b: c').catch(err => t.truthy(err));
 });
+
+test('parse errors within the async processor', (t) => {
+    return parser((selectors) => new Promise((res, rej) => {
+        setTimeout(() => {
+            rej(selectors.error("async error"));
+        }, 1);
+    })).process('.foo').catch(err => t.truthy(err));
+});
+
+test('parse errors within the async processor before the promise returns', (t) => {
+    return parser((selectors) => {
+        throw selectors.error("async error");
+    }).process('.foo').catch(err => t.truthy(err));
+});
+
+test('returning a promise to the sync processor fails', (t) => {
+    t.throws(() => {
+        return parser(() => new Promise((res) => {
+            setTimeout(() => {
+                res();
+            }, 1);
+        })).processSync('.foo');
+    });
+});
+
+test('Passing a rule works async', (t) => {
+    let rule = {selector: '.foo'};
+    return parser((root) => new Promise((res) => {
+        setTimeout(() => {
+            root.walkClasses((node) => {
+                node.value = "bar";
+            });
+            res();
+        }, 1);
+    })).process(rule)
+    .then(newSel => {
+        t.deepEqual(newSel, ".bar");
+        t.deepEqual(rule.selector, ".bar");
+    });
+});
+
+test('Passing a rule with mutation disabled works async', (t) => {
+    let rule = {selector: '.foo'};
+    return parser((root) => new Promise((res) => {
+        setTimeout(() => {
+            root.walkClasses((node) => {
+                node.value = "bar";
+            });
+            res();
+        }, 1);
+    })).process(rule, {updateSelector: false})
+    .then(newSel => {
+        t.deepEqual(newSel, ".bar");
+        t.deepEqual(rule.selector, ".foo");
+    });
+});
+
+test('Passing a rule with mutation works sync', (t) => {
+    let rule = {selector: '.foo'};
+    let newSel = parser((root) => {
+        root.walkClasses((node) => {
+            node.value = "bar";
+        });
+    }).processSync(rule, {updateSelector: true});
+    t.deepEqual(newSel, ".bar");
+    t.deepEqual(rule.selector, ".bar");
+});
+
+test('Transform a selector synchronously', (t) => {
+    let rule = {selector: '.foo'};
+    let count = parser((root) => {
+        let classCount = 0;
+        root.walkClasses((node) => {
+            classCount++;
+            node.value = "bar";
+        });
+        return classCount;
+    }).transformSync(rule, {updateSelector: true});
+    t.deepEqual(count, 1);
+    t.deepEqual(rule.selector, ".bar");
+});
+
+test('Transform a selector asynchronously', (t) => {
+    let rule = {selector: '.foo'};
+    return parser((root) => new Promise(res => {
+        setTimeout(() => {
+            let classCount = 0;
+            root.walkClasses((node) => {
+                classCount++;
+                node.value = "bar";
+            });
+            res(classCount);
+        }, 1);
+    })).transform(rule, {updateSelector: true}).then(count => {
+        t.deepEqual(count, 1);
+        t.deepEqual(rule.selector, ".bar");
+    });
+});
+
+test('get AST of a selector synchronously', (t) => {
+    let rule = {selector: '.foo'};
+    let ast = parser((root) => {
+        let classCount = 0;
+        root.walkClasses((node) => {
+            classCount++;
+            node.value = "bar";
+        });
+        return classCount;
+    }).astSync(rule, {updateSelector: true});
+    t.deepEqual(ast.nodes[0].nodes[0].value, "bar");
+    t.deepEqual(rule.selector, ".bar");
+});
+
+test('get AST a selector asynchronously', (t) => {
+    let rule = {selector: '.foo'};
+    return parser((root) => new Promise(res => {
+        setTimeout(() => {
+            let classCount = 0;
+            root.walkClasses((node) => {
+                classCount++;
+                node.value = "bar";
+            });
+            res(classCount);
+        }, 1);
+    })).ast(rule, {updateSelector: true}).then(ast => {
+        t.deepEqual(ast.nodes[0].nodes[0].value, "bar");
+        t.deepEqual(rule.selector, ".bar");
+    });
+});