<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB">
	<id>https://wiki.shroudforge.net/index.php?action=history&amp;feed=atom&amp;title=Module%3ADependencies%2FParse</id>
	<title>Module:Dependencies/Parse - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.shroudforge.net/index.php?action=history&amp;feed=atom&amp;title=Module%3ADependencies%2FParse"/>
	<link rel="alternate" type="text/html" href="https://wiki.shroudforge.net/index.php?title=Module:Dependencies/Parse&amp;action=history"/>
	<updated>2026-11-05T05:28:54Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.46.2</generator>
	<entry>
		<id>https://wiki.shroudforge.net/index.php?title=Module:Dependencies/Parse&amp;diff=90&amp;oldid=prev</id>
		<title>Bonsaibauer: 1 revision imported: Import Star Citizen Modules</title>
		<link rel="alternate" type="text/html" href="https://wiki.shroudforge.net/index.php?title=Module:Dependencies/Parse&amp;diff=90&amp;oldid=prev"/>
		<updated>2026-11-04T22:22:09Z</updated>

		<summary type="html">&lt;p&gt;1 revision imported: Import Star Citizen Modules&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw-interface=&quot;&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en-GB&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 22:22, 4 November 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;4&quot; class=&quot;diff-notice&quot; lang=&quot;en-GB&quot;&gt;&lt;div class=&quot;mw-diff-empty&quot;&gt;(No difference)&lt;/div&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;!-- diff cache key mediawiki-mediawiki:diff:1.41:old-89:rev-90 --&gt;
