mkdocs2confluence 0.19.0__tar.gz → 0.20.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 (111) hide show
  1. {mkdocs2confluence-0.19.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.20.0}/PKG-INFO +1 -1
  2. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/pyproject.toml +1 -1
  3. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0/src/mkdocs2confluence.egg-info}/PKG-INFO +1 -1
  4. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/cli.py +4 -1
  5. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/emitter/xhtml.py +51 -2
  6. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/loader/config.py +29 -1
  7. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/parser/markdown.py +1 -1
  8. mkdocs2confluence-0.20.0/tests/test_admonition_titles.py +118 -0
  9. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_publish_config.py +42 -0
  10. mkdocs2confluence-0.19.0/tests/test_admonition_titles.py +0 -62
  11. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/LICENSE +0 -0
  12. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/README.md +0 -0
  13. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/setup.cfg +0 -0
  14. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
  15. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  16. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  17. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  18. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  19. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/__init__.py +0 -0
  20. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/compiler/__init__.py +0 -0
  21. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/compiler/models.py +0 -0
  22. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/compiler/page.py +0 -0
  23. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  24. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  25. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  26. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  27. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  28. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  29. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  30. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/loader/page.py +0 -0
  31. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  32. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  33. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  34. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  35. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  36. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  37. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preprocess/captions.py +0 -0
  38. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  39. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  40. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  41. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  42. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  43. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  44. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preview/render.py +0 -0
  45. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/preview/server.py +0 -0
  46. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  47. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/publisher/changelog.py +0 -0
  48. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  49. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/publisher/executor.py +0 -0
  50. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/publisher/http_retry.py +0 -0
  51. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/publisher/models.py +0 -0
  52. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/publisher/planner.py +0 -0
  53. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/skill_installer.py +0 -0
  54. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/skills/mkdocs-changelog/SKILL.md +0 -0
  55. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/skills/mkdocs-changelog/scripts/changelog_data.py +0 -0
  56. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
  57. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
  58. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/sync/command.py +0 -0
  59. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/sync/comments.py +0 -0
  60. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/sync/github.py +0 -0
  61. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/sync/platform.py +0 -0
  62. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/sync/state.py +0 -0
  63. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  64. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/_kroki.py +0 -0
  65. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  66. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/admonition_titles.py +0 -0
  67. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  68. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/attachment_previews.py +0 -0
  69. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/captions.py +0 -0
  70. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  71. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
  72. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  73. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  74. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  75. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/src/mkdocs_to_confluence/transforms/plantuml.py +0 -0
  76. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_abbrevs.py +0 -0
  77. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_attachment_previews.py +0 -0
  78. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_captions.py +0 -0
  79. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_changelog_config.py +0 -0
  80. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_changelog_publish.py +0 -0
  81. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_children_macro.py +0 -0
  82. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_cli.py +0 -0
  83. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_editlink.py +0 -0
  84. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_emitter.py +0 -0
  85. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_exclude_properties_config.py +0 -0
  86. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_extra_css.py +0 -0
  87. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_footer.py +0 -0
  88. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_frontmatter.py +0 -0
  89. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_icons.py +0 -0
  90. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_images.py +0 -0
  91. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_internallinks.py +0 -0
  92. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_ir.py +0 -0
  93. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_linkdefs.py +0 -0
  94. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_loader.py +0 -0
  95. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_mermaid.py +0 -0
  96. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_page_loader.py +0 -0
  97. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_parser.py +0 -0
  98. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_pdf.py +0 -0
  99. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_plantuml.py +0 -0
  100. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_preprocess.py +0 -0
  101. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_preview.py +0 -0
  102. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_publish_client.py +0 -0
  103. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_publish_pipeline.py +0 -0
  104. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_server.py +0 -0
  105. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_skill_installer.py +0 -0
  106. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_sync_anchoring.py +0 -0
  107. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_sync_command.py +0 -0
  108. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_sync_comments.py +0 -0
  109. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_sync_github.py +0 -0
  110. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_sync_state.py +0 -0
  111. {mkdocs2confluence-0.19.0 → mkdocs2confluence-0.20.0}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.19.0
3
+ Version: 0.20.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.19.0"
3
+ version = "0.20.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.19.0
3
+ Version: 0.20.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
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
 
11
11
  from mkdocs_to_confluence import __version__
