158 lines
5.0 KiB
Lua
158 lines
5.0 KiB
Lua
--[[
|
|
literate.lua
|
|
(C) 2023 perro tuerto <hi@perrotuerto.blog>
|
|
Code under GPLv3 <https://www.gnu.org/licenses/gpl-3.0.en.html>
|
|
]]
|
|
|
|
-- Lua LPeg shortcuts
|
|
local P, S, R, Cf, Cc, Ct, V, Cs, Cg, Cb, B, C, Cmt =
|
|
lpeg.P, lpeg.S, lpeg.R, lpeg.Cf, lpeg.Cc, lpeg.Ct, lpeg.V,
|
|
lpeg.Cs, lpeg.Cg, lpeg.Cb, lpeg.B, lpeg.C, lpeg.Cmt
|
|
|
|
-- Lexical elements
|
|
local space = S(" \t\r\n")
|
|
local lbracket = P"("
|
|
local rbracket = P")"
|
|
local word = (1 - (space + lbracket + rbracket))
|
|
|
|
-- Table of Lisp functions.
|
|
-- The key is the operator and the value is the operator function.
|
|
-- All functions 'e' are the operands, a list of Lisp atoms as pandoc.Inlines.
|
|
-- Note: it doesn't support all the available Lisp functions, just the required
|
|
-- for this exercise.
|
|
local operators = {
|
|
-- Lisp addition function.
|
|
-- The operands are looped, converted to a number and added to the result.
|
|
-- The result is a number that is converted to pandoc.Str because Pandoc
|
|
-- structure only accepts Pandoc elements.
|
|
["+"] = function (e)
|
|
local result = 0
|
|
e:walk {
|
|
Str = function (str)
|
|
local num = tonumber(pandoc.utils.stringify(str))
|
|
result = result + num
|
|
end
|
|
}
|
|
return pandoc.Str(result)
|
|
end,
|
|
-- Lisp list function.
|
|
-- Operands are already in a list, so it returns the same.
|
|
["list"] = function (e) return e end,
|
|
-- Lisp first function.
|
|
-- Since operands are already in a list, it returns the first element.
|
|
["first"] = function (e) return e[1] end,
|
|
}
|
|
|
|
-- Transforms lists to spans.
|
|
-- Each Lisp list turns to Pandoc Span.
|
|
-- @param ... pandoc.Inline: Lisp atoms are treated as Pandoc Inline.
|
|
local function embed (...)
|
|
local els = table.pack(...)
|
|
return pandoc.Span(els)
|
|
end
|
|
|
|
-- Evals Lisp list.
|
|
-- The first list element is the operator for evaluation.
|
|
-- @param list pandoc.Inlines: Lisp list as pandoc.Inlines.
|
|
-- @return pandoc.Span or pandoc.Str: Evaluation result.
|
|
local function eval_list(list)
|
|
local operator = pandoc.utils.stringify(list[1])
|
|
list:remove(1)
|
|
-- When first atom is a number and there aren't other atoms.
|
|
if operator:match("[%d]") and #list == 0 then
|
|
return operator
|
|
-- When first atom is not a number.
|
|
-- Note: this assumes that the function is implemented in 'operators'.
|
|
elseif operator:match("[%D]") then
|
|
return operators[operator](list)
|
|
-- Rest of cases.
|
|
-- For example, when first atom is a number but it has other atoms.
|
|
else
|
|
print("ERROR: " .. operator .. " is not a function")
|
|
sys.exit(1)
|
|
end
|
|
end
|
|
|
|
-- Evals Lisp atom.
|
|
-- When atom is in a list, it doesn't get affected; otherwise, all the
|
|
-- non digit elements are eliminated.
|
|
-- @param atom pandoc.Str: Lisp atom as pandoc.Str.
|
|
-- @param in_list boolean: Indicates if atom is in a list or not.
|
|
-- @return atom pandoc.Str: Lisp atom.
|
|
local function eval_atom(atom, in_list)
|
|
if not in_list then
|
|
atom = pandoc.utils.stringify(atom)
|
|
atom = atom:gsub("[%D]", "")
|
|
-- Note: Pandoc content empty structures are ignored on conversion, this
|
|
-- enables expressions like '+ 1 2' be '1 2'.
|
|
atom = pandoc.Str(atom)
|
|
end
|
|
return atom
|
|
end
|
|
|
|
-- Formats code.
|
|
-- Adds and space between each pandoc.Str (Lisp atom).
|
|
-- @param res pandoc.Plain: Lisp eval result.
|
|
-- @return res string: Formatted result.
|
|
local function format(res)
|
|
local res = res:walk {
|
|
Str = function (str) return pandoc.Inlines{str, pandoc.Space()} end
|
|
}
|
|
return pandoc.utils.stringify(res)
|
|
end
|
|
|
|
-- Evals code.
|
|
-- The code consists in a table of pandoc.Plain elements,
|
|
-- each pandoc.Plain is a table of pandoc.Span or pandoc.Str,
|
|
-- so the equivalency with Lisp is:
|
|
-- pandoc.Plain => Lisp expression
|
|
-- pandoc.Span => Lisp list
|
|
-- pandoc.Str => Lisp atom
|
|
-- According to the children Pandoc element of pandoc.Plain, this
|
|
-- function deals with Lisp lists or Lisp atoms. The last walk is just for
|
|
-- formatting, since it separates atoms with an space.
|
|
-- @param code pandoc.Plain: Lisp expression as pandoc.Plain.
|
|
-- @return res string: Result of evaluation.
|
|
local function eval(code)
|
|
local res = code:walk {
|
|
Plain = function (plain)
|
|
local in_span = plain.content[1].tag == "Span"
|
|
return plain:walk {
|
|
Span = function (span) return eval_list(span.content) end,
|
|
Str = function (str) return eval_atom(str, in_span) end
|
|
}
|
|
end
|
|
}
|
|
return format(res)
|
|
end
|
|
|
|
-- Grammar for Pandoc parser that converts:
|
|
-- Lisp expression => pandoc.Plain
|
|
-- Lisp list => pandoc.Span
|
|
-- Lisp atom => pandoc.Str
|
|
G = P{
|
|
"Doc";
|
|
Doc = space^0 * Ct(V"SExpr"^0) * space^0 / pandoc.Pandoc;
|
|
SExpr = (V"List" + V"Atom");
|
|
List = lbracket * V"SExpr"^0 * rbracket / embed;
|
|
Atom = space + V"Word";
|
|
Word = word^1 / pandoc.Str;
|
|
}
|
|
|
|
return {
|
|
{
|
|
CodeBlock = function (block)
|
|
if block.classes:includes("eval") then
|
|
local raw = block.text
|
|
local code = lpeg.match(G, raw)
|
|
local res = eval(code)
|
|
print("⚙️ ", raw)
|
|
print("", "=>", res)
|
|
if block.classes:includes("replace") then
|
|
return pandoc.CodeBlock(res, {code=raw})
|
|
end
|
|
end
|
|
end,
|
|
}
|
|
}
|