Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Documentation for this module may be created at Module:Slug/doc

local p = {}

-- Canonical record slug.
--
-- Mirrors slugify() in tools/upload_newsitems.py (itself a mirror of
-- newsbot_move.php), so a title built by Form:NewsItem matches the
-- batch-created corpus exactly. Verified against all 357 live News:
-- pages that carry Has event headline.
--
-- Order matters: quotes and apostrophes are DROPPED before the
-- non-alphanumeric sweep, so "don't" slugs to "dont", not "don-t".

local function slugify(s)
	if not s or s == '' then
		return ''
	end

	local U = mw.ustring

	s = U.gsub(s, '^%s+', '')
	s = U.gsub(s, '%s+$', '')
	s = U.lower(s)

	s = U.gsub(s, '&', ' and ')

	-- Dropped outright, not hyphenated: straight and curly apostrophes,
	-- straight and curly double quotes.
	s = U.gsub(s, "'", '')
	s = U.gsub(s, '‘', '')
	s = U.gsub(s, '’', '')
	s = U.gsub(s, '"', '')
	s = U.gsub(s, '“', '')
	s = U.gsub(s, '”', '')

	-- Every run of anything that is not a letter or a digit collapses to a
	-- single hyphen. %w is Unicode-aware under mw.ustring, so non-ASCII
	-- place names survive.
	s = U.gsub(s, '[^%w]+', '-')

	s = U.gsub(s, '^%-+', '')
	s = U.gsub(s, '%-+$', '')

	-- Cap length, then re-trim in case the cut landed on a hyphen.
	if U.len(s) > 110 then
		s = U.sub(s, 1, 110)
		s = U.gsub(s, '%-+$', '')
	end

	return s
end

local function isTaken( name )
	local t = mw.title.new( name )
	return t ~= nil and t.exists
end

-- Slug only.
function p.make( frame )
	local s = frame.args[1] or frame.args.text or ''
	return slugify( s )
end

-- Whole canonical NewsItem page name, including the corpus's collision
-- convention: the bare title, then -2, -3, ...
--
-- This has to live here rather than in the form, because Page Forms'
-- own <unique number> cannot express it. PF swaps <unique number> for a
-- {num} placeholder BEFORE running the parser and only substitutes the
-- real value AFTER (PF_AutoeditAPI::generateTargetName), so at parse
-- time a conditional is looking at the literal string "{num}" and can
-- never tell "first page" from "third". PF also appends a BARE number,
-- giving slug2 where the corpus uses slug-2.
--
-- Usage: {{#invoke:Slug|newsitem|<date>|<category>|<headline>}}
-- Returns '' on missing input, so an incomplete form fails loudly with
-- an invalid-target error rather than building a malformed title.
function p.newsitem( frame )
	local a = frame.args
	local date = mw.text.trim( a[1] or a.date or '' )
	local cat = mw.text.trim( a[2] or a.category or '' )
	local slug = slugify( a[3] or a.headline or '' )

	if date == '' or cat == '' or slug == '' then
		return ''
	end

	local base = 'News:' .. date .. '/' .. cat .. '/' .. slug
	if not isTaken( base ) then
		return base
	end

	-- .exists is an expensive parser call; the deepest run in the live
	-- corpus is -3, so this ceiling is never reached in practice.
	for n = 2, 20 do
		local candidate = base .. '-' .. n
		if not isTaken( candidate ) then
			return candidate
		end
	end

	return base .. '-21'
end

return p