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é.

You can have any star you want, as long it is gold

One huge flaw with Google‘s iPhone app for Gmail is that it doesn’t support multiple star types. You are only allowed a gold star, while with the computer-based web interface, you can have many different colour stars, warning, and other alerts.

This is a huge oversight in the GMail app compounded by the fact that the applications like Safari, which allow the user to simulate Desktop browsing crash when you activate the standard web interface and try to select a star colour other than gold.

It used to be you could just open up a desktop browser session to manually set the star level but now even that doesn’t work and still the app can’t handle it.

Stars are a very useful aspect to GMail. With stars you can denote more than just that an email is important, but why it is important. For instance, I like to use the blue stars for coupons. I don’t want to mix coupons in with stars to indicate a SpamGourmet email address is about to expire.

This, in my opinion, is a major flaw to the Apple iOS GMail app and I hope someday they add an option to modify star type because I had to spend two hours today unable get my coveted blue star and ended up having to get out of bed and go to the computer just for this very simple action.

As a software, I know they could do better. As a software engineer, I may just end up doing better. Thanks to the Python interface to Google, I likely will do better.

So unless they want to hire me, bugger you, Google!

Meetup Online: It’s Okay to Zoom

As an Internet Security professional, I have heard some folks expressing dismay over various security issues in the Zoom video conferencing package and the MatterMost chat services. I may do a piece on MatterMost at a later date, but for now I want to focus on Zoom because Zoom is what Meetup is suggesting as one of their preferred video conferencing platforms. (The other, Google Hangouts, is limited to ten people and thus isn’t practical for a number of the meetups I run.)

The thing is, many of the earlier security issues which plagued Zoom at the beginning of the recent surge in online meetings have been solved. Tom’s Hardware wrote a very insightful analysis of these issues in a recent article by Paul Wagenseil, Zoom privacy and security issues: Here’s everything that’s wrong (so far).

Most of the issues covered have already been patched, such as UNC password theft under Microsoft Windows. This was a rather insidious security flaw but fortunately the folks at Zoom stepped up to the plate and patched.

iOS profiling also seems to be fixed. Since I do a lot of my Zoom conferencing, with the National Popular Vote Interstate Compact grassroots coalition, on the iPhone, this has been a great relief. Now, though, I do most of my meetup Zoom conferences on my laptop.

The decrypting of streams at the Zoom servers and re-encrypting them as they go out to the far-end client is at first blush worrisome, but that in part is necessary for folks recording their zoom sessions and though it puts a vulnerability at the level of Zoom staff, one hopes Zoom is careful with whom it employs. But it must be said, nothing I do on Zoom is something I would be embarrassed about were it to leak. I nonetheless want to do everything in my power to make sure it stays secure and I’m happy to hear Zoom is looking into closing this security flaw.

The auto-download for Macintosh is worrisome but again I am happy to say this practice is also ending as it is a backdoor that Zoom can use to allow third party software onto ones Mac. Zoom also has ceased allowing team profiles to share email addresses, though this is not a feature I’m using for any of my Zoom conferences.

As for recording leaking onto the Internet or folks joining your conference uninvited (Zoom Bombing) or war drive scanning Zoom to find your conference, all of these can be solved by user diligence. It’s important to be mindful of who you let into a conference, and don’t let just anyone have access to your recordings. For my Writing Groups, only myself, the account owner, and the persons being reviewed will ever have access to the recordings, and if the reviewed doesn’t need the recordings, we will delete them.

Also, as of this morning, 5 April 2020, at 0:00 UTC, Zoom now requires passwords on all new Zoom events. Thus, even with a Zoom ID scan, you won’t be able to get into the meeting without the password and although the URL can encode the password in an obfuscated way, simply scanning Zoom IDs will not get you into the conferences. And even if you did, I’d still have to approve you. I won’t.

Zoom
The Zoom Logo

