mkdocs2confluence 0.11.1__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.11.1/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.12.2}/PKG-INFO +9 -2
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/README.md +7 -1
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/pyproject.toml +2 -1
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2/src/mkdocs2confluence.egg-info}/PKG-INFO +9 -2
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/SOURCES.txt +7 -0
- {mkdocs2confluence-0.11.1 → 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.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/config.py +25 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/nav.py +8 -1
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/client.py +31 -54
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/executor.py +378 -0
- mkdocs2confluence-0.12.2/src/mkdocs_to_confluence/publisher/http_retry.py +50 -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.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/assets.py +4 -3
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_images.py +41 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_loader.py +70 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_publish_client.py +25 -24
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_publish_config.py +71 -0
- mkdocs2confluence-0.11.1/src/mkdocs_to_confluence/publisher/pipeline.py +0 -838
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/LICENSE +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/setup.cfg +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/cli.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/document.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/pdf/render.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/preview/server.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/command.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/comments.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/github.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/platform.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/sync/state.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_children_macro.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_cli.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_emitter.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_footer.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_mermaid.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_parser.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_pdf.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_preprocess.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_preview.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_publish_pipeline.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_server.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_sync_anchoring.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_sync_command.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_sync_comments.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_sync_github.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_sync_state.py +0 -0
- {mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/tests/test_treeutil.py +0 -0
{mkdocs2confluence-0.11.1/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.
|
|
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.
|
|
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.11.1 → 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.
|
|
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.11.1 → 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,7 +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
|
|
44
|
+
src/mkdocs_to_confluence/publisher/http_retry.py
|
|
45
|
+
src/mkdocs_to_confluence/publisher/models.py
|
|
40
46
|
src/mkdocs_to_confluence/publisher/pipeline.py
|
|
47
|
+
src/mkdocs_to_confluence/publisher/planner.py
|
|
41
48
|
src/mkdocs_to_confluence/sync/__init__.py
|
|
42
49
|
src/mkdocs_to_confluence/sync/anchoring.py
|
|
43
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
|
+
)
|
{mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/config.py
RENAMED
|
@@ -33,6 +33,7 @@ class ConfluenceConfig:
|
|
|
33
33
|
github_repo: str | None = None # "owner/repo" — required for sync-comments
|
|
34
34
|
github_token: str | None = None # GitHub PAT (falls back to GITHUB_TOKEN env var)
|
|
35
35
|
github_base_branch: str = "main" # base branch for review PRs
|
|
36
|
+
allow_any_host: bool = False # set True to allow non-Atlassian Cloud base_url hosts
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@dataclass(frozen=True)
|
|
@@ -209,6 +210,29 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
|
|
|
209
210
|
if not isinstance(base_url, str) or not base_url.strip():
|
|
210
211
|
raise ConfigError("mkdocs.yml: 'confluence.base_url' is required and must be a non-empty string.")
|
|
211
212
|
|
|
213
|
+
# Security: require HTTPS so credentials are never sent in plaintext.
|
|
214
|
+
parsed_url = urlparse(base_url.strip())
|
|
215
|
+
if parsed_url.scheme != "https":
|
|
216
|
+
raise ConfigError(
|
|
217
|
+
"mkdocs.yml: 'confluence.base_url' must use HTTPS (got "
|
|
218
|
+
f"{parsed_url.scheme!r}). Plain HTTP would transmit credentials in "
|
|
219
|
+
"cleartext."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
allow_any_host = bool(raw_conf.get("allow_any_host", False))
|
|
223
|
+
|
|
224
|
+
# Security: guard against a repo-controlled base_url redirecting credentials
|
|
225
|
+
# to an attacker host. Non-Atlassian Cloud hosts require an explicit opt-in.
|
|
226
|
+
host = parsed_url.hostname or ""
|
|
227
|
+
_is_atlassian = host == "atlassian.net" or host.endswith(".atlassian.net")
|
|
228
|
+
if not _is_atlassian and not allow_any_host:
|
|
229
|
+
raise ConfigError(
|
|
230
|
+
f"mkdocs.yml: 'confluence.base_url' host {host!r} is not an "
|
|
231
|
+
"Atlassian Cloud domain (*.atlassian.net). If you are using a "
|
|
232
|
+
"self-hosted Confluence instance, add 'allow_any_host: true' under "
|
|
233
|
+
"the 'confluence:' block to acknowledge this."
|
|
234
|
+
)
|
|
235
|
+
|
|
212
236
|
space_key: str | None = raw_conf.get("space_key") or None
|
|
213
237
|
if space_key:
|
|
214
238
|
space_key = space_key.strip() or None
|
|
@@ -248,6 +272,7 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
|
|
|
248
272
|
github_token=(str(raw_conf["github_token"]) if raw_conf.get("github_token")
|
|
249
273
|
else os.environ.get("GITHUB_TOKEN") or None),
|
|
250
274
|
github_base_branch=str(raw_conf.get("github_base_branch", "main")),
|
|
275
|
+
allow_any_host=allow_any_host,
|
|
251
276
|
)
|
|
252
277
|
|
|
253
278
|
# --- extra_css (optional) ---
|
{mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/loader/nav.py
RENAMED
|
@@ -220,7 +220,14 @@ def _traverse(nav: list[Any], docs_dir: Path, level: int, nav_file: str = ".page
|
|
|
220
220
|
elif isinstance(value, str):
|
|
221
221
|
# Could be a page file or a directory reference (awesome-pages style)
|
|
222
222
|
target = (docs_dir / value).resolve()
|
|
223
|
-
|
|
223
|
+
docs_root = docs_dir.resolve()
|
|
224
|
+
if not target.is_relative_to(docs_root):
|
|
225
|
+
warnings.warn(
|
|
226
|
+
f"Nav page '{title}' resolves outside docs_dir ('{target}') — "
|
|
227
|
+
"it will be omitted from the resolved nav.",
|
|
228
|
+
stacklevel=4,
|
|
229
|
+
)
|
|
230
|
+
elif target.is_dir():
|
|
224
231
|
children = _resolve_nav_dir(target, docs_dir, level + 1, nav_file)
|
|
225
232
|
nodes.append(
|
|
226
233
|
NavNode(
|
{mkdocs2confluence-0.11.1 → mkdocs2confluence-0.12.2}/src/mkdocs_to_confluence/publisher/client.py
RENAMED
|
@@ -7,9 +7,6 @@ Authentication is HTTP Basic with email + API token.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import base64
|
|
10
|
-
import random
|
|
11
|
-
import time
|
|
12
|
-
from collections.abc import Callable
|
|
13
10
|
from pathlib import Path
|
|
14
11
|
from typing import Any, cast
|
|
15
12
|
from urllib.parse import parse_qs, urlparse
|
|
@@ -17,9 +14,10 @@ from urllib.parse import parse_qs, urlparse
|
|
|
17
14
|
import httpx
|
|
18
15
|
|
|
19
16
|
from mkdocs_to_confluence.loader.config import ConfluenceConfig
|
|
17
|
+
from mkdocs_to_confluence.publisher.http_retry import ConfluenceError, http_request_with_retry
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
# Re-export so existing callers (`from publisher.client import ConfluenceError`) keep working.
|
|
20
|
+
__all__ = ["ConfluenceClient", "ConfluenceError"]
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
def _extract_cursor(next_url: str) -> str:
|
|
@@ -29,10 +27,6 @@ def _extract_cursor(next_url: str) -> str:
|
|
|
29
27
|
return cursors[0] if cursors else ""
|
|
30
28
|
|
|
31
29
|
|
|
32
|
-
class ConfluenceError(RuntimeError):
|
|
33
|
-
"""Raised when the Confluence API returns an unexpected response."""
|
|
34
|
-
|
|
35
|
-
|
|
36
30
|
class ConfluenceClient:
|
|
37
31
|
"""Thin HTTP wrapper around the Confluence Cloud REST API."""
|
|
38
32
|
|
|
@@ -92,35 +86,6 @@ class ConfluenceClient:
|
|
|
92
86
|
f"{context}: HTTP {response.status_code} — {response.text[:400]}"
|
|
93
87
|
)
|
|
94
88
|
|
|
95
|
-
def _request(self, fn: Callable[[], httpx.Response], context: str) -> httpx.Response:
|
|
96
|
-
"""Call *fn* and retry up to ``_MAX_RETRIES`` times on HTTP 429.
|
|
97
|
-
|
|
98
|
-
Respects the ``Retry-After`` response header (capped at
|
|
99
|
-
``_RETRY_AFTER_CAP`` seconds). Falls back to exponential backoff with
|
|
100
|
-
jitter when the header is absent or invalid. Prints a warning on each
|
|
101
|
-
retry. Raises :class:`ConfluenceError` if all retries are exhausted.
|
|
102
|
-
"""
|
|
103
|
-
for attempt in range(_MAX_RETRIES + 1):
|
|
104
|
-
resp = fn()
|
|
105
|
-
if resp.status_code != 429:
|
|
106
|
-
return resp
|
|
107
|
-
if attempt == _MAX_RETRIES:
|
|
108
|
-
raise ConfluenceError(
|
|
109
|
-
f"{context}: rate-limited after {_MAX_RETRIES} retries — giving up"
|
|
110
|
-
)
|
|
111
|
-
header = resp.headers.get("Retry-After", "")
|
|
112
|
-
try:
|
|
113
|
-
wait = min(float(header), _RETRY_AFTER_CAP)
|
|
114
|
-
except ValueError:
|
|
115
|
-
wait = min(2.0 ** attempt + random.uniform(0.0, 1.0), _RETRY_AFTER_CAP)
|
|
116
|
-
print(
|
|
117
|
-
f" ⚠ rate-limited ({context}) — retrying in {wait:.1f}s"
|
|
118
|
-
f" (attempt {attempt + 1}/{_MAX_RETRIES})",
|
|
119
|
-
flush=True,
|
|
120
|
-
)
|
|
121
|
-
time.sleep(wait)
|
|
122
|
-
raise ConfluenceError(f"{context}: rate-limited") # unreachable
|
|
123
|
-
|
|
124
89
|
# ── Space ──────────────────────────────────────────────────────────────────
|
|
125
90
|
|
|
126
91
|
def get_space_id(self, space_key: str) -> str:
|
|
@@ -228,7 +193,10 @@ class ConfluenceClient:
|
|
|
228
193
|
payload: dict[str, Any] = {"spaceId": space_id, "title": title}
|
|
229
194
|
if parent_id is not None:
|
|
230
195
|
payload["parentId"] = parent_id
|
|
231
|
-
resp =
|
|
196
|
+
resp = http_request_with_retry(
|
|
197
|
+
lambda: self._http.post(self._v2("/folders"), json=payload),
|
|
198
|
+
f"create_folder({title!r})",
|
|
199
|
+
)
|
|
232
200
|
if resp.status_code == 400:
|
|
233
201
|
body = resp.text
|
|
234
202
|
if "folder exists with the same title" in body.lower() or "same title" in body.lower():
|
|
@@ -278,7 +246,10 @@ class ConfluenceClient:
|
|
|
278
246
|
if parent_id is not None:
|
|
279
247
|
payload["parentId"] = parent_id
|
|
280
248
|
|
|
281
|
-
resp =
|
|
249
|
+
resp = http_request_with_retry(
|
|
250
|
+
lambda: self._http.post(self._v2("/pages"), json=payload),
|
|
251
|
+
f"create_page({title!r})",
|
|
252
|
+
)
|
|
282
253
|
self._raise_for_status(resp, f"create_page({title!r})")
|
|
283
254
|
return resp.json() # type: ignore[no-any-return]
|
|
284
255
|
|
|
@@ -315,7 +286,7 @@ class ConfluenceClient:
|
|
|
315
286
|
}
|
|
316
287
|
if parent_id is not None:
|
|
317
288
|
payload["parentId"] = parent_id
|
|
318
|
-
resp =
|
|
289
|
+
resp = http_request_with_retry(
|
|
319
290
|
lambda: self._http.put(self._v2(f"/pages/{page_id}"), json=payload),
|
|
320
291
|
f"update_page({page_id!r})",
|
|
321
292
|
)
|
|
@@ -335,7 +306,7 @@ class ConfluenceClient:
|
|
|
335
306
|
|
|
336
307
|
if get_resp.status_code == 200:
|
|
337
308
|
current_version = get_resp.json().get("version", {}).get("number", 1)
|
|
338
|
-
|
|
309
|
+
http_request_with_retry(
|
|
339
310
|
lambda: self._http.put(
|
|
340
311
|
prop_url,
|
|
341
312
|
json={"key": key, "value": "full-width", "version": {"number": current_version + 1}},
|
|
@@ -343,7 +314,7 @@ class ConfluenceClient:
|
|
|
343
314
|
"set_page_full_width",
|
|
344
315
|
)
|
|
345
316
|
else:
|
|
346
|
-
|
|
317
|
+
http_request_with_retry(
|
|
347
318
|
lambda: self._http.post(
|
|
348
319
|
self._v1(f"/content/{page_id}/property"),
|
|
349
320
|
json={"key": key, "value": "full-width", "version": {"number": 1}},
|
|
@@ -376,7 +347,7 @@ class ConfluenceClient:
|
|
|
376
347
|
get_resp = self._http.get(prop_url)
|
|
377
348
|
if get_resp.status_code == 200:
|
|
378
349
|
current_version = get_resp.json().get("version", {}).get("number", 1)
|
|
379
|
-
|
|
350
|
+
http_request_with_retry(
|
|
380
351
|
lambda: self._http.put(
|
|
381
352
|
prop_url,
|
|
382
353
|
json={"key": key, "value": hash_str, "version": {"number": current_version + 1}},
|
|
@@ -384,7 +355,7 @@ class ConfluenceClient:
|
|
|
384
355
|
"set_content_hash",
|
|
385
356
|
)
|
|
386
357
|
else:
|
|
387
|
-
|
|
358
|
+
http_request_with_retry(
|
|
388
359
|
lambda: self._http.post(
|
|
389
360
|
self._v1(f"/content/{page_id}/property"),
|
|
390
361
|
json={"key": key, "value": hash_str, "version": {"number": 1}},
|
|
@@ -406,7 +377,7 @@ class ConfluenceClient:
|
|
|
406
377
|
for lbl in existing_resp.json().get("results", []):
|
|
407
378
|
name = lbl.get("name", "")
|
|
408
379
|
if name:
|
|
409
|
-
|
|
380
|
+
http_request_with_retry(
|
|
410
381
|
lambda: self._http.delete(label_url, params={"name": name}),
|
|
411
382
|
f"set_page_labels({page_id!r})",
|
|
412
383
|
)
|
|
@@ -414,7 +385,10 @@ class ConfluenceClient:
|
|
|
414
385
|
# Apply new labels (if any)
|
|
415
386
|
if labels:
|
|
416
387
|
payload = [{"prefix": "global", "name": lbl} for lbl in labels]
|
|
417
|
-
resp =
|
|
388
|
+
resp = http_request_with_retry(
|
|
389
|
+
lambda: self._http.post(label_url, json=payload),
|
|
390
|
+
f"set_page_labels({page_id!r})",
|
|
391
|
+
)
|
|
418
392
|
self._raise_for_status(resp, f"set_page_labels({page_id!r})")
|
|
419
393
|
|
|
420
394
|
def set_page_status(self, page_id: str, status_key: str, space_key: str | None = None) -> None:
|
|
@@ -461,7 +435,7 @@ class ConfluenceClient:
|
|
|
461
435
|
|
|
462
436
|
# `status` query param tells the API which page version to target (current vs draft).
|
|
463
437
|
url = self._v1(f"/content/{page_id}/state")
|
|
464
|
-
resp =
|
|
438
|
+
resp = http_request_with_retry(
|
|
465
439
|
lambda: self._http.put(url, json=body, params={"status": "current"}),
|
|
466
440
|
f"set_page_status({page_id!r}, {status_key!r})",
|
|
467
441
|
)
|
|
@@ -533,7 +507,7 @@ class ConfluenceClient:
|
|
|
533
507
|
else:
|
|
534
508
|
url = self._v1(f"/content/{page_id}/child/attachment")
|
|
535
509
|
|
|
536
|
-
resp =
|
|
510
|
+
resp = http_request_with_retry(
|
|
537
511
|
lambda: self._http.post(
|
|
538
512
|
url,
|
|
539
513
|
files={"file": (filename, content)},
|
|
@@ -559,7 +533,7 @@ class ConfluenceClient:
|
|
|
559
533
|
get_resp = self._http.get(url)
|
|
560
534
|
if get_resp.status_code == 200:
|
|
561
535
|
return # already stamped
|
|
562
|
-
|
|
536
|
+
http_request_with_retry(
|
|
563
537
|
lambda: self._http.post(
|
|
564
538
|
self._v2(f"/pages/{page_id}/properties"),
|
|
565
539
|
json={"key": "mk2conf-managed", "value": True},
|
|
@@ -599,7 +573,10 @@ class ConfluenceClient:
|
|
|
599
573
|
|
|
600
574
|
def delete_page(self, page_id: str) -> None:
|
|
601
575
|
"""Permanently delete *page_id* from Confluence."""
|
|
602
|
-
resp =
|
|
576
|
+
resp = http_request_with_retry(
|
|
577
|
+
lambda: self._http.delete(self._v2(f"/pages/{page_id}")),
|
|
578
|
+
f"delete_page({page_id!r})",
|
|
579
|
+
)
|
|
603
580
|
self._raise_for_status(resp, f"delete_page({page_id!r})")
|
|
604
581
|
|
|
605
582
|
# ── Comments ───────────────────────────────────────────────────────────────
|
|
@@ -641,7 +618,7 @@ class ConfluenceClient:
|
|
|
641
618
|
def add_comment_reply(self, comment_id: str, reply_text: str) -> None:
|
|
642
619
|
"""Post a reply to *comment_id* using the v1 content API."""
|
|
643
620
|
url = self._v1(f"/content/{comment_id}/child/comment")
|
|
644
|
-
resp =
|
|
621
|
+
resp = http_request_with_retry(lambda: self._http.post(url, json={
|
|
645
622
|
"type": "comment",
|
|
646
623
|
"body": {
|
|
647
624
|
"storage": {
|
|
@@ -660,7 +637,7 @@ class ConfluenceClient:
|
|
|
660
637
|
data = get_resp.json()
|
|
661
638
|
version = data.get("version", {}).get("number", 1)
|
|
662
639
|
body_value = data.get("body", {}).get("storage", {}).get("value", "")
|
|
663
|
-
resp =
|
|
640
|
+
resp = http_request_with_retry(lambda: self._http.put(url, json={
|
|
664
641
|
"version": {"number": version + 1},
|
|
665
642
|
"resolved": True,
|
|
666
643
|
"body": {"representation": "storage", "value": body_value},
|
|
@@ -675,7 +652,7 @@ class ConfluenceClient:
|
|
|
675
652
|
data = get_resp.json()
|
|
676
653
|
version = data.get("version", {}).get("number", 1)
|
|
677
654
|
body_value = data.get("body", {}).get("storage", {}).get("value", "")
|
|
678
|
-
resp =
|
|
655
|
+
resp = http_request_with_retry(lambda: self._http.put(url, json={
|
|
679
656
|
"version": {"number": version + 1},
|
|
680
657
|
"resolved": True,
|
|
681
658
|
"body": {"representation": "storage", "value": body_value},
|