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.
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.
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:
| Attribute | Resolved value | Came from |
|---|---|---|
FirstLineIndent | 18 | Body Indented (the applied style) |
PointSize | 14 | Body |
SpaceAfter | 6 | Body |
AppliedFont | Open Sans | the root |
FillColor | Color/Black | the 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.
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:
- the value set directly on the range (highest priority),
- the applied style, resolved through its
BasedOnchain (this page), - 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.
Styles
IDML keeps formatting in named, reusable style sheets, and the parser resolves a style reference plus its chain of parents into the values a run actually renders with.
Paragraph styles
The ParagraphStyle element and its key attributes — identity, the BasedOn parent, font and size defaults, alignment, indents, and spacing — plus the RootParagraphStyleGroup that holds them.