Template text processor

This modulel realises a template text processor by applying recursive text block substititon filters:

Parser (input, FiltersTable, Variables)
  repeat for input
    block    <- collect text block
    filter() <- FiltersTable (block)
    data     <- filter (block, FiltersTable, Variables)
    output   <- for data substitute text fragments from Variables

Built-in text blocks

Single text lines, terminated with '\n'

  • Lines with first character ';' are discarded. Formally, the filter() applied here returns empty data.
  • Lines not starting with a key word :<keyName>: are passed on as-is (i.e. unfiltered). A key word can be preceeded by spaces.

Multi lines text:

  • all text lines between :STASH: and :END:
    :STASH:
    .. text block ..
    :END:

    are collected and shashed as variable @STASHED@. Formally, the filter() applied here returns empty data.

Other text blocks

All other text blocks are defined by means of a lookup table which may differ with every filter() recursion. A lookup table entry looks like

("<keyName>",(<ifFilter>,<elseFilter>))

where the <keyName> is looked up and one of the filter functions <ifFilter> or <elseFilter> is applied to the selected text block.

  • If <keyName> matches IF_<ifName> for some text <ifName>, then the following syntax is supported
    :IF_<ifName>:
    .. text block ..
    :ELSE:
    .. alternative text block ..
    :END:
    
    :IF_NOT_<ifName>:
    .. alternative text block ..
    :ELSE:
    .. text block ..
    :END:

    where in the first case the filters are applied as

    ifFilter(".. text block ..")
    elseFilter(".. alternative text block ..")

    and in the second case

    elseFilter(".. alternative text block ..")
    ifFilter(".. text block ..")

    If the :ELSE: clause is missing the corresponding filter is not applied.

    A trivial example for an if/else filter table entry for a true condition can be set up with the toIfElse() convenience function as

    ("IF_TRUE", toIfElse(true))
  • Any other <keyName> value (not matching IF_<ifName>) supports the clause
    :<keyName>:
    .. text block ..
    :END:

    in which case the <elseFilter> of the lookup table entry is ignored. A loop filter can be contructed from an iterator example

    iterator exampleItems(p: TmplParser): TmplParser {.closure.} =
      for k in .. items ..:
        var localVars = @[("@VAR1@", val1(k)), ("@VAR2@", val2(k)), ..]
        yield (p.filter, localVars.concat(p.vars))

    using a variant of the toIfElse() convenience function as

    ("FOR_ITEMS", toIfElse(exampleItems))

Other directives

The key word :STOP: teminates any text bock collection other than in :STASH: scope.

Variable substitution filter

After a filter() is applied to a text block, the Variables list consisting of pairs

("<textN>", "<substituteN>")

is appied to the fitered data replacing each ocurrence of <textN> with <substituteN> for all N.

Parser usage example

Example:

import
  algorithm, os, sequtils

iterator envItems(p: TmplParser): TmplParser {.closure.} =
  for k in envPairs.toSeq.mapIt(it.key.string).sorted:
    var
      # Loop-local variables
      key = ("@KEY@", k)
      value = ("@VALUE@", k.getEnv.string)
    yield (p.filter, @[key, value].concat(p.vars))

# Text template processor parser specification
proc newSpecs(): TmplParser =
  result = (newTmplFilter(), newSeq[(string,string)]())

  # Global variable
  result.vars.add(("@TITLE@", "Hello World"))

  # Simple if/else clause
  result.filter["IF_ENABLED"] = toIfElse(true)

  # Loop clause
  result.filter["FOR_ENV"] = envItems.toIfElse(result)

# Template text page
var tmplPage = """
@TITLE@
;                  -- comment
:STASH:
stashed text
:END:

:IF_ENABLED:       -- outer
+++ aaa
  :IF_NOT_ENABLED: -- inner
+++ bbb
  :ELSE:           -- inner
+++ ddd @STASHED@
  :END:            -- inner
+++ eee
:ELSE:             -- outer
+++ bbb
:END:              -- outer

:STASH:
***
:END:
:FOR_ENV:
@STASHED@ @KEY@ = @VALUE@
:END:
"""

# Apply template processor
var expandedPage =
  tmplPage.tmplParser(newSpecs())
then the contents of the variable expandedPage above contains something like
Hello World

+++ aaa
+++ ddd stashed text
+++ eee

*** COLUMNS = 79
*** DBUS_SESSION_BUS_ADDRESS = unix:path=/run/user/1000/bus
*** DESKTOP_AUTOSTART_ID = 1054b02024a...
*** DESKTOP_SESSION = lightdm-xsession
*** DESKTOP_STARTUP_ID = x-session-manager-...
*** DISPLAY = :0
*** GDMSESSION = lightdm-xsession
*** GPG_AGENT_INFO = /run/user/1000/...
*** GTK3_MODULES = gtk-vector-screenshot
*** GTK_MODULES = gail:atk-bridge
*** ...

Debugging

When including the library, compile with the flag tracer as in

nim c -r -d:tracer:1 ...

in order to dump the parser state to stderr after each action of the template processor.

Types

TmplFilterFn = proc (x: string): string {...}{.closure.}
TmplIfElse = (TmplFilterFn, TmplFilterFn)
TmplFilter = TableRef[string, TmplIfElse]
TmplParser = tuple[filter: TmplFilter, vars: seq[(string, string)]]
TmplLoopIt = iterator (parser: TmplParser): TmplParser {...}{.closure.}

Procs

proc tmplUncomment(tmplText: string): string {...}{.raises: [], tags: [].}
Filter out all lines starting with ';'.
proc newTmplFilter(): TmplFilter {...}{.raises: [], tags: [].}
Create empty table to be eventually used in as filters argument in tmplParser().
proc tmplParser(tmplText: string; parser: TmplParser): string {...}{.
    raises: [ValueError, Exception, KeyError], tags: [RootEffect].}
This function realises the template text processor as described above in the module documentation.
proc tmplParser(tmplText: string; filters: TmplFilter;
                vars: varargs[(string, string)]): string {...}{.
    raises: [ValueError, Exception, KeyError], tags: [RootEffect].}
Variant of tmplParser().
proc tmplParser(tmplText: string; vars: varargs[(string, string)]): string {...}{.
    raises: [ValueError, Exception, KeyError], tags: [RootEffect].}
Simplified template parser without block section clauses. As a consequence, only variable/text substitution is performed.
proc toIfElse(isIfClauseOk = true): TmplIfElse {...}{.raises: [], tags: [].}
Returns the
(<pass-through-filter>,<discard-filter>)

filter pair if argument isIfClauseOk is true, otherwise the tuple entries are swapped. The tuple returned is supposed to be used with some lookup entry for the filters argument table for the tmplParser() function.

proc toIfElse(tmplIt: TmplLoopIt; parser: TmplParser): TmplIfElse {...}{.raises: [],
    tags: [].}
Returns a TmplIfElse filter derived from the iterator argument tmplIt(). The return value is supposed to be used with some lookup entry for the filters argument table for the tmplParser() function.
proc toIfElse(tmplIt: TmplLoopIt; filters: TmplFilter;
              vars: varargs[(string, string)]): TmplIfElse {...}{.raises: [],
    tags: [].}
Variant of toIfElse().
proc toIfElseNil(): TmplIfElse {...}{.inline, raises: [], tags: [].}
Simple text discarding <sectionFilter> pair for tmplTextFilter()
proc toIfElseNil(ignIt: TmplLoopIt; ignFilters: TmplFilter;
                 ignVars: varargs[(string, string)]): TmplIfElse {...}{.inline,
    raises: [], tags: [].}
Variant of toIfElseNil().