Add audio properties to input queries and group inputs/outputs by location (Front, Rear, Side) (via update-page on MediaWiki MCP Server)
Add support for non-standard location values (e.g. "rear/front") by querying for "Other" locations that don't match front/rear/side exactly (via update-page on MediaWiki MCP Server)
Line 58: Line 58:
end
end


-- Helper to query inputs for a specific location
-- Helper to query inputs for a specific location (or non-standard locations)
local function queryInputsByLocation(frame, fullPageName, location)
local function queryInputsByLocation(frame, fullPageName, location, isOther)
local locationFilter = ''
local locationFilter = ''
if location then
if isOther then
-- Query for inputs that don't have standard locations
locationFilter = ' [[Has input location::!front]]\n' ..
' [[Has input location::!rear]]\n' ..
' [[Has input location::!side]]\n'
elseif location then
locationFilter = ' [[Has input location::' .. location .. ']]\n'
locationFilter = ' [[Has input location::' .. location .. ']]\n'
end
end
Line 76: Line 81:
' |?Has audio connector=audio_connector\n' ..
' |?Has audio connector=audio_connector\n' ..
' |?Has audio shared with=audio_shared\n' ..
' |?Has audio shared with=audio_shared\n' ..
' |?Has input location=location\n' ..
' |format=ul\n' ..
' |format=ul\n' ..
' |template=CRT input inline\n' ..
' |template=CRT input inline\n' ..
Line 87: Line 93:
end
end


-- Helper to count inputs for a specific location
-- Helper to count inputs for a specific location (or non-standard locations)
local function countInputsByLocation(frame, fullPageName, location)
local function countInputsByLocation(frame, fullPageName, location, isOther)
local locationFilter = ''
local locationFilter = ''
if location then
if isOther then
-- Count inputs that don't have standard locations
locationFilter = ' [[Has input location::!front]]\n' ..
' [[Has input location::!rear]]\n' ..
' [[Has input location::!side]]\n'
elseif location then
locationFilter = ' [[Has input location::' .. location .. ']]\n'
locationFilter = ' [[Has input location::' .. location .. ']]\n'
end
end
Line 107: Line 118:
end
end


-- Helper to query outputs for a specific location
-- Helper to query outputs for a specific location (or non-standard locations)
local function queryOutputsByLocation(frame, fullPageName, location)
local function queryOutputsByLocation(frame, fullPageName, location, isOther)
local locationFilter = ''
local locationFilter = ''
if location then
if isOther then
-- Query for outputs that don't have standard locations
locationFilter = ' [[Has output location::!front]]\n' ..
' [[Has output location::!rear]]\n' ..
' [[Has output location::!side]]\n'
elseif location then
locationFilter = ' [[Has output location::' .. location .. ']]\n'
locationFilter = ' [[Has output location::' .. location .. ']]\n'
end
end
Line 124: Line 140:
' |?Has output count=count\n' ..
' |?Has output count=count\n' ..
' |?Has output label=label\n' ..
' |?Has output label=label\n' ..
' |?Has output location=location\n' ..
' |format=ul\n' ..
' |format=ul\n' ..
' |template=CRT output inline\n' ..
' |template=CRT output inline\n' ..
Line 135: Line 152:
end
end


-- Helper to count outputs for a specific location
-- Helper to count outputs for a specific location (or non-standard locations)
local function countOutputsByLocation(frame, fullPageName, location)
local function countOutputsByLocation(frame, fullPageName, location, isOther)
local locationFilter = ''
local locationFilter = ''
if location then
if isOther then
-- Count outputs that don't have standard locations
locationFilter = ' [[Has output location::!front]]\n' ..
' [[Has output location::!rear]]\n' ..
' [[Has output location::!side]]\n'
elseif location then
locationFilter = ' [[Has output location::' .. location .. ']]\n'
locationFilter = ' [[Has output location::' .. location .. ']]\n'
end
end
Line 563: Line 585:
end
end


