Add Tube section to infobox: reads mask_type, pitch, phosphor_type, gun_type, deflection_angle, neck_diameter, visible_w, visible_h, visible_diag and displays them between ECID and Dimensions (via update-page on MediaWiki MCP Server)
Add Accessories section, smart remote display (strips redundant brand prefix) (via update-page on MediaWiki MCP Server)
Line 56: Line 56:
fullDiv:wikitext(valueHtmlOrWikitext)
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 getAccessoryDisplayText(accessoryName, crtBrand)
if not accessoryName then return nil end
if not crtBrand then return accessoryName end
-- Check if accessory name starts with the CRT brand followed by a space
local brandPrefix = crtBrand .. ' '
if accessoryName:sub(1, #brandPrefix) == brandPrefix then
-- Return just the model code portion
return accessoryName:sub(#brandPrefix + 1)
end
-- Otherwise return the full name
return accessoryName
end
end


Line 260: Line 277:
end
end


-- Remote control row: use Accessory form, strip brand prefix if it matches CRT brand
if remote_control then
if remote_control then
local displayText = getAccessoryDisplayText(remote_control, brand)
local remoteWikitext = string.format(
local remoteWikitext = string.format(
'{{#formredlink: target=%s |form=Remote control |existing page link text=%s }}',
'{{#formredlink: target=%s |form=Accessory |existing page link text=%s }}',
remote_control, remote_control
remote_control, displayText
)
)
local remoteHtml = frame:preprocess(remoteWikitext)
local remoteHtml = frame:preprocess(remoteWikitext)
Line 616: Line 635:
addSectionRow(container, 'Control Ports')
addSectionRow(container, 'Control Ports')
addFullRow(container, controlPortsHtml, 'padding:0.2em 0.4em;', true)
addFullRow(container, controlPortsHtml, 'padding:0.2em 0.4em;', true)
end
-- Accessories section: Query for non-remote accessories linked via Has related CRT model
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
if hasAccessories then
addSectionRow(container, 'Accessories')
local accessoriesDiv = container:tag('div')
css(accessoriesDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')
-- Query accessories and display as a list
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' ..
' |?Has description=desc\n' ..
' |format=broadtable\n' ..
' |headers=hide\n' ..
' |link=none\n' ..
'}}'
local accessoriesResult = frame:preprocess(accessoriesQueryWikitext)
-- Parse accessories from broadtable result
local accessories = {}
if accessoriesResult then
for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do
local cells = {}
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
local cleanContent = cellContent:gsub('<[^>]+>', '')
table.insert(cells, trim(cleanContent))
end
-- cells[1] = page name, cells[2] = type, cells[3] = model code, cells[4] = description
if #cells >= 2 then
local accPageName = cells[1]
local accType = cells[2]
local accModel = cells[3]
local accDesc = cells[4]
if accPageName then
table.insert(accessories, {
page = accPageName,
type = accType,
model = accModel,
desc = accDesc
})
end
end
end
end
if #accessories > 0 then
local ul = accessoriesDiv:tag('ul')
ul:css('margin', '0'):css('padding-left', '1.5em')
for _, acc in ipairs(accessories) do
local li = ul:tag('li')
-- Get display text: strip brand prefix if it matches CRT brand
local displayText = getAccessoryDisplayText(acc.page, brand)
-- Build the display: model code (optionally with description)
local linkText = displayText
if acc.desc and acc.desc ~= '' then
linkText = displayText .. ' – ' .. acc.desc
end
-- Add type prefix in small text
if acc.type then
li:wikitext('<small>' .. acc.type .. ':</small> [[' .. acc.page .. '|' .. linkText .. ']]')
else
li:wikitext('[[' .. acc.page .. '|' .. linkText .. ']]')
end
end
end
end
end



Revision as of 17:38, 9 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 getAccessoryDisplayText(accessoryName, crtBrand)
	if not accessoryName then return nil end
	if not crtBrand then return accessoryName end
	
	-- Check if accessory name starts with the CRT brand followed by a space
	local brandPrefix = crtBrand .. ' '
	if accessoryName:sub(1, #brandPrefix) == brandPrefix then
		-- Return just the model code portion
		return accessoryName:sub(#brandPrefix + 1)
	end
	
	-- Otherwise return the full name
	return accessoryName
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 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'
	)

	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

	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 = getAccessoryDisplayText(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
						-- Single voltage (e.g., "120V")
						rangeStr = range.min .. 'V'
					else
						-- Range (e.g., "100–120V")
						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
				-- Join multiple ranges with " / " (e.g., "100–120V / 220–240V")
				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')
		
		-- AV system row (baseband video color support)
		if av_system then
			-- Link each system to its wiki page
			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
		
		-- Tuner system row (RF broadcast standards)
		if tuner_system then
			-- Link each system to its wiki page
			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: show if we have any scan range specs
	if h_scan_min or h_scan_max or v_scan_min or v_scan_max then
		addSectionRow(container, 'Scan Range')
		
		-- Horizontal scan row
		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
					-- Single frequency (e.g., "15.7 kHz")
					hDisplay = h_scan_min .. ' kHz'
				else
					-- Range (e.g., "15–100 kHz")
					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
		
		-- Vertical scan row
		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
					-- Single frequency (e.g., "60 Hz")
					vDisplay = v_scan_min .. ' Hz'
				else
					-- Range (e.g., "50–160 Hz")
					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 section: query ALL inputs with location shown inline
	local inputsCountWikitext =
		'{{#ask:\n' ..
		' [[Has CRT model::' .. fullPageName .. ']]\n' ..
		' [[Has video signal format::+]]\n' ..
		' |format=count\n' ..
		'}}'
	local inputCount = frame:preprocess(inputsCountWikitext)
	local hasInputs = inputCount and tonumber(trim(inputCount)) and tonumber(trim(inputCount)) > 0
	
	if hasInputs then
		local askWikitext =
			'{{#ask:\n' ..
			' [[Has CRT model::' .. fullPageName .. ']]\n' ..
			' [[Has video signal format::+]]\n' ..
			' |?Has video signal format=signal\n' ..
			' |?Has connector type=connector\n' ..
			' |?Has input count=count\n' ..
			' |?Has input location=location\n' ..
			' |?Has audio type=audio\n' ..
			' |?Has audio connector=audio_connector\n' ..
			' |?Has audio shared with=audio_shared\n' ..
			' |format=ul\n' ..
			' |template=CRT input inline\n' ..
			' |named args=yes\n' ..
			' |link=none\n' ..
			' |searchlabel=\n' ..
			'}}'
		local askHtml = frame:preprocess(askWikitext)
		
		addSectionRow(container, 'Inputs')
		addFullRow(container, askHtml, 'padding:0.2em 0.4em;', true)
	end

	-- Outputs section: query ALL outputs with location shown inline
	local outputsCountWikitext =
		'{{#ask:\n' ..
		' [[Has CRT model::' .. fullPageName .. ']]\n' ..
		' [[Has output signal format::+]]\n' ..
		' |format=count\n' ..
		'}}'
	local outputCount = frame:preprocess(outputsCountWikitext)
	local hasOutputs = outputCount and tonumber(trim(outputCount)) and tonumber(trim(outputCount)) > 0
	
	if hasOutputs then
		local outputsAskWikitext =
			'{{#ask:\n' ..
			' [[Has CRT model::' .. fullPageName .. ']]\n' ..
			' [[Has output signal format::+]]\n' ..
			' |?Has output type=type\n' ..
			' |?Has output signal format=signal\n' ..
			' |?Has output connector=connector\n' ..
			' |?Has output count=count\n' ..
			' |?Has output label=label\n' ..
			' |?Has output location=location\n' ..
			' |format=ul\n' ..
			' |template=CRT output inline\n' ..
			' |named args=yes\n' ..
			' |link=none\n' ..
			' |searchlabel=\n' ..
			'}}'
		local outputsHtml = frame:preprocess(outputsAskWikitext)
		
		addSectionRow(container, 'Outputs')
		addFullRow(container, outputsHtml, 'padding:0.2em 0.4em;', true)
	end

	-- Control Ports section: query for control port subobjects
	local controlPortsCountWikitext =
		'{{#ask:\n' ..
		' [[Has CRT model::' .. fullPageName .. ']]\n' ..
		' [[Has control signal format::+]]\n' ..
		' |format=count\n' ..
		'}}'
	local controlPortCount = frame:preprocess(controlPortsCountWikitext)
	local hasControlPorts = controlPortCount and tonumber(trim(controlPortCount)) and tonumber(trim(controlPortCount)) > 0
	
	if hasControlPorts then
		local controlPortsAskWikitext =
			'{{#ask:\n' ..
			' [[Has CRT model::' .. fullPageName .. ']]\n' ..
			' [[Has control signal format::+]]\n' ..
			' |?Has control signal format=signal\n' ..
			' |?Has control port connector=connector\n' ..
			' |?Has control port direction=direction\n' ..
			' |?Has control port count=count\n' ..
			' |?Has control port label=label\n' ..
			' |?Has control port location=location\n' ..
			' |format=ul\n' ..
			' |template=CRT control port inline\n' ..
			' |named args=yes\n' ..
			' |link=none\n' ..
			' |searchlabel=\n' ..
			'}}'
		local controlPortsHtml = frame:preprocess(controlPortsAskWikitext)
		
		addSectionRow(container, 'Control Ports')
		addFullRow(container, controlPortsHtml, 'padding:0.2em 0.4em;', true)
	end

	-- Accessories section: Query for non-remote accessories linked via Has related CRT model
	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
	
	if hasAccessories then
		addSectionRow(container, 'Accessories')
		
		local accessoriesDiv = container:tag('div')
		css(accessoriesDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')
		
		-- Query accessories and display as a list
		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' ..
			' |?Has description=desc\n' ..
			' |format=broadtable\n' ..
			' |headers=hide\n' ..
			' |link=none\n' ..
			'}}'
		local accessoriesResult = frame:preprocess(accessoriesQueryWikitext)
		
		-- Parse accessories from broadtable result
		local accessories = {}
		if accessoriesResult then
			for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do
				local cells = {}
				for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
					local cleanContent = cellContent:gsub('<[^>]+>', '')
					table.insert(cells, trim(cleanContent))
				end
				
				-- cells[1] = page name, cells[2] = type, cells[3] = model code, cells[4] = description
				if #cells >= 2 then
					local accPageName = cells[1]
					local accType = cells[2]
					local accModel = cells[3]
					local accDesc = cells[4]
					
					if accPageName then
						table.insert(accessories, {
							page = accPageName,
							type = accType,
							model = accModel,
							desc = accDesc
						})
					end
				end
			end
		end
		
		if #accessories > 0 then
			local ul = accessoriesDiv:tag('ul')
			ul:css('margin', '0'):css('padding-left', '1.5em')
			
			for _, acc in ipairs(accessories) do
				local li = ul:tag('li')
				
				-- Get display text: strip brand prefix if it matches CRT brand
				local displayText = getAccessoryDisplayText(acc.page, brand)
				
				-- Build the display: model code (optionally with description)
				local linkText = displayText
				if acc.desc and acc.desc ~= '' then
					linkText = displayText .. ' – ' .. acc.desc
				end
				
				-- Add type prefix in small text
				if acc.type then
					li:wikitext('<small>' .. acc.type .. ':</small> [[' .. acc.page .. '|' .. linkText .. ']]')
				else
					li:wikitext('[[' .. acc.page .. '|' .. linkText .. ']]')
				end
			end
		end
	end

	-- Mods section: Query for mod subobjects and display
	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)
	
	-- Parse mods from broadtable result
	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
	
	-- Display mods section if there are any
	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')
			
			-- Determine display text: use description if available, otherwise type
			local displayText = mod.description or mod.type
			
			-- Check if mod is not feasible
			local isFeasible = mod.feasible ~= 'No'
			
			if isFeasible then
				-- Normal link for feasible mods using wikitext external link
				li:wikitext('[' .. mod.url .. ' ' .. displayText .. ']')
			else
				-- Red styling with X for impossible mods
				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')
	
	-- Build the upload link with pseudorandom unique identifier
	local timestamp = os.time()
	math.randomseed(timestamp + pageId)  -- Seed with timestamp + page ID for better randomness
	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
		}
	)
	
	-- Query for document count
	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
	
	-- Create the content div
	local docsDiv = container:tag('div')
	css(docsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')
	
	if hasDocuments then
		-- Use #ask to get documents
		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)
		
		-- Create UL element
		local ul = mw.html.create('ul')
		ul:css('margin', '0'):css('padding-left', '1.5em')
		
		-- Parse the HTML table - extract data from <td> tags
		if docsResult then
			-- Match each table row
			for rowContent in docsResult:gmatch('<tr[^>]*>(.-)</tr>') do
				local cells = {}
				-- Extract content from each <td>
				for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
					-- Strip any remaining HTML tags and get just the text
					local cleanContent = cellContent:gsub('<[^>]+>', '')
					table.insert(cells, cleanContent)
				end
				
				-- cells[1] = page name, cells[2] = type, cells[3] = URL, cells[4] = file, cells[5] = title
				if #cells >= 2 then
					local docType = trim(cells[2])
					local docUrl = trim(cells[3])
					local docFile = trim(cells[4])
					local docTitle = trim(cells[5])
					
					-- Determine what to link to (URL or file)
					local linkTarget = nil
					if docUrl then
						linkTarget = docUrl
					elseif docFile then
						-- Extract filename from "File:..." format if present
						local filename = docFile:match('File:(.+)') or docFile
						linkTarget = tostring(mw.uri.fullUrl('File:' .. filename))
					end

					-- Always link to the wiki page
					local docPageName = trim(cells[1])
					
					if docPageName then
						-- Determine display text priority: custom title > type > default
						local displayText = 'Document'
						if docTitle then
							displayText = docTitle
						elseif docType then
							displayText = docType
						end
						
						-- Create list item with link to wiki page
						local li = ul:tag('li')
						li:wikitext('[[' .. docPageName .. '|' .. displayText .. ']]')
					end
				end
			end
		end
		
		docsDiv:node(ul)
		
		-- Add upload link as a button
		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 a document</span>]')
		
		docsDiv:node(uploadDiv)
	else
		-- Just show the upload link as a button
		local uploadDiv = mw.html.create('div')
		uploadDiv:css('text-align', 'center')
		uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add a document</span>]')
		
		docsDiv:node(uploadDiv)
	end

	-- Links section: Query for link subobjects and display by type
	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)
	
	-- Parse links and organize by type
	local linksByType = {
		Manufacturer = {},
		Retailer = {},
		Database = {}
	}
	local hasLinks = false
	
	if linksResult then
		-- Match each table row
		for rowContent in linksResult:gmatch('<tr[^>]*>(.-)</tr>') do
			local cells = {}
			-- Extract content from each <td>
			for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
				-- Strip any remaining HTML tags and get just the text
				local cleanContent = cellContent:gsub('<[^>]+>', '')
				table.insert(cells, trim(cleanContent))
			end
			
			-- cells[1] = page/subobject, cells[2] = type, cells[3] = source, cells[4] = URL
			if #cells >= 4 then
				local linkType = cells[2]
				local linkSource = cells[3]
				local linkUrl = cells[4]
				
				if linkType and linkSource and linkUrl and linkUrl ~= '' then
					if linksByType[linkType] then
						table.insert(linksByType[linkType], {source = linkSource, url = linkUrl})
						hasLinks = true
					end
				end
			end
		end
	end
	
	-- Display links section if there are any links
	if hasLinks then
		addSectionRow(container, 'Archival Links')
		
		-- Add links in order: Database, Manufacturer, Retailer
		local typeOrder = {'Database', 'Manufacturer', 'Retailer'}
		for _, linkType in ipairs(typeOrder) do
			if #linksByType[linkType] > 0 then
				-- If multiple links of the same type, combine them in one row
				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')
	
	-- Build the edit link URL
	local editUrl = mw.uri.fullUrl(
		"Special:FormStart",
		{
			form = "CRT model",
			page_name = pageName
		}
	)
	
	-- Create the edit button div
	local editDiv = container:tag('div')
	css(editDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em; text-align:center;')
	
	-- Add edit button with primary action styling (using class for night mode compatibility)
	editDiv:wikitext('[' .. tostring(editUrl) .. ' <span class="infobox-button infobox-button-primary">Edit this data</span>]')

	-- Return templatestyles tag + container HTML
	return styleTag .. tostring(container)
end

return p
MediaWiki Appliance - Powered by TurnKey Linux