Skip to content

Latest commit

 

History

History
706 lines (565 loc) · 20.3 KB

rule.md

File metadata and controls

706 lines (565 loc) · 20.3 KB
id title
rule
Creating Rules

textlint's AST(Abstract Syntax Tree) is defined at the page.

Each rules are represented by an 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) {
            const 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) {
    const 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.StrExit](node) {
            // this method is called
        }
    };
}

Note: textlint@11.1.1+ support *Exit constant value like Syntax.DocumentExit. In textlint@11.1.0<=, you had to write [Syntax.Document + ":exit"].

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 API

RuleContext object has following property:

  • Syntax.*
  • report(<node>, <ruleError>): void
    • This method 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>): string
    • This method is a method gets the source code for the given node.
    • e.g.) context.getSource(node); // => "text"
  • getFilePath(): string | undefined
    • This method return file path that is linting target.
    • e.g.) context.getFilePath(): // => /path/to/file.md or undefined
  • getConfigBaseDir(): string | undefined (New in 9.0.0)
    • Available @textlint/get-config-base-dir polyfill for backward compatibility
    • This method return config base directory path that is the place of .textlintrc
    • e.g.) /path/to/dir/.textlintrc
    • getConfigBaseDir() return "/path/to/dir/".
  • fixer

RuleError

RuleError is an 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
const error = new RuleError("message");
//
// OR
// add location-based padding
const paddingLine = 1;
const paddingColumn = 1;
const 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
const paddingIndex = 1;
const 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 an error (defined in each rules).

For example:

export default function (context) {
    return {
        [context.Syntax.Str](node) {
            // get source code of this `node`
            const 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 using npx that is included in npm:

# Create `textlint-rule-no-todo` project and setup!
npx create-textlint-rule no-todo

Or use npm install command:

# Install `create-textlint-rule` to global
npm install --global create-textlint-rule 
# Create `textlint-rule-no-todo` project and setup!
create-textlint-rule no-todo

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

📝 If you want to write TypeScript, Pass --typescript flag to create-textlint-rule. For more details, see create-textlint-rule's README.

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) {
            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
                    })
                );
            }
        }
    };
}

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) {
    const result = [];
    // child node has `parent` property.
    let 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) {
    const parents = getParents(node);
    const 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 a 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 options

.textlintrc is the config file for textlint.

For example, very-nice-rule's option is { "key": "value" } in .textlintrc

{
  "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"
        }
    */
}

The options value is {} (empty object) by default.

For example, very-nice-rule's option is true (enable the rule) in .textlintrc

{
  "rules": {
    "very-nice-rule": true
  }
}

very-nice-rule.js rule get {} (empty object) as options.

export default function (context, options) {
    console.log(options); // {}
}

History: This behavior is changed in textlint@11.

Advanced example

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

Publishing

If you want to publish your textlint rule, see following documents.

Package Naming Conventions

textlint rule package naming should have textlint-rule- prefix.

  • textlint-rule-<name>
  • @scope/textlint-rule-<name>

Example: textlint-rule-no-todo

textlint user use it following:

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

Example: @scope/textlint-rule-awesome

textlint user use it following:

{
    "rules": {
        "@scope/awesome": true
    }
}

Rule Naming Conventions

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 information:

Example rules:

Keywords

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"
  ]
}

FAQ: Publishing

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

A. You should

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

A. If the update contains a breaking change on your rule, should update as major. If the update does not contain a breaking change on your rule, update as 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.