From af3f10e35b19e5c7e5b52dcbd8e00553bbb2b02d Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Sat, 7 Oct 2023 07:04:58 +0300 Subject: [PATCH] logseq-like format parsing + markdown (stub) + ff support... Signed-off-by: Alex A. Naanou --- experiments/outline-editor/editor.css | 7 -- experiments/outline-editor/editor.js | 171 ++++++++++++++++++-------- experiments/outline-editor/generic.js | 36 +++++- experiments/outline-editor/index.html | 4 + 4 files changed, 161 insertions(+), 57 deletions(-) diff --git a/experiments/outline-editor/editor.css b/experiments/outline-editor/editor.css index a4a926a..60096ce 100755 --- a/experiments/outline-editor/editor.css +++ b/experiments/outline-editor/editor.css @@ -52,13 +52,6 @@ } /* show/hide node's view/code... */ -.editor .outline [tabindex]>span+textarea:not(:focus), -.editor .outline [tabindex]:has(>span+textarea:focus)>span:first-child { - position: absolute; - opacity: 0; - top: 0; -} - .editor .outline [tabindex]>textarea:focus+span, .editor .outline [tabindex]>textarea:not(:focus) { position: absolute; diff --git a/experiments/outline-editor/editor.js b/experiments/outline-editor/editor.js index 47e0593..e866856 100755 --- a/experiments/outline-editor/editor.js +++ b/experiments/outline-editor/editor.js @@ -82,19 +82,6 @@ var Outline = { return this.dom.querySelector('.toolbar') }, - // XXX revise name... - Block: function(place=none){ - var block = document.createElement('div') - block.setAttribute('tabindex', '0') - block.append( - document.createElement('textarea') - .autoUpdateSize(), - document.createElement('span')) - var cur = this.get() - place && cur - && cur[place](block) - return block }, - // // .get([]) // .get('focused'[, ]) @@ -287,28 +274,47 @@ var Outline = { return this }, // block serialization... + // XXX these should be symetrical... __code2html__: function(code){ - return code - .replace(/\n\s*/g, '
') + var elem = { + collapsed: false, + } + elem.text = code + // attributes... + // XXX make this generic... + .replace(/\n\s*collapsed::\s*(.*)\s*$/, + function(_, value){ + elem.collapsed = value.trim() == 'true' + return '' }) + // markdown... // XXX STUB... - .replace(/^# (.*)\s*$/g, '

$1

') - .replace(/^## (.*)\s*$/g, '

$1

') - .replace(/^### (.*)\s*$/g, '

$1

') - .replace(/^#### (.*)\s*$/g, '

$1

') + .replace(/^#\s*(.*)\s*(\n|$)/, '

$1

') + .replace(/^##\s*(.*)\s*(\n|$)/, '

$1

') + .replace(/^###\s*(.*)\s*(\n|$)/, '

$1

') + .replace(/^####\s*(.*)\s*(\n|$)/, '

$1

') + .replace(/^#####\s*(.*)\s*(\n|$)/, '
$1
') + .replace(/^######\s*(.*)\s*(\n|$)/, '
$1
') .replace(/\*(.*)\*/g, '$1') .replace(/~([^~]*)~/g, '$1') - .replace(/_([^_]*)_/g, '$1') }, + .replace(/_([^_]*)_/g, '$1') + .replace(/(\n|^)---*\h*(\n|$)/, '$1
') + .replace(/\n/g, '
\n') + return elem }, __html2code__: function(html){ return html - .replace(/
\s*/g, '\n') // XXX STUB... - .replace(/^

