mkdocs2confluence 0.7.7__tar.gz → 0.7.9__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 (72) hide show
  1. {mkdocs2confluence-0.7.7/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.7.9}/PKG-INFO +1 -1
  2. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/pyproject.toml +1 -1
  3. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9/src/mkdocs2confluence.egg-info}/PKG-INFO +1 -1
  4. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/emitter/xhtml.py +5 -1
  5. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/ir/nodes.py +2 -0
  6. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/parser/markdown.py +4 -0
  7. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preprocess/icons.py +102 -107
  8. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_emitter.py +12 -0
  9. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_icons.py +43 -46
  10. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_parser.py +15 -0
  11. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/LICENSE +0 -0
  12. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/README.md +0 -0
  13. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/setup.cfg +0 -0
  14. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
  15. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  16. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  17. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  18. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  19. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/__init__.py +0 -0
  20. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/cli.py +0 -0
  21. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  22. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  23. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/ir/document.py +0 -0
  24. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  25. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  26. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/loader/config.py +0 -0
  27. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  28. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  29. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/loader/page.py +0 -0
  30. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  31. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  32. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  33. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  34. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  35. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  36. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  37. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  38. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  39. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  40. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  41. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preview/render.py +0 -0
  42. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/preview/server.py +0 -0
  43. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  44. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  45. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/publisher/pipeline.py +0 -0
  46. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  47. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  48. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  49. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  50. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  51. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  52. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  53. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_abbrevs.py +0 -0
  54. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_cli.py +0 -0
  55. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_editlink.py +0 -0
  56. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_extra_css.py +0 -0
  57. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_frontmatter.py +0 -0
  58. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_images.py +0 -0
  59. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_internallinks.py +0 -0
  60. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_ir.py +0 -0
  61. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_linkdefs.py +0 -0
  62. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_loader.py +0 -0
  63. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_mermaid.py +0 -0
  64. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_page_loader.py +0 -0
  65. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_pdf.py +0 -0
  66. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_preprocess.py +0 -0
  67. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_preview.py +0 -0
  68. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_publish_client.py +0 -0
  69. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_publish_config.py +0 -0
  70. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_publish_pipeline.py +0 -0
  71. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_server.py +0 -0
  72. {mkdocs2confluence-0.7.7 → mkdocs2confluence-0.7.9}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.7.7
3
+ Version: 0.7.9
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.7.7"
3
+ version = "0.7.9"
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.7.7
3
+ Version: 0.7.9
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
@@ -307,7 +307,9 @@ def _emit_admonition(node: Admonition) -> str:
307
307
  body = emit(node.children)
308
308
 
309
309
  # ??? and ???+ → Confluence expand macro (collapsible)
310
- if node.collapsible:
310
+ # ???+ means "expanded by default" — Confluence expand has no such option,
311
+ # so degrade to a regular admonition macro (always visible).
312
+ if node.collapsible and not node.expanded:
311
313
  return (
312
314
  '<ac:structured-macro ac:name="expand">\n'
313
315
  f' <ac:parameter ac:name="title">{html.escape(title)}</ac:parameter>\n'
@@ -315,6 +317,8 @@ def _emit_admonition(node: Admonition) -> str:
315
317
  "</ac:structured-macro>\n"
316
318
  )
317
319
 
320
+ # Fall through for ???+ (expanded=True) — render as a regular admonition below.
321
+
318
322
  if node.kind in _ADMONITION_DANGER_KINDS:
