Module:T

--

-- Experimental rewrite of w:c:dev:Global Lua Modules/T with an overhauled -- parser and improved renderers. Redesigned from the ground up to be powerful, -- intuitive, and extensible. -- -- WIP -- -- @todo --    Consider pointing the legacy entry points to a separate module with the --    old code. This would make T backwards-compatible, and would cost nothing --    since I need to keep the old code either way. On the flip side, users --    would get no warning about the deprecated API, so they wouldn't know to --     upgrade. It would also make bug reports more annoying, since I'd need to --    figure out which version to test. -- --    The first issue can be mollified by using `mw.log` to explain the change; --    users won't get immediate feedback, but they'll find out eventually. The --    second issue will likely remain even if I throw errors, so I don't think --    it weighs against the proposal. -- -- @todo Replace newlines with tags, for Mercury. -- @todo Add warning for accidential named args. -- @todo Improve error handling. -- @todo Improve code docs. -- @todo Perform manual testing. -- @todo Get code review (pls). -- @todo Prepare new tests!! -- @todo Prepare explainer for legacy mode. -- @todo Prepare new documentation. -- @todo Move everything to the correct place. -- @todo Fix T usage on Dev wiki. -- -- @todo --    Consider adding `user-select: none;` to description nodes, to stop users --    users from naively copy/pasting them. This would make T more useful for --    template documentation pages, since it could simultaneously describe each --    parameter and eliminate boilerplate. On the other hand, users might be --    surprised that they aren't able to copy text between angle brackets, or --     might not realize that the feature exists at all. Additionally, the --    algorithm used to select text (at least, on Firefox on Windows 10) adds --    additional newlines which don't exist in the source, so it might be a --     usability concern. -- -- @todo Consider adding i18n for error messages. -- @todo Improve `parseTitle` to handle non-template namespaces. -- @todo Improve `parseTitle` to only match i18n'ed titles based on language.

local p = {} local getArgs = require("Dev:Arguments").getArgs local inspect = require("Dev:Inspect") local userError = require("Dev:User error") local forms = mw.loadData("Module:T/forms") local Nodes = {}

-- @todo

do local metatable = { __index = {}, }

-- @todo --   -- @param {?} self --    @todo -- @returns {?} --    @todo function metatable.__index:render return self.text end

-- @todo --   -- @param {?} text --    @todo -- @returns {?} --    @todo function Nodes.syntax(text) local self = setmetatable({}, metatable)

self.kind = "syntax" self.text = text

return self end end

-- @todo

do local metatable = { __index = {}, }

-- @todo --   -- @param {?} self --    @todo -- @returns {?} --    @todo function metatable.__index:render return self.text end

-- @todo --   -- @param {?} str --    @todo -- @param {?} expectation --    @todo -- @returns {?} --    @todo function Nodes.link(str, expectation) local self = setmetatable({}, metatable) local label = mw.text.trim(str)

if label == "" then error("invalid link " .. inspect(str)) end

local space = mw.text.split(str, label, true)

self.kind = "link" self.text = string.format(           "%s%s%s",            space[1],            expectation.target:format(label),            label,            space[2]        )

return self end end

-- @todo

do local metatable = { __index = {}, }

-- @todo --   -- @param {?} self --    @todo -- @returns {?} --    @todo function metatable.__index:render return self.text end

-- @todo --   -- @param {?} text --    @todo -- @returns {?} --    @todo function Nodes.literal(text) local self = setmetatable({}, metatable)

self.kind = "literal" self.text = text

return self end end

-- @todo

do local metatable = { __index = {}, }

-- @todo --   -- @param {?} self --    @todo -- @returns {?} --    @todo function metatable.__index:render return mw.html.create("i") :css("opacity", "0.65") :wikitext(mw.text.nowiki("<")) :wikitext(self.text) :wikitext(mw.text.nowiki(">")) end

-- @todo --   -- @param {?} text --    @todo -- @returns {?} --    @todo function Nodes.description(text) local self = setmetatable({}, metatable)

self.kind = "description" self.text = text

return self end end

-- @todo

do local metatable = { __index = {}, }

-- @todo --   -- @param {?} self --    @todo -- @returns {?} --    @todo function metatable.__index:render local html = mw.html.create(nil)

if self.name then local name = html:tag("b") :css("font-weight", "bold")

for _, component in ipairs(self.name) do               name:node(component:render) end end

for _, component in ipairs(self.value) do           html:node(component:render) end

return html end

-- @todo --   -- @param {?} str --    @todo -- @returns {?} --    @todo local function parseComponents(str) local components = {} local prevIndex

while true do           local startIndex, stopIndex = str:find("%$%b", prevIndex)

