1 // Copyright 2017 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package driver 16 17 import "html/template" 18 19 // addTemplates adds a set of template definitions to templates. 20 func addTemplates(templates *template.Template) { 21 template.Must(templates.Parse(` 22 {{define "css"}} 23 <style type="text/css"> 24 html { 25 height: 100%; 26 min-height: 100%; 27 margin: 0px; 28 } 29 body { 30 margin: 0px; 31 width: 100%; 32 height: 100%; 33 min-height: 100%; 34 overflow: hidden; 35 } 36 #graphcontainer { 37 display: flex; 38 flex-direction: column; 39 height: 100%; 40 min-height: 100%; 41 width: 100%; 42 min-width: 100%; 43 margin: 0px; 44 } 45 #graph { 46 flex: 1 1 auto; 47 overflow: hidden; 48 } 49 svg { 50 width: 100%; 51 height: auto; 52 } 53 button { 54 margin-top: 5px; 55 margin-bottom: 5px; 56 } 57 #detailtext { 58 display: none; 59 position: fixed; 60 top: 20px; 61 right: 10px; 62 background-color: #ffffff; 63 min-width: 160px; 64 border: 1px solid #888; 65 box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2); 66 z-index: 1; 67 } 68 #closedetails { 69 float: right; 70 margin: 2px; 71 } 72 #home { 73 font-size: 14pt; 74 padding-left: 0.5em; 75 padding-right: 0.5em; 76 float: right; 77 } 78 .menubar { 79 display: inline-block; 80 background-color: #f8f8f8; 81 border: 1px solid #ccc; 82 width: 100%; 83 } 84 .menu-header { 85 position: relative; 86 display: inline-block; 87 padding: 2px 2px; 88 font-size: 14pt; 89 } 90 .menu { 91 display: none; 92 position: absolute; 93 background-color: #f8f8f8; 94 border: 1px solid #888; 95 box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2); 96 z-index: 1; 97 margin-top: 2px; 98 left: 0px; 99 min-width: 5em; 100 } 101 .menu-header, .menu { 102 cursor: default; 103 user-select: none; 104 -moz-user-select: none; 105 -ms-user-select: none; 106 -webkit-user-select: none; 107 } 108 .menu hr { 109 background-color: #fff; 110 margin-top: 0px; 111 margin-bottom: 0px; 112 } 113 .menu a, .menu button { 114 display: block; 115 width: 100%; 116 margin: 0px; 117 padding: 2px 0px 2px 0px; 118 text-align: left; 119 text-decoration: none; 120 color: #000; 121 background-color: #f8f8f8; 122 font-size: 12pt; 123 border: none; 124 } 125 .menu-header:hover { 126 background-color: #ccc; 127 } 128 .menu a:hover, .menu button:hover { 129 background-color: #ccc; 130 } 131 .menu a.disabled { 132 color: gray; 133 pointer-events: none; 134 } 135 #searchbox { 136 margin-left: 10pt; 137 } 138 #bodycontainer { 139 width: 100%; 140 height: 100%; 141 max-height: 100%; 142 overflow: scroll; 143 padding-top: 5px; 144 } 145 #toptable { 146 border-spacing: 0px; 147 width: 100%; 148 padding-bottom: 1em; 149 } 150 #toptable tr th { 151 border-bottom: 1px solid black; 152 text-align: right; 153 padding-left: 1em; 154 padding-top: 0.2em; 155 padding-bottom: 0.2em; 156 } 157 #toptable tr td { 158 padding-left: 1em; 159 font: monospace; 160 text-align: right; 161 white-space: nowrap; 162 cursor: default; 163 } 164 #toptable tr th:nth-child(6), 165 #toptable tr th:nth-child(7), 166 #toptable tr td:nth-child(6), 167 #toptable tr td:nth-child(7) { 168 text-align: left; 169 } 170 #toptable tr td:nth-child(6) { 171 max-width: 30em; // Truncate very long names 172 overflow: hidden; 173 } 174 #flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr { 175 cursor: ns-resize; 176 } 177 .hilite { 178 background-color: #ccf; 179 } 180 </style> 181 {{end}} 182 183 {{define "header"}} 184 <div id="detailtext"> 185 <button id="closedetails">Close</button> 186 {{range .Legend}}<div>{{.}}</div>{{end}} 187 </div> 188 189 <div class="menubar"> 190 191 <div class="menu-header"> 192 View 193 <div class="menu"> 194 <a title="{{.Help.top}}" href="/top" id="topbtn">Top</a> 195 <a title="{{.Help.graph}}" href="/" id="graphbtn">Graph</a> 196 <a title="{{.Help.peek}}" href="/peek" id="peek">Peek</a> 197 <a title="{{.Help.list}}" href="/source" id="list">Source</a> 198 <a title="{{.Help.disasm}}" href="/disasm" id="disasm">Disassemble</a> 199 <hr> 200 <button title="{{.Help.details}}" id="details">Details</button> 201 </div> 202 </div> 203 204 <div class="menu-header"> 205 Refine 206 <div class="menu"> 207 <a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a> 208 <a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a> 209 <a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a> 210 <a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a> 211 <hr> 212 <a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a> 213 </div> 214 </div> 215 216 <input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40> 217 218 <span id="home">{{.Title}}</span> 219 220 </div> <!-- menubar --> 221 222 <div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div> 223 {{end}} 224 225 {{define "graph" -}} 226 <!DOCTYPE html> 227 <html> 228 <head> 229 <meta charset="utf-8"> 230 <title>{{.Title}}</title> 231 {{template "css" .}} 232 </head> 233 <body> 234 235 {{template "header" .}} 236 <div id="graphcontainer"> 237 <div id="graph"> 238 {{.HTMLBody}} 239 </div> 240 241 </div> 242 {{template "script" .}} 243 <script>viewer({{.BaseURL}}, {{.Nodes}})</script> 244 </body> 245 </html> 246 {{end}} 247 248 {{define "script"}} 249 <script> 250 // Make svg pannable and zoomable. 251 // Call clickHandler(t) if a click event is caught by the pan event handlers. 252 function initPanAndZoom(svg, clickHandler) { 253 'use strict'; 254 255 // Current mouse/touch handling mode 256 const IDLE = 0 257 const MOUSEPAN = 1 258 const TOUCHPAN = 2 259 const TOUCHZOOM = 3 260 let mode = IDLE 261 262 // State needed to implement zooming. 263 let currentScale = 1.0 264 const initWidth = svg.viewBox.baseVal.width 265 const initHeight = svg.viewBox.baseVal.height 266 267 // State needed to implement panning. 268 let panLastX = 0 // Last event X coordinate 269 let panLastY = 0 // Last event Y coordinate 270 let moved = false // Have we seen significant movement 271 let touchid = null // Current touch identifier 272 273 // State needed for pinch zooming 274 let touchid2 = null // Second id for pinch zooming 275 let initGap = 1.0 // Starting gap between two touches 276 let initScale = 1.0 // currentScale when pinch zoom started 277 let centerPoint = null // Center point for scaling 278 279 // Convert event coordinates to svg coordinates. 280 function toSvg(x, y) { 281 const p = svg.createSVGPoint() 282 p.x = x 283 p.y = y 284 let m = svg.getCTM() 285 if (m == null) m = svg.getScreenCTM() // Firefox workaround. 286 return p.matrixTransform(m.inverse()) 287 } 288 289 // Change the scaling for the svg to s, keeping the point denoted 290 // by u (in svg coordinates]) fixed at the same screen location. 291 function rescale(s, u) { 292 // Limit to a good range. 293 if (s < 0.2) s = 0.2 294 if (s > 10.0) s = 10.0 295 296 currentScale = s 297 298 // svg.viewBox defines the visible portion of the user coordinate 299 // system. So to magnify by s, divide the visible portion by s, 300 // which will then be stretched to fit the viewport. 301 const vb = svg.viewBox 302 const w1 = vb.baseVal.width 303 const w2 = initWidth / s 304 const h1 = vb.baseVal.height 305 const h2 = initHeight / s 306 vb.baseVal.width = w2 307 vb.baseVal.height = h2 308 309 // We also want to adjust vb.baseVal.x so that u.x remains at same 310 // screen X coordinate. In other words, want to change it from x1 to x2 311 // so that: 312 // (u.x - x1) / w1 = (u.x - x2) / w2 313 // Simplifying that, we get 314 // (u.x - x1) * (w2 / w1) = u.x - x2 315 // x2 = u.x - (u.x - x1) * (w2 / w1) 316 vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1) 317 vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1) 318 } 319 320 function handleWheel(e) { 321 if (e.deltaY == 0) return 322 // Change scale factor by 1.1 or 1/1.1 323 rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)), 324 toSvg(e.offsetX, e.offsetY)) 325 } 326 327 function setMode(m) { 328 mode = m 329 touchid = null 330 touchid2 = null 331 } 332 333 function panStart(x, y) { 334 moved = false 335 panLastX = x 336 panLastY = y 337 } 338 339 function panMove(x, y) { 340 let dx = x - panLastX 341 let dy = y - panLastY 342 if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return // Ignore tiny moves 343 344 moved = true 345 panLastX = x 346 panLastY = y 347 348 // Firefox workaround: get dimensions from parentNode. 349 const swidth = svg.clientWidth || svg.parentNode.clientWidth 350 const sheight = svg.clientHeight || svg.parentNode.clientHeight 351 352 // Convert deltas from screen space to svg space. 353 dx *= (svg.viewBox.baseVal.width / swidth) 354 dy *= (svg.viewBox.baseVal.height / sheight) 355 356 svg.viewBox.baseVal.x -= dx 357 svg.viewBox.baseVal.y -= dy 358 } 359 360 function handleScanStart(e) { 361 if (e.button != 0) return // Do not catch right-clicks etc. 362 setMode(MOUSEPAN) 363 panStart(e.clientX, e.clientY) 364 e.preventDefault() 365 svg.addEventListener("mousemove", handleScanMove) 366 } 367 368 function handleScanMove(e) { 369 if (e.buttons == 0) { 370 // Missed an end event, perhaps because mouse moved outside window. 371 setMode(IDLE) 372 svg.removeEventListener("mousemove", handleScanMove) 373 return 374 } 375 if (mode == MOUSEPAN) panMove(e.clientX, e.clientY) 376 } 377 378 function handleScanEnd(e) { 379 if (mode == MOUSEPAN) panMove(e.clientX, e.clientY) 380 setMode(IDLE) 381 svg.removeEventListener("mousemove", handleScanMove) 382 if (!moved) clickHandler(e.target) 383 } 384 385 // Find touch object with specified identifier. 386 function findTouch(tlist, id) { 387 for (const t of tlist) { 388 if (t.identifier == id) return t 389 } 390 return null 391 } 392 393 // Return distance between two touch points 394 function touchGap(t1, t2) { 395 const dx = t1.clientX - t2.clientX 396 const dy = t1.clientY - t2.clientY 397 return Math.hypot(dx, dy) 398 } 399 400 function handleTouchStart(e) { 401 if (mode == IDLE && e.changedTouches.length == 1) { 402 // Start touch based panning 403 const t = e.changedTouches[0] 404 setMode(TOUCHPAN) 405 touchid = t.identifier 406 panStart(t.clientX, t.clientY) 407 e.preventDefault() 408 } else if (mode == TOUCHPAN && e.touches.length == 2) { 409 // Start pinch zooming 410 setMode(TOUCHZOOM) 411 const t1 = e.touches[0] 412 const t2 = e.touches[1] 413 touchid = t1.identifier 414 touchid2 = t2.identifier 415 initScale = currentScale 416 initGap = touchGap(t1, t2) 417 centerPoint = toSvg((t1.clientX + t2.clientX) / 2, 418 (t1.clientY + t2.clientY) / 2) 419 e.preventDefault() 420 } 421 } 422 423 function handleTouchMove(e) { 424 if (mode == TOUCHPAN) { 425 const t = findTouch(e.changedTouches, touchid) 426 if (t == null) return 427 if (e.touches.length != 1) { 428 setMode(IDLE) 429 return 430 } 431 panMove(t.clientX, t.clientY) 432 e.preventDefault() 433 } else if (mode == TOUCHZOOM) { 434 // Get two touches; new gap; rescale to ratio. 435 const t1 = findTouch(e.touches, touchid) 436 const t2 = findTouch(e.touches, touchid2) 437 if (t1 == null || t2 == null) return 438 const gap = touchGap(t1, t2) 439 rescale(initScale * gap / initGap, centerPoint) 440 e.preventDefault() 441 } 442 } 443 444 function handleTouchEnd(e) { 445 if (mode == TOUCHPAN) { 446 const t = findTouch(e.changedTouches, touchid) 447 if (t == null) return 448 panMove(t.clientX, t.clientY) 449 setMode(IDLE) 450 e.preventDefault() 451 if (!moved) clickHandler(t.target) 452 } else if (mode == TOUCHZOOM) { 453 setMode(IDLE) 454 e.preventDefault() 455 } 456 } 457 458 svg.addEventListener("mousedown", handleScanStart) 459 svg.addEventListener("mouseup", handleScanEnd) 460 svg.addEventListener("touchstart", handleTouchStart) 461 svg.addEventListener("touchmove", handleTouchMove) 462 svg.addEventListener("touchend", handleTouchEnd) 463 svg.addEventListener("wheel", handleWheel, true) 464 } 465 466 function initMenus() { 467 'use strict'; 468 469 let activeMenu = null; 470 let activeMenuHdr = null; 471 472 function cancelActiveMenu() { 473 if (activeMenu == null) return; 474 activeMenu.style.display = "none"; 475 activeMenu = null; 476 activeMenuHdr = null; 477 } 478 479 // Set click handlers on every menu header. 480 for (const menu of document.getElementsByClassName("menu")) { 481 const hdr = menu.parentElement; 482 if (hdr == null) return; 483 function showMenu(e) { 484 // menu is a child of hdr, so this event can fire for clicks 485 // inside menu. Ignore such clicks. 486 if (e.target != hdr) return; 487 activeMenu = menu; 488 activeMenuHdr = hdr; 489 menu.style.display = "block"; 490 } 491 hdr.addEventListener("mousedown", showMenu); 492 hdr.addEventListener("touchstart", showMenu); 493 } 494 495 // If there is an active menu and a down event outside, retract the menu. 496 for (const t of ["mousedown", "touchstart"]) { 497 document.addEventListener(t, (e) => { 498 // Note: to avoid unnecessary flicker, if the down event is inside 499 // the active menu header, do not retract the menu. 500 if (activeMenuHdr != e.target.closest(".menu-header")) { 501 cancelActiveMenu(); 502 } 503 }, { passive: true, capture: true }); 504 } 505 506 // If there is an active menu and an up event inside, retract the menu. 507 document.addEventListener("mouseup", (e) => { 508 if (activeMenu == e.target.closest(".menu")) { 509 cancelActiveMenu(); 510 } 511 }, { passive: true, capture: true }); 512 } 513 514 function viewer(baseUrl, nodes) { 515 'use strict'; 516 517 // Elements 518 const search = document.getElementById("searchbox") 519 const graph0 = document.getElementById("graph0") 520 const svg = (graph0 == null ? null : graph0.parentElement) 521 const toptable = document.getElementById("toptable") 522 523 let regexpActive = false 524 let selected = new Map() 525 let origFill = new Map() 526 let searchAlarm = null 527 let buttonsEnabled = true 528 529 function handleDetails() { 530 const detailsText = document.getElementById("detailtext") 531 if (detailsText != null) detailsText.style.display = "block" 532 } 533 534 function handleCloseDetails() { 535 const detailsText = document.getElementById("detailtext") 536 if (detailsText != null) detailsText.style.display = "none" 537 } 538 539 function handleKey(e) { 540 if (e.keyCode != 13) return 541 window.location.href = 542 updateUrl(new URL({{.BaseURL}}, window.location.href), "f") 543 e.preventDefault() 544 } 545 546 function handleSearch() { 547 // Delay expensive processing so a flurry of key strokes is handled once. 548 if (searchAlarm != null) { 549 clearTimeout(searchAlarm) 550 } 551 searchAlarm = setTimeout(selectMatching, 300) 552 553 regexpActive = true 554 updateButtons() 555 } 556 557 function selectMatching() { 558 searchAlarm = null 559 let re = null 560 if (search.value != "") { 561 try { 562 re = new RegExp(search.value) 563 } catch (e) { 564 // TODO: Display error state in search box 565 return 566 } 567 } 568 569 function match(text) { 570 return re != null && re.test(text) 571 } 572 573 // drop currently selected items that do not match re. 574 selected.forEach(function(v, n) { 575 if (!match(nodes[n])) { 576 unselect(n, document.getElementById("node" + n)) 577 } 578 }) 579 580 // add matching items that are not currently selected. 581 for (let n = 0; n < nodes.length; n++) { 582 if (!selected.has(n) && match(nodes[n])) { 583 select(n, document.getElementById("node" + n)) 584 } 585 } 586 587 updateButtons() 588 } 589 590 function toggleSvgSelect(elem) { 591 // Walk up to immediate child of graph0 592 while (elem != null && elem.parentElement != graph0) { 593 elem = elem.parentElement 594 } 595 if (!elem) return 596 597 // Disable regexp mode. 598 regexpActive = false 599 600 const n = nodeId(elem) 601 if (n < 0) return 602 if (selected.has(n)) { 603 unselect(n, elem) 604 } else { 605 select(n, elem) 606 } 607 updateButtons() 608 } 609 610 function unselect(n, elem) { 611 if (elem == null) return 612 selected.delete(n) 613 setBackground(elem, false) 614 } 615 616 function select(n, elem) { 617 if (elem == null) return 618 selected.set(n, true) 619 setBackground(elem, true) 620 } 621 622 function nodeId(elem) { 623 const id = elem.id 624 if (!id) return -1 625 if (!id.startsWith("node")) return -1 626 const n = parseInt(id.slice(4), 10) 627 if (isNaN(n)) return -1 628 if (n < 0 || n >= nodes.length) return -1 629 return n 630 } 631 632 function setBackground(elem, set) { 633 // Handle table row highlighting. 634 if (elem.nodeName == "TR") { 635 elem.classList.toggle("hilite", set) 636 return 637 } 638 639 // Handle svg element highlighting. 640 const p = findPolygon(elem) 641 if (p != null) { 642 if (set) { 643 origFill.set(p, p.style.fill) 644 p.style.fill = "#ccccff" 645 } else if (origFill.has(p)) { 646 p.style.fill = origFill.get(p) 647 } 648 } 649 } 650 651 function findPolygon(elem) { 652 if (elem.localName == "polygon") return elem 653 for (const c of elem.children) { 654 const p = findPolygon(c) 655 if (p != null) return p 656 } 657 return null 658 } 659 660 // convert a string to a regexp that matches that string. 661 function quotemeta(str) { 662 return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1') 663 } 664 665 // Update id's href to reflect current selection whenever it is 666 // liable to be followed. 667 function makeLinkDynamic(id) { 668 const elem = document.getElementById(id) 669 if (elem == null) return 670 671 // Most links copy current selection into the "f" parameter, 672 // but Refine menu links are different. 673 let param = "f" 674 if (id == "ignore") param = "i" 675 if (id == "hide") param = "h" 676 if (id == "show") param = "s" 677 678 // We update on mouseenter so middle-click/right-click work properly. 679 elem.addEventListener("mouseenter", updater) 680 elem.addEventListener("touchstart", updater) 681 682 function updater() { 683 elem.href = updateUrl(new URL(elem.href), param) 684 } 685 } 686 687 // Update URL to reflect current selection. 688 function updateUrl(url, param) { 689 url.hash = "" 690 691 // The selection can be in one of two modes: regexp-based or 692 // list-based. Construct regular expression depending on mode. 693 let re = regexpActive 694 ? search.value 695 : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join("|") 696 697 // Copy params from this page's URL. 698 const params = url.searchParams 699 for (const p of new URLSearchParams(window.location.search)) { 700 params.set(p[0], p[1]) 701 } 702 703 if (re != "") { 704 // For focus/show, forget old parameter. For others, add to re. 705 if (param != "f" && param != "s" && params.has(param)) { 706 const old = params.get(param) 707 if (old != "") { 708 re += "|" + old 709 } 710 } 711 params.set(param, re) 712 } else { 713 params.delete(param) 714 } 715 716 return url.toString() 717 } 718 719 function handleTopClick(e) { 720 // Walk back until we find TR and then get the Name column (index 5) 721 let elem = e.target 722 while (elem != null && elem.nodeName != "TR") { 723 elem = elem.parentElement 724 } 725 if (elem == null || elem.children.length < 6) return 726 727 e.preventDefault() 728 const tr = elem 729 const td = elem.children[5] 730 if (td.nodeName != "TD") return 731 const name = td.innerText 732 const index = nodes.indexOf(name) 733 if (index < 0) return 734 735 // Disable regexp mode. 736 regexpActive = false 737 738 if (selected.has(index)) { 739 unselect(index, elem) 740 } else { 741 select(index, elem) 742 } 743 updateButtons() 744 } 745 746 function updateButtons() { 747 const enable = (search.value != "" || selected.size != 0) 748 if (buttonsEnabled == enable) return 749 buttonsEnabled = enable 750 for (const id of ["focus", "ignore", "hide", "show"]) { 751 const link = document.getElementById(id) 752 if (link != null) { 753 link.classList.toggle("disabled", !enable) 754 } 755 } 756 } 757 758 // Initialize button states 759 updateButtons() 760 761 // Setup event handlers 762 initMenus() 763 if (svg != null) { 764 initPanAndZoom(svg, toggleSvgSelect) 765 } 766 if (toptable != null) { 767 toptable.addEventListener("mousedown", handleTopClick) 768 toptable.addEventListener("touchstart", handleTopClick) 769 } 770 771 const ids = ["topbtn", "graphbtn", "peek", "list", "disasm", 772 "focus", "ignore", "hide", "show"] 773 ids.forEach(makeLinkDynamic) 774 775 // Bind action to button with specified id. 776 function addAction(id, action) { 777 const btn = document.getElementById(id) 778 if (btn != null) { 779 btn.addEventListener("click", action) 780 btn.addEventListener("touchstart", action) 781 } 782 } 783 784 addAction("details", handleDetails) 785 addAction("closedetails", handleCloseDetails) 786 787 search.addEventListener("input", handleSearch) 788 search.addEventListener("keydown", handleKey) 789 790 // Give initial focus to main container so it can be scrolled using keys. 791 const main = document.getElementById("bodycontainer") 792 if (main) { 793 main.focus() 794 } 795 } 796 </script> 797 {{end}} 798 799 {{define "top" -}} 800 <!DOCTYPE html> 801 <html> 802 <head> 803 <meta charset="utf-8"> 804 <title>{{.Title}}</title> 805 {{template "css" .}} 806 <style type="text/css"> 807 </style> 808 </head> 809 <body> 810 811 {{template "header" .}} 812 813 <div id="bodycontainer"> 814 <table id="toptable"> 815 <tr> 816 <th id="flathdr1">Flat 817 <th id="flathdr2">Flat% 818 <th>Sum% 819 <th id="cumhdr1">Cum 820 <th id="cumhdr2">Cum% 821 <th id="namehdr">Name 822 <th>Inlined?</tr> 823 <tbody id="rows"> 824 </tbody> 825 </table> 826 </div> 827 828 {{template "script" .}} 829 <script> 830 function makeTopTable(total, entries) { 831 const rows = document.getElementById("rows") 832 if (rows == null) return 833 834 // Store initial index in each entry so we have stable node ids for selection. 835 for (let i = 0; i < entries.length; i++) { 836 entries[i].Id = "node" + i 837 } 838 839 // Which column are we currently sorted by and in what order? 840 let currentColumn = "" 841 let descending = false 842 sortBy("Flat") 843 844 function sortBy(column) { 845 // Update sort criteria 846 if (column == currentColumn) { 847 descending = !descending // Reverse order 848 } else { 849 currentColumn = column 850 descending = (column != "Name") 851 } 852 853 // Sort according to current criteria. 854 function cmp(a, b) { 855 const av = a[currentColumn] 856 const bv = b[currentColumn] 857 if (av < bv) return -1 858 if (av > bv) return +1 859 return 0 860 } 861 entries.sort(cmp) 862 if (descending) entries.reverse() 863 864 function addCell(tr, val) { 865 const td = document.createElement('td') 866 td.textContent = val 867 tr.appendChild(td) 868 } 869 870 function percent(v) { 871 return (v * 100.0 / total).toFixed(2) + "%" 872 } 873 874 // Generate rows 875 const fragment = document.createDocumentFragment() 876 let sum = 0 877 for (const row of entries) { 878 const tr = document.createElement('tr') 879 tr.id = row.Id 880 sum += row.Flat 881 addCell(tr, row.FlatFormat) 882 addCell(tr, percent(row.Flat)) 883 addCell(tr, percent(sum)) 884 addCell(tr, row.CumFormat) 885 addCell(tr, percent(row.Cum)) 886 addCell(tr, row.Name) 887 addCell(tr, row.InlineLabel) 888 fragment.appendChild(tr) 889 } 890 891 rows.textContent = '' // Remove old rows 892 rows.appendChild(fragment) 893 } 894 895 // Make different column headers trigger sorting. 896 function bindSort(id, column) { 897 const hdr = document.getElementById(id) 898 if (hdr == null) return 899 const fn = function() { sortBy(column) } 900 hdr.addEventListener("click", fn) 901 hdr.addEventListener("touch", fn) 902 } 903 bindSort("flathdr1", "Flat") 904 bindSort("flathdr2", "Flat") 905 bindSort("cumhdr1", "Cum") 906 bindSort("cumhdr2", "Cum") 907 bindSort("namehdr", "Name") 908 } 909 910 viewer({{.BaseURL}}, {{.Nodes}}) 911 makeTopTable({{.Total}}, {{.Top}}) 912 </script> 913 </body> 914 </html> 915 {{end}} 916 917 {{define "sourcelisting" -}} 918 <!DOCTYPE html> 919 <html> 920 <head> 921 <meta charset="utf-8"> 922 <title>{{.Title}}</title> 923 {{template "css" .}} 924 {{template "weblistcss" .}} 925 {{template "weblistjs" .}} 926 </head> 927 <body> 928 929 {{template "header" .}} 930 931 <div id="bodycontainer"> 932 {{.HTMLBody}} 933 </div> 934 935 {{template "script" .}} 936 <script>viewer({{.BaseURL}}, null)</script> 937 </body> 938 </html> 939 {{end}} 940 941 {{define "plaintext" -}} 942 <!DOCTYPE html> 943 <html> 944 <head> 945 <meta charset="utf-8"> 946 <title>{{.Title}}</title> 947 {{template "css" .}} 948 </head> 949 <body> 950 951 {{template "header" .}} 952 953 <div id="bodycontainer"> 954 <pre> 955 {{.TextBody}} 956 </pre> 957 </div> 958 959 {{template "script" .}} 960 <script>viewer({{.BaseURL}}, null)</script> 961 </body> 962 </html> 963 {{end}} 964 `)) 965 } 966