├── config.nims ├── test ├── subdir │ ├── safds │ ├── README.md │ ├── subsubdir │ │ └── markdown_subsubdir.md │ ├── lenna.jpg │ └── markdown_subdir.md ├── test.md ├── README.md ├── z_latest.md └── latest.md ├── .gitignore ├── nerc.nimble ├── LICENSE ├── src ├── res │ ├── config.json │ ├── template.html │ └── styles.css └── nerc.nim └── README.md /config.nims: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/subdir/safds: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/subdir/README.md: -------------------------------------------------------------------------------- 1 | # Subdir Index 2 | -------------------------------------------------------------------------------- /test/subdir/subsubdir/markdown_subsubdir.md: -------------------------------------------------------------------------------- 1 | # BLARGH! 2 | -------------------------------------------------------------------------------- /test/test.md: -------------------------------------------------------------------------------- 1 | # This is also a test 2 | 3 | 4 | 5 | Keep moving. 6 | -------------------------------------------------------------------------------- /test/subdir/lenna.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8bitprodigy/nerc/HEAD/test/subdir/lenna.jpg -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # This is a test 4 | 5 | Nothing to see here, folks. 6 | 7 | 8 | Move along. 9 | -------------------------------------------------------------------------------- /test/subdir/markdown_subdir.md: -------------------------------------------------------------------------------- 1 | # This is in a subdirectory 2 | 3 | 4 | 5 | ![Miss. November](/home/chris/Projects/nerc/test/subdir/lenna.jpg) 6 | -------------------------------------------------------------------------------- /test/z_latest.md: -------------------------------------------------------------------------------- 1 | # This is the actual newest file 2 | 3 | 4 | 5 | With `"sort"` set to `"newest"`, this should be sorted to appear first in the sidebar. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nerc 2 | test/*.css 3 | test/*.json 4 | test/*.htm 5 | test/*.html 6 | test/*/*.css 7 | test/*/*.json 8 | test/*/*.htm 9 | test/*/*.html 10 | test/*/*/*.css 11 | test/*/*/*.json 12 | test/*/*/*.htm 13 | test/*/*/*.html 14 | -------------------------------------------------------------------------------- /nerc.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.2.0" 4 | author = "8bitprodigy" 5 | description = "A simple web anti-framework written in Nim." 6 | license = "0BSD" 7 | srcDir = "src" 8 | bin = @["nerc"] 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 2.2.2" 14 | requires "markdown" 15 | -------------------------------------------------------------------------------- /test/latest.md: -------------------------------------------------------------------------------- 1 | # Latest 2 | 3 | This is the latest document created. 4 | 5 | If you set `"index": "newest"` in your `config.json` file, you can have the latest file created be the one that becomes `index.htm`. 6 | 7 | This is useful if you intend to use `nerc` for a blog. 8 | 9 | ## My Linkable header 10 | 11 | [Link to my linkable header.](#my-linkable-header) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Zero-Clause BSD 2 | 3 | =============== 4 | 5 | Permission to use, copy, modify, and/or distribute this software for 6 | any purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 9 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 11 | FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 12 | DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 13 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 14 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /src/res/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "$readme", 3 | "sort": "alphabetical", 4 | "page title": "Default nerc site", 5 | "links": [ 6 | {"label": "Main", "link": "/"}, 7 | {"label": "", "link": "SPACER"}, 8 | {"label": "nerc", "link": "https://github.com/8bitprodigy/nerc"}, 9 | ], 10 | "site title": "Default nerc site", 11 | "subtitle": "These settings can be overridden by putting a config.json file in the root of this nerc site.", 12 | "upper nav": false, 13 | "doc title": true, 14 | "lower nav": false, 15 | "footer left": "This page was generated by nerc.", 16 | "footer right": "nerc is public domain/0BSD software." 17 | } 18 | -------------------------------------------------------------------------------- /src/res/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <!--page title--> 6 | 7 | 11 | 12 | 13 |
14 | 17 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/res/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0px; 3 | } 4 | 5 | body { 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | position: absolute; 11 | padding: 0px; 12 | } 13 | 14 | .spacer { 15 | display: flex; 16 | flex-grow: 1; 17 | } 18 | 19 | .nerc { 20 | margin:5px; 21 | display: flex; 22 | padding: 0px; 23 | } 24 | 25 | #container { 26 | display: flex; 27 | flex-direction: column; 28 | height:100vh; 29 | padding: 5px; 30 | width: 1024px; 31 | margin: auto; 32 | } 33 | 34 | #links { 35 | padding-left: 8px; 36 | padding-right: 8px; 37 | } 38 | 39 | #header { 40 | padding-left: 8px; 41 | } 42 | #header h1 { 43 | display: block; 44 | margin-top: auto; 45 | margin-right: 20px; 46 | } 47 | #header h2 { 48 | display: block; 49 | margin-top: auto; 50 | } 51 | 52 | #body { 53 | width: 100%; 54 | display:flex; 55 | flex-grow: 1; 56 | flex-direction: row; 57 | } 58 | 59 | #sidebar { 60 | display: block; 61 | left: 0px; 62 | padding-top: 20px; 63 | padding-left: 40px; 64 | min-width:200px; 65 | flex-shrink:0; 66 | } 67 | #sidebar ul li { 68 | padding-left: 8px; 69 | font-size: 14pt; 70 | display: list-item; 71 | align-items: center; 72 | } 73 | 74 | #content { 75 | display: block; 76 | flex-grow: 1; 77 | padding-left: 80px; 78 | padding-right: 80px; 79 | padding-top: 40px; 80 | padding-bottom: 40px; 81 | } 82 | #content h1 { 83 | display: block; 84 | text-align: center; 85 | border-bottom: 1px solid black; 86 | margin-bottom: 24px; 87 | } 88 | #content p { 89 | text-indent: 1.5em; 90 | display: block; 91 | max-width: 800px; 92 | margin: auto; 93 | } 94 | #content img { 95 | display: block; 96 | width: 100%; 97 | height: auto; 98 | margin: auto; 99 | } 100 | 101 | #footer { 102 | padding-left:8px; 103 | padding-right: 8px; 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nerc 2 | 3 | Anti-web anti-framework in Nim. 4 | 5 | ## Rationale 6 | 7 | Too many web frameworks are written in slow, heavy, bloated interpreted languages, reflective of most of web technologies, so this is an *anti*-web *anti*-framework written in a fast, lightweight, and *nim*ble compiled language. 8 | 9 | ## Function 10 | 11 | Converts markdown files into html files on a directory basis. 12 | 13 | ## Usage 14 | 15 | Navigate to your directory you wish to be the root of your website and run: 16 | 17 | ``` 18 | nerc 19 | ``` 20 | 21 | Then upload the contents of that directory to the root of your website's hosting directory. 22 | 23 | ### Things to note 24 | 25 | - `readme.md`(case insensitive) files will become `index.htm` files so you can use github hosting for your website, and people visiting the repo will get more or less the same experience. 26 | 27 | - File and directory names become page titles with underscores turned into spaces, so `My_Portfolio.md` will be given the page name `My Portfolio` when linked in the sidebar and after the site title in the browser titebar/tab. 28 | 29 | - Files and directories starting with a `.` or `_` will be ignored for linkage to ensure directories like `.git/` don't get scanned. 30 | 31 | - Directories that don't contain a `readme.md` will not be linkified in the sidebar, but any `.md` documents they contain will be. 32 | 33 | - You can add your own html pages you made, but they will only be linkified if they end in `.html`. 34 | 35 | ## Options 36 | 37 | Options can be overridden on a per-directory basis. Each directory can have its own `config.json`, `styles.css`, and `template.html`, overriding one or more of a parent directory's settings. 38 | 39 | ### config.json 40 | 41 | You'll likely first wish to create your own `config.json` file to override the default settings built in to `nerc`. 42 | This is a list of the various config options: 43 | 44 | | **Key** | **Value Type** | **Description** | 45 | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 46 | | `"index"` | string (default: `"$readme"`): `"newest"` to use the newest created file, to select a specific file name (excluding file extension), make the first character a `$`. | Sets what document gets converted into `index.htm`. | 47 | | `"sort"` | string (default: `"alphabetical"`): `"alphabetical"` to sort alphabetically, `"newest"` to sort by newest | Sets how page links are sorted in the sidebar. | 48 | | `"page title"` | string | Sets the page's title tag (in the browser window titlebar/tab). | 49 | | `"links"` | array of JSON objects: `{"label": [string to hyperlink], "link":[string of URL to link to]}` | Sets the links that appear at the top of the page. Spacers can be inserted by inserting the following JSON object: `{"label": "", "link": "SPACER"}` | 50 | | `"site title"` | string | Sets the title at the top of each page, below the links row. | 51 | | `"subtitle"` | string | Sets the subtitle for the site that appears next to the title. | 52 | | `"upper nav"` | bool (default: `false`) | If true inserts previous/next navigation links at the top of the document to navigate to other documents within the current directory | 53 | | `"doc title"` | bool (default: `true`) | If true, inserts the document's formatted file name (underscores and file extension removed) into the top of the document in a `

