TransWikia.com

Expressão regular para detectar estruturas aninhadas em um template

Stack Overflow em Português Asked by Luiz Felipe on January 2, 2022

Estou tentando criar um template engine usando JavaScript. A sintaxe será mais ou menos parecida com a do Laravel (blade), com algumas modificações.

Estou na parte da criação das expressões. A primeira que estou fazendo é a do if, mas logo de cara encarei um problema.

Antes de tudo, gostaria de deixar claro que eu criei um template num arquivo separado, que tem o conteúdo visualizado através do módulo fs. Para que seja possível executar no browser, coloquei toda a string retornada dentro de uma constante template:

const template = `
<div class="wrapper">
  <p>Um paráfrafo qualquer fora do if.</p>
  @if (true)
    <p>
      Um parágrado dentro do if.
      <strong>Tags aninhadas.</strong>
    </p>
  @endif
</div>
`;

Eu criei a expressão regular a seguir para fazer as buscas dentro da view:

/@if(?:s+|)(([sS]*?))([sS]*?)@endif/gi

E estou usando o seguinte código para "executar" a expressão if:

const renderView = (template, data = {}) => {
  const IF_REGEX = /@if(?:s+|)(([sS]*?))([sS]*?)@endif/gi;
  template = template.replace(IF_REGEX, (template, conditional, content) => {
    if (! eval(conditional)) return '';
    return content;
  });

  return template;
};

Funciona perfeitamente quando só há um bloco if (sem aninhamento de blocos).

No entanto, quando eu aninho dois blocos:

<div class="wrapper">
  <p>Parágrafo fora dos ifs.</p>

  @if (false)
    <div>Oi?</div>
    @if (true)
    <span>Span...</span>
    @endif
  @endif
</div> 

Ele não funciona como esperado:

inserir a descrição da imagem aqui

Como posso corrigir este erro? Imagino que seja na expressão regular.

2 Answers

Como já disseram nos comentários, não use regex, use um parser (ou adapte um já existente, ou construa um). Regex não é a ferramenta adequada, ainda mais quando há estruturas aninhadas. Ao longo da resposta, espero que entenda os motivos, então vamos lá...


A outra resposta pode ter funcionado para o seu caso específico, mas e se o seu template tiver outro @if depois, aí já não funciona mais:

function renderView(template, data = {}) {
  const IF_REGEX = /@if(?:s+|)(([sS]*?))([sS]*?)+@endif/gi;
  template = template.replace(IF_REGEX, (template, conditional, content) => {
    if (! eval(conditional)) return '';
    return content;
  });

  return template;
}

const template = `
<div class="wrapper">
  <p>Parágrafo fora dos ifs.</p>

  @if (true)
    <div>Oi?</div>
    @if (false)
    <span>Não devo ser renderizado</span>
    @endif
  @endif

  <p>Estou entre os if's</p>

  @if (true)
    <div>estou em outro if</div>
  @endif
</div>
`;

console.log(renderView(template));

O resultado é:

<div class="wrapper">
  <p>Parágrafo fora dos ifs.</p>

   
</div>

Que não é bem o que deveria ser - pelo que entendi, também deveria ter renderizado as tags <div>Oi?</div>, <p>Estou entre os if's</p> e <div>estou em outro if</div>.

Isso acontece porque a regex pegou tudo desde o primeiro @if até o último @endif. Simplesmente adicionar um quantificador ao trecho ([sS]*?) só faz com que ele possa se repetir várias vezes, mas como [sS] corresponde a qualquer coisa, a regex pode inclusive pegar ocorrências de @endif, caso ache necessário.

E ao colocar o quantificador aplicado ao grupo de captura, fez com que o content seja vazio (veja aqui, o conteúdo do Group 2 é vazio). Então o callback passado para replace acaba retornando vazio, eliminando todo o trecho entre o primeiro @if e o último @endif.

Aliás, usar ([sS]*?)+ é meio "estranho", pois ao usar *? você está indicando que [sS] deve se repetir o menor número de vezes (veja aqui e aqui para entender melhor como funciona o *?), mas ao circundar tudo com +, você está dizendo que isso pode se repetir o maior número possível de vezes (ou seja, o comportamento padrão do quantificador +, que é ser "ganancioso", cancela o comportamento "lazy" do *?, então na prática usar apenas [sS]* daria no mesmo - gerando inclusive o mesmo problema já citado acima - com a vantagem que a regex fica um pouco mais rápida, pois ao colocar quantificadores aninhados, você aumenta as possibilidades a serem testadas; já usando apenas um, há menos possibilidades e a regex tem menos casos para testar).

Outro detalhe é que (?:s+|) significa "um ou mais s, ou nada", então pode ser trocado para s* (zero ou mais s).


Então como eu faço para pegar os if's separadamente? Com uma única regex que faz tudo de uma vez, não é possível (talvez até seja se usarmos regex recursivas com sub-rotinas, mas são recursos que o JavaScript não suporta).

