From f9e323f70a98f4bfc9ccc39d74074247a2b810df Mon Sep 17 00:00:00 2001 From: wu2kong Date: Fri, 29 May 2026 12:46:34 +0800 Subject: [PATCH] Add gitignore and initial project files --- .gitignore | 39 + README.md | 177 +++++ bin/webook.js | 7 + package-lock.json | 1146 +++++++++++++++++++++++++++++ package.json | 31 + public/app.js | 438 +++++++++++ public/highlight-dark.css | 39 + public/highlight-light.css | 39 + public/live-reload.js | 77 ++ public/style.css | 411 +++++++++++ src/cli.js | 133 ++++ src/renderer.js | 70 ++ src/scanner.js | 112 +++ src/server.js | 235 ++++++ src/watcher.js | 82 +++ test-notes/README.md | 9 + test-notes/工具/Git常用命令.md | 35 + test-notes/编程/JavaScript基础.md | 64 ++ test-notes/编程/Python笔记.md | 43 ++ test-notes/阅读/如何阅读一本书.md | 29 + 20 files changed, 3216 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bin/webook.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/app.js create mode 100644 public/highlight-dark.css create mode 100644 public/highlight-light.css create mode 100644 public/live-reload.js create mode 100644 public/style.css create mode 100644 src/cli.js create mode 100644 src/renderer.js create mode 100644 src/scanner.js create mode 100644 src/server.js create mode 100644 src/watcher.js create mode 100644 test-notes/README.md create mode 100644 test-notes/工具/Git常用命令.md create mode 100644 test-notes/编程/JavaScript基础.md create mode 100644 test-notes/编程/Python笔记.md create mode 100644 test-notes/阅读/如何阅读一本书.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c823726 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Runtime data +*.pid +*.pid.lock +*.seed +*.out +*.err + +# Coverage / build output +coverage/ +dist/ +build/ +tmp/ +temp/ +.cache/ +.turbo/ +.parcel-cache/ +.vite/ + +# Environment files +.env +.env.* +!.env.example + +# Editor and OS files +.DS_Store +Thumbs.db +.vscode/ +.idea/ +*.swp +*.swo diff --git a/README.md b/README.md new file mode 100644 index 0000000..16592f8 --- /dev/null +++ b/README.md @@ -0,0 +1,177 @@ +# webook + +📖 一个命令行工具,将 Markdown 笔记目录渲染为 Web 预览页面。 + +## 特性 + +- **双栏布局** — 左侧文件导航树 + 右侧内容预览 +- **自动导航** — 启动时递归扫描目录结构,生成可折叠的文件树 +- **元信息展示** — 显示笔记标题、创建时间、修改时间、字数统计 +- **浅/暗主题** — 一键切换,主题偏好自动保存到 localStorage +- **可隐藏侧边栏** — 专注于阅读内容 +- **搜索过滤** — 侧边栏顶部搜索框,快速过滤文件(快捷键 `Ctrl+K` / `Cmd+K`) +- **代码高亮** — 基于 highlight.js,支持 190+ 编程语言 +- **本地图片** — 自动重写相对路径图片链接,支持本地静态资源 +- **热更新** — 文件变更时自动刷新浏览器(WebSocket) +- **响应式设计** — 移动端自动适配 + +## 安装 + +```bash +npm install -g webook +``` + +或本地开发: + +```bash +git clone +cd webook +npm install +npm link # 将 webook 链接到全局 +``` + +## 使用 + +### 基本用法 + +```bash +# 预览当前目录下的 Markdown 笔记 +webook + +# 预览指定目录 +webook ~/my-notes + +# 指定端口 +webook ~/my-notes --port=3000 + +# 指定监听地址和网站标题 +webook ~/my-notes --host=0.0.0.0 --title="我的知识库" +``` + +### 参数说明 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `` | 笔记根目录路径 | 当前目录 `.` | +| `--port, -p ` | 监听端口(被占用时自动递增) | `8000` | +| `--host, -h
` | 监听地址 | `127.0.0.1` | +| `--title, -t ` | 网站标题 | 根目录名 | +| `--theme <path>` | 自定义 CSS 主题文件路径 | 无 | +| `--open` | 启动后自动打开浏览器 | `false` | +| `--version, -V` | 显示版本号 | — | + +### 示例 + +```bash +# 最小化使用 +webook + +# 完整参数 +webook ~/Documents/notes --port=8080 --host=0.0.0.0 --title="我的笔记" --open + +# 使用自定义主题 +webook ./docs --theme=./my-theme.css +``` + +## 页面功能 + +### 侧边栏 + +- **文件树**:自动递归生成,目录可折叠/展开 +- **搜索**:输入关键词实时过滤文件,按 `Esc` 清空搜索 +- **隐藏**:点击工具栏 `☰` 按钮或使用快捷键 + +### 内容区 + +- 显示笔记标题、创建时间、修改时间、字数 +- 支持 Markdown 全部语法:标题、表格、代码块、引用、列表、任务列表等 +- 代码块自动语法高亮 + +### 主题切换 + +- 点击工具栏 `🌙` / `☀️` 按钮切换浅/暗主题 +- 选择会自动保存,下次打开保持 + +### 移动端 + +- 在手机上浏览时,侧边栏默认隐藏 +- 点击右下角浮动按钮打开侧边栏 +- 点击遮罩层或选文件后自动关闭 + +## 自定义主题 + +通过 `--theme` 参数传入外部 CSS 文件路径(支持相对路径和绝对路径),CSS 文件必须是有效的 CSS。建议参考 `public/style.css` 中的 CSS 变量: + +```css +:root { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --text-primary: #212529; + --text-secondary: #6c757d; + --border-color: #dee2e6; + --accent-color: #4a90d9; + /* ... */ +} + +[data-theme="dark"] { + --bg-primary: #1a1b1e; + --text-primary: #c1c2c5; + /* ... */ +} +``` + +## 目录结构要求 + +webook 只扫描 `.md` 和 `.markdown` 文件。目录推荐按主题分组: + +``` +my-notes/ +├── README.md # 首页(如果存在会自动加载) +├── 编程/ +│ ├── JavaScript.md +│ └── Python.md +├── 阅读/ +│ └── 读书笔记.md +└── 工具/ + └── Git常用命令.md +``` + +- 隐藏文件(`.` 开头)和 `node_modules` 会被自动跳过 +- 目录和文件均按名称字母排序(中文按拼音) + +## npm 发布 + +要将 webook 发布到 npm(例如 `@wu2kong/webook`),修改 `package.json` 中的 `name` 字段: + +```json +{ + "name": "@wu2kong/webook", + ... +} +``` + +然后: + +```bash +npm login +npm publish --access public +``` + +发布后,用户可以通过以下命令安装: + +```bash +npm install -g @wu2kong/webook +``` + +## 技术栈 + +- **后端**: Node.js + Express +- **Markdown 渲染**: markdown-it +- **代码高亮**: highlight.js +- **文件监听**: chokidar +- **热更新**: WebSocket (ws) +- **CLI**: commander + +## License + +MIT diff --git a/bin/webook.js b/bin/webook.js new file mode 100755 index 0000000..faf8f77 --- /dev/null +++ b/bin/webook.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +'use strict'; +const { main } = require('../src/cli'); +main().catch((err) => { + console.error('Fatal error:', err.message); + process.exit(1); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..848b5db --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1146 @@ +{ + "name": "webook", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webook", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "commander": "^12.1.0", + "express": "^4.21.0", + "highlight.js": "^11.10.0", + "markdown-it": "^14.1.0", + "ws": "^8.18.0" + }, + "bin": { + "webook": "bin/webook.js" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.1", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f2d4dac --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "webook", + "version": "1.0.0", + "description": "A CLI tool to preview markdown notes in a web browser with live reload", + "main": "src/server.js", + "bin": { + "webook": "./bin/webook.js" + }, + "type": "commonjs", + "files": [ + "bin/", + "src/", + "views/", + "public/" + ], + "scripts": { + "start": "node bin/webook.js", + "dev": "node bin/webook.js . --port=8000" + }, + "keywords": ["markdown", "notes", "preview", "cli", "web", "live-reload"], + "author": "wu2kong", + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "commander": "^12.1.0", + "express": "^4.21.0", + "highlight.js": "^11.10.0", + "markdown-it": "^14.1.0", + "ws": "^8.18.0" + } +} diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..eceb922 --- /dev/null +++ b/public/app.js @@ -0,0 +1,438 @@ +(function () { + 'use strict'; + + const DATA = window.__WEBOOK__; + const navTree = DATA.nav || []; + const defaultFile = DATA.defaultFile; + const initialContent = DATA.initialContent; + + // === DOM Elements === + const sidebarNav = document.getElementById('sidebarNav'); + const searchInput = document.getElementById('searchInput'); + const btnToggleSidebar = document.getElementById('btnToggleSidebar'); + const btnToggleTheme = document.getElementById('btnToggleTheme'); + const contentBody = document.getElementById('contentBody'); + const emptyState = document.getElementById('emptyState'); + const markdownContent = document.getElementById('markdownContent'); + const articleTitle = document.getElementById('articleTitle'); + const articleMeta = document.getElementById('articleMeta'); + const articleBody = document.getElementById('articleBody'); + const tocNav = document.getElementById('tocNav'); + const tocPanel = document.getElementById('tocPanel'); + const loadingBar = document.getElementById('loadingBar'); + const sidebar = document.getElementById('sidebar'); + const mobileNavBtn = document.getElementById('mobileNavBtn'); + + let currentFile = null; + + // === Theme === + function getTheme() { + return localStorage.getItem('webook-theme') || 'light'; + } + + function setTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem('webook-theme', theme); + btnToggleTheme.textContent = theme === 'dark' ? '☀︎' : '☾'; + + // Swap highlight theme + const hlLink = document.querySelector('.theme-highlight'); + if (hlLink) { + hlLink.href = theme === 'dark' + ? '/_webook/highlight-dark.css' + : '/_webook/highlight-light.css'; + } + } + + function toggleTheme() { + const current = getTheme(); + setTheme(current === 'light' ? 'dark' : 'light'); + } + + // Initialize theme + setTheme(getTheme()); + + // === Sidebar Toggle === + function getSidebarVisible() { + return localStorage.getItem('webook-sidebar') !== 'hidden'; + } + + function setSidebarVisible(visible) { + if (visible) { + sidebar.classList.remove('collapsed'); + localStorage.setItem('webook-sidebar', 'visible'); + btnToggleSidebar.textContent = '☰'; + } else { + sidebar.classList.add('collapsed'); + localStorage.setItem('webook-sidebar', 'hidden'); + btnToggleSidebar.textContent = '☷'; + } + } + + function toggleSidebar() { + setSidebarVisible(sidebar.classList.contains('collapsed')); + } + + // Initialize sidebar state + setSidebarVisible(getSidebarVisible()); + + // === Mobile Sidebar === + let sidebarOverlay = null; + + function createOverlay() { + if (sidebarOverlay) return; + sidebarOverlay = document.createElement('div'); + sidebarOverlay.className = 'sidebar-overlay'; + sidebarOverlay.addEventListener('click', hideMobileSidebar); + document.body.appendChild(sidebarOverlay); + } + + function showMobileSidebar() { + createOverlay(); + sidebar.classList.add('mobile-visible'); + sidebar.classList.remove('collapsed'); + sidebarOverlay.classList.add('visible'); + } + + function hideMobileSidebar() { + sidebar.classList.remove('mobile-visible'); + if (sidebarOverlay) { + sidebarOverlay.classList.remove('visible'); + } + // Restore collapsed state if needed + if (!getSidebarVisible()) { + sidebar.classList.add('collapsed'); + } + } + + // === Navigation Rendering === + function renderNavTree(items, level = 0) { + if (!items || items.length === 0) return ''; + + let html = '<ul class="nav-tree">'; + for (const item of items) { + if (item.type === 'dir') { + html += ` + <li class="nav-dir collapsed" data-path="${escapeHtml(item.path)}"> + <div class="nav-dir-header" data-dir-path="${escapeHtml(item.path)}"> + <span class="nav-dir-arrow">▾</span> + <span class="nav-dir-icon" aria-hidden="true"></span> + <span class="nav-dir-name">${escapeHtml(item.name)}</span> + </div> + <div class="nav-dir-children"> + ${renderNavTree(item.children, level + 1)} + </div> + </li>`; + } else { + const isActive = item.path === defaultFile; + html += ` + <li class="nav-file${isActive ? ' active' : ''}" data-file-path="${escapeHtml(item.path)}"> + <span class="nav-file-icon" aria-hidden="true"></span> + <span class="nav-file-name">${escapeHtml(item.meta ? item.meta.title : item.name)}</span> + </li>`; + } + } + html += '</ul>'; + return html; + } + + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + // Render nav + sidebarNav.innerHTML = renderNavTree(navTree); + + // === Navigation Events === + sidebarNav.addEventListener('click', function (e) { + // Directory toggle + const dirHeader = e.target.closest('.nav-dir-header'); + if (dirHeader) { + const dirItem = dirHeader.parentElement; + dirItem.classList.toggle('collapsed'); + return; + } + + // File click + const fileItem = e.target.closest('.nav-file'); + if (fileItem) { + const filePath = fileItem.getAttribute('data-file-path'); + loadFile(filePath); + setActiveFile(fileItem); + // On mobile, hide sidebar after selection + if (window.innerWidth <= 768) { + hideMobileSidebar(); + } + } + }); + + function setActiveFile(fileItem) { + const prev = sidebarNav.querySelector('.nav-file.active'); + if (prev) prev.classList.remove('active'); + if (fileItem) fileItem.classList.add('active'); + } + + // === Search === + searchInput.addEventListener('input', function () { + const query = this.value.trim().toLowerCase(); + + if (!query) { + // Show all + sidebarNav.querySelectorAll('.nav-hidden').forEach(el => el.classList.remove('nav-hidden')); + removeEmptyMessage(); + return; + } + + let hasVisible = false; + + // Process directories + const dirs = sidebarNav.querySelectorAll('.nav-dir'); + dirs.forEach(dir => { + let dirHasVisible = false; + + const files = dir.querySelectorAll('.nav-file'); + files.forEach(file => { + const name = file.querySelector('.nav-file-name').textContent.toLowerCase(); + if (name.includes(query)) { + file.classList.remove('nav-hidden'); + dirHasVisible = true; + } else { + file.classList.add('nav-hidden'); + } + }); + + // Show/hide directory + if (dirHasVisible) { + dir.classList.remove('nav-hidden', 'collapsed'); + hasVisible = true; + } else { + dir.classList.add('nav-hidden'); + } + }); + + // Root-level files + const rootFiles = sidebarNav.querySelectorAll(':scope > .nav-tree > .nav-file'); + rootFiles.forEach(file => { + const name = file.querySelector('.nav-file-name').textContent.toLowerCase(); + if (name.includes(query)) { + file.classList.remove('nav-hidden'); + hasVisible = true; + } else { + file.classList.add('nav-hidden'); + } + }); + + if (!hasVisible) { + showEmptyMessage('未找到匹配的文件'); + } else { + removeEmptyMessage(); + } + }); + + function showEmptyMessage(msg) { + removeEmptyMessage(); + const div = document.createElement('div'); + div.className = 'nav-empty'; + div.id = 'navEmptyMsg'; + div.textContent = msg; + sidebarNav.appendChild(div); + } + + function removeEmptyMessage() { + const existing = document.getElementById('navEmptyMsg'); + if (existing) existing.remove(); + } + + // === File Loading === + async function loadFile(filePath) { + if (currentFile === filePath) return; + + currentFile = filePath; + showLoading(); + + try { + const resp = await fetch('/api/file?path=' + encodeURIComponent(filePath)); + const json = await resp.json(); + + if (!json.success) { + throw new Error(json.error || 'Failed to load file'); + } + + const data = json.data; + + // Update content + emptyState.style.display = 'none'; + markdownContent.style.display = ''; + + articleTitle.textContent = data.title; + articleBody.innerHTML = data.html; + buildTOC(); + + // Update meta + const created = formatDate(data.meta.created); + const modified = formatDate(data.meta.modified); + const wordCount = data.meta.wordCount; + + articleMeta.innerHTML = ` + <span class="article-meta-item">📅 创建: ${created}</span> + <span class="article-meta-item">✏️ 修改: ${modified}</span> + <span class="article-meta-item">📝 字数: ${wordCount.toLocaleString()}</span> + `; + + // Scroll to top + contentBody.scrollTop = 0; + + // Update URL hash + if (history.pushState) { + history.pushState(null, '', '#' + encodeURIComponent(filePath)); + } + + // Apply syntax highlighting to code blocks in content + applyHighlighting(); + } catch (err) { + emptyState.style.display = 'flex'; + emptyState.querySelector('p').textContent = '加载失败: ' + err.message; + markdownContent.style.display = 'none'; + } finally { + hideLoading(); + } + } + + function showLoading() { + loadingBar.classList.add('visible'); + loadingBar.classList.remove('done'); + } + + function hideLoading() { + loadingBar.classList.add('done'); + setTimeout(() => { + loadingBar.classList.remove('visible', 'done'); + }, 350); + } + + function formatDate(isoStr) { + if (!isoStr) return '-'; + const d = new Date(isoStr); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const h = String(d.getHours()).padStart(2, '0'); + const min = String(d.getMinutes()).padStart(2, '0'); + return `${y}-${m}-${day} ${h}:${min}`; + } + + function applyHighlighting() { + // highlight.js is loaded via CDN or bundled, but we handle it server-side. + // If any code blocks need client-side highlighting, do it here. + // For now, server-side rendering handles highlighting. + } + + function slugifyHeading(text, index) { + return 'toc-' + text + .toLowerCase() + .replace(/['"`]/g, '') + .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-') + .replace(/^-+|-+$/g, '') + '-' + index; + } + + function buildTOC() { + if (!tocNav || !tocPanel) return; + const headings = articleBody.querySelectorAll('h2, h3, h4'); + if (!headings.length) { + tocPanel.classList.add('toc-empty'); + tocNav.innerHTML = '<div class="toc-empty-state">当前页面没有可用目录</div>'; + return; + } + + tocPanel.classList.remove('toc-empty'); + const items = []; + headings.forEach((heading, index) => { + const level = Number(heading.tagName.slice(1)); + if (!heading.id) { + heading.id = slugifyHeading(heading.textContent || 'section', index); + } + items.push({ + id: heading.id, + text: heading.textContent.trim(), + level + }); + }); + + tocNav.innerHTML = items.map((item) => ` + <a class="toc-link toc-level-${item.level}" href="#${escapeHtml(item.id)}" data-target="${escapeHtml(item.id)}"> + <span class="toc-bullet"></span> + <span class="toc-text">${escapeHtml(item.text)}</span> + </a> + `).join(''); + } + + // === Initial Load === + if (initialContent) { + emptyState.style.display = 'none'; + markdownContent.style.display = ''; + articleTitle.textContent = initialContent.title; + articleBody.innerHTML = initialContent.html; + buildTOC(); + + const created = formatDate(initialContent.meta.created); + const modified = formatDate(initialContent.meta.modified); + const wordCount = initialContent.meta.wordCount; + articleMeta.innerHTML = ` + <span class="article-meta-item">📅 创建: ${created}</span> + <span class="article-meta-item">✏️ 修改: ${modified}</span> + <span class="article-meta-item">📝 字数: ${wordCount.toLocaleString()}</span> + `; + + currentFile = defaultFile; + if (defaultFile && history.replaceState) { + history.replaceState(null, '', '#' + encodeURIComponent(defaultFile)); + } + } + + // === Hash-based navigation === + function handleHashChange() { + const hash = decodeURIComponent(window.location.hash.slice(1)); + if (hash && hash !== currentFile) { + loadFile(hash); + // Find and activate the nav item + const fileItem = sidebarNav.querySelector(`[data-file-path="${CSS.escape(hash)}"]`); + if (fileItem) { + setActiveFile(fileItem); + } + } + } + + window.addEventListener('hashchange', handleHashChange); + + // === Event Listeners === + btnToggleTheme.addEventListener('click', toggleTheme); + btnToggleSidebar.addEventListener('click', toggleSidebar); + mobileNavBtn.addEventListener('click', showMobileSidebar); + + // === Keyboard Shortcuts === + document.addEventListener('keydown', function (e) { + // Ctrl+K or Cmd+K: focus search + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + searchInput.focus(); + searchInput.select(); + } + + // Escape: clear search + if (e.key === 'Escape' && document.activeElement === searchInput) { + searchInput.value = ''; + searchInput.dispatchEvent(new Event('input')); + searchInput.blur(); + } + }); + + // Close mobile sidebar on Escape + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && sidebar.classList.contains('mobile-visible')) { + hideMobileSidebar(); + } + }); + +})(); diff --git a/public/highlight-dark.css b/public/highlight-dark.css new file mode 100644 index 0000000..12f0dc2 --- /dev/null +++ b/public/highlight-dark.css @@ -0,0 +1,39 @@ +/* Dark theme for highlight.js - GitHub Dark-like */ +.hljs { display: block; overflow-x: auto; padding: 0; background: transparent; color: #c9d1d9; } +.hljs-comment, +.hljs-quote { color: #8b949e; font-style: italic; } +.hljs-keyword, +.hljs-selector-tag, +.hljs-subst { color: #ff7b72; font-weight: bold; } +.hljs-number, +.hljs-literal, +.hljs-variable, +.hljs-template-variable, +.hljs-tag .hljs-attr { color: #79c0ff; } +.hljs-string, +.hljs-doctag { color: #a5d6ff; } +.hljs-title, +.hljs-section, +.hljs-selector-id { color: #d2a8ff; font-weight: bold; } +.hljs-subst { font-weight: normal; } +.hljs-type, +.hljs-class .hljs-title { color: #d2a8ff; font-weight: bold; } +.hljs-tag, +.hljs-name, +.hljs-attribute { color: #7ee787; font-weight: normal; } +.hljs-regexp, +.hljs-link { color: #a5d6ff; } +.hljs-symbol, +.hljs-bullet { color: #ffa657; } +.hljs-built_in, +.hljs-builtin-name { color: #79c0ff; } +.hljs-meta { color: #8b949e; font-weight: bold; } +.hljs-deletion { background: #490202; } +.hljs-addition { background: #04260f; } +.hljs-emphasis { font-style: italic; } +.hljs-strong { font-weight: bold; } + +/* Line numbers */ +.hljs-ln { border-collapse: collapse; } +.hljs-ln td { padding: 0; } +.hljs-ln-n { text-align: right; padding-right: 8px; color: #484f58; user-select: none; } diff --git a/public/highlight-light.css b/public/highlight-light.css new file mode 100644 index 0000000..79fa9ef --- /dev/null +++ b/public/highlight-light.css @@ -0,0 +1,39 @@ +/* Light theme for highlight.js - GitHub-like */ +.hljs { display: block; overflow-x: auto; padding: 0; background: transparent; color: #24292e; } +.hljs-comment, +.hljs-quote { color: #6a737d; font-style: italic; } +.hljs-keyword, +.hljs-selector-tag, +.hljs-subst { color: #d73a49; font-weight: bold; } +.hljs-number, +.hljs-literal, +.hljs-variable, +.hljs-template-variable, +.hljs-tag .hljs-attr { color: #005cc5; } +.hljs-string, +.hljs-doctag { color: #032f62; } +.hljs-title, +.hljs-section, +.hljs-selector-id { color: #6f42c1; font-weight: bold; } +.hljs-subst { font-weight: normal; } +.hljs-type, +.hljs-class .hljs-title { color: #6f42c1; font-weight: bold; } +.hljs-tag, +.hljs-name, +.hljs-attribute { color: #22863a; font-weight: normal; } +.hljs-regexp, +.hljs-link { color: #032f62; } +.hljs-symbol, +.hljs-bullet { color: #e36209; } +.hljs-built_in, +.hljs-builtin-name { color: #005cc5; } +.hljs-meta { color: #6a737d; font-weight: bold; } +.hljs-deletion { background: #ffeef0; } +.hljs-addition { background: #f0fff4; } +.hljs-emphasis { font-style: italic; } +.hljs-strong { font-weight: bold; } + +/* Line numbers */ +.hljs-ln { border-collapse: collapse; } +.hljs-ln td { padding: 0; } +.hljs-ln-n { text-align: right; padding-right: 8px; color: #959da5; user-select: none; } diff --git a/public/live-reload.js b/public/live-reload.js new file mode 100644 index 0000000..5ebfb02 --- /dev/null +++ b/public/live-reload.js @@ -0,0 +1,77 @@ +(function() { + 'use strict'; + + let socket; + let reconnectTimer; + let reconnectDelay = 1000; + + function connect() { + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = protocol + '//' + location.host + '/_ws'; + + try { + socket = new WebSocket(wsUrl); + + socket.onopen = function() { + console.log('[webook] Live reload connected'); + reconnectDelay = 1000; + }; + + socket.onmessage = function(event) { + try { + const msg = JSON.parse(event.data); + if (msg.type === 'reload') { + console.log('[webook] File changed, reloading...'); + // If a markdown file changed and we're currently viewing it, reload via API + if (msg.file && /\.(md|markdown)$/i.test(msg.file)) { + const currentFile = getCurrentFile(); + if (currentFile) { + reloadCurrentFile(); + } + } else { + // For CSS/js/images, do a full reload + location.reload(); + } + } + } catch (e) { + location.reload(); + } + }; + + socket.onclose = function() { + console.log('[webook] Live reload disconnected'); + scheduleReconnect(); + }; + + socket.onerror = function() { + socket.close(); + }; + } catch (e) { + scheduleReconnect(); + } + } + + function scheduleReconnect() { + clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(function() { + reconnectDelay = Math.min(reconnectDelay * 1.5, 30000); + connect(); + }, reconnectDelay); + } + + function getCurrentFile() { + const hash = decodeURIComponent(location.hash.slice(1)); + if (hash) return hash; + return window.__WEBOOK__ ? window.__WEBOOK__.defaultFile : null; + } + + function reloadCurrentFile() { + const hash = decodeURIComponent(location.hash.slice(1)); + if (!hash) return location.reload(); + + // Dispatch hashchange to trigger reload + window.dispatchEvent(new HashChangeEvent('hashchange')); + } + + connect(); +})(); diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..3b1fe1b --- /dev/null +++ b/public/style.css @@ -0,0 +1,411 @@ +:root { + --bg: #f6f7fb; + --bg-elevated: rgba(255, 255, 255, 0.86); + --bg-panel: #ffffff; + --bg-soft: #eef2f7; + --bg-hover: #e8eef7; + --text: #1f2937; + --text-muted: #6b7280; + --text-soft: #94a3b8; + --border: rgba(148, 163, 184, 0.22); + --border-strong: rgba(148, 163, 184, 0.35); + --brand: #3a6ff7; + --brand-strong: #2757d6; + --brand-soft: rgba(58, 111, 247, 0.12); + --code-bg: #0f172a; + --code-fg: #e5edf9; + --shadow-lg: 0 20px 50px rgba(15, 23, 42, 0.08); + --shadow-md: 0 10px 28px rgba(15, 23, 42, 0.08); + --shadow-sm: 0 4px 16px rgba(15, 23, 42, 0.06); + --radius-lg: 20px; + --radius-md: 14px; + --radius-sm: 10px; + --sidebar-width: 300px; + --toolbar-height: 68px; + --transition: 180ms ease; + --font-sans: "Inter", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif; + --font-mono: "SFMono-Regular", "SF Mono", "JetBrains Mono", "Menlo", monospace; +} + +[data-theme="dark"] { + --bg: #0b1120; + --bg-elevated: rgba(15, 23, 42, 0.82); + --bg-panel: #0f172a; + --bg-soft: #111827; + --bg-hover: #182033; + --text: #e5edf9; + --text-muted: #a3b2c6; + --text-soft: #6f8099; + --border: rgba(148, 163, 184, 0.16); + --border-strong: rgba(148, 163, 184, 0.26); + --brand: #7aa2ff; + --brand-strong: #93b4ff; + --brand-soft: rgba(122, 162, 255, 0.14); + --code-bg: #020617; + --code-fg: #dbeafe; + --shadow-lg: 0 24px 60px rgba(2, 6, 23, 0.34); + --shadow-md: 0 14px 32px rgba(2, 6, 23, 0.28); + --shadow-sm: 0 6px 18px rgba(2, 6, 23, 0.2); +} + +*, *::before, *::after { box-sizing: border-box; } +html, body { margin: 0; min-height: 100%; } +body { + font-family: var(--font-sans); + color: var(--text); + background: + radial-gradient(circle at top left, rgba(58, 111, 247, 0.12), transparent 30%), + radial-gradient(circle at top right, rgba(56, 189, 248, 0.10), transparent 26%), + linear-gradient(180deg, #fbfcff 0%, var(--bg) 100%); + overflow: hidden; +} +[data-theme="dark"] body { + background: + radial-gradient(circle at top left, rgba(58, 111, 247, 0.16), transparent 30%), + radial-gradient(circle at top right, rgba(56, 189, 248, 0.10), transparent 26%), + linear-gradient(180deg, #0b1120 0%, #09101c 100%); +} +a { color: var(--brand); text-decoration: none; } +a:hover { color: var(--brand-strong); } + +.app { + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + height: 100vh; +} + +.sidebar { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + padding: 18px 14px 18px 18px; + border-right: 1px solid var(--border); + background: var(--bg-elevated); + backdrop-filter: blur(18px); + box-shadow: inset -1px 0 0 rgba(255,255,255,0.3); +} +.sidebar.collapsed { margin-left: calc(-1 * var(--sidebar-width)); } +.sidebar-header { display: grid; gap: 14px; padding-bottom: 14px; } +.sidebar-brand { + display: flex; align-items: center; gap: 12px; + color: var(--text); + padding: 10px 12px; border-radius: var(--radius-md); +} +.sidebar-brand:hover { background: var(--bg-soft); } +.sidebar-brand-mark { + width: 36px; height: 36px; border-radius: 12px; + display: grid; place-items: center; + background: linear-gradient(135deg, var(--brand), #7c3aed); + color: #fff; font-weight: 700; box-shadow: var(--shadow-sm); +} +.sidebar-brand-text { display: grid; line-height: 1.15; } +.sidebar-brand-text strong { font-size: 15px; } +.sidebar-brand-text small { color: var(--text-muted); font-size: 12px; } +.search-box input { + width: 100%; border: 1px solid var(--border); + border-radius: 999px; padding: 11px 14px; + background: var(--bg-panel); color: var(--text); + outline: none; box-shadow: var(--shadow-sm); +} +.search-box input:focus { border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-soft); } +.search-box input::placeholder { color: var(--text-soft); } + +.sidebar-nav { flex: 1; overflow: auto; padding: 4px 2px 0 0; } +.nav-tree { list-style: none; margin: 0; padding: 0; } +.nav-tree ul { list-style: none; margin: 0; padding: 0; } +.nav-dir { margin-bottom: 4px; } +.nav-dir-header { + display: flex; align-items: center; gap: 8px; + padding: 10px 12px; border-radius: 12px; + color: var(--text-muted); font-size: 13px; font-weight: 600; + cursor: pointer; user-select: none; +} +.nav-dir-header:hover { background: var(--bg-soft); color: var(--text); } +.nav-dir-arrow { width: 14px; text-align: center; transition: transform var(--transition); color: var(--text-soft); } +.nav-dir.collapsed .nav-dir-arrow { transform: rotate(-90deg); } +.nav-dir-icon { + width: 12px; + height: 12px; + border-radius: 4px; + border: 1.5px solid currentColor; + position: relative; + flex: 0 0 auto; + opacity: 0.75; +} +.nav-dir-icon::after { + content: ''; + position: absolute; + right: -1px; + top: -1px; + width: 4px; + height: 4px; + border-top: 1.5px solid currentColor; + border-right: 1.5px solid currentColor; + border-radius: 0 4px 0 0; + transform: skew(12deg); +} +.nav-dir-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.nav-dir-children { overflow: hidden; padding-left: 10px; } +.nav-dir.collapsed .nav-dir-children { max-height: 0 !important; } + +.nav-file { + display: flex; align-items: center; gap: 10px; + margin: 2px 0; padding: 10px 12px 10px 28px; + border-radius: 12px; color: var(--text); + cursor: pointer; border: 1px solid transparent; +} +.nav-file:hover { background: var(--bg-soft); } +.nav-file.active { + background: linear-gradient(180deg, rgba(58,111,247,0.12), rgba(58,111,247,0.08)); + border-color: rgba(58,111,247,0.18); + box-shadow: 0 8px 18px rgba(58,111,247,0.08); +} +.nav-file-icon { + width: 9px; + height: 9px; + border-radius: 50%; + background: currentColor; + opacity: 0.45; + flex: 0 0 auto; +} +.nav-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; } +.nav-hidden { display: none !important; } +.nav-empty { + margin: 14px 10px; padding: 18px 14px; border-radius: 16px; + background: var(--bg-soft); color: var(--text-muted); text-align: center; +} + +.content { + min-width: 0; display: flex; flex-direction: column; + height: 100vh; overflow: hidden; +} +.content-toolbar { + height: var(--toolbar-height); + display: flex; align-items: center; justify-content: space-between; + gap: 16px; padding: 0 18px 0 22px; + background: var(--bg-elevated); border-bottom: 1px solid var(--border); + backdrop-filter: blur(18px); +} +.toolbar-breadcrumb { display: flex; align-items: center; gap: 10px; min-width: 0; } +.toolbar-kicker { + display: inline-flex; align-items: center; + padding: 5px 10px; border-radius: 999px; + background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 700; +} +.toolbar-divider, .toolbar-title { color: var(--text-muted); font-size: 14px; } +.toolbar-title { color: var(--text); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.toolbar-actions { display: flex; align-items: center; gap: 8px; } +.btn-icon { + width: 38px; height: 38px; border: 1px solid transparent; + border-radius: 12px; background: transparent; color: var(--text-muted); + cursor: pointer; display: grid; place-items: center; font-size: 16px; +} +.btn-icon:hover { background: var(--bg-soft); color: var(--text); border-color: var(--border); } + +.content-body { flex: 1; overflow: auto; } +.empty-state { + display: grid; place-items: center; min-height: 100%; + padding: 32px; +} +.empty-card { + max-width: 420px; width: 100%; + padding: 32px; border-radius: 24px; + background: var(--bg-elevated); border: 1px solid var(--border); + box-shadow: var(--shadow-lg); text-align: center; backdrop-filter: blur(18px); +} +.empty-icon { font-size: 48px; margin-bottom: 16px; } +.empty-card h2 { margin: 0 0 8px; font-size: 22px; } +.empty-card p { margin: 0; color: var(--text-muted); line-height: 1.7; } + +.markdown-body { + width: min(880px, calc(100% - 48px)); + margin: 0 auto; + padding: 36px 0 72px; +} +.article-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 230px; + gap: 24px; + align-items: start; +} +.article-header { + padding: 22px 24px; margin-bottom: 24px; + border: 1px solid var(--border); border-radius: 24px; + background: var(--bg-elevated); box-shadow: var(--shadow-md); backdrop-filter: blur(18px); +} +.article-title { + margin: 0 0 12px; font-size: clamp(30px, 4vw, 44px); line-height: 1.08; + letter-spacing: -0.03em; color: var(--text); +} +.article-meta { display: flex; flex-wrap: wrap; gap: 10px 16px; color: var(--text-muted); font-size: 13px; } +.article-meta-item { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 999px; background: var(--bg-soft); } +.article-body { + font-size: 16px; line-height: 1.85; color: var(--text); +} +.article-body > :first-child { margin-top: 0; } +.article-body h1, .article-body h2, .article-body h3, .article-body h4 { + letter-spacing: -0.02em; line-height: 1.25; scroll-margin-top: 96px; +} +.article-body h1 { font-size: 2rem; margin: 1.5em 0 0.6em; } +.article-body h2 { + font-size: 1.5rem; margin: 1.8em 0 0.8em; padding-bottom: 10px; + border-bottom: 1px solid var(--border); +} +.article-body h3 { font-size: 1.15rem; margin: 1.4em 0 0.65em; } +.article-body h4, .article-body h5, .article-body h6 { font-size: 1rem; margin: 1.2em 0 0.6em; } +.article-body p { margin: 0.85em 0; color: var(--text); } +.article-body a { text-decoration: underline; text-decoration-color: rgba(58,111,247,0.28); text-underline-offset: 2px; } +.article-body ul, .article-body ol { padding-left: 1.5em; margin: 0.9em 0; } +.article-body li { margin: 0.35em 0; } +.article-body blockquote { + margin: 1.2em 0; padding: 14px 18px; + border-left: 4px solid var(--brand); border-radius: 0 16px 16px 0; + background: var(--bg-soft); color: var(--text-muted); +} +.article-body code { + font-family: var(--font-mono); font-size: 0.92em; + padding: 0.18em 0.42em; border-radius: 8px; + background: rgba(148, 163, 184, 0.14); color: var(--text); +} +.article-body pre { + margin: 1.2em 0; padding: 18px 20px; + border-radius: 20px; overflow: auto; + background: var(--code-bg) !important; color: var(--code-fg); + border: 1px solid rgba(148, 163, 184, 0.12); + box-shadow: var(--shadow-md); +} +.article-body pre code { + padding: 0; background: transparent; color: inherit; font-size: 0.92rem; +} +.article-body table { + width: 100%; margin: 1.2em 0; border-collapse: collapse; + overflow: hidden; border-radius: 16px; border: 1px solid var(--border); +} +.article-body th, .article-body td { + padding: 11px 14px; border-bottom: 1px solid var(--border); text-align: left; +} +.article-body th { background: var(--bg-soft); font-weight: 700; } +.article-body tr:last-child td { border-bottom: 0; } +.article-body img { max-width: 100%; height: auto; border-radius: 18px; box-shadow: var(--shadow-sm); } +.article-body hr { margin: 2em 0; border: 0; border-top: 1px solid var(--border); } +.article-body input[type="checkbox"] { margin-right: 8px; } + +.toc-panel { display: none; position: sticky; top: 18px; } +.toc-card { + padding: 16px 14px; + border: 1px solid var(--border); + border-radius: 20px; + background: var(--bg-elevated); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(18px); +} +.toc-title { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 12px; +} +.toc-nav { + display: grid; + gap: 4px; +} +.toc-link { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 12px; + color: var(--text-muted); + font-size: 13px; + line-height: 1.35; +} +.toc-link:hover { + background: var(--bg-soft); + color: var(--text); +} +.toc-link.toc-level-3 { padding-left: 20px; font-size: 12.5px; } +.toc-link.toc-level-4 { padding-left: 30px; font-size: 12px; } +.toc-bullet { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; + opacity: 0.35; + flex: 0 0 auto; +} +.toc-empty-state { + color: var(--text-muted); + font-size: 13px; + padding: 6px 2px 2px; +} + +.loading-bar { + position: fixed; top: 0; left: 0; height: 3px; width: 0; + background: linear-gradient(90deg, var(--brand), #38bdf8); + z-index: 1000; pointer-events: none; transition: width 0.25s ease, opacity 0.2s ease; +} +.loading-bar.visible { width: 55%; } +.loading-bar.done { width: 100%; opacity: 0; } + +.mobile-nav-btn { display: none; } + +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.35); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; +} +::-webkit-scrollbar-thumb:hover { background: rgba(148, 163, 184, 0.5); background-clip: content-box; } + +@media (max-width: 900px) { + .app { grid-template-columns: 1fr; } + .sidebar { + position: fixed; inset: 0 auto 0 0; z-index: 220; + width: min(86vw, 320px); transform: translateX(-100%); + transition: transform 220ms ease; + box-shadow: var(--shadow-lg); + } + .sidebar.mobile-visible { transform: translateX(0); } + .sidebar.collapsed { margin-left: 0; } + .content-toolbar { padding-left: 18px; } + .toolbar-breadcrumb { min-width: 0; } + .markdown-body { width: min(100%, calc(100% - 28px)); padding-top: 20px; padding-bottom: 72px; } + .article-layout { grid-template-columns: minmax(0, 1fr); } + .toc-panel { display: none !important; } + .article-header { padding: 20px; } + .mobile-nav-btn { + display: grid; place-items: center; position: fixed; right: 18px; bottom: 18px; + width: 52px; height: 52px; border-radius: 18px; + border: 1px solid var(--border); background: var(--bg-elevated); + color: var(--text); box-shadow: var(--shadow-lg); z-index: 210; + backdrop-filter: blur(18px); + } +} + +[data-doc-theme="toc"] .content { + background: + radial-gradient(circle at top right, rgba(58, 111, 247, 0.08), transparent 28%), + transparent; +} +[data-doc-theme="toc"] .markdown-body { + width: min(1180px, calc(100% - 48px)); +} +[data-doc-theme="toc"] .article-layout { + grid-template-columns: minmax(0, 1fr) 240px; + gap: 28px; +} +[data-doc-theme="toc"] .toc-panel { display: block; } +[data-doc-theme="toc"] .toc-card { + position: sticky; + top: 18px; +} + +.sidebar-overlay { + display: none; position: fixed; inset: 0; z-index: 210; + background: rgba(2, 6, 23, 0.34); backdrop-filter: blur(4px); +} +@media (max-width: 900px) { + .sidebar-overlay.visible { display: block; } +} diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..8dee2de --- /dev/null +++ b/src/cli.js @@ -0,0 +1,133 @@ +'use strict'; + +const { program } = require('commander'); +const path = require('path'); +const http = require('http'); +const net = require('net'); +const { createServer } = require('./server'); +const { createWatcher } = require('./watcher'); + +/** + * Check if a port is available + */ +function isPortAvailable(port, host) { + return new Promise((resolve) => { + const server = net.createServer(); + server.once('error', () => resolve(false)); + server.once('listening', () => { + server.close(); + resolve(true); + }); + server.listen(port, host); + }); +} + +/** + * Find an available port starting from the given port + */ +async function findAvailablePort(startPort, host) { + let port = startPort; + while (!(await isPortAvailable(port, host))) { + port++; + if (port > startPort + 100) { + throw new Error('No available port found in range ' + startPort + '-' + (startPort + 100)); + } + } + return port; +} + +async function main() { + program + .name('webook') + .description('A CLI tool to preview markdown notes in a web browser') + .argument('[dir]', 'Root directory of your markdown notes', '.') + .option('-p, --port <number>', 'Port to listen on (auto-detect if not specified)', '0') + .option('-h, --host <address>', 'Host address to bind', '127.0.0.1') + .option('-t, --title <title>', 'Site title (defaults to directory name)') + .option('--theme <path>', 'Custom CSS theme file path') + .option('--open', 'Automatically open in browser', false) + .version(require('../package.json').version) + .parse(process.argv); + + const args = program.args; + const opts = program.opts(); + + // Resolve directory + const dir = path.resolve(args[0] || '.'); + + // Validate directory + const fs = require('fs'); + if (!fs.existsSync(dir)) { + console.error(`Error: Directory not found: ${dir}`); + process.exit(1); + } + if (!fs.statSync(dir).isDirectory()) { + console.error(`Error: Not a directory: ${dir}`); + process.exit(1); + } + + // Determine title + const title = opts.title || path.basename(dir); + + // Determine port + let port = parseInt(opts.port, 10); + if (!port || isNaN(port)) { + port = 8000; + } + port = await findAvailablePort(port, opts.host); + + // Theme + const theme = opts.theme || null; + + // Create Express app + const app = createServer({ dir, title, theme, port, host: opts.host }); + + // Create HTTP server + const server = http.createServer(app); + + // Create file watcher + const watcher = createWatcher(server, dir); + + // Start server + server.listen(port, opts.host, () => { + const url = `http://${opts.host === '0.0.0.0' ? 'localhost' : opts.host}:${port}`; + console.log(''); + console.log(' 📖 webook is running!'); + console.log(` 📂 Notes directory: ${dir}`); + console.log(` 🌐 Open: ${url}`); + console.log(` 🔄 Live reload: enabled`); + console.log(''); + console.log(' Press Ctrl+C to stop'); + console.log(''); + + // Auto open browser + if (opts.open) { + const { exec } = require('child_process'); + const platform = process.platform; + const cmd = platform === 'darwin' ? 'open' : + platform === 'win32' ? 'start' : 'xdg-open'; + exec(`${cmd} ${url}`); + } + }); + + // Graceful shutdown + function shutdown() { + console.log('\n Shutting down...'); + watcher.close(); + server.close(); + process.exit(0); + } + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +// Auto-execute when run directly (not required by another module) +if (!module.parent) { + main().catch((err) => { + console.error('Fatal error:', err.message); + process.exit(1); + }); +} + +module.exports = { main }; diff --git a/src/renderer.js b/src/renderer.js new file mode 100644 index 0000000..688f30f --- /dev/null +++ b/src/renderer.js @@ -0,0 +1,70 @@ +'use strict'; + +const MarkdownIt = require('markdown-it'); +const hljs = require('highlight.js'); +const path = require('path'); +const fs = require('fs'); + +/** + * Create a markdown renderer instance + */ +function createRenderer(notesDir) { + const md = new MarkdownIt({ + html: true, + linkify: true, + typographer: true, + breaks: false, + highlight: function (str, lang) { + if (lang && hljs.getLanguage(lang)) { + try { + return '<pre class="hljs"><code>' + + hljs.highlight(str, { language: lang, ignoreIllegals: true }).value + + '</code></pre>'; + } catch (__) {} + } + try { + return '<pre class="hljs"><code>' + + hljs.highlightAuto(str).value + + '</code></pre>'; + } catch (__) {} + return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'; + } + }); + + // Custom image renderer: rewrite local paths to /_notes/ URLs + const defaultImageRender = md.renderer.rules.image || function (tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + + md.renderer.rules.image = function (tokens, idx, options, env, self) { + const token = tokens[idx]; + const src = token.attrGet('src'); + if (src && !/^https?:\/\//i.test(src) && !src.startsWith('/') && !src.startsWith('data:')) { + const fileDir = env.filePath ? path.dirname(env.filePath) : ''; + const absSrc = path.resolve(notesDir, fileDir, src); + if (fs.existsSync(absSrc)) { + const relSrc = path.relative(notesDir, absSrc).split(path.sep).join('/'); + token.attrSet('src', '/_notes/' + relSrc); + } + } + return defaultImageRender(tokens, idx, options, env, self); + }; + + return { + /** + * Render markdown content to HTML + */ + render(content, env = {}) { + return md.render(content, env); + }, + + /** + * Get markdown-it instance (for direct use) + */ + getInstance() { + return md; + } + }; +} + +module.exports = { createRenderer }; diff --git a/src/scanner.js b/src/scanner.js new file mode 100644 index 0000000..2b7ec39 --- /dev/null +++ b/src/scanner.js @@ -0,0 +1,112 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +/** + * Extract title from markdown content (first h1 heading) + */ +function extractTitle(content, filename) { + const match = content.match(/^#\s+(.+)$/m); + return match ? match[1].trim() : filename.replace(/\.(md|markdown)$/i, ''); +} + +/** + * Count words: Chinese characters + English words + */ +function countWords(content) { + const chineseChars = (content.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g) || []).length; + const englishWords = (content.match(/[a-zA-Z0-9]+/g) || []).length; + return chineseChars + englishWords; +} + +function normalizeSortKey(name) { + const volumeMatch = name.match(/^(?:第)?卷\s*([一二三四五六七八九十百千0-9]+)$/) || name.match(/^卷\s*([一二三四五六七八九十百千0-9]+)$/); + const chapterMatch = name.match(/^第\s*([一二三四五六七八九十百千0-9]+)\s*[章节篇卷]$/); + const numericMatch = name.match(/(\d+)/); + const toNumber = (value) => { + if (/^\d+$/.test(value)) return parseInt(value, 10); + const map = { 一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9, 十: 10, 百: 100, 千: 1000 }; + if (value.length === 1) return map[value] || Number.MAX_SAFE_INTEGER; + if (value === '十') return 10; + const digits = value.split(''); + if (digits.length === 2 && digits[0] === '十') return 10 + (map[digits[1]] || 0); + if (digits.length === 2 && digits[1] === '十') return (map[digits[0]] || 0) * 10; + return value.split('').reduce((sum, ch) => sum + (map[ch] || 0), 0) || Number.MAX_SAFE_INTEGER; + }; + + if (volumeMatch) return { group: 0, value: toNumber(volumeMatch[1]), name }; + if (chapterMatch) return { group: 1, value: toNumber(chapterMatch[1]), name }; + if (numericMatch) return { group: 2, value: parseInt(numericMatch[1], 10), name }; + return { group: 3, value: name.toLocaleLowerCase('zh-CN'), name }; +} + +function compareNames(a, b) { + const ak = normalizeSortKey(a.name); + const bk = normalizeSortKey(b.name); + if (ak.group !== bk.group) return ak.group - bk.group; + if (ak.value !== bk.value) return ak.value < bk.value ? -1 : 1; + return a.name.localeCompare(b.name, 'zh-CN', { numeric: true, sensitivity: 'base' }); +} + +/** + * Recursively scan directory for markdown files + */ +function scanDirectory(rootPath, relativePath = '') { + const fullPath = path.join(rootPath, relativePath); + let entries; + try { + entries = fs.readdirSync(fullPath, { withFileTypes: true }); + } catch (err) { + return []; + } + + const result = []; + + for (const entry of entries) { + // Skip hidden files/dirs and node_modules + if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; + + const rel = relativePath ? path.posix.join(relativePath, entry.name) : entry.name; + + if (entry.isDirectory()) { + const children = scanDirectory(rootPath, rel); + if (children.length > 0) { + result.push({ + name: entry.name, + path: rel, + type: 'dir', + children + }); + } + } else if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) { + try { + const content = fs.readFileSync(path.join(fullPath, entry.name), 'utf-8'); + const stats = fs.statSync(path.join(fullPath, entry.name)); + result.push({ + name: entry.name, + path: rel, + type: 'file', + meta: { + title: extractTitle(content, entry.name), + created: stats.birthtime.toISOString(), + modified: stats.mtime.toISOString(), + wordCount: countWords(content) + } + }); + } catch (err) { + // Skip files that can't be read + } + } + } + + // Sort: directories first (alphabetical), then files (alphabetical) + result.sort((a, b) => { + if (a.type !== b.type) return a.type === 'dir' ? -1 : 1; + return compareNames(a, b); + }); + + return result; +} + +module.exports = { scanDirectory }; diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..108fcb8 --- /dev/null +++ b/src/server.js @@ -0,0 +1,235 @@ +'use strict'; + +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const { scanDirectory } = require('./scanner'); +const { createRenderer } = require('./renderer'); + +/** + * Generate the HTML page template + */ +function renderPage({ title, nav, theme, defaultFile, initialContent }) { + const navJson = JSON.stringify(nav).replace(/</g, '\\u003c'); + const isTocTheme = theme === 'toc'; + const themeLink = theme && !isTocTheme + ? `<link rel="stylesheet" href="/_notes/${theme.replace(/^\/+/, '')}">` + : ''; + const initialContentJson = initialContent + ? JSON.stringify(initialContent).replace(/</g, '\\u003c') + : 'null'; + + return `<!DOCTYPE html> +<html lang="zh-CN" data-theme="light" data-doc-theme="${isTocTheme ? 'toc' : 'default'}"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>${title} + + + ${themeLink} + + +
+
+
+ + W + + ${title} + 文档浏览器 + + +
+ +
+
+
+
+
+
+
+ Docs + / + ${title} +
+
+ + +
+
+
+
+
+
📘
+

