A hackable template for creating small and fast browser games.
1import hl from "highlight.js/lib/core";
2import hl_typescript from "highlight.js/lib/languages/typescript";
3import {marked} from "marked";
4import fs from "node:fs";
5import {parseArgs} from "node:util";
6hl.registerLanguage("typescript", hl_typescript);
7
8let {positionals, values} = parseArgs({
9 allowPositionals: true,
10 options: {
11 component: {
12 type: "string",
13 multiple: true,
14 },
15 system: {
16 type: "string",
17 multiple: true,
18 },
19 library: {
20 type: "string",
21 multiple: true,
22 },
23 utility: {
24 type: "string",
25 multiple: true,
26 },
27 },
28});
29
30let source_ts = positionals.shift();
31let content = fs.readFileSync(source_ts);
32let lines = content.toString().split("\n");
33
34let source_gh = "https://github.com/piesku/goodluck/blob/main/" + source_ts.slice(3);
35
36class Section {
37 constructor(docs, code) {
38 this.docs = docs;
39 this.code = code;
40 }
41}
42
43class Line {
44 constructor(lineno, text) {
45 this.lineno = lineno;
46 this.value = text;
47 }
48}
49
50let sections = [];
51
52let in_comment = false;
53let in_indent = false;
54let code = [];
55let docs = [];
56
57let lineno = 0;
58
59for (let line of lines) {
60 lineno++;
61 if (line.startsWith("/**")) {
62 in_comment = true;
63 } else if (in_comment && line.startsWith(" *")) {
64 let lineobj = new Line(lineno, line.slice(3));
65 docs.push(lineobj);
66 } else if (in_comment && line.startsWith("*/")) {
67 in_comment = false;
68 } else if (line === "" && !in_indent) {
69 let section = new Section(docs, code);
70 sections.push(section);
71 code = [];
72 docs = [];
73 } else {
74 in_comment = false;
75 if (/^\s/.test(line)) {
76 in_indent = true;
77 } else {
78 in_indent = false;
79 }
80 let lineobj = new Line(lineno, line);
81 code.push(lineobj);
82 }
83}
84
85//console.log(JSON.stringify(sections, null, 4));
86
87marked.setOptions({
88 renderer: new marked.Renderer(),
89 highlight: function (code, lang) {
90 const language = hl.getLanguage(lang) ? lang : "typescript";
91 return hl.highlight(code, {language}).value;
92 },
93 langPrefix: "hljs language-", // highlight.js css expects a top-level 'hljs' class.
94 pedantic: false,
95 gfm: true,
96 breaks: false,
97 sanitize: false,
98 smartLists: true,
99 smartypants: false,
100 xhtml: false,
101});
102
103marked.use({
104 extensions: [
105 {
106 name: "definition_list",
107 level: "block",
108 start(src) {
109 // Hint to Marked.js to stop and check for a match
110 return src.match(/@(param|returns)/)?.index;
111 },
112 tokenizer(src, tokens) {
113 if (src.startsWith("@param") || src.startsWith("@returns")) {
114 let token = {
115 // Token to generate
116 type: "definition_list", // Should match "name" above
117 raw: src, // Text to consume from the source
118 tokens: [], // Array where child inline tokens will be generated
119 };
120 // Queue this data to be processed for inline tokens
121 this.lexer.inline(src.trim(), token.tokens);
122 return token;
123 }
124 },
125 renderer(token) {
126 return `<dl>${this.parser.parseInline(token.tokens)}\n</dl>`;
127 },
128 },
129 {
130 name: "definition_item",
131 level: "inline",
132 start(src) {
133 // Hint to Marked.js to stop and check for a match
134 return src.match(/@(param|returns)/)?.index;
135 },
136 tokenizer(src, tokens) {
137 // Regex for the complete token, anchored to string start
138 let rule = /^@(?:(param) ([^ ]*)(?: -)?|(returns)) ([^]*?)(?:\n(?=@)|$)/;
139 let match = rule.exec(src);
140 if (match) {
141 return {
142 // Token to generate
143 type: "definition_item", // Should match "name" above
144 raw: match[0], // Text to consume from the source
145 tag: match[1] || match[3], // The tag: param or returns
146 dt: match[2],
147 dd: this.lexer.inlineTokens(match[4].trim()),
148 };
149 }
150 },
151 renderer(token) {
152 return `<dt>
153 ${token.dt ? `<code>${token.dt}</code>` : ""}
154 <small>${token.tag}</small>
155 </dt><dd>${this.parser.parseInline(token.dd)}</dd>`;
156 },
157 // Child tokens to be visited by walkTokens
158 childTokens: ["dt", "dd"],
159 },
160 ],
161});
162
163function render_docs(section) {
164 let content = section.docs.map((line) => line.value).join("\n");
165 if (content.length > 0) {
166 return `<section class="docs">
167 ${marked(content)}
168 </section>`;
169 }
170
171 return "";
172}
173
174function render_code(section) {
175 let content = section.code.map((line) => line.value).join("\n");
176 if (content.length > 0) {
177 return `<section class="code">
178 <pre><code>${hl.highlight(content, {language: "typescript"}).value}</code></pre>
179 </section>`;
180 }
181
182 return "";
183}
184
185function render_link(filename_html) {
186 let filename_ts = filename_html.replace(".html", ".ts");
187 let nice_name = filename_html.replace(/^(com|lib)_/, "").replace(/.html$/, "");
188 if (source_ts.includes(filename_ts)) {
189 return nice_name;
190 }
191
192 return `<a href="${filename_html}">${nice_name}</a>`;
193}
194
195let first_section = sections[0];
196if (first_section.code.length === 0) {
197 // The first section is module-wide docs.
198 sections.shift();
199} else {
200 first_section = null;
201}
202
203console.log(`<!DOCTYPE html>
204<html lang="en">
205<meta charset="utf-8">
206<meta name="viewport" content="width=device-width, initial-scale=1">
207<style>
208body {
209 background-color: whitesmoke;
210 margin: 0;
211}
212
213@media (min-width: 1024px) {
214 body {
215 display: grid;
216 grid-template-columns: 1fr 2fr;
217 grid-template-rows: auto auto;
218 }
219}
220
221main {
222 grid-row: 1;
223 min-width: 340px;
224 padding: 15px;
225 background-color: #fefefe;
226}
227
228aside {
229 grid-row: 1 / span 2;
230 grid-column: 2;
231 min-width: 340px;
232 padding: 15px;
233}
234
235footer {
236 grid-row: 2;
237 padding: 15px;
238 background-color: #fefefe;
239 column-width: 10rem;
240}
241
242aside .docs {
243 background-color: cornsilk;
244 border: 1px solid #999;
245 border-radius: 10px;
246 margin-bottom: 15px;
247 padding: 0 15px;
248}
249
250@media (min-width: 768px) {
251 aside .docs {
252 float: right;
253 width: 40%;
254 margin-left: 15px;
255 }
256}
257
258hr, h1 {
259 column-span: all;
260}
261
262aside .docs p:first-child {
263 font-weight: bold;
264}
265
266aside header {
267 text-align: right;
268}
269
270pre {
271 white-space: pre-wrap;
272}
273
274code {
275 font: 14px/1.3 Inconsolata, monospace;
276}
277
278dt small {
279 font-style: italic;
280}
281</style>
282<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.6.0/build/styles/vs.min.css">
283<main>
284 <header>
285 <a href="/">Goodluck</a> /
286 <a href="/reference/">API Reference</a>
287 </header>
288 <article>
289 ${first_section ? render_docs(first_section) : ""}
290 </article>
291</main>
292<aside>
293 <header>
294 <a href="${source_gh}">View source on GitHub</a>
295 </header>
296${sections
297 .flatMap((section) => [render_docs(section), render_code(section), "<br clear=right>"])
298 .join("\n")}
299</aside>
300<footer>
301 <hr>
302 ${
303 values.component
304 ? `<section>
305 <h1>Core Components</h1>
306 ${values.component.map(render_link).join("<br>")}
307 </section>`
308 : ""
309 }
310 ${
311 values.system
312 ? `<section>
313 <h1>Core Systems</h1>
314 ${values.system.map(render_link).join("<br>")}
315 </section>`
316 : ""
317 }
318 ${
319 values.library
320 ? `<section>
321 <h1>Libraries</h1>
322 ${values.library.map(render_link).join("<br>")}
323 </section>`
324 : ""
325 }
326 ${
327 values.utility
328 ? `<section>
329 <h1>Utilities</h1>
330 ${values.utility.map(render_link).join("<br>")}
331 </section>`
332 : ""
333 }
334</footer>
335</html>
336`);