@ng-prism/plugin-jsdoc 22.5.0 → 22.7.0-beta.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.
Files changed (46) hide show
  1. package/README.md +18 -15
  2. package/dist/api-page/api-page.helpers.d.ts +79 -0
  3. package/dist/api-page/api-page.helpers.d.ts.map +1 -0
  4. package/dist/api-page/api-page.helpers.js +133 -0
  5. package/dist/api-page/api-page.model.d.ts +62 -0
  6. package/dist/api-page/api-page.model.d.ts.map +1 -0
  7. package/dist/api-page/api-page.model.js +122 -0
  8. package/dist/api-page/component-usage.d.ts +14 -0
  9. package/dist/api-page/component-usage.d.ts.map +1 -0
  10. package/dist/api-page/component-usage.js +120 -0
  11. package/dist/api-page/doc-links.d.ts +15 -0
  12. package/dist/api-page/doc-links.d.ts.map +1 -0
  13. package/dist/api-page/doc-links.js +45 -0
  14. package/dist/api-page/jsdoc-api-page.component.d.ts +98 -0
  15. package/dist/api-page/jsdoc-api-page.component.d.ts.map +1 -0
  16. package/dist/api-page/jsdoc-api-page.component.js +1458 -0
  17. package/dist/api-page/pipe-usage.d.ts +13 -0
  18. package/dist/api-page/pipe-usage.d.ts.map +1 -0
  19. package/dist/api-page/pipe-usage.js +35 -0
  20. package/dist/api-page/service-usage.d.ts +31 -0
  21. package/dist/api-page/service-usage.d.ts.map +1 -0
  22. package/dist/api-page/service-usage.js +66 -0
  23. package/dist/jsdoc-extractor.d.ts.map +1 -1
  24. package/dist/jsdoc-extractor.js +17 -10
  25. package/dist/jsdoc-panel.component.d.ts +8 -25
  26. package/dist/jsdoc-panel.component.d.ts.map +1 -1
  27. package/dist/jsdoc-panel.component.js +34 -474
  28. package/dist/jsdoc-plugin.browser.js +2 -2
  29. package/dist/jsdoc-plugin.d.ts.map +1 -1
  30. package/dist/jsdoc-plugin.js +4 -1
  31. package/dist/service-page/api-page.model.d.ts +35 -0
  32. package/dist/service-page/api-page.model.d.ts.map +1 -0
  33. package/dist/service-page/api-page.model.js +54 -0
  34. package/dist/service-page/jsdoc-service-page.component.d.ts +82 -0
  35. package/dist/service-page/jsdoc-service-page.component.d.ts.map +1 -0
  36. package/dist/service-page/jsdoc-service-page.component.js +1178 -0
  37. package/dist/service-page/pipe-usage.d.ts +13 -0
  38. package/dist/service-page/pipe-usage.d.ts.map +1 -0
  39. package/dist/service-page/pipe-usage.js +35 -0
  40. package/dist/service-page/service-page.model.d.ts +78 -0
  41. package/dist/service-page/service-page.model.d.ts.map +1 -0
  42. package/dist/service-page/service-page.model.js +130 -0
  43. package/dist/service-page/service-usage.d.ts +28 -0
  44. package/dist/service-page/service-usage.d.ts.map +1 -0
  45. package/dist/service-page/service-usage.js +61 -0
  46. package/package.json +3 -2
package/README.md CHANGED
@@ -12,13 +12,18 @@ npm install @ng-prism/plugin-jsdoc
12
12
 
13
13
  ### Peer Dependencies
14
14
 
15
- | Package | Version |
16
- | ----------------- | ---------- |
17
- | `@ng-prism/core` | `>=21.0.0` |
18
- | `@angular/core` | `>=20.0.0` |
19
- | `typescript` | `>=5.5.0` |
20
- | `highlight.js` | `>=11.0.0` |
21
- | `ngx-highlightjs` | `>=14.0.0` |
15
+ | Package | Version |
16
+ | ---------------------------- | --------------------- |
17
+ | `@ng-prism/core` | `^<plugin version>-0` |
18
+ | `@angular/common` | `>=20.0.0` |
19
+ | `@angular/core` | `>=20.0.0` |
20
+ | `@angular/platform-browser` | `>=20.0.0` |
21
+ | `@angular-devkit/schematics` | `>=20.0.0` |
22
+ | `typescript` | `>=5.5.0` |
23
+ | `highlight.js` | `>=11.0.0` |
24
+ | `ngx-highlightjs` | `>=14.0.0` |
25
+
26
+ Each release requires the `@ng-prism/core` of its own version (`^22.7.0-0` for 22.7.0), so keep the core and the plugin on the same version.
22
27
 
23
28
  ## Setup
24
29
 
