Better Rich Text Buy licence

Documents

On this page
  1. Setup
  2. Commands
  3. Word
  4. Export
  5. Import
  6. Validation
  7. PDF
  8. In the browser
  9. Fonts
  10. Images
  11. Without the editor
  12. Print
  13. Server PDF
  14. Page breaks
  15. Full page
  16. Email
  17. Hooks
  18. Limitations

Word import and export, PDF files, full-page HTML and email export. Each format is its own package, so an editor only bundles what it uses:

PackageContents
@better-rich-text/format-docx DOCX export and import, plus the ZIP and XML readers they use. No DOM, runs in Node.
@better-rich-text/format-email Email-safe HTML: CSS inlining, table layout, plain-text alternative. No DOM.
@better-rich-text/format-pdf PDF files written from the document model, in the browser or Node. No dependencies. Loaded on demand by the documents plugin.
@better-rich-text/format-html parseHtmlDocument and serializeHtmlDocument for full HTML pages.
@better-rich-text/plugin-documents Editor plugin (commands, hooks, the documents service), the print pipeline and the toolbar feature.
@better-rich-text/plugin-page-break Page breaks that print, export to Word and survive HTML round trips. See Page breaks.

Setup

TypeScript
import { createRichTextEditor, standardPreset } from '@better-rich-text/core';
import { createDocumentsPlugin, documentsFeature } from '@better-rich-text/plugin-documents';

const editor = createRichTextEditor('#editor', {
  plugins: [createDocumentsPlugin()],
  features: [...standardPreset.features, documentsFeature],
  toolbar: `documents | ${standardPreset.toolbar}`,
});

documents is a menu with Import Word document, Export Word, Download PDF, Export PDF and Document properties. Each action is also a toolbar item of its own: importWord, exportWord, downloadPdf, exportPdf and documentProperties. Dropping a .docx file on the editor opens the import preview.

When a Word file has comments or tracked changes, the import preview lists how many and offers two choices: keep the comments, and keep the tracked changes as suggestions, accept them all or reject them all.

Plugin options:

OptionDefaultPurpose
filename the title, else document Base name for downloads.
docx.page A4 portrait, 25.4 mm margins Page size, orientation, margins, header, footer and page numbers written to the Word file. Overrides pdf.settings; the document's own page settings override both.
docx.defaultFont Calibri 11pt Body font in styles.xml.
docx.comments true Write comment threads as Word comments.
docx.trackedChanges 'track' Pending suggestions: track writes Word tracked changes, accept and reject apply or drop them.
docx.authorInitials(author) the user directory, else from the name Initials shown on Word comments.
docx.import { comments?, trackedChanges?, resolveAuthor? } for reading Word files (see Import).
fetchImage(src, node) fetch (same-origin credentials) Loads images that are not data: or blob: URLs for embedding.
uploadImage(image) data URL Stores images found in imported Word files. With no option set, the import dialog uses the image plugin's upload provider when there is one.
maxImportBytes 50 MB Largest file accepted for import.
pdf.renderer none Server PDF renderer (see Server PDF).
pdf.settings A4, 18 mm, page numbers Default page settings for PDF files and printing.
pdf.builtin true Write PDFs in the browser. false hides Download PDF and makes Export PDF open the print dialog.
pdf.fonts none TrueType fonts for text the built-in fonts cannot show, or an async function returning them (see Fonts).
pdf.options Body fontFamily (sans-serif or serif), fontSize, pageNumberFormat, preferEmbeddedFonts, decodeImage, compress.
email Default exportEmail options.
scripts drop Full-page script policy: drop or preserve (kept as inert data).
save(blob, filename) browser download Replace the download, for example to upload the file.

Commands

All commands are validated and appear in editor.toolDefinitions().

CommandInputResult
document.setProperties { title?, description?, keywords?, language?, direction? } (null clears) Undoable. Returns the new properties.
document.getProperties none { title, description, keywords, language, direction }
document.setFullPageHtml { html } Replaces body and head metadata in one undoable step.
document.getFullPageHtml { includeScripts? } { html }, a complete page.
document.commitImport { root, metadata?, annotations?, mode?: 'replace' | 'insert' } Commits an import candidate with its comment threads and suggestions. Undoable.
document.exportDocx { filename?, comments?, trackedChanges? } Starts a Word download.
document.downloadPdf print settings and filename? Writes the PDF in the browser and downloads it.
document.exportPdf print settings and filename? Server renderer when configured, otherwise the same as document.downloadPdf (or the print dialog when pdf.builtin is false).
document.print print settings Always opens the print dialog.
document.exportEmail { layout?, width?, preheader?, baseUrl? } { html, text, issues }

File work is asynchronous, so it lives on the documents service rather than inside a transaction:

TypeScript
const docs = editor.editor.service<DocumentsService>('documents')!;

const file = await docs.exportDocx();             // { blob, filename, issues, revision }
await docs.exportDocx('clean', { comments: false, trackedChanges: 'accept' });
const candidate = await docs.prepareImport(file);  // nothing changes yet
candidate.review;                                  // { comments: 2, trackedChanges: 5 }
docs.commit(candidate, 'replace');                 // one undoable step
docs.commit(candidate, 'replace', { comments: false, trackedChanges: 'reject' });
docs.pageSettings();                               // print and Word page settings for this document

docs.fullPageHtml();                               // complete HTML page
docs.exportEmail({ preheader: 'This week' });      // { html, text, issues }
const pdf = await docs.renderPdf({ size: 'Letter' }); // { blob, filename, pages, issues, revision }
await docs.downloadPdf(document, { size: 'Letter' });
await docs.exportPdf(document, { size: 'Letter' });  // renderer, built-in or print

Exports read the document at the moment they are called. Typing during a slow export does not change the file being written, and the result carries the revision it was made from.

Word

Export

exportDocx(root, options) writes a real OOXML package (document.xml, styles.xml, numbering.xml, settings.xml, relationships, document properties and media). It does not wrap HTML in an altChunk.

ContentWord output
Paragraphs, alignment, indent, line height, spacing, direction Paragraph properties
Headings 1 to 6 Heading1 to Heading6 styles with outline levels; anchors become bookmarks
Bold, italic, underline styles, strikethrough, superscript, subscript, small caps, inline code Run properties
Text colour, highlight (any colour), font family, font size w:color, w:shd, w:rFonts, w:sz
Links External hyperlink relationships; #anchor links point at bookmarks
Bulleted, numbered and nested lists, list start and style Numbering definitions; each list restarts its numbering
Checklists ☐ and ☒ bullets
Tables Grid columns from column widths, gridSpan for merged columns, vMerge for merged rows, cell shading, per-side borders, header rows that repeat on each page, captions
Images Embedded media sized from the image attributes or the file header, capped to the text width
Quotes, code blocks, horizontal lines Quote and code styles, a bottom-bordered paragraph
Footnotes footnotes.xml, when the schema has a footnote node
Page breaks A paragraph holding <w:br w:type="page"/>, when the schema has a pageBreak node

Content with no Word equivalent keeps its text and is listed in issues.

Review content and page furniture come from the options:

OptionWord output
annotations (editor.state.annotations) Comment threads and suggestion records to write
comments (default true) Each message of a thread is a Word comment in comments.xml, with author, initials and date. The anchored text sits between w:commentRangeStart and w:commentRangeEnd, followed by a w:commentReference run. Replies point at their thread through w15:paraIdParent in commentsExtended.xml, and resolved threads are marked w15:done. Mentions become @Name.
trackedChanges: 'track' (default) Suggested insertions and deletions become w:ins and w:del (deleted text in w:delText) with author and date. Format suggestions become w:rPrChange with the previous formatting. Proposed paragraph breaks, removed breaks and whole blocks become inserted or deleted paragraph marks, with w:ins or w:del on table rows. Paragraph type changes, such as paragraph to heading, become w:pPrChange.
trackedChanges: 'accept' | 'reject' Every pending suggestion is applied or dropped first, so the file has no tracked changes.
authorInitials(author) Initials on comments; by default the first letters of the first and last names.
page.header, page.footer header1.xml (centred) and footer1.xml (on the left), referenced from the section. {title} and {date} become Word TITLE and DATE fields.
page.pageNumbers "Page 3 of 7" on the right of the footer, with PAGE and NUMPAGES fields.
page.size, page.orientation, page.margins w:pgSz and w:pgMar. Sizes: A4, A5, A3, Letter, Legal and Tabloid.

The documents plugin passes the document's annotations, and takes the header, footer and page numbers from the PDF settings unless docx.page or the document says otherwise. Page numbers are on by default, as in print.

