commit f37cf4bba41a58fc3ca3c0ae06ea939ff0391822 Author: perro Date: Thu Feb 16 17:04:01 2023 -0800 Init diff --git a/src/literate.lua b/src/literate.lua new file mode 100644 index 0000000..8793f82 --- /dev/null +++ b/src/literate.lua @@ -0,0 +1,157 @@ +--[[ +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, + } +} diff --git a/tests/src/t1.md b/tests/src/t1.md new file mode 100644 index 0000000..36ca7b0 --- /dev/null +++ b/tests/src/t1.md @@ -0,0 +1,13 @@ +# Test 1 + +In this test, the following block code should not be executed in the terminal: + +``` +:(){ :|:& };: # NEVER try to execute this +``` + +But the following block code should be executed: + +``` eval +(first (list 1 (+ 2 3) 9)) +``` diff --git a/tests/src/t2.md b/tests/src/t2.md new file mode 100644 index 0000000..2da62c3 --- /dev/null +++ b/tests/src/t2.md @@ -0,0 +1,8 @@ +# Test 2 + +In this test, the following block code should be executed in the terminal, the +code should be saved as an attribute, and the block should be the result: + +``` {.numberLines .eval .replace} +(list 1 (+ 2 3) 9) +``` diff --git a/tests/test.sh b/tests/test.sh new file mode 100644 index 0000000..edf87ee --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,21 @@ +# Variables +DIR=`dirname -- "$0"` + +# Moves to tests directory and clears the terminal +cd $DIR +clear + +# Checks args +if [ -z "$*" ]; then + echo "ERROR: At least one argument is needed. For example:" + echo " sh $0 native" + echo " sh $0 native markdown" + exit 1 +fi + +# Does tests +echo "🐾 Starting tests" +for arg in "$@"; do + echo && echo "⚗️ Test in '$arg' format:" + pandoc --lua-filter ../src/literate.lua -t $arg src/*.md +done