FANDOM


local mw = require "Module:mw-patch"  -- our lua is outdated, this patches it
local _I = require "Module:iterator"
local rbxTypes = require "Module:roblox types"
local customTags = require "Module:API/CustomTags"
local templateParser = require "Module:template parser"
 
local api = {}
local apiData = mw.loadData("Module:API/data")
 
local tagDescriptions = {
    notCreatable = {
        name = "Uncreatable",
        text = "This object cannot be created with [[Instance.new]].",
        backgroundColor = "F2F2CE",
        borderColor = "ACAC2F",
        category = "Uncreatable objects"
    },
    abstract = {
        name = "Abstract",
        text = "This object is abstract. It cannot be created with [[Instance.new]], and its members are inherited by [[#Inherited Classes|other classes]].",
        backgroundColor = "F2F2CE",
        borderColor = "ACAC2F",
        category = "Abstract objects"
    },
    internal = {
        name = "Internal",
        text = "This object has been marked as internal. It currently serves no significant use to developers.",
        backgroundColor = "FFC3C3",
        borderColor = "FF0000",
        category = "Internal objects"
    },
    unscriptable = {
        name = "Unscriptable",
        text = "This cannot be accessed by any script, and attempting to do so will throw an error. You may be able to change it from ROBLOX Studio, and it may accidentally show up in [[API:Class/Instance/Changed|Instance.Changed]]",
        backgroundColor = "FFC3C3",
        borderColor = "FF0000",
        category = "Unscriptable objects"
    },
    pluginLevel = {
        name = "Plugin class",
        text = "This object has been marked as a ''plugin class''. Most if not all of its members are exclusive to the ''PluginSecurity'' level.",
        backgroundColor = "F2E0CE",
        borderColor = "AC6E2F",
        category = "Plugin classes"
    },
    noInheritance = {
        name = "Filtered-inheritance mode",
        text = "While this class technically inherits all [[Instance]] class members, some (if not all) have been hidden because they serve no purpose for this object.",
        backgroundColor = "AAD7FF",
        borderColor = "324C63"
    },
    noChildrenModifiers = {
        name = "No Children Modifiers",
        text = "Some of the [[Instance]] members on this class page have been hidden, because this class should not need to modify or handle children. You can still use those members, but its not recommended.",
        backgroundColor = "F2E0CE",
        borderColor = "AC6E2F"
    },
    settings = {
        name = "Settings",
        text = "This object has been marked as a ''settings object''. It is used to hold persistant settings, which may be accessible by ROBLOX Studio's settings menu, or the in-game menu. This object cannot be created.",
        backgroundColor = "F2E0CE",
        borderColor = "AC6E2F",
        category = "Settings objects"
    },
    service = {
        name = "Service",
        text = "This class is a [[service]]. It is a top-level singleton which can be obtained with the [[API:Class/ServiceProvider/GetService|GetService]] method.",
        backgroundColor = "cae7ca",
        borderColor = "6eb96e",
        category = "Services"
    },
    RobloxSecurity = {
        text = "This member is intended for scripts created by ROBLOX and is not usable by players. Attempting to do so will cause an error.",
        backgroundColor = "F2CECE",
        borderColor = "AC2F2F",
        category = "RobloxSecurity members"
    },
    RobloxScriptSecurity = {
        text = "This member can only be used in CoreScripts. Attempting to use this member outside of a CoreScript will cause an error.",
        backgroundColor = "F2CECE",
        borderColor = "AC2F2F",
        category = "RobloxScriptSecurity members"
    },
    WritePlayerSecurity = {
        text = "This member is intended for scripts created by ROBLOX and is not usable by players. Attempting to do so will cause an error.",
        backgroundColor = "F2CECE",
        borderColor = "AC2F2F",
        category = "WritePlayerSecurity members"
    },
    LocalUserSecurity = {
        text = "This member cannot be used in scripts, but is usable in the [[command bar]].",
        backgroundColor = "F2E0CE", --would a color somewhere between the two other security levels' be more appropriate?
        borderColor = "AC6E2F",
        category = "LocalUserSecurity members"
    },
    PluginSecurity = {
        text = "This member cannot be used in scripts, but is usable in the [[command bar]] and [[plugin]]s.",
        backgroundColor = "F2E0CE",
        borderColor = "AC6E2F",
        category = "PluginSecurity members"
    },
    RobloxPlaceSecurity = {
        text = "This member cannot be used in scripts, but is usable in the [[command bar]] and [[plugin]]s.",
        backgroundColor = "F2E0CE",
        borderColor = "AC6E2F",
        category = "RobloxPlaceSecurity members"
    },
    ConsoleOnly = {
        name = "Xbox only", -- TODO: Change this to "Console only" if ROBLOX expands to other console platforms.
        text = "This only exists in the API while the user is on an Xbox One.",
        backgroundColor = "cae7ca",
        borderColor = "6eb96e",
    },
    deprecated = {
        name = "'''<del>Deprecated</del>'''",
        text = "This item is deprecated. Do not use it for new work.",
        backgroundColor = "FFF5FA",
        borderColor = "AC2F6E",
        category = "Deprecated members"
    },
    readonly = {
        name = "Read-only",
        text = "This property can only be read from. Attempting to write to it will cause an error.",
        backgroundColor = "F2F2CE",
        borderColor = "ACAC2F",
        category = "Read-only members"
    },
    writeonly = {
        name = "Write-only",
        text = "This property can only be written to. Attempting to read from it will cause an error.",
        backgroundColor = "F2F2CE",
        borderColor = "ACAC2F",
        category = "Write-only members"
    },
    hidden = {
        name = "Hidden",
        text = "This item is not shown in the [[object browser]]. It is likely not intended for widespread use. Expect problems and changes.",
        backgroundColor = "FFFAF5",
        borderColor = "AC2F2F",
        category = "Hidden members"
    },
    library = {
        name = "Library",
        text = "This is documentation for a library. It is a built-in Lua-based library that can be retrieved using [[LoadLibrary]].",
        backgroundColor = "cae7ca",
        borderColor = "6eb96e",
        category = "Libraries"
    },
    useDot = {
        name = "Warning",
        text = "When calling this function, you should use a '''.''' to index it, rather than a ''':'''",
        backgroundColor = "FFC3C3",
        borderColor = "FF0000",
    }
}
 
