markdown-to-confluence 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2022 Levente Hunyadi
3
+ Copyright (c) 2022-2023 Levente Hunyadi
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,24 +1,31 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: markdown-to-confluence
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Home-page: https://github.com/hunyadi/md2conf
6
6
  Author: Levente Hunyadi
7
7
  Author-email: hunyadi@gmail.com
8
8
  License: MIT
9
+ Classifier: Development Status :: 5 - Production/Stable
9
10
  Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
10
12
  Classifier: License :: OSI Approved :: MIT License
11
13
  Classifier: Operating System :: OS Independent
12
14
  Classifier: Programming Language :: Python :: 3
13
15
  Classifier: Programming Language :: Python :: 3.8
14
16
  Classifier: Programming Language :: Python :: 3.9
15
17
  Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.8
16
21
  Description-Content-Type: text/markdown
17
22
  License-File: LICENSE
18
- Requires-Dist: lxml
19
- Requires-Dist: markdown
20
- Requires-Dist: pymdown-extensions
21
- Requires-Dist: requests
23
+ Requires-Dist: lxml (>=4.9)
24
+ Requires-Dist: markdown (>=3.4)
25
+ Requires-Dist: pymdown-extensions (>=9.11)
26
+ Requires-Dist: requests (>=2.28)
27
+ Requires-Dist: types-markdown (>=3.4)
28
+ Requires-Dist: types-requests (>=2.28)
22
29
 
23
30
  # Publish Markdown files to Confluence wiki
24
31
 
@@ -43,7 +50,7 @@ This Python package
43
50
  ## Getting started
44
51
 
45
52
  In order to get started, you will need
46
- * your organization URL (e.g. `https://instructure.atlassian.net`),
53
+ * your organization domain name (e.g. `instructure.atlassian.net`),
47
54
  * your Confluence username (e.g. `levente.hunyadi@instructure.com`),
48
55
  * a Confluence API token (a string of alphanumeric characters), and
49
56
  * the space key in Confluence (e.g. `DAP`) you are publishing content to.
