Skip to content
quarry Artifact [ece27ccbf6]
Login

Artifact [ece27ccbf6]

Artifact ece27ccbf63f46e5cd8c001b4c74971b9ef40001ac49be249ff68814efc79fb1:


// 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
}