diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a0efb43 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,44 @@ +version: 2 +updates: + # ------------------------------------------------------- + # 1. GitHub Actions (Universal for all your projects) + # Keeps your workflow files (checkout, setup-node) updated + # ------------------------------------------------------- + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: + - "*" + + # ------------------------------------------------------- + # 2. NPM (Specific to tree-fs / Node projects) + # ------------------------------------------------------- + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + # Ignore major updates automatically to prevent breaking changes + # Remove this 'ignore' block if you want to see v1 -> v2 updates + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + groups: + # Group all minor/patch updates into one PR + npm-dependencies: + patterns: + - "*" + + # ------------------------------------------------------- + # 3. (Optional) Uncomment for Python projects + # ------------------------------------------------------- + # - package-ecosystem: "pip" + # directory: "/" + # schedule: + # interval: "weekly" + # groups: + # python-deps: + # patterns: + # - "*" \ No newline at end of file diff --git a/.github/workflows/deploy-docmd.yml b/.github/workflows/deploy-docmd.yml index 1650e01..a92231b 100644 --- a/.github/workflows/deploy-docmd.yml +++ b/.github/workflows/deploy-docmd.yml @@ -2,8 +2,8 @@ name: deploy docmd on: push: - branches: [main] # Your default branch - workflow_dispatch: # Allows manual triggering + branches: [main] + workflow_dispatch: permissions: contents: read @@ -18,25 +18,34 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - name: Checkout πŸ›ŽοΈ - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node.js βš™οΈ - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '22' cache: 'npm' + - name: Update NPM to Latest πŸ†• + run: npm install -g npm@latest + - name: Install Dependencies πŸ“¦ run: npm ci - + - name: Build docmd's Own Docs πŸ› οΈ run: node ./bin/docmd.js build + - name: Build Live Editor ⚑ + run: node scripts/build-live.js + + - name: Inject Live Editor into Site + run: mv dist site/live + - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: ./site diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index ce0791e..c07ba56 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,49 +1,38 @@ -name: Publish Package to NPM and GitHub Packages +name: Publish package to npm on: release: types: [published] workflow_dispatch: +permissions: + contents: read + id-token: write + jobs: publish: runs-on: ubuntu-latest - permissions: - contents: read - packages: write + steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Set up Node.js v22 - uses: actions/setup-node@v4 + - name: Set up Node.js + uses: actions/setup-node@v6 with: node-version: '22' - cache: 'npm' + registry-url: 'https://registry.npmjs.org' + + - name: Ensure recent npm + run: npm install -g npm@latest - name: Install dependencies run: npm ci - # ------------------------------------------ - # --- Publish to NPM Registry (npmjs.com) --- - # ------------------------------------------ - - name: Configure NPM for public registry - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + - name: Run tests (optional) + run: npm test --if-present - - name: Publish to NPM Registry - run: npm publish --access=public + - name: Publish to npm + run: npm publish --access public # env: - # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - # ---------------------------------------- - # --- Publish to GitHub Packages (GPR) --- - # ---------------------------------------- - - name: Configure NPM for GitHub Packages registry - run: | - echo "@mgks:registry=https://npm.pkg.github.com" > ~/.npmrc - echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> ~/.npmrc - - - name: Publish to GitHub Packages - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a2d4e80..3ca3569 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,5 @@ ROADMAP.md .DS_Store genctx.json +genctx.config.json +genctx.context.md diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..7f0b8cb --- /dev/null +++ b/.npmignore @@ -0,0 +1,12 @@ +.git +.github +.DS_Store +.gitignore +.gitattributes +genctx.config.json +genctx.context.md +docs/ +assets/css/welcome.css +assets/images/preview-* +preview.gif +docmd-preview.png \ No newline at end of file diff --git a/README.md b/README.md index 0fcb9d5..7db9802 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,63 @@ -

-
+

- docmd logo + docmd logo -

- -

- The minimalist, zero-config documentation generator for Node.js developers. -
- Turn Markdown into beautiful, blazing-fast websites in seconds. -
-

+

+ The minimalist, zero-config documentation generator. +

+

- npm version - npm downloads - license + npm version +commits + downloads stars + license

View Live Demo β€’ - Documentation β€’ + Read Documentation β€’ + Live Editor β€’ Report Bug

-
-

- 519536477-8d948e18-8e2d-420d-8902-96e1aafab1ba-modified + docmd preview +
docmd noStyle page preview in dark mode