` tag. | 54 | | `"lower nav"` | bool (default: `false`) | If true inserts previous/next navigation links at the bottom of the document to navigate to other documents within the current directory | 55 | | `"footer right"` | string | Sets the text string that is inserted into the footer at the bottom of the page on the right. You can insert HTML into this portion for formatting effects, or if you'd like to add a search bar or something. | 56 | 57 | Only defined config options will be overridden, so any settings not defined in a directory's `config.json` file will be inherited either from their parent's directory, or from the default settings defined in `nerc`. 58 | 59 | ### styles.css 60 | 61 | If one is not present in the root directory, a `styles.css` file will be generated and linked to by all pages in the directory and any child directories. If you wish to define your own style for your site, it is recommended to allow `nerc` to generate the `styles.css` file and modify that to set global style settings. 62 | 63 | Styles are overridden by including each `styles.css` along the path to whatever page is being generated, so if a page is in `/places/America/Maryland/`, and `/`, `America`, and `Maryland` each has their own style, they'd be included as such: 64 | 65 | ```html 66 | ... 67 | 72 | ... 73 | ``` 74 | 75 | So as to allow individual styles to be overridden on a per-directory basis. 76 | Here are a list of classes and IDs which can be styled: 77 | 78 | | **Class/ID** | ** Description** | 79 | | ------------ | ------------------------------------------------------------------------------------------- | 80 | | `.spacer` | Styles spacers used to separate links and the left and right text in the footer. | 81 | | `.nerc` | Styles shared by every element that generated content gets inserted to in the visible page. | 82 | | `#container` | Element containing all the elements of the page. | 83 | | `#links` | Element containing links at the top of the page. | 84 | | `#header` | Element containing the Page Title and Subtitle. | 85 | | `#body` | Element containing Sidebar and Content. | 86 | | `#sidebar` | Element containing an unordered list linking to different pages and directories. | 87 | | `#content` | Element containing the contents of the document, rendered as HTML | 88 | | `#foote` | Element containing the footer contents. | 89 | 90 | ### template.html 91 | 92 | Like `config.json`, pages in a directory will use the last defined template up the chain of directories back to the root. Unlike `config.json` and `styles.css`, however, `template.html` overrides the whole page, so if you wish to override it, you'll have to define the whole page layout. 93 | In order for content to be inserted into the page during generation, you'll need to declare the following tokens in your template: 94 | 95 | | **Tag** | **Description** | 96 | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- | 97 | | `` | Inserts the string defined by `"page title"` in `config.json`. | 98 | | `` | Inserts the chain of `styles.css` files that apply to the current directory. | 99 | | `` | Inserts a series of `` tags and spacers defined by `"links"` in `config.json`. | 100 | | `` | Inserts the string defined by `"site title"` in `config.json`. | 101 | | `` | Inserts the string defined by `"subtitle"` in `config.json`. | 102 | | `` | Inserts an unordered list representing the directory structure of the website and all its generated/discovered pages/directories. | 103 | | `` | Inserts the contents of the markdown document, rendered as HMTL elements. | 104 | | `` | Inserts the string defined by "footer left" in `config.json`. | 105 | | `` | Inserts the string defined by "footer right" in `config.json`. | 106 | 107 | ## Building 108 | 109 | Be sure to have [Nim-markdown](https://github.com/soasme/nim-markdown) installed first: 110 | 111 | ```shell 112 | nimble install markdown 113 | ``` 114 | 115 | Then: 116 | 117 | ```shell 118 | git clone https://github.com/8bitprodigy/nerc 119 | cd nerc 120 | nimble build 121 | ``` 122 | 123 | ## 124 | 125 | ## Installation 126 | 127 | ```shell 128 | install nerc /usr/local/bin 129 | ``` 130 | 131 | Or: 132 | 133 | ```shell 134 | install nerc ~/.local/bin 135 | ``` 136 | 137 | ## 138 | 139 | ## License: 140 | 141 | This code is dedicated to the public domain, but is also made available under the terms of the 0-clause BSD license, as some jurisdictions do not recognize the public domain. 142 | 143 | The terms of the 0-clause BSD license are thus:Copyright (C) 2025 Christopher DeBoy 144 | 145 | Permission to use, copy, modify, and/or distribute this software for 146 | any purpose with or without fee is hereby granted. 147 | 148 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL 149 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 150 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 151 | FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 152 | DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 153 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 154 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 155 | -------------------------------------------------------------------------------- /src/nerc.nim: -------------------------------------------------------------------------------- 1 | #[ TODO: 2 | - command line arguments 3 | - Clean up the code a little, I guess 4 | ]# 5 | import 6 | algorithm, 7 | json, 8 | macros, 9 | markdown, 10 | options, 11 | os, 12 | posix, 13 | re, 14 | sequtils, 15 | strformat, 16 | strutils, 17 | terminal, 18 | times 19 | 20 | 21 | type 22 | ItemKind = enum 23 | itemFile, 24 | itemDir 25 | 26 | FileKind = enum 27 | fileMarkdown, 28 | fileJSON 29 | fileHTML 30 | fileTemplate 31 | 32 | DirTreeNode = ref object 33 | depth: uint 34 | name: string 35 | label: string 36 | path: string 37 | parent: DirTreeNode 38 | case kind: ItemKind 39 | of itemDir: 40 | contents: seq[DirTreeNode] 41 | html: string 42 | config: JsonNode 43 | hasIndex: DirTreeNode 44 | hasCSS: DirTreeNode 45 | hasHTML: DirTreeNode 46 | of itemFile: 47 | fileKind: FileKind 48 | date: times.Time 49 | 50 | 51 | proc getConfig(node: DirTreeNode, key: string): JsonNode 52 | proc genLinks(node: DirTreeNode): string 53 | proc genPath(node: DirTreeNode): string 54 | proc removeSuffixInsensitive(s, suffix: string): string 55 | proc convertMarkdownToNercPage(node: DirTreeNode) 56 | proc buildDirTree(node: DirTreeNode, depth: uint) 57 | proc printTree(node: DirTreeNode) 58 | proc genSidebar( tree: DirTreeNode, currentItem: DirTreeNode): string 59 | proc populateDirs(treeRoot: DirTreeNode) 60 | 61 | 62 | const 63 | Spacer: string = "" 64 | 65 | PageTitleTag: string = "" 66 | StylesTag: string = "" 67 | LinksTag: string = "" 68 | SiteTitleTag: string = "" 69 | SubtitleTag: string = "" 70 | SidebarTag: string = "" 71 | ContentTag: string = "" 72 | FooterLeftTag: string = "" 73 | FooterRightTag: string = "" 74 | 75 | DefaultTemplate = staticRead("res/template.html") 76 | DefaultStyles = staticRead("res/styles.css") 77 | DefaultJson = staticRead("res/config.json") 78 | 79 | var defaultconfig: JsonNode 80 | 81 | try: 82 | defaultconfig = parseJson(DefaultJson) 83 | except JsonParsingError: 84 | echo "[ERROR] Built-in config.json does not pass validation. Fix error and recompile." 85 | 86 | let DefaultConfig = defaultconfig 87 | 88 | var 89 | pageTitle: string 90 | htmlSidebar: string 91 | htmlLinksLeft: string 92 | htmlLinksRight: string 93 | htmlFooterLeft: string 94 | htmlFooterRight: string 95 | 96 | fsTree: DirTreeNode = DirTreeNode(depth: 0, name: "", label: "Main", path: ".", kind: itemDir) 97 | 98 | 99 | fsTree.html = DefaultTemplate 100 | fsTree.config = DefaultConfig 101 | 102 | # Function to generate Github-style slugs from header text 103 | proc genSlug(text: string): string = 104 | result = text.toLowerAscii() 105 | # Replace spaces and non-alphanumeric chars with hyphens 106 | for i, c in result: 107 | if not c.isAlphaNumeric(): 108 | result[i] = '-' 109 | 110 | # Remove multiple consecutive hyphens and getCreationTime 111 | result = result.replace(re"--+", "-") 112 | result = result.strip(chars = {'-'}) 113 | 114 | # Add header anchors to generated HMTL 115 | proc addHeaderAnchors(html: string): string = 116 | result = html 117 | # find and replace header tags with anchored versions 118 | for level in 1..6: 119 | let 120 | openTag = "" 121 | closeTag = "" 122 | 123 | var pos = 0 124 | while true: 125 | let startPos = result.find(openTag, pos) 126 | if startPos == -1: break 127 | 128 | let contentStart = startPos + openTag.len 129 | let endPos = result.find(closeTag, contentStart) 130 | if endPos == -1: break 131 | 132 | let text = result[contentStart..""" & text & "" 135 | 136 | result = result[0.." 152 | 153 | if hasPrev: 154 | let prevNode = node.parent.contents[prev] 155 | var path = prevNode.genPath().removeSuffixInsensitive(".md") 156 | if node.parent.hasIndex == prevNode: path = path.removeSuffixInsensitive(prevNode.name.removeSuffixInsensitive(".md")) & "index" 157 | if prevNode.kind == itemFile and 158 | prevNode.fileKind == fileMarkdown: 159 | result = result & "<- Prev" 160 | 161 | result = result & Spacer 162 | 163 | if hasNext: 164 | let nextNode = node.parent.contents[next] 165 | var path = nextNode.genPath().removeSuffixInsensitive(".md") 166 | if node.parent.hasIndex == nextNode: path = path.removeSuffixInsensitive(nextNode.name.removeSuffixInsensitive(".md")) & "index" 167 | if nextNode.kind == itemFile and 168 | nextNode.fileKind == fileMarkdown: 169 | result = result & "Next ->" 170 | 171 | result = result & "" 172 | 173 | 174 | proc getLatestNode(nodes: seq[DirTreeNode]): DirTreeNode = 175 | result = nil 176 | for node in nodes: 177 | if node.kind == itemFile: 178 | if result == nil: 179 | result = node 180 | elif node.date > result.date: 181 | result = node 182 | 183 | 184 | 185 | proc getDirectorySorted(dir: string): seq[tuple[kind: PathComponent, path: string]] = 186 | # Create sequences to store directories and files 187 | var dirs: seq[tuple[kind: PathComponent, path: string]] = @[] 188 | var files: seq[tuple[kind: PathComponent, path: string]] = @[] 189 | 190 | # Walk through directory contents 191 | for ikind, ipath in walkDir(dir,relative=true): 192 | case ikind 193 | of pcDir: 194 | # Skip current and parent directories 195 | if not (ipath.endsWith("/.") or ipath.endsWith("/..")): 196 | dirs.add((ikind, ipath)) 197 | of pcFile: 198 | files.add((ikind, ipath)) 199 | else: 200 | continue 201 | 202 | result.concat(dirs, files) 203 | 204 | 205 | proc removeSuffixInsensitive(s, suffix: string): string = 206 | if s.toLowerAscii().endsWith(suffix.toLowerAscii()): 207 | return s[0 ..< s.len - suffix.len] 208 | return s 209 | 210 | 211 | proc getTemplate(node: DirTreeNode): string = 212 | if node.hasHTML != nil: 213 | if node.html != "": 214 | return node.html 215 | 216 | if node.parent != nil: 217 | return node.parent.getTemplate() 218 | 219 | return DefaultTemplate 220 | 221 | 222 | proc getConfig(node: DirTreeNode, key: string): JsonNode = 223 | if not node.config.isNil: 224 | if node.config.hasKey(key): 225 | return node.config[key] 226 | 227 | if node.parent != nil: 228 | return node.parent.getConfig(key) 229 | 230 | return DefaultConfig[key] 231 | 232 | 233 | proc getStyles(node: DirTreeNode): string = 234 | if node.hasCSS != nil: 235 | let styles: string = if node.parent!=nil: node.parent.getStyles() else: "" 236 | return styles & "\n@import url(\"" & node.genPath() & "styles.css\");" 237 | if node.parent != nil: 238 | return node.parent.getStyles() 239 | return "" 240 | 241 | 242 | proc genLinks(node: DirTreeNode): string = 243 | var 244 | links: string = "" 245 | separator: string = "" 246 | addSeparator: bool = false 247 | 248 | if node.config.isNil: return node.parent.genLinks() 249 | let config: JsonNode = node.getConfig("links") 250 | 251 | if config.kind != JArray: 252 | return node.parent.genLinks() 253 | 254 | for item in config: 255 | if item.kind != JObject: continue 256 | if not item.hasKey("label") and not item.hasKey("link"): continue 257 | if item["label"].kind != JString: continue 258 | if item["link"].kind != JString: continue 259 | let 260 | label = item["label"].getStr() 261 | link = item["link"].getStr() 262 | 263 | separator = if addSeparator: " | " else: "" 264 | 265 | if label == "" and link == "SPACER": 266 | links = links & Spacer 267 | addSeparator = false 268 | continue 269 | 270 | links = links & separator & "" & label & "" 271 | addSeparator = true 272 | 273 | return links 274 | 275 | 276 | proc genPath(node: DirTreeNode): string = 277 | if node.parent == nil: return "/" 278 | var path = genPath(node.parent) & node.name 279 | if node.kind == itemDir: path = path & '/' 280 | return path 281 | 282 | 283 | proc convertMarkdownToNercPage(node: DirTreeNode) = 284 | if node.kind == itemDir: return 285 | let config: string = node.parent.getConfig("index").getStr() 286 | 287 | var outPath: string = node.path[2..^1] 288 | outPath = outPath.removeSuffixInsensitive(".md") 289 | if node.parent.hasIndex == node: 290 | if config[0]=='$': 291 | outpath = outPath.removeSuffixInsensitive(config[1..^1]) & "index" 292 | elif config == "newest": 293 | outpath = outpath.removeSuffixInsensitive(node.name.removeSuffixInsensitive(".md")) & "index" 294 | outPath = outPath & ".htm" 295 | 296 | let 297 | mdFile = readFile(node.path[2..^1]) 298 | navLinks = genNav(node) 299 | 300 | var 301 | htmlTxt = node.parent.getTemplate() 302 | pageTitle: string = "" 303 | content: string = "" 304 | 305 | if node.parent.getConfig("upper nav").getBool(): 306 | content = content & navLinks & "\n
\n" 307 | 308 | if node.parent.getConfig("doc title").getBool() and 309 | (node.parent.hasIndex != node): 310 | content = content & "

