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.
Files changed (27) hide show
  1. {fr_docs-2.4.0 → fr_docs-2.5.0}/PKG-INFO +1 -1
  2. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/__main__.py +8 -8
  3. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/init.py +186 -16
  4. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/PKG-INFO +1 -1
  5. fr_docs-2.5.0/fr_docs.egg-info/entry_points.txt +2 -0
  6. {fr_docs-2.4.0 → fr_docs-2.5.0}/pyproject.toml +2 -2
  7. fr_docs-2.4.0/fr_docs.egg-info/entry_points.txt +0 -2
  8. {fr_docs-2.4.0 → fr_docs-2.5.0}/LICENSE.md +0 -0
  9. {fr_docs-2.4.0 → fr_docs-2.5.0}/README.md +0 -0
  10. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/__init__.py +0 -0
  11. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/build.py +0 -0
  12. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/config.py +0 -0
  13. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/config_accessors.py +0 -0
  14. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/frontmatter.py +0 -0
  15. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/git.py +0 -0
  16. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/html_pipeline.py +0 -0
  17. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/markdown.py +0 -0
  18. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/search.py +0 -0
  19. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/slug.py +0 -0
  20. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/syntax.py +0 -0
  21. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/template.py +0 -0
  22. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs/utils.py +0 -0
  23. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/SOURCES.txt +0 -0
  24. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/dependency_links.txt +0 -0
  25. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/requires.txt +0 -0
  26. {fr_docs-2.4.0 → fr_docs-2.5.0}/fr_docs.egg-info/top_level.txt +0 -0
  27. {fr_docs-2.4.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.4.0
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
@@ -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
- # `fr-docs init config` means "initialize config in ./docs".
91
- if args.dir == "config" and args.target is None:
92
- init_main("docs", True)
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 == "config")
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": "^\\s*([0-9]+[A-Za-z])\\s*[-:—–]\\s*(.+)",
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
- def download_file(url, dest_path) -> bool | None:
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
- with urlopen(url, timeout=30) as response:
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
- def create_default_files(docs_dir, project_name) -> None:
356
- """Create default markdown files in src directory."""
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
- def main(docs_dir_str="docs", config_only=False) -> None:
373
- """Initialize a new documentation project."""
374
- docs_dir = Path(docs_dir_str).resolve()
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 config_only:
391
- return
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
- main(sys.argv[1] if len(sys.argv) > 1 else "docs")
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fr-docs
3
- Version: 2.4.0
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
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fr-docs = fr_docs.__main__:main
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "fr-docs"
9
- version = "2.4.0"
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.build:main"
53
+ fr-docs = "fr_docs.__main__:main"
54
54
 
55
55
  [tool.setuptools.packages.find]
56
56
  include = [
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- fr-docs = fr_docs.build:main
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