Estamos perto do fim da série, devem faltar no máximo umas 3 partes. O tema da vez é capturing e backreferencing.

Caso ainda não tenha lido as partes anteriores, LEIA para entender tudo que usaremos hoje. Seguem os links abaixo:

Capturing groups

Se podemos considerar um match como uma extração de texto, então capture groups são a extração da extração! Capturing ou capture groups (grupos de captura) são recursos que usaremos para determinar trechos nas expressões regulares que formarão matches adicionais separadamente. Para tal, colocamos os trechos desejados entre parêntesis. Por exemplo: /(para)lele(pi(pe))do/, capturaria tanto paralelepipedo, como para, pipe e pe, respectivamente.

Inception!

Um bom caso de uso para este recurso é quando você precisa extrair uma informação específica que deve estar acompanhada de alguma outra regra. Ou então apenas convenientemente separar matches.

.exec() versus .match()

Artigos atrás, quando mostrei pela primeira vez a função .exec()do objeto RegExp, omiti um detalhe que agora nos fará diferença. Quando temos uma expressão que pode ter mais de um match, portanto com a flag global, cada chamada a esta função trará separadamente o próximo match disponível e atualizará a propriedade lastIndex. Assim sucessivamente até quando não houverem mais, e então retorna null. Enquanto que com a função .match() dos objetos String, retorna o array com todos os matches de uma vez. Porém essa última é incapaz de trazer múltiplos matches e seus respectivos capture groups JUNTOS.

Cade as capturas dos grupos de nome (\w+) e idade (\d+)?

Resolvemos esse problema com uma estrutura de repetição while que usa como condição de término um null check do que é extraído no .exec(), a cada iteração. A dica de ouro aqui é NUNCA criar a expressão direto na condição (nem via RegExp literal ou construtor), como em:

// NÃO FAZER ASSIM
while(/abc/g.exec(texto) !== null) {
// fazer algo
}

Pois fazendo desta forma, estaremos sempre recriando a expressão e, consequentemente, resetando sua propriedade lastIndex, fazendo com que sempre capturemos a primeira ocorrência infinitamente.

Vemos abaixo uma forma de fazer isso rapidamente e tratando (array único e sem o valor nulo) o resultado final.

Não é o super enxuto nem bonito, mas resolve.

Backreferencing groups

Backreference é mais simples. Ele consiste em literalmente referenciar um mesmo valor previamente capturado. Este recurso é bastante usado em mecânicas replacede ferramentas como IDEs, editores de texto, etc… Mas também serve para “recapturarmos” um grupo já capturado à escolha. Para cada grupo de captura gerado, há uma respectiva referência.

Um detalhe confuso é que usando a funçãoreplace, cada grupo é representado pela sua posição precedida de um cifrão:$1, $2, $3, $n. Já dentro da própria expressão, devemos usar a barra invertida:\1, \2, \3, \n.

Enquanto que usando a referência enumerada só traz a substring capturada, para retornarmos o match inteiro, use $&.

Referência do grupo de captura versus match inteiro.

Praticando

Imaginar exercícios construtivos deste tema do “mundo real” são difíceis (talvez por falta de prática ou familiaridade). Não estou totalmente satisfeito com os que descrevi abaixo, mas não posso mais prolongar esse artigo por este motivo.

1) Monte uma expressão regular para remover repetições de um texto. A definição que usaremos de repetição aqui é simplesmente um conjunto de caracteres (palavra) que seja seguido deles mesmos, separados apenas por um ou mais espaços em branco.

Exemplo:

removerRepeticoes('estou estou repetido') // estou repetido
removerRepeticoes('eu não eu não') // eu não eu não
removerRepeticoes('iô-iô') // iô-iô

Solução:

Tá fácil, vai…

Não tem muito o que comentar desse script além do RegEx, que é bem simples por sinal. Usamos word boundaries \b para delimitarmos o match de uma palavra para outra. Então capturamos qualquer conjunto de alfa-numéricos com (\w+). Finalmente, com (\s+\1)+ dizemos que deve haver um espaço em branco seguido da mesma palavra que a anterior. Com isto feito, só precisamos utilizar da função .replace() passando como argumentos a expressão regular montada e a referência do primeiro grupo de captura. Pronto! texto limpo.

2) Monte uma expressão regular para capturar tags HTML. Para esta tarefa, não se preocupe em se ater 100% das regras descritas nas especificações do HTML5. Apenas considere que deve aceitar o formato do exemplo abaixo:

Exemplo:

<minha-tag um-atributo=”oi”>Texto Interno</minha-tag>

Solução:

/<([-a-z]+)(\s[-a-z]+=".*?")*>.*?<\/\1>/

Antes de tudo, garantimos o mínimo da sintaxe colocando os icônicos símbolos de <e> das tags HTML. Em ([-a-z]+), capturamos o nome da tag. Garantimos que capturamos qualquer letra do alfabeto e/ou hifens (a este estilo de nomenclatura damos o nome de kebab case). A seguir temos (\s[-a-z]+=".*?")* que é o trecho que representa os atributos, estes podem ser opcionais ou múltiplos, então englobamos tudo em parêntesis seguidos do quantificador *. Cada nome de atributo segue a mesma regra do nome das tags, mas são acompanhados de atribuições de valores que aceitam vazio ou qualquer coisa. Lembrando o uso do ? para mudarmos o comportamento para lazy, fazendo com que capturemos o mínimo possível de caractéres até as aspas duplas de fechamento. Para o conteúdo interno da tag, também opcional, usamos o mesmo conjunto de regras do valor dos atributos, porém sem as aspas duplas. Por fim, tem a tag de fechamento, com a barra e referenciando o nome escolhido no começo com \1.

Bônus

Acaso ou não, durante a longa (meses) escrita desse artigo, me deparei com o Github da biblioteca Mongoose, famosa e usada para criar Schemas a nível de aplicação do banco de dados Mongo. Eu queria saber como funcionava o algoritmo de pluralização de nome de coleções automático. Eis a surpresa (mas como poderia ser diferente disso?) quando vi que usava extensivamente de backreferences. Segue para os outros curiosos como eu:

https://github.com/Automattic/mongoose/blob/v4.11.13/lib/utils.js

Glossário do dia

  • Capturing: usar () nas expressões para capturar trechos, a fim de reutilizar ou agrupar valores;
  • Backreferencing: complementar a capturing, é o passo que define reutilizar os valores, usando $n em .replace e \n dentro das próprias expressões;
  • Null check: validação para checar se uma variável contém valor nulo;
  • Kebab case: estilo de nomenclatura que as palavras são minúsculas e separadas por hífens;
  • Schema: definição da estrutura de uma entidade de banco de dados;

Resumindo

Aprendemos que capture groups e backreferencing são ótimos tanto para possibilitar o reuso de textos imprevisíveis quanto para facilitar uma repetição extensa e consciente. Entendemos alguns detalhes importantes do funcionamento e acompanhamos dois exercícios simples que utilizavam duas formas diferentes de usá-los. Até mesmo vimos que apesar de parecerem mais esporádicos, são funcionalidades usadas em grandes bibliotecas. Interessante, não?

Repeat after me!

/keepCoding 🤓/

--

--