198 lines
6.2 KiB
Lua
Executable File
198 lines
6.2 KiB
Lua
Executable File
--[[
|
||
ScholarlyMeta – normalize author/affiliation meta variables
|
||
|
||
Copyright (c) 2017-2021 Albert Krewinkel, Robert Winkler
|
||
|
||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||
with or without fee is hereby granted, provided that the above copyright notice
|
||
and this permission notice appear in all copies.
|
||
|
||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||
THIS SOFTWARE.
|
||
]]
|
||
local List = require("pandoc.List")
|
||
|
||
--- Returns the type of a metadata value.
|
||
--
|
||
-- @param v a metadata value
|
||
-- @treturn string one of `Blocks`, `Inlines`, `List`, `Map`, `string`, `boolean`
|
||
local function metatype(v)
|
||
if PANDOC_VERSION <= "2.16.2" then
|
||
local metatag = type(v) == "table" and v.t and v.t:gsub("^Meta", "")
|
||
return metatag and metatag ~= "Map" and metatag or type(v)
|
||
end
|
||
return pandoc.utils.type(v)
|
||
end
|
||
|
||
local type = pandoc.utils.type or metatype
|
||
|
||
-- Split a string at commas.
|
||
local function comma_separated_values(str)
|
||
local acc = List:new({})
|
||
for substr in str:gmatch("([^,]*)") do
|
||
acc[#acc + 1] = substr:gsub("^%s*", ""):gsub("%s*$", "") -- trim
|
||
end
|
||
return acc
|
||
end
|
||
|
||
--- Ensure the return value is a list.
|
||
local function ensure_list(val)
|
||
if type(val) == "List" then
|
||
return val
|
||
elseif type(val) == "Inlines" then
|
||
-- check if this is really a comma-separated list
|
||
local csv = comma_separated_values(pandoc.utils.stringify(val))
|
||
if #csv >= 2 then
|
||
return csv
|
||
end
|
||
return List:new({ val })
|
||
elseif type(val) == "table" and #val > 0 then
|
||
return List:new(val)
|
||
else
|
||
-- Anything else, use as a singleton (or empty list if val == nil).
|
||
return List:new({ val })
|
||
end
|
||
end
|
||
|
||
--- Returns a function which checks whether an object has the given ID.
|
||
local function has_id(id)
|
||
return function(x)
|
||
return x.id == id
|
||
end
|
||
end
|
||
|
||
--- Copy all key-value pairs of the first table into the second iff there is no
|
||
-- such key yet in the second table.
|
||
-- @returns the second argument
|
||
function add_missing_entries(a, b)
|
||
for k, v in pairs(a) do
|
||
b[k] = b[k] or v
|
||
end
|
||
return b
|
||
end
|
||
|
||
--- Create an object with a name. The name is either taken directly from the
|
||
-- `name` field, or from the *only* field name (i.e., key) if the object is a
|
||
-- dictionary with just one entry. If neither exists, the name is left unset
|
||
-- (`nil`).
|
||
function to_named_object(obj)
|
||
local named = {}
|
||
if type(obj) == "Inlines" then
|
||
-- Treat inlines as the name
|
||
named.name = obj
|
||
named.id = pandoc.utils.stringify(obj)
|
||
elseif type(obj) ~= "table" then
|
||
-- if the object isn't a table, just use its value as a name.
|
||
named.name = pandoc.MetaInlines({ pandoc.Str(tostring(obj)) })
|
||
named.id = tostring(obj)
|
||
elseif obj.name ~= nil then
|
||
-- object has name attribute → just create a copy of the object
|
||
add_missing_entries(obj, named)
|
||
named.id = pandoc.utils.stringify(named.id or named.name)
|
||
elseif next(obj) and next(obj, next(obj)) == nil then
|
||
-- Single-entry table. The entry's key is taken as the name, the value
|
||
-- contains the attributes.
|
||
key, attribs = next(obj)
|
||
if type(attribs) == "string" or type(attribs) == "Inlines" then
|
||
named.name = attribs
|
||
else
|
||
add_missing_entries(attribs, named)
|
||
named.name = named.name or pandoc.MetaInlines({ pandoc.Str(tostring(key)) })
|
||
end
|
||
named.id = named.id and pandoc.utils.stringify(named.id) or key
|
||
else
|
||
-- this is not a named object adhering to the usual conventions.
|
||
error("not a named object: " .. tostring(obj))
|
||
end
|
||
return named
|
||
end
|
||
|
||
--- Resolve affiliations placeholders to full named objects
|
||
local function resolve_affiliations(affiliations, known_affiliations)
|
||
local unresolved_affiliations
|
||
if affiliations == nil then
|
||
unresolved_affiliations = {}
|
||
elseif type(affiliations) == "string" or type(affiliations) == "number" then
|
||
unresolved_affiliations = { affiliations }
|
||
else
|
||
unresolved_affiliations = affiliations
|
||
end
|
||
|
||
local result = List:new({})
|
||
for i, inst in ipairs(unresolved_affiliations) do
|
||
result[i] = known_affiliations[tonumber(inst)]
|
||
or known_affiliations:find_if(has_id(pandoc.utils.stringify(inst)))
|
||
or to_named_object(inst)
|
||
end
|
||
return result
|
||
end
|
||
|
||
--- Insert a named object into a list; if an object of the same name exists
|
||
-- already, add all properties only present in the new object to the existing
|
||
-- item.
|
||
function merge_on_id(list, namedObj)
|
||
local elem, idx = list:find_if(has_id(namedObj.id))
|
||
local res = elem and add_missing_entries(namedObj, elem) or namedObj
|
||
local obj_idx = idx or (#list + 1)
|
||
-- return res, obj_idx
|
||
list[obj_idx] = res
|
||
return res, #list
|
||
end
|
||
|
||
--- Flatten a list of lists.
|
||
local function flatten(lists)
|
||
local result = List:new({})
|
||
for _, lst in ipairs(lists) do
|
||
result:extend(lst)
|
||
end
|
||
return result
|
||
end
|
||
|
||
--- Canonicalize authors and affiliations
|
||
local function canonicalize(raw_author, raw_affiliations)
|
||
local affiliations = ensure_list(raw_affiliations):map(to_named_object)
|
||
local authors = ensure_list(raw_author):map(to_named_object)
|
||
|
||
for _, author in ipairs(authors) do
|
||
author.affiliations = resolve_affiliations(ensure_list(author.affiliations), affiliations)
|
||
end
|
||
|
||
-- Merge affiliations defined in author objects with those defined in the
|
||
-- top-level list.
|
||
local author_insts = flatten(authors:map(function(x)
|
||
return x.affiliations
|
||
end))
|
||
for _, inst in ipairs(author_insts) do
|
||
merge_on_id(affiliations, inst)
|
||
end
|
||
|
||
-- Add list indices to affiliations for numbering and reference purposes
|
||
for idx, inst in ipairs(affiliations) do
|
||
inst.index = pandoc.MetaInlines({ pandoc.Str(tostring(idx)) })
|
||
end
|
||
|
||
-- replace affiliations with their indices
|
||
local to_index = function(inst)
|
||
return tostring(select(2, affiliations:find_if(has_id(inst.id))))
|
||
end
|
||
for _, author in ipairs(authors) do
|
||
author.affiliations = pandoc.MetaList(author.affiliations:map(to_index))
|
||
end
|
||
|
||
return authors, affiliations
|
||
end
|
||
|
||
return {
|
||
{
|
||
Meta = function(meta)
|
||
meta.author, meta.affiliations = canonicalize(meta.author, meta.affiliations)
|
||
return meta
|
||
end,
|
||
},
|
||
}
|