Overall, I’m quite happy with Zoom and hope to use it all through Covidapolis. Overall, I give it this Security Engineers line of approval. And please note, I am available for hire if you like what you see!

The danger of Upgrading WordPress

Late last night, just as I completed my post about Tesla trying to scam me, I decided to upgrade WordPress to version 5.4. Normally, this shouldn’t be an issue, but for me, since I run a multisite system, there are extra security issues and directory layout complications that must be taken into account.

The first step was, apparently, to backup my database. Since I’ve never backed up the mysql database before, I felt this seemed like a reasonable approach. I certainly didn’t want to pay JetPack to do it; I’m a genuine code jockey, I can do my own backups. After some digging around, I found mysqldump. Unfortunately, all the instructions on how to use it were incorrect.

After some further poking around, I finally came across the correct syntax. Essentially, the user name and host have to come before the --all-databases command. Also, the host can’t be localhost, it must be the IP for the local host. Unfortunately, I was not able to find a way to get it to prompt for my password which meant I had to type my password in the command line, leaving all there in the open for any history recall to see. Not very secure at all.

mysqldump -h 127.0.0.1 --user=<uid> --password=<pw> --all-databases > mysql.2020.03.31.bak

mysqldump command; note <uid> and <pw> are placeholders for the user name and password; you must replace this with your own values.

Alas, I was not able to find a way to get mysqldump to prompt for a password. I think if I have more time, I may write a python script which builds the command by first prompting for the password. At least that way, the password wouldn’t be stored in the command line history.

The mysqldump command is quite clever. It just stores the list of sql commands that would be required to recreate the databases you have stored. However, the file is rather big and being text, it compresses nicely with bzip2 -9, which is what I did.

Once I did this, I was ready for the main Upgrade. I held my breath and pulled the trigger…

Wordpress
This site is built with WordPress… and a very skilled programmer who has been writing HTML since 1993 and hacking UNIX for even longer.

The install progressed along nicely until it tried to write a file to the wordpress directory. 🤦🏻‍♂️ I logged into my server and sure enough, the permissions on the wordpress directory were 755, which meant the user could add and remove files, but the group and anyone else could not. You see, with my multisite, I try to have all wordpress files with user wp-user and group as www-data, to work with apache. And apache runs all web processes as www-data for both user and group. Thus, when WordPress asked to add a file to its codebase, apache could not write it because the www-data group didn’t have permission, only wp-user did.

find /usr/share/wordpress -type 'd' -print0 | xargs -0 sudo chmod 775

Change all the wordpress directors to allow www-data to add and remove files from them.

Realizing my mistake, I changed the permissions on all directories to be 775 (both wp-user and www-data could add and remove files). Unfortunately, it was too late. Instead, I had no choice but to blow away my current install and replace it with a fresh, new install of WordPress 5.4. At least, that’s what I did on a high level. The details, though, are a bit more complex.

Once I extracted all the wordpress files, I needed to get their ownership to match the settings for my wordpress install. I was able to do this quite easily with the chown command.

sudo chown -R wp-user:www-data wordpress/

Command to set the right file ownership for wordpress.

Next, I set the directories as above. Finally, the files themselves had to have the right permissions. Namely, they should be readable and writable by wp-user and the www-data group, but only readable to others, not writable. Namely, they needed to be set to permission 664.

find wordpress/ -type 'f' -print0 | xargs -0 sudo chmod 664

Change all the wordpress files to allow www-data to modify them.

Next, I had to copy over the active wordpress configuration file. This file is actually fairly spartan as all the active site configurations are actually stored in /etc/wordpress; my wp-config.php actually just scans this directory for configurations. The configurations, in turn, point to directories in /srv/www/wp-content with the site-specific files. I thuns needed to bring that file over to the new install.

cp /usr/share/wordpress/wp-config.php wordpress

Copy the configuration to the new wordpress install.

