Module:PersonRelatedNews: Difference between revisions
From TwogPedia
No edit summary |
No edit summary |
||
| (5 intermediate revisions by the same user not shown) | |||
| Line 18: | Line 18: | ||
-- local viewAllElement = mw.html.create('div'):addClass('related__news__view-all'):attr('id', 'view-all-button'):wikitext('View All') | -- local viewAllElement = mw.html.create('div'):addClass('related__news__view-all'):attr('id', 'view-all-button'):wikitext('View All') | ||
local viewAllElement = mw.html.create(' | local viewAllElement = mw.html.create('span') | ||
:addClass('related__news__view-all') | :addClass('related__news__view-all') | ||
:attr('id', 'view-all-button') | :attr('id', 'view-all-button') | ||
: | :wikitext('[[News|View All]]') | ||
titleContainer:node(viewAllElement) | titleContainer:node(viewAllElement) | ||
| Line 94: | Line 94: | ||
return os.date("%b %d, %Y", time) | return os.date("%b %d, %Y", time) | ||
end | end | ||
-- function getRelatedNews(limit, title) | |||
-- local tables = 'News' | |||
-- local fields = '_pageName, date, tags, image, category, content' | |||
-- local words = mw.text.split(title, ' ') | |||
-- local conditions = {} | |||
-- for _, word in ipairs(words) do | |||
-- if #word > 2 then | |||
-- table.insert(conditions, '_pageName LIKE "%' .. word .. '%"') | |||
-- end | |||
-- end | |||
-- local whereClause = table.concat(conditions, ' OR ') | |||
-- local cargoArgs = { | |||
-- where = whereClause, | |||
-- orderBy = 'date DESC', | |||
-- limit = limit | |||
-- } | |||
-- local results = cargo.query(tables, fields, cargoArgs) | |||
-- return results or {} | |||
-- end | |||
function getRelatedNews(limit, title) | function getRelatedNews(limit, title) | ||
local tables = 'News' | local tables = 'News' | ||
local fields = '_pageName, date, tags, image, category, content' | local fields = '_pageName, date, tags, image, category, content' | ||
limit = tonumber(limit) or 4 | |||
-- Local helpers (scoped to this function) | |||
local STOP = { | |||
["the"]=true,["and"]=true,["for"]=true,["with"]=true,["from"]=true,["into"]=true, | |||
["of"]=true,["in"]=true,["on"]=true,["at"]=true,["to"]=true,["by"]=true,["is"]=true,["are"]=true, | |||
["a"]=true,["an"]=true,["as"]=true,["or"]=true,["if"]=true,["but"]=true,["not"]=true | |||
} | |||
local function escape_like(s) | |||
return (s or ""):gsub("%%","%%%%"):gsub("_","__"):gsub('"','\\"') | |||
end | |||
local function normalize(s) | |||
s = mw.ustring.lower(s or "") | |||
s = s:gsub("^[^:]+:", "") -- strip namespace if present | |||
s = s:gsub("[%p%c]", " ") -- punctuation/control -> space | |||
s = s:gsub("%s+", " ") | |||
return mw.text.trim(s) | |||
end | |||
local function terms_from_title(t) | |||
local out, seen = {}, {} | |||
for w in mw.ustring.gmatch(normalize(t), "[%w%d]+") do | |||
if #w >= 3 and not STOP[w] and not seen[w] then | |||
seen[w] = true | |||
table.insert(out, w) | |||
end | |||
end | |||
return out | |||
end | |||
-- Simple deterministic RNG (LCG) so "randoms" are stable per page title | |||
local function stable_seed(s) | |||
local h = 0 | |||
for i = 1, #s do | |||
h = (h * 131 + string.byte(s, i)) % 2147483647 | |||
end | |||
if h == 0 then h = 1 end | |||
return h | |||
end | |||
local function shuffle_in_place(t, seed) | |||
local mod = 2147483647 | |||
local a, c = 1103515245, 12345 | |||
local function rnd() | |||
seed = (a * seed + c) % mod | |||
return seed / mod | |||
end | |||
for i = #t, 2, -1 do | |||
local j = math.floor(rnd() * i) + 1 | |||
t[i], t[j] = t[j], t[i] | |||
end | |||
end | |||
local current = mw.title.getCurrentTitle().fullText | |||
local terms = terms_from_title(title) | |||
-- Build strict "related" filter: require >=2 distinct term matches (tags or title) | |||
local results, picked = {}, {} | |||
if #terms >= 2 then | |||
table.sort(terms, function(a,b) return #a > #b end) | |||
local MAX_TERMS = 6 | |||
while #terms > MAX_TERMS do table.remove(terms) end | |||
local pairClauses = {} | |||
for i = 1, #terms - 1 do | |||
local ti = escape_like(terms[i]) | |||
local left = string.format('(tags HOLDS LIKE "%%%s%%" OR _pageName LIKE "%%%s%%")', ti, ti) | |||
for j = i + 1, #terms do | |||
local tj = escape_like(terms[j]) | |||
local right = string.format('(tags HOLDS LIKE "%%%s%%" OR _pageName LIKE "%%%s%%")', tj, tj) | |||
table.insert(pairClauses, '(' .. left .. ' AND ' .. right .. ')') | |||
end | |||
end | |||
local whereClause = '(' .. table.concat(pairClauses, ' OR ') .. | |||
string.format(') AND _pageName != "%s"', escape_like(current)) | |||
local related = mw.ext.cargo.query(tables, fields, { | |||
where = whereClause, | |||
orderBy = 'date DESC', | |||
limit = limit * 2 | |||
}) or {} | |||
for _, r in ipairs(related) do | |||
if not picked[r._pageName] then | |||
table.insert(results, r) | |||
picked[r._pageName] = true | |||
if #results >= limit then break end | |||
end | |||
end | end | ||
end | end | ||
local | -- If not enough related, fill with random recent (excluding current & duplicates) | ||
if #results < limit then | |||
local recent = mw.ext.cargo.query(tables, fields, { | |||
orderBy = 'date DESC', | |||
limit = math.max(limit * 10, 40) -- pull a pool to randomize | |||
}) or {} | |||
-- filter out current and already picked | |||
local pool = {} | |||
for _, r in ipairs(recent) do | |||
if r._pageName ~= current and not picked[r._pageName] then | |||
table.insert(pool, r) | |||
end | |||
end | |||
if #pool > 0 then | |||
shuffle_in_place(pool, stable_seed(current)) | |||
for _, r in ipairs(pool) do | |||
table.insert(results, r) | |||
picked[r._pageName] = true | |||
if #results >= limit then break end | |||
end | |||
end | |||
end | |||
-- Final guarantee: never empty. If still short (very small dataset), just return what we have. | |||
return results | return results | ||
end | end | ||
return p | return p | ||
Latest revision as of 08:36, 23 October 2025
Documentation for this module may be created at Module:PersonRelatedNews/doc
local cargo = mw.ext.cargo
local p = {}
function p.main(frame)
local newsItemContainer = mw.html.create('div'):addClass('related__news')
local title = mw.title.getCurrentTitle().fullText
local newsTitle = "Related news"
-- if title:match("^Companies/") then
-- newsTitle = "Team News"
-- end
local iconHtml = '[[File:News-icon.png|30px|alt=' .. newsTitle .. ' Icon]]'
local iconElement = mw.html.create('h3'):addClass('news-icon'):wikitext(iconHtml .. ' ' .. newsTitle)
local titleContainer = mw.html.create('div'):addClass('related__news__title-container'):attr('style', 'display: flex; align-items: center;')
titleContainer:node(iconElement)
-- local viewAllElement = mw.html.create('div'):addClass('related__news__view-all'):attr('id', 'view-all-button'):wikitext('View All')
local viewAllElement = mw.html.create('span')
:addClass('related__news__view-all')
:attr('id', 'view-all-button')
:wikitext('[[News|View All]]')
titleContainer:node(viewAllElement)
local container = mw.html.create('div'):addClass('related__news__container'):node(titleContainer):node(newsItemContainer)
local relatedNews = getRelatedNews(4, title)
if #relatedNews > 0 then
for i = 1, #relatedNews do
addArticleToContainer(relatedNews[i], newsItemContainer)
end
end
return container
end
function addArticleToContainer(result, container)
local image = result.image or 'News placeholder.png'
local link = result._pageName
local category = result.category and mw.text.split(result.category, ',')[1] or 'Uncategorized'
local categoryClassMap = {
['News'] = 'news-category-news',
['Transfer Market'] = 'news-category-transfer',
['Drama'] = 'news-category-drama',
['Business'] = 'news-category-business',
['Sponsorships'] = 'news-category-sponsorships'
}
local categoryClass = categoryClassMap[category] or 'default-category-class'
local date = result.date and formatDate(result.date) or 'Unknown date'
local title = mw.html.create('div')
:addClass('news-title-twogpedia')
:wikitext('[[' .. result._pageName .. '|' .. mw.ext.displaytitle.get(result._pageName) .. ']]')
local metaContainer = mw.html.create('div'):addClass('news-meta-container')
local categoryElement = mw.html.create('p'):addClass('news-meta news-category-meta ' .. categoryClass):wikitext(category)
local dateElement = mw.html.create('p'):addClass('news-meta news-date'):wikitext(date)
metaContainer:node(categoryElement):node(dateElement)
local contentSnippet = getContentSnippet(result.content)
local contentElement = mw.html.create('p'):addClass('news-content'):wikitext(contentSnippet)
local imageHtml = '[[File:' .. image .. '|link=' .. link .. '|800px|class=news-image|alt=News Image]]'
local imageContainer = mw.html.create('div'):addClass('news-image-container'):wikitext(imageHtml)
local newsItem = mw.html.create('div')
:addClass('related__article')
:node(imageContainer)
:node(title)
:node(contentElement)
:node(metaContainer)
container:node(newsItem)
end
function getContentSnippet(content)
local words = mw.text.split(content, ' ')
local snippet = table.concat(words, ' ', 1, math.min(20, #words))
return snippet .. ( #words > 20 and '...' or '' )
end
function formatDate(dateStr)
local year, month, day = dateStr:match("(%d+)%-(%d+)%-(%d+)")
local time = os.time({year = year, month = month, day = day})
return os.date("%b %d, %Y", time)
end
-- function getRelatedNews(limit, title)
-- local tables = 'News'
-- local fields = '_pageName, date, tags, image, category, content'
-- local words = mw.text.split(title, ' ')
-- local conditions = {}
-- for _, word in ipairs(words) do
-- if #word > 2 then
-- table.insert(conditions, '_pageName LIKE "%' .. word .. '%"')
-- end
-- end
-- local whereClause = table.concat(conditions, ' OR ')
-- local cargoArgs = {
-- where = whereClause,
-- orderBy = 'date DESC',
-- limit = limit
-- }
-- local results = cargo.query(tables, fields, cargoArgs)
-- return results or {}
-- end
function getRelatedNews(limit, title)
local tables = 'News'
local fields = '_pageName, date, tags, image, category, content'
limit = tonumber(limit) or 4
-- Local helpers (scoped to this function)
local STOP = {
["the"]=true,["and"]=true,["for"]=true,["with"]=true,["from"]=true,["into"]=true,
["of"]=true,["in"]=true,["on"]=true,["at"]=true,["to"]=true,["by"]=true,["is"]=true,["are"]=true,
["a"]=true,["an"]=true,["as"]=true,["or"]=true,["if"]=true,["but"]=true,["not"]=true
}
local function escape_like(s)
return (s or ""):gsub("%%","%%%%"):gsub("_","__"):gsub('"','\\"')
end
local function normalize(s)
s = mw.ustring.lower(s or "")
s = s:gsub("^[^:]+:", "") -- strip namespace if present
s = s:gsub("[%p%c]", " ") -- punctuation/control -> space
s = s:gsub("%s+", " ")
return mw.text.trim(s)
end
local function terms_from_title(t)
local out, seen = {}, {}
for w in mw.ustring.gmatch(normalize(t), "[%w%d]+") do
if #w >= 3 and not STOP[w] and not seen[w] then
seen[w] = true
table.insert(out, w)
end
end
return out
end
-- Simple deterministic RNG (LCG) so "randoms" are stable per page title
local function stable_seed(s)
local h = 0
for i = 1, #s do
h = (h * 131 + string.byte(s, i)) % 2147483647
end
if h == 0 then h = 1 end
return h
end
local function shuffle_in_place(t, seed)
local mod = 2147483647
local a, c = 1103515245, 12345
local function rnd()
seed = (a * seed + c) % mod
return seed / mod
end
for i = #t, 2, -1 do
local j = math.floor(rnd() * i) + 1
t[i], t[j] = t[j], t[i]
end
end
local current = mw.title.getCurrentTitle().fullText
local terms = terms_from_title(title)
-- Build strict "related" filter: require >=2 distinct term matches (tags or title)
local results, picked = {}, {}
if #terms >= 2 then
table.sort(terms, function(a,b) return #a > #b end)
local MAX_TERMS = 6
while #terms > MAX_TERMS do table.remove(terms) end
local pairClauses = {}
for i = 1, #terms - 1 do
local ti = escape_like(terms[i])
local left = string.format('(tags HOLDS LIKE "%%%s%%" OR _pageName LIKE "%%%s%%")', ti, ti)
for j = i + 1, #terms do
local tj = escape_like(terms[j])
local right = string.format('(tags HOLDS LIKE "%%%s%%" OR _pageName LIKE "%%%s%%")', tj, tj)
table.insert(pairClauses, '(' .. left .. ' AND ' .. right .. ')')
end
end
local whereClause = '(' .. table.concat(pairClauses, ' OR ') ..
string.format(') AND _pageName != "%s"', escape_like(current))
local related = mw.ext.cargo.query(tables, fields, {
where = whereClause,
orderBy = 'date DESC',
limit = limit * 2
}) or {}
for _, r in ipairs(related) do
if not picked[r._pageName] then
table.insert(results, r)
picked[r._pageName] = true
if #results >= limit then break end
end
end
end
-- If not enough related, fill with random recent (excluding current & duplicates)
if #results < limit then
local recent = mw.ext.cargo.query(tables, fields, {
orderBy = 'date DESC',
limit = math.max(limit * 10, 40) -- pull a pool to randomize
}) or {}
-- filter out current and already picked
local pool = {}
for _, r in ipairs(recent) do
if r._pageName ~= current and not picked[r._pageName] then
table.insert(pool, r)
end
end
if #pool > 0 then
shuffle_in_place(pool, stable_seed(current))
for _, r in ipairs(pool) do
table.insert(results, r)
picked[r._pageName] = true
if #results >= limit then break end
end
end
end
-- Final guarantee: never empty. If still short (very small dataset), just return what we have.
return results
end
return p
No categories