-- Inputs section: grouped by location
-- Inputs section: grouped by location (front, rear, side, other)
local locations = {'front', 'rear', 'side'}
local locations = {'front', 'rear', 'side'}
local locationLabels = {front = 'Front', rear = 'Rear', side = 'Side'}
local locationLabels = {front = 'Front', rear = 'Rear', side = 'Side'}
-- Count total inputs
-- Count total inputs across all standard locations plus "other"
local totalInputs = 0
local totalInputs = 0
for _, loc in ipairs(locations) do
for _, loc in ipairs(locations) do
totalInputs = totalInputs + countInputsByLocation(frame, fullPageName, loc)
totalInputs = totalInputs + countInputsByLocation(frame, fullPageName, loc, false)
end
end
-- Also count inputs with non-standard locations
local otherInputCount = countInputsByLocation(frame, fullPageName, nil, true)
totalInputs = totalInputs + otherInputCount
local hasInputs = totalInputs > 0
local hasInputs = totalInputs > 0
Line 578: Line 604:
local inputsContent = ''
local inputsContent = ''
-- Standard locations
for _, loc in ipairs(locations) do
for _, loc in ipairs(locations) do
local count = countInputsByLocation(frame, fullPageName, loc)
local count = countInputsByLocation(frame, fullPageName, loc, false)
if count > 0 then
if count > 0 then
local locInputs = queryInputsByLocation(frame, fullPageName, loc)
local locInputs = queryInputsByLocation(frame, fullPageName, loc, false)
if locInputs and locInputs ~= '' then
if locInputs and locInputs ~= '' then
inputsContent = inputsContent .. "<div style='margin-bottom:0.3em;'><strong>" .. locationLabels[loc] .. "</strong><br/>" .. locInputs .. "</div>"
inputsContent = inputsContent .. "<div style='margin-bottom:0.3em;'><strong>" .. locationLabels[loc] .. "</strong><br/>" .. locInputs .. "</div>"
end
end
end
end
-- Non-standard locations (e.g., "rear/front", "multiple", etc.)
if otherInputCount > 0 then
local otherInputs = queryInputsByLocation(frame, fullPageName, nil, true)
if otherInputs and otherInputs ~= '' then
inputsContent = inputsContent .. "<div style='margin-bottom:0.3em;'><strong>Multiple</strong><br/>" .. otherInputs .. "</div>"
end
end
end
end
Line 595: Line 629:
local totalOutputs = 0
local totalOutputs = 0
for _, loc in ipairs(locations) do
for _, loc in ipairs(locations) do
totalOutputs = totalOutputs + countOutputsByLocation(frame, fullPageName, loc)
totalOutputs = totalOutputs + countOutputsByLocation(frame, fullPageName, loc, false)
end
end
-- Also count outputs with non-standard locations
local otherOutputCount = countOutputsByLocation(frame, fullPageName, nil, true)
totalOutputs = totalOutputs + otherOutputCount
local hasOutputs = totalOutputs > 0
local hasOutputs = totalOutputs > 0
Line 603: Line 641:
local outputsContent = ''
local outputsContent = ''
-- Standard locations
for _, loc in ipairs(locations) do
for _, loc in ipairs(locations) do
local count = countOutputsByLocation(frame, fullPageName, loc)
local count = countOutputsByLocation(frame, fullPageName, loc, false)
if count > 0 then
if count > 0 then
local locOutputs = queryOutputsByLocation(frame, fullPageName, loc)
local locOutputs = queryOutputsByLocation(frame, fullPageName, loc, false)
if locOutputs and locOutputs ~= '' then
if locOutputs and locOutputs ~= '' then
outputsContent = outputsContent .. "<div style='margin-bottom:0.3em;'><strong>" .. locationLabels[loc] .. "</strong><br/>" .. locOutputs .. "</div>"
outputsContent = outputsContent .. "<div style='margin-bottom:0.3em;'><strong>" .. locationLabels[loc] .. "</strong><br/>" .. locOutputs .. "</div>"
end
end
end
end
-- Non-standard locations
if otherOutputCount > 0 then
local otherOutputs = queryOutputsByLocation(frame, fullPageName, nil, true)
if otherOutputs and otherOutputs ~= '' then
outputsContent = outputsContent .. "<div style='margin-bottom:0.3em;'><strong>Multiple</strong><br/>" .. otherOutputs .. "</div>"
end
end
end
end