@@ -0,0 +1,12 @@
1
+ md2conf/__init__.py,sha256=avTS3zopvm51d_SksbxXSfH7QHEO0o_zJOTEHLwC8c4,402
2
+ md2conf/__main__.py,sha256=jvYGy1ZaYCzyryOtiWMHtTdQ1LWwTW7GzqorRFCfrvs,2354
3
+ md2conf/api.py,sha256=eew86DhTOyaEJ2jm2K3wiQtnwcyIT2RM55qUlMIXGAM,11273
4
+ md2conf/application.py,sha256=w-wTKA-26L_RRk3QzaH39p4SsmV5XWzUhbYbGZ3qDoc,864
5
+ md2conf/converter.py,sha256=eWg1EeKjHA-rSInGwulUc39miOJxmSII-anEZ8QZ1OY,9018
6
+ md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ markdown_to_confluence-0.1.6.dist-info/LICENSE,sha256=W_F6JttWaUq8-m1T7FrjMU0PYCTHYC-H4DUaPNZ7YGc,1077
8
+ markdown_to_confluence-0.1.6.dist-info/METADATA,sha256=i_6t2hO9l6Evzfm3s-0aZIDhBLuJAIjR1y8YEFTSK90,3956
9
+ markdown_to_confluence-0.1.6.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
10
+ markdown_to_confluence-0.1.6.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
11
+ markdown_to_confluence-0.1.6.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
12
+ markdown_to_confluence-0.1.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.38.4)
2
+ Generator: bdist_wheel (0.40.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
md2conf/__init__.py CHANGED
@@ -1 +1,13 @@
1
- __version__ = "0.1.4"
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Parses Markdown files, converts Markdown content into the Confluence Storage Format (XHTML), and invokes
5
+ Confluence API endpoints to upload images and content.
6
+ """
7
+
8
+ __version__ = "0.1.6"
9
+ __author__ = "Levente Hunyadi"
10
+ __copyright__ = "Copyright 2022-2023, Levente Hunyadi"
11
+ __license__ = "MIT"
12
+ __maintainer__ = "Levente Hunyadi"
13
+ __status__ = "Production"
md2conf/__main__.py CHANGED
@@ -1,9 +1,13 @@
1
1
  import argparse
2
2
  import logging
3
3
  import os.path
4
+ import sys
5
+
6
+ import requests
4
7
 
5
8
  from .api import ConfluenceAPI
6
9
  from .application import synchronize_page
10
+ from .converter import ConfluenceDocumentOptions
7
11
 
8
12
 
9
13
  class Arguments(argparse.Namespace):
@@ -13,6 +17,7 @@ class Arguments(argparse.Namespace):
13
17
  apikey: str
14
18
  space: str
15
19
  loglevel: str
20
+ generated_by: bool
16
21
 
17
22
 
18
23
  parser = argparse.ArgumentParser()
@@ -46,6 +51,18 @@ parser.add_argument(
46
51
  default=logging.getLevelName(logging.INFO),
47
52
  help="Use this option to set the log verbosity.",
48
53
  )
54
+ parser.add_argument(
55
+ "--generated-by",
56
+ action="store_true",
57
+ help="Add 'generated by a tool' prompt to pages.",
58
+ )
59
+ parser.add_argument(
60
+ "--no-generated-by",
61
+ dest="generated_by",
62
+ action="store_false",
63
+ help="Do not add 'generated by a tool' prompt to pages.",
64
+ )
65
+ parser.set_defaults(generated_by=True)
49
66
 
50
67
  args = Arguments()
51
68
  parser.parse_args(namespace=args)
@@ -55,5 +72,17 @@ logging.basicConfig(
55
72
  format="%(asctime)s - %(levelname)s - %(funcName)s [%(lineno)d] - %(message)s",
56
73
  )
57
74
 
58
- with ConfluenceAPI(args.domain, args.username, args.apikey, args.space) as api:
59
- synchronize_page(api, args.mdfile)
75
+ try:
76
+ with ConfluenceAPI(args.domain, args.username, args.apikey, args.space) as api:
77
+ synchronize_page(api, args.mdfile, ConfluenceDocumentOptions(args.generated_by))
78
+ except requests.exceptions.HTTPError as err:
79
+ logging.error(err)
80
+
81
+ # print details for a response with JSON body
82
+ try:
83
+ response: requests.Response = err.response
84
+ logging.error(response.json())
85
+ except requests.exceptions.JSONDecodeError:
86
+ pass
87
+
88
+ sys.exit(1)
md2conf/api.py CHANGED
@@ -3,20 +3,34 @@ import logging
3
3
  import mimetypes
4
4
  import os
5
5
  import os.path
6
- import urllib.parse
6
+ import sys
7
+ import typing
7
8
  from contextlib import contextmanager
8
9
  from dataclasses import dataclass
9
- from typing import Dict, Generator
10
+ from types import TracebackType
11
+ from typing import Dict, Generator, List, Optional, Type, Union
12
+ from urllib.parse import urlencode, urlparse, urlunparse
10
13
 
11
14
  import requests
12
15
 
13
16
  from .converter import ParseError, sanitize_confluence
14
17
 
18
+ # a JSON type with possible `null` values
19
+ JsonType = Union[
20
+ None,
21
+ bool,
22
+ int,
23
+ float,
24
+ str,
25
+ Dict[str, "JsonType"],
26
+ List["JsonType"],
27
+ ]
15
28
 
16
- def build_url(base_url: str, query: Dict[str, str] = None):
29
+
30
+ def build_url(base_url: str, query: Optional[Dict[str, str]] = None) -> str:
17
31
  "Builds a URL with scheme, host, port, path and query string parameters."
18
32
 
19
- scheme, netloc, path, params, query_str, fragment = urllib.parse.urlparse(base_url)
33
+ scheme, netloc, path, params, query_str, fragment = urlparse(base_url)
20
34
 
21
35
  if params:
22
36
  raise ValueError("expected: url with no parameters")
@@ -25,9 +39,26 @@ def build_url(base_url: str, query: Dict[str, str] = None):
25
39
  if fragment:
26
40
  raise ValueError("expected: url with no fragment")
27
41
 
28
- query_str = urllib.parse.urlencode(query) if query else None
29
- url_parts = (scheme, netloc, path, None, query_str, None)
30
- return urllib.parse.urlunparse(url_parts)
42
+ url_parts = (scheme, netloc, path, None, urlencode(query) if query else None, None)
43
+ return urlunparse(url_parts)
44
+
45
+
46
+ if sys.version_info >= (3, 9):
47
+
48
+ def removeprefix(string: str, prefix: str) -> str:
49
+ "If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
50
+
51
+ return string.removeprefix(prefix)
52
+
53
+ else:
54
+
55
+ def removeprefix(string: str, prefix: str) -> str:
56
+ "If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
57
+
58
+ if string.startswith(prefix):
59
+ return string[len(prefix) :]
60
+ else:
61
+ return string
31
62
 
32
63
 
33
64
  LOGGER = logging.getLogger(__name__)
@@ -59,36 +90,54 @@ class ConfluenceAPI:
59
90
  user_name: str
60
91
  api_key: str
61
92
 
62
- session: "ConfluenceSession"
93
+ session: Optional["ConfluenceSession"] = None
63
94
 
64
95
  def __init__(
65
96
  self,
66
- domain: str = None,
67
- user_name: str = None,
68
- api_key: str = None,
69
- space_key: str = None,
97
+ domain: Optional[str] = None,
98
+ user_name: Optional[str] = None,
99
+ api_key: Optional[str] = None,
100
+ space_key: Optional[str] = None,
70
101
  ) -> None:
71
- self.domain = domain or os.getenv("CONFLUENCE_DOMAIN")
72
- self.user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
73
- self.api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
74
- self.space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
102
+ opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
103
+ opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
104
+ opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
105
+ opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
75
106
 
76
- if not self.domain:
107
+ if not opt_domain:
77
108
  raise ConfluenceError("Confluence domain not specified")
78
- if not self.user_name:
109
+ if not opt_user_name:
79
110
  raise ConfluenceError("Confluence user name not specified")
80
- if not self.api_key:
111
+ if not opt_api_key:
81
112
  raise ConfluenceError("Confluence API key not specified")
113
+ if not opt_space_key:
114
+ raise ConfluenceError("Confluence space key not specified")
82
115
 
83
- def __enter__(self):
116
+ if opt_domain.startswith(("http://", "https://")):
117
+ raise ConfluenceError(
118
+ "Confluence domain looks like a URL; only host name required"
119
+ )
120
+
121
+ self.domain = opt_domain
122
+ self.user_name = opt_user_name
123
+ self.api_key = opt_api_key
124
+ self.space_key = opt_space_key
125
+
126
+ def __enter__(self) -> "ConfluenceSession":
84
127
  session = requests.Session()
85
128
  session.auth = (self.user_name, self.api_key)
86
129
  self.session = ConfluenceSession(session, self.domain, self.space_key)
87
130
  return self.session
88
131
 
89
- def __exit__(self, type, value, traceback):
90
- self.session.close()
91
- self.session = None
132
+ def __exit__(
133
+ self,
134
+ exc_type: Optional[Type[BaseException]],
135
+ exc_val: Optional[BaseException],
136
+ exc_tb: Optional[TracebackType],
137
+ ) -> None:
138
+ if self.session is not None:
139
+ self.session.close()
140
+ self.session = None
92
141
 
93
142
 
94
143
  class ConfluenceSession:
@@ -113,11 +162,11 @@ class ConfluenceSession:
113
162
  finally:
114
163
  self.space_key = old_space_key
115
164
 
116
- def _build_url(self, path: str, query: Dict[str, str] = None) -> str:
165
+ def _build_url(self, path: str, query: Optional[Dict[str, str]] = None) -> str:
117
166
  base_url = f"https://{self.domain}/wiki/rest/api{path}"
118
167
  return build_url(base_url, query)
119
168
 
120
- def _invoke(self, path: str, query: Dict[str, str]) -> str:
169
+ def _invoke(self, path: str, query: Dict[str, str]) -> JsonType:
121
170
  url = self._build_url(path, query)
122
171
  response = self.session.get(url)
123
172
  response.raise_for_status()
@@ -137,17 +186,18 @@ class ConfluenceSession:
137
186
  ) -> ConfluenceAttachment:
138
187
  path = f"/content/{page_id}/child/attachment"
139
188
  query = {"spaceKey": self.space_key, "filename": filename}
140
- data = self._invoke(path, query)
189
+ data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
141
190
 
142
- results = data["results"]
191
+ results = typing.cast(List[JsonType], data["results"])
143
192
  if len(results) != 1:
144
193
  raise ConfluenceError(f"no such attachment on page {page_id}: {filename}")
194
+ result = typing.cast(Dict[str, JsonType], results[0])
145
195
 
146
- id = results[0]["id"]
147
- extensions = results[0]["extensions"]
148
- media_type = extensions["mediaType"]
149
- file_size = extensions["fileSize"]
150
- comment = extensions["comment"]
196
+ id = typing.cast(str, result["id"])
197
+ extensions = typing.cast(Dict[str, JsonType], result["extensions"])
198
+ media_type = typing.cast(str, extensions["mediaType"])
199
+ file_size = typing.cast(int, extensions["fileSize"])
200
+ comment = typing.cast(str, extensions["comment"])
151
201
  return ConfluenceAttachment(id, media_type, file_size, comment)
152
202
 
153
203
  def upload_attachment(
@@ -155,9 +205,8 @@ class ConfluenceSession:
155
205
  page_id: str,
156
206
  attachment_path: str,
157
207
  attachment_name: str,
158
- comment: str = None,
208
+ comment: Optional[str] = None,
159
209
  ) -> None:
160
-
161
210
  content_type = mimetypes.guess_type(attachment_path, strict=True)[0]
162
211
 
163
212
  if not os.path.isfile(attachment_path):
@@ -170,7 +219,7 @@ class ConfluenceSession:
170
219
  LOGGER.info("Up-to-date attachment: %s", attachment_name)
171
220
  return
172
221
 
173
- id = attachment.id.removeprefix("att")
222
+ id = removeprefix(attachment.id, "att")
174
223
  path = f"/content/{page_id}/child/attachment/{id}/data"
175
224
 
176
225
  except ConfluenceError:
@@ -190,7 +239,9 @@ class ConfluenceSession:
190
239
  }
191
240
  LOGGER.info("Uploading attachment: %s", attachment_name)
192
241
  response = self.session.post(
193
- url, files=file_to_upload, headers={"X-Atlassian-Token": "no-check"}
242
+ url,
243
+ files=file_to_upload, # type: ignore
244
+ headers={"X-Atlassian-Token": "no-check"},
194
245
  )
195
246
 
196
247
  response.raise_for_status()
@@ -210,8 +261,7 @@ class ConfluenceSession:
210
261
  def _update_attachment(
211
262
  self, page_id: str, attachment_id: str, version: int, attachment_title: str
212
263
  ) -> None:
213
-
214
- id = attachment_id.removeprefix("att")
264
+ id = removeprefix(attachment_id, "att")
215
265
  path = f"/content/{page_id}/child/attachment/{id}"
216
266
  data = {
217
267
  "id": attachment_id,
@@ -236,13 +286,14 @@ class ConfluenceSession:
236
286
  LOGGER.info("Looking up page with title: %s", title)
237
287
  path = "/content"
238
288
  query = {"title": title, "spaceKey": self.space_key}
239
- data = self._invoke(path, query)
289
+ data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
240
290
 
241
- results = data["results"]
291
+ results = typing.cast(List[JsonType], data["results"])
242
292
  if len(results) != 1:
243
293
  raise ConfluenceError(f"page not found with title: {title}")
244
294
 
245
- id = results[0]["id"]
295
+ result = typing.cast(Dict[str, JsonType], results[0])
296
+ id = typing.cast(str, result["id"])
246
297
  return id
247
298
 
248
299
  def get_page(self, page_id: str) -> ConfluencePage:
@@ -251,13 +302,16 @@ class ConfluenceSession:
251
302
  "spaceKey": self.space_key,
252
303
  "expand": "body.storage,version",
253
304
  }
254
- data = self._invoke(path, query)
305
+ data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
306
+ version = typing.cast(Dict[str, JsonType], data["version"])
307
+ body = typing.cast(Dict[str, JsonType], data["body"])
308
+ storage = typing.cast(Dict[str, JsonType], body["storage"])
255
309
 
256
310
  return ConfluencePage(
257
311
  id=page_id,
258
- title=data["title"],
259
- version=data["version"]["number"],
260
- content=data["body"]["storage"]["value"],
312
+ title=typing.cast(str, data["title"]),
313
+ version=typing.cast(int, version["number"]),
314
+ content=typing.cast(str, storage["value"]),
261
315
  )
262
316
 
263
317
  def get_page_version(self, page_id: str) -> int:
@@ -266,8 +320,9 @@ class ConfluenceSession:
266
320
  "spaceKey": self.space_key,
267
321
  "expand": "version",
268
322
  }
269
- data = self._invoke(path, query)
270
- return data["version"]["number"]
323
+ data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
324
+ version = typing.cast(Dict[str, JsonType], data["version"])
325
+ return typing.cast(int, version["number"])
271
326
 
272
327
  def update_page(self, page_id: str, new_content: str) -> None:
273
328
  page = self.get_page(page_id)
@@ -277,8 +332,8 @@ class ConfluenceSession:
277
332
  if old_content == new_content:
278
333
  LOGGER.info("Up-to-date page: %s", page_id)
279
334
  return
280
- except ParseError:
281
- pass
335
+ except ParseError as exc:
336
+ LOGGER.warning(exc)
282
337
 
283
338
  path = f"/content/{page_id}"
284
339
  data = {
md2conf/application.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os.path
2
2
 
3
3
  from .api import ConfluenceSession
4
- from .converter import ConfluenceDocument, markdown_to_html
4
+ from .converter import ConfluenceDocument, ConfluenceDocumentOptions
5
5
 
6
6
 
7
7
  def update_document(
@@ -15,11 +15,13 @@ def update_document(
15
15
  api.update_page(document.page_id, document.xhtml())
16
16
 
17
17
 
18
- def synchronize_page(api: ConfluenceSession, path: str) -> None:
18
+ def synchronize_page(
19
+ api: ConfluenceSession, path: str, options: ConfluenceDocumentOptions
20
+ ) -> None:
19
21
  page_path = os.path.abspath(path)
20
22
  base_path = os.path.dirname(page_path)
21
23
 
22
- document = ConfluenceDocument(path)
24
+ document = ConfluenceDocument(path, options)
23
25
 
24
26
  if document.space_key:
25
27
  with api.switch_space(document.space_key):
md2conf/converter.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os.path
2
2
  import re
3
+ from dataclasses import dataclass
3
4
  from typing import List, Optional, Tuple
4
5
  from urllib.parse import urlparse
5
6
 
@@ -24,7 +25,7 @@ class ParseError(RuntimeError):
24
25
  pass
25
26
 
26
27
 
27
- def is_absolute_url(url):
28
+ def is_absolute_url(url: str) -> bool:
28
29
  return bool(urlparse(url).netloc)
29
30
 
30
31
 
@@ -153,7 +154,11 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
153
154
  def _transform_block(self, code: ET.Element) -> ET.Element:
154
155
  language = code.attrib.get("class")
155
156
  if language:
156
- language = re.match("^language-(.*)$", language).group(1)
157
+ m = re.match("^language-(.*)$", language)
158
+ if m:
159
+ language = m.group(1)
160
+ else:
161
+ language = "none"
157
162
  if language not in _languages:
158
163
  language = "none"
159
164
  content: str = code.text
@@ -175,11 +180,11 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
175
180
  def transform(self, child: ET.Element) -> Optional[ET.Element]:
176
181
  # normalize line breaks to regular space in element text
177
182
  if child.text:
178
- s: str = child.text
179
- child.text = s.replace("\n", " ")
183
+ text: str = child.text
184
+ child.text = text.replace("\n", " ")
180
185
  if child.tail:
181
- s: str = child.tail
182
- child.tail = s.replace("\n", " ")
186
+ tail: str = child.tail
187
+ child.tail = tail.replace("\n", " ")
183
188
 
184
189
  # <p><img src="..." /></p>
185
190
  if child.tag == "p" and len(child) == 1 and child[0].tag == "img":
@@ -197,6 +202,8 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
197
202
  elif child.tag == "pre" and len(child) == 1 and child[0].tag == "code":
198
203
  return self._transform_block(child[0])
199
204
 
205
+ return None
206
+
200
207
 
201
208
  class ConfluenceStorageFormatCleaner(NodeVisitor):
202
209
  "Removes volatile attributes from a Confluence storage format XHTML document."
@@ -204,13 +211,14 @@ class ConfluenceStorageFormatCleaner(NodeVisitor):
204
211
  def transform(self, child: ET.Element) -> Optional[ET.Element]:
205
212
  child.attrib.pop(ET.QName(namespaces["ac"], "macro-id"), None)
206
213
  child.attrib.pop(ET.QName(namespaces["ri"], "version-at-save"), None)
214
+ return None
207
215
 
208
216
 
209
217
  class DocumentError(RuntimeError):
210
218
  pass
211
219
 
212
220
 
213
- def _extract_value(pattern, string) -> Tuple[Optional[str], str]:
221
+ def _extract_value(pattern: str, string: str) -> Tuple[Optional[str], str]:
214
222
  values: List[str] = []
215
223
 
216
224
  def _repl_func(matchobj: re.Match) -> str:
@@ -222,15 +230,29 @@ def _extract_value(pattern, string) -> Tuple[Optional[str], str]:
222
230
  return value, string
223
231
 
224
232
 
233
+ @dataclass
234
+ class ConfluenceDocumentOptions:
235
+ """
236
+ Options that control the generated page content.
237
+
238
+ :param show_generated: Whether to display a prompt "This page has been generated with a tool."
239
+ """
240
+
241
+ generated_by: bool = True
242
+
243
+
225
244
  class ConfluenceDocument:
226
245
  page_id: str
227
246
  space_key: Optional[str] = None
228
247
  links: List[str]
229
248
  images: List[str]
230
249
 
250
+ options: ConfluenceDocumentOptions
231
251
  root: ET.Element
232
252
 
233
- def __init__(self, path: str) -> None:
253
+ def __init__(self, path: str, options: ConfluenceDocumentOptions) -> None:
254
+ self.options = options
255
+
234
256
  path = os.path.abspath(path)
235
257
 
236
258
  with open(path, "r") as f:
@@ -252,14 +274,16 @@ class ConfluenceDocument:
252
274
  )
253
275
 
254
276
  # parse Markdown document
255
- self.root = elements_from_strings(
256
- [
277
+ if self.options.generated_by:
278
+ content = [
257
279
  '<ac:structured-macro ac:name="info" ac:schema-version="1">',
258
280
  "<ac:rich-text-body><p>This page has been generated with a tool.</p></ac:rich-text-body>",
259
281
  "</ac:structured-macro>",
260
282
  html,
261
283
  ]
262
- )
284
+ else:
285
+ content = [html]
286
+ self.root = elements_from_strings(content)
263
287
 
264
288
  converter = ConfluenceStorageFormatConverter(os.path.dirname(path))
265
289
  converter.visit(self.root)
@@ -273,6 +297,9 @@ class ConfluenceDocument:
273
297
  def sanitize_confluence(html: str) -> str:
274
298
  "Generates a sanitized version of a Confluence storage format XHTML document with no volatile attributes."
275
299
 
300
+ if not html:
301
+ return ""
302
+
276
303
  root = elements_from_strings([html])
277
304
  ConfluenceStorageFormatCleaner().visit(root)
278
305
  return _content_to_string(root)
@@ -281,4 +308,7 @@ def sanitize_confluence(html: str) -> str:
281
308
  def _content_to_string(root: ET.Element) -> str:
282
309
  xml = ET.tostring(root, encoding="utf8", method="xml").decode("utf8")
283
310
  m = re.match(r"^<root\s+[^>]*>(.*)</root>\s*$", xml, re.DOTALL)
284
- return m.group(1)
311
+ if m:
312
+ return m.group(1)
313
+ else:
314
+ raise ValueError("expected: Confluence content")
md2conf/py.typed ADDED
File without changes
@@ -1,11 +0,0 @@
1
- md2conf/__init__.py,sha256=Wzf5T3NBDfhQoTnhnRNHSlAsE0XMqbclXG-M81Vas70,22
2
- md2conf/__main__.py,sha256=bu6wW-88PjLleQ2_CKstFeZbJkt3YTfUDIRE_H0d8u4,1574
3
- md2conf/api.py,sha256=TlsG1YLslvwaAjWe1vPNovJpQO22RSTlNK5fFE3Uzi4,8954
4
- md2conf/application.py,sha256=O9lRzSNyd87scg0OwcU3LVGFpCTnDQrHKRIZ8qj5jbI,804
5
- md2conf/converter.py,sha256=PN-uhEfZK1tvpi8dBJW_w765KoFEe6OxURoEGjWFqhU,8257
6
- markdown_to_confluence-0.1.4.dist-info/LICENSE,sha256=-qGcHiU0z5bJg5WET7IW7FWy--1bTAPM47jslRt9UhA,1072
7
- markdown_to_confluence-0.1.4.dist-info/METADATA,sha256=b06yqXShDrW-fdZX2K20VSa86eaDsIzMcoQlpyddmE4,3636
8
- markdown_to_confluence-0.1.4.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
9
- markdown_to_confluence-0.1.4.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
10
- markdown_to_confluence-0.1.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
- markdown_to_confluence-0.1.4.dist-info/RECORD,,