Skip to content

Paged layout

The current implementation/source map and smoke-test handoff are in DEVELOPER_REFERENCE.md. This document is the geometry and pagination contract.

Every Templar template must render in both modes:

  • Pageless: width follows the Markdown pane and text can rewrap when the pane changes.
  • Paged: typography is laid out on a fixed-width sheet. Pane changes alter only the scale of the entire sheet; text positions and line breaks stay fixed.

The mode belongs to the note and is stored with the copied design. It is not a template variant.

page:
mode: paged # paged | pageless
size: a4 # a4 | letter | custom
width: 794 # CSS pixels
height: 1123 # CSS pixels
gap: 32 # requested visual gap
scale-to-fit: true

Presets use 96-CSS-pixel approximations:

Preset Width Height
A4 794 1123
US Letter 816 1056

Custom dimensions are clamped to 480–1800px wide and 640–2400px high.

In paged mode, .templar-page-content always retains the stored CSS width. The pane never changes this layout width. PageLayoutService computes:

scale = min(1, usable pane width / stored page width)

Usable width excludes the page root’s computed inline padding and a small sheet gutter. Fit-to-screen mode deliberately has no minimum scale: even a wide custom sheet fits a phone instead of silently overflowing. Templar writes only --templar-page-scale. The compiler applies that through CSS zoom, which scales visual and layout footprint together while descendants continue to calculate against the fixed unscaled page width.

DOM engines have historically disagreed about whether getBoundingClientRect() includes CSS zoom; WebKit only standardized zoom-scaled rectangle values in Safari 26.4. Templar feature-detects the behavior on every layout pass by comparing the rendered content width with the stored fixed width, then uses that measured geometry factor for all pagination math. This keeps older iOS WebViews and Chromium on the same page coordinates.

This means:

  • a 90-character line wraps at the same word in a full window, split pane, or phone;
  • images using percentages calculate from the same page content box;
  • font sizes and baseline positions remain fixed in page coordinates;
  • the user can still scroll vertically through sheets.

If scale-to-fit is false, the scale remains 1 and the page can scroll horizontally in a narrow pane.

The page content has an isolated ::before surface containing the template paper color and pattern. A repeating mask reveals one fixed-height sheet followed by a transparent gap, producing a PDF-like stack over Obsidian’s secondary background.

The requested gap is adjusted upward to make this equation true:

(page height + effective gap) modulo baseline unit = 0

Because the page span is a grid multiple, a continuous repeating ruling has the same phase at the top of every sheet. The paper therefore follows the measured font baseline on page 2 exactly as on page 1.

The compiler emits a fixed paper origin from configured top padding plus the measured body baseline. BaselineGridController uses that absolute lattice in both active adapters; it never derives phase from the first currently attached child and never changes phase on scroll. Properties/frontmatter UI is excluded from ownership, renderer widgets are measured at their outer owner, and ordinary Live Preview lines remain CodeMirror-owned. Pagination and paper paint therefore use the same lattice, so ordinary glyphs rest on the rule and descenders cross below it without a one-row phase change when the view switches or a note omits Properties.

CSS alone cannot paginate editable CodeMirror DOM vertically. PageLayoutService uses rendered-block fitting without rewriting Markdown:

  1. Observe the page root, content, rendered break candidates, and images for size and child/text changes.
  2. Coalesce work into one animation frame.
  3. Clear prior computed break variables.
  4. Enumerate visible top-level Reading blocks or CodeMirror lines/widgets.
  5. Measure each block in unscaled page coordinates against the same absolute baseline lattice used by both view adapters.
  6. If a block would cross the printable bottom or begins in a gap, add a computed top-margin offset to the next sheet’s content start.
  7. Continue sequentially so later measurements include earlier breaks.

The offset uses CSS custom properties:

margin-top: calc(
var(--templar-original-margin-top) +
var(--templar-page-break)
);

No Markdown text or renderer-owned node is moved. Clearing the style or switching to pageless removes all variables.

Reading view exposes top-level rendered blocks. Templar avoids splitting a single block across wrappers; a block taller than the printable area is left intact rather than entering an infinite move loop.

