fr-docs 2.5.0__tar.gz → 3.1.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.5.0 → fr_docs-3.1.0}/PKG-INFO +1 -1
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/__main__.py +1 -1
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/build.py +19 -9
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/git.py +27 -4
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/html_pipeline.py +23 -13
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/markdown.py +12 -6
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/search.py +2 -2
- fr_docs-3.1.0/fr_docs/slug.py +79 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/template.py +2 -2
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs.egg-info/PKG-INFO +1 -1
- {fr_docs-2.5.0 → fr_docs-3.1.0}/pyproject.toml +1 -1
- fr_docs-2.5.0/fr_docs/slug.py +0 -71
- {fr_docs-2.5.0 → fr_docs-3.1.0}/LICENSE.md +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/README.md +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/__init__.py +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/config.py +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/config_accessors.py +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/frontmatter.py +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/init.py +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/syntax.py +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs/utils.py +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs.egg-info/SOURCES.txt +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs.egg-info/dependency_links.txt +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs.egg-info/entry_points.txt +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs.egg-info/requires.txt +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/fr_docs.egg-info/top_level.txt +0 -0
- {fr_docs-2.5.0 → fr_docs-3.1.0}/setup.cfg +0 -0
|
@@ -626,12 +626,25 @@ def main(argv=None) -> None:
|
|
|
626
626
|
if prefix_dir.is_symlink() or prefix_dir.exists():
|
|
627
627
|
shutil.rmtree(prefix_dir, ignore_errors=True)
|
|
628
628
|
prefix_dir.mkdir(parents=True, exist_ok=True)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
629
|
+
symlinked = 0
|
|
630
|
+
for f in sorted(Path(config["_out_dir"]).rglob("*")):
|
|
631
|
+
if not f.is_file() or f.name.startswith("."):
|
|
632
|
+
continue
|
|
633
|
+
# Skip files inside the prefix directory itself
|
|
634
|
+
try:
|
|
635
|
+
f.relative_to(prefix_dir)
|
|
636
|
+
continue
|
|
637
|
+
except ValueError:
|
|
638
|
+
pass
|
|
639
|
+
rel = f.relative_to(config["_out_dir"])
|
|
640
|
+
link_path = prefix_dir / rel
|
|
641
|
+
link_path.parent.mkdir(parents=True, exist_ok=True)
|
|
642
|
+
if link_path.is_symlink() or link_path.exists():
|
|
643
|
+
with contextlib.suppress(OSError):
|
|
644
|
+
link_path.unlink()
|
|
645
|
+
link_path.symlink_to(f.resolve())
|
|
646
|
+
symlinked += 1
|
|
647
|
+
print(f" ✓ Symlinked {symlinked} files into {prefix}/")
|
|
635
648
|
except OSError as e:
|
|
636
649
|
print(f" ✗ Failed to create {prefix}/ symlinks: {e}")
|
|
637
650
|
|
|
@@ -646,6 +659,3 @@ def get_all_slugs(config):
|
|
|
646
659
|
|
|
647
660
|
return slugs
|
|
648
661
|
|
|
649
|
-
|
|
650
|
-
if __name__ == "__main__":
|
|
651
|
-
main()
|
|
@@ -38,7 +38,13 @@ def _parse_commit_log(log_out):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def _build_pages_by_commit(out, slugs):
|
|
41
|
-
"""Build pages_by_commit mapping from git log output.
|
|
41
|
+
"""Build pages_by_commit mapping from git log output.
|
|
42
|
+
|
|
43
|
+
Each slug in ``slugs`` is a normalized slug like ``core/entity``. The
|
|
44
|
+
git log ``--name-only`` output lists file paths like
|
|
45
|
+
``docs/src/core/entity.md``; we strip the configured source prefix
|
|
46
|
+
and ``.md`` suffix to recover the slug.
|
|
47
|
+
"""
|
|
42
48
|
pages_by_commit = {}
|
|
43
49
|
current_commit = None
|
|
44
50
|
for line in out.splitlines():
|
|
@@ -49,9 +55,26 @@ def _build_pages_by_commit(out, slugs):
|
|
|
49
55
|
if re.fullmatch(r"[0-9a-f]{7,40}", line):
|
|
50
56
|
current_commit = line
|
|
51
57
|
elif current_commit:
|
|
52
|
-
slug
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
# Recover the slug from the file path: strip dir prefix and .md
|
|
59
|
+
slug = line
|
|
60
|
+
if slug.endswith(".md"):
|
|
61
|
+
slug = slug[:-3]
|
|
62
|
+
# Try to match against known slugs (handles prefix variations)
|
|
63
|
+
matched = None
|
|
64
|
+
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:
|
|
67
|
+
matched = s
|
|
68
|
+
break
|
|
69
|
+
if matched is None:
|
|
70
|
+
# Fall back to basename matching for flat layouts
|
|
71
|
+
base = Path(line).stem
|
|
72
|
+
for s in slugs:
|
|
73
|
+
if s.rsplit("/", 1)[-1] == base:
|
|
74
|
+
matched = s
|
|
75
|
+
break
|
|
76
|
+
if matched is not None:
|
|
77
|
+
pages_by_commit.setdefault(current_commit, []).append(matched)
|
|
55
78
|
return pages_by_commit
|
|
56
79
|
|
|
57
80
|
|
|
@@ -35,7 +35,7 @@ from .markdown import (
|
|
|
35
35
|
rewrite_md_links,
|
|
36
36
|
)
|
|
37
37
|
from .search import search_include_config
|
|
38
|
-
from .slug import slug_output_name
|
|
38
|
+
from .slug import relative_slug_path, slug_output_name
|
|
39
39
|
from .syntax import (
|
|
40
40
|
URL_ATTR_RE,
|
|
41
41
|
format_custom_tags,
|
|
@@ -282,7 +282,7 @@ def _render_backlinks_and_related(body_md, body_html, slug, config, search_index
|
|
|
282
282
|
and page_data.get("backlinks")
|
|
283
283
|
and not no_backlinks
|
|
284
284
|
):
|
|
285
|
-
backlinks_html = _render_backlinks(page_data["backlinks"], search_index)
|
|
285
|
+
backlinks_html = _render_backlinks(page_data["backlinks"], search_index, slug)
|
|
286
286
|
if has_backlinks_tag:
|
|
287
287
|
body_html = body_html.replace("<p><backlinks></p>", backlinks_html)
|
|
288
288
|
body_html = body_html.replace("<p><backlinks></p>\n", backlinks_html)
|
|
@@ -293,7 +293,7 @@ def _render_backlinks_and_related(body_md, body_html, slug, config, search_index
|
|
|
293
293
|
and page_data.get("related")
|
|
294
294
|
and not no_related
|
|
295
295
|
):
|
|
296
|
-
related_html = _render_related(page_data["related"], search_index)
|
|
296
|
+
related_html = _render_related(page_data["related"], search_index, slug)
|
|
297
297
|
if has_related_tag:
|
|
298
298
|
body_html = body_html.replace("<p><related></p>", related_html)
|
|
299
299
|
body_html = body_html.replace("<p><related></p>\n", related_html)
|
|
@@ -332,7 +332,7 @@ def build_page(slug, config, slug_page_keys) -> None:
|
|
|
332
332
|
body_html = rewrite_md_links(body_html, slug, slug_page_keys)
|
|
333
333
|
|
|
334
334
|
if feature_enabled(config, "auto_link"):
|
|
335
|
-
body_html = auto_link_filenames(body_html, config.get("_slug_page_keys", {}))
|
|
335
|
+
body_html = auto_link_filenames(body_html, config.get("_slug_page_keys", {}), slug)
|
|
336
336
|
|
|
337
337
|
code_refs = []
|
|
338
338
|
if feature_enabled(config, "code_references"):
|
|
@@ -378,6 +378,7 @@ def build_page(slug, config, slug_page_keys) -> None:
|
|
|
378
378
|
|
|
379
379
|
out_name = slug_output_name(slug, config)
|
|
380
380
|
out_path = Path(config["_out_dir"], out_name)
|
|
381
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
381
382
|
out_path.write_text(out_html, encoding="utf-8")
|
|
382
383
|
|
|
383
384
|
|
|
@@ -389,9 +390,11 @@ def _strip_site_prefix(out_dir, stripped_prefix, backed_up):
|
|
|
389
390
|
prefix_pat = re.compile(
|
|
390
391
|
rf'href=(["\']?){re.escape(stripped_prefix)}/([^"\'\s>]+)\1'
|
|
391
392
|
)
|
|
392
|
-
for html_file in out_dir.
|
|
393
|
+
for html_file in out_dir.rglob("*.html"):
|
|
394
|
+
if html_file.name.startswith("."):
|
|
395
|
+
continue
|
|
393
396
|
raw = html_file.read_text(encoding="utf-8")
|
|
394
|
-
backup =
|
|
397
|
+
backup = html_file.parent / f".critical_orig_{html_file.stem}.html"
|
|
395
398
|
backup.write_text(raw, encoding="utf-8")
|
|
396
399
|
backed_up.append(backup)
|
|
397
400
|
rewritten = prefix_pat.sub(
|
|
@@ -436,7 +439,8 @@ def _run_critical_on_file(html_file, out_dir):
|
|
|
436
439
|
def _restore_backups(out_dir, backed_up):
|
|
437
440
|
"""Restore original HTML for pages critical didn't inline, and clean up backups."""
|
|
438
441
|
for backup in backed_up:
|
|
439
|
-
|
|
442
|
+
# Backup is stored alongside the original: <dir>/.critical_orig_<stem>.html
|
|
443
|
+
target = backup.parent / f"{backup.stem.replace('.critical_orig_', '')}.html"
|
|
440
444
|
if target.exists() and backup.exists():
|
|
441
445
|
target_content = target.read_text(encoding="utf-8")
|
|
442
446
|
if "data-critical" not in target_content:
|
|
@@ -478,7 +482,9 @@ def optimize_all_pages(config) -> None:
|
|
|
478
482
|
|
|
479
483
|
try:
|
|
480
484
|
_strip_site_prefix(out_dir, stripped_prefix, backed_up)
|
|
481
|
-
for html_file in out_dir.
|
|
485
|
+
for html_file in out_dir.rglob("*.html"):
|
|
486
|
+
if html_file.name.startswith("."):
|
|
487
|
+
continue
|
|
482
488
|
_run_critical_on_file(html_file, out_dir)
|
|
483
489
|
finally:
|
|
484
490
|
_restore_backups(out_dir, backed_up)
|
|
@@ -495,7 +501,9 @@ def minify_all_pages(config) -> None:
|
|
|
495
501
|
if not out_dir.is_dir():
|
|
496
502
|
return
|
|
497
503
|
|
|
498
|
-
for html_file in sorted(out_dir.
|
|
504
|
+
for html_file in sorted(out_dir.rglob("*.html")):
|
|
505
|
+
if html_file.name.startswith("."):
|
|
506
|
+
continue
|
|
499
507
|
try:
|
|
500
508
|
raw = html_file.read_text(encoding="utf-8")
|
|
501
509
|
minified = minify_html(raw, config)
|
|
@@ -555,7 +563,7 @@ def add_internal_prefetch_links(html_text, config):
|
|
|
555
563
|
logger = logging.getLogger(__name__)
|
|
556
564
|
|
|
557
565
|
|
|
558
|
-
def _render_backlinks(backlinks, search_index)
|
|
566
|
+
def _render_backlinks(backlinks, search_index, current_slug=""):
|
|
559
567
|
"""Render backlinks HTML."""
|
|
560
568
|
if not backlinks:
|
|
561
569
|
return ""
|
|
@@ -564,9 +572,9 @@ def _render_backlinks(backlinks, search_index) -> str:
|
|
|
564
572
|
if page := next((p for p in search_index if p.get("slug") == bl_slug), None):
|
|
565
573
|
url = page.get("url", f"{bl_slug}.html")
|
|
566
574
|
title = page.get("title", bl_slug)
|
|
575
|
+
if not url.startswith("http"):
|
|
576
|
+
url = relative_slug_path(current_slug, bl_slug)
|
|
567
577
|
items.append(f'<li><a href="{url}">{title}</a></li>')
|
|
568
|
-
if not items:
|
|
569
|
-
return ""
|
|
570
578
|
return (
|
|
571
579
|
f"<h2>Backlinks</h2>"
|
|
572
580
|
f'<details class="backlinks-details">'
|
|
@@ -580,7 +588,7 @@ def _render_backlinks(backlinks, search_index) -> str:
|
|
|
580
588
|
)
|
|
581
589
|
|
|
582
590
|
|
|
583
|
-
def _render_related(related, search_index)
|
|
591
|
+
def _render_related(related, search_index, current_slug=""):
|
|
584
592
|
"""Render related pages HTML."""
|
|
585
593
|
if not related:
|
|
586
594
|
return ""
|
|
@@ -589,6 +597,8 @@ def _render_related(related, search_index) -> str:
|
|
|
589
597
|
if page := next((p for p in search_index if p.get("slug") == rel_slug), None):
|
|
590
598
|
url = page.get("url", f"{rel_slug}.html")
|
|
591
599
|
title = page.get("title", rel_slug)
|
|
600
|
+
if not url.startswith("http"):
|
|
601
|
+
url = relative_slug_path(current_slug, rel_slug)
|
|
592
602
|
items.append(f'<li><a href="{url}">{title}</a></li>')
|
|
593
603
|
|
|
594
604
|
if not items:
|
|
@@ -12,6 +12,7 @@ from markdown.extensions.tables import TableExtension
|
|
|
12
12
|
|
|
13
13
|
from .slug import (
|
|
14
14
|
normalize_slug,
|
|
15
|
+
relative_slug_path,
|
|
15
16
|
slug_output_name,
|
|
16
17
|
)
|
|
17
18
|
from .syntax import URL_ATTR_RE
|
|
@@ -91,7 +92,12 @@ def convert_markdown(text):
|
|
|
91
92
|
|
|
92
93
|
|
|
93
94
|
def resolve_md_target(md_target, current_slug, slug_page_keys):
|
|
94
|
-
"""Resolve a markdown link target to
|
|
95
|
+
"""Resolve a markdown link target to a page-relative output HTML filename.
|
|
96
|
+
|
|
97
|
+
Returns a path relative to the current page (e.g. ``../index.html`` from
|
|
98
|
+
``core/entity.html`` to ``index.html``) so links work at any depth in the
|
|
99
|
+
directory tree.
|
|
100
|
+
"""
|
|
95
101
|
raw = str(md_target or "").strip()
|
|
96
102
|
if not raw:
|
|
97
103
|
return raw
|
|
@@ -116,9 +122,9 @@ def resolve_md_target(md_target, current_slug, slug_page_keys):
|
|
|
116
122
|
|
|
117
123
|
for candidate in candidates:
|
|
118
124
|
if candidate in slug_page_keys:
|
|
119
|
-
return
|
|
125
|
+
return relative_slug_path(current_slug, candidate)
|
|
120
126
|
|
|
121
|
-
return
|
|
127
|
+
return relative_slug_path(current_slug, normalize_slug(raw))
|
|
122
128
|
|
|
123
129
|
|
|
124
130
|
def rewrite_md_links(html_text, current_slug, slug_page_keys):
|
|
@@ -139,7 +145,7 @@ def rewrite_md_links(html_text, current_slug, slug_page_keys):
|
|
|
139
145
|
)
|
|
140
146
|
|
|
141
147
|
|
|
142
|
-
def auto_link_filenames(html_text, slug_page_keys):
|
|
148
|
+
def auto_link_filenames(html_text, slug_page_keys, current_slug=""):
|
|
143
149
|
"""Auto-link bare filename references like `config.json` to their .md pages.
|
|
144
150
|
|
|
145
151
|
Only processes filenames INSIDE inline code tags (<code>...</code>).
|
|
@@ -148,12 +154,12 @@ def auto_link_filenames(html_text, slug_page_keys):
|
|
|
148
154
|
if not slug_page_keys:
|
|
149
155
|
return html_text
|
|
150
156
|
|
|
151
|
-
# Build a map of filename (without .md) -> output HTML file
|
|
157
|
+
# Build a map of filename (without .md) -> page-relative output HTML file
|
|
152
158
|
filename_map = {}
|
|
153
159
|
for slug in slug_page_keys:
|
|
154
160
|
filename = slug.rsplit("/", 1)[-1]
|
|
155
161
|
if filename:
|
|
156
|
-
output =
|
|
162
|
+
output = relative_slug_path(current_slug, slug)
|
|
157
163
|
filename_map[filename.lower()] = output
|
|
158
164
|
filename_map[f"{filename}.md".lower()] = output
|
|
159
165
|
|
|
@@ -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,
|
|
8
|
+
from .slug import normalize_slug, relative_slug_path, slug_output_name
|
|
9
9
|
from .utils import output_href
|
|
10
10
|
|
|
11
11
|
# Symbol extraction patterns for various languages
|
|
@@ -536,7 +536,7 @@ def build_search_index(slugs, config):
|
|
|
536
536
|
title = meta.get("title", slug.capitalize())
|
|
537
537
|
sections = _parse_search_sections(body_md)
|
|
538
538
|
|
|
539
|
-
page_key =
|
|
539
|
+
page_key = normalize_slug(slug)
|
|
540
540
|
url = output_href(slug_output_name(slug, config), config)
|
|
541
541
|
search_index.append(
|
|
542
542
|
{
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Slug normalization and output-key utilities for fr-docs."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def slug_basename(slug):
|
|
5
|
+
"""Extract the filename part from a possibly prefixed slug (e.g. 'core/entity' → 'entity')."""
|
|
6
|
+
normalized = str(slug).replace("\\", "/").strip("/")
|
|
7
|
+
return normalized.rsplit("/", 1)[-1] if normalized else ""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_slug(slug):
|
|
11
|
+
"""Normalize slugs to forward-slash format for cross-platform consistency.
|
|
12
|
+
Also resolves relative path segments like '..' and '.'.
|
|
13
|
+
"""
|
|
14
|
+
# Convert to forward slashes and strip
|
|
15
|
+
normalized = str(slug).replace("\\", "/").strip("/")
|
|
16
|
+
# Resolve relative path segments
|
|
17
|
+
parts = []
|
|
18
|
+
for part in normalized.split("/"):
|
|
19
|
+
if part == "..":
|
|
20
|
+
if parts:
|
|
21
|
+
parts.pop()
|
|
22
|
+
elif part and part != ".":
|
|
23
|
+
parts.append(part)
|
|
24
|
+
return "/".join(parts)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_slug_page_keys(slugs):
|
|
28
|
+
"""Build a mapping of normalized slug → normalized slug (identity mapping).
|
|
29
|
+
|
|
30
|
+
With directory-tree output, file name collisions are avoided by
|
|
31
|
+
preserving the directory structure in the output path, so no
|
|
32
|
+
disambiguation suffixes are needed.
|
|
33
|
+
"""
|
|
34
|
+
return {normalize_slug(slug): normalize_slug(slug) for slug in slugs}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def slug_page_key(slug, config):
|
|
38
|
+
"""Return the stable output key for a slug (the normalized slug)."""
|
|
39
|
+
return normalize_slug(slug)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def slug_output_name(slug, config=None) -> str:
|
|
43
|
+
"""Return output HTML filename for a slug, preserving directory structure.
|
|
44
|
+
|
|
45
|
+
Slugs like 'core/entity' produce 'core/entity.html'. The 'index' slug
|
|
46
|
+
at the root produces 'index.html'; an 'index' nested under a directory
|
|
47
|
+
(e.g. 'core/index') produces 'core/index.html'.
|
|
48
|
+
"""
|
|
49
|
+
if config is None:
|
|
50
|
+
return f"{slug}.html"
|
|
51
|
+
return f"{normalize_slug(slug)}.html"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def relative_slug_path(from_slug, to_slug):
|
|
55
|
+
"""Compute the page-relative path from ``from_slug`` to ``to_slug``.
|
|
56
|
+
|
|
57
|
+
Returns a string like ``../index.html`` or ``core/entity.html`` suitable
|
|
58
|
+
for use as an ``href`` value in a page at ``from_slug``.
|
|
59
|
+
"""
|
|
60
|
+
from_name = slug_output_name(from_slug)
|
|
61
|
+
to_name = slug_output_name(to_slug)
|
|
62
|
+
|
|
63
|
+
from_parts = from_name.split("/")
|
|
64
|
+
to_parts = to_name.split("/")
|
|
65
|
+
|
|
66
|
+
# Find common prefix
|
|
67
|
+
i = 0
|
|
68
|
+
while i < min(len(from_parts), len(to_parts)) and from_parts[i] == to_parts[i]:
|
|
69
|
+
i += 1
|
|
70
|
+
|
|
71
|
+
# Number of directory levels to go up (from the from_page's directory)
|
|
72
|
+
# from_parts has the filename as the last element; we need to go up
|
|
73
|
+
# from the directory containing from_name.
|
|
74
|
+
up = len(from_parts) - 1 - i
|
|
75
|
+
# Remaining path to the target
|
|
76
|
+
down = to_parts[i:]
|
|
77
|
+
|
|
78
|
+
parts = [".."] * up + down
|
|
79
|
+
return "/".join(parts) if parts else to_name
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
5
|
from .config_accessors import feature_enabled
|
|
6
|
-
from .slug import
|
|
6
|
+
from .slug import relative_slug_path
|
|
7
7
|
|
|
8
8
|
TEMPLATE = """\
|
|
9
9
|
<!DOCTYPE html>
|
|
@@ -141,7 +141,7 @@ def build_sidebar_html(current_slug, sidebar_config, tagged_sections=None, confi
|
|
|
141
141
|
)
|
|
142
142
|
for slug, label in pages:
|
|
143
143
|
active = ' class="active"' if slug == current_slug else ""
|
|
144
|
-
href =
|
|
144
|
+
href = relative_slug_path(current_slug, slug)
|
|
145
145
|
display = label
|
|
146
146
|
parts.append(f' <li><a href="{href}"{active}>{display}</a></li>')
|
|
147
147
|
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "fr-docs"
|
|
9
|
-
version = "
|
|
9
|
+
version = "3.1.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"
|
fr_docs-2.5.0/fr_docs/slug.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Slug normalization and output-key utilities for fr-docs."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def slug_basename(slug):
|
|
5
|
-
"""Extract the filename part from a possibly prefixed slug (e.g. 'core/entity' → 'entity')."""
|
|
6
|
-
normalized = str(slug).replace("\\", "/").strip("/")
|
|
7
|
-
return normalized.rsplit("/", 1)[-1] if normalized else ""
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def normalize_slug(slug):
|
|
11
|
-
"""Normalize slugs to forward-slash format for cross-platform consistency.
|
|
12
|
-
Also resolves relative path segments like '..' and '.'.
|
|
13
|
-
"""
|
|
14
|
-
# Convert to forward slashes and strip
|
|
15
|
-
normalized = str(slug).replace("\\", "/").strip("/")
|
|
16
|
-
# Resolve relative path segments
|
|
17
|
-
parts = []
|
|
18
|
-
for part in normalized.split("/"):
|
|
19
|
-
if part == "..":
|
|
20
|
-
if parts:
|
|
21
|
-
parts.pop()
|
|
22
|
-
elif part and part != ".":
|
|
23
|
-
parts.append(part)
|
|
24
|
-
return "/".join(parts)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def build_slug_page_keys(slugs):
|
|
28
|
-
"""Build unique output keys for slugs, disambiguating basename collisions."""
|
|
29
|
-
groups = {}
|
|
30
|
-
for slug in slugs:
|
|
31
|
-
norm = normalize_slug(slug)
|
|
32
|
-
base = slug_basename(norm).lower()
|
|
33
|
-
groups.setdefault(base, []).append(norm)
|
|
34
|
-
|
|
35
|
-
keys = {}
|
|
36
|
-
used = set()
|
|
37
|
-
for base, entries in sorted(groups.items()):
|
|
38
|
-
entries = sorted(set(entries))
|
|
39
|
-
has_collision = len(entries) > 1
|
|
40
|
-
for norm in entries:
|
|
41
|
-
if not has_collision:
|
|
42
|
-
base_key = slug_basename(norm)
|
|
43
|
-
elif base == "index" == norm:
|
|
44
|
-
base_key = "index"
|
|
45
|
-
else:
|
|
46
|
-
base_key = norm.replace("/", "__")
|
|
47
|
-
|
|
48
|
-
candidate = base_key
|
|
49
|
-
suffix = 2
|
|
50
|
-
while candidate in used:
|
|
51
|
-
candidate = f"{base_key}-{suffix}"
|
|
52
|
-
suffix += 1
|
|
53
|
-
|
|
54
|
-
used.add(candidate)
|
|
55
|
-
keys[norm] = candidate
|
|
56
|
-
|
|
57
|
-
return keys
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def slug_page_key(slug, config):
|
|
61
|
-
"""Return the stable output key for a slug."""
|
|
62
|
-
norm = normalize_slug(slug)
|
|
63
|
-
return config.get("_slug_page_keys", {}).get(norm, slug_basename(norm))
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def slug_output_name(slug, config=None) -> str:
|
|
67
|
-
"""Return output HTML filename for a slug."""
|
|
68
|
-
if config is None:
|
|
69
|
-
return f"{slug}.html"
|
|
70
|
-
key = slug_page_key(slug, config)
|
|
71
|
-
return "index.html" if key == "index" else f"{key}.html"
|
|
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
|