Compare commits

..

3 commits

Author SHA1 Message Date
Magnus Åhall
b59c6c8b58 Table formatting WIP 2026-06-01 17:50:17 +02:00
Magnus Åhall
88304b90a4 Update node content if only pasting a file 2026-06-01 16:28:51 +02:00
Magnus Åhall
7bad7a5ce8 Fixed concurrent map write 2026-06-01 16:20:07 +02:00
3 changed files with 106 additions and 19 deletions

View file

@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
"sync"
) )
type Engine struct { type Engine struct {
@ -22,6 +23,10 @@ type Engine struct {
DevMode bool DevMode bool
} }
var (
templateLock sync.Mutex
)
func NewEngine(viewFS, staticFS fs.FS, devmode bool) (e Engine, err error) { // {{{ func NewEngine(viewFS, staticFS fs.FS, devmode bool) (e Engine, err error) { // {{{
e.parsedTemplates = make(map[string]*template.Template) e.parsedTemplates = make(map[string]*template.Template)
e.viewFS = viewFS e.viewFS = viewFS
@ -62,7 +67,9 @@ func (e *Engine) getComponentFilenames() (files []string, err error) { // {{{
} // }}} } // }}}
func (e *Engine) ReloadTemplates() { // {{{ func (e *Engine) ReloadTemplates() { // {{{
templateLock.Lock()
e.parsedTemplates = make(map[string]*template.Template) e.parsedTemplates = make(map[string]*template.Template)
templateLock.Unlock()
} // }}} } // }}}
func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{ func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{
@ -119,7 +126,9 @@ func (e *Engine) getPage(layout, page string) (tmpl *template.Template, err erro
return return
} }
templateLock.Lock()
e.parsedTemplates[page] = tmpl e.parsedTemplates[page] = tmpl
templateLock.Unlock()
return return
} // }}} } // }}}
func (e *Engine) Render(p Page, w http.ResponseWriter, r *http.Request) (err error) { // {{{ func (e *Engine) Render(p Page, w http.ResponseWriter, r *http.Request) (err error) { // {{{

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="7.1437349mm"
height="6.3499999mm"
viewBox="0 0 7.1437349 6.3499999"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
sodipodi:docname="icon_table.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.81067621"
inkscape:cx="69.078134"
inkscape:cy="155.4258"
inkscape:window-width="1916"
inkscape:window-height="1161"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-86.666022,-107.47612)">
<title
id="title1">table</title>
<path
d="m 87.459775,107.47612 h 5.556243 a 0.79374826,0.79374826 0 0 1 0.793739,0.79375 v 4.7625 a 0.79374826,0.79374826 0 0 1 -0.793739,0.79375 h -5.556243 a 0.79374826,0.79374826 0 0 1 -0.793753,-0.79375 v -4.7625 a 0.79374826,0.79374826 0 0 1 0.793753,-0.79375 m 0,1.5875 v 1.58749 h 2.381245 v -1.58749 h -2.381245 m 3.174998,0 v 1.58749 h 2.381245 v -1.58749 h -2.381245 m -3.174998,2.38125 v 1.5875 h 2.381245 v -1.5875 h -2.381245 m 3.174998,0 v 1.5875 h 2.381245 v -1.5875 z"
id="path1"
style="stroke-width:0.396873" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,4 +1,4 @@
import { ROOT_NODE, uuidv7, StoreFile } from 'node_store' import { ROOT_NODE, uuidv7 } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { MarkedPosition } from './marked_position.mjs' import { MarkedPosition } from './marked_position.mjs'
@ -9,7 +9,7 @@ export class N2PageNodeUI extends CustomHTMLElement {
<style> <style>
.el-functions { .el-functions {
display: grid; display: grid;
grid-template-columns: 1fr min-content; grid-template-columns: 1fr min-content min-content;
grid-gap: 8px; grid-gap: 8px;
align-items: center; align-items: center;
justify-items: end; justify-items: end;
@ -27,6 +27,7 @@ export class N2PageNodeUI extends CustomHTMLElement {
<div data-el="functions"> <div data-el="functions">
<img data-el="icon-markdown"> <img data-el="icon-markdown">
<img data-el="icon-save" src="/images/${_VERSION}/icon_save_disabled.svg"> <img data-el="icon-save" src="/images/${_VERSION}/icon_save_disabled.svg">
<img data-el="icon-table-format" src="/images/${_VERSION}/icon_table.svg">
</div> </div>
` `
}// }}} }// }}}
@ -74,6 +75,14 @@ export class N2PageNodeUI extends CustomHTMLElement {
this.elNodeContent.addEventListener('input', event => this.contentChanged(event)) this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event)) this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event))
this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown())) this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown()))
this.elIconTableFormat.addEventListener('click', () => {
const from = this.elNodeContent.selectionStart
const to = this.elNodeContent.selectionEnd
const sel = this.elNodeContent.value.slice(from, to)
this.formatTable(sel)
})
this.showMarkdown(true) this.showMarkdown(true)
}// }}} }// }}}
@ -119,7 +128,7 @@ export class N2PageNodeUI extends CustomHTMLElement {
return this.classList.contains('show-markdown') return this.classList.contains('show-markdown')
} }
}// }}} }// }}}
async pasteHandler(event) { async pasteHandler(event) {// {{{
const clipboardItems = event.clipboardData?.items const clipboardItems = event.clipboardData?.items
if (!clipboardItems) if (!clipboardItems)
return return
@ -139,33 +148,53 @@ export class N2PageNodeUI extends CustomHTMLElement {
const [start, end] = [this.elNodeContent.selectionStart, this.elNodeContent.selectionEnd] const [start, end] = [this.elNodeContent.selectionStart, this.elNodeContent.selectionEnd]
this.elNodeContent.setRangeText(`![${file.name}](db://${uuid})`, start, end, 'select'); this.elNodeContent.setRangeText(`![${file.name}](db://${uuid})`, start, end, 'select');
// Editing the textarea programatically doesn't generate the events it usually gets when edited interactively.
this.node.setContent(this.elNodeContent.value)
break break
default: default:
alert(`Unknown paste type of '${item.kind}'`) alert(`Unknown paste type of '${item.kind}'`)
} }
} }
} }// }}}
// Example usage: Displaying the image or preparing it for upload
handleImageBlob(blob) {
// 1. Create a local URL to preview it in an <img> tag if needed
const localUrl = URL.createObjectURL(blob)
console.log('Local preview URL:', localUrl)
// 2. Or prepare it for a FormData upload
const formData = new FormData()
formData.append('image', blob, 'pasted-image.png')
// fetch('/upload', { method: 'POST', body: formData })
}
editMarkdown(data) {// {{{ editMarkdown(data) {// {{{
this.showMarkdown(false) this.showMarkdown(false)
this.elNodeContent.selectionStart = data.position.start this.elNodeContent.selectionStart = data.position.start
this.elNodeContent.selectionEnd = data.position.end this.elNodeContent.selectionEnd = data.position.end
this.elNodeContent.focus() this.elNodeContent.focus()
}// }}} }// }}}
formatTable(t) {
const lines = t.split(/\r?\n/)
if (lines < 1)
return
let first = -1
let last = -1
let numColumns = 0
let colwidth = []
for (let i = 0; i < lines.length; i++) {
// -1 for split, -1 because number of columns are one less than number of pipes.
const columns = lines[i].split('|')
const linecols = columns.length - 1 - 1
numColumns = Math.max(numColumns, linecols)
if (linecols >= 1) {
if (first == -1)
first = i
last = i
continue
}
if (linecols < 1 && last > -1)
break
// Keep count of column width
}
console.log(first, last, columns)
}
} }
customElements.define('n2-nodeui', N2PageNodeUI) customElements.define('n2-nodeui', N2PageNodeUI)