components[#components + 1] = Nodes.literal(               str:sub((prevIndex or 0) + 1, (startIndex or 0) - 1)            )

if not startIndex then break end

components[#components + 1] = Nodes.description(               str:sub(startIndex + 2, stopIndex - 1)            )

prevIndex = stopIndex end

return components end

-- @todo --   -- @param {?} str --    @todo -- @param {?} expectation --    @todo -- @returns {?} --    @todo function Nodes.param(str, expectation) local self = setmetatable({}, metatable) local lhs, rhs

if expectation.parseName then lhs, rhs = str :gsub(mw.text.nowiki("="), "=", 1) :match("^(.-)(=.-)$") end

self.kind = "param" self.name = lhs and parseComponents(lhs) or nil self.value = parseComponents(rhs or str)

return self end end

-- @todo

do local metatable = { __index = {}, }

-- @todo --   -- @param {?} self --    @todo -- @returns {?} --    @todo function metatable.__index:render local wrapper = mw.html.create("code") :css("all", "unset") :css("font-family", "monospace") :wikitext(mw.text.nowiki(""))

return wrapper end

-- @todo --   -- @param {?} ... --    @todo -- @returns {?} --    @todo local function sanitizeArgs(...) local typ = type(...)

if select("#", ...) ~= 1 then return {...} elseif typ == "string" then return mw.text.split(..., "|") elseif typ == "table" then return ... else error("invalid input") end end

-- @todo --   -- @param {?} title --    @todo -- @returns {?} --    @todo local function parseTitle(title) local form = forms.map[mw.ustring.lower(title)]

if not form or (form.caseSensitive and not form.titles[title]) then return forms.default end

return form end

-- @todo --   -- @param {?} args --    @todo -- @param {?} form --    @todo -- @returns {?} --    @todo local function parseChildren(args, form) local children = {}

for i = 1, math.huge do           local expectation = form.knownArgs[i] or form.extraArgs local currentArg = args[i]

-- @todo -- simplify this tangled cluster of logic; extracting it would be           -- pointless, since most branches throw an error or exit the loop if currentArg and expectation then if type(currentArg) == "string" then children[i] = Nodes[expectation.format](                       currentArg,                        expectation                    ) else error("invalid argument at index " .. i)               end elseif currentArg and not expectation then error("unexpected argument at index " .. i)           elseif not currentArg and expectation then if (i - 1) < form.minimumArity then error("missing argument at index " .. i)               else break end elseif not currentArg and not expectation then break end end

return children end

-- @todo --   -- @param {?} ... --    @todo -- @returns {?} --    @todo function Nodes.root(...) local args = sanitizeArgs(...) local form = parseTitle(mw.text.trim(args[1] or "")) local self = setmetatable({}, metatable)

self.kind = "root" self.skippedDelimiters = form.skippedDelimiters self.children = parseChildren(args, form)

return self end end

-- @todo -- -- @see  -- -- @param {?} item --    @todo -- @param {?} path --    @todo -- @returns {?} --    @todo

local function censorMetatables(item, path) if path[#path] ~= inspect.METATABLE then return item end end

-- @todo -- -- @param {?} ... --    @todo -- @returns {?} --    @todo

function p._debug(...) local parseTree = inspect(Nodes.root(...), {       process = censorMetatables,    })

return tostring(mw.html.create("pre"):wikitext(parseTree)) end

-- @todo -- -- @param {?} ... --    @todo -- @returns {?} --    @todo

function p._render(...) return tostring(Nodes.root(...):render) end

-- @todo -- -- @param {?} frame --    @todo -- @returns {?} --    @todo

function p.debug(frame) local args = getArgs(frame, {       trim = false,        removeBlanks = false,    })

local success, response = pcall(p._debug, args)

return success and frame:preprocess(response) or userError(response) end

-- @todo -- -- @param {?} frame --    @todo -- @returns {?} --    @todo

function p.render(frame) local args = getArgs(frame, {       trim = false,        removeBlanks = false,    })

local success, response = pcall(p._render, args)

return success and frame:preprocess(response) or userError(response) end

-- @todo

do local legacyEntryPoints = {"transclusion", "invocation", "main"} local deprecationWarning = "the  API is deprecated. Please see this article for more information" local deprecationCategory = "Pages using the legacy API from Dev:T"

for _, legacyEntryPoint in ipairs(legacyEntryPoints) do       p[legacyEntryPoint] = function return userError(               deprecationWarning:format(legacyEntryPoint),                deprecationCategory            ) end end end

-- @todo

return setmetatable(p, {   __call = function (self, ...)        return self._render(...)    end, })