Skip to content

The theme validator ​

The portal only distributes a theme if it passes the theme validator: it must implement the entire Core v1 language, be safe and load nothing from outside. This page explains what it checks, and why.

Why a validator ​

A browser never reports a CSS error: a badly written rule is silently ignored, a forgotten class simply does nothing. Someone who writes <!-- _class: content gallery-bento --> with a theme that forgot gallery-bento gets a broken slide, with no message at all.

The validator therefore guarantees that a distributed theme implements the whole language, and that it is safe and self-contained: it will be sent to every machine and included in every render.

Where it runs ​

A single module, packages/core/src/theme-rules, applied identically everywhere:

LocationWhenStagesRole
Portal editor (browser)While typing, after a short pause1 to 6Immediate feedback, preview, geometry measurement
Portal serverSaving a draft, publishing1 to 5, plus the stage 6 reportAuthority: rejects a non-compliant theme
VS Code extensionInstalling and using any theme, from the portal or local1 to 4Reports a non-compliant theme in Problems and the status bar

Each result carries the rules version (theme-rules@2), stored with the published version. A rule added later does not invalidate versions already published: they keep their original report.

Error or warning ​

Each finding has a stable identifier (contract.class-without-effect…), a severity, a position in the CSS when it makes sense, and a translated message with a suggested fix.

  • Error: the version cannot be published, so it is never distributed.
  • Warning: displayed, does not prevent publishing.

A draft can be saved with contract or rendering errors (work in progress), but not with reading, identity or security errors (stages 1 to 3). Publishing requires zero errors.

The six stages ​

The CSS goes through six stages, in order. Stages 1 to 5 are static: they run everywhere, including on the server, without a browser. Stage 6 requires a real rendering engine.

Stage 1 — Reading ​

RuleCheckWhy
format.encodingNo null characterCorrupted or binary file
format.syntaxThe CSS is parsed by a real CSS parser (css-tree); each syntax error is reported with its lineThe browser would ignore the faulty rule without warning

From then on, the validator works on a syntax tree, not on the text: a comment, a string or an escape cannot fool the following checks.

Stage 2 — Identity ​

RuleCheckWhy
identity.theme-missing, identity.theme-multipleExactly one /* @theme Name */This is the name authors write in theme:
identity.theme-nameA letter, then letters, digits, - or _; 64 characters maxA name usable everywhere without ambiguity
identity.name-stableA new version keeps the theme nameOtherwise existing presentations would lose their theme
identity.language-missing, identity.language-unknown/* @dwa-language core-v1 */ present; only core-v1 existsDeclares the implemented contract
identity.slide-size-unit, identity.slide-sizeIf the theme sets the size of section, it is in pixels and in 16:9 formatPowerPoint export and overflow measurement assume 16:9

With no declared size, Marp uses 1280 × 720: nothing is reported.

Stage 3 — Security and self-containment ​

A remote resource would make it possible to track when presentations are opened, would break offline work and could change after publication: the theme fingerprint would no longer guarantee anything.

RuleCheck
security.urlEvery url(…) (and the strings in src(), image-set(), cross-fade()) is a data: URI. https:, relative paths and other schemes are rejected
security.data-encodingThe embedded resource is base64-encoded
security.mimeAccepted types: PNG, JPEG, GIF, WebP, WOFF, WOFF2, TTF, OTF
security.payloadThe first decoded bytes match the declared type (PNG, JPEG, WOFF signature…)
security.svgSVG rejected, even embedded: it can contain scripts and external references
security.import@import rejected, except for Marp's built-in themes (default, gaia, uncover)
security.at-ruleAccepted at-rules: @font-face, @media, @supports, @keyframes, @layer, @container, @charset, @import (case above)
security.style-breakNo </style sequence, even in a comment: Marp inserts the theme in a <style> tag
security.legacyexpression(), behavior, -moz-binding, javascript: rejected

