diff --git a/experiments/outline-editor/editor.css b/experiments/outline-editor/editor.css index 889a402..729608b 100755 --- a/experiments/outline-editor/editor.css +++ b/experiments/outline-editor/editor.css @@ -688,6 +688,64 @@ } +/* XXX EXPERIMENTAL... +* - need to handle nested blocks somehow... +*/ +.editor .outline .block.table-2 { + + &>.view { + font-size: small; + color: rgba(0,0,0,0.4); + } + + &.focused { + background: rgba(0,0,0,0.05); + } + &:focus { + background: rgba(0,0,0,0.07); + } + + &>.children { + display: table !important; + width: 100%; + + &>.block { + display: table-row; + + &:first-child>.view td { + font-weight: bold; + border-bottom: solid 0.1rem silver; + } + + &:nth-child(even) { + background: rgba(0,0,0,0.03); + } + &:not(:first-child) { + &>.view td { + font-weight: normal !important; + } + } + + &.focused { + background: rgba(0,0,0,0.03); + } + &:focus { + background: rgba(0,0,0,0.07); + } + } + } + + .block>.children, + .view, + table, + tbody, + tr { + display: contents !important; + } +} + + + /********************************************************* Testing ***/ :host.show-click-zones .outline .block, @@ -710,5 +768,6 @@ + /********************************************************************** * vim:set ts=4 sw=4 : */ diff --git a/experiments/outline-editor/editor.js b/experiments/outline-editor/editor.js index 6e461fa..d4fc058 100755 --- a/experiments/outline-editor/editor.js +++ b/experiments/outline-editor/editor.js @@ -102,9 +102,16 @@ var getCharOffset = function(elem, x, y, data){ return data.c - 1 } // count "virtual" newlines between text and block elements... - var block = ['block', 'table', 'flex', 'grid'] - .includes( - getComputedStyle(e).display) + var type = getComputedStyle(e).display + var block = [ + 'block', + // XXX these do not add up yet... + //'table', + //'table-row', + //'table-cell', + 'flex', + 'grid', + ].includes(type) if(block && data.prev_elem && data.prev_elem != 'block'){ @@ -115,6 +122,13 @@ var getCharOffset = function(elem, x, y, data){ // handle the node... data = getCharOffset(e, x, y, data) + + // compensate for table stuff... + if(type == 'table-row'){ + data.c -= 1 } + if(type == 'table-cell'){ + data.c += 1 } + if(typeof(data) != 'object'){ return data } } } return arguments.length > 3 ? @@ -200,8 +214,11 @@ var plugin = { return function(_, text){ elem.style ??= [] elem.style.push(...style) - return code - ?? text } }, + return typeof(code) == 'function' ? + code(...arguments) + : code != null ? + code + : text } }, } @@ -632,15 +649,23 @@ var syntax = { var tables = { __proto__: plugin, + // XXX EXPERIMENTAL + __pre_parse__: function(text, editor, elem){ + return text + .replace(/^(--table--)$/m, this.style(editor, elem, 'table-2')) }, + __parse__: function(text, editor, elem){ return text .replace(/^\s*(?${ - body - .replace(/\s*\|\s*\n\s*\|\s*/gm, '\n') - .replace(/\s*\|\s*/gm, '') - }` }) }, + this.style(editor, elem, + 'table', + function(_, body){ + return `\n
${ + body + .trim() + .replace(/\s*\|\s*\n\s*\|\s*/gm, '
') + .replace(/\s*\|\s*/gm, '') + }
` })) }, } @@ -732,17 +757,6 @@ var escaping = { //--------------------------------------------------------------------- var JSONOutline = { - // Format: - // ::= [ - // { - // text: , - // children: , - // ... - // }, - // ... - // ] - json: undefined, - // format: // { // : , @@ -777,7 +791,7 @@ var JSONOutline = { var i = 0 // all nodes.. if(node == null || node == 'all' || node == 'visible'){ - for(var e of this.json){ + for(var e of this.json()){ yield* this.__iter(e, [i++], node) } // single node... } else { @@ -789,7 +803,7 @@ var JSONOutline = { this.get(...args), mode) } }, [Symbol.iterator]: function*(mode='all'){ - for(var node of this.json){ + for(var node of this.json()){ for(var [_, n] of this.__iter(node, mode)){ yield n } } }, iter: function*(node, mode){ @@ -817,12 +831,285 @@ var JSONOutline = { crop: function(){}, uncrop: function(){}, - parseBlockAttrs: function(){}, - parse: function(){}, + // NOTE: this is auto-populated by plugin.style(..)... + __styles: undefined, + + // block render... + __code2html__: function(code, elem={}){ + var that = this + + // only whitespace -> keep element blank... + if(code.trim() == ''){ + elem.text = code + return elem } + + // helpers... + var run = function(stage, text){ + var meth = { + pre: '__pre_parse__', + main: '__parse__', + post: '__post_parse__', + }[stage] + return that.threadPlugins(meth, text, that, elem) } + + elem = this.parseBlockAttrs(code, elem) + code = elem.text + + // 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 , ... ] + .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 }, + + // output format... + __code2text__: function(code){ + return code + .replace(/(\n\s*)-/g, '$1\\-') }, + __text2code__: function(text){ + text = text + .replace(/(\n\s*)\\-/g, '$1-') + return this.trim_block_text ? + text.trim() + : text }, + + __block_attrs__: { + id: 'attr', + collapsed: 'attr', + focused: 'cls', + }, + // + // Parse attrs... + // .parseBlockAttrs([, ]) + // -> + // + // Parse attrs keeping non-system attrs in .text... + // .parseBlockAttrs(, true[, ]) + // -> + // + // Parse attrs keeping all attrs in .text... + // .parseBlockAttrs(, 'all'[, ]) + // -> + // + parseBlockAttrs: function(text, keep=false, elem={}){ + if(typeof(keep) == 'object'){ + elem = keep + keep = typeof(elem) == 'boolean' ? + elem + : false } + var system = this.__block_attrs__ + var clean = text + // XXX for some reason changing the first group into (?<= .. ) + // still eats up the whitespace... + // ...putting the same pattern in a normal group and + // returning it works fine... + //.replace(/(?<=[\n\h]*)(?:(?:\n|^)\s*\w*\s*::\s*[^\n]*\s*)*$/, + .replace(/([\n\t ]*)(?:(?:\n|^)[\t ]*\w+[\t ]*::[\t ]*[^\n]+[\t ]*)+$/, + function(match, ws){ + var attrs = match + .trim() + .split(/(?:[\t ]*::[\t ]*|[\t ]*\n[\t ]*)/g) + while(attrs.length > 0){ + var [name, val] = attrs.splice(0, 2) + elem[name] = + val == 'true' ? + true + : val == 'false' ? + false + : val + // keep non-system attrs... + if(keep + && !(name in system)){ + ws += `\n${name}::${val}` } } + return ws }) + elem.text = keep == 'all' ? + text + : clean + return elem }, + parse: function(text){ + var that = this + text = text + .replace(/^[ \t]*\n/, '') + text = ('\n' + text) + .split(/\n([ \t]*)(?:- |-\s*$)/gm) + .slice(1) + var tab = ' '.repeat(this.tab_size || 8) + var level = function(lst, prev_sep=undefined, parent=[]){ + while(lst.length > 0){ + sep = lst[0].replace(/\t/gm, tab) + // 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 attrs = that.parseBlockAttrs(block) + attrs.text = that.__text2code__(attrs.text + // normalize indent... + .split(new RegExp('\n'+sep+' ', 'g')) + .join('\n')) + parent.push({ + collapsed: false, + focused: false, + ...attrs, + children: [], + }) + // indent... + } else { + parent.at(-1).children = level(lst, sep) } } + return parent } + return level(text) }, data: function(){}, load: function(){}, - text: function(){}, + + // Format: + // ::= [ + // { + // text: , + // children: , + // ... + // }, + // ... + // ] + // XXX + json: function(){}, + + // XXX add option to customize indent size... + text: function(node, indent, level){ + // .text(, ) + if(typeof(node) == 'string'){ + ;[node, indent=' ', level=''] = [undefined, ...arguments] } + node ??= this.json(node) + indent ??= ' ' + level ??= '' + var text = [] + for(var elem of node){ + text.push( + level +'- ' + + this.__code2text__(elem.text) + .replace(/\n/g, '\n'+ level +' ') + // attrs... + + (Object.keys(elem) + .reduce(function(res, attr){ + return (attr == 'text' + || attr == 'children') ? + res + : res + + (elem[attr] ? + '\n'+level+' ' + `${ attr }:: ${ elem[attr] }` + : '') }, '')), + (elem.children + && elem.children.length > 0) ? + this.text(elem.children || [], indent, level+indent) + : [] ) } + return text + .flat() + .join('\n') }, + + // XXX add read-only option... + htmlBlock: function(data, options={}){ + var that = this + + var parsed = this.__code2html__(data.text, {...data}) + + var cls = parsed.style ?? [] + delete parsed.style + + var attrs = [] + + for(var [attr, value] of Object.entries({...data, ...parsed})){ + if(attr == 'children' || attr == 'text'){ + continue } + var i + var type = this.__block_attrs__[attr] + if(type == 'cls'){ + value ? + cls.push(attr) + : (i = cls.indexOf(attr)) >= 0 ? + cls.splice(i, 1) + : undefined + } else if(type == 'attr' + || type == undefined){ + typeof(value) == 'boolean'? + (value ? + attrs.push(attr) + : (i = attrs.indexOf(attr)) >= 0 ? + attrs.splice(i, 1) + : undefined) + : value != null ? + attrs.push(`${attr}="${value}"`) + : (i = attrs.indexOf(attr)) >= 0 ? + attrs.splice(i, 1) + : undefined } } + + var children = data.children + .map(function(data){ + return that.htmlBlock(data) }) + .join('') + return ( +`
\ +\ +${ parsed.text }\ +
${ children }
\ +
`) }, + html: function(data, options=false){ + var that = this + if(typeof(data) == 'boolean'){ + options = data + data = undefined } + data = data == null ? + this.json() + : typeof(data) == 'string' ? + this.parse(data) + : data instanceof Array ? + data + : [data] + options = + typeof(options) == 'boolean' ? + {full: options} + : (options + ?? {}) + + var nodes = data + .map(function(data){ + return that.htmlBlock(data) }) + .join('') + + return !options.full ? + nodes + : ( +`
\ +
\ +\ +
${ nodes }
\ +
`) }, } @@ -830,6 +1117,8 @@ var JSONOutline = { // XXX experiment with a concatinative model... // .get(..) -> Outline (view) var Outline = { + __proto__: JSONOutline, + dom: undefined, // config... @@ -1039,18 +1328,22 @@ var Outline = { : offset == 'visible' ? [...node.querySelectorAll('.block')] .filter(function(e){ - return e.querySelector('.view').offsetParent != null }) + //return e.querySelector('.view').offsetParent != null }) + return e.offsetParent != null }) : offset == 'viewport' ? [...node.querySelectorAll('.block')] .filter(function(e){ - return e.querySelector('.view').offsetParent != null - && e.querySelector('.code').visibleInViewport() }) + //return e.querySelector('.view').offsetParent != null + // && e.querySelector('.code').visibleInViewport() }) + return e.offsetParent != null + && e.visibleInViewport() }) : offset == 'editable' ? [...node.querySelectorAll('.block>.code')] : offset == 'selected' ? [...node.querySelectorAll('.block[selected]')] .filter(function(e){ - return e.querySelector('.view').offsetParent != null }) + //return e.querySelector('.view').offsetParent != null }) + return e.offsetParent != null }) : offset == 'children' ? children(node) : offset == 'siblings' ? @@ -1141,13 +1434,15 @@ var Outline = { }, // NOTE: this does not internally handle undo as it would be too // granular... + _updateTextareaSize: function(elem){ + elem.style.height = getComputedStyle(elem.nextSibling).height }, update: function(node='focused', data){ var node = this.get(node) data ??= this.data(node, false) var parsed = {} if('text' in data){ - var text = node.querySelector('.code') + var code = node.querySelector('.code') var html = node.querySelector('.view') if(this.__code2html__){ // NOTE: we are ignoring the .collapsed attr here @@ -1171,8 +1466,10 @@ var Outline = { // NOTE: adding a space here is done to prevent the browser // from hiding the last newline... : data.text + ' ' } - text.value = data.text - text.updateSize() } + code.value = data.text + code.updateSize() + // NOTE: this will have no effect if the element is not attached... + this._updateTextareaSize(code) } for(var [attr, value] of Object.entries({...data, ...parsed})){ if(attr == 'children' || attr == 'text'){ @@ -1425,70 +1722,6 @@ var Outline = { ;[this.__redo_stack] = this.__undo(this.__redo_stack) return this }, - // block render... - // NOTE: this is auto-populated by .__code2html__(..) - __styles: undefined, - __code2html__: function(code, elem={}){ - var that = this - - // only whitespace -> keep element blank... - if(code.trim() == ''){ - elem.text = code - return elem } - - // helpers... - var run = function(stage, text){ - var meth = { - pre: '__pre_parse__', - main: '__parse__', - post: '__post_parse__', - }[stage] - return that.threadPlugins(meth, text, that, elem) } - - elem = this.parseBlockAttrs(code, elem) - code = elem.text - - // 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 , ... ] - .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 }, - // output format... - __code2text__: function(code){ - return code - .replace(/(\n\s*)-/g, '$1\\-') }, - __text2code__: function(text){ - text = text - .replace(/(\n\s*)\\-/g, '$1-') - return this.trim_block_text ? - text.trim() - : text }, - // serialization... data: function(elem, deep=true){ elem = this.get(elem) @@ -1521,121 +1754,6 @@ var Outline = { return children .map(function(elem){ return that.data(elem) }) }, - // XXX add option to customize indent size... - text: function(node, indent, level){ - // .text(, ) - if(typeof(node) == 'string'){ - ;[node, indent=' ', level=''] = [undefined, ...arguments] } - node ??= this.json(node) - indent ??= ' ' - level ??= '' - var text = [] - for(var elem of node){ - text.push( - level +'- ' - + this.__code2text__(elem.text) - .replace(/\n/g, '\n'+ level +' ') - // attrs... - + (Object.keys(elem) - .reduce(function(res, attr){ - return (attr == 'text' - || attr == 'children') ? - res - : res - + (elem[attr] ? - '\n'+level+' ' + `${ attr }:: ${ elem[attr] }` - : '') }, '')), - (elem.children - && elem.children.length > 0) ? - this.text(elem.children || [], indent, level+indent) - : [] ) } - return text - .flat() - .join('\n') }, - - // - // Parse attrs... - // .parseBlockAttrs([, ]) - // -> - // - // Parse attrs keeping non-system attrs in .text... - // .parseBlockAttrs(, true[, ]) - // -> - // - // Parse attrs keeping all attrs in .text... - // .parseBlockAttrs(, 'all'[, ]) - // -> - // - parseBlockAttrs: function(text, keep=false, elem={}){ - if(typeof(keep) == 'object'){ - elem = keep - keep = typeof(elem) == 'boolean' ? - elem - : false } - var system = this.__block_attrs__ - var clean = text - // XXX for some reason changing the first group into (?<= .. ) - // still eats up the whitespace... - // ...putting the same pattern in a normal group and - // returning it works fine... - //.replace(/(?<=[\n\h]*)(?:(?:\n|^)\s*\w*\s*::\s*[^\n]*\s*)*$/, - .replace(/([\n\t ]*)(?:(?:\n|^)[\t ]*\w+[\t ]*::[\t ]*[^\n]+[\t ]*)+$/, - function(match, ws){ - var attrs = match - .trim() - .split(/(?:[\t ]*::[\t ]*|[\t ]*\n[\t ]*)/g) - while(attrs.length > 0){ - var [name, val] = attrs.splice(0, 2) - elem[name] = - val == 'true' ? - true - : val == 'false' ? - false - : val - // keep non-system attrs... - if(keep - && !(name in system)){ - ws += `\n${name}::${val}` } } - return ws }) - elem.text = keep == 'all' ? - text - : clean - return elem }, - parse: function(text){ - var that = this - text = text - .replace(/^[ \t]*\n/, '') - text = ('\n' + text) - .split(/\n([ \t]*)(?:- |-\s*$)/gm) - .slice(1) - var tab = ' '.repeat(this.tab_size || 8) - var level = function(lst, prev_sep=undefined, parent=[]){ - while(lst.length > 0){ - sep = lst[0].replace(/\t/gm, tab) - // 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 attrs = that.parseBlockAttrs(block) - attrs.text = that.__text2code__(attrs.text - // normalize indent... - .split(new RegExp('\n'+sep+' ', 'g')) - .join('\n')) - parent.push({ - collapsed: false, - focused: false, - ...attrs, - children: [], - }) - // indent... - } else { - parent.at(-1).children = level(lst, sep) } } - return parent } - return level(text) }, // XXX should this handle children??? // XXX revise name... @@ -1699,6 +1817,8 @@ var Outline = { cur[place](block) : undefined + this._updateTextareaSize(code) + this.setUndo(this.path(cur), 'remove', [this.path(block)]) } return block }, // XXX see inside... @@ -1728,8 +1848,9 @@ var Outline = { // ...this is done by expanding the textarea to the element // size and enabling it to intercept clicks correctly... setTimeout(function(){ + var f = that._updateTextareaSize for(var e of [...that.outline.querySelectorAll('textarea')]){ - e.updateSize() } }, 0) + f(e) } }, 0) // restore focus... this.focus() return this }, diff --git a/experiments/outline-editor/index.html b/experiments/outline-editor/index.html index 8a547ce..1b9f55c 100755 --- a/experiments/outline-editor/index.html +++ b/experiments/outline-editor/index.html @@ -70,6 +70,10 @@ var setup = function(){ (BUG also the above line is not italic -- can't reproduce) - clicking right of the last line places cursor wrong - _this is a problem with the new version of `getMarkdownOffset(..)`_ + - DONE text text text + - DONE text text text + text text text + text text text - DONE M M can't place cursor before first char M @@ -87,6 +91,9 @@ var setup = function(){ text text text ``` text text text + - DONE |text|text|text| + |text|text|text| + |text|text|text| - BUG: parser: code blocks do not ignore single back-quotes... - ``` x = `moo` @@ -103,8 +110,14 @@ var setup = function(){ - side margins are a bit too large (account for toolbat to the right) - - ## ToDo: + - Q: should we use `HTMLTextAreaElement.autoUpdateSize(..)` or handle it globally in setup??? + - _...I'm leaning towards the later..._ - Q: can we place a cursor in a table correctly??? - Q: should tables be text-based markdown or higher-level? + - for reference a normal table + - | col 1 | col 2 | col 3 | + | moo | foo | boo | + | 1 | 2 | 3 | - block-based -- adjacent blocks in table format (a-la markdown) are treated as rows of one table... - here is an example - | col 1 | col 2 | col 3 | @@ -114,17 +127,18 @@ var setup = function(){ - not yet sure how are we going to allign columns (CSS preffered) - block-children -- similar to how lists are done now - a demo - - table: - - | A | B | B | + - --table-- + - | A | B | C | - | 1 | 2 | 3 | - - | X | y | Z | + - | moo | foo | boo | - - the header may be used as:: - header row - caption text - both? - - see CSS grids + 'display: contents' (might help hide non-grid elemnts... - - + - Q: how do we handle indenting a table row? + - Q: how do we handle unmarked text? + - - might be fun to make the general syntax (with "=" removed) to be compatible with markdown... - might also be fun to auto-generat (template) new blocks within a table... - this would greatly simplify table navigation and creation @@ -374,13 +388,13 @@ var setup = function(){ | a-s | toggle status | | a-x | toggle status DONE | | a-r | toggle status REJECT | - | c-z | normal: undo | - | c-s-z | normal: redo | - | c | normal: crop current node | + | c-z | normal: undo | + | c-s-z | normal: redo | + | c | normal: crop current node | | enter | normal: edit node | | | edit: create node below | | esc | crop: exit crop | - | | edit: exit edit mode | + | | edit: exit edit mode | - ### Formatting - The formatting mostly adheres to the markdown spec with a few minor differences -