@docmd/core 0.4.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/LICENSE +21 -0
- package/README.md +166 -0
- package/bin/docmd.js +52 -0
- package/package.json +23 -0
- package/src/commands/build.js +262 -0
- package/src/commands/dev.js +353 -0
- package/src/commands/init.js +277 -0
- package/src/commands/live.js +15 -0
- package/src/utils/config-loader.js +38 -0
- package/src/utils/fs-utils.js +40 -0
- package/src/utils/logger.js +21 -0
- package/src/utils/plugin-loader.js +90 -0
- package/src/utils/plugin-runner.js +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 docmd (docmd.io)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<!-- PROJECT TITLE -->
|
|
4
|
+
<h1>
|
|
5
|
+
<img src="https://github.com/docmd-io/docmd/blob/main/src/assets/images/docmd-logo-dark.png?raw=true" alt="docmd logo" width="200" />
|
|
6
|
+
<!-- docmd -->
|
|
7
|
+
</h1>
|
|
8
|
+
|
|
9
|
+
<!-- ONE LINE SUMMARY -->
|
|
10
|
+
<p>
|
|
11
|
+
<b>The minimalist, zero-config documentation generator.</b>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<!-- BADGES -->
|
|
15
|
+
<p>
|
|
16
|
+
<a href="https://www.npmjs.com/package/@docmd/core"><img src="https://img.shields.io/npm/v/@docmd/core.svg?style=flat-square&color=d25353" alt="npm version"></a>
|
|
17
|
+
<a href="https://www.npmjs.com/package/@docmd/core?activeTab=versions"><img src="https://img.shields.io/npm/dt/@docmd/core.svg?style=flat-square&color=38bd24" alt="downloads"></a>
|
|
18
|
+
<a href="https://github.com/docmd-io/docmd/stargazers"><img src="https://img.shields.io/github/stars/docmd-io/docmd?style=flat-square&logo=github" alt="stars"></a>
|
|
19
|
+
<a href="https://github.com/docmd-io/docmd/blob/main/LICENSE"><img src="https://img.shields.io/github/license/docmd-io/docmd.svg?style=flat-square&color=blue" alt="license"></a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<!-- MENU -->
|
|
23
|
+
<p>
|
|
24
|
+
<h4>
|
|
25
|
+
<a href="https://docmd.io">View Demo</a> •
|
|
26
|
+
<a href="https://docs.docmd.io/getting-started/installation/">Documentation</a> •
|
|
27
|
+
<a href="https://live.docmd.io">Live Editor</a> •
|
|
28
|
+
<a href="https://github.com/docmd-io/docmd/issues">Report Bug</a>
|
|
29
|
+
</h4>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<!-- PREVIEW -->
|
|
33
|
+
<p>
|
|
34
|
+
<img width="800" alt="docmd preview" src="https://github.com/user-attachments/assets/1a74d6f7-10f9-41fa-be8a-faeee278dbb9" />
|
|
35
|
+
<br/>
|
|
36
|
+
<sup><i>docmd noStyle page preview in light mode</i></sup>
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Zero Config**: Works out of the box with sensible defaults. Just `init` and go.
|
|
44
|
+
- **Blazing Fast**: Generates **pure, static HTML**. No React hydration lag, no heavy bundles.
|
|
45
|
+
- **Smart Search**: Built-in, **offline-capable** full-text search with fuzzy matching. No API keys required.
|
|
46
|
+
- **Isomorphic Core**: Runs anywhere, Node.js CLI, CI/CD pipelines, or **directly in the browser**.
|
|
47
|
+
- **Rich Content**: Built-in support for Callouts, Cards, Tabs, Steps, Changelogs, and Mermaid diagrams.
|
|
48
|
+
- **Theming**: Beautiful light/dark modes and multiple pre-built themes (`sky`, `ruby`, `retro`).
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install -g @docmd/core
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
### CLI
|
|
59
|
+
|
|
60
|
+
The Command Line Interface is the primary way to interact with `docmd`.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
docmd init # Initialize a new project with config and assets
|
|
64
|
+
docmd dev # Start a local development server with hot-reload
|
|
65
|
+
docmd build # Generate a production-ready static site in ./site
|
|
66
|
+
docmd live # Launch the browser-based Live Editor locally
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### API
|
|
70
|
+
|
|
71
|
+
`docmd` exports its core engine, allowing you to build documentation programmatically within your own Node.js scripts or build tools.
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const { build, buildLive } = require('@docmd/core');
|
|
75
|
+
|
|
76
|
+
// Trigger a standard documentation build
|
|
77
|
+
await build('./docmd.config.js', {
|
|
78
|
+
isDev: false,
|
|
79
|
+
preserve: true
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Trigger a Live Editor bundle build
|
|
83
|
+
await buildLive();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Live Editor
|
|
87
|
+
|
|
88
|
+
`docmd` features a modular architecture that allows the core engine to run client-side.
|
|
89
|
+
|
|
90
|
+
Running `docmd live` builds a standalone web application where you can write Markdown and see the preview instantly without any server-side processing. You can embed the generated `docmd-live.js` bundle to add Markdown capabilities to your own applications.
|
|
91
|
+
|
|
92
|
+
## Project Structure
|
|
93
|
+
|
|
94
|
+
`docmd` keeps it simple. Your content lives in `docs/`, your config in `docmd.config.js`.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
my-docs/
|
|
98
|
+
├── docs/ # Your Markdown files
|
|
99
|
+
│ ├── index.md # Homepage
|
|
100
|
+
│ └── guide.md # Content page
|
|
101
|
+
├── assets/ # Images and custom CSS
|
|
102
|
+
├── docmd.config.js # Configuration
|
|
103
|
+
└── package.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
Customize your site in seconds via `docmd.config.js`:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
module.exports = {
|
|
112
|
+
siteTitle: 'My Project',
|
|
113
|
+
siteUrl: 'https://mysite.com',
|
|
114
|
+
srcDir: 'docs',
|
|
115
|
+
outputDir: 'site',
|
|
116
|
+
|
|
117
|
+
// Theme Settings
|
|
118
|
+
theme: {
|
|
119
|
+
name: 'sky', // 'default', 'sky', 'ruby', 'retro'
|
|
120
|
+
defaultMode: 'system', // 'light', 'dark', or 'system'
|
|
121
|
+
enableModeToggle: true
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Sidebar Navigation
|
|
125
|
+
navigation: [
|
|
126
|
+
{ title: 'Home', path: '/', icon: 'home' },
|
|
127
|
+
{
|
|
128
|
+
title: 'Guide',
|
|
129
|
+
icon: 'book',
|
|
130
|
+
children: [
|
|
131
|
+
{ title: 'Installation', path: '/guide/install' }
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
|
|
136
|
+
// Plugins
|
|
137
|
+
plugins: {
|
|
138
|
+
seo: { /* ... */ },
|
|
139
|
+
sitemap: { /* ... */ }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Comparison
|
|
145
|
+
|
|
146
|
+
| Feature | docmd | Docusaurus | MkDocs | Mintlify |
|
|
147
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
148
|
+
| **Language** | **Node.js** | React.js | Python | Proprietary |
|
|
149
|
+
| **Output** | **Static HTML** | React SPA | Static HTML | Hosted |
|
|
150
|
+
| **JS Payload** | **Tiny (< 15kb)** | Heavy | Minimal | Medium |
|
|
151
|
+
| **Search** | **Built-in (Offline)** | Algolia (Ext) | Built-in | Built-in |
|
|
152
|
+
| **Setup** | **~1 min** | ~15 mins | ~10 mins | Instant |
|
|
153
|
+
| **Cost** | **Free OSS** | Free OSS | Free OSS | Freemium |
|
|
154
|
+
|
|
155
|
+
## Community & Support
|
|
156
|
+
|
|
157
|
+
- **Contributing**: We welcome PRs! See [CONTRIBUTING.md](.github/CONTRIBUTING.md).
|
|
158
|
+
- **Support**: If you find `docmd` useful, please consider [sponsoring the project](https://github.com/sponsors/mgks) or giving it a star ⭐.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
163
|
+
|
|
164
|
+
> **{ github.com/mgks }**
|
|
165
|
+
>
|
|
166
|
+
>  
|
package/bin/docmd.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { version } = require('../package.json');
|
|
5
|
+
const { initProject } = require('../src/commands/init');
|
|
6
|
+
const { buildSite } = require('../src/commands/build');
|
|
7
|
+
const { startDevServer } = require('../src/commands/dev');
|
|
8
|
+
const { buildLive } = require('../src/commands/live');
|
|
9
|
+
const { printBanner } = require('../src/utils/logger');
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('docmd')
|
|
13
|
+
.description('The minimalist, zero-config documentation generator')
|
|
14
|
+
.version(version);
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('init')
|
|
18
|
+
.action(() => {
|
|
19
|
+
printBanner();
|
|
20
|
+
initProject();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command('build')
|
|
25
|
+
.option('-c, --config <path>', 'Path to config', 'docmd.config.js')
|
|
26
|
+
.option('--offline', 'Optimize for file:// viewing')
|
|
27
|
+
.action((opts) => {
|
|
28
|
+
buildSite(opts.config, { isDev: false, offline: opts.offline });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('dev')
|
|
33
|
+
.option('-c, --config <path>', 'Path to config', 'docmd.config.js')
|
|
34
|
+
.option('-p, --port <number>', 'Port to run server')
|
|
35
|
+
.action((opts) => {
|
|
36
|
+
printBanner();
|
|
37
|
+
startDevServer(opts.config, opts);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('live')
|
|
42
|
+
.action(async () => {
|
|
43
|
+
try {
|
|
44
|
+
await buildLive();
|
|
45
|
+
// ... (Add the spawn 'serve' logic here from the backup if desired)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error(e);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@docmd/core",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "The minimalist, zero-config documentation generator.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"docmd": "./bin/docmd.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"chalk": "^4.1.2",
|
|
10
|
+
"chokidar": "^3.5.3",
|
|
11
|
+
"commander": "^11.0.0",
|
|
12
|
+
"ws": "^8.13.0",
|
|
13
|
+
"@docmd/themes": "0.4.0",
|
|
14
|
+
"@docmd/ui": "0.4.0",
|
|
15
|
+
"@docmd/parser": "0.4.0",
|
|
16
|
+
"@docmd/live": "0.4.0",
|
|
17
|
+
"@docmd/plugin-seo": "0.4.0",
|
|
18
|
+
"@docmd/plugin-sitemap": "0.4.0",
|
|
19
|
+
"@docmd/plugin-search": "0.4.0",
|
|
20
|
+
"@docmd/plugin-analytics": "0.4.0"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT"
|
|
23
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('../utils/fs-utils');
|
|
3
|
+
const { loadConfig } = require('../utils/config-loader');
|
|
4
|
+
const { loadPlugins } = require('../utils/plugin-loader');
|
|
5
|
+
const parser = require('@docmd/parser');
|
|
6
|
+
const ui = require('@docmd/ui');
|
|
7
|
+
const themes = require('@docmd/themes');
|
|
8
|
+
const { findPageNeighbors } = require('@docmd/parser/src/utils/navigation-helper');
|
|
9
|
+
|
|
10
|
+
async function findMarkdownFilesRecursive(dir) {
|
|
11
|
+
let files = [];
|
|
12
|
+
if (!await fs.exists(dir)) return [];
|
|
13
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const fullPath = path.join(dir, item.name);
|
|
16
|
+
if (item.isDirectory()) {
|
|
17
|
+
files = files.concat(await findMarkdownFilesRecursive(fullPath));
|
|
18
|
+
} else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.markdown'))) {
|
|
19
|
+
files.push(fullPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateTag(pathOrUrl, type, attributes = {}) {
|
|
26
|
+
const attrs = Object.entries(attributes).map(([k,v]) => v === true ? k : `${k}="${v}"`).join(' ');
|
|
27
|
+
if (type === 'css') return `<link rel="stylesheet" href="${pathOrUrl}" ${attrs}>`;
|
|
28
|
+
if (type === 'js') return `<script src="${pathOrUrl}" ${attrs}></script>`;
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function buildSite(configPath, options = { isDev: false, offline: false }) {
|
|
33
|
+
const CWD = process.cwd();
|
|
34
|
+
const config = await loadConfig(configPath);
|
|
35
|
+
const hooks = loadPlugins(config);
|
|
36
|
+
const buildHash = Date.now().toString(36);
|
|
37
|
+
|
|
38
|
+
const srcDir = path.resolve(CWD, config.srcDir);
|
|
39
|
+
const outputDir = path.resolve(CWD, config.outputDir);
|
|
40
|
+
|
|
41
|
+
if (!await fs.exists(srcDir)) throw new Error(`Source directory not found: ${srcDir}`);
|
|
42
|
+
await fs.ensureDir(outputDir);
|
|
43
|
+
|
|
44
|
+
// --- 1. ASSET COPYING (Simplified) ---
|
|
45
|
+
|
|
46
|
+
// A. Copy ALL UI Assets (Deep copy)
|
|
47
|
+
// This handles css, js, images, AND favicon.ico in root of assets
|
|
48
|
+
const uiAssets = ui.getAssetsDir();
|
|
49
|
+
if (await fs.exists(uiAssets)) {
|
|
50
|
+
await fs.copy(uiAssets, path.join(outputDir, 'assets'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// B. Copy Themes
|
|
54
|
+
const themesDir = themes.getThemesDir();
|
|
55
|
+
if (await fs.exists(themesDir)) {
|
|
56
|
+
await fs.copy(themesDir, path.join(outputDir, 'assets/css'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// C. Copy User Assets (Override)
|
|
60
|
+
const userAssets = path.resolve(CWD, 'assets');
|
|
61
|
+
if (await fs.exists(userAssets)) await fs.copy(userAssets, path.join(outputDir, 'assets'));
|
|
62
|
+
|
|
63
|
+
// --- 2. GENERATE TAGS ---
|
|
64
|
+
const assetTags = { head: [], body: [] };
|
|
65
|
+
|
|
66
|
+
if (config.theme && config.theme.name && config.theme.name !== 'default') {
|
|
67
|
+
const themeFileName = `docmd-theme-${config.theme.name}.css`;
|
|
68
|
+
if (await fs.exists(path.join(themes.getThemesDir(), themeFileName))) {
|
|
69
|
+
assetTags.head.push(rel => generateTag(`${rel}assets/css/${themeFileName}?v=${buildHash}`, 'css'));
|
|
70
|
+
} else {
|
|
71
|
+
if (!options.isDev) console.warn(`⚠️ Theme not found: ${themeFileName}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Core JS (Keep this, or move to EJS if you want full control, but keeping here is fine)
|
|
76
|
+
assetTags.body.push(rel => generateTag(`${rel}assets/js/docmd-main.js?v=${buildHash}`, 'js'));
|
|
77
|
+
|
|
78
|
+
// Lightbox
|
|
79
|
+
if(await fs.exists(path.join(uiAssets, 'js/docmd-image-lightbox.js'))) {
|
|
80
|
+
assetTags.body.push(rel => generateTag(`${rel}assets/js/docmd-image-lightbox.js?v=${buildHash}`, 'js'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Plugin Assets
|
|
84
|
+
if (hooks.assets) {
|
|
85
|
+
for (const getAssetsFn of hooks.assets) {
|
|
86
|
+
const assets = getAssetsFn();
|
|
87
|
+
if (Array.isArray(assets)) {
|
|
88
|
+
for (const asset of assets) {
|
|
89
|
+
let tagGen;
|
|
90
|
+
if (asset.src && asset.dest) {
|
|
91
|
+
const destPath = path.join(outputDir, asset.dest);
|
|
92
|
+
if (await fs.exists(asset.src)) {
|
|
93
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
94
|
+
await fs.copy(asset.src, destPath);
|
|
95
|
+
}
|
|
96
|
+
tagGen = (rel) => generateTag(`${rel}${asset.dest}?v=${buildHash}`, asset.type, asset.attributes);
|
|
97
|
+
} else if (asset.url) {
|
|
98
|
+
tagGen = () => generateTag(asset.url, asset.type, asset.attributes);
|
|
99
|
+
}
|
|
100
|
+
if (tagGen) assetTags[asset.location === 'head' ? 'head' : 'body'].push(tagGen);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- 3. PROCESSING ---
|
|
107
|
+
const mdProcessor = parser.createMarkdownProcessor(config, (md) => hooks.markdownSetup.forEach(hook => hook(md)));
|
|
108
|
+
const themeInitPath = path.join(ui.getTemplatesDir(), 'partials', 'theme-init.js');
|
|
109
|
+
let themeInitScript = '';
|
|
110
|
+
if (await fs.exists(themeInitPath)) themeInitScript = `<script>${await fs.readFile(themeInitPath, 'utf8')}</script>`;
|
|
111
|
+
let footerHtml = config.footer ? mdProcessor.renderInline(config.footer) : '';
|
|
112
|
+
|
|
113
|
+
const mdFiles = await findMarkdownFilesRecursive(srcDir);
|
|
114
|
+
const pages = [];
|
|
115
|
+
|
|
116
|
+
for (const filePath of mdFiles) {
|
|
117
|
+
const rawContent = await fs.readFile(filePath, 'utf8');
|
|
118
|
+
const processed = parser.processContent(rawContent, mdProcessor, config);
|
|
119
|
+
if (!processed) continue;
|
|
120
|
+
|
|
121
|
+
const relativePath = path.relative(srcDir, filePath);
|
|
122
|
+
const isIndex = path.basename(relativePath).startsWith('index.');
|
|
123
|
+
const htmlOutputPath = isIndex ? path.join(path.dirname(relativePath), 'index.html') : relativePath.replace(/\.md$/, '/index.html');
|
|
124
|
+
pages.push({ ...processed, sourcePath: filePath, outputPath: htmlOutputPath });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- 4. RENDER LOOP ---
|
|
128
|
+
for (const page of pages) {
|
|
129
|
+
// 1. Determine Output Location
|
|
130
|
+
const finalPath = path.join(outputDir, page.outputPath);
|
|
131
|
+
|
|
132
|
+
// 2. Calculate Relative Path to Root (CRITICAL FIX)
|
|
133
|
+
// "content/nested/index.html" -> dir "content/nested" -> relative "../.."
|
|
134
|
+
const fileDir = path.dirname(page.outputPath);
|
|
135
|
+
let relativePathToRoot = path.relative(fileDir, '.');
|
|
136
|
+
|
|
137
|
+
if (relativePathToRoot === '') relativePathToRoot = './';
|
|
138
|
+
else relativePathToRoot += '/';
|
|
139
|
+
relativePathToRoot = relativePathToRoot.replace(/\\/g, '/'); // Windows fix
|
|
140
|
+
|
|
141
|
+
// 3. Normalize Nav Path for Matching
|
|
142
|
+
// We use the "clean" URL path for checking active state
|
|
143
|
+
let navPath = '/' + page.outputPath.replace(/\\/g, '/').replace(/\/index\.html$/, '').replace(/^index\.html$/, '');
|
|
144
|
+
if(navPath === '/.') navPath = '/';
|
|
145
|
+
|
|
146
|
+
// 4. Navigation & Neighbors
|
|
147
|
+
const { prevPage, nextPage } = findPageNeighbors(config.navigation, navPath);
|
|
148
|
+
|
|
149
|
+
// Fix Neighbor Links (Prepend relative root)
|
|
150
|
+
if (prevPage && !prevPage.path.startsWith('http')) {
|
|
151
|
+
let p = prevPage.path.replace(/^\//, ''); // Strip leading slash
|
|
152
|
+
if(options.offline && !p.endsWith('.html')) p = p.replace(/\/$/, '') + '/index.html';
|
|
153
|
+
prevPage.url = relativePathToRoot + p;
|
|
154
|
+
}
|
|
155
|
+
if (nextPage && !nextPage.path.startsWith('http')) {
|
|
156
|
+
let p = nextPage.path.replace(/^\//, '');
|
|
157
|
+
if(options.offline && !p.endsWith('.html')) p = p.replace(/\/$/, '') + '/index.html';
|
|
158
|
+
nextPage.url = relativePathToRoot + p;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 5. Asset Injection (Head/Body)
|
|
162
|
+
const assetHeadHtml = assetTags.head.map(gen => gen(relativePathToRoot)).join('\n');
|
|
163
|
+
const assetBodyHtml = assetTags.body.map(gen => gen(relativePathToRoot)).join('\n');
|
|
164
|
+
|
|
165
|
+
const pageContext = { frontmatter: page.frontmatter, outputPath: page.outputPath };
|
|
166
|
+
|
|
167
|
+
const fullHeadHtml = [
|
|
168
|
+
hooks.injectHead.map(fn => fn(config, pageContext, relativePathToRoot)).join('\n'),
|
|
169
|
+
assetHeadHtml
|
|
170
|
+
].join('\n');
|
|
171
|
+
|
|
172
|
+
const fullBodyHtml = [
|
|
173
|
+
assetBodyHtml,
|
|
174
|
+
hooks.injectBody.map(fn => fn(config, pageContext)).join('\n')
|
|
175
|
+
].join('\n');
|
|
176
|
+
|
|
177
|
+
// 6. Helpers
|
|
178
|
+
let faviconLinkHtml = '';
|
|
179
|
+
if (config.favicon) {
|
|
180
|
+
const cleanFavicon = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
|
|
181
|
+
const finalFavicon = `${relativePathToRoot}${cleanFavicon}?v=${buildHash}`;
|
|
182
|
+
faviconLinkHtml = `<link rel="icon" href="${finalFavicon}" type="image/x-icon" sizes="any">\n<link rel="shortcut icon" href="${finalFavicon}" type="image/x-icon">`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const isActivePage = page.htmlContent && page.htmlContent.trim().length > 0;
|
|
186
|
+
|
|
187
|
+
let editUrl = null, editLinkText = 'Edit this page';
|
|
188
|
+
if (config.editLink && config.editLink.enabled && config.editLink.baseUrl) {
|
|
189
|
+
const cleanBase = config.editLink.baseUrl.replace(/\/$/, '');
|
|
190
|
+
const cleanPath = page.outputPath.replace(/\/index\.html$/, '.md');
|
|
191
|
+
editUrl = `${cleanBase}/${cleanPath}`;
|
|
192
|
+
if (page.outputPath.endsWith('index.html') && page.outputPath !== 'index.html') editUrl = editUrl.replace('.md', '/index.md');
|
|
193
|
+
if (page.outputPath === 'index.html') editUrl = `${cleanBase}/index.md`;
|
|
194
|
+
editLinkText = config.editLink.text || editLinkText;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 7. Render
|
|
198
|
+
const templateName = page.frontmatter.noStyle ? 'no-style' : 'layout';
|
|
199
|
+
const templatePath = ui.getTemplatePath(templateName);
|
|
200
|
+
const templateString = await fs.readFile(templatePath, 'utf8');
|
|
201
|
+
const navTemplateString = await fs.readFile(ui.getTemplatePath('navigation'), 'utf8');
|
|
202
|
+
|
|
203
|
+
const navigationHtml = parser.renderTemplate(navTemplateString, {
|
|
204
|
+
config,
|
|
205
|
+
navItems: config.navigation,
|
|
206
|
+
currentPagePath: navPath,
|
|
207
|
+
relativePathToRoot,
|
|
208
|
+
isOfflineMode: options.offline
|
|
209
|
+
}, { filename: ui.getTemplatePath('navigation') });
|
|
210
|
+
|
|
211
|
+
const fullHtml = parser.renderTemplate(templateString, {
|
|
212
|
+
content: page.htmlContent,
|
|
213
|
+
frontmatter: page.frontmatter,
|
|
214
|
+
headings: page.headings,
|
|
215
|
+
config,
|
|
216
|
+
buildHash,
|
|
217
|
+
siteTitle: config.siteTitle,
|
|
218
|
+
pageTitle: page.frontmatter.title,
|
|
219
|
+
description: page.frontmatter.description || '',
|
|
220
|
+
defaultMode: config.theme?.defaultMode || 'light',
|
|
221
|
+
relativePathToRoot,
|
|
222
|
+
isOfflineMode: options.offline,
|
|
223
|
+
navigationHtml,
|
|
224
|
+
prevPage,
|
|
225
|
+
nextPage,
|
|
226
|
+
logo: config.logo,
|
|
227
|
+
theme: config.theme,
|
|
228
|
+
sidebarConfig: config.sidebar || {},
|
|
229
|
+
footer: config.footer,
|
|
230
|
+
sponsor: config.sponsor,
|
|
231
|
+
customCssFiles: config.theme.customCss || [],
|
|
232
|
+
customJsFiles: config.customJs || [],
|
|
233
|
+
|
|
234
|
+
pluginHeadScriptsHtml: fullHeadHtml,
|
|
235
|
+
pluginBodyScriptsHtml: fullBodyHtml,
|
|
236
|
+
|
|
237
|
+
faviconLinkHtml,
|
|
238
|
+
themeInitScript,
|
|
239
|
+
footerHtml,
|
|
240
|
+
isActivePage,
|
|
241
|
+
editUrl,
|
|
242
|
+
editLinkText,
|
|
243
|
+
themeCssLinkHtml: '',
|
|
244
|
+
metaTagsHtml: '',
|
|
245
|
+
pluginStylesHtml: ''
|
|
246
|
+
}, { filename: templatePath });
|
|
247
|
+
|
|
248
|
+
// 8. Write File
|
|
249
|
+
await fs.ensureDir(path.dirname(finalPath));
|
|
250
|
+
await fs.writeFile(finalPath, fullHtml);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await Promise.all(hooks.onPostBuild.map(fn => fn({
|
|
254
|
+
config,
|
|
255
|
+
pages,
|
|
256
|
+
outputDir,
|
|
257
|
+
log: (msg) => !options.isDev && console.log(msg)
|
|
258
|
+
})));
|
|
259
|
+
if (!options.isDev) console.log(`✅ Build complete. Generated ${pages.length} pages.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = { buildSite };
|