AutoMouser/util/xpath_utils.js
Octo Ghost aac0e05770 minor
2025-11-30 20:29:21 -08:00

201 lines
6.6 KiB
JavaScript

// XPath generation utilities - Multiple algorithms for robust element identification
/**
* Generate multiple XPath expressions for an element
* Returns array of unique, validated XPaths
*/
export function getXpaths(element) {
if (!element || !element.tagName) return [];
var paths = [];
paths.push(getElementInfo_Custom(element, false));
paths.push(getElementInfo_Custom(element, true));
paths.push(getElementInfo_Moz(element));
// Deduplicate paths
var unique = paths.filter(function(path, index, self) {
return path && self.indexOf(path) === index;
});
// Validate each XPath
unique = unique.filter(function(xpath) {
try {
var result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);
var count = 0;
var node = result.iterateNext();
while (node) {
count++;
node = result.iterateNext();
}
return count === 1; // Only return unique XPaths
} catch (e) {
return false;
}
});
return unique;
}
/**
* Custom XPath generation with optional attribute-based targeting
*/
export function getElementInfo_Custom(element, useAttributes) {
var getSiblingInfo = function(element, useAttributes) {
var info = { num: 0, index: 1 };
var prevCount = 0;
var nextCount = 0;
var prevSibling = element.previousSibling;
var nextSibling = element.nextSibling;
if (useAttributes) {
while (prevSibling !== null) {
if (prevSibling.id === element.id &&
prevSibling.tagName === element.tagName &&
prevSibling.name === element.name &&
prevSibling.className === element.className &&
prevSibling.type === element.type) {
prevCount += 1;
}
prevSibling = prevSibling.previousSibling;
}
while (nextSibling !== null) {
if (nextSibling.id === element.id &&
nextSibling.tagName === element.tagName &&
nextSibling.name === element.name &&
nextSibling.className === element.className &&
nextSibling.type === element.type) {
nextCount += 1;
}
nextSibling = nextSibling.nextSibling;
}
} else {
while (prevSibling !== null) {
if (prevSibling.tagName === element.tagName) {
prevCount += 1;
}
prevSibling = prevSibling.previousSibling;
}
while (nextSibling !== null) {
if (nextSibling.tagName === element.tagName) {
nextCount += 1;
}
nextSibling = nextSibling.nextSibling;
}
}
info.num = prevCount + nextCount;
info.index += prevCount;
return info;
};
var getElementString = function(element, useAttributes) {
// Check if element has tagName property
if (!element || !element.tagName) {
return '';
}
var tagName = element.tagName.toLowerCase();
var siblingInfo = getSiblingInfo(element, useAttributes);
if (useAttributes) {
var attributes = [];
var attrNames = ["@type=", "@class=", "@name=", "@id="];
var attrValues = [element.type, element.className, element.name, element.id];
for (var i = attrNames.length - 1; i >= 0; i--) {
if (typeof attrValues[i] !== "undefined" && attrValues[i] !== "") {
attributes.push(attrNames[i] + "'" + attrValues[i] + "'");
}
}
if (attributes.length > 0) {
tagName += "[" + attributes.join(" and ") + "]";
}
}
if (siblingInfo.num > 0) {
tagName += "[" + siblingInfo.index + "]";
}
return tagName;
};
var buildPath = function(element, useAttributes) {
var path = getElementString(element, useAttributes);
if (!path) {
return '';
}
var parent = element.parentNode;
while (parent && parent.tagName) {
if (!useAttributes) {
if (element.id !== "") {
return "//" + element.tagName.toLowerCase() + "[@id='" + element.id + "']";
}
if (parent.id !== "") {
return "//" + parent.tagName.toLowerCase() + "[@id='" + parent.id + "']/" + path;
}
}
path = getElementString(parent, useAttributes) + "/" + path;
parent = parent.parentNode;
}
return path.replace(/^((\/){0,2}html)/, "/html");
};
return buildPath(element, useAttributes);
}
/**
* Mozilla-style XPath generation (more standardized approach)
*/
export function getElementInfo_Moz(element) {
if (element && element.id) {
return '//*[@id="' + element.id + '"]';
}
var paths = [];
for (; element && element.nodeType === Node.ELEMENT_NODE; element = element.parentNode) {
var index = 0;
var hasFollowingSiblings = false;
for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {
if (sibling.nodeType === Node.DOCUMENT_TYPE_NODE) continue;
if (sibling.nodeName === element.nodeName) ++index;
}
for (var sibling = element.nextSibling; sibling && !hasFollowingSiblings; sibling = sibling.nextSibling) {
if (sibling.nodeName === element.nodeName) hasFollowingSiblings = true;
}
var tagName = (element.prefix ? element.prefix + ":" : "") + element.localName;
var pathIndex = "[" + (index + 1) + "]";
paths.splice(0, 0, tagName + pathIndex);
}
return paths.length ? "/" + paths.join("/") : null;
}
/**
* Validate XPath syntax and uniqueness
*/
export function validateXPath(xpath) {
try {
const result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);
let count = 0;
let node = result.iterateNext();
while (node) {
count++;
node = result.iterateNext();
}
return {
valid: true,
count: count,
isUnique: count === 1
};
} catch (e) {
return {
valid: false,
error: e.message
};
}
}