1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
import { Marked } from "https://deno.land/x/markdown/mod.ts"
/** references a forum post */
type ForumEntry = {
forum?: number;
topic?: number;
post: number;
} | number;
/** the structure of the front matter */
interface PolicyYFM {
name: string;
description: string;
aliases?: string[];
ignore?: boolean;
autoupdate?: {
forums?: ForumEntry | ForumEntry[];
wiki?: string | string[];
};
}
const decoder = new TextDecoder("utf-8");
const encoder = new TextEncoder();
/** the source of the index page */
const index = {
prefix: `
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Mana World Policies</title>
<meta name="description" content="Active policies on The Mana World">
<link rel="stylesheet" href="style.css">
<link rel="canonical" href="https://policies.themanaworld.org/">
</head>
<body>
<main>
<nav>
<h1>TMW Policies</h1>
<ul>
`.trim(), list: "", suffix: "</ul>\n</nav>\n</main>\n</body>\n</html>"
};
/** the _redirects file used by netlify */
let _redirects = "";
/** the [routes] portion of the yaml file used by Render */
let render_yaml = "";
/** a map of all generated path aliases */
const routes: Map<string, Set<string>> = new Map();
console.info(">> Building the static site...");
// empty the build directory
await Deno.remove("build", { recursive: true });
// loop through policy files
for await (const dirEntry of Deno.readDir("policies")) {
const file: string = dirEntry.name;
const [shortName, type] = file.split(".");
if (type.toLowerCase() !== "md") {
console.log(`Unsupported policy file format: ${file}`);
continue;
}
const rawPolicy = decoder.decode(await Deno.readFile(`policies/${file}`));
if (!rawPolicy.trimStart().startsWith("---")) {
console.log(`Ignoring policy file with no front matter: ${file}`);
continue;
}
// parse front matter and markdown:
const {content: html, meta: YFM} = Marked.parse(rawPolicy);
if (Reflect.has(YFM, "ignore") || file.startsWith(".")) {
console.log(`Ignoring disabled policy file: ${file}`);
continue;
}
// add to the index page
index.list += `<li><a href="/${shortName}" title="${YFM.description}" aria-label="${YFM.description}">${YFM.name}</a></li>\n`;
// add to the netlify redirect file
_redirects += `/${shortName} /${shortName}/index.html 200!\n`;
/** a Set<string> of all aliases for this policy */
const aliases: Set<string> = new Set(Reflect.has(YFM, "aliases") ? YFM.aliases : []);
/** generates aliases from common path substitutions */
const generateCommonAliases = (path: string) => {
const substitutions = [
["-", "_"],
["_", "-"],
["-"],
["_"],
];
for (const substitution of substitutions) {
const replacement = path.replace(substitution[0], substitution[1] ?? "");
aliases.add(replacement);
aliases.add(replacement.toLowerCase());
}
// lower case
aliases.add(path.toLowerCase());
};
// built-in aliases
generateCommonAliases(shortName);
// process path aliases (and duplicate before iterating)
for (const alias of new Set(aliases)) {
generateCommonAliases(alias);
}
// make sure we don't include the article itself in its alias Set
aliases.delete(shortName);
// add all aliases to the netlify _redirects file and the Render yaml file
for (const alias of aliases) {
_redirects += `/${alias} /${shortName} 302\n`
render_yaml += `- type: redirect\n source: /${alias}\n destination: /${shortName}\n`;
}
// record the aliases in the global routes map
routes.set(shortName, aliases);
// wrap the generated html inside our template
const policyPage = encoder.encode(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Mana World – ${YFM.name}</title>
<meta name="description" content="${YFM.description}">
<link rel="stylesheet" href="../style.css">
<link rel="canonical" href="https://policies.themanaworld.org/${shortName}">
</head>
<body>
<article>
${html}
</article>
<footer>Copyright © The Mana World — Generated on ${(new Date()).toISOString()}</footer>
</body>
</html>
`.trim());
// create a subdirectory for it
await Deno.mkdir(`build/${shortName}`, {recursive: true});
await Deno.writeFile(`build/${shortName}/index.html`, policyPage, {create: true});
await Deno.mkdir(`build/${shortName}/raw`, {recursive: true});
await Deno.writeFile(`build/${shortName}/raw/index.html`, encoder.encode(html), {create: true});
}
// write the index page
const indexPage = encoder.encode(index.prefix + index.list + index.suffix);
await Deno.writeFile("build/index.html", indexPage, {create: true});
// write the _redirects file (netlify)
await Deno.writeFile("build/_redirects", encoder.encode(_redirects), {create: true});
// write the render.yaml file (render)
await Deno.writeFile("build/render.yaml", encoder.encode(render_yaml), {create: true});
// write the routes.json file (generic)
const routes_obj: any = {};
for (const [route, aliases] of routes) {
routes_obj[route] = [...aliases]; // convert the Map
}
await Deno.writeFile("build/routes.json", encoder.encode(JSON.stringify(routes_obj)), {create: true});
// copy static assets
for await (const dirEntry of Deno.readDir("src/static")) {
await Deno.copyFile(`src/static/${dirEntry.name}`, `build/${dirEntry.name}`,);
}
console.info(">> Build success ✅");
|