ytml: como criei uma linguagem de marcação(ou quase isso)
Eu acredito que muitos tenham ótimas ideias de como uma tecnologia possa ser melhorada, ideias de novas features ou mudanças sutis que fazem toda a diferença, e com isso, projetos incríveis como o sass surgem. O que venho mostrar hoje é o ytml(eu sei, eu sei, muito criativo, não?), que tem por objetivo escrever HTML com uma estrutura diferente que não faça uso daquelas tags rodeadas com </>. Ele funciona de forma parecida com o sass: é compilado em html(no caso do sass é em css).
Vamos por partes, o objetivo é transformar isto:
<html lang="pt-br">
<body>
<p>Hello World</p>
</body>
</html>
Nisto:
html(lang = "pt-br"){
body {
p {
Hello World
}
}
}
(Ou tecnicamente o contrário...)
Para isso, é importante perceber que tag é um conceito só. Seja em html, ytml ou qualquer linguagem de marcação baseada em html, tags sempre possuem um nome, propriedades, e um interior, que pode ter outras tags ou texto simples. Por isso, é possível abstrair esses conceitos em uma estrutura de dados que represente uma tag, como por exemplo:
pub enum TagInnerElement {
Tag { tag: Tag },
Text { content: String },
}
pub struct Tag {
pub name: String,
pub attributes: HashMap<String, String>,
pub inner: Vec<TagInnerElement>,
}
(Sim, estou a utilizar o tão famoso rust) Note que a struct acima representa inteiramente o que uma tag html possui: nome, atributos e interior, que pode ser tanto uma outra tag quanto um texto simples.
O próximo passo é de alguma forma transformar essas estruturas em html de fato, e o código à seguir faz exatamente isto:
use super::ast::{Tag, TagInnerElement};
pub fn ast_to_html(ast: Vec<Tag>) -> String {
let mut html_content = String::new();
for root_tag in ast.iter() {
let html_tag = ast_tag_to_html(root_tag, 0);
html_content.push_str(&format!("{}\n", html_tag));
}
html_content
}
pub fn ast_tag_to_html(ast: &Tag, indent_level: usize) -> String {
let mut tag_content = String::new();
let mut attributes_rep = String::new();
for (key, val) in ast.attributes.iter() {
attributes_rep.push_str(&format!("{attribute} = {val} ", attribute = key, val = val));
}
tag_content.push_str(&format!(
"{indent}<{tagname} {attributes_rep}>",
tagname = ast.name,
indent = String::from(" ".repeat(indent_level))
));
for child in &ast.inner {
match child {
TagInnerElement::Tag { tag } => {
tag_content.push_str(&format!(
"\n{html}",
html = &ast_tag_to_html(tag, indent_level + 2),
));
}
TagInnerElement::Text { content } => tag_content.push_str(&format!(
"\n{indent}{content}",
content = content,
indent = String::from(" ".repeat(indent_level + 2))
)),
}
}
tag_content.push_str(&format!(
"\n{indent}</{tagname}>",
tagname = ast.name,
indent = String::from(" ".repeat(indent_level))
));
tag_content
}
Aliás, a função ast_to_html
aceita um conjunto de tags porque nada nos impede de criar um arquivo html com múltiplas tags pai.
E acredite, a parte mais trabalhosa foi ajeitar a indentação e quebras de linha💀
Com isso, já é possível transformar uma struct Tag em tags html de fato, mas falta ler código ytml e converter em Tag, para que assim seja possível transformar ytml em html.
Para isso, utilizei a biblioteca pest.rs, que permite escrever um parser. Não vou entrar muito em detalhes mas em resumo é como se ele transformasse uma simples string em uma árvore, categorizando cada elemento ali. Pensa numa DOM, não é exatamente igual, mas me parece uma boa analogia.
O código com a definição da linguagem se parece com isso
text = { (ASCII_ALPHANUMERIC | "-" | "!" | "_")+ }
string = ${ "\"" ~ text ~ "\"" }
tag_name = { text }
prop_name = { text }
prop_value = { string }
tag_props = { "(" ~ tag_prop+ ~ ")" }
tag_prop = { prop_name ~ "=" ~ prop_value }
tag_inner = { tag+ | text }
tag_multiplier_number = {ASCII_DIGIT+}
tag_multiplier = { "*" ~ tag_multiplier_number}
tag_class = {"." ~ text}
tag_id = {"#" ~ text}
tag_modifier = _{ tag_multiplier | tag_class | tag_id }
tag = { tag_name ~ tag_modifier* ~ tag_props? ~ "{" ~ tag_inner? ~ "}" }
doc = { SOI ~ tag* ~ EOI }
WHITESPACE = _{ " " | "\n" }
Depois de escrever uma função que vai percorrer a árvore gerada e converte-la em Tag, fica fácil transformar em html, e o resto das tarefas é um pouco trivial: ler de um arquivo, escrever em outro, configurar a CLI, entre outros detalhes.
Atualmente o projeto conta com função de parse, watch mode e operador de multiplicação, tudo detalhado no repositório😁
Bom, acho que é isso, foi um projeto curto mas que eu me diverti demais fazendo. Tenho quase certeza que não resolvi todos os problemas da melhor forma, principalmente na parte de geração de html. E por isso pretendo aprender mais para melhorar meu código e trazer novas features e sempre estarei aberto a sugestões e dicas🚀 Link do repositório: https://github.com/GabrielBrandao1618/ytml