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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/metadata.nim:
--------------------------------------------------------------------------------
1 | import strutils, times, parseutils, os
2 |
3 | type
4 | TArticleMetadata* = object
5 | title*: string
6 | pubDate*: DateTime
7 | modDate*: DateTime
8 | tags*: seq[string]
9 | isDraft*: bool
10 | body*: string
11 | MetadataInProgress = object
12 | data: TArticleMetadata
13 | progress: int
14 | title: bool
15 | pubDate: bool
16 | modDate: bool
17 | tags: bool
18 | body: bool
19 |
20 | proc parseDate(val: string): DateTime =
21 | parse(val, "yyyy-MM-dd HH:mm:ss", utc())
22 |
23 | proc parseMetadata*(filename: string): TArticleMetadata =
24 | var meta = MetadataInProgress(data: TArticleMetadata(pubDate: now(), modDate: now()))
25 | template `:=`(a, b: untyped): untyped =
26 | assert(not meta.a)
27 | meta.data.a = b
28 | meta.a = true
29 | inc meta.progress
30 |
31 | let article = readFile(filename)
32 | var i = 0
33 | i.inc skip(article, "---", i)
34 | if i == 0:
35 | raise newException(ValueError,
36 | "Article must begin with '---' signifying meta data.")
37 | i.inc skipWhitespace(article, i)
38 | while true:
39 | if article[i .. i + 2] == "---": break
40 | if article[i] == '#':
41 | i.inc skipUntil(article, Whitespace - {' '}, i)
42 | i.inc skipWhitespace(article, i)
43 | continue
44 |
45 | var key = ""
46 | i.inc parseUntil(article, key, {':'} + Whitespace, i)
47 | if article[i] != ':':
48 | raise newException(ValueError, "Expected ':' after key in meta data.")
49 | i.inc # skip :
50 | i.inc skipWhitespace(article, i)
51 |
52 | var value = ""
53 | i.inc parseUntil(article, value, Whitespace - {' '}, i)
54 | i.inc skipWhitespace(article, i)
55 | case key.normalize
56 | of "title":
57 | if value[0] == '"' and value[^1] == '"':
58 | value = value[1 .. ^2]
59 | title := value
60 | of "date", "pubdate":
61 | pubDate := parseDate(value)
62 | of "moddate":
63 | modDate := parseDate(value)
64 | of "tags":
65 | tags := @[]
66 | for i in value.split(','):
67 | meta.data.tags.add(i.strip)
68 | of "draft":
69 | let vn = value.normalize
70 | meta.data.isDraft = vn in ["t", "true", "y", "yes"]
71 | else:
72 | raise newException(ValueError, "Unknown key: " & key)
73 | i.inc 3 # skip ---
74 | i.inc skipWhitespace(article, i)
75 | body := article[i .. ^1]
76 | # Give last modification date as file timestamp if nothing else was found.
77 | if not meta.modDate:
78 | modDate := filename.getLastModificationTime.utc
79 |
80 | doAssert(meta.progress == 5)
81 | return meta.data
82 |
--------------------------------------------------------------------------------
/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/rstrender.nim:
--------------------------------------------------------------------------------
1 | import packages/docutils/rst, packages/docutils/rstast, packages/docutils/highlite
2 | import strutils, htmlgen, xmltree, strtabs, os
3 |
4 | proc strRst(node: PRstNode, indent: int = 0): string =
5 | ## Internal proc for debugging.
6 | result.add(repeat(' ', indent) & $node.kind & "(t: " & node.text & ", l=" & $node.level & ")" & "\n")
7 | if node.sons.len > 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 "