mkdocs2confluence 0.6.2__tar.gz → 0.6.4__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.
- {mkdocs2confluence-0.6.2/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.6.4}/PKG-INFO +7 -5
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/README.md +6 -4
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/pyproject.toml +1 -1
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4/src/mkdocs2confluence.egg-info}/PKG-INFO +7 -5
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/SOURCES.txt +1 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/cli.py +44 -11
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/nav.py +15 -8
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/publisher/pipeline.py +71 -38
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/mermaid.py +46 -18
- mkdocs2confluence-0.6.4/tests/test_cli.py +196 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_loader.py +22 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_mermaid.py +100 -4
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_preview.py +0 -2
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_publish_pipeline.py +64 -4
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/LICENSE +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/setup.cfg +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/ir/document.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/config.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/publisher/client.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_emitter.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_images.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_parser.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_preprocess.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_publish_client.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_publish_config.py +0 -0
- {mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/tests/test_treeutil.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
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
|
|
@@ -88,6 +88,8 @@ cd mkdocs2confluence
|
|
|
88
88
|
pip install -e ".[dev]"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
> **Package name vs. command name** — This follows the same convention used by many popular CLI tools (e.g. `pip install httpie` → `http` command). The PyPI package is `mkdocs2confluence` (descriptive, easy to find), and the CLI command is `mk2conf` (short, fast to type). Install once, run everywhere as `mk2conf`.
|
|
92
|
+
|
|
91
93
|
---
|
|
92
94
|
|
|
93
95
|
## Quick start
|
|
@@ -112,7 +114,7 @@ CONFLUENCE_API_TOKEN=your_token mk2conf publish --config mkdocs.yml
|
|
|
112
114
|
|
|
113
115
|
### `mk2conf preview`
|
|
114
116
|
|
|
115
|
-
Compile a single page and inspect the output — no
|
|
117
|
+
Compile a single page and inspect the output — no Confluence API calls required. Mermaid diagrams are rendered via Kroki unless `mermaid_render: none` is set.
|
|
116
118
|
|
|
117
119
|
```
|
|
118
120
|
mk2conf preview [--config PATH] --page PATH [--out FILE] [--html]
|
|
@@ -142,7 +144,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
|
|
|
142
144
|
| `--config PATH` | `./mkdocs.yml` | Path to your `mkdocs.yml` |
|
|
143
145
|
| `--page PATH` | *(all nav pages)* | Publish a single page only |
|
|
144
146
|
| `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
|
|
145
|
-
| `--dry-run` | off | Print the publish plan without making any API calls |
|
|
147
|
+
| `--dry-run` | off | Print the publish plan without making any Confluence API calls (Mermaid diagrams are still rendered via Kroki unless `mermaid_render: none` is set) |
|
|
146
148
|
| `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
|
|
147
149
|
| `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
|
|
148
150
|
|
|
@@ -172,7 +174,7 @@ The API token is read from (in priority order):
|
|
|
172
174
|
- Pages with `ready: false` in their YAML front matter are **skipped**, even if listed in the nav.
|
|
173
175
|
- Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
|
|
174
176
|
- All locally linked assets are uploaded as Confluence page attachments automatically.
|
|
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.
|
|
177
|
+
- **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. As a side effect, Confluence's built-in version history becomes a meaningful audit trail: every version represents a real content change, each stamped *"Updated by mk2conf"* so automated publishes are clearly distinguishable from manual edits. You can diff any two versions directly inside Confluence, with inline highlighting showing exactly which paragraphs, headings, or code blocks changed.
|
|
176
178
|
- **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
179
|
|
|
178
180
|
#### Orphan pruning
|
|
@@ -346,7 +348,7 @@ See [Setup.md](Setup.md) for environment setup.
|
|
|
346
348
|
|
|
347
349
|
```bash
|
|
348
350
|
pytest # run tests
|
|
349
|
-
ruff check src
|
|
351
|
+
ruff check src tests # lint
|
|
350
352
|
mypy src # type-check
|
|
351
353
|
bandit -r src -ll # security scan
|
|
352
354
|
```
|
|
@@ -50,6 +50,8 @@ cd mkdocs2confluence
|
|
|
50
50
|
pip install -e ".[dev]"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
> **Package name vs. command name** — This follows the same convention used by many popular CLI tools (e.g. `pip install httpie` → `http` command). The PyPI package is `mkdocs2confluence` (descriptive, easy to find), and the CLI command is `mk2conf` (short, fast to type). Install once, run everywhere as `mk2conf`.
|
|
54
|
+
|
|
53
55
|
---
|
|
54
56
|
|
|
55
57
|
## Quick start
|
|
@@ -74,7 +76,7 @@ CONFLUENCE_API_TOKEN=your_token mk2conf publish --config mkdocs.yml
|
|
|
74
76
|
|
|
75
77
|
### `mk2conf preview`
|
|
76
78
|
|
|
77
|
-
Compile a single page and inspect the output — no
|
|
79
|
+
Compile a single page and inspect the output — no Confluence API calls required. Mermaid diagrams are rendered via Kroki unless `mermaid_render: none` is set.
|
|
78
80
|
|
|
79
81
|
```
|
|
80
82
|
mk2conf preview [--config PATH] --page PATH [--out FILE] [--html]
|
|
@@ -104,7 +106,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
|
|
|
104
106
|
| `--config PATH` | `./mkdocs.yml` | Path to your `mkdocs.yml` |
|
|
105
107
|
| `--page PATH` | *(all nav pages)* | Publish a single page only |
|
|
106
108
|
| `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
|
|
107
|
-
| `--dry-run` | off | Print the publish plan without making any API calls |
|
|
109
|
+
| `--dry-run` | off | Print the publish plan without making any Confluence API calls (Mermaid diagrams are still rendered via Kroki unless `mermaid_render: none` is set) |
|
|
108
110
|
| `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
|
|
109
111
|
| `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
|
|
110
112
|
|
|
@@ -134,7 +136,7 @@ The API token is read from (in priority order):
|
|
|
134
136
|
- Pages with `ready: false` in their YAML front matter are **skipped**, even if listed in the nav.
|
|
135
137
|
- Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
|
|
136
138
|
- All locally linked assets are uploaded as Confluence page attachments automatically.
|
|
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.
|
|
139
|
+
- **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. As a side effect, Confluence's built-in version history becomes a meaningful audit trail: every version represents a real content change, each stamped *"Updated by mk2conf"* so automated publishes are clearly distinguishable from manual edits. You can diff any two versions directly inside Confluence, with inline highlighting showing exactly which paragraphs, headings, or code blocks changed.
|
|
138
140
|
- **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
141
|
|
|
140
142
|
#### Orphan pruning
|
|
@@ -308,7 +310,7 @@ See [Setup.md](Setup.md) for environment setup.
|
|
|
308
310
|
|
|
309
311
|
```bash
|
|
310
312
|
pytest # run tests
|
|
311
|
-
ruff check src
|
|
313
|
+
ruff check src tests # lint
|
|
312
314
|
mypy src # type-check
|
|
313
315
|
bandit -r src -ll # security scan
|
|
314
316
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mkdocs2confluence"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.4"
|
|
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.6.
|
|
3
|
+
Version: 0.6.4
|
|
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
|
|
@@ -88,6 +88,8 @@ cd mkdocs2confluence
|
|
|
88
88
|
pip install -e ".[dev]"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
> **Package name vs. command name** — This follows the same convention used by many popular CLI tools (e.g. `pip install httpie` → `http` command). The PyPI package is `mkdocs2confluence` (descriptive, easy to find), and the CLI command is `mk2conf` (short, fast to type). Install once, run everywhere as `mk2conf`.
|
|
92
|
+
|
|
91
93
|
---
|
|
92
94
|
|
|
93
95
|
## Quick start
|
|
@@ -112,7 +114,7 @@ CONFLUENCE_API_TOKEN=your_token mk2conf publish --config mkdocs.yml
|
|
|
112
114
|
|
|
113
115
|
### `mk2conf preview`
|
|
114
116
|
|
|
115
|
-
Compile a single page and inspect the output — no
|
|
117
|
+
Compile a single page and inspect the output — no Confluence API calls required. Mermaid diagrams are rendered via Kroki unless `mermaid_render: none` is set.
|
|
116
118
|
|
|
117
119
|
```
|
|
118
120
|
mk2conf preview [--config PATH] --page PATH [--out FILE] [--html]
|
|
@@ -142,7 +144,7 @@ mk2conf publish [--config PATH] [--page PATH] [--section PATH] [--dry-run] [--re
|
|
|
142
144
|
| `--config PATH` | `./mkdocs.yml` | Path to your `mkdocs.yml` |
|
|
143
145
|
| `--page PATH` | *(all nav pages)* | Publish a single page only |
|
|
144
146
|
| `--section PATH` | *(whole nav)* | Publish only a nav subtree (e.g. `Guide` or `Guide/Setup`) |
|
|
145
|
-
| `--dry-run` | off | Print the publish plan without making any API calls |
|
|
147
|
+
| `--dry-run` | off | Print the publish plan without making any Confluence API calls (Mermaid diagrams are still rendered via Kroki unless `mermaid_render: none` is set) |
|
|
146
148
|
| `--report FILE` | *(none)* | Write a JSON publish report to `FILE` |
|
|
147
149
|
| `--prune` | off | Delete managed Confluence pages no longer in `nav:` (see [Orphan pruning](#orphan-pruning)) |
|
|
148
150
|
|
|
@@ -172,7 +174,7 @@ The API token is read from (in priority order):
|
|
|
172
174
|
- Pages with `ready: false` in their YAML front matter are **skipped**, even if listed in the nav.
|
|
173
175
|
- Section nodes (nav groups without a page) become empty parent pages in Confluence, mirroring the nav hierarchy.
|
|
174
176
|
- All locally linked assets are uploaded as Confluence page attachments automatically.
|
|
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.
|
|
177
|
+
- **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. As a side effect, Confluence's built-in version history becomes a meaningful audit trail: every version represents a real content change, each stamped *"Updated by mk2conf"* so automated publishes are clearly distinguishable from manual edits. You can diff any two versions directly inside Confluence, with inline highlighting showing exactly which paragraphs, headings, or code blocks changed.
|
|
176
178
|
- **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
179
|
|
|
178
180
|
#### Orphan pruning
|
|
@@ -346,7 +348,7 @@ See [Setup.md](Setup.md) for environment setup.
|
|
|
346
348
|
|
|
347
349
|
```bash
|
|
348
350
|
pytest # run tests
|
|
349
|
-
ruff check src
|
|
351
|
+
ruff check src tests # lint
|
|
350
352
|
mypy src # type-check
|
|
351
353
|
bandit -r src -ll # security scan
|
|
352
354
|
```
|
{mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs2confluence.egg-info/SOURCES.txt
RENAMED
|
@@ -42,6 +42,7 @@ src/mkdocs_to_confluence/transforms/images.py
|
|
|
42
42
|
src/mkdocs_to_confluence/transforms/internallinks.py
|
|
43
43
|
src/mkdocs_to_confluence/transforms/mermaid.py
|
|
44
44
|
tests/test_abbrevs.py
|
|
45
|
+
tests/test_cli.py
|
|
45
46
|
tests/test_editlink.py
|
|
46
47
|
tests/test_emitter.py
|
|
47
48
|
tests/test_extra_css.py
|
|
@@ -33,7 +33,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
33
33
|
# --- preview ---
|
|
34
34
|
preview = sub.add_parser(
|
|
35
35
|
"preview",
|
|
36
|
-
help="Compile a
|
|
36
|
+
help="Compile a page (or whole section) and inspect the output — no Confluence API calls.",
|
|
37
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
38
|
+
epilog=(
|
|
39
|
+
"Examples:\n"
|
|
40
|
+
" mk2conf preview --page index.md\n"
|
|
41
|
+
" mk2conf preview --page guide/setup.md --html --out /tmp/setup.html\n"
|
|
42
|
+
" mk2conf preview --section Guide\n"
|
|
43
|
+
"\n"
|
|
44
|
+
" Either --page or --section is required.\n"
|
|
45
|
+
" Mermaid diagrams are rendered via Kroki unless 'mermaid_render: none' is set.\n"
|
|
46
|
+
),
|
|
37
47
|
)
|
|
38
48
|
preview.add_argument(
|
|
39
49
|
"--config",
|
|
@@ -47,7 +57,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
47
57
|
default=None,
|
|
48
58
|
help=(
|
|
49
59
|
"Relative path to the markdown file to compile. "
|
|
50
|
-
"
|
|
60
|
+
"Required unless --section is given."
|
|
51
61
|
),
|
|
52
62
|
)
|
|
53
63
|
preview.add_argument(
|
|
@@ -65,7 +75,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
65
75
|
"--section",
|
|
66
76
|
metavar="NAME",
|
|
67
77
|
default=None,
|
|
68
|
-
help=
|
|
78
|
+
help=(
|
|
79
|
+
"Nav section to preview (slash-separated path, e.g. 'Guide' or 'Guide/Setup'). "
|
|
80
|
+
"Without --page, renders all pages in the section as a browseable HTML index."
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
preview.add_argument(
|
|
85
|
+
"--quiet",
|
|
86
|
+
"-q",
|
|
87
|
+
action="store_true",
|
|
88
|
+
help="Suppress per-item progress output; only the final summary and warnings are shown.",
|
|
69
89
|
)
|
|
70
90
|
|
|
71
91
|
# --- publish ---
|
|
@@ -125,6 +145,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
125
145
|
),
|
|
126
146
|
)
|
|
127
147
|
|
|
148
|
+
publish.add_argument(
|
|
149
|
+
"--quiet",
|
|
150
|
+
"-q",
|
|
151
|
+
action="store_true",
|
|
152
|
+
help="Suppress per-item progress output; only the final summary and warnings are shown.",
|
|
153
|
+
)
|
|
154
|
+
|
|
128
155
|
return parser
|
|
129
156
|
|
|
130
157
|
|
|
@@ -136,12 +163,17 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
136
163
|
parser.print_help()
|
|
137
164
|
sys.exit(0)
|
|
138
165
|
|
|
139
|
-
|
|
166
|
+
if sys.stdout.isatty():
|
|
167
|
+
print(f"mk2conf {__version__}")
|
|
140
168
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
try:
|
|
170
|
+
if args.command == "preview":
|
|
171
|
+
_cmd_preview(args)
|
|
172
|
+
elif args.command == "publish":
|
|
173
|
+
_cmd_publish(args)
|
|
174
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
175
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
176
|
+
sys.exit(1)
|
|
145
177
|
|
|
146
178
|
|
|
147
179
|
def _parse_out_path(out_arg: str | None) -> tuple[Path, str]:
|
|
@@ -202,7 +234,7 @@ def _cmd_preview(args: argparse.Namespace) -> None:
|
|
|
202
234
|
for node in pages:
|
|
203
235
|
html_name = page_link_map.get(node.title, f"{Path(node.docs_path or node.title).stem}.html")
|
|
204
236
|
try:
|
|
205
|
-
xhtml, _attachments, _labels = compile_page(node, config, link_map)
|
|
237
|
+
xhtml, _attachments, _labels = compile_page(node, config, link_map, quiet=args.quiet)
|
|
206
238
|
except PageLoadError as exc:
|
|
207
239
|
print(f" warning: skipping '{node.title}': {exc}", file=sys.stderr)
|
|
208
240
|
continue
|
|
@@ -227,7 +259,7 @@ def _cmd_preview(args: argparse.Namespace) -> None:
|
|
|
227
259
|
|
|
228
260
|
try:
|
|
229
261
|
link_map = build_link_map(nodes)
|
|
230
|
-
xhtml, _attachments, _labels = compile_page(page_node, config, link_map)
|
|
262
|
+
xhtml, _attachments, _labels = compile_page(page_node, config, link_map, quiet=args.quiet)
|
|
231
263
|
except PageLoadError as exc:
|
|
232
264
|
print(f"error: {exc}", file=sys.stderr)
|
|
233
265
|
sys.exit(1)
|
|
@@ -316,7 +348,7 @@ def _cmd_publish(args: argparse.Namespace) -> None:
|
|
|
316
348
|
file=sys.stderr,
|
|
317
349
|
)
|
|
318
350
|
sys.exit(1)
|
|
319
|
-
plan = plan_publish(nav_nodes, client, config, conf_config, space_id=space_id)
|
|
351
|
+
plan = plan_publish(nav_nodes, client, config, conf_config, space_id=space_id, quiet=args.quiet)
|
|
320
352
|
# --prune is silently disabled for partial publishes (--page / --section)
|
|
321
353
|
# because published_ids would only cover the subset, not the full nav.
|
|
322
354
|
partial = bool(getattr(args, "page", None) or getattr(args, "section", None))
|
|
@@ -325,6 +357,7 @@ def _cmd_publish(args: argparse.Namespace) -> None:
|
|
|
325
357
|
docs_dir=config.docs_dir, full_width=conf_config.full_width,
|
|
326
358
|
root_page_id=conf_config.parent_page_id,
|
|
327
359
|
prune=getattr(args, "prune", False) and not partial,
|
|
360
|
+
quiet=args.quiet,
|
|
328
361
|
)
|
|
329
362
|
except ConfluenceError as exc:
|
|
330
363
|
print(f"error: {exc}", file=sys.stderr)
|
|
@@ -82,19 +82,26 @@ def _discover(docs_dir: Path, nav_file: str) -> list[NavNode]:
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def _read_nav_file(directory: Path, nav_file: str) -> list[Any] | None:
|
|
85
|
-
"""Return the nav list from *directory*/<nav_file>, or None if absent
|
|
85
|
+
"""Return the nav list from *directory*/<nav_file>, or None if absent.
|
|
86
|
+
|
|
87
|
+
Raises ValueError if the file exists but cannot be parsed or has an
|
|
88
|
+
unexpected format — a present-but-broken nav file is a configuration
|
|
89
|
+
error, not a reason to silently fall back to full discovery.
|
|
90
|
+
"""
|
|
86
91
|
path = directory / nav_file
|
|
87
92
|
if not path.exists():
|
|
88
93
|
return None
|
|
89
94
|
try:
|
|
90
95
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
raise ValueError(f"Could not parse nav file {path}: {exc}") from exc
|
|
98
|
+
if isinstance(data, dict) and isinstance(data.get("nav"), list):
|
|
99
|
+
return cast(list[Any], data["nav"])
|
|
100
|
+
if isinstance(data, list):
|
|
101
|
+
return data
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Nav file {path} has unexpected format (expected a list or a dict with a 'nav' key, got {type(data).__name__})"
|
|
104
|
+
)
|
|
98
105
|
|
|
99
106
|
|
|
100
107
|
def _traverse_nav_dir(
|
{mkdocs2confluence-0.6.2 → mkdocs2confluence-0.6.4}/src/mkdocs_to_confluence/publisher/pipeline.py
RENAMED
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import hashlib
|
|
17
17
|
import re
|
|
18
|
+
import sys
|
|
18
19
|
from dataclasses import dataclass, field
|
|
19
20
|
from datetime import datetime, timezone
|
|
20
21
|
from pathlib import Path
|
|
@@ -134,6 +135,8 @@ def compile_page(
|
|
|
134
135
|
node: NavNode,
|
|
135
136
|
config: MkDocsConfig,
|
|
136
137
|
link_map: dict[str, str] | None = None,
|
|
138
|
+
*,
|
|
139
|
+
quiet: bool = False,
|
|
137
140
|
) -> tuple[str, list[Path], tuple[str, ...]]:
|
|
138
141
|
"""Run the full compile pipeline for one page.
|
|
139
142
|
|
|
@@ -174,7 +177,7 @@ def compile_page(
|
|
|
174
177
|
mermaid_render[len("kroki:"):] if mermaid_render.startswith("kroki:")
|
|
175
178
|
else DEFAULT_KROKI_URL
|
|
176
179
|
)
|
|
177
|
-
ir_nodes, mermaid_attachments = render_mermaid_diagrams(ir_nodes, kroki_url)
|
|
180
|
+
ir_nodes, mermaid_attachments = render_mermaid_diagrams(ir_nodes, kroki_url, quiet=quiet)
|
|
178
181
|
attachments = attachments + mermaid_attachments
|
|
179
182
|
effective_link_map = link_map if link_map is not None else {}
|
|
180
183
|
if node.docs_path:
|
|
@@ -220,6 +223,7 @@ def plan_publish(
|
|
|
220
223
|
conf_config: ConfluenceConfig,
|
|
221
224
|
*,
|
|
222
225
|
space_id: str,
|
|
226
|
+
quiet: bool = False,
|
|
223
227
|
) -> list[PageAction]:
|
|
224
228
|
"""Build a publish plan for the entire nav tree.
|
|
225
229
|
|
|
@@ -229,8 +233,9 @@ def plan_publish(
|
|
|
229
233
|
"""
|
|
230
234
|
actions: list[PageAction] = []
|
|
231
235
|
link_map = build_link_map(nav_nodes)
|
|
232
|
-
|
|
233
|
-
|
|
236
|
+
if not quiet:
|
|
237
|
+
print("Planning...")
|
|
238
|
+
_plan_nodes(nav_nodes, client, config, space_id, conf_config.parent_page_id, False, actions, link_map, quiet=quiet)
|
|
234
239
|
return actions
|
|
235
240
|
|
|
236
241
|
|
|
@@ -243,6 +248,8 @@ def _plan_nodes(
|
|
|
243
248
|
parent_is_folder: bool,
|
|
244
249
|
actions: list[PageAction],
|
|
245
250
|
link_map: dict[str, str] | None = None,
|
|
251
|
+
*,
|
|
252
|
+
quiet: bool = False,
|
|
246
253
|
) -> None:
|
|
247
254
|
for node in nodes:
|
|
248
255
|
# Strip icon shortcodes from titles — nav titles bypass the body
|
|
@@ -261,13 +268,15 @@ def _plan_nodes(
|
|
|
261
268
|
except OSError:
|
|
262
269
|
pass
|
|
263
270
|
if ready is not False and index_child.source_path is not None:
|
|
264
|
-
|
|
271
|
+
if not quiet:
|
|
272
|
+
print(f" compiling '{clean_title}' (section index)")
|
|
265
273
|
try:
|
|
266
|
-
xhtml, attachments, labels = compile_page(index_child, config, link_map)
|
|
274
|
+
xhtml, attachments, labels = compile_page(index_child, config, link_map, quiet=quiet)
|
|
267
275
|
existing = client.find_page(space_id, clean_title)
|
|
268
276
|
xhtml_h = _xhtml_hash(xhtml)
|
|
269
277
|
if existing is not None and client.get_content_hash(str(existing["id"])) == xhtml_h:
|
|
270
|
-
|
|
278
|
+
if not quiet:
|
|
279
|
+
print(f" unchanged '{clean_title}' (content unchanged)")
|
|
271
280
|
actions.append(PageAction(
|
|
272
281
|
node=node,
|
|
273
282
|
title=clean_title,
|
|
@@ -278,7 +287,8 @@ def _plan_nodes(
|
|
|
278
287
|
))
|
|
279
288
|
non_index = [c for c in node.children if c is not index_child]
|
|
280
289
|
_plan_nodes(
|
|
281
|
-
non_index, client, config, space_id,
|
|
290
|
+
non_index, client, config, space_id,
|
|
291
|
+
str(existing["id"]), False, actions, link_map, quiet=quiet
|
|
282
292
|
)
|
|
283
293
|
continue
|
|
284
294
|
page_action = PageAction(
|
|
@@ -301,16 +311,18 @@ def _plan_nodes(
|
|
|
301
311
|
# Recurse remaining children — index.md is already consumed.
|
|
302
312
|
non_index = [c for c in node.children if c is not index_child]
|
|
303
313
|
_plan_nodes(
|
|
304
|
-
non_index, client, config, space_id, None, False, actions, link_map
|
|
314
|
+
non_index, client, config, space_id, None, False, actions, link_map, quiet=quiet
|
|
305
315
|
)
|
|
306
316
|
continue
|
|
307
317
|
except (PageLoadError, OSError) as exc:
|
|
308
318
|
print(
|
|
309
|
-
f"
|
|
310
|
-
" falling back to folder"
|
|
319
|
+
f" [warn] '{clean_title}' index.md load error ({exc}),"
|
|
320
|
+
" falling back to folder",
|
|
321
|
+
file=sys.stderr,
|
|
311
322
|
)
|
|
312
323
|
|
|
313
|
-
|
|
324
|
+
if not quiet:
|
|
325
|
+
print(f" compiling '{clean_title}' (folder)")
|
|
314
326
|
# Folder find-or-create is deferred to execute_publish once the
|
|
315
327
|
# parent folder ID is known (nested folders don't have a parent ID
|
|
316
328
|
# yet at plan time).
|
|
@@ -327,7 +339,7 @@ def _plan_nodes(
|
|
|
327
339
|
actions.append(page_action)
|
|
328
340
|
# Children will be placed under this folder's ID (resolved at execute)
|
|
329
341
|
_plan_nodes(
|
|
330
|
-
list(node.children), client, config, space_id, None, True, actions, link_map
|
|
342
|
+
list(node.children), client, config, space_id, None, True, actions, link_map, quiet=quiet
|
|
331
343
|
)
|
|
332
344
|
else:
|
|
333
345
|
# Page node — read raw to check ready flag
|
|
@@ -340,7 +352,8 @@ def _plan_nodes(
|
|
|
340
352
|
pass
|
|
341
353
|
|
|
342
354
|
if ready is False:
|
|
343
|
-
|
|
355
|
+
if not quiet:
|
|
356
|
+
print(f" skipping '{clean_title}' (ready: false)")
|
|
344
357
|
actions.append(
|
|
345
358
|
PageAction(
|
|
346
359
|
node=node,
|
|
@@ -351,11 +364,13 @@ def _plan_nodes(
|
|
|
351
364
|
)
|
|
352
365
|
continue
|
|
353
366
|
|
|
354
|
-
|
|
367
|
+
if not quiet:
|
|
368
|
+
print(f" compiling '{clean_title}'")
|
|
355
369
|
try:
|
|
356
|
-
xhtml, attachments, labels = compile_page(node, config, link_map)
|
|
370
|
+
xhtml, attachments, labels = compile_page(node, config, link_map, quiet=quiet)
|
|
357
371
|
except (PageLoadError, OSError) as exc:
|
|
358
|
-
|
|
372
|
+
if not quiet:
|
|
373
|
+
print(f" skipping '{clean_title}' (error: {exc})")
|
|
359
374
|
actions.append(
|
|
360
375
|
PageAction(
|
|
361
376
|
node=node,
|
|
@@ -369,7 +384,8 @@ def _plan_nodes(
|
|
|
369
384
|
existing = client.find_page(space_id, clean_title)
|
|
370
385
|
xhtml_h = _xhtml_hash(xhtml)
|
|
371
386
|
if existing is not None and client.get_content_hash(str(existing["id"])) == xhtml_h:
|
|
372
|
-
|
|
387
|
+
if not quiet:
|
|
388
|
+
print(f" unchanged '{clean_title}' (content unchanged)")
|
|
373
389
|
actions.append(
|
|
374
390
|
PageAction(
|
|
375
391
|
node=node,
|
|
@@ -406,6 +422,8 @@ def _upload_assets(
|
|
|
406
422
|
attachments: list[Path],
|
|
407
423
|
docs_dir: Path,
|
|
408
424
|
client: ConfluenceClient,
|
|
425
|
+
*,
|
|
426
|
+
quiet: bool = False,
|
|
409
427
|
) -> tuple[int, int, list[tuple[str, str]]]:
|
|
410
428
|
"""Upload attachments for one page **sequentially**.
|
|
411
429
|
|
|
@@ -442,13 +460,15 @@ def _upload_assets(
|
|
|
442
460
|
path.stat().st_mtime, tz=timezone.utc
|
|
443
461
|
)
|
|
444
462
|
if local_mtime <= confluence_ts:
|
|
445
|
-
|
|
463
|
+
if not quiet:
|
|
464
|
+
print(f" skipping {name} (unchanged)")
|
|
446
465
|
skipped += 1
|
|
447
466
|
continue
|
|
448
467
|
except (KeyError, ValueError, OSError):
|
|
449
468
|
pass # can't compare — fall through to upload
|
|
450
469
|
|
|
451
|
-
|
|
470
|
+
if not quiet:
|
|
471
|
+
print(f" uploading {name}")
|
|
452
472
|
try:
|
|
453
473
|
client.upload_attachment(page_id, path, name, existing)
|
|
454
474
|
uploaded += 1
|
|
@@ -464,6 +484,8 @@ def _execute_folder_action(
|
|
|
464
484
|
space_id: str,
|
|
465
485
|
root_page_id: str | None,
|
|
466
486
|
report: PublishReport,
|
|
487
|
+
*,
|
|
488
|
+
quiet: bool = False,
|
|
467
489
|
) -> None:
|
|
468
490
|
"""Handle folder create/find for a single folder action."""
|
|
469
491
|
if action.page_id is not None:
|
|
@@ -482,8 +504,9 @@ def _execute_folder_action(
|
|
|
482
504
|
)
|
|
483
505
|
except Exception as find_exc:
|
|
484
506
|
print(
|
|
485
|
-
f"
|
|
486
|
-
f"(parent_id={action.parent_id}): {find_exc}"
|
|
507
|
+
f" [warn] find_folder_under failed "
|
|
508
|
+
f"(parent_id={action.parent_id}): {find_exc}",
|
|
509
|
+
file=sys.stderr,
|
|
487
510
|
)
|
|
488
511
|
if existing_folder is not None:
|
|
489
512
|
action.page_id = str(existing_folder["id"])
|
|
@@ -497,11 +520,12 @@ def _execute_folder_action(
|
|
|
497
520
|
)
|
|
498
521
|
action.page_id = str(folder["id"])
|
|
499
522
|
report.created += 1
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
523
|
+
if not quiet:
|
|
524
|
+
print(
|
|
525
|
+
f" folder id={action.page_id}"
|
|
526
|
+
f" parent_id={action.parent_id}"
|
|
527
|
+
f" parent_is_folder={action.parent_is_folder}"
|
|
528
|
+
)
|
|
505
529
|
else:
|
|
506
530
|
# Parent is a dynamically-created page (e.g. a section-index page).
|
|
507
531
|
# Confluence folders cannot be nested under pages — use a stub page.
|
|
@@ -563,8 +587,9 @@ def _execute_page_action(
|
|
|
563
587
|
if not is_stale:
|
|
564
588
|
raise
|
|
565
589
|
print(
|
|
566
|
-
f"
|
|
567
|
-
" stale page_id; falling back to create"
|
|
590
|
+
f" [warn] update failed ({err[:80].strip()}) —"
|
|
591
|
+
" stale page_id; falling back to create",
|
|
592
|
+
file=sys.stderr,
|
|
568
593
|
)
|
|
569
594
|
action.page_id = None
|
|
570
595
|
page = client.create_page(
|
|
@@ -602,6 +627,7 @@ def _post_process_action(
|
|
|
602
627
|
full_width: bool,
|
|
603
628
|
docs_dir: Path,
|
|
604
629
|
report: PublishReport,
|
|
630
|
+
quiet: bool = False,
|
|
605
631
|
) -> None:
|
|
606
632
|
"""Run all non-fatal post-create/update work for a single action."""
|
|
607
633
|
# Store content hash after create/update so the next run can skip unchanged pages.
|
|
@@ -628,7 +654,7 @@ def _post_process_action(
|
|
|
628
654
|
# Upload assets — skip files whose mtime is not newer than Confluence.
|
|
629
655
|
if action.page_id and action.attachments:
|
|
630
656
|
uploaded, asset_skipped, asset_errors = _upload_assets(
|
|
631
|
-
action.page_id, action.attachments, docs_dir, client
|
|
657
|
+
action.page_id, action.attachments, docs_dir, client, quiet=quiet
|
|
632
658
|
)
|
|
633
659
|
report.assets_uploaded += uploaded
|
|
634
660
|
report.assets_skipped += asset_skipped
|
|
@@ -646,6 +672,7 @@ def execute_publish(
|
|
|
646
672
|
full_width: bool = True,
|
|
647
673
|
root_page_id: str | None = None,
|
|
648
674
|
prune: bool = False,
|
|
675
|
+
quiet: bool = False,
|
|
649
676
|
) -> PublishReport:
|
|
650
677
|
"""Execute the publish plan.
|
|
651
678
|
|
|
@@ -674,7 +701,8 @@ def execute_publish(
|
|
|
674
701
|
|
|
675
702
|
active = [a for a in plan if a.action != "skip"]
|
|
676
703
|
total = len(active)
|
|
677
|
-
|
|
704
|
+
if not quiet:
|
|
705
|
+
print(f"\nPublishing {total} page(s)...")
|
|
678
706
|
counter = 0
|
|
679
707
|
|
|
680
708
|
for action in plan:
|
|
@@ -683,11 +711,12 @@ def execute_publish(
|
|
|
683
711
|
continue
|
|
684
712
|
|
|
685
713
|
counter += 1
|
|
686
|
-
|
|
714
|
+
if not quiet:
|
|
715
|
+
print(f" [{counter}/{total}] {action.action:<6} '{action.title}'")
|
|
687
716
|
|
|
688
717
|
try:
|
|
689
718
|
if action.is_folder:
|
|
690
|
-
_execute_folder_action(action, client, space_id, root_page_id, report)
|
|
719
|
+
_execute_folder_action(action, client, space_id, root_page_id, report, quiet=quiet)
|
|
691
720
|
else:
|
|
692
721
|
_execute_page_action(action, client, space_id, report)
|
|
693
722
|
except Exception as exc:
|
|
@@ -700,11 +729,11 @@ def execute_publish(
|
|
|
700
729
|
if action.node.is_section and action.page_id:
|
|
701
730
|
_wire_children(action, action_by_node)
|
|
702
731
|
|
|
703
|
-
_post_process_action(action, client, full_width=full_width, docs_dir=docs_dir, report=report)
|
|
732
|
+
_post_process_action(action, client, full_width=full_width, docs_dir=docs_dir, report=report, quiet=quiet)
|
|
704
733
|
|
|
705
734
|
if prune and root_page_id:
|
|
706
735
|
published_ids = {a.page_id for a in plan if a.page_id}
|
|
707
|
-
_prune_orphans(client, root_page_id, published_ids, report)
|
|
736
|
+
_prune_orphans(client, root_page_id, published_ids, report, quiet=quiet)
|
|
708
737
|
|
|
709
738
|
return report
|
|
710
739
|
|
|
@@ -714,6 +743,8 @@ def _prune_orphans(
|
|
|
714
743
|
root_page_id: str,
|
|
715
744
|
published_ids: set[str],
|
|
716
745
|
report: PublishReport,
|
|
746
|
+
*,
|
|
747
|
+
quiet: bool = False,
|
|
717
748
|
) -> None:
|
|
718
749
|
"""Delete managed descendant pages that are no longer in the publish plan.
|
|
719
750
|
|
|
@@ -723,20 +754,22 @@ def _prune_orphans(
|
|
|
723
754
|
try:
|
|
724
755
|
all_descendants = client.get_descendant_ids(root_page_id)
|
|
725
756
|
except Exception as exc:
|
|
726
|
-
print(f" [warn] prune: could not fetch descendants — {exc}")
|
|
757
|
+
print(f" [warn] prune: could not fetch descendants — {exc}", file=sys.stderr)
|
|
727
758
|
return
|
|
728
759
|
|
|
729
760
|
orphan_candidates = [pid for pid in all_descendants if pid not in published_ids]
|
|
730
761
|
if not orphan_candidates:
|
|
731
762
|
return
|
|
732
763
|
|
|
733
|
-
|
|
764
|
+
if not quiet:
|
|
765
|
+
print(f"\nPruning: checking {len(orphan_candidates)} orphan candidate(s)...")
|
|
734
766
|
for page_id in orphan_candidates:
|
|
735
767
|
try:
|
|
736
768
|
if not client.is_managed(page_id):
|
|
737
769
|
continue
|
|
738
770
|
client.delete_page(page_id)
|
|
739
771
|
report.pruned += 1
|
|
740
|
-
|
|
772
|
+
if not quiet:
|
|
773
|
+
print(f" deleted orphan page {page_id}")
|
|
741
774
|
except Exception as exc:
|
|
742
|
-
print(f" [warn] prune: failed to delete page {page_id} — {exc}")
|
|
775
|
+
print(f" [warn] prune: failed to delete page {page_id} — {exc}", file=sys.stderr)
|