Module:Lorebook
Appearance
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 ~= '' then
-- Escape special pattern characters
local escaped = n:gsub('[%^%$%(%)%%%.%[%]%*%+%-%?]', '%%%1')
-- Try word boundary pattern with fallback to simple match
local patterns = {
'%f[%w]' .. escaped .. '%f[%W]', -- Word boundaries (Lua 5.2+)
'[%W]' .. escaped .. '[%W]', -- Non-word char boundaries
'^' .. escaped .. '[%W]', -- Start of string
'[%W]' .. escaped .. '$', -- End of string
'^' .. escaped .. '$' -- Exact match
}
for _, pattern in ipairs(patterns) do
if s:find(pattern) then
return true
end
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)
-- SMW returns property values as tables sometimes
if type(val) == 'table' then
return val[1] or ''
end
return val or ''
end
local function getAllConcepts(world)
local query = string.format('[[Category:Concept]][[Belongs to world::%s]]', world)
local res = mw.smw.ask(query, {
mainlabel = '-',
['?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
})
if not res then return {} end
-- Normalize all property values
local normalized = {}
for i, row in ipairs(res) do
normalized[i] = {
page = normalizeValue(row[1]),
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
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