<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:ml="http://nwalsh.com/ns/ml-macro#"
                xmlns:xdt="http://www.w3.org/2005/xpath-datatypes"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:ef="http://exslt.org/functions"
                xmlns:ec="http://exslt.org/common"
                xmlns:re="http://exslt.org/regular-expressions"
                extension-element-prefixes="ef ec re"
                exclude-result-prefixes="ml xdt xs ef ec re"
                version="1.0">

<rdf:Description rdf:about=''
                 xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
                 xmlns:cc="http://web.resource.org/cc/"
                 xmlns:dc='http://purl.org/dc/elements/1.1/'
                 xmlns:dcterms="http://purl.org/dc/terms/">
  <rdf:type rdf:resource="http://web.resource.org/cc/Work"/>
  <rdf:type rdf:resource="http://norman.walsh.name/knows/taxonomy#XSL"/>
  <dc:type rdf:resource='http://purl.org/dc/dcmitype/Text'/>
  <dc:format>application/xsl+xml</dc:format>
  <dc:title>Macro expansion stylesheet</dc:title>
  <dc:date>2006-02-04</dc:date>
  <dc:creator rdf:resource='http://norman.walsh.name/knows/who#norman-walsh'/>
  <dc:contributor rdf:resource="tag:jlc6@po.cwru.edu,2005:self"/>
  <dc:isVersionOf rdf:resource="http://norman.walsh.name/2006/01/06/examples/ml-macro.xsl"/>
  <dc:rights>Copyright &#169; 2006 Norman Walsh. This work is licensed under the Creative Commons Attribution-NonCommercial License.</dc:rights>
  <cc:license rdf:resource="http://creativecommons.org/licenses/by-nc/2.0/"/>
  <dc:description>Expands macros in text content and attribute values. See http://norman.walsh.name/2006/01/06/doctype or http://infinitesque.net/articles/2006/doctype-xslt1/.</dc:description>
</rdf:Description>

<xsl:output method="xml" encoding="utf-8" indent="no"/>
<xsl:preserve-space elements="*"/>

<!-- URI of a resource that contains default macros -->
<xsl:param name="defaultMacros" select="'#macros'"/>

<!-- Load the default macros -->
<xsl:variable name="ml:defaultMacros" select="document($defaultMacros)"/>

<!-- Default macros -->
<ml:collection xml:id="macros">
  <!-- Defaults here, or override $defaultMacros to point elsewhere -->
  <!--
  <ml:macro name="ndw">Norman Walsh</ml:macro>
  -->
</ml:collection>

<!-- Dynamic macros -->
<xsl:template name="ml:dynamicMacros">
  <!-- Define any dynamic macros here. -->
  <!--
  <ml:macro name="time">
    <xsl:value-of select="format-time(current-time(), '[h01]:[m01][Pn,*-1]')"/>
  </ml:macro>
  <ml:macro name="timeZ">
    <xsl:value-of 
        select="format-dateTime(adjust-dateTime-to-timezone(current-dateTime(),
                                     xdt:dayTimeDuration('PT0H')),
                                '[h01]:[m01][Pn,*-1]')"/>
  </ml:macro>
  <ml:macro name="date">
    <xsl:value-of select="format-date(current-date(), '[D] [MNn,*-3] [Y]')"/>
  </ml:macro>
  <ml:macro name="dateZ">
    <xsl:value-of 
        select="format-dateTime(adjust-dateTime-to-timezone(current-dateTime(),
                                     xdt:dayTimeDuration('PT0H')),
                                '[D] [MNn,*-3] [Y]')"/>
  </ml:macro>
  -->
</xsl:template>

<!-- Select the relevant processing instructions: ones before the
     document element or before the first child element inside the
     document element. -->
<xsl:variable
  name="ml:pis" 
  select="/*/preceding-sibling::processing-instruction('ml-macro')
          |/*/processing-instruction('ml-macro')
            [not(preceding-sibling::*)]"/>

<!-- Select the open delimiter -->
<xsl:variable name="ml:odre">
  <xsl:choose>
    <xsl:when test="/*/preceding-sibling::processing-instruction('ml-macro-odre')">
      <xsl:value-of select="/*/preceding-sibling::processing-instruction('ml-macro-odre')[1]"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:text>\[\[</xsl:text>
    </xsl:otherwise>
  </xsl:choose>
</xsl:variable>

<!-- Select the close delimiter -->
<xsl:variable name="ml:cdre">
  <xsl:choose>
    <xsl:when test="/*/preceding-sibling::processing-instruction('ml-macro-cdre')">
      <xsl:value-of select="/*/preceding-sibling::processing-instruction('ml-macro-cdre')[1]"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:text>\]\]</xsl:text>
    </xsl:otherwise>
  </xsl:choose>
</xsl:variable>

<!-- Parse all the relevant PIs once and build an element list that will
     be easier to search. -->
<xsl:variable name="macros">
  <!-- All ml-macro PIs before or within the root element -->
  <xsl:for-each select="$ml:pis">
    <xsl:variable name="data" select="concat(' ',normalize-space(.))"/>
    <xsl:variable name="macro" select="ml:find-pseudo($data, 'name')"/>
    <xsl:variable name="text" select="ml:find-pseudo($data, 'text')"/>
    <xsl:if test="$macro != ''">
      <ml:macro name="{$macro}">
        <xsl:value-of select="$text"/>
      </ml:macro>
    </xsl:if>
  </xsl:for-each>

  <!-- Then all the ones in externally referenced files -->
  <xsl:for-each select="$ml:pis">
    <xsl:variable name="data" select="concat(' ',normalize-space(.))"/>
    <xsl:variable name="href" select="ml:find-pseudo($data, 'href')"/>
    <xsl:if test="$href != ''">
      <xsl:variable name="macs" select="document($href,/)"/>
      <xsl:copy-of select="$macs/ml:collection/ml:macro"/>
    </xsl:if>
  </xsl:for-each>

  <!-- Then all the default ones -->
  <xsl:choose>
    <xsl:when test="$ml:defaultMacros/ml:macro">
      <xsl:copy-of select="$ml:defaultMacros/ml:macro"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:copy-of select="$ml:defaultMacros/ml:collection/ml:macro"/>
    </xsl:otherwise>
  </xsl:choose>

  <!-- Then the dynamic ones -->
  <xsl:call-template name="ml:dynamicMacros"/>
</xsl:variable>

<xsl:variable name="ml:macros" select="ec:node-set($macros)"/>

<!-- Do the whole tree -->
<xsl:template match="/">
  <xsl:apply-templates mode="ml:expand-macros"/>
</xsl:template>

<!-- Copy elements; expanding macros as we go -->
<xsl:template match="*" mode="ml:expand-macros">
  <xsl:param name="seen" select="/.."/>

  <xsl:copy>
    <xsl:apply-templates select="@*|node()" mode="ml:expand-macros">
      <xsl:with-param name="seen" select="$seen"/>
    </xsl:apply-templates>
  </xsl:copy>
</xsl:template>

<!-- Expand macros in attribute values -->
<xsl:template match="@*" mode="ml:expand-macros">
  <xsl:param name="seen" select="/.."/>

  <xsl:choose>
    <xsl:when test="count($ml:macros) &gt; 0">
      <xsl:attribute name="{name(.)}" namespace="{namespace-uri(.)}">
        <xsl:value-of select="ml:expand-macros(.,$seen)"/>
      </xsl:attribute>
    </xsl:when>
    <xsl:otherwise>
      <xsl:copy/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="@xml:*" mode="ml:expand-macros">
  <xsl:param name="seen" select="/.."/>

  <xsl:choose>
    <xsl:when test="count($ml:macros) &gt; 0">
      <xsl:attribute name="{name(.)}">
        <xsl:value-of select="ml:expand-macros(.,$seen)"/>
      </xsl:attribute>
    </xsl:when>
    <xsl:otherwise>
      <xsl:copy/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<!-- Expand macros in text nodes -->
<xsl:template match="text()" mode="ml:expand-macros">
  <xsl:param name="seen" select="/.."/>
  <xsl:choose>
    <xsl:when test="count($ml:macros) &gt; 0">
      <xsl:copy-of select="ml:expand-macros(.,$seen)"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:copy/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<!-- Drop ?ml-macro PIs; there's not much point in holding onto them now -->
<xsl:template match="processing-instruction('ml-macro')
                     |processing-instruction('ml-macro-odre')
                     |processing-instruction('ml-macro-cdre')"
              mode="ml:expand-macros">
</xsl:template>

<!-- Expand macros in comments -->
<xsl:template match="comment()" mode="ml:expand-macros">
  <xsl:param name="seen" select="/.."/>
  <xsl:comment>
    <xsl:value-of select="ml:expand-macros(.,$seen)"/>
  </xsl:comment>
</xsl:template>

<!-- Expand macros in PIs -->
<xsl:template match="processing-instruction()" mode="ml:expand-macros">
  <xsl:param name="seen" select="/.."/>
  <xsl:processing-instruction name="{name(.)}">
    <xsl:value-of select="ml:expand-macros(.,$seen)"/>
  </xsl:processing-instruction>
