id | title |
---|---|
rule |
Creating Rules |
textlint's AST(Abstract Syntax Tree) is defined at the page.
- txtnode.md
- If you want to know AST of a text, use Online Parsing Demo
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: [email protected]+ support *Exit
constant value like Syntax.DocumentExit
.
In [email protected]<=, you had to write [Syntax.Document + ":exit"]
.
visualize-txt-traverse help you better understand this traversing.
AST explorer for textlint help you better understand TxtAST.
Related information:
RuleContext object has following property:
Syntax.*
- This is const values of TxtNode type.
- e.g.)
context.Syntax.Str
- packages/@textlint/ast-node-types/src/index.ts
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
- This is creator object of fix command.
- See How to create Fixable Rule? for details
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});
- e.g.)
RuleError(<message>, [{ index }])
- e.g.)
new context.RuleError("Found rule error", { index: paddingIndex });
- e.g.)
// 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);
index
could not used withline
andcolumn
.- It means that to use
{ line, column }
or{ index }
- It means that to use
index
,line
,column
is a relative value from thenode
which is reported.
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"));
}
}
};
}
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
});
}
};
}
This example aim to create no-todo
rule that throw error if the text includes - [ ]
or todo:
.
textlint prepare useful generator tool that is create-textlint-rule command.
- textlint/create-textlint-rule: Create textlint rule project with no configuration.
- textlint/textlint-scripts: textlint npm-run-scripts CLI help to create textlint rule.
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.
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
Run test code in test/
folder.
Test textlint rule by textlint-tester.
npm test
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
When linting following text with above no-todo
rule, a result was error.
[todo:image](http://example.com)
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)
- The created rule is textlint-rule-no-todo.
- These helper functions like
getParents
are implemented in textlint/textlint-rule-helper.
You can already run test by npm test
command.
(This test scripts is setup by create-textlint-rule
)
This test script use textlint-tester.
textlint-tester depend on Mocha.
npm install -D textlint-tester mocha
- Write tests by using textlint-tester
- 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.
.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.
If you want to know more details, please see other example.
If you want to publish your textlint rule, see following documents.
textlint rule package naming should have textlint-rule-
prefix.
textlint-rule-<name>
@scope/textlint-rule-<name>
- textlint supports Scoped packages
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
}
}
The rule naming conventions for textlint are simple:
- If your rule is disallowing something, prefix it with
no-
.- For example,
no-todo
disallowingTODO:
andno-exclamation-question-mark
for disallowing!
and?
.
- For example,
- 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.
- If the rule for english, please uf
- Keep your rule names as short as possible, use abbreviations where appropriate.
- Use dashes(
-
) between words.
npm information:
Example rules:
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"
]
}
A. You should
- Add
textlint >= 5.5
topeerDependencies
- See example: textlint-rule-no-todo/package.json
- Release the rule package as major because it has breaking change.
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.
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%
textlint ignore duplicated message/rules by default.
- If already the rule with config is loaded, Don't load this(same rule with same config).
- Duplicated error message is ignored by default
- Duplicated error messages is that have same range and same message.
- Proposal: duplicated messages is ignored by default · Issue #209 · textlint/textlint