Skip to content

Instantly share code, notes, and snippets.

@eps1lon
Last active March 19, 2022 08:29
Show Gist options
  • Save eps1lon/586e9a003c4d1cf3b74a6cd943edc46d to your computer and use it in GitHub Desktop.
Save eps1lon/586e9a003c4d1cf3b74a6cd943edc46d to your computer and use it in GitHub Desktop.
ARIA spec to aria-query

ARIA spec to aria-query

Usage

Using Chrome

WAI-ARIA

  1. goto ARIA stable editor's draft
  2. Open devtools (e.g. via F12)
  3. Goto "Sources" > "Snippets"
  4. Add new snippet ("New snippet")
  5. Paste content of parseAriaRole.js
  6. Run snippet e.g. via CTRK+Enter
  7. Copy JSON that was logged to console ("Copy string contents")
  8. Paste into ./scripts/roles.json
  9. Revert removal of doc-* roles
  10. Revert removal of graphics-* roles

GRAPHICS-ARIA

  1. goto GRAPHICS-ARIA 1.0
  2. Open devtools (e.g. via F12)
  3. Goto "Sources" > "Snippets"
  4. Add new snippet ("New snippet")
  5. Paste content of parseGraphicsAriaRole.js
  6. Run snippet e.g. via CTRK+Enter
  7. Copy JSON that was logged to console ("Copy string contents")
  8. Replace graphics-* keys in ./scripts/roles.json with clipboard content
  9. Make sureonly graphics-* keys are affected

aria-query

  1. node scripts/breakUpAriaJSON.js
  2. node scripts/breakUpAriaJSON.js might crash due to unparseable stuff from the spec. Use good judgment to revert changes to role.json that didn't make sense
