Opening a repository shows its model card, rendered from markdown, with its base model, license and languages above it; the listing names each repository's base model and license under its name. The card is a stranger's text: raw HTML and dangerous links are dropped, images are shown as links rather than loaded, and relative links point into the repository. Adds github.com/yuin/goldmark for the markdown. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
115 lines
3.5 KiB
Go
115 lines
3.5 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/extension"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/renderer"
|
|
"github.com/yuin/goldmark/renderer/html"
|
|
"github.com/yuin/goldmark/text"
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
// renderCard turns a model card into HTML for the mirror page.
|
|
//
|
|
// The card is a stranger's text, so what it may do is narrow:
|
|
// - raw HTML is dropped, which goldmark does unless told otherwise, and
|
|
// links to javascript: and the like are emptied, which it also does;
|
|
// - images are not loaded but shown as links, because an admin page that
|
|
// fetches whatever a README points at tells a stranger who is looking;
|
|
// - relative links, to the repository's own LICENSE and files, are made to
|
|
// point at the repository rather than at this page.
|
|
func renderCard(card, repoBase string) template.HTML {
|
|
if strings.TrimSpace(card) == "" {
|
|
return ""
|
|
}
|
|
md := goldmark.New(
|
|
goldmark.WithExtensions(extension.GFM),
|
|
goldmark.WithParserOptions(parser.WithASTTransformers(
|
|
util.Prioritized(cardLinks{base: repoBase}, 100))),
|
|
goldmark.WithRendererOptions(
|
|
renderer.WithNodeRenderers(util.Prioritized(imageAsLink{}, 100))),
|
|
)
|
|
var out bytes.Buffer
|
|
if err := md.Convert([]byte(card), &out); err != nil {
|
|
return ""
|
|
}
|
|
// Safe by construction: goldmark in its default, safe mode, with the two
|
|
// changes above. Not something a template may escape again.
|
|
return template.HTML(out.String()) //nolint:gosec
|
|
}
|
|
|
|
// cardLinks resolves a card's relative links against its repository.
|
|
type cardLinks struct{ base string }
|
|
|
|
func (c cardLinks) Transform(doc *ast.Document, _ text.Reader, _ parser.Context) {
|
|
base, err := url.Parse(c.base)
|
|
if err != nil || c.base == "" {
|
|
return
|
|
}
|
|
_ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
var dest *[]byte
|
|
switch l := n.(type) {
|
|
case *ast.Link:
|
|
dest = &l.Destination
|
|
case *ast.Image:
|
|
dest = &l.Destination
|
|
default:
|
|
return ast.WalkContinue, nil
|
|
}
|
|
u, err := url.Parse(string(*dest))
|
|
if err != nil || u.IsAbs() || strings.HasPrefix(string(*dest), "#") {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
*dest = []byte(base.ResolveReference(u).String())
|
|
return ast.WalkContinue, nil
|
|
})
|
|
}
|
|
|
|
// imageAsLink renders an image as a link to it, named by its alt text.
|
|
type imageAsLink struct{}
|
|
|
|
func (imageAsLink) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|
reg.Register(ast.KindImage, func(w util.BufWriter, src []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
img := n.(*ast.Image)
|
|
alt := strings.TrimSpace(string(nodeText(img, src)))
|
|
if alt == "" {
|
|
alt = "image"
|
|
}
|
|
dest := img.Destination
|
|
if html.IsDangerousURL(dest) {
|
|
_, _ = w.WriteString(`<span class="img">[` + template.HTMLEscapeString(alt) + `]</span>`)
|
|
return ast.WalkSkipChildren, nil
|
|
}
|
|
_, _ = w.WriteString(`<a class="img" rel="noopener noreferrer" href="` +
|
|
template.HTMLEscapeString(string(util.URLEscape(dest, true))) + `">[` +
|
|
template.HTMLEscapeString(alt) + `]</a>`)
|
|
return ast.WalkSkipChildren, nil
|
|
})
|
|
}
|
|
|
|
// nodeText is the plain text beneath a node.
|
|
func nodeText(n ast.Node, src []byte) []byte {
|
|
var b bytes.Buffer
|
|
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
|
if t, ok := c.(*ast.Text); ok {
|
|
b.Write(t.Segment.Value(src))
|
|
continue
|
|
}
|
|
b.Write(nodeText(c, src))
|
|
}
|
|
return b.Bytes()
|
|
}
|