Reusing stylesheets¶
A real transformation is rarely one file. As it grows, the same scaffolding —
formatting rules, shared helper functions, common parameters, element templates —
gets copied between projects. The fix is the same as in any other language:
factor the shared parts into a base stylesheet and let each project pull it
in. XSLT offers two top-level elements for this, xsl:include and xsl:import,
and the difference between them is entirely about precedence.
What they pull in is every top-level declaration of the other stylesheet —
not just xsl:template. Params, variables, functions, keys, output settings,
attribute-sets: all of it merges in, and precedence decides what happens when two
declarations collide. It is easy to read the examples below as being about
templates (templates have a little extra machinery, so they make the tidiest
demos), but the mechanism is general. The most common real use — including the
mapping pipelines this section builds toward — overrides
parameters and functions and never touches a match template at all.
xsl:include — textual merge, same precedence¶
xsl:include is the simple one. It takes the declarations from another
stylesheet and merges them in as if you had pasted them at the point of
inclusion. Everything ends up at the same import precedence.
- Pulls in every template from
cd-templates.xslat the same precedence as the rules written here. It is a top-level element — a direct child ofxsl:stylesheet. - The
cdtemplate that handles these can live incd-templates.xsl; the merge makes it available exactly as if it were defined locally.
Same precedence means conflicts are errors
Because an included stylesheet sits at the same precedence as the including
one, two templates matching the same nodes with the same priority is a genuine
conflict. The XSLT 1.0 spec lets a processor either signal an error or pick the
last one — it is processor-dependent, so never rely on it. Use include
only when the files are partitioned so no rule overlaps.
xsl:import — lower precedence, so you can override¶
xsl:import brings in another stylesheet too, but its declarations get
lower import precedence than the importing stylesheet. That single rule is
what makes overriding possible: when both define the same thing — a parameter of
the same name, a function of the same signature, a template for the same nodes —
the importing stylesheet wins, no conflict, no error. The plainest case is a
parameter:
xsl:import must come first
All xsl:import elements must appear before every other top-level element
in the stylesheet — before any template, variable, output declaration, even
before xsl:include. A processor will reject an import that follows other
top-level content.
| main.xsl | |
|---|---|
- Must be the first top-level element.
base.xslis now at lower precedence. base.xslalso declares a globalheadingparam; this one has higher import precedence, so it wins — no conflict, no error. Overriding is just "re-declare it higher up," and it works the same for a variable, a function, or a template.
include vs import at a glance¶
xsl:include |
xsl:import |
|
|---|---|---|
| Precedence of pulled-in declarations | same as host | lower than host |
| Can the host override them? | no — same precedence | yes — host wins cleanly |
| Conflicting declarations | error / processor-dependent | resolved by precedence |
| Position among top-level elements | anywhere | must be first |
| Reach the overridden rule (templates only) | n/a | xsl:apply-imports |
Precedence applies to every declaration kind¶
Import precedence is not a template feature. It is a property of every kind of top-level declaration, and it resolves a collision differently depending on the kind:
| Declaration | What import precedence does on a clash |
|---|---|
xsl:param / xsl:variable |
higher-precedence (importing) declaration wins; the imported one is ignored |
xsl:function |
a same-name/same-arity function at higher precedence overrides the imported one |
xsl:template (match) |
higher precedence wins — and the shadowed rule is still reachable via xsl:apply-imports / xsl:next-match |
xsl:key, xsl:attribute-set |
same-name declarations combine rather than override |
xsl:output, xsl:decimal-format |
merged property-by-property |
Each row is a different kind of thing a stylesheet can declare — and every one
of them is what import/include carries across. Here is one of each in a single
base module; the match template is just the last line, not the centre of gravity:
Import or include that, and all of it comes along — the output settings, the
param, the variable, the key, the attribute-set, the function, both templates.
A higher-precedence module redeclaring any of those names follows its row in the
table above: title and f:money are replaced, cell and cd-by-id combine,
the output settings merge.
Two declarations of the same name at the same import precedence are an error
(e.g. two global params with one name) — import exists precisely to put one of
them lower so the clash resolves cleanly instead. The only kind with extra
machinery is the template: apply-imports lets an override reach back to the
rule it shadowed (a whole family of uses builds on that).
There is no equivalent for "call the param I overrode" — for
params, variables, and functions, the higher-precedence declaration simply
replaces the lower one. That makes them the simplest things to override, which
is why the most common pattern uses exactly them.
Override parameters, not templates¶
This is the plainest use of xsl:import and the one that scales to real mapping
work. A base stylesheet owns all the output — the templates, the types, the
guards — and declares its inputs as bare parameters with no select. It is
hand-authored once and rarely changes:
- No
select— the base does not know where the value comes from. Each parameter is a named slot a source module fills. - The one and only output template lives here. Source modules add none.
A source module imports the scaffold and re-declares each parameter with a
select that reaches into one particular input shape. Because the importing
module has higher import precedence, its declaration wins; the bare one in the
base is shadowed and the select becomes the value:
- First, as always. Everything in
card-base.xslis now lower precedence. - Higher precedence than the base's
nameparam, so this one is used and itsselect(evaluated against the source document's root) supplies the value. No template here at all — the base'smatch="/"fires and reads these params.
Point Saxon at a <contact> document with contact-to-card.xsl and you get the
<card>. A second input vocabulary needs a second module that binds the same
three slots from a different shape — <xsl:param name="name" select="vcard/fn"/>
and so on — with the scaffold untouched.
This is the codegen seam
A binding module is nothing but a list of <xsl:param … select="…"/> lines —
a flat slot → XPath table. That is exactly the kind of file you can
generate from a mapping spreadsheet or a script, while the scaffold (the
output structure, the typed signatures, the exists() guards) stays
hand-authored. Generated and hand-written XSLT meet only at the parameter
names. The same trick works for xsl:function: put a default implementation
in the base and override it in a module that needs different behaviour.
A whole case study builds out this
generated-plus-hand-written split — and shows why the two binding files want
include, not import.
Templates add one more thing¶
Match templates are the one declaration kind with extra machinery: an override can
re-run the rule it replaced, against the current node, with xsl:apply-imports
(and its sibling xsl:next-match). That turns importing into a customization
mechanism — inherit a body of rendering rules, override and decorate a few — with
a family of real uses of its own: shared rendering bases, per-format layers, and
customizing a third-party stylesheet you can't edit. Those have their own page:
Case study: reusing match templates. Everything on this
page is the whole composition mechanism without it; params and functions, the
common seams, neither have that machinery nor need it.
Where href is resolved
For both include and import, href is resolved relative to the
stylesheet that contains it — not relative to the source document or the
working directory. If main.xsl and base.xsl sit side by side,
href="../base.xsl" is correct; a base in a subfolder would be
href="../common/base.xsl".
What goes in a module¶
Once a stylesheet is more than one file, a handful of module shapes recur. The
most common is a function or helper library — a file of xsl:functions (date
and money formatting, string helpers, code-list lookups) with no match
templates, included wherever they are needed. Close behind is a parameter
module, a file of global xsl:param defaults that downstream layers import
and override — the configuration surface of large stylesheets and
the scaffold half of the generated-plus-hand-written
pattern. Lookup-data modules hold xsl:variable maps or xsl:key
declarations over embedded code lists; output-settings modules centralise
xsl:output, xsl:strip-space, and xsl:decimal-format. At larger scale the
split turns structural rather than by-kind: a base layer others override,
per-output-format layers (HTML vs print vs EPUB), a module per element
family, and modes wiring them together — the layering of match templates
that reusing match templates covers, and that the
DocBook case study walks through in real code.
Two things this page does not cover. Pulling in runtime data — a second XML
file, JSON, plain text — is a different mechanism, not import/include; that is
external documents, below. And for a genuine library with
an enforced public surface rather than the textual merge import performs, XSLT
3.0's packages are the stronger tool.
Next¶
Pulling in other stylesheets has a sibling: pulling in other data.
External documents covers document(), which lets a
transformation read a second XML file at run time.