window.PARSE_ROLE_FLAGS = {
/**
* aria-query did previously not try to convert default props to numbers
* The following flag can enable this behavior.
*/
enableNumericDefaultValues: false,
};
function main(ariaQuery) {
const roles = Array.from(
document.querySelector("dl#index_role").querySelectorAll("dt a"),
(anchor) => {
return anchor.textContent;
}
)
.map((role) => {
return role.replace(/\s+\(abstract role\)\s*/, "");
})
.filter((role) => {
return (
role !== "none" &&
// `image` is a synonym for `img`.
// We'll link `image` to `img` later
role !== "image"
);
});
const aria = Object.fromEntries(
roles
.map((role) => {
//console.groupCollapsed(role)
const data = computeRoleData(role);
//console.groupEnd(role)
// relatedConcepts are differently used in aria-query
data.relatedConcepts =
ariaQuery[role]?.relatedConcepts ?? data.relatedConcepts;
return [role, data];
})
.concat([
[
"none",
{
abstract: false,
accessibleNameRequired: false,
childrenPresentational: false,
nameFrom: [],
prohibitedProps: [],
props: [],
relatedConcepts: [],
requiredContextRole: [],
requiredOwnedElements: [],
requiredProps: [],
superClass: [],
},
],
])
.sort((a, b) => a[0].localeCompare(b[0]))
);
// `image` is a synonym for `img`.
aria.image = aria.img;
console.log(aria);
return JSON.stringify(aria, null, 2);
}
function computeRoleData(role) {
const featureTable = document.querySelector(
`section#${role} table.role-features`
);
if (featureTable === null) {
throw new TypeError(`No feature table for role '${role}'`);
}
const defaultValues = computeDefaultValues(featureTable);
const data = {
abstract: computeAbstract(featureTable),
accessibleNameRequired: computeAccessibleNameRequired(featureTable),
childrenPresentational: computeChildrenPresentational(featureTable),
nameFrom: computeNameFrom(featureTable).sort((a, b) => a.localeCompare(b)),
prohibitedProps: computeProhibitedProps(featureTable).sort((a, b) =>
a.localeCompare(b)
),
props: computeProps(featureTable).sort((a, b) => {
if (Array.isArray(a) && Array.isArray(b)) {
return a[0].localeCompare(b[0]);
}
// sort props with default values last
if (Array.isArray(a)) {
return 1;
}
if (Array.isArray(b)) {
return -1;
}
return a.localeCompare(b);
}),
relatedConcepts: computeRelatedConcepts(featureTable),
requiredContextRole: computeRequiredContextRole(featureTable),
requiredOwnedElements: computeRequiredOwnedElements(featureTable),
requiredProps: computeRequiredProps(featureTable).sort((a, b) => {
if (Array.isArray(a) && Array.isArray(b)) {
return a[0].localeCompare(b[0]);
}
// sort props with default values last
if (Array.isArray(a)) {
return 1;
}
if (Array.isArray(b)) {
return -1;
}
return a.localeCompare(b);
}),
superClass: computeSuperClass(featureTable),
};
return data;
}
function computeSuperClass(featureTable) {
const value = featureTable.querySelector(".role-parent");
if (value !== null && value.querySelector("ul") !== null) {
return Array.from(value.querySelectorAll("li")).map((listitem) =>
listitem.textContent.trim()
);
}
return value === null ? [] : [value.textContent.trim()];
}
function computeAccessibleNameRequired(featureTable) {
return Boolean(
featureTable.querySelector(".role-namerequired")?.textContent === "True"
);
}
function computeNameFrom(featureTable) {
const value = featureTable.querySelector(".role-namefrom");
if (value.querySelector("ul") !== null) {
return Array.from(value.querySelectorAll("li"))
.map((listitem) => listitem.textContent)
.filter((naming) => naming !== "n/a");
}
return value.textContent === "n/a" ? [] : [value.textContent];
}
function computeRequiredContextRole(featureTable) {
const value = featureTable.querySelector(".role-scope");
if (value !== null && value.querySelector("ul") !== null) {
return Array.from(value.querySelectorAll("li")).map(
(listitem) => listitem.textContent
);
}
return value === null ? [] : [value.textContent];
}
function computeRequiredOwnedElements(featureTable) {
const value = featureTable.querySelector(".role-mustcontain");
let ownedElements = [];
if (value !== null && value.querySelector("ul") !== null) {
ownedElements = Array.from(value.querySelectorAll("li")).map(
(listitem) => listitem.textContent
);
} else if (value !== null) {
ownedElements = [value.textContent];
}
return ownedElements
.map((element) => {
// e.g. "rowgroup → row" => ["row", "rowgroup"]
if (element.indexOf("→") !== -1) {
return element
.split("→")
.map((node) => node.trim())
.reverse();
}
return element;
})
.map((elementOrArray) => {
// consistently return arrays
// https://github.com/A11yance/aria-query/pull/46#discussion_r439229900
if (Array.isArray(elementOrArray)) {
return elementOrArray;
}
return [elementOrArray];
});
}
function computeRequiredProps(featureTable) {
const props = [
...coerceListitems(
featureTable.querySelector(".role-required-properties")
).map((listitem) => {
return listitem.textContent;
}),
...coerceListitems(featureTable.querySelector(".role-inherited"))
.filter((listitem) => {
return listitem.textContent.indexOf("(required)") !== -1;
})
.map((listitem) => {
return listitem.textContent.split(" ")[0].trim();
}),
];
const defaultValues = computeDefaultValues(featureTable);
return props.map((prop) => {
if (defaultValues[prop] !== undefined) {
return [prop, defaultValues[prop]];
}
return prop;
});
}
function computeAbstract(featureTable) {
return Boolean(
featureTable.querySelector(".role-abstract")?.textContent === "True"
);
}
function computeRelatedConcepts(featureTable) {
const concepts = Array.from(
featureTable.querySelectorAll(".role-related li")
);
return concepts.map((listitem) => {
const htmlMatch = listitem.textContent.match(/(.*) in \[HTML\]/);
if (htmlMatch !== null) {
return {
module: "HTML",
concept: {
name: htmlMatch[1],
},
};
}
return {
module: "ARIA",
concept: {
name: listitem.textContent,
},
};
});
}
function computeDefaultValues(featureTable) {
const defaultValues = {};
const matchedDefaultValues = featureTable
.querySelector(".implicit-values")
?.textContent.matchAll(/Default for (.+?) is (.+?)\./g);
if (matchedDefaultValues !== undefined) {
for (const match of matchedDefaultValues) {
defaultValues[match[1]] = match[2];
if (PARSE_ROLE_FLAGS.enableNumericDefaultValues) {
const valueAsString = match[2];
const valueAsNumber = parseInt(valueAsString, 10);
defaultValues[match[1]] = Number.isNaN(valueAsNumber)
? valueAsString
: valueAsNumber;
}
}
}
return defaultValues;
}
function computeProps(featureTable) {
const props = [
...coerceListitems(featureTable.querySelector(".role-properties")),
...coerceListitems(featureTable.querySelector(".role-inherited")),
]
.filter((listitem) => {
return listitem.textContent.indexOf("deprecated") === -1;
})
.map((listitem) => listitem.textContent.trim().split(" ")[0].trim());
const defaultValues = computeDefaultValues(featureTable);
return (
props
.concat(
computeRequiredProps(featureTable).filter(
(requiredProp) => props.includes(requiredProp) === false
)
)
.map((prop) => {
if (defaultValues[prop] !== undefined) {
return [prop, defaultValues[prop]];
}
return prop;
})
// aria spec editorial issue
.concat(
Object.entries(defaultValues).filter(([prop]) => {
return false;
return props.includes(prop) === false;
})
)
);
}
function computeChildrenPresentational(featureTable) {
return Boolean(
featureTable.querySelector(".role-childpresentational")?.textContent ===
"True"
);
}
function coerceListitems(container) {
if (container === null) {
return [];
}
if (container.querySelector("ul") !== null) {
return Array.from(container.querySelectorAll("li"));
}
return [container];
}
function computeProhibitedProps(featureTable) {
return coerceListitems(featureTable.querySelector(".role-disallowed")).map(
(listitem) => listitem.textContent.trim().split(" ")[0].trim()
);
}
main({
alert: {
relatedConcepts: [{ concept: { name: "alert" }, module: "XForms" }],
},
alertdialog: {
relatedConcepts: [{ concept: { name: "alert" }, module: "XForms" }],
},
application: {
relatedConcepts: [
{ concept: { name: "Device Independence Delivery Unit" } },
],
},
article: {
relatedConcepts: [{ concept: { name: "article" }, module: "HTML" }],
},
banner: {
relatedConcepts: [
{
concept: {
constraints: ["direct descendant of document"],
name: "header",
},
module: "HTML",
},
],
},
blockquote: { relatedConcepts: [] },
button: {
relatedConcepts: [
{
concept: {
attributes: [
{ constraints: ["set"], name: "aria-pressed" },
{ name: "type", value: "checkbox" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [{ name: "aria-expanded", value: "false" }],
name: "summary",
},
module: "HTML",
},
{
concept: {
attributes: [{ name: "aria-expanded", value: "true" }],
constraints: [
"direct descendant of details element with the open attribute defined",
],
name: "summary",
},
module: "HTML",
},
{
concept: {
attributes: [{ name: "type", value: "button" }],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [{ name: "type", value: "image" }],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [{ name: "type", value: "reset" }],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [{ name: "type", value: "submit" }],
name: "input",
},
module: "HTML",
},
{ concept: { name: "button" }, module: "HTML" },
{ concept: { name: "trigger" }, module: "XForms" },
],
},
caption: { relatedConcepts: [] },
cell: {
relatedConcepts: [
{
concept: { constraints: ["descendant of table"], name: "td" },
module: "HTML",
},
],
},
checkbox: {
relatedConcepts: [
{
concept: {
attributes: [{ name: "type", value: "checkbox" }],
name: "input",
},
module: "HTML",
},
{ concept: { name: "option" }, module: "ARIA" },
],
},
code: { relatedConcepts: [] },
columnheader: {
relatedConcepts: [
{
attributes: [{ name: "scope", value: "col" }],
concept: { name: "th" },
module: "HTML",
},
],
},
combobox: {
relatedConcepts: [
{
concept: {
attributes: [
{ constraints: ["set"], name: "list" },
{ name: "type", value: "email" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["set"], name: "list" },
{ name: "type", value: "search" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["set"], name: "list" },
{ name: "type", value: "tel" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["set"], name: "list" },
{ name: "type", value: "text" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["set"], name: "list" },
{ name: "type", value: "url" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["set"], name: "list" },
{ name: "type", value: "url" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["undefined"], name: "multiple" },
{ constraints: ["undefined"], name: "size" },
],
name: "select",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["undefined"], name: "multiple" },
{ name: "size", value: 1 },
],
name: "select",
},
module: "HTML",
},
{ concept: { name: "select" }, module: "XForms" },
],
},
command: {
relatedConcepts: [{ concept: { name: "menuitem" }, module: "HTML" }],
},
complementary: {
relatedConcepts: [{ concept: { name: "aside" }, module: "HTML" }],
},
composite: { relatedConcepts: [] },
contentinfo: {
relatedConcepts: [
{
concept: {
constraints: ["direct descendant of document"],
name: "footer",
},
module: "HTML",
},
],
},
definition: {
relatedConcepts: [{ concept: { name: "dd" }, module: "HTML" }],
},
deletion: { relatedConcepts: [] },
dialog: {
relatedConcepts: [{ concept: { name: "dialog" }, module: "HTML" }],
},
directory: { relatedConcepts: [{ module: "DAISY Guide" }] },
"doc-abstract": {
relatedConcepts: [
{ concept: { name: "abstract [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-acknowledgments": {
relatedConcepts: [
{ concept: { name: "acknowledgments [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-afterword": {
relatedConcepts: [
{ concept: { name: "afterword [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-appendix": {
relatedConcepts: [
{ concept: { name: "appendix [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-backlink": {
relatedConcepts: [
{ concept: { name: "referrer [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-biblioentry": {
relatedConcepts: [
{ concept: { name: "EPUB biblioentry [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-bibliography": {
relatedConcepts: [
{ concept: { name: "bibliography [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-biblioref": {
relatedConcepts: [
{ concept: { name: "biblioref [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-chapter": {
relatedConcepts: [
{ concept: { name: "chapter [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-colophon": {
relatedConcepts: [
{ concept: { name: "colophon [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-conclusion": {
relatedConcepts: [
{ concept: { name: "conclusion [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-cover": {
relatedConcepts: [
{ concept: { name: "cover [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-credit": {
relatedConcepts: [
{ concept: { name: "credit [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-credits": {
relatedConcepts: [
{ concept: { name: "credits [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-dedication": {
relatedConcepts: [
{ concept: { name: "dedication [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-endnote": {
relatedConcepts: [
{ concept: { name: "rearnote [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-endnotes": {
relatedConcepts: [
{ concept: { name: "rearnotes [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-epigraph": {
relatedConcepts: [
{ concept: { name: "epigraph [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-epilogue": {
relatedConcepts: [
{ concept: { name: "epilogue [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-errata": {
relatedConcepts: [
{ concept: { name: "errata [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-example": { relatedConcepts: [] },
"doc-footnote": {
relatedConcepts: [
{ concept: { name: "footnote [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-foreword": {
relatedConcepts: [
{ concept: { name: "foreword [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-glossary": {
relatedConcepts: [
{ concept: { name: "glossary [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-glossref": {
relatedConcepts: [
{ concept: { name: "glossref [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-index": {
relatedConcepts: [
{ concept: { name: "index [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-introduction": {
relatedConcepts: [
{ concept: { name: "introduction [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-noteref": {
relatedConcepts: [
{ concept: { name: "noteref [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-notice": {
relatedConcepts: [
{ concept: { name: "notice [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-pagebreak": {
relatedConcepts: [
{ concept: { name: "pagebreak [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-pagelist": {
relatedConcepts: [
{ concept: { name: "page-list [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-part": {
relatedConcepts: [{ concept: { name: "part [EPUB-SSV]" }, module: "EPUB" }],
},
"doc-preface": {
relatedConcepts: [
{ concept: { name: "preface [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-prologue": {
relatedConcepts: [
{ concept: { name: "prologue [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-pullquote": {
relatedConcepts: [
{ concept: { name: "pullquote [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-qna": {
relatedConcepts: [{ concept: { name: "qna [EPUB-SSV]" }, module: "EPUB" }],
},
"doc-subtitle": {
relatedConcepts: [
{ concept: { name: "subtitle [EPUB-SSV]" }, module: "EPUB" },
],
},
"doc-tip": {
relatedConcepts: [{ concept: { name: "help [EPUB-SSV]" }, module: "EPUB" }],
},
"doc-toc": {
relatedConcepts: [{ concept: { name: "toc [EPUB-SSV]" }, module: "EPUB" }],
},
document: {
relatedConcepts: [
{ concept: { name: "Device Independence Delivery Unit" } },
{ concept: { name: "body" }, module: "HTML" },
],
},
emphasis: { relatedConcepts: [] },
feed: { relatedConcepts: [] },
figure: {
relatedConcepts: [{ concept: { name: "figure" }, module: "HTML" }],
},
form: {
relatedConcepts: [
{
concept: {
attributes: [{ constraints: ["set"], name: "aria-label" }],
name: "form",
},
module: "HTML",
},
{
concept: {
attributes: [{ constraints: ["set"], name: "aria-labelledby" }],
name: "form",
},
module: "HTML",
},
{
concept: {
attributes: [{ constraints: ["set"], name: "name" }],
name: "form",
},
module: "HTML",
},
],
},
generic: {
relatedConcepts: [
{ concept: { name: "span" }, module: "HTML" },
{ concept: { name: "div" }, module: "HTML" },
],
},
grid: {
relatedConcepts: [
{
concept: {
attributes: [{ name: "role", value: "grid" }],
name: "table",
},
module: "HTML",
},
],
},
gridcell: {
relatedConcepts: [
{
concept: {
attributes: [{ name: "role", value: "gridcell" }],
name: "td",
},
module: "HTML",
},
],
},
group: {
relatedConcepts: [
{ concept: { name: "details" }, module: "HTML" },
{ concept: { name: "fieldset" }, module: "HTML" },
{ concept: { name: "optgroup" }, module: "HTML" },
],
},
heading: {
relatedConcepts: [
{ concept: { name: "h1" }, module: "HTML" },
{ concept: { name: "h2" }, module: "HTML" },
{ concept: { name: "h3" }, module: "HTML" },
{ concept: { name: "h4" }, module: "HTML" },
{ concept: { name: "h5" }, module: "HTML" },
{ concept: { name: "h6" }, module: "HTML" },
],
},
img: {
relatedConcepts: [
{
concept: {
attributes: [{ constraints: ["set"], name: "alt" }],
name: "img",
},
module: "HTML",
},
{
concept: {
attributes: [{ constraints: ["undefined"], name: "alt" }],
name: "img",
},
module: "HTML",
},
{ concept: { name: "imggroup" }, module: "DTB" },
],
},
input: {
relatedConcepts: [{ concept: { name: "input" }, module: "XForms" }],
},
insertion: { relatedConcepts: [] },
landmark: { relatedConcepts: [] },
link: {
relatedConcepts: [
{
concept: { attributes: [{ name: "href" }], name: "a" },
module: "HTML",
},
{
concept: { attributes: [{ name: "href" }], name: "area" },
module: "HTML",
},
{
concept: { attributes: [{ name: "href" }], name: "link" },
module: "HTML",
},
],
},
list: {
relatedConcepts: [
{ concept: { name: "menu" }, module: "HTML" },
{ concept: { name: "ol" }, module: "HTML" },
{ concept: { name: "ul" }, module: "HTML" },
],
},
listbox: {
relatedConcepts: [
{
concept: {
attributes: [
{ constraints: [">1"], name: "size" },
{ name: "multiple" },
],
name: "select",
},
module: "HTML",
},
{
concept: {
attributes: [{ constraints: [">1"], name: "size" }],
name: "select",
},
module: "HTML",
},
{
concept: { attributes: [{ name: "multiple" }], name: "select" },
module: "HTML",
},
{ concept: { name: "datalist" }, module: "HTML" },
{ concept: { name: "list" }, module: "ARIA" },
{ concept: { name: "select" }, module: "XForms" },
],
},
listitem: {
relatedConcepts: [
{
concept: {
constraints: ["direct descendant of ol, ul or menu"],
name: "li",
},
module: "HTML",
},
{ concept: { name: "item" }, module: "XForms" },
],
},
log: { relatedConcepts: [] },
main: {
relatedConcepts: [{ concept: { name: "main" }, module: "HTML" }],
},
marquee: { relatedConcepts: [] },
math: {
relatedConcepts: [{ concept: { name: "math" }, module: "HTML" }],
},
menu: {
relatedConcepts: [
{ concept: { name: "MENU" }, module: "JAPI" },
{ concept: { name: "list" }, module: "ARIA" },
{ concept: { name: "select" }, module: "XForms" },
{ concept: { name: "sidebar" }, module: "DTB" },
],
},
menubar: {
relatedConcepts: [{ concept: { name: "toolbar" }, module: "ARIA" }],
},
menuitem: {
relatedConcepts: [
{ concept: { name: "MENU_ITEM" }, module: "JAPI" },
{ concept: { name: "listitem" }, module: "ARIA" },
{ concept: { name: "menuitem" }, module: "HTML" },
{ concept: { name: "option" }, module: "ARIA" },
],
},
menuitemcheckbox: {
relatedConcepts: [{ concept: { name: "menuitem" }, module: "ARIA" }],
},
menuitemradio: {
relatedConcepts: [{ concept: { name: "menuitem" }, module: "ARIA" }],
},
meter: { relatedConcepts: [] },
navigation: {
relatedConcepts: [{ concept: { name: "nav" }, module: "HTML" }],
},
none: { relatedConcepts: [] },
note: { relatedConcepts: [] },
option: {
relatedConcepts: [
{ concept: { name: "item" }, module: "XForms" },
{ concept: { name: "listitem" }, module: "ARIA" },
{ concept: { name: "option" }, module: "HTML" },
],
},
paragraph: { relatedConcepts: [] },
presentation: { relatedConcepts: [] },
progressbar: {
relatedConcepts: [
{ concept: { name: "progress" }, module: "HTML" },
{ concept: { name: "status" }, module: "ARIA" },
],
},
radio: {
relatedConcepts: [
{
concept: {
attributes: [{ name: "type", value: "radio" }],
name: "input",
},
module: "HTML",
},
],
},
radiogroup: {
relatedConcepts: [{ concept: { name: "list" }, module: "ARIA" }],
},
range: { relatedConcepts: [] },
region: {
relatedConcepts: [
{
concept: {
attributes: [{ constraints: ["set"], name: "aria-label" }],
name: "section",
},
module: "HTML",
},
{
concept: {
attributes: [{ constraints: ["set"], name: "aria-labelledby" }],
name: "section",
},
module: "HTML",
},
{
concept: { name: "Device Independence Glossart perceivable unit" },
},
{ concept: { name: "frame" }, module: "HTML" },
],
},
roletype: {
relatedConcepts: [
{ concept: { name: "rel" }, module: "HTML" },
{ concept: { name: "role" }, module: "XHTML" },
{ concept: { name: "type" }, module: "Dublin Core" },
],
},
row: {
relatedConcepts: [{ concept: { name: "tr" }, module: "HTML" }],
},
rowgroup: {
relatedConcepts: [
{ concept: { name: "tbody" }, module: "HTML" },
{ concept: { name: "tfoot" }, module: "HTML" },
{ concept: { name: "thead" }, module: "HTML" },
],
},
rowheader: {
relatedConcepts: [
{
concept: {
attributes: [{ name: "scope", value: "row" }],
name: "th",
},
module: "HTML",
},
],
},
scrollbar: { relatedConcepts: [] },
search: { relatedConcepts: [] },
searchbox: {
relatedConcepts: [
{
concept: {
attributes: [
{ constraints: ["undefined"], name: "list" },
{ name: "type", value: "search" },
],
name: "input",
},
module: "HTML",
},
],
},
section: {
relatedConcepts: [
{ concept: { name: "frontmatter" }, module: "DTB" },
{ concept: { name: "level" }, module: "DTB" },
{ concept: { name: "level" }, module: "SMIL" },
],
},
sectionhead: { relatedConcepts: [] },
select: { relatedConcepts: [] },
separator: {
relatedConcepts: [{ concept: { name: "hr" }, module: "HTML" }],
},
slider: {
relatedConcepts: [
{
concept: {
attributes: [{ name: "type", value: "range" }],
name: "input",
},
module: "HTML",
},
],
},
spinbutton: {
relatedConcepts: [
{
concept: {
attributes: [{ name: "type", value: "number" }],
name: "input",
},
module: "HTML",
},
],
},
status: {
relatedConcepts: [{ concept: { name: "output" }, module: "HTML" }],
},
strong: { relatedConcepts: [] },
structure: { relatedConcepts: [] },
subscript: { relatedConcepts: [] },
superscript: { relatedConcepts: [] },
switch: {
relatedConcepts: [{ concept: { name: "button" }, module: "ARIA" }],
},
tab: { relatedConcepts: [] },
table: {
relatedConcepts: [{ concept: { name: "table" }, module: "HTML" }],
},
tablist: {
relatedConcepts: [{ module: "DAISY", concept: { name: "guide" } }],
},
tabpanel: { relatedConcepts: [] },
term: {
relatedConcepts: [{ concept: { name: "dfn" }, module: "HTML" }],
},
textbox: {
relatedConcepts: [
{
concept: {
attributes: [
{ constraints: ["undefined"], name: "type" },
{ constraints: ["undefined"], name: "list" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["undefined"], name: "list" },
{ name: "type", value: "email" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["undefined"], name: "list" },
{ name: "type", value: "tel" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["undefined"], name: "list" },
{ name: "type", value: "text" },
],
name: "input",
},
module: "HTML",
},
{
concept: {
attributes: [
{ constraints: ["undefined"], name: "list" },
{ name: "type", value: "url" },
],
name: "input",
},
module: "HTML",
},
{ concept: { name: "input" }, module: "XForms" },
{ concept: { name: "textarea" }, module: "HTML" },
],
},
time: { relatedConcepts: [] },
timer: { relatedConcepts: [] },
toolbar: {
relatedConcepts: [{ concept: { name: "menubar" }, module: "ARIA" }],
},
tooltip: { relatedConcepts: [] },
tree: { relatedConcepts: [] },
treegrid: { relatedConcepts: [] },
treeitem: { relatedConcepts: [] },
widget: { relatedConcepts: [] },
window: { relatedConcepts: [] },
});
window.PARSE_ROLE_FLAGS = {
/**
* aria-query did previously not try to convert default props to numbers
* The following flag can enable this behavior.
*/
enableNumericDefaultValues: false,
};
function main(ariaQuery) {
const roles = Array.from(
document.querySelector("dl#index_role").querySelectorAll("dt a"),
(anchor) => {
return anchor.textContent;
}
)
.map((role) => {
return role.replace(/\s+\(abstract role\)\s*/, "");
});
const ariaGraphics = Object.fromEntries(
roles
.map((role) => {
//console.groupCollapsed(role)
const data = computeRoleData(role);
//console.groupEnd(role)
// relatedConcepts are differently used in aria-query
data.relatedConcepts =
ariaQuery[role]?.relatedConcepts ?? data.relatedConcepts;
return [role, data];
})
.sort((a, b) => a[0].localeCompare(b[0]))
);
console.log(ariaGraphics);
return JSON.stringify(ariaGraphics, null, 2);
}
function computeRoleData(role) {
const featureTable = document.querySelector(
`section#${role} table.role-features`
);
if (featureTable === null) {
throw new TypeError(`No feature table for role '${role}'`);
}
const defaultValues = computeDefaultValues(featureTable);
const data = {
abstract: computeAbstract(featureTable),
accessibleNameRequired: computeAccessibleNameRequired(featureTable),
childrenPresentational: computeChildrenPresentational(featureTable),
nameFrom: computeNameFrom(featureTable).sort((a, b) => a.localeCompare(b)),
prohibitedProps: computeProhibitedProps(featureTable).sort((a, b) =>
a.localeCompare(b)
),
props: computeProps(featureTable).sort((a, b) => {
if (Array.isArray(a) && Array.isArray(b)) {
return a[0].localeCompare(b[0]);
}
// sort props with default values last
if (Array.isArray(a)) {
return 1;
}
if (Array.isArray(b)) {
return -1;
}
return a.localeCompare(b);
}),
relatedConcepts: computeRelatedConcepts(featureTable),
requiredContextRole: computeRequiredContextRole(featureTable),
requiredOwnedElements: computeRequiredOwnedElements(featureTable),
requiredProps: computeRequiredProps(featureTable).sort((a, b) => {
if (Array.isArray(a) && Array.isArray(b)) {
return a[0].localeCompare(b[0]);
}
// sort props with default values last
if (Array.isArray(a)) {
return 1;
}
if (Array.isArray(b)) {
return -1;
}
return a.localeCompare(b);
}),
superClass: computeSuperClass(featureTable),
};
return data;
}
function computeSuperClass(featureTable) {
const value = featureTable.querySelector(".role-parent");
if (value !== null && value.querySelector("ul") !== null) {
return Array.from(value.querySelectorAll("li")).map((listitem) =>
listitem.textContent.trim()
);
}
return value === null ? [] : [value.textContent.trim()];
}
function computeAccessibleNameRequired(featureTable) {
return Boolean(
featureTable.querySelector(".role-namerequired")?.textContent === "True"
);
}
function computeNameFrom(featureTable) {
const value = featureTable.querySelector(".role-namefrom");
if (value.querySelector("ul") !== null) {
return Array.from(value.querySelectorAll("li"))
.map((listitem) => listitem.textContent)
.filter((naming) => naming !== "n/a");
}
return value.textContent === "n/a" ? [] : [value.textContent];
}
function computeRequiredContextRole(featureTable) {
const value = featureTable.querySelector(".role-scope");
if (value !== null && value.querySelector("ul") !== null) {
return Array.from(value.querySelectorAll("li")).map(
(listitem) => listitem.textContent
);
}
return value === null ? [] : [value.textContent];
}
function computeRequiredOwnedElements(featureTable) {
const value = featureTable.querySelector(".role-mustcontain");
let ownedElements = [];
if (value !== null && value.querySelector("ul") !== null) {
ownedElements = Array.from(value.querySelectorAll("li")).map(
(listitem) => listitem.textContent
);
} else if (value !== null) {
ownedElements = [value.textContent];
}
return ownedElements
.map((element) => {
// e.g. "rowgroup → row" => ["row", "rowgroup"]
if (element.indexOf("→") !== -1) {
return element
.split("→")
.map((node) => node.trim())
.reverse();
}
return element;
})
.map((elementOrArray) => {
// consistently return arrays
// https://github.com/A11yance/aria-query/pull/46#discussion_r439229900
if (Array.isArray(elementOrArray)) {
return elementOrArray;
}
return [elementOrArray];
});
}
function computeRequiredProps(featureTable) {
const props = [
...coerceListitems(
featureTable.querySelector(".role-required-properties")
).map((listitem) => {
return listitem.textContent;
}),
...coerceListitems(featureTable.querySelector(".role-inherited"))
.filter((listitem) => {
return listitem.textContent.indexOf("(required)") !== -1;
})
.map((listitem) => {
return listitem.textContent.split(" ")[0].trim();
}),
];
const defaultValues = computeDefaultValues(featureTable);
return props.map((prop) => {
if (defaultValues[prop] !== undefined) {
return [prop, defaultValues[prop]];
}
return prop;
});
}
function computeAbstract(featureTable) {
return Boolean(
featureTable.querySelector(".role-abstract")?.textContent === "True"
);
}
function computeRelatedConcepts(featureTable) {
const concepts = Array.from(
featureTable.querySelectorAll(".role-related li")
);
return concepts.map((listitem) => {
const htmlMatch = listitem.textContent.match(/(.*) in \[HTML\]/);
if (htmlMatch !== null) {
return {
module: "HTML",
concept: {
name: htmlMatch[1],
},
};
}
const name = listitem.textContent;
const moduleName = name.startsWith('graphics') ? 'GRAPHICS' : 'ARIA'
return {
module: moduleName,
concept: {
name,
},
};
});
}
function computeDefaultValues(featureTable) {
const defaultValues = {};
const matchedDefaultValues = featureTable
.querySelector(".implicit-values")
?.textContent.matchAll(/Default for (.+?) is (.+?)\./g);
if (matchedDefaultValues !== undefined) {
for (const match of matchedDefaultValues) {
defaultValues[match[1]] = match[2];
if (PARSE_ROLE_FLAGS.enableNumericDefaultValues) {
const valueAsString = match[2];
const valueAsNumber = parseInt(valueAsString, 10);
defaultValues[match[1]] = Number.isNaN(valueAsNumber)
? valueAsString
: valueAsNumber;
}
}
}
return defaultValues;
}
function computeProps(featureTable) {
const props = [
...coerceListitems(featureTable.querySelector(".role-properties")),
...coerceListitems(featureTable.querySelector(".role-inherited")),
]
.filter((listitem) => {
return listitem.textContent.indexOf("deprecated") === -1;
})
.map((listitem) => listitem.textContent.trim().split(" ")[0].trim());
const defaultValues = computeDefaultValues(featureTable);
return (
props
.concat(
computeRequiredProps(featureTable).filter(
(requiredProp) => props.includes(requiredProp) === false
)
)
.map((prop) => {
if (defaultValues[prop] !== undefined) {
return [prop, defaultValues[prop]];
}
return prop;
})
// aria spec editorial issue
.concat(
Object.entries(defaultValues).filter(([prop]) => {
return false;
return props.includes(prop) === false;
})
)
);
}
function computeChildrenPresentational(featureTable) {
return Boolean(
featureTable.querySelector(".role-childpresentational")?.textContent ===
"True"
);
}
function coerceListitems(container) {
if (container === null) {
return [];
}
if (container.querySelector("ul") !== null) {
return Array.from(container.querySelectorAll("li"));
}
return [container];
}
function computeProhibitedProps(featureTable) {
return coerceListitems(featureTable.querySelector(".role-disallowed")).map(
(listitem) => listitem.textContent.trim().split(" ")[0].trim()
);
}
main({});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment