From dd27be67b94c866c9c8e23b96828b1a537977988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Mon, 16 Jun 2025 21:30:46 +0200 Subject: [PATCH 01/24] Tree expansion and keyboard navigation works --- static/css/notes2.css | 5 + static/js/node.mjs | 3 +- static/js/notes2.mjs | 6 +- static/js/tree.mjs | 397 ++++++++++++++++++++++++++++++++++++++ static/less/notes2.less | 6 + views/layouts/main.gotmpl | 23 +-- views/pages/notes2.gotmpl | 6 + 7 files changed, 433 insertions(+), 13 deletions(-) create mode 100644 static/js/tree.mjs diff --git a/static/css/notes2.css b/static/css/notes2.css index b6b0963..74b6392 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -17,6 +17,11 @@ html { display: none; } } +/* +#tree-native { + grid-area: tree; +} +*/ #tree { grid-area: tree; padding: 16px 32px; diff --git a/static/js/node.mjs b/static/js/node.mjs index 92c8879..bd4dadd 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -408,7 +408,8 @@ export class Node { } // Notify the tree that all children are fetched and ready to process. - _notes2.current.tree.fetchChildrenOn(this.UUID) + //_notes2.current.tree.fetchChildrenOn(this.UUID) + _mbus.dispatch(`NODE_CHILDREN_FETCHED_${this.UUID}`) return this.Children }//}}} diff --git a/static/js/notes2.mjs b/static/js/notes2.mjs index d73ef53..22820f0 100644 --- a/static/js/notes2.mjs +++ b/static/js/notes2.mjs @@ -3,6 +3,7 @@ import { signal } from 'preact/signals' import htm from 'htm' import { Node, NodeUI } from 'node' import { ROOT_NODE } from 'node_store' +import { TreeNative, TreeNodeNative } from 'tree' const html = htm.bind(h) export class Notes2 extends Component { @@ -14,6 +15,7 @@ export class Notes2 extends Component { startNode: null, } this.op = signal('') + this.treeNative = new TreeNative() window._sync = new Sync() window._sync.run() @@ -76,6 +78,7 @@ export class Notes2 extends Component { this.nodeUI.current.setNode(node) this.nodeUI.current.setCrumbs(ancestors) this.tree.setSelected(node, dontExpand) + this.treeNative.setSelected(node, dontExpand) }//}}} logout() {//{{{ localStorage.removeItem('session.UUID') @@ -107,6 +110,7 @@ class Tree extends Component { this.treeNodeComponents[node.UUID] = createRef() return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />` }) + return html`
@@ -118,7 +122,7 @@ class Tree extends Component {
` }//}}} componentDidMount() {//{{{ - this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event)) + //this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event)) // This will show and select the treenode that is selected in the node UI. const node = _notes2.current?.nodeUI.current?.node.value diff --git a/static/js/tree.mjs b/static/js/tree.mjs new file mode 100644 index 0000000..3c0a1ff --- /dev/null +++ b/static/js/tree.mjs @@ -0,0 +1,397 @@ +import { ROOT_NODE } from 'node_store' + +export class TreeNative { + constructor() {// {{{ + this.treeNodeComponents = {} + this.treeTrunk = [] + this.expandedNodes = {} // keyed on UUID + this.selectedNode = null + this.rendered = false + + window._mbus.subscribe('TREE_TRUNK_FETCHED', ()=>{ + document.getElementById('tree').append(this.render()) + }) + + this.populateFirstLevel() + }// }}} + render() {// {{{ + if (this.rendered) + alert('Tree should only be rendered once.') + + const tmpl = document.createElement('template') + tmpl.innerHTML = '
' + const treeEl = tmpl.content.getElementById('tree-nodes') + treeEl.addEventListener('keydown', event=>this.keyHandler(event)) + + for (const node of this.treeTrunk) { + const treenode = new TreeNodeNative(this, node) + this.treeNodeComponents[node.UUID] = treenode + treeEl.appendChild(treenode.render()) + + //return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />` + } + + this.rendered = true + return tmpl.content + }// }}} + populateFirstLevel() {//{{{ + nodeStore.get(ROOT_NODE) + .then(node => node.fetchChildren()) + .then(children => { + this.treeNodeComponents = {} + this.treeTrunk = [] + for (const node of children) { + // The root node isn't supposed to be shown in the tree. + if (node.UUID === ROOT_NODE) + continue + if (node.ParentUUID === ROOT_NODE) + this.treeTrunk.push(node) + } + _mbus.dispatch('TREE_TRUNK_FETCHED') + }) + .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) }) + }//}}} + getNodeExpanded(UUID) {//{{{ + if (this.expandedNodes[UUID] === undefined) + this.expandedNodes[UUID] = false + return this.expandedNodes[UUID] + }//}}} + setNodeExpanded(node, value) {//{{{ + // Creating a default value if it doesn't exist already. + this.getNodeExpanded(node.UUID) + this.expandedNodes[node.UUID] = value + _mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value) + }//}}} + setSelected(node, dontExpand) {//{{{ + // The previously selected node, if any, needs to be rerendered + // to not retain its 'selected' class. + const prevUUID = this.selectedNode?.UUID + this.selectedNode = node + if (prevUUID) + this.treeNodeComponents[prevUUID]?.render(true) + + // And now the newly selected node is rerendered. + this.treeNodeComponents[node.UUID]?.render(true) + + if (!dontExpand) + this.setNodeExpanded(node, true) + }//}}} + isSelected(node) {//{{{ + return this.selectedNode?.UUID === node.UUID + }//}}} + + async keyHandler(event) {//{{{ + let handled = true + const n = this.selectedNode + const Space = ' ' + + // This handler would otherwise react to stuff like Ctrl+L. + if (event.ctrlKey || event.altKey) + return + + switch (event.key) { + // Space and enter is toggling expansion. + // Holding shift down does it recursively. + case Space: + case 'Enter': + const expanded = this.getNodeExpanded(n.UUID) + if (event.shiftKey) { + this.recursiveExpand(n, !expanded) + } else { + this.setNodeExpanded(n, !expanded) + } + break + + case 'g': + case 'Home': + this.navigateTop() + break + + case 'G': + case 'End': + this.navigateBottom() + break + + case 'j': + case 'ArrowDown': + await this.navigateDown(this.selectedNode) + break + + case 'k': + case 'ArrowUp': + await this.navigateUp(this.selectedNode) + break + + case 'h': + case 'ArrowLeft': + await this.navigateLeft(this.selectedNode) + break + + case 'l': + case 'ArrowRight': + await this.navigateRight(this.selectedNode) + break + + default: + // nonsole.log(event.key) + handled = false + } + + if (handled) { + event.preventDefault() + event.stopPropagation() + } + }//}}} + async navigateLeft(n) {//{{{ + if (n === null) + return + + const expanded = this.getNodeExpanded(n.UUID) + if (expanded && n.hasChildren()) { + this.setNodeExpanded(n, false) + return + } + + if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) { + await _notes2.current.goToNode(n.getParent()?.UUID, true, true) + return + } + + const siblingBefore = n.getSiblingBefore() + const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID) + if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { + const siblingAbove = this.getLastExpandedNode(siblingBefore) + await _notes2.current.goToNode(siblingAbove?.UUID, true, true) + return + } + + await _notes2.current.goToNode(n.getSiblingBefore()?.UUID, true, true) + }//}}} + async navigateRight(n) {//{{{ + if (n === null) + return + + const siblingAfter = n.getSiblingAfter() + const expanded = this.getNodeExpanded(n.UUID) + + if (!expanded && n.hasChildren()) { + this.setNodeExpanded(n, true) + return + } + + if (expanded && n.hasChildren()) { + await _notes2.current.goToNode(n.Children[0]?.UUID, true, true) + return + } + + if (n.isLastSibling()) { + const nextNode = this.getParentWithNextSibling(n) + await _notes2.current.goToNode(nextNode?.UUID, true, true) + return + } + + await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true) + }//}}} + async navigateUp(n) {//{{{ + if (n === null) + return + + let parent = null + const siblingBefore = n.getSiblingBefore() + let siblingExpanded = false + if (siblingBefore !== null) + siblingExpanded = this.getNodeExpanded(siblingBefore.UUID) + + if (n.isFirstSibling()) { + parent = n.getParent() + if (parent?.UUID === ROOT_NODE) + return + await _notes2.current.goToNode(parent?.UUID, true, true) + return + } + + if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { + await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true) + return + } + + if (siblingBefore) { + await _notes2.current.goToNode(siblingBefore.UUID, true, true) + return + } + }//}}} + async navigateDown(n) {//{{{ + if (n === null) + return + + const nodeExpanded = this.getNodeExpanded(n.UUID) + + // Last node, not expanded, so it matters not whether it has children or not. + // Traverse upward to nearest parent with next sibling. + if (!nodeExpanded && n.isLastSibling()) { + const wantedNode = this.getParentWithNextSibling(n) + await _notes2.current.goToNode(wantedNode?.UUID, true, true) + return + } + + if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) { + const wantedNode = this.getParentWithNextSibling(n) + await _notes2.current.goToNode(wantedNode?.UUID, true, true) + return + } + + // Node not expanded. Go to this node's next sibling. + // GoToNode will abort if given null. + if (!nodeExpanded || !n.hasChildren()) { + await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true) + return + } + + // Node is expanded. + // Children will be visually beneath this node, if any. + if (nodeExpanded && n.hasChildren()) { + await _notes2.current.goToNode(n.Children[0].UUID, true, true) + return + } + }//}}} + async navigateTop() {//{{{ + const root = await nodeStore.get(ROOT_NODE) + if (root.Children.length === 0) + return + await _notes2.current.goToNode(root.Children[0]?.UUID, true, true) + }//}}} + async navigateBottom() {//{{{ + const root = await nodeStore.get(ROOT_NODE) + if (root.Children.length === 0) + return + + const toplevel = root.Children[root.Children.length - 1] + const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID) + + if (toplevelExpanded) { + const lastnode = this.getLastExpandedNode(toplevel) + await _notes2.current.goToNode(lastnode?.UUID, true, true) + } else + await _notes2.current.goToNode(root.Children[root.Children.length - 1]?.UUID, true, true) + }//}}} + + getParentWithNextSibling(node) {//{{{ + let currNode = node + while (currNode !== null && currNode.UUID !== ROOT_NODE && currNode.getSiblingAfter() === null) { + currNode = currNode.getParent() + } + return currNode?.getSiblingAfter() + }//}}} + getLastExpandedNode(node) {//{{{ + let currNode = node + while (this.getNodeExpanded(currNode.UUID) && currNode.hasChildren()) { + currNode = currNode.Children[currNode.Children.length - 1] + } + return currNode + }//}}} + async recursiveExpand(node, state) {//{{{ + if (state) + await this.setNodeExpanded(node, true) + + for (const child of node.Children) + await this.recursiveExpand(child, state) + + if (!state) + await this.setNodeExpanded(node, false) + }//}}} +} + +export class TreeNodeNative { + constructor(tree, node, parent) {//{{{ + this.tree = tree + this.node = node + this.parent = parent + + this.element = document.createElement('div') + this.element.classList.add('node') + this.icon_expand = document.createElement('img') + + this.children_populated = false + + this.createElements() + + _mbus.subscribe(`NODE_CHILDREN_FETCHED_${node.UUID}`, ()=>{ + this.render(true) + }) + + _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state=>{ + this.render(true) + }) + + this.rendered = false + }//}}} + createElements() {// {{{ + this.element.innerHTML = ` +
+
+
+ ` + + this.element.children[0].addEventListener('click', ()=>this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID))) + this.element.children[0].appendChild(this.icon_expand) + this.element.children[1].addEventListener('click', ()=>window._notes2.current.goToNode(this.node.UUID)) + }// }}} + render(force_update) {//{{{ + if (this.rendered && force_update !== true) + return this.element + + // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible. + const expanded = this.node.Children.length > 0 && this.tree.getNodeExpanded(this.node.UUID) + const selected = this.tree.isSelected(this.node) ? 'selected' : '' + + if (!this.children_populated && this.tree.getNodeExpanded(this.parent?.node.UUID)) { + this.node.fetchChildren().then(()=>this.children_populated = true) + } + + // Update the name and selected status + this.element.children[1].innerText = this.node.get('Name') + this.element.children[1].className = `name ${selected}` + + // Update expansion state + this.element.children[2].className = `children ${expanded ? 'expanded' : 'collapsed'}` + + // The expand icon is cached to not get a flickering when re-rendering. + if (this.icon_expand === null) + this.icon_expand = document.createElement('img') + + if (this.node.Children.length === 0) + this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/leaf.svg`) + else if (this.tree.getNodeExpanded(this.node.UUID)) + this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/expanded.svg`) + else + this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/collapsed.svg`) + + // Should children be rendered? + this.element.children[2].innerHTML = '' + let children = [] + if (expanded) + children = this.node.Children.map(node => { + let treenode = this.tree.treeNodeComponents[node.UUID] + if (treenode === undefined) { + treenode = new TreeNodeNative(this.tree, node, this) + this.tree.treeNodeComponents[node.UUID] = treenode + } + return treenode + //return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${tree} node=${node} parent=${this} ref=${tree.treeNodeComponents[node.UUID]} selected=${node.UUID === tree.props.app.startNode?.UUID} />` + }) + + for(const c of children) + this.element.children[2].appendChild(c.render()) + + this.rendered = true + return this.element + }//}}} + + setImgSrc(img, newSrc) {// {{{ + if (img.getAttribute('src') === newSrc) + return + img.setAttribute('src', newSrc) + }// }}} +} + +// vim: foldmethod=marker diff --git a/static/less/notes2.less b/static/less/notes2.less index a1ae783..a5778ed 100644 --- a/static/less/notes2.less +++ b/static/less/notes2.less @@ -46,6 +46,12 @@ html { } } +/* +#tree-native { + grid-area: tree; +} +*/ + #tree { grid-area: tree; padding: 16px 32px; diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl index 123fc68..470cfe5 100644 --- a/views/layouts/main.gotmpl +++ b/views/layouts/main.gotmpl @@ -4,16 +4,6 @@ - - + + diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index f633692..4a828cd 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -1,4 +1,10 @@ {{ define "page" }} +
+
+
+
+
+
From 1ce8e29e376ed61c032c731bfeb570364b4fe13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 25 Jun 2025 14:59:21 +0200 Subject: [PATCH 02/24] Tree render and navigation with note rendering --- static/css/notes2.css | 23 ++- static/js/app.mjs | 298 ++++++++++++++++++++++++++++++++++++++ static/js/mbus.mjs | 37 ++++- static/js/node.mjs | 74 +++++++++- static/js/tree.mjs | 98 +++++++++---- static/less/notes2.less | 27 ++-- views/pages/notes2.gotmpl | 20 ++- 7 files changed, 515 insertions(+), 62 deletions(-) create mode 100644 static/js/app.mjs diff --git a/static/css/notes2.css b/static/css/notes2.css index 74b6392..0a23116 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -17,15 +17,10 @@ html { display: none; } } -/* -#tree-native { - grid-area: tree; -} -*/ #tree { grid-area: tree; - padding: 16px 32px; - background-color: #333; + display: grid; + padding: 16px 0px 16px 16px; color: #ddd; z-index: 100; border-left: 2px solid #333; @@ -37,9 +32,11 @@ html { display: grid; position: relative; justify-items: center; + margin-top: 8px; margin-bottom: 8px; margin-left: 24px; margin-right: 24px; + cursor: pointer; } #tree #logo img { width: 128px; @@ -85,12 +82,18 @@ html { #tree .node .children.collapsed { display: none; } +#tree-nodes { + padding: 16px 32px; + background-color: #333; + border-radius: 8px; + box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75); +} #crumbs { grid-area: crumbs; display: grid; align-items: start; justify-items: center; - margin: 0px 16px; + margin: 16px 16px; } #crumbs .crumbs { display: flex; @@ -114,6 +117,10 @@ html { user-select: none; -webkit-tap-highlight-color: transparent; } +#crumbs .crumbs .crumb a { + text-decoration: none; + color: inherit; +} #crumbs .crumbs .crumb:after { content: "•"; margin-left: 8px; diff --git a/static/js/app.mjs b/static/js/app.mjs new file mode 100644 index 0000000..c6529fc --- /dev/null +++ b/static/js/app.mjs @@ -0,0 +1,298 @@ +import { ROOT_NODE } from 'node_store' +import { TreeNative } from 'tree' +import { NodeUINative } from 'node' + +export class App { + constructor() {// {{{ + this.currentNode = null + this.treeNative = new TreeNative() + this.crumbs = new Crumbs() + this.crumbsElement = document.getElementById('crumbs') + this.nodeUI = new NodeUINative(document.getElementById('note')) + + _mbus.subscribe('TREE_TRUNK_FETCHED', async () => { + document.getElementById('tree').append(this.treeNative.render()) + document.getElementById('tree-nodes')?.focus() + + const startNode = await this.getStartNode() + this.goToNode(startNode.UUID, false, false) + }) + + _mbus.subscribe('TREE_NODE_SELECTED', event => { + const node = event.detail.data + this.goToNode(node.UUID, false, false) + }) + + _mbus.subscribe('GO_TO_NODE', event => { + const node = event.detail.data + this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand) + }) + + window.addEventListener('keydown', event => this.keyHandler(event)) + + window._sync = new Sync() + window._sync.run() + }// }}} + + keyHandler(event) {//{{{ + let handled = true + + // All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees. + // Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving. + // Thus, the exception is acceptable to consequent use of alt+shift. + if (!(event.shiftKey && event.altKey) && !(event.key.toUpperCase() === 'S' && event.ctrlKey)) + return + + switch (event.key.toUpperCase()) { + case 'T': + console.log(document.activeElement.id) + if (document.activeElement.id === 'tree-nodes') + document.getElementById('node-content').focus() + else + document.getElementById('tree-nodes').focus() + break + + case 'F': + _mbus.dispatch('op-search') + break + /* + case 'C': + this.showPage('node') + break + + case 'E': + this.showPage('keys') + break + + case 'M': + this.toggleMarkdown() + break + + case 'N': + this.createNode() + break + + case 'P': + this.showPage('node-properties') + break + + */ + case 'S': + this.saveNode() + /* + else if (this.page.value === 'node-properties') + this.nodeProperties.current.save() + */ + break + /* + + case 'U': + this.showPage('upload') + break + + case 'F': + this.showPage('search') + break + */ + + default: + handled = false + } + + if (handled) { + event.preventDefault() + event.stopPropagation() + } + }//}}} + async getStartNode() {//{{{ + let nodeUUID = ROOT_NODE + + // Is a UUID provided on the URI as an anchor? + const parts = document.URL.split('#') + if (parts[1]?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) + nodeUUID = parts[1] + + return await nodeStore.get(nodeUUID) + }//}}} + async saveNode() {//{{{ + if (!this.currentNode.isModified()) + return + + /* The node history is a local store for node history. + * This could be provisioned from the server or cleared if + * deemed unnecessary. + * + * The send queue is what will be sent back to the server + * to have a recorded history of the notes. + * + * A setting to be implemented in the future could be to + * not save the history locally at all. */ + const node = this.currentNode + + // The node is still in its old state and will present + // the unmodified content to the node store. + const history = nodeStore.nodesHistory.add(node) + + // Prepares the node object for saving. + // Sets Updated value to current date and time. + await node.save() + + // Updated node is added to the send queue to be stored on server. + const sendQueue = nodeStore.sendQueue.add(node) + + // Updated node is saved to the primary node store. + const nodeStoreAdding = nodeStore.add([node]) + + await Promise.all([history, sendQueue, nodeStoreAdding]) + }//}}} + async goToNode(nodeUUID, dontPush, dontExpand) {//{{{ + if (nodeUUID === null || nodeUUID === undefined) + return + + // Don't switch notes until saved. + if (this.nodeUI.isModified()) { + if (!confirm("Changes not saved. Do you want to discard changes?")) + return + } + + if (!dontPush) + history.pushState({ nodeUUID }, '', `/notes2#${nodeUUID}`) + + const node = nodeStore.node(nodeUUID) + + node.reset() // any modifications are discarded. + + this.currentNode = node + this.treeNative.setSelected(node, dontExpand) + + const ancestors = await nodeStore.getNodeAncestry(node) + _mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render())) + _mbus.dispatch('NODE_UI_OPEN', node) + _mbus.dispatch('NODE_UNMODIFIED') + + // Scrolls node into view. + this.treeNative.makeVisible(node) + }//}}} +} + +class Crumbs { + constructor() {// {{{ + this.crumbs = [] + this.crumbsDiv = document.createElement('div') + this.crumbsDiv.classList.add('crumbs') + + _mbus.subscribe('CRUMBS_SET', event => { + this.crumbs = event.detail.data + }) + }// }}} + render() {// {{{ + const crumbs = this.crumbs.map(node => + (new Crumb( + node.get('Name'), + node.UUID, + )).render() + ) + + const start = (new Crumb('Start', ROOT_NODE)).render() + crumbs.push(start) + + this.crumbsDiv.replaceChildren(...crumbs.reverse()) + return this.crumbsDiv + }// }}} +} + +class Crumb { + constructor(label, uuid) {// {{{ + this.label = label + this.uuid = uuid + }// }}} + render() {// {{{ + const crumb = document.createElement('div') + crumb.classList.add('crumb') + + const link = document.createElement('a') + link.href = `/notes2#${this.uuid}` + link.innerText = this.label + + crumb.appendChild(link) + return crumb + + }// }}} +} + +class Op { + constructor(id) { + this.id = id + _mbus.subscribe(this.id, p => this.render(p)) + } + render(html) { + const op = document.getElementById('op') + const t = document.createElement('template') + t.innerHTML = `${html}` + op.replaceChildren(t.content) + document.getElementById(this.id).showModal() + } + get(selector) { + return document.querySelector(`#${this.id} ${selector}`) + } + bind(selector, event, fn) { + this.get(selector).addEventListener(event, evt => fn(evt)) + } +} + +function tmpl(html) { + const el = document.createElement('template') + el.innerHTML = html + return el.content.children +} + +class OpSearch extends Op { + constructor() { + super('op-search') + } + + render() { + super.render(` +
Search
+
+ +
+
Results
+
+ `) + + this.bind('input[type="text"]', 'keydown', evt => this.search(evt)) + } + + search(event) { + if (event.key !== 'Enter') + return + + const searchFor = document.querySelector('#op-search input').value + nodeStore.search(searchFor, ROOT_NODE) + .then(res => this.displayResults(res)) + } + + displayResults(results) { + const rs = [] + for (const r of results) { + const ancestors = r.ancestry.reverse().map(a => { + const div = tmpl(`
${a.data.Name}
`) + div[0].addEventListener('click', ()=>_notes2.current.goToNode(a.UUID)) + return div[0] + }) + + + const div = tmpl(`
${r.name}
`) + div[0].addEventListener('click', ()=>_notes2.current.goToNode(r.uuid)) + rs.push(...div) + + const ancDev = tmpl('
') + ancDev[0].append(...ancestors) + rs.push(ancDev[0]) + } + this.get('.results').replaceChildren(...rs) + } +} + +// vim: foldmethod=marker diff --git a/static/js/mbus.mjs b/static/js/mbus.mjs index da5f098..0daa63c 100644 --- a/static/js/mbus.mjs +++ b/static/js/mbus.mjs @@ -1,17 +1,48 @@ export class MessageBus { constructor() { + this.log = false this.bus = new EventTarget() } subscribe(eventName, fn) { - this.bus.addEventListener(eventName, fn) + if (this.log) { + console.groupCollapsed('MBUS subscribe - ', eventName); + console.trace(); // hidden in collapsed group + console.groupEnd(); + } + + this.bus.addEventListener(eventName, event=>{ + fn(event) + if (event.detail.callback !== undefined) + event.detail.callback(event) + }) } unsubscribe(eventName, fn) { + if (this.log) { + console.groupCollapsed('MBUS unsubscribe - ', eventName); + console.trace(); // hidden in collapsed group + console.groupEnd(); + } + this.bus.removeEventListener(eventName, fn) } - dispatch(eventName, data) { - this.bus.dispatchEvent(new CustomEvent(eventName, { detail: data })) + dispatch(eventName, data, callback) { + if (this.log) { + console.groupCollapsed('MBUS dispatch - ', eventName); + console.log('data', data); + console.trace(); // hidden in collapsed group + console.groupEnd(); + } + + const event = new CustomEvent(eventName, { + detail: { + data, + callback, + } + }) + + this.bus.dispatchEvent(event) } } diff --git a/static/js/node.mjs b/static/js/node.mjs index bd4dadd..7c2d64e 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -331,6 +331,61 @@ class NodeContent extends Component { }//}}} } +export class NodeUINative { + constructor(parentElement) {// {{{ + this.node = null + this.parent = parentElement + this.parent.replaceChildren(this.createElements()) + + _mbus.subscribe('NODE_UI_OPEN', event => { + this.node = event.detail.data + this.render() + }) + + _mbus.subscribe('NODE_MODIFIED', ()=>{ + document.querySelector('#crumbs .crumbs')?.classList.add('node-modified') + }) + + _mbus.subscribe('NODE_UNMODIFIED', ()=>{ + document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified') + }) + }// }}} + createElements() {// {{{ + const tmpl = document.createElement('template') + tmpl.innerHTML = ` +
+ + ` + + tmpl.content.querySelector('#node-content').addEventListener('input', event=>this.contentChanged(event)) + + return tmpl.content + }// }}} + render() {// {{{ + this.parent.querySelector('.grow-wrap').style.display = (this.node === null ? 'none' : 'grid') + this.parent.querySelector('#name').innerText = this.node?.get('Name') ?? '' + this.parent.querySelector('#node-content').value = this.node?.get('Content') ?? '' + + this.resize() + return this.parent + }// }}} + + resize() {//{{{ + const textarea = this.parent.querySelector('#node-content') + textarea.parentNode.dataset.replicatedValue = textarea.value + }//}}} + contentChanged(event) {//{{{ + this.node.setContent(event.target.value) + this.resize() + }//}}} + isModified() {// {{{ + return this.node?.isModified() + }// }}} + +} + export class Node { static sort(a, b) {//{{{ if (a.data.Name < b.data.Name) return -1 @@ -340,7 +395,6 @@ export class Node { constructor(nodeData, level) {//{{{ this.Level = level this.data = nodeData - this.UUID = nodeData.UUID // Toplevel nodes are normalized to have the ROOT_NODE as parent. @@ -354,13 +408,12 @@ export class Node { this.Children = [] this.Ancestors = [] - this._content = this.data.Content - this._modified = false - this._sibling_before = null this._sibling_after = null this._parent = null + this.reset() + /* this.RenderMarkdown = signal(nodeData.RenderMarkdown) this.Markdown = false @@ -377,6 +430,10 @@ export class Node { */ }//}}} + reset() {// {{{ + this._content = this.data.Content + this._modified = false + }// }}} get(prop) {//{{{ return this.data[prop] }//}}} @@ -384,6 +441,9 @@ export class Node { // '2024-12-17T17:33:48.85939Z return new Date(Date.parse(this.data.Updated)) }//}}} + isModified() {// {{{ + return this._modified + }// }}} hasFetchedChildren() {//{{{ return this._children_fetched }//}}} @@ -436,12 +496,12 @@ export class Node { if (this.CryptoKeyID != 0 && !this._decrypted) this.#decrypt() */ - this.modified = true return this._content }//}}} - setContent(new_content) {//{{{ this._content = new_content + this._modified = true + _mbus.dispatch('NODE_MODIFIED') /* TODO - implement crypto if (this.CryptoKeyID == 0) // Logic behind plaintext not being decrypted is that @@ -456,6 +516,8 @@ export class Node { this.data.Updated = new Date().toISOString() this._modified = false + _mbus.dispatch('NODE_UNMODIFIED') + // When stored into database and ancestry was changed, // the ancestry path could be interesting. const ancestors = await nodeStore.getNodeAncestry(this) diff --git a/static/js/tree.mjs b/static/js/tree.mjs index 3c0a1ff..2bf9f7a 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -8,10 +8,6 @@ export class TreeNative { this.selectedNode = null this.rendered = false - window._mbus.subscribe('TREE_TRUNK_FETCHED', ()=>{ - document.getElementById('tree').append(this.render()) - }) - this.populateFirstLevel() }// }}} render() {// {{{ @@ -19,10 +15,25 @@ export class TreeNative { alert('Tree should only be rendered once.') const tmpl = document.createElement('template') - tmpl.innerHTML = '
' + tmpl.innerHTML = ` +
+ +
+ + +
+
` + + /* + onclick=${() => _mbus.dispatch('op-search')} + onclick=${() => _sync.run()} + */ + const treeEl = tmpl.content.getElementById('tree-nodes') treeEl.addEventListener('keydown', event=>this.keyHandler(event)) + tmpl.content.getElementById('logo').addEventListener('click', ()=>_app.goToNode(ROOT_NODE, false, false)) + for (const node of this.treeTrunk) { const treenode = new TreeNodeNative(this, node) this.treeNodeComponents[node.UUID] = treenode @@ -57,12 +68,23 @@ export class TreeNative { return this.expandedNodes[UUID] }//}}} setNodeExpanded(node, value) {//{{{ - // Creating a default value if it doesn't exist already. - this.getNodeExpanded(node.UUID) + let expanded = this.expandedNodes[node.UUID] + + if (expanded === undefined) { + this.expandedNodes[node.UUID] = false + expanded = false + } + + if (expanded === value) + return + this.expandedNodes[node.UUID] = value _mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value) }//}}} setSelected(node, dontExpand) {//{{{ + if (node === undefined) + return + // The previously selected node, if any, needs to be rerendered // to not retain its 'selected' class. const prevUUID = this.selectedNode?.UUID @@ -143,7 +165,7 @@ export class TreeNative { } }//}}} async navigateLeft(n) {//{{{ - if (n === null) + if (n === null || n === undefined) return const expanded = this.getNodeExpanded(n.UUID) @@ -153,7 +175,7 @@ export class TreeNative { } if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) { - await _notes2.current.goToNode(n.getParent()?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: true, dontExpand: true }) return } @@ -161,14 +183,14 @@ export class TreeNative { const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID) if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { const siblingAbove = this.getLastExpandedNode(siblingBefore) - await _notes2.current.goToNode(siblingAbove?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: true, dontExpand: true }) return } - await _notes2.current.goToNode(n.getSiblingBefore()?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: true, dontExpand: true }) }//}}} async navigateRight(n) {//{{{ - if (n === null) + if (n === null || n === undefined) return const siblingAfter = n.getSiblingAfter() @@ -180,20 +202,20 @@ export class TreeNative { } if (expanded && n.hasChildren()) { - await _notes2.current.goToNode(n.Children[0]?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: true, dontExpand: true }) return } if (n.isLastSibling()) { const nextNode = this.getParentWithNextSibling(n) - await _notes2.current.goToNode(nextNode?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: true, dontExpand: true }) return } - await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true }) }//}}} async navigateUp(n) {//{{{ - if (n === null) + if (n === null || n === undefined) return let parent = null @@ -206,22 +228,22 @@ export class TreeNative { parent = n.getParent() if (parent?.UUID === ROOT_NODE) return - await _notes2.current.goToNode(parent?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: true, dontExpand: true }) return } if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { - await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: true, dontExpand: true }) return } if (siblingBefore) { - await _notes2.current.goToNode(siblingBefore.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: true, dontExpand: true }) return } }//}}} async navigateDown(n) {//{{{ - if (n === null) + if (n === null || n === undefined) return const nodeExpanded = this.getNodeExpanded(n.UUID) @@ -230,27 +252,27 @@ export class TreeNative { // Traverse upward to nearest parent with next sibling. if (!nodeExpanded && n.isLastSibling()) { const wantedNode = this.getParentWithNextSibling(n) - await _notes2.current.goToNode(wantedNode?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true }) return } if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) { const wantedNode = this.getParentWithNextSibling(n) - await _notes2.current.goToNode(wantedNode?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true }) return } // Node not expanded. Go to this node's next sibling. // GoToNode will abort if given null. if (!nodeExpanded || !n.hasChildren()) { - await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true }) return } // Node is expanded. // Children will be visually beneath this node, if any. if (nodeExpanded && n.hasChildren()) { - await _notes2.current.goToNode(n.Children[0].UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: true, dontExpand: true }) return } }//}}} @@ -258,7 +280,7 @@ export class TreeNative { const root = await nodeStore.get(ROOT_NODE) if (root.Children.length === 0) return - await _notes2.current.goToNode(root.Children[0]?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: true, dontExpand: true }) }//}}} async navigateBottom() {//{{{ const root = await nodeStore.get(ROOT_NODE) @@ -270,9 +292,9 @@ export class TreeNative { if (toplevelExpanded) { const lastnode = this.getLastExpandedNode(toplevel) - await _notes2.current.goToNode(lastnode?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: true, dontExpand: true }) } else - await _notes2.current.goToNode(root.Children[root.Children.length - 1]?.UUID, true, true) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: true, dontExpand: true }) }//}}} getParentWithNextSibling(node) {//{{{ @@ -299,6 +321,17 @@ export class TreeNative { if (!state) await this.setNodeExpanded(node, false) }//}}} + async makeVisible(node) {// {{{ + const treenode = this.treeNodeComponents[node.UUID] + + const ancestors = await nodeStore.getNodeAncestry(node) + for (const ancestor of ancestors.reverse()) { + this.setNodeExpanded(ancestor, true) + } + + // The ROOT_NODE for example hasn't got a treenode. + treenode?.element.scrollIntoView({ block: 'nearest' }) + }// }}} } export class TreeNodeNative { @@ -312,6 +345,7 @@ export class TreeNodeNative { this.icon_expand = document.createElement('img') this.children_populated = false + this.rendered = false this.createElements() @@ -323,7 +357,8 @@ export class TreeNodeNative { this.render(true) }) - this.rendered = false + if (this.node.Level === 0 || this.tree.getNodeExpanded(this.node.UUID)) + this.fetchChildren() }//}}} createElements() {// {{{ this.element.innerHTML = ` @@ -334,8 +369,13 @@ export class TreeNodeNative { this.element.children[0].addEventListener('click', ()=>this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID))) this.element.children[0].appendChild(this.icon_expand) - this.element.children[1].addEventListener('click', ()=>window._notes2.current.goToNode(this.node.UUID)) + + this.element.children[1].addEventListener('click', ()=>_mbus.dispatch('TREE_NODE_SELECTED', this.node)) }// }}} + async fetchChildren() {//{{{ + await this.node.fetchChildren() + this.children_populated = true + }//}}} render(force_update) {//{{{ if (this.rendered && force_update !== true) return this.element diff --git a/static/less/notes2.less b/static/less/notes2.less index a5778ed..2c57f53 100644 --- a/static/less/notes2.less +++ b/static/less/notes2.less @@ -46,16 +46,10 @@ html { } } -/* -#tree-native { - grid-area: tree; -} -*/ - #tree { grid-area: tree; - padding: 16px 32px; - background-color: #333; + display: grid; + padding: 16px 0px 16px 16px; color: #ddd; z-index: 100; // Over crumbs shadow border-left: 2px solid #333; @@ -68,9 +62,12 @@ html { display: grid; position: relative; justify-items: center; + margin-top: 8px; margin-bottom: 8px; margin-left: 24px; margin-right: 24px; + cursor: pointer; + img { width: 128px; left: -20px; @@ -130,12 +127,19 @@ html { } } +#tree-nodes { + padding: 16px 32px; + background-color: #333; + border-radius: 8px; + box-shadow: 5px 5px 10px -5px rgba(0,0,0,0.75); +} + #crumbs { grid-area: crumbs; display: grid; align-items: start; justify-items: center; - margin: 0px 16px; + margin: 16px 16px; .crumbs { background: #e4e4e4; @@ -160,6 +164,11 @@ html { cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; + + a { + text-decoration: none; + color: inherit; + } } .crumb:after { diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index 4a828cd..52d128a 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -1,11 +1,18 @@ {{ define "page" }} -
-
-
+
+
+
+
+
+ +
+
-
{{ end }} From 454d065baa1d2d26a59643d30d8e3e195cb4fef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sun, 3 May 2026 09:51:48 +0200 Subject: [PATCH 10/24] push/popState is handled better --- static/js/app.mjs | 6 +++++- static/js/tree.mjs | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/static/js/app.mjs b/static/js/app.mjs index c68475e..16dab2b 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -30,6 +30,7 @@ export class App { }) window.addEventListener('keydown', event => this.keyHandler(event)) + window.addEventListener('popstate', event => this.popState(event)) document.getElementById('notes2').addEventListener('click', event => { if (event.target.id === 'notes2') document.getElementById('node-content')?.focus() @@ -114,6 +115,9 @@ export class App { event.stopPropagation() } }//}}} + popState(event) {// {{{ + _mbus.dispatch("GO_TO_NODE", { nodeUUID: event.state.nodeUUID, dontPush: true, dontExpand: true }) + }// }}} async getStartNode() {//{{{ let nodeUUID = ROOT_NODE @@ -181,7 +185,6 @@ export class App { history.pushState({ nodeUUID }, '', `/notes2#${nodeUUID}`) const node = nodeStore.node(nodeUUID) - node.reset() // any modifications are discarded. this.currentNode = node @@ -246,6 +249,7 @@ class N2Crumb extends CustomHTMLElement { this.elLink.href = `/notes2#${this.uuid}` this.elLink.innerText = this.label + this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true })) }// }}} } customElements.define('n2-crumb', N2Crumb) diff --git a/static/js/tree.mjs b/static/js/tree.mjs index c76369f..d7a11ee 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -175,7 +175,7 @@ export class N2Tree extends CustomHTMLElement { } if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) { - _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: false, dontExpand: true }) return } @@ -183,11 +183,11 @@ export class N2Tree extends CustomHTMLElement { const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID) if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { const siblingAbove = this.getLastExpandedNode(siblingBefore) - _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: false, dontExpand: true }) return } - _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: false, dontExpand: true }) }//}}} async navigateRight(n) {//{{{ if (n === null || n === undefined) @@ -202,17 +202,17 @@ export class N2Tree extends CustomHTMLElement { } if (expanded && n.hasChildren()) { - _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: false, dontExpand: true }) return } if (n.isLastSibling()) { const nextNode = this.getParentWithNextSibling(n) - _mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: false, dontExpand: true }) return } - _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true }) }//}}} async navigateUp(n) {//{{{ if (n === null || n === undefined) @@ -228,17 +228,17 @@ export class N2Tree extends CustomHTMLElement { parent = n.getParent() if (parent?.UUID === ROOT_NODE) return - _mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: false, dontExpand: true }) return } if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { - _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: false, dontExpand: true }) return } if (siblingBefore) { - _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: false, dontExpand: true }) return } }//}}} @@ -252,27 +252,27 @@ export class N2Tree extends CustomHTMLElement { // Traverse upward to nearest parent with next sibling. if (!nodeExpanded && n.isLastSibling()) { const wantedNode = this.getParentWithNextSibling(n) - _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, dontExpand: true }) return } if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) { const wantedNode = this.getParentWithNextSibling(n) - _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, dontExpand: true }) return } // Node not expanded. Go to this node's next sibling. // GoToNode will abort if given null. if (!nodeExpanded || !n.hasChildren()) { - _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true }) return } // Node is expanded. // Children will be visually beneath this node, if any. if (nodeExpanded && n.hasChildren()) { - _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: false, dontExpand: true }) return } }//}}} @@ -280,7 +280,7 @@ export class N2Tree extends CustomHTMLElement { const root = await nodeStore.get(ROOT_NODE) if (root.Children.length === 0) return - _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: false, dontExpand: true }) }//}}} async navigateBottom() {//{{{ const root = await nodeStore.get(ROOT_NODE) @@ -292,9 +292,9 @@ export class N2Tree extends CustomHTMLElement { if (toplevelExpanded) { const lastnode = this.getLastExpandedNode(toplevel) - _mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: false, dontExpand: true }) } else - _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: true, dontExpand: true }) + _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: false, dontExpand: true }) }//}}} getParentWithNextSibling(node) {//{{{ From 26ca510785717de7ffcce1503a43b35461671c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sun, 3 May 2026 19:45:39 +0200 Subject: [PATCH 11/24] Fixed tree reset after sync, optimized sync with IndexedDB --- static/js/app.mjs | 8 ++--- static/js/node.mjs | 7 ++-- static/js/node_store.mjs | 70 +++++++++++++++++++++++----------------- static/js/sync.mjs | 28 +++++++++------- static/js/tree.mjs | 11 ++++++- 5 files changed, 72 insertions(+), 52 deletions(-) diff --git a/static/js/app.mjs b/static/js/app.mjs index 16dab2b..45cd85b 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -6,13 +6,13 @@ import { Node } from 'node' export class App { constructor() {// {{{ this.currentNode = null - this.treeNative = new N2Tree() + this.tree = new N2Tree() this.crumbs = new N2Crumbs() this.crumbsElement = document.getElementById('crumbs') this.nodeUI = document.getElementById('note') _mbus.subscribe('TREE_TRUNK_FETCHED', async () => { - document.getElementById('tree').append(this.treeNative.render()) + document.getElementById('tree').append(this.tree.render()) document.getElementById('tree-nodes')?.focus() const startNode = await this.getStartNode() @@ -188,7 +188,7 @@ export class App { node.reset() // any modifications are discarded. this.currentNode = node - this.treeNative.setSelected(node, dontExpand) + this.tree.setSelected(node, dontExpand) const ancestors = await nodeStore.getNodeAncestry(node) _mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render())) @@ -196,7 +196,7 @@ export class App { _mbus.dispatch('NODE_UNMODIFIED') // Scrolls node into view. - this.treeNative.makeVisible(node) + this.tree.makeVisible(node) }//}}} } diff --git a/static/js/node.mjs b/static/js/node.mjs index 949724c..d611c64 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -54,7 +54,7 @@ export class Node { if (a.data.Name > b.data.Name) return 0 return 0 }//}}} - static create(name, parentUUID) { + static create(name, parentUUID) {// {{{ return new Node({ UUID: uuidv7(), Created: (new Date()).toISOString(), @@ -64,7 +64,7 @@ export class Node { Markdown: false, History: false, }) - } + }// }}} constructor(nodeData, level) {//{{{ @@ -123,9 +123,6 @@ export class Node { return this._children_fetched }//}}} async fetchChildren() {//{{{ - if (this._children_fetched) - return this.Children - this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1) this._children_fetched = true diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index 98642d1..e849e29 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -159,6 +159,7 @@ export class NodeStore { }) }//}}} + /* upsertNodeRecords(records) {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction('nodes', 'readwrite') @@ -187,16 +188,10 @@ export class NodeStore { record.modified = 0 addReq = nodeStore.put(record) } - addReq.onsuccess = () => { - console.debug(`${op} ${record.UUID} (${record.Name})`) - } - addReq.onerror = (event) => { - console.log(`error ${op} ${record.UUID}`, event.target.error) - reject(event.target.error) - } } }) }//}}} + */ getTreeNodes(parent, newLevel) {//{{{ return new Promise((resolve, reject) => { // Parent of toplevel nodes is ROOT_NODE in indexedDB. @@ -219,7 +214,7 @@ export class NodeStore { req.onerror = (event) => reject(event.target.error) }) }//}}} - async search(searchfor, parent) {//{{{ + search(searchfor, parent) {//{{{ return new Promise((resolve, reject) => { const trx = this.db.transaction('nodes', 'readonly') const nodeStore = trx.objectStore('nodes') @@ -249,43 +244,55 @@ export class NodeStore { }) }//}}} - add(records) {//{{{ + add(records, objstore) {//{{{ return new Promise((resolve, reject) => { try { - const t = this.db.transaction('nodes', 'readwrite') - const nodeStore = t.objectStore('nodes') - t.onerror = (event) => { - console.error('transaction error', event.target.error) - reject(event.target.error) + // A nodestore can be provided in order to + // avoid creating new transactions. + let nodeStore = objstore + let t + + if (nodeStore === undefined) { + t = this.db.transaction('nodes', 'readwrite') + nodeStore = t.objectStore('nodes') + + t.oncomplete = (_event) => { + resolve() + } + + t.onerror = (event) => { + console.error('transaction error', event.target.error) + reject(event.target.error) + } } // records is an object, not an array. - const promises = [] for (const recordIdx in records) { const record = records[recordIdx] - const addReq = nodeStore.put(record.data) - - const promise = new Promise((resolve, reject) => { - addReq.onsuccess = () => resolve() - addReq.onerror = (event) => { - console.error('Error!', event.target.error, record.ID) - reject(event.target.error) - } - }) - promises.push(promise) + nodeStore.put(record.data) } - Promise.all(promises).then(() => resolve()) + resolve() } catch (e) { - console.log(e) + console.error(e) + reject(e) } }) }//}}} - get(uuid) {//{{{ + get(uuid, suppliedNodestore) {//{{{ return new Promise((resolve, reject) => { - const trx = this.db.transaction('nodes', 'readonly') - const nodeStore = trx.objectStore('nodes') + // A nodestore can be provided in order to + // avoid creating new transactions. + let trx + let nodeStore = suppliedNodestore + + if (nodeStore === undefined) { + trx = this.db.transaction('nodes', 'readonly') + nodeStore = trx.objectStore('nodes') + } + const getRequest = nodeStore.get(uuid) + getRequest.onsuccess = (event) => { // Node not found in IndexedDB. if (event.target.result === undefined) { @@ -328,6 +335,9 @@ export class NodeStore { }) }//}}} + newTransaction(objectStore, mode) {// {{{ + return this.db.transaction(objectStore, mode) + }// }}} nodeCount() {//{{{ return new Promise((resolve, reject) => { diff --git a/static/js/sync.mjs b/static/js/sync.mjs index fd606f3..9b58cf7 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -65,9 +65,16 @@ export class Sync { * sync be preserved in the backend. */ let backendNode = null + + // Create a single transaction to be used in the chain of + // this sync. Otherwise it would take more time to create + // transactions for each node. + const trx = nodeStore.newTransaction('nodes', 'readwrite') + const objstore = trx.objectStore('nodes') + for (const i in res.Nodes) { backendNode = new Node(res.Nodes[i], -1) - await window._sync.handleNode(backendNode) + await this.handleNode(backendNode, objstore) handled++ if (handled % 100 === 0) @@ -88,16 +95,16 @@ export class Sync { } return (syncEnd - syncStart) }//}}} - async handleNode(backendNode) {//{{{ + async handleNode(backendNode, objstore) {//{{{ try { /* Retrieving the local copy of this node from IndexedDB. * The backend node can be discarded if it is older than * the local copy since it is considered history preserved * in the backend. */ - return nodeStore.get(backendNode.UUID) - .then(async localNode => { + return nodeStore.get(backendNode.UUID, objstore) + .then(localNode => { if (localNode.updated() >= backendNode.updated()) { - console.log(`History from backend: ${backendNode.UUID}`) + console.debug(`History from backend: ${backendNode.UUID}`) return } @@ -107,12 +114,12 @@ export class Sync { * * If the local node has seen change, the change is already * placed into the send_queue anyway. */ - return nodeStore.add([backendNode]) + return nodeStore.add([backendNode], objstore) }) - .catch(async () => { + .catch(() => { // Not found in IndexedDB - OK to just insert since it only exists in backend. - return nodeStore.add([backendNode]) + return nodeStore.add([backendNode], objstore) }) } catch (e) { console.error(e) @@ -198,10 +205,7 @@ export class N2SyncProgress extends CustomHTMLElement { break // Reload the tree nodes to reflect the new/updated nodes. - if (window._notes2?.current?.reloadTree.value !== null) { - nodeStore.purgeCache() - window._notes2.current.reloadTree.value = window._notes2.current.reloadTree.value + 1 - } + window._app.tree.reset() break } this.render() diff --git a/static/js/tree.mjs b/static/js/tree.mjs index d7a11ee..1da5dee 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -10,6 +10,7 @@ export class N2Tree extends CustomHTMLElement {
+
` }// }}} @@ -39,12 +40,20 @@ export class N2Tree extends CustomHTMLElement { for (const node of this.treeTrunk) { const treenode = new N2TreeNode(this, node) this.treeNodeComponents[node.UUID] = treenode - this.appendChild(treenode.render()) + this.elTreenodes.appendChild(treenode.render()) } this.rendered = true return this }// }}} + reset() { + console.log('tree reset') + this.treeNodeComponents = {} + this.treeTrunk = [] + this.rendered = false + this.elTreenodes.replaceChildren() + this.populateFirstLevel() + } populateFirstLevel() {//{{{ nodeStore.get(ROOT_NODE) .then(node => node.fetchChildren()) From 5a0340c22627f9c413de215e97e6e81212140c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 15 May 2026 08:22:43 +0200 Subject: [PATCH 12/24] Added markdown rendering --- static/css/markdown.css | 30 + static/css/notes2.css | 63 +- static/images/icon_markdown.svg | 57 + static/images/icon_markdown_hollow.svg | 50 + static/images/icon_save.svg | 49 + static/images/icon_save_disabled.svg | 49 + static/js/app.mjs | 4 +- static/js/lib/node_modules/.package-lock.json | 18 +- .../marked-token-position/LICENSE | 21 + .../marked-token-position/README.md | 160 + .../marked-token-position/lib/index.d.ts | 59 + .../marked-token-position/lib/index.esm.js | 6 + .../lib/index.esm.js.map | 7 + .../marked-token-position/lib/index.umd.js | 9 + .../lib/index.umd.js.map | 7 + .../marked-token-position/package.json | 66 + .../marked-token-position/src/index.ts | 192 ++ .../marked/{LICENSE.md => LICENSE} | 0 static/js/lib/node_modules/marked/README.md | 10 +- static/js/lib/node_modules/marked/bin/main.js | 45 +- .../js/lib/node_modules/marked/bin/marked.js | 3 +- .../js/lib/node_modules/marked/lib/marked.cjs | 2442 --------------- .../node_modules/marked/lib/marked.cjs.map | 1 - .../lib/node_modules/marked/lib/marked.d.cts | 638 ---- .../lib/node_modules/marked/lib/marked.d.ts | 644 ++-- .../lib/node_modules/marked/lib/marked.esm.js | 2481 +--------------- .../node_modules/marked/lib/marked.esm.js.map | 8 +- .../lib/node_modules/marked/lib/marked.umd.js | 2507 +--------------- .../node_modules/marked/lib/marked.umd.js.map | 8 +- .../js/lib/node_modules/marked/man/marked.1 | 6 +- .../lib/node_modules/marked/man/marked.1.md | 3 +- .../js/lib/node_modules/marked/marked.min.js | 6 - .../js/lib/node_modules/marked/package.json | 113 +- static/js/lib/node_modules/preact/LICENSE | 21 + static/js/lib/node_modules/preact/README.md | 185 ++ .../js/lib/node_modules/preact/compat/LICENSE | 21 + .../node_modules/preact/compat/client.d.ts | 13 + .../lib/node_modules/preact/compat/client.js | 21 + .../lib/node_modules/preact/compat/client.mjs | 24 + .../node_modules/preact/compat/dist/compat.js | 2 + .../preact/compat/dist/compat.js.map | 1 + .../preact/compat/dist/compat.mjs | 2 + .../preact/compat/dist/compat.module.js | 2 + .../preact/compat/dist/compat.module.js.map | 1 + .../preact/compat/dist/compat.umd.js | 2 + .../preact/compat/dist/compat.umd.js.map | 1 + .../preact/compat/jsx-dev-runtime.js | 3 + .../preact/compat/jsx-dev-runtime.mjs | 3 + .../node_modules/preact/compat/jsx-runtime.js | 3 + .../preact/compat/jsx-runtime.mjs | 3 + .../node_modules/preact/compat/package.json | 51 + .../node_modules/preact/compat/scheduler.js | 15 + .../node_modules/preact/compat/scheduler.mjs | 23 + .../preact/compat/server.browser.js | 11 + .../lib/node_modules/preact/compat/server.js | 26 + .../lib/node_modules/preact/compat/server.mjs | 15 + .../preact/compat/src/Children.js | 21 + .../preact/compat/src/PureComponent.js | 16 + .../preact/compat/src/forwardRef.js | 47 + .../node_modules/preact/compat/src/hooks.js | 70 + .../node_modules/preact/compat/src/index.d.ts | 346 +++ .../node_modules/preact/compat/src/index.js | 238 ++ .../preact/compat/src/internal.d.ts | 48 + .../node_modules/preact/compat/src/memo.js | 34 + .../node_modules/preact/compat/src/portals.js | 75 + .../node_modules/preact/compat/src/render.js | 313 ++ .../preact/compat/src/suspense-list.d.ts | 16 + .../preact/compat/src/suspense-list.js | 127 + .../preact/compat/src/suspense.d.ts | 19 + .../preact/compat/src/suspense.js | 273 ++ .../node_modules/preact/compat/src/util.js | 33 + .../node_modules/preact/compat/test-utils.js | 1 + .../js/lib/node_modules/preact/debug/LICENSE | 21 + .../node_modules/preact/debug/dist/debug.js | 2 + .../preact/debug/dist/debug.js.map | 1 + .../node_modules/preact/debug/dist/debug.mjs | 2 + .../preact/debug/dist/debug.module.js | 2 + .../preact/debug/dist/debug.module.js.map | 1 + .../preact/debug/dist/debug.umd.js | 2 + .../preact/debug/dist/debug.umd.js.map | 1 + .../node_modules/preact/debug/package.json | 27 + .../preact/debug/src/check-props.js | 54 + .../preact/debug/src/component-stack.js | 146 + .../preact/debug/src/constants.js | 3 + .../node_modules/preact/debug/src/debug.js | 594 ++++ .../node_modules/preact/debug/src/index.d.ts | 4 + .../node_modules/preact/debug/src/index.js | 6 + .../preact/debug/src/internal.d.ts | 82 + .../lib/node_modules/preact/debug/src/util.js | 15 + .../lib/node_modules/preact/devtools/LICENSE | 21 + .../preact/devtools/dist/devtools.js | 2 + .../preact/devtools/dist/devtools.js.map | 1 + .../preact/devtools/dist/devtools.mjs | 2 + .../preact/devtools/dist/devtools.module.js | 2 + .../devtools/dist/devtools.module.js.map | 1 + .../preact/devtools/dist/devtools.umd.js | 2 + .../preact/devtools/dist/devtools.umd.js.map | 1 + .../node_modules/preact/devtools/package.json | 25 + .../preact/devtools/src/devtools.js | 21 + .../preact/devtools/src/index.d.ts | 8 + .../node_modules/preact/devtools/src/index.js | 15 + .../js/lib/node_modules/preact/dist/preact.js | 2 + .../node_modules/preact/dist/preact.js.map | 1 + .../node_modules/preact/dist/preact.min.js | 2 + .../preact/dist/preact.min.js.map | 1 + .../preact/dist/preact.min.module.js | 2 + .../preact/dist/preact.min.module.js.map | 1 + .../preact/dist/preact.min.umd.js | 2 + .../preact/dist/preact.min.umd.js.map | 1 + .../lib/node_modules/preact/dist/preact.mjs | 2 + .../node_modules/preact/dist/preact.module.js | 2 + .../preact/dist/preact.module.js.map | 1 + .../node_modules/preact/dist/preact.umd.js | 2 + .../preact/dist/preact.umd.js.map | 1 + .../js/lib/node_modules/preact/hooks/LICENSE | 21 + .../node_modules/preact/hooks/dist/hooks.js | 2 + .../preact/hooks/dist/hooks.js.map | 1 + .../node_modules/preact/hooks/dist/hooks.mjs | 2 + .../preact/hooks/dist/hooks.module.js | 2 + .../preact/hooks/dist/hooks.module.js.map | 1 + .../preact/hooks/dist/hooks.umd.js | 2 + .../preact/hooks/dist/hooks.umd.js.map | 1 + .../node_modules/preact/hooks/package.json | 35 + .../node_modules/preact/hooks/src/index.d.ts | 145 + .../node_modules/preact/hooks/src/index.js | 549 ++++ .../preact/hooks/src/internal.d.ts | 101 + .../node_modules/preact/jsx-runtime/LICENSE | 21 + .../preact/jsx-runtime/dist/jsxRuntime.js | 2 + .../preact/jsx-runtime/dist/jsxRuntime.js.map | 1 + .../preact/jsx-runtime/dist/jsxRuntime.mjs | 2 + .../jsx-runtime/dist/jsxRuntime.module.js | 2 + .../jsx-runtime/dist/jsxRuntime.module.js.map | 1 + .../preact/jsx-runtime/dist/jsxRuntime.umd.js | 2 + .../jsx-runtime/dist/jsxRuntime.umd.js.map | 1 + .../preact/jsx-runtime/package.json | 28 + .../preact/jsx-runtime/src/index.d.ts | 62 + .../preact/jsx-runtime/src/index.js | 185 ++ .../preact/jsx-runtime/src/utils.js | 36 + .../js/lib/node_modules/preact/package.json | 263 ++ static/js/lib/node_modules/preact/src/cjs.js | 3 + .../node_modules/preact/src/clone-element.js | 48 + .../lib/node_modules/preact/src/component.js | 241 ++ .../lib/node_modules/preact/src/constants.js | 21 + .../node_modules/preact/src/create-context.js | 65 + .../node_modules/preact/src/create-element.js | 97 + .../preact/src/diff/catch-error.js | 44 + .../node_modules/preact/src/diff/children.js | 448 +++ .../lib/node_modules/preact/src/diff/index.js | 660 +++++ .../lib/node_modules/preact/src/diff/props.js | 176 ++ .../lib/node_modules/preact/src/index-5.d.ts | 398 +++ .../js/lib/node_modules/preact/src/index.d.ts | 398 +++ .../js/lib/node_modules/preact/src/index.js | 13 + .../lib/node_modules/preact/src/internal.d.ts | 190 ++ .../js/lib/node_modules/preact/src/jsx.d.ts | 2615 +++++++++++++++++ .../js/lib/node_modules/preact/src/options.js | 16 + .../js/lib/node_modules/preact/src/render.js | 78 + static/js/lib/node_modules/preact/src/util.js | 28 + .../preact/test-utils/dist/testUtils.js | 2 + .../preact/test-utils/dist/testUtils.js.map | 1 + .../preact/test-utils/dist/testUtils.mjs | 2 + .../test-utils/dist/testUtils.module.js | 2 + .../test-utils/dist/testUtils.module.js.map | 1 + .../preact/test-utils/dist/testUtils.umd.js | 2 + .../test-utils/dist/testUtils.umd.js.map | 1 + .../preact/test-utils/package.json | 28 + .../preact/test-utils/src/index.d.ts | 3 + .../preact/test-utils/src/index.js | 129 + static/js/lib/package-lock.json | 21 +- static/js/lib/package.json | 3 +- static/js/marked_position.mjs | 298 ++ static/js/node.mjs | 62 +- views/layouts/main.gotmpl | 2 +- 172 files changed, 12198 insertions(+), 8338 deletions(-) create mode 100644 static/css/markdown.css create mode 100644 static/images/icon_markdown.svg create mode 100644 static/images/icon_markdown_hollow.svg create mode 100644 static/images/icon_save.svg create mode 100644 static/images/icon_save_disabled.svg create mode 100644 static/js/lib/node_modules/marked-token-position/LICENSE create mode 100644 static/js/lib/node_modules/marked-token-position/README.md create mode 100644 static/js/lib/node_modules/marked-token-position/lib/index.d.ts create mode 100644 static/js/lib/node_modules/marked-token-position/lib/index.esm.js create mode 100644 static/js/lib/node_modules/marked-token-position/lib/index.esm.js.map create mode 100644 static/js/lib/node_modules/marked-token-position/lib/index.umd.js create mode 100644 static/js/lib/node_modules/marked-token-position/lib/index.umd.js.map create mode 100644 static/js/lib/node_modules/marked-token-position/package.json create mode 100644 static/js/lib/node_modules/marked-token-position/src/index.ts rename static/js/lib/node_modules/marked/{LICENSE.md => LICENSE} (100%) delete mode 100644 static/js/lib/node_modules/marked/lib/marked.cjs delete mode 100644 static/js/lib/node_modules/marked/lib/marked.cjs.map delete mode 100644 static/js/lib/node_modules/marked/lib/marked.d.cts delete mode 100644 static/js/lib/node_modules/marked/marked.min.js create mode 100644 static/js/lib/node_modules/preact/LICENSE create mode 100644 static/js/lib/node_modules/preact/README.md create mode 100644 static/js/lib/node_modules/preact/compat/LICENSE create mode 100644 static/js/lib/node_modules/preact/compat/client.d.ts create mode 100644 static/js/lib/node_modules/preact/compat/client.js create mode 100644 static/js/lib/node_modules/preact/compat/client.mjs create mode 100644 static/js/lib/node_modules/preact/compat/dist/compat.js create mode 100644 static/js/lib/node_modules/preact/compat/dist/compat.js.map create mode 100644 static/js/lib/node_modules/preact/compat/dist/compat.mjs create mode 100644 static/js/lib/node_modules/preact/compat/dist/compat.module.js create mode 100644 static/js/lib/node_modules/preact/compat/dist/compat.module.js.map create mode 100644 static/js/lib/node_modules/preact/compat/dist/compat.umd.js create mode 100644 static/js/lib/node_modules/preact/compat/dist/compat.umd.js.map create mode 100644 static/js/lib/node_modules/preact/compat/jsx-dev-runtime.js create mode 100644 static/js/lib/node_modules/preact/compat/jsx-dev-runtime.mjs create mode 100644 static/js/lib/node_modules/preact/compat/jsx-runtime.js create mode 100644 static/js/lib/node_modules/preact/compat/jsx-runtime.mjs create mode 100644 static/js/lib/node_modules/preact/compat/package.json create mode 100644 static/js/lib/node_modules/preact/compat/scheduler.js create mode 100644 static/js/lib/node_modules/preact/compat/scheduler.mjs create mode 100644 static/js/lib/node_modules/preact/compat/server.browser.js create mode 100644 static/js/lib/node_modules/preact/compat/server.js create mode 100644 static/js/lib/node_modules/preact/compat/server.mjs create mode 100644 static/js/lib/node_modules/preact/compat/src/Children.js create mode 100644 static/js/lib/node_modules/preact/compat/src/PureComponent.js create mode 100644 static/js/lib/node_modules/preact/compat/src/forwardRef.js create mode 100644 static/js/lib/node_modules/preact/compat/src/hooks.js create mode 100644 static/js/lib/node_modules/preact/compat/src/index.d.ts create mode 100644 static/js/lib/node_modules/preact/compat/src/index.js create mode 100644 static/js/lib/node_modules/preact/compat/src/internal.d.ts create mode 100644 static/js/lib/node_modules/preact/compat/src/memo.js create mode 100644 static/js/lib/node_modules/preact/compat/src/portals.js create mode 100644 static/js/lib/node_modules/preact/compat/src/render.js create mode 100644 static/js/lib/node_modules/preact/compat/src/suspense-list.d.ts create mode 100644 static/js/lib/node_modules/preact/compat/src/suspense-list.js create mode 100644 static/js/lib/node_modules/preact/compat/src/suspense.d.ts create mode 100644 static/js/lib/node_modules/preact/compat/src/suspense.js create mode 100644 static/js/lib/node_modules/preact/compat/src/util.js create mode 100644 static/js/lib/node_modules/preact/compat/test-utils.js create mode 100644 static/js/lib/node_modules/preact/debug/LICENSE create mode 100644 static/js/lib/node_modules/preact/debug/dist/debug.js create mode 100644 static/js/lib/node_modules/preact/debug/dist/debug.js.map create mode 100644 static/js/lib/node_modules/preact/debug/dist/debug.mjs create mode 100644 static/js/lib/node_modules/preact/debug/dist/debug.module.js create mode 100644 static/js/lib/node_modules/preact/debug/dist/debug.module.js.map create mode 100644 static/js/lib/node_modules/preact/debug/dist/debug.umd.js create mode 100644 static/js/lib/node_modules/preact/debug/dist/debug.umd.js.map create mode 100644 static/js/lib/node_modules/preact/debug/package.json create mode 100644 static/js/lib/node_modules/preact/debug/src/check-props.js create mode 100644 static/js/lib/node_modules/preact/debug/src/component-stack.js create mode 100644 static/js/lib/node_modules/preact/debug/src/constants.js create mode 100644 static/js/lib/node_modules/preact/debug/src/debug.js create mode 100644 static/js/lib/node_modules/preact/debug/src/index.d.ts create mode 100644 static/js/lib/node_modules/preact/debug/src/index.js create mode 100644 static/js/lib/node_modules/preact/debug/src/internal.d.ts create mode 100644 static/js/lib/node_modules/preact/debug/src/util.js create mode 100644 static/js/lib/node_modules/preact/devtools/LICENSE create mode 100644 static/js/lib/node_modules/preact/devtools/dist/devtools.js create mode 100644 static/js/lib/node_modules/preact/devtools/dist/devtools.js.map create mode 100644 static/js/lib/node_modules/preact/devtools/dist/devtools.mjs create mode 100644 static/js/lib/node_modules/preact/devtools/dist/devtools.module.js create mode 100644 static/js/lib/node_modules/preact/devtools/dist/devtools.module.js.map create mode 100644 static/js/lib/node_modules/preact/devtools/dist/devtools.umd.js create mode 100644 static/js/lib/node_modules/preact/devtools/dist/devtools.umd.js.map create mode 100644 static/js/lib/node_modules/preact/devtools/package.json create mode 100644 static/js/lib/node_modules/preact/devtools/src/devtools.js create mode 100644 static/js/lib/node_modules/preact/devtools/src/index.d.ts create mode 100644 static/js/lib/node_modules/preact/devtools/src/index.js create mode 100644 static/js/lib/node_modules/preact/dist/preact.js create mode 100644 static/js/lib/node_modules/preact/dist/preact.js.map create mode 100644 static/js/lib/node_modules/preact/dist/preact.min.js create mode 100644 static/js/lib/node_modules/preact/dist/preact.min.js.map create mode 100644 static/js/lib/node_modules/preact/dist/preact.min.module.js create mode 100644 static/js/lib/node_modules/preact/dist/preact.min.module.js.map create mode 100644 static/js/lib/node_modules/preact/dist/preact.min.umd.js create mode 100644 static/js/lib/node_modules/preact/dist/preact.min.umd.js.map create mode 100644 static/js/lib/node_modules/preact/dist/preact.mjs create mode 100644 static/js/lib/node_modules/preact/dist/preact.module.js create mode 100644 static/js/lib/node_modules/preact/dist/preact.module.js.map create mode 100644 static/js/lib/node_modules/preact/dist/preact.umd.js create mode 100644 static/js/lib/node_modules/preact/dist/preact.umd.js.map create mode 100644 static/js/lib/node_modules/preact/hooks/LICENSE create mode 100644 static/js/lib/node_modules/preact/hooks/dist/hooks.js create mode 100644 static/js/lib/node_modules/preact/hooks/dist/hooks.js.map create mode 100644 static/js/lib/node_modules/preact/hooks/dist/hooks.mjs create mode 100644 static/js/lib/node_modules/preact/hooks/dist/hooks.module.js create mode 100644 static/js/lib/node_modules/preact/hooks/dist/hooks.module.js.map create mode 100644 static/js/lib/node_modules/preact/hooks/dist/hooks.umd.js create mode 100644 static/js/lib/node_modules/preact/hooks/dist/hooks.umd.js.map create mode 100644 static/js/lib/node_modules/preact/hooks/package.json create mode 100644 static/js/lib/node_modules/preact/hooks/src/index.d.ts create mode 100644 static/js/lib/node_modules/preact/hooks/src/index.js create mode 100644 static/js/lib/node_modules/preact/hooks/src/internal.d.ts create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/LICENSE create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/dist/jsxRuntime.js create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/dist/jsxRuntime.js.map create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/dist/jsxRuntime.mjs create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js.map create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/dist/jsxRuntime.umd.js create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/dist/jsxRuntime.umd.js.map create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/package.json create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/src/index.d.ts create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/src/index.js create mode 100644 static/js/lib/node_modules/preact/jsx-runtime/src/utils.js create mode 100644 static/js/lib/node_modules/preact/package.json create mode 100644 static/js/lib/node_modules/preact/src/cjs.js create mode 100644 static/js/lib/node_modules/preact/src/clone-element.js create mode 100644 static/js/lib/node_modules/preact/src/component.js create mode 100644 static/js/lib/node_modules/preact/src/constants.js create mode 100644 static/js/lib/node_modules/preact/src/create-context.js create mode 100644 static/js/lib/node_modules/preact/src/create-element.js create mode 100644 static/js/lib/node_modules/preact/src/diff/catch-error.js create mode 100644 static/js/lib/node_modules/preact/src/diff/children.js create mode 100644 static/js/lib/node_modules/preact/src/diff/index.js create mode 100644 static/js/lib/node_modules/preact/src/diff/props.js create mode 100644 static/js/lib/node_modules/preact/src/index-5.d.ts create mode 100644 static/js/lib/node_modules/preact/src/index.d.ts create mode 100644 static/js/lib/node_modules/preact/src/index.js create mode 100644 static/js/lib/node_modules/preact/src/internal.d.ts create mode 100644 static/js/lib/node_modules/preact/src/jsx.d.ts create mode 100644 static/js/lib/node_modules/preact/src/options.js create mode 100644 static/js/lib/node_modules/preact/src/render.js create mode 100644 static/js/lib/node_modules/preact/src/util.js create mode 100644 static/js/lib/node_modules/preact/test-utils/dist/testUtils.js create mode 100644 static/js/lib/node_modules/preact/test-utils/dist/testUtils.js.map create mode 100644 static/js/lib/node_modules/preact/test-utils/dist/testUtils.mjs create mode 100644 static/js/lib/node_modules/preact/test-utils/dist/testUtils.module.js create mode 100644 static/js/lib/node_modules/preact/test-utils/dist/testUtils.module.js.map create mode 100644 static/js/lib/node_modules/preact/test-utils/dist/testUtils.umd.js create mode 100644 static/js/lib/node_modules/preact/test-utils/dist/testUtils.umd.js.map create mode 100644 static/js/lib/node_modules/preact/test-utils/package.json create mode 100644 static/js/lib/node_modules/preact/test-utils/src/index.d.ts create mode 100644 static/js/lib/node_modules/preact/test-utils/src/index.js create mode 100644 static/js/marked_position.mjs diff --git a/static/css/markdown.css b/static/css/markdown.css new file mode 100644 index 0000000..19df20a --- /dev/null +++ b/static/css/markdown.css @@ -0,0 +1,30 @@ +.el-node-markdown { + h1 { + border-bottom: 1px solid #ccc; + margin-top: 48px; + margin-bottom: 8px; + + display: inline-block; + font-size: 1.25em; + border-radius: 8px; + color: #fff; + background-color: var(--color1); + padding: 4px 12px; + + &:first-child { + margin-top: 32px; + } + } + + h2 { + font-size: 1.25em; + margin-bottom: 0px; + color: var(--color1); + } + + h3:before { + font-size: 1.0em; + content: "> "; + color: var(--color1); + } +} diff --git a/static/css/notes2.css b/static/css/notes2.css index e6d0ef6..4945518 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -13,17 +13,13 @@ html { display: grid; grid-template-areas: - "tree crumbs" - "tree name" - "tree sync" - "tree content" - /* - "tree checklist" - "tree files" - */ - "tree blank" + "tree hum crumbs crumbs ding" + "tree hum name name ding" + "tree hum sync functions ding" + "tree hum content content ding" + "tree hum blank blank ding" ; - grid-template-columns: min-content 1fr; + grid-template-columns: min-content minmax(16px, 1fr) minmax(min-content, 820px) 80px minmax(16px, 1fr); grid-template-rows: min-content min-content 48px 1fr; @@ -34,10 +30,6 @@ html { "sync" "name" "content" - /* - "checklist" - "files" - */ "blank" ; grid-template-columns: 1fr; @@ -61,11 +53,10 @@ html { padding: 16px 0px 16px 16px; color: #ddd; z-index: 100; - /* Over crumbs shadow */ border-left: 2px solid #333; &:focus { - border-left: 2px solid #FE5F55; + border-left: 2px solid #fe5f55; } #logo { @@ -151,7 +142,7 @@ html { align-items: start; justify-items: center; height: min-content; - margin: 16px 16px; + margin: 0 16px 16px 16px; n2-crumbs { background: #e4e4e4; @@ -223,8 +214,7 @@ n2-syncprogress { } progress { - width: calc(100% - 32px); - max-width: var(--content-width); + width: 100%; height: 24px; border-radius: 8px; } @@ -272,6 +262,7 @@ n2-nodeui { margin-bottom: 32px; .el-name { + grid-area: name; color: #333; font-weight: bold; text-align: center; @@ -280,27 +271,27 @@ n2-nodeui { margin-bottom: 0px; } + .el-functions { + grid-area: functions; + } + .el-node-content { + grid-area: content; justify-self: center; word-wrap: break-word; font-family: monospace; color: #333; - /* width: 100%; max-width: var(--content-width); field-sizing: content; - */ - - width: calc(100% - 32px); - max-width: var(--content-width); - field-sizing: content; resize: none; - border: none; outline: none; - padding: 16px 0; + padding: 32px 0; + border-left: none; + border-right: none; border-top: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0; margin-bottom: 32px; @@ -310,6 +301,24 @@ n2-nodeui { padding-top: 16px; } } + + .el-node-markdown { + grid-area: content; + display: none; + + border-top: 1px solid #e0e0e0; + border-bottom: 1px solid #e0e0e0; + margin-bottom: 32px; + } + + &.show-markdown { + .el-node-content { + display: none; + } + .el-node-markdown { + display: block; + } + } } #blank { diff --git a/static/images/icon_markdown.svg b/static/images/icon_markdown.svg new file mode 100644 index 0000000..f8d0aae --- /dev/null +++ b/static/images/icon_markdown.svg @@ -0,0 +1,57 @@ + + + + + + + Markdown icon + + + + + Markdown icon + + + + diff --git a/static/images/icon_markdown_hollow.svg b/static/images/icon_markdown_hollow.svg new file mode 100644 index 0000000..d938c6f --- /dev/null +++ b/static/images/icon_markdown_hollow.svg @@ -0,0 +1,50 @@ + + + + + + + diff --git a/static/images/icon_save.svg b/static/images/icon_save.svg new file mode 100644 index 0000000..0846a73 --- /dev/null +++ b/static/images/icon_save.svg @@ -0,0 +1,49 @@ + + + + + + + + content-save + + + diff --git a/static/images/icon_save_disabled.svg b/static/images/icon_save_disabled.svg new file mode 100644 index 0000000..907cee6 --- /dev/null +++ b/static/images/icon_save_disabled.svg @@ -0,0 +1,49 @@ + + + + + + + + content-save + + + diff --git a/static/js/app.mjs b/static/js/app.mjs index 45cd85b..03a3916 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -72,12 +72,12 @@ export class App { case 'E': this.showPage('keys') break + */ case 'M': - this.toggleMarkdown() + globalThis._mbus.dispatch('MARKDOWN_TOGGLE') break - */ case 'N': this.createNode() break diff --git a/static/js/lib/node_modules/.package-lock.json b/static/js/lib/node_modules/.package-lock.json index 441fdf4..3d3d164 100644 --- a/static/js/lib/node_modules/.package-lock.json +++ b/static/js/lib/node_modules/.package-lock.json @@ -4,14 +4,24 @@ "requires": true, "packages": { "node_modules/marked": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/marked/-/marked-11.1.1.tgz", - "integrity": "sha512-EgxRjgK9axsQuUa/oKMx5DEY8oXpKJfk61rT5iY3aRlgU6QJtUcxU5OAymdhCvWvhYcd9FKmO5eQoX8m9VGJXg==", + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz", + "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==", + "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/marked-token-position": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/marked-token-position/-/marked-token-position-2.0.2.tgz", + "integrity": "sha512-IMyr4mR3A5uFReXn7cxLDgDLjefG110ANy0oMGs5+gB7NsdIbv9YoVoJuGxuMSFHWOeIFkAzjdSoFNVKcMPfZw==", + "license": "MIT", + "peerDependencies": { + "marked": ">=16.2.0 <19" } }, "node_modules/preact": { diff --git a/static/js/lib/node_modules/marked-token-position/LICENSE b/static/js/lib/node_modules/marked-token-position/LICENSE new file mode 100644 index 0000000..5d36390 --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 @UziTech + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/static/js/lib/node_modules/marked-token-position/README.md b/static/js/lib/node_modules/marked-token-position/README.md new file mode 100644 index 0000000..a10f43c --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/README.md @@ -0,0 +1,160 @@ +# marked-token-position + +Add `position` field for each token. + +```ts +interface Position { + /** + * Positions for each line of the token. LinePositions will not include the newline character for the line. + */ + lines: LinePosition[] + /** + * Position at the beginning of token + */ + start: PositionFields; + /** + * Position at the end of token + */ + end: PositionFields; +} + +interface LinePosition { + /** + * Position at the beginning of line + */ + start: PositionFields; + /** + * Position at the end of line. Will not include the newline character. + */ + end: PositionFields; +} + +interface PositionFields { + /** + * Number of characters from the beginning of the markdown string + */ + offset: number; + /** + * Line number of the token. Starts at line 0. + */ + line: number; + /** + * Column number of the token. Starts at column 0. + */ + column: number; +} +``` + +# Usage + +## Extension + +```js +import {Marked} from "marked"; +import markedTokenPosition from "marked-token-position"; + +// or UMD script +// +// +// const Marked = marked.Marked; + +const marked = new Marked(); + +function anotherExtension { + return { + walkTokens(token) { + // token has `position` field + } + hooks: { + processAllTokens(tokens) { + // tokens have `position` field + } + } + }; +} + +marked.use(anotherExtension(), markedTokenPosition()); + +marked.parse("# example markdown"); +``` + +The `position` field will be added to the tokens so any other extension can +use the `position` field in a `walkTokens` function or `processAllTokens` hook. + +> [!CAUTION] +> The `processAllTokens` hook is used by this extension so any other extension +> using `processAllTokens` that requires the `position` field must be added +> before this extension because marked calls the `processAllTokens` hooks in +> reverse order. + +The tokens will look like: + +```json +[ + { + "type": "heading", + "raw": "# example markdown", + "depth": 1, + "text": "example markdown", + "tokens": [ + { + "type": "text", + "raw": "example markdown", + "text": "example markdown", + "escaped": false, + "position": { + "start": { + "offset": 2, + "line": 0, + "column": 2 + }, + "end": { + "offset": 18, + "line": 0, + "column": 18 + } + } + } + ], + "position": { + "start": { + "offset": 0, + "line": 0, + "column": 0 + }, + "end": { + "offset": 18, + "line": 0, + "column": 18 + } + } + } +] +``` + +## addTokenPositions + +Calling `marked.lexer()` will not add the `position` field with the extension +since the extension is only called on `marked.parse()` and `marked.parseInline()`. + +An `addTokenPositions` function is exported to add the `position` field to the +tokens returned by `marked.lexer()`. + +```js +import {Marked} from "marked"; +import {addTokenPositions} from "marked-token-position"; + +// or UMD script +// +// +// const Marked = marked.Marked; +// const addTokenPositions = markedTokenPosition.addTokenPositions; + + +const marked = new Marked(); +const tokens = marked.lexer("# example markdown"); + +addTokenPositions(tokens); + +// tokens now have a `position` field +``` diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.d.ts b/static/js/lib/node_modules/marked-token-position/lib/index.d.ts new file mode 100644 index 0000000..f61cd65 --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/lib/index.d.ts @@ -0,0 +1,59 @@ +// Generated by dts-bundle-generator v9.5.1 + +import { MarkedExtension, Token, Tokens } from 'marked'; + +export interface TokenWithPosition extends Tokens.Generic { + position: Position; +} +export interface Position { + /** + * Positions for each line of the token. LinePositions will not include the newline character for the line. + */ + lines: LinePosition[]; + /** + * Position at the beginning of token + */ + start: PositionFields; + /** + * Position at the end of token + */ + end: PositionFields; +} +export interface LinePosition { + /** + * Position at the beginning of line + */ + start: PositionFields; + /** + * Position at the end of line. Will not include the newline character. + */ + end: PositionFields; +} +export interface PositionFields { + /** + * Number of characters from the beginning of the markdown string + */ + offset: number; + /** + * Line number of the token. Starts at line 0. + */ + line: number; + /** + * Column number of the token. Starts at column 0. + */ + column: number; +} +/** + * Add position field to tokens + */ +export declare function addTokenPositions(tokens: Token[]): TokenWithPosition[]; +/** + * Marked extension to add position field to tokens + */ +declare function _default(options?: {}): MarkedExtension; + +export { + _default as default, +}; + +export {}; diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.esm.js b/static/js/lib/node_modules/marked-token-position/lib/index.esm.js new file mode 100644 index 0000000..41a85f7 --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/lib/index.esm.js @@ -0,0 +1,6 @@ +function g(u){let i=u.map(r=>r.raw).join("");return h(u,0,0,0,i).tokens}function b(u={}){return{hooks:{processAllTokens(i){return g(i)}}}}function h(u,i,r,f,l){for(let s of u){let n=s,a=T(i,r,f,l,n.raw);if(n.position=a,n.tokens&&h(n.tokens,i,r,f,l),n.childTokens){let c=i,t=r,e=f,d=l;for(let k of n.childTokens){let o=h(n[k],c,t,e,d);c=o.offset,t=o.line,e=o.column,d=o.markdown}}if(n.type==="list"&&h(n.items,i,r,f,l),n.type==="table"){let c=i,t=r,e=f,d=l;for(let k of n.header){let o=h(k.tokens,c,t,e,d);c=o.offset,t=o.line,e=o.column,d=o.markdown}for(let k of n.rows)for(let o of k){let P=h(o.tokens,c,t,e,d);c=P.offset,t=P.line,e=P.column,d=P.markdown}}let m=a.end.offset-i;i=a.end.offset,r=a.end.line,f=a.end.column,l=l.slice(m)}return{tokens:u,offset:i,line:r,column:f,markdown:l}}function T(u,i,r,f,l){let s=[],n=l.split(` +`),a=f.split(` +`);n:for(let t=0;t<=a.length-n.length;t++){s=[];for(let e=0;e0?` +`:""),x={offset:u+P.length+o,line:i+t+e,column:(t+e===0?r:0)+o},p={offset:x.offset+k.length,line:x.line,column:x.column+k.length};s.push({start:x,end:p})}break}if(s.length===0)throw new Error(`Cannot find ${JSON.stringify(l)} in ${JSON.stringify(f)}`);let m=s[0].start,c=s.at(-1).end;return s.length>1&&s.at(-1).start.offset===c.offset&&(s=s.slice(0,-1)),{lines:s,start:m,end:c}}export{g as addTokenPositions,b as default}; +//# sourceMappingURL=index.esm.js.map diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.esm.js.map b/static/js/lib/node_modules/marked-token-position/lib/index.esm.js.map new file mode 100644 index 0000000..4eb0539 --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/lib/index.esm.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/index.ts"], + "sourcesContent": ["/* node:coverage ignore next */\nimport type { MarkedExtension, Token, Tokens } from 'marked';\n\nexport interface TokenWithPosition extends Tokens.Generic {\n position: Position;\n}\ninterface Position {\n /**\n * Positions for each line of the token. LinePositions will not include the newline character for the line.\n */\n lines: LinePosition[]\n /**\n * Position at the beginning of token\n */\n start: PositionFields;\n /**\n * Position at the end of token\n */\n end: PositionFields;\n}\n\ninterface LinePosition {\n /**\n * Position at the beginning of line\n */\n start: PositionFields;\n /**\n * Position at the end of line. Will not include the newline character.\n */\n end: PositionFields;\n}\n\ninterface PositionFields {\n /**\n * Number of characters from the beginning of the markdown string\n */\n offset: number;\n /**\n * Line number of the token. Starts at line 0.\n */\n line: number;\n /**\n * Column number of the token. Starts at column 0.\n */\n column: number;\n}\n\n/**\n * Add position field to tokens\n */\nexport function addTokenPositions(tokens: Token[]) {\n const markdown = tokens.map(token => token.raw).join('');\n return addPosition(tokens, 0, 0, 0, markdown).tokens;\n}\n\n/**\n * Marked extension to add position field to tokens\n */\nexport default function(options = {}): MarkedExtension {\n return {\n hooks: {\n processAllTokens(tokens) {\n return addTokenPositions(tokens);\n },\n },\n };\n}\n\nfunction addPosition(tokens: Token[], offset: number, line: number, column: number, markdown: string) {\n for (const token of tokens) {\n const genericToken = token as Tokens.Generic;\n const position = getPosition(offset, line, column, markdown, genericToken.raw);\n genericToken.position = position;\n\n if (genericToken.tokens) {\n addPosition(genericToken.tokens, offset, line, column, markdown);\n }\n\n if (genericToken.childTokens) {\n let nextOffset = offset;\n let nextLine = line;\n let nextColumn = column;\n let nextMarkdown = markdown;\n for (const childToken of genericToken.childTokens) {\n const nextPosition = addPosition(genericToken[childToken], nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n }\n\n if (genericToken.type === 'list') {\n addPosition(genericToken.items, offset, line, column, markdown);\n }\n\n if (genericToken.type === 'table') {\n let nextOffset = offset;\n let nextLine = line;\n let nextColumn = column;\n let nextMarkdown = markdown;\n for (const headerCell of genericToken.header) {\n const nextPosition = addPosition(headerCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n for (const row of genericToken.rows) {\n for (const rowCell of row) {\n const nextPosition = addPosition(rowCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n }\n }\n\n const deltaOffset = position.end.offset - offset;\n offset = position.end.offset;\n line = position.end.line;\n column = position.end.column;\n markdown = markdown.slice(deltaOffset);\n }\n\n return {\n tokens: tokens as TokenWithPosition[],\n offset,\n line,\n column,\n markdown,\n };\n}\n\nfunction getPosition(offset: number, line: number, column: number, markdown: string, raw: string): Position {\n let lines: LinePosition[] = [];\n const rawLines = raw.split('\\n');\n const markdownLines = markdown.split('\\n');\n\n // eslint-disable-next-line no-labels\n md: for (let i = 0; i <= markdownLines.length - rawLines.length; i++) {\n lines = [];\n for (let j = 0; j < rawLines.length; j++) {\n const markdownLine = markdownLines[i + j];\n const rawLine = rawLines[j];\n const lineStartOffset = markdownLine.indexOf(rawLine);\n\n if (lineStartOffset === -1) {\n // eslint-disable-next-line no-labels\n continue md;\n }\n\n const beforeMarkdownLines = markdownLines.slice(0, i + j).join('\\n') + (i + j > 0 ? '\\n' : '');\n const start = {\n offset: offset + beforeMarkdownLines.length + lineStartOffset,\n line: line + i + j,\n column: (i + j === 0 ? column : 0) + lineStartOffset,\n };\n const end = {\n offset: start.offset + rawLine.length,\n line: start.line,\n column: start.column + rawLine.length,\n };\n\n lines.push({\n start,\n end,\n });\n }\n break;\n }\n\n /* node:coverage ignore next 4 */\n if (lines.length === 0) {\n // This shouldn't ever happen but if it does it would be nice to have a good error message\n throw new Error(`Cannot find ${JSON.stringify(raw)} in ${JSON.stringify(markdown)}`);\n }\n\n const start = lines[0].start;\n const end = lines.at(-1)!.end;\n\n if (lines.length > 1 && lines.at(-1)!.start.offset === end.offset) {\n lines = lines.slice(0, -1);\n }\n\n return {\n lines,\n start,\n end,\n };\n}\n"], + "mappings": "AAkDO,SAASA,EAAkBC,EAAiB,CACjD,IAAMC,EAAWD,EAAO,IAAIE,GAASA,EAAM,GAAG,EAAE,KAAK,EAAE,EACvD,OAAOC,EAAYH,EAAQ,EAAG,EAAG,EAAGC,CAAQ,EAAE,MAChD,CAKe,SAARG,EAAiBC,EAAU,CAAC,EAAoB,CACrD,MAAO,CACL,MAAO,CACL,iBAAiBL,EAAQ,CACvB,OAAOD,EAAkBC,CAAM,CACjC,CACF,CACF,CACF,CAEA,SAASG,EAAYH,EAAiBM,EAAgBC,EAAcC,EAAgBP,EAAkB,CACpG,QAAWC,KAASF,EAAQ,CAC1B,IAAMS,EAAeP,EACfQ,EAAWC,EAAYL,EAAQC,EAAMC,EAAQP,EAAUQ,EAAa,GAAG,EAO7E,GANAA,EAAa,SAAWC,EAEpBD,EAAa,QACfN,EAAYM,EAAa,OAAQH,EAAQC,EAAMC,EAAQP,CAAQ,EAG7DQ,EAAa,YAAa,CAC5B,IAAIG,EAAaN,EACbO,EAAWN,EACXO,EAAaN,EACbO,EAAed,EACnB,QAAWe,KAAcP,EAAa,YAAa,CACjD,IAAMQ,EAAed,EAAYM,EAAaO,CAAU,EAAGJ,EAAYC,EAAUC,EAAYC,CAAY,EACzGH,EAAaK,EAAa,OAC1BJ,EAAWI,EAAa,KACxBH,EAAaG,EAAa,OAC1BF,EAAeE,EAAa,QAC9B,CACF,CAMA,GAJIR,EAAa,OAAS,QACxBN,EAAYM,EAAa,MAAOH,EAAQC,EAAMC,EAAQP,CAAQ,EAG5DQ,EAAa,OAAS,QAAS,CACjC,IAAIG,EAAaN,EACbO,EAAWN,EACXO,EAAaN,EACbO,EAAed,EACnB,QAAWiB,KAAcT,EAAa,OAAQ,CAC5C,IAAMQ,EAAed,EAAYe,EAAW,OAAQN,EAAYC,EAAUC,EAAYC,CAAY,EAClGH,EAAaK,EAAa,OAC1BJ,EAAWI,EAAa,KACxBH,EAAaG,EAAa,OAC1BF,EAAeE,EAAa,QAC9B,CACA,QAAWE,KAAOV,EAAa,KAC7B,QAAWW,KAAWD,EAAK,CACzB,IAAMF,EAAed,EAAYiB,EAAQ,OAAQR,EAAYC,EAAUC,EAAYC,CAAY,EAC/FH,EAAaK,EAAa,OAC1BJ,EAAWI,EAAa,KACxBH,EAAaG,EAAa,OAC1BF,EAAeE,EAAa,QAC9B,CAEJ,CAEA,IAAMI,EAAcX,EAAS,IAAI,OAASJ,EAC1CA,EAASI,EAAS,IAAI,OACtBH,EAAOG,EAAS,IAAI,KACpBF,EAASE,EAAS,IAAI,OACtBT,EAAWA,EAAS,MAAMoB,CAAW,CACvC,CAEA,MAAO,CACL,OAAQrB,EACR,OAAAM,EACA,KAAAC,EACA,OAAAC,EACA,SAAAP,CACF,CACF,CAEA,SAASU,EAAYL,EAAgBC,EAAcC,EAAgBP,EAAkBqB,EAAuB,CAC1G,IAAIC,EAAwB,CAAC,EACvBC,EAAWF,EAAI,MAAM;AAAA,CAAI,EACzBG,EAAgBxB,EAAS,MAAM;AAAA,CAAI,EAGzCyB,EAAI,QAASC,EAAI,EAAGA,GAAKF,EAAc,OAASD,EAAS,OAAQG,IAAK,CACpEJ,EAAQ,CAAC,EACT,QAASK,EAAI,EAAGA,EAAIJ,EAAS,OAAQI,IAAK,CACxC,IAAMC,EAAeJ,EAAcE,EAAIC,CAAC,EAClCE,EAAUN,EAASI,CAAC,EACpBG,EAAkBF,EAAa,QAAQC,CAAO,EAEpD,GAAIC,IAAoB,GAEtB,SAASL,EAGX,IAAMM,EAAsBP,EAAc,MAAM,EAAGE,EAAIC,CAAC,EAAE,KAAK;AAAA,CAAI,GAAKD,EAAIC,EAAI,EAAI;AAAA,EAAO,IACrFK,EAAQ,CACZ,OAAQ3B,EAAS0B,EAAoB,OAASD,EAC9C,KAAMxB,EAAOoB,EAAIC,EACjB,QAASD,EAAIC,IAAM,EAAIpB,EAAS,GAAKuB,CACvC,EACMG,EAAM,CACV,OAAQD,EAAM,OAASH,EAAQ,OAC/B,KAAMG,EAAM,KACZ,OAAQA,EAAM,OAASH,EAAQ,MACjC,EAEAP,EAAM,KAAK,CACT,MAAAU,EACA,IAAAC,CACF,CAAC,CACH,CACA,KACF,CAGA,GAAIX,EAAM,SAAW,EAEnB,MAAM,IAAI,MAAM,eAAe,KAAK,UAAUD,CAAG,CAAC,OAAO,KAAK,UAAUrB,CAAQ,CAAC,EAAE,EAGrF,IAAMgC,EAAQV,EAAM,CAAC,EAAE,MACjBW,EAAMX,EAAM,GAAG,EAAE,EAAG,IAE1B,OAAIA,EAAM,OAAS,GAAKA,EAAM,GAAG,EAAE,EAAG,MAAM,SAAWW,EAAI,SACzDX,EAAQA,EAAM,MAAM,EAAG,EAAE,GAGpB,CACL,MAAAA,EACA,MAAAU,EACA,IAAAC,CACF,CACF", + "names": ["addTokenPositions", "tokens", "markdown", "token", "addPosition", "index_default", "options", "offset", "line", "column", "genericToken", "position", "getPosition", "nextOffset", "nextLine", "nextColumn", "nextMarkdown", "childToken", "nextPosition", "headerCell", "row", "rowCell", "deltaOffset", "raw", "lines", "rawLines", "markdownLines", "md", "i", "j", "markdownLine", "rawLine", "lineStartOffset", "beforeMarkdownLines", "start", "end"] +} diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.umd.js b/static/js/lib/node_modules/marked-token-position/lib/index.umd.js new file mode 100644 index 0000000..253cb7d --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/lib/index.umd.js @@ -0,0 +1,9 @@ +(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("markedTokenPosition",f)}else {g["markedTokenPosition"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; +var m=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var F=(e,n)=>()=>(e&&(n=e(e=0)),n);var M=(e,n)=>{for(var o in n)m(e,o,{get:n[o],enumerable:!0})},j=(e,n,o,f)=>{if(n&&typeof n=="object"||typeof n=="function")for(let t of y(n))!C.call(e,t)&&t!==o&&m(e,t,{get:()=>n[t],enumerable:!(f=O(n,t))||f.enumerable});return e};var b=e=>j(m({},"__esModule",{value:!0}),e);var T={};M(T,{addTokenPositions:()=>L,default:()=>E});function L(e){let n=e.map(o=>o.raw).join("");return x(e,0,0,0,n).tokens}function E(e={}){return{hooks:{processAllTokens(n){return L(n)}}}}function x(e,n,o,f,t){for(let c of e){let i=c,a=S(n,o,f,t,i.raw);if(i.position=a,i.tokens&&x(i.tokens,n,o,f,t),i.childTokens){let d=n,r=o,s=f,u=t;for(let k of i.childTokens){let l=x(i[k],d,r,s,u);d=l.offset,r=l.line,s=l.column,u=l.markdown}}if(i.type==="list"&&x(i.items,n,o,f,t),i.type==="table"){let d=n,r=o,s=f,u=t;for(let k of i.header){let l=x(k.tokens,d,r,s,u);d=l.offset,r=l.line,s=l.column,u=l.markdown}for(let k of i.rows)for(let l of k){let P=x(l.tokens,d,r,s,u);d=P.offset,r=P.line,s=P.column,u=P.markdown}}let p=a.end.offset-n;n=a.end.offset,o=a.end.line,f=a.end.column,t=t.slice(p)}return{tokens:e,offset:n,line:o,column:f,markdown:t}}function S(e,n,o,f,t){let c=[],i=t.split(` +`),a=f.split(` +`);n:for(let r=0;r<=a.length-i.length;r++){c=[];for(let s=0;s0?` +`:""),h={offset:e+P.length+l,line:n+r+s,column:(r+s===0?o:0)+l},w={offset:h.offset+k.length,line:h.line,column:h.column+k.length};c.push({start:h,end:w})}break}if(c.length===0)throw new Error(`Cannot find ${JSON.stringify(t)} in ${JSON.stringify(f)}`);let p=c[0].start,d=c.at(-1).end;return c.length>1&&c.at(-1).start.offset===d.offset&&(c=c.slice(0,-1)),{lines:c,start:p,end:d}}var g=F(()=>{"use strict"});module.exports=(g(),b(T)).default;module.exports.addTokenPositions=(g(),b(T)).addTokenPositions; + +if(__exports != exports)module.exports = exports;return module.exports})); +//# sourceMappingURL=index.umd.js.map diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.umd.js.map b/static/js/lib/node_modules/marked-token-position/lib/index.umd.js.map new file mode 100644 index 0000000..7894776 --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/lib/index.umd.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/index.ts", ""], + "sourcesContent": ["/* node:coverage ignore next */\nimport type { MarkedExtension, Token, Tokens } from 'marked';\n\nexport interface TokenWithPosition extends Tokens.Generic {\n position: Position;\n}\ninterface Position {\n /**\n * Positions for each line of the token. LinePositions will not include the newline character for the line.\n */\n lines: LinePosition[]\n /**\n * Position at the beginning of token\n */\n start: PositionFields;\n /**\n * Position at the end of token\n */\n end: PositionFields;\n}\n\ninterface LinePosition {\n /**\n * Position at the beginning of line\n */\n start: PositionFields;\n /**\n * Position at the end of line. Will not include the newline character.\n */\n end: PositionFields;\n}\n\ninterface PositionFields {\n /**\n * Number of characters from the beginning of the markdown string\n */\n offset: number;\n /**\n * Line number of the token. Starts at line 0.\n */\n line: number;\n /**\n * Column number of the token. Starts at column 0.\n */\n column: number;\n}\n\n/**\n * Add position field to tokens\n */\nexport function addTokenPositions(tokens: Token[]) {\n const markdown = tokens.map(token => token.raw).join('');\n return addPosition(tokens, 0, 0, 0, markdown).tokens;\n}\n\n/**\n * Marked extension to add position field to tokens\n */\nexport default function(options = {}): MarkedExtension {\n return {\n hooks: {\n processAllTokens(tokens) {\n return addTokenPositions(tokens);\n },\n },\n };\n}\n\nfunction addPosition(tokens: Token[], offset: number, line: number, column: number, markdown: string) {\n for (const token of tokens) {\n const genericToken = token as Tokens.Generic;\n const position = getPosition(offset, line, column, markdown, genericToken.raw);\n genericToken.position = position;\n\n if (genericToken.tokens) {\n addPosition(genericToken.tokens, offset, line, column, markdown);\n }\n\n if (genericToken.childTokens) {\n let nextOffset = offset;\n let nextLine = line;\n let nextColumn = column;\n let nextMarkdown = markdown;\n for (const childToken of genericToken.childTokens) {\n const nextPosition = addPosition(genericToken[childToken], nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n }\n\n if (genericToken.type === 'list') {\n addPosition(genericToken.items, offset, line, column, markdown);\n }\n\n if (genericToken.type === 'table') {\n let nextOffset = offset;\n let nextLine = line;\n let nextColumn = column;\n let nextMarkdown = markdown;\n for (const headerCell of genericToken.header) {\n const nextPosition = addPosition(headerCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n for (const row of genericToken.rows) {\n for (const rowCell of row) {\n const nextPosition = addPosition(rowCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n }\n }\n\n const deltaOffset = position.end.offset - offset;\n offset = position.end.offset;\n line = position.end.line;\n column = position.end.column;\n markdown = markdown.slice(deltaOffset);\n }\n\n return {\n tokens: tokens as TokenWithPosition[],\n offset,\n line,\n column,\n markdown,\n };\n}\n\nfunction getPosition(offset: number, line: number, column: number, markdown: string, raw: string): Position {\n let lines: LinePosition[] = [];\n const rawLines = raw.split('\\n');\n const markdownLines = markdown.split('\\n');\n\n // eslint-disable-next-line no-labels\n md: for (let i = 0; i <= markdownLines.length - rawLines.length; i++) {\n lines = [];\n for (let j = 0; j < rawLines.length; j++) {\n const markdownLine = markdownLines[i + j];\n const rawLine = rawLines[j];\n const lineStartOffset = markdownLine.indexOf(rawLine);\n\n if (lineStartOffset === -1) {\n // eslint-disable-next-line no-labels\n continue md;\n }\n\n const beforeMarkdownLines = markdownLines.slice(0, i + j).join('\\n') + (i + j > 0 ? '\\n' : '');\n const start = {\n offset: offset + beforeMarkdownLines.length + lineStartOffset,\n line: line + i + j,\n column: (i + j === 0 ? column : 0) + lineStartOffset,\n };\n const end = {\n offset: start.offset + rawLine.length,\n line: start.line,\n column: start.column + rawLine.length,\n };\n\n lines.push({\n start,\n end,\n });\n }\n break;\n }\n\n /* node:coverage ignore next 4 */\n if (lines.length === 0) {\n // This shouldn't ever happen but if it does it would be nice to have a good error message\n throw new Error(`Cannot find ${JSON.stringify(raw)} in ${JSON.stringify(markdown)}`);\n }\n\n const start = lines[0].start;\n const end = lines.at(-1)!.end;\n\n if (lines.length > 1 && lines.at(-1)!.start.offset === end.offset) {\n lines = lines.slice(0, -1);\n }\n\n return {\n lines,\n start,\n end,\n };\n}\n", "\nmodule.exports = require(\"./src/index.ts\").default;\nmodule.exports.addTokenPositions = require(\"./src/index.ts\").addTokenPositions;\n"], + "mappings": ";+bAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,uBAAAE,EAAA,YAAAC,IAkDO,SAASD,EAAkBE,EAAiB,CACjD,IAAMC,EAAWD,EAAO,IAAIE,GAASA,EAAM,GAAG,EAAE,KAAK,EAAE,EACvD,OAAOC,EAAYH,EAAQ,EAAG,EAAG,EAAGC,CAAQ,EAAE,MAChD,CAKe,SAARF,EAAiBK,EAAU,CAAC,EAAoB,CACrD,MAAO,CACL,MAAO,CACL,iBAAiBJ,EAAQ,CACvB,OAAOF,EAAkBE,CAAM,CACjC,CACF,CACF,CACF,CAEA,SAASG,EAAYH,EAAiBK,EAAgBC,EAAcC,EAAgBN,EAAkB,CACpG,QAAWC,KAASF,EAAQ,CAC1B,IAAMQ,EAAeN,EACfO,EAAWC,EAAYL,EAAQC,EAAMC,EAAQN,EAAUO,EAAa,GAAG,EAO7E,GANAA,EAAa,SAAWC,EAEpBD,EAAa,QACfL,EAAYK,EAAa,OAAQH,EAAQC,EAAMC,EAAQN,CAAQ,EAG7DO,EAAa,YAAa,CAC5B,IAAIG,EAAaN,EACbO,EAAWN,EACXO,EAAaN,EACbO,EAAeb,EACnB,QAAWc,KAAcP,EAAa,YAAa,CACjD,IAAMQ,EAAeb,EAAYK,EAAaO,CAAU,EAAGJ,EAAYC,EAAUC,EAAYC,CAAY,EACzGH,EAAaK,EAAa,OAC1BJ,EAAWI,EAAa,KACxBH,EAAaG,EAAa,OAC1BF,EAAeE,EAAa,QAC9B,CACF,CAMA,GAJIR,EAAa,OAAS,QACxBL,EAAYK,EAAa,MAAOH,EAAQC,EAAMC,EAAQN,CAAQ,EAG5DO,EAAa,OAAS,QAAS,CACjC,IAAIG,EAAaN,EACbO,EAAWN,EACXO,EAAaN,EACbO,EAAeb,EACnB,QAAWgB,KAAcT,EAAa,OAAQ,CAC5C,IAAMQ,EAAeb,EAAYc,EAAW,OAAQN,EAAYC,EAAUC,EAAYC,CAAY,EAClGH,EAAaK,EAAa,OAC1BJ,EAAWI,EAAa,KACxBH,EAAaG,EAAa,OAC1BF,EAAeE,EAAa,QAC9B,CACA,QAAWE,KAAOV,EAAa,KAC7B,QAAWW,KAAWD,EAAK,CACzB,IAAMF,EAAeb,EAAYgB,EAAQ,OAAQR,EAAYC,EAAUC,EAAYC,CAAY,EAC/FH,EAAaK,EAAa,OAC1BJ,EAAWI,EAAa,KACxBH,EAAaG,EAAa,OAC1BF,EAAeE,EAAa,QAC9B,CAEJ,CAEA,IAAMI,EAAcX,EAAS,IAAI,OAASJ,EAC1CA,EAASI,EAAS,IAAI,OACtBH,EAAOG,EAAS,IAAI,KACpBF,EAASE,EAAS,IAAI,OACtBR,EAAWA,EAAS,MAAMmB,CAAW,CACvC,CAEA,MAAO,CACL,OAAQpB,EACR,OAAAK,EACA,KAAAC,EACA,OAAAC,EACA,SAAAN,CACF,CACF,CAEA,SAASS,EAAYL,EAAgBC,EAAcC,EAAgBN,EAAkBoB,EAAuB,CAC1G,IAAIC,EAAwB,CAAC,EACvBC,EAAWF,EAAI,MAAM;AAAA,CAAI,EACzBG,EAAgBvB,EAAS,MAAM;AAAA,CAAI,EAGzCwB,EAAI,QAASC,EAAI,EAAGA,GAAKF,EAAc,OAASD,EAAS,OAAQG,IAAK,CACpEJ,EAAQ,CAAC,EACT,QAASK,EAAI,EAAGA,EAAIJ,EAAS,OAAQI,IAAK,CACxC,IAAMC,EAAeJ,EAAcE,EAAIC,CAAC,EAClCE,EAAUN,EAASI,CAAC,EACpBG,EAAkBF,EAAa,QAAQC,CAAO,EAEpD,GAAIC,IAAoB,GAEtB,SAASL,EAGX,IAAMM,EAAsBP,EAAc,MAAM,EAAGE,EAAIC,CAAC,EAAE,KAAK;AAAA,CAAI,GAAKD,EAAIC,EAAI,EAAI;AAAA,EAAO,IACrFK,EAAQ,CACZ,OAAQ3B,EAAS0B,EAAoB,OAASD,EAC9C,KAAMxB,EAAOoB,EAAIC,EACjB,QAASD,EAAIC,IAAM,EAAIpB,EAAS,GAAKuB,CACvC,EACMG,EAAM,CACV,OAAQD,EAAM,OAASH,EAAQ,OAC/B,KAAMG,EAAM,KACZ,OAAQA,EAAM,OAASH,EAAQ,MACjC,EAEAP,EAAM,KAAK,CACT,MAAAU,EACA,IAAAC,CACF,CAAC,CACH,CACA,KACF,CAGA,GAAIX,EAAM,SAAW,EAEnB,MAAM,IAAI,MAAM,eAAe,KAAK,UAAUD,CAAG,CAAC,OAAO,KAAK,UAAUpB,CAAQ,CAAC,EAAE,EAGrF,IAAM+B,EAAQV,EAAM,CAAC,EAAE,MACjBW,EAAMX,EAAM,GAAG,EAAE,EAAG,IAE1B,OAAIA,EAAM,OAAS,GAAKA,EAAM,GAAG,EAAE,EAAG,MAAM,SAAWW,EAAI,SACzDX,EAAQA,EAAM,MAAM,EAAG,EAAE,GAGpB,CACL,MAAAA,EACA,MAAAU,EACA,IAAAC,CACF,CACF,CA/LA,IAAAC,EAAAC,EAAA,oBCCA,OAAO,QAAU,WAA0B,QAC3C,OAAO,QAAQ,kBAAoB,WAA0B", + "names": ["src_exports", "__export", "addTokenPositions", "src_default", "tokens", "markdown", "token", "addPosition", "options", "offset", "line", "column", "genericToken", "position", "getPosition", "nextOffset", "nextLine", "nextColumn", "nextMarkdown", "childToken", "nextPosition", "headerCell", "row", "rowCell", "deltaOffset", "raw", "lines", "rawLines", "markdownLines", "md", "i", "j", "markdownLine", "rawLine", "lineStartOffset", "beforeMarkdownLines", "start", "end", "init_src", "__esmMin"] +} diff --git a/static/js/lib/node_modules/marked-token-position/package.json b/static/js/lib/node_modules/marked-token-position/package.json new file mode 100644 index 0000000..4f359e9 --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/package.json @@ -0,0 +1,66 @@ +{ + "name": "marked-token-position", + "version": "2.0.2", + "description": "marked extension template", + "main": "./lib/index.esm.js", + "module": "./lib/index.esm.js", + "browser": "./lib/index.umd.js", + "type": "module", + "keywords": [ + "marked", + "extension" + ], + "files": [ + "lib/", + "src/" + ], + "exports": { + ".": { + "typescript": "./src/index.ts", + "types": "./lib/index.d.ts", + "default": "./lib/index.esm.js" + } + }, + "scripts": { + "build": "npm run build:esbuild && npm run build:types", + "build:esbuild": "node esbuild.config.js", + "build:types": "tsc && dts-bundle-generator --export-referenced-types --project tsconfig.json -o lib/index.d.ts src/index.ts", + "format": "eslint --fix", + "lint": "eslint", + "test": "npm run build:esbuild && node --experimental-transform-types ./spec/test.config.js", + "test:cover": "npm run build:esbuild && node --experimental-transform-types --experimental-test-coverage ./spec/test.config.js -- --cover", + "test:only": "npm run build:esbuild && node --experimental-transform-types ./spec/test.config.js -- --only", + "test:types": "npm run build:types && tsc --project tsconfig-test-types.json && attw -P --entrypoints . --profile esm-only", + "test:update": "npm run build:esbuild && node --experimental-transform-types --test-update-snapshots ./spec/test.config.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/UziTech/marked-token-position.git" + }, + "author": "Tony Brix (https://Tony.Brix.ninja)", + "license": "MIT", + "bugs": { + "url": "https://github.com/UziTech/marked-token-position/issues" + }, + "homepage": "https://github.com/UziTech/marked-token-position#readme", + "peerDependencies": { + "marked": ">=16.2.0 <19" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.18.2", + "@markedjs/eslint-config": "^1.0.14", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/commit-analyzer": "^13.0.1", + "@semantic-release/git": "^10.0.1", + "@semantic-release/github": "^12.0.6", + "@semantic-release/npm": "^13.1.5", + "@semantic-release/release-notes-generator": "^14.1.0", + "dts-bundle-generator": "^9.5.1", + "esbuild": "^0.28.0", + "esbuild-plugin-umd-wrapper": "^3.0.0", + "eslint": "^10.2.0", + "marked": "^18.0.0", + "semantic-release": "^25.0.3", + "typescript": "^6.0.2" + } +} diff --git a/static/js/lib/node_modules/marked-token-position/src/index.ts b/static/js/lib/node_modules/marked-token-position/src/index.ts new file mode 100644 index 0000000..d624389 --- /dev/null +++ b/static/js/lib/node_modules/marked-token-position/src/index.ts @@ -0,0 +1,192 @@ +/* node:coverage ignore next */ +import type { MarkedExtension, Token, Tokens } from 'marked'; + +export interface TokenWithPosition extends Tokens.Generic { + position: Position; +} +interface Position { + /** + * Positions for each line of the token. LinePositions will not include the newline character for the line. + */ + lines: LinePosition[] + /** + * Position at the beginning of token + */ + start: PositionFields; + /** + * Position at the end of token + */ + end: PositionFields; +} + +interface LinePosition { + /** + * Position at the beginning of line + */ + start: PositionFields; + /** + * Position at the end of line. Will not include the newline character. + */ + end: PositionFields; +} + +interface PositionFields { + /** + * Number of characters from the beginning of the markdown string + */ + offset: number; + /** + * Line number of the token. Starts at line 0. + */ + line: number; + /** + * Column number of the token. Starts at column 0. + */ + column: number; +} + +/** + * Add position field to tokens + */ +export function addTokenPositions(tokens: Token[]) { + const markdown = tokens.map(token => token.raw).join(''); + return addPosition(tokens, 0, 0, 0, markdown).tokens; +} + +/** + * Marked extension to add position field to tokens + */ +export default function(options = {}): MarkedExtension { + return { + hooks: { + processAllTokens(tokens) { + return addTokenPositions(tokens); + }, + }, + }; +} + +function addPosition(tokens: Token[], offset: number, line: number, column: number, markdown: string) { + for (const token of tokens) { + const genericToken = token as Tokens.Generic; + const position = getPosition(offset, line, column, markdown, genericToken.raw); + genericToken.position = position; + + if (genericToken.tokens) { + addPosition(genericToken.tokens, offset, line, column, markdown); + } + + if (genericToken.childTokens) { + let nextOffset = offset; + let nextLine = line; + let nextColumn = column; + let nextMarkdown = markdown; + for (const childToken of genericToken.childTokens) { + const nextPosition = addPosition(genericToken[childToken], nextOffset, nextLine, nextColumn, nextMarkdown); + nextOffset = nextPosition.offset; + nextLine = nextPosition.line; + nextColumn = nextPosition.column; + nextMarkdown = nextPosition.markdown; + } + } + + if (genericToken.type === 'list') { + addPosition(genericToken.items, offset, line, column, markdown); + } + + if (genericToken.type === 'table') { + let nextOffset = offset; + let nextLine = line; + let nextColumn = column; + let nextMarkdown = markdown; + for (const headerCell of genericToken.header) { + const nextPosition = addPosition(headerCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown); + nextOffset = nextPosition.offset; + nextLine = nextPosition.line; + nextColumn = nextPosition.column; + nextMarkdown = nextPosition.markdown; + } + for (const row of genericToken.rows) { + for (const rowCell of row) { + const nextPosition = addPosition(rowCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown); + nextOffset = nextPosition.offset; + nextLine = nextPosition.line; + nextColumn = nextPosition.column; + nextMarkdown = nextPosition.markdown; + } + } + } + + const deltaOffset = position.end.offset - offset; + offset = position.end.offset; + line = position.end.line; + column = position.end.column; + markdown = markdown.slice(deltaOffset); + } + + return { + tokens: tokens as TokenWithPosition[], + offset, + line, + column, + markdown, + }; +} + +function getPosition(offset: number, line: number, column: number, markdown: string, raw: string): Position { + let lines: LinePosition[] = []; + const rawLines = raw.split('\n'); + const markdownLines = markdown.split('\n'); + + // eslint-disable-next-line no-labels + md: for (let i = 0; i <= markdownLines.length - rawLines.length; i++) { + lines = []; + for (let j = 0; j < rawLines.length; j++) { + const markdownLine = markdownLines[i + j]; + const rawLine = rawLines[j]; + const lineStartOffset = markdownLine.indexOf(rawLine); + + if (lineStartOffset === -1) { + // eslint-disable-next-line no-labels + continue md; + } + + const beforeMarkdownLines = markdownLines.slice(0, i + j).join('\n') + (i + j > 0 ? '\n' : ''); + const start = { + offset: offset + beforeMarkdownLines.length + lineStartOffset, + line: line + i + j, + column: (i + j === 0 ? column : 0) + lineStartOffset, + }; + const end = { + offset: start.offset + rawLine.length, + line: start.line, + column: start.column + rawLine.length, + }; + + lines.push({ + start, + end, + }); + } + break; + } + + /* node:coverage ignore next 4 */ + if (lines.length === 0) { + // This shouldn't ever happen but if it does it would be nice to have a good error message + throw new Error(`Cannot find ${JSON.stringify(raw)} in ${JSON.stringify(markdown)}`); + } + + const start = lines[0].start; + const end = lines.at(-1)!.end; + + if (lines.length > 1 && lines.at(-1)!.start.offset === end.offset) { + lines = lines.slice(0, -1); + } + + return { + lines, + start, + end, + }; +} diff --git a/static/js/lib/node_modules/marked/LICENSE.md b/static/js/lib/node_modules/marked/LICENSE similarity index 100% rename from static/js/lib/node_modules/marked/LICENSE.md rename to static/js/lib/node_modules/marked/LICENSE diff --git a/static/js/lib/node_modules/marked/README.md b/static/js/lib/node_modules/marked/README.md index d4ab251..60f0b28 100644 --- a/static/js/lib/node_modules/marked/README.md +++ b/static/js/lib/node_modules/marked/README.md @@ -5,7 +5,6 @@ # Marked [![npm](https://badgen.net/npm/v/marked)](https://www.npmjs.com/package/marked) -[![gzip size](https://badgen.net/badgesize/gzip/https://cdn.jsdelivr.net/npm/marked/marked.min.js)](https://cdn.jsdelivr.net/npm/marked/marked.min.js) [![install size](https://badgen.net/packagephobia/install/marked)](https://packagephobia.now.sh/result?p=marked) [![downloads](https://badgen.net/npm/dt/marked)](https://www.npmjs.com/package/marked) [![github actions](https://github.com/markedjs/marked/workflows/Tests/badge.svg)](https://github.com/markedjs/marked/actions) @@ -18,7 +17,7 @@ ## Demo -Checkout the [demo page](https://marked.js.org/demo/) to see marked in action ⛹️ +Check out the [demo page](https://marked.js.org/demo/) to see Marked in action ⛹️ ## Docs @@ -33,7 +32,7 @@ Also read about: **Node.js:** Only [current and LTS](https://nodejs.org/en/about/releases/) Node.js versions are supported. End of life Node.js versions may become incompatible with Marked at any point in time. -**Browser:** Not IE11 :) +**Browser:** [Baseline Widely Available](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility) ## Installation @@ -84,7 +83,7 @@ $ marked --help
- + - From 45cbd4934562184c8250b1a3b392de37da03aa4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 20 May 2026 18:28:16 +0200 Subject: [PATCH 13/24] Tree focus --- static/css/markdown.css | 43 ++++++++++++++++++++++++++++++++++++++++- static/css/notes2.css | 15 ++++++++++++-- static/js/app.mjs | 8 +++++--- static/js/node.mjs | 11 ++++++++--- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/static/css/markdown.css b/static/css/markdown.css index 19df20a..8b4c97e 100644 --- a/static/css/markdown.css +++ b/static/css/markdown.css @@ -1,11 +1,12 @@ .el-node-markdown { h1 { border-bottom: 1px solid #ccc; - margin-top: 48px; + margin-top: 32px; margin-bottom: 8px; display: inline-block; font-size: 1.25em; + border-radius: 8px; color: #fff; background-color: var(--color1); @@ -27,4 +28,44 @@ content: "> "; color: var(--color1); } + + img { + max-width: var(--thumbnail-width); + max-height: var(--thumbnail-height); + } + + table { + border: 1px solid #ccc; + border-collapse: collapse; + + th { + text-align: left; + padding: 8px; + } + + th, + td { + border: 1px solid #ccc; + padding: 8px; + } + } + + code { + background-color: #f8f8f8; + border: 1px solid #ccc; + padding: 2px 4px; + border-radius: 4px; + } + + pre { + background-color: #f8f8f8; + border: 1px solid #ccc; + padding: 8px; + border-radius: 4px; + + code { + border: unset; + padding: unset; + } + } } diff --git a/static/css/notes2.css b/static/css/notes2.css index 4945518..477b05a 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -2,6 +2,8 @@ :root { --content-width: 900px; + --thumbnail-width: 300px; + --thumbnail-height: 100px; } html { @@ -55,10 +57,18 @@ html { z-index: 100; border-left: 2px solid #333; - &:focus { - border-left: 2px solid #fe5f55; + n2-tree { + border: 2px solid #333; } + &:focus-within { + n2-tree { + border: 2px solid #fe5f55; + } + + } + + #logo { display: grid; position: relative; @@ -315,6 +325,7 @@ n2-nodeui { .el-node-content { display: none; } + .el-node-markdown { display: block; } diff --git a/static/js/app.mjs b/static/js/app.mjs index 03a3916..7fa4dda 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -55,10 +55,12 @@ export class App { switch (event.key.toUpperCase()) { case 'T': - if (document.activeElement.id === 'tree-nodes') - this.nodeUI.takeFocus() - else + if (document.activeElement.id === 'tree-nodes') { + console.log('take focus') this.nodeUI.takeFocus() + } else { + this.tree.focus() + } break case 'F': diff --git a/static/js/node.mjs b/static/js/node.mjs index a8afc50..4d5e83d 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -22,7 +22,7 @@ export class N2NodeUI extends CustomHTMLElement {
-
+
@@ -55,7 +55,7 @@ export class N2NodeUI extends CustomHTMLElement { }) _mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown())) - _mbus.subscribe('MARKDOWN_EDIT', ({detail}) => this.editMarkdown(detail.data)) + _mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data)) this.elNodeContent.addEventListener('input', event => this.contentChanged(event)) this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown())) @@ -68,7 +68,12 @@ export class N2NodeUI extends CustomHTMLElement { this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value) }// }}} takeFocus() {// {{{ - this.elNodeContent.focus() + console.log('taking focus', this.showMarkdown()) + if (this.showMarkdown()) { + this.elNodeMarkdown.focus() + console.log(this.elNodeMarkdown) + } else + this.elNodeContent.focus() }// }}} contentChanged(event) {//{{{ From 84181858c8384a5f99b0c7283036c6b50e81b9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 20 May 2026 18:49:43 +0200 Subject: [PATCH 14/24] Show markdown only when there is content --- static/js/node.mjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/static/js/node.mjs b/static/js/node.mjs index 4d5e83d..339016d 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -41,6 +41,7 @@ export class N2NodeUI extends CustomHTMLElement { _mbus.subscribe('NODE_UI_OPEN', event => { this.node = event.detail.data + this.showMarkdown(true) this.render() }) @@ -68,10 +69,8 @@ export class N2NodeUI extends CustomHTMLElement { this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value) }// }}} takeFocus() {// {{{ - console.log('taking focus', this.showMarkdown()) if (this.showMarkdown()) { this.elNodeMarkdown.focus() - console.log(this.elNodeMarkdown) } else this.elNodeContent.focus() }// }}} @@ -83,7 +82,12 @@ export class N2NodeUI extends CustomHTMLElement { return this.node?.isModified() }// }}} showMarkdown(state) {// {{{ - switch (state) { + // No point in showing markdown if there is no data. + // If there is no data, it will show a blank page regardless, and the user will most + // likely want to edit content, which can't be done in markdown. + const show = this.node.content().trim() !== '' && state + + switch (show) { case true: this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value) this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown.svg` From f37ebc1c4120e92ba8198b549c7060d22d9ab554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 20 May 2026 19:58:34 +0200 Subject: [PATCH 15/24] Offline first --- main.go | 10 +++++ static/js/node.mjs | 2 +- static/service_worker.js | 81 +++++++++++++++++++++++++++++++------- views/pages/offline.gotmpl | 4 ++ 4 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 views/pages/offline.gotmpl diff --git a/main.go b/main.go index 96f08aa..b44319e 100644 --- a/main.go +++ b/main.go @@ -132,6 +132,7 @@ func main() { // {{{ http.HandleFunc("/notes2", pageNotes2) http.HandleFunc("/login", pageLogin) http.HandleFunc("/sync", pageSync) + http.HandleFunc("/offline", pageOffline) http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) @@ -226,6 +227,15 @@ func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{ return } } // }}} +func pageOffline(w http.ResponseWriter, r *http.Request) { // {{{ + page := NewPage("offline") + + err := Webengine.Render(page, w, r) + if err != nil { + w.Write([]byte(err.Error())) + return + } +} // }}} func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{ page := NewPage("login") diff --git a/static/js/node.mjs b/static/js/node.mjs index 339016d..c4b2be0 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -85,7 +85,7 @@ export class N2NodeUI extends CustomHTMLElement { // No point in showing markdown if there is no data. // If there is no data, it will show a blank page regardless, and the user will most // likely want to edit content, which can't be done in markdown. - const show = this.node.content().trim() !== '' && state + const show = this.node?.content().trim() !== '' && state switch (show) { case true: diff --git a/static/service_worker.js b/static/service_worker.js index c48c162..6c2e8e2 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -2,27 +2,41 @@ const CACHE_NAME = 'notes2-{{ .VERSION }}' const CACHED_ASSETS = [ '/', '/notes2', + '/offline', '/css/{{ .VERSION }}/main.css', + '/css/{{ .VERSION }}/markdown.css', '/css/{{ .VERSION }}/notes2.css', + '/css/{{ .VERSION }}/theme.css', - '/js/{{ .VERSION }}/lib/fullcalendar.min.js', - '/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js', - '/js/{{ .VERSION }}/lib/sjcl.js', + '/images/{{ .VERSION }}/collapsed.svg', + '/images/{{ .VERSION }}/expanded.svg', + '/images/{{ .VERSION }}/icon_markdown_hollow.svg', + '/images/{{ .VERSION }}/icon_markdown.svg', + '/images/{{ .VERSION }}/icon_refresh.svg', + '/images/{{ .VERSION }}/icon_save_disabled.svg', + '/images/{{ .VERSION }}/icon_search.svg', + '/images/{{ .VERSION }}/leaf.svg', + '/images/{{ .VERSION }}/logo.svg', '/js/{{ .VERSION }}/api.mjs', + '/js/{{ .VERSION }}/app.mjs', + '/js/{{ .VERSION }}/checklist.mjs', + '/js/{{ .VERSION }}/crypto.mjs', + '/js/{{ .VERSION }}/key.mjs', + '/js/{{ .VERSION }}/lib/custom_html_element.mjs', + '/js/{{ .VERSION }}/lib/fullcalendar.min.js', + '/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js', + '/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js', + '/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js', + '/js/{{ .VERSION }}/lib/sjcl.js', + '/js/{{ .VERSION }}/marked_position.mjs', + '/js/{{ .VERSION }}/mbus.mjs', '/js/{{ .VERSION }}/node.mjs', '/js/{{ .VERSION }}/node_store.mjs', '/js/{{ .VERSION }}/notes2.mjs', '/js/{{ .VERSION }}/sync.mjs', - '/js/{{ .VERSION }}/key.mjs', - '/js/{{ .VERSION }}/crypto.mjs', - '/js/{{ .VERSION }}/checklist.mjs', - - '/images/{{ .VERSION }}/logo.svg', - '/images/{{ .VERSION }}/leaf.svg', - '/images/{{ .VERSION }}/collapsed.svg', - '/images/{{ .VERSION }}/expanded.svg', + '/js/{{ .VERSION }}/tree.mjs', ] async function precache() { @@ -32,13 +46,50 @@ async function precache() { async function fetchAsset(event) { try { - return await fetch(event.request) - } catch (e) { const cache = await caches.open(CACHE_NAME) - return cache.match(event.request) + const match = await cache.match(event.request) + + if (match !== undefined) { + // ----------------------------------------------- + // This page is precached - return it immediately. + // ----------------------------------------------- + //console.log('From cache', event.request.url) + return match + } else { + // --------------------------------------------------------------- + // Not in cache - send it for an online request/browser cache hit. + // --------------------------------------------------------------- + console.log('From network', event.request.url) + const resp = await fetch(event.request) + + // This will trigger on an HTTP error such as 502. + if (!resp.ok) { + console.log('HTTP error', resp.status) + + // When JSON is expected, return that instead of the offline HTML page. + return await offline(event, `${resp.status} ${resp.statusText}`) + } + return resp + } + } catch (e) { + // An error here is something like a DNS problem, not a regular HTTP problem. + console.log('Network error', e, event.request.url) + return await offline(event, e) } } +async function offline(event, errText) { + if (event.request.headers.get('X-JSON')) { + return new Response('{ "OK": false, "Error": "Network is offline"}', { headers: { 'Content-Type': 'application/json' } }) + } + + const cache = await caches.open(CACHE_NAME) + const offline = await cache.match('/offline') + let body = await offline.text() + body = body.replace('||ERROR||', errText) + return new Response(body, { headers: { 'Content-Type': 'text/html' } }) +} + async function cleanupCache() { const keys = await caches.keys() const keysToDelete = keys.map(key => { @@ -61,6 +112,6 @@ self.addEventListener('activate', event => { }) self.addEventListener('fetch', event => { - //console.log('SERVICE WORKER: fetch') + //console.log('SERVICE WORKER: fetch', event.request.url) event.respondWith(fetchAsset(event)) }) diff --git a/views/pages/offline.gotmpl b/views/pages/offline.gotmpl new file mode 100644 index 0000000..0c283f9 --- /dev/null +++ b/views/pages/offline.gotmpl @@ -0,0 +1,4 @@ +{{ define "page" }} +
Site is offline.
+
||ERROR||
+{{ end }} From 12b629756f6229134cca9e4e42f1510a69cc4fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 20 May 2026 20:06:10 +0200 Subject: [PATCH 16/24] Disable caching when developing --- static/service_worker.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/static/service_worker.js b/static/service_worker.js index 6c2e8e2..b750e54 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -53,18 +53,18 @@ async function fetchAsset(event) { // ----------------------------------------------- // This page is precached - return it immediately. // ----------------------------------------------- - //console.log('From cache', event.request.url) + console.debug('From cache', event.request.url) return match } else { // --------------------------------------------------------------- // Not in cache - send it for an online request/browser cache hit. // --------------------------------------------------------------- - console.log('From network', event.request.url) + console.debug('From network', event.request.url) const resp = await fetch(event.request) // This will trigger on an HTTP error such as 502. if (!resp.ok) { - console.log('HTTP error', resp.status) + console.error('HTTP error', resp.status) // When JSON is expected, return that instead of the offline HTML page. return await offline(event, `${resp.status} ${resp.statusText}`) @@ -73,7 +73,7 @@ async function fetchAsset(event) { } } catch (e) { // An error here is something like a DNS problem, not a regular HTTP problem. - console.log('Network error', e, event.request.url) + console.error('Network error', e, event.request.url) return await offline(event, e) } } @@ -100,18 +100,22 @@ async function cleanupCache() { } self.addEventListener('install', event => { - console.log('SERVICE WORKER: install') + console.debug('SERVICE WORKER: install') self.skipWaiting() event.waitUntil(precache()) }) self.addEventListener('activate', event => { - console.log('SERVICE WORKER: activate') + console.debug('SERVICE WORKER: activate') self.clients.claim() event.waitUntil(cleanupCache()) }) self.addEventListener('fetch', event => { - //console.log('SERVICE WORKER: fetch', event.request.url) + // console.debug('SERVICE WORKER: fetch', event.request.url) + + if ({{ .DevMode }}) + return event + event.respondWith(fetchAsset(event)) }) From 13467c2f283d117614558a6bd30e35ed214d93ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 20 May 2026 20:22:45 +0200 Subject: [PATCH 17/24] Fixed adding node with ROOT_NODE parent UUID --- sql/00011.sql | 166 +++++++++++++++++++++++++++++++++++++++++++++ static/js/node.mjs | 1 - 2 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 sql/00011.sql diff --git a/sql/00011.sql b/sql/00011.sql new file mode 100644 index 0000000..a3245bc --- /dev/null +++ b/sql/00011.sql @@ -0,0 +1,166 @@ +CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb) +LANGUAGE PLPGSQL AS $$ + +DECLARE + node_data jsonb; + node_updated timestamptz; + db_updated timestamptz; + db_uuid bpchar; + db_client bpchar; + db_client_seq int; + node_uuid bpchar; + parent_uuid bpchar; + +BEGIN + RAISE NOTICE '--------------------------'; + FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) + LOOP + node_uuid = (node_data->>'UUID')::bpchar; + node_updated = (node_data->>'Updated')::timestamptz; + + IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN + parent_uuid = NULL; + ELSE + parent_uuid = node_data->>'ParentUUID'; + END IF; + + /* Retrieve the current modified timestamp for this node from the database. */ + SELECT + uuid, updated, client, client_sequence + INTO + db_uuid, db_updated, db_client, db_client_seq + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid; + + /* Is the node not in database? It needs to be created. */ + IF db_uuid IS NULL THEN + RAISE NOTICE '01 New node %', node_uuid; + INSERT INTO public."node" ( + user_id, "uuid", parent_uuid, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + parent_uuid, + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ); + CONTINUE; + END IF; + + + /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ + IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN + RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; + CONTINUE; + END IF; + + /* Determine if the incoming node data is to go into history or replace the current node. */ + IF db_updated > node_updated THEN + RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid; + /* Incoming node is going straight to history since it is older than the current node. */ + INSERT INTO node_history( + user_id, "uuid", parents, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ) + ON CONFLICT (client, client_sequence) + DO NOTHING; + ELSE + RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence'); + /* Incoming node is newer and will replace the current node. + * + * The current node is copied to the node_history table and then modified in place + * with the incoming data. */ + INSERT INTO node_history( + user_id, "uuid", parents, + created, updated, "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + SELECT + user_id, + "uuid", + ( + WITH RECURSIVE nodes AS ( + SELECT + uuid, + COALESCE(parent_uuid, '') AS parent_uuid, + name, + 0 AS depth + FROM node + WHERE + uuid = node_uuid + + UNION + + SELECT + n.uuid, + COALESCE(n.parent_uuid, '') AS parent_uuid, + n.name, + nr.depth+1 AS depth + FROM node n + INNER JOIN nodes nr ON n.uuid = nr.parent_uuid + ) + SELECT ARRAY ( + SELECT name + FROM nodes + ORDER BY depth DESC + OFFSET 1 /* discard itself */ + ) + ), + created, + updated, + name, + content, + markdown, + content_encrypted, + client, + client_sequence + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid + ON CONFLICT (client, client_sequence) + DO NOTHING; + + /* Current node in database is updated with incoming data. */ + UPDATE public."node" + SET + updated = (node_data->>'Updated')::timestamptz, + updated_seq = nextval('node_updates'), + name = (node_data->>'Name')::varchar, + content = (node_data->>'Content')::text, + markdown = (node_data->>'Markdown')::bool, + client = p_client_uuid, + client_sequence = (node_data->>'ClientSequence')::int + WHERE + user_id = p_user_id AND + uuid = node_uuid; + END IF; + + END LOOP; +END +$$; diff --git a/static/js/node.mjs b/static/js/node.mjs index c4b2be0..5f002a8 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -130,7 +130,6 @@ export class Node { }// }}} constructor(nodeData, level) {//{{{ - this.Level = level this.data = nodeData this.UUID = nodeData.UUID From d4e6c58256b9f5c2455431f1b4bcd26cd5926741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 20 May 2026 20:24:06 +0200 Subject: [PATCH 18/24] Whitespace OCD --- sql/00011.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sql/00011.sql b/sql/00011.sql index a3245bc..5b67839 100644 --- a/sql/00011.sql +++ b/sql/00011.sql @@ -14,7 +14,7 @@ DECLARE BEGIN RAISE NOTICE '--------------------------'; FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) - LOOP + LOOP node_uuid = (node_data->>'UUID')::bpchar; node_updated = (node_data->>'Updated')::timestamptz; @@ -58,7 +58,7 @@ BEGIN CONTINUE; END IF; - + /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; @@ -161,6 +161,6 @@ BEGIN uuid = node_uuid; END IF; - END LOOP; + END LOOP; END $$; From 0ae8ef939b0b990d97b2d9c1b3fe61fe17832a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 21 May 2026 18:05:45 +0200 Subject: [PATCH 19/24] Removed old dependency on marked --- static/service_worker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/static/service_worker.js b/static/service_worker.js index b750e54..806eaad 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -27,7 +27,6 @@ const CACHED_ASSETS = [ '/js/{{ .VERSION }}/lib/custom_html_element.mjs', '/js/{{ .VERSION }}/lib/fullcalendar.min.js', '/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js', - '/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js', '/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js', '/js/{{ .VERSION }}/lib/sjcl.js', '/js/{{ .VERSION }}/marked_position.mjs', From 068e21c962a328e8a0eaa150e82f4e055341cd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sat, 23 May 2026 22:55:16 +0200 Subject: [PATCH 20/24] Rename nodes. --- static/js/node.mjs | 38 ++++++++++++++++++++++---------------- static/js/tree.mjs | 15 +++++++++++++-- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/static/js/node.mjs b/static/js/node.mjs index 5f002a8..acebbb7 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -48,6 +48,7 @@ export class N2NodeUI extends CustomHTMLElement { _mbus.subscribe('NODE_MODIFIED', () => { document.querySelector('#crumbs .crumbs')?.classList.add('node-modified') this.elIconSave.src = `/images/${_VERSION}/icon_save.svg` + this.render() }) _mbus.subscribe('NODE_UNMODIFIED', () => { @@ -58,6 +59,18 @@ export class N2NodeUI extends CustomHTMLElement { _mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown())) _mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data)) + this.elName.addEventListener('click', () => { + const name = prompt('Change title', this.node.data.Name) + if (name === null) + return + + try { + this.node.setName(name) + } catch (err) { + console.error(err) + alert(err) + } + }) this.elNodeContent.addEventListener('input', event => this.contentChanged(event)) this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown())) @@ -150,21 +163,6 @@ export class Node { this._parent = null this.reset() - - /* - this.RenderMarkdown = signal(nodeData.RenderMarkdown) - this.Markdown = false - this.ShowChecklist = signal(false) - this._content = nodeData.Content - this.Crumbs = [] - this.Files = [] - this._decrypted = false - this._expanded = false // start value for the TreeNode component, - this.ChecklistGroups = {} - this.ScheduleEvents = signal([]) - // it doesn't control it afterwards. - // Used to expand the crumbs upon site loading. - */ }//}}} reset() {// {{{ @@ -235,8 +233,16 @@ export class Node { setContent(new_content) {//{{{ this._content = new_content this._modified = true - _mbus.dispatch('NODE_MODIFIED') + _mbus.dispatch('NODE_MODIFIED', { node: this }) }//}}} + setName(new_name) {// {{{ + if (new_name.trim() === '') + throw new Error(`The name can't be empty`) + + this.data.Name = new_name + this._modified = true + _mbus.dispatch('NODE_MODIFIED', { node: this }) + }// }}} async save() {//{{{ this.data.Content = this._content this.data.Updated = new Date().toISOString() diff --git a/static/js/tree.mjs b/static/js/tree.mjs index 1da5dee..3732fc5 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -31,6 +31,17 @@ export class N2Tree extends CustomHTMLElement { this.elSync.addEventListener('click', () => _sync.run()) this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false)) + _mbus.subscribe('NODE_MODIFIED', ({ detail })=>{ + const node = detail.data.node + const treenode = this.treeNodeComponents[node.get('UUID')] + + if (!treenode) + return + + treenode.node = node + treenode.render(true) + }) + this.populateFirstLevel() }// }}} render() {// {{{ @@ -46,14 +57,14 @@ export class N2Tree extends CustomHTMLElement { this.rendered = true return this }// }}} - reset() { + reset() {// {{{ console.log('tree reset') this.treeNodeComponents = {} this.treeTrunk = [] this.rendered = false this.elTreenodes.replaceChildren() this.populateFirstLevel() - } + }// }}} populateFirstLevel() {//{{{ nodeStore.get(ROOT_NODE) .then(node => node.fetchChildren()) From 21d93f0188b1d86a891df1222133bafdcef518ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sat, 23 May 2026 22:55:29 +0200 Subject: [PATCH 21/24] Design change. --- static/css/markdown.css | 5 ++ static/css/notes2.css | 31 +++++++----- static/images/collapsed.svg | 94 ++++++++++++++----------------------- static/images/expanded.svg | 77 +++++++++++------------------- static/images/leaf.svg | 77 ++++++++++++++---------------- 5 files changed, 124 insertions(+), 160 deletions(-) diff --git a/static/css/markdown.css b/static/css/markdown.css index 8b4c97e..84eb0b2 100644 --- a/static/css/markdown.css +++ b/static/css/markdown.css @@ -19,6 +19,7 @@ h2 { font-size: 1.25em; + margin-top: 32px; margin-bottom: 0px; color: var(--color1); } @@ -29,6 +30,10 @@ color: var(--color1); } + p { + line-height: 150%; + } + img { max-width: var(--thumbnail-width); max-height: var(--thumbnail-height); diff --git a/static/css/notes2.css b/static/css/notes2.css index 477b05a..71737dd 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -52,18 +52,22 @@ html { #tree { grid-area: tree; display: grid; - padding: 16px 0px 16px 16px; - color: #ddd; + background-color: #fafafa; + color: #444; z-index: 100; - border-left: 2px solid #333; + + border-right: 1px solid #ddd; n2-tree { - border: 2px solid #333; + /*border: 2px solid #f8f8f8;*/ + padding: 16px 48px 16px 24px; } &:focus-within { n2-tree { + /* border: 2px solid #fe5f55; + */ } } @@ -95,18 +99,20 @@ html { .node { display: grid; - grid-template-columns: 24px min-content; + grid-template-columns: 40px min-content; grid-template-rows: min-content 1fr; margin-top: 12px; - + align-items: center; .expand-toggle { user-select: none; + cursor: pointer; + justify-self: center; img { - width: 16px; - height: 16px; + width: auto; + height: 18px; } } @@ -128,8 +134,8 @@ html { .children { padding-left: 24px; - margin-left: 8px; - border-left: 1px solid #444; + margin-left: 18px; + border-left: 1px solid #ddd; grid-column: 1 / -1; &.collapsed { @@ -141,9 +147,12 @@ html { #tree-nodes { padding: 16px 32px; - background-color: #333; + /* border-radius: 8px; +*/ + /* box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75); + */ } #crumbs { diff --git a/static/images/collapsed.svg b/static/images/collapsed.svg index 8bd376f..d93f4ca 100644 --- a/static/images/collapsed.svg +++ b/static/images/collapsed.svg @@ -2,73 +2,49 @@ + image/svg+xml + + + transform="translate(-102.39375,-146.31458)"> + folder-outline + + + diff --git a/static/images/expanded.svg b/static/images/expanded.svg index e1a6f66..017e8a4 100644 --- a/static/images/expanded.svg +++ b/static/images/expanded.svg @@ -2,64 +2,43 @@ image/svg+xml + transform="translate(-101.33542,-147.10833)">folder-openfolder-open-outline diff --git a/static/images/leaf.svg b/static/images/leaf.svg index ed44541..306a2a0 100644 --- a/static/images/leaf.svg +++ b/static/images/leaf.svg @@ -2,56 +2,51 @@ image/svg+xml + transform="translate(-107.95,-148.16667)">folder-openfolder-open-outlinenotebook-outlinetext-box-outline From 19d0ce91059d2260414110e9a109d1f64b375cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sun, 24 May 2026 16:08:29 +0200 Subject: [PATCH 22/24] New colors on icons --- static/images/icon_refresh.svg | 13 +++++++------ static/images/icon_search.svg | 16 ++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/static/images/icon_refresh.svg b/static/images/icon_refresh.svg index d46322e..a6aa907 100644 --- a/static/images/icon_refresh.svg +++ b/static/images/icon_refresh.svg @@ -7,7 +7,7 @@ viewBox="0 0 4.2333398 5.8208399" version="1.1" id="svg1" - inkscape:version="1.3.2 (091e20e, 2023-11-25)" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" sodipodi:docname="icon_refresh.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -23,15 +23,16 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" - inkscape:zoom="0.83651094" - inkscape:cx="7.7703706" - inkscape:cy="11.356695" + inkscape:zoom="23.548693" + inkscape:cx="6.9218279" + inkscape:cy="12.5697" inkscape:window-width="1916" inkscape:window-height="1161" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" - inkscape:current-layer="layer1" /> + inkscape:current-layer="layer1" + showgrid="false" /> + style="stroke-width:0.264583;fill:#fe5f55;fill-opacity:1" /> diff --git a/static/images/icon_search.svg b/static/images/icon_search.svg index 8be3977..6de83dd 100644 --- a/static/images/icon_search.svg +++ b/static/images/icon_search.svg @@ -7,7 +7,7 @@ viewBox="0 0 109.40056 109.39984" version="1.1" id="svg8" - inkscape:version="1.4 (e7c3feb, 2024-10-09)" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" sodipodi:docname="icon_search.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -27,16 +27,16 @@ inkscape:pageshadow="2" inkscape:zoom="0.70710678" inkscape:cx="206.47518" - inkscape:cy="207.18229" + inkscape:cy="207.88939" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" units="px" - inkscape:window-width="2190" - inkscape:window-height="1404" - inkscape:window-x="1463" - inkscape:window-y="16" - inkscape:window-maximized="0" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="18" + inkscape:window-maximized="1" inkscape:showpageshadow="true" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d6d6d6" @@ -62,6 +62,6 @@ + style="stroke-width:6.25145;fill:#fe5f55;fill-opacity:1" /> From 7cde8cf87499a83ca96694fd9185af83d96852bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Tue, 26 May 2026 10:03:24 +0200 Subject: [PATCH 23/24] Added updated SQL --- sql/00012.sql | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 sql/00012.sql diff --git a/sql/00012.sql b/sql/00012.sql new file mode 100644 index 0000000..e62f011 --- /dev/null +++ b/sql/00012.sql @@ -0,0 +1,166 @@ +CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb) +LANGUAGE PLPGSQL AS $$ + +DECLARE + node_data jsonb; + node_updated timestamptz; + db_updated timestamptz; + db_uuid bpchar; + db_client bpchar; + db_client_seq int; + node_uuid bpchar; + parent_uuid_nullable bpchar; + +BEGIN + RAISE NOTICE '--------------------------'; + FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) + LOOP + node_uuid = (node_data->>'UUID')::bpchar; + node_updated = (node_data->>'Updated')::timestamptz; + + IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN + parent_uuid_nullable = NULL; + ELSE + parent_uuid_nullable = node_data->>'ParentUUID'; + END IF; + + /* Retrieve the current modified timestamp for this node from the database. */ + SELECT + uuid, updated, client, client_sequence + INTO + db_uuid, db_updated, db_client, db_client_seq + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid; + + /* Is the node not in database? It needs to be created. */ + IF db_uuid IS NULL THEN + RAISE NOTICE '01 New node %', node_uuid; + INSERT INTO public."node" ( + user_id, "uuid", parent_uuid, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + parent_uuid_nullable, + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ); + CONTINUE; + END IF; + + + /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ + IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN + RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; + CONTINUE; + END IF; + + /* Determine if the incoming node data is to go into history or replace the current node. */ + IF db_updated > node_updated THEN + RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid; + /* Incoming node is going straight to history since it is older than the current node. */ + INSERT INTO node_history( + user_id, "uuid", parents, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ) + ON CONFLICT (client, client_sequence) + DO NOTHING; + ELSE + RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence'); + /* Incoming node is newer and will replace the current node. + * + * The current node is copied to the node_history table and then modified in place + * with the incoming data. */ + INSERT INTO node_history( + user_id, "uuid", parents, + created, updated, "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + SELECT + user_id, + "uuid", + ( + WITH RECURSIVE nodes AS ( + SELECT + uuid, + COALESCE(parent_uuid, '') AS parent_uuid, + name, + 0 AS depth + FROM node + WHERE + uuid = node_uuid + + UNION + + SELECT + n.uuid, + COALESCE(n.parent_uuid, '') AS parent_uuid, + n.name, + nr.depth+1 AS depth + FROM node n + INNER JOIN nodes nr ON n.uuid = nr.parent_uuid + ) + SELECT ARRAY ( + SELECT name + FROM nodes + ORDER BY depth DESC + OFFSET 1 /* discard itself */ + ) + ), + created, + updated, + name, + content, + markdown, + content_encrypted, + client, + client_sequence + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid + ON CONFLICT (client, client_sequence) + DO NOTHING; + + /* Current node in database is updated with incoming data. */ + UPDATE public."node" + SET + updated = (node_data->>'Updated')::timestamptz, + updated_seq = nextval('node_updates'), + name = (node_data->>'Name')::varchar, + content = (node_data->>'Content')::text, + markdown = (node_data->>'Markdown')::bool, + client = p_client_uuid, + client_sequence = (node_data->>'ClientSequence')::int + WHERE + user_id = p_user_id AND + uuid = node_uuid; + END IF; + + END LOOP; +END +$$; From 40d1c388fd1375d5d31641e1791ebf6ee6b4328c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Tue, 26 May 2026 10:03:49 +0200 Subject: [PATCH 24/24] Bumped to v2 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index b44319e..d25f72f 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v1" +const VERSION = "v2" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200