Permalink
2c1a8fb Jan 8, 2017
637 lines (507 sloc) 18.2 KB

Creating Rules

textlint's AST(Abstract Syntax Tree) is defined these pages.

Each rules are represented by a object with some properties. The properties are equivalent to AST node types from TxtNode.

The basic source code format for a rule is:

/**
 * @param {RuleContext} context
 */
export default function (context) {
    // rule object
    return {
        [context.Syntax.Document](node) {
        },

        [context.Syntax.Paragraph](node) {
        },

        [context.Syntax.Str](node) {
            var text = context.getSource(node);
            if(/found wrong use-case/.test(text)){
                // report error
                context.report(node, new context.RuleError("Found wrong"));
            }
        },

        [context.Syntax.Break](node) {
        }
    };
}

If your rule wants to know when an Str node is found in the AST, then add a method called context.Syntax.Str, such as:

// ES6
export default function (context) {
    return {
        [context.Syntax.Str](node) {
            // this method is called
        }
    };
}
// or ES5
module.exports = function (context) {
    var exports = {};
    exports[context.Syntax.Str] = function (node) {
        // this method is called
    };
    return exports;
};

By default, the method matching a node name is called during the traversal when the node is first encountered(This is called Enter), on the way down the AST.

You can also specify to visit the node on the other side of the traversal, as it comes back up the tree(This is called Leave), but adding :exit to the end of the node type, such as:

export default function (context) {
    return {
        // Str:exit
        [`${context.Syntax.Str}:exit`](node) {
            // this method is called
        }
    };
}

visualize-txt-traverse help you better understand this traversing.

gif visualize-txt-traverse

AST explorer for textlint help you better understand TxtAST.

ast-explorer fork

Related information:

RuleContext

RuleContext object has following property:

  • Syntax.* is const values of TxtNode type.
  • report(<node>, <ruleError>) is a method that reports a message from one of the rules.
    • e.g.) context.report(node, new context.RuleError("found rule error"));
  • getSource(<node>) is a method gets the source code for the given node.
    • e.g.) context.getSource(node); // => "text"
  • getFilePath() return file path that is linting target.
    • e.g.) context.getFilePath(): // => /path/to/file.md or undefined
  • fixer is creator of fix command.

RuleError

RuleError is a object like Error. Use it with report function.

  • RuleError(<message>, [{ line , column }])
    • e.g.) new context.RuleError("Found rule error");
    • e.g.) new context.RuleError("Found rule error", { line: paddingLine, column: paddingColumn});
  • RuleError(<message>, [{ index }])
    • e.g.) new context.RuleError("Found rule error", { index: paddingIndex });
// No padding information
var error = new RuleError("message");
// 
// OR
// add location-based padding
var paddingLine = 1;
var paddingColumn = 1;
var errorWithPadding = new RuleError("message", {
    line: paddingLine, // padding line number from node.loc.start.line. default: 0
    column: paddingColumn // padding column number from node.loc.start.column. default: 0
});
// context.report(node, errorWithPadding);
//
// OR
// add index-based padding
var paddingIndex = 1;
var errorWithPaddingIndex = new RuleError("message", {
    index: paddingIndex // padding index value from `node.range[0]`. default: 0
});
// context.report(node, errorWithPaddingIndex);

⚠️ Caution ⚠️

  • index could not used with line and column.
    • It means that to use { line, column } or { index }
  • index, line, column is a relative value from the node which is reported.

Report error

You will use mainly method is context.report(), which publishes a error (defined in each rules).

For example:

export default function (context) {
    return {
        [context.Syntax.Str](node) {
            // get source code of this `node`
            var text = context.getSource(node);
            if(/found wrong use-case/.test(text)){
                // report error
                context.report(node, new context.RuleError("Found wrong"));
            }
        }
    };
}

How to write async task in the rule

Return Promise object in the node function and the rule work asynchronously.

export default function (context) {
    const {Syntax} = context;
    return {
        [Syntax.Str](node){
            // textlint wait for resolved the promise.
            return new Promise((resolve, reject) => {
                // async task
            });
        }
    };
}

Example: creating no-todo rules.

This example aim to create no-todo rule that throw error if the text includes - [ ] or todo:.

Setup for creating rule

textlint prepare useful generator tool that is create-textlint-rule command.

You can setup textlint rule by following steps:

npm install create-textlint-rule -g
# Install `create-textlint-rule` command
create-textlint-rule no-todo
# Create `textlint-rule-no-todo` project and setup!
# rm src/index.js test/index-tes.js

This generated project contains textlint-scripts that provide build script and test script.

Build

Builds source codes for publish to the lib/ folder. You can write ES2015+ source codes in src/ folder. The source codes in src/ built by following command.

npm run build

Tests

Run test code in test/ folder. Test textlint rule by textlint-tester.

npm test

Let's create no-todo rule

File Name: no-todo.js

/**
 * @param {RuleContext} context
 */
export default function (context) {
    const helper = new RuleHelper(context);
    const {Syntax, getSource, RuleError, report} = context;
    return {
        /*
            # Header
            Todo: quick fix this.
            ^^^^^
            Hit!
        */
        [Syntax.Str](node) {
            // get text from node
            const text = getSource(node);
            // does text contain "todo:"?
            const match = text.match(/todo:/i);
            if (match) {
                report(node, new RuleError(`Found TODO: '${text}'`, {
                    index: match.index
                }));
            }
        },
        /*
            # Header
            - [ ] Todo
              ^^^
              Hit!
        */
        [Syntax.ListItem](node) {
            var text = context.getSource(node);
            var match = text.match(/\[\s+\]\s/i);
            if (match) {
                report(node, new context.RuleError(`Found TODO: '${text}'`, {
                    index: match.index
                }));
            }
        }
    };
}

Example text:

# Header

this is Str.

Todo: quick fix this.

- list 1
- [ ] todo

Run Lint!

$ npm run build
$ textlint --rulesdir lib/ README.md -f pretty-error

result error

Advanced rules

When linting following text with above no-todo rule, a result was error.

