diff --git a/lifeplayer67.CanvasView/CHANGELOG.md b/lifeplayer67.CanvasView/CHANGELOG.md new file mode 100644 index 000000000..3eba7b00a --- /dev/null +++ b/lifeplayer67.CanvasView/CHANGELOG.md @@ -0,0 +1,109 @@ +# Canvas View Changelog + +## [0.9.5] - 2026-09-25 (unreleased) + +- Markdown in cards is rendered by showdown (the converter NotePlan's own helpers + use): full lists, GFM tables, tasklists, emoji, hashtags/mentions/highlights; + local images embed as data URIs. Identical styling for text and note cards. +- Notes open beside the canvas (split view by default; setting for new window + or main editor). +- Editor polish: caret lands where you double-clicked and is scrolled into view; + card bodies scroll only when the card is selected (Obsidian behaviour). + +## [0.8.0] - 2026-09-25 (unreleased) + +- New command: `/new canvas` — creates an empty `.canvas` file (in the Canvas + folder setting, or `Canvases/` by default; a folder prefix in the name works) + and opens it. Opening an existing name just opens it. +- All UI strings are in English for the community release. + +## [0.7.0] - 2026-09-25 (unreleased) + +- Copy & paste: ⌘C puts the selection on the system clipboard as a JSON Canvas + snippet, ⌘V pastes it at the view center with fresh ids — works across canvases + (and from any app that puts JSON Canvas on the clipboard). +- File cards refresh their note content when the canvas window regains focus, so + edits made in the NotePlan editor show up without reopening (throttled; never + interrupts an open card editor). +- F zooms to the selection; with nothing selected it fits the whole canvas. + +## [0.6.4] - 2026-09-25 (unreleased) + +Obsidian-style group interaction: + +- An unselected group's interior behaves like the background: rubber-band its + children, double-click to create a card inside; the group activates only by + its frame (~14 px, zoom-independent) or its label. +- Once selected, the group drags from anywhere inside its bounds. +- Starting any background action (click or rubber-band) drops the old selection + immediately; a rubber band that fully encloses a group selects the group too. + +## [0.6.0] - 2026-09-24 (unreleased) + +Obsidian parity push: + +- Drag-to-add: grab a palette button and drop it where the card should appear + (clicking still creates at the center). +- Note picker with autocomplete: adding a note card (or double-clicking a file + card's header) opens a search over all NotePlan notes — arrows + Enter to pick, + free text is kept as a raw path. +- Images: file cards pointing at png/jpg/gif/webp/svg render the image. +- Link cards embed a live website preview (iframe; sites that forbid embedding + via X-Frame-Options show blank — see README limitations). +- The canvas now opens in a split pane inside the main NotePlan window by default + (setting: "Open canvas in the main window"). + +## [0.5.0] - 2026-09-24 (unreleased) + +- Groups fixed: the edges SVG layer sat above groups and swallowed their clicks — + now clicks pass through, so groups select, drag, resize and rename normally. +- Group creation: ⌘G wraps the current selection in a group; the palette also has + a "+ група" button. +- New-card palette (top-left): create text, note (file) and group cards; the right + editor opens immediately after creation. (No separate "link" button — text cards + autolink URLs; the link node type is still read/rendered for Obsidian compatibility.) +- Obsidian-style selection: plain drag on the background draws a rubber-band + selection (Shift+drag pans; scroll/trackpad pans as before). The selection toolbar + gained a ⊞ button that wraps the selected cards in a group. +- Markdown: `- [ ]` / `- [x]` checkboxes render in cards. + +## [0.4.1] - 2026-09-24 (unreleased) + +- File cards render the linked note's CONTENT (like Obsidian) and edit it inline: + double-click the body to write — saved straight into the NotePlan note; if the note + doesn't exist yet, it is created on first save. Double-click the header to change + which file the card points to. ⌘+click opens the note. +- URL/path editors are now full-card inline inputs instead of a floating box. + +## [0.3.0] - 2026-09-21 (unreleased) + +Editing parity push (Obsidian-like): + +- Create text cards: double-click empty canvas. +- Draw edges: hover a card, drag from a side anchor dot onto another card. +- Selection: click, shift+click multi-select, shift+drag marquee; Esc clears. +- Delete (⌫), duplicate (⌘D), undo/redo (⌘Z / ⇧⌘Z). +- Resize: 8 handles on a single selected card. +- Colors: floating toolbar (6 presets + none) or keys 1–6 / 0; works for edges too. +- Edge labels: double-click an edge to add/edit; group labels editable the same way. +- Dragging a group moves everything fully inside it. +- Architecture: state now lives in the window (requiredFiles/canvasClient.js); the + plugin persists whole documents via 'saveCanvas' (debounced 400 ms, validated + before writing). Unknown fields from other tools survive. + +## [0.2.0] - 2026-09-20 (unreleased) + +- Editing: drag nodes to move them, double-click a text node to edit its markdown + (Cmd+Enter or click away to save, Esc to cancel); changes are written back to the + same `.canvas` file, staying compatible with Obsidian (unknown fields preserved). +- Note links (file nodes, `[[wikilinks]]`) now open via the np.Shared comms bridge — + `noteplan://` links are blocked inside the HTML window, so clicks are relayed to the + plugin, which calls `Editor.openNoteByTitle`. A message explains when the note + doesn't exist in NotePlan yet. +- Edges are drawn client-side and follow nodes live while dragging. + +## [0.1.0] - 2026-09-20 (unreleased) + +- Initial version: `/open canvas` command — read-only viewer for JSON Canvas (`.canvas`) + files with pan & zoom, all four node types (text, file, link, group), edges with labels + and arrows, preset + hex colors, clickable note links. diff --git a/lifeplayer67.CanvasView/README.md b/lifeplayer67.CanvasView/README.md new file mode 100644 index 000000000..60f535e8e --- /dev/null +++ b/lifeplayer67.CanvasView/README.md @@ -0,0 +1,72 @@ +# Canvas View plugin + +View [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files — the open format used by +Obsidian Canvas — right inside NotePlan. + +## Commands + +- `/new canvas` — creates an empty `.canvas` file (in the *Canvas folder* setting, or + `Canvases/` by default) and opens it. +- `/open canvas` — pick any indexed `.canvas` file from a list and open it in the + canvas editor (opens as a split pane inside the main window by default). + - Scroll / trackpad to pan, ⌘+scroll (or ctrl+scroll) to zoom, drag the background to pan, + double-click the background to re-fit. + - Drag a node to move it; double-click a text node to edit its markdown + (⌘+Enter or click away to save, Esc to cancel). Changes are saved back to the + same `.canvas` file and stay compatible with Obsidian. + - File and link cards: plain click selects, ⌘+click opens (the note in NotePlan / the + URL in your browser), double-click edits the file path or URL. `[[wikilinks]]` inside + text cards open on plain click. Notes are matched by title and must exist in NotePlan. + +## Editor cheat-sheet + +| Action | How | +|---|---| +| Create a card | double-click the background, or drag/click a palette button (`+ card`, `+ note`, `+ group`) | +| Connect cards | hover a card, drag from a side anchor dot onto another card | +| Select | click; ⇧+click adds; drag on the background rubber-bands (a band fully enclosing a group selects the group) | +| Move | drag a card; a selected group drags from anywhere inside; an unselected group only by its frame or label | +| Edit | double-click: text card → markdown, note card body → the note itself, note card header → note picker, group frame/label → rename, edge → label | +| Open | ↗ button or ⌘+click (note in NotePlan / URL in browser); links inside text open on plain click | +| Colors | toolbar swatches or keys 1–6, 0 clears | +| Group / duplicate / delete | ⌘G / ⌘D / ⌫ | +| Copy & paste | ⌘C / ⌘V (JSON Canvas snippet via the system clipboard — works across canvases) | +| Undo / redo | ⌘Z / ⇧⌘Z | +| Navigate | scroll pans, ⌘+scroll zooms, ⇧+drag pans, F zooms to selection, double-click background re-fits | + +## Settings + +- **Canvas folder** — absolute path to the folder holding your `.canvas` files (e.g. your + Obsidian vault). Relative paths given to the command are resolved against it. + +## Roadmap + +- v0.1: read-only viewer +- v0.2: editing — drag nodes, edit text, save back to the same `.canvas` file (this version) +- Creating/deleting nodes and edges; group dragging with children +- Importer command: convert a canvas into a structured NotePlan note + +## Platform limitations (API wishlist) + +Feature parity with Obsidian Canvas is limited by a few NotePlan plugin-API gaps. +Native support (or new APIs) for these would close the remaining distance: + +1. **No directory listing API.** `DataStore.loadData` reads a known path, but plugins + cannot enumerate folder contents — so an autocomplete over media/attachment files + (like Obsidian's "drag to add media") is not implementable. A `DataStore.listFiles(folder)` + would solve it. +2. **Website previews are best-effort.** Link cards embed sites via `' : '' + inner = '
🔗 ' + escapeHtml(url) + '↗
' + frame + } + return '
' + inner + anchors + '
' + } + + function hexAlpha(hex, a) { + var m = hex.match(/^#([0-9a-f]{6})$/i) + if (!m) return hex + var v = parseInt(m[1], 16) + return 'rgba(' + ((v >> 16) & 255) + ',' + ((v >> 8) & 255) + ',' + (v & 255) + ',' + a + ')' + } + + function renderScene() { + nodeById = {} + canvas.nodes.forEach(function (n) { nodeById[n.id] = n }) + var groups = canvas.nodes.filter(function (n) { return n.type === 'group' }).sort(function (a, b) { return b.width * b.height - a.width * a.height }) + var others = canvas.nodes.filter(function (n) { return n.type !== 'group' }) + world.innerHTML = groups.map(nodeHTML).join('') + '' + others.map(nodeHTML).join('') + svg = document.getElementById('edges') + drawEdges() + placeHandles() + updateToolbar() + } + + function drawEdges() { + var defs = '', paths = '' + canvas.edges.forEach(function (e) { + var from = nodeById[e.fromNode], to = nodeById[e.toNode] + if (!from || !to) return + var auto = autoSides(from, to) + var fs = e.fromSide || auto[0], ts = e.toSide || auto[1] + var p1 = anchorPoint(from, fs), p2 = anchorPoint(to, ts) + var dist = Math.hypot(p2.x - p1.x, p2.y - p1.y) + var bend = Math.max(40, Math.min(dist / 2, 200)) + var n1 = sideNormal(fs), n2 = sideNormal(ts) + var col = resolveColor(e.color) + var sel = selectedEdges.has(e.id) + var d = 'M ' + p1.x + ' ' + p1.y + ' C ' + (p1.x + n1.x * bend) + ' ' + (p1.y + n1.y * bend) + ', ' + (p2.x + n2.x * bend) + ' ' + (p2.y + n2.y * bend) + ', ' + p2.x + ' ' + p2.y + defs += '' + var ms = (e.fromEnd || 'none') === 'arrow' ? ' marker-start="url(#arrow-' + escapeHtml(e.id) + ')"' : '' + var me = (e.toEnd || 'arrow') === 'arrow' ? ' marker-end="url(#arrow-' + escapeHtml(e.id) + ')"' : '' + paths += '' + paths += '' + if (e.label) { + paths += '' + escapeHtml(e.label) + '' + } + }) + var pad = 100 + var ns = canvas.nodes + var minX = (ns.length ? Math.min.apply(null, ns.map(function (n) { return n.x })) : 0) - pad + var minY = (ns.length ? Math.min.apply(null, ns.map(function (n) { return n.y })) : 0) - pad + var maxX = (ns.length ? Math.max.apply(null, ns.map(function (n) { return n.x + n.width })) : 800) + pad + var maxY = (ns.length ? Math.max.apply(null, ns.map(function (n) { return n.y + n.height })) : 600) + pad + svg.style.left = minX + 'px' + svg.style.top = minY + 'px' + svg.style.width = maxX - minX + 'px' + svg.style.height = maxY - minY + 'px' + svg.setAttribute('viewBox', minX + ' ' + minY + ' ' + (maxX - minX) + ' ' + (maxY - minY)) + svg.innerHTML = '' + defs + '' + paths + (tempEdgeSVG || '') + return [minX, minY, maxX, maxY] + } + + // ---------- pan & zoom ---------- + + function apply() { + world.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + scale + ')' + zoomLabel.textContent = Math.round(scale * 100) + '%' + } + + function fit() { + // a restored split pane can report 0×0 for a few frames — wait until it has a size + if (!viewport.clientWidth || !viewport.clientHeight) { setTimeout(fit, 120); return } + var b = drawEdges() + var w = b[2] - b[0], h = b[3] - b[1] + scale = Math.min(viewport.clientWidth / w, viewport.clientHeight / h, 1.5) + tx = (viewport.clientWidth - w * scale) / 2 - b[0] * scale + ty = (viewport.clientHeight - h * scale) / 2 - b[1] * scale + apply() + } + + function toWorld(sx, sy) { + return { x: (sx - tx) / scale, y: (sy - ty) / scale } + } + + viewport.addEventListener('wheel', function (e) { + // editors always scroll natively; so does a scrollable card body + if (e.target.closest('textarea, input')) return + if (!e.ctrlKey && !e.metaKey) { + // Obsidian-style: a card's body scrolls only once the card is selected; + // otherwise two-finger scroll over a card pans the canvas as usual + var scrollable = e.target.closest('.file-content') + if (scrollable && scrollable.scrollHeight > scrollable.clientHeight) { + var card = scrollable.closest('.node') + if (card && selectedNodes.has(card.dataset.id)) return + } + } + e.preventDefault() + if (e.ctrlKey || e.metaKey) { + var factor = Math.exp(-e.deltaY * 0.01) + var ns = Math.min(Math.max(scale * factor, 0.05), 4) + tx = e.clientX - ((e.clientX - tx) / scale) * ns + ty = e.clientY - ((e.clientY - ty) / scale) * ns + scale = ns + } else { + tx -= e.deltaX + ty -= e.deltaY + } + apply() + }, { passive: false }) + + // ---------- selection ---------- + + function clearSelection() { + selectedNodes.clear() + selectedEdges.clear() + } + + function updateToolbar() { + toolbar.style.display = selectedNodes.size || selectedEdges.size ? 'flex' : 'none' + } + + /** + * Refresh selection visuals WITHOUT rebuilding the DOM. Rebuilding on mousedown + * destroys the element under the cursor, which kills the browser's dblclick — + * that's how text editing broke in 0.3.0. Only structural changes use renderScene(). + */ + function updateSelectionUI() { + world.querySelectorAll('.node').forEach(function (el) { + el.classList.toggle('selected', selectedNodes.has(el.dataset.id)) + }) + world.querySelectorAll('.handle').forEach(function (h) { h.remove() }) + placeHandles() + drawEdges() + updateToolbar() + } + + function applyColor(c) { + if (!selectedNodes.size && !selectedEdges.size) return + pushUndo() + canvas.nodes.forEach(function (n) { if (selectedNodes.has(n.id)) { if (c) n.color = c; else delete n.color } }) + canvas.edges.forEach(function (e) { if (selectedEdges.has(e.id)) { if (c) e.color = c; else delete e.color } }) + renderScene() + persist() + } + + function deleteSelection() { + if (!selectedNodes.size && !selectedEdges.size) return + pushUndo() + canvas.nodes = canvas.nodes.filter(function (n) { return !selectedNodes.has(n.id) }) + canvas.edges = canvas.edges.filter(function (e) { + return !selectedEdges.has(e.id) && !selectedNodes.has(e.fromNode) && !selectedNodes.has(e.toNode) + }) + clearSelection() + renderScene() + persist() + } + + /** ⌘C: put the selection on the system clipboard as a JSON Canvas snippet */ + function copySelection() { + if (!selectedNodes.size) return + var nodes = canvas.nodes.filter(function (n) { return selectedNodes.has(n.id) }) + var edges = canvas.edges.filter(function (e) { + return (selectedNodes.has(e.fromNode) && selectedNodes.has(e.toNode)) || selectedEdges.has(e.id) + }) + toPlugin('setClipboard', { text: JSON.stringify({ nodes: nodes, edges: edges }, null, '\t') }) + } + + /** ⌘V (after the plugin sends the clipboard back): paste a snippet at the view center */ + function pasteSnippet(text) { + var snippet + try { snippet = JSON.parse(text) } catch (e) { return } + if (!snippet || !Array.isArray(snippet.nodes) || !snippet.nodes.length) return + pushUndo() + var xs = snippet.nodes.map(function (n) { return n.x }), ys = snippet.nodes.map(function (n) { return n.y }) + var x2 = snippet.nodes.map(function (n) { return n.x + n.width }), y2 = snippet.nodes.map(function (n) { return n.y + n.height }) + var cx = (Math.min.apply(null, xs) + Math.max.apply(null, x2)) / 2 + var cy = (Math.min.apply(null, ys) + Math.max.apply(null, y2)) / 2 + var target = toWorld(viewport.clientWidth / 2, viewport.clientHeight / 2) + var dx = Math.round(target.x - cx), dy = Math.round(target.y - cy) + var idMap = {} + clearSelection() + snippet.nodes.forEach(function (n) { + var c = JSON.parse(JSON.stringify(n)) + idMap[n.id] = c.id = genId() + c.x += dx + c.y += dy + canvas.nodes.push(c) + selectedNodes.add(c.id) + }) + ;(snippet.edges || []).forEach(function (e) { + if (!idMap[e.fromNode] || !idMap[e.toNode]) return + var c = JSON.parse(JSON.stringify(e)) + c.id = genId() + c.fromNode = idMap[e.fromNode] + c.toNode = idMap[e.toNode] + canvas.edges.push(c) + }) + renderScene() + persist() + } + + /** F: zoom to the selection (or fit everything when nothing is selected) */ + function fitSelection() { + if (!selectedNodes.size) { fit(); return } + var sel = canvas.nodes.filter(function (n) { return selectedNodes.has(n.id) }) + var pad = 80 + var minX = Math.min.apply(null, sel.map(function (n) { return n.x })) - pad + var minY = Math.min.apply(null, sel.map(function (n) { return n.y })) - pad + var maxX = Math.max.apply(null, sel.map(function (n) { return n.x + n.width })) + pad + var maxY = Math.max.apply(null, sel.map(function (n) { return n.y + n.height })) + pad + var w = maxX - minX, h = maxY - minY + scale = Math.min(viewport.clientWidth / w, viewport.clientHeight / h, 2) + tx = (viewport.clientWidth - w * scale) / 2 - minX * scale + ty = (viewport.clientHeight - h * scale) / 2 - minY * scale + apply() + } + + function duplicateSelection() { + if (!selectedNodes.size) return + pushUndo() + var clones = [] + canvas.nodes.forEach(function (n) { + if (!selectedNodes.has(n.id)) return + var c = JSON.parse(JSON.stringify(n)) + c.id = genId() + c.x += 30 + c.y += 30 + clones.push(c) + }) + canvas.nodes = canvas.nodes.concat(clones) + clearSelection() + clones.forEach(function (c) { selectedNodes.add(c.id) }) + renderScene() + persist() + } + + // ---------- resize handles ---------- + + var HANDLES = [ + ['nw', 0, 0], ['n', 0.5, 0], ['ne', 1, 0], ['e', 1, 0.5], + ['se', 1, 1], ['s', 0.5, 1], ['sw', 0, 1], ['w', 0, 0.5], + ] + + function placeHandles() { + if (selectedNodes.size !== 1) return + var id = selectedNodes.values().next().value + var el = world.querySelector('.node[data-id="' + CSS.escape(id) + '"]') + if (!el) return + HANDLES.forEach(function (h) { + var d = document.createElement('div') + d.className = 'handle handle-' + h[0] + d.dataset.handle = h[0] + el.appendChild(d) + }) + } + + var resizing = null + function startResize(el, handle, e) { + var n = nodeById[el.dataset.id] + resizing = { node: n, handle: handle, startX: e.clientX, startY: e.clientY, ox: n.x, oy: n.y, ow: n.width, oh: n.height } + pushUndo() + } + + function doResize(e) { + var r = resizing + var dx = (e.clientX - r.startX) / scale + var dy = (e.clientY - r.startY) / scale + var h = r.handle + var MIN = 40 + if (h.indexOf('w') >= 0) { r.node.x = Math.round(Math.min(r.ox + dx, r.ox + r.ow - MIN)); r.node.width = Math.round(Math.max(MIN, r.ow - dx)) } + if (h.indexOf('e') >= 0) { r.node.width = Math.round(Math.max(MIN, r.ow + dx)) } + if (h.indexOf('n') >= 0 && h !== 'ne' && h !== 'nw') { r.node.y = Math.round(Math.min(r.oy + dy, r.oy + r.oh - MIN)); r.node.height = Math.round(Math.max(MIN, r.oh - dy)) } + if (h === 'ne' || h === 'nw') { r.node.y = Math.round(Math.min(r.oy + dy, r.oy + r.oh - MIN)); r.node.height = Math.round(Math.max(MIN, r.oh - dy)) } + if (h.indexOf('s') >= 0) { r.node.height = Math.round(Math.max(MIN, r.oh + dy)) } + var el = world.querySelector('.node[data-id="' + CSS.escape(r.node.id) + '"]') + el.style.left = r.node.x + 'px' + el.style.top = r.node.y + 'px' + el.style.width = r.node.width + 'px' + el.style.height = r.node.height + 'px' + drawEdges() + } + + // ---------- edge creation ---------- + + var edgeDraft = null + var tempEdgeSVG = '' + + function startEdge(nodeEl, side) { + edgeDraft = { fromNode: nodeEl.dataset.id, fromSide: side } + viewport.classList.add('linking') + } + + function updateEdgeDraft(e) { + var from = nodeById[edgeDraft.fromNode] + var p1 = anchorPoint(from, edgeDraft.fromSide) + var w = toWorld(e.clientX, e.clientY) + tempEdgeSVG = '' + drawEdges() + } + + function finishEdge(e) { + var el = e.target.closest && e.target.closest('.node') + tempEdgeSVG = '' + viewport.classList.remove('linking') + if (el && el.dataset.id !== edgeDraft.fromNode) { + var to = nodeById[el.dataset.id] + var w = toWorld(e.clientX, e.clientY) + pushUndo() + canvas.edges.push({ + id: genId(), + fromNode: edgeDraft.fromNode, + fromSide: edgeDraft.fromSide, + toNode: to.id, + toSide: nearestSide(to, w.x, w.y), + toEnd: 'arrow', + }) + persist() + } + edgeDraft = null + drawEdges() + } + + // ---------- text / label editing ---------- + + /** Focus a card editor with the caret at `caret`, scrolled into view (WebKit + * does not scroll a textarea to a programmatic selection by itself). */ + function focusEditorAtCaret(ta, caret) { + ta.focus() + ta.setSelectionRange(caret, caret) + var lineIndex = (ta.value.slice(0, caret).match(/\n/g) || []).length + var lineHeight = parseFloat(getComputedStyle(ta).lineHeight) || 18 + ta.scrollTop = Math.max(0, lineIndex * lineHeight - ta.clientHeight / 2) + } + + /** + * Best-effort caret placement: map the double-click point in the RENDERED card + * to an offset in the raw markdown by matching a snippet of the clicked text node. + * Falls back to 0 (top of the editor) when the snippet isn't found verbatim + * (e.g. the click landed on styled text whose source contains markdown syntax). + */ + function caretIndexFromClick(ev, source) { + try { + if (!ev || !document.caretRangeFromPoint) return 0 + var range = document.caretRangeFromPoint(ev.clientX, ev.clientY) + if (!range || !range.startContainer || range.startContainer.nodeType !== 3) return 0 + var txt = range.startContainer.textContent || '' + var off = range.startOffset + for (var len = 24; len >= 4; len -= 5) { + var start = Math.max(0, off - Math.floor(len / 2)) + var snip = txt.slice(start, start + len) + if (snip.trim().length < 3) continue + var idx = source.indexOf(snip) + if (idx >= 0) return idx + (off - start) + } + return 0 + } catch (e) { + return 0 + } + } + + function editTextNode(el, ev) { + if (el.querySelector('textarea')) return + console.log('canvasClient: editTextNode open for ' + el.dataset.id) + var n = nodeById[el.dataset.id] + var caret = caretIndexFromClick(ev, n.text || '') + var content = el.querySelector('.content') + var ta = document.createElement('textarea') + ta.value = n.text || '' + if (content) content.style.display = 'none' + el.appendChild(ta) + setTimeout(function () { focusEditorAtCaret(ta, caret) }, 0) + var cancelled = false + function commit() { + if (cancelled) return + if (ta.value !== (n.text || '')) { + pushUndo() + n.text = ta.value + persist() + } + renderScene() + } + ta.addEventListener('blur', commit) + ta.addEventListener('keydown', function (ev) { + if ((ev.metaKey || ev.ctrlKey) && ev.key === 'Enter') ta.blur() + if (ev.key === 'Escape') { cancelled = true; renderScene() } + ev.stopPropagation() + }) + } + + /** Edit the underlying value of a file node ('file' path) or link node ('url') */ + function editCardValue(el, prop) { + var n = nodeById[el.dataset.id] + prompt2(el, n[prop] || '', function (v) { + if (v === (n[prop] || '')) return + pushUndo() + n[prop] = v + renderScene() + persist() + }, true) + } + + /** + * Link cards edit in a multiline textarea (Enter = newline, like text cards). + * If the committed value is a bare URL the node stays a 'link'; anything richer + * (prose, several lines, code) converts the node to a 'text' card, where URLs + * autolink — so nothing gets painted as one giant hyperlink. + */ + function editLinkCard(el) { + if (el.querySelector('textarea')) return + var n = nodeById[el.dataset.id] + var ta = document.createElement('textarea') + ta.value = n.url || '' + el.appendChild(ta) + setTimeout(function () { ta.focus() }, 0) + var cancelled = false + function commit() { + if (cancelled) return + var v = ta.value.trim() + if (v !== (n.url || '') && v !== '') { + pushUndo() + if (/^https?:\/\/\S+$/.test(v)) { + n.url = v + } else { + n.type = 'text' + n.text = ta.value + delete n.url + } + persist() + } + renderScene() + } + ta.addEventListener('blur', commit) + ta.addEventListener('keydown', function (ev) { + if ((ev.metaKey || ev.ctrlKey) && ev.key === 'Enter') ta.blur() + if (ev.key === 'Escape') { cancelled = true; renderScene() } + ev.stopPropagation() + }) + } + + /** Edit the CONTENT of the note behind a file card; saves into the NotePlan note itself */ + function editNoteContent(el, ev) { + var n = nodeById[el.dataset.id] + var fc = fileContents[n.id] + var caret = ev ? caretIndexFromClick(ev, (fc && fc.content) || '') : 0 + if (!fc) { + var base = String(n.file || '').split('/').pop() + fc = fileContents[n.id] = { found: false, title: base.replace(/\.[^.]+$/, '') } + } + if (el.querySelector('textarea')) return + console.log('canvasClient: editNoteContent open for ' + n.id) + var ta = document.createElement('textarea') + ta.value = fc.content || '' + el.appendChild(ta) + setTimeout(function () { focusEditorAtCaret(ta, caret) }, 0) + var cancelled = false + function commit() { + if (cancelled) return + if (ta.value !== (fc.content || '')) { + fc.content = ta.value + fc.found = true // first write creates the note plugin-side + toPlugin('saveNote', { title: fc.title, content: ta.value }) + } + renderScene() + } + ta.addEventListener('blur', commit) + ta.addEventListener('keydown', function (ev) { + if ((ev.metaKey || ev.ctrlKey) && ev.key === 'Enter') ta.blur() + if (ev.key === 'Escape') { cancelled = true; renderScene() } + ev.stopPropagation() + }) + } + + function editGroupLabel(el) { + var n = nodeById[el.dataset.id] + prompt2(el, n.label || '', function (v) { + pushUndo() + n.label = v + renderScene() + persist() + }) + } + + function editEdgeLabel(edgeId) { + var e = null + canvas.edges.forEach(function (x) { if (x.id === edgeId) e = x }) + if (!e) return + var from = nodeById[e.fromNode], to = nodeById[e.toNode] + if (!from || !to) return + var p1 = anchorPoint(from, e.fromSide || autoSides(from, to)[0]) + var p2 = anchorPoint(to, e.toSide || autoSides(from, to)[1]) + var input = document.createElement('input') + input.className = 'label-input' + input.value = e.label || '' + input.style.left = (p1.x + p2.x) / 2 - 80 + 'px' + input.style.top = (p1.y + p2.y) / 2 - 14 + 'px' + world.appendChild(input) + setTimeout(function () { input.focus() }, 0) + function commit() { + pushUndo() + if (input.value.trim() === '') delete e.label + else e.label = input.value + input.remove() + renderScene() + persist() + } + input.addEventListener('blur', commit) + input.addEventListener('keydown', function (ev) { + if (ev.key === 'Enter') input.blur() + if (ev.key === 'Escape') { input.removeEventListener('blur', commit); input.remove() } + ev.stopPropagation() + }) + } + + function prompt2(anchorEl, initial, onCommit, inset) { + var input = document.createElement('input') + input.className = inset ? 'card-input' : 'label-input' + input.value = initial + anchorEl.appendChild(input) + setTimeout(function () { input.focus() }, 0) + function commit() { + onCommit(input.value) + input.remove() + } + input.addEventListener('blur', commit) + input.addEventListener('keydown', function (ev) { + if (ev.key === 'Enter') input.blur() + if (ev.key === 'Escape') { input.removeEventListener('blur', commit); input.remove() } + ev.stopPropagation() + }) + } + + // ---------- node creation ---------- + + var NEW_NODE_DEFAULTS = { + text: { width: 250, height: 60, props: { text: '' } }, + file: { width: 320, height: 180, props: { file: '' } }, + link: { width: 300, height: 60, props: { url: '' } }, + group: { width: 500, height: 360, props: { label: '' } }, + } + + function createNode(kind, wx, wy) { + var def = NEW_NODE_DEFAULTS[kind] + pushUndo() + var n = { id: genId(), type: kind, x: Math.round(wx - def.width / 2), y: Math.round(wy - def.height / 2), width: def.width, height: def.height } + Object.keys(def.props).forEach(function (k) { n[k] = def.props[k] }) + canvas.nodes.push(n) + clearSelection() + selectedNodes.add(n.id) + renderScene() + persist() + var el = world.querySelector('.node[data-id="' + CSS.escape(n.id) + '"]') + if (!el) return + if (kind === 'text') editTextNode(el) + else if (kind === 'file') openNotePicker(el) + else if (kind === 'link') editLinkCard(el) + else if (kind === 'group') editGroupLabel(el) + } + + /** Autocomplete picker over the NotePlan note index; free text = raw file path */ + function openNotePicker(el) { + if (el.querySelector('.picker-input')) return + var n = nodeById[el.dataset.id] + var input = document.createElement('input') + input.className = 'card-input picker-input' + input.value = n.file || '' + input.placeholder = 'Note title or path…' + var list = document.createElement('div') + list.className = 'picker' + el.appendChild(input) + el.appendChild(list) + setTimeout(function () { input.focus(); input.select() }, 0) + var items = [], active = 0 + function refresh() { + items = filterNotes(noteIndex, input.value) + active = 0 + list.innerHTML = items + .map(function (it, i) { + return '
' + escapeHtml(it.t) + '' + escapeHtml(it.f) + '
' + }) + .join('') + } + function highlight() { + list.querySelectorAll('.picker-row').forEach(function (row, i) { row.classList.toggle('active', i === active) }) + } + function close() { input.remove(); list.remove() } + function commit(filename) { + var v = filename != null ? filename : input.value.trim() + close() + if (v === '' || v === (n.file || '')) { renderScene(); return } + pushUndo() + n.file = v + delete fileContents[n.id] + renderScene() + persist() + var t = v.split('/').pop().replace(/\.[^.]+$/, '') + if (!/\.(png|jpe?g|gif|webp|svg)$/i.test(v)) toPlugin('getNoteContent', { id: n.id, title: t }) + } + input.addEventListener('input', refresh) + input.addEventListener('keydown', function (ev) { + if (ev.key === 'ArrowDown') { active = Math.min(active + 1, items.length - 1); highlight(); ev.preventDefault() } + else if (ev.key === 'ArrowUp') { active = Math.max(active - 1, 0); highlight(); ev.preventDefault() } + else if (ev.key === 'Enter') { commit(items[active] ? items[active].f : null) } + else if (ev.key === 'Escape') { close(); renderScene() } + ev.stopPropagation() + }) + input.addEventListener('blur', function () { + // slight delay so a click on a picker row lands before we close + setTimeout(function () { if (document.body.contains(input)) { close(); renderScene() } }, 150) + }) + list.addEventListener('mousedown', function (ev) { + var row = ev.target.closest('.picker-row') + if (row) { ev.preventDefault(); commit(items[Number(row.dataset.i)].f) } + }) + refresh() + } + + function createTextNode(wx, wy) { + createNode('text', wx, wy) + } + + /** ⌘G: wrap the selected nodes in a new group */ + function groupSelection() { + if (!selectedNodes.size) return + var xs = [], ys = [], x2 = [], y2 = [] + canvas.nodes.forEach(function (n) { + if (!selectedNodes.has(n.id)) return + xs.push(n.x); ys.push(n.y); x2.push(n.x + n.width); y2.push(n.y + n.height) + }) + if (!xs.length) return + pushUndo() + var PAD = 30 + var g = { + id: genId(), + type: 'group', + label: '', + x: Math.min.apply(null, xs) - PAD, + y: Math.min.apply(null, ys) - PAD, + width: Math.max.apply(null, x2) - Math.min.apply(null, xs) + PAD * 2, + height: Math.max.apply(null, y2) - Math.min.apply(null, ys) + PAD * 2, + } + canvas.nodes.push(g) + clearSelection() + selectedNodes.add(g.id) + renderScene() + persist() + var el = world.querySelector('.node[data-id="' + CSS.escape(g.id) + '"]') + if (el) editGroupLabel(el) + } + + // ---------- drag / pan / marquee state machine ---------- + + var mode = null // 'pan' | 'drag' | 'marquee' | 'resize' | 'link' + var dragNodes = [] // [{node, ox, oy}] + var px = 0, py = 0, moved = false + var marqueeEl = null, marqueeStart = null + + var placeKind = null, ghostEl = null, interiorGroupClick = false + + viewport.addEventListener('mousedown', function (e) { + // Obsidian-style drag-to-add: grab a palette button and drop it on the canvas + var paletteBtn = e.target.closest('#palette button') + if (paletteBtn) { + mode = 'place' + placeKind = paletteBtn.dataset.new + px = e.clientX; py = e.clientY; moved = false + ghostEl = document.createElement('div') + ghostEl.id = 'ghost' + ghostEl.textContent = paletteBtn.textContent + ghostEl.style.left = e.clientX + 'px' + ghostEl.style.top = e.clientY + 'px' + document.body.appendChild(ghostEl) + e.preventDefault() + return + } + if (e.target.closest('textarea, input, #toolbar, #palette')) return + // inline links inside text content keep native click behavior and never start a drag; + // card-level links (file/link nodes) fall through so the card can be selected/dragged + if (e.target.closest('.content a')) return + px = e.clientX + py = e.clientY + moved = false + + // Double-click routing lives here (not in a 'dblclick' listener): e.detail counts + // rapid clicks by time/position, so it survives any DOM rebuild in between. + if (e.detail >= 2) { + // Without this, mousedown's default focus handling lands AFTER we focus the + // editor textarea, blurring it in the same frame — the editor dies instantly. + e.preventDefault() + var tEl = e.target.closest('.node.text') + if (tEl) { editTextNode(tEl, e); return } + var fEl = e.target.closest('.node.file') + if (fEl) { + if (e.target.closest('.file-head')) openNotePicker(fEl) + else editNoteContent(fEl, e) + return + } + var lEl = e.target.closest('.node.link') + if (lEl) { editLinkCard(lEl); return } + var gEl = e.target.closest('.node.group') + if (gEl) { + // frame/label double-click renames the group; its interior acts like the + // background — a new text card appears right there, inside the group + var gdp = toWorld(e.clientX, e.clientY) + var gdn = nodeById[gEl.dataset.id] + if (e.target.closest('.group-label') || (gdn && isGroupBorderHit(gdn, gdp.x, gdp.y, 14 / scale))) editGroupLabel(gEl) + else createTextNode(gdp.x, gdp.y) + return + } + var eEl = e.target.closest('.edge-hit, .edge-label, .edge-line') + if (eEl) { editEdgeLabel(eEl.dataset.edgeId); return } + if (!e.target.closest('.node')) { + var wpt = toWorld(e.clientX, e.clientY) + createTextNode(wpt.x, wpt.y) + return + } + return + } + + var handle = e.target.closest('.handle') + if (handle) { + mode = 'resize' + startResize(handle.parentElement, handle.dataset.handle, e) + e.preventDefault() + return + } + var anchor = e.target.closest('.anchor') + if (anchor) { + mode = 'link' + startEdge(anchor.parentElement, anchor.dataset.side) + e.preventDefault() + return + } + var nodeEl = e.target.closest('.node') + // An UNSELECTED group's empty interior behaves like the background (so you can + // rubber-band its children); it activates only by its frame or label. Once the + // group IS selected, dragging anywhere inside its bounds moves it. + interiorGroupClick = false + if (nodeEl && nodeEl.classList.contains('group') && !e.target.closest('.group-label') && !selectedNodes.has(nodeEl.dataset.id)) { + var gp = toWorld(e.clientX, e.clientY) + var gn = nodeById[nodeEl.dataset.id] + if (gn && !isGroupBorderHit(gn, gp.x, gp.y, 14 / scale)) { + nodeEl = null + interiorGroupClick = true + } + } + if (nodeEl) { + var n = nodeById[nodeEl.dataset.id] + if (!e.shiftKey && !selectedNodes.has(n.id)) { + clearSelection() + selectedNodes.add(n.id) + } else if (e.shiftKey) { + if (selectedNodes.has(n.id)) selectedNodes.delete(n.id) + else selectedNodes.add(n.id) + } + selectedEdges.clear() + updateSelectionUI() + mode = 'drag' + dragNodes = [] + var members = new Set(selectedNodes) + // dragging a group carries every node fully inside it + canvas.nodes.forEach(function (g) { + if (g.type === 'group' && selectedNodes.has(g.id)) { + canvas.nodes.forEach(function (m) { if (nodeInsideGroup(m, g)) members.add(m.id) }) + } + }) + canvas.nodes.forEach(function (m) { + if (members.has(m.id)) dragNodes.push({ node: m, ox: m.x, oy: m.y }) + }) + return + } + // background + var edgeHit = e.target.closest && e.target.closest('.edge-hit, .edge-line, .edge-label') + if (edgeHit) { + var id = edgeHit.dataset.edgeId + if (!e.shiftKey) clearSelection() + if (selectedEdges.has(id)) selectedEdges.delete(id) + else selectedEdges.add(id) + updateSelectionUI() + return + } + // Obsidian-style: plain drag on the background draws a selection rectangle; + // panning is scroll/trackpad (or hold Shift to drag-pan) + if (e.shiftKey) { + mode = 'pan' + viewport.classList.add('panning') + } else { + mode = 'marquee' + // starting a rubber-band (or a plain background click) drops the old selection right away + if (selectedNodes.size || selectedEdges.size) { + clearSelection() + updateSelectionUI() + } + marqueeStart = { x: e.clientX, y: e.clientY } + marqueeEl = document.createElement('div') + marqueeEl.id = 'marquee' + viewport.appendChild(marqueeEl) + } + }) + + window.addEventListener('mousemove', function (e) { + if (!mode) return + var dx = e.clientX - px, dy = e.clientY - py + if (Math.abs(e.clientX - px) + Math.abs(e.clientY - py) > 2) moved = true + if (mode === 'pan') { + tx += dx + ty += dy + px = e.clientX + py = e.clientY + apply() + } else if (mode === 'drag') { + if (!moved) return + if (!dragUndoPushed) { pushUndo(); dragUndoPushed = true } + dragNodes.forEach(function (d) { + d.node.x = Math.round(d.ox + (e.clientX - pxStart) / scale) + d.node.y = Math.round(d.oy + (e.clientY - pyStart) / scale) + var el = world.querySelector('.node[data-id="' + CSS.escape(d.node.id) + '"]') + if (el) { el.style.left = d.node.x + 'px'; el.style.top = d.node.y + 'px' } + }) + drawEdges() + } else if (mode === 'resize') { + doResize(e) + } else if (mode === 'link') { + updateEdgeDraft(e) + } else if (mode === 'place') { + ghostEl.style.left = e.clientX + 'px' + ghostEl.style.top = e.clientY + 'px' + } else if (mode === 'marquee') { + var x1 = Math.min(marqueeStart.x, e.clientX), y1 = Math.min(marqueeStart.y, e.clientY) + var x2 = Math.max(marqueeStart.x, e.clientX), y2 = Math.max(marqueeStart.y, e.clientY) + marqueeEl.style.left = x1 + 'px' + marqueeEl.style.top = y1 + 'px' + marqueeEl.style.width = x2 - x1 + 'px' + marqueeEl.style.height = y2 - y1 + 'px' + } + }) + + // pointer position at drag start (used for cumulative node drags) + var pxStart = 0, pyStart = 0, dragUndoPushed = false + viewport.addEventListener('mousedown', function (e) { + pxStart = e.clientX + pyStart = e.clientY + }, true) + + window.addEventListener('mouseup', function (e) { + if (mode === 'place') { + if (ghostEl) ghostEl.remove() + ghostEl = null + if (moved && !e.target.closest('#palette')) { + var wp = toWorld(e.clientX, e.clientY) + createNode(placeKind, wp.x, wp.y) + } else if (!moved) { + var cc = toWorld(viewport.clientWidth / 2, viewport.clientHeight / 2) + createNode(placeKind, cc.x, cc.y) + } + placeKind = null + mode = null + return + } + if (mode === 'drag' && moved) persist() + if (mode === 'link' && edgeDraft) finishEdge(e) + if (mode === 'marquee') { + if (moved) { + var r = marqueeEl.getBoundingClientRect() + var w1 = toWorld(r.left, r.top), w2 = toWorld(r.right, r.bottom) + canvas.nodes.forEach(function (n) { + if (n.type === 'group') { + // a group joins the selection only when the rubber band encloses it fully — + // intersection would grab the surrounding group on every band drawn inside it + if (n.x >= w1.x && n.y >= w1.y && n.x + n.width <= w2.x && n.y + n.height <= w2.y) selectedNodes.add(n.id) + return + } + if (n.x < w2.x && n.x + n.width > w1.x && n.y < w2.y && n.y + n.height > w1.y) selectedNodes.add(n.id) + }) + } else if (interiorGroupClick || !e.target.closest('.node, a, #toolbar, #palette, .edge-hit, .edge-line')) { + clearSelection() // plain click on the background (or a group's interior) deselects + } + marqueeEl.remove() + marqueeEl = null + updateSelectionUI() + } + if (mode === 'pan' && !moved && !e.target.closest('.node, a, #toolbar, .edge-hit, .edge-line')) { + clearSelection() + updateSelectionUI() + } + dragUndoPushed = false + viewport.classList.remove('panning') + mode = null + if (pendingRender && !document.querySelector('.node textarea, .card-input, .label-input')) { + pendingRender = false + renderScene() + } + }) + + // ---------- clicks: note links & toolbar ---------- + + document.addEventListener('click', function (e) { + var link = e.target.closest('a') + if (link && link.closest('.node')) { + // explicit open buttons (↗ on link cards) open on plain click + if (link.classList.contains('ext-open')) { + if (moved) e.preventDefault() + return + } + var noteTitle = link.dataset.noteTitle + var isInline = !!link.closest('.content') + if (isInline) { + // wikilinks / md-links inside a text card: plain click follows them + if (noteTitle) { e.preventDefault(); toPlugin('openNote', { title: noteTitle }) } + return + } + // card-level link (file/link node): plain click only selects; ⌘+click opens + if (moved || (!e.metaKey && !e.ctrlKey)) { e.preventDefault(); return } + if (noteTitle) { e.preventDefault(); toPlugin('openNote', { title: noteTitle }) } + return // external URL cards keep the default ⌘+click open (target=_blank) + } + var openBtn = e.target.closest('.open-btn') + if (openBtn && !moved) { + toPlugin('openNote', { title: openBtn.closest('.node').dataset.noteTitle }) + return + } + var fileCard = e.target.closest('.node.file') + if (fileCard && (e.metaKey || e.ctrlKey) && !moved) { + toPlugin('openNote', { title: fileCard.dataset.noteTitle }) + return + } + var swatch = e.target.closest('#toolbar .swatch') + if (swatch) { applyColor(swatch.dataset.color || null); return } + if (e.target.closest('#toolbar .tb-group')) { groupSelection(); return } + if (e.target.closest('#toolbar .tb-delete')) deleteSelection() + }) + + // ---------- keyboard ---------- + + window.addEventListener('keydown', function (e) { + if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return + var meta = e.metaKey || e.ctrlKey + if (meta && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); return } + if (meta && (e.key === 'Z' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); redo(); return } + if (meta && e.key === 'd') { e.preventDefault(); duplicateSelection(); return } + if (meta && e.key === 'g') { e.preventDefault(); groupSelection(); return } + if (meta && e.key === 'c') { e.preventDefault(); copySelection(); return } + if (meta && e.key === 'v') { e.preventDefault(); toPlugin('getClipboard', {}); return } + if (e.key === 'f' && !meta) { fitSelection(); return } + if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); deleteSelection(); return } + if (e.key === 'Escape') { clearSelection(); updateSelectionUI(); return } + if (/^[1-6]$/.test(e.key)) { applyColor(e.key); return } + if (e.key === '0') applyColor(null) + }) + + // Re-pull note contents when the user comes back to the canvas, so file cards + // don't go stale after edits in the NotePlan editor. In the embedded split view + // the window 'focus' event is unreliable, so the pointer ENTERING the pane is + // the primary trigger (that's exactly "I clicked over to the canvas"). + var lastRefresh = 0 + var pendingRender = false + function refreshNoteCards() { + var now = Date.now() + if (now - lastRefresh < 2000) return + if (document.querySelector('.node textarea, .card-input, .label-input')) return + lastRefresh = now + var items = [] + canvas.nodes.forEach(function (n) { + var fc = fileContents[n.id] + if (n.type === 'file' && fc && fc.found && !fc.media) items.push({ id: n.id, title: fc.title }) + }) + if (items.length) { + console.log('canvasClient: refreshing ' + items.length + ' note card(s)') + toPlugin('refreshNotes', { items: items }) + } + } + viewport.addEventListener('mouseenter', refreshNoteCards) + window.addEventListener('focus', refreshNoteCards) + + window.addEventListener('resize', fit) + + // ---------- boot ---------- + console.log('canvasClient v0.9.5 booted: ' + canvas.nodes.length + ' nodes, ' + canvas.edges.length + ' edges') + renderScene() + fit() +})() diff --git a/lifeplayer67.CanvasView/requiredFiles/showdown.min.js b/lifeplayer67.CanvasView/requiredFiles/showdown.min.js new file mode 100644 index 000000000..fb64306b9 --- /dev/null +++ b/lifeplayer67.CanvasView/requiredFiles/showdown.min.js @@ -0,0 +1,3 @@ +/*! showdown v 1.9.1 - 02-11-2019 */ +(function(){function e(e){"use strict";var r={omitExtraWLInCodeBlocks:{defaultValue:!1,describe:"Omit the default extra whiteline added to code blocks",type:"boolean"},noHeaderId:{defaultValue:!1,describe:"Turn on/off generated header id",type:"boolean"},prefixHeaderId:{defaultValue:!1,describe:"Add a prefix to the generated header ids. Passing a string will prefix that string to the header id. Setting to true will add a generic 'section-' prefix",type:"string"},rawPrefixHeaderId:{defaultValue:!1,describe:'Setting this option to true will prevent showdown from modifying the prefix. This might result in malformed IDs (if, for instance, the " char is used in the prefix)',type:"boolean"},ghCompatibleHeaderId:{defaultValue:!1,describe:"Generate header ids compatible with github style (spaces are replaced with dashes, a bunch of non alphanumeric chars are removed)",type:"boolean"},rawHeaderId:{defaultValue:!1,describe:"Remove only spaces, ' and \" from generated header ids (including prefixes), replacing them with dashes (-). WARNING: This might result in malformed ids",type:"boolean"},headerLevelStart:{defaultValue:!1,describe:"The header blocks level start",type:"integer"},parseImgDimensions:{defaultValue:!1,describe:"Turn on/off image dimension parsing",type:"boolean"},simplifiedAutoLink:{defaultValue:!1,describe:"Turn on/off GFM autolink style",type:"boolean"},excludeTrailingPunctuationFromURLs:{defaultValue:!1,describe:"Excludes trailing punctuation from links generated with autoLinking",type:"boolean"},literalMidWordUnderscores:{defaultValue:!1,describe:"Parse midword underscores as literal underscores",type:"boolean"},literalMidWordAsterisks:{defaultValue:!1,describe:"Parse midword asterisks as literal asterisks",type:"boolean"},strikethrough:{defaultValue:!1,describe:"Turn on/off strikethrough support",type:"boolean"},tables:{defaultValue:!1,describe:"Turn on/off tables support",type:"boolean"},tablesHeaderId:{defaultValue:!1,describe:"Add an id to table headers",type:"boolean"},ghCodeBlocks:{defaultValue:!0,describe:"Turn on/off GFM fenced code blocks support",type:"boolean"},tasklists:{defaultValue:!1,describe:"Turn on/off GFM tasklist support",type:"boolean"},smoothLivePreview:{defaultValue:!1,describe:"Prevents weird effects in live previews due to incomplete input",type:"boolean"},smartIndentationFix:{defaultValue:!1,description:"Tries to smartly fix indentation in es6 strings",type:"boolean"},disableForced4SpacesIndentedSublists:{defaultValue:!1,description:"Disables the requirement of indenting nested sublists by 4 spaces",type:"boolean"},simpleLineBreaks:{defaultValue:!1,description:"Parses simple line breaks as
(GFM Style)",type:"boolean"},requireSpaceBeforeHeadingText:{defaultValue:!1,description:"Makes adding a space between `#` and the header text mandatory (GFM Style)",type:"boolean"},ghMentions:{defaultValue:!1,description:"Enables github @mentions",type:"boolean"},ghMentionsLink:{defaultValue:"https://github.com/{u}",description:"Changes the link generated by @mentions. Only applies if ghMentions option is enabled.",type:"string"},encodeEmails:{defaultValue:!0,description:"Encode e-mail addresses through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities",type:"boolean"},openLinksInNewWindow:{defaultValue:!1,description:"Open all links in new windows",type:"boolean"},backslashEscapesHTMLTags:{defaultValue:!1,description:"Support for HTML Tag escaping. ex:
foo
",type:"boolean"},emoji:{defaultValue:!1,description:"Enable emoji support. Ex: `this is a :smile: emoji`",type:"boolean"},underline:{defaultValue:!1,description:"Enable support for underline. Syntax is double or triple underscores: `__underline word__`. With this option enabled, underscores no longer parses into `` and ``",type:"boolean"},completeHTMLDocument:{defaultValue:!1,description:"Outputs a complete html document, including ``, `` and `` tags",type:"boolean"},metadata:{defaultValue:!1,description:"Enable support for document metadata (defined at the top of the document between `«««` and `»»»` or between `---` and `---`).",type:"boolean"},splitAdjacentBlockquotes:{defaultValue:!1,description:"Split adjacent blockquote blocks",type:"boolean"}};if(!1===e)return JSON.parse(JSON.stringify(r));var t={};for(var a in r)r.hasOwnProperty(a)&&(t[a]=r[a].defaultValue);return t}function r(e,r){"use strict";var t=r?"Error in "+r+" extension->":"Error in unnamed extension",n={valid:!0,error:""};a.helper.isArray(e)||(e=[e]);for(var s=0;s").replace(/&/g,"&")};var c=function(e,r,t,a){"use strict";var n,s,o,i,l,c=a||"",u=c.indexOf("g")>-1,d=new RegExp(r+"|"+t,"g"+c.replace(/g/g,"")),p=new RegExp(r,c.replace(/g/g,"")),h=[];do{for(n=0;o=d.exec(e);)if(p.test(o[0]))n++||(i=(s=d.lastIndex)-o[0].length);else if(n&&!--n){l=o.index+o[0].length;var _={left:{start:i,end:s},match:{start:s,end:o.index},right:{start:o.index,end:l},wholeMatch:{start:i,end:l}};if(h.push(_),!u)return h}}while(n&&(d.lastIndex=s));return h};a.helper.matchRecursiveRegExp=function(e,r,t,a){"use strict";for(var n=c(e,r,t,a),s=[],o=0;o0){var d=[];0!==i[0].wholeMatch.start&&d.push(e.slice(0,i[0].wholeMatch.start));for(var p=0;p=0?n+(t||0):n},a.helper.splitAtIndex=function(e,r){"use strict";if(!a.helper.isString(e))throw"InvalidArgumentError: first parameter of showdown.helper.regexIndexOf function must be a string";return[e.substring(0,r),e.substring(r)]},a.helper.encodeEmailAddress=function(e){"use strict";var r=[function(e){return"&#"+e.charCodeAt(0)+";"},function(e){return"&#x"+e.charCodeAt(0).toString(16)+";"},function(e){return e}];return e=e.replace(/./g,function(e){if("@"===e)e=r[Math.floor(2*Math.random())](e);else{var t=Math.random();e=t>.9?r[2](e):t>.45?r[1](e):r[0](e)}return e})},a.helper.padEnd=function(e,r,t){"use strict";return r>>=0,t=String(t||" "),e.length>r?String(e):((r-=e.length)>t.length&&(t+=t.repeat(r/t.length)),String(e)+t.slice(0,r))},"undefined"==typeof console&&(console={warn:function(e){"use strict";alert(e)},log:function(e){"use strict";alert(e)},error:function(e){"use strict";throw e}}),a.helper.regexes={asteriskDashAndColon:/([*_:~])/g},a.helper.emojis={"+1":"👍","-1":"👎",100:"💯",1234:"🔢","1st_place_medal":"🥇","2nd_place_medal":"🥈","3rd_place_medal":"🥉","8ball":"🎱",a:"🅰️",ab:"🆎",abc:"🔤",abcd:"🔡",accept:"🉑",aerial_tramway:"🚡",airplane:"✈️",alarm_clock:"⏰",alembic:"⚗️",alien:"👽",ambulance:"🚑",amphora:"🏺",anchor:"⚓️",angel:"👼",anger:"💢",angry:"😠",anguished:"😧",ant:"🐜",apple:"🍎",aquarius:"♒️",aries:"♈️",arrow_backward:"◀️",arrow_double_down:"⏬",arrow_double_up:"⏫",arrow_down:"⬇️",arrow_down_small:"🔽",arrow_forward:"▶️",arrow_heading_down:"⤵️",arrow_heading_up:"⤴️",arrow_left:"⬅️",arrow_lower_left:"↙️",arrow_lower_right:"↘️",arrow_right:"➡️",arrow_right_hook:"↪️",arrow_up:"⬆️",arrow_up_down:"↕️",arrow_up_small:"🔼",arrow_upper_left:"↖️",arrow_upper_right:"↗️",arrows_clockwise:"🔃",arrows_counterclockwise:"🔄",art:"🎨",articulated_lorry:"🚛",artificial_satellite:"🛰",astonished:"😲",athletic_shoe:"👟",atm:"🏧",atom_symbol:"⚛️",avocado:"🥑",b:"🅱️",baby:"👶",baby_bottle:"🍼",baby_chick:"🐤",baby_symbol:"🚼",back:"🔙",bacon:"🥓",badminton:"🏸",baggage_claim:"🛄",baguette_bread:"🥖",balance_scale:"⚖️",balloon:"🎈",ballot_box:"🗳",ballot_box_with_check:"☑️",bamboo:"🎍",banana:"🍌",bangbang:"‼️",bank:"🏦",bar_chart:"📊",barber:"💈",baseball:"⚾️",basketball:"🏀",basketball_man:"⛹️",basketball_woman:"⛹️‍♀️",bat:"🦇",bath:"🛀",bathtub:"🛁",battery:"🔋",beach_umbrella:"🏖",bear:"🐻",bed:"🛏",bee:"🐝",beer:"🍺",beers:"🍻",beetle:"🐞",beginner:"🔰",bell:"🔔",bellhop_bell:"🛎",bento:"🍱",biking_man:"🚴",bike:"🚲",biking_woman:"🚴‍♀️",bikini:"👙",biohazard:"☣️",bird:"🐦",birthday:"🎂",black_circle:"⚫️",black_flag:"🏴",black_heart:"🖤",black_joker:"🃏",black_large_square:"⬛️",black_medium_small_square:"◾️",black_medium_square:"◼️",black_nib:"✒️",black_small_square:"▪️",black_square_button:"🔲",blonde_man:"👱",blonde_woman:"👱‍♀️",blossom:"🌼",blowfish:"🐡",blue_book:"📘",blue_car:"🚙",blue_heart:"💙",blush:"😊",boar:"🐗",boat:"⛵️",bomb:"💣",book:"📖",bookmark:"🔖",bookmark_tabs:"📑",books:"📚",boom:"💥",boot:"👢",bouquet:"💐",bowing_man:"🙇",bow_and_arrow:"🏹",bowing_woman:"🙇‍♀️",bowling:"🎳",boxing_glove:"🥊",boy:"👦",bread:"🍞",bride_with_veil:"👰",bridge_at_night:"🌉",briefcase:"💼",broken_heart:"💔",bug:"🐛",building_construction:"🏗",bulb:"💡",bullettrain_front:"🚅",bullettrain_side:"🚄",burrito:"🌯",bus:"🚌",business_suit_levitating:"🕴",busstop:"🚏",bust_in_silhouette:"👤",busts_in_silhouette:"👥",butterfly:"🦋",cactus:"🌵",cake:"🍰",calendar:"📆",call_me_hand:"🤙",calling:"📲",camel:"🐫",camera:"📷",camera_flash:"📸",camping:"🏕",cancer:"♋️",candle:"🕯",candy:"🍬",canoe:"🛶",capital_abcd:"🔠",capricorn:"♑️",car:"🚗",card_file_box:"🗃",card_index:"📇",card_index_dividers:"🗂",carousel_horse:"🎠",carrot:"🥕",cat:"🐱",cat2:"🐈",cd:"💿",chains:"⛓",champagne:"🍾",chart:"💹",chart_with_downwards_trend:"📉",chart_with_upwards_trend:"📈",checkered_flag:"🏁",cheese:"🧀",cherries:"🍒",cherry_blossom:"🌸",chestnut:"🌰",chicken:"🐔",children_crossing:"🚸",chipmunk:"🐿",chocolate_bar:"🍫",christmas_tree:"🎄",church:"⛪️",cinema:"🎦",circus_tent:"🎪",city_sunrise:"🌇",city_sunset:"🌆",cityscape:"🏙",cl:"🆑",clamp:"🗜",clap:"👏",clapper:"🎬",classical_building:"🏛",clinking_glasses:"🥂",clipboard:"📋",clock1:"🕐",clock10:"🕙",clock1030:"🕥",clock11:"🕚",clock1130:"🕦",clock12:"🕛",clock1230:"🕧",clock130:"🕜",clock2:"🕑",clock230:"🕝",clock3:"🕒",clock330:"🕞",clock4:"🕓",clock430:"🕟",clock5:"🕔",clock530:"🕠",clock6:"🕕",clock630:"🕡",clock7:"🕖",clock730:"🕢",clock8:"🕗",clock830:"🕣",clock9:"🕘",clock930:"🕤",closed_book:"📕",closed_lock_with_key:"🔐",closed_umbrella:"🌂",cloud:"☁️",cloud_with_lightning:"🌩",cloud_with_lightning_and_rain:"⛈",cloud_with_rain:"🌧",cloud_with_snow:"🌨",clown_face:"🤡",clubs:"♣️",cocktail:"🍸",coffee:"☕️",coffin:"⚰️",cold_sweat:"😰",comet:"☄️",computer:"💻",computer_mouse:"🖱",confetti_ball:"🎊",confounded:"😖",confused:"😕",congratulations:"㊗️",construction:"🚧",construction_worker_man:"👷",construction_worker_woman:"👷‍♀️",control_knobs:"🎛",convenience_store:"🏪",cookie:"🍪",cool:"🆒",policeman:"👮",copyright:"©️",corn:"🌽",couch_and_lamp:"🛋",couple:"👫",couple_with_heart_woman_man:"💑",couple_with_heart_man_man:"👨‍❤️‍👨",couple_with_heart_woman_woman:"👩‍❤️‍👩",couplekiss_man_man:"👨‍❤️‍💋‍👨",couplekiss_man_woman:"💏",couplekiss_woman_woman:"👩‍❤️‍💋‍👩",cow:"🐮",cow2:"🐄",cowboy_hat_face:"🤠",crab:"🦀",crayon:"🖍",credit_card:"💳",crescent_moon:"🌙",cricket:"🏏",crocodile:"🐊",croissant:"🥐",crossed_fingers:"🤞",crossed_flags:"🎌",crossed_swords:"⚔️",crown:"👑",cry:"😢",crying_cat_face:"😿",crystal_ball:"🔮",cucumber:"🥒",cupid:"💘",curly_loop:"➰",currency_exchange:"💱",curry:"🍛",custard:"🍮",customs:"🛃",cyclone:"🌀",dagger:"🗡",dancer:"💃",dancing_women:"👯",dancing_men:"👯‍♂️",dango:"🍡",dark_sunglasses:"🕶",dart:"🎯",dash:"💨",date:"📅",deciduous_tree:"🌳",deer:"🦌",department_store:"🏬",derelict_house:"🏚",desert:"🏜",desert_island:"🏝",desktop_computer:"🖥",male_detective:"🕵️",diamond_shape_with_a_dot_inside:"💠",diamonds:"♦️",disappointed:"😞",disappointed_relieved:"😥",dizzy:"💫",dizzy_face:"😵",do_not_litter:"🚯",dog:"🐶",dog2:"🐕",dollar:"💵",dolls:"🎎",dolphin:"🐬",door:"🚪",doughnut:"🍩",dove:"🕊",dragon:"🐉",dragon_face:"🐲",dress:"👗",dromedary_camel:"🐪",drooling_face:"🤤",droplet:"💧",drum:"🥁",duck:"🦆",dvd:"📀","e-mail":"📧",eagle:"🦅",ear:"👂",ear_of_rice:"🌾",earth_africa:"🌍",earth_americas:"🌎",earth_asia:"🌏",egg:"🥚",eggplant:"🍆",eight_pointed_black_star:"✴️",eight_spoked_asterisk:"✳️",electric_plug:"🔌",elephant:"🐘",email:"✉️",end:"🔚",envelope_with_arrow:"📩",euro:"💶",european_castle:"🏰",european_post_office:"🏤",evergreen_tree:"🌲",exclamation:"❗️",expressionless:"😑",eye:"👁",eye_speech_bubble:"👁‍🗨",eyeglasses:"👓",eyes:"👀",face_with_head_bandage:"🤕",face_with_thermometer:"🤒",fist_oncoming:"👊",factory:"🏭",fallen_leaf:"🍂",family_man_woman_boy:"👪",family_man_boy:"👨‍👦",family_man_boy_boy:"👨‍👦‍👦",family_man_girl:"👨‍👧",family_man_girl_boy:"👨‍👧‍👦",family_man_girl_girl:"👨‍👧‍👧",family_man_man_boy:"👨‍👨‍👦",family_man_man_boy_boy:"👨‍👨‍👦‍👦",family_man_man_girl:"👨‍👨‍👧",family_man_man_girl_boy:"👨‍👨‍👧‍👦",family_man_man_girl_girl:"👨‍👨‍👧‍👧",family_man_woman_boy_boy:"👨‍👩‍👦‍👦",family_man_woman_girl:"👨‍👩‍👧",family_man_woman_girl_boy:"👨‍👩‍👧‍👦",family_man_woman_girl_girl:"👨‍👩‍👧‍👧",family_woman_boy:"👩‍👦",family_woman_boy_boy:"👩‍👦‍👦",family_woman_girl:"👩‍👧",family_woman_girl_boy:"👩‍👧‍👦",family_woman_girl_girl:"👩‍👧‍👧",family_woman_woman_boy:"👩‍👩‍👦",family_woman_woman_boy_boy:"👩‍👩‍👦‍👦",family_woman_woman_girl:"👩‍👩‍👧",family_woman_woman_girl_boy:"👩‍👩‍👧‍👦",family_woman_woman_girl_girl:"👩‍👩‍👧‍👧",fast_forward:"⏩",fax:"📠",fearful:"😨",feet:"🐾",female_detective:"🕵️‍♀️",ferris_wheel:"🎡",ferry:"⛴",field_hockey:"🏑",file_cabinet:"🗄",file_folder:"📁",film_projector:"📽",film_strip:"🎞",fire:"🔥",fire_engine:"🚒",fireworks:"🎆",first_quarter_moon:"🌓",first_quarter_moon_with_face:"🌛",fish:"🐟",fish_cake:"🍥",fishing_pole_and_fish:"🎣",fist_raised:"✊",fist_left:"🤛",fist_right:"🤜",flags:"🎏",flashlight:"🔦",fleur_de_lis:"⚜️",flight_arrival:"🛬",flight_departure:"🛫",floppy_disk:"💾",flower_playing_cards:"🎴",flushed:"😳",fog:"🌫",foggy:"🌁",football:"🏈",footprints:"👣",fork_and_knife:"🍴",fountain:"⛲️",fountain_pen:"🖋",four_leaf_clover:"🍀",fox_face:"🦊",framed_picture:"🖼",free:"🆓",fried_egg:"🍳",fried_shrimp:"🍤",fries:"🍟",frog:"🐸",frowning:"😦",frowning_face:"☹️",frowning_man:"🙍‍♂️",frowning_woman:"🙍",middle_finger:"🖕",fuelpump:"⛽️",full_moon:"🌕",full_moon_with_face:"🌝",funeral_urn:"⚱️",game_die:"🎲",gear:"⚙️",gem:"💎",gemini:"♊️",ghost:"👻",gift:"🎁",gift_heart:"💝",girl:"👧",globe_with_meridians:"🌐",goal_net:"🥅",goat:"🐐",golf:"⛳️",golfing_man:"🏌️",golfing_woman:"🏌️‍♀️",gorilla:"🦍",grapes:"🍇",green_apple:"🍏",green_book:"📗",green_heart:"💚",green_salad:"🥗",grey_exclamation:"❕",grey_question:"❔",grimacing:"😬",grin:"😁",grinning:"😀",guardsman:"💂",guardswoman:"💂‍♀️",guitar:"🎸",gun:"🔫",haircut_woman:"💇",haircut_man:"💇‍♂️",hamburger:"🍔",hammer:"🔨",hammer_and_pick:"⚒",hammer_and_wrench:"🛠",hamster:"🐹",hand:"✋",handbag:"👜",handshake:"🤝",hankey:"💩",hatched_chick:"🐥",hatching_chick:"🐣",headphones:"🎧",hear_no_evil:"🙉",heart:"❤️",heart_decoration:"💟",heart_eyes:"😍",heart_eyes_cat:"😻",heartbeat:"💓",heartpulse:"💗",hearts:"♥️",heavy_check_mark:"✔️",heavy_division_sign:"➗",heavy_dollar_sign:"💲",heavy_heart_exclamation:"❣️",heavy_minus_sign:"➖",heavy_multiplication_x:"✖️",heavy_plus_sign:"➕",helicopter:"🚁",herb:"🌿",hibiscus:"🌺",high_brightness:"🔆",high_heel:"👠",hocho:"🔪",hole:"🕳",honey_pot:"🍯",horse:"🐴",horse_racing:"🏇",hospital:"🏥",hot_pepper:"🌶",hotdog:"🌭",hotel:"🏨",hotsprings:"♨️",hourglass:"⌛️",hourglass_flowing_sand:"⏳",house:"🏠",house_with_garden:"🏡",houses:"🏘",hugs:"🤗",hushed:"😯",ice_cream:"🍨",ice_hockey:"🏒",ice_skate:"⛸",icecream:"🍦",id:"🆔",ideograph_advantage:"🉐",imp:"👿",inbox_tray:"📥",incoming_envelope:"📨",tipping_hand_woman:"💁",information_source:"ℹ️",innocent:"😇",interrobang:"⁉️",iphone:"📱",izakaya_lantern:"🏮",jack_o_lantern:"🎃",japan:"🗾",japanese_castle:"🏯",japanese_goblin:"👺",japanese_ogre:"👹",jeans:"👖",joy:"😂",joy_cat:"😹",joystick:"🕹",kaaba:"🕋",key:"🔑",keyboard:"⌨️",keycap_ten:"🔟",kick_scooter:"🛴",kimono:"👘",kiss:"💋",kissing:"😗",kissing_cat:"😽",kissing_closed_eyes:"😚",kissing_heart:"😘",kissing_smiling_eyes:"😙",kiwi_fruit:"🥝",koala:"🐨",koko:"🈁",label:"🏷",large_blue_circle:"🔵",large_blue_diamond:"🔷",large_orange_diamond:"🔶",last_quarter_moon:"🌗",last_quarter_moon_with_face:"🌜",latin_cross:"✝️",laughing:"😆",leaves:"🍃",ledger:"📒",left_luggage:"🛅",left_right_arrow:"↔️",leftwards_arrow_with_hook:"↩️",lemon:"🍋",leo:"♌️",leopard:"🐆",level_slider:"🎚",libra:"♎️",light_rail:"🚈",link:"🔗",lion:"🦁",lips:"👄",lipstick:"💄",lizard:"🦎",lock:"🔒",lock_with_ink_pen:"🔏",lollipop:"🍭",loop:"➿",loud_sound:"🔊",loudspeaker:"📢",love_hotel:"🏩",love_letter:"💌",low_brightness:"🔅",lying_face:"🤥",m:"Ⓜ️",mag:"🔍",mag_right:"🔎",mahjong:"🀄️",mailbox:"📫",mailbox_closed:"📪",mailbox_with_mail:"📬",mailbox_with_no_mail:"📭",man:"👨",man_artist:"👨‍🎨",man_astronaut:"👨‍🚀",man_cartwheeling:"🤸‍♂️",man_cook:"👨‍🍳",man_dancing:"🕺",man_facepalming:"🤦‍♂️",man_factory_worker:"👨‍🏭",man_farmer:"👨‍🌾",man_firefighter:"👨‍🚒",man_health_worker:"👨‍⚕️",man_in_tuxedo:"🤵",man_judge:"👨‍⚖️",man_juggling:"🤹‍♂️",man_mechanic:"👨‍🔧",man_office_worker:"👨‍💼",man_pilot:"👨‍✈️",man_playing_handball:"🤾‍♂️",man_playing_water_polo:"🤽‍♂️",man_scientist:"👨‍🔬",man_shrugging:"🤷‍♂️",man_singer:"👨‍🎤",man_student:"👨‍🎓",man_teacher:"👨‍🏫",man_technologist:"👨‍💻",man_with_gua_pi_mao:"👲",man_with_turban:"👳",tangerine:"🍊",mans_shoe:"👞",mantelpiece_clock:"🕰",maple_leaf:"🍁",martial_arts_uniform:"🥋",mask:"😷",massage_woman:"💆",massage_man:"💆‍♂️",meat_on_bone:"🍖",medal_military:"🎖",medal_sports:"🏅",mega:"📣",melon:"🍈",memo:"📝",men_wrestling:"🤼‍♂️",menorah:"🕎",mens:"🚹",metal:"🤘",metro:"🚇",microphone:"🎤",microscope:"🔬",milk_glass:"🥛",milky_way:"🌌",minibus:"🚐",minidisc:"💽",mobile_phone_off:"📴",money_mouth_face:"🤑",money_with_wings:"💸",moneybag:"💰",monkey:"🐒",monkey_face:"🐵",monorail:"🚝",moon:"🌔",mortar_board:"🎓",mosque:"🕌",motor_boat:"🛥",motor_scooter:"🛵",motorcycle:"🏍",motorway:"🛣",mount_fuji:"🗻",mountain:"⛰",mountain_biking_man:"🚵",mountain_biking_woman:"🚵‍♀️",mountain_cableway:"🚠",mountain_railway:"🚞",mountain_snow:"🏔",mouse:"🐭",mouse2:"🐁",movie_camera:"🎥",moyai:"🗿",mrs_claus:"🤶",muscle:"💪",mushroom:"🍄",musical_keyboard:"🎹",musical_note:"🎵",musical_score:"🎼",mute:"🔇",nail_care:"💅",name_badge:"📛",national_park:"🏞",nauseated_face:"🤢",necktie:"👔",negative_squared_cross_mark:"❎",nerd_face:"🤓",neutral_face:"😐",new:"🆕",new_moon:"🌑",new_moon_with_face:"🌚",newspaper:"📰",newspaper_roll:"🗞",next_track_button:"⏭",ng:"🆖",no_good_man:"🙅‍♂️",no_good_woman:"🙅",night_with_stars:"🌃",no_bell:"🔕",no_bicycles:"🚳",no_entry:"⛔️",no_entry_sign:"🚫",no_mobile_phones:"📵",no_mouth:"😶",no_pedestrians:"🚷",no_smoking:"🚭","non-potable_water":"🚱",nose:"👃",notebook:"📓",notebook_with_decorative_cover:"📔",notes:"🎶",nut_and_bolt:"🔩",o:"⭕️",o2:"🅾️",ocean:"🌊",octopus:"🐙",oden:"🍢",office:"🏢",oil_drum:"🛢",ok:"🆗",ok_hand:"👌",ok_man:"🙆‍♂️",ok_woman:"🙆",old_key:"🗝",older_man:"👴",older_woman:"👵",om:"🕉",on:"🔛",oncoming_automobile:"🚘",oncoming_bus:"🚍",oncoming_police_car:"🚔",oncoming_taxi:"🚖",open_file_folder:"📂",open_hands:"👐",open_mouth:"😮",open_umbrella:"☂️",ophiuchus:"⛎",orange_book:"📙",orthodox_cross:"☦️",outbox_tray:"📤",owl:"🦉",ox:"🐂",package:"📦",page_facing_up:"📄",page_with_curl:"📃",pager:"📟",paintbrush:"🖌",palm_tree:"🌴",pancakes:"🥞",panda_face:"🐼",paperclip:"📎",paperclips:"🖇",parasol_on_ground:"⛱",parking:"🅿️",part_alternation_mark:"〽️",partly_sunny:"⛅️",passenger_ship:"🛳",passport_control:"🛂",pause_button:"⏸",peace_symbol:"☮️",peach:"🍑",peanuts:"🥜",pear:"🍐",pen:"🖊",pencil2:"✏️",penguin:"🐧",pensive:"😔",performing_arts:"🎭",persevere:"😣",person_fencing:"🤺",pouting_woman:"🙎",phone:"☎️",pick:"⛏",pig:"🐷",pig2:"🐖",pig_nose:"🐽",pill:"💊",pineapple:"🍍",ping_pong:"🏓",pisces:"♓️",pizza:"🍕",place_of_worship:"🛐",plate_with_cutlery:"🍽",play_or_pause_button:"⏯",point_down:"👇",point_left:"👈",point_right:"👉",point_up:"☝️",point_up_2:"👆",police_car:"🚓",policewoman:"👮‍♀️",poodle:"🐩",popcorn:"🍿",post_office:"🏣",postal_horn:"📯",postbox:"📮",potable_water:"🚰",potato:"🥔",pouch:"👝",poultry_leg:"🍗",pound:"💷",rage:"😡",pouting_cat:"😾",pouting_man:"🙎‍♂️",pray:"🙏",prayer_beads:"📿",pregnant_woman:"🤰",previous_track_button:"⏮",prince:"🤴",princess:"👸",printer:"🖨",purple_heart:"💜",purse:"👛",pushpin:"📌",put_litter_in_its_place:"🚮",question:"❓",rabbit:"🐰",rabbit2:"🐇",racehorse:"🐎",racing_car:"🏎",radio:"📻",radio_button:"🔘",radioactive:"☢️",railway_car:"🚃",railway_track:"🛤",rainbow:"🌈",rainbow_flag:"🏳️‍🌈",raised_back_of_hand:"🤚",raised_hand_with_fingers_splayed:"🖐",raised_hands:"🙌",raising_hand_woman:"🙋",raising_hand_man:"🙋‍♂️",ram:"🐏",ramen:"🍜",rat:"🐀",record_button:"⏺",recycle:"♻️",red_circle:"🔴",registered:"®️",relaxed:"☺️",relieved:"😌",reminder_ribbon:"🎗",repeat:"🔁",repeat_one:"🔂",rescue_worker_helmet:"⛑",restroom:"🚻",revolving_hearts:"💞",rewind:"⏪",rhinoceros:"🦏",ribbon:"🎀",rice:"🍚",rice_ball:"🍙",rice_cracker:"🍘",rice_scene:"🎑",right_anger_bubble:"🗯",ring:"💍",robot:"🤖",rocket:"🚀",rofl:"🤣",roll_eyes:"🙄",roller_coaster:"🎢",rooster:"🐓",rose:"🌹",rosette:"🏵",rotating_light:"🚨",round_pushpin:"📍",rowing_man:"🚣",rowing_woman:"🚣‍♀️",rugby_football:"🏉",running_man:"🏃",running_shirt_with_sash:"🎽",running_woman:"🏃‍♀️",sa:"🈂️",sagittarius:"♐️",sake:"🍶",sandal:"👡",santa:"🎅",satellite:"📡",saxophone:"🎷",school:"🏫",school_satchel:"🎒",scissors:"✂️",scorpion:"🦂",scorpius:"♏️",scream:"😱",scream_cat:"🙀",scroll:"📜",seat:"💺",secret:"㊙️",see_no_evil:"🙈",seedling:"🌱",selfie:"🤳",shallow_pan_of_food:"🥘",shamrock:"☘️",shark:"🦈",shaved_ice:"🍧",sheep:"🐑",shell:"🐚",shield:"🛡",shinto_shrine:"⛩",ship:"🚢",shirt:"👕",shopping:"🛍",shopping_cart:"🛒",shower:"🚿",shrimp:"🦐",signal_strength:"📶",six_pointed_star:"🔯",ski:"🎿",skier:"⛷",skull:"💀",skull_and_crossbones:"☠️",sleeping:"😴",sleeping_bed:"🛌",sleepy:"😪",slightly_frowning_face:"🙁",slightly_smiling_face:"🙂",slot_machine:"🎰",small_airplane:"🛩",small_blue_diamond:"🔹",small_orange_diamond:"🔸",small_red_triangle:"🔺",small_red_triangle_down:"🔻",smile:"😄",smile_cat:"😸",smiley:"😃",smiley_cat:"😺",smiling_imp:"😈",smirk:"😏",smirk_cat:"😼",smoking:"🚬",snail:"🐌",snake:"🐍",sneezing_face:"🤧",snowboarder:"🏂",snowflake:"❄️",snowman:"⛄️",snowman_with_snow:"☃️",sob:"😭",soccer:"⚽️",soon:"🔜",sos:"🆘",sound:"🔉",space_invader:"👾",spades:"♠️",spaghetti:"🍝",sparkle:"❇️",sparkler:"🎇",sparkles:"✨",sparkling_heart:"💖",speak_no_evil:"🙊",speaker:"🔈",speaking_head:"🗣",speech_balloon:"💬",speedboat:"🚤",spider:"🕷",spider_web:"🕸",spiral_calendar:"🗓",spiral_notepad:"🗒",spoon:"🥄",squid:"🦑",stadium:"🏟",star:"⭐️",star2:"🌟",star_and_crescent:"☪️",star_of_david:"✡️",stars:"🌠",station:"🚉",statue_of_liberty:"🗽",steam_locomotive:"🚂",stew:"🍲",stop_button:"⏹",stop_sign:"🛑",stopwatch:"⏱",straight_ruler:"📏",strawberry:"🍓",stuck_out_tongue:"😛",stuck_out_tongue_closed_eyes:"😝",stuck_out_tongue_winking_eye:"😜",studio_microphone:"🎙",stuffed_flatbread:"🥙",sun_behind_large_cloud:"🌥",sun_behind_rain_cloud:"🌦",sun_behind_small_cloud:"🌤",sun_with_face:"🌞",sunflower:"🌻",sunglasses:"😎",sunny:"☀️",sunrise:"🌅",sunrise_over_mountains:"🌄",surfing_man:"🏄",surfing_woman:"🏄‍♀️",sushi:"🍣",suspension_railway:"🚟",sweat:"😓",sweat_drops:"💦",sweat_smile:"😅",sweet_potato:"🍠",swimming_man:"🏊",swimming_woman:"🏊‍♀️",symbols:"🔣",synagogue:"🕍",syringe:"💉",taco:"🌮",tada:"🎉",tanabata_tree:"🎋",taurus:"♉️",taxi:"🚕",tea:"🍵",telephone_receiver:"📞",telescope:"🔭",tennis:"🎾",tent:"⛺️",thermometer:"🌡",thinking:"🤔",thought_balloon:"💭",ticket:"🎫",tickets:"🎟",tiger:"🐯",tiger2:"🐅",timer_clock:"⏲",tipping_hand_man:"💁‍♂️",tired_face:"😫",tm:"™️",toilet:"🚽",tokyo_tower:"🗼",tomato:"🍅",tongue:"👅",top:"🔝",tophat:"🎩",tornado:"🌪",trackball:"🖲",tractor:"🚜",traffic_light:"🚥",train:"🚋",train2:"🚆",tram:"🚊",triangular_flag_on_post:"🚩",triangular_ruler:"📐",trident:"🔱",triumph:"😤",trolleybus:"🚎",trophy:"🏆",tropical_drink:"🍹",tropical_fish:"🐠",truck:"🚚",trumpet:"🎺",tulip:"🌷",tumbler_glass:"🥃",turkey:"🦃",turtle:"🐢",tv:"📺",twisted_rightwards_arrows:"🔀",two_hearts:"💕",two_men_holding_hands:"👬",two_women_holding_hands:"👭",u5272:"🈹",u5408:"🈴",u55b6:"🈺",u6307:"🈯️",u6708:"🈷️",u6709:"🈶",u6e80:"🈵",u7121:"🈚️",u7533:"🈸",u7981:"🈲",u7a7a:"🈳",umbrella:"☔️",unamused:"😒",underage:"🔞",unicorn:"🦄",unlock:"🔓",up:"🆙",upside_down_face:"🙃",v:"✌️",vertical_traffic_light:"🚦",vhs:"📼",vibration_mode:"📳",video_camera:"📹",video_game:"🎮",violin:"🎻",virgo:"♍️",volcano:"🌋",volleyball:"🏐",vs:"🆚",vulcan_salute:"🖖",walking_man:"🚶",walking_woman:"🚶‍♀️",waning_crescent_moon:"🌘",waning_gibbous_moon:"🌖",warning:"⚠️",wastebasket:"🗑",watch:"⌚️",water_buffalo:"🐃",watermelon:"🍉",wave:"👋",wavy_dash:"〰️",waxing_crescent_moon:"🌒",wc:"🚾",weary:"😩",wedding:"💒",weight_lifting_man:"🏋️",weight_lifting_woman:"🏋️‍♀️",whale:"🐳",whale2:"🐋",wheel_of_dharma:"☸️",wheelchair:"♿️",white_check_mark:"✅",white_circle:"⚪️",white_flag:"🏳️",white_flower:"💮",white_large_square:"⬜️",white_medium_small_square:"◽️",white_medium_square:"◻️",white_small_square:"▫️",white_square_button:"🔳",wilted_flower:"🥀",wind_chime:"🎐",wind_face:"🌬",wine_glass:"🍷",wink:"😉",wolf:"🐺",woman:"👩",woman_artist:"👩‍🎨",woman_astronaut:"👩‍🚀",woman_cartwheeling:"🤸‍♀️",woman_cook:"👩‍🍳",woman_facepalming:"🤦‍♀️",woman_factory_worker:"👩‍🏭",woman_farmer:"👩‍🌾",woman_firefighter:"👩‍🚒",woman_health_worker:"👩‍⚕️",woman_judge:"👩‍⚖️",woman_juggling:"🤹‍♀️",woman_mechanic:"👩‍🔧",woman_office_worker:"👩‍💼",woman_pilot:"👩‍✈️",woman_playing_handball:"🤾‍♀️",woman_playing_water_polo:"🤽‍♀️",woman_scientist:"👩‍🔬",woman_shrugging:"🤷‍♀️",woman_singer:"👩‍🎤",woman_student:"👩‍🎓",woman_teacher:"👩‍🏫",woman_technologist:"👩‍💻",woman_with_turban:"👳‍♀️",womans_clothes:"👚",womans_hat:"👒",women_wrestling:"🤼‍♀️",womens:"🚺",world_map:"🗺",worried:"😟",wrench:"🔧",writing_hand:"✍️",x:"❌",yellow_heart:"💛",yen:"💴",yin_yang:"☯️",yum:"😋",zap:"⚡️",zipper_mouth_face:"🤐",zzz:"💤",octocat:':octocat:',showdown:"S"},a.Converter=function(e){"use strict";function t(e,t){if(t=t||null,a.helper.isString(e)){if(e=a.helper.stdExtName(e),t=e,a.extensions[e])return console.warn("DEPRECATION WARNING: "+e+" is an old extension that uses a deprecated loading method.Please inform the developer that the extension should be updated!"),void function(e,t){"function"==typeof e&&(e=e(new a.Converter));a.helper.isArray(e)||(e=[e]);var n=r(e,t);if(!n.valid)throw Error(n.error);for(var s=0;s[ \t]+¨NBSP;<"),!r){if(!window||!window.document)throw new Error("HTMLParser is undefined. If in a webworker or nodejs environment, you need to provide a WHATWG DOM and HTML such as JSDOM");r=window.document}var n=r.createElement("div");n.innerHTML=e;var s={preList:function(e){for(var r=e.querySelectorAll("pre"),t=[],n=0;n'}else t.push(r[n].innerHTML),r[n].innerHTML="",r[n].setAttribute("prenum",n.toString());return t}(n)};t(n);for(var o=n.childNodes,i="",l=0;l? ?(['"].*['"])?\)$/m)>-1)o="";else if(!o){if(s||(s=n.toLowerCase().replace(/ ?\n/g," ")),o="#"+s,a.helper.isUndefined(t.gUrls[s]))return e;o=t.gUrls[s],a.helper.isUndefined(t.gTitles[s])||(c=t.gTitles[s])}var u='"};return e=(e=t.converter._dispatch("anchors.before",e,r,t)).replace(/\[((?:\[[^\]]*]|[^\[\]])*)] ?(?:\n *)?\[(.*?)]()()()()/g,n),e=e.replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g,n),e=e.replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]??(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g,n),e=e.replace(/\[([^\[\]]+)]()()()()()/g,n),r.ghMentions&&(e=e.replace(/(^|\s)(\\)?(@([a-z\d]+(?:[a-z\d.-]+?[a-z\d]+)*))/gim,function(e,t,n,s,o){if("\\"===n)return t+s;if(!a.helper.isString(r.ghMentionsLink))throw new Error("ghMentionsLink option must be a string");var i=r.ghMentionsLink.replace(/\{u}/g,o),l="";return r.openLinksInNewWindow&&(l=' rel="noopener noreferrer" target="¨E95Eblank"'),t+'"+s+""})),e=t.converter._dispatch("anchors.after",e,r,t)});var u=/([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+?\.[^'">\s]+?)()(\1)?(?=\s|$)(?!["<>])/gi,d=/([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?,()\[\]])?(\1)?(?=\s|$)(?!["<>])/gi,p=/()<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)()>()/gi,h=/(^|\s)(?:mailto:)?([A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gim,_=/<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,g=function(e){"use strict";return function(r,t,n,s,o,i,l){var c=n=n.replace(a.helper.regexes.asteriskDashAndColon,a.helper.escapeCharactersCallback),u="",d="",p=t||"",h=l||"";return/^www\./i.test(n)&&(n=n.replace(/^www\./i,"http://www.")),e.excludeTrailingPunctuationFromURLs&&i&&(u=i),e.openLinksInNewWindow&&(d=' rel="noopener noreferrer" target="¨E95Eblank"'),p+'"+c+""+u+h}},m=function(e,r){"use strict";return function(t,n,s){var o="mailto:";return n=n||"",s=a.subParser("unescapeSpecialChars")(s,e,r),e.encodeEmails?(o=a.helper.encodeEmailAddress(o+s),s=a.helper.encodeEmailAddress(s)):o+=s,n+''+s+""}};a.subParser("autoLinks",function(e,r,t){"use strict";return e=t.converter._dispatch("autoLinks.before",e,r,t),e=e.replace(p,g(r)),e=e.replace(_,m(r,t)),e=t.converter._dispatch("autoLinks.after",e,r,t)}),a.subParser("simplifiedAutoLinks",function(e,r,t){"use strict";return r.simplifiedAutoLink?(e=t.converter._dispatch("simplifiedAutoLinks.before",e,r,t),e=r.excludeTrailingPunctuationFromURLs?e.replace(d,g(r)):e.replace(u,g(r)),e=e.replace(h,m(r,t)),e=t.converter._dispatch("simplifiedAutoLinks.after",e,r,t)):e}),a.subParser("blockGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("blockGamut.before",e,r,t),e=a.subParser("blockQuotes")(e,r,t),e=a.subParser("headers")(e,r,t),e=a.subParser("horizontalRule")(e,r,t),e=a.subParser("lists")(e,r,t),e=a.subParser("codeBlocks")(e,r,t),e=a.subParser("tables")(e,r,t),e=a.subParser("hashHTMLBlocks")(e,r,t),e=a.subParser("paragraphs")(e,r,t),e=t.converter._dispatch("blockGamut.after",e,r,t)}),a.subParser("blockQuotes",function(e,r,t){"use strict";e=t.converter._dispatch("blockQuotes.before",e,r,t),e+="\n\n";var n=/(^ {0,3}>[ \t]?.+\n(.+\n)*\n*)+/gm;return r.splitAdjacentBlockquotes&&(n=/^ {0,3}>[\s\S]*?(?:\n\n)/gm),e=e.replace(n,function(e){return e=e.replace(/^[ \t]*>[ \t]?/gm,""),e=e.replace(/¨0/g,""),e=e.replace(/^[ \t]+$/gm,""),e=a.subParser("githubCodeBlocks")(e,r,t),e=a.subParser("blockGamut")(e,r,t),e=e.replace(/(^|\n)/g,"$1 "),e=e.replace(/(\s*
[^\r]+?<\/pre>)/gm,function(e,r){var t=r;return t=t.replace(/^  /gm,"¨0"),t=t.replace(/¨0/g,"")}),a.subParser("hashBlock")("
\n"+e+"\n
",r,t)}),e=t.converter._dispatch("blockQuotes.after",e,r,t)}),a.subParser("codeBlocks",function(e,r,t){"use strict";e=t.converter._dispatch("codeBlocks.before",e,r,t);return e=(e+="¨0").replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g,function(e,n,s){var o=n,i=s,l="\n";return o=a.subParser("outdent")(o,r,t),o=a.subParser("encodeCode")(o,r,t),o=a.subParser("detab")(o,r,t),o=o.replace(/^\n+/g,""),o=o.replace(/\n+$/g,""),r.omitExtraWLInCodeBlocks&&(l=""),o="
"+o+l+"
",a.subParser("hashBlock")(o,r,t)+i}),e=e.replace(/¨0/,""),e=t.converter._dispatch("codeBlocks.after",e,r,t)}),a.subParser("codeSpans",function(e,r,t){"use strict";return void 0===(e=t.converter._dispatch("codeSpans.before",e,r,t))&&(e=""),e=e.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(e,n,s,o){var i=o;return i=i.replace(/^([ \t]*)/g,""),i=i.replace(/[ \t]*$/g,""),i=a.subParser("encodeCode")(i,r,t),i=n+""+i+"",i=a.subParser("hashHTMLSpans")(i,r,t)}),e=t.converter._dispatch("codeSpans.after",e,r,t)}),a.subParser("completeHTMLDocument",function(e,r,t){"use strict";if(!r.completeHTMLDocument)return e;e=t.converter._dispatch("completeHTMLDocument.before",e,r,t);var a="html",n="\n",s="",o='\n',i="",l="";void 0!==t.metadata.parsed.doctype&&(n="\n","html"!==(a=t.metadata.parsed.doctype.toString().toLowerCase())&&"html5"!==a||(o=''));for(var c in t.metadata.parsed)if(t.metadata.parsed.hasOwnProperty(c))switch(c.toLowerCase()){case"doctype":break;case"title":s=""+t.metadata.parsed.title+"\n";break;case"charset":o="html"===a||"html5"===a?'\n':'\n';break;case"language":case"lang":i=' lang="'+t.metadata.parsed[c]+'"',l+='\n';break;default:l+='\n'}return e=n+"\n\n"+s+o+l+"\n\n"+e.trim()+"\n\n",e=t.converter._dispatch("completeHTMLDocument.after",e,r,t)}),a.subParser("detab",function(e,r,t){"use strict";return e=t.converter._dispatch("detab.before",e,r,t),e=e.replace(/\t(?=\t)/g," "),e=e.replace(/\t/g,"¨A¨B"),e=e.replace(/¨B(.+?)¨A/g,function(e,r){for(var t=r,a=4-t.length%4,n=0;n/g,">"),e=t.converter._dispatch("encodeAmpsAndAngles.after",e,r,t)}),a.subParser("encodeBackslashEscapes",function(e,r,t){"use strict";return e=t.converter._dispatch("encodeBackslashEscapes.before",e,r,t),e=e.replace(/\\(\\)/g,a.helper.escapeCharactersCallback),e=e.replace(/\\([`*_{}\[\]()>#+.!~=|-])/g,a.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeBackslashEscapes.after",e,r,t)}),a.subParser("encodeCode",function(e,r,t){"use strict";return e=t.converter._dispatch("encodeCode.before",e,r,t),e=e.replace(/&/g,"&").replace(//g,">").replace(/([*_{}\[\]\\=~-])/g,a.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeCode.after",e,r,t)}),a.subParser("escapeSpecialCharsWithinTagAttributes",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.before",e,r,t)).replace(/<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi,function(e){return e.replace(/(.)<\/?code>(?=.)/g,"$1`").replace(/([\\`*_~=|])/g,a.helper.escapeCharactersCallback)}),e=e.replace(/-]|-[^>])(?:[^-]|-[^-])*)--)>/gi,function(e){return e.replace(/([\\`*_~=|])/g,a.helper.escapeCharactersCallback)}),e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.after",e,r,t)}),a.subParser("githubCodeBlocks",function(e,r,t){"use strict";return r.ghCodeBlocks?(e=t.converter._dispatch("githubCodeBlocks.before",e,r,t),e+="¨0",e=e.replace(/(?:^|\n)(?: {0,3})(```+|~~~+)(?: *)([^\s`~]*)\n([\s\S]*?)\n(?: {0,3})\1/g,function(e,n,s,o){var i=r.omitExtraWLInCodeBlocks?"":"\n";return o=a.subParser("encodeCode")(o,r,t),o=a.subParser("detab")(o,r,t),o=o.replace(/^\n+/g,""),o=o.replace(/\n+$/g,""),o="
"+o+i+"
",o=a.subParser("hashBlock")(o,r,t),"\n\n¨G"+(t.ghCodeBlocks.push({text:e,codeblock:o})-1)+"G\n\n"}),e=e.replace(/¨0/,""),t.converter._dispatch("githubCodeBlocks.after",e,r,t)):e}),a.subParser("hashBlock",function(e,r,t){"use strict";return e=t.converter._dispatch("hashBlock.before",e,r,t),e=e.replace(/(^\n+|\n+$)/g,""),e="\n\n¨K"+(t.gHtmlBlocks.push(e)-1)+"K\n\n",e=t.converter._dispatch("hashBlock.after",e,r,t)}),a.subParser("hashCodeTags",function(e,r,t){"use strict";e=t.converter._dispatch("hashCodeTags.before",e,r,t);return e=a.helper.replaceRecursiveRegExp(e,function(e,n,s,o){var i=s+a.subParser("encodeCode")(n,r,t)+o;return"¨C"+(t.gHtmlSpans.push(i)-1)+"C"},"]*>","","gim"),e=t.converter._dispatch("hashCodeTags.after",e,r,t)}),a.subParser("hashElement",function(e,r,t){"use strict";return function(e,r){var a=r;return a=a.replace(/\n\n/g,"\n"),a=a.replace(/^\n/,""),a=a.replace(/\n+$/g,""),a="\n\n¨K"+(t.gHtmlBlocks.push(a)-1)+"K\n\n"}}),a.subParser("hashHTMLBlocks",function(e,r,t){"use strict";e=t.converter._dispatch("hashHTMLBlocks.before",e,r,t);var n=["pre","div","h1","h2","h3","h4","h5","h6","blockquote","table","dl","ol","ul","script","noscript","form","fieldset","iframe","math","style","section","header","footer","nav","article","aside","address","audio","canvas","figure","hgroup","output","video","p"],s=function(e,r,a,n){var s=e;return-1!==a.search(/\bmarkdown\b/)&&(s=a+t.converter.makeHtml(r)+n),"\n\n¨K"+(t.gHtmlBlocks.push(s)-1)+"K\n\n"};r.backslashEscapesHTMLTags&&(e=e.replace(/\\<(\/?[^>]+?)>/g,function(e,r){return"<"+r+">"}));for(var o=0;o]*>)","im"),c="<"+n[o]+"\\b[^>]*>",u="";-1!==(i=a.helper.regexIndexOf(e,l));){var d=a.helper.splitAtIndex(e,i),p=a.helper.replaceRecursiveRegExp(d[1],s,c,u,"im");if(p===d[1])break;e=d[0].concat(p)}return e=e.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,a.subParser("hashElement")(e,r,t)),e=a.helper.replaceRecursiveRegExp(e,function(e){return"\n\n¨K"+(t.gHtmlBlocks.push(e)-1)+"K\n\n"},"^ {0,3}\x3c!--","--\x3e","gm"),e=e.replace(/(?:\n\n)( {0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,a.subParser("hashElement")(e,r,t)),e=t.converter._dispatch("hashHTMLBlocks.after",e,r,t)}),a.subParser("hashHTMLSpans",function(e,r,t){"use strict";function a(e){return"¨C"+(t.gHtmlSpans.push(e)-1)+"C"}return e=t.converter._dispatch("hashHTMLSpans.before",e,r,t),e=e.replace(/<[^>]+?\/>/gi,function(e){return a(e)}),e=e.replace(/<([^>]+?)>[\s\S]*?<\/\1>/g,function(e){return a(e)}),e=e.replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g,function(e){return a(e)}),e=e.replace(/<[^>]+?>/gi,function(e){return a(e)}),e=t.converter._dispatch("hashHTMLSpans.after",e,r,t)}),a.subParser("unhashHTMLSpans",function(e,r,t){"use strict";e=t.converter._dispatch("unhashHTMLSpans.before",e,r,t);for(var a=0;a]*>\\s*]*>","^ {0,3}\\s*
","gim"),e=t.converter._dispatch("hashPreCodeTags.after",e,r,t)}),a.subParser("headers",function(e,r,t){"use strict";function n(e){var n,s;if(r.customizedHeaderId){var o=e.match(/\{([^{]+?)}\s*$/);o&&o[1]&&(e=o[1])}return n=e,s=a.helper.isString(r.prefixHeaderId)?r.prefixHeaderId:!0===r.prefixHeaderId?"section-":"",r.rawPrefixHeaderId||(n=s+n),n=r.ghCompatibleHeaderId?n.replace(/ /g,"-").replace(/&/g,"").replace(/¨T/g,"").replace(/¨D/g,"").replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g,"").toLowerCase():r.rawHeaderId?n.replace(/ /g,"-").replace(/&/g,"&").replace(/¨T/g,"¨").replace(/¨D/g,"$").replace(/["']/g,"-").toLowerCase():n.replace(/[^\w]/g,"").toLowerCase(),r.rawPrefixHeaderId&&(n=s+n),t.hashLinkCounts[n]?n=n+"-"+t.hashLinkCounts[n]++:t.hashLinkCounts[n]=1,n}e=t.converter._dispatch("headers.before",e,r,t);var s=isNaN(parseInt(r.headerLevelStart))?1:parseInt(r.headerLevelStart),o=r.smoothLivePreview?/^(.+)[ \t]*\n={2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n=+[ \t]*\n+/gm,i=r.smoothLivePreview?/^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n-+[ \t]*\n+/gm;e=(e=e.replace(o,function(e,o){var i=a.subParser("spanGamut")(o,r,t),l=r.noHeaderId?"":' id="'+n(o)+'"',c=""+i+"";return a.subParser("hashBlock")(c,r,t)})).replace(i,function(e,o){var i=a.subParser("spanGamut")(o,r,t),l=r.noHeaderId?"":' id="'+n(o)+'"',c=s+1,u=""+i+"";return a.subParser("hashBlock")(u,r,t)});var l=r.requireSpaceBeforeHeadingText?/^(#{1,6})[ \t]+(.+?)[ \t]*#*\n+/gm:/^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm;return e=e.replace(l,function(e,o,i){var l=i;r.customizedHeaderId&&(l=i.replace(/\s?\{([^{]+?)}\s*$/,""));var c=a.subParser("spanGamut")(l,r,t),u=r.noHeaderId?"":' id="'+n(i)+'"',d=s-1+o.length,p=""+c+"";return a.subParser("hashBlock")(p,r,t)}),e=t.converter._dispatch("headers.after",e,r,t)}),a.subParser("horizontalRule",function(e,r,t){"use strict";e=t.converter._dispatch("horizontalRule.before",e,r,t);var n=a.subParser("hashBlock")("
",r,t);return e=e.replace(/^ {0,2}( ?-){3,}[ \t]*$/gm,n),e=e.replace(/^ {0,2}( ?\*){3,}[ \t]*$/gm,n),e=e.replace(/^ {0,2}( ?_){3,}[ \t]*$/gm,n),e=t.converter._dispatch("horizontalRule.after",e,r,t)}),a.subParser("images",function(e,r,t){"use strict";function n(e,r,n,s,o,i,l,c){var u=t.gUrls,d=t.gTitles,p=t.gDimensions;if(n=n.toLowerCase(),c||(c=""),e.search(/\(? ?(['"].*['"])?\)$/m)>-1)s="";else if(""===s||null===s){if(""!==n&&null!==n||(n=r.toLowerCase().replace(/ ?\n/g," ")),s="#"+n,a.helper.isUndefined(u[n]))return e;s=u[n],a.helper.isUndefined(d[n])||(c=d[n]),a.helper.isUndefined(p[n])||(o=p[n].width,i=p[n].height)}r=r.replace(/"/g,""").replace(a.helper.regexes.asteriskDashAndColon,a.helper.escapeCharactersCallback);var h=''+r+'"}return e=(e=t.converter._dispatch("images.before",e,r,t)).replace(/!\[([^\]]*?)] ?(?:\n *)?\[([\s\S]*?)]()()()()()/g,n),e=e.replace(/!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g,function(e,r,t,a,s,o,i,l){return a=a.replace(/\s/g,""),n(e,r,t,a,s,o,0,l)}),e=e.replace(/!\[([^\]]*?)][ \t]*()\([ \t]?<([^>]*)>(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(?:(["'])([^"]*?)\6))?[ \t]?\)/g,n),e=e.replace(/!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g,n),e=e.replace(/!\[([^\[\]]+)]()()()()()/g,n),e=t.converter._dispatch("images.after",e,r,t)}),a.subParser("italicsAndBold",function(e,r,t){"use strict";function a(e,r,t){return r+e+t}return e=t.converter._dispatch("italicsAndBold.before",e,r,t),e=r.literalMidWordUnderscores?(e=(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return a(r,"","")})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return a(r,"","")})).replace(/\b_(\S[\s\S]*?)_\b/g,function(e,r){return a(r,"","")}):(e=(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e})).replace(/_([^\s_][\s\S]*?)_/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e}),e=r.literalMidWordAsterisks?(e=(e=e.replace(/([^*]|^)\B\*\*\*(\S[\s\S]*?)\*\*\*\B(?!\*)/g,function(e,r,t){return a(t,r+"","")})).replace(/([^*]|^)\B\*\*(\S[\s\S]*?)\*\*\B(?!\*)/g,function(e,r,t){return a(t,r+"","")})).replace(/([^*]|^)\B\*(\S[\s\S]*?)\*\B(?!\*)/g,function(e,r,t){return a(t,r+"","")}):(e=(e=e.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e})).replace(/\*\*(\S[\s\S]*?)\*\*/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e})).replace(/\*([^\s*][\s\S]*?)\*/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e}),e=t.converter._dispatch("italicsAndBold.after",e,r,t)}),a.subParser("lists",function(e,r,t){"use strict";function n(e,n){t.gListLevel++,e=e.replace(/\n{2,}$/,"\n");var s=/(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0| {0,3}([*+-]|\d+[.])[ \t]+))/gm,o=/\n[ \t]*\n(?!¨0)/.test(e+="¨0");return r.disableForced4SpacesIndentedSublists&&(s=/(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0|\2([*+-]|\d+[.])[ \t]+))/gm),e=e.replace(s,function(e,n,s,i,l,c,u){u=u&&""!==u.trim();var d=a.subParser("outdent")(l,r,t),p="";return c&&r.tasklists&&(p=' class="task-list-item" style="list-style-type: none;"',d=d.replace(/^[ \t]*\[(x|X| )?]/m,function(){var e='-1?(d=a.subParser("githubCodeBlocks")(d,r,t),d=a.subParser("blockGamut")(d,r,t)):(d=(d=a.subParser("lists")(d,r,t)).replace(/\n$/,""),d=(d=a.subParser("hashHTMLBlocks")(d,r,t)).replace(/\n\n+/g,"\n\n"),d=o?a.subParser("paragraphs")(d,r,t):a.subParser("spanGamut")(d,r,t)),d=d.replace("¨A",""),d=""+d+"\n"}),e=e.replace(/¨0/g,""),t.gListLevel--,n&&(e=e.replace(/\s+$/,"")),e}function s(e,r){if("ol"===r){var t=e.match(/^ *(\d+)\./);if(t&&"1"!==t[1])return' start="'+t[1]+'"'}return""}function o(e,t,a){var o=r.disableForced4SpacesIndentedSublists?/^ ?\d+\.[ \t]/gm:/^ {0,3}\d+\.[ \t]/gm,i=r.disableForced4SpacesIndentedSublists?/^ ?[*+-][ \t]/gm:/^ {0,3}[*+-][ \t]/gm,l="ul"===t?o:i,c="";if(-1!==e.search(l))!function r(u){var d=u.search(l),p=s(e,t);-1!==d?(c+="\n\n<"+t+p+">\n"+n(u.slice(0,d),!!a)+"\n",l="ul"===(t="ul"===t?"ol":"ul")?o:i,r(u.slice(d))):c+="\n\n<"+t+p+">\n"+n(u,!!a)+"\n"}(e);else{var u=s(e,t);c="\n\n<"+t+u+">\n"+n(e,!!a)+"\n"}return c}return e=t.converter._dispatch("lists.before",e,r,t),e+="¨0",e=t.gListLevel?e.replace(/^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm,function(e,r,t){return o(r,t.search(/[*+-]/g)>-1?"ul":"ol",!0)}):e.replace(/(\n\n|^\n?)(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm,function(e,r,t,a){return o(t,a.search(/[*+-]/g)>-1?"ul":"ol",!1)}),e=e.replace(/¨0/,""),e=t.converter._dispatch("lists.after",e,r,t)}),a.subParser("metadata",function(e,r,t){"use strict";function a(e){t.metadata.raw=e,(e=(e=e.replace(/&/g,"&").replace(/"/g,""")).replace(/\n {4}/g," ")).replace(/^([\S ]+): +([\s\S]+?)$/gm,function(e,r,a){return t.metadata.parsed[r]=a,""})}return r.metadata?(e=t.converter._dispatch("metadata.before",e,r,t),e=e.replace(/^\s*«««+(\S*?)\n([\s\S]+?)\n»»»+\n/,function(e,r,t){return a(t),"¨M"}),e=e.replace(/^\s*---+(\S*?)\n([\s\S]+?)\n---+\n/,function(e,r,n){return r&&(t.metadata.format=r),a(n),"¨M"}),e=e.replace(/¨M/g,""),e=t.converter._dispatch("metadata.after",e,r,t)):e}),a.subParser("outdent",function(e,r,t){"use strict";return e=t.converter._dispatch("outdent.before",e,r,t),e=e.replace(/^(\t|[ ]{1,4})/gm,"¨0"),e=e.replace(/¨0/g,""),e=t.converter._dispatch("outdent.after",e,r,t)}),a.subParser("paragraphs",function(e,r,t){"use strict";for(var n=(e=(e=(e=t.converter._dispatch("paragraphs.before",e,r,t)).replace(/^\n+/g,"")).replace(/\n+$/g,"")).split(/\n{2,}/g),s=[],o=n.length,i=0;i=0?s.push(l):l.search(/\S/)>=0&&(l=(l=a.subParser("spanGamut")(l,r,t)).replace(/^([ \t]*)/g,"

"),l+="

",s.push(l))}for(o=s.length,i=0;i]*>\s*]*>/.test(u)&&(d=!0)}s[i]=u}return e=s.join("\n"),e=e.replace(/^\n+/g,""),e=e.replace(/\n+$/g,""),t.converter._dispatch("paragraphs.after",e,r,t)}),a.subParser("runExtension",function(e,r,t,a){"use strict";if(e.filter)r=e.filter(r,a.converter,t);else if(e.regex){var n=e.regex;n instanceof RegExp||(n=new RegExp(n,"g")),r=r.replace(n,e.replace)}return r}),a.subParser("spanGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("spanGamut.before",e,r,t),e=a.subParser("codeSpans")(e,r,t),e=a.subParser("escapeSpecialCharsWithinTagAttributes")(e,r,t),e=a.subParser("encodeBackslashEscapes")(e,r,t),e=a.subParser("images")(e,r,t),e=a.subParser("anchors")(e,r,t),e=a.subParser("autoLinks")(e,r,t),e=a.subParser("simplifiedAutoLinks")(e,r,t),e=a.subParser("emoji")(e,r,t),e=a.subParser("underline")(e,r,t),e=a.subParser("italicsAndBold")(e,r,t),e=a.subParser("strikethrough")(e,r,t),e=a.subParser("ellipsis")(e,r,t),e=a.subParser("hashHTMLSpans")(e,r,t),e=a.subParser("encodeAmpsAndAngles")(e,r,t),r.simpleLineBreaks?/\n\n¨K/.test(e)||(e=e.replace(/\n+/g,"
\n")):e=e.replace(/ +\n/g,"
\n"),e=t.converter._dispatch("spanGamut.after",e,r,t)}),a.subParser("strikethrough",function(e,r,t){"use strict";return r.strikethrough&&(e=(e=t.converter._dispatch("strikethrough.before",e,r,t)).replace(/(?:~){2}([\s\S]+?)(?:~){2}/g,function(e,n){return function(e){return r.simplifiedAutoLink&&(e=a.subParser("simplifiedAutoLinks")(e,r,t)),""+e+""}(n)}),e=t.converter._dispatch("strikethrough.after",e,r,t)),e}),a.subParser("stripLinkDefinitions",function(e,r,t){"use strict";var n=function(e,n,s,o,i,l,c){return n=n.toLowerCase(),s.match(/^data:.+?\/.+?;base64,/)?t.gUrls[n]=s.replace(/\s/g,""):t.gUrls[n]=a.subParser("encodeAmpsAndAngles")(s,r,t),l?l+c:(c&&(t.gTitles[n]=c.replace(/"|'/g,""")),r.parseImgDimensions&&o&&i&&(t.gDimensions[n]={width:o,height:i}),"")};return e=(e+="¨0").replace(/^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm,n),e=e.replace(/^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm,n),e=e.replace(/¨0/,"")}),a.subParser("tables",function(e,r,t){"use strict";function n(e){return/^:[ \t]*--*$/.test(e)?' style="text-align:left;"':/^--*[ \t]*:[ \t]*$/.test(e)?' style="text-align:right;"':/^:[ \t]*--*[ \t]*:$/.test(e)?' style="text-align:center;"':""}function s(e,n){var s="";return e=e.trim(),(r.tablesHeaderId||r.tableHeaderId)&&(s=' id="'+e.replace(/ /g,"_").toLowerCase()+'"'),e=a.subParser("spanGamut")(e,r,t),""+e+"\n"}function o(e,n){return""+a.subParser("spanGamut")(e,r,t)+"\n"}function i(e){var i,l=e.split("\n");for(i=0;i\n\n\n",n=0;n\n";for(var s=0;s\n"}return t+="\n\n"}(p,_)}if(!r.tables)return e;return e=t.converter._dispatch("tables.before",e,r,t),e=e.replace(/\\(\|)/g,a.helper.escapeCharactersCallback),e=e.replace(/^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm,i),e=e.replace(/^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm,i),e=t.converter._dispatch("tables.after",e,r,t)}),a.subParser("underline",function(e,r,t){"use strict";return r.underline?(e=t.converter._dispatch("underline.before",e,r,t),e=r.literalMidWordUnderscores?(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return""+r+""})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return""+r+""}):(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?""+r+"":e}),e=e.replace(/(_)/g,a.helper.escapeCharactersCallback),e=t.converter._dispatch("underline.after",e,r,t)):e}),a.subParser("unescapeSpecialChars",function(e,r,t){"use strict";return e=t.converter._dispatch("unescapeSpecialChars.before",e,r,t),e=e.replace(/¨E(\d+)E/g,function(e,r){var t=parseInt(r);return String.fromCharCode(t)}),e=t.converter._dispatch("unescapeSpecialChars.after",e,r,t)}),a.subParser("makeMarkdown.blockquote",function(e,r){"use strict";var t="";if(e.hasChildNodes())for(var n=e.childNodes,s=n.length,o=0;o "+t.split("\n").join("\n> ")}),a.subParser("makeMarkdown.codeBlock",function(e,r){"use strict";var t=e.getAttribute("language"),a=e.getAttribute("precodenum");return"```"+t+"\n"+r.preList[a]+"\n```"}),a.subParser("makeMarkdown.codeSpan",function(e){"use strict";return"`"+e.innerHTML+"`"}),a.subParser("makeMarkdown.emphasis",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="*";for(var n=e.childNodes,s=n.length,o=0;o",e.hasAttribute("width")&&e.hasAttribute("height")&&(r+=" ="+e.getAttribute("width")+"x"+e.getAttribute("height")),e.hasAttribute("title")&&(r+=' "'+e.getAttribute("title")+'"'),r+=")"),r}),a.subParser("makeMarkdown.links",function(e,r){"use strict";var t="";if(e.hasChildNodes()&&e.hasAttribute("href")){var n=e.childNodes,s=n.length;t="[";for(var o=0;o",e.hasAttribute("title")&&(t+=' "'+e.getAttribute("title")+'"'),t+=")"}return t}),a.subParser("makeMarkdown.list",function(e,r,t){"use strict";var n="";if(!e.hasChildNodes())return"";for(var s=e.childNodes,o=s.length,i=e.getAttribute("start")||1,l=0;l"+r.preList[t]+""}),a.subParser("makeMarkdown.strikethrough",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="~~";for(var n=e.childNodes,s=n.length,o=0;otr>th"),l=e.querySelectorAll("tbody>tr");for(t=0;t_&&(_=g)}for(t=0;t/g,"\\$1>"),r=r.replace(/^#/gm,"\\#"),r=r.replace(/^(\s*)([-=]{3,})(\s*)$/,"$1\\$2$3"),r=r.replace(/^( {0,3}\d+)\./gm,"$1\\."),r=r.replace(/^( {0,3})([+-])/gm,"$1\\$2"),r=r.replace(/]([\s]*)\(/g,"\\]$1\\("),r=r.replace(/^ {0,3}\[([\S \t]*?)]:/gm,"\\[$1]:")});"function"==typeof define&&define.amd?define(function(){"use strict";return a}):"undefined"!=typeof module&&module.exports?module.exports=a:this.showdown=a}).call(this); +//# sourceMappingURL=showdown.min.js.map diff --git a/lifeplayer67.CanvasView/src/NPCanvasView.js b/lifeplayer67.CanvasView/src/NPCanvasView.js new file mode 100644 index 000000000..4d69ce3fc --- /dev/null +++ b/lifeplayer67.CanvasView/src/NPCanvasView.js @@ -0,0 +1,339 @@ +// @flow +// ----------------------------------------------------------------------------- +// Command implementations for the Canvas View plugin. +// ----------------------------------------------------------------------------- + +import pluginJson from '../plugin.json' +import { renderCanvasHTML, type TCanvasData } from './canvasRenderer' +import { showHTMLV2, sendToHTMLWindow, getNoteContentAsHTML } from '@helpers/HTMLView' +import { logDebug, logError, clo } from '@helpers/dev' +import { showMessage } from '@helpers/userInput' + +const PLUGIN_ID = pluginJson['plugin.id'] + +/** One window/sidebar entry per canvas, so two canvases never fight over one pane */ +function windowIdFor(fullPath: string): string { + return `${PLUGIN_ID}.${fullPath.split('/').pop() ?? 'main'}` +} + +type TSettings = { + canvasFolder: string, + openInSplitView: boolean, + openNoteIn: string, +} + +function getSettings(): TSettings { + const settings = DataStore.settings ?? {} + return { + canvasFolder: (settings.canvasFolder ?? '').trim(), + openInSplitView: settings.openInSplitView !== false, + openNoteIn: settings.openNoteIn ?? 'Split view', + } +} + +const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|svg)$/i +const IMAGE_MIME: { [string]: string } = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml' } + +/** Try to load an image referenced by a file node as a data URI (base64, ≤ ~2.5 MB) */ +function loadImageDataURI(filePath: string): ?string { + const candidates = [`../../../Notes/${filePath}`] + if (filePath.startsWith('Notes/')) candidates.push(`../../../${filePath}`) + for (const p of candidates) { + const b64 = DataStore.loadData(p, false) + if (b64 != null && b64 !== '' && b64.length < 3_500_000) { + const ext = (filePath.split('.').pop() ?? '').toLowerCase() + return `data:${IMAGE_MIME[ext] ?? 'image/png'};base64,${b64}` + } + } + return null +} + +/** + * Resolve the user-supplied path to something DataStore.loadData can read. + * loadData always reads relative to [NotePlan]/Plugins/data// (absolute + * paths are treated as relative!), but '../' escapes are allowed — the same trick + * helpers/HTMLView.js uses to reach the Notes folder ('../../../Notes/...'). + * So: relative input -> the NotePlan Notes folder (+ optional canvasFolder subfolder), + * absolute input -> climb to the filesystem root with enough '../' segments. + */ +function resolveCanvasPath(input: string, canvasFolder: string): string { + const trimmed = input.trim() + const withExt = /\.canvas$/i.test(trimmed) ? trimmed : `${trimmed}.canvas` + if (withExt.startsWith('/')) return `${'../'.repeat(12)}${withExt.slice(1)}` + const folder = canvasFolder.replace(/^\/+|\/+$/g, '') + return folder === '' ? `../../../Notes/${withExt}` : `../../../Notes/${folder}/${withExt}` +} + +function loadCanvas(fullPath: string): ?TCanvasData { + const raw = DataStore.loadData(fullPath, true) + if (raw == null || raw === '') return null + try { + return JSON.parse(raw) + } catch (parseError) { + // Seen in the wild: stray '…' (U+2026) after '[' / '{' and unicode whitespace + // between tokens (source unclear — possibly a sync/text-layer round-trip). + // Strip garbage that sits OUTSIDE strings and retry before giving up. + const cleaned = raw.replace(/([[{,]\s*)…\s*/g, '$1').replace(/[\u2028\u2029\u00A0\u200B\uFEFF]/g, ' ') + const parsed = JSON.parse(cleaned) // still broken → throws to the caller's message + logError(pluginJson, `loadCanvas: auto-repaired invalid JSON in '${fullPath}' (was: ${parseError.message})`) + DataStore.saveData(cleaned, fullPath, true) + return parsed + } +} + +/** Note HTML via the shared converter, remapped to the theme's 'p b' / 'p emph' selectors */ +async function noteContentHTML(content: string, note: TNote): Promise { + const html = await getNoteContentAsHTML(content, note) + return html.replace(/<(\/?)strong>/g, '<$1b>').replace(/<(\/?)em>/g, '<$1emph>') +} + +// HTML pieces for the two-way comms bridge (np.Shared must be installed). + +// onMessageFromPlugin must be defined before pluginToHTMLCommsBridge.js loads. +const COMMS_BRIDGE_HTML = ` + + + +` + +/** + * Open a .canvas (JSON Canvas) file in a viewer/editor window. + * @param {string?} pathArg path to the file; prompted for if not given + */ +export async function openCanvas(pathArg?: string): Promise { + try { + const settings = getSettings() + let path = pathArg ?? '' + let fullPath = '' + if (path === '') { + // NotePlan indexes .canvas files as notes, so we can offer a proper chooser + const canvasNotes = DataStore.projectNotes.filter((n) => n.filename.toLowerCase().endsWith('.canvas')) + if (canvasNotes.length > 0) { + const options = canvasNotes.map((n) => n.filename) + const res = await CommandBar.showOptions(options, 'Open which canvas?') + if (res == null) return + fullPath = `../../../Notes/${options[res.index]}` + } else { + const answer = await CommandBar.textPrompt( + 'Canvas View', + settings.canvasFolder !== '' ? `Path to .canvas file (relative to Notes/${settings.canvasFolder})` : 'Path to .canvas file (relative to your Notes folder)', + '', + ) + if (answer === false || answer == null || String(answer).trim() === '') return + path = String(answer) + } + } + if (fullPath === '') fullPath = resolveCanvasPath(path, settings.canvasFolder) + await showCanvasWindow(fullPath, settings) + } catch (error) { + logError(pluginJson, `openCanvas: ${error.message}`) + clo(error, 'openCanvas error') + } +} + +/** + * Create a new empty .canvas file and open it. + * @param {string?} nameArg name for the canvas, optionally with a folder ('Ideas' or 'Projects/Ideas') + */ +export async function newCanvas(nameArg?: string): Promise { + try { + const settings = getSettings() + let name = (nameArg ?? '').trim() + if (name === '') { + const answer = await CommandBar.textPrompt('Canvas View', "Name for the new canvas (a folder is fine too, e.g. 'Projects/Ideas')", '') + if (answer === false || answer == null || String(answer).trim() === '') return + name = String(answer).trim() + } + const withExt = /\.canvas$/i.test(name) ? name : `${name}.canvas` + const defaultFolder = settings.canvasFolder !== '' ? settings.canvasFolder.replace(/^\/+|\/+$/g, '') : 'Canvases' + const relPath = withExt.includes('/') ? withExt : `${defaultFolder}/${withExt}` + const fullPath = `../../../Notes/${relPath}` + const existing = DataStore.loadData(fullPath, true) + if (existing == null || existing === '') { + const ok = DataStore.saveData(JSON.stringify({ nodes: [], edges: [] }, null, '\t'), fullPath, true) + if (!ok) { + await showMessage(`Could not create '${relPath}'.`, 'OK', 'Canvas View') + return + } + logDebug(pluginJson, `newCanvas: created '${relPath}'`) + } else { + logDebug(pluginJson, `newCanvas: '${relPath}' already exists — opening it`) + } + await showCanvasWindow(fullPath, settings) + } catch (error) { + logError(pluginJson, `newCanvas: ${error.message}`) + } +} + +/** Load a canvas by its resolved path and open the editor window/pane for it */ +async function showCanvasWindow(fullPath: string, settings: TSettings): Promise { + try { + logDebug(pluginJson, `showCanvasWindow: loading '${fullPath}'`) + + let canvas: ?TCanvasData + try { + canvas = loadCanvas(fullPath) + } catch (parseError) { + await showMessage(`'${fullPath}' is not valid JSON Canvas: ${parseError.message}`, 'OK', 'Canvas View') + return + } + if (canvas == null) { + await showMessage(`Could not read '${fullPath}'. Check the path and the 'Canvas folder' plugin setting.`, 'OK', 'Canvas View') + return + } + const nodeCount = canvas.nodes?.length ?? 0 + const edgeCount = canvas.edges?.length ?? 0 + logDebug(pluginJson, `openCanvas: parsed ${nodeCount} nodes, ${edgeCount} edges`) + + // Resolve file-node contents so the window can render and edit them inline; + // image files become data URIs so they display right in the card + const fileContents: { [string]: Object } = {} + for (const node of canvas.nodes ?? []) { + if (node.type !== 'file') continue + const filePath = String(node.file ?? '') + if (IMAGE_EXT_RE.test(filePath)) { + const dataURI = loadImageDataURI(filePath) + fileContents[node.id] = dataURI != null ? { found: true, media: dataURI } : { found: false, title: filePath.split('/').pop() ?? '' } + continue + } + const noteTitle = (filePath.split('/').pop() ?? '').replace(/\.[^.]+$/, '') + const matches = DataStore.projectNoteByTitle(noteTitle) ?? [] + fileContents[node.id] = + matches.length > 0 + ? { + found: true, + title: noteTitle, + content: matches[0].content ?? '', + html: await noteContentHTML(matches[0].content ?? '', matches[0]), + } + : { found: false, title: noteTitle } + } + + // Note index for the autocomplete picker in the window + const noteIndex = DataStore.projectNotes + .filter((n) => (n.title ?? '') !== '') + .map((n) => ({ t: n.title ?? '', f: n.filename })) + + const title = (fullPath.split('/').pop() ?? '').replace(/\.canvas$/i, '') + const body = COMMS_BRIDGE_HTML + renderCanvasHTML(canvas, `${title} — ${nodeCount} nodes, ${edgeCount} edges`, fullPath, fileContents, noteIndex) + + await showHTMLV2(body, { + windowTitle: `Canvas: ${title}`, + customId: windowIdFor(fullPath), + savedFilename: '../../lifeplayer67.CanvasView/canvas-view.html', + width: 1100, + height: 750, + shouldFocus: true, + showInMainWindow: settings.openInSplitView, + splitView: settings.openInSplitView, + icon: 'diagram-project', + iconColor: 'orange-500', + // no generalCSSIn: let showHTMLV2 inject CSS generated from the user's NotePlan + // theme — it defines --bg-main-color/--fg-main-color/etc., which our CSS consumes + }) + } catch (error) { + logError(pluginJson, `showCanvasWindow: ${error.message}`) + clo(error, 'showCanvasWindow error') + } +} + +/** + * Receive messages sent from the HTML window via the np.Shared comms bridge. + * data can arrive as a JSON string when invoked through x-callback (for testing). + */ +export async function onMessageFromHTMLView(actionType: string, data: any): Promise { + try { + // Tolerate the one-array calling convention too: onMessageFromHTMLView(['action', {...}]) + let action = actionType + let rawData = data + if (Array.isArray(action)) { + rawData = action[1] + action = String(action[0]) + } + const payload = typeof rawData === 'string' ? JSON.parse(rawData) : rawData ?? {} + logDebug(pluginJson, `onMessageFromHTMLView: '${action}'`) + switch (action) { + case 'saveCanvas': { + const jsonStr = String(payload.json) + JSON.parse(jsonStr) // validate before touching the file — never write a broken canvas + const ok = DataStore.saveData(jsonStr, payload.path, true) + if (!ok) logError(pluginJson, `saveCanvas: saveData failed for '${payload.path}'`) + break + } + case 'saveNote': { + const noteTitle = String(payload.title ?? '') + const content = String(payload.content) + const matches = DataStore.projectNoteByTitle(noteTitle) ?? [] + if (matches.length > 0) { + matches[0].content = content + logDebug(pluginJson, `saveNote: updated '${noteTitle}' (${content.length} chars)`) + } else { + // First write into a card whose note doesn't exist yet: create it + const withTitle = content.startsWith('# ') ? content : `# ${noteTitle}\n${content}` + const filename = DataStore.newNoteWithContent(withTitle, '') + logDebug(pluginJson, `saveNote: created '${filename}' for title '${noteTitle}'`) + } + break + } + case 'getNoteContent': { + // The picker attached a card to a different note: send its content back + const noteTitle = String(payload.title ?? '') + const matches = DataStore.projectNoteByTitle(noteTitle) ?? [] + await sendToHTMLWindow(windowIdFor(String(payload.path ?? '')), 'NOTE_CONTENT', { + id: payload.id, + found: matches.length > 0, + title: noteTitle, + content: matches.length > 0 ? matches[0].content ?? '' : '', + html: matches.length > 0 ? await noteContentHTML(matches[0].content ?? '', matches[0]) : '', + }) + break + } + case 'setClipboard': { + Clipboard.string = String(payload.text ?? '') + break + } + case 'getClipboard': { + await sendToHTMLWindow(windowIdFor(String(payload.path ?? '')), 'CLIPBOARD', { text: Clipboard.string ?? '' }) + break + } + case 'refreshNotes': { + const windowId = windowIdFor(String(payload.path ?? '')) + for (const item of payload.items ?? []) { + const matches = DataStore.projectNoteByTitle(String(item.title)) ?? [] + await sendToHTMLWindow(windowId, 'NOTE_CONTENT', { + id: item.id, + found: matches.length > 0, + title: String(item.title), + content: matches.length > 0 ? matches[0].content ?? '' : '', + html: matches.length > 0 ? await noteContentHTML(matches[0].content ?? '', matches[0]) : '', + }) + } + break + } + case 'openNote': { + const title = String(payload.title ?? '') + // keep the canvas pane on screen: open the note beside it, not instead of it + const mode = getSettings().openNoteIn + const note = + mode === 'New window' + ? await Editor.openNoteByTitle(title, true) + : mode === 'Main editor' + ? await Editor.openNoteByTitle(title) + : await Editor.openNoteByTitle(title, false, 0, 0, true) + if (note == null) { + await showMessage(`No note titled '${title}' found in NotePlan. Import it first, then the link will work.`, 'OK', 'Canvas View') + } + break + } + default: + logError(pluginJson, `onMessageFromHTMLView: unknown actionType '${action}'`) + } + } catch (error) { + logError(pluginJson, `onMessageFromHTMLView: ${error.message}`) + } +} diff --git a/lifeplayer67.CanvasView/src/canvasRenderer.js b/lifeplayer67.CanvasView/src/canvasRenderer.js new file mode 100644 index 000000000..a2460b77f --- /dev/null +++ b/lifeplayer67.CanvasView/src/canvasRenderer.js @@ -0,0 +1,180 @@ +// @flow +// ----------------------------------------------------------------------------- +// Server-side shell for the canvas window. All rendering and interaction live +// in requiredFiles/canvasClient.js (loaded via +
+
+
${SWATCHES}
×
⊞
🗑
+
+ + + +
+
${escapeHtml(title)}
+
+ + +` +} + +const CSS = ` +html, body { margin: 0; padding: 0; overflow: hidden; height: 100%; } +#viewport { position: absolute; inset: 0; overflow: hidden; cursor: default; + background: var(--bg-main-color, #1e1e1e); + background-image: radial-gradient(circle, rgba(128,128,128,0.25) 1px, transparent 1px); + background-size: 24px 24px; } +#viewport.panning { cursor: grabbing; } +#viewport.linking { cursor: crosshair; } +#world { position: absolute; transform-origin: 0 0; } +.node { position: absolute; box-sizing: border-box; border: 2px solid; border-radius: 8px; + padding: 10px 12px; overflow: visible; font-family: -apple-system, sans-serif; font-size: 14px; + background: var(--bg-alt-color, #262626); color: var(--fg-main-color, #d4d4d4); } +.node > .content, .node > a, .node > span { display: block; overflow: hidden; width: 100%; height: 100%; } +.node.group { border-style: dashed; border-radius: 12px; z-index: 0; padding: 0; background-clip: padding-box; } +.node.text, .node.file, .node.link { z-index: 2; cursor: default; } +.node.selected { outline: 2px solid #4da3ff; outline-offset: 2px; } +.node.dragging { opacity: 0.85; } +.group-label { position: absolute; top: -24px; left: 8px; font-weight: 600; font-size: 13px; } +.node a { color: #4da3ff; text-decoration: none; cursor: pointer; -webkit-user-drag: none; } +.node code { background: rgba(128,128,128,0.25); border-radius: 3px; padding: 0 3px; font-size: 0.9em; } +.node h1, .node h2, .node h3, .node h4 { margin: 0 0 6px 0; background: transparent; } +.node p { margin: 0; } +.node .blank { height: 0.7em; } +.node textarea { position: absolute; inset: 0; width: 100%; height: 100%; box-sizing: border-box; + border: none; outline: none; resize: none; padding: 10px 12px; font: inherit; + background: var(--bg-alt-color, #262626); color: inherit; border-radius: 6px; z-index: 5; } +.file-link { display: flex !important; align-items: center; gap: 6px; font-weight: 500; } +.file-head { display: flex; align-items: center; gap: 6px; font-weight: 600; font-size: 13px; + padding-bottom: 5px; margin-bottom: 6px; border-bottom: 1px solid rgba(128,128,128,0.3); } +.fh-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.open-btn { cursor: pointer; opacity: 0.55; font-size: 15px; padding: 0 2px; } +.open-btn:hover { opacity: 1; } +.file-content { font-size: 12px; overflow-y: auto !important; max-height: calc(100% - 34px); } +.file-missing { opacity: 0.65; font-style: italic; font-size: 12px; } +.card-input { position: absolute; inset: 0; width: 100%; height: 100%; box-sizing: border-box; + border: none; outline: 2px solid #4da3ff; border-radius: 6px; padding: 10px 12px; font: inherit; + background: var(--bg-alt-color, #262626); color: inherit; z-index: 6; } +.picker-input { inset: auto 0 auto 0; top: 0; height: 40px; } +.picker { position: absolute; top: 42px; left: 0; right: 0; z-index: 40; border-radius: 8px; + background: var(--bg-alt-color, #262626); border: 1px solid rgba(128,128,128,0.4); + box-shadow: 0 8px 24px rgba(0,0,0,0.4); overflow: hidden; } +.picker-row { padding: 6px 10px; font-size: 13px; cursor: pointer; display: flex; gap: 8px; align-items: baseline; } +.picker-row .p-path { font-size: 11px; opacity: 0.55; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.picker-row.active, .picker-row:hover { background: rgba(77,163,255,0.25); } +.card-img { max-width: 100%; max-height: calc(100% - 30px); object-fit: contain; display: block; } +/* shared content styling so text cards and note cards look identical */ +.node .content img { max-width: 100%; border-radius: 4px; margin: 4px 0; } +.node .content table { border-collapse: collapse; margin: 6px 0; font-size: 0.95em; } +.node .content th, .node .content td { border: 1px solid rgba(128,128,128,0.4); padding: 3px 8px; text-align: left; } +.node .content th { font-weight: 600; background: rgba(128,128,128,0.12); } +.node .content ul, .node .content ol { margin: 2px 0 2px 4px; padding-left: 16px; } +.node .content ul { list-style: disc; } +.node .content li { margin: 1px 0; } +.node .content p { margin: 0; } +.node .content blockquote { margin: 4px 0; padding-left: 8px; border-left: 3px solid rgba(128,128,128,0.5); opacity: 0.9; } +.web-frame { width: 100%; height: calc(100% - 30px); border: none; border-radius: 0 0 6px 6px; + pointer-events: none; background: #fff; } +#ghost { position: fixed; z-index: 50; pointer-events: none; padding: 8px 14px; border-radius: 8px; + border: 2px dashed #4da3ff; background: rgba(77,163,255,0.15); font-family: -apple-system, sans-serif; + font-size: 13px; color: var(--fg-main-color, #d4d4d4); transform: translate(-50%, -50%); } +.anchor { position: absolute; width: 12px; height: 12px; border-radius: 50%; background: #4da3ff; + opacity: 0; transition: opacity 0.12s; z-index: 6; cursor: crosshair; } +.node:hover .anchor { opacity: 0.9; } +.anchor[data-side="top"] { top: -7px; left: calc(50% - 6px); } +.anchor[data-side="bottom"] { bottom: -7px; left: calc(50% - 6px); } +.anchor[data-side="left"] { left: -7px; top: calc(50% - 6px); } +.anchor[data-side="right"] { right: -7px; top: calc(50% - 6px); } +.handle { position: absolute; width: 10px; height: 10px; background: #fff; border: 1.5px solid #4da3ff; + border-radius: 2px; z-index: 7; } +.handle-nw { top: -6px; left: -6px; cursor: nwse-resize; } +.handle-n { top: -6px; left: calc(50% - 5px); cursor: ns-resize; } +.handle-ne { top: -6px; right: -6px; cursor: nesw-resize; } +.handle-e { right: -6px; top: calc(50% - 5px); cursor: ew-resize; } +.handle-se { bottom: -6px; right: -6px; cursor: nwse-resize; } +.handle-s { bottom: -6px; left: calc(50% - 5px); cursor: ns-resize; } +.handle-sw { bottom: -6px; left: -6px; cursor: nesw-resize; } +.handle-w { left: -6px; top: calc(50% - 5px); cursor: ew-resize; } +/* pointer-events none is load-bearing: the svg spans the whole canvas ABOVE groups + (z 1 vs 0), so without it the transparent svg box eats every click on a group */ +#edges { position: absolute; z-index: 1; overflow: visible; pointer-events: none; } +.edge-line { fill: none; stroke-width: 2; pointer-events: none; } +.edge-line.selected { stroke-width: 4; filter: drop-shadow(0 0 3px #4da3ff); } +.edge-hit { fill: none; stroke: transparent; stroke-width: 14; pointer-events: stroke; cursor: pointer; } +.edge-temp { fill: none; stroke: #4da3ff; stroke-width: 2; stroke-dasharray: 6 4; pointer-events: none; } +.edge-label { fill: var(--fg-main-color, #d4d4d4); font-size: 12px; font-family: -apple-system, sans-serif; cursor: pointer; } +.label-input { position: absolute; z-index: 8; width: 160px; padding: 3px 6px; font-size: 12px; + font-family: -apple-system, sans-serif; border: 1px solid #4da3ff; border-radius: 4px; + background: var(--bg-alt-color, #262626); color: var(--fg-main-color, #d4d4d4); } +.node .label-input { top: -28px; left: 4px; } +#marquee { position: absolute; border: 1px solid #4da3ff; background: rgba(77,163,255,0.12); z-index: 20; pointer-events: none; } +#toolbar { position: fixed; top: 10px; left: 50%; transform: translateX(-50%); z-index: 30; + display: none; gap: 8px; align-items: center; padding: 8px 12px; border-radius: 10px; + background: rgba(30,30,30,0.9); border: 1px solid rgba(128,128,128,0.35); } +#toolbar .swatch { width: 18px; height: 18px; border-radius: 50%; cursor: pointer; + border: 1.5px solid rgba(255,255,255,0.4); text-align: center; line-height: 15px; + color: #ccc; font-family: -apple-system, sans-serif; font-size: 13px; } +#toolbar .swatch[data-color="1"] { background: #e93147; } +#toolbar .swatch[data-color="2"] { background: #ec7500; } +#toolbar .swatch[data-color="3"] { background: #e0ac00; } +#toolbar .swatch[data-color="4"] { background: #08b94e; } +#toolbar .swatch[data-color="5"] { background: #00bfbc; } +#toolbar .swatch[data-color="6"] { background: #7852ee; } +#toolbar .tb-group { cursor: pointer; font-size: 17px; margin-left: 6px; color: var(--fg-main-color, #d4d4d4); } +#toolbar .tb-group:hover, #toolbar .tb-delete:hover { transform: scale(1.15); } +#toolbar .tb-delete { cursor: pointer; font-size: 15px; margin-left: 4px; } +#palette { position: fixed; top: 10px; left: 10px; z-index: 30; display: flex; flex-direction: column; + gap: 4px; padding: 6px; border-radius: 10px; background: rgba(30,30,30,0.9); + border: 1px solid rgba(128,128,128,0.35); } +#palette button { border: none; border-radius: 6px; padding: 4px 10px; font-size: 12px; text-align: left; + font-family: -apple-system, sans-serif; cursor: pointer; background: transparent; + color: var(--fg-main-color, #d4d4d4); } +#palette button:hover { background: rgba(128,128,128,0.25); } +#hud { position: fixed; bottom: 10px; left: 12px; z-index: 10; font-family: -apple-system, sans-serif; + font-size: 12px; color: rgba(128,128,128,0.9); user-select: none; } +#zoom-level { margin-left: 10px; } +@media (prefers-color-scheme: light) { + #viewport { background: var(--bg-main-color, #fafafa); } + .node { background: var(--bg-alt-color, #ffffff); color: var(--fg-main-color, #222); } + .node textarea { background: var(--bg-alt-color, #ffffff); } + #toolbar { background: rgba(250,250,250,0.95); } + #palette { background: rgba(250,250,250,0.95); } + .label-input { background: #fff; color: #222; } +} +` diff --git a/lifeplayer67.CanvasView/src/index.js b/lifeplayer67.CanvasView/src/index.js new file mode 100644 index 000000000..f2104c019 --- /dev/null +++ b/lifeplayer67.CanvasView/src/index.js @@ -0,0 +1,29 @@ +// @flow +// ----------------------------------------------------------------------------- +// Canvas View plugin for NotePlan +// Entry point: exports the plugin's commands and lifecycle hooks. +// ----------------------------------------------------------------------------- + +export { openCanvas, newCanvas, onMessageFromHTMLView } from './NPCanvasView' + +import pluginJson from '../plugin.json' +import { updateSettingData, pluginUpdated } from '@helpers/NPConfiguration' +import { logDebug, logError } from '@helpers/dev' + +export function init(): void { + // Placeholder for future startup work (e.g. checking for plugin updates) +} + +export function onSettingsUpdated(): void { + // Nothing to do yet; settings are read fresh on every command run +} + +export async function onUpdateOrInstall(): Promise { + try { + logDebug(pluginJson, 'onUpdateOrInstall running') + const updateResult = await updateSettingData(pluginJson) + await pluginUpdated(pluginJson, { code: updateResult, message: 'Settings updated' }) + } catch (error) { + logError(pluginJson, `onUpdateOrInstall: ${error.message}`) + } +}