Dusting off the Résumé, Part 2: Converting to HTML

Earlier, I shared with you the Schema I use for writing my Résumé in XML. In this post, I want to talk about how that XML becomes something human-readable: Hyper Text Markup Language (HTML).

I wrote my first Web Page in 1993, one year after Tim Berners-Lee released the now ubiquitous language of the Internet. Back then, to get information on the Internet you were restricted to dial-up Bulletin Board Systems (BBS), Gopher and Archie for file search and retrieval, and Usenet for staying abreast of the latest computer news or just chew the fat. I was, in fact, a very early adopter, immediately seeing the potential of HTML to revolutionize the world of digital communications.

What’s more, unlike JavaScript Object Notation (JSON), which is best for HTML POST requests and related operations, because HTML is a markup language and a full subset of XML, it makes more sense to encode my work history and Curriculum Vitae (CV). This is why I continue to maintain my Résumé and the longer form CV in XML and don’t convert it to JSON.

That said, it would be fascicle to convert XML to JSON and many application already do this.

The translation from XML to HTML, however, takes more finesse. Fortunately, there’s a subset of XML known as eXtensible Stylesheet Language Transformation (XSLT). XSLT is a rudimentary rule-based language with recursion. It allows me to iterate over fields and concatenate them with commas (,), or hide fields which are deprecated as no longer relevant to the modern software job market.

As such, XSLT is a very verbose language which requires many elements to define how each XML component will be handled. Nonetheless, the full transform is included below.

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:param name="amazon" select="'http://www.amazon.com/exec/obidos/ASIN/'"/>
<xsl:param name="deprecated_text" select="'Hidden text has been excized from this document!'"/>
<xsl:param name="redacted_text" select="'Redacted text has been blacked out from this document!'"/>
<xsl:param name="col_of_data" select="2"/>

<xsl:variable name="deprecated">
  <span class="deprecated">deprecated</span>
  <xsl:comment>
    <xsl:value-of select="$deprecated_text"/>
  </xsl:comment>
</xsl:variable>

<xsl:variable name="redacted">
  <span class="redacted">redacted</span>
  <xsl:comment>
    <xsl:value-of select="$redacted_text"/>
  </xsl:comment>
</xsl:variable>

<!-- Entities -->
<!-- Question: How do I get Entities to appear as entities?? -->

<!-- Pure (PCDATA) Elements -->
<!-- Names and Addresses -->

<xsl:template match="name">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="oldname">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="street">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="apartment">
  <xsl:choose>
    <!-- Use param / param-with -->
    <xsl:when test="@spellout = 'true'">
      <xsl:text disable-output-escaping="yes">Apartment&amp;nbsp;</xsl:text>
    </xsl:when>
    <xsl:otherwise>
      <xsl:text disable-output-escaping="yes">Apt.&amp;nbsp;</xsl:text>
    </xsl:otherwise>
  </xsl:choose>
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="city">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="state">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="province">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="postal">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="country">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="e-mail">
  <a href="mailto:{.}">
    <xsl:choose>
      <xsl:when test="parent::address">
        <xsl:attribute name="class">
          <xsl:text>address</xsl:text>
        </xsl:attribute>
      </xsl:when>
      <xsl:when test="parent::reference">
        <xsl:attribute name="class">
          <xsl:text>reference</xsl:text>
        </xsl:attribute>
      </xsl:when>
    </xsl:choose>
    <xsl:apply-templates/>
  </a>
</xsl:template>

<xsl:template match="phone">
  <xsl:apply-templates/> (H)
</xsl:template>

<xsl:template match="mobile">
  <xsl:apply-templates/> (M)
</xsl:template>
<!-- Keys and Headings -->

<xsl:template match="heading">
  <hr class="separator"/>
  <h2 class="heading">
    <xsl:apply-templates/>
  </h2>
</xsl:template>

<xsl:template match="key">
  <span class="text_heading">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="value">
  <xsl:apply-templates/>
</xsl:template>

<!-- Date and Time -->

<xsl:template match="year">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="month">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="day">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="hour">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="minute">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="second">
  <xsl:apply-templates/>
</xsl:template>

<!-- Descriptive Elements -->

<xsl:template match="position">
  <span class="position">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="product">
  <span class="product">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="degree">
  <span class="degree">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="course">
  <span class="course">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="author">
  <span class="author">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="author-et-al">
  <xsl:text> </xsl:text><span class="author"><em>et al</em></span>
</xsl:template>

<xsl:template match="publisher">
  <span class="publisher">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="subject">
  <p class="subject">
    <xsl:apply-templates/>
  </p>
</xsl:template>

<xsl:template match="isbn">
  <a class="isbn" href="{$amazon}{.}/">
    <xsl:apply-templates/>
  </a>
</xsl:template>

<!-- deprecated -->

<xsl:template match="deprecated">
  <!-- Ingore deprecated text -->
  <xsl:copy-of select="$deprecated"/>
</xsl:template>

<!-- Compound Elements -->
<!-- Compound Time -->

<xsl:template match="date">
  <span class="date">
    <xsl:if test="day">
      <!-- European Date Format -->
      <xsl:apply-templates select="day"/>
      <!-- day requires month, so we know that the month is next -->
      <xsl:text> </xsl:text>
    </xsl:if>
    <xsl:if test="month">
      <xsl:apply-templates select="month"/>
    </xsl:if>
    <xsl:if test="year">
      <xsl:if test="month">
        <xsl:text> </xsl:text>
      </xsl:if>
      <xsl:apply-templates select="year"/>
    </xsl:if>
    <xsl:if test="hour">
      <xsl:if test="not(*[position() = 1 and self::hour])">
        <xsl:text> </xsl:text>
      </xsl:if>
      <xsl:apply-templates select="country"/>
    </xsl:if>
    <xsl:if test="minute">
      <!-- Always follows hour -->
      <xsl:text>:</xsl:text>
      <xsl:apply-templates select="minute"/>
    </xsl:if>
    <xsl:if test="second">
      <!-- Always follows minute -->
      <xsl:text>:</xsl:text>
      <xsl:apply-templates select="second"/>
    </xsl:if>
  </span>
</xsl:template>

<xsl:template match="from">
  <span class="from_date">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="to">
  <span class="to_date">
    <xsl:apply-templates/>
  </span>
</xsl:template>

<xsl:template match="period">
  <xsl:choose>
    <xsl:when test="date">
      <p class="period">
        <xsl:apply-templates select="date"/>
      </p>
    </xsl:when>
    <xsl:when test="from and to">
      <p class="period">
        <xsl:apply-templates select="from"/>
        <xsl:text disable-output-escaping="yes"> &amp;ndash; </xsl:text>
        <xsl:apply-templates select="to"/>
      </p>
    </xsl:when>
  </xsl:choose>
</xsl:template>

<!-- Simple Compound Elements -->

