import os, strutils, strformat
import unicode, algorithm
import docopt
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())