initial commit
authorMatthew Owens <matthew@owens.tech>
Sun, 31 Mar 2024 14:51:13 +0000 (15:51 +0100)
committerMatthew Owens <matthew@owens.tech>
Sun, 31 Mar 2024 14:51:13 +0000 (15:51 +0100)
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
build [new file with mode: 0755]
filters/columns.lua [new file with mode: 0644]
out.pdf [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
src/body.md [new file with mode: 0644]
src/preamble.md [new file with mode: 0644]
src/vars.yaml [new file with mode: 0644]
template.tex [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..d358731
--- /dev/null
@@ -0,0 +1,2 @@
+out.tex
+venv
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..c7d5181
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# Fief
+
+TTRPG Source
+
+## Environment
+```
+python3 -m venv venv
+source venv/bin/activate
+pip install -r requirements.txt
+deactivate
+```
+
+## Building
+run `build` script
+
diff --git a/build b/build
new file mode 100755 (executable)
index 0000000..f5369cb
--- /dev/null
+++ b/build
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+#pandoc -B header.md --toc -s main.md -o out.pdf
+#pandoc -B header.md -f gfm --toc -s main.md -o out.pdf
+#pandoc -s --toc -B header.md main.md -o out.pdf
+work=~/documents/untitled-ttrpg
+src=$work/src
+tmp=/tmp
+#template=eisvogel
+template=template.tex
+
+cd $work
+
+source ./venv/bin/activate
+#pandoc -s --toc body.md -o out.pdf
+#pandoc -s --toc body.md -o out.pdf --from markdown --template eisvogel
+#pandoc $src_dir/preamble.md -o $tmp_dir/
+#pandoc -s --toc -B pre-body.md body.md -o out.pdf
+#pandoc -s --toc $src/preamble.md $src/body.md -o out.pdf
+#pandoc --filter pandoc-mustache -s $src/body.md -o $work/out.pdf
+pandoc --lua-filter filters/columns.lua --filter pandoc-mustache -s $src/body.md -o $work/out.pdf
+#pandoc --filter pandoc-mustache -s $src/body.md -o $work/out.pdf --template $template
+#pandoc --filter pandoc-mustache -t latex -s $src/body.md -o $work/out.tex --template $template
+deactivate
+
diff --git a/filters/columns.lua b/filters/columns.lua
new file mode 100644 (file)
index 0000000..7300e96
--- /dev/null
@@ -0,0 +1,1087 @@
+--[[-- # Columns - multiple column support in Pandoc's markdown.
+
+This Lua filter provides support for multiple columns in
+latex and html outputs. For details, see README.md.
+
+@author Julien Dutant <julien.dutant@kcl.ac.uk>
+@copyright 2021 Julien Dutant
+@license MIT - see LICENSE file for details.
+@release 1.1.3
+]]
+
+-- # Version control
+local required_version = '2.9.0'
+local version_err_msg = "ERROR: pandoc >= "..required_version
+                .." required for columns filter"
+-- pandoc 2.9 required for pandoc.List insert method
+if PANDOC_VERSION == nil then -- if pandoc_version < 2.1
+  error(version_err_msg)
+elseif PANDOC_VERSION[1] < 3 and PANDOC_VERSION[2] < 9 then
+  error(version_err_msg)
+else  
+  PANDOC_VERSION:must_be_at_least(required_version, version_err_msg)
+end
+local utils = require('pandoc.utils') -- this is superfluous in Pandoc >= 2.7 I think
+
+-- # Internal settings
+
+-- target_formats  filter is triggered when those formats are targeted
+local target_formats = {
+  "html.*",
+  "latex",
+}
+local options = {
+  raggedcolumns = false; -- global ragged columns option
+}
+
+-- # Helper functions
+
+--- type: pandoc-friendly type function
+-- panbdoc.utils.type is only defined in Pandoc >= 2.17
+-- if it isn't, we extend Lua's type function to give the same values
+-- as pandoc.utils.type on Meta objects: Inlines, Inline, Blocks, Block,
+-- string and booleans
+-- Caution: not to be used on non-Meta Pandoc elements, the
+-- results will differ (only 'Block', 'Blocks', 'Inline', 'Inlines' in
+-- >=2.17, the .t string in <2.17).
+local type = utils.type or function (obj)
+        local tag = type(obj) == 'table' and obj.t and obj.t:gsub('^Meta', '')
+        return tag and tag ~= 'Map' and tag or type(obj)
+    end
+
+--- Test whether the target format is in a given list.
+-- @param formats list of formats to be matched
+-- @return true if match, false otherwise
+local function format_matches(formats)
+  for _,format in pairs(formats) do
+    if FORMAT:match(format) then
+      return true
+    end
+  end
+  return false
+end
+
+
+--- Add a block to the document's header-includes meta-data field.
+-- @param meta the document's metadata block
+-- @param block Pandoc block element (e.g. RawBlock or Para) to be added to header-includes
+-- @return meta the modified metadata block
+local function add_header_includes(meta, block)
+
+    local header_includes = pandoc.List:new()
+
+    -- use meta['header-includes']
+
+    if meta['header-includes'] then
+      if type(meta['header-includes']) ==  'List' then
+        header_includes:extend(meta['header-includes'])
+      else
+        header_includes:insert(meta['header-includes'])
+      end
+    end
+
+    -- insert `block` in header-includes
+
+    header_includes:insert(pandoc.MetaBlocks({block}))
+
+    -- save header-includes in the document's meta
+
+    meta['header-includes'] = header_includes
+
+    return meta
+end
+
+--- Add a class to an element.
+-- @param element Pandoc AST element
+-- @param class name of the class to be added (string)
+-- @return the modified element, or the unmodified element if the element has no classes
+local function add_class(element, class)
+
+  -- act only if the element has classes
+  if element.attr and element.attr.classes then
+
+    -- if the class is absent, add it
+    if not element.attr.classes:includes(class) then
+      element.attr.classes:insert(class)
+    end
+
+  end
+
+  return element
+end
+
+--- Removes a class from an element.
+-- @param element Pandoc AST element
+-- @param class name of the class to be removed (string)
+-- @return the modified element, or the unmodified element if the element has no classes
+local function remove_class(element, class)
+
+  -- act only if the element has classes
+  if element.attr and element.attr.classes then
+
+    -- if the class is present, remove it
+    if element.attr.classes:includes(class) then
+      element.attr.classes = element.attr.classes:filter(
+        function(x)
+          return not (x == class)
+        end
+        )
+    end
+
+  end
+
+  return element
+end
+
+--- Set the value of an element's attribute.
+-- @param element Pandoc AST element to be modified
+-- @param key name of the attribute to be set (string)
+-- @param value value to be set. If nil, the attribute is removed.
+-- @return the modified element, or the element if it's not an element with attributes.
+local function set_attribute(element,key,value)
+
+  -- act only if the element has attributes
+  if element.attr and element.attr.attributes then
+
+    -- if `value` is `nil`, remove the attribute
+    if value == nil then
+      if element.attr.attributes[key] then
+       element.attr.attributes[key] = nil
+     end
+
+    -- otherwise set its value
+    else
+      element.attr.attributes[key] = value
+    end
+
+  end
+
+  return element
+end
+
+--- Add html style markup to an element's attributes.
+-- @param element the Pandoc AST element to be modified
+-- @param style the style markup to add (string in CSS)
+-- @return the modified element, or the unmodified element if it's an element without attributes
+local function add_to_html_style(element, style)
+
+  -- act only if the element has attributes
+  if element.attr and element.attr.attributes then
+
+    -- if the element has style markup, append
+    if element.attr.attributes['style'] then
+
+      element.attr.attributes['style'] =
+        element.attr.attributes['style'] .. '; ' .. style .. ' ;'
+
+    -- otherwise create
+    else
+
+      element.attr.attributes['style'] = style .. ' ;'
+
+    end
+
+  end
+
+  return element
+
+end
+
+--- Translate an English number name into a number.
+-- Converts cardinals ("one") and numerals ("first").
+-- Returns nil if the name isn't understood.
+-- @param name an English number name (string)
+-- @return number or nil
+local function number_by_name(name)
+
+  local names = {
+    one = 1,
+    two = 2,
+    three = 3,
+    four = 4,
+    five = 5,
+    six = 6,
+    seven = 7,
+    eight = 8,
+    nine = 9,
+    ten = 10,
+    first = 1,
+    second = 2,
+    third = 3,
+    fourth = 4,
+    fifth = 5,
+    sixth = 6,
+    seventh = 7,
+    eighth = 8,
+    ninth = 9,
+    tenth = 10,
+  }
+
+  result = nil
+
+  if name and names[name] then
+      return names[name]
+  end
+
+end
+
+--- Convert some CSS values (lengths, colous) to LaTeX equivalents.
+-- Example usage: `css_values_to_latex("1px solid black")` returns
+-- `{ length = "1pt", color = "black", colour = "black"}`.
+-- @param css_str a CSS string specifying a value
+-- @return table with keys `length`, `color` (alias `colour`) if found
+local function css_values_to_latex(css_str)
+
+  -- color conversion table
+  --  keys are CSS values, values are LaTeX equivalents
+
+  latex_colors = {
+    -- xcolor always available
+    black = 'black',
+    blue = 'blue',
+    brown = 'brown',
+    cyan = 'cyan',
+    darkgray = 'darkgray',
+    gray = 'gray',
+    green = 'green',
+    lightgray = 'lightgray',
+    lime = 'lime',
+    magenta = 'magenta',
+    olive = 'olive',
+    orange = 'orange',
+    pink = 'pink',
+    purple = 'purple',
+    red = 'red',
+    teal = 'teal',
+    violet = 'violet',
+    white = 'white',
+    yellow = 'yellow',
+    -- css1 colors
+    silver = 'lightgray',
+    fuschia = 'magenta',
+    aqua = 'cyan',
+  }
+
+  local result = {}
+
+  -- look for color values
+  --  by color name
+  --  rgb, etc.: to be added
+
+  local color = ''
+
+  -- space in front simplifies pattern matching
+  css_str = ' ' .. css_str
+
+  -- look for colour names
+  for text in string.gmatch(css_str, '[%s](%a+)') do
+
+    -- if we have LaTeX equivalent of `text`, store it
+    if latex_colors[text] then
+      result['color'] = latex_colors[text]
+    end
+
+  end
+
+  -- provide British spelling
+
+  if result['color'] then
+    result['colour'] = result['color']
+  end
+
+  -- look for lengths
+
+  --  0 : converted to 0em
+  if string.find(css_str, '%s0%s') then
+   result['length'] = '0em'
+  end
+
+  --  px : converted to pt
+  for text in string.gmatch(css_str, '(%s%d+)px') do
+   result['length'] = text .. 'pt'
+  end
+
+  -- lengths units to be kept as is
+  --  nb, % must be escaped
+  --  nb, if several found, the latest type is preserved
+  keep_units = { '%%', 'pt', 'mm', 'cm', 'in', 'ex', 'em' }
+
+  for _,unit in pairs(keep_units) do
+
+    -- .11em format
+    for text in string.gmatch(css_str, '%s%.%d+'.. unit) do
+      result['length'] = text
+    end
+
+    -- 2em and 1.2em format
+    for text in string.gmatch(css_str, '%s%d+%.?%d*'.. unit) do
+      result['length'] = text
+    end
+
+  end
+
+  return result
+
+end
+
+--- Ensures that a string specifies a LaTeX length
+-- @param text text to be checked
+-- @return text if it is a LaTeX length, `nil` otherwise
+local function ensures_latex_length(text)
+
+  -- LaTeX lengths units
+  --  nb, % must be escaped in lua patterns
+  units = { '%%', 'pt', 'mm', 'cm', 'in', 'ex', 'em' }
+
+  local result = nil
+
+  -- ignore spaces, controls and punctuation other than
+  -- dot, plus, minus
+  text = string.gsub(text, "[%s%c,;%(%)%[%]%*%?%%%^%$]+", "")
+
+  for _,unit in pairs(units) do
+
+    -- match .11em format and 1.2em format
+    if string.match(text, '^%.%d+'.. unit .. '$') or
+      string.match(text, '^%d+%.?%d*'.. unit .. '$') then
+
+      result = text
+
+    end
+
+  end
+
+  return result
+end
+
+
+-- # Filter-specific functions
+
+--- Process the metadata block.
+-- Adds any needed material to the document's metadata block.
+-- @param meta the document's metadata element
+local function process_meta(meta)
+
+  -- in LaTeX, require the `multicols` package
+  if FORMAT:match('latex') then
+
+    return add_header_includes(meta,
+      pandoc.RawBlock('latex', '\\usepackage{multicol}\n'))
+
+  end
+
+  -- in html, ensure that the first element of `columns` div
+  -- has a top margin of zero (otherwise we get white space
+  -- on the top of the first column)
+  -- idem for the first element after a `column-span` element
+  if FORMAT:match('html.*') then
+
+    html_header = [[
+<style>
+/* Styles added by the columns.lua pandoc filter */
+  .columns :first-child {margin-top: 0;}
+  .column-span + * {margin-top: 0;}
+</style>
+]]
+
+    return add_header_includes(meta, pandoc.RawBlock('html', html_header))
+
+  end
+
+  return meta
+
+end
+
+--- Convert explicit columnbreaks.
+-- This function converts any explict columnbreak markup in an element
+-- into a single syntax: a Div with class `columnbreak`.
+-- Note: if there are `column` Divs in the element we keep them
+-- in case they harbour further formatting (e.g. html classes). However
+-- we remove their `column` class to avoid double-processing when
+-- column fields are nested.
+-- @param elem Pandoc native Div element
+-- @return elem modified as needed
+local function convert_explicit_columbreaks(elem)
+
+  -- if `elem` ends with a `column` Div, this last Div should
+  -- not generate a columnbreak. We tag it to make sure we don't convert it.
+
+  if #elem.content > 0
+    and elem.content[#elem.content].t == 'Div'
+    and elem.content[#elem.content].classes:includes('column') then
+
+    elem.content[#elem.content] =
+      add_class(elem.content[#elem.content], 'column-div-in-last-position')
+
+  end
+
+  -- processes `column` Divs and `\columnbreak` LaTeX RawBlocks
+  filter = {
+
+    Div = function (el)
+
+      -- syntactic sugar: `column-break` converted to `columnbreak`
+      if el.classes:includes("column-break") then
+
+        el = add_class(el,"columnbreak")
+        el = remove_class(el,"column-break")
+
+      end
+
+      if el.classes:includes("column") then
+
+        -- with `column` Div, add a break if it's not in last position
+        if not el.classes:includes('column-div-in-last-position') then
+
+          local breaking_div = pandoc.Div({})
+          breaking_div = add_class(breaking_div, "columnbreak")
+
+          el.content:insert(breaking_div)
+
+        -- if it's in the last position, remove the custom tag
+        else
+
+          el = remove_class(el, 'column-div-in-last-position')
+
+        end
+
+        -- remove `column` classes, but leave the div and other
+        -- attributes the user might have added
+        el = remove_class(el, 'column')
+
+      end
+
+      return el
+    end,
+
+    RawBlock = function (el)
+      if el.format == "tex" and el.text == '\\columnbreak' then
+
+        local breaking_div = pandoc.Div({})
+        breaking_div = add_class(breaking_div, "columnbreak")
+
+        return breaking_div
+
+      else
+
+        return el
+
+      end
+
+    end
+
+  }
+
+  return pandoc.walk_block(elem, filter)
+
+end
+
+--- Tag an element with the number of explicit columnbreaks it contains.
+-- Counts the number of epxlicit columnbreaks contained in an element and
+-- tags the element with a `number_explicit_columnbreaks` attribute.
+-- In the process columnbreaks are tagged with the class `columnbreak_already_counted`
+-- in order to avoid double-counting when multi-columns are nested.
+-- @param elem Pandoc element (native Div element of class `columns`)
+-- @return elem with the attribute `number_explicit_columnbreaks` set.
+local function tag_with_number_of_explicit_columnbreaks(elem)
+
+  local number_columnbreaks = 0
+
+  local filter = {
+
+    Div = function(el)
+
+      if el.classes:includes('columnbreak') and
+        not el.classes:includes('columnbreak_already_counted')  then
+
+          number_columnbreaks = number_columnbreaks + 1
+          el = add_class(el, 'columnbreak_already_counted')
+
+      end
+
+      return el
+
+    end
+  }
+
+  elem = pandoc.walk_block(elem, filter)
+
+  elem = set_attribute(elem, 'number_explicit_columnbreaks',
+      number_columnbreaks)
+
+  return elem
+
+end
+
+--- Consolidate aliases for column attributes.
+-- Provides syntacic sugar: unifies various ways of
+-- specifying attributes of a multi-column environment.
+-- When several specifications conflit, favours `column-gap` and
+-- `column-rule` specifications.
+-- @param elem Pandoc element (Div of class `columns`) with column attributes.
+-- @return elem modified as needed.
+local function consolidate_colattrib_aliases(elem)
+
+  if elem.attr and elem.attr.attributes then
+
+    -- `column-gap` if the preferred syntax is set, erase others
+    if elem.attr.attributes["column-gap"] then
+
+      elem = set_attribute(elem, "columngap", nil)
+      elem = set_attribute(elem, "column-sep", nil)
+      elem = set_attribute(elem, "columnsep", nil)
+
+    -- otherwise fetch and unset any alias
+    else
+
+      if elem.attr.attributes["columnsep"] then
+
+        elem = set_attribute(elem, "column-gap",
+            elem.attr.attributes["columnsep"])
+        elem = set_attribute(elem, "columnsep", nil)
+
+      end
+
+      if elem.attr.attributes["column-sep"] then
+
+        elem = set_attribute(elem, "column-gap",
+            elem.attr.attributes["column-sep"])
+        elem = set_attribute(elem, "column-sep", nil)
+
+      end
+
+      if elem.attr.attributes["columngap"] then
+
+        elem = set_attribute(elem, "column-gap",
+            elem.attr.attributes["columngap"])
+        elem = set_attribute(elem, "columngap", nil)
+
+      end
+
+    end
+
+    -- `column-rule` if the preferred syntax is set, erase others
+    if elem.attr.attributes["column-rule"] then
+
+      elem = set_attribute(elem, "columnrule", nil)
+
+    -- otherwise fetch and unset any alias
+    else
+
+      if elem.attr.attributes["columnrule"] then
+
+        elem = set_attribute(elem, "column-rule",
+            elem.attr.attributes["columnrule"])
+        elem = set_attribute(elem, "columnrule", nil)
+
+      end
+
+    end
+
+  end
+
+  return elem
+
+end
+
+--- Pre-process a Div of class `columns`.
+-- Converts explicit column breaks into a unified syntax
+-- and count the Div's number of columns.
+-- When several columns are nested Pandoc will apply
+-- this filter to the innermost `columns` Div first;
+-- we use that feature to prevent double-counting.
+-- @param elem Pandoc element to be processes (Div of class `columns`)
+-- @return elem modified as needed
+local function preprocess_columns(elem)
+
+  -- convert any explicit column syntax in a single format:
+  -- native Divs with class `columnbreak`
+
+  elem = convert_explicit_columbreaks(elem)
+
+  -- count explicit columnbreaks
+
+  elem = tag_with_number_of_explicit_columnbreaks(elem)
+
+  return elem
+end
+
+--- Determine the number of column in a `columns` Div.
+-- Looks up two attributes in the Div: the user-specified
+-- `columns-count` and the filter-generated `number_explicit_columnbreaks`
+-- which is based on the number of explicit breaks specified.
+-- The final number of columns will be 2 or whichever of `column-count` and
+-- `number_explicit_columnbreaks` is the highest. This ensures there are
+-- enough columns for all explicit columnbreaks.
+-- This provides a single-column when the user specifies `column-count = 1` and
+-- there are no explicit columnbreaks.
+-- @param elem Pandoc element (Div of class `columns`) whose number of columns is to be determined.
+-- @return number of columns (number, default 2).
+local function determine_column_count(elem)
+
+    -- is there a specified column count?
+  local specified_column_count = 0
+  if elem.attr.attributes and elem.attr.attributes['column-count'] then
+      specified_column_count = tonumber(
+        elem.attr.attributes["column-count"])
+  end
+
+  -- is there an count of explicit columnbreaks?
+  local number_explicit_columnbreaks = 0
+  if elem.attr.attributes and elem.attr.attributes['number_explicit_columnbreaks'] then
+
+      number_explicit_columnbreaks = tonumber(
+        elem.attr.attributes['number_explicit_columnbreaks']
+        )
+
+      set_attribute(elem, 'number_explicit_columnbreaks', nil)
+
+  end
+
+  -- determines the number of columns
+  -- default 2
+  -- recall that number of columns = nb columnbreaks + 1
+
+  local number_columns = 2
+
+  if specified_column_count > 0 or number_explicit_columnbreaks > 0 then
+
+      if (number_explicit_columnbreaks + 1) > specified_column_count then
+        number_columns = number_explicit_columnbreaks + 1
+      else
+        number_columns = specified_column_count
+      end
+
+  end
+
+  return number_columns
+
+end
+
+--- Convert a pandoc Header to a list of inlines for latex output.
+-- @param header Pandoc Header element
+-- @return list of Inline elements
+local function header_to_latex_and_inlines(header)
+
+-- @todo check if level interpretation has been shifted, e.g. section is level 2
+-- @todo we could check the Pandoc state to check whether hypertargets are required?
+
+  local latex_header = {
+    'section',
+    'subsection',
+    'subsubsection',
+    'paragraph',
+    'subparagraph',
+  }
+
+  -- create a list if the header's inlines
+  local inlines = pandoc.List:new(header.content)
+
+  -- wrap in a latex_header if available
+
+  if header.level and latex_header[header.level] then
+
+    inlines:insert(1, pandoc.RawInline('latex',
+        '\\' .. latex_header[header.level] .. '{'))
+    inlines:insert(pandoc.RawInline('latex', '}'))
+
+  end
+
+  -- wrap in a link if available
+  if header.identifier then
+
+    inlines:insert(1, pandoc.RawInline('latex',
+        '\\hypertarget{' .. header.identifier .. '}{%\n'))
+    inlines:insert(pandoc.RawInline('latex',
+        '\\label{' .. header.identifier .. '}}'))
+
+  end
+
+  return inlines
+
+end
+
+--- Format column span in LaTeX.
+-- Formats a bit of text spanning across all columns for LaTeX output.
+-- If the colspan is only one block, it is turned into an option
+-- of a new `multicol` environment. Otherwise insert it is
+-- inserted between the two `multicol` environments.
+-- @param elem Pandoc element that is supposed to span across all
+--    columns.
+-- @param number_columns number of columns in the present environment.
+-- @return a pandoc RawBlock element in LaTeX format
+local function format_colspan_latex(elem, number_columns)
+
+    local result = pandoc.List:new()
+
+    -- does the content consists of a single header?
+
+    if #elem.content == 1 and elem.content[1].t == 'Header' then
+
+      -- create a list of inlines
+      inlines = pandoc.List:new()
+      inlines:insert(pandoc.RawInline('latex',
+        "\\end{multicols}\n"))
+      inlines:insert(pandoc.RawInline('latex',
+        "\\begin{multicols}{".. number_columns .."}["))
+      inlines:extend(header_to_latex_and_inlines(elem.content[1]))
+      inlines:insert(pandoc.RawInline('latex',"]\n"))
+
+      -- insert as a Plain block
+      result:insert(pandoc.Plain(inlines))
+
+      return result
+
+    else
+
+      result:insert(pandoc.RawBlock('latex',
+        "\\end{multicols}\n"))
+      result:extend(elem.content)
+      result:insert(pandoc.RawBlock('latex',
+        "\\begin{multicols}{".. number_columns .."}"))
+      return result
+
+    end
+
+end
+
+--- Format columns for LaTeX output
+-- @param elem Pandoc element (Div of "columns" class) containing the
+--    columns to be formatted.
+-- @return elem with suitable RawBlocks in LaTeX added
+local function format_columns_latex(elem)
+
+  -- make content into a List object
+  pandoc.List:new(elem.content)
+
+  -- how many columns?
+  number_columns = determine_column_count(elem)
+
+  -- set properties and insert LaTeX environment
+  --  we wrap the entire environment in `{...}` to
+  --  ensure properties (gap, rule) don't carry
+  --  over to following columns
+
+  local latex_begin = '{'
+  local latex_end = '}'
+  local ragged = options.raggedcolumns
+
+  -- override global ragged setting?
+  if elem.classes:includes('ragged')
+      or elem.classes:includes('raggedcolumns')
+      or elem.classes:includes('ragged-columns') then
+        ragged = true
+  elseif elem.classes:includes('justified')
+      or elem.classes:includes('justifiedcolumns')
+      or elem.classes:includes('justified-columns') then
+        ragged = false
+  end
+  if ragged then
+    latex_begin = latex_begin..'\\raggedcolumns'
+  end
+
+  if elem.attr.attributes then
+
+    if elem.attr.attributes["column-gap"] then
+
+      local latex_value = ensures_latex_length(
+        elem.attr.attributes["column-gap"])
+
+      if latex_value then
+
+        latex_begin = latex_begin ..
+          "\\setlength{\\columnsep}{" .. latex_value .. "}\n"
+
+      end
+
+      -- remove the `column-gap` attribute
+      elem = set_attribute(elem, "column-gap", nil)
+
+    end
+
+    if elem.attr.attributes["column-rule"] then
+
+      -- converts CSS value string to LaTeX values
+      local latex_values = css_values_to_latex(
+        elem.attr.attributes["column-rule"])
+
+      if latex_values["length"] then
+
+        latex_begin = latex_begin ..
+          "\\setlength{\\columnseprule}{" ..
+          latex_values["length"] .. "}\n"
+
+      end
+
+      if latex_values["color"] then
+
+        latex_begin = latex_begin ..
+          "\\renewcommand{\\columnseprulecolor}{\\color{" ..
+          latex_values["color"] .. "}}\n"
+
+      end
+
+
+      -- remove the `column-rule` attribute
+      elem = set_attribute(elem, "column-rule", nil)
+
+    end
+
+  end
+
+  latex_begin = latex_begin ..
+    "\\begin{multicols}{" .. number_columns .. "}\n"
+  latex_end = "\\end{multicols}\n" .. latex_end
+
+  elem.content:insert(1, pandoc.RawBlock('latex', latex_begin))
+  elem.content:insert(pandoc.RawBlock('latex', latex_end))
+
+  -- process blocks contained in `elem`
+  --  turn any explicit columnbreaks into LaTeX markup
+  --  turn `column-span` Divs into LaTeX markup
+
+  filter = {
+
+    Div = function(el)
+
+      if el.classes:includes("columnbreak") then
+        return pandoc.RawBlock('latex', "\\columnbreak\n")
+      end
+
+      if el.classes:includes("column-span-to-be-processed") then
+        return format_colspan_latex(el, number_columns)
+      end
+
+    end
+
+  }
+
+  elem = pandoc.walk_block(elem, filter)
+
+  return elem
+
+end
+
+
+--- Formats columns for html output.
+-- Uses CSS3 style added to the elements themselves.
+-- @param elem Pandoc element (Div of `columns` style)
+-- @return elem with suitable html attributes
+local function format_columns_html(elem)
+
+  -- how many columns?
+  number_columns = determine_column_count(elem)
+
+  -- add properties to the `columns` Div
+
+  elem = add_to_html_style(elem, 'column-count: ' .. number_columns)
+  elem = set_attribute(elem, 'column-count', nil)
+
+  if elem.attr.attributes then
+
+    if elem.attr.attributes["column-gap"] then
+
+      elem = add_to_html_style(elem, 'column-gap: ' ..
+        elem.attr.attributes["column-gap"])
+
+      -- remove the `column-gap` attribute
+      elem = set_attribute(elem, "column-gap")
+
+    end
+
+    if elem.attr.attributes["column-rule"] then
+
+      elem = add_to_html_style(elem, 'column-rule: ' ..
+        elem.attr.attributes["column-rule"])
+
+      -- remove the `column-rule` attribute
+      elem = set_attribute(elem, "column-rule", nil)
+
+    end
+
+  end
+
+  -- convert any explicit columnbreaks in CSS markup
+
+  filter = {
+
+    Div = function(el)
+
+      -- format column-breaks
+      if el.classes:includes("columnbreak") then
+
+        el = add_to_html_style(el, 'break-after: column')
+
+        -- remove columbreaks class to avoid double processing
+        -- when nested
+        -- clean up already-counted tag
+        el = remove_class(el, "columnbreak")
+        el = remove_class(el, "columnbreak_already_counted")
+
+      -- format column-spans
+      elseif el.classes:includes("column-span-to-be-processed") then
+
+        el = add_to_html_style(el, 'column-span: all')
+
+        -- remove column-span-to-be-processed class to avoid double processing
+        -- add column-span class to allow for styling
+        el = add_class(el, "column-span")
+        el = remove_class(el, "column-span-to-be-processed")
+
+      end
+
+      return el
+
+    end
+
+  }
+
+  elem = pandoc.walk_block(elem, filter)
+
+  return elem
+
+end
+
+
+-- # Main filters
+
+--- Formating filter.
+-- Applied last, converts prepared columns in target output formats
+-- @field Div looks for `columns` class
+format_filter = {
+
+  Div = function (element)
+
+    -- pick up `columns` Divs for formatting
+    if element.classes:includes ("columns") then
+
+      if FORMAT:match('latex') then
+        element = format_columns_latex(element)
+      elseif FORMAT:match('html.*') then
+        element = format_columns_html(element)
+      end
+
+      return element
+
+    end
+
+  end
+}
+
+--- Preprocessing filter.
+-- Processes meta-data fields and walks the document to pre-process
+-- columns blocks. Determine how many columns they contain, tags the
+-- last column Div, etc. Avoids double-counting when columns environments
+-- are nested.
+-- @field Div looks for `columns` class
+-- @field Meta processes the metadata block
+preprocess_filter = {
+
+  Div = function (element)
+
+      -- send `columns` Divs to pre-processing
+      if element.classes:includes("columns") then
+        return preprocess_columns(element)
+      end
+
+    end,
+
+  Meta = function (meta)
+
+    return process_meta(meta)
+
+  end
+}
+
+--- Syntactic sugar filter.
+-- Provides alternative ways of specifying columns properties.
+-- Kept separate from the pre-processing filter for clarity.
+-- @field Div looks for Div of classes `columns` (and related) and `column-span`
+syntactic_sugar_filter = {
+
+  Div = function(element)
+
+      -- convert "two-columns" into `columns` Divs
+      for _,class in pairs(element.classes) do
+
+        -- match xxxcolumns, xxx_columns, xxx-columns
+        -- if xxx is the name of a number, make
+        -- a `columns` div and set its `column-count` attribute
+        local number = number_by_name(
+          string.match(class,'(%a+)[_%-]?columns$')
+          )
+
+        if number then
+
+          element = set_attribute(element,
+              "column-count", tostring(number))
+          element = remove_class(element, class)
+          element = add_class(element, "columns")
+
+        end
+
+      end
+
+      -- allows different ways of specifying `columns` attributes
+      if element.classes:includes('columns') then
+
+        element = consolidate_colattrib_aliases(element)
+
+      end
+
+      -- `column-span` syntax
+      -- mark up as "to-be-processed" to avoid
+      --  double processing when nested
+      if element.classes:includes('column-span') or
+        element.classes:includes('columnspan') then
+
+        element = add_class(element, 'column-span-to-be-processed')
+        element = remove_class(element, 'column-span')
+        element = remove_class(element, 'columnspan')
+
+      end
+
+    return element
+
+  end
+
+}
+
+--- Read options filter
+read_options_filter = {
+  Meta = function (meta)
+
+    if not meta then return end
+
+    -- global vertical ragged / justified settings
+    if meta.raggedcolumns or meta['ragged-columns'] then
+      options.raggedcolumns = true
+    elseif meta.justifiedcolumns or meta['justified-columns'] then
+      options.raggedcolumns = false
+    end
+
+  end
+}
+
+-- Main statement returns filters only if the
+-- target format matches our list. The filters
+-- returned are applied in the following order:
+-- 1. `syntatic_sugar_filter` deals with multiple syntax
+-- 2. `preprocessing_filter` converts all explicit
+--    columnbreaks into a common syntax and tags
+--    those that are already counted. We must do
+--    that for all `columns` environments before
+--    turning any break back into LaTeX `\columnbreak` blocks
+--    otherwise we mess up the count in nested `columns` Divs.
+-- 3. `format_filter` formats the columns after the counting
+--    has been done
+if format_matches(target_formats) then
+  return {
+    read_options_filter,
+    syntactic_sugar_filter,
+    preprocess_filter,
+    format_filter
+  }
+else
+  return
+end
diff --git a/out.pdf b/out.pdf
new file mode 100644 (file)
index 0000000..5080395
Binary files /dev/null and b/out.pdf differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..187b301
--- /dev/null
@@ -0,0 +1,6 @@
+click==8.1.7
+future==1.0.0
+pandoc-mustache==0.1.0
+panflute==2.3.1
+pystache==0.6.5
+PyYAML==6.0.1
diff --git a/src/body.md b/src/body.md
new file mode 100644 (file)
index 0000000..8a72490
--- /dev/null
@@ -0,0 +1,330 @@
+---
+title: Fief
+toc: true
+geometry: margin=2cm
+mustache:
+- ./src/vars.yaml
+include-before: |
+    where does this line end up?
+
+    or this one?
+---
+\pagebreak
+# Gameplay
+{{title}} is a blah blah blah
+## Checks
+Checks are made against a character's [*skills*](#skills). To make a skill attempt,
+roll 3d6 and compare the roll to the skill's [*ceiling roll (CR)*](#ceiling-roll). If
+you rolled under the [*CR*](#ceiling-roll), congratulations! You succeeded.
+
+### Ceiling Roll
+A skill's Ceiling Roll is equal to the highest associated [*Stat*](#stats) value,
+plus the skill's modifier, plus any situational modifiers. For example:
+
+> Grug is attempting to intimidate a goblin into fleeing, and a skill
+> check is called. The player rolls 3d6 for a total of **9**. Grug is
+> [*trained*](#trained-attempts) with intimidation and has a Strength score of
+> {{grug_str}}. The total CR is:
+>
+> > {{grug_str}} (stat value) + 0 (skill value) + 1 (Situational Bonus, Grug has just
+> ripped another goblin's head off) = **13**
+>
+> Grug succeeds.
+
+### Untrained Attempts
+Characters that are not trained in a skill can still make *Untrained Attempts*.
+These are treated as a skill value of **-{{untrained_malus}}**. For example:
+
+> Grug spies a strange goblin shrine, and wants to piece together what it means;
+> a skill check is called. The player rolls {{untrained_malus}}d6 for a total of **11**. Grug is
+> *untrained* in Knowledge (Occult) and has an Intelligence score of {{grug_int}}. The total CR is:
+>
+> > 9 (stat value) - {{untrained_malus}} (skill value, untrained) + 0 (no situational bonus) = **6**
+>
+> Grug fails.
+
+### Trained Attempts
+
+If a character is *trained* in a skill, they can attempt to make a skill check
+without a penalty. We saw an example of this [above](#ceiling-roll). However,
+characters can gain a bonus to their skill values, through either [character creation](#character-creation)
+or [advancement](#advancement). The skill value for trained skills begins at +0
+when the skill is learned, up to a maximum of +3
+
+### Contested Checks
+
+When a contested check is required, the character initiating the contest makes
+their skill check. If that succeeds, then the defender makes their skill check.
+If the Initiator fails, or the defender succeeds, then the Initiator's attempt fails.
+Let's looks at an example:
+
+> Grug is trying to sneak past a guard, a Stealth skill check is called, and Grug succeeds.
+> The guard then makes a Notice check, and fails.
+> Grug is able to sneak past the guard.
+
+### Critical Success and Critical Failures
+A roll of a **3** on a [*check*](#check) is considered a Critical Success,
+likewise an **18** is a Critical Failure.
+
+#### Criticals in Combat
+On a Critical Success *Fighting* or *Shooting* check in combat, the damage dice
+are doubled, and the roll value is maximised. For example:
+
+> Grug swings his sword at a goblin, and rolls a critical success. Rather than
+> rolling the {{grug_dam_dice}} damage for his Greatsword, he deals 2x(6+6) = 24 damage.
+
+#### Critical Healing
+More details on Criticals and healing can be found under [*Healing*](#healing)
+on page X. But for a quick summary:
+A Critical Success on a *Healing* check restores all wounds!
+A Critical Failure on a *Healing* check kills the patient!
+
+#### Criticals in Other Contexts
+The results of a Critical Success or Critical Failure on a skill check in other
+contexts are left up to the GM, but should be suitably spectacular.
+
+## Combat
+### Initiative
+When combat begins, Characters are called to make Initiative checks. Characters that succeed
+their check go before characters that fail, with ties being broken by Initiative scores.
+Initiative for Monsters/NPCs can either be determined the same way, or placed in order of their Initiative scores.
+
+Play order then alternates between Characters and Monsters/NPCs
+
+### Actions in Combat
+On a turn in combat, a Character can take one action without penalty. If a Character
+wishes to make a second action in their turn, they must declare their intended actions
+first. If a Character is taking two actions in a turn, any checks performed by
+that Character are made with a **-{{minor_malus}}** situational modifier until the start of
+their next turn.
+A Character cannot cast two [*Spells*](#magic) during a turn.
+During a turn a Character may:
+
+* Attack
+* Move
+* Cast a Spell
+* Hide
+* Search
+* Use an Object
+* Communicate
+
+### Attacking and Dealing Damage
+To make a successful attack against a foe, a Character must succeed on a  [*Fighting*](#skills) or
+[*Shooting*](#skills) check to hit a foe. Damage dice are then rolled depending on the weapon type.
+If the damage value is greater than the foe's [*Armour Value*](#vitals) + [*Toughness*](#vitals),
+the strike lands true and the foe gains a Wound.
+
+> Grug attempts to stab an Orc, with a Strength of {{grug_str}} and a Fighting skill value of {{grug_fighting}},
+> a skill check is called and the player rolls 3d6 for a total of 7, a success!
+> The player rolls {{grug_dam_dice}} for damage, but only rolls a 2, since this
+> is under the Orc's Toughness of {{orc_tn}}, the Orc is able to shrug off the
+> blow.
+
+### Moving
+A Character can move up to 30ft (6 inches) if they choose to take the *Move* action.
+
+### Casting a Spell
+When a Character takes the *Cast a Spell* action, they declare the spell that
+they are beginning to cast. And make a check using their appropriate skill.
+On a failure, the spell fizzles. On a success, the Character continues casting
+the spell until the start of their next Turn. At the start of the Character's
+next turn, the spell resolves; and it's effect(s) occur.
+If the Character casting a spell gains a Wound before their spell resolves, it fizzles.
+Resolving a spell does not require an action.
+
+### Hiding
+A Character may take the *Hide* action when the line of sight between them and
+one or more other characters has been broken. A successful *Stealth* check
+hides the Character from the others, who mark the [*Lost Track (Character)*](#status-effects)
+condition. A Character may re-enter the sightline of a defender who has
+[*Lost Track*](#status-effects) of them without immediate penalty.
+
+Attacking character who has [*Lost Track*](#status-effects) of you gives you a
++{{minor_boon}} bonus to your [*CR*](#ceiling-roll) to attack them.
+
+> TODO: Example
+
+### Search
+A Character can search for a hidden door, object or Character in combat with the Search action.
+When a Character wishes to Search, a [*Notice*](#skills) check is called. On a success, the
+Character finds the door, object, etc.
+
+When Searching for a Character that you have [*Lost Track*](#status-effects) of,
+you have a +{{major_boon}} bonus to your [*Notice*](#skills) check if the target of your search is within
+line of sight.
+
+### Use an Object
+Objects may generally be retrieved from a pouch, bandolier, belt etc without an action - but *using* an object; such
+as quaffing a potion, zapping a wand etc requires an Action to use.
+
+
+### Communicate
+Communicating to other Characters in combat requires an action. This a very broad
+action that encompasses everything from communicating tactics; Intimidating or
+Persuading foes and more.
+
+If a Character has [*Lost Track*](#status-effects) of another, you can use the
+Communicate action to point out their location, provided that your Character
+hasn't also [*Lost Track*](#status-effects) of them.
+
+
+## Healing
+The Healing skill is used to remove [*Wounds*](#vitals). Each attempt requires
+10 minutes. A success removes one [*Wound*](#vitals). Failing the roll means
+that healer isn't able to treat those particular injuries. A change in circumstance,
+such as the discovery of medical supplies may warrant another attempt.
+Once the Wounds are over an hour old, only [*natural healing*](#natural-healing)
+or magic can heal Wounds.
+A Critical Success on a *Healing* check restores all wounds!
+A Critical Failure on a *Healing* check kills the patient!
+
+### Natural Healing
+Wounded character make a [*Fortitude Save*](#saves) once per night's rest.
+Success removes one wound, and a Critical Success removes all wounds. A Critical
+Failure when healing naturally *increases* the Character's wounds by one,
+this could be the result of blood loss, aggravating the wound or infection.
+If this new wound would [*Incapacitate*](#status-effects) the Character, then
+they must immediately succeed a second [*Fortitude Save*](#saves) or expire.
+If another Character is keeping watch over the wounded Character, and witnesses
+the wounded Character about to expire, they can make a *Healing* check; saving and
+stabilising the wounded Character on a success.
+
+### Stabilisation
+[*Incapacitated*](#status-effects) Characters can be *Stabilised* with a
+successful Healing roll from another Character.
+
+## Supporting
+Successfully *Supporting* another Character grants them a +1 to the [*CR*](#ceiling-roll)
+of their next check. To support another Character, first envision how you are trying
+to help them, and make the appropriate check when called. For example:
+
+> Grug's more talkative friend is trying to negotiate a bargain with a shop-keep.
+> While his friend is trying to put forward a persuasive argument, Grug grins;
+> hefts his Greatsword and gives it a few test swings, keeping his eyes on the shop-keep.
+> Grug succeeds his *Intimidation* check, and his friend gains a +1 to the [*CR*](#ceiling-roll)
+> of their *Persuasion* check.
+
+Multiple Characters can *Support* a check, each granting a cumulative +1 to the
+[*CR*](#ceiling-roll)
+
+## Encumberance
+Many items in {{title}} can be stuffed into a backpack and not given a second thought.
+Items tagged with *Cumbersome* are different, however. A Character can carry a number of
+*Cumbersome* items equal to their Strength [*stat*](#stats) $\div$ 4, rounding down.
+Some items are *Heavy*. *Heavy* items usually require two Characters to lift.
+
+## Status Effects
+## Situational Rules
+
+# Character Creation
+## Stats
+Character in {{title}} are, in part, defined by 8 stats. **Strength**,
+**Intelligence**, **Constitution**, **Willpower**, **Dexterity**, **Perception**,
+**Beauty** and **Charisma**. To get a better understanding of what these stats
+actually _represent_, we can lay them out like so:
+
+
+|                | **Physical** | **Cerebral** |
+|----------------|--------------|--------------|
+| **Power**      | Strength     | Intelligence |
+| **Resistance** | Constitution | Willpower    |
+| **Prowess**    | Dexterity    | Perception   |
+| **Appearance** | Beauty       | Charisma     |
+
+To generate your stats, roll 3d6; in order of Strength, Intelligence, Constitution,
+Willpower, Dexterity, Perception, Beauty and Charisma.
+
+## Consider your Character
+Given the stats you've rolled, you are beginning to get an idea of where your
+character's strengths and weaknesses lie. A value of **10** in a stat can be considered
+average for a human, a **7** would be notably deficient, and a **13** would be well above-average.
+
+## Advantages & Disadvantages
+Select two Advantages and Disadvantages for your character:
+
+### List of Disadvantages
+### List of Advantages
+
+## TODO: Tragic Flaw & Glorious Endeavour
+
+## Skills
+You have **three** skill-points to spend, a skill-point can be spent to become
+[*trained*](#trained-attempts) in a skill, or increase the [*CR*](#ceiling-roll)
+of a [*trained*](#trained-attempts) skill by one, this can only be done once per skill.
+Additionally, choose one [*Save*](#saves) to become [*trained*](#trained-attempts) in.
+
+### Core Skills
+
+Core skills are skills that all characters are [*trained*](#trained-attempts) in by default. These are:
+
+* Athletics (Strength, Dexterity)
+* Deception (Charisma)
+* Initiative (Dexterity)
+* Knowledge, Common (Intelligence)
+* Persuasion (Charisma, Beauty)
+* Stealth (Dexterity)
+
+### List of Skills
+* Animal Handling (Willpower)
+* Escape Artistry (Dexterity)
+* Faith (Willpower)
+* Fighting (Strength)
+* Flirting (Beauty, Charisma)
+* Grappling (Strength)
+* Healing (Intelligence, Willpower)
+* Insight (Perception)
+* Intimidation (Charisma, Strength)
+* Knowledge, Academics (Intelligence)
+* Knowledge, History (Intelligence)
+* Knowledge, Nature (Intelligence)
+* Knowledge, Occult (Intelligence)
+* Knowledge, Religion (Intelligence)
+* Notice (Perception)
+* Performance (Charisma)
+* Shooting (Dexterity)
+* Slight of Hand (Dexterity)
+* Survival (Willpower)
+* Swimming (Strength)
+* Thievery (Dexterity)
+* Wizardry (Intelligence)
+
+### Saves
+Saves are skills used to resist harmful effects:
+
+* Reflex (Dexterity)
+* Will (Willpower)
+* Fortitude (Constitution)
+
+## Vitals
+Characters also have a set of Vitals, these are separate from [*Stats*](#stats)
+in that they can change based on circumstance, [*Advantages*](#advantages-&-disadvantages)
+and [*Disadvantages*](#advantages-&-disadvantages).
+A character's Vitals are as follows:
+
+* **Speed** - How fast a Character can move in a turn, measured in feet.
+* **Size** - How much space a Character takes up, ranging from -1 to +3. See [*Scale*](#scale) on page X for more.
+* **Toughness** - A representation of how generally tough a Character is,
+incoming Damage is reduced by a Character's Toughness
+* **Armour Value** - A representation of how heavily armoured a Character is,
+incoming Damage is reduced by a Character's Armour Value
+* **Wounds** - A Character can typically take 3 wounds while still functioning
+normally, any further damage taken when a Character is at 3 wounds [*incapacitates*](#incapacitation)
+them.
+
+## TODO: Races
+## TODO: Wealth
+## TODO: Starting Gear
+
+# TODO: Advancement
+# TODO: Dungeoneering
+## TODO: Time in the Dungeon
+## TODO: Generating a Dungeon
+
+# TODO: Hex Crawls
+
+# TODO: Playing with Miniatures
+## TODO: Scale
+# TODO: Areas of Effect
+
+# TODO: Running The Game
+# TODO: Tables
diff --git a/src/preamble.md b/src/preamble.md
new file mode 100644 (file)
index 0000000..4f757d9
--- /dev/null
@@ -0,0 +1 @@
+# Fief
diff --git a/src/vars.yaml b/src/vars.yaml
new file mode 100644 (file)
index 0000000..f6ee163
--- /dev/null
@@ -0,0 +1,11 @@
+title: Fief
+untrained_malus: 3
+minor_malus: 2
+major_malus: 4
+minor_boon: 1
+major_boon: 2
+grug_str: 12
+grug_int: 9
+grug_dam_dice: 2d6
+grug_fighting: +1
+orc_tn: 2
diff --git a/template.tex b/template.tex
new file mode 100644 (file)
index 0000000..6d29e34
--- /dev/null
@@ -0,0 +1,594 @@
+% Options for packages loaded elsewhere
+\PassOptionsToPackage{unicode$for(hyperrefoptions)$,$hyperrefoptions$$endfor$}{hyperref}
+\PassOptionsToPackage{hyphens}{url}
+$if(colorlinks)$
+\PassOptionsToPackage{dvipsnames,svgnames,x11names}{xcolor}
+$endif$
+$if(CJKmainfont)$
+\PassOptionsToPackage{space}{xeCJK}
+$endif$
+%
+\documentclass[
+$if(fontsize)$
+  $fontsize$,
+$endif$
+$if(papersize)$
+  $papersize$paper,
+$endif$
+$if(beamer)$
+  ignorenonframetext,
+$if(handout)$
+  handout,
+$endif$
+$if(aspectratio)$
+  aspectratio=$aspectratio$,
+$endif$
+$endif$
+$for(classoption)$
+  $classoption$$sep$,
+$endfor$
+]{$documentclass$}
+\usepackage{multicol}
+$if(beamer)$
+$if(background-image)$
+\usebackgroundtemplate{%
+  \includegraphics[width=\paperwidth]{$background-image$}%
+}
+% In beamer background-image does not work well when other images are used, so this is the workaround
+\pgfdeclareimage[width=\paperwidth,height=\paperheight]{background}{$background-image$}
+\usebackgroundtemplate{\pgfuseimage{background}}
+$endif$
+\usepackage{pgfpages}
+\setbeamertemplate{caption}[numbered]
+\setbeamertemplate{caption label separator}{: }
+\setbeamercolor{caption name}{fg=normal text.fg}
+\beamertemplatenavigationsymbols$if(navigation)$$navigation$$else$empty$endif$
+$for(beameroption)$
+\setbeameroption{$beameroption$}
+$endfor$
+% Prevent slide breaks in the middle of a paragraph
+\widowpenalties 1 10000
+\raggedbottom
+$if(section-titles)$
+\setbeamertemplate{part page}{
+  \centering
+  \begin{beamercolorbox}[sep=16pt,center]{part title}
+    \usebeamerfont{part title}\insertpart\par
+  \end{beamercolorbox}
+}
+\setbeamertemplate{section page}{
+  \centering
+  \begin{beamercolorbox}[sep=12pt,center]{part title}
+    \usebeamerfont{section title}\insertsection\par
+  \end{beamercolorbox}
+}
+\setbeamertemplate{subsection page}{
+  \centering
+  \begin{beamercolorbox}[sep=8pt,center]{part title}
+    \usebeamerfont{subsection title}\insertsubsection\par
+  \end{beamercolorbox}
+}
+\AtBeginPart{
+  \frame{\partpage}
+}
+\AtBeginSection{
+  \ifbibliography
+  \else
+    \frame{\sectionpage}
+  \fi
+}
+\AtBeginSubsection{
+  \frame{\subsectionpage}
+}
+$endif$
+$endif$
+$if(beamerarticle)$
+\usepackage{beamerarticle} % needs to be loaded first
+$endif$
+\usepackage{amsmath,amssymb}
+$if(linestretch)$
+\usepackage{setspace}
+$endif$
+\usepackage{iftex}
+\ifPDFTeX
+  \usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc}
+  \usepackage[utf8]{inputenc}
+  \usepackage{textcomp} % provide euro and other symbols
+\else % if luatex or xetex
+$if(mathspec)$
+  \ifXeTeX
+    \usepackage{mathspec} % this also loads fontspec
+  \else
+    \usepackage{unicode-math} % this also loads fontspec
+  \fi
+$else$
+  \usepackage{unicode-math} % this also loads fontspec
+$endif$
+  \defaultfontfeatures{Scale=MatchLowercase}$-- must come before Beamer theme
+  \defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
+\fi
+$if(fontfamily)$
+$else$
+$-- Set default font before Beamer theme so the theme can override it
+\usepackage{lmodern}
+$endif$
+$-- Set Beamer theme before user font settings so they can override theme
+$if(beamer)$
+$if(theme)$
+\usetheme[$for(themeoptions)$$themeoptions$$sep$,$endfor$]{$theme$}
+$endif$
+$if(colortheme)$
+\usecolortheme{$colortheme$}
+$endif$
+$if(fonttheme)$
+\usefonttheme{$fonttheme$}
+$endif$
+$if(mainfont)$
+\usefonttheme{serif} % use mainfont rather than sansfont for slide text
+$endif$
+$if(innertheme)$
+\useinnertheme{$innertheme$}
+$endif$
+$if(outertheme)$
+\useoutertheme{$outertheme$}
+$endif$
+$endif$
+$-- User font settings (must come after default font and Beamer theme)
+$if(fontfamily)$
+\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$}
+$endif$
+\ifPDFTeX\else
+  % xetex/luatex font selection
+$if(mainfont)$
+  \setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
+$endif$
+$if(sansfont)$
+  \setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$}
+$endif$
+$if(monofont)$
+  \setmonofont[$for(monofontoptions)$$monofontoptions$$sep$,$endfor$]{$monofont$}
+$endif$
+$for(fontfamilies)$
+  \newfontfamily{$fontfamilies.name$}[$for(fontfamilies.options)$$fontfamilies.options$$sep$,$endfor$]{$fontfamilies.font$}
+$endfor$
+$if(mathfont)$
+$if(mathspec)$
+  \ifXeTeX
+    \setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
+  \else
+    \setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
+  \fi
+$else$
+  \setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
+$endif$
+$endif$
+$if(CJKmainfont)$
+  \ifXeTeX
+    \usepackage{xeCJK}
+    \setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
+    $if(CJKsansfont)$
+      \setCJKsansfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKsansfont$}
+    $endif$
+    $if(CJKmonofont)$
+      \setCJKmonofont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmonofont$}
+    $endif$
+  \fi
+$endif$
+$if(luatexjapresetoptions)$
+  \ifLuaTeX
+    \usepackage[$for(luatexjapresetoptions)$$luatexjapresetoptions$$sep$,$endfor$]{luatexja-preset}
+  \fi
+$endif$
+$if(CJKmainfont)$
+  \ifLuaTeX
+    \usepackage[$for(luatexjafontspecoptions)$$luatexjafontspecoptions$$sep$,$endfor$]{luatexja-fontspec}
+    \setmainjfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
+  \fi
+$endif$
+\fi
+$if(zero-width-non-joiner)$
+%% Support for zero-width non-joiner characters.
+\makeatletter
+\def\zerowidthnonjoiner{%
+  % Prevent ligatures and adjust kerning, but still support hyphenating.
+  \texorpdfstring{%
+    \TextOrMath{\nobreak\discretionary{-}{}{\kern.03em}%
+      \ifvmode\else\nobreak\hskip\z@skip\fi}{}%
+  }{}%
+}
+\makeatother
+\ifPDFTeX
+  \DeclareUnicodeCharacter{200C}{\zerowidthnonjoiner}
+\else
+  \catcode`^^^^200c=\active
+  \protected\def ^^^^200c{\zerowidthnonjoiner}
+\fi
+%% End of ZWNJ support
+$endif$
+% Use upquote if available, for straight quotes in verbatim environments
+\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
+\IfFileExists{microtype.sty}{% use microtype if available
+  \usepackage[$for(microtypeoptions)$$microtypeoptions$$sep$,$endfor$]{microtype}
+  \UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
+}{}
+$if(indent)$
+$else$
+\makeatletter
+\@ifundefined{KOMAClassName}{% if non-KOMA class
+  \IfFileExists{parskip.sty}{%
+    \usepackage{parskip}
+  }{% else
+    \setlength{\parindent}{0pt}
+    \setlength{\parskip}{6pt plus 2pt minus 1pt}}
+}{% if KOMA class
+  \KOMAoptions{parskip=half}}
+\makeatother
+$endif$
+$if(verbatim-in-note)$
+\usepackage{fancyvrb}
+$endif$
+\usepackage{xcolor}
+$if(geometry)$
+$if(beamer)$
+\geometry{$for(geometry)$$geometry$$sep$,$endfor$}
+$else$
+\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
+$endif$
+$endif$
+$if(beamer)$
+\newif\ifbibliography
+$endif$
+$if(listings)$
+\usepackage{listings}
+\newcommand{\passthrough}[1]{#1}
+\lstset{defaultdialect=[5.3]Lua}
+\lstset{defaultdialect=[x86masm]Assembler}
+$endif$
+$if(lhs)$
+\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
+$endif$
+$if(highlighting-macros)$
+$highlighting-macros$
+$endif$
+$if(tables)$
+\usepackage{longtable,booktabs,array}
+$if(multirow)$
+\usepackage{multirow}
+$endif$
+\usepackage{calc} % for calculating minipage widths
+$if(beamer)$
+\usepackage{caption}
+% Make caption package work with longtable
+\makeatletter
+\def\fnum@table{\tablename~\thetable}
+\makeatother
+$else$
+% Correct order of tables after \paragraph or \subparagraph
+\usepackage{etoolbox}
+\makeatletter
+\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
+\makeatother
+% Allow footnotes in longtable head/foot
+\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}
+\makesavenoteenv{longtable}
+$endif$
+$endif$
+$if(graphics)$
+\usepackage{graphicx}
+\makeatletter
+\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
+\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
+\makeatother
+% Scale images if necessary, so that they will not overflow the page
+% margins by default, and it is still possible to overwrite the defaults
+% using explicit options in \includegraphics[width, height, ...]{}
+\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
+% Set default figure placement to htbp
+\makeatletter
+\def\fps@figure{htbp}
+\makeatother
+$endif$
+$if(svg)$
+\usepackage{svg}
+$endif$
+$if(strikeout)$
+$-- also used for underline
+\ifLuaTeX
+  \usepackage{luacolor}
+  \usepackage[soul]{lua-ul}
+\else
+  \usepackage{soul}
+$if(CJKmainfont)$
+  \ifXeTeX
+    % soul's \st doesn't work for CJK:
+    \usepackage{xeCJKfntef}
+    \renewcommand{\st}[1]{\sout{#1}}
+  \fi
+$endif$
+\fi
+$endif$
+\setlength{\emergencystretch}{3em} % prevent overfull lines
+\providecommand{\tightlist}{%
+  \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
+$if(numbersections)$
+\setcounter{secnumdepth}{$if(secnumdepth)$$secnumdepth$$else$5$endif$}
+$else$
+\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
+$endif$
+$if(subfigure)$
+\usepackage{subcaption}
+$endif$
+$if(beamer)$
+$else$
+$if(block-headings)$
+% Make \paragraph and \subparagraph free-standing
+\ifx\paragraph\undefined\else
+  \let\oldparagraph\paragraph
+  \renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}}
+\fi
+\ifx\subparagraph\undefined\else
+  \let\oldsubparagraph\subparagraph
+  \renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}}
+\fi
+$endif$
+$endif$
+$if(pagestyle)$
+\pagestyle{$pagestyle$}
+$endif$
+$if(csl-refs)$
+% definitions for citeproc citations
+\NewDocumentCommand\citeproctext{}{}
+\NewDocumentCommand\citeproc{mm}{%
+  \begingroup\def\citeproctext{#2}\cite{#1}\endgroup}
+\makeatletter
+ % allow citations to break across lines
+ \let\@cite@ofmt\@firstofone
+ % avoid brackets around text for \cite:
+ \def\@biblabel#1{}
+ \def\@cite#1#2{{#1\if@tempswa , #2\fi}}
+\makeatother
+\newlength{\cslhangindent}
+\setlength{\cslhangindent}{1.5em}
+\newlength{\csllabelwidth}
+\setlength{\csllabelwidth}{3em}
+\newenvironment{CSLReferences}[2] % #1 hanging-indent, #2 entry-spacing
+ {\begin{list}{}{%
+  \setlength{\itemindent}{0pt}
+  \setlength{\leftmargin}{0pt}
+  \setlength{\parsep}{0pt}
+  % turn on hanging indent if param 1 is 1
+  \ifodd #1
+   \setlength{\leftmargin}{\cslhangindent}
+   \setlength{\itemindent}{-1\cslhangindent}
+  \fi
+  % set entry spacing
+  \setlength{\itemsep}{#2\baselineskip}}}
+ {\end{list}}
+\usepackage{calc}
+\newcommand{\CSLBlock}[1]{\hfill\break#1\hfill\break}
+\newcommand{\CSLLeftMargin}[1]{\parbox[t]{\csllabelwidth}{\strut#1\strut}}
+\newcommand{\CSLRightInline}[1]{\parbox[t]{\linewidth - \csllabelwidth}{\strut#1\strut}}
+\newcommand{\CSLIndent}[1]{\hspace{\cslhangindent}#1}
+$endif$
+$if(lang)$
+\ifLuaTeX
+\usepackage[bidi=basic]{babel}
+\else
+\usepackage[bidi=default]{babel}
+\fi
+$if(babel-lang)$
+\babelprovide[main,import]{$babel-lang$}
+$if(mainfont)$
+\ifPDFTeX
+\else
+\babelfont{rm}[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
+\fi
+$endif$
+$endif$
+$for(babel-otherlangs)$
+\babelprovide[import]{$babel-otherlangs$}
+$endfor$
+$for(babelfonts/pairs)$
+\babelfont[$babelfonts.key$]{rm}{$babelfonts.value$}
+$endfor$
+% get rid of language-specific shorthands (see #6817):
+\let\LanguageShortHands\languageshorthands
+\def\languageshorthands#1{}
+$endif$
+$for(header-includes)$
+$header-includes$
+$endfor$
+\ifLuaTeX
+  \usepackage{selnolig}  % disable illegal ligatures
+\fi
+$if(dir)$
+\ifPDFTeX
+  \TeXXeTstate=1
+  \newcommand{\RL}[1]{\beginR #1\endR}
+  \newcommand{\LR}[1]{\beginL #1\endL}
+  \newenvironment{RTL}{\beginR}{\endR}
+  \newenvironment{LTR}{\beginL}{\endL}
+\fi
+$endif$
+$if(natbib)$
+\usepackage[$natbiboptions$]{natbib}
+\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
+$endif$
+$if(biblatex)$
+\usepackage[$if(biblio-style)$style=$biblio-style$,$endif$$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$]{biblatex}
+$for(bibliography)$
+\addbibresource{$bibliography$}
+$endfor$
+$endif$
+$if(nocite-ids)$
+\nocite{$for(nocite-ids)$$it$$sep$, $endfor$}
+$endif$
+$if(csquotes)$
+\usepackage{csquotes}
+$endif$
+\IfFileExists{bookmark.sty}{\usepackage{bookmark}}{\usepackage{hyperref}}
+\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
+\urlstyle{$if(urlstyle)$$urlstyle$$else$same$endif$}
+$if(links-as-notes)$
+% Make links footnotes instead of hotlinks:
+\DeclareRobustCommand{\href}[2]{#2\footnote{\url{#1}}}
+$endif$
+$if(verbatim-in-note)$
+\VerbatimFootnotes % allow verbatim text in footnotes
+$endif$
+\hypersetup{
+$if(title-meta)$
+  pdftitle={$title-meta$},
+$endif$
+$if(author-meta)$
+  pdfauthor={$author-meta$},
+$endif$
+$if(lang)$
+  pdflang={$lang$},
+$endif$
+$if(subject)$
+  pdfsubject={$subject$},
+$endif$
+$if(keywords)$
+  pdfkeywords={$for(keywords)$$keywords$$sep$, $endfor$},
+$endif$
+$if(colorlinks)$
+  colorlinks=true,
+  linkcolor={$if(linkcolor)$$linkcolor$$else$Maroon$endif$},
+  filecolor={$if(filecolor)$$filecolor$$else$Maroon$endif$},
+  citecolor={$if(citecolor)$$citecolor$$else$Blue$endif$},
+  urlcolor={$if(urlcolor)$$urlcolor$$else$Blue$endif$},
+$else$
+$if(boxlinks)$
+$else$
+  hidelinks,
+$endif$
+$endif$
+  pdfcreator={LaTeX via pandoc}}
+
+$if(title)$
+\title{$title$$if(thanks)$\thanks{$thanks$}$endif$}
+$endif$
+$if(subtitle)$
+$if(beamer)$
+$else$
+\usepackage{etoolbox}
+\makeatletter
+\providecommand{\subtitle}[1]{% add subtitle to \maketitle
+  \apptocmd{\@title}{\par {\large #1 \par}}{}{}
+}
+\makeatother
+$endif$
+\subtitle{$subtitle$}
+$endif$
+\author{$for(author)$$author$$sep$ \and $endfor$}
+\date{$date$}
+$if(beamer)$
+$if(institute)$
+\institute{$for(institute)$$institute$$sep$ \and $endfor$}
+$endif$
+$if(titlegraphic)$
+\titlegraphic{\includegraphics{$titlegraphic$}}
+$endif$
+$if(logo)$
+\logo{\includegraphics{$logo$}}
+$endif$
+$endif$
+
+\begin{document}
+$if(has-frontmatter)$
+\frontmatter
+$endif$
+$if(title)$
+$if(beamer)$
+\frame{\titlepage}
+$else$
+\maketitle
+$endif$
+$if(abstract)$
+\begin{abstract}
+$abstract$
+\end{abstract}
+$endif$
+$endif$
+
+$for(include-before)$
+$include-before$
+
+$endfor$
+$if(toc)$
+$if(toc-title)$
+\renewcommand*\contentsname{$toc-title$}
+$endif$
+$if(beamer)$
+\begin{frame}[allowframebreaks]
+$if(toc-title)$
+  \frametitle{$toc-title$}
+$endif$
+  \tableofcontents[hideallsubsections]
+\end{frame}
+$else$
+{
+$if(colorlinks)$
+\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$$endif$}
+$endif$
+\setcounter{tocdepth}{$toc-depth$}
+\tableofcontents
+}
+$endif$
+$endif$
+$if(lof)$
+\listoffigures
+$endif$
+$if(lot)$
+\listoftables
+$endif$
+$if(linestretch)$
+\setstretch{$linestretch$}
+$endif$
+$if(has-frontmatter)$
+\mainmatter
+$endif$
+\begin{multicols}{2}
+$body$
+\end{multicols}
+
+$if(has-frontmatter)$
+\backmatter
+$endif$
+$if(natbib)$
+$if(bibliography)$
+$if(biblio-title)$
+$if(has-chapters)$
+\renewcommand\bibname{$biblio-title$}
+$else$
+\renewcommand\refname{$biblio-title$}
+$endif$
+$endif$
+$if(beamer)$
+\begin{frame}[allowframebreaks]{$biblio-title$}
+  \bibliographytrue
+$endif$
+  \bibliography{$for(bibliography)$$bibliography$$sep$,$endfor$}
+$if(beamer)$
+\end{frame}
+$endif$
+
+$endif$
+$endif$
+$if(biblatex)$
+$if(beamer)$
+\begin{frame}[allowframebreaks]{$biblio-title$}
+  \bibliographytrue
+  \printbibliography[heading=none]
+\end{frame}
+$else$
+\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
+$endif$
+
+$endif$
+$for(include-after)$
+$include-after$
+
+$endfor$
+\end{document}