--
--  layout-functions.lua
--  speedata publisher
--
--  For a list of authors see `git blame'
--  See file COPYING in the root directory for license info.


file_start("layout_functions.lua")

local links_module = require("publisher.links")

local box_lookup = {
    ["artbox"]   = "art",
    ["cropbox"]  = "crop",
    ["trimbox"]  = "trim",
    ["mediabox"] = "media",
    ["bleedbox"] = "bleed",
}

local luxor      = do_luafile("luxor.lua")
local sha        = require('shalocal')

-- Return filename, pagenumber, box and unit from the arg. Used in imagewidth et al.
--- @param arg table
--- @return string, number, string, string | nil
local function get_filename_pagenum_box_unit_from_arg(arg)
    local filename, box, unit
    box = "cropbox"
    local pagenumber = 1
    filename = arg[1]
    for i = 2, #arg do
        local ai = arg[i]
        if tonumber(ai) then
            pagenumber = tonumber(ai) or 1
        elseif ai == "cm" or ai == "mm" or ai == "in" or ai == "sp" or ai == "pc" or ai == "pt" or ai == "pp" or ai == "cc" then
            unit = ai
        elseif ai == "artbox" or ai == "cropbox" or ai == "bleedbox" or ai == "trimbox" or ai == "mediabox" then
            box = ai
        end
    end
    return filename, pagenumber, box, unit
end


local function visiblepagenumber(pagenumber)
    pagenumber = tonumber(pagenumber)
    return publisher.visible_pagenumbers[pagenumber] or pagenumber
end

local function allocated(dataxml, arg)
    local x = arg[1]
    local y = arg[2]
    local areaname = arg[3]
    local framenumber = arg[4]
    publisher.setup_page(nil, "layout_functions#allocated",dataxml)
    return publisher.current_grid:isallocated(x, y, areaname, framenumber)
end

local function current_page(dataxml)
    publisher.setup_page(nil, "layout_functions#current_page",dataxml)
    return publisher.current_pagenumber
end

local function current_row(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#current_row",dataxml)
    return publisher.current_grid:current_row(arg and arg[1])
end

-- Evaluate the string arg as a dimension. The return value is a string with the dimension "sp".
local function dimexpression(dataxml, arg)
    arg = table.concat(arg)
    xpath.push_state()
    local ret = xpath.parse(dataxml, arg, "")
    xpath.pop_state()
    return ret
end

--- Get the page number of a marker
local function fnpagenumber(dataxml, arg)
    local m = publisher.markers[arg[1]]
    if m then
        return m.page
    else
        return nil
    end
end

local function current_column(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#current_column",dataxml)
    return publisher.current_grid:current_column(arg and arg[1])
end

local function alternating(dataxml, arg)
    local alt_type = arg[1]
    if not publisher.alternating[alt_type] then
        publisher.alternating[alt_type] = 1
    else
        publisher.alternating[alt_type] = math.fmod(publisher.alternating[alt_type], #arg - 1) + 1
    end
    local val = arg[publisher.alternating[alt_type] + 1]
    publisher.alternating_value[alt_type] = val
    return val
end

local function first_free_row(dataxml, arg)
    local ret = 0
    if arg and arg[1] then
        ret = publisher.current_grid:first_free_row(arg[1])
    end
    return ret
end

-- Get the first mark of a page (for example used in the head of dictionaires)
local function firstmark(dataxml, arg)
    local pagenumber = arg[1]
    if not tonumber(pagenumber) then err("firstmark: cannot get page number") end
    local minid = publisher.marker_min[pagenumber]
    if not minid then return "" end
    return publisher.marker_id_value[minid].name
end

-- Get the last mark of a page (for example used in the head of dictionaires)
local function lastmark(dataxml, arg)
    local pagenumber = arg[1]
    if not tonumber(pagenumber) then err("lasttmark: cannot get page number") end
    local maxid = publisher.marker_max[pagenumber]
    if not maxid then return "" end
    return publisher.marker_id_value[maxid].name
end

-- Read the contents given in arg[1] and write it to a temporary file.
-- Return the name of the file. Useful in conjunction with sd:decode-base64()
-- and Image to read an image from the data.
local function filecontents(dataxml, arg)
    local tmpdir = os.getenv("SP_TEMPDIR")
    if tmpdir == nil then
        err("SD_TEMPDIR is nil")
        return
    end
    lfs.mkdir(tmpdir)
    local filename = publisher.string_random(20)
    local path = tmpdir .. publisher.os_separator .. filename
    local file, e = io.open(path, "wb")
    if file == nil then
        err("Could not write filecontents into temp directory: %q", e)
        return nil
    end
    file:write(arg[1])
    file:close()
    return path
end

local function mode(dataxml, arg)
    local entry
    for _, v in pairs(arg) do
        entry = publisher.modes[v]
        if entry == true then return true end
    end
    return false
end

local function keepalternating(dataxml, arg)
    local alt_type = arg[1]
    return publisher.alternating_value[alt_type]
end


local function reset_alternating(dataxml, arg)
    local alt_type = arg[1]
    publisher.alternating[alt_type] = 0
end

local function number_of_datasets(dataxml, d)
    if not d then return 0 end
    local count = 0
    for i = 1, #d do
        if type(d[i]) == 'table' then
            count = count + 1
        end
    end
    return count
end

local function number_of_columns(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#number_of_columns",dataxml)
    return publisher.current_grid:number_of_columns(arg and arg[1])
end

--- Merge numbers like '1,2,3,4,5, 8, 9,10' into '1-5, 8-10'
local function merge_pagenumbers(dataxml, arg)
    local pagenumbers_string = string.gsub(arg[1] or "", "%s", "")
    local mergechar          = arg[2] or "–"
    local spacer             = arg[3] or ", "
    local interaction        = arg[4] or false

    local pagenumbers        = string.explode(pagenumbers_string, ",")
    -- let's remove duplicates now
    local dupes              = {}
    local withoutdupes       = {}
    local cap1, cap2
    for i = 1, #pagenumbers do
        local num = pagenumbers[i]
        cap1, cap2 = string.match(num, "^(.)-(.)$")
        if cap1 then
            for i = tonumber(cap1), tonumber(cap2) do
                num = tostring(i)
                if (not dupes[num]) then
                    withoutdupes[#withoutdupes + 1] = num
                    dupes[num] = true
                end
            end
        else
            if (not dupes[num]) then
                withoutdupes[#withoutdupes + 1] = num
                dupes[num] = true
            end
        end
    end
    publisher.stable_sort(withoutdupes, function(elta, eltb) return tonumber(elta) < tonumber(eltb) end)
    local gethyperlink
    if interaction then
        gethyperlink = function(pagenum) return { hyperlink = links_module.hlpage(publisher.options, pagenum) } end
    else
        gethyperlink = function(pagenum) return nil end
    end

    local p = par:new(nil, "merge-pagenumbers")
    if mergechar == "" then
        local pagenumber
        for i = 1, #withoutdupes - 1 do
            pagenumber = withoutdupes[i]
            p:append(visiblepagenumber(pagenumber), gethyperlink(pagenumber))
            p:append(spacer)
        end
        pagenumber = withoutdupes[#withoutdupes]
        p:append(visiblepagenumber(pagenumber), gethyperlink(pagenumber))
    else
        -- Buckets have consecutive pages. For example 1,2,3,4,5
        -- So when merging the numbers, we just have to look for the first and last
        -- entry in a bucket.
        local buckets = {}
        local bucket
        local cur
        local prev = -99
        for i = 1, #withoutdupes do
            cur = tonumber(withoutdupes[i])
            if cur == prev + 1 then
                -- same bucket
                bucket[#bucket + 1] = cur
            else
                bucket = { cur }
                buckets[#buckets + 1] = bucket
            end
            prev = cur
        end

        for i = 1, #buckets do
            if #buckets[i] > 2 then
                local from, to = buckets[i][1], buckets[i][#buckets[i]]
                p:append(visiblepagenumber(from), gethyperlink(from))
                p:append(mergechar)
                p:append(visiblepagenumber(to), gethyperlink(to))
            elseif #buckets[i] == 2 then
                local from, to = buckets[i][1], buckets[i][#buckets[i]]
                p:append(visiblepagenumber(from), gethyperlink(from))
                p:append(spacer)
                p:append(visiblepagenumber(to), gethyperlink(to))
            else
                local to = buckets[i][1]
                p:append(visiblepagenumber(to), gethyperlink(to))
            end
            if i < #buckets then
                p:append(spacer)
            end
        end
    end
    return p
end

local function number_of_rows(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#number_of_rows",dataxml)
    return publisher.current_grid:number_of_rows(arg and arg[1])
end

local function number_of_pages(dataxml, arg)
    local filename = arg[1]
    local img = publisher.imageinfo(filename)
    return img.img.pages
end

local function imagewidth(dataxml, arg)
    local filename, pagenumber, box, unit = get_filename_pagenum_box_unit_from_arg(arg)
    local img = publisher.imageinfo(filename, pagenumber, box)
    publisher.setup_page(nil, "layout_functions#imagewidth",dataxml)

    local width
    if unit then
        width = img.img.width
        local ret
        if unit == "cm" then
            ret = width / publisher.tenmm_sp
        elseif unit == "mm" then
            ret = width / publisher.onemm_sp
        elseif unit == "in" then
            ret = width / publisher.onein_sp
        elseif unit == "sp" then
            ret = width
        elseif unit == "pc" then
            ret = width / publisher.onepc_sp
        elseif unit == "pt" then
            ret = width / publisher.onept_sp
        elseif unit == "pp" then
            ret = width / publisher.onepp_sp
        elseif unit == "dd" then
            ret = width / publisher.onedd_sp
        elseif unit == "cc" then
            ret = width / publisher.onecc_sp
        else
            err("unsupported unit: %q, please use 'sp', 'pt', 'pc', 'cm', 'mm', 'in', 'dd' or 'cc'", unit)
        end
        return math.round(ret, 4)
    else
        width = publisher.current_grid:width_in_gridcells_sp(img.img.width)
        return width
    end
end

local function imageheight(dataxml, arg)
    local filename, pagenumber, box, unit = get_filename_pagenum_box_unit_from_arg(arg)
    local img = publisher.imageinfo(filename, pagenumber, box)
    publisher.setup_page(nil, "layout_functions#imageheight",dataxml)
    local height
    if unit then
        height = img.img.height
        local ret
        if unit == "cm" then
            ret = height / publisher.tenmm_sp
        elseif unit == "mm" then
            ret = height / publisher.onemm_sp
        elseif unit == "in" then
            ret = height / publisher.onein_sp
        elseif unit == "sp" then
            ret = height
        elseif unit == "pc" then
            ret = height / publisher.onepc_sp
        elseif unit == "pt" then
            ret = height / publisher.onept_sp
        elseif unit == "pp" then
            ret = height / publisher.onepp_sp
        elseif unit == "dd" then
            ret = height / publisher.onedd_sp
        elseif unit == "cc" then
            ret = height / publisher.onecc_sp
        else
            err("unsupported unit: %q, please use 'sp', 'pt', 'pc', 'cm', 'mm', 'in', 'dd' or 'cc'", unit)
        end
        return math.round(ret, 4)
    else
        height = publisher.current_grid:height_in_gridcells_sp(img.img.height)
        return height
    end
end

local function file_exists(dataxml, arg)
    local filename = arg[1]
    if not filename then return false end
    if filename == "" then return false end
    return kpse.find_file(filename) ~= nil
end

--- Insert 1000's separator and comma separator
local function format_number(dataxml, arg)
    local num, thousandssep, commasep = arg[1], arg[2], arg[3]
    local sign, digits, commadigits = string.match(tostring(num), "([%-%+]?)(%d*)%.?(%d*)")
    local first_digits = math.fmod(#digits, 3)
    local ret = {}
    if first_digits > 0 then
        ret[1] = string.sub(digits, 0, first_digits)
    end
    for i = 1, (#digits - first_digits) / 3 do
        ret[#ret + 1] = string.sub(digits, first_digits + (i - 1) * 3 + 1, first_digits + i * 3)
    end
    local retstr = table.concat(ret, thousandssep)
    if commadigits and #commadigits > 0 then
        return sign .. retstr .. commasep .. commadigits
    else
        return sign .. retstr
    end
end

local function format_string(dataxml, arg)
    local argument = {}
    for i = 1, #arg - 1 do
        argument[#argument + 1] = table_textvalue(arg[i])
    end
    local unpacked = table.unpack(argument)
    if unpacked == nil or unpacked == "" then
        err("format-string: first arguments are empty")
        return ""
    end
    local ret = string.format(arg[#arg], unpacked)
    return ret
end


local function even(dataxml, arg)
    if not tonumber(arg[1]) then
        err("sd:even() - argument is not a number")
        return false
    end
    return math.fmod(arg[1], 2) == 0
end

local function current_frame_number(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#current_framenumber",dataxml)
    local framename = arg[1]
    if framename == nil then return 1 end
    local current_framenumber = publisher.current_grid:framenumber(framename)
    return current_framenumber
end

local function groupheight(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#groupheight",dataxml)
    local groupname = arg[1]
    if not publisher.groups[groupname] then
        err("Can't find group with the name %q", groupname)
        return 0
    end

    local groupcontents = publisher.groups[groupname].contents
    if not groupcontents then
        err("Can't find group with the name %q", groupname)
        return 0
    end
    local height
    local unit = arg[2]
    if unit then
        height = groupcontents.height
        local ret
        if unit == "cm" then
            ret = height / publisher.tenmm_sp
        elseif unit == "mm" then
            ret = height / publisher.onemm_sp
        elseif unit == "in" then
            ret = height / publisher.onein_sp
        elseif unit == "sp" then
            ret = height
        elseif unit == "pc" then
            ret = height / publisher.onepc_sp
        elseif unit == "pt" then
            ret = height / publisher.onept_sp
        elseif unit == "pp" then
            ret = height / publisher.onepp_sp
        elseif unit == "dd" then
            ret = height / publisher.onedd_sp
        elseif unit == "cc" then
            ret = height / publisher.onecc_sp
        else
            err("unsupported unit: %q, please use 'sp', 'pt', 'pc', 'cm', 'mm', 'in', 'dd' or 'cc'", unit)
        end
        return math.round(ret, 4)
    else
        local grid = publisher.current_grid
        height = grid:height_in_gridcells_sp(groupcontents.height)
        return height
    end
end

local function groupwidth(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#groupwidth",dataxml)
    local groupname = arg[1]
    if not publisher.groups[groupname] then
        err("Can't find group with the name %q", groupname)
        return 0
    end
    local groupcontents = publisher.groups[groupname].contents

    if not groupcontents then
        err("Can't find group with the name %q", groupname)
        return 0
    end
    local unit = arg[2]
    local width
    if unit then
        width = groupcontents.width
        local ret
        if unit == "cm" then
            ret = width / publisher.tenmm_sp
        elseif unit == "mm" then
            ret = width / publisher.onemm_sp
        elseif unit == "in" then
            ret = width / publisher.onein_sp
        elseif unit == "sp" then
            ret = width
        elseif unit == "pc" then
            ret = width / publisher.onepc_sp
        elseif unit == "pt" then
            ret = width / publisher.onept_sp
        elseif unit == "pp" then
            ret = width / publisher.onepp_sp
        elseif unit == "dd" then
            ret = width / publisher.onedd_sp
        elseif unit == "cc" then
            ret = width / publisher.onecc_sp
        else
            err("unsupported unit: %q, please use 'sp', 'pt', 'pc', 'cm', 'mm', 'in', 'dd' or 'cc'", unit)
        end
        return math.round(ret, 4)
    else
        local grid = publisher.current_grid
        width = grid:width_in_gridcells_sp(groupcontents.width)
        return width
    end
end


local function odd(dataxml, arg)
    local num = arg[1]
    if not tonumber(num) then
        err("sd:odd() - argument is not a number")
        return false
    end
    return math.fmod(num, 2) ~= 0
end

local function variable(dataxml, arg)
    local varname = table.concat(arg)
    local var = publisher.xpath.get_variable(varname)
    return var
end

local function attr(dataxml, arg)
    local attname = table.concat(arg)
    local att = dataxml[attname]
    return att
end

local function variable_exists(dataxml, arg)
    local var = publisher.xpath.get_variable(arg[1])
    return var ~= nil
end

-- SHA-1
local function shaone(dataxml, arg)
    local message = table.concat(arg)
    local ret = sha.sha1(message)
    return ret
end

local function sha256(dataxml, arg)
    local message = table.concat(arg)
    local ret = sha.sha256(message)
    return ret
end

local function sha512(dataxml, arg)
    local message = table.concat(arg)
    local ret = sha.sha512(message)
    return ret
end

local function md5(dataxml, arg)
    local message = table.concat(arg)
    local ret = sha.md5(message)
    return ret
end

-- convert a textual dimension (e.g. '2cm') to a scalar in another dimension.
local function tounit(dataxml, arg)
    local unit = arg[1]
    local value = arg[2]
    local decimal = arg[3] or 0
    local width = tex.sp(arg[2])
    local ret
    if unit == "cm" then
        ret = width / publisher.onecm_sp
    elseif unit == "mm" then
        ret = width / publisher.onemm_sp
    elseif unit == "in" then
        ret = width / publisher.onein_sp
    elseif unit == "sp" then
        ret = width
    elseif unit == "pc" then
        ret = width / publisher.onepc_sp
    elseif unit == "pt" then
        ret = width / publisher.onept_sp
    elseif unit == "pp" then
        ret = width / publisher.onepp_sp
    elseif unit == "dd" then
        ret = width / publisher.onedd_sp
    elseif unit == "cc" then
        ret = width / publisher.onecc_sp
    else
        err("unsupported unit: %q, please use 'sp', 'pt', 'pc', 'cm', 'mm', 'in', 'dd' or 'cc'", unit)
    end
    return math.round(ret, decimal)
end

local function markdown(dataxml, arg)
    if arg == nil then
        arg = dataxml
    end
    local str = table_textvalue(arg[1])
    local htmltext = splib.markdown(str)
    if htmltext then
        htmltext = publisher.splib.htmltoxml(htmltext)
        local ret = luxor.parse_xml("<dummy><dummy>" .. htmltext .. "</dummy></dummy>")
        return { ret }, nil
    end
    return {}, nil
end


-- Turn escaped HTML into table. Uses Go XML parser + CSS
local function html(dataxml, arg)
    if arg == nil then
        arg = dataxml
    end
    arg = table_textvalue(arg)
    local tab = splib.parse_html_text(arg, publisher.css:gettext())
    if type(tab) == "string" then
        local a, b = load(tab)
        if a then
            a()
        else
            err(b)
            return
        end
        return { csshtmltree }
    end
end

-- Turn &lt;b&gt;Hello&lt;b /&gt; into an HTML table and then into XML structure.
local function decode_html(dataxml, arg)
    if arg == nil then
        arg = dataxml
    end
    arg = table_textvalue(arg)
    if type(arg) == "string" then
        local msg = publisher.splib.htmltoxml(arg)
        if msg == nil then
            err("decode-html failed")
            return nil
        end
        -- two dummy tags because xpath.parse_raw removes the surrounding table
        local ret = luxor.parse_xml("<dummy><dummy>" .. msg .. "</dummy></dummy>")
        return ret
    end
end

local function decode_base64(dataxml, arg)
    local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    local data = tostring(arg[1])
    data = string.gsub(data, '[^' .. b .. '=]', '')
    return (data:gsub('.', function(x)
        if (x == '=') then return '' end
        local r, f = '', (b:find(x) - 1)
        for i = 6, 1, -1 do r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and '1' or '0') end
        return r;
    end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
        if (#x ~= 8) then return '' end
        local c = 0
        for i = 1, 8 do c = c + (x:sub(i, i) == '1' and 2 ^ (8 - i) or 0) end
        return string.char(c)
    end))
end

local function count_saved_pages(dataxml, arg)
    local tmp = publisher.pagestore[arg[1]]
    if not tmp then
        err("count-saved-pages(): no saved pages found. Return 0")
        return 0
    else
        return #tmp
    end
end

local function randomitem(dataxml, arg)
    local x = math.random(#arg)
    return arg[x]
end

local function romannumeral(dataxml, arg)
    return tex.romannumeral(arg[1])
end

local function aspectratio(dataxml, arg)
    local filename, pagenumber, box, _ = get_filename_pagenum_box_unit_from_arg(arg)
    local img = publisher.imageinfo(filename, pagenumber, box)
    return img.img.xsize / img.img.ysize
end

local function pageheight(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#pageheight",dataxml)
    local unit = arg[1] or "mm"
    if unit then
        local width = publisher.current_page.height
        local ret
        if unit == "cm" then
            ret = width / publisher.tenmm_sp
        elseif unit == "mm" then
            ret = width / publisher.onemm_sp
        elseif unit == "in" then
            ret = width / publisher.onein_sp
        elseif unit == "sp" then
            ret = width
        elseif unit == "pc" then
            ret = width / publisher.onepc_sp
        elseif unit == "pt" then
            ret = width / publisher.onept_sp
        elseif unit == "pp" then
            ret = width / publisher.onepp_sp
        elseif unit == "dd" then
            ret = width / publisher.onedd_sp
        elseif unit == "cc" then
            ret = width / publisher.onecc_sp
        else
            err("unsupported unit: %q, please use 'sp', 'pt', 'pc', 'cm', 'mm', 'in', 'dd' or 'cc'", unit)
        end
        return math.round(ret, 0)
    end
end


local function pagewidth(dataxml, arg)
    publisher.setup_page(nil, "layout_functions#pagewidth",dataxml)
    local unit = arg[1] or "mm"
    if unit then
        local width = publisher.current_page.width
        local ret
        if unit == "cm" then
            ret = width / publisher.tenmm_sp
        elseif unit == "mm" then
            ret = width / publisher.onemm_sp
        elseif unit == "in" then
            ret = width / publisher.onein_sp
        elseif unit == "sp" then
            ret = width
        elseif unit == "pc" then
            ret = width / publisher.onepc_sp
        elseif unit == "pt" then
            ret = width / publisher.onept_sp
        elseif unit == "pp" then
            ret = width / publisher.onepp_sp
        elseif unit == "dd" then
            ret = width / publisher.onedd_sp
        elseif unit == "cc" then
            ret = width / publisher.onecc_sp
        else
            err("unsupported unit: %q, please use 'sp', 'pt', 'pc', 'cm', 'mm', 'in', 'dd' or 'cc'", unit)
        end
        return math.round(ret, 0)
    end
end

local function visible_pagenumber(dataxml, arg)
    return visiblepagenumber(arg[1])
end

local function loremipsum(dataxml, arg)
    local count = arg and arg[1] or 1
    local lorem = [[
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
        veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
        commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
        velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
        occaecat cupidatat non proident, sunt in culpa qui officia deserunt
        mollit anim id est laborum.
    ]]
    return string.rep(lorem:gsub("^%s*(.-)%s*$", "%1"):gsub("[%s\n]+", " "), count, " ")
end

local sdns = "urn:speedata:2009/publisher/functions/en"


local register = publisher.xpath.register_function
register(sdns, "attr", attr)
register(sdns, "dimexpr", dimexpression)
register(sdns, "alternating", alternating)
register(sdns, "aspectratio", aspectratio)
register(sdns, "count-saved-pages", count_saved_pages)
register(sdns, "current-page", current_page)
register(sdns, "current-row", current_row)
register(sdns, "current-framenumber", current_frame_number)
register(sdns, "current-column", current_column)
register(sdns, "html", html)
register(sdns, "decode-html", decode_html)
register(sdns, "decode-base64", decode_base64)
register(sdns, "dummytext", loremipsum)
register(sdns, "loremipsum", loremipsum)
register(sdns, "even", even)
register(sdns, "first-free-row", first_free_row)
register(sdns, "firstmark", firstmark)
register(sdns, "lastmark", lastmark)
register(sdns, "filecontents", filecontents)
register(sdns, "file-exists", file_exists)
register(sdns, "format-number", format_number)
register(sdns, "format-string", format_string)
register(sdns, "group-height", groupheight)
register(sdns, "groupheight", groupheight)
register(sdns, "group-width", groupwidth)
register(sdns, "groupwidth", groupwidth)
register(sdns, "imagewidth", imagewidth)
register(sdns, "imageheight", imageheight)
register(sdns, "markdown", markdown)
register(sdns, "mode", mode)
register(sdns, "allocated", allocated)
register(sdns, "keep-alternating", keepalternating)
register(sdns, "merge-pagenumbers", merge_pagenumbers)
register(sdns, "number-of-datasets", number_of_datasets)
register(sdns, "number-of-rows", number_of_rows)
register(sdns, "number-of-columns", number_of_columns)
register(sdns, "number-of-pages", number_of_pages)
register(sdns, "odd", odd)
register(sdns, "pagenumber", fnpagenumber)
register(sdns, "pagewidth", pagewidth)
register(sdns, "pageheight", pageheight)
register(sdns, "randomitem", randomitem)
register(sdns, "romannumeral", romannumeral)
register(sdns, "reset_alternating", reset_alternating) -- backward comp.
register(sdns, "reset-alternating", reset_alternating)
register(sdns, "sha1", shaone)
register(sdns, "sha256", sha256)
register(sdns, "sha512", sha512)
register(sdns, "md5", md5)
register(sdns, "todimen", tounit)
register(sdns, "tounit", tounit)
register(sdns, "variable", variable)
register(sdns, "variable-exists", variable_exists)
register(sdns, "visible-pagenumber", visible_pagenumber)

file_end("layout_functions.lua")