-## πŸš€ Why docmd? - -Most documentation tools today are too heavy (React hydration, massive bundles) or require ecosystems you don't use (Python/Ruby). - -**docmd** fills the gap. It is a native Node.js tool that generates **pure, static HTML**. +## Features -* ⚑ **Blazing Fast:** No hydration delay. Instant page loads. -* πŸ” **Offline Search:** Powerful full-text search included automatically. -* πŸ›  **Zero Config:** Works out of the box with sensible defaults. -* 🎨 **Theming:** Built-in light/dark modes and multiple themes (`sky`, `ruby`, `retro`). -* πŸ“¦ **Node.js Native:** No Python, no Gemfiles. Just `npm install`. -* 🧩 **Rich Content:** Built-in support for Callouts, Cards, Tabs, Steps, and Changelogs. +- **Zero Config**: Works out of the box with sensible defaults. Just `init` and go. +- **Blazing Fast**: Generates **pure, static HTML**. No React hydration lag, no heavy bundles. +- **Smart Search**: Built-in, **offline-capable** full-text search with fuzzy matching. No API keys required. +- **Isomorphic Core**: Runs anywhereβ€”Node.js CLI, CI/CD pipelines, or **directly in the browser** via WASM. +- **Rich Content**: Built-in support for Callouts, Cards, Tabs, Steps, Changelogs, and Mermaid diagrams. +- **Theming**: Beautiful light/dark modes and multiple pre-built themes (`sky`, `ruby`, `retro`). -## 🏁 Quick Start +## Quick Start -You don't need to install anything globally to try it out. +**Installation:** ```bash -# 1. Initialize a new project -npx @mgks/docmd init my-docs +npm install -g @mgks/docmd +``` -# 2. Enter directory -cd my-docs +**Run:** -# 3. Start the dev server -npm start +```bash +docmd init my-docs # Initialize a new project +cd my-docs # Enter directory +docmd dev # Start live-reloading server +docmd build # Generate static site for deployment +docmd live # Launch live editor to preview and design pages ``` -**Dev server output:** +**Dev Server:** -``` +```js _ _ _| |___ ___ _____ _| | @@ -72,91 +67,92 @@ npm start v0.x.x -πŸš€ Performing initial build for dev server... -βœ… Generated sitemap at ./site/sitemap.xml -βœ… Initial build complete. -πŸ‘€ Watching for changes in: - - Source: ./docs - - Config: ./docmd.config.js - - Assets: ./assets - - docmd Templates: ./src/templates (internal) - - docmd Assets: ./src/assets (internal) -πŸŽ‰ Dev server started at http://localhost:3000 -Serving content from: ./site -Live reload is active. Browser will refresh automatically when files change. -``` +πŸš€ Performing initial build... -## ✨ Features - -| Feature | Description | -| :--- | :--- | -| **Markdown First** | Standard Markdown + Frontmatter. No proprietary syntax to learn. | -| **Smart CLI** | Intelligent config validation catches typos before they break your build. | -| **Custom Containers** | Use `::: callout`, `::: card`, `::: steps`, `::: tabs`, `::: collapsible`, `::: changelog`, and more to enrich content. | -| **Smart Search** | Built-in, offline-capable full-text search with fuzzy matching and highlighting. No API keys required. | -| **Diagrams** | Create flowcharts, relationship diagrams, journey, piecharts, graphs, timelines and more with Mermaid. | -| **No-Style Pages** | Create custom landing pages (highly customizable custom HTML pages) without theme constraints. | -| **Auto Dark Mode** | Respects system preference and saves user choice. | -| **Plugins** | SEO, Sitemap, and Analytics support included out-of-the-box. | +πŸ‘€ Watching for changes in: + - Source: ./docs + - Config: ./config.js + - Assets: ./assets -## πŸ†š Comparison +──────────────────────────────────────── + SERVER RUNNING (v0.3.5) -How does `docmd` stack up against the giants? + Local: http://127.0.0.1:3000 + Network: http://192.1.1.1:3000 -| Feature | docmd | Docusaurus | MkDocs (Material) | Mintlify | -| :--- | :--- | :--- | :--- | :--- | -| **Language** | **Node.js** | React.js | Python | Proprietary | -| **Output** | **Static HTML** | React SPA | Static HTML | Hosted / Next.js | -| **JS Payload** | **Tiny (< 15kb)** | Heavy | Minimal | Medium | -| **Setup** | **~2 mins** | ~15 mins | ~10 mins | Instant (SaaS) | -| **Cost** | **100% Free OSS** | 100% Free OSS | 100% Free OSS | Freemium | + Serving: ./site +──────────────────────────────────────── +``` -πŸ‘‰ *[Read the full comparison](https://docmd.mgks.dev/comparison/)* +## Usage in Detail -## πŸ“¦ Installation +### Project Structure -For frequent use, install globally: +`docmd` keeps it simple. Your content lives in `docs/`, your config in `docmd.config.js`. ```bash -npm install -g @mgks/docmd +my-docs/ +β”œβ”€β”€ docs/ # Your Markdown files +β”‚ β”œβ”€β”€ index.md # Homepage +β”‚ └── guide.md # Content page +β”œβ”€β”€ assets/ # Images and custom CSS +└── docmd.config.js # Configuration ``` -### Commands - -* `docmd init` - Create a new documentation project. -* `docmd dev` - Start the live-reloading local server. -* `docmd build` - Generate static files to `site/` for deployment. +### Configuration -## 🎨 Themes - -Switching themes is as easy as changing one line in your `docmd.config.js`. +Customize your site in seconds via `docmd.config.js`: ```javascript module.exports = { + siteTitle: 'My Project', + srcDir: 'docs', + outputDir: 'site', theme: { - name: 'sky', // Options: 'default', 'sky', 'ruby', 'retro' - defaultMode: 'dark' - } + name: 'sky', // 'default', 'sky', 'ruby', 'retro' + defaultMode: 'dark', // 'light' or 'dark' + enableModeToggle: true + }, + navigation: [ + { title: 'Home', path: '/', icon: 'home' }, + { title: 'Guide', path: '/guide', icon: 'book' } + ] } ``` -## 🀝 Contributing +## Live Editor -We welcome contributions! Please see our [Contribution Guidelines](.github/CONTRIBUTING.md) for details. +`docmd` comes with a modular architecture that allows the core engine to run client-side. -1. Fork the repository. -2. Create your feature branch (`git checkout -b feature/AmazingFeature`). -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`). -4. Push to the branch (`git push origin feature/AmazingFeature`). -5. Open a Pull Request. +**Launch locally:** +```bash +docmd live +``` +This builds and serves a local editor where you can write Markdown and see the preview instantly without any server-side processing. + +**Embed in your app:** +You can also use the `dist/docmd-live.js` bundle to add Markdown compilation capabilities to your own web applications. + +## Comparison + +| Feature | docmd | Docusaurus | MkDocs | Mintlify | +| :--- | :--- | :--- | :--- | :--- | +| **Language** | **Node.js** | React.js | Python | Proprietary | +| **Output** | **Static HTML** | React SPA | Static HTML | Hosted | +| **JS Payload** | **Tiny (< 15kb)** | Heavy | Minimal | Medium | +| **Search** | **Built-in (Offline)** | Algolia (Ext) | Built-in | Built-in | +| **Setup** | **~1 min** | ~15 mins | ~10 mins | Instant | +| **Cost** | **Free OSS** | Free OSS | Free OSS | Freemium | -## ❀️ Support +## Community & Support -This project is open source and free to use. If you find it valuable, please consider: +- **Contributing**: We welcome PRs! See [CONTRIBUTING.md](.github/CONTRIBUTING.md). +- **Support**: If you find `docmd` useful, please consider [sponsoring the project](https://github.com/sponsors/mgks) or giving it a star ⭐. -1. ⭐️ **Starring the repo** on GitHub (it helps a lot!) -2. β˜• **[Sponsoring the project](https://github.com/sponsors/mgks)** to support ongoing development. +## License -## πŸ“„ License +Distributed under the MIT License. See `LICENSE` for more information. -Distributed under the MIT License. See `LICENSE` for more information. \ No newline at end of file +> **{ github.com/mgks }** +> +> ![Website Badge](https://img.shields.io/badge/Visit-mgks.dev-blue?style=flat&link=https%3A%2F%2Fmgks.dev) ![Sponsor Badge](https://img.shields.io/badge/%20%20Become%20a%20Sponsor%20%20-red?style=flat&logo=github&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fmgks) \ No newline at end of file diff --git a/bin/docmd.js b/bin/docmd.js index ea58ae4..151b743 100755 --- a/bin/docmd.js +++ b/bin/docmd.js @@ -1,113 +1,75 @@ #!/usr/bin/env node -const { Command } = require('commander'); -const fs = require('fs-extra'); -const path = require('path'); -const { version } = require('../package.json'); -const { initProject } = require('../src/commands/init'); +const { program } = require('commander'); +// This import corresponds to module.exports = { startDevServer } in src/commands/dev.js +const { startDevServer } = require('../src/commands/dev'); const { buildSite } = require('../src/commands/build'); -const { startDevServer } = require('../src/commands/dev'); +const { initProject } = require('../src/commands/init'); +const { version } = require('../package.json'); const { printBanner } = require('../src/core/logger'); - -// Helper function to find the config file -const findConfigFile = () => { - const newConfigPath = 'docmd.config.js'; - const oldConfigPath = 'config.js'; - - if (fs.existsSync(path.resolve(process.cwd(), newConfigPath))) { - return newConfigPath; - } - if (fs.existsSync(path.resolve(process.cwd(), oldConfigPath))) { - return oldConfigPath; - } - - throw new Error('Configuration file not found. Please create a docmd.config.js file or run "docmd init".'); -}; - -const program = new Command(); +const path = require('path'); +const { spawn } = require('child_process'); program .name('docmd') - .description('Generate beautiful, lightweight static documentation sites directly from your Markdown files.') - .version(version); + .description('The minimalist, zero-config documentation generator') + .version(version, '-v, --version', 'Output the current version') + .helpOption('-h, --help', 'Display help for command'); program .command('init') - .description('Initialize a new docmd project (creates docs/ and config file)') - .action(async () => { - try { - await initProject(); - console.log('βœ… docmd project initialized successfully!'); - } catch (error) { - console.error('❌ Error initializing project:', error.message); - process.exit(1); - } + .description('Initialize a new documentation project') + .action(() => { + printBanner(); + initProject(); }); program - .command('build') - .description('Build the static site from Markdown files and config') - .option('-c, --config ', 'Path to config file') - .option('-p, --preserve', 'Preserve existing asset files instead of updating them') - .option('--no-preserve', 'Force update all asset files, overwriting existing ones') - .option('--silent', 'Suppress log output') - .action(async (options) => { - try { - if (!options.silent) { printBanner(); } - - const originalLog = console.log; - if (options.silent) { console.log = () => {}; } - - const configPath = options.config || findConfigFile(); - console.log(`πŸš€ Starting build process using ${configPath}...`); - await buildSite(configPath, { - preserve: options.preserve - }); - - console.log = originalLog; - if (!options.silent) { - console.log('βœ… Build complete! Site generated in `site/` directory.'); - } - - } catch (error) { - console.error('❌ Build failed:', error.message); - // console.error(error.stack); - process.exit(1); - } + .command('dev') + .description('Start the development server with live reload') + .option('-c, --config ', 'Path to configuration file', 'docmd.config.js') + .option('-p, --port ', 'Port to run the server on') + .option('--preserve', 'Preserve existing assets', false) + .action((options) => { + printBanner(); + startDevServer(options.config, options); }); program - .command('dev') - .description('Start a live preview development server') - .option('-c, --config ', 'Path to config file') - .option('--port ', 'Specify a port for the dev server') - .option('-p, --preserve', 'Preserve existing asset files instead of updating them') - .option('--no-preserve', 'Force update all asset files, overwriting existing ones') - .option('--silent', 'Suppress log output') - .action(async (options) => { - try { - if (!options.silent) { printBanner(); } + .command('build') + .description('Build the static documentation site') + .option('-c, --config ', 'Path to configuration file', 'docmd.config.js') + .option('--preserve', 'Preserve existing assets', false) + .action((options) => { + buildSite(options.config, { isDev: false, preserve: options.preserve }); + }) + .option('--offline', 'Generate a build optimized for file:// viewing (appends index.html)', false) + .action((options) => { + buildSite(options.config, { isDev: false, preserve: options.preserve, offline: options.offline }); + }); - if (options.silent) { - const originalLog = console.log; - console.log = (message) => { - if (message && message.includes('Dev server started at')) { - originalLog(message); - } - }; +program + .command('live') + .description('Build and serve the browser-based live editor') + .action(() => { + const scriptPath = path.resolve(__dirname, '../scripts/build-live.js'); + const distPath = path.resolve(__dirname, '../dist'); + + console.log('πŸš€ Starting Live Editor build...'); + + // Using spawn ensures the build runs in a fresh process context + const build = spawn(process.execPath, [scriptPath], { stdio: 'inherit' }); + + build.on('close', (code) => { + if (code === 0) { + console.log('\n🌍 Launching server...'); + console.log(' Press Ctrl+C to stop.\n'); + + // Fix for Node DeprecationWarning regarding shell: true + const serveCmd = `npx serve "${distPath}"`; + spawn(serveCmd, { stdio: 'inherit', shell: true }); } - const configPath = options.config || findConfigFile(); - await startDevServer(configPath, { preserve: options.preserve, port: options.port }); - - } catch (error) { - console.error('❌ Dev server failed:', error.message); - // console.error(error.stack); - process.exit(1); - } + }); }); -program.parse(process.argv); - -if (!process.argv.slice(2).length) { - program.outputHelp(); -} \ No newline at end of file +program.parse(); \ No newline at end of file diff --git a/config.js b/config.js deleted file mode 100644 index 13a4d94..0000000 --- a/config.js +++ /dev/null @@ -1,175 +0,0 @@ -// Source file from the docmd project β€” https://github.com/mgks/docmd - -module.exports = { - // --- Core Metadata --- - siteTitle: 'docmd', - siteUrl: 'https://docmd.mgks.dev', // No trailing slash - - // --- Branding --- - logo: { - light: '/assets/images/docmd-logo-light.png', - dark: '/assets/images/docmd-logo-dark.png', - alt: 'docmd Logo', - href: '/', - }, - favicon: '/assets/favicon.ico', - - // --- Structure --- - srcDir: 'docs', // Source markdown files directory - outputDir: 'site', // Output directory for generated site - - // --- Features & UX --- - search: true, // Built-in offline search - minify: true, // Production build optimization - autoTitleFromH1: true, // Auto-generate title from first H1 if frontmatter title is missing - copyCode: true, // Enable "copy to clipboard" on code blocks - pageNavigation: true, // Next/Prev links - - // --- Sidebar & Theme --- - sidebar: { - collapsible: true, - defaultCollapsed: false, - }, - theme: { - name: 'sky', // 'default', 'sky', 'ruby', 'retro' - defaultMode: 'light', // 'light' or 'dark' - enableModeToggle: true, // Show theme mode toggle button - positionMode: 'top', // 'top' or 'bottom' of header - codeHighlight: true, // Enable code syntax highlighting - customCss: [], // Add paths relative to outputDir here - }, - customJs: [ - '/assets/js/docmd-image-lightbox.js', - ], - - // --- Plugins --- - plugins: { - seo: { - defaultDescription: 'The minimalist, zero-config documentation generator for Node.js developers.', - openGraph: { - defaultImage: '/assets/images/docmd-preview.png', - }, - twitter: { - cardType: 'summary_large_image', - } - }, - analytics: { - googleV4: { - measurementId: 'G-8QVBDQ4KM1' - } - }, - sitemap: { - defaultChangefreq: 'weekly', - defaultPriority: 0.8 - } - }, - - // --- Doc Source Link --- - editLink: { - enabled: true, - baseUrl: 'https://github.com/mgks/docmd/edit/main/docs', - text: 'Edit this page on GitHub' - }, - - // --- Navigation --- - navigation: [ - { title: 'Welcome', path: '/', icon: 'feather' }, - { title: 'Overview', path: '/overview', icon: 'home' }, - - { - title: 'Getting Started', - icon: 'rocket', - path: '/getting-started/', - children: [ - { title: 'Installation', path: '/getting-started/installation', icon: 'download' }, - { title: 'Basic Usage', path: '/getting-started/basic-usage', icon: 'play' }, - ], - }, - - { title: 'Configuration', path: '/configuration', icon: 'settings' }, - - { - title: 'Content', - icon: 'layout-template', - path: '/content/', - collapsible: true, - children: [ - { title: 'Markdown Syntax', path: '/content/markdown-syntax', icon: 'code-2' }, - { title: 'Frontmatter', path: '/content/frontmatter', icon: 'file-text' }, - { title: 'Images & Lightbox', path: '/content/images', icon: 'image' }, - { title: 'Search', path: '/content/search', icon: 'search' }, - { title: 'Mermaid Diagrams', path: '/content/mermaid', icon: 'network' }, - { - title: 'Containers', - path: '/content/containers/', - icon: 'box', - collapsible: true, - children: [ - { title: 'Callouts', path: '/content/containers/callouts', icon: 'megaphone' }, - { title: 'Cards', path: '/content/containers/cards', icon: 'panel-top' }, - { title: 'Steps', path: '/content/containers/steps', icon: 'list-ordered' }, - { title: 'Tabs', path: '/content/containers/tabs', icon: 'columns-3' }, - { title: 'Collapsible', path: '/content/containers/collapsible', icon: 'chevrons-down' }, - { title: 'Changelogs', path: '/content/containers/changelogs', icon: 'history' }, - { title: 'Buttons', path: '/content/containers/buttons', icon: 'mouse-pointer-click' }, - { title: 'Nested Containers', path: '/content/containers/nested-containers', icon: 'folder-tree' }, - ] - }, - { title: 'No-Style Pages', path: '/content/no-style-pages', icon: 'layout' }, - ], - }, - - { - title: 'Theming', - icon: 'palette', - path: '/theming/', - collapsible: true, - children: [ - { title: 'Available Themes', path: '/theming/available-themes', icon: 'layout-grid' }, - { title: 'Light & Dark Mode', path: '/theming/light-dark-mode', icon: 'sun-moon' }, - { title: 'Custom CSS & JS', path: '/theming/custom-css-js', icon: 'file-code' }, - { title: 'Icons', path: '/theming/icons', icon: 'pencil-ruler' }, - ], - }, - - { - title: 'Plugins', - icon: 'puzzle', - path: '/plugins/', - collapsible: true, - children: [ - { title: 'SEO & Meta', path: '/plugins/seo', icon: 'search' }, - { title: 'Analytics', path: '/plugins/analytics', icon: 'bar-chart' }, - { title: 'Sitemap', path: '/plugins/sitemap', icon: 'map' }, - ], - }, - - { - title: 'Recipes', - icon: 'chef-hat', - path: '/recipes/', - collapsible: true, - children: [ - { title: 'Landing Page', path: '/recipes/landing-page', icon: 'layout-template' }, - { title: 'Custom Fonts', path: '/recipes/custom-fonts', icon: 'type' }, - { title: 'Favicon', path: '/recipes/favicon', icon: 'image-plus' }, - ], - }, - - { title: 'CLI Commands', path: '/cli-commands', icon: 'terminal' }, - { title: 'Deployment', path: '/deployment', icon: 'upload-cloud' }, - { title: 'Comparison', path: '/comparison', icon: 'scale' }, - { title: 'Contributing', path: '/contributing', icon: 'git-pull-request' }, - - { title: 'GitHub', path: 'https://github.com/mgks/docmd', icon: 'github', external: true }, - { title: 'Discussions', path: 'https://github.com/mgks/docmd/discussions', icon: 'message-circle', external: true }, - ], - - // --- Footer & Sponsor --- - footer: 'Β© ' + new Date().getFullYear() + ' Project docmd.', - sponsor: { - enabled: true, - title: 'Sponsor', - link: 'https://github.com/sponsors/mgks', - }, -}; \ No newline at end of file diff --git a/docmd.config.js b/docmd.config.js new file mode 100644 index 0000000..2ec5fa5 --- /dev/null +++ b/docmd.config.js @@ -0,0 +1,175 @@ +// Source file from the docmd project β€” https://github.com/mgks/docmd + +module.exports = { + // --- Core Metadata --- + siteTitle: 'docmd', + siteUrl: 'https://docmd.mgks.dev', // No trailing slash + + // --- Branding --- + logo: { + light: 'assets/images/docmd-logo-light.png', + dark: 'assets/images/docmd-logo-dark.png', + alt: 'docmd Logo', + href: './', + }, + favicon: 'assets/favicon.ico', + + // --- Structure --- + srcDir: 'docs', // Source markdown files directory + outputDir: 'site', // Output directory for generated site + + // --- Features & UX --- + search: true, // Built-in offline search + minify: true, // Production build optimization + autoTitleFromH1: true, // Auto-generate title from first H1 if frontmatter title is missing + copyCode: true, // Enable "copy to clipboard" on code blocks + pageNavigation: true, // Next/Prev links + + // --- Sidebar & Theme --- + sidebar: { + collapsible: true, + defaultCollapsed: false, + }, + theme: { + name: 'sky', // 'default', 'sky', 'ruby', 'retro' + defaultMode: 'light', // 'light' or 'dark' + enableModeToggle: true, // Show theme mode toggle button + positionMode: 'top', // 'top' or 'bottom' of header + codeHighlight: true, // Enable code syntax highlighting + customCss: [], // Add paths relative to outputDir here + }, + customJs: [ + 'assets/js/docmd-image-lightbox.js', + ], + + // --- Plugins --- + plugins: { + seo: { + defaultDescription: 'The minimalist, zero-config documentation generator for Node.js developers.', + openGraph: { + defaultImage: 'assets/images/docmd-preview.png', + }, + twitter: { + cardType: 'summary_large_image', + } + }, + analytics: { + googleV4: { + measurementId: 'G-8QVBDQ4KM1' + } + }, + sitemap: { + defaultChangefreq: 'weekly', + defaultPriority: 0.8 + } + }, + + // --- Doc Source Link --- + editLink: { + enabled: true, + baseUrl: 'https://github.com/mgks/docmd/edit/main/docs', + text: 'Edit this page on GitHub' + }, + + // --- Navigation --- + navigation: [ + { title: 'Welcome', path: './', icon: 'feather' }, + { title: 'Overview', path: './overview', icon: 'home' }, + + { + title: 'Getting Started', + icon: 'rocket', + path: './getting-started/', + children: [ + { title: 'Installation', path: './getting-started/installation', icon: 'download' }, + { title: 'Basic Usage', path: './getting-started/basic-usage', icon: 'play' }, + ], + }, + + { title: 'Configuration', path: './configuration', icon: 'settings' }, + { + title: 'Content', + icon: 'layout-template', + path: './content/', + collapsible: true, + children: [ + { title: 'Markdown Syntax', path: './content/markdown-syntax', icon: 'code-2' }, + { title: 'Frontmatter', path: './content/frontmatter', icon: 'file-text' }, + { title: 'Images & Lightbox', path: './content/images', icon: 'image' }, + { title: 'Search', path: './content/search', icon: 'search' }, + { title: 'Mermaid Diagrams', path: './content/mermaid', icon: 'network' }, + { + title: 'Containers', + path: './content/containers/', + icon: 'box', + collapsible: true, + children: [ + { title: 'Callouts', path: './content/containers/callouts', icon: 'megaphone' }, + { title: 'Cards', path: './content/containers/cards', icon: 'panel-top' }, + { title: 'Steps', path: './content/containers/steps', icon: 'list-ordered' }, + { title: 'Tabs', path: './content/containers/tabs', icon: 'columns-3' }, + { title: 'Collapsible', path: './content/containers/collapsible', icon: 'chevrons-down' }, + { title: 'Changelogs', path: './content/containers/changelogs', icon: 'history' }, + { title: 'Buttons', path: './content/containers/buttons', icon: 'mouse-pointer-click' }, + { title: 'Nested Containers', path: './content/containers/nested-containers', icon: 'folder-tree' }, + ] + }, + { title: 'No-Style Pages', path: './content/no-style-pages', icon: 'layout' }, + { title: 'Live Preview', path: './content/live-preview', icon: 'monitor-play' }, + ], + }, + + { + title: 'Theming', + icon: 'palette', + path: './theming/', + collapsible: true, + children: [ + { title: 'Available Themes', path: './theming/available-themes', icon: 'layout-grid' }, + { title: 'Light & Dark Mode', path: './theming/light-dark-mode', icon: 'sun-moon' }, + { title: 'Custom CSS & JS', path: './theming/custom-css-js', icon: 'file-code' }, + { title: 'Icons', path: './theming/icons', icon: 'pencil-ruler' }, + ], + }, + + { + title: 'Plugins', + icon: 'puzzle', + path: './plugins/', + collapsible: true, + children: [ + { title: 'SEO & Meta', path: './plugins/seo', icon: 'search' }, + { title: 'Analytics', path: './plugins/analytics', icon: 'bar-chart' }, + { title: 'Sitemap', path: './plugins/sitemap', icon: 'map' }, + ], + }, + + { + title: 'Recipes', + icon: 'chef-hat', + path: './recipes/', + collapsible: true, + children: [ + { title: 'Landing Page', path: './recipes/landing-page', icon: 'layout-template' }, + { title: 'Custom Fonts', path: './recipes/custom-fonts', icon: 'type' }, + { title: 'Favicon', path: './recipes/favicon', icon: 'image-plus' }, + ], + }, + + { title: 'CLI Commands', path: './cli-commands', icon: 'terminal' }, + { title: 'Deployment', path: './deployment', icon: 'upload-cloud' }, + { title: 'Comparison', path: './comparison', icon: 'scale' }, + { title: 'Contributing', path: './contributing', icon: 'git-pull-request' }, + + { title: 'GitHub', path: 'https://github.com/mgks/docmd', icon: 'github', external: true }, + { title: 'Discussions', path: 'https://github.com/mgks/docmd/discussions', icon: 'message-circle', external: true }, + ], + + // --- Footer & Sponsor --- + footer: 'Β© ' + new Date().getFullYear() + ' Project docmd.', + sponsor: { + enabled: true, + title: 'Sponsor', + link: 'https://github.com/sponsors/mgks', + }, +}; \ No newline at end of file diff --git a/docs/comparison.md b/docs/comparison.md index 0e9c71e..3af7011 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -1,5 +1,5 @@ --- -title: "Comparison between docmd, Docusaurus, MkDocs, Mintlify and more" +title: "Comparison" description: "A detailed comparison of docmd against Docusaurus, MkDocs, Mintlify, and Docsify." --- @@ -13,9 +13,9 @@ Choosing the right tool depends on your specific needs. `docmd` was built to fil | :--- | :--- | :--- | :--- | :--- | :--- | | **Core Tech** | Node.js (Native) | React.js | Python | Proprietary | JS (Runtime) | | **Output** | Static HTML | React SPA (Hydrated) | Static HTML | Hosted / Next.js | None (Runtime SPA) | -| **Setup Time** | ~2 minutes | ~15 minutes | ~10 mins (Python env) | Instant (SaaS) | Instant | +| **Browser Engine** | **Yes (Isomorphic)** | No | No | No | Yes | +| **Setup Time** | ~1 minute | ~15 mins | ~10 mins (Python env) | Instant (SaaS) | Instant | | **Client JS Size** | **Tiny (< 15kb)** | Heavy (React Bundle) | Minimal | Medium | Medium | -| **Customization** | Standard CSS/JS | React Components | Python/Jinja2 | JSON Config | Vue/Plugins | | **Search** | **Built-in (Offline)** | Algolia (Requires Setup) | Built-in (Lunr) | Built-in | Client-side Plugin | | **SEO** | **Excellent** | Excellent | Excellent | Excellent | **Poor** | | **Hosting** | **Anywhere** | Anywhere | Anywhere | **Vendor Locked** | Anywhere | @@ -23,8 +23,13 @@ Choosing the right tool depends on your specific needs. `docmd` was built to fil ## Detailed Breakdown +### The "Live" Advantage +Unlike Docusaurus or MkDocs, which are strictly "Build Tools" that run on your server/computer, `docmd` has a **modular, isomorphic core**. +* **Run it anywhere:** You can run the full `docmd` compilation engine directly in a web browser. +* **Live Previews:** This enables features like the [Live Editor](/live/), allowing you to build CMS interfaces or live preview tools for your users without needing a backend server. + ### The Search Advantage -* **Docusaurus and others** rely on 3rd party services like Algolia and others. This is great for enterprise scale, but for most projects, it's a hassle. You have to apply for an account, manage API keys, and configure crawlers. +* **Docusaurus and others** often rely on 3rd party services like Algolia. This is great for enterprise scale, but for most projects, it's a hassle. You have to apply for an account, manage API keys, and configure crawlers. * **docmd** includes a production-grade search engine out of the box. It generates a local index during the build. This means your documentation is **searchable even offline** (perfect for Intranets or air-gapped networks) and respects user privacy completely. ### vs. Docusaurus @@ -37,15 +42,10 @@ Choosing the right tool depends on your specific needs. `docmd` was built to fil * **Choose MkDocs if:** You are already in the Python ecosystem or need its mature plugin ecosystem immediately. * **Choose docmd if:** You are a JavaScript/Node.js developer. You want to run `npm install` and go, without dealing with `pip`, `requirements.txt`, or Python version conflicts. -### vs. Mintlify -**Mintlify** is a modern, hosted documentation platform focused on design. -* **Choose Mintlify if:** You have a budget and don't want to touch *any* code or configuration. You just want to upload markdown and pay someone to host and style it. -* **Choose docmd if:** You want full control over your HTML/CSS and want to host your docs for **free** on GitHub Pages, Vercel, or Netlify without branding watermarks or custom domain fees. - ### vs. Docsify **Docsify** is a "magical" generator that parses Markdown on the fly in the browser. * **Choose Docsify if:** You absolutely cannot run a build step (e.g., you are hosting on a legacy server that only serves static files and you can't run CI/CD). -* **Choose docmd if:** You care about **SEO** and **Performance**. Docsify requires the user's browser to download the Markdown parser and the content before rendering anything, which is bad for search engines and users on slow connections. `docmd` builds real HTML files that load instantly. +* **Choose docmd if:** You care about **SEO** and **Performance**. Docsify requires the user's browser to download the Markdown parser and the content before rendering anything, which is bad for search engines. `docmd` gives you the best of both worlds: Static HTML for SEO, plus a Browser Engine if you need dynamic previews. ## The docmd Philosophy diff --git a/docs/content/live-preview.md b/docs/content/live-preview.md new file mode 100644 index 0000000..430f4cb --- /dev/null +++ b/docs/content/live-preview.md @@ -0,0 +1,71 @@ +--- +title: "Live Preview" +description: "Run docmd entirely in the browser without a server using the new Live architecture." +--- + +# Live Preview & Browser Support + +::: button Open_Live_Editor /live/ color:#007bff +::: + +Starting with version 0.3.4, `docmd` features a modular architecture that separates file system operations from core processing logic. This allows the documentation engine to run **entirely in the browser** (client-side), opening up possibilities for live editors, CMS previews, and zero-latency feedback loops. + +## The Live Editor + +`docmd` comes with a built-in "Live Editor" that demonstrates this capability. It provides a split-pane interface where you can write Markdown on the left and see the rendered documentation on the rightβ€”instantly, without a server round-trip. + +### Running the Editor Locally + +To launch the live editor on your machine: + +```bash +docmd live +``` + +This command will: +1. Bundle the core logic into `dist/docmd-live.js`. +2. Copy necessary assets (CSS, templates). +3. Start a local static server opening the editor. + +## Embedding docmd in Your Site + +You can use the browser-compatible bundle to add Markdown preview capabilities to your own applications. + +### 1. Include the Script and Assets + +You need to serve the `docmd-live.js` bundle and the `assets/` folder (which contains themes and styles). + +```html + + + +``` + +### 2. Use the API + +The bundle exposes a global `docmd` object. You can use the `compile` function to transform Markdown into a full HTML page string. + +```javascript +const markdown = "# Hello World\n\nThis is **live** documentation."; + +const config = { + siteTitle: 'My Live Doc', + theme: { + name: 'sky', + defaultMode: 'light' + } +}; + +// Compile returns the full HTML string including , , etc. +const html = docmd.compile(markdown, config, { + // Optional: Help the renderer resolve relative paths + relativePathToRoot: './' +}); + +// Inject into an iframe or DOM element +document.getElementById('preview-frame').srcdoc = html; +``` + +::: callout warning Limitation +The Live browser build cannot scan your local hard drive for files. Features that rely on file scanning (like automatically generating the Sidebar Navigation based on folder structure) must be passed explicitly via the `config` object or navigation options. +::: \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 6813a6d..a77bb17 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ seo: programmingLanguage: "Node.js" installUrl: "https://www.npmjs.com/package/@mgks/docmd" customHead: | - + `; @@ -167,148 +104,122 @@ async function startDevServer(configPathOption, options = { preserve: false, por next(); }); - // Add Last-Modified header to all responses for polling fallback - app.use((req, res, next) => { - res.setHeader('Last-Modified', new Date().toUTCString()); - next(); - }); - - // Serve static files from the output directory - // This middleware needs to be dynamic if outputDir changes let staticMiddleware = express.static(paths.outputDir); app.use((req, res, next) => staticMiddleware(req, res, next)); - // Initial build - console.log('πŸš€ Performing initial build for dev server...'); + // --- 1. Initial Build --- + console.log(chalk.blue('πŸš€ Performing initial build...')); try { - await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true }); // Use the original config path option - console.log('βœ… Initial build complete.'); + await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true }); } catch (error) { - console.error('❌ Initial build failed:', error.message, error.stack); - // Optionally, don't start server if initial build fails, or serve a specific error page. + console.error(chalk.red('❌ Initial build failed:'), error.message); } - // Check if user assets directory exists + // --- 2. Setup Watcher & Logs --- const userAssetsDirExists = await fs.pathExists(paths.userAssetsDir); - - // Watch for changes - const watchedPaths = [ - paths.srcDirToWatch, - paths.configFileToWatch, - ]; - - // Add user assets directory to watched paths if it exists - if (userAssetsDirExists) { - watchedPaths.push(paths.userAssetsDir); - } - - // Add internal paths for docmd development (not shown to end users) - const internalPaths = [DOCMD_TEMPLATES_DIR, DOCMD_ASSETS_DIR, DOCMD_COMMANDS_DIR, DOCMD_CORE_DIR, DOCMD_PLUGINS_DIR]; + const watchedPaths = [paths.srcDirToWatch, paths.configFileToWatch]; + if (userAssetsDirExists) watchedPaths.push(paths.userAssetsDir); - // Only in development environments, we might want to watch internal files too if (process.env.DOCMD_DEV === 'true') { - watchedPaths.push(...internalPaths); + watchedPaths.push( + path.join(DOCMD_ROOT, 'templates'), + path.join(DOCMD_ROOT, 'assets'), + path.join(DOCMD_ROOT, 'core'), + path.join(DOCMD_ROOT, 'plugins') + ); } - console.log(`πŸ‘€ Watching for changes in:`); - console.log(` - Source: ${formatPathForDisplay(paths.srcDirToWatch, CWD)}`); - console.log(` - Config: ${formatPathForDisplay(paths.configFileToWatch, CWD)}`); + // LOGS: Explicitly print what we are watching + console.log(chalk.dim('\nπŸ‘€ Watching for changes in:')); + console.log(chalk.dim(` - Source: ${chalk.cyan(formatPathForDisplay(paths.srcDirToWatch, CWD))}`)); + console.log(chalk.dim(` - Config: ${chalk.cyan(formatPathForDisplay(paths.configFileToWatch, CWD))}`)); if (userAssetsDirExists) { - console.log(` - Assets: ${formatPathForDisplay(paths.userAssetsDir, CWD)}`); + console.log(chalk.dim(` - Assets: ${chalk.cyan(formatPathForDisplay(paths.userAssetsDir, CWD))}`)); } if (process.env.DOCMD_DEV === 'true') { - console.log(` - docmd Templates: ${formatPathForDisplay(DOCMD_TEMPLATES_DIR, CWD)} (internal)`); - console.log(` - docmd Assets: ${formatPathForDisplay(DOCMD_ASSETS_DIR, CWD)} (internal)`); + console.log(chalk.dim(` - docmd Internal: ${chalk.magenta(formatPathForDisplay(DOCMD_ROOT, CWD))}`)); } + console.log(''); const watcher = chokidar.watch(watchedPaths, { - ignored: /(^|[\/\\])\../, // ignore dotfiles + ignored: /(^|[\/\\])\../, persistent: true, - ignoreInitial: true, // Don't trigger for initial scan - awaitWriteFinish: { // Helps with rapid saves or large file writes - stabilityThreshold: 100, - pollInterval: 100 - } + ignoreInitial: true, + awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 } }); watcher.on('all', async (event, filePath) => { const relativeFilePath = path.relative(CWD, filePath); - console.log(`πŸ”„ Detected ${event} in ${relativeFilePath}. Rebuilding...`); + process.stdout.write(chalk.dim(`↻ Change in ${relativeFilePath}... `)); + try { if (filePath === paths.configFileToWatch) { - console.log('Config file changed. Reloading configuration...'); - config = await loadConfig(configPathOption); // Reload config + config = await loadConfig(configPathOption); const newPaths = resolveConfigPaths(config); - - // Update watcher if srcDir changed - Chokidar doesn't easily support dynamic path changes after init. - // For simplicity, we might need to restart the watcher or inform user to restart dev server if srcDir/outputDir change. - // For now, we'll at least update the static server path. if (newPaths.outputDir !== paths.outputDir) { - console.log(`Output directory changed from ${formatPathForDisplay(paths.outputDir, CWD)} to ${formatPathForDisplay(newPaths.outputDir, CWD)}. Updating static server.`); staticMiddleware = express.static(newPaths.outputDir); } - // If srcDirToWatch changes, chokidar won't automatically pick it up. - // A full dev server restart would be more robust for such config changes. - // For now, the old srcDir will still be watched. - paths = newPaths; // Update paths for next build reference + paths = newPaths; } - await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true }); // Re-build using the potentially updated config path + await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true }); broadcastReload(); - console.log('βœ… Rebuild complete.'); + process.stdout.write(chalk.green('Done.\n')); + } catch (error) { - console.error('❌ Rebuild failed:', error.message, error.stack); + console.error(chalk.red('\n❌ Rebuild failed:'), error.message); } }); - watcher.on('error', error => console.error(`Watcher error: ${error}`)); - - // Try different ports if the default port is in use const PORT = options.port || process.env.PORT || 3000; const MAX_PORT_ATTEMPTS = 10; - let currentPort = parseInt(PORT, 10); - // Function to try starting the server on different ports function tryStartServer(port, attempt = 1) { - server.listen(port) + // 0.0.0.0 allows network access + server.listen(port, '0.0.0.0') .on('listening', async () => { - // Check if index.html exists after initial build const indexHtmlPath = path.join(paths.outputDir, 'index.html'); + const networkIp = getNetworkIp(); + + // Use 127.0.0.1 explicitly + const localUrl = `http://127.0.0.1:${port}`; + const networkUrl = networkIp ? `http://${networkIp}:${port}` : null; + + const border = chalk.gray('────────────────────────────────────────'); + console.log(border); + console.log(` ${chalk.bold.green('SERVER RUNNING')} ${chalk.dim(`(v${require('../../package.json').version})`)}`); + console.log(''); + console.log(` ${chalk.bold('Local:')} ${chalk.cyan(localUrl)}`); + if (networkUrl) { + console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`); + } + console.log(''); + console.log(` ${chalk.dim('Serving:')} ${formatPathForDisplay(paths.outputDir, CWD)}`); + console.log(border); + console.log(''); + if (!await fs.pathExists(indexHtmlPath)) { - console.warn(`⚠️ Warning: ${formatPathForDisplay(indexHtmlPath, CWD)} not found after initial build. - The dev server is running, but you might see a 404 for the root page. - Ensure your '${config.srcDir}' directory contains an 'index.md' or your navigation points to existing files.`); + console.warn(chalk.yellow(`⚠️ Warning: Root index.html not found.`)); } - console.log(`πŸŽ‰ Dev server started at http://localhost:${port}`); - console.log(`Serving content from: ${formatPathForDisplay(paths.outputDir, CWD)}`); - console.log(`Live reload is active. Browser will refresh automatically when files change.`); }) .on('error', (err) => { if (err.code === 'EADDRINUSE' && attempt < MAX_PORT_ATTEMPTS) { - console.log(`Port ${port} is in use, trying port ${port + 1}...`); - server.close(); tryStartServer(port + 1, attempt + 1); } else { - console.error(`Failed to start server: ${err.message}`); + console.error(chalk.red(`Failed to start server: ${err.message}`)); process.exit(1); } }); } - // Start the server with port fallback - tryStartServer(currentPort); + tryStartServer(parseInt(PORT, 10)); - // Graceful shutdown process.on('SIGINT', () => { - console.log('\nπŸ›‘ Shutting down dev server...'); + console.log(chalk.yellow('\nπŸ›‘ Shutting down...')); watcher.close(); - wss.close(() => { - server.close(() => { - console.log('Server closed.'); - process.exit(0); - }); - }); + process.exit(0); }); } +// Ensure this export is here! module.exports = { startDevServer }; \ No newline at end of file diff --git a/src/commands/init.js b/src/commands/init.js index 07f2ef8..92185ae 100644 --- a/src/commands/init.js +++ b/src/commands/init.js @@ -14,10 +14,10 @@ module.exports = { // Logo Configuration logo: { - light: '/assets/images/docmd-logo-light.png', // Path relative to outputDir root - dark: '/assets/images/docmd-logo-dark.png', // Path relative to outputDir root + light: 'assets/images/docmd-logo-light.png', // Path relative to outputDir root + dark: 'assets/images/docmd-logo-dark.png', // Path relative to outputDir root alt: 'docmd logo', // Alt text for the logo - href: '/', // Link for the logo, defaults to site root + href: './', // Link for the logo, defaults to site root }, // Directory Configuration @@ -44,14 +44,14 @@ module.exports = { positionMode: 'top', // 'top' or 'bottom' for the theme toggle codeHighlight: true, // Enable/disable codeblock highlighting and import of highlight.js customCss: [ // Array of paths to custom CSS files - // '/assets/css/custom.css', // Custom TOC styles + // 'assets/css/custom.css', // Custom TOC styles ] }, // Custom JavaScript Files customJs: [ // Array of paths to custom JS files, loaded at end of body - // '/assets/js/custom-script.js', // Paths relative to outputDir root - '/assets/js/docmd-image-lightbox.js', // Image lightbox functionality + // 'assets/js/custom-script.js', // Paths relative to outputDir root + 'assets/js/docmd-image-lightbox.js', // Image lightbox functionality ], // Content Processing @@ -71,7 +71,7 @@ module.exports = { // siteName: 'docmd Documentation', // Optional, defaults to config.siteTitle // Default image for og:image if not specified in page frontmatter // Path relative to outputDir root - defaultImage: '/assets/images/docmd-preview.png', + defaultImage: 'assets/images/docmd-preview.png', }, twitter: { // For Twitter Cards cardType: 'summary_large_image', // 'summary', 'summary_large_image' @@ -139,7 +139,7 @@ module.exports = { // Favicon Configuration // Path relative to outputDir root - favicon: '/assets/favicon.ico', + favicon: 'assets/favicon.ico', }; `; diff --git a/src/core/config-loader.js b/src/core/config-loader.js index 6dc94dd..9ed5b33 100644 --- a/src/core/config-loader.js +++ b/src/core/config-loader.js @@ -5,12 +5,32 @@ const fs = require('fs-extra'); const { validateConfig } = require('./config-validator'); async function loadConfig(configPath) { - const absoluteConfigPath = path.resolve(process.cwd(), configPath); + const cwd = process.cwd(); + let absoluteConfigPath = path.resolve(cwd, configPath); + + // 1. Check if the requested config file exists if (!await fs.pathExists(absoluteConfigPath)) { - throw new Error(`Configuration file not found at: ${absoluteConfigPath}\nRun "docmd init" to create one.`); + // 2. Fallback Logic: + // If the user didn't specify a custom path (i.e., using default 'docmd.config.js') + // AND 'docmd.config.js' is missing... + // Check if legacy 'config.js' exists. + if (configPath === 'docmd.config.js') { + const legacyPath = path.resolve(cwd, 'config.js'); + if (await fs.pathExists(legacyPath)) { + // console.log('⚠️ Using legacy config.js. Please rename to docmd.config.js'); // Optional warning + absoluteConfigPath = legacyPath; + } else { + // Neither exists + throw new Error(`Configuration file not found at: ${absoluteConfigPath}\nRun "docmd init" to create one.`); + } + } else { + // User specified a custom path that doesn't exist + throw new Error(`Configuration file not found at: ${absoluteConfigPath}`); + } } + try { - // Clear require cache to always get the freshest config + // Clear require cache to always get the freshest config (important for dev mode reloading) delete require.cache[require.resolve(absoluteConfigPath)]; const config = require(absoluteConfigPath); diff --git a/src/core/file-processor.js b/src/core/file-processor.js index 39c22af..8c0a2af 100644 --- a/src/core/file-processor.js +++ b/src/core/file-processor.js @@ -34,12 +34,17 @@ function formatPathForDisplay(absolutePath) { async function processMarkdownFile(filePath, md, config) { const rawContent = await fs.readFile(filePath, 'utf8'); + return processMarkdownContent(rawContent, md, config, filePath); +} + +// Pure logic, no file reading (Used by Live Editor) +function processMarkdownContent(rawContent, md, config, filePath = 'memory') { let frontmatter, markdownContent; try { ({ data: frontmatter, content: markdownContent } = matter(rawContent)); } catch (e) { - console.error(`❌ Error parsing frontmatter in ${formatPathForDisplay(filePath)}:`); + console.error(`❌ Error parsing frontmatter in ${filePath === 'memory' ? 'content' : formatPathForDisplay(filePath)}:`); console.error(` ${e.message}`); return null; } @@ -70,7 +75,7 @@ async function processMarkdownFile(filePath, md, config) { return { frontmatter, htmlContent, headings, searchData }; } - + async function findMarkdownFiles(dir) { let files = []; const items = await fs.readdir(dir, { withFileTypes: true }); @@ -87,6 +92,7 @@ async function findMarkdownFiles(dir) { module.exports = { processMarkdownFile, + processMarkdownContent, createMarkdownItInstance, extractHeadingsFromHtml, findMarkdownFiles diff --git a/src/core/html-generator.js b/src/core/html-generator.js index 057e512..ca7e44c 100644 --- a/src/core/html-generator.js +++ b/src/core/html-generator.js @@ -8,18 +8,63 @@ const { generateSeoMetaTags } = require('../plugins/seo'); const { generateAnalyticsScripts } = require('../plugins/analytics'); const { renderIcon } = require('./icon-renderer'); -// Create a markdown instance for inline rendering let mdInstance = null; - let themeInitScript = ''; + (async () => { - const themeInitPath = path.join(__dirname, '..', 'templates', 'partials', 'theme-init.js'); - if (await fs.pathExists(themeInitPath)) { - const scriptContent = await fs.readFile(themeInitPath, 'utf8'); - themeInitScript = ``; + if (typeof __dirname !== 'undefined') { + const themeInitPath = path.join(__dirname, '..', 'templates', 'partials', 'theme-init.js'); + if (await fs.pathExists(themeInitPath)) { + const scriptContent = await fs.readFile(themeInitPath, 'utf8'); + themeInitScript = ``; + } } })(); +// Helper to handle link rewriting based on build mode +function fixHtmlLinks(htmlContent, relativePathToRoot, isOfflineMode) { + if (!htmlContent) return ''; + const root = relativePathToRoot || './'; + + // Regex matches hrefs starting with /, ./, or ../ + return htmlContent.replace(/href="((?:\/|\.\/|\.\.\/)[^"]*)"/g, (match, href) => { + let finalPath = href; + + // 1. Convert Absolute to Relative + if (href.startsWith('/')) { + finalPath = root + href.substring(1); + } + + // 2. Logic based on Mode + if (isOfflineMode) { + // Offline Mode: Force index.html for directories + const cleanPath = finalPath.split('#')[0].split('?')[0]; + // If it has no extension (like .html, .css, .png), treat as directory + if (!path.extname(cleanPath)) { + if (finalPath.includes('#')) { + // Handle anchors: ./foo/#bar -> ./foo/index.html#bar + const parts = finalPath.split('#'); + const prefix = parts[0].endsWith('/') ? parts[0] : parts[0] + '/'; + finalPath = prefix + 'index.html#' + parts[1]; + } else { + if (finalPath.endsWith('/')) { + finalPath += 'index.html'; + } else { + finalPath += '/index.html'; + } + } + } + } else { + // Online/Dev Mode: Strip index.html for clean URLs + if (finalPath.endsWith('/index.html')) { + finalPath = finalPath.substring(0, finalPath.length - 10); + } + } + + return `href="${finalPath}"`; + }); +} + async function processPluginHooks(config, pageData, relativePathToRoot) { let metaTagsHtml = ''; let faviconLinkHtml = ''; @@ -28,63 +73,52 @@ async function processPluginHooks(config, pageData, relativePathToRoot) { let pluginHeadScriptsHtml = ''; let pluginBodyScriptsHtml = ''; - // Favicon (built-in handling) + const safeRoot = relativePathToRoot || './'; + + // Favicon if (config.favicon) { - const faviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon; - faviconLinkHtml = `\n`; + const cleanFaviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon; + const finalFaviconHref = `${safeRoot}${cleanFaviconPath}`; + + faviconLinkHtml = ` \n`; + faviconLinkHtml += ` \n`; } - // Theme CSS (built-in handling for theme.name) if (config.theme && config.theme.name && config.theme.name !== 'default') { const themeCssPath = `assets/css/docmd-theme-${config.theme.name}.css`; - themeCssLinkHtml = ` \n`; + themeCssLinkHtml = ` \n`; } - // SEO Plugin (if configured) if (config.plugins?.seo) { - metaTagsHtml += generateSeoMetaTags(config, pageData, relativePathToRoot); + metaTagsHtml += generateSeoMetaTags(config, pageData, safeRoot); } - // Analytics Plugin (if configured) if (config.plugins?.analytics) { const analyticsScripts = generateAnalyticsScripts(config, pageData); pluginHeadScriptsHtml += analyticsScripts.headScriptsHtml; pluginBodyScriptsHtml += analyticsScripts.bodyScriptsHtml; } - return { - metaTagsHtml, - faviconLinkHtml, - themeCssLinkHtml, - pluginStylesHtml, - pluginHeadScriptsHtml, - pluginBodyScriptsHtml, - }; + return { metaTagsHtml, faviconLinkHtml, themeCssLinkHtml, pluginStylesHtml, pluginHeadScriptsHtml, pluginBodyScriptsHtml }; } -async function generateHtmlPage(templateData) { - const { - content, siteTitle, navigationHtml, - relativePathToRoot, config, frontmatter, outputPath, - prevPage, nextPage, currentPagePath, headings - } = templateData; - +async function generateHtmlPage(templateData, isOfflineMode = false) { + let { content, siteTitle, navigationHtml, relativePathToRoot, config, frontmatter, outputPath, prevPage, nextPage, currentPagePath, headings } = templateData; const pageTitle = frontmatter.title; - // Process plugins to get their HTML contributions - const pluginOutputs = await processPluginHooks( - config, - { frontmatter, outputPath }, - relativePathToRoot - ); + if (!relativePathToRoot) relativePathToRoot = './'; + + // Fix Content Links based on mode + content = fixHtmlLinks(content, relativePathToRoot, isOfflineMode); + + const pluginOutputs = await processPluginHooks(config, { frontmatter, outputPath }, relativePathToRoot); let footerHtml = ''; if (config.footer) { - // Initialize mdInstance if not already done - if (!mdInstance) { - mdInstance = createMarkdownItInstance(config); - } + if (!mdInstance) mdInstance = createMarkdownItInstance(config); footerHtml = mdInstance.renderInline(config.footer); + // Fix Footer Links based on mode + footerHtml = fixHtmlLinks(footerHtml, relativePathToRoot, isOfflineMode); } let templateName = 'layout.ejs'; @@ -100,89 +134,60 @@ async function generateHtmlPage(templateData) { const isActivePage = currentPagePath && content && content.trim().length > 0; - // Calculate Edit Link let editUrl = null; let editLinkText = 'Edit this page'; - if (config.editLink && config.editLink.enabled && config.editLink.baseUrl) { - // Normalize URL (remove trailing slash) - const baseUrl = config.editLink.baseUrl.replace(/\/$/, ''); - - // Get the source file path relative to srcDir - let relativeSourcePath = outputPath - .replace(/\/index\.html$/, '.md') // folder/index.html -> folder.md - .replace(/\\/g, '/'); // fix windows slashes - - // Special case: The root index.html comes from index.md - if (relativeSourcePath === 'index.html') relativeSourcePath = 'index.md'; - - // Let's assume a standard 1:1 mapping for v0.2.x - editUrl = `${baseUrl}/${relativeSourcePath}`; - editLinkText = config.editLink.text || editLinkText; + editUrl = `${config.editLink.baseUrl.replace(/\/$/, '')}/${outputPath.replace(/\/index\.html$/, '.md').replace(/\\/g, '/')}`; + if (outputPath.endsWith('index.html') && outputPath !== 'index.html') editUrl = editUrl.replace('.md', '/index.md'); + if (outputPath === 'index.html') editUrl = `${config.editLink.baseUrl.replace(/\/$/, '')}/index.md`; + editLinkText = config.editLink.text || editLinkText; } const ejsData = { - content, - pageTitle, - themeInitScript, - description: frontmatter.description, - siteTitle, - navigationHtml, - editUrl, - editLinkText, - defaultMode: config.theme?.defaultMode || 'light', - relativePathToRoot, - logo: config.logo, - sidebarConfig: { - collapsible: config.sidebar?.collapsible ?? false, - defaultCollapsed: config.sidebar?.defaultCollapsed ?? false, - }, - theme: config.theme, - customCssFiles: config.theme?.customCss || [], - customJsFiles: config.customJs || [], - sponsor: config.sponsor, - footer: config.footer, - footerHtml, - renderIcon, - prevPage, - nextPage, - currentPagePath, - headings: frontmatter.toc !== false ? (headings || []) : [], - isActivePage, - frontmatter, - config: config, - ...pluginOutputs, + content, pageTitle, themeInitScript, description: frontmatter.description, siteTitle, navigationHtml, + editUrl, editLinkText, defaultMode: config.theme?.defaultMode || 'light', relativePathToRoot, + logo: config.logo, sidebarConfig: config.sidebar || {}, theme: config.theme, + customCssFiles: config.theme?.customCss || [], customJsFiles: config.customJs || [], + sponsor: config.sponsor, footer: config.footer, footerHtml, renderIcon, + prevPage, nextPage, currentPagePath, headings: frontmatter.toc !== false ? (headings || []) : [], + isActivePage, frontmatter, config, ...pluginOutputs, + isOfflineMode }; + return renderHtmlPage(layoutTemplate, ejsData, layoutTemplatePath); +} + +function renderHtmlPage(templateContent, ejsData, filename = 'template.ejs', options = {}) { try { - return ejs.render(layoutTemplate, ejsData, { - filename: layoutTemplatePath + return ejs.render(templateContent, ejsData, { + filename: filename, + ...options }); } catch (e) { - console.error(`❌ Error rendering EJS template for ${outputPath}: ${e.message}`); - console.error("EJS Data:", JSON.stringify(ejsData, null, 2).substring(0, 1000) + "..."); + console.error(`❌ Error rendering EJS template: ${e.message}`); throw e; } } -async function generateNavigationHtml(navItems, currentPagePath, relativePathToRoot, config) { +// FIX: Added isOfflineMode parameter +async function generateNavigationHtml(navItems, currentPagePath, relativePathToRoot, config, isOfflineMode = false) { const navTemplatePath = path.join(__dirname, '..', 'templates', 'navigation.ejs'); if (!await fs.pathExists(navTemplatePath)) { throw new Error(`Navigation template not found: ${navTemplatePath}`); } const navTemplate = await fs.readFile(navTemplatePath, 'utf8'); - const ejsHelpers = { renderIcon }; - - return ejs.render(navTemplate, { - navItems, - currentPagePath, - relativePathToRoot, - config, - ...ejsHelpers - }, { - filename: navTemplatePath - }); + + const safeRoot = relativePathToRoot || './'; + + return ejs.render(navTemplate, { + navItems, + currentPagePath, + relativePathToRoot: safeRoot, + config, + isOfflineMode, // <--- Passing the variable here + ...ejsHelpers + }, { filename: navTemplatePath }); } -module.exports = { generateHtmlPage, generateNavigationHtml }; \ No newline at end of file +module.exports = { generateHtmlPage, generateNavigationHtml, renderHtmlPage }; \ No newline at end of file diff --git a/src/live/core.js b/src/live/core.js new file mode 100644 index 0000000..aa4e487 --- /dev/null +++ b/src/live/core.js @@ -0,0 +1,63 @@ +const { processMarkdownContent, createMarkdownItInstance } = require('../core/file-processor'); +const { renderHtmlPage } = require('../core/html-generator'); +const templates = require('./templates'); + +function compile(markdown, config = {}, options = {}) { + // Default config values for the browser + const defaults = { + siteTitle: 'Live Preview', + theme: { defaultMode: 'light', name: 'default' }, + ...config + }; + + const md = createMarkdownItInstance(defaults); + const result = processMarkdownContent(markdown, md, defaults, 'memory'); + + if (!result) return '

