Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/recipe-books.git
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:r="http://www.whitemagicsoftware.com/software/recipe"
  xmlns:string="http://symphony-cms.com/functions"
  xmlns:str="http://exslt.org/strings"
  exclude-result-prefixes="r str string">

  <xsl:import href="string-utilities.xsl" />
  <xsl:import href="fractions.xsl" />

  <xsl:output method="text" encoding="utf-8" />

  <xsl:strip-space elements="*" />

  <!-- LaTeX premable inserted at the top of the document as text. -->
  <xsl:param name="preamble" select="''" />

  <!-- If true, generate the table of contents. -->
  <xsl:param name="recipe-toc" select="true()" />

  <!-- If true, generate the index of ingredients (and categories). -->
  <xsl:param name="ingredient-index" select="true()" />

  <!-- If true, generate the copyright page. -->
  <xsl:param name="book-copyright" select="true()" />

  <!-- If true, include a front cover image. -->
  <xsl:param name="book-front-cover" select="true()" />

  <!-- If false, the will use a print layout. -->
  <xsl:param name="ebook" select="true()" />

  <!-- If true, the page number appears on full-page photo pages. -->
  <xsl:param name="page-photo-footer" select="false()" />

<!--
 | Citation style format; valid values:
 |   endcitations (default)
 |   page-photo-footercitations
 |   inlinecitations
 +-->
 <xsl:param name="citation-style" select="'endcitations'" />

<!-- These will come from the XML...
<xsl:param name="book-back-cover" select="true()" />
-->

<!-- Keys -->
<xsl:key name="ingredient-id" match="/recipe-book/ingredients/ingredient" use="@id" />
<xsl:key name="recipe-by-category" match="recipe" use="tags/tag[1]" />

<!-- Punctuation that is not preceded with a space. -->
<xsl:variable name="punctuation">.,:;!?</xsl:variable>

<!--
 | A recipe book consists of book details, recipes, categories, and tags.
 +-->
 <xsl:template match="/recipe-book">
  <xsl:text>\documentclass</xsl:text>
  <xsl:text>[</xsl:text>
  <xsl:if test="not($ebook)">
    <xsl:text>print,</xsl:text>
  </xsl:if>
  <xsl:if test="$page-photo-footer">
    <xsl:text>fullpagepage-photo-footer,</xsl:text>
  </xsl:if>
  <xsl:value-of select="$citation-style"/>
  <xsl:text>]</xsl:text>
  <xsl:text>{recipe-book}&#xa;</xsl:text>
  <xsl:text>\usepackage{hyperref}&#xa;</xsl:text>
  <xsl:value-of select="$preamble" />

  <xsl:text>\usepackage{imakeidx}&#xa;</xsl:text>

  <!-- Add indexes to TOC. -->
  <xsl:text>\makeindex[intoc,name=flatingred]&#xa;</xsl:text>
  <xsl:text>\makeindex[intoc,name=catingred]&#xa;</xsl:text>
  <xsl:apply-templates select='book' />
  <xsl:text>\begin{document}&#xa;</xsl:text>

  <!-- Breaks without imakeidx
  <xsl:text>  \let\index\Index&#xa;</xsl:text>
  -->

  <xsl:text>\frontmatter&#xa;</xsl:text>
  <xsl:if test="$book-front-cover">
    <xsl:text>\makecover</xsl:text>
    <xsl:if test="$book-copyright">
      <xsl:text>[copyrightpage]</xsl:text>
    </xsl:if>
    <xsl:text>&#xa;</xsl:text>
  </xsl:if>
  <xsl:apply-templates select="book/overview" mode="book-overview" />
  <xsl:if test="$recipe-toc">
    <xsl:text>\cleartoverso\tableofcontents*&#xa;</xsl:text>
  </xsl:if>
  <xsl:text>\mainmatter&#xa;</xsl:text>

  <!-- 
   | "Muenchian Method" for sorting tagged recipes into chapters, described in
   | XSLT Cookbook, by Sal Mangano, O'Reilly, pg195.
   +-->
   <xsl:for-each select="recipe[count(. | key('recipe-by-category',tags/tag[1])[1]) = 1]">
    <xsl:variable name="current-grouping-key" select="tags/tag[1]"/>
    <xsl:if test="$current-grouping-key != ''">
      <xsl:text>&#xa;\recipecategory{</xsl:text>
      <xsl:apply-templates select="$current-grouping-key" mode="escape-capitalize"/>
      <xsl:text>}&#xa;&#xa;</xsl:text>
      <xsl:variable name="current-group" 
        select="key('recipe-by-category', $current-grouping-key)" />
        <xsl:for-each select="$current-group">
          <xsl:apply-templates select="." />
        </xsl:for-each>
      </xsl:if>
    </xsl:for-each>

  <!--
   | All untagged recipes to go into a "Recipes" chapter.
   +-->
   <xsl:if test="recipe[tags/tag = '' or not(tags/tag)]">
    <xsl:text>&#xa;\recipecategory{Recipes}&#xa;&#xa;</xsl:text>
    <xsl:apply-templates select="recipe[tags/tag = '' or not(tags/tag)]" />
  </xsl:if>

  <xsl:text>\backmatter&#xa;</xsl:text>
  <xsl:text>\renewcommand{\clearforchapter}{\newpage}&#xa;</xsl:text>
  <xsl:if test="$ingredient-index">
    <xsl:text>\renewcommand{\indexname}{Alphabetized Ingredients}&#xa;</xsl:text>
    <xsl:text>\printindex[flatingred]&#xa;</xsl:text>
    <xsl:text>\renewcommand{\indexname}{Categorized Ingredients}&#xa;</xsl:text>
    <xsl:text>\reduceindexindent&#xa;</xsl:text>
    <xsl:text>\printindex[catingred]&#xa;</xsl:text>
  </xsl:if>
  <xsl:text>\end{document}&#xa;</xsl:text>