<xsl:template match="address">
  <span class="address">
    <xsl:if test="street">
      <xsl:apply-templates select="street"/>
      <xsl:if test="apartment">
        <xsl:text>, </xsl:text>
        <xsl:apply-templates select="apartment"/>
      </xsl:if>
    </xsl:if>
    <xsl:if test="city">
      <xsl:if test="not(*[position() = 1 and self::city])">
        <xsl:text>, </xsl:text>
      </xsl:if>
      <xsl:apply-templates select="city"/>
    </xsl:if>
    <xsl:choose>
      <xsl:when test="state">
        <xsl:if test="not(*[position() = 1 and self::state])">
          <xsl:text>, </xsl:text>
        </xsl:if>
        <xsl:apply-templates select="state"/>
      </xsl:when>
      <xsl:when test="province">
        <xsl:if test="not(*[position() = 1 and self::province])">
          <xsl:text>, </xsl:text>
        </xsl:if>
        <xsl:apply-templates select="province"/>
      </xsl:when>
    </xsl:choose>
    <xsl:if test="postal">
      <xsl:if test="not(*[position() = 1 and self::postal])">
        <xsl:text> </xsl:text>
      </xsl:if>
      <xsl:apply-templates select="postal"/>
    </xsl:if>
    <xsl:if test="country">
      <xsl:if test="not(*[position() = 1 and self::country])">
        <xsl:text> </xsl:text>
      </xsl:if>
      <xsl:apply-templates select="country"/>
    </xsl:if>
    <xsl:if test="e-mail">
      <xsl:if test="not(*[position() = 1 and self::e-mail])">
        <br/>
      </xsl:if>
      <xsl:apply-templates select="e-mail"/>
    </xsl:if>
    <!-- TODO: Put the phone and mobile phone on the same line. -->
    <xsl:if test="phone">
      <xsl:if test="not(*[position() = 1 and self::phone])">
        <br/>
      </xsl:if>
      <xsl:apply-templates select="phone"/>
    </xsl:if>
    <xsl:if test="mobile">
      <xsl:if test="not(*[position() = 1 and self::mobile])">
        <br/>
      </xsl:if>
      <xsl:apply-templates select="mobile"/>
    </xsl:if>
  </span>
</xsl:template>

<xsl:template match="book">
  <xsl:choose>
    <xsl:when test="not(boolean(@deprecated))">
      <!-- This is a full Book Definition -->
      <p class="bibliography">
        <xsl:if test="author">
          <xsl:for-each select="author">
            <xsl:if test="not(position() = 1)">
              <xsl:text>, </xsl:text>
            </xsl:if>
            <xsl:if test="(position() = last() and not(boolean(../author-et-al)))">
              <xsl:text> and </xsl:text>
            </xsl:if>
            <xsl:apply-templates select="."/>
          </xsl:for-each>
          <xsl:if test="author-et-al">
            <xsl:apply-templates select="author-et-al"/>
          </xsl:if>
          <xsl:text>. </xsl:text>
        </xsl:if>
        <!-- If ISBN is provided, use to link to Amazon -->
        <xsl:choose>
          <xsl:when test="isbn">
            <a class="book" href="{$amazon}{isbn}/">
              <xsl:apply-templates select="name"/>
            </a>
          </xsl:when>
          <xsl:otherwise>
            <span class="book">
              <xsl:apply-templates select="name"/>
            </span>
          </xsl:otherwise>
        </xsl:choose>
        <xsl:if test="publisher | date">
          <xsl:text>. </xsl:text>
        </xsl:if>
        <xsl:if test="address">
          <xsl:apply-templates select="address"/>
          <!-- Publisher must follow -->
          <xsl:text>: </xsl:text>
        </xsl:if>
        <xsl:if test="publisher">
          <xsl:apply-templates select="publisher"/>
          <xsl:if test="date">
            <xsl:text>, </xsl:text>
          </xsl:if>
        </xsl:if>
        <xsl:if test="date">
          <xsl:apply-templates select="date"/>
        </xsl:if>
      </p>
      <xsl:if test="subject">
        <xsl:apply-templates select="subject"/>
      </xsl:if>
    </xsl:when>
    <xsl:otherwise>
      <!-- Ingore deprecated text -->
      <xsl:copy-of select="$deprecated"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="institution">
  <xsl:choose>
    <xsl:when test="not(boolean(@deprecated))">
      <!-- This is a full Institutional Definition -->
      <!-- Normally a Job or School will access the elements
           independently, so this is only bibliographical. -->
      <p class="institution">
        <xsl:choose>
          <xsl:when test="@url">
            <span class="institution">
              <a href="{@url}">
                <xsl:apply-templates select="name"/>
              </a>
            </span>
          </xsl:when>
          <xsl:otherwise>
            <span class="institution">
              <xsl:apply-templates select="name"/>
            </span>
          </xsl:otherwise>
        </xsl:choose>
        <xsl:for-each select="oldname">
          <xsl:choose>
            <xsl:when test="position() = 1">
              <xsl:text> (Formerly </xsl:text>
            </xsl:when>
            <xsl:when test="not(position() = last())">
              <xsl:text>; </xsl:text>
            </xsl:when>
          </xsl:choose>
          <xsl:apply-templates/>
          <xsl:if test="position() = last()">
            <xsl:text>)</xsl:text>
          </xsl:if>
        </xsl:for-each>
        <xsl:if test="address">
          <xsl:text>, </xsl:text>
          <xsl:apply-templates select="address"/>
        </xsl:if>
      </p>
    </xsl:when>
    <xsl:otherwise>
      <!-- Ingore deprecated text -->
      <xsl:copy-of select="$deprecated"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="bookref">
  <xsl:variable name="ref" select="@ref"/>
  <xsl:variable name="book" select="//book[@name = $ref]"/>
  <xsl:choose>
    <xsl:when test="not(boolean($book/@deprecated))">
      <xsl:choose>
        <xsl:when test="$book/@url">
          <a class="bookref" href="{$book/@url}">
            <xsl:apply-templates select="$book/name"/>
          </a>
        </xsl:when>
        <xsl:when test="$book/isbn">
          <a class="bookref" href="{$amazon}{$book/isbn}/">
            <xsl:apply-templates select="$book/name"/>
          </a>
        </xsl:when>
        <xsl:otherwise>
          <span class="bookref">
            <xsl:apply-templates select="$book/name"/>
          </span>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:when>
    <xsl:otherwise>
      <!-- Ingore deprecated text -->
      <xsl:copy-of select="$deprecated"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="instref">
  <xsl:variable name="ref" select="@ref"/>
  <xsl:variable name="inst" select="//institution[@name = $ref]"/>
  <xsl:choose>
    <xsl:when test="not(boolean($inst/@deprecated))">
      <!-- This is an Inline Institution, therefore treat as character formatting -->
      <xsl:choose>
        <xsl:when test="$inst/@url">
          <span class="instref">
            <a href="{$inst/@url}">
              <xsl:apply-templates select="$inst/name"/>
            </a>
          </span>
        </xsl:when>
        <xsl:otherwise>
          <span class="instref">
            <xsl:apply-templates select="$inst/name"/>
          </span>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:if test="$inst/address">
        <xsl:text>, </xsl:text>
        <xsl:apply-templates select="$inst/address"/>
      </xsl:if>
    </xsl:when>
    <xsl:otherwise>
      <!-- Ingore deprecated text -->
      <xsl:copy-of select="$deprecated"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<!-- Simple Compound Markups -->