applySuggestions(root, annotations, mode) accepts or rejects every pending suggestion in a tree without an editor; resolveTrackedChanges(schema, root, annotations, mode) does the same, normalises the result and keeps comment anchors attached.

Import

importDocx(bytes, { schema, ids }) reads the package, resolves styles and numbering, and builds semantic HTML that then goes through the schema parser. Anything the schema does not accept is dropped there, the same as paste.

  • Heading, title, quote and code paragraph styles map to those blocks, following basedOn chains.

  • Numbered paragraphs become nested lists by level, with list style and start number.

  • Merged cells, shading, borders and header rows come back as table attributes.

  • Images go to uploadImage, or become data URLs.

  • Content controls and field results keep their text.

  • Comments become comment threads, anchored where their ranges were. Replies join their thread and resolved threads stay resolved when the file has commentsExtended.xml (Word 2013 and later, LibreOffice). A comment with no range is anchored at its reference, or kept as an orphaned thread.

  • Tracked changes (w:ins, w:del, w:moveTo, w:moveFrom, w:rPrChange, inserted and deleted paragraph marks) become pending suggestions with the original author and date. Changes next to each other by one author at one time form one suggestion. Inserted and deleted paragraph marks become proposed paragraph breaks and joins.

  • The last section's page size, orientation and margins, and the default header and footer text, become result.page. TITLE and date fields come back as {title} and {date}, and PAGE and NUMPAGES fields (with "Page … of …" around them) as pageNumbers: true. The documents plugin keeps these in the document's page metadata, which print and the next Word export use.

  • Footnotes become superscript markers and a numbered list at the end.

  • Page breaks (w:br w:type="page" and w:pageBreakBefore) become pageBreak nodes when the schema has one, splitting the paragraph at the break. Otherwise they are dropped.

Import options for the review layer:

OptionDefaultPurpose
comments true Read comments into threads.
trackedChanges 'track' track keeps changes as suggestions, accept and reject apply or drop them. track needs the suggestions plugin in the schema; without it changes are accepted and a warning is reported.
resolveAuthor({ name, initials }) word:<name> Maps a Word author to a user id. The documents plugin matches names against the user directory (@better-rich-text/users): the current user, then a search.

The result adds annotations (comment threads and structural suggestion records, ready for editor.state.annotations), page and review ({ comments, trackedChanges } counts for previews). document.commitImport adds the annotations with the content.

The reader rejects encrypted and ZIP64 archives, unsafe paths and DTDs, and enforces limits on entry count, total size, compression ratio and XML node count before content is committed.

Validation

validateDocxPackage(bytes) checks a package before it is offered for download or after it arrives: the archive opens, every XML part is well-formed and has a content type, relationships point at existing parts of the right type, comment ids are unique, every comment range opens before it closes and names an existing comment, revision ids are unique and have an author, deleted text is in w:delText inside a deletion, and header and footer references point at header and footer parts.

PDF

There are three ways to make a PDF:

WayHowUse it for
In the browser (default) Download PDF, or Export PDF without a renderer A file straight away, with no dialog and no server
Print Print in the Export PDF dialog, or document.print The browser's own print engine and its "Save as PDF"
Server pdf.renderer (see Server PDF) Full CSS fidelity, and scripts that need shaping

All three take the same settings:

SettingDefault
size A4 (also A5, A3, Letter, Legal, Tabloid)
orientation portrait
margins 18 mm each side
header, footer none; {title} and {date} are replaced. Files written in the browser also replace {page} and {pages}
pageNumbers true ("Page 3 of 7")
backgrounds true (table shading, highlights and code backgrounds)

In the browser

@better-rich-text/format-pdf lays the document out from the model, not from a screenshot of the page, and writes a PDF 1.7 file. Text stays selectable and searchable. The documents plugin loads it with a dynamic import() the first time someone makes a PDF, so it adds nothing to the editor bundle until then (about 24 KiB gzip when it loads).

The file has:

  • paragraphs with bold, italic, underline, strikethrough, superscript, subscript, small caps, inline code, text colour, highlight, font size and font family, aligned left, centre, right or justified;

  • links: web, mail and phone addresses become link annotations, and #anchor links jump to the heading or anchor they name;

  • headings 1 to 6, which also become bookmarks in the reader's sidebar;

  • bulleted, numbered (decimal, letters, Roman numerals, Greek letters, reversed, any start) and checklist items, nested to any depth;

  • quotes, code blocks (wrapped, indentation kept), horizontal rules (solid, dashed, dotted, double and thick) and callouts;

  • tables with column widths, merged cells, cell shading, borders and alignment, captions, and header rows that repeat on every page;

  • images: JPEG as is, PNG with transparency, and other formats through the browser;

  • page breaks from @better-rich-text/plugin-page-break;

  • running header and footer text, page numbers, and the title, author, description, keywords and language from the document properties.

