Module:Util: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>OmegaK2
(added tooltip function)
>TheFrz
mNo edit summary
Line 13: Line 13:
function util.cast.boolean(value)
function util.cast.boolean(value)
     -- Takes an abitary value and casts it to a bool value
     -- Takes an abitary value and casts it to a bool value
     --  
     --
     -- for strings false will be according to util.cast.bool_false
     -- for strings false will be according to util.cast.bool_false
     local t = type(value)
     local t = type(value)
Line 34: Line 34:
         error(string.format('value "%s" of type "%s" is not a boolean', value, t))
         error(string.format('value "%s" of type "%s" is not a boolean', value, t))
     end
     end
   
 
end
end


Line 47: Line 47:
         args = {}
         args = {}
     end
     end
   
 
     local t = type(value)
     local t = type(value)
     local val
     local val
   
 
     if t == 'nil' then
     if t == 'nil' then
         val = nil
         val = nil
Line 64: Line 64:
         val = tonumber(value)
         val = tonumber(value)
     end
     end
   
 
     if val == nil then
     if val == nil then
         if args.default ~= nil then
         if args.default ~= nil then
Line 72: Line 72:
         end
         end
     end
     end
   
 
     if args.min ~= nil and val < args.min then
     if args.min ~= nil and val < args.min then
         error(string.format('"%i" is too small. Minimum: "%i"', val, args.min))
         error(string.format('"%i" is too small. Minimum: "%i"', val, args.min))
     end
     end
   
 
     if args.max ~= nil and val > args.max then
     if args.max ~= nil and val > args.max then
         error(string.format('"%i" is too large. Maximum: "%i"', val, args.max))
         error(string.format('"%i" is too large. Maximum: "%i"', val, args.max))
     end
     end
   
 
     return val
     return val
end
end
Line 87: Line 87:
     -- Takes a string value and returns as version number
     -- Takes a string value and returns as version number
     -- If the version number is invalid an error is raised
     -- If the version number is invalid an error is raised
     --  
     --
     -- args:
     -- args:
     --  return_type: defaults to "table"
     --  return_type: defaults to "table"
     --  table  - Returns the version number broken down into sub versions as a table
     --  table  - Returns the version number broken down into sub versions as a table
     --  string - Returns the version number as string
     --  string - Returns the version number as string
     --  
     --
     if args == nil then
     if args == nil then
         args = {}
         args = {}
     end
     end
   
 
     local result
     local result
     if args.return_type == 'table' or args.return_type == nil then
     if args.return_type == 'table' or args.return_type == nil then
         result = util.string.split(value, '%.')
         result = util.string.split(value, '%.')
       
 
         if #result ~= 3 then
         if #result ~= 3 then
             error(string.format('Malformed version string "%s"', value))
             error(string.format('Malformed version string "%s"', value))
         end
         end
       
 
         result[4] = string.match(result[3], '%a+')
         result[4] = string.match(result[3], '%a+')
         result[3] = string.match(result[3], '%d+')
         result[3] = string.match(result[3], '%d+')
       
 
         for i=1,3 do
         for i=1,3 do
             local v = tonumber(result[i])
             local v = tonumber(result[i])
Line 118: Line 118:
         result = string.match(value, '%d+%.%d+%.%d+%a*')
         result = string.match(value, '%d+%.%d+%.%d+%a*')
     end
     end
   
 
     if result == nil then
     if result == nil then
         error(string.format('"%s" is not a recognized version number', value))
         error(string.format('"%s" is not a recognized version number', value))
     end
     end
   
 
     return result
     return result
end
end
Line 155: Line 155:
     args.subobject_prefix = args.subobject_prefix or ''
     args.subobject_prefix = args.subobject_prefix or ''
     args.properties = args.properties or {}
     args.properties = args.properties or {}
   
 
     local i = 0
     local i = 0
     local stats = {}
     local stats = {}
Line 166: Line 166:
             value = string.format('%sstat%s_value', args.prefix, i),
             value = string.format('%sstat%s_value', args.prefix, i),
         }
         }
       
 
         local value = {}
         local value = {}
         for key, args_key in pairs(id) do
         for key, args_key in pairs(id) do
             value[key] = argtbl[args_key]
             value[key] = argtbl[args_key]
         end
         end
       
 
       
 
         if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
         if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
             local properties = {}
             local properties = {}
