Module:SMWUtils: Difference between revisions
Add debug function and improve property chain handling (via update-page on MediaWiki MCP Server) |
Rewrite to use frame:preprocess() with #ask queries since mw.smw.ask() doesn't support property chains (via update-page on MediaWiki MCP Server) |
||
| Line 1: | Line 1: | ||
-- Module:SMWUtils | -- Module:SMWUtils | ||
-- Utility functions for Semantic MediaWiki queries | -- Utility functions for Semantic MediaWiki queries | ||
-- | -- Uses frame:preprocess() for #ask queries (supports property chains) | ||
local p = {} | local p = {} | ||
| Line 13: | Line 13: | ||
end | end | ||
-- | -- Parse an HTML table from #ask broadtable format into rows of cells | ||
local function | local function parseHtmlTable(html) | ||
local rows = {} | |||
if | if not html then return rows end | ||
for rowContent in html:gmatch('<tr[^>]*>(.-)</tr>') do | |||
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 | end | ||
-- | -- Extract plain text from HTML (for numeric comparison) | ||
local function stripHtml(s) | |||
if not s then return nil end | |||
return s:gsub('<[^>]+>', ''):gsub('&[^;]+;', '') | |||
if not | |||
end | end | ||
--- Find all results where a property equals its minimum value | --- Find all results where a property equals its minimum value | ||
-- @param frame | -- @param frame with args: | ||
-- conditions: SMW | -- conditions: SMW conditions (e.g., "[[Has video signal format::Component (YPbPr)]]") | ||
-- minProperty: Property to | -- minProperty: Property to minimize (e.g., "Has CRT model.Has screen size inches") | ||
-- printouts: Comma-separated | -- printouts: Comma-separated properties to display | ||
-- format: | -- headers: Comma-separated header names | ||
-- format: table, ul, ol (default: table) | |||
-- intro: | -- intro: Custom intro text | ||
function p.findAllWithMin(frame) | function p.findAllWithMin(frame) | ||
local args = frame.args | local args = frame.args | ||
| Line 124: | Line 59: | ||
end | end | ||
-- Build | -- Build printout list | ||
local | local printoutProps = {} | ||
local printoutQuery = {} | |||
for prop in printouts:gmatch('[^,]+') do | for prop in printouts:gmatch('[^,]+') do | ||
table.insert( | local p = trim(prop) | ||
table.insert(printoutProps, p) | |||
table.insert(printoutQuery, '|?' .. p) | |||
end | end | ||
-- | -- Add minProperty to query | ||
table.insert( | 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 | local rows = parseHtmlTable(result) | ||
local | |||
if | if #rows == 0 then | ||
return "No results found." | return "No results found." | ||
end | end | ||
-- Find the minimum value | -- The minProperty value is in the last column (we added it with =#) | ||
local | -- Find the minimum value from the first row (already sorted asc) | ||
local minValueCol = #printoutProps + 1 | |||
local minValueText = stripHtml(rows[1][minValueCol]) | |||
local minValue = tonumber(minValueText) | |||
if minValue | if not minValue then | ||
return "Could not determine minimum value." | return "Could not determine minimum value." | ||
end | end | ||
-- Filter | -- Filter to only rows matching the minimum | ||
local filtered = {} | local filtered = {} | ||
for _, row in ipairs( | for _, row in ipairs(rows) do | ||
local val = | local val = tonumber(stripHtml(row[minValueCol])) | ||
if val | if val == minValue then | ||
table.insert(filtered, row) | table.insert(filtered, row) | ||
end | end | ||
end | end | ||
-- Build | -- Build headers list | ||
local headerList = {} | local headerList = {} | ||
for h in headers:gmatch('[^,]+') do | for h in headers:gmatch('[^,]+') do | ||
table.insert(headerList, trim(h)) | table.insert(headerList, trim(h)) | ||
end | end | ||
local introText = intro or ("Smallest screen size with this input: '''" .. minValue .. "\"'''") | local introText = intro or ("Smallest screen size with this input: '''" .. minValue .. "\"'''") | ||
if format == ' | if format == 'ul' then | ||
return p. | return p._formatList(filtered, printoutProps, headerList, 'ul', introText) | ||
elseif | elseif format == 'ol' then | ||
return p._formatList(filtered, printoutProps, | return p._formatList(filtered, printoutProps, headerList, 'ol', introText) | ||
else | else | ||
return p._formatTable(filtered, printoutProps, headerList, introText) | return p._formatTable(filtered, printoutProps, headerList, introText) | ||
| Line 203: | Line 138: | ||
end | end | ||
-- Build | -- Build printout list | ||
local | local printoutProps = {} | ||
local printoutQuery = {} | |||
for prop in printouts:gmatch('[^,]+') do | for prop in printouts:gmatch('[^,]+') do | ||
table.insert( | local p = trim(prop) | ||
table.insert(printoutProps, p) | |||
table.insert(printoutQuery, '|?' .. p) | |||
end | end | ||
table.insert( | 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 | local rows = parseHtmlTable(result) | ||
local | |||
if | if #rows == 0 then | ||
return "No results found." | return "No results found." | ||
end | end | ||
local maxValueCol = #printoutProps + 1 | |||
local | local maxValueText = stripHtml(rows[1][maxValueCol]) | ||
local maxValue = tonumber(maxValueText) | |||
if maxValue | if not maxValue then | ||
return "Could not determine maximum value." | return "Could not determine maximum value." | ||
end | end | ||
-- Filter | -- Filter to only rows matching the maximum | ||
local filtered = {} | local filtered = {} | ||
for _, row in ipairs( | for _, row in ipairs(rows) do | ||
local val = | local val = tonumber(stripHtml(row[maxValueCol])) | ||
if val | if val == maxValue then | ||
table.insert(filtered, row) | table.insert(filtered, row) | ||
end | end | ||
end | end | ||
-- Build | -- Build headers list | ||
local headerList = {} | local headerList = {} | ||
for h in headers:gmatch('[^,]+') do | for h in headers:gmatch('[^,]+') do | ||
table.insert(headerList, trim(h)) | table.insert(headerList, trim(h)) | ||
end | end | ||
local introText = intro or ("Largest screen size with this input: '''" .. maxValue .. "\"'''") | local introText = intro or ("Largest screen size with this input: '''" .. maxValue .. "\"'''") | ||
if format == ' | if format == 'ul' then | ||
return p. | return p._formatList(filtered, printoutProps, headerList, 'ul', introText) | ||
elseif | elseif format == 'ol' then | ||
return p._formatList(filtered, printoutProps, | return p._formatList(filtered, printoutProps, headerList, 'ol', introText) | ||
else | else | ||
return p._formatTable(filtered, printoutProps, headerList, introText) | return p._formatTable(filtered, printoutProps, headerList, introText) | ||
| Line 267: | Line 200: | ||
end | end | ||
-- | -- Format results as a wikitable | ||
function p._formatTable( | function p._formatTable(rows, printouts, headers, introText) | ||
local output = {} | local output = {} | ||
| Line 282: | Line 215: | ||
table.insert(output, '\n') | table.insert(output, '\n') | ||
-- Data rows | -- Data rows (only show printout columns, not the min/max value column) | ||
for i, row in ipairs( | for i, row in ipairs(rows) do | ||
local tr = '|-\n| ' .. tostring(i) | local tr = '|-\n| ' .. tostring(i) | ||
for | for j = 1, #printouts do | ||
tr = tr .. ' || ' .. (row[j] or '') | |||
tr = tr .. ' || ' .. | |||
end | end | ||
table.insert(output, tr .. '\n') | table.insert(output, tr .. '\n') | ||
| Line 297: | Line 229: | ||
end | end | ||
-- | -- Format results as a list | ||
function p._formatList( | function p._formatList(rows, printouts, headers, listType, introText) | ||
local output = {} | local output = {} | ||
local bullet = listType == 'ol' and '#' or '*' | local bullet = listType == 'ol' and '#' or '*' | ||
| Line 304: | Line 236: | ||
table.insert(output, introText .. "\n\n") | table.insert(output, introText .. "\n\n") | ||
for _, row in ipairs( | for _, row in ipairs(rows) do | ||
local parts = {} | local parts = {} | ||
for | for j = 1, #printouts do | ||
if row[j] and row[j] ~= '' then | |||
table.insert(parts, row[j]) | |||
table.insert(parts, | |||
end | end | ||
end | end | ||
Revision as of 07:07, 15 January 2026
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
local function parseHtmlTable(html)
local rows = {}
if not html then return rows end
for rowContent in html:gmatch('<tr[^>]*>(.-)</tr>') do
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
return rows
end
-- Extract plain text from HTML (for numeric comparison)
local function stripHtml(s)
if not s then return nil end
return s:gsub('<[^>]+>', ''):gsub('&[^;]+;', '')
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
-- 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)
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 p = trim(prop)
table.insert(printoutProps, p)
table.insert(printoutQuery, '|?' .. p)
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
-- The minProperty value is in the last column (we added it with =#)
-- Find the minimum value from the first row (already sorted asc)
local minValueCol = #printoutProps + 1
local minValueText = stripHtml(rows[1][minValueCol])
local minValue = tonumber(minValueText)
if not minValue then
return "Could not determine minimum value."
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
-- 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)
elseif format == 'ol' then
return p._formatList(filtered, printoutProps, headerList, 'ol', introText)
else
return p._formatTable(filtered, printoutProps, headerList, introText)
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)
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 p = trim(prop)
table.insert(printoutProps, p)
table.insert(printoutQuery, '|?' .. p)
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
local maxValueCol = #printoutProps + 1
local maxValueText = stripHtml(rows[1][maxValueCol])
local maxValue = tonumber(maxValueText)
if not maxValue then
return "Could not determine maximum value."
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
-- 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)
elseif format == 'ol' then
return p._formatList(filtered, printoutProps, headerList, 'ol', introText)
else
return p._formatTable(filtered, printoutProps, headerList, introText)
end
end
-- Format results as a wikitable
function p._formatTable(rows, printouts, headers, introText)
local output = {}
table.insert(output, introText .. "\n\n")
table.insert(output, '{| class="wikitable sortable"\n')
-- Header row
table.insert(output, '|-\n! #')
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 (only show printout columns, not the min/max value column)
for i, row in ipairs(rows) do
local tr = '|-\n| ' .. tostring(i)
for j = 1, #printouts do
tr = tr .. ' || ' .. (row[j] 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)
local output = {}
local bullet = listType == 'ol' and '#' or '*'
table.insert(output, introText .. "\n\n")
for _, row in ipairs(rows) do
local parts = {}
for j = 1, #printouts do
if row[j] and row[j] ~= '' then
table.insert(parts, row[j])
end
end
table.insert(output, bullet .. ' ' .. table.concat(parts, ' – ') .. '\n')
end
return table.concat(output)
end
return p