Module:DynamicYSWSList/sandbox
Documentation for this module may be created at Module:DynamicYSWSList/sandbox/doc
-- Module:DynamicYSWSList
-- Public Hack Club events API only.
local p = {}
local API_URL = 'https://hackclub.com/api/v1/events'
local function str(value)
if value == nil then
return ''
end
if type(value) ~= 'string' and type(value) ~= 'number' then
return ''
end
return mw.text.trim(tostring(value))
end
local function firstNonEmpty(...)
for i = 1, select('#', ...) do
local value = str(select(i, ...))
if value ~= '' then
return value
end
end
return ''
end
local function validUrl(url)
url = str(url)
if url:match('^https?://[^%s<>]+$') then
return url
end
return nil
end
local function normalizeDate(value)
value = str(value)
-- Accept ISO dates and ISO timestamps.
local year, month, day =
value:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)')
if not year then
return nil
end
year = tonumber(year)
month = tonumber(month)
day = tonumber(day)
if not year or not month or not day then
return nil
end
if month < 1 or month > 12 then
return nil
end
local days = {
31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31
}
local leap = year % 4 == 0 and
(year % 100 ~= 0 or year % 400 == 0)
if leap then
days[2] = 29
end
if day < 1 or day > days[month] then
return nil
end
return string.format(
'%04d-%02d-%02d', year, month, day
)
end
local MONTHS = {
'January', 'February', 'March',
'April', 'May', 'June',
'July', 'August', 'September',
'October', 'November', 'December'
}
local function displayDate(iso)
if not iso then
return '—'
end
local year, month, day =
iso:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)$')
return string.format(
'%d %s %s',
tonumber(day),
MONTHS[tonumber(month)],
year
)
end
local function fetchPrograms()
if not mw.ext or not mw.ext.externalData then
return nil, 'External Data is unavailable.'
end
local ok, data, errors = pcall(
mw.ext.externalData.getExternalData,
{
url = API_URL,
format = 'json with jsonpath',
data = {
name = '$.data[*].name',
description = '$.data[*].description',
url = '$.data[*].url',
startDate = '$.data[*].startDate',
endDate = '$.data[*].endDate'
}
}
)
if not ok then
return nil, 'Unable to retrieve the event feed.'
end
if not data or #data == 0 then
return nil, 'The event feed returned no programs.'
end
return data
end
local function normalizePrograms(data)
local programs = {}
for _, row in ipairs(data) do
local name = firstNonEmpty(row.name)
if name ~= '' then
local description =
firstNonEmpty(row.description)
-- Keep descriptions compact and plain-text.
description = mw.text.decode(description)
description = description:gsub('<[^>]*>', '')
description = description:gsub('%s+', ' ')
if mw.ustring.len(description) > 240 then
description =
mw.ustring.sub(description, 1, 237)
.. '...'
end
local startDate = normalizeDate(
firstNonEmpty(row.startDate, row.start)
)
local endDate = normalizeDate(
firstNonEmpty(row.endDate, row['end'])
)
programs[#programs + 1] = {
name = name,
description = description,
url = validUrl(row.url),
startDate = startDate,
endDate = endDate
}
end
end
table.sort(programs, function(a, b)
return mw.ustring.lower(a.name) <
mw.ustring.lower(b.name)
end)
return programs
end
local function addDateCell(tr, date, className)
local cell = tr:tag('td')
:addClass(className)
:attr('data-sort-value', date or '9999-12-31')
if date then
cell:tag('time')
:attr('datetime', date)
:text(displayDate(date))
else
cell:text('—')
end
end
local function renderTable(programs)
local root = mw.html.create('div')
:addClass('ysws-directory')
root:tag('p')
:addClass('ysws-directory-count')
:text(string.format(
'%d public event entries listed.',
#programs
))
local controls = root:tag('div')
:addClass('ysws-controls')
controls:tag('label')
:attr('for', 'ysws-search')
:text('Search programs')
controls:tag('input')
:attr('id', 'ysws-search')
:attr('type', 'search')
:attr('placeholder', 'Search name or description')
:attr('autocomplete', 'off')
:attr('aria-controls', 'ysws-table')
controls:tag('p')
:addClass('ysws-results')
:attr('role', 'status')
:attr('aria-live', 'polite')
:text('')
local wrapper = root:tag('div')
:addClass('ysws-table-wrapper')
local tbl = wrapper:tag('table')
:attr('id', 'ysws-table')
:addClass('wikitable')
:addClass('sortable')
:addClass('ysws-table')
local header = tbl:tag('thead'):tag('tr')
for _, heading in ipairs({
'Program',
'Description',
'Start date',
'End date',
'Status',
'Website'
}) do
header:tag('th')
:attr('scope', 'col')
:text(heading)
end
local tbody = tbl:tag('tbody')
for _, program in ipairs(programs) do
local tr = tbody:tag('tr')
:addClass('ysws-program')
:attr('data-start', program.startDate or '')
:attr('data-end', program.endDate or '')
tr:tag('th')
:attr('scope', 'row')
:addClass('ysws-name')
:text(program.name)
tr:tag('td')
:addClass('ysws-description')
:text(program.description ~= ''
and program.description or '—')
addDateCell(tr, program.startDate, 'ysws-start')
addDateCell(tr, program.endDate, 'ysws-end')
tr:tag('td')
:addClass('ysws-status')
:text('Unknown')
local website = tr:tag('td')
:addClass('ysws-website')
if program.url then
website:tag('a')
:attr('href', program.url)
:attr('rel', 'noopener noreferrer')
:text('Website')
else
website:text('—')
end
end
root:tag('p')
:addClass('ysws-empty')
:attr('hidden', 'hidden')
:text('No programs match your search.')
root:tag('p')
:addClass('ysws-source-note')
:text(
'Generated from the public Hack Club events API. '
.. 'Dates and descriptions reflect the public feed.'
)
return tostring(root)
end
function p.main(frame)
local data, err = fetchPrograms()
if not data then
return tostring(
mw.html.create('p')
:addClass('error')
:text(err)
)
end
local programs = normalizePrograms(data)
if #programs == 0 then
return 'No public programs were returned.'
end
return renderTable(programs)
end
return p