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 rule 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:
// ES2015+
export default function (context) {
const { Syntax } = context;
return {
[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 more details
locator
RuleError
is an Error object for reporting.
Use it with report
function.
report(node, new RuleError(<message>))
- report new error
- textlint show the
<mesage>
againstnode
's range.
report(node, new RuleError(<message>, { padding }))
- report new error with
padding
. - textlint show the
<mesage>
againstnode
's range +padding
. - You can control correct error range by
padding
property ofRuleError
- report new error with
Example: report the Str
node that is typo.
This is typo.
^^^^^^^^^^^^^
|
Found a typo.
export default function (context) {
const { Syntax, report, RuleError, getSource } = context;
return {
[Syntax.Str](node) {
// get source code of this `node`
const text = getSource(node);
if (/typo/.test(text)) {
// report error
report(node, new RuleError("Found a typo"));
}
}
};
}
Example: report typo
string that is typo using padding
This is typo.
^^^^
|
Found a typo.
export default function (context) {
const { Syntax, report, RuleError, getSource, locator } = context;
return {
[Syntax.Str](node) {
// get source code of this `node`
const text = getSource(node);
const match = text.match(/typo/);
if (match) {
// report error with padding
// node's start + padding's range
// As a result, report the error that is [node.range[0] + typo.index, node.range[1] + typo.index + type.length]
report(
node,
new RuleError("Found a typo", {
padding: locator.range([match.index, match.index + match[0].length])
})
);
}
}
};
}
The context.locator
object has the following methods:
locator.at(index)
- return padding value that is relative index from node's start index.- The
index
value is 0-based value - This method is alias to
locator.range([index, index + 1])
- The
locator.range([startIndex, endIndex])
- return padding value that is relative range from node's start index- Each
index
value is 0-based value
- Each
locator.loc({ start: { line, column }, end: { line, column })
- return padding value that is relative location from node's start loc- 📝 This
line
andcolumn
is relative value. It is not absolute value. So, These are 0-based value.
- 📝 This
📝 context.locator
object in introduced in v12.2.0.
export type TextlintRulePaddingLocator = {
/**
* at's index is 0-based value.
* It is alias to `range([index, index + 1])`
* @param index relative index from node's start position
*/
at(index: number): TextlintRuleErrorPaddingLocation;
/**
* range's index is 0-based value
* @param range relative range from node's start position
*/
range(range: readonly [startIndex: number, endIndex: number]): TextlintRuleErrorPaddingLocation;
/**
* line is relative padding value from node's start position
* column is relative padding value from node's start position
* @param location relative location from node's start position
*/
loc(location: {
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
}): TextlintRuleErrorPaddingLocation;
};
Examples
// locator.at
report(node, new RuleError(message, {
padding: locator.at(11)
}));
// locator.range
report(node, new RuleError(message, {
padding: locator.range([5, 10])
}));
// locator.loc
report(node, new RuleError(message, {
padding: locator.loc({
start: {
line: 1,
column: 1
},
end: {
line: 2,
column: 2
}
})
}));
📝 padding
option and locator
object are introduced in textlint v12.2.0+.
You can declare your dependency on textlint in package.json
using the peerDependencies and peerDependenciesMeta fields.
"peerDependencies": {
"textlint": ">= 12.2.0"
},
"peerDependenciesMeta": {
"textlint": {
"optional": true
}
}
Deprecated: { line, column } and { index } properties
{ line, column }
and { index }
properties are deprecated.
Instead of these, textlint v12.2.0 introduce padding
property and locator
object.
Deprecated way:
// report without padding
const error = new RuleError("message");
// OR
// report with 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
});
// OR
// report with index-based padding
const paddingIndex = 1;
const errorWithPaddingIndex = new RuleError("message", {
index: paddingIndex // padding index value from `node.range[0]`. default: 0
});
Current way:
// No padding information
// It means, this error point out the whole node
const error = new RuleError("message");
// OR
const errorWithPadding = new RuleError("message", {
padding: locator.loc({
start: {
line: 1, // padding line number from node.loc.start.line. default: 0
column: 1 // padding column number from node.loc.start.column. default: 0
},
end: {
line: 1, // padding line number from node.loc.start.line. default: 0
column: 2 // padding column number from node.loc.start.column. default: 0
}
})
});
// OR
const paddingIndex = 1;
const errorWithPaddingIndex = new RuleError("message", {
// padding [startIndex, endIndex] from node's start index.
padding: locator.range([1, 2])
});
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) {
const { Syntax, report, RuleError } = context;
return {
[Syntax.Str](node) {
// get source code of this `node`
const text = context.getSource(node);
if (/found wrong use-case/.test(text)) {
// report error
report(node, new 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 { Syntax, getSource, RuleError, report, locator } = 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}'`, {
padding: locator.range([match.index, match.index + text.length])
})
);
}
},
/*
# 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}'`, {
padding: locator.range([match.index, match.index + text.length])
})
);
}
}
};
}
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;
});
});
}
export default function (context) {
const { Syntax, getSource, RuleError, report, locator } = context;
return {
/*
Todo: quick fix this.
*/
[Syntax.Str](node) {
// Ignore the node if the node is child of some Node types
if (isNodeWrapped(node, [Syntax.Link, Syntax.Image, Syntax.BlockQuote])) {
return;
}
// get text from node
const text = getSource(node);
// Does the text contain "todo:"?
const matches = text.matchAll(/todo:/gi);
for (const match of matches) {
const index = match.index ?? 0;
const length = match[0].length;
report(
node,
new RuleError(`Found TODO: '${text}'`, {
padding: locator.range([index, index + length])
})
);
}
},
/*
- [ ] Todo
*/
[Syntax.ListItem](node) {
const text = context.getSource(node);
// Does the ListItem's text starts with `- [ ]`
const match = text.match(/^-\s\[\s+]\s/i);
if (match && match.index !== undefined) {
report(
node,
new context.RuleError(`Found TODO: '${text}'`, {
padding: locator.range([match.index, match.index + match[0].length])
})
);
}
}
};
}
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
:
import TextLintTester from "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: [
// range
{
text: "- [ ] string",
// ^^^^^^
errors: [
{
message: "Found TODO: '- [ ] string'",
range: [0, 6]
}
]
},
// loc
{
text: "- [ ] string",
// ^^^^^^
errors: [
{
message: "Found TODO: '- [ ] string'",
loc: {
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 7
}
}
}
]
}
]
});
Run the tests:
$ npm test
# or
$ npx mocha test/
ℹ️ Please see 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.json
{
"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.json
{
"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.0",
"homepage": "https://github.com/textlint/textlint-custom-rules/",
"keywords": [
"textlintrule"
]
}
A. You should add textlint >= x.y.z
to peerDependencies
in package.json
The recommended way to declare a dependency for future-proof compatibility is with the ">="
range syntax, using the lowest required eslint version. For example:
"peerDependencies": {
"textlint": ">= 5.5.0"
}
See example: textlint-rule-no-todo/package.json
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