@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 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
@@ -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
+ }