-- DocRenderer tests: page structure, both-form signatures, privacy, cross-links,
-- enum/alias tables, and site scaffolding — all on in-memory snippets.
() ->
Parser = require "l0.MoonCats.Parser"
DocRenderer = require "l0.MoonCats.DocRenderer"
UnitTestSuite = require "l0.DependencyControl.UnitTestSuite"
{:receiverName, :linkifyType} = UnitTestSuite\getTestExports DocRenderer
-- parse snippets and render; returns the result plus a page lookup by require id
renderModules = (ut, modules, opts) ->
parser = Parser!
irs = {}
packageSymbols = {typeNameByRequireId: {}, aliases: {}}
for mod in *modules
ir, err = parser\parse mod.source, mod.requireId
ut\assertNotNil ir, err
table.insert irs, ir
if ir.export.kind == "class" and ir.export.class
packageSymbols.typeNameByRequireId[ir.requireId] = ir.export.class.typeName
for name in pairs ir.aliases
packageSymbols.aliases[name] = true
result = DocRenderer!\render irs, packageSymbols, opts
pageFor = (requireId) ->
for page in *result.pages
return page.text if page.requireId == requireId or page.title == requireId
result, pageFor
render = (ut, source, requireId = "test.Mod", opts) ->
result, pageFor = renderModules ut, {{:source, :requireId}}, opts
(pageFor requireId), result
{
_description: "MoonCatsDocRenderer: module IRs to markdown API documentation."
-- ── page skeleton ───────────────────────────────────────────────────────
page_headerAndRequireSnippet: (ut) ->
text = render ut, "class Foo\n go: => 1\nreturn Foo", "l0.Test.Foo"
ut\assertMatches text, "^# l0%.Test%.Foo\n"
ut\assertMatches text, 'Foo = require "l0%.Test%.Foo"'
ut\assertMatches text, 'local Foo = require%("l0%.Test%.Foo"%)'
prose_indentedExampleFenced: (ut) ->
-- a blank-separated indented run (the annotations' inline-example form) becomes a
-- highlighted fence, dedented
text = render ut, "---Filters things.\n---\n--- Filter!\\include \"x\"\n--- Filter!\\includeAll!\n---@class Filter\nclass Filter\n go: => 1\nreturn Filter"
ut\assertMatches text, "Filters things%.\n\n```moonscript\nFilter!\\include \"x\"\nFilter!\\includeAll!\n```"
prose_hangingIndentStaysInline: (ut) ->
-- an indented continuation directly under a paragraph line is prose, not code
text = render ut, "class Foo\n ---Compares deeply:\n --- tables must match at identical indexes\n ---@param a any\n go: (a) => a\nreturn Foo"
ut\assertFalse text\match("```moonscript") != nil
ut\assertMatches text, "Compares deeply:\n tables must match at identical indexes"
page_classProseRendered: (ut) ->
text = render ut, "---A documented class.\n---Second prose line.\n---@class Foo\nclass Foo\n go: => 1\nreturn Foo"
ut\assertMatches text, "## Foo \n\nA documented class%.\nSecond prose line%."
-- a leading ---doc block detached from any declaration (above all code) documents the module itself
page_moduleDocRendered: (ut) ->
text = render ut, "---Overview line.\n---\n---Second paragraph.\n\nfoo = -> 1\nreturn {:foo}", "l0.Test.Mod"
ut\assertMatches text, "Overview line%."
ut\assertMatches text, "Second paragraph%."
-- a leading ---block bound to the first declaration is that declaration's doc, not the module's
page_boundLeadingDocIsNotModuleDoc: (ut) ->
text = render ut, "---Makes one.\n---@param x integer\nmake = (x) -> x\nreturn {:make}", "l0.Test.Mod"
ut\assertMatches text, "Makes one%." -- rendered as the function's doc
-- and not duplicated as a module description above the Functions heading
ut\assertMatches text, "```\n\n## Functions"
-- ── signatures ──────────────────────────────────────────────────────────
signature_constructorBothForms: (ut) ->
text = render ut, "class Widget\n ---Creates it.\n ---@param name string The name.\n new: (@name) =>\nreturn Widget"
ut\assertMatches text, "widget = Widget name"
ut\assertMatches text, "local widget = Widget%(name%)"
-- ctor param table carries the description the definitions lose
ut\assertMatches text, "| name | `string` | The name%. |"
signature_instanceMethodBothForms: (ut) ->
text = render ut, "class Widget\n ---does\n ---@param x integer\n poke: (x) => x\nreturn Widget"
ut\assertMatches text, "widget\\poke x"
ut\assertMatches text, "widget:poke%(x%)"
signature_zeroArgUsesBang: (ut) ->
text = render ut, "class Widget\n ---does\n peek: => 1\nreturn Widget"
ut\assertMatches text, "widget\\peek!"
ut\assertMatches text, "widget:peek%(%)"
signature_fatStaticOnClass: (ut) ->
text = render ut, "class Widget\n ---does\n ---@param ns string\n @find = (ns) => ns\nreturn Widget"
ut\assertMatches text, "Widget\\find ns"
ut\assertMatches text, "Widget:find%(ns%)"
signature_thinStaticDotForm: (ut) ->
text = render ut, "class Widget\n ---does\n ---@param ns string\n @parse = (ns) -> ns\nreturn Widget"
ut\assertMatches text, "Widget%.parse ns"
ut\assertMatches text, "Widget%.parse%(ns%)"
receiver_lowerCamelAndKeywordSafe: (ut) ->
ut\assertEquals receiverName("UpdateFeed"), "updateFeed"
ut\assertEquals receiverName("Timer"), "timer"
-- a receiver that would be a keyword gets a prefix
ut\assertEquals receiverName("Local"), "theLocal"
-- ── member sections ─────────────────────────────────────────────────────
sections_instanceVsClassMethods: (ut) ->
text = render ut, "class Foo\n ---inst\n go: => 1\n ---stat\n @make = -> 1\nreturn Foo"
instancePos = text\find "### Instance methods"
classPos = text\find "### Class methods"
ut\assertNotNil instancePos
ut\assertNotNil classPos
goPos = text\find "#### go"
makePos = text\find "#### make"
ut\assertTrue instancePos < goPos and goPos < classPos
ut\assertTrue classPos < makePos
returns_namedWithDescriptions: (ut) ->
text = render ut, "class Foo\n ---does\n ---@return boolean ok True when it worked.\n ---@return string? err Failure reason.\n go: => true\nreturn Foo"
ut\assertMatches text, "%*%*Returns:%*%*"
ut\assertMatches text, "%- `ok` `boolean` — True when it worked%."
ut\assertMatches text, "%- `err` `string`%? — Failure reason%."
deprecated_blockquoteWithReason: (ut) ->
text = render ut, "class Foo\n ---old\n ---@deprecated Use go2 instead.\n ---@param a string\n go: (a) => a\nreturn Foo"
ut\assertMatches text, "> %*%*Deprecated%*%* — Use go2 instead%."
params_optionalMarkerAndVararg: (ut) ->
text = render ut, "class Foo\n ---does\n ---@param a? integer Count.\n ---@param ... any Extras.\n go: (a, ...) => a\nreturn Foo"
ut\assertMatches text, "| a%? | `integer` | Count%. |"
ut\assertMatches text, "| %.%.%. | `any` | Extras%. |"
-- ── privacy ─────────────────────────────────────────────────────────────
private_omittedByDefault: (ut) ->
text = render ut, "class Foo\n ---@private\n __hidden: => 1\n ---documented\n go: => 1\nreturn Foo"
ut\assertFalse text\match("__hidden") != nil
private_badgedWhenIncluded: (ut) ->
text = render ut, "class Foo\n ---@private\n __hidden: => 1\n go: => 1\nreturn Foo", "test.Mod", {includePrivate: true}
ut\assertMatches text, "#### __hidden 🔒"
private_fieldsFiltered: (ut) ->
text = render ut, "---@class Foo\n---@field name string Public field.\n---@field private __cache table Internal.\nclass Foo\n go: => 1\nreturn Foo"
ut\assertMatches text, "| name | `string` | Public field%. |"
ut\assertFalse text\match("__cache") != nil
-- ── fields and data ─────────────────────────────────────────────────────
fields_accessorPropertyFromClassBlock: (ut) ->
text = render ut, 'Accessors = require "l0.DependencyControl.Accessors"\n---@class Foo\n---@field state integer Read-only view.\nclass Foo\n state: Accessors.property\n get: => 1\nreturn Foo'
ut\assertMatches text, "### Fields"
ut\assertMatches text, "| state | `integer` | Read%-only view%. |"
fields_dataStaticsTyped: (ut) ->
text = render ut, "class Logger\n new: =>\nclass Foo\n @logger = Logger!\n @maxSize = 200\nreturn Foo"
ut\assertMatches text, "| logger | %[Logger%]%(#Logger%) |"
ut\assertMatches text, "| maxSize | `integer` |"
fields_setmetatableTypedTable: (ut) ->
text = render ut, "class Foo\n @instances = setmetatable {}, {__mode: \"v\"}\nreturn Foo"
ut\assertMatches text, "| instances | `table` |"
-- ── enums and aliases ───────────────────────────────────────────────────
enum_tableWithAliasDescriptions: (ut) ->
text = render ut, 'Enum = require "l0.DependencyControl.Enum"\nclass Foo\n ---@alias Mode\n ---| "fast" # Fast: quick and shallow\n ---| "slow" # Slow: careful\n Mode = Enum "Mode", {Fast: "fast", Slow: "slow"}\n @Mode = Mode\nreturn Foo'
ut\assertMatches text, "### Enums"
ut\assertMatches text, "#### Mode"
ut\assertMatches text, "| Fast | `\"fast\"` | quick and shallow |"
ut\assertMatches text, "| Slow | `\"slow\"` | careful |"
enum_plainValuesWithoutAlias: (ut) ->
text = render ut, 'Enum = require "l0.DependencyControl.Enum"\nclass Foo\n @Status = Enum "Status", {Ok: 1, Failed: -1}\nreturn Foo'
ut\assertMatches text, "| Ok | `1` |"
ut\assertMatches text, "| Failed | `%-1` |"
enum_computedKeyShowsSourceExpression: (ut) ->
-- a key referencing another enum's member shows the source Enum.Member expression, value joined from the alias
src = 'Enum = require "l0.DependencyControl.Enum"\nKind = Enum "Kind", {A: "a", B: "b"}\n---@alias Section\n---| "macros" # automation scripts\n---| "modules" # modules\nSection = Enum "Section", {[Kind.A]: "macros", [Kind.B]: "modules"}\n---@class Domain\nreturn {:Kind, :Section}'
text = render ut, src
ut\assertMatches text, "| Kind%.A | `\"macros\"` | automation scripts |"
ut\assertMatches text, "| Kind%.B | `\"modules\"` | modules |"
types_aliasVariantTable: (ut) ->
text = render ut, "---Precision selector.\n---@alias Precision\n---| 'major' # Major: whole releases\n---| 'minor' # Minor: feature releases\n\nx = 1\n\nf = -> 1\nreturn {f: f}"
ut\assertMatches text, "## Types"
ut\assertMatches text, "### Precision"
ut\assertMatches text, "Precision selector%."
ut\assertMatches text, "| `'major'` | Major: whole releases |"
types_annotationOnlyClassTable: (ut) ->
text = render ut, "---Constructor arguments.\n---@class FooArgs\n---@field name string The name.\n---@field count? integer How many.\n\nclass Foo\n go: => 1\nreturn Foo"
ut\assertMatches text, "### FooArgs"
ut\assertMatches text, "| name | `string` | The name%. |"
ut\assertMatches text, "| count%? | `integer` | How many%. |"
-- ── cross-links ─────────────────────────────────────────────────────────
links_crossModuleTypeResolves: (ut) ->
_, pageFor = renderModules ut, {
{requireId: "l0.Test.Logger", source: "---@class Logger\nclass Logger\n go: => 1\nreturn Logger"}
{requireId: "l0.Test.Main", source: 'Logger = require "l0.Test.Logger"\nclass Main\n ---does\n ---@param logger Logger The logger.\n run: (logger) => 1\nreturn Main'}
}
mainText = pageFor "l0.Test.Main"
ut\assertMatches mainText, "%[Logger%]%(l0%.Test%.Logger%.md#Logger%)"
links_unknownTypeStaysCode: (ut) ->
-- the union pipe is escaped because linkified types land in table cells
ut\assertEquals linkifyType("string|integer", {linkIndex: {}, currentPage: "x.md"}), "`string`\\|`integer`"
links_complexTypeSingleSpan: (ut) ->
rendered = linkifyType "fun(a: string): boolean", {linkIndex: {}, currentPage: "x.md"}
ut\assertEquals rendered, "`fun(a: string): boolean`"
-- ── duplicates ──────────────────────────────────────────────────────────
duplicates_documentedWins: (ut) ->
text = render ut, "sleepImpl = (ms) -> ms\nclass Timer\n ---Sleeps.\n ---@param ms number Milliseconds.\n sleep: sleepImpl\n @sleep = sleepImpl\nreturn Timer"
_, count = text\gsub "#### sleep", ""
ut\assertEquals count, 1
ut\assertMatches text, "| ms | `number` | Milliseconds%. |"
-- ── module shapes ───────────────────────────────────────────────────────
module_tableWithFunctionsAndFields: (ut) ->
text = render ut, '{\n NAME: "depctrl"\n ---Greets.\n ---@param who string\n greet: (who) -> who\n}', "l0.Test.Constants"
ut\assertMatches text, "## Functions"
ut\assertMatches text, "Constants%.greet who"
ut\assertMatches text, "## Fields"
ut\assertMatches text, "| NAME | `string` |"
module_tableEnumFieldUnderEnumsSection: (ut) ->
-- a table field holding a module-local enum renders under its own Enums section, like a class enum
text = render ut, 'Enum = require "l0.DependencyControl.Enum"\nOp = Enum "Op", {Eq: "="}\nreturn {:Op}', "l0.Test.ops"
ut\assertMatches text, "## Enums"
ut\assertFalse text\match("| Op |") != nil
module_enumAliasAndOwnClassNotUnderTypes: (ut) ->
-- an exported enum's alias shows under Enums, not duplicated under Types, and the table's own
-- @class is dropped from Types too — leaving no Types section here, while the value type still links
src = 'Enum = require "l0.DependencyControl.Enum"\n---@alias Mode\n---| "a" # A: first\nMode = Enum "Mode", {A: "a"}\n---@class Ops\nOps = {:Mode}\nreturn Ops'
text = render ut, src, "l0.Test.ops"
ut\assertMatches text, "## Enums"
ut\assertFalse text\match("## Types") != nil
ut\assertMatches text, ''
module_ownTypeGetsAnchor: (ut) ->
-- a table module's own declared type gets an anchor so a re-export elsewhere can link to it
text = render ut, '---@class Ops\nOps = {NAME: "x"}\nreturn Ops', "l0.Test.ops"
ut\assertMatches text, ''
module_functionExport: (ut) ->
text = render ut, "---Resolves a host.\n---@param host string The host.\n---@return string resolved The resolution.\nresolveHost = (host) -> host\nreturn resolveHost", "l0.Test.resolve-host"
ut\assertMatches text, "#### resolveHost"
ut\assertMatches text, "resolveHost host"
ut\assertMatches text, "resolveHost%(host%)"
-- ── index and scaffolding ───────────────────────────────────────────────
index_groupsByPackage: (ut) ->
result = nil
do
parser = Parser!
ir = parser\parse "class Foo\n go: => 1\nreturn Foo", "l0.Test.Foo"
result = DocRenderer!\render {ir}, nil, {
siteName: "My API"
packages: {
"l0.Test": {name: "Test Package", version: "1.0.0", description: "A test.", modules: {"l0.Test.Foo"}}
}
}
ut\assertMatches result.indexPage.text, "# My API"
ut\assertMatches result.indexPage.text, "## Test Package `v1%.0%.0`"
ut\assertMatches result.indexPage.text, "%- %[l0%.Test%.Foo%]%(l0%.Test%.Foo%.md%)"
scaffold_mkdocsDefault: (ut) ->
_, result = render ut, "class Foo\n go: => 1\nreturn Foo", "l0.Test.Foo"
ut\assertEquals result.pages[1].path, "docs/l0.Test.Foo.md" -- standalone site nests pages under docs/
ut\assertEquals #result.scaffold, 1
ut\assertEquals result.scaffold[1].path, "mkdocs.yml"
ut\assertMatches result.scaffold[1].text, "site_name: API Documentation"
ut\assertMatches result.scaffold[1].text, "%- l0%.Test%.Foo: l0%.Test%.Foo%.md"
-- the mike version-provider block powers the published site's version picker
ut\assertMatches result.scaffold[1].text, "provider: mike"
scaffold_mdbook: (ut) ->
_, result = render ut, "class Foo\n go: => 1\nreturn Foo", "l0.Test.Foo", {site: "mdbook", siteName: "Book"}
paths = [file.path for file in *result.scaffold]
-- SUMMARY.md must live inside the source dir; book.toml beside it at the output root
ut\assertItemsEqual paths, {"docs/SUMMARY.md", "book.toml"}
for file in *result.scaffold
if file.path == "docs/SUMMARY.md"
ut\assertMatches file.text, "%- %[l0%.Test%.Foo%]%(l0%.Test%.Foo%.md%)"
else
ut\assertMatches file.text, 'title = "Book"'
ut\assertMatches file.text, 'src = "docs"'
-- "none" is the embeddable section: pages sit flat (to drop into a host site's docs dir) and the
-- only scaffold file is a literate-nav SUMMARY.md
scaffold_noneEmbedsFlatWithLiterateNav: (ut) ->
_, result = render ut, "class Foo\n go: => 1\nreturn Foo", "l0.Test.Foo", {site: "none"}
ut\assertEquals result.pages[1].path, "l0.Test.Foo.md" -- flat, no docs/ prefix
ut\assertEquals result.indexPage.path, "index.md"
ut\assertEquals #result.scaffold, 1
ut\assertEquals result.scaffold[1].path, "SUMMARY.md"
ut\assertMatches result.scaffold[1].text, "%* %[Overview%]%(index%.md%)"
ut\assertMatches result.scaffold[1].text, "%* %[l0%.Test%.Foo%]%(l0%.Test%.Foo%.md%)"
-- Under a plain section header, the root module leads as "Overview" and the rest drop the
-- namespace. Ungrouped leftovers stay fully qualified.
scaffold_literateNavRootAsOverview: (ut) ->
result = renderModules ut, {
{source: "class Root\n go: => 1\nreturn Root", requireId: "l0.Test"}
{source: "class Foo\n go: => 1\nreturn Foo", requireId: "l0.Test.Foo"}
{source: "class Bar\n go: => 1\nreturn Bar", requireId: "l0.Other.Bar"}
}, {
site: "none"
packages: {"l0.Test": {name: "Test Package", modules: {"l0.Test", "l0.Test.Foo"}}}
}
summary = result.scaffold[1].text
ut\assertMatches summary, "%* %[Overview%]%(l0%.Test%.md%)"
ut\assertMatches summary, "%* %[Foo%]%(l0%.Test%.Foo%.md%)"
ut\assertMatches summary, "%* %[l0%.Other%.Bar%]%(l0%.Other%.Bar%.md%)"
-- In the embeddable "none" mode the require snippet and signatures render as linked MoonScript/Lua
-- content tabs. Other modes keep one inline block.
page_noneModeRendersLanguageTabs: (ut) ->
tabbed = render ut, "class Foo\n go: (x) => x\nreturn Foo", "l0.Test.Foo", {site: "none"}
ut\assertMatches tabbed, '=== "MoonScript"'
ut\assertMatches tabbed, '=== "Lua"'
inline = render ut, "class Foo\n go: (x) => x\nreturn Foo", "l0.Test.Foo", {site: "mkdocs"}
ut\assertMatches inline, "%-%- MoonScript"
ut\assertFalse inline\match('=== "MoonScript"') != nil
}