0

The following script illustrates a silent failure in Chrome 42.0.2311.135 when attempting to apply a recursive template escapeStringForJson to a string longer a certain length (1874 chars for me). I filed this as a bug, but is there a way to rewrite the template so that it is not recursive?

    var transformXml = function (xml, xslt) {
      var xmlScrapeResult = (new DOMParser()).parseFromString(xml, 'text/xml');
      var transform = (new DOMParser()).parseFromString(xslt, 'text/xml');
      var xsltProcessor = new XSLTProcessor();
      xsltProcessor.importStylesheet(transform);

      var transformed = xsltProcessor.transformToFragment(xmlScrapeResult, document);

      if (transformed != null) {
        return transformed;
      }
      else {
        throw 'xsltProcessor.transformToFragment() unexpectedly returned null';
      }
    }

    var xslt = '<?xml version="1.0"?>\
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">\
    <xsl:preserve-space elements="*" />\
    <xsl:template match="/MyRoot">\
    {\n\
      "escapedDoubleQuotesAndNewlines": "<xsl:call-template name="escapeStringForJson"><xsl:with-param name="text" select="MyElement"/></xsl:call-template>"\n\
    }\
    </xsl:template>\
    <xsl:template name="escapeStringForJson">\
      <xsl:param name="text"/>\
      <xsl:if test="$text != \'\'">\
        <xsl:variable name="head" select="substring($text, 1, 1)"/>\
        <xsl:variable name="tail" select="substring($text, 2)"/>\
        <xsl:variable name="apos">\'</xsl:variable>\
        <xsl:variable name="quot">"</xsl:variable>\
        <xsl:variable name="nl"><xsl:text>&#10;</xsl:text></xsl:variable>\
        <xsl:variable name="cr"><xsl:text>&#13;</xsl:text></xsl:variable>\
        <xsl:variable name="sl">\</xsl:variable>\
        <xsl:choose>\
          <!--<xsl:when test="$head = $apos">\\\'</xsl:when>-->\
          <xsl:when test="$head = $quot">\\"</xsl:when>\
          <xsl:when test="$head = $nl">\\n</xsl:when>\
          <xsl:when test="$head = $cr">\\r</xsl:when>\
          <xsl:when test="$head = $sl">\\\\</xsl:when>\
          <xsl:otherwise><xsl:value-of select="$head"/></xsl:otherwise>\
        </xsl:choose>\
        <xsl:call-template name="escapeStringForJson">\
          <xsl:with-param name="text" select="$tail"/>\
        </xsl:call-template>\
      </xsl:if>\
    </xsl:template>\
    </xsl:stylesheet>';

    var works1 = '<?xml version="1.0"?>\
    <MyRoot>\
      <MyElement>This is "test 1" illustrating escapeStringForJson purpose.</MyElement>\
    </MyRoot>';

    var works2 = '<?xml version="1.0"?>\
    <MyRoot>\
      <MyElement>' +
    '0123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 100' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 200' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 300' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 400' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 500' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 600' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 700' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 800' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 900' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1000' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1100' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1200' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1300' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1400' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1500' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1600' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1700' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1800' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 12345678 1874' +
    '</MyElement>\
    </MyRoot>';

    var nowork = '<?xml version="1.0"?>\
    <MyRoot>\
      <MyElement>' +
    '0123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 100' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 200' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 300' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 400' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 500' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 600' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 700' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 800' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 900' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1000' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1100' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1200' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1300' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1400' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1500' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1600' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1700' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234 1800' +
    ' 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1875' +
    '</MyElement>\
    </MyRoot>';

    console.log(new XMLSerializer().serializeToString(transformXml(works1, xslt)));
    console.log(new XMLSerializer().serializeToString(transformXml(works2, xslt))); // these two length-related test-cases do not have any characters that need escaping
    console.log(new XMLSerializer().serializeToString(transformXml(nowork, xslt))); // these two length-related test-cases do not have any characters that need escaping
(See your developer console output)
Jason Kleban
  • 20,024
  • 18
  • 75
  • 125

2 Answers2

1

There is no way you can avoid a recursive template - but you can minimize the number of recursive calls significantly if, instead of checking each and every one character of the input text, you jump directly to the next objectionable character that needs to be replaced. See an example here: XSL: replace single and double quotes with &apos; and &quot;

You could also download a more elaborate template from here:
http://exslt.org/str/functions/replace/index.html

Community
  • 1
  • 1
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
0

Here's what I came up with thanks to @michael.hor257k.

This splits the string at the first " it finds (or other sought character, in order) into a before and after and recursively calls the template on the two halves. The before will not have a " because we already split at the first one we found in the calling step and so it can only match on a later character, in order. Therefore, the before branch is certain to makes progress as it exhausts the list of sought characters. The after branch could find another of the same character, but is certain to make progress along the length of the string, always advancing by at least one character.

<xsl:template name="escapeForJson">
    <xsl:param name="text"/>
    <xsl:variable name="quot">
        <xsl:text>"</xsl:text>
    </xsl:variable>
    <xsl:variable name="nl">
        <xsl:text>&#10;</xsl:text>
    </xsl:variable>
    <xsl:variable name="cr">
        <xsl:text>&#13;</xsl:text>
    </xsl:variable>
    <xsl:variable name="sl">
        <xsl:text>\</xsl:text>
    </xsl:variable>
    <xsl:choose>
        <xsl:when test="contains($text, $quot)">
            <xsl:call-template name="escapeForJson">
                <xsl:with-param name="text" select="substring-before($text,$quot)"/>
            </xsl:call-template>
            <xsl:text>\"</xsl:text>
            <xsl:call-template name="escapeForJson">
                <xsl:with-param name="text" select="substring-after($text,$quot)"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:when test="contains($text, $nl)">
            <xsl:call-template name="escapeForJson">
                <xsl:with-param name="text" select="substring-before($text,$nl)"/>
            </xsl:call-template>
            <xsl:text>\n</xsl:text>
            <xsl:call-template name="escapeForJson">
                <xsl:with-param name="text" select="substring-after($text,$nl)"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:when test="contains($text, $cr)">
            <xsl:call-template name="escapeForJson">
                <xsl:with-param name="text" select="substring-before($text,$cr)"/>
            </xsl:call-template>
            <xsl:text>\r</xsl:text>
            <xsl:call-template name="escapeForJson">
                <xsl:with-param name="text" select="substring-after($text,$cr)"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:when test="contains($text, $sl)">
            <xsl:call-template name="escapeForJson">
                <xsl:with-param name="text" select="substring-before($text,$sl)"/>
            </xsl:call-template>
            <xsl:text>\\</xsl:text>
            <xsl:call-template name="escapeForJson">
                <xsl:with-param name="text" select="substring-after($text,$sl)"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="$text" disable-output-escaping="yes"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>
Jason Kleban
  • 20,024
  • 18
  • 75
  • 125