~kungtotte/dtt

ref: f2aa3241167242ee372e2f8ef3095d90def6e241 dtt/src/dtt.nim -rw-r--r-- 5.5 KiB
f2aa3241Thomas Landin Swap nim-markdown for slimdown for --gc:arc 9 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import os, strutils, strformat
import unicode, algorithm
import tables

import mustache
import argparse

import tmpl
import utils

const
  bin = "dtt"
  outFilePerms = {fpUserExec, fpUserWrite, fpUserRead, fpGroupExec,
                  fpGRoupRead, fpOthersExec, fpOthersRead}

let
  version = "0.3.0"

type
  DttError {.pure.} = enum
    CreateDirectoryFailed,
    DirectoryNotFound,
    DirectoryNotEmpty,
    NotADttDirectory,
    RemoveDirectoryFailed,
    UnknownError

  PageMeta = tuple
    filename: string
    abs_path: string
    out_path: string
    rel_path: string
    tmpl: string
    title: string

proc init(force: bool = false, dir: string) =
  let wd = if dir == "nil": getCurrentDir() else: getCurrentDir() / dir
  if not existsDir(wd):
    createDir(wd)

  # TODO: Update this so that init will re-create any -missing- files
  # but won't overwrite existing files.
  # My hunch is that the best way to do this is to have a list of things
  # to create and as we walk that list we check them one by one if the target
  # already exists, overriding it with `force`.
  if not force:
    for f in walkDirRec(wd, {pcFile, pcDir}, relative = true):
      case getFileInfo(wd / f).kind:
        of pcDir:
          if os.existsDir(wd / f):
            stderr.writeLine(fmt"{bin}: working directory not empty, add -f/--force to overwrite")
            quit(ord(DttError.DirectoryNotEmpty))
        of pcFile:
          if os.existsFile(wd / f):
            stderr.writeLine(fmt"{bin}: working directory not empty, add -f/--force to overwrite")
            quit(ord(DttError.DirectoryNotEmpty))
        else:
          stderr.writeLine(fmt"Unknown error occurred: {f} is neither file nor directory")
          quit(ord(DttError.UnknownError))
  try:
    os.createDir(wd / content_dir)
    os.createDir(wd / template_dir)
    os.createDir(wd / output_dir)
    os.createDir(wd / output_dir / css_dir)
  except OSError:
    let
      e = repr(getCurrentException())
      msg = getCurrentExceptionMsg()
    stderr.writeLine(fmt"{bin}: Exception: {e} with message: {msg}")
    quit(ord(DttError.CreateDirectoryFailed))

  writeFile(wd / template_dir / "page.mustache", tmpl.page)
  writeFile(wd / template_dir / "post.mustache", tmpl.post)
  writeFile(wd / template_dir / "header.mustache", tmpl.header)
  writeFile(wd / template_dir / "footer.mustache", tmpl.footer)
  writeFile(wd / output_dir / css_dir / "style.css", tmpl.style)
  setFilePermissions(wd / output_dir / css_dir / "style.css", outFilePerms)

proc build(posts_per_page: int = 5) =
  var working_dir: string
  try:
    working_dir = findBaseDir(os.getCurrentDir())
  except IOError:
    stderr.writeLine(fmt"{bin}: Could not find a content folder. Is this a dtt directory?")
    quit(ord(DttError.NotADttDirectory))
  let
    content_dir = working_dir / content_dir
    tmpl_dir = working_dir / template_dir
    output_dir = working_dir / output_dir

  if not existsDir(output_dir):
    createDir(output_dir)

  var
    pages: seq[PageMeta]
    posts: seq[BlogPost]
    links: seq[Table[string, string]]
  for f in walkDirRec(content_dir, relative = true):
    let
      (dir, name, ext) = splitFile(f)
      savepath = output_dir / dir
    createDir(savepath)
    if ext != ".md":
      # Any non-Markdown files are just copied straight across
      # This lets people use static assets and also hand-craft
      # html pages if they want to.
      let output_file = savepath / name & ext
      copyFile(content_dir / f, output_file)
      setFilePermissions(output_file, outFilePerms)
    else:
      var page: PageMeta
      page.title = name.title()
      page.filename = name & ".html"
      page.abs_path = content_dir / f
      page.rel_path = dir / name & ".html"
      page.out_path = savepath
      page.tmpl = if existsFile(tmpl_dir / name & ".mustache"): name else: "page"
      pages.add(page)
      if isBlog(dir, name):
        let post = buildBlogPost(content_dir / f, tmpl_dir)
        posts.add(post)
      else:
        links.add({"target": page.rel_path, "title": page.title}.toTable)

  let
    num_blogs = if posts_per_page == -1: posts.high else: min(posts.len - 1, posts_per_page)
  sort(posts, dateSort, Descending) # Sort blogs in reverse date order
  for page in pages:
    var context = newContext(searchDirs = @[tmpl_dir])
    context["links"] = links
    if posts.len > 0:
      context["latest_post"] = postsToContext(posts[0..<1])
      context["blog_posts"] = postsToContext(posts[0..num_blogs])
      context["blog_index"] = postsToContext(posts[0..posts.high])
    let
      tmpl = loadTemplate(page.tmpl, tmpl_dir)
      rendered = context.renderTemplate(readFile(page.abs_path), tmpl)
      output_file = page.out_path / page.filename
    writeFile(output_file, rendered)

const doc = """
An extremely straight-forward static site generator that will convert
Markdown files into html and publish them along with any static content
(images, CSS, html files, etc.) to an output dir to be served as static
content from any host.
"""

when isMainModule:
  var argp = newParser(bin):
    help(doc)
    flag("-v", "--version")
    run:
      if opts.version:
        echo fmt"{bin} {version}"
    command("init"):
      help("Initialize a dtt project in the current directory, or [dir] if passed.")
      arg("dir", default="nil")
      flag("-f", "--force")
      run:
        init(opts.force, opts.dir)
    command("build"):
      help("Construct the site inside the ./output/ directory.")
      arg("posts", default="5")
      run:
        build(opts.posts.parseInt)
  argp.run(commandLineParams())