464 lines
14 KiB
JavaScript
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)
|
|
}
|
|
}
|
|
}
|