@pantoken/typedoc-plugin-demo 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/README.md +61 -0
- package/dist/index.d.mts +45 -0
- package/dist/index.mjs +148 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @pantoken/typedoc-plugin-demo
|
|
2
|
+
|
|
3
|
+
A TypeDoc plugin for a `@demo` block tag. Authors attach a live, embeddable demo to any symbol, and
|
|
4
|
+
this plugin turns it into a fenced `demo` block your docs renderer picks up as an iframe — an
|
|
5
|
+
MDN-style "live sample" panel.
|
|
6
|
+
|
|
7
|
+
It's deliberately provider-agnostic: it doesn't know or care what `stackblitz` or `wp-playground`
|
|
8
|
+
means. It just moves the spec into a fence; your renderer resolves it.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm i -D @pantoken/typedoc-plugin-demo typedoc
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
Add the plugin, and register `@demo` in `blockTags` (the comment parser reads that list before
|
|
19
|
+
plugins load, so the tag must be declared in config to avoid an "unknown block tag" warning):
|
|
20
|
+
|
|
21
|
+
```jsonc
|
|
22
|
+
// typedoc.json
|
|
23
|
+
{
|
|
24
|
+
"plugin": ["typedoc-plugin-markdown", "@pantoken/typedoc-plugin-demo"],
|
|
25
|
+
"blockTags": ["@param", "@returns", "@example", "@demo"],
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Authoring
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
/**
|
|
33
|
+
* A button stylesheet.
|
|
34
|
+
*
|
|
35
|
+
* @demo self:button
|
|
36
|
+
* @demo stackblitz:abc123
|
|
37
|
+
*/
|
|
38
|
+
export function buttonCss() {}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Each `@demo` becomes a `demo` fence in the symbol's description, in order. A spec is either a bare
|
|
42
|
+
URL (`@demo https://…` or `@demo /path`) or a `<provider>:<ref>` pair. Common providers a renderer
|
|
43
|
+
might support: `self`, `url`, `stackblitz`, `codesandbox`, `codepen`, `dartpad`, `wp-playground`.
|
|
44
|
+
|
|
45
|
+
## Rendering
|
|
46
|
+
|
|
47
|
+
The plugin only emits the fence. To display it, teach your docs to turn a `demo` fence into an
|
|
48
|
+
iframe — for example a markdown-it fence rule that reads the spec and renders the right embed. In
|
|
49
|
+
pantoken's VitePress site, `@pantoken/demo` resolves the provider spec into iframe attributes.
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
- **`load(app)`** — the TypeDoc entry point.
|
|
54
|
+
- **`rewriteComment(comment)`** — move a comment's `@demo` tags into summary fences (exported for
|
|
55
|
+
testing and reuse).
|
|
56
|
+
- **`toDemoFence(spec)`** — wrap one spec in a `demo` fence.
|
|
57
|
+
- **`DEMO_TAG`** (`"@demo"`), **`DEMO_FENCE`** (`"demo"`) — the tag and fence-language constants.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Application, Comment } from "typedoc";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
/** The block tag this plugin consumes. Register it in TypeDoc's `blockTags` so it won't warn. */
|
|
5
|
+
declare const DEMO_TAG = "@demo";
|
|
6
|
+
/** The fenced-code language the demo spec is emitted under — target it in your renderer. */
|
|
7
|
+
declare const DEMO_FENCE = "demo";
|
|
8
|
+
/**
|
|
9
|
+
* Wrap one demo spec in a fenced ```demo``` block.
|
|
10
|
+
*
|
|
11
|
+
* @param spec - A demo spec: a bare URL or a `<provider>:<ref>` pair.
|
|
12
|
+
* @returns The fenced code block as a string.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { toDemoFence } from "@pantoken/typedoc-plugin-demo";
|
|
17
|
+
*
|
|
18
|
+
* toDemoFence("stackblitz:abc123"); // "```demo\nstackblitz:abc123\n```"
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare function toDemoFence(spec: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Move every `@demo` block tag on a comment into ```demo``` fences appended to its summary, in order.
|
|
24
|
+
* Block-tag content gets re-fenced by the markdown theme, so the fence must live in the summary
|
|
25
|
+
* prose, which is emitted verbatim.
|
|
26
|
+
*
|
|
27
|
+
* @param comment - The comment to rewrite in place.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { rewriteComment } from "@pantoken/typedoc-plugin-demo";
|
|
32
|
+
*
|
|
33
|
+
* // Given a comment with `@demo self:button`, appends a ```demo``` fence and drops the tag.
|
|
34
|
+
* rewriteComment(comment);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare function rewriteComment(comment: Comment): void;
|
|
38
|
+
/**
|
|
39
|
+
* TypeDoc entry point. Registers a resolve-time pass that rewrites `@demo` tags into demo fences.
|
|
40
|
+
*
|
|
41
|
+
* @param app - The TypeDoc application.
|
|
42
|
+
*/
|
|
43
|
+
declare function load(app: Application): void;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { DEMO_FENCE, DEMO_TAG, load, rewriteComment, toDemoFence };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Converter, RendererEvent } from "typedoc";
|
|
4
|
+
//#region src/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* `@pantoken/typedoc-plugin-demo` — a TypeDoc plugin for the `@demo` block tag.
|
|
7
|
+
*
|
|
8
|
+
* Authors attach a live, embeddable demo to any symbol with `@demo <spec>`, where `<spec>` is either
|
|
9
|
+
* a bare URL or a `<provider>:<ref>` pair (for example `stackblitz:abc123`, `codesandbox:xy12z`,
|
|
10
|
+
* `wp-playground:https://…/blueprint.json`, or `self:button`). This plugin registers nothing about
|
|
11
|
+
* providers itself — it stays deliberately dumb and reusable: it moves each `@demo` tag's spec into
|
|
12
|
+
* a fenced ```demo``` block appended to the symbol's summary, and your docs renderer decides how to
|
|
13
|
+
* turn a spec into an iframe. (See `@pantoken/demo` for a renderer that resolves the providers.)
|
|
14
|
+
*
|
|
15
|
+
* The fence rides through markdown untouched — including any translation pipeline that preserves
|
|
16
|
+
* code blocks — so the demo survives localization.
|
|
17
|
+
*
|
|
18
|
+
* **Setup:** add `"@demo"` to TypeDoc's `blockTags` option. The comment parser reads that list before
|
|
19
|
+
* plugins load, so a plugin can't register the tag late enough to suppress the "unknown block tag"
|
|
20
|
+
* warning; it must be in your `typedoc.json`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```jsonc
|
|
24
|
+
* // typedoc.json
|
|
25
|
+
* {
|
|
26
|
+
* "plugin": ["typedoc-plugin-markdown", "@pantoken/typedoc-plugin-demo"],
|
|
27
|
+
* "blockTags": ["@param", "@returns", "@example", "@demo"]
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
* @beta
|
|
33
|
+
*/
|
|
34
|
+
/** The block tag this plugin consumes. Register it in TypeDoc's `blockTags` so it won't warn. */
|
|
35
|
+
const DEMO_TAG = "@demo";
|
|
36
|
+
/** The fenced-code language the demo spec is emitted under — target it in your renderer. */
|
|
37
|
+
const DEMO_FENCE = "demo";
|
|
38
|
+
/**
|
|
39
|
+
* Wrap one demo spec in a fenced ```demo``` block.
|
|
40
|
+
*
|
|
41
|
+
* @param spec - A demo spec: a bare URL or a `<provider>:<ref>` pair.
|
|
42
|
+
* @returns The fenced code block as a string.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { toDemoFence } from "@pantoken/typedoc-plugin-demo";
|
|
47
|
+
*
|
|
48
|
+
* toDemoFence("stackblitz:abc123"); // "```demo\nstackblitz:abc123\n```"
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function toDemoFence(spec) {
|
|
52
|
+
return `\`\`\`${DEMO_FENCE}\n${spec}\n\`\`\``;
|
|
53
|
+
}
|
|
54
|
+
/** Yield every comment attached to a reflection (its own, plus signature/accessor comments). */
|
|
55
|
+
function* commentsOf(reflection) {
|
|
56
|
+
if (reflection.comment) yield reflection.comment;
|
|
57
|
+
const declaration = reflection;
|
|
58
|
+
for (const signature of declaration.signatures ?? []) if (signature.comment) yield signature.comment;
|
|
59
|
+
if (declaration.getSignature?.comment) yield declaration.getSignature.comment;
|
|
60
|
+
if (declaration.setSignature?.comment) yield declaration.setSignature.comment;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Move every `@demo` block tag on a comment into ```demo``` fences appended to its summary, in order.
|
|
64
|
+
* Block-tag content gets re-fenced by the markdown theme, so the fence must live in the summary
|
|
65
|
+
* prose, which is emitted verbatim.
|
|
66
|
+
*
|
|
67
|
+
* @param comment - The comment to rewrite in place.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { rewriteComment } from "@pantoken/typedoc-plugin-demo";
|
|
72
|
+
*
|
|
73
|
+
* // Given a comment with `@demo self:button`, appends a ```demo``` fence and drops the tag.
|
|
74
|
+
* rewriteComment(comment);
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
function rewriteComment(comment) {
|
|
78
|
+
const demos = comment.blockTags.filter((tag) => tag.tag === DEMO_TAG);
|
|
79
|
+
if (demos.length === 0) return;
|
|
80
|
+
comment.blockTags = comment.blockTags.filter((tag) => tag.tag !== DEMO_TAG);
|
|
81
|
+
for (const demo of demos) {
|
|
82
|
+
const spec = demo.content.map((part) => part.text).join("").trim();
|
|
83
|
+
if (!spec) continue;
|
|
84
|
+
const fence = {
|
|
85
|
+
kind: "text",
|
|
86
|
+
text: `\n\n${toDemoFence(spec)}\n`
|
|
87
|
+
};
|
|
88
|
+
comment.summary.push(fence);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function normalizeModuleLink(link) {
|
|
92
|
+
return link.replace(/^\/api\//, "").replace(/^\//, "").replace(/\/+$/, "");
|
|
93
|
+
}
|
|
94
|
+
function flattenSrcNodes(items, targets) {
|
|
95
|
+
return items.map((item) => {
|
|
96
|
+
if (item.items) {
|
|
97
|
+
item.items = flattenSrcNodes(item.items, targets);
|
|
98
|
+
const srcIndex = item.items.findIndex((child) => child.text === "src" && child.link);
|
|
99
|
+
if (srcIndex >= 0) {
|
|
100
|
+
const srcNode = item.items[srcIndex];
|
|
101
|
+
const relativeLink = normalizeModuleLink(srcNode.link);
|
|
102
|
+
if (relativeLink.endsWith("/src")) targets.push({
|
|
103
|
+
link: relativeLink,
|
|
104
|
+
title: item.text
|
|
105
|
+
});
|
|
106
|
+
item.link = srcNode.link;
|
|
107
|
+
const remaining = item.items.filter((_, index) => index !== srcIndex);
|
|
108
|
+
item.items = [...srcNode.items ?? [], ...remaining];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return item;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function replaceFirstLine(content, oldText, newText) {
|
|
115
|
+
const lines = content.split("\n");
|
|
116
|
+
if (lines.length > 0 && lines[0].includes(oldText)) lines[0] = lines[0].replace(oldText, newText);
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
119
|
+
function rewriteModuleHeading(indexPath, title, modulePath) {
|
|
120
|
+
if (!existsSync(indexPath)) return;
|
|
121
|
+
const original = readFileSync(indexPath, "utf8");
|
|
122
|
+
const final = replaceFirstLine(original.replace(/^#\s+.+$/m, `# ${title}`), ` / ${modulePath}`, ` / ${title}`);
|
|
123
|
+
if (final !== original) writeFileSync(indexPath, final, "utf8");
|
|
124
|
+
}
|
|
125
|
+
function normalizeDocsOutput(outputDirectory) {
|
|
126
|
+
const sidebarPath = join(outputDirectory, "typedoc-sidebar.json");
|
|
127
|
+
if (!existsSync(sidebarPath)) return;
|
|
128
|
+
const sidebar = JSON.parse(readFileSync(sidebarPath, "utf8"));
|
|
129
|
+
const moduleTargets = [];
|
|
130
|
+
const normalized = flattenSrcNodes(sidebar, moduleTargets);
|
|
131
|
+
writeFileSync(sidebarPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
132
|
+
for (const target of moduleTargets) rewriteModuleHeading(join(outputDirectory, target.link, "index.md"), target.title, target.link);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* TypeDoc entry point. Registers a resolve-time pass that rewrites `@demo` tags into demo fences.
|
|
136
|
+
*
|
|
137
|
+
* @param app - The TypeDoc application.
|
|
138
|
+
*/
|
|
139
|
+
function load(app) {
|
|
140
|
+
app.converter.on(Converter.EVENT_RESOLVE_BEGIN, (context) => {
|
|
141
|
+
for (const reflection of Object.values(context.project.reflections)) for (const comment of commentsOf(reflection)) rewriteComment(comment);
|
|
142
|
+
});
|
|
143
|
+
app.renderer.on(RendererEvent.END, (event) => {
|
|
144
|
+
normalizeDocsOutput(event.outputDirectory);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { DEMO_FENCE, DEMO_TAG, load, rewriteComment, toDemoFence };
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pantoken/typedoc-plugin-demo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeDoc plugin: turn a @demo <provider>:<ref> block tag into an embeddable demo fence your docs render as an iframe.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.mjs",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^24.13.3",
|
|
19
|
+
"typedoc": "^0.28.20",
|
|
20
|
+
"typescript": "^6.0.3",
|
|
21
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.4",
|
|
22
|
+
"vite-plus": "0.2.4"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"typedoc": ">=0.28"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "vp pack",
|
|
29
|
+
"dev": "vp pack --watch",
|
|
30
|
+
"test": "vp test",
|
|
31
|
+
"check": "vp check"
|
|
32
|
+
}
|
|
33
|
+
}
|