Work in progress — this reference is being written in the open. Unfinished pages are excluded from search engines.
Paged · IDML Reference
Styles

The cascade

BasedOn links a style to a parent; the parser folds that chain into one resolved set of values, bounds cycles with a depth cap, and lets a local override on a range still win.

Intermediate· explanation

The cascade is how the parser folds a style and its BasedOn parents into one resolved set of values.

In short: A style in IDML rarely sets everything it needs. It inherits from a parent through the BasedOn attribute, which names another style of the same kind, all the way down to a root sentinel. When something applies a style, the parser walks that chain upward and merges the styles together so that the first style in the walk to set an attribute wins and the walk only continues to fill what is still unset. The walk is bounded at sixteen hops so a cyclic or corrupt file degrades gracefully, and a value set directly on a range still sits above the whole cascade. This page explains how BasedOn resolution works; how a local override layers on top of it is covered in conflict resolution.

A style rarely says everything. A "Body Indented" style might set only a first-line indent and leave its font, size, and colour to be inherited from "Body", which in turn inherits the document's base font from the root. IDML records that lineage with one attribute — BasedOn — and leaves the work of following it to whoever reads the file. This page is about how our parser does that following.

[No paragraphstyle] (root)BasedOn"Body"appliedthe runPointSize="18" (local override wins)
A run resolves through the BasedOn chain; a local override on the range wins over both.

BasedOn names a parent

Every paragraph, character, object, cell, and table style may carry a BasedOn attribute (or, in InDesign's serialisation, a <BasedOn> child element inside <Properties> — the parser accepts both forms). Its value is the Self id of another style of the same kind, or the root sentinel — ParagraphStyle/$ID/[No paragraph style] for paragraphs, CharacterStyle/$ID/[No character style] for characters. A style with no BasedOn is its own root.

The example below has a three-link chain. The root carries the base font, size, and fill; "Body" adds a larger size and some space after; "Body Indented" adds only the indent. The story applies "Body Indented" — and gets all three styles' contributions.

A BasedOn chain: "Body Indented" → "Body" → the [No paragraph style] root.

Resources/Styles.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<idPkg:Styles xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="20.0">
  <RootCharacterStyleGroup>
    <CharacterStyle Self="CharacterStyle/$ID/[No character style]" Name="$ID/[No character style]"/>
  </RootCharacterStyleGroup>
  <RootParagraphStyleGroup>
    <ParagraphStyle Self="ParagraphStyle/$ID/[No paragraph style]" Name="$ID/[No paragraph style]" AppliedFont="Open Sans" PointSize="12" FillColor="Color/Black"/>
    <ParagraphStyle Self="ParagraphStyle/Body" Name="Body" BasedOn="ParagraphStyle/$ID/[No paragraph style]" PointSize="14" SpaceAfter="6"/>
    <ParagraphStyle Self="ParagraphStyle/Body Indented" Name="Body Indented" BasedOn="ParagraphStyle/Body" FirstLineIndent="18"/>
  </RootParagraphStyleGroup>
  <RootObjectStyleGroup>
    <ObjectStyle Self="ObjectStyle/$ID/[None]" Name="$ID/[None]" FillColor="Swatch/None" StrokeColor="Swatch/None" StrokeWeight="0" AppliedParagraphStyle="ParagraphStyle/$ID/[No paragraph style]" CornerOption="None" CornerRadius="0" EndCap="ButtEndCap" EndJoin="MiterEndJoin" MiterLimit="4" StrokeAlignment="CenterAlignment" StrokeType="StrokeStyle/$ID/Solid" Nonprinting="false"/>
  </RootObjectStyleGroup>
</idPkg:Styles>

How a chain resolves

When something applies a style, the parser does not read just that one <ParagraphStyle>. It walks the BasedOn chain from the applied style upward and folds the styles together into a single resolved set of values. The rule for each attribute is the same: the first style in the walk that sets it wins, and the walk continues upward only to fill attributes still unset.

Concretely, resolve_paragraph (and its siblings resolve_character, resolve_object, resolve_cell, resolve_table) starts an empty accumulator at the applied style and, for each style in the chain, calls merge_below: every field the accumulator has not yet filled is taken from the current style; fields already filled are left alone. The cursor then moves to that style's BasedOn parent and repeats. (crates/paged-parse/src/styles.rs, resolve_paragraph at the Resolved* resolvers; merge_below per resolved type.)

For the example above, resolving "Body Indented" produces:

AttributeResolved valueCame from
FirstLineIndent18Body Indented (the applied style)
PointSize14Body
SpaceAfter6Body
AppliedFontOpen Sansthe root
FillColorColor/Blackthe root

Because the walk fills "first setter wins", a child can shadow a parent: if "Body Indented" had its own PointSize, that value would have filled the slot before the walk ever reached "Body". This is inheritance with override built into a single pass — there is no separate "later rule overrides earlier rule" step the way CSS has. The order is the chain order.

The depth cap

IDML's schema does not forbid a BasedOn cycle — A based on B based on A — and nothing stops a hand-edited or corrupt file from containing one. A naive walk would loop forever. The resolver therefore bounds every walk at a fixed number of hops: MAX_BASED_ON_DEPTH = 16 (crates/paged-parse/src/styles.rs:51). After sixteen hops the walk simply stops, returning whatever it has accumulated so far.

SupportedCycle-safe by construction

Sixteen is comfortably above any real document — typical chains are one to three links — so the cap never truncates a legitimate hierarchy; it exists only so a pathological file degrades gracefully instead of hanging. A missing parent (a BasedOn pointing at an id that is not in the style sheet) ends the walk the same way: the lookup fails, the walk stops, and the attributes gathered so far stand.

Local overrides still win

The cascade resolves the applied style into a set of values. But the text that applies a style can also set an attribute directly on its range — PointSize="24" right on a <CharacterStyleRange>, say. That local value is not part of the style sheet and is not touched by BasedOn resolution at all. It sits above the whole cascade: when the effective formatting of a run is computed, the local value is seeded first and the resolved style only fills what the local value left blank.

In other words there are two layers stacked on top of the chain:

  1. the value set directly on the range (highest priority),
  2. the applied style, resolved through its BasedOn chain (this page),
  3. the root defaults at the bottom of that chain.

This page covered layer 2 — turning one applied style into resolved values. How layers 1 and 2 combine for a real run, attribute by attribute, is the subject of conflict resolution. The local-override example there shows a range that applies a character style and then overrides one of its attributes.

Frequently asked questions

What does BasedOn do in an IDML style? BasedOn names the parent a style inherits from — another style of the same kind, or a root sentinel like ParagraphStyle/$ID/[No paragraph style]. The parser accepts it either as an attribute or as a <BasedOn> child inside <Properties>, and a style with no BasedOn is its own root.

How does the parser resolve a chain of styles? It starts an empty accumulator at the applied style and walks the BasedOn chain upward, filling each attribute from the first style in the walk that sets it and leaving already-filled attributes alone. There is no separate "later rule overrides earlier rule" step the way CSS has — the chain order is the precedence order.

What happens if a BasedOn chain has a cycle or a missing parent? The walk is bounded at MAX_BASED_ON_DEPTH = 16 hops, so a cycle stops after sixteen hops and returns whatever was accumulated; a BasedOn pointing at an id not in the style sheet ends the walk the same way. Either case resolves gracefully instead of looping forever or erroring.

Does the cascade override a value set directly on a range? No. A value set directly on a <ParagraphStyleRange> or <CharacterStyleRange> is not part of the style sheet and is not touched by BasedOn resolution; it sits above the whole cascade and the resolved style only fills what the local value left blank.

On this page