diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 000000000..96e4850fc --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,181 @@ +# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages +# This workflow builds and deploys your MkDocs site when changes are pushed to main +name: Deploy MkDocs Documentation + +on: + # Trigger on push to main branch + push: + branches: + - main + - master # Also support master branch + # Only run when docs files change + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'requirements-docs.txt' + - '.github/workflows/deploy-docs.yml' + - 'scripts/fix-mkdocs-structure.ts' + + # Allow manual triggering from Actions tab + workflow_dispatch: + + # Run on pull requests for preview deployments + pull_request: + branches: + - main + - master + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'requirements-docs.txt' + - '.github/workflows/deploy-docs.yml' + - 'scripts/fix-mkdocs-structure.ts' + +jobs: + build-and-deploy: + name: Build and Deploy MkDocs + runs-on: ubuntu-latest + timeout-minutes: 10 + + # Required permissions for deployment + permissions: + contents: read + deployments: write + pull-requests: write # For PR preview comments + id-token: write # For OIDC authentication (if needed) + + steps: + - name: Checkout Repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: 'pip' + cache-dependency-path: 'requirements-docs.txt' + + - name: Install MkDocs and Dependencies + run: | + pip install --upgrade pip + pip install -r requirements-docs.txt + env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + + # Setup pnpm before fixing docs structure + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + # Setup Node.js with pnpm + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + # Install Node.js dependencies for the TypeScript script + - name: Install Dependencies + run: | + pnpm install --frozen-lockfile + + - name: Fix Documentation Structure + run: | + # Fix duplicate navigation entries by moving overview pages to index.md + pnpm run chore:fix-mkdocs-structure + + - name: Build MkDocs Site + run: | + # Build with strict mode but allow expected warnings + mkdocs build --verbose || { + EXIT_CODE=$? + # Check if the only issue is expected warnings + if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \ + [ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then + echo "✅ Build succeeded with expected warnings" + mkdocs build --verbose + else + echo "❌ Build failed with unexpected errors" + exit $EXIT_CODE + fi + } + + - name: Validate Built Site + run: | + # Basic validation that important files exist + test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1) + test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1) + test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1) + echo "✅ Site validation passed" + + # Install wrangler globally to avoid workspace issues + - name: Install Wrangler + run: | + npm install -g wrangler + + # Deploy using Wrangler (use pre-installed wrangler) + - name: Deploy to Cloudflare Pages + id: deploy + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy site --project-name=trilium-docs --branch=${{ github.ref_name }} + wranglerVersion: '' # Use pre-installed version + + # Deploy preview for PRs + - name: Deploy Preview to Cloudflare Pages + id: preview-deployment + if: github.event_name == 'pull_request' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy site --project-name=trilium-docs --branch=pr-${{ github.event.pull_request.number }} + wranglerVersion: '' # Use pre-installed version + + # Post deployment URL as PR comment + - name: Comment PR with Preview URL + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.issue.number; + // Construct preview URL based on Cloudflare Pages pattern + const previewUrl = `https://pr-${prNumber}.trilium-docs.pages.dev`; + const mainUrl = 'https://docs.triliumnotes.org'; + + // Check if we already commented + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const botComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Documentation preview is ready') + ); + + const commentBody = `📚 Documentation preview is ready!\n\n🔗 Preview URL: ${previewUrl}\n📖 Production URL: ${mainUrl}\n\n✅ All checks passed\n\n_This preview will be updated automatically with new commits._`; + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body: commentBody + }); + } diff --git a/.gitignore b/.gitignore index 66e9781f9..b2c4e3c46 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ upload *.tsbuildinfo /result -.svelte-kit \ No newline at end of file +.svelte-kit + +# docs +site/ diff --git a/docs/.pages b/docs/.pages new file mode 100644 index 000000000..276a4015a --- /dev/null +++ b/docs/.pages @@ -0,0 +1,8 @@ +# Control navigation order for top-level sections +nav: + - index.md + - User Guide + - Developer Guide + - Script API + - Release Notes + - ... # Include all other directories/files not explicitly listed \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..5675758ee --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# Trilium Notes + +Please see the [main documentation](index.md) or visit one of our translated versions: + +- [Español](README.es.md) +- [Italiano](README.it.md) +- [日本語](README.ja.md) +- [Русский](README.ru.md) +- [简体中文](README-ZH_CN.md) +- [繁體中文](README-ZH_TW.md) + +For the full application README, please visit our [GitHub repository](https://github.com/triliumnext/trilium). \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..5bf104216 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,94 @@ +# Trilium Notes Documentation + +Welcome to the official documentation for **Trilium Notes** - a hierarchical note-taking application with a focus on building large personal knowledge bases. + +![Trilium Notes Screenshot](app.png) + +## What is Trilium Notes? + +Trilium Notes is a powerful, feature-rich note-taking application designed for building and managing extensive personal knowledge bases. It offers: + +- **Hierarchical organization** with unlimited nesting of notes +- **Rich text editing** with markdown support +- **Powerful search** capabilities +- **Note relations** and attributes for semantic connections +- **Scripting support** for automation and customization +- **Synchronization** between devices +- **Encryption** for sensitive notes +- **Web clipper** for saving web content + +## Quick Links + +
+ +- :material-rocket-launch-outline: **[Quick Start Guide](User%20Guide/quick-start.md)** + + Get up and running with Trilium in minutes + +- :material-download: **[Installation](User%20Guide/installation.md)** + + Download and install Trilium on your platform + +- :material-docker: **[Docker Setup](User%20Guide/docker.md)** + + Deploy Trilium using Docker containers + +- :material-book-open-variant: **[User Guide](User%20Guide/index.md)** + + Comprehensive guide to all features + +- :material-code-braces: **[Script API](Script%20API/index.md)** + + Automate and extend Trilium with scripting + +- :material-wrench: **[Developer Guide](Developer%20Guide/index.md)** + + Contributing and development documentation + +
+ +## Features Overview + +### Note Organization +- Create unlimited hierarchical note structures +- Clone notes to appear in multiple locations +- Use attributes and relations for metadata +- Template system for consistent note creation + +### Content Types +- **Text notes** with rich formatting +- **Code notes** with syntax highlighting +- **Canvas notes** for drawing and diagrams +- **File attachments** of any type +- **Web view** for embedded content +- **Mermaid diagrams** support + +### Advanced Features +- **Full-text search** with advanced operators +- **Note map** visualization +- **Day notes** for journaling +- **Book notes** for long-form content +- **Protected notes** with encryption +- **Note versioning** and history + +### Automation & Integration +- JavaScript-based scripting +- Custom widgets and themes +- REST API for external integrations +- Web clipper browser extension +- Import/export in multiple formats + +## Getting Help + +- **[FAQ](support/faq.md)** - Frequently asked questions +- **[Troubleshooting](support/troubleshooting.md)** - Common issues and solutions +- **[Community Forum](https://github.com/triliumnext/trilium/discussions)** - Ask questions and share tips +- **[Issue Tracker](https://github.com/triliumnext/trilium/issues)** - Report bugs and request features + +## Contributing + +Trilium is open-source and welcomes contributions! Check out our [Contributing Guide](Developer%20Guide/contributing.md) to get started. + +## License + +Trilium Notes is licensed under [AGPL-3.0](https://github.com/triliumnext/trilium/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/javascripts/extra.js b/docs/javascripts/extra.js new file mode 100644 index 000000000..f5c075755 --- /dev/null +++ b/docs/javascripts/extra.js @@ -0,0 +1,111 @@ +// Custom JavaScript for Trilium Notes documentation + +// Add smooth scrolling for anchor links +document.addEventListener('DOMContentLoaded', function() { + // Smooth scroll for internal links + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }); + }); + + // Add copy button to code blocks if not already present + const codeBlocks = document.querySelectorAll('pre code'); + codeBlocks.forEach(block => { + if (!block.parentElement.querySelector('.copy-button')) { + const button = document.createElement('button'); + button.className = 'copy-button'; + button.textContent = 'Copy'; + button.addEventListener('click', () => { + navigator.clipboard.writeText(block.textContent); + button.textContent = 'Copied!'; + setTimeout(() => { + button.textContent = 'Copy'; + }, 2000); + }); + block.parentElement.appendChild(button); + } + }); + + // Add external link indicators + document.querySelectorAll('a[href^="http"]').forEach(link => { + if (!link.hostname.includes('trilium')) { + link.classList.add('external-link'); + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + } + }); + + // Platform detection for download buttons + const platform = detectPlatform(); + const downloadButtons = document.querySelectorAll('.download-button'); + downloadButtons.forEach(button => { + if (button.dataset.platform === platform) { + button.classList.add('recommended'); + button.innerHTML += ' Recommended'; + } + }); +}); + +// Detect user's platform +function detectPlatform() { + const userAgent = navigator.userAgent.toLowerCase(); + if (userAgent.includes('win')) return 'windows'; + if (userAgent.includes('mac')) return 'macos'; + if (userAgent.includes('linux')) return 'linux'; + return 'unknown'; +} + +// Add search shortcuts +document.addEventListener('keydown', function(e) { + // Ctrl/Cmd + K to focus search + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + const searchInput = document.querySelector('.md-search__input'); + if (searchInput) { + searchInput.focus(); + } + } +}); + +// Version selector enhancement +const versionSelector = document.querySelector('.md-version__current'); +if (versionSelector) { + // Add version comparison tooltip + versionSelector.addEventListener('mouseenter', function() { + const tooltip = document.createElement('div'); + tooltip.className = 'version-tooltip'; + tooltip.textContent = 'Click to view other versions'; + this.appendChild(tooltip); + }); +} + +// Analytics event tracking for documentation +if (typeof gtag !== 'undefined') { + // Track external link clicks + document.querySelectorAll('a[href^="http"]').forEach(link => { + link.addEventListener('click', () => { + gtag('event', 'click', { + 'event_category': 'external_link', + 'event_label': link.href + }); + }); + }); + + // Track code copy events + document.querySelectorAll('.copy-button').forEach(button => { + button.addEventListener('click', () => { + gtag('event', 'copy_code', { + 'event_category': 'engagement', + 'event_label': window.location.pathname + }); + }); + }); +} \ No newline at end of file diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 000000000..33ea4b928 --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,13 @@ +// MathJax configuration for mathematical notation support +window.MathJax = { + tex: { + inlineMath: [['$', '$'], ['\\(', '\\)']], + displayMath: [['$$', '$$'], ['\\[', '\\]']], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: 'no-mathjax', + processHtmlClass: 'mathjax' + } +}; \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 000000000..354beb509 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,121 @@ +/* Custom styles for Trilium Notes documentation */ + +/* Grid cards for homepage */ +.md-typeset .grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + margin-top: 1rem; +} + +.md-typeset .grid.cards > ul { + display: contents; +} + +.md-typeset .grid.cards > ul > li { + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: .25rem; + display: flex; + flex-direction: column; + padding: 1rem; + transition: border-color .25s, box-shadow .25s; +} + +.md-typeset .grid.cards > ul > li:hover { + border-color: var(--md-accent-fg-color); + box-shadow: 0 0 0 .1rem var(--md-accent-fg-color--transparent); +} + +/* Improve code block appearance */ +.md-typeset pre > code { + font-size: .85rem; +} + +/* Better admonition spacing */ +.md-typeset .admonition { + margin: 1.5rem 0; +} + +/* Trilium brand colors */ +:root { + --trilium-primary: #4a5568; + --trilium-accent: #805ad5; +} + +/* Custom badge styles */ +.badge { + background-color: var(--md-accent-fg-color); + border-radius: .125rem; + color: var(--md-accent-bg-color); + display: inline-block; + font-size: .75rem; + font-weight: 700; + padding: .125rem .375rem; + text-transform: uppercase; +} + +/* Version badge */ +.version-badge { + background-color: var(--md-primary-fg-color); + margin-left: .5rem; +} + +/* Platform badges */ +.platform-badge { + margin: 0 .25rem; +} + +.platform-badge.windows { + background-color: #0078d4; +} + +.platform-badge.macos { + background-color: #000000; +} + +.platform-badge.linux { + background-color: #fcc624; + color: #000000; +} + +/* Improve table readability */ +.md-typeset table:not([class]) { + font-size: .85rem; +} + +.md-typeset table:not([class]) th { + background-color: var(--md-default-bg-color); + font-weight: 700; +} + +/* API reference styling */ +.api-method { + background-color: var(--md-code-bg-color); + border-radius: .125rem; + font-family: var(--md-code-font-family); + font-weight: 600; + padding: .125rem .25rem; +} + +.api-method.get { + color: #10b981; +} + +.api-method.post { + color: #3b82f6; +} + +.api-method.put { + color: #f59e0b; +} + +.api-method.delete { + color: #ef4444; +} + +/* Responsive improvements */ +@media screen and (max-width: 76.1875em) { + .md-typeset .grid { + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + } +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..3d787e9ed --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,191 @@ +# MkDocs configuration for Trilium Notes documentation +site_name: Trilium Notes Documentation +site_url: https://docs.triliumnext.com +site_description: Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases +site_author: Trilium Notes Team + +# Repository information +repo_name: triliumnext/trilium +repo_url: https://github.com/triliumnext/trilium +edit_uri: edit/main/docs/ + +# Copyright +copyright: Copyright © 2025 Trilium Notes + +# Use document-style URLs to fix image paths +use_directory_urls: false + +# Theme configuration +theme: + name: material + + # Color scheme + palette: + # Light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: deep-purple + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: blue-grey + accent: deep-purple + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Font configuration + font: + text: Inter + code: JetBrains Mono + + # Features + features: + - announce.dismiss + - content.action.edit + - content.action.view + - content.code.annotate + - content.code.copy + - content.tooltips + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.progress + - navigation.path + - navigation.prune + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + - toc.integrate + + # Icons + icon: + logo: material/note-multiple + repo: fontawesome/brands/github + +# Plugins +plugins: + - search: + separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' + lang: + - en + - awesome-pages: + collapse_single_pages: false + strict: false + order: asc + sort_type: natural + order_by: title + - minify: + minify_html: true + minify_js: true + minify_css: true + htmlmin_opts: + remove_comments: true + - git-revision-date-localized: + enable_creation_date: true + type: iso_datetime + fallback_to_build_date: true + +# Extensions +markdown_extensions: + # Python Markdown + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - toc: + permalink: true + permalink_title: Anchor link to this section for reference + + # Python Markdown Extensions + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + combine_header_slug: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + +# Extra CSS and JavaScript (if needed) +extra_css: + - stylesheets/extra.css + +extra_javascript: + - javascripts/extra.js + # MathJax for mathematical notation + - javascripts/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + +# Extra configuration +extra: + # Social links + social: + - icon: fontawesome/brands/github + link: https://github.com/triliumnext/trilium + - icon: fontawesome/brands/docker + link: https://hub.docker.com/r/triliumnext/trilium + - icon: fontawesome/solid/globe + link: https://trilium.cc + + # Analytics (optional - add your own if needed) + analytics: + provider: google + property: G-XXXXXXXXXX # Replace with your Google Analytics ID + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! Help us improve this page by + opening an issue. + + # Version + version: + provider: mike + default: stable + +# Navigation is automatically generated from folder structure by awesome-pages plugin +# To customize order or titles, create .pages files in directories diff --git a/package.json b/package.json index 6eafcf347..5b3d3c3f1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "chore:generate-openapi": "tsx ./scripts/generate-openapi.ts", "chore:update-build-info": "tsx ./scripts/update-build-info.ts", "chore:update-version": "tsx ./scripts/update-version.ts", + "chore:fix-mkdocs-structure": "tsx ./scripts/fix-mkdocs-structure.ts", "edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs", "edit-docs:edit-demo": "pnpm run --filter edit-docs edit-demo", "test:all": "pnpm test:parallel && pnpm test:sequential", diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 000000000..a7fa25aec --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,21 @@ +# MkDocs and Material theme requirements for Trilium documentation +mkdocs>=1.6.0 +mkdocs-material>=9.5.0 +mkdocs-material-extensions>=1.3.0 + +# Essential plugins +mkdocs-awesome-pages-plugin>=2.9.0 # Auto-generate navigation from folder structure +mkdocs-minify-plugin>=0.8.0 +mkdocs-git-revision-date-localized-plugin>=1.2.0 + +# Optional but recommended plugins +mkdocs-redirects>=1.2.0 +mkdocs-rss-plugin>=1.12.0 +mkdocs-glightbox>=0.3.0 + +# For advanced features +pillow>=10.0.0 # For social cards generation +cairosvg>=2.7.0 # For social cards with SVG support + +# Search enhancements +mkdocs-material[imaging]>=9.5.0 \ No newline at end of file diff --git a/scripts/fix-mkdocs-structure.ts b/scripts/fix-mkdocs-structure.ts new file mode 100644 index 000000000..e0df764d6 --- /dev/null +++ b/scripts/fix-mkdocs-structure.ts @@ -0,0 +1,308 @@ +#!/usr/bin/env node +/** + * Fix MkDocs structure by moving overview pages to index.md inside their directories. + * This prevents duplicate navigation entries when a file and directory have the same name. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +interface FixResult { + message: string; +} + +/** + * Find markdown files that have a corresponding directory with the same name, + * and move them to index.md inside that directory. + */ +function fixDuplicateEntries(docsDir: string): FixResult[] { + const fixesMade: FixResult[] = []; + + function walkDir(dir: string): void { + let files: string[]; + try { + files = fs.readdirSync(dir); + } catch (err) { + console.warn(`Warning: Unable to read directory ${dir}: ${err.message}`); + return; + } + + for (const file of files) { + const filePath = path.join(dir, file); + let stat: fs.Stats; + + try { + stat = fs.statSync(filePath); + } catch (err) { + // File might have been moved already, skip it + continue; + } + + if (stat.isDirectory()) { + walkDir(filePath); + } else if (file.endsWith('.md')) { + const basename = file.slice(0, -3); // Remove .md extension + const dirPath = path.join(dir, basename); + + // Check if there's a directory with the same name + if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) { + const indexPath = path.join(dirPath, 'index.md'); + + // Check if index.md already exists in that directory + if (!fs.existsSync(indexPath)) { + // Move the file to index.md in the directory + fs.renameSync(filePath, indexPath); + fixesMade.push({ + message: `Moved ${path.relative(docsDir, filePath)} -> ${path.relative(docsDir, indexPath)}` + }); + + // Move associated images with pattern basename_* + try { + const dirFiles = fs.readdirSync(dir); + for (const imgFile of dirFiles) { + if (imgFile.startsWith(`${basename}_`)) { + const imgSrc = path.join(dir, imgFile); + try { + if (!fs.statSync(imgSrc).isDirectory()) { + const imgDest = path.join(dirPath, imgFile); + fs.renameSync(imgSrc, imgDest); + fixesMade.push({ + message: `Moved ${path.relative(docsDir, imgSrc)} -> ${path.relative(docsDir, imgDest)}` + }); + } + } catch (err) { + // File might have been moved already, skip it + } + } + } + } catch (err) { + // Directory might not exist anymore, skip it + } + + // Move exact match images + const imgExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg']; + for (const ext of imgExtensions) { + const imgFile = path.join(dir, `${basename}${ext}`); + if (fs.existsSync(imgFile)) { + const imgDest = path.join(dirPath, `${basename}${ext}`); + fs.renameSync(imgFile, imgDest); + fixesMade.push({ + message: `Moved ${path.relative(docsDir, imgFile)} -> ${path.relative(docsDir, imgDest)}` + }); + } + } + } + } + } + } + } + + walkDir(docsDir); + return fixesMade; +} + +/** + * Update references in markdown files to point to the new locations. + */ +function updateReferences(docsDir: string): FixResult[] { + const updatesMade: FixResult[] = []; + + function fixLink(match: string, text: string, link: string, currentDir: string, isIndex: boolean): string { + // Skip external links + if (link.startsWith('http')) { + return match; + } + + // Decode URL-encoded paths for processing + // Use decodeURIComponent which is equivalent to Python's unquote + let decodedLink: string; + try { + decodedLink = decodeURIComponent(link); + } catch (err) { + // If decoding fails, use the original link + decodedLink = link; + } + + // Special case: if we're in index.md and the link starts with the parent directory name + if (isIndex && decodedLink.includes('/')) { + const pathParts = decodedLink.split('/'); + const parentDirName = path.basename(currentDir); + + // Check if first part matches the parent directory name + if (pathParts[0] === parentDirName) { + // This is a self-referential path, strip the first part + const fixedLink = pathParts.slice(1).join('/'); + // Continue processing with the fixed link + const decodedFixedLink = fixedLink; + + // Check if this fixed link points to a directory with index.md + if (!decodedFixedLink.startsWith('/')) { + const resolvedPath = path.resolve(currentDir, decodedFixedLink); + + if (resolvedPath.endsWith('.md')) { + const potentialDir = resolvedPath.slice(0, -3); + const potentialIndex = path.join(potentialDir, 'index.md'); + + if (fs.existsSync(potentialIndex)) { + // Check if they share the same parent directory + if (path.dirname(potentialDir) === path.dirname(currentDir)) { + // It's a sibling - just use directory name + const dirName = path.basename(potentialDir).replace(/ /g, '%20'); + return `[${text}](${dirName}/)`; + } + + // Calculate relative path from current file to the directory + const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20'); + return `[${text}](${newPath}/)`; + } + } + } + + // If no special handling needed for the fixed link, return it as-is + const fixedLinkEncoded = fixedLink.replace(/ /g, '%20'); + return `[${text}](${fixedLinkEncoded})`; + } + } + + // For any .md link, check if there's a directory with index.md + if (!decodedLink.startsWith('/')) { + const resolvedPath = path.resolve(currentDir, decodedLink); + + // Check if this points to a file that should be a directory + if (resolvedPath.endsWith('.md')) { + const potentialDir = resolvedPath.slice(0, -3); + const potentialIndex = path.join(potentialDir, 'index.md'); + + // If a directory with index.md exists, update the link + if (fs.existsSync(potentialIndex)) { + if (isIndex) { + // Check if they share the same parent directory + if (path.dirname(potentialDir) === path.dirname(currentDir)) { + // It's a sibling - just use directory name + const dirName = path.basename(potentialDir).replace(/ /g, '%20'); + return `[${text}](${dirName}/)`; + } + } + + // Calculate relative path from current file to the directory + const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20'); + return `[${text}](${newPath}/)`; + } + } + } + + // Also handle local references (same directory) - should be 'if', not 'elif' + // This is intentional to handle both absolute and relative paths + if (!decodedLink.includes('/')) { + const basename = decodedLink.slice(0, -3); // Remove .md + const possibleDir = path.join(currentDir, basename); + + if (fs.existsSync(possibleDir) && fs.statSync(possibleDir).isDirectory()) { + const encodedBasename = basename.replace(/ /g, '%20'); + return `[${text}](${encodedBasename}/)`; + } + } + + return match; + } + + function walkDir(dir: string): void { + let files: string[]; + try { + files = fs.readdirSync(dir); + } catch (err) { + console.warn(`Warning: Unable to read directory ${dir}: ${err.message}`); + return; + } + + for (const file of files) { + const filePath = path.join(dir, file); + let stat: fs.Stats; + + try { + stat = fs.statSync(filePath); + } catch (err) { + // File might have been moved already, skip it + continue; + } + + if (stat.isDirectory()) { + walkDir(filePath); + } else if (file.endsWith('.md')) { + let content = fs.readFileSync(filePath, 'utf-8'); + const originalContent = content; + + const isIndex = file === 'index.md'; + const currentDir = path.dirname(filePath); + + // Update markdown links: [text](path.md) + const pattern = /\[([^\]]*)\]\(([^)]+\.md)\)/g; + content = content.replace(pattern, (match, text, link) => { + return fixLink(match, text, link, currentDir, isIndex); + }); + + if (content !== originalContent) { + fs.writeFileSync(filePath, content, 'utf-8'); + updatesMade.push({ + message: `Updated references in ${path.relative(docsDir, filePath)}` + }); + } + } + } + } + + walkDir(docsDir); + return updatesMade; +} + +function main(): number { + // Get the docs directory + const scriptDir = path.dirname(new URL(import.meta.url).pathname); + const projectRoot = path.dirname(scriptDir); + const docsDir = path.join(projectRoot, 'docs'); + + // Handle Windows paths (remove leading slash if on Windows) + const normalizedDocsDir = process.platform === 'win32' && docsDir.startsWith('/') + ? docsDir.substring(1) + : docsDir; + + if (!fs.existsSync(normalizedDocsDir)) { + console.error(`Error: docs directory not found at ${normalizedDocsDir}`); + return 1; + } + + console.log(`Fixing MkDocs structure in ${normalizedDocsDir}`); + console.log('-'.repeat(50)); + + // Fix duplicate entries + const fixes = fixDuplicateEntries(normalizedDocsDir); + if (fixes.length > 0) { + console.log('Files reorganized:'); + for (const fix of fixes) { + console.log(` - ${fix.message}`); + } + } else { + console.log('No duplicate entries found that need fixing'); + } + + console.log(); + + // Update references + const updates = updateReferences(normalizedDocsDir); + if (updates.length > 0) { + console.log('References updated:'); + for (const update of updates) { + console.log(` - ${update.message}`); + } + } else { + console.log('No references needed updating'); + } + + console.log('-'.repeat(50)); + console.log(`Structure fix complete: ${fixes.length} files moved, ${updates.length} files updated`); + + return 0; +} + +// Run the main function +process.exit(main()); \ No newline at end of file