mkdocs-nav-numbering-plugin 0.1.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

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.
@@ -1,4 +1,5 @@
1
1
  import re
2
+ import unicodedata
2
3
  from typing import Dict, List
3
4
 
4
5
  from mkdocs.config import config_options
@@ -6,6 +7,39 @@ from mkdocs.plugins import BasePlugin
6
7
  from mkdocs.structure.nav import Navigation, Section, Page, Link
7
8
 
8
9
 
10
+ def slugify(text: str) -> str:
11
+ """
12
+ Convert text to a URL-friendly slug, matching MkDocs' default toc behavior.
13
+
14
+ - Converts to lowercase
15
+ - Normalizes unicode characters
16
+ - Replaces spaces and underscores with hyphens
17
+ - Removes non-alphanumeric characters (except hyphens)
18
+ - Collapses multiple hyphens into one
19
+ - Strips leading/trailing hyphens
20
+ """
21
+ # Normalize unicode characters (e.g., accented chars -> base form)
22
+ text = unicodedata.normalize("NFKD", text)
23
+ text = text.encode("ascii", "ignore").decode("ascii")
24
+
25
+ # Convert to lowercase
26
+ text = text.lower()
27
+
28
+ # Replace spaces and underscores with hyphens
29
+ text = re.sub(r"[\s_]+", "-", text)
30
+
31
+ # Remove non-alphanumeric characters except hyphens
32
+ text = re.sub(r"[^\w-]", "", text)
33
+
34
+ # Collapse multiple hyphens into one
35
+ text = re.sub(r"-+", "-", text)
36
+
37
+ # Strip leading/trailing hyphens
38
+ text = text.strip("-")
39
+
40
+ return text
41
+
42
+
9
43
  class NavNumberingPlugin(BasePlugin):
10
44
  """
11
45
  MkDocs plugin to add hierarchical numbering to navigation items and page headings.
@@ -19,6 +53,7 @@ class NavNumberingPlugin(BasePlugin):
19
53
  number_h1: Add number to the first h1 (page title) (default: true)
20
54
  separator: Separator between number parts (default: ".")
21
55
  exclude: List of page paths to exclude from numbering (default: [])
56
+ preserve_anchor_ids: Preserve original anchor IDs without number prefixes (default: false)
22
57
  """
23
58
 
24
59
  config_scheme = (
@@ -30,6 +65,7 @@ class NavNumberingPlugin(BasePlugin):
30
65
  ("number_h1", config_options.Type(bool, default=True)),
31
66
  ("separator", config_options.Type(str, default=".")),
32
67
  ("exclude", config_options.Type(list, default=[])),
68
+ ("preserve_anchor_ids", config_options.Type(bool, default=False)),
33
69
  )
34
70
 
35
71
  def __init__(self):
@@ -97,6 +133,9 @@ class NavNumberingPlugin(BasePlugin):
97
133
  Prepend the nav number to headings inside each page.
98
134
  - The first h1 (page title) gets the base_number (e.g., 3.1.1)
99
135
  - Subsequent h2, h3, etc. get sub-numbers (e.g., 3.1.1.1, 3.1.1.2)
