package main // uidiskmap.go — the disk map page: a squarified treemap of this machine's own // disk, drawn server-side as SVG. // // WHY IT EXISTS. The ask was old-school WinDirStat: turn it on, it maps the // drive, and you get the 3D colour-coded layout of the drives — integrated // into this UI. It is the best demo this product has, and the reason is one // line: every other claim in the pitch becomes legible the moment a person can // SEE their own machine. "Your AI, on a network you own" — own is a thing you // have to be able to look at. // // WHAT IT READS: names, sizes and modification times. Nothing else, ever. The // walk and the two tests that hold that line are in diskmap.go; this file only // draws what that one counted, and it touches no disk at all. // // ── NO JAVASCRIPT, AND THE CSP IS WHY ────────────────────────────────────── // // Every page in this UI is served with `default-src 'self'; base-uri 'none'; // form-action 'self'` (ui.go renderWith), and default-src covers style-src. // So there is no 'unsafe-inline' and an inline style="" attribute is BLOCKED — // app.css:169-173 records the first time that bit somebody here, when an // inline style="margin-top:1rem" was silently losing to the policy. // // A treemap is a thousand rectangles at a thousand different positions, which // is the single most natural thing in the world to write as inline styles. It // is written as SVG instead, and that is not a workaround — it is the correct // tool: // // - x, y, width, height and fill on an are SVG PRESENTATION // ATTRIBUTES, not CSS. style-src does not govern them, so per-rect // geometry needs no policy hole at all. // - inside a rect is the browser's own native tooltip. No JS, no // hover handler, no CSS. // - <a href> inside SVG is an ordinary link, so drilling into a folder is a // navigation. The browser's BACK BUTTON becomes "drill back out" for free, // history works, and a middle-click opens a subtree in a new tab. // // The rejected alternative was a client-side layout with d3-hierarchy, which // is what everyone reaches for. It fails three ways here at once: it is a CDN // fetch in a privacy product's local UI (ui.go's header forbids it and a // locked-down box would fail the load), or it is another 90 KB vendored into a // security binary; it needs the whole tree shipped to the browser as JSON, // which is 800,000 entries of this machine's filenames serialised into a // document; and it would need script-src widened. Laying out in Go costs one // function (dmSquarify) and ships the ANSWER instead of the data. // // TestTheMapNeverEmitsInlineStyle asserts the whole point: no style attribute // and no <style> element ever reaches the page. import ( "fmt" "html" "html/template" "math" "net/http" "net/url" "path/filepath" "sort" "strconv" "strings" "time" ) // ── the drawing budget, measured ──────────────────────────────────────────── // The canvas is a viewBox, not pixels — diskmap.css scales it to the width of // the page. 1000×620 is close to 16:10 because a squarified treemap's aspect // ratios are best in a near-square container and worst in a long thin one, and // a panel-shaped map is what a person recognises as WinDirStat. const ( dmCanvasW = 1000.0 dmCanvasH = 620.0 ) // dmMinCellArea is the smallest rectangle worth drawing, in viewBox units. // Below it the cell is not emitted at all and is counted into the "too small // to draw" line, which the page prints with the number of items and their // total — so the map never quietly omits, it says what it left out. // // 9 is a 3×3 cell. Under a 3-unit side the rect is sub-pixel on a 1366-wide // laptop once the 1000-unit viewBox is scaled down, so it would be paint with // no information in it — and a page full of those is what makes an SVG // treemap slow for nothing. const dmMinCellArea = 9.0 // dmMinRecurseArea is when a directory is opened up rather than drawn as one // solid block. It is deliberately well above dmMinCellArea: a folder is only // worth unpacking if its own children can clear dmMinCellArea inside it, and a // 400-unit box that holds forty children gives each of them ten. const dmMinRecurseArea = 400.0 // dmMaxDepth is how many levels below the folder being viewed get drawn. // // Four, and the reason is legibility rather than cost: past four nestings the // rectangles are fragments of fragments and a person cannot tell which folder // any of them belongs to, so the extra levels add paint and remove meaning. // Measured max depth on this box's whole-machine walk is 19, so this is a real // cut and the page says so — "click a folder to go deeper" is the interaction // that replaces it. const dmMaxDepth = 4 // dmMaxCells is the hard ceiling on rectangles in one picture, and it is a // PAGE-WEIGHT number rather than a legibility one — dmMinCellArea is what // decides legibility, and in practice it is also what decides the count. // // MEASURED on one build box (TestMeasureARealMap, which is how to redo // this rather than trust it): // // root entries cells too small SVG per cell drawn in // /root 323,963 4,022 30,490 894 KB 227 B 7.1 ms // / 800,341 1,229 6,776 284 KB 236 B 3.4 ms // // So the real ceiling is dmMinCellArea, and a whole-machine map is SMALLER // than a home-directory one because at the top level a handful of enormous // folders take the canvas and everything else falls under three pixels. 6,000 // is roughly half again on the largest measured picture: it is a backstop // against a pathological tree, not the number that shapes the ordinary one, // and neither measured root comes near it. // // ~900 KB of markup is more than a text page and it is the right trade. It is // an in-process string over a loopback socket with no network in it, a browser // lays out 8,000 SVG nodes in well under a frame, and the alternative — a // chunky map of 500 blocks — loses the exact thing that makes a person // recognise their own disk. const dmMaxCells = 6000 // dmBiggestRows is the length of the "biggest things here" table under the // map. The table exists because the map's small rectangles are unreadable by // construction: everything big enough to see is already a block you can hover, // so the list's whole job is to name the top of the ordering exactly. Fifteen // covers a directory's heavy hitters without becoming a file manager, and the // page prints how many more there are. const dmBiggestRows = 15 // ── colour by type, the way WinDirStat does ───────────────────────────────── // dmKindDef is one colour in the legend. The extension list is the whole rule: // there is no content sniffing anywhere in this feature, and a NAME is all the // walk ever read, so the type shown is the type the name claims. That is worth // being explicit about — a page that said "this is a video" after opening the // file would be a different product. type dmKindDef struct { Key string Label string Fill string Exts []string } // dmKinds, heaviest-first in the order a person scanning the legend cares // about. Colours are chosen against the panel background (app.css --bg // #16181d) so every one of them reads as itself on a dark page. // // `model` is here and is not a category WinDirStat would have had. It earns // its place on this product: the host can pull models onto this machine // (model:pull, modelswap.go) and they are the largest single files most of // these boxes will ever hold, so "what is that 40 GB" has to have an answer // better than "other". var dmKinds = []dmKindDef{ // `ts` is deliberately NOT here, and it is the one genuinely ambiguous // extension in this table: an MPEG transport stream and a TypeScript source // share it. It is filed under code because of who runs this — a machine with // the Privatae bastion on it is far more likely to hold TypeScript than raw // broadcast video, and colouring a node_modules tree as VIDEO would make the // map actively misleading in the direction that matters. Caught by the // duplicate check in TestTheTypeIsReadOffTheNameAndNothingElse, which exists // because dmKindIndex is built in order and the LATER kind silently wins. {"video", "Video", "#b45bd1", []string{"mp4", "mkv", "mov", "avi", "wmv", "webm", "m4v", "mpg", "mpeg", "flv", "m2ts"}}, {"image", "Images", "#4aa3d8", []string{"jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff", "webp", "heic", "heif", "raw", "cr2", "nef", "arw", "svg", "ico", "psd", "ai"}}, {"audio", "Audio", "#d8843c", []string{"mp3", "wav", "flac", "aac", "ogg", "oga", "m4a", "wma", "aiff", "opus"}}, {"model", "Models", "#e0658a", []string{"gguf", "safetensors", "ckpt", "pt", "pth", "onnx", "mlmodel", "tflite", "npz"}}, // pack/idx are git packfiles and pak is a Chromium resource bundle. They // are here because the measurement put them there: 1.7 GB of .pack on this // box alone, which as "everything else" tells a user nothing about why // their repositories are large. {"archive", "Archives", "#d15a52", []string{"zip", "tar", "gz", "tgz", "bz2", "xz", "zst", "7z", "rar", "jar", "war", "iso", "dmg", "pkg", "deb", "rpm", "cab", "msi", "apk", "whl", "pack", "idx", "pak"}}, {"vmdisk", "Disk images", "#8a6fd1", []string{"vmdk", "vdi", "qcow2", "vhd", "vhdx", "img", "ova", "vbox"}}, {"document", "Documents", "#52b788", []string{"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "txt", "md", "rtf", "epub", "mobi", "csv", "tsv", "pages", "numbers", "key"}}, {"code", "Code and config", "#e0c74a", []string{"go", "py", "js", "mjs", "cjs", "ts", "tsx", "jsx", "c", "h", "cc", "cpp", "hpp", "rs", "java", "rb", "php", "cs", "swift", "kt", "scala", "sh", "bash", "ps1", "sql", "html", "htm", "css", "scss", "json", "jsonl", "yaml", "yml", "toml", "ini", "conf", "xml", "proto", "lua", "vim", "el", "map", "mod", "sum", "lock"}}, {"binary", "Programs", "#7e8aa0", []string{"exe", "dll", "so", "dylib", "bin", "o", "a", "lib", "pdb", "wasm", "class", "pyc", "node", "ko", "sys", "app", "rlib", "rmeta"}}, {"db", "Databases", "#3fb5b5", []string{"db", "sqlite", "sqlite3", "sqlitedb", "mdb", "accdb", "realm", "ldb", "sst", "myd", "frm", "wal"}}, {"logtmp", "Logs and temporary", "#6a7482", []string{"log", "tmp", "temp", "cache", "bak", "swp", "swo", "old", "dmp", "crash", "part", "download"}}, // EVERYTHING ELSE IS THE BIGGEST CATEGORY AND THAT IS NOT A GAP TO CLOSE. // Measured on this box: 15.6 GB of the 44.2 GB is files with NO EXTENSION // AT ALL — container layers, git objects, caches, binaries with no suffix. // A name is the only thing this feature ever reads, so for those files the // honest answer is that it does not know, and the way to find out would be // to open them. That is the line diskmap.go's header exists to hold, and // the legend is where a user meets it: the largest block on their disk is // labelled "everything else" because this program did not look inside it. {"other", "Everything else", "#4e5563", nil}, } // dmFolderFill is a folded directory whose contents have no dominant type — // empty, or unreadable, or genuinely mixed. The darkest thing on the map, // because "there is something in here and I cannot characterise it" should not // look like an answer. const dmFolderFill = "#2b323d" // dmDominantKind is the type holding the most BYTES under a node, or "" when // there is nothing under it. // // WHY A FOLDED FOLDER IS COLOURED AT ALL. The first cut drew every folded // directory in dmFolderFill, on the reasoning that a folder is not a file and // must not be mistakable for one. Rendered against this box it produced a grey // desert: a map of /root came out as a few thousand near-identical dark blocks // with a bright corner, because at four levels deep almost everything is a // folded folder. That is honest and it is unreadable, and an unreadable map is // not a more honest one — it just says less. // // So a folded folder takes the colour of what is inside it, which is both // prettier and MORE informative: a block of video reads purple whether the // video is one file or four hundred, and that is exactly what WinDirStat's // picture conveys by drawing every file individually. The <title> still says // "folder", and the size beside it is the roll-up, so nothing about the block // claims to be a single file. // // COST IS O(n) OVER THE WHOLE MAP, not O(n) per folded block: once a directory // is folded the renderer does not recurse into it, so the subtrees this walks // are disjoint and their sizes sum to the tree. Measured by // TestMeasureARealMap on a whole-machine map of 801,782 nodes, drawing the // whole picture went from 3.4 ms to 25.9 ms when this was added — one extra // pass over the tree, paid once per render, and invisible beside the 1.9 s the // walk itself takes. func dmDominantKind(res *dmResult, at int32) string { bytes := map[string]int64{} stack := []int32{at} for len(stack) > 0 { i := stack[len(stack)-1] stack = stack[:len(stack)-1] for c := res.Nodes[i].FirstChild; c >= 0; c = res.Nodes[c].NextSibling { n := &res.Nodes[c] if n.Dir { stack = append(stack, c) continue } bytes[dmKindOf(n.Name)] += n.Size } } best, bestN := "", int64(0) // dmKinds order breaks a tie, so the same folder is always the same colour // rather than whichever key the map handed back first. for _, kd := range dmKinds { if bytes[kd.Key] > bestN { best, bestN = kd.Key, bytes[kd.Key] } } return best } // dmKindIndex maps an extension to its kind, built once. var dmKindIndex = func() map[string]string { m := map[string]string{} for _, k := range dmKinds { for _, e := range k.Exts { m[e] = k.Key } } return m }() var dmKindByKey = func() map[string]dmKindDef { m := map[string]dmKindDef{} for _, k := range dmKinds { m[k.Key] = k } return m }() // dmKindOf reads the type off the NAME and nothing else. // // A dotfile with no other dot ( .bashrc, .env ) has no extension — the leading // dot is not one — and lands in "everything else". That is right rather than a // gap: guessing that .env is config would be inventing a fact about a file // this code deliberately never opened. func dmKindOf(name string) string { i := strings.LastIndexByte(name, '.') if i <= 0 || i == len(name)-1 { return "other" } if k, ok := dmKindIndex[strings.ToLower(name[i+1:])]; ok { return k } return "other" } func dmFillOf(kind string) string { if k, ok := dmKindByKey[kind]; ok { return k.Fill } return dmKindByKey["other"].Fill } // ── the squarified layout ─────────────────────────────────────────────────── type dmRect struct{ X, Y, W, H float64 } func (r dmRect) area() float64 { return r.W * r.H } // dmSquarify is Bruls, Huizing and van Wijk's squarified treemap (2000): lay // the values out in rows along the shortest side of the remaining rectangle, // growing each row for exactly as long as doing so IMPROVES the worst aspect // ratio in it. // // Why squarified rather than the obvious slice-and-dice, which is ten lines: // slice-and-dice gives every cell the full height of its parent, so a folder // with two hundred children is two hundred vertical hairlines. Nothing is // hoverable, nothing is clickable, and the picture carries no shape. Aspect // ratio is the whole reason a treemap is legible, and it is the only thing // this algorithm optimises. // // vals MUST be descending — dmChildren sorts by size, which is where they come // from — because the greedy row-growing is only near-optimal on sorted input. // Zero and negative values get an empty rectangle and take no space: a 0-byte // file has no area, and dividing by it is how this returns NaN. func dmSquarify(vals []float64, r dmRect) []dmRect { out := make([]dmRect, len(vals)) if len(vals) == 0 || r.W <= 0 || r.H <= 0 { return out } total := 0.0 for _, v := range vals { if v > 0 { total += v } } if total <= 0 { return out } scale := r.area() / total areas := make([]float64, len(vals)) for i, v := range vals { if v > 0 { areas[i] = v * scale } } cur := r i := 0 for i < len(areas) { if cur.W <= 0 || cur.H <= 0 { break } if areas[i] <= 0 { // Everything from here on is zero (the input is descending), so // there is nothing left with any area to place. break } side := math.Min(cur.W, cur.H) sum := areas[i] j := i + 1 best := dmWorstRatio(areas[i:j], sum, side) for j < len(areas) && areas[j] > 0 { s2 := sum + areas[j] w2 := dmWorstRatio(areas[i:j+1], s2, side) if w2 > best { break } best, sum, j = w2, s2, j+1 } cur = dmLayRow(areas[i:j], sum, cur, out[i:j]) i = j } return out } // dmWorstRatio is the worst aspect ratio a row would have if it were laid out // now: the row's thickness is sum/side, so the largest member is the flattest // and the smallest is the thinnest, and one of those two is always the worst. func dmWorstRatio(row []float64, sum, side float64) float64 { if len(row) == 0 || sum <= 0 || side <= 0 { return math.Inf(1) } mx, mn := row[0], row[0] for _, a := range row { if a > mx { mx = a } if a < mn { mn = a } } if mn <= 0 { return math.Inf(1) } s2, side2 := sum*sum, side*side return math.Max(side2*mx/s2, s2/(side2*mn)) } // dmLayRow places one finished row and returns what is left of the rectangle. func dmLayRow(areas []float64, sum float64, cur dmRect, out []dmRect) dmRect { if cur.H <= cur.W { // The height is the shortest side, so the row is a COLUMN down the // left edge and the remainder is everything to its right. w := sum / cur.H y := cur.Y for k, a := range areas { h := a / w out[k] = dmRect{X: cur.X, Y: y, W: w, H: h} y += h } return dmRect{X: cur.X + w, Y: cur.Y, W: cur.W - w, H: cur.H} } h := sum / cur.W x := cur.X for k, a := range areas { w := a / h out[k] = dmRect{X: x, Y: cur.Y, W: w, H: h} x += w } return dmRect{X: cur.X, Y: cur.Y + h, W: cur.W, H: cur.H - h} } // ── drawing ───────────────────────────────────────────────────────────────── // dmDraw walks the tree and the layout together, emitting SVG. type dmDraw struct { res *dmResult sb strings.Builder // cells is what was actually drawn; tiny/tinyBytes is what was not, and // the page prints both because the second number is the honesty. cells int tiny int tinyBytes int64 // budget exhausted: dmMaxCells was reached and the picture is incomplete // for a reason that is this renderer's and not the disk's. overflow bool } // dmNumber formats a coordinate. One decimal place: integers visibly misalign // adjacent cells at small sizes (two rects rounding the same edge in opposite // directions leave a gap or overlap), and a second decimal adds bytes to every // attribute of every cell for precision below a tenth of a viewBox unit, which // is a fiftieth of a pixel on screen. func dmNumber(f float64) string { return strconv.FormatFloat(f, 'f', 1, 64) } func dmEsc(s string) string { return html.EscapeString(s) } // draw emits one node's children into r, recursing while the rectangles stay // big enough to mean anything. func (d *dmDraw) draw(node int32, r dmRect, depth int) { if depth > dmMaxDepth || r.W <= 0 || r.H <= 0 { return } kids := d.res.dmChildren(node) if len(kids) == 0 { return } vals := make([]float64, len(kids)) for i, k := range kids { vals[i] = float64(d.res.Nodes[k].Size) } rects := dmSquarify(vals, r) for i, k := range kids { rect := rects[i] n := &d.res.Nodes[k] if rect.area() < dmMinCellArea { // Not drawn, but COUNTED. Every byte under here is still inside // its parent's rectangle — the map's totals stay exact and only // the detail is gone, which is the difference between a cap and a // lie. d.tiny++ d.tinyBytes += n.Size continue } if d.cells >= dmMaxCells { d.overflow = true return } if n.Dir && depth < dmMaxDepth && rect.area() >= dmMinRecurseArea { // A folder big enough to open: draw it as its own dark ground and // then lay its children on top, inset by one unit so the boundary // between two folders is visible as a hairline of background. d.cell(k, rect, dmFolderFill, true) in := dmRect{X: rect.X + 1, Y: rect.Y + 1, W: rect.W - 2, H: rect.H - 2} if in.W > 0 && in.H > 0 { d.draw(k, in, depth+1) } continue } fill := dmFolderFill switch { case !n.Dir: fill = dmFillOf(dmKindOf(n.Name)) default: // Folded: colour it by what is in it. See dmDominantKind. if kind := dmDominantKind(d.res, k); kind != "" { fill = dmFillOf(kind) } } d.cell(k, rect, fill, n.Dir) } } // cell emits one rectangle, its tooltip, and — where it is big enough for the // effect to be visible — the cushion overlay that makes the map look like // WinDirStat rather than like a bar chart. // // THE CUSHION IS ONE GRADIENT IN <defs>, REUSED. WinDirStat's look is Van // Wijk's cushion shading, which is a per-pixel surface normal and is not // something SVG does cheaply. What IS cheap is a second rect over the first // carrying a shared linearGradient in objectBoundingBox units, which is the // SVG default — so one definition scales itself to every cell and gives each // block a light top-left and a dark bottom-right. It costs one <defs> entry // and 44 bytes per cell, and it is skipped below dmCushionFrom because at // three pixels across there is nothing for a highlight to fall on. func (d *dmDraw) cell(node int32, r dmRect, fill string, isDir bool) { d.cells++ n := &d.res.Nodes[node] x, y, w, h := dmNumber(r.X), dmNumber(r.Y), dmNumber(r.W), dmNumber(r.H) // A folder is a link and a file is not, because there is nowhere for a // file to lead. The <a> wrapper is also the largest per-cell cost, so // spending it only where it does something is most of why a whole-machine // map fits in one page. if isDir { d.sb.WriteString(`<a href="/diskmap?p=` + dmEsc(url.QueryEscape(d.res.dmPath(node))) + `">`) } d.sb.WriteString(`<rect x="` + x + `" y="` + y + `" width="` + w + `" height="` + h + `" fill="` + fill + `">`) d.sb.WriteString(`<title>` + dmEsc(n.Name) + " · " + dmEsc(dmBytes(n.Size))) if isDir { d.sb.WriteString(" · folder") } if n.Mtime > 0 { d.sb.WriteString(" · " + dmEsc(time.Unix(n.Mtime, 0).Local().Format("2 Jan 2006"))) } d.sb.WriteString(``) if r.area() >= dmCushionFrom { d.sb.WriteString(``) } if isDir { d.sb.WriteString(``) } } // dmCushionFrom is the smallest cell that gets the 3D overlay. 36 units is a // 6×6 block; below that the highlight and the shadow are each under three // units wide and the eye reads the whole cell as one flat muddier colour — so // the overlay would cost bytes to make the map LESS legible, not more. const dmCushionFrom = 36.0 // dmSVG renders the map of one node. func dmSVG(res *dmResult, node int32) (svg string, cells, tiny int, tinyBytes int64, overflow bool) { d := &dmDraw{res: res} d.sb.WriteString(``) // One gradient, reused by every cell. x2/y2 put the light source at the // top-left, which is the convention every desktop treemap uses and the one // a person's eye already expects. d.sb.WriteString(`` + `` + `` + `` + ``) d.draw(node, dmRect{X: 0, Y: 0, W: dmCanvasW, H: dmCanvasH}, 1) d.sb.WriteString(``) return d.sb.String(), d.cells, d.tiny, d.tinyBytes, d.overflow } // ── bytes in words ────────────────────────────────────────────────────────── // dmBytes spells a size the way this machine already spells one. // // It DELEGATES to modelBytesWords (modelswap.go:1060) from a megabyte up, // rather than writing a second formatter, so the number a user reads on this // page and the number they read when approving a model download are spelled // identically. The kilobyte tier below it is the one thing this page needs and // that one does not: a disk map is mostly small files, and "487424 bytes" on // every row is technically the same fact and unreadable. func dmBytes(n int64) string { if n >= 1<<20 { return modelBytesWords(n) } if n >= 1<<10 { return fmt.Sprintf("%.0f KB", float64(n)/float64(1<<10)) } return fmt.Sprintf("%d bytes", n) } // dmAgo says how old something is in the words a person uses for it. func dmAgo(d time.Duration) string { switch { case d < time.Minute: return "just now" case d < 2*time.Minute: return "a minute ago" case d < time.Hour: return fmt.Sprintf("%d minutes ago", int(d.Minutes())) case d < 2*time.Hour: return "an hour ago" case d < 24*time.Hour: return fmt.Sprintf("%d hours ago", int(d.Hours())) } return fmt.Sprintf("%d days ago", int(d.Hours()/24)) } // dmSpan says how long something took or has been running. func dmSpan(d time.Duration) string { if d < time.Minute { if n := int(d.Seconds() + 0.5); n == 1 { return "1 second" } return fmt.Sprintf("%.0f seconds", d.Seconds()) } m := int(d.Minutes()) s := int(d.Seconds()) % 60 return fmt.Sprintf("%dm %02ds", m, s) } // ── what the page shows ───────────────────────────────────────────────────── type uiDiskCrumb struct { Name string Path string Here bool } type uiDiskRow struct { Name string Path string Size string Bytes int64 Share int // whole percent of the folder being viewed Dir bool Kind string Fill string Modified string } type uiDiskLegendRow struct { Label string Fill string Size string Items int64 } type uiDiskRoot struct { Path string Size string } type uiDiskMap struct { Version string CheckedAt string // Scanning and what it has counted so far. Scanning bool Running string SeenAt string Entries int64 SeenSize string // The map itself. HaveMap bool TakenAgo string TookFor string Stale bool Roots []uiDiskRoot RootNotes []string // Scope is what this map covers — "home" or "machine" — and ScopeWords is // the same thing in the words the page says out loud. Both are carried // because the template needs to COMPARE (to decide which offer to make) and // to PRINT, and doing the comparison on the printed words is how a copy // edit silently changes behaviour. Scope string ScopeWords string // MachineRoots is what a whole-machine request would walk. Shown BEFORE // that request is made, never after. MachineRoots []string Path string Crumbs []uiDiskCrumb Total string Items int NotFound bool // Want is the path the person ASKED for, which is not always one this map // can resolve yet. It exists because a re-scan starts with an empty map, so // Path is unset, so Query was empty, so the fragment's own poll came back // with no ?p= and dropped the user at the root the moment the scan finished. // Path answers "where are you"; Want answers "where // were you going", and only the poll needs the second. Want string // SVG is template.HTML because it IS markup this file built, and every // value that came off the disk went through dmEsc on the way in. That is // the whole bargain of the type: it turns escaping from something the // template does into something this file is responsible for, so // TestAFilenameCannotInjectMarkupIntoTheMap exists to hold the other end // of it with a file literally named `