Mô đun:Transcluder

-- Module:Transcluder is a general-purpose transclusion engine
-- Documentation and master version: https://en.wikipedia.org/wiki/Module:Transcluder
-- Authors: User:Sophivorus, User:Certes & others
-- License: CC-BY-SA-3.0

local p = {}

-- Helper function to test for truthy and falsy values
-- @todo Somehow internationalize it
local function truthy(value)
if not value or value or value 0 or value '0' or value 'false' or value 'no' or value 'non' then
return false
end
return true
end

-- Helper function to match from a list of regular expressions
-- Like so: match pre..list[1]..post or pre..list[2]..post or ...
local function matchAny(text, pre, list, post, init)
local match = {}
for i = 1, list do
match = { mw.ustring.match(text, pre .. list[i] .. post, init) }
if match[1] then return unpack(match) end
end
return nil
end

-- Like matchAny but for Category/File links with less overhead
local function matchAnyLink(text, list)
local match
for _, v in ipairs(list) do
match = string.match(text, '%[%[%s' .. v .. '%s:.%]%]')
if match then break end
end
return match
end

-- Helper function to escape a string for use in regexes
local function escapeString(str)
return string.gsub(str, '[%^%$%(%)%.%[%]%%+%-%?%%]', '%%%0')
end

-- Helper function to remove a string from a text
local function removeString(text, str)
local pattern = escapeString(str)
if pattern > 9999 then -- strings longer than 10000 bytes can't be put into regexes
pattern = escapeString(mw.ustring.sub(str, 1, 999)) .. '.-' .. escapeString(mw.ustring.sub(str, -999))
end
return string.gsub(text, pattern, )
end

-- Helper function to convert a comma-separated list of numbers or min-max ranges into a list of booleans
-- @param flags Comma-separated list of numbers or min-max ranges, for example '1,3-5'
-- @return Map from integers to booleans, for example {1=true,2=false,3=true,4=true,5=true}
-- @return Boolean indicating whether the flags should be treated as a blacklist or not
local function parseFlags(value)
local flags = {}
local blacklist = false

if not value then return nil, false end

if type(value) == 'number' then
if value tags
text = string.gsub(text, '.-', ) -- remove noinclude bits

-- Keep tags
if string.find(text, 'onlyinclude') then -- avoid expensive search if possible
text = text
:gsub('.-', ) -- remove text between onlyinclude sections
:gsub('^.-', ) -- remove text before first onlyinclude section
:gsub('.', ) -- remove text after last onlyinclude section
end

return text, title.prefixedText
end

-- Get the requested files from the given wikitext.
-- @param text Required. Wikitext to parse.
-- @param flags Range of files to return, for example 2 or '1,3-5'. Omit to return all files.
-- @return Sequence of strings containing the wikitext of the requested files.
-- @return Original wikitext minus requested files.
local function getFiles(text, flags)
local files = {}
local flags, blacklist = parseFlags(flags)
local fileNamespaces = getNamespaces('File')
local name
local count = 0
for file in string.gmatch(text, '%b[]') do
if matchAnyLink(file, fileNamespaces) then
name = string.match(file, '%[%[[^:]-:([^]|]+)')
count = count + 1
if not blacklist and ( not flags or flags[count] or matchFlag(name, flags) )
or blacklist and flags and not flags[count] and not matchFlag(name, flags) then
table.insert(files, file)
else
text = removeString(text, file)
end
end
end

return files, text
end

-- Get the requested tables from the given wikitext.
-- @param text Required. Wikitext to parse.
-- @param flags Range of tables to return, for example 2 or '1,3-5'. Omit to return all tables.
-- @return Sequence of strings containing the wikitext of the requested tables.
-- @return Original wikitext minus requested tables.
local function getTables(text, flags)
local tables = {}
local flags, blacklist = parseFlags(flags)
local id
local count = 0
for t in string.gmatch('\n' .. text, '\n%b{}') do
if string.sub(t, 1, 3) == '\n]-|(.)}}')
if params then
count = 0
-- Temporarily replace pipes in subtemplates and links to avoid chaos
for subtemplate in string.gmatch(params, '{%b{}}') do
params = string.gsub(params, escapeString(subtemplate), string.gsub(subtemplate, ".", {["%"]="%%", ["|"]="@@:@@", ["="]="@@_@@"}) )
end
for link in string.gmatch(params, '%b[]') do
params = string.gsub(params, escapeString(link), string.gsub(link, ".", {["%"]="%%", ["|"]="@@:@@", ["="]="@@_@@"}) )
end
for parameter in mw.text.gsplit(params, '|') do
parts = mw.text.split(parameter, '=')
key = mw.text.trim(parts[1])
if parts == 1 then
value = key
count = count + 1
key = count
else
value = mw.text.trim(table.concat(parts, '=', 2))
end
value = string.gsub(string.gsub(value, '@@:@@', '|'), '@@_@@', '=')
if not blacklist and ( not flags or matchFlag(key, flags) )
or blacklist and flags and not matchFlag(key, flags) then
table.insert(parameterOrder, key)
parameters[key] = value
else
text = removeString(text, parameter)
end
end
end
end
return parameters, text, parameterOrder
end

