Module:AVDevice: Difference between revisions
Rename args.oem_manufacturer to args.oem to match CRT model convention (via update-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) |
||
| (One intermediate revision by the same user not shown) | |||
| Line 58: | Line 58: | ||
end | end | ||
-- Helper function to strip brand prefix from item name if it matches device brand | |||
local function getDisplayTextWithoutBrand(itemName, deviceBrand) | local function getDisplayTextWithoutBrand(itemName, deviceBrand) | ||
if not itemName then return nil end | if not itemName then return nil end | ||
if not deviceBrand then return itemName end | if not deviceBrand then return itemName end | ||
local brandPrefix = deviceBrand .. ' ' | local brandPrefix = deviceBrand .. ' ' | ||
if itemName:sub(1, #brandPrefix) == brandPrefix then | if itemName:sub(1, #brandPrefix) == brandPrefix then | ||
return itemName:sub(#brandPrefix + 1) | return itemName:sub(#brandPrefix + 1) | ||
end | end | ||
return itemName | return itemName | ||
end | end | ||
-- Helper function to format comma-separated values as a bulleted list or inline | |||
local function formatList(value, inline) | local function formatList(value, inline) | ||
if not value then return nil end | if not value then return nil end | ||
| Line 73: | Line 77: | ||
for item in value:gmatch('[^,]+') do | for item in value:gmatch('[^,]+') do | ||
local trimmed = trim(item) | local trimmed = trim(item) | ||
if trimmed then table.insert(items, trimmed) end | if trimmed then | ||
table.insert(items, trimmed) | |||
end | |||
end | end | ||
if #items == 0 then return nil end | if #items == 0 then return nil end | ||
| Line 81: | Line 87: | ||
local ul = mw.html.create('ul') | local ul = mw.html.create('ul') | ||
ul:css('margin', '0'):css('padding-left', '1.2em') | ul:css('margin', '0'):css('padding-left', '1.2em') | ||
for _, item in ipairs(items) do ul:tag('li'):wikitext(item) end | for _, item in ipairs(items) do | ||
ul:tag('li'):wikitext(item) | |||
end | |||
return tostring(ul) | return tostring(ul) | ||
end | end | ||
end | end | ||
-- Helper to format MSRP with dollar sign and commas | |||
local function formatMSRP(value) | local function formatMSRP(value) | ||
if not value then return nil end | if not value then return nil end | ||
local num = tonumber(value) | local num = tonumber(value) | ||
if not num then return '$' .. value end | if not num then return '$' .. value end | ||
-- Format with commas | |||
local formatted = string.format("%.2f", num) | local formatted = string.format("%.2f", num) | ||
local intPart, decPart = formatted:match("^(%d+)%.(%d+)$") | local intPart, decPart = formatted:match("^(%d+)%.(%d+)$") | ||
| Line 100: | Line 110: | ||
end | end | ||
-- Helper to format related models as wiki links, stripping brand prefix | |||
local function formatRelatedModels(value, deviceBrand) | local function formatRelatedModels(value, deviceBrand) | ||
if not value then return nil end | if not value then return nil end | ||
| Line 114: | Line 125: | ||
end | end | ||
-- Helper to query inputs/outputs for a specific location | |||
local function queryIOByLocation(frame, fullPageName, ioType, location) | local function queryIOByLocation(frame, fullPageName, ioType, location) | ||
local locationFilter = '' | local locationFilter = '' | ||
| Line 119: | Line 131: | ||
locationFilter = ' [[Has ' .. (ioType == 'input' and 'input' or 'output') .. ' location::' .. location .. ']]\n' | locationFilter = ' [[Has ' .. (ioType == 'input' and 'input' or 'output') .. ' location::' .. location .. ']]\n' | ||
end | end | ||
local signalProp = ioType == 'input' and 'Has video signal format' or 'Has output signal format' | 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 countProp = ioType == 'input' and 'Has input count' or 'Has output count' | ||
| Line 125: | Line 138: | ||
local labelProp = ioType == 'input' and 'Has input label' or 'Has output label' | 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' | local templateName = ioType == 'input' and 'AV device input row' or 'AV device output row' | ||
-- Build audio property queries (only for inputs) | |||
local audioProps = '' | local audioProps = '' | ||
if ioType == 'input' then | if ioType == 'input' then | ||
| Line 131: | Line 146: | ||
' |?Has audio shared with=audio_shared\n' | ' |?Has audio shared with=audio_shared\n' | ||
end | end | ||
local queryWikitext = | local queryWikitext = | ||
'{{#ask:\n' .. | '{{#ask:\n' .. | ||
| Line 147: | Line 163: | ||
' |link=none\n' .. | ' |link=none\n' .. | ||
'}}' | '}}' | ||
local result = frame:preprocess(queryWikitext) | local result = frame:preprocess(queryWikitext) | ||
return trim(result) | return trim(result) | ||
end | 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 function discoverLocations(frame, fullPageName, ioType) | ||
local locationProp, signalProp | local locationProp, signalProp | ||
| Line 156: | Line 175: | ||
locationProp = 'Has input location' | locationProp = 'Has input location' | ||
signalProp = 'Has video signal format' | signalProp = 'Has video signal format' | ||
else | else -- output | ||
locationProp = 'Has output location' | locationProp = 'Has output location' | ||
signalProp = 'Has output signal format' | signalProp = 'Has output signal format' | ||
end | end | ||
local queryWikitext = | local queryWikitext = | ||
'{{#ask:\n' .. | '{{#ask:\n' .. | ||
| Line 170: | Line 190: | ||
' |link=none\n' .. | ' |link=none\n' .. | ||
'}}' | '}}' | ||
local result = frame:preprocess(queryWikitext) | local result = frame:preprocess(queryWikitext) | ||
-- Parse distinct locations from broadtable rows | |||
local seen = {} | local seen = {} | ||
local locations = {} | local locations = {} | ||
-- Preferred display order for common locations | |||
local preferredOrder = {rear = 1, front = 2, side = 3, top = 4, bottom = 5, internal = 6} | local preferredOrder = {rear = 1, front = 2, side = 3, top = 4, bottom = 5, internal = 6} | ||
if result then | if result then | ||
for rowContent in result:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in result:gmatch('<tr[^>]*>(.-)</tr>') do | ||
| Line 181: | Line 207: | ||
table.insert(cells, trim(cleanContent)) | table.insert(cells, trim(cleanContent)) | ||
end | end | ||
-- cells[1] = subobject name, cells[2] = location value | |||
if #cells >= 2 and cells[2] then | if #cells >= 2 and cells[2] then | ||
local loc = mw.text.trim(cells[2]) | local loc = mw.text.trim(cells[2]) | ||
-- Normalize to lowercase for deduplication | |||
local locLower = loc:lower() | local locLower = loc:lower() | ||
if locLower ~= '' and not seen[locLower] then | if locLower ~= '' and not seen[locLower] then | ||
| Line 191: | Line 219: | ||
end | end | ||
end | end | ||
-- Sort: preferred locations first (in defined order), then remaining alphabetically | |||
table.sort(locations, function(a, b) | table.sort(locations, function(a, b) | ||
local orderA = preferredOrder[a] or 1000 | local orderA = preferredOrder[a] or 1000 | ||
local orderB = preferredOrder[b] or 1000 | local orderB = preferredOrder[b] or 1000 | ||
if orderA ~= orderB then return orderA < orderB end | if orderA ~= orderB then | ||
return orderA < orderB | |||
end | |||
return a < b | return a < b | ||
end) | end) | ||
return locations | return locations | ||
end | end | ||
-- Capitalize first letter of a location string for display | |||
local function locationLabel(loc) | local function locationLabel(loc) | ||
if not loc or loc == '' then return '' end | if not loc or loc == '' then return '' end | ||
| Line 214: | Line 248: | ||
local pageId = title.id or 0 | local pageId = title.id or 0 | ||
-- Extract parameters | |||
local brand = trim(args.brand) | local brand = trim(args.brand) | ||
local model_code = trim(args.model_code) | local model_code = trim(args.model_code) | ||
| Line 238: | Line 273: | ||
local related_models = trim(args.related_models) | local related_models = trim(args.related_models) | ||
-- Use frame:extensionTag to create a proper <style> tag | |||
local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template:Infobox styles.css'}) | local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template:Infobox styles.css'}) | ||
| Line 245: | Line 281: | ||
css(container, 'float:right; clear:right; margin:0 0 1em 1em; width:22em; font-size:88%; line-height:1.5em; display:grid; grid-template-columns:auto 1fr; gap:3px; padding:3px;') | css(container, 'float:right; clear:right; margin:0 0 1em 1em; width:22em; font-size:88%; line-height:1.5em; display:grid; grid-template-columns:auto 1fr; gap:3px; padding:3px;') | ||
addFullRow(container, (brand or '') .. ' ' .. (model_code or ''), 'text-align:center; font-size:125%; font-weight:bold; padding:0.5em;', false, 'infobox-title') | -- Title row | ||
addFullRow( | |||
container, | |||
(brand or '') .. ' ' .. (model_code or ''), | |||
'text-align:center; font-size:125%; font-weight:bold; padding:0.5em;', | |||
false, | |||
'infobox-title' | |||
) | |||
-- Type badge (breadcrumb) — plainlinks hides external icon, $wgNoFollowDomainExceptions handles nofollow | |||
local badgeWikitext = '[[AV Device Search|AV device]]' | local badgeWikitext = '[[AV Device Search|AV device]]' | ||
if device_type then | if device_type then | ||
| Line 256: | Line 300: | ||
if image_main then | if image_main then | ||
local file = image_main:gsub('^File:', '') | local file = image_main:gsub('^File:', '') | ||
addFullRow(container, string.format('[[File:%s|frameless|300px]]', file), 'text-align:center; padding:0;', false, 'infobox-image') | addFullRow( | ||
container, | |||
string.format('[[File:%s|frameless|300px]]', file), | |||
'text-align:center; padding:0;', | |||
false, | |||
'infobox-image' | |||
) | |||
end | end | ||
addSectionRow(container, 'Overview') | addSectionRow(container, 'Overview') | ||
-- Brand row using #formredlink | |||
if brand then | if brand then | ||
local brandWikitext = string.format('{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', brand, brand) | local brandWikitext = string.format( | ||
addRow(container, 'Brand', | '{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', | ||
brand, brand | |||
) | |||
local brandHtml = frame:preprocess(brandWikitext) | |||
addRow(container, 'Brand', brandHtml, true) | |||
end | end | ||
-- OEM row (shown only when set) | |||
if oem_manufacturer then | if oem_manufacturer then | ||
local oemWikitext = string.format('{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', oem_manufacturer, oem_manufacturer) | local oemWikitext = string.format( | ||
addRow(container, 'OEM', | '{{#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 | |||
if model_code then | |||
addRow(container, 'Model', model_code, false) | |||
end | end | ||
-- Market row | |||
if market then addRow(container, 'Market', market, false) end | 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 | ||
local typeSearchUrl = tostring(mw.uri.fullUrl('AV Device Search', {device_type = device_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 | end | ||
-- Format family with link to category | |||
if format_family then | if format_family then | ||
addRow(container, 'Format family', '[[:Category:' .. format_family .. ' devices|' .. format_family .. ']]', true) | addRow(container, 'Format family', '[[:Category:' .. format_family .. ' devices|' .. format_family .. ']]', true) | ||
end | end | ||
-- Year info | |||
if year_released or year_discontinued then | if year_released or year_discontinued then | ||
local yearStr = '' | local yearStr = '' | ||
if year_released and year_discontinued then yearStr = year_released .. ' – ' .. year_discontinued | if year_released and year_discontinued then | ||
elseif year_released then yearStr = year_released .. ' –' | yearStr = year_released .. ' – ' .. year_discontinued | ||
elseif year_discontinued then yearStr = '– ' .. year_discontinued end | elseif year_released then | ||
yearStr = year_released .. ' –' | |||
elseif year_discontinued then | |||
yearStr = '– ' .. year_discontinued | |||
end | |||
addRow(container, 'Years', yearStr, false) | addRow(container, 'Years', yearStr, false) | ||
end | end | ||
if msrp then addRow(container, 'MSRP', formatMSRP(msrp), false) end | -- MSRP | ||
if msrp then | |||
addRow(container, 'MSRP', formatMSRP(msrp), false) | |||
end | |||
-- AV System support | |||
if av_system then | if av_system then | ||
local formattedAV = formatList(av_system, true) | local formattedAV = formatList(av_system, true) | ||
if formattedAV then addRow(container, 'AV system', formattedAV, false) end | if formattedAV then | ||
addRow(container, 'AV system', formattedAV, false) | |||
end | |||
end | end | ||
-- Related models | |||
if related_models then | if related_models then | ||
local formattedRelated = formatRelatedModels(related_models, brand) | local formattedRelated = formatRelatedModels(related_models, brand) | ||
if formattedRelated then addRow(container, 'Related', formattedRelated, true) end | if formattedRelated then | ||
addRow(container, 'Related', formattedRelated, true) | |||
end | |||
end | end | ||
-- ECID row | |||
local ecidWikitext = string.format('[{{fullurl:%s|curid=%d}} EC%d]', pageName, pageId, pageId) | local ecidWikitext = string.format('[{{fullurl:%s|curid=%d}} EC%d]', pageName, pageId, pageId) | ||
addRow(container, 'ECID', frame:preprocess(ecidWikitext), true) | addRow(container, 'ECID', frame:preprocess(ecidWikitext), true) | ||
-- Format compatibility section (only show if tape-based device) | |||
if supported_formats or recording_formats then | if supported_formats or recording_formats then | ||
addSectionRow(container, 'Format Compatibility') | addSectionRow(container, 'Format Compatibility') | ||
if supported_formats then | if supported_formats then | ||
local | local formattedSupported = formatList(supported_formats, true) | ||
if | if formattedSupported then | ||
addRow(container, 'Plays', formattedSupported, false) | |||
end | |||
end | end | ||
if recording_formats then | if recording_formats then | ||
local | local formattedRecording = formatList(recording_formats, true) | ||
if | if formattedRecording then | ||
addRow(container, 'Records', formattedRecording, false) | |||
end | |||
end | end | ||
end | end | ||
-- Inputs and Outputs sections (matching CRT model style) | |||
local function renderIOSection(sectionTitle, ioType) | local function renderIOSection(sectionTitle, ioType) | ||
local locations = discoverLocations(frame, fullPageName, ioType) | local locations = discoverLocations(frame, fullPageName, ioType) | ||
local locationData = {} | local locationData = {} | ||
for _, loc in ipairs(locations) do | for _, loc in ipairs(locations) do | ||
local content = queryIOByLocation(frame, fullPageName, ioType, loc) | local content = queryIOByLocation(frame, fullPageName, ioType, loc) | ||
| Line 327: | Line 420: | ||
end | end | ||
end | end | ||
if #locationData > 0 then | if #locationData > 0 then | ||
addSectionRow(container, sectionTitle) | addSectionRow(container, sectionTitle) | ||
| Line 334: | Line 428: | ||
css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | ||
labelDiv:wikitext(locationLabel(data.location)) | labelDiv:wikitext(locationLabel(data.location)) | ||
local valueDiv = container:tag('div') | local valueDiv = container:tag('div') | ||
valueDiv:addClass('infobox-value') | valueDiv:addClass('infobox-value') | ||
| Line 345: | Line 440: | ||
renderIOSection('Outputs', 'output') | renderIOSection('Outputs', 'output') | ||
-- Features section | |||
if features then | if features then | ||
addSectionRow(container, 'Features') | addSectionRow(container, 'Features') | ||
| Line 350: | Line 446: | ||
end | end | ||
-- Dimensions section | |||
if height or width or depth or weight then | if height or width or depth or weight then | ||
addSectionRow(container, 'Dimensions') | addSectionRow(container, 'Dimensions') | ||
if height or width or depth then | if height or width or depth then | ||
addRow(container, 'H × W × D', string.format('%s × %s × %s', | local h = height or '–' | ||
local w = width or '–' | |||
local d = depth or '–' | |||
addRow(container, 'H × W × D', string.format('%s × %s × %s', h, w, d), false) | |||
end | end | ||
if weight then | if weight then | ||
local weightWikitext = '{{#show: ' .. fullPageName .. ' |?Weight # lb }} / {{#show: ' .. fullPageName .. ' |?Weight # kg }}' | local weightWikitext = '{{#show: ' .. fullPageName .. ' |?Weight # lb }} / {{#show: ' .. fullPageName .. ' |?Weight # kg }}' | ||
| Line 361: | Line 463: | ||
end | end | ||
-- Power section - query voltage ranges and display power specs | |||
local voltageQueryWikitext = | 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}}' | '{{#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 voltageResult = frame:preprocess(voltageQueryWikitext) | ||
local hasVoltage = voltageResult and trim(voltageResult) and trim(voltageResult) ~= '' | local hasVoltage = voltageResult and trim(voltageResult) and trim(voltageResult) ~= '' | ||
| Line 368: | Line 480: | ||
if hasVoltage or frequency or power_avg or power_max then | if hasVoltage or frequency or power_avg or power_max then | ||
addSectionRow(container, 'Power') | addSectionRow(container, 'Power') | ||
if hasVoltage then addRow(container, 'Voltage', voltageResult, true) end | |||
if hasVoltage then | |||
addRow(container, 'Voltage', voltageResult, true) | |||
end | |||
if frequency then | if frequency then | ||
local formattedFreq = formatList(frequency, true) | local formattedFreq = formatList(frequency, true) | ||
if formattedFreq then addRow(container, 'AC frequency', formattedFreq, false) end | if formattedFreq then | ||
addRow(container, 'AC frequency', formattedFreq, false) | |||
end | |||
end | end | ||
if power_avg or power_max then | if power_avg or power_max then | ||
local powerStr = '' | local powerStr = '' | ||
if power_avg and power_max then powerStr = power_avg .. 'W (avg) / ' .. power_max .. 'W (max)' | if power_avg and power_max then | ||
elseif power_avg then powerStr = power_avg .. 'W' | powerStr = power_avg .. 'W (avg) / ' .. power_max .. 'W (max)' | ||
elseif power_max then powerStr = power_max .. 'W (max)' end | elseif power_avg then | ||
powerStr = power_avg .. 'W' | |||
elseif power_max then | |||
powerStr = power_max .. 'W (max)' | |||
end | |||
addRow(container, 'Power', powerStr, false) | addRow(container, 'Power', powerStr, false) | ||
end | end | ||
end | end | ||
local tbcCountWikitext = '{{#ask:\n [[TBC type::+]]\n [[-Has subobject::' .. fullPageName .. ']]\n |format=count\n}}' | -- 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 tbcCountResult = frame:preprocess(tbcCountWikitext) | ||
local hasTBC = tbcCountResult and tonumber(trim(tbcCountResult)) and tonumber(trim(tbcCountResult)) > 0 | local hasTBC = tbcCountResult and tonumber(trim(tbcCountResult)) and tonumber(trim(tbcCountResult)) > 0 | ||
| Line 388: | Line 517: | ||
if hasTBC then | if hasTBC then | ||
addSectionRow(container, 'Time Base Correction') | 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 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) | local tbcResult = frame:preprocess(tbcQueryWikitext) | ||
if tbcResult then | if tbcResult then | ||
local entryIndex = 0 | |||
for rowContent in tbcResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in tbcResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, cleanContent) | |||
end | end | ||
if #cells >= 2 then | if #cells >= 2 then | ||
local tbcType = trim(cells[2]) | local tbcType = trim(cells[2]) | ||
local tbcBuffer = trim(cells[3]) | local tbcBuffer = trim(cells[3]) | ||
local tbcNotes = trim(cells[4]) | local tbcNotes = trim(cells[4]) | ||
if tbcType then | if tbcType then | ||
entryIndex = entryIndex + 1 | |||
-- Create a full-width grouped container with nested grid | |||
local group = container:tag('div') | local group = container:tag('div') | ||
group:addClass('infobox-tbc-group') | group:addClass('infobox-tbc-group') | ||
css(group, 'grid-column: 1 / -1; display:grid; grid-template-columns:auto 1fr; gap:3px;') | css(group, 'grid-column: 1 / -1; display:grid; grid-template-columns:auto 1fr; gap:3px;') | ||
local | |||
css( | -- Type row | ||
local typeLabelDiv = group:tag('div') | |||
local | typeLabelDiv:addClass('infobox-label') | ||
css( | 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 | if tbcBuffer then | ||
local | local bufLabelDiv = group:tag('div') | ||
css( | bufLabelDiv:addClass('infobox-label') | ||
css(bufLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
local | bufLabelDiv:wikitext('Buffer') | ||
css( | |||
local bufValueDiv = group:tag('div') | |||
bufValueDiv:addClass('infobox-value') | |||
css(bufValueDiv, 'padding:0.2em 0.4em;') | |||
bufValueDiv:wikitext(tbcBuffer) | |||
end | end | ||
-- Notes row (conditional) | |||
if tbcNotes then | if tbcNotes then | ||
local | local notesLabelDiv = group:tag('div') | ||
css( | notesLabelDiv:addClass('infobox-label') | ||
css(notesLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
local | notesLabelDiv:wikitext('Notes') | ||
css( | |||
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 | ||
| Line 432: | Line 595: | ||
end | end | ||
-- Accessories section | -- Accessories section: Always show (like Parts/Documents), with "Add accessory" button | ||
addSectionRow(container, 'Accessories') | addSectionRow(container, 'Accessories') | ||
local addAccessoryUrl = mw.uri.fullUrl("Create an accessory", {av_devices = pageName}) | |||
local accessoriesCountWikitext = '{{#ask:\n [[Category:Accessories]]\n [[Has related AV device::' .. fullPageName .. ']]\n |format=count\n}}' | -- 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 accessoryCount = frame:preprocess(accessoriesCountWikitext) | ||
local hasAccessories = accessoryCount and tonumber(trim(accessoryCount)) and tonumber(trim(accessoryCount)) > 0 | local hasAccessories = accessoryCount and tonumber(trim(accessoryCount)) and tonumber(trim(accessoryCount)) > 0 | ||
local accDiv = container:tag('div') | local accDiv = container:tag('div') | ||
css(accDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | css(accDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | ||
if hasAccessories then | if hasAccessories then | ||
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}}' | -- 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) | local accessoriesResult = frame:preprocess(accessoriesQueryWikitext) | ||
-- Parse accessories from broadtable result and group by type | |||
local accessoriesByType = {} | local accessoriesByType = {} | ||
local typeOrder = {} | local typeOrder = {} | ||
if accessoriesResult then | if accessoriesResult then | ||
for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | end | ||
-- cells[1] = page name, cells[2] = type, cells[3] = model code | |||
if #cells >= 2 then | if #cells >= 2 then | ||
local accPageName = cells[1] | local accPageName = cells[1] | ||
local accType = cells[2] or 'Accessory' | local accType = cells[2] or 'Accessory' | ||
if accPageName then | if accPageName then | ||
if not accessoriesByType[accType] then | if not accessoriesByType[accType] then | ||
| Line 460: | Line 657: | ||
table.insert(typeOrder, accType) | table.insert(typeOrder, accType) | ||
end | end | ||
table.insert(accessoriesByType[accType], {page = accPageName}) | |||
table.insert(accessoriesByType[accType], { | |||
page = accPageName | |||
}) | |||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
-- Create a nested grid for accessory rows | |||
local accGrid = accDiv:tag('div') | local accGrid = accDiv:tag('div') | ||
css(accGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;') | 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 | for _, accType in ipairs(typeOrder) do | ||
local accs = accessoriesByType[accType] | local accs = accessoriesByType[accType] | ||
if accs and #accs > 0 then | if accs and #accs > 0 then | ||
local accLinks = {} | local accLinks = {} | ||
for _, acc in ipairs(accs) do table.insert(accLinks, '[[' .. acc.page .. ']]') end | for _, acc in ipairs(accs) do | ||
local | table.insert(accLinks, '[[' .. acc.page .. ']]') | ||
css( | end | ||
local | local labelDiv = accGrid:tag('div') | ||
css( | 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 | ||
end | end | ||
-- Add "Add accessory" button | |||
local addAccDiv = mw.html.create('div') | local addAccDiv = mw.html.create('div') | ||
addAccDiv:css('margin-top', '0.5em'):css('text-align', 'center') | addAccDiv:css('margin-top', '0.5em'):css('text-align', 'center') | ||
| Line 485: | Line 697: | ||
accDiv:node(addAccDiv) | accDiv:node(addAccDiv) | ||
else | else | ||
-- Just show the add accessory link as a button | |||
local addAccDiv = mw.html.create('div') | local addAccDiv = mw.html.create('div') | ||
addAccDiv:css('text-align', 'center') | addAccDiv:css('text-align', 'center') | ||
| Line 493: | Line 706: | ||
-- Parts section | -- Parts section | ||
addSectionRow(container, 'Parts') | addSectionRow(container, 'Parts') | ||
local addPartUrl = mw.uri.fullUrl("Create a part", {av_devices = pageName}) | |||
local partsCountWikitext = '{{#ask:\n [[Category:Parts]]\n [[Has related AV device::' .. fullPageName .. ']]\n |format=count\n}}' | -- 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 partsCount = frame:preprocess(partsCountWikitext) | ||
local hasParts = partsCount and tonumber(trim(partsCount)) and tonumber(trim(partsCount)) > 0 | local hasParts = partsCount and tonumber(trim(partsCount)) and tonumber(trim(partsCount)) > 0 | ||
local partsDiv = container:tag('div') | local partsDiv = container:tag('div') | ||
css(partsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | css(partsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | ||
if hasParts then | if hasParts then | ||
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}}' | -- 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) | local partsResult = frame:preprocess(partsQueryWikitext) | ||
-- Parse parts from broadtable result and group by type | |||
local partsByType = {} | local partsByType = {} | ||
local partTypeOrder = {} | local partTypeOrder = {} | ||
if partsResult then | if partsResult then | ||
for rowContent in partsResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in partsResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | end | ||
-- cells[1] = page name, cells[2] = type, cells[3] = subtype, cells[4] = model code | |||
if #cells >= 2 then | if #cells >= 2 then | ||
local partPageName = cells[1] | local partPageName = cells[1] | ||
local partType = cells[2] or 'Part' | local partType = cells[2] or 'Part' | ||
local partSubtype = cells[3] | local partSubtype = cells[3] | ||
-- Use subtype + type as the grouping key if subtype exists | |||
local groupKey = partType | local groupKey = partType | ||
if partSubtype and partSubtype ~= '' then groupKey = partSubtype .. ' ' .. partType end | if partSubtype and partSubtype ~= '' then | ||
groupKey = partSubtype .. ' ' .. partType | |||
end | |||
if partPageName then | if partPageName then | ||
-- Track type order for consistent display | |||
if not partsByType[groupKey] then | if not partsByType[groupKey] then | ||
partsByType[groupKey] = {} | partsByType[groupKey] = {} | ||
table.insert(partTypeOrder, groupKey) | table.insert(partTypeOrder, groupKey) | ||
end | end | ||
table.insert(partsByType[groupKey], {page = partPageName}) | |||
table.insert(partsByType[groupKey], { | |||
page = partPageName | |||
}) | |||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
-- Create a nested grid for part rows | |||
local partsGrid = partsDiv:tag('div') | local partsGrid = partsDiv:tag('div') | ||
css(partsGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;') | 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 | for _, partType in ipairs(partTypeOrder) do | ||
local parts = partsByType[partType] | local parts = partsByType[partType] | ||
if parts and #parts > 0 then | if parts and #parts > 0 then | ||
local partLinks = {} | local partLinks = {} | ||
for _, part in ipairs(parts) do table.insert(partLinks, '[[' .. part.page .. ']]') end | for _, part in ipairs(parts) do | ||
local | table.insert(partLinks, '[[' .. part.page .. ']]') | ||
css( | end | ||
local | local labelDiv = partsGrid:tag('div') | ||
css( | 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 | end | ||
-- Add "Add part" button | |||
local addPartDiv = mw.html.create('div') | local addPartDiv = mw.html.create('div') | ||
addPartDiv:css('margin-top', '0.5em'):css('text-align', 'center') | addPartDiv:css('margin-top', '0.5em'):css('text-align', 'center') | ||
| Line 547: | Line 813: | ||
partsDiv:node(addPartDiv) | partsDiv:node(addPartDiv) | ||
else | else | ||
-- Just show the add part link as a button | |||
local addPartDiv = mw.html.create('div') | local addPartDiv = mw.html.create('div') | ||
addPartDiv:css('text-align', 'center') | addPartDiv:css('text-align', 'center') | ||
| Line 553: | Line 820: | ||
end | end | ||
-- Archival Links section | -- 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 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) | local linksResult = frame:preprocess(linksQueryWikitext) | ||
local linksByType = {Manufacturer = {}, Retailer = {}, Database = {}} | |||
-- Parse links and organize by type | |||
local linksByType = { | |||
Manufacturer = {}, | |||
Retailer = {}, | |||
Database = {} | |||
} | |||
local hasLinks = false | local hasLinks = false | ||
if linksResult then | if linksResult then | ||
for rowContent in linksResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in linksResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | end | ||
-- cells[1] = page/subobject, cells[2] = type, cells[3] = source, cells[4] = URL | |||
if #cells >= 4 then | if #cells >= 4 then | ||
local linkType = cells[2] | local linkType = cells[2] | ||
local linkSource = cells[3] | local linkSource = cells[3] | ||
local linkUrl = cells[4] | local linkUrl = cells[4] | ||
if linkType and linkSource and linkUrl and linkUrl ~= '' | |||
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 | ||
end | end | ||
end | end | ||
if hasLinks then | if hasLinks then | ||
addSectionRow(container, 'Archival Links') | addSectionRow(container, 'Archival Links') | ||
local linkTypeOrder = {'Database', 'Manufacturer', 'Retailer'} | |||
for _, linkType in ipairs(linkTypeOrder) do | |||
if #linksByType[linkType] > 0 then | if #linksByType[linkType] > 0 then | ||
local linksHtml = {} | local linksHtml = {} | ||
| Line 590: | Line 882: | ||
-- Documents section | -- Documents section | ||
addSectionRow(container, 'Documents') | addSectionRow(container, 'Documents') | ||
local timestamp = os.time() | local timestamp = os.time() | ||
math.randomseed(timestamp + pageId) | math.randomseed(timestamp + pageId) | ||
local random = math.random(1000, 9999) | local random = math.random(1000, 9999) | ||
local target = "Document:" .. timestamp .. "-" .. random | local target = "Document:" .. timestamp .. "-" .. random | ||
local uploadUrl = mw.uri.fullUrl("Special:FormEdit", {form = "Document", target = target, ["Document[av_devices]"] = pageName}) | local uploadUrl = mw.uri.fullUrl( | ||
local documentsCountWikitext = '{{#ask:\n [[Category:Documents]]\n [[Has related AV device::' .. fullPageName .. ']]\n |format=count\n}}' | "Special:FormEdit", | ||
{ | |||
form = "Document", | |||
target = target, | |||
["Document[av_devices]"] = pageName | |||
} | |||
) | |||
-- Query for document count | |||
local documentsCountWikitext = | |||
'{{#ask:\n' .. | |||
' [[Category:Documents]]\n' .. | |||
' [[Has related AV device::' .. fullPageName .. ']]\n' .. | |||
' |format=count\n' .. | |||
'}}' | |||
local documentCount = frame:preprocess(documentsCountWikitext) | local documentCount = frame:preprocess(documentsCountWikitext) | ||
local hasDocuments = documentCount and tonumber(trim(documentCount)) and tonumber(trim(documentCount)) > 0 | local hasDocuments = documentCount and tonumber(trim(documentCount)) and tonumber(trim(documentCount)) > 0 | ||
local docsDiv = container:tag('div') | local docsDiv = container:tag('div') | ||
css(docsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | css(docsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | ||
if hasDocuments then | if hasDocuments then | ||
local docsQueryWikitext = '{{#ask:\n [[Category:Documents]]\n [[Has related AV device::' .. fullPageName .. ']]\n |?Has document type\n |?Has document URL\n |?Has document file\n |?Has document title\n |format=broadtable\n |headers=hide\n |link=none\n}}' | local docsQueryWikitext = | ||
'{{#ask:\n' .. | |||
' [[Category:Documents]]\n' .. | |||
' [[Has related AV device::' .. fullPageName .. ']]\n' .. | |||
' |?Has document type\n' .. | |||
' |?Has document URL\n' .. | |||
' |?Has document file\n' .. | |||
' |?Has document title\n' .. | |||
' |format=broadtable\n' .. | |||
' |headers=hide\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local docsResult = frame:preprocess(docsQueryWikitext) | local docsResult = frame:preprocess(docsQueryWikitext) | ||
local ul = mw.html.create('ul') | local ul = mw.html.create('ul') | ||
ul:css('margin', '0'):css('padding-left', '1.5em') | ul:css('margin', '0'):css('padding-left', '1.5em') | ||
if docsResult then | if docsResult then | ||
for rowContent in docsResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in docsResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, cleanContent) | |||
end | end | ||
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]) | ||
if docPageName then | if docPageName then | ||
local displayText = 'Document' | local displayText = 'Document' | ||
if docTitle then displayText = docTitle | if docTitle then | ||
elseif docType then displayText = docType end | displayText = docTitle | ||
ul:tag('li'):wikitext('[[' .. docPageName .. '|' .. displayText .. ']]') | elseif docType then | ||
displayText = docType | |||
end | |||
local li = ul:tag('li') | |||
li:wikitext('[[' .. docPageName .. '|' .. displayText .. ']]') | |||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
docsDiv:node(ul) | docsDiv:node(ul) | ||
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 document</span>]') | uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | ||
docsDiv:node(uploadDiv) | docsDiv:node(uploadDiv) | ||
else | else | ||
| Line 634: | Line 966: | ||
uploadDiv:css('text-align', 'center') | uploadDiv:css('text-align', 'center') | ||
uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | ||
docsDiv:node(uploadDiv) | docsDiv:node(uploadDiv) | ||
end | end | ||
| Line 639: | Line 972: | ||
-- Contribute section | -- Contribute section | ||
addSectionRow(container, 'Contribute') | addSectionRow(container, 'Contribute') | ||
local editUrl = mw.uri.fullUrl("Special:FormStart", {form = "AV device", page_name = pageName}) | |||
local editUrl = mw.uri.fullUrl( | |||
"Special:FormStart", | |||
{ | |||
form = "AV device", | |||
page_name = pageName | |||
} | |||
) | |||
local editDiv = container:tag('div') | local editDiv = container:tag('div') | ||
css(editDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em; text-align:center;') | css(editDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em; text-align:center;') | ||
Latest revision as of 06:24, 25 February 2026
Documentation for this module may be created at Module:AVDevice/doc
-- Module:AVDevice
-- Builds the AV device infobox as strict HTML to avoid whitespace issues
-- Uses frame:preprocess() so parser functions like #ask and #formredlink evaluate.
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
local function css(node, cssText)
if cssText and cssText ~= '' then
node:cssText(cssText)
end
return node
end
local function addSectionRow(container, title)
local section = container:tag('div')
section:addClass('infobox-section-header')
css(section, 'grid-column: 1 / -1; text-align:center; padding:0.5em; font-weight:bold;')
section:wikitext(title)
end
local function addRow(container, label, valueHtmlOrWikitext, isRawHtml)
local labelDiv = container:tag('div')
labelDiv:addClass('infobox-label')
css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
labelDiv:wikitext(label)
local valueDiv = container:tag('div')
valueDiv:addClass('infobox-value')
css(valueDiv, 'padding:0.2em 0.4em;')
if isRawHtml then
valueDiv:node(mw.html.create('span'):wikitext(valueHtmlOrWikitext))
else
valueDiv:wikitext(valueHtmlOrWikitext)
end
end
local function addFullRow(container, valueHtmlOrWikitext, cssText, isRawHtml, extraClass)
local fullDiv = container:tag('div')
css(fullDiv, 'grid-column: 1 / -1; ' .. (cssText or ''))
if extraClass then
fullDiv:addClass(extraClass)
end
if isRawHtml then
fullDiv:wikitext(valueHtmlOrWikitext)
else
fullDiv:wikitext(valueHtmlOrWikitext)
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
function p.infobox(frame)
local parent = frame:getParent()
local args = parent and parent.args or frame.args
local title = mw.title.getCurrentTitle()
local pageName = title.text
local fullPageName = title.fullText
local pageId = title.id or 0
-- Extract parameters
local brand = trim(args.brand)
local model_code = trim(args.model_code)
local oem_manufacturer = trim(args.oem)
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 year_released = trim(args.year_released)
local year_discontinued = trim(args.year_discontinued)
local msrp = trim(args.msrp)
local av_system = trim(args.av_system)
local tuner_system = trim(args.tuner_system)
local frequency = trim(args.frequency)
local power_avg = trim(args.power_avg)
local power_max = trim(args.power_max)
local features = trim(args.features)
local height = trim(args.height)
local width = trim(args.width)
local depth = trim(args.depth)
local weight = trim(args.weight)
local related_models = trim(args.related_models)
-- Use frame:extensionTag to create a proper <style> tag
local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template:Infobox styles.css'})
local container = mw.html.create('div')
container:addClass('infobox')
container:addClass('infobox-av-device')
css(container, 'float:right; clear:right; margin:0 0 1em 1em; width:22em; font-size:88%; line-height:1.5em; display:grid; grid-template-columns:auto 1fr; gap:3px; padding:3px;')
-- Title row
addFullRow(
container,
(brand or '') .. ' ' .. (model_code or ''),
'text-align:center; font-size:125%; font-weight:bold; padding:0.5em;',
false,
'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
local file = image_main:gsub('^File:', '')
addFullRow(
container,
string.format('[[File:%s|frameless|300px]]', file),
'text-align:center; padding:0;',
false,
'infobox-image'
)
end
addSectionRow(container, 'Overview')
-- Brand row using #formredlink
if brand then
local brandWikitext = string.format(
'{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}',
brand, brand
)
local brandHtml = frame:preprocess(brandWikitext)
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
if model_code then
addRow(container, 'Model', model_code, false)
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
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
-- Year info
if year_released or year_discontinued then
local yearStr = ''
if year_released and year_discontinued then
yearStr = year_released .. ' – ' .. year_discontinued
elseif year_released then
yearStr = year_released .. ' –'
elseif year_discontinued then
yearStr = '– ' .. year_discontinued
end
addRow(container, 'Years', yearStr, false)
end
-- MSRP
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
-- ECID row
local ecidWikitext = string.format('[{{fullurl:%s|curid=%d}} EC%d]', pageName, pageId, pageId)
addRow(container, 'ECID', frame:preprocess(ecidWikitext), true)
-- Format compatibility section (only show if tape-based device)
if supported_formats or recording_formats then
addSectionRow(container, 'Format Compatibility')
if supported_formats then
local formattedSupported = formatList(supported_formats, true)
if formattedSupported then
addRow(container, 'Plays', formattedSupported, false)
end
end
if recording_formats then
local formattedRecording = formatList(recording_formats, true)
if formattedRecording then
addRow(container, 'Records', formattedRecording, false)
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
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
renderIOSection('Inputs', 'input')
renderIOSection('Outputs', 'output')
-- Features section
if features then
addSectionRow(container, 'Features')
addFullRow(container, features, 'padding:0.2em 0.4em;', false)
end
-- Dimensions section
if height or width or depth or weight then
addSectionRow(container, 'Dimensions')
if height or width or depth then
local h = height or '–'
local w = width or '–'
local d = depth or '–'
addRow(container, 'H × W × D', string.format('%s × %s × %s', h, w, d), false)
end
if weight then
local weightWikitext = '{{#show: ' .. fullPageName .. ' |?Weight # lb }} / {{#show: ' .. fullPageName .. ' |?Weight # kg }}'
addRow(container, 'Weight', frame:preprocess(weightWikitext), true)
end
end
-- Power section - query voltage ranges and display power specs
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')
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
-- 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
-- Documents section
addSectionRow(container, 'Documents')
local timestamp = os.time()
math.randomseed(timestamp + pageId)
local random = math.random(1000, 9999)
local target = "Document:" .. timestamp .. "-" .. random
local uploadUrl = mw.uri.fullUrl(
"Special:FormEdit",
{
form = "Document",
target = target,
["Document[av_devices]"] = pageName
}
)
-- Query for document count
local documentsCountWikitext =
'{{#ask:\n' ..
' [[Category:Documents]]\n' ..
' [[Has related AV device::' .. fullPageName .. ']]\n' ..
' |format=count\n' ..
'}}'
local documentCount = frame:preprocess(documentsCountWikitext)
local hasDocuments = documentCount and tonumber(trim(documentCount)) and tonumber(trim(documentCount)) > 0
local docsDiv = container:tag('div')
css(docsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')
if hasDocuments then
local docsQueryWikitext =
'{{#ask:\n' ..
' [[Category:Documents]]\n' ..
' [[Has related AV device::' .. fullPageName .. ']]\n' ..
' |?Has document type\n' ..
' |?Has document URL\n' ..
' |?Has document file\n' ..
' |?Has document title\n' ..
' |format=broadtable\n' ..
' |headers=hide\n' ..
' |link=none\n' ..
'}}'
local docsResult = frame:preprocess(docsQueryWikitext)
local ul = mw.html.create('ul')
ul:css('margin', '0'):css('padding-left', '1.5em')
if docsResult then
for rowContent in docsResult: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 docType = trim(cells[2])
local docTitle = trim(cells[5])
local docPageName = trim(cells[1])
if docPageName then
local displayText = 'Document'
if docTitle then
displayText = docTitle
elseif docType then
displayText = docType
end
local li = ul:tag('li')
li:wikitext('[[' .. docPageName .. '|' .. displayText .. ']]')
end
end
end
end
docsDiv:node(ul)
local uploadDiv = mw.html.create('div')
uploadDiv:css('margin-top', '0.5em'):css('text-align', 'center')
uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]')
docsDiv:node(uploadDiv)
else
local uploadDiv = mw.html.create('div')
uploadDiv:css('text-align', 'center')
uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]')
docsDiv:node(uploadDiv)
end
-- Contribute section
addSectionRow(container, 'Contribute')
local editUrl = mw.uri.fullUrl(
"Special:FormStart",
{
form = "AV device",
page_name = pageName
}
)
local editDiv = container:tag('div')
css(editDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em; text-align:center;')
editDiv:wikitext('[' .. tostring(editUrl) .. ' <span class="infobox-button infobox-button-primary">Edit this data</span>]')
return styleTag .. tostring(container)
end
return p