A hackable template for creating small and fast browser games.
at main 336 lines 8.5 kB view raw
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`);