fr-docs 2.4.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.4.0 → fr_docs-2.5.0}/PKG-INFO +1 -1
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/__main__.py +8 -8
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/init.py +186 -16
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/PKG-INFO +1 -1
- fr_docs-2.5.0/fr_docs.egg-info/entry_points.txt +2 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/pyproject.toml +2 -2
- fr_docs-2.4.0/fr_docs.egg-info/entry_points.txt +0 -2
- {fr_docs-2.4.0 → fr_docs-2.5.0}/LICENSE.md +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/README.md +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/__init__.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/build.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/config.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/config_accessors.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/frontmatter.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/git.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/html_pipeline.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/markdown.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/search.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/slug.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/syntax.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/template.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/utils.py +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/SOURCES.txt +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/dependency_links.txt +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/requires.txt +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/top_level.txt +0 -0
- {fr_docs-2.4.0 → fr_docs-2.5.0}/setup.cfg +0 -0
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Usage:
|
|
4
4
|
python -m fr_docs build [--production] [--symlink] [--config CONFIG]
|
|
5
5
|
fr-docs build [--production] [--symlink] [--config CONFIG]
|
|
6
|
-
python -m fr_docs init [DIR] [config]
|
|
7
|
-
fr-docs init [DIR] [config]
|
|
6
|
+
python -m fr_docs init [DIR] [config|fonts]
|
|
7
|
+
fr-docs init [DIR] [config|fonts]
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
@@ -64,9 +64,9 @@ def main() -> None:
|
|
|
64
64
|
init_parser.add_argument(
|
|
65
65
|
"target",
|
|
66
66
|
nargs="?",
|
|
67
|
-
choices=["config"],
|
|
67
|
+
choices=["config", "fonts"],
|
|
68
68
|
default=None,
|
|
69
|
-
help="Initialize only the configuration.",
|
|
69
|
+
help="Initialize only the configuration or download fonts.",
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
args = parser.parse_args(sys.argv[1:])
|
|
@@ -87,12 +87,12 @@ def main() -> None:
|
|
|
87
87
|
return
|
|
88
88
|
|
|
89
89
|
if args.command == "init":
|
|
90
|
-
#
|
|
91
|
-
if args.dir
|
|
92
|
-
init_main("
|
|
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
93
|
return
|
|
94
94
|
|
|
95
|
-
init_main(args.dir, args.target
|
|
95
|
+
init_main(args.dir, args.target)
|
|
96
96
|
return
|
|
97
97
|
|
|
98
98
|
# Default to build for backwards compatibility.
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import platform
|
|
5
|
+
import re
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
9
|
+
import urllib.request
|
|
8
10
|
from pathlib import Path
|
|
9
|
-
from urllib.request import urlopen
|
|
10
11
|
|
|
11
12
|
DEFAULT_CONFIG = {
|
|
12
13
|
"$schema": "https://raw.githubusercontent.com/Omena0/fr-docs/main/config.schema.json",
|
|
@@ -37,7 +38,7 @@ DEFAULT_CONFIG = {
|
|
|
37
38
|
"git_meta_filename": "git_meta.zst",
|
|
38
39
|
},
|
|
39
40
|
"versioning": {
|
|
40
|
-
"commit_message_pattern": "
|
|
41
|
+
"commit_message_pattern": r"^\s*([0-9]+[A-Za-z])\s*[-:—–]\s*(.+)",
|
|
41
42
|
"live_label": "Live",
|
|
42
43
|
},
|
|
43
44
|
"source_files": {
|
|
@@ -292,12 +293,34 @@ jobs:
|
|
|
292
293
|
uses: Omena0/fr-docs@main
|
|
293
294
|
"""
|
|
294
295
|
|
|
296
|
+
DEFAULT_CSS_URL = (
|
|
297
|
+
"https://fonts.googleapis.com/css2?"
|
|
298
|
+
"family=Inter:ital,opsz,wght@0,14..32,400..800"
|
|
299
|
+
"&family=JetBrains+Mono:wght@400..700&display=swap"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
FONT_FACE_RE = re.compile(
|
|
303
|
+
r"@font-face\s*\{\n"
|
|
304
|
+
r"\s*font-family:\s*['\"](?P<family>[^'\"]+)['\"]\s*;\n"
|
|
305
|
+
r"\s*font-style:\s*(?P<style>\w+)\s*;\n"
|
|
306
|
+
r"\s*font-weight:\s*(?P<weight>\d+)\s*;\n"
|
|
307
|
+
r"\s*font-display:\s*(?P<display>\w+)\s*;\n"
|
|
308
|
+
r"\s*src:\s*url\((?P<src>[^)]+)\)\s*format\(['\"](?P<format>[^'\"]+)['\"]\)\s*;\n"
|
|
309
|
+
r"\}"
|
|
310
|
+
)
|
|
295
311
|
|
|
296
|
-
|
|
312
|
+
METRICS = {
|
|
313
|
+
"Inter": "1984 494",
|
|
314
|
+
"JetBrains Mono": "1020 300",
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def download_file(url, dest_path) -> bool:
|
|
297
319
|
"""Download a file from URL to dest_path."""
|
|
298
320
|
try:
|
|
299
321
|
print(f" Downloading {url}...")
|
|
300
|
-
|
|
322
|
+
req = urllib.request.Request(url, headers={"User-Agent": "fr-docs/1.0"})
|
|
323
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
301
324
|
content = response.read()
|
|
302
325
|
dest_path.write_bytes(content)
|
|
303
326
|
print(f" ✓ Saved to {dest_path}")
|
|
@@ -306,12 +329,96 @@ def download_file(url, dest_path) -> bool | None:
|
|
|
306
329
|
print(f" ✗ Failed to download {url}: {e}")
|
|
307
330
|
return False
|
|
308
331
|
|
|
332
|
+
|
|
333
|
+
def fetch_fonts_css(css_url: str) -> str:
|
|
334
|
+
"""Fetch Google Fonts CSS text."""
|
|
335
|
+
req = urllib.request.Request(css_url, headers={"User-Agent": "fr-docs/1.0"})
|
|
336
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
337
|
+
return resp.read().decode("utf-8")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def download_font(url: str, dest: Path) -> bool:
|
|
341
|
+
"""Download a single font file."""
|
|
342
|
+
if dest.exists() and dest.stat().st_size > 0:
|
|
343
|
+
return True
|
|
344
|
+
try:
|
|
345
|
+
req = urllib.request.Request(url, headers={"User-Agent": "fr-docs/1.0"})
|
|
346
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
347
|
+
data = resp.read()
|
|
348
|
+
if len(data) < 1000:
|
|
349
|
+
return False
|
|
350
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
dest.write_bytes(data)
|
|
352
|
+
return True
|
|
353
|
+
except Exception as exc: # noqa: BLE001
|
|
354
|
+
print(f" ! failed to download {url}: {exc}", file=sys.stderr)
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def run_fetch_fonts(docs_dir: Path, css_url: str = DEFAULT_CSS_URL) -> int:
|
|
359
|
+
"""Download Google Fonts and generate local fonts.css."""
|
|
360
|
+
out_dir = docs_dir / "fonts"
|
|
361
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
362
|
+
|
|
363
|
+
print(f"Fetching Google Fonts CSS from {css_url}")
|
|
364
|
+
css = fetch_fonts_css(css_url)
|
|
365
|
+
|
|
366
|
+
faces = FONT_FACE_RE.findall(css)
|
|
367
|
+
if not faces:
|
|
368
|
+
print("ERROR: no @font-face blocks found in CSS", file=sys.stderr)
|
|
369
|
+
return 1
|
|
370
|
+
|
|
371
|
+
print(f"Found {len(faces)} font-face rules")
|
|
372
|
+
|
|
373
|
+
local_rules = []
|
|
374
|
+
for family, style, weight, _display, src_url, fmt in faces:
|
|
375
|
+
src_url = src_url.strip().strip("'\"")
|
|
376
|
+
fname = Path(src_url.split("?")[0]).name
|
|
377
|
+
if not fname:
|
|
378
|
+
continue
|
|
379
|
+
dest = out_dir / fname
|
|
380
|
+
print(f" {family} {weight} {style} -> {fname}")
|
|
381
|
+
if not download_font(src_url, dest):
|
|
382
|
+
print(f" ! skipping {family} {weight} {style} (download failed)")
|
|
383
|
+
continue
|
|
384
|
+
if metrics := METRICS.get(family, ""):
|
|
385
|
+
local_rules.append(
|
|
386
|
+
f"""@font-face {{
|
|
387
|
+
font-family: '{family}';
|
|
388
|
+
font-style: {style};
|
|
389
|
+
font-weight: {weight};
|
|
390
|
+
font-display: swap;
|
|
391
|
+
src: url('fonts/{fname}') format('{fmt}');
|
|
392
|
+
font-ascent-descent: {metrics};
|
|
393
|
+
}}"""
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
local_rules.append(
|
|
397
|
+
f"""@font-face {{
|
|
398
|
+
font-family: '{family}';
|
|
399
|
+
font-style: {style};
|
|
400
|
+
font-weight: {weight};
|
|
401
|
+
font-display: swap;
|
|
402
|
+
src: url('fonts/{fname}') format('{fmt}');
|
|
403
|
+
}}"""
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if not local_rules:
|
|
407
|
+
print("ERROR: no fonts were downloaded", file=sys.stderr)
|
|
408
|
+
return 1
|
|
409
|
+
|
|
410
|
+
fonts_css = "\n".join(local_rules) + "\n"
|
|
411
|
+
css_out = docs_dir / "fonts.css"
|
|
412
|
+
css_out.write_text(fonts_css, encoding="utf-8")
|
|
413
|
+
print(f"\nWrote {len(local_rules)} font-face rules to {css_out}")
|
|
414
|
+
return 0
|
|
415
|
+
|
|
416
|
+
|
|
309
417
|
def create_uv_venv(docs_dir) -> None:
|
|
310
418
|
"""Create a uv virtual environment on Linux if uv is installed."""
|
|
311
419
|
if platform.system() != "Linux":
|
|
312
420
|
return
|
|
313
421
|
|
|
314
|
-
# Check if uv is available
|
|
315
422
|
if not shutil.which("uv"):
|
|
316
423
|
return
|
|
317
424
|
|
|
@@ -336,6 +443,7 @@ def create_uv_venv(docs_dir) -> None:
|
|
|
336
443
|
except Exception as e: # noqa: BLE001
|
|
337
444
|
print(f" ✗ Failed to create virtual environment: {e}")
|
|
338
445
|
|
|
446
|
+
|
|
339
447
|
def create_github_workflow(docs_dir) -> None:
|
|
340
448
|
"""Create .github/workflows/pages.yml for GitHub Pages deployment."""
|
|
341
449
|
workflow_dir = docs_dir.parent / ".github" / "workflows"
|
|
@@ -352,9 +460,25 @@ def create_github_workflow(docs_dir) -> None:
|
|
|
352
460
|
except Exception as e: # noqa: BLE001
|
|
353
461
|
print(f" ✗ Failed to create GitHub workflow: {e}")
|
|
354
462
|
|
|
355
|
-
|
|
356
|
-
|
|
463
|
+
|
|
464
|
+
def src_dir_has_content(src_dir: Path) -> bool:
|
|
465
|
+
"""Check if src directory has any files or folders."""
|
|
466
|
+
if not src_dir.exists():
|
|
467
|
+
return False
|
|
468
|
+
try:
|
|
469
|
+
return any(src_dir.iterdir())
|
|
470
|
+
except OSError:
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def create_default_files(docs_dir: Path, project_name: str) -> None:
|
|
475
|
+
"""Create default markdown files in src directory, but only if src is empty."""
|
|
357
476
|
src_dir = docs_dir / "src"
|
|
477
|
+
|
|
478
|
+
if src_dir_has_content(src_dir):
|
|
479
|
+
print(f" ⊘ {src_dir} already has content, skipping default markdown files")
|
|
480
|
+
return
|
|
481
|
+
|
|
358
482
|
src_dir.mkdir(parents=True, exist_ok=True)
|
|
359
483
|
|
|
360
484
|
files = {
|
|
@@ -369,9 +493,27 @@ def create_default_files(docs_dir, project_name) -> None:
|
|
|
369
493
|
filepath.write_text(content, encoding="utf-8")
|
|
370
494
|
print(f" ✓ Created {filepath}")
|
|
371
495
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
496
|
+
|
|
497
|
+
def create_config_only(docs_dir: Path) -> None:
|
|
498
|
+
"""Create only config.json in the specified directory."""
|
|
499
|
+
docs_dir = docs_dir.resolve()
|
|
500
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
501
|
+
|
|
502
|
+
config_path = docs_dir / "config.json"
|
|
503
|
+
if config_path.exists():
|
|
504
|
+
print(f" ⊘ {config_path} already exists, not overwriting")
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
config = DEFAULT_CONFIG.copy()
|
|
508
|
+
config["project_name"] = docs_dir.name.replace("-", " ").replace("_", " ").title()
|
|
509
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
510
|
+
json.dump(config, f, indent=2)
|
|
511
|
+
print(f" ✓ Created {config_path}")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def create_full_init(docs_dir: Path) -> None:
|
|
515
|
+
"""Create a full documentation project with config, markdown, assets, and workflow."""
|
|
516
|
+
docs_dir = docs_dir.resolve()
|
|
375
517
|
|
|
376
518
|
print(f"📁 Initializing fr-docs project in {docs_dir}")
|
|
377
519
|
|
|
@@ -387,11 +529,9 @@ def main(docs_dir_str="docs", config_only=False) -> None:
|
|
|
387
529
|
json.dump(config, f, indent=2)
|
|
388
530
|
print(f" ✓ Created {config_path}")
|
|
389
531
|
|
|
390
|
-
if
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
# Create default markdown files
|
|
394
|
-
create_default_files(docs_dir, config["project_name"])
|
|
532
|
+
# Create default markdown files (only if src is empty)
|
|
533
|
+
project_name = str(config["project_name"])
|
|
534
|
+
create_default_files(docs_dir, project_name)
|
|
395
535
|
|
|
396
536
|
# Download static assets
|
|
397
537
|
print("📥 Downloading static assets...")
|
|
@@ -416,5 +556,35 @@ def main(docs_dir_str="docs", config_only=False) -> None:
|
|
|
416
556
|
print(" 3. Run 'fr-docs build' to generate your site")
|
|
417
557
|
print(f" 4. Preview with 'cd {docs_dir}/site && python -m http.server 8000'")
|
|
418
558
|
|
|
559
|
+
|
|
560
|
+
def main(docs_dir_str="docs", target=None) -> None:
|
|
561
|
+
"""Initialize a new documentation project.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
docs_dir_str: Directory to initialize (default: "docs")
|
|
565
|
+
target: One of None (full init), "config" (config only), "fonts" (download fonts)
|
|
566
|
+
"""
|
|
567
|
+
docs_dir = Path(docs_dir_str).resolve()
|
|
568
|
+
|
|
569
|
+
if target == "config":
|
|
570
|
+
print(f"📁 Creating config.json in {docs_dir}")
|
|
571
|
+
create_config_only(docs_dir)
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
if target == "fonts":
|
|
575
|
+
print(f"📁 Downloading fonts in {docs_dir}")
|
|
576
|
+
sys.exit(run_fetch_fonts(docs_dir))
|
|
577
|
+
|
|
578
|
+
# Full init
|
|
579
|
+
create_full_init(docs_dir)
|
|
580
|
+
|
|
581
|
+
|
|
419
582
|
if __name__ == "__main__":
|
|
420
|
-
|
|
583
|
+
# Support legacy: fr-docs init [dir] [config]
|
|
584
|
+
# New: fr-docs init [dir] [config|fonts]
|
|
585
|
+
if len(sys.argv) > 2 and sys.argv[2] in ("config", "fonts"):
|
|
586
|
+
main(sys.argv[1], sys.argv[2])
|
|
587
|
+
elif len(sys.argv) > 1 and sys.argv[1] in ("config", "fonts"):
|
|
588
|
+
main("docs", sys.argv[1])
|
|
589
|
+
else:
|
|
590
|
+
main(sys.argv[1] if len(sys.argv) > 1 else "docs")
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "fr-docs"
|
|
9
|
-
version = "2.
|
|
9
|
+
version = "2.5.0"
|
|
10
10
|
description = "Fast static documentation generator with search, versioning, and zero-config setup"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.14"
|
|
@@ -50,7 +50,7 @@ Repository = "https://github.com/Omena0/fr-docs"
|
|
|
50
50
|
Issues = "https://github.com/Omena0/fr-docs/issues"
|
|
51
51
|
|
|
52
52
|
[project.scripts]
|
|
53
|
-
fr-docs = "fr_docs.
|
|
53
|
+
fr-docs = "fr_docs.__main__:main"
|
|
54
54
|
|
|
55
55
|
[tool.setuptools.packages.find]
|
|
56
56
|
include = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|