mkdocs2confluence 0.9.3__tar.gz → 0.9.5__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 (87) hide show
  1. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/PKG-INFO +6 -1
  2. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/README.md +5 -0
  3. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/pyproject.toml +1 -1
  4. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs2confluence.egg-info/PKG-INFO +6 -1
  5. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/transforms/mermaid.py +57 -3
  6. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_mermaid.py +59 -0
  7. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/LICENSE +0 -0
  8. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/setup.cfg +0 -0
  9. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
  10. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  11. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  12. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  13. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  14. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/__init__.py +0 -0
  15. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/cli.py +0 -0
  16. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  17. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  18. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  19. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/ir/document.py +0 -0
  20. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  21. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  22. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  23. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/loader/config.py +0 -0
  24. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  25. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  26. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/loader/page.py +0 -0
  27. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  28. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  29. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  30. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  31. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  32. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  33. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  34. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  35. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  36. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  37. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  38. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  39. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  40. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preview/render.py +0 -0
  41. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/preview/server.py +0 -0
  42. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  43. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  44. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/publisher/pipeline.py +2 -2
  45. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
  46. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
  47. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/sync/command.py +0 -0
  48. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/sync/comments.py +0 -0
  49. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/sync/github.py +0 -0
  50. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/sync/platform.py +0 -0
  51. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/sync/state.py +0 -0
  52. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  53. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  54. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  55. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  56. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
  57. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  58. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  59. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_abbrevs.py +0 -0
  60. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_children_macro.py +0 -0
  61. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_cli.py +0 -0
  62. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_editlink.py +0 -0
  63. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_emitter.py +0 -0
  64. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_extra_css.py +0 -0
  65. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_footer.py +0 -0
  66. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_frontmatter.py +0 -0
  67. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_icons.py +0 -0
  68. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_images.py +0 -0
  69. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_internallinks.py +0 -0
  70. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_ir.py +0 -0
  71. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_linkdefs.py +0 -0
  72. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_loader.py +0 -0
  73. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_page_loader.py +0 -0
  74. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_parser.py +0 -0
  75. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_pdf.py +0 -0
  76. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_preprocess.py +0 -0
  77. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_preview.py +0 -0
  78. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_publish_client.py +0 -0
  79. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_publish_config.py +0 -0
  80. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_publish_pipeline.py +0 -0
  81. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_server.py +0 -0
  82. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_sync_anchoring.py +0 -0
  83. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_sync_command.py +0 -0
  84. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_sync_comments.py +0 -0
  85. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_sync_github.py +0 -0
  86. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_sync_state.py +0 -0
  87. {mkdocs2confluence-0.9.3 → mkdocs2confluence-0.9.5}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.9.3
