Documentation for this module may be created at Module:BladeEngine/doc

--
-- BLADE ENGINE v.5.0.0
-- Engine for handling data modules
-- Documentation: [[Module talk:BladeEngine]]
--
local p = {}
local q = {}
local html = mw.html
local getArgs = require("Dev:Arguments").getArgs
local image = require "Module:Image"

function formatnumberfunction(a, b)
    --expedite the process
    if not b or not (
        b.signs
        or b.dp
        or b.padleft
        ) then
        return tostring(a)
    end

    --resolve the sign
    local sign = ""
    if a < 0 then sign = "-"
    elseif b.signs then
        if a == 0 then sign = "±"
        else sign = "+"
        end
    end

    a = math.abs(a)

    --expedite the process
    if not (b.dp or b.padleft) then
        return sign .. a
    end

    local fig, dec = math.floor(a), a%1
    --round away from 0
    if b and b.dp then
        dec = math.floor((dec * 10^b.dp) + 0.5)
        if #tostring(dec) > b.dp then
            fig = fig + 1
            dec = ""
        end
        dec = dec .. ("0"):rep(b.dp):sub(#tostring(dec)+1)
    else
        dec = tostring(dec):sub(3)
    end
    if b and b.padleft then
        fig = ("0"):rep(b.padleft):sub(#tostring(fig)) .. fig
    end
    local out = sign .. fig
    if dec ~= "" then
        out = out .. "." .. tostring(html.create("span"):css("font-size", "90%"):wikitext(dec))
    end

    return out
end

local datatypelist = {
    ["string"] = {
        formats = {
            default = {
                func = function(a)
                    return a
                end
            },
            quote = {
                func = function(a)
                    return '<q>' .. a .. '</q>'
                end,
                textAlign = 'left',
            },
            longtext = {
                func = function(a)
                    return a
                end,
                textAlign = 'left',
            },
        },
        textAlign = 'center',
        parse = function(a) return tostring(a) end
    },
    ["number"] = {
        formats = {
            default = {
                func = formatnumberfunction
            },
            percent = {
                func = function(a, b)
                    return formatnumberfunction(a, b) .. "%"
                end,
                textAlign = "right",
            }
        },
        textAlign = 'center',
        parse = function(a) return tonumber(a) end
    },
    ["boolean"] = {
        formats = {
            default = {
                func = function(a)
                    if a then return "true" end
                    return "false"
                end
            },
            yesno = {
                func = function(a)
                    if a then return "yes" end
                    return "no"
                end
            },
            bit = {
                func = function(a)
                    if a then return "1" end
                    return "0"
                end
            },
            tickcross = {
                func = function(a)
                    x = {
                        [true] = {"checkmark", "Yes", "Y"},
                        [false] = {"xmark", "No", "N"}
                    }
                    return '<span class="'
                    .. x[a][1] .. '" title="'
                    .. x[a][2] .. '"></span><span style="display:none">'
                    .. x[a][3] .. '</span>'
                end
            }
        },
        textAlign = "center",
        parse = function(a)
            if a:lower() == "true" then
                a = true
            elseif a:lower() == "false" then
                a = false
            else
                a = nil
            end
        end
    },
    ["list"] = {
        formats = {
            default = {
                func = function(a, b)
                    local separator = ", "
                    if b and b.separator then
                        if b.separator == "space" then
                            separator = " "
                        elseif b.separator == "semicolon" then
                            separator = "; "
                        elseif b.separator == "slash" then
                            separator = "/"
                        elseif b.separator == "dash" then
                            separator = " - "
                        elseif b.separator == "newline" then
                            separator = "<br/>"
                        end
                    end
                    local new = {}
                    for _, v in ipairs(a) do
                        table.insert(new, v)
                    end
                    return table.concat(new, separator)
                end
            },
            ordered = {
                func = function(a)
                    if not a[1] then return "" end
                    ol = html.create("ol")
                    for _, v in ipairs(a) do
                        ol:tag("tl"):wikitext(v)
                    end
                    return tostring(ol)
                end
            }
        },
        textAlign = "left",
        parse = function(a)
            --an entire list cannot be queried, only its items. this shouldn't get used
            return a
        end
    },
    ["image"] = {
        formats = {
            default = {
                func = function(a, b)
                    if not a then return nil end
                    local img = image.new(a)
                    if b and b.maxwidth then
                        img:setWidth(b.maxwidth)
                    end
                    if b and b.maxheight then
                        img:setWidth(b.maxheight)
                    end
                    return img
                end
            }
        },
        textAlign = "center",
        parse = function(a)
            return image.new(a):getFilename()
        end
    }
}

local fielddefinitionlist = {
    datatype = {
        datatype = "string",
    },
    format = {
        datatype = "string",
    },
    title = {
        datatype = "string",
    },
    description = {
        datatype = "string"
    },
    default = {
        datatype = "value"
    },
    unique = {
        datatype = "boolean"
    },
    required = {
        datatype = "boolean"
    },
    fromlist = {
        datatype = "list<value>"
    },
    source = {
        datatype = "string"
    },
    dp = {
        datatype = "number",
    },
    padleft = {
        datatype = "number",
    },
    signs = {
        datatype = "boolean"
    },
    min = {
        datatype = "number",
    },
    max = {
        datatype = "number",
    },
    maxdp = {
        datatype = "number"
    },
    mask = {
        datatype = "string"
    },
    length = {
        datatype = "number",
    },
    minlength = {
        datatype = "number",
    },
    maxlength = {
        datatype = "number",
    },
    count = {
        datatype = "number",
    },
    mincount = {
        datatype = "number",
    },
    maxwidth = {
        datatype = "number",
    },
    maxheight = {
        datatype = "number",
    },
    extensions = {
        datatype = "list<string>"
    },
    separator = {
        datatype = "string"
    }
}

function p.new(obj, modulename)
    p.modulename = modulename
    p.codename = modulename:match("^.*:([^_]*) .+$")
    for k,v in pairs(q) do
        obj[k] = v
    end
    return p
end

function japanese(args)
    return require "Module:Japanese".initJ(args)
end

function foot(args)
    return require "Module:Foot".render(args)
end

function staticclone(table)
    local t = {}
    for k, v in pairs(table) do
        if type(v)=="table" then
            t[k] = staticclone(v)--no recursion protection
        elseif type(v)=="function" then
            mw.log("WHAT A FUNCTION DOIN HERE?")
        else
            t[k] = v
        end
    end
    return t
end

--[[
  this function isn't currently used, but is a batch version of
  Module:Image's isExist. This would be used if it turns out repeated
  uses of preprocess is a bit bad. but this is only done in errorcheck
  so if it's not too bad then it doesn't matter

function massfileexists(image)
    local returnfirst = false
    if type(image) == "string" then
        image = { image }
        returnfirst = true
    end

    local frame = mw.getCurrentFrame()
    local existarray = {}

    if frame then
        --packing up the images to do the preprocesses in one go
        local pre = "{{filepath:"
        local suf = "}}"
        local delim = "|"
        local parsetext = pre..table.concat(image, suf..delim..pre)..suf
        local urllist = mw.text.split(frame:preprocess(parsetext), "|")
        for i=1, #urllist do
            existarray[i] = urllist[i]~=""
        end
    else
        --you're in console, we'll just lie
        for i=1, #image do
            existarray[i] = true
        end
    end
    return returnfirst and existarray[1] or existarray
end
--]]

function p.fieldtitle(data, field)
    local fieldtitle, fielddesc = field
    if data.fields and data.fields[field]
    then
        fieldtitle = data.fields[field].title or field
        fielddesc = data.fields[field].description
    end
    if fielddesc then return tostring(foot({fieldtitle, fielddesc})) end
    return fieldtitle
end

function p.formataltjpnname(data, entry, jpnfield, romfield, litfield)
    --data doesn't do anything here, but for consistency
    if entry[litfield] then
        local jp = tostring(japanese({entry[jpnfield], entry[romfield], fmt="j(r)"}))
        return tostring(foot({entry[litfield], "Name in Japanese: " .. jp}))
    end
end

function p.formatlink(data, entry, linkfield, namefield)
    if entry[linkfield] then
        return "[[" .. entry[linkfield] .. "|" .. entry[namefield] .. "]]"
    end
    return entry[namefield]
end

function p.listlink(data, entry, anchorfield, namefield)
    if not namefield then namefield = anchorfield end
    local mainpage = data.mainpage
    if mainpage then
        return "[[" .. mainpage .. "#" .. entry[anchorfield] .. "|" .. entry[namefield] .. "]]"
    end
    return entry[namefield]
end

function p.formatfield(data, entry, field)
    if type(entry) == "number" then entry = data[entry] end
    if not data.fields
    or not data.fields[field]
    or not data.fields[field].datatype
    then
        return entry[field]
    else
        return p.format(entry[field], data.fields[field])
    end
end

function p.cellformatfield(data, field)
    if not data.fields
    or not data.fields[field]
    then
        return {}
    else
        local datatype = data.fields[field].datatype or "string"
        local typeformat = data.fields[field].format

        return p.cellformat(datatype, typeformat)
    end
end

function p.cellformat(datatype, typeformat)
    if not datatype then datatype = "string" end
    if not typeformat then typeformat = "default" end
    datatype = datatypelist[datatype]
    typeformat = datatype.formats[typeformat]
    return {style = "text-align:" .. (typeformat.textAlign or datatype.textAlign) }
end

function p.format(data, datatype, typeformat, modifiers)
    -- 3 overloads
    ----data, field
    ----data, datatype(, typeformat, modifiers)
    ----data, typeformat(, modifiers)
    if type(datatype) == "table" then
        modifiers = datatype
        datatype = modifiers.datatype
        typeformat = modifiers.format
    elseif datatypelist[datatype] then
    else
        modifiers = typeformat
        typeformat = datatype
        datatype = type(data)
    end
    if not typeformat then typeformat = "default" end
    if data == nil then data = modifiers.default end
    if data == nil then return "" end
    return datatypelist[datatype].formats[typeformat].func(data, modifiers)

end

function p.select(data, args, parameters)

    if type(data) == "string" then
        data = mw.loadData(p.modulename .. "/" .. data)
    end

    local keyfield = args["select by"] or data.keyfield

    --composite keyfield
    if keyfield:match("::") then
        local tmp = {}

        for part in mw.text.gsplit(keyfield, "::") do
            table.insert(tmp, mw.text.trim(part))
        end

        keyfield = tmp
    end

    local sortreverse = false

    local sortfield = args["sort by"]

    if sortfield then
        sortreverse = sortfield:match("^[^ ]* (.*)$")
        if sortreverse then
            sortreverse = sortreverse:upper()=="DESC"
        end
        sortfield = sortfield:match("^([^ ]*)")
    end

    local fieldinfo = data.fields or {}

    local defaultfield = { datatype = "string" }

    if parameters then
        --using keys prevents duplicates
        local newp = {}
        for i=1, #parameters do
            local thisp = parameters[i]
            newp[parameters[i]] = staticclone(fieldinfo[thisp] or defaultfield)
        end
        parameters = newp
    else
        parameters = staticclone(fieldinfo)
    end

    local select = {}

    if args[1] then
        local selectednames = {}

        for part in mw.text.gsplit(args[1], ";") do
            if not part:find("^%s+$") then
                table.insert(selectednames, mw.text.trim(part))
            end
        end

        for i=1,#selectednames do
            --this makes things easier so i don't have to repeatedly search
            --the array
            selectednames[selectednames[i]] = i
        end
        if type(keyfield)=="table" then
            --composite key
            local defaults = {}
            for _,key in ipairs(keyfield) do
                defaults[key] = (fieldinfo[key] or {default = ""}).default
            end
            for i,_ in ipairs(data) do
                local item = data[i]
                local comparison = {}
                local compdefault = {}
                for j,key in ipairs(keyfield) do
                    local keyval = item[key] or defaults[key]
                    table.insert(compdefault, keyval == defaults[key])
                    table.insert(comparison, keyval)
                end
                for j=#keyfield, 1, -1 do
                    local key = keyfield[j]
                    local test = table.concat(comparison, "::", 1, j)
                    local match = selectednames[test]
                    if match then
                        select[match] = { i, item[sortfield] }
                        break
                    end
                    if not compdefault[j] then break end
                end
            end
        else
            --single field keyfield
            for i,_ in ipairs(data) do
                local item = data[i]
                local match = selectednames[item[keyfield]]
                if match then
                    select[match] = { i, item[sortfield] }
                end
            end
        end
    else
        local checks = {}

        local opfuncs = {
            eq = function(a, b) return a == b end,
            lt = function(a, b) return a < b end,
            gt = function(a, b) return a > b end,
            ne = function(a, b) return a ~= b end,
            md = function(a, b) return a % b == 0 end,
            cn = function(a, b) return a:find(b, 1, true) end,
            sw = function(a, b) return a:sub(1, #b) == b end,
            ew = function(a, b) return a:sub(-#b) == b end,
            ac = function(a, b)
                for _, v in ipairs(a) do
                    if v == b then return true end
                end
            end,
            an = function(a, b)
                for _, v in ipairs(a) do
                    if v == b then return false end
                end
                return true
            end,
            iq = function(a, b) return
                image.new(a):getFilename() ==
                image.new(b):getFilename()
            end,
            ni = function(a, b) return
                image.new(a):getFilename() ~=
                image.new(b):getFilename()
            end,
        }
        local numops = {
            ["="] = "eq",
            ["<"] = "lt",
            [">"] = "gt",
            ["!"] = "ne",
            ["%"] = "md"
        }
        local strops = {
            ["="] = "eq",
            ["!"] = "ne",
            ["^"] = "sw",
            ["*"] = "cn",
            ["$"] = "ew"
        }
        local lstops = {
            ["="] = "ac",
            ["!"] = "an"
        }
        local imgops = {
            ["="] = "iq",
            ["!"] = "ni"
        }
        for k, _ in pairs(parameters) do
            local datatype = "string"
            if fieldinfo and fieldinfo[k] and fieldinfo[k].datatype then
                datatype = fieldinfo[k].datatype
            end
            if args[k] and mw.text.trim(args[k]):lower() == "nil" then
                table.insert(checks, {
                    key = k,
                    op = "nil"
                })
            elseif datatype == "number" then
                if args[k] then
                    local opsign = mw.text.trim(args[k]):sub(1,1)
                    if numops[opsign] then
                        table.insert(checks, {
                            key = k,
                            op = numops[opsign],
                            value = tonumber(mw.text.trim(mw.text.trim(args[k]):sub(2)))
                        })
                    else
                        table.insert(checks, {
                            key = k,
                            op = "eq",
                            value = tonumber(mw.text.trim(args[k]))
                        })
                    end
                end
            elseif datatype == "string" then
                if args[k] then
                    local opsign = mw.text.trim(args[k]):sub(1,1)
                    if strops[opsign] then
                        table.insert(checks, {
                            key = k,
                            op = strops[opsign],
                            value = mw.text.trim(mw.text.trim(args[k]):sub(2))
                        })
                    else
                        table.insert(checks, {
                            key = k,
                            op = "eq",
                            value = mw.text.trim(args[k])
                        })
                    end
                end
            elseif datatype == "image" then
                if args[k] then
                    local opsign = mw.text.trim(args[k]):sub(1,1)
                    if strops[opsign] then
                        table.insert(checks, {
                            key = k,
                            op = strops[opsign],
                            value = mw.text.trim(mw.text.trim(args[k]):sub(2))
                        })
                    else
                        table.insert(checks, {
                            key = k,
                            op = "iq",
                            value = mw.text.trim(args[k])
                        })
                    end
                end
            elseif datatype == "list" then
                if args[k] then
                    local opsign = mw.text.trim(args[k]):sub(1,1)
                    if strops[opsign] then
                        table.insert(checks, {
                            key = k,
                            op = lstops[opsign],
                            value = mw.text.trim(mw.text.trim(args[k]):sub(2))
                        })
                    else
                        table.insert(checks, {
                            key = k,
                            op = "ac",
                            value = mw.text.trim(args[k])
                        })
                    end
                end
            elseif datatype == "boolean" then
                if args[k] then
                    if mw.text.trim(args[k]):upper() == "TRUE" then
                        table.insert(checks, {
                            key = k,
                            op = "eq",
                            value = true
                        })
                    else op = "eq"
                        table.insert(checks, {
                            key = k,
                            op = "eq",
                            value = false
                        })
                    end
                end
            end
        end
        for i, _ in ipairs(data) do
            local item = data[i]
            local passed = true
            for j=1, #checks do
                local par = checks[j].value

                local default
                local parfieldinfo = fieldinfo and fieldinfo[checks[j].key] or nil
                if parfieldinfo then
                    default = parfieldinfo.default
                end

                local val = item[checks[j].key]
                if val == nil then
                    val = default
                end

                local op = checks[j].op

                local foundmatch
                if op == "nil" then
                    foundmatch = val==nil
                elseif val == nil then
                    foundmatch = false
                else
                    foundmatch = opfuncs[op](val, par)
                end
                if not foundmatch then
                    passed = false break
                elseif foundmatch and unique and fieldinfo and fieldinfo[par] and fieldinfo[par].unique then
                    break
                end
            end
            if passed then table.insert(select, {i, item[sortfield]}) end
        end
    end

    if sortfield then
        if sortreverse then
            table.sort(select, function(a, b) return a[2] > b[2] end)
        else
            table.sort(select, function(a, b) return a[2] < b[2] end)
        end
    end

    outdata = {}
    for i=1, #select do
        local item = {}
        for k, _ in pairs(parameters) do
            local stats = data[select[i][1]]

            local default
            local parfieldinfo = fieldinfo and fieldinfo[k] or nil
            if parfieldinfo then
                default = parfieldinfo.default
            end

            --clone table to prevent editing errors
            local newdata
            if type(stats[k]) == "table" then
                newdata = {}
                for k, v in pairs(stats[k]) do
                    newdata[k] = v
                end
            else
                newdata = stats[k]
            end
            item[k] = (function() if newdata~=nil then return newdata end return default end)()
        end
        table.insert(outdata, item)
    end
    outdata.fields = parameters
    return outdata, select
end

function q.createtable(frame)
    local args = getArgs(frame)
    local subpage, head, cols, rows = args[1], {}, {}, {}
    local modulename = p.modulename .. "/" .. subpage
    local codename = p.codename or "series"
    local data = mw.loadData(modulename)

    for part in mw.text.gsplit(args["th"] or "", ";") do
        if not part:find("^%s+$") then
            table.insert(head, mw.text.trim(part))
        end
    end

    for part in mw.text.gsplit(args["td"] or "", ";") do
        if not part:find("^%s+$") then
            table.insert(cols, mw.text.trim(part))
        end
    end

    for part in mw.text.gsplit(args["colspan"] or "", ";") do
        if not part:find("^%s+$") then
            table.insert(rows, mw.text.trim(part))
        end
    end

    args[1] = args[2]

    return p.table(data, args, modulename, nil, codename, head, cols, rows)
end

function q.rawget(frame)
    local args = getArgs(frame)
    local subpage, field, nilvalue = args[1], args[2], args["nil value"]
    local modulename = p.modulename .. "/" .. subpage
    local data = mw.loadData(modulename)

    args[1] = args[3]

    local querydata = p.select(data, args)

    local value = querydata[1][field]

    if value == nil then
        return nilvalue
    end

    return value
end

function q.matches(frame)
    local args = getArgs(frame)
    local subpage, displaydnav = args[1], args.dnav and args.dnav=="true"
    local modulename = p.modulename .. "/" .. subpage
    local data = mw.loadData(modulename)

    args[1] = args[2]

    local querydata = p.select(data, args)

    return (displaydnav and p.dnav(data, modulename) or "") .. tostring(#querydata)
end

function q.value(frame)
    local args = getArgs(frame)
    local subpage, field, displaydnav = args[1], args[2], args.dnav and args.dnav=="true"
    local modulename = p.modulename .. "/" .. subpage
    local data = mw.loadData(modulename)

    args[1] = args[3]

    local querydata = p.select(data, args)

    if #querydata == 0 then
        return '<strong class="error">No matches found</strong>'
    end

    local formattingvalues
    if data.fields and data.fields[field] then
        formattingvalues = staticclone(data.fields[field])
    else
        formattingvalues = {}
    end

    for k, v in pairs(args) do
        if type(k)=="string" and k:sub(1, 6) == "value " then
            local propname = k:sub(7)
            local propdata = fielddefinitionlist[propname]
            if propdata then
                local propdtype = propdata.datatype
                if propdtype ~= "value"
                and propdtype:sub(1, 4) ~= "list"
                then
                    if propdtype == "number" then
                        v = tonumber(v)
                    elseif propdtype == "boolean" then
                        if v:lower() == "true" then
                            v = true
                        elseif v:lower() == "false" then
                            v = false
                        else
                            v = nil
                        end
                    end
                end
            end
            formattingvalues[propname] = v
        end
    end

    return (displaydnav and p.dnav(data, modulename) or "") .. p.format(querydata[1][field], formattingvalues.datatype, formattingvalues.format, formattingvalues)
end

function q.dnav(frame)
    local args = getArgs(frame)
    local subpage = args[1]

    local modulename = p.modulename .. "/" .. subpage
    local data = mw.loadData(modulename)--it has some values in its header, +if you want the dnav you probably wanna query the data anyway

    return p.dnav(data, modulename)
end

function q.createlist(frame)
    local args = getArgs(frame)
    local subpage, head, prop = args[1], {}, {}
    local modulename = p.modulename .. "/" .. subpage
    local codename = p.codename or "series"
    local data = mw.loadData(modulename)

    if args["th"] then
        for part in mw.text.gsplit(args["th"] or "", ";") do
            if not part:find("^%s+$") then
                table.insert(head, mw.text.trim(part))
            end
        end
    end

    if args["td"] then
        for part in mw.text.gsplit(args["td"] or "", ";") do
            if not part:find("^%s+$") then
                table.insert(prop, mw.text.trim(part))
            end
        end
    end

    args[1] = args[2]

    local listparams = {}

    for k, v in pairs(args) do
        local listparam = k:match("^list (.*)")
        if listparam then listparams[listparam] = v end
    end

    local fielddata = data.fields

    local querydata = p.select(data, args)

    for i=1, #querydata do
        local itm = querydata[i]

        headdata = {}
        propdata = {}

        for j=1, #head do
            local fieldvalue = p.formatfield(data, itm, head[j])
            if fielddata[head[j]].datatype == "image" then
                fieldvalue:setHeight(20)
                fieldvalue = tostring(fieldvalue)
            end
            table.insert(headdata, fieldvalue)
        end
        for j=1, #prop do
            local fieldvalue = p.formatfield(data, itm, prop[j])
            if fielddata[prop[j]].datatype == "image" then
                fieldvalue:setHeight(20)
                fieldvalue = tostring(fieldvalue)
            end
            table.insert(propdata, fieldvalue)
        end
        local itemtext = ""
        if headdata[1] then
            itemtext = '<b>' .. table.concat(headdata, ", ")
            if propdata[1] then
                itemtext = itemtext .. ': '
            end
            itemtext = itemtext .. '</b>'
        end
        if propdata[1] then
            itemtext = itemtext .. table.concat(propdata, ", ")
        end
        table.insert(listparams, itemtext)
    end

    return html.create("div"):node(p.dnav(data, modulename)):node(require "Module:List".init(listparams))
end

function q.errorcheck(frame)
    local args = getArgs(frame)
    local subpage = args[1]
    local modulename = p.modulename .. "/" .. subpage
    local data = mw.loadData(modulename)
    return html.create("div"):node(p.dnav(data, modulename)):node(errorcheck(data, p.modulename))
end

function errorcheck(data, modulename)--setting this as p. for testing purposes
    local errors = {}--errors found
    local unique = {}--unique fields and their unique values
    local required = {}--required fields
    local sourcelist = {}--source fields

    local f = data.fields

    for k, e in pairs(data.fields) do
        local t = "Field [" .. k .. "]"
        if not e.datatype then
            table.insert(errors, t .. " does not specify a data type")
        elseif not datatypelist[e.datatype] then
            table.insert(errors, t .. " uses invalid data type")
        else
            if not datatypelist[e.datatype].formats[e.format or "default"] then
                table.insert(errors, t .. " contains invalid <code>format</code> for data type <code>" .. e.datatype .. "</code>.")
            end
        end
        if e.required then
            table.insert(required, k)
        end
        if e.source then
            local subpage, field = e.source:match("(.*)%.(.*)")
            local foreigndata
            if not subpage then
                subpage, field = "self", e.source
                foreigndata = data
            else
                foreigndata = mw.loadData(modulename .. "/" .. subpage)
            end
            local query = p.select(foreigndata, {}, {field})
            sourcelist[e.source] = {
                    query = query,
                    table = subpage,
                    field = field
            }
            local foreignfield = foreigndata.fields[field]
            if foreignfield.datatype ~= e.datatype and not (e.datatype == "list" and foreignfield.datatype == "string") then
                table.insert(errors, t .. " data type mismatch with foreign source.")
            end
        end
        for propname, prop in pairs(e) do
            if not fielddefinitionlist[propname] and propname:sub(1, 5) ~= "meta_" then
                table.insert(errors, t .. " contains invalid <code>" .. propname .. "</code> field definition property.")
            end
        end
    end

    for i, e in ipairs(data) do
        --local e = data[i]
        local t = "Entry [" .. i .. "] " .. (data.keyfield and e[data.keyfield] or "")
        for k, v in pairs(e) do
            if not f[k] then
                 table.insert(errors, t .. " uses undefined <code>" .. k .. "</code> field.")
            elseif f[k].datatype == "string" and type(v) ~= "string" then
                table.insert(errors, t .. " uses invalid value for <code>" .. k .. "</code> string field.")
            elseif f[k].datatype == "number" then
                if type(v) ~= "number" then
                    table.insert(errors, t .. " uses invalid value for <code>" .. k .. "</code> number field.")
                else
                    if f[k].min and v < f[k].min then
                        table.insert(errors, t .. " uses value less than minimum allowed for <code>" .. k .. "</code> field.")
                    elseif f[k].max and v > f[k].max then
                        table.insert(errors, t .. " uses value greater than maximum allowed for <code>" .. k .. "</code> field.")
                    end
                    if f[k].maxdp and v%1~=0 and #tostring(v):match(".*%.(.*)") > f[k].maxdp then
                        table.insert(errors, t .. " uses more decimal places than maximum allowed for <code>" .. k .. "</code> field.")
                    end
                end
            elseif f[k].datatype == "list" then
                if type(v) ~= "table" then
                    table.insert(errors, t .. " uses invalid value for <code>" .. k .. "</code> table field.")
                elseif f[k].mincount and #v < f[k].mincount then
                    table.insert(errors, t .. " contains fewer items than the minimum allowed for <code>" .. k .. "</code> field.")
                elseif f[k].maxcount and #v > f[k].maxcount then
                    table.insert(errors, t .. " contains more items than the maximum allowed for <code>" .. k .. "</code> field.")
                elseif f[k].count and #v ~= f[k].count then
                    table.insert(errors, t .. " contents does not equal the set item count for <code>" .. k .. "</code> field.")
                end
            elseif f[k].datatype == "boolean" and type(v) ~= "boolean" then
                table.insert(errors, t .. " uses invalid value for <code>" .. k .. "</code> boolean field.")
            elseif f[k].datatype == "image" then
                if type(v) ~= "string" then
                    table.insert(errors, t .. " uses invalid value for <code>" .. k .. "</code> image field.")
                elseif f[k].extensions then
                    local ext = image.new(v):getExtension():lower()
                    local test = false

                    for _, v in ipairs(f[k].extensions) do
                        if v == ext then
                            test = true

                            break
                        end
                    end

                    if not test then
                        table.insert(errors, t .. " uses image with invalid extension for <code>" .. k .. "</code> field.")
                    end
                elseif not image.new(v):isExist() then
                    table.insert(errors, t .. " uses image for <code>" .. k .. "</code> field that does not exist on the wiki.")
                end
            end
            if f[k] and f[k].unique then
                if not unique[k] then unique[k] = {} end
                if not unique[k][v] then
                    unique[k][v] = i
                else
                    table.insert(errors, t .. " has same value for <code>" .. k .. "</code> unique field as [" .. unique[k][v] .. "].")
                end
            end
            local vAsList
            if f[k] and f[k].datatype == "list" then
                vAsList = v
            else
                vAsList = {v}
            end
            for i, v in ipairs(vAsList) do
                if not f[k] then break end
                if f[k].fromlist then
                    local test = false

                    for _, v2 in ipairs(f[k].fromlist) do
                        if v2 == v then
                            test = true

                            break
                        end
                    end

                    if not test then
                        table.insert(errors, t .. " contains a non-whitelisted value (<code>" .. v .. "</code>) for <code>" .. k .. "</code> field.")
                    end
                elseif f[k].source then
                    local src = sourcelist[f[k].source]
                    local match = false
                    for i=1, #src.query do
                        if v == src.query[i][src.field] then
                            match = true
                            break
                        end
                    end
                    if not match then
                        table.insert(errors, t .. " contains a value (<code>" .. v .. "</code>) not found in foreign source for <code>" .. k .. "</code> field.")
                        break
                    end
                elseif f[k].datatype == "string" or f[k].datatype == "list" then
                    if f[k].mask and not v:find(f[k].mask) then
                        table.insert(errors, t .. " contains a value that does not match the input mask for <code>" .. k .. "</code> field.")
                        break
                    end
                    if f[k].length and mw.ustring.len(v) ~= f[k].length then
                        table.insert(errors, t .. " string length does not equal the required size for <code>" .. k .. "</code> field.")
                    elseif f[k].minlength and mw.ustring.len(v) < f[k].minlength then
                        table.insert(errors, t .. " string length is under minimum size for <code>" .. k .. "</code> field.")
                    elseif f[k].maxlength and mw.ustring.len(v) > f[k].maxlength then
                        table.insert(errors, t .. " string length is over the maximum size for <code>" .. k .. "</code> field.")

                    end
                end
            end
        end
        for j=1, #required do
            local k = required[j]
            if not e[k] then
                table.insert(errors, t .. " does not have value for <code>" .. k .. "</code> required field")
            end
        end
    end

    local ul = ""
    if #errors > 0 then
        ul = mw.html.create("ul")
        for i=1, #errors do
            ul:tag("li"):wikitext(errors[i])
        end
        ul = tostring(ul)
    end

    return html.create("div"):wikitext(#errors .. " errors found" .. ul)
end

function p.translationstable(frame, columndefs)
    return q.Translations(frame, columndefs)
end

function q.translationstable(frame, columns)
    local args = getArgs(frame)

    local subpage, head, cols, rows = args[1], args["th"] or "", args["td"] or "", args["colspan"] or ""
    local modulename = p.modulename .. "/" .. subpage
    local codename = p.codename or "series"
    local data = mw.loadData(modulename)

    local fielddata = data.fields

    args[1] = args[2]

    if not columns then columns = {
        {
            field = "jpn",
            official = true,
            current = true
        },
        {
            field = "rom",
        },
        {
            field = "lit",
        },
        {
            field = "ffw",
            current = true
        },
        {
            field = "name",
            official = true,
            current = true
        }
    } end

    if fielddata then
        for i=#columns, 1, -1 do
            if not fielddata[columns[i].field] then
                table.remove(columns, i)
            end
        end
    end

    local querydata = p.select(data, args)

    local widths = (100 / #columns) .. "%"

    local tbl = html.create("table")
        :addClass(codename .. " full-width article-table translations")
    local thead = tbl:tag("tr")
        :addClass("a")
    for i=1, #columns do
        thead:tag("th")
            :wikitext(p.fieldtitle(data, columns[i].field))
            :css("width", widths)
            :css("font-style", not columns[i].official and "italic" or nil)
    end

    for i=1, #querydata do
        local entry = querydata[i]
        local tr = tbl:tag("tr"):attr("id", entry[columns[1].field] or "")

        for i=1, #columns do
            tr:tag("td")
                :wikitext(p.formatfield(data, entry, columns[i].field))
                :addClass(columns[i].current and "b" or nil)
        end
     end

    return html.create("div"):node(p.dnav(data, modulename)):node(tbl)
end

function p.table(data, args, modulename, parameters, codename, headers, cols, rows)

    local fielddata = data.fields

    local querydata = p.select(data, args, parameters)

    args[1] = queryfields

    local tbl = html.create("table")
        :addClass("article-table")
        :addClass(codename or "series")
    local thead = tbl:tag("tr")
        :addClass("a")

    local columnheaderlist = {}
    for i=1, #headers do
        table.insert(columnheaderlist, headers[i])
    end
    for i=1, #cols do
        table.insert(columnheaderlist, cols[i])
    end

    for i=1, #columnheaderlist do
        local thiscol = columnheaderlist[i]
        local thisfielddata = {}

        if fielddata and fielddata[thiscol] then
            thisfielddata = fielddata[thiscol]
        end

        thead:tag("th")
        :wikitext(args[thiscol .. " title"] or thisfielddata.title or thiscol)
        :attr("title", args[thiscol .. " description"] or thisfielddata.description)
        :css("width", args[thiscol .. " width"])
    end

    local colspan = #cols
    for i=1, #querydata do
        local entry = querydata[i]

        local tr = tbl:tag("tr")

        local rowspan = 1
        for j=1, #rows do
            if entry[rows[j]] then rowspan = rowspan + 1 end
        end

        local tr = tbl:tag("tr"):attr("id", entry[headers[1]])
        for j=1, #headers do
            local fieldname = headers[j]

            local thisfielddata = {}
            if fielddata and fielddata[fieldname] then
                thisfielddata = fielddata[fieldname]
            end

            local value = p.formatfield(data, entry, fieldname)

            local thiscolwidth = args[fieldname .. " width"]

            if thisfielddata.datatype == "image"
            and value ~= ""
            and thiscolwidth
            and not thiscolwidth:match("%%") then
                value:setWidth(thiscolwidth)
            end

            tr:tag("th")
            :wikitext(tostring(value))
            :attr(p.cellformatfield(data, fieldname))
            :attr("rowspan", rowspan)
            :addClass("b")
            :cssText(args[fieldname .. " style"])
        end
        for j=1, #cols do
            local fieldname = cols[j]

            local thisfielddata = {}
            if fielddata and fielddata[fieldname] then
                thisfielddata = fielddata[fieldname]
            end

            local value = p.formatfield(data, entry, fieldname)

            local thiscolwidth = args[fieldname .. " width"]

            if thisfielddata.datatype == "image"
            and value ~= ""
            and thiscolwidth
            and not thiscolwidth:match("%%") then
                value:setWidth(thiscolwidth)
            end

            tr:tag("td")
            :wikitext(tostring(value))
            :attr(p.cellformatfield(data, fieldname))
            :cssText(args[fieldname .. " style"])
        end
        for j=1, #rows do
            local fieldname = rows[j]

            local thisfielddata = {}
            if fielddata and fielddata[fieldname] then
                thisfielddata = fielddata[fieldname]
            end

            local description = args[fieldname .. " description"] or (thisfielddata and thisfielddata.description)

            local prefix = args[fieldname .. " title"]
            if not prefix and thisfielddata then
                prefix = thisfielddata.title
            end
            if prefix then
                prefix = tostring(
                    html.create("span")
                    :wikitext('<b>' .. prefix .. '</b>')
                    :attr("title", description)
                ) .. ": "
            else
                prefix = ""
            end

            local value = p.formatfield(data, entry, fieldname)

            local thiscolwidth = args[fieldname .. " width"]

            if thisfielddata.datatype == "image"
            and value ~= ""
            and thiscolwidth
            and not thiscolwidth:match("%%") then
                value:setWidth(thiscolwidth)
            end

            if entry[fieldname] then
                tbl:tag("tr"):tag("td")
                :wikitext(prefix .. tostring(value))
                :attr(p.cellformatfield(data, fieldname))
                :attr("colspan", colspan)
                :cssText(args[fieldname .. " style"])
                :attr("title", (prefix=="") and description or nil)
            end
        end
    end
    return html.create("div"):node(p.dnav(data, modulename)):node(tbl)
end

function p.dnav(data, modulename, datafrom, links)
    local dot = '&nbsp;<span style="font-weight:bold;">&middot;</span>&#32; '
    local framepagename = tostring(mw.title.getCurrentTitle())
    local datapagename = modulename
    local dataeditlink = tostring(mw.uri.fullUrl(datapagename, {action="edit"}))
    local pagepurgelink = tostring(mw.uri.fullUrl(framepagename, {action="purge"}))

    if not links then links = {} end
    if links.data == nil then links.data = true end
    if links.edit == nil then links.edit = true end
    if links.purge == nil then links.purge = true end

    if datafrom==nil or datafrom==true then datafrom = data.datafrom end
    if datafrom then
        datafrom = "Data from " .. datafrom .. dot
    else
        datafrom = ""
    end

    local datanav = "[[" .. datapagename .. "|data]]"
    if data and data.subdata and data.subdata[1] then
        local subdatanav = {}
        for i, sbd in ipairs(data.subdata) do
            local displaytext = (sbd):match(datapagename .. "/" .. "(.*)")
            subdatanav[i] = "[[" .. sbd .. "|" .. (displaytext or sbd) .. "]]"
        end
        datanav = datanav .. " (" .. table.concat(subdatanav, dot) .. ")"
    end
    local editnav = "[" .. dataeditlink .. " edit]"
    local walpurgesnav = "[" .. pagepurgelink .. " purge]"
    local nav = {}
    if links.data then table.insert(nav, datanav) end
    if links.edit then table.insert(nav, editnav) end
    if links.purge then table.insert(nav, walpurgesnav) end

    return html.create("div"):addClass("dnav plainlinks"):wikitext("[&#8203;" .. datafrom .. table.concat(nav, dot) .. "]")
end

function p.datatable(data)
    if data.subdata then
        local i = #data+1
        local tbl
        local t = 0--table index
        local r = 0--row index
        while true do
            if r == 0 or not tbl[r] then
                t = t + 1
                if not data.subdata[t] then break end
                tbl = require (data.subdata[t])
                r = 1
            end

            data[i] = tbl[r]
            r = r + 1
            i = i + 1
        end
    end
    return data
end

return p
Community content is available under CC-BY-SA unless otherwise noted.