local function sortedKeys(t,s)
    local sorted = {}
    assert(t, debug.traceback())
    for i in pairs(t) do
        sorted[#sorted + 1] = i
    end
    table.sort(sorted,s)
    return sorted
end
 
local function sortedPairs(t,s)
    --like pairs, but keys are ordered
    local sorted = sortedKeys(t,s)
    local i = 1
    return function()
        local k = sorted[i]
        i = i + 1
        return k, t[k]
    end
end
 
 
local function defaultArgumentString(arg,typeObj) -- Creates a string specifying the default argument
    if typeObj.type and typeObj.optional then
        return ' = nil'
    end
 
    if not arg.Default then
        return ''
    end
 
    if arg.Type == 'string' then
        return ' = ' .. string.format('%q', arg.Default)
    else
        return ' = ' .. arg.Default
    end
end
 
local function memberLink(class, member)
    return ("[[API:Class/%s/%s|%s]]"):format(class, member, member)
end
 
local function nonEmpty(s)
    if not s then return end
    if s:match("^{{{.*}}}$") then return end
    if s ~= "" then return s end
end
 
 
local function shouldHide(member,filteredTags)
    return _I.keys(member.tags):any(function(tag)
        return filteredTags[tag] == true
    end)
end
 
local function shouldToggleHide(member,class,filteredTags)
    local filteredTags = filteredTags or {}
    local allowPluginSecurity = false
    if class then
        allowPluginSecurity = customTags:ClassHasTag(class,"pluginLevel")
    end 
    for tag,val in pairs(member.tags) do
        if (allowPluginSecurity and tag == "PluginSecurity") or filteredTags[tag] == false then
            return false
        end
        local doesMatch = 
            string.match(tag, "hidden") or
            string.match(tag, "deprecated") or
            string.match(tag, "Security") or
            string.match(tag, "backend")
 
        if doesMatch and val then
            return true
        end
    end
end
 
local function unimportantMember(member)
    return _I.keys(member.tags):any(function(tag)
        return string.match(tag, "Security") or tag == "deprecated"
    end)
end
 
--[[
Takes a typename and a typearg string, and constructs a best-guess paramterized
type object, suitable for passing into rbxTypes.link
]]
local genericTypes = {Object=1,Instance=1,Array=1,Dictionary=1,Function=1,Tuple=1}
local function augmentType(typename, argString)
    local typeObj = {type=typename}
    if argString then
        local frame = mw.getCurrentFrame()
        argString = frame:preprocess(argString)
        typeObj.args = rbxTypes.parse(argString)
        typeObj.grouping = '<'
 
        if typeObj.type == 'Tuple' and #typeObj.args == 1 and typeObj.args[1] then
            typeObj = typeObj.args[1]
        elseif typeObj.type == 'Variant' and #typeObj.args == 1 then
            typeObj = {optional = true, type = typeObj.args[1].type}
        elseif typeObj.type == 'Variant' then
            local optional, args = 0, {}
            for k,v in pairs(typeObj.args) do
                if v.type == "nil" then
                   optional = optional + 1
                else
                    table.insert(args,v)
                end
            end
            if #args == 1 then
                typeObj = {optional = true, type = args[1].type}
            elseif optional > 0 then
                typeObj = {optional = true, type = 'Variant', args = args, grouping = '<'}
            end
        elseif (typeObj.type == 'Instance' or typeObj.type == 'Object') and #typeObj.args then
            --[[_I(typeObj.args):each(function(arg)
                if arg.type:sub(1,6) ~= 'Class/' then
                    error(("Specialization of instance<> must start with Class/: %q does not"):format(arg.type))
                end
            end)]]
            if #typeObj.args > 1 then
                typeObj = { type = 'Variant', grouping = '<', args = typeObj.args }
            else
                typeObj = typeObj.args[1]
            end
        end
    end
 
    if typeObj.type == 'Objects' then
        typeObj.type = 'Array'
        if not typeObj.args then
            typeObj.args = rbxTypes.parse('Instance')
            typeObj.grouping = '<'
        end
    elseif typeObj.type == 'Map' then
        typeObj.type = 'Dictionary'
        if not typeObj.args then
            typeObj.args = rbxTypes.parse('String,Variant')
            typeObj.grouping = '<'
        end
    end
 
    if genericTypes[typeObj.type] and not typeObj.args then
        typeObj.incomplete = true
    end
 
    return typeObj
end
 
-- p.test() on edit page
function api.test()
    local typeObj = augmentType("Variant","int,nil,string")
    for k,v in pairs(typeObj) do
        mw.log(k,'=',v)
        if type(v) == "table" then
            for a,b in pairs(v) do
               mw.log("\t",a,'-',b.type)
            end
        end
    end
    return rbxTypes.link(typeObj) 
end
 
-- Load the full type of a member or argument object from the dump
local function getType(obj, args)
    if obj.ValueType then
        return augmentType(obj.ValueType, args['TypeArgs.value'])
    elseif obj.ReturnType then
        return augmentType(obj.ReturnType, args['TypeArgs.return'])
    elseif obj.Type then
        return augmentType(obj.Type, args['TypeArgs.'..obj.Name])
    end
end
 
local function lookUpMemberType(className,memberName)
	-- Used if member page doesn't define a MemberType.
	local class = apiData.Classes[className]
	local memberTypes = {"Properties","Functions","YieldFunctions","Callbacks","Events"}
	for _,memberType in pairs(memberTypes) do
        local member = class[memberType][memberName]
        if member then
            return member.type
		end
	end
	return "Unknown"
end
 
-- Load the contents of the member information page, given a member object from the dump
local function loadMemberData(member)
    local success, memberpage = pcall(function() return mw.title.new("Class/"..member.Class.."/"..member.Name, "API"):getContent() end)
    if not success    then return {}, "expensive" end -- too many expensive function calls
    if not memberpage then return {}, "absent"    end
 
    -- Let's see if we can "merge" several properties to one page
    -- Basically, if a page has as source: API:Class/CLASS/MEMBER
    -- we assume it's the same property, but the main one
    -- (For Glue/F1 it would be: API:Class/Glue/F0)
    -- That would get the description of F0 instead of F1
    -- While we still keep the feature that when someone
    -- goes to F1, he'll get the information of F0
    -- (With edited names if {{SUBPAGENAME}} is properly used)
    local deferred = memberpage:match("^{{(API:Class/%w+/[%w()]+)}}$")
    if not deferred then
        deferred = memberpage:match("^#[Rr][Ee][Dd][Ii][Rr][Ee][Cc][Tt] %[%[(API:Class/%w+/[%w()]+)%]%]")
    end
    if deferred then
        success, memberpage = pcall(function() return mw.title.new(deferred):getContent() end)
        if not success    then return {}, "expensive" end
        if not memberpage then return {}, "absent"    end
    end
 
    local memberTemplate = templateParser.byName(memberpage, 'APIMemberPage')
    if not memberTemplate then return {}, "unparseable" end
 
    return memberTemplate.args, nil
end
 
local function fixDescription(t)
    if not t then return end
    if t:match("^{{{.*}}}$") then return end
    t = t:gsub("[%.%s]+$","")
    if t == "" then return end
    return t.."."
end
 
local function generateMemberRow(forClass, members, member, category, dark, filteredTags)
    -- HACK: Before we check anything, we need to check a specific condition to prevent confusion.
    --       Value classes have a special version of the Changed event, which actually overrides the inherited Changed event from the Instance class.
    --       If we run into this specific case, we need to just skip this member.
 
    local isValueClass = forClass:match("Value$") -- If the class name has "Value" at the end, it is a value class.
    if isValueClass and member.Class == "Instance" and member.Name == "Changed" then
        return
    end
 
    local uppercasedName = member.Name:gsub("^[a-z]", string.upper)
    -- See if it's a member specific to this class. Used later for categorising
    local ownMember = forClass == member.Class
    local strikeThrough = member.tags["deprecated"] == true
    local clonedMember do
        if member.tags.deprecated and member.Name:match("^[a-z]") then
            local lowerName = member.Name:lower()
            if member.Name == lowerName then 
                -- There are a few specific instances where more than just the first character isn't capitalized in a member name
                -- (for instance: BodyAngularVelocity/angularvelocity)
                for _,m in pairs(members) do
                    if m ~= member and m.Name:lower() == lowerName then
                        clonedMember = m
                        break
                    end
                end
            end
            if not clonedMember then
                clonedMember = members[uppercasedName]
            end
        end
    end
    -- In case it's deprecated, and the ReflectionMetadata looks like "Deprecated. Use WHATEVER Instead", also just print the name
    local replacedMember = member.tags.deprecated and (member.ReflectionMetadataSummary or ""):match("Deprecated")
    replacedMember = (member.ReflectionMetadataSummary or ""):match("Use (%w+)%(?%)? instead") or replacedMember
    replacedMember = members[replacedMember] or replacedMember ~= nil
 
    -- load the member page contents for non-duplicates
    local noMemberPageReason
    local memberData = {}
    if clonedMember then
        memberData, noMemberPageReason = loadMemberData(clonedMember)
        noMemberPageReason = noMemberPageReason or 'cloned'
    elseif type(replacedMember) == "table" then
        -- With the '~= nil' when replacedMember is declared, it'll only be a string in case 'Use ... instead" is found
        -- Now we can use the same logic for clonedMember, loading the replaced member's data instead
        memberData, noMemberPageReason = loadMemberData(replacedMember)
        noMemberPageReason = noMemberPageReason or 'replaced'
    else
        memberData, noMemberPageReason = loadMemberData(member)
    end
 
    -- Print the returned type if given (doesn't apply for events)
    local typeContent = ""
    local memberType = getType(member, memberData)
    if memberType then
        typeContent = rbxTypes.link(memberType, true)
    end
 
    typeContent = ('<span style="text-align: right; padding-right:5px;">%s</span>'):format(typeContent)
 
    -- Print the linkified name
    local nameContent = '<span style="display: block; text-indent: -2em; padding-left: 2em">'
    nameContent = nameContent .. (strikeThrough and "<del>%s</del>" or "%s"):format(
        (strikeThrough and ((clonedMember or replacedMember) and member.Name)) or memberLink(member.Class, member.Name)
    )
 
    -- Print arguments
    if member.Arguments then
        -- for some reason ipairs fails on member.Arguments, hence `.values()`
 
        local argContents = _I(member.Arguments):map(function(arg)
            local typeObj = getType(arg, memberData)
 
            return (' <span style="white-space: nowrap">%s %s%s</span>'):format(
                rbxTypes.link(typeObj),
                arg.Name, defaultArgumentString(arg,typeObj)
            )
        end)
 
        nameContent = nameContent .. ' ('..argContents:join(',')..' )'
    end
 
    -- Print all tags (deprecated, LocalUserSecurity, ...)
    for tag,val in sortedPairs(member.tags) do
        if tag ~= "dontUseDot" and val then
            local Explanation = tagDescriptions[tag] == nil and "No description available." or tagDescriptions[tag].text:gsub("([%[%]]+)", "")
            nameContent = nameContent..' <span style="color:gray;" class="explain" title="' .. Explanation .. '">[' .. tag .. ']</span>'
        end
    end
    nameContent = nameContent.."</span>"
 
    -- Print the description if available, or "Documentation missing."
    -- HACK: to conserve expensive function calls, deprecated members with lowercase names have a description autogenerated if a member with the uppercase name exists
    local description
    if clonedMember or type(replacedMember) == "table" then
        description = "Deprecated in favor of "..memberLink(member.Class, (clonedMember or replacedMember).Name).."."
    else
        if noMemberPageReason ~= 'expensive' then
            if noMemberPageReason and not replacedMember and ownMember then -- Let's not categorise stuff that isn't important
                -- (this way, "Category:API class pages with missing members" shows pages with members that really need more information fast)
                if not unimportantMember(member) then
                    nameContent = nameContent..("[[Category:API class pages with missing important members|%s]]"):format(member.Class)
                end
                nameContent = nameContent..("[[Category:API class pages with missing "..(strikeThrough and "deprecated " or "").."members|%s]]"):format(member.Class)
            end
        end
        description = mw.getCurrentFrame():preprocess(
            fixDescription(memberData["DescriptionShort"])
            or fixDescription(memberData["Description"])
 
            -- could not parse the member page
            or (noMemberPageReason == 'unparseable' and '<span style="color: red">Malformed member page!</span>')
 
            --better fallback than an error messsage
            or (member.ReflectionMetadataSummary and
                ('<em title="from ReflectionMetadataSummary">%s</em>'):format(member.ReflectionMetadataSummary))
 
            -- hit expensive function limit
            or (noMemberPageReason == 'expensive' and '<span style="color: red">Too many members!</span>')
 
            -- there just wasn't a page and we have no RMd summary
            or (noMemberPageReason == 'absent' and '<span style="color:gray">No documentation found.</span>')
 
            -- there was a page but no description found on it
            or '<span style="color: gray">Documentation incomplete.</span>'
        ):gsub("$DESCRIPTION_SHORT",memberData.DescriptionShort or "*NO SHORT DESCRIPTION AVAILABLE*")
    end
 
    local hidetext = ''
    if shouldToggleHide(member,forClass,filteredTags) then
        hidetext = ' class="memberhidden' .. category .. '"'
    end
 
    local style = dark and "background-color:#f8f8f8;"
    if shouldToggleHide(member,forClass,filteredTags) then
        style = (style or "").."height:0px"
    end
    local start = style and ('<tr style="%s">\n'):format(style) or '<tr>\n'
 
    local memberContent = {}
    memberContent.studiohide = start..'\n<td style="width:150px;text-align:right;vertical-align:top;">\n<div' .. hidetext .. '>' .. typeContent .. '</div>\n</td>\n<td style="vertical-align:top;white-space:normal;width:400px;">\n<div' .. hidetext .. '>' .. nameContent .. '</div>\n</td>\n<td style="white-space:normal;vertical-align:top;padding-left:7px;">\n<div' .. hidetext .. '>' .. description .. '</div>\n</td>\n</tr>'
    memberContent.studioshow = '<div>\n<div'.. hidetext ..' style="padding-bottom:10px;white-space:normal;">' .. typeContent .. nameContent .. '<br>\n<span style="width:100%;display:inline-block;white-space:normal;padding-left:10px;">'..description..'</span>\n</div>\n</div>'
 
	memberContent.desc = description:gsub("|/RMD|","")
    return memberContent
end
 
local function superclasses(className)
    -- for iterator to loop over a class and its superclasses
    local class = apiData.Classes[className]
    return function()
        if class then
            local cur = class
            class = apiData.Classes[class.Superclass]
            return cur.Name, cur
        end
    end
end
 
local imageIndexUrl = "File:ExplorerImageIndex%d.png"
 
local function imageIndex(className)
    local override = customTags:GetOverrideIcon(className);
    if override then
        if override:find("File:") then
            return override
        else
            return imageIndex(override)
        end
    else
        if customTags:ClassHasTag(className,"abstract") then
            return "File:Object Icon.png"
        elseif customTags:ClassHasTag(className,"settings") then
            return imageIndex("Configuration")
        elseif customTags:ClassHasTag(className,"internal") then
            return "File:InternalClass.png";
        else
            local result 
            for name, class in superclasses(className) do
                result = class.ExplorerImageIndex
                if result then 
                    return imageIndexUrl:format(result)
                end
            end
            return "File:Object blank Icon.png"
        end
    end
end
 
local italics = [[''%s'']]
local strikeThrough = "<del>%s</del>"
 
local function classLink(className,noCutOff,isPlural)
    local maxDisplayedLength = 30
    if noCutOff then
        maxDisplayedLength = 9999
    end
    local imageName = imageIndex(className)
    local displayedClassName = className
    if #className > maxDisplayedLength then
        displayedClassName = string.sub(className, 1, maxDisplayedLength - 3)
    end
    if isPlural then
        displayedClassName = displayedClassName.."s"
    end
    local class = apiData.Classes[className]
    local tags = class.tags
    if customTags:ClassHasTag(className,"abstract") then
        displayedClassName = italics:format(displayedClassName)
    end
    if customTags:ClassHasTag(className,"internal") or tags.deprecated then
        displayedClassName = strikeThrough:format(displayedClassName)
    end 
    return ('<span style="white-space:nowrap;display:inline-block;vertical-align:bottom">'
            ..'[[%s|link=API:Class/%s]]'
            ..'<span style="display:inline-block;vertical-align:middle;padding-left:2px">[[API:Class/%s|%s]]%s</span>'
          ..'</span>'):format(imageName, className, className, displayedClassName, (#className > maxDisplayedLength and "..." or ""))
end
 
local function filterHidden(list,filteredTags)
    local res = {}
    for k,v in pairs(list) do
        if not shouldHide(v,filteredTags) then
           res[k] = v
        end
    end
    return res
end
 
local function generateMemberTable(realClassName, class, memberType, blockedMembers, filteredTags)
    local started --Many classes don't have Callback members, don't generate a section for them
    local colCount = memberType == "Events" and 2 or 3 --number of <td>s in the table, for colspan
                                                       --events don't have a td for value or return type
    local ret,hasHide,inheriting,dark = ""
    for className, class in superclasses(realClassName) do
        local members = class[memberType]
 
        local alphebetizedPairs = sortedPairs(filterHidden(members,filteredTags))
 
        local filteredMembers,toggleMembers = {},{}
        for memberName,member in alphebetizedPairs do
            if not blockedMembers[className.."/"..memberName] then
                if shouldToggleHide(member,className,filteredTags) then
                    table.insert(toggleMembers,member)
                else
                    table.insert(filteredMembers,member)
                end
            end
        end
        for _, v in ipairs(toggleMembers) do
            table.insert(filteredMembers, v)
        end
        if #toggleMembers > 0 then hasHide = true end
        sortedB = nil
 
        if #filteredMembers > 0 then
            if not started then
                ret = '<h2 class="studioshow" style="font-size:16px;margin:0px;padding:0px;">'..memberType..'</h2>'
                    ..'<div style="width:auto;">'
                      ..'<h2 class="studiohide">'
                         ..memberType
                         ..'<span style="float:right; font-weight: normal; font-size: 50%">'
                           ..'<span class="hideclass"></span>'
                           ..' [<span class="mw-customtoggle-'..memberType:lower()..'" style="cursor:pointer;">toggle</span>]'
                         ..'</span>'
                      ..'</h2>'
 
                      ..'<div class="mw-collapsible" id="mw-customcollapsible-'..memberType:lower()..'" style="overflow-x:scroll;white-space:nowrap;width:100%;box-sizing:border-box;display:inline;">'
                started = true
            end
 
            if inheriting then
                ret = ret..('<span style="color: gray; text-align: left; font-size: 0.8em;">Inherited from %s:</span>'):format(classLink(className))
            end
 
            local studioshow = '<div class="studioshow">\n'
            local studiohide = '<table class="studiohide">\n'
 
			local testthing = ""
 
            for _, member in ipairs(filteredMembers) do
                dark = not dark
                local content = generateMemberRow(realClassName, members, member, memberType, dark, filteredTags)
                if content then
                    studioshow = studioshow .. content.studioshow .. ([[<span style='visibility:hidden'>|RMD member="API:Class/%s/%s"|%s|/RMD|</span>]]):format(member.Class,member.Name,content.desc) .. "\n"
                    studiohide = studiohide ..  content.studiohide .. "\n"
                end
            end
            studioshow = studioshow .. '</div>'
            studiohide = studiohide .. '</table>'
            ret = ret .. studioshow .. studiohide
        end
        inheriting = true
    end
 
    if started then
        ret=ret..'</div></div>'
    end
 
    if hasHide then
        ret = string.gsub(ret, '<span class="hideclass"></span>', '<span class="showhiddenmembers">memberhidden' .. memberType .. '</span>')
    end
 
    return ret
end
 
local function generateTagBoxes(tags)
    local ret = ""
    for tag,val in sortedPairs(tags) do
        if val then
            local tagDesc = tagDescriptions[tag]
            if tagDesc then
                ret = ret..('<div class="box shrink" style="background-color: #%s; border-color: #%s; text-align: center;"><span style="float: left;">%s:</span> %s</div>\n'):format(
                    tagDesc.backgroundColor,
                    tagDesc.borderColor,
                    tagDesc.name or tag,
                    tagDesc.text
                )
                if tagDesc.category then
                    ret = ret .. " [[Category:"..tagDesc.category.."]]"
                end
            end
        end
    end
    return ret
end
 
local plurals = { -- no sense in having this be redefined every time it's called
        Function = "Functions",
        YieldFunction = "YieldFunctions",
        Callback = "Callbacks",
        Property = "Properties",
        Event = "Events"
    }
 
local function hasMemberOfType(className,memberType,noInheritance)
    local class = apiData.Classes[className]
    local type = class[memberType]
    if type then
        local n = next(type) -- If there are members, this won't return nil.
        if n ~= nil then
            return true
        else -- Okay, do we inherit anything?
            local superClass = class.Superclass
            if not (superClass == "Instance" and noInheritance) then
                return hasMemberOfType(superClass,memberType,noInheritance)
            end
        end
    end
    return false
end
 
local function getMembersByName(name)
    local results = {}
    for class_name, class in pairs(apiData.Classes) do
        for _, member_group in pairs(plurals) do
            if class[member_group][name] then
                table.insert(results, class[member_group][name])
            end
        end
    end
    return results
end
 
local function getMemberByName(class, name)
    for member_type, member_group in pairs(plurals) do
        if class[member_group][name] and not class[member_group][name].tags.deprecated then
            return class[member_group][name], member_type
        end
    end
end
 
local function getMembersByNameCI(name,match)  -- case insensitive
    local results,byClass = {},{}
    for _, member in ipairs(getMembersByName(name:gsub("^%l", string.upper))) do
        table.insert(results, member)
        byClass[member.Class] = member
    end
    for _, member in ipairs(getMembersByName(name:gsub("^%u", string.lower))) do
        if not byClass[member.Class] then
            table.insert(results, member)
        end
    end
    return results
end
 
local function getMembersByNameMatch(name)
    local results = {}
    for class_name, class in pairs(apiData.Classes) do
        for _, member_group in pairs(plurals) do
            for property,info in pairs(class[member_group]) do
                if property:match(name) then
                    table.insert(results, class[member_group][property])
                end
            end
        end
    end return results 
end
 
local function referencesType(member, typename)
    return (
        member.ReturnType == typename or
        member.ValueType == typename or
        member.Arguments and _I(member.Arguments):any(function(arg)
            return arg.Type == typename
        end)
    )
end
 
local function getMembersByReferencedType(typename)
    --[[
    Returns a list of definite matches, followed by a list of likely 
 
    TODO: use the second result
 
    Alternatively:
            ^ We would need a module that stores for every member, which types (including classes) arguments/returned value are.
              That way, we don't need to load the member pages for TypeArgs.
              The only problem is, filling that module with data needs loading a lot of member pages.
              Unless it's updated manually (or from a bot), it won't work well with the expensive function limit.
              (There are too much members to load at once, and we can't edit the module from a member page when loaded)
    ]]
 
    local directResults = {}
    local possibleResults = {}
 
    for class_name, class in pairs(apiData.Classes) do
        for _, member_group in pairs(plurals) do
            for name, member in pairs(class[member_group]) do
                if referencesType(member, typename) then
                    table.insert(directResults, member)
                elseif referencesType(member, 'array') or referencesType(member, 'tuple') then
                    -- we'd need to expand tuple/array to find out
                    table.insert(possibleResults, member)
                elseif member.Arguments ~= nil and _I(member.Arguments):any(function(arg) return arg.Name:lower() == typename:lower() end) then
                    -- arguments with the same name as a class are common
                    table.insert(possibleResults, member)
                end
            end
        end
    end
    return directResults, possibleResults
end
 
local function tryToInheritCustomTag(className,tag)
    local classCustomTags = customTags:GetTags(className)
    if not customTags:ClassHasTag(className,tag) then
        for superclass in superclasses(className) do
            if customTags:ClassHasTag(superclass,tag) then
                classCustomTags[tag] = true
                break
            end
        end
    end
end
 
local function doesHaveArg(args,arg)
    local val = args[arg]
    return val ~= nil and not (val == "{{{"..arg.."}}}" or val == "")
end
 
local function hasAnyOfTheseArgs(args,...)
    for _,arg in pairs{...} do
        local has = doesHaveArg(args,arg)
        if has then
            return true
        end
    end
    return false
end
 
function api.generateClassLink(frame)
    local className = frame.args[1]
    local isPlural = frame.args[2] == "plural"
    return classLink(className,true,isPlural)
end
 
function api.findFirstMemberPage(frame)
    local memberName = frame.args[1]
    local member = getMembersByNameCI(memberName)[1]
    if member then
        return frame:preprocess("API:Class/"..member.Class.."/"..member.Name)
    end
    return ""
end
 
function api.generateMemberPage(frame)
    local ret,className,memberName,memberType,description,descriptionShort = "", frame.args[1], frame.args[2], frame.args[3], frame.args[4], frame.args[5]
    local parentArgs = frame:getParent().args
 
    ret = ret .. '<h1 class="studioshow">' .. memberName .. '</h1>'
 
    local data = apiData and apiData.Classes
 
    data = data and data[className]
    if memberType == "{{{MemberType}}}" or memberType == "" then
        memberType = lookUpMemberType(className,memberName)
        if memberType == "Unknown" then
            return "Error: Unknown MemberType! This member may have been removed. [[Category:Pending removal]]"
        end
    end
 
    local isLibrary = (data.tags and data.tags.library ~= nil)
 
    local memberTypePlural = plurals[memberType]
    if not memberTypePlural then
        return "Error: memberType not valid - ["..memberType.."]"
    end
    data = data and data[memberTypePlural]
 
    if data and not data[memberName] and memberName:match('%b()') then
        local patt = memberName:gsub('%b()', '%w+')
        for k, v in pairs(data) do
            if k:match(patt) then
                data = v
                memberName = k
                -- TODO: template based on the wildcard
                break
            end
        end
    else
        data = data and data[memberName]
    end
 
    if not data then
        return ("%s:%s not found in the API Dump! Either check if the MemberType has been changed, or have an editor delete the page![[Category:Member not found in API dump|%s]][[Category:Pending removal]]"):format(
            className, memberName,
            className
        )
    end
 
    local sideTags = {}
 
    if isLibrary and data.tags then
        if not data.tags.dontUseDot then
            sideTags.useDot = true
        end
    end
 
    ret = ret..generateTagBoxes(data.tags or {})..generateTagBoxes(sideTags)
 
    local optionals = {}
    for _,v in pairs(parentArgs) do
        if v:find("MarkOptional:") then
            local str = v:gsub("MarkOptional%:(%w+)","%1")
            optionals[str:sub(1,#str-1)] = true
        elseif v:find("MarkNonOptional:") then
            local str = v:gsub("MarkNonOptional%:(%w+)","%1")
            optionals[str:sub(1,#str-1)] = false
        end
    end
 
 
    ret = ret..('<small>%s of %s</small><br/>'):format(memberType, classLink(className))
 
    local membersByName = getMembersByNameCI(memberName)
    if #membersByName > 1 then
        ret = ret..'<span class="studiohide"><small>There are members of the same name.'
        local pageName = memberName.." (disambiguation)"
        local disambig = mw.title.new(pageName)
        if not disambig.exists then
            pageName = memberName
            disambig = mw.title.new(pageName)
        end
        if disambig.exists then
            ret = ret..(' See the [[%s|disambiguation page]].'):format(pageName)
        end
        ret = ret..'</small></span><br/>'
    end
 
    --if descriptionShort then
    --    ret = ret..'<div class="studiohide">'..descriptionShort..'</div>'
    --end
 
    local hasArguments = data.Arguments and data.Arguments[1]
 
    if data.Arguments then
        local returnType = getType(data, parentArgs)
        if memberType:find("Event") then
            returnType = rbxTypes.parse("RBXScriptSignal")
        end
 
        ret = ret .. ('<pre>%s %s (%s)</pre>'):format(
            rbxTypes.link(returnType, true),
            memberName,
            hasArguments and (
                _I(data.Arguments):map(function(arg)
                    local typeObj = getType(arg, parentArgs)
                    if optionals[arg.Name] ~= nil then
                        typeObj.optional = optionals[arg.Name]
                    end                           
                    return ('\n    %s %s%s'):format(
                        rbxTypes.link(typeObj),
                        arg.Name,
                        defaultArgumentString(arg,typeObj)
                    )
                end)
                :join(',')
            ) .. '\n' or ''
        )
    end
 
    if hasArguments then
        ret = ret..'<span>'
        ret = ret .. '\'\'\'Parameters:\'\'\'<br/><ol style="list-style-type: none; margin-left: 2em;">'
        for k,v in pairs(data.Arguments) do
            local typeObj = getType(v, parentArgs)
            if typeObj.incomplete then
                ret = ret .. ('[[Category:Class members with unqualified types|%s]]'):format(typeObj.type)
            end
 
            if optionals[v.Name] ~= nil then
                typeObj.optional = optionals[v.Name]
            end
 
            ret = ret .. ('<li><em>%s</em>'):format(v.Name)
            ret = ret .. '<br /><ul style="list-style-type: none; list-style-image: none">'
            ret = ret .. '<li>Type: '..rbxTypes.link(typeObj)..'</li>'
 
            if not typeObj.args and typeObj.type == 'Tuple' then
                ret = ret .. ('[[Category:Class members with unqualified tuple types|%s]]'):format(className)
            end
 
            if v.Default then
                if rbxTypes.isEnum(v.Type) then
                    ret = ret .. ('<li>Defaults to: Enum.%s.%s</li>'):format(v.Type, v.Default)
                else
                    ret = ret .. ('<li>Defaults to: %s</li>'):format(v.Default)
                end
            elseif memberType ~= "Event" then
                if typeObj.type and typeObj.optional then
                    ret = ret .. '<li>Defaults to: nil</li>'
                else
                    ret = ret .. '<li>Required</li>'
                end
            end
            ret = ret .. '</ul>'
            ret = ret .. '</li>'
        end
        ret = ret..'</ol></span>'
    end
 
    if data.ReturnType then
        local typeObj = getType(data, parentArgs)
        local msg = memberType == "Callback" and "Expected result" or "Returns"
 
        ret = ret .. '<br/><span>'
        ret = ret .. "'''"..msg..":''' " .. rbxTypes.link(typeObj,true)
        ret = ret .. '</span>'
 
        if typeObj.incomplete then
            ret = ret .. ('[[Category:Class members with unqualified types|%s]]'):format(typeObj.type)
        end
    end
 
    if data.ValueType then
        local typeObj = getType(data, parentArgs)
 
        ret = ret .. '<br/><span>'
        ret = ret .. '\'\'\'Value Type:\'\'\' ' .. rbxTypes.link(typeObj)
        ret = ret .. '</span>'
 
        if typeObj.incomplete then
            ret = ret .. ('[[Category:Class members with unqualified types|%s]]'):format(typeObj.type)
        end
    end
 
    local hasDescription = true
    if description and description:match("%a") then
        description = description
    elseif data.ReflectionMetadataSummary then
        description = data.ReflectionMetadataSummary
    else
        hasDescription = false
        description = ("No Description Found [[Category:API member pages with missing documentation|%s]]"):format(memberName)
    end
 
    if descriptionShort then
        if description:find(descriptionShort) then -- Pages that repeat the short version of the description.
            ret = ret .. "[[Category:API pages that could be optimized]]"
        end
    end
 
 
    description = description:gsub("$DESCRIPTION_SHORT",descriptionShort or "")
 
   -- ret = ret .. '<span class="studioshow"><br/>\'\'\'Description:\'\'\' '..description..'</span>'
 
	ret = ret .. '<span class=""><br/>\'\'\'Description:\'\'\' '..description..'</span>'
 
    if (not description or #description == 0) and data.ReflectionMetadataSummary then
        description = data.ReflectionMetadataSummary
        ret = ret..data.ReflectionMetadataSummary.."\n"
    end
 
    if hasDescription and memberType ~= "Property" then
        -- If this member is described and its not internal, it should have an example available.
        local isInternal = customTags:ClassHasTag(className,"internal")
        if not isInternal then
            local tags = data.tags or {}
            if not tags.deprecated then   
                if not hasAnyOfTheseArgs(parentArgs,"Example","Tutorials") then
                    ret = ret .. ("[[Category:%s that need an Example/Tutorial reference]]"):format(memberTypePlural)
                end
            end
        end
    end
 
    -- add to categories, sorting by member name. We probably want it in both?
    ret = ret .. ("[[Category:Class members|%s]]"):format(memberName)
    ret = ret .. ("[[Category:Class %s|%s]]"):format(memberTypePlural:lower(), memberName)
 
    -- check for disambig
    if #membersByName > 1 then
 
        -- first, check if Member (disambiguation) exists. If it does not,
        -- prompt creation of a redirect (see WP:INTDAB for rationale)
        local dmName = memberName.." (disambiguation)"
        local dmPage = mw.title.new(dmName)
        if not dmPage.exists then
            ret = ret .. ("[[Category:Members without disambiguation redirect|%s]]"):format(memberName)
            ret = ret .. ("[[Category:Members without disambiguation redirect, by ambiguity|%s]]"):format(#membersByName..' '..memberName)
            ret = ret .. frame:preprocess(
                -- Use 'usershow' class to hide for logged out users
                ("<span class='usershow'>{{EmphasisBox|" ..
                    "Disambig redirect page does not exist - "..
                    "[{{fullurl:%s|action=edit&preload=Template:dmbox/preload-redirect&summary=auto+redirect+creation}} create it]" ..
                "|red}}</span>"):format(dmName, memberName)
            )
        end
 
        -- And now check that the page it redirects to exists
        local primaryPage = mw.title.new(memberName)
        local doesExist = primaryPage.exists
        local noExistanceReason = "does not exist"
        local noExistanceAction = "[{{fullurl:%s|action=edit&preload=Template:dmbox/preload-member&summary=auto+disambig+creation}} create it]"
        if doesExist then
            -- Verify this is a legitimate disambiguation page.
            local content = primaryPage:getContent()
            if (not content:find("{{dmbox}}")) and not content:find("{{disregard disambiguation}}") then
                doesExist = false
                noExistanceReason = "is improperly set up"
                noExistanceAction = "if you are an editor, copy the contents of [{{fullurl:Disambig_Format|action=render}} this page], and [{{fullurl:%s|action=edit&summary=manual+disambig+correction}} paste it as the contents of this page]."
            end
        end
        if not doesExist then
            ret = ret .. ("[[Category:Members without disambiguation page|%s]]"):format(memberName)
            ret = ret .. frame:preprocess(
                -- Use 'usershow' class to hide for logged out users
                ("<span class='usershow'>{{EmphasisBox|" ..
                    "Disambig page " .. noExistanceReason .. " - " .. noExistanceAction ..
                "|red}}</span>"):format(memberName, memberName)
            )
        end
 
        -- TODO: check the redirect is valid, and we haven't accidentally redirected to a tutorial page
    else
        local blockedTags = {"deprecated","unscriptable","RobloxSecurity","RobloxScriptSecurity","WritePlayerSecurity","RobloxPlaceSecurity","LocalUserSecurity"}
        local canProceed = true
        for _,tag in pairs(blockedTags) do
            if data.tags[tag] then
                canProceed = false
                break
            end
        end
        if canProceed then
            local basicRedirect = mw.title.new(memberName)
            if not basicRedirect.exists then
                ret = ret .. frame:preprocess(("<span class='usershow'>{{EmphasisBox|" ..
                        "Member redirect page does not exist - "..
                        "[{{fullurl:%s|action=edit&preload=Template:PreloadMemberRedirect&summary=auto+redirect+creation}} create it]" ..
                        "|red}}</span> [[Category:Members without search redirect]]"):format(memberName))
            end
        end
    end
 
    return ret
end
 
function api.generateClassPage(frame)
    local ret,className,description = "",frame.args[1],frame.args[2]
    local parentArgs = frame:getParent().args
    local experimental = (parentArgs.Experimental ~= nil)
 
    local data = apiData and apiData.Classes
    data = data and data[className]
 
    if not data then return "Not found in the API Dump!  [[Category:Class not found in API dump]]" end
 
    --ret = ret .. '<h1 class="studioshow">' .. className .. '</h1>'
 
    tryToInheritCustomTag(className,"noInheritance")
    tryToInheritCustomTag(className,"noChildrenModifiers")
    tryToInheritCustomTag(className,"internal")
 
    local classCustomTags = customTags:GetTags(className)
    local blockedMembers = {}
    local filteredTagBoxes = {}
    local filteredTags = 
    {
        RobloxScriptSecurity = true;
        RobloxSecurity = true;
        WritePlayerSecurity = true;
    }
 
    local function processCmd(cmd,arg)
        if cmd == "BlockInheritance" then
            local class = apiData.Classes[arg]
            for _,memberType in pairs(plurals) do
                for member in pairs(class[memberType]) do
                    local key = arg.."/"..member
                    if blockedMembers[key] == nil then -- If its defined as false, we shouldn't block it. If its nil, we should.
                        blockedMembers[key] = true
                    end
               end
           end
        elseif cmd == "BlockMember" then
            blockedMembers[arg] = true
        elseif cmd == "UnblockMember" then
            blockedMembers[arg] = false
        elseif cmd == "AddCustomTag" then
            classCustomTags[arg] = true
        elseif cmd == "DontShowTagBoxFor" then
            filteredTagBoxes[arg] = true
        elseif cmd == "DontFilterMembersWithTag" then
            filteredTags[arg] = false
        end
    end
 
    for _,param in ipairs(frame:getParent().args) do
        local noWhiteSpace = param:gsub("%s","")
        local cmd,arg = noWhiteSpace:match("(%w+):(.+)")
        if cmd and arg then
            processCmd(cmd,arg)
        end
    end
 
    if classCustomTags.noInheritance then -- Blocks all inheritance from the Instance class.
        processCmd("BlockInheritance","Instance")
        if hasMemberOfType(className,"Properties",true) then -- If this class has properties that aren't inherited from Instance, we'll let Instance/Changed slide.
            processCmd("UnblockMember","Instance/Changed")
        end
    elseif classCustomTags.noChildrenModifiers then -- Classes that shouldn't need to manage children.
        local membersToBlock = {"ClearAllChildren","FindFirstChild","GetChildren","IsAncestorOf","WaitForChild","ChildAdded","ChildRemoved",
                                                 "DescendantAdded","DescendantRemoving","children","findFirstChild","getChildren","childAdded"}
        for _,member in pairs(membersToBlock) do
            processCmd("BlockMember","Instance/"..member)
        end
    elseif classCustomTags.service then -- If its a service, we should remove anything related to cloning/removing it
        processCmd("BlockMember","Instance/Clone")
        processCmd("BlockMember","Instance/Destroy")
        processCmd("BlockMember","Instance/clone")
        processCmd("BlockMember","Instance/remove")
        processCmd("BlockMember","Instance/AncestryChanged")
        processCmd("BlockMember","Instance/Parent")
    end
 
    if classCustomTags.pluginLevel then
        processCmd("DontFilterMembersWithTag","PluginSecurity")
    end
 
    for filteredTagBox in pairs(filteredTagBoxes) do
        if data.tags[filteredTagBox] then
            data.tags[filteredTagBox] = nil
        end
        if classCustomTags[filteredTagBox] then
            classCustomTags[filteredTagBox] = nil
        end
    end
 
    ret = ret..generateTagBoxes(classCustomTags)..generateTagBoxes(data.tags)    
 
    -- print inheritance
    local inheritance
    if data.tags.library then
        inheritance = classLink(className,true)
    elseif data.Superclass then
        inheritance = _I(superclasses(className))
            :map(function(name)
                return classLink(name,true)
            end)
            :join(" : ")
    else
        inheritance = 'This class is the highest class and has no base class (though technically it does inherit the [[ROOT|<<<ROOT>>>]] class)'
    end
    ret = ret..'<p><small>'..inheritance..'</small></p>\n'
 
    -- print description
    if description and string.match(description, "%a") then
        ret = ret..description
    elseif data.ReflectionMetadataSummary then
        ret = ret..data.ReflectionMetadataSummary
    else
        ret = ret..("Documentation missing. [[Category:API class pages with missing documentation|%s]]"):format(className)
    end
    ret = ret..'<br/>\n' --\n needed if last line of description is e.g. a list in wiki syntax
 
 
 
    -- print members
    for _, memberType in ipairs{ "Properties", "Functions", "YieldFunctions", "Callbacks", "Events" } do
        ret = ret..generateMemberTable(className, data, memberType,blockedMembers, filteredTags).."\n"
    end
 
    if --[[experimental and]] data.Subclasses then
        local names = sortedKeys(data.Subclasses)
        local first, studio, dark = true, ""
        --local show = '<tr style="%s"><td style="width:150px;"/><td style="text-align:left;vertical-align:top;"><div>[[API:Class/%s|%s]]<br/></div></td></tr>'
        local show = '<tr style="%s"><td style="text-align:right;vertical-align:top;">%s</td><td style="text-align:left;vertical-align:top;">%s</td></tr>'
        local hide = '<div class="studioshow" style="padding-bottom:10px;white-space:normal;"><span style="width:100%%;display:inline-block;white-space:normal;padding-left:10px;">[[API:Class/%s|%s]]</span></div>'
        for k,v in pairs(names) do
            if true --[[ hasContent(data.Subclasses[v]) ]] then
                if first then
                  ret,first = ret..'<h2>Inherited Classes</h2><table class="studiohide">'
                end
                local link = ("[[API:Class/%s|%s]]"):format(v,v)
                local img = ('[[%s|link=API:Class/%s]]'):format(imageIndex(v),v)
                ret = ret..show:format(dark and 'background-color:#f8f8f8;' or '',img,link)
                --ret = ret..show:format(dark and 'background-color:#f8f8f8;' or '',v,v)
                dark, studio = not dark, studio..hide:format(v,v)
            end
        end
        if not first then ret = ret.."</table>"..studio end
    end
 
    if experimental then
        ret = ret .. "\n\n\n'''Filtered Tag Debug Info:'''"
        for filteredTag,isFiltered in pairs(filteredTags) do
            ret = ret .. "\n\n" .. filteredTag .. " = " .. tostring(isFiltered)
        end
    end
 
    ret = ret ..  ([[<span style='visibility:hidden'>|RMD member="API:Class/%s"|%s|/RMD|</span>]]):format(className,description:gsub("\n"," "))
    ret = ret .. ("[[Category:Classes|%s]]"):format(className) .. "<br/>"
 
    return ret
end
 
function bulletedMemberList(members)
    return ('<ul>%s</ul>'):format(
        _I(members):map(function(member)
            local ret = ''
            local memberData, noMemberPageReason = loadMemberData(member)
 
            local description = (
                nonEmpty(memberData["DescriptionShort"])
                or nonEmpty(memberData["Description"])
            )
 
            local rType = getType(member, memberData)
 
            if rType then
                ret = ret .. rbxTypes.link(rType,true) .. " "
            end
            local nameLink
            if member.tags["deprecated"] then
                if description then
		    description = description:gsub("$DESCRIPTION_SHORT",memberData.DescriptionShort or "*NO SHORT DESCRIPTION AVAILABLE*")
                    nameLink = ('<span title="%s">%s.%s</span>'):format(
                        mw.text.nowiki(description:gsub("%[%[.-|(.-)%]%]", "%1"):gsub("%[%[(.-)%]%]", "%1")),
                        member.Class, member.Name
                    )
                else
                    nameLink = ('%s.%s'):format(
                        member.Class, member.Name,
                        member.Class, member.Name
                    )
                end
            else
                if description then
		    description = description:gsub("$DESCRIPTION_SHORT",memberData.DescriptionShort or "*NO SHORT DESCRIPTION AVAILABLE*")
                    nameLink = ('[[API:Class/%s/%s|<span title="%s">%s.%s</span>]]'):format(
                        member.Class, member.Name,
                        mw.text.nowiki(description:gsub("%[%[.-|(.-)%]%]", "%1"):gsub("%[%[(.-)%]%]", "%1")),
                        member.Class, member.Name
                    )
                else
                    nameLink = ('[[API:Class/%s/%s|%s.%s]]'):format(
                        member.Class, member.Name,
                        member.Class, member.Name
                    )
                end
            end
            if member.tags["deprecated"] then
                nameLink = ('<del>%s</del>'):format(nameLink)
            end
            ret = ret .. nameLink
            if member.Arguments then
                ret = ret .. " (" .. (
                    _I(member.Arguments):map(function(arg)
                        return rbxTypes.link(getType(arg, memberData)) .. " " .. arg.Name
                    end)
                    :join(', ')
                ) .. ")"
            end
 
            return ("<li>%s</li>"):format(ret)
        end)
        :join('\n')
    )
end
 
function api.generateEnumPage(frame)
    local ret,enumName,description = "",frame.args[1],frame.args[2]
 
    local data = apiData.Enums[enumName]
    if not data then return "Not found in the API Dump!" end
 
    -- print enum name for studio
    ret = ret .. '<h1 class="studioshow">' .. enumName .. '</h1>'
 
    -- print description
    ret = ret .. description .. '\n'
 
    -- print all members
    ret = ret..'<div class="mw-collapsible" style="width:auto;"><h2>Enums</h2><div class="mw-collapsible-content"><table>'
    ret = ret..'<tr><th style="padding:5px;">Name</th><th style="padding:5px;">Value</th>'
    local values = {}
    for name, enumItem in pairs(data.EnumItems) do
        values[#values+1] = { Name = name, Value = enumItem.Value }
    end
    table.sort(values, function(a, b) return a.Value < b.Value end)
    local parent = frame:getParent() --retrieve the arguments to Template:APIEnumPage
    local hasItemDescs = false
    for arg in pairs(parent.args) do
        if arg:find("Description.") then
             hasItemDescs = true
             break
        end
    end
    if hasItemDescs then
        ret = ret ..'<th style="padding:5px;">Description</th></tr>'
    end
    for _, item in ipairs(values) do
        local description = (not hasItemDescs) and "" or (parent.args["Description."..item.Name] or '<span style="color:gray;">No description available[[Category:API enum pages with missing item descriptions]]</span>')
        ret = ret..('<tr><td style="padding:5px;">%s</td><td style="padding:5px;">%s</td><td style="padding:5px;">%s</td></tr>'):format(item.Name, item.Value, description)
    end
    ret = ret.."</table></div></div>"
 
    local usedIn = getMembersByReferencedType(enumName)
    ret = ret .. '<h2>Referenced by</h2>'
    if #usedIn == 0 then
        ret = ret .. "''This enum does not appear to be referenced in any API member. " ..
            ("This may be a documentation error, so check [[Special:WhatLinksHere/API:Enum/%s|what links here]] for more information.''"):format(enumName)
    else
        local isDeprecated = true
        for _,member in pairs(usedIn) do
            if not member.tags.deprecated then
                isDeprecated = false
                break
            end
        end
        if isDeprecated then
            local pageContent = mw.title.getCurrentTitle():getContent()
            if not pageContent:find("{{deprecated}}") then
                ret = frame:preprocess("{{deprecated}}")..ret
            end
        end
        ret = ret .. bulletedMemberList(usedIn)
    end
 
    ret = ret .. ("[[Category:Enums|%s]]"):format(enumName)
    ret = ret .. ("[[Category:Enums by use count|%s]]"):format(#usedIn > 9 and '+' or #usedIn)
 
    return ret
end
 
function api.generateReferencedTypeSection(frame)
    local typename = frame.args[1]
    local usedIn = getMembersByReferencedType(typename)
    local ret = '<h2>Referenced by</h2>'
    if #usedIn == 0 then
        ret = ret .. "''This enum does not appear to be referenced in any API member. " ..
            ("This may be a documentation error, so check [[Special:WhatLinksHere/API:Enum/%s|what links here]] for more information.''"):format(enumName)
    else
        ret = ret .. bulletedMemberList(usedIn)
    end
    return ret
end
 
function api.generateReferencePage()
    local ret = mw.loadData("Module:API/data"),""
    local function scan(name,class,tab)
        ret = ret..string.rep("&emsp;",tab)..("[[API:%s|%s]]<br/>"):format(name,name)
        for k,v in pairs(class.Subclasses) do
            scan(k,v,tab+1)
        end
    end
    scan("Instance",apiData.Classes.Instance,0)
    return ret
end
 
function api.generateReferencePageAlpha()
    local ret = ""
    local function scan(name,class,tab)
        ret = ret..string.rep("&emsp;",tab)..("[[API:%s|%s]]<br/>"):format(name,name)
        alphabetizedKeys = {}
        for k,v in pairs(class.Subclasses) do
            table.insert(alphabetizedKeys, k)
        end
        table.sort(alphabetizedKeys)
        for i,n in ipairs(alphabetizedKeys) do
            scan(n, class.Subclasses[n], tab+1)
        end
    end
    scan("Instance",apiData.Classes.Instance,0) return ret
end
 
function tableContent(item)
    for a,b in pairs(item) do
       return true
    end
    return false
end
 
local function hasImageIndex(class)
    if not class then return false end
    if class.ExplorerImageIndex then return true end
    return hasImageIndex(class.Superclass and apiData.Classes[class.Superclass])
end
 
function hasContent(class)
    return hasImageIndex(class) or tableContent(class.Subclasses) or tableContent(class.Properties) or tableContent(class.Functions) or
        tableContent(class.YieldFunctions) or tableContent(class.Events) or tableContent(class.Callbacks)
end
 
function api.generateReferenceTable()
    local ret = "{| \n"
    local function scan(name,class,tab)
        ret = ret..'|<span>' ..string.rep("&emsp;",tab)..classLink(name).."</span>\n|-\n"
        alphabetizedKeys = {}
        for subClass in pairs(class.Subclasses) do
            local isInternal = customTags:ClassHasTag(subClass,"internal")
            local isHidden = customTags:ClassHasTag(subClass,"dontShowOnApiRef")
            if not (isInternal or isHidden) then
                table.insert(alphabetizedKeys, subClass)
            end
        end
        table.sort(alphabetizedKeys)
        for i,n in ipairs(alphabetizedKeys) do
            scan(n, class.Subclasses[n], tab+1)
        end
    end
    scan("Instance",apiData.Classes.Instance,0)
    return ret .. '|}'
end
 
function api.generateEnumTable()
    local ret = '{| \n'
    for _, n in sortedPairs(apiData.Enums) do
        ret = ret .. '| [[API:Enum/' .. n.Name .. '|' .. n.Name .. ']]\n|-\n'
    end
    return ret .. '|}'
end
 
function api.generateClassLinkTable(frame)
    local ret, list = '{| style="width:100%; table-layout:fixed;"\n', frame.args[1]
 
    local first, numLinks = true, 0
    for className in list:gmatch("[^,]+") do
        assert(apiData.Classes[className], className.." doesn't exist")
        if (not first) and numLinks % 3 == 0 then
            ret = ret.."|-\n"
        end
        first = false
        ret = ret..("|%s\n"):format(classLink(className))
        numLinks = numLinks + 1
    end
    ret = ret.."|}"
    return ret
end
 
function api.generateListOfClassesWithTag(frame)
    local tag = frame.args[1]
    local taggedClasses = {}
    local taggedInternalClasses = {}
 
    for className in pairs(apiData.Classes) do
        if customTags:ClassHasTag(className,tag) then
            if customTags:ClassHasTag(className,"internal") then
                table.insert(taggedInternalClasses,className)
            else
                table.insert(taggedClasses,className)
            end
        end
    end
 
    local ret = '{| \n'
    local first = true
 
    local function processClass(className)
        if not first then
            ret = ret .."|-\n"
        end
        first = false
        ret = ret .. ("|%s\n"):format(classLink(className,true))
    end
 
    table.sort(taggedClasses)
    for _,className in pairs(taggedClasses) do
        processClass(className)
    end
 
    if #taggedInternalClasses > 0 and #taggedClasses > 0 then -- If we have a mix of internal and non-internal classes
        ret = ret .. "\n==<small>Internal "..tag.."s</small>==\n"
    end
 
    table.sort(taggedInternalClasses)
    for _,className in pairs(taggedInternalClasses) do
        processClass(className)
    end
 
    ret = ret .."|}"
    return ret
end
 
-- needed for Template:object/image
function api.getClassImage(frame)
    return imageIndex(frame.args[1])
end
 
-- Code to test in console:
-- =p.generateDisambig{args={"TextColor"},preprocess=function(s,...) return ... end,getParent=function() return {args={}} end}
function api.generateDisambig(frame)
    local match = frame:getParent().args[2]
    local matches = match and getMembersByNameMatch(frame.args[1])
                           or getMembersByNameCI(frame.args[1])
    return frame:preprocess(
        _I(matches):map(function(member)
            local f = member.tags["deprecated"] == true
            local members = apiData.Classes[member.Class][plurals[member.type]]
            --[[local res = plurals[member.type].."\n"
            for k,v in pairs(apiData.Classes[member.Class]) do
                res = res..k.." = "..tostring(v).."\n"
            end if true then return res.."\n" end]]
            local clonedMember = member.tags.deprecated and member.Name:match("^[a-z]") and members[uppercasedName]
            -- In case it's deprecated, and the ReflectionMetadata looks like "Deprecated. Use WHATEVER Instead", also just print the name
            local replacedMember = member.tags.deprecated and (member.ReflectionMetadataSummary or ""):match("Deprecated")
            replacedMember = (member.ReflectionMetadataSummary or ""):match("Use (%w+)%(?%)? instead") or replacedMember
            mw.log(replacedMember,members[replacedMember])
            mw.log(member.ReflectionMetadataSummary)
            replacedMember = members[replacedMember] or replacedMember ~= nil
 
            local realMember = (f and (type(replacedMember) == "table" and replacedMember or clonedMember)) or member
            local memberData,ret = loadMemberData(realMember)
 
            if realMember == member then
                ret = ("* [[API:Class/%s/%s|%s]]%s &mdash; the %s of {{object/image|%s}}[[API:Class/%s|%s]] objects"):format(
                    member.Class, member.Name, member.Name,
                    member.type == 'Property'
                        and (", a %s"):format(rbxTypes.link(getType(member, memberData)))
                        or "",
                    member.type:lower(),
                    member.Class, member.Class, member.Class
                )
            else
                ret = ("* %s%s &mdash; the %s of {{object/image|%s}}[[API:Class/%s|%s]] objects"):format(
                    member.Name,
                    member.type == 'Property'
                        and (", a %s"):format(rbxTypes.link(getType(member, memberData)))
                        or "",
                    member.type:lower(),
                    member.Class, member.Class, member.Class
                )
                end
            local description = (
                (f and (replacedMember or clonedMember) and ("Deprecated in favor of [[API:Class/%s/%s|%s]]"):format(realMember.Class,realMember.Name,realMember.Name))
                or nonEmpty(memberData["DescriptionShort"])
                or nonEmpty(memberData["Description"])
            )
            if description then
		description = description:gsub("$DESCRIPTION_SHORT",memberData.DescriptionShort or "*NO SHORT DESCRIPTION AVAILABLE*")
                ret = ret .."\n::"..description
            end
            return ret
        end)
        :join("\n")
    )
end
 
--[[
finds the api object described by a string such as
 
* Instance
* Instance.Name
* Enum.Material
]]
local function objectFromString(s)
    local parts = mw.text.split(s, '%.')
 
    if parts[1] == 'Enum' then
        local name = assert(parts[2], "Enum not specified - expected 'Enum.EnumName'")
        local enum = assert(apiData.Enums[name], ("Enum %q does not exist!"):format(name))
        return enum
    end
 
    local class = apiData.Classes[parts[1]]
    local data = assert(class, ("Class %q does not exist!"):format(parts[1]))
 
    if not parts[2] then
        return class
    end
 
    local name = parts[2]
    local member, memberType = getMemberByName(class, name)
    assert(member, ("Member %s[%q] does not exist!"):format(class.Name, name))
    return member
end
 
function api.generateLink(frame)
    local args = frame:getParent().args
    local s = assert(args[1], "First argument is required")
 
    local obj = objectFromString(s)
 
    local name, link, title
    local displayName = args[2] or obj.Name
 
    name = obj.Name
 
    if obj.type == 'Enum' then
        link = ('API:Enum/%s'):format(name)
    elseif obj.type == 'Class' then
        link = ("API:Class/%s"):format(name)
    else
        local memberData = loadMemberData(obj)
        local desc = nonEmpty(memberData.DescriptionShort) or nonEmpty(memberData.Description)
        link = ("API:Class/%s/%s"):format(obj.Class, name)
        title = ("%s.%s, a %s"):format(obj.Class, obj.Name, obj.type)
        if desc then
	   desc = desc:gsub("$DESCRIPTION_SHORT",memberData.DescriptionShort or "*NO SHORT DESCRIPTION AVAILABLE*")
           title = title .. '. ' .. desc
        end
    end
    if title then
        name = ('<span title="%s">%s</span>'):format(
            mw.text.nowiki(title:gsub("%[%[.-|(.-)%]%]", "%1"):gsub("%[%[(.-)%]%]", "%1")),
            displayName
        )
    end
 
    return ("[[%s|%s]]"):format(link, name)
end
 
function api.generateEmbedded(frame)
    local args = frame:getParent().args
    assert(args[1], "First argument is required")
    local obj = objectFromString(args[1])
 
    if obj.type == 'Enum' then
        error("Embedding an Enum is not supported (yet)")
    elseif obj.type == 'Class' then
        error("Embedding a Class is not supported (yet)")
    else
        local member = obj
        local memberData, noMemberPageReason = loadMemberData(member)
        if noMemberPageReason then error("Couldn't load member data: "..noMemberPageReason) end
 
        local hasArguments = member.Arguments and member.Arguments[1]
        if member.Arguments then
            local returnType = getType(member, memberData)
            if member.type:find("Event") then
                returnType = rbxTypes.parse("RBXScriptSignal")
            end
            local form = member.type:match("Function")
                and '<pre style="/*display:inline-block*/">%s %s:%s(%s)</pre>'
                or '<pre style="/*display:inline-block*/">%s %s.%s (%s)</pre>'
            return (form):format(
                rbxTypes.link(returnType, true),
                ("[[API:Class/%s|<span style='color:black'>%s</span>]]"):format(member.Class,member.Class),
                ("[[API:Class/%s/%s|<span style='color:black'>%s</span>]]"):format(member.Class,member.Name,member.Name),
                hasArguments and
                    _I(member.Arguments):map(function(arg)
                        local typeObj = getType(arg, memberData)
                        return ('\n    %s %s%s'):format(
                            rbxTypes.link(typeObj),
                            arg.Name,
                            defaultArgumentString(arg,typeObj)
                        )
                    end):join(',')..'\n' or ''
            )
        else
            return ('<pre style="/*display:inline-block*/">%s %s.%s</pre>'):format(
                rbxTypes.link(getType(member,memberData)),
                ("[[API:Class/%s|<span style='color:black'>%s</span>]]"):format(member.Class,member.Class),
                ("[[API:Class/%s/%s|<span style='color:black'>%s</span>]]"):format(member.Class,member.Name,member.Name)
            )
        end
    end
end
 
function api.checkOldDisambig(frame)
    local success, memberpage = pcall(function() return mw.title.new(frame.args[1]):getContent() end)
    local res = '<span class="usershow">'
    if success then
        if not memberpage:lower():find("{{member disambig") then
            res = res.."Can't find proper template, flagging as old\n"
            res = res.."[[Category:Old-style disambiguation pages]]\n"
        end
    else
        res = res.."Error during getContent: "..memberpage.."\n" 
    end
    return res.."</span>"
end
 
function api.generateHiddenMembersSection()
	local tags = {"RobloxScriptSecurity","RobloxSecurity","WritePlayerSecurity"}
	local descs = 
	{
		RobloxScriptSecurity = "The following members can be used by ".. classLink("CoreScript",true,true);
		RobloxSecurity = "The following members can only be used by Roblox's backend server (meaning you can't use them, period)";
		WritePlayerSecurity = "Presumably the same as RobloxSecurity";
	}
	local preferred
	local memberTypes = {"Properties","Functions","YieldFunctions","Callbacks","Events"}
	local ret = ""
	local lines = {}
	for _,tag in ipairs(tags) do
		local desc = descs[tag]
		lines[tag] = {}
		table.insert(lines[tag],"=="..tag.."==")
		table.insert(lines[tag],desc)
	end
	for className,class in sortedPairs(apiData.Classes) do
		local classLines = {}
		for _,memberType in pairs(memberTypes) do
			for _,member in sortedPairs(class[memberType]) do
				for _,tag in pairs(tags) do
					if member.tags[tag] then
						if not classLines[tag] then
							classLines[tag] = {}
						end
						local singular do
							if memberType == "Properties" then
								singular = "Property"
							else
								singular = memberType:sub(1,#memberType-1)
							end
						end
						local line = "** '''"..singular.."''' [[API:Class/"..className.."/"..member.Name.."|"..member.Name.."]]"
						table.insert(classLines[tag],line)
					end
				end
			end
		end
		for tag,memberLines in pairs(classLines) do
			local tagLines = lines[tag] -- Who you gonna call?
			table.insert(tagLines,"* "..classLink(class.Name))
			for _,line in ipairs(memberLines) do
				table.insert(tagLines,line)
			end
		end
	end
	for _,tagLines in sortedPairs(lines) do
		for _,line in sortedPairs(tagLines) do
			ret = ret .. "\n"..line
		end
	end
	return ret
end
 
return api

Ad blocker interference detected!


Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.