-- Octane Lua API Complete Exporter -- -- Run this script inside OctaneRender Standalone. It produces: -- 1. A self-contained, searchable HTML reference. -- 2. A JSON schema beside the HTML file for tools and integrations. -- -- The exporter reads metadata through octane.help and octane.apiinfo. It does -- not create nodes and does not modify the current project. local EXPORTER_VERSION = "1.0.0" local function safeCall(fn, ...) if type(fn) ~= "function" then return nil, "Function is unavailable" end local ok, result = pcall(fn, ...) if ok then return result, nil end return nil, tostring(result) end local function sortedKeys(value) local keys = {} if type(value) ~= "table" then return keys end for key in pairs(value) do keys[#keys + 1] = key end table.sort(keys, function(a, b) return tostring(a) < tostring(b) end) return keys end local function shallowCopy(value) local result = {} if type(value) == "table" then for key, item in pairs(value) do result[key] = item end end return result end local function makeSerializable(value, seen, depth) local valueType = type(value) if valueType == "nil" or valueType == "boolean" or valueType == "number" or valueType == "string" then return value end if valueType ~= "table" then return tostring(value) end seen = seen or {} depth = depth or 0 if depth > 20 then return "" end if seen[value] then return "" end seen[value] = true local result = {} for key, item in pairs(value) do local safeKey = key if type(key) ~= "string" and type(key) ~= "number" then safeKey = tostring(key) end result[safeKey] = makeSerializable(item, seen, depth + 1) end seen[value] = nil return result end local function isArray(value) if type(value) ~= "table" then return false, 0 end local count = 0 local maximum = 0 for key in pairs(value) do if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then return false, 0 end count = count + 1 if key > maximum then maximum = key end end if count == 0 then return true, 0 end return count == maximum, maximum end local function jsonEscape(value) value = tostring(value or "") local replacements = { ['"'] = '\\"', ['\\'] = '\\\\', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t' } return value:gsub('[%z\1-\31\\"]', function(character) return replacements[character] or string.format("\\u%04x", string.byte(character)) end) end local function jsonEncode(value, indent, level) indent = indent or " " level = level or 0 local valueType = type(value) if valueType == "nil" then return "null" elseif valueType == "boolean" then return value and "true" or "false" elseif valueType == "number" then if value ~= value or value == math.huge or value == -math.huge then return "null" end return tostring(value) elseif valueType == "string" then return '"' .. jsonEscape(value) .. '"' elseif valueType ~= "table" then return '"' .. jsonEscape(tostring(value)) .. '"' end local array, length = isArray(value) local padding = string.rep(indent, level) local childPadding = string.rep(indent, level + 1) local output = {} if array then for index = 1, length do output[#output + 1] = childPadding .. jsonEncode(value[index], indent, level + 1) end if #output == 0 then return "[]" end return "[\n" .. table.concat(output, ",\n") .. "\n" .. padding .. "]" end for _, key in ipairs(sortedKeys(value)) do output[#output + 1] = childPadding .. '"' .. jsonEscape(tostring(key)) .. '": ' .. jsonEncode(value[key], indent, level + 1) end if #output == 0 then return "{}" end return "{\n" .. table.concat(output, ",\n") .. "\n" .. padding .. "}" end local function htmlEscape(value) value = tostring(value or "") value = value:gsub("&", "&") value = value:gsub("<", "<") value = value:gsub(">", ">") value = value:gsub('"', """) value = value:gsub("'", "'") return value end local function htmlValue(value) if value == nil then return 'none' end if type(value) ~= "table" then return htmlEscape(value) end local values = {} for _, key in ipairs(sortedKeys(value)) do values[#values + 1] = htmlEscape(key) .. "=" .. htmlEscape(value[key]) end return table.concat(values, ", ") end local function fileStem(path) local stem, replacements = path:gsub("%.[^%.\\/]+$", "") if replacements == 0 then stem = path end return stem end local function formatPackedVersion(value, zeroLabel) if value == nil then return "unknown" end if value == 0 then return zeroLabel or "0" end if type(value) ~= "number" then return tostring(value) end -- Octane API versions use four packed bytes. Keep the original value in -- the title so future encoding changes remain visible. local major = math.floor(value / 16777216) % 256 local minor = math.floor(value / 65536) % 256 local patch = math.floor(value / 256) % 256 local build = value % 256 if major > 0 then return string.format("%d.%d.%d.%d", major, minor, patch, build) end return tostring(value) end local warnings = {} local function addWarning(scope, message) warnings[#warnings + 1] = { scope = tostring(scope or "unknown"), message = tostring(message or "unknown error") } end local function nameOrFallback(fn, value, fallback) local name, err = safeCall(fn, value) if name ~= nil and tostring(name) ~= "" then return tostring(name) end if err then addWarning(fallback or "name lookup", err) end return tostring(fallback or value or "unknown") end local apiinfo = octane.apiinfo local systemInfo, systemError = safeCall(apiinfo.getSystemInfo) if not systemInfo then systemInfo = {} addWarning("system info", systemError) end local schema = { metadata = { exporter = "Octane Lua API Complete Exporter", exporterVersion = EXPORTER_VERSION, generatedAt = os.date("!%Y-%m-%dT%H:%M:%SZ"), system = makeSerializable(systemInfo) }, apiModules = {}, nodeTypes = {}, graphTypes = {}, warnings = warnings } -- Collect the public Lua API documentation. if octane.help and type(octane.help.modules) == "function" then local modules, moduleError = safeCall(octane.help.modules) if modules then for _, moduleName in ipairs(sortedKeys(modules)) do local moduleRecord = { name = moduleName, description = modules[moduleName], functions = {}, properties = {}, constants = {} } local functions, functionsError = safeCall(octane.help.functions, moduleName) if functions then table.sort(functions) for _, functionName in ipairs(functions) do local functionDoc, functionError = safeCall(octane.help.functionDoc, moduleName, functionName) if functionDoc then local record = makeSerializable(functionDoc) record.name = functionName record.fullName = "octane." .. moduleName .. "." .. functionName moduleRecord.functions[#moduleRecord.functions + 1] = record else addWarning("function " .. moduleName .. "." .. functionName, functionError) end end elseif functionsError then addWarning("module functions " .. moduleName, functionsError) end local properties, propertiesError = safeCall(octane.help.properties, moduleName) if properties then table.sort(properties) for _, propertyName in ipairs(properties) do local propertyDoc, propertyError = safeCall(octane.help.propertiesDoc, moduleName, propertyName) if propertyDoc then local record = makeSerializable(propertyDoc) record.name = propertyName record.fullName = "octane." .. moduleName .. "." .. propertyName moduleRecord.properties[#moduleRecord.properties + 1] = record else addWarning("property " .. moduleName .. "." .. propertyName, propertyError) end end elseif propertiesError then addWarning("module properties " .. moduleName, propertiesError) end local constants, constantsError = safeCall(octane.help.constants, moduleName) if constants then table.sort(constants) for _, constantName in ipairs(constants) do local constantDoc, constantError = safeCall(octane.help.constantDoc, moduleName, constantName) if constantDoc then local record = makeSerializable(constantDoc) record.name = constantName record.fullName = "octane." .. moduleName .. "." .. constantName moduleRecord.constants[#moduleRecord.constants + 1] = record else addWarning("constant " .. moduleName .. "." .. constantName, constantError) end end elseif constantsError then addWarning("module constants " .. moduleName, constantsError) end schema.apiModules[#schema.apiModules + 1] = moduleRecord end else addWarning("API modules", moduleError) end else addWarning("API modules", "octane.help is unavailable") end -- Collect every registered node type and its static schema. local nodeTypes, nodeTypesError = safeCall(apiinfo.getNodeTypes) if nodeTypes then table.sort(nodeTypes, function(a, b) return nameOrFallback(apiinfo.getNodeTypeName, a) < nameOrFallback(apiinfo.getNodeTypeName, b) end) for _, nodeType in ipairs(nodeTypes) do local typeName = nameOrFallback(apiinfo.getNodeTypeName, nodeType, "nodeType " .. tostring(nodeType)) local info, infoError = safeCall(apiinfo.getNodeInfo, nodeType) if info then local record = makeSerializable(info) record.typeName = typeName record.outputTypeName = nameOrFallback( apiinfo.getPinTypeName, info.outputType, "pinType " .. tostring(info.outputType)) record.pins = {} record.attributes = {} local pinCount = tonumber(info.pinInfoCount) or 0 -- apiinfo metadata indices are one-based in Lua. for pinIndex = 1, pinCount do local pin, pinError = safeCall(apiinfo.getPinInfo, nodeType, pinIndex) if pin then local pinRecord = makeSerializable(pin) pinRecord.index = pinIndex pinRecord.idName = nameOrFallback( apiinfo.getPinIdName, pin.id, "pin " .. tostring(pin.id)) pinRecord.typeName = nameOrFallback( apiinfo.getPinTypeName, pin.type, "pinType " .. tostring(pin.type)) pinRecord.deprecated = safeCall(apiinfo.isDeprecated, pin) or false if pin.defaultNodeType and pin.defaultNodeType ~= 0 then pinRecord.defaultNodeTypeName = nameOrFallback( apiinfo.getNodeTypeName, pin.defaultNodeType, "nodeType " .. tostring(pin.defaultNodeType)) end record.pins[#record.pins + 1] = pinRecord else addWarning(typeName .. " pin " .. pinIndex, pinError) end end local attributeCount = tonumber(info.attributeInfoCount) or 0 for attributeIndex = 1, attributeCount do local attribute, attributeError = safeCall( apiinfo.getNodeAttributeInfo, nodeType, attributeIndex) if attribute then local attributeRecord = makeSerializable(attribute) attributeRecord.index = attributeIndex attributeRecord.idName = nameOrFallback( apiinfo.getAttributeIdName, attribute.id, "attribute " .. tostring(attribute.id)) attributeRecord.typeName = nameOrFallback( apiinfo.getAttributeTypeName, attribute.type, "attributeType " .. tostring(attribute.type)) attributeRecord.deprecated = safeCall(apiinfo.isDeprecated, attribute) or false record.attributes[#record.attributes + 1] = attributeRecord else addWarning(typeName .. " attribute " .. attributeIndex, attributeError) end end schema.nodeTypes[#schema.nodeTypes + 1] = record else addWarning(typeName, infoError) end end else addWarning("node types", nodeTypesError) end -- Collect every registered graph type and its attributes. local graphTypes, graphTypesError = safeCall(apiinfo.getGraphTypes) if graphTypes then table.sort(graphTypes, function(a, b) return nameOrFallback(apiinfo.getGraphTypeName, a) < nameOrFallback(apiinfo.getGraphTypeName, b) end) for _, graphType in ipairs(graphTypes) do local typeName = nameOrFallback(apiinfo.getGraphTypeName, graphType, "graphType " .. tostring(graphType)) local info, infoError = safeCall(apiinfo.getGraphInfo, graphType) if info then local record = makeSerializable(info) record.typeName = typeName record.outputTypeName = nameOrFallback( apiinfo.getPinTypeName, info.outputType, "pinType " .. tostring(info.outputType)) record.attributes = {} local attributeCount = tonumber(info.attributeInfoCount) or 0 for attributeIndex = 1, attributeCount do local attribute, attributeError = safeCall( apiinfo.getGraphAttributeInfo, graphType, attributeIndex) if attribute then local attributeRecord = makeSerializable(attribute) attributeRecord.index = attributeIndex attributeRecord.idName = nameOrFallback( apiinfo.getAttributeIdName, attribute.id, "attribute " .. tostring(attribute.id)) attributeRecord.typeName = nameOrFallback( apiinfo.getAttributeTypeName, attribute.type, "attributeType " .. tostring(attribute.type)) attributeRecord.deprecated = safeCall(apiinfo.isDeprecated, attribute) or false record.attributes[#record.attributes + 1] = attributeRecord else addWarning(typeName .. " attribute " .. attributeIndex, attributeError) end end schema.graphTypes[#schema.graphTypes + 1] = record else addWarning(typeName, infoError) end end else addWarning("graph types", graphTypesError) end local headless = type(arg) == "table" and type(arg[1]) == "string" and arg[1] ~= "" local htmlPath if headless then htmlPath = arg[1] else local dialog = octane.gui.showDialog { type = octane.gui.dialogType.FILE_DIALOG, title = "Save complete Octane Lua API reference", wildcards = "*.html;*.htm", save = true } if not dialog or not dialog.result or dialog.result == "" then return end htmlPath = dialog.result end if not htmlPath:lower():match("%.html?$") then htmlPath = htmlPath .. ".html" end local jsonPath = fileStem(htmlPath) .. ".json" local htmlFile, htmlError = io.open(htmlPath, "w") if not htmlFile then if not headless then octane.gui.showDialog { type = octane.gui.dialogType.ERROR_DIALOG, title = "Unable to write HTML", text = tostring(htmlError) } end error(htmlError) end local function write(text) htmlFile:write(text) end local function writePropertyRows(value, excluded) excluded = excluded or {} for _, key in ipairs(sortedKeys(value)) do if not excluded[key] then write("" .. htmlEscape(key) .. "" .. htmlValue(value[key]) .. "") end end end local function searchableText(value) local parts = {} local function visit(item, depth) if depth > 5 then return end if type(item) == "table" then for key, child in pairs(item) do parts[#parts + 1] = tostring(key) visit(child, depth + 1) end elseif item ~= nil then parts[#parts + 1] = tostring(item) end end visit(value, 0) return table.concat(parts, " "):lower() end write([=[ Octane Lua API Complete Reference

Octane Lua API Complete Reference

]=]) local versionName = systemInfo.octaneVersionName or systemInfo.octaneVersion or "Unknown Octane version" write("

Overview

") write("

" .. htmlEscape(versionName) .. "

") write("
") write("" .. #schema.nodeTypes .. " node types") write("" .. #schema.graphTypes .. " graph types") write("" .. #schema.apiModules .. " API modules") write("" .. #warnings .. " warnings") write("
") writePropertyRows(systemInfo) write("
Exporter version" .. EXPORTER_VERSION .. "
") write("

Node Types

") for _, node in ipairs(schema.nodeTypes) do local deprecatedClass = "" write("
") write("" .. htmlEscape(node.typeName) .. " — " .. htmlEscape(node.defaultName or "") .. "") write("

" .. htmlEscape(node.description or "") .. "

") write("") write("") write("") write("") write("") write("") write("") write("
Output type" .. htmlEscape(node.outputTypeName) .. "
Category" .. htmlEscape(node.category) .. "
Default name" .. htmlEscape(node.defaultName) .. "
Attributes" .. #node.attributes .. "
Static pins" .. #node.pins .. "
Compatibility modes" .. htmlEscape(node.compatibilityModeCount or 0) .. "
Introduced" .. htmlEscape(formatPackedVersion(node.minVersion, "before 11.0.0.10")) .. "
") if #node.pins > 0 then write("

Pins

") for _, pin in ipairs(node.pins) do local className = pin.deprecated and "subitem deprecated" or "subitem" write("

" .. htmlEscape(pin.idName) .. " — " .. htmlEscape(pin.label or pin.name) .. "

") write("

" .. htmlEscape(pin.description or "") .. "

") write("") write("") write("") write("") write("") writePropertyRows(pin, { idName=true,typeName=true,name=true,type=true,label=true, description=true,minVersion=true,endVersion=true,index=true, deprecated=true,id=true }) write("
Name" .. htmlEscape(pin.name) .. "
Type" .. htmlEscape(pin.typeName) .. "
Label" .. htmlEscape(pin.label) .. "
Introduced" .. htmlEscape(formatPackedVersion(pin.minVersion, "before 11.0.0.10")) .. "
Removed" .. htmlEscape(formatPackedVersion(pin.endVersion, "current")) .. "
") end end if #node.attributes > 0 then write("

Attributes

") for _, attribute in ipairs(node.attributes) do local className = attribute.deprecated and "subitem deprecated" or "subitem" write("

" .. htmlEscape(attribute.idName) .. "

") write("

" .. htmlEscape(attribute.description or "") .. "

") write("") write("") write("") write("") writePropertyRows(attribute, { idName=true,typeName=true,type=true,isArray=true, description=true,minVersion=true,endVersion=true,index=true, deprecated=true,id=true }) write("
Type" .. htmlEscape(attribute.typeName) .. "
Array" .. htmlEscape(attribute.isArray) .. "
Introduced" .. htmlEscape(formatPackedVersion(attribute.minVersion, "before 11.0.0.10")) .. "
Removed" .. htmlEscape(formatPackedVersion(attribute.endVersion, "current")) .. "
") end end write("
") end write("
") write("

Graph Types

") for _, graph in ipairs(schema.graphTypes) do write("
" .. htmlEscape(graph.typeName) .. " — " .. htmlEscape(graph.defaultName or "") .. "
") write("

" .. htmlEscape(graph.description or "") .. "

") write("
Output type" .. htmlEscape(graph.outputTypeName) .. "
Category" .. htmlEscape(graph.category) .. "
Inspectable" .. htmlEscape(graph.isInspectable) .. "
") if #graph.attributes > 0 then write("

Attributes

") for _, attribute in ipairs(graph.attributes) do write("

" .. htmlEscape(attribute.idName) .. "

" .. htmlEscape(attribute.description or "") .. "

") writePropertyRows(attribute) write("
") end end write("
") end write("
") write("

Lua API Modules

") for _, module in ipairs(schema.apiModules) do write("
octane." .. htmlEscape(module.name) .. "
") write("

" .. htmlEscape(module.description or "") .. "

") if #module.functions > 0 then write("

Functions

") for _, functionDoc in ipairs(module.functions) do write("

" .. htmlEscape(functionDoc.fullName) .. "

" .. htmlEscape(functionDoc.description or "") .. "

") writePropertyRows(functionDoc, { name=true,fullName=true,description=true }) write("
") end end if #module.properties > 0 then write("

Property Tables

") for _, propertyDoc in ipairs(module.properties) do write("

" .. htmlEscape(propertyDoc.fullName) .. "

" .. htmlEscape(propertyDoc.description or "") .. "

") writePropertyRows(propertyDoc, { name=true,fullName=true,description=true }) write("
") end end if #module.constants > 0 then write("

Constants

") for _, constantDoc in ipairs(module.constants) do write("

" .. htmlEscape(constantDoc.fullName) .. "

" .. htmlEscape(constantDoc.description or "") .. "

") writePropertyRows(constantDoc, { name=true,fullName=true,description=true }) write("
") end end write("
") end write("
") write("

Export Warnings

") if #warnings == 0 then write("

No warnings.

") else write("") for _, warning in ipairs(warnings) do write("") end write("
ScopeMessage
" .. htmlEscape(warning.scope) .. "" .. htmlEscape(warning.message) .. "
") end write([=[
]=]) htmlFile:close() local jsonFile, jsonError = io.open(jsonPath, "w") if jsonFile then jsonFile:write(jsonEncode(schema)) jsonFile:write("\n") jsonFile:close() else addWarning("JSON output", jsonError) if not headless then octane.gui.showDialog { type = octane.gui.dialogType.ERROR_DIALOG, title = "HTML saved, but JSON failed", text = "HTML:\n" .. htmlPath .. "\n\nJSON error:\n" .. tostring(jsonError) } end return end local completionText = "HTML reference:\n" .. htmlPath .. "\n\nJSON schema:\n" .. jsonPath .. "\n\nNodes: " .. #schema.nodeTypes .. "\nGraphs: " .. #schema.graphTypes .. "\nAPI modules: " .. #schema.apiModules .. "\nWarnings: " .. #warnings if headless then print(completionText) else octane.gui.showDialog { type = octane.gui.dialogType.INFO_DIALOG, title = "Octane API export complete", text = completionText } end