#Number Formatting
XSLT provides three instructions for controlling how numbers and characters appear in the output: xsl:number for generating formatted counters and numbering schemes, xsl:decimal-format for customizing how format-number() renders decimal values, and xsl:character-map for post-serialization character substitutions.
#Contents
#xsl:number
xsl:number generates a formatted number, most commonly used for numbering items in the output — chapter numbers, list item numbers, footnote markers, and outline numbering.
C# parallel: There is no single C# equivalent. xsl:number combines counting logic (like LINQ's IndexOf or list position) with format strings (like ToString("D3") for zero-padding or custom Roman numeral formatters).
#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 simply use position(). It counts nodes in the source document based on the level, count, and from attributes. This distinction matters: position() reflects processing order (which can be affected by sorting), while xsl:number reflects document structure.
#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, producing composite numbers like 1.1, 1.2, 2.1. This is the key to outline numbering.
<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. Useful 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 that controls how the format-number() function renders numeric values. This 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 that is applied 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 would otherwise be escaped, or you need to replace characters with multi-character strings. Character maps let you do this without resorting to 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 because 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>