Module:SMWUtils
Documentation for this module may be created at Module:SMWUtils/doc
-- Module:SMWUtils
-- Utility functions for Semantic MediaWiki queries
-- Uses frame:preprocess() for #ask queries (supports property chains)
local p = {}
local function trim(s)
if s == nil then return nil end
s = tostring(s)
s = s:gsub('^%s+', ''):gsub('%s+$', '')
if s == '' then return nil end
return s
end
-- Parse an HTML table from #ask broadtable format into rows of cells
-- Filters out the "further results" row
local function parseHtmlTable(html)
local rows = {}
if not html then return rows end
for rowContent in html:gmatch('<tr[^>]*>(.-)</tr>') do
-- Skip "further results" rows
if not rowContent:match('smw%-broadtable%-furtherresults') then
local cells = {}
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
-- Keep HTML intact for links
table.insert(cells, trim(cellContent) or '')
end
if #cells > 0 then
table.insert(rows, cells)
end
end
end
return rows
end
-- Extract plain text from HTML (for numeric comparison)
local function stripHtml(s)
if not s then return nil end
return trim(s:gsub('<[^>]+>', ''):gsub('&[^;]+;', ''))
end
-- Deduplicate rows by printout columns, keeping the first occurrence
-- Uses stripped text of printout columns (2..n+1) as the dedup key,
-- since subobject queries produce unique subjects (e.g. Page#sub1, Page#sub2)
-- but identical printout values for the same parent page
local function deduplicateRows(rows, numPrintouts)
local seen = {}
local deduped = {}
for _, row in ipairs(rows) do
local keyParts = {}
for j = 2, numPrintouts + 1 do
table.insert(keyParts, stripHtml(row[j]) or '')
end
local key = table.concat(keyParts, '|')
if not seen[key] then
seen[key] = true
table.insert(deduped, row)
end
end
return deduped
end
--- Debug function to see raw query output and parsing
function p.debug(frame)
local args = frame.args
local conditions = trim(args.conditions) or ''
local minProperty = trim(args.minProperty) or ''
local printouts = trim(args.printouts) or ''
-- Build printout list
local printoutProps = {}
local printoutQuery = {}
for prop in printouts:gmatch('[^,]+') do
local propTrimmed = trim(prop)
table.insert(printoutProps, propTrimmed)
table.insert(printoutQuery, '|?' .. propTrimmed)
end
-- Add minProperty to query
if minProperty ~= '' then
table.insert(printoutQuery, '|?' .. minProperty .. '=#')
end
local queryWikitext = '{{#ask:' .. conditions .. '\n' ..
table.concat(printoutQuery, '\n') .. '\n' ..
'|sort=' .. minProperty .. '\n' ..
'|order=asc\n' ..
'|limit=5\n' ..
'|format=broadtable\n' ..
'|headers=hide\n' ..
'|link=all\n' ..
'}}'
local result = frame:preprocess(queryWikitext)
local rows = parseHtmlTable(result)
local output = {}
table.insert(output, "'''Query wikitext:'''\n<pre>" .. mw.text.nowiki(queryWikitext) .. "</pre>\n\n")
table.insert(output, "'''Raw HTML result:'''\n<pre>" .. mw.text.nowiki(result):sub(1, 2000) .. "</pre>\n\n")
table.insert(output, "'''Parsed rows (" .. #rows .. "):'''\n\n")
for i, row in ipairs(rows) do
table.insert(output, "'''Row " .. i .. " (" .. #row .. " cells):'''\n")
for j, cell in ipairs(row) do
local stripped = stripHtml(cell) or "(nil)"
table.insert(output, "* Cell " .. j .. ": <code>" .. mw.text.nowiki(cell):sub(1, 100) .. "</code> → stripped: <code>" .. stripped .. "</code>\n")
end
table.insert(output, "\n")
end
-- Show what we'd calculate as min (corrected: +2 for subject column)
if #rows > 0 then
local minValueCol = #printoutProps + 2 -- +1 for subject column, +1 for minProperty column
table.insert(output, "'''Printout count:''' " .. #printoutProps .. "\n")
table.insert(output, "'''Min value column index:''' " .. minValueCol .. " (printouts + 2 for subject col)\n")
if rows[1][minValueCol] then
local minText = stripHtml(rows[1][minValueCol])
local minNum = tonumber(minText)
table.insert(output, "'''First row min column value:''' " .. (minText or "nil") .. " → tonumber: " .. tostring(minNum) .. "\n")
else
table.insert(output, "'''First row min column:''' (column doesn't exist)\n")
end
end
return table.concat(output)
end
--- Find all results where a property equals its minimum value
-- @param frame with args:
-- conditions: SMW conditions (e.g., "[[Has video signal format::Component (YPbPr)]]")
-- minProperty: Property to minimize (e.g., "Has CRT model.Has screen size inches")
-- printouts: Comma-separated properties to display
-- headers: Comma-separated header names
-- mainlabel: Optional header for the subject/page name column
-- format: table, ul, ol (default: table)
-- intro: Custom intro text
function p.findAllWithMin(frame)
local args = frame.args
local conditions = trim(args.conditions) or ''
local minProperty = trim(args.minProperty)
local printouts = trim(args.printouts) or ''
local format = trim(args.format) or 'table'
local headers = trim(args.headers) or ''
local intro = trim(args.intro)
local mainlabel = trim(args.mainlabel)
if not minProperty then
return '<span class="error">Error: minProperty is required</span>'
end
-- Build printout list
local printoutProps = {}
local printoutQuery = {}
for prop in printouts:gmatch('[^,]+') do
local propTrimmed = trim(prop)
table.insert(printoutProps, propTrimmed)
table.insert(printoutQuery, '|?' .. propTrimmed)
end
-- Add minProperty to query
table.insert(printoutQuery, '|?' .. minProperty .. '=#')
-- Query all results sorted by minProperty ascending
local queryWikitext = '{{#ask:' .. conditions .. '\n' ..
table.concat(printoutQuery, '\n') .. '\n' ..
'|sort=' .. minProperty .. '\n' ..
'|order=asc\n' ..
'|limit=500\n' ..
'|format=broadtable\n' ..
'|headers=hide\n' ..
'|link=all\n' ..
'}}'
local result = frame:preprocess(queryWikitext)
local rows = parseHtmlTable(result)
if #rows == 0 then
return "No results found."
end
-- broadtable format: Column 1 = subject, Columns 2..n+1 = printouts, Column n+2 = minProperty
-- So minValueCol = #printoutProps + 2
local minValueCol = #printoutProps + 2
local minValueText = stripHtml(rows[1][minValueCol])
local minValue = tonumber(minValueText)
if not minValue then
return "Could not determine minimum value. Column " .. minValueCol .. ", raw: " .. tostring(rows[1][minValueCol]) .. ", stripped: " .. tostring(minValueText)
end
-- Filter to only rows matching the minimum
local filtered = {}
for _, row in ipairs(rows) do
local val = tonumber(stripHtml(row[minValueCol]))
if val == minValue then
table.insert(filtered, row)
end
end
-- Deduplicate (multiple subobjects on same page produce duplicate rows)
filtered = deduplicateRows(filtered, #printoutProps)
-- Build headers list
local headerList = {}
for h in headers:gmatch('[^,]+') do
table.insert(headerList, trim(h))
end
local introText = intro or ("Smallest screen size with this input: '''" .. minValue .. "\"'''")
if format == 'ul' then
return p._formatList(filtered, printoutProps, headerList, 'ul', introText, mainlabel)
elseif format == 'ol' then
return p._formatList(filtered, printoutProps, headerList, 'ol', introText, mainlabel)
else
return p._formatTable(filtered, printoutProps, headerList, introText, mainlabel)
end
end
--- Find all results where a property equals its maximum value
function p.findAllWithMax(frame)
local args = frame.args
local conditions = trim(args.conditions) or ''
local maxProperty = trim(args.maxProperty)
local printouts = trim(args.printouts) or ''
local format = trim(args.format) or 'table'
local headers = trim(args.headers) or ''
local intro = trim(args.intro)
local mainlabel = trim(args.mainlabel)
if not maxProperty then
return '<span class="error">Error: maxProperty is required</span>'
end
-- Build printout list
local printoutProps = {}
local printoutQuery = {}
for prop in printouts:gmatch('[^,]+') do
local propTrimmed = trim(prop)
table.insert(printoutProps, propTrimmed)
table.insert(printoutQuery, '|?' .. propTrimmed)
end
table.insert(printoutQuery, '|?' .. maxProperty .. '=#')
-- Query all results sorted by maxProperty descending
local queryWikitext = '{{#ask:' .. conditions .. '\n' ..
table.concat(printoutQuery, '\n') .. '\n' ..
'|sort=' .. maxProperty .. '\n' ..
'|order=desc\n' ..
'|limit=500\n' ..
'|format=broadtable\n' ..
'|headers=hide\n' ..
'|link=all\n' ..
'}}'
local result = frame:preprocess(queryWikitext)
local rows = parseHtmlTable(result)
if #rows == 0 then
return "No results found."
end
-- broadtable format: Column 1 = subject, Columns 2..n+1 = printouts, Column n+2 = maxProperty
local maxValueCol = #printoutProps + 2
local maxValueText = stripHtml(rows[1][maxValueCol])
local maxValue = tonumber(maxValueText)
if not maxValue then
return "Could not determine maximum value. Column " .. maxValueCol .. ", raw: " .. tostring(rows[1][maxValueCol]) .. ", stripped: " .. tostring(maxValueText)
end
-- Filter to only rows matching the maximum
local filtered = {}
for _, row in ipairs(rows) do
local val = tonumber(stripHtml(row[maxValueCol]))
if val == maxValue then
table.insert(filtered, row)
end
end
-- Deduplicate (multiple subobjects on same page produce duplicate rows)
filtered = deduplicateRows(filtered, #printoutProps)
-- Build headers list
local headerList = {}
for h in headers:gmatch('[^,]+') do
table.insert(headerList, trim(h))
end
local introText = intro or ("Largest screen size with this input: '''" .. maxValue .. "\"'''")
if format == 'ul' then
return p._formatList(filtered, printoutProps, headerList, 'ul', introText, mainlabel)
elseif format == 'ol' then
return p._formatList(filtered, printoutProps, headerList, 'ol', introText, mainlabel)
else
return p._formatTable(filtered, printoutProps, headerList, introText, mainlabel)
end
end
-- Format results as a wikitable
-- Note: row[1] is subject, row[2..n+1] are printouts, row[n+2] is min/max value
function p._formatTable(rows, printouts, headers, introText, mainlabel)
local output = {}
table.insert(output, introText .. "\n\n")
table.insert(output, '{| class="wikitable sortable"\n')
-- Header row
table.insert(output, '|-\n! #')
if mainlabel then
table.insert(output, ' !! ' .. mainlabel)
end
for i, prop in ipairs(printouts) do
local headerText = headers[i] or prop:gsub('.*%.', ''):gsub('Has ', '')
table.insert(output, ' !! ' .. headerText)
end
table.insert(output, '\n')
-- Data rows: printout columns start at index 2 (after subject column)
for i, row in ipairs(rows) do
local tr = '|-\n| ' .. tostring(i)
if mainlabel then
tr = tr .. ' || ' .. (row[1] or '')
end
for j = 1, #printouts do
-- Printout j is in column j+1 (offset by subject column)
tr = tr .. ' || ' .. (row[j + 1] or '')
end
table.insert(output, tr .. '\n')
end
table.insert(output, '|}')
return table.concat(output)
end
-- Format results as a list
function p._formatList(rows, printouts, headers, listType, introText, mainlabel)
local output = {}
local bullet = listType == 'ol' and '#' or '*'
table.insert(output, introText .. "\n\n")
for _, row in ipairs(rows) do
local parts = {}
if mainlabel then
local subject = row[1]
if subject and subject ~= '' then
table.insert(parts, subject)
end
end
for j = 1, #printouts do
-- Printout j is in column j+1 (offset by subject column)
local cell = row[j + 1]
if cell and cell ~= '' then
table.insert(parts, cell)
end
end
table.insert(output, bullet .. ' ' .. table.concat(parts, ' – ') .. '\n')
end
return table.concat(output)
end
return p