commit baa26137b8bf1eed60f6a62215fffa6cb3bf1b22
Author: Hugo Soucy <hugo@soucy.cc>
Date: Sat, 30 Jan 2021 15:10:59 -0500
DrFerron is now Satelito
Diffstat:
26 files changed, 2849 insertions(+), 0 deletions(-)
diff --git a/.editorconfig b/.editorconfig
@@ -0,0 +1,16 @@
+# EditorConfig <http://EditorConfig.org>
+
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+
+# 2 space indentation (CSS, YML, and templates HTML big family)
+#[*.{css,yml,xml,xhtml,html,vm,ftl,mustache,twig,hbs,yaml}]
+#indent_size = 2
diff --git a/README.org b/README.org
@@ -0,0 +1,16 @@
+* Satelito
+
+*Satelito* is a static site generator (ssg) made with lua script.
+
+** Installation
+
+You must have Luarocks installed on your computer.
+
+ - =luarocks make --local=
+
+- =find mySiteSrc/ -name '*.md' | satelito --export=
+- =ag '' --markdown -l mySitesrc/ | satelito --export=
+
+Watch changes =find= et =entr=:
+
+- =find mySiteSrc/ | entr -s 'echo "Modification!" && find mySiteSrc/ -name "*.md" | ./satelito/satelito --export'=
diff --git a/bin/satelito b/bin/satelito
@@ -0,0 +1,2 @@
+#!/usr/bin/env lua
+require 'satelito.init'
diff --git a/sample/config.lua b/sample/config.lua
@@ -0,0 +1,28 @@
+
+--
+-- Website Configuration File
+--
+
+return {
+ domainname = "luvgoude.tld",
+ url = "https://luvgoude.tld",
+ language = "en",
+
+ -- Main paths
+ paths = {
+ content = 'content/',
+ css = 'public_html/css/',
+ images = 'public_html/images/',
+ public_html = 'public_html/',
+ templates = 'templates/',
+ },
+
+ -- Accepted mime types of the non-textual content
+ mimetypes = {
+ 'image/svg+xml',
+ 'image/gif',
+ 'image/jpeg',
+ 'image/png',
+ 'application/pdf',
+ },
+}
diff --git a/sample/templates/default-index.html b/sample/templates/default-index.html
@@ -0,0 +1,19 @@
+<article>
+ <div>
+ <%- content %>
+ </div>
+
+ <% if children then %>
+ <ol>
+ <% -- Desc sorting children %>
+ <% table.sort(children, function(a , b) return tonumber(a.time_created) > tonumber(b.time_created) end) %>
+
+ <% for i, child in ipairs(children) do %>
+ <li>
+ <a href="<%= child.rellink -%>"><%= child.title -%></a> —
+ <time datetime="<%= child.date -%>T<%= child.datetime -%>"><%= child.date -%></time>
+ </li>
+ <% end %>
+ </ol>
+ <% end %>
+</article>
diff --git a/sample/templates/default.html b/sample/templates/default.html
@@ -0,0 +1,16 @@
+<article>
+ <%- content %>
+
+ <footer>
+ <p>
+ Published on
+ <time class="dt-published" datetime="<%= date -%>T<%= datetime -%>"><%= date -%></time>,
+ by
+ <a
+ href="<%= author.uri -%>"
+ rel="author">
+ <i><%= author.name -%></i>
+ </a>
+ </p>
+ </footer>
+</article>
diff --git a/sample/templates/feed.xml.html b/sample/templates/feed.xml.html
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr">
+ <title><%- title -%></title>
+ <% if description then %>
+ <subtitle><%- description -%></subtitle>
+ <% end %>
+ <id><%- id -%></id>
+ <link href="<%- dirlink -%>index.xml" rel="self" type="application/atom+xml" />
+ <link href="<%= permalink -%>" rel="alternate" />
+ <link href="https://creativecommons.org/licenses/by/2.5/ca/deed.fr" rel="license" />
+ <updated><%- os.date('%Y-%m-%dT%H:%M:%S', time_modified_child) -%>Z</updated>
+ <author>
+ <name><%- author.name -%></name>
+ <email><%- author.email -%></email>
+ <uri><%- author.uri -%></uri>
+ </author>
+
+ <% table.sort(children, function(a , b) return tonumber(a.time_created) > tonumber(b.time_created) end) %>
+ <% for i, child in ipairs(children) do %>
+ <entry>
+ <title><%- child.title -%></title>
+ <id><%- child.id -%></id>
+ <link href="<%= child.permalink -%>" hreflang="<%= child.language -%>" rel="alternate" type="text/html" />
+ <published><%= child.date -%>T<%= child.datetime -%>Z</published>
+ <updated><%- os.date('%Y-%m-%dT%H:%M:%S', child.time_modification) -%>Z</updated>
+ <content type="html">
+ <%= child.content -%>
+ </content>
+ </entry>
+ <% end %>
+</feed>
diff --git a/sample/templates/head.html b/sample/templates/head.html
@@ -0,0 +1,34 @@
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+ <title><%= title -%> — <%= domainname -%></title>
+
+ <link href="<%= permalink -%>" hreflang="<%= language -%>" rel="canonical" />
+
+ <meta name="robots" content="index, follow" />
+ <meta name="author" content="<%= author.name -%>" />
+ <meta name="generator" content="<%= metas.generator -%>" />
+
+ <% if description then %>
+ <meta content="<%= description -%>" name="description" />
+ <% end %>
+
+ <% if keywords then %>
+ <% local _keywords = table.concat(keywords,',') %>
+ <meta name="keywords" content="<%= _keywords -%>" />
+ <% end %>
+
+ <!-- Atom Feed -->
+ <link
+ href="/index.xml"
+ hreflang="<%= language -%>"
+ rel="alternate"
+ title="Main feed"
+ type="application/atom+xml" />
+
+ <!-- Me elsewhere -->
+ <% for _, relme in ipairs(author.relme) do %>
+ <link href="<%= relme -%>" rel="me" />
+ <% end %>
+</head>
diff --git a/sample/templates/layout.html b/sample/templates/layout.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="<%= language -%>">
+ <%- head %>
+ <body class="layout <%- template or posttype or 'default' -%>" id="t">
+ <%- navigation -%>
+
+ <main class="main" id="m">
+ <%- post %>
+ </main>
+
+ <footer class="footer-banner">
+ <div>
+ <time><%= os.date('%Y') -%></time>
+ </div>
+ </footer>
+ </body>
+</html>
diff --git a/sample/templates/navigation.html b/sample/templates/navigation.html
@@ -0,0 +1,9 @@
+<nav class="nav" id="n">
+ <h2 class="nav__title">Navigation</h2>
+
+ <ol class="nav__list">
+ <li class="nav__item">
+ <a href="/" rel="index">Home</a>
+ </li>
+ </ol>
+</nav>
diff --git a/satelito-dev-1.rockspec b/satelito-dev-1.rockspec
@@ -0,0 +1,42 @@
+package = "satelito"
+version = "dev-1"
+source = {
+ url = "https://soucy.cc/satelito.tar.gz"
+}
+description = {
+ homepage = "https://soucy.cc/git/satelito/files.html",
+ license = "MIT"
+}
+dependencies = {
+ "lua >= 5.3",
+ ---
+ "argparse >= 0.7.1-1",
+ "etlua >= 0.6.0",
+ "mimetypes >= 1.0.0-2",
+ "lua-discount >= 1.2.10.1-1",
+ "luafilesystem >= 1.8.0-1",
+}
+build = {
+ type = "builtin",
+ modules = {
+ satelito = "satelito/init.lua",
+ --
+ ["satelito.assets"] = "satelito/assets.lua",
+ ["satelito.dirtree"] = "satelito/dirtree.lua",
+ ["satelito.feed"] = "satelito/feed.lua",
+ ["satelito.file"] = "satelito/file.lua",
+ ["satelito.list"] = "satelito/list.lua",
+ ["satelito.model"] = "satelito/model.lua",
+ ["satelito.page"] = "satelito/page.lua",
+ ["satelito.site"] = "satelito/site.lua",
+ ["satelito.template"] = "satelito/template.lua",
+ ["satelito.lib.lume.lume"] = "satelito/lib/lume/lume.lua",
+ ["satelito.lib.lume.test.test"] = "satelito/lib/lume/test/test.lua",
+ ["satelito.lib.lume.test.util.tester"] = "satelito/lib/lume/test/util/tester.lua",
+ },
+ install = {
+ bin = {
+ "bin/satelito"
+ }
+ }
+}
diff --git a/satelito/assets.lua b/satelito/assets.lua
@@ -0,0 +1,37 @@
+-- @module assets
+local assets = {}
+--
+local file = require 'satelito.file'
+local lfs = require 'lfs' -- luafilesystem
+local lume = require 'satelito.lib.lume.lume'
+local mimetypes = require 'mimetypes'
+local site = require 'satelito.site'
+
+function assets.export(filepath)
+ local config = assert(site.set_config(filepath), "Sorry, the site configuration can't be set")
+ local mtypes = assert(config.mimetypes)
+ local siblings = assert(lume.array(lfs.dir(file.get_dirname(filepath))))
+
+ for _, sibling in ipairs(siblings) do
+ if lume.any(mtypes, function(mtype) return mtype == mimetypes.guess(sibling) end) then
+ local content_dir = site.get_root(filepath) .. '/' .. config.paths.content
+ local public_dir = site.get_root(filepath) .. '/' .. config.paths.public_html
+ local src_dir = file.get_dirname(filepath)
+ local src_path = src_dir .. sibling
+ local dst_dir = public_dir .. file.get_dirname(file.get_relpath(filepath, content_dir))
+ local dst_path = dst_dir .. sibling
+
+ if not lfs.attributes(dst_path)
+ or lfs.attributes(src_path).modification > lfs.attributes(dst_path).modification
+ then
+ print(src_path, dst_path)
+
+ os.execute('cp ' .. src_path .. ' ' .. dst_path)
+ end
+ end
+ end
+
+ return
+end
+
+return assets
diff --git a/satelito/dirtree.lua b/satelito/dirtree.lua
@@ -0,0 +1,35 @@
+-- @module dirtree
+local lfs = require 'lfs' -- luafilesystem
+local dirtree = {}
+--
+function dirtree.get(dir)
+ assert(dir and dir ~= '', 'directory parameter is missing or empty')
+
+ -- Removes slash if is one
+ if string.sub(dir, -1) == '/' then
+ dir = string.sub(dir, 1, -2)
+ end
+
+ -- Main function of the coroutine (recursive)
+ local function yieldtree(dir)
+ for entry in lfs.dir(dir) do
+ if entry ~= '.' and entry ~= '..' then
+ entry = dir..'/'..entry
+
+ local attr = lfs.attributes(entry)
+
+ --print('DIR ' .. dir, 'ENTRY ' .. entry)
+
+ coroutine.yield(entry,dir,attr)
+
+ if attr.mode == 'directory' then
+ yieldtree(entry)
+ end
+ end
+ end
+ end
+
+ return coroutine.wrap(function() yieldtree(dir) end)
+end
+
+return dirtree
diff --git a/satelito/feed.lua b/satelito/feed.lua
@@ -0,0 +1,35 @@
+-- @module feed
+local feed = {}
+--
+local lume = require 'satelito.lib.lume.lume'
+local file = require 'satelito.file'
+local list = require 'satelito.list'
+local model = require 'satelito.model'
+local site = require 'satelito.site'
+local template = require 'satelito.template'
+
+function feed.build(filepath)
+ local config = assert(site.set_config(filepath), "Sorry, the site configuration can't be set")
+ local relpath = assert(file.get_relpath(filepath, site.get_root(filepath) .. '/' .. config.paths.content))
+ local feed_parameters = assert(model.get(filepath))
+ local feed_entries = { children = assert(file.is_index(filepath) and list.build(filepath) or {}) }
+ local feed_template = assert(template.set_feed(filepath))
+ local feed_xml = feed_template(lume.extend({}, feed_parameters, feed_entries))
+ local feed_xml_path = assert(
+ site.get_root(filepath)
+ .. '/'
+ .. config.paths.public_html
+ .. relpath:match('(.+)%..*') .. '.xml'
+ )
+
+ return feed_xml, feed_xml_path
+end
+
+function feed.export(feedpath, feedxml)
+ file.mkdir(file.get_dirname(feedpath))
+
+ return file.write(feedpath, feedxml)
+end
+
+
+return feed
diff --git a/satelito/file.lua b/satelito/file.lua
@@ -0,0 +1,76 @@
+-- @module file
+local file = {}
+local lfs = require 'lfs'
+local mimetypes = require 'mimetypes'
+local lume = require 'satelito.lib.lume.lume'
+--
+-- Open & read the content of a file
+function file.read(filepath)
+ local _file = assert(io.open(filepath, 'r'))
+ local content = _file:read '*a'
+
+ _file:close()
+
+ return content
+end
+
+-- Create a file and add data in it
+function file.write(filepath, data)
+ local _file = io.open(filepath, 'w+')
+
+ _file:write(data)
+ _file:close()
+
+ return assert(lfs.attributes(filepath).mode == 'file')
+end
+
+-- Get basename from a file path
+function file.get_basename(filepath)
+ return filepath:gsub('(.*/)(.*)', '%2')
+end
+
+-- Get the directory of a file
+function file.get_dirname(filepath)
+ return filepath:match("(.*/)") and filepath:match("(.*/)") or ''
+end
+
+-- Get directory basename
+function file.get_basedir(dirname)
+ return dirname:match('^.+/(.+)$')
+end
+
+function file.get_relpath(filepath, dirname)
+ return filepath:sub((dirname):len() + 1)
+end
+
+-- Get the modified file from a list of paths
+function file.get_lastmodified(filelist)
+ return math.max(table.unpack(lume.map(filelist,
+ function(fl)
+ return lfs.attributes(fl).modification
+ end
+ )))
+end
+
+-- Check if the file is markdown
+function file.is_markdown(filepath)
+ return (mimetypes.guess(filepath) == 'text/x-markdown')
+end
+
+-- Check if the file is index.md
+function file.is_index(filepath)
+ return (file.get_basename(filepath) == 'index.md')
+end
+
+-- Create a dirtree
+function file.mkdir(filepath)
+ local sep, pStr = package.config:sub(1, 1), ''
+
+ for dir in filepath:gmatch('[^' .. sep .. ']+') do
+ pStr = pStr .. sep .. dir
+
+ lfs.mkdir(lfs.currentdir() .. pStr)
+ end
+end
+
+return file
diff --git a/satelito/init.lua b/satelito/init.lua
@@ -0,0 +1,80 @@
+#!/usr/bin/env lua
+--[[
+ *satelito* is a static site generator in lua. Even the [meta]data are in lua table.
+ - You can pipe (stdin) filepaths to it or you can pass a filepath as an argument or you can call it as a module.
+ - It's standalone, so you don't have to install anything to your computer.
+ The couple dependencies are in the `lib/` folder.
+ - But some conventions prevails
+ -- Page who's listed other pages must be named index.md.
+ -- The path of Templates folder must be in the config.lua
+ - satelito don't deal with any preprocessing static content like Sass but you can do it:
+ -- with SassC et entr in a simple shell script ... or pick your favorite libSass flavor.
+
+ It's a only lua and markdown
+ * Site Metadatas => lua
+ * Page HTML
+ ** Content in markdown => HTML
+ ** Metadatas in lua only
+ ** Template with etlua
+--]]
+
+--
+package.path = package.path .. ';'.. arg[0]:match("(.*/)") ..'/?.lua'
+--
+
+local argparse = require 'argparse'
+local assets = require 'drferron.assets'
+local feed = require 'drferron.feed'
+local file = require 'drferron.file'
+local inspect = require 'inspect'
+local page = require 'drferron.page'
+
+local parser = argparse()
+ :name 'satelito'
+ :description '*satelito* is a static site generator in lua script.'
+ :epilog 'For more info, see https://soucy.cc/git/satelito/file/README.org.html'
+
+parser:argument('markdown filepath', 'The filepath to a markdown file'):args '?'
+parser:flag('-e --export', 'Export the outputed HTML in your *public_html/* folder.')
+--parser:flag('-s --site', 'Explicitly process the content in site mode instead of the default page mode.')
+
+
+local args = parser:parse()
+
+if args['markdown filepath'] and file.is_markdown(args['markdown filepath']) then
+ local html, html_path = page.build(args['markdown filepath'])
+
+ if args['export'] then
+ return page.export(html_path, html)
+ else
+ return print(html)
+ end
+else
+ for filepath in (io.lines()) do
+ if file.is_markdown(filepath) then
+ local html, html_path = page.build(filepath)
+ local feed_xml, feed_xml_path
+
+ if args['export'] then
+ page.export(html_path, html)
+
+ -- If filepath is an index
+ -- Then build and export his feed
+ if file.is_index(filepath) then
+ feed_xml, feed_xml_path = feed.build(filepath)
+ feed.export(feed_xml_path, feed_xml)
+ end
+
+ -- Copy assets to the public_html/ folder
+ assets.export(filepath)
+
+ return
+ else
+ return print(html)
+ end
+ else
+ print("Wrong data type!")
+ os.exit()
+ end
+ end
+end
diff --git a/satelito/lib/lume/LICENSE b/satelito/lib/lume/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2020 rxi
+
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/satelito/lib/lume/README.md b/satelito/lib/lume/README.md
@@ -0,0 +1,495 @@
+# Lume
+
+A collection of functions for Lua, geared towards game development.
+
+
+## Installation
+
+The [lume.lua](lume.lua?raw=1) file should be dropped into an existing project
+and required by it:
+
+```lua
+lume = require "lume"
+```
+
+
+## Function Reference
+
+#### lume.clamp(x, min, max)
+Returns the number `x` clamped between the numbers `min` and `max`
+
+#### lume.round(x [, increment])
+Rounds `x` to the nearest integer; rounds away from zero if we're midway
+between two integers. If `increment` is set then the number is rounded to the
+nearest increment.
+```lua
+lume.round(2.3) -- Returns 2
+lume.round(123.4567, .1) -- Returns 123.5
+```
+
+#### lume.sign(x)
+Returns `1` if `x` is 0 or above, returns `-1` when `x` is negative.
+
+#### lume.lerp(a, b, amount)
+Returns the linearly interpolated number between `a` and `b`, `amount` should
+be in the range of 0 - 1; if `amount` is outside of this range it is clamped.
+```lua
+lume.lerp(100, 200, .5) -- Returns 150
+```
+
+#### lume.smooth(a, b, amount)
+Similar to `lume.lerp()` but uses cubic interpolation instead of linear
+interpolation.
+
+#### lume.pingpong(x)
+Ping-pongs the number `x` between 0 and 1.
+
+#### lume.distance(x1, y1, x2, y2 [, squared])
+Returns the distance between the two points. If `squared` is true then the
+squared distance is returned -- this is faster to calculate and can still be
+used when comparing distances.
+
+#### lume.angle(x1, y1, x2, y2)
+Returns the angle between the two points.
+
+#### lume.vector(angle, magnitude)
+Given an `angle` and `magnitude`, returns a vector.
+```lua
+local x, y = lume.vector(0, 10) -- Returns 10, 0
+```
+
+#### lume.random([a [, b]])
+Returns a random number between `a` and `b`. If only `a` is supplied a number
+between `0` and `a` is returned. If no arguments are supplied a random number
+between `0` and `1` is returned.
+
+#### lume.randomchoice(t)
+Returns a random value from array `t`. If the array is empty an error is
+raised.
+```lua
+lume.randomchoice({true, false}) -- Returns either true or false
+```
+
+#### lume.weightedchoice(t)
+Takes the argument table `t` where the keys are the possible choices and the
+value is the choice's weight. A weight should be 0 or above, the larger the
+number the higher the probability of that choice being picked. If the table is
+empty, a weight is below zero or all the weights are 0 then an error is raised.
+```lua
+lume.weightedchoice({ ["cat"] = 10, ["dog"] = 5, ["frog"] = 0 })
+-- Returns either "cat" or "dog" with "cat" being twice as likely to be chosen.
+```
+
+#### lume.isarray(x)
+Returns `true` if `x` is an array -- the value is assumed to be an array if it
+is a table which contains a value at the index `1`. This function is used
+internally and can be overridden if you wish to use a different method to detect
+arrays.
+
+
+#### lume.push(t, ...)
+Pushes all the given values to the end of the table `t` and returns the pushed
+values. Nil values are ignored.
+```lua
+local t = { 1, 2, 3 }
+lume.push(t, 4, 5) -- `t` becomes { 1, 2, 3, 4, 5 }
+```
+
+#### lume.remove(t, x)
+Removes the first instance of the value `x` if it exists in the table `t`.
+Returns `x`.
+```lua
+local t = { 1, 2, 3 }
+lume.remove(t, 2) -- `t` becomes { 1, 3 }
+```
+
+#### lume.clear(t)
+Nils all the values in the table `t`, this renders the table empty. Returns
+`t`.
+```lua
+local t = { 1, 2, 3 }
+lume.clear(t) -- `t` becomes {}
+```
+
+#### lume.extend(t, ...)
+Copies all the fields from the source tables to the table `t` and returns `t`.
+If a key exists in multiple tables the right-most table's value is used.
+```lua
+local t = { a = 1, b = 2 }
+lume.extend(t, { b = 4, c = 6 }) -- `t` becomes { a = 1, b = 4, c = 6 }
+```
+
+#### lume.shuffle(t)
+Returns a shuffled copy of the array `t`.
+
+#### lume.sort(t [, comp])
+Returns a copy of the array `t` with all its items sorted. If `comp` is a
+function it will be used to compare the items when sorting. If `comp` is a
+string it will be used as the key to sort the items by.
+```lua
+lume.sort({ 1, 4, 3, 2, 5 }) -- Returns { 1, 2, 3, 4, 5 }
+lume.sort({ {z=2}, {z=3}, {z=1} }, "z") -- Returns { {z=1}, {z=2}, {z=3} }
+lume.sort({ 1, 3, 2 }, function(a, b) return a > b end) -- Returns { 3, 2, 1 }
+```
+
+#### lume.array(...)
+Iterates the supplied iterator and returns an array filled with the values.
+```lua
+lume.array(string.gmatch("Hello world", "%a+")) -- Returns {"Hello", "world"}
+```
+
+#### lume.each(t, fn, ...)
+Iterates the table `t` and calls the function `fn` on each value followed by
+the supplied additional arguments; if `fn` is a string the method of that name
+is called for each value. The function returns `t` unmodified.
+```lua
+lume.each({1, 2, 3}, print) -- Prints "1", "2", "3" on separate lines
+lume.each({a, b, c}, "move", 10, 20) -- Does x:move(10, 20) on each value
+```
+
+#### lume.map(t, fn)
+Applies the function `fn` to each value in table `t` and returns a new table
+with the resulting values.
+```lua
+lume.map({1, 2, 3}, function(x) return x * 2 end) -- Returns {2, 4, 6}
+```
+
+#### lume.all(t [, fn])
+Returns true if all the values in `t` table are true. If a `fn` function is
+supplied it is called on each value, true is returned if all of the calls to
+`fn` return true.
+```lua
+lume.all({1, 2, 1}, function(x) return x == 1 end) -- Returns false
+```
+
+#### lume.any(t [, fn])
+Returns true if any of the values in `t` table are true. If a `fn` function is
+supplied it is called on each value, true is returned if any of the calls to
+`fn` return true.
+```lua
+lume.any({1, 2, 1}, function(x) return x == 1 end) -- Returns true
+```
+
+#### lume.reduce(t, fn [, first])
+Applies `fn` on two arguments cumulative to the items of the array `t`, from
+left to right, so as to reduce the array to a single value. If a `first` value
+is specified the accumulator is initialised to this, otherwise the first value
+in the array is used. If the array is empty and no `first` value is specified
+an error is raised.
+```lua
+lume.reduce({1, 2, 3}, function(a, b) return a + b end) -- Returns 6
+```
+
+#### lume.unique(t)
+Returns a copy of the `t` array with all the duplicate values removed.
+```lua
+lume.unique({2, 1, 2, "cat", "cat"}) -- Returns {1, 2, "cat"}
+```
+
+#### lume.filter(t, fn [, retainkeys])
+Calls `fn` on each value of `t` table. Returns a new table with only the values
+where `fn` returned true. If `retainkeys` is true the table is not treated as
+an array and retains its original keys.
+```lua
+lume.filter({1, 2, 3, 4}, function(x) return x % 2 == 0 end) -- Returns {2, 4}
+```
+
+#### lume.reject(t, fn [, retainkeys])
+The opposite of `lume.filter()`: Calls `fn` on each value of `t` table; returns
+a new table with only the values where `fn` returned false. If `retainkeys` is
+true the table is not treated as an array and retains its original keys.
+```lua
+lume.reject({1, 2, 3, 4}, function(x) return x % 2 == 0 end) -- Returns {1, 3}
+```
+
+#### lume.merge(...)
+Returns a new table with all the given tables merged together. If a key exists
+in multiple tables the right-most table's value is used.
+```lua
+lume.merge({a=1, b=2, c=3}, {c=8, d=9}) -- Returns {a=1, b=2, c=8, d=9}
+```
+
+#### lume.concat(...)
+Returns a new array consisting of all the given arrays concatenated into one.
+```lua
+lume.concat({1, 2}, {3, 4}, {5, 6}) -- Returns {1, 2, 3, 4, 5, 6}
+```
+
+#### lume.find(t, value)
+Returns the index/key of `value` in `t`. Returns `nil` if that value does not
+exist in the table.
+```lua
+lume.find({"a", "b", "c"}, "b") -- Returns 2
+```
+
+#### lume.match(t, fn)
+Returns the value and key of the value in table `t` which returns true when
+`fn` is called on it. Returns `nil` if no such value exists.
+```lua
+lume.match({1, 5, 8, 7}, function(x) return x % 2 == 0 end) -- Returns 8, 3
+```
+
+#### lume.count(t [, fn])
+Counts the number of values in the table `t`. If a `fn` function is supplied it
+is called on each value, the number of times it returns true is counted.
+```lua
+lume.count({a = 2, b = 3, c = 4, d = 5}) -- Returns 4
+lume.count({1, 2, 4, 6}, function(x) return x % 2 == 0 end) -- Returns 3
+```
+
+#### lume.slice(t [, i [, j]])
+Mimics the behaviour of Lua's `string.sub`, but operates on an array rather
+than a string. Creates and returns a new array of the given slice.
+```lua
+lume.slice({"a", "b", "c", "d", "e"}, 2, 4) -- Returns {"b", "c", "d"}
+```
+
+#### lume.first(t [, n])
+Returns the first element of an array or nil if the array is empty. If `n` is
+specificed an array of the first `n` elements is returned.
+```lua
+lume.first({"a", "b", "c"}) -- Returns "a"
+```
+
+#### lume.last(t [, n])
+Returns the last element of an array or nil if the array is empty. If `n` is
+specificed an array of the last `n` elements is returned.
+```lua
+lume.last({"a", "b", "c"}) -- Returns "c"
+```
+
+#### lume.invert(t)
+Returns a copy of the table where the keys have become the values and the
+values the keys.
+```lua
+lume.invert({a = "x", b = "y"}) -- returns {x = "a", y = "b"}
+```
+
+#### lume.pick(t, ...)
+Returns a copy of the table filtered to only contain values for the given keys.
+```lua
+lume.pick({ a = 1, b = 2, c = 3 }, "a", "c") -- Returns { a = 1, c = 3 }
+```
+
+#### lume.keys(t)
+Returns an array containing each key of the table.
+
+#### lume.clone(t)
+Returns a shallow copy of the table `t`.
+
+#### lume.fn(fn, ...)
+Creates a wrapper function around function `fn`, automatically inserting the
+arguments into `fn` which will persist every time the wrapper is called. Any
+arguments which are passed to the returned function will be inserted after the
+already existing arguments passed to `fn`.
+```lua
+local f = lume.fn(print, "Hello")
+f("world") -- Prints "Hello world"
+```
+
+#### lume.once(fn, ...)
+Returns a wrapper function to `fn` which takes the supplied arguments. The
+wrapper function will call `fn` on the first call and do nothing on any
+subsequent calls.
+```lua
+local f = lume.once(print, "Hello")
+f() -- Prints "Hello"
+f() -- Does nothing
+```
+
+#### lume.memoize(fn)
+Returns a wrapper function to `fn` where the results for any given set of
+arguments are cached. `lume.memoize()` is useful when used on functions with
+slow-running computations.
+```lua
+fib = lume.memoize(function(n) return n < 2 and n or fib(n-1) + fib(n-2) end)
+```
+
+#### lume.combine(...)
+Creates a wrapper function which calls each supplied argument in the order they
+were passed to `lume.combine()`; nil arguments are ignored. The wrapper
+function passes its own arguments to each of its wrapped functions when it is
+called.
+```lua
+local f = lume.combine(function(a, b) print(a + b) end,
+ function(a, b) print(a * b) end)
+f(3, 4) -- Prints "7" then "12" on a new line
+```
+
+#### lume.call(fn, ...)
+Calls the given function with the provided arguments and returns its values. If
+`fn` is `nil` then no action is performed and the function returns `nil`.
+```lua
+lume.call(print, "Hello world") -- Prints "Hello world"
+```
+
+#### lume.time(fn, ...)
+Inserts the arguments into function `fn` and calls it. Returns the time in
+seconds the function `fn` took to execute followed by `fn`'s returned values.
+```lua
+lume.time(function(x) return x end, "hello") -- Returns 0, "hello"
+```
+
+#### lume.lambda(str)
+Takes a string lambda and returns a function. `str` should be a list of
+comma-separated parameters, followed by `->`, followed by the expression which
+will be evaluated and returned.
+```lua
+local f = lume.lambda "x,y -> 2*x+y"
+f(10, 5) -- Returns 25
+```
+
+#### lume.serialize(x)
+Serializes the argument `x` into a string which can be loaded again using
+`lume.deserialize()`. Only booleans, numbers, tables and strings can be
+serialized. Circular references will result in an error; all nested tables are
+serialized as unique tables.
+```lua
+lume.serialize({a = "test", b = {1, 2, 3}, false})
+-- Returns "{[1]=false,["a"]="test",["b"]={[1]=1,[2]=2,[3]=3,},}"
+```
+
+#### lume.deserialize(str)
+Deserializes a string created by `lume.serialize()` and returns the resulting
+value. This function should not be run on an untrusted string.
+```lua
+lume.deserialize("{1, 2, 3}") -- Returns {1, 2, 3}
+```
+
+#### lume.split(str [, sep])
+Returns an array of the words in the string `str`. If `sep` is provided it is
+used as the delimiter, consecutive delimiters are not grouped together and will
+delimit empty strings.
+```lua
+lume.split("One two three") -- Returns {"One", "two", "three"}
+lume.split("a,b,,c", ",") -- Returns {"a", "b", "", "c"}
+```
+
+#### lume.trim(str [, chars])
+Trims the whitespace from the start and end of the string `str` and returns the
+new string. If a `chars` value is set the characters in `chars` are trimmed
+instead of whitespace.
+```lua
+lume.trim(" Hello ") -- Returns "Hello"
+```
+
+#### lume.wordwrap(str [, limit])
+Returns `str` wrapped to `limit` number of characters per line, by default
+`limit` is `72`. `limit` can also be a function which when passed a string,
+returns `true` if it is too long for a single line.
+```lua
+-- Returns "Hello world\nThis is a\nshort string"
+lume.wordwrap("Hello world. This is a short string", 14)
+```
+
+#### lume.format(str [, vars])
+Returns a formatted string. The values of keys in the table `vars` can be
+inserted into the string by using the form `"{key}"` in `str`; numerical keys
+can also be used.
+```lua
+lume.format("{b} hi {a}", {a = "mark", b = "Oh"}) -- Returns "Oh hi mark"
+lume.format("Hello {1}!", {"world"}) -- Returns "Hello world!"
+```
+
+#### lume.trace(...)
+Prints the current filename and line number followed by each argument separated
+by a space.
+```lua
+-- Assuming the file is called "example.lua" and the next line is 12:
+lume.trace("hello", 1234) -- Prints "example.lua:12: hello 1234"
+```
+
+#### lume.dostring(str)
+Executes the lua code inside `str`.
+```lua
+lume.dostring("print('Hello!')") -- Prints "Hello!"
+```
+
+#### lume.uuid()
+Generates a random UUID string; version 4 as specified in
+[RFC 4122](http://www.ietf.org/rfc/rfc4122.txt).
+
+#### lume.hotswap(modname)
+Reloads an already loaded module in place, allowing you to immediately see the
+effects of code changes without having to restart the program. `modname` should
+be the same string used when loading the module with require(). In the case of
+an error the global environment is restored and `nil` plus an error message is
+returned.
+```lua
+lume.hotswap("lume") -- Reloads the lume module
+assert(lume.hotswap("inexistant_module")) -- Raises an error
+```
+
+#### lume.ripairs(t)
+Performs the same function as `ipairs()` but iterates in reverse; this allows
+the removal of items from the table during iteration without any items being
+skipped.
+```lua
+-- Prints "3->c", "2->b" and "1->a" on separate lines
+for i, v in lume.ripairs({ "a", "b", "c" }) do
+ print(i .. "->" .. v)
+end
+```
+
+#### lume.color(str [, mul])
+Takes color string `str` and returns 4 values, one for each color channel (`r`,
+`g`, `b` and `a`). By default the returned values are between 0 and 1; the
+values are multiplied by the number `mul` if it is provided.
+```lua
+lume.color("#ff0000") -- Returns 1, 0, 0, 1
+lume.color("rgba(255, 0, 255, .5)") -- Returns 1, 0, 1, .5
+lume.color("#00ffff", 256) -- Returns 0, 256, 256, 256
+lume.color("rgb(255, 0, 0)", 256) -- Returns 256, 0, 0, 256
+```
+
+#### lume.chain(value)
+Returns a wrapped object which allows chaining of lume functions. The function
+result() should be called at the end of the chain to return the resulting
+value.
+```lua
+lume.chain({1, 2, 3, 4})
+ :filter(function(x) return x % 2 == 0 end)
+ :map(function(x) return -x end)
+ :result() -- Returns { -2, -4 }
+```
+The table returned by the `lume` module, when called, acts in the same manner
+as calling `lume.chain()`.
+```lua
+lume({1, 2, 3}):each(print) -- Prints 1, 2 then 3 on separate lines
+```
+
+## Iteratee functions
+Several lume functions allow a `table`, `string` or `nil` to be used in place
+of their iteratee function argument. The functions that provide this behaviour
+are: `map()`, `all()`, `any()`, `filter()`, `reject()`, `match()` and
+`count()`.
+
+If the argument is `nil` then each value will return itself.
+```lua
+lume.filter({ true, true, false, true }, nil) -- { true, true, true }
+```
+
+If the argument is a `string` then each value will be assumed to be a table,
+and will return the value of the key which matches the string.
+``` lua
+local t = {{ z = "cat" }, { z = "dog" }, { z = "owl" }}
+lume.map(t, "z") -- Returns { "cat", "dog", "owl" }
+```
+
+If the argument is a `table` then each value will return `true` or `false`,
+depending on whether the values at each of the table's keys match the
+collection's value's values.
+```lua
+local t = {
+ { age = 10, type = "cat" },
+ { age = 8, type = "dog" },
+ { age = 10, type = "owl" },
+}
+lume.count(t, { age = 10 }) -- returns 2
+```
+
+
+## License
+
+This library is free software; you can redistribute it and/or modify it under
+the terms of the MIT license. See [LICENSE](LICENSE) for details.
diff --git a/satelito/lib/lume/lume.lua b/satelito/lib/lume/lume.lua
@@ -0,0 +1,780 @@
+--
+-- lume
+--
+-- Copyright (c) 2020 rxi
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy of
+-- this software and associated documentation files (the "Software"), to deal in
+-- the Software without restriction, including without limitation the rights to
+-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+-- of the Software, and to permit persons to whom the Software is furnished to do
+-- so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in all
+-- copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-- SOFTWARE.
+--
+
+local lume = { _version = "2.3.0" }
+
+local pairs, ipairs = pairs, ipairs
+local type, assert, unpack = type, assert, unpack or table.unpack
+local tostring, tonumber = tostring, tonumber
+local math_floor = math.floor
+local math_ceil = math.ceil
+local math_atan2 = math.atan2 or math.atan
+local math_sqrt = math.sqrt
+local math_abs = math.abs
+
+local noop = function()
+end
+
+local identity = function(x)
+ return x
+end
+
+local patternescape = function(str)
+ return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
+end
+
+local absindex = function(len, i)
+ return i < 0 and (len + i + 1) or i
+end
+
+local iscallable = function(x)
+ if type(x) == "function" then return true end
+ local mt = getmetatable(x)
+ return mt and mt.__call ~= nil
+end
+
+local getiter = function(x)
+ if lume.isarray(x) then
+ return ipairs
+ elseif type(x) == "table" then
+ return pairs
+ end
+ error("expected table", 3)
+end
+
+local iteratee = function(x)
+ if x == nil then return identity end
+ if iscallable(x) then return x end
+ if type(x) == "table" then
+ return function(z)
+ for k, v in pairs(x) do
+ if z[k] ~= v then return false end
+ end
+ return true
+ end
+ end
+ return function(z) return z[x] end
+end
+
+
+
+function lume.clamp(x, min, max)
+ return x < min and min or (x > max and max or x)
+end
+
+
+function lume.round(x, increment)
+ if increment then return lume.round(x / increment) * increment end
+ return x >= 0 and math_floor(x + .5) or math_ceil(x - .5)
+end
+
+
+function lume.sign(x)
+ return x < 0 and -1 or 1
+end
+
+
+function lume.lerp(a, b, amount)
+ return a + (b - a) * lume.clamp(amount, 0, 1)
+end
+
+
+function lume.smooth(a, b, amount)
+ local t = lume.clamp(amount, 0, 1)
+ local m = t * t * (3 - 2 * t)
+ return a + (b - a) * m
+end
+
+
+function lume.pingpong(x)
+ return 1 - math_abs(1 - x % 2)
+end
+
+
+function lume.distance(x1, y1, x2, y2, squared)
+ local dx = x1 - x2
+ local dy = y1 - y2
+ local s = dx * dx + dy * dy
+ return squared and s or math_sqrt(s)
+end
+
+
+function lume.angle(x1, y1, x2, y2)
+ return math_atan2(y2 - y1, x2 - x1)
+end
+
+
+function lume.vector(angle, magnitude)
+ return math.cos(angle) * magnitude, math.sin(angle) * magnitude
+end
+
+
+function lume.random(a, b)
+ if not a then a, b = 0, 1 end
+ if not b then b = 0 end
+ return a + math.random() * (b - a)
+end
+
+
+function lume.randomchoice(t)
+ return t[math.random(#t)]
+end
+
+
+function lume.weightedchoice(t)
+ local sum = 0
+ for _, v in pairs(t) do
+ assert(v >= 0, "weight value less than zero")
+ sum = sum + v
+ end
+ assert(sum ~= 0, "all weights are zero")
+ local rnd = lume.random(sum)
+ for k, v in pairs(t) do
+ if rnd < v then return k end
+ rnd = rnd - v
+ end
+end
+
+
+function lume.isarray(x)
+ return type(x) == "table" and x[1] ~= nil
+end
+
+
+function lume.push(t, ...)
+ local n = select("#", ...)
+ for i = 1, n do
+ t[#t + 1] = select(i, ...)
+ end
+ return ...
+end
+
+
+function lume.remove(t, x)
+ local iter = getiter(t)
+ for i, v in iter(t) do
+ if v == x then
+ if lume.isarray(t) then
+ table.remove(t, i)
+ break
+ else
+ t[i] = nil
+ break
+ end
+ end
+ end
+ return x
+end
+
+
+function lume.clear(t)
+ local iter = getiter(t)
+ for k in iter(t) do
+ t[k] = nil
+ end
+ return t
+end
+
+
+function lume.extend(t, ...)
+ for i = 1, select("#", ...) do
+ local x = select(i, ...)
+ if x then
+ for k, v in pairs(x) do
+ t[k] = v
+ end
+ end
+ end
+ return t
+end
+
+
+function lume.shuffle(t)
+ local rtn = {}
+ for i = 1, #t do
+ local r = math.random(i)
+ if r ~= i then
+ rtn[i] = rtn[r]
+ end
+ rtn[r] = t[i]
+ end
+ return rtn
+end
+
+
+function lume.sort(t, comp)
+ local rtn = lume.clone(t)
+ if comp then
+ if type(comp) == "string" then
+ table.sort(rtn, function(a, b) return a[comp] < b[comp] end)
+ else
+ table.sort(rtn, comp)
+ end
+ else
+ table.sort(rtn)
+ end
+ return rtn
+end
+
+
+function lume.array(...)
+ local t = {}
+ for x in ... do t[#t + 1] = x end
+ return t
+end
+
+
+function lume.each(t, fn, ...)
+ local iter = getiter(t)
+ if type(fn) == "string" then
+ for _, v in iter(t) do v[fn](v, ...) end
+ else
+ for _, v in iter(t) do fn(v, ...) end
+ end
+ return t
+end
+
+
+function lume.map(t, fn)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ local rtn = {}
+ for k, v in iter(t) do rtn[k] = fn(v) end
+ return rtn
+end
+
+
+function lume.all(t, fn)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ for _, v in iter(t) do
+ if not fn(v) then return false end
+ end
+ return true
+end
+
+
+function lume.any(t, fn)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ for _, v in iter(t) do
+ if fn(v) then return true end
+ end
+ return false
+end
+
+
+function lume.reduce(t, fn, first)
+ local started = first ~= nil
+ local acc = first
+ local iter = getiter(t)
+ for _, v in iter(t) do
+ if started then
+ acc = fn(acc, v)
+ else
+ acc = v
+ started = true
+ end
+ end
+ assert(started, "reduce of an empty table with no first value")
+ return acc
+end
+
+
+function lume.unique(t)
+ local rtn = {}
+ for k in pairs(lume.invert(t)) do
+ rtn[#rtn + 1] = k
+ end
+ return rtn
+end
+
+
+function lume.filter(t, fn, retainkeys)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ local rtn = {}
+ if retainkeys then
+ for k, v in iter(t) do
+ if fn(v) then rtn[k] = v end
+ end
+ else
+ for _, v in iter(t) do
+ if fn(v) then rtn[#rtn + 1] = v end
+ end
+ end
+ return rtn
+end
+
+
+function lume.reject(t, fn, retainkeys)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ local rtn = {}
+ if retainkeys then
+ for k, v in iter(t) do
+ if not fn(v) then rtn[k] = v end
+ end
+ else
+ for _, v in iter(t) do
+ if not fn(v) then rtn[#rtn + 1] = v end
+ end
+ end
+ return rtn
+end
+
+
+function lume.merge(...)
+ local rtn = {}
+ for i = 1, select("#", ...) do
+ local t = select(i, ...)
+ local iter = getiter(t)
+ for k, v in iter(t) do
+ rtn[k] = v
+ end
+ end
+ return rtn
+end
+
+
+function lume.concat(...)
+ local rtn = {}
+ for i = 1, select("#", ...) do
+ local t = select(i, ...)
+ if t ~= nil then
+ local iter = getiter(t)
+ for _, v in iter(t) do
+ rtn[#rtn + 1] = v
+ end
+ end
+ end
+ return rtn
+end
+
+
+function lume.find(t, value)
+ local iter = getiter(t)
+ for k, v in iter(t) do
+ if v == value then return k end
+ end
+ return nil
+end
+
+
+function lume.match(t, fn)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ for k, v in iter(t) do
+ if fn(v) then return v, k end
+ end
+ return nil
+end
+
+
+function lume.count(t, fn)
+ local count = 0
+ local iter = getiter(t)
+ if fn then
+ fn = iteratee(fn)
+ for _, v in iter(t) do
+ if fn(v) then count = count + 1 end
+ end
+ else
+ if lume.isarray(t) then
+ return #t
+ end
+ for _ in iter(t) do count = count + 1 end
+ end
+ return count
+end
+
+
+function lume.slice(t, i, j)
+ i = i and absindex(#t, i) or 1
+ j = j and absindex(#t, j) or #t
+ local rtn = {}
+ for x = i < 1 and 1 or i, j > #t and #t or j do
+ rtn[#rtn + 1] = t[x]
+ end
+ return rtn
+end
+
+
+function lume.first(t, n)
+ if not n then return t[1] end
+ return lume.slice(t, 1, n)
+end
+
+
+function lume.last(t, n)
+ if not n then return t[#t] end
+ return lume.slice(t, -n, -1)
+end
+
+
+function lume.invert(t)
+ local rtn = {}
+ for k, v in pairs(t) do rtn[v] = k end
+ return rtn
+end
+
+
+function lume.pick(t, ...)
+ local rtn = {}
+ for i = 1, select("#", ...) do
+ local k = select(i, ...)
+ rtn[k] = t[k]
+ end
+ return rtn
+end
+
+
+function lume.keys(t)
+ local rtn = {}
+ local iter = getiter(t)
+ for k in iter(t) do rtn[#rtn + 1] = k end
+ return rtn
+end
+
+
+function lume.clone(t)
+ local rtn = {}
+ for k, v in pairs(t) do rtn[k] = v end
+ return rtn
+end
+
+
+function lume.fn(fn, ...)
+ assert(iscallable(fn), "expected a function as the first argument")
+ local args = { ... }
+ return function(...)
+ local a = lume.concat(args, { ... })
+ return fn(unpack(a))
+ end
+end
+
+
+function lume.once(fn, ...)
+ local f = lume.fn(fn, ...)
+ local done = false
+ return function(...)
+ if done then return end
+ done = true
+ return f(...)
+ end
+end
+
+
+local memoize_fnkey = {}
+local memoize_nil = {}
+
+function lume.memoize(fn)
+ local cache = {}
+ return function(...)
+ local c = cache
+ for i = 1, select("#", ...) do
+ local a = select(i, ...) or memoize_nil
+ c[a] = c[a] or {}
+ c = c[a]
+ end
+ c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)}
+ return unpack(c[memoize_fnkey])
+ end
+end
+
+
+function lume.combine(...)
+ local n = select('#', ...)
+ if n == 0 then return noop end
+ if n == 1 then
+ local fn = select(1, ...)
+ if not fn then return noop end
+ assert(iscallable(fn), "expected a function or nil")
+ return fn
+ end
+ local funcs = {}
+ for i = 1, n do
+ local fn = select(i, ...)
+ if fn ~= nil then
+ assert(iscallable(fn), "expected a function or nil")
+ funcs[#funcs + 1] = fn
+ end
+ end
+ return function(...)
+ for _, f in ipairs(funcs) do f(...) end
+ end
+end
+
+
+function lume.call(fn, ...)
+ if fn then
+ return fn(...)
+ end
+end
+
+
+function lume.time(fn, ...)
+ local start = os.clock()
+ local rtn = {fn(...)}
+ return (os.clock() - start), unpack(rtn)
+end
+
+
+local lambda_cache = {}
+
+function lume.lambda(str)
+ if not lambda_cache[str] then
+ local args, body = str:match([[^([%w,_ ]-)%->(.-)$]])
+ assert(args and body, "bad string lambda")
+ local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend"
+ lambda_cache[str] = lume.dostring(s)
+ end
+ return lambda_cache[str]
+end
+
+
+local serialize
+
+local serialize_map = {
+ [ "boolean" ] = tostring,
+ [ "nil" ] = tostring,
+ [ "string" ] = function(v) return string.format("%q", v) end,
+ [ "number" ] = function(v)
+ if v ~= v then return "0/0" -- nan
+ elseif v == 1 / 0 then return "1/0" -- inf
+ elseif v == -1 / 0 then return "-1/0" end -- -inf
+ return tostring(v)
+ end,
+ [ "table" ] = function(t, stk)
+ stk = stk or {}
+ if stk[t] then error("circular reference") end
+ local rtn = {}
+ stk[t] = true
+ for k, v in pairs(t) do
+ rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk)
+ end
+ stk[t] = nil
+ return "{" .. table.concat(rtn, ",") .. "}"
+ end
+}
+
+setmetatable(serialize_map, {
+ __index = function(_, k) error("unsupported serialize type: " .. k) end
+})
+
+serialize = function(x, stk)
+ return serialize_map[type(x)](x, stk)
+end
+
+function lume.serialize(x)
+ return serialize(x)
+end
+
+
+function lume.deserialize(str)
+ return lume.dostring("return " .. str)
+end
+
+
+function lume.split(str, sep)
+ if not sep then
+ return lume.array(str:gmatch("([%S]+)"))
+ else
+ assert(sep ~= "", "empty separator")
+ local psep = patternescape(sep)
+ return lume.array((str..sep):gmatch("(.-)("..psep..")"))
+ end
+end
+
+
+function lume.trim(str, chars)
+ if not chars then return str:match("^[%s]*(.-)[%s]*$") end
+ chars = patternescape(chars)
+ return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$")
+end
+
+
+function lume.wordwrap(str, limit)
+ limit = limit or 72
+ local check
+ if type(limit) == "number" then
+ check = function(s) return #s >= limit end
+ else
+ check = limit
+ end
+ local rtn = {}
+ local line = ""
+ for word, spaces in str:gmatch("(%S+)(%s*)") do
+ local s = line .. word
+ if check(s) then
+ table.insert(rtn, line .. "\n")
+ line = word
+ else
+ line = s
+ end
+ for c in spaces:gmatch(".") do
+ if c == "\n" then
+ table.insert(rtn, line .. "\n")
+ line = ""
+ else
+ line = line .. c
+ end
+ end
+ end
+ table.insert(rtn, line)
+ return table.concat(rtn)
+end
+
+
+function lume.format(str, vars)
+ if not vars then return str end
+ local f = function(x)
+ return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}")
+ end
+ return (str:gsub("{(.-)}", f))
+end
+
+
+function lume.trace(...)
+ local info = debug.getinfo(2, "Sl")
+ local t = { info.short_src .. ":" .. info.currentline .. ":" }
+ for i = 1, select("#", ...) do
+ local x = select(i, ...)
+ if type(x) == "number" then
+ x = string.format("%g", lume.round(x, .01))
+ end
+ t[#t + 1] = tostring(x)
+ end
+ print(table.concat(t, " "))
+end
+
+
+function lume.dostring(str)
+ return assert((loadstring or load)(str))()
+end
+
+
+function lume.uuid()
+ local fn = function(x)
+ local r = math.random(16) - 1
+ r = (x == "x") and (r + 1) or (r % 4) + 9
+ return ("0123456789abcdef"):sub(r, r)
+ end
+ return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
+end
+
+
+function lume.hotswap(modname)
+ local oldglobal = lume.clone(_G)
+ local updated = {}
+ local function update(old, new)
+ if updated[old] then return end
+ updated[old] = true
+ local oldmt, newmt = getmetatable(old), getmetatable(new)
+ if oldmt and newmt then update(oldmt, newmt) end
+ for k, v in pairs(new) do
+ if type(v) == "table" then update(old[k], v) else old[k] = v end
+ end
+ end
+ local err = nil
+ local function onerror(e)
+ for k in pairs(_G) do _G[k] = oldglobal[k] end
+ err = lume.trim(e)
+ end
+ local ok, oldmod = pcall(require, modname)
+ oldmod = ok and oldmod or nil
+ xpcall(function()
+ package.loaded[modname] = nil
+ local newmod = require(modname)
+ if type(oldmod) == "table" then update(oldmod, newmod) end
+ for k, v in pairs(oldglobal) do
+ if v ~= _G[k] and type(v) == "table" then
+ update(v, _G[k])
+ _G[k] = v
+ end
+ end
+ end, onerror)
+ package.loaded[modname] = oldmod
+ if err then return nil, err end
+ return oldmod
+end
+
+
+local ripairs_iter = function(t, i)
+ i = i - 1
+ local v = t[i]
+ if v ~= nil then
+ return i, v
+ end
+end
+
+function lume.ripairs(t)
+ return ripairs_iter, t, (#t + 1)
+end
+
+
+function lume.color(str, mul)
+ mul = mul or 1
+ local r, g, b, a
+ r, g, b = str:match("#(%x%x)(%x%x)(%x%x)")
+ if r then
+ r = tonumber(r, 16) / 0xff
+ g = tonumber(g, 16) / 0xff
+ b = tonumber(b, 16) / 0xff
+ a = 1
+ elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
+ local f = str:gmatch("[%d.]+")
+ r = (f() or 0) / 0xff
+ g = (f() or 0) / 0xff
+ b = (f() or 0) / 0xff
+ a = f() or 1
+ else
+ error(("bad color string '%s'"):format(str))
+ end
+ return r * mul, g * mul, b * mul, a * mul
+end
+
+
+local chain_mt = {}
+chain_mt.__index = lume.map(lume.filter(lume, iscallable, true),
+ function(fn)
+ return function(self, ...)
+ self._value = fn(self._value, ...)
+ return self
+ end
+ end)
+chain_mt.__index.result = function(x) return x._value end
+
+function lume.chain(value)
+ return setmetatable({ _value = value }, chain_mt)
+end
+
+setmetatable(lume, {
+ __call = function(_, ...)
+ return lume.chain(...)
+ end
+})
+
+
+return lume
diff --git a/satelito/lib/lume/test/test.lua b/satelito/lib/lume/test/test.lua
@@ -0,0 +1,624 @@
+local tester = require "util.tester"
+
+package.path = "../?.lua;" .. package.path
+
+local lume = require "lume"
+
+local tests = {}
+local testeq = tester.test.equal
+
+-- lume.clamp
+tests["lume.clamp"] = function()
+ testeq( lume.clamp(8, 5, 10), 8 )
+ testeq( lume.clamp(12, 5, 10), 10 )
+ testeq( lume.clamp(-1, 5, 10), 5 )
+ testeq( lume.clamp(-1, -10, 10), -1 )
+ testeq( lume.clamp(-100, -10, 10), -10 )
+ testeq( lume.clamp(13, 8, 8), 8 )
+ testeq( lume.clamp(3, 8, 8), 8 )
+end
+
+-- lume.round
+tests["lume.round"] = function()
+ testeq( lume.round(.5), 1 )
+ testeq( lume.round(-.5), -1 )
+ testeq( lume.round(2.4), 2 )
+ testeq( lume.round(123, 10), 120 )
+ testeq( lume.round(129, 64), 128 )
+ testeq( lume.round(-123.45, .1), -123.5 )
+ testeq( lume.round(0), 0 )
+end
+
+-- lume.sign
+tests["lume.sign"] = function()
+ testeq( lume.sign(-10), -1 )
+ testeq( lume.sign(10), 1 )
+ testeq( lume.sign(0), 1 )
+end
+
+-- lume.lerp
+tests["lume.lerp"] = function()
+ testeq( lume.lerp(100, 200, .5), 150 )
+ testeq( lume.lerp(100, 200, .25), 125 )
+ testeq( lume.lerp(100, 200, 2), 200 )
+ testeq( lume.lerp(100, 200, -2), 100 )
+end
+
+-- lume.smooth
+tests["lume.smooth"] = function()
+ testeq( lume.smooth(100, 200, .5), 150 )
+ testeq( lume.smooth(100, 200, 0), 100 )
+ testeq( lume.smooth(100, 200, 1), 200 )
+ testeq( lume.smooth(100, 200, 2), 200 )
+ testeq( lume.smooth(100, 200, -2), 100 )
+end
+
+-- lume.pingpong
+tests["lume.pingpong"] = function()
+ testeq( lume.pingpong(0), 0 )
+ testeq( lume.pingpong(1.5), .5 )
+ testeq( lume.pingpong(-.2), .2 )
+ testeq( lume.pingpong(-1.6), .4 )
+ testeq( lume.pingpong(1.8), .2 )
+end
+
+-- lume.distance
+tests["lume.distance"] = function()
+ testeq( lume.distance(15, 20, 15, 20), 0 )
+ testeq( lume.distance(13, 44, 156, 232), 236.205419074 )
+ testeq( lume.distance(-23, 66, -232, 123), 216.633330769 )
+ local x = lume.distance(13, 15, -2, 81)
+ testeq( lume.distance(13, 15, -2, 81, true), x * x )
+end
+
+-- lume.angle
+tests["lume.angle"] = function()
+ testeq( lume.angle(10, 10, 10, 10), math.rad(0) )
+ testeq( lume.angle(10, 10, 20, 10), math.rad(0) )
+ testeq( lume.angle(10, 10, 5, 10), math.rad(180) )
+ testeq( lume.angle(10, 10, 20, 20), math.rad(45) )
+ testeq( lume.angle(10, 10, 10, 30), math.rad(90) )
+end
+
+-- lume.vector
+tests["lume.vector"] = function()
+ local function cmp(a, b) return math.abs(a - b) < 10e-6 end
+ local x, y
+ x, y = lume.vector(0, 10)
+ testeq( cmp(x, 10) and cmp(y, 0), true )
+ x, y = lume.vector(math.pi, 100)
+ testeq( cmp(x, -100) and cmp(y, 0), true )
+ x, y = lume.vector(math.pi * 0.25, 100)
+ testeq( cmp(x, 70.71067811865476) and cmp(y, 70.71067811865476), true )
+end
+
+-- lume.random
+tests["lume.random"] = function()
+ testeq( type(lume.random()), "number" )
+ testeq( type(lume.random(1)), "number" )
+ testeq( type(lume.random(1, 2)), "number" )
+end
+
+-- lume.randomchoice
+tests["lume.randomchoice"] = function()
+ local t = {}
+ for i = 0, 1000 do
+ t[lume.randomchoice({"a", "b", "c", "d"})] = true
+ end
+ testeq( t.a and t.b and t.c and t.d, true )
+ testeq( lume.randomchoice({true}), true )
+end
+
+-- lume.weightedchoice
+tests["lume.weightedchoice"] = function()
+ testeq( lume.weightedchoice( {a = 1} ), "a" )
+ testeq( lume.weightedchoice( {a = 0, b = 1} ), "b" )
+ tester.test.error( lume.weightedchoice, {} )
+ tester.test.error( lume.weightedchoice, { a = 0, b = 0 } )
+ tester.test.error( lume.weightedchoice, { a = 1, b = -1 } )
+end
+
+-- lume.push
+tests["lume.push"] = function()
+ local t = { 1, 2 }
+ lume.push(t, 3, 4)
+ testeq(t, { 1, 2, 3, 4 })
+ lume.push(t, 5, nil, 6, nil, 7)
+ testeq(t, { 1, 2, 3, 4, 5, 6, 7 })
+ lume.push(t)
+ testeq(t, { 1, 2, 3, 4, 5, 6, 7 })
+ local x, y = lume.push(t, 123, 456)
+ testeq(x, 123)
+ testeq(y, 456)
+end
+
+-- lume.remove
+tests["lume.remove"] = function()
+ local t = { 1, 2, 3, 4, 5 }
+ lume.remove(t, 3)
+ testeq(t, { 1, 2, 4, 5 })
+ lume.remove(t, 1)
+ testeq(t, { 2, 4, 5 })
+ lume.remove(t, 5)
+ testeq(t, { 2, 4 })
+ local x = lume.remove(t, 123)
+ testeq(x, 123)
+end
+
+-- lume.clear
+tests["lume.clear"] = function()
+ local t = { 1, 2, 3 }
+ lume.clear(t)
+ testeq(t, {})
+ local m = { a = 1, b = 2, c = 3 }
+ lume.clear(m)
+ testeq(m, {})
+ testeq( lume.clear(t) == t, true )
+end
+
+-- lume.extend
+tests["lume.extend"] = function()
+ local t = { a = 10, b = 20, c = 30 }
+ testeq( lume.extend(t) == t, true )
+ lume.extend(t, { d = 40 }, { e = 50 })
+ testeq( t, { a = 10, b = 20, c = 30, d = 40, e = 50 } )
+ lume.extend(t, { a = "cat", b = "dog" }, { b = "owl", c = "fox" })
+ testeq( t, { a = "cat", b = "owl", c = "fox", d = 40, e = 50 } )
+end
+
+-- lume.shuffle
+tests["lume.shuffle"] = function()
+ local t = {1, 2, 3, 4, 5}
+ t = lume.shuffle(t)
+ table.sort(t)
+ testeq( t, {1, 2, 3, 4, 5} )
+ testeq( lume.shuffle({}), {} )
+end
+
+-- lume.sort
+tests["lume.sort"] = function()
+ local t = { 1, 5, 2, 4, 3 }
+ local fn = function(a, b) return a > b end
+ testeq( t == lume.sort(t), false )
+ testeq( lume.sort(t), { 1, 2, 3, 4, 5 } )
+ testeq( lume.sort(t, fn), { 5, 4, 3, 2, 1 } )
+ testeq( t, { 1, 5, 2, 4, 3 } )
+ local t = { { id = 2 }, { id = 3 }, { id = 1 } }
+ testeq( lume.sort(t, "id"), { { id = 1 }, { id = 2 }, { id = 3 } })
+end
+
+-- lume.array
+tests["lume.array"] = function()
+ local t = lume.array(pairs({a=0, b=0, c=0}))
+ table.sort(t)
+ testeq( t, {"a", "b", "c"} )
+ testeq( lume.array(ipairs({0, 0, 0})), {1, 2, 3} )
+end
+
+-- lume.each
+tests["lume.each"] = function()
+ local acc = 1
+ lume.each({1, 2, 3}, function(x) acc = acc + x end)
+ testeq( acc, 7 )
+
+ local acc = 1
+ local f = function(o, x) acc = acc + x end
+ local f2 = function() end
+ local t = {a = {f = f}, b = {f = f}, c = {f = f2}}
+ lume.each(t, "f", 10)
+ testeq( acc, 21 )
+end
+
+-- lume.map
+tests["lume.map"] = function()
+ testeq( lume.map({1, 2, 3}, function(x) return x * 2 end), {2, 4, 6} )
+ testeq( lume.map({a=2,b=3}, function(x) return x * 2 end), {a=4,b=6} )
+ local t = {{ id = 10 }, { id = 20 }, { id = 30 }}
+ testeq( lume.map(t, "id"), { 10, 20, 30 })
+end
+
+-- lume.all
+tests["lume.all"] = function()
+ testeq( lume.all({true, true, false, true}), false )
+ testeq( lume.all({true, true, true, true}), true )
+ testeq( lume.all({2, 3, 4, 5}, function(x) return x % 2 == 0 end), false )
+ testeq( lume.all({2, 4, 6, 8}, function(x) return x % 2 == 0 end), true )
+ testeq( lume.all({{ x = 1 }, {}, { x = 3 }}, "x"), false )
+ testeq( lume.all({{ x = 1 }, { x = 2 }, { x = 3 }}, "x"), true )
+ testeq( lume.all({{ x = 1 }, { x = 2 }, { x = 3 }}, { x = 2 }), false )
+ testeq( lume.all({{ x = 2 }, { x = 2 }, { x = 2 }}, { x = 2 }), true )
+end
+
+-- lume.any
+tests["lume.any"] = function()
+ testeq( lume.any({true, true, false, true}), true )
+ testeq( lume.any({false, false, false}), false )
+ testeq( lume.any({2, 3, 4, 5}, function(x) return x % 2 == 0 end), true )
+ testeq( lume.any({1, 3, 5, 7}, function(x) return x % 2 == 0 end), false )
+ local t = {{ id = 10 }, { id = 20 }, { id = 30 }}
+ testeq( lume.any(t, { id = 10 }), true )
+ testeq( lume.any(t, { id = 40 }), false )
+end
+
+-- lume.reduce
+tests["lume.reduce"] = function()
+ local concat = function(a, b) return a .. b end
+ local add = function(a, b) return a + b end
+ local any = function(a, b) return a or b end
+ testeq( lume.reduce({"cat", "dog"}, concat, ""), "catdog" )
+ testeq( lume.reduce({"cat", "dog"}, concat, "pig"), "pigcatdog" )
+ testeq( lume.reduce({"me", "ow"}, concat), "meow" )
+ testeq( lume.reduce({1, 2, 3, 4}, add), 10 )
+ testeq( lume.reduce({1, 2, 3, 4}, add, 5), 15 )
+ testeq( lume.reduce({1}, add), 1 )
+ testeq( lume.reduce({}, concat, "potato"), "potato" )
+ testeq( lume.reduce({a=1, b=2}, add, 5), 8 )
+ testeq( lume.reduce({a=1, b=2}, add), 3 )
+ testeq( lume.reduce({false, false, false}, any), false )
+ testeq( lume.reduce({false, true, false}, any), true )
+ tester.test.error(lume.reduce, {}, add)
+end
+
+-- lume.unique
+tests["lume.unique"] = function()
+ testeq( lume.unique({}), {} )
+ local t = lume.unique({1, 2, 3, 2, 5, 6, 6})
+ table.sort(t)
+ testeq( t, {1, 2, 3, 5, 6} )
+ local t = lume.unique({"a", "b", "c", "b", "d"})
+ table.sort(t)
+ testeq( t, {"a", "b", "c", "d"} )
+end
+
+-- lume.filter
+tests["lume.filter"] = function()
+ local t = lume.filter({1, 2, 3, 4, 5}, function(x) return x % 2 == 0 end )
+ testeq( t, {2, 4} )
+ local t = lume.filter({a=1, b=2, c=3}, function(x) return x == 2 end, true)
+ testeq( t, {b=2} )
+ local t = lume.filter({{ x=1, y=1 }, { x=2, y=2 }, { x=1, y=3 }}, { x = 1 })
+ testeq( t, {{ x=1, y=1 }, {x=1, y=3}} )
+end
+
+-- lume.reject
+tests["lume.reject"] = function()
+ local t = lume.reject({1, 2, 3, 4, 5}, function(x) return x % 2 == 0 end )
+ testeq( t, {1, 3, 5} )
+ local t = lume.reject({a=1, b=2, c=3}, function(x) return x == 2 end, true)
+ testeq( t, {a=1, c=3} )
+ local t = lume.reject({{ x=1, y=1 }, { x=2, y=2 }, { x=1, y=3 }}, { x = 1 })
+ testeq( t, {{ x=2, y=2 }} )
+end
+
+-- lume.merge
+tests["lume.merge"] = function()
+ testeq( lume.merge(), {} )
+ testeq( lume.merge({x=1, y=2}), {x=1, y=2} )
+ testeq( lume.merge({a=1, b=2}, {b=3, c=4}), {a=1, b=3, c=4} )
+end
+
+-- lume.concat
+tests["lume.concat"] = function()
+ testeq( lume.concat(nil), {} )
+ testeq( lume.concat({1, 2, 3}), {1, 2, 3} )
+ testeq( lume.concat({1, 2, 3}, {4, 5, 6}), {1, 2, 3, 4, 5, 6} )
+ testeq( lume.concat({1, 2, 3}, {4, 5, 6}, nil, {7}), {1, 2, 3, 4, 5, 6, 7} )
+end
+
+-- lume.find
+tests["lume.find"] = function()
+ testeq( lume.find({"a", "b", "c"}, "b"), 2 )
+ testeq( lume.find({"a", "b", "c"}, "c"), 3 )
+ testeq( lume.find({a=1, b=5, c=7}, 5), "b" )
+end
+
+-- lume.match
+tests["lume.match"] = function()
+ local t = { "a", "b", "c", "d" }
+ local t2 = { a = 1, b = 2, c = 3, d = 4 }
+ local t3 = { {x=1, y=2}, {x=3, y=4}, {x=5, y=6} }
+ local v, k = lume.match(t, function(x) return x > "c" end)
+ testeq( v, "d" )
+ testeq( k, 4 )
+ local v, k = lume.match(t, function(x) return x < "b" end)
+ testeq( v, "a" )
+ testeq( k, 1 )
+ local v, k = lume.match(t2, function(x) return x < 2 end)
+ testeq( v, 1 )
+ testeq( k, "a" )
+ local v, k = lume.match(t2, function(x) return x > 5 end)
+ testeq( v, nil )
+ testeq( k, nil )
+ local v, k = lume.match(t3, { x = 3, y = 4 })
+ testeq( k, 2 )
+end
+
+-- lume.count
+tests["lume.count"] = function()
+ local t = { a = 1, b = 2, c = 5, [13] = 22, z = 8 }
+ testeq( lume.count(t), 5 )
+ testeq( lume.count(t, function(x) return x % 2 == 0 end ), 3 )
+ local a = { 5, 6, 7, 8, 9 }
+ testeq( lume.count(a), #a )
+ local t = { { n = 20 }, { n = 30 }, { n = 40 }, { n = 20 } }
+ testeq( lume.count(t, { n = 20 }), 2 )
+ testeq( lume.count(t, { n = 30 }), 1 )
+ testeq( lume.count(t, { n = 50 }), 0 )
+end
+
+-- lume.slice
+tests["lume.slice"] = function()
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 2, 4), {"b", "c", "d"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 2, -2), {"b", "c", "d"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 3, -1), {"c", "d", "e"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 3), {"c", "d", "e"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 4), {"d", "e"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 1, 1), {"a"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 2, 1), {} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, -3, -2), {"c", "d"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, -3, 1), {} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 0, 1), {"a"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 0, 0), {} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, -3), {"c", "d", "e"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, -3, 900), {"c", "d", "e"} )
+end
+
+-- lume.first
+tests["lume.first"] = function()
+ local t = { "a", "b", "c", "d", "e" }
+ testeq( lume.first(t), "a" )
+ testeq( lume.first(t, 1), { "a" } )
+ testeq( lume.first(t, 2), { "a", "b" } )
+end
+
+-- lume.last
+tests["lume.last"] = function()
+ local t = { "a", "b", "c", "d", "e" }
+ testeq( lume.last(t), "e" )
+ testeq( lume.last(t, 1), { "e" } )
+ testeq( lume.last(t, 2), { "d", "e" } )
+end
+
+-- lume.invert
+tests["lume.invert"] = function()
+ testeq( lume.invert({}), {} )
+ testeq( lume.invert{a = "x", b = "y"}, {x = "a", y = "b"} )
+ testeq( lume.invert{a = 1, b = 2}, {"a", "b"} )
+ testeq( lume.invert(lume.invert{a = 1, b = 2}), {a = 1, b = 2} )
+end
+
+-- lume.pick
+tests["lume.pick"] = function()
+ local t = { cat = 10, dog = 20, fox = 30, owl = 40 }
+ testeq( lume.pick(t, "cat", "dog"), { cat = 10, dog = 20 } )
+ testeq( lume.pick(t, "fox", "owl"), { fox = 30, owl = 40 } )
+ testeq( lume.pick(t, "owl"), { owl = 40 } )
+ testeq( lume.pick(t), {} )
+end
+
+-- lume.keys
+tests["lume.keys"] = function()
+ testeq( lume.keys({}), {} )
+ local t = lume.keys({ aaa = 1, bbb = 2, ccc = 3 })
+ table.sort(t)
+ testeq( t, {"aaa", "bbb", "ccc"} )
+ local t = lume.keys({ "x", "x", "x" })
+ testeq( t, {1, 2, 3} )
+end
+
+-- lume.clone
+tests["lume.clone"] = function()
+ local t = {6, 7, 4, 5}
+ testeq( lume.clone(t) ~= t, true )
+ testeq( lume.clone(t), {6, 7, 4, 5} )
+ testeq( lume.clone({x=2, y="a"}), {x=2, y="a"} )
+end
+
+-- lume.fn
+tests["lume.fn"] = function()
+ local f = lume.fn(function(a, b) return a + b end, 10)
+ testeq( f(5), 15 )
+ tester.test.error( lume.fn, 123 )
+end
+
+-- lume.once
+tests["lume.once"] = function()
+ local f = lume.once(function(a, b) return a + b end, 10)
+ testeq( f(5), 15 )
+ testeq( f(5), nil )
+ tester.test.error( lume.once, 123 )
+end
+
+-- lume.memoize
+tests["lume.memoize"] = function()
+ local f = lume.memoize(
+ function(a, b, c)
+ return tostring(a) .. tostring(b) .. tostring(c)
+ end)
+ testeq( f("hello", nil, 15), "hellonil15" )
+ testeq( f("hello", nil, 15), "hellonil15" )
+ testeq( f(), "nilnilnil" )
+ testeq( f(), "nilnilnil" )
+ local f2 = lume.memoize(function() end)
+ testeq( f2(), nil )
+end
+
+-- lume.combine
+tests["lume.combine"] = function()
+ local acc = 0
+ local a = function(x, y) acc = acc + x + y end
+ local b = function(x, y) acc = acc + x * y end
+ local fn = lume.combine(a, b)
+ fn(10, 20)
+ testeq( acc, 230 )
+ acc = 0
+ fn = lume.combine(nil, a, nil, b, nil)
+ fn(10, 20)
+ testeq( acc, 230 )
+ local x = false
+ fn = lume.combine(function() x = true end)
+ fn()
+ testeq( x, true )
+ testeq( type(lume.combine(nil)), "function" )
+ testeq( type(lume.combine()), "function" )
+end
+
+-- lume.call
+tests["lume.call"] = function()
+ local add = function(a, b) return a + b end
+ testeq( lume.call(), nil )
+ testeq( lume.call(nil, 1, 2, 3), nil )
+ testeq( lume.call(add, 1, 2), 3 )
+end
+
+-- lume.time
+tests["lume.time"] = function()
+ local t, a, b, c = lume.time(function(x) return 50, 60, x end, 70)
+ testeq( type(t), "number" )
+ testeq( {a, b, c}, {50, 60, 70} )
+end
+
+-- lume.lambda
+tests["lume.lambda"] = function()
+ testeq( lume.lambda "x->x*x"(10), 100 )
+ testeq( lume.lambda "x->x*x"(20), 400 )
+ testeq( lume.lambda "x,y -> 2*x+y"(10,5), 25 )
+ testeq( lume.lambda "a, b -> a / b"(1, 2), .5 )
+ testeq( lume.lambda "a -> 'hi->' .. a"("doggy"), "hi->doggy" )
+ testeq( lume.lambda "A1,_->A1.._"("te","st"), "test" )
+ testeq( lume.lambda "->"(1,2,3), nil )
+ tester.test.error( lume.lambda, "abc" )
+ tester.test.error( lume.lambda, "" )
+ tester.test.error( lume.lambda, "a,b->a->b" )
+ tester.test.error( lume.lambda, "(a),b->a+b" )
+end
+
+-- lume.serialize / lume.deserialize
+tests["lume.serialize, lume.deserialize"] = function()
+ local t = { 1, 2, 3, 4, true, false, "cat", "dog", {1, 2, 3} }
+ local s = lume.serialize(t)
+ testeq( lume.deserialize(s), t )
+ testeq( lume.deserialize(lume.serialize(math.huge)), math.huge )
+ testeq( lume.deserialize(lume.serialize(-math.huge)), -math.huge )
+ local x = lume.deserialize(lume.serialize(0 / 0)) -- nan
+ testeq( x ~= x, true )
+end
+
+-- lume.split
+tests["lume.split"] = function()
+ testeq( lume.split("cat dog pig"), {"cat", "dog", "pig"} )
+ testeq( lume.split("cat,dog,pig", ","), {"cat", "dog", "pig"} )
+ testeq( lume.split("cat,dog;pig", ";"), {"cat,dog", "pig"} )
+ testeq( lume.split("cat,dog,,pig", ","), {"cat", "dog", "", "pig"} )
+ testeq( lume.split(";;;cat;", ";"), {"", "", "", "cat", ""} )
+ testeq( lume.split("cat.dog", "."), {"cat", "dog"} )
+ testeq( lume.split("cat%dog", "%"), {"cat", "dog"} )
+ testeq( lume.split("1<>2<>3", "<>"), {"1", "2", "3"} )
+ tester.test.error( lume.split, "abc", "" )
+end
+
+-- lume.trim
+tests["lume.trim"] = function()
+ testeq( lume.trim(" hello world "), "hello world" )
+ testeq( lume.trim("-=-hello-world===", "-="), "hello-world" )
+ testeq( lume.trim("***hello world*-*", "*"), "hello world*-" )
+ testeq( lume.trim("...hello world.", "."), "hello world" )
+ testeq( lume.trim("^.hello world]^", "^.]"), "hello world" )
+end
+
+-- lume.wordwrap
+tests["lume.wordwrap"] = function()
+ local str = "A small string with some words and then some more words"
+ local b = "A small string with \nsome words and then \nsome more words"
+ local fn = function(str) return #str >= 20 end
+ testeq( lume.wordwrap(str), str )
+ testeq( lume.wordwrap(str, 20), b )
+ testeq( lume.wordwrap(str, fn), b )
+end
+
+-- lume.format
+tests["lume.format"] = function()
+ local str = lume.format("a {a} in a {b}", {a = "mouse", b = "house"})
+ testeq( str, "a mouse in a house" )
+ testeq( lume.format("number {num}", {num = 13}), "number 13" )
+ testeq( lume.format("{missing} {keys}", {}), "{missing} {keys}" )
+ testeq( lume.format("A {missing} table"), "A {missing} table" )
+ testeq( lume.format("{1} idx {2}", {"an", "test"}), "an idx test" )
+ testeq( lume.format("bad idx {-1}", {"x"}), "bad idx {-1}" )
+ testeq( lume.format("empty {}", {"idx"}), "empty {}" )
+end
+
+-- lume.trace
+tests["lume.trace"] = function()
+ local oldprint = print
+ local file, line, msg
+ print = function(x)
+ file, line, msg = x:match("(.-):(.-): (.*)")
+ end
+ lume.trace("Hi world", 123.456, 1, nil)
+ print = oldprint
+ testeq( file:match(".lua$"), ".lua" )
+ testeq( tonumber(line) ~= nil, true )
+ testeq( msg, "Hi world 123.46 1 nil" )
+end
+
+-- lume.dostring
+tests["lume.dostring"] = function()
+ testeq( lume.dostring([[return "hello!"]]), "hello!" )
+ testeq( lume.dostring([[return 12345]]), 12345 )
+end
+
+-- lume.uuid
+tests["lume.uuid"] = function()
+ testeq( type(lume.uuid()), "string" )
+ testeq( #lume.uuid(), 36 )
+end
+
+-- lume.hotswap
+tests["lume.hotswap"] = function()
+ local ok, err = lume.hotswap("bad_module_name")
+ testeq( ok, nil )
+ testeq( type(err), "string" )
+end
+
+-- lume.ripairs
+tests["lume.ripairs"] = function()
+ local t = { "a", "b", false, "c" }
+ local r = {}
+ for i, v in lume.ripairs(t) do
+ table.insert(r, { i, v })
+ end
+ testeq( r, { { 4, "c" }, { 3, false }, { 2, "b" }, { 1, "a" } })
+ tester.test.error(lume.ripairs, nil)
+end
+
+-- lume.color
+tests["lume.color"] = function()
+ testeq({ lume.color("#ff0000") }, { 1, 0, 0, 1 } )
+ testeq({ lume.color("#00ff00") }, { 0, 1, 0, 1 } )
+ testeq({ lume.color("#0000ff") }, { 0, 0, 1, 1 } )
+ testeq({ lume.color("rgb( 255, 255, 255 )") }, { 1, 1, 1, 1 } )
+ testeq({ lume.color("rgb (0, 0, 0)") }, { 0, 0, 0, 1 } )
+ testeq({ lume.color("rgba(255, 255, 255, .5)") }, { 1, 1, 1, .5 } )
+ testeq({ lume.color("#ffffff", 2) }, { 2, 2, 2, 2 } )
+ testeq({ lume.color("rgba(255, 255, 255, 1)", 3) }, { 3, 3, 3, 3 } )
+ tester.test.error(lume.color, "#ff00f")
+ tester.test.error(lume.color, "#xyzxyz")
+ tester.test.error(lume.color, "rgba(hello)")
+ tester.test.error(lume.color, "rgba()")
+ tester.test.error(lume.color, "rgba(1, 1, 1, 1")
+end
+
+-- lume.chain
+tests["lume.chain"] = function()
+ local t = lume.chain({1, 2}):map(function(x) return x * 2 end):result()
+ testeq( t, { 2, 4 } )
+ testeq( lume.chain(10):result(), 10 )
+ local t = lume({1, 2}):map(function(x) return x * 2 end):result()
+ testeq( t, { 2, 4 } )
+end
+
+
+tester.dotests(tests)
+tester.test.global()
+tester.printresults()
diff --git a/satelito/lib/lume/test/util/tester.lua b/satelito/lib/lume/test/util/tester.lua
@@ -0,0 +1,166 @@
+
+
+local tester = {
+ test = {},
+ linecache = {},
+ globals = {},
+ passcount = 0,
+ failcount = 0
+}
+
+
+local function isequal(a, b)
+ if type(a) ~= type(b) then return nil end
+ local t = {}
+ function t.table(a, b)
+ for k, v in pairs(a) do if not isequal(b[k], v) then return nil end end
+ for k, v in pairs(b) do if not isequal(a[k], v) then return nil end end
+ return true
+ end
+ function t.number(a, b) return math.abs(a - b) < 10e-9 end
+ return t[type(a)] and t[type(a)](a, b) or (a == b)
+end
+
+
+local function stringify(x)
+ if type(x) == "number" then return string.format("%.2f", x) end
+ return string.format("%q", tostring(x))
+end
+
+
+local function getline(file, line)
+ if not tester.linecache[file] then
+ local t = {}
+ for line in io.lines(file) do
+ t[#t + 1] = line
+ end
+ tester.linecache[file] = t
+ end
+ return tester.linecache[file][line]
+end
+
+
+local function truncate(str, max)
+ max = max or 72
+ if #str > max then
+ return str:sub(1, max - 3) .. "..."
+ end
+ return str
+end
+
+
+local function has(t, value)
+ for k, v in pairs(t) do
+ if v == value then return true end
+ end
+ return false
+end
+
+
+local function makelogstr(passed, file, line)
+ local t = {}
+ t[#t + 1] = passed and "[\27[32mPASS\27[0m]" or "[\27[31mFAIL\27[0m]"
+ t[#t + 1] = file .. ":" .. line .. ":"
+ t[#t + 1] = getline(file, line) :gsub(" %s+", " ") :gsub("^ *", "")
+ return truncate(table.concat(t, " "))
+end
+
+
+local function dopass(file, line)
+ print(makelogstr(true, file, line))
+ tester.passcount = tester.passcount + 1
+end
+
+
+local function dofail(file, line)
+ print(makelogstr(false, file, line))
+ tester.failcount = tester.failcount + 1
+end
+
+
+local function printfailmsg(str)
+ print(string.rep(" ", 7) .. str)
+end
+
+
+
+
+function tester.init()
+ for k, v in pairs(_G) do
+ tester.globals[k] = v
+ end
+ return tester
+end
+
+
+function tester.test.global(expectedglobals)
+ expectedglobals = expectedglobals or {}
+ local info = debug.getinfo(2)
+ local unexpected = {}
+ for k in pairs(_G) do
+ if not tester.globals[k] and not has(expectedglobals, k) then
+ table.insert(unexpected, "Unexpected global '" .. k .. "'")
+ end
+ end
+ if #unexpected == 0 then
+ dopass(info.short_src, info.currentline)
+ else
+ dofail(info.short_src, info.currentline)
+ for _, v in pairs(unexpected) do printfailmsg(v) end
+ end
+end
+
+
+function tester.test.equal(result, expected)
+ local passed = isequal(result, expected)
+ local info = debug.getinfo(2)
+ if passed then
+ dopass(info.short_src, info.currentline)
+ else
+ dofail(info.short_src, info.currentline)
+ if type(expected) == "table" and type(result) == "table" then
+ printfailmsg("Tables do not match")
+ else
+ printfailmsg(string.format("Expected %s got %s",
+ stringify(expected), stringify(result) ))
+ end
+ end
+end
+
+
+function tester.test.error(fn, ...)
+ local passed = not pcall(fn, ...)
+ local info = debug.getinfo(2)
+ if passed then
+ dopass(info.short_src, info.currentline)
+ else
+ dofail(info.short_src, info.currentline)
+ printfailmsg("Expected an error to be raised")
+ end
+end
+
+
+function tester.dotests(t)
+ local keys = {}
+ for k in pairs(t) do table.insert(keys, k) end
+ table.sort(keys)
+ for _, k in pairs(keys) do
+ print("\27[33m-- " .. k .. "\27[0m")
+ t[k]()
+ end
+end
+
+
+function tester.printresults()
+ local str = table.concat{
+ "-- ",
+ string.format("Results: %d Total", tester.passcount + tester.failcount),
+ " ", string.format("%d Passed", tester.passcount),
+ " ", string.format("%d Failed", tester.failcount),
+ " --", }
+ local b = string.rep("-", #str)
+ print(table.concat{b, "\n", str, "\n", b})
+end
+
+
+return tester.init()
diff --git a/satelito/list.lua b/satelito/list.lua
@@ -0,0 +1,35 @@
+-- @module list
+local list = {}
+--
+local inspect = require 'inspect'
+local dirtree = require 'satelito.dirtree'
+local file = require 'satelito.file'
+local lume = require 'satelito.lib.lume.lume'
+local markdown = require 'discount' -- lua-discount
+local model = require 'satelito.model'
+
+function list.build(filepath)
+ local _list = {}
+
+ do
+ if file.is_index(filepath) then
+ local dirname = file.get_dirname(filepath)
+
+ for child in dirtree.get(dirname) do
+ if child
+ and file.is_markdown(child)
+ and not file.is_index(child)
+ then
+ local child_content = { content = assert(markdown(file.read(child))) }
+ local child_parameters = assert(model.get(child))
+
+ table.insert(_list, lume.extend({}, child_content, child_parameters))
+ end
+ end
+ end
+ end
+
+ return _list
+end
+
+return list
diff --git a/satelito/model.lua b/satelito/model.lua
@@ -0,0 +1,62 @@
+-- @module model
+local model = {}
+--
+local lfs = require 'lfs' -- luafilesystem
+local lume = require 'satelito.lib.lume.lume'
+local dirtree = require 'satelito.dirtree'
+local file = require 'satelito.file'
+local site = require 'satelito.site'
+
+function model.get(filepath)
+ -- The site config.lua file
+ local config = assert(site.set_config(filepath))
+ -- The metadata file of a filepath
+ local parameters_file = assert(
+ lfs.attributes(filepath:match('(.+)%..*') .. '.lua')
+ and dofile(filepath:match('(.+)%..*') .. '.lua')
+ or {}
+ )
+
+ -- If there's no parameter file for the markdown
+ -- Then set the minimum from the markdown file itself
+ if #parameters_file == 0 then
+ parameters_file['title'] = file.get_basename(filepath):match('(.+)%..*')
+ parameters_file['date'] = os.date('%Y-%m-%d', lfs.attributes(filepath).change)
+ parameters_file['datetime'] = os.date('%H:%M:%S', lfs.attributes(filepath).change)
+
+ -- print('There is no "' .. filepath:gsub('(.*/)(.*)', '%2'):match('(.+)%..*') .. '.lua" for the ' .. filepath .. ' file.')
+ -- print('You should create one.')
+ end
+
+ local contentdir = site.get_root(filepath) .. '/' .. config.paths.content
+ local relpath = assert(file.get_relpath(filepath, contentdir))
+ local rellink = assert(
+ file.is_index(filepath)
+ and file.get_dirname(file.get_relpath(filepath, contentdir))
+ or '/' .. relpath:match('(.+)%..*') .. '.html'
+ )
+ local permalink = assert(config.url .. '/' .. relpath:match('(.+)%..*') .. '.html')
+ local directory = assert(filepath:match("(.*/)"))
+ local time_modified_child = assert(file.get_lastmodified(lume.array(dirtree.get(directory))))
+ local id = assert('tag:' .. config.domainname .. ',' .. parameters_file.date .. ':' .. rellink)
+
+ return lume.extend({},
+ config,
+ parameters_file,
+ {directory = directory},
+ {directory = directory},
+ {path = filepath},
+ {relpath = relpath},
+ {rellink = rellink},
+ {permalink = permalink},
+ {dirlink = permalink:match("(.*/)")},
+ {time_created = (parameters_file.date..parameters_file.datetime):gsub('%W','')},
+ {time_updated = lfs.attributes(filepath).modification},
+ {time_change = lfs.attributes(filepath).change},
+ {time_modification = lfs.attributes(filepath).modification},
+ {time_modified_child = time_modified_child},
+ {id = id}
+ )
+end
+
+return model
diff --git a/satelito/page.lua b/satelito/page.lua
@@ -0,0 +1,71 @@
+local page = {}
+--
+local markdown = require 'discount' -- lua-discount
+local lume = require 'satelito.lib.lume.lume'
+local file = require 'satelito.file'
+local list = require 'satelito.list'
+local model = require 'satelito.model'
+local site = require 'satelito.site'
+local template = require 'satelito.template'
+
+--- Build a page from markdown/lua to HTML
+-- @name page.build
+-- @param filepath a pathname to a markdown file
+-- @return a string that is an HTML code block
+function page.build(filepath)
+ if file.is_markdown(filepath) then
+ local config = assert(site.set_config(filepath), "Sorry, the site configuration can't be set")
+ -- Post
+ local post_content = { content = assert(markdown(file.read(filepath))) }
+ local post_parameters = assert(model.get(filepath))
+ local post_children = { children = (file.is_index(filepath) and list.build(filepath)) }
+ local post_template = assert(template.set_post(post_parameters, filepath))
+ local post_html
+
+ if post_children then
+ post_html = post_template(lume.extend({}, post_content, post_parameters, post_children))
+ else
+ post_html = post_template(lume.extend({}, post_content, post_parameters))
+ end
+
+ -- Head
+ local head_template = assert(template.set_head(filepath))
+ local head_html = head_template(lume.extend({}, post_parameters))
+
+ -- Navigation
+ local navigation_template = assert(template.set_navigation(filepath))
+ local navigation_html = navigation_template(lume.extend({}, post_parameters))
+
+ -- Page
+ local page_layout = assert(template.set_layout(post_parameters, filepath))
+ local page_html = page_layout(
+ lume.extend({},
+ config,
+ post_parameters,
+ {head = head_html},
+ {navigation = navigation_html},
+ {post = post_html}
+ )
+ )
+ local page_html_path = assert(
+ site.get_root(filepath) .. '/' .. config.paths.public_html .. post_parameters.relpath:match('(.+)%..*') .. '.html'
+ )
+
+ return page_html, page_html_path
+ else
+ return print("Sorry i have nothing to do ...")
+ end
+end
+
+--- Export an HTML file to a specific location
+-- @name page.export
+-- @param htmlpath a pathname to the HTML file
+-- @param html a string that is an HTML code block
+-- @return a file.write function
+function page.export(htmlpath, html)
+ file.mkdir(file.get_dirname(htmlpath))
+
+ return file.write(htmlpath, html)
+end
+
+return page
diff --git a/satelito/site.lua b/satelito/site.lua
@@ -0,0 +1,34 @@
+local site = {}
+local lfs = require 'lfs'
+
+--- From a filepath get the closest 'config.lua' by climbing the
+-- directory tree
+-- Recursive function
+local function find_config(filepath)
+ assert(filepath and filepath ~= '', 'The filepath parameter is missing or empty.')
+
+ local dir = filepath:match("(.*/)")
+ local dir_parent = string.sub(dir, 1, -2):match("(.*/)")
+
+ for entry in lfs.dir(dir) do
+ if entry and entry == 'config.lua' then
+ return dir .. entry
+ end
+ end
+
+ return find_config(dir_parent)
+end
+
+function site.get_root(filepath)
+ return find_config(filepath):match("(.*/)"):sub(1, -2)
+end
+
+function site.set_config(filepath)
+ local site_root = site.get_root(filepath)
+
+ package.path = package.path .. ';'.. site_root ..'/?.lua'
+
+ return require 'config'
+end
+
+return site
diff --git a/satelito/template.lua b/satelito/template.lua
@@ -0,0 +1,69 @@
+-- @module template
+local template = {}
+--
+local etlua = require 'etlua'
+local lume = require 'satelito.lib.lume.lume'
+local dirtree = require 'satelito.dirtree'
+local file = require 'satelito.file'
+local site = require 'satelito.site'
+
+-- Find the path to a template file from his name
+local function find_template(templatedir, templatename)
+ return lume.match(
+ -- Make an array with all template files
+ lume.array(dirtree.get(templatedir)),
+ function(tpl)
+ -- Try to find a match
+ return string.gsub(tpl, '(.*/)(.*)', '%2') == templatename .. '.html'
+ end
+ )
+end
+
+function template.set_post(parameters, filepath)
+ local config = assert(site.set_config(filepath))
+ local post_template = assert(parameters.posttype and parameters.posttype or parameters.template or 'default')
+ local templates_dir = assert(site.get_root(filepath) .. '/' .. config.paths.templates)
+ local template_file = assert(
+ find_template(templates_dir, post_template) or find_template(templates_dir, 'default'),
+ 'The template of this type of post (' .. post_template .. '.html) is missing!'
+ )
+
+ return etlua.compile(file.read(template_file))
+end
+
+function template.set_layout(parameters, filepath)
+ local config = assert(site.set_config(filepath))
+ local layout = assert(parameters.layout and parameters.layout or 'layout')
+ local templates_dir = assert(site.get_root(filepath) .. '/' .. config.paths.templates)
+ local template_file = assert(find_template(templates_dir, layout), 'The "layout.html" template is missing!')
+
+ return etlua.compile(file.read(template_file))
+end
+
+function template.set_head(filepath)
+ local config = assert(site.set_config(filepath))
+ local templates_dir = assert(site.get_root(filepath) .. '/' .. config.paths.templates)
+ local template_file = assert(find_template(templates_dir, 'head'), 'The "head.html" template is missing!')
+
+ return etlua.compile(file.read(template_file))
+end
+
+function template.set_navigation(filepath)
+ local config = assert(site.set_config(filepath))
+ local templates_dir = assert(site.get_root(filepath) .. '/' .. config.paths.templates)
+ local template_file = assert(
+ find_template(templates_dir, 'navigation'), 'The "navigation.html" template is missing!'
+ )
+
+ return etlua.compile(file.read(template_file))
+end
+
+function template.set_feed(filepath)
+ local config = assert(site.set_config(filepath))
+ local templates_dir = assert(site.get_root(filepath) .. '/' .. config.paths.templates)
+ local template_file = assert(find_template(templates_dir, 'feed.xml'), 'The "feed.xml" template is missing!')
+
+ return etlua.compile(file.read(template_file))
+end
+
+return template