From Arms of God Wiki

Revision as of 06:08, 10 June 2026 by Ta1ha (talk | contribs) (bot: operator iteration 2026-06-10d (hover tooltips / filter chips + DPS sort / damage-type hubs / stat reverse-lookup pages))

Shared helper that renders sortable index tables from Data: pages.

Library module shipped by the publishing bot; shared across categories. Bot-published — edits are overwritten on re-publish.


-- Module:Index — sortable-wikitable renderer for category index pages.
-- Public API: Index.render(records, opts).
-- API contract:
--   records : list of Phase-4.5 flattened records (NOT a dict).
--   opts    : { columns = {{name, source}, ...}, thumbnail_size, sort_default }
-- Column `source` semantics:
--   "thumbnail" → [[File:<rec.icon>|<thumbnail_size>px|alt=<rec.name>]] (em-dash on empty icon)
--   "name"      → rec.name (bare)
--   "link"      → [[<rec.slug>|<rec.name>]]
--   any other   → rec.infobox[source], fallback rec[source], fallback em-dash
-- Maintainer don'ts: never recompute a slug here; never call Slugs.lookup;
-- never gsub a display name. Plans use the infobox `field` label as `source`,
-- not the raw entity-source field name.

local p = {}

local function _isEmpty(v)
  return v == nil or v == ''
end

local function _flatten(v)
  if type(v) ~= 'table' then return tostring(v) end
  local parts = {}
  for i = 1, #v do
    local item = v[i]
    if type(item) == 'table' and item.name then
      table.insert(parts, tostring(item.name))
    else
      table.insert(parts, tostring(item))
    end
  end
  return table.concat(parts, ', ')
end

local function _cell(rec, source, opts)
  if source == 'thumbnail' then
    local icon = rec.icon
    if _isEmpty(icon) then return '—' end
    return string.format('[[File:%s|%dpx|alt=%s]]',
      tostring(icon),
      opts.thumbnail_size or 48,
      tostring(rec.name or rec.id or ''))
  end
  if source == 'name' then
    if _isEmpty(rec.name) then return '—' end
    return tostring(rec.name)
  end
  if source == 'link' then
    local slug = rec.slug or rec.name or rec.id
    if _isEmpty(slug) then return '—' end
    return '<span class="wm-tip" data-tip-title="' .. tostring(slug)
      .. '">[[' .. tostring(slug) .. '|' .. tostring(rec.name or slug) .. ']]</span>'
  end
  -- Plan-declared infobox label first; raw record field as fallback
  -- (covers top-level scalars like `family_key`).
  local v = (rec.infobox and rec.infobox[source])
  if _isEmpty(v) then v = rec[source] end
  if _isEmpty(v) then return '—' end
  return _flatten(v)
end

-- Stable sort by the column whose name matches sort_default. Sorting
-- happens on the textual cell value (case-insensitive). Records that
-- render '—' sort last regardless of column.
local function _sort(records, columns, sort_default, opts)
  if not sort_default or #records < 2 then return records end
  local sort_col
  for _, col in ipairs(columns or {}) do
    if col.name == sort_default then sort_col = col; break end
  end
  if not sort_col then return records end
  local source = sort_col.source
  -- Case-folded sort key for the chosen column. NOTE: this case-fold
  -- is for SORT ORDER only — it is NOT a slug operation. The library
  -- never derives a link target from a display name (slugs come from
  -- rec.slug). Using `string.lower(...)` (function-call form) rather
  -- than `:lower()` keeps the F1 lint clean: the lint catches the
  -- method form because that's the shape slug-computation code in
  -- prior runs took.
  local function key(rec)
    local c = _cell(rec, source, opts)
    if c == '—' then return string.char(255) end  -- last
    return string.lower(c)
  end
  local list = {}
  for i, r in ipairs(records) do list[i] = r end
  table.sort(list, function(a, b) return key(a) < key(b) end)
  return list
end

function p.render(records, opts)
  records = records or {}
  opts    = opts or {}
  local columns = opts.columns or {}
  if #columns == 0 or #records == 0 then return '' end

  local ordered = _sort(records, columns, opts.sort_default, opts)

  local rows = { '{| class="wikitable sortable" style="background:var(--table-row-odd, #1b1c20);color:var(--table-text, #e6e6e6);border-color:var(--table-border, #3a3c44);"' }
  -- Header row. MediaWiki inline header separator is `!!`, not single `!` —
  -- `! Icon ! Name` parses as ONE cell with literal `!` text. Use `!!`.
  local hdr = {}
  for _, col in ipairs(columns) do
    table.insert(hdr, 'style="background:var(--table-header-bg, #26272e);color:var(--infobox-header-fg, #f1e9d2);" | ' .. tostring(col.name))
  end
  table.insert(rows, '! ' .. table.concat(hdr, ' !! '))
  -- Body rows.
  for _, rec in ipairs(ordered) do
    local attrs = ''
    local tier = rec.infobox and rec.infobox.Tier
    if tier ~= nil and tier ~= '' then
      attrs = ' data-tier="' .. tostring(tier) .. '"'
    end
    table.insert(rows, '|-' .. attrs)
    local cells = {}
    for _, col in ipairs(columns) do
      table.insert(cells, _cell(rec, col.source, opts))
    end
    -- F2/F9-safe: column separators are plain `||` on a single `|`
    -- line, OUTSIDE any #if. No conditional cells.
    table.insert(rows, '| ' .. table.concat(cells, ' || '))
  end
  table.insert(rows, '|}')

  -- F2 (SC-03): preprocess so any {{Template:...}} embedded in a cell
  -- (rare under the Phase 4.5 contract — most cells are already plain
  -- strings — but possible for caption-style cells) expands. Plain
  -- `return` from #invoke does NOT trigger another template-expansion
  -- pass.
  return mw.getCurrentFrame():preprocess(table.concat(rows, '\n'))
end

return p