On this page
Binary code with text "XPath" on it

Get XPath from the element using JavaScript

Learn how to extract XPath from any DOM element using JavaScript, enhancing your web scraping and automation skills.

To get the XPath of an element using JavaScript, you can use an approach that traverses up the DOM tree from the target element, constructing the XPath string as it goes.

Custom function to get XPath

The provided JavaScript function getXpath is designed to generate an XPath expression that uniquely identifies a given DOM element (el). It traverses up the DOM tree from the starting element, collecting information about its type and any siblings of the same type, to construct a relative XPath expression.

Note that this function is also SVG friendly by using *[name()='svg'] for namespace sensitivity and it is very useful in XML-heavy contexts.

Here is an implementation written in JavaScript and optimized for performance:

Get XPath from the element using TypeScript, superset of JavaScript
public static getXPath(el: Node | null): string {
  if ((el instanceof Node) === false) {
    return '';
  }

  const escapeXPath = (name: string): string => {
    return name.replace(/([:*])/g, '\\$1');
  };

  const escapeXPathString = (value: string): string => {
    if (value.includes('"') === false) {
      return `"${value}"`;
    }

    if (value.includes('\'') === false) {
      return `'${value}'`;
    }

    // Fallback for strings containing both ' and "
    return `concat(${value
      .split('"')
      .map((part, i) => {
        return (i === 0 ? `"${part}"` : `,'"',"${part}"`);
      }
      )
      .join('')})`;
  };

  const isUniqueId = (el: Element): boolean => {
    if (!el.id || !el.ownerDocument) {
      return false;
    }

    return el.ownerDocument.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1;
  };

  // Skip parser-recovery garbage from malformed HTML
  const isIgnorableElement = (el: Element): boolean => {
    if (el.nodeName.toLowerCase() === 'html' || el.nodeName.toLowerCase() === 'body') {
      return false;
    }

    if (el.id) {
      return false;
    }

    // invalid tag names like: metacontent="home
    return !(/^[a-z][a-z0-9-]*$/i).test(el.nodeName);
  };

  const getSameNameSiblings = (parent: ParentNode, node: Node): Node[] => {
    const result: ChildNode[] = [];
    const targetName: string = node.nodeName;

    for (const child of parent.childNodes) {
      if (child.nodeName === targetName) {
        result.push(child);
      }
    }

    return result;
  };

  let element: Node | null = el;
  let parent: ParentNode | null = element.parentNode ?? element.ownerDocument;
  let relativePath = '';

  while (parent && element) {
    // Anchor on nearest ancestor with unique ID
    if (element instanceof Element && isUniqueId(element)) {
      return `//*[@id=${escapeXPathString(element.id)}]${relativePath}`;
    }

    switch (element.nodeType) {
      case Node.ELEMENT_NODE: {
        const el = element as Element;

        if (isIgnorableElement(el)) {
          element = parent as Node;
          parent = element.parentNode ?? element.ownerDocument;
          continue;
        }

        const tag: string = el.namespaceURI === 'http://www.w3.org/2000/svg' ?
          `*[name()='${el.tagName.toLowerCase()}']` :
          escapeXPath(el.tagName).toLowerCase();

        const sames: Node[] = getSameNameSiblings(parent, element);

        if (sames.length === 1) {
          relativePath = `/${tag}${relativePath}`;
        } else {
          const index = sames.indexOf(element) + 1;

          relativePath = `/${tag}[${index}]${relativePath}`;
        }

        break;
      }

      case Node.TEXT_NODE: {
        const texts = Array.from(parent.childNodes)
          .filter((node: ChildNode): boolean => {
            return node.nodeType === Node.TEXT_NODE;
          });

        if (texts.length === 1) {
          relativePath = `/text()${relativePath}`;
        } else {
          relativePath = `/text()[${texts.indexOf(element as ChildNode) + 1}]${relativePath}`;
        }

        break;
      }

      case Node.COMMENT_NODE: {
        const comments = Array.from(parent.childNodes)
          .filter((node: ChildNode): boolean => {
            return node.nodeType === Node.COMMENT_NODE;
          });

        relativePath = `/comment()[${comments.indexOf(element as ChildNode) + 1}]${relativePath}`;
        break;
      }

      case Node.ATTRIBUTE_NODE: {
        relativePath = `/@${escapeXPath(element.nodeName).toLowerCase()}${relativePath}`;
        break;
      }

      default:
        break;
    }

    element = parent as Node;
    parent = element.parentNode ?? element.ownerDocument;
  }

  // Fallback (should rarely happen)
  return `.//${relativePath.replace(/^\/+/, '')}`;
}

Breakdown of how it works

This function generates an XPath for a DOM node by traversing upward from the target to the root, building path segments for elements, text nodes, comments, and attributes. It optimizes the path by anchoring on elements with unique IDs when possible, handles malformed HTML by skipping invalid elements, and properly escapes special characters and quotes. The result is a compact, robust XPath that can uniquely locate the node in the document structure.

Additional tools

There are several tools that can be handy when working with XPath:

Related posts

Comments

Leave a Reply

Search in sitelint.com

Struggling with writing tests for accessibility?

You can cut 80% of the time you spend writing tests using SiteLint.