</xsl:template>

<!--
 | Book meta information.
 +-->
<xsl:template match="book">
  <xsl:text>\title{</xsl:text>
  <xsl:apply-templates select="title" mode="escape-capitalize" />
  <xsl:text>}&#xa;</xsl:text>
  <xsl:text>\date{</xsl:text>
  <xsl:apply-templates select="created" mode="escape-capitalize" />
  <xsl:text>}&#xa;</xsl:text>
  <xsl:text>\author{</xsl:text>
  <xsl:apply-templates select="author" mode="escape-capitalize" />
  <xsl:text>}&#xa;</xsl:text>
  <xsl:apply-templates select="photo" mode="book" />
</xsl:template>

<xsl:template match="overview" mode="book-overview">
  <xsl:text>&#xa;\begin{overview}{</xsl:text>
  <xsl:apply-templates select="@label" mode="escape-capitalize" />
  <xsl:text>}&#xa;</xsl:text>
  <xsl:if test="../photo[@use='overview']">
    <xsl:apply-templates select="../photo[@use='overview']/uri" mode="book-overview"/>
  </xsl:if>
  <xsl:apply-templates select="p" />
  <xsl:text>\end{overview}&#xa;&#xa;</xsl:text>
</xsl:template>

<!--
 | Allows users to provide descriptive text for the book.
 +-->
 <xsl:template match="p">
  <xsl:apply-templates mode="escape" />
  <xsl:text>&#xa;&#xa;</xsl:text>
</xsl:template>