&lt;/table&gt;</summary>
		<author><name>Bonsaibauer</name></author>
	</entry>
	<entry>
		<id>https://wiki.shroudforge.net/index.php?title=Module:Dependencies/Parse&amp;diff=89&amp;oldid=prev</id>
		<title>starcitizen&gt;Alistar Bot: Sync from Git (563b59e): address review of the Files group (via update-page on MediaWiki MCP Server)</title>
		<link rel="alternate" type="text/html" href="https://wiki.shroudforge.net/index.php?title=Module:Dependencies/Parse&amp;diff=89&amp;oldid=prev"/>
		<updated>2026-10-08T16:43:07Z</updated>

		<summary type="html">&lt;p&gt;Sync from Git (563b59e): address review of the Files group (via update-page on MediaWiki MCP Server)&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;require(&amp;#039;strict&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
--- @module Dependencies/Parse&lt;br /&gt;
--- Reads a page&amp;#039;s own source and returns what it depends on: the wiki modules a&lt;br /&gt;
--- Lua module requires or loads, the module functions a template invokes, the&lt;br /&gt;
--- TemplateStyles stylesheets either one loads, and the image files it names.&lt;br /&gt;
--- Pure: the caller passes the source text.&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
--- Libraries a module may require that are not wiki pages.&lt;br /&gt;
local BUILTINS = { libraryUtil = true, strict = true, bit32 = true }&lt;br /&gt;
&lt;br /&gt;
--- Extensions a name must end in to count as a file.&lt;br /&gt;
local FILE_EXTENSIONS = { svg = true, png = true, jpg = true, jpeg = true, gif = true, webp = true }&lt;br /&gt;
&lt;br /&gt;
--- A URL on this wiki&amp;#039;s file storage, up to the end of its path. The path is&lt;br /&gt;
--- `a/ab/&amp;lt;File&amp;gt;` (or `thumb/a/ab/&amp;lt;File&amp;gt;/…`), `ab` being the start of the md5 of&lt;br /&gt;
--- the file&amp;#039;s name.&lt;br /&gt;
local MEDIA_URL = &amp;#039;%a*:?//media%.starcitizen%.tools/[^%s&amp;quot;\&amp;#039;%)%[%]|{}&amp;lt;&amp;gt;]+&amp;#039;&lt;br /&gt;
&lt;br /&gt;
--- A run of wikitext between the characters that cannot be part of a file name&lt;br /&gt;
--- and the syntax around one: `[[File:X.svg|20px]]` yields `File`, `X.svg`, `20px`.&lt;br /&gt;
local SEGMENT = &amp;#039;[^|=%[%]{}&amp;lt;&amp;gt;\n:&amp;quot;]+&amp;#039;&lt;br /&gt;
&lt;br /&gt;
--- A `string.format` directive, which stands for a value filled in at runtime.&lt;br /&gt;
local FORMAT_DIRECTIVE = &amp;#039;%%[-%d%.]*[sdi]&amp;#039;&lt;br /&gt;
&lt;br /&gt;
--- The calls that name a module, and the list each one feeds.&lt;br /&gt;
local CALLS = {&lt;br /&gt;
	{ pattern = &amp;#039;require&amp;#039;, list = &amp;#039;requires&amp;#039; },&lt;br /&gt;
	{ pattern = &amp;#039;mw%.loadData&amp;#039;, list = &amp;#039;loads&amp;#039; },&lt;br /&gt;
	{ pattern = &amp;#039;mw%.loadJsonData&amp;#039;, list = &amp;#039;loads&amp;#039; },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
--- A Lua pattern matching `word` in any letter case.&lt;br /&gt;
--- @param word string&lt;br /&gt;
--- @return string&lt;br /&gt;
local function caseless(word)&lt;br /&gt;
	return (word:gsub(&amp;#039;%a&amp;#039;, function(letter)&lt;br /&gt;
		return &amp;#039;[&amp;#039; .. letter:upper() .. letter:lower() .. &amp;#039;]&amp;#039;&lt;br /&gt;
	end))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- The start of a `&amp;lt;templatestyles … src=` tag, up to the attribute value.&lt;br /&gt;
local STYLES_TAG = &amp;#039;&amp;lt;&amp;#039; .. caseless(&amp;#039;templatestyles&amp;#039;) .. &amp;#039;%s[^&amp;gt;]-&amp;#039; .. caseless(&amp;#039;src&amp;#039;) .. &amp;#039;%s*=%s*&amp;#039;&lt;br /&gt;
&lt;br /&gt;
--- @param text string wikitext&lt;br /&gt;
--- @return string&lt;br /&gt;
local function stripHtmlComments(text)&lt;br /&gt;
	return (text:gsub(&amp;#039;&amp;lt;!%-%-.-%-%-&amp;gt;&amp;#039;, &amp;#039;&amp;#039;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- @param set table&amp;lt;string, boolean&amp;gt;&lt;br /&gt;
--- @return string[]&lt;br /&gt;
local function sortedKeys(set)&lt;br /&gt;
	local list = {}&lt;br /&gt;
	for key in pairs(set) do&lt;br /&gt;
		list[#list + 1] = key&lt;br /&gt;
	end&lt;br /&gt;
	table.sort(list)&lt;br /&gt;
	return list&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- The string a single literal argument holds, or nil. A value containing its&lt;br /&gt;
--- own quote character is the concatenation of two literals, not one.&lt;br /&gt;
--- @param arg string&lt;br /&gt;
--- @return string|nil&lt;br /&gt;
local function literal(arg)&lt;br /&gt;
	local quote, value = arg:match(&amp;#039;^%s*([\&amp;#039;&amp;quot;])(.-)%1%s*$&amp;#039;)&lt;br /&gt;
	if value == nil or value:find(quote, 1, true) then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	return value&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- The string literal assigned to a variable elsewhere in the source, or nil.&lt;br /&gt;
--- @param src string&lt;br /&gt;
--- @param name string an identifier&lt;br /&gt;
--- @return string|nil&lt;br /&gt;
local function variableValue(src, name)&lt;br /&gt;
	local quote, value = src:match(&amp;#039;[^%w_]&amp;#039; .. name .. &amp;#039;%s*=%s*([\&amp;#039;&amp;quot;])(.-)%1&amp;#039;)&lt;br /&gt;
	if quote == nil then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	return value&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Joins a concatenation&amp;#039;s literal parts, with `…` for every non-literal part.&lt;br /&gt;
--- @param arg string&lt;br /&gt;
--- @return string&lt;br /&gt;
local function concatenation(arg)&lt;br /&gt;
	local parts = {}&lt;br /&gt;
	for piece in (arg .. &amp;#039;..&amp;#039;):gmatch(&amp;#039;(.-)%.%.&amp;#039;) do&lt;br /&gt;
		parts[#parts + 1] = literal(piece) or &amp;#039;…&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	return table.concat(parts)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Removes Lua comments: long comments (`--[[ ]]`, `--[==[ ]==]`) first, then&lt;br /&gt;
--- line comments. A `--` inside a string literal is also cut, which can only&lt;br /&gt;
--- drop a dependency, never invent one.&lt;br /&gt;
--- @param src string&lt;br /&gt;
--- @return string&lt;br /&gt;
function p.stripComments(src)&lt;br /&gt;
	src = src:gsub(&amp;#039;%-%-%[(=*)%[.-%]%1%]&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
	return (src:gsub(&amp;#039;%-%-[^\n]*&amp;#039;, &amp;#039;&amp;#039;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- `Module:` plus the name with its first letter upper-cased, or nil when the&lt;br /&gt;
--- name is not in the Module namespace.&lt;br /&gt;
--- @param name string&lt;br /&gt;
--- @return string|nil&lt;br /&gt;
function p.normaliseModule(name)&lt;br /&gt;
	local rest = mw.text.trim(name):match(&amp;#039;^[Mm][Oo][Dd][Uu][Ll][Ee]%s*:%s*(.+)$&amp;#039;)&lt;br /&gt;
	if rest == nil then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	return &amp;#039;Module:&amp;#039; .. rest:sub(1, 1):upper() .. rest:sub(2)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- The module an `#invoke` names: with or without its `Module:` prefix.&lt;br /&gt;
--- @param target string&lt;br /&gt;
--- @return string|nil&lt;br /&gt;
function p.normaliseInvokeTarget(target)&lt;br /&gt;
	local name = mw.text.trim(target):gsub(&amp;#039;^[Mm][Oo][Dd][Uu][Ll][Ee]%s*:%s*&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
	if name == &amp;#039;&amp;#039; then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	return &amp;#039;Module:&amp;#039; .. name:sub(1, 1):upper() .. name:sub(2)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- The page a TemplateStyles `src` names. A `src` without a namespace is a&lt;br /&gt;
--- Template page, which is how TemplateStyles resolves it.&lt;br /&gt;
--- @param src string&lt;br /&gt;
--- @return string|nil&lt;br /&gt;
function p.normaliseStylesheet(src)&lt;br /&gt;
	local name = mw.text.trim(src)&lt;br /&gt;
	if name == &amp;#039;&amp;#039; then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	local namespace, rest = name:match(&amp;#039;^([^:/]+):%s*(.+)$&amp;#039;)&lt;br /&gt;
	if namespace == nil then&lt;br /&gt;
		namespace, rest = &amp;#039;Template&amp;#039;, name&lt;br /&gt;
	else&lt;br /&gt;
		namespace = mw.text.trim(namespace)&lt;br /&gt;
		local canonical = { module = &amp;#039;Module&amp;#039;, template = &amp;#039;Template&amp;#039; }&lt;br /&gt;
		namespace = canonical[namespace:lower()] or namespace&lt;br /&gt;
	end&lt;br /&gt;
	return namespace .. &amp;#039;:&amp;#039; .. rest:sub(1, 1):upper() .. rest:sub(2)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Stylesheets named by `&amp;lt;templatestyles src=…&amp;gt;` tags.&lt;br /&gt;
--- @param text string|nil wikitext, or Lua source whose strings hold such tags&lt;br /&gt;
--- @return string[] sorted, unique&lt;br /&gt;
function p.styles(text)&lt;br /&gt;
	local found = {}&lt;br /&gt;
	for rest in stripHtmlComments(text or &amp;#039;&amp;#039;):gmatch(STYLES_TAG .. &amp;#039;([^&amp;gt;]*)&amp;#039;) do&lt;br /&gt;
		local value = rest:match(&amp;#039;^&amp;quot;([^&amp;quot;]*)&amp;quot;&amp;#039;) or rest:match(&amp;quot;^&amp;#039;([^&amp;#039;]*)&amp;#039;&amp;quot;) or rest:match(&amp;#039;^([^%s&amp;gt;&amp;quot;\&amp;#039;]+)&amp;#039;)&lt;br /&gt;
		local name = value and p.normaliseStylesheet((value:gsub(&amp;#039;/$&amp;#039;, &amp;#039;&amp;#039;)))&lt;br /&gt;
		if name then&lt;br /&gt;
			found[name] = true&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return sortedKeys(found)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- A file&amp;#039;s name as its page title shows it, without the namespace: underscores&lt;br /&gt;
--- as spaces, first letter upper-cased. Nil when `text` is not a file name with&lt;br /&gt;
--- an image extension. `%` is refused, so a Lua pattern such as `&amp;#039;%.svg&amp;#039;` is not&lt;br /&gt;
--- a file.&lt;br /&gt;
--- @param text string&lt;br /&gt;
--- @return string|nil&lt;br /&gt;
function p.normaliseFile(text)&lt;br /&gt;
	local name = mw.text.trim((text:gsub(&amp;#039;[%s_]+&amp;#039;, &amp;#039; &amp;#039;)))&lt;br /&gt;
	name = name:gsub(&amp;#039;^[&amp;quot;\&amp;#039;]&amp;#039;, &amp;#039;&amp;#039;):gsub(&amp;#039;[&amp;quot;\&amp;#039;]$&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
	local base, extension = name:match(&amp;#039;^(.-)%.(%w+)$&amp;#039;)&lt;br /&gt;
	if&lt;br /&gt;
		base == nil&lt;br /&gt;
		or not FILE_EXTENSIONS[extension:lower()]&lt;br /&gt;
		or not base:find(&amp;#039;%w&amp;#039;)&lt;br /&gt;
		or base:sub(1, 1) == &amp;#039;.&amp;#039;&lt;br /&gt;
		or name:find(&amp;#039;[#&amp;lt;&amp;gt;%[%]|{}/\\%%]&amp;#039;)&lt;br /&gt;
	then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	return name:sub(1, 1):upper() .. name:sub(2)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- @class DependenciesFileUrl&lt;br /&gt;
--- @field name string the file the URL names&lt;br /&gt;
--- @field path string the URL&amp;#039;s md5 directories, e.g. `e/e8`&lt;br /&gt;
&lt;br /&gt;
--- @class DependenciesFiles&lt;br /&gt;
--- @field names string[] file names without the namespace, sorted, unique&lt;br /&gt;
--- @field urls DependenciesFileUrl[] the media URLs among them, sorted by name then path&lt;br /&gt;
--- @field dynamic string[] names built while the page runs, such as `Sc-icon-brand-….svg`&lt;br /&gt;
&lt;br /&gt;
--- @return table&lt;br /&gt;
local function collector()&lt;br /&gt;
	return { names = {}, urls = {}, dynamic = {} }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- @param found table a collector()&lt;br /&gt;
--- @return DependenciesFiles&lt;br /&gt;
local function finish(found)&lt;br /&gt;
	local urls = {}&lt;br /&gt;
	for _, url in pairs(found.urls) do&lt;br /&gt;
		urls[#urls + 1] = url&lt;br /&gt;
	end&lt;br /&gt;
	table.sort(urls, function(a, b)&lt;br /&gt;
		return a.name .. &amp;#039;|&amp;#039; .. a.path &amp;lt; b.name .. &amp;#039;|&amp;#039; .. b.path&lt;br /&gt;
	end)&lt;br /&gt;
	return { names = sortedKeys(found.names), urls = urls, dynamic = sortedKeys(found.dynamic) }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Adds the files that media URLs in `text` name to `found`, and returns&lt;br /&gt;
--- `text` without those URLs.&lt;br /&gt;
--- @param text string&lt;br /&gt;
--- @param found table a collector()&lt;br /&gt;
--- @return string&lt;br /&gt;
local function scanUrls(text, found)&lt;br /&gt;
	for url in text:gmatch(MEDIA_URL) do&lt;br /&gt;
		local path = (url:match(&amp;#039;//[^/]+/(.*)$&amp;#039;):gsub(&amp;#039;^thumb/&amp;#039;, &amp;#039;&amp;#039;))&lt;br /&gt;
		local first, pair, file = path:match(&amp;#039;^(%x)/(%x%x)/([^/?#]+)&amp;#039;)&lt;br /&gt;
		local name = first and file and p.normaliseFile(mw.uri.decode(file, &amp;#039;PATH&amp;#039;))&lt;br /&gt;
		if name and pair:sub(1, 1) == first then&lt;br /&gt;
			found.names[name] = true&lt;br /&gt;
			found.urls[name .. &amp;#039;|&amp;#039; .. first .. &amp;#039;/&amp;#039; .. pair] = { name = name, path = first .. &amp;#039;/&amp;#039; .. pair }&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return (text:gsub(MEDIA_URL, &amp;#039; &amp;#039;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Adds the files `text` names to `found`: media URLs, and every segment&lt;br /&gt;
--- (see SEGMENT) that is a whole file name. A name holding `…` is built at&lt;br /&gt;
--- runtime and goes to `found.dynamic`.&lt;br /&gt;
--- @param text string&lt;br /&gt;
--- @param found table a collector()&lt;br /&gt;
--- @param transform (fun(segment: string): string)|nil applied to each segment first&lt;br /&gt;
local function scan(text, found, transform)&lt;br /&gt;
	for segment in scanUrls(text, found):gmatch(SEGMENT) do&lt;br /&gt;
		local name = p.normaliseFile(transform and transform(segment) or segment)&lt;br /&gt;
		if name and name:find(&amp;#039;…&amp;#039;, 1, true) then&lt;br /&gt;
			found.dynamic[name] = true&lt;br /&gt;
		elseif name then&lt;br /&gt;
			found.names[name] = true&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Files a template names: `[[File:…]]`, a parameter value that is a whole&lt;br /&gt;
--- file name, and media URLs.&lt;br /&gt;
--- @param wikitext string|nil&lt;br /&gt;
--- @return DependenciesFiles&lt;br /&gt;
function p.files(wikitext)&lt;br /&gt;
	local found = collector()&lt;br /&gt;
	scan(stripHtmlComments(wikitext or &amp;#039;&amp;#039;), found)&lt;br /&gt;
	return finish(found)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Files a stylesheet names through media URLs.&lt;br /&gt;
--- @param css string|nil&lt;br /&gt;
--- @return DependenciesFiles&lt;br /&gt;
function p.stylesheetFiles(css)&lt;br /&gt;
	local found = collector()&lt;br /&gt;
	scanUrls(((css or &amp;#039;&amp;#039;):gsub(&amp;#039;/%*.-%*/&amp;#039;, &amp;#039;&amp;#039;)), found)&lt;br /&gt;
	return finish(found)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- @class DependenciesLiteral&lt;br /&gt;
--- @field value string&lt;br /&gt;
--- @field first integer the position of its opening quote or bracket&lt;br /&gt;
--- @field last integer the position of its closing quote or bracket&lt;br /&gt;
&lt;br /&gt;
--- The string literals in Lua source, in order. Reads comments itself rather&lt;br /&gt;
--- than through stripComments(), which also cuts a `--` inside a string (as in&lt;br /&gt;
--- a CSS custom property) and so would leave a quote unmatched.&lt;br /&gt;
--- @param src string&lt;br /&gt;
--- @return DependenciesLiteral[]&lt;br /&gt;
local function literals(src)&lt;br /&gt;
	local found, i = {}, 1&lt;br /&gt;
	while true do&lt;br /&gt;
		local at = src:find(&amp;#039;[%-\&amp;#039;&amp;quot;%[]&amp;#039;, i)&lt;br /&gt;
		if at == nil then&lt;br /&gt;
			return found&lt;br /&gt;
		end&lt;br /&gt;
		local char = src:sub(at, at)&lt;br /&gt;
		if char == &amp;#039;-&amp;#039; then&lt;br /&gt;
			if src:sub(at + 1, at + 1) ~= &amp;#039;-&amp;#039; then&lt;br /&gt;
				i = at + 1&lt;br /&gt;
			else&lt;br /&gt;
				local level = src:match(&amp;#039;^%[(=*)%[&amp;#039;, at + 2)&lt;br /&gt;
				local stop&lt;br /&gt;
				if level then&lt;br /&gt;
					stop = select(2, src:find(&amp;#039;]&amp;#039; .. level .. &amp;#039;]&amp;#039;, at + 4 + #level, true))&lt;br /&gt;
				else&lt;br /&gt;
					stop = src:find(&amp;#039;\n&amp;#039;, at, true)&lt;br /&gt;
				end&lt;br /&gt;
				i = (stop or #src) + 1&lt;br /&gt;
			end&lt;br /&gt;
		elseif char == &amp;#039;[&amp;#039; then&lt;br /&gt;
			local level = src:match(&amp;#039;^%[(=*)%[&amp;#039;, at)&lt;br /&gt;
			if level == nil then&lt;br /&gt;
				i = at + 1&lt;br /&gt;
			else&lt;br /&gt;
				local open = at + #level + 2&lt;br /&gt;
				local close, stop = src:find(&amp;#039;]&amp;#039; .. level .. &amp;#039;]&amp;#039;, open, true)&lt;br /&gt;
				found[#found + 1] = { value = src:sub(open, (close or #src + 1) - 1), first = at, last = stop or #src }&lt;br /&gt;
				i = (stop or #src) + 1&lt;br /&gt;
			end&lt;br /&gt;
		else&lt;br /&gt;
			local parts, j = {}, at + 1&lt;br /&gt;
			while true do&lt;br /&gt;
				local special = src:find(&amp;#039;[\\\n&amp;#039; .. char .. &amp;#039;]&amp;#039;, j)&lt;br /&gt;
				if special == nil then&lt;br /&gt;
					parts[#parts + 1] = src:sub(j)&lt;br /&gt;
					j = #src&lt;br /&gt;
					break&lt;br /&gt;
				end&lt;br /&gt;
				parts[#parts + 1] = src:sub(j, special - 1)&lt;br /&gt;
				if src:sub(special, special) == &amp;#039;\\&amp;#039; then&lt;br /&gt;
					parts[#parts + 1] = src:sub(special + 1, special + 1)&lt;br /&gt;
					j = special + 2&lt;br /&gt;
				else&lt;br /&gt;
					j = special&lt;br /&gt;
					break&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
			found[#found + 1] = { value = table.concat(parts), first = at, last = j }&lt;br /&gt;
			i = j + 1&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- The text a concatenation ending in `list[k]` builds, with `…` for each part&lt;br /&gt;
--- that is not a literal; nil when `list[k]` is not concatenated onto anything.&lt;br /&gt;
--- @param src string&lt;br /&gt;
--- @param list DependenciesLiteral[]&lt;br /&gt;
--- @param k integer&lt;br /&gt;
--- @return string|nil&lt;br /&gt;
local function concatenated(src, list, k)&lt;br /&gt;
	local parts, i = { list[k].value }, k&lt;br /&gt;
	while true do&lt;br /&gt;
		local before = src:sub(i &amp;gt; 1 and list[i - 1].last + 1 or 1, list[i].first - 1)&lt;br /&gt;
		local head = before:match(&amp;#039;^(.-)%s*%.%.%s*$&amp;#039;)&lt;br /&gt;
		if head == nil then&lt;br /&gt;
			break&lt;br /&gt;
		end&lt;br /&gt;
		if head:find(&amp;#039;%S&amp;#039;) then&lt;br /&gt;
			table.insert(parts, 1, &amp;#039;…&amp;#039;)&lt;br /&gt;
			-- `&amp;#039;a&amp;#039; .. name .. &amp;#039;b&amp;#039;`: one name, call or index between two literals.&lt;br /&gt;
			local operand = (head:gsub(&amp;#039;%b()&amp;#039;, &amp;#039;&amp;#039;):gsub(&amp;#039;%b[]&amp;#039;, &amp;#039;&amp;#039;))&lt;br /&gt;
			if not operand:find(&amp;#039;^%s*%.%.%s*[%w_%.:]+$&amp;#039;) then&lt;br /&gt;
				break&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		if i == 1 then&lt;br /&gt;
			break&lt;br /&gt;
		end&lt;br /&gt;
		i = i - 1&lt;br /&gt;
		table.insert(parts, 1, list[i].value)&lt;br /&gt;
	end&lt;br /&gt;
	if #parts == 1 then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	return table.concat(parts)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- A segment with its `string.format` directives as `…`, unless it holds a&lt;br /&gt;
--- `%` that is not one, as a Lua pattern does.&lt;br /&gt;
--- @param segment string&lt;br /&gt;
--- @return string&lt;br /&gt;
local function formatted(segment)&lt;br /&gt;
	local replaced = segment:gsub(FORMAT_DIRECTIVE, &amp;#039;…&amp;#039;)&lt;br /&gt;
	if replaced:find(&amp;#039;%&amp;#039;, 1, true) then&lt;br /&gt;
		return segment&lt;br /&gt;
	end&lt;br /&gt;
	return replaced&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- @param src string a Lua module&amp;#039;s source&lt;br /&gt;
--- @return DependenciesFiles&lt;br /&gt;
local function luaFiles(src)&lt;br /&gt;
	local found = collector()&lt;br /&gt;
	local list = literals(src)&lt;br /&gt;
	for k, literal in ipairs(list) do&lt;br /&gt;
		local extension = literal.value:match(&amp;#039;%.(%w+)%s*$&amp;#039;)&lt;br /&gt;
		local joined = extension and FILE_EXTENSIONS[extension:lower()] and concatenated(src, list, k)&lt;br /&gt;
		scan(joined or literal.value, found, formatted)&lt;br /&gt;
	end&lt;br /&gt;
	return finish(found)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- @class DependenciesLua&lt;br /&gt;
--- @field requires string[] wiki modules required, sorted, unique&lt;br /&gt;
--- @field loads string[] modules read with mw.loadData or mw.loadJsonData&lt;br /&gt;
--- @field dynamic string[] required patterns such as `Module:X/…`&lt;br /&gt;
--- @field strict boolean the module requires `strict`&lt;br /&gt;
--- @field styles string[] TemplateStyles pages loaded through frame:extensionTag or a tag in a string&lt;br /&gt;
--- @field files DependenciesFiles the files its strings name&lt;br /&gt;
&lt;br /&gt;
--- @param src string|nil a Lua module&amp;#039;s source&lt;br /&gt;
--- @return DependenciesLua&lt;br /&gt;
function p.lua(src)&lt;br /&gt;
	local files = luaFiles(src or &amp;#039;&amp;#039;)&lt;br /&gt;
	src = p.stripComments(src or &amp;#039;&amp;#039;)&lt;br /&gt;
	local found = { requires = {}, loads = {}, dynamic = {} }&lt;br /&gt;
	local strict = false&lt;br /&gt;
&lt;br /&gt;
	local function add(list, arg)&lt;br /&gt;
		local value = literal(arg)&lt;br /&gt;
		if value == nil then&lt;br /&gt;
			local identifier = arg:match(&amp;#039;^%s*([%a_][%w_]*)%s*$&amp;#039;)&lt;br /&gt;
			if identifier then&lt;br /&gt;
				value = variableValue(src, identifier)&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		if value == nil and arg:find(&amp;#039;%.%.&amp;#039;) then&lt;br /&gt;
			local joined = concatenation(arg)&lt;br /&gt;
			if joined:find(&amp;#039;…&amp;#039;, 1, true) then&lt;br /&gt;
				local pattern = p.normaliseModule(joined)&lt;br /&gt;
				-- `Module:…` (no literal text after the namespace) tells the reader&lt;br /&gt;
				-- nothing, so it is dropped rather than recorded.&lt;br /&gt;
				if pattern and pattern ~= &amp;#039;Module:…&amp;#039; then&lt;br /&gt;
					found.dynamic[pattern] = true&lt;br /&gt;
				end&lt;br /&gt;
				return&lt;br /&gt;
			end&lt;br /&gt;
			value = joined&lt;br /&gt;
		end&lt;br /&gt;
		if value == nil then&lt;br /&gt;
			return&lt;br /&gt;
		end&lt;br /&gt;
		if value == &amp;#039;strict&amp;#039; then&lt;br /&gt;
			strict = true&lt;br /&gt;
		end&lt;br /&gt;
		if BUILTINS[value] then&lt;br /&gt;
			return&lt;br /&gt;
		end&lt;br /&gt;
		local name = p.normaliseModule(value)&lt;br /&gt;
		if name then&lt;br /&gt;
			found[list][name] = true&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	for _, call in ipairs(CALLS) do&lt;br /&gt;
		local head = &amp;#039;%f[%w_%.:]&amp;#039; .. call.pattern&lt;br /&gt;
		for arg in src:gmatch(head .. &amp;#039;%s*(%b())&amp;#039;) do&lt;br /&gt;
			add(call.list, arg:sub(2, -2))&lt;br /&gt;
		end&lt;br /&gt;
		for quote, value in src:gmatch(head .. &amp;#039;%s*([\&amp;#039;&amp;quot;])(.-)%1&amp;#039;) do&lt;br /&gt;
			add(call.list, quote .. value .. quote)&lt;br /&gt;
		end&lt;br /&gt;
		for arg in src:gmatch(&amp;#039;%f[%w_]pcall%s*%(%s*&amp;#039; .. call.pattern .. &amp;#039;%s*,([^%),]+)&amp;#039;) do&lt;br /&gt;
			add(call.list, arg)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local styles = {}&lt;br /&gt;
	for _, sheet in ipairs(p.styles(src)) do&lt;br /&gt;
		styles[sheet] = true&lt;br /&gt;
	end&lt;br /&gt;
	for _, shape in ipairs({ &amp;#039;%b()&amp;#039;, &amp;#039;%b{}&amp;#039; }) do&lt;br /&gt;
		for arg in src:gmatch(&amp;#039;%f[%w_]extensionTag%s*(&amp;#039; .. shape .. &amp;#039;)&amp;#039;) do&lt;br /&gt;
			if arg:lower():find(&amp;#039;templatestyles&amp;#039;, 1, true) then&lt;br /&gt;
				local _, value = arg:match(&amp;#039;src%s*=%s*([\&amp;#039;&amp;quot;])(.-)%1&amp;#039;)&lt;br /&gt;
				if value == nil then&lt;br /&gt;
					local identifier = arg:match(&amp;#039;src%s*=%s*([%a_][%w_]*)%s*[,}]&amp;#039;)&lt;br /&gt;
					value = identifier and variableValue(src, identifier)&lt;br /&gt;
				end&lt;br /&gt;
				local name = value and p.normaliseStylesheet(value)&lt;br /&gt;
				if name then&lt;br /&gt;
					styles[name] = true&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	return {&lt;br /&gt;
		requires = sortedKeys(found.requires),&lt;br /&gt;
		loads = sortedKeys(found.loads),&lt;br /&gt;
		dynamic = sortedKeys(found.dynamic),&lt;br /&gt;
		strict = strict,&lt;br /&gt;
		styles = sortedKeys(styles),&lt;br /&gt;
		files = files,&lt;br /&gt;
	}&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- @class DependenciesInvoke&lt;br /&gt;
--- @field module string `Module:Name`&lt;br /&gt;
--- @field func string function name; a parameter such as `{{{1}}}` is kept verbatim&lt;br /&gt;
&lt;br /&gt;
--- @param wikitext string|nil a template&amp;#039;s source&lt;br /&gt;
--- @return DependenciesInvoke[] sorted by module then function, unique&lt;br /&gt;
function p.invokes(wikitext)&lt;br /&gt;
	local text = stripHtmlComments(wikitext or &amp;#039;&amp;#039;)&lt;br /&gt;
	local seen, calls = {}, {}&lt;br /&gt;
	for target, func in text:gmatch(&amp;#039;#[Ii][Nn][Vv][Oo][Kk][Ee]%s*:([^|{}]+)|%s*([^|}]*)&amp;#039;) do&lt;br /&gt;
		local module = p.normaliseInvokeTarget(target)&lt;br /&gt;
		func = mw.text.trim(func)&lt;br /&gt;
		if func:sub(1, 3) == &amp;#039;{{{&amp;#039; then&lt;br /&gt;
			func = func .. &amp;#039;}}}&amp;#039;&lt;br /&gt;
		end&lt;br /&gt;
		local key = module and (module .. &amp;#039;|&amp;#039; .. func)&lt;br /&gt;
		if key and func ~= &amp;#039;&amp;#039; and not seen[key] then&lt;br /&gt;
			seen[key] = true&lt;br /&gt;
			calls[#calls + 1] = { module = module, func = func }&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	table.sort(calls, function(a, b)&lt;br /&gt;
		return a.module .. &amp;#039;|&amp;#039; .. a.func &amp;lt; b.module .. &amp;#039;|&amp;#039; .. b.func&lt;br /&gt;
	end)&lt;br /&gt;
	return calls&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>starcitizen&gt;Alistar Bot</name></author>
	</entry>
</feed>