Difference between revisions of "Module:Excerpt"

From blackwiki
Jump to navigation Jump to search
blackwiki>Sophivorus
(Critical bugfix: regex was matching nested templates as well as the main template being evaluated)
blackwiki>Sophivorus
(Update to new version)
Line 3: Line 3:
  
 
local p = {}
 
local p = {}
 
-- Helper function to debug
 
-- Returns blank text or an error message if requested
 
local errors
 
local function err(msg,a,b)
 
local text = mw.ustring.format(d.error[msg] or msg or '',a,b)
 
if errors then error(text, 2) end
 
return ""
 
end
 
  
 
-- Helper function to test for truthy and falsy values
 
-- Helper function to test for truthy and falsy values
Line 19: Line 10:
 
end
 
end
 
return true
 
return true
 +
end
 +
 +
-- Error handling function
 +
-- Throws a Lua error or returns an empty string if error reporting is disabled
 +
errors = true -- show errors by default
 +
local function luaError(message, value)
 +
if not is(errors) then return "" end -- error reporting is disabled
 +
message = d.errors[message] or message or ""
 +
message = mw.ustring.format(message, value)
 +
error(message, 2)
 +
end
 +
 +
-- Error handling function
 +
-- Returns a wiki friendly error or an empty string if error reporting is disabled
 +
local function wikiError(message, value)
 +
if not is(errors) then return "" end -- error reporting is disabled
 +
message = d.errors[message] or message or ""
 +
message = mw.ustring.format(message, value)
 +
message = d.errors.prefix .. message
 +
if mw.title.getCurrentTitle().isContentPage then
 +
local errorsCategory = mw.title.new(d.errorsCategory, 'Category')
 +
if errorsCategory then message = message .. '[[' .. errorsCategory.prefixedText .. ']]' end
 +
end
 +
message = mw.html.create('div'):addClass('error'):wikitext(message)
 +
return message
 
end
 
end
  
Line 32: Line 48:
 
end
 
end
  
-- Help gsub to remove unwanted templates and pseudo-templates such as #tag:ref and DEFAULTSORT
+
-- Helper function to convert imagemaps into standard images
 +
local function convertImageMap(imagemap)
 +
local image = matchAny(imagemap, "[>\n]%s*", d.fileNamespaces, "[^\n]*")
 +
if image then
 +
return "<!--imagemap-->[[" .. mw.ustring.gsub(image, "[>\n]%s*", "", 1) .. "]]"
 +
else
 +
return "" -- remove entire block if image can't be extracted
 +
end
 +
end
 +
 
 +
-- Helper function to convert a comma-separated list of numbers or min-max ranges into a list of booleans
 +
-- For example: "1,3-5" to {1=true,2=false,3=true,4=true,5=true}
 +
local function numberFlags(str)
 +
if not str then return {} end
 +
local flags = {}
 +
local ranges = mw.text.split(str, ",") -- parse ranges: "1,3-5" to {"1","3-5"}
 +
for _, r in pairs(ranges) do
 +
local min, max = mw.ustring.match(r, "^%s*(%d+)%s*%-%s*(%d+)%s*$") -- "3-5" to min=3 max=5
 +
if not max then min, max = mw.ustring.match(r, "^%s*((%d+))%s*$") end -- "1" to min=1 max=1
 +
if max then
 +
for p = min, max do flags[p] = true end
 +
end
 +
end
 +
return flags
 +
end
 +
 
 +
-- Helper function to convert template arguments into an array of arguments fit for get()
 +
local function parseArgs(frame)
 +
local args = {}
 +
for key, value in pairs(frame:getParent().args) do args[key] = value end
 +
for key, value in pairs(frame.args) do args[key] = value end -- args from a Lua call have priority over parent args from template
 +
args.paraflags = numberFlags(args["paragraphs"] or "") -- parse paragraphs: "1,3-5" to {"1","3-5"}
 +
args.fileflags = numberFlags(args["files"] or "") -- parse file numbers
 +
return args
 +
end
 +
 
 +
-- Helper function to remove unwanted templates and pseudo-templates such as #tag:ref and DEFAULTSORT
 
local function stripTemplate(t)
 
local function stripTemplate(t)
 
-- If template is unwanted then return "" (gsub will replace by nothing), else return nil (gsub will keep existing string)
 
-- If template is unwanted then return "" (gsub will replace by nothing), else return nil (gsub will keep existing string)
Line 55: Line 107:
 
end
 
end
  
-- Get a page's content, following redirects, and processing file description pages for files.
+
-- Get a page's content, following redirects
 
-- Also returns the page name, or the target page name if a redirect was followed, or false if no page found
 
-- Also returns the page name, or the target page name if a redirect was followed, or false if no page found
local function getContent(page, frame)
+
-- For file pages, returns the content of the file description page
local title = mw.title.new(page) -- Read description page (for :File:Foo rather than File:Foo)
+
local function getContent(page)
 +
local title = mw.title.new(page)
 
if not title then return false, false end
 
if not title then return false, false end
  
Line 65: Line 118:
  
 
return title:getContent(), title.prefixedText
 
return title:getContent(), title.prefixedText
 +
end
 +
 +
-- Get the tables only
 +
local function getTables(text, options)
 +
local tables = {}
 +
for candidate in mw.ustring.gmatch(text, "%b{}") do
 +
if mw.ustring.sub(candidate, 1, 2) == '{|' then
 +
table.insert(tables, candidate)
 +
end
 +
end
 +
return table.concat(tables, '\n')
 +
end
 +
 +
-- Get the lists only
 +
local function getLists(text, options)
 +
local lists = {}
 +
for list in mw.ustring.gmatch(text, "\n[*#][^\n]+") do
 +
table.insert(lists, list)
 +
end
 +
return table.concat(lists, '\n')
 
end
 
end
  
Line 72: Line 145:
 
if not page then return false end
 
if not page then return false end
  
-- Limit to image types: .gif, .jpg, .jpeg, .png, .svg, .tiff, .xcf (exclude .ogg audio etc.)
+
-- Limit to image types: .gif, .jpg, .jpeg, .png, .svg, .tiff, .xcf (exclude .ogg, audio, etc.)
if not matchAny(page, "%.", {"[Gg][Ii][Ff]", "[Jj][Pp][Ee]?[Gg]", "[Pp][Nn][Gg]", "[Ss][Vv][Gg]", "[Tt][Ii][Ff][Ff]", "[Xx][Cc][Ff]"}, "%s*$") then
+
local fileTypes = {"[Gg][Ii][Ff]", "[Jj][Pp][Ee]?[Gg]", "[Pp][Nn][Gg]", "[Ss][Vv][Gg]", "[Tt][Ii][Ff][Ff]", "[Xx][Cc][Ff]"}
return false
+
if not matchAny(page, "%.", fileTypes, "%s*$") then return false end
end
 
  
 +
-- Check the local wiki
 
local fileDescription, fileTitle = getContent(page) -- get file description and title after following any redirect
 
local fileDescription, fileTitle = getContent(page) -- get file description and title after following any redirect
if fileDescription and fileDescription ~= "" then -- found description on local wiki
+
if not fileTitle or fileTitle == "" then return false end -- the image doesn't exist
if mw.ustring.match(fileDescription, "[Nn]on%-free") then return false end
+
 
fileDescription = mw.ustring.gsub(fileDescription, "%b{}", stripTemplate) -- remove DEFAULTSORT etc. to avoid side effects of frame:preprocess
+
-- Check Commons
elseif not fileTitle then
+
if not fileDescription or fileDescription == "" then
return false
+
local frame = mw.getCurrentFrame()
else
+
fileDescription = frame:preprocess("{{" .. fileTitle .. "}}")
-- try commons
 
fileDescription = "{{" .. fileTitle .. "}}"
 
 
end
 
end
frame = frame or mw.getCurrentFrame()
 
fileDescription = frame:preprocess(fileDescription)
 
  
return ( fileDescription and fileDescription ~= "" and not mw.ustring.match(fileDescription, "[Nn]on%-free") ) and true or false -- hide non-free image
+
-- Filter non-free images
 +
if not fileDescription or fileDescription == "" or mw.ustring.match(fileDescription, "[Nn]on%-free") then return false end
 +
 
 +
return true
 
end
 
end
  
Line 303: Line 375:
 
return imageTokens
 
return imageTokens
 
end
 
end
 
-- Help gsub convert imagemaps into standard images
 
local function convertImageMap(imagemap)
 
local image = matchAny(imagemap, "[>\n]%s*", d.fileNamespaces, "[^\n]*")
 
if image then
 
return "<!--imagemap-->[[" .. mw.ustring.gsub(image, "[>\n]%s*", "", 1) .. "]]"
 
else
 
return "" -- remove entire block if image can't be extracted
 
end
 
end
 
 
-- Convert a comma-separated list of numbers or min-max ranges into a list of booleans, e.g. "1,3-5" → {1=true,2=false,3=true,4=true,5=true}
 
local function numberFlags(str)
 
local ranges = mw.text.split(str, ",") -- parse ranges, e.g. "1,3-5" → {"1","3-5"}
 
local flags = {}
 
for _, r in pairs(ranges) do
 
local min, max = mw.ustring.match(r, "^%s*(%d+)%s*%-%s*(%d+)%s*$") -- "3-5" → min=3 max=5
 
if not max then min, max = mw.ustring.match(r, "^%s*((%d+))%s*$") end -- "1" → min=1 max=1
 
if max then
 
for p = min, max do flags[p] = true end
 
end
 
end
 
return flags
 
end
 
 
local imageArgGroups = {
 
{"thumb", "thumbnail", "frame", "framed", "frameless"},
 
{"right", "left", "center", "none"},
 
{"baseline", "middle", "sub", "super", "text-top", "text-bottom", "top", "bottom"}
 
}
 
  
 
local function modifyImage(image, fileArgs)
 
local function modifyImage(image, fileArgs)
Line 339: Line 381:
 
local fa = mw.ustring.gsub(filearg, "=.*", "") -- "upright=0.75" → "upright"
 
local fa = mw.ustring.gsub(filearg, "=.*", "") -- "upright=0.75" → "upright"
 
local group = {fa} -- group of "border" is ["border"]...
 
local group = {fa} -- group of "border" is ["border"]...
for _, g in pairs(imageArgGroups) do
+
for _, g in pairs(d.imageParams) do
 
for _, a in pairs(g) do
 
for _, a in pairs(g) do
 
if fa == a then group = g end -- ...but group of "left" is ["right", "left", "center", "none"]
 
if fa == a then group = g end -- ...but group of "left" is ["right", "left", "center", "none"]
Line 358: Line 400:
 
--  @param text : Wikitext to be processed
 
--  @param text : Wikitext to be processed
 
--  @param options : A table of options...
 
