Module:Documentation

From Coral Island Wiki
Revision as of 03:38, 4 August 2023 by Admin coral island (talk | contribs) (Created page with "-- <pre> -------------------------------------------------------------------------------- -- This module implements {{T|Documentation}}. -- -- @module documentation -- @alias p -- @release stable -- @require Dev:Arguments -- @require Dev:Config -- @require Dev:I18n -- @require Dev:Languages -- @require [[Global Lua Modules/Message box|Dev:Message bo...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Documentation for this module may be created at Module:Documentation/doc

-- <pre>
--------------------------------------------------------------------------------
-- This module implements {{T|Documentation}}.
--
-- @module documentation
-- @alias p
-- @release stable
-- @require [[Global Lua Modules/Arguments|Dev:Arguments]]
-- @require [[Global Lua Modules/Config|Dev:Config]]
-- @require [[Global Lua Modules/I18n|Dev:I18n]]
-- @require [[Global Lua Modules/Languages|Dev:Languages]]
-- @require [[Global Lua Modules/Message box|Dev:Message box]]
-- @require [[Global Lua Modules/Yesno|Dev:Yesno]]
-- @require Module:Documentation/config
-- @require Module:Documentation/i18n
-- @require [[MediaWiki:Module:Documentation.css]]
-- @author [[User:FishTank]]
-- @author [[User:ExE Boss]]
-- @attribution [[wikipedia:Module:Documentation|Module:Documentation]] (Wikipedia)
-- @see [[wikipedia:Module:Documentation|Original module on Wikipedia]]
-- @see {{T|Documentation}}
--------------------------------------------------------------------------------

local libraryUtil = require('libraryUtil');
local checkType = libraryUtil.checkType;

-- Get required modules.
local getArgs = require('Dev:Arguments').getArgs
local pageExists = require('Dev:Linkless Exists').pageExists
local messageBox = require('Dev:Message box')
local yesno = require('Dev:Yesno')

-- Get the config table.
local cfg = require('Dev:Config').loadConfig('Documentation')
local i18n = require('Dev:I18n').loadMessages('Documentation', 'Common')
local languages = require('Dev:Languages')

local p = {}
p.i18n = i18n

-- Often-used functions.
local ugsub = mw.ustring.gsub

local IS_DEV_WIKI = mw.site.server == 'https://dev.fandom.com';

-- --------------------------------------------------------------------------
-- Helper functions
--
-- These are defined as local functions, but are made available in the p
-- table for testing purposes.
-- --------------------------------------------------------------------------

--[[
-- Gets a message from the cfg table and formats it if appropriate.
-- The function raises an error if the value from the cfg table is not
-- of the type expectType. The default type for expectType is 'string'.
-- If the table valArray is present, strings such as $1, $2 etc. in the
-- message are substituted with values from the table keys [1], [2] etc.
-- For example, if the message "foo-message" had the value 'Foo $2 bar $1.',
-- message('foo-message', {'baz', 'qux'}) would return "Foo qux bar baz."
--
-- @function p.message
-- @private
-- @param {string} cfgKey
-- @param[opt] {table} valArray
-- @param[opt] {string} expectType
-- @return {string|number|boolean|table|nil}
--]]
local function message(cfgKey, valArray, expectType)
	local msg = cfg:getValue(cfgKey)
	expectType = expectType or 'string'
	if type(msg) ~= expectType then
		error('message: type error in message cfg.' .. cfgKey .. ' (' .. expectType .. ' expected, got ' .. type(msg) .. ')', 2)
	end
	if not valArray then
		return msg
	end

	local function getMessageVal(match)
		match = tonumber(match)
		return valArray[match] or error('message: no value found for key $' .. match .. ' in message cfg.' .. cfgKey, 4)
	end

	local ret = ugsub(msg, '$([1-9][0-9]*)', getMessageVal)
	return ret
end

p.message = message

local function makeWikilink(page, display)
	if display then
		return mw.ustring.format('[[%s|%s]]', page, display)
	else
		return mw.ustring.format('[[%s]]', page)
	end
end

p.makeWikilink = makeWikilink

local function makeUrlWikilink(page, display)
	-- This code prevents redlinks, but doesn't work for categories, and those
	-- are always blue-linked by the CategoryBlueLinks extension anyway.
	return mw.ustring.format('[%s %s]', tostring(mw.uri.fullUrl(page)), display or page)
end

p.makeUrlWikilink = makeUrlWikilink

local function makeCategoryLink(cat, sort)
	local catns = mw.site.namespaces[14].name
	return makeWikilink(catns .. ':' .. cat, sort)
end

p.makeCategoryLink = makeCategoryLink

local function makeUrlLink(url, display)
	return mw.ustring.format('[%s %s]', url, display)
end

p.makeUrlLink = makeUrlLink

local function makeToolbar(...)
	local lim = select('#', ...)
	if lim < 1 then
		return nil
	end

	local frame = mw.getCurrentFrame()
	local ret = mw.html.create('small'):wikitext('(')

	local isFirst = true
	for i = 1, lim do
		local v = select(i, ...)
		if v then
			if isFirst then
				isFirst = false
			else
				ret:wikitext(' ', frame:preprocess('{{int:pipe-separator}}'), '&nbsp;')
			end
			ret:wikitext(v)
		end
	end

	return not isFirst and ret:wikitext(')'):done() or nil
end

p.makeToolbar = makeToolbar

--[[
-- @function p.resolveNamespace
-- @private
-- @param {int} subjectSpace
-- @return {string}
--]]
local function resolveNamespace(subjectSpace)
	if subjectSpace == 10 then -- Template namespace
		return 'template'
	elseif subjectSpace == 828 then -- Module namespace
		return 'module'
	elseif subjectSpace == 6 then -- File namespace
		return 'file'
	end
	return 'other'
end

p.resolveNamespace = resolveNamespace;

-- --------------------------------------------------------------------------
-- Argument processing
-- --------------------------------------------------------------------------

local function makeInvokeFunc(funcName)
	return function (frame)
		local args = getArgs(frame, {
			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 tostring(p[funcName](args))
	end
end

-- --------------------------------------------------------------------------
-- Main function
-- --------------------------------------------------------------------------

--[[
-- This function defines logic flow for the module.
--
-- ; Messages:
-- : 'main-div-id' --> 'template-documentation'
-- : 'main-div-classes' --> 'template-documentation iezoomfix'
--
-- @function p.main
-- @param {table|Frame} args - table of arguments passed by the user
-- @return {string}
--]]
p.main = makeInvokeFunc('_main')

function p._main(args)
	local env = p.getEnvironment(args)
	local root = mw.html.create()
	root
		:wikitext(p.protectionTemplate(env))
		:wikitext(p.sandboxNotice(args, env))
		:tag('div')
			:attr('id', message('main-div-id'))
			:addClass(message('main-div-classes'))
			:newline()
			:node(p._startBox(args, env))
			:node(p._content(args, env))
			:node(p._endBox(args, env))
		:done()
		:wikitext(p.addTrackingCategories(env))
	return root
end

-- --------------------------------------------------------------------------
-- Environment settings
-- --------------------------------------------------------------------------

--- A table with information about the environment, including title objects and
-- other namespace- or path-related data.
-- @type Environment
--
-- @note All table lookups are passed through pcall so that errors are caught.
-- If an error occurs, the value returned will be nil.
--
-- ; Title objects include:
--- @property {Title|nil} Environment.title - the page we are making documentation for (usually the current title)
--- @property {Title|nil} Environment.templateTitle - the template (or module, file, etc.)
--- @property {Title|nil} Environment.docTitle - the /doc subpage.
--- @property {Title|nil} Environment.docTitleEn - the /doc/en subpage.
--- @property {Title|nil} Environment.sandboxTitle - the /sandbox subpage.
--- @property {Title|nil} Environment.testcasesTitle - the /testcases subpage.
--- @property {Title|nil} Environment.printTitle - the print version of the template, located at the /Print subpage.
--
-- ; Data includes:
--- @property {Title.protectionLevels|nil} Environment.protectionLevels - the protection levels table of the title object.
--- @property {int|nil} Environment.subjectSpace - the number of the title's subject namespace.
--- @property {int|nil} Environment.docSpace - the number of the namespace the title puts its documentation in.
--- @property {string|nil} Environment.docpageBase - the text of the base page of the /doc, /sandbox and /testcases pages, with namespace.
--- @property {string|nil} Environment.compareUrl - URL of the Special:ComparePages page comparing the sandbox with the template.

--[[
-- Returns a table with information about the environment, including title objects and other namespace- or
-- path-related data.
--
-- @function p.getEnvironment
-- @private
-- @param {table} args - table of arguments passed by the user
-- @return {Environment}
--]]
function p.getEnvironment(args)
	local env, envFuncs = {}, {}

	-- Set up the metatable. If triggered we call the corresponding function in the envFuncs table. The value
	-- returned by that function is memoized in the env table so that we don't call any of the functions
	-- more than once. (Nils won't be memoized.)
	setmetatable(env, {
		__index = function (t, key)
			local envFunc = envFuncs[key]
			if envFunc then
				local success, val = pcall(envFunc)
				if success then
					env[key] = val -- Memoise the value.
					return val
				end
			end
			return nil
		end
	})

	-- The title object for the current page, or a test page passed with args.page.
	function envFuncs.title()
		local title
		local titleArg = args.page
		if titleArg then
			title = mw.title.new(titleArg)
		else
			title = mw.title.getCurrentTitle()
		end
		return title
	end

	-- The template (or module, etc.) title object.
	-- ; Messages:
	-- : 'sandbox-subpage' --> 'sandbox'
	-- : 'testcases-subpage' --> 'testcases'
	function envFuncs.templateTitle()
		local subjectSpace = env.subjectSpace
		local title = env.title
		local subpage = title.subpageText
		if subpage == message('sandbox-subpage') or subpage == message('testcases-subpage') then
			return mw.title.makeTitle(subjectSpace, title.baseText)
		else
			return mw.title.makeTitle(subjectSpace, title.text)
		end
	end

	-- Title object of the /doc subpage.
	-- ; Messages:
	-- : 'doc-subpage' --> 'doc'
	function envFuncs.docTitle()
		local docname = args[1] -- User-specified doc page.
		local docpage
		if docname then
			docpage = docname
		else
			docpage = env.docpageBase .. '/' .. message('doc-subpage')
		end
		return mw.title.new(docpage)
	end

	-- Title object of the /doc/en subpage.
	function envFuncs.docTitleEn()
		local docTitle = env.docTitle
		return mw.title.new(docTitle.text .. '/en', docTitle.nsText)
	end

	function envFuncs.subpages()
		local docTitle = env.docTitle
		return languages.subpages(docTitle.text, docTitle.nsText)
	end

	function envFuncs.hasSubpages()
		local subpages = env.subpages
		return #subpages > 1 or subpages[1] ~= ''
	end

	function envFuncs.docTitleCreate()
		local hasSubpages = env.hasSubpages
		local docTitle = env.docTitle
		local docTitleEn = env.docTitleEn
		local forceI18n = yesno(args.i18n)
		if (forceI18n or hasSubpages) and (pageExists(docTitleEn.prefixedText) or not pageExists(docTitle.prefixedText)) then
			return docTitleEn
		end
		return docTitle
	end

	function envFuncs.docTitleCurrentLang()
		local currentLang = i18n:getLang()
		if currentLang == 'en' or yesno(args.ignoreCurrentLang) then
			return env.docTitleCreate
		else
			local docTitle = env.docTitle
			return mw.title.new(docTitle.text .. '/' .. currentLang, docTitle.nsText)
		end
	end

	-- Title object for the /sandbox subpage.
	-- ; Messages:
	-- : 'sandbox-subpage' --> 'sandbox'
	function envFuncs.sandboxTitle()
	    -- return nil
		return mw.title.new(env.docpageBase .. '/' .. message('sandbox-subpage'))
	end

	-- Title object for the /testcases subpage.
	-- ; Messages:
	-- : 'testcases-subpage' --> 'testcases'
	function envFuncs.testcasesTitle()
	    -- return nil
		return mw.title.new(env.docpageBase .. '/' .. message('testcases-subpage'))
	end

	-- Title object for the /Print subpage.
	-- ; Messages:
	-- : 'print-subpage' --> 'Print'
	function envFuncs.printTitle()
		if message('print-show') then
			return env.templateTitle:subPageTitle(message('print-subpage'))
		end
	end

	-- The protection levels table of the title object.
	function envFuncs.protectionLevels()
		return env.title.protectionLevels
	end

	-- The subject namespace number.
	function envFuncs.subjectSpace()
		return mw.site.namespaces[env.title.namespace].subject.id
	end

	-- The documentation namespace number. For most namespaces this is the same as the
	-- subject namespace. However, pages in the Article, File, MediaWiki or Category
	-- namespaces must have their /doc, /sandbox and /testcases pages in talk space.
	function envFuncs.docSpace()
		local subjectSpace = env.subjectSpace
		if subjectSpace == 0 or subjectSpace == 6 or subjectSpace == 8 or subjectSpace == 14 then
			return subjectSpace + 1
		else
			return subjectSpace
		end
	end

	-- The base page of the /doc, /sandbox, and /testcases subpages.
	-- For some namespaces this is the talk page, rather than the template page.
	function envFuncs.docpageBase()
		local templateTitle = env.templateTitle
		local docSpace = env.docSpace
		local docSpaceText = mw.site.namespaces[docSpace].name
		-- Assemble the link. docSpace is never the main namespace, so we can hardcode the colon.
		return docSpaceText .. ':' .. templateTitle.text
	end

	-- Diff link between the sandbox and the main template using [[Special:ComparePages]].
	function envFuncs.compareUrl()
		local templateTitle = env.templateTitle
		local sandboxTitle = env.sandboxTitle
		if pageExists(templateTitle.prefixedText) and pageExists(sandboxTitle.prefixedText) then
			local compareUrl = mw.uri.fullUrl(
				'Special:ComparePages',
				{page1 = templateTitle.prefixedText, page2 = sandboxTitle.prefixedText}
			)
			return tostring(compareUrl)
		else
			return nil
		end
	end

	function envFuncs.docStatus()
		local docTitle = env.docTitle
		if not args.content then
			if not pageExists(docTitle.prefixedText) and not env.hasSubpages then
				return 'nodoc'
			elseif not pageExists(docTitle.prefixedText) and not pageExists(env.docTitleEn.prefixedText) and env.hasSubpages then
				return 'baddoc'
			end
		end
	end

	function envFuncs.docIcon()
		local docStatus = env.docStatus
		if not docStatus then
			return message('documentation-icon')
		end
		return message('documentation-icon-' .. docStatus)
	end

	return env
end

-- --------------------------------------------------------------------------
-- Auxiliary templates
-- --------------------------------------------------------------------------

---
-- Generates a sandbox notice for display above sandbox pages.
--
-- ; Messages:
-- : 'sandbox-notice-image' --> '[[Image:Sandbox.svg|50px|alt=|link=]]'
-- : 'sandbox-notice-blurb' --> 'This is the $1 for $2.'
-- : 'sandbox-notice-diff-blurb' --> 'This is the $1 for $2 ($3).'
-- : 'sandbox-notice-pagetype-template' --> '[[Wikipedia:Template test cases|template sandbox]] page'
-- : 'sandbox-notice-pagetype-module' --> '[[Wikipedia:Template test cases|module sandbox]] page'
-- : 'sandbox-notice-pagetype-other' --> 'sandbox page'
-- : 'sandbox-notice-compare-link-display' --> 'diff'
-- : 'sandbox-notice-testcases-blurb' --> 'See also the companion subpage for $1.'
-- : 'sandbox-notice-testcases-link-display' --> 'test cases'
-- : 'sandbox-category' --> 'Template sandboxes'
--
-- @function p.sandboxNotice
-- @private
-- @param {table} args - a table of arguments passed by the user
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string|nil}
function p.sandboxNotice(args, env)
	if not message('sandbox-notice-show', nil, 'boolean') then
		return nil
	end

	local title = env.title
	local sandboxTitle = env.sandboxTitle
	local templateTitle = env.templateTitle
	local subjectSpace = env.subjectSpace
	if not (subjectSpace and title and sandboxTitle and templateTitle and mw.title.equals(title, sandboxTitle)) then
		return nil
	end
	-- Build the table of arguments to pass to {{T|ombox}}. We need just two fields, "image" and "text".
	local omargs = {}
	omargs.image = message('sandbox-notice-image')
	-- Get the text. We start with the opening blurb, which is something like
	-- "This is the template sandbox for [[Template:Foo]] (diff)."
	local text = ''
	local frame = mw.getCurrentFrame()
	local isPreviewing = frame:preprocess('{{REVISIONID}}') == '' -- True if the page is being previewed.
	local pagetype
	if subjectSpace == 10 then
		pagetype = message('sandbox-notice-pagetype-template')
	elseif subjectSpace == 828 then
		pagetype = message('sandbox-notice-pagetype-module')
	else
		pagetype = message('sandbox-notice-pagetype-other')
	end
	local templateLink = makeUrlWikilink(templateTitle.prefixedText)
	local compareUrl = env.compareUrl
	if isPreviewing or not compareUrl then
		text = text .. message('sandbox-notice-blurb', {pagetype, templateLink})
	else
		local compareDisplay = message('sandbox-notice-compare-link-display')
		local compareLink = makeUrlLink(compareUrl, compareDisplay)
		text = text .. message('sandbox-notice-diff-blurb', {pagetype, templateLink, compareLink})
	end
	-- Get the test cases page blurb if the page exists. This is something like
	-- "See also the companion subpage for [[Template:Foo/testcases|test cases]]."
	local testcasesTitle = env.testcasesTitle
	if testcasesTitle and pageExists(testcasesTitle.prefixedText) then
		if testcasesTitle.namespace == mw.site.namespaces.Module.id then
			local testcasesLinkDisplay = message('sandbox-notice-testcases-link-display')
			local testcasesRunLinkDisplay = message('sandbox-notice-testcases-run-link-display')
			local testcasesLink = makeUrlWikilink(testcasesTitle.prefixedText, testcasesLinkDisplay)
			local testcasesRunLink = makeUrlWikilink(testcasesTitle.talkPageTitle.prefixedText, testcasesRunLinkDisplay)
			text = text .. '<br />' .. message('sandbox-notice-testcases-run-blurb', {testcasesLink, testcasesRunLink})
		else
			local testcasesLinkDisplay = message('sandbox-notice-testcases-link-display')
			local testcasesLink = makeUrlWikilink(testcasesTitle.prefixedText, testcasesLinkDisplay)
			text = text .. '<br />' .. message('sandbox-notice-testcases-blurb', {testcasesLink})
		end
	end
	-- Add the sandbox to the sandbox category.
	text = text .. makeCategoryLink(message('sandbox-category'))
	omargs.text = text
	local ret = '<div style="clear: both;"></div>'
	ret = ret .. messageBox.main('ombox', omargs)
	return ret
end

--[[
-- Generates the padlock icon in the top right.
--
-- ; Messages:
-- : 'protection-template' --> 'pp-template'
-- : 'protection-template-args' --> {docusage = 'yes'}
--
-- @function p.protectionTemplate
-- @private
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string|nil}
--]]
function p.protectionTemplate(env)
	-- This depends on [[Module:Protection banner]]
	do return end

	local protectionLevels, mProtectionBanner
	local title = env.title
	if title.namespace ~= 10 and title.namespace ~= 828 then
		-- Don't display the protection template if we are not in the template or module namespaces.
		return nil
	end
	protectionLevels = env.protectionLevels
	if not protectionLevels then
		return nil
	end
	local editProt = protectionLevels.edit and protectionLevels.edit[1]
	local moveProt = protectionLevels.move and protectionLevels.move[1]
	if editProt then
		-- The page is edit-protected.
		mProtectionBanner = require('Dev:Protection banner')
		local reason = message('protection-reason-edit')
		return mProtectionBanner._main{reason, small = true}
	elseif moveProt and moveProt ~= 'autoconfirmed' then
		-- The page is move-protected but not edit-protected. Exclude move
		-- protection with the level "autoconfirmed", as this is equivalent to
		-- no move protection at all.
		mProtectionBanner = require('Dev:Protection banner')
		return mProtectionBanner._main{action = 'move', small = true}
	else
		return nil
	end
end

-- --------------------------------------------------------------------------
-- Start box
-- --------------------------------------------------------------------------

--[[
-- This function generates the start box.
--
-- The actual work is done by p.makeStartBoxLinksData and p.renderStartBoxLinks which make
-- the [view] [edit] [history] [purge] links, and by p.makeStartBoxData and p.renderStartBox
-- which generate the box HTML.
--
-- @function p.startBox
-- @param {table|Frame} args - a table of arguments passed by the user
-- @param[opt] {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string|nil}
--]]
p.startBox = makeInvokeFunc('_startBox')

function p._startBox(args, env)
	env = env or p.getEnvironment(args)
	local links
	local content = args.content
	if not content then
		-- No need to include the links if the documentation is on the template page itself.
		local linksData = p.makeStartBoxLinksData(args, env)
		if linksData then
			links = p.renderStartBoxLinks(linksData)
		end
	end
	-- Generate the start box html.
	local data = p.makeStartBoxData(args, env, links)
	if data then
		return p.renderStartBox(data)
	else
		-- User specified no heading.
		return nil
	end
end

--[[
-- Does initial processing of data to make the [view] [edit] [history] [purge] links.
--
-- ; Messages:
-- : 'file-docpage-preload' --> 'Template:Documentation/preload-filespace'
-- : 'module-preload' --> 'Template:Documentation/preload-module-doc'
-- : 'docpage-preload' --> 'Template:Documentation/preload'
--
-- @function p.makeStartBoxLinksData
-- @private
-- @param {table} args - a table of arguments passed by the user
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {table|nil}
--]]
function p.makeStartBoxLinksData(args, env)
	local subjectSpace = env.subjectSpace
	local title = env.title
	local docTitleCurrentLang = env.docTitleCurrentLang
	if not title or not docTitleCurrentLang then
		return nil
	end

	local frame = mw.getCurrentFrame()
	local data = {}
	data.title = title
	data.docTitle = docTitleCurrentLang
	-- View, display, edit, and purge links if /doc exists.
	data.viewLinkDisplay = frame:preprocess('{{lc:{{int:view}}}}')
	data.editLinkDisplay = frame:preprocess('{{lc:{{int:edit}}}}')
	data.historyLinkDisplay = frame:preprocess('{{lc:{{int:history_short}}}}')
	data.purgeLinkDisplay = frame:preprocess('{{lc:{{int:page-header-action-button-purge}}}}')
	-- Create link if /doc doesn't exist.
	local preload = args.preload
	if not preload then
		if subjectSpace == 6 then -- File namespace
			preload = message('file-docpage-preload')
		elseif subjectSpace == 828 then -- Module namespace
			preload = message('module-preload')
		else
			preload = message('docpage-preload')
		end
	end
	data.preload = preload
	data.createLinkDisplay = frame:preprocess('{{lc:{{int:create}}}}')
	return data
end

--[[
-- Generates the [view][edit][history][purge] or [create] links from the data table.
--
-- @function p.renderStartBoxLinks
-- @private
-- @param {table} data - a table of data generated by p.makeStartBoxLinksData
-- @return {string}
--]]
function p.renderStartBoxLinks(data)

	local function escapeBrackets(s)
		-- Escapes square brackets with HTML entities.
		s = s:gsub('%[', '&#91;') -- Replace square brackets with HTML entities.
		s = s:gsub('%]', '&#93;')
		return s
	end

	local ret
	local docTitle = data.docTitle
	local title = data.title
	if pageExists(docTitle.prefixedText) then
		local viewLink = makeWikilink(docTitle.prefixedText, data.viewLinkDisplay)
		local editLink = makeUrlLink(docTitle:fullUrl({ action = 'edit' }, 'https'), data.editLinkDisplay)
		local historyLink = makeUrlLink(docTitle:fullUrl({ action = 'history' }, 'https'), data.historyLinkDisplay)
		local purgeLink = makeUrlLink(title:fullUrl({ action = 'purge' }, 'https'), data.purgeLinkDisplay)
		ret = '[%s] [%s] [%s] [%s]'
		ret = escapeBrackets(ret)
		ret = mw.ustring.format(ret, viewLink, editLink, historyLink, purgeLink)
	else
		local createLink = makeUrlLink(docTitle:fullUrl({
			action = 'edit',
			redlink = '1',
			preload = data.preload,
		}, 'https'), data.createLinkDisplay)
		ret = '[%s]'
		ret = escapeBrackets(ret)
		ret = mw.ustring.format(ret, createLink)
	end
	return ret
end

---
-- Does initial processing of data to pass to the start-box render function, p.renderStartBox.
--
-- ; Messages:
-- : 'documentation-icon-wikitext' --> '[[File:$1|50px|link=|alt=Documentation icon]]'
-- : 'documentation-icon' --> 'Template-info.svg'
-- : 'documentation-icon-nodoc' --> 'Template-noinfo.svg'
-- : 'documentation-icon-baddoc' --> 'Template-badinfo.svg'
-- : 'start-box-linkclasses' --> 'mw-editsection-like plainlinks'
-- : 'start-box-link-id' --> 'doc_editlinks'
--
-- @function p.makeStartBoxData
-- @private
-- @param {table} args - a table of arguments passed by the user
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @param {string|nil} links - a string containing the [view][edit][history][purge] links - could be nil if there's an error.
-- @return {table}
function p.makeStartBoxData(args, env, links)
	local subjectSpace = env.subjectSpace
	if not subjectSpace then
		-- Default to an "other namespaces" namespace, so that we get at least some output
		-- if an error occurs.
		subjectSpace = 2
	end
	local data = {}

	-- Heading
	local heading = args.heading -- Blank values are not removed.
	if heading == '' then
		-- Don't display the start box if the heading arg is defined but blank.
		return nil
	end

	if heading then
		data.heading = heading
	elseif subjectSpace == 10 then -- Template namespace
		data.heading = message('documentation-icon-wikitext', {env.docIcon}) .. ' ' .. i18n:msg('documentation-heading')
		data.subHeading = i18n:msg('documentation-visibility')
	elseif subjectSpace == 828 then -- Module namespace
		data.heading = message('documentation-icon-wikitext', {env.docIcon}) .. ' ' .. i18n:msg('module-namespace-heading')
	elseif subjectSpace == 6 then -- File namespace
		data.heading = i18n:msg('file-namespace-heading')
	else
		data.heading = i18n:msg('other-namespaces-heading')
	end

	-- Heading CSS
	local headingStyle = args['heading-style']
	if headingStyle then
		data.headingStyleText = headingStyle
	elseif subjectSpace == 10 then
		-- We are in the template or template talk namespaces.
		data.headingFontWeight = 'bold'
		data.headingFontSize = '125%'
	else
		data.headingFontSize = '150%'
	end

	-- Data for the [view][edit][history][purge] or [create] links.
	if links then
		data.linksClass = message('start-box-linkclasses')
		data.linksId = message('start-box-link-id')
		data.links = links
	end

	return data
end

--[[
-- Renders the start box html.
--
-- ; Messages
-- : 'start-box-div-classes' --> 'template-documentation-header'
--
-- @function p.renderStartBox
-- @private
-- @param {table} data - a table of data generated by p.makeStartBoxData.
-- @return {string}
--]]
function p.renderStartBox(data)
	local sbox = mw.html.create('div')
	sbox
		:addClass(message('start-box-div-classes'))
		:newline()
		:tag('span')
			:cssText(data.headingStyleText)
			:css('font-weight', data.headingFontWeight)
			:css('font-size', data.headingFontSize)
			:wikitext(data.heading)
	local links = data.links
	if links then
		sbox
			:tag('div')
			:css('float', 'right')
				:tag('span')
				:addClass(data.linksClass)
				:attr('id', data.linksId)
				:wikitext(links)
	end
	local subHeading = data.subHeading
	if subHeading then
		sbox
			:tag('br')
			:css('clear', 'right')
		sbox
			:tag('i')
			:wikitext(subHeading)
	end
	return sbox
end

-- --------------------------------------------------------------------------
-- Documentation content
-- --------------------------------------------------------------------------

--[[
-- Displays the documentation contents
--
-- ; Messages
-- : 'content-box-div-classes' --> 'template-documentation-content'
--
-- @function p.content
-- @param {table|Frame} args - a table of arguments passed by the user
-- @param[opt] {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string}
--]]
p.content = makeInvokeFunc('_content')

function p._content(args, env)
	env = env or p.getEnvironment(args)
	local docTitle = env.docTitle
	local content = args.content
	local root = mw.html.create()
	if not content and docTitle then
		local subjectSpace = env.subjectSpace
		local preload = args.preload
		if not preload then
			if subjectSpace == 6 then -- File namespace
				preload = message('file-docpage-preload')
			elseif subjectSpace == 828 then -- Module namespace
				preload = message('module-preload')
			else
				preload = message('docpage-preload')
			end
		end

		local hasSubpages = env.hasSubpages
		local docTitleCreate = env.docTitleCreate

		local docMissing = i18n:msg(
			resolveNamespace(subjectSpace) .. '-documentation-missing',
			docTitleCreate:fullUrl({
				action = 'edit',
				redlink = '1',
				preload = mw.uri.encode(preload, 'WIKI'),
			}, 'https'):gsub('%%', '%%%%')
		)

		if not pageExists(docTitle.prefixedText) and not hasSubpages then
			content = docMissing
		else
			if yesno(args.i18n, false) or hasSubpages then
				local currentLang = i18n:getLang()
				root:node(languages.langs{
					'en',
					currentLang,
					format = 'list',
					page = docTitle.prefixedText,
					class = message('languages-list-div-classes'),
					select = currentLang,
					editintro = 'Template:Documentation/editintro',
				})
			end
			
			content = args._content or tostring(languages.langs{
				format = 'transclude',
				page = docTitle.prefixedText,
				notice = 'none',
				missing = (function()
				    -- prefixedText here does not work since it does not leave a colon in front of the page name
				    -- if the page is in main namespace, making expandTemplate assume we're invoking a template.
				    local prefixedTitle = docTitle.nsText .. ':' .. docTitle.text
					if pcall(function() return mw.getCurrentFrame():expandTemplate{title = prefixedTitle} end) then
						return mw.getCurrentFrame():expandTemplate{title = prefixedTitle}
					else
						return docMissing	
					end
				end)()
			})
		end
	end
	local cbox = root:tag('div')
	cbox
		:addClass(message('content-box-div-classes'))
		:css("display", "flow-root")
		-- The line breaks are necessary so that "=== Headings ===" at the start
		-- and end of docs are interpreted correctly.
		:wikitext(mw.getCurrentFrame():expandTemplate{title = message('template-tocright')})
		:newline()
		:wikitext(content or '')
		:newline()
	return root
end

--[[
-- Gets the content title
--
-- @function p.contentTitle
-- @param {table|Frame} args - a table of arguments passed by the user
-- @param[opt] {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string}
--]]
p.contentTitle = makeInvokeFunc('_contentTitle')

function p._contentTitle(args, env)
	env = env or p.getEnvironment(args)
	local docTitle = env.docTitle
	if not args.content and docTitle and pageExists(docTitle.prefixedText) then
		return docTitle.prefixedText
	else
		return ''
	end
end

-- --------------------------------------------------------------------------
-- End box
-- --------------------------------------------------------------------------

---
-- This function generates the end box (also known as the link box).
--
-- The HTML is generated by the <code>{​{fmbox}}</code> template, courtesy of [[Module:Message box]].
--
-- ; Messages:
-- : 'fmbox-id' --> 'documentation-meta-data'
-- : 'fmbox-style' --> 'background-color: #ecfcf4'
-- : 'fmbox-textstyle' --> 'font-style: italic'
--
-- @function p.endBox
-- @param {table|Frame} args - a table of arguments passed by the user
-- @param[opt] {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string}
p.endBox = makeInvokeFunc('_endBox')

function p._endBox(args, env)
	-- Get environment data.
	env = env or p.getEnvironment(args)

	local subjectSpace = env.subjectSpace
	local docTitle = env.docTitleCurrentLang

	local root = mw.html.create('div')
		:attr('id', message('end-box-div-id'))
		:addClass(message('end-box-div-classes'))
		:css('clear', 'both')

	local hasDocPage   = subjectSpace and docTitle and pageExists(docTitle.prefixedText)
	local linkBoxSpace = subjectSpace == 2 or subjectSpace == 10 or subjectSpace == 828

	-- Check whether we should output the end box at all. Add the end
	-- box by default if the documentation exists or if we are in the
	-- user, module or template namespaces.
	local linkBox = args['link box']
	local expandedLinkBox = yesno(linkBox, true)

	if (
		not hasDocPage and not linkBoxSpace
		and not (linkBox and yesno(linkBox) == nil)
	) then
		return root
			:attr('id', nil)
			:attr('class', nil)
		:allDone()
	end

	if IS_DEV_WIKI then
		-- Hack to ensure that the style of the end box matches the existing style
		root
			--[[
			:cssText('background-color:#EEE')
			:css('background-color', 'var(--theme-page-background-color--secondary,#EEE)')
			:css('border-color', 'var(--theme-border-color,#CCC)')
			--]]
			:css('font-size', '100%')
			:css('margin', '0')
			:css('padding', '.5em 1em')
	end
	local customLanguagesNotice = i18n:msg('custom-languages-notice', docTitle.prefixedText,
		docTitle:fullUrl({ action = 'edit' }, 'https'):gsub("%%", "%%%%"), 'bottom')

	if expandedLinkBox == false then
		return root:wikitext(customLanguagesNotice):allDone()
	end

	if linkBox and yesno(linkBox) == nil then
		root:wikitext(linkBox)
	else
		local isFirstLine = true
		if (not args.content) and hasDocPage then
			isFirstLine = false
			root:wikitext(customLanguagesNotice)
		end

		if linkBoxSpace then
			-- We are in the user, template or module namespaces.
			-- Add sandbox and testcases links:
			-- "Editors can experiment in this template's sandbox (edit | history / create | mirror) and testcases (edit/create) pages."
			local experimentBlurb = p.makeExperimentBlurb(args, env)
			if experimentBlurb then
				if isFirstLine then
					isFirstLine = false
				else
					root:tag('br')
				end
				root:node(experimentBlurb)
			end

			-- "Please add categories to the /doc subpage."
			-- Don't show this message with inline docs or with an explicitly specified doc page,
			-- as then it is unclear where to add the categories.
			local categoriesBlurb
			if not args.content and not args[1] then
				categoriesBlurb = p.makeCategoriesBlurb(args, env)
			end

			-- "Subpages of this template"
			local subpagesBlurb = p.makeSubpagesBlurb(args, env)

			if categoriesBlurb or subpagesBlurb then
				if isFirstLine then
					isFirstLine = false
				else
					root:tag('br'):done()
				end

				root:node(categoriesBlurb)

				if categoriesBlurb and subpagesBlurb then
					root:wikitext(' ');
				end

				root:node(subpagesBlurb)
			end

			-- TODO: consider adding the print blurb
		end
	end

	return root:allDone()
end

--[[
-- Renders the text "Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages."
--
-- ; Messages:
-- : 'module-sandbox-preload' --> 'Template:Documentation/preload-module-sandbox'
-- : 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox'
-- : 'mirror-link-preload' --> 'Template:Documentation/mirror'
-- : 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox'
-- : 'module-testcases-preload' --> 'Template:Documentation/preload-module-testcases'
-- : 'template-testcases-preload' --> 'Template:Documentation/preload-testcases'
--
-- @function p.makeExperimentBlurb
-- @private
-- @param {table} args - a table of arguments passed by the user
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string|nil}
--]]
function p.makeExperimentBlurb(args, env)
	local subjectSpace = env.subjectSpace
	local templateTitle = env.templateTitle
	local sandboxTitle = env.sandboxTitle
	local testcasesTitle = env.testcasesTitle

	if not subjectSpace or not templateTitle or not sandboxTitle or not testcasesTitle then
		return nil
	end

	local templatePage = templateTitle.prefixedText

	-- Make links.
	local frame = mw.getCurrentFrame()
	local sandboxLinks = mw.html.create()
	local testcasesLinks = mw.html.create()

	if pageExists(sandboxTitle.prefixedText) then
		sandboxLinks:wikitext(
			makeUrlWikilink(sandboxTitle.prefixedText, i18n:msg('sandbox-link-display')),
			' '
		)

		local sandboxEditLink = makeUrlLink(
			sandboxTitle:fullUrl({ action = 'edit' }, 'https'),
			frame:preprocess('{{lc:{{int:edit}}}}')
		)
		local compareUrl = env.compareUrl
		local compareLink
		if compareUrl then
			compareLink = makeUrlLink(
				compareUrl,
				frame:preprocess('{{lc:{{int:diff}}}}')
			)
		end

		sandboxLinks:node(makeToolbar(sandboxEditLink, compareLink))
	elseif message('sandbox-subpage-show', nil, 'boolean') then
		sandboxLinks:wikitext(
			i18n:msg('sandbox-link-display'),
			' '
		)

		local sandboxCreateLink = makeUrlLink(
			sandboxTitle:fullUrl({
				action  = 'edit',
				redlink = '1',
				preload = subjectSpace == 828
					and message('module-sandbox-preload')
					or  message('template-sandbox-preload'),
			}, 'https'),
			frame:preprocess('{{lc:{{int:create}}}}')
		)

		local mirrorLink = makeUrlLink(
			sandboxTitle:fullUrl({
				action  = 'edit',
				redlink = '1',
				preload = message('mirror-link-preload'),
				summary = i18n:msg('mirror-edit-summary', makeWikilink(templatePage)),
			}, 'https'),
			i18n:msg('mirror-link-display')
		)

		sandboxLinks:node(makeToolbar(sandboxCreateLink, mirrorLink))
	else
		sandboxLinks = nil
	end

	if pageExists(testcasesTitle.prefixedText) then
		testcasesLinks:wikitext(
			makeUrlWikilink(testcasesTitle.prefixedText, i18n:msg('testcases-link-display')),
			' '
		)

		local testcasesEditLink = makeUrlLink(
			testcasesTitle:fullUrl({ action = 'edit' }, 'https'),
			frame:preprocess('{{lc:{{int:edit}}}}')
		)

		testcasesLinks:node(makeToolbar(testcasesEditLink))
	else
		testcasesLinks:wikitext(
			i18n:msg('testcases-link-display'),
			' '
		)

		local testcasesCreateLink = makeUrlLink(
			testcasesTitle:fullUrl({
				action  = 'edit',
				redlink = '1',
				preload = subjectSpace == 828
					and message('module-testcases-preload')
					or  message('template-testcases-preload'),
			}, 'https'),
			frame:preprocess('{{lc:{{int:create}}}}')
		)

		testcasesLinks:node(makeToolbar(testcasesCreateLink))
	end

	local msgArgs = { tostring(testcasesLinks):gsub('%%', '%%%%') or '' }

	local messageName =
		subjectSpace == 828
			and 'experiment-blurb-module'
			or  'experiment-blurb-template'

	if sandboxLinks then
		table.insert(msgArgs, 1, tostring(sandboxLinks):gsub('%%', '%%%%') or '')
	else
		messageName = messageName .. '-nosandbox'
	end

	return i18n:msg{
		key  = messageName,
		args = msgArgs,
	}
end

--[[
-- Generates the text "Please add categories to the /doc subpage."
--
-- ; Messages:
-- : 'doc-link-display' --> '/doc'
-- : 'add-categories-blurb' --> 'Please add categories to the $1 subpage.'
--
-- @function p.makeCategoriesBlurb
-- @private
-- @param {table} args - a table of arguments passed by the user
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string|nil}
--]]
function p.makeCategoriesBlurb(args, env)
	local docTitle = env.docTitle
	if not docTitle then
		return nil
	end

	local docPathLink = makeUrlWikilink(
		docTitle.prefixedText,
		message('doc-link-display')
	):gsub("%%", "%%%%")

	return i18n:msg('add-categories-blurb', docPathLink)
end

--[[
-- Generates the "Subpages of this template" link.
--
-- ; Messages:
-- : 'template-pagetype' --> 'template'
-- : 'module-pagetype' --> 'module'
-- : 'default-pagetype' --> 'page'
-- : 'subpages-link-display' --> 'Subpages of this $1'
--
-- @function p.makeSubpagesBlurb
-- @private
-- @param {table} args - a table of arguments passed by the user
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string|nil}
--]]
function p.makeSubpagesBlurb(args, env)
	local subjectSpace = env.subjectSpace
	local templateTitle = env.templateTitle
	if not subjectSpace or not templateTitle then
		return nil
	end

	local namespaceName = resolveNamespace(subjectSpace)
	assert(namespaceName ~= 'file')

	return i18n:msg(
		resolveNamespace(subjectSpace) .. '-subpages-link',
		'Special:PrefixIndex/' .. templateTitle.prefixedText .. '/'
	)
end

---
-- Generates the blurb displayed when there is a print version of the template available.
--
-- ; Messages:
-- : 'print-link-display' --> '/Print'
-- : 'print-blurb' --> 'A [[Help:Books/for experts#Improving the book layout|print version]]'
--		.. ' of this template exists at $1.'
--		.. ' If you make a change to this template, please update the print version as well.'
-- : 'display-print-category' --> true
-- : 'print-category' --> 'Templates with print versions'
--
-- @function p.makePrintBlurb
-- @private
-- @param {table} args - a table of arguments passed by the user
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string|nil}
function p.makePrintBlurb(args, env)
	local printTitle = env.printTitle
	if not printTitle then
		return nil
	end
	local ret
	if pageExists(printTitle.prefixedText) then
		local printLink = makeWikilink(printTitle.prefixedText, message('print-link-display'))
		ret = message('print-blurb', {printLink})
		local displayPrintCategory = message('display-print-category', nil, 'boolean')
		if displayPrintCategory then
			ret = ret .. makeCategoryLink(message('print-category'))
		end
	end
	return ret
