134 lines
3.6 KiB
Go
134 lines
3.6 KiB
Go
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, ""
|
|
}
|