This commit is contained in:
perro tuerto 2023-02-16 17:04:01 -08:00
commit f37cf4bba4
4 changed files with 199 additions and 0 deletions

157
src/literate.lua Normal file
View File

@ -0,0 +1,157 @@
--[[
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,
}
}

13
tests/src/t1.md Normal file
View File

@ -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))
```

8
tests/src/t2.md Normal file
View File

@ -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)
```

21
tests/test.sh Normal file
View File

@ -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