~talon/gmi-web

b2420d5a410bc2fef4159b51bb67729038dcd210 — Talon Poole a month ago 4aa3547
sideline setting source for now add CSS vars
3 files changed, 58 insertions(+), 125 deletions(-)

M docs/js.gmi
M gmi.js
M min/gmi.min.js
M docs/js.gmi => docs/js.gmi +13 -20
@@ 4,47 4,40 @@
gmi.js is made up of lines! Use ctrl+shift+i to open a console and paste this in:
```js
const line = Gemini.line({
  type: Gemini.type.LIST,
  type: "UL",
  content: "manipulate the dom\nbut like in a Gemini way\ntry it!"
})
document.body.prepend(line.dom)
```
> now try changing the type and content and observing the effects.  
```js
line.type = Gemini.type.QUOTE
line.type = "BLOCKQUOTE"
line.content = "now it's a quote"
```
A line can be of any type available via Gemini.type and its content should be a string with optional newlines. Use .delete() to remove the line. Checkout the Gemini.line source to see the complete API.

A document provides a way to handle many lines together:
```js
window.gmi = new Gemini(document.body)
window.gmi.lines[2].type = Gemini.TYPE.TEXT
window.gmi.lines[2].type = "P"
window.gmi.lines = [
  Gemini.line({content: "interesting", type: Gemini.type.H1}),
  Gemini.line({content: "interesting", type: "H1"}),
  Gemini.line({content: "that's convienient"}),
]
```
> you can even work with the source directly
> the gemtext source is available via .source
```js
console.log(window.gmi.source)
window.gmi.source = "=> https://talon.computer/js/ uh, go back!"
```
> view the source as an editable pre block by setting it as the content of a Gemini.type.PREFORMATTED line ;)
```js
window.gmi.lines = [Gemini.line({
  content: window.gmi.source, 
  type: Gemini.type.PREFORMATTED, 
  editable: true
})]
```
> render any changes you may have made
```js
window.gmi.source = window.gmi.lines[0].content
window.gmi.source
```
> or maybe a .gmi file would be easier?
```js
window.gmi.download()
```

All the gmi.css variables are also available as properties.
=> /css/ learn more about gmi.css
```
window.gmi.foreground = "white"
window.gmi.background = "black"
```

=> /gmi.js view/download (minified version available via .min.js) CC0