-- Get the requested lists from the given wikitext.
-- @param text Required. Wikitext to parse.
-- @param flags Range of lists to return, for example 2 or '1,3-5'. Omit to return all lists.
-- @return Sequence of strings containing the wikitext of the requested lists.
-- @return Original wikitext minus requested lists.
local function getLists(text, flags)
local lists = {}
local flags, blacklist = parseFlags(flags)
local count = 0
for list in string.gmatch('\n' .. text .. '\n\n', '\n([].-)\n[^]') do
count = count + 1
if not blacklist and ( not flags or flags[count] )
or blacklist and flags and not flags[count] then
table.insert(lists, list)
else
text = removeString(text, list)
end
end
return lists, text
end

-- Get the requested paragraphs from the given wikitext.
-- @param text Required. Wikitext to parse.
-- @param flags Range of paragraphs to return, for example 2 or '1,3-5'. Omit to return all paragraphs.
-- @return Sequence of strings containing the wikitext of the requested paragraphs.
-- @return Original wikitext minus requested paragraphs.
local function getParagraphs(text, flags)
local paragraphs = {}
local flags, blacklist = parseFlags(flags)

-- Remove non-paragraphs
local elements
local temp = '\n' .. text .. '\n'
elements, temp = getLists(temp, 0) -- remove lists
elements, temp = getFiles(temp, 0) -- remove files
temp = mw.text.trim((temp
:gsub('\n%b{} \n', '\n%0\n') -- add spacing between tables and block templates
:gsub('\n%b{} \n', '\n') -- remove tables and block templates
:gsub('\n==+[^=]+==+ \n', '\n') -- remove section titles
))

-- Assume that anything remaining is a paragraph
local count = 0
for paragraph in mw.text.gsplit(temp, '\n\n+') do
if mw.text.trim(paragraph) ~= then
count = count + 1
if not blacklist and ( not flags or flags[count] )
or blacklist and flags and not flags[count] then
table.insert(paragraphs, paragraph)
else
text = removeString(text, paragraph)
end
end
end

return paragraphs, text
end

-- Get the requested categories from the given wikitext.
-- @param text Required. Wikitext to parse.
-- @param flags Range of categories to return, for example 2 or '1,3-5'. Omit to return all categories.
-- @return Sequence of strings containing the wikitext of the requested categories.
-- @return Original wikitext minus requested categories.
local function getCategories(text, flags)
local categories = {}
local flags, blacklist = parseFlags(flags)
local categoryNamespaces = getNamespaces('Category')
local name
local count = 0
for category in string.gmatch(text, '%b[]') do
if matchAnyLink(category, categoryNamespaces) then
name = string.match(category, '%[%[[^:]-:([^]|]+)')
count = count + 1
if not blacklist and ( not flags or flags[count] or matchFlag(name, flags) )
or blacklist and flags and not flags[count] and not matchFlag(name, flags) then
table.insert(categories, category)
else
text = removeString(text, category)
end
end
end
return categories, text
end

-- Get the requested references from the given wikitext.
-- @param text Required. Wikitext to parse.
-- @param flags Range of references to return, for example 2 or '1,3-5'. Omit to return all references.
-- @return Sequence of strings containing the wikitext of the requested references.
-- @return Original wikitext minus requested references.
local function getReferences(text, flags)
local references = {}

-- Remove all references, including citations, when 0 references are requested
-- This is kind of hacky but currently necessary because the rest of the code
-- doesn't remove citations like if Foo is defined elsewhere
if flags and not truthy(flags) then
text = string.gsub(text, '/]>.-', )
text = string.gsub(text, '/]/%s>', )
return references, text
end

