leafdocs 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- leafdocs-0.3.0/PKG-INFO +261 -0
- leafdocs-0.3.0/README.md +235 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/pyproject.toml +3 -1
- leafdocs-0.3.0/src/leafdocs/app.py +347 -0
- leafdocs-0.3.0/src/leafdocs/static/reader-render.js +61 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/VERSIONS.md +71 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/LICENSE +21 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/contrib/auto-render.min.js +1 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/katex.min.css +1 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/katex/katex.min.js +1 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/mermaid/LICENSE +21 -0
- leafdocs-0.3.0/src/leafdocs/static/vendor/mermaid/mermaid.min.js +4328 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs/templates/index.html +83 -11
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs/templates/reader.html +45 -5
- leafdocs-0.3.0/src/leafdocs.egg-info/PKG-INFO +261 -0
- leafdocs-0.3.0/src/leafdocs.egg-info/SOURCES.txt +44 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs.egg-info/requires.txt +2 -0
- leafdocs-0.3.0/tests/test_app.py +819 -0
- leafdocs-0.1.0/PKG-INFO +0 -310
- leafdocs-0.1.0/README.md +0 -286
- leafdocs-0.1.0/src/leafdocs/app.py +0 -178
- leafdocs-0.1.0/src/leafdocs.egg-info/PKG-INFO +0 -310
- leafdocs-0.1.0/src/leafdocs.egg-info/SOURCES.txt +0 -16
- leafdocs-0.1.0/tests/test_app.py +0 -211
- {leafdocs-0.1.0 → leafdocs-0.3.0}/LICENSE +0 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/setup.cfg +0 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs/__init__.py +0 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs/static/favicon.ico +0 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs/templates/base.html +0 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs/templates/login.html +0 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs.egg-info/dependency_links.txt +0 -0
- {leafdocs-0.1.0 → leafdocs-0.3.0}/src/leafdocs.egg-info/top_level.txt +0 -0
leafdocs-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leafdocs
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Turn a directory of Markdown files into a self-hosted, searchable web reader.
|
|
5
|
+
Author-email: Anshul <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: markdown,docs,flask,self-hosted
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Framework :: Flask
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: flask>=3.0
|
|
16
|
+
Requires-Dist: markdown>=3.5
|
|
17
|
+
Requires-Dist: python-frontmatter>=1.1
|
|
18
|
+
Requires-Dist: bcrypt>=4.1
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0
|
|
20
|
+
Requires-Dist: nh3>=0.2.22
|
|
21
|
+
Requires-Dist: pymdown-extensions>=10.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-flask; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# leafdocs
|
|
28
|
+
|
|
29
|
+
A lightweight Python library that turns a directory of Markdown files into a self-hosted, searchable web reader.
|
|
30
|
+
|
|
31
|
+
Install it, point it at a folder, get a running Flask app you can deploy anywhere.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from leafdocs import LeafDocs
|
|
35
|
+
|
|
36
|
+
ld = LeafDocs(docs_dir="./docs")
|
|
37
|
+
ld.run()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install leafdocs
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Minimal setup
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from leafdocs import LeafDocs
|
|
56
|
+
|
|
57
|
+
ld = LeafDocs(docs_dir="./docs")
|
|
58
|
+
ld.run()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Visit `http://127.0.0.1:5000` to see a searchable index of all `.md` files in your `docs/` directory, including subfolders.
|
|
62
|
+
|
|
63
|
+
### Constructor arguments
|
|
64
|
+
|
|
65
|
+
| Argument | Type | Default | Description |
|
|
66
|
+
|--------------|-----------------|-----------------|--------------------------------------------------|
|
|
67
|
+
| `docs_dir` | `str` | `"./docs"` | Path to the directory containing `.md` files |
|
|
68
|
+
| `secret_key` | `str` or `None` | `None` | Flask session secret key (see Auth section) |
|
|
69
|
+
|
|
70
|
+
### Accessing the Flask app
|
|
71
|
+
|
|
72
|
+
The underlying Flask app is exposed as `ld.flask_app`. Use it to add routes, middleware, or blueprints:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from leafdocs import LeafDocs
|
|
76
|
+
|
|
77
|
+
ld = LeafDocs(docs_dir="./docs")
|
|
78
|
+
|
|
79
|
+
@ld.flask_app.route("/health")
|
|
80
|
+
def health():
|
|
81
|
+
return {"status": "ok"}
|
|
82
|
+
|
|
83
|
+
ld.run()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Frontmatter
|
|
89
|
+
|
|
90
|
+
Each `.md` file can optionally include YAML frontmatter. Supported fields:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
---
|
|
94
|
+
title: My Document Title
|
|
95
|
+
tags: [python, tutorial]
|
|
96
|
+
---
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Field | Type | Fallback |
|
|
100
|
+
|---------|--------------------------------|----------------------|
|
|
101
|
+
| `title` | string | Filename, titlecased |
|
|
102
|
+
| `tags` | list or comma-separated string | None |
|
|
103
|
+
|
|
104
|
+
Frontmatter is optional — files without it are discovered and served normally.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Folders and images
|
|
109
|
+
|
|
110
|
+
Subfolders under `docs_dir` are shown on the index as a collapsible tree, folders first, then docs. Folder names are titlecased the same way as filenames (`getting-started` becomes `Getting Started`). Folders with no `.md` files anywhere beneath them are left out. While the search box has text, matching docs are shown with their folders opened; clearing it restores the folders you had open.
|
|
111
|
+
|
|
112
|
+
A doc's URL is its path relative to `docs_dir` without `.md`, so `docs/guides/setup.md` is served at `/guides/setup`. Links ending in `.md`, such as `/guides/setup.md`, redirect to the same URL without it.
|
|
113
|
+
|
|
114
|
+
Skipped during discovery and returned as 404 by the reader:
|
|
115
|
+
|
|
116
|
+
- Any file or folder whose name starts with `.`, such as `.git/`
|
|
117
|
+
- Top-level `login.md`, `logout.md`, and anything under `static/`, since those paths belong to built-in routes. Nested files such as `guides/login.md` are fine.
|
|
118
|
+
|
|
119
|
+
Symlinks that point outside `docs_dir` are not listed and return 403.
|
|
120
|
+
|
|
121
|
+
Images (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`) inside `docs_dir` are served at their path, behind the same login as the docs, so a doc can reference an image next to it with a relative path:
|
|
122
|
+
|
|
123
|
+
```markdown
|
|
124
|
+

|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
No other file types are served. SVG is excluded because it can carry scripts.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Math and diagrams
|
|
132
|
+
|
|
133
|
+
Docs can contain TeX math and [mermaid](https://mermaid.js.org) diagrams. Both are rendered in the browser by copies of [KaTeX](https://katex.org) and mermaid bundled with the package, so nothing is fetched from a CDN. Pages without math or diagrams load neither library.
|
|
134
|
+
|
|
135
|
+
Inline math uses `\(...\)`. Single dollar signs are left alone so prices in prose render normally. Block math uses `$$...$$` or `\[...\]`:
|
|
136
|
+
|
|
137
|
+
```markdown
|
|
138
|
+
The area is \(\pi r^2\), and it costs $5.
|
|
139
|
+
|
|
140
|
+
$$
|
|
141
|
+
\int_0^1 x^2 \, dx = \frac{1}{3}
|
|
142
|
+
$$
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Diagrams are fenced code blocks tagged `mermaid`:
|
|
146
|
+
|
|
147
|
+
````markdown
|
|
148
|
+
```mermaid
|
|
149
|
+
flowchart LR
|
|
150
|
+
Write --> Commit --> Read
|
|
151
|
+
```
|
|
152
|
+
````
|
|
153
|
+
|
|
154
|
+
Mermaid runs with `securityLevel: "strict"`, so click handlers and raw HTML in diagram labels are disabled. Bundled versions and their licenses are listed in [`src/leafdocs/static/vendor/VERSIONS.md`](src/leafdocs/static/vendor/VERSIONS.md).
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Authentication
|
|
159
|
+
|
|
160
|
+
By default the server runs open with no login required.
|
|
161
|
+
|
|
162
|
+
To enable pin-based auth, add pins to a `.env` file in your working directory:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
LEAFDOCS_PINS=mypin123,anotherpin
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Restart the server. All routes will now require a valid pin.
|
|
169
|
+
|
|
170
|
+
**How it works:**
|
|
171
|
+
- Pins are hashed with bcrypt at startup — raw values are never stored in memory
|
|
172
|
+
- A successful login issues a session cookie with `HttpOnly`, `SameSite=Lax`, and `Secure` (see `LEAFDOCS_COOKIE_SECURE` below)
|
|
173
|
+
- Multiple pins are supported — useful for sharing access without a shared secret
|
|
174
|
+
- To invalidate all sessions, rotate or remove the pin and restart
|
|
175
|
+
|
|
176
|
+
### Session secret key
|
|
177
|
+
|
|
178
|
+
By default a random secret key is generated at startup, which means sessions are invalidated on every restart. For persistent sessions across restarts, set a stable key:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
LEAFDOCS_SECRET_KEY=your-long-random-secret-here
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Or pass it directly in code:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
ld = LeafDocs(docs_dir="./docs", secret_key="your-long-random-secret-here")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Generate a good key with:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
python -c "import secrets; print(secrets.token_hex(32))"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Configuration reference
|
|
199
|
+
|
|
200
|
+
All pins and the secret key are configured via `.env` only. Copy `.env.example` to `.env` to get started:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
LEAFDOCS_PINS=pin1,pin2
|
|
204
|
+
LEAFDOCS_SECRET_KEY=your-secret-here
|
|
205
|
+
LEAFDOCS_COOKIE_SECURE=true
|
|
206
|
+
LEAFDOCS_ALLOW_HTML=false
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
| Variable | Default | Description |
|
|
210
|
+
|--------------------------|---------|-------------|
|
|
211
|
+
| `LEAFDOCS_PINS` | unset | Comma-separated pins. Unset or empty means no auth. |
|
|
212
|
+
| `LEAFDOCS_SECRET_KEY` | random | Session signing key. Random means sessions reset on restart. |
|
|
213
|
+
| `LEAFDOCS_COOKIE_SECURE` | `true` | Sets the `Secure` flag on the session cookie. `false`, `0`, `no`, or `off` disables it. Disable only when serving over plain HTTP, such as local or raw-IP testing; browsers will not send a `Secure` cookie over HTTP, so login loops back to the login page. |
|
|
214
|
+
| `LEAFDOCS_ALLOW_HTML` | `false` | Skips HTML sanitizing of rendered Markdown. `true`, `1`, `yes`, or `on` enables it. Only safe when you are the sole author of all content. |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Known limitations
|
|
219
|
+
|
|
220
|
+
- **No caching** — Markdown is rendered on every request. Fine for personal or low-traffic use; not suitable for high-traffic production serving.
|
|
221
|
+
- **No per-device session revocation** — rotating the pin invalidates all active sessions across all devices.
|
|
222
|
+
- **No plugin system** — the primary extension hook is `ld.flask_app`. Add routes and middleware directly on the Flask app.
|
|
223
|
+
- **Raw HTML in docs is sanitized** - rendered Markdown is passed through [nh3](https://github.com/messense/nh3) with an allowlist covering what the enabled extensions produce (headings with ids, lists, tables with alignment, code blocks with `language-*` classes, links, images, the `[TOC]` block, and `span` and `div` elements with class `arithmatex` for math). Scripts, event handler attributes, iframes, inline styles other than table alignment, and URL schemes other than `http`, `https`, and `mailto` are removed. Links get `rel="noopener noreferrer"`. Raw HTML outside the allowlist, such as `<details>`, is stripped, and `class` on `span` and `div` is removed unless it is exactly `arithmatex` (or `toc` on `div`). Set `LEAFDOCS_ALLOW_HTML=true` to turn sanitizing off; only do this when you are the sole author of all content.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Deployment
|
|
228
|
+
|
|
229
|
+
> **Disclaimer:** leafdocs is a Flask application. Running it with `ld.run()` uses Flask's built-in development server, which is not suitable for production. For production use, you are responsible for:
|
|
230
|
+
> - Running behind a production WSGI server (Gunicorn recommended)
|
|
231
|
+
> - Terminating HTTPS at a reverse proxy (Nginx recommended)
|
|
232
|
+
> - Managing the process with a supervisor (systemd recommended)
|
|
233
|
+
|
|
234
|
+
For full production setup on **AWS EC2** or **GCP Compute Engine** — Gunicorn + Nginx + systemd, raw-IP access, and two options for a custom domain (direct DNS + Certbot, or Cloudflare Tunnel for boxes without a public IPv4) — see **[DEPLOY.md](DEPLOY.md)**.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Claude Code skill
|
|
239
|
+
|
|
240
|
+
[`SKILL.md`](SKILL.md) is a [Claude Code](https://claude.com/claude-code) skill that generates Markdown files with the correct leafdocs frontmatter (`title`, `tags`) from raw notes or existing content, ready to drop into your `docs/` directory. Drop it into your Claude Code skills directory to use it.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Development
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
git clone https://github.com/anshulraj10/leafdocs
|
|
248
|
+
cd leafdocs
|
|
249
|
+
python -m venv .venv
|
|
250
|
+
source .venv/bin/activate
|
|
251
|
+
pip install -e ".[dev]"
|
|
252
|
+
pytest tests/ -v
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`python main.py` runs a dev server on http://127.0.0.1:5000 (cookie Secure flag is off for plain http).
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT
|
leafdocs-0.3.0/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# leafdocs
|
|
2
|
+
|
|
3
|
+
A lightweight Python library that turns a directory of Markdown files into a self-hosted, searchable web reader.
|
|
4
|
+
|
|
5
|
+
Install it, point it at a folder, get a running Flask app you can deploy anywhere.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from leafdocs import LeafDocs
|
|
9
|
+
|
|
10
|
+
ld = LeafDocs(docs_dir="./docs")
|
|
11
|
+
ld.run()
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install leafdocs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Minimal setup
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from leafdocs import LeafDocs
|
|
30
|
+
|
|
31
|
+
ld = LeafDocs(docs_dir="./docs")
|
|
32
|
+
ld.run()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Visit `http://127.0.0.1:5000` to see a searchable index of all `.md` files in your `docs/` directory, including subfolders.
|
|
36
|
+
|
|
37
|
+
### Constructor arguments
|
|
38
|
+
|
|
39
|
+
| Argument | Type | Default | Description |
|
|
40
|
+
|--------------|-----------------|-----------------|--------------------------------------------------|
|
|
41
|
+
| `docs_dir` | `str` | `"./docs"` | Path to the directory containing `.md` files |
|
|
42
|
+
| `secret_key` | `str` or `None` | `None` | Flask session secret key (see Auth section) |
|
|
43
|
+
|
|
44
|
+
### Accessing the Flask app
|
|
45
|
+
|
|
46
|
+
The underlying Flask app is exposed as `ld.flask_app`. Use it to add routes, middleware, or blueprints:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from leafdocs import LeafDocs
|
|
50
|
+
|
|
51
|
+
ld = LeafDocs(docs_dir="./docs")
|
|
52
|
+
|
|
53
|
+
@ld.flask_app.route("/health")
|
|
54
|
+
def health():
|
|
55
|
+
return {"status": "ok"}
|
|
56
|
+
|
|
57
|
+
ld.run()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Frontmatter
|
|
63
|
+
|
|
64
|
+
Each `.md` file can optionally include YAML frontmatter. Supported fields:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
---
|
|
68
|
+
title: My Document Title
|
|
69
|
+
tags: [python, tutorial]
|
|
70
|
+
---
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| Field | Type | Fallback |
|
|
74
|
+
|---------|--------------------------------|----------------------|
|
|
75
|
+
| `title` | string | Filename, titlecased |
|
|
76
|
+
| `tags` | list or comma-separated string | None |
|
|
77
|
+
|
|
78
|
+
Frontmatter is optional — files without it are discovered and served normally.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Folders and images
|
|
83
|
+
|
|
84
|
+
Subfolders under `docs_dir` are shown on the index as a collapsible tree, folders first, then docs. Folder names are titlecased the same way as filenames (`getting-started` becomes `Getting Started`). Folders with no `.md` files anywhere beneath them are left out. While the search box has text, matching docs are shown with their folders opened; clearing it restores the folders you had open.
|
|
85
|
+
|
|
86
|
+
A doc's URL is its path relative to `docs_dir` without `.md`, so `docs/guides/setup.md` is served at `/guides/setup`. Links ending in `.md`, such as `/guides/setup.md`, redirect to the same URL without it.
|
|
87
|
+
|
|
88
|
+
Skipped during discovery and returned as 404 by the reader:
|
|
89
|
+
|
|
90
|
+
- Any file or folder whose name starts with `.`, such as `.git/`
|
|
91
|
+
- Top-level `login.md`, `logout.md`, and anything under `static/`, since those paths belong to built-in routes. Nested files such as `guides/login.md` are fine.
|
|
92
|
+
|
|
93
|
+
Symlinks that point outside `docs_dir` are not listed and return 403.
|
|
94
|
+
|
|
95
|
+
Images (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`) inside `docs_dir` are served at their path, behind the same login as the docs, so a doc can reference an image next to it with a relative path:
|
|
96
|
+
|
|
97
|
+
```markdown
|
|
98
|
+

|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
No other file types are served. SVG is excluded because it can carry scripts.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Math and diagrams
|
|
106
|
+
|
|
107
|
+
Docs can contain TeX math and [mermaid](https://mermaid.js.org) diagrams. Both are rendered in the browser by copies of [KaTeX](https://katex.org) and mermaid bundled with the package, so nothing is fetched from a CDN. Pages without math or diagrams load neither library.
|
|
108
|
+
|
|
109
|
+
Inline math uses `\(...\)`. Single dollar signs are left alone so prices in prose render normally. Block math uses `$$...$$` or `\[...\]`:
|
|
110
|
+
|
|
111
|
+
```markdown
|
|
112
|
+
The area is \(\pi r^2\), and it costs $5.
|
|
113
|
+
|
|
114
|
+
$$
|
|
115
|
+
\int_0^1 x^2 \, dx = \frac{1}{3}
|
|
116
|
+
$$
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Diagrams are fenced code blocks tagged `mermaid`:
|
|
120
|
+
|
|
121
|
+
````markdown
|
|
122
|
+
```mermaid
|
|
123
|
+
flowchart LR
|
|
124
|
+
Write --> Commit --> Read
|
|
125
|
+
```
|
|
126
|
+
````
|
|
127
|
+
|
|
128
|
+
Mermaid runs with `securityLevel: "strict"`, so click handlers and raw HTML in diagram labels are disabled. Bundled versions and their licenses are listed in [`src/leafdocs/static/vendor/VERSIONS.md`](src/leafdocs/static/vendor/VERSIONS.md).
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Authentication
|
|
133
|
+
|
|
134
|
+
By default the server runs open with no login required.
|
|
135
|
+
|
|
136
|
+
To enable pin-based auth, add pins to a `.env` file in your working directory:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
LEAFDOCS_PINS=mypin123,anotherpin
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Restart the server. All routes will now require a valid pin.
|
|
143
|
+
|
|
144
|
+
**How it works:**
|
|
145
|
+
- Pins are hashed with bcrypt at startup — raw values are never stored in memory
|
|
146
|
+
- A successful login issues a session cookie with `HttpOnly`, `SameSite=Lax`, and `Secure` (see `LEAFDOCS_COOKIE_SECURE` below)
|
|
147
|
+
- Multiple pins are supported — useful for sharing access without a shared secret
|
|
148
|
+
- To invalidate all sessions, rotate or remove the pin and restart
|
|
149
|
+
|
|
150
|
+
### Session secret key
|
|
151
|
+
|
|
152
|
+
By default a random secret key is generated at startup, which means sessions are invalidated on every restart. For persistent sessions across restarts, set a stable key:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
LEAFDOCS_SECRET_KEY=your-long-random-secret-here
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Or pass it directly in code:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
ld = LeafDocs(docs_dir="./docs", secret_key="your-long-random-secret-here")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Generate a good key with:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
python -c "import secrets; print(secrets.token_hex(32))"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Configuration reference
|
|
173
|
+
|
|
174
|
+
All pins and the secret key are configured via `.env` only. Copy `.env.example` to `.env` to get started:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
LEAFDOCS_PINS=pin1,pin2
|
|
178
|
+
LEAFDOCS_SECRET_KEY=your-secret-here
|
|
179
|
+
LEAFDOCS_COOKIE_SECURE=true
|
|
180
|
+
LEAFDOCS_ALLOW_HTML=false
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| Variable | Default | Description |
|
|
184
|
+
|--------------------------|---------|-------------|
|
|
185
|
+
| `LEAFDOCS_PINS` | unset | Comma-separated pins. Unset or empty means no auth. |
|
|
186
|
+
| `LEAFDOCS_SECRET_KEY` | random | Session signing key. Random means sessions reset on restart. |
|
|
187
|
+
| `LEAFDOCS_COOKIE_SECURE` | `true` | Sets the `Secure` flag on the session cookie. `false`, `0`, `no`, or `off` disables it. Disable only when serving over plain HTTP, such as local or raw-IP testing; browsers will not send a `Secure` cookie over HTTP, so login loops back to the login page. |
|
|
188
|
+
| `LEAFDOCS_ALLOW_HTML` | `false` | Skips HTML sanitizing of rendered Markdown. `true`, `1`, `yes`, or `on` enables it. Only safe when you are the sole author of all content. |
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Known limitations
|
|
193
|
+
|
|
194
|
+
- **No caching** — Markdown is rendered on every request. Fine for personal or low-traffic use; not suitable for high-traffic production serving.
|
|
195
|
+
- **No per-device session revocation** — rotating the pin invalidates all active sessions across all devices.
|
|
196
|
+
- **No plugin system** — the primary extension hook is `ld.flask_app`. Add routes and middleware directly on the Flask app.
|
|
197
|
+
- **Raw HTML in docs is sanitized** - rendered Markdown is passed through [nh3](https://github.com/messense/nh3) with an allowlist covering what the enabled extensions produce (headings with ids, lists, tables with alignment, code blocks with `language-*` classes, links, images, the `[TOC]` block, and `span` and `div` elements with class `arithmatex` for math). Scripts, event handler attributes, iframes, inline styles other than table alignment, and URL schemes other than `http`, `https`, and `mailto` are removed. Links get `rel="noopener noreferrer"`. Raw HTML outside the allowlist, such as `<details>`, is stripped, and `class` on `span` and `div` is removed unless it is exactly `arithmatex` (or `toc` on `div`). Set `LEAFDOCS_ALLOW_HTML=true` to turn sanitizing off; only do this when you are the sole author of all content.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Deployment
|
|
202
|
+
|
|
203
|
+
> **Disclaimer:** leafdocs is a Flask application. Running it with `ld.run()` uses Flask's built-in development server, which is not suitable for production. For production use, you are responsible for:
|
|
204
|
+
> - Running behind a production WSGI server (Gunicorn recommended)
|
|
205
|
+
> - Terminating HTTPS at a reverse proxy (Nginx recommended)
|
|
206
|
+
> - Managing the process with a supervisor (systemd recommended)
|
|
207
|
+
|
|
208
|
+
For full production setup on **AWS EC2** or **GCP Compute Engine** — Gunicorn + Nginx + systemd, raw-IP access, and two options for a custom domain (direct DNS + Certbot, or Cloudflare Tunnel for boxes without a public IPv4) — see **[DEPLOY.md](DEPLOY.md)**.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Claude Code skill
|
|
213
|
+
|
|
214
|
+
[`SKILL.md`](SKILL.md) is a [Claude Code](https://claude.com/claude-code) skill that generates Markdown files with the correct leafdocs frontmatter (`title`, `tags`) from raw notes or existing content, ready to drop into your `docs/` directory. Drop it into your Claude Code skills directory to use it.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
git clone https://github.com/anshulraj10/leafdocs
|
|
222
|
+
cd leafdocs
|
|
223
|
+
python -m venv .venv
|
|
224
|
+
source .venv/bin/activate
|
|
225
|
+
pip install -e ".[dev]"
|
|
226
|
+
pytest tests/ -v
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`python main.py` runs a dev server on http://127.0.0.1:5000 (cookie Secure flag is off for plain http).
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "leafdocs"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Turn a directory of Markdown files into a self-hosted, searchable web reader."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -23,6 +23,8 @@ dependencies = [
|
|
|
23
23
|
"python-frontmatter>=1.1",
|
|
24
24
|
"bcrypt>=4.1",
|
|
25
25
|
"python-dotenv>=1.0",
|
|
26
|
+
"nh3>=0.2.22",
|
|
27
|
+
"pymdown-extensions>=10.0",
|
|
26
28
|
]
|
|
27
29
|
|
|
28
30
|
[project.optional-dependencies]
|