235 lines
6.4 KiB
JavaScript
235 lines
6.4 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);
|
|
}
|
|
}
|