Escape

The HTML target

Intent

@quario/html serialises the event stream into a fragment you drop into your own page. It’s not a document. There is no <html>, no <head>, and no presentation beyond what a definition declared. The fragment goes into a template, an email body or a server-rendered response.

Design decisions

This target is a fragment, not a painter, and not a flow target. It has no pages of its own to break or to hand to a reader’s application, which is why page bands go unread below. Painters, flow targets, and a fragment describes the family.

The fragment carries no pagination, and leaves page bands unread. Page furniture belongs to the host’s print CSS. A group’s break maps to a class instead of vanishing, so your print stylesheet can act on it. q-break marks an instance whose leading edge turns, and q-break-after one whose trailing edge does.

This target escapes everything a definition or its data contributes. Escaping happens here, at the markup edge, because the event stream carries unescaped data. The target escapes literal template text and header cells as well as interpolated values. A definition’s own <h2> reaches the reader as the characters it’s made of.

It escapes generated attributes too. Data attributes, inline styles, src, alt and colspan all carry data. Class names are constants this target owns.

The q-* selectors are the contract, and the stylesheet carries no look of its own. It holds the band-role defaults, the table’s structure, and the honor and pagination rules. They’re ordinary rules a later host sheet overrides on source order. Since 0.7.0 it draws no hairline under a table’s header row, and no rule or bold weight on its total rows. It draws no rule above the report footer either, because no other target ever drew them. A report that declares no borders now reads the same on screen as in the PDF and the worksheet.

Styled runs render as spans. One <span style="…"> per styled run, carrying its whole resolved inline style. Unstyled runs stay bare, so a cell without runs emits the markup it always did.

A fragment without the sheet still carries the classes and the text. It doesn’t occupy, break or paginate until some stylesheet says so.

The report default rides the root as an inline style. That’s the one place the rest of the document can inherit it from.

The unlicensed marking sits outside the report root. Inside it, an author’s declared size would reach it. It’s the fragment’s first element and the root’s sibling, holding only the escaped wording.

API walkthrough

Rendering

render.ts
import { html } from '@quario/html'

const fragment = await report.render(html(), data)

Rendering is asynchronous and returns control to the event loop between batches, so a large report doesn’t block the host. Compilation stays synchronous, and render errors reject with their location attached.

The same definition, data, and options produce an identical fragment. The target stamps nothing, and an unformatted Date writes as ISO 8601 UTC rather than in the host’s zone.

Styling the output

page.css
@import '@quario/html/style.css';

Two rules in that sheet are worth knowing, because a fragment without them behaves differently. .q-item carries min-height: 1lh and white-space: pre-line, so a visible text item occupies a line and a literal newline is a break. Table cells carry pre-line too, and empty cells have no min-height, so they don’t occupy.

The classes are yours to restyle. Override on source order, the way you would any vendor sheet.

A host that wants the look the sheet carried before 0.7.0 adds three rules after the shipped one.

page.css
.q-table thead th { border-bottom: 0.5pt solid #000; }
.q-table tfoot td { border-top: 0.5pt solid #000; font-weight: bold; }
.q-item.q-report-footer { border-top: 1pt solid #000; padding-top: 4pt; }

The report header’s own rule is a factor rather than a point size: .q-item.q-report-header is font-size: 1.4em. An unstyled fragment renders its headline at 1.4× the host page’s body type rather than at a fixed 14pt. A host whose body isn’t 10pt sees that move. A host rule on the class still wins, the way it always did.

Linking a run

The target wraps a run carrying an href in an <a> inside its own span, so the style the run declared paints the link. It’s a real anchor rather than a styled span, so a keyboard reaches it the way it reaches any link. The target escapes the URL at the one markup edge like every other interpolated value. The engine admitted it before it crossed, so this target decides nothing about it.

A cell’s own href links the cell’s text. The target asks the style a run wears, which is the cell’s wherever the run declares none. A run declaring its own replaces the cell’s rather than nesting inside it.

Mapping a font family

render.ts
import { html } from '@quario/html'

const target = html({ fonts: { Inter: 'var(--font-inter)' } })

A declared family name maps to a CSS font-family value. Names match case-insensitively, and a mapping may cover the generics like any other name. A value containing ; or } throws from html() rather than reaching the page.

This mapping resolves names a report declared, and never the face of text that declared nothing. That baseline lives in the reference stylesheet, which is where a host moves it.

Mapping output back to the definition

render.ts
import { html } from '@quario/html'

const target = html({ paths: true })

Each element whose event carries a schema path gets data-q-path. It’s off by default, and it’s the selection seam the editor builds on.

Printing

A fragment plus a print stylesheet is a legitimate route to paper, and it’s the one the browser already knows how to drive. The trade is that pagination becomes the browser’s, so page bands, repeated table headers, and deterministic bytes aren’t part of the bargain. Where those matter, render through the PDF target instead, from the same definition.

A document that declares its paper gets one <style> in the fragment. It holds an @page rule with that size and each margin, so the print dialog opens on the right sheet. It’s the only <style> this target writes.

A note on Content Security Policy

Conditional formatting emits inline style attributes, so a restrictive style-src has to account for them. An image item embeds bytes as a data: URI (data:image/svg+xml for an SVG), which needs img-src data:. Neither is a hole, and neither affects the no-string-to-code guarantee, which covers script execution. Trust and CSP has the whole picture.