Revision as of 19:43, 4 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 to query inputs for a specific location (or non-standard locations)
local function queryInputsByLocation(frame, fullPageName, location, isOther)
	local locationFilter = ''
	if isOther then
		-- Query for inputs that don't have standard locations
		locationFilter = ' [[Has input location::!front]]\n' ..
			' [[Has input location::!rear]]\n' ..
			' [[Has input location::!side]]\n'
	elseif location then
		locationFilter = ' [[Has input location::' .. location .. ']]\n'
	end
	
	local queryWikitext =
		'{{#ask:\n' ..
		' [[Has CRT model::' .. fullPageName .. ']]\n' ..
		' [[Has video signal format::+]]\n' ..
		locationFilter ..
		' |?Has video signal format=signal\n' ..
		' |?Has connector type=connector\n' ..
		' |?Has input count=count\n' ..
		' |?Has audio type=audio\n' ..
		' |?Has audio connector=audio_connector\n' ..
		' |?Has audio shared with=audio_shared\n' ..
		' |?Has input location=location\n' ..
		' |format=ul\n' ..
		' |template=CRT input inline\n' ..
		' |named args=yes\n' ..
		' |link=none\n' ..
		' |searchlabel=\n' ..
		'}}'
	
	local result = frame:preprocess(queryWikitext)
	return trim(result)
end

-- Helper to count inputs for a specific location (or non-standard locations)
local function countInputsByLocation(frame, fullPageName, location, isOther)
	local locationFilter = ''
	if isOther then
		-- Count inputs that don't have standard locations
		locationFilter = ' [[Has input location::!front]]\n' ..
			' [[Has input location::!rear]]\n' ..
			' [[Has input location::!side]]\n'
	elseif location then
		locationFilter = ' [[Has input location::' .. location .. ']]\n'
	end
	
	local countWikitext =
		'{{#ask:\n' ..
		' [[Has CRT model::' .. fullPageName .. ']]\n' ..
		' [[Has video signal format::+]]\n' ..
		locationFilter ..
		' |format=count\n' ..
		'}}'
	
	local result = frame:preprocess(countWikitext)
	local count = tonumber(trim(result)) or 0
	return count
end

-- Helper to query outputs for a specific location (or non-standard locations)
local function queryOutputsByLocation(frame, fullPageName, location, isOther)
	local locationFilter = ''
	if isOther then
		-- Query for outputs that don't have standard locations
		locationFilter = ' [[Has output location::!front]]\n' ..
			' [[Has output location::!rear]]\n' ..
			' [[Has output location::!side]]\n'
	elseif location then
		locationFilter = ' [[Has output location::' .. location .. ']]\n'
	end
	
	local queryWikitext =
		'{{#ask:\n' ..
		' [[Has CRT model::' .. fullPageName .. ']]\n' ..
		' [[Has output signal format::+]]\n' ..
		locationFilter ..
		' |?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 result = frame:preprocess(queryWikitext)
	return trim(result)
end