Next, I wanted to preserve the Languages I had installed. I just copied the entire directory over to the local install.

cp -r /usr/share/wordpress/wp-content/languages wordpress/wp-content/

Copy the Languages directory to the wordpress install.

I also have an upgrades directory that I wanted to preserve.

cp -r /usr/share/wordpress/wp-content/upgrades/ wordpress/wp-content/

Copy the Upgrades directory to the wordpress install.

Finally, I needed to move the links to my shared, dynamic contents that are for all the sites on my server. Specifically, the uploads, themes, and plugins folders all rest in /var/lib/wordpress/wp-content. (Technically, Uploads rests in blog.restonwriters.org site-specific Uploads directory, but that’s something I’ll fix later to conform with the same layout Themes and Plugins use.) Since these are already symbolic links, they can be moved to the new wordpress install directory to replace the defaults.

One caveat however, is the default install for wordpress comes with one plugin and three themes. In order to preserve those, I renamed the default plugins directory to plugins-default, and the default themes directory to themes-default. This was necessary before the symbolic links could be moved since those directories were in the way.

mv /usr/share/wordpress/wp-content/uploads wordpress/wp-content
mv /usr/share/wordpress/wp-content/plugins wordpress/wp-content
mv /usr/share/wordpress/wp-content/themes wordpress/wp-content

Move the symbolic links to the plugins, themes, and uploads directories.

Finally, the apache permissions file needed to be moved as it was also a link, pointing to /etc/wordpress/htaccess. I store the file there because it makes it easier to maintain in case I accidentally bow the .htaccess file away.

cp /usr/share/wordpress/.htaccess wordpress

Move the symbolic link to the .htaccess file.

Once all this is done, it’s a good idea to run the chown and chmod commands from above on the wordpress install directory once more to make sure the copied files and moved links are also properly attributed.

Finally, it was time to perform the brain transplant and move my staged wordpress install to the active /usr/share/ directory. I moved the current install to a temporary directory and then moved my staging install to the /usr/share/ directory to replace it.

mv /usr/share/wordpress /usr/share/wordpress-old
mv wordpress /usr/share/wordpress

Replace the installed wordpress with the new version.

Once all this was done, I was able to get to my web page, and wordpress prompted me to upgrade my database. Once this was done my sites were back online. In total, this site and its sister sites were down for a total of about forty-five minutes. It was a long day yesterday and I was exhausted but I did get it done and you can now see the results.

I hope you enjoyed my story about hacking UNIX. Please note, I am available for hire if you like what you see!

I Am Irate

Google ate me email

From about 2020-03-23T14:30:00Z (10:30 am, Monday) to about 2020-03-23T23:30:00Z (7:30 pm, Monday), Google was redirecting all my email and either bouncing it or deleting it.

I Am Irate
Too angry for words!

Let me repeat, google deleted or bounced my email for Nine Hours, as a part of the setup of my setup for a paid Google Apps account. The setup for these accounts are a bit weird. They require you to create a new google entity with your own company URL. Fortunately, I have multiple domains I own and maintain, including this one, TimeHorse.com.

I probably should have used my writing group domain, RestonWriters.org. After all, the whole reason I wanted to get a paid Google account is because Meetup was moving to Online-Only meetings, following the outbreak of SARS-COV-2, and I needed a tool that allowed for video conferencing.

Skype was a non-starter. For one thing, it’s great for person-to-person communications, but for group chats, it has this annoying habit of muting everyone except the current speaker and you have to wait until that speaker stops to get a word in edgewise. My understanding is WhatsApp has the same problem.

Meetup actually suggested using Google Hangouts or Zoom. I happen to like Zoom. I use it for my regular NPVIC Grassroots strategy meetings and for Toastmasters and it’s always worked great. Zoom does support up to a hundred participants, both free and Pro. The only problem is, each of those Zoom sessions are either limited to the free forty-minute block or are using an up-to-24-hour Zoom Pro Account. Since most of my Meetups are at least an hour, breaking meeting up into forty-minute chunks would be tedious. And, at $14.99 a month, the professional account is well out of my price range.

