markdown-to-confluence 0.4.1__py3-none-any.whl → 0.4.3__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.
md2conf/processor.py CHANGED
@@ -14,8 +14,9 @@ from pathlib import Path
14
14
  from typing import Iterable, Optional
15
15
 
16
16
  from .collection import ConfluencePageCollection
17
- from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID
18
- from .matcher import Matcher, MatcherOptions
17
+ from .converter import ConfluenceDocument
18
+ from .domain import ConfluenceDocumentOptions, ConfluencePageID
19
+ from .matcher import DirectoryEntry, FileEntry, Matcher, MatcherOptions
19
20
  from .metadata import ConfluenceSiteMetadata
20
21
  from .properties import ArgumentError
21
22
  from .scanner import Scanner
@@ -28,6 +29,7 @@ class DocumentNode:
28
29
  page_id: Optional[str]
29
30
  space_key: Optional[str]
30
31
  title: Optional[str]
32
+ synchronized: bool
31
33
 
32
34
  _children: list["DocumentNode"]
33
35
 
@@ -35,13 +37,15 @@ class DocumentNode:
35
37
  self,
36
38
  absolute_path: Path,
37
39
  page_id: Optional[str],
38
- space_key: Optional[str] = None,
39
- title: Optional[str] = None,
40
+ space_key: Optional[str],
41
+ title: Optional[str],
42
+ synchronized: bool,
40
43
  ):
41
44
  self.absolute_path = absolute_path
42
45
  self.page_id = page_id
43
46
  self.space_key = space_key
44
47
  self.title = title
48
+ self.synchronized = synchronized
45
49
  self._children = []
46
50
 
47
51
  def count(self) -> int:
@@ -98,16 +102,11 @@ class Processor:
98
102
  local_dir = local_dir.resolve(True)
99
103
  LOGGER.info("Processing directory: %s", local_dir)
100
104
 
101
- # Step 1: build index of all Markdown files in directory hierarchy
105
+ # build index of all Markdown files in directory hierarchy
102
106
  root = self._index_directory(local_dir, None)
103
107
  LOGGER.info("Indexed %d document(s)", root.count())
104
108
 
105
- # Step 2: synchronize directory tree structure with page hierarchy in space
106
- self._synchronize_tree(root, self.options.root_page_id)
107
-
108
- # Step 3: synchronize files in directory hierarchy with pages in space
109
- for path, metadata in self.page_metadata.items():
110
- self._synchronize_page(path, ConfluencePageID(metadata.page_id))
109
+ self._process_items(root)
111
110
 
112
111
  def process_page(self, path: Path) -> None:
113
112
  """
@@ -115,16 +114,22 @@ class Processor:
115
114
  """
116
115
 
117
116
  LOGGER.info("Processing page: %s", path)
118
-
119
- # Step 1: parse Markdown file
120
117
  root = self._index_file(path)
121
118
 
122
- # Step 2: find matching page in Confluence
119
+ self._process_items(root)
120
+
121
+ def _process_items(self, root: DocumentNode) -> None:
122
+ """
123
+ Processes a sub-tree rooted at an ancestor node.
124
+ """
125
+
126
+ # synchronize directory tree structure with page hierarchy in space (find matching pages in Confluence)
123
127
  self._synchronize_tree(root, self.options.root_page_id)
124
128
 
125
- # Step 3: synchronize document with page in space
129
+ # synchronize files in directory hierarchy with pages in space
126
130
  for path, metadata in self.page_metadata.items():
127
- self._synchronize_page(path, ConfluencePageID(metadata.page_id))
131
+ if metadata.synchronized:
132
+ self._synchronize_page(path, ConfluencePageID(metadata.page_id))
128
133
 
129
134
  def _synchronize_page(self, path: Path, page_id: ConfluencePageID) -> None:
130
135
  """
@@ -161,36 +166,40 @@ class Processor:
161
166
 
162
167
  matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
163
168
 
164
- files: list[Path] = []
165
- directories: list[Path] = []
169
+ files: list[FileEntry] = []
170
+ directories: list[DirectoryEntry] = []
166
171
  for entry in os.scandir(local_dir):
167
172
  if matcher.is_excluded(entry):
168
173
  continue
169
174
 
170
175
  if entry.is_file():
171
- files.append(local_dir / entry.name)
176
+ files.append(FileEntry(entry.name))
172
177
  elif entry.is_dir():
173
- directories.append(local_dir / entry.name)
178
+ directories.append(DirectoryEntry(entry.name))
179
+
180
+ files.sort()
181
+ directories.sort()
174
182
 
175
183
  # make page act as parent node
176
184
  parent_doc: Optional[Path] = None
177
- if (local_dir / "index.md") in files:
185
+ if FileEntry("index.md") in files:
178
186
  parent_doc = local_dir / "index.md"
179
- elif (local_dir / "README.md") in files:
187
+ elif FileEntry("README.md") in files:
180
188
  parent_doc = local_dir / "README.md"
181
- elif (local_dir / f"{local_dir.name}.md") in files:
189
+ elif FileEntry(f"{local_dir.name}.md") in files:
182
190
  parent_doc = local_dir / f"{local_dir.name}.md"
183
191
 
184
192
  if parent_doc is None and self.options.keep_hierarchy:
185
193
  parent_doc = local_dir / "index.md"
186
194
 
187
195
  # create a blank page for directory entry
188
- with open(parent_doc, "w"):
189
- pass
196
+ with open(parent_doc, "w") as f:
197
+ print("[[_LISTING_]]", file=f)
190
198
 
191
199
  if parent_doc is not None:
192
- if parent_doc in files:
193
- files.remove(parent_doc)
200
+ parent_entry = FileEntry(parent_doc.name)
201
+ if parent_entry in files:
202
+ files.remove(parent_entry)
194
203
 
195
204
  # promote Markdown document in directory as parent page in Confluence
196
205
  node = self._index_file(parent_doc)
@@ -201,11 +210,11 @@ class Processor:
201
210
  raise ArgumentError(f"root page requires corresponding top-level Markdown document in {local_dir}")
202
211
 
203
212
  for file in files:
204
- node = self._index_file(file)
213
+ node = self._index_file(local_dir / Path(file.name))
205
214
  parent.add_child(node)
206
215
 
207
216
  for directory in directories:
208
- self._index_directory(directory, parent)
217
+ self._index_directory(local_dir / Path(directory.name), parent)
209
218
 
210
219
  return parent
211
220
 
@@ -224,6 +233,7 @@ class Processor:
224
233
  page_id=document.page_id,
225
234
  space_key=document.space_key,
226
235
  title=document.title,
236
+ synchronized=document.synchronized if document.synchronized is not None else True,
227
237
  )
228
238
 
229
239
  def _generate_hash(self, absolute_path: Path) -> str:
md2conf/scanner.py CHANGED
@@ -69,6 +69,7 @@ class DocumentProperties:
69
69
  :param generated_by: Text identifying the tool that generated the document.
70
70
  :param title: The title extracted from front-matter.
71
71
  :param tags: A list of tags (content labels) extracted from front-matter.
72
+ :param synchronized: True if the document content is parsed and synchronized with Confluence.
72
73
  :param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
73
74
  """
74
75
 
@@ -79,6 +80,7 @@ class DocumentProperties:
79
80
  generated_by: Optional[str]
80
81
  title: Optional[str]
81
82
  tags: Optional[list[str]]
83
+ synchronized: Optional[bool]
82
84
  properties: Optional[dict[str, JsonType]]
83
85
 
84
86
 
@@ -92,6 +94,7 @@ class ScannedDocument:
92
94
  :param generated_by: Text identifying the tool that generated the document.
93
95
  :param title: The title extracted from front-matter.
94
96
  :param tags: A list of tags (content labels) extracted from front-matter.
97
+ :param synchronized: True if the document content is parsed and synchronized with Confluence.
95
98
  :param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
96
99
  :param text: Text that remains after front-matter and inline properties have been extracted.
97
100
  """
@@ -101,6 +104,7 @@ class ScannedDocument:
101
104
  generated_by: Optional[str]
102
105
  title: Optional[str]
103
106
  tags: Optional[list[str]]
107
+ synchronized: Optional[bool]
104
108
  properties: Optional[dict[str, JsonType]]
105
109
  text: str
106
110
 
@@ -126,6 +130,7 @@ class Scanner:
126
130
 
127
131
  title: Optional[str] = None
128
132
  tags: Optional[list[str]] = None
133
+ synchronized: Optional[bool] = None
129
134
  properties: Optional[dict[str, JsonType]] = None
130
135
 
131
136
  # extract front-matter
@@ -137,6 +142,7 @@ class Scanner:
137
142
  generated_by = generated_by or p.generated_by
138
143
  title = p.title
139
144
  tags = p.tags
145
+ synchronized = p.synchronized
140
146
  properties = p.properties
141
147
 
142
148
  return ScannedDocument(
@@ -145,6 +151,7 @@ class Scanner:
145
151
  generated_by=generated_by,
146
152
  title=title,
147
153
  tags=tags,
154
+ synchronized=synchronized,
148
155
  properties=properties,
149
156
  text=text,
150
157
  )
md2conf/xml.py ADDED
@@ -0,0 +1,70 @@
1
+ from typing import Iterable, Optional, Union
2
+
3
+ import lxml.etree as ET
4
+
5
+
6
+ def _attrs_equal_excluding(attrs1: ET._Attrib, attrs2: ET._Attrib, exclude: set[Union[str, ET.QName]]) -> bool:
7
+ """
8
+ Compares two dictionary objects, excluding keys in the skip set.
9
+ """
10
+
11
+ # create key sets to compare, excluding keys to be skipped
12
+ keys1 = {k for k in attrs1.keys() if k not in exclude}
13
+ keys2 = {k for k in attrs2.keys() if k not in exclude}
14
+ if keys1 != keys2:
15
+ return False
16
+
17
+ # compare values for each key
18
+ for key in keys1:
19
+ if attrs1.get(key) != attrs2.get(key):
20
+ return False
21
+
22
+ return True
23
+
24
+
25
+ class ElementComparator:
26
+ skip_attributes: set[Union[str, ET.QName]]
27
+
28
+ def __init__(self, *, skip_attributes: Optional[Iterable[Union[str, ET.QName]]] = None):
29
+ self.skip_attributes = set(skip_attributes) if skip_attributes else set()
30
+
31
+ def is_equal(self, e1: ET._Element, e2: ET._Element) -> bool:
32
+ """
33
+ Recursively check if two XML elements are equal.
34
+ """
35
+
36
+ if e1.tag != e2.tag:
37
+ return False
38
+
39
+ e1_text = e1.text.strip() if e1.text else ""
40
+ e2_text = e2.text.strip() if e2.text else ""
41
+ if e1_text != e2_text:
42
+ return False
43
+
44
+ e1_tail = e1.tail.strip() if e1.tail else ""
45
+ e2_tail = e2.tail.strip() if e2.tail else ""
46
+ if e1_tail != e2_tail:
47
+ return False
48
+
49
+ if not _attrs_equal_excluding(e1.attrib, e2.attrib, self.skip_attributes):
50
+ return False
51
+ if len(e1) != len(e2):
52
+ return False
53
+ return all(self.is_equal(c1, c2) for c1, c2 in zip(e1, e2))
54
+
55
+
56
+ def is_xml_equal(
57
+ tree1: ET._Element,
58
+ tree2: ET._Element,
59
+ *,
60
+ skip_attributes: Optional[Iterable[Union[str, ET.QName]]] = None,
61
+ ) -> bool:
62
+ """
63
+ Compare two XML documents for equivalence, ignoring leading/trailing whitespace differences and attribute definition order.
64
+
65
+ :param tree1: XML document as an element tree.
66
+ :param tree2: XML document as an element tree.
67
+ :returns: True if equivalent, False otherwise.
68
+ """
69
+
70
+ return ElementComparator(skip_attributes=skip_attributes).is_equal(tree1, tree2)
@@ -1,25 +0,0 @@
1
- markdown_to_confluence-0.4.1.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
2
- md2conf/__init__.py,sha256=K6ZE42N5KJjN5o2GqIFa_lcPZvMMCXPMMRWEkvlmcp0,402
3
- md2conf/__main__.py,sha256=MJm9U75savKWKYm4dLREqlsyBWEqkTtaM4YTWkEeo0E,8388
4
- md2conf/api.py,sha256=RQ_nb0Z0VnhJma1BU9ABeb4MQZvZEfFS5mTXXKcY6bk,37584
5
- md2conf/application.py,sha256=cXYXYdEdmMXwhxF69eUiPPG2Ixt4xtlWHXa28wTq150,7571
6
- md2conf/collection.py,sha256=EAXuIFcIRBO-Giic2hdU2d4Hpj0_ZFBAWI3aKQ2fjrI,775
7
- md2conf/converter.py,sha256=x2LAY1Hpw5mTVFNJE5_Zm-o7p5y6TTds6KfrpdM5qQk,38823
8
- md2conf/emoji.py,sha256=UzDrxqFo59wHmbbJmMNdn0rYFDXbZE4qirOM-_egzXc,2603
9
- md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
10
- md2conf/extra.py,sha256=VuMxuOnnC2Qwy6y52ukIxsaYhrZArRqMmRHRE4QZl8g,687
11
- md2conf/local.py,sha256=MVwGxy_n00uqCInLK8FVGaaVnaOp1nfn28PVrWz3cCQ,3496
12
- md2conf/matcher.py,sha256=izgL_MAMqbXjKPvAz3KpFv5OTDsaJ9GplTJbixrT3oY,4918
13
- md2conf/mermaid.py,sha256=f0x7ISj-41ZMh4zTAFPhIWwr94SDcsVZUc1NWqmH_G4,2508
14
- md2conf/metadata.py,sha256=TxgUrskqsWor_pvlQx-p86C0-0qRJ2aeQhuDcXU9Dpc,886
15
- md2conf/processor.py,sha256=yWVRYtbc9UHSUfRxqyPDsgeVqO7gx0s3RiGL1GzMotE,9405
16
- md2conf/properties.py,sha256=RC1jY_TKVbOv2bJxXn27Fj4fNWzyoNUQt6ltgUyVQAQ,3987
17
- md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
18
- md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- md2conf/scanner.py,sha256=qXfnJkaEwDbz6G6Z9llqifBp2TLAlrXAIP4qkCbGdWo,4964
20
- markdown_to_confluence-0.4.1.dist-info/METADATA,sha256=rAXtL2mR1LHmc_pwkmnwrGpIDMEw-7kZjIJOnMi-NLA,24864
21
- markdown_to_confluence-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- markdown_to_confluence-0.4.1.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
23
- markdown_to_confluence-0.4.1.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
24
- markdown_to_confluence-0.4.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
25
- markdown_to_confluence-0.4.1.dist-info/RECORD,,