Jump to content

Module:Lorebook

From example
Revision as of 17:42, 17 October 2025 by M4tsuri (talk | contribs)

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

local p = {}

local function splitCSV(s)
  if not s or s == '' then return {} end
  local t = {}
  for token in mw.text.gsplit(s, ',', true) do
    token = mw.text.trim(token)
    if token ~= '' then table.insert(t, token:lower()) end
  end
  return t
end

local function containsAny(haystack, needles)
  local s = ' ' .. haystack:lower() .. ' '
  for _, n in ipairs(needles) do
    if n ~= '' and s:find('%f[%w]' .. mw.ustring.gsub(mw.text.nowiki(n), '([^%w])', '%%%1') .. '%f[%w]') then
      return true
    end
  end
  return false
end

local function anyMatch(haystack, primary, _secondary)
  return containsAny(haystack, primary)
end

local function andMatch(haystack, primary, secondary)
  return containsAny(haystack, primary) and containsAny(haystack, secondary)
end

local function notMatch(haystack, primary, secondary)
  return containsAny(haystack, primary) and not containsAny(haystack, secondary)
end

local logicDispatch = {
  ANY = anyMatch,
  AND = andMatch,
  NOT = notMatch,
}

local function getAllConcepts(world)
  local query = string.format('[[Category:Concept]][[Belongs to world::%s]]', world)
  local res = mw.smw.ask(query, {
    mainlabel = 'page',
    ['?Plist'] = 'plist',
    ['?AliChat'] = 'alichat',
    ['?Primary keys'] = 'primary',
    ['?Secondary keys'] = 'secondary',
    ['?Logic'] = 'logic',
    ['?Key mode'] = 'mode',
    ['?Parent concept'] = 'parent',
    ['?Non-recursable'] = 'nonrec',
    ['?Placement'] = 'placement',
    limit = 999
  })
  return res or {}
end

local function indexByTitle(rows)
  local idx = {}
  for _, r in ipairs(rows) do
    idx[r.page] = r
  end
  return idx
end

local function shouldTrigger(row, haystack)
  local mode = (row.mode or 'conditional'):upper()
  if mode == 'DISABLED' then return false end
  if mode == 'CONSTANT' then return true end
  local logic = (row.logic or 'ANY'):upper()
  local f = logicDispatch[logic] or anyMatch
  local primary = splitCSV(row.primary)
  local secondary = splitCSV(row.secondary)
  return f(haystack, primary, secondary)
end

local function walkRecursion(rows, idx, startPages)
  local children = {}
  for _, r in ipairs(rows) do
    local parent = r.parent
    if parent and parent ~= '' then
      children[parent] = children[parent] or {}
      table.insert(children[parent], r.page)
    end
  end

  local visited = {}
  local stack = {}
  for _, ptitle in ipairs(startPages) do table.insert(stack, ptitle) end

  local ordered = {}
  while #stack > 0 do
    local cur = table.remove(stack)
    if not visited[cur] then
      visited[cur] = true
      table.insert(ordered, cur)
      local row = idx[cur]
      if row and (row.nonrec or ''):lower() ~= 'yes' then
        for _, c in ipairs(children[cur] or {}) do
          table.insert(stack, c)
        end
      end
    end
  end
  return ordered
end

local function buildInjection(rows, triggered)
  local plistChunks, chatChunks = {}, {}
  for _, title in ipairs(triggered) do
    local r
    for _, row in ipairs(rows) do if row.page == title then r = row break end end
    if r then
      if r.plist and r.plist ~= '' then table.insert(plistChunks, r.plist) end
      if r.alichat and r.alichat ~= '' then table.insert(chatChunks, r.alichat) end
    end
  end
  local text = ''
  if #plistChunks > 0 then
    text = text .. table.concat(plistChunks, '\n') .. '\n'
  end
  if #chatChunks > 0 then
    text = text .. table.concat(chatChunks, '\n')
  end
  return text
end

function p.inject(frame)
  local world = frame.args.world or 'Farlandia'
  local haystack = frame.args.context or ''
  local rows = getAllConcepts(world)
  local idx = indexByTitle(rows)

  local start = {}
  for _, r in ipairs(rows) do
    if shouldTrigger(r, haystack) then table.insert(start, r.page) end
  end

  local triggered = walkRecursion(rows, idx, start)
  return buildInjection(rows, triggered)
end

function p.export(frame)
  local world = frame.args.world or 'Farlandia'
  local rows = getAllConcepts(world)
  local entries = {}
  for _, r in ipairs(rows) do
    table.insert(entries, {
      title = r.page,
      plist = r.plist or '',
      alichat = r.alichat or '',
      primary = r.primary or '',
      secondary = r.secondary or '',
      logic = (r.logic or 'ANY'),
      mode = (r.mode or 'conditional'),
      parent = r.parent or '',
      nonrecursable = r.nonrec or '',
      placement = r.placement or ''
    })
  end
  return mw.text.jsonEncode(entries)
end

return p