+1
.gitignore
+1
.gitignore
···
1
+
dist/
+14
cli/build.ts
+14
cli/build.ts
···
1
+
import { Command } from "@cliffy/command";
2
+
import { buildStatic } from "../core/renderer.ts";
3
+
4
+
export const build = new Command()
5
+
.arguments("<source:string>")
6
+
.option("-o, --output <output:string>", "Output directory", {
7
+
default: "./dist",
8
+
})
9
+
.description("Build static presentation")
10
+
.action(async (options, source: string) => {
11
+
const { output } = options;
12
+
await buildStatic(source, output);
13
+
console.log(`Static build complete! Output: ${output}/index.html`);
14
+
});
+27
core/renderer.ts
+27
core/renderer.ts
···
1
1
import Markdoc, { Node } from "@markdoc/markdoc";
2
2
import { createMarkdocConfig } from "./markdoc-config.ts";
3
+
import { Eta } from "@eta-dev/eta";
4
+
5
+
const eta = new Eta({ views: Deno.cwd() + "/templates" });
3
6
4
7
export async function renderBody(path: string) {
5
8
const file = await Deno.readTextFile(path);
···
33
36
body: Markdoc.renderers.html(await Promise.resolve(tree)),
34
37
};
35
38
}
39
+
40
+
export async function buildStatic(sourcePath: string, outputDir: string) {
41
+
// Ensure output directory exists
42
+
await Deno.mkdir(outputDir, { recursive: true });
43
+
44
+
// Get the presentation title from the first heading
45
+
const sourceContent = await Deno.readTextFile(sourcePath);
46
+
const titleMatch = sourceContent.match(/^# (.+)$/m);
47
+
const title = titleMatch ? titleMatch[1] : "Presentation";
48
+
49
+
// Render the presentation
50
+
const { body, scripts } = await renderBody(sourcePath);
51
+
52
+
// Generate static HTML
53
+
const html = eta.render("static", {
54
+
body,
55
+
scripts: Array.from(scripts),
56
+
title
57
+
});
58
+
59
+
// Write to output file
60
+
const outputPath = `${outputDir}/index.html`;
61
+
await Deno.writeTextFile(outputPath, html);
62
+
}
+2
-1
deno.json
+2
-1
deno.json
···
1
1
{
2
2
"tasks": {
3
-
"dev": "deno run --unstable-broadcast-channel --allow-env --allow-net --allow-read --allow-write --watch main.ts dev"
3
+
"dev": "deno run --unstable-broadcast-channel --allow-env --allow-net --allow-read --allow-write --watch main.ts dev",
4
+
"build": "deno run --allow-env --allow-read --allow-write main.ts build"
4
5
},
5
6
"imports": {
6
7
"@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.8",
+2
main.ts
+2
main.ts
···
1
1
import { Command } from "@cliffy/command";
2
2
import { dev } from "./cli/dev.ts";
3
+
import { build } from "./cli/build.ts";
3
4
4
5
await new Command()
5
6
.name("morkdeck")
6
7
.version("0.0.0")
7
8
.description("Turn Markdoc files into web-powered slide decks")
8
9
.command("dev", dev)
10
+
.command("build", build)
9
11
.parse();
+121
templates/static.eta
+121
templates/static.eta
···
1
+
<!doctype html>
2
+
<html>
3
+
<head>
4
+
<title><%= it.title || "Presentation" %></title>
5
+
<link rel="preconnect" href="https://fonts.googleapis.com">
6
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
7
+
<link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap" rel="stylesheet">
8
+
9
+
<style>
10
+
body {
11
+
margin: 0;
12
+
box-sizing: border-box;
13
+
font-family: "Recursive", sans-serif;
14
+
background-color: #191724;
15
+
color: #908caa;
16
+
}
17
+
18
+
h1, h2, h3, h4, h5, h6 {
19
+
display: block;
20
+
color: #e0def4;
21
+
margin-bottom: 2cqi;
22
+
}
23
+
24
+
p, ul, ol {
25
+
font-size: 2.5cqi;
26
+
margin: 0;
27
+
}
28
+
29
+
pre {
30
+
margin: 0;
31
+
font-variation-settings: "MONO" 1;
32
+
font-family: "Recursive", monospace;
33
+
}
34
+
35
+
code {
36
+
font-variation-settings: "MONO" 1;
37
+
font-family: "Recursive", monospace;
38
+
}
39
+
40
+
article {
41
+
width: 100vw;
42
+
height: 100vh;
43
+
overflow-y: scroll;
44
+
scroll-behavior: smooth;
45
+
scroll-snap-type: y mandatory;
46
+
}
47
+
48
+
section {
49
+
width: 100%;
50
+
height: 100%;
51
+
scroll-snap-align: center;
52
+
position: relative;
53
+
}
54
+
55
+
section > div {
56
+
container: slide / inline-size;
57
+
box-sizing: border-box;
58
+
position: absolute;
59
+
inset: 0;
60
+
margin: auto;
61
+
aspect-ratio: 16 / 9;
62
+
max-width: 100%;
63
+
max-height: 100%;
64
+
65
+
display: flex;
66
+
flex-direction: column;
67
+
justify-content: center;
68
+
align-items: center;
69
+
70
+
padding: 6cqmin;
71
+
background: #1f1d2e;
72
+
}
73
+
74
+
h1 {
75
+
font-size: 8cqi;
76
+
margin: 0;
77
+
font-weight: 900;
78
+
}
79
+
80
+
h2 {
81
+
font-size: 6cqi;
82
+
margin-top: 0;
83
+
margin-bottom: 3cqi;
84
+
font-weight: 900;
85
+
}
86
+
87
+
h1:first-of-type {
88
+
font-size: 10cqi;
89
+
margin-bottom: 3cqi;
90
+
font-weight: 1000;
91
+
line-height: 0.8em;
92
+
}
93
+
94
+
h1:first-of-type ~ :is(h2, h3, h4, h5, h6, p) {
95
+
font-size: 2.8cqi;
96
+
font-weight: 700;
97
+
margin: 0;
98
+
}
99
+
100
+
div:has(h1:first-of-type) {
101
+
display: flex;
102
+
flex-direction: column;
103
+
justify-content: center;
104
+
}
105
+
106
+
.shiki {
107
+
padding: 2cqi;
108
+
font-size: 2cqi;
109
+
border-radius: 1em;
110
+
}
111
+
</style>
112
+
</head>
113
+
<body>
114
+
<article>
115
+
<%~ it.body %>
116
+
</article>
117
+
<% it.scripts.forEach(src => { %>
118
+
<script src="<%= src %>"></script>
119
+
<% }) %>
120
+
</body>
121
+
</html>