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 = - "" + - content + - ' ' + - type + - ""; - 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 += - '
' + - item.name + - "
\n"; - propIndex += "
" + item.desc + "
\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 += "
  • "; - if (lItem.is === "state") { - globalSPIndex += - "' + - lItem.name + - " (state)"; - } else { - globalSPIndex += - "" + - lItem.name + - ""; - } - if (lItem.prohibited) { - globalSPIndex += " (Except where prohibited)"; - } - if (lItem.deprecated) { - globalSPIndex += " (Global use deprecated in ARIA 1.2)"; - } - globalSPIndex += "
  • \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 = `

    ${content} ${type}`; +}; - // 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 = `

    ${itemEntry.name} ${itemEntry.is}

    `; +}; - 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) => + `
    ${item.name}
    \n
    ${item.desc}
    \n` + ) + .join(""); + indexStatePropPlaceholder.outerHTML = `
    ${indexStatePropContent}
    `; +}; - 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 `
  • <${tagName} ${ + item.prohibited ? "data-prohibited " : "" + }${item.deprecated ? "data-deprecated " : ""}${ + isState ? `title="${item.name}"` : "" + }>${item.name}${isState ? " (state)" : ""}${ + // 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)" : "" + }
  • \n`; + }) + .join(""); + const globalStatesPropertiesPlaceholder = document.querySelector( + "#global_states .placeholder" + ); + globalStatesPropertiesPlaceholder.outerHTML = ``; - 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 = - "" + - content + - ' ' + - type + - ""; - // 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 += - '
    ' + - content + - "" + - (isAbstract ? " (abstract role) " : "") + - "
    \n"; - roleIndex += "
    " + desc + "
    \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 += - '
  • ' + - content + - "" + - req + - "
  • "; - } - if (node.textContent.indexOf("heading") !== -1) { - fromHeading += - '
  • ' + - content + - "" + - req + - "
  • "; - } - if ( - !isAbstract && - node.textContent.indexOf("content") !== -1 - ) { - fromContent += - '
  • ' + - content + - "" + - req + - "
  • "; - } - if (node.textContent.indexOf("prohibited") !== -1) { - fromProhibited += - '
  • ' + - content + - "" + - req + - "
  • "; - } - }); - } - 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 = ``; +}; - 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 `
    ${content}${ + isAbstract ? " (abstract role) " : "" + }
    \n
    ${desc}
    \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 `
  • ${content}${ + isRequired ? " (name required)" : "" + }
  • `; // 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 = " (required)"; - } - if (property.deprecated) { - dep = - " (deprecated on this role in ARIA 1.2)"; - } - if (prev != property.name) { - output += "
  • "; - if (property.is === "state") { - output += - "" + - property.name + - " (state)" + - req + - dep; - } else { - output += - "" + - property.name + - "" + - req + - dep; - } - output += "
  • \n"; - prev = property.name; - } - } - if (output !== "") { - output = "\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 += "
  • " + sortedList[j] + "
  • \n"; - } - if (output !== "") { - output = "\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 += "
  • " + sItem + "
  • \n"; - last = sItem; - } - } - if (output !== "") { - output = "\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 += "
  • " + sortedList[j] + "
  • \n"; - } - if (output !== "") { - output = "\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 += "
  • " + sItem + "
  • \n"; - last = sItem; - } - } - if (output !== "") { - output = "\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 += "" + sortedList[j] + ", "; - } - output += - "" + sortedList[sortedList.length - 1] + ""; - 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 = "\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 ? " (required)" : ""; + const dep = property.deprecated + ? " (deprecated on this role in ARIA 1.2)" + : ""; + + return `
  • <${tag}>${property.name}${suffix}${req}${dep}
  • \n`; + }) + .join(""); + if (output !== "") { + document.querySelector( + "#" + item.fragID + " .role-inherited" + ).innerHTML = `\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 = `
    ${roleIndex}
    `; + document.getElementById( + "index_fromauthor" + ).outerHTML = ``; + document.getElementById( + "index_fromcontent" + ).outerHTML = ``; + document.getElementById( + "index_fromprohibited" + ).outerHTML = ``; + // TODO: remove if-check after w3c/aria#1860 + if (document.getElementById("index_fromheading")) + document.getElementById( + "index_fromheading" + ).outerHTML = ``; +}; - 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) => `${role}`) + .join(", ")}`; + return; } + + // Otherwise, i.e., + // Cases: placeholderText "Placeholder" or "Use as a global deprecated in ARIA 1.2" + + // populate placeholder + placeholder.innerHTML = `\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 = `\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 = `\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