261021.6 · 3b7c3e3a532072fcb3d4af911d225d2ec8295832
Graphviz fences render as diagrams
A fence marked dot or graphviz goes through the dot program and lands in the page as inline SVG, kept by the hash of its source. SERVER_NAME tells dot to refuse the image files a graph names, so a diagram shows nothing from outside the tree. Without dot, or on a bad graph, the block shows as code under a line that says why. The page's colours replace dot's black and white, so a graph reads in either theme.
reading declarations
Changes to graph.go. All files
@@ -0,0 +1,90 @@+package main++import (+ "bytes"+ "context"+ "crypto/sha256"+ "errors"+ "html/template"+ "os"+ "os/exec"+ "strings"+ "time"+)++// graphCache keeps the SVG that dot drew for each fence, by the hash of+// its source, so a doc with diagrams renders again within the budget.+var graphCache = newCache(8 << 20)++// graphTimeout bounds one run of dot; a large graph can take seconds.+const graphTimeout = 5 * time.Second++// maxGraph bounds the SVG a fence may produce.+const maxGraph = 4 << 20++// isGraph reports whether a fence's language names Graphviz.+func isGraph(lang string) bool {+ return lang == "dot" || lang == "graphviz"+}++// drawGraph returns a fence's diagram as inline SVG, drawn by dot. dot+// reads the source as data: SERVER_NAME makes it refuse the image files a+// graph may name, so a diagram cannot show what lies outside the tree.+// Without dot, or when dot rejects the source, the error says why.+func drawGraph(src string) (template.HTML, error) {+ sum := sha256.Sum256([]byte(src))+ key := string(sum[:])+ if svg, ok := graphCache.get(key); ok {+ return svg.(template.HTML), nil+ }+ dot, err := exec.LookPath("dot")+ if err != nil {+ return "", errors.New("dot is not installed; install graphviz to draw this graph")+ }+ ctx, cancel := context.WithTimeout(context.Background(), graphTimeout)+ defer cancel()+ cmd := exec.CommandContext(ctx, dot, "-Tsvg")+ // fontconfig keeps its font cache under home; without it every run+ // scans the fonts again.+ cmd.Env = []string{"SERVER_NAME=" + progName, "GV_FILE_PATH=/nonexistent"}+ for _, k := range []string{"HOME", "XDG_CACHE_HOME", "LANG"} {+ if v, ok := os.LookupEnv(k); ok {+ cmd.Env = append(cmd.Env, k+"="+v)+ }+ }+ cmd.Stdin = strings.NewReader(src)+ var out, stderr bytes.Buffer+ cmd.Stdout, cmd.Stderr = &limited{&out, maxGraph}, &stderr+ if err := cmd.Run(); err != nil {+ msg := strings.TrimSpace(stderr.String())+ if ctx.Err() != nil {+ msg = "dot took over " + graphTimeout.String()+ }+ if msg == "" {+ msg = err.Error()+ }+ return "", errors.New(msg)+ }+ // dot writes an XML prolog and doctype before the svg element, which+ // HTML does not take.+ svg := out.String()+ if i := strings.Index(svg, "<svg"); i >= 0 {+ svg = svg[i:]+ }+ h := template.HTML(`<div class="graph">` + svg + `</div>`)+ graphCache.put(key, h, int64(len(h)))+ return h, nil+}++// limited fails a write past n bytes, which stops dot.+type limited struct {+ w *bytes.Buffer+ n int+}++func (l *limited) Write(p []byte) (int, error) {+ if l.w.Len()+len(p) > l.n {+ return 0, errors.New("graph larger than 4 MB")+ }+ return l.w.Write(p)+}