Instructions Reference
Number Formatting
xsl:number, xsl:decimal-format, xsl:character-map — numbering and format customization
#Number Formatting
XSLT provides three instructions for numbers and characters in the output. xsl:number generates formatted counters and numbering schemes. xsl:decimal-format customizes how format-number() renders decimal values. xsl:character-map performs character substitutions after serialization.
For C# developers: none of these instructions has a single C# equivalent.
xsl:numbercombines counting logic, similar to LINQ'sIndexOfor a list position, with a format string likeToString("D3")for zero-padding or a custom Roman-numeral formatter.xsl:decimal-formatcorresponds toCultureInfo.NumberFormator a customNumberFormatInfo.xsl:character-mapcorresponds tostring.Replace()applied to the final output, or a customTextWriterthat substitutes characters.
#Contents
#xsl:number
xsl:number generates a formatted number. Common uses include chapter numbers, list item numbers, footnote markers, and outline numbering.
#Basic Usage
<!-- Number each product in document order -->
<xsl:template match="product">
<div>
<xsl:number/>. <xsl:value-of select="name"/>
</div>
</xsl:template>
Output:
1. Laptop
2. Phone
3. Tablet
#How xsl:number Counts
xsl:number does not use position(). It counts nodes in the source document, based on the level, count, and from attributes. position() reflects processing order, and sorting can change that order. xsl:number reflects document structure instead.
#The level Attribute
The level attribute controls the counting scope:
#level="single" (Default)
Counts siblings of the same type. This is the most common mode.
<!-- Number each item within its parent -->
<xsl:template match="item">
<xsl:number level="single" count="item"/>
<xsl:text>. </xsl:text>
<xsl:value-of select="."/>
</xsl:template>
Given:
<list>
<item>First</item>
<item>Second</item>
<item>Third</item>
</list>
Output: 1. First, 2. Second, 3. Third
#level="multiple"
Counts at multiple levels of the hierarchy. The result is a composite number, such as 1.1, 1.2, or 2.1. Outline numbering relies on this level value.
<xsl:template match="section/title">
<xsl:number level="multiple" count="section" format="1.1"/>
<xsl:text> </xsl:text>
<xsl:value-of select="."/>
</xsl:template>
Given:
<document>
<section>
<title>Introduction</title>
<section>
<title>Background</title>
</section>
<section>
<title>Scope</title>
</section>
</section>
<section>
<title>Methods</title>
<section>
<title>Data Collection</title>
</section>
</section>
</document>
Output:
1 Introduction
1.1 Background
1.2 Scope
2 Methods
2.1 Data Collection
#level="any"
Counts all matching nodes in the document, regardless of hierarchy. Use this level for footnotes or sequential numbering across sections.
<xsl:template match="footnote">
<sup>
<xsl:number level="any" count="footnote" format="1"/>
</sup>
</xsl:template>
No matter how deeply nested footnotes are in sections, they get sequential numbers: 1, 2, 3, 4, ...
#The count and from Attributes
|
Attribute |
Description |
Default |
|---|---|---|
|
|
Pattern matching nodes to count |
Current node's type |
|
|
Pattern marking where counting restarts |
Document root |
<!-- Count items, restarting at each section -->
<xsl:template match="item">
<xsl:number level="any" count="item" from="section"/>
<xsl:text>. </xsl:text>
<xsl:value-of select="."/>
</xsl:template>
In this example, each section resets the count. Items in the first section are numbered 1, 2, 3. Items in the second section start over at 1.
#The format Attribute
The format attribute controls the output style. The first character, or characters, of the format string determines the numbering system:
|
Format |
Output |
Description |
|---|---|---|
|
|
1, 2, 3, ... |
Arabic numerals (default) |
|
|
01, 02, ..., 10, 11, ... |
Zero-padded arabic |
|
|
001, 002, ..., 010, ... |
Three-digit zero-padded |
|
|
a, b, c, ..., z, aa, ab, ... |
Lowercase alphabetic |
|
|
A, B, C, ..., Z, AA, AB, ... |
Uppercase alphabetic |
|
|
i, ii, iii, iv, v, ... |
Lowercase Roman numerals |
|
|
I, II, III, IV, V, ... |
Uppercase Roman numerals |
|
|
one, two, three, ... |
Words (lowercase) |
|
|
ONE, TWO, THREE, ... |
Words (uppercase) |
|
|
One, Two, Three, ... |
Words (title case) |
#Multi-Level Format Strings
For level="multiple", the format string includes separators:
<!-- Chapter.Section numbering -->
<xsl:number level="multiple" count="chapter|section" format="1.1"/>
<!-- Output: 1.1, 1.2, 2.1, etc. -->
<!-- Chapter.Section.Subsection with different styles -->
<xsl:number level="multiple" count="chapter|section|subsection" format="I.A.1"/>
<!-- Output: I.A.1, I.A.2, I.B.1, II.A.1, etc. -->
<!-- Legal document style -->
<xsl:number level="multiple" count="part|section|clause" format="1.1.a"/>
<!-- Output: 1.1.a, 1.1.b, 1.2.a, 2.1.a, etc. -->
<!-- Outline style with parentheses -->
<xsl:number level="multiple" count="section" format="I.A.1.a)"/>
<!-- Output: I.A.1.a), etc. -->
#The ordinal Attribute
The ordinal attribute produces ordinal suffixes (1st, 2nd, 3rd). Support varies by processor and language:
<xsl:number value="position()" format="1" ordinal="yes"/>
<!-- Output: 1st, 2nd, 3rd, 4th, ... -->
#The grouping-separator and grouping-size Attributes
For large numbers, add thousands separators:
<xsl:number value="$total-count" grouping-separator="," grouping-size="3"/>
<!-- Output: 1,234,567 -->
#Using value Instead of Counting
Instead of counting nodes, you can format a specific number:
<!-- Format a calculated value -->
<xsl:number value="count(//error)" format="1"/>
<!-- Format a number as Roman numerals -->
<xsl:number value="$chapter-number" format="I"/>
<!-- Format as words -->
<xsl:number value="$position" format="Ww"/>
<!-- Output: "One", "Two", "Three", ... -->
#Complete Example: Document with Multi-Level Numbering
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:output method="html" indent="yes"/>
<xsl:template match="document">
<html>
<body>
<!-- Table of Contents -->
<nav>
<h2>Table of Contents</h2>
<xsl:apply-templates select=".//section" mode="toc"/>
</nav>
<!-- Content -->
<xsl:apply-templates/>
</body>
</html>
</xsl:template>
<!-- TOC entry with multi-level numbering -->
<xsl:template match="section" mode="toc">
<p style="margin-left: {count(ancestor::section) * 20}px">
<a href="#section-{generate-id()}">
<xsl:number level="multiple" count="section" format="1.1"/>
<xsl:text> </xsl:text>
<xsl:value-of select="title"/>
</a>
</p>
</xsl:template>
<!-- Section content with numbered heading -->
<xsl:template match="section">
<xsl:variable name="depth" select="count(ancestor::section) + 1"/>
<div class="section" id="section-{generate-id()}">
<xsl:element name="h{min(($depth + 1, 6))}">
<xsl:number level="multiple" count="section" format="1.1"/>
<xsl:text> </xsl:text>
<xsl:value-of select="title"/>
</xsl:element>
<xsl:apply-templates select="* except title"/>
</div>
</xsl:template>
<xsl:template match="para">
<p><xsl:apply-templates/></p>
</xsl:template>
<!-- Footnotes numbered across the entire document -->
<xsl:template match="footnote">
<xsl:variable name="num">
<xsl:number level="any" count="footnote"/>
</xsl:variable>
<sup><a href="#fn-{$num}">[<xsl:value-of select="$num"/>]</a></sup>
</xsl:template>
</xsl:stylesheet>
#Use Cases for xsl:number
|
Use Case |
Configuration |
|---|---|
|
Simple list numbering |
|
|
Chapter numbering |
|
|
Section outline (1.1, 1.2) |
|
|
Footnote numbering |
|
|
Appendix letters |
|
|
Roman numeral preface pages |
|
|
Zero-padded item codes |
|
|
Numbered within each chapter |
|
#xsl:decimal-format
xsl:decimal-format declares a named, or default, decimal format. This format controls how the format-number() function renders numeric values. It is a top-level declaration.
C# parallel: CultureInfo.NumberFormat or custom NumberFormatInfo:
var nfi = new NumberFormatInfo
{
NumberDecimalSeparator = ",",
NumberGroupSeparator = ".",
CurrencySymbol = "EUR"
};
var formatted = amount.ToString("#,##0.00", nfi);
#Default Format
Without any xsl:decimal-format declaration, format-number() uses US/English conventions:
<xsl:value-of select="format-number(1234567.89, '#,##0.00')"/>
<!-- Output: 1,234,567.89 -->
#Declaring a Named Format
<xsl:decimal-format name="european"
decimal-separator=","
grouping-separator="."/>
<xsl:decimal-format name="swiss"
decimal-separator="."
grouping-separator="'"/>
Using named formats:
<!-- US format (default) -->
<xsl:value-of select="format-number(1234567.89, '#,##0.00')"/>
<!-- Output: 1,234,567.89 -->
<!-- European format -->
<xsl:value-of select="format-number(1234567.89, '#.##0,00', 'european')"/>
<!-- Output: 1.234.567,89 -->
<!-- Swiss format -->
<xsl:value-of select="format-number(1234567.89, '#''##0.00', 'swiss')"/>
<!-- Output: 1'234'567.89 -->
#All Properties
|
Property |
Default |
Description |
|---|---|---|
|
|
|
Character separating integer from fractional part |
|
|
|
Character separating digit groups (thousands) |
|
|
|
String representation of positive infinity |
|
|
|
Character for negative numbers |
|
|
|
String representation of Not-a-Number |
|
|
|
Percent sign character |
|
|
|
Per-mille sign character |
|
|
|
Character representing a mandatory digit |
|
|
|
Character representing an optional digit |
|
|
|
Separates positive and negative sub-patterns |
|
|
|
Character separating mantissa from exponent |
#Format Pattern Syntax
The format-number() pattern uses these symbols:
|
Symbol |
Meaning |
Example |
|---|---|---|
|
|
Mandatory digit (shows zero if absent) |
|
|
|
Optional digit (suppressed if zero) |
|
|
|
Decimal point |
|
|
|
Grouping separator |
|
|
|
Multiply by 100 and show percent |
|
|
|
Scientific notation |
|
#Positive and Negative Patterns
Use the pattern separator (;) to define separate formats for positive and negative numbers:
<xsl:value-of select="format-number(-42, '#,##0;(#,##0)')"/>
<!-- Output: (42) -->
<xsl:value-of select="format-number(42, '#,##0;(#,##0)')"/>
<!-- Output: 42 -->
<xsl:value-of select="format-number(-1500, '+#,##0;-#,##0')"/>
<!-- Output: -1,500 -->
<xsl:value-of select="format-number(1500, '+#,##0;-#,##0')"/>
<!-- Output: +1,500 -->
#Real-World Example: Multi-Locale Financial Report
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:decimal-format name="us"
decimal-separator="."
grouping-separator=","/>
<xsl:decimal-format name="de"
decimal-separator=","
grouping-separator="."/>
<xsl:decimal-format name="fr"
decimal-separator=","
grouping-separator=" "/>
<xsl:param name="locale" select="'us'"/>
<xsl:template match="financial-report">
<table>
<tr>
<th>Item</th>
<th>Amount</th>
</tr>
<xsl:for-each select="line-item">
<tr>
<td><xsl:value-of select="@name"/></td>
<td>
<xsl:choose>
<xsl:when test="$locale = 'us'">
$<xsl:value-of select="format-number(@amount, '#,##0.00', 'us')"/>
</xsl:when>
<xsl:when test="$locale = 'de'">
<xsl:value-of select="format-number(@amount, '#.##0,00', 'de')"/> EUR
</xsl:when>
<xsl:when test="$locale = 'fr'">
<xsl:value-of select="format-number(@amount, '# ##0,00', 'fr')"/> EUR
</xsl:when>
</xsl:choose>
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Output for $locale = 'de' with amount 1234567.89:
1.234.567,89 EUR
#xsl:character-map and xsl:output-character
xsl:character-map defines a mapping from single characters to replacement strings. The serializer applies this mapping during serialization. Unlike most XSLT processing, which operates on the data model, character maps operate on the serialized output bytes.
C# parallel: string.Replace() applied to the final output, or custom TextWriter that substitutes characters:
// Character substitution during output
output = output
.Replace("\u00A0", " ")
.Replace("\u2018", "‘")
.Replace("\u2019", "’");
#Why Character Maps?
The XML serializer normally escapes certain characters:
-
<becomes< -
>becomes> -
&becomes&
Sometimes you need to output characters that the serializer would otherwise escape, or you need to replace characters with multi-character strings. Character maps let you do this without disable-output-escaping.
#Declaring a Character Map
<xsl:character-map name="html-entities">
<xsl:output-character character=" " string="&nbsp;"/>
<xsl:output-character character="©" string="&copy;"/>
<xsl:output-character character="®" string="&reg;"/>
<xsl:output-character character="™" string="&trade;"/>
</xsl:character-map>
#Activating a Character Map
Reference the character map from xsl:output:
<xsl:output method="html" use-character-maps="html-entities"/>
Now, whenever the serializer encounters the non-breaking space character (U+00A0) in the output, it writes the literal string instead.
#Use Case: Smart Quote Replacement
Replace Unicode smart quotes with HTML entities for compatibility:
<xsl:character-map name="smart-quotes">
<xsl:output-character character="‘" string="&lsquo;"/> <!-- left single -->
<xsl:output-character character="’" string="&rsquo;"/> <!-- right single -->
<xsl:output-character character="“" string="&ldquo;"/> <!-- left double -->
<xsl:output-character character="”" string="&rdquo;"/> <!-- right double -->
<xsl:output-character character="—" string="&mdash;"/> <!-- em dash -->
<xsl:output-character character="–" string="&ndash;"/> <!-- en dash -->
</xsl:character-map>
<xsl:output method="html" use-character-maps="smart-quotes"/>
#Use Case: Outputting Raw Markup Characters
Sometimes you need to output characters that the XML serializer would escape. Use a placeholder character mapped to the raw string:
<xsl:character-map name="raw-markup">
<!-- Map a private-use character to a literal < -->
<xsl:output-character character="" string="<"/>
<xsl:output-character character="" string=">"/>
</xsl:character-map>
<xsl:output method="xml" use-character-maps="raw-markup"/>
Then in your templates, use the placeholder characters:
<xsl:template match="code-sample">
<!-- These private-use characters will become < and > in the output -->
<xsl:value-of select="translate(., '<>', '')"/>
</xsl:template>
This approach is cleaner than disable-output-escaping. It works with any output method and does not break the XSLT data model.
#Composing Character Maps
A character map can reference other character maps with use-character-maps:
<xsl:character-map name="html-entities">
<xsl:output-character character=" " string="&nbsp;"/>
<xsl:output-character character="©" string="&copy;"/>
</xsl:character-map>
<xsl:character-map name="smart-quotes">
<xsl:output-character character="‘" string="&lsquo;"/>
<xsl:output-character character="’" string="&rsquo;"/>
</xsl:character-map>
<!-- Combined map includes both -->
<xsl:character-map name="all-substitutions"
use-character-maps="html-entities smart-quotes">
<!-- Additional mappings specific to this map -->
<xsl:output-character character="™" string="&trade;"/>
</xsl:character-map>
<xsl:output method="html" use-character-maps="all-substitutions"/>
#Complete Example: HTML Output with Entity Preservation
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:character-map name="html-compat">
<!-- Non-breaking space -->
<xsl:output-character character=" " string="&nbsp;"/>
<!-- Typography -->
<xsl:output-character character="‘" string="&lsquo;"/>
<xsl:output-character character="’" string="&rsquo;"/>
<xsl:output-character character="“" string="&ldquo;"/>
<xsl:output-character character="”" string="&rdquo;"/>
<xsl:output-character character="—" string="&mdash;"/>
<!-- Symbols -->
<xsl:output-character character="©" string="&copy;"/>
<xsl:output-character character="®" string="&reg;"/>
</xsl:character-map>
<xsl:output method="html" indent="yes" use-character-maps="html-compat"/>
<xsl:template match="article">
<html>
<head><title><xsl:value-of select="title"/></title></head>
<body>
<h1><xsl:value-of select="title"/></h1>
<!-- Content with smart quotes and special characters
will be serialized using HTML entities -->
<xsl:apply-templates select="body/*"/>
<footer>
<p>© 2025 — All rights reserved.</p>
<!-- Outputs: © 2025 — All rights reserved. -->
</footer>
</body>
</html>
</xsl:template>
<xsl:template match="p">
<p><xsl:apply-templates/></p>
</xsl:template>
</xsl:stylesheet>