--  @param options : A table of options...
--          options.paraflags : Which number paragraphs to keep, as either a string (e.g. `1,3-5`) or a table (e.g. `{1=true,2=false,3=true,4=true,5=true}`. If not present, all paragraphs will be kept.
+
--          options.paraflags : Which number paragraphs to keep, as either a string (e.g. '1,3-5') or a table (e.g. {1=true,2=false,3=true,4=true,5=true}. If not present, all paragraphs will be kept.
--          options.fileflags : table of which files to keep, as either a string (e.g. `1,3-5`) or a table (e.g. `{1=true,2=false,3=true,4=true,5=true}`
+
--          options.fileflags : table of which files to keep, as either a string (e.g. '1,3-5') or a table (e.g. {1=true,2=false,3=true,4=true,5=true}
--          options.fileargs : args for the [[File:]] syntax, such as `left`
+
--          options.fileargs : args for the [[File:]] syntax, such as 'left'
--   @param filesOnly : If set, only return the files and not the prose
+
-- options.filesOnly : only return the files and not the prose
local function parse(text, options, filesOnly)
+
local function parse(text, options)
 
local allParagraphs = true -- keep all paragraphs?
 
local allParagraphs = true -- keep all paragraphs?
 
if options.paraflags then
 
if options.paraflags then
Line 370: Line 412:
 
end
 
end
 
end
 
end
if filesOnly then
+
if is(options.filesOnly) then
 
allParagraphs = false
 
allParagraphs = false
 
options.paraflags = {}
 
options.paraflags = {}
Line 422: Line 464:
 
if token then -- found a template which is not the prefix to a line of text
 
if token then -- found a template which is not the prefix to a line of text
  
if leadStart then -- lead has already started, so keep the template within the text, unless it's a whole line (navbox etc.)
+
if is(options.keepTables) and mw.ustring.sub(token, 1, 2) == '{|' then
if not filesOnly and not startLine then t = t .. token end
+
t = t .. token -- keep tables
 +
 
 +
elseif mw.ustring.sub(token, 1, 3) == '{{#' then
 +
t = t .. token -- keep parser functions
 +
 
 +
elseif leadStart then -- lead has already started, so keep the template within the text, unless it's a whole line (navbox etc.)
 +
if not is(options.filesOnly) and not startLine then t = t .. token end
  
 
elseif matchAny(token, "^{{%s*", d.wantedBlockTemplates, "%s*%f[|}]") then
 
elseif matchAny(token, "^{{%s*", d.wantedBlockTemplates, "%s*%f[|}]") then
 
t = t .. token -- keep wanted block templates
 
t = t .. token -- keep wanted block templates
 
elseif is(options.keepTables) and mw.ustring.sub(token, 1, 2) == '{|' then
 
t = t .. token -- keep tables
 
  
 
elseif files < maxfile then -- discard template, but if we are still collecting images...
 
elseif files < maxfile then -- discard template, but if we are still collecting images...
Line 484: Line 529:
  
 
text = mw.ustring.gsub(t, "\n+$", "") -- remove trailing line feeds, so "{{Transclude text excerpt|Foo}} more" flows on one line
 
text = mw.ustring.gsub(t, "\n+$", "") -- remove trailing line feeds, so "{{Transclude text excerpt|Foo}} more" flows on one line
return fileText, text
+
 
 +
return fileText .. text
 
end
 
end
  
Line 520: Line 566:
 
local escapedSection = mw.ustring.gsub(mw.uri.decode(section), "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") -- %26 → & etc, then ^ → %^ etc.
 
local escapedSection = mw.ustring.gsub(mw.uri.decode(section), "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") -- %26 → & etc, then ^ → %^ etc.
 
local level, content = mw.ustring.match(text .. "\n", "\n(==+)%s*" .. escapedSection .. "%s*==.-\n(.*)")
 
local level, content = mw.ustring.match(text .. "\n", "\n(==+)%s*" .. escapedSection .. "%s*==.-\n(.*)")
if not content then return nil end -- no such section
+
if not content then return luaError("sectionNotFound", section) end
 
local nextSection
 
local nextSection
 
if mainOnly then
 
if mainOnly then
Line 528: Line 574:
 
end
 
end
 
content = mw.ustring.gsub(content, nextSection, "") -- remove later sections with headings at this level or higher
 
content = mw.ustring.gsub(content, nextSection, "") -- remove later sections with headings at this level or higher
 +
if mw.ustring.match(content, "^%s*$") then return luaError("sectionEmpty", section) end
 
return content
 
return content
 +
end
 +
 +
-- Parse a <section begin="Name of the fragment">
 +
-- @todo Implement custom parsing of fragments rather than relying on #lst
 +
local function getFragment(page, fragment)
 +
local frame = mw.getCurrentFrame()
 +
local text = frame:callParserFunction('#lst', page, fragment)
 +
if mw.ustring.match(text, "^%s*$") then return luaError("fragmentEmpty", fragment) end
 +
return text
 
end
 
end
  
Line 551: Line 607:
 
end
 
end
  
-- Main function returns a string value: text of the lead of a page
+
local function fixTemplates(text)
local function main(pageNames, options)
+
repeat -- hide matched {{template}}s including nested templates
if not pageNames or #pageNames < 1 then return err("No page names given") end
+
local t = text
local pageName
+
text = mw.ustring.gsub(text, "{(%b{})}", "\27{\27%1\27}\27") -- {{sometemplate}} → E{Esometemplate}E}E where E represents escape
local text
+
text = mw.ustring.gsub(text, "(< *math[^>]*>[^<]-)}}(.-< */math *>)", "%1}\27}\27%2") -- <math>\{sqrt\{hat{x}}</math> → <math>\{sqrt\{hat{x}E}E</math>
local pageCount = #pageNames
+
until text == t
local firstPage = pageNames[1] or "(nil)" -- save for error message, as it the name will be deleted
+
text = text.gsub(text, "([{}])%1[^\27].*", "") -- remove unmatched {{, }} and everything thereafter, avoiding }E}E etc.
local gotOptions
+
text = text.gsub(text, "([{}])%1$", "") -- remove unmatched {{, }} at end of text
local pageOptionsString
+
text = mw.ustring.gsub(text, "\27", "") -- unhide matched pairs: E{E{ → {{, etc.
local section
+
return text
 +
end
  
-- read the page, or a random one if multiple pages were provided
+
local function fixLinks(text)
if pageCount > 1 then math.randomseed(os.time()) end
+
repeat -- hide matched [[wikilink]]s including nested links like [[File:Example.jpg|Some [[nested]] link.]]
while not text and pageCount > 0 do
+
local t = text
local pageNumber = 1
+
text = mw.ustring.gsub(text, "%[(%b[])%]", "\27[\27%1\27]\27")
if pageCount > 1 then pageNumber = math.random(pageCount) end -- pick a random title
+
until text == t
pageName = pageNames[pageNumber]
+
text = text.gsub(text, "([%[%]])%1[^\27].*", "") -- remove unmatched [[ or ]] and everything thereafter, avoiding ]E]E etc.
if pageName and pageName ~= "" then
+
text = text.gsub(text, "([%[%]])%1$", "") -- remove unmatched [[ or ]] at end of text
-- We have page or [[page]] or [[page|text]], possibly followed by |opt1|opt2...
+
text = mw.ustring.gsub(text, "\27", "") -- unhide matched pairs: ]E]E → ]], etc.
local pn
+
return text
pn, gotOptions, pageOptionsString = mw.ustring.match(pageName, "^%s*(%[%b[]%])%s*(|?)(.*)")
+
end
if pn then
 
pageName = mw.ustring.match(pn, "%[%[([^|%]]*)") -- turn [[page|text]] into page, discarding text
 
else -- we have page or page|opt...
 
pageName, gotOptions, pageOptionsString = mw.ustring.match(pageName, "%s*([^|]*[^|%s])%s*(|?)(.*)")
 
end
 
  
if pageName and pageName ~= "" then
+
-- Replace the first call to each reference defined outside of the text for the full reference, to prevent undefined references
local pn
+
-- Then prefix the page title to the reference names to prevent conflicts
pn, section = mw.ustring.match(pageName, "(.-)#(.*)")
+
-- that is, replace <ref name="Foo"> for <ref name="Title of the article Foo">
pageName = pn or pageName
+
-- and also <ref name="Foo" /> for <ref name="Title of the article Foo" />
text, normalisedPageName = getContent(pageName)
+
-- also remove reference groups: <ref name="Foo" group="Bar"> for <ref name="Title of the article Foo">
if is(options.fragment) then
+
-- and <ref group="Bar"> for <ref>
local frame = mw.getCurrentFrame()
+
-- @todo The current regex may fail in cases with both kinds of quotes, like <ref name="Darwin's book">
text = frame:callParserFunction('#lst', normalisedPageName, options.fragment)
+
local function fixRefs(text, page, full)
 +
if not full then full = getContent(page) end
 +
local refNames = {}
 +
local refName
 +
local refBody
 +
local position = 1
 +
while position < mw.ustring.len(text) do
 +
refName, position = mw.ustring.match(text, "<%s*[Rr][Ee][Ff][^>]*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 = mw.ustring.gsub(refName, "[%^%$%(%)%.%[%]%*%+%-%?%%]", "%%%0") -- escape special characters
 +
refBody = mw.ustring.match(text, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?%s*" .. refName .. "%s*[\"']?[^>/]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>")
 +
if not refBody then -- the ref body is not in the excerpt
 +
refBody = mw.ustring.match(full, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?%s*" .. refName .. "%s*[\"']?[^/>]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>")
 +
if refBody then -- the ref body was found elsewhere
 +
text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?%s*" .. refName .. "%s*[\"']?[^>]*/?%s*>", refBody, 1)
 +
end
 
end
 
end
if not normalisedPageName then
 
return err("No title for page name " .. pageName)
 
else
 
pageName = normalisedPageName
 
end
 
if text and options.nostubs then
 
local isStub = mw.ustring.find(text, "%s*{{[^{|}]*%-[Ss]tub%s*}}")
 
if isStub then text = nil end
 
end
 
if not section then
 
section = mw.ustring.match(pageName, ".-#(.*)") -- parse redirect to Page#Section
 
end
 
if text and section and section ~= "" then text = getSection(text, section) end
 
 
end
 
end
 +
else
 +
position = mw.ustring.len(text)
 
end
 
end
if not text then table.remove(pageNames, pageNumber) end -- this one didn't work; try another
 
pageCount = pageCount - 1 -- ensure that we exit the loop after at most #pageNames iterations
 
 
end
 
end
if not text then return err("Cannot read a valid page: first name is " .. firstPage) end
+
text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?([^\"'>/]+)[\"']?[^>/]*(/?)%s*>", '<ref name="' .. page .. ' %1" %2>')
 
+
text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*group%s*=%s*[\"']?[^\"'>/]+[\"']%s*>", '<ref>')
text = cleanupText(text, options)
+
return text
 
+
end
local pageOptions = {} -- pageOptions (even if value is "") have priority over global options
 
for k, v in pairs(options) do pageOptions[k] = v end
 
if gotOptions and gotOptions ~= "" then
 
for _, t in pairs(mw.text.split(pageOptionsString, "|")) do
 
local k, v = mw.ustring.match(t, "%s*([^=]-)%s*=(.-)%s*$")
 
pageOptions[k] = v
 
end
 
pageOptions.paraflags = numberFlags(pageOptions["paragraphs"] or "") -- parse paragraphs, e.g. "1,3-5" → {"1","3-5"}
 
pageOptions.fileflags = numberFlags(pageOptions["files"] or "") -- parse file numbers
 
if pageOptions.more and pageOptions.more == "" then pageOptions.more = "Read more..." end -- more= is short for this default text
 
end
 
 
 
local fileText
 
fileText, text = parse(text, pageOptions)
 
  
-- replace the bold title or synonym near the start of the article by a wikilink to the article
+
-- Replace the bold title or synonym near the start of the article by a wikilink to the article
 +
function linkBold(text, page)
 
local lang = mw.language.getContentLanguage()
 
local lang = mw.language.getContentLanguage()
local pos = mw.ustring.find(text, "'''" .. lang:ucfirst(pageName) .. "'''", 1, true) -- look for "'''Foo''' is..." (uc) or "A '''foo''' is..." (lc)
+
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(pageName) .. "'''", 1, true) -- plain search: special characters in pageName represent themselves
+
or mw.ustring.find(text, "'''" .. lang:lcfirst(page) .. "'''", 1, true) -- plain search: special characters in page represent themselves
if pos then
+
if position then
local len = mw.ustring.len(pageName)
+
local length = mw.ustring.len(page)
text = mw.ustring.sub(text, 1, pos + 2) .. "[[" .. mw.ustring.sub(text, pos + 3, pos + len + 2) .. "]]" .. mw.ustring.sub(text, pos + len + 3, -1) -- link it
+
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)
 
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)
 
text = mw.ustring.gsub(text, "()'''(.-'*)'''", function(a, b)
if a < 100 and not mw.ustring.find(b, "%[") then ---if early in article and not wikilinked
+
if not mw.ustring.find(b, "%[") then -- if not wikilinked
return "'''[[" .. pageName .. "|" .. b .. "]]'''" -- replace '''Foo''' by '''[[pageName|Foo]]'''
+
return "'''[[" .. page .. "|" .. b .. "]]'''" -- replace '''Foo''' by '''[[page|Foo]]'''
 
else
 
else
 
return nil -- instruct gsub to make no change
 
return nil -- instruct gsub to make no change
Line 640: Line 684:
 
end, 1) -- "end" here terminates the anonymous replacement function(a, b) passed to gsub
 
end, 1) -- "end" here terminates the anonymous replacement function(a, b) passed to gsub
 
end
 
end
 +
return text
 +
end
  
-- remove '''bold text''' if requested
+
-- Main function for modules
if is(pageOptions.nobold) then text = mw.ustring.gsub(text, "'''", "") end
+
local function get(page, options)
 +
if options.errors then errors = options.errors end
  
text = fileText .. text
+
if not page or page == "" then return luaError("noPage") end
  
-- Seek and destroy unterminated templates and wikilinks
+
local text
repeat -- hide matched {{template}}s including nested templates
+
page, section = mw.ustring.match(page, "([^#]+)#?([^#]*)")
local t = text
+
text, page = getContent(page)
text = mw.ustring.gsub(text, "{(%b{})}", "\27{\27%1\27}\27") -- {{sometemplate}} → E{Esometemplate}E}E where E represents escape
+
if not page then return luaError("noPage") end
text = mw.ustring.gsub(text, "(< *math[^>]*>[^<]-)}}(.-< */math *>)", "%1}\27}\27%2") -- <math>\{sqrt\{hat{x}}</math> → <math>\{sqrt\{hat{x}E}E</math>
+
if not text then return luaError("pageNotFound", page) end
until text == t
+
local full = text -- save the full text for later
repeat -- do similar for [[wikilink]]s
 
local t = text
 
text = mw.ustring.gsub(text, "%[(%b[])%]", "\27[\27%1\27]\27")
 
until text == t
 
 
 
text = text.gsub(text, "([{}%[%]])%1[^\27].*", "") -- remove unmatched {{, }}, [[ or ]] and everything thereafter, avoiding ]E]E etc.
 
text = text.gsub(text, "([{}%[%]])%1$", "") -- remove unmatched {{, }}, [[ or ]] at end of text
 
text = mw.ustring.gsub(text, "\27", "") -- unhide matched pairs: E{E{ → {{, ]E]E → ]], etc.
 
 
 
-- Ensure div tags match
 
text = fixTags(text, "div")
 
  
if pageOptions.more then text = text .. " '''[[" .. pageName .. "|" .. pageOptions.more .. "]]'''" end -- wikilink to article for more info
+
if is(options.fragment) then
 
+
text = getFragment(page, options.fragment)
if pageOptions.list and not pageOptions.showall then -- add a collapsed list of pages which might appear
 
local listtext = pageOptions.list
 
if listtext == "" then listtext = "Other articles" end
 
text = text .. "{{collapse top|title={{resize|85%|" ..listtext .. "}}|bg=fff}}{{hlist"
 
for _, p in pairs(pageNames) do
 
