diff options
author | Albert Krewinkel <albert@zeitkraut.de> | 2019-09-15 21:11:58 +0200 |
---|---|---|
committer | John MacFarlane <jgm@berkeley.edu> | 2019-09-15 12:11:58 -0700 |
commit | d0261d7387cdf1a910a1029a747e239453190f71 (patch) | |
tree | f420021bdd6dc101054c5c9f17095cc8bf0620c8 | |
parent | f580da203382efbb3c1cc8da37eacd27025034d7 (diff) | |
download | pandoc-d0261d7387cdf1a910a1029a747e239453190f71.tar.gz |
Lua filters: allow passing of HTML-like tables instead of Attr (#5750)
Attr values can now be given as normal Lua tables; this can be used as a
convenient alternative to define Attr values, instead of constructing
values with `pandoc.Attr`. Identifiers are taken from the *id* field,
classes must be given as space separated words in the *class* field. All
remaining fields are included as misc attributes.
With this change, the following lines now create equal elements:
pandoc.Span('test', {id = 'test', class = 'a b', check = 1})
pandoc.Span('test', pandoc.Attr('test', {'a','b'}, {check = 1}))
This also works when using the *attr* setter:
local span = pandoc.Span 'text'
span.attr = {id = 'test', class = 'a b', check = 1}
Furthermore, the *attributes* field of AST elements can now be a plain
key-value table even when using the `attributes` accessor:
local span = pandoc.Span 'test'
span.attributes = {check = 1} -- works as expected now
Closes: #5744
-rw-r--r-- | data/pandoc.lua | 91 | ||||
-rw-r--r-- | doc/lua-filters.md | 14 | ||||
-rw-r--r-- | test/lua/module/pandoc.lua | 41 |
3 files changed, 129 insertions, 17 deletions
diff --git a/data/pandoc.lua b/data/pandoc.lua index 97e75efca..d1c88d0a1 100644 --- a/data/pandoc.lua +++ b/data/pandoc.lua @@ -50,11 +50,15 @@ local utils = M.utils -- @param indices list of indices, starting with the most deeply nested -- @return newly created function -- @local -function make_indexing_function(template, indices) +function make_indexing_function(template, ...) + local indices = {...} local loadstring = loadstring or load local bracketed = {} for i = 1, #indices do - bracketed[i] = string.format('[%d]', indices[#indices - i + 1]) + local idx = indices[#indices - i + 1] + bracketed[i] = type(idx) == 'number' + and string.format('[%d]', idx) + or string.format('.%s', idx) end local fnstr = string.format('return ' .. template, table.concat(bracketed)) return assert(loadstring(fnstr))() @@ -69,11 +73,16 @@ local function create_accessor_functions (fn_template, accessors) local res = {} function add_accessors(acc, ...) if type(acc) == 'string' then - res[acc] = make_indexing_function(fn_template, {...}) + res[acc] = make_indexing_function(fn_template, ...) elseif type(acc) == 'table' and #acc == 0 and next(acc) then + -- Named substructure: the given names are accessed via the substructure, + -- but the accessors are also added to the result table, enabling direct + -- access from the parent element. Mainly used for `attr`. local name, substructure = next(acc) - res[name] = make_indexing_function(fn_template, {...}) - add_accessors(substructure, ...) + res[name] = make_indexing_function(fn_template, ...) + for _, subname in ipairs(substructure) do + res[subname] = make_indexing_function(fn_template, subname, ...) + end else for i = 1, #(acc or {}) do add_accessors(acc[i], i, ...) @@ -272,6 +281,35 @@ local function ensureDefinitionPairs (pair) return {inlines, blocks} end +--- Split a string into it's words, using whitespace as separators. +local function words (str) + local ws = {} + for w in str:gmatch("([^%s]+)") do ws[#ws + 1] = w end + return ws +end + +--- Try hard to turn the arguments into an Attr object. +local function ensureAttr(attr) + if type(attr) == 'table' then + if #attr > 0 then return M.Attr(table.unpack(attr)) end + + -- assume HTML-like key-value pairs + local ident = attr.id or '' + local classes = words(attr.class or '') + local attributes = attr + attributes.id = nil + attributes.class = nil + return M.Attr(ident, classes, attributes) + elseif attr == nil then + return M.Attr() + elseif type(attr) == 'string' then + -- treat argument as ID + return M.Attr(attr) + end + -- print(arg, ...) + error('Could not convert to Attr') +end + ------------------------------------------------------------------------ --- Pandoc Document -- @section document @@ -402,7 +440,7 @@ M.BulletList = M.Block:create_constructor( -- @treturn Block code block element M.CodeBlock = M.Block:create_constructor( "CodeBlock", - function(text, attr) return {c = {attr or M.Attr(), text}} end, + function(text, attr) return {c = {ensureAttr(attr), text}} end, {{attr = {"identifier", "classes", "attributes"}}, "text"} ) @@ -426,7 +464,7 @@ M.DefinitionList = M.Block:create_constructor( M.Div = M.Block:create_constructor( "Div", function(content, attr) - return {c = {attr or M.Attr(), ensureList(content)}} + return {c = {ensureAttr(attr), ensureList(content)}} end, {{attr = {"identifier", "classes", "attributes"}}, "content"} ) @@ -440,7 +478,7 @@ M.Div = M.Block:create_constructor( M.Header = M.Block:create_constructor( "Header", function(level, content, attr) - return {c = {level, attr or M.Attr(), ensureInlineList(content)}} + return {c = {level, ensureAttr(attr), ensureInlineList(content)}} end, {"level", {attr = {"identifier", "classes", "attributes"}}, "content"} ) @@ -569,7 +607,7 @@ M.Cite = M.Inline:create_constructor( -- @treturn Inline code element M.Code = M.Inline:create_constructor( "Code", - function(text, attr) return {c = {attr or M.Attr(), text}} end, + function(text, attr) return {c = {ensureAttr(attr), text}} end, {{attr = {"identifier", "classes", "attributes"}}, "text"} ) @@ -594,8 +632,7 @@ M.Image = M.Inline:create_constructor( "Image", function(caption, src, title, attr) title = title or "" - attr = attr or M.Attr() - return {c = {attr, ensureInlineList(caption), {src, title}}} + return {c = {ensureAttr(attr), ensureInlineList(caption), {src, title}}} end, {{attr = {"identifier", "classes", "attributes"}}, "caption", {"src", "title"}} ) @@ -619,7 +656,7 @@ M.Link = M.Inline:create_constructor( "Link", function(content, target, title, attr) title = title or "" - attr = attr or M.Attr() + attr = ensureAttr(attr) return {c = {attr, ensureInlineList(content), {target, title}}} end, {{attr = {"identifier", "classes", "attributes"}}, "content", {"target", "title"}} @@ -743,7 +780,7 @@ M.Space = M.Inline:create_constructor( M.Span = M.Inline:create_constructor( "Span", function(content, attr) - return {c = {attr or M.Attr(), ensureInlineList(content)}} + return {c = {ensureAttr(attr), ensureInlineList(content)}} end, {{attr = {"identifier", "classes", "attributes"}}, "content"} ) @@ -902,17 +939,21 @@ function M.Attr:new (identifier, classes, attributes) identifier = identifier or '' classes = ensureList(classes or {}) attributes = setmetatable(to_alist(attributes or {}), AttributeList) - return {identifier, classes, attributes} + return setmetatable({identifier, classes, attributes}, self.behavior) end M.Attr.behavior.clone = M.types.clone.Attr +M.Attr.behavior.tag = 'Attr' M.Attr.behavior._field_names = {identifier = 1, classes = 2, attributes = 3} M.Attr.behavior.__eq = utils.equals M.Attr.behavior.__index = function(t, k) - return rawget(t, getmetatable(t)._field_names[k]) or + return (k == 't' and t.tag) or + rawget(t, getmetatable(t)._field_names[k]) or getmetatable(t)[k] end M.Attr.behavior.__newindex = function(t, k, v) - if getmetatable(t)._field_names[k] then + if k == 'attributes' then + rawset(t, 3, setmetatable(to_alist(v or {}), AttributeList)) + elseif getmetatable(t)._field_names[k] then rawset(t, getmetatable(t)._field_names[k], v) else rawset(t, k, v) @@ -927,6 +968,24 @@ M.Attr.behavior.__pairs = function(t) return make_next_function(fields), t, nil end +-- Monkey-patch setters for `attr` fields to be more forgiving in the input that +-- results in a valid Attr value. +function augment_attr_setter (setters) + if setters.attr then + local orig = setters.attr + setters.attr = function(k, v) + orig(k, ensureAttr(v)) + end + end +end +for _, blk in pairs(M.Block.constructor) do + augment_attr_setter(blk.behavior.setters) +end +for _, inln in pairs(M.Inline.constructor) do + augment_attr_setter(inln.behavior.setters) +end + + -- Citation M.Citation = AstElement:make_subtype'Citation' M.Citation.behavior.clone = M.types.clone.Citation diff --git a/doc/lua-filters.md b/doc/lua-filters.md index 87071ffc4..723f3cd08 100644 --- a/doc/lua-filters.md +++ b/doc/lua-filters.md @@ -1247,7 +1247,19 @@ Superscripted text ### Attr {#type-ref-Attr} -A set of element attributes +A set of element attributes. Values of this type can be created +with the [`pandoc.Attr`](#Attr) constructor. For convenience, it +is usually not necessary to construct the value directly if it is +part of an element, and it is sufficient to pass an HTML-like +table. E.g., to create a span with identifier "text" and classes +"a" and "b", on can write: + + local span = pandoc.Span('text', {id = 'text', class = 'a b'}) + +This also works when using the `attr` setter: + + local span = pandoc.Span 'text' + span.attr = {id = 'text', class = 'a b', other_attribute = '1'} Object equality is determined via [`pandoc.utils.equals`](#utils-equals). diff --git a/test/lua/module/pandoc.lua b/test/lua/module/pandoc.lua index 1725d275b..fa1748c18 100644 --- a/test/lua/module/pandoc.lua +++ b/test/lua/module/pandoc.lua @@ -86,6 +86,47 @@ return { assert.are_equal(count, 3) end) }, + group 'HTML-like attribute tables' { + test('in element constructor', function () + local html_attributes = { + id = 'the-id', + class = 'class1 class2', + width = '11', + height = '12' + } + local attr = pandoc.Span('test', html_attributes).attr + assert.are_equal(attr.identifier, 'the-id') + assert.are_equal(attr.classes[1], 'class1') + assert.are_equal(attr.classes[2], 'class2') + assert.are_equal(attr.attributes.width, '11') + assert.are_equal(attr.attributes.height, '12') + end), + test('element attr setter', function () + local html_attributes = { + id = 'the-id', + class = 'class1 class2', + width = "11", + height = "12" + } + local span = pandoc.Span 'test' + span.attr = html_attributes + assert.are_equal(span.attr.identifier, 'the-id') + assert.are_equal(span.attr.classes[1], 'class1') + assert.are_equal(span.attr.classes[2], 'class2') + assert.are_equal(span.attr.attributes.width, '11') + assert.are_equal(span.attr.attributes.height, '12') + end), + test('element attrbutes setter', function () + local attributes = { + width = "11", + height = "12" + } + local span = pandoc.Span 'test' + span.attributes = attributes + assert.are_equal(span.attr.attributes.width, '11') + assert.are_equal(span.attr.attributes.height, '12') + end) + } }, group 'clone' { |