Skip to main content

Stylesheet API

The stylesheet system lets html-to-document keep style rules outside of parsed DocumentElement.styles and resolve them later during serialization or adapter conversion.

This is especially useful for:

  • HTML tag defaults that should behave like rules, not inline styles
  • document-level rules passed into init()
  • adapter-specific default styles
  • adapters that need to inspect top-level at-rules such as @page

The two style sources

There are now two different places styles can come from:

1. Inline element styles

These are styles parsed from the HTML itself, such as:

<p style="color: red; font-weight: bold">Hello</p>

Those values still end up on element.styles.

2. Stylesheet rules

These are rules stored in an IStylesheet instance.

They come from:

  • built-in seeded rules like heading defaults
  • tags.defaultStyles
  • stylesheetRules passed to init()
  • adapter defaultStyles
  • a custom stylesheet passed to init()

These rules are matched later by adapters using selectors.

Using stylesheetRules in init()

The main API for supplying rules directly is stylesheetRules.

import { init, DocxAdapter } from 'html-to-document';

const converter = init({
stylesheetRules: [
{
kind: 'style',
selectors: ['p.note', 'div.note'],
declarations: {
color: 'rebeccapurple',
fontWeight: 'bold',
},
},
{
kind: 'at-rule',
name: 'page',
descriptors: {
size: 'A4',
margin: '1in',
},
},
],
adapters: {
register: [{ format: 'docx', adapter: DocxAdapter }],
},
});

stylesheetRules is the easiest way to seed rule statements without manually creating a stylesheet instance first.

Using a custom stylesheet

If you need full control, you can provide your own stylesheet implementation or a prebuilt stylesheet instance.

import { init, createStylesheet } from 'html-to-document';

const stylesheet = createStylesheet();
stylesheet.addStyleRule('p.note', { color: 'green' });
stylesheet.addAtRule({
kind: 'at-rule',
name: 'page',
descriptors: { size: 'A4' },
});

const converter = init({
stylesheet,
});

When you pass stylesheet, the library still appends other seeded rules onto it.

What init() seeds into the stylesheet

When you call init(), the base stylesheet is built in this order:

  1. built-in base rules from createBaseStylesheet()
  2. rules generated from tags.defaultStyles
  3. rules from stylesheetRules
  4. for each adapter, format-specific adapters.defaultStyles

Each adapter gets its own cloned stylesheet instance, so adapters do not share mutable rule state.

tags.defaultStyles now become stylesheet rules

tags.defaultStyles no longer gets merged into parsed element.styles.

Instead, each tag default is converted into a selector rule using the tag name itself.

const converter = init({
tags: {
defaultStyles: [
{ key: 'p', styles: { marginBottom: '8px' } },
{ key: 'table', styles: { borderStyle: 'solid' } },
],
},
});

That behaves like this conceptually:

[
{
kind: 'style',
selectors: ['p'],
declarations: { marginBottom: '8px' },
},
{
kind: 'style',
selectors: ['table'],
declarations: { borderStyle: 'solid' },
},
];

This means tag defaults remain selector-driven and can be resolved consistently by adapters.

adapters.defaultStyles also become stylesheet rules

Adapter default styles are still keyed by document element type:

const converter = init({
adapters: {
register: [{ format: 'docx', adapter: DocxAdapter }],
defaultStyles: [
{
format: 'docx',
styles: {
paragraph: { color: 'darkblue' },
heading: { fontFamily: 'Aptos Display' },
},
},
],
},
});

These are converted into rules internally and appended to that adapter's stylesheet.

Rule shapes

A stylesheet stores ordered StylesheetStatement values.

Style rule

const rule = {
kind: 'style',
selectors: ['p', '.note', '#intro'],
declarations: {
color: 'red',
textAlign: 'center',
},
} as const;

Style rules may also contain nested at-rules.

const rule = {
kind: 'style',
selectors: ['.card'],
declarations: { padding: '12px' },
children: [
{
kind: 'at-rule',
name: 'container',
prelude: 'card (min-width: 20rem)',
children: [
{
kind: 'style',
selectors: ['&'],
declarations: { padding: '16px' },
},
],
},
],
} as const;

These nested at-rules are currently preserved for consumers and future compatibility, but they are not evaluated by the current matcher.

At-rule

const rule = {
kind: 'at-rule',
name: 'page',
prelude: ':first',
descriptors: {
size: 'A4',
margin: '1in',
},
} as const;

Nested children are also supported for block at-rules. Style rules may also preserve nested at-rules for future use.

Selector support

The matcher currently supports simple selectors:

  • tag selectors: p
  • class selectors: .note
  • id selectors: #intro
  • universal selector: *
  • attribute selectors:
    • [data-x]
    • [data-x="a"]
    • [data-x~="a"]
    • [lang|="en"]
    • [data-x^="pre"]
    • [data-x$="end"]
    • [data-x*="mid"]
  • selector lists: h1, h2, h3

Complex selector relationships like descendant, child, sibling, and pseudo selectors are not currently matched.

How adapters use the stylesheet

Adapters receive a stylesheet in IConverterDependencies.

class MyAdapter {
constructor(dependencies) {
console.log(dependencies.stylesheet);
}
}

Useful methods:

stylesheet.getMatchedStyles(element)

Returns only styles resolved from matching rules.

const matched = stylesheet.getMatchedStyles(element);

stylesheet.getComputedStyles(element, cascadedStyles)

Returns merged styles in this order:

{
...cascadedStyles,
...stylesheet.getMatchedStyles(element),
...(element.styles ?? {}),
}

So inline styles on the parsed element still win over stylesheet matches.

stylesheet.getStatements()

Returns all stored top-level statements.

stylesheet.getAtRules(name?)

Lets adapters inspect at-rules like @page.

const pageRules = stylesheet.getAtRules('page');

Built-in seeded rules

createBaseStylesheet() currently seeds heading defaults:

  • h1fontSize: '32px', fontWeight: 'bold'
  • h2fontSize: '24px', fontWeight: 'bold'
  • h3fontSize: '18.72px', fontWeight: 'bold'
  • h4fontSize: '16px', fontWeight: 'bold'
  • h5fontSize: '13.28px', fontWeight: 'bold'
  • h6fontSize: '10.72px', fontWeight: 'bold'

There are many other rules which aim to make it look like the browsers' defaults.

Summary

  • use stylesheetRules in init() to provide rules directly
  • use stylesheet when you want to provide a custom stylesheet instance
  • tags.defaultStyles now seed stylesheet rules instead of inlining styles
  • adapters.defaultStyles are converted into adapter-specific stylesheet rules
  • inline HTML styles still live on element.styles and override stylesheet matches