Skip to content

Custom Rules

HTMLHint allows you to create custom rules to extend its functionality for your specific needs. Custom rules can be loaded using the --rulesdir option and follow the same pattern as built-in rules.

Custom rules are JavaScript modules that export a function. The function receives the HTMLHint instance as a parameter and should register the custom rule using HTMLHint.addRule().

my-custom-rule.js
module.exports = function(HTMLHint) {
HTMLHint.addRule({
id: 'my-custom-rule',
description: 'This is my custom rule description',
init: function(parser, reporter, options) {
// Rule implementation goes here
}
});
};

Each custom rule should have the following properties:

  • id (string): Unique identifier for the rule. This is used in configuration files and command line options.
  • description (string): Human-readable description of what the rule does. This appears in the --list output.
  • init (function): Function that initializes the rule with the parser and reporter.

The init function is where your rule logic goes. It receives three parameters:

  • parser: The HTML parser instance that provides events as it parses the HTML
  • reporter: The reporter instance for generating warnings, errors, or info messages
  • options: Rule-specific options from the configuration

When adding event listeners, always use arrow functions instead of function expressions to ensure the correct this context is maintained when calling reporter methods:

// ✅ Correct - Arrow function preserves 'this' context
parser.addListener('tagstart', (event) => {
reporter.warn('Message', event.line, event.col, this, event.raw);
});
// ❌ Incorrect - Function expression loses 'this' context
parser.addListener('tagstart', function(event) {
reporter.warn('Message', event.line, event.col, this, event.raw); // 'this' is parser, not rule
});

This rule warns when a specific tag is used:

no-div-tags.js
module.exports = function(HTMLHint) {
HTMLHint.addRule({
id: 'no-div-tags',
description: 'Div tags are not allowed',
init: function(parser, reporter, options) {
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase();
if (tagName === 'div') {
reporter.warn(
'Div tags are not allowed. Use semantic HTML elements instead.',
event.line,
event.col,
this,
event.raw
);
}
});
}
});
};

This rule checks for specific attribute patterns:

data-attributes-required.js
module.exports = function(HTMLHint) {
HTMLHint.addRule({
id: 'data-attributes-required',
description: 'Elements with class "component" must have a data-component attribute',
init: function(parser, reporter, options) {
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase();
const mapAttrs = parser.getMapAttrs(event.attrs);
// Check if element has class "component"
if (mapAttrs.class && mapAttrs.class.includes('component')) {
// Check if it has data-component attribute
if (!mapAttrs['data-component']) {
reporter.warn(
'Elements with class "component" must have a data-component attribute',
event.line,
event.col,
this,
event.raw
);
}
}
});
}
});
};

Example 3: Complex Validation with Options

Section titled “Example 3: Complex Validation with Options”

This rule accepts configuration options:

max-attributes.js
module.exports = function(HTMLHint) {
HTMLHint.addRule({
id: 'max-attributes',
description: 'Elements should not have more than the specified number of attributes',
init: function(parser, reporter, options) {
const maxAttrs = options || 5; // Default to 5 if no options provided
parser.addListener('tagstart', (event) => {
const attrCount = event.attrs.length;
if (attrCount > maxAttrs) {
reporter.warn(
`Element has ${attrCount} attributes, but maximum allowed is ${maxAttrs}`,
event.line,
event.col,
this,
event.raw
);
}
});
}
});
};

Use the --rulesdir option to load custom rules:

Terminal window
# Load a single custom rule file
npx htmlhint --rulesdir ./my-custom-rule.js index.html
# Load all custom rules from a directory
npx htmlhint --rulesdir ./custom-rules/ index.html

After loading custom rules, you can enable them in several ways:

{
"no-div-tags": true,
"data-attributes-required": true,
"max-attributes": 3
}
Terminal window
npx htmlhint --rulesdir ./custom-rules/ --rules no-div-tags,data-attributes-required,max-attributes:3 index.html
<!-- htmlhint no-div-tags:true,data-attributes-required:true -->
<html lang="en">
<body>
<div class="component">This will trigger warnings</div>
</body>
</html>

The HTML parser provides several events you can listen to:

  • tagstart: Fired when a start tag is encountered
  • tagend: Fired when an end tag is encountered
  • text: Fired when text content is encountered
  • comment: Fired when a comment is encountered
  • cdata: Fired when CDATA is encountered
  • doctype: Fired when a DOCTYPE declaration is encountered

Event objects typically contain:

  • tagName: The name of the tag
  • attrs: Array of attributes
  • line: Line number where the event occurred
  • col: Column number where the event occurred
  • raw: The raw HTML string for this element

The reporter provides three methods for generating messages:

  • reporter.warn(message, line, col, rule, raw): Creates a warning message
  • reporter.error(message, line, col, rule, raw): Creates an error message
  • reporter.info(message, line, col, rule, raw): Creates an info message
  1. Use arrow functions: Always use arrow functions for event listeners to preserve the correct this context
  2. Use descriptive rule IDs: Choose clear, descriptive names for your rules
  3. Provide helpful error messages: Make error messages actionable and informative
  4. Handle options gracefully: Always provide sensible defaults for rule options
  5. Test your rules: Create test cases to ensure your rules work correctly
  6. Follow existing patterns: Look at built-in rules for examples of good practices
  7. Document your rules: Include clear descriptions and examples

When organizing multiple custom rules, you can use a directory structure:

custom-rules/
├── accessibility/
│ ├── aria-required.js
│ └── semantic-headings.js
├── performance/
│ ├── image-optimization.js
│ └── script-loading.js
└── style/
├── class-naming.js
└── attribute-order.js

HTMLHint will recursively find all .js files in the specified directory and load them as custom rules.

  • Rule not loading: Check that your file exports a function and calls HTMLHint.addRule()
  • Rule not running: Ensure the rule is enabled in your configuration
  • Parser errors: Verify that you’re using the correct event names and object properties
  • Silent failures: Custom rules that fail to load are silently ignored - check your JavaScript syntax
  • Incorrect rule reporting: Make sure you’re using arrow functions for event listeners to preserve the this context