mkdocs2confluence 0.16.0__tar.gz → 0.17.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 (113) hide show
  1. {mkdocs2confluence-0.16.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.17.0}/PKG-INFO +2 -2
  2. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/README.md +1 -1
  3. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/pyproject.toml +1 -1
  4. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0/src/mkdocs2confluence.egg-info}/PKG-INFO +2 -2
  5. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs2confluence.egg-info/SOURCES.txt +3 -0
  6. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/compiler/page.py +9 -3
  7. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/emitter/xhtml.py +25 -3
  8. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/ir/nodes.py +25 -6
  9. mkdocs2confluence-0.17.0/src/mkdocs_to_confluence/parser/__init__.py +5 -0
  10. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/parser/markdown.py +13 -0
  11. mkdocs2confluence-0.17.0/src/mkdocs_to_confluence/transforms/_kroki.py +132 -0
  12. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/abbrevs.py +10 -6
  13. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/mermaid.py +15 -59
  14. mkdocs2confluence-0.17.0/src/mkdocs_to_confluence/transforms/plantuml.py +137 -0
  15. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_abbrevs.py +48 -4
  16. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_emitter.py +18 -2
  17. mkdocs2confluence-0.17.0/tests/test_plantuml.py +270 -0
  18. mkdocs2confluence-0.16.0/src/mkdocs_to_confluence/parser/__init__.py +0 -5
  19. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/LICENSE +0 -0
  20. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/setup.cfg +0 -0
  21. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  22. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  23. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  24. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  25. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/__init__.py +0 -0
  26. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/cli.py +0 -0
  27. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/compiler/__init__.py +0 -0
  28. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/compiler/models.py +0 -0
  29. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  30. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  31. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/ir/document.py +0 -0
  32. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  33. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  34. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/loader/config.py +0 -0
  35. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  36. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  37. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/loader/page.py +0 -0
  38. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  39. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  40. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  41. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  42. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  43. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preprocess/captions.py +0 -0
  44. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  45. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  46. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  47. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  48. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  49. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  50. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preview/render.py +0 -0
  51. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/preview/server.py +0 -0
  52. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  53. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/publisher/changelog.py +0 -0
  54. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  55. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/publisher/executor.py +0 -0
  56. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/publisher/http_retry.py +0 -0
  57. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/publisher/models.py +0 -0
  58. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/publisher/pipeline.py +0 -0
  59. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/publisher/planner.py +0 -0
  60. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/skill_installer.py +0 -0
  61. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/skills/mkdocs-changelog/SKILL.md +0 -0
  62. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/skills/mkdocs-changelog/scripts/changelog_data.py +0 -0
  63. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
  64. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
  65. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/sync/command.py +0 -0
  66. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/sync/comments.py +0 -0
  67. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/sync/github.py +0 -0
  68. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/sync/platform.py +0 -0
  69. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/sync/state.py +0 -0
  70. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  71. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/admonition_titles.py +0 -0
  72. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  73. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/attachment_previews.py +0 -0
  74. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/captions.py +0 -0
  75. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  76. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
  77. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  78. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  79. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_admonition_titles.py +0 -0
  80. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_attachment_previews.py +0 -0
  81. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_captions.py +0 -0
  82. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_changelog_config.py +0 -0
  83. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_changelog_publish.py +0 -0
  84. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_children_macro.py +0 -0
  85. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_cli.py +0 -0
  86. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_editlink.py +0 -0
  87. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_exclude_properties_config.py +0 -0
  88. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_extra_css.py +0 -0
  89. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_footer.py +0 -0
  90. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_frontmatter.py +0 -0
  91. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_icons.py +0 -0
  92. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_images.py +0 -0
  93. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_internallinks.py +0 -0
  94. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_ir.py +0 -0
  95. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_linkdefs.py +0 -0
  96. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_loader.py +0 -0
  97. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_mermaid.py +0 -0
  98. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_page_loader.py +0 -0
  99. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_parser.py +0 -0
  100. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_pdf.py +0 -0
  101. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_preprocess.py +0 -0
  102. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_preview.py +0 -0
  103. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_publish_client.py +0 -0
  104. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_publish_config.py +0 -0
  105. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_publish_pipeline.py +0 -0
  106. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_server.py +0 -0
  107. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_skill_installer.py +0 -0
  108. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_sync_anchoring.py +0 -0
  109. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_sync_command.py +0 -0
  110. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_sync_comments.py +0 -0
  111. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_sync_github.py +0 -0
  112. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_sync_state.py +0 -0
  113. {mkdocs2confluence-0.16.0 → mkdocs2confluence-0.17.0}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.16.0
