├── .gitignore ├── LICENSE.md ├── ipsum.nimrod.cfg ├── ipsumgenera.nimble ├── layouts ├── article.html ├── articles.html ├── atom.xml ├── default.html ├── static.html └── tag.html ├── readme.md └── src ├── config.nim ├── ipsum.nim ├── metadata.nim └── rstrender.nim /.gitignore: -------------------------------------------------------------------------------- 1 | # Wildcard 2 | *.swp 3 | .DS_Store 4 | 5 | # Absolute paths 6 | /ipsum 7 | /nimcache 8 | /output 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Dominik Picheta 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /ipsum.nimrod.cfg: -------------------------------------------------------------------------------- 1 | --path:"$lib/packages/docutils" 2 | --verbosity:"0" 3 | -------------------------------------------------------------------------------- /ipsumgenera.nimble: -------------------------------------------------------------------------------- 1 | version = "0.1.1" 2 | author = "Dominik Picheta" 3 | description = "A static blog generator." 4 | license = "MIT" 5 | srcDir = "src" 6 | bin = @["ipsum"] 7 | 8 | requires "nim >= 0.19.4" 9 | -------------------------------------------------------------------------------- /layouts/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${title} 4 | 5 | 6 | 7 | ${body} 8 |
9 |

Last update: ${modDate}

10 | 11 | 12 | -------------------------------------------------------------------------------- /layouts/articles.html: -------------------------------------------------------------------------------- 1 | #? stdtmpl(subsChar = '$', metaChar = '#') 2 | #proc renderArticles(articles: seq[TArticleMetadata], prefix: string): string = 3 | 4 | # for a in articles: 5 | 6 | 7 | 8 | 9 | # end for 10 |
${a.title}${format(a.pubDate, "dd/MM/yyyy HH:mm")}
11 | #end proc 12 | # 13 | #proc renderTags(tags: seq[string], prefix: string): string = 14 | # for i in 0..tags.len-1: 15 | ${tags[i]} 16 | ${if i < tags.len-1: "," else: ""} 17 | # end for 18 | #end proc 19 | -------------------------------------------------------------------------------- /layouts/atom.xml: -------------------------------------------------------------------------------- 1 | #? stdtmpl(subsChar = '$', metaChar = '#') 2 | #proc renderAtom(meta: seq[TArticleMetadata], title, url, feedUrl, author: string): string = 3 | 4 | 5 | ${title} 6 | 7 | 8 | ${url} 9 | ipsumgenera 10 | ${getTime().utc().format("yyyy-MM-dd'T'HH:mm:ss'Z'")} 11 | # for a in meta: 12 | # let absoluteUrl = joinUrl(url, escapePath(genURL(a))) 13 | # let absoluteBase = splitPath(absoluteUrl).head 14 | 15 | ${a.title} 16 | 17 | ${absoluteUrl} 18 | ${a.pubDate.format("yyyy-MM-dd'T'HH:mm:ss'Z'")} 19 | ${a.modDate.format("yyyy-MM-dd'T'HH:mm:ss'Z'")} 20 | ${author} 21 | 22 | ${xmltree.escape(renderRst(a.body, url, absoluteUrls = absoluteBase))} 23 | 24 | 25 | # end for 26 | 27 | #end proc 28 | -------------------------------------------------------------------------------- /layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A default page 4 | 5 | 6 | 7 | ${body} 8 | 9 | 10 | -------------------------------------------------------------------------------- /layouts/static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${title} 4 | 5 | 6 | 7 | ${body} 8 |
9 |

Last update: ${modDate}

