A reply streams as text as before; once it ends, the server renders it with the model card's rules - raw HTML dropped, dangerous links emptied, images as links - and the page places that. Headings, lists, tables and code blocks read as they were written. Maths outside code is set aside before the markdown, so its underscores and asterisks are not taken for emphasis, and marked for the page to typeset. KaTeX no longer searches the reply for delimiters, which took "$5 and $10" for a formula. A reply that cannot be rendered stays plain text, with the old search. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
121 lines
3.7 KiB
Go
121 lines
3.7 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 {
|
|
return safeMarkdown(card, repoBase)
|
|
}
|
|
|
|
// safeMarkdown renders a stranger's markdown under the rules above. base, when
|
|
// set, is what relative links resolve against.
|
|
func safeMarkdown(src, base string) template.HTML {
|
|
if strings.TrimSpace(src) == "" {
|
|
return ""
|
|
}
|
|
md := goldmark.New(
|
|
goldmark.WithExtensions(extension.GFM),
|
|
goldmark.WithParserOptions(parser.WithASTTransformers(
|
|
util.Prioritized(cardLinks{base: base}, 100))),
|
|
goldmark.WithRendererOptions(
|
|
renderer.WithNodeRenderers(util.Prioritized(imageAsLink{}, 100))),
|
|
)
|
|
var out bytes.Buffer
|
|
if err := md.Convert([]byte(src), &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()
|
|
}
|