Module:CRTModel: Difference between revisions
Fix badge: replace broken mw.html.create('a') with plain wikilinks (MediaWiki sanitizer strips raw a tags) (via update-page on MediaWiki MCP Server) |
Change "Add accessory" button to link to Create_an_accessory with crt_models parameter (via update-page on MediaWiki MCP Server) |
||
| (3 intermediate revisions by the same user not shown) | |||
| Line 298: | Line 298: | ||
) | ) | ||
-- Type badge (breadcrumb) — | -- Type badge (breadcrumb) — plainlinks hides external icon, $wgNoFollowDomainExceptions handles nofollow | ||
local badgeWikitext = '[[CRT Search|CRT]]' | local badgeWikitext = '[[CRT Search|CRT]]' | ||
if crt_type then | if crt_type then | ||
badgeWikitext = badgeWikitext .. ' › [ | local typeUrl = mw.title.new('CRT Search'):fullUrl({crt_type = crt_type}) | ||
badgeWikitext = badgeWikitext .. ' › <span class="plainlinks">[' .. typeUrl .. ' ' .. crt_type .. ']</span>' | |||
end | end | ||
addFullRow(container, badgeWikitext, '', false, 'infobox-type-badge') | addFullRow(container, badgeWikitext, '', false, 'infobox-type-badge') | ||
| Line 413: | Line 413: | ||
if chassis_family then | if chassis_family then | ||
-- Link directly to the chassis family page (no namespace prefix) | |||
local familyWikitext = string.format( | local familyWikitext = string.format( | ||
'{{#formredlink: target=%s |form=Chassis family |existing page link text=%s }}', | '{{#formredlink: target=%s |form=Chassis family |existing page link text=%s }}', | ||
| Line 420: | Line 421: | ||
if chassis then | if chassis then | ||
-- Both: show chassis with family in parentheses on next line | |||
chassisDisplay = chassisDisplay .. '<br><small>Family: ' .. familyHtml .. '</small>' | chassisDisplay = chassisDisplay .. '<br><small>Family: ' .. familyHtml .. '</small>' | ||
else | else | ||
-- Only family: show family alone | |||
chassisDisplay = familyHtml .. ' <small>(family)</small>' | chassisDisplay = familyHtml .. ' <small>(family)</small>' | ||
end | end | ||
| Line 440: | Line 443: | ||
end | end | ||
-- ECID row | -- ECID row: build as a normal external link wikitext; preprocess not required, but fine. | ||
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) | ||
-- Tube section | -- Tube section: show if we have any tube specs | ||
if mask_type or pitch or phosphor_type or gun_type or deflection_angle or neck_diameter or visible_w or visible_h or visible_diag then | if mask_type or pitch or phosphor_type or gun_type or deflection_angle or neck_diameter or visible_w or visible_h or visible_diag then | ||
addSectionRow(container, 'Tube') | addSectionRow(container, 'Tube') | ||
if mask_type then addRow(container, 'Mask', mask_type, false) end | |||
if pitch then addRow(container, 'Pitch', pitch .. ' mm', false) end | if mask_type then | ||
if phosphor_type then addRow(container, 'Phosphors', phosphor_type, false) end | addRow(container, 'Mask', mask_type, false) | ||
if gun_type then addRow(container, 'Gun type', gun_type, false) end | end | ||
if deflection_angle then addRow(container, 'Deflection', deflection_angle .. '°', false) end | |||
if neck_diameter then addRow(container, 'Neck', neck_diameter .. ' mm', false) end | if pitch then | ||
addRow(container, 'Pitch', pitch .. ' mm', false) | |||
end | |||
if phosphor_type then | |||
addRow(container, 'Phosphors', phosphor_type, false) | |||
end | |||
if gun_type then | |||
addRow(container, 'Gun type', gun_type, false) | |||
end | |||
if deflection_angle then | |||
addRow(container, 'Deflection', deflection_angle .. '°', false) | |||
end | |||
if neck_diameter then | |||
addRow(container, 'Neck', neck_diameter .. ' mm', false) | |||
end | |||
-- Visible picture size | |||
if visible_w and visible_h then | if visible_w and visible_h then | ||
addRow(container, 'Visible area', visible_w .. ' × ' .. visible_h .. ' mm', false) | addRow(container, 'Visible area', visible_w .. ' × ' .. visible_h .. ' mm', false) | ||
| Line 458: | Line 481: | ||
addRow(container, 'Visible area', visible_diag .. '″ (diagonal)', false) | addRow(container, 'Visible area', visible_diag .. '″ (diagonal)', false) | ||
end | end | ||
-- Show diagonal even if we have W×H, as supplementary info | |||
if visible_diag and visible_w and visible_h then | if visible_diag and visible_w and visible_h then | ||
addRow(container, 'Visible diag.', visible_diag .. '″', false) | addRow(container, 'Visible diag.', visible_diag .. '″', false) | ||
| Line 463: | Line 488: | ||
end | end | ||
-- Dimensions section | -- Dimensions section: show if we have height/width/depth OR weight | ||
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 | ||
local h = height or '–' | local h = height or '–' | ||
| Line 472: | Line 498: | ||
addRow(container, 'H × W × D', string.format('%s × %s × %s', h, w, d), false) | addRow(container, 'H × W × D', string.format('%s × %s × %s', h, w, d), false) | ||
end | end | ||
-- Weight row: uses #show, so preprocess it | |||
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 478: | Line 506: | ||
end | end | ||
-- Speakers section | -- Speakers section: show if we have any speaker specs | ||
if speaker_type or speaker_watts or subwoofer then | if speaker_type or speaker_watts or subwoofer then | ||
addSectionRow(container, 'Speakers') | addSectionRow(container, 'Speakers') | ||
if speaker_type then addRow(container, 'Type', speaker_type, false) end | |||
if speaker_watts then addRow(container, 'Power', speaker_watts, false) end | if speaker_type then | ||
if subwoofer then addRow(container, 'Subwoofer', subwoofer, false) end | addRow(container, 'Type', speaker_type, false) | ||
end | |||
if speaker_watts then | |||
addRow(container, 'Power', speaker_watts, false) | |||
end | |||
if subwoofer then | |||
addRow(container, 'Subwoofer', subwoofer, false) | |||
end | |||
end | end | ||
-- Power section | -- Power section: query voltage ranges from subobjects | ||
-- First, query for voltage range subobjects | |||
local voltageQueryWikitext = | local voltageQueryWikitext = | ||
'{{#ask:\n' .. | '{{#ask:\n' .. | ||
| Line 500: | Line 538: | ||
'}}' | '}}' | ||
local voltageResult = frame:preprocess(voltageQueryWikitext) | local voltageResult = frame:preprocess(voltageQueryWikitext) | ||
-- Parse voltage ranges from broadtable result | |||
local voltageRanges = {} | local voltageRanges = {} | ||
if voltageResult then | if voltageResult then | ||
| Line 508: | Line 548: | ||
table.insert(cells, trim(cleanContent)) | table.insert(cells, trim(cleanContent)) | ||
end | end | ||
-- cells[1] = subobject, cells[2] = voltage_min, cells[3] = voltage_max | |||
if #cells >= 3 then | if #cells >= 3 then | ||
local vMin = tonumber(cells[2]) | local vMin = tonumber(cells[2]) | ||
| Line 517: | Line 559: | ||
end | end | ||
end | end | ||
local hasVoltageRanges = #voltageRanges > 0 | local hasVoltageRanges = #voltageRanges > 0 | ||
-- Show Power section if we have voltage ranges, frequency, or power consumption | |||
if hasVoltageRanges or frequency or power_avg or power_max then | if hasVoltageRanges or frequency or power_avg or power_max then | ||
addSectionRow(container, 'Power') | addSectionRow(container, 'Power') | ||
-- Voltage row: display ranges from subobjects | |||
if hasVoltageRanges then | if hasVoltageRanges then | ||
local rangeStrings = {} | local rangeStrings = {} | ||
| Line 536: | Line 582: | ||
rangeStr = range.max .. 'V' | rangeStr = range.max .. 'V' | ||
end | end | ||
if rangeStr then table.insert(rangeStrings, rangeStr) end | if rangeStr then | ||
table.insert(rangeStrings, rangeStr) | |||
end | |||
end | end | ||
if #rangeStrings > 0 then | if #rangeStrings > 0 then | ||
addRow(container, 'Voltage', table.concat(rangeStrings, ' / '), false) | addRow(container, 'Voltage', table.concat(rangeStrings, ' / '), false) | ||
end | end | ||
end | end | ||
if frequency then addRow(container, 'Frequency', frequency, false) end | |||
-- Frequency row | |||
if frequency then | |||
addRow(container, 'Frequency', frequency, false) | |||
end | |||
-- Power consumption row | |||
if power_avg or power_max then | if power_avg or power_max then | ||
local powerDisplay | local powerDisplay | ||
| Line 556: | Line 611: | ||
end | end | ||
-- TV Systems section | -- TV Systems section: show if we have AV system or tuner system | ||
if av_system or tuner_system then | if av_system or tuner_system then | ||
addSectionRow(container, 'TV Systems') | addSectionRow(container, 'TV Systems') | ||
if av_system then | if av_system then | ||
local systems = {} | local systems = {} | ||
for system in av_system:gmatch('[^,]+') do | for system in av_system:gmatch('[^,]+') do | ||
local s = trim(system) | local s = trim(system) | ||
if s then table.insert(systems, '[[' .. s .. '|' .. s .. ']]') end | if s then | ||
table.insert(systems, '[[' .. s .. '|' .. s .. ']]') | |||
end | |||
end | |||
if #systems > 0 then | |||
addRow(container, 'AV system', table.concat(systems, ', '), true) | |||
end | end | ||
end | end | ||
if tuner_system then | if tuner_system then | ||
local systems = {} | local systems = {} | ||
for system in tuner_system:gmatch('[^,]+') do | for system in tuner_system:gmatch('[^,]+') do | ||
local s = trim(system) | local s = trim(system) | ||
if s then table.insert(systems, '[[' .. s .. '|' .. s .. ']]') end | if s then | ||
table.insert(systems, '[[' .. s .. '|' .. s .. ']]') | |||
end | |||
end | |||
if #systems > 0 then | |||
addRow(container, 'Tuner', table.concat(systems, ', '), true) | |||
end | end | ||
end | end | ||
end | end | ||
| Line 580: | Line 645: | ||
if h_scan_min or h_scan_max or v_scan_min or v_scan_max then | if h_scan_min or h_scan_max or v_scan_min or v_scan_max then | ||
addSectionRow(container, 'Scan Range') | addSectionRow(container, 'Scan Range') | ||
if h_scan_min or h_scan_max then | if h_scan_min or h_scan_max then | ||
local hDisplay | local hDisplay | ||
if h_scan_min and h_scan_max then | if h_scan_min and h_scan_max then | ||
if h_scan_min == h_scan_max then | |||
hDisplay = h_scan_min .. ' kHz' | |||
else | |||
hDisplay = h_scan_min .. '–' .. h_scan_max .. ' kHz' | |||
end | |||
elseif h_scan_min then | |||
hDisplay = h_scan_min .. ' kHz' | |||
else | else | ||
hDisplay = | hDisplay = h_scan_max .. ' kHz' | ||
end | end | ||
addRow(container, 'Horizontal', hDisplay, false) | addRow(container, 'Horizontal', hDisplay, false) | ||
end | end | ||
if v_scan_min or v_scan_max then | if v_scan_min or v_scan_max then | ||
local vDisplay | local vDisplay | ||
if v_scan_min and v_scan_max then | if v_scan_min and v_scan_max then | ||
if v_scan_min == v_scan_max then | |||
vDisplay = v_scan_min .. ' Hz' | |||
else | |||
vDisplay = v_scan_min .. '–' .. v_scan_max .. ' Hz' | |||
end | |||
elseif v_scan_min then | |||
vDisplay = v_scan_min .. ' Hz' | |||
else | else | ||
vDisplay = | vDisplay = v_scan_max .. ' Hz' | ||
end | end | ||
addRow(container, 'Vertical', vDisplay, false) | addRow(container, 'Vertical', vDisplay, false) | ||
| Line 604: | Line 683: | ||
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 610: | Line 690: | ||
end | end | ||
end | end | ||
if #locationData > 0 then | if #locationData > 0 then | ||
addSectionRow(container, sectionTitle) | addSectionRow(container, sectionTitle) | ||
| Line 617: | Line 698: | ||
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 631: | Line 713: | ||
-- Accessories section | -- Accessories section | ||
addSectionRow(container, 'Accessories') | addSectionRow(container, 'Accessories') | ||
local addAccessoryUrl = mw.uri.fullUrl(" | |||
local addAccessoryUrl = mw.uri.fullUrl( | |||
"Create an accessory", | |||
{ | |||
crt_models = pageName | |||
} | |||
) | |||
local accessoriesCountWikitext = | local accessoriesCountWikitext = | ||
'{{#ask:\n [[Category:Accessories]]\n [[Has related CRT model::' .. fullPageName .. ']]\n [[Has accessory type::!Remote control]]\n |format=count\n}}' | '{{#ask:\n' .. | ||
' [[Category:Accessories]]\n' .. | |||
' [[Has related CRT model::' .. fullPageName .. ']]\n' .. | |||
' [[Has accessory type::!Remote control]]\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 | ||
| Line 642: | Line 736: | ||
if hasAccessories then | if hasAccessories then | ||
local accessoriesQueryWikitext = | local accessoriesQueryWikitext = | ||
'{{#ask:\n [[Category:Accessories]]\n [[Has related CRT model::' .. fullPageName .. ']]\n [[Has accessory type::!Remote control]]\n |?Has accessory type=type\n |?Has model code=model\n |format=broadtable\n |headers=hide\n |link=none\n}}' | '{{#ask:\n' .. | ||
' [[Category:Accessories]]\n' .. | |||
' [[Has related CRT model::' .. fullPageName .. ']]\n' .. | |||
' [[Has accessory type::!Remote control]]\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) | ||
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 | ||
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 660: | Line 768: | ||
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 | ||
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;') | ||
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 | ||
table.insert(accLinks, '[[' .. acc.page .. ']]') | |||
end | |||
local labelDiv = accGrid:tag('div') | local labelDiv = accGrid:tag('div') | ||
labelDiv:addClass('infobox-label') | labelDiv:addClass('infobox-label') | ||
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(accType) | labelDiv:wikitext(accType) | ||
local valueDiv = accGrid:tag('div') | local valueDiv = accGrid:tag('div') | ||
valueDiv:addClass('infobox-value') | valueDiv:addClass('infobox-value') | ||
| Line 682: | Line 799: | ||
end | end | ||
end | end | ||
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 695: | Line 813: | ||
-- Parts section | -- Parts section | ||
addSectionRow(container, 'Parts') | addSectionRow(container, 'Parts') | ||
local addPartUrl = mw.uri.fullUrl(" | |||
local addPartUrl = mw.uri.fullUrl( | |||
"Create a part", | |||
{ | |||
crt_models = pageName | |||
} | |||
) | |||
local partsCountWikitext = | local partsCountWikitext = | ||
'{{#ask:\n [[Category:Parts]]\n [[Has related CRT model::' .. fullPageName .. ']]\n |format=count\n}}' | '{{#ask:\n' .. | ||
' [[Category:Parts]]\n' .. | |||
' [[Has related CRT model::' .. 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 = | local partsQueryWikitext = | ||
'{{#ask:\n [[Category:Parts]]\n [[Has related CRT model::' .. 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}}' | '{{#ask:\n' .. | ||
' [[Category:Parts]]\n' .. | |||
' [[Has related CRT model::' .. 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) | ||
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 | ||
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] | ||
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 | ||
if not partsByType[groupKey] then | if not partsByType[groupKey] then | ||
| Line 726: | Line 873: | ||
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 | ||
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;') | ||
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 | ||
table.insert(partLinks, '[[' .. part.page .. ']]') | |||
end | |||
local labelDiv = partsGrid:tag('div') | local labelDiv = partsGrid:tag('div') | ||
labelDiv:addClass('infobox-label') | labelDiv:addClass('infobox-label') | ||
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(partType) | labelDiv:wikitext(partType) | ||
local valueDiv = partsGrid:tag('div') | local valueDiv = partsGrid:tag('div') | ||
valueDiv:addClass('infobox-value') | valueDiv:addClass('infobox-value') | ||
| Line 748: | Line 904: | ||
end | end | ||
end | end | ||
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 761: | Line 918: | ||
-- Mods section | -- Mods section | ||
local modsQueryWikitext = | local modsQueryWikitext = | ||
'{{#ask:\n [[Has CRT model::' .. fullPageName .. ']]\n [[Has mod URL::+]]\n |?Has mod type=type\n |?Has mod description=description\n |?Has mod feasible=feasible\n |?Has mod URL=url\n |format=broadtable\n |headers=hide\n |link=none\n}}' | '{{#ask:\n' .. | ||
' [[Has CRT model::' .. fullPageName .. ']]\n' .. | |||
' [[Has mod URL::+]]\n' .. | |||
' |?Has mod type=type\n' .. | |||
' |?Has mod description=description\n' .. | |||
' |?Has mod feasible=feasible\n' .. | |||
' |?Has mod URL=url\n' .. | |||
' |format=broadtable\n' .. | |||
' |headers=hide\n' .. | |||
' |link=none\n' .. | |||
'}}' | |||
local modsResult = frame:preprocess(modsQueryWikitext) | local modsResult = frame:preprocess(modsQueryWikitext) | ||
local mods = {} | local mods = {} | ||
if modsResult then | if modsResult then | ||
for rowContent in modsResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in modsResult: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 | ||
if #cells >= 3 then | if #cells >= 3 then | ||
local modData = {type = nil, description = nil, feasible = nil, url = nil} | local modData = { | ||
type = nil, | |||
description = nil, | |||
feasible = nil, | |||
url = nil | |||
} | |||
for i = 2, #cells do | for i = 2, #cells do | ||
local cell = cells[i] | local cell = cells[i] | ||
if cell then | if cell then | ||
if cell:match('^https?://') then modData.url = cell | if cell:match('^https?://') then | ||
elseif cell == 'Yes' or cell == 'No' then modData.feasible = cell | modData.url = cell | ||
elseif cell == 'RGB' or cell == 'S-video' or cell == 'Other' then modData.type = cell | elseif cell == 'Yes' or cell == 'No' then | ||
else modData.description = cell end | modData.feasible = cell | ||
elseif cell == 'RGB' or cell == 'S-video' or cell == 'Other' then | |||
modData.type = cell | |||
else | |||
modData.description = cell | |||
end | |||
end | end | ||
end | end | ||
if modData.url and modData.type then table.insert(mods, modData) end | |||
if modData.url and modData.type then | |||
table.insert(mods, modData) | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
if #mods > 0 then | if #mods > 0 then | ||
addSectionRow(container, 'Mods') | addSectionRow(container, 'Mods') | ||
local modsDiv = container:tag('div') | local modsDiv = container:tag('div') | ||
css(modsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | css(modsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | ||
local ul = modsDiv:tag('ul') | local ul = modsDiv:tag('ul') | ||
ul:css('margin', '0'):css('padding-left', '1.5em') | ul:css('margin', '0'):css('padding-left', '1.5em') | ||
for _, mod in ipairs(mods) do | for _, mod in ipairs(mods) do | ||
local li = ul:tag('li') | local li = ul:tag('li') | ||
local displayText = mod.description or mod.type | local displayText = mod.description or mod.type | ||
local isFeasible = mod.feasible ~= 'No' | local isFeasible = mod.feasible ~= 'No' | ||
if isFeasible then | if isFeasible then | ||
li:wikitext('[' .. mod.url .. ' ' .. displayText .. ']') | li:wikitext('[' .. mod.url .. ' ' .. displayText .. ']') | ||
| Line 810: | Line 1,001: | ||
-- 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[crt_models]"] = pageName}) | local uploadUrl = mw.uri.fullUrl( | ||
"Special:FormEdit", | |||
{ | |||
form = "Document", | |||
target = target, | |||
["Document[crt_models]"] = pageName | |||
} | |||
) | |||
local documentsCountWikitext = | local documentsCountWikitext = | ||
'{{#ask:\n [[Category:Documents]]\n [[Has related CRT model::' .. fullPageName .. ']]\n |format=count\n}}' | '{{#ask:\n' .. | ||
' [[Category:Documents]]\n' .. | |||
' [[Has related CRT model::' .. 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 = | local docsQueryWikitext = | ||
'{{#ask:\n [[Category:Documents]]\n [[Has related CRT model::' .. 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}}' | '{{#ask:\n' .. | ||
' [[Category:Documents]]\n' .. | |||
' [[Has related CRT model::' .. 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]) | ||
| Line 839: | Line 1,058: | ||
local docFile = trim(cells[4]) | local docFile = trim(cells[4]) | ||
local docTitle = trim(cells[5]) | local docTitle = trim(cells[5]) | ||
local linkTarget = nil | |||
if docUrl then | |||
linkTarget = docUrl | |||
elseif docFile then | |||
local filename = docFile:match('File:(.+)') or docFile | |||
linkTarget = tostring(mw.uri.fullUrl('File:' .. filename)) | |||
end | |||
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 | ||
elseif docType then | |||
displayText = docType | |||
end | |||
local li = ul:tag('li') | local li = ul:tag('li') | ||
li:wikitext('[[' .. docPageName .. '|' .. displayText .. ']]') | li:wikitext('[[' .. docPageName .. '|' .. displayText .. ']]') | ||
| Line 850: | Line 1,083: | ||
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 859: | Line 1,095: | ||
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 864: | Line 1,101: | ||
-- Links section | -- Links section | ||
local linksQueryWikitext = | local linksQueryWikitext = | ||
'{{#ask:\n [[CRT model::' .. fullPageName .. ']]\n |?Model link type\n |?Model link source\n |?Model link URL\n |format=broadtable\n |headers=hide\n |link=none\n}}' | '{{#ask:\n' .. | ||
' [[CRT model::' .. 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 = {}} | |||
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 | ||
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'} | local linkTypeOrder = {'Database', 'Manufacturer', 'Retailer'} | ||
for _, linkType in ipairs(linkTypeOrder) do | for _, linkType in ipairs(linkTypeOrder) do | ||
| Line 901: | Line 1,159: | ||
-- Contribute section | -- Contribute section | ||
addSectionRow(container, 'Contribute') | addSectionRow(container, 'Contribute') | ||
local editUrl = mw.uri.fullUrl("Special:FormStart", {form = "CRT model", page_name = pageName}) | |||
local editUrl = mw.uri.fullUrl( | |||
"Special:FormStart", | |||
{ | |||
form = "CRT model", | |||
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;') | ||
editDiv:wikitext('[' .. tostring(editUrl) .. ' <span class="infobox-button infobox-button-primary">Edit this data</span>]') | editDiv:wikitext('[' .. tostring(editUrl) .. ' <span class="infobox-button infobox-button-primary">Edit this data</span>]') | ||
Latest revision as of 04:37, 19 February 2026
Documentation for this module may be created at Module:CRTModel/doc
-- Module:CRTModel
-- Builds the CRT infobox as strict HTML to avoid whitespace -> <p><br></p>
-- 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 extract model code from "Brand Model" format
-- If the accessory starts with the same brand as the CRT, strip it for cleaner display
local function getDisplayTextWithoutBrand(itemName, deviceBrand)
if not itemName then return nil end
if not deviceBrand then return itemName end
-- Check if item name starts with the device brand followed by a space
local brandPrefix = deviceBrand .. ' '
if itemName:sub(1, #brandPrefix) == brandPrefix then
-- Return just the model code portion
return itemName:sub(#brandPrefix + 1)
end
-- Otherwise return the full name
return itemName
end
-- Helper to query inputs/outputs/control ports for a specific location
local function queryIOByLocation(frame, fullPageName, ioType, location)
local locationProp
if ioType == 'input' then
locationProp = 'Has input location'
elseif ioType == 'output' then
locationProp = 'Has output location'
else -- control
locationProp = 'Has control port location'
end
local locationFilter = ''
if location then
locationFilter = ' [[' .. locationProp .. '::' .. location .. ']]\n'
end
local signalProp, countProp, connectorProp, notesProp, labelProp, templateName
local extraProps = ''
if ioType == 'input' then
signalProp = 'Has video signal format'
countProp = 'Has input count'
connectorProp = 'Has connector type'
notesProp = 'Has input notes'
labelProp = 'Has input label'
templateName = 'CRT input row'
extraProps = ' |?Has audio type=audio\n' ..
' |?Has audio connector=audio_connector\n' ..
' |?Has audio shared with=audio_shared\n'
elseif ioType == 'output' then
signalProp = 'Has output signal format'
countProp = 'Has output count'
connectorProp = 'Has output connector'
notesProp = 'Has output notes'
labelProp = 'Has output label'
templateName = 'CRT output row'
else -- control
signalProp = 'Has control signal format'
countProp = 'Has control port count'
connectorProp = 'Has control port connector'
notesProp = 'Has control port notes'
labelProp = 'Has control port label'
templateName = 'CRT control port row'
extraProps = ' |?Has control port direction=direction\n'
end
local queryWikitext =
'{{#ask:\n' ..
' [[Has CRT model::' .. fullPageName .. ']]\n' ..
' [[' .. signalProp .. '::+]]\n' ..
locationFilter ..
' |?' .. countProp .. '=count\n' ..
' |?' .. signalProp .. '=signal\n' ..
' |?' .. connectorProp .. '=connector\n' ..
' |?' .. labelProp .. '=label\n' ..
' |?' .. notesProp .. '=notes\n' ..
extraProps ..
' |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'
elseif ioType == 'output' then
locationProp = 'Has output location'
signalProp = 'Has output signal format'
else -- control
locationProp = 'Has control port location'
signalProp = 'Has control signal format'
end
local queryWikitext =
'{{#ask:\n' ..
' [[Has CRT model::' .. 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
local brand = trim(args.brand)
local model_code = trim(args.model_code)
local variants = trim(args.variants)
local market = trim(args.market)
local series = trim(args.series)
local image_main = trim(args.image_main)
local model_3d = trim(args.model_3d)
local crt_type = trim(args.crt_type)
local form_factor = trim(args.form_factor)
local built_in_devices = trim(args.built_in_devices)
local screen_size_in = trim(args.screen_size_in)
local aspect_ratio = trim(args.aspect_ratio)
local tvl = trim(args.tvl)
local chassis = trim(args.chassis)
local chassis_family = trim(args.chassis_family)
local remote_control = trim(args.remote_control)
local height = trim(args.height)
local width = trim(args.width)
local depth = trim(args.depth)
local weight = trim(args.weight)
-- Power specifications (frequency and power consumption still from template args)
local frequency = trim(args.frequency)
local power_avg = trim(args.power_avg)
local power_max = trim(args.power_max)
-- TV system support
local av_system = trim(args.av_system)
local tuner_system = trim(args.tuner_system)
-- Scan range
local h_scan_min = trim(args.h_scan_min)
local h_scan_max = trim(args.h_scan_max)
local v_scan_min = trim(args.v_scan_min)
local v_scan_max = trim(args.v_scan_max)
-- Speakers
local speaker_type = trim(args.speaker_type)
local speaker_watts = trim(args.speaker_watts)
local subwoofer = trim(args.subwoofer)
-- Release year and MSRP
local release_year = trim(args.release_year)
local msrp = trim(args.msrp)
-- Tube specifications
local mask_type = trim(args.mask_type)
local pitch = trim(args.pitch)
local phosphor_type = trim(args.phosphor_type)
local gun_type = trim(args.gun_type)
local deflection_angle = trim(args.deflection_angle)
local neck_diameter = trim(args.neck_diameter)
local visible_w = trim(args.visible_w)
local visible_h = trim(args.visible_h)
local visible_diag = trim(args.visible_diag)
-- Use frame:extensionTag to create a proper <style> tag that won't be escaped
local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template:Infobox styles.css'})
local container = mw.html.create('div')
container:addClass('infobox')
container:addClass('infobox-crt-model')
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 (no background color, centered, bold)
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 = '[[CRT Search|CRT]]'
if crt_type then
local typeUrl = mw.title.new('CRT Search'):fullUrl({crt_type = crt_type})
badgeWikitext = badgeWikitext .. ' › <span class="plainlinks">[' .. typeUrl .. ' ' .. crt_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
-- 3D model display (right below main photo)
if model_3d then
local file = model_3d:gsub('^File:', '')
addFullRow(
container,
string.format('[[File:%s]]', file),
'text-align:center; padding:0;',
false,
'infobox-3d-model'
)
end
addSectionRow(container, 'Overview')
-- Brand row using #formredlink (must preprocess)
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
if model_code then
addRow(container, 'Model', model_code, false)
else
addRow(container, 'Model', '', false)
end
-- Regional variants row
if variants then
addRow(container, 'Variants', variants, false)
end
-- Market row
if market then
addRow(container, 'Market', market, false)
end
if series then
local seriesWikitext = string.format(
'{{#formredlink: target=%s |form=Series |existing page link text=%s }}',
series, series
)
local seriesHtml = frame:preprocess(seriesWikitext)
addRow(container, 'Series', seriesHtml, true)
end
-- Release year row
if release_year then
addRow(container, 'Released', release_year, false)
end
-- MSRP row
if msrp then
addRow(container, 'MSRP', msrp, false)
end
if crt_type then
addRow(container, 'Type', crt_type, false)
end
-- Form factor row (only show if not "Standard")
if form_factor and form_factor ~= 'Standard' then
addRow(container, 'Form factor', form_factor, false)
end
-- Built-in devices row
if built_in_devices then
addRow(container, 'Built-in', built_in_devices, false)
end
if screen_size_in then
addRow(container, 'Screen size', screen_size_in .. '″', false)
end
if aspect_ratio then
addRow(container, 'Aspect ratio', aspect_ratio, false)
end
if tvl then
addRow(container, 'TVL', tvl, false)
end
-- Chassis row: display both label chassis and family if available
if chassis or chassis_family then
local chassisDisplay = ''
if chassis then
local chassisWikitext = string.format(
'{{#formredlink: target=%s |form=Chassis |existing page link text=%s }}',
chassis, chassis
)
chassisDisplay = frame:preprocess(chassisWikitext)
end
if chassis_family then
-- Link directly to the chassis family page (no namespace prefix)
local familyWikitext = string.format(
'{{#formredlink: target=%s |form=Chassis family |existing page link text=%s }}',
chassis_family, chassis_family
)
local familyHtml = frame:preprocess(familyWikitext)
if chassis then
-- Both: show chassis with family in parentheses on next line
chassisDisplay = chassisDisplay .. '<br><small>Family: ' .. familyHtml .. '</small>'
else
-- Only family: show family alone
chassisDisplay = familyHtml .. ' <small>(family)</small>'
end
end
addRow(container, 'Chassis', chassisDisplay, true)
end
-- Remote control row: use Accessory form, strip brand prefix if it matches CRT brand
if remote_control then
local displayText = getDisplayTextWithoutBrand(remote_control, brand)
local remoteWikitext = string.format(
'{{#formredlink: target=%s |form=Accessory |existing page link text=%s }}',
remote_control, displayText
)
local remoteHtml = frame:preprocess(remoteWikitext)
addRow(container, 'Remote', remoteHtml, true)
end
-- ECID row: build as a normal external link wikitext; preprocess not required, but fine.
local ecidWikitext = string.format('[{{fullurl:%s|curid=%d}} EC%d]', pageName, pageId, pageId)
addRow(container, 'ECID', frame:preprocess(ecidWikitext), true)
-- Tube section: show if we have any tube specs
if mask_type or pitch or phosphor_type or gun_type or deflection_angle or neck_diameter or visible_w or visible_h or visible_diag then
addSectionRow(container, 'Tube')
if mask_type then
addRow(container, 'Mask', mask_type, false)
end
if pitch then
addRow(container, 'Pitch', pitch .. ' mm', false)
end
if phosphor_type then
addRow(container, 'Phosphors', phosphor_type, false)
end
if gun_type then
addRow(container, 'Gun type', gun_type, false)
end
if deflection_angle then
addRow(container, 'Deflection', deflection_angle .. '°', false)
end
if neck_diameter then
addRow(container, 'Neck', neck_diameter .. ' mm', false)
end
-- Visible picture size
if visible_w and visible_h then
addRow(container, 'Visible area', visible_w .. ' × ' .. visible_h .. ' mm', false)
elseif visible_diag then
addRow(container, 'Visible area', visible_diag .. '″ (diagonal)', false)
end
-- Show diagonal even if we have W×H, as supplementary info
if visible_diag and visible_w and visible_h then
addRow(container, 'Visible diag.', visible_diag .. '″', false)
end
end
-- Dimensions section: show if we have height/width/depth OR weight
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
-- Weight row: uses #show, so preprocess it
if weight then
local weightWikitext = '{{#show: ' .. fullPageName .. ' |?Weight # lb }} / {{#show: ' .. fullPageName .. ' |?Weight # kg }}'
addRow(container, 'Weight', frame:preprocess(weightWikitext), true)
end
end
-- Speakers section: show if we have any speaker specs
if speaker_type or speaker_watts or subwoofer then
addSectionRow(container, 'Speakers')
if speaker_type then
addRow(container, 'Type', speaker_type, false)
end
if speaker_watts then
addRow(container, 'Power', speaker_watts, false)
end
if subwoofer then
addRow(container, 'Subwoofer', subwoofer, false)
end
end
-- Power section: query voltage ranges from subobjects
-- First, query for voltage range subobjects
local voltageQueryWikitext =
'{{#ask:\n' ..
' [[Has CRT model::' .. fullPageName .. ']]\n' ..
' [[Voltage min::+]]\n' ..
' |?Voltage min\n' ..
' |?Voltage max\n' ..
' |format=broadtable\n' ..
' |headers=hide\n' ..
' |link=none\n' ..
' |sort=Voltage min\n' ..
' |order=asc\n' ..
'}}'
local voltageResult = frame:preprocess(voltageQueryWikitext)
-- Parse voltage ranges from broadtable result
local voltageRanges = {}
if voltageResult then
for rowContent in voltageResult: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, cells[2] = voltage_min, cells[3] = voltage_max
if #cells >= 3 then
local vMin = tonumber(cells[2])
local vMax = tonumber(cells[3])
if vMin or vMax then
table.insert(voltageRanges, {min = vMin, max = vMax})
end
end
end
end
local hasVoltageRanges = #voltageRanges > 0
-- Show Power section if we have voltage ranges, frequency, or power consumption
if hasVoltageRanges or frequency or power_avg or power_max then
addSectionRow(container, 'Power')
-- Voltage row: display ranges from subobjects
if hasVoltageRanges then
local rangeStrings = {}
for _, range in ipairs(voltageRanges) do
local rangeStr
if range.min and range.max then
if range.min == range.max then
rangeStr = range.min .. 'V'
else
rangeStr = range.min .. '–' .. range.max .. 'V'
end
elseif range.min then
rangeStr = range.min .. 'V'
elseif range.max then
rangeStr = range.max .. 'V'
end
if rangeStr then
table.insert(rangeStrings, rangeStr)
end
end
if #rangeStrings > 0 then
addRow(container, 'Voltage', table.concat(rangeStrings, ' / '), false)
end
end
-- Frequency row
if frequency then
addRow(container, 'Frequency', frequency, false)
end
-- Power consumption row
if power_avg or power_max then
local powerDisplay
if power_avg and power_max then
powerDisplay = power_avg .. 'W / ' .. power_max .. 'W<br><small>(avg / max)</small>'
elseif power_avg then
powerDisplay = power_avg .. 'W <small>(avg)</small>'
else
powerDisplay = power_max .. 'W <small>(max)</small>'
end
addRow(container, 'Consumption', powerDisplay, true)
end
end
-- TV Systems section: show if we have AV system or tuner system
if av_system or tuner_system then
addSectionRow(container, 'TV Systems')
if av_system then
local systems = {}
for system in av_system:gmatch('[^,]+') do
local s = trim(system)
if s then
table.insert(systems, '[[' .. s .. '|' .. s .. ']]')
end
end
if #systems > 0 then
addRow(container, 'AV system', table.concat(systems, ', '), true)
end
end
if tuner_system then
local systems = {}
for system in tuner_system:gmatch('[^,]+') do
local s = trim(system)
if s then
table.insert(systems, '[[' .. s .. '|' .. s .. ']]')
end
end
if #systems > 0 then
addRow(container, 'Tuner', table.concat(systems, ', '), true)
end
end
end
-- Scan Range section
if h_scan_min or h_scan_max or v_scan_min or v_scan_max then
addSectionRow(container, 'Scan Range')
if h_scan_min or h_scan_max then
local hDisplay
if h_scan_min and h_scan_max then
if h_scan_min == h_scan_max then
hDisplay = h_scan_min .. ' kHz'
else
hDisplay = h_scan_min .. '–' .. h_scan_max .. ' kHz'
end
elseif h_scan_min then
hDisplay = h_scan_min .. ' kHz'
else
hDisplay = h_scan_max .. ' kHz'
end
addRow(container, 'Horizontal', hDisplay, false)
end
if v_scan_min or v_scan_max then
local vDisplay
if v_scan_min and v_scan_max then
if v_scan_min == v_scan_max then
vDisplay = v_scan_min .. ' Hz'
else
vDisplay = v_scan_min .. '–' .. v_scan_max .. ' Hz'
end
elseif v_scan_min then
vDisplay = v_scan_min .. ' Hz'
else
vDisplay = v_scan_max .. ' Hz'
end
addRow(container, 'Vertical', vDisplay, false)
end
end
-- Inputs, Outputs, and Control Ports sections
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')
renderIOSection('Control Ports', 'control')
-- Accessories section
addSectionRow(container, 'Accessories')
local addAccessoryUrl = mw.uri.fullUrl(
"Create an accessory",
{
crt_models = pageName
}
)
local accessoriesCountWikitext =
'{{#ask:\n' ..
' [[Category:Accessories]]\n' ..
' [[Has related CRT model::' .. fullPageName .. ']]\n' ..
' [[Has accessory type::!Remote control]]\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
local accessoriesQueryWikitext =
'{{#ask:\n' ..
' [[Category:Accessories]]\n' ..
' [[Has related CRT model::' .. fullPageName .. ']]\n' ..
' [[Has accessory type::!Remote control]]\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 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
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
local accGrid = accDiv:tag('div')
css(accGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;')
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
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
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')
local addPartUrl = mw.uri.fullUrl(
"Create a part",
{
crt_models = pageName
}
)
local partsCountWikitext =
'{{#ask:\n' ..
' [[Category:Parts]]\n' ..
' [[Has related CRT model::' .. 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
local partsQueryWikitext =
'{{#ask:\n' ..
' [[Category:Parts]]\n' ..
' [[Has related CRT model::' .. 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 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
if #cells >= 2 then
local partPageName = cells[1]
local partType = cells[2] or 'Part'
local partSubtype = cells[3]
local groupKey = partType
if partSubtype and partSubtype ~= '' then
groupKey = partSubtype .. ' ' .. partType
end
if partPageName then
if not partsByType[groupKey] then
partsByType[groupKey] = {}
table.insert(partTypeOrder, groupKey)
end
table.insert(partsByType[groupKey], {
page = partPageName
})
end
end
end
end
local partsGrid = partsDiv:tag('div')
css(partsGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;')
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
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
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
-- Mods section
local modsQueryWikitext =
'{{#ask:\n' ..
' [[Has CRT model::' .. fullPageName .. ']]\n' ..
' [[Has mod URL::+]]\n' ..
' |?Has mod type=type\n' ..
' |?Has mod description=description\n' ..
' |?Has mod feasible=feasible\n' ..
' |?Has mod URL=url\n' ..
' |format=broadtable\n' ..
' |headers=hide\n' ..
' |link=none\n' ..
'}}'
local modsResult = frame:preprocess(modsQueryWikitext)
local mods = {}
if modsResult then
for rowContent in modsResult:gmatch('<tr[^>]*>(.-)</tr>') do
local cells = {}
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
local cleanContent = cellContent:gsub('<[^>]+>', '')
table.insert(cells, trim(cleanContent))
end
if #cells >= 3 then
local modData = {
type = nil,
description = nil,
feasible = nil,
url = nil
}
for i = 2, #cells do
local cell = cells[i]
if cell then
if cell:match('^https?://') then
modData.url = cell
elseif cell == 'Yes' or cell == 'No' then
modData.feasible = cell
elseif cell == 'RGB' or cell == 'S-video' or cell == 'Other' then
modData.type = cell
else
modData.description = cell
end
end
end
if modData.url and modData.type then
table.insert(mods, modData)
end
end
end
end
if #mods > 0 then
addSectionRow(container, 'Mods')
local modsDiv = container:tag('div')
css(modsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')
local ul = modsDiv:tag('ul')
ul:css('margin', '0'):css('padding-left', '1.5em')
for _, mod in ipairs(mods) do
local li = ul:tag('li')
local displayText = mod.description or mod.type
local isFeasible = mod.feasible ~= 'No'
if isFeasible then
li:wikitext('[' .. mod.url .. ' ' .. displayText .. ']')
else
local wrapper = li:tag('span')
wrapper:addClass('mod-not-feasible')
wrapper:wikitext('✗ [' .. mod.url .. ' ' .. displayText .. '] ')
local notPossible = wrapper:tag('i')
notPossible:css('font-size', '0.9em')
notPossible:wikitext('(not possible)')
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[crt_models]"] = pageName
}
)
local documentsCountWikitext =
'{{#ask:\n' ..
' [[Category:Documents]]\n' ..
' [[Has related CRT model::' .. 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 CRT model::' .. 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 docUrl = trim(cells[3])
local docFile = trim(cells[4])
local docTitle = trim(cells[5])
local linkTarget = nil
if docUrl then
linkTarget = docUrl
elseif docFile then
local filename = docFile:match('File:(.+)') or docFile
linkTarget = tostring(mw.uri.fullUrl('File:' .. filename))
end
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
-- Links section
local linksQueryWikitext =
'{{#ask:\n' ..
' [[CRT model::' .. 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 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
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
-- Contribute section
addSectionRow(container, 'Contribute')
local editUrl = mw.uri.fullUrl(
"Special:FormStart",
{
form = "CRT model",
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