(.*)<\/h1>\s*$/g, '# $1') - .replace(/^

(.*)<\/h2>\s*$/g, '## $1') - .replace(/^

(.*)<\/h3>\s*$/g, '### $1') - .replace(/^

(.*)<\/h4>\s*$/g, '#### $1') + .replace(/
$/, '---') + .replace(/
/, '---\n') + .replace(/^

(.*)<\/h1>\s*(.*)$/g, '# $1\n$2') + .replace(/^

(.*)<\/h2>\s*(.*)$/g, '## $1\n$2') + .replace(/^

(.*)<\/h3>\s*(.*)$/g, '### $1\n$2') + .replace(/^

(.*)<\/h4>\s*(.*)$/g, '#### $1\n$2') + .replace(/^

(.*)<\/h5>\s*(.*)$/g, '##### $1\n$2') + .replace(/^
(.*)<\/h6>\s*(.*)$/g, '###### $1\n$2') .replace(/(.*)<\/b>/g, '*$1*') .replace(/(.*)<\/s>/g, '~$1~') - .replace(/(.*)<\/i>/g, '_$1_') }, + .replace(/(.*)<\/i>/g, '_$1_') + .replace(/
\s*/g, '\n') }, // serialization... json: function(node){ @@ -341,23 +347,73 @@ var Outline = { + elem.text .replace(/\n/g, '\n'+ level +' ') +'\n' + + (elem.collapsed ? + level+' ' + 'collapsed:: true\n' + : '') + this.text(elem.children || [], indent, level+indent) } return text }, + parse: function(text){ + text = ('\n' + text) + .split(/\n(\s*)- /g) + .slice(1) + var level = function(lst, prev_sep=undefined, parent=[]){ + while(lst.length > 0){ + sep = lst[0] + // deindent... + if(prev_sep != null + && sep.length < prev_sep.length){ + break } + prev_sep ??= sep + // same level... + if(sep.length == prev_sep.length){ + var [_, block] = lst.splice(0, 2) + var collapsed = false + block = block + .replace(/\n\s*collapsed::\s*(.*)\s*$/, + function(_, value){ + collapsed = value == 'true' + return '' }) + parent.push({ + text: block, + collapsed, + children: [], + }) + // indent... + } else { + parent.at(-1).children = level(lst, sep) } } + return parent } + return level(text) }, + + // XXX revise name... + Block: function(data={}, place=null){ + if(typeof(data) != 'object'){ + place = data + data = {} } + var block = document.createElement('div') + block.setAttribute('tabindex', '0') + data.collapsed + && block.setAttribute('collapsed', '') + var text + var html + block.append( + text = document.createElement('textarea') + .autoUpdateSize(), + html = document.createElement('span')) + if(data.text){ + text.value = data.text + html.innerHTML = this.__code2html__ ? + this.__code2html__(data.text) + : data.text } + var cur = this.get() + place && cur + && cur[place](block) + return block }, // XXX use .__code2html__(..) load: function(data){ - // text... - if(typeof(data) == 'string'){ - // XXX - data = data.split(/\n(\s*)- /g) - var level = function(lst){ - while(lst.length > 0){ - } - } - } - // json... - // XXX - + data = typeof(data) == 'string' ? + this.parse(data) + : data // generate dom... // XXX return this }, @@ -372,6 +428,8 @@ var Outline = { if(edited){ if(!atLine(edited, 0)){ return } + /*/ + //*/ state = 'edited' } evt.preventDefault() this.get(state, -1)?.focus() }, @@ -381,16 +439,22 @@ var Outline = { if(edited){ if(!atLine(edited, -1)){ return } - //window.getSelection() state = 'edited' } evt.preventDefault() this.get(state, 1)?.focus() }, // horizontal navigation / collapse... - // XXX if at start/end of element move to prev/next... ArrowLeft: function(evt){ - if(this.outline.querySelector('textarea:focus')){ - // XXX if at end of element move to next... + var edited = this.get('edited') + if(edited){ + // move caret to prev element... + if(edited.selectionStart == edited.selectionEnd + && edited.selectionStart == 0){ + evt.preventDefault() + edited = this.get('edited', 'prev') + edited.focus() + edited.selectionStart = + edited.selectionEnd = edited.value.length + 1 } return } ;((this.left_key_collapses || evt.shiftKey) @@ -399,8 +463,16 @@ var Outline = { this.toggleCollapse(true) : this.get('parent')?.focus() }, ArrowRight: function(evt){ - if(this.outline.querySelector('textarea:focus')){ - // XXX if at end of element move to next... + var edited = this.get('edited') + if(edited){ + // move caret to next element... + if(edited.selectionStart == edited.selectionEnd + && edited.selectionStart == edited.value.length){ + evt.preventDefault() + edited = this.get('edited', 'next') + edited.focus() + edited.selectionStart = + edited.selectionEnd = 0 } return } if(this.right_key_expands){ this.toggleCollapse(false) @@ -499,10 +571,13 @@ var Outline = { var node = evt.target if(node.nodeName == 'TEXTAREA' && node?.nextElementSibling?.nodeName == 'SPAN'){ - node.nextElementSibling.innerHTML = - that.__code2html__ ? - that.__code2html__(node.value) - : node.value } }) + if(that.__code2html__){ + var data = that.__code2html__(node.value) + node.nextElementSibling.innerHTML = data.text + data.collapsed + && node.parentElement.setAttribute('collapsed', '') + } else { + node.nextElementSibling.innerHTML = node.value } } }) // toolbar... var toolbar = this.toolbar diff --git a/experiments/outline-editor/generic.js b/experiments/outline-editor/generic.js index 437432e..71d182a 100755 --- a/experiments/outline-editor/generic.js +++ b/experiments/outline-editor/generic.js @@ -15,20 +15,52 @@ HTMLTextAreaElement.prototype.autoUpdateSize = function(){ that.updateSize() }) return this } +// calculate number of lines in text area (both wrapped and actual lines) +Object.defineProperty(HTMLTextAreaElement.prototype, 'heightLines', { + enumerable: false, + get: function(){ + var style = getComputedStyle(this) + return Math.floor( + (this.scrollHeight + - parseFloat(style.paddingTop) + - parseFloat(style.paddingBottom)) + / (parseFloat(style.lineHeight) + || parseFloat(style.fontSize))) }, }) +Object.defineProperty(HTMLTextAreaElement.prototype, 'lines', { + enumerable: false, + get: function(){ + return this.value + .split(/\n/g) + .length }, }) +// XXX this does not account for wrapping... Object.defineProperty(HTMLTextAreaElement.prototype, 'caretLine', { enumerable: false, get: function(){ var offset = this.selectionStart - console.log('---', this) return offset != null ? this.value .slice(0, offset) .split(/\n/g) .length - : undefined }, + : undefined }, }) + + +Object.defineProperty(HTMLTextAreaElement.prototype, 'caretOffset', { + enumerable: false, + get: function(){ + var offset = this.selectionStart + var r = document.createRange() + r.setStart(this, offset) + r.setEnd(this, offset) + var rect = r.getBoundingClientRect() + return { + top: rect.top, + left: rect.left, + } }, }) + /********************************************************************** * vim:set ts=4 sw=4 : */ diff --git a/experiments/outline-editor/index.html b/experiments/outline-editor/index.html index 76716ad..cccc765 100755 --- a/experiments/outline-editor/index.html +++ b/experiments/outline-editor/index.html @@ -69,6 +69,10 @@ var setup = function(){
 TODO:
+- caret
+  - go to next/prev element's start/end when moving off last/first char
+  - handle up/down on wrapped blocks
+    ...can't seem to get caret line in a non-hacky way
 - persistent empty first/last node (a button to create a new node)
 - loading from DOM -- fill textarea
 - Firefox compatibility -- remove ':has(..)'