Live Preview is CodeMirror 6 and virtualizes long documents. Templar fits currently rendered .cm-line and widget blocks. As CodeMirror changes the rendered viewport, MutationObserver schedules another pass. This keeps editing intact, though it is not a typesetting engine for splitting a single giant widget across sheets.

  • Pane resize: recalculate scale, then page positions; fixed layout width does not change.
  • Page style or mode change: rebuild scoped CSS and observers.
  • Font/style change: FontMetricsService recalculates, compiler changes baseline variables, layout repaginates.
  • Image load/resize: image compensation and page layout observers schedule new fitting.
  • Plugin unload or leaf close: disconnect observers, cancel animation frames, remove scale/break properties.

PrintService cooperates with the host browser/Obsidian print pipeline; it does not generate PDF files itself. Before invoking window.print() it:

  1. acquires a per-service busy lock and verifies that the requested file still owns the requested leaf;
  2. stores the exact Markdown view state and temporarily switches that leaf to Reading View;
  3. forces the leaf through the latest renderer generation;
  4. waits for document.fonts.ready, current images to decode or settle, and a quiet mutation/resize window followed by two animation frames;
  5. prepares PageLayoutService at scale 1 with the screen gap removed and repaginates;
  6. appends temporary print rules to that leaf’s renderer-owned scoped style.

A4 emits @page { size: A4 }, Letter emits @page { size: Letter }, and custom pages request their stored CSS-pixel dimensions. Paged sheets lose screen-only shadows/gaps while keeping paper, pattern, watermark, images, tables, callouts, code, and calculated breaks. Pageless notes use natural paper pagination while retaining content styling. afterprint, a platform fallback timeout, an exception, or service destruction removes temporary CSS/ownership classes, restores screen scaling/layout, and returns the leaf to its exact pre-print mode.

Printing remains unavailable where the host platform does not expose a usable print action; no Electron or Node dependency is introduced.

With strict or balanced baseline alignment, a Markdown horizontal rule owns one complete grid row in both Reading and Live Preview. Its external margins are zero, the visible stroke is centered, and all five divider styles share the same footprint. Pagination therefore measures a divider as exactly one unit: it either fits as a whole row or moves according to the ordinary fitted-block algorithm. Free/disabled baseline modes retain non-grid spacing.

Tables, Mermaid and other rendered fenced blocks, callouts, embeds, and media retain their natural height. In strict/balanced modes, BaselineGridController observes each direct renderer-owned Reading section or Live Preview widget and appends only the missing fraction to make its complete border-box-plus-margins footprint the next baseline multiple. It never observes the whole Reading document. Precise resize measurements and DOM writes are coalesced to an animation frame, and the previous owned tail is subtracted before recalculation so the observer cannot feed back into itself. Trailing rhythm is written as an explicit occupied margin; no pseudo-element is used to fake flow height. The page-layout observer sees the corrected size and schedules normal block fitting, including after an async diagram or embed changes height. Explicit blank-line rows are separate from this correction, so pagination measures and preserves them. Pageless, A4, Letter, and custom pages share the same calculation; free/disabled baseline modes skip it.

Paged mode uses only web-platform APIs available to Obsidian mobile: CSS, ResizeObserver, MutationObserver, animation frames, and DOM geometry. It does not use Electron, Node.js, filesystem paths, or desktop-only zoom controls.

The fixed canvas normally scales below 1 on a phone. Touch scrolling remains on the Obsidian page root, not a nested transformed overlay. Template custom CSS cannot use viewport-width media queries, preventing a template from silently changing its layout when a phone rotates or a split changes.

For a paged renderer change:

  1. Create the same long note in A4 and Letter.
  2. Record line endings and page starts at a wide width.
  3. resize the pane through at least five widths, including phone width.
  4. Confirm line endings and page-start blocks remain identical while only apparent scale changes.
  5. Edit near the top, middle, and bottom in Live Preview.
  6. Load a late image and confirm later pages repaginate.
  7. Switch paged → pageless → paged and confirm all computed style variables cleanly reset.
  8. Test strict ruled and free blank templates.
  9. Repeat with a missing first-choice font to exercise fallback metrics.
  10. Run on physical iOS and Android before release.
  11. Print A4, Letter, custom, and pageless samples; verify gap/shadow removal, page-size requests, patterns, watermarks, images, tables, callouts, code, and page breaks.
  12. Place solid/dashed/dotted/double/fade rules before, after, consecutively, and near a page boundary at several grid units; every following baseline must remain congruent.
  13. Place tables, Mermaid diagrams, callouts, embeds, and media before ordinary text, with zero/one/several explicit blank lines, then resize or let async content settle near A4 and Letter boundaries; following text must remain congruent without observer loops.