Rewrite or strip broken [[wikilinks]] across the wiki in place. For each Markdown page under ``wiki`` (excluding ``reports/`` and ``sources/`` and excluded files), runs :func:`strip_ghost_wikilinks` against the set of valid targets currently on disk. Targets that match fuzzily (case
(
wiki: Path,
*,
restrict_to: list[Path] | None = None,
)
| 185 | |
| 186 | |
| 187 | def fix_broken_links( |
| 188 | wiki: Path, |
| 189 | *, |
| 190 | restrict_to: list[Path] | None = None, |
| 191 | ) -> tuple[int, int]: |
| 192 | """Rewrite or strip broken [[wikilinks]] across the wiki in place. |
| 193 | |
| 194 | For each Markdown page under ``wiki`` (excluding ``reports/`` and |
| 195 | ``sources/`` and excluded files), runs :func:`strip_ghost_wikilinks` |
| 196 | against the set of valid targets currently on disk. Targets that match |
| 197 | fuzzily (case, ``_`` vs ``-``, NFKC) are rewritten to canonical form; |
| 198 | targets that have no match are demoted to plain text. |
| 199 | |
| 200 | Args: |
| 201 | wiki: Path to the wiki root directory. |
| 202 | restrict_to: When provided, only rewrite these files (must live |
| 203 | under ``wiki``). Paths outside the wiki and non-existent |
| 204 | paths are silently skipped. An empty list is a no-op — the |
| 205 | valid-target whitelist is still computed from the entire |
| 206 | wiki, so links like ``[[concepts/sibling]]`` resolve |
| 207 | correctly even when ``sibling.md`` is not in the scope. |
| 208 | Used by ``openkb remove`` (issue #58 / Bug 2) to clean only |
| 209 | the pages it actually touched instead of sweeping the |
| 210 | whole wiki and stripping pre-existing dangling links the |
| 211 | user may want to keep. |
| 212 | |
| 213 | Returns: |
| 214 | Tuple of ``(files_changed, ghosts_stripped)``. |
| 215 | """ |
| 216 | pages = _all_wiki_pages(wiki) |
| 217 | # The same fuzzy normalization _all_wiki_pages stores both the full |
| 218 | # relative path (e.g. ``concepts/attention``) and the bare stem |
| 219 | # (``attention``). Use the full-path keys so that links like |
| 220 | # ``[[concepts/foo]]`` resolve against ``concepts/`` files only. |
| 221 | known_targets: set[str] = {key for key in pages if "/" in key or key == "index"} |
| 222 | # Build the normalized index once and reuse across every file — |
| 223 | # otherwise strip_ghost_wikilinks would rebuild it per file (O(F·M)). |
| 224 | norm_index = build_norm_index(known_targets) |
| 225 | |
| 226 | if restrict_to is None: |
| 227 | candidates: list[Path] = [ |
| 228 | md |
| 229 | for md in wiki.rglob("*.md") |
| 230 | if md.name not in _EXCLUDED_FILES |
| 231 | and md.relative_to(wiki).parts[:1] not in (("reports",), ("sources",)) |
| 232 | ] |
| 233 | else: |
| 234 | wiki_resolved = wiki.resolve() |
| 235 | candidates = [] |
| 236 | for raw in restrict_to: |
| 237 | if not raw.is_file(): |
| 238 | continue |
| 239 | try: |
| 240 | raw.resolve().relative_to(wiki_resolved) |
| 241 | except ValueError: |
| 242 | continue # outside wiki — skip silently |
| 243 | candidates.append(raw) |
| 244 |