The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
<?xml version="1.0" encoding="utf-8"?>

<!-- AUTHOR: Chris Mungall  cjm at fruitfly dot org  -->

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                version="1.0">

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

  <!-- index everything with an ID attribute -->
  <xsl:key name="k-macro" match="//*" use="@id"/>

  <!-- match everything -->
  <xsl:template match="/ | @* | node()">
    <xsl:choose>
      <!-- replace terminal xxx_id elements with expanded macro -->
      <xsl:when test="contains(name(.),'_id') and not(*)">
        <xsl:if test="not(key('k-macro',.))">
          <xsl:message>
            No such ID: 
            <xsl:value-of select="."/> 
            in
            <xsl:value-of select="name(.)"/>
        </xsl:message>
        </xsl:if>
        <!-- fetch expansion term from ID index -->
        <xsl:copy>
          <xsl:variable name="macro" select="key('k-macro',.)"/>
          <!-- recursively process macros (removing id attr) -->
          <xsl:for-each select="$macro">
            <xsl:copy>
              <xsl:apply-templates select="$macro/*"/>
            </xsl:copy>
          </xsl:for-each>
        </xsl:copy>
      </xsl:when>
      <xsl:when test="count(@id)">
        <!-- remove existing macros (a macro is anything with id attr) -->
      </xsl:when>
      <xsl:otherwise>
        <!-- recursively apply this same template again -->
        <xsl:copy>
          <xsl:apply-templates select="@* | node()" />
        </xsl:copy>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

</xsl:stylesheet>