Error parsing markdown

'; + + const { frontmatter, htmlContent, headings } = result; + + const pageData = { + content: htmlContent, + frontmatter, + headings, + siteTitle: defaults.siteTitle, + pageTitle: frontmatter.title || 'Untitled', + description: frontmatter.description || '', + defaultMode: defaults.theme.defaultMode, + editUrl: null, + editLinkText: '', + navigationHtml: '', // Navigation is usually empty in a single-page preview + relativePathToRoot: options.relativePathToRoot || './', // Important for finding CSS in dist/assets + outputPath: 'index.html', + currentPagePath: '/index', + prevPage: null, nextPage: null, + config: defaults, + // Empty hooks + metaTagsHtml: '', faviconLinkHtml: '', themeCssLinkHtml: '', + pluginStylesHtml: '', pluginHeadScriptsHtml: '', pluginBodyScriptsHtml: '', + themeInitScript: '', + logo: defaults.logo, sidebarConfig: { collapsible: false }, theme: defaults.theme, + customCssFiles: [], customJsFiles: [], + sponsor: { enabled: false }, footer: '', footerHtml: '', + renderIcon: () => '', // Icons disabled in live preview to save weight + isActivePage: true + }; + + let templateName = frontmatter.noStyle === true ? 'no-style.ejs' : 'layout.ejs'; + const templateContent = templates[templateName]; + + if (!templateContent) return `Template ${templateName} not found`; + + const ejsOptions = { + includer: (originalPath) => { + let name = originalPath.endsWith('.ejs') ? originalPath : originalPath + '.ejs'; + if (templates[name]) return { template: templates[name] }; + return null; + } + }; + + return renderHtmlPage(templateContent, pageData, templateName, ejsOptions); +} + +module.exports = { compile }; \ No newline at end of file diff --git a/src/live/index.html b/src/live/index.html new file mode 100644 index 0000000..bf1aeef --- /dev/null +++ b/src/live/index.html @@ -0,0 +1,201 @@ + + + + + + Docmd Live + + + + + + +
+ + +
+ + +
+ + + +
+ + +
+ +
+
Markdown
+ +
+ + +
+ + +
+
Preview
+ +
+
+ + +
+ + +
+ + + + \ No newline at end of file diff --git a/src/live/live.css b/src/live/live.css new file mode 100644 index 0000000..3197ca0 --- /dev/null +++ b/src/live/live.css @@ -0,0 +1,167 @@ +:root { + --header-height: 50px; + --border-color: #e0e0e0; + --bg-color: #f9fafb; + --primary-color: #007bff; + --resizer-width: 8px; +} + +body { + margin: 0; + height: 100vh; + display: flex; + flex-direction: column; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + overflow: hidden; +} + +/* --- Top Bar --- */ +.top-bar { + height: var(--header-height); + background: #fff; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + flex-shrink: 0; +} + +.logo { + font-weight: 700; + font-size: 1.1rem; + display: flex; + align-items: center; + gap: 12px; +} + +.logo span { + background: var(--primary-color); + color: white; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.75rem; + text-transform: uppercase; +} + +.back-link { + display: flex; + align-items: center; + justify-content: center; + color: #666; + transition: color 0.2s, transform 0.2s; + text-decoration: none; + padding: 4px; + border-radius: 4px; +} + +.back-link:hover { + color: var(--primary-color); + background: #f0f0f0; + transform: translateX(-2px); +} + +.view-controls { display: flex; gap: 8px; background: #f0f0f0; padding: 3px; border-radius: 6px; } +.view-btn { border: none; background: transparent; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 0.85rem; color: #666; font-weight: 500; } +.view-btn.active { background: white; color: black; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } + +/* --- Main Layout --- */ +.workspace { + flex: 1; + display: flex; + position: relative; + overflow: hidden; +} + +.pane { + height: 100%; + display: flex; + flex-direction: column; + min-width: 300px; /* Minimum width constraint */ + background: white; +} + +.pane-header { + padding: 8px 16px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: #888; + background: var(--bg-color); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; +} + +/* Editor Pane */ +.editor-pane { width: 50%; } +textarea#input { + flex: 1; + border: none; + resize: none; + padding: 20px; + font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; + font-size: 14px; + line-height: 1.6; + outline: none; + background: var(--bg-color); +} + +/* Preview Pane */ +.preview-pane { flex: 1; background: white; } +iframe#preview { width: 100%; height: 100%; border: none; display: block; } + +/* --- Resizer Handle --- */ +.resizer { + width: var(--resizer-width); + background: var(--bg-color); + border-left: 1px solid var(--border-color); + border-right: 1px solid var(--border-color); + cursor: col-resize; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + z-index: 10; +} +.resizer:hover, .resizer.resizing { background: #e0e0e0; } +.resizer::after { content: "||"; color: #aaa; font-size: 10px; letter-spacing: 1px; } + +/* --- View Modes (Single vs Split) --- */ + +/* Single Mode (Mobile style on Desktop) */ +body.mode-single .resizer { display: none; } +body.mode-single .pane { width: 100% !important; min-width: 0; } +body.mode-single .editor-pane { display: none; } +body.mode-single .preview-pane { display: none; } +body.mode-single.show-editor .editor-pane { display: flex; } +body.mode-single.show-preview .preview-pane { display: flex; } + +/* --- Mobile Responsive Overrides --- */ +@media (max-width: 768px) { + /* Force single mode on mobile, hide desktop view controls */ + .desktop-only { display: none !important; } + .resizer { display: none !important; } + + .pane { width: 100% !important; } + .editor-pane { display: none; } + .preview-pane { display: none; } + + body.mobile-tab-editor .editor-pane { display: flex; } + body.mobile-tab-preview .preview-pane { display: flex; } + + .mobile-tabs { + display: flex !important; + position: fixed; + bottom: 0; left: 0; right: 0; + height: 50px; + background: white; + border-top: 1px solid var(--border-color); + z-index: 100; + } + .mobile-tab-btn { flex: 1; border: none; background: transparent; font-weight: 600; color: #888; cursor: pointer; } + .mobile-tab-btn.active { color: var(--primary-color); border-top: 2px solid var(--primary-color); } + + .workspace { padding-bottom: 50px; } /* Space for tabs */ +} + +.mobile-tabs { display: none; } /* Hidden on desktop */ \ No newline at end of file diff --git a/src/live/shims.js b/src/live/shims.js new file mode 100644 index 0000000..a44475c --- /dev/null +++ b/src/live/shims.js @@ -0,0 +1 @@ +import { Buffer } from 'buffer'; globalThis.Buffer = Buffer; \ No newline at end of file diff --git a/src/live/templates.js b/src/live/templates.js new file mode 100644 index 0000000..717fb33 --- /dev/null +++ b/src/live/templates.js @@ -0,0 +1,9 @@ + +const templates = { + "layout.ejs": "\n\n\n \n \n\n <%- metaTagsHtml || '' %> <%# SEO Plugin Meta Tags %>\n\n <%= pageTitle %> : <%= siteTitle %>\n <% if (description && !(metaTagsHtml && metaTagsHtml.includes('name=\"description\"'))) { %>\n \">\n <% } %>\n\n <%- faviconLinkHtml || '' %> <%# Favicon %>\n\n \n \n\n \n <% if (config.theme?.codeHighlight !== false) { %>\n assets/css/docmd-highlight-<%= defaultMode === 'dark' ? 'dark' : 'light' %>.css\" data-base-href=\"<%= relativePathToRoot %>assets/css/\">\n <% } %>\n\n \n assets/css/docmd-main.css\">\n \n <%- themeCssLinkHtml || '' %> <%# For theme.name specific CSS %>\n\n <% (customCssFiles || []).forEach(cssFile => { %>\n <%- cssFile.startsWith('/') ? cssFile.substring(1) : cssFile %>\">\n <% }); %>\n\n <%- pluginStylesHtml || '' %> <%# Plugin specific CSS %>\n\n \n <%- themeInitScript %>\n\n <%- pluginHeadScriptsHtml || '' %> <%# Plugin specific head scripts %>\n\n\"\n data-default-collapsed=\"<%= sidebarConfig.defaultCollapsed %>\"\n data-copy-code-enabled=\"<%= config.copyCode === true %>\">\n \n
\n
\n
\n <% if (sidebarConfig.collapsible) { %>\n \n <% } %>\n

<%= pageTitle %>

\n
\n <% if (theme && theme.enableModeToggle && theme.positionMode === 'top') { %>\n
\n <% if (config.search !== false) { %>\n \n <% } %>\n \n
\n <% } %>\n
\n
\n
\n
\n <%- content %>\n \n <% if (config.pageNavigation && (prevPage || nextPage)) { %>\n \n <% } %>\n
\n \n \n \n \n
\n <%- include('toc', { content, headings, navigationHtml, isActivePage }) %>\n
\n
\n\n \n \n
\n\n
\n
\n
\n <%- footerHtml || '' %>\n
\n
\n Build with docmd.\n
\n
\n
\n
\n\n <% if (config.search !== false) { %>\n \n
\n
\n
\n \n \n \n
\n
\n \n
\n
\n ↑ ↓ to navigate\n ESC to close\n
\n
\n
\n <% } %>\n\n \n\n \n\n <% if (config.search !== false) { %>\n \n \n \n <% } %>\n\n \n \n \n\n <% (customJsFiles || []).forEach(jsFile => { %>\n \n <% }); %>\n\n <%- pluginBodyScriptsHtml || '' %>\n \n <% if (sponsor && sponsor.enabled) { %>\n \n <% } %>\n\n", + "navigation.ejs": "<%# navigation.ejs - Renders the sidebar navigation %>\n", + "no-style.ejs": "\n\n\n \n \n\n <% if (frontmatter.components?.meta !== false) { %>\n <%- metaTagsHtml || '' %>\n <%= pageTitle %><% if (frontmatter.components?.siteTitle !== false) { %> | <%= siteTitle %><% } %>\n <% if (description && !(metaTagsHtml && metaTagsHtml.includes('name=\"description\"'))) { %>\n \">\n <% } %>\n <% } %>\n\n <% if (frontmatter.components?.favicon !== false) { %>\n <%- faviconLinkHtml || '' %>\n <% } %>\n\n <% if (frontmatter.components?.themeMode !== false) { %>\n \n <% } %>\n\n <% if (frontmatter.components?.css !== false) { %>\n assets/css/docmd-main.css\">\n <% if (frontmatter.components?.highlight !== false) { %>\n assets/css/docmd-highlight-<%= defaultMode === 'dark' ? 'dark' : 'light' %>.css\" id=\"highlight-theme\">\n <% } %>\n <% } %>\n\n <% if (frontmatter.components?.themeMode !== false) { %>\n <%- themeInitScript %>\n <% } %>\n\n <% if (frontmatter.components?.theme !== false) { %>\n <%- themeCssLinkHtml || '' %>\n <% } %>\n\n <% if (frontmatter.components?.customCss !== false && customCssFiles && customCssFiles.length > 0) { %>\n <% customCssFiles.forEach(cssFile => { %>\n <%- cssFile.startsWith('/') ? cssFile.substring(1) : cssFile %>\">\n <% }); %>\n <% } %>\n\n <% if (frontmatter.components?.pluginStyles !== false) { %>\n <%- pluginStylesHtml || '' %>\n <% } %>\n\n <% if (frontmatter.components?.pluginHeadScripts !== false) { %>\n <%- pluginHeadScriptsHtml || '' %>\n <% } %>\n \n <% if (frontmatter.customHead) { %>\n <%- frontmatter.customHead %>\n <% } %>\n\n data-theme=\"<%= defaultMode %>\"<%\n }\n %><%\n if (frontmatter.bodyClass) {\n %> class=\"<%= frontmatter.bodyClass %>\"<%\n }\n %> data-copy-code-enabled=\"<%= config.copyCode === true %>\">\n <% if (frontmatter.components?.layout === true || frontmatter.components?.layout === 'full') { %>\n
\n <% if (frontmatter.components?.header !== false) { %>\n
\n <% if (frontmatter.components?.pageTitle !== false) { %>\n

<%= pageTitle %>

\n <% } %>\n
\n <% } %>\n
\n
\n
\n <%- content %>\n
\n
\n
\n <% if (frontmatter.components?.footer !== false) { %>\n
\n
\n
\n <%- footerHtml || '' %>\n
\n <% if (frontmatter.components?.branding !== false) { %>\n
\n Build with docmd.\n
\n <% } %>\n
\n
\n <% } %>\n
\n <% } else if (frontmatter.components?.sidebar === true) { %>\n \n
\n <% if (frontmatter.components?.header !== false) { %>\n
\n <% if (frontmatter.components?.pageTitle !== false) { %>\n

<%= pageTitle %>

\n <% } %>\n
\n <% } %>\n
\n
\n
\n <%- content %>\n
\n <% if (frontmatter.components?.toc !== false && headings && headings.length > 0) { %>\n
\n <%- include('toc', { content, headings, navigationHtml, isActivePage }) %>\n
\n <% } %>\n
\n
\n <% if (frontmatter.components?.footer !== false) { %>\n
\n
\n
\n <%- footerHtml || '' %>\n
\n <% if (frontmatter.components?.branding !== false) { %>\n
\n Build with docmd.\n
\n <% } %>\n
\n
\n <% } %>\n
\n <% } else { %>\n <%- content %>\n <% } %>\n\n <% if (frontmatter.components?.scripts === true) { %>\n <% if (frontmatter.components?.mainScripts === true) { %>\n \n <% } %>\n \n <% if (frontmatter.components?.lightbox === true && frontmatter.components?.mainScripts === true) { %>\n \n <% } %>\n \n <% if (frontmatter.components?.customJs === true && customJsFiles && customJsFiles.length > 0) { %>\n <% customJsFiles.forEach(jsFile => { %>\n \n <% }); %>\n <% } %>\n \n <% if (frontmatter.components?.pluginBodyScripts === true) { %>\n <%- pluginBodyScriptsHtml || '' %>\n <% } %>\n <% } %>\n \n <% if (frontmatter.customScripts) { %>\n <%- frontmatter.customScripts %>\n <% } %>\n\n ", + "toc.ejs": "<%# src/templates/toc.ejs %>\n<% \n// Helper function to decode HTML entities\nfunction decodeHtmlEntities(html) {\n return html\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ');\n}\n\n// Use the isActivePage flag if provided, otherwise fall back to checking navigationHtml\nconst shouldShowToc = typeof isActivePage !== 'undefined' ? isActivePage : \n (typeof navigationHtml !== 'undefined' && navigationHtml && navigationHtml.includes('class=\"active\"'));\n\nif (shouldShowToc && !frontmatter?.toc || frontmatter?.toc !== 'false') {\n // If direct headings aren't available, we'll try to extract them from the content\n let tocHeadings = [];\n if (headings && headings.length > 0) {\n // Use provided headings if available\n tocHeadings = headings.filter(h => h.level >= 2 && h.level <= 4);\n } else if (content) {\n // Basic regex to extract headings from HTML content\n const headingRegex = /]*?(?:id=\"([^\"]*)\")?[^>]*?>([\\s\\S]*?)<\\/h\\1>/g;\n let match;\n let contentStr = content.toString();\n \n while ((match = headingRegex.exec(contentStr)) !== null) {\n const level = parseInt(match[1], 10);\n // Use ID if available, or generate one from the text\n let id = match[2];\n // Remove any HTML tags inside the heading text\n const textWithTags = match[3].replace(/<\\/?[^>]+(>|$)/g, '');\n // Decode HTML entities\n const text = decodeHtmlEntities(textWithTags);\n \n if (!id) {\n // Generate an ID from the heading text if none exists\n id = text\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^\\w-]/g, '')\n .replace(/--+/g, '-')\n .replace(/^-+|-+$/g, '');\n }\n \n tocHeadings.push({ id, level, text });\n }\n }\n\n // Only show TOC if there are enough headings\n if (tocHeadings.length > 1) { \n%>\n
\n

On This Page<%- renderIcon(\"chevrons-down-up\") %>

\n \n
\n<% } \n} %> " +}; +if (typeof globalThis !== 'undefined') globalThis.__DOCMD_TEMPLATES__ = templates; +module.exports = templates; diff --git a/src/templates/layout.ejs b/src/templates/layout.ejs index 4fb00ea..cc69c17 100644 --- a/src/templates/layout.ejs +++ b/src/templates/layout.ejs @@ -44,17 +44,17 @@ data-copy-code-enabled="<%= config.copyCode === true %>">