<xsl:template match="buzzword">
  <xsl:choose>
    <xsl:when test="not(boolean(@deprecated))">
      <xsl:choose>
        <xsl:when test="@url">
          <span class="buzzword">
            <a href="{@url}">
              <xsl:apply-templates/>
            </a>
          </span>
        </xsl:when>
        <xsl:otherwise>
          <span class="buzzword">
            <xsl:apply-templates/>
          </span>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:when>
    <xsl:otherwise>
      <!-- Ingore deprecated text -->
      <xsl:copy-of select="$deprecated"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="language">
  <xsl:choose>
    <xsl:when test="not(boolean(@deprecated))">
      <span class="language">
        <xsl:apply-templates/>
      </span>
      <!-- Level is Required -->
      <xsl:text> (</xsl:text>
      <xsl:value-of select="@level"/>
      <xsl:text>)</xsl:text>
    </xsl:when>
    <xsl:otherwise>
      <!-- Ingore deprecated text -->
      <xsl:copy-of select="$deprecated"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="status">
  <p class="status">
    <span class="text_heading">
      <xsl:value-of select="key"/>
      <xsl:text>: </xsl:text>
    </span>
    <xsl:apply-templates select="value"/>
  </p>
</xsl:template>

<xsl:template match="skill">
  <xsl:choose>
    <xsl:when test="not(boolean(@deprecated))">
      <p class="skill">
        <span class="text_heading">
          <xsl:value-of select="key"/>
          <xsl:text>: </xsl:text>
        </span>
        <!-- Use for-each to get buzzwords or languages because they
             cannot appear in the same skill set, so if one for-each
             has not element, it is not executed and saves me the trouble
             of writing an xsl:if or writing xsl:for-each
             select="*[self::buzzword | self::langauge]" -->
        <xsl:for-each select="buzzword[not(boolean(@deprecated))]">
          <xsl:if test="not(position() = 1)">
            <xsl:text>, </xsl:text>
          </xsl:if>
          <xsl:apply-templates select="."/>
        </xsl:for-each>
        <xsl:for-each select="language[not(boolean(@deprecated))]">
          <xsl:if test="not(position() = 1)">
            <xsl:text>, </xsl:text>
          </xsl:if>
          <xsl:apply-templates select="."/>
        </xsl:for-each>
      </p>
    </xsl:when>
    <xsl:otherwise>
      <!-- Ingore deprecated text -->
      <xsl:copy-of select="$deprecated"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="curriculum">
  <xsl:choose>
    <xsl:when test="not(boolean(@deprecated))">
      <p class="curriculum">
        <xsl:if test="child::key">
          <span class="text_heading">
            <xsl:value-of select="key"/>
            <xsl:text>: </xsl:text>
          </span>
        </xsl:if>
        <xsl:for-each select="course">
          <xsl:if test="not(position() = 1)">
            <xsl:text>, </xsl:text>
          </xsl:if>
          <xsl:apply-templates select="."/>
        </xsl:for-each>
      </p>
    </xsl:when>
    <xsl:otherwise>
      <!-- Ingore deprecated text -->
      <xsl:copy-of select="$deprecated"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="tools">
  <span class="tools">
    <xsl:for-each select="buzzword[not(boolean(@deprecated))]">
      <xsl:if test="not(position() = 1)">
        <xsl:text>, </xsl:text>
      </xsl:if>
      <xsl:apply-templates select="."/>
    </xsl:for-each>
  </span>
</xsl:template>

<xsl:template match="task">
  <li class="task"/>
  <p class="task">
    <xsl:apply-templates/>
  </p>
</xsl:template>

<xsl:template match="interest">
  <xsl:choose>
    <xsl:when test="@url">
      <a class="interest" href="{@url}">
        <xsl:apply-templates/>
      </a>
    </xsl:when>
    <xsl:otherwise>
      <span class="interest">
        <xsl:apply-templates/>
      </span>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="para">
  <!-- This will typically be overridden by another template -->
  <p>
    <xsl:apply-templates/>
  </p>
</xsl:template>

<xsl:template match="objective">
  <xsl:for-each select="para">
    <p class="objective">
      <xsl:apply-templates/>
    </p>
  </xsl:for-each>
</xsl:template>

<xsl:template match="summary">
  <xsl:for-each select="para">
    <p class="summary">
      <xsl:apply-templates select="."/>
    </p>
  </xsl:for-each>
  <table border="0" width="100%">
    <xsl:for-each select="task">
      <xsl:choose>
        <xsl:when test="position() mod $col_of_data = 1">
          <xsl:text disable-output-escaping="yes">&lt;tr&gt;</xsl:text>
        </xsl:when>
        <xsl:otherwise>
          <td>
            <img src="spacer.gif" class="spacer"/>
          </td>
        </xsl:otherwise>
      </xsl:choose>
      <!-- Spacer messes the percentage up! -->
      <td class="task" width="{100 div $col_of_data}%">
        <xsl:apply-templates select="."/>
      </td>
      <xsl:if test="position() mod $col_of_data = 0">
        <xsl:text disable-output-escaping="yes">&lt;/tr&gt;</xsl:text>
      </xsl:if>
    </xsl:for-each>
    <!-- Extra Close Row for odd numbers -->
    <xsl:if test="count(task) mod $col_of_data != 0">
      <xsl:text disable-output-escaping="yes">&lt;/tr&gt;</xsl:text>
    </xsl:if>
  </table>
</xsl:template>

<xsl:template match="achievement">
  <!-- Do the First Paragraph Separately with Tools, then the rest -->
  <!-- Problem with indentation and blank line after -->
  <li class="achievement"/>
  <p class="achievement">
    <xsl:if test="tools">
      <!-- Double Spanning on Tools! -->
      <xsl:apply-templates select="tools"/>
      <xsl:if test="para">
        <span class="tools">
          <xsl:text>: </xsl:text>
        </span>
      </xsl:if>
    </xsl:if>
    <!-- Wacky way of saying "Don't Apply this template,
         apply the ones below! -->
    <xsl:for-each select="para[1]">
      <xsl:apply-templates/>
    </xsl:for-each>
  </p>
  <xsl:for-each select="para[position() &gt; 1]">
    <!-- Convert to Unordered List -->
    <p class="achievement">
      <xsl:apply-templates/>
    </p>
  </xsl:for-each>
</xsl:template>

<xsl:template match="title">
  <span class="title">
    <h1 class="name">
      <xsl:apply-templates select="name"/>
      <xsl:for-each select="oldname">
        <xsl:if test="position() = 1">
          <xsl:text> (</xsl:text>
        </xsl:if>
        <xsl:apply-templates/>
        <xsl:choose>
          <xsl:when test="position() = last()">
            <xsl:text>)</xsl:text>
          </xsl:when>
          <xsl:otherwise>
            <xsl:text>, </xsl:text>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:for-each>
    </h1>
    <p class="address">
      <xsl:apply-templates select="address"/>
    </p>
  </span>