<!--
 | Transforms a recipe from XML into LaTeX.
 +-->
 <xsl:template match="recipe">
  <!-- 
   | If a facing full-page photo is used, check whether a filler photo
   | (e.g., advertsement) is required to make up the page number.
   +-->
   <xsl:if test="description/photo[@use='one-page' or @use='two-page']">
    <xsl:text>\checkfiller&#xa;</xsl:text>
  </xsl:if>
  <xsl:if test="description/photo[@use='two-page']">
    <xsl:text>\preparespread&#xa;</xsl:text>
  </xsl:if>
  <xsl:text>\begin{recipe}</xsl:text>
  <xsl:apply-templates select="description/citation[descendant::*]" />
  <xsl:text>{</xsl:text>
  <xsl:apply-templates select="description/title" />
  <xsl:text>}&#xa;</xsl:text>
  <xsl:apply-templates select="description/photo[@use='recipe-inset']" mode="recipe" />
  <xsl:apply-templates />
  <xsl:apply-templates select="description/photo[@use='one-page' or @use='two-page']" mode="recipe" />
  <xsl:text>\end{recipe}&#xa;&#xa;</xsl:text>
  <xsl:if test="description/photo[@use='two-page']">
    <xsl:text>\cancelspread&#xa;</xsl:text>
  </xsl:if>
</xsl:template>

<xsl:template match="photo[@use='front-cover']" mode="book">
  <xsl:apply-templates select="uri" mode="book" />
</xsl:template>

<xsl:template match="photo[@use='overview']" mode="book" />

<!--
<xsl:template match="photo[@use='recipe-inset']" mode="recipe">
  <xsl:apply-templates select="uri" mode="recipe-inset" />
</xsl:template>

<xsl:template match="photo[@use='one-page']" mode="recipe">
  <xsl:apply-templates select="uri" mode="one-page" />
</xsl:template>

<xsl:template match="photo[@use='two-page']" mode="recipe">
  <xsl:apply-templates select="uri" mode="two-page" />
</xsl:template>
-->
<xsl:template match="photo" mode="recipe">
  <xsl:text>\photo[embed=</xsl:text>
  <xsl:choose>
    <xsl:when test="@use='recipe-inset'">
      <xsl:text>inset</xsl:text>
    </xsl:when>
    <xsl:when test="@use='one-page'">
      <xsl:text>fullpage</xsl:text>
    </xsl:when>
    <xsl:when test="@use='two-page'">
      <xsl:text>twopagespread</xsl:text>
    </xsl:when>
  </xsl:choose>
  <xsl:text>,</xsl:text>
  <xsl:apply-templates match="citation[descendant::*]" />
  <xsl:text>]{</xsl:text>
  <xsl:apply-templates select="uri" mode="escape"/>
  <xsl:text>}&#xa;</xsl:text>
</xsl:template>

<xsl:template match="uri" mode="book-overview">
  <xsl:text>\begin{wrapfigure}{l}{.45\textwidth}&#xa;</xsl:text>
  <xsl:text>\insetphoto[</xsl:text>
  <xsl:apply-templates select="following-sibling::citation[descendant::*]" />
  <xsl:text>]{</xsl:text>
  <xsl:apply-templates />
  <xsl:text>}&#xa;</xsl:text>
  <xsl:text>\end{wrapfigure}&#xa;&#xa;</xsl:text>
</xsl:template>

<xsl:template match="uri" mode="book">
  <xsl:if test="$book-front-cover">
    <xsl:text>\frontcoverphoto[</xsl:text>
    <xsl:apply-templates select="following-sibling::citation[descendant::*]" />
    <xsl:text>]{</xsl:text>
    <xsl:apply-templates />
    <xsl:text>}&#xa;</xsl:text>
  </xsl:if>
</xsl:template>

<!--
<xsl:template match="uri" mode="recipe-inset">
  <xsl:text>\insetphoto</xsl:text>
    <xsl:apply-templates select="following-sibling::citation[descendant::*]" />
    <xsl:text>{</xsl:text>
    <xsl:apply-templates />
  <xsl:text>}&#xa;</xsl:text>
</xsl:template>

<xsl:template match="uri" mode="one-page">
  <xsl:text>\fullpagephoto</xsl:text>
    <xsl:apply-templates select="following-sibling::citation[descendant::*]" />
    <xsl:text>{</xsl:text>
    <xsl:apply-templates />
  <xsl:text>}&#xa;</xsl:text>
</xsl:template>

