gmi-web/gmi.js
2020-12-30 00:27:29 +00:00

143 lines
5.8 KiB
JavaScript

/* gmi.js is licensed under CC0 */
class Gemini {
static syntax = { P: "", A: "=>", UL: "*", BLOCKQUOTE: ">", PRE: "```", H1: "#", H2: "##", H3: "###", }
static line(line, type) {
if (typeof line === "string") line = {content: line, type: type || "P"}
let dom = Gemini.render(line).dom
return {
get dom() { return dom },
get type() { return this.dom.nodeName },
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) { 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.toUpperCase()) {
case "PRE":
return `${syntax}\n${content}\n${syntax}`
break
default:
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) },
}
}
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 || "P")
}
line.dom.contentEditable = line.editable || "inherit"
switch (line.type.toUpperCase()) {
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 "UL":
line.dom.innerHTML = line.content.split("\n").map(content => content.length > 0 ?
`<li>${content}</li>` : ""
).join("\n")
break
case "BLOCKQUOTE":
line.dom.innerHTML = line.content.split("\n").map(content =>
`<div>${content}</div>`
).join("\n")
break
case "PRE":
line.dom.textContent = line.content
break
default:
line.dom.innerHTML = line.content.replace(/\n+/g, "<br>")
}
return line
}
static contentFrom(dom) {
switch (dom.nodeName.toUpperCase()) {
case "BLOCKQUOTE":
return Array.from(dom.childNodes).map(child =>
child.textContent
).join("\n")
break
case "UL":
return Array.from(dom.children).map(child =>
child.textContent
).join("\n")
break
case "A":
const {href, content} = Gemini.link(dom.textContent)
return `${href || dom.href} ${content}`
break
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 editable() { return this.root.contentEditable === "true" }
set editable(value) {
this.root.contentEditable = value
this.lines.forEach(line => line.editable = value)
}
get lines() {
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") }
// 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/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)
}
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) }
}