--[[ literate.lua (C) 2023 perro tuerto Code under GPLv3 ]] -- 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, } }