mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-05 05:48:42 +00:00
update
This commit is contained in:
2
docs/.gitattributes
vendored
Normal file
2
docs/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
17
docs/.github/workflows/ci.yml
vendored
Normal file
17
docs/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm install
|
||||
- run: npm test
|
2
docs/.gitignore
vendored
Normal file
2
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
_site/
|
||||
node_modules/
|
@@ -1,25 +1,55 @@
|
||||
# SeedPass Documentation
|
||||
# Archivox
|
||||
|
||||
This directory contains supplementary guides for using SeedPass.
|
||||
Archivox is a lightweight static site generator aimed at producing documentation sites similar to "Read the Docs". Write your content in Markdown, run the generator, and deploy the static files anywhere.
|
||||
|
||||
## Quick Example: Get a TOTP Code
|
||||
[](https://github.com/PR0M3TH3AN/Archivox/actions/workflows/ci.yml)
|
||||
|
||||
Run `seedpass entry get <query>` to retrieve a time-based one-time password (TOTP).
|
||||
The `<query>` can be a label, title, or index. A progress bar shows the remaining
|
||||
seconds in the current period.
|
||||
## Features
|
||||
- Markdown based pages with automatic navigation
|
||||
- Responsive layout with sidebar and search powered by Lunr.js
|
||||
- Simple configuration through `config.yaml`
|
||||
- Extensible via plugins and custom templates
|
||||
|
||||
## Getting Started
|
||||
Install the dependencies and start the development server:
|
||||
|
||||
```bash
|
||||
$ seedpass entry get "email"
|
||||
[##########----------] 15s
|
||||
Code: 123456
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
To show all stored TOTP codes with their countdown timers, run:
|
||||
The site will be available at `http://localhost:8080`. Edit files inside the `content/` directory to update pages.
|
||||
|
||||
To create a new project from the starter template you can run:
|
||||
|
||||
```bash
|
||||
$ seedpass entry totp-codes
|
||||
npx create-archivox my-docs --install
|
||||
```
|
||||
|
||||
## CLI and API Reference
|
||||
## Building
|
||||
When you are ready to publish your documentation run:
|
||||
|
||||
See [advanced_cli.md](advanced_cli.md) for a list of command examples. Detailed information about the REST API is available in [api_reference.md](api_reference.md). When starting the API, set `SEEDPASS_CORS_ORIGINS` if you need to allow requests from specific web origins.
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The generated site is placed in the `_site/` folder.
|
||||
|
||||
## Customization
|
||||
- **`config.yaml`** – change the site title, theme options and other settings.
|
||||
- **`plugins/`** – add JavaScript files exporting hook functions such as `onPageRendered` to extend the build process.
|
||||
- **`templates/`** – modify or replace the Nunjucks templates for full control over the HTML.
|
||||
|
||||
## Hosting
|
||||
Upload the contents of `_site/` to any static host. For Netlify you can use the provided `netlify.toml`:
|
||||
|
||||
```toml
|
||||
[build]
|
||||
command = "npm run build"
|
||||
publish = "_site"
|
||||
```
|
||||
|
||||
## Documentation
|
||||
See the files under the `docs/` directory for a full guide to Archivox including an integration tutorial for existing projects.
|
||||
|
||||
Archivox is released under the MIT License.
|
||||
|
34
docs/__tests__/buildNav.test.js
Normal file
34
docs/__tests__/buildNav.test.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const { buildNav } = require('../src/generator');
|
||||
|
||||
test('generates navigation tree', () => {
|
||||
const pages = [
|
||||
{ file: 'guide/install.md', data: { title: 'Install', order: 1 } },
|
||||
{ file: 'guide/usage.md', data: { title: 'Usage', order: 2 } },
|
||||
{ file: 'guide/nested/info.md', data: { title: 'Info', order: 1 } }
|
||||
];
|
||||
const tree = buildNav(pages);
|
||||
const guide = tree.find(n => n.name === 'guide');
|
||||
expect(guide).toBeDefined();
|
||||
expect(guide.children.length).toBe(3);
|
||||
const install = guide.children.find(c => c.name === 'install.md');
|
||||
expect(install.path).toBe('/guide/install.html');
|
||||
});
|
||||
|
||||
test('adds display names and section flags', () => {
|
||||
const pages = [
|
||||
{ file: '02-api.md', data: { title: 'API', order: 2 } },
|
||||
{ file: '01-guide/index.md', data: { title: 'Guide', order: 1 } },
|
||||
{ file: '01-guide/setup.md', data: { title: 'Setup', order: 2 } },
|
||||
{ file: 'index.md', data: { title: 'Home', order: 10 } }
|
||||
];
|
||||
const nav = buildNav(pages);
|
||||
expect(nav[0].name).toBe('index.md');
|
||||
const guide = nav.find(n => n.name === '01-guide');
|
||||
expect(guide.displayName).toBe('Guide');
|
||||
expect(guide.isSection).toBe(true);
|
||||
const api = nav.find(n => n.name === '02-api.md');
|
||||
expect(api.displayName).toBe('API');
|
||||
// alphabetical within same order
|
||||
expect(nav[1].name).toBe('01-guide');
|
||||
expect(nav[2].name).toBe('02-api.md');
|
||||
});
|
13
docs/__tests__/loadConfig.test.js
Normal file
13
docs/__tests__/loadConfig.test.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const loadConfig = require('../src/config/loadConfig');
|
||||
|
||||
test('loads configuration and merges defaults', () => {
|
||||
const dir = fs.mkdtempSync(path.join(__dirname, 'cfg-'));
|
||||
const file = path.join(dir, 'config.yaml');
|
||||
fs.writeFileSync(file, 'site:\n title: Test Site\n');
|
||||
const cfg = loadConfig(file);
|
||||
expect(cfg.site.title).toBe('Test Site');
|
||||
expect(cfg.navigation.search).toBe(true);
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
23
docs/__tests__/pluginHooks.test.js
Normal file
23
docs/__tests__/pluginHooks.test.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const loadPlugins = require('../src/config/loadPlugins');
|
||||
|
||||
test('plugin hook modifies data', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'plugins-'));
|
||||
const pluginFile = path.join(dir, 'test.plugin.js');
|
||||
fs.writeFileSync(
|
||||
pluginFile,
|
||||
"module.exports = { onParseMarkdown: ({ content }) => ({ content: content + '!!' }) };\n"
|
||||
);
|
||||
|
||||
const plugins = loadPlugins({ pluginsDir: dir, plugins: ['test.plugin'] });
|
||||
let data = { content: 'hello' };
|
||||
for (const plugin of plugins) {
|
||||
if (typeof plugin.onParseMarkdown === 'function') {
|
||||
const res = await plugin.onParseMarkdown(data);
|
||||
if (res !== undefined) data = res;
|
||||
}
|
||||
}
|
||||
expect(data.content).toBe('hello!!');
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
77
docs/__tests__/renderMarkdown.test.js
Normal file
77
docs/__tests__/renderMarkdown.test.js
Normal file
@@ -0,0 +1,77 @@
|
||||
jest.mock('@11ty/eleventy', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
return class Eleventy {
|
||||
constructor(input, output) {
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
}
|
||||
setConfig() {}
|
||||
async write() {
|
||||
const walk = d => {
|
||||
const entries = fs.readdirSync(d, { withFileTypes: true });
|
||||
let files = [];
|
||||
for (const e of entries) {
|
||||
const p = path.join(d, e.name);
|
||||
if (e.isDirectory()) files = files.concat(walk(p));
|
||||
else if (p.endsWith('.md')) files.push(p);
|
||||
}
|
||||
return files;
|
||||
};
|
||||
for (const file of walk(this.input)) {
|
||||
const rel = path.relative(this.input, file).replace(/\.md$/, '.html');
|
||||
const dest = path.join(this.output, rel);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, '<header></header><aside class="sidebar"></aside>');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { generate } = require('../src/generator');
|
||||
|
||||
function getPaths(tree) {
|
||||
const paths = [];
|
||||
for (const node of tree) {
|
||||
if (node.path) paths.push(node.path);
|
||||
if (node.children) paths.push(...getPaths(node.children));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
test('markdown files render with layout and appear in nav/search', async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'df-test-'));
|
||||
const contentDir = path.join(tmp, 'content');
|
||||
const outputDir = path.join(tmp, '_site');
|
||||
fs.mkdirSync(path.join(contentDir, 'guide'), { recursive: true });
|
||||
fs.writeFileSync(path.join(contentDir, 'index.md'), '# Home\nWelcome');
|
||||
fs.writeFileSync(path.join(contentDir, 'guide', 'install.md'), '# Install\nSteps');
|
||||
const configPath = path.join(tmp, 'config.yaml');
|
||||
fs.writeFileSync(configPath, 'site:\n title: Test\n');
|
||||
|
||||
await generate({ contentDir, outputDir, configPath });
|
||||
|
||||
const indexHtml = fs.readFileSync(path.join(outputDir, 'index.html'), 'utf8');
|
||||
const installHtml = fs.readFileSync(path.join(outputDir, 'guide', 'install.html'), 'utf8');
|
||||
expect(indexHtml).toContain('<header');
|
||||
expect(indexHtml).toContain('<aside class="sidebar"');
|
||||
expect(installHtml).toContain('<header');
|
||||
expect(installHtml).toContain('<aside class="sidebar"');
|
||||
|
||||
const nav = JSON.parse(fs.readFileSync(path.join(outputDir, 'navigation.json'), 'utf8'));
|
||||
const navPaths = getPaths(nav);
|
||||
expect(navPaths).toContain('/index.html');
|
||||
expect(navPaths).toContain('/guide/install.html');
|
||||
|
||||
const search = JSON.parse(fs.readFileSync(path.join(outputDir, 'search-index.json'), 'utf8'));
|
||||
const docs = search.docs.map(d => d.id);
|
||||
expect(docs).toContain('index.html');
|
||||
expect(docs).toContain('guide/install.html');
|
||||
const installDoc = search.docs.find(d => d.id === 'guide/install.html');
|
||||
expect(installDoc.body).toContain('Steps');
|
||||
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
128
docs/__tests__/responsive.test.js
Normal file
128
docs/__tests__/responsive.test.js
Normal file
@@ -0,0 +1,128 @@
|
||||
jest.mock('@11ty/eleventy', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
return class Eleventy {
|
||||
constructor(input, output) {
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
}
|
||||
setConfig() {}
|
||||
async write() {
|
||||
const walk = d => {
|
||||
const entries = fs.readdirSync(d, { withFileTypes: true });
|
||||
let files = [];
|
||||
for (const e of entries) {
|
||||
const p = path.join(d, e.name);
|
||||
if (e.isDirectory()) files = files.concat(walk(p));
|
||||
else if (p.endsWith('.md')) files.push(p);
|
||||
}
|
||||
return files;
|
||||
};
|
||||
for (const file of walk(this.input)) {
|
||||
const rel = path.relative(this.input, file).replace(/\.md$/, '.html');
|
||||
const dest = path.join(this.output, rel);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
dest,
|
||||
`<!DOCTYPE html><html><head><link rel="stylesheet" href="/assets/theme.css"></head><body><header><button id="sidebar-toggle" class="sidebar-toggle">☰</button></header><div class="container"><aside class="sidebar"></aside><main></main></div><script src="/assets/theme.js"></script></body></html>`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const os = require('os');
|
||||
const puppeteer = require('puppeteer');
|
||||
const { generate } = require('../src/generator');
|
||||
|
||||
jest.setTimeout(30000);
|
||||
|
||||
let server;
|
||||
let browser;
|
||||
let port;
|
||||
let tmp;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'df-responsive-'));
|
||||
const contentDir = path.join(tmp, 'content');
|
||||
const outputDir = path.join(tmp, '_site');
|
||||
fs.mkdirSync(contentDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(contentDir, 'index.md'), '# Home\n');
|
||||
await generate({ contentDir, outputDir });
|
||||
fs.cpSync(path.join(__dirname, '../assets'), path.join(outputDir, 'assets'), { recursive: true });
|
||||
|
||||
server = http.createServer((req, res) => {
|
||||
let filePath = path.join(outputDir, req.url === '/' ? 'index.html' : req.url);
|
||||
if (req.url.startsWith('/assets')) {
|
||||
filePath = path.join(outputDir, req.url);
|
||||
}
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath).slice(1);
|
||||
const type = { html: 'text/html', js: 'text/javascript', css: 'text/css' }[ext] || 'application/octet-stream';
|
||||
res.writeHead(200, { 'Content-Type': type });
|
||||
res.end(data);
|
||||
});
|
||||
});
|
||||
await new Promise(resolve => {
|
||||
server.listen(0, () => {
|
||||
port = server.address().port;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (browser) await browser.close();
|
||||
if (server) server.close();
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('sidebar opens on small screens', async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 500, height: 800 });
|
||||
await page.goto(`http://localhost:${port}/`);
|
||||
await page.waitForSelector('#sidebar-toggle');
|
||||
await page.click('#sidebar-toggle');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
const bodyClass = await page.evaluate(() => document.body.classList.contains('sidebar-open'));
|
||||
const sidebarLeft = await page.evaluate(() => getComputedStyle(document.querySelector('.sidebar')).left);
|
||||
expect(bodyClass).toBe(true);
|
||||
expect(sidebarLeft).toBe('0px');
|
||||
});
|
||||
|
||||
test('clicking outside closes sidebar on small screens', async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 500, height: 800 });
|
||||
await page.goto(`http://localhost:${port}/`);
|
||||
await page.waitForSelector('#sidebar-toggle');
|
||||
await page.click('#sidebar-toggle');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
await page.click('main');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
const bodyClass = await page.evaluate(() => document.body.classList.contains('sidebar-open'));
|
||||
expect(bodyClass).toBe(false);
|
||||
});
|
||||
|
||||
test('sidebar toggles on large screens', async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1024, height: 800 });
|
||||
await page.goto(`http://localhost:${port}/`);
|
||||
await page.waitForSelector('#sidebar-toggle');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
let sidebarWidth = await page.evaluate(() => getComputedStyle(document.querySelector('.sidebar')).width);
|
||||
expect(sidebarWidth).toBe('240px');
|
||||
await page.click('#sidebar-toggle');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
sidebarWidth = await page.evaluate(() => getComputedStyle(document.querySelector('.sidebar')).width);
|
||||
expect(sidebarWidth).toBe('0px');
|
||||
});
|
3475
docs/assets/lunr.js
Normal file
3475
docs/assets/lunr.js
Normal file
File diff suppressed because it is too large
Load Diff
160
docs/assets/theme.css
Normal file
160
docs/assets/theme.css
Normal file
@@ -0,0 +1,160 @@
|
||||
:root {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #333333;
|
||||
--sidebar-bg: #f3f3f3;
|
||||
--sidebar-width: 240px;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg-color: #222222;
|
||||
--text-color: #eeeeee;
|
||||
--sidebar-bg: #333333;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--sidebar-bg);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1100;
|
||||
}
|
||||
.search-input {
|
||||
margin-left: auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
.search-results {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 100%;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid #ccc;
|
||||
width: 250px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
}
|
||||
.search-results a {
|
||||
display: block;
|
||||
padding: 0.25rem;
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.search-results a:hover {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
.search-results .no-results {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
.logo { text-decoration: none; color: var(--text-color); font-weight: bold; }
|
||||
.sidebar-toggle,
|
||||
.theme-toggle { background: none; border: none; font-size: 1.2rem; margin-right: 1rem; cursor: pointer; }
|
||||
.container { display: flex; flex: 1; }
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: var(--sidebar-bg);
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.sidebar ul { list-style: none; padding: 0; margin: 0; }
|
||||
.sidebar li { margin: 0.25rem 0; }
|
||||
.sidebar a { text-decoration: none; color: var(--text-color); display: block; padding: 0.25rem 0; }
|
||||
.sidebar nav { font-size: 0.9rem; }
|
||||
.nav-link:hover { text-decoration: underline; }
|
||||
.nav-link.active { font-weight: bold; }
|
||||
.nav-section summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.nav-section summary::-webkit-details-marker { display: none; }
|
||||
.nav-section summary::before {
|
||||
content: '▸';
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.nav-section[open] > summary::before { transform: rotate(90deg); }
|
||||
.nav-level { padding-left: 1rem; margin-left: 0.5rem; border-left: 2px solid #ccc; }
|
||||
.sidebar ul ul { padding-left: 1rem; margin-left: 0.5rem; border-left: 2px solid #ccc; }
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
}
|
||||
.breadcrumbs a { color: var(--text-color); text-decoration: none; }
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: var(--sidebar-bg);
|
||||
position: relative;
|
||||
}
|
||||
.footer-links {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.footer-links a {
|
||||
margin: 0 0.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.footer-permanent-links {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
bottom: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.footer-permanent-links a {
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
body.sidebar-open .sidebar-overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: -100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
transition: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
body.sidebar-open .sidebar { left: 0; }
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.sidebar {
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
body:not(.sidebar-open) .sidebar {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
107
docs/assets/theme.js
Normal file
107
docs/assets/theme.js
Normal file
@@ -0,0 +1,107 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
const root = document.documentElement;
|
||||
|
||||
function setTheme(theme) {
|
||||
root.dataset.theme = theme;
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
const stored = localStorage.getItem('theme');
|
||||
if (stored) setTheme(stored);
|
||||
|
||||
if (window.innerWidth > 768) {
|
||||
document.body.classList.add('sidebar-open');
|
||||
}
|
||||
|
||||
sidebarToggle?.addEventListener('click', () => {
|
||||
document.body.classList.toggle('sidebar-open');
|
||||
});
|
||||
|
||||
sidebarOverlay?.addEventListener('click', () => {
|
||||
document.body.classList.remove('sidebar-open');
|
||||
});
|
||||
|
||||
themeToggle?.addEventListener('click', () => {
|
||||
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
});
|
||||
|
||||
// search
|
||||
let lunrIndex;
|
||||
let docs = [];
|
||||
async function loadIndex() {
|
||||
if (lunrIndex) return;
|
||||
try {
|
||||
const res = await fetch('/search-index.json');
|
||||
const data = await res.json();
|
||||
lunrIndex = lunr.Index.load(data.index);
|
||||
docs = data.docs;
|
||||
} catch (e) {
|
||||
console.error('Search index failed to load', e);
|
||||
}
|
||||
}
|
||||
|
||||
function highlight(text, q) {
|
||||
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&') + ')', 'gi');
|
||||
return text.replace(re, '<mark>$1</mark>');
|
||||
}
|
||||
|
||||
searchInput?.addEventListener('input', async e => {
|
||||
const q = e.target.value.trim();
|
||||
await loadIndex();
|
||||
if (!lunrIndex || !q) {
|
||||
searchResults.style.display = 'none';
|
||||
searchResults.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const matches = lunrIndex.search(q);
|
||||
searchResults.innerHTML = '';
|
||||
if (!matches.length) {
|
||||
searchResults.innerHTML = '<div class="no-results">No matches found</div>';
|
||||
searchResults.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
matches.forEach(m => {
|
||||
const doc = docs.find(d => d.id === m.ref);
|
||||
if (!doc) return;
|
||||
const a = document.createElement('a');
|
||||
a.href = doc.url;
|
||||
const snippet = doc.body ? doc.body.slice(0, 160) + (doc.body.length > 160 ? '...' : '') : '';
|
||||
a.innerHTML = '<strong>' + highlight(doc.title, q) + '</strong><br><small>' + highlight(snippet, q) + '</small>';
|
||||
searchResults.appendChild(a);
|
||||
});
|
||||
searchResults.style.display = 'block';
|
||||
});
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (!searchResults.contains(e.target) && e.target !== searchInput) {
|
||||
searchResults.style.display = 'none';
|
||||
}
|
||||
if (
|
||||
window.innerWidth <= 768 &&
|
||||
document.body.classList.contains('sidebar-open') &&
|
||||
sidebar &&
|
||||
!sidebar.contains(e.target) &&
|
||||
e.target !== sidebarToggle
|
||||
) {
|
||||
document.body.classList.remove('sidebar-open');
|
||||
}
|
||||
});
|
||||
|
||||
// breadcrumbs
|
||||
const bc = document.getElementById('breadcrumbs');
|
||||
if (bc) {
|
||||
const parts = location.pathname.split('/').filter(Boolean);
|
||||
let path = '';
|
||||
bc.innerHTML = '<a href="/">Home</a>';
|
||||
parts.forEach((p) => {
|
||||
path += '/' + p;
|
||||
bc.innerHTML += ' / <a href="' + path + '">' + p.replace(/-/g, ' ') + '</a>';
|
||||
});
|
||||
}
|
||||
});
|
45
docs/bin/create-archivox.js
Executable file
45
docs/bin/create-archivox.js
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function copyDir(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyDir(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const install = args.includes('--install');
|
||||
const targetArg = args.find(a => !a.startsWith('-')) || '.';
|
||||
const targetDir = path.resolve(process.cwd(), targetArg);
|
||||
|
||||
const templateDir = path.join(__dirname, '..', 'starter');
|
||||
copyDir(templateDir, targetDir);
|
||||
|
||||
const pkgPath = path.join(targetDir, 'package.json');
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
const version = require('../package.json').version;
|
||||
if (pkg.dependencies && pkg.dependencies.archivox)
|
||||
pkg.dependencies.archivox = `^${version}`;
|
||||
pkg.name = path.basename(targetDir);
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
||||
}
|
||||
|
||||
if (install) {
|
||||
execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
console.log(`Archivox starter created at ${targetDir}`);
|
||||
}
|
||||
|
||||
main();
|
15
docs/build-docs.js
Executable file
15
docs/build-docs.js
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
const path = require('path');
|
||||
const { generate } = require('./src/generator');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const contentDir = path.join(__dirname, 'docs', 'content');
|
||||
const configPath = path.join(__dirname, 'docs', 'config.yaml');
|
||||
const outputDir = path.join(__dirname, '_site');
|
||||
await generate({ contentDir, outputDir, configPath });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
13
docs/docs/config.yaml
Normal file
13
docs/docs/config.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
site:
|
||||
title: "Archivox Docs"
|
||||
description: "Simple static docs."
|
||||
|
||||
navigation:
|
||||
search: true
|
||||
|
||||
footer:
|
||||
links:
|
||||
- text: "Custom Link 1"
|
||||
url: "https://example.com"
|
||||
- text: "Custom Link 2"
|
||||
url: "https://example.com/other"
|
3
docs/docs/content/01-getting-started/index.md
Normal file
3
docs/docs/content/01-getting-started/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Getting Started
|
||||
|
||||
Welcome to SeedPass!
|
15
docs/docs/content/index.md
Normal file
15
docs/docs/content/index.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Archivox Documentation
|
||||
|
||||
Welcome to the official documentation for **Archivox**, a lightweight static site generator designed for "Read the Docs" style websites.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # start local server at http://localhost:8080
|
||||
npm run build # generate the _site/ folder
|
||||
```
|
||||
|
||||
Archivox converts Markdown files inside a `content/` folder into a full documentation site with search, navigation, and responsive design.
|
||||
|
||||
Check the **Getting Started** section to learn how to run Archivox locally and the **Project Integration** guide to drop Archivox into an existing codebase.
|
11
docs/docs/package.json
Normal file
11
docs/docs/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "eleventy --serve",
|
||||
"build": "node node_modules/archivox/src/generator/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"archivox": "^1.0.0"
|
||||
}
|
||||
}
|
3
docs/netlify.toml
Normal file
3
docs/netlify.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[build]
|
||||
command = "node build-docs.js"
|
||||
publish = "_site"
|
6357
docs/package-lock.json
generated
Normal file
6357
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
docs/package.json
Normal file
25
docs/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "archivox",
|
||||
"version": "1.0.0",
|
||||
"description": "Archivox static site generator",
|
||||
"scripts": {
|
||||
"dev": "eleventy --serve",
|
||||
"build": "node src/generator/index.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11ty/eleventy": "^2.0.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^11.1.1",
|
||||
"lunr": "^2.3.9",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.6.1",
|
||||
"puppeteer": "^24.12.1"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"create-archivox": "./bin/create-archivox.js"
|
||||
}
|
||||
}
|
7
docs/plugins/analytics.js
Normal file
7
docs/plugins/analytics.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
onPageRendered: async ({ html, file }) => {
|
||||
// Example: inject analytics script into each page
|
||||
const snippet = '\n<script>console.log("Page viewed: ' + file + '")</script>';
|
||||
return { html: html.replace('</body>', `${snippet}</body>`) };
|
||||
}
|
||||
};
|
70
docs/src/config/loadConfig.js
Normal file
70
docs/src/config/loadConfig.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
function deepMerge(target, source) {
|
||||
for (const key of Object.keys(source)) {
|
||||
if (
|
||||
source[key] &&
|
||||
typeof source[key] === 'object' &&
|
||||
!Array.isArray(source[key])
|
||||
) {
|
||||
target[key] = deepMerge(target[key] || {}, source[key]);
|
||||
} else if (source[key] !== undefined) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function loadConfig(configPath = path.join(process.cwd(), 'config.yaml')) {
|
||||
let raw = {};
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
raw = yaml.load(fs.readFileSync(configPath, 'utf8')) || {};
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse ${configPath}: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
site: {
|
||||
title: 'Archivox',
|
||||
description: '',
|
||||
logo: '',
|
||||
favicon: ''
|
||||
},
|
||||
navigation: {
|
||||
search: true
|
||||
},
|
||||
footer: {},
|
||||
theme: {
|
||||
name: 'minimal',
|
||||
darkMode: false
|
||||
},
|
||||
features: {},
|
||||
pluginsDir: 'plugins',
|
||||
plugins: []
|
||||
};
|
||||
|
||||
const config = deepMerge(defaults, raw);
|
||||
|
||||
const errors = [];
|
||||
if (
|
||||
!config.site ||
|
||||
typeof config.site.title !== 'string' ||
|
||||
!config.site.title.trim()
|
||||
) {
|
||||
errors.push('site.title is required in config.yaml');
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
errors.forEach(err => console.error(`Config error: ${err}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
module.exports = loadConfig;
|
24
docs/src/config/loadPlugins.js
Normal file
24
docs/src/config/loadPlugins.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
function loadPlugins(config) {
|
||||
const dir = path.resolve(process.cwd(), config.pluginsDir || 'plugins');
|
||||
const names = Array.isArray(config.plugins) ? config.plugins : [];
|
||||
const plugins = [];
|
||||
for (const name of names) {
|
||||
const file = path.join(dir, name.endsWith('.js') ? name : `${name}.js`);
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
const mod = require(file);
|
||||
plugins.push(mod);
|
||||
} catch (e) {
|
||||
console.error(`Failed to load plugin ${name}:`, e);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Plugin not found: ${file}`);
|
||||
}
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
module.exports = loadPlugins;
|
235
docs/src/generator/index.js
Normal file
235
docs/src/generator/index.js
Normal file
@@ -0,0 +1,235 @@
|
||||
// Generator entry point for Archivox
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const matter = require('gray-matter');
|
||||
const lunr = require('lunr');
|
||||
const marked = require('marked');
|
||||
const { lexer } = marked;
|
||||
const loadConfig = require('../config/loadConfig');
|
||||
const loadPlugins = require('../config/loadPlugins');
|
||||
|
||||
function formatName(name) {
|
||||
return name
|
||||
.replace(/^\d+[-_]?/, '')
|
||||
.replace(/\.md$/, '');
|
||||
}
|
||||
|
||||
async function readDirRecursive(dir) {
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const res = path.resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...await readDirRecursive(res));
|
||||
} else {
|
||||
files.push(res);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function buildNav(pages) {
|
||||
const tree = {};
|
||||
for (const page of pages) {
|
||||
const rel = page.file.replace(/\\/g, '/');
|
||||
if (rel === 'index.md') {
|
||||
if (!tree.children) tree.children = [];
|
||||
tree.children.push({
|
||||
name: 'index.md',
|
||||
children: [],
|
||||
page: page.data,
|
||||
path: `/${rel.replace(/\.md$/, '.html')}`,
|
||||
order: page.data.order || 0
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const parts = rel.split('/');
|
||||
let node = tree;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const isLast = i === parts.length - 1;
|
||||
const isIndex = isLast && part === 'index.md';
|
||||
if (isIndex) {
|
||||
node.page = page.data;
|
||||
node.path = `/${rel.replace(/\.md$/, '.html')}`;
|
||||
node.order = page.data.order || 0;
|
||||
break;
|
||||
}
|
||||
if (!node.children) node.children = [];
|
||||
let child = node.children.find(c => c.name === part);
|
||||
if (!child) {
|
||||
child = { name: part, children: [] };
|
||||
node.children.push(child);
|
||||
}
|
||||
node = child;
|
||||
if (isLast) {
|
||||
node.page = page.data;
|
||||
node.path = `/${rel.replace(/\.md$/, '.html')}`;
|
||||
node.order = page.data.order || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finalize(node, isRoot = false) {
|
||||
if (node.page && node.page.title) {
|
||||
node.displayName = node.page.title;
|
||||
} else if (node.name) {
|
||||
node.displayName = formatName(node.name);
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(c => finalize(c));
|
||||
node.children.sort((a, b) => {
|
||||
const orderDiff = (a.order || 0) - (b.order || 0);
|
||||
if (orderDiff !== 0) return orderDiff;
|
||||
return (a.displayName || '').localeCompare(b.displayName || '');
|
||||
});
|
||||
node.isSection = node.children.length > 0;
|
||||
} else {
|
||||
node.isSection = false;
|
||||
}
|
||||
if (isRoot && node.children) {
|
||||
const idx = node.children.findIndex(c => c.name === 'index.md');
|
||||
if (idx > 0) {
|
||||
const [first] = node.children.splice(idx, 1);
|
||||
node.children.unshift(first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalize(tree, true);
|
||||
return tree.children || [];
|
||||
}
|
||||
|
||||
async function generate({ contentDir = 'content', outputDir = '_site', configPath } = {}) {
|
||||
const config = loadConfig(configPath);
|
||||
const plugins = loadPlugins(config);
|
||||
|
||||
async function runHook(name, data) {
|
||||
for (const plugin of plugins) {
|
||||
if (typeof plugin[name] === 'function') {
|
||||
const res = await plugin[name](data);
|
||||
if (res !== undefined) data = res;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
if (!fs.existsSync(contentDir)) {
|
||||
console.error(`Content directory not found: ${contentDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await readDirRecursive(contentDir);
|
||||
const pages = [];
|
||||
const assets = [];
|
||||
const searchDocs = [];
|
||||
|
||||
for (const file of files) {
|
||||
const rel = path.relative(contentDir, file);
|
||||
if (file.endsWith('.md')) {
|
||||
const srcStat = await fs.promises.stat(file);
|
||||
const outPath = path.join(outputDir, rel.replace(/\.md$/, '.html'));
|
||||
if (fs.existsSync(outPath)) {
|
||||
const outStat = await fs.promises.stat(outPath);
|
||||
if (srcStat.mtimeMs <= outStat.mtimeMs) {
|
||||
continue; // skip unchanged
|
||||
}
|
||||
}
|
||||
let raw = await fs.promises.readFile(file, 'utf8');
|
||||
const mdObj = await runHook('onParseMarkdown', { file: rel, content: raw });
|
||||
if (mdObj && mdObj.content) raw = mdObj.content;
|
||||
const parsed = matter(raw);
|
||||
const tokens = lexer(parsed.content || '');
|
||||
const firstHeading = tokens.find(t => t.type === 'heading');
|
||||
const title = parsed.data.title || (firstHeading ? firstHeading.text : path.basename(rel, '.md'));
|
||||
const headings = tokens.filter(t => t.type === 'heading').map(t => t.text).join(' ');
|
||||
const htmlBody = require('marked').parse(parsed.content || '');
|
||||
const bodyText = htmlBody.replace(/<[^>]+>/g, ' ');
|
||||
pages.push({ file: rel, data: { ...parsed.data, title } });
|
||||
searchDocs.push({ id: rel.replace(/\.md$/, '.html'), url: '/' + rel.replace(/\.md$/, '.html'), title, headings, body: bodyText });
|
||||
} else {
|
||||
assets.push(rel);
|
||||
}
|
||||
}
|
||||
|
||||
const nav = buildNav(pages);
|
||||
await fs.promises.mkdir(outputDir, { recursive: true });
|
||||
await fs.promises.writeFile(path.join(outputDir, 'navigation.json'), JSON.stringify(nav, null, 2));
|
||||
await fs.promises.writeFile(path.join(outputDir, 'config.json'), JSON.stringify(config, null, 2));
|
||||
|
||||
const searchIndex = lunr(function() {
|
||||
this.ref('id');
|
||||
this.field('title');
|
||||
this.field('headings');
|
||||
this.field('body');
|
||||
searchDocs.forEach(d => this.add(d));
|
||||
});
|
||||
await fs.promises.writeFile(
|
||||
path.join(outputDir, 'search-index.json'),
|
||||
JSON.stringify({ index: searchIndex.toJSON(), docs: searchDocs }, null, 2)
|
||||
);
|
||||
|
||||
const nunjucks = require('nunjucks');
|
||||
const env = new nunjucks.Environment(
|
||||
new nunjucks.FileSystemLoader('templates')
|
||||
);
|
||||
env.addGlobal('navigation', nav);
|
||||
env.addGlobal('config', config);
|
||||
|
||||
for (const page of pages) {
|
||||
const outPath = path.join(outputDir, page.file.replace(/\.md$/, '.html'));
|
||||
await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
|
||||
const srcPath = path.join(contentDir, page.file);
|
||||
const raw = await fs.promises.readFile(srcPath, 'utf8');
|
||||
const { content, data } = matter(raw);
|
||||
const body = require('marked').parse(content);
|
||||
|
||||
const pageContext = {
|
||||
title: data.title || page.data.title,
|
||||
content: body,
|
||||
page: { url: '/' + page.file.replace(/\.md$/, '.html') }
|
||||
};
|
||||
|
||||
let html = env.render('layout.njk', pageContext);
|
||||
const result = await runHook('onPageRendered', { file: page.file, html });
|
||||
if (result && result.html) html = result.html;
|
||||
await fs.promises.writeFile(outPath, html);
|
||||
}
|
||||
|
||||
|
||||
for (const asset of assets) {
|
||||
const srcPath = path.join(contentDir, asset);
|
||||
const destPath = path.join(outputDir, asset);
|
||||
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
||||
try {
|
||||
const sharp = require('sharp');
|
||||
if (/(png|jpg|jpeg)/i.test(path.extname(asset))) {
|
||||
await sharp(srcPath).toFile(destPath);
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
// sharp not installed, fallback
|
||||
}
|
||||
await fs.promises.copyFile(srcPath, destPath);
|
||||
}
|
||||
|
||||
// Copy the main assets directory (theme, js, etc.)
|
||||
// Always resolve assets relative to the Archivox package so it works
|
||||
// regardless of the current working directory or config location.
|
||||
const mainAssetsSrc = path.resolve(__dirname, '../../assets');
|
||||
const mainAssetsDest = path.join(outputDir, 'assets');
|
||||
|
||||
if (fs.existsSync(mainAssetsSrc)) {
|
||||
console.log(`Copying main assets from ${mainAssetsSrc} to ${mainAssetsDest}`);
|
||||
// Use fs.promises.cp for modern Node.js, it's like `cp -R`
|
||||
await fs.promises.cp(mainAssetsSrc, mainAssetsDest, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { generate, buildNav };
|
||||
|
||||
if (require.main === module) {
|
||||
generate().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
6
docs/starter/config.yaml
Normal file
6
docs/starter/config.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
site:
|
||||
title: "Archivox Docs"
|
||||
description: "Simple static docs."
|
||||
|
||||
navigation:
|
||||
search: true
|
3
docs/starter/content/01-getting-started/01-install.md
Normal file
3
docs/starter/content/01-getting-started/01-install.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Install
|
||||
|
||||
Run `npm install` then `npm run build` to generate your site.
|
3
docs/starter/content/01-getting-started/index.md
Normal file
3
docs/starter/content/01-getting-started/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Getting Started
|
||||
|
||||
This section helps you begin with Archivox.
|
3
docs/starter/content/index.md
Normal file
3
docs/starter/content/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Welcome to Archivox
|
||||
|
||||
This is your new documentation site. Start editing files in the `content/` folder.
|
11
docs/starter/package.json
Normal file
11
docs/starter/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "my-archivox-site",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "eleventy --serve",
|
||||
"build": "node node_modules/archivox/src/generator/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"archivox": "*"
|
||||
}
|
||||
}
|
23
docs/templates/layout.njk
vendored
Normal file
23
docs/templates/layout.njk
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="{% if config.theme.darkMode %}dark{% else %}light{% endif %}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ title | default(config.site.title) }}</title>
|
||||
<link rel="stylesheet" href="/assets/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
{% include "partials/header.njk" %}
|
||||
<div id="sidebar-overlay" class="sidebar-overlay"></div>
|
||||
<div class="container">
|
||||
{% include "partials/sidebar.njk" %}
|
||||
<main id="content">
|
||||
<nav id="breadcrumbs" class="breadcrumbs"></nav>
|
||||
{{ content | safe }}
|
||||
</main>
|
||||
</div>
|
||||
{% include "partials/footer.njk" %}
|
||||
<script src="/assets/lunr.js"></script>
|
||||
<script src="/assets/theme.js"></script>
|
||||
</body>
|
||||
</html>
|
14
docs/templates/partials/footer.njk
vendored
Normal file
14
docs/templates/partials/footer.njk
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<footer class="footer">
|
||||
{% if config.footer.links %}
|
||||
<nav class="footer-links">
|
||||
{% for link in config.footer.links %}
|
||||
<a href="{{ link.url }}">{{ link.text }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
<p>© {{ config.site.title }}</p>
|
||||
<div class="footer-permanent-links">
|
||||
<a href="https://github.com/PR0M3TH3AN/Archivox">GitHub</a>
|
||||
<a href="https://nostrtipjar.netlify.app/?n=npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx">Tip Jar</a>
|
||||
</div>
|
||||
</footer>
|
7
docs/templates/partials/header.njk
vendored
Normal file
7
docs/templates/partials/header.njk
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<header class="header">
|
||||
<button id="sidebar-toggle" class="sidebar-toggle" aria-label="Toggle navigation">☰</button>
|
||||
<a href="/" class="logo">{{ config.site.title }}</a>
|
||||
<input id="search-input" class="search-input" type="search" placeholder="Search..." aria-label="Search" />
|
||||
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode">🌓</button>
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</header>
|
29
docs/templates/partials/sidebar.njk
vendored
Normal file
29
docs/templates/partials/sidebar.njk
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{% macro renderNav(items, pageUrl) %}
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% if item.children and item.children.length %}
|
||||
{% set sectionPath = item.path | replace('index.html', '') %}
|
||||
<details class="nav-section" {% if pageUrl.startsWith(sectionPath) %}open{% endif %}>
|
||||
<summary>
|
||||
<a href="{{ item.path }}" class="nav-link{% if item.path === pageUrl %} active{% endif %}">
|
||||
{{ item.displayName or item.page.title }}
|
||||
</a>
|
||||
</summary>
|
||||
{{ renderNav(item.children, pageUrl) }}
|
||||
</details>
|
||||
{% else %}
|
||||
<a href="{{ item.path }}" class="nav-link{% if item.path === pageUrl %} active{% endif %}">
|
||||
{{ item.displayName or item.page.title }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<nav>
|
||||
{{ renderNav(navigation, page.url) }}
|
||||
</nav>
|
||||
</aside>
|
Reference in New Issue
Block a user