refactoring...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2023-10-14 02:42:29 +03:00
parent 34105a355c
commit 9a1d851112
3 changed files with 349 additions and 188 deletions

View File

@ -465,6 +465,11 @@ editor .outline .block:focus {
-1 * var(--checkbox-size)
- var(--checkbox-margin));
}
/* status... */
.editor .outline .block>.view .completion[completion]:before {
content: "(" attr(completion) ")";
color: gray;
}
/*---------------------------------------------------------- Code ---*/

View File

@ -27,37 +27,106 @@ var atLine = function(elem, index){
//---------------------------------------------------------------------
var codeBlock = {
// can be used in:
// <string>.replace(codeBlock.pattern, codeBlock.handler)
// or:
// codeBlock
pattern: /(?<!\\)```(.*\s*\n)((\n|.)*?)\h*(?<!\\)```/g,
handler: function(_, language, code){
var quote = this?.quote
|| codeBlock.quote
language = language.trim()
language = language ?
'language-'+language
: language
return `<pre>`
+`<code contenteditable="true" class="${language}">${
quote ?
quote(code)
: code
}</code>`
+`</pre>` },
var plugin = {
// XXX make this more generic...
style: function(editor, elem, style, code=undefined){
style = [style].flat()
editor.__styles = [...new Set([
...(editor.__styles ?? []),
...style,
])]
return function(_, text){
elem.style ??= []
elem.style.push(...style)
return code
?? text } },
}
quote: function(text){
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var attributes = {
__proto__: plugin,
__pre_parse__: function(text, editor, elem){
return text
// hidden attributes...
// XXX make this generic...
// collapsed...
.replace(/(\n|^)\s*collapsed::\s*(.*)\s*(\n|$)/,
function(_, value){
elem.collapsed = value.trim() == 'true'
return '' })
// id...
.replace(/(\n|^)\s*id::\s*(.*)\s*(\n|$)/,
function(_, value){
elem.id = value.trim()
return '' }) },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var blocks = {
__proto__: plugin,
__pre_parse__: function(text, editor, elem){
return text
// markdown...
// style: headings...
.replace(/^(?<!\\)######\s+(.*)$/m, this.style(editor, elem, 'heading-6'))
.replace(/^(?<!\\)#####\s+(.*)$/m, this.style(editor, elem, 'heading-5'))
.replace(/^(?<!\\)####\s+(.*)$/m, this.style(editor, elem, 'heading-4'))
.replace(/^(?<!\\)###\s+(.*)$/m, this.style(editor, elem, 'heading-3'))
.replace(/^(?<!\\)##\s+(.*)$/m, this.style(editor, elem, 'heading-2'))
.replace(/^(?<!\\)#\s+(.*)$/m, this.style(editor, elem, 'heading-1'))
// style: list...
//.replace(/^(?<!\\)[-\*]\s+(.*)$/m, style('list-item'))
.replace(/^\s*(.*)(?<!\\):\s*$/m, this.style(editor, elem, 'list'))
.replace(/^\s*(.*)(?<!\\)#\s*$/m, this.style(editor, elem, 'numbered-list'))
// style: misc...
.replace(/^\s*(?<!\\)>\s+(.*)$/m, this.style(editor, elem, 'quote'))
.replace(/^\s*(?<!\\)((\/\/|;)\s+.*)$/m, this.style(editor, elem, 'comment'))
.replace(/^\s*(?<!\\)NOTE:?\s*(.*)$/m, this.style(editor, elem, 'NOTE'))
.replace(/^\s*(?<!\\)XXX\s+(.*)$/m, this.style(editor, elem, 'XXX'))
.replace(/^(.*)\s*(?<!\\)XXX$/m, this.style(editor, elem, 'XXX')) } ,
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX add actions...
var quoted = {
__proto__: plugin,
encode: function(text){
return text
.replace(/(?<!\\)&/g, '&amp;')
.replace(/(?<!\\)</g, '&lt;')
.replace(/(?<!\\)>/g, '&gt;')
.replace(/\\(?!`)/g, '\\\\') },
map: function(text, func){
return text.replace(this.pattern, func) },
// can be used in:
// <string>.replace(quoted.pattern, quoted.handler)
quote_pattern: /(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm,
quote: function(_, code){
return `<code>${ this.encode(code) }</code>` },
pre_pattern: /(?<!\\)```(.*\s*\n)((\n|.)*?)\h*(?<!\\)```/g,
pre: function(_, language, code){
language = language.trim()
language = language ?
'language-'+language
: language
return `<pre>`
+`<code contenteditable="true" class="${language}">${
this.encode(code)
}</code>`
+`</pre>` },
map: function(text, func){
return text.replace(this.pre_pattern, func) },
replace: function(text, index, updated){
return this.map(text,
function(match, language, code){
@ -68,9 +137,169 @@ var codeBlock = {
updated(code)
: updated)
+'```') }) },
toHTML: function(text){
return this.map(text, this.handler) },
__pre_parse__: function(text, editor, elem){
return text
.replace(this.pre_pattern, this.pre.bind(this))
.replace(this.quote_pattern, this.quote.bind(this)) },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX Hackish...
var syntax = {
__proto__: plugin,
update: function(){
window.hljs
&& hljs.highlightAll()
return this },
__setup__: function(editor){
return this.update() },
// XXX make a local update...
__changed__: function(editor, node){
return this.update() },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var tables = {
__proto__: plugin,
__parse__: function(text, editor, elem){
return text
.replace(/^\s*(?<!\\)\|\s*((.|\n)*)\s*\|\s*$/,
function(_, body){
return `<table><tr><td>${
body
.replace(/\s*\|\s*\n\s*\|\s*/gm, '</td></tr>\n<tr><td>')
.replace(/\s*\|\s*/gm, '</td><td>')
}</td></td></table>` }) },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var styling = {
__proto__: plugin,
__parse__: function(text, editor, elem){
return text
// markers...
.replace(/(\s*)(?<!\\)(FEATURE:|Q:|Question:|Note:)(\s*)/gm,
'$1<b class="$2">$2</b>$3')
.replace(/(\s*)(?<!\\)(ASAP|BUG|FIX|HACK|STUB|WARNING|CAUTION)(\s*)/gm,
'$1<span class="highlight $2">$2</span>$3')
// elements...
.replace(/(\n|^)(?<!\\)---*\h*(\n|$)/m, '$1<hr>')
// basic styling...
// XXX revise...
.replace(/(?<!\\)\*(?=[^\s*])(([^*]|\\\*)*[^\s*])(?<!\\)\*/gm, '<b>$1</b>')
.replace(/(?<!\\)~(?=[^\s~])(([^~]|\\~)*[^\s~])(?<!\\)~/gm, '<s>$1</s>')
.replace(/(?<!\\)_(?=[^\s_])(([^_]|\\_)*[^\s_])(?<!\\)_/gm, '<i>$1</i>')
// code/quoting...
//.replace(/(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm, quote)
// XXX support "\==" in mark...
.replace(/(?<!\\)==(?=[^\s])(.*[^\s])(?<!\\)==/gm, '<mark>$1</mark>')
// links...
.replace(/(?<!\\)\[([^\]]*)\]\(([^)]*)\)/g, '<a href="$2">$1</a>')
.replace(/((?:https?:|ftps?:)[^\s]*)(\s*)/g, '<a href="$1">$1</a>$2') },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX use ligatures for these???
var symbols = {
__proto__: plugin,
__parse__: function(text, editor, elem){
return text
// characters...
.replace(/(?<!\\)\(i\)/gm, '🛈')
.replace(/(?<!\\)\(c\)/gm, '©')
.replace(/(?<!\\)\/!\\/gm, '⚠')
.replace(/(?<!\\)---(?!-)/gm, '&mdash;')
.replace(/(?<!\\)--(?!-)/gm, '&ndash;') },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var escaping = {
__proto__: plugin,
__post_parse__: function(text, editor, elem){
return text
// quoting...
// NOTE: this must be last...
.replace(/(?<!\\)\\(.)/gm, '$1') },
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX add actions...
var tasks = {
__proto__: plugin,
updateStatus: function(editor, node){
node = editor.get(node)
if(node == null){
return this }
var state = node
.querySelector('.view')
.querySelector('.completion')
if(state){
var c =
((node.querySelectorAll('input[type=checkbox]:checked').length
/ node.querySelectorAll('input[type=checkbox]').length)
* 100)
.toFixed(0)
!isNaN(c)
&& state.setAttribute('completion', c +'%') }
return this },
updateBranch: function(editor, node){
if(!node){
return this }
var outline = editor.outline
var p = node
while(p !== outline){
this.updateStatus(editor, p)
p = editor.get(p, 'parent') }
return this },
updateAll: function(editor){
for(var e of [...editor.outline.querySelectorAll('.block>.view .completion')]){
this.updateStatus(editor, e) }
return this },
__setup__: function(editor){
return this.updateAll(editor) },
__changed__: function(editor, node){
return this.updateBranch(editor, node) },
__parse__: function(text, editor, elem){
return text
// block checkboxes...
// NOTE: these are separate as we need to align block text
// to leading chekbox...
.replace(/^\s*(?<!\\)\[[_ ]\]\s*/m,
this.style(editor, elem, 'todo', '<input type="checkbox">'))
.replace(/^\s*(?<!\\)\[[Xx]\]\s*/m,
this.style(editor, elem, 'todo', '<input type="checkbox" checked>'))
// inline checkboxes...
.replace(/\s*(?<!\\)\[[_ ]\]\s*/gm,
this.style(editor, elem, 'check', '<input type="checkbox">'))
.replace(/\s*(?<!\\)\[[Xx]\]\s*/gm,
this.style(editor, elem, 'check', '<input type="checkbox" checked>'))
// completion...
// XXX add support for being like a todo checkbox...
.replace(/(?<!\\)\[[%]\]/gm, '<span class="completion"></span>') },
}
@ -91,6 +320,32 @@ var Outline = {
carot_jump_edge_then_block: false,
plugins: [
attributes,
blocks,
quoted,
styling,
tables,
symbols,
syntax,
tasks,
// keep this last...
// XXX revise -- should this be external???
escaping,
],
runPlugins: function(method, ...args){
for(var plugin of this.plugins){
method in plugin
&& plugin[method](...args) }
return this },
threadPlugins: function(method, value, ...args){
for(var plugin of this.plugins){
method in plugin
&& (value = plugin[method](value, ...args)) }
return value },
get code(){
return this.dom.querySelector('.code') },
get outline(){
@ -363,154 +618,49 @@ var Outline = {
var elem = {
collapsed: false,
}
// only whitespace -> keep element blank...
if(code.trim() == ''){
elem.text = ''
return elem }
// helpers...
var style = function(style, code=undefined){
style = [style].flat()
that.__styles = [...new Set([
...(that.__styles ?? []),
...style,
])]
return function(_, text){
elem.style ??= []
elem.style.push(...style)
return code
?? text } }
var quoteText = function(text){
return text
.replace(/(?<!\\)&/g, '&amp;')
.replace(/(?<!\\)</g, '&lt;')
.replace(/(?<!\\)>/g, '&gt;')
.replace(/\\(?!`)/g, '\\\\') }
var quote = function(_, code){
return `<code>${quoteText(code)}</code>` }
var table = function(_, body){
return `<table><tr><td>${
body
.replace(/\s*\|\s*\n\s*\|\s*/gm, '</td></tr>\n<tr><td>')
.replace(/\s*\|\s*/gm, '</td><td>')
}</td></td></table>` }
var run = function(stage, text){
var meth = {
pre: '__pre_parse__',
main: '__parse__',
post: '__post_parse__',
}[stage]
return that.threadPlugins(meth, text, that, elem) }
var preParse = function(text){
return text
// hidden attributes...
// XXX make this generic...
// collapsed...
.replace(/(\n|^)\s*collapsed::\s*(.*)\s*(\n|$)/,
function(_, value){
elem.collapsed = value.trim() == 'true'
return '' })
// id...
.replace(/(\n|^)\s*id::\s*(.*)\s*(\n|$)/,
function(_, value){
elem.id = value.trim()
return '' }) }
var blockParse = function(text){
return text
// markdown...
// style: headings...
.replace(/^(?<!\\)######\s+(.*)$/m, style('heading-6'))
.replace(/^(?<!\\)#####\s+(.*)$/m, style('heading-5'))
.replace(/^(?<!\\)####\s+(.*)$/m, style('heading-4'))
.replace(/^(?<!\\)###\s+(.*)$/m, style('heading-3'))
.replace(/^(?<!\\)##\s+(.*)$/m, style('heading-2'))
.replace(/^(?<!\\)#\s+(.*)$/m, style('heading-1'))
// style: list...
//.replace(/^(?<!\\)[-\*]\s+(.*)$/m, style('list-item'))
.replace(/^\s*(.*)(?<!\\):\s*$/m, style('list'))
.replace(/^\s*(.*)(?<!\\)#\s*$/m, style('numbered-list'))
// style: misc...
.replace(/^\s*(?<!\\)>\s+(.*)$/m, style('quote'))
.replace(/^\s*(?<!\\)((\/\/|;)\s+.*)$/m, style('comment'))
.replace(/^\s*(?<!\\)NOTE:?\s*(.*)$/m, style('NOTE'))
.replace(/^\s*(?<!\\)XXX\s+(.*)$/m, style('XXX'))
.replace(/^(.*)\s*(?<!\\)XXX$/m, style('XXX')) }
var quoteParse = function(text){
return text
.replace(codeBlock.pattern, codeBlock.handler)
.replace(/(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm, quote) }
var inlineParse = function(text){
return text
.replace(/(\s*)(?<!\\)(FEATURE:|Q:|Question:|Note:)(\s*)/gm,
'$1<b class="$2">$2</b>$3')
.replace(/(\s*)(?<!\\)(ASAP|BUG|FIX|HACK|STUB|WARNING|CAUTION)(\s*)/gm,
'$1<span class="highlight $2">$2</span>$3')
// elements...
.replace(/(\n|^)(?<!\\)---*\h*(\n|$)/m, '$1<hr>')
// ToDo...
// NOTE: these are separate as we need to align block text
// to leading chekbox...
.replace(/^\s*(?<!\\)\[[_ ]\]\s*/m,
style('todo', '<input type="checkbox">'))
.replace(/^\s*(?<!\\)\[[Xx]\]\s*/m,
style('todo', '<input type="checkbox" checked>'))
// inline checkboxes...
.replace(/\s*(?<!\\)\[[_ ]\]\s*/gm,
style('check', '<input type="checkbox">'))
.replace(/\s*(?<!\\)\[[Xx]\]\s*/gm,
style('check', '<input type="checkbox" checked>'))
// tables...
.replace(/^\s*(?<!\\)\|\s*((.|\n)*)\s*\|\s*$/, table)
// basic styling...
// XXX revise...
.replace(/(?<!\\)\*(?=[^\s*])(([^*]|\\\*)*[^\s*])(?<!\\)\*/gm, '<b>$1</b>')
.replace(/(?<!\\)~(?=[^\s~])(([^~]|\\~)*[^\s~])(?<!\\)~/gm, '<s>$1</s>')
.replace(/(?<!\\)_(?=[^\s_])(([^_]|\\_)*[^\s_])(?<!\\)_/gm, '<i>$1</i>')
// code/quoting...
//.replace(/(?<!\\)`(?=[^\s])(([^`]|\\`)*[^\s])(?<!\\)`/gm, quote)
// XXX support "\==" in mark...
.replace(/(?<!\\)==(?=[^\s])(.*[^\s])(?<!\\)==/gm, '<mark>$1</mark>')
// links...
.replace(/(?<!\\)\[([^\]]*)\]\(([^)]*)\)/g, '<a href="$2">$1</a>')
.replace(/((?:https?:|ftps?:)[^\s]*)(\s*)/g, '<a href="$1">$1</a>$2')
// characters...
// XXX use ligatures for these???
.replace(/(?<!\\)\(i\)/gm, '🛈')
.replace(/(?<!\\)\(c\)/gm, '©')
.replace(/(?<!\\)\/!\\/gm, '⚠')
.replace(/(?<!\\)---(?!-)/gm, '&mdash;')
.replace(/(?<!\\)--(?!-)/gm, '&ndash;') }
var postParse = function(text){
return text
// quoting...
// NOTE: this must be last...
.replace(/(?<!\\)\\(.)/gm, '$1') }
var parse = function(text){
// split text into parsable and non-parsable sections...
// stage: pre...
var text = run('pre',
// pre-sanitize...
code.replace(/\x00/g, ''))
// split text into parsable and non-parsable sections...
var sections = text
// split fomat:
// [ text <match> <type> <body>, ... ]
var pattern = /(<(pre|code)(?:|\s[^>]*)>((?:\n|.)*)<\/\2>)/g
var sections =
quoteParse(
blockParse(
preParse(text
.replace(/\x00/g, ''))))
.split(pattern)
// sort out the sections...
var parsable = []
var quoted = []
while(sections.length > 0){
var [section, match] = sections.splice(0, 4)
parsable.push(section)
quoted.push(match) }
// parse only the parsable sections...
return postParse(
inlineParse(
parsable
.join('\x00'))
.split(/\x00/g)
.map(function(section){
return [section, quoted.shift() ?? ''] })
.flat()
.join('')) }
elem.text = parse(code)
.split(/(<(pre|code)(?:|\s[^>]*)>((?:\n|.)*)<\/\2>)/g)
// sort out the sections...
var parsable = []
var quoted = []
while(sections.length > 0){
var [section, match] = sections.splice(0, 4)
parsable.push(section)
quoted.push(match) }
// stage: main...
text = run('main',
// parse only the parsable sections...
parsable.join('\x00'))
.split(/\x00/g)
// merge the quoted sections back in...
.map(function(section){
return [section, quoted.shift() ?? ''] })
.flat()
.join('')
// stage: post...
elem.text = run('post', text)
return elem },
// XXX essentially here we need to remove service stuff like some
@ -835,6 +985,7 @@ var Outline = {
function(evt){
var elem = evt.target
// prevent focusing parent by clicking between blocks...
if(elem.classList.contains('children')){
return }
@ -882,7 +1033,9 @@ var Outline = {
return i-- == 0 ?
to
: m }
text.value = text.value.replace(/\[[Xx_]\]/g, toggle) } })
text.value = text.value.replace(/\[[Xx_]\]/g, toggle)
// update status...
tasks.updateBranch(that, node) } })
// keyboard handling...
outline.addEventListener('keydown',
function(evt){
@ -918,7 +1071,7 @@ var Outline = {
.querySelectorAll('.view code[contenteditable=true]')]
.indexOf(elem)
// update element content...
code.value = codeBlock.replace(code.value, i, update)
code.value = quoted.replace(code.value, i, update)
return } })
// toggle view/code of nodes...
@ -951,12 +1104,10 @@ var Outline = {
if(node.nodeName == 'TEXTAREA'
&& node?.nextElementSibling?.nodeName == 'SPAN'){
var block = node.parentElement
that.update(block, { text: node.value }) }
that.update(block, { text: node.value })
// XXX do a plugin...
window.hljs
&& hljs.highlightAll()
})
that.runPlugins('__changed__', that, node)
} })
// update .code...
var update_code_timeout
outline.addEventListener('change',
@ -997,9 +1148,7 @@ var Outline = {
.replace(/&gt;/g, '>'))
console.log(`Parse: ${Date.now() - t}ms`)}
// XXX do a plugin...
window.hljs
&& hljs.highlightAll()
this.runPlugins('__setup__', this)
return this },
}

View File

@ -57,8 +57,6 @@ var setup = function(){
- ASAP: editor: shifting nodes up/down
- ASAP: scroll into view is bad...
- ASAP: need to reach checkboxes via keyboard
- FEATURE: "percentage complete" in parent blocks with todo's nested
- _...use `[%]` (preferred), `%%`, or something similar..._
- FEATURE: read-only mode
- FEATURE: `collapse-children:: true` block option -- when loading collapse all immediate children
- FF: figure out a way to draw expand/collapse bullets without the use of CSS' `:has(..)`
@ -67,7 +65,7 @@ var setup = function(){
code
```
- _bullet should be either in the middle of the block or at the first line of code (preferred)..._
- custom element...
- editor as a custom element...
- Nerd fonts (option???)
- multiple node selection
- copy/paste nodes/trees
@ -76,6 +74,13 @@ var setup = function(){
- delete node
- indent/deindent
- edit node
- FEATURE? block templates...
collapsed:: true
- something like: `TPL: [_] <editable/> -- <editable/>`
- `TPL:` -- template marker
- `<editable/>` -- field marker
- each child node will copy the template and allow editing of only fields
- not clear how to handle template changes...
- Q: can we get the caret line in a textarea???
- _...this will fix a lot of issues with moving between blocks in edit mode..._
- Q: do we use \\t for indent? (option???)
@ -96,8 +101,9 @@ var setup = function(){
* list item
block text
- NOTE: this is only a problem if making list-items manually -- disable???
- ~Q: can we edit code in a code block directly? (a-la Logseq)~
- empty item height is a bit off...
- ~Q: can we edit code in a code block directly? (a-la Logseq)~
- ~"percentage complete" in parent blocks with todo's nested~
- ~`.editor .outline:empty` view and behavior...~
- ~editor: semi-live update styles~
- ~do a better expand/collapse icons~
@ -113,11 +119,11 @@ var setup = function(){
- ~add optional text styling to nodes~
-
- ## Refactoring:
- Plugin architecture
- Item parser (`.__code2html__(..)`)
- split out
- define api
- define a way to extend/stack parsers
_...add wikiwords, ..._
- ~split out~
- ~define~/doc api
- ~define a way to extend/stack parsers~
- Format parser/generator
- split out
- define api
@ -193,12 +199,13 @@ var setup = function(){
- ---
- Markers: ASAP, BUG, FIX, HACK, STUB, WARNING, and CAUTION
- Basic task management
- Inline [X] checkboxes [_]
- To do items/blocks
- [_] undone item
_(clicking the checkbox updates the item)_
- [X] done item
- [_] we can also add inline [x] checkboxes
- [%] Completion status
- Inline [X] checkboxes [_]
- To do items/blocks
- [_] undone item
_(clicking the checkbox updates the item)_
- [X] done item
- [_] we can also add inline [x] checkboxes and states: [%]
- links
- [example](about:blank)
- https://example.com