</xsl:template>

<xsl:template match="bibliography">
  <xsl:if test="not(boolean(@deprecated))">
    <xsl:if test="heading">
      <xsl:apply-templates select="heading"/>
    </xsl:if>
    <xsl:for-each select="book">
      <xsl:apply-templates select="."/>
    </xsl:for-each>
    <xsl:for-each select="institution">
      <xsl:apply-templates select="."/>
    </xsl:for-each>
  </xsl:if>
</xsl:template>

<xsl:template match="job">
  <xsl:if test="not(boolean(@deprecated))">
    <table width="100%">
      <tr>
        <td>
          <xsl:apply-templates select="institution"/>
        </td>
        <td>
          <img src="spacer.gif" class="spacer"/>
        </td>
        <td>
          <!-- To Do: Please try to if the position is
               present, extend this cell and the spacer
               to rowspan="2" -->
          <!-- Problem: Width of "Period" too small! -->
          <xsl:apply-templates select="period"/>
        </td>
      </tr>
      <xsl:if test="position">
        <tr>
          <td colspan="2">
            <xsl:apply-templates select="position"/>
          </td>
        </tr>
      </xsl:if>
      <xsl:for-each select="achievement">
        <tr>
          <td colspan="2">
            <xsl:apply-templates select="."/>
          </td>
        </tr>
      </xsl:for-each>
    </table>
  </xsl:if>
</xsl:template>

<xsl:template match="school">
  <xsl:if test="not(boolean(@deprecated))">
    <table width="100%">
      <tr>
        <td>
          <xsl:apply-templates select="institution"/>
        </td>
        <td>
          <img src="spacer.gif" class="spacer"/>
        </td>
        <td>
          <!-- To Do: Please try to, if the degree is
               present, extend this cell and the spacer
               to rowspan="2" -->
          <!-- Problem: Width of "Period" too small! -->
          <xsl:apply-templates select="period"/>
        </td>
      </tr>
      <xsl:if test="degree">
        <tr>
          <td colspan="2">
            <xsl:apply-templates select="degree"/>
          </td>
        </tr>
      </xsl:if>
      <xsl:for-each select="achievement">
        <tr>
          <td colspan="2">
            <xsl:apply-templates select="."/>
          </td>
        </tr>
      </xsl:for-each>
      <xsl:for-each select="curriculum">
        <tr>
          <td colspan="2">
            <xsl:apply-templates select="."/>
          </td>
        </tr>
      </xsl:for-each>
    </table>
  </xsl:if>
</xsl:template>

<xsl:template match="reference">
  <xsl:if test="not(boolean(@deprecated))">
    <p class="reference">
      <xsl:apply-templates select="name"/>
      <xsl:if test="position">
        <br/>
        <xsl:apply-templates select="position"/>
      </xsl:if>
      <xsl:if test="instref">
        <!-- Since we know the Institution-Reference requires a
             position, we will add the comma here before the
             institution definition -->
        <xsl:text>, </xsl:text>
        <xsl:apply-templates select="instref"/>
      </xsl:if>
      <xsl:if test="e-mail">
        <br/>
        <xsl:apply-templates select="e-mail"/>
      </xsl:if>
      <xsl:if test="phone">
        <br/>
        <xsl:apply-templates select="phone"/>
      </xsl:if>
      <xsl:if test="mobile">
        <br/>
        <xsl:apply-templates select="mobile"/>
      </xsl:if>
    </p>
  </xsl:if>
</xsl:template>

<xsl:template match="section">
  <xsl:if test="not(boolean(@deprecated))">
    <xsl:apply-templates select="heading"/>
    <xsl:choose>
      <xsl:when test="child::objective">
        <xsl:apply-templates select="objective"/>
      </xsl:when>
      <xsl:when test="child::status">
        <table border="0" width="100%">
          <!-- For Simplicity I will but the Spanning cells first -->
          <!-- Ideally, the XSL should use an iterative approach that
               would end a row tag when it finds a spanning, then
               span the cell across all columns, then reset the
               numbering to "1" or some odd number to continue the
               data as if the next cell was the first one.  The problem
               with this is simply that I can't reset the position()
               counter to do this.  So, voilà, the simple approach -->
          <xsl:for-each select="status[@spanning = 'true']">
            <tr>
              <td class="status" colspan="{$col_of_data}">
                <xsl:apply-templates select="."/>
              </td>
            </tr>
          </xsl:for-each>
          <xsl:for-each select="status[not(boolean(@spanning))]">
            <xsl:choose>
              <xsl:when test="position() mod $col_of_data = 1">
                <xsl:text disable-output-escaping="yes">&lt;tr&gt;</xsl:text>
              </xsl:when>
              <xsl:otherwise>
                <td>
                  <img src="spacer.gif" class="spacer"/>
                </td>
              </xsl:otherwise>
            </xsl:choose>
            <!-- Spacer messes the percentage up! -->
            <td class="status" width="{100 div $col_of_data}%">
              <xsl:apply-templates select="."/>
            </td>
            <xsl:if test="position() mod $col_of_data = 0">
              <xsl:text disable-output-escaping="yes">&lt;/tr&gt;</xsl:text>
            </xsl:if>
          </xsl:for-each>
          <!-- Extra Close Row for odd numbers -->
          <xsl:if test="count(resume/section/summary/task) mod $col_of_data != 0">
            <xsl:text disable-output-escaping="yes">&lt;/tr&gt;</xsl:text>
          </xsl:if>
        </table>
      </xsl:when>
      <xsl:when test="child::summary">
        <xsl:apply-templates select="summary"/>
      </xsl:when>
      <xsl:when test="child::skill">
        <xsl:for-each select="skill">
          <xsl:apply-templates select="."/>
        </xsl:for-each>
      </xsl:when>
      <xsl:when test="child::job">
        <xsl:for-each select="job">
          <xsl:apply-templates select="."/>
          <!-- Blank Line After to separate Records? -->
        </xsl:for-each>
      </xsl:when>
      <xsl:when test="child::school">
        <xsl:for-each select="school">
          <!-- Blank Line After to separate Records? -->
          <xsl:apply-templates select="."/>
        </xsl:for-each>
      </xsl:when>
      <xsl:when test="child::interest">
        <xsl:for-each select="interest">
          <xsl:if test="not(position() = 1)">
            <xsl:text>, </xsl:text>
          </xsl:if>
          <xsl:apply-templates select="."/>
        </xsl:for-each>
      </xsl:when>
      <xsl:when test="child::reference">
        <xsl:for-each select="reference">
          <xsl:apply-templates select="."/>
        </xsl:for-each>
      </xsl:when>
    </xsl:choose>
  </xsl:if>
</xsl:template>

