Module:Signpost

local INDEX_MODULE = 'Module:Signpost/index' local lang = mw.language.getContentLanguage local libraryUtil = require('libraryUtil') local checkType = libraryUtil.checkType local checkTypeForNamedArg = libraryUtil.checkTypeForNamedArg

-- Article class

local Article = {} Article.__index = Article

Article.viewSpans = {7, 15, 30, 60, 90, 120, 180}

Article.rowMethods = { page    = 'getPage', fullpage = 'getFullPage', date    = 'getDate', title   = 'getTitle', subpage = 'getSubpage', author  = 'getAuthor', subhead = 'getSubhead', }

-- Register viewsNN row methods for _, span in ipairs(Article.viewSpans) do Article.rowMethods["views" .. span] = "getViews" .. span end

function Article.new(data) local self = setmetatable({}, Article) self.data = data self.matchedTags = {} return self end

function Article:getSortKey return self.data.sortKey end

function Article:getPage return self.data.page end

function Article:getDate return self.data.date end

function Article:getTitle return self.data.title end

function Article:getSubpage return self.data.subpage end

function Article:getAuthor return self.data.authors[1] end

function Article:getAuthors return self.data.authors end

-- Add getViewsNN methods for _, span in ipairs(Article.viewSpans) do Article["getViews" .. span] = function(self) if self.data.views then return self.data.views[string.format("d%03d", span)] else return nil end end end

function Article:getSubhead return self.data.subhead end

function Article:getFragment local fragment = self:getMatchedTags[1] if fragment then return mw.uri.anchorEncode(fragment) end end

function Article:getFullPage local page = self:getPage local fragment = self:getFragment if fragment then return page .. '#' .. fragment else return page end end

function Article:addMatchedTag(tag) table.insert(self.matchedTags, tag) end

function Article:getMatchedTags table.sort(self.matchedTags) return self.matchedTags end

function Article:hasAllTags(t) local tags = self.data.tags for i, testTag in ipairs(t) do		local hasTag = false for j, tag in ipairs(tags) do			if tag == testTag then hasTag = true end end if not hasTag then return false end end return true end

function Article:makeRowArgs local methods = self.rowMethods local args = setmetatable({}, {		__index = function (t, key)			local method = methods[key]			if method then				return self[method](self)			else				error(string.format(					"'%s' is not a valid parameter name",					key				), 2)			end		end	}) return args end

function Article:renderTemplate(template, frame) frame = frame or mw.getCurrentFrame local args = {} for key, method in pairs(self.rowMethods) do		args[key] = self[method](self) end return frame:expandTemplate{ title = template, args = args } end

function Article:renderFormat(format) local args = self:makeRowArgs(articleObj) local ret = format:gsub('(%${([%a%d]+)})', function (match, key)		return args[key] or match	end) return ret end

-- List class

local List = {} List.__index = List

function List.new(options) checkType('List.new', 1, options, 'table') checkTypeForNamedArg('List.new', 'args', options.args, 'table', true) local self = setmetatable({}, List) self.index = options.index or mw.loadData(INDEX_MODULE) self.frame = options.frame or mw.getCurrentFrame local args = options.args or {}

-- Set output formats if not options.suppressFormatErrors and args.rowtemplate and args.rowformat then error("you cannot use both the 'rowtemplate' and the 'rowformat' arguments", 2) elseif not options.suppressFormatErrors and not args.rowtemplate and not args.rowformat then error("you must use either the 'rowtemplate' or the 'rowformat' argument", 2) else self.rowtemplate = args.rowtemplate self.rowformat = args.rowformat end if args.rowseparator == 'newline' then self.rowseparator = '\n' else self.rowseparator = args.rowseparator end self.noarticles = args.noarticles -- Get article objects, filtered by page, date and tag, and sort them. if args.page then self.articles = { self:getPageArticle(args.page) } elseif args.date then self.articles = self:getDateArticles(args.date) else self.articles = self:getTagArticles(args.tags, args.tagmatch) if not self.articles then self.articles = self:getAllArticles end self:filterArticlesByDate(args.startdate, args.enddate) self:filterArticlesByAuthor(args.author) end self:sortArticles(args.sortdir, args.sortfield) if (args.limit and tonumber(args.limit)) or (args.start and tonumber(args.start)) then self:limitArticleCount(tonumber(args.start), tonumber(args.limit)) end return self end

-- Static methods

function List.normalizeDate(date) if not date then return nil end return lang:formatDate('Y-m-d', date) end

-- Normal methods

