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 `| ${
+ body
+ .trim()
+ .replace(/\s*\|\s*\n\s*\|\s*/gm, ' |
\n| ')
+ .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
+ : (
+``) },
}
@@ -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
-