Module:SMWUtils: Difference between revisions

Rewrite to use frame:preprocess() instead of mw.smw.ask() (Semantic Scribunto not installed) (via update-page on MediaWiki MCP Server)
Fix deduplication: key on printout columns instead of subject (subobject subjects are unique per-input) (via update-page on MediaWiki MCP Server)
 
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:SMWUtils
-- Module:SMWUtils
-- Utility functions for Semantic MediaWiki queries
-- Utility functions for Semantic MediaWiki queries
-- Uses frame:preprocess() since Semantic Scribunto is not installed
-- Uses frame:preprocess() for #ask queries (supports property chains)


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


--- Find all results where a chained property equals its minimum value
-- Parse an HTML table from #ask broadtable format into rows of cells
-- Uses a two-pass approach: first find min, then query for all matching
-- Filters out the "further results" row
-- @param frame Frame object with args:
local function parseHtmlTable(html)
--   conditions: SMW query conditions (e.g., "[[Has video signal format::Component (YPbPr)]]")
    local rows = {}
--   minProperty: Property to find minimum of (e.g., "Has CRT model.Has screen size inches")
    if not html then return rows end
--   printouts: Comma-separated list of properties to display
   
--  format: Output format (table, ul, ol) - default "table"
    for rowContent in html:gmatch('<tr[^>]*>(.-)</tr>') do
--  headers: Comma-separated header names for table format
        -- Skip "further results" rows
--  intro: Optional intro text
        if not rowContent:match('smw%-broadtable%-furtherresults') then
-- @return Wikitext output
            local cells = {}