3
+ Version: 0.17.0
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
@@ -158,7 +158,7 @@ confluence:
158
158
  email: user@example.com
159
159
  token: !ENV CONFLUENCE_API_TOKEN # never hardcode the token
160
160
  parent_page_id: "123456" # optional root page
161
- mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
161
+ mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none" — also controls PlantUML rendering
162
162
  full_width: true # default: true
163
163
  attachment_preview: false # default: false — inline PDF/Office previews
164
164
  changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
@@ -116,7 +116,7 @@ confluence:
116
116
  email: user@example.com
117
117
  token: !ENV CONFLUENCE_API_TOKEN # never hardcode the token
118
118
  parent_page_id: "123456" # optional root page
119
- mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
119
+ mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none" — also controls PlantUML rendering
120
120
  full_width: true # default: true
121
121
  attachment_preview: false # default: false — inline PDF/Office previews
122
122
  changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.16.0"
3
+ version = "0.17.0"
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.16.0
3
+ Version: 0.17.0
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
@@ -158,7 +158,7 @@ confluence:
158
158
  email: user@example.com
159
159
  token: !ENV CONFLUENCE_API_TOKEN # never hardcode the token
160
160
  parent_page_id: "123456" # optional root page
161
- mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
161
+ mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none" — also controls PlantUML rendering
162
162
  full_width: true # default: true
163
163
  attachment_preview: false # default: false — inline PDF/Office previews
164
164
  changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
@@ -58,6 +58,7 @@ src/mkdocs_to_confluence/sync/github.py
58
58
  src/mkdocs_to_confluence/sync/platform.py
59
59
  src/mkdocs_to_confluence/sync/state.py
60
60
  src/mkdocs_to_confluence/transforms/__init__.py
61
+ src/mkdocs_to_confluence/transforms/_kroki.py
61
62
  src/mkdocs_to_confluence/transforms/abbrevs.py
62
63
  src/mkdocs_to_confluence/transforms/admonition_titles.py
63
64
  src/mkdocs_to_confluence/transforms/assets.py
@@ -68,6 +69,7 @@ src/mkdocs_to_confluence/transforms/footer.py
68
69
  src/mkdocs_to_confluence/transforms/images.py
69
70
  src/mkdocs_to_confluence/transforms/internallinks.py
70
71
  src/mkdocs_to_confluence/transforms/mermaid.py
72
+ src/mkdocs_to_confluence/transforms/plantuml.py
71
73
  tests/test_abbrevs.py
72
74
  tests/test_admonition_titles.py
73
75
  tests/test_attachment_previews.py
@@ -92,6 +94,7 @@ tests/test_mermaid.py
92
94
  tests/test_page_loader.py
93
95
  tests/test_parser.py
94
96
  tests/test_pdf.py
97
+ tests/test_plantuml.py
95
98
  tests/test_preprocess.py
96
99
  tests/test_preview.py
97
100
  tests/test_publish_client.py
@@ -8,7 +8,7 @@ from mkdocs_to_confluence.ir.nodes import ChildrenMacro, FrontMatter, SourceFoot
8
8
  from mkdocs_to_confluence.loader.config import MkDocsConfig
9
9
  from mkdocs_to_confluence.loader.nav import NavNode
10
10
  from mkdocs_to_confluence.loader.page import load_page
