Difference between revisions of "Module:Test"

From RimWorld Wiki
Jump to navigation Jump to search
Line 1: Line 1:
----------------------------------------------
+
local p = {}
-- deal with differences in the environment --
+
 
----------------------------------------------
+
------------------------------------------------------------------
 +
-- deal with differences between MediaWiki and dev environments --
 +
------------------------------------------------------------------
  
 
if mw then
 
if mw then
Line 20: Line 22:
 
   diet = require("./data/diet")
 
   diet = require("./data/diet")
  
   function pinspect(tbl, title)
+
   function pp(tbl, title) -- pretty print tables
 
     util.hl(title)
 
     util.hl(title)
 
     print(inspect(tbl))
 
     print(inspect(tbl))
 
   end
 
   end
  
   -- define used mw functions that don't exist in dev environment
+
   -- (re)define used mw functions that don't exist in dev environment
 
   mw.logObject = function(obj, prefix)
 
   mw.logObject = function(obj, prefix)
 
     if prefix then
 
     if prefix then
Line 47: Line 49:
 
-- load data --
 
-- load data --
 
---------------
 
---------------
 
data = {}
 
  
 
if ENV == "dev" then
 
if ENV == "dev" then
   data["Biomes"] = loadfile("./data/BiomeDefs.lua")()
+
   data = loadfile("../output.lua")()
   data["Races"] = loadfile("./data/ThingDefs_Races.lua")()
+
   diet = loadfile("./data/diet.lua")()
 
elseif ENV == "wiki" then
 
elseif ENV == "wiki" then
   data["Biomes"] = mw.loadData('Module:Test/data/biomes')
+
   data = mw.loadData('Module:Test/data')
  data["Races"] = mw.loadData('Module:Test/data/races')
 
 
end
 
end
 +
 +
version = data.version
 +
data.version = nil
  
 
------------------
 
------------------
Line 62: Line 64:
 
------------------
 
------------------
  
-- this could be implemented with metatable events
+
local virtual_store = {}
-- they get added in get(id_pair)
+
local virtual_keys = {
 +
  ["Pawn"] = {
 +
 
 +
    function (def)
 +
      virtField = "lives_in"
 +
      biomes = {}
 +
      for k,v in pairs(data) do
 +
        prefix = string.match(k, '(.+):')
 +
        if prefix == "BiomeDef" then
 +
          table.insert(biomes, v)
 +
        end
 +
      end
  
local virtual_keys = {
 
  ["Races"] = {
 
    ["lives_in"] = function (race, biomes)
 
 
       local list = {}
 
       local list = {}
       for biome_key, biome in pairs(biomes) do
+
       for _,biome in pairs(biomes) do
         for _,animal in ipairs(biome.wildAnimals) do
+
         for animal,_ in pairs(biome.wildAnimals or {}) do
           if race.defName == animal then
+
           if def.defName == animal then
             table.insert(list, biome_key)
+
             table.insert(list, biome.label)
 +
          end
 +
        end
 +
      end
 +
 
 +
      def._virtual_[virtField] = list
 +
    end,
 +
 
 +
    function (def)
 +
      virtField = "foodTypes"
 +
      foodTypes = def.race.foodType
 +
      flags = {}
 +
      virtual_store.diet = {}
 +
 
 +
      for _,foodType in ipairs(foodTypes) do
 +
        for foodItem,_ in pairs(diet.foodType[foodType]) do
 +
          flags[foodItem] = true
 +
        end
 +
      end
 +
 
 +
      for flag,_ in pairs(flags) do
 +
        table.insert(virtual_store.diet, flag)
 +
      end
 +
 
 +
      def._virtual_[virtField] = virtual_store.diet
 +
    end,
 +
 
 +
    function (def)
 +
      virtField = "foodTypesExpanded"
 +
      flags = {}
 +
      eats = {}
 +
 
 +
      for _,def in pairs(data) do
 +
        if def.defName and def.ingestible and def.ingestible.foodType then
 +
            for _,ingestible in ipairs(def.ingestible.foodType) do
 +
              for _,dietV in ipairs(virtual_store.diet) do
 +
                if ingestible == dietV then
 +
                  flags[def.defName] = true
 +
                end
 +
              end
 +
            end
 
           end
 
           end
 
         end
 
         end
 +
 +
      for flag,_ in pairs(flags) do
 +
        table.insert(eats, flag)
 
       end
 
       end
       return list
+
 
     end
+
       def._virtual_[virtField] = eats
 +
     end,
 +
 
 
   }
 
   }
 
}
 
}
  
-------------
+
-----------------------
-- private --
+
-- private functions --
-------------
+
-----------------------
  
 
local function vardefine(name, value)
 
local function vardefine(name, value)
Line 96: Line 151:
  
  
local function search_parent_def_table(key, def_table)
+
local function mergeParents(baseDef, ignoreKeys)
   local ParentName = getParentName(def_table)
+
   local ancestorIDs = {}
   if not ParentName then return nil end
+
   local mergedDef = {}
   local parentdef_table = search_table_recursive(ParentName, data)
+
   local def = baseDef
  if not parentdef_table then return nil end
 
  
   local found = search_table_recursive(key, parentdef_table)
+
   while def._.ParentName do
  if found then return found
+
    local parentID = def._.DefCategory .. ":" .. def._.ParentName
  else
+
     table.insert(ancestorIDs, parentID)
     found = search_parent_def_table(key, parentdef_table)
+
     def = data[parentID]
     if found then return found end
 
 
   end
 
   end
end
 
  
 +
  ancestorIDs = util.table.reverse(ancestorIDs)
 +
  table.insert(ancestorIDs, baseDef._.DefCategory .. ":" .. baseDef.defName)
  
local function merge_def(base_def_table, def_category, ignore_keys)
+
  for _,parentID in ipairs(ancestorIDs) do
 
+
     util.table.overwrite(mergedDef, data[parentID], ignoreKeys)
  local ancestors = {}
 
  local parent_name = base_def_table["ParentName"]
 
  local parent_table = data[def_category][parent_name]
 
 
 
  while parent_name do
 
     table.insert(ancestors, parent_name)
 
    parent_name = parent_table["ParentName"]
 
    parent_table = data[def_category][parent_name]
 
 
   end
 
   end
  
  local inheritance_chain = util.shallowcopy(util.reverse_numeric_table(ancestors))
+
   return mergedDef
  table.insert(inheritance_chain, base_def_table.defName)
 
 
 
  local merged = {}
 
  for i,v in ipairs(inheritance_chain) do
 
    util.overwrite_first_table_with_second(merged, data[def_category][inheritance_chain[i]], ignore_keys)
 
  end
 
 
 
   return merged
 
 
end
 
end
  
  
function get_def(defName)
+
function getDef(defIDsuffix, defIDprefix)
   local base_def_table
+
   local ignoreKeys = {"Abstract", "Name", "ParentName"}
   local def_category
+
   local baseDef
 +
  local mergedDef
  
   for catK,_ in pairs(data) do
+
   if defIDprefix then
     for defK,def in pairs(data[catK]) do
+
    local defID = defIDprefix .. ":" .. defIDsuffix
       if defK == defName then
+
    baseDef = data[defID]
         base_def_table = def
+
    assert(not baseDef, string.format("getDef: Def '%s' not found", defID))
         def_category = catK
+
  else
 +
     for defID,def in pairs(data) do
 +
      -- WARNING: this depends on there not being any preexisting colons in the relevant substrings
 +
      prefix = string.match(defID, '(.+):')
 +
      suffix = string.match(defID, ':(.+)')
 +
       if suffix == defIDsuffix then
 +
         assert(not baseDef, string.format("getDef: Def conflict (more than one '%s')", defIDsuffix))
 +
         baseDef = def
 
       end
 
       end
 
     end
 
     end
 +
    assert(baseDef, string.format("getDef: Def '%s' not found", defIDsuffix))
 
   end
 
   end
  
   if not base_def_table then return nil end
+
   mergedDef = mergeParents(baseDef, ignoreKeys)
 
 
  local def = merge_def(base_def_table, def_category, {"ParentName", "Abstract"})
 
 
 
  -- add virtual keys
 
  if virtual_keys[def_category] then
 
    def._virtual = {}
 
    for k,func in pairs(virtual_keys[def_category]) do
 
      def._virtual[k] = func(def, data.Biomes)
 
    end
 
  end
 
 
 
--~  mw.logObject(def, "def")
 
  return def
 
end
 
  
 
+
   if virtual_keys[mergedDef.category] then