M gmi.js => gmi.js +44 -104
@@ 1,13 1,14 @@
/* gmi.js is licensed under CC0 */
class Gemini {
  static syntax = { P: "", A: "=>", UL: "*", BLOCKQUOTE: ">", PRE: "```", H1: "#", H2: "##", H3: "###", }
  static line(line) {
    let dom = Gemini.render(line).dom
    return {
      get dom() { return dom },
      get type() { return this.dom.nodeName },
      set type(type) { dom = Gemini.render({dom, type, content: Gemini.contentFrom(dom)}).dom },
      set type(type) { dom = Gemini.render({dom: this.dom, type, content: Gemini.contentFrom(this.dom)}).dom },
      get content() { return Gemini.contentFrom(dom) },
      set content(content) { dom = Gemini.render({dom, type: dom.nodeName, content}) },
      set content(content) { 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() },


@@ 15,7 16,7 @@ class Gemini {
        const syntax = Gemini.syntax[this.type]
        const content = Gemini.contentFrom(this.dom).replace(/\n?$/, "")
        switch (this.type) {
          case Gemini.type.PREFORMATTED:
          case "PRE":
            return `${syntax}\n${content}\n${syntax}`
            break
          default:


@@ 43,26 44,26 @@ class Gemini {
        editable: line.contentEditable
      }
    } else {
      line.dom = line.dom || document.createElement(line.type || Gemini.type.TEXT)
      line.dom = line.dom || document.createElement(line.type || "P")
    }
    line.dom.contentEditable = line.editable || "inherit"
    switch (line.type) {
      case Gemini.type.LINK:
        const {href, content} = Gemini.PARSE_LINK.exec(line.content).groups
      case "A":
        const {href, content} = Gemini.link(line.content)
        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 ? `<li>${content}</li>` : ""
      case "UL": 
        line.dom.innerHTML = line.content.split("\n").map(content => content.length > 0 ? 
          `<li>${content}</li>` : ""
        ).join("\n")
        break
      case Gemini.type.QUOTE: 
      case "BLOCKQUOTE": 
        line.dom.innerHTML = line.content.split("\n").map(content => 
          `<div>${content}</div>`
        ).join("\n")
        break
      case Gemini.type.PREFORMATTED:
      case "PRE":
        line.dom.textContent = line.content
        break
      default:


@@ 72,125 73,64 @@ class Gemini {
  }
  static contentFrom(dom) {
    switch (dom.nodeName) {
      case Gemini.type.QUOTE:
      case "BLOCKQUOTE":
        return Array.from(dom.childNodes).map(child => 
          child.textContent
        ).join("\n")
        break
      case Gemini.type.LIST: 
      case "UL": 
        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
      case "A": 
        const {href, content} = Gemini.link(dom.textContent)
        return `${href || dom.href} ${content}`
        break
      case Gemini.type.PREFORMATTED: 
      case "PRE": 
        return dom.textContent
        break
      default:
        return dom.innerHTML.replace(/<br>/g, "\n")
    }
  }
  // TODO: rename/move/idk this is awk
  static link(content = "") { return /((?<href>[^\s]+\/\/[^\s]+)\s)?(?<content>.+)/.exec(content).groups }

  constructor(root) { this.root = root }
  get lines() {
    return Array.from(this.root.children)
      .filter(el => Object.values(Gemini.type).includes(el.nodeName))
      .map(Gemini.line)
    return Array.from(this.root.children).filter(el => [
      "P", "BLOCKQUOTE", "A", "PRE", "UL", "H1", "H2", "H3"
    ].includes(el.nodeName)).map(Gemini.line)
  }
  set lines(lines) {
    this.root.textContent = ""
    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}))
  }
  // TODO: set source is a DOM thing, but parsing should be isomorphic
  // set source(gmi) {}
  download() {
    const el = document.createElement('a')
    el.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(this.source))
    el.setAttribute('download', `${this.lines[0].content.replace(/\s/g, "_")}.gmi`)
    el.setAttribute('href', 'data:text/gemini;charset=utf-8,' 
      + encodeURIComponent(this.source))
    el.setAttribute('download', 
      `${this.lines[0].content.replace(/\s/g, "_")}.gmi`)
    el.style.display = 'none'
    document.body.appendChild(el); el.click(); document.body.removeChild(el)
  }