选择左侧文件开始阅读

+

支持 Markdown、代码块、图片和基础排版。

+
+
+
+
+

+
+
+
+
+
+
+
目录
+
+
+
+
+
+
+
+
+
+
☰
+ + + + +`; +} + +/** + * Create and start the webook server + */ +function createServer(opts) { + const { + dir, + title, + theme, + port, + host + } = opts; + + const app = express(); + const renderer = createRenderer(dir); + + // Serve webook public assets (CSS, JS) + app.use('/_webook', express.static(path.join(__dirname, '..', 'public'))); + + // Serve notes directory as static files (for images, attachments, custom theme CSS) + app.use('/_notes', express.static(dir)); + + // API: Get navigation tree + app.get('/api/nav', (req, res) => { + try { + const nav = scanDirectory(dir); + res.json({ success: true, data: nav }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + }); + + // API: Get rendered file content + app.get('/api/file', (req, res) => { + try { + const filePath = req.query.path; + if (!filePath) { + return res.status(400).json({ success: false, error: 'Missing path parameter' }); + } + + // Security: prevent directory traversal + const safePath = path.normalize(filePath).replace(/^(\.\.(\/|\\|$))+/, ''); + const fullPath = path.join(dir, safePath); + + if (!fullPath.startsWith(path.resolve(dir))) { + return res.status(403).json({ success: false, error: 'Access denied' }); + } + + if (!fs.existsSync(fullPath)) { + return res.status(404).json({ success: false, error: 'File not found' }); + } + + if (!/\.(md|markdown)$/i.test(fullPath)) { + return res.status(400).json({ success: false, error: 'Not a markdown file' }); + } + + const content = fs.readFileSync(fullPath, 'utf-8'); + const stats = fs.statSync(fullPath); + + // Render markdown to HTML + const env = { filePath: safePath }; + const html = renderer.render(content, env); + + // Extract metadata + const match = content.match(/^#\s+(.+)$/m); + const title = match ? match[1].trim() : path.basename(filePath, path.extname(filePath)); + + // Count words + const chineseChars = (content.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g) || []).length; + const englishWords = (content.match(/[a-zA-Z0-9]+/g) || []).length; + + res.json({ + success: true, + data: { + title, + html, + meta: { + created: stats.birthtime.toISOString(), + modified: stats.mtime.toISOString(), + wordCount: chineseChars + englishWords, + fileName: path.basename(filePath) + } + } + }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + }); + + // Main page + app.get('/', (req, res) => { + try { + const nav = scanDirectory(dir); + // Try to find a README.md or index.md as default + const defaultFiles = ['README.md', 'readme.md', 'index.md', 'INDEX.md']; + let initialContent = null; + let defaultFile = null; + + for (const df of defaultFiles) { + const dfPath = path.join(dir, df); + if (fs.existsSync(dfPath)) { + defaultFile = df; + const content = fs.readFileSync(dfPath, 'utf-8'); + const env = { filePath: df }; + const html = renderer.render(content, env); + const stats = fs.statSync(dfPath); + const match = content.match(/^#\s+(.+)$/m); + const dfTitle = match ? match[1].trim() : df.replace(/\.md$/i, ''); + const chineseChars = (content.match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g) || []).length; + const englishWords = (content.match(/[a-zA-Z0-9]+/g) || []).length; + + initialContent = { + title: dfTitle, + html, + meta: { + created: stats.birthtime.toISOString(), + modified: stats.mtime.toISOString(), + wordCount: chineseChars + englishWords, + fileName: df + } + }; + break; + } + } + + res.send(renderPage({ title, nav, theme, defaultFile, initialContent })); + } catch (err) { + res.status(500).send(`

