conjira-cli 0.2.4__tar.gz → 0.2.5__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.
Files changed (31) hide show
  1. {conjira_cli-0.2.4/src/conjira_cli.egg-info → conjira_cli-0.2.5}/PKG-INFO +1 -1
  2. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/pyproject.toml +1 -1
  3. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/markdown_import.py +42 -2
  4. {conjira_cli-0.2.4 → conjira_cli-0.2.5/src/conjira_cli.egg-info}/PKG-INFO +1 -1
  5. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_markdown_import.py +103 -0
  6. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/LICENSE +0 -0
  7. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/README.md +0 -0
  8. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/setup.cfg +0 -0
  9. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/setup.py +0 -0
  10. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/__init__.py +0 -0
  11. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/__main__.py +0 -0
  12. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/cli.py +0 -0
  13. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/client.py +0 -0
  14. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/config.py +0 -0
  15. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/inline_comments.py +0 -0
  16. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/markdown_export.py +0 -0
  17. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/section_edit.py +0 -0
  18. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/setup_macos.py +0 -0
  19. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli/tree_export.py +0 -0
  20. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli.egg-info/SOURCES.txt +0 -0
  21. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli.egg-info/dependency_links.txt +0 -0
  22. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli.egg-info/entry_points.txt +0 -0
  23. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/src/conjira_cli.egg-info/top_level.txt +0 -0
  24. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_cli.py +0 -0
  25. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_client.py +0 -0
  26. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_config.py +0 -0
  27. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_inline_comments.py +0 -0
  28. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_markdown_export.py +0 -0
  29. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_section_edit.py +0 -0
  30. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_setup_macos.py +0 -0
  31. {conjira_cli-0.2.4 → conjira_cli-0.2.5}/tests/test_tree_export.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conjira-cli
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Unofficial agent-friendly CLI for self-hosted Confluence and Jira
5
5
  Author: quanttraderkim
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conjira-cli"
7
- version = "0.2.4"
7
+ version = "0.2.5"
8
8
  description = "Unofficial agent-friendly CLI for self-hosted Confluence and Jira"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import html
4
4
  import os
5
5
  import re
6
+ import xml.etree.ElementTree as ET
6
7
  from typing import Optional
7
8
 
8
9
 
@@ -371,19 +372,58 @@ def _parse_table(lines: list[str], start: int) -> tuple[str, int]:
371
372
  if has_header:
372
373
  parts.append(
373
374
  "<tr>{0}</tr>".format(
374
- "".join("<th>{0}</th>".format(_render_inline(cell)) for cell in rows[0])
375
+ "".join("<th>{0}</th>".format(_render_table_cell(cell)) for cell in rows[0])
375
376
  )
376
377
  )
377
378
  for row in body_rows:
378
379
  parts.append(
379
380
  "<tr>{0}</tr>".format(
380
- "".join("<td>{0}</td>".format(_render_inline(cell)) for cell in row)
381
+ "".join("<td>{0}</td>".format(_render_table_cell(cell)) for cell in row)
381
382
  )
382
383
  )
383
384
  parts.append("</tbody></table>")
384
385
  return "".join(parts), i
385
386
 
386
387
 
388
+ _CELL_HTML_BLOCK_PREFIX_RE = re.compile(
389
+ r"^<(ul|ol|p|div|table|blockquote|h[1-6]|ac:[\w-]+|ri:[\w-]+|atlassian:[\w-]+)\b",
390
+ re.IGNORECASE,
391
+ )
392
+ _CELL_XML_WRAPPER_PREFIX = (
393
+ '<root xmlns:ac="urn:ac" xmlns:ri="urn:ri" xmlns:atlassian="urn:atlassian">'
394
+ )
395
+
396
+
397
+ def _render_table_cell(cell: str) -> str:
398
+ """Render a markdown table cell.
399
+
400
+ Standard markdown tables only support inline content per cell, but
401
+ Confluence cells frequently need nested structures (``<ul><li>``,
402
+ ``<p>``, etc.). When the cell content is a well-formed HTML block,
403
+ we pass it through unchanged so users can hand-author rich cells
404
+ without leaving the markdown pipeline.
405
+
406
+ Pass-through covers the entire cell: any trailing content after the
407
+ HTML block is emitted verbatim and not re-rendered as markdown.
408
+ """
409
+ stripped = cell.strip()
410
+ if (
411
+ stripped.startswith("<")
412
+ and _CELL_HTML_BLOCK_PREFIX_RE.match(stripped)
413
+ and _is_well_formed_xhtml(stripped)
414
+ ):
415
+ return stripped
416
+ return _render_inline(cell)
417
+
418
+
419
+ def _is_well_formed_xhtml(fragment: str) -> bool:
420
+ try:
421
+ ET.fromstring(_CELL_XML_WRAPPER_PREFIX + fragment + "</root>")
422
+ except ET.ParseError:
423
+ return False
424
+ return True
425
+
426
+
387
427
  def _split_table_row(line: str) -> list[str]:
