diff --git a/docs/css.gmi b/docs/css.gmi
index 5279a89..0978b0d 100644
--- a/docs/css.gmi
+++ b/docs/css.gmi
@@ -38,7 +38,6 @@
=> //talon.computer/gmi.css gmi.css
=> //talon.computer/gmi.min.css gmi.min.css
-Psst! gmi.js enables light and dark mode toggling as well as custom themes– check it out!
-=> /js/ gmi.js
+=> /js/ Psst! gmi.js might be interesting to you as well– check it out! :)
CC0
diff --git a/docs/js.gmi b/docs/js.gmi
index eb752a9..f3b5a8d 100644
--- a/docs/js.gmi
+++ b/docs/js.gmi
@@ -47,6 +47,4 @@ window.gmi.source = window.gmi.lines[0].content
window.gmi.download()
```
-=> /gmi.js view/download (minified version available via .min.js)
-
-CC0
+=> /gmi.js view/download (minified version available via .min.js) CC0
diff --git a/gmi.js b/gmi.js
index 292ffd9..d357c8c 100644
--- a/gmi.js
+++ b/gmi.js
@@ -1,84 +1,96 @@
/* gmi.js is licensed under CC0 */
class Gemini {
- static line(line = {}) {
- let dom = line.nodeName ? line : document.createElement(line.type || Gemini.type.TEXT)
- if (line.editable) dom.contentEditable = line.editable
- switch (line.type) {
- case Gemini.type.LINK:
- const {href, content} = Gemini.PARSE_LINK.exec(line.content).groups
- dom.href = line.href || href || content
- dom.innerHTML = Gemini.innerHTML(Object.assign(line, { content, href }))
- break
- default:
- dom.innerHTML = line.innerHTML || Gemini.innerHTML(line)
- }
+ static line(line) {
+ let dom = Gemini.render(line).dom
return {
get dom() { return dom },
get type() { return this.dom.nodeName },
- set type(type) {
- let replacement
- switch (line.type) {
- case Gemini.type.LINK:
- const {href, content} = Gemini.PARSE_LINK.exec(this.content).groups
- replacement = {type, href, content, editable: this.editable }
- break
- default:
- replacement = {type, content: this.content, editable: this.editable}
- }
- const line = Gemini.line(replacement)
- this.dom.replaceWith(line.dom)
- dom = line.dom
- },
- get content() {
- switch (this.type) {
- case Gemini.type.QUOTE:
- return Array.from(this.dom.childNodes).map(child => child.textContent).join("\n")
- case Gemini.type.LIST:
- return Array.from(this.dom.children).map(child => child.textContent).join("\n")
- case Gemini.type.LINK:
- const {href, content} = Gemini.PARSE_LINK.exec(this.dom.textContent).groups
- return `${href || this.dom.href} ${content}`
- case Gemini.type.PREFORMATTED:
- return this.dom.textContent
- default:
- return this.dom.innerHTML.replace(/
/g, "\n").replace(/\n?$/, "")
- }
- },
- set content(value) {
- switch (this.type) {
- case Gemini.type.LINK:
- const {href, content} = Gemini.PARSE_LINK.exec(value).groups
- this.dom.innerHTML = Gemini.innerHTML({ type: this.type, content, href })
- this.dom.href = href
- break
- default:
- this.dom.innerHTML = Gemini.innerHTML({ type: this.type, content: value })
- }
- },
+ set type(type) { dom = Gemini.render({dom, type, content: Gemini.contentFrom(dom)}).dom },
+ get content() { return Gemini.contentFrom(dom) },
+ set content(content) { dom = Gemini.render({dom, type: dom.nodeName, content}) },
+ get editable() { return this.dom.contentEditable === "true" },
+ set editable(value) { Gemini.render({dom: this.dom, type: this.type, content: this.content, editable: value}) },
+ delete() { return this.dom.remove() },
get gmi() {
+ const syntax = Gemini.syntax[this.type]
+ const content = Gemini.contentFrom(this.dom).replace(/\n?$/, "")
switch (this.type) {
case Gemini.type.PREFORMATTED:
- return Gemini.syntax[this.type] + `\n${this.content}\n` + Gemini.syntax[this.type]
+ return `${syntax}\n${content}\n${syntax}`
break
default:
- return this.content.split("\n").map(content => `${Gemini.syntax[this.type] !== "" ? Gemini.syntax[this.type] + " ": ""}${content}`).join("\n")
- }
- },
- get editable() { return this.dom.contentEditable === "true" },
- set editable(value) {
- this.dom.contentEditable = value
- switch (this.type) {
- case Gemini.type.LINK:
- let {href, content} = Gemini.PARSE_LINK.exec(this.content).groups
- this.dom.innerHTML = Gemini.innerHTML({ type: this.type, editable: value, content, href })
- this.dom.href = href
+ return content.split("\n").map(line =>
+ `${syntax !== "" ? syntax + " " : ""}${line}`
+ ).join("\n")
}
},
get before() { return Gemini.line(this.dom.previousElementSibling) },
set before(line) { this.before.dom.after(line.dom) },
get after() { return Gemini.line(this.dom.nextElementSibling) },
set after(line) { this.after.dom.before(line.dom) },
- delete() { return this.dom.remove() },
+ }
+ }
+ static render(line) {
+ if (line.dom && line.dom.nodeName !== line.type) {
+ const replacement = document.createElement(line.type)
+ line.dom.replaceWith(replacement)
+ line.dom = replacement
+ } else if (line.nodeName) {
+ line = {
+ dom: line,
+ type: line.nodeName,
+ content: Gemini.contentFrom(line),
+ editable: line.contentEditable
+ }
+ } else {
+ line.dom = line.dom || document.createElement(line.type || Gemini.type.TEXT)
+ }
+ line.dom.contentEditable = line.editable || "inherit"
+ switch (line.type) {
+ case Gemini.type.LINK:
+ const {href, content} = Gemini.PARSE_LINK.exec(line.content).groups
+ line.dom.innerHTML = line.editable && href !== content ? `${href} ${content}` : content
+ line.dom.href = href
+ break
+ case Gemini.type.LIST:
+ line.dom.innerHTML = line.content.split("\n").map(content =>
+ content.length > 0 ? `
${content}` : ""
+ ).join("\n")
+ break
+ case Gemini.type.QUOTE:
+ line.dom.innerHTML = line.content.split("\n").map(content =>
+ `${content}
`
+ ).join("\n")
+ break
+ case Gemini.type.PREFORMATTED:
+ line.dom.textContent = line.content
+ break
+ default:
+ line.dom.innerHTML = line.content.replace(/\n+/g, "
")
+ }
+ return line
+ }
+ static contentFrom(dom) {
+ switch (dom.nodeName) {
+ case Gemini.type.QUOTE:
+ return Array.from(dom.childNodes).map(child =>
+ child.textContent
+ ).join("\n")
+ break
+ case Gemini.type.LIST:
+ return Array.from(dom.children).map(child =>
+ child.textContent
+ ).join("\n")
+ break
+ case Gemini.type.LINK:
+ const {href, content} = Gemini.PARSE_LINK.exec(dom.textContent).groups
+ return `${href || dom.href} ${content}`
+ break
+ case Gemini.type.PREFORMATTED:
+ return dom.textContent
+ break
+ default:
+ return dom.innerHTML.replace(/
/g, "\n")
}
}
get lines() {
@@ -91,6 +103,10 @@ class Gemini {
this.root.append(...lines.map(line => line.dom))
}
get source() { return this.lines.map(line => line.gmi).join("\n") }
+ set source(gmi) {
+ this.lines = Gemini.parse(gmi).map(([content, type]) =>
+ Gemini.line({type, content}))
+ }
download() {
const el = document.createElement('a')
el.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(this.source))
@@ -100,64 +116,6 @@ class Gemini {
}
// TODO: Refactor all of this into something cohesive
- // perserve newlines between link/heading and multiline types
- static innerHTML(line) {
- switch (line.type) {
- case Gemini.type.LINK:
- return line.editable && line.href !== line.content ? `${line.href} ${line.content}` : line.content
- case Gemini.type.LIST:
- return line.content.split("\n").map(content =>
- content.length > 0 ? `${content}` : ""
- ).join("\n")
- case Gemini.type.QUOTE:
- return line.content.split("\n").map(content =>
- `${content}
`
- ).join("\n")
- case Gemini.type.PREFORMATTED:
- return line.content
- default:
- return line.content.replace(/\n+/g, "
")
- }
- }
- // TODO: pull parsing out and make it more useful
- set source(gmi) {
- let lines = gmi.split("\n")
- let parsed = []
- while (lines.length) {
- const line = lines.shift()
- const syntax = /^[^\s]+/
- const type = syntax.test(line) ? Gemini.nodeName[line.match(syntax)[0]] || Gemini.type.TEXT : Gemini.type.TEXT
-
- let content = ""
- let until
- switch (type) {
- case Gemini.type.PREFORMATTED:
- until = lines.findIndex(line => line === Gemini.syntax[Gemini.type.PREFORMATTED])
- content = lines.slice(0, until).join("\n")
- lines = lines.slice(until + 1)
- break
- case Gemini.type.QUOTE:
- until = lines.findIndex(line => /[^>]/.test(line))
- content = [line, ...lines].slice(0, until + 1).map(line => line.replace("> ", "")).join("\n")
- lines = lines.slice(until)
- break
- case Gemini.type.LIST:
- until = lines.findIndex(line => /[^\*]/.test(line))
- content = [line, ...lines].slice(0, until + 2).map(line => line.replace("* ", "")).join("\n")
- lines = lines.slice(until + 1)
- break
- case Gemini.type.TEXT:
- content = line
- break
- default:
- content = line.replace(syntax, "")
- }
-
- parsed.push({type, content: content.length === 0 ? "\n" : content.trim()})
- }
-
- this.lines = parsed.map(Gemini.line)
- }
static PARSE_LINK = /((?[^\s]+\/\/[^\s]+)\s)?(?.+)/
static type = {
TEXT: "P",
@@ -186,6 +144,41 @@ class Gemini {
return flipped
}, {})
}
+ static parse(gmi) {
+ let lines = gmi.split("\n")
+ let parsed = []
+ while (lines.length) {
+ const line = lines.shift()
+ const syntax = /^[^\s]+/
+ const type = syntax.test(line) ? Gemini.nodeName[line.match(syntax)[0]] || Gemini.type.TEXT : Gemini.type.TEXT
+ let content = ""
+ let until
+ switch (type) {
+ case Gemini.type.PREFORMATTED:
+ until = lines.findIndex(line => line === Gemini.syntax[Gemini.type.PREFORMATTED])
+ content = lines.slice(0, until).join("\n")
+ lines = lines.slice(until + 1)
+ break
+ case Gemini.type.QUOTE:
+ until = lines.findIndex(line => /[^>]/.test(line))
+ content = [line, ...lines].slice(0, until + 1).map(line => line.replace("> ", "")).join("\n")
+ lines = lines.slice(until)
+ break
+ case Gemini.type.LIST:
+ until = lines.findIndex(line => /[^\*]/.test(line))
+ content = [line, ...lines].slice(0, until + 2).map(line => line.replace("* ", "")).join("\n")
+ lines = lines.slice(until + 1)
+ break
+ case Gemini.type.TEXT:
+ content = line
+ break
+ default:
+ content = line.replace(syntax, "")
+ }
+ parsed.push([content.length === 0 ? "\n" : content.trim(), type])
+ }
+ return parsed
+ }
constructor(root) {
this.root = root
// TODO: reference gmi2html implementation, fix these quirks
diff --git a/min/gmi.min.js b/min/gmi.min.js
index f7f760e..ff53d26 100644
--- a/min/gmi.min.js
+++ b/min/gmi.min.js
@@ -1 +1 @@
-class Gemini{static line(e={}){let t=e.nodeName?e:document.createElement(e.type||Gemini.type.TEXT);switch(e.editable&&(t.contentEditable=e.editable),e.type){case Gemini.type.LINK:const{href:n,content:i}=Gemini.PARSE_LINK.exec(e.content).groups;t.href=e.href||n||i,t.innerHTML=Gemini.innerHTML(Object.assign(e,{content:i,href:n}));break;default:t.innerHTML=e.innerHTML||Gemini.innerHTML(e)}return{get dom(){return t},get type(){return this.dom.nodeName},set type(e){let n;switch(i.type){case Gemini.type.LINK:const{href:t,content:i}=Gemini.PARSE_LINK.exec(this.content).groups;n={type:e,href:t,content:i,editable:this.editable};break;default:n={type:e,content:this.content,editable:this.editable}}const i=Gemini.line(n);this.dom.replaceWith(i.dom),t=i.dom},get content(){switch(this.type){case Gemini.type.QUOTE:return Array.from(this.dom.childNodes).map((e=>e.textContent)).join("\n");case Gemini.type.LIST:return Array.from(this.dom.children).map((e=>e.textContent)).join("\n");case Gemini.type.LINK:const{href:e,content:t}=Gemini.PARSE_LINK.exec(this.dom.textContent).groups;return`${e||this.dom.href} ${t}`;case Gemini.type.PREFORMATTED:return this.dom.textContent;default:return this.dom.innerHTML.replace(/
/g,"\n").replace(/\n?$/,"")}},set content(e){switch(this.type){case Gemini.type.LINK:const{href:t,content:n}=Gemini.PARSE_LINK.exec(e).groups;this.dom.innerHTML=Gemini.innerHTML({type:this.type,content:n,href:t}),this.dom.href=t;break;default:this.dom.innerHTML=Gemini.innerHTML({type:this.type,content:e})}},get gmi(){switch(this.type){case Gemini.type.PREFORMATTED:return Gemini.syntax[this.type]+`\n${this.content}\n`+Gemini.syntax[this.type];default:return this.content.split("\n").map((e=>`${""!==Gemini.syntax[this.type]?Gemini.syntax[this.type]+" ":""}${e}`)).join("\n")}},get editable(){return"true"===this.dom.contentEditable},set editable(e){switch(this.dom.contentEditable=e,this.type){case Gemini.type.LINK:let{href:t,content:n}=Gemini.PARSE_LINK.exec(this.content).groups;this.dom.innerHTML=Gemini.innerHTML({type:this.type,editable:e,content:n,href:t}),this.dom.href=t}},get before(){return Gemini.line(this.dom.previousElementSibling)},set before(e){this.before.dom.after(e.dom)},get after(){return Gemini.line(this.dom.nextElementSibling)},set after(e){this.after.dom.before(e.dom)},delete(){return this.dom.remove()}}}get lines(){return Array.from(this.root.children).filter((e=>Object.values(Gemini.type).includes(e.nodeName))).map(Gemini.line)}set lines(e){this.root.textContent="",this.root.append(...e.map((e=>e.dom)))}get source(){return this.lines.map((e=>e.gmi)).join("\n")}download(){const e=document.createElement("a");e.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(this.source)),e.setAttribute("download",this.lines[0].content.replace(/\s/g,"_")+".gmi"),e.style.display="none",document.body.appendChild(e),e.click(),document.body.removeChild(e)}static innerHTML(e){switch(e.type){case Gemini.type.LINK:return e.editable&&e.href!==e.content?`${e.href} ${e.content}`:e.content;case Gemini.type.LIST:return e.content.split("\n").map((e=>e.length>0?`${e}`:"")).join("\n");case Gemini.type.QUOTE:return e.content.split("\n").map((e=>`${e}
`)).join("\n");case Gemini.type.PREFORMATTED:return e.content;default:return e.content.replace(/\n+/g,"
")}}set source(e){let t=e.split("\n"),n=[];for(;t.length;){const e=t.shift(),i=/^[^\s]+/,s=i.test(e)&&Gemini.nodeName[e.match(i)[0]]||Gemini.type.TEXT;let r,o="";switch(s){case Gemini.type.PREFORMATTED:r=t.findIndex((e=>e===Gemini.syntax[Gemini.type.PREFORMATTED])),o=t.slice(0,r).join("\n"),t=t.slice(r+1);break;case Gemini.type.QUOTE:r=t.findIndex((e=>/[^>]/.test(e))),o=[e,...t].slice(0,r+1).map((e=>e.replace("> ",""))).join("\n"),t=t.slice(r);break;case Gemini.type.LIST:r=t.findIndex((e=>/[^\*]/.test(e))),o=[e,...t].slice(0,r+2).map((e=>e.replace("* ",""))).join("\n"),t=t.slice(r+1);break;case Gemini.type.TEXT:o=e;break;default:o=e.replace(i,"")}n.push({type:s,content:0===o.length?"\n":o.trim()})}this.lines=n.map(Gemini.line)}static PARSE_LINK=/((?[^\s]+\/\/[^\s]+)\s)?(?.+)/;static type={TEXT:"P",LINK:"A",LIST:"UL",QUOTE:"BLOCKQUOTE",PREFORMATTED:"PRE",H1:"H1",H2:"H2",H3:"H3"};static syntax={[Gemini.type.TEXT]:"",[Gemini.type.LINK]:"=>",[Gemini.type.LIST]:"*",[Gemini.type.QUOTE]:">",[Gemini.type.PREFORMATTED]:"```",[Gemini.type.H1]:"#",[Gemini.type.H2]:"##",[Gemini.type.H3]:"###"};static get nodeName(){return Object.entries(Gemini.syntax).reduce(((e,t)=>{const[n,i]=t;return e[i]=n,e}),{})}constructor(e){this.root=e;for(let e of this.root.childNodes){if("BR"===e.nodeName){let t=document.createElement(Gemini.type.TEXT);t.innerHTML="
",e.replaceWith(t)}"P"===e.nodeName&&e.firstChild.nodeName===Gemini.type.LINK&&e.replaceWith(e.firstChild)}}}
+class Gemini{static line(e){let t=Gemini.render(e).dom;return{get dom(){return t},get type(){return this.dom.nodeName},set type(e){t=Gemini.render({dom:t,type:e,content:Gemini.contentFrom(t)}).dom},get content(){return Gemini.contentFrom(t)},set content(e){t=Gemini.render({dom:t,type:t.nodeName,content:e})},get editable(){return"true"===this.dom.contentEditable},set editable(e){Gemini.render({dom:this.dom,type:this.type,content:this.content,editable:e})},delete(){return this.dom.remove()},get gmi(){const e=Gemini.syntax[this.type],t=Gemini.contentFrom(this.dom).replace(/\n?$/,"");switch(this.type){case Gemini.type.PREFORMATTED:return`${e}\n${t}\n${e}`;default:return t.split("\n").map((t=>`${""!==e?e+" ":""}${t}`)).join("\n")}},get before(){return Gemini.line(this.dom.previousElementSibling)},set before(e){this.before.dom.after(e.dom)},get after(){return Gemini.line(this.dom.nextElementSibling)},set after(e){this.after.dom.before(e.dom)}}}static render(e){if(e.dom&&e.dom.nodeName!==e.type){const t=document.createElement(e.type);e.dom.replaceWith(t),e.dom=t}else e.nodeName?e={dom:e,type:e.nodeName,content:Gemini.contentFrom(e),editable:e.contentEditable}:e.dom=e.dom||document.createElement(e.type||Gemini.type.TEXT);switch(e.dom.contentEditable=e.editable||"inherit",e.type){case Gemini.type.LINK:const{href:t,content:n}=Gemini.PARSE_LINK.exec(e.content).groups;e.dom.innerHTML=e.editable&&t!==n?`${t} ${n}`:n,e.dom.href=t;break;case Gemini.type.LIST:e.dom.innerHTML=e.content.split("\n").map((e=>e.length>0?`${e}`:"")).join("\n");break;case Gemini.type.QUOTE:e.dom.innerHTML=e.content.split("\n").map((e=>`${e}
`)).join("\n");break;case Gemini.type.PREFORMATTED:e.dom.textContent=e.content;break;default:e.dom.innerHTML=e.content.replace(/\n+/g,"
")}return e}static contentFrom(e){switch(e.nodeName){case Gemini.type.QUOTE:return Array.from(e.childNodes).map((e=>e.textContent)).join("\n");case Gemini.type.LIST:return Array.from(e.children).map((e=>e.textContent)).join("\n");case Gemini.type.LINK:const{href:t,content:n}=Gemini.PARSE_LINK.exec(e.textContent).groups;return`${t||e.href} ${n}`;case Gemini.type.PREFORMATTED:return e.textContent;default:return e.innerHTML.replace(/
/g,"\n")}}get lines(){return Array.from(this.root.children).filter((e=>Object.values(Gemini.type).includes(e.nodeName))).map(Gemini.line)}set lines(e){this.root.textContent="",this.root.append(...e.map((e=>e.dom)))}get source(){return this.lines.map((e=>e.gmi)).join("\n")}set source(e){this.lines=Gemini.parse(e).map((([e,t])=>Gemini.line({type:t,content:e})))}download(){const e=document.createElement("a");e.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(this.source)),e.setAttribute("download",this.lines[0].content.replace(/\s/g,"_")+".gmi"),e.style.display="none",document.body.appendChild(e),e.click(),document.body.removeChild(e)}static PARSE_LINK=/((?[^\s]+\/\/[^\s]+)\s)?(?.+)/;static type={TEXT:"P",LINK:"A",LIST:"UL",QUOTE:"BLOCKQUOTE",PREFORMATTED:"PRE",H1:"H1",H2:"H2",H3:"H3"};static syntax={[Gemini.type.TEXT]:"",[Gemini.type.LINK]:"=>",[Gemini.type.LIST]:"*",[Gemini.type.QUOTE]:">",[Gemini.type.PREFORMATTED]:"```",[Gemini.type.H1]:"#",[Gemini.type.H2]:"##",[Gemini.type.H3]:"###"};static get nodeName(){return Object.entries(Gemini.syntax).reduce(((e,t)=>{const[n,i]=t;return e[i]=n,e}),{})}static parse(e){let t=e.split("\n"),n=[];for(;t.length;){const e=t.shift(),i=/^[^\s]+/,o=i.test(e)&&Gemini.nodeName[e.match(i)[0]]||Gemini.type.TEXT;let r,m="";switch(o){case Gemini.type.PREFORMATTED:r=t.findIndex((e=>e===Gemini.syntax[Gemini.type.PREFORMATTED])),m=t.slice(0,r).join("\n"),t=t.slice(r+1);break;case Gemini.type.QUOTE:r=t.findIndex((e=>/[^>]/.test(e))),m=[e,...t].slice(0,r+1).map((e=>e.replace("> ",""))).join("\n"),t=t.slice(r);break;case Gemini.type.LIST:r=t.findIndex((e=>/[^\*]/.test(e))),m=[e,...t].slice(0,r+2).map((e=>e.replace("* ",""))).join("\n"),t=t.slice(r+1);break;case Gemini.type.TEXT:m=e;break;default:m=e.replace(i,"")}n.push([0===m.length?"\n":m.trim(),o])}return n}constructor(e){this.root=e;for(let e of this.root.childNodes){if("BR"===e.nodeName){let t=document.createElement(Gemini.type.TEXT);t.innerHTML="
",e.replaceWith(t)}"P"===e.nodeName&&e.firstChild.nodeName===Gemini.type.LINK&&e.replaceWith(e.firstChild)}}}