Just before the first week of Virtual meetings began, my writing colleagues and I, including Elizabeth Hayes, who runs The Hourlings, tested both free Zoom and Google Hangout. Despite being limited to ten people, we decided on Google Hangout and I mapped it to our official Virtual Meeting URL.

Ten people worked fine for Reston Writers and for the Saturday Morning Review. The Saturday Morning Review actually worked out quite well because Meetup, despite suggesting we move to a virtual platform, still won’t let you delete the venue from your event and mark it as virtual, which, when editing events can cause some confusion. But when the Library cancelled all our events, I just deleted them all from the Meetup Calendar, and recreated them with no Venue and just announced them as occurring in Cyberspace.

Stay with me folks, I’m getting to the email…

As Sunday approached, I new ten participants wouldn’t be enough. Google Hangout would be fine for Bewie Bevy of Brainy Books and Saturday Morning Review, and likely The Science Book Club, as they all usually have fewer than ten participants for each meeting. The Hourlings, on the other hand, often had twelve, and sometimes as many as sixteen!

I new Zoom was $14.99 a month, but I read that Google App accounts could up the number of participants to twenty-five. Unfortunately my 2TB Google Drive account didn’t qualify. I had to get a Google Apps account.

And that’s where my troubles began.

At first, I could only sign up for the $12 per month account, even though I’d read it could be had for $6. Since the setup has a fortnight trial period, I didn’t worry about the financial discrepancy. I set up the account with my business email address for TimeHorse, LLC. I associated it with with that email, it connected to my Gandi Registrar, and my account was ready to go. I created a Google Hangout and assigned it to the Virtual Meeting URL, hoping it would allow twenty-five. The plan was to use it with the Hourlings to verify that fact.

It failed! We still could only get ten people into the meetup despite it being a paid account.

Unfortunately, since Monday I’ve been on Weather and Safety Leave from work because my Telework agreement was revoked, but that’s a story for another day as this post is long as it is! However, it did allow me to speak to Google and they suggested I try Google Meet. Meet was included with all Google App paid accounts, and it would allow for up to a hundred people and could be as long as I needed. Also, I could downgrade to the $6 per month account and I would still be able to use it. I thus downgraded.

We tried it with Reston Writers Review and it worked wonderfully. We had up to twelve connections simultaneously! But I’m getting ahead of myself.

At around 10:30 am, that Monday, after chatting with Google, I was examining my Google Apps account more closely. It was telling me I had one last step I needed to complete: integrate me email with Gmail.

Stop
Stop, do not pass Go. You’re done!

That’s when my troubles began. You see, what this innocuous, turn-key step says it does is it says it sets up GMail for your company. What it actually does is obliterate all the MX Records (email routing information) of your DNS (Internet routing information) Zone File (routing configuration file) on Gandi and replace it with MX Records that point to Google. The setup wizard doesn’t actually tell you this and I’m totally oblivious.

At current writing, I have 188 forwarded email addresses set up on Gandi with their MX Servers. One of those is my business email, the one Google took over and is my Google Apps login. That’s the email google set up as the official email address used in GMail. Once the GMail setup goes through and I send an email from the GMail interface to my personal email address on the timehorse.com domain.

It never arrives. All day long, I watch my email and, strangely, nothing arrives after 10:30 in the morning. I refresh and refresh, and it’s still nothing. Where have all my emails gone?

It’s not until I’m setting up for Reston Writers that I decide to contact Google about this. I’m crazy-busy setting up the Google Meet, opening up the pieces we’d be reviewing on my computer, and, simultaneously, chatting with Google, trying to figure out why I’m not receiving any email.

