diff --git a/experiments/outline-editor/editor.css b/experiments/outline-editor/editor.css index f035604..126aa51 100755 --- a/experiments/outline-editor/editor.css +++ b/experiments/outline-editor/editor.css @@ -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 ---*/ diff --git a/experiments/outline-editor/editor.js b/experiments/outline-editor/editor.js index 1ae6492..f1fb0e3 100755 --- a/experiments/outline-editor/editor.js +++ b/experiments/outline-editor/editor.js @@ -27,37 +27,106 @@ var atLine = function(elem, index){ //--------------------------------------------------------------------- -var codeBlock = { - // can be used in: - // .replace(codeBlock.pattern, codeBlock.handler) - // or: - // codeBlock - pattern: /(?` - +`${ - quote ? - quote(code) - : code - }` - +`` }, +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, 'quote')) + .replace(/^\s*(?/g, '>') .replace(/\\(?!`)/g, '\\\\') }, - map: function(text, func){ - return text.replace(this.pattern, func) }, + // can be used in: + // .replace(quoted.pattern, quoted.handler) + quote_pattern: /(?${ this.encode(code) }` }, + pre_pattern: /(?` + +`${ + this.encode(code) + }` + +`` }, + + 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*(?${ + body + .replace(/\s*\|\s*\n\s*\|\s*/gm, '\n') + .replace(/\s*\|\s*/gm, '') + }` }) }, +} + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +var styling = { + __proto__: plugin, + + __parse__: function(text, editor, elem){ + return text + // markers... + .replace(/(\s*)(?$2$3') + .replace(/(\s*)(?$2$3') + // elements... + .replace(/(\n|^)(?') + // basic styling... + // XXX revise... + .replace(/(?$1') + .replace(/(?$1') + .replace(/(?$1') + // code/quoting... + //.replace(/(?$1') + // links... + .replace(/(?$1') + .replace(/((?:https?:|ftps?:)[^\s]*)(\s*)/g, '$1$2') }, +} + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// XXX use ligatures for these??? +var symbols = { + __proto__: plugin, + + __parse__: function(text, editor, elem){ + return text + // characters... + .replace(/(?.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*(?')) + .replace(/^\s*(?')) + // inline checkboxes... + .replace(/\s*(?')) + .replace(/\s*(?')) + // completion... + // XXX add support for being like a todo checkbox... + .replace(/(?') }, } @@ -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, '>') - .replace(/\\(?!`)/g, '\\\\') } - var quote = function(_, code){ - return `${quoteText(code)}` } - var table = function(_, body){ - return `\n
${ - body - .replace(/\s*\|\s*\n\s*\|\s*/gm, '
') - .replace(/\s*\|\s*/gm, '') - }
` } + 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('quote')) - .replace(/^\s*(?$2$3') - .replace(/(\s*)(?$2$3') - // elements... - .replace(/(\n|^)(?') - // ToDo... - // NOTE: these are separate as we need to align block text - // to leading chekbox... - .replace(/^\s*(?')) - .replace(/^\s*(?')) - // inline checkboxes... - .replace(/\s*(?')) - .replace(/\s*(?')) - // tables... - .replace(/^\s*(?$1') - .replace(/(?$1') - .replace(/(?$1') - // code/quoting... - //.replace(/(?$1') - // links... - .replace(/(?$1') - .replace(/((?:https?:|ftps?:)[^\s]*)(\s*)/g, '$1$2') - // characters... - // XXX use ligatures for these??? - .replace(/(? , ... ] - 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 }) } - - // XXX do a plugin... - window.hljs - && hljs.highlightAll() - }) + that.update(block, { text: node.value }) + + that.runPlugins('__changed__', that, node) + } }) // update .code... var update_code_timeout outline.addEventListener('change', @@ -997,9 +1148,7 @@ var Outline = { .replace(/>/g, '>')) console.log(`Parse: ${Date.now() - t}ms`)} - // XXX do a plugin... - window.hljs - && hljs.highlightAll() + this.runPlugins('__setup__', this) return this }, } diff --git a/experiments/outline-editor/index.html b/experiments/outline-editor/index.html index bc16cfa..c9df958 100755 --- a/experiments/outline-editor/index.html +++ b/experiments/outline-editor/index.html @@ -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: [_] -- ` + - `TPL:` -- template marker + - `` -- 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