mkdocs2confluence 0.14.0__tar.gz → 0.16.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 (109) hide show
  1. {mkdocs2confluence-0.14.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.16.0}/PKG-INFO +10 -1
  2. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/README.md +9 -0
  3. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/pyproject.toml +29 -1
  4. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0/src/mkdocs2confluence.egg-info}/PKG-INFO +10 -1
  5. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs2confluence.egg-info/SOURCES.txt +8 -0
  6. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/compiler/page.py +21 -1
  7. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/emitter/xhtml.py +19 -2
  8. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/ir/nodes.py +13 -0
  9. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/loader/config.py +26 -0
  10. mkdocs2confluence-0.16.0/src/mkdocs_to_confluence/preprocess/captions.py +51 -0
  11. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preprocess/frontmatter.py +16 -6
  12. mkdocs2confluence-0.16.0/src/mkdocs_to_confluence/transforms/admonition_titles.py +55 -0
  13. mkdocs2confluence-0.16.0/src/mkdocs_to_confluence/transforms/attachment_previews.py +42 -0
  14. mkdocs2confluence-0.16.0/src/mkdocs_to_confluence/transforms/captions.py +31 -0
  15. mkdocs2confluence-0.16.0/tests/test_admonition_titles.py +62 -0
  16. mkdocs2confluence-0.16.0/tests/test_attachment_previews.py +148 -0
  17. mkdocs2confluence-0.16.0/tests/test_captions.py +158 -0
  18. mkdocs2confluence-0.16.0/tests/test_exclude_properties_config.py +52 -0
  19. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_frontmatter.py +49 -0
  20. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_publish_pipeline.py +49 -0
  21. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/LICENSE +0 -0
  22. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/setup.cfg +0 -0
  23. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  24. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  25. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  26. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  27. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/__init__.py +0 -0
  28. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/cli.py +0 -0
  29. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/compiler/__init__.py +0 -0
  30. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/compiler/models.py +0 -0
  31. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  32. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  33. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/ir/document.py +0 -0
  34. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  35. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  36. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  37. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  38. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/loader/page.py +0 -0
  39. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  40. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  41. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  42. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  43. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  44. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  45. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  46. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  47. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  48. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  49. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  50. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  51. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preview/render.py +0 -0
  52. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/preview/server.py +0 -0
  53. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  54. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/publisher/changelog.py +0 -0
  55. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  56. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/publisher/executor.py +0 -0
  57. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/publisher/http_retry.py +0 -0
  58. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/publisher/models.py +0 -0
  59. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/publisher/pipeline.py +0 -0
  60. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/publisher/planner.py +0 -0
  61. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/skill_installer.py +0 -0
  62. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/skills/mkdocs-changelog/SKILL.md +0 -0
  63. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/skills/mkdocs-changelog/scripts/changelog_data.py +0 -0
  64. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
  65. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
  66. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/sync/command.py +0 -0
  67. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/sync/comments.py +0 -0
  68. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/sync/github.py +0 -0
  69. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/sync/platform.py +0 -0
  70. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/sync/state.py +0 -0
  71. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  72. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  73. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  74. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  75. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
  76. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  77. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  78. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  79. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_abbrevs.py +0 -0
  80. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_changelog_config.py +0 -0
  81. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_changelog_publish.py +0 -0
  82. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_children_macro.py +0 -0
  83. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_cli.py +0 -0
  84. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_editlink.py +0 -0
  85. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_emitter.py +0 -0
  86. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_extra_css.py +0 -0
  87. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_footer.py +0 -0
  88. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_icons.py +0 -0
  89. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_images.py +0 -0
  90. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_internallinks.py +0 -0
  91. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_ir.py +0 -0
  92. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_linkdefs.py +0 -0
  93. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_loader.py +0 -0
  94. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_mermaid.py +0 -0
  95. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_page_loader.py +0 -0
  96. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_parser.py +0 -0
  97. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_pdf.py +0 -0
  98. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_preprocess.py +0 -0
  99. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_preview.py +0 -0
  100. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_publish_client.py +0 -0
  101. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_publish_config.py +0 -0
  102. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_server.py +0 -0
  103. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_skill_installer.py +0 -0
  104. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_sync_anchoring.py +0 -0
  105. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_sync_command.py +0 -0
  106. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_sync_comments.py +0 -0
  107. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_sync_github.py +0 -0
  108. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_sync_state.py +0 -0
  109. {mkdocs2confluence-0.14.0 → mkdocs2confluence-0.16.0}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.14.0
3
+ Version: 0.16.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
@@ -53,6 +53,7 @@ Dynamic: license-file
53
53
  [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
54
54
  [![mypy](https://img.shields.io/badge/type--checked-mypy-blue.svg)](https://mypy-lang.org/)
55
55
  [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
56
+ [![imports: import-linter](https://img.shields.io/badge/imports-linted-blue.svg)](https://github.com/seddonym/import-linter)
56
57
  [![SLSA Level 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)
57
58
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/jeckyl2010/mkdocs2confluence/badge)](https://securityscorecards.dev/viewer/?uri=github.com/jeckyl2010/mkdocs2confluence)
58
59
 
@@ -159,9 +160,17 @@ confluence:
159
160
  parent_page_id: "123456" # optional root page
160
161
  mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
161
162
  full_width: true # default: true
163
+ attachment_preview: false # default: false — inline PDF/Office previews
162
164
  changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
165
+ exclude_properties: # optional: front matter keys to hide from Page Properties
166
+ - source_documents
167
+ - internal_ref
163
168
  ```
164
169
 
170
+ `exclude_properties` lists raw front matter keys to omit from the Page Properties table (e.g. internal tooling fields). Matching is exact and case-sensitive; `title`, `tags`, and `status` keep their special behavior even if excluded from the table.
171
+
172
+ `attachment_preview` (bool, default `false`) renders links to local PDF/Office files (`.pdf`, `.doc(x)`, `.xls(x)`, `.ppt(x)`) as inline `view-file` previews instead of download links. Other file types always remain download links.
173
+
165
174
  The `confluence:` block is also accepted under `extra:` for MkDocs strict-mode compatibility. The API token is read from `token:` in `mkdocs.yml`, then `CONFLUENCE_API_TOKEN`, then `MK2CONF_TOKEN`.
166
175
 
167
176
  ### Changelog / What's New page
@@ -11,6 +11,7 @@
11
11
  [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
12
12
  [![mypy](https://img.shields.io/badge/type--checked-mypy-blue.svg)](https://mypy-lang.org/)
13
13
  [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
14
+ [![imports: import-linter](https://img.shields.io/badge/imports-linted-blue.svg)](https://github.com/seddonym/import-linter)
14
15
  [![SLSA Level 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)
15
16
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/jeckyl2010/mkdocs2confluence/badge)](https://securityscorecards.dev/viewer/?uri=github.com/jeckyl2010/mkdocs2confluence)
16
17
 
@@ -117,9 +118,17 @@ confluence:
117
118
  parent_page_id: "123456" # optional root page
118
119
  mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
119
120
  full_width: true # default: true
121
+ attachment_preview: false # default: false — inline PDF/Office previews
120
122
  changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
123
+ exclude_properties: # optional: front matter keys to hide from Page Properties
124
+ - source_documents
125
+ - internal_ref
121
126
  ```
122
127
 
128
+ `exclude_properties` lists raw front matter keys to omit from the Page Properties table (e.g. internal tooling fields). Matching is exact and case-sensitive; `title`, `tags`, and `status` keep their special behavior even if excluded from the table.
129
+
130
+ `attachment_preview` (bool, default `false`) renders links to local PDF/Office files (`.pdf`, `.doc(x)`, `.xls(x)`, `.ppt(x)`) as inline `view-file` previews instead of download links. Other file types always remain download links.
131
+
123
132
  The `confluence:` block is also accepted under `extra:` for MkDocs strict-mode compatibility. The API token is read from `token:` in `mkdocs.yml`, then `CONFLUENCE_API_TOKEN`, then `MK2CONF_TOKEN`.
124
133
 
125
134
  ### Changelog / What's New page
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.14.0"
3
+ version = "0.16.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" }
@@ -103,8 +103,36 @@ exclude_lines = [
103
103
  "raise NotImplementedError",
104
104
  ]
105
105
 
106
+ # Architecture boundaries — enforces the compile pipeline stage separation
107
+ # documented in .github/copilot-instructions.md. Higher layers may import lower
108
+ # ones; siblings joined by "|" must stay independent of each other.
109
+ [tool.importlinter]
110
+ root_package = "mkdocs_to_confluence"
111
+
112
+ [[tool.importlinter.contracts]]
113
+ name = "Compiler pipeline stage layering"
114
+ type = "layers"
115
+ containers = ["mkdocs_to_confluence"]
116
+ layers = [
117
+ "sync",
118
+ "publisher",
119
+ "compiler",
120
+ "preprocess | parser | transforms | emitter",
121
+ "ir | loader",
122
+ ]
123
+
124
+ [[tool.importlinter.contracts]]
125
+ name = "Renderer layering (preview/pdf)"
126
+ type = "layers"
127
+ containers = ["mkdocs_to_confluence"]
128
+ layers = [
129
+ "pdf",
130
+ "preview",
131
+ ]
132
+
106
133
  [dependency-groups]
107
134
  dev = [
135
+ "import-linter>=2.11",
108
136
  "pip-audit>=2.10.0",
109
137
  "radon>=6.0.1",
110
138
  "vulture>=2.16",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.14.0
3
+ Version: 0.16.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
@@ -53,6 +53,7 @@ Dynamic: license-file
53
53
  [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
54
54
  [![mypy](https://img.shields.io/badge/type--checked-mypy-blue.svg)](https://mypy-lang.org/)
55
55
  [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
56
+ [![imports: import-linter](https://img.shields.io/badge/imports-linted-blue.svg)](https://github.com/seddonym/import-linter)
56
57
  [![SLSA Level 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)
57
58
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/jeckyl2010/mkdocs2confluence/badge)](https://securityscorecards.dev/viewer/?uri=github.com/jeckyl2010/mkdocs2confluence)
58
59
 
@@ -159,9 +160,17 @@ confluence:
159
160
  parent_page_id: "123456" # optional root page
160
161
  mermaid_render: kroki # "kroki" (default) | "kroki:https://your-kroki" | "none"
161
162
  full_width: true # default: true
163
+ attachment_preview: false # default: false — inline PDF/Office previews
162
164
  changelog: CHANGELOG.md # optional: publish as a top-level "What's New" page
165
+ exclude_properties: # optional: front matter keys to hide from Page Properties
166
+ - source_documents
167
+ - internal_ref
163
168
  ```
164
169
 
170
+ `exclude_properties` lists raw front matter keys to omit from the Page Properties table (e.g. internal tooling fields). Matching is exact and case-sensitive; `title`, `tags`, and `status` keep their special behavior even if excluded from the table.
171
+
172
+ `attachment_preview` (bool, default `false`) renders links to local PDF/Office files (`.pdf`, `.doc(x)`, `.xls(x)`, `.ppt(x)`) as inline `view-file` previews instead of download links. Other file types always remain download links.
173
+
165
174
  The `confluence:` block is also accepted under `extra:` for MkDocs strict-mode compatibility. The API token is read from `token:` in `mkdocs.yml`, then `CONFLUENCE_API_TOKEN`, then `MK2CONF_TOKEN`.
166
175
 
167
176
  ### Changelog / What's New page
@@ -31,6 +31,7 @@ src/mkdocs_to_confluence/pdf/generator.py
31
31
  src/mkdocs_to_confluence/pdf/render.py
32
32
  src/mkdocs_to_confluence/preprocess/__init__.py
33
33
  src/mkdocs_to_confluence/preprocess/abbrevs.py
34
+ src/mkdocs_to_confluence/preprocess/captions.py
34
35
  src/mkdocs_to_confluence/preprocess/fence.py
35
36
  src/mkdocs_to_confluence/preprocess/frontmatter.py
36
37
  src/mkdocs_to_confluence/preprocess/icons.py
@@ -58,19 +59,26 @@ src/mkdocs_to_confluence/sync/platform.py
58
59
  src/mkdocs_to_confluence/sync/state.py
59
60
  src/mkdocs_to_confluence/transforms/__init__.py
60
61
  src/mkdocs_to_confluence/transforms/abbrevs.py
62
+ src/mkdocs_to_confluence/transforms/admonition_titles.py
61
63
  src/mkdocs_to_confluence/transforms/assets.py
64
+ src/mkdocs_to_confluence/transforms/attachment_previews.py
65
+ src/mkdocs_to_confluence/transforms/captions.py
62
66
  src/mkdocs_to_confluence/transforms/editlink.py
63
67
  src/mkdocs_to_confluence/transforms/footer.py
64
68
  src/mkdocs_to_confluence/transforms/images.py
65
69
  src/mkdocs_to_confluence/transforms/internallinks.py
66
70
  src/mkdocs_to_confluence/transforms/mermaid.py
67
71
  tests/test_abbrevs.py
72
+ tests/test_admonition_titles.py
73
+ tests/test_attachment_previews.py
74
+ tests/test_captions.py
68
75
  tests/test_changelog_config.py
69
76
  tests/test_changelog_publish.py
70
77
  tests/test_children_macro.py
71
78
  tests/test_cli.py
72
79
  tests/test_editlink.py
73
80
  tests/test_emitter.py
81
+ tests/test_exclude_properties_config.py
74
82
  tests/test_extra_css.py
75
83
  tests/test_footer.py
76
84
  tests/test_frontmatter.py
@@ -13,6 +13,7 @@ from mkdocs_to_confluence.preprocess.abbrevs import (
13
13
  extract_abbreviations,
14
14
  strip_abbreviation_defs,
15
15
  )
16
+ from mkdocs_to_confluence.preprocess.captions import rewrite_figure_captions
16
17
  from mkdocs_to_confluence.preprocess.frontmatter import extract_front_matter
17
18
  from mkdocs_to_confluence.preprocess.icons import strip_icon_shortcodes
18
19
  from mkdocs_to_confluence.preprocess.includes import (
@@ -26,7 +27,14 @@ from mkdocs_to_confluence.preprocess.linkdefs import (
26
27
  strip_link_defs,
27
28
  )
28
29
  from mkdocs_to_confluence.transforms.abbrevs import apply_abbreviations
30
+ from mkdocs_to_confluence.transforms.admonition_titles import (
31
+ strip_links_in_admonition_titles,
32
+ )
29
33
  from mkdocs_to_confluence.transforms.assets import resolve_local_assets
34
+ from mkdocs_to_confluence.transforms.attachment_previews import (
35
+ resolve_attachment_previews,
36
+ )
37
+ from mkdocs_to_confluence.transforms.captions import resolve_captions
30
38
  from mkdocs_to_confluence.transforms.editlink import attach_source_url
31
39
  from mkdocs_to_confluence.transforms.footer import build_source_footer
32
40
  from mkdocs_to_confluence.transforms.internallinks import resolve_internal_links
@@ -53,15 +61,22 @@ def compile_page(
53
61
  docs_dir=config.docs_dir,
54
62
  )
55
63
  preprocessed = strip_unsupported_html(preprocessed)
64
+ preprocessed = rewrite_figure_captions(preprocessed)
56
65
  preprocessed = strip_html_comments(preprocessed)
57
66
  preprocessed = strip_icon_shortcodes(preprocessed)
58
- front_matter, preprocessed = extract_front_matter(preprocessed)
67
+ exclude_properties = (
68
+ config.confluence.exclude_properties if config.confluence else ()
69
+ )
70
+ front_matter, preprocessed = extract_front_matter(
71
+ preprocessed, exclude_properties=exclude_properties
72
+ )
59
73
  abbrevs = extract_abbreviations(preprocessed)
60
74
  preprocessed = strip_abbreviation_defs(preprocessed)
61
75
  link_defs = collect_link_defs(preprocessed)
62
76
  preprocessed = expand_link_refs(preprocessed, link_defs)
63
77
  preprocessed = strip_link_defs(preprocessed)
64
78
  ir_nodes = parse(preprocessed)
79
+ ir_nodes = strip_links_in_admonition_titles(ir_nodes, node.docs_path or "")
65
80
  if is_section_index:
66
81
  ir_nodes = ir_nodes + (ChildrenMacro(),)
67
82
  ir_nodes = apply_abbreviations(ir_nodes, abbrevs, page_text=preprocessed)
@@ -70,6 +85,11 @@ def compile_page(
70
85
  page_path=node.source_path,
71
86
  docs_dir=config.docs_dir,
72
87
  )
88
+ ir_nodes = resolve_captions(ir_nodes)
89
+ attachment_preview = (
90
+ config.confluence.attachment_preview if config.confluence else False
91
+ )
92
+ ir_nodes = resolve_attachment_previews(ir_nodes, enabled=attachment_preview)
73
93
  mermaid_render = config.confluence.mermaid_render if config.confluence else "kroki"
74
94
  if mermaid_render != "none":
75
95
  kroki_url = (
@@ -25,6 +25,7 @@ from mkdocs_to_confluence.ir.nodes import (
25
25
  AbbrevGlossaryBlock,
26
26
  Admonition,
27
27
  AnchorNode,
28
+ AttachmentPreview,
28
29
  BlockQuote,
29
30
  BoldNode,
30
31
  BulletList,
@@ -685,6 +686,8 @@ def _emit_inline(node: IRNode) -> str:
685
686
  return f"<code{style_attr}>{html.escape(node.code)}</code>"
686
687
  if isinstance(node, LinkNode):
687
688
  return _emit_link(node)
689
+ if isinstance(node, AttachmentPreview):
690
+ return _emit_attachment_preview(node)
688
691
  if isinstance(node, ImageNode):
689
692
  return _emit_image(node)
690
693
  if isinstance(node, FootnoteRef):
@@ -709,6 +712,15 @@ def _emit_inline(node: IRNode) -> str:
709
712
  return html.escape(repr(node))
710
713
 
711
714
 
715
+ def _emit_attachment_preview(node: AttachmentPreview) -> str:
716
+ filename = html.escape(node.filename, quote=True)
717
+ return (
718
+ '<ac:structured-macro ac:name="view-file">'
719
+ f'<ac:parameter ac:name="name"><ri:attachment ri:filename="{filename}"/></ac:parameter>'
720
+ "</ac:structured-macro>"
721
+ )
722
+
723
+
712
724
  def _emit_link(node: LinkNode) -> str:
713
725
  # Same-page anchor link: [text](#id) → ac:link with only ac:anchor.
714
726
  # Confluence strips a raw <a href="#id">, so it must use the anchor form.
@@ -758,14 +770,19 @@ def _emit_image(node: ImageNode) -> str:
758
770
  height_attr = f' ac:height="{node.height}"' if node.height is not None else ""
759
771
  align_attr = f' ac:align="{html.escape(node.align)}"' if node.align else ""
760
772
  size_attrs = width_attr + height_attr + align_attr
773
+ caption = (
774
+ f"<ac:caption><p>{html.escape(node.caption)}</p></ac:caption>"
775
+ if node.caption
776
+ else ""
777
+ )
761
778
  # Local file → attachment reference; URL → external ri:url
762
779
  src = node.src
763
780
  if src.startswith(("http://", "https://", "//", "data:")):
764
781
  ref = f'<ri:url ri:value="{html.escape(src)}"/>'
765
- return f"<ac:image{alt_attr}{title_attr}{size_attrs}>{ref}</ac:image>"
782
+ return f"<ac:image{alt_attr}{title_attr}{size_attrs}>{ref}{caption}</ac:image>"
766
783
  else:
767
784
  filename = html.escape(node.attachment_name or Path(src).name)
768
785
  # data-local-path is used by the preview renderer only (not valid XHTML)
769
786
  local_attr = f' data-local-path="{html.escape(src)}"'
770
787
  ref = f'<ri:attachment ri:filename="{filename}"/>'
771
- return f"<ac:image{alt_attr}{title_attr}{size_attrs}{local_attr}>{ref}</ac:image>"
788
+ return f"<ac:image{alt_attr}{title_attr}{size_attrs}{local_attr}>{ref}{caption}</ac:image>"
@@ -162,6 +162,7 @@ class ImageNode(IRNode):
162
162
  src: str
163
163
  alt: str
164
164
  title: str | None = None
165
+ caption: str | None = None
165
166
  attachment_name: str | None = None
166
167
  width: int | None = None
167
168
  height: int | None = None
@@ -452,6 +453,18 @@ class ChildrenMacro(IRNode):
452
453
  """
453
454
 
454
455
 
456
+ @dataclass(frozen=True)
457
+ class AttachmentPreview(IRNode):
458
+ """An inline preview of an uploaded attachment (PDF/Office file).
459
+
460
+ ``filename`` is the collision-safe Confluence attachment name (the same
461
+ value carried on the originating ``LinkNode.attachment_name``). The emitter
462
+ renders a ``view-file`` macro referencing the attachment.
463
+ """
464
+
465
+ filename: str
466
+
467
+
455
468
  @dataclass(frozen=True)
456
469
  class SourceFooter(IRNode):
457
470
  """Footer panel showing source-control links and last-commit info.
@@ -35,6 +35,8 @@ class ConfluenceConfig:
35
35
  github_base_branch: str = "main" # base branch for review PRs
36
36
  allow_any_host: bool = False # set True to allow non-Atlassian Cloud base_url hosts
37
37
  changelog_file: str | None = None # path relative to docs_dir; None means disabled
38
+ exclude_properties: tuple[str, ...] = () # front matter keys to omit from Page Properties table
39
+ attachment_preview: bool = False # render PDF/Office attachment links as view-file macros
38
40
 
39
41
 
40
42
  @dataclass(frozen=True)
@@ -276,6 +278,28 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
276
278
  )
277
279
  changelog_file = cl_str
278
280
 
281
+ # exclude_properties (optional) — raw front matter keys to omit from
282
+ # the Page Properties table. Pure list of literal keys, no wildcards.
283
+ raw_exclude = raw_conf.get("exclude_properties")
284
+ if raw_exclude is None:
285
+ exclude_properties: tuple[str, ...] = ()
286
+ elif isinstance(raw_exclude, list):
287
+ exclude_properties = tuple(str(k) for k in raw_exclude)
288
+ else:
289
+ raise ConfigError(
290
+ "mkdocs.yml: 'confluence.exclude_properties' must be a list of "
291
+ f"front matter keys, got {type(raw_exclude).__name__}."
292
+ )
293
+
294
+ # attachment_preview (optional) — render PDF/Office attachment links inline
295
+ raw_preview = raw_conf.get("attachment_preview", False)
296
+ if not isinstance(raw_preview, bool):
297
+ raise ConfigError(
298
+ "mkdocs.yml: 'confluence.attachment_preview' must be a boolean, "
299
+ f"got {type(raw_preview).__name__}."
300
+ )
301
+ attachment_preview = raw_preview
302
+
279
303
  confluence = ConfluenceConfig(
280
304
  base_url=base_url.rstrip("/"),
281
305
  space_key=space_key,
@@ -291,6 +315,8 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
291
315
  github_base_branch=str(raw_conf.get("github_base_branch", "main")),
292
316
  allow_any_host=allow_any_host,
293
317
  changelog_file=changelog_file,
318
+ exclude_properties=exclude_properties,
319
+ attachment_preview=attachment_preview,
294
320
  )
295
321
 
296
322
  # --- extra_css (optional) ---
@@ -0,0 +1,51 @@
1
+ """Figure/figcaption preprocess rewrite.
2
+
3
+ Material for MkDocs authors captions with the ``md_in_html`` figure form::
4
+
5
+ <figure markdown="span">
6
+ ![alt](img.png)
7
+ <figcaption>The caption</figcaption>
8
+ </figure>
9
+
10
+ Confluence storage format has no ``<figure>`` element, so this pass rewrites
11
+ such a block into a single titled Markdown image::
12
+
13
+ ![alt](img.png "The caption")
14
+
15
+ The image then flows through the normal parser and the ``resolve_captions``
16
+ transform promotes the title to an ``ac:caption``. The figcaption text always
17
+ wins over any pre-existing image title (it is substituted into the title slot).
18
+
19
+ Limitations (graceful — non-matching blocks are left untouched):
20
+
21
+ * The figcaption is treated as plain text (``[^<]*``); inline HTML such as
22
+ ``<em>`` inside a figcaption is not supported and the block is skipped.
23
+ * An image with a trailing attr-list (``![a](x.png){ width=300 }``) is skipped.
24
+ * ``src`` must be space-free (percent-encode spaces), per Markdown convention.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import re
30
+
31
+ # The caption group is plain-text only (``[^<]*``) so it cannot run past a
32
+ # missing ``</figcaption>`` and swallow a following ``<figure>`` block.
33
+ _FIGURE_RE = re.compile(
34
+ r"<figure\b[^>]*>\s*"
35
+ r"!\[(?P<alt>[^\]]*)\]\((?P<src>[^\s)]+)(?:\s+\"[^\"]*\")?\)\s*"
36
+ r"<figcaption>(?P<cap>[^<]*)</figcaption>\s*"
37
+ r"</figure>",
38
+ re.IGNORECASE,
39
+ )
40
+
41
+
42
+ def rewrite_figure_captions(text: str) -> str:
43
+ """Rewrite ``<figure>…<figcaption>…</figure>`` blocks to titled images."""
44
+
45
+ def _sub(m: re.Match[str]) -> str:
46
+ alt = m.group("alt")
47
+ src = m.group("src")
48
+ cap = m.group("cap").strip().replace('"', "'")
49
+ return f'![{alt}]({src} "{cap}")'
50
+
51
+ return _FIGURE_RE.sub(_sub, text)
@@ -80,11 +80,16 @@ _FRONT_MATTER_RE = re.compile(r"\A---\s*\n(.*?\n?)---\s*\n?", re.DOTALL)
80
80
  # ── Public API ────────────────────────────────────────────────────────────────
81
81
 
82
82
 
83
- def extract_front_matter(text: str) -> tuple[FrontMatter | None, str]:
83
+ def extract_front_matter(
84
+ text: str, exclude_properties: tuple[str, ...] = ()
85
+ ) -> tuple[FrontMatter | None, str]:
84
86
  """Parse YAML front matter from the top of *text*.
85
87
 
86
88
  Args:
87
89
  text: Raw markdown content.
90
+ exclude_properties: Raw front matter keys to omit from the Page
91
+ Properties table. Matching is exact and case-sensitive. Special
92
+ behaviors (title, tags->labels, status) are unaffected.
88
93
 
89
94
  Returns:
90
95
  A ``(FrontMatter | None, remaining_text)`` tuple. ``FrontMatter`` is
@@ -105,14 +110,19 @@ def extract_front_matter(text: str) -> tuple[FrontMatter | None, str]:
105
110
  if not isinstance(raw, dict):
106
111
  return None, text
107
112
 
108
- return _build_node(raw), remaining
113
+ return _build_node(raw, exclude_properties), remaining
109
114
 
110
115
 
111
116
  # ── Private helpers ───────────────────────────────────────────────────────────
112
117
 
113
118
 
114
- def _build_node(raw: dict[str, Any]) -> FrontMatter:
115
- """Convert a raw front matter dict to a :class:`FrontMatter` IR node."""
119
+ def _build_node(raw: dict[str, Any], exclude: tuple[str, ...] = ()) -> FrontMatter:
120
+ """Convert a raw front matter dict to a :class:`FrontMatter` IR node.
121
+
122
+ ``exclude`` lists raw keys to omit from the properties table (table rows
123
+ only — title/labels/status side-effects are computed independently below).
124
+ """
125
+ skip = _STRIP_FIELDS | set(exclude)
116
126
  title: str | None = _stringify(raw.get("title")) if "title" in raw else None
117
127
  subtitle: str | None = _stringify(raw.get("subtitle")) if "subtitle" in raw else None
118
128
 
@@ -129,14 +139,14 @@ def _build_node(raw: dict[str, Any]) -> FrontMatter:
129
139
  seen: set[str] = set()
130
140
 
131
141
  for key in _FIELD_ORDER:
132
- if key in raw and key not in _STRIP_FIELDS and key != "subtitle":
142
+ if key in raw and key not in skip and key != "subtitle":
133
143
  display = _DISPLAY_NAMES.get(key, _humanize(key))
134
144
  properties.append((display, _format_value(key, raw[key])))
135
145
  seen.add(key)
136
146
 
137
147
  # Append any remaining unknown fields in document order.
138
148
  for key, value in raw.items():
139
- if key in seen or key in _STRIP_FIELDS or key == "subtitle":
149
+ if key in seen or key in skip or key == "subtitle":
140
150
  continue
141
151
  display = _DISPLAY_NAMES.get(key, _humanize(key))
142
152
  properties.append((display, _format_value(key, value)))
@@ -0,0 +1,55 @@
1
+ """Degrade Markdown links inside admonition titles.
2
+
3
+ A Confluence macro title is an ``<ac:parameter>`` and holds plain text only —
4
+ it cannot contain ``<ac:link>`` or any markup. A Markdown link written in an
5
+ admonition title therefore cannot render. This transform replaces each inline
6
+ link ``[text](target)`` in an ``Admonition`` title with its ``text`` and warns
7
+ to stderr so the author can adjust the source.
8
+
9
+ Images (``![alt](src)``) are deliberately left untouched (negative lookbehind on
10
+ ``!``). Reference-style links and autolinks are out of scope.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import dataclasses
16
+ import re
17
+ import sys
18
+
19
+ from mkdocs_to_confluence.ir.nodes import Admonition, IRNode, walk
20
+ from mkdocs_to_confluence.ir.treeutil import replace_nodes
21
+
22
+ # Inline Markdown link [text](target), but not an image (no leading '!').
23
+ _LINK_RE = re.compile(r"(?<!!)\[([^\]]+)\]\(([^)]+)\)")
24
+
25
+
26
+ def strip_links_in_admonition_titles(
27
+ nodes: tuple[IRNode, ...], page_path: str
28
+ ) -> tuple[IRNode, ...]:
29
+ """Replace inline links in admonition titles with their link text.
30
+
31
+ Emits a transpiler warning to stderr for each affected title. Returns the
32
+ original *nodes* unchanged when no title contained a link.
33
+ """
34
+ replacements: dict[int, IRNode] = {}
35
+
36
+ for top in nodes:
37
+ for node in walk(top):
38
+ if not isinstance(node, Admonition) or node.title is None:
39
+ continue
40
+ new_title = _LINK_RE.sub(r"\1", node.title)
41
+ if new_title == node.title:
42
+ continue
43
+ _warn(
44
+ "link in admonition title not supported by Confluence "
45
+ f'(using link text): "{node.title}" in {page_path}'
46
+ )
47
+ replacements[id(node)] = dataclasses.replace(node, title=new_title)
48
+
49
+ if not replacements:
50
+ return nodes
51
+ return replace_nodes(nodes, replacements)
52
+
53
+
54
+ def _warn(msg: str) -> None:
55
+ print(f" warning {msg}", file=sys.stderr)
@@ -0,0 +1,42 @@
1
+ """Attachment inline preview transform.
2
+
3
+ When enabled, replaces ``LinkNode``s that point at an uploaded PDF or Office
4
+ attachment (``attachment_name`` set by ``resolve_local_assets``) with an
5
+ :class:`AttachmentPreview` node, which the emitter renders as a Confluence
6
+ ``view-file`` macro. Links to non-previewable file types, external URLs, and
7
+ internal pages are left unchanged.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from mkdocs_to_confluence.ir.nodes import AttachmentPreview, IRNode, LinkNode, walk
13
+ from mkdocs_to_confluence.ir.treeutil import replace_nodes
14
+
15
+ PREVIEWABLE_EXTENSIONS: frozenset[str] = frozenset(
16
+ {".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"}
17
+ )
18
+
19
+
20
+ def resolve_attachment_previews(
21
+ nodes: tuple[IRNode, ...], *, enabled: bool
22
+ ) -> tuple[IRNode, ...]:
23
+ """Swap eligible attachment links for ``AttachmentPreview`` nodes."""
24
+ if not enabled:
25
+ return nodes
26
+ replacements: dict[int, IRNode] = {}
27
+ for top_node in nodes:
28
+ for node in walk(top_node):
29
+ if not isinstance(node, LinkNode) or node.attachment_name is None:
30
+ continue
31
+ ext = _extension(node.attachment_name)
32
+ if ext not in PREVIEWABLE_EXTENSIONS:
33
+ continue
34
+ replacements[id(node)] = AttachmentPreview(filename=node.attachment_name)
35
+ if not replacements:
36
+ return nodes
37
+ return replace_nodes(nodes, replacements)
38
+
39
+
40
+ def _extension(name: str) -> str:
41
+ dot = name.rfind(".")
42
+ return name[dot:].lower() if dot != -1 else ""
@@ -0,0 +1,31 @@
1
+ """Caption resolution transform.
2
+
3
+ Fills :attr:`ImageNode.caption` from the image ``title`` attribute when no
4
+ caption is already present, and clears ``title`` so the same text is not also
5
+ emitted as a hover tooltip. Images whose caption is already set (e.g. from a
6
+ ``<figcaption>`` rewrite) are left untouched, so figcaptions take precedence.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+
13
+ from mkdocs_to_confluence.ir.nodes import ImageNode, IRNode, walk
14
+ from mkdocs_to_confluence.ir.treeutil import replace_nodes
15
+
16
+
17
+ def resolve_captions(nodes: tuple[IRNode, ...]) -> tuple[IRNode, ...]:
18
+ """Promote image ``title`` to ``caption`` where no caption exists yet."""
19
+ replacements: dict[int, IRNode] = {}
20
+ for top_node in nodes:
21
+ for node in walk(top_node):
22
+ if not isinstance(node, ImageNode):
23
+ continue
24
+ if node.caption is not None or node.title is None:
25
+ continue
26
+ replacements[id(node)] = dataclasses.replace(
27
+ node, caption=node.title, title=None
28
+ )
29
+ if not replacements:
30
+ return nodes
31
+ return replace_nodes(nodes, replacements)