Module:Builder

--**************************************************** --**                                               ** --**  The actual code for handling the various data ** --** files. Most things can be accomplished by   ** --** by editing the various Builder/Data files. ** --** This contains actual Lua script and should    ** --** should only be changed by people with some    ** --** basic programming knowledge. ** --**                                               ** --****************************************************

local Builder = {} Builder.__index = Builder -- Class factory setmetatable(Builder, {   __call = function (cls, ...)        return cls.new(...)    end, })

-- Constructor function Builder.new(args) local self = setmetatable({}, Builder) self.d = mw.loadData('Module:Builder/Data/Main') self.isInTip = args and args.show and args.show == 'tip' local title = mw.title.getCurrentTitle if title then self.page = title.text end return self end

function Builder:find(key, noRecurse) local function firstUpper(first, rest) return first:upper..rest:lower end

local searchKey = key local c = string.sub(key, 2, 2) if c and c ~= string.upper(c) then searchKey = string.gsub(key, "(%a)([%w_']*)", firstUpper) end

local ret for _, k in ipairs(self.d.Sections) do       ret = self.d[k][searchKey]

if (ret) then break end end

-- Search for plural/singular if not ret and not noRecurse then if searchKey:sub(-1) == 's' then --try without an s           searchKey = string.sub(searchKey, 1, -2) ret = self:find(searchKey, true) if not ret and string.sub(searchKey, -1) == 'e' then searchKey = string.sub(searchKey, 1, -2) ret = self:find(searchKey, true) if not ret and string.sub(searchKey, -1) == 'i' then searchKey = string.sub(searchKey, 1, -2) .. 'y'                   ret = self:find(searchKey, true) end end else --try with an s           ret = self:find(key .. "s", true) if not ret then --try with an es               ret = self:find(key .. "es", true) if not ret and string.sub(searchKey, -1) == 'y' then ret = self:find(string.sub(searchKey, 1, -2) .. 'ies', true) end end end end return ret end

-- Helper function, return the first non-null value function Builder:getDefault(override, lookup, default) if override == 'false' then return false elseif override ~= nil then return override elseif lookup ~= nil then return lookup else return default end end

function Builder:getInfoIconData(key, args) if not args then args = {} end local e = self:find(key)

if not e then if not args.image and not args.link and not args.tip then return nil else e = {text = (args.text or key), key = key, tip = args.tip or false} end end

local def = self.d.TipDefaults[e.t] or self.d.TipDefaults.default or {} local link = self:getDefault(args['link'], e.link, def.link) if link then link = string.gsub(link, '< >', e.key) end if self.page and link then if link == self.page then link = false else link = string.gsub(link, self.page .. '#', '#') end end

local tip = self:getDefault(args['tip'], e.tip, def.tip) if tip then tip = string.gsub(tip, '< >', e.key) end

local image = self:getDefault(args['image'], e.image, def.image) if (def.image == false) then image = nil end if image then image = string.gsub(image, '< >', e.key) end

local text = self:getDefault(args['text'], e.text, def.text) if text then text = string.gsub(text, '< >', key) end if args['imageOnly'] and image then text = nil if link and string.sub(link, 1, 9) ~= "Strategic" then link = nil end end local width = self:getDefault(args['width'], e.width, def.width) local size = self:getDefault(args['size'], e.size, def.size) return {key = key, def = def, link = link, tip = tip, image = image, text = text, width = width, size = size, glossary = e.glossary} end

function Builder:buildInfoIcon(key, args, noLinkKey) if not args then args = {} end

ii = self:getInfoIconData(key, args) if not ii then return args.text or key end local hadLink = false local hasText = ii.text and ii.text > '' if (noLinkKey and key == noLinkKey) or self.isInTip then ii.link = nil end if self.isInTip then ii.tip = nil end

local image = '' if ii.image then local alt = '' if not hasText then alt = '|alt=' .. ii.key end if ii.link and not hasText then image = '' else image = '' end end local text = '' if hasText then if ii.link then text =  .. ii.text ..  else text = ii.text end end if ii.image and ii.text then image = image .. ' '   end local tip = '' if ii.tip then local tipClass = '' if ii.text and (ii.text > ) and ((not ii.link) or ii.link == ) then tipClass = 'isg-nolink-tip ' end tip = '#invoke:Builder|getTip|' .. ii.tip if ii.size == 'small' then tipClass = tipClass .. 'isg-small-tooltip' elseif ii.size == 'medium' then tipClass = tipClass .. 'isg-medium-tooltip' elseif ii.size == 'large' then tipClass = tipClass .. 'isg-large-tooltip' elseif ii.size == 'dynamic' then tipClass = tipClass .. 'isg-dynamic-tooltip' elseif string.find(ii.tip, 'card') or string.find(ii.tip, 'tbl') or string.find(ii.tip, 'width') then tipClass = tipClass .. 'isg-dynamic-tooltip' else tipClass = tipClass .. 'isg-medium-tooltip' end

tip = ' class="' .. tipClass ..'" data-template="' .. tip .. '"' elseif self.isInTip then tip = ' class="isg-link-color"' end

local ret

if image >  and text >  then ret = ' ' .. image .. text .. ' '   elseif tip and tip > '' then ret = '' .. image .. text .. ' '   else ret = image .. text end

return ret end

function Builder:parseTemplate(s) local function split(s, d)       local ret = {}; for p in (s .. d):gmatch("(.-)" .. d) do           table.insert(ret, p); end return ret end if not s then return '', {} end local parts = split(s, '|') local template = '' local params = {} for i, p in ipairs(parts) do       if i == 1 then template = p       else local kv = split(p, '=') if #kv > 1 then params[kv[1]] = kv[2] else table.insert(params, p)           end end end return template, params end

function Builder:getTip(args, frame) if not args then return '' end local tip = '' if args.concept then local c = self:find(args.concept) if c and c.tiptext then tip = self:expandInfoIcons(c.tiptext, nil, args.concept) end elseif args.card then tip = self:buildCard(args.card, args) elseif args.tbl then tip = ' ' .. self:buildTable(args.tbl, args) .. ' '   elseif args.text then local t = mw.loadData('Module:Builder/Data/Text') tip = self:expandInfoIcons(t[args.text] or args.text) elseif args.raw and frame then tip = frame:preprocess(args.raw) elseif args.page and frame then local s = '' if args.section then if string.sub(args.section, 1, 1) == '#' then s = 'include=##.*' .. string.sub(args.section, 2) .. '.*'           else s = 'include=' .. args.section end s = '\ntitle=' .. args.page .. '\n' .. s .. '\nallowcachedresults=true\nskipthispage=no\nreset=categories\n' else s = '' end tip = frame:preprocess(s) else tip = self:expandInfoIcons(args[1] or '') end if args.width then tip = ' ' .. tip .. ' '   end return tip end

function Builder:expandInfoIcons(s, args, noLinkKey) local function build(t) local template local params template, params = self:parseTemplate(t) for k, v in pairs(args) do           params[k] = v        end return self:buildInfoIcon(template, params, noLinkKey) end if not args then args = {} end local ret = string.gsub(s, '', build ) return ret end

function Builder:expandStats(key, e, s)   local ret = string.gsub(s, '{!(.-)!}',         function(stat)             if stat == 'wm' and e['wm'] then                local m = {}                local any = false                local txt                for _, v in ipairs(e.wm) do                    any = true                    if v.key == 'NRD' then                        txt = 'No Rng Disp'                    else                        txt = string.gsub(v.n, ' ', ' ')                    end                    table.insert(m,  )                end                if any then                    return table.concat(m, ' ')                else                    return                 end            elseif stat == 'sr' and e.sr then                return ' '            elseif stat == 'key' then                return e.headerColumn or key            elseif stat == 'notes' and e['notes'] then local a = self:copyArray(e.notes) return '' .. table.concat(a, '') .. '' elseif stat == 'id' then return string.gsub(key, ' ', '_') elseif stat == 'description' and e.description then return table.concat(self:copyArray(e.description), ' ') elseif stat == 'rl' and e.rt == 'Ruins' then return '' else return e[stat] or '' end end )   return ret end

-- table.concat doesn't like mw.loadData function Builder:copyArray(a) local ret = {} for _, s in ipairs(a) do       table.insert(ret, s)    end return ret end

function Builder:buildCard(key, args) local e = self:find(key) if not e then return '' end local image = '' if e.image then image = ' ' end local name = '' .. image .. (e.n or key) .. ' '   local def = self.d.StatLines[e.t] or self.d.StatLines.default if def then stats = ' ' .. self:expandStats(key, e, def) .. ' '   end local notes = '' if e.notes then local n = self:copyArray(e.notes) notes = '' .. table.concat(n, '') .. '' end local ret = ' ' .. name .. stats ..notes .. ' '

ret = self:expandInfoIcons(ret, (args['show'] and {link = false, tip = false}) or {}) return ret end

-- return an array elements from the Builder table -- filtered by key/value pairs function Builder:getFiltered(filters, tblName) local ret = {} local f   local tbl local sections if not tblName then tbl = self.d       sections = tbl.Sections else tbl = { a = self.d[tblName] } sections = {'a'} end for _, sect in ipairs(sections) do       if sect ~= 'Aliases' then for key, e in pairs(tbl[sect]) do               f = true if filters then for k, v in pairs(filters) do                       if v == true then if not e[k] then f = false; break end elseif v == 'nil' then if e[k] ~= nil then f = false; break end elseif string.sub(v, 1, 1) == '!' then if e[k] == string.sub(v, 2) then f = false; break end elseif e[k] ~= v then f = false; break end end end if f then table.insert(ret, e)               end end end end return ret end

-- return a filtered and sorted array of elements -- from the Builder table function Builder:getSorted(t, keys, direction) local function compareItem(a,b) local i = 0 if (not a) and b then i = 1 elseif a and (not b) then i = -1 elseif (not a) and (not b) then i = 0 elseif a > b then i = -1 elseif a < b then i = 1 end if direction == 'desc' then i = -i end return i   end local function compare(a,b) local r = 0 for _, k in ipairs(keys) do           r = compareItem(a[k], b[k]) if r == 1 then return true elseif r == -1 then return false end end return false end

table.sort(t, compare) return t end

function Builder:buildTable(tableType, args) if self.inTip then return '' end if not args then args = {} end local tableDef = nil if args.def then tableDef = self.d.TableDefs[args.def] end tableDef = tableDef or self.d.TableDefs[tableType] or self.d.TableDefs.default local sortby = tableDef.sortby or {'rl', 'rt', 'key'} local filtered local noNotes = (args.exclude == 'notes') if tableType == 'Building' then filtered = self:getFiltered({t = tableType, group = 'nil'}) local groups = self:getFiltered({t = tableType}, 'TechGroups')

for _, g in ipairs(groups) do           table.insert(filtered, g)        end elseif args.filterKey1 then local i = 1 local filter = {} while (args['filterKey' .. i]) do           filter[args['filterKey' .. i]] = args['filterValue' .. i]           i = i + 1 end filtered = self:getFiltered(filter) else filtered = self:getFiltered({t = tableType}) end local items = self:getSorted(filtered, sortby, 'asc') local t = {} local headerDef = self:copyArray(tableDef.header) if noNotes then table.remove(headerDef) end local style = '' if self.isInTip then style = ' style = "width: 100%;"' elseif args.width then style = ' style = "width: ' .. args.width .. ';"' end

local mainClass = tableDef.class or 'igsTable' local subClass = tableDef.subclass or '' local id = ''; if noNotes then subClass = subClass .. ' igsCenterLast' end if self.isInTip then subClass = subClass .. ' igsInTip' end if args.id then id = ' id = "' .. string.gsub(args.id, ' ', '_') .. '-table"' end

table.insert(t, ' ') return self:expandInfoIcons(table.concat(t), {imageOnly = true}) end

function Builder:repeatItem(args) local tbl = args.tbl local filter = nil local template = self.d.Templates[args.template or tbl] if args.filterKey1 then local i = 1 filter = {} while (args['filterKey' .. i]) do           filter[args['filterKey' .. i]] = args['filterValue' .. i]           i = i + 1 end end if (not filter) and (not tbl) then return ''; end local filtered = self:getFiltered(filter, tbl) local items = self:getSorted(filtered, args.sortby or template.sortby or 'key', 'asc') local t = {} local row for _, r in ipairs(items) do       row = self:expandStats(r.key, r, template.def) table.insert(t, row) end return self:expandInfoIcons(table.concat(t), {imageOnly = true}) end

local p = {}

function p._getArgs(frame) if frame.args[1] then return frame.args else local parent = frame:getParent if (parent and parent.args[1]) then return parent.args else return {} end end end

function p._buildTable(args) local b = Builder(args) return b:buildTable(args[1], args) end function p.buildTable(frame) return p._buildTable(p._getArgs(frame)) end function p._card(args) local b = Builder return b:buildCard(args[1], args) end function p.card(frame) return p._card(p._getArgs(frame)) end function p._infoIcon(args) local b = Builder return b:buildInfoIcon(args[1], args) end function p.infoIcon(frame) return p._infoIcon(p._getArgs(frame)) end

function p._getText(args) local b = Builder local t = mw.loadData('Module:Builder/Data/Text') return b:expandInfoIcons(t[args[1]] or args[1]) end

function p.getText(frame) return p._getText(p._getArgs(frame)) end

function p.glossary(frame) local b = Builder local entries = {} for _, v in pairs(b.d.Concepts) do       local t = b:getInfoIconData(v.key)

if t and t.tip and t.glossary ~= false then table.insert(entries, t)       end end local entries = b:getSorted(entries, {'text'}) local glossary = {} for _, t in ipairs(entries) do       table.insert(glossary, ' <td class="glossary-col1" id="' .. t.key ..'">' ..             ' ' ..            '<td class="glossary-col2">  ') end local s = ' ' s = b:expandInfoIcons(s) if (frame) then return frame:preprocess(s) else return s   end end

function p._getTip(args, frame) b = Builder(args) return b:getTip(args, frame) end

function p.getTip(frame) return p._getTip(frame.args, frame) end

function p._flatToc(args) local ret = { } for _, v in ipairs(args) do       table.insert(ret,  .. v .. ) end

return '<div class="flatToc" style="width: 100%; text-align: center; display: block; border-top: 1px solid #b2af9c; border-bottom: 1px solid #b2af9c; padding: 4px"> ' .. table.concat(ret, ' | ') .. ' ' end

function p.flatToc(frame) return p._flatToc(p._getArgs(frame)) end

function p._repeatItem(args) b = Builder(args) return b:repeatItem(args) end

function p.repeatItem(frame) return p._repeatItem(frame.args, frame) end

function p._flexBox(args) local itemDiv = '' if (args.widths) then if args.widths == 'same' then itemDiv = 'flex: 1 1 0;' else itemDiv = 'width:' .. args.widths .. ';'       end end

itemDiv = itemDiv .. (args.itemStyle or '') .. '" '   itemDiv = itemDiv .. (args.itemParams or '') .. '>'    local itemDivLast = '<div style="' .. itemDiv itemDiv = '<div style="padding-right:' ..        (args.padRight or '8px') .. ';' ..        'padding-bottom:' ..        (args.padBottom or '8px') .. ';' ..        itemDiv    local flexDiv = '<div style="display:flex;' if not args.nowrap then flexDiv = flexDiv .. 'flex-wrap: wrap;' end if args.center then flexDiv = flexDiv .. 'text-align: center;' end flexDiv = flexDiv .. (args.flexStyle or '') .. '" '   flexDiv = flexDiv .. (args.flexParams or '') .. '>'    local tbl = {flexDiv}    local maxIndex = 1    for i, v in ipairs(args) do        maxIndex = i    end

for i, v in ipairs(args) do       if i == maxIndex then table.insert(tbl, itemDivLast) else table.insert(tbl, itemDiv) end table.insert(tbl, v)       table.insert(tbl, ' ') end table.insert(tbl, ' ') return table.concat(tbl) end

function p.flexBox(frame) return p._flexBox(frame.args, frame) end

return p