Extraindo o XPath de um elemento no navegador
Tags: javascript, braziliandevs
Published: 2021-02-15 16:40:44

*(Ilustração da capa por Ilya Nazarov em [ArtStation](https://www.artstation.com/artwork/ez82Y))*

# Porque eu deveria me importar

Todo mundo que manipula elementos HTML está familiarizado com métodos como `querySelector()`, `querySelectorAll()`, ou outros antigos e ainda suportados como `getElementById()` e variações. A ideia deles é encontrar elementos a partir de seletores como classes CSS, ids, nomes das tags, entre outros. Algumas vezes, é preciso encontrar um *seletor universal*, algo que identifique especificamente qual é o elemento. O XPath pode ser uma alternativa pra isso.

# O que é XPath

Se pensarmos na DOM como uma estrutura em árvore com suas muitas divisões e *galhos*, seria possível localizar qualquer ponto nela indicando o caminho a ser percorrido. Pra exemplificar a metáfora da árvore: se pensarmos nas centenas ou milhares de folhas que uma árvore pode ter, seria muito complicado apontar pra alguém uma determinada folha dizendo "é  aquela verde" ou "a que tá virada pra cima". É muito mais **preciso** dizer "depois do segundo galho que vem do tronco, tem outros dois menores, e uns ramos... é a folha que tá no primeiro". De forma muita rasa e concisa, o XPath é esse caminho, só que pra [árvore do DOM](https://developer.mozilla.org/pt-BR/docs/DOM/Referencia_do_DOM/Introdu%C3%A7%C3%A3o). Considere o trecho HTML a seguir:

```html
<!DOCTYPE html>
<html>
  <head>
    <!-- ... -->
  </head>

  <body>
    <div>
      <span>
        <!-- ... -->
      </span>
      <span>
        <!-- ... -->
      </span>
    </div>
    <div>
      <span>
        <!-- ... -->
      </span>
      <span>
        <!-- ... -->
      </span>
    </div>
    <div>
      <span>
        <!-- ... -->
      </span>
      <span>
        <!-- ... -->
      </span>
    </div>
  </body>
</html>
```

Se quiséssemos pegar, digamos, algum `span` dentro de um `div`, não teríamos exatamente uma maneira precisa de dizer qual elemento queremos, pois os métodos citados lá atrás retornariam listas de elementos. Esses elementos também não tem seletores específicos como classes ou atributos HTML. Se quisesse o segundo `span` do terceiro `div`, por exemplo, teríamos que dizer "segundo `span`, dentro do terceiro `div`, dentro do `body`".
É aí que o XPath entra em ação, ele é **literalmente** isso:

> `/html/body/div[3]/span[2]`

É uma *notação* muito familiar, muito parecido com árvores de diretórios, seria algo como "dentro de `html`, dentro de `body`, dentro do terceiro `div`, selecione o segundo `span`. O mais interessante é que seria uma espécie de seletor universal. É uma notação interessante que pode ser comunicada entre plataformas, guardada na aplicação pra algum uso futuro, replicada em algum outro momento. Existem inúmeros problemas específicos que o XPath soluciona justamente pela flexibilidade de uso.

> É precisar observar que, diferentemente da indexação de arrays em JS, num XPath ela começa no `1` e não no `0`, ou seja, o segundo elemento vai ser achado pelo índice `[2]` e não `[1]`. Ao lidar com os arrays dos elementos no código, é preciso *compensar* essa diferença.

Se você quiser entender a fundo mesmo, recomendo olhar a [documentação oficial](https://www.w3.org/TR/1999/REC-xpath-19991116). Ela talvez seja demais pra esse artigo, mas vale a pena ao fim dele entrar e tentar descobrir novas formas de implementar o que tá descrito lá.
Por padrão, os navegadores não implementam um método de encontrarmos o XPath de um elemento, por isso temos que pensar numa maneira de, quando precisarmos, implementar a lógica por trás pra geração desse caminho.

# Implementação básica

## Pensando no código

Bom, o raciocínio inicial é: percorrer a árvore do DOM até o elemento raiz (`html`) e ir montando o nosso XPath de acordo. Pra isso, eu resolvi colocar toda a lógica dentro de uma só função, que recebe o elemento e retorna o XPath como string. Sem muito mistério, direto ao ponto.

```javascript
const getXPath = (element) => {
  // 
}
```

Na função, achei que seria interessante separar o processo em duas etapas: 1) coletar todos os elementos da árvore de ascendência, a partir do elemento inicial até o `html`, e depois 2) montar a partir disso o XPath. Seguindo o raciocínio da primeira parte:

```javascript
  // Array que vai guardar os elementos ascendentes em ordem
  const ascendingElements = []

  // Guarda o primeiro elemento logo de cara, já que ele obviamente faz parte do XPath
  ascendingElements.push(element)

  // Estrutura do/while, que executa a iteração enquanto houver elementos pai
  do {
   ascendingElements.unshift(ascendingElements[0].parentElement)
  } while (ascendingElements[0].parentElement !== null)
```

Dentro do `do..while` o que acontece é: verificamos se o primeiro elemento do array tem um pai válido (não-nulo). Se tem, adiciona ele a mesma lista no **começo** usando o método `unshift()`.

> Nota: Dá pra usar aqui o método `push()` também, só lembrando que ele insere o item no **fim** do array, então sempre teríamos que verificar qual é o *último* elemento utilizando `length - 1` do próprio array, e depois teríamos que inverter a ordem do array completo pra montar o XPath corretamente. Não tem problema nenhum, só considerei que seria um pouco mais legível assim.

Ao atingirmos um `parentElement` igual a `null`, significa que atingimos o fim do documento, ou seja, o próprio `html`, já que ele não tem elemento pai. O loop então se encerra e teremos no array `ascendingElements` todos os elementos em ordem.
A partir de então podemos trabalhar na criação do XPath em si. Todos os elementos podem ter seu nome acessado através da propriedade `tagName` e facilmente poderemos percorrer o array concatenando os nomes:

```javascript
  // Criamos um novo array através de .map() iterando sobre os elementos e retornando só os seus nomes
  const orderedTagNames = ascendingElements.map(element => element.tagName)
  // Podemos juntar todas as strings, colocando entre elas uma "/" e transformando tudo em minúscula, já que `tagName` retorna o nome em maiúsculo.
  const xPath = orderedTagNames.join('/').toLowerCase()

  // A função retorna a string completa concatenada a uma barra inicial, indicando que ali é a raiz do documento.
  return `/${xPath}`
```

A função completa seria então
```javascript
const getXPath = (element) => {
  const ascendingElements = []
  ascendingElements.push(element)

  do {
    ascendingElements.unshift(ascendingElements[0].parentElement)
  } while (ascendingElements[0].parentElement !== null)

  const orderedTagNames = ascendingElements.map(element => element.tagName)
  const xPath = orderedTagNames.join('/').toLowerCase()

  return `/${xPath}`
}
```

Temos a função completa! Ela retorna o XPath de um elemento completo. Vamos aplicar ao exemplo do início do texto. Se formos tentar criar o XPath do segundo `span` do terceiro elemento `div`, por exemplo, teremos

> `/html/body/div/span`

À primeira vista tudo certo, porém não temos a indicação da ordem do elemento! Se fossemos seguir esse XPath gerado, pegaríamos o primeiro `span` dentro do primeiro `div`. O nosso código não leva em conta que podem haver elementos de mesmo nome filhos do mesmo pai. O certo nesse exemplo seria indicar que era o `span[2]` depois de `div[3]`, de acordo com a especificação. Pra resolver isso, poderíamos então verificar em qual posição o elemento filho está *relativa ao seus similares*.

```javascript
  // Vamos retornar o nome dos elementos já com a indicação da sua posição
  const orderedTagNames = ascendingElements.map((element, elementLevel) => {
    const elementSelector = element.tagName

    // Um contador pra guardar, dentro de cada novo elemento que estamos verificando, em qual ordem ele está entre os seus similires
    let sameTagNameCounter = 0

    if (ascendingElements[elementLevel - 1] !== undefined) {
      for (let child of ascendingElements[elementLevel - 1].children) {

        // Se o elemento tem nome igual, adicionamos uma unidade ao seu contador. Ele servirá pra montarmos o nome com a posição correta ao fim do loop
        if (elementSelector === child.tagName) {
          sameTagNameCounter++ 
        }

        // Não precisamos conhecer quais são todos os elementos filhos em comum, precisamos encontrar somente a posição do elemento atual e depois disso podemos encerrar o loop
        if (element === child) {
          break
        }
      }
    }

    // Aplica a formatação "nomeDaTag[posição]" caso tenhamos mais de um elemento
    return `${elementSelector}${sameTagNameCounter > 1 ? `[${sameTagNameCounter}]` : ''}`
  })
```

Agora sim, se executarmos a função com o mesmo exemplo, teremos o resultado correto.
> `/html/body/div[3]/span[2]`


# Melhoramentos

Existem muitas maneiras de implementar essa lógica. Essa é mais uma sugestão simplificada do que regra, mas que poderia ter sido feita de outras formas. Poderíamos usar recursividade e diminuir algumas linhas de código? Com certeza. A manipulação dos elementos poderia ter sido feita com outros métodos? Há uma infinidade de maneiras de se abordar o mesmo problema, e contanto que resolva e siga boas práticas, tá tudo bem. Poderíamos desestruturar essa função em duas ou mais? Se estivéssemos em produção eu diria que *deveríamos*.

Mas não só do ponto de vista técnico, mas também do funcional. O XPath é uma notação extremamente robusta: é possível também usar funções pra selecionar algum id específico, acessar via seletores CSS, atributos e mais uma infinidade de coisas. Tem uma colinha bem legal [aqui](https://devhints.io/xpath), recomendo.
O código que a gente trabalhou é funcional, mas também é bem básico. Pra solucionar problemas mais complexos ou cenários mais robustos, considere pesquisar alguma biblioteca já bem estabelecida que resolva esses problemas.