if mw.ustring.match(p, "%S") then text = text .. "|[[" .. mw.text.trim(p) .. "]]" end
 
end
 
text = text .. "}}\n{{collapse bottom}}"
 
 
end
 
end
  
return text
+
if is(section) then
end
+
text = getSection(text, section)
 
 
-- Shared template invocation code for lead and random functions
 
local function invoke(frame, template)
 
-- args = { 1,2,... = page names, paragraphs = list e.g. "1,3-5", files = list, more = text}
 
local args = {} -- args[k] = frame.args[k] or frame:getParent().args[k] for all k in either (numeric or not)
 
for k, v in pairs(frame:getParent().args) do args[k] = v end
 
for k, v in pairs(frame.args) do args[k] = v end -- args from a Lua call have priority over parent args from template
 
errors = args["errors"] -- set the module level boolean used in local function err
 
 
 
local articleCount = #args -- must be 1 except with selected=Foo and Foo=Somepage
 
if articleCount < 1 and not (template == "selected" and args[template] and args[args[template]]) then
 
return err("No articles provided")
 
 
end
 
end
  
local pageNames = {}
+
-- Strip text of all undersirables
if template == "lead" then
+
text = cleanupText(text, options)
pageNames = { args[1] }
+
text = parse(text, options)
elseif template == "linked" or template == "listitem" then
 
-- Read named page and find its wikilinks
 
local page = args[1]
 
local text, title = getContent(page)
 
if not title then
 
return err("No title for page name " .. page)
 
elseif not text then
 
return err("No content for page name " .. page)
 
end
 
if args["section"] then -- check relevant section only
 
text = getSection(text, args["section"], args["sectiononly"])
 
if not text then return err("No section " .. args["section"] .. " in page " .. page) end
 
end
 
-- replace annotated links with real links
 
text = mw.ustring.gsub(text, "{{%s*[Aa]nnotated[ _]link%s*|%s*(.-)%s*}}", "[[%1]]")
 
if template == "linked" then
 
for p in mw.ustring.gmatch(text, "%[%[%s*([^%]|\n]*)") do table.insert(pageNames, p) end
 
else -- listitem: first wikilink on a line beginning *, :#, etc. except in "See also" or later section
 
text = mw.ustring.gsub(text, "\n== *See also.*", "")
 
for p in mw.ustring.gmatch(text, "\n:*[%*#][^\n]-%[%[%s*([^%]|\n]*)") do table.insert(pageNames, p) end
 
end
 
elseif template == "random" then
 
-- accept any number of page names.  If more than one, we'll pick one randomly
 
for i, p in pairs(args) do
 
if p and type(i) == 'number' then table.insert(pageNames, p) end
 
end
 
elseif template == "selected" then
 
local articleKey = args[template]
 
if tonumber(articleKey) then -- normalise article number into the range 1..#args
 
articleKey = articleKey % articleCount
 
if articleKey == 0 then articleKey = articleCount end
 
end
 
pageNames = { args[articleKey] }
 
end
 
  
local options = args -- pick up miscellaneous options: more, errors, fileargs
+
-- Replace the bold title or synonym near the start of the article by a wikilink to the article
options.paraflags = numberFlags(args["paragraphs"] or "") -- parse paragraphs, e.g. "1,3-5" → {"1","3-5"}
+
text = linkBold(text, page)
options.fileflags = numberFlags(args["files"] or "") -- parse file numbers
 
if options.more and options.more == "" then options.more = "Read more..." end -- more= is short for this default text
 
  
local text = ""
+
-- Remove '''bold text''' if requested
if options.showall then
+
if is(options.nobold) then text = mw.ustring.gsub(text, "'''", "") end
local separator = ""
 
for _, p in pairs(pageNames) do
 
local t = main({ p }, options)
 
if t ~= "" then
 
text = text .. separator .. t
 
separator = options.showall
 
if separator == "" then separator = "{{clear}}{{hr}}" end
 
end
 
end
 
else
 
text = main(pageNames, options)
 
end
 
  
if text == "" and d.brokenCategory and d.brokenCategory ~= "" and mw.title.getCurrentTitle().isContentPage then
+
-- Keep only tables if requested
return "[[Category:" .. d.brokenCategory .. "]]"
+
if is(options.tablesOnly) then text = getTables(text) end
else
 
return frame:preprocess(text)
 
end
 
end
 
  
-- Replicate {{Excerpt}} entirely in Lua for reduced Post-expand include size
+
-- Keep only lists if requested
local function excerpt(frame)
+
if is(options.listsOnly) then text = getLists(text) end
local args = {} -- args[k] = frame.args[k] or frame:getParent().args[k] for all k in either (numeric or not)
 
for k, v in pairs(frame:getParent().args) do args[k] = v end
 
for k, v in pairs(frame.args) do args[k] = v end -- args from a Lua call have priority over parent args from template
 
  
local tag = is(args.tag) and args.tag or 'div'
+
-- Seek and destroy unterminated templates, links and tags
local article = is(args.article) and args.article or args[1] or '{{{1}}}'
+
text = fixTemplates(text)
local section = is(args.section) and args.section or args[2]
+
text = fixLinks(text)
 +
text = fixTags(text, "div")
  
local output = {}
+
-- Fix broken references
output[1] = frame:extensionTag{ name = 'templatestyles', args = {src='Excerpt/styles.css'} }
+
if is(options.keepRefs) then text = fixRefs(text, page, full) end
output[2] = '<' .. tag ..  ' class="excerpt-block">'
 
output[3] = is(args.indicator) and ('<' .. tag .. ' class="excerpt-indicator">') or ''
 
if is(args.nohat) then
 
output[4] = ''
 
else
 
local hatnote = {}
 
hatnote[1] = 'This' .. (is(args.indicator) and '' or ' section') .. ' is an excerpt from '
 
hatnote[2] = '[['
 
hatnote[3] = article .. (is(section) and ('#' .. frame:callParserFunction( 'urlencode', section, 'WIKI' )) or '')
 
hatnote[4] = '|'
 
hatnote[5] = article .. (is(section) and (frame:callParserFunction( '#tag:nowiki', ' § ' ) .. section) or '')
 
hatnote[6] = ']]'
 
hatnote[7] = "''" .. '<span class="mw-editsection-like plainlinks"><span>[ </span>['
 
local title = mw.title.new(article) or mw.title.getCurrentTitle()
 
hatnote[8] = title:fullUrl('action=edit') .. ' edit'
 
hatnote[9] = ']<span> ]</span></span>' .. "''"
 
 
output[4] = require('Module:Hatnote')._hatnote(table.concat(hatnote), {selfref=true}) or err("Error generating hatnote")
 
end
 
output[5] =  '<' .. tag ..  ' class="excerpt">\n'
 
if article ~= '{{{1}}}' then
 
local options = args -- turn template arguments into module options
 
options.paraflags = args.paragraphs
 
options.fileflags = args.files or 1
 
options.nobold = 1
 
options.fragment = args.fragment
 
options.keepTables = args.tables or 1
 
options.keepRefs = args.references or 1
 
options.keepSubsections = args.subsections
 
  
local pageNames = { (article .. '#' .. (section or '')) }
+
return text
local text = main(pageNames, options)
+
end
  
if text == "" and d.brokenCategory and d.brokenCategory ~= "" and mw.title.getCurrentTitle().isContentPage then
+
-- Main invocation function for templates
output[6] = "[[Category:" .. d.brokenCategory .. "]]"
+
local function main(frame)
else
+
local args = parseArgs(frame)
output[6] = frame:preprocess(text) or err("Error processing text")
+
local page = args[1]
end
+
local ok, text = pcall(get, page, args)
else
+
if not ok then
output[6] = err("No article provided")
+
text = d.errors.prefix .. text
end
+
if d.errorsCategory and d.errorsCategory ~= "" and mw.title.getCurrentTitle().isContentPage then
output[7] = '</' .. tag .. '>'
+
text = text .. '[[' .. d.errorsCategory .. ']]'
output[8] = is(args.indicator) and ('</' .. tag .. '>') or ''
+
end
output[9] = '</' .. tag .. '>'
+
return mw.html.create('div'):addClass('error'):wikitext(text)
output[10] = mw.title.getCurrentTitle().isContentPage and '[[Category:Articles with excerpts]]' or ''
+
end
+
return frame:preprocess(text)
return table.concat(output)
 
 
end
 
end
  
-- Entry points for template callers using #invoke:
+
-- Entry points for templates
function p.lead(frame) return invoke(frame, "lead") end -- {{Transclude lead excerpt}} reads the first and only article
+
function p.main(frame) return main(frame) end
function p.linked(frame) return invoke(frame, "linked") end -- {{Transclude linked excerpt}} reads a randomly selected article linked from the given page
+
function p.wikiError(message, value) return wikiError(message, value) end
function p.listitem(frame) return invoke(frame, "listitem") end -- {{Transclude list item excerpt}} reads a randomly selected article listed on the given page
 
function p.random(frame) return invoke(frame, "random") end -- {{Transclude random excerpt}} reads any article (default for invoke with one argument)
 