Lines break at spaces, after hyphens and dashes, and between Chinese, Japanese and Korean characters. A word wider than the line breaks between letters. No hyphens are added. A heading stays on the same page as the line after it, a paragraph never leaves a single line at the bottom or top of a page, and a table row moves to the next page whole unless it is taller than a page, when it splits between its blocks.

renderPdf(settings) returns { blob, filename, pages, issues, revision } without downloading. downloadPdf(document, settings) also saves the file, through save or a download link. Problems that do not stop the export, such as an image that could not be loaded, come back in issues. After a download from the toolbar they appear in a notice.

Fonts

Latin text uses the fonts every PDF reader has: Helvetica (or Times, with pdf.options.fontFamily: 'serif') and Courier for code. They cover Windows-1252: English and the Western European languages, plus curly quotes, dashes, the euro sign and similar punctuation.

For other scripts, such as Greek, Cyrillic, Chinese, Japanese and Korean, supply TrueType fonts. A character the built-in fonts cannot show uses the first supplied font that has it, so one font per script is enough. Only the glyphs used are embedded, and a ToUnicode map keeps the text copyable and searchable. Pass a function to load large fonts only when someone exports:

TypeScript
const load = async (url: string) => new Uint8Array(await (await fetch(url)).arrayBuffer());

createDocumentsPlugin({
  pdf: {
    fonts: async () => [
      { data: await load('/fonts/NotoSansJP-Regular.ttf') },
      { data: await load('/fonts/NotoSansJP-Bold.ttf'), bold: true },
    ],
  },
});
  • Fonts need TrueType outlines: .ttf files, or .ttc collections with index. OpenType fonts with CFF outlines (most .otf files) are reported and skipped. Google Fonts serves TrueType files for most families, Noto included.

  • A regular font also covers bold and italic text: bold is drawn with a thin outline and italic with a slant. Supply bold and italic files for better results. bold and italic on a font source override what the font says about itself.

  • preferEmbeddedFonts: true uses the supplied fonts for every character they have, so the whole document is set in your font.

  • Fonts whose licence does not allow embedding are reported and skipped.

A character no font can show is drawn as ?. The export reports it once, with the code pdf-missing-glyphs and the characters in details.characters. Nothing throws.

Images

JPEG files go into the PDF unchanged, and so do PNG files without transparency. PNGs with an alpha channel, a transparent colour or interlacing are decoded and written with a soft mask, so transparency survives. GIF, WebP, BMP and SVG are drawn to a canvas by the browser first. Images load through fetchImage, as for Word export. An image that cannot be loaded or decoded is written as its alternative text and reported.

Without the editor

exportPdf works on any document root, in the browser or in Node:

TypeScript
import { exportPdf } from '@better-rich-text/format-pdf';

const { data, pages, issues } = await exportPdf(root, {
  metadata, // title, language, head.author, head.description, head.keywords
  size: 'A4',
  margins: { top: 20, bottom: 20 },
  header: '{title}',
  footer: 'Page {page} of {pages}',
  pageNumbers: false,
  fonts: [{ data: fontBytes }],
  fetchImage: async (src) => ({ data: await read(src), contentType: 'image/png' }),
});

Node has no canvas, so only JPEG and PNG images are embedded there unless you pass decodeImage(bytes, type) returning RGBA pixels. Streams are compressed with CompressionStream, or written uncompressed where it is missing.

Print

Print renders the published HTML into a hidden, script-free frame with a print stylesheet and opens the print dialog, where "Save as PDF" writes the file. Set pdf.builtin: false to make Export PDF do this instead of writing the file in the browser.

Settings come from pdf.settings, then the document's own page settings (from an imported Word file), then the dialog. Page size and margins use @page. The header, footer and page numbers use paged-media margin boxes, which Chromium 131 and later support, as do headless Chromium, Prince and WeasyPrint. Firefox and Safari print the content with the right size and margins but without those running elements.

