Refactor Archival Links to use broadtable + group-by-type (matching CRT model pattern) instead of template=AV device link row bullet list (via update-page on MediaWiki MCP Server)
Add "Add an accessory" button to Accessories section, always show section (like Parts/Documents) (via update-page on MediaWiki MCP Server)
Line 524: Line 524:
end
end


-- Accessories section: Query for non-remote accessories linked via Has related AV device
-- Accessories section: Always show (like Parts/Documents), with "Add an accessory" button
addSectionRow(container, 'Accessories')
 
-- Build the add accessory link with prefilled AV device
local accTimestamp = os.time()
math.randomseed(accTimestamp + pageId + 2)
local accRandom = math.random(1000, 9999)
local accTarget = accTimestamp .. "-" .. accRandom
local addAccessoryUrl = mw.uri.fullUrl(
"Special:FormEdit",
{
form = "Accessory",
target = accTarget,
["Accessory[av_devices]"] = pageName
}
)
 
-- Query for non-remote accessories linked via Has related AV device
local accessoriesCountWikitext =
local accessoriesCountWikitext =
'{{#ask:\n' ..
'{{#ask:\n' ..
Line 534: Line 551:
local accessoryCount = frame:preprocess(accessoriesCountWikitext)
local accessoryCount = frame:preprocess(accessoriesCountWikitext)
local hasAccessories = accessoryCount and tonumber(trim(accessoryCount)) and tonumber(trim(accessoryCount)) > 0
local hasAccessories = accessoryCount and tonumber(trim(accessoryCount)) and tonumber(trim(accessoryCount)) > 0
 
