在 Lua 中创建自定义 Pandoc 读取器
介绍
如果你需要解析 Pandoc
尚不支持的格式,你可以使用 Lua 语言创建一个自定义读取器。Pandoc
内置了 Lua
解释器,因此你无需安装任何额外的软件来实现这一点。
一个自定义读取器是一个 Lua 文件,它定义了一个名为 Reader
的函数,该函数接受两个参数:
- 要被解析的原始输入,作为源列表
- 可选的,一个读者选项表,例如
{ columns = 62, standalone = true }
。
Reader
函数应该返回一个 Pandoc
抽象语法树(AST
)。这可以使用自动在作用域内的 pandoc 模块 中的函数来创建。( 实际上,所有对 Lua 过滤器 可用的实用函数在自定义读取器中也是可用的。)
每个源项对应于传递给 pandoc
并包含其文本和名称的文件或流。例如,如果单个文件 input.txt
传递给 pandoc
,那么源列表将只包含一个元素 s
,其中 s.name == 'input.txt'
且 s.text
包含文件内容作为字符串 。
源列表及其每个元素都可以通过 Lua
标准库函数 tostring
转换为字符串。
一个最简单的示例是
function Reader(input)
return pandoc.Pandoc({ pandoc.CodeBlock(tostring(input)) })
end
这只会返回一个包含所有输入的大代码块。或者,要为每个输入文件创建一个单独的代码块,可以编写
function Reader(input)
return pandoc.Pandoc(input:map(
function (s) return pandoc.CodeBlock(s.text) end))
end
在一个非平凡的读取器中,你可能希望解析输入。你可以使用标准 Lua 库函数(例如,patterns 库)或强大而快速的 lpeg 解析库来实现这一点,后者会自动在作用域内。你也可以使用外部 Lua 库(例如,一个 XML 解析器)。
Pandoc
的早期版本将原始字符串而不是源列表传递给 Reader
函数。依赖于此行为的 Reader
函数已过时,但仍受支持:Pandoc
分析任何脚本错误,检测代码是否假设了旧的行为。在这种情况下,代码会再次运行,但输入为原始字符串,从而确保了向后兼容性。
字节串读取器
为了读取二进制格式,包括 docx
、odt
和 epub
,Pandoc
支持 ByteStringReader
函数。ByteStringReader
函数类似于处理文本输入的 Reader
函数。它接收的不是源列表,而是一个字节串,即包含二进制输入的字符串。
-- 将输入作为epub读取
function ByteStringReader (input)
return pandoc.read(input, 'epub')
end
格式扩展
自定义读取器可以构建得使其行为可通过格式扩展控制,如 smart
、citations
或 hard-line-breaks
。支持的扩展是那些作为全局 Extensions
表中的键存在的。启用的扩展默认值为 true
或 enable
,而被支持但禁用的扩展的值为 false
或 disable
。
示例:具有以下全局表的写入器支持 smart
、citations
和 foobar
扩展,其中 smart
被启用,而其他两个默认被禁用:
Extensions = {
smart = 'enable',
citations = 'disable',
foobar = true
}
用户像平常一样控制扩展,例如,pandoc -f my-reader.lua+citations
。扩展可以通过读者选项的 extensions
字段访问,例如:
function Reader (input, opts)
print(
'The citations extension is',
opts.extensions:includes 'citations' and 'enabled' or 'disabled'
)
-- ...
end
在 Extensions
字段中既未启用也未禁用的扩展被视为不受读取器支持。尝试通过命令行修改这样的扩展会导致错误。
示例:纯文本读取器
这是一个简单的示例,使用 lpeg 解析输入为由空格分隔的字符串和由空白行分隔的段落。
-- 一个简单的自定义读取器示例,它只是将文本解析为由空白行分隔的段落,
-- 段落中的单词由空格分隔。
-- 为了更好的性能,我们将这些函数放在局部变量中:
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
local whitespacechar = S(" \t\r\n")
local wordchar = (1 - whitespacechar)
local spacechar = S(" \t")
local newline = P"\r"^-1 * P"\n"
local blanklines = newline * (spacechar^0 * newline)^1
local endline = newline - blanklines
-- 语法
G = P{ "Pandoc",
Pandoc = Ct(V"Block"^0) / pandoc.Pandoc;
Block = blanklines^0 * V"Para" ;
Para = Ct(V"Inline"^1) / pandoc.Para;
Inline = V"Str" + V"Space" + V"SoftBreak" ;
Str = wordchar^1 / pandoc.Str;
Space = spacechar^1 / pandoc.Space;
SoftBreak = endline / pandoc.SoftBreak;
}
function Reader(input)
return lpeg.match(G, tostring(input))
end
使用示例:
% pandoc -f plain.lua -t native
*Hello there*, this is plain text with no formatting
except paragraph breaks.
- Like this one.
^D
[ Para
[ Str "*Hello"
, Space
, Str "there*,"
, Space
, Str "this"
, Space
, Str "is"
, Space
, Str "plain"
, Space
, Str "text"
, Space
, Str "with"
, Space
, Str "no"
, Space
, Str "formatting"
, SoftBreak
, Str "except"
, Space
, Str "paragraph"
, Space
, Str "breaks."
]
, Para
[ Str "-"
, Space
, Str "Like"
, Space
, Str "this"
, Space
, Str "one."
]
]
示例:wiki Creole 阅读器
这是一个解析器,用于 Creole 通用 wiki 标记语言 。它使用了 lpeg 语法。有趣的事实是,这个自定义阅读器比 pandoc
内置的 Creole
阅读器更快!这表明高性能的阅读器可以通过这种方式设计。
使用示例:
% pandoc -f creole.lua -t markdown
== Wiki Creole
You can make things **bold** or //italic// or **//both//** or //**both**//.
Character formatting extends across line breaks: **bold,
this is still bold. This line deliberately does not end in star-star.
Not bold. Character formatting does not cross paragraph boundaries.
You can use [[internal links]] or [[http://www.wikicreole.org|external links]],
give the link a [[internal links|different]] name.
^D
## Wiki Creole
You can make things **bold** or *italic* or ***both*** or ***both***.
Character formatting extends across line breaks: \*\*bold, this is still
bold. This line deliberately does not end in star-star.
Not bold. Character formatting does not cross paragraph boundaries.
You can use [internal links](internal links) or [external
links](http://www.wikicreole.org), give the link a
[different](internal links) name.
示例:从 API 解析 JSON
这个自定义读取器消费来自 https://www.reddit.com/r/haskell.json 的 JSON 输出,并生成一个文档,包含 Haskell 子版块当前的热门文章。
它假设 pandoc.json
库可用,该库随 pandoc
版本 3.1 之后(不含 3.1)提供。如果使用的是旧版 pandoc,可以使用其他 JSON 库实现。例如,可以通过 luarocks install luajson
安装 luajson
— 但确保它是为 Lua 5.4
安装的,这是 pandoc
打包的版本。
local json = require 'pandoc.json'
local function read_inlines(raw)
local doc = pandoc.read(raw, "commonmark")
return pandoc.utils.blocks_to_inlines(doc.blocks)
end
local function read_blocks(raw)
local doc = pandoc.read(raw, "commonmark")
return doc.blocks
end
function Reader(input)
local parsed = json.decode(tostring(input))
local blocks = {}
for _,entry in ipairs(parsed.data.children) do
local d = entry.data
table.insert(blocks, pandoc.Header(2,
pandoc.Link(read_inlines(d.title), d.url)))
for _,block in ipairs(read_blocks(d.selftext)) do
table.insert(blocks, block)
end
end
return pandoc.Pandoc(blocks)
end
类似的代码可用于消费来自其他 API 的 JSON 输出。
请注意,文本字段的内容是 Markdown 格式,因此我们使用 pandoc.read()
进行转换。
示例:语法高亮的代码文件
这是一个读取器,它将每个输入文件的内容放入代码块中,将文件扩展名设置为块的类以启用代码高亮,并在每个代码块上方放置文件名作为标题。
function to_code_block (source)
local _, lang = pandoc.path.split_extension(source.name)
return pandoc.Div{
pandoc.Header(1, source.name == '' and '<stdin>' or source.name),
pandoc.CodeBlock(source.text, {class=lang}),
}
end
function Reader (input, opts)
return pandoc.Pandoc(input:map(to_code_block))
end
示例:从网页提取内容
这个读取器使用命令行程序 readable
(通过 npm install -g readability-cli
安装)来清理 HTML 输入中与导航和布局相关的部分,仅留下内容。
-- 自定义读取器,用于从HTML文档中提取内容,
-- 忽略导航和布局元素。此读取器先通过'readable'程序预处理输入
-- (可使用'npm install -g readability-cli'安装),然后调用HTML读取器。
-- 此外,移除看似仅具有布局功能的Div,以避免杂乱。
function make_readable(source)
local result
if not pcall(function ()
local name = source.name
if not name:match("http") then
name = "file:///" .. name
end
result = pandoc.pipe("readable",
{"--keep-classes","--base",name},
source.text)
end) then
io.stderr:write("Error running 'readable': do you have it installed?\n")
io.stderr:write("npm install -g readability-cli\n")
os.exit(1)
end
return result
end
local boring_classes =
{ row = true,
page = true,
container = true
}
local boring_attributes = { "role" }
local function is_boring_class(cl)
return boring_classes[cl] or cl:match("col%-") or cl:match("pull%-")
end
local function handle_div(el)
for i,class in ipairs(el.classes) do
if is_boring_class(class) then
el.classes[i] = nil
end
end
for i,k in ipairs(boring_attributes) do
el.attributes[k] = nil
end
if el.identifier:match("readability%-") then
el.identifier = ""
end
if #el.classes == 0 and #el.attributes == 0 and #el.identifier == 0 then
return el.content
else
return el
end
end
function Reader(sources)
local readable = ''
for _,source in ipairs(sources) do
readable = readable .. make_readable(source)
end
local doc = pandoc.read(readable, "html", PANDOC_READER_OPTIONS)
-- Now remove Divs used only for layout
return doc:walk{ Div = handle_div }
end
使用示例:
pandoc -f readable.lua -t markdown https://pandoc.org
并将输出与下列命令的输出进行比较:
pandoc -f html -t markdown https://pandoc.org