Como analisar linguagens de modelo em Ragel?
Pergunta
Tenho trabalhado em um analisador para linguagem de modelo simples.Estou usando Ragel.
Os requisitos são modestos.Estou tentando encontrar [[tags]] que possam ser incorporadas em qualquer lugar da string de entrada.
Estou tentando analisar uma linguagem de modelo simples, algo que pode ter tags como {{foo}} incorporadas ao HTML.Tentei várias abordagens para analisar isso, mas tive que recorrer ao uso de um scanner Ragel e usar a abordagem ineficiente de combinar apenas um único caractere como "pega tudo".Eu sinto que esta é a maneira errada de fazer isso.Basicamente, estou abusando do preconceito de correspondência mais longa do scanner para implementar minha regra padrão (ela pode ter apenas 1 caractere, portanto deve ser sempre o último recurso).
%%{
machine parser;
action start { tokstart = p; }
action on_tag { results << [:tag, data[tokstart..p]] }
action on_static { results << [:static, data[p..p]] }
tag = ('[[' lower+ ']]') >start @on_tag;
main := |*
tag;
any => on_static;
*|;
}%%
(ações escritas em Ruby, mas devem ser fáceis de entender).
Como você escreveria um analisador para uma linguagem tão simples?Ragel talvez não seja a ferramenta certa?Parece que você tem que lutar contra Ragel com unhas e dentes se a sintaxe for imprevisível como esta.
Solução
Ragel funciona bem.Você só precisa ter cuidado com o que está combinando.Sua pergunta usa ambos [[tag]]
e {{tag}}
, mas seu exemplo usa [[tag]]
, então acho que é isso que você está tentando tratar como especial.
O que você quer fazer é comer o texto até atingir um colchete aberto.Se esse colchete for seguido por outro colchete, então é hora de começar a comer caracteres minúsculos até chegar a um colchete fechado.Como o texto na tag não pode incluir nenhum colchete, você sabe que o único caractere sem erro que pode seguir esse colchete é outro colchete.Nesse ponto, você está de volta ao ponto de partida.
Bem, essa é uma descrição literal desta máquina:
tag = '[[' lower+ ']]';
main := (
(any - '[')* # eat text
('[' ^'[' | tag) # try to eat a tag
)*;
A parte complicada é: onde você chama suas ações?Não pretendo ter a melhor resposta para isso, mas aqui está o que descobri:
static char *text_start;
%%{
machine parser;
action MarkStart { text_start = fpc; }
action PrintTextNode {
int text_len = fpc - text_start;
if (text_len > 0) {
printf("TEXT(%.*s)\n", text_len, text_start);
}
}
action PrintTagNode {
int text_len = fpc - text_start - 1; /* drop closing bracket */
printf("TAG(%.*s)\n", text_len, text_start);
}
tag = '[[' (lower+ >MarkStart) ']]' @PrintTagNode;
main := (
(any - '[')* >MarkStart %PrintTextNode
('[' ^'[' %PrintTextNode | tag) >MarkStart
)* @eof(PrintTextNode);
}%%
Existem algumas coisas não óbvias:
- O
eof
é necessária acção porque%PrintTextNode
só é invocado ao sair de uma máquina.Se a entrada terminar com texto normal, não haverá entrada para fazê-la sair desse estado.Como também será chamado quando a entrada terminar com uma tag e não houver nenhum nó de texto final não impresso,PrintTextNode
testa se tem algum texto para imprimir. - O
%PrintTextNode
ação aninhada após o^'['
é necessário porque, embora tenhamos marcado o início quando atingimos o[
, depois de atingirmos um não-[
, começaremos a tentar analisar qualquer coisa novamente e observaremos o ponto inicial.Precisamos liberar esses dois caracteres antes que isso aconteça, daí a invocação da ação.
O analisador completo segue.Eu fiz isso em C porque é isso que eu sei, mas você deve ser capaz de transformá-lo em qualquer idioma que precisar facilmente:
/* ragel so_tag.rl && gcc so_tag.c -o so_tag */
#include <stdio.h>
#include <string.h>
static char *text_start;
%%{
machine parser;
action MarkStart { text_start = fpc; }
action PrintTextNode {
int text_len = fpc - text_start;
if (text_len > 0) {
printf("TEXT(%.*s)\n", text_len, text_start);
}
}
action PrintTagNode {
int text_len = fpc - text_start - 1; /* drop closing bracket */
printf("TAG(%.*s)\n", text_len, text_start);
}
tag = '[[' (lower+ >MarkStart) ']]' @PrintTagNode;
main := (
(any - '[')* >MarkStart %PrintTextNode
('[' ^'[' %PrintTextNode | tag) >MarkStart
)* @eof(PrintTextNode);
}%%
%% write data;
int
main(void) {
char buffer[4096];
int cs;
char *p = NULL;
char *pe = NULL;
char *eof = NULL;
%% write init;
do {
size_t nread = fread(buffer, 1, sizeof(buffer), stdin);
p = buffer;
pe = p + nread;
if (nread < sizeof(buffer) && feof(stdin)) eof = pe;
%% write exec;
if (eof || cs == %%{ write error; }%%) break;
} while (1);
return 0;
}
Aqui estão algumas entradas de teste:
[[header]]
<html>
<head><title>title</title></head>
<body>
<h1>[[headertext]]</h1>
<p>I am feeling very [[emotion]].</p>
<p>I like brackets: [ is cool. ] is cool. [] are cool. But [[tag]] is special.</p>
</body>
</html>
[[footer]]
E aqui está a saída do analisador:
TAG(header)
TEXT(
<html>
<head><title>title</title></head>
<body>
<h1>)
TAG(headertext)
TEXT(</h1>
<p>I am feeling very )
TAG(emotion)
TEXT(.</p>
<p>I like brackets: )
TEXT([ )
TEXT(is cool. ] is cool. )
TEXT([])
TEXT( are cool. But )
TAG(tag)
TEXT( is special.</p>
</body>
</html>
)
TAG(footer)
TEXT(
)
O nó de texto final contém apenas a nova linha no final do arquivo.