@textmode/tooling-scripts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +40 -0
- package/policy.json +29 -0
- package/src/cli/check-deps.mjs +61 -0
- package/src/cli/install-husky.mjs +23 -0
- package/src/config/create-config.mjs +75 -0
- package/src/husky/commit-msg +1 -0
- package/src/husky/pre-commit +1 -0
- package/src/husky/pre-push +6 -0
- package/src/scripts/add-api-doc-links.mjs +92 -0
- package/src/scripts/check-examples.mjs +81 -0
- package/src/scripts/check-public-api-docs.mjs +128 -0
- package/src/scripts/lib/api-doc-checks.mjs +147 -0
- package/src/scripts/lib/api-doc-links-config.mjs +32 -0
- package/src/scripts/lib/api-doc-routes.mjs +118 -0
- package/src/scripts/lib/example-checks.mjs +389 -0
- package/src/scripts/lib/jsdoc-comments.mjs +78 -0
- package/src/scripts/lib/reflection-policy.mjs +106 -0
- package/src/scripts/lib/reporting.mjs +23 -0
- package/src/scripts/lib/typedoc-project.mjs +25 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/README.md +45 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/accessor-comments.js +53 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/index.js +32 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/render-accessor.js +144 -0
- package/src/typedoc-plugins/accessor-docs-normalizer/theme-patch.js +28 -0
- package/src/typedoc-plugins/all-member-pages-router/README.md +57 -0
- package/src/typedoc-plugins/all-member-pages-router/child-pages.js +65 -0
- package/src/typedoc-plugins/all-member-pages-router/constants.js +58 -0
- package/src/typedoc-plugins/all-member-pages-router/index.js +28 -0
- package/src/typedoc-plugins/all-member-pages-router/member-paths.js +48 -0
- package/src/typedoc-plugins/all-member-pages-router/member-reflections.js +164 -0
- package/src/typedoc-plugins/all-member-pages-router/namespace-paths.js +44 -0
- package/src/typedoc-plugins/all-member-pages-router/router.js +58 -0
- package/src/typedoc-plugins/api-frontmatter/README.md +66 -0
- package/src/typedoc-plugins/api-frontmatter/constants.js +52 -0
- package/src/typedoc-plugins/api-frontmatter/descriptions.js +80 -0
- package/src/typedoc-plugins/api-frontmatter/frontmatter.js +56 -0
- package/src/typedoc-plugins/api-frontmatter/index.js +38 -0
- package/src/typedoc-plugins/api-frontmatter/library-context.js +62 -0
- package/src/typedoc-plugins/api-frontmatter/reflection-metadata.js +95 -0
- package/src/typedoc-plugins/include-code-examples/README.md +40 -0
- package/src/typedoc-plugins/include-code-examples/code-fences.js +55 -0
- package/src/typedoc-plugins/include-code-examples/example-metadata.js +41 -0
- package/src/typedoc-plugins/include-code-examples/index.js +23 -0
- package/src/typedoc-plugins/include-code-examples/transform-markdown.js +33 -0
- package/src/typedoc-plugins/strip-api-self-links/index.js +126 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export function getLineStartOffsets(text) {
|
|
2
|
+
const offsets = [0];
|
|
3
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
4
|
+
if (text[index] === '\n') {
|
|
5
|
+
offsets.push(index + 1);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return offsets;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function findJsdocBeforeLine(text, lineStarts, line) {
|
|
12
|
+
const declarationOffset = lineStarts[line - 1] ?? text.length;
|
|
13
|
+
const searchText = text.slice(0, declarationOffset);
|
|
14
|
+
const start = searchText.lastIndexOf('/**');
|
|
15
|
+
const end = searchText.lastIndexOf('*/');
|
|
16
|
+
|
|
17
|
+
if (start === -1 || end === -1 || start > end) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { start, end };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function replaceHostedApiSeeLines(commentText, seeLine, apiBaseUrl) {
|
|
25
|
+
const lines = commentText.split('\n');
|
|
26
|
+
let replaced = false;
|
|
27
|
+
const nextLines = [];
|
|
28
|
+
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (line.includes('@see') && line.includes(apiBaseUrl)) {
|
|
31
|
+
if (!replaced) {
|
|
32
|
+
nextLines.push(line.replace(/^(\s*\*\s*).*$/, `$1${seeLine}`));
|
|
33
|
+
replaced = true;
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
nextLines.push(line);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return replaced ? nextLines.join('\n') : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function upsertSeeLine(text, comment, seeLine, apiBaseUrl) {
|
|
45
|
+
const commentText = text.slice(comment.start, comment.end + 2);
|
|
46
|
+
const replacedComment = replaceHostedApiSeeLines(commentText, seeLine, apiBaseUrl);
|
|
47
|
+
if (replacedComment !== undefined) {
|
|
48
|
+
return `${text.slice(0, comment.start)}${replacedComment}${text.slice(comment.end + 2)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!commentText.includes('\n')) {
|
|
52
|
+
const lineStart = text.lastIndexOf('\n', comment.start) + 1;
|
|
53
|
+
const indent = text.slice(lineStart, comment.start);
|
|
54
|
+
const summary = commentText
|
|
55
|
+
.replace(/^\/\*\*\s?/, '')
|
|
56
|
+
.replace(/\s?\*\/$/, '')
|
|
57
|
+
.trim();
|
|
58
|
+
const replacement = [
|
|
59
|
+
`${indent}/**`,
|
|
60
|
+
summary ? `${indent} * ${summary}` : undefined,
|
|
61
|
+
`${indent} *`,
|
|
62
|
+
`${indent} * ${seeLine}`,
|
|
63
|
+
`${indent} */`,
|
|
64
|
+
]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join('\n');
|
|
67
|
+
|
|
68
|
+
return `${text.slice(0, lineStart)}${replacement}${text.slice(comment.end + 2)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lineStart = text.lastIndexOf('\n', comment.end) + 1;
|
|
72
|
+
const closingLine = text.slice(lineStart, comment.end + 2);
|
|
73
|
+
const prefixMatch = closingLine.match(/^(\s*)\*\//);
|
|
74
|
+
const indent = prefixMatch?.[1] ?? '';
|
|
75
|
+
const insertion = `${indent}*\n${indent}* ${seeLine}\n`;
|
|
76
|
+
|
|
77
|
+
return `${text.slice(0, lineStart)}${insertion}${text.slice(lineStart)}`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
export function isIncludedReflection(reflection, targetKinds) {
|
|
6
|
+
if (!reflection.kindOf(targetKinds)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (reflection.flags.isPrivate || reflection.flags.isProtected || reflection.flags.isInherited) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (let current = reflection; current && !current.isProject(); current = current.parent) {
|
|
15
|
+
if (current.name.startsWith('_')) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getIncludedReflections(project, targetKinds) {
|
|
24
|
+
return project
|
|
25
|
+
.getReflectionsByKind(targetKinds)
|
|
26
|
+
.filter((reflection) => isIncludedReflection(reflection, targetKinds));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldProcessEntryPointReflection(entryPoint, reflection) {
|
|
30
|
+
return !entryPoint.namespaceExportsOnly || reflection.kind === ReflectionKind.Namespace;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getRelevantComments(reflection) {
|
|
34
|
+
const comments = [];
|
|
35
|
+
|
|
36
|
+
if (reflection.comment) {
|
|
37
|
+
comments.push({ comment: reflection.comment, source: reflection.sources?.[0] });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!reflection.isDeclaration()) {
|
|
41
|
+
return comments;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const signature of reflection.signatures ?? []) {
|
|
45
|
+
if (signature.comment) {
|
|
46
|
+
comments.push({ comment: signature.comment, source: signature.sources?.[0] ?? reflection.sources?.[0] });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (reflection.getSignature?.comment) {
|
|
51
|
+
comments.push({
|
|
52
|
+
comment: reflection.getSignature.comment,
|
|
53
|
+
source: reflection.getSignature.sources?.[0] ?? reflection.sources?.[0],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (reflection.setSignature?.comment) {
|
|
58
|
+
comments.push({
|
|
59
|
+
comment: reflection.setSignature.comment,
|
|
60
|
+
source: reflection.setSignature.sources?.[0] ?? reflection.sources?.[0],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return comments;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getSourceCandidates(reflection) {
|
|
68
|
+
if (!reflection.isDeclaration()) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
...(reflection.sources ?? []),
|
|
74
|
+
...(reflection.signatures ?? []).flatMap((signature) => signature.sources ?? []),
|
|
75
|
+
...(reflection.getSignature?.sources ?? []),
|
|
76
|
+
...(reflection.setSignature?.sources ?? []),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getPrimaryLocation(reflection) {
|
|
81
|
+
const [source] = getSourceCandidates(reflection);
|
|
82
|
+
|
|
83
|
+
if (!source) {
|
|
84
|
+
return {
|
|
85
|
+
file: path.resolve('<unknown>'),
|
|
86
|
+
line: 1,
|
|
87
|
+
column: 1,
|
|
88
|
+
printable: '<unknown>:1:1',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const file = source.fullFileName || path.resolve(source.fileName);
|
|
93
|
+
const line = source.line || 1;
|
|
94
|
+
const column = (source.character ?? 0) + 1;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
file,
|
|
98
|
+
line,
|
|
99
|
+
column,
|
|
100
|
+
printable: `${file}:${line}:${column}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function sourceIsInRoots(source, sourceRoots) {
|
|
105
|
+
return Boolean(source?.fullFileName) && sourceRoots.some((root) => source.fullFileName.startsWith(root));
|
|
106
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function renderIssueSection(title, issues, stream = console.log) {
|
|
2
|
+
if (issues.length === 0) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
stream('');
|
|
7
|
+
stream(`${title}: ${issues.length}`);
|
|
8
|
+
|
|
9
|
+
for (const issue of issues) {
|
|
10
|
+
stream(`- ${issue.name}`);
|
|
11
|
+
stream(` ${issue.location}`);
|
|
12
|
+
if (issue.detail) {
|
|
13
|
+
stream(` ${issue.detail}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function renderList(title, items, stream = console.error) {
|
|
19
|
+
stream(title);
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
stream(` * ${item}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Application, PackageJsonReader, TSConfigReader } from 'typedoc';
|
|
2
|
+
|
|
3
|
+
export async function loadTypeDocProject({ entryPoint, tsconfig = 'tsconfig.json', errorMessage }) {
|
|
4
|
+
const app = await Application.bootstrap(
|
|
5
|
+
{
|
|
6
|
+
entryPoints: [entryPoint],
|
|
7
|
+
entryPointStrategy: 'resolve',
|
|
8
|
+
tsconfig,
|
|
9
|
+
excludeInternal: true,
|
|
10
|
+
excludePrivate: true,
|
|
11
|
+
excludeProtected: true,
|
|
12
|
+
readme: 'none',
|
|
13
|
+
emit: 'none',
|
|
14
|
+
logLevel: 'Error',
|
|
15
|
+
},
|
|
16
|
+
[new PackageJsonReader(), new TSConfigReader()]
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const project = await app.convert();
|
|
20
|
+
if (!project || app.logger.hasErrors()) {
|
|
21
|
+
throw new Error(errorMessage ?? `TypeDoc failed to resolve ${entryPoint}.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return project;
|
|
25
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Accessor Docs Normalizer
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
TypeDoc stores comments for TypeScript accessors on the getter and setter signatures. `typedoc-plugin-markdown`
|
|
6
|
+
overview tables read descriptions from the accessor reflection itself. When the reflection has no direct comment,
|
|
7
|
+
the generated table falls back to `-` even though the getter or setter is documented in source.
|
|
8
|
+
|
|
9
|
+
This plugin normalizes that shape for the textmode.js API docs:
|
|
10
|
+
|
|
11
|
+
- Copies the first getter or setter signature comment onto an accessor reflection when the accessor has no comment.
|
|
12
|
+
- Tracks copied comments so the accessor page does not render the same summary twice.
|
|
13
|
+
- Replaces the markdown accessor partial so getter documentation appears before setter documentation.
|
|
14
|
+
- Keeps getter return sections, setter parameter sections, inheritance sections, and `disableSources` behavior intact.
|
|
15
|
+
|
|
16
|
+
The plugin does not change runtime library behavior, the public TypeScript API, source JSDoc ownership, or TypeDoc
|
|
17
|
+
reflection conversion. It only adjusts markdown rendering after TypeDoc has built the reflection model.
|
|
18
|
+
|
|
19
|
+
This exists to avoid source-level documentation stubs or declaration-merging boilerplate for accessor descriptions.
|
|
20
|
+
Accessor docs should stay in the source declarations that own the getter and setter behavior.
|
|
21
|
+
|
|
22
|
+
## Preserved Output Contract
|
|
23
|
+
|
|
24
|
+
- Accessor overview tables show the first available getter or setter summary.
|
|
25
|
+
- Accessor pages do not render copied comments twice.
|
|
26
|
+
- Getter content renders before setter content.
|
|
27
|
+
- Getter return sections, setter parameter sections, inheritance sections, and `disableSources` behavior match
|
|
28
|
+
`typedoc-plugin-markdown`.
|
|
29
|
+
|
|
30
|
+
## Contributor Notes
|
|
31
|
+
|
|
32
|
+
- Safe to edit: helper names, tests, README wording, and the local comment-normalization traversal.
|
|
33
|
+
- Be careful with: `theme-patch.js`, because it monkey-patches `MarkdownTheme.prototype.getRenderContext`.
|
|
34
|
+
- Do not casually change: accessor partial output order, copied-comment tracking, or source-display conditions.
|
|
35
|
+
|
|
36
|
+
## Maintenance
|
|
37
|
+
|
|
38
|
+
This plugin depends on `typedoc-plugin-markdown` theme internals, especially `MarkdownTheme.getRenderContext` and the
|
|
39
|
+
`partials.accessor` contract. Re-check this plugin after upgrading `typedoc-plugin-markdown`.
|
|
40
|
+
|
|
41
|
+
Upgrade checklist:
|
|
42
|
+
|
|
43
|
+
- Compare `render-accessor.js` against the current upstream accessor partial.
|
|
44
|
+
- Confirm `MarkdownTheme.getRenderContext` still exists and returns a mutable partials object.
|
|
45
|
+
- Run `npm run test:tooling` and inspect generated accessor pages for duplicate summaries.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
const syntheticAccessorComments = new WeakSet();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the first signature-level comment available for an accessor reflection.
|
|
9
|
+
* TypeDoc currently keeps getter/setter comments on their signatures, while
|
|
10
|
+
* overview tables read from the accessor declaration itself.
|
|
11
|
+
*
|
|
12
|
+
* @param {import('typedoc').DeclarationReflection} accessor
|
|
13
|
+
* @returns {import('typedoc').Comment | undefined}
|
|
14
|
+
*/
|
|
15
|
+
function getAccessorSignatureComment(accessor) {
|
|
16
|
+
return accessor.getSignature?.comment ?? accessor.setSignature?.comment;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Copy signature comments onto accessor declarations when needed so markdown
|
|
21
|
+
* overview tables can render descriptions instead of falling back to "-".
|
|
22
|
+
*
|
|
23
|
+
* @param {import('typedoc').Reflection | undefined} model
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeAccessorComments(model) {
|
|
27
|
+
if (!model) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (model.kind === ReflectionKind.Accessor && 'comment' in model && !model.comment) {
|
|
32
|
+
const signatureComment = getAccessorSignatureComment(model);
|
|
33
|
+
if (signatureComment) {
|
|
34
|
+
model.comment = signatureComment;
|
|
35
|
+
syntheticAccessorComments.add(model);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
model.traverse?.((child) => {
|
|
40
|
+
normalizeAccessorComments(child);
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check whether a comment was copied onto an accessor by this plugin.
|
|
47
|
+
*
|
|
48
|
+
* @param {import('typedoc').DeclarationReflection} accessor
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
export function hasSyntheticAccessorComment(accessor) {
|
|
52
|
+
return syntheticAccessorComments.has(accessor);
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown';
|
|
4
|
+
|
|
5
|
+
import { normalizeAccessorComments } from './accessor-comments.js';
|
|
6
|
+
import { renderAccessorDocs } from './render-accessor.js';
|
|
7
|
+
import { patchMarkdownThemeRenderContext } from './theme-patch.js';
|
|
8
|
+
|
|
9
|
+
class AccessorDocsMarkdownThemeContext extends MarkdownThemeContext {
|
|
10
|
+
/**
|
|
11
|
+
* @param {MarkdownTheme} theme
|
|
12
|
+
* @param {import('typedoc-plugin-markdown').MarkdownPageEvent<import('typedoc').Reflection>} page
|
|
13
|
+
* @param {import('typedoc').Options} options
|
|
14
|
+
*/
|
|
15
|
+
constructor(theme, page, options) {
|
|
16
|
+
super(theme, page, options);
|
|
17
|
+
normalizeAccessorComments(page.model);
|
|
18
|
+
this.partials.accessor = (accessor, partialOptions) => renderAccessorDocs(this, accessor, partialOptions);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('typedoc').Application} app
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
export function load(app) {
|
|
27
|
+
patchMarkdownThemeRenderContext(
|
|
28
|
+
(theme, page) => new AccessorDocsMarkdownThemeContext(theme, page, theme.application.options)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
app.logger.verbose('[typedoc] Registered accessor docs normalizer');
|
|
32
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { i18n, ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
import { hasSyntheticAccessorComment } from './accessor-comments.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {number} level
|
|
9
|
+
* @param {string} text
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function heading(level, text) {
|
|
13
|
+
return `${'#'.repeat(level)} ${text}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render one getter or setter signature.
|
|
18
|
+
*
|
|
19
|
+
* @param {import('typedoc-plugin-markdown').MarkdownThemeContext} context
|
|
20
|
+
* @param {import('typedoc').SignatureReflection} signature
|
|
21
|
+
* @param {{
|
|
22
|
+
* accessor: 'get' | 'set';
|
|
23
|
+
* headingLevel: number;
|
|
24
|
+
* includeParameters: boolean;
|
|
25
|
+
* showSources: boolean;
|
|
26
|
+
* title: string;
|
|
27
|
+
* }} options
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function renderAccessorSignature(context, signature, options) {
|
|
31
|
+
const md = [];
|
|
32
|
+
|
|
33
|
+
md.push(heading(options.headingLevel, options.title));
|
|
34
|
+
md.push(
|
|
35
|
+
context.partials.signatureTitle(signature, {
|
|
36
|
+
accessor: options.accessor,
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (options.showSources && !context.options.getValue('disableSources') && signature.sources) {
|
|
41
|
+
md.push(context.partials.sources(signature));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (signature.comment) {
|
|
45
|
+
md.push(
|
|
46
|
+
context.partials.comment(signature.comment, {
|
|
47
|
+
headingLevel: options.headingLevel + 1,
|
|
48
|
+
showTags: false,
|
|
49
|
+
showSummary: true,
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (options.includeParameters && signature.parameters?.length) {
|
|
55
|
+
md.push(heading(options.headingLevel + 1, ReflectionKind.pluralString(ReflectionKind.Parameter)));
|
|
56
|
+
if (context.helpers.useTableFormat('parameters')) {
|
|
57
|
+
md.push(context.partials.parametersTable(signature.parameters));
|
|
58
|
+
} else {
|
|
59
|
+
md.push(
|
|
60
|
+
context.partials.parametersList(signature.parameters, {
|
|
61
|
+
headingLevel: options.headingLevel + 1,
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (signature.type) {
|
|
68
|
+
md.push(
|
|
69
|
+
context.partials.signatureReturns(signature, {
|
|
70
|
+
headingLevel: options.headingLevel + 1,
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (signature.comment) {
|
|
76
|
+
md.push(
|
|
77
|
+
context.partials.comment(signature.comment, {
|
|
78
|
+
headingLevel: options.headingLevel + 1,
|
|
79
|
+
showTags: true,
|
|
80
|
+
showSummary: false,
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return md.filter(Boolean).join('\n\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Render accessor documentation with getter content before setter content.
|
|
90
|
+
*
|
|
91
|
+
* @param {import('typedoc-plugin-markdown').MarkdownThemeContext} context
|
|
92
|
+
* @param {import('typedoc').DeclarationReflection} accessor
|
|
93
|
+
* @param {{ headingLevel: number }} options
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
export function renderAccessorDocs(context, accessor, options) {
|
|
97
|
+
const md = [];
|
|
98
|
+
const showSources = accessor.parent?.kind !== ReflectionKind.TypeLiteral;
|
|
99
|
+
|
|
100
|
+
if (accessor.getSignature) {
|
|
101
|
+
md.push(
|
|
102
|
+
renderAccessorSignature(context, accessor.getSignature, {
|
|
103
|
+
accessor: 'get',
|
|
104
|
+
headingLevel: options.headingLevel,
|
|
105
|
+
includeParameters: false,
|
|
106
|
+
showSources,
|
|
107
|
+
title: i18n.kind_get_signature(),
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (accessor.setSignature) {
|
|
113
|
+
md.push(
|
|
114
|
+
renderAccessorSignature(context, accessor.setSignature, {
|
|
115
|
+
accessor: 'set',
|
|
116
|
+
headingLevel: options.headingLevel,
|
|
117
|
+
includeParameters: true,
|
|
118
|
+
showSources,
|
|
119
|
+
title: i18n.kind_set_signature(),
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
showSources &&
|
|
126
|
+
!context.options.getValue('disableSources') &&
|
|
127
|
+
!accessor.getSignature &&
|
|
128
|
+
!accessor.setSignature
|
|
129
|
+
) {
|
|
130
|
+
md.push(context.partials.sources(accessor));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (accessor.comment && !hasSyntheticAccessorComment(accessor)) {
|
|
134
|
+
md.push(
|
|
135
|
+
context.partials.comment(accessor.comment, {
|
|
136
|
+
headingLevel: options.headingLevel,
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
md.push(context.partials.inheritance(accessor, { headingLevel: options.headingLevel }));
|
|
142
|
+
|
|
143
|
+
return md.filter(Boolean).join('\n\n');
|
|
144
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { MarkdownTheme } from 'typedoc-plugin-markdown';
|
|
4
|
+
|
|
5
|
+
let isPatched = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Patch typedoc-plugin-markdown's theme render-context factory once.
|
|
9
|
+
*
|
|
10
|
+
* This is the only place this plugin reaches into markdown theme internals.
|
|
11
|
+
* Keep the patch idempotent because TypeDoc can load plugins repeatedly in
|
|
12
|
+
* watch mode and tests can import the plugin more than once.
|
|
13
|
+
*
|
|
14
|
+
* @param {(theme: MarkdownTheme, page: import('typedoc-plugin-markdown').MarkdownPageEvent<import('typedoc').Reflection>) => import('typedoc-plugin-markdown').MarkdownThemeContext} createContext
|
|
15
|
+
* @returns {boolean} True when this call installed the patch.
|
|
16
|
+
*/
|
|
17
|
+
export function patchMarkdownThemeRenderContext(createContext) {
|
|
18
|
+
if (isPatched) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
MarkdownTheme.prototype.getRenderContext = function (page) {
|
|
23
|
+
return createContext(this, page);
|
|
24
|
+
};
|
|
25
|
+
isPatched = true;
|
|
26
|
+
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# All Member Pages Router
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This TypeDoc router makes direct public methods render as focused Markdown pages when an owner page would otherwise
|
|
6
|
+
render too many Sandpack examples.
|
|
7
|
+
|
|
8
|
+
`typedoc-plugin-markdown` can render top-level API structures as pages, but textmode.js also needs compact class and
|
|
9
|
+
interface overview pages that link to individual methods once their inline examples would make the page heavy. This
|
|
10
|
+
router keeps those owner pages small and gives each direct method a stable page.
|
|
11
|
+
|
|
12
|
+
## Member Pages
|
|
13
|
+
|
|
14
|
+
The router creates pages for direct class/interface methods when rendering the owner page inline would exceed three
|
|
15
|
+
Sandpack examples:
|
|
16
|
+
|
|
17
|
+
- methods
|
|
18
|
+
|
|
19
|
+
Nested type-literal fields intentionally remain inline or anchor-based so structural types do not explode into many
|
|
20
|
+
small pages.
|
|
21
|
+
|
|
22
|
+
## Paths
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
|
|
26
|
+
- `classes/Textmodifier/methods/background.md`
|
|
27
|
+
- `namespaces/layering/classes/TextmodeLayer/methods/draw.md`
|
|
28
|
+
|
|
29
|
+
## Preserved Output Contract
|
|
30
|
+
|
|
31
|
+
- The router name remains `all-member-pages`.
|
|
32
|
+
- Direct methods get pages only when their class/interface owner would otherwise exceed three Sandpack examples.
|
|
33
|
+
- Properties, accessors, constructors, variables, type aliases, functions, and nested type-literal members do not get
|
|
34
|
+
extra pages from this router.
|
|
35
|
+
- Existing `MemberRouter` behavior for modules, namespaces, documents, and configured `membersWithOwnFile` kinds is
|
|
36
|
+
preserved.
|
|
37
|
+
- Project-root namespaces use the flattened textmode.js path shape, for example `namespaces/color/classes/TextmodeColor.md`.
|
|
38
|
+
- Sidebar entries include generated member pages under their owners.
|
|
39
|
+
|
|
40
|
+
## Contributor Notes
|
|
41
|
+
|
|
42
|
+
- Safe to edit: constants, helper names, tests, and README wording.
|
|
43
|
+
- Be careful with: `child-pages.js`, which intentionally mirrors upstream `MemberRouter` traversal.
|
|
44
|
+
- Do not casually change: path segment names, namespace path flattening, example-count thresholds, or direct-method page
|
|
45
|
+
eligibility.
|
|
46
|
+
|
|
47
|
+
## Maintenance
|
|
48
|
+
|
|
49
|
+
This plugin extends `typedoc-plugin-markdown`'s `MemberRouter` and copies its child-page traversal policy so direct
|
|
50
|
+
members can participate in TypeDoc's `hasOwnDocument` behavior. Re-check this router after upgrading TypeDoc or
|
|
51
|
+
`typedoc-plugin-markdown`, especially around router APIs, `membersWithOwnFile`, and namespace path handling.
|
|
52
|
+
|
|
53
|
+
Upgrade checklist:
|
|
54
|
+
|
|
55
|
+
- Compare `child-pages.js` against the current upstream `MemberRouter.buildChildPages`.
|
|
56
|
+
- Confirm `PageKind.Reflection`, `Slugger`, `fullUrls`, and `sluggers` are still the correct router APIs.
|
|
57
|
+
- Run `npm run build:docs` and verify generated method links still resolve.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { Reflection, Slugger } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
import { isDirectMemberPageReflection, shouldRenderOwnPage } from './member-reflections.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* extension: string;
|
|
10
|
+
* fullUrls: Map<import('typedoc').Reflection, string>;
|
|
11
|
+
* sluggerConfiguration: unknown;
|
|
12
|
+
* sluggers: Map<import('typedoc').Reflection, Slugger>;
|
|
13
|
+
* getFileName(idealName: string): string;
|
|
14
|
+
* getIdealBaseName(reflection: import('typedoc').Reflection): string;
|
|
15
|
+
* getPageKind(reflection: import('typedoc').Reflection): import('typedoc').PageKind | undefined;
|
|
16
|
+
* shouldWritePage(reflection: import('typedoc').Reflection): boolean;
|
|
17
|
+
* buildAnchors(reflection: import('typedoc').Reflection, parent?: import('typedoc').Reflection): void;
|
|
18
|
+
* membersWithOwnFile: string[];
|
|
19
|
+
* kindsToString: Map<import('typedoc').ReflectionKind, string>;
|
|
20
|
+
* }} ChildPageRouter
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build child pages using typedoc-plugin-markdown's MemberRouter traversal
|
|
25
|
+
* shape, while delegating textmode.js' expanded own-page policy to local
|
|
26
|
+
* helpers. This mirrors upstream traversal intentionally; when upgrading
|
|
27
|
+
* typedoc-plugin-markdown, compare this function against MemberRouter.
|
|
28
|
+
*
|
|
29
|
+
* @param {ChildPageRouter} router
|
|
30
|
+
* @param {import('typedoc').Reflection} reflection
|
|
31
|
+
* @param {import('typedoc').PageDefinition[]} outPages
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
export function buildMemberRouterChildPages(router, reflection, outPages) {
|
|
35
|
+
const kind = router.getPageKind(reflection);
|
|
36
|
+
if (!kind) {
|
|
37
|
+
router.buildAnchors(reflection, reflection.parent);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const shouldWritePage = isDirectMemberPageReflection(reflection) || router.shouldWritePage(reflection);
|
|
42
|
+
const idealName = router.getIdealBaseName(reflection);
|
|
43
|
+
const actualName = shouldWritePage ? router.getFileName(idealName) : `${idealName}${router.extension}`;
|
|
44
|
+
router.fullUrls.set(reflection, actualName);
|
|
45
|
+
|
|
46
|
+
if (shouldRenderOwnPage(reflection, router)) {
|
|
47
|
+
if (shouldWritePage) {
|
|
48
|
+
router.sluggers.set(reflection, new Slugger(router.sluggerConfiguration));
|
|
49
|
+
outPages.push({
|
|
50
|
+
kind,
|
|
51
|
+
model: reflection,
|
|
52
|
+
url: actualName,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
router.buildAnchors(reflection, reflection.parent);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (reflection instanceof Reflection) {
|
|
60
|
+
reflection.traverse((child) => {
|
|
61
|
+
buildMemberRouterChildPages(router, child, outPages);
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|