Module:API

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 = "F2F2E6", borderColor = "ACAC82", category = "Uncreatable objects" },   abstract = { name = "Abstract", text = "This object is abstract. It cannot be created with Instance.new, and its members are inherited by other classes.", backgroundColor = "F2F2E6", borderColor = "ACAC82", category = "Abstract objects" },   internal = { name = "Internal", text = "This object has been marked as internal. It currently serves no significant use to developers.", backgroundColor = "FFEBEB", borderColor = "FFA9A9", 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 Instance.Changed", backgroundColor = "FFEBEB", borderColor = "FFA9A9", 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 = "F2ECE6", borderColor = "AC9782", 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 = "E2F1FF", borderColor = "525B63" },   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 it is not recommended.", backgroundColor = "F2ECE6", borderColor = "AC9782" },   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 = "F2ECE6", borderColor = "AC9782", category = "Settings objects" },   service = { name = "Service", text = "This class is a service. It is a top-level singleton which can be obtained with the GetService method.", backgroundColor = "DDE7DD", borderColor = "A0B9A0", 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 = "F2E6E6", borderColor = "AC8282", 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 = "F2E6E6", borderColor = "AC8282", 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 = "F2E6E6", borderColor = "AC8282", category = "WritePlayerSecurity members" },   LocalUserSecurity = { text = "This member cannot be used in scripts, but is usable in the command bar.", backgroundColor = "F2ECE6", --would a color somewhere between the two other security levels' be more appropriate? borderColor = "AC9782", category = "LocalUserSecurity members" },   PluginSecurity = { text = "This member cannot be used in scripts, but is usable in the command bar and plugins.", backgroundColor = "F2ECE6", borderColor = "AC9782", category = "PluginSecurity members" },   RobloxPlaceSecurity = { text = "This member cannot be used in scripts, but is usable in the command bar and plugins.", backgroundColor = "F2ECE6", borderColor = "AC9782", category = "RobloxPlaceSecurity members" },   deprecated = { name = " Deprecated ", text = "This item is deprecated. Do not use it for new work.", backgroundColor = "FFEBF5", 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 = "F2F2E6", borderColor = "AC9782", 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 = "F2F2E6", borderColor = "AC9782", 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 = "FFFDFB", borderColor = "AC8282", 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 = "DDE7DD", borderColor = "A0B9A0", category = "Libraries" },   useDot = { name = "Warning", text = "When calling this function, you should use a . to index it, rather than a :", backgroundColor = "FFEBEB", borderColor = "FFA9A9", } }

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 ("%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  is properly used) local deferred = memberpage:match("^$") 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 forClass == "Humanoid" and member.Name:match("Status") then memberData, noMemberPageReason = { DescriptionShort = "Deprecated, but the wiki page remained for reference" }, 'tooManyMembersFilter' elseif 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 = (' %s '):format(typeContent)

-- Print the linkified name local nameContent = ' ' nameContent = nameContent .. (strikeThrough and " %s " 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 (' %s %s%s '):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..' [' .. tag .. '] '       end end nameContent = nameContent.." "

-- 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' and noMemberPageReason ~= 'tooManyMembersFilter' 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..(""):format(member.Class) end nameContent = nameContent..(""):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 ' Malformed member page! ')

--better fallback than an error messsage or (member.ReflectionMetadataSummary and               ('%s '):format(member.ReflectionMetadataSummary))

-- hit expensive function limit or (noMemberPageReason == 'expensive' and ' Too many members! ')

-- there just wasn't a page and we have no RMd summary or (noMemberPageReason == 'absent' and ' No documentation found. ')

-- there was a page but no description found on it           or ' Documentation incomplete. '       ):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 ('\n'):format(style) or ' \n'

local memberContent = {} memberContent.studiohide = start..'\n\n' .. typeContent .. ' \n \n\n' .. nameContent .. ' \n \n\n' .. description .. ' \n \n ' memberContent.studioshow = ' \n' .. typeContent .. nameContent .. ' \n '..description..' \n \n '

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 = " %s "

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 (' '           ..'link=API:Class/%s'            ..' %s%s '          ..' '):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 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 = ''..memberType..' ' ..' '                     ..' '                         ..memberType ..' '                          ..' '                           ..' [toggle ]' ..' '                     ..' '

..'' started = true end if inheriting then ret = ret..(' Inherited from %s: '):format(classLink(className)) end

local studioshow = ' \n' local studiohide = ' ' ret = ret .. studioshow .. studiohide end inheriting = true end

if started then ret=ret..' ' end

if hasHide then ret = string.gsub(ret, ' ', ' memberhidden' .. memberType .. ' ') 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..( %s: %s ):format(                    tagDesc.backgroundColor,                    tagDesc.backgroundColor,                    tagDesc.name or tag,                    tagDesc.text                ) if tagDesc.category then ret = ret .. " "               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] 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 == "" 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 .. ' ' .. memberName .. ' '

local data = apiData and apiData.Classes

data = data and data[className] if memberType == "" or memberType == "" then memberType = lookUpMemberType(className,memberName) if memberType == "Unknown" then return "Error: Unknown MemberType! This member may have been removed. " 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!"):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..(' %s of %s '):format(memberType, classLink(className))

local membersByName = getMembersByNameCI(memberName) if #membersByName > 1 then ret = ret..' 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 disambiguation page.'):format(pageName) end ret = ret..' ' end

--if descriptionShort then --   ret = ret..' '..descriptionShort..' ' --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 .. (' %s %s (%s) '):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..' ' ret = ret .. '\'\'\'Parameters:\'\'\' ' for k,v in pairs(data.Arguments) do           local typeObj = getType(v, parentArgs) if typeObj.incomplete then ret = ret .. (''):format(typeObj.type) end

if optionals[v.Name] ~= nil then typeObj.optional = optionals[v.Name] end

ret = ret .. (' %s '):format(v.Name) ret = ret .. ' ' ret = ret .. 'Type: '..rbxTypes.link(typeObj)..'</li>'

if not typeObj.args and typeObj.type == 'Tuple' then ret = ret .. (''):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> ' end

if data.ReturnType then local typeObj = getType(data, parentArgs) local msg = memberType == "Callback" and "Expected result" or "Returns" ret = ret .. ' '        ret = ret .. ""..msg..": " .. rbxTypes.link(typeObj,true) ret = ret .. ' '

if typeObj.incomplete then ret = ret .. (''):format(typeObj.type) end end

if data.ValueType then local typeObj = getType(data, parentArgs)

ret = ret .. ' '        ret = ret .. '\'\'\'Value Type:\'\'\' ' .. rbxTypes.link(typeObj) ret = ret .. ' '

if typeObj.incomplete then ret = ret .. (''):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 "):format(memberName) end

if descriptionShort then if description:find(descriptionShort) then -- Pages that repeat the short version of the description. ret = ret .. ""       end end

description = description:gsub("$DESCRIPTION_SHORT",descriptionShort or "") -- ret = ret .. ' \'\'\'Description:\'\'\' '..description..' ' ret = ret .. ' \'\'\'Description:\'\'\' '..description..' '

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 .. (""):format(memberTypePlural) end end end end

-- add to categories, sorting by member name. We probably want it in both? ret = ret .. (""):format(memberName) ret = ret .. (""):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 .. (""):format(memberName) ret = ret .. (""):format(#membersByName..' '..memberName) ret = ret .. frame:preprocess(               -- Use 'usershow' class to hide for logged out users                ("  "):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 = "[ create it]" if doesExist then -- Verify this is a legitimate disambiguation page. local content = primaryPage:getContent if (not content:find("")) and not content:find("") then doesExist = false noExistanceReason = "is improperly set up" noExistanceAction = "if you are an editor, copy the contents of [ this page], and [ paste it as the contents of this page]." end end if not doesExist then ret = ret .. (""):format(memberName) ret = ret .. frame:preprocess(               -- Use 'usershow' class to hide for logged out users                ("  "):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 and memberType == "Property" then ret = ret .. frame:preprocess((" "):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! " end

--ret = ret .. ' ' .. className .. ' '

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>>> class)' end ret = ret..' '..inheritance..' \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. "):format(className) end ret = ret..' \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;"> %s   ' local show = '<tr style="%s"><td style="text-align:right;vertical-align:top;">%s <td style="text-align:left;vertical-align:top;">%s ' local hide = ' %s  ' for k,v in pairs(names) do           if true -- hasContent(data.Subclasses[v])  then if first then ret,first = ret..' Inherited Classes "..studio end   end

if experimental then ret = ret .. "\n\n\nFiltered Tag Debug Info:" for filteredTag,isFiltered in pairs(filteredTags) do           ret = ret .. "\n\n" .. filteredTag .. " = " .. tostring(isFiltered) end end

ret = ret .. (%s|/RMD| ):format(className,description:gsub("\n"," ")) ret = ret .. (""):format(className) .. " "   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 = (' %s.%s '):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 = (' %s.%s '):format(                       member.Class, member.Name,                        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 end if member.tags["deprecated"] then nameLink = (' %s '):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 .. ' ' .. enumName .. ' '

-- print description ret = ret .. description .. '\n'

-- print all members ret = ret..' Enums "

local usedIn = getMembersByReferencedType(enumName) ret = ret .. ' Referenced by ' 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 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("") then ret = frame:preprocess("")..ret end end ret = ret .. bulletedMemberList(usedIn) end

ret = ret .. (""):format(enumName) ret = ret .. (""):format(#usedIn > 9 and '+' or #usedIn)

return ret end

function api.generateReferencedTypeSection(frame) local typename = frame.args[1] local usedIn = getMembersByReferencedType(typename) local ret = ' Referenced by ' 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 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)..("%s "):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)..("%s "):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..'| ' ..string.rep("&emsp;",tab)..classLink(name).." \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 .. '| ' .. 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== Internal "..tag.."s ==\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 = ("* %s%s &mdash; the %s of %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 %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 %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

]] local function objectFromString(s) local parts = mw.text.split(s, '%.')
 * Instance
 * Instance.Name
 * Enum.Material

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 displayName = (' %s '):format(           mw.text.nowiki(title:gsub("%[%[.-|(.-)%]%]", "%1"):gsub("%[%[(.-)%]%]", "%1")),            displayName        ) end

return ("%s"):format(link, displayName) 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) ' or '<pre style="/*display:inline-block*/">%s %s.%s (%s) ' return (form):format(               rbxTypes.link(returnType, true),                (" %s "):format(member.Class,member.Class),                (" %s "):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 '):format(               rbxTypes.link(getType(member,memberData)),                (" %s "):format(member.Class,member.Class),                (" %s "):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 = ' ' if success then if not memberpage:lower:find("{{member disambig") then res = res.."Can't find proper template, flagging as old\n" res = res.."\n" end else res = res.."Error during getContent: "..memberpage.."\n" end return res.." " end

function api.generateHiddenMembersSection local tags = {"RobloxScriptSecurity","RobloxSecurity"} 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)"; }	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.." "..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

local function generateClassCategoryRef(classCategory,description) local description = description or "" local ret = "==" .. classCategory .. "==\n" .. description .. "\n\n" local classes = {} for className,class in pairs(apiData.Classes) do		if class.ClassCategory == classCategory then table.insert(classes,className) end end if #classes > 0 then ret = ret .. "{{#invoke:API|generateClassLinkTable|" .. table.concat(classes,",") .. "}}"	end return ret end

function api.generateClassCategoryRef(frame) local classCategory = frame.args[1] local description = frame.args[2] local ret = generateClassCategoryRef(classCategory,description) return frame:preprocess(ret) end

function api.generateClassCategories(frame) local categories = {} local cache = {} for _,class in pairs(apiData.Classes) do	   local category = class.ClassCategory if category and not cache[category] then cache[category] = true table.insert(categories,category) end end table.sort(categories) local refs = {} for _,category in ipairs(categories) do		local ref = generateClassCategoryRef(category,frame.args[category]) table.insert(refs,ref) end local ret = table.concat(refs,"\n") return frame:preprocess(ret) end

return api