</xsl:template>

<xsl:template name="expand-part">
  <xsl:param name="match-parts"/>
  <xsl:param name="text"/>
  <xsl:param name="seen"/>

  <xsl:value-of select="substring-before($text, $match-parts[1])"/>

  <xsl:variable name="repl"
                select="re:match($match-parts[1],
                                 concat($ml:odre, '(.*?)', $ml:cdre))[2]"/>

  <xsl:variable name="seen-updated">
    <xsl:copy-of select="$seen"/>

    <item><xsl:value-of select="$repl"/></item>
  </xsl:variable>

  <xsl:choose>
    <xsl:when test="ec:node-set($seen)/item = $repl">
      <xsl:message>
        <xsl:text>Expansion of "</xsl:text>
        <xsl:value-of select="$repl"/>
        <xsl:text>" is recursive. Giving up.</xsl:text>
      </xsl:message>
      <xsl:value-of select="$match-parts[1]"/>
    </xsl:when>
    <xsl:when test="$ml:macros/ml:macro[@name = $repl]">
      <xsl:apply-templates select="$ml:macros/ml:macro[@name = $repl][1]/node()"
                           mode="ml:expand-macros">
        <xsl:with-param name="seen" select="$seen-updated"/>
      </xsl:apply-templates>
    </xsl:when>
    <xsl:otherwise>
      <xsl:message>
        <xsl:text>Undefined macro encountered: </xsl:text>
        <xsl:value-of select="$repl"/>
      </xsl:message>
      <xsl:value-of select="."/>
    </xsl:otherwise>
  </xsl:choose>

  <xsl:choose>
    <xsl:when test="count($match-parts) &gt; 1">
      <xsl:call-template name="expand-part">
        <xsl:with-param name="match-parts"
                        select="$match-parts[position() &gt; 1]"/>

        <xsl:with-param name="text"
                        select="substring-after($text, $match-parts[1])"/>

        <xsl:with-param name="seen" select="$seen-updated"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="substring-after($text, $match-parts[1])"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<!-- Function to expand [[macro-name]] macros -->
<ef:function name="ml:expand-macros">
  <xsl:param name="text"/>
  <xsl:param name="seen"/>

  <xsl:variable name="match-parts"
    select="re:match($text, concat('(', $ml:odre, '.*?', $ml:cdre, ')'),
                     'g')"/>

  <ef:result>
    <xsl:choose>
      <xsl:when test="$match-parts[1]">
        <xsl:call-template name="expand-part">
          <xsl:with-param name="match-parts" select="$match-parts"/>
          <xsl:with-param name="text" select="$text"/>
          <xsl:with-param name="seen" select="$seen"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$text"/>
      </xsl:otherwise>
    </xsl:choose>
  </ef:result>
</ef:function>

<!-- Attempt to parse pseudo-attribute values from PIs. This isn't likely
     to handle badly formed PIs very well. So don't do that. -->
<ef:function name="ml:find-pseudo">
  <xsl:param name="text"/>
  <xsl:param name="find"/>

  <xsl:variable name="re-s" select='"^\s*([\w.-:]+)=([&apos;"'/>
  <xsl:variable name="re-e" select="'&quot;]).*$'"/>
  <xsl:variable name="matches"
                select="re:match($text, concat($re-s, $re-e))"/>

  <xsl:if test="count($matches) = 3">
    <xsl:variable name="pseudo" select="string($matches[2])"/>
    <xsl:variable name="quote" select="string($matches[3])"/>
    <xsl:variable
        name="value"
        select="substring-before(substring-after($text, $quote),$quote)"/>
    <xsl:variable
        name="rest"
        select="substring-after(substring-after($text, $quote),$quote)"/>

    <!--
    <xsl:message>
      <xsl:value-of select="$pseudo"/>
      <xsl:text>, </xsl:text>
      <xsl:value-of select="$quote"/>
      <xsl:text>, </xsl:text>
      <xsl:value-of select="$value"/>
      <xsl:text>, </xsl:text>
      <xsl:value-of select="$rest"/>
    </xsl:message>
    -->

    <ef:result>
      <xsl:choose>
        <xsl:when test="$pseudo = $find">
          <xsl:value-of select="$value"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="ml:find-pseudo($rest, $find)"/>
        </xsl:otherwise>
      </xsl:choose>
    </ef:result>
  </xsl:if>
</ef:function>      

</xsl:stylesheet>