" & node.label & "

\n" 311 | 312 | content = content & addHeaderAnchors(mdFile.markdown()) 313 | 314 | if node.parent.getConfig("lower nav").getBool(): 315 | content = content & "\n
\n" & navLinks 316 | 317 | if node.parent.hasIndex != node: pageTitle = " - " & node.label 318 | if htmlTxt.contains(PageTitleTag): htmlTxt = htmlTxt.replace( PageTitleTag, node.parent.getConfig("page title").getStr() & pageTitle ) 319 | if htmlTxt.contains(StylesTag): htmlTxt = htmlTxt.replace( StylesTag, node.parent.getStyles() ) 320 | if htmlTxt.contains(LinksTag): htmlTxt = htmlTxt.replace( LinksTag, node.parent.genLinks() ) 321 | if htmlTxt.contains(SiteTitleTag): htmlTxt = htmlTxt.replace( SiteTitleTag, node.parent.getConfig("site title").getStr() ) 322 | if htmlTxt.contains(SubtitleTag): htmlTxt = htmlTxt.replace( SubtitleTag, node.parent.getConfig("subtitle").getStr() ) 323 | if htmlTxt.contains(SidebarTag): htmlTxt = htmlTxt.replace( SidebarTag, fsTree.genSidebar(node) ) 324 | if htmlTxt.contains(ContentTag): htmlTxt = htmlTxt.replace( ContentTag, content ) 325 | if htmlTxt.contains(FooterLeftTag): htmlTxt = htmlTxt.replace( FooterLeftTag, node.parent.getConfig("footer left").getStr() ) 326 | if htmlTxt.contains(FooterRightTag): htmlTxt = htmlTxt.replace( FooterRightTag, node.parent.getConfig("footer right").getStr() ) 327 | 328 | writefile(outPath, htmlTxt) 329 | echo "[GENERATED]: ", outPath 330 | 331 | 332 | proc buildDirTree(node: DirTreeNode, depth: uint) = 333 | let 334 | path = node.path 335 | files = getDirectorySorted(path) 336 | 337 | for (kind, name) in files: 338 | if name.startsWith(".") or name.startsWith("_"): continue # Skip hidden files and directories (such as .git) 339 | if kind == pcFile: 340 | var new_node: DirTreeNode = DirTreeNode(depth: depth, kind: itemFile, name: name, path: path & '/' & name, parent: node) 341 | 342 | if name.toLowerAscii().endsWith(".md"): 343 | new_node.fileKind = fileMarkdown 344 | new_node.label = name.removeSuffixInsensitive(".md").replace('_', ' ') 345 | #convertMarkdownToNercPage(path) 346 | elif name.toLowerAscii() == "config.json": 347 | var file: string = readFile(new_node.path[2..^1]) 348 | try: 349 | node.config = parseJson(file) 350 | except JsonParsingError: 351 | echo "[ERROR] ", name, " at ", path, " did not pass validation and will be ignored." 352 | continue 353 | 354 | elif name.toLowerAscii() == "template.html": 355 | node.hasHTML = new_node 356 | node.html = readFile(new_node.path) 357 | continue 358 | 359 | elif name.toLowerAscii() == "styles.css": 360 | node.hasCSS = new_node 361 | continue 362 | 363 | elif name.toLowerAscii().endsWith(".html"): 364 | new_node.fileKind = fileHTML 365 | else: continue 366 | new_node.date = getCreationTime(new_node.path) 367 | new_node.parent = node 368 | node.contents.add(new_node) 369 | 370 | elif kind == pcDir: 371 | var new_node: DirTreeNode = DirTreeNode(depth: depth, kind: itemDir, name: name, label: name.split('.')[0].replace('_', ' '), path: path & '/' & name, parent: node) 372 | new_node.parent = node 373 | buildDirTree(new_node, depth+1) 374 | node.contents.add(new_node) 375 | 376 | 377 | let sortMode = node.getConfig("sort").getStr() 378 | if sortMode == "newest": 379 | var sortedSeq: seq[DirTreeNode] = @[node.contents[0]] 380 | let size = node.contents.len 381 | while sortedSeq.len < size: 382 | let latestNode: DirTreeNode = node.contents.getLatestNode() 383 | sortedSeq.add(latestNode) 384 | node.contents.delete(node.contents.find(latestNode)) 385 | node.contents = sortedSeq.reversed() 386 | 387 | let index = node.getConfig("index").getStr().toLowerAscii() 388 | if index == "newest": 389 | node.hasIndex = node.contents.getLatestNode() 390 | elif index[0] == '$': 391 | for e in node.contents: 392 | if e.name.removeSuffixInsensitive(".md").toLowerAscii() == index[1..^1]: 393 | node.hasIndex = e 394 | break 395 | if node.hasIndex != nil: node.hasIndex.label = "index" 396 | 397 | 398 | 399 | proc printTree(node: DirTreeNode) = 400 | if node.kind == itemFile: 401 | var fileType: string 402 | case node.fileKind 403 | of fileMarkdown: fileType = "Markdown" 404 | of fileJSON: fileType = "JSON" 405 | of fileTemplate: fileType = "Template" 406 | of fileHTML: fileType = "HTML" 407 | echo "[FILE]", repeat('\t', node.depth), node.name, " : " 408 | 409 | elif node.kind == itemDir: 410 | echo "[DIR]", repeat('\t', node.depth), node.path, " : ", node.name, " : ", node.contents.len() 411 | 412 | for item in node.contents: 413 | printTree(item) 414 | continue 415 | 416 | 417 | proc genSidebar(tree: DirTreeNode, currentItem: DirTreeNode): string = 418 | var sidebar: string 419 | 420 | if tree.kind == itemFile: 421 | var 422 | name = tree.name 423 | label = tree.label 424 | path = tree.genPath() 425 | 426 | if tree.fileKind != fileMarkdown and tree.fileKind != fileHTML: return "" 427 | 428 | if tree.fileKind == fileMarkdown: 429 | path.removeSuffix(".md") 430 | path = path & ".htm" 431 | 432 | if "readme" == toLowerAscii(label): return "" 433 | 434 | if "index" == toLowerAscii(label): return "" 435 | if tree == currentItem: label = ">> " & label & " <<" 436 | 437 | return repeat('\t', tree.depth) & "
  • " & label & "
  • \n" 438 | 439 | elif tree.kind == itemDir: 440 | var 441 | itemList: string 442 | name = tree.name 443 | label = tree.label 444 | path = tree.genPath() 445 | 446 | #if tree.depth == 0: name = "Main" 447 | if tree.hasIndex == currentItem: label = ">> " & label & " <<" 448 | 449 | for item in tree.contents: 450 | itemList = itemList & genSidebar(item, currentItem) 451 | 452 | if tree.hasIndex != nil: label = "" & label & "\n" 453 | itemList = 454 | "
  • " & label & "
      \n" & 455 | itemList & 456 | "
    \n
  • " 457 | 458 | if tree.depth == 0: itemList = "" 459 | 460 | return itemList 461 | 462 | 463 | proc populateDirs(treeRoot: DirTreeNode) = 464 | for node in treeRoot.contents: 465 | if node.kind == itemFile: 466 | if node.fileKind != fileMarkdown: continue 467 | convertMarkdownToNercPage(node) 468 | elif node.kind == itemDir: 469 | populateDirs(node) 470 | 471 | 472 | proc main() = 473 | let args = commandLineParams() 474 | 475 | if 0 < args.len: 476 | if isValidFileName(args[0]): 477 | echo args[0] 478 | 479 | echo "Scanning..." 480 | buildDirTree(fsTree, 1) 481 | if fsTree.hasCSS == nil: writefile("styles.css", DefaultStyles) 482 | printTree(fsTree) 483 | echo "\nGenerating pages..." 484 | populateDirs(fsTree) 485 | echo "\nDone!" 486 | 487 | main() 488 | --------------------------------------------------------------------------------