Started implementation: YAML metadata processing, change detection.
h3rald h3rald@h3rald.com
Sun, 23 Oct 2016 19:59:05 +0200
9 files changed,
1028 insertions(+),
0 deletions(-)
A
config.nim
@@ -0,0 +1,37 @@
+import + os, + parsecfg, + streams, + strutils + +const + cfgfile = "hastysite.nimble".slurp + +var + appname* = "HastySite" + version*: string + f = newStringStream(cfgfile) + +if f != nil: + var p: CfgParser + open(p, f, "hastysite.nimble") + while true: + var e = next(p) + case e.kind + of cfgEof: + break + of cfgKeyValuePair: + case e.key: + of "version": + version = e.value + else: + discard + of cfgError: + stderr.writeLine("Configuration error.") + quit(1) + else: + discard + close(p) +else: + stderr.writeLine("Cannot process configuration file.") + quit(2)
A
hastysite.nim
@@ -0,0 +1,185 @@
+import + json, + strutils, + yaml, + pegs, + os, + securehash, + sequtils + +import + config + +type + HastySite* = object + assets*: string + contents*: string + layouts*: string + output*: string + rules*: string + temp*: string + meta: string + checksums: string + tempContents: string + modified: seq[string] + NoMetadataException* = ref Exception + + +proc preprocessFile(file, dir: string, obj: var JsonNode): string = + let fileid = file.replace(dir, "") + var f: File + discard f.open(file) + var s, yaml = "" + result = "" + var delimiter = 0 + while f.readLine(s): + if delimiter >= 2: + result &= s + else: + if s.match(peg"'-' '-' '-' '-'*"): + delimiter.inc + else: + yaml &= "\n" & s + if yaml == "": + raise NoMetadataException(msg: "No metadata found in file: " & file) + if not obj.hasKey("contents"): + obj["contents"] = newJObject() + obj["contents"][fileid] = yaml.loadToJson()[0] + f.close() + +proc checkFile(file, dir: string, obj: var JsonNode): bool = + let fileid = file.replace(dir, "") + if not obj.hasKey("contents"): + obj["contents"] = newJObject() + var oldChecksum = "" + if obj["contents"].hasKey(fileid): + oldChecksum = obj["contents"][fileid].getStr + var newChecksum = $secureHashFile(file) + obj["contents"][fileid] = %newChecksum + return oldChecksum != newChecksum + +proc get(json: JsonNode, key, default: string): string = + if json.hasKey(key): + return json[key].getStr + else: + return default + +proc confirmClean(hs: HastySite): bool = + stdout.write("Delete directory '$1' and all its contents? [Y/n] " % hs.temp) + let confirm = stdin.readChar + return confirm == 'Y' or confirm == 'y' + +proc quitIfNotExists(file: string) = + if not file.fileExists: + quit("Error: File '$1' not found." % file) + +proc newHastySite*(file: string): HastySite = + let json = file.parseFile() + result.assets = json.get("assets", "assets") + result.contents = json.get("contents", "contents") + result.layouts = json.get("layouts", "layouts") + result.output = json.get("output", "output") + result.rules = json.get("rules", "rules.min") + result.temp = json.get("temp", "temp") + result.meta = result.temp / "metadata.json" + result.checksums = result.temp / "checksums.json" + result.tempContents = result.temp / result.contents + +proc preprocess*(hs: HastySite) = + var meta = newJObject() + for f in hs.contents.walkDirRec(): + let content = f.preprocessFile(hs.contents & DirSep, meta) + let dest = hs.temp/f + dest.parentDir.createDir + dest.writeFile(content) + hs.meta.writeFile(meta.pretty) + +proc detectChanges*(hs: var HastySite) = + hs.modified = newSeq[string](0) + if not hs.checksums.fileExists: + hs.checksums.writeFile("{}") + var cs = hs.checksums.parseFile() + let files = toSeq(hs.tempContents.walkDirRec()) + let dir = hs.tempContents + hs.modified = filter(files) do (f: string) -> bool: f.checkFile(dir & DirSep, cs) + hs.checksums.writeFile(cs.pretty) + +proc init*(dir: string) = + var json = newJObject() + json["contents"] = %"contents" + json["assets"] = %"assets" + json["layouts"] = %"layouts" + json["temp"] = %"temp" + json["output"] = %"output" + for key, value in json.pairs: + createDir(dir/value.getStr) + json["title"] = %"My Web Site" + json["rules"] = %"rules.min" + writeFile(dir/json["rules"].getStr, "") + writeFile(dir/"config.json", json.pretty) + +proc clean*(hs: HastySite) = + hs.temp.removeDir + +proc build*(hs: var HastySite) = + echo "Preprocessing..." + hs.preprocess() + hs.detectChanges() + # TODO + echo hs.modified + +when isMainModule: + + import + vendor/commandeer + + proc usage(): string = + return """ $1 v$2 - a tiny static site generator + (c) 2016 Fabio Cevasco + + Usage: + hastysite command + + Commands: + init Initializes a new site in the current directory. + build Builds the site. + clean Cleans temporary file. + rebuild Rebuilds the site, after cleaning temporary files. + Options: + -h, --help Print this help + -v, --version Print the program version""" % [appname, version] + + + + commandline: + argument command, string + exitoption "help", "h", usage() + exitoption "version", "v", version + errormsg usage() + + let pwd = getCurrentDir() + let cfg = pwd/"config.json" + case command: + of "init": + pwd.init() + of "build": + quitIfNotExists(cfg) + var hs = newHastySite(cfg) + hs.build() + of "clean": + quitIfNotExists(cfg) + var hs = newHastySite(cfg) + if hs.confirmClean(): + hs.clean() + else: + quit("Aborted.") + of "rebuild": + quitIfNotExists(cfg) + var hs = newHastySite(cfg) + if hs.confirmClean(): + hs.clean() + hs.build() + else: + quit("Aborted.") + else: + quit("Error: Command '$1' is not supported" % command)
A
hastysite.nim.cfg
@@ -0,0 +1,24 @@
+# https://gist.github.com/Drakulix/9881160 +amd64.windows.gcc.path = "/usr/local/mingw/bin" +amd64.windows.gcc.exe = "x86_64-w64-mingw32-gcc" +amd64.windows.gcc.linkerexe = "x86_64-w64-mingw32-gcc" + +# https://gist.github.com/Drakulix/9881160 +i386.windows.gcc.path = "/usr/local/mingw/bin" +i386.windows.gcc.exe = "i686-w64-mingw32-gcc" +i386.windows.gcc.linkerexe = "i686-w64-mingw32-gcc" + +# http://crossgcc.rts-software.org/doku.php?id=compiling_for_linux +i386.linux.gcc.path = "/usr/local/gcc-4.8.1-for-linux32/bin" +i386.linux.gcc.exe = "i586-pc-linux-gcc" +i386.linux.gcc.linkerexe = "i586-pc-linux-gcc" + +# http://crossgcc.rts-software.org/doku.php?id=compiling_for_linux +amd64.linux.gcc.path = "/usr/local/gcc-4.8.1-for-linux64/bin" +amd64.linux.gcc.exe = "x86_64-pc-linux-gcc" +amd64.linux.gcc.linkerexe = "x86_64-pc-linux-gcc" + +# http://www.jaredwolff.com/toolchains/ +arm.linux.gcc.path = "/usr/local/arm-none-linux-gnueabi/bin" +arm.linux.gcc.exe = "arm-none-linux-gnueabi-gcc" +arm.linux.gcc.linkerexe = "arm-none-linux-gnueabi-gcc"
A
hastysite.nimble
@@ -0,0 +1,10 @@
+[Package] +name = "hastysite" +version = "1.0.0" +author = "Fabio Cevasco" +description = "A small but powerful static site generator" +license = "MIT" +bin = "hastysite" + +[Deps] +requires: "nim >= 0.15.0, yaml >= 0.7.0, hastyscribe >= 1.4.0"
A
vendor/commandeer.nim
@@ -0,0 +1,236 @@
+ +import parseopt2 +import strutils +import strtabs + + +var + argumentList = newSeq[string]() + shortOptions = newStringTable(modeCaseSensitive) + longOptions = newStringTable(modeCaseSensitive) + argumentIndex = 0 + errorMsgs : seq[string] = @[] + customErrorMsg : string + inSubcommand = false + subcommandSelected = false + + +## String conversion +proc convert(s : string, t : char): char = + result = s[0] +proc convert(s : string, t : int): int = + result = parseInt(s) +proc convert(s : string, t : float): float = + result = parseFloat(s) +proc convert(s : string, t : bool): bool = + ## will accept "yes", "true" as true values + if s == "": + ## the only way we get an empty string here is because of a key + ## with no value, in which case the presence of the key is enough + ## to return true + result = true + else: + result = parseBool(s) +proc convert(s : string, t : string): string = + result = s.strip + + +template argumentIMPL(identifier : untyped, t : typeDesc): untyped = + + var identifier : t + + if (inSubcommand and subcommandSelected) or not inSubcommand: + if len(argumentList) <= argumentIndex: + let eMsg = "Missing command line arguments" + if len(errorMsgs) == 0: + errorMsgs.add(eMsg) + else: + if not (errorMsgs[high(errorMsgs)][0] == 'M'): + errorMsgs.add(eMsg) + else: + var typeVar : t + try: + identifier = convert(argumentList[argumentIndex], typeVar) + except ValueError: + let eMsg = capitalizeAscii(getCurrentExceptionMsg()) & + " for argument " & $(argumentIndex+1) + errorMsgs.add(eMsg) + + inc(argumentIndex) + + +template argumentsIMPL(identifier : untyped, t : typeDesc, atLeast1 : bool): untyped = + + var identifier = newSeq[t]() + + if (inSubcommand and subcommandSelected) or not inSubcommand: + if len(argumentList) <= argumentIndex: + if atLeast1: + let eMsg = "Missing command line arguments" + if len(errorMsgs) == 0: + errorMsgs.add(eMsg) + else: + if not (errorMsgs[high(errorMsgs)][0] == 'M'): + errorMsgs.add(eMsg) + else: + discard + else: + var typeVar : t + var firstError = true + while true: + if len(argumentList) == argumentIndex: + break + try: + let argument = argumentList[argumentIndex] + inc(argumentIndex) + identifier.add(convert(argument, typeVar)) + firstError = false + except ValueError: + if atLeast1 and firstError: + let eMsg = capitalizeAscii(getCurrentExceptionMsg()) & + " for argument " & $(argumentIndex+1) + errorMsgs.add(eMsg) + break + + +template optionDefaultIMPL(identifier : untyped, t : typeDesc, longName : string, + shortName : string, default : t): untyped = + + var identifier : t + + if (inSubcommand and subcommandSelected) or not inSubcommand: + var typeVar : t + if strtabs.hasKey(longOptions, longName): + try: + identifier = convert(longOptions[longName], typeVar) + except ValueError: + let eMsg = capitalizeAscii(getCurrentExceptionMsg()) & + " for option --" & longName + errorMsgs.add(eMsg) + elif strtabs.hasKey(shortOptions, shortName): + try: + identifier = convert(shortOptions[shortName], typeVar) + except ValueError: + let eMsg = capitalizeAscii(getCurrentExceptionMsg()) & + " for option -" & shortName + errorMsgs.add(eMsg) + else: + #default values + identifier = default + + +template optionIMPL(identifier : untyped, t : typeDesc, longName : string, + shortName : string): untyped = + + var identifier : t + + if (inSubcommand and subcommandSelected) or not inSubcommand: + var typeVar : t + if strtabs.hasKey(longOptions, longName): + try: + identifier = convert(longOptions[longName], typeVar) + except ValueError: + let eMsg = capitalizeAscii(getCurrentExceptionMsg()) & + " for option --" & longName + errorMsgs.add(eMsg) + elif strtabs.hasKey(shortOptions, shortName): + try: + identifier = convert(shortOptions[shortName], typeVar) + except ValueError: + let eMsg = capitalizeAscii(getCurrentExceptionMsg()) & + " for option -" & shortName + errorMsgs.add(eMsg) + +template exitoptionIMPL(longName, shortName, msg : string): untyped = + + if (inSubcommand and subcommandSelected) or not inSubcommand: + if strtabs.hasKey(longOptions, longName): + quit msg, QuitSuccess + elif strtabs.hasKey(shortOptions, shortName): + quit msg, QuitSuccess + + +template errormsgIMPL(msg : string): untyped = + + if (inSubcommand and subcommandSelected) or not inSubcommand: + customErrorMsg = msg + + +template subcommandIMPL(identifier : untyped, subcommandName : string, stmts : untyped): untyped = + + var identifier : bool = false + inSubcommand = true + + if len(argumentList) > 0 and argumentList[0] == subcommandName: + identifier = true + inc(argumentIndex) + subcommandSelected = true + + stmts + + subcommandSelected = false + inSubcommand = false + + +template commandline*(statements : untyped): untyped = + + template argument(identifier : untyped, t : typeDesc): untyped = + argumentIMPL(identifier, t) + + template arguments(identifier : untyped, t : typeDesc, atLeast1 : bool = true): untyped = + argumentsIMPL(identifier, t, atLeast1) + + template option(identifier : untyped, t : typeDesc, longName : string, + shortName : string, default : untyped): untyped = + optionDefaultIMPL(identifier, t, longName, shortName, default) + + template option(identifier : untyped, t : typeDesc, longName : string, + shortName : string): untyped = + optionIMPL(identifier, t, longName, shortName) + + template exitoption(longName, shortName, msg : string): untyped = + exitoptionIMPL(longName, shortName, msg) + + template errormsg(msg : string): untyped = + errormsgIMPL(msg) + + template subcommand(identifier : untyped, subcommandName : string, stmts : untyped): untyped = + subcommandIMPL(identifier, subcommandName, stmts) + + for kind, key, value in parseopt2.getopt(): + case kind + of parseopt2.cmdArgument: + argumentList.add(key) + of parseopt2.cmdLongOption: + longOptions[key] = value + of parseopt2.cmdShortOption: + shortOptions[key] = value + else: + discard + + #Call the passed statements so that the above templates are called + statements + + if len(errorMsgs) > 0: + if not customErrorMsg.isNil: + errorMsgs.add(customErrorMsg) + quit join(errorMsgs, "\n") + + +when isMainModule: + import unittest + + test "convert() returns converted type value from strings": + var intVar : int + var floatVar : float + var boolVar : bool + var stringVar : string + var charVar : char + + check convert("10", intVar) == 10 + check convert("10.0", floatVar) == 10 + check convert("10", floatVar) == 10 + check convert("yes", boolVar) == true + check convert("false", boolVar) == false + check convert("no ", stringVar) == "no" + check convert("*", charVar) == '*'
A
vendor/moustachu.nim
@@ -0,0 +1,223 @@
+ +## A mustache templating engine written in Nim. + +import strutils +import sequtils +import os + +import moustachupkg/context +import moustachupkg/tokenizer + +export context + +let + htmlReplaceBy = [("&", "&"), + ("<", "<"), + (">", ">"), + ("\\", "\"), + ("\"", """)] + + +proc lookupContext(contextStack: seq[Context], tagkey: string): Context = + ## Return the Context associated with `tagkey` where `tagkey` + ## can be a dotted tag e.g. a.b.c + ## If the Context at `tagkey` does not exist, return nil. + var currCtx = contextStack[contextStack.high] + if tagkey == ".": return currCtx + let subtagkeys = tagkey.split(".") + for i in countDown(contextStack.high, contextStack.low): + currCtx = contextStack[i] + + for subtagkey in subtagkeys: + currCtx = currCtx[subtagkey] + if currCtx == nil: + break + + if currCtx != nil: + return currCtx + + return currCtx + +proc lookupString(contextStack: seq[Context], tagkey: string): string = + ## Return the string associated with `tagkey` in Context `c`. + ## If the Context at `tagkey` does not exist, return the empty string. + result = lookupContext(contextStack, tagkey).toString() + +proc ignore(tag: string, tokens: seq[Token], index: int): int = + #ignore + var i = index + 1 + var nexttoken = tokens[i] + var openedsections = 1 + let lentokens = len(tokens) + + while i < lentokens and openedsections > 0: + if nexttoken.value == tag: + if nexttoken.tokentype in [TokenType.section, TokenType.invertedsection]: + openedsections += 1 + elif nexttoken.tokentype == TokenType.ender: + openedsections -= 1 + else: discard + else: discard + + i += 1 + nexttoken = tokens[i] + + return i + +proc parallelReplace(str: string, + substitutions: openArray[tuple[pattern: string, by: string]]): string = + ## Returns a modified copy of `str` with the `substitutions` applied + result = str + for sub in substitutions: + result = result.replace(sub[0], sub[1]) + +proc render(tmplate: string, contextStack: seq[Context], pwd="."): string = + ## Take a mustache template `tmplate` and an evaluation Context `c` + ## and return the rendered string. This is the main procedure. + var renderings : seq[string] = @[] + + #Object + var sections : seq[string] = @[] + var contextStack = contextStack + + #Array + var loopStartPositions : seq[int] = @[] + var loopCounters : seq[int] = @[] + + #Indentation + var indentation = "" + + let tokens = toSeq(tokenizer.tokenize(tmplate)) + let lentokens = len(tokens) + + var index = 0 + + while index < lentokens: + let token = tokens[index] + + case token.tokentype + of TokenType.comment: + discard + + of TokenType.escapedvariable: + var viewvalue = contextStack.lookupString(token.value) + viewvalue = viewvalue.parallelReplace(htmlReplaceBy) + renderings.add(viewvalue) + + of TokenType.unescapedvariable: + var viewvalue = contextStack.lookupString(token.value) + renderings.add(viewvalue) + + of TokenType.section: + let ctx = contextStack.lookupContext(token.value) + if ctx == nil: + index = ignore(token.value, tokens, index) + continue + elif ctx.kind == CObject: + # enter a new section + contextStack.add(ctx) + sections.add(token.value) + elif ctx.kind == CArray: + # update the array loop stacks + if ctx.len == 0: + index = ignore(token.value, tokens, index) + continue + else: + #do looping + index += 1 + loopStartPositions.add(index) + loopCounters.add(ctx.len) + sections.add(token.value) + contextStack.add(ctx[ctx.len - loopCounters[^1]]) + continue + elif ctx.kind == CValue: + if not ctx: + index = ignore(token.value, tokens, index) + continue + else: discard #we will render the text inside the section + + of TokenType.invertedsection: + let ctx = contextStack.lookupContext(token.value) + if ctx != nil: + if ctx.kind == CObject: + index = ignore(token.value, tokens, index) + continue + elif ctx.kind == CArray: + if ctx.len != 0: + index = ignore(token.value, tokens, index) + continue + elif ctx.kind == CValue: + if ctx: + index = ignore(token.value, tokens, index) + continue + else: discard #we will render the text inside the section + + of TokenType.ender: + var ctx = contextStack.lookupContext(token.value) + if ctx != nil: + if ctx.kind == CObject: + discard contextStack.pop() + discard sections.pop() + elif ctx.kind == CArray: + if ctx.len > 0: + loopCounters[^1] -= 1 + discard contextStack.pop() + if loopCounters[^1] == 0: + discard loopCounters.pop() + discard loopStartPositions.pop() + discard sections.pop() + else: + index = loopStartPositions[^1] + contextStack.add(ctx[ctx.len - loopCounters[^1]]) + continue + + of TokenType.indenter: + if token.value != "": + indentation = token.value + renderings.add(indentation) + + of TokenType.partial: + echo pwd + var partialTemplate = pwd.joinPath(token.value & ".mustache").readFile + partialTemplate = partialTemplate.replace("\n", "\n" & indentation) + if indentation != "": + partialTemplate = partialTemplate.strip(leading=false, chars={' '}) + indentation = "" + renderings.add(render(partialTemplate, contextStack, pwd)) + + else: + renderings.add(token.value) + + index += 1 + + result = join(renderings, "") + +proc render*(tmplate: string, c: Context, pwd="."): string = + var contextStack = @[c] + result = tmplate.render(contextStack, pwd) + + +when isMainModule: + import json + import os + import commandeer + + proc usage(): string = + result = "Usage! moustachu <context>.json <template>.mustache [--file=<outputFilename>]" + + commandline: + argument jsonFilename, string + argument tmplateFilename, string + option outputFilename, string, "file", "f" + exitoption "help", "h", usage() + exitoption "version", "v", "0.10.3" + errormsg usage() + + var c = newContext(parseFile(jsonFilename)) + var tmplate = readFile(tmplateFilename) + var pwd = tmplateFilename.parentDir() + + if outputFilename.isNil(): + echo render(tmplate, c, pwd) + else: + writeFile(outputFilename, render(tmplate, c, pwd))
A
vendor/moustachupkg/context.nim
@@ -0,0 +1,176 @@
+import json +import sequtils +import strutils +import tables + + +type + ContextKind* = enum ## possible Context types + CArray, + CObject, + CValue + + ## Context used to render a mustache template + Context* = ref ContextObj + ContextObj = object + case kind*: ContextKind + of CValue: + val: JsonNode + of CArray: + elems: seq[Context] + of CObject: + fields: Table[string, Context] + +## Builders + +proc newContext*(j : JsonNode = nil): Context = + ## Create a new Context based on a JsonNode object + new(result) + if j == nil: + result.kind = CObject + result.fields = initTable[string, Context](4) + else: + case j.kind + of JObject: + result.kind = CObject + result.fields = initTable[string, Context](4) + for key, val in pairs(j.fields): + result.fields[key] = newContext(val) + of JArray: + result.kind = CArray + result.elems = @[] + for val in j.elems: + result.elems.add(newContext(val)) + else: + result.kind = CValue + result.val = j + +proc newArrayContext*(): Context = + ## Create a new Context of kind CArray + new(result) + result.kind = CArray + result.elems = @[] + +## Getters + +proc `[]`*(c: Context, key: string): Context = + ## Return the Context associated with `key`. + ## If the Context at `key` does not exist, return nil. + assert(c != nil) + if c.kind != CObject: return nil + if c.fields.hasKey(key): return c.fields[key] else: return nil + +proc `[]`*(c: Context, index: int): Context = + assert(c != nil) + if c.kind != CArray: return nil else: return c.elems[index] + +## Setters + +proc `[]=`*(c: var Context, key: string, value: Context) = + ## Assign a context `value` to `key` in context `c` + assert(c.kind == CObject) + c.fields[key] = value + +proc `[]=`*(c: var Context, key: string, value: JsonNode) = + ## Convert and assign `value` to `key` in `c` + assert(c.kind == CObject) + c[key] = newContext(value) + +proc `[]=`*(c: var Context; key: string, value: BiggestInt) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + c[key] = newContext(newJInt(value)) + +proc `[]=`*(c: var Context; key: string, value: string) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + c[key] = newContext(newJString(value)) + +proc `[]=`*(c: var Context; key: string, value: float) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + c[key] = newContext(newJFloat(value)) + +proc `[]=`*(c: var Context; key: string, value: bool) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + c[key] = newContext(newJBool(value)) + +proc `[]=`*(c: var Context, key: string, value: openarray[Context]) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + var contextList = newArrayContext() + for v in value: + contextList.elems.add(v) + c[key] = contextList + +proc `[]=`*(c: var Context, key: string, value: openarray[string]) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + c[key] = map(value, proc(x: string): Context = newContext(newJString(x))) + +proc `[]=`*(c: var Context, key: string, value: openarray[int]) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + c[key] = map(value, proc(x: int): Context = newContext(newJInt(x))) + +proc `[]=`*(c: var Context, key: string, value: openarray[float]) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + c[key] = map(value, proc(x: float): Context = newContext(newJFloat(x))) + +proc `[]=`*(c: var Context, key: string, value: openarray[bool]) = + ## Assign `value` to `key` in Context `c` + assert(c.kind == CObject) + c[key] = map(value, proc(x: bool): Context = newContext(newJBool(x))) + +## Printers + +proc `$`*(c: Context): string = + ## Return a string representing the context. Useful for debugging + result = "Context->[kind: " & $c.kind + case c.kind + of CValue: result &= "\nval: " & $c.val + of CArray: + var strArray = map(c.elems, proc(c: Context): string = $c) + result &= "\nelems: [" & join(strArray, ", ") & "]" + of CObject: + var strArray : seq[string] = @[] + for key, val in pairs(c.fields): + strArray.add(key & ": " & $val) + result &= "\nfields: {" & join(strArray, ", ") & "}" + result &= "\n]" + +proc toString*(c: Context): string = + ## Return string representation of `c` relevant to mustache + if c != nil: + if c.kind == CValue: + case c.val.kind + of JString: + return c.val.str + of JFloat: + return c.val.fnum.formatFloat(ffDefault, 0) + of JInt: + return $c.val.num + of JNull: + return "" + of JBool: + return if c.val.bval: "true" else: "" + else: + return $c.val + else: + return $c + else: + return "" + +proc len*(c: Context): int = + if c.kind == CArray: result = c.elems.len + else: discard + +converter toBool*(c: Context): bool = + assert(c.kind == CValue) + case c.val.kind + of JBool: result = c.val.bval + of JNull: result = false + of JString: result = c.val.str != "" + else: result = true
A
vendor/moustachupkg/tokenizer.nim
@@ -0,0 +1,137 @@
+import sequtils +import strutils + +type + TokenType* {.pure.} = enum + rawtext, + escapedvariable, + unescapedvariable, + section, + invertedsection, + comment, + partial, + ender, + indenter + + Token* = tuple[tokentype: TokenType, value: string] + + +proc left_side_empty(tmplate: string, pivotindex: int): tuple[empty: bool, newlineindex: int] = + var ls_i = 0 + var ls_empty = false + var i = pivotindex - 1 + while i > -1 and tmplate[i] in {' ', '\t'}: dec(i) + if (i == -1) or (tmplate[i] == '\l'): + ls_i = i + ls_empty = true + return (empty: ls_empty, newlineindex: ls_i) + + +iterator tokenize*(tmplate: string): Token = + let opening = "{{" + var pos = 0 + + while pos < tmplate.len: + let originalpos = pos + var closing = "}}" + + # find start of tag + var opening_index = tmplate.find(opening, start=pos) + if opening_index == -1: + yield (tokentype: TokenType.rawtext, value: tmplate[pos..high(tmplate)]) + break + + #Check if the left side is empty + var left_side = left_side_empty(tmplate, opening_index) + var ls_empty = left_side.empty + var ls_i = left_side.newlineindex + + #Deal with text before tag + var beforetoken = (tokentype: TokenType.rawtext, value: "") + if opening_index > pos: + #safe bet for now + beforetoken.value = tmplate[pos..opening_index-1] + + pos = opening_index + opening.len + + if not (pos < tmplate.len): + yield (tokentype: TokenType.rawtext, value: tmplate[opening_index..high(tmplate)]) + break + + #Determine TokenType + var tt = TokenType.escapedvariable + + case tmplate[pos] + of '!': + tt = TokenType.comment + pos += 1 + of '&': + tt = TokenType.unescapedvariable + pos += 1 + of '{': + tt = TokenType.unescapedvariable + pos += 1 + closing &= "}" + of '#': + tt = TokenType.section + pos += 1 + of '^': + tt = TokenType.invertedsection + pos += 1 + of '/': + tt = TokenType.ender + pos += 1 + of '>': + tt = TokenType.partial + pos += 1 + else: + tt = TokenType.escapedvariable + + #find end of tag + var closingindex = tmplate.find(closing, start=pos) + if closingindex == -1: + if beforetoken.value != "": yield beforetoken + yield (tokentype: TokenType.rawtext, value: tmplate[opening_index..pos-1]) + continue + + #Check if the right side is empty + var rs_i = 0 + var rs_empty = false + var i = 0 + if ls_empty: + i = closingindex + closing.len + while i < tmplate.len and tmplate[i] in {' ', '\t'}: inc(i) + if i == tmplate.len: + rs_i = i - 1 + rs_empty = true + elif tmplate[i] == '\c' and (i+1 < tmplate.len) and (tmplate[i+1] == '\l'): + rs_i = i + 1 + rs_empty = true + elif tmplate[i] == '\l': + rs_i = i + rs_empty = true + else: + discard + + if tt in [TokenType.comment, TokenType.section, + TokenType.invertedsection, TokenType.ender, TokenType.partial]: + # Standalone tokens + if rs_empty: + if beforetoken.value != "": + beforetoken.value = tmplate[originalpos..ls_i] + yield beforetoken + + if tt == TokenType.partial: + if ls_i+1 <= opening_index-1: + yield (tokentype: TokenType.indenter, value: tmplate[ls_i+1..opening_index-1]) + + yield (tokentype: tt, value: tmplate[pos..closingindex-1].strip) + pos = rs_i + 1 # remove new line of this line + else: + if beforetoken.value != "": yield beforetoken + yield (tokentype: tt, value: tmplate[pos..closingindex-1].strip) + pos = closingindex + closing.len + else: + if beforetoken.value != "": yield beforetoken + yield (tokentype: tt, value: tmplate[pos..closingindex-1].strip) + pos = closingindex + closing.len