Jump to content

Module:Lorebook/Debug

From example

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

local p = {}

-- 调试模式标志
local DEBUG = true

local function debug_log(msg)
  if DEBUG then
    return "<!-- DEBUG: " .. msg .. " -->\n"
  end
  return ""
end

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 ~= '' then
      local needle = n:lower()
      -- Escape special pattern characters
      local escaped = needle:gsub('[%^%$%(%)%%%.%[%]%*%+%-%?]', '%%%1')
      
      -- Try multiple matching strategies
      -- 1. Exact match
      if s == needle then
        return true
      end
      
      -- 2. Word boundary patterns
      local patterns = {
        '^' .. escaped .. '%W',    -- At start followed by non-word
        '%W' .. escaped .. '%W',   -- Surrounded by non-word chars
        '%W' .. escaped .. '$',    -- Preceded by non-word at end
        '^' .. escaped .. '$'      -- Exact match (redundant but safe)
      }
      
      for _, pattern in ipairs(patterns) do
        if s:find(pattern) then
          return true
        end
      end
      
      -- 3. Simple substring match as last resort
      if s:find(escaped, 1, true) then
        return true
      end
    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 normalizeValue(val)
  if type(val) == 'table' then
    return val[1] or ''
  end
  return val or ''
end

local function getAllConcepts(world)
  local query = {
    '[[Category:Concept]][[Belongs to world::' .. world .. ']]',
    '?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
  }
  
  local res = mw.smw.ask(query)
  
  if not res then return {} end
  
  local normalized = {}
  for i, row in ipairs(res) do
    -- Debug: log raw row structure for first result
    if DEBUG and i == 1 then
      local debug_info = "Raw row[1]: " .. tostring(row[1]) .. " (type: " .. type(row[1]) .. ")\n"
      debug_info = debug_info .. "Raw row.primary: " .. tostring(row.primary) .. " (type: " .. type(row.primary) .. ")\n"
      debug_info = debug_info .. "Raw row['Primary keys']: " .. tostring(row['Primary keys']) .. " (type: " .. type(row['Primary keys']) .. ")\n"
      
      -- List all keys in the row table
      debug_info = debug_info .. "\nAll keys in row:\n"
      for k, v in pairs(row) do
        debug_info = debug_info .. "  [" .. tostring(k) .. "] = " .. tostring(v) .. " (type: " .. type(v) .. ")\n"
      end
      
      -- Store debug info in a global to access later
      _G._debug_row_info = debug_info
    end
    
    normalized[i] = {
      page = normalizeValue(row[1]),  -- First element is the page title
      plist = normalizeValue(row.plist),
      alichat = normalizeValue(row.alichat),
      primary = normalizeValue(row.primary),
      secondary = normalizeValue(row.secondary),
      logic = normalizeValue(row.logic),
      mode = normalizeValue(row.mode),
      parent = normalizeValue(row.parent),
      nonrec = normalizeValue(row.nonrec),
      placement = normalizeValue(row.placement)
    }
  end
  
  return normalized
end

local function indexByTitle(rows)
  local idx = {}
  for _, r in ipairs(rows) do
    if r.page and r.page ~= '' then
      idx[r.page] = r
    end
  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
    if #plistChunks > 0 then
      text = text .. '\n'  -- Add extra newline between plist and chat sections
    end
    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 output = debug_log("Starting inject function")
  output = output .. debug_log("World: " .. world)
  output = output .. debug_log("Context: " .. haystack)
  
  local rows = getAllConcepts(world)
  output = output .. debug_log("Found " .. #rows .. " concepts")
  
  -- Show raw row debug info
  if _G._debug_row_info then
    output = output .. "\n'''Raw SMW data (first row):'''\n<pre>\n" .. _G._debug_row_info .. "</pre>\n"
    _G._debug_row_info = nil  -- Clear after use
  end
  
  if #rows == 0 then
    output = output .. "\n'''⚠️ No concepts found for world: " .. world .. "'''\n\n"
    output = output .. "Possible issues:\n"
    output = output .. "* Concepts not created yet\n"
    output = output .. "* Concepts not using {{Concept}} template\n"
    output = output .. "* SMW properties not set correctly\n"
    output = output .. "* SMW data cache needs refresh (re-save concept pages)\n\n"
    output = output .. "Debug query: <code><nowiki>{{#ask: [[Category:Concept]] [[Belongs to world::" .. world .. "]] }}</nowiki></code>\n"
    return output
  end
  
  -- 显示找到的概念列表(调试)
  if DEBUG then
    output = output .. "\n'''Found concepts:'''\n"
    for i, r in ipairs(rows) do
      local primary_debug = "none"
      if r.primary then
        primary_debug = tostring(r.primary) .. " (type: " .. type(r.primary) .. ")"
      end
      output = output .. "* " .. (r.page or "unnamed") .. 
                        " (mode: " .. (r.mode or "conditional") .. 
                        ", primary: " .. primary_debug .. ")\n"
    end
    output = output .. "\n"
  end
  
  local idx = indexByTitle(rows)
  
  local start = {}
  for _, r in ipairs(rows) do
    if shouldTrigger(r, haystack) then 
      table.insert(start, r.page)
      output = output .. debug_log("Triggered: " .. r.page)
    end
  end
  
  output = output .. debug_log("Triggered " .. #start .. " concepts")
  
  if #start == 0 then
    output = output .. "\n'''ℹ️ No concepts triggered for context: " .. haystack .. "'''\n\n"
    output = output .. "Available trigger keys:\n"
    for _, r in ipairs(rows) do
      if r.primary and r.primary ~= '' then
        output = output .. "* " .. r.page .. ": <code>" .. r.primary .. "</code>\n"
      end
    end
    return output
  end
  
  local triggered = walkRecursion(rows, idx, start)
  output = output .. debug_log("After recursion: " .. #triggered .. " concepts")
  
  local result = buildInjection(rows, triggered)
  
  if DEBUG then
    output = output .. "\n'''Injected concepts:'''\n"
    for _, t in ipairs(triggered) do
      output = output .. "* " .. t .. "\n"
    end
    output = output .. "\n'''=== Output ==='''\n<pre>\n"
  end
  
  -- Escape curly braces and use <pre> tag to preserve formatting
  if result and result ~= '' then
    result = result:gsub('{', '&#123;'):gsub('}', '&#125;')
    output = output .. result .. '\n</pre>\n'
  else
    output = output .. "(empty output)\n</pre>\n"
  end
  
  return output
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