Shko te përmbajtja

Moduli:Showcase of the week

Page protected
Nga Wikipedia, enciklopedia e lirë

--------------------------------------------------------------------------------
-- Module:Showcase of the week
--
-- The "Vitrina e javës" card on the Main Page (Faqja kryesore), with {{Vitrina e javës}}: a file of the week, a
-- picture, a video or a sound.
--   {{#invoke:Showcase of the week|main}}
-- For a week:
--  1. If the first line of its week page (e.g. "Wikipedia:Vitrina e javës/2026/45") names a file, the card shows it
--     (a video or a sound with the player of the wiki), with the rest of the page as its caption.
--  2. If the week page is written out in full (a gallery, a table…), it is shown as it is.
--  3. If there is no week page, or its file does not exist, the card shows what Wikimedia Commons features, the
--     three kinds in turn, one a week, each once before any comes again (DAILY): the picture of the day of the
--     Monday, which is always one of its featured pictures; a sound recording from its media of the day; a video
--     from its media of the day. A module cannot read Commons, so the card holds a place for the file with the date
--     of the Monday (class "potw-commons", data-potd="2026-10-26", data-sound="2026-11-02" or
--     data-video="2026-11-09"), and MediaWiki:Showcase-of-the-week.js puts the file there: the picture from the
--     wiki's own feed of featured content (/api/rest_v1/feed/featured/2026/10/26); the sound or the video from the
--     media of the day of Commons (Template:Motd/2026-11-02), the first of its kind in the three weeks from the
--     Monday on, until the next week of its kind (or, when there is none, the latest one before it that no earlier
--     week took, so that none comes twice), in the player of the wiki; each with its caption when Commons has one in
--     Albanian, and its date in the note. MediaWiki:Common.js and MediaWiki:Mobile.js load that script on the pages
--     that hold such a place. Without scripts the card shows a link to the files of the day instead. Nothing is kept
--     in a table, and no robot is needed.
-- The weeks, the week pages, the window of the next four weeks (Kuvendi), the archive box and the heads of the week
-- pages are those of every weekly rubric of the Main Page: Module:Weekly rubric.
--------------------------------------------------------------------------------

local Weekly = require('Module:Weekly rubric')

local DAY, WEEK = Weekly.DAY, Weekly.WEEK

-- What Commons features, the kinds that the weeks without a choice take in turn, one a week: the picture of the day,
-- a sound recording and a video from the media of the day. For each: the attribute of the place in the card (it
-- holds the date of the Monday), the main page of the kind on Commons and, for the picture, the page of a date
-- (that of the sound and the video is found by MediaWiki:Showcase-of-the-week.js), and what the card calls it in
-- the forms the sentences need: as the subject, the article that follows it ("fotografia e ditës e Commons",
-- "regjistrimi zanor i ditës i Commons") and as the object.
local DAILY = {
	{ key = 'potd', day = 'commons:Template:Potd/', home = 'commons:Commons:Picture of the day',
		name = 'fotografia e ditës', of = 'e', object = 'fotografinë e ditës' },
	{ key = 'sound', home = 'commons:Commons:Media of the day',
		name = 'regjistrimi zanor i ditës', of = 'i', object = 'regjistrimin zanor të ditës' },
	{ key = 'video', home = 'commons:Commons:Media of the day',
		name = 'videoja e ditës', of = 'e', object = 'videon e ditës' },
}

-- The icon before the name of a sound recording in the card of a week, and the longest name it shows in full
-- (as MediaWiki:Showcase-of-the-week.js)
local SOUND_ICON = 'OOjs UI icon volumeUp-ltr.svg'
local NAME_MAX = 60

-- The Monday (at noon) of the week that takes the first kind, 2026/41; the weeks after it take the kinds in turn,
-- counted in weeks, so that the turn goes on from one year to the next whether a year has 52 weeks or 53
local DAILY_FIRST = os.time{ year = 2026, month = 10, day = 5, hour = 12 }

-- The size of a picture or a video on the Main Page: it fits in this box and is made smaller on narrow screens
-- (MediaWiki:Showcase-of-the-week.js fits the picture of the day of Commons in the same box); a sound has a player as
-- wide as SOUND_SIZE
local SIZE = '800x500px'
local SOUND_SIZE = '400px'

local MONTHS = { 'janar', 'shkurt', 'mars', 'prill', 'maj', 'qershor', 'korrik', 'gusht', 'shtator', 'tetor',
	'nëntor', 'dhjetor' }

-- The file namespace and its other names, and the words that are options of a file link, not its caption
local FILE_NAMES = { 'Skeda', 'File', 'Figura', 'Image', 'Foto' }
local OPTIONS = { thumb = true, thumbnail = true, parapamje = true, ['miniaturë'] = true, frame = true, framed = true,
	frameless = true, border = true, left = true, right = true, center = true, centre = true, none = true,
	majtas = true, djathtas = true, ['qendër'] = true, qender = true, upright = true, baseline = true, middle = true,
	sub = true, super = true, top = true, ['text-top'] = true, bottom = true, ['text-bottom'] = true }

-- The extensions of the files that the card can show: pictures, and the videos and sounds that the wiki plays
local PICTURES = { jpg = true, jpeg = true, png = true, gif = true, svg = true, tif = true, tiff = true, webp = true }
local PLAYED = { webm = true, ogv = true, mpg = true, mpeg = true, ogg = true, oga = true, opus = true, mp3 = true,
	wav = true, flac = true, mid = true, midi = true }

local rubric

-- The end of the link or call that opens at "start" ('[[' or '{{'), counting the ones inside it
local function closing(text, start)
	local depth, i = 0, start
	while i < #text do
		local two = text:sub(i, i + 1)
		if two == '[[' or two == '{{' then
			depth, i = depth + 1, i + 2
		elseif two == ']]' or two == '}}' then
			depth, i = depth - 1, i + 2
			if depth == 0 then
				return i - 1
			end
		else
			i = i + 1
		end
	end
	return #text
end

-- The parts of a link or a call (the text between [[ ]] or {{ }}), split at the bars that are not inside an inner
-- link or call
local function split(inner)
	local parts, depth, start, i = {}, 0, 1, 1
	while i <= #inner do
		local two = inner:sub(i, i + 1)
		if two == '[[' or two == '{{' then
			depth, i = depth + 1, i + 2
		elseif (two == ']]' or two == '}}') and depth > 0 then
			depth, i = depth - 1, i + 2
		elseif inner:sub(i, i) == '|' and depth == 0 then
			table.insert(parts, inner:sub(start, i - 1))
			start, i = i + 1, i + 1
		else
			i = i + 1
		end
	end
	table.insert(parts, inner:sub(start))
	return parts
end

-- Whether a part of a file link is one of its options (a size, a place, a frame, alt=, link=…), not its caption
local function isOption(part)
	part = mw.ustring.lower(mw.text.trim(part))
	return OPTIONS[part] or part:find('^%d*x?%d+%s*px$') or part:find('^upright') or part:find('^[%a_]+%s*=') ~= nil
end

-- The name of a file without its namespace ("Skeda:Emri.jpg" → "Emri.jpg"), or nil if it is not one
local function withoutNamespace(name)
	name = mw.text.trim(name or '')
	for _, ns in ipairs(FILE_NAMES) do
		local first = ns:sub(1, 1)
		local rest = name:match('^[' .. first:upper() .. first:lower() .. ']' .. ns:sub(2) .. '%s*:%s*(.+)$')
		if rest then
			return mw.text.trim(rest)
		end
	end
	return name
end

-- The first file link of a text ([[Skeda:…]] or one of the other names): where it starts, or nil
local function firstFileLink(text)
	local best
	for _, ns in ipairs(FILE_NAMES) do
		local first = ns:sub(1, 1)
		local s = text:find('%[%[%s*[' .. first:upper() .. first:lower() .. ']' .. ns:sub(2) .. '%s*:')
		if s and (not best or s < best) then
			best = s
		end
	end
	return best
end

-- The file and the caption of the file link that starts at "s"
local function fileLink(text, s)
	local parts = split(text:sub(s + 2, closing(text, s) - 2))
	local file = withoutNamespace(parts[1])
	local caption = ''
	for i = 2, #parts do
		local part = mw.text.trim(parts[i])
		if part ~= '' and not isOption(part) then
			caption = part
		end
	end
	return file, caption
end

-- The file of the first line of a week page and its caption (the rest of the page, or the caption of the link):
-- "Skeda:Emri.jpg", "[[Skeda:Emri.jpg|Përshkrimi]]" or "Emri.jpg". nil if the first line does not name a picture, a
-- video or a sound (the page is then written out in full).
local function chosenFile(text)
	text = rubric:bare(text)
	local first, rest = text:match('^([^\n]*)\n?(.*)$')
	first = mw.text.trim(first or '')
	local file, caption
	if first:find('^%[%[') and first:find('%]%]$') then
		local s = firstFileLink(first)
		if s ~= 1 then
			return nil
		end
		file, caption = fileLink(first, 1)
	else
		file = withoutNamespace(first)
	end
	local extension = file:match('%.(%w+)$')
	extension = extension and mw.ustring.lower(extension)
	if file == '' or file:find('[%[%]{}|<>\n]') or not extension or not (PICTURES[extension] or PLAYED[extension]) then
		return nil
	end
	rest = mw.text.trim(rest or '')
	return file, rest ~= '' and rest or caption or ''
end

-- What the file is, if it exists (here or on Commons): 'picture', 'video' or 'sound' (a sound has no size; an Ogg
-- file can be either). nil if it does not exist.
local function fileKind(name)
	local title = mw.title.new(name, 'File')
	local file = title and title.file
	if not (file and file.exists) then
		return nil
	end
	if PICTURES[mw.ustring.lower(name:match('%.(%w+)$') or '')] then
		return 'picture'
	end
	return (file.width or 0) > 0 and 'video' or 'sound'
end

-- What Commons features that the week that starts on the given Monday takes, when it has no choice (DAILY)
local function dailyOf(monday)
	return DAILY[math.floor((monday - DAILY_FIRST) / WEEK + 0.5) % #DAILY + 1]
end

-- Decides what to show in the week that starts on the given Monday. Returns a table with:
--  mode: 'file' (a file named on the week page), 'page' (week page written out in full) or 'commons' (what Commons
--  features on the Monday of the week, "day": "daily" says which kind, see DAILY)
--  page (the week page), exists (whether it exists), and file, kind (see fileKind) and caption for a file named on
--  the week page
local function choose(frame, monday)
	local page, text = rubric:weekPageText(frame, monday)
	if text then
		local file, caption = chosenFile(text)
		if not file then
			return { mode = 'page', page = page, exists = true, text = text }
		end
		local kind = fileKind(file)
		if kind then
			return { mode = 'file', file = file, kind = kind, caption = caption, page = page, exists = true }
		end
		-- the file does not exist: the picture of the day of Commons, as for a week without a page
	end
	return { mode = 'commons', day = monday, daily = dailyOf(monday), page = page, exists = text ~= nil }
end

-- Roughly the text a reader sees, for the alternative text of the picture
local function plain(wikitext)
	local t = wikitext:gsub('%[%[[^%]|]*|([^%]]*)%]%]', '%1'):gsub('%[%[([^%]]*)%]%]', '%1')
	t = t:gsub('%[%a+://%S+%s*([^%]]*)%]', '%1'):gsub("'''?", ''):gsub('<[^>]*>', ''):gsub('{%b{}}', '')
	t = mw.text.trim((t:gsub('[|%[%]]', ' '):gsub('%s+', ' ')))
	if mw.ustring.len(t) > 200 then
		t = mw.ustring.sub(t, 1, 199) .. '…'
	end
	return t
end

-- The name of a file as a title: without "_" and without its extension; with "max", a longer one is cut after a
-- word, with "…"
local function fileTitle(file, max)
	local name = (file:gsub('_', ' '):gsub('%.%w+$', ''))
	if max and mw.ustring.len(name) > max then
		local head = mw.ustring.sub(name, 1, max - 1)
		local cut = mw.ustring.find(head, ' [^ ]*$')
		if cut and cut > max / 2 then
			head = mw.ustring.sub(head, 1, cut - 1)
		end
		name = mw.ustring.gsub(head, '[%s,;:.–-]+$', '') .. '…'
	end
	return name
end

-- A day in words: "12 tetor 2026"
local function dayText(t)
	local d = os.date('!*t', t)
	return d.day .. ' ' .. MONTHS[d.month] .. ' ' .. d.year
end

-- A day as Commons names its pictures of the day: "2026-10-12"
local function isoDay(t)
	return os.date('!%Y-%m-%d', t)
end

-- The card: the file and its caption (a video or a sound in the player of the wiki); for a week without a chosen file,
-- the place of the file of the day of Commons (filled by MediaWiki:Showcase-of-the-week.js) and the note, in two
-- forms: the one for readers whose browser runs the script, and the link to the file for the others
-- (Stampa:Vitrina e javës/styles.css shows one of the two)
local function card(frame, choice)
	if choice.mode == 'commons' then
		local day, daily = isoDay(choice.day), choice.daily
		local nojs = daily.day and string.format('[[%s%s|%s]] të Wikimedia Commons për datën %s', daily.day, day,
			daily.object, dayText(choice.day)) or string.format('[[%s|skedat multimediale të ditës]] të Wikimedia Commons',
			daily.home)
		return table.concat({
			string.format('<div class="mp-potw potw-commons potw-%s" data-%s="%s">', daily.key, daily.key, day),
			'<div class="mp-potw-image potw-commons-image"></div>',
			string.format('<div class="mp-potw-auto potw-js">Për këtë javë nuk është zgjedhur asnjë skedë, ndaj po shfaqet '
				.. '[[%s|%s]] %s Wikimedia Commons për datën <span class="potw-date">%s</span>.</div>', daily.home,
				daily.name, daily.of, dayText(choice.day)),
			'<div class="mp-potw-auto potw-nojs">Për këtë javë nuk është zgjedhur asnjë skedë. Shihni ' .. nojs .. '.</div>',
			'</div>',
		}, '\n')
	end
	local caption = mw.text.trim(choice.caption or '')
	local kind = choice.kind or 'picture'
	-- the alternative text of a picture; a player has the name of its file
	local options = SIZE
	if kind == 'picture' then
		options = SIZE .. '|alt=' .. (caption ~= '' and plain(caption) or choice.file:gsub('%.%w+$', ''))
	elseif kind == 'sound' then
		options = SOUND_SIZE
	end
	local parts = { '<div class="mp-potw">' }
	-- a sound without a caption has its name above its player, which shows nothing of it
	if kind == 'sound' and caption == '' then
		table.insert(parts, string.format('<div class="mp-potw-title">[[:File:%s|%s]]</div>', choice.file,
			fileTitle(choice.file)))
	end
	table.insert(parts, string.format('<div class="mp-potw-image mp-potw-%s">[[File:%s|%s]]</div>', kind, choice.file,
		options))
	if caption ~= '' then
		table.insert(parts, '<div class="mp-potw-caption">' .. Weekly.withoutRedLinks(caption) .. '</div>')
	end
	table.insert(parts, '</div>')
	return table.concat(parts, '\n')
end

-- A small picture of a week's choice, for its card in the window, with the name of its file under it, linked to the
-- page of the file: a picture; a video as a small player, which shows its first frame; a sound as its name after the
-- icon of a loudspeaker. For the file of the day of Commons, an empty place with the date of the Monday, which
-- MediaWiki:Showcase-of-the-week.js fills in the same way.
local function thumbnail(file, kind)
	if kind == 'sound' then
		return string.format('<span class="window-wr-sound">[[File:%s|16px|link=|alt=|class=skin-invert]] '
			.. '[[:File:%s|%s]]</span>', SOUND_ICON, file, fileTitle(file, NAME_MAX))
	end
	return string.format('<span class="window-wr-thumb window-wr-%s">[[File:%s|120x80px|link=File:%s|alt=]]</span>'
		.. '<span class="window-wr-name">[[:File:%s|%s]]</span>', kind, file, file, file, fileTitle(file, NAME_MAX))
end

local function commonsThumbnail(day, daily)
	return string.format('<span class="window-wr-thumb potw-commons potw-commons-thumb potw-%s" data-%s="%s"></span>',
		daily.key, daily.key, isoDay(day))
end

-- The picture of the day that the Main Page had before the showcase of the week (2011–2022), and the pages of its
-- years: its yearly archive where there is one, otherwise the list of its daily pages of that year. It ended in 2022,
-- so the list does not change.
local OLDER_PREFIX = 'Wikipedia:Faqja kryesore/Fotografia e ditës/'
local OLDER_ARCHIVES = { [2011] = true, [2019] = true, [2020] = true, [2022] = true }

rubric = Weekly.new{
	name = 'Vitrina e javës',
	home = 'Wikipedia:Vitrina e javës',
	prefix = 'Wikipedia:Vitrina e javës/',
	head = 'Showcase of the week page',
	firstYear = 2026,
	older = { home = 'Wikipedia:Faqja kryesore/Fotografia e ditës', label = 'Fotografia e ditës', years = { 2011, 2012,
		2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 }, link = function (year)
			return OLDER_ARCHIVES[year] and (OLDER_PREFIX .. 'Arkivi/' .. year)
				or ('Special:PrefixIndex/' .. OLDER_PREFIX .. year .. '/')
		end },
	item = 'skeda',
	oneItem = 'një skedë',
	formHint = 'Hap faqen e kësaj jave, ku shkruani emrin e skedës dhe përshkrimin e saj.',
	aim = '<b>Synimi është që çdo javë të ketë një skedë multimediale të zgjedhur nga komuniteti.</b> Kur për një javë '
		.. 'nuk zgjidhet asnjë, faqja kryesore shfaq vetë, me radhë, fotografinë e ditës, një regjistrim zanor ose një '
		.. 'video nga skedat multimediale të ditës të Wikimedia Commons.',
	choose = choose,
	-- the card of a week in the window: a small copy of the file chosen by hand, or the file of the day of Commons
	-- that the Main Page shows in its place
	pick = function (frame, choice, i, monday)
		if choice.mode == 'file' then
			return true, thumbnail(choice.file, choice.kind)
		elseif choice.mode == 'page' then
			return true, '[[' .. choice.page .. '|faqe e shkruar e plotë]]'
		end
		local daily = choice.daily
		return false, string.format('<div class="window-wr-fallback">%s si zëvendësim [[%s|%s]] %s Commons për datën '
			.. '<span class="potw-date">%s</span>%s</div>', i == 1 and 'Po shfaqet' or 'Do të shfaqet',
			daily.day and daily.day .. isoDay(monday) or daily.home, daily.name, daily.of, dayText(monday),
			choice.exists and ' (skeda te faqja e javës nuk ekziston)' or '')
			.. commonsThumbnail(monday, daily)
	end,
	-- the head of a week page
	describe = function (frame, choice, monday)
		if choice.mode == 'file' then
			return { kind = 'chosen', label = 'Është zgjedhur skeda:', detail = string.format('[[:File:%s|%s]]',
				choice.file, (choice.file:gsub('_', ' '))), titleOnly = true, preview = true }
		elseif choice.mode == 'page' then
			return { kind = 'written', label = 'Kjo faqe është shkruar e plotë',
				detail = 'Faqja kryesore e shfaq ashtu siç është.' }
		end
		local daily = choice.daily
		return { kind = 'auto',
			label = choice.exists and 'Skeda e shkruar këtu nuk ekziston' or 'Ende nuk është zgjedhur një skedë',
			detail = daily.day and string.format('Faqja kryesore do të shfaqë vetë [[%s%s|%s]] të Wikimedia Commons për '
				.. 'datën %s.', daily.day, isoDay(monday), daily.object, dayText(monday))
				or string.format('Faqja kryesore do të shfaqë vetë [[%s|%s]] të Wikimedia Commons për një ditë nga %s deri '
				.. 'më %s (ose më herët, kur këto ditë nuk kanë asnjë).', daily.home, daily.object, dayText(monday),
				dayText(monday + 20 * DAY)),
			preview = true }
	end,
}

-- {{#invoke:Showcase of the week|main}}: the card of the current week (or of the week its first parameter names,
-- "202645", for the heads of the week pages)
local function main(frame)
	local choice = choose(frame, (Weekly.weekOf(frame)))
	if choice.mode == 'page' then
		return frame:expandTemplate{ title = choice.page }
	end
	return card(frame, choice)
end

return rubric:exports(main)