Line 183: Line 183:
                 value.max = util.cast.number(value.max)
                 value.max = util.cast.number(value.max)
                 argtbl[id.max] = value.max
                 argtbl[id.max] = value.max
               
 
                 -- Also set average value
                 -- Also set average value
                 value.avg = (value.min + value.max)/2
                 value.avg = (value.min + value.max)/2
Line 190: Line 190:
             argtbl[string.format('%sstat%s', args.prefix, i)] = value
             argtbl[string.format('%sstat%s', args.prefix, i)] = value
             stats[#stats+1] = value
             stats[#stats+1] = value
           
 
             if args.frame ~= nil then
             if args.frame ~= nil then
                 local properties = {}
                 local properties = {}
Line 196: Line 196:
                     properties[property] = value
                     properties[property] = value
                 end
                 end
               
 
                 for key, property in pairs(util.args.stat_properties) do
                 for key, property in pairs(util.args.stat_properties) do
                     properties[string.format(property, args.property_prefix)] = value[key]
                     properties[string.format(property, args.property_prefix)] = value[key]
                 end
                 end
               
 
                 util.smw.subobject(args.frame, string.format('%sstat%s_%s', args.subobject_prefix, i, value.id), properties)
                 util.smw.subobject(args.frame, string.format('%sstat%s_%s', args.subobject_prefix, i, value.id), properties)
             end
             end
Line 210: Line 210:
         end
         end
     until value == nil
     until value == nil
   
 
     argtbl[string.format('%sstats', args.prefix)] = stats
     argtbl[string.format('%sstats', args.prefix)] = stats
end
end
Line 232: Line 232:
         },
         },
     }
     }
   
 
     local version_ids = {}
     local version_ids = {}
     local version_keys = {}
     local version_keys = {}
   
 
     for key, data in pairs(args.variables) do
     for key, data in pairs(args.variables) do
         local full_key = string.format('%s_version', key)
         local full_key = string.format('%s_version', key)
Line 248: Line 248:
         end
         end
     end
     end
   
 
     -- no need to do a query if nothing was fetched
     -- no need to do a query if nothing was fetched
     if #version_ids > 0 then
     if #version_ids > 0 then
Line 254: Line 254:
             error('Properties were set, but frame was not')
             error('Properties were set, but frame was not')
         end
         end
   
 
         local query = {}
         local query = {}
         query[#query+1] = string.format('[[Is version::%s]]', table.concat(version_ids, '||'))
         query[#query+1] = string.format('[[Is version::%s]]', table.concat(version_ids, '||'))
         query[#query+1] = '?Has release date#'
         query[#query+1] = '?Has release date#'
         query[#query+1] = '?Is version'
         query[#query+1] = '?Is version'
       
 
         local results = util.smw.query(query, args.frame)
         local results = util.smw.query(query, args.frame)
       
 
         if #results ~= #version_ids then
         if #results ~= #version_ids then
             error(string.format('The number of results (%s) does not match the number version arguments (%s)', #results, #version_ids))
             error(string.format('The number of results (%s) does not match the number version arguments (%s)', #results, #version_ids))
         end
         end
       
 
         for _, row in ipairs(results) do
         for _, row in ipairs(results) do
             local key = version_keys[row['Is version']]
             local key = version_keys[row['Is version']]
Line 271: Line 271:
         end
         end
     end
     end
   
 
     if args.set_properties ~= nil then
     if args.set_properties ~= nil then
         local properties = {}
         local properties = {}
Line 280: Line 280:
             end
             end
         end
         end
       
 
         util.smw.set(args.frame, properties)
         util.smw.set(args.frame, properties)
     end
     end
Line 302: Line 302:
         args = {}
         args = {}
     end
     end
   
 
     local err = mw.html.create('span')
     local err = mw.html.create('span')
     err
     err
Line 308: Line 308:
         :wikitext('Module Error: ' .. (args.msg or ''))
         :wikitext('Module Error: ' .. (args.msg or ''))
         :done()
         :done()
       
 
     return tostring(err)
     return tostring(err)
end
end
Line 387: Line 387:
         categories = {categories}
         categories = {categories}
     end
     end
   
 
     if args == nil then
     if args == nil then
         args = {}
         args = {}
     end
     end
   
 
   
 
     local title = mw.title.getCurrentTitle()
     local title = mw.title.getCurrentTitle()
     local sub_blacklist = args.sub_page_blacklist or util.misc.category_blacklist.sub_pages
     local sub_blacklist = args.sub_page_blacklist or util.misc.category_blacklist.sub_pages
     local ns_blacklist = args.namespace_blacklist or util.misc.category_blacklist.namespaces
     local ns_blacklist = args.namespace_blacklist or util.misc.category_blacklist.namespaces
   
 
     if args.namespace ~= nil and title.namespace ~= args.namespace then
     if args.namespace ~= nil and title.namespace ~= args.namespace then
         return ''
         return ''
     end
     end
   
 
     if args.ingore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
     if args.ingore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
         return ''
         return ''
     end
     end
 
 
     local cats = {}
     local cats = {}
   
 
     for i, cat in ipairs(categories) do
     for i, cat in ipairs(categories) do
         cats[i] = string.format('[[Category:%s]]', cat)
         cats[i] = string.format('[[Category:%s]]', cat)
Line 414: Line 414:


function util.misc.raise_error_or_return(args)
function util.misc.raise_error_or_return(args)
     --  
     --
     -- Arguments:
     -- Arguments:
     -- args: table of arguments to this function (must be set)
     -- args: table of arguments to this function (must be set)
Line 460: Line 460:
     -- frame          : frame object
     -- frame          : frame object
     -- parser_function: the whole parser function string
     -- parser_function: the whole parser function string
     -- args          : table of arguments  
     -- args          : table of arguments
     for k, v in pairs(args) do
     for k, v in pairs(args) do
         if type(v) == 'table' then
         if type(v) == 'table' then
Line 488: Line 488:
     -- query: table of query arguments to pass
     -- query: table of query arguments to pass
     -- frame: current frame object
     -- frame: current frame object
   
 
     -- the characters here for sep/header/propsep are control characters; I'm farily certain they should not appear in regular text.
     -- the characters here for sep/header/propsep are control characters; I'm farily certain they should not appear in regular text.
     query.sep = '�'
     query.sep = '�'
Line 495: Line 495:
     query.format = 'array'
     query.format = 'array'
     query.headers = 'plain'
     query.headers = 'plain'
   
 
     local result = frame:callParserFunction('#ask', query)
     local result = frame:callParserFunction('#ask', query)
   
 
     -- "<span class=\"smw-highlighter\" data-type=\"4\" data-state=\"inline\" data-title=\"Error\"><span class=\"smwtticon warning\"></span><div class=\"smwttcontent\">Some subquery has no valid condition.</div></span>"
     -- "<span class=\"smw-highlighter\" data-type=\"4\" data-state=\"inline\" data-title=\"Error\"><span class=\"smwtticon warning\"></span><div class=\"smwttcontent\">Some subquery has no valid condition.</div></span>"
     if mw.ustring.find(result, 'data%-title="Error"') ~= nil then
     if mw.ustring.find(result, 'data%-title="Error"') ~= nil then
Line 504: Line 504:


     local out = {}
     local out = {}
   
 
     for row_string in string.gmatch(result, '[^�]+') do
     for row_string in string.gmatch(result, '[^�]+') do
         local row = {}
         local row = {}
Line 510: Line 510:
             local kv = util.string.split(str, query.headersep)
             local kv = util.string.split(str, query.headersep)
             if #kv == 1 then
             if #kv == 1 then
                 row[#row+1] = kv[1]  
                 row[#row+1] = kv[1]
             elseif #kv == 2 then
             elseif #kv == 2 then
                 row[kv[1]] = kv[2]
                 row[kv[1]] = kv[2]
Line 517: Line 517:
         out[#out+1] = row
         out[#out+1] = row
     end
     end
   
 
     return out
     return out
end
end
Line 533: Line 533:
         return true
         return true
     end
     end
   
 
     local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
     local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
     if util.smw.data.rejected_namespaces:contains(namespace) then
     if util.smw.data.rejected_namespaces:contains(namespace) then
         return false
         return false
     end
     end
   
 
     return true
     return true
end
end
Line 553: Line 553:
     -- str: string to split
     -- str: string to split
     -- pattern: pattern to use for splitting
     -- pattern: pattern to use for splitting
     out = {}
     local out = {}
     local i = 1
     local i = 1
     local split_start, split_end = string.find(str, pattern, i)
     local split_start, split_end = string.find(str, pattern, i)
Line 573: Line 573:
     --  kvsep: separator to use for key value pairs (default: =)
     --  kvsep: separator to use for key value pairs (default: =)
     local out = {}
     local out = {}
   
 
     if args == nil then
     if args == nil then
         args = {}
         args = {}
     end
     end
   
 
     args.sep = args.sep or ','
     args.sep = args.sep or ','
     args.kvsep = args.kvsep or '='
     args.kvsep = args.kvsep or '='
   
 
     if str ~= nil then
     if str ~= nil then
         local row
         local row
Line 586: Line 586:
             row = util.string.split(str, args.kvsep)
             row = util.string.split(str, args.kvsep)
             if #row == 1 then
             if #row == 1 then
                 out[#out+1] = row[1]  
                 out[#out+1] = row[1]
             elseif #row == 2 then
             elseif #row == 2 then
                 out[row[1]] = row[2]
                 out[row[1]] = row[2]
Line 594: Line 594:
         end
         end
     end
     end
   
 
     return out
     return out
end
end
Line 603: Line 603:


util.table = {}
util.table = {}
function util.table.length(tbl)
    -- Get length of the metatable
    local length = 0
    for _ in ipairs(tbl) do
        length = length + 1
    end
    return length
end
function util.table.has_all_value(tbl, keys, value)
function util.table.has_all_value(tbl, keys, value)
     -- Whether all the table values with the specified keys are the specified value
     -- Whether all the table values with the specified keys are the specified value
Line 623: Line 632:
end
end


function util.table.find_in_nested_array(args)  
function util.table.find_in_nested_array(args)
     -- Iterates thoguh the given nested array and finds the given value
     -- Iterates thoguh the given nested array and finds the given value
     --  
     --
     -- ex.  
     -- ex.
     -- data = {
     -- data = {
     -- {a=5}, {a=6}}
     -- {a=5}, {a=6}}
Line 632: Line 641:
     -- find_nested_array(arg=10, tbl=data, key='a'} -> nil
     -- find_nested_array(arg=10, tbl=data, key='a'} -> nil
     -- -> returns "6"
     -- -> returns "6"
   
 
     --
     --
     -- args: Table containing:
     -- args: Table containing:
Line 640: Line 649:
     --  rtrkey: if key is table, return this key instead of the value instead
     --  rtrkey: if key is table, return this key instead of the value instead
     --  rtrvalue: default: true
     --  rtrvalue: default: true
   
 
     local rtr
     local rtr
   
 
     if type(args.key) == 'table' then
     if type(args.key) == 'table' then
         for _, item in ipairs(args.tbl) do
         for _, item in ipairs(args.tbl) do
Line 667: Line 676:
         end
         end
     end
     end
   
 
     if rtr == nil then
     if rtr == nil then
         return rtr
         return rtr
     end
     end


     if args.rtrkey ~= nil then  
     if args.rtrkey ~= nil then
         return rtr[args.rtrkey]
         return rtr[args.rtrkey]
     elseif args.rtrvalue or args.rtrvalue == nil then
     elseif args.rtrvalue or args.rtrvalue == nil then

Revision as of 17:26, 3 January 2017

Module documentation[view] [edit] [history] [purge]


This is a meta module.

This module is meant to be used only by other modules. It should not be invoked in wikitext.

Lua logo

This module depends on the following other modules:

Overview

Provides utility functions for programming modules.

Structure

Group Description
util.cast utilities for casting values (i.e. from arguments)
util.html shorthand functions for creating some html tags
util.misc miscellaneous functions

Usage

This module should be loaded with require().

ru:Модуль:Util

-- Utility stuff

local xtable = require('Module:Table')
local util = {}

-- ----------------------------------------------------------------------------
-- util.cast
-- ----------------------------------------------------------------------------

util.cast = {}
util.cast.bool_false = {'false', '0', 'disabled', 'off', 'no', ''}

function util.cast.boolean(value)
    -- Takes an abitary value and casts it to a bool value
    --
    -- for strings false will be according to util.cast.bool_false
    local t = type(value)
    if t == 'nil' then
        return false
    elseif t == 'boolean' then
        return value
    elseif t == 'number' then
        if value == 0 then return false end
        return true
    elseif t == 'string' then
        local tmp = string.lower(value)
        for _, v in ipairs(util.cast.bool_false) do
            if v == tmp then
                return false
            end
        end
        return true
    else
        error(string.format('value "%s" of type "%s" is not a boolean', value, t))
    end

end

function util.cast.number(value, args)
    -- Takes an abitary value and attempts to cast it to int
    --
    -- args:
    --  default: for strings, if default is nil and the conversion fails, an error will be returned
    --  min: error if <min
    --  max: error if >max
    if args == nil then
        args = {}
    end

    local t = type(value)
    local val

    if t == 'nil' then
        val = nil
    elseif t == 'boolean' then
        if value then
            val = 1
        else
            val = 0
        end
    elseif t == 'number' then
        val = value
    elseif t == 'string' then
        val = tonumber(value)
    end

    if val == nil then
        if args.default ~= nil then
            val = args.default
        else
            error(string.format('value "%s" of type "%s" is not an integer', tostring(value), t))
        end
    end

    if args.min ~= nil and val < args.min then
        error(string.format('"%i" is too small. Minimum: "%i"', val, args.min))
    end

    if args.max ~= nil and val > args.max then
        error(string.format('"%i" is too large. Maximum: "%i"', val, args.max))
    end

    return val
end

function util.cast.version(value, args)
    -- Takes a string value and returns as version number
    -- If the version number is invalid an error is raised
    --
    -- args:
    --  return_type: defaults to "table"
    --   table  - Returns the version number broken down into sub versions as a table
    --   string - Returns the version number as string
    --
    if args == nil then
        args = {}
    end

    local result
    if args.return_type == 'table' or args.return_type == nil then
        result = util.string.split(value, '%.')

        if #result ~= 3 then
            error(string.format('Malformed version string "%s"', value))
        end

        result[4] = string.match(result[3], '%a+')
        result[3] = string.match(result[3], '%d+')

        for i=1,3 do
            local v = tonumber(result[i])
            if v == nil then
                error(string.format('"%s" has an non-number component', value))
            end
            result[i] = v
        end
    elseif args.return_type == 'string' then
        result = string.match(value, '%d+%.%d+%.%d+%a*')
    end

    if result == nil then
        error(string.format('"%s" is not a recognized version number', value))
    end

    return result
end

-- ----------------------------------------------------------------------------
-- util.args
-- ----------------------------------------------------------------------------

util.args = {}

util.args.stat_properties = {
    id = 'Has %sstat id',
    min = 'Has minimum %sstat value',
    max = 'Has maximum %sstat value',
    avg = 'Has average %sstat value',
    value = 'Has %sstat value',
}

function util.args.stats(argtbl, args)
    -- in any prefix spaces should be included
    --
    -- argtbl: argument table to work with
    -- args:
    --  prefix: prefix if any
    --  frame: frame used to set subobjects; if not set dont set properties
    --  property_prefix: property prefix if any
    --  subobject_prefix: subobject prefix if any
    --  properties: table of properties to add if any
    args = args or {}
    args.prefix = args.prefix or ''
    args.property_prefix = args.property_prefix or ''
    args.subobject_prefix = args.subobject_prefix or ''
    args.properties = args.properties or {}

    local i = 0
    local stats = {}
    repeat
        i = i + 1
        local id = {
            id = string.format('%sstat%s_id', args.prefix, i),
            min = string.format('%sstat%s_min', args.prefix, i),
            max = string.format('%sstat%s_max', args.prefix, i),
            value = string.format('%sstat%s_value', args.prefix, i),
        }

        local value = {}
        for key, args_key in pairs(id) do
            value[key] = argtbl[args_key]
        end


        if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
            local properties = {}
            if value.value then
                value.value = util.cast.number(value.value)
                argtbl[id.value] = value.value
            else
                value.min = util.cast.number(value.min)
                argtbl[id.min] = value.min
                value.max = util.cast.number(value.max)
                argtbl[id.max] = value.max

                -- Also set average value
                value.avg = (value.min + value.max)/2
                argtbl[string.format('%sstat%s_avg', args.prefix, i)] = value.avg
            end
            argtbl[string.format('%sstat%s', args.prefix, i)] = value
            stats[#stats+1] = value

            if args.frame ~= nil then
                local properties = {}
                for property, value in pairs(args.properties) do
                    properties[property] = value
                end

                for key, property in pairs(util.args.stat_properties) do
                    properties[string.format(property, args.property_prefix)] = value[key]
                end

                util.smw.subobject(args.frame, string.format('%sstat%s_%s', args.subobject_prefix, i, value.id), properties)
            end
        elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then
            value = nil
        -- all other cases should be improperly set value
        else
            error(string.format('%sstat%s is improperly set; id and either value or min/max must be specified.', args.prefix, i))
        end
    until value == nil

    argtbl[string.format('%sstats', args.prefix)] = stats
end

function util.args.version (argtbl, args)
    -- in any prefix spaces should be included
    --
    -- argtbl: argument table to work with
    -- args:
    --  frame: frame for queries
    --  set_properties: if defined, set properties on the page
    --  variables: table of prefixes of
    --   property: property; if not set skip fetching and setting release date
    args = args or {}
    args.variables = args.variables or {
        release = {
            property = 'Has release',
        },
        removal = {
            property = 'Has removal',
        },
    }

    local version_ids = {}
    local version_keys = {}

    for key, data in pairs(args.variables) do
        local full_key = string.format('%s_version', key)
        if argtbl[full_key] ~= nil then
            local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
            argtbl[full_key] = value
            data.value = value
            if data.property ~= nil then
                version_ids[#version_ids+1] = value
                version_keys[value] = key
            end
        end
    end

    -- no need to do a query if nothing was fetched
    if #version_ids > 0 then
        if args.frame == nil then
            error('Properties were set, but frame was not')
        end

        local query = {}
        query[#query+1] = string.format('[[Is version::%s]]', table.concat(version_ids, '||'))
        query[#query+1] = '?Has release date#'
        query[#query+1] = '?Is version'

        local results = util.smw.query(query, args.frame)

        if #results ~= #version_ids then
            error(string.format('The number of results (%s) does not match the number version arguments (%s)', #results, #version_ids))
        end

        for _, row in ipairs(results) do
            local key = version_keys[row['Is version']]
            argtbl[string.format('%s_date', key)] = row['Has release date']
        end
    end

    if args.set_properties ~= nil then
        local properties = {}
        for key, data in pairs(args.variables) do
            if data.property ~= nil then
                properties[string.format('%s version', data.property)] = argtbl[string.format('%s_version', key)]
                properties[string.format('%s date', data.property)] = argtbl[string.format('%s_date', key)]
            end
        end

        util.smw.set(args.frame, properties)
    end
end

-- ----------------------------------------------------------------------------
-- util.html
-- ----------------------------------------------------------------------------

util.html = {}
function util.html.abbr(abbr, text, class)
    return string.format('<abbr title="%s" class="%s">%s</abbr>', text or '', class or '', abbr or '')
end

function util.html.error(args)
    -- Create an error message box
    --
    -- Args:
    --  msg - message
    if args == nil then
        args = {}
    end

    local err = mw.html.create('span')
    err
        :attr('class', 'module-error')
        :wikitext('Module Error: ' .. (args.msg or ''))
        :done()

    return tostring(err)
end

function util.html.poe_color(label, text)
    if text == nil or text == '' then
        return nil
    end
    return tostring(mw.html.create('em')
        :attr('class', 'tc -' .. label)
        :wikitext(text))
end

function util.html.tooltip(abbr, text, class)
    return string.format('<span class="tooltip-activator %s">%s<span class="tooltip-content">%s</span></span>', class or '', abbr or '', text or '')
end

util.html.td = {}
function util.html.td.na(args)
    --
    -- Args:
    --  as_tag
    args = args or {}
    -- N/A table row, requires mw.html.create instance to be passed
    local td = mw.html.create('td')
    td
        :attr('class', 'table-na')
        :wikitext('N/A')
        :done()
    if args.as_tag then
        return td
    else
        return tostring(td)
    end
end

-- ----------------------------------------------------------------------------
-- util.misc
-- ----------------------------------------------------------------------------

util.misc = {}
function util.misc.is_frame(frame)
    -- the type of the frame is a table containing the functions, so check whether some of these exist
    -- should be enough to avoid collisions.
    return not(frame == nil or type(frame) ~= 'table' or (frame.argumentPairs == nil and frame.callParserFunction == nil))
end

function util.misc.get_frame(frame)
    if util.misc.is_frame(frame) then
        return frame
    end
    return mw.getCurrentFrame()
end

util.misc.category_blacklist = {}
util.misc.category_blacklist.sub_pages = {
    doc = true,
    sandbox = true,
    sandbox2 = true,
    testcases = true,
}

util.misc.category_blacklist.namespaces = {
    Template = true,
    Template_talk = true,
    Module = true,
    Module_talk = true,
}

function util.misc.add_category(categories, args)
    -- categories: table of categories
    -- args: table of extra arguments
    --  namespace: id of namespace to validate against
    --  ingore_blacklist: set to non-nil to ingore the blacklist
    --  sub_page_blacklist: blacklist of subpages to use (if empty, use default)
    --  namespace_blacklist: blacklist of namespaces to use (if empty, use default)
    if type(categories) == 'string' then
        categories = {categories}
    end

    if args == nil then
        args = {}
    end


    local title = mw.title.getCurrentTitle()
    local sub_blacklist = args.sub_page_blacklist or util.misc.category_blacklist.sub_pages
    local ns_blacklist = args.namespace_blacklist or util.misc.category_blacklist.namespaces

    if args.namespace ~= nil and title.namespace ~= args.namespace then
        return ''
    end

    if args.ingore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
        return ''
    end

    local cats = {}

    for i, cat in ipairs(categories) do
        cats[i] = string.format('[[Category:%s]]', cat)
    end
    return table.concat(cats)
end

function util.misc.raise_error_or_return(args)
    --
    -- Arguments:
    -- args: table of arguments to this function (must be set)
    --  One required:
    --  raise_required: Don't raise errors and return html errors instead unless raisae is set in arguments
    --  no_raise_required: Don't return html errors and raise errors insetad unless no_raise is set in arguments
    --
    --  Optional:
    --  msg: error message to raise or return, default: nil
    --  args: argument directory to validate against (e.x. template args), default: {}
    args.args = args.args or {}
    args.msg = args.msg or ''
    if args.raise_required ~= nil then
        if args.args.raise ~= nil then
            error(args.msg)
        else
            return util.html.error{msg=args.msg}
        end
    elseif args.no_raise_required ~= nil then
        if args.args.no_raise ~= nil then
            return util.html.error{msg=args.msg}
        else
            error(args.msg)
        end
    else
        error('Invalid usage of raise_error_or_return.')
    end
end

-- ----------------------------------------------------------------------------
-- util.smw
-- ----------------------------------------------------------------------------

util.smw = {}

util.smw.data = {}
util.smw.data.rejected_namespaces = xtable:new({'User'})

function util.smw._parser_function(frame, parser_function, args)
    -- Executes a semantic parser functions and sets the arguments args
    --
    -- This function is a helper for handling tables since +sep= parameter
    -- appears to be broken.
    --
    -- frame          : frame object
    -- parser_function: the whole parser function string
    -- args           : table of arguments
    for k, v in pairs(args) do
        if type(v) == 'table' then
            for _, value in ipairs(v) do
                frame:callParserFunction(parser_function, {[k] = value})
            end
            args[k] = nil
        elseif type(v) == 'boolean' then
            args[k] = tostring(v)
        end
    end
    frame:callParserFunction(parser_function, args)
end

function util.smw.set(frame, args)
    util.smw._parser_function(frame, '#set:', args)
end

function util.smw.subobject(frame, id, args)
    util.smw._parser_function(frame, '#subobject:' .. id, args)
end

function util.smw.query(query, frame)
    -- Executes a semantic media wiki #ask query and returns the result as an
    -- array containing each row as table.
    --
    -- query: table of query arguments to pass
    -- frame: current frame object

    -- the characters here for sep/header/propsep are control characters; I'm farily certain they should not appear in regular text.
    query.sep = '�'
    query.propsep = '<PROP>'
    query.headersep = '<HEAD>'
    query.format = 'array'
    query.headers = 'plain'

    local result = frame:callParserFunction('#ask', query)

    -- "<span class=\"smw-highlighter\" data-type=\"4\" data-state=\"inline\" data-title=\"Error\"><span class=\"smwtticon warning\"></span><div class=\"smwttcontent\">Some subquery has no valid condition.</div></span>"
    if mw.ustring.find(result, 'data%-title="Error"') ~= nil then
        error(mw.ustring.sub(result, mw.ustring.find(result, '<span class="smw-highlighter"', 1, true), -1))
    end

    local out = {}

    for row_string in string.gmatch(result, '[^�]+') do
        local row = {}
        for _, str in ipairs(util.string.split(row_string, query.propsep)) do
            local kv = util.string.split(str, query.headersep)
            if #kv == 1 then
                row[#row+1] = kv[1]
            elseif #kv == 2 then
                row[kv[1]] = kv[2]
            end
        end
        out[#out+1] = row
    end

    return out
end

function util.smw.safeguard(args)
    -- Used for safeguarding data entry so it doesn't get added on user space stuff
    --
    -- Args:
    --  smw_ingore_safeguard - ingore safeguard and return true
    if args == nil then
        args = {}
    end

    if args.smw_ingore_safeguard then
        return true
    end

    local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
    if util.smw.data.rejected_namespaces:contains(namespace) then
        return false
    end

    return true
end


-- ----------------------------------------------------------------------------
-- util.string
-- ----------------------------------------------------------------------------

util.string = {}
function util.string.split(str, pattern)
    -- Splits string into a table
    --
    -- str: string to split
    -- pattern: pattern to use for splitting
    local out = {}
    local i = 1
    local split_start, split_end = string.find(str, pattern, i)
    while split_start do
        out[#out+1] = string.sub(str, i, split_start-1)
        i = split_end+1
        split_start, split_end = string.find(str, pattern, i)
    end
    out[#out+1] = string.sub(str, i)
    return out
end

function util.string.split_args(str, args)
    -- Splits arguments string into a table
    --
    -- str: String of arguments to split
    -- args: table of extra arguments
    --  sep: separator to use (default: ,)
    --  kvsep: separator to use for key value pairs (default: =)
    local out = {}

    if args == nil then
        args = {}
    end

    args.sep = args.sep or ','
    args.kvsep = args.kvsep or '='

    if str ~= nil then
        local row
        for _, str in ipairs(util.string.split(str, args.sep)) do
            row = util.string.split(str, args.kvsep)
            if #row == 1 then
                out[#out+1] = row[1]
            elseif #row == 2 then
                out[row[1]] = row[2]
            else
                error(string.format('Number of arguments near = is too large (%s).', #row))
            end
        end
    end

    return out
end

-- ----------------------------------------------------------------------------
-- util.table
-- ----------------------------------------------------------------------------

util.table = {}
function util.table.length(tbl)
    -- Get length of the metatable
    local length = 0
    for _ in ipairs(tbl) do
        length = length + 1
    end
    return length
end

function util.table.has_all_value(tbl, keys, value)
    -- Whether all the table values with the specified keys are the specified value
    for _, k in ipairs(keys or {}) do
        if tbl[k] ~= value then
            return false
        end
    end
    return true
end

function util.table.has_one_value(tbl, keys, value)
    -- Whether one of table values with the specified keys is the specified value
    for _, k in ipairs(keys or {}) do
        if tbl[k] == value then
            return true
        end
    end
    return false
end

function util.table.find_in_nested_array(args)
    -- Iterates thoguh the given nested array and finds the given value
    --
    -- ex.
    -- data = {
    -- {a=5}, {a=6}}
    -- find_nested_array{arg=6, tbl=data, key='a'} -> 6
    -- find_nested_array(arg=10, tbl=data, key='a'} -> nil
    -- -> returns "6"

    --
    -- args: Table containing:
    --  value: value of the argument
    --  tbl: table of valid options
    --  key: key or table of key of in tbl
    --  rtrkey: if key is table, return this key instead of the value instead
    --  rtrvalue: default: true

    local rtr

    if type(args.key) == 'table' then
        for _, item in ipairs(args.tbl) do
            for _, k in ipairs(args.key) do
                if item[k] == args.value then
                    rtr = item
                    break
                end
            end
        end
    elseif args.key == nil then
        for _, item in ipairs(args.tbl) do
            if item == args.value then
                rtr = item
                break
            end
        end
    else
        for _, item in ipairs(args.tbl) do
            if item[args.key] == args.value then
                rtr = item
                break
            end
        end
    end

    if rtr == nil then
        return rtr
    end

    if args.rtrkey ~= nil then
        return rtr[args.rtrkey]
    elseif args.rtrvalue or args.rtrvalue == nil then
        return args.value
    else
        return rtr
    end
end

-- ----------------------------------------------------------------------------

return util