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
-- Requires Semantic Scribunto extension for mw.smw.ask()
-- Uses frame:preprocess() for #ask queries (supports property chains)


local p = {}
local p = {}
Line 13: Line 13:
end
end


-- Helper to get a value from a row, trying multiple key formats for property chains
-- Parse an HTML table from #ask broadtable format into rows of cells
local function getRowValue(row, prop)
local function parseHtmlTable(html)
     -- Try the exact property name first
     local rows = {}
     if row[prop] ~= nil then
     if not html then return rows end
        return row[prop]
    end
      
      
     -- For property chains like "Has CRT model.Has brand", also try just "Has brand"
     for rowContent in html:gmatch('<tr[^>]*>(.-)</tr>') do
    local lastPart = prop:match('%.([^%.]+)$')
        local cells = {}
    if lastPart and row[lastPart] ~= nil then
         for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
         return row[lastPart]
            -- Keep HTML intact for links
    end
            table.insert(cells, trim(cellContent) or '')
   
        end
    return nil
        if #cells > 0 then
end
            table.insert(rows, cells)
 
         end
-- Helper to format a value for display
local function formatValue(val)
    if val == nil then
         return ''
     end
     end
      
      
     if type(val) == 'table' then
     return rows
        -- Handle linked values (pages)
        if val.fulltext then
            return '[[' .. val.fulltext .. ']]'
        elseif #val > 0 then
            -- Array of values
            local parts = {}
            for _, v in ipairs(val) do
                if type(v) == 'table' and v.fulltext then
                    table.insert(parts, '[[' .. v.fulltext .. ']]')
                else
                    table.insert(parts, tostring(v))
                end
            end
            return table.concat(parts, ', ')
        end
        return ''
    else
        return tostring(val)
    end
end
end


--- Debug function to show what keys mw.smw.ask() returns
-- Extract plain text from HTML (for numeric comparison)
-- Usage: {{#invoke:SMWUtils|debug|query=[[Has video signal format::Component (YPbPr)]]|printouts=Has CRT model, Has CRT model.Has brand}}
local function stripHtml(s)
function p.debug(frame)
     if not s then return nil end
    local args = frame.args
     return s:gsub('<[^>]+>', ''):gsub('&[^;]+;', '')
    local query = trim(args.query) or ''
    local printouts = trim(args.printouts) or ''
   
    -- Build printout list
    local printoutList = {}
    for prop in printouts:gmatch('[^,]+') do
        table.insert(printoutList, '?' .. trim(prop))
    end
   
    local queryString = query .. '\n' .. table.concat(printoutList, '\n') .. '\n|limit=3'
    local results = mw.smw.ask(queryString)
   
     if not results or #results == 0 then
        return "No results found for query: <code>" .. mw.text.nowiki(queryString) .. "</code>"
    end
      
    local output = {}
    table.insert(output, "'''Query:''' <code>" .. mw.text.nowiki(queryString) .. "</code>\n\n")
    table.insert(output, "'''Results (" .. #results .. " shown):'''\n\n")
   
    for i, row in ipairs(results) do
        table.insert(output, "'''Row " .. i .. " keys:'''\n")
        for k, v in pairs(row) do
            local valStr
            if type(v) == 'table' then
                if v.fulltext then
                    valStr = "table{fulltext=\"" .. v.fulltext .. "\"}"
                else
                    valStr = "table[" .. #v .. "]"
                end
            else
                valStr = tostring(v)
            end
            table.insert(output, "* <code>" .. tostring(k) .. "</code> = " .. valStr .. "\n")
        end
        table.insert(output, "\n")
    end
   
    return table.concat(output)
end
end


--- Find all results where a property equals its minimum value across the result set
--- Find all results where a property equals its minimum value
-- @param frame Frame object with args:
-- @param frame with args:
--  conditions: SMW query conditions (e.g., "[[Has video signal format::Component (YPbPr)]]")
--  conditions: SMW conditions (e.g., "[[Has video signal format::Component (YPbPr)]]")
--  minProperty: Property to find minimum of (e.g., "Has CRT model.Has screen size inches")
--  minProperty: Property to minimize (e.g., "Has CRT model.Has screen size inches")
--  printouts: Comma-separated list of properties to display
--  printouts: Comma-separated properties to display
--  format: Output format (table, ul, ol) - default "table"
--  headers: Comma-separated header names
--  headers: Comma-separated header names for table format
--  format: table, ul, ol (default: table)
--  intro: Optional intro text (supports wikitext)
--  intro: Custom intro text
-- @return Wikitext output
function p.findAllWithMin(frame)
function p.findAllWithMin(frame)
     local args = frame.args
     local args = frame.args
Line 124: Line 59:
     end
     end
      
      
     -- Build the printout list for the query
     -- Build printout list
     local printoutList = {}
     local printoutProps = {}
    local printoutQuery = {}
     for prop in printouts:gmatch('[^,]+') do
     for prop in printouts:gmatch('[^,]+') do
         table.insert(printoutList, '?' .. trim(prop))
        local p = trim(prop)
        table.insert(printoutProps, p)
         table.insert(printoutQuery, '|?' .. p)
     end
     end
     -- Always include the minProperty for comparison
     -- Add minProperty to query
     table.insert(printoutList, '?' .. minProperty)
     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' ..
        '}}'
      
      
    -- Build and execute the query
     local result = frame:preprocess(queryWikitext)
     local queryString = conditions .. '\n' .. table.concat(printoutList, '\n') .. '\n|limit=500'
     local rows = parseHtmlTable(result)
     local results = mw.smw.ask(queryString)
      
      
     if not results or #results == 0 then
     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 minValue = nil
     -- Find the minimum value from the first row (already sorted asc)
     for _, row in ipairs(results) do
     local minValueCol = #printoutProps + 1
        local val = getRowValue(row, minProperty)
     local minValueText = stripHtml(rows[1][minValueCol])
        if val then
    local minValue = tonumber(minValueText)
            local numVal = tonumber(val)
            if numVal then
                if minValue == nil or numVal < minValue then
                    minValue = numVal
                end
            end
        end
    end
      
      
     if minValue == nil then
     if not minValue then
         return "Could not determine minimum value."
         return "Could not determine minimum value."
     end
     end
      
      
     -- Filter results to only those matching the minimum
     -- Filter to only rows matching the minimum
     local filtered = {}
     local filtered = {}
     for _, row in ipairs(results) do
     for _, row in ipairs(rows) do
         local val = getRowValue(row, minProperty)
         local val = tonumber(stripHtml(row[minValueCol]))
         if val and tonumber(val) == minValue then
         if val == minValue then
             table.insert(filtered, row)
             table.insert(filtered, row)
         end
         end
     end
     end
      
      
     -- Build output based on format
     -- 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
   
    local printoutProps = {}
    for prop in printouts:gmatch('[^,]+') do
        table.insert(printoutProps, trim(prop))
     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 == 'table' then
     if format == 'ul' then
         return p._formatTable(filtered, printoutProps, headerList, introText)
         return p._formatList(filtered, printoutProps, headerList, 'ul', introText)
     elseif format == 'ul' or format == 'ol' then
     elseif format == 'ol' then
         return p._formatList(filtered, printoutProps, format, introText)
         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 the printout list
     -- Build printout list
     local printoutList = {}
     local printoutProps = {}
    local printoutQuery = {}
     for prop in printouts:gmatch('[^,]+') do
     for prop in printouts:gmatch('[^,]+') do
         table.insert(printoutList, '?' .. trim(prop))
        local p = trim(prop)
        table.insert(printoutProps, p)
         table.insert(printoutQuery, '|?' .. p)
     end
     end
     table.insert(printoutList, '?' .. maxProperty)
     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' ..
        '}}'
      
      
    -- Build and execute the query
     local result = frame:preprocess(queryWikitext)
     local queryString = conditions .. '\n' .. table.concat(printoutList, '\n') .. '\n|limit=500'
     local rows = parseHtmlTable(result)
     local results = mw.smw.ask(queryString)
      
      
     if not results or #results == 0 then
     if #rows == 0 then
         return "No results found."
         return "No results found."
     end
     end
      
      
    -- Find the maximum value
     local maxValueCol = #printoutProps + 1
     local maxValue = nil
     local maxValueText = stripHtml(rows[1][maxValueCol])
     for _, row in ipairs(results) do
    local maxValue = tonumber(maxValueText)
        local val = getRowValue(row, maxProperty)
        if val then
            local numVal = tonumber(val)
            if numVal then
                if maxValue == nil or numVal > maxValue then
                    maxValue = numVal
                end
            end
        end
    end
      
      
     if maxValue == nil then
     if not maxValue then
         return "Could not determine maximum value."
         return "Could not determine maximum value."
     end
     end
      
      
     -- Filter results to only those matching the maximum
     -- Filter to only rows matching the maximum
     local filtered = {}
     local filtered = {}
     for _, row in ipairs(results) do
     for _, row in ipairs(rows) do
         local val = getRowValue(row, maxProperty)
         local val = tonumber(stripHtml(row[maxValueCol]))
         if val and tonumber(val) == maxValue then
         if val == maxValue then
             table.insert(filtered, row)
             table.insert(filtered, row)
         end
         end
     end
     end
      
      
     -- Build output
     -- 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
   
    local printoutProps = {}
    for prop in printouts:gmatch('[^,]+') do
        table.insert(printoutProps, trim(prop))
     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 == 'table' then
     if format == 'ul' then
         return p._formatTable(filtered, printoutProps, headerList, introText)
         return p._formatList(filtered, printoutProps, headerList, 'ul', introText)
     elseif format == 'ul' or format == 'ol' then
     elseif format == 'ol' then
         return p._formatList(filtered, printoutProps, format, introText)
         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


-- Internal: Format results as a wikitable
-- Format results as a wikitable
function p._formatTable(results, printouts, headers, introText)
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(results) do
     for i, row in ipairs(rows) do
         local tr = '|-\n| ' .. tostring(i)
         local tr = '|-\n| ' .. tostring(i)
         for _, prop in ipairs(printouts) do
         for j = 1, #printouts do
            local val = getRowValue(row, prop)
             tr = tr .. ' || ' .. (row[j] or '')
             tr = tr .. ' || ' .. formatValue(val)
         end
         end
         table.insert(output, tr .. '\n')
         table.insert(output, tr .. '\n')
Line 297: Line 229:
end
end


-- Internal: Format results as a list
-- Format results as a list
function p._formatList(results, printouts, listType, introText)
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(results) do
     for _, row in ipairs(rows) do
         local parts = {}
         local parts = {}
         for _, prop in ipairs(printouts) do
         for j = 1, #printouts do
             local val = getRowValue(row, prop)
             if row[j] and row[j] ~= '' then
            local formatted = formatValue(val)
                 table.insert(parts, row[j])
            if formatted ~= '' then
                 table.insert(parts, formatted)
             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
MediaWiki Appliance - Powered by TurnKey Linux