143 lines
5.8 KiB
JavaScript
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) }
|
|
}
|