/**
  Markdeep diagrams; extracted from Markdeep.js Version 1.13
  Refined for use with nodejs


  Copyright 2015-2021, Morgan McGuire, https://casual-effects.com
  All rights reserved.

  -------------------------------------------------------------

  You may use, extend, and redistribute this code under the terms of
  the BSD license at https://opensource.org/licenses/BSD-2-Clause.
*/
'use strict';

// Mappings and constants used by markdeep.
const STROKE_WIDTH = 2;
const ARROW_COLOR = ' fill="black"'; // + ' stroke="none"', but xml2rfc doesn't like that.
const STROKE_COLOR = ' fill="none" stroke="black"';
const TEXT_COLOR = ' stroke="black"';
['min', 'max', 'abs', 'sign'].forEach(f => {
    global[f] = Math[f];
});
String.prototype.ss = String.prototype.substring;
String.prototype.rp = String.prototype.replace;

function escapeHTMLEntities(str) {
    return String(str).rp(/&/g, '&amp;').rp(/</g, '&lt;').rp(/>/g, '&gt;').rp(/"/g, '&quot;');
}

function strToArray(s) {
    return Array.from(s);
}

/**
   Adds whitespace at the end of each line of str, so that all lines have equal length in
   unicode characters (which is not the same as JavaScript characters when high-index/escape
   characters are present).
*/
function equalizeLineLengths(str) {
    var lineArray = str.split('\n');

    if ((lineArray.length > 0) && (lineArray[lineArray.length - 1] === '')) {
        // Remove the empty last line generated by split on a trailing newline
        lineArray.pop();
    }

    var longest = 0;
    lineArray.forEach(function (line) {
        longest = max(longest, strToArray(line).length);
    });

    // Worst case spaces needed for equalizing lengths
    // http://stackoverflow.com/questions/1877475/repeat-character-n-times
    var spaces = Array(longest + 1).join(' ');

    var result = '';
    lineArray.forEach(function (line) {
        // Append the needed number of spaces onto each line, and
        // reconstruct the output with newlines
        result += line + spaces.ss(strToArray(line).length) + '\n';
    });

    return result;
}

/** Finds the longest common whitespace prefix of all non-empty lines
    and then removes it */
function removeLeadingSpace(str) {
    var lineArray = str.split('\n');

    var minimum = Infinity;
    lineArray.forEach(function (line) {
        if (line.trim() !== '') {
            // This is a non-empty line
            var spaceArray = line.match(/^([ \t]*)/);
            if (spaceArray) {
                minimum = min(minimum, spaceArray[0].length);
            }
        }
    });

    if (minimum === 0) {
        // No leading space
        return str;
    }

    var result = '';
    lineArray.forEach(function (line) {
        // Strip the common spaces
        result += line.ss(minimum) + '\n';
    });

    return result;
}

/** Returns true if this character is a "letter" under the ASCII definition */
function isASCIILetter(c) {
    var code = c.charCodeAt(0);
    return ((code >= 65) && (code <= 90)) || ((code >= 97) && (code <= 122));
}

const MARKERS = { 'o': '\ue004', 'v': '\ue005', 'V': '\ue006' };

function hideChar(s, i) {
    let r = new Array(3);
    r.fill('[a-zA-Z' + Object.values(MARKERS).join('') + ']');
    r[i] = '[' + Object.keys(MARKERS).join('') + ']';
    return s.replace(new RegExp(r.join(''), 'g'),
        v => v.substring(0, i) + MARKERS[v.charAt(i)] + v.substring(i + 1));
}

function unhideMarkers(s) {
    Object.keys(MARKERS).forEach(k => {
        s = s.rp(new RegExp(MARKERS[k], 'g'), k);
    });
    return s;
}

function hideMarkers(s) {
    s = hideChar(s, 0);
    s = hideChar(s, 1);
    s = hideChar(s, 2);
    // Unhide strings that only contain 'o' or 'v'.
    // Note: Using \B as \ue00? is a non-word character.
    const allHidden = '\\B[' + Object.values(MARKERS).join('') + ']{3,}\\B';
    return s.replace(new RegExp(allHidden, 'g'), unhideMarkers);
}


/** Converts diagramString, which is a Markdeep diagram without the surrounding asterisks, to
    SVG (HTML). Lines may have ragged lengths.

    `options` is a dictionary with the following keys:

    backdrop (default: false) will add a white <rect> element as a backdrop so that
        the background of the image is not transparent
    disableText (default: false) will disable passing text
    showGrid (default: false) will display a debug grid
    spaces (default: 2) the number of spaces between different strings
    style (default: {}) a dictionary of attributes to attach to the <svg> element
 */
function diagramToSVG(diagramString, options) {
    // Clean up diagramString
    diagramString = equalizeLineLengths(removeLeadingSpace(diagramString));
    options = options || {};
    if (!Number.isInteger(options.spaces)) { options.spaces = 2; } // 0 is valid so falsy tests fail.

    // Temporarily replace 'o', 'v', and 'V' if they are surrounded by other
    // text. Use another character to avoid processing them as decorations.
    // These will be swapped back in the final SVG.
    diagramString = hideMarkers(diagramString);

    /** Pixels per character */
    var SCALE = 8;

    /** Multiply Y coordinates by this when generating the final SVG
        result to account for the aspect ratio of text files. */
    var ASPECT = 2;

    var DIAGONAL_ANGLE = Math.atan(1.0 / ASPECT) * 180 / Math.PI;

    var EPSILON = 1e-6;

    // The order of the following is based on rotation angles
    // and is used for ArrowSet.toSVG
    var ARROW_HEAD_CHARACTERS = '>v<^';
    var POINT_CHARACTERS = 'o*◌○◍●⊕';
    var JUMP_CHARACTERS = '()';
    var UNDIRECTED_VERTEX_CHARACTERS = "+";
    var VERTEX_CHARACTERS = UNDIRECTED_VERTEX_CHARACTERS + ".',`";

    // GRAY[i] is the Unicode block character for (i+1)/4 level gray
    var GRAY_CHARACTERS = '\u2591\u2592\u2593\u2588';

    // TRI[i] is a right-triangle rotated by 90*i
    var TRI_CHARACTERS = '\u25E2\u25E3\u25E4\u25E5';

    var DECORATION_CHARACTERS = ARROW_HEAD_CHARACTERS + POINT_CHARACTERS + JUMP_CHARACTERS + GRAY_CHARACTERS + TRI_CHARACTERS;

    function isUndirectedVertex(c) { return UNDIRECTED_VERTEX_CHARACTERS.indexOf(c) + 1; }
    function isVertex(c) { return VERTEX_CHARACTERS.indexOf(c) !== -1; }
    function isTopVertex(c) { return isUndirectedVertex(c) || (c === '.') || (c === ','); }
    function isBottomVertex(c) { return isUndirectedVertex(c) || (c === "'") || (c === '`'); }
    function isVertexOrLeftDecoration(c) { return isVertex(c) || (c === '<') || isPoint(c); }
    function isVertexOrRightDecoration(c) { return isVertex(c) || (c === '>') || isPoint(c); }
    function isGray(c) { return GRAY_CHARACTERS.indexOf(c) + 1; }
    function isTri(c) { return TRI_CHARACTERS.indexOf(c) + 1; }

    // "D" = Diagonal slash (/), "B" = diagonal Backslash (\)
    // Characters that may appear anywhere on a solid line
    function isSolidHLine(c) { return (c === '-') || (c === '\u2501') || isUndirectedVertex(c) || isJump(c); }
    function isDoubleHLine(c) { return (c === '=') || (c === '\u2550') || isUndirectedVertex(c) || isJump(c); }
    function isSolidVLineOrJumpOrPoint(c) { return isSolidVLine(c) || isJump(c) || isPoint(c); }
    function isSolidVLine(c) { return (c === '|') || (c === '\u2503') || isUndirectedVertex(c); }
    function isDoubleVLine(c) { return (c === '\u2551') || isUndirectedVertex(c); }
    function isSolidDLine(c) { return (c === '/') || isUndirectedVertex(c) }
    function isSolidBLine(c) { return (c === '\\') || isUndirectedVertex(c); }
    function isJump(c) { return JUMP_CHARACTERS.indexOf(c) + 1; }
    function isPoint(c) { return POINT_CHARACTERS.indexOf(c) + 1; }
    function isDecoration(c) { return DECORATION_CHARACTERS.indexOf(c) + 1; }

    ///////////////////////////////////////////////////////////////////////////////
    // Math library

    /** Invoke as new Vec2(v) to clone or new Vec2(x, y) to create from coordinates.
        Can also invoke without new for brevity. */
    function Vec2(x, y) {
        // Detect when being run without new
        if (!(this instanceof Vec2)) { return new Vec2(x, y); }

        if (y === undefined) {
            if (x === undefined) { x = y = 0; }
            else if (x instanceof Vec2) { y = x.y; x = x.x; }
            else { throw new Error("Vec2 requires one Vec2 or (x, y) as an argument"); }
        }
        this.x = x;
        this.y = y;
        Object.seal(this);
    }

    /** Returns coordinates */
    Vec2.prototype.coords = function () {
        function s(x) { return x.toFixed(5).replace(/\.?0*$/, ''); }
        return s((this.x + 1) * SCALE) + ',' + s((this.y + 1) * SCALE * ASPECT);
    }
    /** Returns an SVG representation, with a trailing space */
    Vec2.prototype.toString = Vec2.prototype.toSVG =
        function () { return this.coords() + ' '; };
    /** Return an offset Vec2 */
    Vec2.prototype.offset = function (dx, dy) { return Vec2(this.x + dx, this.y + dy); }

    /** Converts a "rectangular" string defined by newlines into 2D
        array of characters. Grids are immutable. */
    function makeGrid(str) {
        /** Returns ' ' for out of bounds values */
        var grid = function (x, y) {
            if (y === undefined) {
                if (x instanceof Vec2) { y = x.y; x = x.x; }
                else { throw new Error('grid requires either a Vec2 or (x, y)'); }
            }

            return ((x >= 0) && (x < grid.width) && (y >= 0) && (y < grid.height)) ?
                str[y * (grid.width + 1) + x] : ' ';
        };

        // Elements are true when consumed
        grid._used = [];

        grid.height = str.split('\n').length;
        if (str[str.length - 1] === '\n') { --grid.height; }

        // Convert the string to an array to better handle greater-than 16-bit unicode
        // characters, which JavaScript does not process correctly with indices. Do this after
        // the above string processing.
        str = strToArray(str);
        grid.width = str.indexOf('\n');

        grid.v = function (x, y) {
            return ((x >= 0) && (x < grid.width) && (y >= 0) && (y < grid.height)) ?
                str[y * (grid.width + 1) + x] : ' ';
        };

        /** Mark this location. Takes a Vec2 or (x, y) */
        grid.setUsed = function (x, y) {
            if (y === undefined) {
                if (x instanceof Vec2) { y = x.y; x = x.x; }
                else { throw new Error('grid requires either a Vec2 or (x, y)'); }
            }
            if ((x >= 0) && (x < grid.width) && (y >= 0) && (y < grid.height)) {
                // Match the source string indexing
                grid._used[y * (grid.width + 1) + x] = true;
            }
        };

        grid.isUsed = function (x, y) {
            if (y === undefined) {
                if (x instanceof Vec2) { y = x.y; x = x.x; }
                else { throw new Error('grid requires either a Vec2 or (x, y)'); }
            }
            return (this._used[y * (this.width + 1) + x] === true);
        };

        /** Returns the x offset of the next run of text on the line;
            returns this.width if there is no text. */
        grid.textStart = function (x, y) {
            for (; x < this.width; ++x) {
                if (this.isUsed(x, y)) { continue; }
                if (this(x, y) !== ' ') { break; }
            }
            return x;
        }

        /** Returns the text at the given location, marking these locations as used. */
        grid.text = function (x, y) {
            let spaces = 0;
            let end = x;
            for (; end < this.width; ++end) {
                if (this.isUsed(end, y)) {
                    break;
                }
                if (this(end, y) === ' ') {
                    spaces++;
                } else {
                    spaces = 0;
                }
                if (spaces >= options.spaces) {
                    end++;
                    break;
                }
            }
            end -= spaces;
            for (let i = x; i < end; ++i) {
                this._used[y * (this.width + 1) + i] = true;
            }
            return str.slice(y * (this.width + 1) + x, y * (this.width + 1) + end);
        }

        /** Returns true if there is a solid vertical line passing through (x, y) */
        grid.isVLineAt = function (x, y, f, short) {
            if (y === undefined) { y = x.x; x = x.x; }

            var up = grid(x, y - 1);
            var c = grid(x, y);
            var dn = grid(x, y + 1);

            var uprt = grid(x + 1, y - 1);
            var uplt = grid(x - 1, y - 1);

            if (f(c)) {
                // Looks like a vertical line...does it continue?
                return (isTopVertex(up) || (up === '^') || f(up) || isJump(up) ||
                    isBottomVertex(dn) || (dn === 'v') || f(dn) || isJump(dn) ||
                    isPoint(up) || isPoint(dn) || (grid(x, y - 1) === '_') || (uplt === '_') ||
                    (uprt === '_') ||
                    // Special case of 1-high vertical on two curved corners
                    ((isTopVertex(uplt) || isTopVertex(uprt)) &&
                        (isBottomVertex(grid(x - 1, y + 1)) || isBottomVertex(grid(x + 1, y + 1)))));

            } else if (isTopVertex(c) || (c === '^')) {
                // May be the top of a vertical line
                return f(dn) || (isJump(dn) && (c !== '.'));
            } else if (isBottomVertex(c) || (c === 'v' || c === 'V')) {
                return f(up) || (isJump(up) && (c !== "'"));
            } else if (isPoint(c)) {
                return f(up) || f(dn);
            }

            return false;
        };
        grid.isSolidVLineAt = function (x, y) {
            return grid.isVLineAt(x, y, isSolidVLine, true);
        };
        grid.isDoubleVLineAt = function (x, y) {
            return grid.isVLineAt(x, y, isDoubleVLine, false);
        };

        /** Returns true if there is a solid middle (---) horizontal line
            passing through (x, y). Ignores underscores. */
        grid.isHLineAt = function (x, y, f) {
            if (y === undefined) { y = x.x; x = x.x; }

            var ltlt = grid(x - 2, y);
            var lt = grid(x - 1, y);
            var c = grid(x + 0, y);
            var rt = grid(x + 1, y);
            var rtrt = grid(x + 2, y);

            if (f(c) || (f(lt) && isJump(c))) {
                // Looks like a horizontal line...does it continue? We need three in a row.
                if (f(lt)) {
                    return f(rt) || isVertexOrRightDecoration(rt) || f(ltlt) || isVertexOrLeftDecoration(ltlt);
                } else if (isVertexOrLeftDecoration(lt)) {
                    return f(rt);
                } else {
                    return f(rt) && (f(rtrt) || isVertexOrRightDecoration(rtrt));
                }

            } else if (c === '<') {
                return f(rt) && f(rtrt)

            } else if (c === '>') {
                return f(lt) && f(ltlt);

            } else if (isVertex(c)) {
                return ((f(lt) && f(ltlt)) || (f(rt) && f(rtrt)));
            }

            return false;
        };
        grid.isSolidHLineAt = function (x, y) {
            return this.isHLineAt(x, y, isSolidHLine);
        };
        grid.isDoubleHLineAt = function (x, y) {
            return this.isHLineAt(x, y, isDoubleHLine);
        };


        /** Returns true if there is a solid backslash line passing through (x, y) */
        grid.isSolidBLineAt = function (x, y) {
            if (y === undefined) { y = x.x; x = x.x; }
            var c = grid(x, y);
            var lt = grid(x - 1, y - 1);
            var rt = grid(x + 1, y + 1);

            if (c === '\\') {
                // Looks like a diagonal line...does it continue? We need two in a row.
                return (isSolidBLine(rt) || isBottomVertex(rt) || isPoint(rt) || (rt === 'v' || rt === 'V') ||
                    isSolidBLine(lt) || isTopVertex(lt) || isPoint(lt) || (lt === '^') ||
                    (grid(x, y - 1) === '/') || (grid(x, y + 1) === '/') || (rt === '_') || (lt === '_') ||
                    (grid(x + 1, y) === '/' && grid(x - 1, y - 1) === '_') ||
                    (grid(x - 1, y) === '/' && grid(x + 1, y) === '_'));
            } else if (c === '.') {
                return (rt === '\\');
            } else if (c === "'") {
                return (lt === '\\');
            } else if (c === '^') {
                return rt === '\\';
            } else if (c === 'v' || c === 'V') {
                return lt === '\\';
            } else if (isVertex(c) || isPoint(c) || (c === '|')) {
                return isSolidBLine(lt) || isSolidBLine(rt);
            }
        };


        /** Returns true if there is a solid diagonal line passing through (x, y) */
        grid.isSolidDLineAt = function (x, y) {
            if (y === undefined) { y = x.x; x = x.x; }

            var c = grid(x, y);
            var lt = grid(x - 1, y + 1);
            var rt = grid(x + 1, y - 1);

            if (c === '/' && ((grid(x, y - 1) === '\\') || (grid(x, y + 1) === '\\'))) {
                // Special case of tiny hexagon corner
                return true;
            } else if (c === '/' && ((grid(x + 1, y) === '\\' && grid(x - 1, y) === '_') ||
                (grid(x - 1, y) === '\\' && grid(x + 1, y - 1) === '_'))) {
                // _/\ or   _
                //        \/
                return true;
            } else if (isSolidDLine(c)) {
                // Looks like a diagonal line...does it continue? We need two in a row.
                return (isSolidDLine(rt) || isTopVertex(rt) || isPoint(rt) || (rt === '^') || (rt === '_') ||
                    isSolidDLine(lt) || isBottomVertex(lt) || isPoint(lt) || (lt === 'v' || lt === 'V') || (lt === '_'));
            } else if (c === '.' || c === ',') {
                return (lt === '/');
            } else if (c === "'") {
                return (rt === '/');
            } else if (c === '^') {
                return lt === '/';
            } else if (c === 'v' || c === 'V') {
                return rt === '/';
            } else if (isVertex(c) || isPoint(c) || (c === '|')) {
                return isSolidDLine(lt) || isSolidDLine(rt);
            }
            return false;
        };

        grid.toString = function () { return str; };

        return Object.freeze(grid);
    }


    /** A 1D curve. If C is specified, the result is a bezier with
        that as the tangent control point */
    function Path(A, B, C, D, style) {
        if (!((A instanceof Vec2) && (B instanceof Vec2))) {
            throw new Error('Path constructor requires at least two Vec2s');
        }
        this.A = A;
        this.B = B;
        if (C) {
            this.C = C;
            if (D) {
                this.D = D;
            } else {
                this.D = C;
            }
        }

        this.dashed = style === 'dashed' || false;
        this.double = style === 'double' || false;

        Object.freeze(this);
    }

    var _ = Path.prototype;
    _.isVertical = function () {
        return this.B.x === this.A.x;
    };

    _.isHorizontal = function () {
        return this.B.y === this.A.y;
    };

    /** Diagonal lines look like: / See also backDiagonal */
    _.isDiagonal = function () {
        var dx = this.B.x - this.A.x;
        var dy = this.B.y - this.A.y;
        return (abs(dy + dx) < EPSILON);
    };

    _.isBackDiagonal = function () {
        var dx = this.B.x - this.A.x;
        var dy = this.B.y - this.A.y;
        return (abs(dy - dx) < EPSILON);
    };

    _.isCurved = function () {
        return this.C !== undefined;
    };

    /** Does this path have any end at (x, y) */
    _.endsAt = function (x, y) {
        if (y === undefined) { y = x.y; x = x.x; }
        return ((this.A.x === x) && (this.A.y === y)) ||
            ((this.B.x === x) && (this.B.y === y));
    };

    /** Does this path have an up end at (x, y) */
    _.upEndsAt = function (x, y) {
        if (y === undefined) { y = x.y; x = x.x; }
        return this.isVertical() && (this.A.x === x) && (min(this.A.y, this.B.y) === y);
    };

    /** Does this path have an up end at (x, y) */
    _.diagonalUpEndsAt = function (x, y) {
        if (!this.isDiagonal()) { return false; }
        if (y === undefined) { y = x.y; x = x.x; }
        if (this.A.y < this.B.y) {
            return (this.A.x === x) && (this.A.y === y);
        } else {
            return (this.B.x === x) && (this.B.y === y);
        }
    };

    /** Does this path have a down end at (x, y) */
    _.diagonalDownEndsAt = function (x, y) {
        if (!this.isDiagonal()) { return false; }
        if (y === undefined) { y = x.y; x = x.x; }
        if (this.B.y < this.A.y) {
            return (this.A.x === x) && (this.A.y === y);
        } else {
            return (this.B.x === x) && (this.B.y === y);
        }
    };

    /** Does this path have an up end at (x, y) */
    _.backDiagonalUpEndsAt = function (x, y) {
        if (!this.isBackDiagonal()) { return false; }
        if (y === undefined) { y = x.y; x = x.x; }
        if (this.A.y < this.B.y) {
            return (this.A.x === x) && (this.A.y === y);
        } else {
            return (this.B.x === x) && (this.B.y === y);
        }
    };

    /** Does this path have a down end at (x, y) */
    _.backDiagonalDownEndsAt = function (x, y) {
        if (!this.isBackDiagonal()) { return false; }
        if (y === undefined) { y = x.y; x = x.x; }
        if (this.B.y < this.A.y) {
            return (this.A.x === x) && (this.A.y === y);
        } else {
            return (this.B.x === x) && (this.B.y === y);
        }
    };

    /** Does this path have a down end at (x, y) */
    _.downEndsAt = function (x, y) {
        if (y === undefined) { y = x.y; x = x.x; }
        return this.isVertical() && (this.A.x === x) && (max(this.A.y, this.B.y) === y);
    };

    /** Does this path have a left end at (x, y) */
    _.leftEndsAt = function (x, y) {
        if (y === undefined) { y = x.y; x = x.x; }
        return this.isHorizontal() && (this.A.y === y) && (min(this.A.x, this.B.x) === x);
    };

    /** Does this path have a right end at (x, y) */
    _.rightEndsAt = function (x, y) {
        if (y === undefined) { y = x.y; x = x.x; }
        return this.isHorizontal() && (this.A.y === y) && (max(this.A.x, this.B.x) === x);
    };

    _.verticalPassesThrough = function (x, y) {
        if (y === undefined) { y = x.y; x = x.x; }
        return this.isVertical() &&
            (this.A.x === x) &&
            (min(this.A.y, this.B.y) <= y) &&
            (max(this.A.y, this.B.y) >= y);
    }

    _.horizontalPassesThrough = function (x, y) {
        if (y === undefined) { y = x.y; x = x.x; }
        return this.isHorizontal() &&
            (this.A.y === y) &&
            (min(this.A.x, this.B.x) <= x) &&
            (max(this.A.x, this.B.x) >= x);
    }

    _.offsetLine = function (dx, dy) {
        let svg = '<path d="M ' + this.A.offset(dx, dy);
        if (this.isCurved()) {
            let c =
                svg += 'C ' + this.C.offset(dx, dy) + this.D.offset(dx, dy);
        } else {
            svg += 'L ';
        }
        svg += this.B.offset(dx, dy).coords() + '"' + STROKE_COLOR;
        if (this.dashed) {
            svg += ' stroke-dasharray="3,6"';
        }
        svg += '/>';
        return svg;
    }

    /** Returns a string suitable for inclusion in an SVG tag */
    _.toSVG = function () {
        let svg = '';
        if (this.double) {
            let vx = this.B.x - this.A.x;
            let vy = this.B.y - this.A.y;
            let s = Math.sqrt(vx * vx + vy * vy);
            vx /= s * SCALE;
            vy /= s * SCALE / ASPECT;
            svg += this.offsetLine(vy, -vx);
            svg += this.offsetLine(-vy, vx);
        } else {
            svg = this.offsetLine(0, 0);
        }

        return svg;
    };


    /** A group of 1D curves. This was designed so that all of the
        methods can later be implemented in O(1) time, but it
        currently uses O(n) implementations for source code
        simplicity. */
    function PathSet() {
        this._pathArray = [];
    }

    var PS = PathSet.prototype;
    PS.insert = function (path) {
        this._pathArray.push(path);
    };

    /** Returns a new method that returns true if method(x, y)
        returns true on any element of _pathAray */
    function makeFilterAny(method) {
        return function (x, y) {
            for (var i = 0; i < this._pathArray.length; ++i) {
                if (method.call(this._pathArray[i], x, y)) { return true; }
            }
            // Fall through: return undefined == false
        }
    }

    // True if an up line ends at these coordinates. Recall that the
    // variable _ is bound to the Path prototype still.
    PS.upEndsAt = makeFilterAny(_.upEndsAt);
    PS.diagonalUpEndsAt = makeFilterAny(_.diagonalUpEndsAt);
    PS.backDiagonalUpEndsAt = makeFilterAny(_.backDiagonalUpEndsAt);
    PS.diagonalDownEndsAt = makeFilterAny(_.diagonalDownEndsAt);
    PS.backDiagonalDownEndsAt = makeFilterAny(_.backDiagonalDownEndsAt);
    PS.downEndsAt = makeFilterAny(_.downEndsAt);
    PS.leftEndsAt = makeFilterAny(_.leftEndsAt);
    PS.rightEndsAt = makeFilterAny(_.rightEndsAt);
    PS.endsAt = makeFilterAny(_.endsAt);
    PS.verticalPassesThrough = makeFilterAny(_.verticalPassesThrough);
    PS.horizontalPassesThrough = makeFilterAny(_.horizontalPassesThrough);

    /** Returns an SVG string */
    PS.toSVG = function () {
        var svg = '';
        for (var i = 0; i < this._pathArray.length; ++i) {
            svg += this._pathArray[i].toSVG() + '\n';
        }
        return svg;
    };


    function DecorationSet() {
        this._decorationArray = [];
    }

    var DS = DecorationSet.prototype;

    /** insert(x, y, type, <angle>)
        insert(vec, type, <angle>)

        angle is the angle in degrees to rotate the result */
    DS.insert = function (x, y, type, angle) {
        if (type === undefined) { type = y; y = x.y; x = x.x; }

        if (!isDecoration(type)) {
            throw new Error('Illegal decoration character: ' + type);
        }
        var d = { C: Vec2(x, y), type: type, angle: angle || 0 };

        // Put arrows at the front and points at the back so that
        // arrows always draw under points
        if (isPoint(type)) {
            this._decorationArray.push(d);
        } else {
            this._decorationArray.unshift(d);
        }
    };


    DS.toSVG = function () {
        var svg = '';
        for (var i = 0; i < this._decorationArray.length; ++i) {
            var decoration = this._decorationArray[i];
            var C = decoration.C;

            if (isJump(decoration.type)) {
                // Slide jumps
                var dx = (decoration.type === ')') ? +0.75 : -0.75;
                var up = Vec2(C.x, C.y - 0.5);
                var dn = Vec2(C.x, C.y + 0.5);
                var cup = Vec2(C.x + dx, C.y - 0.5);
                var cdn = Vec2(C.x + dx, C.y + 0.5);

                svg += '<path class="jump" d="M ' + dn + 'C ' + cdn + cup + up.coords() + '"' + STROKE_COLOR + '/>';

            } else if (isPoint(decoration.type)) {
                const CLASSES = { '*': 'closed', 'o': 'open', '◌': 'dotted', '○': 'open', '◍': 'shaded', '●': 'closed', '⊕': 'xor' };
                const FILL = { 'closed': 'black', 'open': 'white', 'dotted': 'white', 'shaded': '#666', 'xor': 'white' };
                const STROKE = {
                    'closed': '',
                    'open': ' stroke="black"',
                    'dotted': ' stroke="black" stroke-dasharray="1,1"',
                    'shaded': ' stroke="black"',
                    'xor': ' stroke="black"',
                };
                var cls = CLASSES[decoration.type];
                svg += '<circle cx="' + ((C.x + 1) * SCALE) + '" cy="' + ((C.y + 1) * SCALE * ASPECT) +
                    '" r="' + (SCALE - STROKE_WIDTH) + '" class="' + cls + 'dot"' +
                    ' fill="' + FILL[cls] + '"' + STROKE[cls] + '/>\n';
                if (decoration.type === '⊕') {
                    svg += '<line x1="' + ((C.x) * SCALE + STROKE_WIDTH) +
                        '" y1="' + ((C.y + 1) * SCALE * ASPECT) +
                        '" x2="' + ((C.x + 2) * SCALE - STROKE_WIDTH) +
                        '" y2="' + ((C.y + 1) * SCALE * ASPECT) +
                        '" stroke="black"/>';
                    svg += '<line x1="' + ((C.x + 1) * SCALE) +
                        '" y1="' + ((C.y + 1) * SCALE * ASPECT - SCALE + STROKE_WIDTH) +
                        '" x2="' + ((C.x + 1) * SCALE) +
                        '" y2="' + ((C.y + 1) * SCALE * ASPECT + SCALE - STROKE_WIDTH) +
                        '" stroke="black"/>';
                }
            } else if (isGray(decoration.type)) {
                var shade = Math.round((3 - GRAY_CHARACTERS.indexOf(decoration.type)) * 63.75);
                svg += '<rect class="gray" x="' + ((C.x + 0.5) * SCALE) + '" y="' + ((C.y + 0.5) * SCALE * ASPECT) +
                    '" width="' + SCALE + '" height="' + (SCALE * ASPECT) +
                    '" fill="rgb(' + shade + ',' + shade + ',' + shade + ')"/>\n';
            } else if (isTri(decoration.type)) {
                // 30-60-90 triangle
                var index = TRI_CHARACTERS.indexOf(decoration.type);
                var xs = 0.5 - (index & 1);
                var ys = 0.5 - (index >> 1);
                xs *= sign(ys);
                var tip = Vec2(C.x + xs, C.y - ys);
                var up = Vec2(C.x + xs, C.y + ys);
                var dn = Vec2(C.x - xs, C.y + ys);
                svg += '<polygon class="triangle" points="' + tip + up + dn.coords(4) + '"' + ARROW_COLOR + '/>\n';
            } else { // Arrow head
                var tip = Vec2(C.x + 1, C.y);
                var up = Vec2(C.x - 0.5, C.y - 0.35);
                var dn = Vec2(C.x - 0.5, C.y + 0.35);
                svg += '<polygon class="arrowhead" points="' + tip + up + dn.coords() + '"' + ARROW_COLOR +
                    ' transform="rotate(' + decoration.angle + ',' + C.coords() + ')"/>\n';
            }
        }
        return svg;
    };

    ////////////////////////////////////////////////////////////////////////////

    function findPaths(grid, pathSet) {
        // Does the line from A to B contain at least one c?
        function lineContains(A, B, c) {
            var dx = sign(B.x - A.x);
            var dy = sign(B.y - A.y);
            var x, y;

            for (x = A.x, y = A.y; (x !== B.x) || (y !== B.y); x += dx, y += dy) {
                if (grid(x, y) === c) { return true; }
            }

            // Last point
            return (grid(x, y) === c);
        }

        // Find all solid vertical lines. Iterate horizontally
        // so that we never hit the same line twice
        for (var x = 0; x < grid.width; ++x) {
            for (var y = 0; y < grid.height; ++y) {
                function vline(p, boxt, boxb, style) {
                    if (grid[p](x, y)) {
                        // This character begins a vertical line...now, find the end
                        var A = Vec2(x, y);
                        do { grid.setUsed(x, y); ++y; } while (grid[p](x, y));
                        var B = Vec2(x, y - 1);

                        var up = grid(A);
                        var upup = grid(A.x, A.y - 1);

                        if (!isVertex(up) && ((upup === '-') || (upup === '_') ||
                            (boxt.indexOf(upup) >= 0) ||
                            (grid(A.x - 1, A.y - 1) === '_') ||
                            (grid(A.x + 1, A.y - 1) === '_') ||
                            isBottomVertex(upup)) || isJump(upup)) {
                            // Stretch up to almost reach the line above (if there is a decoration,
                            // it will finish the gap)
                            A.y -= 0.5;
                        }

                        var dn = grid(B);
                        var dndn = grid(B.x, B.y + 1);
                        if (!isVertex(dn) && ((dndn === '-') || (boxb.indexOf(dndn) >= 0) || isTopVertex(dndn)) || isJump(dndn) ||
                            (grid(B.x - 1, B.y) === '_') || (grid(B.x + 1, B.y) === '_')) {
                            // Stretch down to almost reach the line below
                            B.y += 0.5;
                        }

                        // Don't insert degenerate lines
                        if ((A.x !== B.x) || (A.y !== B.y)) {
                            pathSet.insert(new Path(A, B, null, null, style));
                            return true;
                        }
                        // Continue the search from the end value y+1
                    }
                    return false;
                }

                if (vline("isSolidVLineAt", '\u2564', '\u2567') ||
                    vline("isDoubleVLineAt", '\u2565\u2566', '\u2568\u2569', "double")) { continue; }

                // Some very special patterns for the short lines needed on
                // circuit diagrams. Only invoke these if not also on a curve
                //      _  _
                //    -'    '-   -'
                if ((grid(x, y) === "'") &&
                    (((grid(x - 1, y) === '-') && (grid(x + 1, y - 1) === '_') &&
                        !isSolidVLineOrJumpOrPoint(grid(x - 1, y - 1))) ||
                        ((grid(x - 1, y - 1) === '_') && (grid(x + 1, y) === '-') &&
                            !isSolidVLineOrJumpOrPoint(grid(x + 1, y - 1))))) {
                    pathSet.insert(new Path(Vec2(x, y - 0.5), Vec2(x, y)));
                }

                //    _.-  -._
                else if ((grid(x, y) === '.') &&
                    (((grid(x - 1, y) === '_') && (grid(x + 1, y) === '-') &&
                        !isSolidVLineOrJumpOrPoint(grid(x + 1, y + 1))) ||
                        ((grid(x - 1, y) === '-') && (grid(x + 1, y) === '_') &&
                            !isSolidVLineOrJumpOrPoint(grid(x - 1, y + 1))))) {
                    pathSet.insert(new Path(Vec2(x, y), Vec2(x, y + 0.5)));
                }

                // For drawing resistors: -.╱
                else if ((grid(x, y) === '.') &&
                    (grid(x - 1, y) === '-') &&
                    (grid(x + 1, y) === '\u2571')) {
                    pathSet.insert(new Path(Vec2(x, y), Vec2(x + 0.5, y + 0.5)));
                }

                // For drawing resistors: ╱'-
                else if ((grid(x, y) === "'") &&
                    (grid(x + 1, y) === '-') &&
                    (grid(x - 1, y) === '\u2571')) {
                    pathSet.insert(new Path(Vec2(x, y), Vec2(x - 0.5, y - 0.5)));
                }

            } // y
        } // x


        // Find all solid horizontal lines
        for (var y = 0; y < grid.height; ++y) {
            for (var x = 0; x < grid.width; ++x) {
                function hline(p, boxl, boxr, style) {
                    if (grid[p](x, y)) {
                        // Begins a line...find the end
                        var A = Vec2(x, y);
                        do { grid.setUsed(x, y); ++x; } while (grid[p](x, y));
                        var B = Vec2(x - 1, y);

                        // Detect adjacent box-drawing characters and lengthen the edge
                        if (boxr.indexOf(grid(B.x + 1, B.y)) >= 0) { B.x += 0.5; }
                        if (boxl.indexOf(grid(A.x - 1, A.y)) >= 0) { A.x -= 0.5; }

                        // Detect curves and shorten the edge
                        if (!isVertex(grid(A.x - 1, A.y)) &&
                            ((isTopVertex(grid(A)) && isSolidVLineOrJumpOrPoint(grid(A.x - 1, A.y + 1))) ||
                                (isBottomVertex(grid(A)) && isSolidVLineOrJumpOrPoint(grid(A.x - 1, A.y - 1))))) {
                            ++A.x;
                        }

                        if (!isVertex(grid(B.x + 1, B.y)) &&
                            ((isTopVertex(grid(B)) && isSolidVLineOrJumpOrPoint(grid(B.x + 1, B.y + 1))) ||
                                (isBottomVertex(grid(B)) && isSolidVLineOrJumpOrPoint(grid(B.x + 1, B.y - 1))))) {
                            --B.x;
                        }

                        // Only insert non-degenerate lines
                        if ((A.x !== B.x) || (A.y !== B.y)) {
                            pathSet.insert(new Path(A, B, null, null, style));
                            return true;
                        }

                        // Continue the search from the end x+1
                    }
                    return false;
                }

                hline("isSolidHLineAt", '\u2523', '\u252b', null) ||
                    hline("isDoubleHLineAt", '\u255E', '\u2561', "double");
            }
        } // y

        // Find all solid left-to-right downward diagonal lines (BACK DIAGONAL)
        for (var i = -grid.height; i < grid.width; ++i) {
            for (var x = i, y = 0; y < grid.height; ++y, ++x) {
                if (grid.isSolidBLineAt(x, y)) {
                    // Begins a line...find the end
                    var A = Vec2(x, y);
                    do { ++x; ++y; } while (grid.isSolidBLineAt(x, y));
                    var B = Vec2(x - 1, y - 1);

                    // Ensure that the entire line wasn't just vertices
                    if (lineContains(A, B, '\\')) {
                        for (var j = A.x; j <= B.x; ++j) {
                            grid.setUsed(j, A.y + (j - A.x));
                        }

                        var top = grid(A);
                        var up = grid(A.x, A.y - 1);
                        var uplt = grid(A.x - 1, A.y - 1);
                        if ((up === '/') || (uplt === '_') || (up === '_') ||
                            (!isVertex(top) &&
                                (isSolidHLine(uplt) || isSolidVLine(uplt)))) {
                            // Continue half a cell more to connect for:
                            //  ___   ___
                            //  \        \    /      ----     |
                            //   \        \   \        ^      |^
                            A.x -= 0.5; A.y -= 0.5;
                        } else if (isPoint(uplt)) {
                            // Continue 1/4 cell more to connect for:
                            //
                            //  o
                            //   ^
                            //    \
                            A.x -= 0.25; A.y -= 0.25;
                        } else if (top === '\\' && grid.isSolidDLineAt(A.x - 1, A.y)) {
                            // Cap a sharp vertex:
                            //   \  /   \  _
                            //    \/     \/
                            A.x -= 0.5; A.y -= 0.5;
                        }

                        var bottom = grid(B);
                        var dnrt = grid(B.x + 1, B.y + 1);
                        if ((grid(B.x, B.y + 1) === '/') || (grid(B.x + 1, B.y) === '_') ||
                            (grid(B.x - 1, B.y) === '_') ||
                            (!isVertex(grid(B)) &&
                                (isSolidHLine(dnrt) || isSolidVLine(dnrt)))) {
                            // Continue half a cell more to connect for:
                            //                       \      \ |
                            //  \       \     \       v      v|
                            //   \__   __\    /      ----     |

                            B.x += 0.5; B.y += 0.5;
                        } else if (isPoint(dnrt)) {
                            // Continue 1/4 cell more to connect for:
                            //
                            //    \
                            //     v
                            //      o

                            B.x += 0.25; B.y += 0.25;
                        } else if (bottom === '\\' && grid.isSolidDLineAt(B.x + 1, B.y)) {
                            // Cap a sharp vertex:
                            //     /\   _/\
                            //    /  \     \
                            B.x += 0.5; B.y += 0.5;
                        }

                        pathSet.insert(new Path(A, B));
                        // Continue the search from the end x+1,y+1
                    } // lineContains
                }
            }
        } // i


        // Find all solid left-to-right upward diagonal lines (DIAGONAL)
        for (var i = -grid.height; i < grid.width; ++i) {
            for (var x = i, y = grid.height - 1; y >= 0; --y, ++x) {
                if (grid.isSolidDLineAt(x, y)) {
                    // Begins a line...find the end
                    var A = Vec2(x, y);
                    do { ++x; --y; } while (grid.isSolidDLineAt(x, y));
                    var B = Vec2(x - 1, y + 1);

                    if (lineContains(A, B, '/')) {
                        // This is definitely a line. Commit the characters on it
                        for (var j = A.x; j <= B.x; ++j) {
                            grid.setUsed(j, A.y - (j - A.x));
                        }

                        var up = grid(B.x, B.y - 1);
                        var uprt = grid(B.x + 1, B.y - 1);
                        var bottom = grid(B);
                        if ((up === '\\') || (up === '_') || (uprt === '_') ||
                            (!isVertex(grid(B)) &&
                                (isSolidHLine(uprt) || isSolidVLine(uprt)))) {

                            // Continue half a cell more to connect at:
                            //     __   __  ---     |
                            //    /      /   ^     ^|
                            //   /      /   /     / |

                            B.x += 0.5; B.y -= 0.5;
                        } else if (isPoint(uprt)) {

                            // Continue 1/4 cell more to connect at:
                            //
                            //       o
                            //      ^
                            //     /

                            B.x += 0.25; B.y -= 0.25;
                        } if (bottom === '/' && grid.isSolidBLineAt(B.x + 1, B.y)) {
                            // Cap a sharp vertex:
                            //   \  /   \  _
                            //    \/     \/
                            B.x += 0.5; B.y -= 0.5;
                        }

                        var dnlt = grid(A.x - 1, A.y + 1);
                        var top = grid(A);
                        if ((grid(A.x, A.y + 1) === '\\') || (grid(A.x - 1, A.y) === '_') || (grid(A.x + 1, A.y) === '_') ||
                            (!isVertex(grid(A)) &&
                                (isSolidHLine(dnlt) || isSolidVLine(dnlt)))) {

                            // Continue half a cell more to connect at:
                            //               /     \ |
                            //    /  /      v       v|
                            // __/  /__   ----       |

                            A.x -= 0.5; A.y += 0.5;
                        } else if (isPoint(dnlt)) {

                            // Continue 1/4 cell more to connect at:
                            //
                            //       /
                            //      v
                            //     o

                            A.x -= 0.25; A.y += 0.25;
                        } else if (top === '/' && grid.isSolidBLineAt(A.x - 1, A.y)) {
                            // Cap a sharp vertex:
                            //    \  /    _  /
                            //     \/      \/
                            A.x -= 0.5; A.y += 0.5;
                        }
                        pathSet.insert(new Path(A, B));

                        // Continue the search from the end x+1,y-1
                    } // lineContains
                }
            }
        } // y


        // Now look for curved corners. The syntax constraints require
        // that these can always be identified by looking at three
        // horizontally-adjacent characters.
        for (var y = 0; y < grid.height; ++y) {
            for (var x = 0; x < grid.width; ++x) {
                const CURVE = 0.551915024494; // https://spencermortensen.com/articles/bezier-circle/
                const CURVE_X = 2 * CURVE;
                const CURVE_Y = CURVE;
                var c = grid(x, y);

                // Note that because of undirected vertices, the
                // following cases are not exclusive
                if (isTopVertex(c)) {
                    // -.
                    //   |
                    if (isSolidHLine(grid(x - 1, y)) && isSolidVLine(grid(x + 1, y + 1))) {
                        grid.setUsed(x - 1, y); grid.setUsed(x, y); grid.setUsed(x + 1, y + 1);
                        pathSet.insert(new Path(Vec2(x - 1, y), Vec2(x + 1, y + 1),
                            Vec2(x - 1 + CURVE_X, y), Vec2(x + 1, y + 1 - CURVE_Y)));
                    }

                    //  .-
                    // |
                    if (isSolidHLine(grid(x + 1, y)) && isSolidVLine(grid(x - 1, y + 1))) {
                        grid.setUsed(x - 1, y + 1); grid.setUsed(x, y); grid.setUsed(x + 1, y);
                        pathSet.insert(new Path(Vec2(x + 1, y), Vec2(x - 1, y + 1),
                            Vec2(x + 1 - CURVE_X, y), Vec2(x - 1, y + 1 - CURVE_Y)));
                    }
                }

                // Special case patterns:
                //   .  .   .  .
                //  (  o     )  o
                //   '  .   '  '
                if (((c === ')') || isPoint(c)) && (grid(x - 1, y - 1) === '.') && (grid(x - 1, y + 1) === "\'")) {
                    grid.setUsed(x, y); grid.setUsed(x - 1, y - 1); grid.setUsed(x - 1, y + 1);
                    pathSet.insert(new Path(Vec2(x - 2, y - 1), Vec2(x - 2, y + 1),
                        Vec2(x + 0.6, y - 1), Vec2(x + 0.6, y + 1)));
                }

                if (((c === '(') || isPoint(c)) && (grid(x + 1, y - 1) === '.') && (grid(x + 1, y + 1) === "\'")) {
                    grid.setUsed(x, y); grid.setUsed(x + 1, y - 1); grid.setUsed(x + 1, y + 1);
                    pathSet.insert(new Path(Vec2(x + 2, y - 1), Vec2(x + 2, y + 1),
                        Vec2(x - 0.6, y - 1), Vec2(x - 0.6, y + 1)));
                }

                if (isBottomVertex(c)) {
                    //   |
                    // -'
                    if (isSolidHLine(grid(x - 1, y)) && isSolidVLine(grid(x + 1, y - 1))) {
                        grid.setUsed(x - 1, y); grid.setUsed(x, y); grid.setUsed(x + 1, y - 1);
                        pathSet.insert(new Path(Vec2(x - 1, y), Vec2(x + 1, y - 1),
                            Vec2(x - 1 + CURVE_X, y), Vec2(x + 1, y - 1 + CURVE_Y)));
                    }

                    // |
                    //  '-
                    if (isSolidHLine(grid(x + 1, y)) && isSolidVLine(grid(x - 1, y - 1))) {
                        grid.setUsed(x - 1, y - 1); grid.setUsed(x, y); grid.setUsed(x + 1, y);
                        pathSet.insert(new Path(Vec2(x + 1, y), Vec2(x - 1, y - 1),
                            Vec2(x + 1 - CURVE_X, y), Vec2(x - 1, y - 1 + CURVE_Y)));
                    }
                }

            } // for x
        } // for y

        // Find low horizontal lines marked with underscores. These
        // are so simple compared to the other cases that we process
        // them directly here without a helper function. Process these
        // from top to bottom and left to right so that we can read
        // them in a single sweep.
        //
        // Exclude the special case of double underscores going right
        // into an ASCII character, which could be a source code
        // identifier such as __FILE__ embedded in the diagram.
        for (var y = 0; y < grid.height; ++y) {
            for (var x = 0; x < grid.width - 2; ++x) {
                var lt = grid(x - 1, y);

                if ((grid(x, y) === '_') && (grid(x + 1, y) === '_') &&
                    (!isASCIILetter(grid(x + 2, y)) || (lt === '_')) &&
                    (!isASCIILetter(lt) || (grid(x + 2, y) === '_'))) {

                    var ltlt = grid(x - 2, y);
                    var A = Vec2(x - 0.5, y + 0.5);

                    if ((lt === '|') || (grid(x - 1, y + 1) === '|') ||
                        (lt === '.') || (grid(x - 1, y + 1) === "'")) {
                        // Extend to meet adjacent vertical
                        A.x -= 0.5;

                        // Very special case of overrunning into the side of a curve,
                        // needed for logic gate diagrams
                        if ((lt === '.') &&
                            ((ltlt === '-') ||
                                (ltlt === '.')) &&
                            (grid(x - 2, y + 1) === '(')) {
                            A.x -= 0.5;
                        }
                    } else if (lt === '/') {
                        A.x -= 1.0;
                    }

                    // Detect overrun of a tight double curve
                    if ((lt === '(') && (ltlt === '(') &&
                        (grid(x, y + 1) === "'") && (grid(x, y - 1) === '.')) {
                        A.x += 0.5;
                    }
                    lt = ltlt = undefined;

                    do { grid.setUsed(x, y); ++x; } while (grid(x, y) === '_');

                    var B = Vec2(x - 0.5, y + 0.5);
                    var c = grid(x, y);
                    var rt = grid(x + 1, y);
                    var dn = grid(x, y + 1);

                    if ((c === '|') || (dn === '|') || (c === '.') || (dn === "'")) {
                        // Extend to meet adjacent vertical
                        B.x += 0.5;

                        // Very special case of overrunning into the side of a curve,
                        // needed for logic gate diagrams
                        if ((c === '.') &&
                            ((rt === '-') || (rt === '.')) &&
                            (grid(x + 1, y + 1) === ')')) {
                            B.x += 0.5;
                        }
                    } else if ((c === '\\')) {
                        B.x += 1.0;
                    }

                    // Detect overrun of a tight double curve
                    if ((c === ')') && (rt === ')') && (grid(x - 1, y + 1) === "'") && (grid(x - 1, y - 1) === '.')) {
                        B.x += -0.5;
                    }

                    pathSet.insert(new Path(A, B));
                }
            } // for x
        } // for y
    } // findPaths


    function findDecorations(grid, pathSet, decorationSet) {
        function isEmptyOrVertex(c) { return (c === ' ') || /[^a-zA-Z0-9]|[ov]/.test(c); }

        /** Is the point in the center of these values on a line? Allow points that are vertically
            adjacent but not horizontally--they wouldn't fit anyway, and might be text. */
        function onLine(up, dn, lt, rt) {
            return ((isEmptyOrVertex(dn) || isPoint(dn)) &&
                (isEmptyOrVertex(up) || isPoint(up)) &&
                isEmptyOrVertex(rt) &&
                isEmptyOrVertex(lt));
        }

        for (var x = 0; x < grid.width; ++x) {
            for (var j = 0; j < grid.height; ++j) {
                var c = grid(x, j);
                var y = j;

                if (isJump(c)) {

                    // Ensure that this is really a jump and not a stray character
                    if (pathSet.downEndsAt(x, y - 0.5) &&
                        pathSet.upEndsAt(x, y + 0.5)) {
                        decorationSet.insert(x, y, c);
                        grid.setUsed(x, y);
                    }

                } else if (isPoint(c)) {
                    var up = grid(x, y - 1);
                    var dn = grid(x, y + 1);
                    var lt = grid(x - 1, y);
                    var rt = grid(x + 1, y);

                    if (pathSet.rightEndsAt(x - 1, y) ||   // Must be at the end of a line...
                        pathSet.leftEndsAt(x + 1, y) ||    // or completely isolated NSEW
                        pathSet.downEndsAt(x, y - 1) ||
                        pathSet.upEndsAt(x, y + 1) ||

                        pathSet.upEndsAt(x, y) ||    // For points on vertical lines
                        pathSet.downEndsAt(x, y) ||  // that are surrounded by other characters

                        onLine(up, dn, lt, rt)) {

                        decorationSet.insert(x, y, c);
                        grid.setUsed(x, y);
                    }
                } else if (isGray(c)) {
                    decorationSet.insert(x, y, c);
                    grid.setUsed(x, y);
                } else if (isTri(c)) {
                    decorationSet.insert(x, y, c);
                    grid.setUsed(x, y);
                } else { // Arrow heads

                    // If we find one, ensure that it is really an
                    // arrow head and not a stray character by looking
                    // for a connecting line.
                    const BACKOFF_X = 0.5;
                    const BACKOFF_Y = 0.5 / ASPECT;
                    var dx = 0;
                    var dy = 0;
                    if ((c === '>') && (pathSet.rightEndsAt(x, y) ||
                        pathSet.horizontalPassesThrough(x, y))) {
                        if (isPoint(grid(x + 1, y))) {
                            // Back up if connecting to a point so as to not
                            // overlap it
                            dx = -BACKOFF_X;
                        }
                        decorationSet.insert(x + dx, y, '>', 0);
                        grid.setUsed(x, y);
                    } else if ((c === '<') && (pathSet.leftEndsAt(x, y) ||
                        pathSet.horizontalPassesThrough(x, y))) {
                        if (isPoint(grid(x - 1, y))) {
                            // Back up if connecting to a point so as to not
                            // overlap it
                            dx = BACKOFF_X;
                        }
                        decorationSet.insert(x + dx, y, '>', 180);
                        grid.setUsed(x, y);
                    } else if (c === '^') {
                        // Because of the aspect ratio, we need to look
                        // in two slots for the end of the previous line
                        if (pathSet.upEndsAt(x, y - 0.5)) {
                            decorationSet.insert(x, y - 0.5, '>', 270);
                            grid.setUsed(x, y);
                        } else if (pathSet.upEndsAt(x, y)) {
                            decorationSet.insert(x, y, '>', 270);
                            grid.setUsed(x, y);
                        } else if (pathSet.diagonalUpEndsAt(x + 0.5, y - 0.5)) {
                            decorationSet.insert(x + 0.5, y - 0.5, '>', 270 + DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.diagonalUpEndsAt(x + 0.25, y - 0.25)) {
                            decorationSet.insert(x + 0.25, y - 0.25, '>', 270 + DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.diagonalUpEndsAt(x, y)) {
                            decorationSet.insert(x, y, '>', 270 + DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.backDiagonalUpEndsAt(x, y)) {
                            decorationSet.insert(x, y, c, 270 - DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.backDiagonalUpEndsAt(x - 0.5, y - 0.5)) {
                            decorationSet.insert(x - 0.5, y - 0.5, c, 270 - DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.backDiagonalUpEndsAt(x - 0.25, y - 0.25)) {
                            decorationSet.insert(x - 0.25, y - 0.25, c, 270 - DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.verticalPassesThrough(x, y)) {
                            // Only try this if all others failed
                            if (isPoint(grid(x, y - 1))) {
                                dy = BACKOFF_Y; // Back up if connecting to a point
                            }
                            decorationSet.insert(x, y - 0.5 + dy, '>', 270);
                            grid.setUsed(x, y);
                        }
                    } else if (c === 'v' || c === 'V') {
                        if (pathSet.downEndsAt(x, y + 0.5)) {
                            decorationSet.insert(x, y + 0.5, '>', 90);
                            grid.setUsed(x, y);
                        } else if (pathSet.downEndsAt(x, y)) {
                            decorationSet.insert(x, y, '>', 90);
                            grid.setUsed(x, y);
                        } else if (pathSet.diagonalDownEndsAt(x, y)) {
                            decorationSet.insert(x, y, '>', 90 + DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.diagonalDownEndsAt(x - 0.5, y + 0.5)) {
                            decorationSet.insert(x - 0.5, y + 0.5, '>', 90 + DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.diagonalDownEndsAt(x - 0.25, y + 0.25)) {
                            decorationSet.insert(x - 0.25, y + 0.25, '>', 90 + DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.backDiagonalDownEndsAt(x, y)) {
                            decorationSet.insert(x, y, '>', 90 - DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.backDiagonalDownEndsAt(x + 0.5, y + 0.5)) {
                            decorationSet.insert(x + 0.5, y + 0.5, '>', 90 - DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.backDiagonalDownEndsAt(x + 0.25, y + 0.25)) {
                            decorationSet.insert(x + 0.25, y + 0.25, '>', 90 - DIAGONAL_ANGLE);
                            grid.setUsed(x, y);
                        } else if (pathSet.verticalPassesThrough(x, y)) {
                            // Only try this if all others failed
                            if (isPoint(grid(x, y + 1))) {
                                dy = -BACKOFF_Y; // Back up if connecting to a point
                            }
                            decorationSet.insert(x, y + 0.5 + dy, '>', 90);
                            grid.setUsed(x, y);
                        }
                    } // arrow heads
                } // decoration type
            } // y
        } // x
    } // findArrowHeads

    // Cases where we want to redraw at graphical unicode character
    // to adjust its weight or shape for a conventional application
    // in constructing a diagram.
    function findReplacementCharacters(grid, pathSet) {
        for (var x = 0; x < grid.width; ++x) {
            for (var y = 0; y < grid.height; ++y) {
                if (grid.isUsed(x, y)) continue;
                var c = grid(x, y);
                switch (c) {
                    case '╱':
                        pathSet.insert(new Path(Vec2(x - 0.5, y + 0.5), Vec2(x + 0.5, y - 0.5)));
                        grid.setUsed(x, y);
                        break;
                    case '╲':
                        pathSet.insert(new Path(Vec2(x - 0.5, y - 0.5), Vec2(x + 0.5, y + 0.5)));
                        grid.setUsed(x, y);
                        break;
                }
            }
        }
    } // findReplacementCharacters

    var grid = makeGrid(diagramString);

    var pathSet = new PathSet();
    var decorationSet = new DecorationSet();

    findPaths(grid, pathSet);
    findReplacementCharacters(grid, pathSet);
    findDecorations(grid, pathSet, decorationSet);

    let width = ((options.width || grid.width) + 1) * SCALE;
    let height = ((options.height || grid.height) + 1) * SCALE * ASPECT;
    if ((options.width && options.width < grid.width) ||
        (options.height && options.height < grid.height)) {
        console.warn("warning: diagram overflows viewbox set by width/height option");
    }
    let attrs = options.style;
    attrs.xmlns = 'http://www.w3.org/2000/svg';
    attrs.version = '1.1';
    if (!options.fill) {
        attrs.height = height.toString();
        attrs.width = width.toString();
    }
    attrs.viewBox = '0 0 ' + width + ' ' + height;
    // These attributes can be overridden:
    const DEFAULT_ATTRS = {
        'class': 'diagram',
        'text-anchor': 'middle',
        'font-family': 'monospace',
        'font-size': (SCALE * 13 / 8).toString() + 'px',
    };
    Object.keys(DEFAULT_ATTRS).forEach(k => {
        if (!attrs[k]) { attrs[k] = DEFAULT_ATTRS[k]; }
    });
    let svg = '<svg ' + Object.keys(attrs)
        .filter(k => typeof attrs[k] === 'string')
        .map(k => k + '="' + escapeHTMLEntities(attrs[k]) + '"').join(' ') + '>\n';

    if (options.backdrop) {
        svg += '<rect class="backdrop" x="0" y="0" width="' + ((grid.width + 1) * SCALE)
            + '" height="' + ((grid.height + 1) * SCALE * ASPECT)
            + '" rx="3px" ry="3px" fill="white" opacity="0.9"/>\n';
    }
    if (options.grid) {
        svg += '<g class="grid" opacity="0.1">\n';
        for (var x = 0; x < grid.width; ++x) {
            for (var y = 0; y < grid.height; ++y) {
                svg += '<rect x="' + ((x + 0.5) * SCALE + 1) +
                    '" y="' + ((y + 0.5) * SCALE * ASPECT + 2) +
                    '" width="' + (SCALE - 2) + '" height="' + (SCALE * ASPECT - 2) +
                    '" fill="';
                if (grid.isUsed(x, y)) {
                    svg += 'red';
                } else if (grid(x, y) === ' ') {
                    svg += 'gray" opacity="0.05';
                } else {
                    svg += 'blue';
                }
                svg += '"/>\n';
            }
        }
        svg += '</g>\n';
    }

    svg += pathSet.toSVG();
    svg += decorationSet.toSVG();

    // Convert any remaining characters
    if (!options.disableText) {
        svg += '<g class="text">\n';
        // Enlarge hexagons so that they fill a grid
        for (var y = 0; y < grid.height; ++y) {
            for (var x = 0; x < grid.width; ++x) {
                var c = grid(x, y);
                if (/[\u2B22\u2B21]/.test(c)) {
                    svg += '<text x="' + ((x + 1) * SCALE) +
                        '" y="' + (4 + (y + 1) * SCALE * ASPECT) +
                        '"' + TEXT_COLOR + ' font-size="20.5px">' +
                        escapeHTMLEntities(c) + '</text>\n';
                    grid.setUsed(x, y);
                }
            } // x
        } // y
        for (var y = 0; y < grid.height; ++y) {
            let x = grid.textStart(0, y);
            while (x < grid.width) {
                let t = grid.text(x, y);
                let s = t.join('');
                svg += '<text x="' + ((x + (t.length / 2) + 0.5) * SCALE) +
                    '" y="' + (4 + (y + 1) * SCALE * ASPECT);
                if (options.spaces > 2 && s.indexOf('  ') >= 0) {
                    svg += '" xml:space="preserve'
                }
                if (options.stretch) {
                    svg += '" textLength="' + (t.length * SCALE) +
                        '" lengthAdjust="spacingAndGlyphs';
                }
                svg += '">' + escapeHTMLEntities(s) + '</text>\n';
                x = grid.textStart(x + t.length, y);
            }
        } // y
        svg += '</g>\n';
    }

    if (options.source) {
        svg += '<g class="source" fill="red" font-size="12px">\n';
        for (var y = 0; y < grid.height; ++y) {
            for (var x = 0; x < grid.width; ++x) {
                var c = grid(x, y);
                if (c !== ' ') {
                    // Offset the characters by 2 for easier viewing
                    svg += '<text x="' + ((x + 1) * SCALE + 2) +
                        '" y="' + (6 + (y + 1) * SCALE * ASPECT) +
                        '">' + escapeHTMLEntities(c) + '</text>';
                } // if
            } // x
        } // y
        svg += '</g>';
    } // if

    svg += '</svg>';

    return unhideMarkers(svg);
}

module.exports = { diagramToSVG };
