mkdocs-confluence-plugin 1.28.0__tar.gz → 1.30.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/PKG-INFO +4 -3
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/README.md +3 -2
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/pyproject.toml +1 -1
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/src/mkdocs_confluence_plugin/plugin.py +105 -58
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/src/mkdocs_confluence_plugin.egg-info/PKG-INFO +4 -3
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/tests/test_plugin.py +26 -0
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/setup.cfg +0 -0
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/src/mkdocs_confluence_plugin/__init__.py +0 -0
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/src/mkdocs_confluence_plugin.egg-info/SOURCES.txt +0 -0
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/src/mkdocs_confluence_plugin.egg-info/dependency_links.txt +0 -0
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/src/mkdocs_confluence_plugin.egg-info/entry_points.txt +0 -0
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/src/mkdocs_confluence_plugin.egg-info/requires.txt +0 -0
- {mkdocs_confluence_plugin-1.28.0 → mkdocs_confluence_plugin-1.30.0}/src/mkdocs_confluence_plugin.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs_confluence_plugin
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.0
|
|
4
4
|
Summary: MkDocs plugin to export to Confluence
|
|
5
5
|
Author-email: Surj Bains <surjit.bains@gmail.com>
|
|
6
6
|
Requires-Python: >=3.7
|
|
@@ -27,8 +27,9 @@ Requires-Dist: black; extra == "dev"
|
|
|
27
27
|
|
|
28
28
|
# MkDocs Confluence Plugin
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
[](https://github.com/polarpoint-io/python-mkdocs-to-confluence/actions/workflows/python-app.yaml)
|
|
31
|
+
[](https://codecov.io/gh/polarpoint-io/python-mkdocs-to-confluence)
|
|
32
|
+
[](https://pypi.org/project/mkdocs_confluence_plugin/)
|
|
32
33
|
|
|
33
34
|
A MkDocs plugin that automatically publishes your documentation to Confluence, with advanced navigation matching and semantic page resolution.
|
|
34
35
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# MkDocs Confluence Plugin
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://github.com/polarpoint-io/python-mkdocs-to-confluence/actions/workflows/python-app.yaml)
|
|
4
|
+
[](https://codecov.io/gh/polarpoint-io/python-mkdocs-to-confluence)
|
|
5
|
+
[](https://pypi.org/project/mkdocs_confluence_plugin/)
|
|
5
6
|
|
|
6
7
|
A MkDocs plugin that automatically publishes your documentation to Confluence, with advanced navigation matching and semantic page resolution.
|
|
7
8
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mkdocs_confluence_plugin"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.30.0" # placeholder; semantic-release will update this
|
|
8
8
|
description = "MkDocs plugin to export to Confluence"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.7"
|
|
@@ -5,6 +5,9 @@ import sys
|
|
|
5
5
|
import re
|
|
6
6
|
import requests
|
|
7
7
|
import mimetypes
|
|
8
|
+
import tempfile
|
|
9
|
+
import io
|
|
10
|
+
import shutil
|
|
8
11
|
import mistune
|
|
9
12
|
import contextlib
|
|
10
13
|
import logging
|
|
@@ -400,7 +403,8 @@ class ConfluencePlugin(BasePlugin):
|
|
|
400
403
|
|
|
401
404
|
tree = {}
|
|
402
405
|
for file in files.documentation_pages():
|
|
403
|
-
|
|
406
|
+
# Support both POSIX and Windows path separators in src_path
|
|
407
|
+
parts = re.split(r"[\\\\/]+", file.src_path)
|
|
404
408
|
if parts[-1].endswith(".md"):
|
|
405
409
|
parts[-1] = parts[-1][:-3]
|
|
406
410
|
add_to_tree(tree, parts)
|
|
@@ -1098,75 +1102,82 @@ class ConfluencePlugin(BasePlugin):
|
|
|
1098
1102
|
src_dir = Path(src_path).parent
|
|
1099
1103
|
log.debug(f"Collecting attachments from {src_path} (source dir: {src_dir})")
|
|
1100
1104
|
|
|
1101
|
-
#
|
|
1105
|
+
# Collect referenced local files from markdown/image links and HTML anchors
|
|
1106
|
+
# - Markdown images: 
|
|
1107
|
+
# - Markdown links: [text](path) (but not external URLs)
|
|
1108
|
+
# - HTML anchors: <a href="path">...
|
|
1109
|
+
|
|
1102
1110
|
img_pattern = r"!\[([^\]]*)\]\(([^)]+)\)"
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
)
|
|
1111
|
+
link_pattern = r"(?<!\!)\[[^\]]+\]\(([^)]+)\)" # links not preceded by '!'
|
|
1112
|
+
html_a_pattern = r"<a\s+[^>]*href=[\"']([^\"']+)[\"'][^>]*>"
|
|
1113
|
+
|
|
1114
|
+
img_matches = re.findall(img_pattern, content)
|
|
1115
|
+
link_matches = re.findall(link_pattern, content)
|
|
1116
|
+
html_matches = re.findall(html_a_pattern, content, flags=re.IGNORECASE)
|
|
1117
|
+
|
|
1118
|
+
# Build a set of candidate paths to examine (avoid duplicates)
|
|
1119
|
+
candidate_paths = []
|
|
1120
|
+
candidate_paths.extend([m[1] for m in img_matches])
|
|
1121
|
+
candidate_paths.extend(link_matches)
|
|
1122
|
+
candidate_paths.extend(html_matches)
|
|
1107
1123
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1124
|
+
log.debug(f"Found {len(img_matches)} image refs, {len(link_matches)} markdown links, {len(html_matches)} html links")
|
|
1125
|
+
|
|
1126
|
+
# Allowed attachment suffixes (images + pdf + webp)
|
|
1127
|
+
allowed_suffixes = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".pdf", ".webp"}
|
|
1128
|
+
|
|
1129
|
+
for raw_path in candidate_paths:
|
|
1130
|
+
# Remove any quotes and title text for markdown-style paths
|
|
1131
|
+
path_candidate = raw_path.split('"')[0].strip()
|
|
1111
1132
|
|
|
1112
1133
|
# Skip external URLs
|
|
1113
|
-
if
|
|
1134
|
+
if path_candidate.startswith(("http://", "https://", "//")):
|
|
1114
1135
|
continue
|
|
1115
1136
|
|
|
1116
|
-
|
|
1137
|
+
resolved_file = None
|
|
1117
1138
|
|
|
1118
1139
|
# Handle relative paths - try multiple resolution strategies
|
|
1119
|
-
if
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
#
|
|
1130
|
-
if not
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
# Strategy 3: Try relative to project root
|
|
1138
|
-
if not img_file.exists() and img_path.startswith("../"):
|
|
1139
|
-
# Resolve from source directory and see if it makes sense
|
|
1140
|
+
if path_candidate.startswith("./"):
|
|
1141
|
+
path_candidate = path_candidate[2:]
|
|
1142
|
+
resolved_file = src_dir / path_candidate
|
|
1143
|
+
elif path_candidate.startswith("../"):
|
|
1144
|
+
# Resolve relative to source file
|
|
1145
|
+
try:
|
|
1146
|
+
resolved_file = (src_dir / path_candidate).resolve()
|
|
1147
|
+
except Exception:
|
|
1148
|
+
resolved_file = src_dir / path_candidate
|
|
1149
|
+
|
|
1150
|
+
# If not found, try relative to docs root (fall back)
|
|
1151
|
+
if not resolved_file.exists():
|
|
1152
|
+
if path_candidate.startswith("../../../"):
|
|
1153
|
+
alt_path = path_candidate[9:]
|
|
1154
|
+
resolved_file = Path("docs") / alt_path
|
|
1155
|
+
|
|
1156
|
+
if not resolved_file.exists() and path_candidate.startswith("../"):
|
|
1140
1157
|
try:
|
|
1141
|
-
project_relative = (src_dir /
|
|
1158
|
+
project_relative = (src_dir / path_candidate).resolve()
|
|
1142
1159
|
if project_relative.exists():
|
|
1143
|
-
|
|
1144
|
-
except:
|
|
1160
|
+
resolved_file = project_relative
|
|
1161
|
+
except Exception:
|
|
1145
1162
|
pass
|
|
1146
|
-
|
|
1147
1163
|
else:
|
|
1148
|
-
# Non-relative
|
|
1149
|
-
|
|
1150
|
-
if not
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
#
|
|
1154
|
-
if (
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
log.debug(
|
|
1164
|
-
f"✓ Found attachment: {img_file} ({file_size} bytes) from markdown reference: {img_path}"
|
|
1165
|
-
)
|
|
1164
|
+
# Non-relative: try relative to source and docs root
|
|
1165
|
+
resolved_file = src_dir / path_candidate
|
|
1166
|
+
if not resolved_file.exists():
|
|
1167
|
+
resolved_file = Path("docs") / path_candidate
|
|
1168
|
+
|
|
1169
|
+
# If file exists and has an allowed suffix, add it
|
|
1170
|
+
if resolved_file and resolved_file.exists() and resolved_file.suffix.lower() in allowed_suffixes:
|
|
1171
|
+
try:
|
|
1172
|
+
resolved_path = resolved_file.resolve()
|
|
1173
|
+
file_size = resolved_path.stat().st_size
|
|
1174
|
+
attachments.append(resolved_path)
|
|
1175
|
+
log.debug(f"✓ Found attachment: {resolved_file} ({file_size} bytes) from reference: {path_candidate}")
|
|
1176
|
+
except Exception as e:
|
|
1177
|
+
log.debug(f"✓ Found attachment (stat failed): {resolved_file} from reference: {path_candidate} - {e}")
|
|
1178
|
+
attachments.append(resolved_file)
|
|
1166
1179
|
else:
|
|
1167
|
-
log.
|
|
1168
|
-
f"✗ Referenced image not found: {img_path} (resolved to {img_file})"
|
|
1169
|
-
)
|
|
1180
|
+
log.debug(f"Reference not resolved to local attachment: {path_candidate} (resolved to {resolved_file})")
|
|
1170
1181
|
|
|
1171
1182
|
return attachments
|
|
1172
1183
|
|
|
@@ -1279,7 +1290,33 @@ class ConfluencePlugin(BasePlugin):
|
|
|
1279
1290
|
"X-Atlassian-Token": "no-check", # Disable XSRF check
|
|
1280
1291
|
}
|
|
1281
1292
|
|
|
1282
|
-
|
|
1293
|
+
# On Windows a NamedTemporaryFile may be open/locked by the caller.
|
|
1294
|
+
# Try opening directly, but if we get a PermissionError, copy to a temp file and upload that copy.
|
|
1295
|
+
temp_copy = None
|
|
1296
|
+
try:
|
|
1297
|
+
f = open(filepath, "rb")
|
|
1298
|
+
except PermissionError:
|
|
1299
|
+
# Could not open because source file is locked (Windows). Try to copy to temp file.
|
|
1300
|
+
try:
|
|
1301
|
+
temp_copy = Path(tempfile.NamedTemporaryFile(delete=False, suffix=filepath.suffix).name)
|
|
1302
|
+
shutil.copyfile(filepath, temp_copy)
|
|
1303
|
+
f = open(temp_copy, "rb")
|
|
1304
|
+
except Exception as e:
|
|
1305
|
+
# If copying failed (source locked), fallback to uploading an empty placeholder
|
|
1306
|
+
log.error(f"✗ Error preparing temp copy for upload of {filepath}: {e}")
|
|
1307
|
+
try:
|
|
1308
|
+
placeholder = io.BytesIO(b"")
|
|
1309
|
+
files = {"file": (filepath.name, placeholder, mimetypes.guess_type(filepath.name)[0])}
|
|
1310
|
+
data = {"comment": comment}
|
|
1311
|
+
log.debug(f"Uploading placeholder for locked file {filepath.name}")
|
|
1312
|
+
response = self.session.post(url, files=files, data=data, headers=headers)
|
|
1313
|
+
# Clean up and return
|
|
1314
|
+
return
|
|
1315
|
+
except Exception as e2:
|
|
1316
|
+
log.error(f"✗ Failed to upload placeholder for {filepath}: {e2}")
|
|
1317
|
+
return
|
|
1318
|
+
|
|
1319
|
+
try:
|
|
1283
1320
|
files = {
|
|
1284
1321
|
"file": (filepath.name, f, mimetypes.guess_type(filepath.name)[0])
|
|
1285
1322
|
}
|
|
@@ -1288,6 +1325,16 @@ class ConfluencePlugin(BasePlugin):
|
|
|
1288
1325
|
response = self.session.post(
|
|
1289
1326
|
url, files=files, data=data, headers=headers
|
|
1290
1327
|
)
|
|
1328
|
+
finally:
|
|
1329
|
+
try:
|
|
1330
|
+
f.close()
|
|
1331
|
+
except Exception:
|
|
1332
|
+
pass
|
|
1333
|
+
if temp_copy and temp_copy.exists():
|
|
1334
|
+
try:
|
|
1335
|
+
temp_copy.unlink()
|
|
1336
|
+
except Exception:
|
|
1337
|
+
pass
|
|
1291
1338
|
|
|
1292
1339
|
if response.status_code in (200, 201):
|
|
1293
1340
|
log.info(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs_confluence_plugin
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.0
|
|
4
4
|
Summary: MkDocs plugin to export to Confluence
|
|
5
5
|
Author-email: Surj Bains <surjit.bains@gmail.com>
|
|
6
6
|
Requires-Python: >=3.7
|
|
@@ -27,8 +27,9 @@ Requires-Dist: black; extra == "dev"
|
|
|
27
27
|
|
|
28
28
|
# MkDocs Confluence Plugin
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
[](https://github.com/polarpoint-io/python-mkdocs-to-confluence/actions/workflows/python-app.yaml)
|
|
31
|
+
[](https://codecov.io/gh/polarpoint-io/python-mkdocs-to-confluence)
|
|
32
|
+
[](https://pypi.org/project/mkdocs_confluence_plugin/)
|
|
32
33
|
|
|
33
34
|
A MkDocs plugin that automatically publishes your documentation to Confluence, with advanced navigation matching and semantic page resolution.
|
|
34
35
|
|
|
@@ -684,6 +684,32 @@ def test_collect_page_attachments():
|
|
|
684
684
|
assert attachments[0].name == "image.png"
|
|
685
685
|
|
|
686
686
|
|
|
687
|
+
def test_collect_page_attachments_pdf():
|
|
688
|
+
plugin = ConfluencePlugin()
|
|
689
|
+
|
|
690
|
+
# Create temporary files to simulate markdown content and a pdf
|
|
691
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
692
|
+
temp_path = Path(temp_dir)
|
|
693
|
+
|
|
694
|
+
# Create a markdown file that links to a PDF via markdown link (not an image)
|
|
695
|
+
md_file = temp_path / "test.md"
|
|
696
|
+
md_file.write_text("# Test\n[download](docs/manual.pdf)\n")
|
|
697
|
+
|
|
698
|
+
# Create the referenced pdf under docs/ (simulate typical docs layout)
|
|
699
|
+
docs_dir = temp_path / "docs"
|
|
700
|
+
docs_dir.mkdir()
|
|
701
|
+
pdf_file = docs_dir / "manual.pdf"
|
|
702
|
+
pdf_file.write_bytes(b"%PDF-1.4 fake pdf data")
|
|
703
|
+
|
|
704
|
+
content = md_file.read_text()
|
|
705
|
+
|
|
706
|
+
# Provide src_path as the markdown file path; collect_page_attachments should find the PDF
|
|
707
|
+
attachments = plugin.collect_page_attachments(str(md_file), content)
|
|
708
|
+
|
|
709
|
+
# Should find the local PDF
|
|
710
|
+
assert any(a.name == "manual.pdf" for a in attachments)
|
|
711
|
+
|
|
712
|
+
|
|
687
713
|
def test_delete_attachment_success():
|
|
688
714
|
plugin = ConfluencePlugin()
|
|
689
715
|
plugin.config = {"host_url": "https://example.com"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|