chore: add simple dom util (#32332)

This commit is contained in:
Pavel Feldman 2024-08-26 16:28:40 -07:00 committed by GitHub
parent 6f55b57e5a
commit 177576a51b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 82 additions and 0 deletions

View File

@ -0,0 +1,82 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils';
import { beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName } from './roleUtils';
import { isElementVisible } from './domUtils';
const leafRoles = new Set([
'button',
'checkbox',
'combobox',
'link',
'textbox',
]);
export function simpleDom(document: Document): { markup: string, elements: Map<string, Element> } {
const normalizeWhitespace = (text: string) => text.replace(/[\s\n]+/g, match => match.includes('\n') ? '\n' : ' ');
const tokens: string[] = [];
const idMap = new Map<string, Element>();
let lastId = 0;
const visit = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) {
tokens.push(node.nodeValue!);
return;
}
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
if (element.nodeName === 'SCRIPT' || element.nodeName === 'STYLE' || element.nodeName === 'NOSCRIPT')
return;
if (isElementVisible(element)) {
const role = getAriaRole(element) as string;
if (role && leafRoles.has(role)) {
let value: string | undefined;
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA')
value = (element as HTMLInputElement | HTMLTextAreaElement).value;
const name = getElementAccessibleName(element, false);
const structuralId = String(++lastId);
idMap.set(structuralId, element);
tokens.push(renderTag(role, name, structuralId, { value }));
return;
}
}
for (let child = element.firstChild; child; child = child.nextSibling)
visit(child);
}
};
beginAriaCaches();
try {
visit(document.body);
} finally {
endAriaCaches();
}
return {
markup: normalizeWhitespace(tokens.join(' ')),
elements: idMap
};
}
function renderTag(role: string, name: string, id: string, params?: { value?: string }): string {
const escapedTextContent = escapeHTML(name);
const escapedValue = escapeHTMLAttribute(params?.value || '');
switch (role) {
case 'button': return `<button id="${id}">${escapedTextContent}</button>`;
case 'link': return `<a id="${id}">${escapedTextContent}</a>`;
case 'textbox': return `<input id="${id}" title="${escapedTextContent}" value="${escapedValue}"></input>`;
}
return `<div role=${role} id="${id}">${escapedTextContent}</div>`;
}