Skip to content

Commit 2fe4060

Browse files
committed
AstroAutodoc(feat[rst-lite]): Render docstrings with rst-lite
why: Replace docutils HTML with deterministic RST-lite rendering for docstrings. what: - add rst-lite docstring rendering helper and tests - wire render mode into loadApiPackage options and docs app - add rst-lite dependency for astro-autodoc
1 parent c5631c5 commit 2fe4060

7 files changed

Lines changed: 138 additions & 2 deletions

File tree

astro/apps/docs/src/lib/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const getApiModel = async (): Promise<ApiPackage> => {
3232
introspectPackage: 'libtmux',
3333
annotationFormat: 'string',
3434
mockImports: ['pytest'],
35+
docstringRenderer: 'rst-lite',
3536
})
3637

3738
return cached

astro/packages/astro-autodoc/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"@libtmux/api-model": "workspace:*",
2525
"@libtmux/py-bridge": "workspace:*",
2626
"@libtmux/py-introspect": "workspace:*",
27-
"@libtmux/py-parse": "workspace:*"
27+
"@libtmux/py-parse": "workspace:*",
28+
"@libtmux/rst-lite": "workspace:*"
2829
},
2930
"peerDependencies": {
3031
"astro": "^5.0.0"
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { ApiClass, ApiFunction, ApiMethod, ApiModule, ApiPackage, ApiVariable } from '@libtmux/api-model'
2+
import { parseRst, type RoleResolver, renderHtml } from '@libtmux/rst-lite'
3+
4+
export type DocstringRenderMode = 'introspect' | 'rst-lite' | 'none'
5+
6+
export type DocstringRenderOptions = {
7+
mode?: DocstringRenderMode
8+
roleResolver?: RoleResolver
9+
}
10+
11+
const shouldRender = (docstring: string | null, format: string): boolean => {
12+
if (!docstring) {
13+
return false
14+
}
15+
if (format === 'markdown') {
16+
return false
17+
}
18+
return true
19+
}
20+
21+
const renderDocstring = (docstring: string | null, format: string, roleResolver?: RoleResolver): string | null => {
22+
if (!shouldRender(docstring, format)) {
23+
return null
24+
}
25+
const doc = parseRst(docstring ?? '')
26+
return renderHtml(doc, { roleResolver })
27+
}
28+
29+
const applyToFunction = (fn: ApiFunction, roleResolver?: RoleResolver): ApiFunction => {
30+
return {
31+
...fn,
32+
docstringHtml: renderDocstring(fn.docstring, fn.docstringFormat, roleResolver),
33+
}
34+
}
35+
36+
const applyToMethod = (method: ApiMethod, roleResolver?: RoleResolver): ApiMethod => {
37+
return {
38+
...method,
39+
docstringHtml: renderDocstring(method.docstring, method.docstringFormat, roleResolver),
40+
}
41+
}
42+
43+
const applyToVariable = (variable: ApiVariable, roleResolver?: RoleResolver): ApiVariable => {
44+
return {
45+
...variable,
46+
docstringHtml: renderDocstring(variable.docstring, variable.docstringFormat, roleResolver),
47+
}
48+
}
49+
50+
const applyToClass = (klass: ApiClass, roleResolver?: RoleResolver): ApiClass => {
51+
return {
52+
...klass,
53+
docstringHtml: renderDocstring(klass.docstring, klass.docstringFormat, roleResolver),
54+
methods: klass.methods.map((method) => applyToMethod(method, roleResolver)),
55+
attributes: klass.attributes.map((attribute) => applyToVariable(attribute, roleResolver)),
56+
}
57+
}
58+
59+
const applyToModule = (module: ApiModule, roleResolver?: RoleResolver): ApiModule => {
60+
return {
61+
...module,
62+
docstringHtml: renderDocstring(module.docstring, module.docstringFormat, roleResolver),
63+
classes: module.classes.map((klass) => applyToClass(klass, roleResolver)),
64+
functions: module.functions.map((fn) => applyToFunction(fn, roleResolver)),
65+
variables: module.variables.map((variable) => applyToVariable(variable, roleResolver)),
66+
}
67+
}
68+
69+
export const renderDocstrings = (api: ApiPackage, options: DocstringRenderOptions = {}): ApiPackage => {
70+
const mode = options.mode ?? 'introspect'
71+
if (mode !== 'rst-lite') {
72+
return api
73+
}
74+
75+
return {
76+
...api,
77+
modules: api.modules.map((module) => applyToModule(module, options.roleResolver)),
78+
}
79+
}

astro/packages/astro-autodoc/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ export const autodocComponents = {
1616
Docstring,
1717
}
1818

19+
export * from './docstrings.ts'
1920
export * from './load.ts'
2021
export * from './utils.ts'

astro/packages/astro-autodoc/src/load.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { buildApiPackage } from '@libtmux/api-model'
33
import type { PythonCommand } from '@libtmux/py-bridge'
44
import { introspectPackage } from '@libtmux/py-introspect'
55
import { scanPythonPaths } from '@libtmux/py-parse'
6+
import type { RoleResolver } from '@libtmux/rst-lite'
7+
import { type DocstringRenderMode, renderDocstrings } from './docstrings.ts'
68

79
export type LoadApiOptions = {
810
name: string
@@ -15,6 +17,8 @@ export type LoadApiOptions = {
1517
annotationFormat?: 'string' | 'value'
1618
mockImports?: string[]
1719
autodocMock?: boolean
20+
docstringRenderer?: DocstringRenderMode
21+
docstringRoleResolver?: RoleResolver
1822
pythonCommand?: PythonCommand
1923
}
2024

@@ -39,11 +43,16 @@ export const loadApiPackage = async (options: LoadApiOptions): Promise<ApiPackag
3943
).modules
4044
: undefined
4145

42-
return buildApiPackage(modules, {
46+
const api = buildApiPackage(modules, {
4347
name: options.name,
4448
root: options.root,
4549
includePrivate: options.includePrivate,
4650
generatedAt: options.generatedAt,
4751
introspection,
4852
})
53+
54+
return renderDocstrings(api, {
55+
mode: options.docstringRenderer,
56+
roleResolver: options.docstringRoleResolver,
57+
})
4958
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { ApiPackage } from '@libtmux/api-model'
2+
import { describe, expect, it } from 'vitest'
3+
import { renderDocstrings } from '../src/docstrings.ts'
4+
5+
const location = {
6+
lineno: 1,
7+
colOffset: 0,
8+
endLineno: null,
9+
endColOffset: null,
10+
}
11+
12+
const sampleApi: ApiPackage = {
13+
name: 'demo',
14+
root: '/demo',
15+
generatedAt: '2026-01-05T00:00:00.000Z',
16+
modules: [
17+
{
18+
kind: 'module',
19+
name: 'demo',
20+
qualname: 'demo',
21+
docstring: 'Hello *world*',
22+
docstringFormat: 'rst',
23+
docstringHtml: null,
24+
summary: null,
25+
isPrivate: false,
26+
location,
27+
path: '/demo/__init__.py',
28+
exports: [],
29+
imports: [],
30+
classes: [],
31+
functions: [],
32+
variables: [],
33+
},
34+
],
35+
}
36+
37+
describe('renderDocstrings', () => {
38+
it('renders rst-lite HTML for module docstrings', () => {
39+
const rendered = renderDocstrings(sampleApi, { mode: 'rst-lite' })
40+
expect(rendered.modules[0]?.docstringHtml).toMatchInlineSnapshot('"<p>Hello <em>world</em></p>"')
41+
})
42+
})

astro/pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)