11
- from mkdocs_to_confluence.parser.markdown import parse
11
+ from mkdocs_to_confluence.parser.markdown import parse, parse_inline
12
12
  from mkdocs_to_confluence.preprocess.abbrevs import (
13
13
  extract_abbreviations,
14
14
  strip_abbreviation_defs,
@@ -26,6 +26,7 @@ from mkdocs_to_confluence.preprocess.linkdefs import (
26
26
  expand_link_refs,
27
27
  strip_link_defs,
28
28
  )
29
+ from mkdocs_to_confluence.transforms._kroki import DEFAULT_KROKI_URL
29
30
  from mkdocs_to_confluence.transforms.abbrevs import apply_abbreviations
30
31
  from mkdocs_to_confluence.transforms.admonition_titles import (
31
32
  strip_links_in_admonition_titles,
@@ -38,7 +39,8 @@ from mkdocs_to_confluence.transforms.captions import resolve_captions
38
39
  from mkdocs_to_confluence.transforms.editlink import attach_source_url
39
40
  from mkdocs_to_confluence.transforms.footer import build_source_footer
40
41
  from mkdocs_to_confluence.transforms.internallinks import resolve_internal_links
41
- from mkdocs_to_confluence.transforms.mermaid import DEFAULT_KROKI_URL, render_mermaid_diagrams
42
+ from mkdocs_to_confluence.transforms.mermaid import render_mermaid_diagrams
43
+ from mkdocs_to_confluence.transforms.plantuml import render_plantuml_diagrams
42
44
 
43
45
 
44
46
  def compile_page(
@@ -79,7 +81,9 @@ def compile_page(
79
81
  ir_nodes = strip_links_in_admonition_titles(ir_nodes, node.docs_path or "")
80
82
  if is_section_index:
81
83
  ir_nodes = ir_nodes + (ChildrenMacro(),)
82
- ir_nodes = apply_abbreviations(ir_nodes, abbrevs, page_text=preprocessed)
84
+ # Parse definitions as inline markdown so links etc. survive into the glossary.
85
+ parsed_abbrevs = {abbr: parse_inline(defn) for abbr, defn in abbrevs.items()}
86
+ ir_nodes = apply_abbreviations(ir_nodes, parsed_abbrevs, page_text=preprocessed)
83
87
  ir_nodes, attachments = resolve_local_assets(
84
88
  ir_nodes,
85
89
  page_path=node.source_path,
@@ -97,6 +101,8 @@ def compile_page(
97
101
  )
98
102
  ir_nodes, mermaid_attachments = render_mermaid_diagrams(ir_nodes, kroki_url, quiet=quiet)
99
103
  attachments = attachments + mermaid_attachments
104
+ ir_nodes, plantuml_attachments = render_plantuml_diagrams(ir_nodes, kroki_url, quiet=quiet)
105
+ attachments = attachments + plantuml_attachments
100
106
  effective_link_map = link_map if link_map is not None else {}
101
107
  if node.docs_path:
102
108
  ir_nodes = resolve_internal_links(ir_nodes, effective_link_map, node.docs_path)
@@ -51,6 +51,7 @@ from mkdocs_to_confluence.ir.nodes import (
51
51
  MermaidDiagram,
52
52
  OrderedList,
53
53
  Paragraph,
54
+ PlantUMLDiagram,
54
55
  RawHTML,
55
56
  RawInlineHtml,
56
57
  Section,
@@ -187,6 +188,8 @@ def _emit_node(node: IRNode) -> str:
187
188
  return _emit_raw_html(node)
188
189
  if isinstance(node, MermaidDiagram):
189
190
  return _emit_mermaid(node)
191
+ if isinstance(node, PlantUMLDiagram):
192
+ return _emit_plantuml(node)
190
193
  if isinstance(node, ContentTabs):
191
194
  return _emit_content_tabs(node)
192
195
  if isinstance(node, Expandable):
@@ -505,6 +508,25 @@ def _emit_mermaid(node: MermaidDiagram) -> str:
505
508
  )
506
509
 
507
510
 
511
+ def _emit_plantuml(node: PlantUMLDiagram) -> str:
512
+ if node.attachment_name is not None:
513
+ filename = html.escape(node.attachment_name)
514
+ local_attr = (
515
+ f' data-local-path="{html.escape(str(node.local_path))}"'
516
+ if node.local_path is not None
517
+ else ""
518
+ )
519
+ return f'<ac:image ac:align="center"{local_attr}><ri:attachment ri:filename="{filename}"/></ac:image>\n'
520
+ # Fallback: show source as a code block when rendering was skipped/disabled.
521
+ safe = node.source.replace("]]>", "]]]]><![CDATA[>")
522
+ return (
523
+ '<ac:structured-macro ac:name="code">\n'
524
+ ' <ac:parameter ac:name="language">text</ac:parameter>\n'
525
+ f" <ac:plain-text-body><![CDATA[{safe}]]></ac:plain-text-body>\n"
526
+ "</ac:structured-macro>\n"
527
+ )
528
+
529
+
508
530
  def _emit_content_tabs(node: ContentTabs) -> str:
509
531
  # Degrade to a series of expand macros (one per tab)
510
532
  parts: list[str] = []
@@ -623,11 +645,11 @@ def _emit_abbrev_glossary_block(node: AbbrevGlossaryBlock) -> str:
623
645
  f"</ac:structured-macro>"
624
646
  )
625
647
  abbr = html.escape(fn.abbr)
626
- defn = html.escape(fn.definition)
648
+ defn = _emit_inlines(fn.definition)
627
649
  parts.append(f"<li>{anchor_macro}<strong>{abbr}</strong> — {defn}</li>\n")
628
- for abbr, defn in node.extras:
650
+ for fn in node.extras:
629
651
  # No anchor — these only appeared in headings/titles, no inline superscript links here.
630
- parts.append(f"<li><strong>{html.escape(abbr)}</strong> — {html.escape(defn)}</li>\n")
652
+ parts.append(f"<li><strong>{html.escape(fn.abbr)}</strong> — {_emit_inlines(fn.definition)}</li>\n")
631
653
  parts.append("</ol>\n")
632
654
  return "".join(parts)
633
655
 
@@ -378,6 +378,20 @@ class MermaidDiagram(IRNode):
378
378
  local_path: Path | None = None # set by mermaid transform; used by preview renderer
379
379
 
380
380
 
381
+ @dataclass(frozen=True)
382
+ class PlantUMLDiagram(IRNode):
383
+ """A PlantUML diagram (`` ```plantuml `` fenced block).
384
+
385
+ ``source`` is the raw PlantUML DSL. When ``attachment_name`` is set the
386
+ emitter renders an ``<ac:image>`` referencing the uploaded PNG; otherwise
387
+ it falls back to a code block showing the raw source.
388
+ """
389
+
390
+ source: str
391
+ attachment_name: str | None = None
392
+ local_path: Path | None = None # set by plantuml transform; used by preview renderer
393
+
394
+
381
395
  @dataclass(frozen=True)
382
396
  class Tab(IRNode):
383
397
  """A single tab inside a :class:`ContentTabs` group."""
@@ -497,11 +511,16 @@ class AbbrevFootnoteNode(IRNode):
497
511
 
498
512
  The emitter renders ``ABBR<sup>[N]</sup>`` where ``[N]`` links to the
499
513
  corresponding entry in the end-of-page :class:`AbbrevGlossaryBlock`.
514
+
515
+ ``definition`` holds the definition text parsed as inline IR nodes so
516
+ that links and other inline formatting survive into the glossary.
500
517
  """
501
518
 
502
519
  abbr: str
503
- definition: str
504
- number: int # 1-based, assigned by the transform in order of first encounter
520
+ definition: tuple[IRNode, ...]
521
+ # 1-based, assigned by the transform in order of first encounter.
522
+ # ``None`` for glossary-only entries (no inline superscript, no anchor).
523
+ number: int | None
505
524
 
506
525
 
507
526
  @dataclass(frozen=True)
@@ -509,17 +528,17 @@ class AbbrevGlossaryBlock(IRNode):
509
528
  """End-of-page abbreviations reference block.
510
529
 
511
530
  Rendered as a numbered list (with Confluence anchor targets for the
512
- back-links) followed by an optional bullet list of abbreviations that
513
- only appeared in headings or other non-expandable contexts.
531
+ back-links) followed by entries for abbreviations that only appeared
532
+ in headings or other non-expandable contexts.
514
533
 
515
534
  Attributes:
516
535
  footnoted: Abbreviations annotated inline, ordered by first encounter.
517
- extras: ``(abbr, definition)`` pairs for abbreviations that could
536
+ extras: Entries (``number=None``) for abbreviations that could
518
537
  not be annotated inline, sorted alphabetically.
519
538
  """
520
539
 
521
540
  footnoted: tuple[AbbrevFootnoteNode, ...]
522
- extras: tuple[tuple[str, str], ...]
541
+ extras: tuple[AbbrevFootnoteNode, ...]
523
542
 
524
543
 
525
544
  # ── Footnotes ────────────────────────────────────────────────────────────────
@@ -0,0 +1,5 @@
1
+ """Markdown parser: convert preprocessed markdown text into IR nodes."""
2
+
3
+ from mkdocs_to_confluence.parser.markdown import parse, parse_inline
4
+
5
+ __all__ = ["parse", "parse_inline"]
@@ -81,6 +81,7 @@ from mkdocs_to_confluence.ir.nodes import (
81
81
  MermaidDiagram,
82
82
  OrderedList,
83
83
  Paragraph,
84
+ PlantUMLDiagram,
84
85
  RawInlineHtml,
85
86
  Section,
86
87
  StrikethroughNode,
@@ -111,6 +112,16 @@ def parse(text: str) -> tuple[IRNode, ...]:
111
112
  return _build_tree(tokens)
112
113
 
113
114
 
115
+ def parse_inline(text: str) -> tuple[IRNode, ...]:
116
+ """Parse a standalone snippet of inline markdown into IR inline nodes.
117
+
118
+ Used for one-line fragments that live outside the main document flow,
119
+ e.g. abbreviation definitions, where links and other inline formatting
120
+ must still be honoured.
121
+ """
122
+ return _parse_inline(text)
123
+
124
+
114
125
  # ── Internal token types ──────────────────────────────────────────────────────
115
126
 
116
127
 
@@ -1131,6 +1142,8 @@ def _build_tree(
1131
1142
  elif isinstance(token, _CodeToken):
1132
1143
  if token.language and token.language.lower() == "mermaid":
1133
1144
  _append_content(MermaidDiagram(source=token.code), stack, root)
1145
+ elif token.language and token.language.lower() == "plantuml":
1146
+ _append_content(PlantUMLDiagram(source=token.code), stack, root)
1134
1147
  else:
1135
1148
  _append_content(
1136
1149
  CodeBlock(
@@ -0,0 +1,132 @@
1
+ """Generic Kroki diagram rendering utilities shared by all diagram transforms.
2
+
3
+ Provides:
4
+ - :func:`kroki_post_png` — low-level HTTP POST to Kroki for any diagram type.
5
+ - :func:`render_diagrams` — concurrent deduplication/replacement loop used by
6
+ every diagram-type transform. Each transform supplies its own ``render_fn``
7
+ that handles type-specific caching and retry behaviour.
8
+
9
+ Shared constants (timeouts, retry counts, etc.) are defined here so diagram
10
+ transforms stay in sync without duplication.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import dataclasses
16
+ import sys
17
+ import threading
18
+ import urllib.request
19
+ from collections.abc import Callable
20
+ from concurrent.futures import ThreadPoolExecutor, as_completed
21
+ from pathlib import Path
22
+ from typing import Any, Protocol, TypeVar, cast
23
+
24
+ from mkdocs_to_confluence.ir.nodes import IRNode, walk
25
+ from mkdocs_to_confluence.ir.treeutil import replace_nodes
26
+
27
+ DEFAULT_KROKI_URL = "https://kroki.io"
28
+
29
+ _TIMEOUT = 30 # seconds
30
+ _MIN_PNG_BYTES = 67 # smallest valid PNG (1×1 px) is 67 bytes
31
+ _CACHE_LOCK = threading.Lock()
32
+ _MAX_WORKERS = 8
33
+ _RETRY_ATTEMPTS = 3
34
+ _RETRY_BACKOFF = 1.0 # seconds; doubles each attempt
35
+ _RETRYABLE_HTTP = {429, 500, 502, 503, 504}
36
+
37
+
38
+ class _DiagramNode(Protocol):
39
+ """Structural protocol for diagram IR nodes (Mermaid, PlantUML, …)."""
40
+
41
+ source: str
42
+ attachment_name: str | None
43
+ local_path: Path | None
44
+
45
+
46
+ _D = TypeVar("_D", bound="_DiagramNode")
47
+
48
+
49
+ def kroki_post_png(source: str, diagram_type: str, kroki_url: str) -> bytes:
50
+ """POST *source* to Kroki for *diagram_type* and return PNG bytes."""
51
+ url = f"{kroki_url.rstrip('/')}/{diagram_type}/png"
52
+ body = source.encode("utf-8")
53
+ req = urllib.request.Request(
54
+ url,
55
+ data=body,
56
+ headers={
57
+ "Content-Type": "text/plain",
58
+ "Accept": "image/png",
59
+ "User-Agent": "mk2conf/1.0",
60
+ },
61
+ method="POST",
62
+ )
63
+ with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 # nosec B310
64
+ return cast(bytes, resp.read())
65
+
66
+
67
+ def warn(msg: str) -> None:
68
+ print(f" warning {msg}", file=sys.stderr)
69
+
70
+
71
+ def render_diagrams(
72
+ nodes: tuple[IRNode, ...],
73
+ node_class: type[Any],
74
+ render_fn: Callable[[str, bool], Path | None],
75
+ *,
76
+ quiet: bool = False,
77
+ ) -> tuple[tuple[IRNode, ...], list[Path]]:
78
+ """Render all *node_class* nodes to PNG concurrently.
79
+
80
+ *render_fn* is called as ``render_fn(source, quiet)`` for each unique
81
+ diagram source; it must return a :class:`Path` on success or ``None`` on
82
+ failure. Each diagram type supplies its own ``render_fn`` that handles
83
+ caching, retries, and any type-specific fallback.
84
+
85
+ Returns the updated IR node tuple (``attachment_name`` set on successful
86
+ renders) and a list of PNG paths to upload as page attachments. Failed
87
+ nodes are left unchanged so the emitter can fall back to a code block.
88
+ """
89
+ diagrams: list[Any] = []
90
+ seen_sources: set[str] = set()
91
+ for top_node in nodes:
92
+ for node in walk(top_node):
93
+ if isinstance(node, node_class) and node.attachment_name is None:
94
+ if node.source not in seen_sources:
95
+ diagrams.append(node)
96
+ seen_sources.add(node.source)
97
+
98
+ if not diagrams:
99
+ return nodes, []
100
+
101
+ source_to_path: dict[str, Path | None] = {}
102
+ with ThreadPoolExecutor(max_workers=min(_MAX_WORKERS, len(diagrams))) as pool:
103
+ future_to_source = {
104
+ pool.submit(render_fn, d.source, quiet): d.source
105
+ for d in diagrams
106
+ }
107
+ for future in as_completed(future_to_source):
108
+ source = future_to_source[future]
109
+ source_to_path[source] = future.result()
110
+
111
+ attachments: list[Path] = []
112
+ replacements: dict[int, IRNode] = {}
113
+ seen_paths: set[Path] = set()
114
+
115
+ for top_node in nodes:
116
+ for node in walk(top_node):
117
+ if not isinstance(node, node_class) or node.attachment_name is not None:
118
+ continue
119
+ path = source_to_path.get(node.source)
120
+ if path is None:
121
+ continue
122
+ if path not in seen_paths:
123
+ attachments.append(path)
124
+ seen_paths.add(path)
125
+ replacements[id(node)] = dataclasses.replace(node, attachment_name=path.name, local_path=path)
126
+
127
+ if not replacements:
128
+ return nodes, attachments
129
+
130
+ return replace_nodes(nodes, replacements), attachments
131
+
132
+
@@ -65,7 +65,7 @@ _BLOCK_TYPES = (
65
65
  class _State:
66
66
  """Mutable transform state threaded through the recursive walk."""
67
67
 
68
- def __init__(self, abbrevs: dict[str, str]) -> None:
68
+ def __init__(self, abbrevs: dict[str, tuple[IRNode, ...]]) -> None:
69
69
  self.abbrevs = abbrevs
70
70
  self._expanded_list: list[str] = [] # ordered by first encounter
71
71
  self._expanded_set: set[str] = set() # fast membership test
@@ -207,7 +207,7 @@ def _transform_inline(node: IRNode, state: _State, safe: bool) -> tuple[IRNode,
207
207
  # ── Glossary builder ──────────────────────────────────────────────────────────
208
208
 
209
209
 
210
- def _find_mentioned(text: str, abbrevs: dict[str, str]) -> set[str]:
210
+ def _find_mentioned(text: str, abbrevs: dict[str, tuple[IRNode, ...]]) -> set[str]:
211
211
  """Return abbreviations that appear as whole words anywhere in *text*."""
212
212
  return {
213
213
  abbr
@@ -221,7 +221,7 @@ def _find_mentioned(text: str, abbrevs: dict[str, str]) -> set[str]:
221
221
 
222
222
  def apply_abbreviations(
223
223
  nodes: tuple[IRNode, ...],
224
- abbrevs: dict[str, str],
224
+ abbrevs: dict[str, tuple[IRNode, ...]],
225
225
  *,
226
226
  page_text: str = "",
227
227
  ) -> tuple[IRNode, ...]:
@@ -229,8 +229,12 @@ def apply_abbreviations(
229
229
 
230
230
  Args:
231
231
  nodes: Top-level IR nodes returned by :func:`parse`.
232
- abbrevs: ``{abbreviation: definition}`` mapping, typically from
233
- :func:`~mkdocs_to_confluence.preprocess.abbrevs.extract_abbreviations`.
232
+ abbrevs: ``{abbreviation: definition}`` mapping where each
233
+ definition is parsed inline IR nodes (the caller parses
234
+ the raw definition strings from
235
+ :func:`~mkdocs_to_confluence.preprocess.abbrevs.extract_abbreviations`
236
+ with :func:`~mkdocs_to_confluence.parser.parse_inline` so
237
+ links and other inline formatting are preserved).
234
238
  page_text: The preprocessed page text (after stripping abbreviation
235
239
  definition lines) used to detect which abbreviations are
236
240
  actually present on the page.
@@ -255,7 +259,7 @@ def apply_abbreviations(
255
259
  for i, abbr in enumerate(state._expanded_list)
256
260
  )
257
261
  extras = tuple(
258
- (abbr, abbrevs[abbr])
262
+ AbbrevFootnoteNode(abbr=abbr, definition=abbrevs[abbr], number=None)
259
263
  for abbr in sorted(mentioned - state._expanded_set)
260
264
  )
261
265
 
@@ -23,32 +23,30 @@ out to external services.
23
23
  from __future__ import annotations
24
24
 
25
25
  import base64
26
- import dataclasses
27
26
  import hashlib
28
27
  import json
29
28
  import sys
30
- import threading
31
29
  import time
32
30
  import urllib.error
33
31
  import urllib.request
34
32
  import zlib
35
- from concurrent.futures import ThreadPoolExecutor, as_completed
36
33
  from pathlib import Path
37
34
  from typing import cast
38
35
 
39
- from mkdocs_to_confluence.ir.nodes import IRNode, MermaidDiagram, walk
40
- from mkdocs_to_confluence.ir.treeutil import replace_nodes
36
+ from mkdocs_to_confluence.ir.nodes import IRNode, MermaidDiagram
37
+ from mkdocs_to_confluence.transforms._kroki import (
38
+ _CACHE_LOCK,
39
+ _MIN_PNG_BYTES,
40
+ _RETRY_ATTEMPTS,
41
+ _RETRY_BACKOFF,
42
+ _RETRYABLE_HTTP,
43
+ _TIMEOUT,
44
+ DEFAULT_KROKI_URL,
45
+ render_diagrams,
46
+ )
41
47
 
42
48
  _CACHE_DIR = Path.home() / ".cache" / "mk2conf" / "mermaid"
43
- DEFAULT_KROKI_URL = "https://kroki.io"
44
49
  _MERMAID_INK_URL = "https://mermaid.ink"
45
- _TIMEOUT = 30 # seconds
46
- _MIN_PNG_BYTES = 67 # smallest valid PNG (1×1 px) is 67 bytes
47
- _CACHE_LOCK = threading.Lock()
48
- _MAX_WORKERS = 8
49
- _RETRY_ATTEMPTS = 3
50
- _RETRY_BACKOFF = 1.0 # seconds; doubles each attempt
51
- _RETRYABLE_HTTP = {429, 500, 502, 503, 504}
52
50
 
53
51
 
54
52
  def _kroki_png(source: str, kroki_url: str) -> bytes:
@@ -189,49 +187,7 @@ def render_mermaid_diagrams(
189
187
  except OSError as exc:
190
188
  _warn(f"cannot create mermaid cache dir {_CACHE_DIR}: {exc} — all diagrams will fall back to code blocks")
191
189
 
192
- # Collect all unresolved MermaidDiagram nodes (deduplicated by source).
193
- diagrams: list[MermaidDiagram] = []
194
- seen_sources: set[str] = set()
195
- for top_node in nodes:
196
- for node in walk(top_node):
197
- if isinstance(node, MermaidDiagram) and node.attachment_name is None:
198
- if node.source not in seen_sources:
199
- diagrams.append(node)
200
- seen_sources.add(node.source)
201
-
202
- if not diagrams:
203
- return nodes, []
204
-
205
- # Render all diagrams concurrently, preserving source → result mapping.
206
- source_to_path: dict[str, Path | None] = {}
207
- with ThreadPoolExecutor(max_workers=min(_MAX_WORKERS, len(diagrams))) as pool:
208
- future_to_source = {
209
- pool.submit(_render_one, d.source, kroki_url, quiet=quiet): d.source for d in diagrams
210
- }
211
- for future in as_completed(future_to_source):
212
- source = future_to_source[future]
213
- source_to_path[source] = future.result()
214
-
215
- # Build replacements and attachments from results.
216
- attachments: list[Path] = []
217
- replacements: dict[int, IRNode] = {}
218
- seen_paths: set[Path] = set()
219
-
220
- for top_node in nodes:
221
- for node in walk(top_node):
222
- if not isinstance(node, MermaidDiagram) or node.attachment_name is not None:
223
- continue
224
- path = source_to_path.get(node.source)
225
- if path is None:
226
- continue # render failed — leave as code block
227
- if path not in seen_paths:
228
- attachments.append(path)
229
- seen_paths.add(path)
230
- replacements[id(node)] = dataclasses.replace(
231
- node, attachment_name=path.name, local_path=path
232
- )
233
-
234
- if not replacements:
235
- return nodes, attachments
236
-
237
- return replace_nodes(nodes, replacements), attachments
190
+ def render_fn(source: str, q: bool) -> Path | None:
191
+ return _render_one(source, kroki_url, quiet=q)
192
+
193
+ return render_diagrams(nodes, MermaidDiagram, render_fn, quiet=quiet)