local flags, blacklist = parseFlags(flags)
local name
local count = 0
for reference in string.gmatch(text, '/]>.-') do
name = string.match(reference, ']name%s=%s["\']?([^"\'>/]+)["\']?[^>]%s>')
count = count + 1
if not blacklist and ( not flags or flags[count] or matchFlag(name, flags) )
or blacklist and flags and not flags[count] and not matchFlag(name, flags) then
table.insert(references, reference)
else
text = removeString(text, reference)
if name then
for citation in string.gmatch(text, ']name%s=%s["\']?' .. escapeString(name) .. '["\']?[^/>]/%s>') do
text = removeString(text, citation)
end
end
end
end
return references, text
end

-- Get the lead section from the given wikitext.
-- @param text Required. Wikitext to parse.
-- @return Wikitext of the lead section.
local function getLead(text)
text = string.gsub('\n' .. text, '\n==.', )
text = mw.text.trim(text)
if not text then return throwError('lead-empty') end
return text
end

-- Get the requested sections from the given wikitext.
-- @param text Required. Wikitext to parse.
-- @param flags Range of sections to return, for example 2 or '1,3-5'. Omit to return all sections.
-- @return Sequence of strings containing the wikitext of the requested sections.
-- @return Original wikitext minus requested sections.
local function getSections(text, flags)
local sections = {}
local flags, blacklist = parseFlags(flags)
local count = 0
local prefix, section, suffix
for title in string.gmatch('\n' .. text .. '\n', '\n+%s([^=]+)%s==+') do
count = count + 1
prefix, section, suffix = string.match('\n' .. text .. '\n', '\n()+%s' .. escapeString(title) .. '%s+(.-)()\n')
if not blacklist and ( not flags or flags[count] or matchFlag(title, flags) )
or blacklist and flags and not flags[count] and not matchFlag(title, flags) then
sections[title] = section
else
text = string.sub(text, 1, prefix) .. string.sub(text, suffix)
text = string.gsub(text, '\n?$', ) -- remove the trailing \n
end
end
return sections, text
end

-- Get the requested section or tag from the given wikitext (including subsections).
-- @param text Required. Wikitext to parse.
-- @param section Required. Title of the section to get (in wikitext), for example 'History' or 'History of Athens'.
-- @return Wikitext of the requested section.
local function getSection(text, section)
section = mw.text.trim(section)
local escapedSection = escapeString(section)
-- First check if the section title matches a tag
if string.find(text, ) then -- avoid expensive search if possible
text = mw.text.trim((text
:gsub('.-', ) -- remove text between section tags
:gsub('^.-', ) -- remove text before first section tag
:gsub('.', ) -- remove text after last section tag
))
if text == then return throwError('section-tag-empty', section) end
return text
end
local level, text = string.match('\n' .. text .. '\n', '\n(+)%s' .. escapedSection .. '%s.-\n(.)')
if not text then return throwError('section-not-found', section) end
local nextSection = '\n==' .. string.rep('=?', level - 2) .. '[^=].'
text = string.gsub(text, nextSection, ) -- remove later sections with headings at this level or higher
text = mw.text.trim(text)
if text == then return throwError('section-empty', section) end
return text
end

-- Replace the first call to each reference defined outside of the text for the full reference, to prevent undefined references
-- Then prefix the page title to the reference names to prevent conflicts
-- that is, replace for
-- also remove reference groups: for
-- and for
-- @todo The current regex may fail in cases with both kinds of quotes, like
local function fixReferences(text, page, full)
if not full then full = getText(page) end
local refNames = {}
local refName
local refBody
local position = 1
while position ]name%s=%s["\']?([^"\'>]+)["\']?[^>]/%s>()', position)
if refName then
refName = mw.text.trim(refName)
if not refNames[refName] then -- make sure we process each ref name only once
table.insert(refNames, refName)
refName = escapeString(refName)
refBody = mw.ustring.match(text, ']name%s=%s["\']?%s' .. refName .. '%s["\']?[^>/]>.-')
if not refBody then -- the ref body is not in the excerpt
refBody = mw.ustring.match(full, ']name%s=%s["\']?%s' .. refName .. '%s["\']?[^/>]>.-')
if refBody then -- the ref body was found elsewhere
text = mw.ustring.gsub(text, ']name%s=%s["\']?%s' .. refName .. '%s["\']?[^>]/?%s>', mw.ustring.gsub(refBody, '%%', '%%%%'), 1)
end
end
end
else
position = mw.ustring.len(text)
end
end
page = string.gsub(page, '"', ) -- remove any quotation marks from the page title
text = mw.ustring.gsub(text, ']name%s=%s["\']?([^"\'>/]+)["\']?[^>/](/?)%s>', )
text = mw.ustring.gsub(text, '/]+["\']%s>', )
return text
end

