mkdocs2confluence 0.12.0__tar.gz → 0.12.2__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.12.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.12.2}/PKG-INFO +9 -2
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/README.md +7 -1
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/pyproject.toml +2 -1
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2/src/mkdocs2confluence.egg-info}/PKG-INFO +9 -2
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/SOURCES.txt +6 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/requires.txt +1 -0
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/compiler/__init__.py +6 -0
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/compiler/models.py +17 -0
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/compiler/page.py +110 -0
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/executor.py +378 -0
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/models.py +60 -0
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/pipeline.py +46 -0
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/planner.py +308 -0
- mkdocs2confluence-0.12.0/src/mkdocs_to_confluence/publisher/pipeline.py +0 -838
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/LICENSE +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/setup.cfg +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/cli.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/document.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/config.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/nav.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/render.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/server.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/client.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/http_retry.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/command.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/comments.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/github.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/platform.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/state.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_children_macro.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_cli.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_emitter.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_footer.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_images.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_loader.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_mermaid.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_parser.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_pdf.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_preprocess.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_preview.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_publish_client.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_publish_config.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_publish_pipeline.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_server.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_anchoring.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_command.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_comments.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_github.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_sync_state.py +0 -0
- {mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/tests/test_treeutil.py +0 -0
{mkdocs2confluence-0.12.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.12.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.2
|
|
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
|
|
@@ -25,6 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: PyYAML>=6.0.3
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: idna>=3.15
|
|
28
29
|
Requires-Dist: tinycss2>=1.5.1
|
|
29
30
|
Provides-Extra: pdf
|
|
30
31
|
Requires-Dist: weasyprint>=60.0; extra == "pdf"
|
|
@@ -181,7 +182,13 @@ mk2conf publish # go live
|
|
|
181
182
|
|
|
182
183
|

|
|
183
184
|
|
|
184
|
-
Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
|
|
185
|
+
Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
|
|
186
|
+
|
|
187
|
+
The publisher is split into two phases:
|
|
188
|
+
- `planner.py` builds a nav-ordered publish plan, compiles pages, and makes the read-side API calls needed to decide create vs update vs skip.
|
|
189
|
+
- `executor.py` applies that plan, performs the write-side API calls, uploads attachments, and wires parent/child relationships in nav order so parent pages always exist before their children.
|
|
190
|
+
|
|
191
|
+
`publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
|
|
185
192
|
|
|
186
193
|
---
|
|
187
194
|
|
|
@@ -140,7 +140,13 @@ mk2conf publish # go live
|
|
|
140
140
|
|
|
141
141
|

|
|
142
142
|
|
|
143
|
-
Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
|
|
143
|
+
Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
|
|
144
|
+
|
|
145
|
+
The publisher is split into two phases:
|
|
146
|
+
- `planner.py` builds a nav-ordered publish plan, compiles pages, and makes the read-side API calls needed to decide create vs update vs skip.
|
|
147
|
+
- `executor.py` applies that plan, performs the write-side API calls, uploads attachments, and wires parent/child relationships in nav order so parent pages always exist before their children.
|
|
148
|
+
|
|
149
|
+
`publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
|
|
144
150
|
|
|
145
151
|
---
|
|
146
152
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mkdocs2confluence"
|
|
3
|
-
version = "0.12.
|
|
3
|
+
version = "0.12.2"
|
|
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" }
|
|
@@ -32,6 +32,7 @@ classifiers = [
|
|
|
32
32
|
dependencies = [
|
|
33
33
|
"PyYAML>=6.0.3",
|
|
34
34
|
"httpx>=0.27",
|
|
35
|
+
"idna>=3.15",
|
|
35
36
|
"tinycss2>=1.5.1",
|
|
36
37
|
]
|
|
37
38
|
|
{mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2/src/mkdocs2confluence.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.2
|
|
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
|
|
@@ -25,6 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: PyYAML>=6.0.3
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: idna>=3.15
|
|
28
29
|
Requires-Dist: tinycss2>=1.5.1
|
|
29
30
|
Provides-Extra: pdf
|
|
30
31
|
Requires-Dist: weasyprint>=60.0; extra == "pdf"
|
|
@@ -181,7 +182,13 @@ mk2conf publish # go live
|
|
|
181
182
|
|
|
182
183
|

|
|
183
184
|
|
|
184
|
-
Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
|
|
185
|
+
Pipeline stages: **loader → preprocess → IR → transforms → emitter → publisher**.
|
|
186
|
+
|
|
187
|
+
The publisher is split into two phases:
|
|
188
|
+
- `planner.py` builds a nav-ordered publish plan, compiles pages, and makes the read-side API calls needed to decide create vs update vs skip.
|
|
189
|
+
- `executor.py` applies that plan, performs the write-side API calls, uploads attachments, and wires parent/child relationships in nav order so parent pages always exist before their children.
|
|
190
|
+
|
|
191
|
+
`publisher/pipeline.py` remains a compatibility facade that re-exports the public publish surface used by the CLI and tests.
|
|
185
192
|
|
|
186
193
|
---
|
|
187
194
|
|
{mkdocs2confluence-0.12.0 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/SOURCES.txt
RENAMED
|
@@ -9,6 +9,9 @@ src/mkdocs2confluence.egg-info/requires.txt
|
|
|
9
9
|
src/mkdocs2confluence.egg-info/top_level.txt
|
|
10
10
|
src/mkdocs_to_confluence/__init__.py
|
|
11
11
|
src/mkdocs_to_confluence/cli.py
|
|
12
|
+
src/mkdocs_to_confluence/compiler/__init__.py
|
|
13
|
+
src/mkdocs_to_confluence/compiler/models.py
|
|
14
|
+
src/mkdocs_to_confluence/compiler/page.py
|
|
12
15
|
src/mkdocs_to_confluence/emitter/__init__.py
|
|
13
16
|
src/mkdocs_to_confluence/emitter/xhtml.py
|
|
14
17
|
src/mkdocs_to_confluence/ir/__init__.py
|
|
@@ -37,8 +40,11 @@ src/mkdocs_to_confluence/preview/render.py
|
|
|
37
40
|
src/mkdocs_to_confluence/preview/server.py
|
|
38
41
|
src/mkdocs_to_confluence/publisher/__init__.py
|
|
39
42
|
src/mkdocs_to_confluence/publisher/client.py
|
|
43
|
+
src/mkdocs_to_confluence/publisher/executor.py
|
|
40
44
|
src/mkdocs_to_confluence/publisher/http_retry.py
|
|
45
|
+
src/mkdocs_to_confluence/publisher/models.py
|
|
41
46
|
src/mkdocs_to_confluence/publisher/pipeline.py
|
|
47
|
+
src/mkdocs_to_confluence/publisher/planner.py
|
|
42
48
|
src/mkdocs_to_confluence/sync/__init__.py
|
|
43
49
|
src/mkdocs_to_confluence/sync/anchoring.py
|
|
44
50
|
src/mkdocs_to_confluence/sync/command.py
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Typed models for compiler outputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CompileResult:
|
|
11
|
+
"""Result of compiling a single MkDocs page to Confluence storage XHTML."""
|
|
12
|
+
|
|
13
|
+
xhtml: str
|
|
14
|
+
attachments: list[Path] = field(default_factory=list)
|
|
15
|
+
labels: tuple[str, ...] = ()
|
|
16
|
+
confluence_status: str | None = None
|
|
17
|
+
version_message: str | None = None
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Page compilation pipeline for MkDocs-to-Confluence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mkdocs_to_confluence.compiler.models import CompileResult
|
|
6
|
+
from mkdocs_to_confluence.emitter.xhtml import emit
|
|
7
|
+
from mkdocs_to_confluence.ir.nodes import ChildrenMacro, FrontMatter, SourceFooter
|
|
8
|
+
from mkdocs_to_confluence.loader.config import MkDocsConfig
|
|
9
|
+
from mkdocs_to_confluence.loader.nav import NavNode
|
|
10
|
+
from mkdocs_to_confluence.loader.page import load_page
|
|
11
|
+
from mkdocs_to_confluence.parser.markdown import parse
|
|
12
|
+
from mkdocs_to_confluence.preprocess.abbrevs import (
|
|
13
|
+
extract_abbreviations,
|
|
14
|
+
strip_abbreviation_defs,
|
|
15
|
+
)
|
|
16
|
+
from mkdocs_to_confluence.preprocess.frontmatter import extract_front_matter
|
|
17
|
+
from mkdocs_to_confluence.preprocess.icons import strip_icon_shortcodes
|
|
18
|
+
from mkdocs_to_confluence.preprocess.includes import (
|
|
19
|
+
preprocess_includes,
|
|
20
|
+
strip_html_comments,
|
|
21
|
+
strip_unsupported_html,
|
|
22
|
+
)
|
|
23
|
+
from mkdocs_to_confluence.preprocess.linkdefs import (
|
|
24
|
+
collect_link_defs,
|
|
25
|
+
expand_link_refs,
|
|
26
|
+
strip_link_defs,
|
|
27
|
+
)
|
|
28
|
+
from mkdocs_to_confluence.transforms.abbrevs import apply_abbreviations
|
|
29
|
+
from mkdocs_to_confluence.transforms.assets import resolve_local_assets
|
|
30
|
+
from mkdocs_to_confluence.transforms.editlink import attach_source_url
|
|
31
|
+
from mkdocs_to_confluence.transforms.footer import build_source_footer
|
|
32
|
+
from mkdocs_to_confluence.transforms.internallinks import resolve_internal_links
|
|
33
|
+
from mkdocs_to_confluence.transforms.mermaid import DEFAULT_KROKI_URL, render_mermaid_diagrams
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def compile_page(
|
|
37
|
+
node: NavNode,
|
|
38
|
+
config: MkDocsConfig,
|
|
39
|
+
link_map: dict[str, str] | None = None,
|
|
40
|
+
*,
|
|
41
|
+
is_section_index: bool = False,
|
|
42
|
+
quiet: bool = False,
|
|
43
|
+
) -> CompileResult:
|
|
44
|
+
"""Run the full compile pipeline for one page and return a typed result."""
|
|
45
|
+
if node.source_path is None:
|
|
46
|
+
return CompileResult(xhtml="")
|
|
47
|
+
|
|
48
|
+
raw = load_page(node)
|
|
49
|
+
|
|
50
|
+
preprocessed = preprocess_includes(
|
|
51
|
+
raw,
|
|
52
|
+
source_path=node.source_path,
|
|
53
|
+
docs_dir=config.docs_dir,
|
|
54
|
+
)
|
|
55
|
+
preprocessed = strip_unsupported_html(preprocessed)
|
|
56
|
+
preprocessed = strip_html_comments(preprocessed)
|
|
57
|
+
preprocessed = strip_icon_shortcodes(preprocessed)
|
|
58
|
+
front_matter, preprocessed = extract_front_matter(preprocessed)
|
|
59
|
+
abbrevs = extract_abbreviations(preprocessed)
|
|
60
|
+
preprocessed = strip_abbreviation_defs(preprocessed)
|
|
61
|
+
link_defs = collect_link_defs(preprocessed)
|
|
62
|
+
preprocessed = expand_link_refs(preprocessed, link_defs)
|
|
63
|
+
preprocessed = strip_link_defs(preprocessed)
|
|
64
|
+
ir_nodes = parse(preprocessed)
|
|
65
|
+
if is_section_index:
|
|
66
|
+
ir_nodes = ir_nodes + (ChildrenMacro(),)
|
|
67
|
+
ir_nodes = apply_abbreviations(ir_nodes, abbrevs, page_text=preprocessed)
|
|
68
|
+
ir_nodes, attachments = resolve_local_assets(
|
|
69
|
+
ir_nodes,
|
|
70
|
+
page_path=node.source_path,
|
|
71
|
+
docs_dir=config.docs_dir,
|
|
72
|
+
)
|
|
73
|
+
mermaid_render = config.confluence.mermaid_render if config.confluence else "kroki"
|
|
74
|
+
if mermaid_render != "none":
|
|
75
|
+
kroki_url = (
|
|
76
|
+
mermaid_render[len("kroki:"):] if mermaid_render.startswith("kroki:") else DEFAULT_KROKI_URL
|
|
77
|
+
)
|
|
78
|
+
ir_nodes, mermaid_attachments = render_mermaid_diagrams(ir_nodes, kroki_url, quiet=quiet)
|
|
79
|
+
attachments = attachments + mermaid_attachments
|
|
80
|
+
effective_link_map = link_map if link_map is not None else {}
|
|
81
|
+
if node.docs_path:
|
|
82
|
+
ir_nodes = resolve_internal_links(ir_nodes, effective_link_map, node.docs_path)
|
|
83
|
+
if front_matter is not None:
|
|
84
|
+
ir_nodes = (front_matter,) + ir_nodes
|
|
85
|
+
edit_url = config.page_edit_url(node.docs_path or "")
|
|
86
|
+
site_url = config.page_site_url(node.docs_path or "")
|
|
87
|
+
if site_url:
|
|
88
|
+
ir_nodes = attach_source_url(ir_nodes, "", site_url)
|
|
89
|
+
if edit_url:
|
|
90
|
+
abs_path = str(config.docs_dir / (node.docs_path or ""))
|
|
91
|
+
footer = build_source_footer(edit_url, abs_path)
|
|
92
|
+
ir_nodes = ir_nodes + (footer,)
|
|
93
|
+
|
|
94
|
+
labels: tuple[str, ...] = ()
|
|
95
|
+
confluence_status: str | None = None
|
|
96
|
+
version_message: str | None = None
|
|
97
|
+
for node_item in ir_nodes:
|
|
98
|
+
if isinstance(node_item, FrontMatter):
|
|
99
|
+
labels = node_item.labels
|
|
100
|
+
confluence_status = node_item.confluence_status
|
|
101
|
+
if isinstance(node_item, SourceFooter) and node_item.commit_sha and node_item.commit_summary:
|
|
102
|
+
version_message = f"{node_item.commit_sha}: {node_item.commit_summary}"
|
|
103
|
+
|
|
104
|
+
return CompileResult(
|
|
105
|
+
xhtml=emit(ir_nodes),
|
|
106
|
+
attachments=attachments,
|
|
107
|
+
labels=labels,
|
|
108
|
+
confluence_status=confluence_status,
|
|
109
|
+
version_message=version_message,
|
|
110
|
+
)
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Execution helpers for the nav-driven publish pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from mkdocs_to_confluence.publisher.client import ConfluenceError
|
|
11
|
+
from mkdocs_to_confluence.publisher.models import PageAction, PublishReport
|
|
12
|
+
from mkdocs_to_confluence.transforms.assets import _make_attachment_name
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from mkdocs_to_confluence.publisher.client import ConfluenceClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _upload_assets(
|
|
19
|
+
page_id: str,
|
|
20
|
+
attachments: list[Path],
|
|
21
|
+
docs_dir: Path,
|
|
22
|
+
client: ConfluenceClient,
|
|
23
|
+
*,
|
|
24
|
+
quiet: bool = False,
|
|
25
|
+
) -> tuple[int, int, list[tuple[str, str]]]:
|
|
26
|
+
"""Upload attachments for one page sequentially."""
|
|
27
|
+
pairs = [(_make_attachment_name(p, docs_dir), p) for p in attachments]
|
|
28
|
+
uploaded = 0
|
|
29
|
+
skipped = 0
|
|
30
|
+
errors: list[tuple[str, str]] = []
|
|
31
|
+
|
|
32
|
+
# Fetch once; reuse across all uploads (read-only).
|
|
33
|
+
existing = client.list_attachments(page_id)
|
|
34
|
+
|
|
35
|
+
for name, path in pairs:
|
|
36
|
+
# Skip upload if local file is not newer than what Confluence already has.
|
|
37
|
+
if name in existing:
|
|
38
|
+
try:
|
|
39
|
+
created_at = existing[name]["version"]["createdAt"]
|
|
40
|
+
confluence_ts = datetime.fromisoformat(
|
|
41
|
+
created_at.replace("Z", "+00:00")
|
|
42
|
+
)
|
|
43
|
+
local_mtime = datetime.fromtimestamp(
|
|
44
|
+
path.stat().st_mtime, tz=timezone.utc
|
|
45
|
+
)
|
|
46
|
+
if local_mtime <= confluence_ts:
|
|
47
|
+
if not quiet:
|
|
48
|
+
print(f" skipping {name} (unchanged)")
|
|
49
|
+
skipped += 1
|
|
50
|
+
continue
|
|
51
|
+
except (KeyError, ValueError, OSError):
|
|
52
|
+
pass # can't compare — fall through to upload
|
|
53
|
+
|
|
54
|
+
if not quiet:
|
|
55
|
+
print(f" uploading {name}")
|
|
56
|
+
try:
|
|
57
|
+
client.upload_attachment(page_id, path, name, existing)
|
|
58
|
+
uploaded += 1
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
errors.append((name, str(exc)))
|
|
61
|
+
|
|
62
|
+
return uploaded, skipped, errors
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _execute_folder_action(
|
|
66
|
+
action: PageAction,
|
|
67
|
+
client: ConfluenceClient,
|
|
68
|
+
space_id: str,
|
|
69
|
+
root_page_id: str | None,
|
|
70
|
+
report: PublishReport,
|
|
71
|
+
*,
|
|
72
|
+
quiet: bool = False,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Handle folder create/find for a single folder action."""
|
|
75
|
+
if action.page_id is not None:
|
|
76
|
+
# Already found at plan time — reuse.
|
|
77
|
+
report.updated += 1
|
|
78
|
+
elif action.parent_is_folder or action.parent_id == root_page_id:
|
|
79
|
+
# Parent is a Confluence folder, or this is a top-level section
|
|
80
|
+
# directly under the configured root page — use native folder API.
|
|
81
|
+
existing_folder = None
|
|
82
|
+
if action.parent_id is not None:
|
|
83
|
+
try:
|
|
84
|
+
existing_folder = client.find_folder_under(
|
|
85
|
+
action.parent_id,
|
|
86
|
+
action.title,
|
|
87
|
+
parent_is_folder=action.parent_is_folder,
|
|
88
|
+
)
|
|
89
|
+
except Exception as find_exc:
|
|
90
|
+
print(
|
|
91
|
+
f" [warn] find_folder_under failed "
|
|
92
|
+
f"(parent_id={action.parent_id}): {find_exc}",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
if existing_folder is not None:
|
|
96
|
+
action.page_id = str(existing_folder["id"])
|
|
97
|
+
report.updated += 1
|
|
98
|
+
else:
|
|
99
|
+
folder_parent = (
|
|
100
|
+
action.parent_id if action.parent_is_folder else None
|
|
101
|
+
)
|
|
102
|
+
folder = client.create_folder(
|
|
103
|
+
space_id, action.title, parent_id=folder_parent
|
|
104
|
+
)
|
|
105
|
+
action.page_id = str(folder["id"])
|
|
106
|
+
report.created += 1
|
|
107
|
+
if not quiet:
|
|
108
|
+
print(
|
|
109
|
+
f" folder id={action.page_id}"
|
|
110
|
+
f" parent_id={action.parent_id}"
|
|
111
|
+
f" parent_is_folder={action.parent_is_folder}"
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
# Parent is a dynamically-created page (e.g. a section-index page).
|
|
115
|
+
# Confluence folders cannot be nested under pages — use a stub page.
|
|
116
|
+
action.is_folder = False
|
|
117
|
+
existing = client.find_page(space_id, action.title)
|
|
118
|
+
if existing is not None:
|
|
119
|
+
action.page_id = str(existing["id"])
|
|
120
|
+
report.updated += 1
|
|
121
|
+
else:
|
|
122
|
+
stub = client.create_page(
|
|
123
|
+
space_id, action.title, "", parent_id=action.parent_id,
|
|
124
|
+
)
|
|
125
|
+
action.page_id = str(stub["id"])
|
|
126
|
+
report.created += 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _execute_page_action(
|
|
130
|
+
action: PageAction,
|
|
131
|
+
client: ConfluenceClient,
|
|
132
|
+
space_id: str,
|
|
133
|
+
report: PublishReport,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Handle create/update (with stale fallback) for a single page action."""
|
|
136
|
+
if action.action == "create":
|
|
137
|
+
page = client.create_page(
|
|
138
|
+
space_id,
|
|
139
|
+
action.title,
|
|
140
|
+
action.xhtml or "",
|
|
141
|
+
parent_id=action.parent_id,
|
|
142
|
+
)
|
|
143
|
+
action.page_id = str(page["id"])
|
|
144
|
+
report.created += 1
|
|
145
|
+
try:
|
|
146
|
+
client.stamp_managed(action.page_id)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass # non-fatal
|
|
149
|
+
elif action.action == "update":
|
|
150
|
+
if action.page_id is None or action.version is None:
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
f"Update action for '{action.title}' is missing page_id or version"
|
|
153
|
+
)
|
|
154
|
+
try:
|
|
155
|
+
client.update_page(
|
|
156
|
+
action.page_id,
|
|
157
|
+
action.title,
|
|
158
|
+
action.xhtml or "",
|
|
159
|
+
action.version + 1,
|
|
160
|
+
parent_id=action.parent_id,
|
|
161
|
+
version_message=action.version_message,
|
|
162
|
+
)
|
|
163
|
+
report.updated += 1
|
|
164
|
+
except ConfluenceError as upd_exc:
|
|
165
|
+
err = str(upd_exc)
|
|
166
|
+
# 404 = page deleted; 400 "another space" = stale page_id
|
|
167
|
+
# from a different Confluence space. Both mean the existing
|
|
168
|
+
# page can't be updated — fall back to create a fresh one.
|
|
169
|
+
is_stale = "HTTP 404" in err or (
|
|
170
|
+
"HTTP 400" in err and "another space" in err.lower()
|
|
171
|
+
)
|
|
172
|
+
if not is_stale:
|
|
173
|
+
raise
|
|
174
|
+
print(
|
|
175
|
+
f" [warn] update failed ({err[:80].strip()}) —"
|
|
176
|
+
" stale page_id; falling back to create",
|
|
177
|
+
file=sys.stderr,
|
|
178
|
+
)
|
|
179
|
+
action.page_id = None
|
|
180
|
+
page = client.create_page(
|
|
181
|
+
space_id,
|
|
182
|
+
action.title,
|
|
183
|
+
action.xhtml or "",
|
|
184
|
+
parent_id=action.parent_id,
|
|
185
|
+
)
|
|
186
|
+
action.page_id = str(page["id"])
|
|
187
|
+
report.created += 1
|
|
188
|
+
try:
|
|
189
|
+
client.stamp_managed(action.page_id)
|
|
190
|
+
except Exception:
|
|
191
|
+
pass # non-fatal
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _wire_children(
|
|
195
|
+
action: PageAction,
|
|
196
|
+
action_by_node: dict[int, PageAction],
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Propagate a section's resolved page_id to all its direct children."""
|
|
199
|
+
if action.page_id is None:
|
|
200
|
+
return
|
|
201
|
+
for child_node in action.node.children:
|
|
202
|
+
child_action = action_by_node.get(id(child_node))
|
|
203
|
+
if child_action is not None:
|
|
204
|
+
child_action.parent_id = action.page_id
|
|
205
|
+
child_action.parent_is_folder = action.is_folder
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _apply_page_presentation(
|
|
209
|
+
action: PageAction,
|
|
210
|
+
client: ConfluenceClient,
|
|
211
|
+
*,
|
|
212
|
+
full_width: bool,
|
|
213
|
+
space_key: str | None = None,
|
|
214
|
+
suppress_full_width_errors: bool = False,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Apply page status and full-width presentation updates."""
|
|
217
|
+
if action.page_id and action.confluence_status and not action.is_folder:
|
|
218
|
+
try:
|
|
219
|
+
print(f" [status] setting '{action.confluence_status}' on page {action.page_id!r}...")
|
|
220
|
+
client.set_page_status(action.page_id, action.confluence_status, space_key=space_key)
|
|
221
|
+
print(" [status] ok")
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
# Always print status errors — user configured status explicitly
|
|
224
|
+
print(f" [warn] could not set page status '{action.confluence_status}': {exc}")
|
|
225
|
+
|
|
226
|
+
# Set full-width LAST — Confluence's state/label PUTs can reset the appearance property.
|
|
227
|
+
if full_width and action.page_id and not action.is_folder:
|
|
228
|
+
try:
|
|
229
|
+
client.set_page_full_width(action.page_id)
|
|
230
|
+
except Exception as exc:
|
|
231
|
+
if not suppress_full_width_errors:
|
|
232
|
+
print(f" [warn] could not set full-width on page {action.page_id!r}: {exc}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _post_process_action(
|
|
236
|
+
action: PageAction,
|
|
237
|
+
client: ConfluenceClient,
|
|
238
|
+
*,
|
|
239
|
+
full_width: bool,
|
|
240
|
+
docs_dir: Path,
|
|
241
|
+
report: PublishReport,
|
|
242
|
+
space_key: str | None = None,
|
|
243
|
+
quiet: bool = False,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Run all non-fatal post-create/update work for a single action."""
|
|
246
|
+
# Store content hash after create/update so the next run can skip unchanged pages.
|
|
247
|
+
if action.page_id and action.content_hash and action.action in ("create", "update"):
|
|
248
|
+
try:
|
|
249
|
+
client.set_content_hash(action.page_id, action.content_hash)
|
|
250
|
+
except Exception:
|
|
251
|
+
pass # non-fatal
|
|
252
|
+
|
|
253
|
+
# Apply labels (tags) from front matter — non-fatal on failure.
|
|
254
|
+
if action.page_id and action.labels and not action.is_folder:
|
|
255
|
+
try:
|
|
256
|
+
client.set_page_labels(action.page_id, action.labels)
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
_apply_page_presentation(
|
|
261
|
+
action,
|
|
262
|
+
client,
|
|
263
|
+
full_width=full_width,
|
|
264
|
+
space_key=space_key,
|
|
265
|
+
suppress_full_width_errors=True,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Upload assets — skip files whose mtime is not newer than Confluence.
|
|
269
|
+
if action.page_id and action.attachments:
|
|
270
|
+
uploaded, asset_skipped, asset_errors = _upload_assets(
|
|
271
|
+
action.page_id, action.attachments, docs_dir, client, quiet=quiet
|
|
272
|
+
)
|
|
273
|
+
report.assets_uploaded += uploaded
|
|
274
|
+
report.assets_skipped += asset_skipped
|
|
275
|
+
for name, msg in asset_errors:
|
|
276
|
+
report.errors.append((f"{action.title} / {name}", msg))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def execute_publish(
|
|
280
|
+
plan: list[PageAction],
|
|
281
|
+
client: ConfluenceClient,
|
|
282
|
+
*,
|
|
283
|
+
dry_run: bool = False,
|
|
284
|
+
space_id: str,
|
|
285
|
+
space_key: str | None = None,
|
|
286
|
+
docs_dir: Path,
|
|
287
|
+
full_width: bool = True,
|
|
288
|
+
root_page_id: str | None = None,
|
|
289
|
+
prune: bool = False,
|
|
290
|
+
quiet: bool = False,
|
|
291
|
+
) -> PublishReport:
|
|
292
|
+
"""Execute the publish plan."""
|
|
293
|
+
report = PublishReport()
|
|
294
|
+
|
|
295
|
+
if dry_run:
|
|
296
|
+
report.skipped = sum(1 for a in plan if a.action == "skip")
|
|
297
|
+
report.created = sum(1 for a in plan if a.action == "create")
|
|
298
|
+
report.updated = sum(1 for a in plan if a.action == "update")
|
|
299
|
+
return report
|
|
300
|
+
|
|
301
|
+
# Index: nav node id → PageAction, used to wire child parent_ids after
|
|
302
|
+
# each section is created/updated.
|
|
303
|
+
action_by_node: dict[int, PageAction] = {id(a.node): a for a in plan}
|
|
304
|
+
|
|
305
|
+
active = [a for a in plan if a.action != "skip"]
|
|
306
|
+
total = len(active)
|
|
307
|
+
if not quiet:
|
|
308
|
+
print(f"\nPublishing {total} page(s)...")
|
|
309
|
+
counter = 0
|
|
310
|
+
|
|
311
|
+
for action in plan:
|
|
312
|
+
if action.action == "skip":
|
|
313
|
+
report.skipped += 1
|
|
314
|
+
_apply_page_presentation(action, client, full_width=full_width, space_key=space_key)
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
counter += 1
|
|
318
|
+
if not quiet:
|
|
319
|
+
print(f" [{counter}/{total}] {action.action:<6} '{action.title}'")
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
if action.is_folder:
|
|
323
|
+
_execute_folder_action(action, client, space_id, root_page_id, report, quiet=quiet)
|
|
324
|
+
else:
|
|
325
|
+
_execute_page_action(action, client, space_id, report)
|
|
326
|
+
except Exception as exc:
|
|
327
|
+
report.errors.append((action.title, str(exc)))
|
|
328
|
+
# Do NOT `continue` — all post-execute blocks below are guarded by
|
|
329
|
+
# action.page_id checks, so they are safely skipped on failure.
|
|
330
|
+
# Critically, child parent_id wiring must still run for section
|
|
331
|
+
# pages whose children were planned with parent_id=None.
|
|
332
|
+
|
|
333
|
+
if action.node.is_section and action.page_id:
|
|
334
|
+
_wire_children(action, action_by_node)
|
|
335
|
+
|
|
336
|
+
_post_process_action(
|
|
337
|
+
action, client,
|
|
338
|
+
full_width=full_width, docs_dir=docs_dir, space_key=space_key, report=report, quiet=quiet,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if prune and root_page_id:
|
|
342
|
+
published_ids = {a.page_id for a in plan if a.page_id}
|
|
343
|
+
_prune_orphans(client, root_page_id, published_ids, report, quiet=quiet)
|
|
344
|
+
|
|
345
|
+
return report
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _prune_orphans(
|
|
349
|
+
client: ConfluenceClient,
|
|
350
|
+
root_page_id: str,
|
|
351
|
+
published_ids: set[str],
|
|
352
|
+
report: PublishReport,
|
|
353
|
+
*,
|
|
354
|
+
quiet: bool = False,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Delete managed descendant pages that are no longer in the publish plan."""
|
|
357
|
+
try:
|
|
358
|
+
all_descendants = client.get_descendant_ids(root_page_id)
|
|
359
|
+
except Exception as exc:
|
|
360
|
+
print(f" [warn] prune: could not fetch descendants — {exc}", file=sys.stderr)
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
orphan_candidates = [pid for pid in all_descendants if pid not in published_ids]
|
|
364
|
+
if not orphan_candidates:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
if not quiet:
|
|
368
|
+
print(f"\nPruning: checking {len(orphan_candidates)} orphan candidate(s)...")
|
|
369
|
+
for page_id in orphan_candidates:
|
|
370
|
+
try:
|
|
371
|
+
if not client.is_managed(page_id):
|
|
372
|
+
continue
|
|
373
|
+
client.delete_page(page_id)
|
|
374
|
+
report.pruned += 1
|
|
375
|
+
if not quiet:
|
|
376
|
+
print(f" deleted orphan page {page_id}")
|
|
377
|
+
except Exception as exc:
|
|
378
|
+
print(f" [warn] prune: failed to delete page {page_id} — {exc}", file=sys.stderr)
|