<xsl:template match="uri" mode="two-page">
  <xsl:text>\twopagespreadphoto</xsl:text>
    <xsl:apply-templates select="following-sibling::citation[descendant::*]" />
    <xsl:text>{</xsl:text>
    <xsl:apply-templates />
  <xsl:text>}&#xa;</xsl:text>
</xsl:template>
-->
<xsl:template match="citation[descendant::*]">
  <!--  <xsl:text>[</xsl:text>-->
  <xsl:if test="author">
    <xsl:text>author={</xsl:text>
    <xsl:apply-templates select="author" mode="escape" />
    <xsl:text>},</xsl:text>
  </xsl:if>
  <xsl:if test="author/@url">
    <xsl:text>authorurl={</xsl:text>
    <xsl:apply-templates select="author/@url" mode="escape" />
    <xsl:text>},</xsl:text>
  </xsl:if>
  <xsl:if test="license">
    <xsl:text>license={</xsl:text>
    <xsl:apply-templates select="license" mode="escape" />
    <xsl:text>},</xsl:text>
  </xsl:if>
  <xsl:if test="license/@url">
    <xsl:text>licenseurl={</xsl:text>
    <xsl:apply-templates select="license/@url" mode="escape" />
    <xsl:text>},</xsl:text>
  </xsl:if>
  <!--  <xsl:text>]</xsl:text> -->
</xsl:template>

<!--
<xsl:template match="description/citation[descendant::*]">
  <xsl:if test="author">
    <xsl:text>\recipecredit</xsl:text>
    <xsl:if test="author/@url">
      <xsl:text>[</xsl:text>
      <xsl:apply-templates select="author/@url" mode="escape" />
      <xsl:text>]</xsl:text>
    </xsl:if>
    <xsl:text>{</xsl:text>
    <xsl:apply-templates select="author" mode="escape" />
    <xsl:text>}&#xa;</xsl:text>
  </xsl:if>

  <xsl:if test="license">
    <xsl:text>\recipelicense</xsl:text>
    <xsl:if test="license/@url">
      <xsl:text>[</xsl:text>
      <xsl:apply-templates select="license/@url" mode="escape" />
      <xsl:text>]</xsl:text>
    </xsl:if>
    <xsl:text>{</xsl:text>
    <xsl:apply-templates select="license" mode="escape" />
    <xsl:text>}&#xa;</xsl:text>
  </xsl:if>
</xsl:template>
-->

<xsl:template match="description/title">
  <xsl:apply-templates mode="escape-capitalize" />
</xsl:template>

<!-- Pre-heated oven temperature, not to be confused with ingredient prep. -->
<xsl:template match="preparation">
  <xsl:apply-templates />
</xsl:template>

<xsl:template match="oven">
  <xsl:text>\oven{</xsl:text>
  <xsl:value-of select="@temperature" />
  <xsl:text>\,</xsl:text>
  <xsl:choose>
    <xsl:when test="@unit='C' or @unit='F'">
      <xsl:text>\textdegree </xsl:text>
    </xsl:when>
  </xsl:choose>
  <xsl:value-of select="@unit" />
  <xsl:text>}&#xa;&#xa;</xsl:text>
</xsl:template>

<xsl:template match="equipment[descendant::text()]">
  <xsl:text>\begin{equipment}&#xa;</xsl:text>
  <!-- Select non-empty child nodes. -->
  <xsl:for-each select="*[child::*]">
    <xsl:text>  \item[</xsl:text>
    <xsl:call-template name="escape-capitalize">
      <xsl:with-param name="ec" select="name()" />
    </xsl:call-template>
    <xsl:text>] </xsl:text>
    <!-- Oxford-comma the "object" children nodes -->
    <xsl:call-template name="escape-capitalize-oxfordcomma">
      <xsl:with-param name="itemlist" select="object" />
    </xsl:call-template>
    <xsl:text>&#xa;</xsl:text>
  </xsl:for-each>
  <xsl:text>\end{equipment}&#xa;&#xa;</xsl:text>
</xsl:template>