12
12
  from mkdocs_to_confluence.compiler.page import compile_page
13
- from mkdocs_to_confluence.emitter.xhtml import configure_styles
13
+ from mkdocs_to_confluence.emitter.xhtml import configure_admonitions, configure_styles
14
14
  from mkdocs_to_confluence.loader.config import load_config
15
15
  from mkdocs_to_confluence.loader.nav import find_section, find_section_by_folder, flat_pages, resolve_nav
16
16
  from mkdocs_to_confluence.loader.page import PageLoadError, find_page
@@ -340,6 +340,7 @@ def _cmd_preview(args: argparse.Namespace) -> None:
340
340
  config_path = Path(args.config).resolve()
341
341
  config = load_config(config_path)
342
342
  configure_styles(config.extra_styles)
343
+ configure_admonitions(config.confluence.admonitions if config.confluence else None)
343
344
 
344
345
  all_nodes = resolve_nav(config)
345
346
 
@@ -495,6 +496,7 @@ def _cmd_publish(args: argparse.Namespace) -> None:
495
496
  config_path = Path(args.config).resolve()
496
497
  config = load_config(config_path)
497
498
  configure_styles(config.extra_styles)
499
+ configure_admonitions(config.confluence.admonitions if config.confluence else None)
498
500
 
499
501
  conf_config = config.confluence
500
502
  if conf_config is None:
@@ -687,6 +689,7 @@ def _cmd_pdf(args: argparse.Namespace) -> None:
687
689
  config_path = Path(args.config).resolve()
688
690
  config = load_config(config_path)
689
691
  configure_styles(config.extra_styles)
692
+ configure_admonitions(config.confluence.admonitions if config.confluence else None)
690
693
 
691
694
  nodes = resolve_nav(config)
692
695
 
@@ -17,6 +17,7 @@ https://developer.atlassian.com/server/confluence/confluence-storage-format/
17
17
  from __future__ import annotations
18
18
 
19
19
  import html
20
+ import sys
20
21
  from pathlib import Path
21
22
  from typing import Sequence
22
23
 
@@ -77,6 +78,32 @@ def configure_styles(styles: ExtraStyles | None) -> None:
77
78
  global _styles
78
79
  _styles = styles
79
80
 
81
+
82
+ # ── Custom admonition aliases (set once per run via configure_admonitions) ─────
83
+
84
+ _admonition_aliases: dict[str, str] = {}
85
+
86
+
87
+ def configure_admonitions(aliases: dict[str, str] | None) -> None:
88
+ """Map custom admonition kinds onto built-in ones, from ``confluence.admonitions``.
89
+
90
+ Targets naming no built-in kind are kept but warned about — they resolve
91
+ through the same fallback as any unknown kind, so a typo degrades rather
92
+ than fails the build.
93
+ """
94
+ global _admonition_aliases
95
+ _admonition_aliases = dict(aliases or {})
96
+ unknown = sorted(
97
+ f"{k} -> {v}" for k, v in _admonition_aliases.items() if v not in _KNOWN_ADMONITION_KINDS
98
+ )
99
+ if unknown:
100
+ print(
101
+ "warning: confluence.admonitions targets an unknown admonition kind: "
102
+ + ", ".join(unknown)
103
+ + f". Known kinds: {', '.join(sorted(_KNOWN_ADMONITION_KINDS))}.",
104
+ file=sys.stderr,
105
+ )
106
+
80
107
  # ── Admonition kind → Confluence macro name ───────────────────────────────────
81
108
 
82
109
  # Types that map to Confluence's four native panel macros (include built-in icons).
