// SPDX-License-Identifier: AGPL-3.0-or-later package web import ( "context" "strings" "time" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/lexers" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" goldhtml "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/util" ) const ( markdownBlockBytes = 64 << 10 markdownDocumentBytes = 256 << 10 markdownLexTime = 200 * time.Millisecond markdownBudgetKey = "markdown-lex-budget" ) // Budget belongs to the parsed document, never to the shared renderer. // Charge attempts, including failures, so repeated bad blocks cannot bypass it. type markdownLexBudget struct { bytes, blocks int deadline time.Time } type fencedRenderer struct{} // Capture goldmark's default renderer rather than duplicate its escaping rules. type fencedFallback struct{ render renderer.NodeRendererFunc } func (f *fencedFallback) Register(k ast.NodeKind, fn renderer.NodeRendererFunc) { if k == ast.KindFencedCodeBlock { f.render = fn } } func (fencedRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) { var fallback fencedFallback goldhtml.NewRenderer().RegisterFuncs(&fallback) r.Register(ast.KindFencedCodeBlock, func(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.FencedCodeBlock) if !entering { return ast.WalkContinue, nil } toks, lang, ok := markdownTokens(n, source) if !ok { // Default rendering is plain escaped text. Omit unsafe language classes. if !safeMarkdownLanguage(string(n.Language(source))) { plain := ast.NewFencedCodeBlock(nil) plain.SetLines(n.Lines()) n = plain } _, err := fallback.render(w, source, n, true) if err != nil { return ast.WalkStop, err } return fallback.render(w, source, n, false) } _, _ = w.WriteString(`
`)
		// EnsureNL lexers may append a newline: never emit bytes not in the block.
		remaining := 0
		for i := 0; i < n.Lines().Len(); i++ {
			line := n.Lines().At(i)
			remaining += len(line.Value(source))
		}
		// The tokens are not coalesced (chroma's coalescer copies strings
		// quadratically): adjacent tokens of one class share a span here.
		open := ""
		for _, tk := range toks {
			v := tk.Value
			if len(v) > remaining {
				v = v[:remaining]
			}
			remaining -= len(v)
			if v == "" {
				continue
			}
			if c := tokenClass(tk.Type); c != open {
				if open != "" {
					_, _ = w.WriteString(``)
				}
				if c != "" {
					_, _ = w.WriteString(``)
				}
				open = c
			}
			writeMarked(w, v)
		}
		if open != "" {
			_, _ = w.WriteString(``)
		}
		_, _ = w.WriteString("
\n") return ast.WalkSkipChildren, nil }) } func safeMarkdownLanguage(s string) bool { if s == "" { return false } for _, c := range s { switch { case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', strings.ContainsRune("_+.#-", c): default: return false } } return true } func markdownTokens(n *ast.FencedCodeBlock, source []byte) ([]chroma.Token, string, bool) { var lang string if n.Info != nil { if words := strings.Fields(string(n.Info.Segment.Value(source))); len(words) > 0 { lang = words[0] } } if !safeMarkdownLanguage(lang) { return nil, "", false } value, _ := n.OwnerDocument().AttributeString(markdownBudgetKey) budget, ok := value.(*markdownLexBudget) if !ok || budget.blocks >= 128 || time.Now().After(budget.deadline) { return nil, "", false } size := 0 for i := 0; i < n.Lines().Len(); i++ { line := n.Lines().At(i) size += len(line.Value(source)) if size > markdownBlockBytes { return nil, "", false } } if size > markdownDocumentBytes-budget.bytes { return nil, "", false } budget.blocks++ budget.bytes += size lx := lexers.Get(lang) if lx == nil { return nil, "", false } select { case lexSlots <- struct{}{}: defer func() { <-lexSlots }() default: return nil, "", false } var code strings.Builder code.Grow(size) for i := 0; i < n.Lines().Len(); i++ { line := n.Lines().At(i) code.Write(line.Value(source)) } ctx, cancel := context.WithDeadline(context.Background(), budget.deadline) defer cancel() toks, ok := tokenise(ctx, lx, code.String()) return toks, lang, ok }