Previous Up Next


p:string-replace

p:string-replace — Replaces matched nodes with the result of evaluating an XPath expression.

Synopsis

<p:declare-step type="p:string-replace">
     <p:input port="source"/>
     <p:output port="result"/>
     <p:option name="match" required="true"/>                      <!-- XSLTMatchPattern -->
     <p:option name="replace" required="true"/>                    <!-- XPathExpression -->
</p:declare-step>

Description

The p:string-replace step replaces matched nodes with the result of evaluating an XPath expression. In other words, it allows you to identify a node (or nodes) in the document, perform a computation on that node, and then update the document to contain the result of that computation instead of the original node.

More technically, the p:string-replace step replaces nodes in the document provided on the source port that match the pattern specified in the match option with the string result of evaluating the XPath expression specified in the replace option.

For each node in the document provided on the source port:

  • If the match pattern specified in the match option matches the node, then the XPath expression provided in the replace option is evaluated with the matched node as the context.

    If the node is an attribute node then the string value of the expression replaces the attribute value, if the node is any other kind of node, then the string value of the expression replaces the entire node (not just the content of the node).

  • If the match pattern does not match the node, then the string replace operation is performed on each of the node's attributes (if it is an element) and its children. The resulting, transformed node is copied to the output.

One of the least convenient parts of the p:string-replace step is the replace option. What you must always bear in mind is that the value of the replace option is treated as an XPath expression by the step.

Often, this is what you want. Unfortunately, in one of the most common cases, where you'd like to replace an attribute value with some computed value, the obvious approach does not do what you'd expect.

Consider this pipeline:

  1 <p:declare-step xmlns:p="http://www.w3.org/ns/xproc"
                    name="main" version="1.0">
      <p:output port="result"/>
    
  5   <p:option name="new-class" select="'new-value'"/>
    
      <p:string-replace match="p/@class">
        <p:input port="source">
          <p:inline><p class="old-value">Some text.</p></p:inline>
 10     </p:input>
    
        <p:with-option name="replace" select="$new-class"/>
      </p:string-replace>
    
 15 </p:declare-step>

On the surface, this pipeline appears to replace the value of the class attribute with the value passed to the pipeline in the new-class option. The string replace step is effectively the same as this one:

  1   <p:string-replace match="p/@class" replace="new-value">
        <p:input port="source">
          <p:inline><p class="old-value">Some text.</p></p:inline>
        </p:input>
  5   </p:string-replace>

Here we can see that the value of the replace option is a bare name. Recall that p:string-replace treats the value of replace as an XPath expression. A bare name in XPath selects child elements with that name. So, as written, what this string replace step does is replace the value of the class attribute with the string value of its new-value element children.

Of course, attributes don't have element children, so the result of evaluating that XPath expression is always the empty string and that's the value that will be used for the class attribute.

The step that we need to evaluate effectively is this one:

  1   <p:string-replace match="p/@class" replace="'new-value'">
        <p:input port="source">
          <p:inline><p class="old-value">Some text.</p></p:inline>
        </p:input>
  5   </p:string-replace>

That can be achieved in the following way:

  1 <p:declare-step xmlns:p="http://www.w3.org/ns/xproc"
                    name="main" version="1.0">
      <p:output port="result"/>
    
  5   <p:option name="new-class" select="'new-value'"/>
    
      <p:string-replace match="p/@class">
        <p:input port="source">
          <p:inline><p class="old-value">Some text.</p></p:inline>
 10     </p:input>
    
        <p:with-option name="replace"
                       select="concat(&quot;'&quot;,$new-class,&quot;'&quot;)"/>
      </p:string-replace>
 15 
    </p:declare-step>

Which is undeniably pretty ugly.

Examples

This example updates the class attributes in a document:

  1 <p:pipeline xmlns:p="http://www.w3.org/ns/xproc"
                version="1.0">
    
      <p:string-replace match="*[@class='oldclass'
  5                     replace="'newclass'"/>
    </p:pipeline>
Input Output
1 <div>
<p class="oldclass red">Red.</p>
<p class="oldclass">Old.</p>
<p class="otherclass oldclass">Something else.</p>
5 </div>
 
