Hierarchical headers

by jmakeig

HTML

<section id="input">
  <h2>Input</h2>
  <pre>const h = new Hierarchy(null, 
  new Hierarchy('1'),       // leaf
  new Hierarchy('2', 
    new Hierarchy('3', 
      new Hierarchy('4'),   // leaf
      new Hierarchy('5')),  // leaf
    new Hierarchy('6')      // leaf
  ), 
  new Hierarchy('7', 
    new Hierarchy('8', 
      new Hierarchy('9')    // leaf
    ),
    new Hierarchy('A')      // leaf
  ),
  new Hierarchy('B')        // leaf
);</pre>
</section>
<section id="dynamic">
  <h2>Dynamic</h2>
  <div></div>
</section>
<section id="static">
  <h2>Static</h2>
  <div>
    <h3>Row-Oriented</h3>
    <table>
      <tr>
        <th scope="row" colspan="3">1</th>
      </tr>
      <tr>
        <th scope="rowgroup" rowspan="3">2</th>
        <th scope="rowgroup" rowspan="2">3</th>
        <th scope="row">4</th>
      </tr>
      <tr>
        <th scope="row">5</th>
      </tr>
      <tr>
        <th scope="row" colspan="2">6</th>
      </tr>
      <tr>
        <th scope="rowgroup" rowspan="2">7</th>
        <th scope="rowgroup">8</th>
        <th scope="row">9</th>
      </tr>
      <tr>
        <th scope="row" colspan="2">A</th>
      </tr>
      <tr>
        <th scope="row" colspan="3">B</th>
      </tr>
    </table>

    <h3>Column-Oriented</h3>
    <table>
      <thead>
        <tr>
          <th scope="col" rowspan="3">1</th>
          <th scope="colgroup" colspan="3">2</th>
          <th scope="colgroup" colspan="2">7</th>
          <th scope="col" rowspan="3">B</th>
        </tr>
        <tr>
          <th scope="colgroup" colspan="2">3</th>
          <th scope="col" rowspan="2">6</th>
          <th scope="colgroup">8</th>
          <th scope="col" rowspan="2">A</th>
        </tr>
        <tr>
          <th scope="col">4</th>
          <th scope="col">5</th>
          <th scope="col">9</th>
        </tr>
      </thead>
    </table>
  </div>
</section>

CSS

body {
  font-family: Helvetica, sans-serif;
}
table {
  margin: 1em 0;

  border-collapse: collapse;
}
th,
td {
  padding: 0.5em;

  border: solid 1px #ccc;
}
th,
th[scope='colgroup'],
th[scope='col'] {
  text-align: center;
  vertical-align: top;
}
th[scope='rowgroup'],
th[scope='row'] {
  text-align: left;
}
th[scope='colgroup'],
th[scope='rowgroup'] {
  background: orange;
}
th:not([scope]) {
  background: red;
}
pre {
  padding: 0.25em 0.5em;

  border: solid 0.5px green;

  font-family: 'SF Mono', 'Inconsolata', 'Consolas', monospace;
  font-size: 0.65em;
  line-height: 1.55;
}

JavaScript

/**
 * Whether something is iterable, not including `string` instances.
 *
 * @param {*} item
 * @param {boolean} [ignoreStrings = true]
 * @return {boolean}
 */
function isIterable(item, ignoreStrings = true) {
  if (!exists(item)) return false;
  if ('function' === typeof item[Symbol.iterator]) {
    return 'string' !== typeof item || !ignoreStrings;
  }
  return false;
}
/**
 * Whether something is not `undefined` or `null`
 *
 * @param {*} item
 * @return {boolean}
 */
function exists(item) {
  return !('undefined' === typeof item || null === item);
}
/**
 * Whether something doesn’t exist or is *not* an empty `string`
 *
 * @param {*} item
 * @return {boolean}
 */
function isEmpty(item) {
  return !exists(item) || '' === item;
}
/**
 * Guarantees an interable, even if passed a non-iterable,
 * except for `undefined` and `null`, which are returned as-is.
 *
 * @param {*} oneOrMany
 * @return {Iterable|Array|null|undefined}
 */
function toIterable(oneOrMany) {
  if (!exists(oneOrMany)) return oneOrMany;
  if (isIterable(oneOrMany)) return oneOrMany;
  return [oneOrMany];
}

/**
 * Creates a `Node` instance.
 *
 * @param {Node|string|null|undefined} name
 * @return {Node}
 */
function createElement(name) {
  if (name instanceof Node) return name;
  if (isEmpty(name)) return document.createDocumentFragment();
  return document.createElement(String(name));
}

/**
 *
 * @param {Iterable|Node|string|Object} param
 * @param {Node} el
 * @return {Node}
 */
function applyToElement(param, el) {
  if (isIterable(param)) {
    for (const item of param) {
      applyToElement(item, el);
    }
    return el;
  }

  if (param instanceof Node) {
    el.appendChild(param);
    return el;
  }

  if ('string' === typeof param) {
    el.appendChild(document.createTextNode(param));
    return el;
  }

  if (exists(param) && 'object' === typeof param) {
    for (const p of [
      ...Object.getOwnPropertyNames(param),
      ...Object.getOwnPropertySymbols(param)
    ]) {
     ...