The print.document filter receives the complete print HTML before it is rendered, and the print.ready action receives the frame's document once its images have loaded.

Server PDF

Pass a renderer to produce the PDF on a server instead. It receives the same self-contained HTML and returns PDF bytes:

TypeScript
import { createDocumentsPlugin, httpPdfRenderer } from '@better-rich-text/plugin-documents';

createDocumentsPlugin({ pdf: { renderer: httpPdfRenderer('/api/pdf') } });

httpPdfRenderer posts a PdfRenderRequest as JSON: { html, title, filename, settings, documentId, revision }. A Node endpoint using Playwright:

TypeScript
import { chromium } from 'playwright';

app.post('/api/pdf', async (req, res) => {
  const { html } = req.body;
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({ javaScriptEnabled: false });
    await page.route('**/*', (route) => (route.request().url().startsWith('data:') ? route.continue() : route.abort()));
    await page.setContent(html, { waitUntil: 'load' });
    const pdf = await page.pdf({ preferCSSPageSize: true, printBackground: true });
    res.type('application/pdf').send(pdf);
  } finally {
    await browser.close();
  }
});

Run the renderer without network access to the host's internal services, with JavaScript disabled, and with time and size limits. A repeated export cancels the request still in flight instead of starting a second job.

Page breaks

@better-rich-text/plugin-page-break adds a pageBreak block. The editor shows it as a dashed line labelled "Page break"; printing, PDF and Word start a new page there.

TypeScript
import { createPageBreakPlugin, pageBreakFeature } from '@better-rich-text/plugin-page-break';

const editor = createRichTextEditor('#editor', {
  plugins: [createPageBreakPlugin()],
  features: [...standardPreset.features, pageBreakFeature],
  toolbar: `${standardPreset.toolbar} | pageBreak`,
});
editor.commands.pageBreak.insert();

pageBreak.insert splits the block at the caret and puts the break between the halves. At the start of a block, or in an empty one, the break goes before it. With a block selected as a node, the break goes after it. Pass { split: false } to put the break after the current block instead. The shortcut is Mod-Enter. Inside a code block Mod-Enter still leaves the block, because codeBlock.exit binds it at a higher priority.

Click a page break, or press an arrow key at the edge of the next or previous block, to select it. Backspace and Delete remove it.

OptionDefaultPurpose
exportAs 'div' div writes <div style="page-break-after: always; break-after: page"></div>, hr puts the same style on an <hr>, comment writes TinyMCE's <!-- pagebreak -->
separator none Any HTML string to write instead, like TinyMCE's pagebreak_separator. It is read back as a page break
splitBlock true false puts the break after the block, like TinyMCE's pagebreak_split_block: false
shortcut 'Mod-Enter' Another key, or false for none
selectors .page-break, .pagebreak, .pageBreak Empty elements that also count as a page break on import
label 'Page break' Text on the divider, the toolbar button and in the command palette

Pasted and imported HTML keeps page breaks from:

  • this plugin's own output, in any exportAs mode;

  • TinyMCE: <!-- pagebreak --> and its img.mce-pagebreak editing image;

  • Word: <br clear=all style="page-break-before: always">, including mso-special-character: line-break;

  • any block with page-break-before/after: always or break-before/after: page, such as Word headings and Google Docs' hidden <hr>. A break inside a paragraph splits it.

Comments and custom separators still print as page breaks: the plugin swaps them for the div form in the print.document filter. Markdown export writes the div form as HTML.

Full page

TinyMCE's Full Page HTML equivalent. The editor edits the body; the rest of the page is kept in document metadata:

HTMLMetadata
<title> metadata.title
<html lang>, <html dir> metadata.language, metadata.direction
<meta name="description">, keywords, author, viewport metadata.head.description, and so on
Other <meta>, approved <link> relations, <style> metadata.head.meta, links, styles
<body> and <html> attributes metadata.head.bodyAttributes, htmlAttributes

Nothing from the head runs. Scripts are removed (or kept as inert data under scripts: 'preserve', and only written back with includeScripts). Event handlers, <base>, <meta http-equiv="refresh">, @import, expression() and javascript: URLs are removed and reported. Output is always UTF-8.

TypeScript
import { parseHtmlDocument, serializeHtmlDocument } from '@better-rich-text/format-html';

const { body, metadata, diagnostics } = parseHtmlDocument(html);
const page = serializeHtmlDocument(bodyHtml, metadata);

