mkdocs2confluence 0.5.28__tar.gz → 0.6.1__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 (67) hide show
  1. {mkdocs2confluence-0.5.28/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.6.1}/PKG-INFO +18 -3
  2. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/README.md +17 -2
  3. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/pyproject.toml +9 -2
  4. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1/src/mkdocs2confluence.egg-info}/PKG-INFO +18 -3
  5. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/cli.py +13 -0
  6. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/config.py +1 -1
  7. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/nav.py +1 -3
  8. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/includes.py +1 -1
  9. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/publisher/client.py +64 -0
  10. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/publisher/pipeline.py +225 -131
  11. mkdocs2confluence-0.6.1/tests/test_images.py +320 -0
  12. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_preprocess.py +11 -1
  13. mkdocs2confluence-0.6.1/tests/test_preview.py +326 -0
  14. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_publish_client.py +237 -0
  15. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_publish_config.py +152 -0
  16. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_publish_pipeline.py +887 -0
  17. mkdocs2confluence-0.5.28/tests/test_images.py +0 -160
  18. mkdocs2confluence-0.5.28/tests/test_preview.py +0 -133
  19. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/LICENSE +0 -0
  20. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/setup.cfg +0 -0
  21. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
  22. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  23. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  24. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  25. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  26. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/__init__.py +0 -0
  27. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  28. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  29. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  30. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/ir/document.py +0 -0
  31. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  32. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  33. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  34. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  35. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/page.py +0 -0
  36. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  37. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  38. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  39. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  40. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  41. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  42. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  43. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  44. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  45. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preview/render.py +0 -0
  46. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  47. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  48. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  49. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  50. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  51. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  52. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  53. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  54. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_abbrevs.py +0 -0
  55. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_editlink.py +0 -0
  56. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_emitter.py +0 -0
  57. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_extra_css.py +0 -0
  58. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_frontmatter.py +0 -0
  59. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_icons.py +0 -0
  60. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_internallinks.py +0 -0
  61. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_ir.py +0 -0
  62. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_linkdefs.py +0 -0
  63. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_loader.py +0 -0
  64. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_mermaid.py +0 -0
  65. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_page_loader.py +0 -0
  66. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_parser.py +0 -0
  67. {mkdocs2confluence-0.5.28 → mkdocs2confluence-0.6.1}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.5.28
