Download
50 lines 1.6 KiBGoModified 2026-10-17 raw
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
package render

import (
	"bytes"
	"encoding/xml"
	"slices"
	"strings"
)

// svgRootEnd returns the offset just past the root <svg ...> start tag,
// where themeSVG injects its style, or -1 when the document does not start
// with an <svg> element, the root is empty (<svg/>), or the XML decoder
// cannot read that far. Nothing in an SVG needs vetting: it is embedded
// through <img>, which runs no script, and served under httpd's sandbox
// CSP when opened directly.
func svgRootEnd(data []byte) int {
	d := xml.NewDecoder(bytes.NewReader(data))
	d.Strict = true
	d.Entity = xml.HTMLEntity
	for {
		tok, err := d.RawToken()
		if err != nil {
			return -1
		}
		if t, ok := tok.(xml.StartElement); ok {
			if off := int(d.InputOffset()); strings.EqualFold(t.Name.Local, "svg") && data[off-2] != '/' {
				return off
			}
			return -1
		}
	}
}

// themeSVG returns a copy of an SVG document with a <style> injected at
// rootEnd, just after the root start tag (as svgRootEnd reports it), that sets
// the document colour, so currentColor resolves to the page foreground when
// the SVG is shown through <img>. With rootEnd < 0 or no colour, data is
// returned unchanged.
func themeSVG(data []byte, rootEnd int, colour, light string) []byte {
	if rootEnd < 0 || colour == "" {
		return data
	}
	css := `:root{color:` + colour + `}`
	if light != "" {
		// An image cannot see the page's theme toggle, only the system
		// preference; that is the best it can follow.
		css += `@media (prefers-color-scheme:light){:root{color:` + light + `}}`
	}
	return slices.Concat(data[:rootEnd], []byte(`<style>`+css+`</style>`), data[rootEnd:])
}