Module:Reference
Appearance
Documentation for this module may be created at Module:Reference/doc
-- English Martinus Wiki reference renderer.
--
-- Stage 1 source artifact. This module deliberately contains the routing and
-- validation policy in one small, data-driven module so it can be copied to
-- Module:Reference after Scribunto is enabled. It does not call ParserFunctions.
-- The public entry points are p.ref (the template wrapper) and p.render (a
-- direct module entry point); all state is local to an invocation.
local p = {}
local EN_TTT = "https://www.martinus.dk/en/online-library/ttt/index.php"
local DA_TTT = "https://www.martinus.dk/da/onlinebibliotek/dtt/index.php"
local EN_SYMBOLS = "https://www.martinus.dk/en/online-library/symbols/index.php"
local DA_LECTURES = "https://www.martinus.dk/da/onlinebibliotek/foredrag/index.php"
local ERROR_CATEGORY = "[[Category:Pages with Martinus reference errors]]"
-- The section ranges are the ranges shown by the official online library. LB6
-- and LB7 are retained for compatibility, but their English flag is false until
-- an English text is confirmed by the Institute.
local BOOKS = {
LB1 = { id = 51, title = "Livets Bog 1", first = 1, last = 288, english = true },
LB2 = { id = 52, title = "Livets Bog 2", first = 289, last = 638, english = true },
LB3 = { id = 53, title = "Livets Bog 3", first = 639, last = 1052, english = true },
LB4 = { id = 54, title = "Livets Bog 4", first = 1053, last = 1590, english = true },
LB5 = { id = 55, title = "Livets Bog 5", first = 1591, last = 1938, english = true },
LB6 = { id = 56, title = "Livets Bog 6", first = 1939, last = 2395, english = false },
LB7 = { id = 57, title = "Livets Bog 7", first = 2396, last = 2664, english = false },
}
local TEWP = {
[1] = { id = 61, title = "The Eternal World Picture 1", english = true },
[2] = { id = 62, title = "The Eternal World Picture 2", english = true },
[3] = { id = 63, title = "The Eternal World Picture 3", english = true },
[4] = { id = 64, title = "The Eternal World Picture 4", english = true },
[5] = { id = 65, title = "The Eternal World Picture 5", english = false },
[6] = { id = 66, title = "The Eternal World Picture 6", english = false },
}
local ALLOWED = {
["1"] = true, ["2"] = true, ["3"] = true, ["4"] = true,
["5"] = true, ["6"] = true, ["7"] = true, ["8"] = true,
["9"] = true, ["10"] = true,
work = true, source = true, section = true, subsection = true,
kind = true,
}
local function trim(value)
if value == nil then
return nil
end
value = tostring(value):gsub("^%s+", ""):gsub("%s+$", "")
if value == "" then
return nil
end
return value
end
local function text(value)
-- All values that reach this helper have already passed a strict grammar,
-- except Kosmos text. mw.text.encode prevents that free text becoming HTML.
return mw.text.encode(tostring(value))
end
local function error_message(code, detail)
local suffix = detail and (" (" .. text(detail) .. ")") or ""
return '<span class="error reference-error">Reference error: ' .. code .. suffix .. "</span>" .. ERROR_CATEGORY
end
local function link(url, label)
return "[" .. url .. " " .. text(label) .. "]"
end
local function is_integer(value)
return value ~= nil and value:match("^[1-9]%d*$") ~= nil
end
local function is_decimal_section(value)
return value ~= nil and value:match("^[1-9]%d*%.[1-9]%d*$") ~= nil
end
local function source_suffix(book)
if book.english then
return "English online text"
end
return "Danish original; no English translation confirmed"
end
local function book_url(book, section, subsection)
local base = book.english and EN_TTT or DA_TTT
local mode = section and (book.english and "opslag" or "forside") or "forside"
local url = base .. "?bog=" .. book.id .. "&mode=" .. mode
if section then
url = url .. "&stk=" .. section
end
if subsection then
url = url .. "&pkt=" .. subsection
end
return url
end
local function book_reference(book, section, subsection)
if not book then
return nil
end
if section and not is_integer(section) then
return nil, "INVALID_SECTION"
end
if subsection and not is_integer(subsection) then
return nil, "INVALID_SUBSECTION"
end
if subsection and not section then
return nil, "MISSING_SECTION"
end
if section then
local number = tonumber(section)
if number < book.first or number > book.last then
return nil, "SECTION_OUT_OF_RANGE"
end
end
local label = book.title
if section then
label = label .. ", section " .. section
if subsection then
label = label .. ", subsection " .. subsection
end
else
label = label .. " (book)"
end
local url = book_url(book, section, subsection)
return link(url, label .. " — " .. source_suffix(book))
end
local function find_book_for_section(section)
local number = tonumber(section)
for _, key in ipairs({ "LB1", "LB2", "LB3", "LB4", "LB5", "LB6", "LB7" }) do
local book = BOOKS[key]
if number >= book.first and number <= book.last then
return book
end
end
return nil
end
local function normalise_work(value)
local upper = value:upper():gsub("%s+", " ")
local lb = upper:match("^(LB[1-7])$")
if lb then
return "LB", tonumber(lb:sub(3))
end
local tewp = upper:match("^TEWP%s+([1-6])$")
or upper:match("^DEV%s+([1-6])$")
or upper:match("^THE ETERNAL WORLD PICTURE%s+([1-6])$")
if tewp then
return "TEWP", tonumber(tewp)
end
local short = upper:match("^SHORT BOOK%s+([1-9]%d*)$")
or upper:match("^SMÅBOG%s+([1-9]%d*)$")
or upper:match("^SMABOG%s+([1-9]%d*)$")
or upper:match("^PAMPHLET%s+([1-9]%d*)$")
if short then
return "SHORT", tonumber(short)
end
return upper
end
local function collect_args(argument_frame)
local values = {}
-- p.ref passes the Template:Ref frame explicitly. This is important:
-- putting #invoke inside literal <ref> markup creates another parser frame
-- and loses the template arguments. Reading this exact frame also keeps
-- every argument visible to validation, including unknown names.
if not argument_frame then
return nil, "MISSING_PRIMARY_ARGUMENT"
end
for key, raw in pairs(argument_frame.args or {}) do
local name = tostring(key)
local value = trim(raw)
if value ~= nil then
if not ALLOWED[name] then
return nil, "UNSUPPORTED_ARGUMENT", name
end
values[name] = value
end
end
return values
end
local function positional(values)
local result = {}
for index = 1, 10 do
if values[tostring(index)] ~= nil then
result[index] = values[tostring(index)]
end
end
for index = 2, 10 do
if result[index] ~= nil and result[index - 1] == nil then
return nil, "MISSING_PRIMARY_ARGUMENT"
end
end
for index = 4, 10 do
if result[index] ~= nil then
return nil, "TOO_MANY_POSITIONAL_ARGUMENTS"
end
end
return result
end
local function render_book(work, section, subsection)
local family, volume = normalise_work(work)
if family == "LB" then
local book = BOOKS["LB" .. volume]
if not book then
return nil, "UNKNOWN_VOLUME"
end
return book_reference(book, section, subsection)
end
if family == "TEWP" then
local book = TEWP[volume]
if not book then
return nil, "UNKNOWN_VOLUME"
end
if subsection then
return nil, "TOO_MANY_SOURCE_ARGUMENTS"
end
if not section or not is_decimal_section(section) then
return nil, "INVALID_SYMBOL_SECTION"
end
local base = book.english and EN_TTT or DA_TTT
local url = base .. "?bog=" .. book.id .. "&mode=opslag&pkt=" .. section:match("%d+$") .. "&stk=" .. section:match("^%d+")
return link(url, book.title .. ", section " .. section .. " — " .. source_suffix(book))
end
if family == "SHORT" then
if subsection then
return nil, "TOO_MANY_SOURCE_ARGUMENTS"
end
if not section or not is_decimal_section(section) then
return nil, "INVALID_SHORT_BOOK_SECTION"
end
local chapter, passage = section:match("^(%d+)%.(%d+)$")
local url = DA_TTT .. "?bog=" .. volume .. "&art=" .. chapter .. "&stk=" .. passage
return link(url, "Short book " .. volume .. ", section " .. section .. " — Danish original; English translation not confirmed")
end
return nil, "UNKNOWN_WORK"
end
local function render_special(kind, value)
local lowered = kind:lower()
if lowered == "symbol" or lowered == "symbols" then
if not is_integer(value) or tonumber(value) > 100 then
return nil, "INVALID_SYMBOL_ID"
end
return link(EN_SYMBOLS .. "?symbol=" .. value, "Symbol " .. value .. " — English symbol page")
end
if lowered == "article" or lowered == "articles" then
if not value or value:match("^%d%d%d%d$") == nil then
return nil, "INVALID_ARTICLE_ID"
end
return link(EN_TTT .. "?art=" .. value .. "&mode=artikelopslag", "Article " .. value .. " — English online text if available")
end
if lowered == "lecture" or lowered == "lectures" or lowered == "foredrag" then
if not is_integer(value) then
return nil, "INVALID_LECTURE_ID"
end
return link(DA_LECTURES .. "?nr=" .. value, "Lecture " .. value .. " — Danish original; English translation not confirmed")
end
if lowered == "kosmos" then
if not value then
return nil, "MISSING_KOSMOS_DESCRIPTION"
end
return text("Kosmos: " .. value .. " — Danish original; no direct online-library link confirmed")
end
return nil, "UNKNOWN_SOURCE_KIND"
end
local function render_reference(argument_frame)
local values, collect_error, collect_detail = collect_args(argument_frame)
if not values then
return error_message(collect_error, collect_detail)
end
local pos, positional_error = positional(values)
if not pos then
return error_message(positional_error)
end
local work = values.work or values.source
local section = values.section
local subsection = values.subsection
local kind = values.kind
if values.work and values.source then
return error_message("DUPLICATE_WORK_ARGUMENTS")
end
if values.section and pos[2] then
return error_message("DUPLICATE_SECTION_ARGUMENT")
end
if values.subsection and pos[3] then
return error_message("DUPLICATE_SUBSECTION_ARGUMENT")
end
if kind then
if pos[2] or pos[3] or work or section or subsection then
return error_message("MIXED_REFERENCE_FORMS")
end
local special_result, special_error = render_special(kind, pos[1] or values["1"])
if not special_result then
return error_message(special_error)
end
return special_result
end
if work and pos[1] then
return error_message("MIXED_REFERENCE_FORMS")
end
if work then
section = section or pos[2]
subsection = subsection or pos[3]
else
work = pos[1]
section = section or pos[2]
subsection = subsection or pos[3]
end
if not work then
return error_message("MISSING_PRIMARY_ARGUMENT")
end
if is_integer(work) then
if section and subsection then
return error_message("DUPLICATE_SUBSECTION_ARGUMENT")
end
if section and not is_integer(section) then
return error_message("INVALID_SUBSECTION")
end
local book = find_book_for_section(work)
if not book then
return error_message("SECTION_OUT_OF_RANGE")
end
-- For numeric shorthand the second value is the subsection. Accept
-- either positional 2 or the named subsection form, but never discard
-- a third positional value.
local numeric_subsection = subsection or section
local result, render_error = book_reference(book, work, numeric_subsection)
if not result then
return error_message(render_error)
end
return result
end
if work:lower() == "e" or work:lower() == "epilogue" then
if section or subsection then
return error_message("UNEXPECTED_EPILOGUE_ARGUMENT")
end
local book = BOOKS.LB7
return link(book_url(book, "2665"), "Livets Bog 7, epilogue (section 2665) — Danish original; no English translation confirmed")
end
local special_kind = work:lower()
if special_kind == "symbol" or special_kind == "symbols"
or special_kind == "article" or special_kind == "articles"
or special_kind == "lecture" or special_kind == "lectures"
or special_kind == "foredrag" or special_kind == "kosmos" then
if subsection then
return error_message("TOO_MANY_SOURCE_ARGUMENTS")
end
local special_result, special_error = render_special(work, section)
if not special_result then
return error_message(special_error)
end
return special_result
end
local result, render_error = render_book(work, section, subsection)
if not result then
return error_message(render_error)
end
return result
end
function p.render(frame)
return render_reference(frame)
end
function p.ref(frame)
local parent = frame and frame:getParent()
local content = render_reference(parent)
local has_error_category = content:sub(-#ERROR_CATEGORY) == ERROR_CATEGORY
if has_error_category then
-- Category links inside a Cite extension tag are not reliably attached
-- to the page. Keep the error text in the note, but emit the marker
-- outside the tag so the maintenance category is authoritative.
content = content:sub(1, -#ERROR_CATEGORY - 1)
end
local reference = frame:extensionTag("ref", content)
if has_error_category then
reference = reference .. ERROR_CATEGORY
end
return reference
end
return p