├── .gitignore ├── hotdoc.nimble ├── LICENSE ├── src ├── hotdoc.nim └── hotdoc │ ├── generators.nim │ └── application.nim └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | nimblecache/ 3 | htmldocs/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /hotdoc.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "Willyboar" 5 | description = "Single page documentation generator" 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["hotdoc"] 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.4.6" 13 | requires "docopt >= 0.6.8" 14 | requires "karax >= 1.2.1" 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 willyboar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/hotdoc.nim: -------------------------------------------------------------------------------- 1 | # STANDARD LIBRARY IMPORTS 2 | import std/strformat 3 | 4 | # INSIDE IMPORTS 5 | import hotdoc/generators 6 | 7 | # EXTERNAL LIBRARIES IMPORTS 8 | import docopt 9 | 10 | const 11 | VERSION* = "hotdoc v.0.1.0" 12 | 13 | let doc = fmt""" 14 | 15 | --------------------------------------------------- 16 | 17 | ██╗ ██╗ ██████╗ ████████╗██████╗ ██████╗ ██████╗ 18 | ██║ ██║██╔═══██╗╚══██╔══╝██╔══██╗██╔═══██╗██╔════╝ 19 | ███████║██║ ██║ ██║ ██║ ██║██║ ██║██║ 20 | ██╔══██║██║ ██║ ██║ ██║ ██║██║ ██║██║ 21 | ██║ ██║╚██████╔╝ ██║ ██████╔╝╚██████╔╝╚██████╗ 22 | ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ 23 | 24 | Usage: 25 | hotdoc new 26 | hotdoc build 27 | hotdoc (-h | --help) 28 | hotdoc (-v | --version) 29 | 30 | Options: 31 | -h --help Show this screen. 32 | -v --version Show version. 33 | 34 | Available Commands: 35 | new Generates a new documentation site. 36 | build Builds your documentation site. 37 | 38 | """ 39 | 40 | when isMainModule: 41 | 42 | # Parsing doc for commands 43 | let args = docopt(doc, version = VERSION) 44 | 45 | # Generate new documentation site 46 | if args["new"]: 47 | discard newDocSite($args[""]) 48 | 49 | # Needs a lot of work to stay 50 | elif args["build"]: 51 | discard buildDocSite("docs") 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hotdoc 2 | 3 | Hotdoc is a Single Page Documentation Generator made with Karax and Markdown. 4 | 5 | ## Install 6 | 7 | To use Hotdoc you must have Nim installed. You can follow the instructions [here](https://nim-lang.org/install.html). 8 | 9 | Then you can install Hotdoc through nimble: 10 | 11 | nimble install hotdoc 12 | 13 | Then type: 14 | 15 | hotdoc -v 16 | 17 | If everything is ok you will see something like this: 18 | 19 | Hotdoc v.0.1.0 20 | 21 | ## Usage 22 | 23 | Once you have hotdoc installed you can create a new documentation site by type: 24 | 25 | hotdoc new your_site_name 26 | 27 | This will create a directory that contains: 28 | 29 | - An empty "contents" directory 30 | - A hotdoc.nim file 31 | 32 | Hotdoc scans contents dir and creates a category for every folder. For every markdown file inside this folders creates a section and prints content as html. 33 | 34 | When you are finished type: 35 | 36 | hotdoc build 37 | 38 | inside your documentation folder. 39 | 40 | This will create a docs folder containing your documentation site. 41 | 42 | Everytime you are adding categories or files on contents folder run again the command and your site will be updated. 43 | 44 | ## Deployments 45 | 46 | You can host docs directory in every hosting service. 47 | You can also enable Github pages to point on your docs folder. 48 | 49 | ## License 50 | MIT License. See [here](https://github.com/Willyboar/hotdoc/blob/main/LICENSE). 51 | -------------------------------------------------------------------------------- /src/hotdoc/generators.nim: -------------------------------------------------------------------------------- 1 | # STANDARD LIBRARY IMPORTS 2 | import std / [os, osproc] 3 | 4 | # INTERNAL IMPORTS 5 | import application 6 | 7 | # EXTERNAL IMPORTS 8 | 9 | # Deafult Site Structure Type 10 | type SiteStruct* = ref object 11 | docsite*: string 12 | contentsDir*: string 13 | buildDir*: string 14 | buildAssets: string 15 | cssDir*: string 16 | jsDir*: string 17 | imgDir*: string 18 | 19 | 20 | # Constant variables to set names of directories 21 | const 22 | contentsDirName* = "contents" 23 | assetsDirName* = "assets" 24 | cssDirName* = "css" 25 | jsDirName* = "js" 26 | imgDirName* = "img" 27 | buildDirName* = "docs" 28 | 29 | 30 | 31 | # Creates a new hotdoc documentation site 32 | proc newDocSite*(docsite : string) : SiteStruct = 33 | new result 34 | 35 | let 36 | dir = getCurrentDir() 37 | siteDir = joinpath(dir, docsite) 38 | 39 | 40 | 41 | if not dirExists(siteDir): 42 | block createSite: 43 | createDir(siteDir) 44 | 45 | result.contentsDir = joinPath(siteDir, contentsDirName) 46 | 47 | block createContentsDir: 48 | if not dirExists(result.contentsDir): 49 | createDir(result.contentsDir) 50 | 51 | writeFile(siteDir / "hotdoc.nim", app) 52 | 53 | # Builds the hotdoc static documentation site 54 | proc buildDocSite*(folder : string) : SiteStruct = 55 | new result 56 | 57 | let 58 | dir = getCurrentDir() 59 | buildDir = joinPath(dir, buildDirName) 60 | buildAssets = joinPath(buildDir, assetsDirName) 61 | 62 | if not fileExists("hotdoc.nim"): 63 | echo "hotdoc file not found. Please cd into hotdoc project." 64 | else: 65 | block createBuild: 66 | if not dirExists(buildDir): 67 | createDir(buildDir) 68 | 69 | result.buildAssets = joinPath(buildDir, assetsDirName) 70 | 71 | block createAssetsDir: 72 | if not dirExists(buildAssets): 73 | createDir(result.buildAssets) 74 | 75 | result.cssDir = joinpath(result.buildAssets, cssDirName) 76 | 77 | block createCss: 78 | if not dirExists(result.cssDir): 79 | createDir(result.cssDir) 80 | 81 | writeFile(result.cssDir / "style.css", css) 82 | 83 | result.jsDir = joinpath(result.buildAssets, jsDirName) 84 | 85 | block createJs: 86 | if not dirExists(result.jsDir): 87 | createDir(result.jsDir) 88 | 89 | result.imgDir = joinpath(result.buildAssets, imgDirName) 90 | 91 | block createImg: 92 | if not dirExists(result.imgDir): 93 | createDir(result.imgDir) 94 | 95 | 96 | writeFile(buildDir / "index.html", index) 97 | 98 | discard execCmd("nim js --outdir:docs/assets/js --verbosity:0 --hints:off --warnings:off hotdoc.nim") 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/hotdoc/application.nim: -------------------------------------------------------------------------------- 1 | import std / [strutils] 2 | 3 | const app* = """ 4 | import std / [os, macros, strutils, unicode] 5 | include karax / [prelude, kdom] 6 | 7 | type 8 | Item = object 9 | path: string 10 | 11 | case kind: PathComponent 12 | of {pcDir, pcLinkToDir}: 13 | children: seq[Item] 14 | else: 15 | content: string 16 | 17 | proc renderMarkdown(input: cstring): cstring {.importjs: "md.render(#)".} 18 | 19 | proc getFile(kind: PathComponent, path: string): Item = 20 | result.path = splitPath(path).tail 21 | result.kind = kind 22 | case result.kind: 23 | of {pcDir, pcLinkToDir}: 24 | for k, childPath in walkDir(path): 25 | result.children.add getFile(k, childPath) 26 | else: 27 | result.content = readFile(path) 28 | 29 | proc drawItem(item: Item, printName = true): VNode = 30 | result = buildHtml(tdiv): 31 | case item.kind: 32 | of {pcDir, pcLinkToDir}: 33 | if printName: a(class="title", href = "#" & replace(item.path, ".md" , "")): text replace(item.path, ".md" , "").capitalize 34 | for child in item.children: 35 | drawItem(child) 36 | else: 37 | a(class = "section", href="#" & replace(item.path, ".md" , "")): 38 | text replace(item.path, ".md" , "").capitalize 39 | 40 | 41 | proc drawMd(item: Item, printName = true): VNode = 42 | result = buildHtml(tdiv): 43 | case item.kind: 44 | of {pcDir, pcLinkToDir}: 45 | if printName: h1(class="title", id = item.path): text item.path.capitalize 46 | for child in item.children: 47 | drawMd(child) 48 | else: 49 | tdiv(class = "content-div"): 50 | h2(id = replace(item.path, ".md" , "")): 51 | text replace(item.path, ".md" , "").capitalize 52 | tdiv: 53 | verbatim(item.content.renderMarkdown()) 54 | 55 | 56 | proc createDom: VNode = 57 | const root = getFile(pcDir, getProjectPath() & "/contents") 58 | 59 | result = buildHtml(): 60 | body: 61 | tdiv(class = "container clear"): 62 | tdiv(class = "row wrapper"): 63 | tdiv(class = "toc"): 64 | span(class = "logo"): 65 | text "Hotdoc" 66 | text "®" 67 | span(class = "switch"): 68 | button: 69 | text "🌞🌚" 70 | proc onclick() = 71 | document.body.classList.toggle("dark") 72 | document.querySelector("#ROOT").classList.toggle("dark") 73 | drawItem(root, printName=false) 74 | tdiv(class = "content"): 75 | drawMd(root, printName=false) 76 | footer(class = "container row"): 77 | text "👑 Made with " 78 | a(href = "https://github.com/willyboar/hotdoc"): 79 | text "Hotdoc" 80 | text " 🌭.\n " 81 | a(href="#", class="button"): 82 | text "⬆" 83 | 84 | proc main = 85 | setRenderer createDom 86 | 87 | asm *** 88 | var script = document.createElement('script') 89 | script.onload = function () { 90 | window.md = new Remarkable() 91 | `main`() 92 | } 93 | script.src = "https://cdn.jsdelivr.net/remarkable/1.7.1/remarkable.min.js" 94 | document.head.appendChild(script) 95 | *** 96 | 97 | """.replace('*', '"') 98 | 99 | const index* = """ 100 | 101 | 102 | 103 | 104 | 105 | Hotdoc - Single Page Documentation 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
115 | 116 | 117 | 118 | """ 119 | 120 | const css* = """ 121 | /* 122 | * Root Variables 123 | */ 124 | 125 | :root { 126 | --white: #fff; 127 | --black: #000; 128 | --light-blue: #f6f8fa; 129 | --yellow: #ffea7d; 130 | --dark-gray: #242526; 131 | --pink: #fc4cc3; 132 | --blue: #53b2e7; 133 | --table-border:#dddddd; 134 | --font: 'Epilogue', sans-serif; 135 | } 136 | 137 | /* 138 | * Basic Configuration 139 | */ 140 | 141 | body { 142 | font-family: var(--font); 143 | color: var(--dark-gray) !important; 144 | background-color: var(--white) !important; 145 | margin: 0; 146 | } 147 | 148 | body h1, 149 | h2, 150 | h3, 151 | h4, 152 | h5, 153 | h6 { 154 | color: var(--pink); 155 | } 156 | 157 | .hljs, 158 | code { 159 | background-color: var(--light-blue) !important; 160 | } 161 | 162 | blockquote { 163 | background-color: var(--light-blue) !important; 164 | max-width:100%; 165 | padding:1em; 166 | margin:0; 167 | border-left:1px solid var(--dark-gray); 168 | } 169 | 170 | blockquote:nth(child) { 171 | padding:0; 172 | } 173 | 174 | a { 175 | color: var(--blue); 176 | text-decoration: none; 177 | } 178 | 179 | a:hover { 180 | text-decoration: underline; 181 | } 182 | 183 | table { 184 | border-collapse: collapse; 185 | width: 100%; 186 | } 187 | 188 | td, 189 | th { 190 | border: 1px solid var(--table-border); 191 | text-align: left; 192 | padding: 8px; 193 | } 194 | 195 | th { 196 | background-color: var(--light-blue) !important; 197 | } 198 | 199 | /* 200 | * Dark Side 201 | */ 202 | 203 | .dark { 204 | background-color: var(--dark-gray) !important; 205 | color: var(--yellow) !important; 206 | } 207 | 208 | .dark a { 209 | color: var(--blue); 210 | } 211 | 212 | .dark code { 213 | background-color: #2b2b2b !important; 214 | } 215 | 216 | .dark th { 217 | background-color: #2b2b2b !important; 218 | } 219 | 220 | .dark .hljs { 221 | background-color: #2b2b2b !important; 222 | color: #f8f8f2; 223 | } 224 | 225 | .dark blockquote { 226 | background-color: #2b2b2b !important; 227 | color: #f8f8f2; 228 | border-left:1px solid #f8f8f2; 229 | } 230 | 231 | /* 232 | * Toggle switch-button 233 | */ 234 | 235 | .switch { 236 | width: 100%; 237 | text-align: right; 238 | } 239 | 240 | .switch button { 241 | font-size: 2em; 242 | border: none; 243 | background: none; 244 | cursor: pointer; 245 | padding:5px 0 0; 246 | } 247 | 248 | /* 249 | * Logo 250 | */ 251 | 252 | .logo { 253 | font-size: 2em; 254 | float: left; 255 | margin: 0; 256 | width: 100%; 257 | font-weight: bold; 258 | text-align:left; 259 | } 260 | 261 | .logo img { 262 | max-width: 150px; 263 | } 264 | 265 | /* 266 | * General Config 267 | */ 268 | 269 | .container { 270 | max-width: 100%; 271 | } 272 | 273 | .row { 274 | max-width: 60rem; 275 | margin: auto; 276 | display: block; 277 | } 278 | 279 | .wrapper { 280 | display: flex; 281 | padding: 0 20px; 282 | } 283 | 284 | /* 285 | * Table of Contents (ToC) 286 | */ 287 | 288 | .toc { 289 | width: 200px; 290 | margin-top:1.5em; 291 | overflow-x: hidden; 292 | position: fixed; 293 | height: auto; 294 | background-color:inherit; 295 | } 296 | 297 | .toc a.title { 298 | font-size: 18px; 299 | font-weight: 600; 300 | text-align: left; 301 | text-decoration: none; 302 | color: var(--pink); 303 | display: block; 304 | padding: 5px 0; 305 | } 306 | 307 | .toc a.section { 308 | font-size: 15px; 309 | text-align: left; 310 | line-height: 30px; 311 | display: block; 312 | text-decoration: none; 313 | color: inherit; 314 | } 315 | 316 | .toc a.sub-section { 317 | font-size: 15px; 318 | text-align: left; 319 | line-height: 25px; 320 | display: block; 321 | text-decoration: none; 322 | color: inherit; 323 | padding-left: 20px; 324 | } 325 | 326 | .toc a.title:hover, 327 | .toc a.section:hover, 328 | .toc a.sub-section:hover { 329 | text-decoration: underline; 330 | } 331 | 332 | /* 333 | * Content area 334 | */ 335 | 336 | .content { 337 | text-align: left; 338 | width: 100%; 339 | margin: 1em 0 0 220px; 340 | min-height: 30vh; 341 | background-color:inherit; 342 | } 343 | 344 | .content h1 { 345 | font-size: 1.8em; 346 | line-height: 1.8em; 347 | margin: 0; 348 | text-decoration:underline; 349 | } 350 | 351 | .content h2 { 352 | font-size: 1.3em; 353 | line-height: 1.3em; 354 | margin-bottom: 10px; 355 | text-align: left; 356 | } 357 | 358 | .content p { 359 | font-size: 1em; 360 | line-height: 1.3em; 361 | margin-bottom: 15px; 362 | } 363 | 364 | .content ol, 365 | ul { 366 | line-height: 1.5em; 367 | } 368 | 369 | .content img { 370 | max-width:100%; 371 | } 372 | 373 | 374 | /* 375 | * Expandable Area 376 | */ 377 | 378 | .expander:last-of-type .expander-content { 379 | border-bottom: 1px dotted var(--table-border); 380 | margin-bottom: 0; 381 | } 382 | input[type='checkbox'] { 383 | display: none !important; 384 | } 385 | .expander-toggle { 386 | display: block; 387 | padding: 1em 0; 388 | cursor: pointer; 389 | border-radius: 2px; 390 | transition: 200ms cubic-bezier(0.4, 0, 0.2, 1); 391 | } 392 | .expander-toggle::before { 393 | content: ' '; 394 | display: inline-block; 395 | border-top: 5px solid transparent; 396 | border-bottom: 5px solid transparent; 397 | border-left: 5px solid currentColor; 398 | vertical-align: middle; 399 | margin-right: 0.7rem; 400 | transform: translateY(-2px); 401 | transition: 200ms cubic-bezier(0.4, 0, 0.2, 1); 402 | } 403 | .toggle:checked + .expander-toggle::before { 404 | transform: rotate(90deg) translateX(-3px); 405 | } 406 | .expander-content { 407 | max-height: 0px; 408 | overflow: hidden; 409 | } 410 | .toggle:checked + .expander-toggle + .expander-content { 411 | max-height: 350px; 412 | } 413 | .expander-content .expander-content-inner { 414 | padding:1em 0; 415 | } 416 | .expander .toggle:checked + label + .expander-content { 417 | margin-bottom: 15px; 418 | border-radius: 2px; 419 | transition: 200ms cubic-bezier(0.4, 0, 0.2, 1); 420 | } 421 | 422 | /* 423 | * Back to top button 424 | */ 425 | 426 | .button{ 427 | position:fixed; 428 | bottom:20px; 429 | right:20px; 430 | background:var(--yellow); 431 | color:var(--dark-gray) !important; 432 | text-decoration:none; 433 | transition:0.5s; 434 | cursor:pointer; 435 | font-size:1.5em; 436 | border-radius:100%; 437 | padding:.5em .8em; 438 | } 439 | 440 | .button:hover { 441 | text-decoration: none; 442 | } 443 | 444 | 445 | /* 446 | * Footer 447 | */ 448 | 449 | footer { 450 | padding: 1em 20px; 451 | } 452 | 453 | /* 454 | * Breakpoints 455 | */ 456 | /* 650px and down */ 457 | 458 | @media only screen and (max-width: 650px) { 459 | .switch { 460 | text-align: center; 461 | float: left; 462 | } 463 | 464 | .wrapper { 465 | display: block; 466 | } 467 | .toc { 468 | width: 100%; 469 | text-align: center; 470 | position: relative; 471 | height: auto; 472 | } 473 | .toc a.title, 474 | .toc a.section, 475 | .toc a.sub-section { 476 | text-align: center; 477 | } 478 | .content { 479 | position: relative; 480 | margin-top: 20px; 481 | margin-left: 0px; 482 | } 483 | 484 | .logo { 485 | text-align:center; 486 | } 487 | 488 | 489 | } 490 | 491 | """ --------------------------------------------------------------------------------