1 <div>
<p class="oldclass red">Red.</p>
<p class="newclass">Old.</p>
<p class="otherclass oldclass">Something else.</p>
5 </div>

That misses class attributes that contain more than one value. We can improve on that with a slightly more sophisticated match pattern.

  1 <p:pipeline xmlns:p="http://www.w3.org/ns/xproc"
                version="1.0">
    
      <p:string-replace match="*[contains(@class,'oldclass')]/@class"
  5                     replace="'newclass'"/>
    </p:pipeline>
Input Output
1 <div>
<p class="oldclass red">Red.</p>
<p class="oldclass">Old.</p>
<p class="otherclass oldclass">Something else.</p>
5 </div>
 
1 <div>
<p class="newclass">Red.</p>
<p class="newclass">Old.</p>
<p class="newclass">Something else.</p>
5 </div>

Better, but now we're clobbering the other values. We can fix that, too.

  1 <p:pipeline xmlns:p="http://www.w3.org/ns/xproc"
                version="1.0">
    
      <p:string-replace match="*[contains(@class,'oldclass')]/@class"
  5                     replace="concat(substring-before(.,'oldclass'),'newclass',substring-after(.,'oldclass'))"/>
    </p:pipeline>
Input Output
1 <div>
<p class="oldclass red">Red.</p>
<p class="oldclass">Old.</p>
<p class="otherclass oldclass">Something else.</p>
5 <p class="someoldclasstoo">Not really old.</p>
</div>
 
1 <div>
<p class="newclass red">Red.</p>
<p class="newclass">Old.</p>
<p class="otherclass newclass">Something else.</p>
5 <p class="somenewclasstoo">Not really old.</p>
</div>

This is still imperfect as it incorrectly replaces “someoldclasstoo” with “somenewclasstoo”. We could improve this further with even more complex match and replace options.

But instead, let's take advantage of the fact that this is a pipeline. We don't have to do everything all at once.

  1 <p:pipeline xmlns:p="http://www.w3.org/ns/xproc"
                version="1.0">
    
      <p:string-replace match="*[@class='oldclass'
  5                     replace="'newclass'"/>
    
      <p:string-replace match="*[starts-with(@class,'oldclass ')]/@class"
                        replace="concat('newclass ', substring-after(.,'oldclass '))"/>
    
 10   <p:string-replace match="*[contains(@class,' oldclass ')]/@class"
                        replace="concat(substring-before(.,' oldclass '),' newclass ',substring-after(.,' oldclass '))"/>
    
      <p:string-replace match="*[ends-with(@class,' oldclass')]/@class"
                        replace="concat(substring-before(.,' oldclass'), ' newclass')"/>
 15 </p:pipeline>
Input Output
1 <div>
<p class="oldclass red">Red.</p>
<p class="oldclass">Old.</p>
<p class="otherclass oldclass">Something else.</p>
5 <p class="someoldclasstoo">Not really old.</p>
</div>
 
1 <div>
<p class="newclass red">Red.</p>
<p class="newclass">Old.</p>
<p class="otherclass newclass">Something else.</p>
5 <p class="someoldclasstoo">Not really old.</p>
</div>

(This still doesn't handle the case where “oldclass” appears more than once in the attribute value, but I'm willing to live with that.)

It's worth observing that matching on attributes is a special case. Attribute matches replace the value. Any other kind of match replaces the entire node. Here we find elements that have the “oldclass” value and replace them with the new class.

  1 <p:pipeline xmlns:p="http://www.w3.org/ns/xproc"
                version="1.0">
    
      <p:string-replace match="*[@class='oldclass'
  5                     replace="'newclass'"/>
    </p:pipeline>
Input Output
1 <div>
<p class="oldclass red">Red.</p>
<p class="oldclass">Old.</p>
<p class="otherclass oldclass">Something else.</p>
5 </div>
 
<div>
1 <div><p class="oldclass red">Red.</p>
newclass
<p class="otherclass oldclass">Something else.</p>
</div>