zvava.org/wiki.js

464 lines
14 KiB
JavaScript

import * as std from "std"
import * as os from "os"
function canRunExec() {
return os.platform != "win32" && os.platform != "js"
}
function stringifyDate(date) {
let d = new Date(date)
let month = (d.getUTCMonth() + 1).toString()
if (month.length == 1) month = "0" + month
let day = d.getUTCDate().toString()
if (day.length == 1) day = "0" + day
return `${d.getUTCFullYear()}/${month}/${day}`
}
function prependRelevantEmoji(x) {
let e = categories[x] || categories["unknown"] || ""
return e.length > 0 ? e + " " + x : x
}
function readDir(path, reject = (e) => { throw new Error(e) }, resolve = r => r) {
let [ files, err ] = os.readdir(path)
if (err) return reject(err)
files = files.filter(x => x != "." && x != "..") // remove relative directories
// check subdirectories
let subdirs = []
files.forEach(d => readDir(path + "/" + d, e => {}, sd => {
subdirs.push(d)
sd.forEach(f => files.push(d + "/" + f))
}))
return resolve(files.filter(x => !subdirs.includes(x))) // remove subdirectories
}
function readFile(path, reject = (e) => { throw new Error(e) }, resolve = r => r) {
let _f = std.open(path, "r")
if (_f == null || _f.error()) return reject("error reading " + path)
let data = _f.readAsString().replace(/\r/g, "") // eliminate crlf
if (path.endsWith(".json")) data = JSON.parse(data) // parse json if necessary
_f.close()
return resolve(data)
}
const categories = readFile("src/templates/categories.json")
main()
// ...
function main() {
scriptArgs.shift()
switch (scriptArgs[0]) {
case "publish": return publish()
case "make": return make()
case "list": return list()
case "new": return newPage()
case "edit": return edit()
case "rm": return rmPage()
default:
print(
"usage: qjs wiki.js command args\n\n" +
"commands:\n" +
" list list all wiki pages\n" +
" new create a new wiki page\n" +
" edit edit a wiki page\n" +
" rm delete wiki pages\n" +
" make run make script\n\n" +
" publish run publish script\n\n" +
"each command has it's own arguments, to list use -h\n" +
"")
}
}
function publish() {
if (scriptArgs.includes("-h") || scriptArgs.includes("--help"))
return print(
"usage: qjs wiki.js publish\n\n" +
"literally just runs the publish script for current platform\n" +
"")
if (canRunExec())
return os.exec(os.platform == "win32"
? ["cmd", "scripts\\publish.bat"]
: ["sh", "scripts/publish.sh"])
print(`publish: cannot run on this platform (${os.platform})`)
}
function make() {
if (scriptArgs.includes("-h") || scriptArgs.includes("--help"))
return print(
"usage: qjs wiki.js make\n\n" +
"literally just runs the make script\n" +
"")
if (canRunExec())
return os.exec(["qjs", "make.js"])
print(`publish: cannot run on this platform (${os.platform})`)
}
function list() {
let args = (scriptArgs[1] || "")
if (args.includes("h"))
return print(
"usage: qjs wiki.js list [-hlLcsTIEAMVHS]\n\n" +
" -\t noop\n" +
" h\t display usage information\n" +
" t\t show TODOs\n" +
" a\t show local links\n" +
" d\t show dead local links\n" +
" l\t list categories of pages as well\n" +
" p\t add padding to the list :) \n" +
" c\t sort by created date instead of modified date\n" +
"\ncategories: s stub (no category)\n" +
" T text I info\n" +
" E event A art\n" +
" M music V video\n" +
" H hardware S software\n" +
"")
let err, pages = readDir("src/wiki", e => err = e)
if (err) return print(err)
let output = []
pages.map(x => x.replace(/\.\w*?$/, "")).forEach(_page => new Promise((resolve, reject) => {
let page = _page.replace(/^[\w\/]*\//, "")
let data = readFile("src/wiki/" + _page + ".gmi")
let metadata = { page: page, content: data, todo: [] }
// extract title
metadata["title"] = data.substring(2, data.indexOf("\n"))
// extract metadata
let metaStart = data.indexOf("```") + 4
let metaEnd = data.indexOf("```", metaStart) - 1
data.substring(metaStart, metaEnd)
.split(/\n+/)
.map(x => x.split(/\s+/))
.forEach((x) => {
let property = x.shift()
metadata[property] = property == "category" ? x.map(y => y.replace(",", "")) : x.join(" ")
})
// check for todos
if (args.includes("t")) {
metadata.content.split("\n").forEach((x, i) => {
if (!x.includes("TODO"))
return false
metadata.todo.push([i, x.replace(/.*todo: */i, "")])
})
}
// ensure there is a modified field
if (!metadata["modified"])
metadata["modified"] = metadata["created"]
// gather links if dead links option is enabled
if (args.includes("a") || args.includes("d")) {
metadata["links"] = data.split(/\n+/)
.filter(x => x.startsWith("=>"))
.map(x => x.split(/\s+/).slice(1))
.filter(x => x[0].startsWith("/"))
if (args.includes("d")) {
metadata["links"] = metadata["links"] // remove media links
.filter(x => !(x[0].startsWith("/media") || x[0].startsWith("/wiki/category") || (x[0].indexOf("/", 1) == -1) || x[0] == "/relative/link.gmi"))
// remove dead links
.filter(x => {
let a = x[0].replace(".ln", ".gmi").replace(/^[\w\/]*\//, "")
let p = x[0].startsWith("/wiki/") ? "/wiki/" + pages.find(p => p.includes(a)) : x[0]
let [, err] = os.stat("src" + p)
return err != 0})
}
}
output.push(metadata)
if (output.length == pages.length) {
let sortBy = (scriptArgs[1] || "").includes("c") ? "created" : "modified"
output = output
// stub filter
.filter(x => (args.includes("s") && x.category[0] == "stub") || !args.includes("s"))
// apply filters
.filter(x => !/[TIEAMVHS]/.test(args) ||
(args.includes("T") && x.category.includes("text")) ||
(args.includes("I") && x.category.includes("info")) ||
(args.includes("E") && x.category.includes("event")) ||
(args.includes("A") && x.category.includes("art")) ||
(args.includes("M") && x.category.includes("music")) ||
(args.includes("V") && x.category.includes("video")) ||
(args.includes("H") && x.category.includes("hardware")) ||
(args.includes("S") && x.category.includes("software")))
// todo + dead link filter
.filter(x => (args.includes("t") && x.todo.length > 0) || !args.includes("t"))
.filter(x => (args.includes("d") && x.links.length > 0) || !args.includes("d"))
// sort
.sort((a, b) => {
let dateA = new Date(a[sortBy].replace(/\//g, "-"))
let dateB = new Date(b[sortBy].replace(/\//g, "-"))
return canRunExec() ? dateA - dateB : dateB - dateA
})
// render
.map(page => {
let dateA = sortBy == "created" ? page.created : page.modified
let dateB = sortBy == "created" ? page.modified : page.created
let out = `${dateA} \x1b[90m${dateB}\x1b[m ${page.title}`
if (args.includes("l")) { // detailed list
let emojis = page.category
.map(x => prependRelevantEmoji(x).split(" ")[0])
.join(" ")
out = std.sprintf("%s\n%-21s \x1b[90m│\x1b[m %s \x1b[90m%s\x1b[m",
out, page.category.join(", "), emojis, page.page)
}
if (args.includes("t")) { // todos
page.todo.forEach(t => {
out += std.sprintf("\n%26s \x1b[90m·\x1b[m %s\x1b[m",
"\x1b[34mTODO @ " + t[0], t[1])
})
}
if (page.links) { // links
page.links.forEach(l => {
out += "\n \x1b[90m⇒\x1b[m " + l[0]
})
}
return out
})
print(output.join(args.includes("p")
? "\n\x1b[90m──────────────────────┤\x1b[m\n" // cozy list
: "\n")) // regular list
}
resolve()
}))
}
function edit() {
let args = scriptArgs.slice(1)
if (args.length == 0 || args.includes("-h") || args.includes("--help"))
return print(
"usage: qjs wiki.js edit [arg] page\n\n" +
"argument:\n" +
" (no argument) open the page in nano\n" +
" -h --help display usage information\n" +
" -m --modify set modified date to today's date\n" +
"")
if (args.length == 1 && (args[0] || "").startsWith("-"))
return print("wiki.js: page titles cannot start with a hyphen\ntip: qjs wiki.js edit --help")
let page = args.pop()
let arg = args.shift()
let path = "src/wiki/" + page + ".gmi"
switch (arg) {
case "-m": case "--modify": try {
// read file
let read = std.open(path, "r")
if (read.error()) throw "error reading " + page + ".gmi"
let data = read.readAsString().replace(/\r/g, "") // windows newline =[
read.close()
let makePos = data.indexOf("created")
let modPos = data.indexOf("modified", makePos)
// if a modified field exists
let modDefined = modPos > -1 && modPos - 20 == makePos
if (modDefined) { // change modified field
data = data.replace(/modified ....\/..\/../, "modified " + stringifyDate(new Date()))
} else { // create modified field
data = data.replace(/(created ....\/..\/..)/, "$1\nmodified " + stringifyDate(new Date()))
}
// write file
let write = std.open(path, "w")
if (write.error()) throw "error writing " + page + ".gmi"
write.puts(data)
write.close()
print("\x1b[90m->\x1b[0m updated", page + ".gmi")
} catch (e) {
print("wiki.js:", e)
} break
default:
os.exec(["nano", path])
}
}
function newPage() {
let args = scriptArgs.slice(1)
if (args.length == 0 || args.includes("-h") || args.includes("--help"))
return print(
"usage: qjs wiki.js new page [args...]\n\n" +
"arguments:\n" +
" -h --help display usage information\n" +
" -f --force don't abort if page already exists\n" +
" -t --title set title\n" +
" -C --created set created date\n" +
" -m --modified set modified date\n" +
" -T --thumb set thumbnail\n" +
" -c --category add a category to the page \n" +
" -h1 --header add large header to the page \n" +
" -h2 --sub add medium header to the page \n" +
" -h3 --subsub add small header to the page \n" +
" -p --text add a paragraph to the page \n" +
" -q --quote add a block quote to the page \n" +
" -l --link add a link to the page\n" +
" -i --image add an image to the page\n" +
" -a --alt set alt text of previous image/link\n"+
"\ndate format `YYYY/MM/DD`\n" +
"thumb format /images/t/`thumb`.png\n" +
"local link format `/wiki/page.ln`\n" +
"\ncategories:\n" +
" + text + info\n" +
" + event + art\n" +
" + music + video\n" +
" + hardware + software\n" +
"")
if ((args[0] || "").startsWith("-"))
return print("page url cannot start with a hyphen")
let _date = new Date()
let page = {
page: args.shift(),
title: "untitled",
created: stringifyDate(_date),
modified: stringifyDate(_date),
thumbnail: undefined,
thumbnailAlt: "thumbnail",
category: [],
body: [],
}
let force = false
while (args.length > 0) {
let arg = args.shift()
let value = args.shift()
switch (arg) {
case "-f": case "--force": force = true; args.unshift(value); break
case "-t": case "--title":
if (value) page.title = value; break
case "-C": case "--created":
if (value) page.created = value; break
case "-m": case "--modified":
if (value) page.modified = value; break
case "-T": case "--thumb":
if (value) page.thumbnail = value; break
case "-c": case "--category":
if (value) page.category.unshift(value); break
case "-h1": case "--header":
if (value) page.body.unshift({ node: "h1", text: value }); break
case "-h2": case "--sub":
if (value) page.body.unshift({ node: "h2", text: value }); break
case "-h3": case "--subsub":
if (value) page.body.unshift({ node: "h3", text: value }); break
case "-p": case "--text":
if (value) page.body.unshift({ node: "text", text: value }); break
case "-q": case "--quote":
if (value) page.body.unshift({ node: "quote", text: value }); break
case "-l": case "--link":
if (value) page.body.unshift({ node: "link", path: value, text: "" }); break
case "-i": case "--image":
if (value) page.body.unshift({ node: "image", path: "/images/" + value, text: "" }); break
case "-a": case "--alt":
if (!value) break
let i = page.body.findIndex(x => x.node == "link" || x.node == "image")
if (i == -1)
page.thumbnailAlt = value
else
page.body[i].text = value
break
}
}
let output = "# " + page.title + "\n"
if (page.thumbnail !== undefined)
output += `=> /images/t/${page.thumbnail}.png ${page.thumbnailAlt}\n`
// metadata
// created
output += "```\ncreated " + page.created + "\n"
// modified
if (page.created != page.modified) output += "modified " + page.modified + "\n"
// category
if (page.category.length == 0) page.category.push("stub")
output += `category ${page.category.join(", ")}\n`
//
output += "```\n"
// body
page.body.reverse().forEach(element => { switch(element.node) {
case "h1": output += "\n# " + element.text + "\n"; break
case "h2": output += "\n## " + element.text + "\n"; break
case "h3": output += "\n### " + element.text + "\n"; break
case "text": output += "\n" + element.text + "\n"; break
case "quote": output += "\n> " + element.text + "\n"; break
case "link": output += `\n=> ${element.path} ${element.text}\n`; break
case "image": output += `\n=> ${element.path} ${element.text}\n`; break
}})
// write to file
let path = "src/wiki/" + page.page + ".gmi"
if (force || os.stat(path)[1] != 0) {
let f = std.open(path, "w")
if (f.error()) {
print("\x1b[90m->\x1b[0m error creating", page.page + ".gmi")
} else {
f.puts(output)
print("\x1b[90m->\x1b[0m created", page.page + ".gmi")
}
} else {
print("wiki.js: page", "src/wiki/" + path, "already exists\ntip: you can --force this action")
}
}
function rmPage() {
let args = scriptArgs.slice(1)
if (args.length == 0|| args.includes("-h") || args.includes("--help"))
return print(
"usage: qjs wiki.js rm [-h] [pages]\n\n" +
" -h --help display usage information\n" +
" -y --yes confirm deletion\n" +
"")
let confirmed = args.includes("-y") || args.includes("--yes")
while (args.length > 0) {
let page = args.shift()
if (page.startsWith("-")) continue
let path = "src/wiki/" + page + ".gmi"
if (confirmed) {
let rm = os.remove(path)
print (path, "\x1b[90m--\x1b[0m", rm == 0 ? "deleted" : "file not exist")
} else {
print("\x1b[90mrm\x1b[0m", path)
}
}
}