@growthbeaker/vscode-help-docs 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 +183 -0
- package/dist/extension.d.ts +4 -0
- package/dist/extension.js +54 -0
- package/dist/help-panel.d.ts +62 -0
- package/dist/help-panel.js +272 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/markdown-renderer.d.ts +27 -0
- package/dist/markdown-renderer.js +139 -0
- package/dist/nav-tree.d.ts +19 -0
- package/dist/nav-tree.js +215 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.js +3 -0
- package/package.json +63 -0
- package/src/webview/main.js +312 -0
- package/src/webview/styles.css +256 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# vscode-help-docs
|
|
2
|
+
|
|
3
|
+
A reusable package that turns a folder of markdown files into a navigable, themed help viewer panel inside VS Code.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
VS Code extensions that ship user-facing docs have three unsatisfying options: open an external browser (loses context), use the built-in markdown preview (no sidebar, no branding), or build a custom webview from scratch. This package eliminates that repeated work.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install vscode-help-docs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createHelpPanel } from "vscode-help-docs";
|
|
19
|
+
|
|
20
|
+
// In your extension's activate()
|
|
21
|
+
const cmd = vscode.commands.registerCommand("myExtension.openHelp", () => {
|
|
22
|
+
createHelpPanel({
|
|
23
|
+
contentRoot: path.join(context.extensionPath, "help"),
|
|
24
|
+
extensionContext: context,
|
|
25
|
+
title: "My Extension Help",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
context.subscriptions.push(cmd);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Register the command in your `package.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"contributes": {
|
|
37
|
+
"commands": [{
|
|
38
|
+
"command": "myExtension.openHelp",
|
|
39
|
+
"title": "Open Help"
|
|
40
|
+
}]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Writing Content
|
|
46
|
+
|
|
47
|
+
### Markdown files
|
|
48
|
+
|
|
49
|
+
Each `.md` file uses YAML frontmatter for metadata:
|
|
50
|
+
|
|
51
|
+
```markdown
|
|
52
|
+
---
|
|
53
|
+
title: Getting Started
|
|
54
|
+
order: 1
|
|
55
|
+
hidden: false
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# Getting Started
|
|
59
|
+
|
|
60
|
+
Your content here...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Field | Type | Default | Description |
|
|
64
|
+
|-------|------|---------|-------------|
|
|
65
|
+
| `title` | string | Filename (title-cased) | Display name in the nav sidebar |
|
|
66
|
+
| `order` | number | Alphabetical | Sort position within its folder |
|
|
67
|
+
| `hidden` | boolean | `false` | If `true`, excluded from nav tree |
|
|
68
|
+
|
|
69
|
+
### Folder metadata
|
|
70
|
+
|
|
71
|
+
Each folder can contain an optional `_meta.json`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"title": "User Guide",
|
|
76
|
+
"order": 1,
|
|
77
|
+
"collapsed": false
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Example content tree
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
help/
|
|
85
|
+
getting-started/
|
|
86
|
+
_meta.json → { "title": "Getting Started", "order": 1 }
|
|
87
|
+
01-installation.md
|
|
88
|
+
02-first-project.md
|
|
89
|
+
features/
|
|
90
|
+
_meta.json → { "title": "Features", "order": 2 }
|
|
91
|
+
overview.md
|
|
92
|
+
troubleshooting/
|
|
93
|
+
_meta.json → { "title": "Troubleshooting", "order": 3 }
|
|
94
|
+
common-issues.md
|
|
95
|
+
faq.md
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
createHelpPanel({
|
|
102
|
+
// Required
|
|
103
|
+
contentRoot: string, // Absolute path to your docs folder
|
|
104
|
+
extensionContext: ExtensionContext,
|
|
105
|
+
|
|
106
|
+
// Optional
|
|
107
|
+
title: "Help", // Panel tab title
|
|
108
|
+
viewColumn: ViewColumn.One, // Which editor column
|
|
109
|
+
defaultPage: "intro.md", // Page shown on open (default: first by sort order)
|
|
110
|
+
enableAnchors: true, // Anchor link scrolling
|
|
111
|
+
enableMermaid: false, // Mermaid diagram rendering (see below)
|
|
112
|
+
metaFilename: "_meta.json", // Folder metadata filename
|
|
113
|
+
customCss: "", // Inline CSS to inject
|
|
114
|
+
customCssPath: "", // Path to a CSS file to inject
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## API
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const help = createHelpPanel(config);
|
|
122
|
+
|
|
123
|
+
help.navigateTo("features/overview.md"); // Navigate programmatically
|
|
124
|
+
help.refresh(); // Rebuild nav tree
|
|
125
|
+
help.dispose(); // Close the panel
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Calling `createHelpPanel` with the same `contentRoot` when a panel is already open reveals the existing panel (singleton behavior).
|
|
129
|
+
|
|
130
|
+
## Mermaid Diagrams
|
|
131
|
+
|
|
132
|
+
Enable Mermaid diagram rendering for fenced ` ```mermaid ` code blocks:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
createHelpPanel({
|
|
136
|
+
// ...
|
|
137
|
+
enableMermaid: true,
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Supports flowcharts, sequence diagrams, class diagrams, state diagrams, pie charts, git graphs, and more.
|
|
142
|
+
|
|
143
|
+
**Note:** Enabling Mermaid adds `'unsafe-inline'` to the webview's `style-src` CSP directive (Mermaid injects inline styles into SVGs). When disabled, the strict CSP is preserved.
|
|
144
|
+
|
|
145
|
+
## Theming
|
|
146
|
+
|
|
147
|
+
The viewer automatically adapts to the active VS Code theme (Light, Dark, High Contrast) using CSS custom properties. Override styles with `customCss` or `customCssPath`.
|
|
148
|
+
|
|
149
|
+
## Error Handling
|
|
150
|
+
|
|
151
|
+
The package handles common content authoring mistakes gracefully:
|
|
152
|
+
|
|
153
|
+
- **Malformed YAML frontmatter** — file still appears in nav using defaults, warning logged
|
|
154
|
+
- **Invalid field types** (e.g., `order: "five"`) — invalid fields fall back to defaults, valid fields preserved
|
|
155
|
+
- **Malformed `_meta.json`** — folder uses defaults, siblings unaffected
|
|
156
|
+
- **Circular symlinks** — detected and skipped, rest of tree built normally
|
|
157
|
+
- **Excessive nesting** (>20 levels) — truncated with warning
|
|
158
|
+
- **Broken internal links** — stays on current page, shows error message, logs diagnostic
|
|
159
|
+
- **Missing anchors** — navigates to page, scrolls to top, shows notification
|
|
160
|
+
|
|
161
|
+
## Links
|
|
162
|
+
|
|
163
|
+
Internal links navigate within the viewer, external links open in the system browser:
|
|
164
|
+
|
|
165
|
+
```markdown
|
|
166
|
+
[Installation](../getting-started/installation.md) <!-- internal -->
|
|
167
|
+
[With anchor](../features/overview.md#configuration) <!-- internal + anchor -->
|
|
168
|
+
[VS Code](https://code.visualstudio.com) <!-- external -->
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Dependencies
|
|
172
|
+
|
|
173
|
+
| Package | Purpose |
|
|
174
|
+
|---------|---------|
|
|
175
|
+
| `markdown-it` | Markdown rendering |
|
|
176
|
+
| `gray-matter` | Frontmatter parsing |
|
|
177
|
+
| `mermaid` | Diagram rendering (used when `enableMermaid: true`) |
|
|
178
|
+
|
|
179
|
+
Optional: `markdown-it-anchor` for heading anchor IDs.
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.activate = activate;
|
|
37
|
+
exports.deactivate = deactivate;
|
|
38
|
+
const vscode = __importStar(require("vscode"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const help_panel_1 = require("./help-panel");
|
|
41
|
+
function activate(context) {
|
|
42
|
+
const cmd = vscode.commands.registerCommand('helpDocs.open', () => {
|
|
43
|
+
(0, help_panel_1.createHelpPanel)({
|
|
44
|
+
contentRoot: path.join(context.extensionPath, 'test-help'),
|
|
45
|
+
extensionContext: context,
|
|
46
|
+
title: 'Help Docs',
|
|
47
|
+
defaultPage: 'getting-started/01-installation.md',
|
|
48
|
+
enableMermaid: true,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
context.subscriptions.push(cmd);
|
|
52
|
+
}
|
|
53
|
+
function deactivate() { }
|
|
54
|
+
//# sourceMappingURL=extension.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import { HelpViewerConfig, IHelpPanel, WebviewToExtensionMessage } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Create (or reveal) a Help Viewer panel.
|
|
5
|
+
* Singleton behavior: if a panel already exists for the same contentRoot, it is revealed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createHelpPanel(config: HelpViewerConfig): IHelpPanel;
|
|
8
|
+
/**
|
|
9
|
+
* HelpPanel class implementing the Help Viewer webview panel.
|
|
10
|
+
*
|
|
11
|
+
* Implements:
|
|
12
|
+
* - screen:help-viewer-panel
|
|
13
|
+
* - flow:broken-internal-link-handler (AC 1.5.4 / nv00001)
|
|
14
|
+
* - flow:anchor-navigation-handler (AC 1.5.5 / nv00002)
|
|
15
|
+
*/
|
|
16
|
+
export declare class HelpPanel implements IHelpPanel {
|
|
17
|
+
readonly panel: vscode.WebviewPanel;
|
|
18
|
+
private readonly config;
|
|
19
|
+
private readonly outputChannel;
|
|
20
|
+
private navTree;
|
|
21
|
+
private currentPage;
|
|
22
|
+
constructor(config: HelpViewerConfig);
|
|
23
|
+
private initialize;
|
|
24
|
+
/**
|
|
25
|
+
* Handle messages from the webview.
|
|
26
|
+
*
|
|
27
|
+
* Implements:
|
|
28
|
+
* - flow:broken-internal-link-handler (navigateTo with missing file)
|
|
29
|
+
* - flow:anchor-navigation-handler (navigateToAnchor with missing anchor)
|
|
30
|
+
*/
|
|
31
|
+
handleWebviewMessage(message: WebviewToExtensionMessage): void;
|
|
32
|
+
/**
|
|
33
|
+
* Navigate to a documentation page.
|
|
34
|
+
*
|
|
35
|
+
* AC 1.5.4 (nv00001): If the target file does not exist:
|
|
36
|
+
* - Do not navigate away from current page
|
|
37
|
+
* - Display non-blocking error message
|
|
38
|
+
* - Log warning to output channel with target path and source document
|
|
39
|
+
* - No crash/reload
|
|
40
|
+
*/
|
|
41
|
+
navigateTo(relativePath: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Handle anchor navigation.
|
|
44
|
+
*
|
|
45
|
+
* AC 1.5.5 (nv00002): If the anchor is not found:
|
|
46
|
+
* - Still navigate to target page (if specified)
|
|
47
|
+
* - Scroll to top when anchor not found
|
|
48
|
+
* - Display non-blocking notification
|
|
49
|
+
* - No crash/reload
|
|
50
|
+
*/
|
|
51
|
+
private handleAnchorNavigation;
|
|
52
|
+
/** Refresh the nav tree and re-render current page */
|
|
53
|
+
refresh(): Promise<void>;
|
|
54
|
+
/** Dispose the panel */
|
|
55
|
+
dispose(): void;
|
|
56
|
+
/** Get the current page path (for testing) */
|
|
57
|
+
getCurrentPage(): string | null;
|
|
58
|
+
private postMessage;
|
|
59
|
+
private findFirstPage;
|
|
60
|
+
private getWebviewHtml;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=help-panel.d.ts.map
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HelpPanel = void 0;
|
|
37
|
+
exports.createHelpPanel = createHelpPanel;
|
|
38
|
+
const vscode = __importStar(require("vscode"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const crypto = __importStar(require("crypto"));
|
|
42
|
+
const nav_tree_1 = require("./nav-tree");
|
|
43
|
+
const markdown_renderer_1 = require("./markdown-renderer");
|
|
44
|
+
/** Map of contentRoot -> active HelpPanel for singleton behavior */
|
|
45
|
+
const activePanels = new Map();
|
|
46
|
+
/**
|
|
47
|
+
* Create (or reveal) a Help Viewer panel.
|
|
48
|
+
* Singleton behavior: if a panel already exists for the same contentRoot, it is revealed.
|
|
49
|
+
*/
|
|
50
|
+
function createHelpPanel(config) {
|
|
51
|
+
const existing = activePanels.get(config.contentRoot);
|
|
52
|
+
if (existing && existing.panel) {
|
|
53
|
+
existing.panel.reveal(config.viewColumn ?? vscode.ViewColumn.One);
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
const panel = new HelpPanel(config);
|
|
57
|
+
activePanels.set(config.contentRoot, panel);
|
|
58
|
+
return panel;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* HelpPanel class implementing the Help Viewer webview panel.
|
|
62
|
+
*
|
|
63
|
+
* Implements:
|
|
64
|
+
* - screen:help-viewer-panel
|
|
65
|
+
* - flow:broken-internal-link-handler (AC 1.5.4 / nv00001)
|
|
66
|
+
* - flow:anchor-navigation-handler (AC 1.5.5 / nv00002)
|
|
67
|
+
*/
|
|
68
|
+
class HelpPanel {
|
|
69
|
+
constructor(config) {
|
|
70
|
+
this.navTree = [];
|
|
71
|
+
this.currentPage = null;
|
|
72
|
+
this.config = config;
|
|
73
|
+
this.outputChannel = vscode.window.createOutputChannel('Help Docs');
|
|
74
|
+
this.panel = vscode.window.createWebviewPanel('helpViewer', config.title ?? 'Help', config.viewColumn ?? vscode.ViewColumn.One, {
|
|
75
|
+
enableScripts: true,
|
|
76
|
+
localResourceRoots: [
|
|
77
|
+
vscode.Uri.file(config.contentRoot),
|
|
78
|
+
vscode.Uri.file(path.join(config.extensionContext.extensionPath, 'node_modules', 'vscode-help-docs', 'src', 'webview')),
|
|
79
|
+
vscode.Uri.file(path.join(__dirname, '..', 'src', 'webview')),
|
|
80
|
+
// Allow loading mermaid.js from node_modules
|
|
81
|
+
vscode.Uri.file(path.join(__dirname, '..', 'node_modules', 'mermaid', 'dist')),
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
this.panel.onDidDispose(() => {
|
|
85
|
+
activePanels.delete(config.contentRoot);
|
|
86
|
+
this.outputChannel.dispose();
|
|
87
|
+
});
|
|
88
|
+
this.panel.webview.onDidReceiveMessage((message) => this.handleWebviewMessage(message));
|
|
89
|
+
this.initialize();
|
|
90
|
+
}
|
|
91
|
+
async initialize() {
|
|
92
|
+
this.navTree = await (0, nav_tree_1.buildNavTree)(this.config.contentRoot, {
|
|
93
|
+
metaFilename: this.config.metaFilename,
|
|
94
|
+
});
|
|
95
|
+
this.panel.webview.html = this.getWebviewHtml();
|
|
96
|
+
// Navigate to default page or first page
|
|
97
|
+
const defaultPage = this.config.defaultPage ?? this.findFirstPage(this.navTree);
|
|
98
|
+
if (defaultPage) {
|
|
99
|
+
this.navigateTo(defaultPage);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Handle messages from the webview.
|
|
104
|
+
*
|
|
105
|
+
* Implements:
|
|
106
|
+
* - flow:broken-internal-link-handler (navigateTo with missing file)
|
|
107
|
+
* - flow:anchor-navigation-handler (navigateToAnchor with missing anchor)
|
|
108
|
+
*/
|
|
109
|
+
handleWebviewMessage(message) {
|
|
110
|
+
switch (message.type) {
|
|
111
|
+
case 'navigateTo':
|
|
112
|
+
this.navigateTo(message.path);
|
|
113
|
+
break;
|
|
114
|
+
case 'navigateToAnchor':
|
|
115
|
+
this.handleAnchorNavigation(message.path, message.anchor);
|
|
116
|
+
break;
|
|
117
|
+
case 'openExternal':
|
|
118
|
+
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
|
119
|
+
break;
|
|
120
|
+
case 'ready':
|
|
121
|
+
this.postMessage({ type: 'navTree', tree: this.navTree });
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Navigate to a documentation page.
|
|
127
|
+
*
|
|
128
|
+
* AC 1.5.4 (nv00001): If the target file does not exist:
|
|
129
|
+
* - Do not navigate away from current page
|
|
130
|
+
* - Display non-blocking error message
|
|
131
|
+
* - Log warning to output channel with target path and source document
|
|
132
|
+
* - No crash/reload
|
|
133
|
+
*/
|
|
134
|
+
navigateTo(relativePath) {
|
|
135
|
+
const fullPath = path.join(this.config.contentRoot, relativePath);
|
|
136
|
+
// AC 1.5.4: Check if file exists
|
|
137
|
+
if (!fs.existsSync(fullPath)) {
|
|
138
|
+
// Display error message to user via webview
|
|
139
|
+
this.postMessage({
|
|
140
|
+
type: 'error',
|
|
141
|
+
message: `Page not found: \`${relativePath}\``,
|
|
142
|
+
});
|
|
143
|
+
// Log diagnostic warning to output channel
|
|
144
|
+
this.outputChannel.appendLine(`[WARN] Broken internal link to "${relativePath}" from "${this.currentPage ?? '(none)'}"`);
|
|
145
|
+
// Do NOT update currentPage — stay on current page
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const rawContent = fs.readFileSync(fullPath, 'utf-8');
|
|
150
|
+
const { frontmatter, content } = (0, markdown_renderer_1.parseFrontmatter)(fullPath, rawContent);
|
|
151
|
+
const html = (0, markdown_renderer_1.renderMarkdown)(content, this.config.enableMermaid);
|
|
152
|
+
const title = frontmatter.title ?? path.basename(relativePath, '.md');
|
|
153
|
+
this.currentPage = relativePath;
|
|
154
|
+
this.postMessage({ type: 'contentLoaded', html, title, path: relativePath });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
158
|
+
this.postMessage({
|
|
159
|
+
type: 'error',
|
|
160
|
+
message: `Error loading page: ${errorMessage}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Handle anchor navigation.
|
|
166
|
+
*
|
|
167
|
+
* AC 1.5.5 (nv00002): If the anchor is not found:
|
|
168
|
+
* - Still navigate to target page (if specified)
|
|
169
|
+
* - Scroll to top when anchor not found
|
|
170
|
+
* - Display non-blocking notification
|
|
171
|
+
* - No crash/reload
|
|
172
|
+
*/
|
|
173
|
+
handleAnchorNavigation(pagePath, anchor) {
|
|
174
|
+
// If a page path is provided, navigate to it first
|
|
175
|
+
if (pagePath) {
|
|
176
|
+
this.navigateTo(pagePath);
|
|
177
|
+
}
|
|
178
|
+
// Send anchor scroll command to webview
|
|
179
|
+
// The webview will report back if the anchor is not found
|
|
180
|
+
this.postMessage({ type: 'scrollToAnchor', anchor });
|
|
181
|
+
}
|
|
182
|
+
/** Refresh the nav tree and re-render current page */
|
|
183
|
+
async refresh() {
|
|
184
|
+
this.navTree = await (0, nav_tree_1.buildNavTree)(this.config.contentRoot, {
|
|
185
|
+
metaFilename: this.config.metaFilename,
|
|
186
|
+
});
|
|
187
|
+
this.postMessage({ type: 'navTree', tree: this.navTree });
|
|
188
|
+
if (this.currentPage) {
|
|
189
|
+
this.navigateTo(this.currentPage);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Dispose the panel */
|
|
193
|
+
dispose() {
|
|
194
|
+
this.panel.dispose();
|
|
195
|
+
}
|
|
196
|
+
/** Get the current page path (for testing) */
|
|
197
|
+
getCurrentPage() {
|
|
198
|
+
return this.currentPage;
|
|
199
|
+
}
|
|
200
|
+
postMessage(message) {
|
|
201
|
+
this.panel.webview.postMessage(message);
|
|
202
|
+
}
|
|
203
|
+
findFirstPage(nodes) {
|
|
204
|
+
for (const node of nodes) {
|
|
205
|
+
if (node.type === 'page') {
|
|
206
|
+
return node.path;
|
|
207
|
+
}
|
|
208
|
+
if (node.children) {
|
|
209
|
+
const found = this.findFirstPage(node.children);
|
|
210
|
+
if (found)
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
getWebviewHtml() {
|
|
217
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
218
|
+
const cspSource = this.panel.webview.cspSource;
|
|
219
|
+
const enableMermaid = this.config.enableMermaid ?? false;
|
|
220
|
+
const styleUri = this.panel.webview.asWebviewUri(vscode.Uri.file(path.join(__dirname, '..', 'src', 'webview', 'styles.css')));
|
|
221
|
+
const scriptUri = this.panel.webview.asWebviewUri(vscode.Uri.file(path.join(__dirname, '..', 'src', 'webview', 'main.js')));
|
|
222
|
+
// Mermaid injects inline styles into SVGs, so we need 'unsafe-inline' in style-src when enabled
|
|
223
|
+
const styleSrc = enableMermaid
|
|
224
|
+
? `${cspSource} 'nonce-${nonce}' 'unsafe-inline'`
|
|
225
|
+
: `${cspSource} 'nonce-${nonce}'`;
|
|
226
|
+
let mermaidScript = '';
|
|
227
|
+
if (enableMermaid) {
|
|
228
|
+
try {
|
|
229
|
+
const mermaidPath = require.resolve('mermaid/dist/mermaid.min.js');
|
|
230
|
+
const mermaidUri = this.panel.webview.asWebviewUri(vscode.Uri.file(mermaidPath));
|
|
231
|
+
mermaidScript = `<script nonce="${nonce}" src="${mermaidUri}"></script>`;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// mermaid not installed — skip silently
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
let customStyles = '';
|
|
238
|
+
if (this.config.customCss) {
|
|
239
|
+
customStyles += `<style nonce="${nonce}">${this.config.customCss}</style>`;
|
|
240
|
+
}
|
|
241
|
+
if (this.config.customCssPath) {
|
|
242
|
+
const customCssUri = this.panel.webview.asWebviewUri(vscode.Uri.file(this.config.customCssPath));
|
|
243
|
+
customStyles += `<link rel="stylesheet" href="${customCssUri}">`;
|
|
244
|
+
}
|
|
245
|
+
return `<!DOCTYPE html>
|
|
246
|
+
<html lang="en">
|
|
247
|
+
<head>
|
|
248
|
+
<meta charset="UTF-8">
|
|
249
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
250
|
+
<meta http-equiv="Content-Security-Policy"
|
|
251
|
+
content="default-src 'none'; style-src ${styleSrc}; script-src 'nonce-${nonce}'; img-src ${cspSource} https:;">
|
|
252
|
+
<link rel="stylesheet" href="${styleUri}">
|
|
253
|
+
${customStyles}
|
|
254
|
+
<title>${this.config.title ?? 'Help'}</title>
|
|
255
|
+
</head>
|
|
256
|
+
<body>
|
|
257
|
+
<div class="help-viewer">
|
|
258
|
+
<nav class="nav-sidebar" id="nav-sidebar"></nav>
|
|
259
|
+
<main class="content-pane" id="content-pane">
|
|
260
|
+
<div class="error-banner" id="error-banner" style="display:none;"></div>
|
|
261
|
+
<div class="notification-toast" id="notification-toast" style="display:none;"></div>
|
|
262
|
+
<article id="content-body"></article>
|
|
263
|
+
</main>
|
|
264
|
+
</div>
|
|
265
|
+
${mermaidScript}
|
|
266
|
+
<script nonce="${nonce}" src="${scriptUri}"></script>
|
|
267
|
+
</body>
|
|
268
|
+
</html>`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
exports.HelpPanel = HelpPanel;
|
|
272
|
+
//# sourceMappingURL=help-panel.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHelpPanel = void 0;
|
|
4
|
+
var help_panel_1 = require("./help-panel");
|
|
5
|
+
Object.defineProperty(exports, "createHelpPanel", { enumerable: true, get: function () { return help_panel_1.createHelpPanel; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Frontmatter } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Parse frontmatter from a markdown file with graceful error handling.
|
|
4
|
+
*
|
|
5
|
+
* Implements:
|
|
6
|
+
* - AC 1.8.2: Malformed YAML Frontmatter Graceful Degradation
|
|
7
|
+
* - AC 1.8.3: Frontmatter Field Type Validation
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseFrontmatter(filePath: string, rawContent: string): {
|
|
10
|
+
frontmatter: Frontmatter;
|
|
11
|
+
content: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Validate frontmatter field types, logging warnings for invalid types.
|
|
15
|
+
*
|
|
16
|
+
* Implements AC 1.8.3: Frontmatter Field Type Validation
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateFrontmatterFields(filePath: string, data: Record<string, unknown>): Frontmatter;
|
|
19
|
+
/** Render markdown content to HTML */
|
|
20
|
+
export declare function renderMarkdown(content: string, enableMermaid?: boolean): string;
|
|
21
|
+
/**
|
|
22
|
+
* Convert a filename to a title-cased display name.
|
|
23
|
+
* Strips .md extension, replaces hyphens/underscores with spaces,
|
|
24
|
+
* removes leading numeric prefixes (e.g., "01-"), and title-cases each word.
|
|
25
|
+
*/
|
|
26
|
+
export declare function titleCaseFilename(filename: string): string;
|
|
27
|
+
//# sourceMappingURL=markdown-renderer.d.ts.map
|