function p.selected(frame) return invoke(frame, "selected") end -- {{Transclude selected excerpt}} reads the article whose key is in the selected= parameter
 
function p.excerpt(frame) return excerpt(frame) end -- {{Excerpt}} transcludes part of an article into another article
 
  
 
-- Entry points for other Lua modules
 
-- Entry points for other Lua modules
function p.getContent(page, frame) return getContent(page, frame) end
+
function p.get(page, options) return get(page, options) end
 +
function p.getContent(page) return getContent(page) end
 +
function p.getSection(text, section) return getSection(text, section) end
 +
function p.getTables(text, options) return getTables(text, options) end
 +
function p.getLists(text, options) return getLists(text, options) end
 +
function p.parse(text, options) return parse(text, options) end
 +
function p.parseImage(text, start) return parseImage(text, start) end
 +
function p.parseArgs(frame) return parseArgs(frame) end
 +
function p.argImage(text) return argImage(text) end
 +
function p.checkImage(image) return checkImage(image) end
 +
function p.cleanupText(text, options) return cleanupText(text, options) end
 +
function p.luaError(message, value) return luaError(message, value) end
 +
function p.is(value) return is(value) end
 +
function p.numberFlags(str) return numberFlags(str) end
 +
 
 +
-- Entry points for backwards compatibility
 
function p.getsection(text, section) return getSection(text, section) end
 
function p.getsection(text, section) return getSection(text, section) end
function p.parse(text, options, filesOnly) return parse(text, options, filesOnly) end
+
function p.parseimage(text, start) return parseImage(text, start) end
 +
function p.checkimage(image) return checkImage(image) end
 
function p.argimage(text) return argImage(text) end
 
function p.argimage(text) return argImage(text) end
function p.checkimage(image) return checkImage(image) end
 
function p.parseimage(text, start) return parseImage(text, start) end
 
function p.cleanupText(text, options) return cleanupText(text, options) end
 
function p.main(pageNames, options) return main(pageNames, options) end
 
 
function p.numberflags(str) return numberFlags(str) end
 
function p.numberflags(str) return numberFlags(str) end
  
 
return p
 
return p

Revision as of 12:56, 1 June 2020

Template:Uses Lua

This module implements Template:Excerpt.

Portals

Portals still use a previous version of this module:



-- Get localized data
local d = require("Module:Excerpt/i18n")

local p = {}

-- Helper function to test for truthy and falsy values
local function is(value)
	if not value or value == "" or value == "0" or value == "false" or value == "no" then
		return false
	end
	return true
end

-- Error handling function
-- Throws a Lua error or returns an empty string if error reporting is disabled
errors = true -- show errors by default
local function luaError(message, value)
	if not is(errors) then return "" end -- error reporting is disabled
	message = d.errors[message] or message or ""
	message = mw.ustring.format(message, value)
	error(message, 2)
end

-- Error handling function
-- Returns a wiki friendly error or an empty string if error reporting is disabled
local function wikiError(message, value)
	if not is(errors) then return "" end -- error reporting is disabled
	message = d.errors[message] or message or ""
	message = mw.ustring.format(message, value)
	message = d.errors.prefix .. message
	if mw.title.getCurrentTitle().isContentPage then
		local errorsCategory = mw.title.new(d.errorsCategory, 'Category')
		if errorsCategory then message = message .. '[[' .. errorsCategory.prefixedText .. ']]' end
	end
	message = mw.html.create('div'):addClass('error'):wikitext(message)
	return message
end

-- Helper function to match from a list 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

-- Helper function to convert imagemaps into standard images
local function convertImageMap(imagemap)
	local image = matchAny(imagemap, "[>\n]%s*", d.fileNamespaces, "[^\n]*")
	if image then
		return "<!--imagemap-->[[" .. mw.ustring.gsub(image, "[>\n]%s*", "", 1) .. "]]"
	else
		return "" -- remove entire block if image can't be extracted
	end
end

-- Helper function to convert a comma-separated list of numbers or min-max ranges into a list of booleans
-- For example: "1,3-5" to {1=true,2=false,3=true,4=true,5=true}
local function numberFlags(str)
	if not str then return {} end
	local flags = {}
	local ranges = mw.text.split(str, ",") -- parse ranges: "1,3-5" to {"1","3-5"}
	for _, r in pairs(ranges) do
		local min, max = mw.ustring.match(r, "^%s*(%d+)%s*%-%s*(%d+)%s*$") -- "3-5" to min=3 max=5
		if not max then	min, max = mw.ustring.match(r, "^%s*((%d+))%s*$") end -- "1" to min=1 max=1
		if max then
			for p = min, max do flags[p] = true end
		end
	end
	return flags
end

-- Helper function to convert template arguments into an array of arguments fit for get()
local function parseArgs(frame)
	local args = {}
	for key, value in pairs(frame:getParent().args) do args[key] = value end
	for key, value in pairs(frame.args) do args[key] = value end -- args from a Lua call have priority over parent args from template
	args.paraflags = numberFlags(args["paragraphs"] or "") -- parse paragraphs: "1,3-5" to {"1","3-5"}
	args.fileflags = numberFlags(args["files"] or "") -- parse file numbers
	return args
end

-- Helper function to remove unwanted templates and pseudo-templates such as #tag:ref and DEFAULTSORT
local function stripTemplate(t)
	-- If template is unwanted then return "" (gsub will replace by nothing), else return nil (gsub will keep existing string)
	if matchAny(t, "^{{%s*", d.unwantedInlineTemplates, "%s*%f[|}]") then return "" end

	-- If template is wanted but produces an unwanted reference then return the string with |shortref or |ref removed
	local noRef = mw.ustring.gsub(t, "|%s*shortref%s*%f[|}]", "")
	noRef = mw.ustring.gsub(noRef, "|%s*ref%s*%f[|}]", "")

	-- If a wanted template has unwanted nested templates, purge them too
	noRef = mw.ustring.sub(noRef, 1, 2) .. mw.ustring.gsub(mw.ustring.sub(noRef, 3), "%b{}", stripTemplate)

	-- Replace {{audio}} by its text parameter: {{Audio|Foo.ogg|Bar}} → Bar
	noRef = mw.ustring.gsub(noRef, "^{{%s*[Aa]udio.-|.-|(.-)%f[|}].*", "%1")

	-- Replace {{Nihongo foot}} by its text parameter: {{Nihongo foot|English|英語|eigo}} → English
	noRef = mw.ustring.gsub(noRef, "^{{%s*[Nn]ihongo[ _]+foot%s*|(.-)%f[|}].*", "%1")

	if noRef ~= t then return noRef end

	return nil -- not an unwanted template: keep
end

-- Get a page's content, following redirects
-- Also returns the page name, or the target page name if a redirect was followed, or false if no page found
-- For file pages, returns the content of the file description page
local function getContent(page)
	local title = mw.title.new(page)
	if not title then return false, false end

	local target = title.redirectTarget
	if target then title = target end

	return title:getContent(), title.prefixedText
end

-- Get the tables only
local function getTables(text, options)
	local tables = {}
	for candidate in mw.ustring.gmatch(text, "%b{}") do
		if mw.ustring.sub(candidate, 1, 2) == '{|' then
			table.insert(tables, candidate)
		end
	end
	return table.concat(tables, '\n')
end

-- Get the lists only
local function getLists(text, options)
	local lists = {}
	for list in mw.ustring.gmatch(text, "\n[*#][^\n]+") do
		table.insert(lists, list)
	end
	return table.concat(lists, '\n')
end

-- Check image for suitability
local function checkImage(image)
	local page = matchAny(image, "", d.fileNamespaces, "%s*:[^|%]]*") -- match File:(name) or Image:(name)
	if not page then return false end

	-- Limit to image types: .gif, .jpg, .jpeg, .png, .svg, .tiff, .xcf (exclude .ogg, audio, etc.)
	local fileTypes = {"[Gg][Ii][Ff]", "[Jj][Pp][Ee]?[Gg]", "[Pp][Nn][Gg]", "[Ss][Vv][Gg]", "[Tt][Ii][Ff][Ff]", "[Xx][Cc][Ff]"}
	if not matchAny(page, "%.", fileTypes, "%s*$") then return false end

	-- Check the local wiki
	local fileDescription, fileTitle = getContent(page) -- get file description and title after following any redirect
	if not fileTitle or fileTitle == "" then return false end -- the image doesn't exist

	-- Check Commons
	if not fileDescription or fileDescription == "" then
		local frame = mw.getCurrentFrame()
		fileDescription = frame:preprocess("{{" .. fileTitle .. "}}")
	end

	-- Filter non-free images
	if not fileDescription or fileDescription == "" or mw.ustring.match(fileDescription, "[Nn]on%-free") then return false end

	return true
end

-- Attempt to parse [[File:...]] or [[Image:...]], either anywhere (start=false) or at the start only (start=true)
local function parseImage(text, start)
	local startre = ""
	if start then startre = "^" end -- a true flag restricts search to start of string
	local image = matchAny(text, startre .. "%[%[%s*", d.fileNamespaces, "%s*:.*") -- [[File: or [[Image: ...
	if image then
		image = mw.ustring.match(image, "%b[]%s*") -- matching [[...]] to handle wikilinks nested in caption
	end
	return image
end

-- Parse a caption, which ends at a | (end of parameter) or } (end of infobox) but may contain nested [..] and {..}
local function parseCaption(caption)
	if not caption then return nil end
	local length = mw.ustring.len(caption)
	local position = 1
	while position <= length do
		local linkStart, linkEnd = mw.ustring.find(caption, "%b[]", position)
		linkStart = linkStart or length + 1 -- avoid comparison with nil when no link
		local templateStart, templateEnd = mw.ustring.find(caption, "%b{}", position)
		templateStart = templateStart or length + 1 -- avoid comparison with nil when no template
		local argEnd = mw.ustring.find(caption, "[|}]", position) or length + 1
		if linkStart < templateStart and linkStart < argEnd then
			position = linkEnd + 1 -- skip wikilink
		elseif templateStart < argEnd then
			position = templateEnd + 1 -- skip template
		else -- argument ends before the next wikilink or template
			return mw.ustring.sub(caption, 1, argEnd - 1)
		end
	end
	return caption -- No terminator found: return entire caption
end					

-- Attempt to construct a [[File:...]] block from {{infobox ... |image= ...}}
local function argImage(text)
	local token = nil
	local hasNamedArgs = mw.ustring.find(text, "|") and mw.ustring.find(text, "=")
	if not hasNamedArgs then return nil end -- filter out any template that obviously doesn't contain an image

	-- ensure image map is captured
	text = mw.ustring.gsub(text, '<!%-%-imagemap%-%->', '|imagemap=')

	-- find all images
	local hasImages = false
	local images = {}
	local captureFrom = 1
	while captureFrom < mw.ustring.len(text) do
		local argname, position, image = mw.ustring.match(text, "|%s*([^=|]-[Ii][Mm][Aa][Gg][Ee][^=|]-)%s*=%s*()(.*)", captureFrom)
		if image then -- ImageCaption=, image_size=, image_upright=, etc. do not introduce an image
			local lcArgName = mw.ustring.lower(argname)
			if mw.ustring.find(lcArgName, "caption")
			 or mw.ustring.find(lcArgName, "size")
			 or mw.ustring.find(lcArgName, "upright") then
				image = nil
			end
		end
		if image then
			hasImages = true
			images[position] = image
			captureFrom = position
		else
			captureFrom = mw.ustring.len(text)
		end
	end
	captureFrom = 1
	while captureFrom < mw.ustring.len(text) do
		local position, image = mw.ustring.match(text, "|%s*[^=|]-[Pp][Hh][Oo][Tt][Oo][^=|]-%s*=%s*()(.*)", captureFrom)
		if image then
			hasImages = true
			images[position] = image
			captureFrom = position
		else
			captureFrom = mw.ustring.len(text)
		end
	end
	captureFrom = 1
	while captureFrom < mw.ustring.len(text) do
		local position, image = mw.ustring.match(text, "|%s*[^=|{}]-%s*=%s*()%[?%[?([^|{}]*%.%a%a%a%a?)%s*%f[|}]", captureFrom)
		if image then
			hasImages = true
			if not images[position] then
				images[position] = image
			end
			captureFrom = position
		else
			captureFrom = mw.ustring.len(text)
		end
	end

	if not hasImages then return nil end

	-- find all captions
	local captions = {}
	captureFrom = 1
	while captureFrom < mw.ustring.len(text) do
		local position, caption = matchAny(text, "|%s*", d.captionParams, "%s*=%s*()([^\n]+)", captureFrom)
		if caption then
			-- extend caption to parse "| caption = Foo {{Template\n on\n multiple lines}} Bar\n"
			local bracedCaption = mw.ustring.match(text, "^[^\n]-%b{}[^\n]+", position)
			if bracedCaption and bracedCaption ~= "" then caption = bracedCaption end
			caption = mw.text.trim(caption)
			local captionStart = mw.ustring.sub(caption, 1, 1)
			if captionStart == '|' or captionStart == '}' then caption = nil end
		end
		if caption then
			-- find nearest image, and use same index for captions table
			local i = position
			while i > 0 and not images[i] do
				i = i - 1
				if images[i] then
					if not captions[i] then
						captions[i] = parseCaption(caption)
					end
				end
			end
			captureFrom = position
		else
			captureFrom = mw.ustring.len(text)
		end
	end

	-- find all alt text
	local altTexts = {}
	for position, altText in mw.ustring.gmatch(text, "|%s*[Aa][Ll][Tt]%s*=%s*()([^\n]*)") do
		if altText then

			-- altText is terminated by }} or |, but first skip any matched [[...]] and {{...}}
			local lookFrom = math.max( -- find position after whichever comes last: start of string, end of last ]] or end of last }}
			 mw.ustring.match(altText, ".*{%b{}}()") or 1, -- if multiple {{...}}, .* consumes all but one, leaving the last for %b
			 mw.ustring.match(altText, ".*%[%b[]%]()") or 1)

			local length = mw.ustring.len(altText)
			local afterText = math.min( -- find position after whichever comes first: end of string, }} or |
			 mw.ustring.match(altText, "()}}", lookFrom) or length+1,
			 mw.ustring.match(altText, "()|", lookFrom) or length+1)
			altText = mw.ustring.sub(altText, 1, afterText-1) -- chop off |... or }}... which is not part of [[...]] or {{...}}

			altText = mw.text.trim(altText)
			local altTextStart = mw.ustring.sub(altText, 1, 1)
			if altTextStart == '|' or altTextStart == '}' then altText = nil end
		end
		if altText then
			-- find nearest image, and use same index for altTexts table
			local i = position
			while i > 0 and not images[i] do
				i = i - 1
				if images[i] then
					if not altTexts[i] then
						altTexts[i] = altText
					end
				end
			end
		end
	end

	-- find all image sizes
	local imageSizes = {}
	for position, imageSizeMatch in mw.ustring.gmatch(text, "|%s*[Ii][Mm][Aa][Gg][Ee][ _]?[Ss][Ii][Zz][Ee]%s*=%s*()([^}|\n]*)") do
		local imageSize = mw.ustring.match(imageSizeMatch, "=%s*([^}|\n]*)")
		if imageSize then
			imageSize = mw.text.trim(imageSize )
			local imageSizeStart = mw.ustring.sub(imageSize, 1, 1)
			if imageSizeStart == '|' or imageSizeStart == '}' then imageSize = nil end
		end
		if imageSize then
			-- find nearest image, and use same index for imageSizes table
			local i = position
			while i > 0 and not images[i] do
				i = i - 1
				if images[i] then
					if not imageSizes[i] then
						imageSizes[i] = imageSize
					end
				end
			end
		end
	end

	-- sort the keys of the images table (in a table sequence), so that images can be iterated over in order
	local keys = {}
	for key, val in pairs(images) do
		table.insert(keys, key)
	end
	table.sort(keys)

	 -- add in relevant optional parameters for each image: caption, alt text and image size
	local imageTokens = {}
	for _, index in ipairs(keys) do
		local image = images[index]
		local token = parseImage(image, true) -- look for image=[[File:...]] etc.
		if not token then
			image = mw.ustring.match(image, "^[^}|\n]*") -- remove later arguments
			token = "[[" -- Add File: unless name already begins File: or Image:
			if not matchAny(image, "^", d.fileNamespaces, "%s*:") then
				token = token .. "File:"
			end
			token = token .. image
			local caption = captions[index]
			if caption and mw.ustring.match(caption, "%S") then token = token .. "|" .. caption end
			local alt = altTexts[index]
			if alt then token = token .. "|alt=" .. alt end
			local image_size = imageSizes[index]
			if image_size and mw.ustring.match(image_size, "%S") then token = token .. "|" .. image_size end
			token = token .. "]]"
		end
		token = mw.ustring.gsub(token, "\n","") .. "\n"
		table.insert(imageTokens, token)
	end
	return imageTokens
end

local function modifyImage(image, fileArgs)
	if fileArgs then
		for _, filearg in pairs(mw.text.split(fileArgs, "|")) do -- handle fileArgs=left|border etc.
			local fa = mw.ustring.gsub(filearg, "=.*", "") -- "upright=0.75" → "upright"
			local group = {fa} -- group of "border" is ["border"]...
			for _, g in pairs(d.imageParams) do
				for _, a in pairs(g) do
					if fa == a then group = g end -- ...but group of "left" is ["right", "left", "center", "none"]
				end
			end
			for _, a in pairs(group) do
				image = mw.ustring.gsub(image, "|%s*" .. a .. "%f[%A]%s*=[^|%]]*", "") -- remove "|upright=0.75" etc.
				image = mw.ustring.gsub(image, "|%s*" .. a .. "%s*([|%]])", "%1") -- replace "|left|" by "|" etc.
			end

			image = mw.ustring.gsub(image, "([|%]])", "|" .. filearg .. "%1", 1) -- replace "|" by "|left|" etc.
		end
	end
	return image
end

-- a basic parser to trim down extracted wikitext
--   @param text : Wikitext to be processed
--   @param options : A table of options...
--          options.paraflags : Which number paragraphs to keep, as either a string (e.g. '1,3-5') or a table (e.g. {1=true,2=false,3=true,4=true,5=true}. If not present, all paragraphs will be kept.
--          options.fileflags : table of which files to keep, as either a string (e.g. '1,3-5') or a table (e.g. {1=true,2=false,3=true,4=true,5=true}
--          options.fileargs : args for the [[File:]] syntax, such as 'left'
--			options.filesOnly : only return the files and not the prose
local function parse(text, options)
	local allParagraphs = true -- keep all paragraphs?
	if options.paraflags then
		if type(options.paraflags) ~= "table" then options.paraflags = numberFlags(options.paraflags) end
		for _, v in pairs(options.paraflags) do
			if v then allParagraphs = false end -- if any para specifically requested, don't keep all
		end
	end
	if is(options.filesOnly) then
		allParagraphs = false
		options.paraflags = {}
	end

	local maxfile = 0 -- for efficiency, stop checking images after this many have been found
	if options.fileflags then
		if type(options.fileflags) ~= "table" then options.fileflags = numberFlags(options.fileflags) end
		for k, v in pairs(options.fileflags) do
			if v and k > maxfile then maxfile = k end -- set maxfile = highest key in fileflags
		end
	end
	
	local fileArgs = options.fileargs and mw.text.trim(options.fileargs)
	if fileArgs == '' then fileArgs = nil end

	local leadStart = nil -- have we found some text yet?
	local t = "" -- the stripped down output text
	local fileText = "" -- output text with concatenated [[File:Foo|...]]\n entries
	local files = 0 -- how many images so far
	local paras = 0 -- how many paragraphs so far
	local startLine = true -- at the start of a line (no non-spaces found since last \n)?

	text = mw.ustring.gsub(text,"^%s*","") -- remove initial white space

	-- Add named files
	local f = options.files
	if f and mw.ustring.match(f, "[^%d%s%-,]") then -- filename rather than number list
		f = mw.ustring.gsub(f, "^%s*File%s*:%s*", "", 1)
		f = mw.ustring.gsub(f, "^%s*Image%s*:%s*", "", 1)
		f = "[[File:" .. f .. "]]"
		f = modifyImage(f, "thumb")
		f = modifyImage(f, fileArgs)
		if checkImage(f) then fileText = fileText .. f .. "\n" end
	end

	repeat -- loop around parsing a template, image or paragraph
		local token = mw.ustring.match(text, "^%b{}%s*") or false -- {{Template}} or {| Table |}
		if not leadStart and not token then token = mw.ustring.match(text, "^%b<>%s*%b{}%s*") end -- allow <tag>{{template}} before lead has started

		local line = mw.ustring.match(text, "[^\n]*")
		if token and line and mw.ustring.len(token) < mw.ustring.len(line) then -- template is followed by text (but it may just be other templates)
			line = mw.ustring.gsub(line, "%b{}", "") -- remove all templates from this line
			line = mw.ustring.gsub(line, "%b<>", "") -- remove all HTML tags from this line
			-- if anything is left, other than an incomplete further template or an image, keep the template: it counts as part of the line
			if mw.ustring.find(line, "%S") and not matchAny(line, "^%s*", { "{{", "%[%[%s*[Ff]ile:", "%[%[%s*[Ii]mage:" }, "") then
				token = nil
			end
		end

		if token then -- found a template which is not the prefix to a line of text

			if is(options.keepTables) and mw.ustring.sub(token, 1, 2) == '{|' then
				t = t .. token -- keep tables

			elseif mw.ustring.sub(token, 1, 3) == '{{#' then
				t = t .. token -- keep parser functions

			elseif leadStart then -- lead has already started, so keep the template within the text, unless it's a whole line (navbox etc.)
				if not is(options.filesOnly) and not startLine then t = t .. token end

			elseif matchAny(token, "^{{%s*", d.wantedBlockTemplates, "%s*%f[|}]") then
				t = t .. token -- keep wanted block templates

			elseif files < maxfile then -- discard template, but if we are still collecting images...
				local images = argImage(token) or {}
				if not images then
					local image = parseImage(token, false) -- look for embedded [[File:...]], |image=, etc.
					if image then table.insert(images, image) end
				end
				for _, image in ipairs(images) do
					if files < maxfile and checkImage(image) then -- if image is found and qualifies (not a sound file, non-free, etc.)
						files = files + 1 -- count the file, whether displaying it or not
						if options.fileflags and options.fileflags[files] then -- if displaying this image
							image = modifyImage(image, "thumb")
							image = modifyImage(image, fileArgs)
							fileText = fileText .. image
						end
					end
				end
			end
		else -- the next token in text is not a template
			token = parseImage(text, true)
			if token then -- the next token in text looks like an image
				if files < maxfile and checkImage(token) then -- if more images are wanted and this is a wanted image
					files = files + 1
					if options.fileflags and options.fileflags[files] then
						local image = token -- copy token for manipulation by adding |right etc. without changing the original
						image = modifyImage(image, fileArgs)
						fileText = fileText .. image
					end
				end
			else -- got a paragraph, which ends at a file, image, blank line or end of text
				local afterEnd = mw.ustring.len(text) + 1
				local blankPosition = mw.ustring.find(text, "\n%s*\n") or afterEnd -- position of next paragraph delimiter (or end of text)
				local endPosition = math.min( -- find position of whichever comes first: [[File:, [[Image: or paragraph delimiter
				 mw.ustring.find(text, "%[%[%s*[Ff]ile%s*:") or afterEnd,
				 mw.ustring.find(text, "%[%[%s*[Ii]mage%s*:") or afterEnd,
				 blankPosition)
				token = mw.ustring.sub(text, 1, endPosition-1)
				if blankPosition < afterEnd and blankPosition == endPosition then -- paragraph ends with a blank line
					token = token .. mw.ustring.match(text, "\n%s*\n", blankPosition)
				end
				local isHatnote = not(leadStart) and mw.ustring.sub(token, 1, 1) == ':'
				if not isHatnote then
					leadStart = leadStart or mw.ustring.len(t) + 1 -- we got a paragraph, so mark the start of the lead section
					paras = paras + 1
					if allParagraphs or (options.paraflags and options.paraflags[paras]) then t = t .. token end -- add if this paragraph wanted
				end
			end -- of "else got a paragraph"
		end -- of "else not a template"

		if token then text = mw.ustring.sub(text, mw.ustring.len(token)+1) end -- remove parsed token from remaining text
		startLine = mw.ustring.find(token, "\n%s*$") -- will the next token be the first non-space on a line?
	until not text or text == "" or not token or token == "" -- loop until all text parsed

	text = mw.ustring.gsub(t, "\n+$", "") -- remove trailing line feeds, so "{{Transclude text excerpt|Foo}} more" flows on one line

	return fileText .. text
end

local function cleanupText(text, options)
	text = mw.ustring.gsub(text, "<!%-%-.-%-%->","") -- remove HTML comments
	text = mw.ustring.gsub(text, "<[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.-</[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>", "") -- remove noinclude bits
	if mw.ustring.find(text, "[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]") then -- avoid expensive search if possible
		text = mw.ustring.gsub(text, "</[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.-<[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>", "") -- remove text between onlyinclude sections
		text = mw.ustring.gsub(text, "^.-<[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>", "") -- remove text before first onlyinclude section
		text = mw.ustring.gsub(text, "</[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.*", "") -- remove text after last onlyinclude section
	end
	if not is(options.keepSubsections) then
		text = mw.ustring.gsub(text, "\n==.*","") -- remove first ==Heading== and everything after it
		text = mw.ustring.gsub(text, "^==.*","") -- ...even if the lead is empty
	end
	if not is(options.keepRefs) then
		text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]-/%s*>", "") -- remove refs cited elsewhere
		text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff].->.-<%s*/%s*[Rr][Ee][Ff]%s*>", "") -- remove refs
		text = mw.ustring.gsub(text, "%b{}", stripTemplate) -- remove unwanted templates such as references
	end
	text = mw.ustring.gsub(text, "<%s*[Ss][Cc][Oo][Rr][Ee].->.-<%s*/%s*[Ss][Cc][Oo][Rr][Ee]%s*>", "") -- remove musical scores
	text = mw.ustring.gsub(text, "<%s*[Ii][Mm][Aa][Gg][Ee][Mm][Aa][Pp].->.-<%s*/%s*[Ii][Mm][Aa][Gg][Ee][Mm][Aa][Pp]%s*>", convertImageMap) -- convert imagemaps into standard images
	text = mw.ustring.gsub(text, "%s*{{%s*[Tt][Oo][Cc].-}}", "") -- remove most common tables of contents
	text = mw.ustring.gsub(text, "%s*__[A-Z]*TOC__", "") -- remove TOC behavior switches
	text = mw.ustring.gsub(text, "\n%s*{{%s*[Pp]p%-.-}}", "\n") -- remove protection templates
	text = mw.ustring.gsub(text, "%s*{{[^{|}]*[Ss]idebar%s*}}", "") -- remove most sidebars
	text = mw.ustring.gsub(text, "%s*{{[^{|}]*%-[Ss]tub%s*}}", "") -- remove most stub templates
	text = mw.ustring.gsub(text, "%s*%[%[%s*:?[Cc]ategory:.-%]%]", "") -- remove categories
	text = mw.ustring.gsub(text, "^:[^\n]+\n","") -- remove DIY hatnote indented with a colon
	return text
end

-- Parse a ==Section== from a page
local function getSection(text, section, mainOnly)
	local escapedSection = mw.ustring.gsub(mw.uri.decode(section), "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") -- %26 → & etc, then ^ → %^ etc.
	local level, content = mw.ustring.match(text .. "\n", "\n(==+)%s*" .. escapedSection .. "%s*==.-\n(.*)")
	if not content then return luaError("sectionNotFound", section) end
	local nextSection
	if mainOnly then
		nextSection = "\n==.*" -- Main part of section terminates at any level of header
	else
		nextSection = "\n==" .. mw.ustring.rep("=?", #level - 2) .. "[^=].*" -- "===" → "\n===?[^=].*", matching "==" or "===" but not "===="
	end
	content = mw.ustring.gsub(content, nextSection, "") -- remove later sections with headings at this level or higher
	if mw.ustring.match(content, "^%s*$") then return luaError("sectionEmpty", section) end
	return content
end

-- Parse a <section begin="Name of the fragment">
-- @todo Implement custom parsing of fragments rather than relying on #lst
local function getFragment(page, fragment)
	local frame = mw.getCurrentFrame()
	local text = frame:callParserFunction('#lst', page, fragment)
	if mw.ustring.match(text, "^%s*$") then return luaError("fragmentEmpty", fragment) end
	return text
end

-- Remove unmatched <tag> or </tag> tags
local function fixTags(text, tag)
	local startCount = 0
	for i in mw.ustring.gmatch(text, "<%s*" .. tag .. "%f[^%w_].->") do startCount = startCount + 1 end

	local endCount = 0
	for i in mw.ustring.gmatch(text, "<%s*/" .. tag .. "%f[^%w_].->") do endCount = endCount + 1 end

	if startCount > endCount then -- more <tag> than </tag>: remove the last few <tag>s
		local i = 0
		text = mw.ustring.gsub(text, "<%s*" .. tag .. "%f[^%w_].->", function(t)
			i = i + 1
			if i > endCount then return "" else return nil end
		end) -- "end" here terminates the anonymous replacement function(t) passed to gsub
	elseif endCount > startCount then -- more </tag> than <tag>: remove the first few </tag>s
		text = mw.ustring.gsub(text, "<%s*/" .. tag .. "%f[^%w_].->", "", endCount - startCount)
	end
	return text
end

local function fixTemplates(text)
	repeat -- hide matched {{template}}s including nested templates
		local t = text
		text = mw.ustring.gsub(text, "{(%b{})}", "\27{\27%1\27}\27") -- {{sometemplate}} → E{Esometemplate}E}E where E represents escape
		text = mw.ustring.gsub(text, "(< *math[^>]*>[^<]-)}}(.-< */math *>)", "%1}\27}\27%2") -- <math>\{sqrt\{hat{x}}</math> → <math>\{sqrt\{hat{x}E}E</math>
	until text == t
	text = text.gsub(text, "([{}])%1[^\27].*", "") -- remove unmatched {{, }} and everything thereafter, avoiding }E}E etc.
	text = text.gsub(text, "([{}])%1$", "") -- remove unmatched {{, }} at end of text
	text = mw.ustring.gsub(text, "\27", "") -- unhide matched pairs: E{E{ → {{, etc.
	return text
end

local function fixLinks(text)
	repeat -- hide matched [[wikilink]]s including nested links like [[File:Example.jpg|Some [[nested]] link.]]
		local t = text
		text = mw.ustring.gsub(text, "%[(%b[])%]", "\27[\27%1\27]\27")
	until text == t
	text = text.gsub(text, "([%[%]])%1[^\27].*", "") -- remove unmatched [[ or ]] and everything thereafter, avoiding ]E]E etc.
	text = text.gsub(text, "([%[%]])%1$", "") -- remove unmatched [[ or ]] at end of text
	text = mw.ustring.gsub(text, "\27", "") -- unhide matched pairs: ]E]E → ]], etc.
	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 <ref name="Foo"> for <ref name="Title of the article Foo">
-- and also <ref name="Foo" /> for <ref name="Title of the article Foo" />
-- also remove reference groups: <ref name="Foo" group="Bar"> for <ref name="Title of the article Foo">
-- and <ref group="Bar"> for <ref>
-- @todo The current regex may fail in cases with both kinds of quotes, like <ref name="Darwin's book">
local function fixRefs(text, page, full)
	if not full then full = getContent(page) end
	local refNames = {}
	local refName
	local refBody
	local position = 1
	while position < mw.ustring.len(text) do
		refName, position = mw.ustring.match(text, "<%s*[Rr][Ee][Ff][^>]*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 = mw.ustring.gsub(refName, "[%^%$%(%)%.%[%]%*%+%-%?%%]", "%%%0") -- escape special characters
				refBody = mw.ustring.match(text, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?%s*" .. refName .. "%s*[\"']?[^>/]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>")
				if not refBody then -- the ref body is not in the excerpt
					refBody = mw.ustring.match(full, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?%s*" .. refName .. "%s*[\"']?[^/>]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>")
					if refBody then -- the ref body was found elsewhere
						text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?%s*" .. refName .. "%s*[\"']?[^>]*/?%s*>", refBody, 1)
					end
				end
			end
		else
			position = mw.ustring.len(text)
		end
	end
	text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?([^\"'>/]+)[\"']?[^>/]*(/?)%s*>", '<ref name="' .. page .. ' %1" %2>')
	text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*group%s*=%s*[\"']?[^\"'>/]+[\"']%s*>", '<ref>')
	return text
end

-- Replace the bold title or synonym near the start of the article by a wikilink to the article
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, "%[") then -- if not wikilinked
				return "'''[[" .. page .. "|" .. b .. "]]'''" -- replace '''Foo''' by '''[[page|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

-- Main function for modules
local function get(page, options)
	if options.errors then errors = options.errors end

	if not page or page == "" then return luaError("noPage") end

	local text
	page, section = mw.ustring.match(page, "([^#]+)#?([^#]*)")
	text, page = getContent(page)
	if not page then return luaError("noPage") end
	if not text then return luaError("pageNotFound", page) end
	local full = text -- save the full text for later

	if is(options.fragment) then
		text = getFragment(page, options.fragment)
	end

	if is(section) then
		text = getSection(text, section)
	end

	-- Strip text of all undersirables
	text = cleanupText(text, options)
	text = parse(text, options)

	-- Replace the bold title or synonym near the start of the article by a wikilink to the article
	text = linkBold(text, page)

	-- Remove '''bold text''' if requested
	if is(options.nobold) then text = mw.ustring.gsub(text, "'''", "") end

	-- Keep only tables if requested
	if is(options.tablesOnly) then text = getTables(text) end

	-- Keep only lists if requested
	if is(options.listsOnly) then text = getLists(text) end

	-- Seek and destroy unterminated templates, links and tags
	text = fixTemplates(text)
	text = fixLinks(text)
	text = fixTags(text, "div")

	-- Fix broken references
	if is(options.keepRefs) then text = fixRefs(text, page, full) end

	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
		text = d.errors.prefix .. text
		if d.errorsCategory and d.errorsCategory ~= "" and mw.title.getCurrentTitle().isContentPage then
			text = text .. '[[' .. d.errorsCategory .. ']]'
		end
		return mw.html.create('div'):addClass('error'):wikitext(text)
	end
	return frame:preprocess(text)
end

-- Entry points for templates
function p.main(frame) return main(frame) end
function p.wikiError(message, value) return wikiError(message, value) end

-- Entry points for other Lua modules
function p.get(page, options) return get(page, options) end
function p.getContent(page) return getContent(page) end
function p.getSection(text, section) return getSection(text, section) end
function p.getTables(text, options) return getTables(text, options) end
function p.getLists(text, options) return getLists(text, options) end
function p.parse(text, options) return parse(text, options) end
function p.parseImage(text, start) return parseImage(text, start) end
function p.parseArgs(frame) return parseArgs(frame) end
function p.argImage(text) return argImage(text) end
function p.checkImage(image) return checkImage(image) end
function p.cleanupText(text, options) return cleanupText(text, options) end
function p.luaError(message, value) return luaError(message, value) end
function p.is(value) return is(value) end
function p.numberFlags(str) return numberFlags(str) end

-- Entry points for backwards compatibility
function p.getsection(text, section) return getSection(text, section) end
function p.parseimage(text, start) return parseImage(text, start) end
function p.checkimage(image) return checkImage(image) end
function p.argimage(text) return argImage(text) end
function p.numberflags(str) return numberFlags(str) end

return p