  // TODO: Refactor all of this into something cohesive 
  static PARSE_LINK = /((?<href>[^\s]+\/\/[^\s]+)\s)?(?<content>.+)/
  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((flipped, entry) => {
      const [key, value] = entry
      flipped[value] = key
      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
    for (let el of this.root.childNodes) {
      if (el.nodeName === "BR") {
        let empty = document.createElement(Gemini.type.TEXT)
        empty.innerHTML = "<br>"
        el.replaceWith(empty)
      }
      if (el.nodeName === "P" && el.firstChild.nodeName === Gemini.type.LINK) {
        el.replaceWith(el.firstChild)
      }
    }
  }
  get foreground() { return getComputedStyle(this.root).getPropertyValue("--foreground") }
  set foreground(value) { return this.root.style.setProperty("--foreground", value) }
  get background() { return getComputedStyle(this.root).getPropertyValue("--background") }
  set background(value) { return this.root.style.setProperty("--background", value) }
  get size() { return getComputedStyle(this.root).getPropertyValue("--font-size") }
  set size(value) { return this.root.style.setProperty("--font-size", value) }
  get lineHeight() { return getComputedStyle(this.root).getPropertyValue("--line-height") }
  set lineHeight(value) { return this.root.style.setProperty("--line-height", value) }
  get serif() { return getComputedStyle(this.root).getPropertyValue("--serif") }
  set serif(value) { return this.root.style.setProperty("--serif", value) }
  get sans() { return getComputedStyle(this.root).getPropertyValue("--sans") }
  set sans(value) { return this.root.style.setProperty("--sans", value) }
  get mono() { return getComputedStyle(this.root).getPropertyValue("--mono") }
  set mono(value) { return this.root.style.setProperty("--mono", value) }
}

M min/gmi.min.js => min/gmi.min.js +1 -1
@@ 1,1 1,1 @@
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?`<li>${e}</li>`:"")).join("\n");break;case Gemini.type.QUOTE:e.dom.innerHTML=e.content.split("\n").map((e=>`<div>${e}</div>`)).join("\n");break;case Gemini.type.PREFORMATTED:e.dom.textContent=e.content;break;default:e.dom.innerHTML=e.content.replace(/\n+/g,"<br>")}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(/<br>/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=/((?<href>[^\s]+\/\/[^\s]+)\s)?(?<content>.+)/;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="<br>",e.replaceWith(t)}"P"===e.nodeName&&e.firstChild.nodeName===Gemini.type.LINK&&e.replaceWith(e.firstChild)}}}
class Gemini{static syntax={P:"",A:"=>",UL:"*",BLOCKQUOTE:">",PRE:"```",H1:"#",H2:"##",H3:"###"};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:this.dom,type:e,content:Gemini.contentFrom(this.dom)}).dom},get content(){return Gemini.contentFrom(t)},set content(e){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"PRE":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||"P");switch(e.dom.contentEditable=e.editable||"inherit",e.type){case"A":const{href:t,content:n}=Gemini.link(e.content);e.dom.innerHTML=e.editable&&t!==n?`${t} ${n}`:n,e.dom.href=t;break;case"UL":e.dom.innerHTML=e.content.split("\n").map((e=>e.length>0?`<li>${e}</li>`:"")).join("\n");break;case"BLOCKQUOTE":e.dom.innerHTML=e.content.split("\n").map((e=>`<div>${e}</div>`)).join("\n");break;case"PRE":e.dom.textContent=e.content;break;default:e.dom.innerHTML=e.content.replace(/\n+/g,"<br>")}return e}static contentFrom(e){switch(e.nodeName){case"BLOCKQUOTE":return Array.from(e.childNodes).map((e=>e.textContent)).join("\n");case"UL":return Array.from(e.children).map((e=>e.textContent)).join("\n");case"A":const{href:t,content:n}=Gemini.link(e.textContent);return`${t||e.href} ${n}`;case"PRE":return e.textContent;default:return e.innerHTML.replace(/<br>/g,"\n")}}static link(e=""){return/((?<href>[^\s]+\/\/[^\s]+)\s)?(?<content>.+)/.exec(e).groups}constructor(e){this.root=e}get lines(){return Array.from(this.root.children).filter((e=>["P","BLOCKQUOTE","A","PRE","UL","H1","H2","H3"].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/gemini;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)}get foreground(){return getComputedStyle(this.root).getPropertyValue("--foreground")}set foreground(e){return this.root.style.setProperty("--foreground",e)}get background(){return getComputedStyle(this.root).getPropertyValue("--background")}set background(e){return this.root.style.setProperty("--background",e)}get size(){return getComputedStyle(this.root).getPropertyValue("--font-size")}set size(e){return this.root.style.setProperty("--font-size",e)}get lineHeight(){return getComputedStyle(this.root).getPropertyValue("--line-height")}set lineHeight(e){return this.root.style.setProperty("--line-height",e)}get serif(){return getComputedStyle(this.root).getPropertyValue("--serif")}set serif(e){return this.root.style.setProperty("--serif",e)}get sans(){return getComputedStyle(this.root).getPropertyValue("--sans")}set sans(e){return this.root.style.setProperty("--sans",e)}get mono(){return getComputedStyle(this.root).getPropertyValue("--mono")}set mono(e){return this.root.style.setProperty("--mono",e)}}