Module:Lorebook/Debug
Appearance
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('{', '{'):gsub('}', '}')
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