<xsl:template match="/">
  <html>
  <head>
  <link href="Résumé.css" rel="stylesheet" type="text/css"/>
  <xsl:apply-templates select="resume/title"/>
  </head>
  <body>
    <xsl:for-each select="resume/section[not(boolean(@deprecated))]">
      <xsl:apply-templates select="."/>
    </xsl:for-each>
    <xsl:apply-templates select="resume/bibliography[not(boolean(@deprecated))]"/>
    <hr class="separator"/>
    <xsl:if test="resume/@url">
      <table width="100%">
        <tr>
          <td width="33%"></td>
          <td width="67%">
            <p class="online">
              <xsl:text>Text available on-line at </xsl:text>
              <a type="online" href="{resume/@url}">
                <xsl:value-of select="resume/@url"/>
              </a>
              <xsl:text>.</xsl:text>
            </p>
          </td>
        </tr>
      </table>
    </xsl:if>
  </body>
  </html>
</xsl:template>
</xsl:stylesheet>

Résumé.xsl, the eXstensible Stylesheet Language Transform of an XML Résumé.

Once the XML has been translated to HTML, it’s still rather rough. However, one can use Cascaded Style Sheets (CSS) to transform that raw XML into something much more pleasant to look at. It also hides Deprecated elements to they don’t appear in the final, rendered page. It sets the font, and colors the hard breaks, and formats everything in neat boxes.

Because the XSLT adds class modifiers to a number of the tags, the CSS can be rather richly defined and very specific to each document element.

body
{
    FONT-SIZE: 11pt;
    FONT-FAMILY: 'Times New Roman'
}
td
{
vertical-align : top;
}
span..buzzword
{
    FONT-STYLE: italic;
    FONT-FAMILY: 'Courier New'
}
span.institution
{
}
p.summary
{
    TEXT-ALIGN: justify
}
hr.separator
{
    BORDER-RIGHT: blue thick groove;
    BORDER-TOP: blue thick groove;
    BORDER-LEFT: blue thick groove;
    BORDER-BOTTOM: blue thick groove
}
H2.heading
{
    BORDER-TOP: medium none;
    FONT-SIZE: 12pt;
    COLOR: blue;
    FONT-FAMILY: Helvetica;
    LETTER-SPACING: 6pt;
    TEXT-ALIGN: center
}
H1.name
{
    FONT-WEIGHT: bolder;
    FONT-SIZE: 20pt;
    TEXT-ALIGN: center
}
SPAN.title
{
    COLOR: #ff9900;
    FONT-STYLE: italic;
    FONT-FAMILY: Helvetica;
    TEXT-ALIGN: center
}
P.address
{
    TEXT-ALIGN: center
}
SPAN.address
{
}
SPAN.text_heading
{
    FONT-WEIGHT: bolder
}
SPAN.position
{
    FONT-STYLE: italic;
    FONT-FAMILY: Arial
}
SPAN.product
{
    FONT-STYLE: italic
}
SPAN.degree
{
    FONT-STYLE: italic;
    FONT-FAMILY: Arial
}
SPAN.course
{
}
SPAN.author
{
}
A.isbn
{
    VISIBILITY: inherit
}
SPAN.date
{
}
SPAN.from_date
{
}
SPAN.to_date
{
}
P.period
{
    COLOR: #800080;
    FONT-STYLE: italic;
    TEXT-ALIGN: right
}
SPAN.publisher
{
}
P.bibliography
{
}
A.book
{
    VISIBILITY: inherit;
    FONT-STYLE: italic
}
SPAN.book
{
    FONT-STYLE: italic
}
SPAN.language
{
}
P.subject
{
}
SPAN.instref
{
    FONT-STYLE: italic
}
P.institution
{
    FONT-WEIGHT: bolder;
    FONT-SIZE: 12pt;
    FONT-FAMILY: Arial
}
A.address
{
    VISIBILITY: inherit
}
P.status
{
}
P.skill
{
}
P.curriculum
{
}
SPAN.tools
{
    FONT-WEIGHT: bolder;
    COLOR: #4c8000
}
P.task
{
}
SPAN.interest
{
}
A.interest
{
    VISIBILITY: inherit
}
P.objective
{
}
HEAD
{
    FONT-SIZE: 11pt;
    FONT-FAMILY: 'Times New Roman'
}
P.reference
{
}
P.online
{
    TEXT-ALIGN: right
}
A.online
{
    VISIBILITY: inherit
}
SPAN.redacted
{
    color: black;
    background-color: black;
    TEXT-DECORATION: line-through;
    font-style: italic
}
SPAN.deprecated
{
    DISPLAY: none;
    TEXT-DECORATION: line-through
}
TD.task
{
    VERTICAL-ALIGN: top;
    LIST-STYLE-TYPE: disc
}
TD.status
{
    VERTICAL-ALIGN: top;
}
img.spacer
{
width : 10px;
}
li.task
{
list-style-type : disc;
}
li.achievement
{
list-style-type : disc;
}

Résumé.css, the Cascaded Style Sheet used to transform the Résumé.

Once the CSS is applied, the final Résumé can be generated. Although I still had to perform some tweaking to the HTML, mainly to fix deprecated sections to eliminate trailing commas and other cleanups, for the most part, once the CSS was applied, the Résumé looked nearly perfect. The only change to the above CSS, because CSS on this site is site-wide, was that I put everything within a resume2002 class so that the CSS didn’t apply to every post, only to those inside a Résumé block.

You can see what the final, 2002 version, with a couple modern edits for some of my subsequent publications, of the Résumé looks like here.

I am working on an update to the XML Résumé thanks to figuring out how to get around the Xmplify bug with including Entity definition in an XML Schema. Apparently, if you Entities in your XML, you need to use a DTD to define your XML, you can’t use XML Schema. Fortunately, I did have the full DTD version of my XML specification, so that was easy to solve in the short term while Xmplify is fixed to allow XML Schema with entity-only DOCTYPE sections at the top of XML documents.

I have one final note about the code included in the Résumé in XML posts. In part 1, I neglected to take into account multiple spaces would get reduced to one, meaning that the formatting lost all of the proper indentation and made the code hard to read. I was hasty when I generated the HTML-compatable markup for the two documents there. When I generated the above documents, however, I generated the correct HTML using the simply four lines of Python below, once for each file.