10 | 11 | 12 | -------------------------------------------------------------------------------- /layouts/tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A tag.html title: ${tag} 4 | 5 | 6 | 7 | ${body} 8 | 9 | 10 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ipsum genera 2 | 3 | *ipsum genera* is a static blog generator written in Nimrod. It's something 4 | I hacked together in a couple of days so you're mostly on your own for now if 5 | you wish to use it. Of course, I love the name *ipsum genera* so much that I 6 | may just be willing to make this more user friendly. 7 | 8 | ## Quick manual 9 | 10 | To set up *ipsum* you first need to install [nimrod's nimble package 11 | manager](https://github.com/nim-lang/nimble). Once you have that installed 12 | you can run: 13 | 14 | ``` 15 | git clone https://github.com/dom96/ipsumgenera 16 | cd ipsumgenera 17 | nimble install 18 | ``` 19 | 20 | This will compile *ipsum* and copy it to your ``$nimbleDir/bin/`` directory. Add 21 | this directory to your ``$PATH`` environment variable so you can run *ipsum* 22 | anywhere. In the future if you need to update your *ipsum* version you will 23 | need to refresh that checkout and run ``nimble install`` again to overwrite your 24 | current copy. 25 | 26 | Now, go to the directory of your choice for storing your own website and 27 | create the structure to hold your content: 28 | 29 | ``` 30 | cd ~/projects 31 | mkdir -p super_website/articles 32 | mkdir -p super_website/static 33 | mkdir -p super_website/layouts 34 | cd super_website 35 | ``` 36 | 37 | Put articles in the ``articles`` folder with metadata similar to jekyll's: 38 | ``` 39 | --- 40 | title: "GTK+: I love GTK and here is why." 41 | date: 2013-08-18 23:03 42 | tags: Nimrod, GTK 43 | --- 44 | ``` 45 | 46 | Save the article as: ``2013-08-18-gtk-plus-i-love-gtk-and-here-is-why.rst``, or 47 | something else, the filename doesn't really matter, *ipsum* does not care as 48 | long as they have an ``rst`` extension. All other extension files are ignored. 49 | 50 | Put static files in the ``static`` folder. If the file ends in ``.rst`` it has 51 | to have metadata like a normal article, but the generated html will keep the 52 | relative path instead of getting a generated one from the date+title. If the 53 | file doesn't end in ``.rst``, it will simply be copied to the output website 54 | directoy. 55 | 56 | You then need to create some layouts and put them into the layouts folder. 57 | You will need the following layouts: 58 | 59 | * article.html -- Template for an article. 60 | * static.html -- Template for a static file. 61 | * default.html -- Template for the index.html page, this includes the article 62 | list. 63 | * tag.html -- Template for the specific tag page, this will include a list of 64 | articles belonging to a certain tag. 65 | 66 | Layouts are simply html templates, *ipsum* will replace a couple of specific 67 | strings in the html templates when it's generating your blog. The format of 68 | these strings is ${key}, and the supported keys are: 69 | 70 | * ``${body}`` -- In ``default.html`` this is the article list, in 71 | ``article.html`` this will be the actual article text. 72 | * ``${title}`` -- Article title in ``article.html``, otherwise the blog title 73 | from ``ipsum.ini``. 74 | * ``${date}`` or ``${pubDate}`` -- Article publication date in ``article.html`` 75 | extracted from the metadata. The date has to be in format **YYYY-MM-DD 76 | hh:mm**. 77 | * ``${modDate}`` -- Article modification date in ``article.html`` extracted 78 | from the metadata, or if not available from the file's last modification 79 | timestamp. 80 | * ``${prefix}`` -- The path to the root in ``article.html`` **only**. 81 | * ``${tag}`` -- The tag name, ``tag.html`` **only**. 82 | 83 | Where ``article.html`` is mentioned, the same applies for ``static.html``. You 84 | will also need to create an ``ipsum.ini`` file, the file should contain the 85 | following information: 86 | 87 | ```ini 88 | title = "Blog title here" 89 | url = "http://this.blog.me" 90 | author = "Your Name" 91 | ``` 92 | 93 | The information from this config is also available in your templates, the key 94 | names match the names in the config file. Additional options you can add to the 95 | ``ipsum.ini`` file: 96 | 97 | * ``numRssEntries`` - Integer with the maximum number of generated entries in 98 | the rss feed. If you don't specify a value for this, the value ``10`` will be 99 | used by default. 100 | 101 | Once you're done with the setup, simply execute ``ipsum`` and your blog should 102 | be generated before you can even blink! 103 | 104 | ## Metadata reference 105 | 106 | * ``title`` - Article title. 107 | * ``date`` or ``pubDate`` - Date the article was written. 108 | * ``modDate`` - Specifies a posterior date the article was updated. If you 109 | don't specify this tag, it will be filled in with the file's last 110 | modification timestamp. 111 | * ``tags`` - Tags belonging to the article. 112 | * ``draft`` - If ``true`` article will not be generated. 113 | 114 | ## License 115 | 116 | [MIT license](LICENSE.md). 117 | -------------------------------------------------------------------------------- /src/config.nim: -------------------------------------------------------------------------------- 1 | import parsecfg, streams, strutils, os 2 | 3 | type 4 | TConfig* = object 5 | title*: string 6 | url*: string 7 | author*: string 8 | numRssEntries*: int 9 | 10 | proc initConfig(): TConfig = 11 | result.title = "" 12 | result.url = "" 13 | result.author = "" 14 | result.numRssEntries = 10 15 | 16 | proc validateConfig(config: TConfig) = 17 | template ra(field: string) = 18 | raise newException(ValueError, 19 | "You need to specify the '$1' field in the config." % field) 20 | if config.title == "": 21 | ra("title") 22 | if config.author == "": 23 | ra("author") 24 | if config.url == "": 25 | ra("url") 26 | if config.numRssEntries < 0: 27 | raise newException(ValueError, 28 | "The numRssEntries value can't be negative.") 29 | 30 | proc parseConfig*(filename: string): TConfig = 31 | if not filename.existsFile: 32 | raise newException(ValueError, "Missing '" & filename & "'") 33 | result = initConfig() 34 | var file = newFileStream(filename, fmRead) 35 | var cfg: CfgParser 36 | open(cfg, file, filename) 37 | while true: 38 | let ev = cfg.next() 39 | case ev.kind 40 | of cfgSectionStart: 41 | raise newException(ValueError, "No sections supported.") 42 | of cfgKeyValuePair, cfgOption: 43 | case ev.key.normalize 44 | of "title": 45 | result.title = ev.value 46 | of "url": 47 | result.url = ev.value 48 | of "author": 49 | result.author = ev.value 50 | of "numrssentries": 51 | result.numRssEntries = ev.value.parseInt 52 | of cfgError: 53 | raise newException(ValueError, ev.msg) 54 | of cfgEof: 55 | break 56 | cfg.close() 57 | file.close() 58 | validateConfig(result) 59 | -------------------------------------------------------------------------------- /src/ipsum.nim: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Dominik Picheta 2 | # Licensed under MIT license. 3 | 4 | import os, times, strutils, algorithm, strtabs, parseutils, tables, xmltree, 5 | sequtils, unicode, unidecode 6 | 7 | import metadata, rstrender, config 8 | 9 | const 10 | articleDir = "articles" 11 | outputDir = "output" 12 | staticDir = "static" 13 | tagPagePrefix = "../" 14 | 15 | 16 | proc normalizeTitle(title: string): string = 17 | for i in title.unidecode.runes(): 18 | case char(i) 19 | of ':', '-', '=', '*', '^', '%', '$', '#', '@', '!', '{', '}', '[', ']', 20 | '<', '>', ',', '.', '?', '|', '~', '\\', '/', '"', '\'': 21 | discard 22 | of '&': result.add "-and-" 23 | of '+': result.add "-plus-" 24 | of ' ': result.add '-' 25 | else: 26 | result.add i.toLower.char() 27 | 28 | proc normalizeTag(tag: string): string = 29 | for i in tag.runes(): 30 | case i 31 | of Rune(' '): 32 | result.add '-' 33 | else: 34 | result.add i.toLower.toUTF8() 35 | 36 | proc joinUrl(x: varargs[string]): string = 37 | for i in x: 38 | var cleanI = i 39 | if cleanI[0] == '/': cleanI = cleanI[1 .. ^1] 40 | if cleanI.endsWith("/"): cleanI = cleanI[0 .. ^2] 41 | 42 | result.add "/" & cleanI 43 | result = result[1 .. ^1] # Get rid of the / at the start. 44 | 45 | proc genUrl(article: TArticleMetadata): string = 46 | # articles/2013/03/title.html 47 | joinUrl(articleDir, format(article.pubDate, "yyyy/MM"), 48 | article.title.normalizeTitle() & ".html") 49 | 50 | proc escapePath(s: string): string = 51 | ## Escapes a path to be valid according to RFC 3987. 52 | ## 53 | ## This proc does not perform the full work of parsing and filtering a full 54 | ## IRI, it is only used to filter local filename paths and escape their 55 | ## possible unicode characters for inclusion as rss identifiers. If you pass 56 | ## a full absolute URL, the scheme part will be malformed. A full correct 57 | ## implementation would handle the different parts of the URL correctly. 58 | ## 59 | ## This is a variation of the algorithm found in cgi.URLencode. Unicode 60 | ## characters are parsed correctly because Nimrod strings happen to be 61 | ## encoded in UTF8 and the rfc specifies that the encoded bytes need to 62 | ## translated to %HH format. 63 | result = newStringOfCap(s.len + s.len shr 2) # assume 12% non-alnum-chars 64 | for i in 0..s.len-1: 65 | case s[i] 66 | of 'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '~', '/', '_': add(result, s[i]) 67 | else: 68 | add(result, '%') 69 | add(result, toHex(ord(s[i]), 2)) 70 | 71 | proc findArticles(): seq[string] = 72 | var dir = getCurrentDir() / articleDir 73 | for f in walkFiles(dir / "*.rst"): 74 | result.add(f) 75 | 76 | proc findStaticFiles(): seq[string] = 77 | ## Returns a list of files in the static subdirectory. 78 | ## 79 | ## Unlike findArticles, the returned paths are not absoulte, they are 80 | ## relative to the static directory. You need to prefix the results with 81 | ## staticDir to reach the file. 82 | const valid = {pcFile, pcLinkToFile, pcDir, pcLinkToDir} 83 | let pruneLen = staticDir.len 84 | for f in walkDirRec(staticDir, valid): 85 | assert f.len > pruneLen + 1 86 | result.add(f[pruneLen + 1 ..< f.len]) 87 | 88 | include "../layouts/articles.html" # TODO: Rename to articlelist.html? 89 | include "../layouts/atom.xml" 90 | const defaultArticleLayout = staticRead("../layouts/article.html") 91 | const defaultStaticLayout = staticRead("../layouts/static.html") 92 | const defaultDefaultLayout = staticRead("../layouts/default.html") 93 | const defaultTagLayout = staticRead("../layouts/tag.html") 94 | 95 | proc replaceKeys(s: string, kv: StringTableRef): string = 96 | var i = 0 97 | while i < s.len: 98 | case s[i] 99 | of '\\': 100 | if s[i+1] == '$': 101 | result.add("$") 102 | i.inc 2 103 | else: 104 | result.add(s[i]) 105 | i.inc 106 | of '$': 107 | assert s[i+1] == '{' 108 | let key = captureBetween(s, '{', '}', i) 109 | if not hasKey(kv, key): 110 | raise newException(ValueError, "Key not found: " & key) 111 | result.add(kv[key]) 112 | i.inc key.len + 3 113 | else: 114 | result.add s[i] 115 | i.inc 116 | 117 | proc createKeys(otherKeys: varargs[tuple[k, v: string]], 118 | cfg: TConfig): StringTableRef = 119 | result = newStringTable({"blog_title": cfg.title, "blog_url": cfg.url, 120 | "blog_author": cfg.author}) 121 | for i in otherKeys: 122 | result[i.k] = i.v 123 | 124 | proc needsRefresh*(target: string, src: varargs[string]): bool = 125 | ## Returns true if target is missing or src has newer modification date. 126 | ## 127 | ## Copied from 128 | ## https://github.com/fowlmouth/nake/blob/078a99849a29a890fc22a14173ca4591a14dea86/nake.nim#L150 129 | assert len(src) > 0, "Pass some parameters to check for" 130 | var targetTime: int64 131 | try: 132 | targetTime = toUnix(getLastModificationTime(target)) 133 | except OSError: 134 | return true 135 | 136 | for s in src: 137 | let srcTime = toUnix(getLastModificationTime(s)) 138 | if srcTime > targetTime: 139 | return true 140 | 141 | 142 | proc generateArticle(filename, dest, style: string, meta: TArticleMetadata, 143 | cfg: TConfig) = 144 | ## Generates an html file from the rst `filename`. 145 | ## 146 | ## Pass as `dest` the relative final path of the input `filename`. `style` is 147 | ## the name of one of the files in th layouts subdirectory. 148 | let 149 | def = 150 | try: 151 | readFile(getCurrentDir() / "layouts" / style) 152 | except IOError: 153 | case style 154 | of "article.html": 155 | echo("Using internal layouts/article.html") 156 | defaultArticleLayout 157 | of "static.html": 158 | echo("Using internal layouts/static.html") 159 | defaultStaticLayout 160 | else: 161 | raise 162 | pubDate = format(meta.pubDate, "dd/MM/yyyy HH:mm") 163 | modDate = format(meta.modDate, "dd/MM/yyyy HH:mm") 164 | # Calculate prefix depending on the depth of `dest`. 165 | var prefix = "" 166 | for i in parentDirs(dest, inclusive = false): 167 | prefix = "../" & prefix 168 | let tags = renderTags(meta.tags, prefix) 169 | let output = replaceKeys(def, 170 | {"title": meta.title, "date": pubDate, "pubDate": pubDate, 171 | "modDate": modDate, "body": renderRst(meta.body, prefix), 172 | "prefix": prefix, "tags": tags}.createKeys(cfg)) 173 | let path = getCurrentDir() / outputDir / dest 174 | createDir(path.splitFile.dir) 175 | 176 | writeFile(path, output) 177 | 178 | proc processStatic(cfg: TConfig): seq[TArticleMetadata] = 179 | ## Processes files found in the static subdirectory. 180 | ## 181 | ## Non rst files will be copied as is, rst files will be processed with the 182 | ## static template. 183 | ## 184 | ## The proc will return the list of the metadata for parsed rst files. 185 | let staticFilenames = findStaticFiles() 186 | for i in staticFilenames: 187 | let 188 | src = staticDir / i 189 | ext = i.splitFile.ext.toLower 190 | if ext == ".rst": 191 | let dest = changeFileExt(i, "html") 192 | echo("Processing ", getCurrentDir() / outputDir / dest) 193 | let meta = parseMetadata(src) 194 | if meta.isDraft: 195 | echo(" Article is a draft, omitting from article list.") 196 | continue 197 | result.add(meta) 198 | generateArticle(src, dest, "static.html", meta, cfg) 199 | else: 200 | let dest = getCurrentDir() / outputDir / i 201 | if dest.needsRefresh(src): 202 | echo "Copying ", dest 203 | createDir(dest.splitFile.dir) 204 | copyFileWithPermissions(src, dest) 205 | 206 | proc generateDefault(mds: seq[TArticleMetadata], cfg: TConfig) = 207 | let def = 208 | try: 209 | readFile(getCurrentDir() / "layouts" / "default.html") 210 | except IOError: 211 | echo("Using internal layouts/default.html") 212 | defaultDefaultLayout 213 | let output = replaceKeys(def, 214 | {"body": renderArticles(mds, ""), "prefix": ""}.createKeys(cfg)) 215 | writeFile(getCurrentDir() / outputDir / "index.html", output) 216 | 217 | proc sortArticles(articles: var seq[TArticleMetadata]) = 218 | articles.sort do (x, y: TArticleMetadata) -> int: 219 | if timeInfoToTime(x.pubDate) > timeInfoToTime(y.pubDate): 220 | -1 221 | elif timeInfoToTime(x.pubDate) == timeInfoToTime(y.pubDate): 222 | 0 223 | else: 224 | 1 225 | 226 | proc processArticles(cfg: TConfig): seq[TArticleMetadata] = 227 | let articleFilenames = findArticles() 228 | for i in articleFilenames: 229 | echo("Processing ", i) 230 | let meta = parseMetadata(i) 231 | if not meta.isDraft: 232 | result.add(meta) 233 | else: 234 | echo(" Article is a draft, omitting from article list.") 235 | generateArticle(i, genURL(meta), "article.html", meta, cfg) 236 | 237 | # Sort articles from newest to oldest. 238 | sortArticles(result) 239 | 240 | proc generateTagPages(meta: seq[TArticleMetadata], cfg: TConfig) = 241 | var tags = initTable[string, seq[TArticleMetadata]]() 242 | for a in meta: 243 | for t in a.tags: 244 | let nt = t.normalizeTag 245 | if not tags.hasKey(nt): 246 | tags[nt] = @[] 247 | tags.mget(nt).add(a) 248 | 249 | let templ = 250 | try: 251 | readFile(getCurrentDir() / "layouts" / "tag.html") 252 | except IOError: 253 | echo("Using internal layouts/tag.html") 254 | defaultTagLayout 255 | createDir(getCurrentDir() / outputDir / "tags") 256 | for tag, articles in tags: 257 | var sorted = articles 258 | sortArticles(sorted) 259 | let output = replaceKeys(templ, 260 | {"body": renderArticles(sorted, tagPagePrefix), "tag": tag, 261 | "prefix": tagPagePrefix}.createKeys(cfg)) 262 | writeFile(getCurrentDir() / outputDir / "tags" / 263 | tag.addFileExt("html"), output) 264 | 265 | proc generateAtomFeed(meta: seq[TArticleMetadata], cfg: TConfig) = 266 | # Prunes the article sequence according to the configuration limit. 267 | assert cfg.numRssEntries >= 0 268 | var meta = meta 269 | if meta.len > cfg.numRssEntries: 270 | meta.delete(cfg.numRssEntries + 1, 0: 8 | for i in node.sons: 9 | if i == nil: 10 | result.add(repeat(' ', indent + 2) & "NIL SON!!!\n"); continue 11 | result.add strRst(i, indent + 2) 12 | 13 | proc renderCodeBlock(n: PRstNode): string = 14 | ## Renders a block with code syntax highlighting. 15 | if n.sons[2] == nil: return 16 | var m = n.sons[2].sons[0] 17 | assert m.kind == rnLeaf 18 | var langstr = strip(getArgument(n)) 19 | var lang: SourceLanguage 20 | if langstr == "": 21 | lang = langNim # default language 22 | else: 23 | lang = getSourceLanguage(langstr) 24 | 25 | result.add "
"
 26 |   if lang == langNone:
 27 |     echo("[Warning] Unsupported language: " & langstr)
 28 |     result.add(xmltree.escape(m.text))
 29 |   else:
 30 |     var g: GeneralTokenizer
 31 |     initGeneralTokenizer(g, m.text)
 32 |     while true:
 33 |       getNextToken(g, lang)
 34 |       case g.kind
 35 |       of gtEof: break
 36 |       of gtNone, gtWhitespace:
 37 |         add(result, substr(m.text, g.start, g.length + g.start - 1))
 38 |       else:
 39 |         result.add span(class=tokenClassToStr[g.kind],
 40 |             xmltree.escape(substr(m.text, g.start, g.length+g.start-1)))
 41 |     deinitGeneralTokenizer(g)
 42 |   result.add "
" 43 | 44 | proc renderLiteralBlock(n: PRstNode): string = 45 | ## Renders a plain literal block. 46 | if len(n.sons) < 1: return 47 | result.add "
"
 48 |   for m in n.sons:
 49 |     assert m.kind == rnLeaf
 50 |     result.add(xmltree.escape(m.text))
 51 |   result.add "
" 52 | 53 | proc renderRawDirective(n: PRstNode): string = 54 | ## Renders all children leaf nodes as plain text without escaping. 55 | ## 56 | ## rnDirArg nodes are not rendered but verified to contain the string html. 57 | ## If the string doesn't match, the rest of the tree is ignored and the proc 58 | ## returns immediately. 59 | for i in n.sons: 60 | if not i.isNil(): 61 | case i.kind 62 | of rnLeaf: 63 | result.add(i.text) 64 | of rnDirArg: 65 | assert i.sons.len == 1 66 | assert i.sons[0].kind == rnLeaf 67 | let params = i.sons[0].text 68 | if params != "html": 69 | echo("Ignoring raw directive block '", params, "'") 70 | return 71 | else: 72 | result.add renderRawDirective(i) 73 | 74 | proc renderPrefixUrl(url, articlePrefix, absoluteUrls: string): string = 75 | ## Adds a prefix to an url, optionally making it absolute. 76 | ## 77 | ## Returns `url` with instances of the ``${prefix}`` substring replaced with 78 | ## `articlePrefix`. 79 | ## 80 | ## Additinally, if `absoluteUrls` is not the empty string, the resulting 81 | ## value is checked for being an absolute path. If it is a relative path, it 82 | ## will be *joined* with `absoluteUrls`. 83 | result = url.replace("${prefix}", articlePrefix) 84 | # Avoid absoluteUrls replacement if nil or emtpy string. 85 | if absoluteUrls.len < 1: return 86 | # Discard absolute urls using domain relative paths (aka "/foo/bar") 87 | if result.len < 1 or result[0] == '/': return 88 | # Discard absolute urls which contain the substring ":/". 89 | let first = result.find({'/', ':'}) 90 | if first > 0 and first + 1 < result.len and 91 | result[first] == ':' and result[first+1] == '/': 92 | return 93 | # If we reached here, it is a relative path. 94 | result = absoluteUrls/result 95 | 96 | 97 | proc renderRst(node: PRstNode, articlePrefix, absoluteUrls: string): string 98 | proc getFieldList(node: PRstNode, 99 | articlePrefix, absoluteUrls: string): StringTableRef = 100 | assert node.kind == rnFieldList 101 | result = newStringTable() 102 | for field in node.sons: 103 | assert field.kind == rnField 104 | assert field.sons[0].kind == rnFieldName 105 | assert field.sons[1].kind == rnFieldBody 106 | let name = renderRst(field.sons[0], articlePrefix, absoluteUrls) 107 | let value = renderRst(field.sons[1], articlePrefix, absoluteUrls) 108 | result[name] = value 109 | 110 | proc renderRst(node: PRstNode, articlePrefix, absoluteUrls: string): string = 111 | proc renderSons(father: PRstNode): string = 112 | for i in father.sons: 113 | if not i.isNil(): 114 | result.add renderRst(i, articlePrefix, absoluteUrls) 115 | 116 | case node.kind 117 | of rnInner: 118 | result.add renderSons(node) 119 | of rnParagraph: 120 | result.add p(renderSons(node)) & "\n" 121 | of rnLeaf: 122 | result.add(xmltree.escape(node.text)) 123 | of rnStandaloneHyperlink: 124 | let hyper = renderSons(node).renderPrefixUrl(articlePrefix, absoluteUrls) 125 | result.add a(href=hyper, hyper) 126 | of rnHyperLink: 127 | let hyper = renderSons(node.sons[1]).renderPrefixUrl(articlePrefix, 128 | absoluteUrls) 129 | result.add a(href=hyper, renderSons(node.sons[0])) 130 | of rnEmphasis: 131 | result.add span(style="font-style: italic;", renderSons(node)) 132 | of rnStrongEmphasis: 133 | result.add span(style="font-weight: bold;", renderSons(node)) 134 | of rnHeadline: 135 | case node.level 136 | of 1: 137 | result.add h1(renderSons(node)) 138 | of 2: 139 | result.add h2(renderSons(node)) 140 | of 3: 141 | result.add h3(renderSons(node)) 142 | else: 143 | assert false, "Unknown headline level: " & $node.level 144 | of rnInlineLiteral: 145 | result.add code(renderSons(node)) 146 | of rnLiteralBlock: 147 | result.add renderLiteralBlock(node) 148 | of rnCodeBlock: 149 | result.add renderCodeBlock(node) 150 | of rnTransition: 151 | result.add hr() 152 | of rnBlockquote: 153 | result.add blockquote(renderSons(node)) 154 | of rnEnumList: 155 | result.add ol(renderSons(node)) 156 | of rnBulletList: 157 | result.add ul(renderSons(node)) 158 | of rnBulletItem, rnEnumItem: 159 | result.add li(renderSons(node)) 160 | of rnImage: 161 | let src = renderSons(node.sons[0]).renderPrefixUrl(articlePrefix, 162 | absoluteUrls) 163 | if not node.sons[1].isNil() and node.sons[1].kind == rnFieldList: 164 | let fieldList = getFieldList(node.sons[1], articlePrefix, absoluteUrls) 165 | var style = "" 166 | for k, v in pairs(fieldList): 167 | case k 168 | of "height", "width": 169 | style.add("$1: $2;" % [k, v]) 170 | else: raise newException(ValueError, "Invalid field name for image.") 171 | result.add "" % [src, style] 172 | else: 173 | result.add img(src=src, alt="") 174 | of rnFieldName, rnFieldBody: 175 | result.add(renderSons(node)) 176 | of rnRawHtml: 177 | result.add(renderRawDirective(node)) 178 | of rnTable: 179 | result.add "" 180 | result.add renderSons(node) 181 | result.add "
" 182 | of rnTableRow: 183 | result.add "" 184 | result.add renderSons(node) 185 | result.add "\n" 186 | of rnTableDataCell: 187 | result.add "" 188 | result.add renderSons(node) 189 | result.add "\n" 190 | of rnTableHeaderCell: 191 | result.add "" 192 | result.add renderSons(node) 193 | result.add "\n" 194 | else: 195 | echo("Unknown node kind in rst: ", node.kind) 196 | doAssert false 197 | 198 | proc renderRst*(text: string, articlePrefix: string, filename = "", 199 | absoluteUrls = ""): string = 200 | ## Returns the rst `text` string as rendered HTML. 201 | ## 202 | ## The `articlePrefix` string will replace strings in the form ${prefix} in 203 | ## the urls found in the rst `text`. You can pass here the absolute root URL 204 | ## to where you will place all the generated HTML files so that you can write 205 | ## links in the form ``${prefix}images/smiley.gif`` and they will resolve 206 | ## correctly from every subdirectory. 207 | ## 208 | ## The `filename` parameter is used for ornamental purposes, if something 209 | ## fails it will be displayed to the user there, but otherwise serves no real 210 | ## purpose. 211 | ## 212 | ## The `absoluteUrls` is only used for RSS HTML generation. Different RSS 213 | ## readers have different behaviours when resolving relative URLs, so if you 214 | ## write a relative link to another article, most likely the RSS reader will 215 | ## build an incorrect absolute URL. To avoid this, pass the absolute URL for 216 | ## the directory where the generated HTML will be placed, and it will be 217 | ## prepended automatically to all relative links. If you are generating 218 | ## standalone HTML, pass the empty string to leave relative link unmodified. 219 | var hasToc = false 220 | var ast = rstParse(text, filename, 0, 0, hasToc, 221 | {roSupportRawDirective, roSupportMarkdown}) 222 | #echo strRst(ast) 223 | result = renderRst(ast, articlePrefix, absoluteUrls) 224 | 225 | when isMainModule: 226 | import os, metadata 227 | var i = 0 228 | var filename = getCurrentDir().parentDir() / "articles" / "2013-03-13-gtk-plus-a-method-to-guarantee-scrolling.rst" 229 | discard parseMetadata(filename, i) 230 | echo renderRst(readFile(filename)[i .. ^1], filename) 231 | --------------------------------------------------------------------------------