Skip to main content

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:

  • 📂 [cwd]
    • 📂 [dir-name]
      • 📄 index.ts # or index.js
      • 📂 rules
        • 📄 [rule-name].ts # or [rule-name].js
        • 📄 [rule-name].spec.ts # or [rule-name].spec.js [Optional]
info

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
}
}
note

By default, the plugin name is the directory name ([dir-name] in the examples above). You can change it in the plugin definition:

./[dir-name]/index.ts
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:

  • 📂 [cwd]
    • 📄 README.md
    • 📄 package.json
    • 📄 tsconfig.json # Only when chose TypeScript
    • 📂 src
      • 📄 index.ts # or index.js
      • 📂 rules
        • 📄 [rule-name].ts # or [rule-name].js
        • 📄 [rule-name].spec.ts # or [rule-name].spec.js [Optional]

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')),
});
}
},
});
Result in English:
Missing the "title" element
Result in Japanese:
「title」要素がありません

See the @markuplint/i18n API for details.

info

Currently only Japanese is available besides English in the dictionaries. Contributions for other languages are welcome.

Next steps​