388
428
  stripped = line.strip()
389
429
  if stripped.startswith("|"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conjira-cli
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Unofficial agent-friendly CLI for self-hosted Confluence and Jira
5
5
  Author: quanttraderkim
6
6
  License-Expression: MIT
@@ -195,3 +195,106 @@ class MarkdownImportTests(unittest.TestCase):
195
195
  wrapped = f'<root xmlns:ac="urn:ac" xmlns:ri="urn:ri">{result}</root>'
196
196
  # Should not raise
197
197
  ET.fromstring(wrapped)
198
+
199
+ def test_table_cell_passes_through_raw_html_list(self) -> None:
200
+ """Cells containing well-formed HTML blocks (e.g. nested <ul>) survive."""
201
+ result = markdown_to_storage_html(
202
+ "\n".join(
203
+ [
204
+ "| 구분 | 내용 |",
205
+ "| --- | --- |",
206
+ "| 포인트 지급 | <ul><li>휴대폰 포인트 지급<ul><li>MDN 기준</li></ul></li></ul> |",
207
+ ]
208
+ )
209
+ )
210
+
211
+ self.assertIn(
212
+ "<td><ul><li>휴대폰 포인트 지급<ul><li>MDN 기준</li></ul></li></ul></td>",
213
+ result,
214
+ )
215
+ self.assertNotIn("&lt;ul&gt;", result)
216
+
217
+ def test_table_cell_passes_through_storage_macro(self) -> None:
218
+ """Cells containing ac:* / ri:* storage-format macros pass through."""
219
+ cell_html = (
220
+ '<ac:structured-macro ac:name="status" ac:schema-version="1">'
221
+ '<ac:parameter ac:name="colour">Green</ac:parameter>'
222
+ '<ac:parameter ac:name="title">Done</ac:parameter>'
223
+ "</ac:structured-macro>"
224
+ )
225
+ result = markdown_to_storage_html(
226
+ "\n".join(
227
+ [
228
+ "| Item | Status |",
229
+ "| --- | --- |",
230
+ "| API | {0} |".format(cell_html),
231
+ ]
232
+ )
233
+ )
234
+
235
+ self.assertIn(cell_html, result)
236
+ self.assertNotIn("&lt;ac:", result)
237
+
238
+ def test_table_cell_with_inline_text_still_escapes(self) -> None:
239
+ """Cells without HTML block content still get inline escaping."""
240
+ result = markdown_to_storage_html(
241
+ "\n".join(
242
+ [
243
+ "| Title | Note |",
244
+ "| --- | --- |",
245
+ "| 5 < 10 | not html |",
246
+ ]
247
+ )
248
+ )
249
+
250
+ self.assertIn("<td>5 &lt; 10</td>", result)
251
+ self.assertIn("<td>not html</td>", result)
252
+
253
+ def test_table_cell_with_malformed_html_falls_back_to_inline(self) -> None:
254
+ """If a cell looks like HTML but isn't well-formed, escape it instead of breaking the page."""
255
+ result = markdown_to_storage_html(
256
+ "\n".join(
257
+ [
258
+ "| Item | Note |",
259
+ "| --- | --- |",
260
+ "| <ul><li>unclosed | broken |",
261
+ ]
262
+ )
263
+ )
264
+
265
+ # Malformed HTML must not pass through; it should be escaped.
266
+ self.assertNotIn("<td><ul><li>unclosed", result)
267
+ self.assertIn("&lt;ul&gt;", result)
268
+
269
+ def test_table_with_html_cells_is_valid_xhtml(self) -> None:
270
+ """Tables with HTML pass-through cells must produce valid XHTML."""
271
+ import xml.etree.ElementTree as ET
272
+
273
+ result = markdown_to_storage_html(
274
+ "\n".join(
275
+ [
276
+ "| 구분 | 내용 |",
277
+ "| --- | --- |",
278
+ "| A | <ul><li>x<ul><li>y</li></ul></li></ul> |",
279
+ "| B | plain text |",
280
+ ]
281
+ )
282
+ )
283
+ wrapped = f'<root xmlns:ac="urn:ac" xmlns:ri="urn:ri">{result}</root>'
284
+ ET.fromstring(wrapped)
285
+
286
+ def test_table_cell_html_block_emits_trailing_content_verbatim(self) -> None:
287
+ """Trailing content after an HTML block in a cell is emitted verbatim, not re-rendered."""
288
+ result = markdown_to_storage_html(
289
+ "\n".join(
290
+ [
291
+ "| A | B |",
292
+ "| --- | --- |",
293
+ "| 1 | <ul><li>x</li></ul> trailing [link](http://example.com) |",
294
+ ]
295
+ )
296
+ )
297
+
298
+ # Whole cell goes through as raw HTML; trailing markdown is NOT rendered.
299
+ self.assertIn("<ul><li>x</li></ul> trailing [link](http://example.com)", result)
300
+ self.assertNotIn('<a href="http://example.com">', result)
File without changes
File without changes
File without changes
File without changes