Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

Jump to content

Module:Docbunto

From Wikipedia, the free encyclopedia

--- Docbunto is an automatic documentation generator for Scribunto modules.
--  The module is based on LuaDoc and LDoc. It produces documentation in
--  the form of MediaWiki markup, using `@tag`-prefixed comments embedded
--  in the source code of a Scribunto module. The taglet parser & doclet
--  renderer Docbunto uses are also publicly exposed to other modules.
--  
--  Docbunto code items are introduced by a block comment (`--[[]]--`), an
--  inline comment with three hyphens (`---`), or an inline `@tag` comment.
--  The module can use static code analysis to infer variable names, item
--  privacy (`local` keyword), tables (`{}` constructor) and functions
--  (`function` keyword). MediaWiki and Markdown formatting is supported.
--  
--  Items are usually rendered in the order they are defined, if they are
--  public items, or emulated classes extending the Lua primitives. There
--  are many customisation options available to change Docbunto behaviour.
--  
--  @module			 docbunto
--  @alias			  p
--  @require			Module:I18n
--  @require			Module:Lua_lexer
--  @require			Module:Unindent
--  @require			Module:Yesno
--  @require			Module:Arguments
--  @require			Module:Documentation
--  @author				[[wikia:dev:User:8nml|8nml]] (Fandom Dev Wiki)
--  @author				[[User:Awesome Aasim|Awesome Aasim]] (Wikimedia port)
--  @attribution		[https://github.com/stevedonovan @stevedonovan] ([https://github.com/stevedonovan/LDoc GitHub])
--  @release			beta
--  <nowiki>
local p = {}

--  Module dependencies.
local title = mw.title.getCurrentTitle()
local i18n = require('Module:I18n').loadMessages('Docbunto'):useContentLang()
local references = mw.loadData('Module:Docbunto/references')
local lexer = require('Module:Lua lexer')
local unindent = require('Module:Unindent')
local yesno = require('Module:Yesno')
local doc = require('Module:Documentation')

local fileTooBig
local usingOptionsContent
local hasTodoItems

--  Module variables.
local DEFAULT_TITLE = title.namespace == 828 and doc.getEnvironment({}).templateTitle.text or ''
local DEFAULT_VARIABLE = DEFAULT_TITLE
	:gsub('^%u', mw.ustring.lower)
	:gsub('%s', '_'):gsub('%p', '.')
local frame, gsub, match

local syntaxhighlightPresent = mw.getCurrentFrame()
	:preprocess("<syntaxhighlight lang=\"lua\">-- Lua</syntaxhighlight>")
		~= "<syntaxhighlight lang=\"lua\">-- Lua</syntaxhighlight>"

--  Docbunto variables & tag tokens.
local TAG_MULTI = 'M'
local TAG_ID = 'ID'
local TAG_SINGLE = 'S'
local TAG_TYPE = 'T'
local TAG_FLAG = 'N'
local TAG_MULTI_LINE = 'ML'

--  Docbunto processing patterns.
local patterns = {}

--- Configuration options.
--  @table              options 
--  @field[opt]         {boolean} options.all Include local items in
--                      documentation.
--  @field[opt]         {boolean} options.autodoc Whether the module is being
--                      called to automatically provide missing documentation.
--  @field[opt]         {boolean} options.boilerplate Removal of
--                      boilerplate (license block comments).
--  @field[opt]         {string} options.caption Infobox image caption.
--  @field[opt]         {boolean} options.code Only document Docbunto code
--                      items - exclude article infobox and lede from
--                      rendered documentation. Permits article to be
--                      edited in VisualEditor.
--  @field[opt]         {boolean} options.colon Format tags with a `:` suffix
--                      and without the `@` prefix. This bypasses the "doctag
--                      soup" some authors complain of.
--  @field[opt]         {string} options.image Infobox image.
--  @field[opt]         {boolean} options.box Include infobox in output.
--  @field[opt]         {boolean} options.footer Append a horizontal line &
--                      "generated by Docbunto" to the bottom of the output.
--  @field[opt]         {boolean} options.noluaref Don't link to the [[mw:
--  					Extension:Scribunto/Lua reference manual|Lua reference manual]] for types.
--  @field[opt]         {boolean} options.card Include endmatter card in the
--                      output at the end of the documentation.
--  @field[opt]         {boolean} options.plain Disable Markdown formatting
--                      in documentation.
--  @field[opt]         {string} options.preface Preface text to insert
--                      between lede & item documentation, used to provide
--                      usage and code examples.
--  @field[opt]         {boolean} options.simple Limit documentation to
--                      descriptions only. Removes documentation of
--                      subitem tags such as `@param` and `@field` ([[#Item
--                      subtags|see list]]).
--  @field[opt]         {boolean} options.sort Sort documentation items in
--                      alphabetical order.
--  @field[opt]         {boolean} options.strip Remove table index in
--                      documentation.
--  @field[opt]         {boolean} options.ulist Indent subitems as `<ul>`
--                      lists (LDoc/JSDoc behaviour).
--  @field[opt]         {boolean} options.verbose Show the code
--                      description of a package function when documenting
--                      function modules.

--  Docbunto private utilities.
local utils = {}

--- @{string.find} optimisation for @{string} functions.
--  @function           utils.strfind_wrap
--  @param              {function} strfunc String library function.
--  @return             {function} Function wrapped in @{string.find} check.
--  @local
function utils.strfind_wrap(func)
	return function(...)
		local arg = {...}
		if string.find(arg[1], arg[2]) then
			return func(...);
		end
	end
end


--- String concatenation utility.
--  @function           join
--  @param              {sequence<string>} tbl Table of string segments
--  @return             {function} A concatenated string.
--  @local
function utils.join(tbl)
	return table.concat(tbl, '')
end

--- Pattern configuration function.
--  Resets patterns for each documentation build.
--  @function           utils.configure_patterns
--  @param              {options} options Configuration options.
--  @local
function utils.configure_patterns(options)
	-- Setup Unicode or ASCII character encoding (optimisation).
	gsub = utils.strfind_wrap(
		options.unicode
			and mw.ustring.gsub
			or  string.gsub
	)
	match = utils.strfind_wrap(
		options.unicode
			and mw.ustring.match
			or  string.match
	)
	patterns.DOCBUNTO_SUMMARY =
		options.iso639_th
			and '^[^ ]+'
			or
		options.unicode
			and '^[^.։。।෴۔።]+[.։。।෴۔።]?'
			or  '^[^.]+%.?'
	patterns.DOCBUNTO_CONCAT = options.iso639_th and '' or ' '
	patterns.DOCBUNTO_TYPE = '^{({*[^}]+}*)}%s*'
	patterns.DOCBUNTO_WIKITEXT = '^[{|!}:#*=]+[%s-}]+'

	-- Setup parsing tag patterns with colon mode support.
	patterns.DOCBUNTO_TAG = options.colon and '^%s*(%w+):' or '^%s*@(%w+)'
	patterns.DOCBUNTO_TAG_COMMENT = '^[-%s]*' .. patterns.DOCBUNTO_TAG:sub(2)
	patterns.DOCBUNTO_TAG_VALUE = patterns.DOCBUNTO_TAG .. '(.*)'
	patterns.DOCBUNTO_TAG_MOD_VALUE = patterns.DOCBUNTO_TAG .. '%[([^%]]*)%](.*)'
	patterns.DOCBUNTO_TAG_OPTION_VALUE = '(%[[^%]]*%])%s*(.*)'
end

--- Tag processor function.
--  @function           utils.process_tag
--  @param              {string} str Tag string to process.
--  @return             {table} Tag object.
--  @local
function utils.process_tag(str)
	local tag = {}

	if str:find(patterns.DOCBUNTO_TAG_MOD_VALUE) then
		tag.name, tag.modifiers, tag.value = str:match(patterns.DOCBUNTO_TAG_MOD_VALUE)
		local modifiers = {}

		for m in tag.modifiers:gmatch('[^%s,]+') do
			local i = m:find('=')
			if i then
				local key = mw.text.trim(m:sub(1, i - 1))
				local val = mw.text.trim(m:sub(i + 1, #m))
				modifiers[key] = val
			else
				modifiers[m] = true
			end
		end

		if modifiers.optchain then
			modifiers.opt = true
			modifiers.optchain = nil
		end

		tag.modifiers = modifiers

	else
		tag.name, tag.value = str:match(patterns.DOCBUNTO_TAG_VALUE)
	end

	tag.value = mw.text.trim(tag.value)

	if p.tags._type_alias[tag.name] then
		if p.tags._type_alias[tag.name] ~= 'variable' then
			tag.value = p.tags._type_alias[tag.name] .. ' ' .. tag.value
			tag.name = 'field'
		end

		if tag.value:match('^%S+') ~= '...' then
		   tag.value = tag.value:gsub('^(%S+)', '{%1}')
		end
	end

	tag.name = p.tags._alias[tag.name] or tag.name

	if tag.name ~= 'usage' and tag.value:find(patterns.DOCBUNTO_TYPE) then
		tag.type = tag.value:match(patterns.DOCBUNTO_TYPE)
		if tag.type:find('^%?') then
			tag.type = tag.type:sub(2) .. '|nil'
		end
		tag.value = tag.value:gsub(patterns.DOCBUNTO_TYPE, '')
	end
	
	if (tag.name == 'param' or tag.name == 'field') and tag.value:find(patterns.DOCBUNTO_TAG_OPTION_VALUE) then
		tag.value = tag.value:gsub(patterns.DOCBUNTO_TAG_OPTION_VALUE, '%1 %2')
		tag.modifiers = tag.modifiers or {}
		tag.modifiers.opt = true
	end

	if p.tags[tag.name] == TAG_FLAG then
		tag.value = true
	end

	return tag
end

--- Module info extraction utility.
--  @function           utils.extract_info
--  @param              {table} documentation Package doclet info.
--  @return             {table} Information name-value map.
--  @local
function utils.extract_info(documentation)
	local info = {}

	for _, tag in ipairs(documentation.tags) do
		if p.tags._module_info[tag.name] then
			if info[tag.name] then
				if not info[tag.name]:find('^%* ') then
					info[tag.name] = '* ' .. info[tag.name]
				end
				info[tag.name] = info[tag.name] .. '\n* ' .. tag.value

			else
				info[tag.name] = tag.value
			end
		end
	end

	return info
end

--- Type extraction utility.
--  @function           utils.extract_types
--  @param              {table} item Item documentation data.
--  @return             {sequence<string>} Item types.
--  @local
function utils.extract_types(item)
	local item_types = {}
	for _, tag in ipairs(item.tags) do
		if p.tags[tag.name] == TAG_TYPE then
			item_types[1] = tag.name

			if tag.name == 'variable' then
				local implied_local = utils.process_tag('@local')
				table.insert(item.tags, mw.clone(implied_local))
				item.tags['local'] = mw.clone(implied_local)
			end

			if
				p.tags._generic_tags[item_types[1]] and
				not p.tags._project_level[item_types[1]] and
				tag.type
			then
				item_types[#item_types + 1] = tag.type 
			end
			break
		end
	end
	return item_types
end

--- Name extraction utility.
--  @function           utils.extract_name
--  @param              {table} item Item documentation data.
--  @param              {boolean} project Whether the item is project-level.
--  @return             {string} Item name.
--  @local
function utils.extract_name(item, opts)
	opts = opts or {}
	local item_name
	for _, tag in ipairs(item.tags) do
		if p.tags[tag.name] == TAG_TYPE then
			item_name = tag.value; break
		end
	end

	if item_name or not opts.project then
		return item_name
	end

	item_name = item.code:match('\nreturn%s+([%w_]+)')

	if item_name == 'p' and not item.tags['alias'] then
		local implied_alias = { name = 'alias', value = 'p' }
		item.tags['alias'] = implied_alias
		table.insert(item.tags, implied_alias)
	end

	item_name = (item_name and item_name ~= 'p')
		and item_name
		or  item.filename
				:gsub('^' .. mw.site.namespaces[828].name .. ':', '')
				:gsub('^(%u)', mw.ustring.lower)
				:gsub('/', '.'):gsub(' ', '_')

	return item_name
end

--- Source code utility for item name detection.
--  @function           utils.deduce_name
--  @param              {string} tokens Stream tokens for first line.
--  @param              {string} index Stream token index.
--  @param              {table} opts Configuration options.
--  @param[opt]         {boolean} opts.lookahead Whether a variable name succeeds the index.
--  @param[opt]         {boolean} opts.lookbehind Whether a variable name precedes the index.
--  @return             {string} Item name.
--  @local
function utils.deduce_name(tokens, index, opts)
	local name = ''

	if opts.lookbehind then
		for i2 = index, 1, -1 do
			if tokens[i2].type ~= 'keyword' then
				name = tokens[i2].data .. name
			else
				break
			end
		end

	elseif opts.lookahead then
		for i2 = index, #tokens do
			if tokens[i2].type ~= 'keyword' and not tokens[i2].data:find('^%(') then
				name = name .. tokens[i2].data
			else
				break
			end
		end
	end

	return name
end

--- Parses Lua item names to extract a hierarchy.
--- @function           utils.parse_hierarchy
--  @param              {string} item_name Documentation item name.
--  @return             {sequence<string>} List of global keys to index the
--                      variable reference.
--  @local
function utils.parse_hierarchy(item_name)
	local hierarchy = {}
	for _, lexeme in ipairs(lexer(item_name)) do
		if lexeme.type == 'ident' or lexeme.type == 'string' or lexeme.type == 'number' then
			hierarchy[#hierarchy + 1] = lexeme.text
		end
	end
	return hierarchy
end

--- Code analysis utility.
--  @function           utils.code_heuristic
--  @param              {table} item Item documentation data.
--  @local
function utils.code_heuristic(item)
	local is_section = item.tags['private'] or item.tags['local'] or item.type == 'type'

	if item.name and item.type and is_section then
		return
	end
	
	local tokens = lexer(item.code:match('^[^\n]*'))[1]
	local t, i = tokens[1], 1
	local item_name, item_type
	local lexemes = {}

	while t do
		if t.type ~= 'whitespace' then
			lexemes[#lexemes + 1] = tokens[i]
		end

		t, i = tokens[i + 1], i + 1
	end
	t, i = lexemes[1], 1

	while t do
		if t.data == '=' and not item.name then
			item_name = utils.deduce_name(lexemes, i - 1, { lookbehind = true })
		end

		if t.data == 'function' and not item.name then
			item_type = 'function'
			if lexemes[i + 1] and lexemes[i + 1].data ~= '(' then
				item_name = utils.deduce_name(lexemes, i + 1, { lookahead = true })
			end
		end

		if t.data == '{' or t.data == '{}' and not item.type then
			item_type = 'table'
		end

		if t.data == 'local' and not is_section then
			local implied_local = utils.process_tag('@local')
			table.insert(item.tags, implied_local)
			item.tags['local'] = mw.clone(implied_local)
		end

		t, i = lexemes[i + 1], i + 1
	end

	item.name = item.name or item_name or ''
	item.type = item.type or item_type
	if #(item.types or {}) == 0 then
		item.types[1] = item.type
	end
end

--- Array hash map conversion utility.
--  @function           utils.hash_map
--  @param              {table} item Item documentation data array.
--  @return             {table} Item documentation data map.
--  @local
function utils.hash_map(array)
	local map = array
	for _, element in ipairs(array) do
		if map[element.name] and not map[element.name].name then
			table.insert(map[element.name], mw.clone(element))
		elseif map[element.name] and map[element.name].name then
			map[element.name] = { map[element.name], mw.clone(element) }
		else
			map[element.name] = mw.clone(element)
		end
	end
	return map
end

--- Item export utility.
--  @function           utils.export_item
--  @param              {table} documentation Package documentation data.
--  @param              {string} item_reference Identifier name for item.
--  @param              {string} item_index Identifier name for item.
--  @param              {string} item_alias Export alias for item.
--  @param              {boolean} factory_item Whether the documentation item is a factory function.
--  @local
function utils.export_item(documentation, item_reference, item_index, item_alias, factory_item)
	for _, item in ipairs(documentation.items) do
		if item_reference == item.name then
			item.tags['local'] = nil
			item.tags['private'] = nil

			for index, tag in ipairs(item.tags) do
				if p.tags._privacy_tags[tag.name] then
					table.remove(item.tags, index)
				end
			end

			item.type, item.types[1] = 'member', 'member'

			local accessor
			if item_alias:find('^%[') then
				accessor = ''
			else
				local is_class = documentation.type == 'classmod' or factory_item
				if is_class and not item.tags['static'] then
					accessor = ':'
				else
					accessor = '.'
				end
			end

			if factory_item then
				item.alias = utils.join{
					documentation.items[item_index].tags['factory'].value,
					accessor,
					item_alias
				}

			else
				item.alias = utils.join{
					((documentation.tags['alias'] or {}).value or documentation.name),
					accessor,
					item_alias
				}
			end

			item.hierarchy = utils.parse_hierarchy(item.alias)
			break
		end
	end
end

--- Subitem tag correction utility.
--  @function           utils.correct_subitem_tag
--  @param              {table} item Item documentation data.
--  @local
function utils.correct_subitem_tag(item)
	local field_tag = item.tags['field']
	if item.type ~= 'function' or not field_tag then
		return
	end

	if field_tag.name then
		field_tag.name = 'param'
	else
		for _, tag_el in ipairs(field_tag) do
			tag_el.name = 'param'
		end
	end

	local param_tag = item.tags['param']
	if param_tag and not param_tag.name then
		if field_tag.name then
			table.insert(param_tag, field_tag)
		else
			for _, tag_el in ipairs(field_tag) do
				table.insert(param_tag, tag_el)
			end
		end

	elseif param_tag and param_tag.name then
		if field_tag.name then
			param_tag = { param_tag, field_tag }

		else
			for i, tag_el in ipairs(field_tag) do
				if i == 1  then
					param_tag = { param_tag }
				end
				for _, tag_el in ipairs(field_tag) do
					table.insert(param_tag, tag_el)
				end
			end
		end

	else
		param_tag = field_tag
	end

	item.tags['field'] = nil
end

--- Item override tag utility.
--  @function           utils.override_item_tag
--  @param              {table} item Item documentation data.
--  @param              {string} name Tag name.
--  @param[opt]         {string} alias Target alias for tag.
--  @local
function utils.override_item_tag(item, name, alias)
	if item.tags[name] then
		item[alias or name] = item.tags[name].value
	end
end

--- Identity function.
--  @function           utils.identity
--  @param              x The variable by reference.
--  @return             The parameter `x` by reference.
--  @local
function utils.identity(x)
	return x
end

--- Argument processing
-- @param				{string} funcName name of function
-- @return				function to process arguments
-- @local
local function makeInvokeFunc(funcName)
	return function (f)
		local args = require("Module:Arguments").getArgs(f, {
			valueFunc = function (key, value)
				if type(value) == 'string' then
					value = value:match('^%s*(.-)%s*$') -- Remove whitespace.
					if key == 'heading' or value ~= '' then
						return value
					else
						return nil
					end
				else
					return value
				end
			end
		})
		return p[funcName](args)
	end
end

--  Docbunto rendering logic.
local frontend = {}

--- Markdown header converter.
--  @function           urils.markdown_header
--  @param              {string} hash Leading hash.
--  @param              {string} text Header text.
--  @return             {string} MediaWiki header.
--  @local
function frontend.markdown_header(hash, text)
	local symbol = '='
	return utils.join{
		'\n',
		symbol:rep(#hash), ' ', text, ' ', symbol:rep(#hash),
		'\n'
	}
end

--- Item reference formatting.
--  @function           urils.item_reference
--  @param              {string} ref Item reference.
--  @return             {string} Internal MediaWiki link to article item.
--  @local
function frontend.item_reference(ref)
	local temp = mw.text.split(ref, '|')
	local item = temp[1]
	local text = temp[2] or temp[1]

	if references.items[item] then
		local interwiki = 'mw'
		item = interwiki .. references.items[item]
	else
		item = '#' .. item
	end

	return mw.text.tag{
		name = 'code',
		content = utils.join{ '[[', item, '|', text, ']]' }
	}
end

--- Doclet type reference preprocessor.
--  Formats types with links to the [[mw:Extension:Scribunto/Lua reference manual|Lua reference manual]].
--  @function           frontend.type_reference
--  @param              {table} item Item documentation data.
--  @param              {options} options Configuration options.
--  @local
function frontend.type_reference(item, options)
	local interwiki = 'mw:'

	if
		not options.noluaref and
		item.value and
		item.value:match('^%S+') == mw.text.tag('code', nil, '...')
	then
		item.value = item.value:gsub('^(%S+)', mw.text.tag{
			name = 'code',
			content = utils.join{ '[[', interwiki, 'Lua reference manual#varargs|...]]' }
		})
	end

	-- Handle tag type references.
	item.types = item.types or item.type and { item.type }

	if not item.types then
		return
	end

	for index, item_type in ipairs(item.types) do
		item.types[index] = item.types[index]:gsub('[%w_. ]+', function(item_type)
			local data = references.types[item_type]
			local name = data and data.name or item_type
			if not name:match('%.') and not name:match('^%u') and data then
				name = i18n:msg('type-' .. name)
			end
			if data and not options.noluaref then
				return utils.join{ '[[', interwiki, data.link, '|', name , ']]' }
			elseif
				item_type:find(i18n:msg('line-no'):gsub('$1', ''))
			then
				return utils.join { '[[' , options.file , '#L-',  item_type:sub(6), '|', name, ']]' }
			elseif
				not options.noluaref and
				not p.tags._generic_tags[item_type]
			then
				return utils.join { '[[#', item_type, '|', name, ']]' }
			end
		end)
	end

	item.type = table.concat(item.types, i18n:msg('separator-dot'))
end

--- Markdown preprocessor to MediaWiki format.
--  @function           frontend.markdown
--  @param              {string} str Unprocessed Markdown string.
--  @return             {string} MediaWiki-compatible markup with HTML formatting.
--  @local
function frontend.markdown(str)
	-- Bold & italic tags.
	str = str:gsub('%*%*%*([^\n*]+)%*%*%*', '<b><i>%1</i></b>')
	str = str:gsub('%*%*([^\n*]+)%*%*', '<b>%1</b>')
	str = str:gsub('%*([^\n*]+)%*', '<i>%1</i>')

	-- Self-closing header support.
	str = str:gsub('%f[^\n%z](#+) *([^\n#]+) *#+%s', frontend.markdown_header)

	-- External and internal links.
	str = str:gsub('%[([^\n%]]+)%]%(([^\n][^\n)]-)%)', '[%2 %1]')
	str = str:gsub('%{@link%s+([^}]+)%}', '[%1]')
	str = str:gsub('%@{([^\n}]+)}', frontend.item_reference)

	-- Programming & scientific notation.
	str = str:gsub('%f["`]`([^\n`]+)`%f[^"`]', '<code><nowiki>%1</nowiki></code>')
	str = str:gsub('%$%$([^\n$]+)%$%$', '<math display="inline">%1</math>')

	-- Strikethroughs.
	str = str:gsub('~~([^\n~]+)~~', '<del>%1</del>')
	
	-- HTML output.
	return str
end

--- Doclet function item preprocessor.
--  Formats item name as a function call with top-level arguments.
--  @function           frontend.pretask_function_name
--  @param              {table} item Item documentation data.
--  @param              {options} options Configuration options.
--  @local
function frontend.pretask_function_name(item, options)
	local target = item.alias and 'alias' or 'name'

	item[target] = item[target] .. '('

	if
		item.tags['param'] and
		item.tags['param'].value and
		not item.tags['param'].value:find('^[%w_]+[.[]')
	then
		if (item.tags['param'].modifiers or {})['opt'] then
			local optional = mw.html.create('span')
			optional:css('opacity', '0.65')
			optional:wikitext(item.tags['param'].value:match('^(%S+)'))
			item[target] = item[target] .. tostring(optional)
		else
			item[target] = item[target] .. item.tags['param'].value:match('^(%S*)')
		end

	elseif item.tags['param'] then
		for index, tag in ipairs(item.tags['param']) do
			if not tag.value:find('^[%w_]+[.[]') then
				if (tag.modifiers or {})['opt'] then
					local param = mw.html.create('span')
					param:css('opacity', '0.65')
					param:wikitext((index > 1 and ', ' or '') .. tag.value:match('^%S+'))
					item[target] = item[target] .. tostring(param)
				else
					item[target] = item[target] .. (index > 1 and ', ' or '') .. tag.value:match('^(%S+)')
				end
			end
		end
	end

	item[target] = item[target] .. ')'
end

--- Doclet parameter/field subitem preprocessor.
--  Indents and wraps variable prefix with `code` tag.
--  @function           pretask_variable_prefix
--  @param              {table} item Item documentation data.
--  @param              {options} options Configuration options.
--  @local
function frontend.pretask_variable_prefix(item, options)
	local indent_symbol = options.ulist and '*' or ':'
	local indent_level, indentation, item_id

	if item.value then
		indent_level = item.value:match('^%S*') == '...'
			and 0
			or  select(2, item.value:match('^%S*'):gsub('[.[]', ''))
		indentation = indent_symbol:rep(indent_level)
		item_id = options.item_id
		item.value = indentation .. item.value:gsub(
			'^(%S+)',
			mw.text.tag{
				name = 'code',
				attrs = { id = item_id .. '~%1' },
				content = '%1'
			},
			1
		)
		

	elseif item then
		for _, item_el in ipairs(item) do
			frontend.pretask_variable_prefix(item_el, options)
		end
	end
end

--- Doclet usage subitem preprocessor.
--  Formats usage example with `<syntaxhighlight>` tag.
--  @function           frontend.pretask_usage_highlight
--  @param              {table} item Item documentation data.
--  @param              {options} options Configuration options.
--  @local
function frontend.pretask_usage_highlight(item, options)
	if item.value then
		item.value = unindent(mw.text.trim(item.value))
		if item.value:find('^{{.+}}$') then
			item.value = item.value:gsub('=', mw.text.nowiki)
			local multi_line = item.value:find('\n') and '|m = 1|' or '|'

			if item.value:match('^{{([^:]+)') == '#invoke' then
				item.value = item.value:gsub('^{{#invoke:', '{{mlx' .. multi_line)

			else
				if options.entrypoint then
					item.value = item.value:gsub('^([^|]+)|%s*([^|}]-)(%s*)([|}])','%1|"%2"%3%4')
				end
				item.value = item.value:gsub('^{{', '{{tlx' .. multi_line)
			end

			local highlight_class = tonumber(mw.site.currentVersion:match('^%d%.%d+')) > 1.19
				and 'mw-highlight'
				or  'mw-geshi'

			if item.value:find('\n') then
				local highlight_div = mw.html.create('div')
				highlight_div.addClass(highlight_class)
				highlight_div.addClass('mw-content-ltr')
				highlight_div:attr('dir', 'ltr')
				highlight_div:wikitext(item.value)
				item.value = tostring(highlight_div)

			else
				item.value = mw.text.tag{
					name = 'span',
					attrs = { class = 'code' },
					content = item.value
				}
			end

		else
			local highlight_tag = mw.html.create('syntaxhighlight')
			highlight_tag:attr('lang', 'lua')
			if not item.value:find('\n') then
				highlight_tag:attr('inline', 'inline')
			end
			highlight_tag:wikitext(item.value)
			item.value = tostring(highlight_tag)
		end

	elseif item then
		for _, item_el in ipairs(item) do
			frontend.pretask_usage_highlight(item_el, options)
		end
	end
end

--- Doclet error subitem preprocessor.
--  Formats line numbers (`{#}`) in error tag values.
--  @function           frontend.pretask_error_line
--  @param              {table} item Item documentation data.
--  @local
function frontend.pretask_error_line(item, options)
	if item.name then
		local line

		if item.modifiers and item.modifiers.line then
			line = item.modifiers.line
		end

		for mod in pairs(item.modifiers or {}) do
			if mod:find('^%d+$') then line = mod end
		end

		if line then
			if item.types then
				item.types[#item.types + 1] = i18n:msg('line-no', line)

			else
				item.type = i18n:msg('line-no', line)
			end
		end

	elseif item then
		for _, item_el in ipairs(item) do
			frontend.pretask_error_line(item_el, options)
		end
	end
end

--- Doclet item renderer.
--  @function           frontend.render_item
--  @param              {table} stream Wikitext documentation stream.
--  @param              {table} item Item documentation data.
--  @param              {options} options Configuration options.
--  @param[opt]         {function} pretask Item data preprocessor.
--  @local
function frontend.render_item(stream, item, options, pretask)
	if pretask then pretask(item, options) end
	local item_name = item.alias or item.name

	frontend.type_reference(item, options)

	local item_type = item.type

	for _, name in ipairs(p.tags._subtype_hierarchy) do
		if item.tags[name] then
			item_type = item_type .. i18n:msg('separator-dot') .. name
		end
	end
	item_type = i18n:msg('parentheses', item_type)

	if options.strip and item.export and item.hierarchy then
		item_name = item_name:gsub('^[%w_]+[.[]?', '')
	end

	-- add single spaces between parentheses similar to
	-- MediaWiki documentation
	if not item_name:find( '()', 1, true ) then
		item_name = item_name:gsub('%(', '( ')
		item_name = item_name:gsub('%)', ' )')
	end
	item_name = item_name:gsub('%[', '[ ')
	item_name = item_name:gsub('%]', ' ]')

	local code_node = mw.html.create('code')
		:attr('id', options.item_id)
		:wikitext(item_name)

	if not usingOptionsContent then
		local span_node = stream:wikitext(';')
			:tag('span')
			:addClass('plainlinks docbunto-link')
		if fileTooBig or not syntaxhighlightPresent then
			span_node:wikitext('[{{fullurl:' .. options.file .. '|action=edit#mw-ce-l' .. item.lineno .. '}} ' .. tostring(code_node) .. ']')
		elseif title.namespace == 828 and options.file == title.text then
			span_node:wikitext('[[' .. '#L-' .. item.lineno .. '|' .. tostring(code_node) .. ']]')
		else
			span_node:wikitext('[[' .. options.file .. '#L-' .. item.lineno .. '|' ..  tostring(code_node) .. ']]')
		end
	else
		stream:wikitext(';' .. tostring(code_node))
	end

	stream
		:wikitext(item_type)
		:newline()

	if (#(item.summary or '') + #item.description) ~= 0 then
		local separator = #(item.summary or '') ~= 0 and #item.description ~= 0
			and (item.description:find(patterns.DOCBUNTO_WIKITEXT) and '\n' or patterns.DOCBUNTO_CONCAT)
			or  ''
		local intro = (item.summary or '') .. separator .. item.description
		intro = intro
			:gsub('\n([{:#*])', '\n:%1')
			:gsub('\n\n([^=])', '\n:%1')
		stream:wikitext(':' .. intro):newline()
	end
end

--- Doclet tag renderer.
--  @function           frontend.render_tag
--  @param              {table} stream Wikitext documentation stream.
--  @param              {string} name Item tag name.
--  @param              {table} tag Item tag data.
--  @param              {options} options Configuration options.
--  @param[opt]         {function} pretask Item data preprocessor.
--  @local
function frontend.render_tag(stream, name, tag, options, pretask)
	if pretask then pretask(tag, options) end
	if tag.value then
		frontend.type_reference(tag, options)
		local tag_name = i18n:msg('tag-' .. name, '1')

		-- Handle ul/ol/dl comment support.
		stream:wikitext(':')
			:tag('b')
				:wikitext(tag_name)
				:done():done()
			:wikitext(i18n:msg('separator-semicolon'))
			:wikitext((mw.text.trim(tag.value):gsub('\n([{:#*])', '\n:%1')))

		if tag.value:find('\n[{:#*]') and (tag.type or (tag.modifiers or {})['opt']) then
			stream:newline():wikitext(':')
		end

		-- Type and optional appositive for tag.
		if tag.type and (tag.modifiers or {})['opt'] then
			stream:wikitext(i18n:msg{
				key = 'parentheses',
				args = {
					tag.type ..
					i18n:msg('separator-colon') ..
					i18n:msg('optional')
				}
			})

		elseif tag.type then
			stream:wikitext(i18n:msg{
				key = 'parentheses',
				args = { tag.type }
			})

		elseif (tag.modifiers or {})['opt'] then
			stream:wikitext(i18n:msg{
				key = 'parentheses',
				args = { i18n:msg('optional') }
			})
		end

		stream:newline()

	else
		local tag_name = i18n:msg('tag-' .. name, tostring(#tag))
		stream
			:wikitext(':')
			:tag('b')
				:wikitext(tag_name)
				:done():done()
			:wikitext(i18n:msg('separator-semicolon'))
			:newline()

		for _, tag_el in ipairs(tag) do
			frontend.type_reference(tag_el, options)
			local marker = options.ulist and '*' or ':'
			stream:wikitext(':' .. marker .. tag_el.value:gsub('\n([{:#*])', '\n:' .. marker .. '%1'))

			if tag_el.value:find('\n[{:#*]') and (tag_el.type or (tag_el.modifiers or {})['opt']) then
				stream:newline():wikitext(':' .. marker .. (tag_el.value:match('^[*:]+') or ''))
			end

			if tag_el.type and (tag_el.modifiers or {})['opt'] then
				stream:wikitext(i18n:msg{
					key = 'parentheses',
					args = {
						tag_el.type ..
						i18n:msg('separator-colon') ..
						i18n:msg('optional')
					}
				})

			elseif tag_el.type then
				stream:wikitext(i18n:msg{
					key = 'parentheses',
					args = { tag_el.type }
				})

			elseif (tag_el.modifiers or {})['opt'] then
				stream:wikitext(i18n:msg{
					key = 'parentheses',
					args = { i18n:msg('optional') }
				})
			end

			stream:newline()
		end
	end
end

--- Template entrypoint for [[Template:Docbunto]].
--  @function           p._main
--  @param              {table} args Arguments
--  @return             {string} Module documentation output.
function p._main(args)
	frame = mw.getCurrentFrame()

	local modname = mw.text.trim(args[1] or args.file or DEFAULT_TITLE)

	local options = {}
	options.file = modname
	options.all = yesno(args.all or false)
	options.autodoc = yesno(args.autodoc or false)
	options.boilerplate = yesno(args.boilerplate or options.autodoc or false)
	options.box = yesno(args.box or args.infobox or options.autodoc or false)
	options.caption = args.caption
	options.card = yesno(args.card or (title.namespace == 828 and not options.autodoc))
	options.code = yesno(args.code or false)
	options.colon = yesno(args.colon or false)
	options.footer = yesno(args.footer or title.namespace == 828)
	options.image = args.image
	options.migration = yesno(args.migration or false)
	options.noluaref = yesno(args.noluaref or false)
	options.plain = yesno(args.plain or false)
	options.preface = args.preface
	options.simple = yesno(args.simple or false)
	options.sort = yesno(args.sort or false)
	options.strip = yesno(args.strip or false)
	options.verbose = yesno(args.verbose or false)
	options.ulist = yesno(args.ulist or false)
	return p.build(modname, options)
end

--- Entrypoint for the module.
--  @function		   p.main
--  @param			  {Frame} frame Module frame.
--  @return			 {string} Module documentation output.
p.main = makeInvokeFunc('_main')

--- Scribunto documentation generator entrypoint.
--  @function           p.build
--  @param[opt]         {string} modname Module page name (without namespace).
--                      Default: second-level subpage.
--  @param[opt]         {options} options Configuration options.
function p.build(modname, options)
	modname = modname or DEFAULT_TITLE
	options = options or {}

	local tagdata = p.taglet(modname, options)
	local docdata = p.doclet(tagdata, options)

	return docdata
end

--- Docbunto taglet parser for Scribunto modules.
--  @function           p.taglet
--  @param[opt]         {string} modname Module page name (without namespace).
--  @param[opt]         {options} options Configuration options.
--  @error[line=1081]   {string} 'Lua source code not found in $1'
--  @error[line=1087]   {string} 'documentation markup for Docbunto not found in $1'
--  @return             {table} Module documentation data.
function p.taglet(modname, options)
	modname = modname or DEFAULT_TITLE
	options = options or {}

	local filepath = mw.site.namespaces[828].name .. ':' .. modname
	local content = mw.title.new(filepath):getContent()

	-- Content checks.
	if options.content then
		content = options.content
		usingOptionsContent = true
	else
		content = mw.title.new(filepath):getContent() or error(i18n:msg('no-content', filepath), 0)
	end

	-- Syntax highlighting is disabled for files above a specific size
	fileTooBig = content:len() > 102400

	if
		not content:match('%-%-%-') and
		not content:match(options.colon and '%w+:' or '@%w+')
	then
		error(i18n:msg('no-markup', filepath))
	end


	-- Remove leading escapes.
	content = content:gsub('^%-%-+%s*<[^>]+>\n', '')

	-- Remove closing pretty comments.
	content = content:gsub('\n%-%-%-%-%-+(\n[^-]+)', '\n-- %1')

	-- Remove boilerplate block comments.
	if options.boilerplate then
		content = content:gsub('^%-%-%[=*%[\n.-\n%-?%-?%]%=*]%-?%-?%s+', '')
		content = content:gsub('%s+%-%-%[=*%[\n.-\n%-?%-?%]%=*]%-?%-?$', '')
	end

	-- Configure patterns for colon mode and Unicode character encoding.
	options.unicode = type(content:find('[^%w%c%p%s]+')) == 'number'
	options.iso639_th = type(content:find('\224\184[\129-\155]')) == 'number'
	utils.configure_patterns(options)

	-- Content lexing.
	local lines = lexer(content)
	local tokens = {}
	local dummy_token = {
		data = '',
		posFirst = 1,
		posLast = 1
	}
	local token_closure = 0
	for _, line in ipairs(lines) do
		if #line == 0 then
			dummy_token.type = token_closure == 0
				and 'whitespace'
				or  tokens[#tokens].type
			table.insert(tokens, mw.clone(dummy_token))
		else
			for _, token in ipairs(line) do
				 if token.data:find('^%[=*%[$') or token.data:find('^%-%-%[=*%[$') then
					token_closure = 1
				end
				if token.data:find(']=*]') then
					token_closure = 0
				end
				table.insert(tokens, token)
			end
		end
	end

	-- Start documentation data.
	local documentation = {}
	documentation.filename = filepath
	documentation.description = ''
	documentation.code = content
	documentation.comments = {}
	documentation.tags = {}
	documentation.items = {}
	local line_no = 0
	local item_index = 0

	-- Taglet tracking variables.
	local start_mode = true
	local comment_mode = false
	local doctag_mode = false

	local export_mode = false
	local special_tag = false
	local pragma_mode = false
	local factory_mode = false
	local return_mode = false

	local comment_tail = ''
	local tag_name = ''

	local new_item = false
	local new_tag = false
	local new_tag_item = false
	local new_item_code = false
	local pretty_comment = false
	local comment_brace = false

	local t, i, item = tokens[1], 1

	local p_success, p_error = pcall(function()

	while t do
		-- Taglet variable update.
		new_item = t.data:find('^%-%-%-') or t.data:find('^%-%-%[%=*%[$')
		comment_tail = t.data:gsub('^%-%-+', '')

		tag_name = comment_tail:match(patterns.DOCBUNTO_TAG)
		tag_name = p.tags._alias[tag_name] or tag_name

		new_tag = type(p.tags[tag_name]) == 'string'
		local last_token = t.posFirst ~= 1 and 2 or 1
		new_tag_item = (
			new_tag and i > 1 and tokens[i - last_token].type ~= 'comment'
			and not tokens[i - last_token].data:find(patterns.DOCBUNTO_TAG_COMMENT)
		)
		pretty_comment =
			t.data:find('^%-%-%-+%s*$')             or
			t.data:find('[^-]+%-%-%-+%s*$')         or
			t.data:find('%-*[ \t]*<nowiki>$')       or
			t.data:find('%-*[ \t]*<pre>$')
		comment_brace =
			t.data:find('^%-%-%[=*%[$')             or
			t.data:find('^%-%-%]=*%]$')             or
			t.data:find('^%]=*%]%-%-$')
		pragma_mode = tag_name == 'pragma'
		export_mode = tag_name == 'export' and not doctag_mode
		special_tag = pragma_mode or export_mode
		local tags, tag, subtokens, separator

		-- Line counter.
		if t.posFirst == 1 then
			line_no = line_no + 1
		end

		-- Data insertion logic.
		if t.type == 'comment' then
			if new_item then
				comment_mode = true
			end
			if (new_tag or new_tag_item) and not special_tag then
				comment_mode = true
				doctag_mode = true
			end

			-- Module-level documentation taglet.
			if start_mode then
				table.insert(documentation.comments, t.data)
			end

			-- Module description.
			if
				start_mode and comment_mode and
				not new_tag and not doctag_mode and
				not pretty_comment and not comment_brace
			then
				separator = mw.text.trim(comment_tail):find(patterns.DOCBUNTO_WIKITEXT)
					and '\n'
					or  (#documentation.description ~= 0 and patterns.DOCBUNTO_CONCAT or '')
				documentation.description = utils.join{
					documentation.description,
					separator,
					mw.text.trim(comment_tail)
				}
			end

			-- Switch from module description to module-level tag list.
			if start_mode and new_tag and not export_mode then
				doctag_mode = true
				table.insert(documentation.tags, utils.process_tag(comment_tail))

			-- Concatenating module-level tag with more information.
			elseif
				start_mode and doctag_mode and
				not pretty_comment and not comment_brace
			then
				tags = documentation.tags
				if p.tags[tags[#tags].name] == TAG_MULTI then
					separator = mw.text.trim(comment_tail):find(patterns.DOCBUNTO_WIKITEXT)
						and '\n'
						or  patterns.DOCBUNTO_CONCAT
					tags[#tags].value = utils.join{
						tags[#tags].value,
						separator,
						mw.text.trim(comment_tail)
					}
				elseif p.tags[tags[#tags].name] == TAG_MULTI_LINE then
					tags[#tags].value = utils.join{
						tags[#tags].value,
						'\n',
						comment_tail
					}
				end
			end

			-- Detect a new documentation item.
			if not start_mode and (new_item or new_tag_item) and not special_tag then
				table.insert(documentation.items, {})
				item_index = item_index + 1
				item = documentation.items[item_index]
				item.lineno = line_no
				item.code = ''
				item.comments = {}
				item.description = ''
				item.tags = {}
			end

			-- Concatenate an item with more information.
			if
				not start_mode and comment_mode and
				not new_tag and not doctag_mode and
				not pretty_comment and not comment_brace
			then
				separator = (mw.text.trim(comment_tail):find(patterns.DOCBUNTO_WIKITEXT))
					and '\n'
					or  (#item.description ~= 0 and patterns.DOCBUNTO_CONCAT or '')
				item.description = utils.join{
					item.description,
					separator,
					mw.text.trim(comment_tail)
				}
			end

			-- Switch from item information to item tags.
			if not start_mode and new_tag and not special_tag then
				doctag_mode = true
				table.insert(item.tags, utils.process_tag(comment_tail))

			-- Extend item tags with lines of further information.
			elseif
				not start_mode and doctag_mode and
				not pretty_comment and not comment_brace
			then
				tags = item.tags
				if p.tags[tags[#tags].name] == TAG_MULTI then
					separator = mw.text.trim(comment_tail):find(patterns.DOCBUNTO_WIKITEXT)
						and '\n'
						or  patterns.DOCBUNTO_CONCAT
					tags[#tags].value = utils.join{
						tags[#tags].value,
						separator,
						mw.text.trim(comment_tail)
					}
				elseif p.tags[tags[#tags].name] == TAG_MULTI_LINE then
					tags[#tags].value = utils.join{
						tags[#tags].value,
						'\n',
						comment_tail
					}
				end
			end

			-- Save pre-item comments to item information.
			if not start_mode and (comment_mode or doctag_mode) then
				table.insert(item.comments, t.data)
			end

			-- Export tag support.
			if export_mode then
				factory_mode = t.posFirst ~= 1
				if factory_mode then
					item.exports = true
				else
					documentation.exports = true
				end

				-- Parse export token data while concatenating code.
				subtokens = {}
				while t and (not factory_mode or (factory_mode and t.data ~= 'end')) do
					if factory_mode then
						item.code = utils.join{
							item.code,
							(t.posFirst == 1 and '\n' or ''),
							t.data
						}
					end

					-- Fetch our item export tokens aka "subtokens".
					t, i = tokens[i + 1], i + 1
					if t and t.posFirst == 1 then
						line_no = line_no + 1
					end
					if t and t.type ~= 'whitespace' and t.type ~= 'keyword' and t.type ~= 'comment' then
						table.insert(subtokens, t)
					end
				end

				-- Tracking and boolean variables for parsing exports.
				local separator = { [','] = true, [';'] = true }
				local brace = { ['{'] = true, ['}'] = true }

				local item_reference, item_alias = '', ''
				local sequence_index, has_key = 0, false
				local subtoken, index = subtokens[2], 2

				-- Logic that exports items to an item or module.
				while not brace[subtoken.data] do
					if subtoken.data == '=' then
						has_key = true

					elseif not separator[subtoken.data] then
						if has_key then
							item_reference = item_reference .. subtoken.data
						else
							item_alias = item_alias .. subtoken.data
						end
					end

					if subtokens[index + 1] and separator[subtokens[index + 1].data] or brace[subtokens[index + 1].data] then
						if not has_key then
							sequence_index = sequence_index + 1
							item_reference, item_alias = item_alias, item_reference
							item_alias = '[' .. tostring(sequence_index) .. ']'
						end

						utils.export_item(documentation, item_reference, item_index, item_alias, factory_mode)
						item_reference, item_alias, has_key = '', '', false
					end

					subtoken, index = subtokens[index + 1], index + 1
				end

				if not factory_mode then
					break -- end of file
				else
					factory_mode = false -- end of item
				end
			end

			-- Pragma tag support.
			if pragma_mode then
				tag = utils.process_tag(comment_tail)
				if tag.modifiers then
					options[tag.value] = yesno((next((tag ).modifiers or {})), true)
				elseif tag.value:find(' ') then
					local index = tag.value:find(' ')
					local key = tag.value:sub(1, index - 1)
					local val = tag.value:sub(index + 1)
					options[key] = yesno(val)
				elseif options[tag.value] == nil then
					options[tag.value] = true
				end
			end
		end

		-- Package data post-processing.
		if (t.type ~= 'comment' or t.data:find('^%-%-%]=*%]$') or t.data:find('^%]=*%]%-%-$')) and (comment_mode or doctag_mode) and start_mode then
			documentation.tags = utils.hash_map(documentation.tags)
			documentation.name = utils.extract_name(documentation, { project = true })
			documentation.info = utils.extract_info(documentation)
			documentation.types = utils.extract_types(documentation) or { 'module' }
			documentation.type = documentation.types[1]

			-- N.B: summary field = 1st comment sentence; description = text remainder.
			if #documentation.description ~= 0 then
				documentation.summary = match(documentation.description, patterns.DOCBUNTO_SUMMARY)
				documentation.description = documentation.description:sub(select(2, documentation.description:find('%s*', #documentation.summary + 1)) + 1)
			end

			documentation.description = documentation.description:gsub('%s%s+', '\n\n')
			documentation.executable = p.tags._code_types[documentation.type] and true or false
			utils.correct_subitem_tag(documentation)
			utils.override_item_tag(documentation, 'name')
			utils.override_item_tag(documentation, 'alias')
			utils.override_item_tag(documentation, 'summary')
			utils.override_item_tag(documentation, 'description')
			utils.override_item_tag(documentation, 'class', 'type')
		end

		-- Item data post-processing.
		if
			(t.type ~= 'comment' or t.data:find('^%-%-%]=*%]$') or t.data:find('^%]=*%]%-%-$'))
			and (comment_mode or doctag_mode) and item_index ~= 0
		then
			item.tags = utils.hash_map(item.tags)
			item.name = utils.extract_name(item)
			item.types = utils.extract_types(item)
			item.type = item.types[1]

			if #item.description ~= 0 then
				item.summary = match(item.description, patterns.DOCBUNTO_SUMMARY)
				item.description = item.description:sub(select(2, item.description:find('%s*', #item.summary + 1)) + 1)
			end

			item.description = item.description:gsub('%s%s+', '\n\n')
			new_item_code = true
		end

		-- Documentation block reset.
		if t.type ~= 'comment' or t.data:find('^%-%-%]=*%]$') or t.data:find('^%]=*%]%-%-$') then
			start_mode = false
			comment_mode = false
			doctag_mode = false
			export_mode = false
			pragma_mode = false
		end

		-- Don't concatenate module return value into item code.
		if t.data == 'return' and t.posFirst == 1 then
			return_mode = true
		end

		-- Item code concatenation.
		if item_index ~= 0 and not doctag_mode and not comment_mode and not comment_brace and not return_mode then
			separator = #item.code ~= 0 and t.posFirst == 1 and '\n' or ''
			item.code = utils.join{ item.code, separator, t.data }

			-- Code analysis on item headline.
			if new_item_code and item.code:find('\n') and t.posFirst == 1 then
				utils.code_heuristic(item)
				new_item_code = false
			end
		end

		t, i = tokens[i + 1], i + 1
	end

	documentation.lineno = line_no

	local package_name = (documentation.tags['alias'] or {}).value or documentation.name or DEFAULT_VARIABLE
	local package_alias = (documentation.tags['alias'] or {}).value or 'p'
	local export_ptn = '^%s([.[])'

	for _, item in ipairs(documentation.items) do
		if item.name == package_alias or (item.name and item.name:match('^' .. package_alias .. '[.[]')) then
			item.alias = item.name:gsub(export_ptn:format(package_alias), documentation.name .. '%1')
		end
	
		if
			item.name == package_name or
			item.tags['export'] or
			(item.name and package_name and item.name:find(export_ptn:format(package_name))) or
			(item.alias and package_name and item.alias:find(export_ptn:format(package_name)))
		then
			item.export = true

			item.tags['local'] = nil
			item.tags['private'] = nil

			for index, tag in ipairs(item.tags) do
				if tag.name == 'local' or tag.name == 'private' then
					table.remove(item.tags, index)
				end
			end
		end

		if item.name and (item.name:find('[.:]') or item.name:find('%[[\'"]')) then
			item.hierarchy = utils.parse_hierarchy(item.name)
		end

		item.type = item.type or ((item.alias or item.name or ''):find('[.[]') and 'member' or 'variable')

		utils.correct_subitem_tag(item)
		utils.override_item_tag(item, 'name')
		utils.override_item_tag(item, 'alias')
		utils.override_item_tag(item, 'summary')
		utils.override_item_tag(item, 'description')
		utils.override_item_tag(item, 'class', 'type')
	end

	-- Sort documentation items by their access level.
	-- Items are sorted by whether they are exported at the module level.
	-- Items are then subsorted by whether the item is `@local` aka `@private`.
	table.sort(documentation.items, function(item1, item2)
		local inaccessible1 = item1.tags['local'] or item1.tags['private']
		local inaccessible2 = item2.tags['local'] or item2.tags['private']
	
			-- Send package items to the top.
		if item1.export and not item2.export then
			return true
		elseif item2.export and not item1.export then
			return false
	
		-- Send private items to the bottom.
		elseif inaccessible1 and not inaccessible2 then
			return false
		elseif inaccessible2 and not inaccessible1 then
			return true
	
		-- Optional alphabetical sort.
		elseif options.sort then
			return (item1.alias or item1.name) < (item2.alias or item2.name)
	
		-- Sort via source code order by default.
		else
			return item1.lineno < item2.lineno
		end
	end)

	end)

	if not p_success then
		mw.addWarning("Docbunto error: " .. p_error)
	end

	return documentation
end

--- Doclet renderer for Docbunto taglet data.
--  @function           p.doclet
--  @param              {table} data Taglet documentation data.
--  @param[opt]         {options} options Configuration options.
--  @return             {string} Wikitext documentation output.
function p.doclet(data, options)
	local documentation = mw.html.create()
	local namespace_ptn = utils.join{ '^',  mw.site.namespaces[828].name, ':' }
	local codepage = data.filename:gsub(namespace_ptn, '', 1)

	options = options or {}
	options.file = data.filename
	frame = frame or mw.getCurrentFrame():getParent() or mw.getCurrentFrame()

	local maybe_md = options.plain and utils.identity or frontend.markdown
	local tohtml = function(str)
		return frame:preprocess(maybe_md(str))
	end

	-- Disable edit sections for automatic documentation pages.
	if not options.code then
		documentation:wikitext(frame:preprocess('__NOEDITSECTION__'))
	end

		
	-- Information
	if not options.code and options.box then
		local custom, infobox = pcall(require, 'Module:Docbunto/infobox')
		if custom and type(infobox) == 'function' then
			documentation:wikitext(infobox(data, codepage, frame, options, title, maybe_md)):newline()
		end
	end

	-- Documentation lede.
	if not options.code and (#(data.summary or '') + #data.description) ~= 0 then
		local separator = #(data.summary or '') ~= 0 and #data.description ~= 0
			and (data.description:find(patterns.DOCBUNTO_WIKITEXT) and '\n\n' or patterns.DOCBUNTO_CONCAT)
			or  ''
		local intro = utils.join{ data.summary or '', separator, data.description }
		intro = tohtml(intro:gsub('^(' .. codepage .. ')', mw.text.tag('b', nil, '%1')))
		if options.migration then
			documentation:wikitext(frame:preprocess(mw.text.tag{
				name = 'syntaxhighlight',
				content = intro
			})):newline():newline()
		else
			documentation:wikitext(intro):newline():newline()
		end
	end

	-- Custom documentation preface.
	if options.preface then
		documentation:wikitext(options.preface):newline():newline()
	end

	-- Start code documentation.
	local codedoc = mw.html.create()
	local function_module = data.tags['param'] or data.tags['return']
	local header_type =
		documentation.type == 'classmod'
			and 'class'
		or  function_module
			and 'function'
			or  'items'
	if (function_module or #data.items ~= 0) and not options.code or options.preface then
		codedoc
			:tag('h2')
				:wikitext(i18n:msg('header-documentation'))
				:done()
			:newline()
	end
	if (function_module or #data.items ~= 0) then
		codedoc
			:tag('h3')
				:wikitext(i18n:msg('header-' .. header_type))
				:done()
			:newline()
	end

	-- Function module support.
	if function_module then
		data.type, data.types[1] = 'function', 'function'

		options.item_id = data.name
		if not options.code or not options.verbose then data.description = '' end
		frontend.render_item(codedoc, data, options, frontend.pretask_function_name)

		if not options.simple and data.tags['param'] then
			frontend.render_tag(codedoc, 'param', data.tags['param'], options, frontend.pretask_variable_prefix)
		end
		if not options.simple and data.tags['error'] then
			frontend.render_tag(codedoc, 'error', data.tags['error'], options, frontend.pretask_error_line)
		end
		if not options.simple and data.tags['return'] then
			frontend.render_tag(codedoc, 'return', data.tags['return'], options)
		end
	end

	-- Render documentation items.
	local other_header = false
	local private_header = false
	local inaccessible
	for _, item in ipairs(data.items) do
		inaccessible = item.tags['local'] or item.tags['private']
		if not options.all and inaccessible then
			break
		end

		if
			not other_header and item.type ~= 'section' and item.type ~= 'type' and
			not item.export and not item.hierarchy and not inaccessible
		then
			codedoc
				:tag('h3')
					:wikitext(i18n:msg('header-other'))
					:done()
				:newline()
			other_header = true
		end
		if not private_header and options.all and inaccessible then
			codedoc
				:tag('h3')
					:wikitext(i18n:msg('header-private'))
					:done()
				:newline()
			private_header = true
		end

		if item.type == 'section' then
			codedoc
				:tag('h3')
					:wikitext((mw.ustring.gsub(item.summary or item.alias or item.name, '[.։。।෴۔።]$', '')))
					:done()
				:newline()
			if #item.description ~= 0 then
				codedoc:wikitext(mw.text.trim(item.description)):newline()
			end

		elseif item.type == 'type' then
			if options.strip and item.export and item.hierarchy then
				if item.alias then
					item.alias = item.alias:gsub('^[%w_]+[.[]?', '')
				else
					item.name = item.name:gsub('^[%w_]+[.[]?', '')
				end
			end
			codedoc
				:tag('h3')
					:wikitext(mw.text.tag('code', {}, item.alias or item.name))
					:done()
				:newline()
			if (#(item.summary or '') + #item.description) ~= 0 then
				local separator = #(item.summary or '') ~= 0 and #item.description ~= 0
					and (item.description:find(patterns.DOCBUNTO_WIKITEXT) and '\n\n' or patterns.DOCBUNTO_CONCAT)
					or  ''
				codedoc:wikitext((item.summary or '') .. separator .. item.description):newline()
			end

		elseif item.type == 'function' then
			options.item_id = item.alias or item.name
			frontend.render_item(codedoc, item, options, frontend.pretask_function_name)
			if not options.simple and item.tags['param'] then
				frontend.render_tag(codedoc, 'param', item.tags['param'], options, frontend.pretask_variable_prefix)
			end
			if not options.simple and item.tags['error'] then
				frontend.render_tag(codedoc, 'error', item.tags['error'], options, frontend.pretask_error_line)
			end
			if not options.simple and item.tags['return'] then
				frontend.render_tag(codedoc, 'return', item.tags['return'], options)
			end

		elseif
			item.type == 'table' or
			item.type ~= nil and (
				item.type == 'member' or
				item.type == 'variable'
			) and (item.alias or item.name)
		then
			options.item_id = item.alias or item.name
			frontend.render_item(codedoc, item, options)
			if not options.simple and item.tags['field'] then
				frontend.render_tag(codedoc, 'field', item.tags['field'], options, frontend.pretask_variable_prefix)
			end
		end

		if item.type ~= 'section' and item.type ~= 'type' then
			if not options.simple and item.tags['note'] then
				frontend.render_tag(codedoc, 'note', item.tags['note'], options)
			end
			if not options.simple and item.tags['warning'] then
				frontend.render_tag(codedoc, 'warning', item.tags['warning'], options)
			end
			if not options.simple and item.tags['fixme'] then
				frontend.render_tag(codedoc, 'fixme', item.tags['fixme'], options)
			end
			if not options.simple and item.tags['todo'] then
				frontend.render_tag(codedoc, 'todo', item.tags['todo'], options)
				hasTodoItems = true
			end
			if not options.simple and item.tags['usage'] then
				frontend.render_tag(codedoc, 'usage', item.tags['usage'], options, frontend.pretask_usage_highlight)
			end
			if not options.simple and item.tags['see'] then
				frontend.render_tag(codedoc, 'see', item.tags['see'], options)
			end
		end
	end

	-- Render module-level annotations.
	local header_level = options.code and 'h3' or 'h2'
	local header_text
	for _, tag_name in ipairs(p.tags._annotation_hierarchy) do
		if data.tags[tag_name] then
			header_text =  i18n:msg('tag-' .. tag_name, data.tags[tag_name].value and '1' or '2')
			header_text = '<' .. header_level .. '>' .. ' ' .. header_text .. ' ' .. '</' .. header_level .. '>'
			codedoc:newline():wikitext(header_text):newline()
			if data.tags[tag_name].value then
				codedoc:wikitext(data.tags[tag_name].value):newline()
			else
				for _, tag_el in ipairs(data.tags[tag_name]) do
					codedoc:wikitext('* ' .. tag_el.value):newline()
				end
			end
		end
	end

	-- Byline for autogeneration.
	if options.footer then
		codedoc:newline()
		codedoc:tag('hr'):done()
		codedoc:newline()
		codedoc:tag('p'):css( 'text-align', 'right' ):wikitext(i18n:msg('message-autogeneration'))
	end

	-- Add nowiki tags for EOF termination in tests.
	codedoc:tag('nowiki', { selfClosing = true })

	-- Code documentation formatting.
	codedoc = tohtml(tostring(codedoc))

	documentation:wikitext(codedoc)

	-- Endmatter table for module information.
	if options.card then
		local endmatter = mw.html.create('table')
		endmatter:addClass('wikitable')
		endmatter:css('width', '75%')
		endmatter:css('margin', '0 auto 1em auto')
		
		local module_type = data.type == 'module'
			and i18n:msg('invocable-module')
			or i18n:msg('meta-module')

		endmatter
			:tag('caption')
			:wikitext('Module information card (autogenerated)')
	
		endmatter
			:tag('tr')
				:tag('th'):attr('scope', 'col'):wikitext('Name'):done()
				:tag('th'):attr('scope', 'col'):wikitext('Value'):done()
	
		if options.image or data.info and data.info['image'] then
			endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('endmatter-image')):done()
					:tag('td')
						:attr('align', 'center')
						:wikitext('[[File:' .. (options.image or data.info['image']) .. '|270px|center]]')
					:done()
				:done()
		end
	
		if options.caption or data.info and data.info['caption'] then
			 endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('endmatter-caption')):done()
					:tag('td')
						:attr('align', options.image or data.info['image'] and 'center' or 'left')
						:wikitext(tohtml(options.caption or data.info['caption']))
					:done()
				:done()
		end
	
		endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext((gsub(i18n:msg('type-variable'), '^%w', mw.ustring.upper))):done()
					:tag('td')
						:attr('align', options.image or data.info and data.info['image'] and 'center' or 'left')
						:wikitext(mw.text.tag({
							name = options.image or data.info and data.info['image'] and 'h1' or 'span',
							content = mw.text.tag({
								name = 'b',
								content = mw.text.tag({
									name = 'code', 
									content = data.name or DEFAULT_VARIABLE
								})
							})
						})):done()
					:done()
				:tag('tr')
					:tag('td'):wikitext(i18n:msg('code')):done()
					:tag('td'):wikitext(tohtml('[[' .. data.filename .. ']]')):done()
				:done()
	
		if data.info and data.info['release'] then
			endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('release-status')):done()
					:tag('td'):wikitext(tohtml(data.info['release'])):done()
				:done()
		end
	
		if data.summary then
			endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('description')):done()
					:tag('td'):wikitext(tohtml(data.summary)):done()
				:done()
		end
	
		if data.info and data.info['author'] then
			endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('author')):done()
					:tag('td'):newline():wikitext(tohtml(data.info['author'])):done()
				:done()
		end
	
		if data.info and data.info['attribution'] then
			endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('using-code-by')):done()
					:tag('td'):wikitext(tohtml(data.info['attribution'])):done()
				:done()
		end
	
		if data.info and data.info['credit'] then
			endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('other-attribution')):done()
					:tag('td'):wikitext(tohtml(data.info['credit'])):done()
				:done()
		end
	
		if data.info and data.info['require'] then
			data.info['require'] = data.info['require']
				:gsub('^[^[%s]+$', '[[%1]]')
				:gsub('%* ([^[%s]+)', '* [[%1]]')
			endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('dependencies')):done()
					:tag('td'):newline():wikitext(tohtml(data.info['require'])):done()
				:done()
		end
	
		if data.info and data.info['demo'] then
			endmatter
				:tag('tr')
					:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('examples')):done()
					:tag('td'):wikitext(tohtml(data.info['demo'])):done()
				:done()
		end

		endmatter:newline()
		endmatter = tostring(endmatter)
		documentation:newline():newline()
		documentation:wikitext(endmatter)
	end

	documentation = tostring(documentation)
	if hasTodoItems and options.autodoc 
		and mw.title.new('Module:' .. options.file).fullText == mw.title.getCurrentTitle().fullText then
		documentation = documentation .. '[[Category:' .. i18n:msg('todo') .. ']]'
	end
	if mw.title.new('Module:Docbunto/styles.css').exists then
		documentation = documentation .. frame:extensionTag('templatestyles', '', {src = 'Module:Docbunto/styles.css'})
	end
	return documentation
end

--- Token dictionary for Docbunto tags.
--  Maps Docbunto tag names to tag tokens.
--   * Multi-line tags use the `'M'` token.
--   * Multi-line preformatted tags use the `'ML'` token.
--   * Identifier tags use the `'ID'` token.
--   * Single-line tags use the `'S'` token.
--   * Flags use the `'N'` token.
--   * Type tags use the `'T'` token.
--  @table              p.tags
p.tags = {
	-- Item-level tags, available for global use.
	['param'] = 'M', ['see'] = 'M', ['note'] = 'M', ['usage'] = 'ML',
	['description'] = 'M', ['field'] = 'M', ['return'] = 'M',
	['fixme'] = 'M', ['todo'] = 'M', ['warning'] = 'M', ['error'] = 'M';
	['class'] = 'ID', ['name'] = 'ID', ['alias'] = 'ID';
	['summary'] = 'S', ['pragma'] = 'S', ['factory'] = 'S',
	['release'] = 'S', ['author'] = 'S', ['copyright'] = 'S', ['license'] = 'S',
	['image'] = 'S', ['caption'] = 'S', ['require'] = 'S', ['attribution'] = 'S',
	['credit'] = 'S', ['demo'] = 'S';
	['local'] = 'N', ['export'] = 'N', ['private'] = 'N', ['constructor'] = 'N',
	['static'] = 'N';
	-- Project-level tags, all scoped to a file.
	['module'] = 'T', ['script'] = 'T', ['classmod'] = 'T', ['topic'] = 'T',
	['submodule'] = 'T', ['example'] = 'T', ['file'] = 'T';
	-- Module-level tags, used to register module items.
	['function'] = 'T', ['table'] = 'T', ['member'] = 'T', ['variable'] = 'T',
	['section'] = 'T', ['type'] = 'T';
}
p.tags._alias = {
	-- Normal aliases.
	['about']       = 'summary',
	['abstract']    = 'summary',
	['brief']       = 'summary',
	['bug']         = 'fixme',
	['argument']    = 'param',
	['credits']     = 'credit',
	['code']        = 'usage',
	['details']     = 'description',
	['discussion']  = 'description',
	['exception']   = 'error',
	['lfunction']   = 'function',
	['package']     = 'module',
	['property']    = 'member',
	['raise']       = 'error',
	['requires']    = 'require',
	['returns']     = 'return',
	['throws']      = 'error',
	['typedef']     = 'type',
	-- Typed aliases.
	['bool']        = 'field',
	['func']        = 'field',
	['int']         = 'field',
	['number']      = 'field',
	['string']      = 'field',
	['tab']         = 'field',
	['vararg']      = 'param',
	['tfield']      = 'field',
	['tparam']      = 'param',
	['treturn']     = 'return'
}
p.tags._type_alias = {
	-- Implicit type value alias.
	['bool']        = 'boolean',
	['func']        = 'function',
	['int']         = 'number',
	['number']      = 'number',
	['string']      = 'string',
	['tab']         = 'table',
	['vararg']      = '...',
	-- Pure typed modifier alias.
	['tfield']      = 'variable',
	['tparam']      = 'variable',
	['treturn']     = 'variable'
}
p.tags._project_level = {
	-- Contains code.
	['module']      = true,
	['script']      = true,
	['classmod']    = true,
	['submodule']   = true,
	['file']        = true,
	-- Contains documentation.
	['topic']       = true,
	['example']     = true
}
p.tags._code_types = {
	['module']      = true,
	['script']      = true,
	['classmod']    = true
}
p.tags._module_info = {
	['image']       = true,
	['caption']     = true,
	['release']     = true,
	['author']      = true,
	['copyright']   = true,
	['license']     = true,
	['require']     = true,
	['credit']      = true,
	['attribution'] = true,
	['demo']        = true
}
p.tags._annotation_tags = {
	['field']       = true,
	['warning']     = true,
	['fixme']       = true,
	['note']        = true,
	['todo']        = true,
	['see']         = true
}
p.tags._annotation_hierarchy = {
	'warning',
	'fixme',
	'note',
	'todo',
	'see'
}
p.tags._privacy_tags = {
	['private']     = true,
	['local']       = true
}
p.tags._generic_tags = {
	['variable']    = true,
	['member']      = true
}
p.tags._subtype_tags = {
	['factory']     = true,
	['local']       = true,
	['private']     = true,
	['constructor'] = true,
	['static']      = true
}
p.tags._subtype_hierarchy = {
	'private',
	'local',
	'static',
	'factory',
	'constructor'
}

return p