Eventually, Google Tech Support starts talking about MX Records and a chill runs down my spine. As you probably gathered by now, I am well versed in DNS records and Zone File manipulation. I even have a Python script which updates my DNS A Record when the IP Address for this server changes.

With trepidation, I logged into my Gandi account and saw the damage. Google had modified my Zone file and added a bunch of strange new MX Records pointing to Google. They had nuked all my Gandi Email forward since they’d redirected all email traffic to google. As google only had one account registered on the domain, timehorse.com, namely my business email address, every other email address I possessed was either being deleted or bounced by google!

Fortunately, Gandi’s Email Forwarding page provides a warning when the Zone file doesn’t point to their email server, listing the correct MX Record settings to use Gandi as the mail hosting server. I quickly commented out the Google MX Records and pasted in the Gandi MX Records around 7:30 pm, in the middle of my Reston Writers meeting.

Needless to say, I was miffed that I could not give my full attention to my writers during our weekly writing gettogether. But it’s good I finally did figure out the disastrous actions committed by Google after only nine hours, and not a day or more.

I may never know what was contained in those nine hours of lost emails. I suppose there is one blessing, though. I get too much email already and still have dozens of unread messages I’m desperately trying to catch up on. One Covidapolis, novel-length email after another from every business under the sun. STFU companies, you’re all doing the same thing and I don’t like reading the same message again, and again, and again! You have a plan, that’s all I need to know!

Maybe Google was doing me a favor?

In the end, I was able to solve the problem because I got skills and I’m available for hire!

Python INI File Parser

A flexible INI parser can be built in Python. Although tools to do this already exist, it’s important to understand how this might be done in the general case to learn new python techniques in general.

The idea is to parse rudimentary INI files of the form:

Field 1: Foo
Field 2: Bar

Flat INI File Format

In this example we will ignore INI groupings and stick with just the INI name-value pairs.

The challenge is in writing a system that maps from INI names to variable names. The application can also be built with flexible punctuation. For instance:

class ini_parser:
    ini = 'test.ini'
    ini_sep = ':'
    ini_mapping = {
        'Field 1': 'field_1',
        'Field 2': 'field_2'
    }

Code to implement a generic INI parser in Python.

In this example, the INI uses names with spaces. Since Python variable names cannot have spaces, the mapping is necessary to allow for dissimilar local variable names relative to the name used in the INI file.

Once we have this boilerplate, we can build an iniparser around it:

def parse_ini(self):
    with open(self.ini) as file_obj:
        for line in file_obj:
            if self.ini_sep in line:
                # Split and trim whitespace around fields
                name, value = tuple([x.strip() for x in l.split(self.ini_sep, 1)])
                setattr(self, self.ini_mapping[name], value)

ini_parser.parse_ini()

In the above example, self.field_1 would get the value 'Foo', and self.field_2 would get the value 'Bar'. The code only looks at the first colon in the line of an INI file, so that INI names can’t contain colons (:), but any subsequent colon (:) is considered part of the value.

Once software makes changes to an applications settings, it’s important to write those changes back to the INI file:

def write_ini(self):
    with open(self.ini, ‘w’) as file_obj:
        for ini_name, local_name in self.ini_mapping.items():
            value = getattr(self, local_name)
            file_obj.write(ini_name + self.ini_sep + ‘ ‘ + value)

ini_parser.write_ini()

Note, for this simple parser, all values will be returned as strings. One could add a type to the ini_parser.ini_mappings to also provide a type for the variable to be assigned, casting value to it. Alternatively, if the variable to be set already exists and has a type, the type command can be used to get that type and then cast value to that type.

Further, if a list type is included in the INI, for instance a series of comma-separated values, then a special list handler will have to be written which will split and strip each value by commas (,) and then join them again with commas (,) when writing. Because lists can be single-element, it’s impossible for software to know by simply searching for commas (,) whether an INI value is a list or not. This is why the type must be included in some way internally to parse list values properly. The code to do this is left as an exercise for the reader.