>>> with open('Résumé.xml', 'rb') as f:
...     xml = f.read().decode('latin_1')
... 
>>> with open('xml.html', 'w') as f:
...     f.write(html.escape(xml).replace('\n', '
').replace(' ', ' '))
... 
>>>

Python code to turn XML into into readable HTML

I would love to show you the 2020 version of my Résumé but, alas, I’m still working on it and still have 2002–2012 to cover. I did write entries for 2010–2012, but then realized that I combined the description of two distinct projects into one, so I have to rewrite that section and then finish the first eight years of entries. That killed my Friday when this post was supposed to go out.

I figure that will take me another week, and have about five more projects to cover, but won’t know for sure until I go through the remaining Year End Reviews. But I do have the complete last eight years and, again, if you like what you see, I’m still happily available for hire.

Dusting off the Résumé, Part 1: The Schema

I wrote my current Résumé in eXtensible Markup Language (XML), back in 2002, just before I started work at the US Naval Research Laboratory. Of course, XML, being a rather free-form markup style, it works best when constrained. At first, I did this via a Document Type Definition (DTD).

<!-- Entities -->
<!ENTITY  nbsp         "&#x00A0;">
<!ENTITY  chi2         "&#x03C7;&#x00B2;">
<!ENTITY  endash       "&#x2013;">
<!ENTITY  eacute       "&#x00E9;">
<!-- Pure (PCDATA) Elements -->
<!-- Names and Addresses -->
<!ELEMENT name         (#PCDATA)>
<!ELEMENT oldname      (#PCDATA)>
<!ELEMENT street       (#PCDATA)>
<!ELEMENT apartment    (#PCDATA)>
  <!ATTLIST apartment    spellout     (true|false) "false">
<!ELEMENT city         (#PCDATA)>
<!ELEMENT state        (#PCDATA)>
<!ELEMENT province     (#PCDATA)>
<!ELEMENT postal       (#PCDATA)>
<!ELEMENT country      (#PCDATA)>
<!ELEMENT e-mail       (#PCDATA)>
<!ELEMENT phone        (#PCDATA)>
<!ELEMENT mobile       (#PCDATA)>
<!-- Keys and Headings -->
<!ELEMENT heading      (#PCDATA)>
<!ELEMENT key          (#PCDATA)>
<!ELEMENT value        (#PCDATA)>
<!-- Date and Time -->
<!ELEMENT year         (#PCDATA)>
<!ELEMENT month        (#PCDATA)>
<!ELEMENT day          (#PCDATA)>
<!ELEMENT hour         (#PCDATA)>
<!ELEMENT minute       (#PCDATA)>
<!ELEMENT second       (#PCDATA)>
<!-- Descriptive Elements -->
<!ELEMENT position     (#PCDATA)>
<!ELEMENT product      (#PCDATA)>
<!ELEMENT degree       (#PCDATA)>
<!ELEMENT course       (#PCDATA)>
<!ELEMENT author       (#PCDATA)>
<!ELEMENT publisher    (#PCDATA)>
<!ELEMENT subject      (#PCDATA)>
<!ELEMENT isbn         (#PCDATA)>
<!-- deprecated -->
<!ELEMENT deprecated   (#PCDATA)>
<!-- Compound Elements -->
<!-- Compound Time -->
<!ELEMENT date         (year?,(month,day?)?,(hour,(minute,second?)?)?)>
<!ELEMENT from         (date)>
<!ELEMENT to           (date)>
<!ELEMENT period       ((from,to)|date)>
<!-- Simple Compound Elements -->
<!ELEMENT address      ((street,apartment?)?,city?,(state|province)?,postal?,country?,e-mail?,phone?,mobile?)>
<!ELEMENT book         (name,author*,(address?,publisher)?,date?,subject?,isbn?)>
  <!ATTLIST book         name         ID           #IMPLIED>
<!ELEMENT institution  (name,oldname*,address?)>
  <!ATTLIST institution  name         ID           #IMPLIED>
  <!ATTLIST institution  url          CDATA        #IMPLIED>
  <!ATTLIST institution  deprecated   (true|false) "false">
<!-- Reference Elements -->
<!ELEMENT bookref      EMPTY>
<!ATTLIST bookref      ref          IDREF        #REQUIRED>
<!ELEMENT instref      EMPTY>
<!ATTLIST instref      ref          IDREF        #REQUIRED>
<!-- Simple Compound Markups -->
<!ELEMENT buzzword     (#PCDATA|deprecated)*>
  <!ATTLIST buzzword     deprecated   (true|false) "false">
  <!ATTLIST buzzword     url          CDATA        #IMPLIED>
<!ELEMENT language     (#PCDATA)>
  <!ATTLIST language     level        (native|fluent|conversational|good|fair|poor) #REQUIRED>
  <!ATTLIST language     deprecated   (true|false) "false">
<!-- Hashing Elements -->
<!-- Should I make the Key and Attribute?? -->
<!ELEMENT status       (key,value)>
  <!ATTLIST status       spanning     (true|false) "false">
<!ELEMENT skill        (key,(buzzword*|language*))>
  <!ATTLIST skill        deprecated   (true|false) "false">
<!ELEMENT curriculum   (key?,course*)>
  <!ATTLIST curriculum   deprecated   (true|false) "false">
<!-- Compound Text Elements -->
<!ELEMENT tools        (buzzword*)>
<!ELEMENT task         (#PCDATA|buzzword|instref)*>
<!ELEMENT interest     (#PCDATA|buzzword|bookref|instref|deprecated)*>
  <!ATTLIST interest     url          CDATA        #IMPLIED>
<!ELEMENT para         (#PCDATA|buzzword|product|instref|deprecated)*>
<!-- Compound Paragraph Elements -->
<!ELEMENT objective    (para*)>
<!ELEMENT summary      (para*,task*)>
<!ELEMENT achievement  (tools?,para*)>
<!-- Complex Compound Elements -->
<!ELEMENT title        (name,oldname*,address)>
<!ELEMENT bibliography (heading?,book*,institution*)>
  <!ATTLIST bibliography deprecated   (true|false) "false">
<!ELEMENT job          (institution,period,position?,achievement*)>
  <!ATTLIST job          deprecated   (true|false) "false">
<!ELEMENT school       (institution,period?,degree,achievement*,curriculum*)>
  <!ATTLIST school       deprecated   (true|false) "false">
<!ELEMENT reference    (name,(position,instref?)?,e-mail?,phone?,mobile?)>
  <!ATTLIST reference    deprecated   (true|false) "false">
<!ELEMENT section      (heading,(objective|status*|summary|skill*|job*|school*|interest*|reference*))>
  <!ATTLIST section      deprecated   (true|false) "false">
<!-- Top Level Tag -->
<!ELEMENT resume       (title,bibliography?,section*)>
  <!ATTLIST resume       url          CDATA        #IMPLIED>

Résumé.dtd, the Résumé Document Type Definition

These days, most DTDs have been thrown by the wayside in favor of XML defining itself through an XML Schema. A few years ago, I upgraded my DTD to a Schema to make it more compatible with common XML editors—though I’ve yet to find a descent (Xmplify was a total failure in that respect with no ability to context-complete based on an associated scheme).

<?xml version="1.0" encoding="UTF-8"?>
<!-- Character Entities -->
<!DOCTYPE xs:schema [
  <!ENTITY  nbsp         "&#x00A0;">
  <!ENTITY  chi2         "&#x03C7;&#x00B2;">
  <!ENTITY  endash       "&#x2013;">
  <!ENTITY  eacute       "&#x00E9;">
]>

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           targetNamespace="http://www.timehorse.com"
           xmlns="http://www.timehorse.com"
           elementFormDefault="qualified">

  <!-- Common Attributes -->
  <xs:attribute name="url" type="xs:anyURI"/>
  <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
  <xs:attribute name="spanning" type="xs:boolean" default="false"/>
  <xs:attribute name="name" type="xs:string"/>
  <xs:attribute name="ref" type="xs:string"/>
  <xs:attribute name="level">
    <xs:simpleType>
      <xs:restriction base="xs:string">
        <xs:enumeration value="native"/>
        <xs:enumeration value="fluent"/>
        <xs:enumeration value="conversational"/>
        <xs:enumeration value="good"/>
        <xs:enumeration value="fair"/>
        <xs:enumeration value="poor"/>
      </xs:restriction>
    </xs:simpleType>
  </xs:attribute>

  <!-- Attribute Groupings -->
  <xs:attributeGroup name="depricatedUrlGroup">
    <xs:attribute ref="url"/>
    <xs:attribute ref="deprecated"/>
  </xs:attributeGroup>

  <!-- Simple Types -->
  <!-- Names and Addresses -->
  <xs:element name="name" type="xs:string"/> <!-- nameType -->
  <xs:element name="oldname" type="xs:string"/> <!-- nameType -->
  <xs:element name="street" type="xs:string"/>
  <xs:element name="city" type="xs:string"/> <!-- nameType -->
  <xs:element name="state" type="xs:string"/> <!-- regionType -->
  <xs:element name="province" type="xs:string"/> <!-- regionType -->
  <xs:element name="postal" type="xs:string"/> <!-- postalType -->
  <xs:element name="country" type="xs:string"/>
  <xs:element name="e-mail" type="xs:string"/> <!-- emailType -->
  <xs:element name="phone" type="phoneType"/>
  <xs:element name="mobile" type="phoneType"/>

  <!-- Keys and Headings -->
  <xs:element name="heading" type="xs:string"/>
  <xs:element name="key" type="xs:string"/>
  <xs:element name="value" type="xs:string"/>

  <!-- Date and Time -->
  <xs:element name="year" type="xs:gYear"/>
  <xs:element name="month" type="monthType"/>
  <xs:element name="day" type="dayType"/>
  <xs:element name="hour" type="hourType"/>
  <xs:element name="minute" type="minuteType"/>
  <xs:element name="second" type="secondType"/>

  <!-- Descriptive Elements -->
  <xs:element name="position" type="xs:string"/>
  <xs:element name="product" type="xs:string"/>
  <xs:element name="degree" type="xs:string"/>
  <xs:element name="course" type="xs:string"/>
  <xs:element name="author" type="xs:string"/> <!-- nameType -->
  <xs:element name="publisher" type="xs:string"/>
  <xs:element name="subject" type="xs:string"/>
  <xs:element name="isbn" type="isbnType"/>

  <!-- Deprecated -->
  <xs:element name="deprecated" type="xs:string"/>

  <!-- Complex, Text-Only -->
  <xs:element name="apartment">
    <xs:complexType>
      <xs:simpleContent>
        <xs:extension base="xs:string">
          <xs:attribute name="spellout" type="xs:boolean" default="false"/>
        </xs:extension>
      </xs:simpleContent>
    </xs:complexType>
  </xs:element>

  <!-- Empty Elements (markers) -->
  <xs:element name="author-et-al">
    <xs:complexType>
      <xs:complexContent>
        <xs:restriction base="xs:anyType"/>
      </xs:complexContent>
    </xs:complexType>
  </xs:element>

  <!-- Compound Elements -->
  <!-- Compound Time -->
  <xs:element name="date">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="year" minOccurs="0" maxOccurs="1"/>
        <xs:sequence minOccurs="0">
          <xs:element ref="month" minOccurs="1" maxOccurs="1"/>
          <xs:element ref="day" minOccurs="0" maxOccurs="1"/>
        </xs:sequence>
        <xs:sequence minOccurs="0">
          <xs:element ref="hour" minOccurs="1" maxOccurs="1"/>
          <xs:sequence minOccurs="0">
            <xs:element ref="minute" minOccurs="1" maxOccurs="1"/>
            <xs:element ref="second" minOccurs="0" maxOccurs="1"/>
          </xs:sequence>
        </xs:sequence>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="from">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="date" minOccurs="1" maxOccurs="1"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="to">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="date" minOccurs="1" maxOccurs="1"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="period">
    <xs:complexType>
      <xs:choice>
        <xs:sequence>
          <xs:element ref="from" minOccurs="1" maxOccurs="1"/>
          <xs:element ref="to" minOccurs="1" maxOccurs="1"/>
        </xs:sequence>
        <xs:element ref="date"/>
      </xs:choice>
    </xs:complexType>
  </xs:element>

  <!-- Simple Compound Elements -->
  <xs:element name="address">
    <xs:complexType>
      <xs:sequence>
        <xs:sequence minOccurs="0">
          <xs:element ref="street" minOccurs="1" maxOccurs="1"/>
          <xs:element ref="apartment" minOccurs="0" maxOccurs="1"/>
        </xs:sequence>
        <xs:element ref="city" minOccurs="0" maxOccurs="1"/>
        <xs:choice minOccurs="0">
          <xs:element ref="state"/>
          <xs:element ref="province"/>
        </xs:choice>
        <xs:element ref="postal" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="country" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="e-mail" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="phone" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="mobile" minOccurs="0" maxOccurs="1"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="book">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="name" minOccurs="1" maxOccurs="1"/>
        <xs:sequence minOccurs="0">
          <xs:element ref="author" minOccurs="1" maxOccurs="unbounded"/>
          <xs:element ref="author-et-al" minOccurs="0" maxOccurs="1"/>
        </xs:sequence>
        <xs:sequence minOccurs="0">
          <xs:element ref="address" minOccurs="0" maxOccurs="1"/>
          <xs:element ref="publisher" minOccurs="1" maxOccurs="1"/>
        </xs:sequence>
        <xs:element ref="date" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="subject" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="isbn" minOccurs="0" maxOccurs="1"/>
      </xs:sequence>
      <xs:attribute name="name" type="xs:string"/>
      <xs:attribute name="url" type="xs:anyURI"/>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="institution">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="name" minOccurs="1" maxOccurs="1"/>
        <xs:element ref="oldname" minOccurs="0" maxOccurs="unbounded"/>
        <xs:element ref="address" minOccurs="0" maxOccurs="1"/>
      </xs:sequence>
      <xs:attribute name="name" type="xs:string"/>
      <xs:attribute name="url" type="xs:anyURI"/>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>

  <!-- Reference Elements -->
  <xs:element name="bookref">
    <xs:complexType>
      <xs:attribute name="ref" type="xs:string" use="required"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="instref">
    <xs:complexType>
      <xs:attribute name="ref" type="xs:string" use="required"/>
    </xs:complexType>
  </xs:element>

  <!-- Simple Compound Markups -->
  <xs:element name="buzzword">
    <xs:complexType mixed="true">
      <xs:sequence>
        <xs:element ref="deprecated" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
      <xs:attribute name="url" type="xs:anyURI"/>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="language">
    <xs:complexType>
      <xs:simpleContent>
        <xs:extension base="xs:string">
          <xs:attribute name="level">
            <xs:simpleType>
              <xs:restriction base="xs:string">
                <xs:enumeration value="native"/>
                <xs:enumeration value="fluent"/>
                <xs:enumeration value="conversational"/>
                <xs:enumeration value="good"/>
                <xs:enumeration value="fair"/>
                <xs:enumeration value="poor"/>
              </xs:restriction>
            </xs:simpleType>
          </xs:attribute>
          <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
        </xs:extension>
      </xs:simpleContent>
    </xs:complexType>
  </xs:element>

  <!-- Hashing Elements -->
  <!-- Note: Should I make the Key and Attributes?? -->
  <xs:element name="status">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="key" minOccurs="1" maxOccurs="1"/>
        <xs:element ref="value" minOccurs="1" maxOccurs="1"/>
      </xs:sequence>
      <xs:attribute name="spanning" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="skill">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="key" minOccurs="1" maxOccurs="1"/>
        <xs:choice>
          <xs:element ref="buzzword" minOccurs="0"
                      maxOccurs="unbounded"/>
          <xs:element ref="language" minOccurs="0"
                      maxOccurs="unbounded"/>
        </xs:choice>
      </xs:sequence>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="curriculum">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="key" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="course" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>

  <!-- Compound Text Elements -->
  <xs:element name="tools">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="buzzword" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="task">
    <xs:complexType mixed="true">
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element ref="buzzword"/>
        <xs:element ref="instref"/>
      </xs:choice>
    </xs:complexType>
  </xs:element>
  <xs:element name="interest">
    <xs:complexType mixed="true">
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element ref="buzzword"/>
        <xs:element ref="bookref"/>
        <xs:element ref="instref"/>
        <xs:element ref="deprecated"/>
      </xs:choice>
      <xs:attribute name="url" type="xs:anyURI"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="para">
    <xs:complexType mixed="true">
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element ref="buzzword"/>
        <xs:element ref="product"/>
        <xs:element ref="instref"/>
        <xs:element ref="deprecated"/>
      </xs:choice>
    </xs:complexType>
  </xs:element>

  <!-- Compound Paragraph Elements -->
  <xs:element name="objective">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="para" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="summary">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="para" minOccurs="0" maxOccurs="unbounded"/>
        <xs:element ref="task" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="achievement">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="tools" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="para" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>

  <!-- Complex Compound Elements -->
  <xs:element name="title">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="name" minOccurs="1" maxOccurs="1"/>
        <xs:element ref="oldname" minOccurs="0" maxOccurs="unbounded"/>
        <xs:element ref="address" minOccurs="1" maxOccurs="1"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="bibliography">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="heading" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="book" minOccurs="0" maxOccurs="unbounded"/>
        <xs:element ref="institution" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="job">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="institution" minOccurs="1" maxOccurs="1"/>
        <xs:element ref="period" minOccurs="1" maxOccurs="1"/>
        <xs:element ref="position" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="achievement" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="school">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="institution" minOccurs="1" maxOccurs="1"/>
        <xs:element ref="period" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="degree" minOccurs="1" maxOccurs="1"/>
        <xs:element ref="achievement" minOccurs="0" maxOccurs="unbounded"/>
        <xs:element ref="curriculum" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="reference">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="name" minOccurs="1" maxOccurs="1"/>
        <xs:sequence minOccurs="0" maxOccurs="1">
          <xs:element ref="position" minOccurs="1" maxOccurs="1"/>
          <xs:element ref="instref" minOccurs="0" maxOccurs="1"/>
        </xs:sequence>
        <xs:element ref="e-mail" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="phone" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="mobile" minOccurs="0" maxOccurs="1"/>
      </xs:sequence>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="section">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="heading" minOccurs="1" maxOccurs="1"/>
        <xs:choice>
          <xs:element ref="objective" minOccurs="0" maxOccurs="unbounded"/>
          <xs:element ref="status" maxOccurs="unbounded"/>
          <xs:element ref="summary" minOccurs="0" maxOccurs="unbounded"/>
          <xs:element ref="skill" maxOccurs="unbounded"/>
          <xs:element ref="job" maxOccurs="unbounded"/>
          <xs:element ref="school" maxOccurs="unbounded"/>
          <xs:element ref="interest" maxOccurs="unbounded"/>
          <xs:element ref="reference" maxOccurs="unbounded"/>
        </xs:choice>
      </xs:sequence>
      <xs:attribute name="deprecated" type="xs:boolean" default="false"/>
    </xs:complexType>
  </xs:element>

  <!-- Top Level Tag -->
  <xs:element name="resume">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="title" minOccurs="1" maxOccurs="1"/>
        <xs:element ref="bibliography" minOccurs="0" maxOccurs="1"/>
        <xs:element ref="section" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
      <xs:attribute name="url" type="xs:anyURI"/>
    </xs:complexType>
  </xs:element>

  <!-- Simple Patern-based Types -->
  <xs:simpleType name="phoneType">
    <xs:restriction base="xs:string">
      <xs:pattern
          value="(\+[1-9][0-9]{0,2} )?(\([1-9][0-9]*\) )?[#*1-9][-#*0-9.pw]*(x[#*0-9.pw]+)?"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="postalType">
    <xs:restriction base="xs:string">
      <xs:pattern value="([0-9]{5}(-[0-9]{4})?|[0-9A-Z]{3} [0-9A-Z]{3}|[0-9]+)"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="regionType">
    <xs:restriction base="xs:string">
      <xs:pattern value="[A-Z]([A-Z]|[a-zé]*( [A-Z][a-zé]*)?)"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="emailType">
    <xs:restriction base="xs:string">
      <xs:pattern
          value="[a-z0-9!#$%&amp;'*+/=?^_`{|}~-]+(\.[a-z0-9!#$%&amp;'*+/=?^_`{|}~-]+)*@([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+([A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|asia|jobs|museum)"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="nameType">
    <xs:restriction base="xs:string">
      <xs:pattern value="[A-Z]([a-z']*|\.)( [A-Z]([a-z']*|\.))*"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="monthType">
    <xs:restriction base="xs:string">
      <xs:pattern value="([A-Z][a-z]*|0?[1-9]|1[0-2])*"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="dayType">
    <xs:restriction base="xs:string">
      <xs:pattern value="(0?[1-9]|[12][0-9]|30|31)"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="hourType">
    <xs:restriction base="xs:string">
      <xs:pattern value="([01]?[0-9]|2[0-3])"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="minuteType">
    <xs:restriction base="xs:string">
      <xs:pattern value="(0?[1-9]|[1-5][0-9]|60)"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="secondType">
    <xs:restriction base="xs:string">
      <xs:pattern value="(0?[1-9]|[1-5][0-9]|6[01])"/>
    </xs:restriction>
  </xs:simpleType>
  <xs:simpleType name="isbnType">
    <xs:restriction base="xs:string">
      <xs:pattern value="[- 0-9]{9,}"/>
    </xs:restriction>
  </xs:simpleType>
</xs:schema>

Résumé.xsd, the Résumé XML Schema

One thing of note is the concept of deprecated. Not everything in my Résumé is relevant to today. I like keeping the older elements in the document but when a skill or position becomes no longer relevant to the current job market, it’s marked for deprecation and won’t appear in the final form. How that’s done will be explained in Part 2: Converting to HTML.

With the DTD and XML Schema, I was able to at least verify my Résumé was compliant and ready for publication. And, as always, I am most assuredly available for hire even with my older Résumé.