$def with (icons)
$var title: Icon Editor
$var body_classes = ["fullwidth"]

<header class=toolbar>
<div id=gallery>
$for icon_name, (icon_width, icon_height, icon_data) in icons.items():
    <svg title="$icon_name" xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 $icon_width $icon_height"><path fill="#666" d="$dedent(icon_data).strip()"
    /></svg>
</div>
<textarea id=source></textarea>
<div id=icon>
    <div id=preview-large></div>
    <div id=preview-medium></div>
    <div id=preview-small></div>
    <a id=save download=icon.svg href="">download</a>
</div>
<div id=toolbox>
    <div id=coords>&nbsp;</div>
    <button class=add data-shape=curve>
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="#666" d="M 120,352C 120,192 224,88 384,88L 384,136C 252,136 168,220 168,352ZM 96,352L 192,352L 192,448L 96,448ZM 384,64L 480,64L 480,160L 384,160Z" /></svg>
    </button>
    <button class=add data-shape=line>
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="#666" d="M 156,352L 384,124L 420,160L 192,388ZM 96,352L 192,352L 192,448L 96,448ZM 384,64L 480,64L 480,160L 384,160Z" /></svg>
    </button>
    <button id=showcase>Showcase</button>
</div>
</header>

<div id=editor>
<div id=canvas></div>
<div id=vrulerl class="ruler vruler"></div>
<div id=vrulerr class="ruler vruler"></div>
<div id=hruler class=ruler></div>
</div>

$def head():
    <script src=/static/utils.js></script>
    <script src=/static/moment-2.21.0.js></script>
    <script src=/static/web.js></script>
    <style>
    $# .article {
    $#     display: grid;
    $#     grid-column-gap: 1em;
    $#     grid-template-columns: 12em 40em 8em;
    $#     grid-template-rows: 2em auto; }

    header.toolbar {
        display: grid;
        grid-column-gap: 1em;
        grid-template-columns: 10em 16em 10em;
        grid-template-rows: auto 2em; }
    
    #gallery svg {
        cursor: pointer;
        height: 28px;
        width: 32px; }
    #gallery .icon {
        margin-right: .5em; }
    
    button.add svg {
        height: 14px;
        width: 16px; }
    
    #preview-large {
        height: 128px;
        width: 144px; }
    #preview-medium {
        height: 32px;
        width: 36px; }
    #preview-small {
        height: 16px;
        width: 18px; }
    
    $# #toolbox {
    $#     grid-column: 3;
    $#     grid-row: 2; }
    
    #source {
        font-size: .8em;
        height: 20em;
        width: 20em; }
    
    #editor {
        $# grid-column: 2;
        $# grid-row: 2;
        display: grid;
        grid-column-gap: .5em;
        grid-row-gap: .25em;
        grid-template-columns: 2em 41.25em 2em;
        grid-template-rows: 36.5em 2em; }
    
    .showcase #canvas {
        background: none;
        border-color: #000; }
    .showcase .ruler, .showcase circle, .showcase line {
        display: none; }
    
    #canvas {
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4gMQEiU3zw0/kAAAAMdJREFUeNrt2tEJxCAMBmDvNnCFrOAKXcH1XcERvA0UTkqhfnktJeV7SH5CU5pURIzZ81LK2Hn/uq7xZP9a6/imwwsAAAAAjq7PalXMKueceu9/N4+I1FpLT/UvpSw/UA4wAwAAAADgzTlgtUrurN09vlsRIQeYAQAAAADgHuAeIAeYAQAAAABwZA5YraI3V85ZDjADAAAAAMA9wD1ADjADAAAAAODIHOD/ADnADAAAAAAA9wD3ADnADAAAAACA83JArXWcDPADG+SewdhVz+sAAAAASUVORK5CYII=);
        border: 1px solid #555;
        grid-column: 2;
        grid-row: 1;
        height: 511px;
        width: 575px; }
    
    .selected {
        color: #3f9; }
    
    .ruler, #coords {
        font-family: Ubuntu Mono; }
    .icon {
        height: 16px;
        width: 18px; }
    
    /* rulers */
    
    .ruler span {
        color: #666;
        font-size: .7em; }
    .ruler strong {
        color: #ccc; }
    .vruler {
        margin-top: -.3em;
        width: 2em; }
    .vruler span {
        display: block;
        height: 32px; }
    #vrulerl {
        grid-column: 1;
        grid-row: 1;
        text-align: right; }
    #vrulerr {
        grid-column: 3;
        grid-row: 1; }
    #hruler {
        grid-column: 2;
        grid-row: 2;
        margin-left: -1.5em;
        width: 110%; }
    #hruler span {
        display: inline-block;
        transform: rotate(90deg);
        height: 32px;
        width: 32px; }
    </style>
    
    <script>
    var paths = [];
    var id = 0;
    
    function render() {
        var canvas = $$("#canvas");
        while (canvas.firstChild) {
            canvas.removeChild(canvas.firstChild) }
        var svglines = $$("#source").value;
        var output = '<svg viewBox="0 0 576 512"><path fill="#999" fill-opacity="0.75" d="' + svglines + '" />';
        var simplified_output = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="#666" d="' + svglines + '" />';
    
        function parseCoord(coord) {
            coords = coord.split(",");
            return [parseInt(coords[0]), parseInt(coords[1])]
        }
    
        function renderShape(shape) {
            paths = [];
            id = 0;
            var lines = svglines.split("\n");
            var path = [];
            var prev;
            for (var i = 0; i < lines.length; i++) {
                var line = lines[i];
                if (line[0] == "Z") {
                    path.push(["Z"]);
                    paths.push(path);
                    path = [];
                    continue
                }
                var parts = line.split(" ");
                switch (line[0]) {
                    case "C":
                        var from = parseCoord(parts[1]);
                        var fromx = from[0];
                        var fromy = from[1];
                        var to = parseCoord(parts[2]);
                        var tox = to[0];
                        var toy = to[1];
                        var node = parseCoord(parts[3]);
                        var nodex = node[0];
                        var nodey = node[1];
                        path.push(["C", id, id + 1, id + 2]);
                        if (shape == "line") {
                            output = output + "<line x1=" + fromx + " y1=" + fromy + " x2=" + prev[0] + " y2=" + prev[1] + " stroke=orange />";
                            output = output + "<line x1=" + tox + " y1=" + toy + " x2=" + nodex + " y2=" + nodey + " stroke=orange />";
                        } else {
                            output = output + '<circle id="n' + id++ + '" cx="' + fromx + '" cy="' + fromy + '" r="2" fill="red" />';
                            output = output + '<circle id="n' + id++ + '" cx="' + tox + '" cy="' + toy + '" r="2" fill="red" />';
                            output = output + '<circle id="n' + id++ + '" cx="' + nodex + '" cy="' + nodey + '" r="4" fill="blue" />';
                        }
                        break;
                    case "L":
                        path.push(["L", id]);
                        var node = parseCoord(parts[1]);
                        var nodex = node[0];
                        var nodey = node[1];
                        if (shape == "circle")
                            output = output + '<circle id="n' + id++ + '" cx="' + nodex + '" cy="' + nodey + '" r="4" fill="blue" />';
                        break;
                    case "M":
                        path.push(["M", id]);
                        var node = parseCoord(parts[1]);
                        var nodex = node[0];
                        var nodey = node[1];
                        if (shape == "circle")
                            output = output + '<circle id="n' + id++ + '" cx="' + nodex + '" cy="' + nodey + '" r="4" fill="blue" />';
                        break;
                    default:
                        continue
                }
                prev = [nodex, nodey, id];
            }
        }
        // paint circles second to appear above the lines
        renderShape("line");
        renderShape("circle");
    
        output = output + '</svg>';
        simplified_output = simplified_output + '</svg>';
    
        $$("#preview-large").innerHTML = output;
        $$$$("#preview-large circle").each(function() {
            this.remove();
        });
        $$$$("#preview-large line").each(function() {
            this.remove();
        });
        $$("#preview-medium").innerHTML = $$("#preview-large").innerHTML;
        $$("#preview-small").innerHTML = $$("#preview-large").innerHTML;
    
        function escapeRegExp(str) {
            return str.replace(/([.*+?^=!:$${}()|\[\]\/\\])/g, "\\$$1");
        }
        function replaceAll(str, find, replace) {
            return str.replace(new RegExp(escapeRegExp(find), 'g'), replace);
        }
        var encoded = replaceAll(simplified_output, "#", "%23");
        $$("#save").setAttribute("href", "data:image/svg+xml," + encoded);
    
        canvas.innerHTML = output;
        $$$$("circle").each(function() {
            this.addEventListener("click", function(ev) {
                this.setAttribute("stroke", "yellow");
                this.setAttribute("stroke-width", "1");
                this.classList.add("selected");
            })
        });
    }
    
    function highlightCoord(textarea) {
        for (var start = textarea.selectionStart; textarea.value[start] != " "; start--) { }
        for (var end = start + 2; textarea.value[end].match(/[\d,]/); end++) { }
        textarea.selectionStart = start + 1;
        textarea.selectionEnd = end;
        textarea.focus();
    }
    
    $$.load(function() {
        document.onkeyup = function(ev) {
            if (ev.key == "q")
                $$("#editor").classList.toggle("showcase");
        }
    
        $$("#source").addEventListener("click", function() {
            if ($$(this).value.length == 0)
                return
            highlightCoord(this);
        });
    
        /* capture selected, determine offset, shift, scratch,
           render & restore selected */
        $$("#editor").addEventListener("wheel", function(ev) {
            var selected = [];
            $$$$("circle.selected").each(function() {
                selected.push($$(this).getAttribute("id"));
            });
            var offset = -4;
            if (ev.deltaY > 0)
                offset = 4;
            var x = 0;
            var y = 0;
            if (ev.ctrlKey)
                x = offset;
            else
                y = offset;
            shiftNodes(selected, x, y);
            scratch();
            render();
            for (let node of selected) {
                var el = $$("circle#" + node);
                el.setAttribute("stroke", "yellow");
                el.setAttribute("stroke-width", "1");
                el.classList.add("selected");
            }
            ev.preventDefault();
        });
    
        function scratch() {
            var output = "";
            for (var i = 0; i < paths.length; i++) {
                var path = paths[i];
                for (var j = 0; j < path.length; j++) {
                    var command = path[j];
                    output = output + command[0] + " ";
                    for (var k = 1; k < command.length; k++) {
                        var coord = $$("#n" + command[k]);
                        output = output + coord.getAttribute("cx") + ","
                                 + coord.getAttribute("cy") + " ";
                    }
                    output = output.trim() + "\n";
                }
                output = output + "\n";
            }
            $$("#source").value = output.trim();
        }
    
        function shiftNodes(nodes, xoffset, yoffset) {
            for (var i = 0; i < paths.length; i++) {
                var path = paths[i];
                for (var j = 0; j < path.length; j++) {
                    var command = path[j];
                    for (var k = 1; k < command.length; k++) {
                        var id = command[k];
                        for (let node of nodes) {
                            if ("n" + id == node) {
                                var n = $$("#" + node);
                                n.setAttribute("cx", parseInt(n.getAttribute("cx")) + xoffset);
                                n.setAttribute("cy", parseInt(n.getAttribute("cy")) + yoffset);
                            }
                        }
                    }
                }
            }
        }
    
        function addNode(shape, x, y) {
            /* require two nodes, insert between */
            x = parseInt(x);
            y = parseInt(y);
            for (var i = 0; i < paths.length; i++) {
                var path = paths[i];
                for (var j = 0; j < path.length; j++) {
                    var command = path[j];
                    for (var k = 1; k < command.length; k++) {
                        var id = command[k];
                        var n = $$("#n" + id);
                        if (n.getAttribute("cx") == x && n.getAttribute("cy") == y) {
                            var canvas = $$("#canvas svg");
                            if (shape == "curve") {
                                canvas.append("<circle id=n9997 cx=" + (x + 12) + " cy=" + (y + 12) + ">");
                                canvas.append("<circle id=n9998 cx=" + (x + 16) + " cy=" + (y + 16) + ">");
                                canvas.append("<circle id=n9999 cx=" + (x - 8) + " cy=" + (y + 8) + ">");
                                path.splice(j + 1, 0, ["C", 9997, 9998, 9999]);
                            } else {
                                canvas.append("<circle id=n9999 cx=" + (x - 12) + " cy=" + (y - 12) + ">");
                                path.splice(j + 1, 0, ["L", 9999]);
                            }
                            return
                        }
                    }
                }
            }
        }
    
        var dragStart = 0;
        var canvasOffsets = $$("#canvas").getBoundingClientRect();
        var canvasOffset = {top: canvasOffsets.top + window.scrollY,
                            left: canvasOffsets.left + window.scrollX};
        function getCoords(ev) {
            var x = ev.clientX - canvasOffset.left;
            var y = ev.clientY - canvasOffset.top;
            return [x, y]
        }
        function getRoundedCoords(ev, s) {
            var x = Math.round((ev.clientX - canvasOffset.left) / s) * s;
            var y = Math.round((ev.clientY - canvasOffset.top) / s) * s;
            return [x, y]
        }
        $$("#canvas").addEventListener("mousedown", function(ev) {
            if (!ev.ctrlKey) {
                $$$$("circle.selected").each(function() {
                    this.setAttribute("stroke-width", "0");
                    this.classList.remove("selected");
                });
            }
            dragStart = getCoords(ev);
            return false
        })
        $$("#canvas").addEventListener("mousemove", function(ev) {
            coords = getRoundedCoords(ev, 16);
            $$("#coords").innerHTML = coords[0] + ":" + coords[1];
        })
        $$("#canvas").addEventListener("mouseup", function(ev) {
            selectCoordsByRange(dragStart, getCoords(ev));
        })
    
        function selectCoordsByRange(a, b) {
            var minx = a[0];
            if (b[0] < a[0])
                minx = b[0];
            var maxx = a[0];
            if (b[0] > a[0])
                maxx = b[0];
            var miny = a[1];
            if (b[1] < a[1])
                miny = b[1];
            var maxy = a[1];
            if (b[1] > a[1])
                maxy = b[1];
            $$$$("circle").each(function() {
                var x = parseInt(this.getAttribute("cx"));
                var y = parseInt(this.getAttribute("cy"));
                if (minx < x && x < maxx && miny < y && y < maxy) {
                    this.setAttribute("stroke", "yellow");
                    this.setAttribute("stroke-width", "1");
                    this.classList.add("selected");
                }
            });
        }
    
        $$("#source").addEventListener("keyup", render);
        render();
    
        $$(".add").addEventListener("click", function() {
            var selected = $$("circle.selected");
            switch (selected.length) {
                case 0:
                    alert("please select at least one node");
                    break;
                case 1:
                    var shape = $$(this).getAttribute("data-shape");
                    addNode(shape, selected.getAttribute("cx"), selected.getAttribute("cy"));
                    scratch();
                    render();
                    break;
                default:
                    alert("can't add a new node with multiple nodes selected");
            }
        });
        $$("#showcase").addEventListener("click", function() {
            $$("#editor").classList.toggle("showcase");
        });
    
        var ruler_html = "";
        for (var i = 0; i < 17; i++) {
            var num = i * 32;
            if (i == 8)
                num = "<strong>" + num + "</strong>";
            ruler_html = ruler_html + "<span>" + num + "</span>";
        }
        $$("#vrulerl").innerHTML = ruler_html;
        $$("#vrulerr").innerHTML = ruler_html;
    
        var ruler_html = "";
        for (var i = 0; i < 19; i++) {
            var num = i * 32;
            if (i == 9)
                num = "<strong>" + num + "</strong>";
            ruler_html = ruler_html + "<span>" + num + "</span>";
        }
        $$("#hruler").innerHTML = ruler_html;
    
        $$$$("#gallery svg").each(function() {
            this.addEventListener("click", function(ev) {
                $$("#source").value = this.querySelector("path").getAttribute("d");
                render();
            });
        });
    });
    </script>
$var head = head