-- Helper to count outputs for a specific location (or non-standard locations)
local function countOutputsByLocation(frame, fullPageName, location, isOther)
	local locationFilter = ''
	if isOther then
		-- Count outputs that don't have standard locations
		locationFilter = ' [[Has output location::!front]]\n' ..
			' [[Has output location::!rear]]\n' ..
			' [[Has output location::!side]]\n'
	elseif location then
		locationFilter = ' [[Has output location::' .. location .. ']]\n'
	end
	
	local countWikitext =
		'{{#ask:\n' ..
		' [[Has CRT model::' .. fullPageName .. ']]\n' ..
		' [[Has output signal format::+]]\n' ..
		locationFilter ..
		' |format=count\n' ..
		'}}'
	
	local result = frame:preprocess(countWikitext)
	local count = tonumber(trim(result)) or 0
	return count
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)

	-- 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

	if remote_control then
		local remoteWikitext = string.format(
			'{{#formredlink: target=%s |form=Remote control |existing page link text=%s }}',
			remote_control, remote_control
		)
		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)

	-- 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: grouped by location (front, rear, side, other)
	local locations = {'front', 'rear', 'side'}
	local locationLabels = {front = 'Front', rear = 'Rear', side = 'Side'}
	
	-- Count total inputs across all standard locations plus "other"
	local totalInputs = 0
	for _, loc in ipairs(locations) do
		totalInputs = totalInputs + countInputsByLocation(frame, fullPageName, loc, false)
	end
	-- Also count inputs with non-standard locations
	local otherInputCount = countInputsByLocation(frame, fullPageName, nil, true)
	totalInputs = totalInputs + otherInputCount
	
	local hasInputs = totalInputs > 0
	
	if hasInputs then
		addSectionRow(container, 'Inputs')
		
		local inputsContent = ''
		-- Standard locations
		for _, loc in ipairs(locations) do
			local count = countInputsByLocation(frame, fullPageName, loc, false)
			if count > 0 then
				local locInputs = queryInputsByLocation(frame, fullPageName, loc, false)
				if locInputs and locInputs ~= '' then
					inputsContent = inputsContent .. "<div style='margin-bottom:0.3em;'><strong>" .. locationLabels[loc] .. "</strong><br/>" .. locInputs .. "</div>"
				end
			end
		end
		-- Non-standard locations (e.g., "rear/front", "multiple", etc.)
		if otherInputCount > 0 then
			local otherInputs = queryInputsByLocation(frame, fullPageName, nil, true)
			if otherInputs and otherInputs ~= '' then
				inputsContent = inputsContent .. "<div style='margin-bottom:0.3em;'><strong>Multiple</strong><br/>" .. otherInputs .. "</div>"
			end
		end
		if inputsContent ~= '' then
			addFullRow(container, inputsContent, 'padding:0.2em 0.4em;', true)
		end
	end

	-- Outputs section: grouped by location
	local totalOutputs = 0
	for _, loc in ipairs(locations) do
		totalOutputs = totalOutputs + countOutputsByLocation(frame, fullPageName, loc, false)
	end
	-- Also count outputs with non-standard locations
	local otherOutputCount = countOutputsByLocation(frame, fullPageName, nil, true)
	totalOutputs = totalOutputs + otherOutputCount
	
	local hasOutputs = totalOutputs > 0
	
	if hasOutputs then
		addSectionRow(container, 'Outputs')
		
		local outputsContent = ''
		-- Standard locations
		for _, loc in ipairs(locations) do
			local count = countOutputsByLocation(frame, fullPageName, loc, false)
			if count > 0 then
				local locOutputs = queryOutputsByLocation(frame, fullPageName, loc, false)
				if locOutputs and locOutputs ~= '' then
					outputsContent = outputsContent .. "<div style='margin-bottom:0.3em;'><strong>" .. locationLabels[loc] .. "</strong><br/>" .. locOutputs .. "</div>"
				end
			end
		end
		-- Non-standard locations
		if otherOutputCount > 0 then
			local otherOutputs = queryOutputsByLocation(frame, fullPageName, nil, true)
			if otherOutputs and otherOutputs ~= '' then
				outputsContent = outputsContent .. "<div style='margin-bottom:0.3em;'><strong>Multiple</strong><br/>" .. otherOutputs .. "</div>"
			end
		end
		if outputsContent ~= '' then
			addFullRow(container, outputsContent, 'padding:0.2em 0.4em;', true)
		end
	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

	-- 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