Module:T-CountryHub: Difference between revisions
// via Wikitext Extension for VSCode Tag: Reverted |
Maintenance update // via Wikitext Extension for VSCode |
||
| (28 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Modules/T-CountryHub.lua | -- Modules/T-CountryHub.lua | ||
-- Blueprint-based full-page template for country hubs with | -- Blueprint-based full-page template for country hubs with discrete content blocks | ||
-- Safe require helpers | -- Safe require helpers | ||
local function safeRequire(name) | local function safeRequire(name) | ||
local ok, mod = pcall(require, name) | local ok, mod = pcall(require, name) | ||
if ok and mod then return mod end | if ok and mod then | ||
return setmetatable({}, { __index = function() return function() return '' end end }) | return mod | ||
end | |||
return setmetatable({}, { | |||
__index = function() | |||
return function() return '' end | |||
end | |||
}) | |||
end | end | ||
| Line 19: | Line 21: | ||
local CountryData = safeRequire('Module:CountryData') | local CountryData = safeRequire('Module:CountryData') | ||
local TemplateHelpers = safeRequire('Module:TemplateHelpers') | local TemplateHelpers = safeRequire('Module:TemplateHelpers') | ||
local LinkParser = safeRequire('Module:LinkParser') | |||
local NormalizationText = safeRequire('Module:NormalizationText') | local NormalizationText = safeRequire('Module:NormalizationText') | ||
local NormalizationDiacritic = safeRequire('Module:NormalizationDiacritic') | local NormalizationDiacritic = safeRequire('Module:NormalizationDiacritic') | ||
local MasonryLayout = safeRequire('Module:MasonryLayout') | |||
local mw = mw | local mw = mw | ||
local html = mw.html | local html = mw.html | ||
local smw = (mw.smw and mw.smw.ask) and mw.smw or nil | local smw = (mw.smw and mw.smw.ask) and mw.smw or nil -- Handle for Semantic MediaWiki's #ask functionality, if available | ||
-- askCached: Performs a Semantic MediaWiki #ask query with caching. | -- askCached: Performs a Semantic MediaWiki #ask query with caching; uses TemplateHelpers.generateCacheKey to create a unique key for the query and TemplateHelpers.withCache to manage the caching logic. | ||
-- @param key string: A unique identifier part for the query, used for generating the cache key. | |||
-- @param params table: Parameters to be passed to the smw.ask function. | |||
-- @return table: The result of the SMW query, or an empty table if SMW is unavailable or the query fails. | |||
local _askCache = {} | local _askCache = {} | ||
local function askCached(key, params) | local function askCached(key, params) | ||
| Line 35: | Line 42: | ||
end | end | ||
-- safeField: Safely retrieves a field from a data row. | -- safeField: Safely retrieves a field from a data row; it first tries to access the field by its string 'key'. If not found or empty, it falls back to the first element of the row (index 1). | ||
-- @param row table: The data row (a table). | |||
-- @param key string/number: The key or index of the field to retrieve. | |||
-- @return string: The field's value, or an empty string if not found or if the row is nil. | |||
local function safeField(row, key) | local function safeField(row, key) | ||
if not row then return '' end | if not row then return '' end | ||
if row[key] ~= nil and row[key] ~= '' then return row[key] end | if row[key] ~= nil and row[key] ~= '' then return row[key] end | ||
if row[1] | if row[1] ~= nil and row[1] ~= '' then return row[1] end -- Fallback for SMW results where data might be in the first unnamed field | ||
return '' | return '' | ||
end | end | ||
-- # | -- renderTable: Generates HTML for a standard MediaWiki table | ||
local function renderTable(result, columns) | |||
local t = html.create('table'):addClass('wikitable') | |||
-- Header row | |||
local header = t:tag('tr') | |||
for _, col in ipairs(columns) do | |||
header:tag('th'):wikitext(col):done() | |||
end | |||
-- Data rows | |||
for _, row in ipairs(result) do | |||
local tr = t:tag('tr') | |||
for _, col in ipairs(columns) do | |||
tr:tag('td'):wikitext(safeField(row, col)):done() | |||
end | |||
end | |||
return tostring(t) | |||
end | |||
-- renderSection: A wrapper function that performs a cached SMW query and renders a table only if results are found. | |||
-- @param key string: The base cache key for the query. | |||
-- @param params table: The parameters for the smw.ask query. | |||
-- @param columns table: A list of column headers for the table. | |||
-- @return string: An HTML table if data is found, otherwise an empty string. | |||
local function renderSection(key, params, columns) | |||
local data = askCached(key, params) | |||
if not data or #data == 0 then | |||
return '' | |||
end | |||
return renderTable(data, columns) | |||
end | |||
local errorContext = ErrorHandling.createContext("T-CountryHub") | |||
-- ================================================================================ | -- ================================================================================ | ||
-- CONTROL OF TEMPLATE FEATURES: THIS LIST SPECIFIES IN AN EXPLICIT MANNER WHAT FEATURES ARE TO BE CALLED/RENDERED BY THE TEMPLATE. | |||
local template = Blueprint.registerTemplate('CountryHub', { | local template = Blueprint.registerTemplate('CountryHub', { | ||
features = { | features = { | ||
fullPage = true, | fullPage = true, -- does not render as a template box, but rather occupies the entire page | ||
countryWrapper = true, | countryWrapper = true, | ||
countryFlag = true, | |||
infoBox = true, | |||
intro = true, | |||
overview = false, | |||
organizations = true, | |||
people = true, | |||
laws = true, | |||
documents = false, | |||
geoTlds = true, | |||
meetings = true, | |||
nra = true, | |||
resources = false, | |||
semanticProperties = true, | semanticProperties = true, | ||
categories = true, | categories = true, | ||
| Line 64: | Line 115: | ||
}) | }) | ||
-- Blueprint default: Initialize standard configuration | |||
Blueprint.initializeConfig(template) | Blueprint.initializeConfig(template) | ||
-- | -- CONTROL THE VISUAL ORDER THAT EACH ASPECT IS RENDERED IN | ||
template.config.blockSequence = { | template.config.blockSequence = { | ||
'wrapperOpen', | 'wrapperOpen', | ||
' | 'featureBanner', | ||
'overview', | |||
'intelligentMasonry', | |||
'wrapperClose', | 'wrapperClose', | ||
'categories', | 'categories', | ||
| Line 75: | Line 129: | ||
} | } | ||
-- MASONRY LAYOUT CONFIGURATION | |||
local cardDefinitions = { | |||
{blockId = 'intro', feature = 'intro', title = 'Welcome'}, | |||
{blockId = 'organizations', feature = 'organizations', title = 'Organizations'}, | |||
{blockId = 'people', feature = 'people', title = 'People'}, | |||
{blockId = 'geoTlds', feature = 'geoTlds', title = 'GeoTLDs'}, | |||
{blockId = 'meetings', feature = 'meetings', title = 'Internet Governance Events'}, | |||
{blockId = 'nra', feature = 'nra', title = 'National Authorities'}, | |||
{blockId = 'laws', feature = 'laws', title = 'Laws and Regulations'}, | |||
{blockId = 'documents', feature = 'documents', title = 'Key Documents'}, | |||
{blockId = 'resources', feature = 'resources', title = 'Resources'}, | |||
{blockId = 'infoBox', feature = 'infoBox', title = 'Country Info'} | |||
} | |||
-- | local masonryOptions = { | ||
columns = 3, | |||
mobileColumns = 1, | |||
containerClass = 'country-hub-masonry-container', | |||
columnClass = 'country-hub-masonry-column', | |||
cardClass = 'country-hub-masonry-card', | |||
-- Note: We cannot detect mobile server-side in MediaWiki | |||
-- The MasonryLayout will output both desktop and mobile HTML | |||
-- CSS media queries will handle the actual display | |||
mobileMode = false -- Always use desktop mode in Lua, CSS handles responsive | |||
} | |||
-- ================================================================================ | -- ================================================================================ | ||
template.config.blocks = template.config.blocks or {} | |||
-- Wrapper open block: Defines the opening div for the main content wrapper and includes the flag image | |||
template.config.blocks.wrapperOpen = { | template.config.blocks.wrapperOpen = { | ||
feature = 'countryWrapper', | feature = 'countryWrapper', | ||
render = function() return '<div class="country-hub-wrapper">' end | render = function(template, args) -- Added template, args | ||
-- local normalizedCountryName = args.has_country -- From preprocessor | |||
-- local flagImageWikitext = "" | |||
-- if normalizedCountryName and normalizedCountryName ~= "" then | |||
-- local isoCode = CountryData.getCountryCodeByName(normalizedCountryName) | |||
-- if isoCode and #isoCode == 2 then | |||
-- local flagFile = "Flag-" .. string.lower(isoCode) .. ".svg" | |||
-- -- Using a new class 'country-wrapper-bg-image' for the image | |||
-- flagImageWikitext = string.format( | |||
-- "[[File:%s|link=|class=country-wrapper-bg-image]]", | |||
-- flagFile | |||
-- ) | |||
-- end | |||
-- end | |||
return '<div class="country-hub-wrapper">' -- .. flagImageWikitext -- Appended flag | |||
end | |||
} | } | ||
template.config.blocks. | -- Feature Preview Banner | ||
feature = ' | template.config.blocks.featureBanner = { | ||
render | feature = 'fullPage', | ||
render = function(template, args) | |||
return '<div class="country-hub-feature-banner">' .. | |||
'<strong>Country Hubs</strong> have been enabled as a feature preview and are still under testing.' .. | |||
' Contribute more knowledge to our database so that they can keep growing!' .. | |||
'</div>' | |||
end | |||
} | } | ||
-- | -- ANCHOR: INFOBOX | ||
template.config.blocks.infoBox = { | |||
feature = 'infoBox', | |||
render = function(template, args) | |||
infoBox = function(template, args) | |||
local displayCountry = (args.country or mw.title.getCurrentTitle().text or ""):gsub('_',' ') | local displayCountry = (args.country or mw.title.getCurrentTitle().text or ""):gsub('_',' ') | ||
-- Derive ccTLD from ISO code via CountryData module | |||
local isoCode = CountryData.getCountryCodeByName(args.has_country) | local isoCode = CountryData.getCountryCodeByName(args.has_country) | ||
local ccTLDText | local ccTLDText | ||
if isoCode and #isoCode == 2 then | |||
local cctldValue = "." .. string.lower(isoCode) | |||
ccTLDText = "[[" .. cctldValue .. "]]" -- Make it a wiki link | |||
else | |||
ccTLDText = string.format('ccTLD data unavailable for %s.', displayCountry) | |||
end | |||
local regionParams = { string.format('[[Has country::%s]]', args.has_country), '?Has ICANN region', format = 'plain', limit = 1 } | -- Fetch ICANN region for the country | ||
local regionParams = { | |||
string.format('[[Has country::%s]]', args.has_country), | |||
'?Has ICANN region', | |||
format = 'plain', | |||
limit = 1 | |||
} | |||
local regionData = askCached('infoBox:region:' .. args.has_country, regionParams) | local regionData = askCached('infoBox:region:' .. args.has_country, regionParams) | ||
local regionText = | local regionText = regionData[1] and regionData[1]['Has ICANN region'] or '' | ||
regionText = LinkParser.processWikiLink(regionText, 'strip') | |||
local ISOCParams = { '[[Category:Internet Society Chapter]]', limit = 200 } | -- Check for an ISOC chapter in the country using fuzzy matching | ||
local ISOCParams = { | |||
'[[Category:Internet Society Chapter]]', | |||
limit = 200 -- Fetch all chapters to perform fuzzy matching | |||
} | |||
local ISOCData = askCached('infoBox:ISOC:all_chapters', ISOCParams) | local ISOCData = askCached('infoBox:ISOC:all_chapters', ISOCParams) | ||
local ISOCText | local ISOCText | ||
| Line 119: | Line 231: | ||
if not str then return '' end | if not str then return '' end | ||
local pageName = string.match(str, '^%[%[([^%]]+)%]%]$') or str | local pageName = string.match(str, '^%[%[([^%]]+)%]%]$') or str | ||
pageName = NormalizationDiacritic.removeDiacritics(pageName) | |||
pageName = string.lower(pageName) | |||
pageName = string.gsub(pageName, 'internet society', '') | |||
pageName = string.gsub(pageName, 'chapter', '') | |||
return NormalizationText.trim(pageName) | |||
end | end | ||
| Line 126: | Line 242: | ||
if countryData then | if countryData then | ||
table.insert(countryNamesToMatch, normalizeForMatching(countryData.name)) | table.insert(countryNamesToMatch, normalizeForMatching(countryData.name)) | ||
for _, variation in pairs(countryData.variations) do | if countryData.variations then | ||
for _, variation in pairs(countryData.variations) do | |||
table.insert(countryNamesToMatch, normalizeForMatching(variation)) | |||
end | |||
end | end | ||
else | else | ||
| Line 150: | Line 268: | ||
end | end | ||
end | end | ||
if not foundMatch then ISOCText = string.format('[[Internet Society %s Chapter]]', args.has_country) end | |||
if not foundMatch then | |||
ISOCText = string.format('[[Internet Society %s Chapter]]', args.has_country) | |||
end | |||
local flagImageWikitext = "" | |||
if args.has_country and args.has_country ~= "" then | |||
local isoCode = CountryData.getCountryCodeByName(args.has_country) | |||
if isoCode and #isoCode == 2 then | |||
local flagFile = "Flag-" .. string.lower(isoCode) .. ".svg" | |||
flagImageWikitext = string.format( | |||
"[[File:%s|link=|class=country-infobox-bg-image]]", | |||
flagFile | |||
) | |||
end | |||
end | |||
-- Assemble the HTML for the infobox table | |||
local infoBoxWrapper = html.create('div') | |||
:addClass('country-hub-infobox-wrapper') | |||
local | local infoBox = infoBoxWrapper:tag('table') | ||
:addClass('country-hub-infobox icannwiki-automatic-text') | |||
-- Header row | |||
infoBox:tag('tr') | |||
:tag('th') | |||
:attr('colspan', '2') | |||
:addClass('country-hub-infobox-header icannwiki-automatic-text') | |||
:wikitext(string.format('%s', displayCountry)) | |||
:done() | |||
:done() | |||
-- ccTLD row | |||
infoBox:tag('tr') | |||
:tag('th'):wikitext('ccTLD'):done() | |||
:tag('td'):wikitext(ccTLDText):done() | |||
:done() | |||
-- ICANN region row | |||
infoBox:tag('tr') | |||
:tag('th'):wikitext('ICANN region'):done() | |||
:tag('td'):wikitext(regionText):done() | |||
:done() | |||
-- REVIEW: Check for ccNSO membership or affiliation in the country / https://ccnso.icann.org/en/about/members.htm | |||
-- ISOC chapter row | |||
infoBox:tag('tr') | |||
:tag('th'):wikitext('ISOC chapter'):done() | |||
:tag('td'):wikitext(ISOCText):done() | |||
:done() | |||
infoBoxWrapper:wikitext(flagImageWikitext) | |||
return tostring(infoBoxWrapper) | |||
end | |||
} | |||
-- ANCHOR: INTRO | |||
template.config.blocks.intro = { | |||
feature = 'intro', | |||
render = function(template, args) | |||
local rawIntroText = '<p>Welcome to ICANNWiki\'s hub for <b>{{PAGENAME}}</b>. With the use of semantics, this page aggregates all Internet Governance content for this territory that is currently indexed in our database.</p>' | |||
if template.current_frame and template.current_frame.preprocess then | |||
return template.current_frame:preprocess(rawIntroText) | |||
end | end | ||
return rawIntroText | |||
end | |||
} | |||
-- ANCHOR: OVERVIEW | |||
template.config.blocks.overview = { | |||
feature = 'overview', | |||
render = function(template, args) | |||
local displayCountry = (args.country or mw.title.getCurrentTitle().text or ""):gsub('_',' ') | |||
local queryCountryName = args.has_country | |||
local params = { | |||
string.format('[[Has country::%s]]', queryCountryName), | |||
'[[Category:Country]]', | |||
'?Has description', | |||
format = 'plain', | |||
limit = 1 | |||
} | } | ||
local data = askCached('overview:' .. queryCountryName, params) | |||
local desc = (data[1] and data[1]['Has description']) or '' | |||
if desc == '' then | |||
desc = 'No overview description found for ' .. displayCountry .. '.' | |||
end | |||
return '== Overview ==\n' .. desc | |||
end | |||
} | |||
-- ANCHOR: ORGANIZATIONS | |||
template.config.blocks.organizations = { | |||
feature = 'organizations', | |||
render = function(template, args) | |||
local params = { | |||
string.format('[[Has country::%s]] [[Has entity type::Organization]]', args.has_country), | |||
limit = 20 | |||
} | } | ||
local data = askCached('organizations:' .. args.has_country, params) | local data = askCached('organizations:' .. args.has_country, params) | ||
if #data == 0 then return | -- Store the raw count for masonry layout | ||
return { | template._rawDataCounts = template._rawDataCounts or {} | ||
template._rawDataCounts.organizations = #data | |||
-- Only render if we have data | |||
if #data == 0 then | |||
return '' | |||
end | |||
return renderTable(data, {'Organizations'}) | |||
end | |||
} | |||
-- ANCHOR: PEOPLE | |||
template.config.blocks.people = { | |||
feature = 'people', | |||
render = function(template, args) | |||
local params = { | |||
string.format('[[Has country::%s]] [[Has entity type::Person]]', | |||
args.has_country), | |||
limit = 20 | |||
} | } | ||
local data = askCached('people:' .. args.has_country, params) | |||
-- Store the raw count for masonry layout | |||
template._rawDataCounts = template._rawDataCounts or {} | |||
template._rawDataCounts.people = #data | |||
-- Only render if we have data | |||
if #data == 0 then | |||
return '' | |||
end | |||
return renderTable(data, {'People'}) | |||
end, | end, | ||
} | |||
local params = { string.format('[[Has country::%s]] [[Has entity type:: | |||
-- ANCHOR: REGULATIONS | |||
template.config.blocks.laws = { | |||
feature = 'laws', | |||
render = function(template, args) | |||
local params = { | |||
string.format('[[Has country::%s]] [[Has entity type::Norm]]', args.has_country), | |||
limit = 20 | |||
} | } | ||
local data = askCached('laws:' .. args.has_country, params) | |||
-- Store the raw count for masonry layout | |||
template._rawDataCounts = template._rawDataCounts or {} | |||
template._rawDataCounts.laws = #data | |||
-- Only render if we have data | |||
if #data == 0 then | |||
return '' | |||
end | |||
return renderTable(data, {'Laws and Regulations'}) | |||
end, | end, | ||
} | |||
local params = { string.format('[[Has country::%s | |||
-- ANCHOR: DOCUMENTS | |||
template.config.blocks.documents = { | |||
feature = 'documents', | |||
render = function(template, args) | |||
local params = { | |||
string.format('[[Has country::%s]]', args.has_country), | |||
'[[Category:Document]]', | |||
mainlabel = 'Document', sort = 'Has date', order = 'desc', | |||
limit = 20 | |||
} | } | ||
return renderSection('documents:' .. args.has_country, params, {'Key Documents'}) | |||
end, | end, | ||
} | |||
local params = { string.format('[[Has country::%s]] [[Has entity type:: | |||
-- ANCHOR: GEOTLDS | |||
template.config.blocks.geoTlds = { | |||
feature = 'geoTlds', | |||
render = function(template, args) | |||
local params = { | |||
string.format('[[Has country::%s]] [[Has entity type::TLD]] [[Has TLD subtype::geoTLD]]', args.has_country), | |||
limit = 20 | |||
} | } | ||
local data = askCached('geoTlds:' .. args.has_country, params) | |||
-- Store the raw count for masonry layout | |||
template._rawDataCounts = template._rawDataCounts or {} | |||
template._rawDataCounts.geoTlds = #data | |||
-- Only render if we have data | |||
if #data == 0 then | |||
return '' | |||
end | |||
return renderTable(data, {'GeoTLDs'}) | |||
end, | end, | ||
} | |||
local params = { string.format('[[Has country::%s | |||
-- ANCHOR: EVENTS | |||
template.config.blocks.meetings = { | |||
feature = 'meetings', | |||
render = function(template, args) | |||
local params = { | |||
string.format('[[Has country::%s]]', args.has_country), | |||
'[[Has entity type::Event]]', | |||
limit = 20 | |||
} | } | ||
end | local data = askCached('events:' .. args.has_country, params) | ||
-- Store the raw count for masonry layout | |||
template._rawDataCounts = template._rawDataCounts or {} | |||
template._rawDataCounts.meetings = #data | |||
-- Only render if we have data | |||
if #data == 0 then | |||
return '' | |||
end | |||
return renderTable(data, {'Internet Governance Events'}) | |||
end, | |||
} | } | ||
-- | -- ANCHOR: AUTHORITIES | ||
template.config.blocks. | template.config.blocks.nra = { | ||
feature = ' | feature = 'nra', | ||
render = function(template, args) | render = function(template, args) | ||
local | local params = { | ||
string.format('[[Has country::%s]] [[Has entity type::Organization]] [[Has organization type::Government agency]]', args.has_country), | |||
limit = 20 | |||
} | |||
local | local data = askCached('nra:' .. args.has_country, params) | ||
for | -- Store the raw count for masonry layout | ||
template._rawDataCounts = template._rawDataCounts or {} | |||
template._rawDataCounts.nra = #data | |||
-- Only render if we have data | |||
if #data == 0 then | |||
return '' | |||
end | end | ||
return renderTable(data, {'National Authorities'}) | |||
end, | |||
} | |||
-- -- REVIEW: CONNECTED COUNTRIES (COLONIES) | |||
-- ANCHOR: RESOURCES | |||
template.config.blocks.resources = { | |||
return | feature = 'resources', | ||
render = function(template, args) | |||
local params = { | |||
string.format('[[Has country::%s]]', args.has_country), | |||
'[[Category:Resource]]', | |||
mainlabel = 'Resource', | |||
limit = 20 | |||
} | |||
return renderSection('resources:' .. args.has_country, params, {'Resources'}) | |||
end, | |||
} | |||
-- Data wrapper blocks | |||
template.config.blocks.dataWrapperOpen = { | |||
feature = 'fullPage', | |||
render = function() return '<div class="country-hub-data-container">' end | |||
} | |||
template.config.blocks.dataWrapperClose = { | |||
feature = 'fullPage', | |||
render = function() return '</div>' end | |||
} | |||
-- Wrapper close block | |||
template.config.blocks.wrapperClose = { | |||
feature = 'countryWrapper', | |||
render = function() return '</div>' end | |||
} | |||
-- INTELLIGENT MASONRY LAYOUT INTEGRATION | |||
-- Single block that handles all masonry logic at render-time (Blueprint pattern) | |||
template.config.blocks.intelligentMasonry = { | |||
feature = 'fullPage', | |||
render = function(template, args) | |||
return MasonryLayout.renderIntelligentLayout(template, args, { | |||
cardDefinitions = cardDefinitions, | |||
options = masonryOptions, | |||
blockRenderers = { | |||
intro = template.config.blocks.intro, | |||
organizations = template.config.blocks.organizations, | |||
people = template.config.blocks.people, | |||
geoTlds = template.config.blocks.geoTlds, | |||
meetings = template.config.blocks.meetings, | |||
nra = template.config.blocks.nra, | |||
laws = template.config.blocks.laws, | |||
documents = template.config.blocks.documents, | |||
resources = template.config.blocks.resources, | |||
infoBox = template.config.blocks.infoBox | |||
} | |||
}) | |||
end | end | ||
} | } | ||
-- | -- Preprocessor: Seeds 'country', 'has_country' (normalized), and 'region' arguments; ensures these key values are available and standardized early in the rendering process | ||
Blueprint.addPreprocessor(template, function(_, args) | Blueprint.addPreprocessor(template, function(_, args) | ||
args.country = args.country or mw.title.getCurrentTitle().text or "" | args.country = args.country or mw.title.getCurrentTitle().text or "" | ||
| Line 351: | Line 561: | ||
end) | end) | ||
-- Semantic property provider: Sets 'Has country' and 'Has ICANN region' semantic properties | |||
Blueprint.registerPropertyProvider(template, function(_, args) | Blueprint.registerPropertyProvider(template, function(_, args) | ||
local props = {} | local props = {} | ||
| Line 360: | Line 571: | ||
end) | end) | ||
-- Category provider | |||
Blueprint.registerCategoryProvider(template, function(_, args) | Blueprint.registerCategoryProvider(template, function(_, args) | ||
local cats = {"Country Hub"} | local cats = {"Country Hub"} | ||
| Line 369: | Line 581: | ||
end) | end) | ||
-- | -- Render entry point; wraps the Blueprint rendering process with error protection | ||
function p.render(frame) | function p.render(frame) | ||
return ErrorHandling.protect( | return ErrorHandling.protect( | ||
| Line 385: | Line 592: | ||
return p | return p | ||