fr-docs 3.4.1__tar.gz → 3.6.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 (28) hide show
  1. {fr_docs-3.4.1 → fr_docs-3.6.0}/PKG-INFO +1 -1
  2. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/build.py +4 -1
  3. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/git.py +3 -4
  4. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/html_pipeline.py +1 -1
  5. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/init.py +14 -4
  6. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/markdown.py +7 -11
  7. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/search.py +6 -6
  8. fr_docs-3.6.0/fr_docs/update.py +92 -0
  9. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs.egg-info/PKG-INFO +1 -1
  10. {fr_docs-3.4.1 → fr_docs-3.6.0}/pyproject.toml +6 -3
  11. fr_docs-3.4.1/fr_docs/update.py +0 -135
  12. {fr_docs-3.4.1 → fr_docs-3.6.0}/LICENSE.md +0 -0
  13. {fr_docs-3.4.1 → fr_docs-3.6.0}/README.md +0 -0
  14. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/__init__.py +0 -0
  15. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/__main__.py +0 -0
  16. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/config.py +0 -0
  17. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/config_accessors.py +0 -0
  18. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/frontmatter.py +0 -0
  19. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/slug.py +0 -0
  20. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/syntax.py +0 -0
  21. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/template.py +0 -0
  22. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs/utils.py +0 -0
  23. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs.egg-info/SOURCES.txt +0 -0
  24. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs.egg-info/dependency_links.txt +0 -0
  25. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs.egg-info/entry_points.txt +0 -0
  26. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs.egg-info/requires.txt +0 -0
  27. {fr_docs-3.4.1 → fr_docs-3.6.0}/fr_docs.egg-info/top_level.txt +0 -0
  28. {fr_docs-3.4.1 → fr_docs-3.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fr-docs
3
- Version: 3.4.1
3
+ Version: 3.6.0
4
4
  Summary: Fast static documentation generator with search, versioning, and zero-config setup
5
5
  Author-email: Omena0 <omena0mc@gmail.com>
6
6
  License: # PolyForm Noncommercial License 1.0.0
@@ -148,7 +148,7 @@ def _resolve_script_version(repo_root, script_rel_path, local_path):
148
148
  remote content is used instead.
149
149
  """
150
150
  local_version, local_hash = _script_version_hash(local_path)
151
- remote_version, remote_hash = _script_version_hash(
151
+ remote_version, _remote_hash = _script_version_hash(
152
152
  Path(repo_root, script_rel_path)
153
153
  )
154
154
 
@@ -569,6 +569,9 @@ def main(argv=None) -> None:
569
569
  if fonts_dir.is_dir():
570
570
  out_fonts = Path(config["_out_dir"]) / "fonts"
571
571
  out_fonts.mkdir(parents=True, exist_ok=True)
572
+ for stale in out_fonts.iterdir():
573
+ if stale.is_file():
574
+ stale.unlink()
572
575
  for f in fonts_dir.iterdir():
573
576
  if f.is_file():
574
577
  shutil.copy2(f, out_fonts / f.name)
@@ -57,13 +57,12 @@ def _build_pages_by_commit(out, slugs):
57
57
  elif current_commit:
58
58
  # Recover the slug from the file path: strip dir prefix and .md
59
59
  slug = line
60
- if slug.endswith(".md"):
61
- slug = slug[:-3]
60
+ slug = slug.removesuffix(".md")
62
61
  # Try to match against known slugs (handles prefix variations)
63
62
  matched = None
64
63
  for s in slugs:
65
- src_path = s + ".md"
66
- if line.endswith(src_path) or line.endswith(f"/{src_path}") or line == src_path:
64
+ src_path = f"{s}.md"
65
+ if line.endswith((src_path, f"/{src_path}")) or line == src_path:
67
66
  matched = s
68
67
  break
69
68
  if matched is None:
@@ -43,7 +43,7 @@ from .syntax import (
43
43
  process_blockquotes,
44
44
  )
45
45
  from .template import TEMPLATE, build_toc_sidebar
46
- from .utils import normalized_site_prefix, output_href, output_asset_href
46
+ from .utils import normalized_site_prefix, output_asset_href, output_href
47
47
 
48
48
 
49
49
  def _determine_tagged_sections(config):
@@ -293,6 +293,8 @@ jobs:
293
293
  uses: Omena0/fr-docs@main
294
294
  """
295
295
 
296
+ BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
297
+
296
298
  DEFAULT_CSS_URL = (
297
299
  "https://fonts.googleapis.com/css2?"
298
300
  "family=Inter:ital,opsz,wght@0,14..32,400..800"
@@ -303,9 +305,10 @@ FONT_FACE_RE = re.compile(
303
305
  r"@font-face\s*\{\n"
304
306
  r"\s*font-family:\s*['\"](?P<family>[^'\"]+)['\"]\s*;\n"
305
307
  r"\s*font-style:\s*(?P<style>\w+)\s*;\n"
306
- r"\s*font-weight:\s*(?P<weight>\d+)\s*;\n"
308
+ r"\s*font-weight:\s*(?P<weight>[\d\s]+)\s*;\n"
307
309
  r"\s*font-display:\s*(?P<display>\w+)\s*;\n"
308
310
  r"\s*src:\s*url\((?P<src>[^)]+)\)\s*format\(['\"](?P<format>[^'\"]+)['\"]\)\s*;\n"
311
+ r"(?:\s*unicode-range:\s*(?P<unicode_range>[^;]+);\n)?"
309
312
  r"\}"
310
313
  )
311
314
 
@@ -332,7 +335,7 @@ def download_file(url, dest_path) -> bool:
332
335
 
333
336
  def fetch_fonts_css(css_url: str) -> str:
334
337
  """Fetch Google Fonts CSS text."""
335
- req = urllib.request.Request(css_url, headers={"User-Agent": "fr-docs/1.0"})
338
+ req = urllib.request.Request(css_url, headers={"User-Agent": BROWSER_UA})
336
339
  with urllib.request.urlopen(req, timeout=30) as resp:
337
340
  return resp.read().decode("utf-8")
338
341
 
@@ -342,7 +345,7 @@ def download_font(url: str, dest: Path) -> bool:
342
345
  if dest.exists() and dest.stat().st_size > 0:
343
346
  return True
344
347
  try:
345
- req = urllib.request.Request(url, headers={"User-Agent": "fr-docs/1.0"})
348
+ req = urllib.request.Request(url, headers={"User-Agent": BROWSER_UA})
346
349
  with urllib.request.urlopen(req, timeout=60) as resp:
347
350
  data = resp.read()
348
351
  if len(data) < 1000:
@@ -371,11 +374,18 @@ def run_fetch_fonts(docs_dir: Path, css_url: str = DEFAULT_CSS_URL) -> int:
371
374
  print(f"Found {len(faces)} font-face rules")
372
375
 
373
376
  local_rules = []
374
- for family, style, weight, _display, src_url, fmt in faces:
377
+ seen = set()
378
+ for family, style, weight, _display, src_url, fmt, _unicode_range in faces:
375
379
  src_url = src_url.strip().strip("'\"")
376
380
  fname = Path(src_url.split("?")[0]).name
377
381
  if not fname:
378
382
  continue
383
+ # De-duplicate: woff2 serves one file per unicode-range subset; keep the
384
+ # first occurrence per (family, weight, style) which is the latin block.
385
+ key = (family, weight, style)
386
+ if key in seen:
387
+ continue
388
+ seen.add(key)
379
389
  dest = out_dir / fname
380
390
  print(f" {family} {weight} {style} -> {fname}")
381
391
  if not download_font(src_url, dest):
@@ -13,7 +13,6 @@ from markdown.extensions.tables import TableExtension
13
13
  from .slug import (
14
14
  normalize_slug,
15
15
  relative_slug_path,
16
- slug_output_name,
17
16
  )
18
17
  from .syntax import URL_ATTR_RE
19
18
 
@@ -38,20 +37,17 @@ def _parse_code_location(location):
38
37
  location = str(location or "").strip()
39
38
  if not location:
40
39
  return {}
40
+
41
41
  if re.fullmatch(r"\d+", location):
42
42
  return {"line": int(location)}
43
- range_match = re.fullmatch(r"(\d+)-(\d+)(?::\d+)?", location)
44
- if range_match:
45
- return {
46
- "line": int(range_match.group(1)),
47
- "end_line": int(range_match.group(2)),
48
- }
43
+
44
+ if range_match := re.fullmatch(r"(\d+)-(\d+)(?::\d+)?", location):
45
+ return {"line": int(range_match[1]), "end_line": int(range_match[2])}
46
+
49
47
  column_match = re.fullmatch(r"(\d+):(\d+)", location)
50
48
  if column_match:
51
- return {
52
- "line": int(column_match.group(1)),
53
- "column": int(column_match.group(2)),
54
- }
49
+ return {"line": int(column_match[1]), "column": int(column_match[2])}
50
+
55
51
  return {"function": location}
56
52
 
57
53
 
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  from .config_accessors import feature_enabled
7
7
  from .frontmatter import parse_frontmatter
8
- from .slug import normalize_slug, relative_slug_path, slug_output_name
8
+ from .slug import normalize_slug, slug_output_name
9
9
  from .utils import output_href
10
10
 
11
11
  # Symbol extraction patterns for various languages
@@ -229,18 +229,18 @@ def extract_symbols_from_content(content, language):
229
229
  if symbol_type in ("function", "method", "decorator_function") and language == "python":
230
230
  # Find function end by indentation
231
231
  indent_match = re.match(r"^[^\S\n]*", lines[line_num - 1])
232
- indent = len(indent_match.group(0)) if indent_match else 0
232
+ indent = len(indent_match[0]) if indent_match else 0
233
233
  end_line = line_num
234
+
234
235
  for i in range(line_num, len(lines)):
235
236
  if lines[i].strip():
236
237
  indent_match = re.match(r"^[^\S\n]*", lines[i])
237
- line_indent = len(indent_match.group(0)) if indent_match else 0
238
+ line_indent = len(indent_match[0]) if indent_match else 0
238
239
  if line_indent <= indent:
239
240
  end_line = i
240
241
  break
241
- end_line = i + 1
242
- else:
243
- end_line = i + 1
242
+
243
+ end_line = i + 1
244
244
 
245
245
  symbols.append(
246
246
  {
@@ -0,0 +1,92 @@
1
+ """Update fr-docs resources (script.js, style.css, fonts) from upstream."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ from .build import (
7
+ _script_version_hash,
8
+ _write_script_js,
9
+ )
10
+ from .config import load_config
11
+ from .config_accessors import project_name
12
+ from .init import (
13
+ FAVICON_SVG_URL,
14
+ SCRIPT_JS_URL,
15
+ STYLE_CSS_URL,
16
+ download_file,
17
+ run_fetch_fonts,
18
+ )
19
+
20
+
21
+ def update_resources(config):
22
+ """Update script.js, style.css, favicon.svg, and fonts from the fr-docs upstream repo.
23
+
24
+ Downloads the latest resources from raw.githubusercontent.com and writes them
25
+ to the local docs/ directory. For script.js, preserves version numbering
26
+ by reading the version from the downloaded content.
27
+ """
28
+ docs_path = Path(config["_docs_dir"]).resolve()
29
+ updated = []
30
+
31
+ # Update script.js
32
+ with tempfile.NamedTemporaryFile(mode="w+", suffix=".js", delete=False) as tmp:
33
+ tmp_path = Path(tmp.name)
34
+ try:
35
+ if download_file(SCRIPT_JS_URL, tmp_path):
36
+ version, _ = _script_version_hash(tmp_path)
37
+ content = tmp_path.read_text(encoding="utf-8")
38
+ local_path = docs_path / "script.js"
39
+ _write_script_js(local_path, version, content)
40
+ updated.append(f"script.js (v{version})")
41
+ finally:
42
+ tmp_path.unlink(missing_ok=True)
43
+
44
+ # Update style.css
45
+ local_style = docs_path / "style.css"
46
+ if download_file(STYLE_CSS_URL, local_style):
47
+ updated.append("style.css")
48
+
49
+ # Update favicon.svg
50
+ local_favicon = docs_path / "favicon.svg"
51
+ if download_file(FAVICON_SVG_URL, local_favicon):
52
+ updated.append("favicon.svg")
53
+
54
+ # Update fonts (download from Google Fonts, generate fonts.css)
55
+ fonts_dir = docs_path / "fonts"
56
+ if fonts_dir.is_dir():
57
+ old_count = len(list(fonts_dir.iterdir()))
58
+ result = run_fetch_fonts(docs_path)
59
+ if result == 0:
60
+ new_count = len(list(fonts_dir.iterdir()))
61
+ if new_count > old_count:
62
+ updated.append(f"fonts ({new_count - old_count} new)")
63
+ else:
64
+ updated.append("fonts (refreshed)")
65
+
66
+ if updated:
67
+ print(f" Updated: {', '.join(updated)}")
68
+ else:
69
+ print(" No resources updated.")
70
+
71
+
72
+ def main(argv=None) -> None:
73
+ import argparse
74
+
75
+ parser = argparse.ArgumentParser(
76
+ prog="fr-docs update",
77
+ description="Update fr-docs resources (script.js, style.css, fonts) from upstream.",
78
+ )
79
+ parser.add_argument(
80
+ "--config",
81
+ help="Path to the configuration JSON file.",
82
+ )
83
+ args = parser.parse_args(argv)
84
+
85
+ config = load_config(args.config)
86
+ print(f"🔄 Updating {project_name(config)} resources...")
87
+ update_resources(config)
88
+ print("✅ Update complete.")
89
+
90
+
91
+ if __name__ == "__main__":
92
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fr-docs
3
- Version: 3.4.1
3
+ Version: 3.6.0
4
4
  Summary: Fast static documentation generator with search, versioning, and zero-config setup
5
5
  Author-email: Omena0 <omena0mc@gmail.com>
6
6
  License: # PolyForm Noncommercial License 1.0.0
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "fr-docs"
9
- version = "3.4.1"
9
+ version = "3.6.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"
@@ -106,5 +106,8 @@ fixable = [
106
106
  "ALL",
107
107
  ]
108
108
 
109
- [tool.mypy]
110
- ignore_missing_imports = true
109
+ [tool.refurb]
110
+ ignore = [
111
+ "FURB107",
112
+ "FURB120",
113
+ ]
@@ -1,135 +0,0 @@
1
- """Update fr-docs resources (script.js, style.css, fonts) from the repo."""
2
-
3
- import json
4
- import shutil
5
- import subprocess
6
- import sys
7
- from pathlib import Path
8
-
9
- from .config import load_config
10
- from .config_accessors import out_dir, project_name, src_dir as config_src_dir
11
- from .build import (
12
- _resolve_script_version,
13
- _write_script_js,
14
- _minify_static_asset,
15
- _script_version_hash,
16
- )
17
-
18
-
19
- def _fetch_remote(repo_root, rel_path):
20
- """Fetch a file from the fr-docs repo via git show HEAD:path."""
21
- try:
22
- return subprocess.check_output(
23
- ["git", "show", f"HEAD:{rel_path}"],
24
- cwd=repo_root,
25
- stderr=subprocess.DEVNULL,
26
- ).decode("utf-8", errors="replace")
27
- except (subprocess.CalledProcessError, OSError):
28
- return None
29
-
30
-
31
- def _git_current_commit(repo_root):
32
- try:
33
- return subprocess.check_output(
34
- ["git", "rev-parse", "HEAD"],
35
- cwd=repo_root,
36
- stderr=subprocess.DEVNULL,
37
- ).decode("utf-8", errors="replace").strip()
38
- except (subprocess.CalledProcessError, OSError):
39
- return None
40
-
41
-
42
- def _git_remote_url(repo_root):
43
- try:
44
- return subprocess.check_output(
45
- ["git", "remote", "get-url", "origin"],
46
- cwd=repo_root,
47
- stderr=subprocess.DEVNULL,
48
- ).decode("utf-8", errors="replace").strip()
49
- except (subprocess.CalledProcessError, OSError):
50
- return None
51
-
52
-
53
- def _is_fr_docs_repo(repo_root):
54
- """Return True if the repo is the fr-docs repo itself."""
55
- url = _git_remote_url(repo_root) or ""
56
- return "Omena0/fr-docs" in url or "omena0/fr-docs" in url.lower()
57
-
58
-
59
- def update_resources(config):
60
- """Update script.js, style.css, favicon.svg, and fonts from the fr-docs repo.
61
-
62
- Only runs when the current repo IS the fr-docs repo (so we can read
63
- the source files from git). For downstream repos, the resources are
64
- pulled from PyPI via the `fr-docs` package or copied from the docs/
65
- directory.
66
- """
67
- docs_path = Path(config["_docs_dir"]).resolve()
68
- repo_root = docs_path.parent
69
- if not _is_fr_docs_repo(repo_root):
70
- print(" Not the fr-docs repo; skipping remote resource update.")
71
- return
72
-
73
- resources = ["script.js", "style.css", "favicon.svg"]
74
- updated = []
75
- for name in resources:
76
- rel = f"docs/{name}"
77
- remote = _fetch_remote(repo_root, rel)
78
- if remote is None:
79
- continue
80
- local_path = docs_path / name
81
- if name == "script.js":
82
- version, content = _resolve_script_version(repo_root, rel, local_path)
83
- _write_script_js(local_path, version, content)
84
- updated.append(f"{name} (v{version})")
85
- else:
86
- local_path.write_text(remote, encoding="utf-8")
87
- updated.append(name)
88
-
89
- # Update fonts
90
- fonts_dir = docs_path / "fonts"
91
- if fonts_dir.is_dir():
92
- remote_fonts = _fetch_remote(repo_root, "docs/fonts")
93
- # Can't fetch a directory; instead list remote files via git ls-tree
94
- try:
95
- out = subprocess.check_output(
96
- ["git", "ls-tree", "-r", "--name-only", "HEAD", "docs/fonts"],
97
- cwd=repo_root,
98
- stderr=subprocess.DEVNULL,
99
- ).decode("utf-8", errors="replace")
100
- for line in out.splitlines():
101
- fname = Path(line).name
102
- content = _fetch_remote(repo_root, line)
103
- if content is not None:
104
- (fonts_dir / fname).write_text(content, encoding="utf-8")
105
- updated.append(f"fonts/{fname}")
106
- except (subprocess.CalledProcessError, OSError):
107
- pass
108
-
109
- if updated:
110
- print(f" Updated: {', '.join(updated)}")
111
- else:
112
- print(" No resources updated.")
113
-
114
-
115
- def main(argv=None) -> None:
116
- import argparse
117
-
118
- parser = argparse.ArgumentParser(
119
- prog="fr-docs update",
120
- description="Update fr-docs resources (script.js, style.css, fonts) from the repo.",
121
- )
122
- parser.add_argument(
123
- "--config",
124
- help="Path to the configuration JSON file.",
125
- )
126
- args = parser.parse_args(argv)
127
-
128
- config = load_config(args.config)
129
- print(f"🔄 Updating {project_name(config)} resources...")
130
- update_resources(config)
131
- print("✅ Update complete.")
132
-
133
-
134
- if __name__ == "__main__":
135
- 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