-- Replace the bold title or synonym near the start of the page by a link to the page
local function linkBold(text, page)
local lang = mw.language.getContentLanguage()
local position = mw.ustring.find(text, "" .. lang:ucfirst(page) .. "", 1, true) -- look for "Foo is..." (uc) or "A foo is..." (lc)
or mw.ustring.find(text, "" .. lang:lcfirst(page) .. "", 1, true) -- plain search: special characters in page represent themselves
if position then
local length = mw.ustring.len(page)
text = mw.ustring.sub(text, 1, position + 2) .. "" .. mw.ustring.sub(text, position + 3, position + length + 2) .. "" .. mw.ustring.sub(text, position + length + 3, -1) -- link it
else -- look for anything unlinked in bold, assumed to be a synonym of the title (e.g. a person's birth name)
text = mw.ustring.gsub(text, "()(.-')", function(a, b)
if not mw.ustring.find(b, "%[") and not mw.ustring.find(b, "%{") then -- if not wikilinked or some weird template
return "" .. b .. "" -- replace Foo by Foo
else
return nil -- instruct gsub to make no change
end
end, 1) -- "end" here terminates the anonymous replacement function(a, b) passed to gsub
end
return text
end

-- Remove non-free files.
-- @param text Required. Wikitext to clean.
-- @return Clean wikitext.
local function removeNonFreeFiles(text)
local fileNamespaces = getNamespaces('File')
local fileName
local fileDescription
local frame = mw.getCurrentFrame()
for file in string.gmatch(text, '%b[]') do
if matchAnyLink(file, fileNamespaces) then
fileName = 'File:' .. string.match(file, '%[%[[^:]-:([^]|]+)')
fileDescription, fileName = getText(fileName)
if fileName then
if not fileDescription or fileDescription == then
fileDescription = frame:preprocess() -- try Commons
end
if fileDescription and string.match(fileDescription, '[Nn]on%-free') then
text = removeString(text, file)
end
end
end
end
return text
end

-- Remove any self links
local function removeSelfLinks(text)
local lang = mw.language.getContentLanguage()
local page = escapeString(mw.title.getCurrentTitle().prefixedText)
local ucpage = lang:ucfirst(page)
local lcpage = lang:lcfirst(page)
text = text
:gsub('%[%[(' .. ucpage .. ')%]%]', '%1')
:gsub('%[%[(' .. lcpage .. ')%]%]', '%1')
:gsub('%[%[' .. ucpage .. '|([^]]+)%]%]', '%1')
:gsub('%[%[' .. lcpage .. '|([^]]+)%]%]', '%1')
return text
end

-- Remove all wikilinks
local function removeLinks(text)
text = text
:gsub('%[%[[^%]|]+|([^]]+)%]%]', '%1')
:gsub('%[%[([^]]+)%]%]', '%1')
:gsub('%[[^ ]+ ([^]]+)%]', '%1')
:gsub('%[([^]]+)%]', '%1')
return text
end

-- Remove HTML comments
local function removeComments(text)
text = string.gsub(text, , )
return text
end

-- Remove behavior switches, such as __NOTOC__
local function removeBehaviorSwitches(text)
text = string.gsub(text, '__[A-Z]+__', )
return text
end

-- Remove bold text
local function removeBold(text)
text = string.gsub(text, "", )
return text
end

-- Main function for modules
local function get(page, options)
if not options then options = {} end

-- Make sure the page exists
if not page then return throwError('no-page') end
page = mw.text.trim(page)
page = mw.text.decode(page)
if page == then return throwError('no-page') end
local page, hash, section = string.match(page, '([^]+)(?)(.)')
local text, temp = getText(page, options.noFollow)
if not temp then return throwError('invalid-title', page) end
page = temp
if not text then return throwError('page-not-found', page) end
local full = text -- save the full text for fixReferences below

-- Get the requested section
if truthy(section) then
text = getSection(text, section)
elseif truthy(hash) then
text = getLead(text)
end

-- Keep only the requested elements
local elements
if options.only then
if options.only == 'sections' then elements = getSections(text, options.sections) end
if options.only == 'lists' then elements = getLists(text, options.lists) end
if options.only == 'files' then elements = getFiles(text, options.files) end
if options.only == 'tables' then elements = getTables(text, options.tables) end
if options.only == 'templates' then elements = getTemplates(text, options.templates) end
if options.only == 'parameters' then elements = getParameters(text, options.parameters) end
if options.only == 'paragraphs' then elements = getParagraphs(text, options.paragraphs) end
if options.only == 'categories' then elements = getCategories(text, options.categories) end
if options.only == 'references' then elements = getReferences(text, options.references) end
text =
if elements then
for key, element in pairs(elements) do
text = text .. '\n' .. element .. '\n'
end
end
end

-- Filter the requested elements
if options.sections and options.only ~= 'sections' then elements, text = getSections(text, options.sections) end
if options.lists and options.only ~= 'lists' then elements, text = getLists(text, options.lists) end
if options.files and options.only ~= 'files' then elements, text = getFiles(text, options.files) end
if options.tables and options.only ~= 'tables' then elements, text = getTables(text, options.tables) end
if options.templates and options.only ~= 'templates' then elements, text = getTemplates(text, options.templates) end
if options.parameters and options.only ~= 'parameters' then elements, text = getParameters(text, options.parameters) end
if options.paragraphs and options.only ~= 'paragraphs' then elements, text = getParagraphs(text, options.paragraphs) end
if options.categories and options.only ~= 'categories' then elements, text = getCategories(text, options.categories) end
if options.references and options.only ~= 'references' then elements, text = getReferences(text, options.references) end

-- Misc options
if truthy(options.fixReferences) then text = fixReferences(text, page, full) end
if truthy(options.linkBold) and not truthy(section) then text = linkBold(text, page) end
if truthy(options.noBold) then text = removeBold(text) end
if truthy(options.noLinks) then text = removeLinks(text) end
if truthy(options.noSelfLinks) then text = removeSelfLinks(text) end
if truthy(options.noNonFreeFiles) then text = removeNonFreeFiles(text) end
if truthy(options.noBehaviorSwitches) then text = removeBehaviorSwitches(text) end
if truthy(options.noComments) then text = removeComments(text) end

-- Remove multiple newlines left over from removing elements
text = string.gsub(text, '\n\n\n+', '\n\n')
text = mw.text.trim(text)

return text
end

-- Main invocation function for templates
local function main(frame)
local args = parseArgs(frame)
local page = args[1]
local ok, text = pcall(get, page, args)
if not ok then return getError(text) end
local raw = args['raw']
if raw then return text end
return frame:preprocess(text)
end

-- Entry points for templates
function p.main(frame) return main(frame) end

-- Entry points for modules
function p.get(page, options) return get(page, options) end
function p.getText(page, noFollow) return getText(page, noFollow) end
function p.getLead(text) return getLead(text) end
function p.getSection(text, section) return getSection(text, section) end
function p.getSections(text, flags) return getSections(text, flags) end
function p.getParagraphs(text, flags) return getParagraphs(text, flags) end
function p.getParameters(text, flags) return getParameters(text, flags) end
function p.getCategories(text, flags) return getCategories(text, flags) end
function p.getReferences(text, flags) return getReferences(text, flags) end
function p.getTemplates(text, flags) return getTemplates(text, flags) end
function p.getTables(text, flags) return getTables(text, flags) end
function p.getLists(text, flags) return getLists(text, flags) end
function p.getFiles(text, flags) return getFiles(text, flags) end
function p.getError(message, value) return getError(message, value) end

-- Expose handy methods
function p.truthy(value) return truthy(value) end
function p.parseArgs(frame) return parseArgs(frame) end
function p.matchAny(text, pre, list, post, init) return matchAny(text, pre, list, post, init) end
function p.matchFlag(value, flags) return matchFlag(value, flags) end
function p.getNamespaces(name) return getNamespaces(name) end
function p.removeBold(text) return removeBold(text) end
function p.removeLinks(text) return removeLinks(text) end
function p.removeSelfLinks(text) return removeSelfLinks(text) end
function p.removeNonFreeFiles(text) return removeNonFreeFiles(text) end
function p.removeBehaviorSwitches(text) return removeBehaviorSwitches(text) end
function p.removeComments(text) return removeComments(text) end

return p