Artifact ece27ccbf63f46e5cd8c001b4c74971b9ef40001ac49be249ff68814efc79fb1:
- File
internal/pikchr/pikchr.go
— part of check-in
[58fe4cf214]
at
2026-08-16 12:53:54
on branch trunk
— markup: Pikchr diagrams via WebAssembly (fossil's third markup)
pikchr.c built to a standalone WASI module (-DPIKCHR_SHELL -sSTANDALONE_WASM) and run under wazero, so the CGO-free constraint holds. Its own CLI reads - and writes SVG, so the host ABI is a pipe: no memory marshalling to get wrong. Renders text/x-pikchr artifacts and pikchr fences inside markdown; a diagram that will not compile degrades to its source plus the compiler's message.
SVG does NOT ride the document policy. It gets a narrow allow-list derived by enumerating what the module actually emits (six elements, a closed geometry/paint attribute set, no script/href/foreignObject), so even a hole in Pikchr's own label escaping cannot reach a page. Fence placeholders are alphanumeric with a per-render nonce: an HTML parser rewrites control bytes, so a NUL-based marker does not survive sanitizing (caught by test). Provenance and the rebuild recipe are in the ledger. (user: dmestas, size: 3577) [annotate] [blame] [check-ins using]
// Package pikchr renders Pikchr diagrams — the third markup Fossil // dispatches on (text/x-pikchr, alongside fossil-wiki and markdown). // Pikchr is drh's C diagram language; quarry runs it as a WebAssembly // module under wazero, so the CGO-free constraint holds (the same // reason the SQLite driver is a Wasm build). // // The module is pikchr.c compiled with -DPIKCHR_SHELL to a standalone // WASI command; its own CLI reads "-" as stdin and writes SVG to // stdout, so the whole ABI is a pipe — no memory marshalling across the // host boundary, nothing to get wrong on a resize. Provenance and the // rebuild recipe live in third_party/PROVENANCE.md. package pikchr import ( "bytes" "context" _ "embed" "errors" "fmt" "strings" "sync" "time" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" ) //go:embed pikchr.wasm var wasmModule []byte // renderTimeout bounds one diagram. Pikchr is not Turing-complete and // real diagrams compile in single-digit milliseconds, so a generous // ceiling still contains a pathological input. const renderTimeout = 5 * time.Second // maxSVG caps a diagram's output; a wiki page is not a place to serve // megabytes of generated markup. const maxSVG = 4 << 20 var ( once sync.Once runtime wazero.Runtime compiled wazero.CompiledModule initErr error ) // ErrUnavailable reports that the Pikchr module could not be prepared; // callers fall back to preformatted text rather than failing a page. var ErrUnavailable = errors.New("pikchr: renderer unavailable") // prepare compiles the module once. Compilation is the expensive step // (tens of ms); instantiation per render is cheap and gives every // diagram a fresh, isolated memory. func prepare() { ctx := context.Background() runtime = wazero.NewRuntime(ctx) if _, err := wasi_snapshot_preview1.Instantiate(ctx, runtime); err != nil { initErr = fmt.Errorf("%w: wasi: %v", ErrUnavailable, err) return } c, err := runtime.CompileModule(ctx, wasmModule) if err != nil { initErr = fmt.Errorf("%w: compile: %v", ErrUnavailable, err) return } compiled = c } // Render compiles Pikchr source to an SVG fragment. The returned markup // is the module's own output and still passes through the caller's // sanitizer — this package does not gate. func Render(src []byte) ([]byte, error) { once.Do(prepare) if initErr != nil { return nil, initErr } ctx, cancel := context.WithTimeout(context.Background(), renderTimeout) defer cancel() var stdout, stderr bytes.Buffer cfg := wazero.NewModuleConfig(). WithName(""). // anonymous: instances must not collide WithArgs("pikchr", "--svg-only", "-"). WithStdin(bytes.NewReader(src)). WithStdout(&stdout). WithStderr(&stderr). WithSysNanotime() // the module reads no clock; keep it deterministic otherwise mod, err := runtime.InstantiateModule(ctx, compiled, cfg) if mod != nil { defer mod.Close(ctx) } if err != nil { // A non-zero exit is how pikchr reports a syntax error; its // diagnostic is on stderr and belongs to the author, not us. if msg := strings.TrimSpace(stderr.String()); msg != "" { return nil, fmt.Errorf("pikchr: %s", firstLine(msg)) } if ctx.Err() != nil { return nil, fmt.Errorf("pikchr: render exceeded %s", renderTimeout) } return nil, fmt.Errorf("pikchr: %w", err) } if stdout.Len() > maxSVG { return nil, fmt.Errorf("pikchr: diagram exceeds %d bytes", maxSVG) } return stdout.Bytes(), nil } func firstLine(s string) string { line, _, _ := strings.Cut(s, "\n") return line }