Module:AVDevice

Revision as of 04:22, 25 February 2026 by Arclight-MCP-Bot (talk | contribs) (Rename args.oem_manufacturer to args.oem to match CRT model convention (via update-page on MediaWiki MCP Server))

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

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

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

local function formatMSRP(value)
	if not value then return nil end
	local num = tonumber(value)
	if not num then return '$' .. value end
	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

local function formatRelatedModels(value, deviceBrand)
	if not value then return nil end
	local items = {}
	for item in value:gmatch('[^,]+') do
		local trimmed = trim(item)
		if trimmed then
			local displayText = getDisplayTextWithoutBrand(trimmed, deviceBrand)
			table.insert(items, '[[' .. trimmed .. '|' .. displayText .. ']]')
		end
	end
	if #items == 0 then return nil end
	return table.concat(items, '<br>')
end

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

local function discoverLocations(frame, fullPageName, ioType)
	local locationProp, signalProp
	if ioType == 'input' then
		locationProp = 'Has input location'
		signalProp = 'Has video signal format'
	else
		locationProp = 'Has output location'
		signalProp = 'Has output signal format'
	end
	local queryWikitext =
		'{{#ask:\n' ..
		' [[Has AV device::' .. fullPageName .. ']]\n' ..
		' [[' .. signalProp .. '::+]]\n' ..
		' [[' .. locationProp .. '::+]]\n' ..
		' |?' .. locationProp .. '=location\n' ..
		' |format=broadtable\n' ..
		' |headers=hide\n' ..
		' |link=none\n' ..
		'}}'
	local result = frame:preprocess(queryWikitext)
	local seen = {}
	local locations = {}
	local preferredOrder = {rear = 1, front = 2, side = 3, top = 4, bottom = 5, internal = 6}
	if result then
		for rowContent in result:gmatch('<tr[^>]*>(.-)</tr>') do
			local cells = {}
			for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
				local cleanContent = cellContent:gsub('<[^>]+>', '')
				table.insert(cells, trim(cleanContent))
			end
			if #cells >= 2 and cells[2] then
				local loc = mw.text.trim(cells[2])
				local locLower = loc:lower()
				if locLower ~= '' and not seen[locLower] then
					seen[locLower] = true
					table.insert(locations, locLower)
				end
			end
		end
	end
	table.sort(locations, function(a, b)
		local orderA = preferredOrder[a] or 1000
		local orderB = preferredOrder[b] or 1000
		if orderA ~= orderB then return orderA < orderB end
		return a < b
	end)
	return locations
end

local function locationLabel(loc)
	if not loc or loc == '' then return '' end
	return loc:sub(1, 1):upper() .. loc:sub(2)
