logseq-like format parsing + markdown (stub) + ff support...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2023-10-07 07:04:58 +03:00
parent 697a9d8347
commit af3f10e35b
4 changed files with 161 additions and 57 deletions

View File

@ -52,13 +52,6 @@
} }
/* show/hide node's view/code... */ /* 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:focus+span,
.editor .outline [tabindex]>textarea:not(:focus) { .editor .outline [tabindex]>textarea:not(:focus) {
position: absolute; position: absolute;

View File

@ -82,19 +82,6 @@ var Outline = {
return this.dom.querySelector('.toolbar') }, 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([<offset>]) // .get([<offset>])
// .get('focused'[, <offset>]) // .get('focused'[, <offset>])
@ -287,28 +274,47 @@ var Outline = {
return this }, return this },
// block serialization... // block serialization...
// XXX these should be symetrical...
__code2html__: function(code){ __code2html__: function(code){
return code var elem = {
.replace(/\n\s*/g, '<br>') 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... // XXX STUB...
.replace(/^# (.*)\s*$/g, '<h1>$1</h1>') .replace(/^#\s*(.*)\s*(\n|$)/, '<h1>$1</h1>')
.replace(/^## (.*)\s*$/g, '<h2>$1</h2>') .replace(/^##\s*(.*)\s*(\n|$)/, '<h2>$1</h2>')
.replace(/^### (.*)\s*$/g, '<h3>$1</h3>') .replace(/^###\s*(.*)\s*(\n|$)/, '<h3>$1</h3>')
.replace(/^#### (.*)\s*$/g, '<h4>$1</h4>') .replace(/^####\s*(.*)\s*(\n|$)/, '<h4>$1</h4>')
.replace(/^#####\s*(.*)\s*(\n|$)/, '<h5>$1</h5>')
.replace(/^######\s*(.*)\s*(\n|$)/, '<h6>$1</h6>')
.replace(/\*(.*)\*/g, '<b>$1</b>') .replace(/\*(.*)\*/g, '<b>$1</b>')
.replace(/~([^~]*)~/g, '<s>$1</s>') .replace(/~([^~]*)~/g, '<s>$1</s>')
.replace(/_([^_]*)_/g, '<i>$1</i>') }, .replace(/_([^_]*)_/g, '<i>$1</i>')
.replace(/(\n|^)---*\h*(\n|$)/, '$1<hr>')
.replace(/\n/g, '<br>\n')
return elem },
__html2code__: function(html){ __html2code__: function(html){
return html return html
.replace(/<br>\s*/g, '\n')
// XXX STUB... // XXX STUB...
.replace(/^<h1>(.*)<\/h1>\s*$/g, '# $1') .replace(/<hr>$/, '---')
.replace(/^<h2>(.*)<\/h2>\s*$/g, '## $1') .replace(/<hr>/, '---\n')
.replace(/^<h3>(.*)<\/h3>\s*$/g, '### $1') .replace(/^<h1>(.*)<\/h1>\s*(.*)$/g, '# $1\n$2')
.replace(/^<h4>(.*)<\/h4>\s*$/g, '#### $1') .replace(/^<h2>(.*)<\/h2>\s*(.*)$/g, '## $1\n$2')
.replace(/^<h3>(.*)<\/h3>\s*(.*)$/g, '### $1\n$2')
.replace(/^<h4>(.*)<\/h4>\s*(.*)$/g, '#### $1\n$2')
.replace(/^<h5>(.*)<\/h5>\s*(.*)$/g, '##### $1\n$2')
.replace(/^<h6>(.*)<\/h6>\s*(.*)$/g, '###### $1\n$2')
.replace(/<b>(.*)<\/b>/g, '*$1*') .replace(/<b>(.*)<\/b>/g, '*$1*')
.replace(/<s>(.*)<\/s>/g, '~$1~') .replace(/<s>(.*)<\/s>/g, '~$1~')
.replace(/<i>(.*)<\/i>/g, '_$1_') }, .replace(/<i>(.*)<\/i>/g, '_$1_')
.replace(/<br>\s*/g, '\n') },
// serialization... // serialization...
json: function(node){ json: function(node){
@ -341,23 +347,73 @@ var Outline = {
+ elem.text + elem.text
.replace(/\n/g, '\n'+ level +' ') .replace(/\n/g, '\n'+ level +' ')
+'\n' +'\n'
+ (elem.collapsed ?
level+' ' + 'collapsed:: true\n'
: '')
+ this.text(elem.children || [], indent, level+indent) } + this.text(elem.children || [], indent, level+indent) }
return text }, 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__(..) // XXX use .__code2html__(..)
load: function(data){ load: function(data){
// text... data = typeof(data) == 'string' ?
if(typeof(data) == 'string'){ this.parse(data)
// XXX : data
data = data.split(/\n(\s*)- /g)
var level = function(lst){
while(lst.length > 0){
}
}
}
// json...
// XXX
// generate dom... // generate dom...
// XXX // XXX
return this }, return this },
@ -372,6 +428,8 @@ var Outline = {
if(edited){ if(edited){
if(!atLine(edited, 0)){ if(!atLine(edited, 0)){
return } return }
/*/
//*/
state = 'edited' } state = 'edited' }
evt.preventDefault() evt.preventDefault()
this.get(state, -1)?.focus() }, this.get(state, -1)?.focus() },
@ -381,16 +439,22 @@ var Outline = {
if(edited){ if(edited){
if(!atLine(edited, -1)){ if(!atLine(edited, -1)){
return } return }
//window.getSelection()
state = 'edited' } state = 'edited' }
evt.preventDefault() evt.preventDefault()
this.get(state, 1)?.focus() }, this.get(state, 1)?.focus() },
// horizontal navigation / collapse... // horizontal navigation / collapse...
// XXX if at start/end of element move to prev/next...
ArrowLeft: function(evt){ ArrowLeft: function(evt){
if(this.outline.querySelector('textarea:focus')){ var edited = this.get('edited')
// XXX if at end of element move to next... 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 } return }
;((this.left_key_collapses ;((this.left_key_collapses
|| evt.shiftKey) || evt.shiftKey)
@ -399,8 +463,16 @@ var Outline = {
this.toggleCollapse(true) this.toggleCollapse(true)
: this.get('parent')?.focus() }, : this.get('parent')?.focus() },
ArrowRight: function(evt){ ArrowRight: function(evt){
if(this.outline.querySelector('textarea:focus')){ var edited = this.get('edited')
// XXX if at end of element move to next... 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 } return }
if(this.right_key_expands){ if(this.right_key_expands){
this.toggleCollapse(false) this.toggleCollapse(false)
@ -499,10 +571,13 @@ var Outline = {
var node = evt.target var node = evt.target
if(node.nodeName == 'TEXTAREA' if(node.nodeName == 'TEXTAREA'
&& node?.nextElementSibling?.nodeName == 'SPAN'){ && node?.nextElementSibling?.nodeName == 'SPAN'){
node.nextElementSibling.innerHTML = if(that.__code2html__){
that.__code2html__ ? var data = that.__code2html__(node.value)
that.__code2html__(node.value) node.nextElementSibling.innerHTML = data.text
: node.value } }) data.collapsed
&& node.parentElement.setAttribute('collapsed', '')
} else {
node.nextElementSibling.innerHTML = node.value } } })
// toolbar... // toolbar...
var toolbar = this.toolbar var toolbar = this.toolbar

View File

@ -15,20 +15,52 @@ HTMLTextAreaElement.prototype.autoUpdateSize = function(){
that.updateSize() }) that.updateSize() })
return this } 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', { Object.defineProperty(HTMLTextAreaElement.prototype, 'caretLine', {
enumerable: false, enumerable: false,
get: function(){ get: function(){
var offset = this.selectionStart var offset = this.selectionStart
console.log('---', this)
return offset != null ? return offset != null ?
this.value this.value
.slice(0, offset) .slice(0, offset)
.split(/\n/g) .split(/\n/g)
.length .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 : */ * vim:set ts=4 sw=4 : */

View File

@ -69,6 +69,10 @@ var setup = function(){
<pre> <pre>
TODO: TODO:
- caret
- <s>go to next/prev element's start/end when moving off last/first char</s>
- handle up/down on wrapped blocks
<i>...can't seem to get caret line in a non-hacky way</i>
- persistent empty first/last node (a button to create a new node) - persistent empty first/last node (a button to create a new node)
- loading from DOM -- fill textarea - loading from DOM -- fill textarea
- Firefox compatibility -- remove ':has(..)' - Firefox compatibility -- remove ':has(..)'