end

-- --------------------------------------------------------------------------
-- Tracking categories
-- --------------------------------------------------------------------------

--[[
-- Check if {{T|documentation}} is transcluded on a /doc or /testcases page.
--
-- ; Messages:
-- : 'display-strange-usage-category' --> true
-- : 'doc-subpage' --> 'doc'
-- : 'testcases-subpage' --> 'testcases'
-- : 'strange-usage-category' --> 'Pages with strange ((documentation)) usage'
-- : 'nodoc-category-template' --> 'Templates with no documentation'
-- : 'nodoc-category-module' --> 'Modules with no documentation'
-- : 'nodoc-category-file' --> 'Files with no summary'
-- : 'nodoc-category-other' --> 'Pages with no documentation'
-- : 'baddoc-category-template' --> 'Templates with bad documentation'
-- : 'baddoc-category-module' --> 'Modules with bad documentation'
-- : 'baddoc-category-file' --> 'Files with bad summary'
-- : 'baddoc-category-other' --> 'Pages with bad documentation'
--
-- /testcases pages in the module namespace are not categorised, as they may have
-- {{T|documentation}} transcluded automatically.
--
-- @function p.addTrackingCategories
-- @private
-- @param {Environment} env - environment table containing title objects, etc., generated with p.getEnvironment
-- @return {string|nil}
--]]
function p.addTrackingCategories(env)
	local title = env.title
	local subjectSpace = env.subjectSpace
	if not title or not subjectSpace then
		return nil
	end
	local subpage = title.subpageText
	local ret = ''
	if message('display-strange-usage-category', nil, 'boolean')
		and (
			subpage == message('doc-subpage')
			or subjectSpace ~= 828 and subpage == message('testcases-subpage')
		)
	then
		ret = ret .. makeCategoryLink(message('strange-usage-category'))
	end
	local docStatus = env.docStatus
	if docStatus then
		ret = ret .. makeCategoryLink(message(docStatus .. '-category-' .. resolveNamespace(subjectSpace)))
	end
	return ret
end

return p
-- </pre>