Module:CRTModel: Difference between revisions
No edit summary |
Change "Add accessory" button to link to Create_an_accessory with crt_models parameter (via update-page on MediaWiki MCP Server) |
||
| (110 intermediate revisions by 2 users not shown) | |||
| Line 20: | Line 20: | ||
end | end | ||
local function addSectionRow( | local function addSectionRow(container, title) | ||
local | 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 | end | ||
local function addRow( | local function addRow(container, label, valueHtmlOrWikitext, isRawHtml) | ||
local | 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 | local valueDiv = container:tag('div') | ||
css( | valueDiv:addClass('infobox-value') | ||
css(valueDiv, 'padding:0.2em 0.4em;') | |||
if isRawHtml then | if isRawHtml then | ||
valueDiv:node(mw.html.create('span'):wikitext(valueHtmlOrWikitext)) | |||
else | else | ||
valueDiv:wikitext(valueHtmlOrWikitext) | |||
end | end | ||
end | end | ||
local function addFullRow( | local function addFullRow(container, valueHtmlOrWikitext, cssText, isRawHtml, extraClass) | ||
local | local fullDiv = container:tag('div') | ||
css(fullDiv, 'grid-column: 1 / -1; ' .. (cssText or '')) | |||
if extraClass then | |||
fullDiv:addClass(extraClass) | |||
end | |||
if isRawHtml then | if isRawHtml then | ||
fullDiv:wikitext(valueHtmlOrWikitext) | |||
else | else | ||
fullDiv:wikitext(valueHtmlOrWikitext) | |||
end | 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 | end | ||
| Line 67: | Line 227: | ||
local brand = trim(args.brand) | local brand = trim(args.brand) | ||
local model_code = trim(args.model_code) | local model_code = trim(args.model_code) | ||
local variants = trim(args.variants) | |||
local market = trim(args.market) | |||
local series = trim(args.series) | |||
local image_main = trim(args.image_main) | local image_main = trim(args.image_main) | ||
local model_3d = trim(args.model_3d) | local model_3d = trim(args.model_3d) | ||
local crt_type = trim(args.crt_type) | 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 screen_size_in = trim(args.screen_size_in) | ||
local aspect_ratio = trim(args.aspect_ratio) | |||
local tvl = trim(args.tvl) | local tvl = trim(args.tvl) | ||
local chassis = trim(args.chassis) | 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 height = trim(args.height) | ||
local width = trim(args.width) | local width = trim(args.width) | ||
local depth = trim(args.depth) | local depth = trim(args.depth) | ||
local weight = trim(args.weight) | local weight = trim(args.weight) | ||
local | |||
local | -- Power specifications (frequency and power consumption still from template args) | ||
local | 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 | local container = mw.html.create('div') | ||
container:addClass('infobox') | |||
css( | 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) | -- Title row (no background color, centered, bold) | ||
addFullRow( | addFullRow( | ||
container, | |||
(brand or '') .. ' ' .. (model_code or ''), | (brand or '') .. ' ' .. (model_code or ''), | ||
'text-align:center; font-size:125%; font-weight:bold; padding:0.5em;', | 'text-align:center; font-size:125%; font-weight:bold; padding:0.5em;', | ||
false | 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 | if image_main then | ||
local file = image_main:gsub('^File:', '') | local file = image_main:gsub('^File:', '') | ||
addFullRow( | addFullRow( | ||
container, | |||
string.format('[[File:%s|frameless|300px]]', file), | string.format('[[File:%s|frameless|300px]]', file), | ||
'text-align:center; padding:0;', | 'text-align:center; padding:0;', | ||
false | false, | ||
'infobox-image' | |||
) | ) | ||
end | end | ||
| Line 107: | Line 321: | ||
local file = model_3d:gsub('^File:', '') | local file = model_3d:gsub('^File:', '') | ||
addFullRow( | addFullRow( | ||
container, | |||
string.format('[[File:%s]]', file), | string.format('[[File:%s]]', file), | ||
'text-align:center; padding:0;', | 'text-align:center; padding:0;', | ||
false | false, | ||
'infobox-3d-model' | |||
) | ) | ||
end | end | ||
addSectionRow( | addSectionRow(container, 'Overview') | ||
-- Brand row using #formredlink (must preprocess) | -- Brand row using #formredlink (must preprocess) | ||
| Line 123: | Line 338: | ||
) | ) | ||
local brandHtml = frame:preprocess(brandWikitext) | local brandHtml = frame:preprocess(brandWikitext) | ||
addRow( | addRow(container, 'Brand', brandHtml, true) | ||
end | end | ||
if model_code then | if model_code then | ||
addRow( | addRow(container, 'Model', model_code, false) | ||
else | else | ||
addRow( | 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 | end | ||
if crt_type then | if crt_type then | ||
addRow( | 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 | end | ||
if screen_size_in then | if screen_size_in then | ||
addRow( | addRow(container, 'Screen size', screen_size_in .. '″', false) | ||
end | |||
if aspect_ratio then | |||
addRow(container, 'Aspect ratio', aspect_ratio, false) | |||
end | end | ||
if tvl then | if tvl then | ||
addRow( | 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 | end | ||
if | -- Remote control row: use Accessory form, strip brand prefix if it matches CRT brand | ||
local | if remote_control then | ||
'{{#formredlink: target=%s |form= | local displayText = getDisplayTextWithoutBrand(remote_control, brand) | ||
local remoteWikitext = string.format( | |||
'{{#formredlink: target=%s |form=Accessory |existing page link text=%s }}', | |||
remote_control, displayText | |||
) | ) | ||
local | local remoteHtml = frame:preprocess(remoteWikitext) | ||
addRow( | addRow(container, 'Remote', remoteHtml, true) | ||
end | end | ||
-- ECID row: build as a normal external link wikitext; preprocess not required, but fine. | -- 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( | addRow(container, 'ECID', frame:preprocess(ecidWikitext), true) | ||
if | -- Tube section: show if we have any tube specs | ||
addSectionRow( | 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( | 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 | end | ||
-- Weight row: uses #show, so preprocess it | -- 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 | end | ||
-- | -- Speakers section: show if we have any speaker specs | ||
local | 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' .. | '{{#ask:\n' .. | ||
' [[Has CRT model::' .. fullPageName .. ']]\n' .. | ' [[Has CRT model::' .. fullPageName .. ']]\n' .. | ||
' |? | ' [[Voltage min::+]]\n' .. | ||
' | | ' |?Voltage min\n' .. | ||
' |?Has | ' |?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 | 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 | end | ||
-- | -- Mods section | ||
if | local modsQueryWikitext = | ||
addSectionRow( | '{{#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 | end | ||
-- Documents section | |||
addSectionRow( | addSectionRow(container, 'Documents') | ||
local timestamp = os.time() | |||
local target = | math.randomseed(timestamp + pageId) | ||
local random = math.random(1000, 9999) | |||
local target = "Document:" .. timestamp .. "-" .. random | |||
local uploadUrl = mw.uri.fullUrl( | local uploadUrl = mw.uri.fullUrl( | ||
"Special:FormEdit", | "Special:FormEdit", | ||
| Line 205: | Line 1,011: | ||
form = "Document", | form = "Document", | ||
target = target, | target = target, | ||
["Document[ | ["Document[crt_models]"] = pageName | ||
} | } | ||
) | ) | ||
local documentsCountWikitext = | local documentsCountWikitext = | ||
'{{#ask:\n' .. | '{{#ask:\n' .. | ||
| Line 219: | Line 1,024: | ||
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 | css(docsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | ||
css( | |||
if hasDocuments then | if hasDocuments then | ||
local docsQueryWikitext = | |||
local | |||
'{{#ask:\n' .. | '{{#ask:\n' .. | ||
' [[Category:Documents]]\n' .. | ' [[Category:Documents]]\n' .. | ||
' [[Has related CRT model::' .. fullPageName .. ']]\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' .. | ' |link=none\n' .. | ||
'}}' | '}}' | ||
local | local docsResult = frame:preprocess(docsQueryWikitext) | ||
- | local ul = mw.html.create('ul') | ||
if | ul:css('margin', '0'):css('padding-left', '1.5em') | ||
for | |||
if docsResult then | |||
if | for rowContent in docsResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | |||
local | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local | 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 | local linkTarget = nil | ||
local | 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 | if docPageName then | ||
local displayText = 'Document' | |||
if docTitle then | |||
displayText = docTitle | |||
elseif docType then | |||
displayText = docType | |||
end | |||
local li = ul:tag('li') | local li = ul:tag('li') | ||
li:wikitext('[[' .. docPageName .. '|' .. displayText .. ']]') | |||
end | end | ||
end | end | ||
| Line 265: | Line 1,084: | ||
end | end | ||
docsDiv:node(ul) | |||
local uploadDiv = mw.html.create('div') | local uploadDiv = mw.html.create('div') | ||
uploadDiv:css('margin-top', '0.5em'):css(' | 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 | else | ||
local uploadDiv = mw.html.create('div') | local uploadDiv = mw.html.create('div') | ||
uploadDiv:css(' | uploadDiv:css('text-align', 'center') | ||
uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | |||
docsDiv:node(uploadDiv) | |||
end | end | ||
-- | -- Links section | ||
if | 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 | end | ||
if hasLinks then | |||
addSectionRow(container, 'Archival Links') | |||
local linkTypeOrder = {'Database', 'Manufacturer', 'Retailer'} | |||
for _, linkType in ipairs(linkTypeOrder) do | |||
if #linksByType[linkType] > 0 then | |||
local linksHtml = {} | |||
for _, link in ipairs(linksByType[linkType]) do | |||
table.insert(linksHtml, '[' .. link.url .. ' ' .. link.source .. ']') | |||
end | |||
addRow(container, linkType, table.concat(linksHtml, '<br>'), true) | |||
end | |||
end | end | ||
end | end | ||
return tostring( | -- 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 | end | ||
return p | return p | ||