XML Whitespace

From TEIWiki

Jump to: navigation, search

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.

Contents

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,

<title type="main">

and

<title     type = "main" >

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,

<name>JoAnn</name>

and

<name>Jo Ann</name>

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

But things can get complicated. Consider this:

<persName>    
    <forename>Jo</forename>
    <forename>Ann</forename>
    <surname>Henry</forename>
</persName>

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

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

  <name>Jo Ann</name>
  <name>Jo    Ann</name>
  <name>Jo 
      Ann</name>

would all be treated as if there were just one space character between Jo and Ann. Moreover many applications will remove, or “trim”, leading and trailing XML whitespace. So these, too,

  <name> Jo Ann</name >
  <name>
      Jo Ann</name>
  <name>
      Jo   
      Ann
  </name>

would be treated as if the XML had been simply <name>Jo Ann</name>.

Sometimes, as in the XSLT function “normalize-space()”, the term “normalize” refers to the combination of collapsing XML whitespace and then trimming. Other times, as in XML Schema, “collapse” 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, that when set to preserve 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 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 is inherited by child elements. One could, for example, put xml:space="preserve" into a TEI <text> element but not in <teiHeader>, to indicate that the request applies to all of the text but to none of the header.

TEI allows xml:space 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 attribute is rarely used. Whatever could be accomplished by setting its value to preserve would be better accomplished by using native TEI elements. So the value is normally left as default 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 xml:space is left as default, 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

  <p>   
      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.
  </p>

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

  <persName>
      <forename>Edward</forename>
      <forename>George</forename>
      <surname type="linked">Bulwer-Lytton</surname>, <roleName>Baron Lytton of
      <placeName>Knebworth</placeName>
      </roleName>
  </persName>

the man's name is Edward George Bulwer-Lytton, Baron Lytton of Knebworth, and not  Edward George Bulwer-Lytton, Baron Lytton of Knebworth  with space on the outsides, or EdwardGeorgeBulwer-Lytton,BaronLyttonofKnebworth, 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 xml:space='preserve', but not all downstream processors honor such requests. Web browsers, for example, do not. It is safer to use TEI's <space> element.

Programmers of downstream applications should feel free to collapse whitespace but should also honor xml:space='preserve' 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 @xml:space has been set to 'preserve', consumers of TEI files should trim such space and encoders should assume such space will be trimmed.

When this is done, these encodings

   <country>Australia</country>
   <country>   Australia   </country>
   <country>
           Australia     
   <country>

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

   <emph rend='underline'> Yes! <emph> 

is meant to underline the space before and after the word, then xml:space='preserve' must be included in the <emph> element and it must be ensured that downstream processors actually honor xml:space='preserve'. 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 <space rend='underline'> will be more reliable.

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

   <name>Ralph Waldo Emerson</name>
   <name>   Ralph Waldo  Emerson   </name>
   <name>
           Ralph    
           Waldo    
          Emerson   
   <name>

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.

  <p>  The <emph> cat </emph> ate  the <foreign>grande croissant</foreign>. I didn't!
    </p>

The <p> element contains five child nodes.

        The  A text node
            <emph> cat </emph> An <emph> element that itself contains one text node
                               ate  the  A text node
                                        <foreign>grande croissant</foreign> A <foreign> element that itself contains one text node
                                                                           . I didn't!
     
A text node that includes a carriage return and then two spaces

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

  <p>The 
  <emph>cat</emph>
  ate the 
  <foreign>croissant</foreign>. 
  I didn't!</p>
  <p>The <emph>cat</emph> ate the <foreign>grande croissant</foreign>. I didn't!</p>

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 

        The 

Because this is the first node in the <p> element, leading space is trimmed and trailing space is collapsed but not trimmed.
            <emph> cat </emph>

                   cat      

Because the only thing inside the <emph> element is a text node, the text there gets collapsed and trimmed.
                                ate  the 

                                ate the 

Space is collapsed but not trimmed on either side.
                                        <foreign>grande croissant</foreign>

                                                 grande croissant

Space in this text-only node is collapsed and trimmed, but no change results.
                                                                         . I didn't!
     

                                                                         . I didn't!

Because this is the last node in the <p> element, trailing space is trimmed and leading space is collapsed but not trimmed.

The result is as if the encoding had been

  <p>The <emph>cat</emph> ate the <foreign>grande croissant</foreign>. I didn't!</p>

Note: The normalization process would have corrupted the text had the encoder put spaces inside the <emph>, like this:

  <p>The<emph> cat </emph>ate the <foreign>grande croissant</foreign>. I didn't!</p>

The resulting text would be:

  Thecatate 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>
      <forename>Edward</forename>
      <forename>George</forename>
      <surname type="linked">Bulwer-Lytton</surname>, <roleName>Baron Lytton of
      <placeName>Knebworth</placeName>
      </roleName>
  </persName>

presumes—though without saying so—that a downstream program will normalize space according to the algorithm above and produce the name Edward George Bulwer-Lytton, Baron Lytton of Knebworth.

Note here that the <persName> 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 EdwardGeorge.

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, <address>, for example, unlike <persName>, may only contain other elements. This

  <address>
     <street>10 Downing Street</street>
     <settlement>London</settlement>
     <postCode>SW1A 2AA</postCode>
  <address>

is valid TEI. But this

  <address>
     <street>10 Downing Street</street>,
     <settlement>London</settlement>
     <postCode>SW1A 2AA</postCode>
  <address>

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

Elements that do not allow free non-whitespace text—structured elements, strictly speaking—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

  <address>
     <settlement type="city">London</settlement>
     <postCode>SW1A 2AA</postCode>
  <address>
  <address>
     <settlement type="city">London</settlement><postCode>SW1A 2AA</postCode>
  <address>

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, <xsl:strip-space>, 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,

  <address>
     <settlement type="city">London</settlement>
     <postCode>SW1A 2AA</postCode>
  <address>

should be taken to encode "London SW1A 2AA". To implement this, the downstream processor could simply treat <address> 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 <xsl:strip-space> 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

<settlement>New</settlement><settlement>York</settlement> 

and

<settlement>New</settlement> <settlement>York</settlement> 

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

Recommendations

  • Programmers should, unless instructed otherwise by @xml:space='preserve', implement code that normalizes space.
  • Encoders should presume such normalization will be done but should include a note in <encodingDesc> announcing the presumption. If normalization is not desired as the default, this certainly should be announced in <encodingDesc>
  • Encoders should use xml:space='preserve' 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 normalize-space() 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.

    <!-- Normalize unpreserved white space. -->
    <xsl:template match="text()[not(ancestor::*[@xml:space][1]/@xml:space='preserve')]">
        <!-- Retain one leading space if node isn't first, has non-space content, and has leading space.-->
        <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()"/>
        <!-- Retain one trailing space if node isn't last, isn't first, and has trailing space 
                                       or node isn't last, is first, has trailing space, and has any non-space content  
                                       or node is an only child, and has content but it's all 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 normalize-space(substring(., string-length())) = '' is just a way to test for whitespace.

An alternative in XSLT 2.0 is the code below, which covers all uses of text(). 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>
        <!-- Retain one leading space if node isn't first, has
	     non-space content, and has leading space.-->
        <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>
          <!-- node is an only child, and has content but it's all space -->
          <xsl:when test="last()=1 and string-length()!=0 and      normalize-space()=''">
            <xsl:text> </xsl:text>
          </xsl:when>
          <!-- node isn't last, isn't first, and has trailing space -->
          <xsl:when test="position()!=1 and position()!=last() and matches(.,'\s$')">
            <xsl:text> </xsl:text>
          </xsl:when>
          <!-- node isn't last, is first, has trailing space, and has non-space content   -->
          <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>

External Links

Personal tools