[todo:image](http://example.com)

Case: ignore child node of Link, Image or BlockQuote.

You want to ignore this case, and write the following:

/**
 * Get parents of node.
 * The parent nodes are returned in order from the closest parent to the outer ones.
 * @param node
 * @returns {Array}
 */
function getParents(node) {
    var result = [];
    // child node has `parent` property.
    var parent = node.parent;
    while (parent != null) {
        result.push(parent);
        parent = parent.parent;
    }
    return result;
}
/**
 * Return true if `node` is wrapped any one of `types`.
 * @param {TxtNode} node is target node
 * @param {string[]} types are wrapped target node
 * @returns {boolean|*}
 */
function isNodeWrapped(node, types) {
    var parents = getParents(node);
    var parentsTypes = parents.map(function (parent) {
        return parent.type;
    });
    return types.some(function (type) {
        return parentsTypes.some(function (parentType) {
            return parentType === type;
        });
    });
}
/**
 * @param {RuleContext} context
 */
export default function (context) {
    const {Syntax, getSource, RuleError, report} = context;
    return {
        /*
            # Header
            Todo: quick fix this.
        */
        [Syntax.Str](node) {
            // not apply this rule to the node that is child of `Link`, `Image` or `BlockQuote` Node.
            if (isNodeWrapped(node, [Syntax.Link, Syntax.Image, Syntax.BlockQuote])) {
                return;
            }
            // get text from node
            const text = getSource(node);
            // does text contain "todo:"?
            const match = text.match(/todo:/i);
            if (match) {
                const todoText = text.substring(match.index);
                report(node, new RuleError(`Found TODO: '${todoText}'`, {
                    // correct position
                    index: match.index
                }));
            }
        },
        /*
            # Header
            - [ ] Todo
        */
        [Syntax.ListItem](node) {
            const text = context.getSource(node);
            const match = text.match(/\[\s+\]\s/i);
            if (match) {
                report(node, new context.RuleError(`Found TODO: '${text}'`, {
                    index: match.index
                }));
            }
        }
    };
}

As as result, linting following text with modified rule, a result was no error.

[todo:image](http://example.com)

How to test the rule?

You can already run test by npm test command. (This test scripts is setup by create-textlint-rule)

This test script use textlint-tester.


Manually Installation

textlint-tester depend on Mocha.

npm install -D textlint-tester mocha

Usage of textlint-tester

  1. Write tests by using textlint-tester
  2. Run tests by Mocha

test/textlint-rule-no-todo-test.js:

const TextLintTester = require("textlint-tester");
const tester = new TextLintTester();
// rule
import rule from "../src/no-todo";
// ruleName, rule, { valid, invalid }
tester.run("no-todo", rule, {
    valid: [
        // no match
        "text",
        // partial match
        "TODOS:",
        // ignore node's type
        "[TODO: this is todo](http://example.com)",
        "![TODO: this is todo](http://example.com/img)",
        "> TODO: this is todo"

    ],
    invalid: [
        // single match
        {
            text: "TODO: this is TODO",
            errors: [
                {
                    message: "Found TODO: 'TODO: this is TODO'",
                    line: 1,
                    column: 1
                }
            ]
        },
        // multiple match in multiple lines
        {
            text: `TODO: this is TODO

- [ ] TODO`,
            errors: [
                {
                    message: "Found TODO: 'TODO: this is TODO'",
                    line: 1,
                    column: 1
                },
                {
                    message: "Found TODO: '- [ ] TODO'",
                    line: 3,
                    column: 3
                }
            ]
        },
        // multiple hit items in a line
        {
            text: "TODO: A TODO: B",
            errors: [
                {
                    message: "Found TODO: 'TODO: A TODO: B'",
                    line: 1,
                    column: 1
                }
            ]
        },
        // exact match or empty
        {
            text: "THIS IS TODO:",
            errors: [
                {
                    message: "Found TODO: 'TODO:'",
                    line: 1,
                    column: 9
                }
            ]
        }
    ]
});

Run the tests:

$ npm test
# or
$(npm bin)/mocha test/

ℹ️ Please see azu/textlint-rule-no-todo for details.

Rule Config

.textlintrc is the config file for textlint.

For example, there are a config file:

{
  "rules": {
    "very-nice-rule": {
        "key": "value"
    }
  }
}

very-nice-rule.js rule get the options defined by the config file.

export default function(context, options){
    console.log(options);
    /*
        {
          "key": "value"
        }
    */
}

Advanced example

If you want to know more details, please see other example.

Information for Publishing

You should add textlintrule to npm's keywords

{
  "name": "textlint-rule-no-todo",
  "description": "Your custom rules description",
  "version": "1.0.1",
  "homepage": "https://github.com/textlint/textlint-custom-rules/",
  "keywords": [
    "textlintrule"
  ]
}

Package Naming conventions

textlint's rule should use textlint-rule- prefix.

e.g.) textlint-rule-no-todo

textlint user use it following:

{
    "rules": {
        "no-todo": true
    }
}

The rule naming conventions for textlint are simple:

  • If your rule is disallowing something, prefix it with no-.
    • For example, no-todo disallowing TODO: and no-exclamation-question-mark for disallowing ! and ?.
  • If your rule is enforcing the inclusion of something, use a short name without a special prefix.
    • If the rule for english, please uf textlint-rule-en- prefix.
  • Keep your rule names as short as possible, use abbreviations where appropriate.
  • Use dashes(-) between words.

npm info:

Example rules:

Reference

FAQ: Publishing

Q. textlint @ 5.5.x has new feature. My rule module want to use it.

A. You should

Q. textlint does major update. Do my rule module major update?

A. If the update contain breaking change, should update as major. if not, update as major or minor.

Performance

Rule Performance

textlint has a built-in method to track performance of individual rules. Setting the TIMING=1 environment variable will trigger the display. It show their individual running time and relative performance impact as a percentage of total rule processing time.

$ TIMING=1 textlint README.md
Rule                            | Time (ms) | Relative
:-------------------------------|----------:|--------:
spellcheck-tech-word            |   124.277 |    70.7%
prh                             |    18.419 |    10.5%
no-mix-dearu-desumasu           |    13.965 |     7.9%
max-ten                         |    13.246 |     7.5%
no-start-duplicated-conjunction |     5.911 |     3.4%

Implementation Node 📝

textlint ignore duplicated message/rules by default.