Module:AVDevice: Difference between revisions
Fix Lua error at line 612: wrap bare gsub() calls in parentheses to prevent multi-return value issue with table.insert (TBC + Documents sections) (via update-page on MediaWiki MCP Server) |
Restore pre-compression source from rev 8199 (with comments, formatting, safe gsub patterns). Only change: args.oem_manufacturer → args.oem (the intended rename from rev 8313, without the destructive compression) (via update-page on MediaWiki MCP Server) |
||
| Line 58: | Line 58: | ||
end | end | ||
-- Helper function to strip brand prefix from item name if it matches device brand | |||
local function getDisplayTextWithoutBrand(itemName, deviceBrand) | local function getDisplayTextWithoutBrand(itemName, deviceBrand) | ||
if not itemName then return nil end | if not itemName then return nil end | ||
if not deviceBrand then return itemName end | if not deviceBrand then return itemName end | ||
local brandPrefix = deviceBrand .. ' ' | local brandPrefix = deviceBrand .. ' ' | ||
if itemName:sub(1, #brandPrefix) == brandPrefix then | if itemName:sub(1, #brandPrefix) == brandPrefix then | ||
return itemName:sub(#brandPrefix + 1) | return itemName:sub(#brandPrefix + 1) | ||
end | end | ||
return itemName | return itemName | ||
end | end | ||
-- Helper function to format comma-separated values as a bulleted list or inline | |||
local function formatList(value, inline) | local function formatList(value, inline) | ||
if not value then return nil end | if not value then return nil end | ||
| Line 73: | Line 77: | ||
for item in value:gmatch('[^,]+') do | for item in value:gmatch('[^,]+') do | ||
local trimmed = trim(item) | local trimmed = trim(item) | ||
if trimmed then table.insert(items, trimmed) end | if trimmed then | ||
table.insert(items, trimmed) | |||
end | |||
end | end | ||
if #items == 0 then return nil end | if #items == 0 then return nil end | ||
| Line 81: | Line 87: | ||
local ul = mw.html.create('ul') | local ul = mw.html.create('ul') | ||
ul:css('margin', '0'):css('padding-left', '1.2em') | ul:css('margin', '0'):css('padding-left', '1.2em') | ||
for _, item in ipairs(items) do ul:tag('li'):wikitext(item) end | for _, item in ipairs(items) do | ||
ul:tag('li'):wikitext(item) | |||
end | |||
return tostring(ul) | return tostring(ul) | ||
end | end | ||
end | end | ||
-- Helper to format MSRP with dollar sign and commas | |||
local function formatMSRP(value) | local function formatMSRP(value) | ||
if not value then return nil end | if not value then return nil end | ||
local num = tonumber(value) | local num = tonumber(value) | ||
if not num then return '$' .. value end | if not num then return '$' .. value end | ||
-- Format with commas | |||
local formatted = string.format("%.2f", num) | local formatted = string.format("%.2f", num) | ||
local intPart, decPart = formatted:match("^(%d+)%.(%d+)$") | local intPart, decPart = formatted:match("^(%d+)%.(%d+)$") | ||
| Line 100: | Line 110: | ||
end | end | ||
-- Helper to format related models as wiki links, stripping brand prefix | |||
local function formatRelatedModels(value, deviceBrand) | local function formatRelatedModels(value, deviceBrand) | ||
if not value then return nil end | if not value then return nil end | ||
| Line 114: | Line 125: | ||
end | end | ||
-- Helper to query inputs/outputs for a specific location | |||
local function queryIOByLocation(frame, fullPageName, ioType, location) | local function queryIOByLocation(frame, fullPageName, ioType, location) | ||
local locationFilter = '' | local locationFilter = '' | ||
| Line 119: | Line 131: | ||
locationFilter = ' [[Has ' .. (ioType == 'input' and 'input' or 'output') .. ' location::' .. location .. ']]\n' | locationFilter = ' [[Has ' .. (ioType == 'input' and 'input' or 'output') .. ' location::' .. location .. ']]\n' | ||
end | end | ||
local signalProp = ioType == 'input' and 'Has video signal format' or 'Has output signal format' | 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 countProp = ioType == 'input' and 'Has input count' or 'Has output count' | ||
| Line 125: | Line 138: | ||
local labelProp = ioType == 'input' and 'Has input label' or 'Has output label' | 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 templateName = ioType == 'input' and 'AV device input row' or 'AV device output row' | ||
-- Build audio property queries (only for inputs) | |||
local audioProps = '' | local audioProps = '' | ||
if ioType == 'input' then | if ioType == 'input' then | ||
| Line 131: | Line 146: | ||
' |?Has audio shared with=audio_shared\n' | ' |?Has audio shared with=audio_shared\n' | ||
end | end | ||
local queryWikitext = | local queryWikitext = | ||
'{{#ask:\n' .. | '{{#ask:\n' .. | ||
| Line 147: | Line 163: | ||
' |link=none\n' .. | ' |link=none\n' .. | ||
'}}' | '}}' | ||
local result = frame:preprocess(queryWikitext) | local result = frame:preprocess(queryWikitext) | ||
return trim(result) | return trim(result) | ||
end | end | ||
-- Helper to discover all distinct location values for a given IO type | |||
-- Returns an ordered list of location strings, sorted with common locations first | |||
local function discoverLocations(frame, fullPageName, ioType) | local function discoverLocations(frame, fullPageName, ioType) | ||
local locationProp, signalProp | local locationProp, signalProp | ||
| Line 156: | Line 175: | ||
locationProp = 'Has input location' | locationProp = 'Has input location' | ||
signalProp = 'Has video signal format' | signalProp = 'Has video signal format' | ||
else | else -- output | ||
locationProp = 'Has output location' | locationProp = 'Has output location' | ||
signalProp = 'Has output signal format' | signalProp = 'Has output signal format' | ||
end | end | ||
local queryWikitext = | local queryWikitext = | ||
'{{#ask:\n' .. | '{{#ask:\n' .. | ||
| Line 170: | Line 190: | ||
' |link=none\n' .. | ' |link=none\n' .. | ||
'}}' | '}}' | ||
local result = frame:preprocess(queryWikitext) | local result = frame:preprocess(queryWikitext) | ||
-- Parse distinct locations from broadtable rows | |||
local seen = {} | local seen = {} | ||
local locations = {} | local locations = {} | ||
-- Preferred display order for common locations | |||
local preferredOrder = {rear = 1, front = 2, side = 3, top = 4, bottom = 5, internal = 6} | local preferredOrder = {rear = 1, front = 2, side = 3, top = 4, bottom = 5, internal = 6} | ||
if result then | if result then | ||
for rowContent in result:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in result:gmatch('<tr[^>]*>(.-)</tr>') do | ||
| Line 181: | Line 207: | ||
table.insert(cells, trim(cleanContent)) | table.insert(cells, trim(cleanContent)) | ||
end | end | ||
-- cells[1] = subobject name, cells[2] = location value | |||
if #cells >= 2 and cells[2] then | if #cells >= 2 and cells[2] then | ||
local loc = mw.text.trim(cells[2]) | local loc = mw.text.trim(cells[2]) | ||
-- Normalize to lowercase for deduplication | |||
local locLower = loc:lower() | local locLower = loc:lower() | ||
if locLower ~= '' and not seen[locLower] then | if locLower ~= '' and not seen[locLower] then | ||
| Line 191: | Line 219: | ||
end | end | ||
end | end | ||
-- Sort: preferred locations first (in defined order), then remaining alphabetically | |||
table.sort(locations, function(a, b) | table.sort(locations, function(a, b) | ||
local orderA = preferredOrder[a] or 1000 | local orderA = preferredOrder[a] or 1000 | ||
local orderB = preferredOrder[b] or 1000 | local orderB = preferredOrder[b] or 1000 | ||
if orderA ~= orderB then return orderA < orderB end | if orderA ~= orderB then | ||
return orderA < orderB | |||
end | |||
return a < b | return a < b | ||
end) | end) | ||
return locations | return locations | ||
end | end | ||
-- Capitalize first letter of a location string for display | |||
local function locationLabel(loc) | local function locationLabel(loc) | ||
if not loc or loc == '' then return '' end | if not loc or loc == '' then return '' end | ||
| Line 214: | Line 248: | ||
local pageId = title.id or 0 | local pageId = title.id or 0 | ||
-- Extract parameters | |||
local brand = trim(args.brand) | local brand = trim(args.brand) | ||
local model_code = trim(args.model_code) | local model_code = trim(args.model_code) | ||
| Line 238: | Line 273: | ||
local related_models = trim(args.related_models) | local related_models = trim(args.related_models) | ||
-- Use frame:extensionTag to create a proper <style> tag | |||
local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template:Infobox styles.css'}) | local styleTag = frame:extensionTag('templatestyles', '', {src = 'Template:Infobox styles.css'}) | ||
| Line 245: | Line 281: | ||
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;') | 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') | -- Title row | ||
addFullRow( | |||
container, | |||
(brand or '') .. ' ' .. (model_code or ''), | |||
'text-align:center; font-size:125%; font-weight:bold; padding:0.5em;', | |||
false, | |||
'infobox-title' | |||
) | |||
-- Type badge (breadcrumb) — plainlinks hides external icon, $wgNoFollowDomainExceptions handles nofollow | |||
local badgeWikitext = '[[AV Device Search|AV device]]' | local badgeWikitext = '[[AV Device Search|AV device]]' | ||
if device_type then | if device_type then | ||
| Line 256: | Line 300: | ||
if image_main then | if image_main then | ||
local file = image_main:gsub('^File:', '') | local file = image_main:gsub('^File:', '') | ||
addFullRow(container, string.format('[[File:%s|frameless|300px]]', file), 'text-align:center; padding:0;', false, 'infobox-image') | addFullRow( | ||
container, | |||
string.format('[[File:%s|frameless|300px]]', file), | |||
'text-align:center; padding:0;', | |||
false, | |||
'infobox-image' | |||
) | |||
end | end | ||
addSectionRow(container, 'Overview') | addSectionRow(container, 'Overview') | ||
-- Brand row using #formredlink | |||
if brand then | if brand then | ||
local brandWikitext = string.format('{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', brand, brand) | local brandWikitext = string.format( | ||
addRow(container, 'Brand', | '{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', | ||
brand, brand | |||
) | |||
local brandHtml = frame:preprocess(brandWikitext) | |||
addRow(container, 'Brand', brandHtml, true) | |||
end | end | ||
-- OEM row (shown only when set) | |||
if oem_manufacturer then | if oem_manufacturer then | ||
local oemWikitext = string.format('{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', oem_manufacturer, oem_manufacturer) | local oemWikitext = string.format( | ||
addRow(container, 'OEM', | '{{#formredlink: target=%s |form=Manufacturer |existing page link text=%s }}', | ||
oem_manufacturer, oem_manufacturer | |||
) | |||
local oemHtml = frame:preprocess(oemWikitext) | |||
addRow(container, 'OEM', oemHtml, true) | |||
end | |||
if model_code then | |||
addRow(container, 'Model', model_code, false) | |||
end | end | ||
-- Market row | |||
if market then addRow(container, 'Market', market, false) end | if market then | ||
addRow(container, 'Market', market, false) | |||
end | |||
-- Type row: link to AV Device Search with filter, not to a (possibly non-existent) article | |||
if device_type then | if device_type then | ||
local typeSearchUrl = tostring(mw.uri.fullUrl('AV Device Search', {device_type = device_type})) | local typeSearchUrl = tostring(mw.uri.fullUrl('AV Device Search', {device_type = device_type})) | ||
local typeWikitext = '<span class="plainlinks">[' .. typeSearchUrl .. ' ' .. device_type .. ']</span>' | |||
addRow(container, 'Type', typeWikitext, true) | |||
end | end | ||
-- Format family with link to category | |||
if format_family then | if format_family then | ||
addRow(container, 'Format family', '[[:Category:' .. format_family .. ' devices|' .. format_family .. ']]', true) | addRow(container, 'Format family', '[[:Category:' .. format_family .. ' devices|' .. format_family .. ']]', true) | ||
end | end | ||
-- Year info | |||
if year_released or year_discontinued then | if year_released or year_discontinued then | ||
local yearStr = '' | local yearStr = '' | ||
if year_released and year_discontinued then yearStr = year_released .. ' – ' .. year_discontinued | if year_released and year_discontinued then | ||
elseif year_released then yearStr = year_released .. ' –' | yearStr = year_released .. ' – ' .. year_discontinued | ||
elseif year_discontinued then yearStr = '– ' .. year_discontinued end | elseif year_released then | ||
yearStr = year_released .. ' –' | |||
elseif year_discontinued then | |||
yearStr = '– ' .. year_discontinued | |||
end | |||
addRow(container, 'Years', yearStr, false) | addRow(container, 'Years', yearStr, false) | ||
end | end | ||
if msrp then addRow(container, 'MSRP', formatMSRP(msrp), false) end | -- MSRP | ||
if msrp then | |||
addRow(container, 'MSRP', formatMSRP(msrp), false) | |||
end | |||
-- AV System support | |||
if av_system then | if av_system then | ||
local formattedAV = formatList(av_system, true) | local formattedAV = formatList(av_system, true) | ||
if formattedAV then addRow(container, 'AV system', formattedAV, false) end | if formattedAV then | ||
addRow(container, 'AV system', formattedAV, false) | |||
end | |||
end | end | ||
-- Related models | |||
if related_models then | if related_models then | ||
local formattedRelated = formatRelatedModels(related_models, brand) | local formattedRelated = formatRelatedModels(related_models, brand) | ||
if formattedRelated then addRow(container, 'Related', formattedRelated, true) end | if formattedRelated then | ||
addRow(container, 'Related', formattedRelated, true) | |||
end | |||
end | end | ||
-- ECID row | |||
local ecidWikitext = string.format('[{{fullurl:%s|curid=%d}} EC%d]', pageName, pageId, pageId) | local ecidWikitext = string.format('[{{fullurl:%s|curid=%d}} EC%d]', pageName, pageId, pageId) | ||
addRow(container, 'ECID', frame:preprocess(ecidWikitext), true) | addRow(container, 'ECID', frame:preprocess(ecidWikitext), true) | ||
-- Format compatibility section (only show if tape-based device) | |||
if supported_formats or recording_formats then | if supported_formats or recording_formats then | ||
addSectionRow(container, 'Format Compatibility') | addSectionRow(container, 'Format Compatibility') | ||
if supported_formats then | if supported_formats then | ||
local | local formattedSupported = formatList(supported_formats, true) | ||
if | if formattedSupported then | ||
addRow(container, 'Plays', formattedSupported, false) | |||
end | |||
end | end | ||
if recording_formats then | if recording_formats then | ||
local | local formattedRecording = formatList(recording_formats, true) | ||
if | if formattedRecording then | ||
addRow(container, 'Records', formattedRecording, false) | |||
end | |||
end | end | ||
end | end | ||
-- Inputs and Outputs sections (matching CRT model style) | |||
local function renderIOSection(sectionTitle, ioType) | local function renderIOSection(sectionTitle, ioType) | ||
local locations = discoverLocations(frame, fullPageName, ioType) | local locations = discoverLocations(frame, fullPageName, ioType) | ||
local locationData = {} | local locationData = {} | ||
for _, loc in ipairs(locations) do | for _, loc in ipairs(locations) do | ||
local content = queryIOByLocation(frame, fullPageName, ioType, loc) | local content = queryIOByLocation(frame, fullPageName, ioType, loc) | ||
| Line 327: | Line 420: | ||
end | end | ||
end | end | ||
if #locationData > 0 then | if #locationData > 0 then | ||
addSectionRow(container, sectionTitle) | addSectionRow(container, sectionTitle) | ||
| Line 334: | Line 428: | ||
css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | css(labelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | ||
labelDiv:wikitext(locationLabel(data.location)) | labelDiv:wikitext(locationLabel(data.location)) | ||
local valueDiv = container:tag('div') | local valueDiv = container:tag('div') | ||
valueDiv:addClass('infobox-value') | valueDiv:addClass('infobox-value') | ||
| Line 345: | Line 440: | ||
renderIOSection('Outputs', 'output') | renderIOSection('Outputs', 'output') | ||
-- Features section | |||
if features then | if features then | ||
addSectionRow(container, 'Features') | addSectionRow(container, 'Features') | ||
| Line 350: | Line 446: | ||
end | end | ||
-- Dimensions section | |||
if height or width or depth or weight then | if height or width or depth or weight then | ||
addSectionRow(container, 'Dimensions') | addSectionRow(container, 'Dimensions') | ||
if height or width or depth then | if height or width or depth then | ||
addRow(container, 'H × W × D', string.format('%s × %s × %s', | 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 | end | ||
if weight then | if weight then | ||
local weightWikitext = '{{#show: ' .. fullPageName .. ' |?Weight # lb }} / {{#show: ' .. fullPageName .. ' |?Weight # kg }}' | local weightWikitext = '{{#show: ' .. fullPageName .. ' |?Weight # lb }} / {{#show: ' .. fullPageName .. ' |?Weight # kg }}' | ||
| Line 361: | Line 463: | ||
end | end | ||
-- Power section - query voltage ranges and display power specs | |||
local voltageQueryWikitext = | 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}}' | '{{#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 voltageResult = frame:preprocess(voltageQueryWikitext) | ||
local hasVoltage = voltageResult and trim(voltageResult) and trim(voltageResult) ~= '' | local hasVoltage = voltageResult and trim(voltageResult) and trim(voltageResult) ~= '' | ||
| Line 368: | Line 480: | ||
if hasVoltage or frequency or power_avg or power_max then | if hasVoltage or frequency or power_avg or power_max then | ||
addSectionRow(container, 'Power') | addSectionRow(container, 'Power') | ||
if hasVoltage then addRow(container, 'Voltage', voltageResult, true) end | |||
if hasVoltage then | |||
addRow(container, 'Voltage', voltageResult, true) | |||
end | |||
if frequency then | if frequency then | ||
local formattedFreq = formatList(frequency, true) | local formattedFreq = formatList(frequency, true) | ||
if formattedFreq then addRow(container, 'AC frequency', formattedFreq, false) end | if formattedFreq then | ||
addRow(container, 'AC frequency', formattedFreq, false) | |||
end | |||
end | end | ||
if power_avg or power_max then | if power_avg or power_max then | ||
local powerStr = '' | local powerStr = '' | ||
if power_avg and power_max then powerStr = power_avg .. 'W (avg) / ' .. power_max .. 'W (max)' | if power_avg and power_max then | ||
elseif power_avg then powerStr = power_avg .. 'W' | powerStr = power_avg .. 'W (avg) / ' .. power_max .. 'W (max)' | ||
elseif power_max then powerStr = power_max .. 'W (max)' end | elseif power_avg then | ||
powerStr = power_avg .. 'W' | |||
elseif power_max then | |||
powerStr = power_max .. 'W (max)' | |||
end | |||
addRow(container, 'Power', powerStr, false) | addRow(container, 'Power', powerStr, false) | ||
end | end | ||
end | end | ||
local tbcCountWikitext = '{{#ask:\n [[TBC type::+]]\n [[-Has subobject::' .. fullPageName .. ']]\n |format=count\n}}' | -- 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 tbcCountResult = frame:preprocess(tbcCountWikitext) | ||
local hasTBC = tbcCountResult and tonumber(trim(tbcCountResult)) and tonumber(trim(tbcCountResult)) > 0 | local hasTBC = tbcCountResult and tonumber(trim(tbcCountResult)) and tonumber(trim(tbcCountResult)) > 0 | ||
| Line 388: | Line 517: | ||
if hasTBC then | if hasTBC then | ||
addSectionRow(container, 'Time Base Correction') | 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 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) | local tbcResult = frame:preprocess(tbcQueryWikitext) | ||
if tbcResult then | if tbcResult then | ||
local entryIndex = 0 | |||
for rowContent in tbcResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in tbcResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, cleanContent) | |||
end | end | ||
if #cells >= 2 then | if #cells >= 2 then | ||
local tbcType = trim(cells[2]) | local tbcType = trim(cells[2]) | ||
local tbcBuffer = trim(cells[3]) | local tbcBuffer = trim(cells[3]) | ||
local tbcNotes = trim(cells[4]) | local tbcNotes = trim(cells[4]) | ||
if tbcType then | if tbcType then | ||
entryIndex = entryIndex + 1 | |||
-- Create a full-width grouped container with nested grid | |||
local group = container:tag('div') | local group = container:tag('div') | ||
group:addClass('infobox-tbc-group') | group:addClass('infobox-tbc-group') | ||
css(group, 'grid-column: 1 / -1; display:grid; grid-template-columns:auto 1fr; gap:3px;') | css(group, 'grid-column: 1 / -1; display:grid; grid-template-columns:auto 1fr; gap:3px;') | ||
local | |||
css( | -- Type row | ||
local typeLabelDiv = group:tag('div') | |||
local | typeLabelDiv:addClass('infobox-label') | ||
css( | 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 | if tbcBuffer then | ||
local | local bufLabelDiv = group:tag('div') | ||
css( | bufLabelDiv:addClass('infobox-label') | ||
css(bufLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
local | bufLabelDiv:wikitext('Buffer') | ||
css( | |||
local bufValueDiv = group:tag('div') | |||
bufValueDiv:addClass('infobox-value') | |||
css(bufValueDiv, 'padding:0.2em 0.4em;') | |||
bufValueDiv:wikitext(tbcBuffer) | |||
end | end | ||
-- Notes row (conditional) | |||
if tbcNotes then | if tbcNotes then | ||
local | local notesLabelDiv = group:tag('div') | ||
css( | notesLabelDiv:addClass('infobox-label') | ||
css(notesLabelDiv, 'padding:0.2em 0.4em; font-weight:bold; text-align:left;') | |||
local | notesLabelDiv:wikitext('Notes') | ||
css( | |||
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 | ||
| Line 432: | Line 595: | ||
end | end | ||
-- Accessories section | -- Accessories section: Always show (like Parts/Documents), with "Add accessory" button | ||
addSectionRow(container, 'Accessories') | 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}}' | -- Build the add accessory link pointing to Create_an_accessory with av_devices parameter | ||
local addAccessoryUrl = mw.uri.fullUrl( | |||
"Create an accessory", | |||
{ | |||
av_devices = pageName | |||
} | |||
) | |||
-- Query for accessories linked via Has related AV device | |||
-- Unlike CRT models which have a dedicated remote_control field, | |||
-- AV devices show all accessory types including remote controls here. | |||
local accessoriesCountWikitext = | |||
'{{#ask:\n' .. | |||
' [[Category:Accessories]]\n' .. | |||
' [[Has related AV device::' .. fullPageName .. ']]\n' .. | |||
' |format=count\n' .. | |||
'}}' | |||
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') | local accDiv = container:tag('div') | ||
css(accDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | css(accDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | ||
if hasAccessories then | 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}}' | -- Query accessories and display grouped by type | ||
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 accessoriesResult = frame:preprocess(accessoriesQueryWikitext) | ||
-- 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 | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | end | ||
-- 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 | ||
if not accessoriesByType[accType] then | if not accessoriesByType[accType] then | ||
| Line 460: | Line 657: | ||
table.insert(typeOrder, accType) | table.insert(typeOrder, accType) | ||
end | end | ||
table.insert(accessoriesByType[accType], {page = accPageName}) | |||
table.insert(accessoriesByType[accType], { | |||
page = accPageName | |||
}) | |||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
-- Create a nested grid for accessory rows | |||
local accGrid = accDiv:tag('div') | local accGrid = accDiv:tag('div') | ||
css(accGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;') | 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] | ||
if accs and #accs > 0 then | if accs and #accs > 0 then | ||
local accLinks = {} | local accLinks = {} | ||
for _, acc in ipairs(accs) do table.insert(accLinks, '[[' .. acc.page .. ']]') end | for _, acc in ipairs(accs) do | ||
local | table.insert(accLinks, '[[' .. acc.page .. ']]') | ||
css( | end | ||
local | local labelDiv = accGrid:tag('div') | ||
css( | 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 accessory" button | |||
local addAccDiv = mw.html.create('div') | local addAccDiv = mw.html.create('div') | ||
addAccDiv:css('margin-top', '0.5em'):css('text-align', 'center') | addAccDiv:css('margin-top', '0.5em'):css('text-align', 'center') | ||
| Line 485: | Line 697: | ||
accDiv:node(addAccDiv) | accDiv:node(addAccDiv) | ||
else | else | ||
-- Just show the add accessory link as a button | |||
local addAccDiv = mw.html.create('div') | local addAccDiv = mw.html.create('div') | ||
addAccDiv:css('text-align', 'center') | addAccDiv:css('text-align', 'center') | ||
| Line 493: | Line 706: | ||
-- Parts section | -- Parts section | ||
addSectionRow(container, 'Parts') | 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}}' | -- Build the add part link pointing to Create_a_part with av_devices parameter | ||
local addPartUrl = mw.uri.fullUrl( | |||
"Create a 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 partsCount = frame:preprocess(partsCountWikitext) | ||
local hasParts = partsCount and tonumber(trim(partsCount)) and tonumber(trim(partsCount)) > 0 | local hasParts = partsCount and tonumber(trim(partsCount)) and tonumber(trim(partsCount)) > 0 | ||
local partsDiv = container:tag('div') | local partsDiv = container:tag('div') | ||
css(partsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | css(partsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | ||
if hasParts then | 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}}' | -- 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) | local partsResult = frame:preprocess(partsQueryWikitext) | ||
-- Parse parts from broadtable result and group by type | |||
local partsByType = {} | local partsByType = {} | ||
local partTypeOrder = {} | local partTypeOrder = {} | ||
if partsResult then | if partsResult then | ||
for rowContent in partsResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in partsResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | end | ||
-- cells[1] = page name, cells[2] = type, cells[3] = subtype, cells[4] = model code | |||
if #cells >= 2 then | if #cells >= 2 then | ||
local partPageName = cells[1] | local partPageName = cells[1] | ||
local partType = cells[2] or 'Part' | local partType = cells[2] or 'Part' | ||
local partSubtype = cells[3] | local partSubtype = cells[3] | ||
-- Use subtype + type as the grouping key if subtype exists | |||
local groupKey = partType | local groupKey = partType | ||
if partSubtype and partSubtype ~= '' then groupKey = partSubtype .. ' ' .. partType end | if partSubtype and partSubtype ~= '' then | ||
groupKey = partSubtype .. ' ' .. partType | |||
end | |||
if partPageName then | if partPageName then | ||
-- Track type order for consistent display | |||
if not partsByType[groupKey] then | if not partsByType[groupKey] then | ||
partsByType[groupKey] = {} | partsByType[groupKey] = {} | ||
table.insert(partTypeOrder, groupKey) | table.insert(partTypeOrder, groupKey) | ||
end | end | ||
table.insert(partsByType[groupKey], {page = partPageName}) | |||
table.insert(partsByType[groupKey], { | |||
page = partPageName | |||
}) | |||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
-- Create a nested grid for part rows | |||
local partsGrid = partsDiv:tag('div') | local partsGrid = partsDiv:tag('div') | ||
css(partsGrid, 'display:grid; grid-template-columns:auto 1fr; gap:3px;') | 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 | for _, partType in ipairs(partTypeOrder) do | ||
local parts = partsByType[partType] | local parts = partsByType[partType] | ||
if parts and #parts > 0 then | if parts and #parts > 0 then | ||
local partLinks = {} | local partLinks = {} | ||
for _, part in ipairs(parts) do table.insert(partLinks, '[[' .. part.page .. ']]') end | for _, part in ipairs(parts) do | ||
local | table.insert(partLinks, '[[' .. part.page .. ']]') | ||
css( | end | ||
local | local labelDiv = partsGrid:tag('div') | ||
css( | 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 | ||
end | end | ||
-- Add "Add part" button | |||
local addPartDiv = mw.html.create('div') | local addPartDiv = mw.html.create('div') | ||
addPartDiv:css('margin-top', '0.5em'):css('text-align', 'center') | addPartDiv:css('margin-top', '0.5em'):css('text-align', 'center') | ||
| Line 547: | Line 813: | ||
partsDiv:node(addPartDiv) | partsDiv:node(addPartDiv) | ||
else | else | ||
-- Just show the add part link as a button | |||
local addPartDiv = mw.html.create('div') | local addPartDiv = mw.html.create('div') | ||
addPartDiv:css('text-align', 'center') | addPartDiv:css('text-align', 'center') | ||
| Line 553: | Line 820: | ||
end | end | ||
-- Archival Links section | -- 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 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 linksResult = frame:preprocess(linksQueryWikitext) | ||
local linksByType = {Manufacturer = {}, Retailer = {}, Database = {}} | |||
-- Parse links and organize by type | |||
local linksByType = { | |||
Manufacturer = {}, | |||
Retailer = {}, | |||
Database = {} | |||
} | |||
local hasLinks = false | local hasLinks = false | ||
if linksResult then | if linksResult then | ||
for rowContent in linksResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in linksResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, trim(cleanContent)) | |||
end | end | ||
-- cells[1] = page/subobject, cells[2] = type, cells[3] = source, cells[4] = URL | |||
if #cells >= 4 then | if #cells >= 4 then | ||
local linkType = cells[2] | local linkType = cells[2] | ||
local linkSource = cells[3] | local linkSource = cells[3] | ||
local linkUrl = cells[4] | local linkUrl = cells[4] | ||
if linkType and linkSource and linkUrl and linkUrl ~= '' | |||
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 | ||
end | end | ||
end | end | ||
if hasLinks then | if hasLinks then | ||
addSectionRow(container, 'Archival Links') | addSectionRow(container, 'Archival Links') | ||
local linkTypeOrder = {'Database', 'Manufacturer', 'Retailer'} | |||
for _, linkType in ipairs(linkTypeOrder) do | |||
if #linksByType[linkType] > 0 then | if #linksByType[linkType] > 0 then | ||
local linksHtml = {} | local linksHtml = {} | ||
| Line 590: | Line 882: | ||
-- Documents section | -- Documents section | ||
addSectionRow(container, 'Documents') | addSectionRow(container, 'Documents') | ||
local timestamp = os.time() | local timestamp = os.time() | ||
math.randomseed(timestamp + pageId) | math.randomseed(timestamp + pageId) | ||
local random = math.random(1000, 9999) | local random = math.random(1000, 9999) | ||
local target = "Document:" .. timestamp .. "-" .. random | local target = "Document:" .. timestamp .. "-" .. random | ||
local uploadUrl = mw.uri.fullUrl("Special:FormEdit", {form = "Document", target = target, ["Document[av_devices]"] = pageName}) | local uploadUrl = mw.uri.fullUrl( | ||
local documentsCountWikitext = '{{#ask:\n [[Category:Documents]]\n [[Has related AV device::' .. fullPageName .. ']]\n |format=count\n}}' | "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 documentCount = frame:preprocess(documentsCountWikitext) | ||
local hasDocuments = documentCount and tonumber(trim(documentCount)) and tonumber(trim(documentCount)) > 0 | local hasDocuments = documentCount and tonumber(trim(documentCount)) and tonumber(trim(documentCount)) > 0 | ||
local docsDiv = container:tag('div') | local docsDiv = container:tag('div') | ||
css(docsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | css(docsDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em;') | ||
if hasDocuments then | 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 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 docsResult = frame:preprocess(docsQueryWikitext) | ||
local ul = mw.html.create('ul') | local ul = mw.html.create('ul') | ||
ul:css('margin', '0'):css('padding-left', '1.5em') | ul:css('margin', '0'):css('padding-left', '1.5em') | ||
if docsResult then | if docsResult then | ||
for rowContent in docsResult:gmatch('<tr[^>]*>(.-)</tr>') do | for rowContent in docsResult:gmatch('<tr[^>]*>(.-)</tr>') do | ||
local cells = {} | local cells = {} | ||
for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | for cellContent in rowContent:gmatch('<td[^>]*>(.-)</td>') do | ||
local cleanContent = cellContent:gsub('<[^>]+>', '') | |||
table.insert(cells, cleanContent) | |||
end | end | ||
if #cells >= 2 then | if #cells >= 2 then | ||
local docType = trim(cells[2]) | local docType = trim(cells[2]) | ||
local docTitle = trim(cells[5]) | local docTitle = trim(cells[5]) | ||
local docPageName = trim(cells[1]) | local docPageName = trim(cells[1]) | ||
if docPageName then | if docPageName then | ||
local displayText = 'Document' | local displayText = 'Document' | ||
if docTitle then displayText = docTitle | if docTitle then | ||
elseif docType then displayText = docType end | displayText = docTitle | ||
ul:tag('li'):wikitext('[[' .. docPageName .. '|' .. displayText .. ']]') | elseif docType then | ||
displayText = docType | |||
end | |||
local li = ul:tag('li') | |||
li:wikitext('[[' .. docPageName .. '|' .. displayText .. ']]') | |||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
docsDiv:node(ul) | docsDiv:node(ul) | ||
local uploadDiv = mw.html.create('div') | local uploadDiv = mw.html.create('div') | ||
uploadDiv:css('margin-top', '0.5em'):css('text-align', 'center') | uploadDiv:css('margin-top', '0.5em'):css('text-align', 'center') | ||
uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | ||
docsDiv:node(uploadDiv) | docsDiv:node(uploadDiv) | ||
else | else | ||
| Line 634: | Line 966: | ||
uploadDiv:css('text-align', 'center') | uploadDiv:css('text-align', 'center') | ||
uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | uploadDiv:wikitext('[' .. tostring(uploadUrl) .. ' <span class="infobox-button infobox-button-secondary">Add document</span>]') | ||
docsDiv:node(uploadDiv) | docsDiv:node(uploadDiv) | ||
end | end | ||
| Line 639: | Line 972: | ||
-- Contribute section | -- Contribute section | ||
addSectionRow(container, 'Contribute') | addSectionRow(container, 'Contribute') | ||
local editUrl = mw.uri.fullUrl("Special:FormStart", {form = "AV device", page_name = pageName}) | |||
local editUrl = mw.uri.fullUrl( | |||
"Special:FormStart", | |||
{ | |||
form = "AV device", | |||
page_name = pageName | |||
} | |||
) | |||
local editDiv = container:tag('div') | local editDiv = container:tag('div') | ||
css(editDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em; text-align:center;') | css(editDiv, 'grid-column: 1 / -1; padding:0.2em 0.4em; text-align:center;') | ||