3
+ Version: 0.6.1
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
@@ -41,6 +41,7 @@ Dynamic: license-file
41
41
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
42
42
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
43
43
  [![PyPI](https://img.shields.io/pypi/v/mkdocs2confluence)](https://pypi.org/project/mkdocs2confluence/)
44
+ [![Downloads](https://img.shields.io/pypi/dm/mkdocs2confluence)](https://pypi.org/project/mkdocs2confluence/)
44
45
  [![Latest Release](https://img.shields.io/github/v/release/jeckyl2010/mkdocs2confluence)](https://github.com/jeckyl2010/mkdocs2confluence/releases/latest)
45
46
  [![CI](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml/badge.svg)](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml)
46
47
  [![Release](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml/badge.svg)](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml)
@@ -133,7 +134,7 @@ The `--html` flag renders Confluence macros as visual HTML panels so you can rev
133
134
  Compile all pages listed in `nav:` and publish them to Confluence Cloud.
134
135
 
135
136
  ```
136
- mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--report FILE]
137
+ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--report FILE] [--prune]
137
138
  ```
138
139
 
139
140
  | Flag | Default | Description |
@@ -143,6 +144,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
143
144
  | `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
144
145
  | `--dry-run` | off | Print the publish plan without making any API calls |
145
146
  | `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
147
+ | `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
146
148
 
147
149
  #### Configuration
148
150
 
@@ -171,6 +173,19 @@ The API token is read from (in priority order):
171
173
  - Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
172
174
  - All locally linked assets are uploaded as Confluence page attachments automatically.
173
175
  - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification.
176
+ - **Orphan pruning** — every page created by mk2conf is stamped with a hidden `mk2conf-managed` property. Pass `--prune` to automatically delete managed pages that have been removed from `nav:`. Manually-created Confluence pages are never deleted.
177
+
178
+ #### Orphan pruning
179
+
180
+ ```bash
181
+ mk2conf publish --prune
182
+ ```
183
+
184
+ After each publish run, `--prune` fetches all descendants of the configured `parent_page_id`, diffs them against the pages just published, and deletes any managed orphans — i.e. pages that carry the `mk2conf-managed` stamp but are no longer in the MkDocs nav.
185
+
186
+ > **Safety:** Only pages stamped by mk2conf are eligible for deletion. Any page created directly in Confluence will never be touched, even if it sits inside the managed hierarchy.
187
+ >
188
+ > **Partial runs:** `--prune` is silently ignored when `--page` or `--section` is used, because a partial publish would only cover a subset of the nav and would incorrectly identify out-of-scope pages as orphans.
174
189
 
175
190
  #### Mermaid rendering
176
191
 
@@ -321,7 +336,7 @@ Any unrecognised block is preserved as a visible `warning` macro — no content
321
336
 
322
337
  ## Roadmap
323
338
 
324
- - [ ] **Delete orphaned pages** — detect and remove Confluence pages that were previously published but have since been removed from `nav:`.
339
+ - [x] **Delete orphaned pages** — `--prune` detects and removes managed Confluence pages that have been removed from `nav:`. Manually-created pages are never deleted.
325
340
 
326
341
  ---
327
342
 
@@ -3,6 +3,7 @@
3
3
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
4
4
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
5
5
  [![PyPI](https://img.shields.io/pypi/v/mkdocs2confluence)](https://pypi.org/project/mkdocs2confluence/)
6
+ [![Downloads](https://img.shields.io/pypi/dm/mkdocs2confluence)](https://pypi.org/project/mkdocs2confluence/)
6
7
  [![Latest Release](https://img.shields.io/github/v/release/jeckyl2010/mkdocs2confluence)](https://github.com/jeckyl2010/mkdocs2confluence/releases/latest)
7
8
  [![CI](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml/badge.svg)](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml)
8
9
  [![Release](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml/badge.svg)](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml)
@@ -95,7 +96,7 @@ The `--html` flag renders Confluence macros as visual HTML panels so you can rev
95
96
  Compile all pages listed in `nav:` and publish them to Confluence Cloud.
96
97
 
97
98
  ```
98
- mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--report FILE]
99
+ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--report FILE] [--prune]
99
100
  ```
100
101
 
101
102
  | Flag | Default | Description |
@@ -105,6 +106,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
105
106
  | `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
106
107
  | `--dry-run` | off | Print the publish plan without making any API calls |
107
108
  | `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
109
+ | `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
108
110
 
109
111
  #### Configuration
110
112
 
@@ -133,6 +135,19 @@ The API token is read from (in priority order):
133
135
  - Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
134
136
  - All locally linked assets are uploaded as Confluence page attachments automatically.
135
137
  - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification.
138
+ - **Orphan pruning** — every page created by mk2conf is stamped with a hidden `mk2conf-managed` property. Pass `--prune` to automatically delete managed pages that have been removed from `nav:`. Manually-created Confluence pages are never deleted.
139
+
140
+ #### Orphan pruning
141
+
142
+ ```bash
143
+ mk2conf publish --prune
144
+ ```
145
+
146
+ After each publish run, `--prune` fetches all descendants of the configured `parent_page_id`, diffs them against the pages just published, and deletes any managed orphans — i.e. pages that carry the `mk2conf-managed` stamp but are no longer in the MkDocs nav.
147
+
148
+ > **Safety:** Only pages stamped by mk2conf are eligible for deletion. Any page created directly in Confluence will never be touched, even if it sits inside the managed hierarchy.
149
+ >
150
+ > **Partial runs:** `--prune` is silently ignored when `--page` or `--section` is used, because a partial publish would only cover a subset of the nav and would incorrectly identify out-of-scope pages as orphans.
136
151
 
137
152
  #### Mermaid rendering
138
153
 
@@ -283,7 +298,7 @@ Any unrecognised block is preserved as a visible `warning` macro — no content
283
298
 
284
299
  ## Roadmap
285
300
 
286
- - [ ] **Delete orphaned pages** — detect and remove Confluence pages that were previously published but have since been removed from `nav:`.
301
+ - [x] **Delete orphaned pages** — `--prune` detects and removes managed Confluence pages that have been removed from `nav:`. Manually-created pages are never deleted.
287
302
 
288
303
  ---
289
304
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.5.28"
3
+ version = "0.6.1"
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" }
@@ -88,4 +88,11 @@ exclude_lines = [
88
88
  "pragma: no cover",
89
89
  "if TYPE_CHECKING:",
90
90
  "raise NotImplementedError",
91
- ]
91
+ ]
92
+
93
+ [dependency-groups]
94
+ dev = [
95
+ "pip-audit>=2.10.0",
96
+ "radon>=6.0.1",
97
+ "vulture>=2.16",
98
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.5.28
3
+ Version: 0.6.1
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
@@ -41,6 +41,7 @@ Dynamic: license-file
41
41
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
42
42
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
43
43
  [![PyPI](https://img.shields.io/pypi/v/mkdocs2confluence)](https://pypi.org/project/mkdocs2confluence/)
44
+ [![Downloads](https://img.shields.io/pypi/dm/mkdocs2confluence)](https://pypi.org/project/mkdocs2confluence/)
44
45
  [![Latest Release](https://img.shields.io/github/v/release/jeckyl2010/mkdocs2confluence)](https://github.com/jeckyl2010/mkdocs2confluence/releases/latest)
45
46
  [![CI](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml/badge.svg)](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml)
46
47
  [![Release](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml/badge.svg)](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml)
@@ -133,7 +134,7 @@ The `--html` flag renders Confluence macros as visual HTML panels so you can rev
133
134
  Compile all pages listed in `nav:` and publish them to Confluence Cloud.
134
135
 
135
136
  ```
136
- mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--report FILE]
137
+ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--report FILE] [--prune]
137
138
  ```
138
139
 
139
140
  | Flag | Default | Description |
@@ -143,6 +144,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
143
144
  | `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
144
145
  | `--dry-run` | off | Print the publish plan without making any API calls |
145
146
  | `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
147
+ | `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
146
148
 
147
149
  #### Configuration
148
150
 
@@ -171,6 +173,19 @@ The API token is read from (in priority order):
171
173
  - Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
172
174
  - All locally linked assets are uploaded as Confluence page attachments automatically.
173
175
  - **Smart update detection** — before calling the Confluence update API, mk2conf compares a `sha256` hash of the compiled output against the hash stored from the previous run (kept as a hidden Confluence page property `mk2conf-content-hash`). Pages whose content has not changed are skipped entirely — no version bump, no watcher notification.
176
+ - **Orphan pruning** — every page created by mk2conf is stamped with a hidden `mk2conf-managed` property. Pass `--prune` to automatically delete managed pages that have been removed from `nav:`. Manually-created Confluence pages are never deleted.
177
+
178
+ #### Orphan pruning
179
+
180
+ ```bash
181
+ mk2conf publish --prune
182
+ ```
183
+
184
+ After each publish run, `--prune` fetches all descendants of the configured `parent_page_id`, diffs them against the pages just published, and deletes any managed orphans — i.e. pages that carry the `mk2conf-managed` stamp but are no longer in the MkDocs nav.
185
+
186
+ > **Safety:** Only pages stamped by mk2conf are eligible for deletion. Any page created directly in Confluence will never be touched, even if it sits inside the managed hierarchy.
187
+ >
188
+ > **Partial runs:** `--prune` is silently ignored when `--page` or `--section` is used, because a partial publish would only cover a subset of the nav and would incorrectly identify out-of-scope pages as orphans.
174
189
 
175
190
  #### Mermaid rendering
176
191
 
@@ -321,7 +336,7 @@ Any unrecognised block is preserved as a visible `warning` macro — no content
321
336
 
322
337
  ## Roadmap
323
338
 
324
- - [ ] **Delete orphaned pages** — detect and remove Confluence pages that were previously published but have since been removed from `nav:`.
339
+ - [x] **Delete orphaned pages** — `--prune` detects and removes managed Confluence pages that have been removed from `nav:`. Manually-created pages are never deleted.
325
340
 
326
341
  ---
327
342
 
@@ -115,6 +115,15 @@ def _build_parser() -> argparse.ArgumentParser:
115
115
  default=None,
116
116
  help="Write a JSON publish report to FILE after the run.",
117
117
  )
118
+ publish.add_argument(
119
+ "--prune",
120
+ action="store_true",
121
+ help=(
122
+ "Delete managed Confluence pages that are no longer in the MkDocs nav. "
123
+ "Only pages stamped by mk2conf are eligible — manually-created pages are never deleted. "
124
+ "Ignored when --page or --section is used."
125
+ ),
126
+ )
118
127
 
119
128
  return parser
120
129
 
@@ -308,10 +317,14 @@ def _cmd_publish(args: argparse.Namespace) -> None:
308
317
  )
309
318
  sys.exit(1)
310
319
  plan = plan_publish(nav_nodes, client, config, conf_config, space_id=space_id)
320
+ # --prune is silently disabled for partial publishes (--page / --section)
321
+ # because published_ids would only cover the subset, not the full nav.
322
+ partial = bool(getattr(args, "page", None) or getattr(args, "section", None))
311
323
  report = execute_publish(
312
324
  plan, client, dry_run=False, space_id=space_id,
313
325
  docs_dir=config.docs_dir, full_width=conf_config.full_width,
314
326
  root_page_id=conf_config.parent_page_id,
327
+ prune=getattr(args, "prune", False) and not partial,
315
328
  )
316
329
  except ConfluenceError as exc:
317
330
  print(f"error: {exc}", file=sys.stderr)
@@ -107,7 +107,7 @@ def _make_env_loader() -> type[yaml.SafeLoader]:
107
107
 
108
108
  # Catch-all: any other unknown tag (e.g. !!python/name:... used by
109
109
  # MkDocs Material) is silently ignored — we only care about nav/site_name.
110
- def _ignore(loader: yaml.SafeLoader, tag_suffix: str, node: yaml.Node) -> None:
110
+ def _ignore(loader: yaml.SafeLoader, _tag_suffix: str, node: yaml.Node) -> None:
111
111
  return None
112
112
 
113
113
  _Loader.add_multi_constructor("", _ignore) # type: ignore[no-untyped-call]
@@ -39,7 +39,7 @@ class NavNode:
39
39
  return self.docs_path is not None
40
40
 
41
41
 
42
- def resolve_nav(config: MkDocsConfig, mkdocs_root: Path | None = None) -> list[NavNode]:
42
+ def resolve_nav(config: MkDocsConfig) -> list[NavNode]:
43
43
  """Resolve *config.nav* into a list of top-level :class:`NavNode` objects.
44
44
 
45
45
  When ``config.nav`` is ``None`` (e.g. projects using ``awesome-pages`` or
@@ -48,8 +48,6 @@ def resolve_nav(config: MkDocsConfig, mkdocs_root: Path | None = None) -> list[N
48
48
 
49
49
  Args:
50
50
  config: Parsed :class:`~mkdocs_to_confluence.loader.config.MkDocsConfig`.
51
- mkdocs_root: Directory containing ``mkdocs.yml``. Used only to compute
52
- ``docs_dir`` when it is not already absolute; defaults to CWD.
53
51
 
54
52
  Returns:
55
53
  List of top-level :class:`NavNode` instances.
@@ -290,7 +290,7 @@ def _resolve_include_path(rel_path: str, source_path: Path, docs_dir: Path) -> P
290
290
  """
291
291
  for base in (docs_dir, source_path.parent):
292
292
  candidate = (base / rel_path).resolve()
293
- if candidate.is_file():
293
+ if candidate.is_file() and candidate.is_relative_to(base.resolve()):
294
294
  return candidate
295
295
  return None
296
296
 
@@ -9,12 +9,20 @@ from __future__ import annotations
9
9
  import base64
10
10
  from pathlib import Path
11
11
  from typing import Any, cast
12
+ from urllib.parse import parse_qs, urlparse
12
13
 
13
14
  import httpx
14
15
 
15
16
  from mkdocs_to_confluence.loader.config import ConfluenceConfig
16
17
 
17
18
 
19
+ def _extract_cursor(next_url: str) -> str:
20
+ """Extract the ``cursor`` query parameter from a pagination ``next`` URL."""
21
+ qs = parse_qs(urlparse(next_url).query)
22
+ cursors = qs.get("cursor", [])
23
+ return cursors[0] if cursors else ""
24
+
25
+
18
26
  class ConfluenceError(RuntimeError):
19
27
  """Raised when the Confluence API returns an unexpected response."""
20
28
 
@@ -409,3 +417,59 @@ class ConfluenceClient:
409
417
  headers={"X-Atlassian-Token": "no-check"},
410
418
  )
411
419
  self._raise_for_status(resp, f"upload_attachment({filename!r})")
420
+
421
+ # ── Orphan detection ───────────────────────────────────────────────────────
422
+
423
+ def stamp_managed(self, page_id: str) -> None:
424
+ """Mark *page_id* as managed by mk2conf via a v2 content property.
425
+
426
+ The property ``mk2conf-managed`` is set to ``true`` on first publish and
427
+ never updated. It is used by :meth:`is_managed` to distinguish pages
428
+ created by mk2conf from manually-created Confluence pages.
429
+
430
+ Errors are swallowed — this is a best-effort stamp and must never block
431
+ a publish.
432
+ """
433
+ url = self._v2(f"/pages/{page_id}/properties/mk2conf-managed")
434
+ get_resp = self._http.get(url)
435
+ if get_resp.status_code == 200:
436
+ return # already stamped
437
+ self._http.post(
438
+ self._v2(f"/pages/{page_id}/properties"),
439
+ json={"key": "mk2conf-managed", "value": True},
440
+ )
441
+
442
+ def get_descendant_ids(self, page_id: str) -> list[str]:
443
+ """Return all descendant page IDs under *page_id* at all depths.
444
+
445
+ Uses ``GET /wiki/api/v2/pages/{id}/descendants?depth=all``.
446
+ Paginates automatically via cursor. Returns page IDs only — callers
447
+ that need to filter by managed status use :meth:`is_managed`.
448
+ """
449
+ ids: list[str] = []
450
+ url = self._v2(f"/pages/{page_id}/descendants")
451
+ params: dict[str, str | int] = {"depth": "all", "limit": 250}
452
+ while True:
453
+ resp = self._http.get(url, params=params)
454
+ self._raise_for_status(resp, f"get_descendant_ids({page_id!r})")
455
+ data = resp.json()
456
+ for item in data.get("results", []):
457
+ if item.get("type") == "page":
458
+ ids.append(str(item["id"]))
459
+ next_url = data.get("_links", {}).get("next")
460
+ if not next_url:
461
+ break
462
+ # next_url is an absolute path — extract cursor and re-request
463
+ url = self._v2(f"/pages/{page_id}/descendants")
464
+ params = {"depth": "all", "limit": 250, "cursor": _extract_cursor(next_url)}
465
+ return ids
466
+
467
+ def is_managed(self, page_id: str) -> bool:
468
+ """Return ``True`` if *page_id* has the ``mk2conf-managed`` property."""
469
+ resp = self._http.get(self._v2(f"/pages/{page_id}/properties/mk2conf-managed"))
470
+ return resp.status_code == 200
471
+
472
+ def delete_page(self, page_id: str) -> None:
473
+ """Permanently delete *page_id* from Confluence."""
474
+ resp = self._http.delete(self._v2(f"/pages/{page_id}"))
475
+ self._raise_for_status(resp, f"delete_page({page_id!r})")