Skip to main content

Custom Converters

You can register custom adapter implementations to handle new output formats by implementing the IDocumentConverter interface. Adapters receive styling helpers such as StyleMapper and defaults via their constructor.

Adapter Constructor & Dependencies

For detailed definitions of the types used here (such as IDocumentConverter, DocumentElement, and IConverterDependencies), see the Types Reference.

Custom adapter classes must implement:

import { IDocumentConverter, DocumentElement, IConverterDependencies } from 'html-to-document';

export class MyAdapter implements IDocumentConverter {
constructor({ styleMapper, defaultStyles }: IConverterDependencies) {
// styleMapper: StyleMapper for CSS→format mapping
// defaultStyles?: default style definitions per element type
}

async convert(elements: DocumentElement[]): Promise<Buffer | Blob> {
// Iterate elements, merge defaults and element styles
for (const el of elements) {
const base = defaultStyles?.[el.type] || {};
const merged = { ...base, ...el.styles };
const props = styleMapper.mapStyles(merged, el);
// create format-specific nodes using props
}
return new Blob();
}
}

Quickstart Guide: Writing a Minimal Custom Adapter

Here's a barebones adapter that outputs plain text from paragraphs and headings. This helps you understand how to traverse and transform DocumentElement nodes.

import { IDocumentConverter, DocumentElement, IConverterDependencies } from 'html-to-document';

export class PlainTextAdapter implements IDocumentConverter {
private mapper: StyleMapper;
constructor({ styleMapper, defaultStyles }: IConverterDependencies) {
this.mapper = styleMapper;
this.defaults = defaultStyles ?? {};
}

async convert(elements: DocumentElement[]): Promise<Blob> {
const text = elements.map(el => {
const styles = { ...this.defaults[el.type], ...el.styles };
const props = this.mapper.mapStyles(styles, el);
switch (el.type) {
case 'paragraph':
return (el.text ?? '') + '\n\n';
case 'heading':
return '#'.repeat(el.level || 1) + ' ' + (el.text ?? '') + '\n\n';
case 'text':
return props.bold ? '**' + (el.text ?? '') + '**' : (el.text ?? '');
default:
return '';
}
}).join('');

return new Blob([text], { type: 'text/plain' });
}
}

You can build on this to support more types like tables, images, and lists.

Element Converter Examples

You can also register custom block, inline, and fallthrough converters by implementing the respective interfaces. Below is a generic example:

import {
IBlockConverter,
IInlineConverter,
IFallthroughConvertedChildrenWrapperConverter,
ElementConverterDependencies,
} from 'html-to-document-core';
import {
DocumentElement,
HeadingElement,
TextElement,
Styles,
} from 'html-to-document-core';

// Block converter: handles heading elements
class HeadingBlockConverter implements IBlockConverter<HeadingElement> {
isMatch(el: DocumentElement): el is HeadingElement {
return el.type === 'heading';
}
convertEement(
deps: ElementConverterDependencies,
el: HeadingElement,
styles: Styles = {}
) {
// implement conversion logic for headings
return [];
}
}

// Inline converter: handles bold text nodes
class BoldInlineConverter implements IInlineConverter<TextElement> {
isMatch(el: DocumentElement): el is TextElement {
return el.type === 'text' && el.styles.fontWeight === 'bold';
}
convertEement(deps: ElementConverterDependencies, el: TextElement, styles: Styles) {
// implement conversion logic for bold text
return [];
}
}

// Fallthrough converter: wraps converted children based on attributes
class IdFallthroughConverter implements IFallthroughConvertedChildrenWrapperConverter {
isMatch(el: DocumentElement): boolean {
return Boolean(el.attributes?.id);
}
fallthroughWrapConvertedChildren(
deps: ElementConverterDependencies,
el: DocumentElement,
children: any[],
styles: Styles,
index?: number
) {
// implement fallthrough logic
return children;
}
}

Provide adapter classes implementing IDocumentConverter via the adapters.register option:

import { init } from 'html-to-document';
import { MyAdapter } from './my-adapter';

const converter = init({
adapters: {
// Register your adapter
register: [
{
format: 'md',
adapter: MyAdapter,
// Optional adapter-specific configuration:
// config: { /* adapter options */ },
},
],
},
});

You can also supply default styles and style mappings for your custom adapter:

const converter = init({
adapters: {
register: [
{
format: 'md',
adapter: MyAdapter,
// Optional adapter-specific configuration:
// config: { /* adapter options */ },
},
],
defaultStyles: [
{ format: 'md', styles: { paragraph: { marginBottom: 8, lineHeight: 1.6 } } },
],
styleMappings: [
{ format: 'md', handlers: { fontWeight: (v) => ({ bold: v === 'bold' }) } },
],
},
});

The MyAdapter class should implement:

import { IDocumentConverter, DocumentElement } from 'html-to-document';

export class MyAdapter implements IDocumentConverter {
constructor(private options: any) {}
async convert(elements: DocumentElement[]): Promise<Buffer | Blob> {
// Implement conversion logic here
return new Blob();
}
}

This method works well cause it handles the initialization of the style mapper and other dependencies for you.

Register at Runtime

After creating a Converter instance, call registerConverter:

const converter = init();
converter.registerConverter('md', new MyAdapter({ /* deps */ }));

Note: When registering at runtime, you must supply an IConverterDependencies object, typically:

import { StyleMapper } from 'html-to-document';
const styleMapper = new StyleMapper();
// Optionally add mappings:
styleMapper.addMapping({ fontWeight: (v) => ({ bold: v === 'bold' }) });
const defaultStyles = { paragraph: { lineHeight: 1.5 } };
converter.registerConverter(
'md',
new MyAdapter({ styleMapper, defaultStyles })
);