Custom Adapters
This guide explains how to create custom adapters for capo.js and validate them using the built-in test utilities.
Why custom adapters?
Section titled âWhy custom adapters?âCustom adapters allow you to use capo.js with different HTML parsers or AST formats, such as:
- JSX/React elements
- Vue template AST
- Svelte component AST
- Custom XML parsers
- Server-side rendering frameworks
Creating a custom adapter
Section titled âCreating a custom adapterâStep 1: Extend the AdapterInterface
Section titled âStep 1: Extend the AdapterInterfaceâimport { AdapterInterface } from '@rviscomi/capo.js/adapters';
export class MyCustomAdapter extends AdapterInterface { // Implement all 11 required methods
isElement(node) { return node && node.type === 'YourElementType'; }
getTagName(node) { return node.tagName?.toLowerCase() || ''; }
getAttribute(node, name) { // ... }
// Implement other required methods: // hasAttribute, getAttributeNames, getTextContent, // getChildren, getParent, getSiblings, stringify}Step 2: Use your adapter
Section titled âStep 2: Use your adapterâimport { MyCustomAdapter } from './my-custom-adapter.js';import { analyzeHead } from '@rviscomi/capo.js';
const adapter = new MyCustomAdapter();const results = analyzeHead(headNode, adapter);Node.js usage
Section titled âNode.js usageâWhile capo.js is designed for the browser, you can use it in Node.js by pairing it with a DOM simulation library like jsdom.
Since jsdom provides a standard DOM API, you can reuse the built-in BrowserAdapter instead of writing a custom one:
import { JSDOM } from 'jsdom';import { analyzeHead, BrowserAdapter } from '@rviscomi/capo.js';
const html = ` <!DOCTYPE html> <html> <head> <title>My Page</title> <meta charset="utf-8"> </head> <body></body> </html>`;
// 1. Create a JSDOM instanceconst dom = new JSDOM(html);const head = dom.window.document.querySelector('head');
// 2. Use the built-in BrowserAdapterconst adapter = new BrowserAdapter();
// 3. Analyzeconst results = analyzeHead(head, adapter);
console.log(results.weights);Validating your adapter
Section titled âValidating your adapterâcapo.js provides three levels of validation for custom adapters:
The simplest validation is to ensure your adapter implements the required interface:
import { validateAdapter } from '@rviscomi/capo.js/adapters';import { MyCustomAdapter } from './my-custom-adapter.js';
const adapter = new MyCustomAdapter();validateAdapter(adapter); // Throws if invalidWhat it checks:
- Adapter class is a valid constructor
- Adapter can be instantiated
- All 11 required methods are implemented
When to use: Quick validation during development.
Level 2: Programmatic validation
Section titled âLevel 2: Programmatic validationâUse the validateAdapter() function for explicit validation:
import { validateAdapter } from '@rviscomi/capo.js/adapters';import { MyCustomAdapter } from './my-custom-adapter.js';
const adapter = new MyCustomAdapter();
try { validateAdapter(adapter); console.log('â
Adapter is valid!');} catch (error) { console.error('â Adapter validation failed:', error.message);}What it checks:
- All 11 required methods exist
- Each method is a function
When to use: Integration tests, CI/CD pipelines.
Level 3: Full test suite
Section titled âLevel 3: Full test suiteâRun the comprehensive test suite to validate behavior:
import { describe } from 'node:test';import { runAdapterTestSuite, testAdapterCompliance } from '@rviscomi/capo.js/adapters/test-suite';import { MyCustomAdapter } from './my-custom-adapter.js';import { parseHtml } from './my-parser.js'; // Your parser
describe('MyCustomAdapter', () => { // Full test suite - tests all methods with edge cases runAdapterTestSuite(MyCustomAdapter, { createElement: (htmlString) => { // Your logic to create a node from HTML return parseHtml(htmlString); }, supportsLocation: true // true if getLocation() works });
// OR: Quick compliance check only testAdapterCompliance(MyCustomAdapter);});Note on parent pointers
Section titled âNote on parent pointersâSome adapters, like those for ESLint, require parent pointers on nodes to support getParent() and getSiblings(). If your parser doesnât provide these pointers by default (like @html-eslint/parser), you must shim them in your test setup.
For ESLint adapters, itâs recommended to use ESLintâs RuleTester in your tests to provide a realistic environment where parent pointers are automatically injected:
import { RuleTester } from 'eslint';import * as htmlParser from 'your-html-parser'; // e.g., @html-eslint/parser
const ruleTester = new RuleTester({ languageOptions: { parser: htmlParser }});
runAdapterTestSuite(MyAdapter, { createElement: (html) => { let capturedNode = null; ruleTester.run('capture', { create: () => ({ Tag: (node) => { if (!capturedNode) capturedNode = node; } }) }, { valid: [html] }); return capturedNode; }});What it tests:
- All 11 methods with various inputs
- Edge cases (null, undefined, empty strings)
- Expected return types
- Case-insensitivity
- 39 individual test cases
When to use: Comprehensive validation before release.
Best practices
Section titled âBest practicesâ- Extend
AdapterInterfaceto get base implementation. - Handle null/undefined inputs in all methods.
- Expect lowercase attribute names as inputs.
- Shim parent pointers if needed for
getParent/getSiblings. - Run the test suite to verify compliance.
API reference
Section titled âAPI referenceârunAdapterTestSuite(AdapterClass, options)
Section titled ârunAdapterTestSuite(AdapterClass, options)âRuns comprehensive tests on a custom adapter.
Parameters:
AdapterClass(Function) - The adapter constructor to testoptions(Object)createElement(Function) - Function that creates test nodes from HTML stringssupportsLocation(boolean, optional) - Whether adapter supportsgetLocation(). Default:false
Example:
runAdapterTestSuite(MyAdapter, { createElement: (html) => parseHtml(html), supportsLocation: true});testAdapterCompliance(AdapterClass)
Section titled âtestAdapterCompliance(AdapterClass)âQuick compliance check that verifies all required methods exist.
Parameters:
AdapterClass(Function) - The adapter constructor to test
Example:
testAdapterCompliance(MyAdapter);validateAdapter(adapter)
Section titled âvalidateAdapter(adapter)âProgrammatically validates an adapter instance.
Parameters:
adapter(Object) - Adapter instance to validate
Throws: Error if validation fails
Example:
const adapter = new MyAdapter();validateAdapter(adapter); // throws if invalidTroubleshooting
Section titled âTroubleshootingââAdapter missing required method: Xâ
- Ensure your adapter implements all 11 required methods
- Check for typos in method names
- Verify methods are functions, not properties
âCannot detect adapter for nodeâ
- Capo.js no longer uses auto-detection. You must explicitly provide the adapter instance to analysis functions.
Tests failing with âcreateElement is requiredâ
- You must provide a
createElementfunction in test options - This function should convert HTML strings to your parserâs node format
Support
Section titled âSupportâFor questions or issues with custom adapters: