Creating Custom Rules
You can create a custom rule by referring to the API documentation, but the easiest way is to use the scaffolding command:
npx @markuplint/create-rule
Answer the interactive questions:
? What purpose do you create the rule for? …
❯ Add the rule to this project
Create the rule and publish it as a package
Choose either "Add the rule to this project" or "Create the rule and publish it as a package."
Adding to your project
The command will ask for a directory name and a rule name. Then choose between TypeScript or JavaScript, and whether to include tests.
The following files are created:
The test code is written in Vitest format. Rewrite it as needed for your test runner.
Then specify it in your configuration to apply it:
{
"plugins": ["./[dir-name]/index.js"], // Need transpile if the source is TypeScript
"rules": {
"[dir-name]/[rule-name]": true
}
}
By default, the plugin name is the directory name ([dir-name] in the examples above). You can change it in the plugin definition:
import { createPlugin } from '@markuplint/ml-core';
import { ruleName } from './rules/ruleName';
export default createPlugin({
name: '[dir-name]', // 👈 Change here if you want
create(setting) {
return {
rules: {
ruleName: ruleName(setting),
},
};
},
});
Creating a plugin as an npm package
The command will ask for a plugin name and a rule name. Then choose between TypeScript or JavaScript, and whether to include tests.
The following files are created:
Basic evaluation
Extract target nodes from the document object, evaluate them, and pass violations to the report function. The document object provides both Markuplint-specific methods (like walkOn) and native DOM APIs (like querySelector):
createRule({
async verify({ document, report }) {
// Walking style
await document.walkOn('Element', el => {
if (el.localName === 'div') {
report({
scope: el,
message: 'The div element is found',
});
}
});
// DOM API traversing style
const el = document.querySelector('div');
if (el) {
report({
scope: el,
message: 'The div element is found',
});
}
},
});
There are two ways to report a violation: pass a node, or pass the line, column, and raw string:
report({
scope: node, // Specify a node (Element, Attribute, or TextNode, etc.)
message: 'Warning message',
});
report({
line: 20,
col: 10,
raw: 'string in range',
message: 'Warning message',
});
Trees
The content of a <template> element and a declarative shadow root are trees of their own, as they are in the DOM. querySelector(), querySelectorAll() and getElementById() search the tree of the node they are called on, and closest() and the selectors stop at its boundary. To look at every tree, use deepQuerySelectorAll() or walkOn(), which visits every node. el.getRootNode() is the tree an element is in, and el.treeParentElement is its parent in that tree. el.content is the content of a <template> element, and el.shadowRoot is a declarative shadow root that is attached to an element with shadowrootmode="open". parentNode and parentElement stay the ones of the syntax tree, so that of the first element in a <template> is the <template> element.
Messages i18n
The translate function (aliased as t) translates messages:
createRule({
async verify({ document, report, translate, t }) {
const noTitle = !document.querySelector('title');
if (noTitle) {
report({
line: 1,
col: 1,
raw: '',
message: translate('missing {0}', t('the "{0*}" {1}', 'title', 'element')),
});
}
},
});
Missing the "title" element
「title」要素がありません
See the @markuplint/i18n API for details.
Currently only Japanese is available besides English in the dictionaries. Contributions for other languages are welcome.
Next steps
- API Documentation — Full API reference for rule development
- Applying Rules — How to apply custom rules in your configuration