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:
Como posso corrigir este erro? Imagino que seja na expressão regular.
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:
Answered by Sam on January 2, 2022
Get help from others!
Recent Questions
Recent Answers
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP