diff --git a/index.html b/index.html index 6ac8931..72cc602 100644 --- a/index.html +++ b/index.html @@ -2,8 +2,8 @@ - Langton's Ant Music v2 - + Langton's Ant Music v3-DEV + @@ -23,13 +23,35 @@ +
-
Langton's Ant Music version 2
-
+
Langton's Ant Music version 3-DEV
+
+
+
Edit  -   Formatting  -   Help  -   @@ -219,7 +241,8 @@

Ant Breeds - <breed> tag

command. In some cases the command needs an argument; this goes inside the tag.

Scroll to the bottom for the list of supported commands and species of ants.

Interpolations

-

Inside each <command>, the parameter can also include +

TODO: rewrite this for the new language

+

Actual Ants - <ant> tag

Ants are simpler and do not have any contents.

A full <ant> looks like this: @@ -541,7 +564,8 @@

Screenshot

Other issues?

If all else fails, or something appears to be broken, you can - report it on Github.

+ report it on Github. +

@@ -593,8 +617,11 @@

Keyboard Shortcuts

+ + + @@ -604,7 +631,8 @@

Keyboard Shortcuts

- + + - + \ No newline at end of file diff --git a/js/actionman.js b/js/actionman.js index 21526cb..14a35cc 100644 --- a/js/actionman.js +++ b/js/actionman.js @@ -1,32 +1,16 @@ -/** - * manager for Action callbacks - */ -class ActionManager { +class ActionManager extends XEventEmitter { constructor() { - /** - * @type {Object} - */ - this.listeners = {}; + super(); } - action(name, callback) { - if (!(name in this.listeners)) { - this.listeners[name] = [callback]; - } else { - this.listeners[name].push(callback); - } + action(name, callback) { + this.on("action." + name, callback); } - trigger(name, payload) { - var cbs = this.listeners[name]; - if (!cbs) { - console.warn("no callbacks for action " + name); - return; - } + + trigger(name, payload) { console.group(name); console.info('payload:', payload); try { - for (var callback of cbs) { - callback(payload); - } + this.emit("action." + name, payload); } finally { console.groupEnd(); diff --git a/js/actions.js b/js/actions.js index f5f8c50..d6a5b34 100644 --- a/js/actions.js +++ b/js/actions.js @@ -1,6 +1,3 @@ -/** - * Save the world state to `localStorage`. - */ function savelocal() { try { showStatus('Saving...'); @@ -12,9 +9,6 @@ function savelocal() { } } -/** - * Shares the world content useing `navigator.share`. - */ function share() { if (location.protocol.startsWith('file')) { showStatus('You must use the Web version to be able to share.', 'red'); @@ -35,10 +29,6 @@ function share() { } } -/** - * Copies the world content to the clipboard. - * @param {boolean} bbcode Whether the XML should be wrapped in `[code][/code]` tags. - */ function copy(bbcode) { if (location.protocol.startsWith('file')) { showStatus('You must use the Web version to be able to copy.', 'red'); @@ -60,9 +50,6 @@ function copy(bbcode) { } } -/** - * Reads the contents of the user's clipboard and lods it. - */ function openclip() { if (location.protocol.startsWith('file')) { showStatus('You must use the Web version to be able to open from clipboard.', 'red'); @@ -86,9 +73,6 @@ function openclip() { } } -/** - * Takes a screenshot of the canvas and downloads it. - */ function savescreenshot() { var a = document.createElement('a'); a.setAttribute('href', playfield.toDataURL()); diff --git a/js/ant.js b/js/ant.js index c6b4983..04db443 100644 --- a/js/ant.js +++ b/js/ant.js @@ -1,11 +1,3 @@ -/** - * Checks to make sure the node nesting is correct. - * @param {HTMLElement} node The XML node to be checked for validity. - * @param {string} name The name of the node that `node` should be. - * @param {string} inside The name of the elemnt that weaps this one, in case of a mismatch and an error. - * @returns {boolean} Whether the node is a `#text` node and should be skipped. - * @throws {string} When `node.nodeName !== name`. - */ function checknode(node, name, inside) { // returns true if it should be skipped. // returns false if it is ok to go. @@ -16,25 +8,13 @@ function checknode(node, name, inside) { return false; } -/** - * Manager class for creating ants. - */ class Breeder { constructor() { this.breeds = {}; } - /** - * Removes all the ant breeds. - */ empty() { this.breeds = {}; } - /** - * Registers a breed. - * @param {string} breedName The name of the breed. - * @param {function} klass The class constructor for the ant. - * @param {HTMLElement} cases The `` tags that describe the ant's behavior. - */ addBreed(breedName, klass, cases) { if (breedName in this.breeds) throw `Breed ${breedName} is already defined.`; var allCases = {}; @@ -59,25 +39,9 @@ class Breeder { } this.breeds[breedName] = [klass, allCases]; } - /** - * Serializes the stored breeds to XML. - * @returns {string} - */ dumpBreeds() { return Object.getOwnPropertyNames(this.breeds).map(breed => ` \n${Object.getOwnPropertyNames(this.breeds[breed][1]).map(p => [p, this.breeds[breed][1][p].map(sc => sc.map(cd => ` ${cd[1]}`).join('\n')).join('\n ')]).map(c => ` \n \n${c[1]}\n \n `).join('\n')}\n `).join('\n'); } - /** - * Creates a new ant. - * @param {string} breed Name of the breed - * @param {World} world - * @param {number} x - * @param {number} y - * @param {AntDirection} dir - * @param {number} state - * @param {Ant[]} antsList Reference to list of ants. - * @param {string} id Arbitrary ant ID. - * @returns - */ createAnt(breed, world, x, y, dir, state, antsList, id) { if (!(breed in this.breeds)) throw `Unknown ant breed ${breed}`; var klass = this.breeds[breed][0]; @@ -86,86 +50,23 @@ class Breeder { } } -/** - * @typedef {0|1|2|3} AntDirection - */ -/** - * Class for an Ant. - */ class Ant { - /** - * - * @param {Breeder} breeder Reverence to the `Breeder` that produced this ant. - * @param {Ant[]} antList A reference to the list of ants this is a member of. - * @param {string} breed The name of this breed. - * @param {World} world A reference to the `World` this ant lives in. - * @param {object} commands Serialized commands processed by the `breeder`. - * @param {number} initialState The state of the ant. - * @param {number} x X-position - * @param {number} y Y-position - * @param {AntDirection} dir Direction. - * @param {string} [id] The arbitrary ID of the ant. - */ constructor(breeder, antList, breed, world, commands, initialState, x, y, dir, id) { - /** - * @type {Breeder} - */ this.breeder = breeder; - /** - * @type {Ant[]} - */ this.antList = antList; - /** - * @type {string} - */ this.breed = breed; - /** - * @type {World} - */ this.world = world; - /** - * @type {number} - */ this.state = initialState; - /** - * @type {number} - */ this.x = x; - /** - * @type {number} - */ this.y = y; - /** - * @type {AntDirection} - */ this.dir = dir; - /** - * @type {object} - */ this.commands = commands; - /** - * @type {any[][]} - */ this.queue = []; - /** - * @type {boolean} - */ this.halted = false; - /** - * @type {boolean} - */ this.dead = false; - /** - * @type {string} - */ this.id = id || `${this.breed}-${randuuid()}`; } - /** - * Processes `#name` substitutions and `#exp;` interpolations for this ant. - * @param {string} arg Raw, unprocessed argument. - * @returns {string} Processed string. - */ processInserts(arg) { var vars = ['dir', 'state']; // Do simple inserts @@ -182,9 +83,6 @@ class Ant { arg = processExpressions(arg); return arg; } - /** - * Advances the ant one tick, executing the ``. - */ tick() { this.ensureQueueNotEmpty(); var commands = this.queue.shift(); @@ -193,9 +91,6 @@ class Ant { this[`do_${name}`](this.processInserts(arg || '')); } } - /** - * If the queue is not empty, fetches more commands from the world and rules. - */ ensureQueueNotEmpty() { if (this.queue.length === 0) { var what = this.commands[`${this.state}:${this.world.getCell(this.x, this.y)}`] ?? []; @@ -206,9 +101,6 @@ class Ant { this.queue.push([]); } } - /** - * Draws the ant on the context. - */ draw(ctx) { ctx.save(); ctx.translate(this.world.cellSize * this.x, this.world.cellSize * this.y); @@ -243,13 +135,6 @@ class Ant { ctx.beginPath(); ctx.arc(-1, -4.5, 0.5, 0, 2 * Math.PI); ctx.fill(); ctx.restore(); } - /** - * Checks the argument is a number, and returns it. - * @param {string} arg The argument to be checked. - * @param {string} methodname The method that requires a number argument. - * @param {number} default_ The default if the argument is the empty string. - * @returns {number} - */ numarg(arg, methodname, default_ = 1) { arg = arg || default_; var argNum = parseInt(arg); @@ -325,10 +210,6 @@ class Ant { } } -/** - * A random 8-character hexadecimal UUID. - * @returns {string} - */ function randuuid() { return Math.floor(Math.random() * (2 ** 32)).toString(16).padStart(8, '0'); } diff --git a/js/antsparser.js b/js/antsparser.js index 19846c6..b4aad46 100644 --- a/js/antsparser.js +++ b/js/antsparser.js @@ -1,24 +1,8 @@ -/** - * Gets the attribute off the element. - * @param {HTMLElement} node The element to be checked. - * @param {string} attr Name of the attribute required. - * @param {string} [fallback] Default value - * @param {boolean} [required=true] Whether the attribute is required. - * @returns {string|null} - */ function checkattr(node, attr, fallback = undefined, required = true) { if (required && (!node.hasAttribute(attr) || node.getAttribute(attr) === '')) throw `Need attribute ${attr} on <${node.nodeName.toLowerCase()}>`; return node.getAttribute(attr) ?? fallback; } -/** - * Gets the attribute off the element which must be an integer. - * @param {HTMLElement} node The node to be checked. - * @param {string} attr Name of the element. - * @param {number} fallback Default value if not provided - * @param {boolean} required Whether the attribute is required. - * @returns {number} - */ function checkint(node, attr, fallback = null, required = true) { var x = checkattr(node, attr, fallback, required); var xn = parseInt(x); @@ -26,9 +10,6 @@ function checkint(node, attr, fallback = null, required = true) { return xn ?? fallback; } -/** - * A custom error thrown to contain the extracted line/col information of the XML error. - */ class LM_XMLError extends SyntaxError { constructor(message, line, col) { super(message); @@ -38,12 +19,6 @@ class LM_XMLError extends SyntaxError { } } -/** - * Extract the error details from the element - * and returns the resultant error details. - * @param {XMLElement} err - * @returns {LM_XMLError} - */ function extractErrorDetails(err) { // See: https://stackoverflow.com/questions/11563554/how-do-i-detect-xml-parsing-errors-when-using-javascripts-domparser-in-a-cross var text = err.textContent; @@ -59,15 +34,6 @@ function extractErrorDetails(err) { return new LM_XMLError(message.trim(), line, col); } -/** - * Parses the world and creates the ants. - * @param {string} text Raw unparsed XML. - * @param {object} antSpecies The breeds of ants available. - * @param {world} world The world to load the ants into. - * @param {Breeder} breeder The breeder to register the breeds onto. - * @param {Ant[]} ants The list of ants. - * @returns {object} The header metadata. - */ function loadWorld(text, antSpecies, world, breeder, ants) { var xml = (new DOMParser()).parseFromString(text, 'application/xml'); for (var err of xml.querySelectorAll('parsererror')) { diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..e7fb420 --- /dev/null +++ b/js/app.js @@ -0,0 +1,11 @@ +/** + * screw the JSDOC comments + */ + + + +class LangtonMusicApp { + constructor(options) { + this.foo; + } +} diff --git a/js/canvastools.js b/js/canvastools.js index 66a2896..8ee6159 100644 --- a/js/canvastools.js +++ b/js/canvastools.js @@ -1,9 +1,3 @@ -/** - * Finds the mouse position from the event on the canvas. - * @param {HTMLCanvasElement} canvas The canvas - * @param {MouseEvent|TouchEvent} evt The mouse event to get the coordinates on. - * @returns {Vector} - */ function getMousePos(canvas, evt) { var rect = canvas.getBoundingClientRect(); var reportedXY; @@ -19,71 +13,23 @@ function getMousePos(canvas, evt) { }; } -/** - * Manager for tools to interact with the canvas. - */ class CanvasToolsManager { - /** - * @param {HTMLCanvasElement} canvas Canvas to control. - * @param {HTMLSelectElement} toolSelector Dropdown to add tools select options to. - * @param {HTMLElement} toolContainer Container element to add the tools' control panels to. - * @param {Tool[]} [tools] List of tools to choose between. The first is the default tool. - */ - constructor(canvas, toolSelector, toolContainer, tools = []) { - /** - * @type {HTMLCanvasElement} - */ - this.canvas = canvas; - /** - * @type {CanvasRenderingContext2D} - */ - this.ctx = canvas.getContext('2d'); - /** - * @type {number} - */ - this.zoom = 1; - /** - * @type {boolean} - */ - this.enabled = true; + constructor(canvas, toolSelector, toolContainer, tools = []) { + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); + this.zoom = 1; + this.enabled = true; if (toolSelector) { - /** - * @type {HTMLElement} - */ - this.toolContainer = toolContainer; - /** - * @type {HTMLSelectElement} - */ - this.toolSelector = toolSelector; + this.toolContainer = toolContainer; + this.toolSelector = toolSelector; - /** - * @type {boolean} - */ - this.mouseDown = false; - /** - * @type {number} - */ - this.timeDown = 0; - /** - * @type {Vector} - */ - this.panxy = { x: 0, y: 0 }; - /** - * @type {Vector} - */ - this.downxy = { x: 0, y: 0 }; - /** - * @type {Vector} - */ - this.lastxy = { x: 0, y: 0 }; - /** - * @type {Tool[]} - */ - this.tools = tools; - /** - * @type {number} - */ - this.activeToolIndex = 0; + this.mouseDown = false; + this.timeDown = 0; + this.panxy = { x: 0, y: 0 }; + this.downxy = { x: 0, y: 0 }; + this.lastxy = { x: 0, y: 0 }; + this.tools = tools; + this.activeToolIndex = 0; // attach event listeners canvas.addEventListener('mousedown', e => { this.mouseDown = true; @@ -162,61 +108,33 @@ class CanvasToolsManager { }); } } - /** - * Zooms the canvas by the specified factor at the center point (on the canvas coordinates). - * @param {number} factor - * @param {Vector} [center] - */ - zoomBy(factor, center) { + zoomBy(factor, center) { if (!center) center = vScale({ x: this.canvas.width, y: this.canvas.height }, 0.5); this.zoom *= factor; this.panxy = vPlus(vMinus(vScale(this.panxy, factor), vScale(center, factor)), center); } - /** - * Pans the canvas by the specified offset. - * @param {Vector} xy - */ - panBy(xy) { + panBy(xy) { this.panxy = vPlus(this.panxy, xy); } - /** - * Saves the current canvas state and translates by x and y and zooms. - */ - enter() { + enter() { this.ctx.save(); this.ctx.setTransform(this.zoom, 0, 0, this.zoom, this.panxy.x, this.panxy.y); } - /** - * Converse of `enter()` it restores the old canvas state. - */ - exit() { + exit() { this.ctx.restore(); } - /** - * Draws the function within an `enter()` / `exit()` pair. - * @param {Function} fun Draw function - */ - drawTransformed(fun) { + drawTransformed(fun) { this.enter(); fun(); this.exit(); } - /** - * Erases the canvas. - */ - clear() { + clear() { this.ctx.save(); this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restore(); } - /** - * Fires the event to the active tool, and if the tool didn't (or couldn't) handle it, defaults to tool #0. - * @param {Event} e event - * @param {string} name Function name - * @param {Vector} point The detail point for the event. - */ - event(e, name, point) { + event(e, name, point) { if (!this.enabled || !this.toolSelector) return; var tool = this.tools[this.activeToolIndex]; var fun = tool[name]; @@ -228,19 +146,10 @@ class CanvasToolsManager { } if (!unhandled) e.preventDefault(); } - /** - * Applies the current transformation to the point to yield the real X/Y. - * @param {Vector} pt Raw point - * @returns {Vector} Trasformed point - */ - transformMousePoint(pt) { + transformMousePoint(pt) { return vScale(vMinus(pt, this.panxy), 1/this.zoom); } - /** - * Switches the currently active tool. - * @param {number} toolIndex The index of the tool. - */ - changeTool(toolIndex) { + changeTool(toolIndex) { if (toolIndex === this.activeToolIndex) return; this.tools[this.activeToolIndex].deactivate(); this.tools[toolIndex].activate(this.toolContainer); @@ -258,11 +167,6 @@ const K_ALT = 0b0000000100000; const K_CTRL = 0b000001000000; const K_META = 0b000010000000; const K_SHIFT = 0b00100000000; -/** - * Turns the event into a bit field that stores mouse buttons and modifier keys (alt, ctrl, etc.) - * @param {UIEvent} e - * @returns {number} - */ function makeModifiers(e) { var out = e.buttons || 0; if (e.altKey) out |= K_ALT; @@ -272,114 +176,31 @@ function makeModifiers(e) { return out; } -/** - * Base class for a tool. - */ class Tool { static displayName = "Nothing"; constructor() { - /** - * @type {HTMLElement} - */ - this.element = document.createElement('span'); + this.element = document.createElement('span'); this.element.classList.add('flex-row'); } - /** - * Handle mouse down events. - * @abstract - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @param {number} mod - * @param {MouseEvent} e - */ - onMouseDown(tm, xy, mod, e) { return true; } - /** - * Handle mouse up events. - * @abstract - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @param {number} mod - * @param {MouseEvent} e - */ - onMouseUp(tm, xy, mod, e) { return true; } - /** - * Handle mouse move events when not clicking. - * @abstract - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @param {number} mod - * @param {MouseEvent} e - */ - onMouseOver(tm, xy, mod, e) { return true; } - /** - * Handle mouse click. - * @abstract - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @param {number} mod - * @param {MouseEvent} e - */ - onClick(tm, xy, mod, e) { return true; } - /** - * Handle mouse drag events. - * @abstract - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @param {number} mod - * @param {MouseEvent} e - */ - onDrag(tm, xy, mod, e) { return true; } - /** - * Handle mouse scroll events. - * @abstract - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @param {number} mod - * @param {WheelEvent} e - */ - onScroll(tm, xy, mod, e) { return true; } - /** - * Handle keypress events. - * @abstract - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @param {number} mod - * @param {KeyboardEvent} e - */ - onKey(tm, xy, mod, e) { return true; } - /** - * Handle key release events. - * @abstract - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @param {number} mod - * @param {KeyboardEvent} e - */ - onKeyUp(tm, xy, mod, e) { return true; } - /** - * Callback to set up this tool's functionality when it is selected. - * @param {HTMLElement} container The container to append to. - */ - activate(container) { + onMouseDown(tm, xy, mod, e) { return true; } + onMouseUp(tm, xy, mod, e) { return true; } + onMouseOver(tm, xy, mod, e) { return true; } + onClick(tm, xy, mod, e) { return true; } + onDrag(tm, xy, mod, e) { return true; } + onScroll(tm, xy, mod, e) { return true; } + onKey(tm, xy, mod, e) { return true; } + onKeyUp(tm, xy, mod, e) { return true; } + activate(container) { container.appendChild(this.element); } - /** - * Callback to clean this tool's functionality when it is deselected. - */ - deactivate() { + deactivate() { this.element.remove(); } } -/** - * Drag, pan, nd zoom tool. - */ class DragTool extends Tool { static displayName = "Drag"; - /** - * @param {number} zoomFactor Factor to change zoom by when scrolling. - */ - constructor(zoomFactor = 1.01) { + constructor(zoomFactor = 1.01) { super(); this.zoomFactor = zoomFactor; } @@ -392,44 +213,23 @@ class DragTool extends Tool { } } -/** - * Base class for tools that edit the world. - */ class WorldEditTool extends Tool { - /** - * @param {World} world - */ - constructor(world) { + constructor(world) { super(); this.world = world; } - /** - * Turns the vector into cell coordinates. - * @param {CanvasToolsManager} tm - * @param {Vector} xy - * @returns {Vector} - */ - toCellCoords(tm, xy) { + toCellCoords(tm, xy) { return vApply(Math.round, vScale(tm.transformMousePoint(xy), 1 / this.world.cellSize)); } } -/** - * Tool to draw cells into the world. - */ class DrawCellsTool extends WorldEditTool { static displayName = "Draw Cells"; constructor(world) { super(world); this.element.innerHTML = ''; - /** - * @type {HTMLInputElement} - */ - this.input = this.element.querySelector('input'); - /** - * @type {boolean} - */ - this.isErasing = false; + this.input = this.element.querySelector('input'); + this.isErasing = false; } onClick(tm, xy, mod) { var c = this.toCellCoords(tm, xy); @@ -449,39 +249,16 @@ class DrawCellsTool extends WorldEditTool { } } -/** - * Tool to draw ants into the world. - */ class DrawAntsTool extends WorldEditTool { static displayName = "Draw Ants"; - /** - * @param {World} world - * @param {Breeder} breeder - * @param {Ant[]} antsList - */ - constructor(world, breeder, antsList) { + constructor(world, breeder, antsList) { super(world); - /** - * @type {Breeder} - */ - this.breeder = breeder; - /** - * @type {Ant[]} - */ - this.antsList = antsList; + this.breeder = breeder; + this.antsList = antsList; this.element.innerHTML = ' '; - /** - * @type {HTMLSelectElement} - */ - this.breedSelect = this.element.querySelector('.bsel'); - /** - * @type {HTMLInputElement} - */ - this.stateSelect = this.element.querySelector('input'); - /** - * @type {HTMLSelectElement} - */ - this.direcSelect = this.element.querySelector('.dirsel'); + this.breedSelect = this.element.querySelector('.bsel'); + this.stateSelect = this.element.querySelector('input'); + this.direcSelect = this.element.querySelector('.dirsel'); // do some monkey patching var oldBreederEmpty = breeder.empty.bind(breeder); var oldBreederAddBreed = breeder.addBreed.bind(breeder); @@ -494,10 +271,7 @@ class DrawAntsTool extends WorldEditTool { this.updateBreedSelector(); }; } - /** - * Mirrors this breeder's breeds to the select element. - */ - updateBreedSelector() { + updateBreedSelector() { var sb = this.breedSelect.value; var breedNames = Object.getOwnPropertyNames(this.breeder.breeds); this.breedSelect.childNodes.forEach(node => { diff --git a/js/dialog.js b/js/dialog.js new file mode 100644 index 0000000..6790b31 --- /dev/null +++ b/js/dialog.js @@ -0,0 +1,67 @@ +class Dialog extends XEventEmitter { + constructor(elem, closeButtonMessage = "close") { + super(); + if (!elem) { + elem = document.createElement("dialog"); + elem.classList.add("big"); + document.body.append(elem); + } + this.elem = elem; + this.elem.addEventListener("close", () => this.emit("close", this.elem.returnValue)); + this.elem.addEventListener("keydown", e => { if (e.key == "Escape") e.preventDefault(); }); + // move elements to span + this.inside = document.createElement("div"); + if (this.elem.childNodes.length > 0 && ![].some.call(this.elem.childNodes, e => ["#comment", "#text"].indexOf(e.nodeName) == -1)) { + // Only a text/comment node! + if ('marked' in window) { + try { + this.inside.innerHTML = marked.parse(dedent(this.elem.textContent)); + } catch (e) { + this.inside.innerHTML = `
Markdown Parse Error:\n${e.stack}
${content}`; + } + } + this.elem.childNodes.forEach(e => e.remove()); + } else { + this.inside.append(...this.elem.childNodes); + } + this.elem.append(this.inside); + if (closeButtonMessage) { + var form = document.createElement("form"); + form.method = "dialog"; + var button = document.createElement("input"); + button.type = "submit"; + button.value = closeButtonMessage; + form.append(button); + this.elem.append(form); + } + } + show() { + if (!this.open) { + this.elem.inert = true; + this.elem.showModal(); + this.elem.inert = false; + } + } + close() { + if (this.open) this.elem.close(); + } + setContent(content, parseMD = true) { + if (typeof content == "string") { + if (parseMD && 'marked' in window) { + try { + content = marked.parse(dedent(content)); + } catch (e) { + content = `
Markdown Parse Error:\n${e.stack}
${content}`; + } + } + this.inside.innerHTML = content; + } + else { + this.inside.innerHTML = ""; + this.inside.append(content); + } + } + get open() { + return this.elem.open; + } +} diff --git a/js/enhancedevents.js b/js/enhancedevents.js new file mode 100644 index 0000000..b69948f --- /dev/null +++ b/js/enhancedevents.js @@ -0,0 +1,15 @@ +class XEventEmitter extends EventTarget { + constructor() { super(); } + + on(event, handler) { this.addEventListener(event, handler); } + + off(event, handler) { this.removeEventListener(event, handler); } + + once(event, handler) { this.addEventListener(event, handler, { once: true }); } + + emit(event, detail) { this.dispatchEvent(new CustomEvent(event, { detail })); } + + pipeTo(selfEvent, otherObj, otherEvent) { this.on(selfEvent, e => otherObj.emit(otherEvent || selfEvent, e.detail)); } + + waitFor(event) { return new Promise(r => this.once(event, e => r(e.detail))); } +}; diff --git a/js/interpol.js b/js/interpol.js index da0fbde..808a34e 100644 --- a/js/interpol.js +++ b/js/interpol.js @@ -1,97 +1,225 @@ -/** - * Processes all the interpolations. - * @param {string} expr - * @returns {string} - */ -function processExpressions(expr) { - // find expressions - while (true) { - expr = expr.trim(); - var match = /#(.+?);/.exec(expr); - if (!match) break; - expr = expr.replaceAll(match[0], evalExpression(match[1])); - } - return expr; -} +// Eh, good enough -/** - * Evaluates the expression. - * @param {string} expr The stripped expression. - * @returns {string|number} - */ -function evalExpression(expr) { - var s = []; - while (expr) { - var match = /^(\d+|`(.+?)`|.)/.exec(expr); - var token = match[0]; - expr = expr.slice(token.length); - if (/\d+/.test(token)) { - s.push(parseInt(token)); +function* level1ParseExpression(string, delimiters, singletons = [], openers = "[{(", closers = "]})", stringers = "\"'", requireOpener = "") { + var origString = string; + var currentString = ""; + var stack = []; + var otc = [].reduce.call(openers, (obj, key, index) => ({ ...obj, [key]: closers[index] }), {}); + delimiters = delimiters.sort((a, b) => b.length - a.length); + singletons = singletons.sort((a, b) => b.length - a.length); + var i = 0; + mainloop: + while (string) { + for (var o of openers) { + if (string.startsWith(o) && (!requireOpener || stack.some(x => x[0] == requireOpener) || o == requireOpener)) { + if (stack.length > 0 && otc[o] === o && otc[o] === stack[stack.length - 1][0]) { + stack.pop(); + if (stack.length == 0) { + yield currentString + o; + currentString = ""; + } else currentString += o; + } else { + if (stack.length == 0 && currentString.length > 0) { + yield currentString; + currentString = o; + } else currentString += o; + stack.push([o, i]); + } + string = string.slice(o.length); + i += o.length; + continue mainloop; + } + } + for (var s of stringers) { + if (string.startsWith(s) && (!requireOpener || stack.some(x => x[0] == requireOpener) || s == requireOpener)) { + if (stack.length > 0 && stack[stack.length - 1][0] == s) { + stack.pop(); + if (stack.length == 0) { + yield currentString + s; + currentString = ""; + } else currentString += s; + } else { + if (stack.length == 0 && currentString.length > 0) { + yield currentString; + currentString = s; + } else currentString += s; + stack.push([s, i]); + } + string = string.slice(s.length); + i += s.length; + continue mainloop; + } } - else if (match[2]) { - s.push(match[2]); + if (stack.length > 0 && stringers.indexOf(stack[stack.length - 1][0]) != -1) { + currentString += string[0]; + i++; + string = string.slice(1); + continue mainloop; } - else if (/[\s']/.test(token)); // spaces and ' are noop - else { - var a = s.pop(); - var b = s.pop(); - switch (token) { - case '\\': - s.push(a, b); - break; - case '$': - s.push(b); - break; - case ':': - s.push(b, a, a); - break; - case '?': - s.push(b, Math.floor(Math.random() * a)); - break; - case '%': - s.push(b % a); - break; - case '^': - s.push(b ^ a); - break; - case '&': - s.push(b & a); - break; - case '*': - s.push(b * a); - break; - case '-': - s.push(b - a); - break; - case '+': - s.push(b + a); - break; - case '/': - s.push(b / a); - break; - case '|': - s.push(b | a); - break; - case '~': - s.push(b, -a); - break; - case '<': - s.push(b < a); - break; - case '>': - s.push(b > a); - break; - case '=': - s.push(b === a); - break; - case '@': - var c = s.pop(); - s.push(a ? b : c); - break; - default: - throw `unknown expression command ${token} starting at ${token}${expr}`; + if (!requireOpener || stack.some(x => x[0] == requireOpener)) { + for (var c of closers) { + if (string.startsWith(c)) { + if (stack.length === 0) throw `unopened ${c}\n${origString}\n${" ".repeat(i)}^`; + var b = otc[stack.pop()[0]]; + if (b !== c) throw `paren mismatch: expected '${b}', got '${c}'\n${origString}\n${" ".repeat(i)}^`; + if (stack.length == 0 && currentString.length > 0) { + yield currentString + c; + currentString = ""; + } else currentString += c; + string = string.slice(c.length); + i += c.length; + continue mainloop; + } } } + if (stack.length == 0) { + for (var d of delimiters) { + if (string.startsWith(d)) { + if (currentString) yield currentString; + currentString = ""; + string = string.slice(d.length); + i += d.length; + continue mainloop; + } + } + for (var s of singletons) { + if (string.startsWith(s)) { + yield currentString; + yield s; + currentString = ""; + string = string.slice(s.length); + i += s.length; + continue mainloop; + } + } + } + currentString += string[0]; + string = string.slice(1); + i++; + } + if (stack.length > 0) { + var [lastC, lastI] = stack[stack.length - 1]; + if (stringers.indexOf(lastC) == -1) throw `unclosed ${lastC}\n${origString}\n${" ".repeat(lastI)}^`; + throw `unclosed string\n${origString}\n${" ".repeat(lastI)}^`; } - return s[s.length - 1]; + yield currentString; +} + +function processExpressions(expr, vars) { + var bits = [...level1ParseExpression(expr, [], [], ["[", "{", "$(", "("], "]}))", "\"'", "$(")]; + bits = bits.map(bit => { + var m = /^\$\((.+)\)$/.exec(bit); + if (!m) return bit; + return evalExpression(m[1], vars).join(" "); + }); + return bits.join(""); +} + +var temp; +const operators = [ + // Unary + { + $: (left, right, vars) => [left, (right in vars ? vars[right] : (() => { throw "no var " + right; })())], + }, + { + ["."]: (left, right) => [left[right]], + }, + { + ["!"]: (temp = (left, right) => [left, !right]), + not: temp, + ["@"]: (left, right) => [left].concat(right), + ["#"]: (left, right) => [left, right.length], + }, + // Math + { + ["**"]: (left, right) => [left ** right], + }, + { + ["*"]: (left, right) => [typeof left === "string" ? left.repeat(right) : (left * right)], + ["/"]: (left, right) => [left / right], + ["%"]: (left, right) => [left % right], + }, + { + ["+"]: (left, right) => [left + right], + ["-"]: (left, right) => [left - right], + }, + // Bitwise + { + ["&"]: (left, right) => [left & right], + ["|"]: (left, right) => [left | right], + ["^"]: (left, right) => [left ^ right], + ["~"]: (left, right) => [left, ~right], + ["<<"]: (left, right) => [left << right], + [">>"]: (left, right) => [left >> right], + }, + // Comparison + { + ["<"]: (left, right) => [left < right], + [">"]: (left, right) => [left > right], + ["<="]: (left, right) => [left <= right], + [">="]: (left, right) => [left >= right], + ["=="]: (left, right) => [left == right], + ["!="]: (left, right) => [left != right], + }, + // Boolean + { + ["&&"]: (temp = (left, right) => [left && right]), + and: temp, + ["||"]: (temp = (left, right) => [left || right]), + or: temp, + }, + // Containment + { + in: (left, right) => [Array.isArray(left) || typeof left === "string" ? left.includes(right) : (() => { throw left + ": not a container" })()], + }, + // Ifelse 1 + { + if: (left, right) => [right ? { true: left } : {}], + }, + // Ifelse 2 + { + else: (left, right) => ["true" in left ? left.true : right], + }, + // Misc + { + ["??"]: (left, right) => [left + (0 | (Math.random() * (right - left)))], + }, +]; +delete temp; + +function evalExpression(string, vars) { + var ss = string.trim(); + var tokens = [...level1ParseExpression(ss, [" "], operators.flatMap(Object.keys))].filter(Boolean); + for (var i = 0; i < tokens.length; i++) { + var t = tokens[i]; + if (typeof t !== "string") continue; + if (t[0] === "(") tokens.splice(i, 1, ...evalExpression(t.slice(1, t.length - 1))); + else if ("{'\"".includes(t[0])) tokens[i] = t.slice(1, t.length - 1); + if (typeof tokens[i] === "string" && !isNaN(+tokens[i])) tokens[i] = parseInt(tokens[i]) || parseFloat(tokens[i]) || +tokens[i] || 0; + } + var hasOps; + do { + hasOps = false; + tokenLoop: + for (var precedenceLevel of operators) { + for (var opName of Object.keys(precedenceLevel)) { + tokens.unshift(undefined); + tokens.push(undefined); + var i = tokens.indexOf(opName); + try { + if (i !== -1) { + var val = precedenceLevel[opName](tokens[i - 1], tokens[i + 1], vars); + tokens.splice(i - 1, 3, ...[].concat(val)); + hasOps = true; + break tokenLoop; + } + } finally { + if (typeof tokens.shift() !== "undefined") throw "postfix at beginning"; + if (typeof tokens.pop() !== "undefined") throw "prefix at end"; + } + } + } + } while (hasOps); + // must return an array because this function is called recursively + return tokens; } diff --git a/js/lib/marked.js b/js/lib/marked.js new file mode 100644 index 0000000..4398664 --- /dev/null +++ b/js/lib/marked.js @@ -0,0 +1,2408 @@ +/** + * marked v9.0.0 - a markdown parser + * Copyright (c) 2011-2023, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.marked = {})); +})(this, (function (exports) { 'use strict'; + + /** + * Gets the original marked default options. + */ + function _getDefaults() { + return { + async: false, + breaks: false, + extensions: null, + gfm: true, + hooks: null, + pedantic: false, + renderer: null, + silent: false, + tokenizer: null, + walkTokens: null + }; + } + exports.defaults = _getDefaults(); + function changeDefaults(newDefaults) { + exports.defaults = newDefaults; + } + + /** + * Helpers + */ + const escapeTest = /[&<>"']/; + const escapeReplace = new RegExp(escapeTest.source, 'g'); + const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/; + const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g'); + const escapeReplacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + const getEscapeReplacement = (ch) => escapeReplacements[ch]; + function escape(html, encode) { + if (encode) { + if (escapeTest.test(html)) { + return html.replace(escapeReplace, getEscapeReplacement); + } + } + else { + if (escapeTestNoEncode.test(html)) { + return html.replace(escapeReplaceNoEncode, getEscapeReplacement); + } + } + return html; + } + const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; + function unescape(html) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(unescapeTest, (_, n) => { + n = n.toLowerCase(); + if (n === 'colon') + return ':'; + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); + } + const caret = /(^|[^\[])\^/g; + function edit(regex, opt) { + regex = typeof regex === 'string' ? regex : regex.source; + opt = opt || ''; + const obj = { + replace: (name, val) => { + val = typeof val === 'object' && 'source' in val ? val.source : val; + val = val.replace(caret, '$1'); + regex = regex.replace(name, val); + return obj; + }, + getRegex: () => { + return new RegExp(regex, opt); + } + }; + return obj; + } + function cleanUrl(href) { + try { + href = encodeURI(href).replace(/%25/g, '%'); + } + catch (e) { + return null; + } + return href; + } + const noopTest = { exec: () => null }; + function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(/\|/g, (match, offset, str) => { + let escaped = false; + let curr = offset; + while (--curr >= 0 && str[curr] === '\\') + escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } + else { + // add space before unescaped | + return ' |'; + } + }), cells = row.split(/ \|/); + let i = 0; + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if (!cells[0].trim()) { + cells.shift(); + } + if (cells.length > 0 && !cells[cells.length - 1].trim()) { + cells.pop(); + } + if (count) { + if (cells.length > count) { + cells.splice(count); + } + else { + while (cells.length < count) + cells.push(''); + } + } + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(/\\\|/g, '|'); + } + return cells; + } + /** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param str + * @param c + * @param invert Remove suffix of non-c chars instead. Default falsey. + */ + function rtrim(str, c, invert) { + const l = str.length; + if (l === 0) { + return ''; + } + // Length of suffix matching the invert condition. + let suffLen = 0; + // Step left until we fail to match the invert condition. + while (suffLen < l) { + const currChar = str.charAt(l - suffLen - 1); + if (currChar === c && !invert) { + suffLen++; + } + else if (currChar !== c && invert) { + suffLen++; + } + else { + break; + } + } + return str.slice(0, l - suffLen); + } + function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + let level = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } + else if (str[i] === b[0]) { + level++; + } + else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; + } + + function outputLink(cap, link, raw, lexer) { + const href = link.href; + const title = link.title ? escape(link.title) : null; + const text = cap[1].replace(/\\([\[\]])/g, '$1'); + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + const token = { + type: 'link', + raw, + href, + title, + text, + tokens: lexer.inlineTokens(text) + }; + lexer.state.inLink = false; + return token; + } + return { + type: 'image', + raw, + href, + title, + text: escape(text) + }; + } + function indentCodeCompensation(raw, text) { + const matchIndentToCode = raw.match(/^(\s+)(?:```)/); + if (matchIndentToCode === null) { + return text; + } + const indentToCode = matchIndentToCode[1]; + return text + .split('\n') + .map(node => { + const matchIndentInNode = node.match(/^\s+/); + if (matchIndentInNode === null) { + return node; + } + const [indentInNode] = matchIndentInNode; + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + return node; + }) + .join('\n'); + } + /** + * Tokenizer + */ + class _Tokenizer { + options; + // TODO: Fix this rules type + rules; + lexer; + constructor(options) { + this.options = options || exports.defaults; + } + space(src) { + const cap = this.rules.block.newline.exec(src); + if (cap && cap[0].length > 0) { + return { + type: 'space', + raw: cap[0] + }; + } + } + code(src) { + const cap = this.rules.block.code.exec(src); + if (cap) { + const text = cap[0].replace(/^ {1,4}/gm, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic + ? rtrim(text, '\n') + : text + }; + } + } + fences(src) { + const cap = this.rules.block.fences.exec(src); + if (cap) { + const raw = cap[0]; + const text = indentCodeCompensation(raw, cap[3] || ''); + return { + type: 'code', + raw, + lang: cap[2] ? cap[2].trim().replace(this.rules.inline._escapes, '$1') : cap[2], + text + }; + } + } + heading(src) { + const cap = this.rules.block.heading.exec(src); + if (cap) { + let text = cap[2].trim(); + // remove trailing #s + if (/#$/.test(text)) { + const trimmed = rtrim(text, '#'); + if (this.options.pedantic) { + text = trimmed.trim(); + } + else if (!trimmed || / $/.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text, + tokens: this.lexer.inline(text) + }; + } + } + hr(src) { + const cap = this.rules.block.hr.exec(src); + if (cap) { + return { + type: 'hr', + raw: cap[0] + }; + } + } + blockquote(src) { + const cap = this.rules.block.blockquote.exec(src); + if (cap) { + const text = cap[0].replace(/^ *>[ \t]?/gm, ''); + const top = this.lexer.state.top; + this.lexer.state.top = true; + const tokens = this.lexer.blockTokens(text); + this.lexer.state.top = top; + return { + type: 'blockquote', + raw: cap[0], + tokens, + text + }; + } + } + list(src) { + let cap = this.rules.block.list.exec(src); + if (cap) { + let bull = cap[1].trim(); + const isordered = bull.length > 1; + const list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [] + }; + bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } + // Get next list item + const itemRegex = new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`); + let raw = ''; + let itemContents = ''; + let endsWithBlankLine = false; + // Check if current bullet point can start a new List Item + while (src) { + let endEarly = false; + if (!(cap = itemRegex.exec(src))) { + break; + } + if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) + break; + } + raw = cap[0]; + src = src.substring(raw.length); + let line = cap[2].split('\n', 1)[0].replace(/^\t+/, (t) => ' '.repeat(3 * t.length)); + let nextLine = src.split('\n', 1)[0]; + let indent = 0; + if (this.options.pedantic) { + indent = 2; + itemContents = line.trimStart(); + } + else { + indent = cap[2].search(/[^ ]/); // Find first non-space char + indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent + itemContents = line.slice(indent); + indent += cap[1].length; + } + let blankLine = false; + if (!line && /^ *$/.test(nextLine)) { // Items begin with at most one blank line + raw += nextLine + '\n'; + src = src.substring(nextLine.length + 1); + endEarly = true; + } + if (!endEarly) { + const nextBulletRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`); + const hrRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`); + const fencesBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`); + const headingBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`); + // Check if following lines should be included in List Item + while (src) { + const rawLine = src.split('\n', 1)[0]; + nextLine = rawLine; + // Re-align to follow commonmark nesting rules + if (this.options.pedantic) { + nextLine = nextLine.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' '); + } + // End list item if found code fences + if (fencesBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new heading + if (headingBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new bullet + if (nextBulletRegex.test(nextLine)) { + break; + } + // Horizontal rule found + if (hrRegex.test(src)) { + break; + } + if (nextLine.search(/[^ ]/) >= indent || !nextLine.trim()) { // Dedent if possible + itemContents += '\n' + nextLine.slice(indent); + } + else { + // not enough indentation + if (blankLine) { + break; + } + // paragraph continuation unless last line was a different block level element + if (line.search(/[^ ]/) >= 4) { // indented code block + break; + } + if (fencesBeginRegex.test(line)) { + break; + } + if (headingBeginRegex.test(line)) { + break; + } + if (hrRegex.test(line)) { + break; + } + itemContents += '\n' + nextLine; + } + if (!blankLine && !nextLine.trim()) { // Check if current line is blank + blankLine = true; + } + raw += rawLine + '\n'; + src = src.substring(rawLine.length + 1); + line = nextLine.slice(indent); + } + } + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } + else if (/\n *\n *$/.test(raw)) { + endsWithBlankLine = true; + } + } + let istask = null; + let ischecked; + // Check for task list items + if (this.options.gfm) { + istask = /^\[[ xX]\] /.exec(itemContents); + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(/^\[[ xX]\] +/, ''); + } + } + list.items.push({ + type: 'list_item', + raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents, + tokens: [] + }); + list.raw += raw; + } + // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + list.items[list.items.length - 1].raw = raw.trimEnd(); + list.items[list.items.length - 1].text = itemContents.trimEnd(); + list.raw = list.raw.trimEnd(); + // Item child tokens handled here at end because we needed to have the final item to trim it first + for (let i = 0; i < list.items.length; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + if (!list.loose) { + // Check if list should be loose + const spacers = list.items[i].tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => /\n.*\n/.test(t.raw)); + list.loose = hasMultipleLineBreaks; + } + } + // Set all items to loose if list is loose + if (list.loose) { + for (let i = 0; i < list.items.length; i++) { + list.items[i].loose = true; + } + } + return list; + } + } + html(src) { + const cap = this.rules.block.html.exec(src); + if (cap) { + const token = { + type: 'html', + block: true, + raw: cap[0], + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', + text: cap[0] + }; + return token; + } + } + def(src) { + const cap = this.rules.block.def.exec(src); + if (cap) { + const tag = cap[1].toLowerCase().replace(/\s+/g, ' '); + const href = cap[2] ? cap[2].replace(/^<(.*)>$/, '$1').replace(this.rules.inline._escapes, '$1') : ''; + const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline._escapes, '$1') : cap[3]; + return { + type: 'def', + tag, + raw: cap[0], + href, + title + }; + } + } + table(src) { + const cap = this.rules.block.table.exec(src); + if (cap) { + const item = { + type: 'table', + raw: cap[0], + header: splitCells(cap[1]).map(c => { + return { text: c, tokens: [] }; + }), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + rows: cap[3] && cap[3].trim() ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : [] + }; + if (item.header.length === item.align.length) { + let l = item.align.length; + let i, j, k, row; + for (i = 0; i < l; i++) { + const align = item.align[i]; + if (align) { + if (/^ *-+: *$/.test(align)) { + item.align[i] = 'right'; + } + else if (/^ *:-+: *$/.test(align)) { + item.align[i] = 'center'; + } + else if (/^ *:-+ *$/.test(align)) { + item.align[i] = 'left'; + } + else { + item.align[i] = null; + } + } + } + l = item.rows.length; + for (i = 0; i < l; i++) { + item.rows[i] = splitCells(item.rows[i], item.header.length).map(c => { + return { text: c, tokens: [] }; + }); + } + // parse child tokens inside headers and cells + // header child tokens + l = item.header.length; + for (j = 0; j < l; j++) { + item.header[j].tokens = this.lexer.inline(item.header[j].text); + } + // cell child tokens + l = item.rows.length; + for (j = 0; j < l; j++) { + row = item.rows[j]; + for (k = 0; k < row.length; k++) { + row[k].tokens = this.lexer.inline(row[k].text); + } + } + return item; + } + } + } + lheading(src) { + const cap = this.rules.block.lheading.exec(src); + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: this.lexer.inline(cap[1]) + }; + } + } + paragraph(src) { + const cap = this.rules.block.paragraph.exec(src); + if (cap) { + const text = cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1]; + return { + type: 'paragraph', + raw: cap[0], + text, + tokens: this.lexer.inline(text) + }; + } + } + text(src) { + const cap = this.rules.block.text.exec(src); + if (cap) { + return { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: this.lexer.inline(cap[0]) + }; + } + } + escape(src) { + const cap = this.rules.inline.escape.exec(src); + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: escape(cap[1]) + }; + } + } + tag(src) { + const cap = this.rules.inline.tag.exec(src); + if (cap) { + if (!this.lexer.state.inLink && /^/i.test(cap[0])) { + this.lexer.state.inLink = false; + } + if (!this.lexer.state.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } + else if (this.lexer.state.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + return { + type: 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: false, + text: cap[0] + }; + } + } + link(src) { + const cap = this.rules.inline.link.exec(src); + if (cap) { + const trimmedUrl = cap[2].trim(); + if (!this.options.pedantic && /^$/.test(trimmedUrl))) { + return; + } + // ending angle bracket cannot be escaped + const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } + else { + // find closing parenthesis + const lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + const start = cap[0].indexOf('!') === 0 ? 5 : 4; + const linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + let href = cap[2]; + let title = ''; + if (this.options.pedantic) { + // split pedantic href and title + const link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + if (link) { + href = link[1]; + title = link[3]; + } + } + else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim(); + if (/^$/.test(trimmedUrl))) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } + else { + href = href.slice(1, -1); + } + } + return outputLink(cap, { + href: href ? href.replace(this.rules.inline._escapes, '$1') : href, + title: title ? title.replace(this.rules.inline._escapes, '$1') : title + }, cap[0], this.lexer); + } + } + reflink(src, links) { + let cap; + if ((cap = this.rules.inline.reflink.exec(src)) + || (cap = this.rules.inline.nolink.exec(src))) { + let link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = links[link.toLowerCase()]; + if (!link) { + const text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text + }; + } + return outputLink(cap, link, cap[0], this.lexer); + } + } + emStrong(src, maskedSrc, prevChar = '') { + let match = this.rules.inline.emStrong.lDelim.exec(src); + if (!match) + return; + // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + if (match[3] && prevChar.match(/[\p{L}\p{N}]/u)) + return; + const nextChar = match[1] || match[2] || ''; + if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { + // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below) + const lLength = [...match[0]].length - 1; + let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; + const endReg = match[0][0] === '*' ? this.rules.inline.emStrong.rDelimAst : this.rules.inline.emStrong.rDelimUnd; + endReg.lastIndex = 0; + // Clip maskedSrc to same section of string as src (move to lexer?) + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + if (!rDelim) + continue; // skip single * in __abc*abc__ + rLength = [...rDelim].length; + if (match[3] || match[4]) { // found another Left Delim + delimTotal += rLength; + continue; + } + else if (match[5] || match[6]) { // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + delimTotal -= rLength; + if (delimTotal > 0) + continue; // Haven't found enough closing delimiters + // Remove extra characters. *a*** -> *a* + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); + const raw = [...src].slice(0, lLength + match.index + rLength + 1).join(''); + // Create `em` if smallest delimiter has odd char count. *a*** + if (Math.min(lLength, rLength) % 2) { + const text = raw.slice(1, -1); + return { + type: 'em', + raw, + text, + tokens: this.lexer.inlineTokens(text) + }; + } + // Create 'strong' if smallest delimiter has even char count. **a*** + const text = raw.slice(2, -2); + return { + type: 'strong', + raw, + text, + tokens: this.lexer.inlineTokens(text) + }; + } + } + } + codespan(src) { + const cap = this.rules.inline.code.exec(src); + if (cap) { + let text = cap[2].replace(/\n/g, ' '); + const hasNonSpaceChars = /[^ ]/.test(text); + const hasSpaceCharsOnBothEnds = /^ /.test(text) && / $/.test(text); + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + text = escape(text, true); + return { + type: 'codespan', + raw: cap[0], + text + }; + } + } + br(src) { + const cap = this.rules.inline.br.exec(src); + if (cap) { + return { + type: 'br', + raw: cap[0] + }; + } + } + del(src) { + const cap = this.rules.inline.del.exec(src); + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2]) + }; + } + } + autolink(src) { + const cap = this.rules.inline.autolink.exec(src); + if (cap) { + let text, href; + if (cap[2] === '@') { + text = escape(cap[1]); + href = 'mailto:' + text; + } + else { + text = escape(cap[1]); + href = text; + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text + } + ] + }; + } + } + url(src) { + let cap; + if (cap = this.rules.inline.url.exec(src)) { + let text, href; + if (cap[2] === '@') { + text = escape(cap[0]); + href = 'mailto:' + text; + } + else { + // do extended autolink path validation + let prevCapZero; + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])[0]; + } while (prevCapZero !== cap[0]); + text = escape(cap[0]); + if (cap[1] === 'www.') { + href = 'http://' + cap[0]; + } + else { + href = cap[0]; + } + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text + } + ] + }; + } + } + inlineText(src) { + const cap = this.rules.inline.text.exec(src); + if (cap) { + let text; + if (this.lexer.state.inRawBlock) { + text = cap[0]; + } + else { + text = escape(cap[0]); + } + return { + type: 'text', + raw: cap[0], + text + }; + } + } + } + + /** + * Block-Level Grammar + */ + // Not all rules are defined in the object literal + // @ts-expect-error + const block = { + newline: /^(?: *(?:\n|$))+/, + code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/, + fences: /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/, + hr: /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/, + heading: /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/, + blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, + list: /^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/, + html: '^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) closing tag + + ')', + def: /^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/, + table: noopTest, + lheading: /^((?:(?!^bull ).|\n(?!\n|bull ))+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + // regex template, placeholders will be replaced according to different paragraph + // interruption rules of commonmark and the original markdown spec: + _paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/, + text: /^[^\n]+/ + }; + block._label = /(?!\s*\])(?:\\.|[^\[\]\\])+/; + block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; + block.def = edit(block.def) + .replace('label', block._label) + .replace('title', block._title) + .getRegex(); + block.bullet = /(?:[*+-]|\d{1,9}[.)])/; + block.listItemStart = edit(/^( *)(bull) */) + .replace('bull', block.bullet) + .getRegex(); + block.list = edit(block.list) + .replace(/bull/g, block.bullet) + .replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))') + .replace('def', '\\n+(?=' + block.def.source + ')') + .getRegex(); + block._tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr' + + '|track|ul'; + block._comment = /|$)/; + block.html = edit(block.html, 'i') + .replace('comment', block._comment) + .replace('tag', block._tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); + block.lheading = edit(block.lheading) + .replace(/bull/g, block.bullet) // lists can interrupt + .getRegex(); + block.paragraph = edit(block._paragraph) + .replace('hr', block.hr) + .replace('heading', ' {0,3}#{1,6} ') + .replace('|lheading', '') // setex headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + block.blockquote = edit(block.blockquote) + .replace('paragraph', block.paragraph) + .getRegex(); + /** + * Normal Block Grammar + */ + block.normal = { ...block }; + /** + * GFM Block Grammar + */ + block.gfm = { + ...block.normal, + table: '^ *([^\\n ].*\\|.*)\\n' // Header + + ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)' // Cells + }; + block.gfm.table = edit(block.gfm.table) + .replace('hr', block.hr) + .replace('heading', ' {0,3}#{1,6} ') + .replace('blockquote', ' {0,3}>') + .replace('code', ' {4}[^\\n]') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', block._tag) // tables can be interrupted by type (6) html blocks + .getRegex(); + block.gfm.paragraph = edit(block._paragraph) + .replace('hr', block.hr) + .replace('heading', ' {0,3}#{1,6} ') + .replace('|lheading', '') // setex headings don't interrupt commonmark paragraphs + .replace('table', block.gfm.table) // interrupt paragraphs with table + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + /** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ + block.pedantic = { + ...block.normal, + html: edit('^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', block._comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: edit(block.normal._paragraph) + .replace('hr', block.hr) + .replace('heading', ' *#{1,6} *[^\n]') + .replace('lheading', block.lheading) + .replace('blockquote', ' {0,3}>') + .replace('|fences', '') + .replace('|list', '') + .replace('|html', '') + .getRegex() + }; + /** + * Inline-Level Grammar + */ + // Not all rules are defined in the object literal + // @ts-expect-error + const inline = { + escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, + autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/, + url: noopTest, + tag: '^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^', + link: /^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/, + reflink: /^!?\[(label)\]\[(ref)\]/, + nolink: /^!?\[(ref)\](?:\[\])?/, + reflinkSearch: 'reflink|nolink(?!\\()', + emStrong: { + lDelim: /^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/, + // (1) and (2) can only be a Right Delimiter. (3) and (4) can only be Left. (5) and (6) can be either Left or Right. + // | Skip orphan inside strong | Consume to delim | (1) #*** | (2) a***#, a*** | (3) #***a, ***a | (4) ***# | (5) #***# | (6) a***a + rDelimAst: /^[^_*]*?__[^_*]*?\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\*)[punct](\*+)(?=[\s]|$)|[^punct\s](\*+)(?!\*)(?=[punct\s]|$)|(?!\*)[punct\s](\*+)(?=[^punct\s])|[\s](\*+)(?!\*)(?=[punct])|(?!\*)[punct](\*+)(?!\*)(?=[punct])|[^punct\s](\*+)(?=[^punct\s])/, + rDelimUnd: /^[^_*]*?\*\*[^_*]*?_[^_*]*?(?=\*\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\s]|$)|[^punct\s](_+)(?!_)(?=[punct\s]|$)|(?!_)[punct\s](_+)(?=[^punct\s])|[\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])/ // ^- Not allowed for _ + }, + code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, + br: /^( {2,}|\\)\n(?!\s*$)/, + del: noopTest, + text: /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\`^|~'; + inline.punctuation = edit(inline.punctuation, 'u').replace(/punctuation/g, inline._punctuation).getRegex(); + // sequences em should skip over [title](link), `code`, + inline.blockSkip = /\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g; + inline.anyPunctuation = /\\[punct]/g; + inline._escapes = /\\([punct])/g; + inline._comment = edit(block._comment).replace('(?:-->|$)', '-->').getRegex(); + inline.emStrong.lDelim = edit(inline.emStrong.lDelim, 'u') + .replace(/punct/g, inline._punctuation) + .getRegex(); + inline.emStrong.rDelimAst = edit(inline.emStrong.rDelimAst, 'gu') + .replace(/punct/g, inline._punctuation) + .getRegex(); + inline.emStrong.rDelimUnd = edit(inline.emStrong.rDelimUnd, 'gu') + .replace(/punct/g, inline._punctuation) + .getRegex(); + inline.anyPunctuation = edit(inline.anyPunctuation, 'gu') + .replace(/punct/g, inline._punctuation) + .getRegex(); + inline._escapes = edit(inline._escapes, 'gu') + .replace(/punct/g, inline._punctuation) + .getRegex(); + inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; + inline._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/; + inline.autolink = edit(inline.autolink) + .replace('scheme', inline._scheme) + .replace('email', inline._email) + .getRegex(); + inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; + inline.tag = edit(inline.tag) + .replace('comment', inline._comment) + .replace('attribute', inline._attribute) + .getRegex(); + inline._label = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; + inline._href = /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/; + inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; + inline.link = edit(inline.link) + .replace('label', inline._label) + .replace('href', inline._href) + .replace('title', inline._title) + .getRegex(); + inline.reflink = edit(inline.reflink) + .replace('label', inline._label) + .replace('ref', block._label) + .getRegex(); + inline.nolink = edit(inline.nolink) + .replace('ref', block._label) + .getRegex(); + inline.reflinkSearch = edit(inline.reflinkSearch, 'g') + .replace('reflink', inline.reflink) + .replace('nolink', inline.nolink) + .getRegex(); + /** + * Normal Inline Grammar + */ + inline.normal = { ...inline }; + /** + * Pedantic Inline Grammar + */ + inline.pedantic = { + ...inline.normal, + strong: { + start: /^__|\*\*/, + middle: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + endAst: /\*\*(?!\*)/g, + endUnd: /__(?!_)/g + }, + em: { + start: /^_|\*/, + middle: /^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/, + endAst: /\*(?!\*)/g, + endUnd: /_(?!_)/g + }, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', inline._label) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', inline._label) + .getRegex() + }; + /** + * GFM Inline Grammar + */ + inline.gfm = { + ...inline.normal, + escape: edit(inline.escape).replace('])', '~|])').getRegex(), + _extended_email: /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/, + url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\ { + return leading + ' '.repeat(tabs.length); + }); + } + let token; + let lastToken; + let cutSrc; + let lastParagraphClipped; + while (src) { + if (this.options.extensions + && this.options.extensions.block + && this.options.extensions.block.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + if (token.raw.length === 1 && tokens.length > 0) { + // if there's a single \n as a spacer, it's terminating the last line, + // so move it there so that we don't get unecessary paragraph tags + tokens[tokens.length - 1].raw += '\n'; + } + else { + tokens.push(token); + } + continue; + } + // code + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + // An indented code block cannot interrupt a paragraph. + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + // fences + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // heading + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // hr + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // blockquote + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // list + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // html + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // def + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title + }; + } + continue; + } + // table (gfm) + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // lheading + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions && this.options.extensions.startBlock) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startBlock.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + lastToken = tokens[tokens.length - 1]; + if (lastParagraphClipped && lastToken.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + lastParagraphClipped = (cutSrc.length !== src.length); + src = src.substring(token.raw.length); + continue; + } + // text + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && lastToken.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + this.state.top = true; + return tokens; + } + inline(src, tokens = []) { + this.inlineQueue.push({ src, tokens }); + return tokens; + } + /** + * Lexing/Compiling + */ + inlineTokens(src, tokens = []) { + let token, lastToken, cutSrc; + // String with links masked to avoid interference with em and strong + let maskedSrc = src; + let match; + let keepPrevChar, prevChar; + // Mask out reflinks + if (this.tokens.links) { + const links = Object.keys(this.tokens.links); + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } + // Mask out other blocks + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + // Mask out escaped characters + while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); + } + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + keepPrevChar = false; + // extensions + if (this.options.extensions + && this.options.extensions.inline + && this.options.extensions.inline.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // tag + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // link + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // reflink, nolink + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // em & strong + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // code + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // br + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // del (gfm) + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // autolink + if (token = this.tokenizer.autolink(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // url (gfm) + if (!this.state.inLink && (token = this.tokenizer.url(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions && this.options.extensions.startInline) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startInline.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (token = this.tokenizer.inlineText(cutSrc)) { + src = src.substring(token.raw.length); + if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + keepPrevChar = true; + lastToken = tokens[tokens.length - 1]; + if (lastToken && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + return tokens; + } + } + + /** + * Renderer + */ + class _Renderer { + options; + constructor(options) { + this.options = options || exports.defaults; + } + code(code, infostring, escaped) { + const lang = (infostring || '').match(/^\S*/)?.[0]; + code = code.replace(/\n$/, '') + '\n'; + if (!lang) { + return '
'
+                    + (escaped ? code : escape(code, true))
+                    + '
\n'; + } + return '
'
+                + (escaped ? code : escape(code, true))
+                + '
\n'; + } + blockquote(quote) { + return `
\n${quote}
\n`; + } + html(html, block) { + return html; + } + heading(text, level, raw) { + // ignore IDs + return `${text}\n`; + } + hr() { + return '
\n'; + } + list(body, ordered, start) { + const type = ordered ? 'ol' : 'ul'; + const startatt = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startatt + '>\n' + body + '\n'; + } + listitem(text, task, checked) { + return `
  • ${text}
  • \n`; + } + checkbox(checked) { + return ''; + } + paragraph(text) { + return `

    ${text}

    \n`; + } + table(header, body) { + if (body) + body = `${body}`; + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + } + tablerow(content) { + return `\n${content}\n`; + } + tablecell(content, flags) { + const type = flags.header ? 'th' : 'td'; + const tag = flags.align + ? `<${type} align="${flags.align}">` + : `<${type}>`; + return tag + content + `\n`; + } + /** + * span level renderer + */ + strong(text) { + return `${text}`; + } + em(text) { + return `${text}`; + } + codespan(text) { + return `${text}`; + } + br() { + return '
    '; + } + del(text) { + return `${text}`; + } + link(href, title, text) { + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = '
    '; + return out; + } + image(href, title, text) { + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = `${text} 0 && item.tokens[0].type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text; + } + } + else { + item.tokens.unshift({ + type: 'text', + text: checkbox + ' ' + }); + } + } + else { + itemBody += checkbox + ' '; + } + } + itemBody += this.parse(item.tokens, loose); + body += this.renderer.listitem(itemBody, task, !!checked); + } + out += this.renderer.list(body, ordered, start); + continue; + } + case 'html': { + const htmlToken = token; + out += this.renderer.html(htmlToken.text, htmlToken.block); + continue; + } + case 'paragraph': { + const paragraphToken = token; + out += this.renderer.paragraph(this.parseInline(paragraphToken.tokens)); + continue; + } + case 'text': { + let textToken = token; + let body = textToken.tokens ? this.parseInline(textToken.tokens) : textToken.text; + while (i + 1 < tokens.length && tokens[i + 1].type === 'text') { + textToken = tokens[++i]; + body += '\n' + (textToken.tokens ? this.parseInline(textToken.tokens) : textToken.text); + } + out += top ? this.renderer.paragraph(body) : body; + continue; + } + default: { + const errMsg = 'Token with "' + token.type + '" type was not found.'; + if (this.options.silent) { + console.error(errMsg); + return ''; + } + else { + throw new Error(errMsg); + } + } + } + } + return out; + } + /** + * Parse Inline Tokens + */ + parseInline(tokens, renderer) { + renderer = renderer || this.renderer; + let out = ''; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + // Run any renderer extensions + if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) { + const ret = this.options.extensions.renderers[token.type].call({ parser: this }, token); + if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(token.type)) { + out += ret || ''; + continue; + } + } + switch (token.type) { + case 'escape': { + const escapeToken = token; + out += renderer.text(escapeToken.text); + break; + } + case 'html': { + const tagToken = token; + out += renderer.html(tagToken.text); + break; + } + case 'link': { + const linkToken = token; + out += renderer.link(linkToken.href, linkToken.title, this.parseInline(linkToken.tokens, renderer)); + break; + } + case 'image': { + const imageToken = token; + out += renderer.image(imageToken.href, imageToken.title, imageToken.text); + break; + } + case 'strong': { + const strongToken = token; + out += renderer.strong(this.parseInline(strongToken.tokens, renderer)); + break; + } + case 'em': { + const emToken = token; + out += renderer.em(this.parseInline(emToken.tokens, renderer)); + break; + } + case 'codespan': { + const codespanToken = token; + out += renderer.codespan(codespanToken.text); + break; + } + case 'br': { + out += renderer.br(); + break; + } + case 'del': { + const delToken = token; + out += renderer.del(this.parseInline(delToken.tokens, renderer)); + break; + } + case 'text': { + const textToken = token; + out += renderer.text(textToken.text); + break; + } + default: { + const errMsg = 'Token with "' + token.type + '" type was not found.'; + if (this.options.silent) { + console.error(errMsg); + return ''; + } + else { + throw new Error(errMsg); + } + } + } + } + return out; + } + } + + class _Hooks { + options; + constructor(options) { + this.options = options || exports.defaults; + } + static passThroughHooks = new Set([ + 'preprocess', + 'postprocess' + ]); + /** + * Process markdown before marked + */ + preprocess(markdown) { + return markdown; + } + /** + * Process HTML after marked is finished + */ + postprocess(html) { + return html; + } + } + + class Marked { + defaults = _getDefaults(); + options = this.setOptions; + parse = this.#parseMarkdown(_Lexer.lex, _Parser.parse); + parseInline = this.#parseMarkdown(_Lexer.lexInline, _Parser.parseInline); + Parser = _Parser; + parser = _Parser.parse; + Renderer = _Renderer; + TextRenderer = _TextRenderer; + Lexer = _Lexer; + lexer = _Lexer.lex; + Tokenizer = _Tokenizer; + Hooks = _Hooks; + constructor(...args) { + this.use(...args); + } + /** + * Run callback for every token + */ + walkTokens(tokens, callback) { + let values = []; + for (const token of tokens) { + values = values.concat(callback.call(this, token)); + switch (token.type) { + case 'table': { + const tableToken = token; + for (const cell of tableToken.header) { + values = values.concat(this.walkTokens(cell.tokens, callback)); + } + for (const row of tableToken.rows) { + for (const cell of row) { + values = values.concat(this.walkTokens(cell.tokens, callback)); + } + } + break; + } + case 'list': { + const listToken = token; + values = values.concat(this.walkTokens(listToken.items, callback)); + break; + } + default: { + const genericToken = token; + if (this.defaults.extensions?.childTokens?.[genericToken.type]) { + this.defaults.extensions.childTokens[genericToken.type].forEach((childTokens) => { + values = values.concat(this.walkTokens(genericToken[childTokens], callback)); + }); + } + else if (genericToken.tokens) { + values = values.concat(this.walkTokens(genericToken.tokens, callback)); + } + } + } + } + return values; + } + use(...args) { + const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; + args.forEach((pack) => { + // copy options to new object + const opts = { ...pack }; + // set async to true if it was set to true before + opts.async = this.defaults.async || opts.async || false; + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (!ext.name) { + throw new Error('extension name required'); + } + if ('renderer' in ext) { // Renderer extensions + const prevRenderer = extensions.renderers[ext.name]; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function (...args) { + let ret = ext.renderer.apply(this, args); + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + return ret; + }; + } + else { + extensions.renderers[ext.name] = ext.renderer; + } + } + if ('tokenizer' in ext) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } + const extLevel = extensions[ext.level]; + if (extLevel) { + extLevel.unshift(ext.tokenizer); + } + else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } + else { + extensions.startBlock = [ext.start]; + } + } + else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } + else { + extensions.startInline = [ext.start]; + } + } + } + } + if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + opts.extensions = extensions; + } + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { + const renderer = this.defaults.renderer || new _Renderer(this.defaults); + for (const prop in pack.renderer) { + const rendererFunc = pack.renderer[prop]; + const rendererKey = prop; + const prevRenderer = renderer[rendererKey]; + // Replace renderer with func to run extension, but fall back if false + renderer[rendererKey] = (...args) => { + let ret = rendererFunc.apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret || ''; + }; + } + opts.renderer = renderer; + } + if (pack.tokenizer) { + const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); + for (const prop in pack.tokenizer) { + const tokenizerFunc = pack.tokenizer[prop]; + const tokenizerKey = prop; + const prevTokenizer = tokenizer[tokenizerKey]; + // Replace tokenizer with func to run extension, but fall back if false + tokenizer[tokenizerKey] = (...args) => { + let ret = tokenizerFunc.apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + // ==-- Parse Hooks extensions --== // + if (pack.hooks) { + const hooks = this.defaults.hooks || new _Hooks(); + for (const prop in pack.hooks) { + const hooksFunc = pack.hooks[prop]; + const hooksKey = prop; + const prevHook = hooks[hooksKey]; + if (_Hooks.passThroughHooks.has(prop)) { + hooks[hooksKey] = (arg) => { + if (this.defaults.async) { + return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); + }); + } + const ret = hooksFunc.call(hooks, arg); + return prevHook.call(hooks, ret); + }; + } + else { + hooks[hooksKey] = (...args) => { + let ret = hooksFunc.apply(hooks, args); + if (ret === false) { + ret = prevHook.apply(hooks, args); + } + return ret; + }; + } + } + opts.hooks = hooks; + } + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { + const walkTokens = this.defaults.walkTokens; + const packWalktokens = pack.walkTokens; + opts.walkTokens = function (token) { + let values = []; + values.push(packWalktokens.call(this, token)); + if (walkTokens) { + values = values.concat(walkTokens.call(this, token)); + } + return values; + }; + } + this.defaults = { ...this.defaults, ...opts }; + }); + return this; + } + setOptions(opt) { + this.defaults = { ...this.defaults, ...opt }; + return this; + } + #parseMarkdown(lexer, parser) { + return (src, options) => { + const origOpt = { ...options }; + const opt = { ...this.defaults, ...origOpt }; + // Show warning if an extension set async to true but the parse was called with async: false + if (this.defaults.async === true && origOpt.async === false) { + if (!opt.silent) { + console.warn('marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored.'); + } + opt.async = true; + } + const throwError = this.#onError(!!opt.silent, !!opt.async); + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } + if (opt.hooks) { + opt.hooks.options = opt; + } + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); + } + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + const tokens = lexer(src, opt); + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } + catch (e) { + return throwError(e); + } + }; + } + #onError(silent, async) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (silent) { + const msg = '

    An error occurred:

    '
    +                        + escape(e.message + '', true)
    +                        + '
    '; + if (async) { + return Promise.resolve(msg); + } + return msg; + } + if (async) { + return Promise.reject(e); + } + throw e; + }; + } + } + + const markedInstance = new Marked(); + function marked(src, opt) { + return markedInstance.parse(src, opt); + } + /** + * Sets the default options. + * + * @param options Hash of options + */ + marked.options = + marked.setOptions = function (options) { + markedInstance.setOptions(options); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }; + /** + * Gets the original marked default options. + */ + marked.getDefaults = _getDefaults; + marked.defaults = exports.defaults; + /** + * Use Extension + */ + marked.use = function (...args) { + markedInstance.use(...args); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }; + /** + * Run callback for every token + */ + marked.walkTokens = function (tokens, callback) { + return markedInstance.walkTokens(tokens, callback); + }; + /** + * Compiles markdown to HTML without enclosing `p` tag. + * + * @param src String of markdown source to be compiled + * @param options Hash of options + * @return String of compiled HTML + */ + marked.parseInline = markedInstance.parseInline; + /** + * Expose + */ + marked.Parser = _Parser; + marked.parser = _Parser.parse; + marked.Renderer = _Renderer; + marked.TextRenderer = _TextRenderer; + marked.Lexer = _Lexer; + marked.lexer = _Lexer.lex; + marked.Tokenizer = _Tokenizer; + marked.Hooks = _Hooks; + marked.parse = marked; + const options = marked.options; + const setOptions = marked.setOptions; + const use = marked.use; + const walkTokens = marked.walkTokens; + const parseInline = marked.parseInline; + const parse = marked; + const parser = _Parser.parse; + const lexer = _Lexer.lex; + + exports.Hooks = _Hooks; + exports.Lexer = _Lexer; + exports.Marked = Marked; + exports.Parser = _Parser; + exports.Renderer = _Renderer; + exports.TextRenderer = _TextRenderer; + exports.Tokenizer = _Tokenizer; + exports.getDefaults = _getDefaults; + exports.lexer = lexer; + exports.marked = marked; + exports.options = options; + exports.parse = parse; + exports.parseInline = parseInline; + exports.parser = parser; + exports.setOptions = setOptions; + exports.use = use; + exports.walkTokens = walkTokens; + +})); +//# sourceMappingURL=marked.umd.js.map diff --git a/js/main.js b/js/main.js index 01d509d..67f315d 100644 --- a/js/main.js +++ b/js/main.js @@ -1,83 +1,30 @@ -/** - * Threshold value for bailing because too many ants are spawning. - * @type {number} - */ const TOO_MANY_ANTS = 64; -/** - * Selector - * @param {string} s Selector - * @returns {HTMLElement} - */ -const $ = s => document.querySelector(s); - -/** - * @type {HTMLCanvasElement} - */ -const playfield = $('#playfield'); -/** - * @type {HTMLButtonElement} - */ -const startStopBtn = $('#startstop'); -/** - * @type {HTMLButtonElement} - */ -const stepBtn = $('#step'); -/** - * @type {HTMLOutputElement} - */ -const stepCounter = $('#stepnum'); -/** - * @type {HTMLInputElement} - */ -const speedSlider = $('#speedslider'); -/** - * @type {HTMLInputElement} - */ -const speedBox = $('#speedbox'); -/** - * @type {HTMLInputElement} - */ -const muteCheckbox = $('#mutecheck'); -/** - * @type {HTMLOutputElement} - */ -const antsCounter = $('#antscount'); -/** - * @type {HTMLButtonElement} - */ -const loadBtn = $('#loadbtn'); -/** - * @type {HTMLButtonElement} - */ -const dumpBtn = $('#dumpbtn'); -/** - * @type {HTMLOutputElement} - */ -const statusBar = $('#statusbar'); -/** - * @type {HTMLButtonElement} - */ -const fitBtn = $('#fit'); -/** - * @type {HTMLInputElement} - */ -const autoFit = $('#autofit'); -/** - * @type {HTMLSelectElement} - */ -const followSelector = $('#follow'); -/** - * @type {HTMLSelectElement} - */ -const actionsSelector = $('#actions'); -/** - * @type {HTMLDivElement} - */ -const debugBar = $('#debugbar'); +const safe$ = selector => { + var elem = document.querySelector(selector); + if (!elem) throw new Error("can't find " + selector); + return elem; +}; + +const playfield = safe$('#playfield'); +const startStopBtn = safe$('#startstop'); +const stepBtn = safe$('#step'); +const stepCounter = safe$('#stepnum'); +const speedSlider = safe$('#speedslider'); +const speedBox = safe$('#speedbox'); +const muteCheckbox = safe$('#mutecheck'); +const antsCounter = safe$('#antscount'); +const loadBtn = safe$('#loadbtn'); +const dumpBtn = safe$('#dumpbtn'); +const statusBar = safe$('#statusbar'); +const fitBtn = safe$('#fit'); +const autoFit = safe$('#autofit'); +const followSelector = safe$('#follow'); +const actionsSelector = safe$('#actions'); +const debugBar = safe$('#debugbar'); ace.config.set('basePath', 'https://cdn.jsdelivr.net/npm/ace-builds@1.10.0/src-noconflict/'); -const textbox = ace.edit('textbox', { mode: 'ace/mode/xml' }); +const textbox = ace.edit('textbox', { mode: 'ace/mode/html' }); function debug(message) { return; @@ -87,58 +34,25 @@ function debug(message) { console.trace(message); } -/** - * @type {Ant[]} - */ var ants = []; -/** - * @type {Breeder} - */ var breeder = new Breeder(); -/** - * @type {CanvasRenderingContext2D} - */ var mainCtx = playfield.getContext('2d'); -/** - * @type {World} - */ var world = new World(); -/** - * @type {CanvasToolsManager} - */ -var canvasTools = new CanvasToolsManager(playfield, $('#toolselect'), $('#tooloption'), [ +var canvasTools = new CanvasToolsManager(playfield, safe$('#toolselect'), safe$('#tooloption'), [ new DragTool(), new DrawCellsTool(world), new DrawAntsTool(world, breeder, ants), ]); -/** - * @type {object} - */ var header = { stepCount: 0 }; -/** - * @type {string[][]} - */ var interpolations = []; -/** - * @type {ActionManager} - */ var actions = new ActionManager(); -/** - * Shows the text in the status bar. - * @param {string} text Text to show - * @param {string} [color='black'] Color; default is black - */ function showStatus(text, color) { statusBar.value = text; statusBar.style.color = color || (DARK_MODE ? 'white' : 'black'); } -/** - * Enables or disable sthe Play/Pause and Step buttons if an error occurred of something changed. - * @param {boolean} canRun Whether the buttons should be enabled. - */ function runEnable(canRun) { if (canRun) { startStopBtn.removeAttribute('disabled'); @@ -149,9 +63,6 @@ function runEnable(canRun) { } } -/** - * Render loop function - */ function render() { canvasTools.clear(); canvasTools.drawTransformed(() => { @@ -162,14 +73,8 @@ function render() { } render(); -/** - * @type {boolean} - */ var running = false; -/** - * @type {boolean} - */ var GLOBAL_MUTE = false; actions.action('start', () => { @@ -203,10 +108,6 @@ actions.action('playpause', () => { startStopBtn.addEventListener('click', () => actions.trigger('playpause')); stepBtn.addEventListener('click', () => actions.trigger('step')); -/** - * Runs the world one tick. - * @param {boolean} force Force run one tick. - */ function tick(force = false) { if (!running && !force) return; syncMediaSession(); @@ -266,9 +167,6 @@ actions.action('autofit', (autofit) => { autoFit.checked = autofit; }); -/** - * Loads the text from the text box and updates the world. - */ actions.action('load', (value) => { actions.trigger('stop'); header.stepCount = 0; @@ -359,18 +257,10 @@ try { console.error(e); } -/** - * Centers the cell in the viewport. - * @param {Vector} cell - */ function center(cell) { canvasTools.panxy = vPlus(vScale(cell, -1 * world.cellSize * canvasTools.zoom), vScale({ x: playfield.width, y: playfield.height }, 0.5)); } -/** - * Centers the requested ant in the viewport, if it exists. - * @param {string} antID - */ function followAnt(antID) { if (!antID) { return; @@ -398,13 +288,10 @@ actions.action('dump', () => { }) dumpBtn.addEventListener('click', () => actions.trigger('dump')); -/** - * Fits the ace code editor to the box it's in when the box changes size. - */ function fitace() { setTimeout(() => { - var rect = $('#textbox').parentElement.getBoundingClientRect(); - $('#textbox').setAttribute('style', `width:${rect.width}px;height:${rect.height}px`); + var rect = safe$('#textbox').parentElement.getBoundingClientRect(); + safe$('#textbox').setAttribute('style', `width:${rect.width}px;height:${rect.height}px`); textbox.resize(true); }, 0); } @@ -419,7 +306,7 @@ window.addEventListener('hashchange', () => { where = '#dumpstatuswrapper'; textbox.setTheme(DARK_MODE ? 'ace/theme/pastel_on_dark' : 'ace/theme/chrome'); } - $(where).append(statusBar); + safe$(where).append(statusBar); fitace(); }); location.hash = "#"; // Don't have editor open by default @@ -466,13 +353,18 @@ if ("serviceWorker" in navigator) { } // Dev version indicator +const heading = safe$("#mainhead"); if (location.host.indexOf('localhost') != -1) { document.title += ' - localhost version'; - $('main .heading').textContent += ' - localhost version'; + heading.textContent += ' - localhost version'; +} +else if (location.host.indexOf('.github.dev') != -1) { + document.title += ' - codespace version'; + heading.textContent += ' - codespace version'; } else if (location.protocol.indexOf('file') != -1) { document.title += ' - file:// version'; - $('main .heading').textContent += ' - file:// version (some features unavailable)'; + heading.textContent += ' - file:// version (some features unavailable)'; } else { // we are in the full web version diff --git a/js/main_load.js b/js/main_load.js new file mode 100644 index 0000000..121e9a5 --- /dev/null +++ b/js/main_load.js @@ -0,0 +1,5 @@ +window.addEventListener("DOMContentLoaded", () => { + var s = document.createElement("script"); + s.src = "js/main.js"; + document.body.append(s); +}); diff --git a/js/notes.js b/js/notes.js index d62e5e5..05addaf 100644 --- a/js/notes.js +++ b/js/notes.js @@ -1,6 +1,3 @@ -/** - * Ant that plays drum sounds. - */ class Beetle extends Ant { constructor(...args) { super(...args); @@ -16,9 +13,6 @@ class Beetle extends Ant { } } -/** - * Ant that plays strings sounds. - */ class Cricket extends Ant { constructor(...args) { super(...args); diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..d65e6e9 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,48 @@ + +function clamp(x, a, b) { + if (x < a) return a; + if (x > b) return b; + return x; +} +function map(x, a, b, c, d, k = true) { + if (k) x = clamp(x, a, b); + return (x - a) * (d - c) / (b - a) + c; +} + +function irange(start, stop, step) { + var out = []; + for (var x = start; x <= stop; x += step) out.push(x); + return x; +} + +function camel2words(camel) { + var words = [...camel.matchAll(/(?:^|_*)([A-Z]?[a-z]+)/g)].map(x => x[1]); + var first = words[0]; + first = first[0].toUpperCase() + first.slice(1).toLowerCase(); + var rest = words.slice(1).map(x => x.toLowerCase()); + return [first].concat(rest).join(" "); +} + +function dedent(strs, vals) { + var strings = strs.raw ? strs.raw : [strs]; + var indented = ""; + for (var i = 0; i < strings.length; i++) { + indented += strings[i]; + if (vals && i < vals.length) indented += vals[i].toString(); + } + var lines = indented.split("\n"); + var min = Infinity; + for (var line of lines) { + var m = line.match(/^(\s+)\S+/); + if (m) min = Math.min(min, m[1].length); + } + var result = indented; + if (min !== Infinity) { + result = lines.map(l => /\s/.test(l[0]) ? l.slice(min) : l).join("\n"); + } + return result.trim().replace(/\\n/g, "\n"); +} + +function todo(foo = "") { + throw new Error("todo: " + foo); +} diff --git a/js/vector.js b/js/vector.js index 9476c1a..f3635f9 100644 --- a/js/vector.js +++ b/js/vector.js @@ -1,72 +1,28 @@ -/** - * @typedef {Object} Vector - * @property {number} x - * @property {number} y - */ -/** - * Applies the function to the vector's x- and y-coordinates. - * @param {Function} fun - * @param {Vector[]} vectors - */ function vApply(fun, ...vectors) { return Object.freeze({ x: fun(...vectors.map(v => v.x)), y: fun(...vectors.map(v => v.y)) }); } -/** - * Distance between two points. - * @param {Vector} p1 - * @param {Vector} p2 - * @returns {number} - */ function vRelMag(p1, p2) { return vMagnitude(vMinus(p1, p2)); } -/** - * Length of the vector. - * @param {Vector} d - * @returns {number} - */ function vMagnitude(d) { return Math.sqrt((d.x * d.x) + (d.y * d.y)); } -/** - * p1-p2 - * @param {Vector} p1 - * @param {Vector} p2 - * @returns {Vector} - */ function vMinus(p1, p2) { return Object.freeze({ x: p1.x - p2.x, y: p1.y - p2.y }); } -/** -* p1+p2 -* @param {Vector} p1 -* @param {Vector} p2 -* @returns {Vector} -*/ function vPlus(p1, p2) { return Object.freeze({ x: p1.x + p2.x, y: p1.y + p2.y }); } -/** -* p1*k -* @param {Vector} p1 -* @param {number} k -* @returns {Vector} -*/ function vScale(p1, k) { return Object.freeze({ x: p1.x * k, y: p1.y * k }); } -/** - * Clone a vector - * @param {Vector} p1 - * @returns {Vector} - */ function vClone(p1) { return Object.freeze({ x: p1.x, y: p1.y }); } \ No newline at end of file diff --git a/js/world.js b/js/world.js index 78bbb9d..db1b30d 100644 --- a/js/world.js +++ b/js/world.js @@ -1,98 +1,46 @@ -/** - * @typedef {Object} BoundingBox - * @property {Vector} tl Top left corner - * @property {Vector} br Bottom right corner - */ -/** - * World that holds a grid of cells. - */ class World { - /** - * @param {CanvasRenderingContext2D} ctx - * @param {number} cellSize The side length of the cell at zoom=1. - * @param {array} colors Object of colors to draw each state in. - */ - constructor(cellSize = 16, colors = {}) { + constructor(cellSize = 16, colors = {}) { this.cells = {}; this.cellSize = cellSize; this.stateColors = colors; this.rng = 'seedrandom' in Math ? new Math.seedrandom('yes') : Math.random; } - /** - * Draws all the cells on the canvas. - */ - draw(ctx) { + draw(ctx) { for (var cell in this.cells) { var [x, y] = cell.split(','); this.drawCell(ctx, parseInt(x, 16), parseInt(y, 16), this.getColor(this.cells[cell])); } } - /** - * Draws one cell. - * @param {number} x - * @param {number} y - * @param {string} color - */ - drawCell(ctx, x, y, color) { + drawCell(ctx, x, y, color) { ctx.save(); ctx.fillStyle = color; ctx.translate(x * this.cellSize, y * this.cellSize); ctx.fillRect(-this.cellSize / 2, -this.cellSize / 2, this.cellSize, this.cellSize); ctx.restore(); } - /** - * Creates or returns the color for this state. - * @param {number} state - * @returns {string} - */ - getColor(state) { + getColor(state) { if (!(state in this.stateColors)) this.stateColors[state] = `rgb(${this.rng() * 255},${this.rng() * 255},${this.rng() * 255})`; return this.stateColors[state]; } - /** - * Gets the cell state at (x, y). - * @param {number} x - * @param {number} y - * @returns {number} - */ - getCell(x, y) { + getCell(x, y) { return this.cells[`${x.toString(16)},${y.toString(16)}`] ?? 0; } - /** - * Sets the cell at (x, y) to state. - * @param {number} x - * @param {number} y - * @param {number} state - */ - setCell(x, y, state) { + setCell(x, y, state) { var coords = `${x.toString(16)},${y.toString(16)}`; if (state === 0) delete this.cells[coords]; else this.cells[coords] = state; } - /** - * Sets the cell at (x, y) to state, or to 0 if it was already that. - * @param {number} x - * @param {number} y - * @param {number} state - */ - paint(x, y, state) { + paint(x, y, state) { var s = this.getCell(x, y); if (s === state) this.setCell(x, y, 0); else this.setCell(x, y, state); } - /** - * Sets all cells to 0. - */ - clear() { + clear() { this.cells = {}; } - /** - * @param {Ants[]} ants List of ants to include in the calculations. - * @returns {BoundingBox} - */ - bbox(ants) { + bbox(ants) { var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity, got = false; for (var cell of Object.getOwnPropertyNames(this.cells)) { var [x, y] = cell.split(','); @@ -109,11 +57,7 @@ class World { if (!got) return { tl: { x: 0, y: 0 }, br: { x: 0, y: 0 } }; return { tl: { x: minX, y: minY }, br: { x: maxX, y: maxY } }; } - /** - * Serializes the data into an `` XML tag. - * @returns {string} - */ - dump() { + dump() { var { tl: { x: minX, y: minY }, br: { x: maxX, y: maxY } } = this.bbox([]); var uncompressed = ''; for (var y = minY; y <= maxY; y++) { @@ -126,13 +70,7 @@ class World { } return `${rleCompress(uncompressed)}`; } - /** - * Uncompresses and pastes the RLE data at (x, y). - * @param {string} rle Raw compressed RLE data - * @param {number} x - * @param {number} y - */ - paste(rle, x, y) { + paste(rle, x, y) { var origX = x; for (var [c] of rleUncompress(rle).matchAll(/[p-x]?[A-X]|[$.]/g)) { if (c === '$') { @@ -147,11 +85,6 @@ class World { } } -/** - * Compresses the RLE string. - * @param {string} text - * @returns {string} - */ function rleCompress(text) { var out = text.replaceAll(/(([p-x]?[A-X]|[$.])+)\1+/g, function (all, one) { return (all.length / one.length) + '(' + one + ')'; @@ -169,11 +102,6 @@ function rleCompress(text) { return out2; } -/** - * Uncompresses the RLE string. - * @param {string} text - * @returns {string} - */ function rleUncompress(text) { return text.replaceAll(/\s+/g, "").replaceAll(/(\d+)\(([^\)]+)\)/g, function (_, times, what) { return what.repeat(parseInt(times)); @@ -182,21 +110,11 @@ function rleUncompress(text) { }); } -/** - * Turns the string of letters into a number. - * @param {string} letters - * @returns {number} - */ function lettersToStateNum(letters) { if (letters.length === 1) return '.ABCDEFGHIJKLMNOPQRSTUVWX'.indexOf(letters); return lettersToStateNum(letters.slice(1)) + (24 * ('pqrstuvwx'.indexOf(letters[0]) + 1)); } -/** - * Turns the state number into a string of letters. - * @param {number} state - * @returns {string} - */ function stateNumToLetters(state) { if (state === undefined) return ''; if (state === 0) return '.'; diff --git a/js3/.gitkeep b/js3/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/js3/.gitkeep @@ -0,0 +1 @@ + diff --git a/serve b/serve index e7373a2..ba75f0c 100755 --- a/serve +++ b/serve @@ -1,2 +1,22 @@ #! /bin/bash +if [[ "$USER" -eq codespace ]] +then +echo "Using Flask to serve https://localhost:4443/ -> port forwarding" +sudo python3 <") +def files(file):return flask.send_from_directory(os.getcwd(), file, max_age=0) +@app.route("/report_error/") +def errror(err):print(urllib.parse.unquote(err),end="\n\n");return "",500 +def go():app.run("localhost", 4443, ssl_context="adhoc") +def kill_other():cmd="kill "+subprocess.check_output(["sh","-c","sudo netstat -tulnp | grep :4443"]).decode("ascii").split()[-1].split("/")[0]+"; sleep 0.1";print(cmd);os.system(cmd) +try:go() +except SystemExit:kill_other();go() +PYTHON +else sudo python3 -m http.server -b localhost 80 +fi +# server corrupted? use this: https://stackoverflow.com/a/51470713 diff --git a/serviceWorker.js b/serviceWorker.js index 1bf9258..1d37ab8 100644 --- a/serviceWorker.js +++ b/serviceWorker.js @@ -1,6 +1,6 @@ const CACHE_NAME = "langton-music-v2"; -if (location.protocol.indexOf('file') === -1 && location.host.indexOf('localhost') === -1) { +if (location.protocol.indexOf('file') === -1 && location.host.indexOf('localhost') === -1 && location.host.indexOf('.github.dev') === -1) { // Copied from https://developer.chrome.com/docs/workbox/caching-strategies-overview/#stale-while-revalidate self.addEventListener('fetch', e => { diff --git a/style.css b/style.css index ebe9f80..3e7a85c 100644 --- a/style.css +++ b/style.css @@ -150,6 +150,7 @@ kbd { padding: 2px; margin: 2px; filter: drop-shadow(0 3px); + transform: translateY(-3px); } input[type="number"], diff --git a/interpoltest.html b/test/interpolations.html similarity index 57% rename from interpoltest.html rename to test/interpolations.html index 31d4f78..6d51bbc 100644 --- a/interpoltest.html +++ b/test/interpolations.html @@ -19,7 +19,7 @@

    Interpolator Test

    - + diff --git a/test/svg_ant.html b/test/svg_ant.html new file mode 100644 index 0000000..491d9de --- /dev/null +++ b/test/svg_ant.html @@ -0,0 +1,151 @@ + + + + + SVG Ant Test + + + + + + +

    Test of Non-Canvas Ants

    + + + + + + + + + + + + + + Ant-Name + + +

    ⬆ This is an SVG!!

    +

    + + + + \ No newline at end of file