@ng-prism/plugin-jsdoc 22.3.0 → 22.4.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  API documentation plugin for [@ng-prism/core](https://github.com/dyingangel666/ng-prism). Extracts JSDoc comments from component source code at build time and renders an interactive API panel.
4
4
 
5
- > **Full documentation:** [ng-prism Docs — JSDoc Plugin](https://dyingangel666.github.io/ng-prism/#/plugins/jsdoc)
5
+ > **Full documentation:** [ng-prism Docs: JSDoc Plugin](https://dyingangel666.github.io/ng-prism/#/plugins/jsdoc)
6
6
 
7
7
  ## Installation
8
8
 
@@ -49,11 +49,11 @@ JSDoc descriptions are rendered as Markdown:
49
49
  - Headings (`#`, `##`, `###`)
50
50
  - Lists (ordered/unordered) and inline code (`` `code` ``)
51
51
  - Bold (`**text**`), italic (`*text*`), and links (`[text](url)`)
52
- - Fenced code blocks with a language hint (` ```html `, ` ```scss ` …)
52
+ - Fenced code blocks with a language hint (` ```html `, ` ```scss `, ...)
53
53
 
54
54
  Class descriptions are rendered as block Markdown; input/output/method descriptions are
55
55
  rendered inline (no wrapping `<p>`). Existing plain-text descriptions keep working
56
- unchanged — plain text is valid Markdown.
56
+ unchanged, since plain text is valid Markdown.
57
57
 
58
58
  ````typescript
59
59
  /**
@@ -61,8 +61,8 @@ unchanged — plain text is valid Markdown.
61
61
  *
62
62
  * ## Size
63
63
  *
64
- * - `small` (4px) — inline indicators
65
- * - `large` (9px, default) — section-level loading
64
+ * - `small` (4px): inline indicators
65
+ * - `large` (9px, default): section-level loading
66
66
  *
67
67
  * @example
68
68
  * ```html
@@ -72,9 +72,9 @@ unchanged — plain text is valid Markdown.
72
72
  ````
73
73
 
74
74
  `@example` blocks accept a fenced code block with a language hint. The language is
75
- passed through to `ngx-highlightjs` (`html`, `scss`, `typescript`, …). Examples without
75
+ passed through to `ngx-highlightjs` (`html`, `scss`, `typescript`, ...). Examples without
76
76
  fences fall back to `typescript` for backwards compatibility. Make sure the languages
77
- you reference are registered with `highlight.js` in your app — `highlight.js`'s core
77
+ you reference are registered with `highlight.js` in your app. `highlight.js`'s core
78
78
  language set is loaded by default, but additional languages must be registered
79
79
  explicitly by the consumer.
80
80
 
@@ -1,3 +1,11 @@
1
1
  import type { JsDocData } from './jsdoc.types.js';
2
- export declare function extractJsDocData(filePath: string, className: string): JsDocData | null;
2
+ /**
3
+ * `baseClasses` come from the scanner, nearest first. Members are read from
4
+ * the class and then from each of them; the first class that declares a
5
+ * member decides its tags, as a redeclaration replaces the inherited one.
6
+ */
7
+ export declare function extractJsDocData(filePath: string, className: string, baseClasses?: {
8
+ className: string;
9
+ filePath: string;
10
+ }[]): JsDocData | null;
3
11
  //# sourceMappingURL=jsdoc-extractor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jsdoc-extractor.d.ts","sourceRoot":"","sources":["../src/jsdoc-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAkC,MAAM,kBAAkB,CAAC;AAElF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CActF"}
1
+ {"version":3,"file":"jsdoc-extractor.d.ts","sourceRoot":"","sources":["../src/jsdoc-extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAkC,MAAM,kBAAkB,CAAC;AAElF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,GAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAAO,GAAG,SAAS,GAAG,IAAI,CAyBnJ"}
@@ -1,16 +1,29 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import ts from 'typescript';
3
- export function extractJsDocData(filePath, className) {
4
- const sourceText = readFileSync(filePath, 'utf-8');
5
- const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.ES2022, true);
6
- const classDecl = findClassByName(sourceFile, className);
3
+ /**
4
+ * `baseClasses` come from the scanner, nearest first. Members are read from
5
+ * the class and then from each of them; the first class that declares a
6
+ * member decides its tags, as a redeclaration replaces the inherited one.
7
+ */
8
+ export function extractJsDocData(filePath, className, baseClasses = []) {
9
+ const sourceFiles = new Map();
10
+ const findClass = (path, name) => {
11
+ let sourceFile = sourceFiles.get(path);
12
+ if (!sourceFile) {
13
+ sourceFile = ts.createSourceFile(path, readFileSync(path, 'utf-8'), ts.ScriptTarget.ES2022, true);
14
+ sourceFiles.set(path, sourceFile);
15
+ }
16
+ return findClassByName(sourceFile, name);
17
+ };
18
+ const classDecl = findClass(filePath, className);
7
19
  if (!classDecl)
8
20
  return null;
21
+ const hierarchy = [classDecl, ...baseClasses.flatMap((base) => findClass(base.filePath, base.className) ?? [])];
9
22
  return {
10
23
  classDescription: extractDescription(classDecl),
11
24
  classTags: extractTags(classDecl),
12
- memberTags: extractAllMemberTags(classDecl),
13
- methods: extractPublicMethods(classDecl)
25
+ memberTags: extractAllMemberTags(hierarchy),
26
+ methods: extractPublicMethods(hierarchy)
14
27
  };
15
28
  }
16
29
  function findClassByName(sourceFile, className) {
@@ -90,18 +103,37 @@ function tagCommentToString(comment) {
90
103
  .join('')
91
104
  .trim() || undefined);
92
105
  }
93
- function extractAllMemberTags(classDecl) {
106
+ function extractAllMemberTags(hierarchy) {
94
107
  const result = {};
108
+ const shadowed = new Set();
109
+ for (const classDecl of hierarchy) {
110
+ for (const member of classDecl.members) {
111
+ const name = getMemberName(member);
112
+ if (!name || shadowed.has(name))
113
+ continue;
114
+ const tags = ts.getJSDocTags(member);
115
+ if (tags.length === 0)
116
+ continue;
117
+ result[name] = buildTagsFromList(tags);
118
+ }
119
+ addMemberNames(classDecl, shadowed);
120
+ }
121
+ return result;
122
+ }
123
+ /**
124
+ * A member with JSDoc of its own hides the same name further up, as it does
125
+ * for TypeScript and for the description the scanner reads; one without
126
+ * keeps the documentation it inherits. Applied after the whole class, so the
127
+ * getter, setter or overloads next to a member are read too. A static member
128
+ * hides nothing, since inputs, outputs and methods are instance members.
129
+ */
130
+ function addMemberNames(classDecl, names) {
95
131
  for (const member of classDecl.members) {
96
132
  const name = getMemberName(member);
97
- if (!name)
98
- continue;
99
- const tags = ts.getJSDocTags(member);
100
- if (tags.length === 0)
101
- continue;
102
- result[name] = buildTagsFromList(tags);
133
+ const isStatic = ts.canHaveModifiers(member) && ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword);
134
+ if (name && !isStatic && ts.getJSDocCommentsAndTags(member).length > 0)
135
+ names.add(name);
103
136
  }
104
- return result;
105
137
  }
106
138
  function getMemberName(member) {
107
139
  if (!member.name)
@@ -122,29 +154,35 @@ const LIFECYCLE_HOOKS = new Set([
122
154
  'ngAfterViewInit',
123
155
  'ngAfterViewChecked'
124
156
  ]);
125
- function extractPublicMethods(classDecl) {
157
+ function extractPublicMethods(hierarchy) {
126
158
  const result = [];
127
- const sourceFile = classDecl.getSourceFile();
128
- for (const member of classDecl.members) {
129
- if (!ts.isMethodDeclaration(member))
130
- continue;
131
- const name = getMemberName(member);
132
- if (!name || name.startsWith('_'))
133
- continue;
134
- if (LIFECYCLE_HOOKS.has(name))
135
- continue;
136
- const isPrivate = member.modifiers?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword);
137
- if (isPrivate)
138
- continue;
139
- if (!hasDirectJsDoc(member, sourceFile))
140
- continue;
141
- const description = extractDescription(member);
142
- const tags = extractTags(member);
143
- const params = extractParamDocs(member);
144
- const returnType = member.type ? member.type.getText() : undefined;
145
- if (!description && Object.keys(tags).length === 0)
146
- continue;
147
- result.push({ name, description, tags, params, returnType });
159
+ const shadowed = new Set();
160
+ for (const classDecl of hierarchy) {
161
+ const sourceFile = classDecl.getSourceFile();
162
+ for (const member of classDecl.members) {
163
+ if (!ts.isMethodDeclaration(member))
164
+ continue;
165
+ const name = getMemberName(member);
166
+ if (!name || shadowed.has(name))
167
+ continue;
168
+ if (name.startsWith('_'))
169
+ continue;
170
+ if (LIFECYCLE_HOOKS.has(name))
171
+ continue;
172
+ const isPrivate = member.modifiers?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword);
173
+ if (isPrivate)
174
+ continue;
175
+ if (!hasDirectJsDoc(member, sourceFile))
176
+ continue;
177
+ const description = extractDescription(member);
178
+ const tags = extractTags(member);
179
+ const params = extractParamDocs(member);
180
+ const returnType = member.type ? member.type.getText() : undefined;
181
+ if (!description && Object.keys(tags).length === 0)
182
+ continue;
183
+ result.push({ name, description, tags, params, returnType });
184
+ }
185
+ addMemberNames(classDecl, shadowed);
148
186
  }
149
187
  return result;
150
188
  }
@@ -427,7 +427,7 @@ export class JsDocPanelComponent {
427
427
  classDescription = computed(() => this.jsdocData()?.classDescription, /* @ts-ignore */
428
428
  ...(ngDevMode ? [{ debugName: "classDescription" }] : /* istanbul ignore next */ []));
429
429
  // Source is component-author JSDoc compiled into the manifest at build time,
430
- // not user input — bypassing sanitization is safe here.
430
+ // not user input, so bypassing sanitization is safe here.
431
431
  renderedClassDescription = computed(() => {
432
432
  const html = renderBlockMarkdown(this.classDescription());
433
433
  return html ? this.sanitizer.bypassSecurityTrustHtml(html) : null;
@@ -3,7 +3,7 @@ export function jsDocPlugin() {
3
3
  return {
4
4
  name: '@ng-prism/plugin-jsdoc',
5
5
  async onComponentScanned(component) {
6
- const jsdoc = extractJsDocData(component.filePath, component.className);
6
+ const jsdoc = extractJsDocData(component.filePath, component.className, component.baseClasses);
7
7
  if (!jsdoc)
8
8
  return;
9
9
  return {
@@ -12,10 +12,10 @@ function injectPlugin(options) {
12
12
  call: PLUGIN_CALL
13
13
  });
14
14
  if (changed) {
15
- context.logger.info(` ${PLUGIN_IMPORT_FROM} → added to ${configPath}`);
15
+ context.logger.info(` ${PLUGIN_IMPORT_FROM} added to ${configPath}`);
16
16
  }
17
17
  else {
18
- context.logger.info(` ${PLUGIN_IMPORT_FROM} already configured — skipped`);
18
+ context.logger.info(` ${PLUGIN_IMPORT_FROM} already configured, skipped`);
19
19
  }
20
20
  return tree;
21
21
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-prism/plugin-jsdoc",
3
- "version": "22.3.0",
3
+ "version": "22.4.0",
4
4
  "description": "JSDoc API documentation panel for ng-prism.",
5
5
  "ng-add": {
6
6
  "save": "devDependencies"
@@ -36,7 +36,7 @@
36
36
  ],
37
37
  "peerDependencies": {
38
38
  "@angular/core": ">=20.0.0",
39
- "@ng-prism/core": "^22.3.0-0",
39
+ "@ng-prism/core": "^22.4.0-0",
40
40
  "highlight.js": ">=11.0.0",
41
41
  "ngx-highlightjs": ">=14.0.0",
42
42
  "typescript": ">=5.5.0",