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... */
.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;

View File

@ -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([<offset>])
// .get('focused'[, <offset>])
@ -287,28 +274,47 @@ var Outline = {
return this },
// block serialization...
// XXX these should be symetrical...
__code2html__: function(code){
return code
.replace(/\n\s*/g, '<br>')
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, '<h1>$1</h1>')
.replace(/^## (.*)\s*$/g, '<h2>$1</h2>')
.replace(/^### (.*)\s*$/g, '<h3>$1</h3>')
.replace(/^#### (.*)\s*$/g, '<h4>$1</h4>')
.replace(/^#\s*(.*)\s*(\n|$)/, '<h1>$1</h1>')
.replace(/^##\s*(.*)\s*(\n|$)/, '<h2>$1</h2>')
.replace(/^###\s*(.*)\s*(\n|$)/, '<h3>$1</h3>')
.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, '<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){
return html
.replace(/<br>\s*/g, '\n')
// XXX STUB...
.replace(/^<h1>(.*)<\/h1>\s*$/g, '# $1')
.replace(/^<h2>(.*)<\/h2>\s*$/g, '## $1')
.replace(/^<h3>(.*)<\/h3>\s*$/g, '### $1')
.replace(/^<h4>(.*)<\/h4>\s*$/g, '#### $1')
.replace(/<hr>$/, '---')
.replace(/<hr>/, '---\n')
.replace(/^<h1>(.*)<\/h1>\s*(.*)$/g, '# $1\n$2')
.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(/<s>(.*)<\/s>/g, '~$1~')
.replace(/<i>(.*)<\/i>/g, '_$1_') },
.replace(/<i>(.*)<\/i>/g, '_$1_')
.replace(/<br>\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

View File

@ -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 : */

View File

@ -69,6 +69,10 @@ var setup = function(){
<pre>
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)
- loading from DOM -- fill textarea
- Firefox compatibility -- remove ':has(..)'