├── LICENSE ├── README.md ├── assets.ts ├── assets ├── LICENSE ├── demo.svg ├── logo.optimized.svg ├── logo.svg └── screenshot.png ├── deno_api.ts ├── deno_doc_json.ts ├── deno_info_json.ts ├── deps.ts ├── docuraptor.ts ├── generator.ts ├── renderer.ts └── utility.ts /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Valentin Anger 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docuraptor 2 | 3 | Docuraptor Logo 4 | 5 | Docuraptor is an offline alternative to the [doc.deno.land](https://doc.deno.land) service. 6 | 7 | It generates and serves HTML documentation for JS/TS modules with the help of [Deno's](https://deno.land) documentation parser. 8 | 9 | ## Features 10 | 11 | - Offline documentation 12 | - Usable without browser JavaScript, for example in `w3m` 13 | - Dark and Light theme 14 | 15 | ## Installation 16 | 17 | `$ deno install -A https://deno.land/x/docuraptor@20200930.0/docuraptor.ts` 18 | 19 | The permissions can be restricted. 20 | Read the `--help` documentation for more details. 21 | 22 | ## Usage 23 | 24 | `$ deno run https://deno.land/x/docuraptor@20200930.0/docuraptor.ts --help` 25 | 26 | ## Examples 27 | 28 | ![Docuraptor in w3m screencast](./assets/demo.svg) 29 | _Docuraptor with `BROWSER=w3m`_ 30 | 31 | ![Docuraptor documentation screenshot](./assets/screenshot.png) 32 | -------------------------------------------------------------------------------- /assets.ts: -------------------------------------------------------------------------------- 1 | interface Asset { 2 | content: string; 3 | mimetype?: string; 4 | } 5 | 6 | const assets: { [name: string]: Asset } = { 7 | css: { 8 | content: ` 9 | :root { 10 | color: #111; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | } 16 | 17 | div.metadata { 18 | border: 0.1em solid gray; 19 | margin: 1em 0; 20 | padding: 1ch; 21 | } 22 | 23 | img.logo { 24 | width: 5ch; 25 | } 26 | 27 | header { 28 | display: flex; 29 | flex-wrap: wrap; 30 | justify-content: center; 31 | margin: 0.5em; 32 | } 33 | 34 | header * { 35 | align-self: center; 36 | margin: 0 0.5ch; 37 | } 38 | 39 | hr { 40 | border: 0.05em dashed #bbb; 41 | } 42 | 43 | li { 44 | border: 0.1em solid #bbb; 45 | margin: 0.2em; 46 | padding: 0.5em 1ch; 47 | } 48 | 49 | li.namespace { 50 | background: #ddd; 51 | } 52 | 53 | li.node { 54 | background: #eee; 55 | } 56 | 57 | li.node li { 58 | font-size: 0.9em; 59 | } 60 | 61 | ol { 62 | margin: 0; 63 | } 64 | 65 | main { 66 | flex-grow: 1; 67 | 68 | font-family: monospace; 69 | font-size: 1.2rem; 70 | } 71 | 72 | nav.sidebar { 73 | background: #eee; 74 | border: 0.1em solid gray; 75 | 76 | box-sizing: border-box; 77 | flex-basis: 25%; 78 | flex-shrink: 0; 79 | height: 100vh; 80 | max-width: 30ch; 81 | overflow: auto; 82 | 83 | position: sticky; 84 | top: 0; 85 | 86 | z-index: 5; 87 | } 88 | 89 | ol.noborder li { 90 | border-style: none; 91 | padding: 0 1ch; 92 | } 93 | 94 | ol.nomarks { 95 | list-style-type: none; 96 | padding-left: 0; 97 | } 98 | 99 | ol.indent { 100 | padding: 0 2ch; 101 | } 102 | 103 | pre { 104 | white-space: pre-wrap; 105 | font-size: 0.9em; 106 | } 107 | 108 | span.exkeyword { 109 | color: #005959; 110 | } 111 | 112 | span.iconize { 113 | display: inline-block; 114 | height: 1em; 115 | margin: 0.2em; 116 | padding: 0.1em; 117 | width: 1em; 118 | 119 | font-family: monospace; 120 | font-weight: bold; 121 | line-height: 1em; 122 | text-align: center; 123 | vertical-align: middle; 124 | 125 | color: black; 126 | background: red; 127 | } 128 | 129 | span.icon-class { 130 | background: #a8e; 131 | } 132 | 133 | span.icon-enum { 134 | background: #6af; 135 | } 136 | 137 | span.icon-function { 138 | background: #fd6; 139 | } 140 | 141 | span.icon-green { 142 | background: lightgreen; 143 | } 144 | 145 | span.icon-import { 146 | background:white; 147 | } 148 | 149 | span.icon-interface { 150 | background: #d8d; 151 | } 152 | 153 | span.icon-namespace { 154 | background: silver; 155 | } 156 | 157 | span.icon-orange { 158 | background: orange; 159 | } 160 | 161 | span.icon-typeAlias { 162 | background: #7bf; 163 | } 164 | 165 | span.icon-variable { 166 | background: #aea; 167 | } 168 | 169 | span.icon-white { 170 | background: white; 171 | } 172 | 173 | span.identifier { 174 | color: black; 175 | font-weight: bold; 176 | } 177 | 178 | span.keyword { 179 | color: darkmagenta; 180 | } 181 | 182 | span.literal { 183 | color: green; 184 | } 185 | 186 | span.typeref { 187 | color: navy; 188 | } 189 | 190 | span.unimplemented { 191 | color: #f44; 192 | background: #224; 193 | border: 0.2em dashed red; 194 | font-size: 0.8em; 195 | } 196 | 197 | .fill { 198 | flex-grow: 1; 199 | } 200 | 201 | .hor-flex { 202 | display: flex; 203 | flex-direction: row; 204 | } 205 | 206 | .inline { 207 | display: inline-block; 208 | } 209 | 210 | .monospace { 211 | font-family: monospace; 212 | } 213 | 214 | .notextwrap { 215 | white-space: nowrap; 216 | } 217 | 218 | .nowrap { 219 | display: inline-flex; 220 | flex-wrap: nowrap; 221 | } 222 | 223 | .padding { 224 | padding: 0.2em 1em; 225 | } 226 | 227 | @media(prefers-color-scheme: dark) { 228 | :root { 229 | color: #eee; 230 | background: #111; 231 | } 232 | 233 | a:link { 234 | color: cornflowerblue; 235 | } 236 | 237 | a:visited { 238 | color: dodgerblue; 239 | } 240 | 241 | li.link { 242 | background: #222; 243 | } 244 | 245 | li.namespace { 246 | background: #333; 247 | } 248 | 249 | li.node { 250 | background: #222; 251 | } 252 | 253 | nav.sidebar { 254 | background: #222; 255 | font-family: sans; 256 | } 257 | 258 | span.exkeyword { 259 | color: #00c1c1; 260 | } 261 | 262 | span.identifier { 263 | color: white; 264 | } 265 | 266 | span.keyword { 267 | color: violet; 268 | } 269 | 270 | span.literal { 271 | color: lightgreen; 272 | } 273 | 274 | span.typeref { 275 | color: deepskyblue; 276 | } 277 | } 278 | 279 | span a:link { 280 | color: inherit; 281 | text-decoration: inherit; 282 | } 283 | 284 | span a:visited { 285 | color: inherit; 286 | text-decoration: inherit; 287 | } 288 | `, 289 | mimetype: "text/css", 290 | }, 291 | logo: { 292 | content: ` 293 | Docuraptorimage/svg+xmlDocuraptor19.07.2020Valentin AngernamespaceDenoDocuraptor`, 294 | mimetype: "image/svg+xml", 295 | }, 296 | script: { 297 | content: ``, 298 | mimetype: "application/javascript", 299 | }, 300 | }; 301 | export default assets; 302 | -------------------------------------------------------------------------------- /assets/LICENSE: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License. 2 | To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-nd/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 3 | -------------------------------------------------------------------------------- /assets/logo.optimized.svg: -------------------------------------------------------------------------------- 1 | 2 | Docuraptorimage/svg+xmlDocuraptor19.07.2020Valentin AngernamespaceDenoDocuraptor 3 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | Docuraptor 20 | 22 | 45 | 48 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | Docuraptor 58 | 60 | 19.07.2020 61 | 62 | 63 | Valentin Anger 64 | 65 | 66 | 67 | 69 | 71 | 73 | 75 | 77 | 79 | 80 | 81 | 82 | 86 | 92 | 93 | 97 | 102 | 103 | 107 | 112 | 113 | 118 | 121 | 125 | 129 | 133 | namespaceDeno 149 | 150 | 151 | 156 | 161 | 167 | 175 | 180 | 185 | 190 | Docuraptor 198 | 199 | 200 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrupThinker/docuraptor/9071dd18654e7f13d393a99a5fbbc5d9a19afacc/assets/screenshot.png -------------------------------------------------------------------------------- /deno_api.ts: -------------------------------------------------------------------------------- 1 | import type * as ddoc from "./deno_doc_json.ts"; 2 | import type * as info from "./deno_info_json.ts"; 3 | 4 | const decoder = new TextDecoder(); 5 | 6 | export async function getDenoData( 7 | specifier?: string, 8 | { private: priv }: { private?: boolean } = {}, 9 | ): Promise<{ doc: ddoc.DocNode[]; info: info.FileInfo | null }> { 10 | let proc_d; 11 | let proc_i; 12 | try { 13 | proc_d = Deno.run({ 14 | cmd: [ 15 | "deno", 16 | "doc", 17 | "--json", 18 | ...(priv ? ["--private"] : []), 19 | specifier ?? "--builtin", 20 | ], 21 | stdin: "null", 22 | stdout: "piped", 23 | stderr: "piped", 24 | }); 25 | 26 | const stdout = decoder.decode(await proc_d.output()); 27 | const stderr = decoder.decode(await proc_d.stderrOutput()); 28 | const { success } = await proc_d.status(); 29 | 30 | if (!success) { 31 | throw { stderr }; 32 | } 33 | 34 | const doc_j: ddoc.DocNode[] = JSON.parse(stdout); 35 | let info_j: info.FileInfo | null = null; 36 | 37 | if (specifier !== undefined) { 38 | proc_i = Deno.run({ 39 | cmd: [ 40 | "deno", 41 | "info", 42 | "--json", 43 | "--unstable", 44 | specifier, 45 | ], 46 | stdin: "null", 47 | stdout: "piped", 48 | stderr: "piped", 49 | }); 50 | 51 | const stdout = decoder.decode(await proc_i.output()); 52 | const stderr = decoder.decode(await proc_i.stderrOutput()); 53 | const { success } = await proc_i.status(); 54 | 55 | if (!success) { 56 | throw { stderr }; 57 | } 58 | 59 | info_j = JSON.parse(stdout); 60 | } 61 | 62 | return { doc: doc_j, info: info_j }; 63 | } finally { 64 | proc_d?.close(); 65 | proc_i?.close(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /deno_doc_json.ts: -------------------------------------------------------------------------------- 1 | export type Accessibility = "public" | "protected" | "private"; 2 | 3 | export interface ClassConstructorDef { 4 | jsDoc: string | null; 5 | accessibility: Accessibility | null; 6 | name: string; 7 | params: ParamDef[]; 8 | location: Location; 9 | } 10 | 11 | export interface ClassDef { 12 | isAbstract: boolean; 13 | constructors: ClassConstructorDef[]; 14 | properties: ClassPropertyDef[]; 15 | indexSignatures: ClassIndexSignatureDef[]; 16 | methods: ClassMethodDef[]; 17 | extends: string | null; 18 | implements: TsTypeDef[]; 19 | typeParams: TsTypeParamDef[]; 20 | superTypeParams: TsTypeDef[]; 21 | } 22 | 23 | export interface ClassIndexSignatureDef { 24 | readonly: boolean; 25 | params: ParamDef[]; 26 | tsType: TsTypeDef | null; 27 | } 28 | 29 | export interface ClassMethodDef { 30 | jsDoc: string | null; 31 | accessibility: Accessibility | null; 32 | optional: boolean; 33 | isAbstract: boolean; 34 | isStatic: boolean; 35 | name: string; 36 | kind: MethodKind; 37 | functionDef: FunctionDef; 38 | location: Location; 39 | } 40 | 41 | export interface ClassPropertyDef { 42 | jsDoc: string | null; 43 | tsType: TsTypeDef; 44 | readonly: boolean; 45 | accessibility: Accessibility | null; 46 | optional: boolean; 47 | isAbstract: boolean; 48 | isStatic: boolean; 49 | name: string; 50 | location: Location; 51 | } 52 | 53 | export type DocNode = 54 | | DocNodeClass 55 | | DocNodeEnum 56 | | DocNodeFunction 57 | | DocNodeImport 58 | | DocNodeInterface 59 | | DocNodeNamespace 60 | | DocNodeTypeAlias 61 | | DocNodeVariable; 62 | 63 | export interface DocNodeBase { 64 | name: string; 65 | location: Location; 66 | jsDoc: string | null; 67 | } 68 | 69 | export interface DocNodeClass extends DocNodeBase { 70 | kind: "class"; 71 | classDef: ClassDef; 72 | } 73 | 74 | export interface DocNodeEnum extends DocNodeBase { 75 | kind: "enum"; 76 | enumDef: EnumDef; 77 | } 78 | 79 | export interface DocNodeFunction extends DocNodeBase { 80 | kind: "function"; 81 | functionDef: FunctionDef; 82 | } 83 | 84 | export interface DocNodeImport extends DocNodeBase { 85 | kind: "import"; 86 | importDef: ImportDef; 87 | } 88 | 89 | export interface DocNodeInterface extends DocNodeBase { 90 | kind: "interface"; 91 | interfaceDef: InterfaceDef; 92 | } 93 | 94 | export interface DocNodeNamespace extends DocNodeBase { 95 | kind: "namespace"; 96 | namespaceDef: NamespaceDef; 97 | } 98 | 99 | export interface DocNodeTypeAlias extends DocNodeBase { 100 | kind: "typeAlias"; 101 | typeAliasDef: TypeAliasDef; 102 | } 103 | 104 | export interface DocNodeVariable extends DocNodeBase { 105 | kind: "variable"; 106 | variableDef: VariableDef; 107 | } 108 | 109 | export interface EnumDef { 110 | members: { name: string }[]; 111 | } 112 | 113 | export interface FunctionDef { 114 | params: ParamDef[]; 115 | returnType: TsTypeDef | null; 116 | isAsync: boolean; 117 | isGenerator: boolean; 118 | typeParams: TsTypeParamDef[]; 119 | } 120 | 121 | export interface ImportDef { 122 | src: string; 123 | imported: string | null; 124 | } 125 | 126 | export interface InterfaceCallSignatureDef { 127 | location: Location; 128 | jsDoc: string | null; 129 | params: ParamDef[]; 130 | tsType: TsTypeDef | null; 131 | typeParams: TsTypeParamDef[]; 132 | } 133 | 134 | export interface InterfaceDef { 135 | extends: TsTypeDef[]; 136 | methods: InterfaceMethodDef[]; 137 | properties: InterfacePropertyDef[]; 138 | callSignatures: InterfaceCallSignatureDef[]; 139 | indexSignatures: InterfaceIndexSignatureDef[]; 140 | typeParams: TsTypeParamDef[]; 141 | } 142 | 143 | export interface InterfaceIndexSignatureDef { 144 | readonly: boolean; 145 | params: ParamDef[]; 146 | tsType: TsTypeDef | null; 147 | } 148 | 149 | export interface InterfaceMethodDef { 150 | name: string; 151 | location: Location; 152 | jsDoc: string | null; 153 | optional: boolean; 154 | params: ParamDef[]; 155 | returnType: TsTypeDef | null; 156 | typeParams: TsTypeParamDef[]; 157 | } 158 | 159 | export interface InterfacePropertyDef { 160 | name: string; 161 | location: Location; 162 | jsDoc: string; 163 | params: ParamDef[]; 164 | computed: boolean; 165 | optional: boolean; 166 | tsType: TsTypeDef | null; 167 | typeParams: TsTypeParamDef[]; 168 | } 169 | 170 | export interface LiteralCallSignatureDef { 171 | params: ParamDef[]; 172 | tsType: TsTypeDef | null; 173 | typeParams: TsTypeParamDef[]; 174 | } 175 | 176 | export type LiteralDef = { 177 | kind: "number"; 178 | number: number; 179 | } | { 180 | kind: "string"; 181 | string: string; 182 | } | { 183 | kind: "boolean"; 184 | boolean: boolean; 185 | }; 186 | 187 | export interface LiteralIndexSignatureDef { 188 | readonly: boolean; 189 | params: ParamDef[]; 190 | tsType: TsTypeDef | null; 191 | } 192 | 193 | export interface LiteralMethodDef { 194 | name: string; 195 | params: ParamDef[]; 196 | returnType: TsTypeDef | null; 197 | typeParams: TsTypeParamDef[]; 198 | } 199 | 200 | export interface LiteralPropertyDef { 201 | name: string; 202 | params: ParamDef[]; 203 | computed: boolean; 204 | optional: boolean; 205 | tsType: TsTypeDef | null; 206 | typeParams: TsTypeParamDef[]; 207 | } 208 | 209 | export interface Location { 210 | filename: string; 211 | line: number; 212 | col: number; 213 | } 214 | 215 | export type MethodKind = "method" | "getter" | "setter"; 216 | 217 | export interface NamespaceDef { 218 | elements: DocNode[]; 219 | } 220 | 221 | export type ObjectPatPropDef = { 222 | kind: "assign"; 223 | key: string; 224 | value: string | null; 225 | } | { 226 | kind: "keyValue"; 227 | key: string; 228 | value: ParamDef; 229 | } | { 230 | kind: "rest"; 231 | arg: ParamDef; 232 | }; 233 | 234 | export type ParamDef = 235 | | ParamDefArray 236 | | ParamDefAssign 237 | | ParamDefIdentifier 238 | | ParamDefObject 239 | | ParamDefRest; 240 | 241 | export interface ParamDefBase { 242 | tsType: TsTypeDef | null; 243 | } 244 | 245 | export interface ParamDefArray extends ParamDefBase { 246 | kind: "array"; 247 | elements: (ParamDef | null)[]; 248 | optional: boolean; 249 | } 250 | 251 | export interface ParamDefAssign extends ParamDefBase { 252 | kind: "assign"; 253 | left: ParamDef; 254 | right: string; 255 | } 256 | 257 | export interface ParamDefIdentifier extends ParamDefBase { 258 | kind: "identifier"; 259 | name: string; 260 | optional: boolean; 261 | } 262 | 263 | export interface ParamDefObject extends ParamDefBase { 264 | kind: "object"; 265 | props: ObjectPatPropDef[]; 266 | optional: boolean; 267 | } 268 | 269 | export interface ParamDefRest extends ParamDefBase { 270 | kind: "rest"; 271 | arg: ParamDef; 272 | } 273 | 274 | export interface TypeAliasDef { 275 | tsType: TsTypeDef; 276 | typeParams: TsTypeParamDef[]; 277 | } 278 | 279 | export type TsTypeDef = { 280 | kind: "array"; 281 | array: TsTypeDef; 282 | } | { 283 | kind: "conditional"; 284 | conditionalType: { 285 | checkType: TsTypeDef; 286 | extendsType: TsTypeDef; 287 | trueType: TsTypeDef; 288 | falseType: TsTypeDef; 289 | }; 290 | } | { 291 | kind: "fnOrConstructor"; 292 | fnOrConstructor: { 293 | constructor: boolean; 294 | tsType: TsTypeDef; 295 | params: ParamDef[]; 296 | typeParams: TsTypeParamDef[]; 297 | }; 298 | } | { 299 | kind: "indexedAccess"; 300 | indexedAccess: { 301 | readonly: boolean; 302 | objType: TsTypeDef; 303 | indexType: TsTypeDef; 304 | }; 305 | } | { 306 | kind: "intersection"; 307 | intersection: TsTypeDef[]; 308 | } | { 309 | kind: "keyword"; 310 | keyword: string; 311 | } | { 312 | kind: "literal"; 313 | literal: LiteralDef; 314 | } | { 315 | kind: "optional"; 316 | optional: TsTypeDef; 317 | } | { 318 | kind: "parenthesized"; 319 | parenthesized: TsTypeDef; 320 | } | { 321 | kind: "rest"; 322 | rest: TsTypeDef; 323 | } | { 324 | kind: "this"; 325 | this: boolean; 326 | } | { 327 | kind: "tuple"; 328 | tuple: TsTypeDef[]; 329 | } | { 330 | kind: "typeLiteral"; 331 | typeLiteral: TsTypeLiteralDef; 332 | } | { 333 | kind: "typeOperator"; 334 | typeOperator: { 335 | operator: string; 336 | tsType: TsTypeDef; 337 | }; 338 | } | { 339 | kind: "typeQuery"; 340 | typeQuery: string; 341 | } | { 342 | kind: "typeRef"; 343 | typeRef: { 344 | typeParams: TsTypeDef[] | null; 345 | typeName: string; 346 | }; 347 | } | { 348 | kind: "union"; 349 | union: TsTypeDef[]; 350 | }; 351 | 352 | export interface TsTypeLiteralDef { 353 | methods: LiteralMethodDef[]; 354 | properties: LiteralPropertyDef[]; 355 | callSignatures: LiteralCallSignatureDef[]; 356 | indexSignatures: LiteralIndexSignatureDef[]; 357 | } 358 | 359 | export interface TsTypeParamDef { 360 | name: string; 361 | constraint?: TsTypeDef; 362 | default?: TsTypeDef; 363 | } 364 | 365 | type VarDeclKind = "var" | "let" | "const"; 366 | 367 | export interface VariableDef { 368 | tsType: TsTypeDef | null; 369 | kind: VarDeclKind; 370 | } 371 | -------------------------------------------------------------------------------- /deno_info_json.ts: -------------------------------------------------------------------------------- 1 | export type Info = DenoInfo | FileInfo; 2 | 3 | export interface DenoInfo { 4 | denoDir: string; 5 | modulesCache: string; 6 | typescriptCache: string; 7 | } 8 | 9 | export interface FileDependency { 10 | size: number; 11 | deps: string[]; 12 | } 13 | 14 | export interface FileInfo { 15 | local: string; 16 | fileType: "TypeScript" | "JavaScript"; 17 | compiled: string | null; 18 | map: string | null; 19 | depCount: number; 20 | totalSize: number; 21 | files: Record; 22 | } 23 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | assert, 3 | unreachable, 4 | } from "https://deno.land/std@0.69.0/testing/asserts.ts"; 5 | export { parse as argsParse } from "https://deno.land/std@0.69.0/flags/mod.ts"; 6 | export { 7 | serve, 8 | ServerRequest, 9 | } from "https://deno.land/std@0.69.0/http/server.ts"; 10 | export { join as pathJoin } from "https://deno.land/std@0.69.0/path/mod.ts"; 11 | export { pooledMap } from "https://deno.land/std@0.69.0/async/pool.ts"; 12 | -------------------------------------------------------------------------------- /docuraptor.ts: -------------------------------------------------------------------------------- 1 | import assets from "./assets.ts"; 2 | import { 3 | assert, 4 | argsParse, 5 | pathJoin, 6 | serve, 7 | ServerRequest, 8 | unreachable, 9 | } from "./deps.ts"; 10 | import { generateStatic } from "./generator.ts"; 11 | import { DocRenderer } from "./renderer.ts"; 12 | import { htmlEscape } from "./utility.ts"; 13 | 14 | const decoder = new TextDecoder(); 15 | 16 | /* 17 | * Request handling 18 | */ 19 | 20 | const doc_prefix = "/doc/"; 21 | async function handleDoc(req: ServerRequest): Promise { 22 | assert(req.url.startsWith(doc_prefix)); 23 | 24 | const args = req.url.substr(doc_prefix.length); 25 | const search_index = args.indexOf("?"); 26 | 27 | const doc_url = decodeURIComponent( 28 | search_index === -1 ? args : args.slice(0, search_index), 29 | ); 30 | const search = new URLSearchParams( 31 | search_index === -1 ? "" : args.slice(search_index), 32 | ); 33 | 34 | let doc; 35 | try { 36 | doc = await new DocRenderer({ 37 | private: !!search.get("private"), 38 | link_module: (mod) => `/doc/${encodeURIComponent(mod)}`, 39 | }).render( 40 | doc_url.length > 0 ? doc_url : undefined, 41 | ); 42 | } catch (err) { 43 | if (err.stderr !== undefined) { 44 | handleFail(req, 500, htmlEscape(err.stderr)); 45 | } else { 46 | handleFail(req, 500, "Documentation generation failed"); 47 | } 48 | return; 49 | } 50 | 51 | await req.respond({ 52 | status: 200, 53 | headers: new Headers({ 54 | "Content-Type": "text/html", 55 | }), 56 | body: doc, 57 | }); 58 | } 59 | 60 | async function handleFail( 61 | req: ServerRequest, 62 | status: number, 63 | message: string, 64 | ): Promise { 65 | const rend = new DocRenderer(); 66 | await req.respond({ 67 | status, 68 | headers: new Headers({ 69 | "Content-Type": "text/html", 70 | }), 71 | body: ` 72 | 73 | 74 | ${rend.renderHead("Docuraptor Error")} 75 | 76 | 77 | ${rend.renderHeader("An error occured")} 78 |
79 |
 80 |             ${htmlEscape(message)}
 81 |           
82 |
83 | 84 | `, 85 | }); 86 | } 87 | 88 | const file_url = new URL("file:/"); 89 | let deps_url: URL | undefined = undefined; 90 | async function handleIndex(req: ServerRequest): Promise { 91 | const known_documentation = []; 92 | 93 | if (deps_url !== undefined) { 94 | for await (const protocol of Deno.readDir(deps_url.pathname)) { 95 | if (!protocol.isDirectory) { 96 | continue; 97 | } 98 | const path_url = new URL(protocol.name + "/", deps_url); 99 | for await (const host of Deno.readDir(path_url.pathname)) { 100 | if (!host.isDirectory) { 101 | continue; 102 | } 103 | const host_url = new URL(host.name + "/", path_url); 104 | for await (const resource of Deno.readDir(host_url.pathname)) { 105 | if (!resource.isFile || !resource.name.endsWith(".metadata.json")) { 106 | continue; 107 | } 108 | 109 | const resource_url = new URL(resource.name, host_url); 110 | const metadata_string = await Deno.readTextFile( 111 | resource_url.pathname, 112 | ); 113 | const metadata: { headers: { [_: string]: string }; url: string } = 114 | JSON.parse(metadata_string); 115 | 116 | known_documentation.push(metadata.url); 117 | } 118 | } 119 | } 120 | } else { 121 | console.warn("Failed to determine cache directory"); 122 | } 123 | 124 | const rend = new DocRenderer(); 125 | await req.respond({ 126 | status: 200, 127 | headers: new Headers({ 128 | "Content-Type": "text/html", 129 | }), 130 | body: ` 131 | 132 | ${rend.renderHead("Docuraptor Index")} 133 | 134 | 135 | ${rend.renderHeader("Docuraptor Index – Locally available modules")} 136 |
137 | 148 |
149 | 150 | `, 151 | }); 152 | } 153 | 154 | const form_prefix = "/form/"; 155 | async function handleForm(req: ServerRequest): Promise { 156 | assert(req.url.startsWith(form_prefix)); 157 | 158 | const args = req.url.substr(form_prefix.length); 159 | const search_index = args.indexOf("?"); 160 | const form_action = args.slice(0, search_index); 161 | const search = new URLSearchParams( 162 | search_index === -1 ? "" : args.slice(search_index), 163 | ); 164 | 165 | switch (form_action) { 166 | case "open": { 167 | if (!search.has("url")) { 168 | await handleFail(req, 400, "Received invalid request"); 169 | return; 170 | } 171 | 172 | await req.respond({ 173 | status: 301, 174 | headers: new Headers({ 175 | "Location": `/doc/${search.get("url")!}`, 176 | }), 177 | }); 178 | break; 179 | } 180 | default: 181 | await handleFail( 182 | req, 183 | 400, 184 | `Invalid form action ${htmlEscape(form_action)}`, 185 | ); 186 | } 187 | } 188 | 189 | const static_prefix = "/static/"; 190 | async function handleStatic(req: ServerRequest): Promise { 191 | assert(req.url.startsWith(static_prefix)); 192 | const resource = req.url.substr(static_prefix.length); 193 | const asset = assets[resource]; 194 | 195 | if (asset === undefined) { 196 | handleFail(req, 404, "Resource not found"); 197 | } else { 198 | await req.respond({ 199 | status: 200, 200 | headers: new Headers({ 201 | "Content-Type": asset.mimetype ?? "application/octet-stream", 202 | }), 203 | body: asset.content, 204 | }); 205 | } 206 | } 207 | 208 | async function handler(req: ServerRequest): Promise { 209 | try { 210 | if (!["HEAD", "GET"].includes(req.method)) { 211 | handleFail(req, 404, "Invalid method"); 212 | } 213 | 214 | if (req.url.startsWith(static_prefix)) { 215 | await handleStatic(req); 216 | } else if (req.url.startsWith(doc_prefix)) { 217 | await handleDoc(req); 218 | } else if (req.url.startsWith(form_prefix)) { 219 | await handleForm(req); 220 | } else if (req.url === "/") { 221 | await handleIndex(req); 222 | } else { 223 | await handleFail(req, 404, "Malformed path"); 224 | } 225 | } finally { 226 | req.finalize(); 227 | req.conn.close(); 228 | } 229 | } 230 | 231 | /* 232 | * Main 233 | */ 234 | 235 | function argCheck( 236 | rest: Record, 237 | specifier_rest: (string | number)[], 238 | ): void { 239 | if (Object.keys(rest).length > 0 || specifier_rest.length > 0) { 240 | console.error( 241 | `Superfluous arguments: ${[Object.keys(rest), specifier_rest].flat()}`, 242 | ); 243 | Deno.exit(1); 244 | } 245 | } 246 | 247 | function open(s: string): void { 248 | let run = Deno.run({ 249 | cmd: Deno.build.os === "windows" 250 | ? ["start", "", s] 251 | : Deno.build.os === "darwin" 252 | ? ["open", s] 253 | : Deno.build.os === "linux" 254 | ? ["xdg-open", s] 255 | : unreachable(), 256 | stdin: "null", 257 | stdout: "null", 258 | stderr: "null", 259 | }); 260 | 261 | run.status().finally(() => run.close()); 262 | } 263 | 264 | async function initialize() { 265 | let p; 266 | try { 267 | p = Deno.run({ 268 | cmd: ["deno", "info", "--json", "--unstable"], 269 | stdin: "null", 270 | stdout: "piped", 271 | stderr: "null", 272 | }); 273 | 274 | const info: { modulesCache: string } = JSON.parse( 275 | decoder.decode(await p.output()), 276 | ); 277 | 278 | if ((await p.status()).success) { 279 | deps_url = new URL(info.modulesCache + "/", file_url); 280 | } 281 | } finally { 282 | p?.close(); 283 | } 284 | } 285 | 286 | async function mainGenerate() { 287 | const { 288 | builtin, 289 | dependencies, 290 | generate, 291 | index, 292 | out, 293 | private: priv, 294 | "_": specifiers, 295 | ...rest 296 | } = argsParse( 297 | Deno.args, 298 | { 299 | boolean: [/*"builtin",*/ "dependencies", "generate", "private"], 300 | string: ["index", "out"], 301 | }, 302 | ); 303 | argCheck(rest, []); 304 | 305 | await generateStatic(specifiers.map((v) => v.toString()), { 306 | builtin, 307 | index_filename: index, 308 | output_directory: out, 309 | private: priv, 310 | recursive: dependencies, 311 | }); 312 | } 313 | 314 | async function mainServer() { 315 | const { 316 | builtin, 317 | hostname, 318 | port, 319 | private: priv, 320 | "skip-browser": skip, 321 | "_": specifier, 322 | ...rest 323 | } = argsParse(Deno.args, { 324 | default: { 325 | hostname: "127.0.0.1", 326 | port: 8709, 327 | }, 328 | boolean: ["builtin", "private", "skip-browser"], 329 | string: ["hostname"], 330 | }); 331 | argCheck(rest, specifier.slice(1)); 332 | 333 | if (typeof port !== "number") { 334 | console.error("Port must be a number"); 335 | Deno.exit(1); 336 | } 337 | 338 | if (builtin && specifier.length > 0) { 339 | console.error("--builtin and are mutually exclusive"); 340 | Deno.exit(1); 341 | } 342 | 343 | if (priv && specifier.length === 0) { 344 | console.error("Must provide a specifier with --private"); 345 | Deno.exit(1); 346 | } 347 | 348 | let url = `http://${hostname}:${port}/`; 349 | if (builtin) { 350 | url += "doc/"; 351 | } else if (specifier.length > 0) { 352 | url += `doc/${encodeURIComponent(specifier[0])}`; 353 | } 354 | if (priv) { 355 | url += "?private=1"; 356 | } 357 | 358 | console.info("Starting server...", url); 359 | 360 | if (!skip) { 361 | try { 362 | const browser = Deno.env.get("DOCURAPTOR_BROWSER") ?? 363 | Deno.env.get("BROWSER"); 364 | if (browser === undefined) { 365 | throw null; 366 | } 367 | 368 | let run = Deno.run({ 369 | cmd: [browser, url], 370 | }); 371 | 372 | run.status().finally(() => run.close()); 373 | } catch { 374 | open(url); 375 | } 376 | } 377 | 378 | for await (const req of serve({ hostname, port })) { 379 | await handler(req); 380 | } 381 | } 382 | 383 | if (import.meta.main) { 384 | const usage_string = `%cDocuraptor%c (${import.meta.url}) 385 | 386 | %cStart documentation server:%c 387 | $ docuraptor [--port=] [--hostname=] 388 | [--skip-browser] [--private] [--builtin | ] 389 | 390 | Opens the selected module or, 391 | if the module specifier is omitted, the documentation index, 392 | in the system browser. 393 | Listens on 127.0.0.1:8709 by default. 394 | 395 | %cAdditionally requires network access for hostname:port.%c 396 | 397 | 398 | %cGenerate HTML documentation:%c 399 | $ docuraptor --generate [--out=] [--index=] 400 | [--dependencies] [--private] ... 401 | 402 | Writes the documentation of the selected modules 403 | to the output directory, defaulting to the 404 | current working directory. 405 | With the dependencies flag set documentation is also 406 | generated for all modules dependet upon. 407 | Writes an index of all generated documentation 408 | to the index file, defaulting to %cindex.html%c. 409 | 410 | %cAdditionally requires write access to the output directory.%c 411 | 412 | 413 | %cAll functions require allow-run and read access to the Deno cache.%c 414 | 415 | The system browser can be overwritten with the 416 | DOCURAPTOR_BROWSER and BROWSER environment variables. 417 | %cRequires allow-env.%c`; 418 | 419 | const usage_css = [ 420 | "font-weight: bold", 421 | "", 422 | "text-decoration: underline;", 423 | "", 424 | "font-style: italic;", 425 | "", 426 | "text-decoration: underline;", 427 | "", 428 | "font-style: italic;", 429 | "", 430 | "font-style: italic;", 431 | "", 432 | "font-style: italic;", 433 | "", 434 | "font-style: italic;", 435 | "", 436 | ]; 437 | 438 | const { help, generate } = argsParse(Deno.args, { 439 | boolean: ["help", "generate"], 440 | }); 441 | 442 | if (help) { 443 | console.log(usage_string, ...usage_css); 444 | Deno.exit(0); 445 | } 446 | 447 | await initialize(); 448 | 449 | if (generate) { 450 | mainGenerate(); 451 | } else { 452 | mainServer(); 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /generator.ts: -------------------------------------------------------------------------------- 1 | import { getDenoData } from "./deno_api.ts"; 2 | import { assert, pathJoin, pooledMap } from "./deps.ts"; 3 | import { DocRenderer } from "./renderer.ts"; 4 | import { htmlEscape, moduleToFile } from "./utility.ts"; 5 | 6 | export interface IndexOptions { 7 | builtin?: boolean; 8 | index_filename?: string; 9 | output_directory?: string; 10 | private?: boolean; 11 | recursive?: boolean; 12 | } 13 | 14 | // TODO Deno builtin documentation 15 | /** 16 | * Generate static interlinked documentation for modules (and their dependencies) and an index 17 | */ 18 | export async function generateStatic( 19 | modules: string[], 20 | options?: IndexOptions, 21 | ): Promise { 22 | const outdir = options?.output_directory ?? ""; 23 | if (outdir !== "") { 24 | await Deno.mkdir(outdir, { recursive: true }); 25 | } 26 | 27 | const full_modules: Set = new Set(); 28 | for await ( 29 | const _ of pooledMap(32, modules, async (mod) => { 30 | const { info } = await getDenoData(mod); 31 | 32 | assert(info, `Deno failed to generate metadata for module ${mod}`); 33 | 34 | if (options?.recursive) { 35 | for (const mod of Object.keys(info.files)) { 36 | full_modules.add(mod); 37 | } 38 | } else { 39 | try { 40 | new URL(mod); 41 | } catch { 42 | mod = new URL(info.local, "file:///").toString(); 43 | } 44 | full_modules.add(mod); 45 | } 46 | }) 47 | ); 48 | 49 | const renderer = new DocRenderer({ 50 | private: options?.private, 51 | static: true, 52 | link_module: (mod) => 53 | full_modules.has(mod) ? `${moduleToFile(mod)}.html` : undefined, 54 | }); 55 | 56 | for await ( 57 | const _ of pooledMap(32, full_modules.keys(), async (mod) => { 58 | const doc_html = await renderer.render(mod); 59 | await Deno.writeTextFile( 60 | pathJoin(outdir, `${moduleToFile(mod)}.html`), 61 | doc_html, 62 | { 63 | create: true, 64 | }, 65 | ); 66 | }) 67 | ); 68 | 69 | await Deno.writeTextFile( 70 | pathJoin(outdir, options?.index_filename ?? "index.html"), 71 | renderIndex(Array.from(full_modules.keys())), 72 | ); 73 | } 74 | 75 | function renderIndex(modules: string[]): string { 76 | const rend = new DocRenderer({ static: true }); 77 | return ` 78 | 79 | ${rend.renderHead("Docuraptor Index")} 80 | 81 | 82 | ${rend.renderHeader("Documentation Index")} 83 |
84 | 94 |
95 | 96 | `; 97 | } 98 | -------------------------------------------------------------------------------- /renderer.ts: -------------------------------------------------------------------------------- 1 | import assets from "./assets.ts"; 2 | import { getDenoData } from "./deno_api.ts"; 3 | import type * as ddoc from "./deno_doc_json.ts"; 4 | import type * as info from "./deno_info_json.ts"; 5 | import { 6 | assert, 7 | unreachable, 8 | } from "./deps.ts"; 9 | import { htmlEscape, identifierId, humanSize } from "./utility.ts"; 10 | 11 | const sort_order: ddoc.DocNode["kind"][] = [ 12 | "import", 13 | "function", 14 | "variable", 15 | "enum", 16 | "class", 17 | "interface", 18 | "typeAlias", 19 | "namespace", 20 | ]; 21 | 22 | function sortDocNode(a: ddoc.DocNode, b: ddoc.DocNode): number { 23 | return a.kind !== b.kind 24 | ? sort_order.indexOf(a.kind) - sort_order.indexOf(b.kind) 25 | : a.name.localeCompare(b.name); 26 | } 27 | 28 | function unimplemented(what: string | undefined | null): string { 29 | return `UNIMPLEMENTED${ 30 | what != null ? ": " + htmlEscape(what) : "" 31 | }`; 32 | } 33 | 34 | interface DocRendererOptions { 35 | link_module?: (module: string) => string | undefined; 36 | private?: boolean; 37 | static?: boolean; 38 | } 39 | 40 | type IdentMap = Map; 41 | export class DocRenderer { 42 | #options: DocRendererOptions; 43 | #ident_map: IdentMap | undefined = undefined; 44 | #namespace: string[]; 45 | 46 | constructor(options: DocRendererOptions = {}, namespace: string[] = []) { 47 | this.#options = options; 48 | this.#namespace = namespace; 49 | } 50 | 51 | async render(specifier?: string): Promise { 52 | const { doc: doc_j, info: info_j } = await getDenoData( 53 | specifier, 54 | { private: this.#options.private }, 55 | ); 56 | this.#ident_map = generateIdentMap(doc_j); 57 | 58 | const filtered_doc = doc_j.filter((node) => 59 | this.#options.private || node.kind !== "import" 60 | ); 61 | 62 | if (info_j !== null) { 63 | try { 64 | new URL(specifier!); 65 | } catch { 66 | specifier = new URL(info_j.local, "file:///").toString(); 67 | } 68 | } 69 | 70 | return ` 71 | 72 | 73 | ${this.renderHead("Docuraptor Documentation")} 74 | 75 | 76 | ${ 77 | this.renderHeader( 78 | "Documentation for " + (specifier ? htmlEscape(specifier) : "Deno"), 79 | { private_toggle: true }, 80 | ) 81 | } 82 | ${info_j ? this.renderInfo(specifier!, info_j) : ""} 83 |
84 | 87 |
88 | ${this.renderDoc(filtered_doc)} 89 |
90 |
91 | 92 | `; 93 | } 94 | 95 | renderClassConstructorDef(doc: ddoc.ClassConstructorDef): string { 96 | let res = `${ 97 | doc.accessibility ? htmlEscape(doc.accessibility) + " " : "" 98 | } constructor(${this.renderParams(doc.params)})`; 99 | 100 | if (doc.jsDoc !== null) { 101 | res += `
${this.renderJSDoc(doc.jsDoc)}`; 102 | } 103 | 104 | return res; 105 | } 106 | 107 | renderClassDef(doc: ddoc.DocNodeClass): string { 108 | const cd = doc.classDef; 109 | 110 | let res = `${ 111 | cd.isAbstract ? "abstract " : "" 112 | }class ${this.renderIdentifier(doc.name)}${ 113 | this.renderTypeParams(cd.typeParams) 114 | }`; 115 | 116 | if (cd.extends !== null) { 117 | res += ` extends ${ 118 | this.renderTypeReference(cd.extends) 119 | }${ 120 | cd.superTypeParams.length > 0 121 | ? `<${ 122 | cd.superTypeParams.map((t) => this.renderTsTypeDef(t)).join(", ") 123 | }>` 124 | : "" 125 | }`; 126 | } 127 | 128 | if (cd.implements.length > 0) { 129 | res += ` implements ${ 130 | cd.implements.map((t) => this.renderTsTypeDef(t)).join(", ") 131 | }`; 132 | } 133 | 134 | if (cd.properties.length > 0) { 135 | res += `
    136 | ${ 137 | cd.properties.filter((p) => 138 | this.#options.private || p.accessibility !== "private" 139 | ) 140 | .map((p) => `
  1. ${this.renderClassPropertyDef(p)}
  2. `).join("") 141 | } 142 |
`; 143 | } 144 | 145 | if (cd.constructors.length > 0) { 146 | res += `
    147 | ${ 148 | cd.constructors.filter((m) => 149 | this.#options.private || m.accessibility !== "private" 150 | ) 151 | .map((p) => `
  1. ${this.renderClassConstructorDef(p)}
  2. `).join("") 152 | } 153 |
`; 154 | } 155 | 156 | if (cd.methods.length > 0) { 157 | res += `
    158 | ${ 159 | cd.methods.filter((m) => 160 | this.#options.private || m.accessibility !== "private" 161 | ) 162 | .map((p) => `
  1. ${this.renderClassMethodDef(p)}
  2. `).join("") 163 | } 164 |
`; 165 | } 166 | 167 | if (cd.indexSignatures.length > 0) { 168 | res += `
    169 | ${ 170 | cd.indexSignatures.map((p) => 171 | `
  1. ${this.renderIndexSignatureDef(p)}
  2. ` 172 | ) 173 | .join("") 174 | } 175 |
`; 176 | } 177 | 178 | return res; 179 | } 180 | 181 | renderClassMethodDef(doc: ddoc.ClassMethodDef): string { 182 | let res = `${ 183 | doc.accessibility ? htmlEscape(doc.accessibility) + " " : "" 184 | }${doc.isAbstract ? "abstract " : ""}${doc.isStatic ? "static " : ""}${ 185 | doc.kind === "getter" 186 | ? "get " 187 | : doc.kind === "setter" 188 | ? "set " 189 | : doc.kind === "method" 190 | ? "" 191 | : unreachable() 192 | } ${htmlEscape(doc.name)}${doc.optional ? "?" : ""}${ 193 | this.renderTypeParams(doc.functionDef.typeParams) 194 | }(${this.renderParams(doc.functionDef.params)})`; 195 | 196 | if (doc.functionDef.returnType !== null) { 197 | res += `: ${this.renderTsTypeDef(doc.functionDef.returnType)}`; 198 | } 199 | 200 | if (doc.jsDoc !== null) { 201 | res += `
${this.renderJSDoc(doc.jsDoc)}`; 202 | } 203 | 204 | return res; 205 | } 206 | 207 | renderClassPropertyDef(doc: ddoc.ClassPropertyDef): string { 208 | let res = `${ 209 | doc.accessibility ? htmlEscape(doc.accessibility) + " " : "" 210 | }${doc.isAbstract ? "abstract " : ""}${doc.isStatic ? "static " : ""}${ 211 | doc.readonly ? "readonly " : "" 212 | } ${htmlEscape(doc.name)}${doc.optional ? "?" : ""}`; 213 | 214 | if (doc.tsType !== null) { 215 | res += `: ${this.renderTsTypeDef(doc.tsType)}`; 216 | } 217 | 218 | if (doc.jsDoc !== null) { 219 | res += `
${this.renderJSDoc(doc.jsDoc)}`; 220 | } 221 | 222 | return res; 223 | } 224 | 225 | renderDoc(doc: ddoc.DocNode[]): string { 226 | let final = "
    "; 227 | for (const node of doc.sort(sortDocNode)) { 228 | final += `
  1. ${ 229 | this.renderDocNode(node) 230 | }
  2. `; 231 | } 232 | final += "
"; 233 | return final; 234 | } 235 | 236 | renderDocNode(doc: ddoc.DocNode): string { 237 | return ` 238 | ${this.renderDocNodeKind(doc)} 239 | ${doc.jsDoc !== null ? `
${this.renderJSDoc(doc.jsDoc)}` : ""} 240 | `; 241 | } 242 | 243 | renderDocNodeKind(doc: ddoc.DocNode): string { 244 | switch (doc.kind) { 245 | case "class": 246 | return this.renderClassDef(doc); 247 | case "enum": 248 | return this.renderEnumDef(doc); 249 | case "function": 250 | return this.renderFunctionDef(doc); 251 | case "import": 252 | return this.renderImportDef(doc); 253 | case "interface": 254 | return this.renderInterfaceDef(doc); 255 | case "namespace": 256 | return this.renderNamespaceDef(doc); 257 | case "typeAlias": 258 | return this.renderTypeAliasDef(doc); 259 | case "variable": 260 | return this.renderVariableDef(doc); 261 | default: 262 | return unimplemented((doc as { kind: string }).kind); 263 | } 264 | } 265 | 266 | renderEnumDef(doc: ddoc.DocNodeEnum): string { 267 | return `enum ${this.renderIdentifier(doc.name)} 268 |
    ${ 269 | doc.enumDef.members.map((m) => `
  1. ${htmlEscape(m.name)}
  2. `).join("") 270 | }
`; 271 | } 272 | 273 | renderFunctionDef( 274 | doc: ddoc.DocNodeFunction, 275 | ): string { 276 | let res = `${ 277 | doc.functionDef.isAsync ? "async " : "" 278 | }function${doc.functionDef.isGenerator ? "*" : ""} ${ 279 | this.renderIdentifier(doc.name) 280 | }${this.renderTypeParams(doc.functionDef.typeParams)}(${ 281 | this.renderParams(doc.functionDef.params) 282 | })`; 283 | 284 | if (doc.functionDef.returnType !== null) { 285 | res += `: ${this.renderTsTypeDef(doc.functionDef.returnType)}`; 286 | } 287 | 288 | return res; 289 | } 290 | 291 | renderHead(title: string): string { 292 | return ` 293 | 294 | 295 | ${title} 296 | ` + ( 297 | this.#options.static 298 | ? ` 299 | 300 | 301 | ` 302 | : ` 303 | 304 | 305 | 306 | ` 307 | ); 308 | } 309 | 310 | renderHeader( 311 | title: string, 312 | { private_toggle = false }: { private_toggle?: boolean } = {}, 313 | ): string { 314 | return `
315 | ${ 316 | this.#options.static 317 | ? "" 318 | : `` 319 | } 320 |

${title}

321 | ${ 322 | this.#options.static ? "" : ` 323 |
324 | ${ 325 | private_toggle 326 | ? this.#options.private 327 | ? 'View Public' 328 | : 'View Private' 329 | : "" 330 | } 331 |
332 | 333 | 334 |
335 |
336 | ` 337 | } 338 |
`; 339 | } 340 | 341 | renderIdentifier(identifier: string, href?: string): string { 342 | const namespace_html = htmlEscape( 343 | this.#namespace.length ? this.#namespace.join(".") + "." : "", 344 | ); 345 | const ident_id = identifierId(this.#namespace, identifier); 346 | 347 | return namespace_html + 348 | `${htmlEscape(identifier)}`; 351 | } 352 | 353 | renderImportDef(doc: ddoc.DocNodeImport): string { 354 | const impd = doc.importDef; 355 | const src = `"${htmlEscape(impd.src)}"`; 356 | const src_doc = this.#options.link_module?.(impd.src); 357 | 358 | let res = `import `; 359 | 360 | if (impd.imported === null) { 361 | // Namespaced import 362 | res += `* as ${ 363 | this.renderIdentifier(doc.name, src_doc) 364 | }`; 365 | } else if (impd.imported === "default") { 366 | // Default import 367 | res += this.renderIdentifier(doc.name, src_doc); 368 | } else if (impd.imported !== doc.name) { 369 | // Named import 370 | res += `{ ${ 371 | this.renderIdentifier( 372 | impd.imported, 373 | src_doc !== undefined 374 | ? undefined 375 | : src_doc + "#" + identifierId([], impd.imported), 376 | ) 377 | } as ${this.renderIdentifier(doc.name)} }`; 378 | } else { 379 | res += `{ ${ 380 | this.renderIdentifier( 381 | doc.name, 382 | src_doc !== undefined 383 | ? undefined 384 | : src_doc + "#" + identifierId([], doc.name), 385 | ) 386 | } }`; 387 | } 388 | 389 | res += ` from 390 | ${src_doc !== undefined ? src : `${src}`} 391 | `; 392 | 393 | return res; 394 | } 395 | 396 | renderIndexSignatureDef( 397 | doc: ddoc.LiteralIndexSignatureDef, 398 | ): string { 399 | let res = `${doc.readonly ? "readonly " : ""} [${ 400 | this.renderParams(doc.params) 401 | }]`; 402 | 403 | if (doc.tsType !== null) { 404 | res += `: ${this.renderTsTypeDef(doc.tsType)}`; 405 | } 406 | 407 | return res; 408 | } 409 | 410 | renderInfo(specifier: string, info: info.FileInfo): string { 411 | const link = ( 412 | spec: string, 413 | dep: info.FileDependency, 414 | icon: string, 415 | icon_type: string, 416 | ) => { 417 | const src_doc = this.#options.link_module?.(spec); 418 | return ``; 425 | }; 426 | 427 | const unique_deps = Object.entries(info.files); 428 | const direct_deps = new Set(info.files[specifier].deps); 429 | 430 | function compare_deps( 431 | a_name: string, 432 | b_name: string, 433 | ): number { 434 | let a_dir = direct_deps.has(a_name); 435 | let b_dir = direct_deps.has(b_name); 436 | 437 | return -(a_name === specifier) || +(b_name === specifier) || 438 | (a_dir === b_dir 439 | ? a_name.localeCompare(b_name) 440 | : Number(b_dir) - Number(a_dir)); 441 | } 442 | 443 | const transitive = unique_deps.length - direct_deps.size - 1; 444 | 445 | return unique_deps.length > 1 446 | ? `` 472 | : ""; 473 | } 474 | 475 | renderInterfaceCallSignatureDef( 476 | doc: ddoc.InterfaceCallSignatureDef, 477 | ): string { 478 | let res = `${this.renderTypeParams(doc.typeParams)}(${ 479 | this.renderParams(doc.params) 480 | })`; 481 | 482 | if (doc.tsType !== null) { 483 | res += `: ${this.renderTsTypeDef(doc.tsType)}`; 484 | } 485 | 486 | if (doc.jsDoc !== null) { 487 | res += `
${this.renderJSDoc(doc.jsDoc)}`; 488 | } 489 | 490 | return res; 491 | } 492 | 493 | renderInterfaceDef( 494 | doc: ddoc.DocNodeInterface, 495 | ): string { 496 | const id = doc.interfaceDef; 497 | 498 | let res = `interface ${ 499 | this.renderIdentifier(doc.name) 500 | }${this.renderTypeParams(id.typeParams)}`; 501 | 502 | if (id.extends.length > 0) { 503 | res += ` extends ${ 504 | id.extends.map((t) => this.renderTsTypeDef(t)).join(", ") 505 | }`; 506 | } 507 | 508 | if (id.properties.length > 0) { 509 | res += `
    510 | ${ 511 | id.properties 512 | .map((p) => `
  1. ${this.renderInterfacePropertyDef(p)}
  2. `).join("") 513 | } 514 |
`; 515 | } 516 | 517 | if (id.callSignatures.length > 0) { 518 | res += `
    519 | ${ 520 | id.callSignatures 521 | .map((p) => `
  1. ${this.renderInterfaceCallSignatureDef(p)}
  2. `) 522 | .join("") 523 | } 524 |
`; 525 | } 526 | 527 | if (id.methods.length > 0) { 528 | res += `
    529 | ${ 530 | id.methods 531 | .map((p) => `
  1. ${this.renderInterfaceMethodDef(p)}
  2. `).join("") 532 | } 533 |
`; 534 | } 535 | 536 | if (id.indexSignatures.length > 0) { 537 | res += `
    538 | ${ 539 | id.indexSignatures.map((p) => 540 | `
  1. ${this.renderIndexSignatureDef(p)}
  2. ` 541 | ) 542 | .join("") 543 | } 544 |
`; 545 | } 546 | 547 | return res; 548 | } 549 | 550 | renderInterfaceMethodDef(doc: ddoc.InterfaceMethodDef): string { 551 | let res = `${htmlEscape(doc.name)}${doc.optional ? "?" : ""}${ 552 | this.renderTypeParams(doc.typeParams) 553 | }(${this.renderParams(doc.params)})`; 554 | 555 | if (doc.returnType !== null) { 556 | res += `: ${this.renderTsTypeDef(doc.returnType)}`; 557 | } 558 | 559 | if (doc.jsDoc !== null) { 560 | res += `
${this.renderJSDoc(doc.jsDoc)}`; 561 | } 562 | 563 | return res; 564 | } 565 | 566 | renderInterfacePropertyDef(doc: ddoc.InterfacePropertyDef): string { 567 | let res = `${htmlEscape(doc.name)}${doc.optional ? "?" : ""}`; 568 | 569 | if (doc.tsType !== null) { 570 | res += `: ${this.renderTsTypeDef(doc.tsType)}`; 571 | } 572 | 573 | if (doc.jsDoc !== null) { 574 | res += `
${this.renderJSDoc(doc.jsDoc)}`; 575 | } 576 | 577 | return res; 578 | } 579 | 580 | renderJSDoc(doc: string): string { 581 | const summary_end = doc.indexOf("\n\n"); 582 | const [summary, remainder] = summary_end !== -1 583 | ? [doc.slice(0, summary_end), doc.slice(summary_end)] 584 | : [doc, undefined]; 585 | 586 | let res = `
${htmlEscape(summary)}
`; 587 | if (remainder !== undefined) { 588 | res = `
${res}
${
589 |         htmlEscape(remainder)
590 |       }
`; 591 | } 592 | return res; 593 | } 594 | 595 | renderLiteralCallSignatureDef( 596 | doc: ddoc.LiteralCallSignatureDef, 597 | ): string { 598 | let res = `${this.renderTypeParams(doc.typeParams)}(${ 599 | this.renderParams(doc.params) 600 | })`; 601 | 602 | if (doc.tsType !== null) { 603 | res += `: ${this.renderTsTypeDef(doc.tsType)}`; 604 | } 605 | 606 | return res; 607 | } 608 | 609 | renderLiteralMethodDef(doc: ddoc.LiteralMethodDef): string { 610 | let res = `${htmlEscape(doc.name)}${ 611 | this.renderTypeParams(doc.typeParams) 612 | }(${this.renderParams(doc.params)})`; 613 | 614 | if (doc.returnType !== null) { 615 | res += `: ${this.renderTsTypeDef(doc.returnType)}`; 616 | } 617 | 618 | return res; 619 | } 620 | 621 | renderLiteralPropertyDef(prop: ddoc.LiteralPropertyDef): string { 622 | let res = `${htmlEscape(prop.name)}${prop.optional ? "?" : ""}`; 623 | 624 | if (prop.tsType !== null) { 625 | res += `: ${this.renderTsTypeDef(prop.tsType)}`; 626 | } 627 | 628 | return res; 629 | } 630 | 631 | renderNamespaceDef( 632 | doc: ddoc.DocNodeNamespace, 633 | ): string { 634 | let res = `namespace ${ 635 | this.renderIdentifier(doc.name) 636 | } `; 637 | this.#namespace.push(doc.name); 638 | res += this.renderDoc(doc.namespaceDef.elements); 639 | this.#namespace.pop(); 640 | return res; 641 | } 642 | 643 | renderParamDef(doc: ddoc.ParamDef): string { 644 | let res = this.renderParamDefKind(doc); 645 | 646 | if (doc.tsType !== null) { 647 | res += `: ${this.renderTsTypeDef(doc.tsType)}`; 648 | } 649 | 650 | return res; 651 | } 652 | 653 | renderObjectPatPropDef(prop: ddoc.ObjectPatPropDef): string { 654 | switch (prop.kind) { 655 | case "assign": 656 | // TODO Does not display assigned value 657 | return htmlEscape(prop.key); 658 | case "keyValue": 659 | return `${htmlEscape(prop.key)}: ${this.renderParamDef(prop.value)}`; 660 | case "rest": 661 | return `...${this.renderParamDef(prop.arg)}`; 662 | } 663 | } 664 | 665 | renderParamDefKind(doc: ddoc.ParamDef): string { 666 | switch (doc.kind) { 667 | case "array": 668 | return `[${ 669 | doc.elements.map((e) => e === null ? "" : this.renderParamDef(e)) 670 | .join(", ") 671 | }]${doc.optional ? "?" : ""}`; 672 | case "assign": 673 | // TODO Does not display assigned value 674 | return this.renderParamDef(doc.left); 675 | case "identifier": 676 | return htmlEscape(doc.name) + (doc.optional ? "?" : ""); 677 | case "object": 678 | return `{${ 679 | doc.props.map((p) => this.renderObjectPatPropDef(p)).join(", ") 680 | }}${doc.optional ? "?" : ""}`; 681 | case "rest": 682 | return `...${this.renderParamDef(doc.arg)}`; 683 | default: 684 | return unimplemented((doc as { kind: string }).kind); 685 | } 686 | } 687 | 688 | renderParams(params: ddoc.ParamDef[]): string { 689 | return params.map((p) => this.renderParamDef(p)).join(", "); 690 | } 691 | 692 | renderSidebar(doc: ddoc.DocNode[]): string { 693 | function collectIdents( 694 | doc: ddoc.DocNode[], 695 | namespace: string[] = [], 696 | ): { kind: ddoc.DocNode["kind"]; id: string; ident: string }[] { 697 | return doc.flatMap(( 698 | d, 699 | ) => [ 700 | { 701 | kind: d.kind, 702 | id: identifierId(namespace, d.name), 703 | ident: htmlEscape([...namespace, d.name].join(".")), 704 | }, 705 | ...(d.kind === "namespace" 706 | ? collectIdents(d.namespaceDef.elements, [...namespace, d.name]) 707 | : []), 708 | ]); 709 | } 710 | 711 | return `
    712 | ${ 713 | collectIdents(doc).sort(({ ident: a }, { ident: b }) => 714 | a.localeCompare(b) 715 | ).map(({ kind, id, ident }) => 716 | `
  1. ${ 717 | kind[0].toLocaleUpperCase() 718 | } ${htmlEscape(ident)}
  2. ` 719 | ).join("") 720 | } 721 |
`; 722 | } 723 | 724 | renderTypeAliasDef( 725 | doc: ddoc.DocNodeTypeAlias, 726 | ): string { 727 | return `type ${this.renderIdentifier(doc.name)}${ 728 | this.renderTypeParams(doc.typeAliasDef.typeParams) 729 | } = ${this.renderTsTypeDef(doc.typeAliasDef.tsType)}`; 730 | } 731 | 732 | renderTsTypeDef(type_def: ddoc.TsTypeDef): string { 733 | switch (type_def.kind) { 734 | case "array": 735 | return this.renderTsTypeDef(type_def.array) + "[]"; 736 | case "conditional": { 737 | const ct = type_def.conditionalType; 738 | return `${ 739 | this.renderTsTypeDef(ct.checkType) 740 | } extends ${ 741 | this.renderTsTypeDef(ct.extendsType) 742 | } ? ${this.renderTsTypeDef(ct.trueType)} : ${ 743 | this.renderTsTypeDef(ct.falseType) 744 | }`; 745 | } 746 | case "fnOrConstructor": { 747 | const fn = type_def.fnOrConstructor; 748 | return `${ 749 | fn.constructor ? "constructor" : "" 750 | }${this.renderTypeParams(fn.typeParams)}(${ 751 | this.renderParams(fn.params) 752 | }) => ${this.renderTsTypeDef(fn.tsType)}`; 753 | } 754 | case "indexedAccess": { 755 | const ia = type_def.indexedAccess; 756 | return `${ia.readonly ? "readonly " : ""}${ 757 | this.renderTsTypeDef(ia.objType) 758 | }[${this.renderTsTypeDef(ia.indexType)}]`; 759 | } 760 | case "intersection": 761 | return type_def.intersection.map((t) => this.renderTsTypeDef(t)).join( 762 | " & ", 763 | ); 764 | case "keyword": 765 | return `${htmlEscape(type_def.keyword)}`; 766 | case "literal": { 767 | const lit = type_def.literal; 768 | return `${ 769 | lit.kind === "boolean" 770 | ? String(lit.boolean) 771 | : lit.kind === "number" 772 | ? String(lit.number) 773 | : lit.kind === "string" 774 | ? `"${htmlEscape(lit.string)}"` 775 | : unreachable() 776 | }`; 777 | } 778 | case "optional": 779 | return `${this.renderTsTypeDef(type_def.optional)}?`; 780 | case "parenthesized": 781 | return `(${this.renderTsTypeDef(type_def.parenthesized)})`; 782 | case "rest": 783 | return `...${this.renderTsTypeDef(type_def.rest)}`; 784 | case "this": 785 | assert(type_def.this); 786 | return `this`; 787 | case "tuple": 788 | return `[${ 789 | type_def.tuple.map((t) => this.renderTsTypeDef(t)).join(", ") 790 | }]`; 791 | case "typeLiteral": 792 | return this.renderTypeLiteral(type_def.typeLiteral); 793 | case "typeOperator": 794 | return `${ 795 | htmlEscape(type_def.typeOperator.operator) 796 | } ${this.renderTsTypeDef(type_def.typeOperator.tsType)}`; 797 | case "typeQuery": 798 | return this.renderTypeReference(type_def.typeQuery); 799 | case "typeRef": { 800 | const tr = type_def.typeRef; 801 | return `${this.renderTypeReference(tr.typeName)}${ 802 | tr.typeParams !== null 803 | ? `<${ 804 | tr.typeParams.map((t) => this.renderTsTypeDef(t)).join( 805 | ", ", 806 | ) 807 | }>` 808 | : "" 809 | }`; 810 | } 811 | case "union": 812 | return type_def.union.map((t) => this.renderTsTypeDef(t)).join(" | "); 813 | default: 814 | return unimplemented((type_def as { kind: string }).kind); 815 | } 816 | } 817 | 818 | renderTypeLiteral(lit: ddoc.TsTypeLiteralDef): string { 819 | return `{ ${ 820 | [ 821 | lit.properties.map((p) => this.renderLiteralPropertyDef(p)), 822 | lit.callSignatures.map((c) => this.renderLiteralCallSignatureDef(c)), 823 | lit.methods.map((m) => this.renderLiteralMethodDef(m)), 824 | lit.indexSignatures.map((i) => this.renderIndexSignatureDef(i)), 825 | "", 826 | ].flat().join("; ") 827 | }}`; 828 | } 829 | 830 | renderTypeParams(type_params: ddoc.TsTypeParamDef[]): string { 831 | return type_params.length !== 0 832 | ? `<${ 833 | type_params.map((t) => this.renderTypeParamDef(t)).join(", ") 834 | }>` 835 | : ""; 836 | } 837 | 838 | renderTypeParamDef(doc: ddoc.TsTypeParamDef): string { 839 | let res = this.renderTypeReference(doc.name); 840 | 841 | if (doc.constraint !== undefined) { 842 | res += ` extends ${ 843 | this.renderTsTypeDef(doc.constraint) 844 | }`; 845 | } 846 | if (doc.default !== undefined) { 847 | res += ` = ${this.renderTsTypeDef(doc.default)}`; 848 | } 849 | 850 | return res; 851 | } 852 | 853 | renderTypeReference(identifier: string): string { 854 | const resolved_type = this.#ident_map 855 | ? resolveType(this.#ident_map, this.#namespace, identifier) 856 | : null; 857 | return `${ 858 | resolved_type === null 859 | ? identifier 860 | : `${identifier}` 863 | }`; 864 | } 865 | 866 | renderVariableDef( 867 | doc: ddoc.DocNodeVariable, 868 | ): string { 869 | let res = `${htmlEscape(doc.variableDef.kind)} ${ 870 | this.renderIdentifier(doc.name) 871 | }`; 872 | 873 | if (doc.variableDef.tsType !== null) { 874 | res += `: ${this.renderTsTypeDef(doc.variableDef.tsType)}`; 875 | } 876 | 877 | return res; 878 | } 879 | } 880 | 881 | function generateIdentMap(docs: ddoc.DocNode[]): IdentMap { 882 | const map: IdentMap = new Map(); 883 | for (const doc of docs) { 884 | switch (doc.kind) { 885 | case "namespace": { 886 | map.set(doc.name, generateIdentMap(doc.namespaceDef.elements)); 887 | break; 888 | } 889 | default: 890 | map.set(doc.name, doc.name); 891 | break; 892 | } 893 | } 894 | 895 | return map; 896 | } 897 | 898 | function identMapContains(ident_map: IdentMap, target_type: string[]): boolean { 899 | let ident_map_: IdentMap | string | undefined = ident_map; 900 | 901 | while (target_type.length > 0) { 902 | if (ident_map_ === undefined || typeof ident_map_ === "string") { 903 | return false; 904 | } 905 | 906 | ident_map_ = ident_map.get(target_type.shift()!); 907 | } 908 | 909 | return true; 910 | } 911 | 912 | // TODO Handle type parameters correctly 913 | function resolveType( 914 | ident_map: IdentMap, 915 | namespace: string[], 916 | identifier: string, 917 | ): string[] | null { 918 | const target_type = identifier.split("."); 919 | 920 | const scopes: (string | IdentMap)[] = [ident_map]; 921 | const resolved_namespace = Array.from(namespace); 922 | 923 | for (const ns of namespace) { 924 | const current_scope = scopes[scopes.length - 1]; 925 | if (typeof current_scope === "string") { 926 | // Cannot decent further to determine scope 927 | return null; 928 | } 929 | if (!current_scope.has(ns)) { 930 | // Cannot determine surrounding scope 931 | return null; 932 | } 933 | scopes.push(current_scope.get(ns)!); 934 | } 935 | 936 | while (scopes.length > 0) { 937 | const current_scope = scopes[scopes.length - 1]; 938 | if ( 939 | typeof current_scope !== "string" && 940 | identMapContains(current_scope, target_type) 941 | ) { 942 | break; 943 | } 944 | scopes.pop(); 945 | resolved_namespace.pop(); 946 | } 947 | 948 | if (scopes.length === 0) { 949 | // Identifier not found 950 | return null; 951 | } 952 | 953 | return resolved_namespace; 954 | } 955 | -------------------------------------------------------------------------------- /utility.ts: -------------------------------------------------------------------------------- 1 | export function htmlEscape(s: string): string { 2 | return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll( 3 | ">", 4 | ">", 5 | ); 6 | } 7 | 8 | const size_units = ["B", "KiB", "MiB", "GiB"]; 9 | export function humanSize(bytes: number): string { 10 | let unit = 0; 11 | while (bytes > 1024 && unit < size_units.length - 1) { 12 | bytes /= 1024; 13 | unit++; 14 | } 15 | 16 | let visual = Math.round(bytes * 100) / 100; 17 | return `${visual !== bytes ? "~" : ""}${visual}${size_units[unit]}`; 18 | } 19 | 20 | export function identifierId(namespace: string[], identifier: string): string { 21 | const namespace_html = htmlEscape( 22 | namespace.length ? namespace.join(".") + "." : "", 23 | ); 24 | return `ident_${namespace_html}${htmlEscape(identifier)}`; 25 | } 26 | 27 | const encoder = new TextEncoder(); 28 | /** 29 | * Convert a module identifier to a file name compatible with URL's and most file systems 30 | * 31 | * Achieved by encoding all non ASCII alphanumeric bytes. 32 | */ 33 | export function moduleToFile(module_url: string): string { 34 | return Array.from(encoder.encode(module_url).values()).map((byte) => 35 | ((0x30 <= byte && byte < 0x3A) || (0x41 <= byte && byte < 0x5B) || 36 | (0x61 <= byte && byte < 0x7B)) 37 | ? String.fromCharCode(byte) 38 | : `$${byte.toString(16).padStart(2, "0")}` 39 | ).join(""); 40 | } 41 | --------------------------------------------------------------------------------