XML Whitespace

TEI has robust features for specifying space, gaps, line breaks, and related aspects of the space between text. But TEI is an XML vocabulary, and XML itself, and programs that read and process XML files, have their own ways to deal with what they call whitespace, that is, space, tab, carriage return and linefeed characters. Sometimes the standards, constraints, and conventions imposed by XML cause problems for TEI encodings and for programs that process TEI files.

This article explains interactions between TEI and XML's treatment of whitespace and concludes with recommendations for both producers of TEI encodings and authors of programs that process TEI encodings.

Where XML Considers Whitespace to be Significant
In XML documents, some whitespace is significant, some is not. For example, inside the brackets that mark XML elements extra whitespace is not significant. For any program processing these as pieces of XML,

&lt;title type="main"&gt;

and

&lt;title        type =   "main"   &gt;

are the same. There is no significance to the extra space. By XML rules, no application that processes the data in this XML file (processing it as XML and not just as text) is allowed to treat these two representations differently. A person or computer editing this file is free to use either one, based merely on readability and aesthetics. The fact that there is whitespace between title and type is significant, but how much or of what kind (space characters, tabs, carriage returns, new lines) is not significant. The space between type and = is not significant.

Whitespace can be significant, however, in the content of an element. For example,

&lt;name&gt;JoAnn&lt;/name&gt;

and

&lt;name&gt;Jo Ann&lt;/name&gt;

are different because of that space between Jo</tt> and Ann</tt>, and any program reading this element in an XML file is obliged to maintain the distinction.

But things can get complicated. Consider this:

&lt;persName&gt; &lt;forename&gt;Jo&lt;/forename&gt; &lt;forename&gt;Ann&lt;/forename&gt; &lt;surname&gt;Henry&lt;/forename&gt; &lt;/persName&gt;

Should the carriage returns and new lines matter? Should it matter if that open area before &lt;surname&gt;</tt> is a tab or is instead four space characters? Should it matter that there is extra space after &lt;persName&gt;</tt> ?

Normalize = Collapse + Trim
Many applications, including web browsers and many programs that read XML files will, unless instructed otherwise, “collapse” XML whitespace, that is, they will replace any contiguous string of space characters (0x20), tabs (0x09), carriage returns (0x0D) and line feeds (0x0A) with just one space character. So

&lt;name&gt;Jo Ann&lt;/name&gt;

&lt;name&gt;Jo   Ann&lt;/name&gt;

&lt;name&gt;Jo Ann&lt;/name&gt;

would all be treated as if there were just one space character between Jo</tt> and Ann</tt>. Moreover many applications will remove, or &ldquo;trim&rdquo;, leading and trailing XML whitespace. So these, too,

&lt;name&gt; Jo Ann&lt;/name &gt;

&lt;name&gt; Jo Ann&lt;/name&gt;

&lt;name&gt; Jo        Ann &lt;/name&gt;

would be treated as if the XML had been simply &lt;name&gt;Jo Ann&lt;/name&gt;</tt>.

Sometimes, as in the XSLT function &ldquo;normalize-space&rdquo;, the term &ldquo;normalize&rdquo; refers to the combination of collapsing XML whitespace and then trimming. Other times, as in XML Schema, &ldquo;collapse&rdquo; is the name of the combined operation. This article uses the XSLT terminology: normalizing is collapsing plus trimming.

Normalizing XML whitespace is very common. It is so pervasive that it is easy to overlook that it is happening and even difficult to know which program processing an XML file is doing the normalizing—is it the XSLT processor, the XSL program, the web browser, the print routine, or some combination?

@xml:space
The XML specification defines an attribute, xml:space</tt>, that when set to preserve</tt> instructs applications to suspend default trimming, collapsing, and normalizing and instead keep all the spaces, tabs, carriage returns, and line feeds just as they are. If xml:space</tt> is set to default or is simply left off, no such request is made; the application is free to do whatever its developer thinks best.