<xsl:template name="escape-capitalize-oxfordcomma">
  <xsl:param name="itemlist" />
  <xsl:variable name="itemcount" select="count($itemlist)" />
  <xsl:variable name="items">
    <xsl:for-each select="$itemlist">
      <xsl:choose>
        <xsl:when test="$itemcount > 2">
          <xsl:choose>
            <xsl:when test="position()=1">
              <xsl:value-of select="concat(' ', ., ',')"/>
            </xsl:when>
            <xsl:when test="position()=last()">
              <xsl:value-of select="concat(' and ', .)"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="concat(' ', .,',')"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:when test="$itemcount = 2">
          <xsl:choose>
            <xsl:when test="position()=1">
              <xsl:value-of select="concat(' ', .,' and')"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="concat(' ', .)"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="concat(' ', .)"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </xsl:variable>

  <xsl:call-template name="escape">
    <xsl:with-param
      name="e" select="string:capitalize(normalize-space($items))" />
    </xsl:call-template>
  </xsl:template>

<!--
 | Puts the value of all labels (ingredients and directions) in brackets.
 +-->
 <xsl:template match="@label">
  <xsl:text>[</xsl:text>
  <xsl:call-template name="escape-capitalize">
    <xsl:with-param name="ec" select="." />
  </xsl:call-template>
  <xsl:text>]</xsl:text>
  <xsl:text>&#xa;</xsl:text>
</xsl:template>

<xsl:template match="ingredients[parent::recipe]">
  <xsl:if test="count(ingredient[not(@substitute) or @substitute='']) &gt; 0">
    <xsl:text>\begin{ingredients}&#xa;</xsl:text>
    <xsl:apply-templates select="@label" />
    <xsl:apply-templates select="ingredient[not(@substitute) or @substitute='']" />
    <xsl:text>\end{ingredients}&#xa;&#xa;</xsl:text>
  </xsl:if>

  <!-- 
   | If non-empty conditions exist, insert a preparation section. 
   +-->
   <xsl:if test="ingredient[@condition != '']">
    <xsl:text>\begin{preparation}&#xa;</xsl:text>
    <xsl:apply-templates select="ingredient[@condition != '']" mode="prep" />
    <xsl:text>\end{preparation}&#xa;</xsl:text>
  </xsl:if>

  <!-- 
   | If non-empty substitutions, insert a substitution section. 
   +-->
   <xsl:if test="ingredient[@substitute != '']">
    <xsl:text>\begin{substitution}&#xa;</xsl:text>
    <xsl:apply-templates select="ingredient[@substitute != '']" mode="subst" />
    <xsl:text>\end{substitution}&#xa;</xsl:text>
  </xsl:if>
</xsl:template>

<xsl:template match="ingredient">
  <xsl:text>  \ingred{</xsl:text>

  <xsl:call-template name="utf-fraction">
    <xsl:with-param name="quantity" select="@min-quantity" />
  </xsl:call-template>

  <xsl:if test="@max-quantity">
    <xsl:text>--</xsl:text>

    <xsl:call-template name="utf-fraction">
      <xsl:with-param name="quantity" select="@max-quantity" />
    </xsl:call-template>
  </xsl:if>
  <xsl:text>}{</xsl:text>
  <xsl:value-of select="@unit" />
  <xsl:text>}{</xsl:text>
  <xsl:apply-templates select="key('ingredient-id', @id)/@name" />
  <xsl:text>}%&#xa;</xsl:text>

  <!-- Put the ingredients in a categorized list. -->
  <xsl:if test="key('ingredient-id',@id)/categories/category">
    <xsl:for-each select="key('ingredient-id',@id)/categories/category">
      <xsl:text>      \index[catingred]{</xsl:text>
      <xsl:call-template name="replace">
        <xsl:with-param name="pText" select="@name" />
        <xsl:with-param name="pToken" select="','" />
        <xsl:with-param name="pSubst" select="'!'" />
      </xsl:call-template>
      <xsl:text>!</xsl:text>
      <xsl:apply-templates select="../../@name" />
      <xsl:text>}%&#xa;</xsl:text>
    </xsl:for-each>
  </xsl:if>

  <!-- Index ingredients into a flat list. -->
  <xsl:text>      \index[flatingred]{</xsl:text> 
  <xsl:apply-templates select="key('ingredient-id', @id)/@name" />
  <xsl:text>!</xsl:text>
  <xsl:apply-templates select="ancestor::recipe/description/title" />
  <xsl:text>}%&#xa;</xsl:text>   