I hope you enjoyed this little tutorial. Please note, I am available for hire if you like what you see!

Truth in Advertising

As a software engineer, I pride myself in honesty. If I say to you, the 3n+1 problem has yet to be solved for any number not leading to 1 \rightarrow 4 \rightarrow 2 \rightarrow 1 in Lothar, it’s because I did my research, studied the Collatz Conjecture, and developed the game around the unsolved mathematical challenge to prove it wrong by entering an arbitrarily long positive integer and see where it leads. As yet, no-one has found a solution, but I welcome you to download my app and try.

So it drives me crazy to see adverts on Facebook which purport to give an interesting challenge, like what levers to pull to rescue your little guy, only to be delivered to some stupid knockoff game that has absolutely nothing to do with it.

False Advertising on Facebook
This is an example of false advertising on Facebook. Although this is not the advert for the game listed in the article, the gist is similar to pulling some levers to rescue a bloke, which had nothing to do with the actual gameplay.

Now, if you’re anything like me, you want to play a game just like this. Pull the levers and try to come up with a solution where the little elf is saved and the goblin is toasted by laval. Especially when you’re told it’s really hard to solve. I so want to play that game. So I click on it, and then I get this!

Farmville knockoff that is a total waste of time
This is the actual game the advert leads to. As can be seen, it’s just a glorified Farmville Knockoff with a few more bells and whistles with modern-gaming quests but mainly not enough resources for game play because you need to pay money for energy or feed grain to get anywhere.

Farmville!? Are they bleeping kidding me!? I don’t want to spend time planting crops, reaping products, hammering stones, just to do it over and over again with constant Grain shortages and energy shortages. This is not my idea of fun. This is total balderdash and a complete waste of my time. I wanted to rescue elves, not visit some stupid plantation called Taonga! I want a challenge that takes at most a half-hour, like Sudoku, or rescue elves by pulling levers, … or by guessing numbers in Lothar.

The problem with false advertising on Facebook has become so pervasive that there’s actually a petition to ban it. Unfortunately, most false advertising suits hang on financial loss, like you paid money thinking you’d get one thing but in the end you got another. In this case, with Freemium games monetization, the cost to the victim isn’t so much money as it is time wasted trying to play the game in the hopes of getting to the elf-rescue level only to finally realize it’ll never come. If the player does spend money to enhance game play, then there may be a suit, but by that time it’s likely the user already accepted the alternate game play and was just falling into the Freemium trap.

So the next time you see an advert on social media that claims to allow you to rescue the elf, or the princess, or the adventurer by pulling some levers to redirect lava, or water, or slime, just scroll past. Just scroll past. And play some Lothar because I wrote it and I’m available for hire.

Coloring through CSS

Someone mentioned some of the links were hard to see on my site because I was only changing the text color and not the link attributes. Now, this should be a lot easier to do because instead of coloring my text blocks manually, I will be coloring them with Cascading Style Sheets (CSS).

Using the palet of CSS, I came up with a number of potential color schemes.

For Coder posts, I’m using this theme.

For Musical posts, I’m using this theme.

For Gaming posts, I’m using this theme.

For Speech Making posts, I’m using this theme.

For Author posts, I’m using this theme.

For Vegetarianism and Diet posts, I’m using this theme.

For Thespian posts, I’m using this theme.

For Science posts, I’m using this theme.

For Polyglot posts, I’m using this theme.

For Electric Car posts, I’m using this theme.

For Foreign Travel posts, I’m using this theme.

For Doctor Who posts, I’m using this theme.

For Aspiration Aviation posts, I’m using this theme.

For Equal Rights Amendment posts, I’m using this theme.

For Cosplay posts, I’m using this theme.

For NPVIC posts, I’m using this theme.

But what am I using this theme for?

