fr-docs 2.3.0__tar.gz → 2.5.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.
- {fr_docs-2.3.0 → fr_docs-2.5.0}/PKG-INFO +18 -36
- fr_docs-2.5.0/README.md +47 -0
- fr_docs-2.5.0/fr_docs/__main__.py +114 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/build.py +78 -83
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/config.py +25 -6
- fr_docs-2.5.0/fr_docs/config_accessors.py +208 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/frontmatter.py +12 -1
- fr_docs-2.5.0/fr_docs/git.py +214 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/html_pipeline.py +268 -218
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/init.py +184 -16
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/markdown.py +20 -23
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/search.py +167 -82
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/slug.py +2 -2
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/syntax.py +45 -12
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/template.py +54 -15
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/utils.py +1 -1
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs.egg-info/PKG-INFO +18 -36
- fr_docs-2.5.0/fr_docs.egg-info/entry_points.txt +2 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/pyproject.toml +54 -2
- fr_docs-2.3.0/README.md +0 -65
- fr_docs-2.3.0/fr_docs/__main__.py +0 -85
- fr_docs-2.3.0/fr_docs/config_accessors.py +0 -117
- fr_docs-2.3.0/fr_docs/git.py +0 -131
- fr_docs-2.3.0/fr_docs.egg-info/entry_points.txt +0 -2
- {fr_docs-2.3.0 → fr_docs-2.5.0}/LICENSE.md +0 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs/__init__.py +0 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs.egg-info/SOURCES.txt +0 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs.egg-info/dependency_links.txt +0 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs.egg-info/requires.txt +0 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/fr_docs.egg-info/top_level.txt +0 -0
- {fr_docs-2.3.0 → fr_docs-2.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fr-docs
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Fast static documentation generator with search, versioning, and zero-config setup
|
|
5
5
|
Author: Omena0
|
|
6
6
|
License: # PolyForm Noncommercial License 1.0.0
|
|
@@ -175,7 +175,6 @@ Generate fast, fully featured, modern documentation pages from markdown for GitH
|
|
|
175
175
|
- [Quickstart](https://omena0.dev/fr-docs/quickstart)
|
|
176
176
|
- [Configuration](https://omena0.dev/fr-docs/config.json)
|
|
177
177
|
- [Showcase](https://omena0.dev/fr-docs/showcase)
|
|
178
|
-
- [Pagespeed](https://omena0.dev/fr-docs/pagespeed) — performance metrics
|
|
179
178
|
|
|
180
179
|
## Overview
|
|
181
180
|
|
|
@@ -183,43 +182,26 @@ Fr-docs is a generic, configurable documentation builder for Python projects. It
|
|
|
183
182
|
|
|
184
183
|
### Key Features
|
|
185
184
|
|
|
186
|
-
- **
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
-
|
|
185
|
+
- **Toggleable features**: Disable any features in the config.
|
|
186
|
+
- **Search**: Search pages, headers, source code files, or symbols. Fuzzy matching included.
|
|
187
|
+
- **Markdown features**: Feature-complete markdown features, including:
|
|
188
|
+
- **Code highlighting**: Highlight code blocks with `fastpylight`
|
|
189
|
+
- **Copy code blocks**: Copy buttons on multiline code blocks, inline copy code blocks.
|
|
190
|
+
- **Tables**: Auto convert markdown tables
|
|
191
|
+
- **Frontmatter metadata**: Title and description as optional frontmatter.
|
|
192
|
+
- **Hover previews**: Hover a link to see the title and description, hover a code ref to see a code preview.
|
|
193
|
+
- **Code references**: Link to a source code files, lines, line ranges, or symbols. Automatically generates a clickable code ref.
|
|
194
|
+
- **Custom tags**: Highlight tags such as `[ext]` automatically across the site.
|
|
195
|
+
- **Auto versioned**: Automatically switch to older documentation based on github tags.<br>
|
|
196
|
+
<sup><sub>(requires site/ to be commited into the repo)</sup></sub>
|
|
197
|
+
- **Auto minification**: Minifies HTML, CSS and JS and inlines Critical CSS.
|
|
198
|
+
- **Same-origin fonts**: Serve fonts from the same origin. Download once before build, serve forever.
|
|
199
|
+
- **Page preloading**: Preload page links for faster browsing.
|
|
200
|
+
- **Configurable elements**: Configure project name, icon, copyright, topbar links, etc.
|
|
190
201
|
|
|
191
|
-
- **Zero-config setup**: Works out of the box with sensible defaults
|
|
192
|
-
- **Search**: Full-text search with Zstandard-compressed index
|
|
193
|
-
- Symbol-level search across all source files
|
|
194
|
-
- File index for browsing source code
|
|
195
|
-
- Per-file source highlights with caching
|
|
196
|
-
|
|
197
|
-
- **Optimized**: HTML minification, critical CSS/JS inlining, Zstd compression
|
|
198
|
-
- Critical CSS extracted and inlined for both mobile and desktop viewports
|
|
199
|
-
- JavaScript and CSS minified with `terser` and `html-minifier-next`
|
|
200
|
-
- Zstd-compressed search index, source files, highlights, and metadata
|
|
201
|
-
- Adaptive zstd level selection (scans 1..configured, picks the smallest)
|
|
202
|
-
|
|
203
|
-
- **Multi-environment**: Different configs for production, staging, development
|
|
204
|
-
- **Parallel builds**: Threaded page generation for faster builds
|
|
205
|
-
- **Git integration**: Automatic metadata extraction from git commits
|
|
206
|
-
- Version options derived from commit messages (e.g. `4D - fix bug` → `4.4.0`)
|
|
207
|
-
- Commit SHA and message embedded in the version selector
|
|
208
|
-
|
|
209
|
-
- **Local font serving**: Fonts downloaded at build time and served from the site's own origin
|
|
210
|
-
- No third-party font round-trips or 404s when Google changes the URL scheme
|
|
211
|
-
- `font-display: swap` with metric-compatible `font-ascent-descent` fallbacks
|
|
212
|
-
- Primary body font weight preloaded for faster first paint
|
|
213
|
-
|
|
214
|
-
- **Sidebar**: Collapsible sections with auto-expansion
|
|
215
|
-
- The section containing the current page is expanded on load
|
|
216
|
-
- Single-page sections and single-section sites are expanded by default
|
|
217
|
-
|
|
218
|
-
- **Inline copy commands**: Prefix inline code with `c` to create copy-on-click links
|
|
219
|
-
- **Code references**: Auto-link bare filename references to their source files
|
|
220
|
-
- **Backlinks and related pages**: Automatically generated navigation between related content
|
|
221
202
|
|
|
222
203
|
### Projects that use fr-docs
|
|
223
204
|
|
|
224
205
|
- [Fr-docs](https://omena0.dev/fr-docs/)
|
|
225
206
|
- [PyJavaBridge](https://omena0.dev/PyJavaBridge)
|
|
207
|
+
<sub><sup>(Legacy)</sup></sub>
|
fr_docs-2.5.0/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
# Fr-docs
|
|
3
|
+
|
|
4
|
+
Generate fast, fully featured, modern documentation pages from markdown for GitHub pages and static hosting.
|
|
5
|
+
|
|
6
|
+
[](https://github.com/Omena0/fr-docs/actions/workflows/lint.yml)
|
|
7
|
+
[](https://github.com/Omena0/fr-docs/actions/workflows/pages.yml)
|
|
8
|
+
[](https://github.com/Omena0/fr-docs/actions/workflows/publish.yml)
|
|
9
|
+
[](https://github.com/Omena0/fr-docs/actions/workflows/github-code-scanning/codeql)
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
- [Getting Started](https://omena0.dev/fr-docs/)
|
|
14
|
+
- [Installation](https://omena0.dev/fr-docs/installation)
|
|
15
|
+
- [Quickstart](https://omena0.dev/fr-docs/quickstart)
|
|
16
|
+
- [Configuration](https://omena0.dev/fr-docs/config.json)
|
|
17
|
+
- [Showcase](https://omena0.dev/fr-docs/showcase)
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
Fr-docs is a generic, configurable documentation builder for Python projects. It converts Markdown source files into a searchable, optimized static HTML site with zero-config setup.
|
|
22
|
+
|
|
23
|
+
### Key Features
|
|
24
|
+
|
|
25
|
+
- **Toggleable features**: Disable any features in the config.
|
|
26
|
+
- **Search**: Search pages, headers, source code files, or symbols. Fuzzy matching included.
|
|
27
|
+
- **Markdown features**: Feature-complete markdown features, including:
|
|
28
|
+
- **Code highlighting**: Highlight code blocks with `fastpylight`
|
|
29
|
+
- **Copy code blocks**: Copy buttons on multiline code blocks, inline copy code blocks.
|
|
30
|
+
- **Tables**: Auto convert markdown tables
|
|
31
|
+
- **Frontmatter metadata**: Title and description as optional frontmatter.
|
|
32
|
+
- **Hover previews**: Hover a link to see the title and description, hover a code ref to see a code preview.
|
|
33
|
+
- **Code references**: Link to a source code files, lines, line ranges, or symbols. Automatically generates a clickable code ref.
|
|
34
|
+
- **Custom tags**: Highlight tags such as `[ext]` automatically across the site.
|
|
35
|
+
- **Auto versioned**: Automatically switch to older documentation based on github tags.<br>
|
|
36
|
+
<sup><sub>(requires site/ to be commited into the repo)</sup></sub>
|
|
37
|
+
- **Auto minification**: Minifies HTML, CSS and JS and inlines Critical CSS.
|
|
38
|
+
- **Same-origin fonts**: Serve fonts from the same origin. Download once before build, serve forever.
|
|
39
|
+
- **Page preloading**: Preload page links for faster browsing.
|
|
40
|
+
- **Configurable elements**: Configure project name, icon, copyright, topbar links, etc.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Projects that use fr-docs
|
|
44
|
+
|
|
45
|
+
- [Fr-docs](https://omena0.dev/fr-docs/)
|
|
46
|
+
- [PyJavaBridge](https://omena0.dev/PyJavaBridge)
|
|
47
|
+
<sub><sup>(Legacy)</sup></sub>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Entry point for fr-docs CLI.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m fr_docs build [--production] [--symlink] [--config CONFIG]
|
|
5
|
+
fr-docs build [--production] [--symlink] [--config CONFIG]
|
|
6
|
+
python -m fr_docs init [DIR] [config|fonts]
|
|
7
|
+
fr-docs init [DIR] [config|fonts]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from .build import main as build_main
|
|
14
|
+
from .init import main as init_main
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="fr-docs",
|
|
20
|
+
description="Fast static documentation generator",
|
|
21
|
+
)
|
|
22
|
+
subparsers = parser.add_subparsers(
|
|
23
|
+
dest="command",
|
|
24
|
+
help="Available commands",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Build command
|
|
28
|
+
build_parser = subparsers.add_parser(
|
|
29
|
+
"build",
|
|
30
|
+
help="Build the documentation site",
|
|
31
|
+
)
|
|
32
|
+
build_parser.add_argument(
|
|
33
|
+
"--production",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Rewrite internal links to site-root absolute paths for deployed docs.",
|
|
36
|
+
)
|
|
37
|
+
build_parser.add_argument(
|
|
38
|
+
"--symlink",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help=(
|
|
41
|
+
"Create a {site_prefix}/ symlink dir under site/ so a local "
|
|
42
|
+
"production build can be opened in a browser. Off by default: "
|
|
43
|
+
"in CI the whole docs/site/ tree is deployed as-is, and a nested "
|
|
44
|
+
"fr-docs/ symlink folder would show up as "
|
|
45
|
+
"omena0.dev/fr-docs/fr-docs/."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
build_parser.add_argument(
|
|
49
|
+
"--config",
|
|
50
|
+
help="Path to the configuration JSON file.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Init command
|
|
54
|
+
init_parser = subparsers.add_parser(
|
|
55
|
+
"init",
|
|
56
|
+
help="Initialize a new documentation project",
|
|
57
|
+
)
|
|
58
|
+
init_parser.add_argument(
|
|
59
|
+
"dir",
|
|
60
|
+
nargs="?",
|
|
61
|
+
default="docs",
|
|
62
|
+
help="Directory to initialize (default: docs)",
|
|
63
|
+
)
|
|
64
|
+
init_parser.add_argument(
|
|
65
|
+
"target",
|
|
66
|
+
nargs="?",
|
|
67
|
+
choices=["config", "fonts"],
|
|
68
|
+
default=None,
|
|
69
|
+
help="Initialize only the configuration or download fonts.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
args = parser.parse_args(sys.argv[1:])
|
|
73
|
+
|
|
74
|
+
if args.command == "build":
|
|
75
|
+
build_argv: list[str] = []
|
|
76
|
+
|
|
77
|
+
if args.production:
|
|
78
|
+
build_argv.append("--production")
|
|
79
|
+
|
|
80
|
+
if args.symlink:
|
|
81
|
+
build_argv.append("--symlink")
|
|
82
|
+
|
|
83
|
+
if args.config:
|
|
84
|
+
build_argv.extend(["--config", args.config])
|
|
85
|
+
|
|
86
|
+
build_main(build_argv)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if args.command == "init":
|
|
90
|
+
# If first arg is "config" or "fonts" with no explicit dir, use current directory
|
|
91
|
+
if args.dir in ("config", "fonts") and args.target is None:
|
|
92
|
+
init_main(".", args.dir)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
init_main(args.dir, args.target)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Default to build for backwards compatibility.
|
|
99
|
+
if len(sys.argv) > 1 and sys.argv[1] in (
|
|
100
|
+
"--production",
|
|
101
|
+
"--symlink",
|
|
102
|
+
"--config",
|
|
103
|
+
"--help",
|
|
104
|
+
"-h",
|
|
105
|
+
):
|
|
106
|
+
build_main(sys.argv[1:])
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
parser.print_help()
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
main()
|
|
@@ -6,6 +6,8 @@ Converts Markdown source files into a static HTML site using configuration.
|
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import concurrent.futures
|
|
9
|
+
import contextlib
|
|
10
|
+
import glob
|
|
9
11
|
import json
|
|
10
12
|
import os
|
|
11
13
|
import re
|
|
@@ -25,10 +27,12 @@ from .config_accessors import (
|
|
|
25
27
|
project_name,
|
|
26
28
|
search_index_filename,
|
|
27
29
|
sidebar,
|
|
28
|
-
src_dir,
|
|
29
30
|
workers,
|
|
30
31
|
zstd_level,
|
|
31
32
|
)
|
|
33
|
+
from .config_accessors import (
|
|
34
|
+
src_dir as config_src_dir,
|
|
35
|
+
)
|
|
32
36
|
from .git import build_version_options, collect_git_metadata
|
|
33
37
|
from .html_pipeline import build_page, minify_all_pages, optimize_all_pages
|
|
34
38
|
from .search import build_search_index, search_include_config
|
|
@@ -42,7 +46,7 @@ from .utils import normalized_site_prefix
|
|
|
42
46
|
|
|
43
47
|
def _compute_hash(data: str) -> str:
|
|
44
48
|
"""Compute xxhash hash of string data."""
|
|
45
|
-
return xxhash.xxh3_128_hexdigest(data.encode(
|
|
49
|
+
return xxhash.xxh3_128_hexdigest(data.encode())
|
|
46
50
|
|
|
47
51
|
|
|
48
52
|
def _best_zstd_level(raw: bytes, configured: int) -> int:
|
|
@@ -78,8 +82,8 @@ def _minify_static_asset(config, src: Path, dst: Path, name: str) -> None:
|
|
|
78
82
|
terser_bin = node_modules / ".bin" / "terser"
|
|
79
83
|
cmd = [
|
|
80
84
|
str(terser_bin) if terser_bin.exists() else "npx",
|
|
81
|
-
"
|
|
82
|
-
*([
|
|
85
|
+
"" if terser_bin.exists() else "--yes",
|
|
86
|
+
*([] if terser_bin.exists() else ["terser"]),
|
|
83
87
|
str(src),
|
|
84
88
|
"--compress",
|
|
85
89
|
"warnings=false",
|
|
@@ -107,8 +111,8 @@ def _minify_static_asset(config, src: Path, dst: Path, name: str) -> None:
|
|
|
107
111
|
wrapper.write_text(wrapped, encoding="utf-8")
|
|
108
112
|
cmd = [
|
|
109
113
|
str(minifier_bin) if minifier_bin.exists() else "npx",
|
|
110
|
-
"
|
|
111
|
-
*(
|
|
114
|
+
"" if minifier_bin.exists() else "--yes",
|
|
115
|
+
*("" if minifier_bin.exists() else ["html-minifier-next"]),
|
|
112
116
|
"--minify-css=true",
|
|
113
117
|
"--remove-comments",
|
|
114
118
|
"--output",
|
|
@@ -121,27 +125,22 @@ def _minify_static_asset(config, src: Path, dst: Path, name: str) -> None:
|
|
|
121
125
|
)
|
|
122
126
|
if result.returncode == 0 and min_out.exists():
|
|
123
127
|
out = min_out.read_text(encoding="utf-8")
|
|
124
|
-
m
|
|
125
|
-
|
|
126
|
-
dst.write_text(m.group(1), encoding="utf-8")
|
|
128
|
+
if m := re.search(r"<style>([\s\S]*)</style>", out):
|
|
129
|
+
dst.write_text(m[1], encoding="utf-8")
|
|
127
130
|
return
|
|
128
131
|
print(f" ! minify failed for {name}, copying raw: {result.stderr[-300:]}")
|
|
129
132
|
finally:
|
|
130
|
-
|
|
133
|
+
with contextlib.suppress(OSError):
|
|
131
134
|
wrapper.unlink()
|
|
132
|
-
|
|
133
|
-
pass
|
|
134
|
-
try:
|
|
135
|
+
with contextlib.suppress(OSError):
|
|
135
136
|
min_out.unlink()
|
|
136
|
-
except OSError:
|
|
137
|
-
pass
|
|
138
137
|
shutil.copyfile(src, dst)
|
|
139
138
|
|
|
140
139
|
|
|
141
140
|
class BuildCache:
|
|
142
141
|
"""Manage caching of build data to avoid unnecessary regeneration."""
|
|
143
142
|
|
|
144
|
-
def __init__(self, cache_path: str):
|
|
143
|
+
def __init__(self, cache_path: str) -> None:
|
|
145
144
|
self.cache_path = Path(cache_path)
|
|
146
145
|
self.cache = self._load_cache()
|
|
147
146
|
|
|
@@ -150,14 +149,14 @@ class BuildCache:
|
|
|
150
149
|
if not self.cache_path.exists():
|
|
151
150
|
return {}
|
|
152
151
|
try:
|
|
153
|
-
with
|
|
152
|
+
with self.cache_path.open() as f:
|
|
154
153
|
return json.load(f)
|
|
155
154
|
except json.JSONDecodeError, OSError:
|
|
156
155
|
return {}
|
|
157
156
|
|
|
158
|
-
def _save_cache(self):
|
|
157
|
+
def _save_cache(self) -> None:
|
|
159
158
|
"""Save the build cache to disk."""
|
|
160
|
-
with
|
|
159
|
+
with self.cache_path.open("w") as f:
|
|
161
160
|
json.dump(self.cache, f, separators=(",", ":"))
|
|
162
161
|
|
|
163
162
|
def needs_regeneration(self, key: str, current_hash: str) -> bool:
|
|
@@ -165,7 +164,7 @@ class BuildCache:
|
|
|
165
164
|
cached_hash = self.cache.get(key)
|
|
166
165
|
return True if cached_hash is None else cached_hash != current_hash
|
|
167
166
|
|
|
168
|
-
def update_cache(self, key: str, new_hash: str):
|
|
167
|
+
def update_cache(self, key: str, new_hash: str) -> None:
|
|
169
168
|
"""Update cache with new hash."""
|
|
170
169
|
self.cache[key] = new_hash
|
|
171
170
|
self._save_cache()
|
|
@@ -194,9 +193,39 @@ def _write_zstd_json(config, filename, value):
|
|
|
194
193
|
return len(raw), len(compressed)
|
|
195
194
|
|
|
196
195
|
|
|
196
|
+
def _should_skip_file(rel_path, ignore_dirs, seen_paths):
|
|
197
|
+
"""Check if a file should be skipped based on hidden dirs, ignore dirs, or duplicates."""
|
|
198
|
+
rel_parts = rel_path.parts
|
|
199
|
+
if any(p.startswith(".") for p in rel_parts):
|
|
200
|
+
return True
|
|
201
|
+
if any(p in ignore_dirs for p in rel_parts):
|
|
202
|
+
return True
|
|
203
|
+
rel_str = str(rel_path)
|
|
204
|
+
return rel_str in seen_paths
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _process_pattern(pattern, search_dir, ignore_dirs, seen_paths, source_files):
|
|
208
|
+
"""Process a single glob pattern within a search directory."""
|
|
209
|
+
for match in glob.glob(str(search_dir / pattern), recursive=True):
|
|
210
|
+
file_path = Path(match)
|
|
211
|
+
if not file_path.is_file():
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
rel_path = file_path.relative_to(search_dir)
|
|
216
|
+
except ValueError:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if _should_skip_file(rel_path, ignore_dirs, seen_paths):
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
seen_paths.add(str(rel_path))
|
|
223
|
+
with contextlib.suppress(OSError, UnicodeDecodeError):
|
|
224
|
+
source_files[str(rel_path)] = file_path.read_text(encoding="utf-8")
|
|
225
|
+
|
|
226
|
+
|
|
197
227
|
def collect_source_files(config):
|
|
198
228
|
"""Collect source code files for code reference feature using glob patterns."""
|
|
199
|
-
import glob as glob_module
|
|
200
229
|
|
|
201
230
|
source_files = {}
|
|
202
231
|
docs_path = Path(config["_docs_dir"])
|
|
@@ -299,50 +328,19 @@ def collect_source_files(config):
|
|
|
299
328
|
for search_dir in search_dirs:
|
|
300
329
|
if not search_dir.exists():
|
|
301
330
|
continue
|
|
302
|
-
|
|
331
|
+
with contextlib.suppress(OSError):
|
|
303
332
|
for pattern in patterns:
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
):
|
|
308
|
-
file_path = Path(match)
|
|
309
|
-
if not file_path.is_file():
|
|
310
|
-
continue
|
|
311
|
-
|
|
312
|
-
# Get relative path from search_dir for key
|
|
313
|
-
try:
|
|
314
|
-
rel_path = file_path.relative_to(search_dir)
|
|
315
|
-
except ValueError:
|
|
316
|
-
continue
|
|
317
|
-
|
|
318
|
-
rel_str = str(rel_path)
|
|
319
|
-
|
|
320
|
-
# Skip hidden directories in the relative path
|
|
321
|
-
rel_parts = rel_path.parts
|
|
322
|
-
if any(p.startswith(".") for p in rel_parts):
|
|
323
|
-
continue
|
|
324
|
-
if any(p in ignore_dirs for p in rel_parts):
|
|
325
|
-
continue
|
|
326
|
-
|
|
327
|
-
# Deduplicate by relative path string
|
|
328
|
-
if rel_str in seen_paths:
|
|
329
|
-
continue
|
|
330
|
-
seen_paths.add(rel_str)
|
|
331
|
-
|
|
332
|
-
try:
|
|
333
|
-
content = file_path.read_text(encoding="utf-8")
|
|
334
|
-
source_files[rel_str] = content
|
|
335
|
-
except OSError, UnicodeDecodeError:
|
|
336
|
-
pass
|
|
337
|
-
except OSError:
|
|
338
|
-
pass
|
|
333
|
+
_process_pattern(
|
|
334
|
+
pattern, search_dir, ignore_dirs, seen_paths, source_files
|
|
335
|
+
)
|
|
339
336
|
|
|
340
337
|
return source_files
|
|
341
338
|
|
|
342
339
|
|
|
343
|
-
def main(argv=None):
|
|
340
|
+
def main(argv=None) -> None:
|
|
344
341
|
if argv is not None and argv and argv[0] == "build":
|
|
345
342
|
argv = argv[1:]
|
|
343
|
+
|
|
346
344
|
elif argv is None and (
|
|
347
345
|
Path(sys.argv[0]).name == "fr-docs"
|
|
348
346
|
and len(sys.argv) > 1
|
|
@@ -375,7 +373,7 @@ def main(argv=None):
|
|
|
375
373
|
config["production"] = bool(args.production)
|
|
376
374
|
|
|
377
375
|
print(f"📖 Building {project_name(config)} docs...")
|
|
378
|
-
print(f" Source: {
|
|
376
|
+
print(f" Source: {config_src_dir(config)}")
|
|
379
377
|
print(f" Output: {out_dir(config)}")
|
|
380
378
|
print(f" Mode: {'production' if args.production else 'development'}")
|
|
381
379
|
if args.production:
|
|
@@ -395,7 +393,7 @@ def main(argv=None):
|
|
|
395
393
|
|
|
396
394
|
# Copy static assets (minifying JS/CSS when production)
|
|
397
395
|
docs_path = Path(config["_docs_dir"])
|
|
398
|
-
|
|
396
|
+
Path(config["_out_dir"]).mkdir(parents=True, exist_ok=True)
|
|
399
397
|
for legacy_name in (
|
|
400
398
|
"file_index.json",
|
|
401
399
|
"git_meta.json",
|
|
@@ -414,7 +412,7 @@ def main(argv=None):
|
|
|
414
412
|
else:
|
|
415
413
|
shutil.copyfile(src, dst)
|
|
416
414
|
except FileNotFoundError:
|
|
417
|
-
print(f"File not found: {
|
|
415
|
+
print(f"File not found: {Path.cwd()}, {src}->{dst}")
|
|
418
416
|
except OSError as e:
|
|
419
417
|
print(f"OSError: {e}")
|
|
420
418
|
|
|
@@ -433,13 +431,12 @@ def main(argv=None):
|
|
|
433
431
|
slugs = get_all_slugs(config)
|
|
434
432
|
|
|
435
433
|
# Also check for any .md files not in the sidebar
|
|
436
|
-
|
|
437
|
-
|
|
434
|
+
src_dir = Path(config["_src_dir"])
|
|
435
|
+
if src_dir.is_dir():
|
|
436
|
+
for dirpath, _dirnames, filenames in os.walk(src_dir):
|
|
438
437
|
for fname in filenames:
|
|
439
438
|
if fname.endswith(".md"):
|
|
440
|
-
rel = os.path.relpath(
|
|
441
|
-
os.path.join(dirpath, fname), config["_src_dir"]
|
|
442
|
-
)
|
|
439
|
+
rel = os.path.relpath(Path(dirpath, fname), src_dir)
|
|
443
440
|
s = rel[:-3]
|
|
444
441
|
if s not in slugs:
|
|
445
442
|
slugs.append(s)
|
|
@@ -453,16 +450,13 @@ def main(argv=None):
|
|
|
453
450
|
config["_search_index"] = search_index
|
|
454
451
|
|
|
455
452
|
# Compress search index
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
cctx = zstandard.ZstdCompressor(
|
|
453
|
+
search_raw = json.dumps(search_index, separators=(",", ":")).encode("utf-8")
|
|
454
|
+
compressed = zstandard.ZstdCompressor(
|
|
459
455
|
level=_best_zstd_level(search_raw, zstd_level(config))
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
search_index_path
|
|
463
|
-
|
|
464
|
-
with open(search_index_path, "wb") as sf:
|
|
465
|
-
sf.write(compressed)
|
|
456
|
+
).compress(search_raw)
|
|
457
|
+
search_index_path = Path(config["_out_dir"], search_index_filename(config))
|
|
458
|
+
search_index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
459
|
+
search_index_path.write_bytes(compressed)
|
|
466
460
|
|
|
467
461
|
if args.production:
|
|
468
462
|
config["_search_index_inline"] = ""
|
|
@@ -471,7 +465,7 @@ def main(argv=None):
|
|
|
471
465
|
|
|
472
466
|
config["_search_index_inline"] = (
|
|
473
467
|
'<script id="zstd-data" type="text/plain">'
|
|
474
|
-
f"{base64.b64encode(compressed).decode('ascii')}"
|
|
468
|
+
f" {base64.b64encode(compressed).decode('ascii')}"
|
|
475
469
|
"</script>"
|
|
476
470
|
)
|
|
477
471
|
|
|
@@ -523,10 +517,9 @@ def main(argv=None):
|
|
|
523
517
|
cache.update_cache(cache_key, content_hash)
|
|
524
518
|
else:
|
|
525
519
|
# Load from per-file cache
|
|
526
|
-
|
|
527
|
-
compressed = f.read()
|
|
520
|
+
compressed = highlight_cache_file.read_bytes()
|
|
528
521
|
decompressed = zstandard.ZstdDecompressor().decompress(compressed)
|
|
529
|
-
source_highlights[path] = json.loads(decompressed.decode(
|
|
522
|
+
source_highlights[path] = json.loads(decompressed.decode())
|
|
530
523
|
|
|
531
524
|
# Write the highlights to zstd
|
|
532
525
|
if source_highlights:
|
|
@@ -576,8 +569,8 @@ def main(argv=None):
|
|
|
576
569
|
# Build pages
|
|
577
570
|
slugs_to_build = []
|
|
578
571
|
for slug in slugs:
|
|
579
|
-
src =
|
|
580
|
-
if
|
|
572
|
+
src = Path(config["_src_dir"], f"{slug}.md")
|
|
573
|
+
if src.exists():
|
|
581
574
|
slugs_to_build.append(slug)
|
|
582
575
|
|
|
583
576
|
if slugs_to_build:
|
|
@@ -617,7 +610,7 @@ def main(argv=None):
|
|
|
617
610
|
except Exception as e: # noqa: BLE001
|
|
618
611
|
print(f" ✗ Minification failed: {e}")
|
|
619
612
|
|
|
620
|
-
# Create a {site_prefix}/ directory under site/ and symlink every
|
|
613
|
+
# Create a {site_prefix}/ directory under site/ and symlink every
|
|
621
614
|
# file into it, so a production build works locally (the HTML
|
|
622
615
|
# references absolute paths like /fr-docs/style.css). Without
|
|
623
616
|
# this, running `python -m fr_docs build --production` and then
|
|
@@ -636,7 +629,9 @@ def main(argv=None):
|
|
|
636
629
|
for f in sorted(Path(config["_out_dir"]).iterdir()):
|
|
637
630
|
if f.is_file() and not f.name.startswith("."):
|
|
638
631
|
(prefix_dir / f.name).symlink_to(f.resolve())
|
|
639
|
-
print(
|
|
632
|
+
print(
|
|
633
|
+
f" ✓ Symlinked {len(list(prefix_dir.iterdir()))} files into {prefix}/"
|
|
634
|
+
)
|
|
640
635
|
except OSError as e:
|
|
641
636
|
print(f" ✗ Failed to create {prefix}/ symlinks: {e}")
|
|
642
637
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Configuration loading for the fr-docs documentation builder."""
|
|
3
3
|
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
import json
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
@@ -58,7 +60,10 @@ def _merge(base, override):
|
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def load_config(config_path=None):
|
|
61
|
-
"""Load and validate the documentation builder configuration
|
|
63
|
+
"""Load and validate the documentation builder configuration.\n
|
|
64
|
+
:param config_path: Optional path to config file
|
|
65
|
+
:type config_path: str | Path | None
|
|
66
|
+
"""
|
|
62
67
|
if config_path is None:
|
|
63
68
|
config_path = Path.cwd() / "config.json"
|
|
64
69
|
if not config_path.exists():
|
|
@@ -71,17 +76,31 @@ def load_config(config_path=None):
|
|
|
71
76
|
config = DEFAULT_CONFIG
|
|
72
77
|
project_name_specified = False
|
|
73
78
|
if config_path.exists():
|
|
74
|
-
with open(config_path,
|
|
79
|
+
with open(config_path, encoding="utf-8") as f:
|
|
75
80
|
loaded = json.load(f)
|
|
76
81
|
config = _merge(DEFAULT_CONFIG, loaded)
|
|
77
82
|
project_name_specified = "project_name" in loaded
|
|
78
83
|
|
|
79
84
|
config_path = config_path.resolve()
|
|
80
|
-
|
|
81
|
-
Path(
|
|
82
|
-
|
|
85
|
+
if docs_dir_raw := config["docs_dir"]:
|
|
86
|
+
docs_dir_path = Path(docs_dir_raw)
|
|
87
|
+
docs_dir = (
|
|
88
|
+
docs_dir_path
|
|
89
|
+
if docs_dir_path.is_absolute()
|
|
90
|
+
else (config_path.parent / docs_dir_path).resolve()
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
docs_dir = config_path.parent
|
|
95
|
+
|
|
83
96
|
src_dir = docs_dir / config["src_dir"] if config["src_dir"] else docs_dir / "src"
|
|
84
|
-
|
|
97
|
+
|
|
98
|
+
out_dir_raw = (
|
|
99
|
+
config.get("build", {}).get("out_dir") or config.get("out_dir") or "site"
|
|
100
|
+
)
|
|
101
|
+
out_dir = (
|
|
102
|
+
Path(out_dir_raw) if Path(out_dir_raw).is_absolute() else docs_dir / out_dir_raw
|
|
103
|
+
)
|
|
85
104
|
|
|
86
105
|
config["_project_name_specified"] = project_name_specified
|
|
87
106
|
config["_config_path"] = str(config_path)
|