local accDiv = container:tag('div')
css(accDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')
 
if hasAccessories then
if hasAccessories then
addSectionRow(container, 'Accessories')
-- Query accessories and display grouped by type
-- Query accessories and display grouped by type
local accessoriesQueryWikitext =
local accessoriesQueryWikitext =
Line 551: Line 569:
'}}'
'}}'
local accessoriesResult = frame:preprocess(accessoriesQueryWikitext)
local accessoriesResult = frame:preprocess(accessoriesQueryWikitext)
 
-- Parse accessories from broadtable result and group by type
-- Parse accessories from broadtable result and group by type
local accessoriesByType = {}
local accessoriesByType = {}
local typeOrder = {}
local typeOrder = {}
 
if accessoriesResult then
if accessoriesResult then
for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do
for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do
Line 563: Line 581:
table.insert(cells, trim(cleanContent))
table.insert(cells, trim(cleanContent))
end
end
 
-- cells[1] = page name, cells[2] = type, cells[3] = model code
-- cells[1] = page name, cells[2] = type, cells[3] = model code
if #cells >= 2 then
if #cells >= 2 then
local accPageName = cells[1]
local accPageName = cells[1]
local accType = cells[2] or 'Accessory'
local accType = cells[2] or 'Accessory'
 
if accPageName then
if accPageName then
-- Track type order for consistent display
if not accessoriesByType[accType] then
if not accessoriesByType[accType] then
accessoriesByType[accType] = {}
accessoriesByType[accType] = {}
table.insert(typeOrder, accType)
table.insert(typeOrder, accType)
end
end
 
table.insert(accessoriesByType[accType], {
table.insert(accessoriesByType[accType], {
page = accPageName
page = accPageName
Line 583: Line 600:
end
end
end
end
 
-- Display as label/value rows grouped by type (like Archival Links)
-- Create a nested grid for accessory rows
local accGrid = accDiv:tag('div')
css(accGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;')
 
-- Display as label/value rows grouped by type (like Parts)
for _, accType in ipairs(typeOrder) do
for _, accType in ipairs(typeOrder) do
local accs = accessoriesByType[accType]
local accs = accessoriesByType[accType]
Line 590: Line 611:
local accLinks = {}
local accLinks = {}
for _, acc in ipairs(accs) do
for _, acc in ipairs(accs) do
-- Show just "Brand Model" as the link text (the page name)
table.insert(accLinks, '[[' .. acc.page .. ']]')
table.insert(accLinks, '[[' .. acc.page .. ']]')
end
end
addRow(container, accType, table.concat(accLinks, '<br>'), true)
 
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
end
end
-- Add "Add an accessory" button
local addAccDiv = mw.html.create('div')
addAccDiv:css('margin-top', '0.5em'):css('text-align', 'center')
addAccDiv:wikitext('[' .. tostring(addAccessoryUrl) .. ' <span class="infobox-button infobox-button-secondary">Add an accessory</span>]')
accDiv:node(addAccDiv)
else
-- Just show the add accessory link as a button
local addAccDiv = mw.html.create('div')
addAccDiv:css('text-align', 'center')
addAccDiv:wikitext('[' .. tostring(addAccessoryUrl) .. ' <span class="infobox-button infobox-button-secondary">Add an accessory</span>]')
accDiv:node(addAccDiv)
end
end



Revision as of 02:57, 14 February 2026

Documentation for this module may be created at Module:AVDevice/doc

-- Module:AVDevice
-- Builds the AV device infobox as strict HTML to avoid whitespace issues
-- Uses frame:preprocess() so parser functions like #ask and #formredlink evaluate.

local p = {}

local function trim(s)
	if s == nil then return nil end
	s = tostring(s)
	s = s:gsub('^%s+', ''):gsub('%s+$', '')
	if s == '' then return nil end
	return s
end

local function css(node, cssText)
	if cssText and cssText ~= '' then
		node:cssText(cssText)
	end
	return node
end

local function addSectionRow(container, title)
	local section = container:tag('div')
	section:addClass('infobox-section-header')
	css(section, 'grid-column: 1 / -1; text-align:center; padding:0.5em; font-weight:bold;')
	section:wikitext(title)
end

local function addRow(container, label, valueHtmlOrWikitext, isRawHtml)
	local labelDiv = container:tag('div')
	labelDiv:addClass('infobox-label')
	css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
	labelDiv:wikitext(label)

	local valueDiv = container:tag('div')
	valueDiv:addClass('infobox-value')
	css(valueDiv, 'padding:0.2em 0.4em;')

	if isRawHtml then
		valueDiv:node(mw.html.create('span'):wikitext(valueHtmlOrWikitext))
	else
		valueDiv:wikitext(valueHtmlOrWikitext)
	end
end

local function addFullRow(container, valueHtmlOrWikitext, cssText, isRawHtml, extraClass)
	local fullDiv = container:tag('div')
	css(fullDiv, 'grid-column: 1 / -1; ' .. (cssText or ''))
	if extraClass then
		fullDiv:addClass(extraClass)
	end

	if isRawHtml then
		fullDiv:wikitext(valueHtmlOrWikitext)
	else
		fullDiv:wikitext(valueHtmlOrWikitext)
	end
end

-- Helper function to strip brand prefix from item name if it matches device brand
local function getDisplayTextWithoutBrand(itemName, deviceBrand)
	if not itemName then return nil end
	if not deviceBrand then return itemName end
	
	local brandPrefix = deviceBrand .. ' '
	if itemName:sub(1, #brandPrefix) == brandPrefix then
		return itemName:sub(#brandPrefix + 1)
	end
	
	return itemName
end

-- Helper function to format comma-separated values as a bulleted list or inline
local function formatList(value, inline)
	if not value then return nil end
	local items = {}
	for item in value:gmatch('[^,]+') do
		local trimmed = trim(item)
		if trimmed then
			table.insert(items, trimmed)
		end
	end
	if #items == 0 then return nil end
	if inline or #items <= 3 then
		return table.concat(items, ', ')
	else
		local ul = mw.html.create('ul')
		ul:css('margin', '0'):css('padding-left', '1.2em')
		for _, item in ipairs(items) do
			ul:tag('li'):wikitext(item)
		end
		return tostring(ul)
	end
end

-- Helper to format MSRP with dollar sign and commas
local function formatMSRP(value)
	if not value then return nil end
	local num = tonumber(value)
	if not num then return '$' .. value end
	-- Format with commas
	local formatted = string.format("%.2f", num)
	local intPart, decPart = formatted:match("^(%d+)%.(%d+)$")
	if intPart then
		local reversed = intPart:reverse()
		local withCommas = reversed:gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
		return '$' .. withCommas .. '.' .. decPart
	end
	return '$' .. formatted
end

-- Helper to query inputs/outputs for a specific location
local function queryIOByLocation(frame, fullPageName, ioType, location)
	local locationFilter = ''
	if location then
		locationFilter = ' [[Has ' .. (ioType == 'input' and 'input' or 'output') .. ' location::' .. location .. ']]\n'
	end
	
	local signalProp = ioType == 'input' and 'Has video signal format' or 'Has output signal format'
	local countProp = ioType == 'input' and 'Has input count' or 'Has output count'
	local connectorProp = ioType == 'input' and 'Has connector type' or 'Has output connector'
	local notesProp = ioType == 'input' and 'Has input notes' or 'Has output notes'
	local labelProp = ioType == 'input' and 'Has input label' or 'Has output label'
	local templateName = ioType == 'input' and 'AV device input row' or 'AV device output row'
	
	-- Build audio property queries (only for inputs)
	local audioProps = ''
	if ioType == 'input' then
		audioProps = ' |?Has audio type=audio\n' ..
			' |?Has audio connector=audio_connector\n' ..
			' |?Has audio shared with=audio_shared\n'
	end
	
	local queryWikitext =
		'{{#ask:\n' ..
		' [[Has AV device::' .. fullPageName .. ']]\n' ..
		' [[' .. signalProp .. '::+]]\n' ..
		locationFilter ..
		' |?' .. countProp .. '=count\n' ..
		' |?' .. signalProp .. '=signal\n' ..
		' |?' .. connectorProp .. '=connector\n' ..
		' |?' .. labelProp .. '=label\n' ..
		' |?' .. notesProp .. '=notes\n' ..
		audioProps ..
		' |format=template\n' ..
		' |template=' .. templateName .. '\n' ..
		' |named args=yes\n' ..
		' |link=none\n' ..
		'}}'
	
	local result = frame:preprocess(queryWikitext)
	return trim(result)
end

-- Helper to count inputs/outputs for a specific location
local function countIOByLocation(frame, fullPageName, ioType, location)
	local locationFilter = ''
	if location then
		locationFilter = ' [[Has ' .. (ioType == 'input' and 'input' or 'output') .. ' location::' .. location .. ']]\n'
	end
	
	local signalProp = ioType == 'input' and 'Has video signal format' or 'Has output signal format'
	
	local countWikitext =
		'{{#ask:\n' ..
		' [[Has AV device::' .. fullPageName .. ']]\n' ..
		' [[' .. signalProp .. '::+]]\n' ..
		locationFilter ..
		' |format=count\n' ..
		'}}'
	
	local result = frame:preprocess(countWikitext)
	local count = tonumber(trim(result)) or 0
	return count
end

-- Helper to build location-grouped I/O content for a value cell
-- Returns HTML string with stripe groups per location, or nil if empty
local function buildLocationGroupedIO(frame, fullPageName, ioType, locations, locationLabels)
	local html = ''
	
	for _, loc in ipairs(locations) do
		local count = countIOByLocation(frame, fullPageName, ioType, loc)
		if count > 0 then
			local locContent = queryIOByLocation(frame, fullPageName, ioType, loc)
			if locContent and locContent ~= '' then
				html = html ..
					'<div class="infobox-io-location-group">' ..
					'<div class="infobox-io-location-header">' .. locationLabels[loc] .. '</div>' ..
					'<div class="infobox-io-location-content">' .. locContent .. '</div>' ..
					'</div>'
			end
		end
	end
	
	if html == '' then return nil end
	return html
end

function p.infobox(frame)
	local parent = frame:getParent()
	local args = parent and parent.args or frame.args

	local title = mw.title.getCurrentTitle()
	local pageName = title.text
	local fullPageName = title.fullText
	local pageId = title.id or 0

	-- Extract parameters
	local brand = trim(args.brand)
	local model_code = trim(args.model_code)
	local device_type = trim(args.device_type)
	local format_family = trim(args.format_family)
	local supported_formats = trim(args.supported_formats)
	local recording_formats = trim(args.recording_formats)
	local image_main = trim(args.image_main)
	local year_released = trim(args.year_released)
	local year_discontinued = trim(args.year_discontinued)
	local msrp = trim(args.msrp)
	local av_system = trim(args.av_system)
	local tuner_system = trim(args.tuner_system)
	local frequency = trim(args.frequency)
	local power_avg = trim(args.power_avg)
	local power_max = trim(args.power_max)
	local features = trim(args.features)
	local height = trim(args.height)
	local width = trim(args.width)
	local depth = trim(args.depth)
	local weight = trim(args.weight)

	-- Use frame:extensionTag to create a proper <style> tag
	local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template:Infobox styles.css'})

	local container = mw.html.create('div')
	container:addClass('infobox')
	container:addClass('infobox-av-device')
	css(container, 'float:right; clear:right; margin:0 0 1em 1em; width:22em; font-size:88%; line-height:1.5em; display:grid; grid-template-columns:auto 1fr; gap:3px; padding:3px;')

	-- Title row
	addFullRow(
		container,
		(brand or '') .. ' ' .. (model_code or ''),
		'text-align:center; font-size:125%; font-weight:bold; padding:0.5em;',
		false,
		'infobox-title'
	)

	if image_main then
		local file = image_main:gsub('^File:', '')
		addFullRow(
			container,
			string.format('[[File:%s|frameless|300px]]', file),
			'text-align:center; padding:0;',
			false,
			'infobox-image'
		)
	end

	addSectionRow(container, 'Overview')

	-- Brand row using #formredlink
	if brand then
		local brandWikitext = string.format(
			'{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}',
			brand, brand
		)
		local brandHtml = frame:preprocess(brandWikitext)
		addRow(container, 'Brand', brandHtml, true)
	end

	if model_code then
		addRow(container, 'Model', model_code, false)
	end

	if device_type then
		addRow(container, 'Type', '[[' .. device_type .. ']]', true)
	end

	-- Format family with link to category
	if format_family then
		addRow(container, 'Format family', '[[:Category:' .. format_family .. ' devices|' .. format_family .. ']]', true)
	end

	-- Year info
	if year_released or year_discontinued then
		local yearStr = ''
		if year_released and year_discontinued then
			yearStr = year_released .. ' – ' .. year_discontinued
		elseif year_released then
			yearStr = year_released .. ' –'
		elseif year_discontinued then
			yearStr = '– ' .. year_discontinued
		end
		addRow(container, 'Years', yearStr, false)
	end

	-- MSRP
	if msrp then
		addRow(container, 'MSRP', formatMSRP(msrp), false)
	end

	-- AV System support
	if av_system then
		local formattedAV = formatList(av_system, true)
		if formattedAV then
			addRow(container, 'AV system', formattedAV, false)
		end
	end

	-- ECID row
	local ecidWikitext = string.format('[{{fullurl:%s|curid=%d}} EC%d]', pageName, pageId, pageId)
	addRow(container, 'ECID', frame:preprocess(ecidWikitext), true)

	-- Format compatibility section (only show if tape-based device)
	if supported_formats or recording_formats then
		addSectionRow(container, 'Format Compatibility')
		
		if supported_formats then
			local formattedSupported = formatList(supported_formats, true)
			if formattedSupported then
				addRow(container, 'Plays', formattedSupported, false)
			end
		end
		
		if recording_formats then
			local formattedRecording = formatList(recording_formats, true)
			if formattedRecording then
				addRow(container, 'Records', formattedRecording, false)
			end
		end
	end

	-- Connectivity section - Inputs and Outputs as main rows,
	-- with location stripe groups inside each value cell
	local locations = {'front', 'rear', 'side'}
	local locationLabels = {front = 'Front', rear = 'Rear', side = 'Side'}
	
	local inputsHtml = buildLocationGroupedIO(frame, fullPageName, 'input', locations, locationLabels)
	local outputsHtml = buildLocationGroupedIO(frame, fullPageName, 'output', locations, locationLabels)

	if inputsHtml or outputsHtml then
		addSectionRow(container, 'Connectivity')
		
		-- Connectivity rows: label stretches full height (default grid behavior),
		-- value has no padding so location groups sit flush
		if inputsHtml then
			local labelDiv = container:tag('div')
			labelDiv:addClass('infobox-label')
			css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
			labelDiv:wikitext('Inputs')

			local valueDiv = container:tag('div')
			valueDiv:addClass('infobox-value')
			css(valueDiv, 'padding:0;')
			valueDiv:node(mw.html.create('span'):wikitext(inputsHtml))
		end
		
		if outputsHtml then
			local labelDiv = container:tag('div')
			labelDiv:addClass('infobox-label')
			css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
			labelDiv:wikitext('Outputs')

			local valueDiv = container:tag('div')
			valueDiv:addClass('infobox-value')
			css(valueDiv, 'padding:0;')
			valueDiv:node(mw.html.create('span'):wikitext(outputsHtml))
		end
	end

	-- Features section
	if features then
		addSectionRow(container, 'Features')
		addFullRow(container, features, 'padding:0.2em 0.4em;', false)
	end

	-- Dimensions section
	if height or width or depth or weight then
		addSectionRow(container, 'Dimensions')
		
		if height or width or depth then
			local h = height or '–'
			local w = width or '–'
			local d = depth or '–'
			addRow(container, 'H × W × D', string.format('%s × %s × %s', h, w, d), false)
		end
		
		if weight then
			local weightWikitext = '{{#show: ' .. fullPageName .. ' |?Weight # lb }} / {{#show: ' .. fullPageName .. ' |?Weight # kg }}'
			addRow(container, 'Weight', frame:preprocess(weightWikitext), true)
		end
	end

	-- Power section - query voltage ranges and display power specs
	local voltageQueryWikitext =
		'{{#ask:\n' ..
		' [[Has AV device::' .. pageName .. ']]\n' ..
		' [[Voltage min::+]]\n' ..
		' |?Voltage min=vmin\n' ..
		' |?Voltage max=vmax\n' ..
		' |format=template\n' ..
		' |template=AV device voltage row\n' ..
		' |named args=yes\n' ..
		' |link=none\n' ..
		'}}'
	local voltageResult = frame:preprocess(voltageQueryWikitext)
	local hasVoltage = voltageResult and trim(voltageResult) and trim(voltageResult) ~= ''

	if hasVoltage or frequency or power_avg or power_max then
		addSectionRow(container, 'Power')
		
		if hasVoltage then
			addRow(container, 'Voltage', voltageResult, true)
		end
		
		if frequency then
			local formattedFreq = formatList(frequency, true)
			if formattedFreq then
				addRow(container, 'AC frequency', formattedFreq, false)
			end
		end
		
		if power_avg or power_max then
			local powerStr = ''
			if power_avg and power_max then
				powerStr = power_avg .. 'W (avg) / ' .. power_max .. 'W (max)'
			elseif power_avg then
				powerStr = power_avg .. 'W'
			elseif power_max then
				powerStr = power_max .. 'W (max)'
			end
			addRow(container, 'Power', powerStr, false)
		end
	end

	-- Time Base Correction section - query AV TBC subobjects
	local tbcCountWikitext =
		'{{#ask:\n' ..
		' [[TBC type::+]]\n' ..
		' [[-Has subobject::' .. fullPageName .. ']]\n' ..
		' |format=count\n' ..
		'}}'
	local tbcCountResult = frame:preprocess(tbcCountWikitext)
	local hasTBC = tbcCountResult and tonumber(trim(tbcCountResult)) and tonumber(trim(tbcCountResult)) > 0

	if hasTBC then
		addSectionRow(container, 'Time Base Correction')

		local tbcQueryWikitext =
			'{{#ask:\n' ..
			' [[TBC type::+]]\n' ..
			' [[-Has subobject::' .. fullPageName .. ']]\n' ..
			' |?TBC type\n' ..
			' |?TBC buffer size\n' ..
			' |?TBC notes\n' ..
			' |format=broadtable\n' ..
			' |headers=hide\n' ..
			' |link=none\n' ..
			'}}'
		local tbcResult = frame:preprocess(tbcQueryWikitext)

		if tbcResult then
			local entryIndex = 0
			for rowContent in tbcResult:gmatch('<tr[^>]*>(.-)</tr>') do
				local cells = {}
				for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
					local cleanContent = cellContent:gsub('<[^>]+>', '')
					table.insert(cells, cleanContent)
				end

				if #cells >= 2 then
					local tbcType = trim(cells[2])
					local tbcBuffer = trim(cells[3])
					local tbcNotes = trim(cells[4])

					if tbcType then
						entryIndex = entryIndex + 1

						-- Create a full-width grouped container with nested grid
						local group = container:tag('div')
						group:addClass('infobox-tbc-group')
						css(group, 'grid-column: 1 / -1; display:grid; grid-template-columns:auto 1fr; gap:3px;')

						-- Type row
						local typeLabelDiv = group:tag('div')
						typeLabelDiv:addClass('infobox-label')
						css(typeLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
						typeLabelDiv:wikitext('Type')

						local typeValueDiv = group:tag('div')
						typeValueDiv:addClass('infobox-value')
						css(typeValueDiv, 'padding:0.2em 0.4em;')
						typeValueDiv:wikitext(tbcType)

						-- Buffer row (conditional)
						if tbcBuffer then
							local bufLabelDiv = group:tag('div')
							bufLabelDiv:addClass('infobox-label')
							css(bufLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
							bufLabelDiv:wikitext('Buffer')

							local bufValueDiv = group:tag('div')
							bufValueDiv:addClass('infobox-value')
							css(bufValueDiv, 'padding:0.2em 0.4em;')
							bufValueDiv:wikitext(tbcBuffer)
						end

						-- Notes row (conditional)
						if tbcNotes then
							local notesLabelDiv = group:tag('div')
							notesLabelDiv:addClass('infobox-label')
							css(notesLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
							notesLabelDiv:wikitext('Notes')

							local notesValueDiv = group:tag('div')
							notesValueDiv:addClass('infobox-value')
							css(notesValueDiv, 'padding:0.2em 0.4em; font-size:0.92em;')
							notesValueDiv:wikitext(tbcNotes)
						end
					end
				end
			end
		end
	end

	-- Accessories section: Always show (like Parts/Documents), with "Add an accessory" button
	addSectionRow(container, 'Accessories')

	-- Build the add accessory link with prefilled AV device
	local accTimestamp = os.time()
	math.randomseed(accTimestamp + pageId + 2)
	local accRandom = math.random(1000, 9999)
	local accTarget = accTimestamp .. "-" .. accRandom
	local addAccessoryUrl = mw.uri.fullUrl(
		"Special:FormEdit",
		{
			form = "Accessory",
			target = accTarget,
			["Accessory[av_devices]"] = pageName
		}
	)

	-- Query for non-remote accessories linked via Has related AV device
	local accessoriesCountWikitext =
		'{{#ask:\n' ..
		' [[Category:Accessories]]\n' ..
		' [[Has related AV device::' .. 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
		-- Query accessories and display grouped by type
		local accessoriesQueryWikitext =
			'{{#ask:\n' ..
			' [[Category:Accessories]]\n' ..
			' [[Has related AV device::' .. 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)

		-- Parse accessories from broadtable result and group by type
		local accessoriesByType = {}
		local typeOrder = {}

		if accessoriesResult then
			for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do
				local cells = {}
				for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
					local cleanContent = cellContent:gsub('<[^>]+>', '')
					table.insert(cells, trim(cleanContent))
				end

				-- cells[1] = page name, cells[2] = type, cells[3] = model code
				if #cells >= 2 then
					local accPageName = cells[1]
					local accType = cells[2] or 'Accessory'

					if accPageName then
						if not accessoriesByType[accType] then
							accessoriesByType[accType] = {}
							table.insert(typeOrder, accType)
						end

						table.insert(accessoriesByType[accType], {
							page = accPageName
						})
					end
				end
			end
		end

		-- Create a nested grid for accessory rows
		local accGrid = accDiv:tag('div')
		css(accGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;')

		-- Display as label/value rows grouped by type (like Parts)
		for _, accType in ipairs(typeOrder) do
			local accs = accessoriesByType[accType]
			if accs and #accs > 0 then
				local accLinks = {}
				for _, acc in ipairs(accs) do
					table.insert(accLinks, '[[' .. acc.page .. ']]')
				end

				local labelDiv = accGrid:tag('div')
				labelDiv:addClass('infobox-label')
				css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
				labelDiv:wikitext(accType)

				local valueDiv = accGrid:tag('div')
				valueDiv:addClass('infobox-value')
				css(valueDiv, 'padding:0.2em 0.4em;')
				valueDiv:wikitext(table.concat(accLinks, '<br>'))
			end
		end

		-- Add "Add an accessory" button
		local addAccDiv = mw.html.create('div')
		addAccDiv:css('margin-top', '0.5em'):css('text-align', 'center')
		addAccDiv:wikitext('[' .. tostring(addAccessoryUrl) .. ' <span class="infobox-button infobox-button-secondary">Add an accessory</span>]')
		accDiv:node(addAccDiv)
	else
		-- Just show the add accessory link as a button
		local addAccDiv = mw.html.create('div')
		addAccDiv:css('text-align', 'center')
		addAccDiv:wikitext('[' .. tostring(addAccessoryUrl) .. ' <span class="infobox-button infobox-button-secondary">Add an accessory</span>]')
		accDiv:node(addAccDiv)
	end

	-- Parts section
	addSectionRow(container, 'Parts')
	
	-- Build the add part link with prefilled AV device
	local partTimestamp = os.time()
	math.randomseed(partTimestamp + pageId + 1)
	local partRandom = math.random(1000, 9999)
	local partTarget = partTimestamp .. "-" .. partRandom
	local addPartUrl = mw.uri.fullUrl(
		"Special:FormEdit",
		{
			form = "Part",
			target = partTarget,
			["Part[av_devices]"] = pageName
		}
	)
	
	-- Query for parts linked via Has related AV device
	local partsCountWikitext =
		'{{#ask:\n' ..
		' [[Category:Parts]]\n' ..
		' [[Has related AV device::' .. fullPageName .. ']]\n' ..
		' |format=count\n' ..
		'}}'
	local partsCount = frame:preprocess(partsCountWikitext)
	local hasParts = partsCount and tonumber(trim(partsCount)) and tonumber(trim(partsCount)) > 0
	
	local partsDiv = container:tag('div')
	css(partsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')
	
	if hasParts then
		-- Query parts and display grouped by type/subtype
		local partsQueryWikitext =
			'{{#ask:\n' ..
			' [[Category:Parts]]\n' ..
			' [[Has related AV device::' .. fullPageName .. ']]\n' ..
			' |?Has part type=type\n' ..
			' |?Has part subtype=subtype\n' ..
			' |?Has model code=model\n' ..
			' |format=broadtable\n' ..
			' |headers=hide\n' ..
			' |link=none\n' ..
			'}}'
		local partsResult = frame:preprocess(partsQueryWikitext)
		
		-- Parse parts from broadtable result and group by type
		local partsByType = {}
		local partTypeOrder = {}
		
		if partsResult then
			for rowContent in partsResult:gmatch('<tr[^>]*>(.-)</tr>') do
				local cells = {}
				for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
					local cleanContent = cellContent:gsub('<[^>]+>', '')
					table.insert(cells, trim(cleanContent))
				end
				
				-- cells[1] = page name, cells[2] = type, cells[3] = subtype, cells[4] = model code
				if #cells >= 2 then
					local partPageName = cells[1]
					local partType = cells[2] or 'Part'
					local partSubtype = cells[3]
					
					-- Use subtype + type as the grouping key if subtype exists
					-- Keep proper capitalization (e.g., "Frame synchronizer IC" not "Frame synchronizer ic")
					local groupKey = partType
					if partSubtype and partSubtype ~= '' then
						groupKey = partSubtype .. ' ' .. partType
					end
					
					if partPageName then
						-- Track type order for consistent display
						if not partsByType[groupKey] then
							partsByType[groupKey] = {}
							table.insert(partTypeOrder, groupKey)
						end
						
						table.insert(partsByType[groupKey], {
							page = partPageName
						})
					end
				end
			end
		end
		
		-- Create a nested grid for part rows
		local partsGrid = partsDiv:tag('div')
		css(partsGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;')
		
		-- Display as label/value rows grouped by type
		for _, partType in ipairs(partTypeOrder) do
			local parts = partsByType[partType]
			if parts and #parts > 0 then
				local partLinks = {}
				for _, part in ipairs(parts) do
					-- Just show the page name (which is "Brand Model"), no description
					table.insert(partLinks, '[[' .. part.page .. ']]')
				end
				
				local labelDiv = partsGrid:tag('div')
				labelDiv:addClass('infobox-label')
				css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
				labelDiv:wikitext(partType)
				
				local valueDiv = partsGrid:tag('div')
				valueDiv:addClass('infobox-value')
				css(valueDiv, 'padding:0.2em 0.4em;')
				valueDiv:wikitext(table.concat(partLinks, '<br>'))
			end
		end
		
		-- Add "Add a part" button
		local addPartDiv = mw.html.create('div')
		addPartDiv:css('margin-top', '0.5em'):css('text-align', 'center')
		addPartDiv:wikitext('[' .. tostring(addPartUrl) .. ' <span class="infobox-button infobox-button-secondary">Add a part</span>]')
		partsDiv:node(addPartDiv)
	else
		-- Just show the add part link as a button
		local addPartDiv = mw.html.create('div')
		addPartDiv:css('text-align', 'center')
		addPartDiv:wikitext('[' .. tostring(addPartUrl) .. ' <span class="infobox-button infobox-button-secondary">Add a part</span>]')
		partsDiv:node(addPartDiv)
	end

	-- Archival Links section: Query for link subobjects and display by type
	local linksQueryWikitext =
		'{{#ask:\n' ..
		' [[AV device::' .. fullPageName .. ']]\n' ..
		' |?Model link type\n' ..
		' |?Model link source\n' ..
		' |?Model link URL\n' ..
		' |format=broadtable\n' ..
		' |headers=hide\n' ..
		' |link=none\n' ..
		'}}'
	local linksResult = frame:preprocess(linksQueryWikitext)
	
	-- Parse links and organize by type
	local linksByType = {
		Manufacturer = {},
		Retailer = {},
		Database = {}
	}
	local hasLinks = false
	
	if linksResult then
		for rowContent in linksResult:gmatch('<tr[^>]*>(.-)</tr>') do
			local cells = {}
			for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
				local cleanContent = cellContent:gsub('<[^>]+>', '')
				table.insert(cells, trim(cleanContent))
			end
			
			-- cells[1] = page/subobject, cells[2] = type, cells[3] = source, cells[4] = URL
			if #cells >= 4 then
				local linkType = cells[2]
				local linkSource = cells[3]
				local linkUrl = cells[4]
				
				if linkType and linkSource and linkUrl and linkUrl ~= '' then
					if linksByType[linkType] then
						table.insert(linksByType[linkType], {source = linkSource, url = linkUrl})
						hasLinks = true
					end
				end
			end
		end
	end
	
	if hasLinks then
		addSectionRow(container, 'Archival Links')
		
		local linkTypeOrder = {'Database', 'Manufacturer', 'Retailer'}
		for _, linkType in ipairs(linkTypeOrder) do
			if #linksByType[linkType] > 0 then
				local linksHtml = {}
				for _, link in ipairs(linksByType[linkType]) do
					table.insert(linksHtml, '[' .. link.url .. ' ' .. link.source .. ']')
				end
				addRow(container, linkType, table.concat(linksHtml, '<br>'), true)
			end
		end
	end

	-- Documents section
	addSectionRow(container, 'Documents')
	
	local timestamp = os.time()
	math.randomseed(timestamp + pageId)
	local random = math.random(1000, 9999)
	local target = "Document:" .. timestamp .. "-" .. random
	local uploadUrl = mw.uri.fullUrl(
		"Special:FormEdit",
		{
			form = "Document",
			target = target,
			["Document[av_devices]"] = pageName
		}
	)
	
	-- Query for document count
	local documentsCountWikitext =
		'{{#ask:\n' ..
		' [[Category:Documents]]\n' ..
		' [[Has related AV device::' .. fullPageName .. ']]\n' ..
		' |format=count\n' ..
		'}}'
	local documentCount = frame:preprocess(documentsCountWikitext)
	local hasDocuments = documentCount and tonumber(trim(documentCount)) and tonumber(trim(documentCount)) > 0
	
	local docsDiv = container:tag('div')
	css(docsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')
	
	if hasDocuments then
		local docsQueryWikitext =
			'{{#ask:\n' ..
			' [[Category:Documents]]\n' ..
			' [[Has related AV device::' .. fullPageName .. ']]\n' ..
			' |?Has document type\n' ..
			' |?Has document URL\n' ..
			' |?Has document file\n' ..
			' |?Has document title\n' ..
			' |format=broadtable\n' ..
			' |headers=hide\n' ..
			' |link=none\n' ..
			'}}'
		local docsResult = frame:preprocess(docsQueryWikitext)
		
		local ul = mw.html.create('ul')
		ul:css('margin', '0'):css('padding-left', '1.5em')
		
		if docsResult then
			for rowContent in docsResult:gmatch('<tr[^>]*>(.-)</tr>') do
				local cells = {}
				for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
					local cleanContent = cellContent:gsub('<[^>]+>', '')
					table.insert(cells, cleanContent)
				end
				
				if #cells >= 2 then
					local docType = trim(cells[2])
					local docUrl = trim(cells[3])
					local docFile = trim(cells[4])
					local docTitle = trim(cells[5])
					local docPageName = trim(cells[1])
					
					if docPageName then
						local displayText = 'Document'
						if docTitle then
							displayText = docTitle
						elseif docType then
							displayText = docType
						end
						
						local li = ul:tag('li')
						li:wikitext('[[' .. docPageName .. '|' .. displayText .. ']]')
					end
				end
			end
		end
		
		docsDiv:node(ul)
		
		local uploadDiv = mw.html.create('div')
		uploadDiv:css('margin-top', '0.5em'):css('text-align', 'center')
		uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add a document</span>]')
		
		docsDiv:node(uploadDiv)
	else
		local uploadDiv = mw.html.create('div')
		uploadDiv:css('text-align', 'center')
		uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add a document</span>]')
		
		docsDiv:node(uploadDiv)
	end

	-- Contribute section
	addSectionRow(container, 'Contribute')
	
	local editUrl = mw.uri.fullUrl(
		"Special:FormStart",
		{
			form = "AV device",
			page_name = pageName
		}
	)
	
	local editDiv = container:tag('div')
	css(editDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em; text-align:center;')
	editDiv:wikitext('[' .. tostring(editUrl) .. ' <span class="infobox-button infobox-button-primary">Edit this data</span>]')

	return styleTag .. tostring(container)
end

return p
MediaWiki Appliance - Powered by TurnKey Linux