With this template, I can check all the themes before they’re deployed to see how they look, at least in terms of text and hyperlinks. I will add CSS for Spoilers and other special text like #CO2Fre and #CO2Fre1, but for now this is what I’m working with. And I’m available for hire.

Double Factorial

Most mathematically-inclined folks know what a factorial is. The simplest, recursive definition is given by:

f(0) = 1
f(n) = n * f(n-1) = n!

Figure 1: Definition of Factorial

For all n in the set of Natural Numbers, ℕ.

In the set of real numbers, ℝ, this can be extended to the Gamma function, Γ, which has the form:

\Gamma(x) = \int_0^\infty t^{x-1} \mathrm{e}^{-t} \mathrm{d}t
\forall n \in \mathbb{N}: \Gamma(n) = (n-1)!

Figure 2: Definition of Gamma

Both Figure 1 and Figure 2 are typically well known definitions. But what’s less known is the double, triple, and even quadruple factorial:

n!! = n (n-2)!!
n!!! = n (n-3)!!!
n!!!! = n (n-4)!!!!
\vdots

Figure 3: Higher Level Factorial Functions

The question is, how do you extend this from Natural Numbers to the general case with Real Numbers, like with the Gamma function from Figure 2?

Fortunately, Python’s math library has math.gamma(n) to compute the Gamma function on a given number. Thus, rather than the recursive definition in Figure 1, we can directly define factorial in terms of Gamma:

def factorial(x):
    return math.gamma(x+1)

Figure 4: Definition of factorial(x)

The question is, can the Double and Higher Factorials be defined in terms of gamma?

The Pochhammer Function

The Pochhammer function is defined in therms of Gamma. Specifically:

(x)_n = \frac{\Gamma (x+n)}{\Gamma (x)} = x(x+1) \cdots (x+n-1)

Figure 5: The Pochhammer Function

The corresponding Python looks like this:

def Pochhammer(n, k):
    return math.gamma(k+m)/math.gamma(n)

Figure 6: Definition of Pochhammer

The Pochhammer brings us closer to our ideal continuous multi-factorial method. For instance, we could assume a version of Factorial over the Rational Numbers, ℚ:

def Rational_Multifactorial(n, numerator, denominator):
    return pow(numerator/denominator, n*denominator/numerator) * Pochhammer(1, n*denominator/numerator)

Figure 7: Definition of Rational_Multifactorial

Unfortunately, that doesn’t give us a Real solution, ℝ. Instead, if we’re to ask WolframAlpha to work it out for us, we end up with something completely different:

x!! = 2^{\frac{x}{2}} (c_2(-1)^x+c_1) \Gamma (1+\frac{x}{2})
where,
c_1 = \frac{1}{2} + \frac{1}{\sqrt{2 \pi}}
c_2 = \frac{1}{2} - \frac{1}{\sqrt{2 \pi}}

Figure 8: WolframAlpha definition of Double Factorial over the Real

The way that was calculated was the simple recursion in Figure 3 was entered and WolframAlpha was told to find an arithmetic solution. The answer was expressed in terms of the constants c1 and c2, which I then solved for knowing a few fixed values for the multiplication.

The result in Python, becomes:

def double_factorial(x):
    c1 = 0.5 + 1/math.sqrt(2*math.pi)
    c2 = 0.5 - 1/math.sqrt(2*math.pi)

    return pow(2, x/2) * (c2*pow(-1, x)+c1)*math.gamma(x/2+1)

Figure 9: Definition of double_factorial

What’s more, the rational solution used Pochhammer, but the real solution used only Gamma. I was able to use WolframAlpha to compute analytic functions for Triple and Quadruple factorials, with their three and four constants, respectively, but there didn’t seem to be any pattern to the calculations and the solutions move on to using Sine and Cosine. And none of them used Pochhammer. Yet the Rational solution in Figure 7 should work for all levels of Factorial.

I’ve writing up all my functions in Factorial.py, which you can find on my Subversion repository and I’m currently available for hire.