function getLabel(defName)
+
    mergedDef._virtual_ = {}
   local label
+
     for k,func in ipairs(virtual_keys[mergedDef.category]) do
  for catK,_ in pairs(data) do
+
       func(mergedDef)
     for defK,def in pairs(data[catK]) do
 
       if def["defName"] == defName then label = def["label"] end
 
 
     end
 
     end
 
   end
 
   end
  return label
 
end
 
  
function linkString(string)
+
   return mergedDef
   return "[[" .. string .. "]]"
 
 
end
 
end
  
------------
 
-- public --
 
------------
 
  
local p = {}
+
----------------------
 +
-- public interface --
 +
----------------------
  
function p.linkString(frame)
+
function p.getDefName(frame)
   local link = linkString(frame.args[1])
+
   local defName
 
+
   local label = frame.args[1]
  if not frame.args[1] then
 
    mw.logObject(frame.args, "frame.args")
 
    mw.log("missing argument #1 (string)")
 
  end
 
 
 
  return link
 
end
 
 
 
 
 
-- will expect frame.args[1] to be defName
 
function p.getLabel(frame)
 
   local label = getLabel(frame.args[1])
 
  
 
   if not label then
 
   if not label then
 
     mw.logObject(frame.args, "frame.args")
 
     mw.logObject(frame.args, "frame.args")
     mw.log(string.format("'%s' not found", frame.args[1]))
+
     mw.log("getDefName: missing argument #1 (label)")
 +
    return nil
 
   end
 
   end
  
  return label
+
   for defID,def in pairs(data) do
end
+
     if string.upper(def.label or "") == string.upper(label) then
 
+
      defName = def.label
-- will expect frame.args[1] to be the label
 
function p.getDefName(frame)
 
  local defName
 
   for catK,_ in pairs(data) do
 
     for defK,def in pairs(data[catK]) do
 
      if def["label"] then
 
        if string.upper(def["label"]) == string.upper(frame.args[1]) then defName = defK end
 
      end
 
 
     end
 
     end
 
   end
 
   end
Line 222: Line 230:
 
   if not defName then
 
   if not defName then
 
     mw.logObject(frame.args, "frame.args")
 
     mw.logObject(frame.args, "frame.args")
     mw.log(string.format("'%s' not found", frame.args[1]))
+
     mw.log(string.format("getDefName: '%s' not found", label))
 
   end
 
   end
  
 
   return defName
 
   return defName
 
end
 
end
 +
  
 
function p.count(frame)
 
function p.count(frame)
Line 233: Line 242:
 
end
 
end
  
-- one function to rule them all, and in the darkness bind them
+
 
 
function p.query(frame)
 
function p.query(frame)
 +
  local argLen = util.table.count(frame.args, "number") -- #frame.args won't work as expected, check the doc
  
   -- implement shitloads of checks for arguments and the log so we know what's going on
+
   -- implement expressive argument checks so we know what's going on
 
   -- use them as a kind of usage guide (give as much info as possible)
 
   -- use them as a kind of usage guide (give as much info as possible)
  -- if wrong arguments are passed to private functions they will cause errors (they better)
 
  
 
   if not frame.args[1] then
 
   if not frame.args[1] then
 
     mw.logObject(frame.args, "frame.args")
 
     mw.logObject(frame.args, "frame.args")
     mw.log("missing argument #1 (defName)")
+
     mw.log("query: missing argument #1 (defName or Name, for abstract Defs)")
 
     return nil
 
     return nil
 
   end
 
   end
  
   local def = get_def(frame.args[1])
+
   local def = getDef(frame.args[1])
  
 
   if not def then
 
   if not def then
 
     mw.logObject(frame.args, "frame.args")
 
     mw.logObject(frame.args, "frame.args")
     mw.log(string.format("bad argument #1 ('%s' not found)", frame.args[1]))
+
     mw.log(string.format("query: bad argument #1 ('%s' not found)", frame.args[1]))
 
     return nil
 
     return nil
 
   end
 
   end
Line 256: Line 265:
 
   local prune = def
 
   local prune = def
  
   -- #frame.args won't work as expected, check the doc
+
   for i,arg in ipairs(frame.args) do -- arguments
  local arg_count = util.count(frame.args, "number")
+
 
 +
    arg = tonumber(arg) or arg -- frame.args are always strings on MediaWiki so convert back the numbers
  
  -- look at all the beautiful ifs!
+
     -- NOTE: might consider doing something about the if tree (trim it down a bit)
  for i,arg in ipairs(frame.args) do
 
     -- frame.args are always strings on MediaWiki so convert the numbers back to numbers
 
    arg = tonumber(arg) or arg
 
  
    -- do stuff for additional arguments
+
     if i > 1 then -- additional arguments
     if i > 1 then
 
  
      -- special checks for the final argument
+
       if i == argLen then -- if final argument
       if i == arg_count then
 
  
        -- sibling
+
         if frame.args["sibling"] then -- sibling
         if frame.args["sibling"] then
 
 
           prune = search.conductor({nil, frame.args["sibling"]} , prune)
 
           prune = search.conductor({nil, frame.args["sibling"]} , prune)
 
           if not prune then
 
           if not prune then
 
             mw.logObject(frame.args, "frame.args")
 
             mw.logObject(frame.args, "frame.args")
             mw.log(string.format("bad argument 'sibling' ('%s' not found in '%s')", frame.args["sibling"], frame.args[i-1]))
+
             mw.log(string.format("query: bad argument 'sibling' ('%s' not found in '%s')", frame.args["sibling"], frame.args[i-1]))
 
             return nil
 
             return nil
 
           else
 
           else
Line 281: Line 285:
 
             if not prune then
 
             if not prune then
 
               mw.logObject(frame.args, "frame.args")
 
               mw.logObject(frame.args, "frame.args")
               mw.log(string.format("bad argument #%i ('%s' is not a sibling of '%s')", i, arg, frame.args["sibling"]))
+
               mw.log(string.format("query: bad argument #%i ('%s' is not a sibling of '%s')", i, arg, frame.args["sibling"]))
 
             end
 
             end
 
           end
 
           end
Line 288: Line 292:
 
           if not prune then
 
           if not prune then
 
             mw.logObject(frame.args, "frame.args")
 
             mw.logObject(frame.args, "frame.args")
             mw.log(string.format("bad argument #%i ('%s' not found in '%s')", i, frame.args[i], frame.args[i-1]))
+
             mw.log(string.format("query: bad argument #%i ('%s' not found in '%s')", i, frame.args[i], frame.args[i-1]))
 
             return nil
 
             return nil
 
           else
 
           else
 
             prune = prune.value
 
             prune = prune.value
 
           end
 
           end
         end
+
         end -- sibling
  
       else
+
       else -- if not final argument
 
         prune = search.conductor(arg, prune)
 
         prune = search.conductor(arg, prune)
 
         if not prune then
 
         if not prune then
 
           mw.logObject(frame.args, "frame.args")
 
           mw.logObject(frame.args, "frame.args")
           mw.log(string.format("bad argument #%i ('%s' not found in '%s')", i, frame.args[i], frame.args[i-1]))
+
           mw.log(string.format("query: bad argument #%i ('%s' not found in '%s')", i, frame.args[i], frame.args[i-1]))
 
           return nil
 
           return nil
 
         else
 
         else
Line 306: Line 310:
 
       end
 
       end
  
     end
+
     end -- additional arguments
  
   end
+
   end -- for arguments
  
 
   if type(prune) == "table" then mw.logObject(prune) end
 
   if type(prune) == "table" then mw.logObject(prune) end
 +
 
   return prune
 
   return prune
 
end
 
end
  
-------------------------------------------------------------------
+
---------------------------------
-- simulate MediaWiki/Scribunto module invocation using 'frame' --
+
-- simulate module invocation  --
-------------------------------------------------------------------
+
---------------------------------
  
 
local simframe = { ["args"] = {} }
 
local simframe = { ["args"] = {} }
 +
frame = frame or simframe
  
simframe.args[1] = "GuineaPig"
+
--~ simframe.args[1] = "fennec fox"
simframe.args[2] = "lives_in"
+
simframe.args[1] = "Hare"
 
+
--~ simframe.args[2] = "eats"
frame = frame or simframe
 
  
 
if ENV == "dev" then
 
if ENV == "dev" then
  
   local lives_in = p.query(frame)
+
--~   p.query(frame)
   for i,biome in ipairs(lives_in) do
+
   pp(p.query(frame))
    lives_in[i] = linkString(getLabel(biome))
