gmi.js redesign
This commit is contained in:
parent
66c615d88a
commit
9d2bbfef0a
5
Makefile
5
Makefile
|
@ -1,3 +1,4 @@
|
||||||
build:
|
build:
|
||||||
cat gmi.css | minify --css > gmi.min.css
|
cat gmi.css | minify --css > min/gmi.min.css
|
||||||
cat gmi.js | minify --js > gmi.min.js
|
cat gmi.js | minify --js > min/gmi.min.js
|
||||||
|
cat gmi.editor.js | minify --js > min/gmi.editor.min.js
|
||||||
|
|
22
README.md
22
README.md
|
@ -1,23 +1,5 @@
|
||||||
# gmi-web
|
# gmi-web
|
||||||
## A bridge between Gemini and HTML
|
## A bridge between Gemini and HTML
|
||||||
|
### [gmi.css](https://talon.computer/css/)
|
||||||
### gmi.css
|
### [gmi.js](https://talon.computer/js/)
|
||||||
> rem based stylesheet for text-focused content inspired by Tachyons.
|
|
||||||
|
|
||||||
* readable
|
|
||||||
* predictable
|
|
||||||
* mobile friendly
|
|
||||||
|
|
||||||
live example/docs: [gmi.css](https://talon.computer/css/)
|
|
||||||
|
|
||||||
### gmi.js
|
|
||||||
> an optional extension to gmi.css
|
|
||||||
|
|
||||||
* light/dark mode
|
|
||||||
* click to copy pre blocks
|
|
||||||
* inline images, audio and video
|
|
||||||
* themeable
|
|
||||||
|
|
||||||
live example/docs: [gmi.js](https://talon.computer/js/)
|
|
||||||
|
|
||||||
![CC0](https://licensebuttons.net/p/zero/1.0/80x15.png)
|
![CC0](https://licensebuttons.net/p/zero/1.0/80x15.png)
|
||||||
|
|
52
docs/js.gmi
Normal file
52
docs/js.gmi
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# [WIP] gmi.js
|
||||||
|
## A bridge between the DOM and Gemini
|
||||||
|
|
||||||
|
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,
|
||||||
|
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.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 = [
|
||||||
|
Gemini.line({content: "interesting", type: Gemini.type.H1}),
|
||||||
|
Gemini.line({content: "that's convienient"}),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
> you can even work with the source directly
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
> or maybe a .gmi file would be easier?
|
||||||
|
```js
|
||||||
|
window.gmi.download()
|
||||||
|
```
|
||||||
|
|
||||||
|
=> /gmi.js view/download (minified version available via .min.js)
|
||||||
|
|
||||||
|
CC0
|
1
gmi.css
1
gmi.css
|
@ -83,6 +83,7 @@ a {
|
||||||
font-size: var(--font-size);
|
font-size: var(--font-size);
|
||||||
font-family: var(--serif);
|
font-family: var(--serif);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
li::before {
|
li::before {
|
||||||
|
|
17
gmi.editor.js
Normal file
17
gmi.editor.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/* gmi.editor.js is licensed under CCO */
|
||||||
|
class GeminiEditor {
|
||||||
|
constructor(gmi) {
|
||||||
|
this.gmi = gmi
|
||||||
|
}
|
||||||
|
|
||||||
|
static next(type) {
|
||||||
|
return function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const types = Object.values(Gemini.TYPE)
|
||||||
|
console.log(content)
|
||||||
|
let next = types.indexOf(type) + 1
|
||||||
|
next = next <= types.length - 1 ? types[next] : types[0]
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
381
gmi.js
381
gmi.js
|
@ -1,202 +1,203 @@
|
||||||
/* gmi.js is licensed under CC0 */
|
/* gmi.js is licensed under CC0 */
|
||||||
class Gemini {
|
class Gemini {
|
||||||
constructor({modes} = {modes: false}) {
|
static line(line = {}) {
|
||||||
this.modes = modes
|
let dom = line.nodeName ? line : document.createElement(line.type || Gemini.type.TEXT)
|
||||||
this.allThemes = {
|
if (line.editable) dom.contentEditable = line.editable
|
||||||
default: {
|
switch (line.type) {
|
||||||
icon: "🎨",
|
case Gemini.type.LINK:
|
||||||
colors: [
|
const {href, content} = Gemini.PARSE_LINK.exec(line.content).groups
|
||||||
getComputedStyle(document.documentElement).getPropertyValue(`--foreground`),
|
dom.href = line.href || href || content
|
||||||
getComputedStyle(document.documentElement).getPropertyValue(`--background`)
|
dom.innerHTML = Gemini.innerHTML(Object.assign(line, { content, href }))
|
||||||
],
|
break
|
||||||
|
default:
|
||||||
|
dom.innerHTML = line.innerHTML || Gemini.innerHTML(line)
|
||||||
|
}
|
||||||
|
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) {
|
||||||
if (this.modes) {
|
case Gemini.type.QUOTE:
|
||||||
css(`#toggleMode {
|
return Array.from(this.dom.childNodes).map(child => child.textContent).join("\n")
|
||||||
font-size: 1rem;
|
case Gemini.type.LIST:
|
||||||
cursor: pointer;
|
return Array.from(this.dom.children).map(child => child.textContent).join("\n")
|
||||||
position: fixed;
|
case Gemini.type.LINK:
|
||||||
top: 0.5rem;
|
const {href, content} = Gemini.PARSE_LINK.exec(this.dom.textContent).groups
|
||||||
right: 0.5rem;
|
return `${href || this.dom.href} ${content}`
|
||||||
padding: 0.3rem;
|
case Gemini.type.PREFORMATTED:
|
||||||
margin-top: 0.1rem;
|
return this.dom.textContent
|
||||||
border: none;
|
default:
|
||||||
color: var(--background);
|
return this.dom.innerHTML.replace(/<br>/g, "\n").replace(/\n?$/, "")
|
||||||
background-color: var(--foreground);
|
|
||||||
}`)
|
|
||||||
this.toggleMode = document.createElement("button")
|
|
||||||
this.toggleMode.id = "toggleMode"
|
|
||||||
this.toggleMode.textContent = this.mode
|
|
||||||
this.toggleMode.onclick = () => {
|
|
||||||
this.mode = this.mode === "light" ? "dark" : "light"
|
|
||||||
this.toggleMode.textContent = this.mode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
this.mode = this.mode
|
|
||||||
document.body.append(this.toggleMode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
set foreground(value) { return this.set("foreground", value) }
|
|
||||||
get(property) { return document.documentElement.style.getPropertyValue(`--${property}`) }
|
|
||||||
set(property, value) { return document.documentElement.style.setProperty(`--${property}`, value) }
|
|
||||||
get foreground() { return this.get("foreground") }
|
|
||||||
get background() { return this.get("background") }
|
|
||||||
set background(value) { return this.set("background", value) }
|
|
||||||
get fontSize() { return this.get("font-size") }
|
|
||||||
set fontSize(value) { return this.set("font-size", value) }
|
|
||||||
get mono() { return this.get("mono") }
|
|
||||||
set mono(value) { return this.set("mono", value) }
|
|
||||||
get serif() { return this.get("serif") }
|
|
||||||
set serif(value) { return this.set("serif", value) }
|
|
||||||
get sansSerif() { return this.get("sans-serif") }
|
|
||||||
set sansSerif(value) { return this.set("sans-serif", value) }
|
|
||||||
|
|
||||||
get mode() { return localStorage.getItem("mode") || "light" }
|
|
||||||
set mode(value) {
|
|
||||||
if (!/^(light|dark)$/.test(value))
|
|
||||||
throw Error("Invalid mode: use either 'light' or 'dark'")
|
|
||||||
localStorage.setItem("mode", value)
|
|
||||||
this.colors()
|
|
||||||
}
|
|
||||||
|
|
||||||
get theme() {
|
|
||||||
const theme = localStorage.getItem("theme")
|
|
||||||
return !theme || !this.allThemes[theme] ? "default" : theme
|
|
||||||
}
|
|
||||||
set theme(theme) {
|
|
||||||
if (this.themeSwitcher) this.themeSwitcher.textContent = this.allThemes[theme].icon
|
|
||||||
localStorage.setItem("theme", theme)
|
|
||||||
this.colors()
|
|
||||||
}
|
|
||||||
|
|
||||||
colors() {
|
|
||||||
const theme = this.allThemes[this.theme]
|
|
||||||
const foreground = theme.colors[0]
|
|
||||||
const background = theme.colors[1]
|
|
||||||
if (this.mode == "dark") {
|
|
||||||
this.foreground = background
|
|
||||||
this.background = foreground
|
|
||||||
} else {
|
|
||||||
this.foreground = foreground
|
|
||||||
this.background = background
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
themes(themes, mode) {
|
|
||||||
this.mode = mode || this.mode
|
|
||||||
this.allThemes = Object.assign(themes, this.allThemes)
|
|
||||||
|
|
||||||
css(`#themeSwitcher {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
position: fixed;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: ${this.modes ? "3.3rem" : "1rem"};
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
background: none;"
|
|
||||||
}`)
|
|
||||||
this.themeSwitcher = document.createElement("button")
|
|
||||||
this.themeSwitcher.id = "themeSwitcher"
|
|
||||||
this.themeSwitcher.onclick = () => {
|
|
||||||
const names = Object.keys(this.allThemes)
|
|
||||||
let next = names.indexOf(this.theme) + 1
|
|
||||||
next = next <= names.length - 1 ? next : 0
|
|
||||||
this.theme = names[next]
|
|
||||||
}
|
|
||||||
document.body.append(this.themeSwitcher)
|
|
||||||
|
|
||||||
this.theme = this.theme
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
clickToCopy() {
|
|
||||||
css(`pre:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
pre:hover::before {
|
|
||||||
content: "click to copy";
|
|
||||||
color: var(--foreground);
|
|
||||||
background-color: var(--background);
|
|
||||||
float: right;
|
|
||||||
}`)
|
|
||||||
for (const pre of document.getElementsByTagName('pre')) {
|
|
||||||
pre.onclick = () => {
|
|
||||||
navigator.clipboard.writeText(pre.textContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
inline({images, audio, video} = {images: false, audio: false, video: false}) {
|
|
||||||
if(images) {
|
|
||||||
css(`img {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
}`)
|
|
||||||
const IMAGE_EXTENSIONS = /\.(jpeg|jpg|png|gif|bmp|svg|ico|webp|tiff)$/
|
|
||||||
for (const link of document.getElementsByTagName('a')) {
|
|
||||||
if (IMAGE_EXTENSIONS.test(link.pathname)) {
|
|
||||||
const img = document.createElement("img")
|
|
||||||
img.src = link.href
|
|
||||||
img.title = link.textContent.replace(/\n$/, "")
|
|
||||||
link.append(img)
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
set content(value) {
|
||||||
|
switch (this.type) {
|
||||||
if (audio) {
|
case Gemini.type.LINK:
|
||||||
css(`audio {
|
const {href, content} = Gemini.PARSE_LINK.exec(value).groups
|
||||||
width: 100%;
|
this.dom.innerHTML = Gemini.innerHTML({ type: this.type, content, href })
|
||||||
display: block;
|
this.dom.href = href
|
||||||
}`)
|
break
|
||||||
const AUDIO_EXTENSIONS = /\.(mp3|ogg)$/
|
default:
|
||||||
for (const link of document.getElementsByTagName('a')) {
|
this.dom.innerHTML = Gemini.innerHTML({ type: this.type, content: value })
|
||||||
if (AUDIO_EXTENSIONS.test(link.pathname)) {
|
|
||||||
const audio = document.createElement("audio")
|
|
||||||
audio.src = link.href
|
|
||||||
audio.title = link.textContent.replace(/\n$/, "")
|
|
||||||
audio.controls = true
|
|
||||||
audio.currentTime = true
|
|
||||||
audio.preload = "none"
|
|
||||||
link.append(audio)
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
get gmi() {
|
||||||
|
switch (this.type) {
|
||||||
if (video) {
|
case Gemini.type.PREFORMATTED:
|
||||||
css(`video {
|
return Gemini.syntax[this.type] + `\n${this.content}\n` + Gemini.syntax[this.type]
|
||||||
width: 100%;
|
break
|
||||||
display: block;
|
default:
|
||||||
}`)
|
return this.content.split("\n").map(content => `${Gemini.syntax[this.type] !== "" ? Gemini.syntax[this.type] + " ": ""}${content}`).join("\n")
|
||||||
const VIDEO_EXTENSIONS = /\.(mp4|webm|ogv|mov)$/
|
|
||||||
for (const link of document.getElementsByTagName('a')) {
|
|
||||||
if (VIDEO_EXTENSIONS.test(link.pathname)) {
|
|
||||||
const video = document.createElement("video")
|
|
||||||
video.src = link.href
|
|
||||||
video.title = link.textContent.replace(/\n$/, "")
|
|
||||||
video.controls = true
|
|
||||||
video.currentTime = true
|
|
||||||
video.preload = "metadata"
|
|
||||||
link.append(video)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get lines() {
|
||||||
|
return Array.from(this.root.children)
|
||||||
|
.filter(el => Object.values(Gemini.type).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") }
|
||||||
|
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.style.display = 'none'
|
||||||
|
document.body.appendChild(el); el.click(); document.body.removeChild(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ? `<li>${content}</li>` : ""
|
||||||
|
).join("\n")
|
||||||
|
case Gemini.type.QUOTE:
|
||||||
|
return line.content.split("\n").map(content =>
|
||||||
|
`<div>${content}</div>`
|
||||||
|
).join("\n")
|
||||||
|
case Gemini.type.PREFORMATTED:
|
||||||
|
return line.content
|
||||||
|
default:
|
||||||
|
return line.content.replace(/\n+/g, "<br>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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()})
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
this.lines = parsed.map(Gemini.line)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function css(css) {
|
|
||||||
if (!this.style) {
|
|
||||||
this.style = document.createElement("style")
|
|
||||||
document.documentElement.append(this.style)
|
|
||||||
}
|
|
||||||
this.style.textContent = this.style.textContent + "\n" + css
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
1
gmi.min.js
vendored
1
gmi.min.js
vendored
|
@ -1 +0,0 @@
|
||||||
class Gemini{constructor({modes:e}={modes:!1}){this.modes=e,this.allThemes={default:{icon:"🎨",colors:[getComputedStyle(document.documentElement).getPropertyValue("--foreground"),getComputedStyle(document.documentElement).getPropertyValue("--background")]}},this.modes&&(css("#toggleMode {\n font-size: 1rem; \n cursor: pointer; \n position: fixed; \n top: 0.5rem; \n right: 0.5rem; \n padding: 0.3rem;\n margin-top: 0.1rem;\n border: none; \n color: var(--background);\n background-color: var(--foreground);\n }"),this.toggleMode=document.createElement("button"),this.toggleMode.id="toggleMode",this.toggleMode.textContent=this.mode,this.toggleMode.onclick=()=>{this.mode="light"===this.mode?"dark":"light",this.toggleMode.textContent=this.mode}),window.addEventListener("load",(()=>{this.mode=this.mode,document.body.append(this.toggleMode)}))}set foreground(e){return this.set("foreground",e)}get(e){return document.documentElement.style.getPropertyValue("--"+e)}set(e,t){return document.documentElement.style.setProperty("--"+e,t)}get foreground(){return this.get("foreground")}get background(){return this.get("background")}set background(e){return this.set("background",e)}get fontSize(){return this.get("font-size")}set fontSize(e){return this.set("font-size",e)}get mono(){return this.get("mono")}set mono(e){return this.set("mono",e)}get serif(){return this.get("serif")}set serif(e){return this.set("serif",e)}get sansSerif(){return this.get("sans-serif")}set sansSerif(e){return this.set("sans-serif",e)}get mode(){return localStorage.getItem("mode")||"light"}set mode(e){if(!/^(light|dark)$/.test(e))throw Error("Invalid mode: use either 'light' or 'dark'");localStorage.setItem("mode",e),this.colors()}get theme(){const e=localStorage.getItem("theme");return e&&this.allThemes[e]?e:"default"}set theme(e){this.themeSwitcher&&(this.themeSwitcher.textContent=this.allThemes[e].icon),localStorage.setItem("theme",e),this.colors()}colors(){const e=this.allThemes[this.theme],t=e.colors[0],o=e.colors[1];"dark"==this.mode?(this.foreground=o,this.background=t):(this.foreground=t,this.background=o)}themes(e,t){return this.mode=t||this.mode,this.allThemes=Object.assign(e,this.allThemes),css(`#themeSwitcher {\n font-size: 1.5rem; \n position: fixed; \n top: 0.5rem; \n right: ${this.modes?"3.3rem":"1rem"}; \n cursor: pointer; \n border: none; \n background: none;"\n }`),this.themeSwitcher=document.createElement("button"),this.themeSwitcher.id="themeSwitcher",this.themeSwitcher.onclick=()=>{const e=Object.keys(this.allThemes);let t=e.indexOf(this.theme)+1;t=t<=e.length-1?t:0,this.theme=e[t]},document.body.append(this.themeSwitcher),this.theme=this.theme,this}clickToCopy(){css('pre:hover {\n cursor: pointer;\n }\n pre:hover::before {\n content: "click to copy";\n color: var(--foreground);\n background-color: var(--background);\n float: right;\n }');for(const e of document.getElementsByTagName("pre"))e.onclick=()=>{navigator.clipboard.writeText(e.textContent)};return this}inline({images:e,audio:t,video:o}={images:!1,audio:!1,video:!1}){if(e){css("img {\n display: block;\n max-width: 100%;\n }");const e=/\.(jpeg|jpg|png|gif|bmp|svg|ico|webp|tiff)$/;for(const t of document.getElementsByTagName("a"))if(e.test(t.pathname)){const e=document.createElement("img");e.src=t.href,e.title=t.textContent.replace(/\n$/,""),t.append(e)}}if(t){css("audio {\n width: 100%;\n display: block;\n }");const e=/\.(mp3|ogg)$/;for(const t of document.getElementsByTagName("a"))if(e.test(t.pathname)){const e=document.createElement("audio");e.src=t.href,e.title=t.textContent.replace(/\n$/,""),e.controls=!0,e.currentTime=!0,e.preload="none",t.append(e)}}if(o){css("video {\n width: 100%;\n display: block;\n }");const e=/\.(mp4|webm|ogv|mov)$/;for(const t of document.getElementsByTagName("a"))if(e.test(t.pathname)){const e=document.createElement("video");e.src=t.href,e.title=t.textContent.replace(/\n$/,""),e.controls=!0,e.currentTime=!0,e.preload="metadata",t.append(e)}}return this}}function css(e){return this.style||(this.style=document.createElement("style"),document.documentElement.append(this.style)),this.style.textContent=this.style.textContent+"\n"+e,this}
|
|
66
js.gmi
66
js.gmi
|
@ -1,66 +0,0 @@
|
||||||
# gmi.js
|
|
||||||
## an optional extension to gmi.css
|
|
||||||
=> /css/ gmi.css
|
|
||||||
|
|
||||||
* light/dark mode
|
|
||||||
* click to copy pre blocks
|
|
||||||
* inline images, audio and video
|
|
||||||
* themeable
|
|
||||||
|
|
||||||
```
|
|
||||||
<meta name="color-scheme" content="dark light">
|
|
||||||
<link rel="stylesheet" href="https://talon.computer/gmi.min.css"/>
|
|
||||||
<script src="https://talon.computer/gmi.min.js"></script>
|
|
||||||
<script>
|
|
||||||
window.gmi = new Gemini({ modes: true })
|
|
||||||
window.onload = () => window.gmi
|
|
||||||
.clickToCopy()
|
|
||||||
.inline({ images: true, audio: true, video: true })
|
|
||||||
.themes({
|
|
||||||
book: {
|
|
||||||
icon: "📖",
|
|
||||||
colors: ["#555555", "#fffceb"]
|
|
||||||
},
|
|
||||||
rose: {
|
|
||||||
icon: "🌹",
|
|
||||||
colors: ["#555555", "#ffdfdf"]
|
|
||||||
},
|
|
||||||
plant: {
|
|
||||||
icon: "🌱",
|
|
||||||
colors: ["#555555", "#9eebcf"]
|
|
||||||
},
|
|
||||||
ocean: {
|
|
||||||
icon: "🌊",
|
|
||||||
colors: ["#555555", "#96ccff"]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
By default gmi.js will determine your theme based on the --foreground and --background CSS variables. You can supply your own themes using the object format shown above. I recommend using readable color palettes.
|
|
||||||
=> https://tachyons.io/docs/themes/skins/ Tachyons Color Schemes
|
|
||||||
|
|
||||||
### Download gmi.js today!
|
|
||||||
=> //talon.computer/gmi.js gmi.js
|
|
||||||
=> //talon.computer/gmi.min.js gmi.min.js
|
|
||||||
|
|
||||||
# inline examples
|
|
||||||
## images
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/09/Sample-ICO-file-Download-for-testing.ico ICO
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/07/sample-jpg-file-for-testing.jpg JPG
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/08/Sample-PNG-File-for-Testing.png PNG
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/08/Sample-SVG-Image-File-Download.svg SVG
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/08/sample-webp-file-for-testing.webp WEBP
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/09/Sample-gif-Image-File-Download.gif GIF
|
|
||||||
|
|
||||||
## audio
|
|
||||||
=> https://learn.esperanto.com/assets/ogg/01.ogg OGG
|
|
||||||
=> https://learn.esperanto.com/assets/mp3/01.mp3 MP3
|
|
||||||
|
|
||||||
## video
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4 MP4
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mov-file.mov MOV
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-webm-file.webm WEBM
|
|
||||||
=> https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-ogv-file.ogv OGV
|
|
||||||
|
|
||||||
CC0
|
|
1
min/gmi.editor.min.js
vendored
Normal file
1
min/gmi.editor.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
class GeminiEditor{constructor(t){this.gmi=t}static next(t){return function(e){e.preventDefault();const n=Object.values(Gemini.TYPE);console.log(content);let i=n.indexOf(t)+1;return i=i<=n.length-1?n[i]:n[0],i}}}
|
1
min/gmi.min.css
vendored
Normal file
1
min/gmi.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*{margin:0;padding:0;overflow-wrap:anywhere}:root{--foreground:black;--background:white;--line-height:1.5;--font-size:1.25rem;--mono:Consolas,monaco,monospace;--serif:font-family:georgia,times,serif;--sans-serif:-apple-system,BlinkMacSystemFont,'avenir next',avenir,helvetica,'helvetica neue',ubuntu,roboto,noto,'segoe ui',arial,sans-serif}body{max-width:48rem;background-color:var(--background);padding:.5rem;margin:0 auto}h1,h2,h3{font-family:var(--sans-serif);line-height:1.25}h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.5rem}p{font-size:var(--font-size);font-family:var(--serif);line-height:var(--line-height)}a,blockquote,h1,h2,h3,p,ul{color:var(--foreground);background-color:var(--background)}br{line-height:1}a::before{font-size:var(--font-size);font-family:var(--mono);content:"⇒";padding-right:.25rem;vertical-align:middle}a:hover{color:var(--background);background-color:var(--foreground)}a{font-size:var(--font-size);font-family:var(--serif);text-decoration:none;display:block}li::before{font-size:var(--font-size);font-family:var(--mono);content:"*";vertical-align:middle;padding-right:.5rem}ul{font-size:var(--font-size);font-family:var(--serif);line-height:1.25;list-style-type:none}blockquote{font-size:var(--font-size);font-family:var(--serif);line-height:var(--line-height);border-left:.5rem solid var(--foreground);padding-left:.75rem}pre{font-size:1rem;font-family:var(--mono);line-height:1;color:var(--background);background-color:var(--foreground);padding:1.25rem;overflow-y:auto}pre+blockquote{padding-top:.5rem;padding-bottom:.5rem}::-moz-selection,::selection{color:var(--background);background-color:var(--foreground)}pre::-moz-selection,pre::selection{color:var(--foreground);background-color:var(--background)}
|
1
min/gmi.min.js
vendored
Normal file
1
min/gmi.min.js
vendored
Normal file
|
@ -0,0 +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(/<br>/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?`<li>${e}</li>`:"")).join("\n");case Gemini.type.QUOTE:return e.content.split("\n").map((e=>`<div>${e}</div>`)).join("\n");case Gemini.type.PREFORMATTED:return e.content;default:return e.content.replace(/\n+/g,"<br>")}}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=/((?<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}),{})}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)}}}
|
Loading…
Reference in a new issue