The Document properties dialog edits title, description, keywords, language and direction through document.setProperties.

Email

exportEmail(host, root, options) (or the document.exportEmail command) produces HTML that mail clients render consistently:

  • Every rule of the content stylesheet is inlined into style attributes by the normal cascade. Rules that cannot be inlined, such as media queries, stay in a <style> block.

  • var() references resolve from options.variables, so a skin's tokens can be passed in. Logical properties become physical ones for the text direction.

  • With layout: 'table' (the default) content sits in a centred table of width pixels (600 by default).

  • Relative links and images become absolute through baseUrl. Local blob: images are removed; data: images are kept and reported.

  • Video, audio and frames become links. Forms, scripts and embedded objects are removed. Checklists become ☐ and ☑ lines. Classes and data-* attributes are dropped.

  • text is a plain-text alternative: underlined headings, list markers, > quotes, link addresses in brackets, pipe-separated table rows, wrapped at 76 columns.

  • Merge fields such as {{first_name}} pass through unchanged.

Pass css to replace the built-in content stylesheet (EMAIL_CONTENT_CSS) or extraCss to add brand rules. inlineCss(html, css) is available on its own.

Hooks

HookKindArguments
docx.exportNode filter (xml: string | null, node, { kind }); return OOXML to replace a node's output
docx.exportParts filter (parts: Map<path, bytes | string>); add or replace package parts
docx.importNode filter (html, element, context); rewrite the HTML produced for a Word block
email.inlineStyles filter (declarations, { tag, attributes }); adjust each element's inline styles
email.output filter ({ html, text, issues }, root)
print.document filter (html, settings)
documents.exported action { format, filename, issues, revision }
documents.imported action { format, mode, issues }
documents.failed action { format, error }
print.ready action (frameDocument, settings)

Limitations

  • Word export writes one section with one default header and footer of plain text. Section breaks, first-page and even-page headers, and formatting or images in headers and footers are not written; on import they are left out and reported, and their text is kept.

  • Comments carry message text, authors, dates, replies and resolved state. Reactions, edit times, who resolved a thread and formatting inside Word comments do not survive. Threads not attached to text are not exported. Comment threads on content inserted at the cursor (rather than replacing the document) become orphaned, because inserted content gets new node ids.

  • Suggestion and author ids do not survive Word. Authors come back by name (see resolveAuthor), and changes by one author at one time next to each other become one suggestion.

  • Suggested wraps, unwraps and moves (lists and quotes) have no Word tracked-change form: they are written as proposed, and trackedChanges: 'reject' leaves them in place with a warning. Changes to images, table cells, numbering and paragraph properties other than the style are imported in their current form. Tracked paragraph marks in lists and tables become proposed breaks only between neighbouring items.

  • Spaces at the start or end of a paragraph are dropped on import.

  • Some attributes do not survive a Word round trip: code block language, reversed and Greek-numbered lists, links on images, image floats (exported inline), table banding, accent and density, and paragraph spacing before and after. Cell text alignment comes back as paragraph alignment.

  • Imported footnotes become superscript numbers and a notes list, because the schema has no inline note container.

  • Endnotes, equations (kept as text), shapes, text boxes, linked external images and ZIP64 or encrypted files are reported and skipped.

  • Images that cannot be fetched (cross-origin without CORS, for example) are exported as their alt text and reported.

  • Import keeps direct formatting that the schema supports (colour, font, size, highlight) and drops the rest, such as character spacing and text effects.

  • Legacy .doc, .rtf, .odt and PDF import are not handled in the browser; they need a conversion service.

  • Printed PDFs depend on the browser's print engine. Files from Download PDF are the same in every browser.

  • PDFs written in the browser do not shape text: Arabic letters are not joined, Indic scripts such as Devanagari lose their conjuncts, and right-to-left text is aligned right but not reordered. Use a server renderer for those scripts.

  • They have no kerning, ligatures or hyphenation, are not tagged for screen readers and are not PDF/A.

  • Floated images sit on their own line instead of having text wrap around them. Formulas are written as their TeX source, and footnotes are listed at the end of the document rather than at the foot of each page.

  • Stylesheets from the full page, the css print setting and the print.document filter apply to printing and server renderers only.

  • Rows joined by a row span always move to a new page together, and a table nested in a cell does not split across pages.

  • Email output is checked against common client rules, but no export renders identically in every client.