diff --git a/.eslintrc.json b/.eslintrc.json
index db4da79..c368dda 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -8,7 +8,7 @@
     },
     "extends": ["eslint:recommended", "plugin:prettier/recommended"],
     "parserOptions": {
-        "ecmaVersion": 8
+        "ecmaVersion": "latest"
     },
     "rules": {
         "no-unused-vars": "off",
diff --git a/script/aria.js b/script/aria.js
index 67d6a62..68ad486 100644
--- a/script/aria.js
+++ b/script/aria.js
@@ -1,3 +1,16 @@
+/**
+ * Clones a node but strips IDs
+ * @param {HTMLElement} node - an element node
+ * @returns {HTMLElement} - cloned node without IDs
+ */
+function cloneWithoutIds(node) {
+    const clone = node.cloneNode(true);
+    for (const elementWithId of clone.querySelectorAll("[id]")) {
+        elementWithId.removeAttribute("id");
+    }
+    return clone;
+}
+
 /**
  * roleInfo is structured like this:
  *
@@ -7,746 +20,395 @@
  * localprops: local properties and states
  */
 
-var roleInfo = {};
+const roleInfo = {};
 
-function ariaAttributeReferences() {
-    var propList = {};
-    var globalSP = [];
+/**
+ * Populates propList for given sdef/pdef
+ * @param {Object} propList -
+ * @param {HTMLElement} item - from nodeList.forEach
+ */
+const populatePropList = function (propList, item) {
+    const type = item.localName === "pdef" ? "property" : "state";
+    const content = item.innerHTML;
+    const title = item.getAttribute("title") || content;
+    const dRef = item.nextElementSibling;
+    const desc = cloneWithoutIds(dRef.firstElementChild).innerHTML;
+    propList[title] = {
+        is: type,
+        title: title,
+        name: content,
+        desc: desc,
+        roles: [],
+    };
+};
 
-    var skipIndex = 0;
-    var myURL = document.URL;
-    if (myURL.match(/\?fast/)) {
-        skipIndex = 1;
+/**
+ * Populates globalSP for given sdef/pdef
+ * @param {Object} propList -
+ * @param {Object} globalSP -
+ * @param {HTMLElement} item - from nodeList.forEach
+ */
+const populateGlobalSP = function (propList, globalSP, item) {
+    const title = item.getAttribute("title") || item.innerHTML;
+    const container = item.parentElement;
+    const itemEntry = propList[title];
+
+    const applicabilityText = container.querySelector(
+        "." + itemEntry.is + "-applicability"
+    ).innerText;
+    const isDefault = applicabilityText === "All elements of the base markup";
+    const isProhibited =
+        applicabilityText ===
+        "All elements of the base markup except for some roles or elements that prohibit its use";
+    const isDeprecated =
+        applicabilityText === "Use as a global deprecated in ARIA 1.2";
+    // NOTE: the only other value for applicabilityText appears to be "Placeholder"
+    if (isDefault || isProhibited || isDeprecated) {
+        globalSP.push(
+            Object.assign(itemEntry, {
+                prohibited: isProhibited,
+                deprecated: isDeprecated,
+            })
+        );
     }
+};
 
-    // process the document before anything else is done
-    // first get the properties
-    Array.prototype.slice
-        .call(document.querySelectorAll("pdef, sdef"))
-        .forEach(function (item) {
-            var type = item.localName === "pdef" ? "property" : "state";
-            var container = item.parentNode;
-            var content = item.innerHTML;
-            var sp = document.createElement("span");
-            var title = item.getAttribute("title");
-            if (!title) {
-                title = content;
-            }
-            sp.className = type + "-name";
-            sp.title = title;
-            sp.innerHTML =
-                "<code>" +
-                content +
-                '</code> <span class="type-indicator">' +
-                type +
-                "</span>";
-            sp.setAttribute("aria-describedby", "desc-" + title);
-            var dRef = item.nextElementSibling;
-            var desc = cloneWithoutIds(dRef.firstElementChild).innerHTML;
-            dRef.id = "desc-" + title;
-            dRef.setAttribute("role", "definition");
-            var heading = document.createElement("h4");
-            heading.appendChild(sp);
-            container.replaceChild(heading, item);
-            // add this item to the index
-            propList[title] = {
-                is: type,
-                title: title,
-                name: content,
-                desc: desc,
-                roles: [],
-            };
-            var abstract = container.querySelector(
-                "." + type + "-applicability"
-            );
-            if (
-                (abstract.textContent || abstract.innerText) ===
-                "All elements of the base markup"
-            ) {
-                globalSP.push({
-                    is: type,
-                    title: title,
-                    name: content,
-                    desc: desc,
-                    prohibited: false,
-                    deprecated: false,
-                });
-            } else if (
-                (abstract.textContent || abstract.innerText) ===
-                "All elements of the base markup except for some roles or elements that prohibit its use"
-            ) {
-                globalSP.push({
-                    is: type,
-                    title: title,
-                    name: content,
-                    desc: desc,
-                    prohibited: true,
-                    deprecated: false,
-                });
-            } else if (
-                (abstract.textContent || abstract.innerText) ===
-                "Use as a global deprecated in ARIA 1.2"
-            ) {
-                globalSP.push({
-                    is: type,
-                    title: title,
-                    name: content,
-                    desc: desc,
-                    prohibited: false,
-                    deprecated: true,
-                });
-            }
-            // the rdef is gone.  if we are in a div, convert that div to a section
-
-            if (container.nodeName.toLowerCase() == "div") {
-                // change the enclosing DIV to a section with notoc
-                var sec = document.createElement("section");
-                Array.prototype.slice
-                    .call(container.attributes)
-                    .forEach(function (attr) {
-                        sec.setAttribute(attr.name, attr.value);
-                    });
-                sec.classList.add("notoc");
-                var theContents = container.innerHTML;
-                sec.innerHTML = theContents;
-                container.parentNode.replaceChild(sec, container);
-            }
+/**
+ *
+ * @param {HTMLElement} container - parent of sdef or pdef or rdef
+ */
+const rewriteDefContainer = (container) => {
+    // if we are in a div, convert that div to a section
+    // TODO:
+    // a) seems to be always the case.
+    // b) Why don't we author the spec this way?
+    if (container.nodeName.toLowerCase() == "div") {
+        // change the enclosing DIV to a section with notoc
+        const sec = document.createElement("section");
+        [...container.attributes].forEach(function (attr) {
+            sec.setAttribute(attr.name, attr.value);
         });
+        sec.classList.add("notoc");
+        const theContents = container.innerHTML;
+        sec.innerHTML = theContents;
+        container.parentNode.replaceChild(sec, container);
+    }
+};
 
-    if (!skipIndex) {
-        // we have all the properties and states - spit out the
-        // index
-        var propIndex = "";
-        var sortedList = [];
-
-        Object.keys(propList).forEach(function (key) {
-            sortedList.push(key);
-        });
-        sortedList = sortedList.sort();
-
-        for (var i = 0; i < sortedList.length; i++) {
-            var item = propList[sortedList[i]];
-            propIndex +=
-                '<dt><a href="#' +
-                item.title +
-                '" class="' +
-                item.is +
-                '-reference">' +
-                item.name +
-                "</a></dt>\n";
-            propIndex += "<dd>" + item.desc + "</dd>\n";
-        }
-        var node = document.getElementById("index_state_prop");
-        var parentNode = node.parentNode;
-        var l = document.createElement("dl");
-        l.id = "index_state_prop";
-        l.className = "compact";
-        l.innerHTML = propIndex;
-        parentNode.replaceChild(l, node);
-
-        var globalSPIndex = "";
-        sortedList = globalSP.sort(function (a, b) {
-            return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
-        });
-        for (i = 0; i < sortedList.length; i++) {
-            var lItem = sortedList[i];
-            globalSPIndex += "<li>";
-            if (lItem.is === "state") {
-                globalSPIndex +=
-                    "<sref " +
-                    (lItem.prohibited ? "data-prohibited " : "") +
-                    (lItem.deprecated ? "data-deprecated " : "") +
-                    'title="' +
-                    lItem.name +
-                    '">' +
-                    lItem.name +
-                    " (state)</sref>";
-            } else {
-                globalSPIndex +=
-                    "<pref " +
-                    (lItem.prohibited ? "data-prohibited " : "") +
-                    (lItem.deprecated ? "data-deprecated " : "") +
-                    ">" +
-                    lItem.name +
-                    "</pref>";
-            }
-            if (lItem.prohibited) {
-                globalSPIndex += " (Except where prohibited)";
-            }
-            if (lItem.deprecated) {
-                globalSPIndex += " (Global use deprecated in ARIA 1.2)";
-            }
-            globalSPIndex += "</li>\n";
-        }
-        parentNode = document.querySelector("#global_states");
-        if (parentNode) {
-            node = parentNode.querySelector(".placeholder");
-            if (node) {
-                l = document.createElement("ul");
-                l.innerHTML = globalSPIndex;
-                parentNode.replaceChild(l, node);
-            }
-        }
-        // there is only one role that uses the global properties
-        parentNode = document.querySelector(
-            "#roletype td.role-properties span.placeholder"
-        );
-        if (parentNode) {
-            node = parentNode.parentNode;
-            if (
-                (parentNode.textContent || parentNode.innerText) ===
-                "Placeholder for global states and properties"
-            ) {
-                l = document.createElement("ul");
-                l.innerHTML = globalSPIndex;
-                node.replaceChild(l, parentNode);
-            }
-        }
+/**
+ *
+ * @param {HTMLElement} item - rdef element
+ */
+const rewriteRdef = function (item) {
+    // TODO: merge with generateHTMLStatesAndProperties() but that creates different HTML
+    const content = item.innerHTML;
+    let title = item.getAttribute("title") || content;
+    let type = "role";
+    const abstract = item.parentNode.querySelectorAll(".role-abstract"); //TODO: maybe #105
+    if (abstract.innerText === "True") {
+        type = "abstract role";
     }
+    const dRef = item.nextElementSibling;
+    dRef.id = "desc-" + title;
+    dRef.setAttribute("role", "definition");
+    item.outerHTML = `<h4 class="role-name" title="${title}" aria-describedby="desc-${title}"><code>${content}</code> <span class="type-indicator">${type}</span>`;
+};
 
-    // what about roles?
-    //
-    // we need to do a few things here:
-    //   1. expand the rdef elements.
-    //   2. accumulate the roles into a table for the indices
-    //   3. grab the parent role reference so we can build up the tree
-    //   4. grab any local states and properties so we can hand those down to the children
-    //
+/**
+ * Replaces sdef/pdef with desired HTML
+ * @param {Object} propList -
+ * @param {HTMLElement} item - sdef or pdef, from nodeList.forEach
+ */
+const generateHTMLStatesAndProperties = function (propList, item) {
+    const title = item.getAttribute("title") || item.innerHTML;
+    const itemEntry = propList[title];
+    const dRef = item.nextElementSibling;
+    dRef.id = "desc-" + title; // TODO: too much of a side-effect?
+    dRef.setAttribute("role", "definition"); // TODO: ditto?
+    // Replace pdef/sdef with HTML
+    item.outerHTML = `<h4><span class="${itemEntry.is}-name" title="${itemEntry.title}" aria-describedby="desc-${itemEntry.title}"><code>${itemEntry.name}</code> <span class="type-indicator">${itemEntry.is}</span></span></h4>`;
+};
 
-    var subRoles = [];
-    var roleIndex = "";
-    var fromAuthor = "";
-    var fromHeading = "";
-    var fromContent = "";
-    var fromProhibited = "";
+/**
+ * Generate index of states and properties
+ * @param {Object} propList
+ */
+const generateIndexStatesAndProperties = (propList) => {
+    const indexStatePropPlaceholder =
+        document.getElementById("index_state_prop");
+    const indexStatePropContent = Object.values(propList)
+        .map(
+            (item) =>
+                `<dt><a href="#${item.title}" class="${item.is}-reference">${item.name}</a></dt>\n<dd>${item.desc}</dd>\n`
+        )
+        .join("");
+    indexStatePropPlaceholder.outerHTML = `<dl id="index_state_prop" class="compact">${indexStatePropContent}</dl>`;
+};
 
-    Array.prototype.slice
-        .call(document.querySelectorAll("rdef"))
-        .forEach(function (item) {
-            var container = item.parentNode;
-            var content = item.innerHTML;
-            var sp = document.createElement("h4");
-            var title = item.getAttribute("title");
-            if (!title) {
-                title = content;
-            }
+/**
+ * Generate index of global states and properties
+ * @param {Object} globalSP
+ */
+const generateIndexGlobalStatesAndProperties = (globalSP) => {
+    const globalStatesPropertiesContent = globalSP
+        .map((item) => {
+            // TODO: This is the only use of globalSP - why does it not just consist of the markup we create here in this loop?
+            const isState = item.is === "state";
+            const tagName = isState ? "sref" : "pref";
+            return `<li><${tagName} ${
+                item.prohibited ? "data-prohibited " : ""
+            }${item.deprecated ? "data-deprecated " : ""}${
+                isState ? `title="${item.name}"` : ""
+            }>${item.name}${isState ? " (state)" : ""}</${tagName}>${
+                // TODO: consider moving "(state)" out of sref/pref tag; then maybe remove title attr for sref (after checking resolveReferences interference)
+                // TODO: cf. extractStatesProperties() and populateRoleInfoPropList() which have extra logic for title set here)
+
+                item.prohibited ? " (Except where prohibited)" : ""
+            }${
+                item.deprecated ? " (Global use deprecated in ARIA 1.2)" : ""
+            }</li>\n`;
+        })
+        .join("");
+    const globalStatesPropertiesPlaceholder = document.querySelector(
+        "#global_states .placeholder"
+    );
+    globalStatesPropertiesPlaceholder.outerHTML = `<ul>${globalStatesPropertiesContent}</ul>`;
 
-            var pnID = title;
-            container.id = pnID;
-            sp.className = "role-name";
-            sp.title = title;
-            // is this a role or an abstract role
-            var type = "role";
-            var isAbstract = false;
-            var abstract = container.querySelectorAll(".role-abstract");
-            if (abstract.innerText === "True") {
-                type = "abstract role";
-                isAbstract = true;
-            }
-            sp.innerHTML =
-                "<code>" +
-                content +
-                '</code> <span class="type-indicator">' +
-                type +
-                "</span>";
-            // sp.id = title;
-            sp.setAttribute("aria-describedby", "desc-" + title);
-            var dRef = item.nextElementSibling;
-            var desc = cloneWithoutIds(dRef.firstElementChild).innerHTML;
-            dRef.id = "desc-" + title;
-            dRef.setAttribute("role", "definition");
-            container.replaceChild(sp, item);
-            roleIndex +=
-                '<dt><a href="#' +
-                pnID +
-                '" class="role-reference"><code>' +
-                content +
-                "</code>" +
-                (isAbstract ? " (abstract role) " : "") +
-                "</a></dt>\n";
-            roleIndex += "<dd>" + desc + "</dd>\n";
-            // grab info about this role
-            // do we have a parent class?  if so, put us in that parents list
-            var node = Array.prototype.slice.call(
-                container.querySelectorAll(".role-parent rref")
-            );
-            // s will hold the name of the parent role if any
-            var s = null;
-            var parentRoles = [];
-            if (node.length) {
-                node.forEach(function (roleref) {
-                    s = roleref.textContent || roleref.innerText;
-
-                    if (!subRoles[s]) {
-                        subRoles.push(s);
-                        subRoles[s] = [];
-                    }
-                    subRoles[s].push(title);
-                    parentRoles.push(s);
-                });
-            }
-            // are there supported states / properties in this role?
-            var attrs = [];
-            Array.prototype.slice
-                .call(
-                    container.querySelectorAll(
-                        ".role-properties, .role-required-properties, .role-disallowed"
-                    )
-                )
-                .forEach(function (node) {
-                    if (
-                        node &&
-                        ((node.textContent && node.textContent.length !== 1) ||
-                            (node.innerText && node.innerText.length !== 1))
-                    ) {
-                        // looks like we do
-                        Array.prototype.slice
-                            .call(node.querySelectorAll("pref,sref"))
-                            .forEach(function (item) {
-                                var name = item.getAttribute("title");
-                                if (!name) {
-                                    name = item.textContent || item.innerText;
-                                }
-                                var type =
-                                    item.localName === "pref"
-                                        ? "property"
-                                        : "state";
-                                var req = node.classList.contains(
-                                    "role-required-properties"
-                                );
-                                var dis =
-                                    node.classList.contains("role-disallowed");
-                                var dep = item.hasAttribute("data-deprecated");
-                                attrs.push({
-                                    is: type,
-                                    name: name,
-                                    required: req,
-                                    disallowed: dis,
-                                    deprecated: dep,
-                                });
-
-                                // remember that the state or property is
-                                // referenced by this role
-                                propList[name].roles.push(title);
-                            });
-                    }
-                });
-            roleInfo[title] = {
-                name: title,
-                fragID: pnID,
-                parentRoles: parentRoles,
-                localprops: attrs,
-            };
-
-            // is there a namefrom indication?  If so, add this one to
-            // the list
-            if (!isAbstract) {
-                Array.prototype.slice
-                    .call(container.querySelectorAll(".role-namefrom"))
-                    .forEach(function (node) {
-                        var reqRef =
-                            container.querySelector(".role-namerequired");
-                        var req = "";
-                        if (reqRef && reqRef.innerText === "True") {
-                            req = " (name required)";
-                        }
-
-                        if (node.textContent.indexOf("author") !== -1) {
-                            fromAuthor +=
-                                '<li><a href="#' +
-                                pnID +
-                                '" class="role-reference"><code>' +
-                                content +
-                                "</code></a>" +
-                                req +
-                                "</li>";
-                        }
-                        if (node.textContent.indexOf("heading") !== -1) {
-                            fromHeading +=
-                                '<li><a href="#' +
-                                pnID +
-                                '" class="role-reference"><code>' +
-                                content +
-                                "</code></a>" +
-                                req +
-                                "</li>";
-                        }
-                        if (
-                            !isAbstract &&
-                            node.textContent.indexOf("content") !== -1
-                        ) {
-                            fromContent +=
-                                '<li><a href="#' +
-                                pnID +
-                                '" class="role-reference"><code>' +
-                                content +
-                                "</code></a>" +
-                                req +
-                                "</li>";
-                        }
-                        if (node.textContent.indexOf("prohibited") !== -1) {
-                            fromProhibited +=
-                                '<li><a href="#' +
-                                pnID +
-                                '" class="role-reference"><code>' +
-                                content +
-                                "</code></a>" +
-                                req +
-                                "</li>";
-                        }
-                    });
-            }
-            if (container.nodeName.toLowerCase() == "div") {
-                // change the enclosing DIV to a section with notoc
-                var sec = document.createElement("section");
-                Array.prototype.slice
-                    .call(container.attributes)
-                    .forEach(function (attr) {
-                        sec.setAttribute(attr.name, attr.value);
-                    });
-
-                sec.classList.add("notoc");
-                var theContents = container.innerHTML;
-                sec.innerHTML = theContents;
-                container.parentNode.replaceChild(sec, container);
-            }
-        });
+    // Populate role=roletype properties with global properties
+    const roletypePropsPlaceholder = document.querySelector(
+        "#roletype td.role-properties span.placeholder"
+    );
+    roletypePropsPlaceholder.outerHTML = `<ul>${globalStatesPropertiesContent}</ul>`;
+};
 
-    var getStates = function (role) {
-        var ref = roleInfo[role];
-        if (!ref) {
-            msg.pub("error", "No role definition for " + role);
-        } else if (ref.allprops) {
-            return ref.allprops;
-        } else {
-            var myList = ref.localprops;
-            Array.prototype.slice
-                .call(ref.parentRoles)
-                .forEach(function (item) {
-                    var pList = getStates(item);
-                    myList = myList.concat(pList);
-                });
-            ref.allprops = myList;
-            return myList;
-        }
+/**
+ * For an rdef element, generates DT+DD content to be added to the Index of Roles
+ * @param {HTMLElement} item - rdef element
+ */
+const generateHTMLRoleIndexEntry = function (item) {
+    const container = item.parentNode;
+    const content = item.innerText;
+    container.id = content;
+    // is this a role or an abstract role
+    let type = "role";
+    let isAbstract = false;
+    const abstract = container.querySelectorAll(".role-abstract"); //TODO: maybe #105
+    if (abstract.innerText === "True") {
+        type = "abstract role";
+        isAbstract = true;
+    }
+    const dRef = item.nextElementSibling;
+    const desc = cloneWithoutIds(dRef.firstElementChild).innerHTML; // TODO: should the spec markup provide something more robust than "next sibling first child"? [same for sdef/pdef "desc"]
+    return `<dt><a href="#${content}" class="role-reference"><code>${content}</code>${
+        isAbstract ? " (abstract role) " : ""
+    }</a></dt>\n<dd>${desc}</dd>\n`;
+};
+
+/**
+ * Generates subrole information
+ * @param {NodeList} rdefs - rdefs
+ */
+const generateSubRoles = (rdefs) => {
+    const subRoles = {};
+    rdefs.forEach((rdef) => {
+        const title = rdef.innerHTML;
+        rdef.parentNode
+            .querySelectorAll(".role-parent rref")
+            .forEach(function (roleref) {
+                const parentRole = roleref.innerText;
+                const parentChildrenRoles = (subRoles[parentRole] ??=
+                    new Set());
+                parentChildrenRoles.add(title);
+            });
+    });
+    return subRoles;
+};
+
+/**
+ *
+ * @param {HTMLElement} item - sdef or pdef inside rdef Characteristics table
+ * @returns
+ */
+const extractStatesProperties = function (item) {
+    const name = item.getAttribute("title") || item.innerText; // TODO: raw HTML doesn't have sref/pref with title attributes but generateIndexGlobalStatesAndProperties() creates them
+    const type = item.localName === "pref" ? "property" : "state";
+    const req = !!item.closest(".role-required-properties");
+    const dis = !!item.closest(".role-disallowed");
+    const dep = item.hasAttribute("data-deprecated");
+    return {
+        is: type,
+        name: name,
+        required: req,
+        disallowed: dis,
+        deprecated: dep,
     };
+};
 
-    // TODO: test this on a page where `skipIndex` is truthy
-    if (!skipIndex) {
-        // build up the complete inherited SP lists for each role
-        // however, if the role already specifies an item, do not include it
-        Object.entries(roleInfo).forEach(function (index) {
-            var item = index[1];
-            var output = "";
-            var placeholder = document.querySelector(
-                "#" + item.fragID + " .role-inherited"
-            );
-
-            if (placeholder) {
-                var myList = [];
-                item.parentRoles.forEach(function (role) {
-                    myList = myList.concat(getStates(role));
-                });
-                // strip out any items that we have locally
-                if (item.localprops.length && myList.length) {
-                    for (var j = myList.length - 1; j >= 0; j--) {
-                        item.localprops.forEach(function (x) {
-                            if (x.name == myList[j].name) {
-                                myList.splice(j, 1);
-                            }
-                        });
-                    }
-                }
+/**
+ *
+ * @param {String} indexTest - string to decide if this index needs it
+ * @param {HTMLElement} rdef - rdef node
+ */
+const generateHTMLNameFromIndices = (indexTest, rdef) => {
+    const container = rdef.parentNode;
+    // is there a namefrom indication?  If so, add this one to
+    // the list
+    const roleFromNode = container.querySelector(".role-namefrom");
+    // is this a role or an abstract role
+    let isAbstract = false;
+    const abstract = container.querySelectorAll(".role-abstract"); //TODO: maybe #105
+    if (abstract.innerText === "True") {
+        isAbstract = true;
+    }
+    if (!isAbstract && roleFromNode) {
+        const content = rdef.innerText;
+        const isRequired =
+            roleFromNode.closest("table").querySelector(".role-namerequired")
+                ?.innerText === "True";
+        if (roleFromNode.textContent.indexOf(indexTest) !== -1)
+            return `<li><a href="#${content}" class="role-reference"><code>${content}</code></a>${
+                isRequired ? " (name required)" : ""
+            }</li>`; // TODO: `textContent.indexOf` feels brittle; right now it's either the exact string or proper list markup with LI with exact string
+    }
+};
 
-                var reducedList = myList.reduce((uniqueList, item) => {
-                    return uniqueList.includes(item)
-                        ? uniqueList
-                        : [...uniqueList, item];
-                }, []);
-
-                var sortedList = reducedList.sort((a, b) => {
-                    if (a.name == b.name) {
-                        // Ensure deprecated false properties occur first
-                        if (a.deprecated !== b.deprecated) {
-                            return a.deprecated ? 1 : b.deprecated ? -1 : 0;
-                        }
-                    }
-                    return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
-                }, []);
-
-                var prev;
-                for (var k = 0; k < sortedList.length; k++) {
-                    var property = sortedList[k];
-                    var req = "";
-                    var dep = "";
-                    if (property.required) {
-                        req = " <strong>(required)</strong>";
-                    }
-                    if (property.deprecated) {
-                        dep =
-                            " <strong>(deprecated on this role in ARIA 1.2)</strong>";
-                    }
-                    if (prev != property.name) {
-                        output += "<li>";
-                        if (property.is === "state") {
-                            output +=
-                                "<sref>" +
-                                property.name +
-                                "</sref> (state)" +
-                                req +
-                                dep;
-                        } else {
-                            output +=
-                                "<pref>" +
-                                property.name +
-                                "</pref>" +
-                                req +
-                                dep;
-                        }
-                        output += "</li>\n";
-                        prev = property.name;
-                    }
-                }
-                if (output !== "") {
-                    output = "<ul>\n" + output + "</ul>\n";
-                    placeholder.innerHTML = output;
-                }
-            }
-        });
+/**
+ * Populates roleInfo and updates proplist alongside it
+ * TODO: separate out propList updates
+ * @param {Object} roleInfo - the roleInfo object
+ * @param {Object} propList - the "list" of properties
+ * @param {HTMLElement} item - an rdef node
+ */
+const populateRoleInfoPropList = function (roleInfo, propList, item) {
+    const container = item.parentNode;
+    const content = item.innerText;
+    container.id = content;
+
+    // grab info about this role
+    // do we have a parent class?  if so, put us in that parents list
+    const rrefs = container.querySelectorAll(".role-parent rref");
+    const parentRoles = [...rrefs].map((rref) => rref.innerText);
+    // are there supported states / properties in this role?
+    const PSDefs = container.querySelectorAll(
+        `:is(.role-properties, .role-required-properties, .role-disallowed) :is(pref, sref)`
+    );
+    const attrs = [...PSDefs].map(extractStatesProperties);
+    // remember that the state or property is
+    // referenced by this role
+    PSDefs.forEach((node) =>
+        propList[node.getAttribute("title") || node.innerText].roles.push(
+            // TODO: cf.  generateIndexGlobalStatesAndProperties() TODO for simplifying title || node.innerText
+            content
+        )
+    );
 
-        // Update state and property role references
-        var getAllSubRoles = function (role) {
-            var ref = subRoles[role];
-            if (ref && ref.length) {
-                var myList = [];
-                ref.forEach(function (item) {
-                    if (!myList.item) {
-                        myList[item] = 1;
-                        myList.push(item);
-                        var childList = getAllSubRoles(item);
-                        myList = myList.concat(childList);
-                    }
-                });
-                return myList;
-            } else {
-                return [];
-            }
-        };
-
-        Object.entries(propList).forEach(function (index) {
-            var output = "";
-            var item = index[1];
-            var section = document.querySelector("#" + item.name);
-            var placeholder = section.querySelector(
-                ".state-applicability, .property-applicability"
-            );
-            if (
-                placeholder &&
-                (placeholder.textContent || placeholder.innerText) ===
-                    "Placeholder" &&
-                item.roles.length
-            ) {
-                // update the used in roles list
-                var sortedList = [];
-                sortedList = item.roles.sort();
-                for (var j = 0; j < sortedList.length; j++) {
-                    output += "<li><rref>" + sortedList[j] + "</rref></li>\n";
-                }
-                if (output !== "") {
-                    output = "<ul>\n" + output + "</ul>\n";
-                }
-                placeholder.innerHTML = output;
-                // also update any inherited roles
-                var myList = [];
-                item.roles.forEach(function (role) {
-                    var children = getAllSubRoles(role);
-                    // Some subroles have required properties which are also required by the superclass.
-                    // Example: The checked state of radio, which is also required by superclass checkbox.
-                    // We only want to include these one time, so filter out the subroles.
-                    children = children.filter(function (subrole) {
-                        return (
-                            subrole.indexOf(propList[item.name].roles) === -1
-                        );
-                    });
-                    myList = myList.concat(children);
-                });
-                placeholder = section.querySelector(
-                    ".state-descendants, .property-descendants"
-                );
-                if (placeholder && myList.length) {
-                    sortedList = myList.sort();
-                    output = "";
-                    var last = "";
-                    for (j = 0; j < sortedList.length; j++) {
-                        var sItem = sortedList[j];
-                        if (last != sItem) {
-                            output += "<li><rref>" + sItem + "</rref></li>\n";
-                            last = sItem;
-                        }
-                    }
-                    if (output !== "") {
-                        output = "<ul>\n" + output + "</ul>\n";
-                    }
-                    placeholder.innerHTML = output;
-                }
-            } else if (
-                placeholder &&
-                (placeholder.textContent || placeholder.innerText) ===
-                    "Use as a global deprecated in ARIA 1.2" &&
-                item.roles.length
-            ) {
-                // update the used in roles list
-                var sortedList = [];
-                sortedList = item.roles.sort();
-                //remove roletype from the sorted list
-                const index = sortedList.indexOf("roletype");
-                if (index > -1) {
-                    sortedList.splice(index, 1);
-                }
+    roleInfo[content] = {
+        name: content,
+        fragID: content,
+        parentRoles: parentRoles,
+        localprops: attrs,
+    };
+};
 
-                for (var j = 0; j < sortedList.length; j++) {
-                    output += "<li><rref>" + sortedList[j] + "</rref></li>\n";
-                }
-                if (output !== "") {
-                    output = "<ul>\n" + output + "</ul>\n";
-                }
-                placeholder.innerHTML = output;
-                // also update any inherited roles
-                var myList = [];
-                item.roles.forEach(function (role) {
-                    var children = getAllSubRoles(role);
-                    // Some subroles have required properties which are also required by the superclass.
-                    // Example: The checked state of radio, which is also required by superclass checkbox.
-                    // We only want to include these one time, so filter out the subroles.
-                    children = children.filter(function (subrole) {
-                        return (
-                            subrole.indexOf(propList[item.name].roles) === -1
-                        );
-                    });
-                    myList = myList.concat(children);
-                });
-                placeholder = section.querySelector(
-                    ".state-descendants, .property-descendants"
-                );
-                if (placeholder && myList.length) {
-                    sortedList = myList.sort();
-                    output = "";
-                    var last = "";
-                    for (j = 0; j < sortedList.length; j++) {
-                        var sItem = sortedList[j];
-                        if (last != sItem) {
-                            output += "<li><rref>" + sItem + "</rref></li>\n";
-                            last = sItem;
-                        }
-                    }
-                    if (output !== "") {
-                        output = "<ul>\n" + output + "</ul>\n";
-                    }
-                    placeholder.innerHTML = output;
-                }
-            } else if (
-                placeholder &&
-                (placeholder.textContent || placeholder.innerText) ===
-                    "All elements of the base markup except for some roles or elements that prohibit its use" &&
-                item.roles.length
-            ) {
-                // for prohibited roles the roles list just includes those roles which are prohibited... weird I know but it is what it is
-                var sortedList = [];
-                sortedList = item.roles.sort();
-                //remove roletype from the sorted list
-                const index = sortedList.indexOf("roletype");
-                if (index > -1) {
-                    sortedList.splice(index, 1);
-                }
-                output +=
-                    "All elements of the base markup except for the following roles: ";
-                for (var j = 0; j < sortedList.length - 1; j++) {
-                    output += "<rref>" + sortedList[j] + "</rref>, ";
-                }
-                output +=
-                    "<rref>" + sortedList[sortedList.length - 1] + "</rref>";
-                placeholder.innerHTML = output;
-            }
+/**
+ * TODO: depends on global roleInfo object
+ * Generats `allprops` array for a role entry in roleInfo
+ * @param {string} role - name of a role
+ * @returns
+ */
+const getStates = function (role) {
+    // TODO: pkra would like to use sets here but allprops part of roleInfo serializaton
+    const ref = roleInfo[role];
+    if (!ref) {
+        msg.pub("error", "No role definition for " + role);
+    } else if (ref.allprops) {
+        return ref.allprops;
+    } else {
+        let myList = ref.localprops;
+        ref.parentRoles.forEach(function (item) {
+            const pList = getStates(item);
+            myList = myList.concat(pList);
         });
+        ref.allprops = myList;
+        return myList;
+    }
+};
 
-        // spit out the index
-        var node = document.getElementById("index_role");
-        var parentNode = node.parentNode;
-        var list = document.createElement("dl");
-        list.id = "index_role";
-        list.className = "compact";
-        list.innerHTML = roleIndex;
-        parentNode.replaceChild(list, node);
-
-        // and the namefrom lists
-        node = document.getElementById("index_fromauthor");
-        if (node) {
-            parentNode = node.parentNode;
-            list = document.createElement("ul");
-            list.id = "index_fromauthor";
-            list.className = "compact";
-            list.innerHTML = fromAuthor;
-            parentNode.replaceChild(list, node);
-        }
-
-        node = document.getElementById("index_fromheading");
-        if (node) {
-            parentNode = node.parentNode;
-            list = document.createElement("ul");
-            list.id = "index_fromheading";
-            list.className = "compact";
-            list.innerHTML = fromHeading;
-            parentNode.replaceChild(list, node);
+/**
+ * Builds up the complete inherited SP lists for each role
+ * However, if the role already specifies an item, do not include it
+ * @param {Object} item - value from Object.values(roleInfo)
+ */
+const buildInheritedStatesProperties = function (item) {
+    // BEGIN TODO: why can't we do, e.g.,
+    // 1. in the main function: Object.keys(roleInfo).forEach(role=> getStates(role)); (see also TODO: near where buildInheritedStatesProperties() is called)
+    //   - Then: let myList = item.allprops; (instead of myList = myList.concat(getStates(role)))
+    //   - NOTE: the HTML stays the same but the exported roleInfo isn't.
+    //   - TODO: BUG? in the existing roleInfo allprops only occurs 30 times
+    let myList = [];
+    item.parentRoles.forEach(function (role) {
+        myList = myList.concat(getStates(role));
+    });
+    // END TODO
+    // strip out any items that we have locally
+    // BEGIN TODO: why can't we do myList.filter( inherited => item.localprops.includes(local => local.name === inherited.name))?
+    // or do something else to simplify this
+    if (item.localprops.length && myList.length) {
+        for (let j = myList.length - 1; j >= 0; j--) {
+            item.localprops.forEach(function (x) {
+                if (x.name == myList[j].name) {
+                    myList.splice(j, 1);
+                }
+            });
         }
+    }
 
-        node = document.getElementById("index_fromcontent");
-        if (node) {
-            parentNode = node.parentNode;
-            list = document.createElement("ul");
-            list.id = "index_fromcontent";
-            list.className = "compact";
-            list.innerHTML = fromContent;
-            parentNode.replaceChild(list, node);
-        }
+    const reducedList = [...new Set(myList)];
 
-        node = document.getElementById("index_fromprohibited");
-        if (node) {
-            parentNode = node.parentNode;
-            list = document.createElement("ul");
-            list.id = "index_fromprohibited";
-            list.className = "compact";
-            list.innerHTML = fromProhibited;
-            parentNode.replaceChild(list, node);
-        }
-        // assuming we found some parent roles, update those parents with their children
-        for (var i = 0; i < subRoles.length; i++) {
-            var item = subRoles[subRoles[i]];
-            var sortedList = item.sort(function (a, b) {
-                return a < b ? -1 : a > b ? 1 : 0;
-            });
-            var output = "<ul>\n";
-            for (var j = 0; j < sortedList.length; j++) {
-                output += "<li><rref>" + sortedList[j] + "</rref></li>\n";
-            }
-            output += "</ul>\n";
-            // put it somewhere
-            var subRolesContainer = document.querySelector("#" + subRoles[i]);
-            if (subRolesContainer) {
-                var subRolesListContainer =
-                    subRolesContainer.querySelector(".role-children");
-                if (subRolesListContainer) {
-                    subRolesListContainer.innerHTML = output;
-                }
+    const sortedList = reducedList.sort((a, b) => {
+        if (a.name == b.name) {
+            //TODO: BUG: deprecated states&props do not actually appear at end
+            // NOTE: removing if (a.deprecated !== b.deprecated) seems to fix this
+            // Ensure deprecated false properties occur first
+            if (a.deprecated !== b.deprecated) {
+                return a.deprecated ? 1 : b.deprecated ? -1 : 0;
             }
         }
+        return a.name.localeCompare(b.name);
+    }, []);
+
+    const uniquePropNames = new Set(sortedList.map((prop) => prop.name));
+    // NOTE: uniquePropNames is needed because sortedList can have duplicates, in particular with different deprecation states. E.g., treeitem inherits aria-disabled from option but also as deprecated-in-1.2 from listitem.
+    // TODO: is it just luck that the not-deprecated state is listed first? (see same comment below)
+    const output = [...uniquePropNames]
+        .map((propName) => {
+            const property = sortedList.find((p) => p.name === propName); // TODO: is it just luck that the not-deprecated state is listed first?
+            const isState = property.is === "state";
+            const suffix = isState ? " (state)" : "";
+            const tag = isState ? "sref" : "pref";
+            const req = property.required ? " <strong>(required)</strong>" : "";
+            const dep = property.deprecated
+                ? " <strong>(deprecated on this role in ARIA 1.2)</strong>"
+                : "";
+
+            return `<li><${tag}>${property.name}</${tag}>${suffix}${req}${dep}</li>\n`;
+        })
+        .join("");
+    if (output !== "") {
+        document.querySelector(
+            "#" + item.fragID + " .role-inherited"
+        ).innerHTML = `<ul>\n${output}</ul>\n`;
     }
+};
 
-    // prune out unused rows throughout the document
-    Array.prototype.slice
-        .call(
-            document.querySelectorAll(
-                ".role-abstract, .role-parent, .role-base, .role-related, .role-scope, .role-mustcontain, .role-required-properties, .role-properties, .role-namefrom, .role-namerequired, .role-namerequired-inherited, .role-childpresentational, .role-presentational-inherited, .state-related, .property-related,.role-inherited, .role-children, .property-descendants, .state-descendants, .implicit-values"
-            )
+/**
+ * prune out unused rows throughout the document
+ *
+ */
+const pruneUnusedRows = () => {
+    document
+        .querySelectorAll(
+            ".role-abstract, .role-parent, .role-base, .role-related, .role-scope, .role-mustcontain, .role-required-properties, .role-properties, .role-namefrom, .role-namerequired, .role-namerequired-inherited, .role-childpresentational, .role-presentational-inherited, .state-related, .property-related,.role-inherited, .role-children, .property-descendants, .state-descendants, .implicit-values"
         )
         .forEach(function (item) {
             var content = item.innerText;
@@ -755,7 +417,6 @@ function ariaAttributeReferences() {
                 item.parentNode.parentNode.removeChild(item.parentNode);
             } else if (
                 content === "Placeholder" &&
-                !skipIndex &&
                 (item.className === "role-inherited" ||
                     item.className === "role-children" ||
                     item.className === "property-descendants" ||
@@ -764,16 +425,229 @@ function ariaAttributeReferences() {
                 item.parentNode.remove();
             }
         });
+};
 
-    updateReferences(document);
+/**
+ * Generates the HTML for various indices in the spec
+ * @param {NodeList} rdefs - all the rdefs
+ */
+const generateHTMLIndices = (rdefs) => {
+    let fromAuthor = [...rdefs]
+        .map(generateHTMLNameFromIndices.bind(null, "author"))
+        .join("");
+    let fromHeading = [...rdefs]
+        .map(generateHTMLNameFromIndices.bind(null, "heading"))
+        .join("");
+    let fromContent = [...rdefs]
+        .map(generateHTMLNameFromIndices.bind(null, "content"))
+        .join("");
+    let fromProhibited = [...rdefs]
+        .map(generateHTMLNameFromIndices.bind(null, "prohibited"))
+        .join("");
+
+    const roleIndex = [...rdefs].map(generateHTMLRoleIndexEntry).join("");
+
+    // spit out the indices
+    document.getElementById(
+        "index_role"
+    ).outerHTML = `<dl id="index_role" class="compact">${roleIndex}</dl>`;
+    document.getElementById(
+        "index_fromauthor"
+    ).outerHTML = `<ul id="index_fromauthor" class="compact">${fromAuthor}</ul>`;
+    document.getElementById(
+        "index_fromcontent"
+    ).outerHTML = `<ul id="index_fromcontent" class="compact">${fromContent}</ul>`;
+    document.getElementById(
+        "index_fromprohibited"
+    ).outerHTML = `<ul id="index_fromprohibited" class="compact">${fromProhibited}</ul>`;
+    // TODO: remove if-check after w3c/aria#1860
+    if (document.getElementById("index_fromheading"))
+        document.getElementById(
+            "index_fromheading"
+        ).outerHTML = `<ul id="index_fromheading" class="compact">${fromHeading}</ul>`;
+};
 
-    function cloneWithoutIds(node) {
-        const clone = node.cloneNode(true);
-        for (const elementWithId of clone.querySelectorAll("[id]")) {
-            elementWithId.removeAttribute("id");
-        }
-        return clone;
+/**
+ * Creates dictionary of "descendant" roles
+ * @param {Object} subRoles - the subroles collection
+ * @returns
+ */
+const createDescendantRoles = (subRoles) => {
+    const descendantRoles = {};
+    const getAllSubRoles = function (key) {
+        const subroleSet = new Set();
+        if (!subRoles[key]) return subroleSet; // NOTE: recursion end
+        subRoles[key].forEach(function (childRole) {
+            subroleSet.add(childRole);
+            const descendantRolesSet = getAllSubRoles(childRole);
+            descendantRolesSet.forEach((role) => subroleSet.add(role));
+        });
+        return subroleSet;
+    };
+    Object.keys(subRoles).forEach(
+        (item) => (descendantRoles[item] = getAllSubRoles(item))
+    );
+    return descendantRoles;
+};
+
+/**
+ * The propList loop.
+ * @param {Object} propList - the propList
+ * @param {Object} descendantRoles - the list of "descendant" roles
+ * @param {Object} item - value from object.values(propList)
+ * @returns
+ */
+const propListLoop = function (propList, descendantRoles, item) {
+    const section = document.querySelector("#" + item.name);
+    let placeholder = section.querySelector(
+        ".state-applicability, .property-applicability"
+    );
+    const placeholderText = placeholder.innerText;
+    // Current values for placeholderText:
+    // * "All elements of the base markup"
+    // * "Placeholder"
+    // * "Use as a global deprecated in ARIA 1.2"
+    // * "All elements of the base markup except for some roles or elements that prohibit its use"
+    // TODO: Maybe use a data attribute instead?
+
+    // Case: nothing to do
+    if (placeholderText === "All elements of the base markup") return;
+
+    // update roles list: sort & maybe remove roletype
+    item.roles.sort();
+    if (placeholderText !== "Placeholder")
+        item.roles.splice(item.roles.indexOf("roletype"), 1);
+
+    // Case: partially prohibited
+    if (
+        placeholderText ===
+        "All elements of the base markup except for some roles or elements that prohibit its use"
+    ) {
+        // for prohibited roles the roles list just includes those roles which are prohibited... weird I know but it is what it is
+
+        placeholder.innerHTML = `All elements of the base markup except for the following roles: ${item.roles
+            .map((role) => `<rref>${role}</rref>`)
+            .join(", ")}`;
+        return;
     }
+
+    // Otherwise, i.e.,
+    // Cases: placeholderText "Placeholder" or "Use as a global deprecated in ARIA 1.2"
+
+    // populate placeholder
+    placeholder.innerHTML = `<ul>\n${item.roles
+        .map((role) => `<li><rref>${role}</rref></li>\n`)
+        .join("")}</ul>\n`;
+
+    // also update any inherited roles
+    const placeholderInheritedRoles = section.querySelector(
+        ".state-descendants, .property-descendants"
+    );
+    let inheritedRoles = new Set();
+    item.roles.forEach(function (role) {
+        // Some subroles have required properties which are also required by the superclass.
+        // Example: The checked state of radio, which is also required by superclass checkbox.
+        // We only want to include these one time, so filter out the subroles.
+        if (!descendantRoles[role]) return;
+        descendantRoles[role].forEach((subrole) => {
+            if (subrole.indexOf(propList[item.name].roles) === -1)
+                inheritedRoles.add(subrole);
+            // TODO: the if-check doesn't make sense
+            // Should it be the other way around? I.e.
+            // if (propList[item.name].roles.indexOf(subrole) === -1)
+            //     inheritedRoles.add(subrole);
+            // But this changes the spec, adding some, removing other entries
+        });
+    });
+
+    placeholderInheritedRoles.innerHTML = `<ul>\n${[...inheritedRoles]
+        .sort()
+        .map((role) => `<li><rref>${role}</rref></li>\n`)
+        .join("")}</ul>\n`;
+};
+
+/**
+ * In Object.entries loop, generates HTML for child role entries
+ * @param {String} role - subRoles key
+ * @param {Object} subRolesSet - subRoles value
+ */
+const generateHTMLRoleChildren = ([role, subroleSet]) => {
+    const item = [...subroleSet];
+    document.querySelector(`#${role} .role-children`).innerHTML = `<ul>\n${item
+        .map((subrole) => `<li><rref>${subrole}</rref></li>\n`)
+        .join("")}</ul>\n`;
+};
+
+function ariaAttributeReferences() {
+    const propList = {};
+    const globalSP = [];
+
+    let skipIndex = 0;
+    const myURL = document.URL;
+    if (myURL.match(/\?fast/)) {
+        skipIndex = 1;
+    }
+
+    // process the document before anything else is done
+    // first get the properties
+    const pdefsAndsdefs = document.querySelectorAll("pdef, sdef");
+    const pdefsAndsdefsContainer = [...pdefsAndsdefs].map(
+        (node) => node.parentNode
+    );
+
+    pdefsAndsdefs.forEach(populatePropList.bind(null, propList));
+    pdefsAndsdefs.forEach(populateGlobalSP.bind(null, propList, globalSP));
+    pdefsAndsdefs.forEach(generateHTMLStatesAndProperties.bind(null, propList));
+    pdefsAndsdefsContainer.forEach(rewriteDefContainer);
+
+    if (!skipIndex) {
+        // Generate index of states and properties
+        generateIndexStatesAndProperties(propList);
+
+        // Generate index of global states and properties
+        generateIndexGlobalStatesAndProperties(globalSP);
+    }
+
+    // what about roles?
+    //
+    // we need to do a few things here:
+    //   1. expand the rdef elements.
+    //   2. accumulate the roles into a table for the indices
+    //   3. grab the parent role reference so we can build up the tree
+    //   4. grab any local states and properties so we can hand those down to the children
+    //
+
+    const rdefs = document.querySelectorAll("rdef");
+    const rdefsContainer = [...rdefs].map((node) => node.parentNode);
+
+    const subRoles = generateSubRoles(rdefs);
+
+    generateHTMLIndices(rdefs);
+
+    rdefs.forEach(populateRoleInfoPropList.bind(null, roleInfo, propList));
+
+    rdefs.forEach(rewriteRdef);
+
+    rdefsContainer.forEach(rewriteDefContainer);
+
+    // TODO: test this on a page where `skipIndex` is truthy
+    if (!skipIndex) {
+        // TODO: why not run  `Object.keys(roleInfo).forEach(role=> getStates(role))` here? (cf. TODO: in buildInheritedStatesProperties )
+        Object.values(roleInfo).forEach(buildInheritedStatesProperties);
+
+        const descendantRoles = createDescendantRoles(subRoles);
+
+        Object.values(propList).forEach(
+            propListLoop.bind(null, propList, descendantRoles)
+        );
+
+        // assuming we found some parent roles, update those parents with their children
+        Object.entries(subRoles).forEach(generateHTMLRoleChildren);
+    }
+
+    pruneUnusedRows();
+
+    updateReferences(document);
 }
 
 require(["core/pubsubhub"], function (respecEvents) {
diff --git a/test.sh b/test.sh
new file mode 100644
index 0000000..3b2fda1
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# NOTE: Assumes there's a copy of w3c/aria in ../aria/
+
+rm before.html after.html
+git -C ../aria/ checkout ./common/script/aria.js
+echo "Run respec on ../aria/index.html to generate 'before.html'"
+npx respec --src ../aria/index.html --out before.html
+echo "Copy ./script/aria.js to ../aria/common/script/"
+cp ./script/aria.js ../aria/common/script/.
+echo "Run respec on ../aria/index.html to generate 'after.html'"
+npx respec --src ../aria/index.html --out after.html
+echo "Run diff on 'before.html' and 'after.html'"
+diff before.html after.html
+echo "Clean up aria spec"
+git -C ../aria/ checkout ./common/script/aria.js