end

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

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

	local brand = trim(args.brand)
	local model_code = trim(args.model_code)
	local oem_manufacturer = trim(args.oem)
	local device_type = trim(args.device_type)
	local format_family = trim(args.format_family)
	local market = trim(args.market)
	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)
	local related_models = trim(args.related_models)

	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;')

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

	local badgeWikitext = '[[AV Device Search|AV device]]'
	if device_type then
		local typeUrl = tostring(mw.uri.fullUrl('AV Device Search', {device_type = device_type}))
		badgeWikitext = badgeWikitext .. ' › <span class="plainlinks">[' .. typeUrl .. ' ' .. device_type .. ']</span>'
	end
	addFullRow(container, badgeWikitext, '', false, 'infobox-type-badge')

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

	addSectionRow(container, 'Overview')

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

	if oem_manufacturer then
		local oemWikitext = string.format('{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', oem_manufacturer, oem_manufacturer)
		addRow(container, 'OEM', frame:preprocess(oemWikitext), true)
	end

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

	if device_type then
		local typeSearchUrl = tostring(mw.uri.fullUrl('AV Device Search', {device_type = device_type}))
		addRow(container, 'Type', '<span class="plainlinks">[' .. typeSearchUrl .. ' ' .. device_type .. ']</span>', true)
	end

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

	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

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

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

	if related_models then
		local formattedRelated = formatRelatedModels(related_models, brand)
		if formattedRelated then addRow(container, 'Related', formattedRelated, true) end
	end

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

	if supported_formats or recording_formats then
		addSectionRow(container, 'Format Compatibility')
		if supported_formats then
			local f = formatList(supported_formats, true)
			if f then addRow(container, 'Plays', f, false) end
		end
		if recording_formats then
			local f = formatList(recording_formats, true)
			if f then addRow(container, 'Records', f, false) end
		end
	end

	local function renderIOSection(sectionTitle, ioType)
		local locations = discoverLocations(frame, fullPageName, ioType)
		local locationData = {}
		for _, loc in ipairs(locations) do
			local content = queryIOByLocation(frame, fullPageName, ioType, loc)
			if content and content ~= '' then
				table.insert(locationData, {location = loc, content = content})
			end
		end
		if #locationData > 0 then
			addSectionRow(container, sectionTitle)
			for _, data in ipairs(locationData) do
				local labelDiv = container:tag('div')
				labelDiv:addClass('infobox-label')
				css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
				labelDiv:wikitext(locationLabel(data.location))
				local valueDiv = container:tag('div')
				valueDiv:addClass('infobox-value')
				css(valueDiv, 'padding:0.2em 0.4em;')
				valueDiv:node(mw.html.create('span'):wikitext(data.content))
			end
		end
	end

	renderIOSection('Inputs', 'input')
	renderIOSection('Outputs', 'output')

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

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

	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

	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
			for rowContent in tbcResult:gmatch('<tr[^>]*>(.-)</tr>') do
				local cells = {}
				for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
					table.insert(cells, cellContent:gsub('<[^>]+>', ''))
				end
				if #cells >= 2 then
					local tbcType = trim(cells[2])
					local tbcBuffer = trim(cells[3])
					local tbcNotes = trim(cells[4])
					if tbcType then
						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;')
						local tl = group:tag('div'):addClass('infobox-label')
						css(tl, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
						tl:wikitext('Type')
						local tv = group:tag('div'):addClass('infobox-value')
						css(tv, 'padding:0.2em 0.4em;')
						tv:wikitext(tbcType)
						if tbcBuffer then
							local bl = group:tag('div'):addClass('infobox-label')
							css(bl, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
							bl:wikitext('Buffer')
							local bv = group:tag('div'):addClass('infobox-value')
							css(bv, 'padding:0.2em 0.4em;')
							bv:wikitext(tbcBuffer)
						end
						if tbcNotes then
							local nl = group:tag('div'):addClass('infobox-label')
							css(nl, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
							nl:wikitext('Notes')
							local nv = group:tag('div'):addClass('infobox-value')
							css(nv, 'padding:0.2em 0.4em; font-size:0.92em;')
							nv:wikitext(tbcNotes)
						end
					end
				end
			end
		end
	end

	-- Accessories section
	addSectionRow(container, 'Accessories')
	local addAccessoryUrl = mw.uri.fullUrl("Create an accessory", {av_devices = pageName})
	local accessoriesCountWikitext = '{{#ask:\n [[Category:Accessories]]\n [[Has related AV device::' .. fullPageName .. ']]\n |format=count\n}}'
	local accessoryCount = frame:preprocess(accessoriesCountWikitext)
	local hasAccessories = accessoryCount and tonumber(trim(accessoryCount)) and tonumber(trim(accessoryCount)) > 0
	local accDiv = container:tag('div')
	css(accDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;')

	if hasAccessories then
		local accessoriesQueryWikitext = '{{#ask:\n [[Category:Accessories]]\n [[Has related AV device::' .. fullPageName .. ']]\n |?Has accessory type=type\n |?Has model code=model\n |format=broadtable\n |headers=hide\n |link=none\n}}'
		local accessoriesResult = frame:preprocess(accessoriesQueryWikitext)
		local accessoriesByType = {}
		local typeOrder = {}
		if accessoriesResult then
			for rowContent in accessoriesResult:gmatch('<tr[^>]*>(.-)</tr>') do
				local cells = {}
				for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do
					table.insert(cells, trim(cellContent:gsub('<[^>]+>', '')))
				end
				if #cells >= 2 then
					local accPageName = cells[1]
					local accType = cells[2] or 'Accessory'
					if accPageName then
						if not accessoriesByType[accType] then
							accessoriesByType[accType] = {}
							table.insert(typeOrder, accType)
						end
						table.insert(accessoriesByType[accType], {page = accPageName})
					end
				end
			end
		end
		local accGrid = accDiv:tag('div')
		css(accGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;')
		for _, accType in ipairs(typeOrder) do
			local accs = accessoriesByType[accType]
			if accs and #accs > 0 then
				local accLinks = {}
				for _, acc in ipairs(accs) do table.insert(accLinks, '[[' .. acc.page .. ']]') end
				local ld = accGrid:tag('div'):addClass('infobox-label')
				css(ld, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
				ld:wikitext(accType)
				local vd = accGrid:tag('div'):addClass('infobox-value')
				css(vd, 'padding:0.2em 0.4em;')
				vd:wikitext(table.concat(accLinks, '<br>'))
			end
		end
		local addAccDiv = mw.html.create('div')
		addAccDiv:css('margin-top', '0.5em'):css('text-align', 'center')
		addAccDiv:wikitext('[' .. tostring(addAccessoryUrl) .. ' <span class="infobox-button infobox-button-secondary">Add accessory</span>]')
		accDiv:node(addAccDiv)
	else
		local addAccDiv = mw.html.create('div')
		addAccDiv:css('text-align', 'center')
		addAccDiv:wikitext('[' .. tostring(addAccessoryUrl) .. ' <span class="infobox-button infobox-button-secondary">Add accessory</span>]')
		accDiv:node(addAccDiv)
	end

	-- Parts section
	addSectionRow(container, 'Parts')
	local addPartUrl = mw.uri.fullUrl("Create a part", {av_devices = pageName})
	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
		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)
		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
					table.insert(cells, trim(cellContent:gsub('<[^>]+>', '')))
				end
				if #cells >= 2 then
					local partPageName = cells[1]
					local partType = cells[2] or 'Part'
					local partSubtype = cells[3]
					local groupKey = partType
					if partSubtype and partSubtype ~= '' then groupKey = partSubtype .. ' ' .. partType end
					if partPageName then
						if not partsByType[groupKey] then
							partsByType[groupKey] = {}
							table.insert(partTypeOrder, groupKey)
						end
						table.insert(partsByType[groupKey], {page = partPageName})
					end
				end
			end
		end
		local partsGrid = partsDiv:tag('div')
		css(partsGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;')
		for _, partType in ipairs(partTypeOrder) do
			local parts = partsByType[partType]
			if parts and #parts > 0 then
				local partLinks = {}
				for _, part in ipairs(parts) do table.insert(partLinks, '[[' .. part.page .. ']]') end
				local ld = partsGrid:tag('div'):addClass('infobox-label')
				css(ld, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;')
				ld:wikitext(partType)
				local vd = partsGrid:tag('div'):addClass('infobox-value')
				css(vd, 'padding:0.2em 0.4em;')
				vd:wikitext(table.concat(partLinks, '<br>'))
			end
		end
		local addPartDiv = mw.html.create('div')
		addPartDiv:css('margin-top', '0.5em'):css('text-align', 'center')
		addPartDiv:wikitext('[' .. tostring(addPartUrl) .. ' <span class="infobox-button infobox-button-secondary">Add part</span>]')
		partsDiv:node(addPartDiv)
	else
		local addPartDiv = mw.html.create('div')
		addPartDiv:css('text-align', 'center')
		addPartDiv:wikitext('[' .. tostring(addPartUrl) .. ' <span class="infobox-button infobox-button-secondary">Add part</span>]')
		partsDiv:node(addPartDiv)
	end

	-- Archival Links section
	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)
	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
				table.insert(cells, trim(cellContent:gsub('<[^>]+>', '')))
			end
			if #cells >= 4 then
				local linkType = cells[2]
				local linkSource = cells[3]
				local linkUrl = cells[4]
				if linkType and linkSource and linkUrl and linkUrl ~= '' and linksByType[linkType] then
					table.insert(linksByType[linkType], {source = linkSource, url = linkUrl})
					hasLinks = true
				end
			end
		end
	end
	if hasLinks then
		addSectionRow(container, 'Archival Links')
		for _, linkType in ipairs({'Database', 'Manufacturer', 'Retailer'}) 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})
	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
					table.insert(cells, cellContent:gsub('<[^>]+>', ''))
				end
				if #cells >= 2 then
					local docType = trim(cells[2])
					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
						ul:tag('li'):wikitext('[[' .. docPageName .. '|' .. displayText .. ']]')
					end
				end
			end
		end
		docsDiv:node(ul)
		local uploadDiv = mw.html.create('div')
		uploadDiv:css('margin-top', '0.5em'):css('text-align', 'center')
		uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]')
		docsDiv:node(uploadDiv)
	else
		local uploadDiv = mw.html.create('div')
		uploadDiv:css('text-align', 'center')
		uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]')
		docsDiv:node(uploadDiv)
	end

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