3
+ Version: 0.9.5
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -245,6 +245,8 @@ mk2conf publish [--config PATH] [--page PATH] [--section SECTION] [--dry-run] [-
245
245
 
246
246
  If Kroki is unreachable the run continues, falling back to the `code` macro for affected diagrams.
247
247
 
248
+ **Automatic mermaid.ink fallback:** when using the public `kroki.io` service and a Mermaid diagram receives a 504 (gateway timeout), mk2conf automatically retries that diagram via [mermaid.ink](https://mermaid.ink) before giving up. No configuration needed — the fallback is transparent and only applies to Mermaid diagrams on the public service. Self-hosted Kroki instances never contact mermaid.ink, preserving network isolation.
249
+
248
250
  #### Styling from extra.css
249
251
 
250
252
  If `mkdocs.yml` lists `extra_css:` files, mk2conf reads them and applies a whitelisted set of CSS properties as inline `style="..."` attributes on Confluence output.
@@ -368,6 +370,7 @@ confluence:
368
370
  | `^^inserted^^` | `<u>` (pymdownx.caret insert) |
369
371
  | `` `inline code` `` | `<code>` |
370
372
  | `[text](url)` | `<a href="...">` |
373
+ | `[text][label]` / `[text][]` with `[label]: url` | Resolved to inline link before parsing |
371
374
  | `https://bare-url` | `<a href="...">` (autolink) |
372
375
  | `[text](file.pdf)` | `<ac:link><ri:attachment .../>` (uploaded as attachment) |
373
376
  | `![alt](src)` | `<ac:image>` with `<ri:attachment>` (local) or `<ri:url>` (remote) |
@@ -433,6 +436,8 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
433
436
  - **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
434
437
  - **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
435
438
 
439
+ The commit SHA and message are also written to the **Confluence version history** on every publish (`sha: message`), so the page history in Confluence stays in sync with your git log.
440
+
436
441
  ### Section index child pages
437
442
 
438
443
  When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
@@ -205,6 +205,8 @@ mk2conf publish [--config PATH] [--page PATH] [--section SECTION] [--dry-run] [-
205
205
 
206
206
  If Kroki is unreachable the run continues, falling back to the `code` macro for affected diagrams.
207
207
 
208
+ **Automatic mermaid.ink fallback:** when using the public `kroki.io` service and a Mermaid diagram receives a 504 (gateway timeout), mk2conf automatically retries that diagram via [mermaid.ink](https://mermaid.ink) before giving up. No configuration needed — the fallback is transparent and only applies to Mermaid diagrams on the public service. Self-hosted Kroki instances never contact mermaid.ink, preserving network isolation.
209
+
208
210
  #### Styling from extra.css
209
211
 
210
212
  If `mkdocs.yml` lists `extra_css:` files, mk2conf reads them and applies a whitelisted set of CSS properties as inline `style="..."` attributes on Confluence output.
@@ -328,6 +330,7 @@ confluence:
328
330
  | `^^inserted^^` | `<u>` (pymdownx.caret insert) |
329
331
  | `` `inline code` `` | `<code>` |
330
332
  | `[text](url)` | `<a href="...">` |
333
+ | `[text][label]` / `[text][]` with `[label]: url` | Resolved to inline link before parsing |
331
334
  | `https://bare-url` | `<a href="...">` (autolink) |
332
335
  | `[text](file.pdf)` | `<ac:link><ri:attachment .../>` (uploaded as attachment) |
333
336
  | `![alt](src)` | `<ac:image>` with `<ri:attachment>` (local) or `<ri:url>` (remote) |
@@ -393,6 +396,8 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
393
396
  - **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
394
397
  - **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
395
398
 
399
+ The commit SHA and message are also written to the **Confluence version history** on every publish (`sha: message`), so the page history in Confluence stays in sync with your git log.
400
+
396
401
  ### Section index child pages
397
402
 
398
403
  When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.9.3"
3
+ version = "0.9.5"
4
4
  description = "Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more"
5
5
  readme = "README.md"
6
6
  license = { text = "GPL-3.0-or-later" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.9.3
3
+ Version: 0.9.5
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -245,6 +245,8 @@ mk2conf publish [--config PATH] [--page PATH] [--section SECTION] [--dry-run] [-
245
245
 
246
246
  If Kroki is unreachable the run continues, falling back to the `code` macro for affected diagrams.
247
247
 
248
+ **Automatic mermaid.ink fallback:** when using the public `kroki.io` service and a Mermaid diagram receives a 504 (gateway timeout), mk2conf automatically retries that diagram via [mermaid.ink](https://mermaid.ink) before giving up. No configuration needed — the fallback is transparent and only applies to Mermaid diagrams on the public service. Self-hosted Kroki instances never contact mermaid.ink, preserving network isolation.
249
+
248
250
  #### Styling from extra.css
249
251
 
250
252
  If `mkdocs.yml` lists `extra_css:` files, mk2conf reads them and applies a whitelisted set of CSS properties as inline `style="..."` attributes on Confluence output.
@@ -368,6 +370,7 @@ confluence:
368
370
  | `^^inserted^^` | `<u>` (pymdownx.caret insert) |
369
371
  | `` `inline code` `` | `<code>` |
370
372
  | `[text](url)` | `<a href="...">` |
373
+ | `[text][label]` / `[text][]` with `[label]: url` | Resolved to inline link before parsing |
371
374
  | `https://bare-url` | `<a href="...">` (autolink) |
372
375
  | `[text](file.pdf)` | `<ac:link><ri:attachment .../>` (uploaded as attachment) |
373
376
  | `![alt](src)` | `<ac:image>` with `<ri:attachment>` (local) or `<ri:url>` (remote) |
@@ -433,6 +436,8 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
433
436
  - **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
434
437
  - **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
435
438
 
439
+ The commit SHA and message are also written to the **Confluence version history** on every publish (`sha: message`), so the page history in Confluence stays in sync with your git log.
440
+
436
441
  ### Section index child pages
437
442
 
438
443
  When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
@@ -10,17 +10,28 @@ of the Mermaid source so unchanged diagrams are never re-fetched.
10
10
  When Kroki is unavailable (network error, HTTP error, timeout, or bad response)
11
11
  each affected diagram is left unchanged so the emitter falls back to a fenced
12
12
  code block. The rest of the pipeline continues unaffected.
13
+
14
+ Fallback behaviour
15
+ ------------------
16
+ When the public ``https://kroki.io`` service returns a 504 (gateway timeout)
17
+ for a Mermaid diagram, mk2conf automatically retries via ``mermaid.ink`` before
18
+ giving up. This fallback does **not** apply to self-hosted Kroki instances
19
+ (configured as ``kroki:<url>``) — those run in isolation and should not reach
20
+ out to external services.
13
21
  """
14
22
 
15
23
  from __future__ import annotations
16
24
 
25
+ import base64
17
26
  import dataclasses
18
27
  import hashlib
28
+ import json
19
29
  import sys
20
30
  import threading
21
31
  import time
22
32
  import urllib.error
23
33
  import urllib.request
34
+ import zlib
24
35
  from concurrent.futures import ThreadPoolExecutor, as_completed
25
36
  from pathlib import Path
26
37
  from typing import cast
@@ -30,7 +41,8 @@ from mkdocs_to_confluence.ir.treeutil import replace_nodes
30
41
 
31
42
  _CACHE_DIR = Path.home() / ".cache" / "mk2conf" / "mermaid"
32
43
  DEFAULT_KROKI_URL = "https://kroki.io"
33
- _TIMEOUT = 30 # seconds — fail fast when Kroki is down
44
+ _MERMAID_INK_URL = "https://mermaid.ink"
45
+ _TIMEOUT = 30 # seconds
34
46
  _MIN_PNG_BYTES = 67 # smallest valid PNG (1×1 px) is 67 bytes
35
47
  _CACHE_LOCK = threading.Lock()
36
48
  _MAX_WORKERS = 8
@@ -57,6 +69,21 @@ def _kroki_png(source: str, kroki_url: str) -> bytes:
57
69
  return cast(bytes, resp.read())
58
70
 
59
71
 
72
+ def _mermaid_ink_png(source: str) -> bytes:
73
+ """Fetch a PNG rendering of *source* from mermaid.ink (GET, pako-encoded).
74
+
75
+ Encodes the diagram as ``{"code": source}`` compressed with zlib and
76
+ base64url-encoded — the pako format that mermaid.ink expects.
77
+ """
78
+ payload = json.dumps({"code": source, "mermaid": {"theme": "default"}}, separators=(",", ":"))
79
+ compressed = zlib.compress(payload.encode("utf-8"))
80
+ encoded = base64.urlsafe_b64encode(compressed).decode().rstrip("=")
81
+ url = f"{_MERMAID_INK_URL}/img/pako:{encoded}?type=png"
82
+ req = urllib.request.Request(url, headers={"User-Agent": "mk2conf/1.0"})
83
+ with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 # nosec B310
84
+ return cast(bytes, resp.read())
85
+
86
+
60
87
  def _cache_path(source: str) -> Path:
61
88
  digest = hashlib.sha256(source.encode()).hexdigest()
62
89
  return _CACHE_DIR / f"mermaid_{digest}.png"
@@ -71,6 +98,10 @@ def _render_one(source: str, kroki_url: str, *, quiet: bool = False) -> Path | N
71
98
 
72
99
  Transient HTTP errors (429, 5xx) and network blips are retried up to
73
100
  ``_RETRY_ATTEMPTS`` times with exponential backoff.
101
+
102
+ When using the public kroki.io and all retries are exhausted due to 504s,
103
+ one final attempt is made via mermaid.ink before giving up. Self-hosted
104
+ Kroki instances never fall back to mermaid.ink.
74
105
  """
75
106
  path = _cache_path(source)
76
107
  if path.exists():
@@ -78,7 +109,10 @@ def _render_one(source: str, kroki_url: str, *, quiet: bool = False) -> Path | N
78
109
  print(" rendering mermaid diagram (cached)")
79
110
  return path
80
111
 
112
+ use_public_kroki = kroki_url.rstrip("/") == DEFAULT_KROKI_URL.rstrip("/")
81
113
  last_exc: Exception | None = None
114
+ last_was_504 = False
115
+
82
116
  for attempt in range(_RETRY_ATTEMPTS):
83
117
  if attempt > 0:
84
118
  delay = _RETRY_BACKOFF * (2 ** (attempt - 1))
@@ -96,17 +130,34 @@ def _render_one(source: str, kroki_url: str, *, quiet: bool = False) -> Path | N
96
130
  except urllib.error.HTTPError as exc:
97
131
  if exc.code in _RETRYABLE_HTTP:
98
132
  last_exc = exc
133
+ last_was_504 = exc.code == 504
99
134
  continue # retry
100
135
  _warn(f"mermaid diagram: Kroki returned HTTP {exc.code} {exc.reason} — falling back to code block")
101
136
  return None
102
137
  except urllib.error.URLError as exc:
103
138
  last_exc = exc
139
+ last_was_504 = False
104
140
  continue # retry — network blip
105
141
  except (OSError, ValueError) as exc:
106
142
  _warn(f"mermaid diagram: {exc} — falling back to code block")
107
143
  return None
108
144
 
109
- # All retries exhausted
145
+ # All Kroki retries exhausted — try mermaid.ink if on public kroki.io and last error was 504.
146
+ if use_public_kroki and last_was_504:
147
+ try:
148
+ _warn("mermaid diagram: kroki.io unavailable (504), trying mermaid.ink as fallback")
149
+ png = _mermaid_ink_png(source)
150
+ if len(png) < _MIN_PNG_BYTES:
151
+ raise ValueError(f"mermaid.ink returned {len(png)} bytes (expected a valid PNG)")
152
+ with _CACHE_LOCK:
153
+ path.write_bytes(png)
154
+ if not quiet:
155
+ print(" rendering mermaid diagram (via mermaid.ink fallback)")
156
+ return path
157
+ except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError) as exc:
158
+ _warn(f"mermaid diagram: mermaid.ink fallback also failed ({exc}) — falling back to code block")
159
+ return None
160
+
110
161
  _warn(f"mermaid diagram: failed after {_RETRY_ATTEMPTS} attempts ({last_exc}) — falling back to code block")
111
162
  return None
112
163
 
@@ -125,7 +176,10 @@ def render_mermaid_diagrams(
125
176
  upload as page attachments.
126
177
 
127
178
  Diagrams that fail to render are left unchanged (code-block fallback).
128
- The pipeline always produces valid output regardless of Kroki availability.
179
+ When using public kroki.io and a diagram fails with a 504, one automatic
180
+ retry via mermaid.ink is attempted before falling back to a code block.
181
+ Self-hosted Kroki instances never contact mermaid.ink.
182
+ The pipeline always produces valid output regardless of service availability.
129
183
  """
130
184
  try:
131
185
  _CACHE_DIR.mkdir(parents=True, exist_ok=True)
@@ -365,3 +365,62 @@ def test_render_mermaid_cached_quiet_suppresses_stdout(tmp_path, capsys):
365
365
  assert result is not None
366
366
  out, _ = capsys.readouterr()
367
367
  assert out == ""
368
+
369
+
370
+ # ── mermaid.ink fallback ──────────────────────────────────────────────────────
371
+
372
+
373
+ def test_render_one_504_on_public_kroki_falls_back_to_mermaid_ink(tmp_path, capsys):
374
+ """Public kroki.io 504 → automatic fallback to mermaid.ink for Mermaid."""
375
+ import urllib.error
376
+
377
+ from mkdocs_to_confluence.transforms.mermaid import _render_one
378
+
379
+ with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
380
+ patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
381
+ patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
382
+ side_effect=urllib.error.HTTPError("url", 504, "Gateway Timeout", {}, None)), \
383
+ patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
384
+ return_value=_FAKE_PNG) as mock_ink:
385
+ result = _render_one(_SAMPLE_SOURCE, "https://kroki.io")
386
+
387
+ assert result is not None
388
+ assert result.exists()
389
+ mock_ink.assert_called_once_with(_SAMPLE_SOURCE)
390
+
391
+
392
+ def test_render_one_504_on_self_hosted_kroki_does_not_fall_back(tmp_path):
393
+ """Self-hosted Kroki 504 → no mermaid.ink fallback (privacy isolation)."""
394
+ import urllib.error
395
+
396
+ from mkdocs_to_confluence.transforms.mermaid import _render_one
397
+
398
+ with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
399
+ patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
400
+ patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
401
+ side_effect=urllib.error.HTTPError("url", 504, "Gateway Timeout", {}, None)), \
402
+ patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
403
+ return_value=_FAKE_PNG) as mock_ink:
404
+ result = _render_one(_SAMPLE_SOURCE, "https://my-internal-kroki.corp")
405
+
406
+ assert result is None
407
+ mock_ink.assert_not_called()
408
+
409
+
410
+ def test_render_one_mermaid_ink_fallback_also_fails_returns_none(tmp_path, capsys):
411
+ """When mermaid.ink fallback also fails, returns None (code-block fallback)."""
412
+ import urllib.error
413
+
414
+ from mkdocs_to_confluence.transforms.mermaid import _render_one
415
+
416
+ with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
417
+ patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
418
+ patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
419
+ side_effect=urllib.error.HTTPError("url", 504, "Gateway Timeout", {}, None)), \
420
+ patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
421
+ side_effect=urllib.error.URLError("connection refused")):
422
+ result = _render_one(_SAMPLE_SOURCE, "https://kroki.io")
423
+
424
+ assert result is None
425
+ _, err = capsys.readouterr()
426
+ assert "mermaid.ink" in err
@@ -169,6 +169,8 @@ def compile_page(
169
169
  preprocessed = expand_link_refs(preprocessed, link_defs)
170
170
  preprocessed = strip_link_defs(preprocessed)
171
171
  ir_nodes = parse(preprocessed)
172
+ if is_section_index:
173
+ ir_nodes = ir_nodes + (ChildrenMacro(),)
172
174
  ir_nodes = apply_abbreviations(ir_nodes, abbrevs, page_text=preprocessed)
173
175
  ir_nodes, attachments = resolve_local_assets(
174
176
  ir_nodes,
@@ -192,8 +194,6 @@ def compile_page(
192
194
  site_url = config.page_site_url(node.docs_path or "")
193
195
  if site_url:
194
196
  ir_nodes = attach_source_url(ir_nodes, "", site_url)
195
- if is_section_index:
196
- ir_nodes = ir_nodes + (ChildrenMacro(),)
197
197
  if edit_url:
198
198
  abs_path = str(config.docs_dir / (node.docs_path or ""))
199
199
  footer = build_source_footer(edit_url, abs_path)