319
323
  colours = "".join(
320
324
  f' <ac:parameter ac:name="{k}">{v}</ac:parameter>\n'
@@ -338,12 +338,14 @@ class Admonition(IRNode):
338
338
  children: Block nodes that form the admonition body.
339
339
  collapsible: ``True`` when opened with ``???`` (collapsed by default)
340
340
  or ``???+`` (expanded by default).
341
+ expanded: ``True`` only for ``???+`` — expanded by default.
341
342
  """
342
343
 
343
344
  kind: str
344
345
  title: str | None
345
346
  children: tuple[IRNode, ...]
346
347
  collapsible: bool = False
348
+ expanded: bool = False
347
349
 
348
350
 
349
351
  @dataclass(frozen=True)
@@ -139,6 +139,7 @@ class _AdmonitionToken:
139
139
  kind: str
140
140
  title: str | None # None → use the kind's default Confluence title
141
141
  collapsible: bool # True for ??? and ???+
142
+ expanded: bool # True for ???+ (expanded by default)
142
143
  body_tokens: list[_Token]
143
144
 
144
145
 
@@ -386,6 +387,7 @@ def _tokenize(text: str) -> list[_Token]:
386
387
  kind = adm_m.group("kind")
387
388
  title = adm_m.group("title") # None if no quoted title given
388
389
  collapsible = marker.startswith("?")
390
+ expanded = marker == "???+"
389
391
  i += 1
390
392
  # Collect body: blank lines or lines indented by ≥4 spaces / 1 tab.
391
393
  body_raw: list[str] = []
@@ -408,6 +410,7 @@ def _tokenize(text: str) -> list[_Token]:
408
410
  kind=kind,
409
411
  title=title,
410
412
  collapsible=collapsible,
413
+ expanded=expanded,
411
414
  body_tokens=body_tokens,
412
415
  )
413
416
  )
@@ -1055,6 +1058,7 @@ def _build_tree(
1055
1058
  kind=token.kind,
1056
1059
  title=token.title,
1057
1060
  collapsible=token.collapsible,
1061
+ expanded=token.expanded,
1058
1062
  children=body_nodes,
1059
1063
  ),
1060
1064
  stack,
@@ -1,18 +1,16 @@
1
1
  """Icon shortcode preprocessing.
2
2
 
3
3
  Replaces MkDocs Material / FontAwesome / Octicons / Simple icon shortcodes
4
- (e.g. ``:material-check-circle:``) with the closest Unicode symbol equivalent,
4
+ (e.g. ``:material-check-circle:``) with the closest Unicode emoji equivalent,
5
5
  or strips them silently when no mapping is found.
6
6
 
7
7
  Strategy: split the icon name on ``-`` and test each part against a small
8
8
  keyword→symbol table. The first matching keyword wins. This keeps the
9
9
  mapping set tiny and maintainable without requiring a full icon inventory.
10
10
 
11
- **BMP-only constraint**: all mapped symbols must lie in the Unicode Basic
12
- Multilingual Plane (U+0000–U+FFFF, 3-byte UTF-8). Supplementary-plane
13
- emoji (U+10000+, 4-byte UTF-8) are not stored correctly by Confluence
14
- deployments that use MySQL ``utf8`` (not ``utf8mb4``) and render as ``???``.
15
- Where no good BMP symbol exists the shortcode is stripped silently (``""``).
11
+ **Unicode range**: Confluence Cloud stores pages as UTF-8 with full Unicode
12
+ support (utf8mb4), so supplementary-plane emoji (U+10000+) are safe.
13
+ Symbols without a clear emoji equivalent are still stripped silently (``""``).
16
14
  """
17
15
 
18
16
  from __future__ import annotations
@@ -29,60 +27,57 @@ _ICON_RE = re.compile(
29
27
  # Must not overlap with _ICON_RE (those all contain a hyphenated prefix).
30
28
  _STANDARD_EMOJI_RE = re.compile(r":([a-z][a-z0-9_]*):")
31
29
 
32
- # Bare emoji shortcode → BMP Unicode symbol (or "" to strip silently).
33
- # BMP-only where a reasonable equivalent exists; supplementary-plane emoji
34
- # are excluded to preserve MySQL utf8 Confluence compatibility.
30
+ # Bare emoji shortcode → Unicode symbol (or "" to strip silently).
35
31
  _STANDARD_EMOJI_MAP: dict[str, str] = {
36
32
  # Alerts / status
37
33
  "warning": "⚠", # U+26A0
38
- "rotating_light": "", # U+26A0 (🚨 U+1F6A8 is supplementary)
34
+ "rotating_light": "🚨", # U+1F6A8
39
35
  "octagonal_sign": "⛔", # U+26D4
40
36
  "no_entry": "⛔", # U+26D4
41
37
  "no_entry_sign": "⛔",
42
38
  "stop_sign": "⛔",
43
39
  "information_source": "ℹ", # U+2139
44
40
  # Checkmarks / marks
45
- "white_check_mark": "✓", # U+2713 (✅ supplementary)
41
+ "white_check_mark": "✓", # U+2713
46
42
  "heavy_check_mark": "✓", # U+2713
47
- "x": "✗", # U+2717 (❌ supplementary)
43
+ "x": "✗", # U+2717
48
44
  "heavy_multiplication_x": "✗",
49
45
  # Objects — tools
50
- "wrench": "", # U+2699 (🔧 supplementary)
46
+ "wrench": "🔧", # U+1F527
51
47
  "gear": "⚙", # U+2699
52
- "hammer": "", # 🔨 supplementary, no BMP equiv — strip
53
- "hammer_and_wrench": "",
48
+ "hammer": "🔨", # U+1F528
49
+ "hammer_and_wrench": "🛠️", # U+1F6E0
54
50
  # Work / business
55
- "briefcase": "", # 💼 supplementary — strip
51
+ "briefcase": "💼", # U+1F4BC
56
52
  # Nature / miscellaneous
57
53
  "star": "★", # U+2605
58
54
  "star2": "★",
59
- "rocket": "", # 🚀 supplementary — strip
60
- "construction": "", # 🚧 supplementary — strip
61
- "tada": "",
62
- "trophy": "",
63
- "thinking": "",
64
- "smile": "",
65
- "laughing": "",
55
+ "rocket": "🚀", # U+1F680
56
+ "construction": "🚧", # U+1F6A7
57
+ "tada": "🎉", # U+1F389
58
+ "trophy": "🏆", # U+1F3C6
59
+ "thinking": "🤔", # U+1F914
60
+ "smile": "😄", # U+1F604
61
+ "laughing": "😆", # U+1F606
66
62
  "heart": "♥", # U+2665
67
- "fire": "", # 🔥 supplementary — strip
68
- "zap": "", # U+26A1 BMP
69
- "bulb": "",
70
- "computer": "",
71
- "notebook": "",
72
- "memo": "",
73
- "clipboard": "",
74
- "link": "",
75
- "label": "",
76
- "bookmark": "",
77
- "chart_with_upwards_trend": "",
78
- "bar_chart": "",
63
+ "fire": "🔥", # U+1F525
64
+ "zap": "", # U+26A1
65
+ "bulb": "💡", # U+1F4A1
66
+ "computer": "💻", # U+1F4BB
67
+ "notebook": "📓", # U+1F4D3
68
+ "memo": "📝", # U+1F4DD
69
+ "clipboard": "📋", # U+1F4CB
70
+ "link": "🔗", # U+1F517
71
+ "label": "🏷️", # U+1F3F7
72
+ "bookmark": "🔖", # U+1F516
73
+ "chart_with_upwards_trend": "📈", # U+1F4C8
74
+ "bar_chart": "📊", # U+1F4CA
79
75
  }
80
76
 
81
77
  # Keyword → symbol. Keys must be lowercase single words (icon name segments).
82
- # ALL values must be BMP characters (U+0000–U+FFFF) or "" (strip silently).
83
78
  # Ordered so earlier, more-specific entries take priority where ambiguous.
84
79
  _KEYWORD_MAP: dict[str, str | None] = {
85
- # Status / validation (all BMP ✓)
80
+ # Status / validation
86
81
  "check": "✓", # U+2713
87
82
  "done": "✓",
88
83
  "complete": "✓",
@@ -103,98 +98,98 @@ _KEYWORD_MAP: dict[str, str | None] = {
103
98
  # Navigation / directional
104
99
  "arrow": None, # resolved with next segment — see _resolve()
105
100
  "chevron": None,
106
- # Security — no good BMP glyph; strip silently
107
- "lock": "",
108
- "security": "",
109
- "shield": "",
110
- "unlock": "",
111
- "key": "",
101
+ # Security
102
+ "lock": "🔒", # U+1F512
103
+ "security": "🛡️", # U+1F6E0
104
+ "shield": "🛡️",
105
+ "unlock": "🔓", # U+1F513
106
+ "key": "🔑", # U+1F511
112
107
  # Actions
113
108
  "download": "↓", # U+2193
114
109
  "upload": "↑", # U+2191
115
- "refresh": "↻", # U+21BB (BMP)
110
+ "refresh": "↻", # U+21BB
116
111
  "sync": "↻",
117
112
  "reload": "↻",
118
- "search": "", # no reliable BMP magnifier glyph; strip
119
- "magnify": "",
120
- "edit": "✎", # U+270E (BMP pencil)
113
+ "search": "🔍", # U+1F50D
114
+ "magnify": "🔍",
115
+ "edit": "✎", # U+270E
121
116
  "pencil": "✎",
122
117
  "pen": "✎",
123
118
  "copy": "", # strip
124
- "clipboard": "",
125
- "trash": "", # strip
126
- "delete": "",
119
+ "clipboard": "📋", # U+1F4CB
120
+ "trash": "🗑️", # U+1F5D1
121
+ "delete": "🗑️",
127
122
  "add": "+",
128
123
  "plus": "+",
129
- "minus": "−", # U+2212 minus sign
130
- "link": "", # strip — no reliable BMP link-chain glyph
131
- "chain": "",
124
+ "minus": "−", # U+2212
125
+ "link": "🔗", # U+1F517
126
+ "chain": "🔗",
132
127
  # Objects / content
133
- "star": "★", # U+2605 (BMP)
128
+ "star": "★", # U+2605
134
129
  "favorite": "★",
135
- "bookmark": "", # strip
136
- "heart": "♥", # U+2665 (BMP)
137
- "fire": "", # strip
138
- "rocket": "", # strip
139
- "launch": "",
140
- "home": "", # strip — ⌂ U+2302 exists but renders poorly
141
- "settings": "⚙", # U+2699 (BMP)
130
+ "bookmark": "🔖", # U+1F516
131
+ "heart": "♥", # U+2665
132
+ "fire": "🔥", # U+1F525
133
+ "rocket": "🚀", # U+1F680
134
+ "launch": "🚀",
135
+ "home": "🏠", # U+1F3E0
136
+ "settings": "⚙", # U+2699
142
137
  "cog": "⚙",
143
138
  "gear": "⚙",
144
- "wrench": "", # strip
145
- "email": "✉", # U+2709 (BMP envelope)
139
+ "wrench": "🔧", # U+1F527
140
+ "email": "✉", # U+2709
146
141
  "mail": "✉",
147
142
  "envelope": "✉",
148
- "phone": "☎", # U+260E (BMP telephone)
149
- "clock": "", # strip
150
- "time": "",
151
- "calendar": "", # strip
152
- "date": "",
153
- "folder": "", # strip
154
- "file": "",
155
- "document": "",
156
- "code": "", # strip
157
- "terminal": "",
158
- "database": "", # strip
159
- "cloud": "☁", # U+2601 (BMP)
160
- "globe": "", # strip
161
- "world": "",
162
- "earth": "",
163
- "chart": "", # strip
164
- "graph": "",
165
- "book": "", # strip
166
- "docs": "",
167
- "note": "", # strip
168
- "tag": "", # strip
169
- "label": "",
170
- "flag": "", # strip
143
+ "phone": "☎", # U+260E
144
+ "clock": "", # U+23F0
145
+ "time": "",
146
+ "calendar": "📅", # U+1F4C5
147
+ "date": "📅",
148
+ "folder": "📁", # U+1F4C1
149
+ "file": "📄", # U+1F4C4
150
+ "document": "📄",
151
+ "code": "💻", # U+1F4BB
152
+ "terminal": "💻",
153
+ "database": "🗄️", # U+1F5C4
154
+ "cloud": "☁", # U+2601
155
+ "globe": "🌍", # U+1F30D
156
+ "world": "🌍",
157
+ "earth": "🌍",
158
+ "chart": "📊", # U+1F4CA
159
+ "graph": "📊",
160
+ "book": "📖", # U+1F4D6
161
+ "docs": "📖",
162
+ "note": "", # strip — ambiguous; not the 📝 emoji
163
+ "tag": "🏷️", # U+1F3F7
164
+ "label": "🏷️",
165
+ "flag": "🚩", # U+1F6A9
171
166
  "eye": "", # strip — was wrongly matching grid/view icons
172
- "view": "", # strip — semantically ambiguous (grid-view ≠ eye)
173
- "grid": "", # strip — layout/grid icons have no BMP analogue
174
- "user": "", # strip
175
- "account": "",
176
- "person": "",
177
- "group": "", # strip
178
- "people": "",
179
- "team": "",
180
- "robot": "", # strip
181
- "bug": "", # strip
167
+ "view": "", # strip — semantically ambiguous
168
+ "grid": "", # strip — layout/grid icons have no emoji analogue
169
+ "user": "👤", # U+1F464
170
+ "account": "👤",
171
+ "person": "👤",
172
+ "group": "👥", # U+1F465
173
+ "people": "👥",
174
+ "team": "👥",
175
+ "robot": "🤖", # U+1F916
176
+ "bug": "🐛", # U+1F41B
182
177
  "test": "", # strip
183
- "flask": "",
184
- "lightbulb": "", # strip
185
- "idea": "",
186
- "package": "", # strip
187
- "server": "",
178
+ "flask": "", # strip
179
+ "lightbulb": "💡", # U+1F4A1
180
+ "idea": "💡",
181
+ "package": "📦", # U+1F4E6
182
+ "server": "🖥️", # U+1F5A5
188
183
  "network": "", # strip
189
- "wifi": "",
184
+ "wifi": "", # strip
190
185
  "battery": "", # strip
191
- "image": "", # strip
192
- "video": "",
193
- "music": "♪", # U+266A (BMP)
186
+ "image": "🖼️", # U+1F5BC
187
+ "video": "🎥", # U+1F3A5
188
+ "music": "♪", # U+266A
194
189
  "printer": "", # strip
195
- "keyboard": "⌨", # U+2328 (BMP)
190
+ "keyboard": "⌨", # U+2328
196
191
  "mouse": "", # strip
197
- "monitor": "", # strip
192
+ "monitor": "🖥️",
198
193
  # Modifier suffixes — never meaningful on their own; always strip
199
194
  "outline": "",
200
195
  "variant": "",
@@ -136,6 +136,18 @@ class TestAdmonitionEmitter:
136
136
  assert 'ac:name="expand"' in out
137
137
  assert 'ac:name="panel"' not in out
138
138
 
139
+ def test_expanded_admonition_renders_as_regular_macro(self) -> None:
140
+ # ???+ (collapsible=True, expanded=True) has no Confluence equivalent for
141
+ # "expanded by default", so we degrade to a regular admonition macro.
142
+ out = emit((
143
+ Admonition(kind="note", title="Visible", children=(Paragraph((TextNode("body"),)),),
144
+ collapsible=True, expanded=True),
145
+ ))
146
+ assert 'ac:name="expand"' not in out
147
+ assert 'ac:name="info"' in out
148
+ assert "Visible" in out
149
+ assert "body" in out
150
+
139
151
 
140
152
  class TestContentTabsEmitter:
141
153
  def test_tabs_render_as_expand_macros(self) -> None:
@@ -1,8 +1,8 @@
1
1
  """Tests for preprocess.icons — icon shortcode stripping/mapping.
2
2
 
3
- All mapped symbols must be BMP characters (U+0000–U+FFFF, ≤ 3-byte UTF-8).
4
- Supplementary-plane emoji (U+10000+) render as ``???`` in Confluence
5
- deployments that use MySQL ``utf8`` rather than ``utf8mb4``.
3
+ Confluence Cloud uses utf8mb4 and supports full Unicode including
4
+ supplementary-plane emoji (U+10000+). Shortcodes with no clear emoji
5
+ equivalent are stripped silently.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -29,23 +29,20 @@ class TestKnownMappings:
29
29
  def test_close_maps_to_cross(self) -> None:
30
30
  assert strip_icon_shortcodes(":material-close:") == "✗"
31
31
 
32
- def test_lock_stripped(self) -> None:
33
- # 🔒 is non-BMP (U+1F512); strip silently instead
34
- assert strip_icon_shortcodes(":material-lock:") == ""
32
+ def test_lock_maps_to_emoji(self) -> None:
33
+ assert strip_icon_shortcodes(":material-lock:") == "🔒"
35
34
 
36
- def test_shield_stripped(self) -> None:
37
- assert strip_icon_shortcodes(":material-shield:") == ""
35
+ def test_shield_maps_to_emoji(self) -> None:
36
+ assert strip_icon_shortcodes(":material-shield:") == "🛡️"
38
37
 
39
- def test_rocket_stripped(self) -> None:
40
- # 🚀 is non-BMP (U+1F680); strip silently instead
41
- assert strip_icon_shortcodes(":material-rocket-launch:") == ""
38
+ def test_rocket_maps_to_emoji(self) -> None:
39
+ assert strip_icon_shortcodes(":material-rocket-launch:") == "🚀"
42
40
 
43
41
  def test_settings_maps_to_gear(self) -> None:
44
42
  assert strip_icon_shortcodes(":material-cog:") == "⚙"
45
43
 
46
- def test_search_stripped(self) -> None:
47
- # 🔍 is non-BMP (U+1F50D); strip silently instead
48
- assert strip_icon_shortcodes(":material-magnify:") == ""
44
+ def test_search_maps_to_emoji(self) -> None:
45
+ assert strip_icon_shortcodes(":material-magnify:") == "🔍"
49
46
 
50
47
  def test_refresh_maps_to_arrow(self) -> None:
51
48
  assert strip_icon_shortcodes(":material-refresh:") == "↻"
@@ -84,18 +81,17 @@ class TestArrowMappings:
84
81
 
85
82
 
86
83
  class TestBuggedIcons:
87
- """Regression tests for icons that previously rendered as ??? in Confluence."""
84
+ """Regression tests for icons that should not produce wrong output."""
88
85
 
89
- def test_link_variant_stripped(self) -> None:
90
- # Was: 🔗 (U+1F517, non-BMP) ?? in Confluence
91
- assert strip_icon_shortcodes(":material-link-variant:") == ""
86
+ def test_link_variant_maps_to_emoji(self) -> None:
87
+ assert strip_icon_shortcodes(":material-link-variant:") == "🔗"
92
88
 
93
89
  def test_grid_view_outline_stripped(self) -> None:
94
- # Was: 👁️ (wrong semantic + non-BMP) → ??? in Confluence
90
+ # grid/view have no meaningful emoji analogue
95
91
  assert strip_icon_shortcodes(":material-grid-view-outline:") == ""
96
92
 
97
- def test_link_stripped(self) -> None:
98
- assert strip_icon_shortcodes(":material-link:") == ""
93
+ def test_link_maps_to_emoji(self) -> None:
94
+ assert strip_icon_shortcodes(":material-link:") == "🔗"
99
95
 
100
96
  def test_view_stripped(self) -> None:
101
97
  assert strip_icon_shortcodes(":material-view-dashboard:") == ""
@@ -124,21 +120,26 @@ class TestUnknownIcons:
124
120
  assert strip_icon_shortcodes(":fontawesome-brands-github:") == ""
125
121
 
126
122
 
127
- class TestBmpSafety:
128
- """All mapped values must be BMP-only ( U+FFFF, max 3-byte UTF-8)."""
123
+ class TestEmojiMappings:
124
+ """Supplementary-plane emoji now map to real emoji (Cloud utf8mb4 support)."""
129
125
 
130
- def test_all_mapped_values_are_bmp(self) -> None:
131
- from mkdocs_to_confluence.preprocess.icons import _KEYWORD_MAP
126
+ def test_fire_maps_to_emoji(self) -> None:
127
+ assert strip_icon_shortcodes(":material-fire:") == "🔥"
132
128
 
133
- non_bmp = {
134
- kw: val
135
- for kw, val in _KEYWORD_MAP.items()
136
- if val is not None and any(ord(c) > 0xFFFF for c in val)
137
- }
138
- assert non_bmp == {}, (
139
- f"Non-BMP characters found in _KEYWORD_MAP (will render as ??? "
140
- f"in Confluence MySQL utf8): {non_bmp}"
141
- )
129
+ def test_lightbulb_maps_to_emoji(self) -> None:
130
+ assert strip_icon_shortcodes(":material-lightbulb:") == "💡"
131
+
132
+ def test_bug_maps_to_emoji(self) -> None:
133
+ assert strip_icon_shortcodes(":material-bug:") == "🐛"
134
+
135
+ def test_package_maps_to_emoji(self) -> None:
136
+ assert strip_icon_shortcodes(":material-package:") == "📦"
137
+
138
+ def test_account_maps_to_emoji(self) -> None:
139
+ assert strip_icon_shortcodes(":material-account:") == "👤"
140
+
141
+ def test_robot_maps_to_emoji(self) -> None:
142
+ assert strip_icon_shortcodes(":material-robot:") == "🤖"
142
143
 
143
144
 
144
145
  class TestInlineReplacement:
@@ -162,14 +163,14 @@ class TestInlineReplacement:
162
163
  class TestStandardEmoji:
163
164
  """Tests for bare emoji shortcodes like :rotating_light:."""
164
165
 
165
- def test_rotating_light_maps_to_warning(self) -> None:
166
- assert strip_icon_shortcodes(":rotating_light:") == ""
166
+ def test_rotating_light_maps_to_siren(self) -> None:
167
+ assert strip_icon_shortcodes(":rotating_light:") == "🚨"
167
168
 
168
169
  def test_octagonal_sign_maps_to_no_entry(self) -> None:
169
170
  assert strip_icon_shortcodes(":octagonal_sign:") == "⛔"
170
171
 
171
- def test_wrench_maps_to_gear(self) -> None:
172
- assert strip_icon_shortcodes(":wrench:") == ""
172
+ def test_wrench_maps_to_wrench_emoji(self) -> None:
173
+ assert strip_icon_shortcodes(":wrench:") == "🔧"
173
174
 
174
175
  def test_information_source_maps_to_info(self) -> None:
175
176
  assert strip_icon_shortcodes(":information_source:") == "ℹ"
@@ -180,8 +181,8 @@ class TestStandardEmoji:
180
181
  def test_x_maps_to_cross(self) -> None:
181
182
  assert strip_icon_shortcodes(":x:") == "✗"
182
183
 
183
- def test_briefcase_stripped(self) -> None:
184
- assert strip_icon_shortcodes(":briefcase:") == ""
184
+ def test_briefcase_maps_to_emoji(self) -> None:
185
+ assert strip_icon_shortcodes(":briefcase:") == "💼"
185
186
 
186
187
  def test_unknown_shortcode_unchanged(self) -> None:
187
188
  assert strip_icon_shortcodes(":unknown_emoji_xyz:") == ":unknown_emoji_xyz:"
@@ -195,11 +196,7 @@ class TestStandardEmoji:
195
196
  result = strip_icon_shortcodes(":material-check-circle:")
196
197
  assert result == "✓"
197
198
 
198
- def test_all_standard_emoji_values_are_bmp(self) -> None:
199
+ def test_all_standard_emoji_values_are_strings(self) -> None:
199
200
  from mkdocs_to_confluence.preprocess.icons import _STANDARD_EMOJI_MAP
200
201
  for name, symbol in _STANDARD_EMOJI_MAP.items():
201
- for ch in symbol:
202
- assert ord(ch) <= 0xFFFF, (
203
- f"_STANDARD_EMOJI_MAP[{name!r}] contains supplementary-plane "
204
- f"character U+{ord(ch):04X} — use BMP or empty string"
205
- )
202
+ assert isinstance(symbol, str), f"_STANDARD_EMOJI_MAP[{name!r}] is not a string"
@@ -515,6 +515,21 @@ class TestAdmonitions:
515
515
  assert isinstance(adm, Admonition)
516
516
  assert adm.collapsible is True
517
517
 
518
+ def test_question_mark_plus_is_expanded(self) -> None:
519
+ adm = first(parse("???+ note\n Body.\n"), Admonition)
520
+ assert isinstance(adm, Admonition)
521
+ assert adm.expanded is True
522
+
523
+ def test_question_mark_no_plus_is_not_expanded(self) -> None:
524
+ adm = first(parse("??? note\n Body.\n"), Admonition)
525
+ assert isinstance(adm, Admonition)
526
+ assert adm.expanded is False
527
+
528
+ def test_bang_is_not_expanded(self) -> None:
529
+ adm = first(parse("!!! note\n Body.\n"), Admonition)
530
+ assert isinstance(adm, Admonition)
531
+ assert adm.expanded is False
532
+
518
533
  def test_body_paragraph_parsed(self) -> None:
519
534
  adm = first(parse("!!! note\n Body text here.\n"), Admonition)
520
535
  assert isinstance(adm, Admonition)