</xsl:template>

<!-- 
 | Extracts preparation instructions from ingredients.
 +-->
 <xsl:template match="ingredient[@condition != '']" mode="prep">
  <xsl:for-each select=".">
    <xsl:text>\item </xsl:text>
    <xsl:apply-templates select="@condition" mode="prep" />
    <xsl:text> the </xsl:text>
    <xsl:apply-templates select="key('ingredient-id', @id)/@name" />
    <xsl:text>.&#xa;</xsl:text>
  </xsl:for-each>
</xsl:template>

<!-- 
 | Extracts substitution instructions from ingredients.
 +-->
 <xsl:template match="ingredient[@substitute != '']" mode="subst">
  <xsl:text>  \item </xsl:text>

  <xsl:call-template name="utf-fraction">
    <xsl:with-param name="quantity" select="@min-quantity" />
  </xsl:call-template>

  <xsl:if test="@max-quantity">
    <xsl:text>--</xsl:text>

    <xsl:call-template name="utf-fraction">
      <xsl:with-param name="quantity" select="@max-quantity" />
    </xsl:call-template>
  </xsl:if>
  <xsl:text> </xsl:text>
  <xsl:value-of select="@unit" />
  <xsl:text> </xsl:text>
  <xsl:apply-templates select="key('ingredient-id', @id)/@name" />
  <xsl:text> for </xsl:text>
  <xsl:apply-templates select="key('ingredient-id', @substitute)/@name" />
  <xsl:text>&#xa;</xsl:text>

  <!-- Put the ingredients in a categorized list. -->
  <xsl:if test="key('ingredient-id',@id)/categories/category">
    <xsl:for-each select="key('ingredient-id',@id)/categories/category">
      <xsl:text>      \index[catingred]{</xsl:text>
      <xsl:call-template name="replace">
        <xsl:with-param name="pText" select="@name" />
        <xsl:with-param name="pToken" select="','" />
        <xsl:with-param name="pSubst" select="'!'" />
      </xsl:call-template>
      <xsl:text>!</xsl:text>
      <xsl:apply-templates select="../../@name" />
      <xsl:text>}%&#xa;</xsl:text>
    </xsl:for-each>
  </xsl:if>

  <!-- Index ingredients into a flat list. -->
  <xsl:text>      \index[flatingred]{</xsl:text> 
  <xsl:apply-templates select="key('ingredient-id', @id)/@name" />
  <xsl:text>!</xsl:text>
  <xsl:apply-templates select="ancestor::recipe/description/title" />
  <xsl:text>}%&#xa;</xsl:text>   
</xsl:template>

<!--
 | Splits the ingredient preconditions using the Oxford serial comma:
 | http://www.mhonarc.org/archive/html/xsl-list/2008-06/msg00401.html
 | by Ronnie Royston
 +-->
 <xsl:template match="@condition" mode="prep">
  <xsl:variable name="actions" select="str:tokenize( ., ',' )" />
  <xsl:variable name="tokens" select="count( $actions )" />

  <xsl:variable name="condition">
    <xsl:for-each select="$actions">
      <xsl:choose>
        <xsl:when test="$tokens > 2">
          <xsl:choose>
            <xsl:when test="position()=1">
              <xsl:value-of select="concat(' ', ., ',')"/>
            </xsl:when>
            <xsl:when test="position()=last()">
              <xsl:value-of select="concat(' and ', .)"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="concat(' ', .,',')"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:when test="$tokens = 2">
          <xsl:choose>
            <xsl:when test="position()=1">
              <xsl:value-of select="concat(' ', .,' and')"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="concat(' ', .)"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="concat(' ', .)"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </xsl:variable>

  <xsl:call-template name="escape">
    <xsl:with-param
      name="e" select="string:capitalize(normalize-space($condition))" />
    </xsl:call-template>
  </xsl:template>