Uma outra alternativa é tratar os @if's de dentro para fora (primeiro eu verifico os mais internos, que não tem outro @if dentro), trato-os, atualizando o template e depois vou repetindo esse processo até não restar mais nenhum @if:

function renderView(template, data = {}) {
  const IF_REGEX = /@ifs*(((?:[sS](?!@if))+?))((?:[sS](?!@if))+?)@endif/gi;

  while (template.includes('@if')) { // enquanto tem @if, substitui
    template = template.replace(IF_REGEX, function(template, conditional, content) {
      if (! eval(conditional)) return '';
      return content;
    });
  }

  return template;
}

const template = `
<div class="wrapper">
  <p>Parágrafo fora dos ifs.</p>
  @if (true)
    <div>Eu serei renderizado</div>
    @if (false)
    <span>Não devo ser renderizado</span>
    @endif
  @endif
  <p>Estou entre os if's</p>
  @if (true)
    <div>estou em outro if</div>
    @if (true)
      <span>Estou em um if aninhado</span>
      @if (true)
        <span>Estou em outro if aninhado</span>
        @if (false)
          <span>Não serei renderizado</span>
        @endif
      @endif
    @endif
  @endif
</div>

@if (true)<div>Eu também apareço</div>@endif
`;

console.log(renderView(template));

A regex usa o lookahead negativo [sS](?!@if), que verifica se é um caractere que não tem @if logo depois (e tudo isso se repete uma ou mais vezes). Aliás, troquei o quantificador * por +, pois o * significa "zero ou mais ocorrências", o que indica que poderia ter algo como @if(). Já trocando por + (uma ou mais ocorrências), garante que tem que ter pelo menos um caractere lá dentro (mas eu não verifico se só tem espaços, por exemplo, então @if ( ) ainda seria aceito).

Enfim, o lookahead garante que vou pegar somente o @if que não tenha outro @if dentro dele. Então eu atualizo o template com o resultado da avaliação deste @if, e continuo verificando se ainda restou algum @if a ser analisado. Quando não tiver mais, retorna o template renderizado.

O resultado é (eliminando as linhas em branco, para facilitar a visualização):

<div class="wrapper">
  <p>Parágrafo fora dos ifs.</p>
    <div>Eu serei renderizado</div>
  <p>Estou entre os if's</p>
    <div>estou em outro if</div>
      <span>Estou em um if aninhado</span>
        <span>Estou em outro if aninhado</span>
</div>
<div>Eu também apareço</div>

Mas é claro que ainda é uma implementação ingênua. Se o template tiver @if comentado (pode ter?), ele tentará avaliar (o que não ocorreria com um parser, já que o comentário seria detectado e corretamente ignorado). Além disso, a regex usa a flag i, então o template poderá ter coisas como @IF e @EndIf. Se é isso mesmo que precisa, pode deixar a flag, senão é melhor removê-la.

Outro ponto é que dependendo do caso, muito processamento poderá ser feito à toa. Por exemplo, se o template for algo assim:

@if (condicao_falsa)
  @if (bla1)
    @if (bla2)
      @if (bla3)
        @if (bla4)
        @endif
      @endif
    @endif
  @endif
@endif

O algoritmo começa avaliando bla4, depois bla3, etc. Mas se as condições de todos os @if's internos retornam true e somente a condição do primeiro mais externo retorna false, todos os internos terão sido renderizados à toa. Em um parser bem implementado, a primeira condição seria avaliada primeiro, e caso fosse falsa, nem precisaria avaliar as mais internas. Mas como já vimos que não é possível garantir com regex que sempre conseguimos pegar o @if mais externo, é mais uma desvantagem que você terá que se aceitar, se usar regex em vez do parser.

Sem contar que o próprio desempenho da regex não será aquelas coisas, pois ela primeiro começa a busca no @if mais externo, até detectar que tem outro @if dentro. Então ela faz o backtracking e tenta a partir do segundo @if, e depois do terceiro, etc, até encontrar o mais interno. E como está um loop, fará todo esse processo de novo (começa do mais externo, encontra um interno, recomeça a partir desse, etc), para cada nível de aninhamento. Para templates muito grandes e com muitos @if's aninhados, acabará se tornando bem ineficiente, e talvez até inviável.

Também tem os casos do template estar com algum erro (por exemplo, um @if interno não tem o fechamento, então a regex acabará indo até o @endif do externo, coisa que um parser não faria porque detectaria que faltou fechar um deles), e muitas outras situações que regex não é capaz de detectar (ou até é, mas acaba ficando tão complicado que não vale a pena).

Answered by hkotsubo on January 2, 2022

O problema está na sua regex que irá selecionar apenas até a primeira ocorrência final em @endif, ignorando a segunda (ou demais).

Para resolver, adicione o quantificador + que permitirá selecionar quantas ocorrências precedidas de @endif que houver:

@if(?:s+|)(([sS]*?))([sS]*?)+@endif
                                   ↑
                             quantificador

Veja o print no regexr.com:

inserir a descrição da imagem aqui

Answered by Sam on January 2, 2022

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP