How to Use unparsed-text-lines in XSLT 3.0?

The unparsed-text-lines() function in XSLT 3.0 provides a native, streamable mechanism to read non-XML text files, such as CSV or plain text, directly into a sequence of strings separated by line breaks. Introduced to simplify plain-text transformations, it eliminates the need to load entire files into a single string before splitting them. This guide explains how unparsed-text-lines() operates, how it compares to legacy approaches, and how to combine it with XPath tokenization functions to convert raw delimited data into structured XML documents.

Core Mechanics of unparsed-text-lines()

The unparsed-text-lines() function reads an external resource identified by a URI and returns the contents as a sequence of xs:string items, where each item represents an individual line.

Syntax and Signatures

fn:unparsed-text-lines($href as xs:string?) as xs:string*
fn:unparsed-text-lines($href as xs:string?, $encoding as xs:string) as xs:string*

Line Ending Handling

The function automatically normalizes line endings across different operating systems. It treats standard line-break characters—such as carriage return (\r), line feed (\n), and carriage-return-line-feed combinations (\r\n)—as delimiters. The returned strings do not contain the trailing newline characters.

Step-by-Step Implementation: Parsing a CSV File

Transforming a CSV file into structured XML using unparsed-text-lines() involves three primary phases: reading the lines, separating header definitions from data rows, and tokenizing individual fields.

1. The Input Data

Assume a plain-text file named users.csv located in the same directory as the stylesheet:

id,name,role,department
101,Alice Smith,Engineer,Platform
102,Bob Jones,Analyst,Operations
103,Carol White,Designer,Product

2. The XSLT 3.0 Stylesheet

The following stylesheet reads the CSV file, treats the first record as field names, and creates an element for each subsequent record.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs">

    <xsl:output method="xml" indent="yes"/>

    <xsl:param name="csv-uri" as="xs:string" select="'users.csv'"/>

    <xsl:template name="xsl:initial-template">
        <xsl:variable name="lines" as="xs:string*" select="unparsed-text-lines($csv-uri, 'utf-8')"/>
        
        <xsl:if test="exists($lines)">
            <xsl:variable name="headers" as="xs:string*" select="tokenize($lines[1], ',')"/>
            
            <users>
                <xsl:for-each select="tail($lines)[normalize-space()]">
                    <xsl:variable name="fields" as="xs:string*" select="tokenize(., ',')"/>
                    <user>
                        <xsl:for-each select="$headers">
                            <xsl:variable name="pos" as="xs:integer" select="position()"/>
                            <xsl:element name="{normalize-space(.)}">
                                <xsl:value-of select="normalize-space($fields[$pos])"/>
                            </xsl:element>
                        </xsl:for-each>
                    </user>
                </xsl:for-each>
            </users>
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

3. Resulting XML Output

<?xml version="1.0" encoding="UTF-8"?>
<users>
   <user>
      <id>101</id>
      <name>Alice Smith</name>
      <role>Engineer</role>
      <department>Platform</department>
   </user>
   <user>
      <id>102</id>
      <name>Bob Jones</name>
      <role>Analyst</role>
      <department>Operations</department>
   </user>
   <user>
      <id>103</id>
      <name>Carol White</name>
      <role>Designer</role>
      <department>Product</department>
   </user>
</users>

Parsing Advanced Delimited Formats

While simple comma-separated files can be broken apart with tokenize(., ','), real-world CSV files often contain edge cases like quoted fields containing commas or escaped characters.

Handling Quoted Fields with xsl:analyze-string

For CSV records containing embedded commas inside quotes (e.g., 104,"Davis, Miller",Manager,Sales), standard tokenization fails. In XSLT 3.0, xsl:analyze-string or regular-expression tokenization provides robust field extraction:

<xsl:variable name="regex" select="'&quot;([^&quot;]*)&quot;|([^,]+)|,'"/>

Alternatively, csv-to-xml extensions or custom XPath 3.1 recursive functions can parse quoted strings without external dependencies.

Key Advantages Over XSLT 2.0 Approaches

Feature unparsed-text() (XSLT 2.0) unparsed-text-lines() (XSLT 3.0)
Return Type Single xs:string containing whole file Sequence of xs:string* items
Memory Footprint Higher; entire file buffered in memory Lower; sequence can be processed lazily
Line Splitting Requires manual tokenize(., '\r?\n') Built-in automatic normalization
Streaming Support Not streamable Streamable in compliant XSLT 3.0 processors

Error Handling and Best Practices

  1. Verify Resource Existence: Use the companion function unparsed-text-available($href) prior to reading files if the URI might be missing or invalid. Calling unparsed-text-lines() on a non-existent file produces dynamic error FOUT1170.
  2. Filter Empty Rows: Trailing newlines can produce empty string items at the end of the sequence. Filter items using predicates like [normalize-space()] or [string-length() gt 0].
  3. Explicit Encodings: Always specify the second parameter (such as 'utf-8' or 'windows-1252') when source data contains extended characters, preventing platform-specific decoding errors.