diff --git a/examples/custom-rules/README.md b/examples/custom-rules/README.md new file mode 100644 index 000000000..e681a6841 --- /dev/null +++ b/examples/custom-rules/README.md @@ -0,0 +1,65 @@ +# Custom Rules Examples + +This directory contains example custom rules for HTMLHint to demonstrate how to create and use custom rules. + +## Example Rule + +The `example-rule.js` file demonstrates a custom rule that: + +1. Checks if images have either a `title` or `alt` attribute for accessibility +2. Validates class names against a configurable pattern + +## Usage + +### Loading the Example Rule + +```shell +# Load the example rule +npx htmlhint --rulesdir ./examples/custom-rules/example-rule.js test.html +``` + +### Configuration + +You can configure the example rule in your `.htmlhintrc` file: + +```json +{ + "example-rule": { + "classPattern": "^[a-z][a-z0-9-]*$" + } +} +``` + +Or via command line: + +```shell +npx htmlhint --rulesdir ./examples/custom-rules/ --rules "example-rule:{classPattern:'^[a-z][a-z0-9-]*$'}" test.html +``` + +### Testing + +Create a test HTML file to see the rule in action: + +```html + + + + Test + + + + + + +
Content
+ + + Description +
Content
+ + +``` + +## Creating Your Own Rules + +See the [Custom Rules documentation](https://htmlhint.com/docs/usage/custom-rules/) for detailed information on creating custom rules. diff --git a/examples/custom-rules/example-rule.js b/examples/custom-rules/example-rule.js new file mode 100644 index 000000000..a7d081578 --- /dev/null +++ b/examples/custom-rules/example-rule.js @@ -0,0 +1,47 @@ +/** + * Example custom rule for HTMLHint + * This rule demonstrates how to create a custom rule that checks for specific patterns + */ + +module.exports = function (HTMLHint) { + HTMLHint.addRule({ + id: 'example-rule', + description: 'Example custom rule that demonstrates custom rule creation', + init: function (parser, reporter, options) { + // Listen for start tags - Note: Use arrow functions for event listeners + parser.addListener('tagstart', (event) => { + const tagName = event.tagName.toLowerCase() + const mapAttrs = parser.getMapAttrs(event.attrs) + + // Example: Check if elements have a title attribute when they should + if (tagName === 'img' && !mapAttrs.title && !mapAttrs.alt) { + reporter.warn( + 'Images should have either a title or alt attribute for accessibility', + event.line, + event.col, + this, + event.raw + ) + } + + // Example: Check for specific class naming convention + if (mapAttrs.class && options && options.classPattern) { + const classPattern = new RegExp(options.classPattern) + const classes = mapAttrs.class.split(/\s+/) + + for (const className of classes) { + if (!classPattern.test(className)) { + reporter.warn( + `Class "${className}" does not match the required pattern: ${options.classPattern}`, + event.line, + event.col, + this, + event.raw + ) + } + } + } + }) + }, + }) +} diff --git a/website/src/content/docs/usage/cli.md b/website/src/content/docs/usage/cli.md index 9a099d2f1..dda5a921e 100644 --- a/website/src/content/docs/usage/cli.md +++ b/website/src/content/docs/usage/cli.md @@ -40,7 +40,15 @@ Set all of the rules available ### `--rulesdir, -R` -Load custom rules from file or folder +Load custom rules from file or folder. See the [custom rules documentation](/usage/custom-rules/) for detailed information on creating and using custom rules. + +```shell +# 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 +``` ### `--version, -V` diff --git a/website/src/content/docs/usage/custom-rules.mdx b/website/src/content/docs/usage/custom-rules.mdx new file mode 100644 index 000000000..ec4956580 --- /dev/null +++ b/website/src/content/docs/usage/custom-rules.mdx @@ -0,0 +1,269 @@ +--- +id: custom-rules +title: Custom Rules +description: Learn how to create and use custom rules to extend HTMLHint's functionality for your specific needs. +sidebar: + order: 4 +--- + +# 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. + +## Creating Custom 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()`. + +### Basic Custom Rule Structure + +```javascript +// 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 + } + }); +}; +``` + +### Rule Object Properties + +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 + +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 + +### Important: Using Arrow Functions + +When adding event listeners, **always use arrow functions** instead of function expressions to ensure the correct `this` context is maintained when calling reporter methods: + +```javascript +// ✅ 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 +}); +``` + +## Example Custom Rules + +### Example 1: Simple Tag Check + +This rule warns when a specific tag is used: + +```javascript +// 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 + ); + } + }); + } + }); +}; +``` + +### Example 2: Attribute Validation + +This rule checks for specific attribute patterns: + +```javascript +// 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 + +This rule accepts configuration options: + +```javascript +// 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 + ); + } + }); + } + }); +}; +``` + +## Using Custom Rules + +### Loading Custom Rules + +Use the `--rulesdir` option to load custom rules: + +```shell +# 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 +``` + +### Enabling Custom Rules + +After loading custom rules, you can enable them in several ways: + +#### In Configuration File + +```json +{ + "no-div-tags": true, + "data-attributes-required": true, + "max-attributes": 3 +} +``` + +#### Via Command Line + +```shell +npx htmlhint --rulesdir ./custom-rules/ --rules no-div-tags,data-attributes-required,max-attributes:3 index.html +``` + +#### Inline in HTML + +```html + + + +
This will trigger warnings
+ + +``` + +## Parser Events + +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 Object Properties + +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 + +## Reporter Methods + +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 + +## Best Practices + +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 + +## Directory Structure + +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. + +## Troubleshooting + +- **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 diff --git a/website/src/content/docs/usage/options.md b/website/src/content/docs/usage/options.md index 3df6ab617..f7a253df8 100644 --- a/website/src/content/docs/usage/options.md +++ b/website/src/content/docs/usage/options.md @@ -38,3 +38,81 @@ Options are: CLI flags: `--ignore, -i` A list of patterns of files or folders to ignore. For example, `--ignore="**/folder/**,**/folder_two/**"` + +## `rulesdir` + +CLI flags: `--rulesdir, -R` + +Load custom rules from a file or directory. This allows you to extend HTMLHint with your own custom rules. + +### Usage + +```shell +# Load a single custom rule file +npx htmlhint --rulesdir ./my-custom-rule.js index.html + +# Load all custom rules from a directory (recursively finds all .js files) +npx htmlhint --rulesdir ./custom-rules/ index.html +``` + +### Custom Rule Format + +Custom rules should be JavaScript modules that export a function. The function receives the `HTMLHint` instance as a parameter and should register the custom rule using `HTMLHint.addRule()`. + +#### Example Custom Rule + +```javascript +// 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 - Note: Use arrow functions for event listeners + parser.addListener('tagstart', (event) => { + const tagName = event.tagName.toLowerCase(); + + if (tagName === 'div') { + reporter.warn( + 'Custom rule: div tags are not allowed', + event.line, + event.col, + this, + event.raw + ); + } + }); + } + }); +}; +``` + +**Important**: Always use arrow functions for event listeners to ensure the correct `this` context is maintained when calling reporter methods. + +#### Using Custom Rules + +After loading custom rules with `--rulesdir`, you can enable them in your configuration: + +```json +{ + "my-custom-rule": true +} +``` + +Or via command line: + +```shell +npx htmlhint --rulesdir ./custom-rules/ --rules my-custom-rule index.html +``` + +### Directory Loading + +When specifying a directory, HTMLHint will: + +1. Recursively search for all `.js` files in the directory +2. Load each file as a custom rule module +3. Skip any files that fail to load (errors are silently ignored) + +This makes it easy to organize multiple custom rules in a single directory structure. + +For detailed information on creating custom rules, see the [Custom Rules documentation](/usage/custom-rules/).