![]() |
CSSMedia is a TypeScript / Node.js engine that reads an XML file and a CSS stylesheet and generates a PDF that follows what the CSS specifications say must be done with that stylesheet.
Styling comes from CSS alone. The engine makes no assumptions about element names or attribute semantics, so any well-formed XML can be styled, not just a specific vocabulary like DITA or DocBook. PDF is the only output.
Work in progress. CSSMedia already generates PDF files and is under active development. Support for CSS features is not complete yet, and more is being added.
- User Manual: how to run CSSMedia and write stylesheets for it.
- CSS Reference: every selector, property, at-rule and descriptor that CSSMedia supports, with the CSS specification that defines it.
- Quick Start
- Why CSSMedia
- Features
- Samples
- Usage
- Installation
- Related Projects
- Support
One article in three languages, formatted by the same stylesheet:
| English | Arabic | Japanese |
|---|---|---|
![]() |
![]() |
![]() |
After installing CSSMedia, run it on the English sample from the samples folder:
cd samples
cssmedia -x xml_gap/xml_gap.xml -c docbook.css -p xml_gap/xml_gap.pdfThe command prints a warning about the DocBook DTD and generates xml_gap/xml_gap.pdf. See xml_gap for the explanation and for the stylesheet techniques that the sample shows.
An XML document describes content, not pages. To publish it as a PDF you have to say how it looks on paper: page size and margins, fonts, headers and footers, page numbers, line breaking for each language.
The traditional way to do that is a two-step pipeline: an XSLT stylesheet transforms the XML into XSL-FO, and an FO processor renders the result. Both steps need specialized knowledge, and the stylesheets are usually written for one XML vocabulary, so each new vocabulary needs new stylesheets.
CSSMedia replaces that pipeline with a single CSS stylesheet applied directly to the XML. This is what changes:
- The design is written in CSS. A CSS rule says how an element looks. There are no templates, modes or XPath expressions to write, and no intermediate XSL-FO document to produce and debug. If you know XSL-FO, you can reuse that knowledge: many XSL-FO properties, such as
font-family,text-alignandpadding, are also CSS properties. - The XML is styled directly. The source document is not converted to another format first, so the structure of the output follows the structure of the document.
- One engine for any XML vocabulary. The engine does not assume anything about element names or attributes. Each vocabulary needs only its own stylesheet.
- One stylesheet for many languages. The same stylesheet can format a document in several languages and scripts, as the xml_gap samples show.
- Behavior defined by specifications. Features are implemented from the CSS specifications, so what a property does is documented by the specification that defines it.
- Multiple languages and scripts: documents can be in any language, including right-to-left scripts such as Arabic and CJK languages such as Japanese. The xml_gap samples show the same article in English, Arabic, Japanese and Russian, all styled by one stylesheet.
- Hyphenation by language: with
hyphens: auto, words are hyphenated using the patterns of the language declared in the document by usingxml:lang. CSSMedia includes 75 hyphenation pattern sets, which cover many languages and some of their variants, such asen-usanden-gb. The swordfish_hyphens.pdf sample is generated with hyphenation turned on. - CSS-only styling: the XML is styled by CSS alone. Selector matching, specificity, cascade, inheritance and value resolution are CSSMedia's own code;
css-treeis used only to parse CSS syntax. - Fonts: OpenType, TrueType, collections, WOFF and WOFF 2.0 files, loaded from
@font-facerules or a fonts folder, plus the 14 standard PDF fonts. - Images: readers for PNG, JPEG, JPEG 2000 and WebP, written in CSSMedia's own code.
- SVG and MathML: SVG and MathML found in a document are handled as images.
- Documentation: the User Manual explains how to use CSSMedia, and the CSS Reference is a separate PDF that lists everything the stylesheets can use, with a link to the specification of each entry.
- Dependencies: XML is parsed with TypesXML, language tags are handled with TypesBCP47, CSS syntax is parsed with
css-tree, andpdfnativeprovides font data and the shaping of Indic and other complex scripts.
The samples folder has XML files, stylesheets and the fonts they use. The stylesheets load the fonts from samples/fonts with @font-face rules.
A DocBook article in four languages, all formatted with docbook.css. The Arabic, Japanese and Russian files were translated automatically with DeepL. The goal of the samples is to test typesetting, not to provide perfect localization.
| Language | XML | |
|---|---|---|
| English | xml_gap.xml | xml_gap.pdf |
| Arabic | xml_gap_ar.xml | xml_gap_ar.pdf |
| Japanese | xml_gap_ja.xml | xml_gap_ja.pdf |
| Russian | xml_gap_ru.xml | xml_gap_ru.pdf |
The files declare a DocBook DTD. Without a catalog, CSSMedia prints the warning External subset not found: docbook/docbookx.dtd and still generates the PDF. To avoid the warning, pass a catalog that resolves the DTD with -g, such as the one in TypesBook.
These are the techniques that the stylesheet uses:
| Result | CSS |
|---|---|
| Local fonts | @font-face rules with src: url("./fonts/NotoSans-Regular.ttf") |
| Page size, margins and a footer with the document title and the page number | @page with @bottom-left and @bottom-right, string-set: doc-title content() on the title and counter(page) |
| The language of each element | DocBook 5 gives it in xml:lang, which CSSMedia reads by itself. DocBook 4 gives it in lang, so * { -cssmedia-lang: attr(lang); } makes that attribute the language of each element |
| A different font for each language | the pseudo-class :lang(), with selectors such as article:lang(ar) and article:lang(ja). It matches the elements in that language, and the ones inside them |
| Right-to-left text in Arabic | direction: rtl |
| Stricter line breaking in Japanese | line-break: strict |
| Links from an XML attribute | -cssmedia-link: attr(url) on ulink |
| An image from an XML attribute | content: attr(fileref url) on imagedata |
| The photo at the start side of the text, on the right in Arabic and on the left in the other languages | float: inline-start on mediaobject |
| Code listings in a monospace font | font-family: "Noto Sans Mono", monospace with white-space: pre |
A DocBook 5 article with mathematics, "From Basel to Gauss: Sums, Products and Integrals". It is a math text, with formulas written in MathML inside the paragraphs and as numbered display equations. It is formatted with docbook.css, the same stylesheet as the xml_gap articles.
| XML | Stylesheet | |
|---|---|---|
| mathml.xml | docbook.css | mathml.pdf |
Run the command from the samples folder:
cssmedia -x mathml/mathml.xml -c docbook.css -p mathml/mathml.pdfThe article is a DocBook 5 document, so it has no DTD and the command prints no warning. These are the techniques that the stylesheet uses for it:
| Result | CSS |
|---|---|
| Formulas in a math font | @font-face for Noto Sans Math, and font-family on m|math with the MathML namespace declared by @namespace |
| Display equations numbered at the right margin | an empty mtd with class="eqnum" at the end of the row of each equation, counter-increment on informalequation and m|mtd.eqnum::before { content: "(" counter(eqnum) ")"; } |
| References that show the number of an equation or of a section | -cssmedia-link-counter() in the content of xref[xrefstyle="equation"]::after and xref[xrefstyle="section"]::after, with the link from -cssmedia-link: "#" attr(linkend) |
Numbered section headings, only in an article with role="numbered" |
counter-increment on section and counter() in article[role~="numbered"] > section > title::before |
| Theorems and proofs | example[role="theorem"] and para[role~="proof"] |
| Table captions numbered in order | counter-increment on table and counter(table) in table > title::before |
The User Guide of Swordfish, a real product, originally written in DITA, merged into one XML file in two ways. Each merged file needs a stylesheet written for its structure.
| Merged with | XML | Stylesheet | |
|---|---|---|---|
| TypesDitac | swordfish.xml | ditac.css | swordfish.pdf |
| TypesDitac | swordfish.xml | ditac_hyphens.css | swordfish_hyphens.pdf |
| DITA Open Toolkit | swordfish_dita-ot.xml | dita-ot.css | swordfish_dita-ot.pdf |
Differences between the stylesheets:
| Stylesheet | Input | Hyphenation |
|---|---|---|
ditac.css |
TypesDitac file | none |
ditac_hyphens.css |
TypesDitac file | hyphens: auto on the root element, with the patterns of each language |
dita-ot.css |
DITA-OT file: the map comes first, then the topics | none |
CSSMedia reads one XML file, so a DITA map and its topics have to be merged first. The commands below use the map of the CSSMedia manual, manual/CSSMedia.ditamap, as the example.
With TypesDitac, use the --merge option and name the output file with -o:
typesditac CSSMedia.ditamap --merge -o CSSMedia_MERGED.xmlWith the DITA Open Toolkit, the PDF transformation writes the merged file to its temporary directory, and DITA-OT deletes that directory after the build. To keep the merged file, set clean.temp to no:
dita --input=CSSMedia.ditamap --format=pdf --output=out --temp=temp --clean.temp=noThe merged file is temp/CSSMedia_MERGED.xml: the name of the map without its extension, followed by _MERGED.xml. The --temp option is optional and makes the location predictable.
Run cssmedia and the program prints this help text:
Usage: cssmedia -x <file> -c <file> -p <file> [-f <fontsFolder>] [-g <catalog>]
-x, --xml <file> XML file to format
-c, --css <file> CSS stylesheet to apply
-p, --pdf <file> PDF file to write
-f, --fonts <folder> (optional) alternative fonts folder
-g, --catalog <file> (optional) OASIS catalog to use when parsing XML
Example:
cssmedia -x document.xml -c document.css -p document.pdfUse -g to pass a custom OASIS catalog, so that the XML file is read with its associated grammars:
cssmedia -x document.xml -c document.css -p document.pdf -g catalog.xmlWarnings are printed to stderr. When processing fails, the program prints the error and exits with code 1; wrong arguments exit with code 2.
Fonts come from the @font-face rules of the stylesheet, then from the folder given with -f. CSSMedia does not search the fonts installed on the computer. The 14 standard PDF fonts are always available under the family names Times, Helvetica, Courier, Symbol and ZapfDingbats. A family declared with @font-face takes precedence over a standard family with the same name, and a standard family takes precedence over a family in the -f folder with the same name.
CSSMedia loads OpenType and TrueType (.otf, .ttf), OpenType collections (.otc, .ttc), WOFF 1.0 (.woff) and WOFF 2.0 (.woff2) fonts. The User Manual describes the @font-face descriptors, the format() and tech() values that are supported, and the sources that are skipped.
PdfGenerator.generate() writes the PDF. The constructor takes the optional font folder and the optional XML catalog, and the warnings of the last run are in warnings:
const generator: PdfGenerator = new PdfGenerator("/path/to/fonts");
generator.generate("document.xml", "document.css", "document.pdf");
generator.warnings.forEach((warning: string): void => console.error(warning));The package is the tarball cssmedia-x.x.x.tgz in the root of this repository.
npm install -g file:cssmedia-x.x.x.tgzThe -g option installs the package globally and adds the cssmedia command, which you can run from any folder. To install it locally in a project instead:
npm install file:cssmedia-x.x.x.tgzYou need Node.js 24 or newer and npm.
git clone https://github.com/maxprograms-com/CSSMedia.git
cd CSSMedia
npm install
npm run buildnpm run build compiles ts/ to dist/. The command is then available as:
node ./dist/cssmedia.js [options]The CSSMedia User Guide (PDF) explains how to run CSSMedia and how to write stylesheets for it. It is still a work in progress.
The CSSMedia CSS Reference (PDF) is the complete reference for CSSMedia. It has the tables of selectors, properties, at-rules and descriptors, grouped by subject, with the values and the specification of each entry. The same tables end the chapters of the User Guide.
Both documents are written in DITA XML and their PDFs are generated with CSSMedia itself, so they are also examples of what CSSMedia produces. See manual/README.md for the build instructions.
- TypesXML parses the XML documents that CSSMedia formats.
- TypesBCP47 validates and normalizes the language tags found in documents.
- TypesDitac merges a DITA map and its topics into the single XML file. The User Guide is built by processing the files in
manualwith TypesDitac and CSSMedia. - TypesBook merges a set of DocBook XML documents into a single XML.
Report problems and request features using GitHub Issues. For support, write to tech@maxprograms.com or visit the Maxprograms support page.
ts/: source files, in folders by component (css,fonts,images,layout,mathml,pages,paint,parse,pdf,svg,writer)dist/: compiled JavaScript and declarations (npm run build)manual/: source of the User Guide, written in DITA XMLlicenses/: licenses of the included components
CSSMedia is Copyright © 2026 Maxprograms. See the LICENSE file for the terms of use. License information for all included components is available in the licenses directory.