<!--
 | Splits a delimited string by substitution text.
 +-->
 <xsl:template name="replace">
  <xsl:param name="pText"/>
  <xsl:param name="pToken"/>
  <xsl:param name="pSubst" /> 

  <xsl:choose>
    <!-- End of recursion. -->
    <xsl:when test="string-length($pText) = 0" />

    <!-- While there are more tokens... -->
    <xsl:when test="contains($pText, $pToken)">
      <xsl:value-of select="substring-before($pText, $pToken)"/>
      <xsl:value-of select="$pSubst"/>

      <xsl:call-template name="replace">
        <xsl:with-param name="pText" select="substring-after($pText, $pToken)"/>
        <xsl:with-param name="pToken" select="$pToken"/>
        <xsl:with-param name="pSubst" select="$pSubst" /> 
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <!-- The text had no tokens, so display it without parsing. -->
      <xsl:value-of select="$pText"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="directions[descendant::text() or @label != '']">
  <xsl:if test="not(preceding-sibling::directions)">
    <xsl:text>\startinstructions&#xa;&#xa;</xsl:text>
  </xsl:if>
  <xsl:text>\begin{instructions}&#xa;</xsl:text>
  <xsl:apply-templates select="@label" />
  <xsl:apply-templates select="step" />
  <xsl:if test="not(step)">
    <xsl:text>\item Serve!</xsl:text>
  </xsl:if>
  <xsl:text>\end{instructions}&#xa;&#xa;</xsl:text>
</xsl:template>

<xsl:template match="step">
  <xsl:text>  \item </xsl:text>
  <xsl:value-of select="normalize-space(string:capitalize(@action))" />
  <!-- If the first character is punctuation, don't insert a space. -->
  <xsl:if test="not(contains($punctuation,substring(normalize-space(text()),1,1)))"><xsl:text> </xsl:text></xsl:if>
  <xsl:apply-templates mode="escape" />
  <xsl:text>.&#xa;</xsl:text>
</xsl:template>

<!--
 | Provides output escaping around user-defined text. This ensures that
 | users cannot embed malicious LaTeX code inside their recipes.
 |
 | All text that must be escaped is transformed through this template.
 |
 | @param e - The string to escape.
 | @see http://stackoverflow.com/a/2627303/59087
 | @see http://tex.stackexchange.com/q/97448/2148
 +-->
 <xsl:template name="escape">
  <xsl:param name="e" />

  <xsl:call-template name="replace">
    <xsl:with-param name="pText" select="$e" />
    <xsl:with-param name="pToken" select="'&amp;'" />
    <xsl:with-param name="pSubst" select="'\&amp;'" />
  </xsl:call-template>
</xsl:template>

<!--
 | Normalizes and capitalizes all the words of an escaped string.
 |
 | @param ec - The string to normalize, capitalize, and escape.
 +-->
 <xsl:template name="escape-capitalize">
  <xsl:param name="ec" />

  <xsl:variable name="escaped">
    <xsl:call-template name="escape">
      <xsl:with-param name="e" select="$ec" />
    </xsl:call-template>
  </xsl:variable>

  <xsl:value-of select="normalize-space(string:capitalize-words($escaped))" />
</xsl:template>

<xsl:template match="text()" mode="escape">
  <xsl:call-template name="escape">
    <xsl:with-param name="e" select="." />
  </xsl:call-template>
</xsl:template>

<xsl:template match="text()" mode="escape-capitalize">
  <xsl:call-template name="escape-capitalize">
    <xsl:with-param name="ec" select="." />
  </xsl:call-template>
</xsl:template>

<!-- Eat items that do not have an existing template. -->
<xsl:template match="*" />

</xsl:stylesheet>