136
+
137
+ When preserve_anchor_ids is enabled, explicit {#slug} attributes are added
138
+ to headings to preserve the original anchor IDs without number prefixes.
100
139
  """
101
140
  if not self.config["enabled"] or not self.config["number_headings"]:
102
141
  return markdown
@@ -109,6 +148,7 @@ class NavNumberingPlugin(BasePlugin):
109
148
  heading_depth = self.config["heading_depth"]
110
149
  number_h1 = self.config["number_h1"]
111
150
  separator = self.config["separator"]
151
+ preserve_anchor_ids = self.config["preserve_anchor_ids"]
112
152
  base_depth = len(base_number.split(separator))
113
153
 
114
154
  # Track whether we've seen the first h1 (page title)
@@ -118,11 +158,42 @@ class NavNumberingPlugin(BasePlugin):
118
158
  # h2 -> counter[0], h3 -> counter[1], etc.
119
159
  counters: List[int] = []
120
160
 
161
+ # Track used slugs for duplicate detection (per page)
162
+ used_slugs: Dict[str, int] = {}
163
+
164
+ def get_unique_slug(title: str) -> str:
165
+ """Generate a unique slug, adding suffix for duplicates."""
166
+ base_slug = slugify(title)
167
+ if not base_slug:
168
+ base_slug = "heading"
169
+
170
+ if base_slug not in used_slugs:
171
+ used_slugs[base_slug] = 0
172
+ return base_slug
173
+ else:
174
+ used_slugs[base_slug] += 1
175
+ return f"{base_slug}-{used_slugs[base_slug]}"
176
+
177
+ def extract_existing_id(title: str) -> tuple:
178
+ """
179
+ Extract existing {#custom-id} from title if present.
180
+ Returns (clean_title, existing_id) or (title, None).
181
+ """
182
+ match = re.search(r'\s*\{#([^}]+)\}\s*$', title)
183
+ if match:
184
+ existing_id = match.group(1)
185
+ clean_title = title[:match.start()].strip()
186
+ return clean_title, existing_id
187
+ return title, None
188
+
121
189
  def repl(match: re.Match) -> str:
122
190
  hashes = match.group("hashes")
123
- title = match.group("title").strip()
191
+ raw_title = match.group("title").strip()
124
192
  level = len(hashes) # "#"=1, "##"=2, etc.
125
193
 
194
+ # Check for existing {#custom-id} attribute
195
+ title, existing_id = extract_existing_id(raw_title)
196
+
126
197
  # Avoid double-numbering if already numbered
127
198
  if re.match(r"^\d+(\.\d+)*\s+", title):
128
199
  return match.group(0)
@@ -132,12 +203,22 @@ class NavNumberingPlugin(BasePlugin):
132
203
  if not first_h1_seen[0]:
133
204
  first_h1_seen[0] = True
134
205
  if number_h1:
206
+ if preserve_anchor_ids and not existing_id:
207
+ slug = get_unique_slug(title)
208
+ return f"{hashes} {base_number} {title} {{#{slug}}}"
209
+ elif existing_id:
210
+ return f"{hashes} {base_number} {title} {{#{existing_id}}}"
135
211
  return f"{hashes} {base_number} {title}"
136
212
  return match.group(0)
137
213
  else:
138
214
  # Additional h1s in the same page - handle gracefully
139
215
  counters.clear()
140
216
  if number_h1:
217
+ if preserve_anchor_ids and not existing_id:
218
+ slug = get_unique_slug(title)
219
+ return f"{hashes} {base_number} {title} {{#{slug}}}"
220
+ elif existing_id:
221
+ return f"{hashes} {base_number} {title} {{#{existing_id}}}"
141
222
  return f"{hashes} {base_number} {title}"
142
223
  return match.group(0)
143
224
 
@@ -162,6 +243,11 @@ class NavNumberingPlugin(BasePlugin):
162
243
  rel = separator.join(str(c) for c in counters[:adjusted_level])
163
244
  full_number = f"{base_number}{separator}{rel}"
164
245
 
246
+ if preserve_anchor_ids and not existing_id:
247
+ slug = get_unique_slug(title)
248
+ return f"{hashes} {full_number} {title} {{#{slug}}}"
249
+ elif existing_id:
250
+ return f"{hashes} {full_number} {title} {{#{existing_id}}}"
165
251
  return f"{hashes} {full_number} {title}"
166
252
 
167
253
  heading_pattern = re.compile(r"^(?P<hashes>#{1,6})\s+(?P<title>.+)$", re.MULTILINE)
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-nav-numbering-plugin
3
- Version: 0.1.0
3
+ Version: 1.1.0
4
4
  Summary: MkDocs plugin to add hierarchical numbering to nav and page headings
5
- Author: ZMT Zurich MedTech AG
5
+ Author: Matus Drobuliak
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/your-org/mkdocs-nav-numbering-plugin
8
- Project-URL: Repository, https://github.com/your-org/mkdocs-nav-numbering-plugin
9
- Project-URL: Issues, https://github.com/your-org/mkdocs-nav-numbering-plugin/issues
7
+ Project-URL: Homepage, https://github.com/matusdrobuliak66/mkdocs-nav-numbering-plugin
8
+ Project-URL: Repository, https://github.com/matusdrobuliak66/mkdocs-nav-numbering-plugin
9
+ Project-URL: Issues, https://github.com/matusdrobuliak66/mkdocs-nav-numbering-plugin/issues
10
10
  Keywords: mkdocs,plugin,navigation,numbering,documentation
11
- Classifier: Development Status :: 4 - Beta
11
+ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Environment :: Plugins
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
@@ -51,9 +51,13 @@ plugins:
51
51
  number_h1: true
52
52
  number_nav: true
53
53
  number_headings: true
54
+ preserve_anchor_ids: false
54
55
  separator: "."
55
56
  exclude:
56
57
  - index.md
58
+
59
+ markdown_extensions:
60
+ - attr_list # Required when preserve_anchor_ids is true
57
61
  ```
58
62
 
59
63
  ## Options
@@ -64,5 +68,21 @@ plugins:
64
68
  - `number_nav` (bool, default: true)
65
69
  - `number_headings` (bool, default: true)
66
70
  - `number_h1` (bool, default: true)
71
+ - `preserve_anchor_ids` (bool, default: false) — Preserve original heading anchor IDs without number prefixes (requires `attr_list`)
67
72
  - `separator` (str, default: ".")
68
73
  - `exclude` (list, default: [])
74
+
75
+ ### `preserve_anchor_ids`
76
+
77
+ By default, MkDocs generates heading IDs from the final heading text. Since this plugin prepends numbering, your anchors can end up including the numbers.
78
+
79
+ Enable `preserve_anchor_ids: true` to add explicit `{#...}` IDs based on the original (un-numbered) heading text. Duplicate headings are automatically suffixed with `-1`, `-2`, etc.
80
+
81
+ ```yaml
82
+ plugins:
83
+ - nav-numbering:
84
+ preserve_anchor_ids: true
85
+
86
+ markdown_extensions:
87
+ - attr_list
88
+ ```
@@ -0,0 +1,8 @@
1
+ mkdocs_nav_numbering_plugin/__init__.py,sha256=McZEZwPtatKF0PvPVKMD7_nBeaOjdHNME6tQNwNIt8M,80
2
+ mkdocs_nav_numbering_plugin/nav_numbering.py,sha256=AC6QEk7muwKj-BwklfAZJXGsmfKIqb4xLZodpJmPW6M,10779
3
+ mkdocs_nav_numbering_plugin-1.1.0.dist-info/licenses/LICENSE,sha256=3xENzADezoOh8w__sH4AAF-e-757l_y4sooIAwjqpL0,1072
4
+ mkdocs_nav_numbering_plugin-1.1.0.dist-info/METADATA,sha256=pEGEBi9VxtxinF__oATLP2c2mQ5WjJWhAt4FZIZGnuI,2760
5
+ mkdocs_nav_numbering_plugin-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ mkdocs_nav_numbering_plugin-1.1.0.dist-info/entry_points.txt,sha256=NnO7O_09mMyAHFIspg2zmsfri_m7nt8Km8xZK0tgvtQ,80
7
+ mkdocs_nav_numbering_plugin-1.1.0.dist-info/top_level.txt,sha256=UcRwMarClerK-Iuj0RooOAq7IVhn_oRmQRiCZgSCfN4,28
8
+ mkdocs_nav_numbering_plugin-1.1.0.dist-info/RECORD,,
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 ZMT Zurich MedTech AG
3
+ Copyright (c) 2026 Matus Drobuliak
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,8 +0,0 @@
1
- mkdocs_nav_numbering_plugin/__init__.py,sha256=McZEZwPtatKF0PvPVKMD7_nBeaOjdHNME6tQNwNIt8M,80
2
- mkdocs_nav_numbering_plugin/nav_numbering.py,sha256=zRiG9_AZdLKe21pnb_b_vQSw8LkWpL8HK6UdY14WoPc,7263
3
- mkdocs_nav_numbering_plugin-0.1.0.dist-info/licenses/LICENSE,sha256=o8hK_UrJ1kd51VuQ5ZP4d8gwK5WPlt6Zg5vO-T42eoo,1078
4
- mkdocs_nav_numbering_plugin-0.1.0.dist-info/METADATA,sha256=Erwhh43scMWp4QOY-uGORxkA-5TPRs88iZfNoqqoDew,2005
5
- mkdocs_nav_numbering_plugin-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
- mkdocs_nav_numbering_plugin-0.1.0.dist-info/entry_points.txt,sha256=NnO7O_09mMyAHFIspg2zmsfri_m7nt8Km8xZK0tgvtQ,80
7
- mkdocs_nav_numbering_plugin-0.1.0.dist-info/top_level.txt,sha256=UcRwMarClerK-Iuj0RooOAq7IVhn_oRmQRiCZgSCfN4,28
8
- mkdocs_nav_numbering_plugin-0.1.0.dist-info/RECORD,,