@@ -106,6 +133,11 @@ _ADMONITION_MACRO: dict[str, str] = {
106
133
  _ADMONITION_DANGER_KINDS: frozenset[str] = frozenset(
107
134
  {"danger", "error", "bug", "failure", "fail", "missing"}
108
135
  )
136
+ # Every kind the emitter understands as an alias target.
137
+ _KNOWN_ADMONITION_KINDS: frozenset[str] = (
138
+ frozenset(_ADMONITION_MACRO) | _ADMONITION_DANGER_KINDS
139
+ )
140
+
109
141
  _DANGER_EMOJI = "🚨"
110
142
  _DANGER_COLOURS = {
111
143
  "borderColor": "#DE350B",
@@ -350,8 +382,25 @@ def _emit_code_block(node: CodeBlock) -> str:
350
382
  return "".join(parts)
351
383
 
352
384
 
385
+ def _resolve_admonition_kind(kind: str) -> str:
386
+ """Resolve *kind* to the built-in kind that decides its Confluence rendering.
387
+
388
+ Order: an explicit ``confluence.admonitions`` alias, then the built-in kinds,
389
+ then ``success`` for anything unrecognised.
390
+ """
391
+ alias = _admonition_aliases.get(kind)
392
+ if alias is not None:
393
+ return alias
394
+ if kind in _KNOWN_ADMONITION_KINDS:
395
+ return kind
396
+ return "success"
397
+
398
+
353
399
  def _emit_admonition(node: Admonition) -> str:
400
+ # The title keeps the author's own kind ("Value-driver"), only the styling
401
+ # follows the resolved kind.
354
402
  title = node.title or _DEFAULT_ADMONITION_TITLES.get(node.kind, node.kind.capitalize())
403
+ kind = _resolve_admonition_kind(node.kind)
355
404
  body = emit(node.children)
356
405
 
357
406
  # ??? and ???+ → Confluence expand macro (collapsible)
@@ -367,7 +416,7 @@ def _emit_admonition(node: Admonition) -> str:
367
416
 
368
417
  # Fall through for ???+ (expanded=True) — render as a regular admonition below.
369
418
 
370
- if node.kind in _ADMONITION_DANGER_KINDS:
419
+ if kind in _ADMONITION_DANGER_KINDS:
371
420
  colours = "".join(
372
421
  f' <ac:parameter ac:name="{k}">{v}</ac:parameter>\n'
373
422
  for k, v in _DANGER_COLOURS.items()
@@ -381,7 +430,7 @@ def _emit_admonition(node: Admonition) -> str:
381
430
  "</ac:structured-macro>\n"
382
431
  )
383
432
 
384
- macro_name = _ADMONITION_MACRO.get(node.kind, "info")
433
+ macro_name = _ADMONITION_MACRO.get(kind, "tip")
385
434
  return (
386
435
  f'<ac:structured-macro ac:name="{macro_name}">\n'
387
436
  f' <ac:parameter ac:name="title">{html.escape(title)}</ac:parameter>\n'
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import re
7
- from dataclasses import dataclass
7
+ from dataclasses import dataclass, field
8
8
  from pathlib import Path
9
9
  from typing import Any
10
10
  from urllib.parse import urlparse
@@ -38,6 +38,10 @@ class ConfluenceConfig:
38
38
  exclude_properties: tuple[str, ...] = () # front matter keys to omit from Page Properties table
39
39
  attachment_preview: bool = False # render PDF/Office attachment links as view-file macros
40
40
  children_macro: bool = True # append Children Display macro to section index pages
41
+ # Custom admonition kind → built-in kind it should render as, e.g.
42
+ # {"value-driver": "tip"}. Kinds absent here fall back to the built-in
43
+ # mapping, then to "success". Emitter owns the target vocabulary.
44
+ admonitions: dict[str, str] = field(default_factory=dict)
41
45
 
42
46
 
43
47
  @dataclass(frozen=True)
@@ -314,6 +318,29 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
314
318
  )
315
319
  children_macro = raw_children
316
320
 
321
+ # admonitions (optional) — map custom admonition kinds onto built-in ones.
322
+ # Only the shape is checked here; the emitter owns the target vocabulary
323
+ # and warns about unknown targets, since loader sits below it in the layering.
324
+ raw_admonitions = raw_conf.get("admonitions", {})
325
+ if not isinstance(raw_admonitions, dict):
326
+ raise ConfigError(
327
+ "mkdocs.yml: 'confluence.admonitions' must be a mapping of "
328
+ f"custom kind to built-in kind, got {type(raw_admonitions).__name__}."
329
+ )
330
+ admonitions: dict[str, str] = {}
331
+ for key, value in raw_admonitions.items():
332
+ if not isinstance(key, str) or not key.strip():
333
+ raise ConfigError(
334
+ "mkdocs.yml: 'confluence.admonitions' keys must be non-empty strings."
335
+ )
336
+ if not isinstance(value, str) or not value.strip():
337
+ raise ConfigError(
338
+ f"mkdocs.yml: 'confluence.admonitions.{key}' must be a non-empty "
339
+ "string naming a built-in admonition kind (e.g. 'tip', 'warning')."
340
+ )
341
+ # Admonition kinds are case-insensitive in Markdown; normalise both sides.
342
+ admonitions[key.strip().lower()] = value.strip().lower()
343
+
317
344
  confluence = ConfluenceConfig(
318
345
  base_url=base_url.rstrip("/"),
319
346
  space_key=space_key,
@@ -332,6 +359,7 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
332
359
  exclude_properties=exclude_properties,
333
360
  attachment_preview=attachment_preview,
334
361
  children_macro=children_macro,
362
+ admonitions=admonitions,
335
363
  )
336
364
 
337
365
  # --- extra_css (optional) ---
@@ -256,7 +256,7 @@ _FENCE_OPEN_RE = re.compile(r"^(?P<fence>`{3,}|~{3,})(?P<info>.*)$")
256
256
 
257
257
  # Matches an admonition opener: !!! kind / !!! kind "title" / ??? kind / ???+ kind
258
258
  _ADMONITION_RE = re.compile(
259
- r"""^[ ]{0,3}(?P<marker>!{3}|\?{3}\+?)\s+(?P<kind>\w+)(?:\s+(?P<q>["'])(?P<title>.*?)(?P=q))?\s*$"""
259
+ r"""^[ ]{0,3}(?P<marker>!{3}|\?{3}\+?)\s+(?P<kind>[\w-]+)(?:\s+(?P<q>["'])(?P<title>.*?)(?P=q))?\s*$"""
260
260
  )
261
261
 
262
262
  # Matches a Material for MkDocs content tab opener: === "Label"
@@ -0,0 +1,118 @@
1
+ """Tests for the admonition-title link degradation transform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from mkdocs_to_confluence.ir.nodes import Admonition, Section, TextNode
6
+ from mkdocs_to_confluence.transforms.admonition_titles import (
7
+ strip_links_in_admonition_titles,
8
+ )
9
+
10
+
11
+ def _adm(title: str | None) -> Admonition:
12
+ return Admonition(kind="warning", title=title, children=())
13
+
14
+
15
+ def test_link_in_title_stripped_to_text() -> None:
16
+ nodes = (_adm("Conflict - see [Hello](foobar.md#hello)"),)
17
+ out = strip_links_in_admonition_titles(nodes, "index.md")
18
+ assert out[0].title == "Conflict - see Hello"
19
+
20
+
21
+ def test_warning_emitted_with_page_and_title(capsys) -> None:
22
+ nodes = (_adm("see [Hello](foobar.md#hello)"),)
23
+ strip_links_in_admonition_titles(nodes, "guide/index.md")
24
+ err = capsys.readouterr().err
25
+ assert "warning" in err
26
+ assert "guide/index.md" in err
27
+ assert "see [Hello](foobar.md#hello)" in err
28
+
29
+
30
+ def test_title_without_link_unchanged_and_no_warning(capsys) -> None:
31
+ nodes = (_adm("Just a plain title"),)
32
+ out = strip_links_in_admonition_titles(nodes, "index.md")
33
+ assert out[0].title == "Just a plain title"
34
+ assert capsys.readouterr().err == ""
35
+
36
+
37
+ def test_none_title_is_safe(capsys) -> None:
38
+ nodes = (_adm(None),)
39
+ out = strip_links_in_admonition_titles(nodes, "index.md")
40
+ assert out[0].title is None
41
+ assert capsys.readouterr().err == ""
42
+
43
+
44
+ def test_multiple_links_all_stripped() -> None:
45
+ nodes = (_adm("[A](a.md) and [B](b.md#x)"),)
46
+ out = strip_links_in_admonition_titles(nodes, "index.md")
47
+ assert out[0].title == "A and B"
48
+
49
+
50
+ def test_nested_admonition_in_section_processed() -> None:
51
+ inner = _adm("see [Hello](foobar.md#hello)")
52
+ section = Section(level=2, anchor="s", title=(TextNode("S"),), children=(inner,))
53
+ out = strip_links_in_admonition_titles((section,), "index.md")
54
+ nested = out[0].children[0]
55
+ assert isinstance(nested, Admonition)
56
+ assert nested.title == "see Hello"
57
+
58
+
59
+ def test_image_in_title_not_mangled() -> None:
60
+ nodes = (_adm("look ![alt](img.png)"),)
61
+ out = strip_links_in_admonition_titles(nodes, "index.md")
62
+ assert out[0].title == "look ![alt](img.png)"
63
+
64
+
65
+ class TestCustomAdmonitionKinds:
66
+ """`confluence.admonitions` config, then built-ins, then a `success` default."""
67
+
68
+ def setup_method(self) -> None:
69
+ from mkdocs_to_confluence.emitter.xhtml import configure_admonitions
70
+ configure_admonitions(None)
71
+
72
+ teardown_method = setup_method
73
+
74
+ def _macro(self, src: str) -> str:
75
+ import re
76
+
77
+ from mkdocs_to_confluence.emitter.xhtml import emit
78
+ from mkdocs_to_confluence.parser import parse
79
+ m = re.search(r'ac:name="(\w+)"', emit(parse(src)))
80
+ return m.group(1) if m else ""
81
+
82
+ def test_hyphenated_kind_parses_as_an_admonition(self) -> None:
83
+ """`!!! value-driver` must not fall through to a paragraph."""
84
+ assert self._macro('!!! value-driver\n\n Body.\n') != ""
85
+
86
+ def test_unknown_kind_defaults_to_success(self) -> None:
87
+ assert self._macro('!!! value-driver\n\n Body.\n') == "tip" # success -> tip
88
+
89
+ def test_builtin_kind_is_unaffected(self) -> None:
90
+ assert self._macro('!!! note\n\n Body.\n') == "info"
91
+ assert self._macro('!!! warning\n\n Body.\n') == "warning"
92
+
93
+ def test_config_alias_wins_over_the_default(self) -> None:
94
+ from mkdocs_to_confluence.emitter.xhtml import configure_admonitions
95
+ configure_admonitions({"value-driver": "warning"})
96
+ assert self._macro('!!! value-driver\n\n Body.\n') == "warning"
97
+
98
+ def test_config_alias_can_target_a_danger_panel(self) -> None:
99
+ from mkdocs_to_confluence.emitter.xhtml import configure_admonitions
100
+ configure_admonitions({"risk": "danger"})
101
+ assert self._macro('!!! risk\n\n Body.\n') == "panel"
102
+
103
+ def test_config_alias_can_override_a_builtin(self) -> None:
104
+ from mkdocs_to_confluence.emitter.xhtml import configure_admonitions
105
+ configure_admonitions({"note": "warning"})
106
+ assert self._macro('!!! note\n\n Body.\n') == "warning"
107
+
108
+ def test_title_keeps_the_authors_kind_not_the_alias(self) -> None:
109
+ from mkdocs_to_confluence.emitter.xhtml import configure_admonitions, emit
110
+ from mkdocs_to_confluence.parser import parse
111
+ configure_admonitions({"value-driver": "warning"})
112
+ assert "Value-driver" in emit(parse('!!! value-driver\n\n Body.\n'))
113
+
114
+ def test_unknown_alias_target_warns_and_degrades(self, capsys) -> None: # type: ignore[no-untyped-def]
115
+ from mkdocs_to_confluence.emitter.xhtml import configure_admonitions
116
+ configure_admonitions({"oops": "nonsense"})
117
+ assert "unknown admonition kind" in capsys.readouterr().err
118
+ assert self._macro('!!! oops\n\n Body.\n') == "tip"
@@ -537,3 +537,45 @@ confluence:
537
537
  config = load_config(config_file)
538
538
  assert config.confluence is not None
539
539
  assert config.confluence.allow_any_host is False
540
+
541
+
542
+ class TestAdmonitionsConfig:
543
+ """`confluence.admonitions` parsing and validation."""
544
+
545
+ def _write(self, tmp_path, block: str): # type: ignore[no-untyped-def]
546
+ from mkdocs_to_confluence.loader.config import load_config
547
+ (tmp_path / "docs").mkdir(exist_ok=True)
548
+ (tmp_path / "mkdocs.yml").write_text(
549
+ "site_name: T\n"
550
+ "confluence:\n"
551
+ " base_url: https://x.atlassian.net\n"
552
+ " email: a@b.c\n"
553
+ " token: t\n"
554
+ " space_key: S\n" + block,
555
+ encoding="utf-8",
556
+ )
557
+ return load_config(tmp_path / "mkdocs.yml")
558
+
559
+ def test_absent_defaults_to_empty(self, tmp_path) -> None: # type: ignore[no-untyped-def]
560
+ cfg = self._write(tmp_path, "")
561
+ assert cfg.confluence is not None
562
+ assert cfg.confluence.admonitions == {}
563
+
564
+ def test_mapping_is_parsed_and_lowercased(self, tmp_path) -> None: # type: ignore[no-untyped-def]
565
+ cfg = self._write(tmp_path, " admonitions:\n Value-Driver: TIP\n risk: danger\n")
566
+ assert cfg.confluence is not None
567
+ assert cfg.confluence.admonitions == {"value-driver": "tip", "risk": "danger"}
568
+
569
+ def test_non_mapping_is_rejected(self, tmp_path) -> None: # type: ignore[no-untyped-def]
570
+ import pytest
571
+
572
+ from mkdocs_to_confluence.loader.config import ConfigError
573
+ with pytest.raises(ConfigError, match="must be a mapping"):
574
+ self._write(tmp_path, " admonitions:\n - tip\n")
575
+
576
+ def test_non_string_value_is_rejected(self, tmp_path) -> None: # type: ignore[no-untyped-def]
577
+ import pytest
578
+
579
+ from mkdocs_to_confluence.loader.config import ConfigError
580
+ with pytest.raises(ConfigError, match="non-empty"):
581
+ self._write(tmp_path, " admonitions:\n risk: 5\n")
@@ -1,62 +0,0 @@
1
- """Tests for the admonition-title link degradation transform."""
2
-
3
- from __future__ import annotations
4
-
5
- from mkdocs_to_confluence.ir.nodes import Admonition, Section, TextNode
6
- from mkdocs_to_confluence.transforms.admonition_titles import (
7
- strip_links_in_admonition_titles,
8
- )
9
-
10
-
11
- def _adm(title: str | None) -> Admonition:
12
- return Admonition(kind="warning", title=title, children=())
13
-
14
-
15
- def test_link_in_title_stripped_to_text() -> None:
16
- nodes = (_adm("Conflict - see [Hello](foobar.md#hello)"),)
17
- out = strip_links_in_admonition_titles(nodes, "index.md")
18
- assert out[0].title == "Conflict - see Hello"
19
-
20
-
21
- def test_warning_emitted_with_page_and_title(capsys) -> None:
22
- nodes = (_adm("see [Hello](foobar.md#hello)"),)
23
- strip_links_in_admonition_titles(nodes, "guide/index.md")
24
- err = capsys.readouterr().err
25
- assert "warning" in err
26
- assert "guide/index.md" in err
27
- assert "see [Hello](foobar.md#hello)" in err
28
-
29
-
30
- def test_title_without_link_unchanged_and_no_warning(capsys) -> None:
31
- nodes = (_adm("Just a plain title"),)
32
- out = strip_links_in_admonition_titles(nodes, "index.md")
33
- assert out[0].title == "Just a plain title"
34
- assert capsys.readouterr().err == ""
35
-
36
-
37
- def test_none_title_is_safe(capsys) -> None:
38
- nodes = (_adm(None),)
39
- out = strip_links_in_admonition_titles(nodes, "index.md")
40
- assert out[0].title is None
41
- assert capsys.readouterr().err == ""
42
-
43
-
44
- def test_multiple_links_all_stripped() -> None:
45
- nodes = (_adm("[A](a.md) and [B](b.md#x)"),)
46
- out = strip_links_in_admonition_titles(nodes, "index.md")
47
- assert out[0].title == "A and B"
48
-
49
-
50
- def test_nested_admonition_in_section_processed() -> None:
51
- inner = _adm("see [Hello](foobar.md#hello)")
52
- section = Section(level=2, anchor="s", title=(TextNode("S"),), children=(inner,))
53
- out = strip_links_in_admonition_titles((section,), "index.md")
54
- nested = out[0].children[0]
55
- assert isinstance(nested, Admonition)
56
- assert nested.title == "see Hello"
57
-
58
-
59
- def test_image_in_title_not_mangled() -> None:
60
- nodes = (_adm("look ![alt](img.png)"),)
61
- out = strip_links_in_admonition_titles(nodes, "index.md")
62
- assert out[0].title == "look ![alt](img.png)"