Pular para o conteúdo

Gerador de site estático

Markdown para HTML com modelo, índice gerado e o texto sempre escapado.

Um gerador de site é um compilador pequeno: a entrada são arquivos de texto, a saída são páginas, e no meio há uma árvore. As duas decisões que importam são escapar tudo que vem do texto antes de montar HTML, e gerar o índice dos mesmos dados que geram as páginas — uma segunda lista sempre diverge.

PeçaO que ela exercita
lines + matchcabeçalho, lista e parágrafo
escape de HTMLo < do texto não vira tag
modelo com $"…"a moldura comum de toda página
o índice dos mesmos dadosnenhuma página fica fora do menu

Estrutura#

text
blog/
  conteudo/
    *.md
  modelos/
    pagina.html
  src/
    markdown.df    texto -> html
    gerar.df       le a pasta, escreve saida/
  saida/           (gerado — nao versione)
forge.toml
[project]
name = "blog"
version = "0.1.0"
description = "Gerador de site"
entry = "src/main.df"
dataforge = ">=1.1"

[dependencies]

[scripts]
start = "run src/main.df"
test = "test tests/"

O núcleo#

Este bloco roda sozinho — copie para um arquivo e rode dataforge run. Ele termina com assert, e é assim que esta página é conferida a cada build.

src/markdown.df
action escapar(t):
    yield t.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")

action em_linha(t):
    t := escapar(t)
    yield regex_sub("\\*\\*(.+?)\\*\\*", "<strong>\\1</strong>", t)

action html_de(markdown):
    saida := []
    em_lista := no
    cycle linha in markdown.lines():
        l := linha.trim()
        given em_lista and not l.startswith("- "):
            saida.append("</ul>")
            em_lista := no
        given l is "":
            skip
        orif l.startswith("## "):
            saida.append($"<h2>{em_linha(l[3:])}</h2>")
        orif l.startswith("# "):
            saida.append($"<h1>{em_linha(l[2:])}</h1>")
        orif l.startswith("- "):
            given not em_lista:
                saida.append("<ul>")
                em_lista := yes
            saida.append($"<li>{em_linha(l[2:])}</li>")
        otherwise:
            saida.append($"<p>{em_linha(l)}</p>")
    given em_lista:
        saida.append("</ul>")
    yield "\n".join(saida)

action pagina(titulo, corpo, menu):
    yield $"<!doctype html><title>{escapar(titulo)}</title><nav>{menu}</nav><main>{corpo}</main>"

POSTS := [
    {"slug": "ola", "titulo": "Ola", "texto": "# Ola\n\nPrimeiro **post**."},
    {"slug": "lista", "titulo": "Uma lista", "texto": "# Lista\n\n- um\n- dois <script>"}
]

menu := "".join(POSTS >> morph p: $"<a href=\"/{p['slug']}.html\">{escapar(p['titulo'])}</a>")
site := {}
cycle p in POSTS:
    site[p["slug"] + ".html"] := pagina(p["titulo"], html_de(p["texto"]), menu)

assert "<strong>post</strong>" in site["ola.html"]
assert "<li>dois &lt;script&gt;</li>" in site["lista.html"]
assert "<script>" not in site["lista.html"]
assert site["ola.html"].count("<a href") is len(POSTS)
out site["lista.html"]

O teste#

No projeto, a regra mora em src/ e o teste a importa pelo caminho relativo — dataforge test tests/ descobre o arquivo sozinho.

tests/nucleo_test.df
adopt ../src/markdown as M

crucible "markdown":
    trial "a lista fecha no fim do arquivo":
        expect M.html_de("- a") is "<ul>\n<li>a</li>\n</ul>"

As decisões#

DecisãoSem ela
escapar antes de montarum post com <script> roda no navegador de quem lê
o menu sai da lista de postso post novo existe e não aparece no site
a lista fecha quando o bloco terminao resto da página fica dentro de um <ul>
saida/ fora do gitcada build gera um diff de milhares de linhas

Para ir além#

Volte para todos os tipos de projeto.