The attribute xml:space</tt> is inherited by child elements. One could, for example, put xml:space="preserve"</tt> into a TEI &lt;text&gt;</tt> element but not in &lt;teiHeader&gt;</tt>, to indicate that the request applies to all of the text but to none of the header.

TEI allows xml:space</tt> to be used on any element. But since TEI has so much rich functionality for encoding spaces, gaps, line breaks, and so on, the xml:space</tt> attribute is rarely used. Whatever could be accomplished by setting its value to <tt>preserve</tt> would be better accomplished by using native TEI elements. So the value is normally left as <tt>default</tt> by simply not including the attribute. Downstream processors are then left free to treat XML whitespace however the application developers want.

Default Whitespace Processing
When <tt>xml:space</tt> is left as <tt>default</tt>, nothing in XML or TEI specifies how consumers of a TEI XML file should treat whitespace.

There are, however, unspecified conventions. TEI encodings generally assume that space will be normalized, that in this encoding

&lt;p&gt; We hold these truths to be self-evident, that all men are are created equal, that they are endowed by their creator with certain inalienable Rights, that among these are Life, Liberty and the pursuit of Happiness. &lt;/p&gt;

some downstream processor will collapse spaces, tabs, carriage returns, and line feeds and will trim the space just after the <tt>&lt;p&gt;</tt> and just before the <tt>&lt;/p&gt;</tt>, and that in this encoding, from the TEI 5 Guidelines,

<persName> Edward George Bulwer-Lytton, <roleName>Baron Lytton of      <placeName>Knebworth</placeName> </roleName> </persName>

the man's name is <tt>Edward George Bulwer-Lytton, Baron Lytton of Knebworth</tt>, and not <tt> Edward George Bulwer-Lytton, Baron Lytton of Knebworth </tt> with space on the outsides, or <tt>EdwardGeorgeBulwer-Lytton,BaronLyttonofKnebworth</tt> , or some name with carriage returns in it.

Collapsing
A TEI encoder should assume that any string of whitespace characters will be collapsed into one space character. In theory, this can be circumvented by setting <tt>xml:space='preserve'</tt>, but not all downstream processors honor such requests. Web browsers, for example, do not. It is safer to use TEI's <tt>&lt;space&gt;</tt> element.

Programmers of downstream applications should feel free to collapse whitespace but should also honor <tt>xml:space='preserve'</tt> unless they can be certain that doing so is unnecessary.

Trimming
Whether text in an element should or will be trimmed depends on whether it is the only text in the element or it has siblings that are themselves elements.

Text-Only Elements
Even when specifications may be unclear on the matter, XML culture, conventions, product features, programming habits, and general best practices are allied not only to collapse but to trim whitespace from elements that contain only text. Encoders and consumers of TEI data should accept this. Unless <tt>@xml:space</tt> has been set to <tt>'preserve'</tt>, consumers of TEI files should trim such space and encoders should assume such space will be trimmed.

When this is done, these encodings

&lt;country&gt;Australia&lt;/country&gt;

&lt;country&gt;  Australia   &lt;/country&gt;

&lt;country&gt; Australia &lt;country&gt;

will all produce the same result. If the processing software were extracting data for use in a database, the resulting field would be <tt>country: Australia    </tt> in all three cases. If an encoder wants leading and trailing space to be preserved, if, for example,

<tt>&lt;emph rend='underline'&gt; Yes! &lt;emph&gt;</tt>

is meant to underline the space before and after the word, then <tt>xml:space='preserve'</tt> must be included in the <tt>&lt;emph&gt;</tt> element and it must be ensured that downstream processors actually honor <tt>xml:space='preserve'</tt>. If the underlining is meant to extend for not one but several spaces, only heroic care by encoder and consumer will ensure that it does. Use of <tt>&lt;space rend='underline'&gt;</tt> will be more reliable.

With both collapsing and trimming&mdash;that is, with normalizing&mdash;all of the following encodings would yield the same result.

&lt;name&gt;Ralph Waldo Emerson&lt;/name&gt;

&lt;name&gt;  Ralph Waldo  Emerson   &lt;/name&gt;

&lt;name&gt; Ralph Waldo Emerson &lt;name&gt;

Mixed-Content Elements
If an element contains not just text, but other elements, where and when space should be trimmed is more complicated. Consider the following encoding.

&lt;p&gt; The &lt;emph&gt; cat &lt;/emph&gt; ate  the &lt;foreign&gt;grande croissant&lt;/foreign&gt;. I didn't!    &lt;/p&gt;

The <tt>&lt;p&gt;</tt> element contains five child nodes.

By convention, it is presumed that this encodes a passage that could have been equivalently encoded one of these ways:

&lt;p&gt;The &lt;emph&gt;cat&lt;/emph&gt; ate the &lt;foreign&gt;croissant&lt;/foreign&gt;. I didn't!&lt;/p&gt;

&lt;p&gt;The &lt;emph&gt;cat&lt;/emph&gt; ate the &lt;foreign&gt;grande croissant&lt;/foreign&gt;. I didn't!&lt;/p&gt;

The algorithm to normalize space in mixed content is:
 * Collapse all white space, then
 * trim:
 * trim leading space on the first text node in an element and
 * trim trailing space on the last text node in an element,
 * trim both if a text node is both first and last, i.e., is the only text node in the element.

Applying that algorithm to the above passage: The result is as if the encoding had been &lt;p&gt;The &lt;emph&gt;cat&lt;/emph&gt; ate the &lt;foreign&gt;grande croissant&lt;/foreign&gt;. I didn't!&lt;/p&gt;

Note: The normalization process would have corrupted the text had the encoder put spaces inside the &lt;emph&gt;, like this: &lt;p&gt;The&lt;emph&gt; cat &lt;/emph&gt;ate the &lt;foreign&gt;grande croissant&lt;/foreign&gt;. I didn't!&lt;/p&gt;

The resulting text would be:

The cat ate the grande croissant. I didn't!

An encoder should assume that an element that includes nothing but text will get trimmed.

Structured Elements and xsl:strip-space
As mentioned above, normalization of whitespace is very common. Programmers implement it without asking encoders. And encoders presume some downstream application will effect it. This complex encoding of a person's name, taken from the TEI 5 Guidelines and mentioned above,

<persName> Edward George Bulwer-Lytton, <roleName>Baron Lytton of      <placeName>Knebworth</placeName> </roleName> </persName>

presumes&mdash;though without saying so&mdash;that a downstream program will normalize space according to the algorithm above and produce the name <tt>Edward George Bulwer-Lytton, Baron Lytton of Knebworth</tt>.

Note here that the <tt> &lt;persName&gt; </tt> element contains both text and elements. Note the comma. And note that had the forenames been encoded without intervening whitespace, the result would have been <tt> EdwardGeorge </tt>.

A problem lurks. Part of defining an XML vocabulary such as TEI is specifying whether an element may contain text and elements or just elements. In TEI 5, <tt>&lt;address&gt;</tt>, for example, unlike <tt> &lt;persName&gt; </tt>, may only contain other elements. This

&lt;address&gt; &lt;street&gt;10 Downing Street&lt;/street&gt; &lt;settlement&gt;London&lt;/settlement&gt; &lt;postCode&gt;SW1A 2AA&lt;/postCode&gt; &lt;address&gt;

is valid TEI. But this

&lt;address&gt; &lt;street&gt;10 Downing Street&lt;/street&gt;, &lt;settlement&gt;London&lt;/settlement&gt; &lt;postCode&gt;SW1A 2AA&lt;/postCode&gt; &lt;address&gt;

is not, because of that comma after the <tt>&lt;street&gt;</tt> element. Free non-whitespace text is not allowed between the elements that comprise the <tt>&lt;address&gt;</tt> element. Though the term is sometimes used more loosely, <tt>&lt;address&gt;</tt> would commonly be called a "structured element."

Elements that do not allow free non-whitespace text&mdash;structured elements, strictly speaking&mdash;mimic database records. When XML is used to move data between databases, such elements are the norm; indeed many XSLT programmers have never worked on anything but structured data. In a TEI file, structured data is more common in the header than in the text. A program extracting metadata from a TEI file will often be looking for structured data in the header, so that it can populate database fields, maybe like this:

street:      10 Downing Street settlement:  London postCode:    SW1A 2AA

Defining an element as a structured element specifies that space between child elements can be completely ignored. Thus these two encodings

&lt;address&gt; &lt;settlement type="city"&gt;London&lt;/settlement&gt; &lt;postCode&gt;SW1A 2AA&lt;/postCode&gt; &lt;address&gt;

&lt;address&gt; &lt;settlement type="city"&gt;London&lt;/settlement&gt;&lt;postCode&gt;SW1A 2AA&lt;/postCode&gt; &lt;address&gt;

are equivalent. They encode:

city:     London postCode: SW1A 2AA

Nothing in the encoding indicates that there should be a space, a comma, a new-line, or anything else between "London" and "SW1A 2AA". What, if anything, will be there is left to the processing application. When rendering prose, the application might insert a comma; when printing a mailing label, it might insert a new-line. If might use different punctuation when mailing to different countries.

To correctly process structured elements, XSL programmers insert an instruction, <tt> &lt;xsl:strip-space&gt; </tt>, at the beginning of their programs, followed by a list of the names of the structured elements. Among other things, this ensures that all whitespace between the children of structured elements will be removed. It will be as if such whitespace was collapsed and trimmed and made to completely disappear.

This situation can produce a temptation best resisted. An encoder may want to request that space be inserted between the components of a structured element, that, for example,

&lt;address&gt; &lt;settlement type="city"&gt;London&lt;/settlement&gt; &lt;postCode&gt;SW1A 2AA&lt;/postCode&gt; &lt;address&gt;

should be taken to encode "London SW1A 2AA". To implement this, the downstream processor could simply treat <tt> &lt;address&gt; </tt> as if it were not a structured element and was a mixed-content element instead. The encoder could then leave whitespace between the child elements and the regular normalization algorithm described above would collapse it and leave one space character.

The temptation is all the more seductive because (1) XML verification will not signal an error, (2) demands on the programmers of downstream applications are reduced, and (3) it is easy to succumb unknowingly. In XSLT, the programmer intentionally or inadvertently leaves <tt> &lt;xsl:strip-space&gt; </tt> off, something the programmer is happy to do since gathering the list of structured elements was inconvenient anyway, and all seems to be well.

But the better practice is indeed to burden the application with properly formatting structured elements. This burden is part of what it means for an element to be structured. If the project team agrees that whitespace in structured elements will be significant, the schema should be customized to make these elements mixed-content elements instead of structured elements. This ensures that future users of the XML files will be able to understand the files' contents. It also signals that

<tt> &lt;settlement&gt;New&lt;/settlement&gt;&lt;settlement&gt;York&lt;/settlement&gt; </tt>

and

<tt> &lt;settlement&gt;New&lt;/settlement&gt; &lt;settlement&gt;York&lt;/settlement&gt; </tt>

are different, which they would not be if the element were a structured one.

Recommendations

 * Programmers should, unless instructed otherwise by <tt> @xml:space='preserve' </tt>, implement code that normalizes space.
 * Encoders should presume such normalization will be done but should include a note in <tt> &lt;encodingDesc&gt; </tt> announcing the presumption. If normalization is not desired as the default, this certainly should be announced in <tt> &lt;encodingDesc&gt; </tt>
 * Encoders should use <tt> xml:space='preserve' </tt> only with the utmost care. Whatever could be accomplished by using it is usually accomplished with less risk by using native TEI elements.
 * Project teams should not intentionally or inadvertently use structured elements as if they were mixed-content elements. If this must be done, the schema should be customized to record the change.

XSLT Normalization Code
To normalize mixed-content elements, XSLT's <tt> normalize-space </tt> function cannot simply be used on all text nodes. The XSLT stylesheet must consider where a text node is among its siblings.

The following XSLT 1.0 code implements the normalization algorithm described above. It works for both text-only and mixed-content elements. The code overrides the built-in template for the appropriate text nodes so may simply be added to XSLT stylesheets. <xsl:template match="text[not(ancestor::*[@xml:space][1]/@xml:space='preserve')]"> <xsl:if test="position!=1 and normalize-space(substring(., 1, 1)) =  and normalize-space!="> <xsl:text> </xsl:text> </xsl:if> <xsl:value-of select="normalize-space"/> <xsl:if test="position!=last and position!=1 and normalize-space(substring(., string-length)) =                   or position!=last and position =1 and normalize-space(substring(., string-length)) =  and normalize-space!=                   or last=1 and string-length!=0 and normalize-space= "> <xsl:text> </xsl:text> </xsl:if> </xsl:template> The <tt> normalize-space(substring(., string-length)) = '' </tt> is just a way to test for whitespace.

An alternative in XSLT 2.0 is the code below, which covers all uses of <tt>text</tt>. it is written in a slightly more verbose style than the XSLT 1.0 version.

<xsl:template match="text"> <xsl:choose> <xsl:when test="ancestor::*[@xml:space][1]/@xml:space='preserve'"> <xsl:value-of select="."/> </xsl:when> <xsl:otherwise> <xsl:if test="position!=1 and         matches(.,'^\s') and          normalize-space!=''"> <xsl:text> </xsl:text> </xsl:if> <xsl:value-of select="normalize-space(.)"/> <xsl:choose> <xsl:when test="last=1 and string-length!=0 and     normalize-space=''"> <xsl:text> </xsl:text> </xsl:when> <xsl:when test="position!=1 and position!=last and matches(.,'\s$')"> <xsl:text> </xsl:text> </xsl:when> <xsl:when test="position=1 and matches(.,'\s$') and normalize-space!=''"> <xsl:text> </xsl:text> </xsl:when> </xsl:choose> </xsl:otherwise> </xsl:choose> </xsl:template>

XQuery Normalization Code
The corresponding XQuery code would run like this:

(: The rules are:  #1 Retain one leading space if the node isn't first, has non-space content, and has leading space.  #2 Retain one trailing space if the node isn't last, isn't first, and has trailing space.   #3 Retain one trailing space if the node isn't last, is first, has trailing space, and has non-space content.  #4 Retain a single space if the node is an only child and only has space content.  :) declare function local:tei-normalize-space($input) {    element {node-name($input)} {$input/@*, for $child in $input/node return if ($child instance of element) then local:tei-normalize-space($child) else if ($child instance of text) then (:#1 Retain one leading space if node isn't first, has non-space content, and has leading space:) if ($child/position ne 1 and matches($child,'^\s') and normalize-space($child) ne '') then (' ', normalize-space($child)) else (:#4 retain one space, if the node is an only child, and has content but it's all space:) if ($child/last eq 1 and string-length($child) ne 0 and normalize-space($child) eq '') (:NB: this overrules standard normalization:) then ' ' else (:#2 if the node isn't last, isn't first, and has trailing space, retain trailing space and collapse and trim the rest:) if ($child/position ne 1 and $child/position ne last and matches($child,'\s$')) then (normalize-space($child), ' ') else (:#3 if the node isn't last, is first, has trailing space, and has non-space content, then keep trailing space:) if ($child/position eq 1 and matches($child,'\s$') and normalize-space($child) ne '') then (normalize-space($child), ' ') (:if the node is an only child, and has content which is not all space, then trim and collapse, that is, apply standard normalization:) else normalize-space($child) (:output comments and pi's:) else $child } };