CSS escapes (\2014) remain allowed: the parser decodes them and the checks apply to the decoded values. A u\72l( is seen as url( and checked.

Stage 4 — Core v1 contract: every class must have an effect ​

This is the heart of the validator. For each class in the reference, the question is: "if an author writes it, does anything change?" The list of classes is generated from the shared catalog, the same one used by the extension's autocompletion.

The differential test:

  1. For each class, the validator builds a test slide: the HTML that Marp produces for the class's example Markdown (the one the extension inserts).
  2. It computes, without a browser, the declared values of each element of the slide and its pseudo-elements: which rules apply, and which one wins according to the cascade (!important, specificity, order). Selectors are evaluated on a virtual DOM, including :is(), :not(), :has() and attribute selectors such as [class*='image-text'].
  3. It redoes the computation on the same slide without the tested class.
  4. If no value changes, the class is not implemented.

Example with theme/DWA.css, which contains section.cols-3 { --columns: 3; … }:

text
Tested class: cols-3            Slide: <section class="content cols-3"> + 3 groups
Without cols-3 → section: --columns not defined
With cols-3    → section: --columns = 3
⇒ effect found ✓

For a theme that forgot gallery-bento, the values are identical with and without the class: error "Class “gallery-bento” has no effect: it is not implemented."

Why values rather than "a rule mentions the class": a rule can mention the class but always be overridden by another; conversely, DWA.css uses fragment selectors ([class*='image-text']) that never mention image-text-70 in full. The test judges the effect, however it is written.

RuleCheckSeverity
contract.class-without-effectThe class changes at least one declared value. Default classes (color-1, fit) may change nothingError
contract.distinct-valuesTwo members of the same family do not give the same result: cols-3 ≠ cols-4, image-text-30 ≠ image-text-70, color-2 ≠ color-5… Otherwise an author would choose an option that does not existError
contract.callout-sizecallout-size-N gives the callout title a size of N px: the extension and PowerPoint export rely on this numberError
contract.text-effectText stays text in the PowerPoint export: text painted by a background (background-clip: text), -webkit-text-fill-color, glyph outlines (text-stroke) and vertical writing (writing-mode) are rejected. Gradients, pseudo-elements and decoration transforms remain freeError
contract.selector-skippedSelector the validator could not evaluate: its effect is not taken into accountWarning

Additional classes specific to the theme are allowed: the contract defines what must exist, not what is forbidden.

Stage 5 — Marp rendering ​

The CSS is registered in Marp Core and all test slides are rendered. A failure is a render.marp error: the theme must be accepted by the tool that will use it.

Stage 6 — Geometry checks ​

The test slides are rendered in a real browser, at the theme's slide size, and the geometry is measured (getBoundingClientRect, 2 px tolerance).

RuleCheckSeverity
visual.insideNo element extends outside the slide (except the intentionally cropped image of image-full)Error
visual.columnscols-N: N groups on the same row, left to right, of equal widthError
visual.splitimage-text-N: image on the left, image share ≈ N % (± 3 points); text-image-N: on the rightError
visual.gallerygallery cols-N: N images per rowError
visual.bentogallery-bento: image 1 spans both rows, image 2 spans two columnsError
visual.timelinetimeline: steps aligned horizontally; timeline-vertical: stackedError
visual.titlewith-title: title above the content, full widthError
visual.legendwith-legend: legend below everything elseError
visual.chart-widechart-wide: two comments side by side above the visualError
visual.contrastFor each palette, text contrast on the background color ≥ 4.5:1Warning

In the portal editor, measurement happens in an isolated iframe, with no scripts or network. The server has no browser: on publication, the editor sends its stage 6 report, and the server requires it to be error-free, computed on the same SHA-256 fingerprint of the CSS and the same rules version. It redoes stages 1 to 5 itself.

Theme size ​

There is no size rule: images and fonts embedded in base64 naturally make the CSS grow. Only a technical transport limit remains: the maximum size of a request sent to the portal.

Known limitations ​

  • Stage 4 proves that each class does something, not that it does it well: that is the role of stage 6, which only covers the measurable compositions listed above.
  • The virtual cascade is not a rendering engine: @media and @supports conditions are not evaluated, and an unevaluated selector is only reported with a warning.
  • Contrast does not analyze background images; check readability in the preview.
  • Test slides have short texts: they do not guarantee that a real presentation will not overflow. The extension's overflow measurement takes care of that.
  • Stage 6 is measured by the editor's browser: someone who forged their report would bypass the geometry checks, but would only publish within their own organization. Security (stages 1 to 3) is always checked by the server.
  • The extension applies stages 1 to 4; it does not redo Marp rendering or geometry checks.

Documentation of the DWA Core v1 language, the VS Code extension, the CLI and the theme portal.