1 // Copyright 2015 The Go Authors. All rights reserved.
2 // Copyright 2019 Dominik Honnef. All rights reserved.
21 func live(f *Function) []bool {
25 for _, b := range f.Blocks {
26 for _, instr := range b.Instrs {
27 if int(instr.ID()) > max {
33 out := make([]bool, max+1)
35 for _, b := range f.Blocks {
36 for _, instr := range b.Instrs {
38 case *BlankStore, *Call, *ConstantSwitch, *Defer, *Go, *If, *Jump, *MapUpdate, *Next, *Panic, *Recv, *Return, *RunDefers, *Send, *Store, *Unreachable:
39 out[instr.ID()] = true
48 for _, op := range v.Operands(ops) {
53 out[(*op).ID()] = true
62 type funcPrinter interface {
63 startBlock(b *BasicBlock, reachable bool)
64 endBlock(b *BasicBlock)
65 value(v Node, live bool)
68 named(n string, vals []Value)
71 func namedValues(f *Function) map[types.Object][]Value {
72 names := map[types.Object][]Value{}
73 for _, b := range f.Blocks {
74 for _, instr := range b.Instrs {
75 if instr, ok := instr.(*DebugRef); ok {
76 if obj := instr.object; obj != nil {
77 names[obj] = append(names[obj], instr.X)
82 // XXX deduplicate values
86 func fprintFunc(p funcPrinter, f *Function) {
87 // XXX does our IR form preserve unreachable blocks?
88 // reachable, live := findlive(f)
91 for _, b := range f.Blocks {
93 // p.startBlock(b, reachable[b.Index])
96 end := len(b.Instrs) - 1
100 for _, v := range b.Instrs[:end] {
101 if _, ok := v.(*DebugRef); !ok {
102 p.value(v, l[v.ID()])
108 names := namedValues(f)
109 keys := make([]types.Object, 0, len(names))
110 for key := range names {
111 keys = append(keys, key)
113 sort.Slice(keys, func(i, j int) bool {
114 return keys[i].Pos() < keys[j].Pos()
116 for _, key := range keys {
117 p.named(key.Name(), names[key])
121 func opName(v Node) string {
122 switch v := v.(type) {
124 if v.Common().IsInvoke() {
135 return "SelectBlocking"
137 return "SelectNonBlocking"
139 return reflect.ValueOf(v).Type().Elem().Name()
143 type HTMLWriter struct {
149 func NewHTMLWriter(path string, funcname, cfgMask string) *HTMLWriter {
150 out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
152 log.Fatalf("%v", err)
154 pwd, err := os.Getwd()
156 log.Fatalf("%v", err)
158 html := HTMLWriter{w: out, path: filepath.Join(pwd, path)}
159 html.dot = newDotWriter()
164 func (w *HTMLWriter) start(name string) {
168 w.WriteString("<html>")
169 w.WriteString(`<head>
170 <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
175 font-family: Arial, sans-serif;
180 display: inline-block;
181 margin: 0 1em .5em 0;
185 display: inline-block;
197 border: 1px solid black;
203 border: 1px solid black;
225 -moz-transform: rotate(-90.0deg); /* FF3.5+ */
226 -o-transform: rotate(-90.0deg); /* Opera 10.5 */
227 -webkit-transform: rotate(-90.0deg); /* Saf3.1+, Chrome */
228 filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0.083); /* IE6,IE7 */
229 -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0.083)"; /* IE8 */
236 code, pre, .lines, .ast {
237 font-family: Menlo, monospace;
272 word-wrap: break-word;
276 list-style-type: none;
280 text-indent: -2em; /* indent wrapped lines */
301 li.ssa-start-block button {
311 background-color: #eee;
357 background-color: #eee;
360 .zoom a:link, .zoom a:visited {
361 text-decoration: none;
369 outline: 1px solid #eee;
372 .highlight-aquamarine { background-color: aquamarine; }
373 .highlight-coral { background-color: coral; }
374 .highlight-lightpink { background-color: lightpink; }
375 .highlight-lightsteelblue { background-color: lightsteelblue; }
376 .highlight-palegreen { background-color: palegreen; }
377 .highlight-skyblue { background-color: skyblue; }
378 .highlight-lightgray { background-color: lightgray; }
379 .highlight-yellow { background-color: yellow; }
380 .highlight-lime { background-color: lime; }
381 .highlight-khaki { background-color: khaki; }
382 .highlight-aqua { background-color: aqua; }
383 .highlight-salmon { background-color: salmon; }
385 .outline-blue { outline: blue solid 2px; }
386 .outline-red { outline: red solid 2px; }
387 .outline-blueviolet { outline: blueviolet solid 2px; }
388 .outline-darkolivegreen { outline: darkolivegreen solid 2px; }
389 .outline-fuchsia { outline: fuchsia solid 2px; }
390 .outline-sienna { outline: sienna solid 2px; }
391 .outline-gold { outline: gold solid 2px; }
392 .outline-orangered { outline: orangered solid 2px; }
393 .outline-teal { outline: teal solid 2px; }
394 .outline-maroon { outline: maroon solid 2px; }
395 .outline-black { outline: black solid 2px; }
397 ellipse.outline-blue { stroke-width: 2px; stroke: blue; }
398 ellipse.outline-red { stroke-width: 2px; stroke: red; }
399 ellipse.outline-blueviolet { stroke-width: 2px; stroke: blueviolet; }
400 ellipse.outline-darkolivegreen { stroke-width: 2px; stroke: darkolivegreen; }
401 ellipse.outline-fuchsia { stroke-width: 2px; stroke: fuchsia; }
402 ellipse.outline-sienna { stroke-width: 2px; stroke: sienna; }
403 ellipse.outline-gold { stroke-width: 2px; stroke: gold; }
404 ellipse.outline-orangered { stroke-width: 2px; stroke: orangered; }
405 ellipse.outline-teal { stroke-width: 2px; stroke: teal; }
406 ellipse.outline-maroon { stroke-width: 2px; stroke: maroon; }
407 ellipse.outline-black { stroke-width: 2px; stroke: black; }
411 <script type="text/javascript">
412 // ordered list of all available highlight colors
414 "highlight-aquamarine",
416 "highlight-lightpink",
417 "highlight-lightsteelblue",
418 "highlight-palegreen",
420 "highlight-lightgray",
428 // state: which value is highlighted this color?
429 var highlighted = {};
430 for (var i = 0; i < highlights.length; i++) {
431 highlighted[highlights[i]] = "";
434 // ordered list of all available outline colors
438 "outline-blueviolet",
439 "outline-darkolivegreen",
449 // state: which value is outlined this color?
451 for (var i = 0; i < outlines.length; i++) {
452 outlined[outlines[i]] = "";
455 window.onload = function() {
456 var ssaElemClicked = function(elem, event, selections, selected) {
457 event.stopPropagation();
459 // TODO: pushState with updated state and read it on page load,
460 // so that state can survive across reloads
462 // find all values with the same name
463 var c = elem.classList.item(0);
464 var x = document.getElementsByClassName(c);
466 // if selected, remove selections from all of them
467 // otherwise, attempt to add
470 for (var i = 0; i < selections.length; i++) {
471 var color = selections[i];
472 if (selected[color] == c) {
479 for (var i = 0; i < x.length; i++) {
480 x[i].classList.remove(remove);
482 selected[remove] = "";
486 // we're adding a selection
487 // find first available color
489 for (var i = 0; i < selections.length; i++) {
490 var color = selections[i];
491 if (selected[color] == "") {
497 alert("out of selection colors; go add more");
501 // set that as the selection
502 for (var i = 0; i < x.length; i++) {
503 x[i].classList.add(avail);
508 var ssaValueClicked = function(event) {
509 ssaElemClicked(this, event, highlights, highlighted);
512 var ssaBlockClicked = function(event) {
513 ssaElemClicked(this, event, outlines, outlined);
516 var ssavalues = document.getElementsByClassName("ssa-value");
517 for (var i = 0; i < ssavalues.length; i++) {
518 ssavalues[i].addEventListener('click', ssaValueClicked);
521 var ssalongvalues = document.getElementsByClassName("ssa-long-value");
522 for (var i = 0; i < ssalongvalues.length; i++) {
523 // don't attach listeners to li nodes, just the spans they contain
524 if (ssalongvalues[i].nodeName == "SPAN") {
525 ssalongvalues[i].addEventListener('click', ssaValueClicked);
529 var ssablocks = document.getElementsByClassName("ssa-block");
530 for (var i = 0; i < ssablocks.length; i++) {
531 ssablocks[i].addEventListener('click', ssaBlockClicked);
534 var lines = document.getElementsByClassName("line-number");
535 for (var i = 0; i < lines.length; i++) {
536 lines[i].addEventListener('click', ssaValueClicked);
539 // Contains phase names which are expanded by default. Other columns are collapsed.
540 var expandedDefault = [
550 function toggler(phase) {
552 toggle_cell(phase+'-col');
553 toggle_cell(phase+'-exp');
557 function toggle_cell(id) {
558 var e = document.getElementById(id);
559 if (e.style.display == 'table-cell') {
560 e.style.display = 'none';
562 e.style.display = 'table-cell';
566 // Go through all columns and collapse needed phases.
567 var td = document.getElementsByTagName("td");
568 for (var i = 0; i < td.length; i++) {
570 var phase = id.substr(0, id.length-4);
571 var show = expandedDefault.indexOf(phase) !== -1
572 if (id.endsWith("-exp")) {
573 var h2 = td[i].getElementsByTagName("h2");
575 h2[0].addEventListener('click', toggler(phase));
578 td[i].addEventListener('click', toggler(phase));
580 if (id.endsWith("-col") && show || id.endsWith("-exp") && !show) {
581 td[i].style.display = 'none';
584 td[i].style.display = 'table-cell';
587 // find all svg block nodes, add their block classes
588 var nodes = document.querySelectorAll('*[id^="graph_node_"]');
589 for (var i = 0; i < nodes.length; i++) {
591 var name = node.id.toString();
592 var block = name.substring(name.lastIndexOf("_")+1);
593 node.classList.remove("node");
594 node.classList.add(block);
595 node.addEventListener('click', ssaBlockClicked);
596 var ellipse = node.getElementsByTagName('ellipse')[0];
597 ellipse.classList.add(block);
598 ellipse.addEventListener('click', ssaBlockClicked);
601 // make big graphs smaller
602 var targetScale = 0.5;
603 var nodes = document.querySelectorAll('*[id^="svg_graph_"]');
604 // TODO: Implement smarter auto-zoom using the viewBox attribute
605 // and in case of big graphs set the width and height of the svg graph to
607 for (var i = 0; i < nodes.length; i++) {
609 var name = node.id.toString();
610 var phase = name.substring(name.lastIndexOf("_")+1);
611 var gNode = document.getElementById("g_graph_"+phase);
612 var scale = gNode.transform.baseVal.getItem(0).matrix.a;
613 if (scale > targetScale) {
614 node.width.baseVal.value *= targetScale / scale;
615 node.height.baseVal.value *= targetScale / scale;
620 function toggle_visibility(id) {
621 var e = document.getElementById(id);
622 if (e.style.display == 'block') {
623 e.style.display = 'none';
625 e.style.display = 'block';
629 function hideBlock(el) {
630 var es = el.parentNode.parentNode.getElementsByClassName("ssa-value-list");
634 if (e.style.display === 'block' || e.style.display === '') {
635 e.style.display = 'none';
638 e.style.display = 'block';
643 // TODO: scale the graph with the viewBox attribute.
644 function graphReduce(id) {
645 var node = document.getElementById(id);
647 node.width.baseVal.value *= 0.9;
648 node.height.baseVal.value *= 0.9;
653 function graphEnlarge(id) {
654 var node = document.getElementById(id);
656 node.width.baseVal.value *= 1.1;
657 node.height.baseVal.value *= 1.1;
662 function makeDraggable(event) {
663 var svg = event.target;
664 if (window.PointerEvent) {
665 svg.addEventListener('pointerdown', startDrag);
666 svg.addEventListener('pointermove', drag);
667 svg.addEventListener('pointerup', endDrag);
668 svg.addEventListener('pointerleave', endDrag);
670 svg.addEventListener('mousedown', startDrag);
671 svg.addEventListener('mousemove', drag);
672 svg.addEventListener('mouseup', endDrag);
673 svg.addEventListener('mouseleave', endDrag);
676 var point = svg.createSVGPoint();
677 var isPointerDown = false;
679 var viewBox = svg.viewBox.baseVal;
681 function getPointFromEvent (event) {
682 point.x = event.clientX;
683 point.y = event.clientY;
685 // We get the current transformation matrix of the SVG and we inverse it
686 var invertedSVGMatrix = svg.getScreenCTM().inverse();
687 return point.matrixTransform(invertedSVGMatrix);
690 function startDrag(event) {
691 isPointerDown = true;
692 pointerOrigin = getPointFromEvent(event);
695 function drag(event) {
696 if (!isPointerDown) {
699 event.preventDefault();
701 var pointerPosition = getPointFromEvent(event);
702 viewBox.x -= (pointerPosition.x - pointerOrigin.x);
703 viewBox.y -= (pointerPosition.y - pointerOrigin.y);
706 function endDrag(event) {
707 isPointerDown = false;
712 w.WriteString("<body>")
713 w.WriteString("<h1>")
714 w.WriteString(html.EscapeString(name))
715 w.WriteString("</h1>")
717 <a href="#" onclick="toggle_visibility('help');return false;" id="helplink">help</a>
721 Click on a value or block to toggle highlighting of that value/block
722 and its uses. (Values and blocks are highlighted by ID, and IDs of
723 dead items may be reused, so not all highlights necessarily correspond
724 to the clicked item.)
728 Faded out values and blocks are dead code that has not been eliminated.
732 Values printed in italics have a dependency cycle.
736 <b>CFG</b>: Dashed edge is for unlikely branches. Blue color is for backward edges.
737 Edge with a dot means that this edge follows the order in which blocks were laidout.
742 w.WriteString("<table>")
743 w.WriteString("<tr>")
746 func (w *HTMLWriter) Close() {
750 io.WriteString(w.w, "</tr>")
751 io.WriteString(w.w, "</table>")
752 io.WriteString(w.w, "</body>")
753 io.WriteString(w.w, "</html>")
755 fmt.Printf("dumped IR to %v\n", w.path)
758 // WriteFunc writes f in a column headed by title.
759 // phase is used for collapsing columns and should be unique across the table.
760 func (w *HTMLWriter) WriteFunc(phase, title string, f *Function) {
764 w.WriteColumn(phase, title, "", funcHTML(f, phase, w.dot))
767 // WriteColumn writes raw HTML in a column headed by title.
768 // It is intended for pre- and post-compilation log output.
769 func (w *HTMLWriter) WriteColumn(phase, title, class, html string) {
773 id := strings.Replace(phase, " ", "-", -1)
775 w.Printf("<td id=\"%v-col\" class=\"collapsed\"><div>%v</div></td>", id, phase)
778 w.Printf("<td id=\"%v-exp\">", id)
780 w.Printf("<td id=\"%v-exp\" class=\"%v\">", id, class)
782 w.WriteString("<h2>" + title + "</h2>")
784 w.WriteString("</td>")
787 func (w *HTMLWriter) Printf(msg string, v ...interface{}) {
788 if _, err := fmt.Fprintf(w.w, msg, v...); err != nil {
789 log.Fatalf("%v", err)
793 func (w *HTMLWriter) WriteString(s string) {
794 if _, err := io.WriteString(w.w, s); err != nil {
795 log.Fatalf("%v", err)
799 func valueHTML(v Node) string {
803 // TODO: Using the value ID as the class ignores the fact
804 // that value IDs get recycled and that some values
805 // are transmuted into other values.
806 class := fmt.Sprintf("t%d", v.ID())
808 switch v := v.(type) {
810 label = v.RelString(nil)
816 return fmt.Sprintf("<span class=\"%s ssa-value\">%s</span>", class, label)
819 func valueLongHTML(v Node) string {
820 // TODO: Any intra-value formatting?
821 // I'm wary of adding too much visual noise,
822 // but a little bit might be valuable.
823 // We already have visual noise in the form of punctuation
824 // maybe we could replace some of that with formatting.
825 s := fmt.Sprintf("<span class=\"t%d ssa-long-value\">", v.ID())
827 linenumber := "<span class=\"no-line-number\">(?)</span>"
828 if v.Pos().IsValid() {
829 line := v.Parent().Prog.Fset.Position(v.Pos()).Line
830 linenumber = fmt.Sprintf("<span class=\"l%v line-number\">(%d)</span>", line, line)
833 s += fmt.Sprintf("%s %s = %s", valueHTML(v), linenumber, opName(v))
835 if v, ok := v.(Value); ok {
836 s += " <" + html.EscapeString(v.Type().String()) + ">"
839 switch v := v.(type) {
841 s += fmt.Sprintf(" {%s}", html.EscapeString(v.name))
843 s += fmt.Sprintf(" {%s}", html.EscapeString(v.Op.String()))
845 s += fmt.Sprintf(" {%s}", html.EscapeString(v.Op.String()))
847 name := v.Tuple.Type().(*types.Tuple).At(v.Index).Name()
848 s += fmt.Sprintf(" [%d] (%s)", v.Index, name)
850 st := v.X.Type().Underlying().(*types.Struct)
851 // Be robust against a bad index.
853 if 0 <= v.Field && v.Field < st.NumFields() {
854 name = st.Field(v.Field).Name()
856 s += fmt.Sprintf(" [%d] (%s)", v.Field, name)
858 st := deref(v.X.Type()).Underlying().(*types.Struct)
859 // Be robust against a bad index.
861 if 0 <= v.Field && v.Field < st.NumFields() {
862 name = st.Field(v.Field).Name()
865 s += fmt.Sprintf(" [%d] (%s)", v.Field, name)
867 s += fmt.Sprintf(" {%t}", v.CommaOk)
869 if v.Common().IsInvoke() {
870 s += fmt.Sprintf(" {%s}", html.EscapeString(v.Common().Method.FullName()))
874 s += " {<nil>}"
876 s += fmt.Sprintf(" {%s}", html.EscapeString(v.Value.String()))
879 s += fmt.Sprintf(" [#%s]", v.From)
881 for _, a := range v.Operands(nil) {
882 s += fmt.Sprintf(" %s", valueHTML(*a))
885 // OPT(dh): we're calling namedValues many times on the same function.
886 allNames := namedValues(v.Parent())
888 for name, values := range allNames {
889 for _, value := range values {
891 names = append(names, name.Name())
897 s += " (" + strings.Join(names, ", ") + ")"
904 func blockHTML(b *BasicBlock) string {
905 // TODO: Using the value ID as the class ignores the fact
906 // that value IDs get recycled and that some values
907 // are transmuted into other values.
908 s := html.EscapeString(b.String())
909 return fmt.Sprintf("<span class=\"%s ssa-block\">%s</span>", s, s)
912 func blockLongHTML(b *BasicBlock) string {
915 if len(b.Instrs) > 0 {
919 // TODO: improve this for HTML?
920 s := fmt.Sprintf("<span class=\"b%d ssa-block\">%s</span>", b.Index, kind)
923 ops := term.Operands(nil)
926 for _, op := range ops {
927 ss = append(ss, valueHTML(*op))
929 s += " " + strings.Join(ss, ", ")
932 if len(b.Succs) > 0 {
933 s += " →" // right arrow
934 for _, c := range b.Succs {
935 s += " " + blockHTML(c)
941 func funcHTML(f *Function, phase string, dot *dotWriter) string {
942 buf := new(bytes.Buffer)
944 dot.writeFuncSVG(buf, phase, f)
946 fmt.Fprint(buf, "<code>")
947 p := htmlFuncPrinter{w: buf}
950 // fprintFunc(&buf, f) // TODO: HTML, not text, <br /> for line breaks, etc.
951 fmt.Fprint(buf, "</code>")
955 type htmlFuncPrinter struct {
959 func (p htmlFuncPrinter) startBlock(b *BasicBlock, reachable bool) {
964 fmt.Fprintf(p.w, "<ul class=\"%s ssa-print-func %s\">", b, dead)
965 fmt.Fprintf(p.w, "<li class=\"ssa-start-block\">%s:", blockHTML(b))
966 if len(b.Preds) > 0 {
967 io.WriteString(p.w, " ←") // left arrow
968 for _, pred := range b.Preds {
969 fmt.Fprintf(p.w, " %s", blockHTML(pred))
972 if len(b.Instrs) > 0 {
973 io.WriteString(p.w, `<button onclick="hideBlock(this)">-</button>`)
975 io.WriteString(p.w, "</li>")
976 if len(b.Instrs) > 0 { // start list of values
977 io.WriteString(p.w, "<li class=\"ssa-value-list\">")
978 io.WriteString(p.w, "<ul>")
982 func (p htmlFuncPrinter) endBlock(b *BasicBlock) {
983 if len(b.Instrs) > 0 { // end list of values
984 io.WriteString(p.w, "</ul>")
985 io.WriteString(p.w, "</li>")
987 io.WriteString(p.w, "<li class=\"ssa-end-block\">")
988 fmt.Fprint(p.w, blockLongHTML(b))
989 io.WriteString(p.w, "</li>")
990 io.WriteString(p.w, "</ul>")
993 func (p htmlFuncPrinter) value(v Node, live bool) {
998 fmt.Fprintf(p.w, "<li class=\"ssa-long-value %s\">", dead)
999 fmt.Fprint(p.w, valueLongHTML(v))
1000 io.WriteString(p.w, "</li>")
1003 func (p htmlFuncPrinter) startDepCycle() {
1004 fmt.Fprintln(p.w, "<span class=\"depcycle\">")
1007 func (p htmlFuncPrinter) endDepCycle() {
1008 fmt.Fprintln(p.w, "</span>")
1011 func (p htmlFuncPrinter) named(n string, vals []Value) {
1012 fmt.Fprintf(p.w, "<li>name %s: ", n)
1013 for _, val := range vals {
1014 fmt.Fprintf(p.w, "%s ", valueHTML(val))
1016 fmt.Fprintf(p.w, "</li>")
1019 type dotWriter struct {
1024 // newDotWriter returns non-nil value when mask is valid.
1025 // dotWriter will generate SVGs only for the phases specified in the mask.
1026 // mask can contain following patterns and combinations of them:
1028 // x-y - x through y, inclusive;
1029 // x,y - x and y, but not the passes between.
1030 func newDotWriter() *dotWriter {
1031 path, err := exec.LookPath("dot")
1036 return &dotWriter{path: path}
1039 func (d *dotWriter) writeFuncSVG(w io.Writer, phase string, f *Function) {
1043 cmd := exec.Command(d.path, "-Tsvg")
1044 pipe, err := cmd.StdinPipe()
1050 buf := new(bytes.Buffer)
1052 bufErr := new(bytes.Buffer)
1060 fmt.Fprint(pipe, `digraph "" { margin=0; size="4,40"; ranksep=.2; `)
1061 id := strings.Replace(phase, " ", "-", -1)
1062 fmt.Fprintf(pipe, `id="g_graph_%s";`, id)
1063 fmt.Fprintf(pipe, `node [style=filled,fillcolor=white,fontsize=16,fontname="Menlo,Times,serif",margin="0.01,0.03"];`)
1064 fmt.Fprintf(pipe, `edge [fontsize=16,fontname="Menlo,Times,serif"];`)
1065 for _, b := range f.Blocks {
1067 fmt.Fprintf(pipe, `%v [label="%v%s\n%v",id="graph_node_%v_%v"];`, b, b, layout, b.Control().String(), id, b)
1069 indexOf := make([]int, len(f.Blocks))
1070 for i, b := range f.Blocks {
1071 indexOf[b.Index] = i
1076 ponums := make([]int32, len(f.Blocks))
1077 _ = postorderWithNumbering(f, ponums)
1078 isBackEdge := func(from, to int) bool {
1079 return ponums[from] <= ponums[to]
1082 isBackEdge := func(from, to int) bool { return false }
1084 for _, b := range f.Blocks {
1085 for i, s := range b.Succs {
1089 if isBackEdge(b.Index, s.Index) {
1092 fmt.Fprintf(pipe, `%v -> %v [label=" %d ",style="%s",color="%s",arrowhead="%s"];`, b, s, i, style, color, arrow)
1095 fmt.Fprint(pipe, "}")
1100 fmt.Printf("dot: %v\n%v\n", err, bufErr.String())
1104 svgID := "svg_graph_" + id
1105 fmt.Fprintf(w, `<div class="zoom"><button onclick="return graphReduce('%s');">-</button> <button onclick="return graphEnlarge('%s');">+</button></div>`, svgID, svgID)
1106 // For now, an awful hack: edit the html as it passes through
1107 // our fingers, finding '<svg ' and injecting needed attributes after it.
1108 err = d.copyUntil(w, buf, `<svg `)
1110 fmt.Printf("injecting attributes: %v\n", err)
1113 fmt.Fprintf(w, ` id="%s" onload="makeDraggable(evt)" width="100%%" `, svgID)
1117 func (d *dotWriter) copyUntil(w io.Writer, buf *bytes.Buffer, sep string) error {
1118 i := bytes.Index(buf.Bytes(), []byte(sep))
1120 return fmt.Errorf("couldn't find dot sep %q", sep)
1122 _, err := io.CopyN(w, buf, int64(i+len(sep)))