primeira versão do e-li-nps construido com IA
This commit is contained in:
commit
06950d6e2c
34 changed files with 2524 additions and 0 deletions
134
internal/elinps/readme_page.go
Normal file
134
internal/elinps/readme_page.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package elinps
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
)
|
||||
|
||||
// ReadmePage serve o README.md renderizado como HTML.
|
||||
//
|
||||
// Motivação: dar uma "home" simples para o serviço (documentação em tempo real).
|
||||
// Sem autenticação, conforme solicitado.
|
||||
//
|
||||
// Implementação: cache em memória por mtime para evitar renderização em toda request.
|
||||
type ReadmePage struct {
|
||||
caminho string
|
||||
|
||||
mu sync.Mutex
|
||||
ultimoMTime time.Time
|
||||
html []byte
|
||||
errMsg string
|
||||
}
|
||||
|
||||
func NewReadmePage(caminho string) *ReadmePage {
|
||||
return &ReadmePage{caminho: caminho}
|
||||
}
|
||||
|
||||
func (p *ReadmePage) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Só respondemos GET/HEAD.
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
html, errMsg := p.renderIfNeeded()
|
||||
if errMsg != "" {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte(errMsg))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if r.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
w.Write(html)
|
||||
}
|
||||
|
||||
func (p *ReadmePage) renderIfNeeded() ([]byte, string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
st, err := os.Stat(p.caminho)
|
||||
if err != nil {
|
||||
p.errMsg = fmt.Sprintf("README não encontrado: %s", p.caminho)
|
||||
p.html = nil
|
||||
p.ultimoMTime = time.Time{}
|
||||
return nil, p.errMsg
|
||||
}
|
||||
|
||||
// Cache: se o arquivo não mudou, devolve o HTML já renderizado.
|
||||
if p.html != nil && st.ModTime().Equal(p.ultimoMTime) {
|
||||
return p.html, ""
|
||||
}
|
||||
|
||||
md, err := os.ReadFile(p.caminho)
|
||||
if err != nil {
|
||||
p.errMsg = "erro ao ler README"
|
||||
p.html = nil
|
||||
p.ultimoMTime = time.Time{}
|
||||
return nil, p.errMsg
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := goldmark.Convert(md, &buf); err != nil {
|
||||
p.errMsg = "erro ao renderizar README"
|
||||
p.html = nil
|
||||
p.ultimoMTime = time.Time{}
|
||||
return nil, p.errMsg
|
||||
}
|
||||
|
||||
// Envelopa em uma página com estilo básico.
|
||||
// Importante: NÃO usamos fmt.Sprintf com o HTML/CSS diretamente,
|
||||
// porque o CSS pode conter "%" (ex.: width:100%) e o fmt interpreta
|
||||
// como placeholders.
|
||||
page := `<!doctype html>
|
||||
<html lang="pt-br">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>e-li.nps • README</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:0;background:#fafafa;color:#111;}
|
||||
.wrap{max-width:980px;margin:0 auto;padding:24px;}
|
||||
.card{background:#fff;border:1px solid #e5e5e5;border-radius:12px;padding:22px;}
|
||||
h1,h2,h3{margin-top:1.2em;}
|
||||
pre{background:#0b1020;color:#e6e6e6;padding:14px;border-radius:12px;overflow:auto;}
|
||||
code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;}
|
||||
table{border-collapse:collapse;width:100%;}
|
||||
th,td{border:1px solid #e5e5e5;padding:8px;text-align:left;}
|
||||
a{color:#111;}
|
||||
.muted{color:#666;font-size:12px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<!--CONTEUDO_README-->
|
||||
<p class="muted" style="margin-top:16px;">Página gerada automaticamente a partir de README.md</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
html := []byte(strings.Replace(page, "<!--CONTEUDO_README-->", buf.String(), 1))
|
||||
|
||||
// Sanitização mínima: como o README é do próprio projeto, aceitamos o HTML gerado.
|
||||
// Se quiser endurecer segurança, podemos usar um sanitizer (bluemonday).
|
||||
_ = template.HTMLEscapeString
|
||||
|
||||
p.html = html
|
||||
p.errMsg = ""
|
||||
p.ultimoMTime = st.ModTime()
|
||||
return p.html, ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue