ivya
Version:
Fork of Playwright's locator resolution
1,352 lines (1,343 loc) • 46.5 kB
JavaScript
import { A as kAriaPressedRoles, C as getElementAccessibleName, D as kAriaDisabledRoles, E as kAriaCheckedRoles, F as normalizeWhiteSpace, I as _defineProperty, M as getElementComputedStyle, O as kAriaExpandedRoles, T as isElementHiddenForAria, _ as getAriaLevel, b as getAriaSelected, d as endAriaCaches, f as getAriaChecked, j as kAriaSelectedRoles, k as kAriaLevelRoles, m as getAriaExpanded, p as getAriaDisabled, u as beginAriaCaches, v as getAriaPressed, w as getPseudoContent, y as getAriaRole } from "../src-c2dVUPx0.js";
//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) {
__defProp(target, name, {
get: all[name],
enumerable: true
});
}
if (!no_symbols) {
__defProp(target, Symbol.toStringTag, { value: "Module" });
}
return target;
};
//#endregion
//#region src/aria/folk/isomorphic/ariaSnapshot.ts
function parseAriaSnapshotUnsafe(yaml, text, options = {}) {
const result = parseAriaSnapshot(yaml, text, options);
if (result.errors.length) throw new Error(result.errors[0].message);
return result.fragment;
}
function parseAriaSnapshot(yaml, text, options = {}) {
var _fragment$children;
const lineCounter = new yaml.LineCounter();
const parseOptions = {
keepSourceTokens: true,
lineCounter,
...options
};
const yamlDoc = yaml.parseDocument(text, parseOptions);
const errors = [];
const convertRange = (range) => {
return [lineCounter.linePos(range[0]), lineCounter.linePos(range[1])];
};
const addError = (error) => {
errors.push({
message: error.message,
range: [lineCounter.linePos(error.pos[0]), lineCounter.linePos(error.pos[1])]
});
};
const convertSeq = (container, seq) => {
for (const item of seq.items) {
if (item instanceof yaml.Scalar && typeof item.value === "string") {
const childNode = KeyParser.parse(item, parseOptions, errors);
if (childNode) {
container.children = container.children || [];
container.children.push(childNode);
}
continue;
}
if (item instanceof yaml.YAMLMap) {
convertMap(container, item);
continue;
}
errors.push({
message: "Sequence items should be strings or maps",
range: convertRange(item.range || seq.range)
});
}
};
const convertMap = (container, map) => {
for (const entry of map.items) {
container.children = container.children || [];
if (!(entry.key instanceof yaml.Scalar && typeof entry.key.value === "string")) {
errors.push({
message: "Only string keys are supported",
range: convertRange(entry.key.range || map.range)
});
continue;
}
const key = entry.key;
const value = entry.value;
if (key.value === "text") {
if (!(value instanceof yaml.Scalar && typeof value.value === "string")) {
errors.push({
message: "Text value should be a string",
range: convertRange(entry.value.range || map.range)
});
continue;
}
container.children.push({
kind: "text",
text: textValue(value.value)
});
continue;
}
if (key.value === "/children") {
if (!(value instanceof yaml.Scalar && typeof value.value === "string") || value.value !== "contain" && value.value !== "equal" && value.value !== "deep-equal") {
errors.push({
message: "Strict value should be \"contain\", \"equal\" or \"deep-equal\"",
range: convertRange(entry.value.range || map.range)
});
continue;
}
container.containerMode = value.value;
continue;
}
if (key.value.startsWith("/")) {
var _container$props;
if (!(value instanceof yaml.Scalar && typeof value.value === "string")) {
errors.push({
message: "Property value should be a string",
range: convertRange(entry.value.range || map.range)
});
continue;
}
container.props = (_container$props = container.props) !== null && _container$props !== void 0 ? _container$props : {};
container.props[key.value.slice(1)] = textValue(value.value);
continue;
}
const childNode = KeyParser.parse(key, parseOptions, errors);
if (!childNode) continue;
if (value instanceof yaml.Scalar) {
const type = typeof value.value;
if (type !== "string" && type !== "number" && type !== "boolean") {
errors.push({
message: "Node value should be a string or a sequence",
range: convertRange(entry.value.range || map.range)
});
continue;
}
container.children.push({
...childNode,
children: [{
kind: "text",
text: textValue(String(value.value))
}]
});
continue;
}
if (value instanceof yaml.YAMLSeq) {
container.children.push(childNode);
convertSeq(childNode, value);
continue;
}
errors.push({
message: "Map values should be strings or sequences",
range: convertRange(entry.value.range || map.range)
});
}
};
const fragment = {
kind: "role",
role: "fragment"
};
yamlDoc.errors.forEach(addError);
if (errors.length) return {
errors,
fragment
};
if (!yamlDoc.contents) return {
fragment,
errors: []
};
if (!(yamlDoc.contents instanceof yaml.YAMLSeq)) errors.push({
message: "Aria snapshot must be a YAML sequence, elements starting with \" -\"",
range: convertRange(yamlDoc.contents.range)
});
if (errors.length) return {
errors,
fragment
};
convertSeq(fragment, yamlDoc.contents);
if (errors.length) return {
errors,
fragment: emptyFragment
};
if (((_fragment$children = fragment.children) === null || _fragment$children === void 0 ? void 0 : _fragment$children.length) === 1 && (!fragment.containerMode || fragment.containerMode === "contain")) return {
fragment: fragment.children[0],
errors: []
};
return {
fragment,
errors: []
};
}
const emptyFragment = {
kind: "role",
role: "fragment"
};
function normalizeWhitespace(text) {
return text.replace(/[\u200b\u00ad]/g, "").replace(/[\r\n\s\t]+/g, " ").trim();
}
function textValue(value) {
return {
raw: value,
normalized: normalizeWhitespace(value)
};
}
var KeyParser = class KeyParser {
static parse(text, options, errors) {
try {
return new KeyParser(text.value)._parse();
} catch (e) {
if (e instanceof ParserError) {
const message = options.prettyErrors === false ? e.message : `${e.message}:\n\n${text.value}\n${" ".repeat(e.pos)}^\n`;
errors.push({
message,
range: [options.lineCounter.linePos(text.range[0]), options.lineCounter.linePos(text.range[0] + e.pos)]
});
return null;
}
throw e;
}
}
constructor(input) {
_defineProperty(this, "_input", void 0);
_defineProperty(this, "_pos", void 0);
_defineProperty(this, "_length", void 0);
this._input = input;
this._pos = 0;
this._length = input.length;
}
_peek() {
return this._input[this._pos] || "";
}
_next() {
if (this._pos < this._length) return this._input[this._pos++];
return null;
}
_eof() {
return this._pos >= this._length;
}
_isWhitespace() {
return !this._eof() && /\s/.test(this._peek());
}
_skipWhitespace() {
while (this._isWhitespace()) this._pos++;
}
_readIdentifier(type) {
if (this._eof()) this._throwError(`Unexpected end of input when expecting ${type}`);
const start = this._pos;
while (!this._eof() && /[a-zA-Z]/.test(this._peek())) this._pos++;
return this._input.slice(start, this._pos);
}
_readString() {
let result = "";
let escaped = false;
while (!this._eof()) {
const ch = this._next();
if (escaped) {
result += ch;
escaped = false;
} else if (ch === "\\") escaped = true;
else if (ch === "\"") return result;
else result += ch;
}
this._throwError("Unterminated string");
}
_throwError(message, offset = 0) {
throw new ParserError(message, offset || this._pos);
}
_readRegex() {
let result = "";
let escaped = false;
let insideClass = false;
while (!this._eof()) {
const ch = this._next();
if (escaped) {
result += ch;
escaped = false;
} else if (ch === "\\") {
escaped = true;
result += ch;
} else if (ch === "/" && !insideClass) return { pattern: result };
else if (ch === "[") {
insideClass = true;
result += ch;
} else if (ch === "]" && insideClass) {
result += ch;
insideClass = false;
} else result += ch;
}
this._throwError("Unterminated regex");
}
_readStringOrRegex() {
const ch = this._peek();
if (ch === "\"") {
this._next();
return normalizeWhitespace(this._readString());
}
if (ch === "/") {
this._next();
return this._readRegex();
}
return null;
}
_readAttributes(result) {
let errorPos = this._pos;
while (true) {
this._skipWhitespace();
if (this._peek() === "[") {
this._next();
this._skipWhitespace();
errorPos = this._pos;
const flagName = this._readIdentifier("attribute");
this._skipWhitespace();
let flagValue = "";
if (this._peek() === "=") {
this._next();
this._skipWhitespace();
errorPos = this._pos;
while (this._peek() !== "]" && !this._isWhitespace() && !this._eof()) flagValue += this._next();
}
this._skipWhitespace();
if (this._peek() !== "]") this._throwError("Expected ]");
this._next();
this._applyAttribute(result, flagName, flagValue || "true", errorPos);
} else break;
}
}
_parse() {
this._skipWhitespace();
const role = this._readIdentifier("role");
if (role === "fragment") this._throwError("Invalid role \"fragment\"");
this._skipWhitespace();
const result = {
kind: "role",
role,
name: this._readStringOrRegex() || void 0
};
this._readAttributes(result);
this._skipWhitespace();
if (!this._eof()) this._throwError("Unexpected input");
return result;
}
_applyAttribute(node, key, value, errorPos) {
if (key === "checked") {
this._assert(value === "true" || value === "false" || value === "mixed", "Value of \"checked\" attribute must be a boolean or \"mixed\"", errorPos);
node.checked = value === "true" ? true : value === "false" ? false : "mixed";
return;
}
if (key === "disabled") {
this._assert(value === "true" || value === "false", "Value of \"disabled\" attribute must be a boolean", errorPos);
node.disabled = value === "true";
return;
}
if (key === "expanded") {
this._assert(value === "true" || value === "false", "Value of \"expanded\" attribute must be a boolean", errorPos);
node.expanded = value === "true";
return;
}
if (key === "active") {
this._assert(value === "true" || value === "false", "Value of \"active\" attribute must be a boolean", errorPos);
node.active = value === "true";
return;
}
if (key === "level") {
this._assert(!Number.isNaN(Number(value)), "Value of \"level\" attribute must be a number", errorPos);
node.level = Number(value);
return;
}
if (key === "pressed") {
this._assert(value === "true" || value === "false" || value === "mixed", "Value of \"pressed\" attribute must be a boolean or \"mixed\"", errorPos);
node.pressed = value === "true" ? true : value === "false" ? false : "mixed";
return;
}
if (key === "selected") {
this._assert(value === "true" || value === "false", "Value of \"selected\" attribute must be a boolean", errorPos);
node.selected = value === "true";
return;
}
this._assert(false, `Unsupported attribute [${key}]`, errorPos);
}
_assert(value, message, valuePos) {
if (!value) this._throwError(message || "Assertion error", valuePos);
}
};
var ParserError = class extends Error {
constructor(message, pos) {
super(message);
_defineProperty(this, "pos", void 0);
this.pos = pos;
}
};
function matchesStringOrRegex(text, template) {
if (!template) return true;
if (!text) return false;
if (typeof template === "string") return text === template;
return !!text.match(new RegExp(template.pattern));
}
function matchesTextValue(text, template) {
if (!(template === null || template === void 0 ? void 0 : template.normalized)) return true;
if (!text) return false;
if (text === template.normalized) return true;
if (text === template.raw) return true;
const regex = cachedRegex(template);
if (regex) return !!text.match(regex);
return false;
}
const cachedRegexSymbol = Symbol("cachedRegex");
function cachedRegex(template) {
if (template[cachedRegexSymbol] !== void 0) return template[cachedRegexSymbol];
const { raw } = template;
const canBeRegex = raw.startsWith("/") && raw.endsWith("/") && raw.length > 1;
let regex;
try {
regex = canBeRegex ? new RegExp(raw.slice(1, -1)) : null;
} catch (_unused) {
regex = null;
}
template[cachedRegexSymbol] = regex;
return regex;
}
/**
* Full-depth recursive match: checks the node, its attributes, and all
* descendants against the template. The `isDeepEqual` parameter does NOT
* control recursion depth — matching is always full-depth. It only
* controls the *child list mode* when `template.containerMode` is unset:
* - `false` → default contain semantics (template children are a subsequence)
* - `true` → inherited deep-equal (positional 1:1, propagated to descendants)
* When `template.containerMode` is explicitly set (e.g. via `/children: equal`),
* it takes precedence over `isDeepEqual`.
*/
function matchesNode(node, template, isDeepEqual) {
if (typeof node === "string" && template.kind === "text") return matchesTextValue(node, template.text);
if (node === null || typeof node !== "object" || template.kind !== "role") return false;
if (template.role !== "fragment" && template.role !== node.role) return false;
if (template.checked !== void 0 && template.checked !== node.checked) return false;
if (template.disabled !== void 0 && template.disabled !== node.disabled) return false;
if (template.expanded !== void 0 && template.expanded !== node.expanded) return false;
if (template.level !== void 0 && template.level !== node.level) return false;
if (template.pressed !== void 0 && template.pressed !== node.pressed) return false;
if (template.selected !== void 0 && template.selected !== node.selected) return false;
if (!matchesStringOrRegex(node.name, template.name)) return false;
if (template.props) {
for (const [key, value] of Object.entries(template.props)) if (!matchesTextValue(node.props[key] || "", value)) return false;
}
if (template.containerMode === "contain") return containsList(node.children || [], template.children || []);
if (template.containerMode === "equal") return listEqual(node.children || [], template.children || [], false);
if (template.containerMode === "deep-equal" || isDeepEqual) return listEqual(node.children || [], template.children || [], true);
return containsList(node.children || [], template.children || []);
}
function listEqual(children, template, isDeepEqual) {
if (template.length !== children.length) return false;
for (let i = 0; i < template.length; ++i) if (!matchesNode(children[i], template[i], isDeepEqual)) return false;
return true;
}
function containsList(children, template) {
if (template.length > children.length) return false;
const cc = children.slice();
const tt = template.slice();
for (const t of tt) {
let c = cc.shift();
while (c) {
if (matchesNode(c, t, false)) break;
c = cc.shift();
}
if (!c) return false;
}
return true;
}
//#endregion
//#region src/aria/yaml.ts
var yaml_exports = /* @__PURE__ */ __exportAll({
LineCounter: () => LineCounter,
Scalar: () => Scalar,
YAMLError: () => YAMLError,
YAMLMap: () => YAMLMap,
YAMLSeq: () => YAMLSeq,
parseDocument: () => parseDocument
});
var Scalar = class {
constructor(value, range = [
0,
0,
0
]) {
_defineProperty(this, "value", void 0);
_defineProperty(this, "range", void 0);
this.value = value;
this.range = range;
}
};
var YAMLMap = class {
constructor(range = [
0,
0,
0
]) {
_defineProperty(this, "items", void 0);
_defineProperty(this, "range", void 0);
this.items = [];
this.range = range;
}
};
var YAMLSeq = class {
constructor(range = [
0,
0,
0
]) {
_defineProperty(this, "items", void 0);
_defineProperty(this, "range", void 0);
this.items = [];
this.range = range;
}
};
var LineCounter = class {
constructor() {
_defineProperty(this, "lineStarts", [0]);
}
addNewLine(offset) {
if (offset > this.lineStarts[this.lineStarts.length - 1]) this.lineStarts.push(offset);
}
linePos(offset) {
let low = 0;
let high = this.lineStarts.length - 1;
while (low < high) {
const mid = low + high + 1 >> 1;
if (this.lineStarts[mid] <= offset) low = mid;
else high = mid - 1;
}
return {
line: low + 1,
col: offset - this.lineStarts[low] + 1
};
}
};
var YAMLError = class extends Error {
constructor(message, pos) {
super(message);
_defineProperty(this, "pos", void 0);
this.pos = pos;
}
};
function parseDocument(text, options = {}) {
const lineCounter = options.lineCounter;
const errors = [];
if (lineCounter) {
for (let i = 0; i < text.length; i++) if (text[i] === "\n") lineCounter.addNewLine(i + 1);
}
try {
return {
contents: new Parser(text, errors).parseRoot(),
errors
};
} catch (e) {
if (e instanceof YAMLError) {
errors.push(e);
return {
contents: null,
errors
};
}
throw e;
}
}
/**
* Single-pass line-based recursive descent parser.
*
* Call graph (recursion points are parseNode calls):
*
* parseRoot
* → parseNode(minIndent)
* → parseSequence(baseIndent)
* → parseNode(baseIndent + 1) "- \n" then indented child
* → parseInlineMap(contentIndent) "- key: value"
* → parseScalarValue inline value
* → parseMapValue(contentIndent) "- key:\n" then child
* → parseNode(contentIndent + 1) indented child
* → parseNode(contentIndent) block seq at same indent
* → parseScalarValue "- scalar"
* → parseMap(baseIndent)
* → parseScalarValue inline value
* → parseMapValue "key:\n" then child
* → parseScalarValue
* → parseQuotedScalar starts with "
*/
var Parser = class {
constructor(text, errors) {
_defineProperty(this, "lines", void 0);
_defineProperty(this, "pos", void 0);
_defineProperty(this, "text", void 0);
_defineProperty(this, "errors", void 0);
this.text = text;
this.errors = errors;
this.lines = [];
this.pos = 0;
let offset = 0;
const rawLines = text.split("\n");
for (const raw of rawLines) {
const stripped = raw.replace(/\r$/, "");
const trimmed = stripped.replace(/^\s+/, "");
const indent = stripped.length - trimmed.length;
this.lines.push({
indent,
content: trimmed,
offset: offset + indent,
lineOffset: offset,
raw: stripped
});
offset += raw.length + 1;
}
while (this.lines.length > 0 && this.lines[this.lines.length - 1].content === "") this.lines.pop();
}
parseRoot() {
if (this.lines.length === 0) return null;
const result = this.parseNode(0);
this.skipEmpty();
if (this.pos < this.lines.length) {
const line = this.currentLine();
this.addError("Unexpected scalar at node end", line.offset);
}
return result;
}
currentLine() {
return this.lines[this.pos];
}
parseNode(minIndent) {
this.skipEmpty();
const line = this.currentLine();
if (!line || line.indent < minIndent) return null;
if (line.content.startsWith("- ") || line.content === "-") return this.parseSequence(line.indent);
if (this.isMapEntry(line.content)) return this.parseMap(line.indent);
this.pos++;
return this.parseScalarValue(line.content, line.offset, line);
}
parseSequence(baseIndent) {
const startLine = this.currentLine();
const seq = new YAMLSeq([
startLine.offset - startLine.indent,
0,
0
]);
let lastOffset = startLine.offset;
while (this.pos < this.lines.length) {
this.skipEmpty();
const line = this.currentLine();
if (!line || line.indent < baseIndent) break;
if (line.indent > baseIndent) {
this.addError("Bad indentation of a sequence entry", line.offset);
break;
}
if (!line.content.startsWith("- ") && line.content !== "-") break;
const dashLen = line.content === "-" ? 1 : 2;
const itemContent = line.content.slice(dashLen);
const itemOffset = line.offset + dashLen;
this.pos++;
if (itemContent === "" || itemContent.trim() === "") {
const child = this.parseNode(baseIndent + 1);
if (child) {
seq.items.push(child);
lastOffset = this.peekLastOffset();
}
} else if (this.isMapEntry(itemContent)) {
const map = this.parseInlineMap(itemContent, itemOffset, baseIndent + dashLen, line);
seq.items.push(map);
lastOffset = map.range[2];
} else {
const scalar = this.parseScalarValue(itemContent, itemOffset, line);
seq.items.push(scalar);
lastOffset = scalar.range[2];
}
}
seq.range[1] = lastOffset;
seq.range[2] = lastOffset;
return seq;
}
parseMap(baseIndent) {
const startLine = this.currentLine();
const map = new YAMLMap([
startLine.offset - startLine.indent,
0,
0
]);
let lastOffset = startLine.offset;
while (this.pos < this.lines.length) {
this.skipEmpty();
const line = this.currentLine();
if (!line || line.indent < baseIndent) break;
if (line.indent > baseIndent) {
this.addError("Bad indentation of a mapping entry", line.offset);
break;
}
if (!this.isMapEntry(line.content)) break;
const { key, valueStr, colonOffset, valueOffset } = this.splitMapEntry(line.content, line.offset);
const keyScalar = new Scalar(key, [
line.offset,
line.offset + key.length,
colonOffset
]);
this.pos++;
let value;
if (valueStr === "") value = this.parseMapValue(baseIndent, colonOffset);
else value = this.parseScalarValue(valueStr.trim(), valueOffset, line);
map.items.push({
key: keyScalar,
value
});
lastOffset = value ? this.getNodeEnd(value) : colonOffset + 1;
}
map.range[1] = lastOffset;
map.range[2] = lastOffset;
return map;
}
/**
* Parse an inline map that starts after "- " in a sequence.
* E.g. "- key: value" or "- key:\n - child"
* May continue with more entries at the same indent level.
*/
parseInlineMap(firstEntry, entryOffset, contentIndent, _sourceLine) {
const map = new YAMLMap([
entryOffset,
0,
0
]);
let lastOffset = entryOffset;
const { key, valueStr, colonOffset, valueOffset } = this.splitMapEntry(firstEntry, entryOffset);
const keyScalar = new Scalar(key, [
entryOffset,
entryOffset + key.length,
colonOffset
]);
let value;
if (valueStr === "") value = this.parseMapValue(contentIndent, colonOffset);
else value = this.parseScalarValue(valueStr.trim(), valueOffset, _sourceLine);
map.items.push({
key: keyScalar,
value
});
lastOffset = value ? this.getNodeEnd(value) : colonOffset + 1;
while (this.pos < this.lines.length) {
this.skipEmpty();
const line = this.currentLine();
if (!line || line.indent < contentIndent) break;
if (line.indent > contentIndent) break;
if (!this.isMapEntry(line.content)) break;
if (line.content.startsWith("- ")) break;
const entry = this.splitMapEntry(line.content, line.offset);
const ks = new Scalar(entry.key, [
line.offset,
line.offset + entry.key.length,
entry.colonOffset
]);
this.pos++;
let v;
if (entry.valueStr === "") v = this.parseMapValue(contentIndent, entry.colonOffset);
else v = this.parseScalarValue(entry.valueStr.trim(), entry.valueOffset, line);
map.items.push({
key: ks,
value: v
});
lastOffset = v ? this.getNodeEnd(v) : entry.colonOffset + 1;
}
map.range[1] = lastOffset;
map.range[2] = lastOffset;
return map;
}
/**
* Parse the value of a map entry when the value is on subsequent lines.
* In YAML, a block sequence can start at the same indent as the key,
* but other block collections must be indented further.
*/
parseMapValue(contentIndent, colonOffset) {
this.skipEmpty();
const nextLine = this.currentLine();
if (!nextLine || nextLine.indent < contentIndent) return new Scalar(null, [
colonOffset + 1,
colonOffset + 1,
colonOffset + 1
]);
if (nextLine.indent === contentIndent && (nextLine.content.startsWith("- ") || nextLine.content === "-")) return this.parseNode(contentIndent);
if (nextLine.indent > contentIndent) return this.parseNode(contentIndent + 1);
return new Scalar(null, [
colonOffset + 1,
colonOffset + 1,
colonOffset + 1
]);
}
getNodeEnd(node) {
return node.range[2];
}
peekLastOffset() {
if (this.pos > 0 && this.pos <= this.lines.length) {
const prev = this.lines[this.pos - 1];
return prev.lineOffset + prev.raw.length;
}
return this.text.length;
}
parseScalarValue(raw, offset, line) {
const trimmed = raw.trim();
const adjOffset = offset + raw.indexOf(trimmed);
const end = adjOffset + trimmed.length;
const lineEnd = line.lineOffset + line.raw.length;
if (trimmed.startsWith("\"")) return this.parseQuotedScalar(trimmed, adjOffset, lineEnd);
if (trimmed === "true" || trimmed === "false") return new Scalar(trimmed === "true", [
adjOffset,
end,
lineEnd
]);
if (trimmed === "null" || trimmed === "~") return new Scalar(null, [
adjOffset,
end,
lineEnd
]);
if (trimmed !== "" && isNumeric(trimmed)) return new Scalar(Number(trimmed), [
adjOffset,
end,
lineEnd
]);
return new Scalar(trimmed, [
adjOffset,
end,
lineEnd
]);
}
parseQuotedScalar(raw, offset, lineEnd) {
let result = "";
let i = 1;
while (i < raw.length) {
const ch = raw[i];
if (ch === "\\") {
i++;
if (i >= raw.length) {
this.addError("Unterminated double-quoted string", offset + i);
break;
}
const esc = raw[i];
switch (esc) {
case "n":
result += "\n";
break;
case "t":
result += " ";
break;
case "r":
result += "\r";
break;
case "\"":
result += "\"";
break;
case "\\":
result += "\\";
break;
case "/":
result += "/";
break;
default: result += esc;
}
} else if (ch === "\"") {
const end = offset + i + 1;
return new Scalar(result, [
offset,
end,
lineEnd
]);
} else result += ch;
i++;
}
this.addError("Unterminated double-quoted string", offset);
return new Scalar(result, [
offset,
offset + raw.length,
lineEnd
]);
}
skipEmpty() {
while (this.pos < this.lines.length && this.lines[this.pos].content === "") this.pos++;
}
/**
* Check if content looks like a map entry: `key: value` or `key:`.
* Must not start with "- ".
* The colon must be followed by a space or end of string.
*/
isMapEntry(content) {
return this.findMapColon(content) >= 0;
}
/**
* Find the colon index for a map entry.
* Skip colons inside double-quoted strings.
*/
findMapColon(content) {
let inQuote = false;
let escaped = false;
for (let i = 0; i < content.length; i++) {
const ch = content[i];
if (escaped) {
escaped = false;
continue;
}
if (ch === "\\" && inQuote) {
escaped = true;
continue;
}
if (ch === "\"") {
inQuote = !inQuote;
continue;
}
if (!inQuote && ch === ":") {
if (i + 1 >= content.length || content[i + 1] === " ") return i;
}
}
return -1;
}
splitMapEntry(content, baseOffset) {
const colonIdx = this.findMapColon(content);
const key = content.slice(0, colonIdx).trim();
const colonOffset = baseOffset + colonIdx;
const afterColon = content.slice(colonIdx + 1);
const valueStr = afterColon.trimStart();
const valueOffset = colonOffset + 1 + (afterColon.length - afterColon.trimStart().length);
if (key.startsWith("\"") && key.endsWith("\"")) return {
key: key.slice(1, -1),
valueStr,
colonOffset,
valueOffset
};
return {
key,
valueStr,
colonOffset,
valueOffset
};
}
addError(message, offset) {
this.errors.push(new YAMLError(message, [offset, offset + 1]));
}
};
function isNumeric(str) {
if (str === "" || str === "-" || str === "+") return false;
return /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(str);
}
//#endregion
//#region src/aria/folk/isomorphic/yaml.ts
function yamlEscapeValueIfNeeded(str) {
if (!yamlStringNeedsQuotes(str)) return str;
return "\"" + str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, (c) => {
switch (c) {
case "\\": return "\\\\";
case "\"": return "\\\"";
case "\b": return "\\b";
case "\f": return "\\f";
case "\n": return "\\n";
case "\r": return "\\r";
case " ": return "\\t";
default: return "\\x" + c.charCodeAt(0).toString(16).padStart(2, "0");
}
}) + "\"";
}
function yamlStringNeedsQuotes(str) {
if (str.length === 0) return true;
if (/^\s|\s$/.test(str)) return true;
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(str)) return true;
if (/^-/.test(str)) return true;
if (/[\n:](\s|$)/.test(str)) return true;
if (/\s#/.test(str)) return true;
if (/[\n\r]/.test(str)) return true;
if (/^[&*\],?!>|@"'#%]/.test(str)) return true;
if (/[{}`]/.test(str)) return true;
if (/^\[/.test(str)) return true;
if (!isNaN(Number(str)) || [
"y",
"n",
"yes",
"no",
"true",
"false",
"on",
"off",
"null"
].includes(str.toLowerCase())) return true;
return false;
}
//#endregion
//#region src/aria/folk/injected/ariaSnapshot.ts
const defaultBox = {
visible: true,
inline: false
};
function generateAriaTree(rootElement) {
const visited = /* @__PURE__ */ new Set();
const root = {
role: "fragment",
name: "",
children: [],
props: {},
box: defaultBox,
receivesPointerEvents: true
};
const visit = (ariaNode, node, parentElementVisible) => {
if (visited.has(node)) return;
visited.add(node);
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
if (!parentElementVisible) return;
const text = node.nodeValue;
if (ariaNode.role !== "textbox" && text) ariaNode.children.push(node.nodeValue || "");
return;
}
if (node.nodeType !== Node.ELEMENT_NODE) return;
const element = node;
const visible = !isElementHiddenForAria(element);
if (!visible) return;
const ariaChildren = [];
if (element.hasAttribute("aria-owns")) {
const ids = element.getAttribute("aria-owns").split(/\s+/);
for (const id of ids) {
const ownedElement = rootElement.ownerDocument.getElementById(id);
if (ownedElement) ariaChildren.push(ownedElement);
}
}
const childAriaNode = toAriaNode(element);
if (childAriaNode) ariaNode.children.push(childAriaNode);
processElement(childAriaNode || ariaNode, element, ariaChildren, visible);
};
function processElement(ariaNode, element, ariaChildren, parentElementVisible) {
var _getElementComputedSt;
const treatAsBlock = (((_getElementComputedSt = getElementComputedStyle(element)) === null || _getElementComputedSt === void 0 ? void 0 : _getElementComputedSt.display) || "inline") !== "inline" || element.nodeName === "BR" ? " " : "";
if (treatAsBlock) ariaNode.children.push(treatAsBlock);
ariaNode.children.push(getPseudoContent(element, "::before"));
const assignedNodes = element.nodeName === "SLOT" ? element.assignedNodes() : [];
if (assignedNodes.length) for (const child of assignedNodes) visit(ariaNode, child, parentElementVisible);
else {
for (let child = element.firstChild; child; child = child.nextSibling) if (!child.assignedSlot) visit(ariaNode, child, parentElementVisible);
if (element.shadowRoot) for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) visit(ariaNode, child, parentElementVisible);
}
for (const child of ariaChildren) visit(ariaNode, child, parentElementVisible);
ariaNode.children.push(getPseudoContent(element, "::after"));
if (treatAsBlock) ariaNode.children.push(treatAsBlock);
if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0]) ariaNode.children = [];
if (ariaNode.role === "link" && element.hasAttribute("href")) {
const href = element.getAttribute("href");
ariaNode.props.url = href;
}
if (ariaNode.role === "textbox" && element.hasAttribute("placeholder") && element.getAttribute("placeholder") !== ariaNode.name) {
const placeholder = element.getAttribute("placeholder");
ariaNode.props.placeholder = placeholder;
}
}
beginAriaCaches();
try {
visit(root, rootElement, true);
} finally {
endAriaCaches();
}
normalizeStringChildren(root);
return root;
}
function toAriaNode(element) {
var _roleUtils$getAriaRol;
const role = (_roleUtils$getAriaRol = getAriaRole(element)) !== null && _roleUtils$getAriaRol !== void 0 ? _roleUtils$getAriaRol : null;
if (!role || role === "presentation" || role === "none") return null;
const result = {
role,
name: normalizeWhiteSpace(getElementAccessibleName(element, false) || ""),
children: [],
props: {},
box: defaultBox,
receivesPointerEvents: true
};
if (kAriaCheckedRoles.includes(role)) result.checked = getAriaChecked(element);
if (kAriaDisabledRoles.includes(role)) result.disabled = getAriaDisabled(element) || void 0;
if (kAriaExpandedRoles.includes(role)) {
const expanded = getAriaExpanded(element);
result.expanded = expanded === "none" ? void 0 : expanded;
}
if (kAriaLevelRoles.includes(role)) result.level = getAriaLevel(element);
if (kAriaPressedRoles.includes(role)) result.pressed = getAriaPressed(element);
if (kAriaSelectedRoles.includes(role)) result.selected = getAriaSelected(element);
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
if (element.type !== "checkbox" && element.type !== "radio" && element.type !== "file") result.children = [element.value];
}
return result;
}
function normalizeStringChildren(rootA11yNode) {
const flushChildren = (buffer, normalizedChildren) => {
if (!buffer.length) return;
const text = normalizeWhiteSpace(buffer.join(""));
if (text) normalizedChildren.push(text);
buffer.length = 0;
};
const visit = (ariaNode) => {
const normalizedChildren = [];
const buffer = [];
for (const child of ariaNode.children || []) if (typeof child === "string") buffer.push(child);
else {
flushChildren(buffer, normalizedChildren);
visit(child);
normalizedChildren.push(child);
}
flushChildren(buffer, normalizedChildren);
ariaNode.children = normalizedChildren.length ? normalizedChildren : [];
if (ariaNode.children.length === 1 && ariaNode.children[0] === ariaNode.name) ariaNode.children = [];
};
visit(rootA11yNode);
}
function renderAriaProps(props, options) {
let s = "";
if (props.level) s += ` [level=${props.level}]`;
if (props.checked === true) s += " [checked]";
if (props.checked === "mixed") s += " [checked=mixed]";
if (props.disabled) s += " [disabled]";
if (props.expanded === true) s += " [expanded]";
if (props.expanded === false && (options === null || options === void 0 ? void 0 : options.renderExpandedFalse)) s += " [expanded=false]";
if (props.pressed === true) s += " [pressed]";
if (props.pressed === "mixed") s += " [pressed=mixed]";
if (props.selected) s += " [selected]";
return s;
}
function createAriaKey(ariaNode) {
let key = ariaNode.role;
if (ariaNode.name && ariaNode.name.length <= 900) key += ` ${JSON.stringify(ariaNode.name)}`;
key += renderAriaProps(ariaNode);
return key;
}
function renderNodeLines(node, indent, lines) {
const key = createAriaKey(node);
const hasProps = Object.keys(node.props).length > 0;
const singleTextChild = node.children.length === 1 && typeof node.children[0] === "string" && !hasProps ? node.children[0] : void 0;
if (!node.children.length && !hasProps) lines.push(`${indent}- ${key}`);
else if (singleTextChild !== void 0) lines.push(`${indent}- ${key}: ${yamlEscapeValueIfNeeded(singleTextChild)}`);
else {
lines.push(`${indent}- ${key}:`);
for (const [name, value] of Object.entries(node.props)) lines.push(`${indent} - /${name}: ${yamlEscapeValueIfNeeded(value)}`);
for (const child of node.children) if (typeof child === "string") {
if (child) lines.push(`${indent} - text: ${yamlEscapeValueIfNeeded(child)}`);
} else renderNodeLines(child, `${indent} `, lines);
}
}
function renderAriaTree(root) {
const lines = [];
const nodesToRender = root.role === "fragment" ? root.children : [root];
for (const nodeToRender of nodesToRender) if (typeof nodeToRender === "string") {
if (nodeToRender) lines.push(`- text: ${yamlEscapeValueIfNeeded(nodeToRender)}`);
} else renderNodeLines(nodeToRender, "", lines);
return lines.join("\n");
}
//#endregion
//#region src/aria/template.ts
function formatTextValue(tv) {
if (cachedRegex(tv)) return `/${tv.raw.slice(1, -1)}/`;
return yamlEscapeValueIfNeeded(tv.normalized);
}
function formatNameValue(name) {
if (typeof name === "string") return JSON.stringify(name);
return `/${name.pattern}/`;
}
function renderAriaTemplate(template) {
const lines = [];
if (template.kind === "text") lines.push(`- text: ${formatTextValue(template.text)}`);
else if (template.role === "fragment") {
if (template.containerMode && template.containerMode !== "contain") lines.push(`- /children: ${template.containerMode}`);
for (const child of template.children || []) renderTemplateLines(child, "", lines);
} else renderTemplateLines(template, "", lines);
return lines.join("\n");
}
function renderTemplateKey(tmpl) {
let key = tmpl.role;
if (tmpl.name !== void 0) key += ` ${formatNameValue(tmpl.name)}`;
key += renderAriaProps(tmpl);
return key;
}
function renderTemplateLines(node, indent, lines) {
if (node.kind === "text") {
lines.push(`${indent}- text: ${formatTextValue(node.text)}`);
return;
}
const key = renderTemplateKey(node);
const children = node.children || [];
const pseudoLines = [];
if (node.containerMode && node.containerMode !== "contain") pseudoLines.push(`${indent} - /children: ${node.containerMode}`);
if (node.props) for (const [name, tv] of Object.entries(node.props)) pseudoLines.push(`${indent} - /${name}: ${formatTextValue(tv)}`);
if (children.length === 0 && pseudoLines.length === 0) {
lines.push(`${indent}- ${key}`);
return;
}
if (children.length === 1 && children[0].kind === "text" && pseudoLines.length === 0) {
lines.push(`${indent}- ${key}: ${formatTextValue(children[0].text)}`);
return;
}
lines.push(`${indent}- ${key}:`);
lines.push(...pseudoLines);
for (const child of children) renderTemplateLines(child, `${indent} `, lines);
}
//#endregion
//#region src/aria/match.ts
function matchAriaTree(root, template) {
var _template$children;
const normalizedRoot = root.role === "fragment" ? root.children : [root];
const normalizedTemplate = isAriaTemplateFragement(template) ? (_template$children = template.children) !== null && _template$children !== void 0 ? _template$children : [] : [template];
const containerMode = isAriaTemplateFragement(template) ? template.containerMode : void 0;
const result = mergeChildLists(normalizedRoot, normalizedTemplate, "", containerMode);
if (result.pass && containerMode && containerMode !== "contain") result.resolved.unshift(`- /children: ${containerMode}`);
return {
pass: result.pass,
resolved: result.resolved.join("\n")
};
}
function isAriaTemplateFragement(template) {
return template.kind === "role" && template.role === "fragment";
}
function isRegexName(name) {
return typeof name === "object" && name !== null && "pattern" in name;
}
/** Build the resolved key through the template's lens:
* only include name/attributes that the template mentions. */
function renderResolvedKey(node, template) {
let key = node.role;
if (template.name === void 0) {} else if (isRegexName(template.name) && matchesStringOrRegex(node.name, template.name)) key += ` ${formatNameValue(template.name)}`;
else if (node.name) key += ` ${JSON.stringify(node.name)}`;
if (template.level !== void 0) key += ` [level=${node.level}]`;
if (template.checked !== void 0) {
if (node.checked === true) key += " [checked]";
else if (node.checked === "mixed") key += " [checked=mixed]";
}
if (template.disabled !== void 0 && node.disabled) key += " [disabled]";
if (template.expanded !== void 0) {
if (node.expanded === true) key += " [expanded]";
else if (node.expanded === false) key += " [expanded=false]";
}
if (template.pressed !== void 0) {
if (node.pressed === true) key += " [pressed]";
else if (node.pressed === "mixed") key += " [pressed=mixed]";
}
if (template.selected !== void 0 && node.selected) key += " [selected]";
return key;
}
function pairChildren(children, templates) {
const pairs = /* @__PURE__ */ new Map();
let ti = 0;
for (let ci = 0; ci < children.length && ti < templates.length; ci++) if (matchesNode(children[ci], templates[ti], false)) {
pairs.set(ci, ti);
ti++;
}
return pairs;
}
/** Pass 2: O(C × T) unordered exact match to recover pairs that pass 1's
* greedy left-to-right scan missed (e.g. due to ordering differences).
* Only affects merge output quality, not pass/fail. */
function pairChildrenFull(children, templates) {
const pairs = /* @__PURE__ */ new Map();
const pairedChildren = /* @__PURE__ */ new Set();
for (let ti = 0; ti < templates.length; ti++) for (let ci = 0; ci < children.length; ci++) {
if (pairedChildren.has(ci)) continue;
if (matchesNode(children[ci], templates[ti], false)) {
pairs.set(ci, ti);
pairedChildren.add(ci);
break;
}
}
return pairs;
}
function renderChildLines(child, indent) {
const lines = [];
if (typeof child === "string") lines.push(`${indent}- text: ${child}`);
else renderNodeLines(child, indent, lines);
return lines;
}
function mergeChildLists(children, templates, indent, containerMode) {
if (children.some((c) => typeof c !== "string" && c.role === "fragment") || templates.some((t) => isAriaTemplateFragement(t))) throw new Error("Internal error: fragment wrappers must be unwrapped at matchAriaTree root");
if (containerMode === "equal" || containerMode === "deep-equal") return mergeChildListsEqual(children, templates, indent, containerMode === "deep-equal");
const resolved = [];
const pairs = pairChildren(children, templates);
if (!(templates.length === pairs.size)) {
const recoveredPairs = pairChildrenFull(children, templates);
for (let ci = 0; ci < children.length; ci++) {
const ti = recoveredPairs.get(ci);
if (ti !== void 0) resolved.push(...mergeNode(children[ci], templates[ti], indent));
else resolved.push(...renderChildLines(children[ci], indent));
}
return {
resolved,
pass: false
};
}
for (let ci = 0; ci < children.length; ci++) {
const ti = pairs.get(ci);
if (ti !== void 0) resolved.push(...mergeNode(children[ci], templates[ti], indent));
}
return {
resolved,
pass: true
};
}
/** Equal/deep-equal mode: positional 1:1 matching. Pass iff same count
* and every positional pair matches (full-depth). When isDeepEqual is
* true, descendant nodes without explicit containerMode inherit equal
* semantics. */
function mergeChildListsEqual(children, templates, indent, isDeepEqual) {
const resolved = [];
const allPositionalMatched = children.length === templates.length && children.every((c, i) => matchesNode(c, templates[i], isDeepEqual));
for (let ci = 0; ci < children.length; ci++) if (ci < templates.length) resolved.push(...mergeNode(children[ci], templates[ci], indent, isDeepEqual));
else resolved.push(...renderChildLines(children[ci], indent));
return {
resolved,
pass: allPositionalMatched
};
}
/** Render a single actual node through the template's lens.
*
* Intentionally returns only resolved lines, not pass/fail. This is
* the unusual part of the design: pass/fail is decided *before* this
* function is called, by mergeChildLists (which uses matchesNode to
* pair children and determine pass). mergeNode is called only after
* pairing is settled — it doesn't need to re-decide, and couldn't
* change the outcome if it tried, because list-level decisions (which
* children to pair, which to skip) have already been made. */
function mergeNode(node, template, indent, isDeepEqual = false) {
var _template$containerMo;
if (typeof node === "string" && template.kind === "text") return [`${indent}- text: ${matchesTextValue(node, template.text) && cachedRegex(template.text) ? formatTextValue(template.text) : node}`];
if (typeof node === "string" || template.kind === "text") return renderChildLines(node, indent);
const resolvedKey = renderResolvedKey(node, template);
const childResult = mergeChildLists(node.children, template.children || [], `${indent} `, (_template$containerMo = template.containerMode) !== null && _template$containerMo !== void 0 ? _template$containerMo : isDeepEqual ? "equal" : "contain");
const resolvedPseudo = [];
if (template.containerMode && template.containerMode !== "contain") resolvedPseudo.push(`${indent} - /children: ${template.containerMode}`);
const allPropKeys = new Set([...Object.keys(node.props), ...Object.keys(template.props || {})]);
for (const prop of allPropKeys) {
var _template$props;
const nodeVal = node.props[prop];
const tmplVal = (_template$props = template.props) === null || _template$props === void 0 ? void 0 : _template$props[prop];
if (nodeVal !== void 0) {
const display = (tmplVal === void 0 || matchesTextValue(nodeVal, tmplVal)) && tmplVal && cachedRegex(tmplVal) ? formatTextValue(tmplVal) : nodeVal;
resolvedPseudo.push(`${indent} - /${prop}: ${display}`);
}
}
const resolved = [];
if (!childResult.resolved.length && !resolvedPseudo.length) resolved.push(`${indent}- ${resolvedKey}`);
else if (childResult.resolved.length === 1 && childResult.resolved[0].trimStart().startsWith("- text: ") && !resolvedPseudo.length) {
const text = childResult.resolved[0].trimStart().slice(8);
resolved.push(`${indent}- ${resolvedKey}: ${text}`);
} else {
resolved.push(`${indent}- ${resolvedKey}:`);
resolved.push(...resolvedPseudo);
resolved.push(...childResult.resolved);
}
return resolved;
}
//#endregion
//#region src/aria/index.ts
/**
* ARIA snapshot: capture, render, parse, match.
*
* Re-exports the Playwright-forked folk/ layer and adds vitest-specific
* three-way merge matching (matchAriaTree) on top.
*
*/
function parseAriaTemplate(text) {
return parseAriaSnapshotUnsafe(yaml_exports, text);
}
//#endregion
export { generateAriaTree, matchAriaTree, parseAriaTemplate, renderAriaTemplate, renderAriaTree };