@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,58 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
export const ROUTER_NAME = 'all-member-pages';
|
|
6
|
+
|
|
7
|
+
export const ALWAYS_OWN_PAGE_KIND_NAMES = ['Module', 'Namespace', 'Document'];
|
|
8
|
+
|
|
9
|
+
export const MAX_INLINE_SANDPACK_EXAMPLES = 3;
|
|
10
|
+
|
|
11
|
+
export const MEMBER_PAGE_OWNER_KINDS = new Set([ReflectionKind.Class, ReflectionKind.Interface]);
|
|
12
|
+
|
|
13
|
+
export const MEMBER_PAGE_KINDS = new Set([ReflectionKind.Method, ReflectionKind.Property, ReflectionKind.Accessor]);
|
|
14
|
+
|
|
15
|
+
export const MEMBER_DIRECTORIES = new Map([
|
|
16
|
+
[ReflectionKind.Method, 'methods'],
|
|
17
|
+
[ReflectionKind.Property, 'properties'],
|
|
18
|
+
[ReflectionKind.Accessor, 'accessors'],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const MEMBER_KIND_BY_NAME = new Map([
|
|
22
|
+
['method', ReflectionKind.Method],
|
|
23
|
+
['property', ReflectionKind.Property],
|
|
24
|
+
['accessor', ReflectionKind.Accessor],
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string[]} memberPageKindNames
|
|
29
|
+
* @returns {void}
|
|
30
|
+
*/
|
|
31
|
+
export function configureMemberPages(memberPageKindNames) {
|
|
32
|
+
MEMBER_PAGE_KINDS.clear();
|
|
33
|
+
MEMBER_DIRECTORIES.clear();
|
|
34
|
+
|
|
35
|
+
const normalizedMemberPageKindNames = Array.isArray(memberPageKindNames)
|
|
36
|
+
? memberPageKindNames
|
|
37
|
+
: String(memberPageKindNames).split(',');
|
|
38
|
+
|
|
39
|
+
for (const memberPageKindName of normalizedMemberPageKindNames) {
|
|
40
|
+
const kind = MEMBER_KIND_BY_NAME.get(String(memberPageKindName).toLowerCase());
|
|
41
|
+
|
|
42
|
+
if (kind === undefined) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
MEMBER_PAGE_KINDS.add(kind);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (MEMBER_PAGE_KINDS.has(ReflectionKind.Method)) {
|
|
50
|
+
MEMBER_DIRECTORIES.set(ReflectionKind.Method, 'methods');
|
|
51
|
+
}
|
|
52
|
+
if (MEMBER_PAGE_KINDS.has(ReflectionKind.Property)) {
|
|
53
|
+
MEMBER_DIRECTORIES.set(ReflectionKind.Property, 'properties');
|
|
54
|
+
}
|
|
55
|
+
if (MEMBER_PAGE_KINDS.has(ReflectionKind.Accessor)) {
|
|
56
|
+
MEMBER_DIRECTORIES.set(ReflectionKind.Accessor, 'accessors');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { Converter, ParameterType } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
import { configureMemberPages, ROUTER_NAME } from './constants.js';
|
|
6
|
+
import { AllMemberPagesRouter } from './router.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TypeDoc plugin load function.
|
|
10
|
+
*
|
|
11
|
+
* @param {import('typedoc').Application} app
|
|
12
|
+
* @returns {void}
|
|
13
|
+
*/
|
|
14
|
+
export function load(app) {
|
|
15
|
+
app.options.addDeclaration({
|
|
16
|
+
name: 'textmodeMemberPageKinds',
|
|
17
|
+
help: 'Direct member kinds that may be split into focused API pages.',
|
|
18
|
+
type: ParameterType.Array,
|
|
19
|
+
defaultValue: ['method', 'property', 'accessor'],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.converter.on(Converter.EVENT_BEGIN, () => {
|
|
23
|
+
configureMemberPages(app.options.getValue('textmodeMemberPageKinds'));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.renderer.defineRouter(ROUTER_NAME, AllMemberPagesRouter);
|
|
27
|
+
app.logger.verbose('[typedoc] Registered all member pages router');
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { MEMBER_DIRECTORIES } from './constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{
|
|
7
|
+
* getIdealBaseName(reflection: import('typedoc').Reflection): string;
|
|
8
|
+
* }} MemberDirectoryRouter
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{
|
|
13
|
+
* getReflectionAlias(reflection: import('typedoc').Reflection): string;
|
|
14
|
+
* }} ReflectionAliasRouter
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the directory name for a direct Sandpack member kind.
|
|
19
|
+
*
|
|
20
|
+
* @param {import('typedoc').ReflectionKind} kind
|
|
21
|
+
* @returns {string | undefined}
|
|
22
|
+
*/
|
|
23
|
+
export function getMemberDirectoryName(kind) {
|
|
24
|
+
return MEMBER_DIRECTORIES.get(kind);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the directory path for a direct Sandpack member page.
|
|
29
|
+
*
|
|
30
|
+
* @param {MemberDirectoryRouter} router
|
|
31
|
+
* @param {import('typedoc').Reflection} reflection
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function getDirectMemberDirectory(router, reflection) {
|
|
35
|
+
const directory = getMemberDirectoryName(reflection.kind);
|
|
36
|
+
return `${router.getIdealBaseName(reflection.parent)}/${directory}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a reflection filename for a direct Sandpack member page.
|
|
41
|
+
*
|
|
42
|
+
* @param {ReflectionAliasRouter} router
|
|
43
|
+
* @param {import('typedoc').Reflection} reflection
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function getReflectionFileName(router, reflection) {
|
|
47
|
+
return router.getReflectionAlias(reflection);
|
|
48
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ALWAYS_OWN_PAGE_KIND_NAMES,
|
|
7
|
+
MEMBER_PAGE_KINDS,
|
|
8
|
+
MEMBER_PAGE_OWNER_KINDS,
|
|
9
|
+
MAX_INLINE_SANDPACK_EXAMPLES,
|
|
10
|
+
} from './constants.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{
|
|
14
|
+
* membersWithOwnFile: string[];
|
|
15
|
+
* kindsToString: Map<import('typedoc').ReflectionKind, string>;
|
|
16
|
+
* }} OwnPagePolicyRouter
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {import('typedoc').Reflection | undefined} reflection
|
|
21
|
+
* @returns {string[]}
|
|
22
|
+
*/
|
|
23
|
+
function getOwnSandpackExamples(reflection) {
|
|
24
|
+
return (reflection?.comment?.blockTags || []).flatMap((tag) => {
|
|
25
|
+
if (tag.tag !== '@example') {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = (tag.content || []).map((part) => part.text).join('');
|
|
30
|
+
return /```(?:js|javascript|jsx|ts|typescript|tsx)\b/i.test(content) && /@title\b/.test(content)
|
|
31
|
+
? [content]
|
|
32
|
+
: [];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {import('typedoc').Reflection} reflection
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
function countReflectionSandpackExamples(reflection) {
|
|
41
|
+
const examples = new Set([
|
|
42
|
+
...getOwnSandpackExamples(reflection),
|
|
43
|
+
...(reflection.signatures || []).flatMap((signature) => getOwnSandpackExamples(signature)),
|
|
44
|
+
...getOwnSandpackExamples(reflection.getSignature),
|
|
45
|
+
...getOwnSandpackExamples(reflection.setSignature),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
return examples.size;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {import('typedoc').Reflection} reflection
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
function isPropertyLikeReflection(reflection) {
|
|
56
|
+
return reflection.kind === ReflectionKind.Property || reflection.kind === ReflectionKind.Accessor;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {import('typedoc').Reflection} reflection
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
function isSandpackThresholdMemberReflection(reflection) {
|
|
64
|
+
return (
|
|
65
|
+
(reflection.kind === ReflectionKind.Method || isPropertyLikeReflection(reflection)) &&
|
|
66
|
+
countReflectionSandpackExamples(reflection) > 0
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {import('typedoc').Reflection} owner
|
|
72
|
+
* @returns {number}
|
|
73
|
+
*/
|
|
74
|
+
function countInlineMemberSandpackExamples(owner) {
|
|
75
|
+
return (owner.children || [])
|
|
76
|
+
.filter((child) => isSandpackThresholdMemberReflection(child))
|
|
77
|
+
.reduce((count, member) => count + countReflectionSandpackExamples(member), 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check whether direct members should become focused pages for this owner.
|
|
82
|
+
*
|
|
83
|
+
* @param {import('typedoc').Reflection} owner
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function shouldRenderMemberPages(owner) {
|
|
87
|
+
if (!MEMBER_PAGE_OWNER_KINDS.has(owner.kind)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const inlineExampleCount = countReflectionSandpackExamples(owner) + countInlineMemberSandpackExamples(owner);
|
|
92
|
+
return inlineExampleCount > MAX_INLINE_SANDPACK_EXAMPLES;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {import('typedoc').Reflection | undefined} reflection
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
function isDirectManagedMemberReflection(reflection) {
|
|
100
|
+
return Boolean(
|
|
101
|
+
reflection?.parent &&
|
|
102
|
+
MEMBER_PAGE_OWNER_KINDS.has(reflection.parent.kind) &&
|
|
103
|
+
MEMBER_PAGE_KINDS.has(reflection.kind)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function shouldSuppressDefaultDirectMemberPagePolicy() {
|
|
108
|
+
return MEMBER_PAGE_KINDS.has(ReflectionKind.Property) || MEMBER_PAGE_KINDS.has(ReflectionKind.Accessor);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function shouldRenderAllDirectMethodPages() {
|
|
112
|
+
return MEMBER_PAGE_KINDS.size === 1 && MEMBER_PAGE_KINDS.has(ReflectionKind.Method);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check whether a reflection is a direct method or example-bearing property
|
|
117
|
+
* that should become a focused page.
|
|
118
|
+
*
|
|
119
|
+
* @param {import('typedoc').Reflection | undefined} reflection
|
|
120
|
+
* @returns {boolean}
|
|
121
|
+
*/
|
|
122
|
+
export function isDirectMemberPageReflection(reflection) {
|
|
123
|
+
if (
|
|
124
|
+
shouldRenderAllDirectMethodPages() &&
|
|
125
|
+
reflection?.parent &&
|
|
126
|
+
MEMBER_PAGE_OWNER_KINDS.has(reflection.parent.kind) &&
|
|
127
|
+
reflection.kind === ReflectionKind.Method
|
|
128
|
+
) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!isDirectManagedMemberReflection(reflection) || !shouldRenderMemberPages(reflection.parent)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (reflection.kind === ReflectionKind.Method) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return isSandpackThresholdMemberReflection(reflection);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Preserve the markdown MemberRouter own-page policy while extending it to
|
|
145
|
+
* direct methods and example-bearing properties when the owner page would
|
|
146
|
+
* otherwise render too many Sandpack examples.
|
|
147
|
+
*
|
|
148
|
+
* @param {import('typedoc').Reflection} reflection
|
|
149
|
+
* @param {OwnPagePolicyRouter} router
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
export function shouldRenderOwnPage(reflection, router) {
|
|
153
|
+
if (isDirectMemberPageReflection(reflection)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (shouldSuppressDefaultDirectMemberPagePolicy() && isDirectManagedMemberReflection(reflection)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return [...ALWAYS_OWN_PAGE_KIND_NAMES, ...router.membersWithOwnFile].includes(
|
|
162
|
+
router.kindsToString.get(reflection.kind)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check whether a namespace is directly rooted in the TypeDoc project after
|
|
7
|
+
* walking through any nested namespace ancestors.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('typedoc').Reflection} reflection
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isProjectNamespace(reflection) {
|
|
13
|
+
let ancestor = reflection.parent;
|
|
14
|
+
while (ancestor && ancestor.kind === ReflectionKind.Namespace) {
|
|
15
|
+
ancestor = ancestor.parent;
|
|
16
|
+
}
|
|
17
|
+
return ancestor?.kind === ReflectionKind.Project;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the flattened project namespace path used by the textmode.js docs.
|
|
22
|
+
*
|
|
23
|
+
* @param {{
|
|
24
|
+
* directories: Map<import('typedoc').ReflectionKind, string>;
|
|
25
|
+
* getReflectionAlias(reflection: import('typedoc').Reflection): string;
|
|
26
|
+
* }} router
|
|
27
|
+
* @param {import('typedoc').Reflection} reflection
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function buildNamespacePath(router, reflection) {
|
|
31
|
+
const stack = [];
|
|
32
|
+
let current = reflection;
|
|
33
|
+
while (current && current.kind === ReflectionKind.Namespace) {
|
|
34
|
+
stack.unshift(current);
|
|
35
|
+
current = current.parent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const segments = [];
|
|
39
|
+
for (const namespace of stack) {
|
|
40
|
+
segments.push(router.directories.get(ReflectionKind.Namespace), router.getReflectionAlias(namespace));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return segments.join('/');
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { PageKind } from 'typedoc';
|
|
4
|
+
import { MemberRouter } from 'typedoc-plugin-markdown';
|
|
5
|
+
|
|
6
|
+
import { buildMemberRouterChildPages } from './child-pages.js';
|
|
7
|
+
import { getDirectMemberDirectory, getReflectionFileName as getMemberReflectionFileName } from './member-paths.js';
|
|
8
|
+
import { isDirectMemberPageReflection } from './member-reflections.js';
|
|
9
|
+
import { buildNamespacePath, isProjectNamespace } from './namespace-paths.js';
|
|
10
|
+
|
|
11
|
+
export class AllMemberPagesRouter extends MemberRouter {
|
|
12
|
+
/** @param {import('typedoc').Reflection} reflection */
|
|
13
|
+
getPageKind(reflection) {
|
|
14
|
+
if (isDirectMemberPageReflection(reflection)) {
|
|
15
|
+
return PageKind.Reflection;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return super.getPageKind(reflection);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This mirrors typedoc-plugin-markdown's MemberRouter child traversal while
|
|
23
|
+
* delegating the "own page" policy to this plugin's helpers.
|
|
24
|
+
*
|
|
25
|
+
* @param {import('typedoc').Reflection} reflection
|
|
26
|
+
* @param {import('typedoc').PageDefinition[]} outPages
|
|
27
|
+
*/
|
|
28
|
+
buildChildPages(reflection, outPages) {
|
|
29
|
+
buildMemberRouterChildPages(this, reflection, outPages);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {import('typedoc').Reflection} reflection */
|
|
33
|
+
getNamespaceDirectory(reflection) {
|
|
34
|
+
if (isProjectNamespace(reflection)) {
|
|
35
|
+
return buildNamespacePath(this, reflection);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return super.getNamespaceDirectory(reflection);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {import('typedoc').Reflection} reflection */
|
|
42
|
+
getReflectionDirectory(reflection) {
|
|
43
|
+
if (isDirectMemberPageReflection(reflection)) {
|
|
44
|
+
return getDirectMemberDirectory(this, reflection);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return super.getReflectionDirectory(reflection);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {import('typedoc').Reflection} reflection */
|
|
51
|
+
getReflectionFileName(reflection) {
|
|
52
|
+
if (isDirectMemberPageReflection(reflection)) {
|
|
53
|
+
return getMemberReflectionFileName(this, reflection);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return super.getReflectionFileName(reflection);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# API Frontmatter
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This TypeDoc plugin adds textmode.js API metadata to Markdown pages generated by `typedoc-plugin-markdown` and
|
|
6
|
+
`typedoc-plugin-frontmatter`.
|
|
7
|
+
|
|
8
|
+
`typedoc-plugin-frontmatter` creates the YAML block and handles configured globals such as `layout` and `editLink`.
|
|
9
|
+
This plugin enriches that frontmatter with API-specific fields used by the VitePress documentation experience.
|
|
10
|
+
|
|
11
|
+
## Metadata
|
|
12
|
+
|
|
13
|
+
The plugin preserves existing frontmatter and adds:
|
|
14
|
+
|
|
15
|
+
- `title`
|
|
16
|
+
- `description`
|
|
17
|
+
- `category`
|
|
18
|
+
- `api`
|
|
19
|
+
- `owner`
|
|
20
|
+
- `namespace`
|
|
21
|
+
- `kind`
|
|
22
|
+
- `ecosystem`
|
|
23
|
+
- `lastModified`
|
|
24
|
+
- `hasConstructor`
|
|
25
|
+
- `isInterface`
|
|
26
|
+
|
|
27
|
+
Undefined values are removed before the page is written.
|
|
28
|
+
|
|
29
|
+
## Behavior
|
|
30
|
+
|
|
31
|
+
Descriptions come from the first useful TypeDoc comment summary on the reflection, call signature, getter signature, or
|
|
32
|
+
setter signature. The summary is reduced to the first paragraph, sanitized for YAML frontmatter, and truncated to a
|
|
33
|
+
short SEO-friendly length. If no summary exists, the plugin generates a kind-specific fallback description.
|
|
34
|
+
|
|
35
|
+
`lastModified` intentionally uses the docs build date to preserve the existing generated output contract.
|
|
36
|
+
|
|
37
|
+
Packages whose names start with `textmode` are assigned to the `textmode.js` ecosystem. Other package names use their
|
|
38
|
+
own name as the ecosystem.
|
|
39
|
+
|
|
40
|
+
## Preserved Output Contract
|
|
41
|
+
|
|
42
|
+
- Existing frontmatter is preserved first, then API fields override matching keys.
|
|
43
|
+
- Undefined values are removed before output.
|
|
44
|
+
- Descriptions prefer reflection comments, then call signatures, getter signatures, setter signatures, then fallback
|
|
45
|
+
templates.
|
|
46
|
+
- Descriptions use the first paragraph, strip markdown links/raw URLs/backticks, collapse whitespace, and truncate to
|
|
47
|
+
160 characters.
|
|
48
|
+
- `lastModified` remains the docs build date.
|
|
49
|
+
|
|
50
|
+
## Contributor Notes
|
|
51
|
+
|
|
52
|
+
- Safe to edit: constants, helper names, fallback tests, and README wording.
|
|
53
|
+
- Be careful with: YAML sanitization and description truncation, because those values feed SEO metadata.
|
|
54
|
+
- Do not casually change: frontmatter key names, merge precedence, textmode ecosystem detection, or build-date output.
|
|
55
|
+
|
|
56
|
+
## Maintenance
|
|
57
|
+
|
|
58
|
+
This plugin is project-specific glue between TypeDoc, `typedoc-plugin-markdown`, `typedoc-plugin-frontmatter`, and the
|
|
59
|
+
textmode.js VitePress site. Re-check the event hook and frontmatter object shape after upgrading TypeDoc or
|
|
60
|
+
`typedoc-plugin-markdown`.
|
|
61
|
+
|
|
62
|
+
Upgrade checklist:
|
|
63
|
+
|
|
64
|
+
- Confirm `MarkdownPageEvent.BEGIN` still exposes `page.model`, `page.project`, and mutable `page.frontmatter`.
|
|
65
|
+
- Run `npm run test:tooling`.
|
|
66
|
+
- Inspect generated frontmatter for a project page, namespace member, class, interface, accessor, and method.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
export const UNKNOWN_LIBRARY_NAME = 'Unknown Library';
|
|
6
|
+
export const TEXTMODE_ECOSYSTEM_NAME = 'textmode.js';
|
|
7
|
+
export const TEXTMODE_PACKAGE_PREFIX = 'textmode';
|
|
8
|
+
export const DESCRIPTION_MAX_LENGTH = 160;
|
|
9
|
+
export const DESCRIPTION_TRUNCATION_SUFFIX = '...';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {ReadonlyMap<import('typedoc').ReflectionKind, string>}
|
|
13
|
+
*/
|
|
14
|
+
export const CATEGORY_BY_KIND = new Map([
|
|
15
|
+
[ReflectionKind.Class, 'Classes'],
|
|
16
|
+
[ReflectionKind.Interface, 'Interfaces'],
|
|
17
|
+
[ReflectionKind.Enum, 'Enumerations'],
|
|
18
|
+
[ReflectionKind.Function, 'Functions'],
|
|
19
|
+
[ReflectionKind.TypeAlias, 'Type Aliases'],
|
|
20
|
+
[ReflectionKind.Variable, 'Variables'],
|
|
21
|
+
[ReflectionKind.Property, 'Properties'],
|
|
22
|
+
[ReflectionKind.Accessor, 'Accessors'],
|
|
23
|
+
[ReflectionKind.Method, 'Methods'],
|
|
24
|
+
[ReflectionKind.Constructor, 'Constructors'],
|
|
25
|
+
[ReflectionKind.Namespace, 'Namespaces'],
|
|
26
|
+
[ReflectionKind.Module, 'Modules'],
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fallback description templates for each reflection kind.
|
|
31
|
+
* `{name}` is replaced with the reflection name and `{library}` with the library name.
|
|
32
|
+
*
|
|
33
|
+
* @type {ReadonlyMap<import('typedoc').ReflectionKind, string>}
|
|
34
|
+
*/
|
|
35
|
+
export const DESCRIPTION_TEMPLATE_BY_KIND = new Map([
|
|
36
|
+
[ReflectionKind.Class, 'API documentation for the {name} class in {library}.'],
|
|
37
|
+
[ReflectionKind.Interface, 'API documentation for the {name} interface in {library}.'],
|
|
38
|
+
[ReflectionKind.Enum, '{name} enumeration values and usage in {library}.'],
|
|
39
|
+
[ReflectionKind.Function, '{name} function API reference for {library}.'],
|
|
40
|
+
[ReflectionKind.TypeAlias, '{name} type definition and usage in {library}.'],
|
|
41
|
+
[ReflectionKind.Variable, '{name} variable reference in {library}.'],
|
|
42
|
+
[ReflectionKind.Property, '{name} property reference for {library}.'],
|
|
43
|
+
[ReflectionKind.Accessor, '{name} accessor reference for {library}.'],
|
|
44
|
+
[ReflectionKind.Method, '{name} method reference for {library}.'],
|
|
45
|
+
[ReflectionKind.Constructor, '{name} constructor reference for {library}.'],
|
|
46
|
+
[ReflectionKind.Namespace, '{name} namespace - types and utilities in {library}.'],
|
|
47
|
+
[ReflectionKind.Project, 'Complete API reference for {library}.'],
|
|
48
|
+
[ReflectionKind.Module, 'API reference documentation for {name}.'],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export const DEFAULT_DESCRIPTION_TEMPLATE = 'API reference for {name} in {library}.';
|
|
52
|
+
export const API_REFERENCE_CATEGORY = 'API Reference';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_DESCRIPTION_TEMPLATE,
|
|
7
|
+
DESCRIPTION_MAX_LENGTH,
|
|
8
|
+
DESCRIPTION_TEMPLATE_BY_KIND,
|
|
9
|
+
DESCRIPTION_TRUNCATION_SUFFIX,
|
|
10
|
+
} from './constants.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize text for YAML frontmatter.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} text
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function sanitizeForYaml(text) {
|
|
19
|
+
return text
|
|
20
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
21
|
+
.replace(/https?:\/\/[^\s]+/g, '')
|
|
22
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
23
|
+
.replace(/\s+/g, ' ')
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a fallback description using reflection kind templates.
|
|
29
|
+
*
|
|
30
|
+
* @param {import('typedoc').Reflection} model
|
|
31
|
+
* @param {import('./library-context.js').LibraryContext} library
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function generateFallbackDescription(model, library) {
|
|
35
|
+
const kind = model.kind;
|
|
36
|
+
const name = model.name;
|
|
37
|
+
|
|
38
|
+
if ((kind === ReflectionKind.Project || kind === ReflectionKind.Module) && name === library.name) {
|
|
39
|
+
return library.description;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const template = DESCRIPTION_TEMPLATE_BY_KIND.get(kind) ?? DEFAULT_DESCRIPTION_TEMPLATE;
|
|
43
|
+
|
|
44
|
+
return template.replace('{name}', name).replace('{library}', library.name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract a page description from TypeDoc comments, or generate a fallback.
|
|
49
|
+
*
|
|
50
|
+
* @param {import('typedoc').Reflection} model
|
|
51
|
+
* @param {import('./library-context.js').LibraryContext} library
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function extractDescription(model, library) {
|
|
55
|
+
const comment =
|
|
56
|
+
model.comment ?? model.signatures?.[0]?.comment ?? model.getSignature?.comment ?? model.setSignature?.comment;
|
|
57
|
+
|
|
58
|
+
if (comment?.summary) {
|
|
59
|
+
const summary = comment.summary
|
|
60
|
+
.map((part) => part.text || '')
|
|
61
|
+
.join('')
|
|
62
|
+
.trim();
|
|
63
|
+
|
|
64
|
+
if (summary) {
|
|
65
|
+
const firstParagraph = summary.split(/\n\s*\n/)[0].trim();
|
|
66
|
+
const sanitized = sanitizeForYaml(firstParagraph);
|
|
67
|
+
|
|
68
|
+
if (sanitized.length > DESCRIPTION_MAX_LENGTH) {
|
|
69
|
+
return (
|
|
70
|
+
sanitized.substring(0, DESCRIPTION_MAX_LENGTH - DESCRIPTION_TRUNCATION_SUFFIX.length) +
|
|
71
|
+
DESCRIPTION_TRUNCATION_SUFFIX
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return sanitized;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return generateFallbackDescription(model, library);
|
|
80
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { ReflectionKind } from 'typedoc';
|
|
4
|
+
|
|
5
|
+
import { extractDescription } from './descriptions.js';
|
|
6
|
+
import {
|
|
7
|
+
getCategoryForKind,
|
|
8
|
+
getHasConstructor,
|
|
9
|
+
getIsInterface,
|
|
10
|
+
getNamespacePath,
|
|
11
|
+
getOwnerName,
|
|
12
|
+
isInNamespace,
|
|
13
|
+
} from './reflection-metadata.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Remove undefined values from a frontmatter object.
|
|
17
|
+
*
|
|
18
|
+
* @param {Record<string, unknown>} frontmatter
|
|
19
|
+
* @returns {Record<string, unknown>}
|
|
20
|
+
*/
|
|
21
|
+
export function removeUndefinedValues(frontmatter) {
|
|
22
|
+
for (const key of Object.keys(frontmatter)) {
|
|
23
|
+
if (frontmatter[key] === undefined) {
|
|
24
|
+
delete frontmatter[key];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return frontmatter;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build API frontmatter for a TypeDoc markdown page.
|
|
33
|
+
*
|
|
34
|
+
* @param {import('typedoc').Reflection} model
|
|
35
|
+
* @param {Record<string, unknown>} existingFrontmatter
|
|
36
|
+
* @param {import('./library-context.js').LibraryContext} library
|
|
37
|
+
* @returns {Record<string, unknown>}
|
|
38
|
+
*/
|
|
39
|
+
export function buildApiFrontmatter(model, existingFrontmatter, library) {
|
|
40
|
+
const kind = model.kind;
|
|
41
|
+
|
|
42
|
+
return removeUndefinedValues({
|
|
43
|
+
...existingFrontmatter,
|
|
44
|
+
title: model.name,
|
|
45
|
+
description: extractDescription(model, library),
|
|
46
|
+
category: getCategoryForKind(kind),
|
|
47
|
+
api: true,
|
|
48
|
+
owner: getOwnerName(model),
|
|
49
|
+
namespace: isInNamespace(model) ? getNamespacePath(model) : undefined,
|
|
50
|
+
kind: ReflectionKind[kind],
|
|
51
|
+
ecosystem: library.ecosystem !== library.name ? library.ecosystem : undefined,
|
|
52
|
+
lastModified: new Date().toISOString().split('T')[0],
|
|
53
|
+
hasConstructor: getHasConstructor(model),
|
|
54
|
+
isInterface: getIsInterface(model),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { MarkdownPageEvent } from 'typedoc-plugin-markdown';
|
|
4
|
+
|
|
5
|
+
import { buildApiFrontmatter } from './frontmatter.js';
|
|
6
|
+
import { extractLibraryContext } from './library-context.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TypeDoc plugin load function.
|
|
10
|
+
*
|
|
11
|
+
* @param {import('typedoc-plugin-markdown').MarkdownApplication} app
|
|
12
|
+
* @returns {void}
|
|
13
|
+
*/
|
|
14
|
+
export function load(app) {
|
|
15
|
+
app.logger.verbose('[frontmatter] Registered textmode.js API frontmatter plugin');
|
|
16
|
+
|
|
17
|
+
/** @type {import('./library-context.js').LibraryContext | null} */
|
|
18
|
+
let libraryContext = null;
|
|
19
|
+
|
|
20
|
+
app.renderer.on(
|
|
21
|
+
MarkdownPageEvent.BEGIN,
|
|
22
|
+
/** @param {import('typedoc-plugin-markdown').MarkdownPageEvent} page */
|
|
23
|
+
(page) => {
|
|
24
|
+
if (!page.model) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!libraryContext) {
|
|
29
|
+
libraryContext = extractLibraryContext(page.project);
|
|
30
|
+
app.logger.verbose(
|
|
31
|
+
`[frontmatter] Library: ${libraryContext.name} (ecosystem: ${libraryContext.ecosystem})`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
page.frontmatter = buildApiFrontmatter(page.model, page.frontmatter, libraryContext);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|