+
--~  pp(p.getDefName(frame))
  end
 
 
 
  pinspect(p.query(frame))
 
  pinspect(lives_in)
 
--~  pinspect(p.getLabel(frame))
 
  print(linkString("foo"))
 
  
 
end
 
end
Line 355: Line 354:
 
end
 
end
  
return p
+
return p -- return module

Revision as of 14:24, 9 May 2021

This module is used for development.

Purpose

This module is used to query information from the uploaded and parsed game files.

Its main purpose is to populate the infoboxes.

Usage

A note on the order of named parameters. All of the parameters that look like ...=... are called named parameters and their order is not important (this is true for all templates).

query

{{#invoke:Test|query|<def ID>[|...|][|tag|][|sibling=...]}}

The work-horse. Output varies based on use:

If only the <def ID> parameter is set, it will show the whole Def in the log.
If simple values are queried it will return them.
If lists are queried it will return nothing but call {{#vardefine}} on all the simple values within it. What got defined can be seen in the page's log.

Named parameters:

<def ID>
This parameter identifies the Def so it is mandatory. It can take two forms, if both are defined then defName takes preference.
defName=<defName>
<defName> (case sensitive) should be replaced with the actual defName of a Def.
label=<label>
<label> (case insensitive) should be replaced with the actual label of a Def.
[sibling=...] (optional) (case sensitive)
Allows querying for something if we know its sibling's value (works only for values at the moment).

Anonymous parameters:

[|...|] (optional) (case sensitive)
Anonymous paramaters before the last one ([tag]) are here to help uniquely identify it. If the [tag] is already unique within a Def tree, then these additional parameters are not needed.
[|tag|] (optional) (case sensitive)
The final anonymous parameter defines what is to be queried.

count

{{#invoke:Test|count|<def ID>[|...|][|tag|][|sibling=...]}}

Parameters are the same as for query. It's basically a wrapped up query that behaves a bit differently.

The difference is in how it handles lists. If a list is queried, unlike query, it will return the length of the list.

How-to

Take a look at a Def

{{#invoke:Test|query|label=desert}}

Lua error at line 60: table from mw.loadData is read-only.

Data is in the log.

Retrieve a simple value

{{#invoke:Test|query|defName=Caribou|description}}

Lua error at line 60: table from mw.loadData is read-only.

Dealing with lists

{{#invoke:Test|query|defName=Mech_Scyther|tools}}

Lua error at line 60: table from mw.loadData is read-only.

When a list is retrieved there will be no output but the log will contain a list of defined variables.

For convenience the list is reprinted here:

tools_1_linkedBodyPartsGroup = LeftBlade
tools_1_cooldownTime = 2
tools_1_label = left blade
tools_1_DPS = 10
tools_1_power = 20
tools_1_capacities_1 = Cut
tools_1_capacities_2 = Stab
tools_2_linkedBodyPartsGroup = RightBlade
tools_2_cooldownTime = 2
tools_2_label = right blade
tools_2_DPS = 10
tools_2_power = 20
tools_2_capacities_1 = Cut
tools_2_capacities_2 = Stab
tools_3_linkedBodyPartsGroup = HeadAttackTool
tools_3_capacities_1 = Blunt
tools_3_label = head
tools_3_DPS = 4.5
tools_3_chanceFactor = 0.2
tools_3_power = 9
tools_3_cooldownTime = 2

All of the above can be accessed with the use of {{#var:...}}.

{{#var:tools_1_DPS}}

DPS is not a normal member of this table but has been added with Lua. Let's call it a virtual field.

Retrieve something if a sibling is known

{{#invoke:Test|query|label=guinea pig|minAge|sibling=AnimalAdult}}

Lua error at line 60: table from mw.loadData is read-only.


local p = {}

------------------------------------------------------------------
-- deal with differences between MediaWiki and dev environments --
------------------------------------------------------------------

if mw then
  ENV = "wiki"
  log = mw.log

  util = require("Module:Test/lib/util")
  search = require("Module:Test/lib/search")
else
  ENV = "dev"

  mw = {}
  log = {}

  inspect = require './lib/inspect'
  util = require("./lib/util")
  search = require("./lib/search")
  diet = require("./data/diet")

  function pp(tbl, title) -- pretty print tables
    util.hl(title)
    print(inspect(tbl))
  end

  -- (re)define used mw functions that don't exist in dev environment
  mw.logObject = function(obj, prefix)
    if prefix then
      assert(type(prefix) == "string")
      table.insert(log, prefix .. " = " .. inspect(obj))
    else
      table.insert(log, inspect(obj))
    end
  end

  mw.dumpObject = function(arg)
    return inspect(arg)
  end

  mw.log = function(arg)
    table.insert(log, arg)
  end
end

---------------
-- load data --
---------------

if ENV == "dev" then
  data = loadfile("../output.lua")()
  diet = loadfile("./data/diet.lua")()
elseif ENV == "wiki" then
  data = mw.loadData('Module:Test/data')
end

version = data.version
data.version = nil

------------------
-- virtual keys --
------------------

local virtual_store = {}
local virtual_keys = {
  ["Pawn"] = {

    function (def)
      virtField = "lives_in"
      biomes = {}
      for k,v in pairs(data) do
        prefix = string.match(k, '(.+):')
        if prefix == "BiomeDef" then
          table.insert(biomes, v)
        end
      end

      local list = {}
      for _,biome in pairs(biomes) do
        for animal,_ in pairs(biome.wildAnimals or {}) do
          if def.defName == animal then
            table.insert(list, biome.label)
          end
        end
      end

      def._virtual_[virtField] = list
    end,

    function (def)
      virtField = "foodTypes"
      foodTypes = def.race.foodType
      flags = {}
      virtual_store.diet = {}

      for _,foodType in ipairs(foodTypes) do
        for foodItem,_ in pairs(diet.foodType[foodType]) do
          flags[foodItem] = true
        end
      end

      for flag,_ in pairs(flags) do
        table.insert(virtual_store.diet, flag)
      end

      def._virtual_[virtField] = virtual_store.diet
    end,

    function (def)
      virtField = "foodTypesExpanded"
      flags = {}
      eats = {}

      for _,def in pairs(data) do
        if def.defName and def.ingestible and def.ingestible.foodType then
            for _,ingestible in ipairs(def.ingestible.foodType) do
              for _,dietV in ipairs(virtual_store.diet) do
                if ingestible == dietV then
                  flags[def.defName] = true
                end
              end
            end
          end
        end

      for flag,_ in pairs(flags) do
        table.insert(eats, flag)
      end

      def._virtual_[virtField] = eats
    end,

  }
}

-----------------------
-- private functions --
-----------------------

local function vardefine(name, value)
  local f_name = "vardefine"
  assert(var_name, string.format("bad argument #1 to '%s' (argument missing, name of variable to define)", f_name))
  assert(var_name == "string", string.format("bad argument #1 to '%s' (string expected, got %s)", f_name, type(var_name)))
  assert(var_value, string.format("bad argument #2 to '%s' (argument missing, value to assign to variable)", f_name))
  assert(var_value == "string" or var_value == "number", string.format("bad argument #2 to '%s' (string or number expected, got %s)", f_name, type(var_value)))

  frame:callParserFunction('#vardefine', var_name, var_value)
end


local function mergeParents(baseDef, ignoreKeys)
  local ancestorIDs = {}
  local mergedDef = {}
  local def = baseDef

  while def._.ParentName do
    local parentID = def._.DefCategory .. ":" .. def._.ParentName
    table.insert(ancestorIDs, parentID)
    def = data[parentID]
  end

  ancestorIDs = util.table.reverse(ancestorIDs)
  table.insert(ancestorIDs, baseDef._.DefCategory .. ":" .. baseDef.defName)

  for _,parentID in ipairs(ancestorIDs) do
    util.table.overwrite(mergedDef, data[parentID], ignoreKeys)
  end

  return mergedDef
end


function getDef(defIDsuffix, defIDprefix)
  local ignoreKeys = {"Abstract", "Name", "ParentName"}
  local baseDef
  local mergedDef

  if defIDprefix then
    local defID = defIDprefix .. ":" .. defIDsuffix
    baseDef = data[defID]
    assert(not baseDef, string.format("getDef: Def '%s' not found", defID))
  else
    for defID,def in pairs(data) do
      -- WARNING: this depends on there not being any preexisting colons in the relevant substrings
      prefix = string.match(defID, '(.+):')
      suffix = string.match(defID, ':(.+)')
      if suffix == defIDsuffix then
        assert(not baseDef, string.format("getDef: Def conflict (more than one '%s')", defIDsuffix))
        baseDef = def
      end
    end
    assert(baseDef, string.format("getDef: Def '%s' not found", defIDsuffix))
  end

  mergedDef = mergeParents(baseDef, ignoreKeys)

  if virtual_keys[mergedDef.category] then
    mergedDef._virtual_ = {}
    for k,func in ipairs(virtual_keys[mergedDef.category]) do
      func(mergedDef)
    end
  end

  return mergedDef
end


----------------------
-- public interface --
----------------------

function p.getDefName(frame)
  local defName
  local label = frame.args[1]

  if not label then
    mw.logObject(frame.args, "frame.args")
    mw.log("getDefName: missing argument #1 (label)")
    return nil
  end

  for defID,def in pairs(data) do
    if string.upper(def.label or "") == string.upper(label) then
      defName = def.label
    end
  end

  if not defName then
    mw.logObject(frame.args, "frame.args")
    mw.log(string.format("getDefName: '%s' not found", label))
  end

  return defName
end


function p.count(frame)
  local query = p.query(frame)
  return #query
end


function p.query(frame)
  local argLen = util.table.count(frame.args, "number") -- #frame.args won't work as expected, check the doc

  -- implement expressive argument checks so we know what's going on
  -- use them as a kind of usage guide (give as much info as possible)

  if not frame.args[1] then
    mw.logObject(frame.args, "frame.args")
    mw.log("query: missing argument #1 (defName or Name, for abstract Defs)")
    return nil
  end

  local def = getDef(frame.args[1])

  if not def then
    mw.logObject(frame.args, "frame.args")
    mw.log(string.format("query: bad argument #1 ('%s' not found)", frame.args[1]))
    return nil
  end

  local prune = def

  for i,arg in ipairs(frame.args) do -- arguments

    arg = tonumber(arg) or arg -- frame.args are always strings on MediaWiki so convert back the numbers

    -- NOTE: might consider doing something about the if tree (trim it down a bit)

    if i > 1 then -- additional arguments

      if i == argLen then -- if final argument

        if frame.args["sibling"] then -- sibling
          prune = search.conductor({nil, frame.args["sibling"]} , prune)
          if not prune then
            mw.logObject(frame.args, "frame.args")
            mw.log(string.format("query: bad argument 'sibling' ('%s' not found in '%s')", frame.args["sibling"], frame.args[i-1]))
            return nil
          else
            prune = prune.parent.table[arg]
            if not prune then
              mw.logObject(frame.args, "frame.args")
              mw.log(string.format("query: bad argument #%i ('%s' is not a sibling of '%s')", i, arg, frame.args["sibling"]))
            end
          end
        else
          prune = search.conductor(arg, prune)
          if not prune then
            mw.logObject(frame.args, "frame.args")
            mw.log(string.format("query: bad argument #%i ('%s' not found in '%s')", i, frame.args[i], frame.args[i-1]))
            return nil
          else
            prune = prune.value
          end
        end -- sibling

      else -- if not final argument
        prune = search.conductor(arg, prune)
        if not prune then
          mw.logObject(frame.args, "frame.args")
          mw.log(string.format("query: bad argument #%i ('%s' not found in '%s')", i, frame.args[i], frame.args[i-1]))
          return nil
        else
          prune = prune.value
        end
      end

    end -- additional arguments

  end -- for arguments

  if type(prune) == "table" then mw.logObject(prune) end

  return prune
end

---------------------------------
-- simulate module invocation  --
---------------------------------

local simframe = { ["args"] = {} }
frame = frame or simframe

--~ simframe.args[1] = "fennec fox"
simframe.args[1] = "Hare"
--~ simframe.args[2] = "eats"

if ENV == "dev" then

--~   p.query(frame)
  pp(p.query(frame))
--~   pp(p.getDefName(frame))

end

local clock = string.format("os.clock(): %i ms", os.clock() * 1000)
mw.log("--" .. string.rep("-", #clock) .. "--")
mw.log("- " .. clock .. " -")
mw.log("--" .. string.rep("-", #clock) .. "--")

----------------------------------------
-- simulate wiki log while developing --
----------------------------------------

if ENV == "dev" then
  util.hl("log")
  for _,v in ipairs(log) do
    print(v)
  end
end

return p -- return module