function p.findAllWithMin(frame)
            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 args = frame.args
     local conditions = trim(args.conditions) or ''
     local conditions = trim(args.conditions) or ''
     local minProperty = trim(args.minProperty)
     local minProperty = trim(args.minProperty) or ''
     local printouts = trim(args.printouts) or ''
     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
    -- Build printout list
         return '<span class="error">Error: minProperty is required</span>'
    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
     end
      
      
    -- Step 1: Find the minimum value using a sorted query with limit=1
     local queryWikitext = '{{#ask:' .. conditions .. '\n' ..
     local minQueryWikitext = '{{#ask:' .. conditions .. '\n' ..
         table.concat(printoutQuery, '\n') .. '\n' ..
         '|?' .. minProperty .. '\n' ..
         '|sort=' .. minProperty .. '\n' ..
         '|sort=' .. minProperty .. '\n' ..
         '|order=asc\n' ..
         '|order=asc\n' ..
         '|limit=1\n' ..
         '|limit=5\n' ..
         '|format=broadtable\n' ..
         '|format=broadtable\n' ..
         '|headers=hide\n' ..
         '|headers=hide\n' ..
         '|link=none\n' ..
         '|link=all\n' ..
         '}}'
         '}}'
      
      
     local minResult = frame:preprocess(minQueryWikitext)
     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
      
      
     -- Parse the minimum value from the table result
     -- Show what we'd calculate as min (corrected: +2 for subject column)
    local minValue = nil
     if #rows > 0 then
     if minResult then
         local minValueCol = #printoutProps + 2  -- +1 for subject column, +1 for minProperty column
         -- Extract from last <td> in the row (the property value)
        table.insert(output, "'''Printout count:''' " .. #printoutProps .. "\n")
         for cellContent in minResult:gmatch('<td[^>]*>(.-)</td>') do
         table.insert(output, "'''Min value column index:''' " .. minValueCol .. " (printouts + 2 for subject col)\n")
             local cleanContent = cellContent:gsub('<[^>]+>', '')
        if rows[1][minValueCol] then
             local numVal = tonumber(trim(cleanContent))
             local minText = stripHtml(rows[1][minValueCol])
            if numVal then
             local minNum = tonumber(minText)
                minValue = numVal
            table.insert(output, "'''First row min column value:''' " .. (minText or "nil") .. " → tonumber: " .. tostring(minNum) .. "\n")
             end
        else
             table.insert(output, "'''First row min column:''' (column doesn't exist)\n")
         end
         end
     end
     end
      
      
     if minValue == nil then
     return table.concat(output)
         return "No results found or could not determine minimum value."
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
     end
      
      
     -- Step 2: Query for all results matching this minimum value
     -- 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 propTrimmed = trim(prop)
        table.insert(printoutProps, propTrimmed)
         table.insert(printoutQuery, '|?' .. propTrimmed)
     end
     end
    -- Add minProperty to query
    table.insert(printoutQuery, '|?' .. minProperty .. '=#')
      
      
     local fullQueryWikitext = '{{#ask:' .. conditions .. '\n' ..
    -- Query all results sorted by minProperty ascending
        '[[' .. minProperty .. '::' .. minValue .. ']]\n' ..
     local queryWikitext = '{{#ask:' .. conditions .. '\n' ..
         table.concat(printoutList, '\n') .. '\n' ..
         table.concat(printoutQuery, '\n') .. '\n' ..
         '|sort=' .. minProperty .. '\n' ..
         '|sort=' .. minProperty .. '\n' ..
         '|limit=100\n' ..
        '|order=asc\n' ..
         '|limit=500\n' ..
         '|format=broadtable\n' ..
         '|format=broadtable\n' ..
         '|headers=hide\n' ..
         '|headers=hide\n' ..
Line 82: Line 172:
         '}}'
         '}}'
      
      
     local fullResult = frame:preprocess(fullQueryWikitext)
     local result = frame:preprocess(queryWikitext)
    local rows = parseHtmlTable(result)
      
      
     -- Parse results from HTML table
     if #rows == 0 then
     local results = p._parseTableResults(fullResult, printouts)
        return "No results found."
     end
      
      
     if #results == 0 then
     -- broadtable format: Column 1 = subject, Columns 2..n+1 = printouts, Column n+2 = minProperty
         return "No results found matching minimum value " .. minValue .. "."
    -- 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
     end
      
      
     -- Build output
    -- Deduplicate (multiple subobjects on same page produce duplicate rows)
    filtered = deduplicateRows(filtered, #printoutProps)
   
     -- 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._formatTableFromParsed(results, printoutProps, headerList, introText)
         return p._formatList(filtered, printoutProps, headerList, 'ul', introText, mainlabel)
     elseif format == 'ul' or format == 'ol' then
     elseif format == 'ol' then
         return p._formatListFromParsed(results, format, introText)
         return p._formatList(filtered, printoutProps, headerList, 'ol', introText, mainlabel)
     else
     else
         return p._formatTableFromParsed(results, printoutProps, headerList, introText)
         return p._formatTable(filtered, printoutProps, headerList, introText, mainlabel)
     end
     end
end
end
Line 122: Line 227:
     local headers = trim(args.headers) or ''
     local headers = trim(args.headers) or ''
     local intro = trim(args.intro)
     local intro = trim(args.intro)
    local mainlabel = trim(args.mainlabel)
      
      
     if not maxProperty then
     if not maxProperty then
Line 127: Line 233:
     end
     end
      
      
     -- Step 1: Find the maximum value using a sorted query with limit=1
     -- Build printout list
     local maxQueryWikitext = '{{#ask:' .. conditions .. '\n' ..
    local printoutProps = {}
         '|?' .. maxProperty .. '\n' ..
    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' ..
         '|sort=' .. maxProperty .. '\n' ..
         '|order=desc\n' ..
         '|order=desc\n' ..
         '|limit=1\n' ..
         '|limit=500\n' ..
         '|format=broadtable\n' ..
         '|format=broadtable\n' ..
         '|headers=hide\n' ..
         '|headers=hide\n' ..
         '|link=none\n' ..
         '|link=all\n' ..
         '}}'
         '}}'
      
      
     local maxResult = frame:preprocess(maxQueryWikitext)
     local result = frame:preprocess(queryWikitext)
    local rows = parseHtmlTable(result)
      
      
    -- Parse the maximum value from the table result
     if #rows == 0 then
    local maxValue = nil
         return "No results found."
     if maxResult then
        for cellContent in maxResult:gmatch('<td[^>]*>(.-)</td>') do
            local cleanContent = cellContent:gsub('<[^>]+>', '')
            local numVal = tonumber(trim(cleanContent))
            if numVal then
                maxValue = numVal
            end
         end
     end
     end
      
      
     if maxValue == nil then
    -- broadtable format: Column 1 = subject, Columns 2..n+1 = printouts, Column n+2 = maxProperty
         return "No results found or could not determine maximum value."
    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
     end
      
      
     -- Step 2: Query for all results matching this maximum value
     -- Filter to only rows matching the maximum
     local printoutList = {}
     local filtered = {}
     for prop in printouts:gmatch('[^,]+') do
     for _, row in ipairs(rows) do
         table.insert(printoutList, '|?' .. trim(prop))
        local val = tonumber(stripHtml(row[maxValueCol]))
         if val == maxValue then
            table.insert(filtered, row)
        end
     end
     end
      
      
     local fullQueryWikitext = '{{#ask:' .. conditions .. '\n' ..
     -- Deduplicate (multiple subobjects on same page produce duplicate rows)
        '[[' .. maxProperty .. '::' .. maxValue .. ']]\n' ..
     filtered = deduplicateRows(filtered, #printoutProps)
        table.concat(printoutList, '\n') .. '\n' ..
        '|sort=' .. maxProperty .. '\n' ..
        '|limit=100\n' ..
        '|format=broadtable\n' ..
        '|headers=hide\n' ..
        '|link=all\n' ..
        '}}'
   
    local fullResult = frame:preprocess(fullQueryWikitext)
      
    -- Parse results from HTML table
    local results = p._parseTableResults(fullResult, printouts)
   
    if #results == 0 then
        return "No results found matching maximum value " .. maxValue .. "."
    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._formatTableFromParsed(results, printoutProps, headerList, introText)
         return p._formatList(filtered, printoutProps, headerList, 'ul', introText, mainlabel)
     elseif format == 'ul' or format == 'ol' then
     elseif format == 'ol' then
         return p._formatListFromParsed(results, format, introText)
         return p._formatList(filtered, printoutProps, headerList, 'ol', introText, mainlabel)
     else
     else
         return p._formatTableFromParsed(results, printoutProps, headerList, introText)
         return p._formatTable(filtered, printoutProps, headerList, introText, mainlabel)
    end
end
 
-- Internal: Parse HTML table results into a Lua table
function p._parseTableResults(htmlResult, printouts)
    local results = {}
   
    if not htmlResult then
        return results
    end
   
    -- Count expected columns from printouts
    local numPrintouts = 0
    for _ in printouts:gmatch('[^,]+') do
        numPrintouts = numPrintouts + 1
    end
   
    -- Match each table row
    for rowContent in htmlResult:gmatch('<tr[^>]*>(.-)</tr>') do
        local cells = {}
        -- Extract content from each <td>, preserving HTML for links
        for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
            table.insert(cells, cellContent)
        end
       
        -- First cell is usually the page link, rest are printout values
        if #cells >= 1 then
            table.insert(results, cells)
        end
     end
     end
   
    return results
end
end


-- Internal: Format parsed results as a wikitable
-- Format results as a wikitable
function p._formatTableFromParsed(results, printouts, headers, introText)
-- 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 = {}
     local output = {}
      
      
Line 243: Line 309:
     -- Header row
     -- Header row
     table.insert(output, '|-\n! #')
     table.insert(output, '|-\n! #')
    if mainlabel then
        table.insert(output, ' !! ' .. mainlabel)
    end
     for i, prop in ipairs(printouts) do
     for i, prop in ipairs(printouts) do
         local headerText = headers[i] or prop:gsub('.*%.', ''):gsub('Has ', '')
         local headerText = headers[i] or prop:gsub('.*%.', ''):gsub('Has ', '')
Line 249: Line 318:
     table.insert(output, '\n')
     table.insert(output, '\n')
      
      
     -- Data rows
     -- Data rows: printout columns start at index 2 (after subject column)
     for i, row in ipairs(results) do
     for i, row in ipairs(rows) do
         table.insert(output, '|-\n| ' .. tostring(i))
         local tr = '|-\n| ' .. tostring(i)
        if mainlabel then
            tr = tr .. ' || ' .. (row[1] or '')
        end
         for j = 1, #printouts do
         for j = 1, #printouts do
             local cellVal = row[j] or ''
             -- Printout j is in column j+1 (offset by subject column)
             table.insert(output, ' || ' .. cellVal)
             tr = tr .. ' || ' .. (row[j + 1] or '')
         end
         end
         table.insert(output, '\n')
         table.insert(output, tr .. '\n')
     end
     end
      
      
Line 264: Line 336:
end
end


-- Internal: Format parsed results as a list
-- Format results as a list
function p._formatListFromParsed(results, listType, introText)
function p._formatList(rows, printouts, headers, listType, introText, mainlabel)
     local output = {}
     local output = {}
     local bullet = listType == 'ol' and '#' or '*'
     local bullet = listType == 'ol' and '#' or '*'
Line 271: Line 343:
     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 _, cell in ipairs(row) do
        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
             if cell and cell ~= '' then
                 table.insert(parts, cell)
                 table.insert(parts, cell)
MediaWiki Appliance - Powered by TurnKey Linux