ソースを参照

Add CATS generator;

Co-Authored-By: Hendrik Mans <[email protected]>
bjorn 5 ヶ月 前
コミット
4ad2eedb9f
1 ファイル変更367 行追加0 行削除
  1. 367 0
      api/generators/cats.lua

+ 367 - 0
api/generators/cats.lua

@@ -0,0 +1,367 @@
+-- We're disabling `@see` tags for the time being because they
+-- ended up confusing the language server a little, breaking
+-- all objects from `lovr.math.*`.
+local ENABLE_SEE_TAGS = false
+
+-- Whether to generate an extra file with the globals
+-- from the `lovr.math` module.
+local INCLUDE_GLOBALS = true
+
+local function getPath(filename, sep)
+  sep = sep or "/"
+  return filename:match("(.*" .. sep .. ")")
+end
+
+local function ensureDirectoryExists(path)
+  os.execute("mkdir -p " .. path)
+end
+
+--- Writes `contents` into a file identified by `filename`.
+local function writeFile(filename, contents)
+  -- make sure the directory exists
+  local path = getPath(filename)
+  if path then
+    ensureDirectoryExists(path)
+  end
+
+  local file = io.open(filename, "w")
+  if not file then
+    print("Failed to open file for writing: " .. filename)
+    return
+  end
+
+  file:write(contents)
+  file:write("\n")
+  file:close()
+end
+
+--- Adds a single value to a table.
+local function add(t, v)
+  t[#t + 1] = v
+end
+
+-- Shorthand to concatenate a table of strings with a delimiter.
+local function join(t, delimiter)
+  return table.concat(t, delimiter)
+end
+
+-- Shorthand to map a table with a function.
+local function map(t, fn)
+  local out = {}
+  for i, v in ipairs(t) do
+    out[i] = fn(v)
+  end
+  return out
+end
+
+--- Returns true if any element in the table `t` satisfies the predicate `fn`.
+local function any(t, fn)
+  for _, v in ipairs(t) do
+    if fn(v) then
+      return true
+    end
+  end
+  return false
+end
+
+--- Renders the given string as a docstring. If the string
+--- contains newlines, they are prefixed with `---`.
+local function doc(str)
+  str = str or ""
+  return "---" .. str:gsub("\n", "\n---")
+end
+
+--- Determines if a parameter is optional. If it is, returns a `?` suffix.
+--- If it's not, returns an empty string.
+local function optionalSuffix(param)
+  if param.type ~= 'table' then
+    return (param.default ~= nil) and '?' or ''
+  elseif not param.table then
+    return ''
+  elseif any(param.table, function(field) return field.default == nil end) then
+    return ''
+  else
+    return '?'
+  end
+end
+
+local function docHeader(str)
+  return doc("#### " .. str .. "\n")
+end
+
+--- Render `@see` tags for a definition.
+local function seeTags(out, def)
+  if not ENABLE_SEE_TAGS then
+    return
+  end
+
+  if def.related then
+    for _, related in ipairs(def.related) do
+      add(out, doc("@see " .. related))
+    end
+  end
+end
+
+-- Forward declaration for `argumentType`.
+local argumentType
+
+--- Returns the type of a table argument.
+local function tableType(arg)
+  local out = {}
+
+  for _, field in ipairs(arg.table) do
+    add(out, ("%s: %s"):format(field.name, argumentType(field)))
+  end
+
+  return ("{%s}"):format(join(out, ", "))
+end
+
+--- Returns the type of a function argument.
+function argumentType(arg)
+  if arg.type == "*" then
+    return "any"
+  elseif arg.type == 'table' and arg.table then
+    return tableType(arg)
+  else
+    return arg.type
+  end
+end
+
+--- Renders the header (description, notes, examples, etc.) for anything that's like a function.
+local function renderFunctionHeader(out, func)
+  add(out, "")
+  add(out, doc(func.description))
+
+  if (func.notes) then
+    add(out, doc(""))
+    add(out, docHeader("Notes:"))
+    add(out, doc(func.notes))
+  end
+
+  if (func.examples) then
+    for _, example in ipairs(func.examples) do
+      add(out, doc(""))
+      add(out, docHeader("Example:"))
+
+      if example.description then
+        add(out, doc(example.description))
+        add(out, doc(""))
+      end
+
+      if example.code then
+        add(out, doc("```lua"))
+        add(out, doc(example.code))
+        add(out, doc("```"))
+      end
+    end
+  end
+
+  add(out, doc(""))
+
+  seeTags(out, func)
+end
+
+local function renderType(out, tag, variant)
+  local returns
+  if #variant.returns > 0 then
+    returns = join(map(variant.returns, function(ret)
+      return ret.type
+    end), ", ")
+  else
+    returns = "nil"
+  end
+
+  local params = join(map(variant.arguments, function(arg)
+    return ("%s%s: %s"):format(arg.name, optionalSuffix(arg), argumentType(arg))
+  end), ", ")
+
+  add(out, doc(("%s fun(%s): %s"):format(tag, params, returns)))
+end
+
+local function renderFunctionVariant(out, func, variant)
+  renderFunctionHeader(out, func)
+
+  -- Document parameters
+  if variant.arguments then
+    for _, arg in ipairs(variant.arguments) do
+      add(out, doc(("@param %s%s %s %s"):format(arg.name, optionalSuffix(arg), argumentType(arg), arg.description)))
+    end
+  end
+
+  -- Document return type
+  if variant.returns then
+    for _, ret in ipairs(variant.returns) do
+      add(out, doc(("@return %s %s %s"):format(ret.type, ret.name, ret.description)))
+    end
+  end
+
+  -- Build function signature
+  local signature = join(map(variant.arguments, function(arg)
+    return arg.name
+  end), ", ")
+
+  add(out, ("function %s(%s) end"):format(func.key, signature))
+end
+
+local function renderFunction(out, func)
+  for _, variant in ipairs(func.variants) do
+    renderFunctionVariant(out, func, variant)
+  end
+end
+
+local function renderEnum(out, enum)
+  add(out, "")
+  add(out, doc(enum.description))
+  add(out, doc("@alias " .. enum.key))
+  for _, value in ipairs(enum.values) do
+    add(out, doc(value.description))
+    add(out, doc("| \"%s\""):format(value.name))
+  end
+end
+
+local function generateSwizzlePermutations(fields, length)
+  local permutations = {}
+
+  local function go(swizzle)
+    swizzle = swizzle or ""
+
+    if #swizzle == length then
+      add(permutations, swizzle)
+    else
+      for _, field in ipairs(fields) do
+        go(swizzle .. field)
+      end
+    end
+  end
+
+  go()
+
+  return permutations
+end
+
+local function renderSwizzleFields(out, fields)
+  for i = 1, 4 do
+    local type = i == 1 and "number" or "Vec" .. i
+    local comp = i == 1 and "component" or "components"
+
+    for _, swizzle in ipairs(generateSwizzlePermutations(fields, i)) do
+      local desc = ("The %s %s of the vector."):format(swizzle, comp)
+      add(out, doc(("@field %s %s %s"):format(swizzle, type, desc)))
+    end
+  end
+end
+
+--- Renders a module's information to CATS format.
+local function renderModule(mod)
+  local out = {}
+
+  add(out, doc("@meta"))
+  add(out, "")
+
+  if mod.description then
+    add(out, doc(mod.description))
+    add(out, doc(""))
+  end
+
+  add(out, doc(("@class %s"):format(mod.key)))
+  add(out, ("%s = {}"):format(mod.key))
+
+  -- Render functions
+  for _, func in ipairs(mod.functions) do
+    renderFunction(out, func)
+  end
+
+  -- Render objects
+  for _, obj in ipairs(mod.objects) do
+    add(out, "")
+    add(out, doc(obj.description))
+    add(out, doc("@class %s"):format(obj.key))
+
+    -- fields
+    if obj.fields then
+      for _, field in ipairs(obj.fields) do
+        add(out, doc(("@field %s %s %s"):format(field.name, field.type, field.description)))
+      end
+    end
+
+    -- swizzles
+    if obj.swizzles then
+      for _, swizzle in ipairs(obj.swizzles.components) do
+        renderSwizzleFields(out, swizzle)
+      end
+    end
+
+    -- see tags
+    seeTags(out, obj)
+
+    -- definition
+    add(out, ("local %s = {}"):format(obj.name))
+
+    -- Render object methods
+    for _, func in ipairs(obj.methods) do
+      renderFunction(out, func)
+    end
+  end
+
+  -- Render enums
+  for _, enum in ipairs(mod.enums) do
+    renderEnum(out, enum)
+  end
+
+
+  return join(out, "\n")
+end
+
+local function generateModuleDocumentation(api)
+  for _, v in ipairs(api.modules) do
+    local text = renderModule(v)
+    local filename = v.name
+    writeFile(("library/%s.lua"):format(filename), text)
+  end
+end
+
+local function renderCallback(out, callback)
+  renderFunctionHeader(out, callback)
+  for i = 1, #callback.variants do
+    local variant = callback.variants[i]
+    renderType(out, "@type", variant)
+  end
+  add(out, ("%s = nil"):format(callback.key))
+end
+
+local function generateCallbackDocumentation(api)
+  local out = {}
+  add(out, doc("@meta"))
+
+  for _, callback in ipairs(api.callbacks) do
+    renderCallback(out, callback)
+  end
+
+  writeFile("library/callback.lua", join(out, "\n"))
+end
+
+local function generateGlobalsDocumentation()
+  local out = {}
+  add(out, doc("@meta"))
+  add(out, "vec2 = lovr.math.vec2")
+  add(out, "Vec2 = lovr.math.newVec2")
+  add(out, "vec3 = lovr.math.vec3")
+  add(out, "Vec3 = lovr.math.newVec3")
+  add(out, "vec4 = lovr.math.vec4")
+  add(out, "Vec4 = lovr.math.newVec4")
+  add(out, "mat4 = lovr.math.mat4")
+  add(out, "Mat4 = lovr.math.newMat4")
+  add(out, "quat = lovr.math.quat")
+  add(out, "Quat = lovr.math.newQuat")
+
+  writeFile("library/globals.lua", join(out, "\n"))
+end
+
+return function(api)
+  generateModuleDocumentation(api)
+  generateCallbackDocumentation(api)
+
+  if INCLUDE_GLOBALS then
+    generateGlobalsDocumentation()
+  end
+end