Error

${err.message}
`); + } + }); + + return app; +} + +module.exports = { createServer }; diff --git a/src/watcher.js b/src/watcher.js new file mode 100644 index 0000000..d586042 --- /dev/null +++ b/src/watcher.js @@ -0,0 +1,82 @@ +'use strict'; + +const chokidar = require('chokidar'); +const { WebSocketServer } = require('ws'); + +/** + * Create file watcher and WebSocket server for live reload + */ +function createWatcher(httpServer, dir) { + const wss = new WebSocketServer({ server: httpServer, path: '/_ws' }); + + // Track connected clients + const clients = new Set(); + + wss.on('connection', (ws) => { + clients.add(ws); + + ws.on('close', () => { + clients.delete(ws); + }); + + ws.on('error', () => { + clients.delete(ws); + }); + }); + + // Broadcast reload message to all clients + function broadcast(event) { + const msg = JSON.stringify(event); + for (const ws of clients) { + try { + if (ws.readyState === 1) { // WebSocket.OPEN + ws.send(msg); + } + } catch (e) { + // ignore + } + } + } + + // Watch for file changes + const watcher = chokidar.watch(dir, { + ignored: [ + /(^|[\/\\])\../, // dotfiles + /node_modules/, + /\.git/ + ], + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 300, + pollInterval: 100 + } + }); + + let debounceTimer; + + watcher.on('all', (event, filePath) => { + // Only trigger reload for markdown files and assets + if (/\.(md|markdown|css|js|png|jpg|jpeg|gif|svg|webp|ico)$/i.test(filePath)) { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + broadcast({ type: 'reload', file: filePath, event }); + }, 200); + } + }); + + watcher.on('error', (err) => { + console.error('Watcher error:', err.message); + }); + + return { + watcher, + wss, + close() { + watcher.close(); + wss.close(); + } + }; +} + +module.exports = { createWatcher }; diff --git a/test-notes/README.md b/test-notes/README.md new file mode 100644 index 0000000..a990c2e --- /dev/null +++ b/test-notes/README.md @@ -0,0 +1,9 @@ +# 我的笔记库 + +欢迎来到我的知识库!这里记录了我的学习笔记和思考。 + +## 目录 + +- [编程笔记](./编程/) +- [读书笔记](./阅读/) +- [工具使用](./工具/) diff --git a/test-notes/工具/Git常用命令.md b/test-notes/工具/Git常用命令.md new file mode 100644 index 0000000..e2c3e40 --- /dev/null +++ b/test-notes/工具/Git常用命令.md @@ -0,0 +1,35 @@ +# Git 常用命令 + +## 基础操作 + +| 命令 | 说明 | +|------|------| +| `git init` | 初始化仓库 | +| `git clone ` | 克隆仓库 | +| `git add .` | 暂存所有更改 | +| `git commit -m "msg"` | 提交更改 | +| `git push` | 推送到远程 | +| `git pull` | 拉取远程更新 | + +## 分支管理 + +```bash +# 创建并切换到新分支 +git checkout -b feature/new-feature + +# 查看所有分支 +git branch -a + +# 合并分支 +git merge feature/new-feature + +# 删除分支 +git branch -d feature/new-feature +``` + +## 撤销操作 + +- `git reset HEAD ` - 取消暂存 +- `git checkout -- ` - 撤销文件修改 +- `git revert ` - 撤销某次提交 +- `git reset --soft HEAD~1` - 撤销最近一次 commit(保留更改) diff --git a/test-notes/编程/JavaScript基础.md b/test-notes/编程/JavaScript基础.md new file mode 100644 index 0000000..ca649dc --- /dev/null +++ b/test-notes/编程/JavaScript基础.md @@ -0,0 +1,64 @@ +# JavaScript 基础 + +JavaScript 是一种轻量级的解释型编程语言,支持面向对象、命令式和声明式编程。 + +## 数据类型 + +JavaScript 有以下几种基本数据类型: + +- `Number` - 数字 +- `String` - 字符串 +- `Boolean` - 布尔值 +- `null` - 空值 +- `undefined` - 未定义 +- `Symbol` - 符号(ES6) +- `BigInt` - 大整数 + +## 代码示例 + +```javascript +// Hello World +function greet(name) { + return `Hello, ${name}!`; +} + +console.log(greet('World')); + +// 箭头函数 +const add = (a, b) => a + b; +console.log(add(1, 2)); // 3 +``` + +## Promise 和 async/await + +```javascript +// Promise 示例 +function fetchData(url) { + return fetch(url) + .then(response => response.json()) + .then(data => console.log(data)) + .catch(error => console.error('Error:', error)); +} + +// async/await 写法 +async function fetchDataAsync(url) { + try { + const response = await fetch(url); + const data = await response.json(); + console.log(data); + } catch (error) { + console.error('Error:', error); + } +} +``` + +## 数组方法 + +| 方法 | 描述 | 返回值 | +|------|------|--------| +| `map()` | 映射每个元素 | 新数组 | +| `filter()` | 过滤元素 | 新数组 | +| `reduce()` | 累加计算 | 单个值 | +| `forEach()` | 遍历元素 | undefined | + +> **提示**: 使用现代 JavaScript 特性可以让代码更简洁易读。 diff --git a/test-notes/编程/Python笔记.md b/test-notes/编程/Python笔记.md new file mode 100644 index 0000000..dd82951 --- /dev/null +++ b/test-notes/编程/Python笔记.md @@ -0,0 +1,43 @@ +# Python 学习笔记 + +Python 是一门优雅且强大的编程语言。 + +## 基本语法 + +```python +# 列表推导式 +squares = [x**2 for x in range(10)] +print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + +# 字典推导式 +word_lengths = {word: len(word) for word in ['hello', 'world', 'python']} +print(word_lengths) +``` + +## 装饰器 + +```python +def timer(func): + def wrapper(*args, **kwargs): + import time + start = time.time() + result = func(*args, **kwargs) + print(f"{func.__name__} took {time.time() - start:.4f}s") + return result + return wrapper + +@timer +def slow_function(): + import time + time.sleep(1) + return "Done" + +print(slow_function()) +``` + +## 常用库 + +1. **NumPy** - 科学计算 +2. **Pandas** - 数据分析 +3. **Flask** - Web 框架 +4. **Requests** - HTTP 请求 diff --git a/test-notes/阅读/如何阅读一本书.md b/test-notes/阅读/如何阅读一本书.md new file mode 100644 index 0000000..4bfa22d --- /dev/null +++ b/test-notes/阅读/如何阅读一本书.md @@ -0,0 +1,29 @@ +# 如何阅读一本书 + +> 阅读是一门艺术,需要刻意练习。 + +## 阅读的四个层次 + +### 1. 基础阅读 +这个层次解决的是"这个句子在说什么?"的问题。 + +### 2. 检视阅读 +在一定时间内,抓住一本书的重点。 + +- **有系统的略读**: 看书名、序言、目录、索引 +- **粗浅的阅读**: 从头到尾先读一遍 + +### 3. 分析阅读 +全盘、完整、优质的阅读,追寻理解。 + +### 4. 主题阅读 +阅读多本书,列举它们之间的相关之处。 + +## 做笔记的方法 + +- 画底线 +- 在空白处做星号或其他符号 +- 在空白处编号 +- 在空白处记下其他页码 +- 将关键字或句子圈出来 +- 在书页空白处做笔记