Module:STARDIT sources
Jump to navigation
Jump to search
Documentation for this module may be created at Module:STARDIT sources/doc
--[[
Module:STARDIT sources
Renders sources for STARDIT reports. Two functions:
cite - a small pointer shown after an entry, e.g. [S1] or [source]
{{#invoke:STARDIT sources|cite|src=...|page=...|quote=...}}
src = one or more source keys (S1, S2...) and/or web addresses,
separated by semicolons
page = optional page number(s)
quote = optional exact words from the source that support the entry
source - one entry in a report's Sources list
{{#invoke:STARDIT sources|source|src_key=...|src_url=...|...}}
Design rules:
* Fails open: anything not understood is shown as plain text, never hidden,
and the page is added to a tracking category for review.
* Shows what was entered as entered: titles, dates and quotes are not
reformatted or filled in.
* Field names map to Wikidata reference properties
(see Template:STARDIT source for the table).
]]
local p = {}
local TRACKING_CAT = '[[Category:STARDIT pages with unrecognised source pointers]]'
-- Quotation marks people often paste around a quote (double marks only,
-- so a real trailing apostrophe such as "patients'" is never removed).
local WRAP_MARKS = { '"', '\226\128\156', '\226\128\157', '\194\171', '\194\187' }
local function clean(s)
if s == nil then return nil end
s = mw.text.trim(tostring(s))
if s == '' then return nil end
return s
end
local function isUrl(s)
return s:match('^[Hh][Tt][Tt][Pp][Ss]?://') ~= nil
end
-- Source keys are S followed by a number (S1, S2, S10). "s01" becomes "S1".
local function normKey(s)
local n = s:match('^[Ss](%d+)$')
if n then return 'S' .. tostring(tonumber(n)) end
return nil
end
local function isContentPage()
local t = mw.title.getCurrentTitle()
return t ~= nil and t.namespace == 0
end
-- Make a web address safe inside [url label] link syntax.
local function sanitiseUrl(u)
u = u:gsub(' ', '%%20')
u = u:gsub('%[', '%%5B')
u = u:gsub('%]', '%%5D')
return u
end
-- Percent-encode everything except letters, digits and . ~ _
-- (dash, comma and ampersand MUST be encoded inside a text fragment).
local function pctEncode(s)
return (s:gsub('[^%w%.~_]', function(c)
return string.format('%%%02X', string.byte(c))
end))
end
local function stripWrapMarks(q)
local changed = true
while changed and q ~= '' do
changed = false
for _, m in ipairs(WRAP_MARKS) do
if q:sub(1, #m) == m then
q = mw.text.trim(q:sub(#m + 1))
changed = true
end
if #q >= #m and q:sub(-#m) == m then
q = mw.text.trim(q:sub(1, -#m - 1))
changed = true
end
end
end
return q
end
local function cleanQuote(q)
q = clean(q)
if not q then return nil end
q = q:gsub('%s+', ' ')
return clean(stripWrapMarks(q))
end
-- Short quotes are matched whole; longer ones by their first and last four
-- words, which survives line breaks and formatting in between.
local function textFragment(q)
local words = mw.text.split(q, ' ', true)
if #words <= 8 then
return 'text=' .. pctEncode(q)
end
local first = table.concat(words, ' ', 1, 4)
local last = table.concat(words, ' ', #words - 3, #words)
return 'text=' .. pctEncode(first) .. ',' .. pctEncode(last)
end
local function isPdf(u)
local l = u:lower()
local path = l:gsub('[#?].*$', '')
return path:match('%.pdf$') ~= nil or l:find('/pdf/', 1, true) ~= nil
end
-- Build the link for a web-address pointer: jump to the page of a PDF,
-- or highlight the quoted passage on a web page.
local function deepLink(u, page, quote)
u = sanitiseUrl(u)
if isPdf(u) then
local n = page and page:match('%d+')
if n and not u:find('#', 1, true) then
return u .. '#page=' .. n
end
return u
end
if quote then
if u:find('#', 1, true) then
return u .. ':~:' .. textFragment(quote)
end
return u .. '#:~:' .. textFragment(quote)
end
return u
end
local function sup(inner)
return '<sup class="stardit-src" style="white-space:nowrap;">' .. inner .. '</sup>'
end
local function pageText(page)
if page:match('^%d') then
return 'p. ' .. mw.text.nowiki(page)
end
return mw.text.nowiki(page)
end
function p.cite(frame)
local args = frame.args
local src = clean(args.src)
local page = clean(args.page)
local quote = cleanQuote(args.quote)
if not src and not page and not quote then
return ''
end
local out = {}
local flag = false
if src then
for _, token in ipairs(mw.text.split(src, ';', true)) do
token = clean(token)
if token then
local key = normKey(token)
if key then
table.insert(out, sup('[[#src-' .. key .. '|[' .. key .. ']]]'))
elseif isUrl(token) then
table.insert(out, sup('[' .. deepLink(token, page, quote) .. ' [source]]'))
else
table.insert(out, sup('[' .. mw.text.nowiki(token) .. ']'))
flag = true
end
end
end
else
-- a page number or quote with nothing to point at
flag = true
end
local details = {}
if page then table.insert(details, pageText(page)) end
if quote then table.insert(details, '\226\128\156' .. mw.text.nowiki(quote) .. '\226\128\157') end
local result = table.concat(out, '')
if #details > 0 then
result = result .. ' <span class="stardit-src-detail" style="font-size:85%;opacity:0.85;">('
.. table.concat(details, ': ') .. ')</span>'
end
if flag and isContentPage() then
result = result .. TRACKING_CAT
end
return result
end
function p.source(frame)
local a = frame.args
local key = clean(a.src_key)
local url = clean(a.src_url)
local title = clean(a.src_title)
local publisher = clean(a.src_publisher)
local date = clean(a.src_date)
local accessed = clean(a.src_accessed)
local arcUrl = clean(a.src_archive_url)
local arcDate = clean(a.src_archive_date)
local doi = clean(a.src_doi)
local stype = clean(a.src_type)
if not (key or url or title or doi or publisher) then
return ''
end
local flag = false
local nkey = key and normKey(key)
local pieces = {}
if nkey then
table.insert(pieces, "'''[" .. nkey .. "]'''")
elseif key then
table.insert(pieces, "'''[" .. mw.text.nowiki(key) .. "]'''")
flag = true
end
local titleText = title and mw.text.nowiki(title)
if url and isUrl(url) then
table.insert(pieces, '[' .. sanitiseUrl(url) .. ' ' .. (titleText or mw.text.nowiki(url)) .. '].')
else
if titleText then table.insert(pieces, "''" .. titleText .. "''.") end
if url then
table.insert(pieces, mw.text.nowiki(url) .. '.')
flag = true
end
end
local pubBits = {}
if publisher then table.insert(pubBits, mw.text.nowiki(publisher)) end
if date then table.insert(pubBits, mw.text.nowiki(date)) end
if #pubBits > 0 then table.insert(pieces, table.concat(pubBits, ', ') .. '.') end
if doi then
local d = doi:gsub('^[Hh][Tt][Tt][Pp][Ss]?://[Dd]?[Xx]?%.?[Dd][Oo][Ii]%.[Oo][Rr][Gg]/', '')
d = d:gsub('^[Dd][Oo][Ii]:%s*', '')
table.insert(pieces, 'doi:[https://doi.org/' .. sanitiseUrl(d) .. ' ' .. mw.text.nowiki(d) .. '].')
end
if stype then table.insert(pieces, mw.text.nowiki(stype) .. '.') end
if accessed then table.insert(pieces, 'Accessed ' .. mw.text.nowiki(accessed) .. '.') end
if arcUrl and isUrl(arcUrl) then
local s = '[' .. sanitiseUrl(arcUrl) .. ' Archived copy]'
if arcDate then s = s .. ' (' .. mw.text.nowiki(arcDate) .. ')' end
table.insert(pieces, s .. '.')
elseif arcUrl then
table.insert(pieces, 'Archive: ' .. mw.text.nowiki(arcUrl) .. '.')
flag = true
elseif arcDate then
table.insert(pieces, 'Archived ' .. mw.text.nowiki(arcDate) .. '.')
end
local idAttr = nkey and (' id="src-' .. nkey .. '"') or ''
local result = '<div class="stardit-source"' .. idAttr .. ' style="margin:0.3em 0;">'
.. table.concat(pieces, ' ') .. '</div>'
if flag and isContentPage() then
result = result .. TRACKING_CAT
end
return result
end
return p