@kaathewise/ssg 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +24 -0
- package/src/config.js +43 -0
- package/src/djot.js +91 -0
- package/src/index.js +12 -0
- package/src/page.js +73 -0
- package/src/proc.js +56 -0
- package/src/render.js +42 -0
- package/src/tailwind.js +32 -0
- package/src/typst.js +59 -0
- package/src/util.js +48 -0
- package/src/vite.js +34 -0
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaathewise/ssg",
|
|
3
|
+
"description": "Djot, Typst & Vite components static site generator",
|
|
4
|
+
"version": "0.1.0-alpha.0",
|
|
5
|
+
"license": "MPL",
|
|
6
|
+
"author": "Andrej Kolčin",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"lint": "biome check --fix --unsafe",
|
|
10
|
+
"check": "biome check",
|
|
11
|
+
"smoke": "cd examples/basic/; node ../../src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@djot/djot": "^0.3.2",
|
|
15
|
+
"@tailwindcss/postcss": "^4.1.11",
|
|
16
|
+
"chokidar": "^4.0.3",
|
|
17
|
+
"handlebars": "^4.7.8",
|
|
18
|
+
"postcss": "^8.5.6",
|
|
19
|
+
"vite": "^7.0.2"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src/"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import process from "node:process"
|
|
4
|
+
|
|
5
|
+
import { find_dir_with_file } from "./util.js"
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
root: undefined,
|
|
9
|
+
|
|
10
|
+
async init() {
|
|
11
|
+
const [root, config_path] = await find_dir_with_file(
|
|
12
|
+
process.cwd(),
|
|
13
|
+
"ssg.config.js",
|
|
14
|
+
)
|
|
15
|
+
if (config_path === null) {
|
|
16
|
+
throw new Error("No config file")
|
|
17
|
+
}
|
|
18
|
+
const module = await import(config_path)
|
|
19
|
+
this.options = module.default
|
|
20
|
+
|
|
21
|
+
this.root = path.resolve(root)
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
get pages() {
|
|
25
|
+
return path.join(this.root, "pages/")
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
get target() {
|
|
29
|
+
return path.join(this.root, "target/")
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
get tree() {
|
|
33
|
+
return path.join(this.target, "tree/")
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
get assets() {
|
|
37
|
+
return path.join(this.tree, "assets/")
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
get gen_assets() {
|
|
41
|
+
return path.join(this.assets, "generated/")
|
|
42
|
+
},
|
|
43
|
+
}
|
package/src/djot.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import djot from "@djot/djot"
|
|
4
|
+
|
|
5
|
+
import config from "./config.js"
|
|
6
|
+
import { CACHE } from "./typst.js"
|
|
7
|
+
|
|
8
|
+
export function parse(src) {
|
|
9
|
+
return djot.parse(src)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function metadata(doc) {
|
|
13
|
+
const block = doc.children[0]
|
|
14
|
+
|
|
15
|
+
if (block.tag === "raw_block" && block.format === "metadata") {
|
|
16
|
+
return JSON.parse(block.text)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function render(doc) {
|
|
23
|
+
await apply_filter(doc, typst_filter)
|
|
24
|
+
await apply_filter(doc, classes_filter)
|
|
25
|
+
|
|
26
|
+
return `<!doctype html>\n ${djot.renderHTML(doc)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function apply_filter(element, filter) {
|
|
30
|
+
if (element.children) {
|
|
31
|
+
for (let i = 0; i < element.children.length; i += 1) {
|
|
32
|
+
const result = await apply_filter(
|
|
33
|
+
element.children[i],
|
|
34
|
+
filter,
|
|
35
|
+
)
|
|
36
|
+
if (result) {
|
|
37
|
+
element.children[i] = result
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (filter instanceof Function) {
|
|
43
|
+
return await filter(element)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const [tag, func] of Object.entries(filter)) {
|
|
47
|
+
if (tag === element.tag) {
|
|
48
|
+
return await func(element)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function apply_func(element, func) {
|
|
54
|
+
const output = await func(element)
|
|
55
|
+
if (output) {
|
|
56
|
+
for (const [key, value] of Object.entries(output)) {
|
|
57
|
+
element[key] = value
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const typst_filter = {
|
|
63
|
+
inline_math: async el => {
|
|
64
|
+
const formula = el.text
|
|
65
|
+
const path = await CACHE.insert(formula)
|
|
66
|
+
return {
|
|
67
|
+
tag: "raw_inline",
|
|
68
|
+
format: "html",
|
|
69
|
+
text: `<img src="/${path}" class=math-inline>`,
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
display_math: async el => {
|
|
74
|
+
const formula = el.text
|
|
75
|
+
const path = await CACHE.insert(formula)
|
|
76
|
+
return {
|
|
77
|
+
tag: "raw_inline",
|
|
78
|
+
format: "html",
|
|
79
|
+
text: `<p class=math-container><img src="/${path}" class=math-display></p>`,
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function classes_filter(element) {
|
|
85
|
+
if (element.attributes?.class) {
|
|
86
|
+
await fs.appendFile(
|
|
87
|
+
path.join(config.target, "classes"),
|
|
88
|
+
`${element.attributes.class}\n`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fork } from "node:child_process"
|
|
2
|
+
import { promises as fs } from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
import Proc from "./proc.js"
|
|
6
|
+
|
|
7
|
+
import config from "./config.js"
|
|
8
|
+
await config.init()
|
|
9
|
+
|
|
10
|
+
const vite = Proc.launch("vite.js")
|
|
11
|
+
const tailwind = Proc.launch("tailwind.js")
|
|
12
|
+
const pages = Proc.launch("render.js")
|
package/src/page.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import Handlebars from "handlebars"
|
|
4
|
+
|
|
5
|
+
import config from "./config.js"
|
|
6
|
+
import * as djot from "./djot.js"
|
|
7
|
+
import { find_dir_with_file } from "./util.js"
|
|
8
|
+
|
|
9
|
+
export class Page {
|
|
10
|
+
constructor(src) {
|
|
11
|
+
this.src = src
|
|
12
|
+
|
|
13
|
+
const parsed = path.parse(path.relative("pages/", src))
|
|
14
|
+
parsed.base = null // it overrules ext and name
|
|
15
|
+
|
|
16
|
+
if (parsed.ext === ".dj") {
|
|
17
|
+
this.type = "djot"
|
|
18
|
+
|
|
19
|
+
if (parsed.name === "index") {
|
|
20
|
+
parsed.ext = "html"
|
|
21
|
+
} else {
|
|
22
|
+
parsed.ext = null
|
|
23
|
+
}
|
|
24
|
+
} else if (parsed.ext === ".html") {
|
|
25
|
+
this.type = "html"
|
|
26
|
+
// left as-is
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`The file must be HTML or Djot, got ${src}`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
this.dst = path.join(config.tree, path.format(parsed))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async raw() {
|
|
36
|
+
return fs.readFile(this.src, "utf8")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async template() {
|
|
40
|
+
const [_, template_path] = await find_dir_with_file(
|
|
41
|
+
path.dirname(this.src),
|
|
42
|
+
"template.html.hbs",
|
|
43
|
+
)
|
|
44
|
+
const template_raw = await fs.readFile(template_path, "utf8")
|
|
45
|
+
const template = Handlebars.compile(template_raw)
|
|
46
|
+
return template
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async render() {
|
|
50
|
+
const raw = await this.raw()
|
|
51
|
+
|
|
52
|
+
switch (this.type) {
|
|
53
|
+
case "djot": {
|
|
54
|
+
return await this.#render_djot(raw)
|
|
55
|
+
}
|
|
56
|
+
case "html":
|
|
57
|
+
return raw
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async #render_djot(raw) {
|
|
62
|
+
const template = await this.template()
|
|
63
|
+
const doc = djot.parse(raw)
|
|
64
|
+
const metadata = djot.metadata(doc)
|
|
65
|
+
const body = await djot.render(doc)
|
|
66
|
+
return template({ body, metadata })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async write() {
|
|
70
|
+
await fs.mkdir(path.dirname(this.dst), { recursive: true })
|
|
71
|
+
await fs.writeFile(this.dst, await this.render())
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/proc.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Main process behavior handling
|
|
2
|
+
|
|
3
|
+
import { fork } from "node:child_process"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import readline from "node:readline"
|
|
6
|
+
|
|
7
|
+
import { dirname } from "./util.js"
|
|
8
|
+
|
|
9
|
+
function is_shutdown_key(key) {
|
|
10
|
+
if (!key) {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return key.name.toLowerCase() === "q" || (key.name === "c" && key.ctrl)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Proc = {
|
|
18
|
+
children: [],
|
|
19
|
+
|
|
20
|
+
init() {
|
|
21
|
+
readline.emitKeypressEvents(process.stdin)
|
|
22
|
+
process.stdin.setRawMode(true)
|
|
23
|
+
|
|
24
|
+
process.stdin.on("keypress", (chunk, key) => {
|
|
25
|
+
if (is_shutdown_key(key)) {
|
|
26
|
+
this.shutdown()
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
process.stdin.on("data", key => {
|
|
31
|
+
if (key === "q" || key === "Q") {
|
|
32
|
+
this.shutdown()
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
process.on("SIGINT", () => this.shutdown())
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
launch(name) {
|
|
40
|
+
const location = path.join(dirname(), name)
|
|
41
|
+
const child = fork(location)
|
|
42
|
+
this.children.push(child)
|
|
43
|
+
return child
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
shutdown() {
|
|
47
|
+
for (const child of this.children) {
|
|
48
|
+
child.kill()
|
|
49
|
+
}
|
|
50
|
+
process.exit()
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Proc.init()
|
|
55
|
+
|
|
56
|
+
export default Proc
|
package/src/render.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import chokidar from "chokidar"
|
|
4
|
+
|
|
5
|
+
import config from "./config.js"
|
|
6
|
+
import { Page } from "./page.js"
|
|
7
|
+
import { silence_warning } from "./util.js"
|
|
8
|
+
|
|
9
|
+
async function update(file) {
|
|
10
|
+
const page = new Page(file)
|
|
11
|
+
await page.write()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function* pages() {
|
|
15
|
+
const pattern = path.join(config.pages, "**/*.dj")
|
|
16
|
+
const files = fs.glob(pattern)
|
|
17
|
+
|
|
18
|
+
for await (const djot_path of files) {
|
|
19
|
+
yield new Page(djot_path)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function update_all() {
|
|
24
|
+
for await (const page of pages()) {
|
|
25
|
+
await page.write()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
silence_warning("ExperimentalWarning")
|
|
30
|
+
await config.init()
|
|
31
|
+
await fs.rm(config.target, { recursive: true, force: true })
|
|
32
|
+
await update_all()
|
|
33
|
+
|
|
34
|
+
const watcher = chokidar.watch("pages/", {
|
|
35
|
+
persistent: true,
|
|
36
|
+
ignoreInitial: true,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
watcher.on("all", async (event, file) => {
|
|
40
|
+
await update(file)
|
|
41
|
+
console.log(`${file} updated`)
|
|
42
|
+
})
|
package/src/tailwind.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import process from "node:process"
|
|
4
|
+
import tailwindcss from "@tailwindcss/postcss"
|
|
5
|
+
import chokidar from "chokidar"
|
|
6
|
+
import postcss from "postcss"
|
|
7
|
+
|
|
8
|
+
import config from "./config.js"
|
|
9
|
+
|
|
10
|
+
async function run() {
|
|
11
|
+
const css = await fs.readFile("style.css", "utf8")
|
|
12
|
+
const result = postcss([tailwindcss]).process(css, {
|
|
13
|
+
from: "style.css",
|
|
14
|
+
to: path.join(config.gen_assets, "style.css"),
|
|
15
|
+
})
|
|
16
|
+
await result
|
|
17
|
+
|
|
18
|
+
fs.writeFile(path.join(config.gen_assets, "style.css"), result.css)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await config.init()
|
|
22
|
+
await run()
|
|
23
|
+
|
|
24
|
+
const watcher = chokidar.watch(["style.css", "pages/", "target/classes"], {
|
|
25
|
+
persistent: true,
|
|
26
|
+
ignoreInitial: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
watcher.on("all", async (event, file_path) => {
|
|
30
|
+
await run()
|
|
31
|
+
console.log("TailwindCSS updated")
|
|
32
|
+
})
|
package/src/typst.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { createHash } from "node:crypto"
|
|
3
|
+
import { promises as fs } from "node:fs"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
import config from "./config.js"
|
|
7
|
+
import { file_exists } from "./util.js"
|
|
8
|
+
|
|
9
|
+
function dir() {
|
|
10
|
+
return path.join(config.gen_assets, "typst/")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function svg_file_path(formula) {
|
|
14
|
+
return path.format({ dir: dir(), name: hash(formula), ext: "svg" })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const CACHE = {
|
|
18
|
+
async contains(formula) {
|
|
19
|
+
return file_exists(svg_file_path(formula))
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// Returns the relative path to the file
|
|
23
|
+
async insert(formula) {
|
|
24
|
+
if (!(await this.contains(formula))) {
|
|
25
|
+
render(formula)
|
|
26
|
+
}
|
|
27
|
+
return path.relative(config.tree, svg_file_path(formula))
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hash(text) {
|
|
32
|
+
return createHash("sha256").update(text).digest("hex")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function render(formula) {
|
|
36
|
+
// TODO: this should probably go elsewhere
|
|
37
|
+
await fs.mkdir(dir(), { recursive: true })
|
|
38
|
+
|
|
39
|
+
const typst = `\
|
|
40
|
+
#set page(width: auto, height: auto, margin: (x: 0pt, y: 5pt))
|
|
41
|
+
#set text(size: 16pt)
|
|
42
|
+
|
|
43
|
+
$${formula}$
|
|
44
|
+
`
|
|
45
|
+
|
|
46
|
+
const dst = svg_file_path(formula)
|
|
47
|
+
const child = spawn("typst", ["compile", "--format", "svg", "-", dst], {
|
|
48
|
+
stdio: ["pipe", null, null],
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
child.stderr.on("data", data => {
|
|
52
|
+
console.log(data.toString("utf8"))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
child.stdin.write(typst)
|
|
56
|
+
child.stdin.end()
|
|
57
|
+
|
|
58
|
+
await child
|
|
59
|
+
}
|
package/src/util.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import url from "node:url"
|
|
4
|
+
|
|
5
|
+
export async function file_exists(file_path) {
|
|
6
|
+
try {
|
|
7
|
+
await fs.access(file_path)
|
|
8
|
+
return true
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function find_dir_with_file(start_dir, name) {
|
|
15
|
+
let current_dir = start_dir
|
|
16
|
+
|
|
17
|
+
while (true) {
|
|
18
|
+
const file_path = path.join(current_dir, name)
|
|
19
|
+
if (await file_exists(file_path)) {
|
|
20
|
+
return [current_dir, file_path]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parent_dir = path.dirname(current_dir)
|
|
24
|
+
if (parent_dir === current_dir) {
|
|
25
|
+
return [null, null]
|
|
26
|
+
}
|
|
27
|
+
current_dir = parent_dir
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function filename() {
|
|
32
|
+
return url.fileURLToPath(import.meta.url)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function dirname() {
|
|
36
|
+
return path.dirname(filename())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function silence_warning(name) {
|
|
40
|
+
const originalEmitWarning = process.emitWarning
|
|
41
|
+
|
|
42
|
+
process.emitWarning = (warning, type, code, ctor) => {
|
|
43
|
+
if (type === name) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
originalEmitWarning(warning, type, code, ctor)
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/vite.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { build } from "vite"
|
|
3
|
+
|
|
4
|
+
import config from "./config.js"
|
|
5
|
+
|
|
6
|
+
async function run() {
|
|
7
|
+
await build(vite_config(["js/main.js", "js/other.js"]))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function vite_config(files) {
|
|
11
|
+
const entries = files.reduce((acc, file) => {
|
|
12
|
+
const name = path.parse(file).name
|
|
13
|
+
acc[name] = file
|
|
14
|
+
return acc
|
|
15
|
+
}, {})
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
build: {
|
|
19
|
+
minify: false,
|
|
20
|
+
lib: {
|
|
21
|
+
entry: entries,
|
|
22
|
+
// only build ES
|
|
23
|
+
fileName: (format, entry) => `${entry}.js`,
|
|
24
|
+
formats: ["es"],
|
|
25
|
+
},
|
|
26
|
+
rollupOptions: {},
|
|
27
|
+
outDir: path.join(config.gen_assets, "vite/"),
|
|
28
|
+
watch: "./js/",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await config.init()
|
|
34
|
+
await run()
|