@@ -36,11 +41,9 @@ export default defineConfig({
36
41
 
37
42
  - Extracts class-level JSDoc description
38
43
  - Extracts per-input and per-output JSDoc comments
39
- - Renders an **API** panel with:
40
- - Component description
41
- - Inputs table (name, type, default, description)
42
- - Outputs table (name, description)
43
- - Supported tags: `@deprecated`, `@since`, `@see`, `@example`
44
+ - Draws the API page of every component and directive with `@Showcase`, the same page a service or a pipe gets (since 22.7.0): a generated usage snippet, the examples, the inputs with their types linked to type cards, the outputs with their payload, the documented methods, the types they reach and an "On this page" list. `{@link}` and `@see` jump to a type card, and `@deprecated`, `@since` and `@example` are supported, see [Components and Directives](https://ng-prism.github.io/ng-prism/#/plugins/jsdoc?id=components-and-directives)
45
+ - Draws the API page of every service with `@Showcase` (since 22.7.0): its properties, methods and setup, a generated usage snippet and the types it reaches, see [Services](https://ng-prism.github.io/ng-prism/#/plugins/jsdoc?id=services)
46
+ - Draws the API page of every pipe with `@Showcase` (since 22.7.0): the template name, purity, every `transform()` signature and setup, a generated usage snippet and the types it reaches, see [Pipes](https://ng-prism.github.io/ng-prism/#/plugins/jsdoc?id=pipes)
44
47
 
45
48
  ## Markdown Support
46
49
 
@@ -98,13 +101,13 @@ export class ButtonComponent {
98
101
  }
99
102
  ```
100
103
 
101
- The API panel shows the class description, a table of inputs with their types and JSDoc, and all supported tags.
104
+ The API tab shows the class description, a usage snippet, the inputs with their types, defaults and JSDoc, and all supported tags.
102
105
 
103
106
  ## How It Works
104
107
 
105
- **Build time:** The `onComponentScanned` hook uses `ts.createSourceFile` with `setParentNodes: true` and `ts.getJSDocTags()` to extract JSDoc data. The extracted data is injected into `showcaseConfig.meta.jsdoc`.
108
+ **Build time:** The plugin sets `componentApi: true`, which has the core scanner read `ScannedComponent.api` for every component; a config without a plugin that sets it skips that work. The `onComponentScanned` hook uses `ts.createSourceFile` with `setParentNodes: true` and `ts.getJSDocTags()` to extract JSDoc data, keeping each parsed file while its mtime and size stay the same, so a rebuild of `serve` only parses the files that changed. The extracted data is injected into `showcaseConfig.meta.jsdoc`.
106
109
 
107
- **Runtime:** The `JsDocPanelComponent` reads the metadata and renders the documentation. The component is lazy-loaded via `loadComponent`.
110
+ **Runtime:** The `JsDocPanelComponent` is lazy-loaded via `loadComponent`. It builds the API page from `ScannedComponent.api`, which the core scanner fills with the types, payloads, methods and links, and from the JSDoc data in `showcaseConfig.meta.jsdoc`, and renders it. Services and pipes get the same page from their own scanned data.
108
111
 
109
112
  ## License
110
113
 
@@ -0,0 +1,79 @@
1
+ import type { ApiParam, ApiSetupEntry, ApiTypeRef } from '@ng-prism/core/plugin';
2
+ import type { ApiPageModel } from './api-page.model.js';
3
+ /** One entry of the API page's "On this page" list. `id` names the section, `level` 2 is a type below "Types". */
4
+ export interface TocEntry {
5
+ id: string;
6
+ label: string;
7
+ level: 1 | 2;
8
+ }
9
+ /** A piece of a type as written; `link` is the TOC id of the documented type the piece names. */
10
+ export interface TypeSegment {
11
+ text: string;
12
+ link?: string;
13
+ }
14
+ /** The TOC id of a documented type's card. */
15
+ export declare function typeAnchor(typeId: string): string;
16
+ /**
17
+ * The sections of an API page in the order the page shows them. Usage is
18
+ * always there, being generated; any other section only when it has content.
19
+ * A component or directive has inputs, outputs and methods, a service
20
+ * properties and methods, a pipe its transform signatures.
21
+ */
22
+ export declare function buildApiPageToc(page: ApiPageModel): TocEntry[];
23
+ /** Where the page is scrolled to, and where each anchor of the "On this page" list sits on it. */
24
+ export interface ScrollSpyState {
25
+ /** The TOC id of each anchor in page order, with its offset from the top of the scrolled content. */
26
+ anchors: readonly {
27
+ id: string;
28
+ top: number;
29
+ }[];
30
+ scrollTop: number;
31
+ clientHeight: number;
32
+ scrollHeight: number;
33
+ /** The entry a jump is heading for, for as long as the jump lasts. */
34
+ jumpTarget: string | null;
35
+ }
36
+ /**
37
+ * The entry the "On this page" list marks. During a jump, the one the reader
38
+ * jumped to, whatever the jump scrolls past. Otherwise the last section whose
39
+ * heading has come up to a line a quarter down the view, except at the bottom
40
+ * of the page: the sections there can never come up to the line, so the last
41
+ * one in view is active.
42
+ */
43
+ export declare function activeTocId({ anchors, scrollTop, clientHeight, scrollHeight, jumpTarget }: ScrollSpyState): string;
44
+ /**
45
+ * Cut a type as written into plain text and the names of documented types, so
46
+ * the page can turn those names into links to their cards.
47
+ *
48
+ * `ref.links` holds type ids, and an id is the type's name, or
49
+ * `${name}@${importPath}` when two reachable types share a name. An identifier
50
+ * in the text is matched against the name part of each id, the id that is the
51
+ * name itself first. Known limitation: a type imported under another name
52
+ * (`import { Item as OtherItem }`) appears in the text by its local name, which
53
+ * no id carries, so it stays unlinked.
54
+ */
55
+ export declare function typeSegments(ref: ApiTypeRef): TypeSegment[];
56
+ /** A parameter as a signature writes it, `config?: ToastConfig` or `...features: Feature[]`, with its type linked. */
57
+ export declare function paramSegments(param: ApiParam): TypeSegment[];
58
+ /**
59
+ * A parameter as its declaration writes it, default included:
60
+ * `config: ToastConfig = {}`. A default makes the parameter optional, so it
61
+ * goes without the `?`; one without a default reads as in a signature.
62
+ */
63
+ export declare function declaredParamSegments(param: ApiParam): TypeSegment[];
64
+ /** Whether a parameter list says more than its signature: a `@param` text or a default. */
65
+ export declare function documentsParams(params: readonly ApiParam[] | undefined): boolean;
66
+ /**
67
+ * What follows a setup entry's name on its signature line: the parameters and
68
+ * the return type of a function, the type of a token.
69
+ */
70
+ export declare function setupSignature(entry: ApiSetupEntry): TypeSegment[];
71
+ /**
72
+ * Split a type's `reachedFrom` (`parameter of show()`, `via options.action`)
73
+ * into its words and the code they end in, so the page can set the code apart.
74
+ */
75
+ export declare function splitReachedFrom(reachedFrom: string): {
76
+ text: string;
77
+ code?: string;
78
+ };
79
+ //# sourceMappingURL=api-page.helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-page.helpers.d.ts","sourceRoot":"","sources":["../../src/api-page/api-page.helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,kHAAkH;AAClH,MAAM,WAAW,QAAQ;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;CAChB;AAED,iGAAiG;AACjG,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8CAA8C;AAC9C,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,EAAE,CAgB9D;AAKD,kGAAkG;AAClG,MAAM,WAAW,cAAc;IAC3B,qGAAqG;IACrG,OAAO,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,EAAE,cAAc,GAAG,MAAM,CAYlH;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,WAAW,EAAE,CAsB3D;AAED,sHAAsH;AACtH,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,WAAW,EAAE,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,GAAG,WAAW,EAAE,CAGpE;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,GAAG,SAAS,GAAG,OAAO,CAEhF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,WAAW,EAAE,CAMlE;AAiBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAKrF"}
@@ -0,0 +1,133 @@
1
+ /** The TOC id of a documented type's card. */
2
+ export function typeAnchor(typeId) {
3
+ return `type-${typeId}`;
4
+ }
5
+ /**
6
+ * The sections of an API page in the order the page shows them. Usage is
7
+ * always there, being generated; any other section only when it has content.
8
+ * A component or directive has inputs, outputs and methods, a service
9
+ * properties and methods, a pipe its transform signatures.
10
+ */
11
+ export function buildApiPageToc(page) {
12
+ const sections = [
13
+ ['usage', 'Usage', true],
14
+ ['setup', 'Setup', page.setup.length > 0],
15
+ ['examples', 'Examples', page.examples.length > 0],
16
+ ['inputs', 'Inputs', !!page.inputs?.length],
17
+ ['outputs', 'Outputs', !!page.outputs?.length],
18
+ ['properties', 'Properties', !!page.properties?.length],
19
+ ['methods', 'Methods', !!page.methods?.length],
20
+ ['transform', 'Transform', !!page.transform?.length],
21
+ ['types', 'Types', page.types.length > 0]
22
+ ];
23
+ const toc = sections.filter(([, , present]) => present).map(([id, label]) => ({ id, label, level: 1 }));
24
+ for (const type of page.types)
25
+ toc.push({ id: typeAnchor(type.id), label: type.name, level: 2 });
26
+ return toc;
27
+ }
28
+ /** How far down the view, as a share of its height, a section's heading has to come to make the section active. */
29
+ const ACTIVATION_LINE = 0.25;
30
+ /**
31
+ * The entry the "On this page" list marks. During a jump, the one the reader
32
+ * jumped to, whatever the jump scrolls past. Otherwise the last section whose
33
+ * heading has come up to a line a quarter down the view, except at the bottom
34
+ * of the page: the sections there can never come up to the line, so the last
35
+ * one in view is active.
36
+ */
37
+ export function activeTocId({ anchors, scrollTop, clientHeight, scrollHeight, jumpTarget }) {
38
+ if (jumpTarget)
39
+ return jumpTarget;
40
+ if (scrollTop + clientHeight >= scrollHeight - 1) {
41
+ const inView = anchors.filter((anchor) => anchor.top >= scrollTop && anchor.top < scrollTop + clientHeight).at(-1);
42
+ if (inView)
43
+ return inView.id;
44
+ }
45
+ const line = scrollTop + clientHeight * ACTIVATION_LINE;
46
+ return (anchors.filter((anchor) => anchor.top <= line).at(-1) ?? anchors[0])?.id ?? 'usage';
47
+ }
48
+ const IDENTIFIER = /[A-Za-z_$][\w$]*/g;
49
+ /**
50
+ * Cut a type as written into plain text and the names of documented types, so
51
+ * the page can turn those names into links to their cards.
52
+ *
53
+ * `ref.links` holds type ids, and an id is the type's name, or
54
+ * `${name}@${importPath}` when two reachable types share a name. An identifier
55
+ * in the text is matched against the name part of each id, the id that is the
56
+ * name itself first. Known limitation: a type imported under another name
57
+ * (`import { Item as OtherItem }`) appears in the text by its local name, which
58
+ * no id carries, so it stays unlinked.
59
+ */
60
+ export function typeSegments(ref) {
61
+ const links = ref.links ?? [];
62
+ const segments = [];
63
+ let plain = '';
64
+ let last = 0;
65
+ for (const match of ref.text.matchAll(IDENTIFIER)) {
66
+ const name = match[0];
67
+ const id = links.find((candidate) => candidate === name) ?? links.find((candidate) => candidate.split('@')[0] === name);
68
+ if (!id)
69
+ continue;
70
+ plain += ref.text.slice(last, match.index);
71
+ if (plain)
72
+ segments.push({ text: plain });
73
+ segments.push({ text: name, link: typeAnchor(id) });
74
+ plain = '';
75
+ last = match.index + name.length;
76
+ }
77
+ plain += ref.text.slice(last);
78
+ if (plain || !segments.length)
79
+ segments.push({ text: plain });
80
+ return segments;
81
+ }
82
+ /** A parameter as a signature writes it, `config?: ToastConfig` or `...features: Feature[]`, with its type linked. */
83
+ export function paramSegments(param) {
84
+ return joinSegments(`${param.rest ? '...' : ''}${param.name}${param.optional ? '?' : ''}: `, typeSegments(param.type));
85
+ }
86
+ /**
87
+ * A parameter as its declaration writes it, default included:
88
+ * `config: ToastConfig = {}`. A default makes the parameter optional, so it
89
+ * goes without the `?`; one without a default reads as in a signature.
90
+ */
91
+ export function declaredParamSegments(param) {
92
+ if (param.defaultValue === undefined)
93
+ return paramSegments(param);
94
+ return joinSegments(`${param.rest ? '...' : ''}${param.name}: `, typeSegments(param.type), ` = ${param.defaultValue}`);
95
+ }
96
+ /** Whether a parameter list says more than its signature: a `@param` text or a default. */
97
+ export function documentsParams(params) {
98
+ return !!params?.some((param) => param.description || param.defaultValue !== undefined);
99
+ }
100
+ /**
101
+ * What follows a setup entry's name on its signature line: the parameters and
102
+ * the return type of a function, the type of a token.
103
+ */
104
+ export function setupSignature(entry) {
105
+ if (entry.kind === 'token')
106
+ return entry.tokenType ? joinSegments(': ', typeSegments(entry.tokenType)) : [];
107
+ const params = (entry.params ?? []).flatMap((param, index) => (index ? [', ', paramSegments(param)] : [paramSegments(param)]));
108
+ return joinSegments('(', ...params, ')', ...(entry.returnType ? [': ', typeSegments(entry.returnType)] : []));
109
+ }
110
+ /** Concatenate text and segments, merging neighbouring plain text into one segment. */
111
+ function joinSegments(...parts) {
112
+ const joined = [];
113
+ for (const segment of parts.flatMap((part) => (typeof part === 'string' ? [{ text: part }] : part))) {
114
+ const previous = joined.at(-1);
115
+ if (!segment.text)
116
+ continue;
117
+ if (previous && !previous.link && !segment.link)
118
+ previous.text += segment.text;
119
+ else
120
+ joined.push({ ...segment });
121
+ }
122
+ return joined;
123
+ }
124
+ /**
125
+ * Split a type's `reachedFrom` (`parameter of show()`, `via options.action`)
126
+ * into its words and the code they end in, so the page can set the code apart.
127
+ */
128
+ export function splitReachedFrom(reachedFrom) {
129
+ const space = reachedFrom.lastIndexOf(' ');
130
+ if (space < 0)
131
+ return { text: reachedFrom };
132
+ return { text: reachedFrom.slice(0, space + 1), code: reachedFrom.slice(space + 1) };
133
+ }
@@ -0,0 +1,62 @@
1
+ import type { ApiDocTags, ApiMethod, ApiProperty, ApiSetupEntry, ApiTypeDoc, ApiTypeRef, ScannedComponent, ScannedPipe, ScannedService } from '@ng-prism/core/plugin';
2
+ /** A chip under the description, `Since: 1.2.0` or a bare `Pure`; the deprecation is a badge of its own. */
3
+ export interface ApiPageChip {
4
+ label: string;
5
+ value?: string;
6
+ deprecated?: true;
7
+ }
8
+ /** What an API page documents. */
9
+ export type ApiPageKind = 'component' | 'directive' | 'service' | 'pipe';
10
+ /** An input of a component or directive, by the name a template binds it by. */
11
+ export interface ApiInputRow {
12
+ name: string;
13
+ type: ApiTypeRef;
14
+ /** As the code writes it: a string in quotes. */
15
+ defaultValue?: string;
16
+ required: boolean;
17
+ description?: string;
18
+ tags: ApiDocTags;
19
+ }
20
+ /** An output of a component or directive: an `output()`, or the change event of a `model()`. */
21
+ export interface ApiOutputRow {
22
+ name: string;
23
+ /** What the event emits. Absent when the scanner did not read it. */
24
+ payload?: ApiTypeRef;
25
+ description?: string;
26
+ tags: ApiDocTags;
27
+ }
28
+ /**
29
+ * What the API page shows, for a component, a directive, a service or a pipe.
30
+ * The page renders this alone, so all four kinds share one look; the sections
31
+ * a kind does not have are left out.
32
+ *
33
+ * The entries of `properties`, `methods`, `transform` and `types` are the
34
+ * scanned objects themselves, not copies: the page caches what it derives from
35
+ * them by object. The rows of `inputs` and `outputs`, and the `methods` of a
36
+ * component without `api`, are built anew by every call.
37
+ */
38
+ export interface ApiPageModel {
39
+ kind: ApiPageKind;
40
+ className: string;
41
+ description?: string;
42
+ chips: ApiPageChip[];
43
+ see: string[];
44
+ usage: string;
45
+ setup: ApiSetupEntry[];
46
+ examples: string[];
47
+ /** Components and directives only. */
48
+ inputs?: ApiInputRow[];
49
+ /** Components and directives only. */
50
+ outputs?: ApiOutputRow[];
51
+ /** Services only. */
52
+ properties?: ApiProperty[];
53
+ /** Services, components and directives: the public methods. */
54
+ methods?: ApiMethod[];
55
+ /** Pipes: one entry per transform signature. */
56
+ transform?: ApiMethod[];
57
+ types: ApiTypeDoc[];
58
+ }
59
+ export declare function servicePage(service: ScannedService): ApiPageModel;
60
+ export declare function pipePage(pipe: ScannedPipe): ApiPageModel;
61
+ export declare function componentApiPage(component: ScannedComponent): ApiPageModel;
62
+ //# sourceMappingURL=api-page.model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-page.model.d.ts","sourceRoot":"","sources":["../../src/api-page/api-page.model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,UAAU,EACV,SAAS,EACT,WAAW,EACX,aAAa,EACb,UAAU,EACV,UAAU,EAEV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACjB,MAAM,uBAAuB,CAAC;AAM/B,4GAA4G;AAC5G,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,IAAI,CAAC;CACrB;AAED,kCAAkC;AAClC,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzE,gFAAgF;AAChF,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;CACpB;AAED,gGAAgG;AAChG,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,sCAAsC;IACtC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,sCAAsC;IACtC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,qBAAqB;IACrB,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;IACtB,gDAAgD;IAChD,SAAS,CAAC,EAAE,SAAS,EAAE,CAAC;IACxB,KAAK,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,YAAY,CAqBjE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,YAAY,CAsBxD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,gBAAgB,GAAG,YAAY,CAiD1E"}
@@ -0,0 +1,122 @@
1
+ import { generateComponentUsage } from './component-usage.js';
2
+ import { generatePipeUsage } from './pipe-usage.js';
3
+ import { generateServiceUsage } from './service-usage.js';
4
+ export function servicePage(service) {
5
+ const { className, importPath, provision, api } = service;
6
+ return {
7
+ kind: 'service',
8
+ className,
9
+ description: api.description,
10
+ chips: [
11
+ ...deprecationChip(api.tags),
12
+ ...valueChip('Since', api.tags.since),
13
+ { label: 'Provided in', value: provision === 'auto' ? 'root/automatically' : 'providers' },
14
+ ...valueChip('Entry point', importPath)
15
+ ],
16
+ see: api.tags.see ?? [],
17
+ usage: generateServiceUsage(service),
18
+ setup: api.setup,
19
+ examples: api.tags.example ?? [],
20
+ properties: api.properties,
21
+ methods: api.methods,
22
+ types: api.types
23
+ };
24
+ }
25
+ export function pipePage(pipe) {
26
+ const { className, importPath, pipeName, pure, api } = pipe;
27
+ return {
28
+ kind: 'pipe',
29
+ className,
30
+ description: api.description,
31
+ chips: [
32
+ ...deprecationChip(api.tags),
33
+ { label: 'Pipe', value: pipeName },
34
+ // Unreadable purity is no reason to guess: the chip goes.
35
+ ...(pure === undefined ? [] : [{ label: pure ? 'Pure' : 'Impure' }]),
36
+ ...valueChip('Since', api.tags.since),
37
+ ...valueChip('Entry point', importPath)
38
+ ],
39
+ see: api.tags.see ?? [],
40
+ usage: generatePipeUsage(pipe),
41
+ setup: api.setup,
42
+ examples: api.tags.example ?? [],
43
+ transform: api.transform,
44
+ types: api.types
45
+ };
46
+ }
47
+ export function componentApiPage(component) {
48
+ const { className, importPath, inputs, outputs, componentMeta, showcaseConfig, api } = component;
49
+ // The jsdoc plugin's own data, for a component that has no `api`: one a plugin hook built, or one from the manifest of an older core.
50
+ const jsdoc = showcaseConfig.meta?.['jsdoc'];
51
+ const description = api ? api.description : jsdoc?.classDescription;
52
+ const tags = api ? api.tags : (jsdoc?.classTags ?? {});
53
+ const memberTags = jsdoc?.memberTags ?? {};
54
+ const outputRows = outputs.map((output) => ({
55
+ name: output.alias ?? output.name,
56
+ payload: api?.outputTypes[output.name],
57
+ description: output.doc,
58
+ tags: memberTags[output.name] ?? {}
59
+ }));
60
+ // Angular adds the change event of a `model()` by itself, so the scanner has no output for it.
61
+ for (const change of api?.modelChanges ?? []) {
62
+ const input = inputs.find((candidate) => candidate.name === change.input);
63
+ const publicName = input ? (input.alias ?? input.name) : change.input;
64
+ outputRows.push({
65
+ name: change.event,
66
+ payload: change.type,
67
+ description: `Emits when \`${publicName}\` changes; bind both ways with \`[(${publicName})]\`.`,
68
+ tags: memberTags[change.input] ?? {}
69
+ });
70
+ }
71
+ return {
72
+ kind: componentMeta.isDirective ? 'directive' : 'component',
73
+ className,
74
+ description,
75
+ chips: [...deprecationChip(tags), ...valueChip('Version', tags.version), ...valueChip('Since', tags.since), ...valueChip('Entry point', importPath)],
76
+ see: tags.see ?? [],
77
+ usage: generateComponentUsage(component),
78
+ setup: [],
79
+ examples: tags.example ?? [],
80
+ inputs: inputs.map((input) => ({
81
+ name: input.alias ?? input.name,
82
+ type: api?.inputTypes[input.name] ?? { text: input.rawType ?? input.type },
83
+ defaultValue: input.defaultValue === undefined ? undefined : formatDefault(input.defaultValue),
84
+ required: input.required,
85
+ description: input.doc,
86
+ tags: memberTags[input.name] ?? {}
87
+ })),
88
+ outputs: outputRows,
89
+ methods: api ? api.methods : (jsdoc?.methods ?? []).map(methodSignature),
90
+ types: api?.types ?? []
91
+ };
92
+ }
93
+ /** The deprecation leads the chips, with its message when the tag has one. */
94
+ function deprecationChip({ deprecated }) {
95
+ if (!deprecated)
96
+ return [];
97
+ return [{ label: 'Deprecated', ...(typeof deprecated === 'string' ? { value: deprecated } : {}), deprecated: true }];
98
+ }
99
+ function valueChip(label, value) {
100
+ return value ? [{ label, value }] : [];
101
+ }
102
+ /** As the code writes a default: a string in quotes, an array or object as JSON (`String()` would give `a,b` and `[object Object]`), anything else by its `String()`. */
103
+ function formatDefault(value) {
104
+ if (typeof value === 'string')
105
+ return `'${value}'`;
106
+ return typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
107
+ }
108
+ /** A method of the jsdoc plugin's data as a signature: it knows no more of the types than their names. */
109
+ function methodSignature(method) {
110
+ return {
111
+ name: method.name,
112
+ params: method.params.map((param) => ({
113
+ name: param.name,
114
+ type: { text: param.type ?? 'unknown' },
115
+ optional: false,
116
+ description: param.description
117
+ })),
118
+ returnType: { text: method.returnType ?? 'void' },
119
+ description: method.description,
120
+ tags: method.tags
121
+ };
122
+ }
@@ -0,0 +1,14 @@
1
+ import { type ScannedComponent } from '@ng-prism/core/plugin';
2
+ /**
3
+ * The snippet that shows how to use a component or directive: the import, the
4
+ * `imports` entry of the component that uses it, and one line of template.
5
+ *
6
+ * It is generated from the scanned selector and inputs, not taken from the
7
+ * author, so it shows the minimum that works: the required inputs, bound to a
8
+ * member of the same name, and nothing else. A directive goes where the
9
+ * showcase puts it: on the `host` element, or on a plain element of its own.
10
+ * A `{ template }` host is the exception: the snippet shows the subject inside
11
+ * that markup, as the first variant that has one renders it.
12
+ */
13
+ export declare function generateComponentUsage(component: ScannedComponent): string;
14
+ //# sourceMappingURL=component-usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-usage.d.ts","sourceRoot":"","sources":["../../src/api-page/component-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkE,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAS9H;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,CAc1E"}
@@ -0,0 +1,120 @@
1
+ import { COMPOSITION_SUBJECT_SLOT } from '@ng-prism/core/plugin';
2
+ import { importLines, joinUsageBlocks } from './service-usage.js';
3
+ /** Elements that have no closing tag. */
4
+ const VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
5
+ /**
6
+ * The snippet that shows how to use a component or directive: the import, the
7
+ * `imports` entry of the component that uses it, and one line of template.
8
+ *
9
+ * It is generated from the scanned selector and inputs, not taken from the
10
+ * author, so it shows the minimum that works: the required inputs, bound to a
11
+ * member of the same name, and nothing else. A directive goes where the
12
+ * showcase puts it: on the `host` element, or on a plain element of its own.
13
+ * A `{ template }` host is the exception: the snippet shows the subject inside
14
+ * that markup, as the first variant that has one renders it.
15
+ */
16
+ export function generateComponentUsage(component) {
17
+ const { className, componentMeta, showcaseConfig } = component;
18
+ const configured = showcaseConfig.host;
19
+ const composed = isHostTemplate(configured) ? firstCase(component) : undefined;
20
+ // A component renders itself; only a directive needs an element to sit on.
21
+ const host = componentMeta.isDirective && !isHostTemplate(configured) ? configured : undefined;
22
+ const hostImport = host && typeof host !== 'string' ? host.import : undefined;
23
+ const extraImports = composed ? (component.composition?.imports ?? []) : hostImport ? [hostImport] : [];
24
+ return joinUsageBlocks([
25
+ importLines(component, extraImports),
26
+ ['// in the component that uses it', `@Component({ imports: [${[className, ...extraImports.map(({ name }) => name)].join(', ')}], … })`],
27
+ ['// in its template', composed ? compositionLine(component, composed) : templateLine(component, host)]
28
+ ]);
29
+ }
30
+ /** `null` is no host, though `typeof null` is `'object'`: a config from untyped code may carry one. */
31
+ function isHostTemplate(host) {
32
+ return !!host && typeof host === 'object' && 'template' in host;
33
+ }
34
+ /** The case of the first variant that has a composition: what the styleguide shows first. */
35
+ function firstCase({ composition }) {
36
+ const index = composition?.variantCases.find((candidate) => candidate >= 0);
37
+ return index === undefined ? undefined : composition?.cases[index];
38
+ }
39
+ /**
40
+ * The template of a case as its author wrote it, with the required inputs on the
41
+ * subject element in place of the slot. A required input the template sets
42
+ * itself is not bound a second time.
43
+ */
44
+ function compositionLine({ inputs }, { template, unbound }) {
45
+ const owned = new Set(unbound.filter(({ reason }) => reason === 'template').map(({ name }) => name));
46
+ const bindings = requiredInputs(inputs)
47
+ .filter(({ member }) => !owned.has(member))
48
+ .map(binding);
49
+ // Split and join, not replace: a member name may hold `$`, which `replace` would read as a pattern.
50
+ return template.split(COMPOSITION_SUBJECT_SLOT).join(bindings.length ? ` ${bindings.join(' ')}` : '');
51
+ }
52
+ function templateLine({ inputs, componentMeta }, host) {
53
+ const selector = parseSelector(componentMeta.selector);
54
+ const hostElement = typeof host === 'string' ? parseHostString(host) : host && { element: host.selector, attributes: hostAttributes(host.inputs) };
55
+ const element = hostElement?.element ?? selector.element ?? 'div';
56
+ const required = requiredInputs(inputs);
57
+ // A binding takes the place of the bare attribute the selector asks for.
58
+ const selectorAttributes = selector.attributes.filter((attribute) => !required.some(({ publicName }) => publicName === attribute.name));
59
+ const attributes = [
60
+ ...(hostElement?.attributes ?? []),
61
+ ...selectorAttributes.map(({ name, value }) => (value === undefined ? name : `${name}="${value}"`)),
62
+ ...required.map(binding)
63
+ ];
64
+ const opening = [element, ...attributes].join(' ');
65
+ return element.includes('-') || VOID_ELEMENTS.has(element) ? `<${opening} />` : `<${opening}></${element}>`;
66
+ }
67
+ /** The required inputs in source order, each by the name a template binds it under and the member it is bound to. */
68
+ function requiredInputs(inputs) {
69
+ return inputs.filter((input) => input.required).map((input) => ({ publicName: input.alias ?? input.name, member: input.name }));
70
+ }
71
+ function binding({ publicName, member }) {
72
+ return `[${publicName}]="${member}"`;
73
+ }
74
+ const SELECTOR_PART = /\[([\w-]+)(?:=(["']?)([^\]"']*)\2)?]|\.([\w-]+)/g;
75
+ /**
76
+ * The first selector of a list as an element and attributes. A class becomes
77
+ * the `class` attribute; negations, other attribute operators and pseudo
78
+ * classes say nothing about what to write, so they are left out.
79
+ */
80
+ function parseSelector(selector) {
81
+ const first = selector
82
+ .split(',')[0]
83
+ .trim()
84
+ .replace(/:not\([^)]*\)/g, '');
85
+ const element = first.match(/^[a-zA-Z][\w-]*/)?.[0];
86
+ const attributes = [];
87
+ for (const [, name, , value, className] of first.slice(element?.length ?? 0).matchAll(SELECTOR_PART)) {
88
+ const classAttribute = attributes.find((attribute) => attribute.name === 'class');
89
+ if (className === undefined)
90
+ attributes.push({ name, value });
91
+ else if (classAttribute)
92
+ classAttribute.value += ` ${className}`;
93
+ else
94
+ attributes.push({ name: 'class', value: className });
95
+ }
96
+ return { element, attributes };
97
+ }
98
+ /**
99
+ * The opening tag a string host starts with and its attributes as written;
100
+ * undefined when the string does not start with a tag. A host may carry
101
+ * content and a closing tag (`<button class="btn">Hover me</button>`): the
102
+ * showcase renders all of it, the snippet only needs the element to put the
103
+ * directive on.
104
+ */
105
+ function parseHostString(host) {
106
+ const match = host.match(/^\s*<\s*([a-zA-Z][\w-]*)([^>]*?)\s*\/?\s*>/);
107
+ if (!match)
108
+ return undefined;
109
+ return { element: match[1], attributes: match[2].trim() ? [match[2].trim()] : [] };
110
+ }
111
+ /** Strings are attributes; numbers and booleans need a binding to stay what they are; anything else is bound to a member of that name. */
112
+ function hostAttributes(inputs) {
113
+ return Object.entries(inputs ?? {}).map(([name, value]) => {
114
+ if (typeof value === 'string')
115
+ return `${name}="${value.replace(/&/g, '&amp;').replace(/"/g, '&quot;')}"`;
116
+ if (typeof value === 'number' || typeof value === 'boolean')
117
+ return `[${name}]="${value}"`;
118
+ return `[${name}]="${name}"`;
119
+ });
120
+ }
@@ -0,0 +1,15 @@
1
+ import type { ApiTypeDoc } from '@ng-prism/core/plugin';
2
+ /** Marks the button of a link, so the page can find it in rendered `innerHTML` that Angular does not bind. */
3
+ export declare const TYPE_LINK_ATTRIBUTE = "data-prism-type-link";
4
+ /**
5
+ * The TOC anchor of the type card a doc reference names: by id, then by name;
6
+ * `X.y` and `X#y` name `X`. A page holds two types of one name only under
7
+ * different ids, so an exact id is what tells them apart.
8
+ */
9
+ export declare function typeLinkTarget(types: readonly ApiTypeDoc[], target: string): string | undefined;
10
+ /**
11
+ * Rewrites `{@link}`, `{@linkcode}` and `{@linkplain}` into what the Markdown renderer shows: a button to the type card
12
+ * `resolve` names, a Markdown link for a URL target, inline code (or the link text) otherwise. Code spans stay as written.
13
+ */
14
+ export declare function resolveDocLinks(text: string, resolve: (target: string) => string | undefined): string;
15
+ //# sourceMappingURL=doc-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc-links.d.ts","sourceRoot":"","sources":["../../src/api-page/doc-links.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAWxD,8GAA8G;AAC9G,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK/F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrG"}