function List:parseTagString(s) local ret = {}

-- Remove whitespace and punctuation for i, tag in ipairs(mw.text.split(s, ',')) do		tag = mw.ustring.gsub(tag, '[%s%p]', '') if tag ~= '' then tag = mw.ustring.lower(tag) table.insert(ret, tag) end end

-- Resolve aliases for i, tag in ipairs(ret) do		ret[i] = self.index.aliases[tag] or tag end -- Remove duplicates local function removeDuplicates(t) local vals, ret = {}, {} for i, val in ipairs(t) do			vals[val] = true end for val in pairs(vals) do			table.insert(ret, val) end table.sort(ret) return ret end ret = removeDuplicates(ret)

return ret end

function List:getPageArticle(page) local data = self.index.pages[page] if data then return Article.new(data) end end

function List:getDateArticles(date) date = self.normalizeDate(date) local dates = self.index.dates[date] local ret = {} if dates then for i, data in ipairs(dates) do			ret[i] = Article.new(data) end end return ret end

function List:getTagArticles(s, tagMatch) if not s then return nil end local tagIndex = self.index.tags local ret, pages = {}, {} local tags = self:parseTagString(s) for i, tag in ipairs(tags) do		local dataArray = tagIndex[tag] if dataArray then for i, data in ipairs(dataArray) do				local obj = Article.new(data) -- Make sure we only have one object per page. if pages[obj:getPage] then obj = pages[obj:getPage] else pages[obj:getPage] = obj end -- Record which tag we matched. obj:addMatchedTag(tag) end end end for page, obj in pairs(pages) do		if not tagMatch or tagMatch == 'any' or tagMatch == 'all' and obj:hasAllTags(tags) then table.insert(ret, obj) end end return ret end

function List:getAllArticles local ret = {} for i, data in ipairs(self.index.list) do		ret[i] = Article.new(data) end return ret end

function List:getArticleCount return #self.articles end

function List:filterArticlesByDate(startDate, endDate) startDate = self.normalizeDate(startDate) or '2005-01-01' endDate = self.normalizeDate(endDate) or lang:formatDate('Y-m-d') local ret = {} for i, article in ipairs(self.articles) do		local date = article:getDate if startDate <= date and date <= endDate then table.insert(ret, article) end end self.articles = ret end

function List:filterArticlesByAuthor(targetAuthor) if not targetAuthor then return end local ret = {} for i, article in ipairs(self.articles) do		for j, author in ipairs(article:getAuthors) do			if author == targetAuthor then table.insert(ret, article) end end end self.articles = ret end

function List:sortArticles(direction, field) local accessor if not field or field == 'date' then accessor = function (article) return article:getSortKey end elseif field == 'page' then accessor = function (article) return article:getPage end elseif field == 'title' then accessor = function (article) return article:getTitle end else error(string.format("'%s' is not a valid sort field", field), 2) end local sortFunc if not direction or direction == 'ascending' then sortFunc = function (a, b)			return accessor(a) < accessor(b) end elseif direction == 'descending' then sortFunc = function (a, b)			return accessor(a) > accessor(b) end else error(string.format("'%s' is not a valid sort direction", direction), 2) end table.sort(self.articles, sortFunc) end

function List:limitArticleCount(start, limit) local ret = {} for i, article in ipairs(self.articles) do 		if limit and #ret >= limit then break end if not start or i > start then table.insert(ret, article) end end self.articles = ret end

function List:renderRow(articleObj) if self.rowtemplate then return articleObj:renderTemplate(self.rowtemplate, self.frame) elseif self.rowformat then return articleObj:renderFormat(self.rowformat) else error('neither rowtemplate nor rowformat were specified') end end

function List:__tostring local ret = {} for i, obj in ipairs(self.articles) do		table.insert(ret, self:renderRow(obj)) end if #ret < 1 then return self.noarticles or ' ' .. 'No articles found for the arguments specified ' else return table.concat(ret, self.rowseparator) end end

-- Exports

local p = {}

local function makeInvokeFunc(func) return function (frame, index) local args = require('Module:Arguments').getArgs(frame, {			parentOnly = true		}) return func(args, index) end end

function p._exportClasses return { Article = Article, List = List } end

function p._count(args, index) local list = List.new{ args = args, index = index, suppressFormatErrors = true }	return list:getArticleCount end

p.count = makeInvokeFunc(p._count)

function p._main(args, index) return tostring(List.new{args = args, index = index}) end

p.main = makeInvokeFunc(p._main)

return p