Module:AVDevice: Difference between revisions
Create AVDevice Lua module for infobox rendering (via create-page on MediaWiki MCP Server) |
Restore pre-compression source from rev 8199 (with comments, formatting, safe gsub patterns). Only change: args.oem_manufacturer → args.oem (the intended rename from rev 8313, without the destructive compression) (via update-page on MediaWiki MCP Server) |
||
| (40 intermediate revisions by the same user not shown) | |||
| Line 56: | Line 56: | ||
fullDiv:wikitext(valueHtmlOrWikitext) | fullDiv:wikitext(valueHtmlOrWikitext) | ||
end | end | ||
end | |||
-- Helper function to strip brand prefix from item name if it matches device brand | |||
local function getDisplayTextWithoutBrand(itemName, deviceBrand) | |||
if not itemName then return nil end | |||
if not deviceBrand then return itemName end | |||
local brandPrefix = deviceBrand .. ' ' | |||
if itemName:sub(1, #brandPrefix) == brandPrefix then | |||
return itemName:sub(#brandPrefix + 1) | |||
end | |||
return itemName | |||
end | |||
-- Helper function to format comma-separated values as a bulleted list or inline | |||
local function formatList(value, inline) | |||
if not value then return nil end | |||
local items = {} | |||
for item in value:gmatch('[^,]+') do | |||
local trimmed = trim(item) | |||
if trimmed then | |||
table.insert(items, trimmed) | |||
end | |||
end | |||
if #items == 0 then return nil end | |||
if inline or #items <= 3 then | |||
return table.concat(items, ', ') | |||
else | |||
local ul = mw.html.create('ul') | |||
ul:css('margin', '0'):css('padding-left', '1.2em') | |||
for _, item in ipairs(items) do | |||
ul:tag('li'):wikitext(item) | |||
end | |||
return tostring(ul) | |||
end | |||
end | |||
-- Helper to format MSRP with dollar sign and commas | |||
local function formatMSRP(value) | |||
if not value then return nil end | |||
local num = tonumber(value) | |||
if not num then return '$' .. value end | |||
-- Format with commas | |||
local formatted = string.format("%.2f", num) | |||
local intPart, decPart = formatted:match("^(%d+)%.(%d+)$") | |||
if intPart then | |||
local reversed = intPart:reverse() | |||
local withCommas = reversed:gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "") | |||
return '$' .. withCommas .. '.' .. decPart | |||
end | |||
return '$' .. formatted | |||
end | |||
-- Helper to format related models as wiki links, stripping brand prefix | |||
local function formatRelatedModels(value, deviceBrand) | |||
if not value then return nil end | |||
local items = {} | |||
for item in value:gmatch('[^,]+') do | |||
local trimmed = trim(item) | |||
if trimmed then | |||
local displayText = getDisplayTextWithoutBrand(trimmed, deviceBrand) | |||
table.insert(items, '[[' .. trimmed .. '|' .. displayText .. ']]') | |||
end | |||
end | |||
if #items == 0 then return nil end | |||
return table.concat(items, '<br>') | |||
end | |||
-- Helper to query inputs/outputs for a specific location | |||
local function queryIOByLocation(frame, fullPageName, ioType, location) | |||
local locationFilter = '' | |||
if location then | |||
locationFilter = ' [[Has ' .. (ioType == 'input' and 'input' or 'output') .. ' location::' .. location .. ']]\n' | |||
end | |||
local signalProp = ioType == 'input' and 'Has video signal format' or 'Has output signal format' | |||
local countProp = ioType == 'input' and 'Has input count' or 'Has output count' | |||
local connectorProp = ioType == 'input' and 'Has connector type' or 'Has output connector' | |||
local notesProp = ioType == 'input' and 'Has input notes' or 'Has output notes' | |||
local labelProp = ioType == 'input' and 'Has input label' or 'Has output label' | |||
local templateName = ioType == 'input' and 'AV device input row' or 'AV device output row' | |||
-- Build audio property queries (only for inputs) | |||
local audioProps = '' | |||
if ioType == 'input' then | |||
audioProps = ' |?Has audio type=audio\n' .. | |||
' |?Has audio connector=audio_connector\n' .. | |||
' |?Has audio shared with=audio_shared\n' | |||
end | |||
local queryWikitext = | |||
'{{#ask:\n' .. | |||
' [[Has AV device::' .. fullPageName .. ']]\n' .. | |||
' [[' .. signalProp .. '::+]]\n' .. | |||
locationFilter .. | |||
' |?' .. countProp .. '=count\n' .. | |||
' |?' .. signalProp .. '=signal\n' .. | |||
' |?' .. connectorProp .. '=connector\n' .. | |||
' |?' .. labelProp .. '=label\n' .. | |||
' |?' .. notesProp .. '=notes\n' .. | |||
audioProps .. | |||
' |format=template\n' .. | |||
' |template=' .. templateName .. '\n' .. | |||
' |named args=yes\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local result = frame:preprocess(queryWikitext) | |||
return trim(result) | |||
end | |||
-- Helper to discover all distinct location values for a given IO type | |||
-- Returns an ordered list of location strings, sorted with common locations first | |||
local function discoverLocations(frame, fullPageName, ioType) | |||
local locationProp, signalProp | |||
if ioType == 'input' then | |||
locationProp = 'Has input location' | |||
signalProp = 'Has video signal format' | |||
else -- output | |||
locationProp = 'Has output location' | |||
signalProp = 'Has output signal format' | |||
end | |||
local queryWikitext = | |||
'{{#ask:\n' .. | |||
' [[Has AV device::' .. fullPageName .. ']]\n' .. | |||
' [[' .. signalProp .. '::+]]\n' .. | |||
' [[' .. locationProp .. '::+]]\n' .. | |||
' |?' .. locationProp .. '=location\n' .. | |||
' |format=broadtable\n' .. | |||
' |headers=hide\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local result = frame:preprocess(queryWikitext) | |||
-- Parse distinct locations from broadtable rows | |||
local seen = {} | |||
local locations = {} | |||
-- Preferred display order for common locations | |||
local preferredOrder = {rear = 1, front = 2, side = 3, top = 4, bottom = 5, internal = 6} | |||
if result then | |||
for rowContent in result:gmatch('<tr[^>]*>(.-)</tr>') do | |||
local cells = {} | |||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | |||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | |||
-- cells[1] = subobject name, cells[2] = location value | |||
if #cells >= 2 and cells[2] then | |||
local loc = mw.text.trim(cells[2]) | |||
-- Normalize to lowercase for deduplication | |||
local locLower = loc:lower() | |||
if locLower ~= '' and not seen[locLower] then | |||
seen[locLower] = true | |||
table.insert(locations, locLower) | |||
end | |||
end | |||
end | |||
end | |||
-- Sort: preferred locations first (in defined order), then remaining alphabetically | |||
table.sort(locations, function(a, b) | |||
local orderA = preferredOrder[a] or 1000 | |||
local orderB = preferredOrder[b] or 1000 | |||
if orderA ~= orderB then | |||
return orderA < orderB | |||
end | |||
return a < b | |||
end) | |||
return locations | |||
end | |||
-- Capitalize first letter of a location string for display | |||
local function locationLabel(loc) | |||
if not loc or loc == '' then return '' end | |||
return loc:sub(1, 1):upper() .. loc:sub(2) | |||
end | end | ||
| Line 70: | Line 251: | ||
local brand = trim(args.brand) | local brand = trim(args.brand) | ||
local model_code = trim(args.model_code) | local model_code = trim(args.model_code) | ||
local oem_manufacturer = trim(args.oem) | |||
local device_type = trim(args.device_type) | local device_type = trim(args.device_type) | ||
local format_family = trim(args.format_family) | |||
local market = trim(args.market) | |||
local supported_formats = trim(args.supported_formats) | |||
local recording_formats = trim(args.recording_formats) | |||
local image_main = trim(args.image_main) | local image_main = trim(args.image_main) | ||
local year_released = trim(args.year_released) | local year_released = trim(args.year_released) | ||
local year_discontinued = trim(args.year_discontinued) | local year_discontinued = trim(args.year_discontinued) | ||
local | local msrp = trim(args.msrp) | ||
local | local av_system = trim(args.av_system) | ||
local | local tuner_system = trim(args.tuner_system) | ||
local | local frequency = trim(args.frequency) | ||
local | local power_avg = trim(args.power_avg) | ||
local power_max = trim(args.power_max) | |||
local features = trim(args.features) | local features = trim(args.features) | ||
local height = trim(args.height) | local height = trim(args.height) | ||
| Line 84: | Line 271: | ||
local depth = trim(args.depth) | local depth = trim(args.depth) | ||
local weight = trim(args.weight) | local weight = trim(args.weight) | ||
local | local related_models = trim(args.related_models) | ||
-- Use frame:extensionTag to create a proper <style> tag | -- Use frame:extensionTag to create a proper <style> tag | ||
local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template: | local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template:Infobox styles.css'}) | ||
local container = mw.html.create('div') | local container = mw.html.create('div') | ||
| Line 103: | Line 289: | ||
'infobox-title' | 'infobox-title' | ||
) | ) | ||
-- Type badge (breadcrumb) — plainlinks hides external icon, $wgNoFollowDomainExceptions handles nofollow | |||
local badgeWikitext = '[[AV Device Search|AV device]]' | |||
if device_type then | |||
local typeUrl = tostring(mw.uri.fullUrl('AV Device Search', {device_type = device_type})) | |||
badgeWikitext = badgeWikitext .. ' › <span class="plainlinks">[' .. typeUrl .. ' ' .. device_type .. ']</span>' | |||
end | |||
addFullRow(container, badgeWikitext, '', false, 'infobox-type-badge') | |||
if image_main then | if image_main then | ||
| Line 125: | Line 319: | ||
local brandHtml = frame:preprocess(brandWikitext) | local brandHtml = frame:preprocess(brandWikitext) | ||
addRow(container, 'Brand', brandHtml, true) | addRow(container, 'Brand', brandHtml, true) | ||
end | |||
-- OEM row (shown only when set) | |||
if oem_manufacturer then | |||
local oemWikitext = string.format( | |||
'{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', | |||
oem_manufacturer, oem_manufacturer | |||
) | |||
local oemHtml = frame:preprocess(oemWikitext) | |||
addRow(container, 'OEM', oemHtml, true) | |||
end | end | ||
| Line 131: | Line 335: | ||
end | end | ||
-- Market row | |||
if market then | |||
addRow(container, 'Market', market, false) | |||
end | |||
-- Type row: link to AV Device Search with filter, not to a (possibly non-existent) article | |||
if device_type then | if device_type then | ||
addRow(container, 'Type', '[[' .. | local typeSearchUrl = tostring(mw.uri.fullUrl('AV Device Search', {device_type = device_type})) | ||
local typeWikitext = '<span class="plainlinks">[' .. typeSearchUrl .. ' ' .. device_type .. ']</span>' | |||
addRow(container, 'Type', typeWikitext, true) | |||
end | |||
-- Format family with link to category | |||
if format_family then | |||
addRow(container, 'Format family', '[[:Category:' .. format_family .. ' devices|' .. format_family .. ']]', true) | |||
end | end | ||
| Line 148: | Line 365: | ||
end | end | ||
if | -- MSRP | ||
addRow(container, ' | if msrp then | ||
addRow(container, 'MSRP', formatMSRP(msrp), false) | |||
end | |||
-- AV System support | |||
if av_system then | |||
local formattedAV = formatList(av_system, true) | |||
if formattedAV then | |||
addRow(container, 'AV system', formattedAV, false) | |||
end | |||
end | |||
-- Related models | |||
if related_models then | |||
local formattedRelated = formatRelatedModels(related_models, brand) | |||
if formattedRelated then | |||
addRow(container, 'Related', formattedRelated, true) | |||
end | |||
end | end | ||
| Line 156: | Line 390: | ||
addRow(container, 'ECID', frame:preprocess(ecidWikitext), true) | addRow(container, 'ECID', frame:preprocess(ecidWikitext), true) | ||
-- | -- Format compatibility section (only show if tape-based device) | ||
if | if supported_formats or recording_formats then | ||
addSectionRow(container, ' | addSectionRow(container, 'Format Compatibility') | ||
if | if supported_formats then | ||
addRow(container, ' | local formattedSupported = formatList(supported_formats, true) | ||
if formattedSupported then | |||
addRow(container, 'Plays', formattedSupported, false) | |||
end | |||
end | end | ||
if | |||
addRow(container, ' | if recording_formats then | ||
local formattedRecording = formatList(recording_formats, true) | |||
if formattedRecording then | |||
addRow(container, 'Records', formattedRecording, false) | |||
end | |||
end | end | ||
end | |||
-- Inputs and Outputs sections (matching CRT model style) | |||
local function renderIOSection(sectionTitle, ioType) | |||
local locations = discoverLocations(frame, fullPageName, ioType) | |||
local locationData = {} | |||
for _, loc in ipairs(locations) do | |||
local content = queryIOByLocation(frame, fullPageName, ioType, loc) | |||
if content and content ~= '' then | |||
table.insert(locationData, {location = loc, content = content}) | |||
end | |||
end | end | ||
if | |||
if #locationData > 0 then | |||
addSectionRow(container, sectionTitle) | |||
for _, data in ipairs(locationData) do | |||
local labelDiv = container:tag('div') | |||
labelDiv:addClass('infobox-label') | |||
css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
labelDiv:wikitext(locationLabel(data.location)) | |||
local valueDiv = container:tag('div') | |||
valueDiv:addClass('infobox-value') | |||
css(valueDiv, 'padding:0.2em 0.4em;') | |||
valueDiv:node(mw.html.create('span'):wikitext(data.content)) | |||
end | |||
end | end | ||
end | end | ||
renderIOSection('Inputs', 'input') | |||
renderIOSection('Outputs', 'output') | |||
-- Features section | -- Features section | ||
| Line 197: | Line 463: | ||
end | end | ||
-- Power section | -- Power section - query voltage ranges and display power specs | ||
if | local voltageQueryWikitext = | ||
'{{#ask:\n' .. | |||
' [[Has AV device::' .. pageName .. ']]\n' .. | |||
' [[Voltage min::+]]\n' .. | |||
' |?Voltage min=vmin\n' .. | |||
' |?Voltage max=vmax\n' .. | |||
' |format=template\n' .. | |||
' |template=AV device voltage row\n' .. | |||
' |named args=yes\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local voltageResult = frame:preprocess(voltageQueryWikitext) | |||
local hasVoltage = voltageResult and trim(voltageResult) and trim(voltageResult) ~= '' | |||
if hasVoltage or frequency or power_avg or power_max then | |||
addSectionRow(container, 'Power') | addSectionRow(container, 'Power') | ||
if | |||
addRow(container, 'Voltage', | if hasVoltage then | ||
addRow(container, 'Voltage', voltageResult, true) | |||
end | |||
if frequency then | |||
local formattedFreq = formatList(frequency, true) | |||
if formattedFreq then | |||
addRow(container, 'AC frequency', formattedFreq, false) | |||
end | |||
end | |||
if power_avg or power_max then | |||
local powerStr = '' | |||
if power_avg and power_max then | |||
powerStr = power_avg .. 'W (avg) / ' .. power_max .. 'W (max)' | |||
elseif power_avg then | |||
powerStr = power_avg .. 'W' | |||
elseif power_max then | |||
powerStr = power_max .. 'W (max)' | |||
end | |||
addRow(container, 'Power', powerStr, false) | |||
end | |||
end | |||
-- Time Base Correction section - query AV TBC subobjects | |||
local tbcCountWikitext = | |||
'{{#ask:\n' .. | |||
' [[TBC type::+]]\n' .. | |||
' [[-Has subobject::' .. fullPageName .. ']]\n' .. | |||
' |format=count\n' .. | |||
'}}' | |||
local tbcCountResult = frame:preprocess(tbcCountWikitext) | |||
local hasTBC = tbcCountResult and tonumber(trim(tbcCountResult)) and tonumber(trim(tbcCountResult)) > 0 | |||
if hasTBC then | |||
addSectionRow(container, 'Time Base Correction') | |||
local tbcQueryWikitext = | |||
'{{#ask:\n' .. | |||
' [[TBC type::+]]\n' .. | |||
' [[-Has subobject::' .. fullPageName .. ']]\n' .. | |||
' |?TBC type\n' .. | |||
' |?TBC buffer size\n' .. | |||
' |?TBC notes\n' .. | |||
' |format=broadtable\n' .. | |||
' |headers=hide\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local tbcResult = frame:preprocess(tbcQueryWikitext) | |||
if tbcResult then | |||
local entryIndex = 0 | |||
for rowContent in tbcResult:gmatch('<tr[^>]*>(.-)</tr>') do | |||
local cells = {} | |||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | |||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, cleanContent) | |||
end | |||
if #cells >= 2 then | |||
local tbcType = trim(cells[2]) | |||
local tbcBuffer = trim(cells[3]) | |||
local tbcNotes = trim(cells[4]) | |||
if tbcType then | |||
entryIndex = entryIndex + 1 | |||
-- Create a full-width grouped container with nested grid | |||
local group = container:tag('div') | |||
group:addClass('infobox-tbc-group') | |||
css(group, 'grid-column: 1 / -1; display:grid; grid-template-columns:auto 1fr; gap:3px;') | |||
-- Type row | |||
local typeLabelDiv = group:tag('div') | |||
typeLabelDiv:addClass('infobox-label') | |||
css(typeLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
typeLabelDiv:wikitext('Type') | |||
local typeValueDiv = group:tag('div') | |||
typeValueDiv:addClass('infobox-value') | |||
css(typeValueDiv, 'padding:0.2em 0.4em;') | |||
typeValueDiv:wikitext(tbcType) | |||
-- Buffer row (conditional) | |||
if tbcBuffer then | |||
local bufLabelDiv = group:tag('div') | |||
bufLabelDiv:addClass('infobox-label') | |||
css(bufLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
bufLabelDiv:wikitext('Buffer') | |||
local bufValueDiv = group:tag('div') | |||
bufValueDiv:addClass('infobox-value') | |||
css(bufValueDiv, 'padding:0.2em 0.4em;') | |||
bufValueDiv:wikitext(tbcBuffer) | |||
end | |||
-- Notes row (conditional) | |||
if tbcNotes then | |||
local notesLabelDiv = group:tag('div') | |||
notesLabelDiv:addClass('infobox-label') | |||
css(notesLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
notesLabelDiv:wikitext('Notes') | |||
local notesValueDiv = group:tag('div') | |||
notesValueDiv:addClass('infobox-value') | |||
css(notesValueDiv, 'padding:0.2em 0.4em; font-size:0.92em;') | |||
notesValueDiv:wikitext(tbcNotes) | |||
end | |||
end | |||
end | |||
end | |||
end | |||
end | |||
-- Accessories section: Always show (like Parts/Documents), with "Add accessory" button | |||
addSectionRow(container, 'Accessories') | |||
-- Build the add accessory link pointing to Create_an_accessory with av_devices parameter | |||
local addAccessoryUrl = mw.uri.fullUrl( | |||
"Create an accessory", | |||
{ | |||
av_devices = pageName | |||
} | |||
) | |||
-- Query for accessories linked via Has related AV device | |||
-- Unlike CRT models which have a dedicated remote_control field, | |||
-- AV devices show all accessory types including remote controls here. | |||
local accessoriesCountWikitext = | |||
'{{#ask:\n' .. | |||
' [[Category:Accessories]]\n' .. | |||
' [[Has related AV device::' .. fullPageName .. ']]\n' .. | |||
' |format=count\n' .. | |||
'}}' | |||
local accessoryCount = frame:preprocess(accessoriesCountWikitext) | |||
local hasAccessories = accessoryCount and tonumber(trim(accessoryCount)) and tonumber(trim(accessoryCount)) > 0 | |||
local accDiv = container:tag('div') | |||
css(accDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | |||
if hasAccessories then | |||
-- Query accessories and display grouped by type | |||
local accessoriesQueryWikitext = | |||
'{{#ask:\n' .. | |||
' [[Category:Accessories]]\n' .. | |||
' [[Has related AV device::' .. fullPageName .. ']]\n' .. | |||
' |?Has accessory type=type\n' .. | |||
' |?Has model code=model\n' .. | |||
' |format=broadtable\n' .. | |||
' |headers=hide\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local accessoriesResult = frame:preprocess(accessoriesQueryWikitext) | |||
-- Parse accessories from broadtable result and group by type | |||
local accessoriesByType = {} | |||
local typeOrder = {} | |||
if accessoriesResult then | |||
for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do | |||
local cells = {} | |||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | |||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | |||
-- cells[1] = page name, cells[2] = type, cells[3] = model code | |||
if #cells >= 2 then | |||
local accPageName = cells[1] | |||
local accType = cells[2] or 'Accessory' | |||
if accPageName then | |||
if not accessoriesByType[accType] then | |||
accessoriesByType[accType] = {} | |||
table.insert(typeOrder, accType) | |||
end | |||
table.insert(accessoriesByType[accType], { | |||
page = accPageName | |||
}) | |||
end | |||
end | |||
end | |||
end | |||
-- Create a nested grid for accessory rows | |||
local accGrid = accDiv:tag('div') | |||
css(accGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;') | |||
-- Display as label/value rows grouped by type (like Parts) | |||
for _, accType in ipairs(typeOrder) do | |||
local accs = accessoriesByType[accType] | |||
if accs and #accs > 0 then | |||
local accLinks = {} | |||
for _, acc in ipairs(accs) do | |||
table.insert(accLinks, '[[' .. acc.page .. ']]') | |||
end | |||
local labelDiv = accGrid:tag('div') | |||
labelDiv:addClass('infobox-label') | |||
css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
labelDiv:wikitext(accType) | |||
local valueDiv = accGrid:tag('div') | |||
valueDiv:addClass('infobox-value') | |||
css(valueDiv, 'padding:0.2em 0.4em;') | |||
valueDiv:wikitext(table.concat(accLinks, '<br>')) | |||
end | |||
end | |||
-- Add "Add accessory" button | |||
local addAccDiv = mw.html.create('div') | |||
addAccDiv:css('margin-top', '0.5em'):css('text-align', 'center') | |||
addAccDiv:wikitext('[' .. tostring(addAccessoryUrl) .. ' <span class="infobox-button infobox-button-secondary">Add accessory</span>]') | |||
accDiv:node(addAccDiv) | |||
else | |||
-- Just show the add accessory link as a button | |||
local addAccDiv = mw.html.create('div') | |||
addAccDiv:css('text-align', 'center') | |||
addAccDiv:wikitext('[' .. tostring(addAccessoryUrl) .. ' <span class="infobox-button infobox-button-secondary">Add accessory</span>]') | |||
accDiv:node(addAccDiv) | |||
end | |||
-- Parts section | |||
addSectionRow(container, 'Parts') | |||
-- Build the add part link pointing to Create_a_part with av_devices parameter | |||
local addPartUrl = mw.uri.fullUrl( | |||
"Create a part", | |||
{ | |||
av_devices = pageName | |||
} | |||
) | |||
-- Query for parts linked via Has related AV device | |||
local partsCountWikitext = | |||
'{{#ask:\n' .. | |||
' [[Category:Parts]]\n' .. | |||
' [[Has related AV device::' .. fullPageName .. ']]\n' .. | |||
' |format=count\n' .. | |||
'}}' | |||
local partsCount = frame:preprocess(partsCountWikitext) | |||
local hasParts = partsCount and tonumber(trim(partsCount)) and tonumber(trim(partsCount)) > 0 | |||
local partsDiv = container:tag('div') | |||
css(partsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | |||
if hasParts then | |||
-- Query parts and display grouped by type/subtype | |||
local partsQueryWikitext = | |||
'{{#ask:\n' .. | |||
' [[Category:Parts]]\n' .. | |||
' [[Has related AV device::' .. fullPageName .. ']]\n' .. | |||
' |?Has part type=type\n' .. | |||
' |?Has part subtype=subtype\n' .. | |||
' |?Has model code=model\n' .. | |||
' |format=broadtable\n' .. | |||
' |headers=hide\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local partsResult = frame:preprocess(partsQueryWikitext) | |||
-- Parse parts from broadtable result and group by type | |||
local partsByType = {} | |||
local partTypeOrder = {} | |||
if partsResult then | |||
for rowContent in partsResult:gmatch('<tr[^>]*>(.-)</tr>') do | |||
local cells = {} | |||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | |||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | |||
-- cells[1] = page name, cells[2] = type, cells[3] = subtype, cells[4] = model code | |||
if #cells >= 2 then | |||
local partPageName = cells[1] | |||
local partType = cells[2] or 'Part' | |||
local partSubtype = cells[3] | |||
-- Use subtype + type as the grouping key if subtype exists | |||
local groupKey = partType | |||
if partSubtype and partSubtype ~= '' then | |||
groupKey = partSubtype .. ' ' .. partType | |||
end | |||
if partPageName then | |||
-- Track type order for consistent display | |||
if not partsByType[groupKey] then | |||
partsByType[groupKey] = {} | |||
table.insert(partTypeOrder, groupKey) | |||
end | |||
table.insert(partsByType[groupKey], { | |||
page = partPageName | |||
}) | |||
end | |||
end | |||
end | |||
end | |||
-- Create a nested grid for part rows | |||
local partsGrid = partsDiv:tag('div') | |||
css(partsGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;') | |||
-- Display as label/value rows grouped by type | |||
for _, partType in ipairs(partTypeOrder) do | |||
local parts = partsByType[partType] | |||
if parts and #parts > 0 then | |||
local partLinks = {} | |||
for _, part in ipairs(parts) do | |||
table.insert(partLinks, '[[' .. part.page .. ']]') | |||
end | |||
local labelDiv = partsGrid:tag('div') | |||
labelDiv:addClass('infobox-label') | |||
css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
labelDiv:wikitext(partType) | |||
local valueDiv = partsGrid:tag('div') | |||
valueDiv:addClass('infobox-value') | |||
css(valueDiv, 'padding:0.2em 0.4em;') | |||
valueDiv:wikitext(table.concat(partLinks, '<br>')) | |||
end | |||
end | end | ||
if | |||
addRow(container, ' | -- Add "Add part" button | ||
local addPartDiv = mw.html.create('div') | |||
addPartDiv:css('margin-top', '0.5em'):css('text-align', 'center') | |||
addPartDiv:wikitext('[' .. tostring(addPartUrl) .. ' <span class="infobox-button infobox-button-secondary">Add part</span>]') | |||
partsDiv:node(addPartDiv) | |||
else | |||
-- Just show the add part link as a button | |||
local addPartDiv = mw.html.create('div') | |||
addPartDiv:css('text-align', 'center') | |||
addPartDiv:wikitext('[' .. tostring(addPartUrl) .. ' <span class="infobox-button infobox-button-secondary">Add part</span>]') | |||
partsDiv:node(addPartDiv) | |||
end | |||
-- Archival Links section: Query for link subobjects and display by type | |||
local linksQueryWikitext = | |||
'{{#ask:\n' .. | |||
' [[AV device::' .. fullPageName .. ']]\n' .. | |||
' |?Model link type\n' .. | |||
' |?Model link source\n' .. | |||
' |?Model link URL\n' .. | |||
' |format=broadtable\n' .. | |||
' |headers=hide\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local linksResult = frame:preprocess(linksQueryWikitext) | |||
-- Parse links and organize by type | |||
local linksByType = { | |||
Manufacturer = {}, | |||
Retailer = {}, | |||
Database = {} | |||
} | |||
local hasLinks = false | |||
if linksResult then | |||
for rowContent in linksResult:gmatch('<tr[^>]*>(.-)</tr>') do | |||
local cells = {} | |||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | |||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | |||
-- cells[1] = page/subobject, cells[2] = type, cells[3] = source, cells[4] = URL | |||
if #cells >= 4 then | |||
local linkType = cells[2] | |||
local linkSource = cells[3] | |||
local linkUrl = cells[4] | |||
if linkType and linkSource and linkUrl and linkUrl ~= '' then | |||
if linksByType[linkType] then | |||
table.insert(linksByType[linkType], {source = linkSource, url = linkUrl}) | |||
hasLinks = true | |||
end | |||
end | |||
end | |||
end | |||
end | |||
if hasLinks then | |||
addSectionRow(container, 'Archival Links') | |||
local linkTypeOrder = {'Database', 'Manufacturer', 'Retailer'} | |||
for _, linkType in ipairs(linkTypeOrder) do | |||
if #linksByType[linkType] > 0 then | |||
local linksHtml = {} | |||
for _, link in ipairs(linksByType[linkType]) do | |||
table.insert(linksHtml, '[' .. link.url .. ' ' .. link.source .. ']') | |||
end | |||
addRow(container, linkType, table.concat(linksHtml, '<br>'), true) | |||
end | |||
end | end | ||
end | end | ||
| Line 265: | Line 937: | ||
if #cells >= 2 then | if #cells >= 2 then | ||
local docType = trim(cells[2]) | local docType = trim(cells[2]) | ||
local docTitle = trim(cells[5]) | local docTitle = trim(cells[5]) | ||
local docPageName = trim(cells[1]) | local docPageName = trim(cells[1]) | ||
| Line 289: | Line 959: | ||
local uploadDiv = mw.html.create('div') | local uploadDiv = mw.html.create('div') | ||
uploadDiv:css('margin-top', '0.5em'):css('text-align', 'center') | uploadDiv:css('margin-top', '0.5em'):css('text-align', 'center') | ||
uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add | uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | ||
docsDiv:node(uploadDiv) | docsDiv:node(uploadDiv) | ||
| Line 295: | Line 965: | ||
local uploadDiv = mw.html.create('div') | local uploadDiv = mw.html.create('div') | ||
uploadDiv:css('text-align', 'center') | uploadDiv:css('text-align', 'center') | ||
uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add | uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | ||
docsDiv:node(uploadDiv) | docsDiv:node(uploadDiv) | ||