atlassian-cli 0.10.2__tar.gz → 0.10.4__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.
- {atlassian_cli-0.10.2/src/atlassian_cli.egg-info → atlassian_cli-0.10.4}/PKG-INFO +3 -1
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/README.md +2 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/pyproject.toml +1 -1
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/adf.py +169 -15
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/confluence.py +14 -4
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4/src/atlassian_cli.egg-info}/PKG-INFO +3 -1
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/tests/test_adf.py +121 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/LICENSE +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/setup.cfg +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/__init__.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/config.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/hints.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/http.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/jira.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/jira_assets.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/jira_issues.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/output.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli/update_check.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/SOURCES.txt +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/dependency_links.txt +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/entry_points.txt +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/requires.txt +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/top_level.txt +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/tests/test_config.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/tests/test_confluence.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/tests/test_http.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/tests/test_jira_assets.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/tests/test_jira_issues.py +0 -0
- {atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/tests/test_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atlassian-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.4
|
|
4
4
|
Summary: Fast CLI tools for Atlassian Cloud (Confluence + Jira) — optimized for AI agents
|
|
5
5
|
Author-email: Alex Fishlock <alex.fishlock@catapult.cx>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -136,6 +136,7 @@ Create, read, update, and delete blog posts in any Confluence space.
|
|
|
136
136
|
confluence blog-create POL "Q1 Product Update"
|
|
137
137
|
confluence blog-create POL "Release Notes" --body "What's new this quarter..."
|
|
138
138
|
confluence blog-create POL "Deep Dive" --file /path/to/adf.json
|
|
139
|
+
confluence blog-create POL "Retrospective" --body "..." --created-at 2024-01-15T10:00:00Z
|
|
139
140
|
|
|
140
141
|
# Download a blog post
|
|
141
142
|
confluence blog-get 123456
|
|
@@ -148,6 +149,7 @@ confluence blog-put 123456 --message "Added screenshots"
|
|
|
148
149
|
# Update title and/or body directly
|
|
149
150
|
confluence blog-update 123456 --title "Q1 Update (Revised)"
|
|
150
151
|
confluence blog-update 123456 --body "Updated content here" -m "Corrections"
|
|
152
|
+
confluence blog-update 123456 --created-at "2024-06-01T08:00:00Z"
|
|
151
153
|
|
|
152
154
|
# List blog posts in a space
|
|
153
155
|
confluence blog-list POL
|
|
@@ -105,6 +105,7 @@ Create, read, update, and delete blog posts in any Confluence space.
|
|
|
105
105
|
confluence blog-create POL "Q1 Product Update"
|
|
106
106
|
confluence blog-create POL "Release Notes" --body "What's new this quarter..."
|
|
107
107
|
confluence blog-create POL "Deep Dive" --file /path/to/adf.json
|
|
108
|
+
confluence blog-create POL "Retrospective" --body "..." --created-at 2024-01-15T10:00:00Z
|
|
108
109
|
|
|
109
110
|
# Download a blog post
|
|
110
111
|
confluence blog-get 123456
|
|
@@ -117,6 +118,7 @@ confluence blog-put 123456 --message "Added screenshots"
|
|
|
117
118
|
# Update title and/or body directly
|
|
118
119
|
confluence blog-update 123456 --title "Q1 Update (Revised)"
|
|
119
120
|
confluence blog-update 123456 --body "Updated content here" -m "Corrections"
|
|
121
|
+
confluence blog-update 123456 --created-at "2024-06-01T08:00:00Z"
|
|
120
122
|
|
|
121
123
|
# List blog posts in a space
|
|
122
124
|
confluence blog-list POL
|
|
@@ -17,6 +17,7 @@ Provides:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import re
|
|
20
|
+
import uuid
|
|
20
21
|
|
|
21
22
|
from atlas_doc_parser.api import NodeDoc
|
|
22
23
|
|
|
@@ -297,22 +298,63 @@ def _to_list_item(item):
|
|
|
297
298
|
return {'type': 'listItem', 'content': [para(str(item))]}
|
|
298
299
|
|
|
299
300
|
|
|
300
|
-
def table(header_cells, rows):
|
|
301
|
-
"""Create a table. header_cells/rows: lists of strings or inline-node lists.
|
|
302
|
-
|
|
301
|
+
def table(header_cells, rows, *, colwidths=None, localid=None):
|
|
302
|
+
"""Create a table. header_cells/rows: lists of strings or inline-node lists.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
header_cells: Column headings (strings or inline-node lists).
|
|
306
|
+
rows: Body rows, each a list of cell values.
|
|
307
|
+
colwidths: Optional list of column widths (int/float). If omitted,
|
|
308
|
+
divides 900 equally.
|
|
309
|
+
localid: Optional table-level UUID. If omitted, generated.
|
|
310
|
+
"""
|
|
311
|
+
n_cols = len(header_cells)
|
|
312
|
+
if colwidths is None:
|
|
313
|
+
colwidths = [round(900.0 / n_cols, 1)] * n_cols
|
|
314
|
+
|
|
315
|
+
def _uuid():
|
|
316
|
+
return str(uuid.uuid4())
|
|
317
|
+
|
|
318
|
+
def cell(val, col_idx, is_header=False, row_localid=None):
|
|
303
319
|
ct = 'tableHeader' if is_header else 'tableCell'
|
|
320
|
+
colwidth = colwidths[col_idx] if col_idx < len(colwidths) else colwidths[-1]
|
|
321
|
+
attrs = {'colspan': 1, 'colwidth': colwidth, 'rowspan': 1}
|
|
304
322
|
if isinstance(val, str):
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
323
|
+
content_nodes = [para(val)]
|
|
324
|
+
elif isinstance(val, list):
|
|
325
|
+
content_nodes = [para(*val)]
|
|
326
|
+
else:
|
|
327
|
+
content_nodes = [para(str(val))]
|
|
328
|
+
node = {
|
|
329
|
+
'type': ct,
|
|
330
|
+
'attrs': attrs,
|
|
331
|
+
'content': content_nodes,
|
|
332
|
+
'localId': _uuid(),
|
|
333
|
+
}
|
|
334
|
+
return node
|
|
335
|
+
|
|
336
|
+
tbl_localid = localid or _uuid()
|
|
337
|
+
|
|
338
|
+
content = [
|
|
339
|
+
{
|
|
340
|
+
'type': 'tableRow',
|
|
341
|
+
'content': [cell(c, ci, is_header=True) for ci, c in enumerate(header_cells)],
|
|
342
|
+
'localId': _uuid(),
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
for ri, row in enumerate(rows):
|
|
346
|
+
row_localid = _uuid()
|
|
347
|
+
cells = []
|
|
348
|
+
for ci, c in enumerate(row):
|
|
349
|
+
cells.append(cell(c, ci, is_header=False))
|
|
350
|
+
content.append({
|
|
351
|
+
'type': 'tableRow',
|
|
352
|
+
'content': cells,
|
|
353
|
+
'localId': row_localid,
|
|
354
|
+
})
|
|
313
355
|
return {
|
|
314
356
|
'type': 'table',
|
|
315
|
-
'attrs': {'isNumberColumnEnabled': False, 'layout': 'default', 'localId':
|
|
357
|
+
'attrs': {'isNumberColumnEnabled': False, 'layout': 'default', 'localId': tbl_localid},
|
|
316
358
|
'content': content,
|
|
317
359
|
}
|
|
318
360
|
|
|
@@ -342,8 +384,8 @@ def md_to_adf(markdown):
|
|
|
342
384
|
"""Convert a markdown string to a list of ADF nodes.
|
|
343
385
|
|
|
344
386
|
Supports: headings, paragraphs, bullet/ordered lists, bold, italic,
|
|
345
|
-
bold+italic, inline code, links, horizontal rules, code blocks, blockquotes
|
|
346
|
-
|
|
387
|
+
bold+italic, inline code, links, horizontal rules, code blocks, blockquotes,
|
|
388
|
+
tables (newline-separated and inline-flattened forms).
|
|
347
389
|
"""
|
|
348
390
|
lines = markdown.split('\n')
|
|
349
391
|
nodes = []
|
|
@@ -414,17 +456,129 @@ def md_to_adf(markdown):
|
|
|
414
456
|
]})
|
|
415
457
|
continue
|
|
416
458
|
|
|
459
|
+
# Table (newline-separated Form A)
|
|
460
|
+
if line.startswith('|'):
|
|
461
|
+
tbl_result = _try_parse_table(lines, i)
|
|
462
|
+
if tbl_result:
|
|
463
|
+
nodes.append(tbl_result[0])
|
|
464
|
+
i = tbl_result[1]
|
|
465
|
+
continue
|
|
466
|
+
|
|
417
467
|
# Paragraph — consecutive non-blank, non-block lines
|
|
418
468
|
para_lines = []
|
|
419
469
|
while i < len(lines) and lines[i].strip() and not _is_block_start(lines[i]):
|
|
420
470
|
para_lines.append(lines[i])
|
|
421
471
|
i += 1
|
|
422
472
|
if para_lines:
|
|
423
|
-
|
|
473
|
+
joined = ' '.join(para_lines)
|
|
474
|
+
# Table (inline-flattened Form B) — detect inside paragraph
|
|
475
|
+
if _looks_like_inline_table(joined):
|
|
476
|
+
tbl_node = _parse_inline_table(joined)
|
|
477
|
+
if tbl_node:
|
|
478
|
+
nodes.append(tbl_node)
|
|
479
|
+
continue
|
|
480
|
+
nodes.append({'type': 'paragraph', 'content': _parse_inline(joined)})
|
|
424
481
|
|
|
425
482
|
return nodes
|
|
426
483
|
|
|
427
484
|
|
|
485
|
+
_SEP_RE = re.compile(r'^:?-+:?$')
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _is_separator_row(cell_texts):
|
|
489
|
+
"""Return True if every non-empty cell matches the separator pattern."""
|
|
490
|
+
return all(_SEP_RE.match(c.strip()) for c in cell_texts if c.strip())
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _split_row(line):
|
|
494
|
+
"""Split a ``|...|...|`` markdown table row into cell strings."""
|
|
495
|
+
parts = line.strip().split('|')
|
|
496
|
+
# first and last are empty (leading/trailing pipe)
|
|
497
|
+
if parts and parts[0] == '':
|
|
498
|
+
parts = parts[1:]
|
|
499
|
+
if parts and parts[-1] == '':
|
|
500
|
+
parts = parts[:-1]
|
|
501
|
+
return [p.strip() for p in parts]
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _looks_like_inline_table(text_str):
|
|
505
|
+
"""Check if a single-line paragraph looks like a flattened markdown table."""
|
|
506
|
+
# Must contain a separator pattern: |---| somewhere
|
|
507
|
+
if not re.search(r'\|\s*:?-{3,}:?\s*\|', text_str):
|
|
508
|
+
return False
|
|
509
|
+
# Must have at least 2 pipes (3 cells minimum after split)
|
|
510
|
+
return text_str.count('|') >= 4
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _parse_inline_table(text_str):
|
|
514
|
+
"""Parse an inline-flattened markdown table into an ADF table node.
|
|
515
|
+
|
|
516
|
+
Row boundaries are ``|`` followed by whitespace then ``|`` (closing pipe
|
|
517
|
+
of one row immediately followed by opening pipe of the next).
|
|
518
|
+
"""
|
|
519
|
+
raw_rows = re.split(r'\|\s+(?=\|)', text_str)
|
|
520
|
+
return _build_table_from_rows(raw_rows)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _try_parse_table(lines, start):
|
|
524
|
+
"""Try to parse a newline-separated markdown table starting at *start*.
|
|
525
|
+
|
|
526
|
+
Returns ``(table_node, new_index)`` or ``None`` if not a valid table.
|
|
527
|
+
"""
|
|
528
|
+
i = start
|
|
529
|
+
raw_rows = []
|
|
530
|
+
while i < len(lines) and lines[i].strip().startswith('|'):
|
|
531
|
+
raw_rows.append(lines[i].strip())
|
|
532
|
+
i += 1
|
|
533
|
+
|
|
534
|
+
if len(raw_rows) < 2:
|
|
535
|
+
return None
|
|
536
|
+
# The second row must be a separator
|
|
537
|
+
cells = _split_row(raw_rows[1])
|
|
538
|
+
if not _is_separator_row(cells):
|
|
539
|
+
return None
|
|
540
|
+
|
|
541
|
+
return _build_table_from_rows(raw_rows), i
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _build_table_from_rows(raw_rows):
|
|
545
|
+
"""Build an ADF table node from a list of ``|...|`` row strings.
|
|
546
|
+
|
|
547
|
+
First row = header, second row = separator (dropped), remainder = body.
|
|
548
|
+
"""
|
|
549
|
+
if len(raw_rows) < 2:
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
header_cells = _split_row(raw_rows[0])
|
|
553
|
+
n_cols = len(header_cells)
|
|
554
|
+
|
|
555
|
+
body_cells = []
|
|
556
|
+
for ri in range(2, len(raw_rows)):
|
|
557
|
+
row = _split_row(raw_rows[ri])
|
|
558
|
+
if not any(r.strip() for r in row):
|
|
559
|
+
continue
|
|
560
|
+
if len(row) < n_cols:
|
|
561
|
+
row.extend([''] * (n_cols - len(row)))
|
|
562
|
+
elif len(row) > n_cols:
|
|
563
|
+
row = row[:n_cols]
|
|
564
|
+
body_cells.append(row)
|
|
565
|
+
|
|
566
|
+
# Parse inline content for header cells, add strong mark
|
|
567
|
+
parsed_header = []
|
|
568
|
+
for h in header_cells:
|
|
569
|
+
inlines = _parse_inline(h)
|
|
570
|
+
for node in inlines:
|
|
571
|
+
if node.get('type') == 'text':
|
|
572
|
+
node.setdefault('marks', []).insert(0, {'type': 'strong'})
|
|
573
|
+
parsed_header.append(inlines)
|
|
574
|
+
|
|
575
|
+
parsed_body = []
|
|
576
|
+
for row in body_cells:
|
|
577
|
+
parsed_body.append([_parse_inline(c) for c in row])
|
|
578
|
+
|
|
579
|
+
return table(parsed_header, parsed_body)
|
|
580
|
+
|
|
581
|
+
|
|
428
582
|
def _is_block_start(line):
|
|
429
583
|
if re.match(r'^#{1,6}\s', line):
|
|
430
584
|
return True
|
|
@@ -117,7 +117,8 @@ def list_pages(session, base, space_id, statuses=('current',)):
|
|
|
117
117
|
# Blog posts (v2)
|
|
118
118
|
# ---------------------------------------------------------------------------
|
|
119
119
|
|
|
120
|
-
def create_blogpost(session, base, space_id, title, *, body=None, file=None
|
|
120
|
+
def create_blogpost(session, base, space_id, title, *, body=None, file=None,
|
|
121
|
+
created_at=None):
|
|
121
122
|
"""Create a blog post in a Confluence space."""
|
|
122
123
|
body_payload = {}
|
|
123
124
|
if file:
|
|
@@ -144,6 +145,8 @@ def create_blogpost(session, base, space_id, title, *, body=None, file=None):
|
|
|
144
145
|
}
|
|
145
146
|
if body_payload:
|
|
146
147
|
payload['body'] = body_payload
|
|
148
|
+
if created_at is not None:
|
|
149
|
+
payload['createdAt'] = created_at
|
|
147
150
|
|
|
148
151
|
return api_post(session, base, f'{V2}/blogposts', payload)
|
|
149
152
|
|
|
@@ -162,8 +165,9 @@ def get_blogpost(session, base, blogpost_id):
|
|
|
162
165
|
|
|
163
166
|
|
|
164
167
|
def update_blogpost(session, base, blogpost_id, *,
|
|
165
|
-
title=None, body=None, file=None, message=None
|
|
166
|
-
|
|
168
|
+
title=None, body=None, file=None, message=None,
|
|
169
|
+
created_at=None):
|
|
170
|
+
"""Update a blog post's title, body, and/or creation date."""
|
|
167
171
|
remote = get_blogpost(session, base, blogpost_id)
|
|
168
172
|
|
|
169
173
|
new_title = title if title is not None else remote.get('title', '')
|
|
@@ -203,6 +207,8 @@ def update_blogpost(session, base, blogpost_id, *,
|
|
|
203
207
|
}
|
|
204
208
|
if body_payload:
|
|
205
209
|
payload['body'] = body_payload
|
|
210
|
+
if created_at is not None:
|
|
211
|
+
payload['createdAt'] = created_at
|
|
206
212
|
|
|
207
213
|
api_put(session, base, f'{V2}/blogposts/{blogpost_id}', payload)
|
|
208
214
|
return new_title, new_version
|
|
@@ -506,7 +512,8 @@ def cmd_blog_create(args):
|
|
|
506
512
|
space_key = space.get('key', args.space_key)
|
|
507
513
|
|
|
508
514
|
result = create_blogpost(session, base, space_id, args.title,
|
|
509
|
-
body=args.body, file=args.file
|
|
515
|
+
body=args.body, file=args.file,
|
|
516
|
+
created_at=getattr(args, 'created_at', None))
|
|
510
517
|
blogpost_id = result['id']
|
|
511
518
|
|
|
512
519
|
if args.dir:
|
|
@@ -531,6 +538,7 @@ def cmd_blog_update(args):
|
|
|
531
538
|
session, base, args.blogpost_id,
|
|
532
539
|
title=args.title, body=args.body, file=args.file,
|
|
533
540
|
message=getattr(args, 'message', None),
|
|
541
|
+
created_at=getattr(args, 'created_at', None),
|
|
534
542
|
)
|
|
535
543
|
|
|
536
544
|
if args.dir:
|
|
@@ -1311,6 +1319,7 @@ def main():
|
|
|
1311
1319
|
p.add_argument('title', help='Blog post title')
|
|
1312
1320
|
p.add_argument('--body', help='Plain text body')
|
|
1313
1321
|
p.add_argument('--file', '-f', help='ADF JSON file for blog body')
|
|
1322
|
+
p.add_argument('--created-at', help='Creation date (ISO 8601, e.g. 2024-01-15T10:00:00Z)')
|
|
1314
1323
|
p.add_argument('--dir', default='pages', help='Pages directory (default: pages)')
|
|
1315
1324
|
p.set_defaults(func=cmd_blog_create)
|
|
1316
1325
|
|
|
@@ -1324,6 +1333,7 @@ def main():
|
|
|
1324
1333
|
p.add_argument('--title', help='New title')
|
|
1325
1334
|
p.add_argument('--body', help='New plain text body')
|
|
1326
1335
|
p.add_argument('--file', '-f', help='ADF JSON file for new blog body')
|
|
1336
|
+
p.add_argument('--created-at', help='Override creation date (ISO 8601, e.g. 2024-01-15T10:00:00Z)')
|
|
1327
1337
|
p.add_argument('--message', '-m', help='Version message')
|
|
1328
1338
|
p.add_argument('--dir', default='pages', help='Pages directory (default: pages)')
|
|
1329
1339
|
p.set_defaults(func=cmd_blog_update)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atlassian-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.4
|
|
4
4
|
Summary: Fast CLI tools for Atlassian Cloud (Confluence + Jira) — optimized for AI agents
|
|
5
5
|
Author-email: Alex Fishlock <alex.fishlock@catapult.cx>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -136,6 +136,7 @@ Create, read, update, and delete blog posts in any Confluence space.
|
|
|
136
136
|
confluence blog-create POL "Q1 Product Update"
|
|
137
137
|
confluence blog-create POL "Release Notes" --body "What's new this quarter..."
|
|
138
138
|
confluence blog-create POL "Deep Dive" --file /path/to/adf.json
|
|
139
|
+
confluence blog-create POL "Retrospective" --body "..." --created-at 2024-01-15T10:00:00Z
|
|
139
140
|
|
|
140
141
|
# Download a blog post
|
|
141
142
|
confluence blog-get 123456
|
|
@@ -148,6 +149,7 @@ confluence blog-put 123456 --message "Added screenshots"
|
|
|
148
149
|
# Update title and/or body directly
|
|
149
150
|
confluence blog-update 123456 --title "Q1 Update (Revised)"
|
|
150
151
|
confluence blog-update 123456 --body "Updated content here" -m "Corrections"
|
|
152
|
+
confluence blog-update 123456 --created-at "2024-06-01T08:00:00Z"
|
|
151
153
|
|
|
152
154
|
# List blog posts in a space
|
|
153
155
|
confluence blog-list POL
|
|
@@ -492,3 +492,124 @@ More text.
|
|
|
492
492
|
assert 'Test' in md_out
|
|
493
493
|
assert 'world' in md_out
|
|
494
494
|
assert 'item one' in md_out
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ---------------------------------------------------------------------------
|
|
498
|
+
# md_to_adf — table support
|
|
499
|
+
# ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
class TestMdToAdfTables:
|
|
502
|
+
def test_md_table_standard(self):
|
|
503
|
+
"""Newline-separated markdown table produces valid ADF table."""
|
|
504
|
+
md = "\n".join([
|
|
505
|
+
"| Header 1 | Header 2 | Header 3 |",
|
|
506
|
+
"| --- | --- | --- |",
|
|
507
|
+
"| a | b | c |",
|
|
508
|
+
"| d | e | f |",
|
|
509
|
+
])
|
|
510
|
+
nodes = md_to_adf(md)
|
|
511
|
+
assert len(nodes) == 1
|
|
512
|
+
t = nodes[0]
|
|
513
|
+
assert t['type'] == 'table'
|
|
514
|
+
assert 'localId' in t['attrs']
|
|
515
|
+
assert t['attrs']['localId'] != ''
|
|
516
|
+
rows = t['content']
|
|
517
|
+
assert len(rows) == 3 # header + 2 data, separator dropped
|
|
518
|
+
assert rows[0]['type'] == 'tableRow'
|
|
519
|
+
assert 'localId' in rows[0]
|
|
520
|
+
assert rows[0]['localId'] != ''
|
|
521
|
+
hdr = rows[0]['content'][0]
|
|
522
|
+
assert hdr['type'] == 'tableHeader'
|
|
523
|
+
assert 'localId' in hdr
|
|
524
|
+
assert hdr['localId'] != ''
|
|
525
|
+
hdr_text = hdr['content'][0]['content'][0]
|
|
526
|
+
assert hdr_text['marks'][0]['type'] == 'strong'
|
|
527
|
+
body_cell = rows[1]['content'][0]
|
|
528
|
+
assert body_cell['type'] == 'tableCell'
|
|
529
|
+
assert body_cell['localId'] != ''
|
|
530
|
+
assert 'colwidth' in body_cell['attrs']
|
|
531
|
+
|
|
532
|
+
def test_md_table_inline_flattened(self):
|
|
533
|
+
"""Inline-flattened table (bug we hit) produces same ADF table."""
|
|
534
|
+
md = "| Header 1 | Header 2 | Header 3 | | --- | --- | --- | | a | b | c | | d | e | f |"
|
|
535
|
+
nodes = md_to_adf(md)
|
|
536
|
+
assert len(nodes) == 1
|
|
537
|
+
t = nodes[0]
|
|
538
|
+
assert t['type'] == 'table'
|
|
539
|
+
rows = t['content']
|
|
540
|
+
assert len(rows) == 3
|
|
541
|
+
assert rows[0]['content'][0]['type'] == 'tableHeader'
|
|
542
|
+
assert rows[1]['content'][0]['type'] == 'tableCell'
|
|
543
|
+
|
|
544
|
+
def test_md_table_inline_formatting(self):
|
|
545
|
+
"""Cells with bold, italic, code, links produce correct inline marks."""
|
|
546
|
+
md = "| A | B | | --- | --- | | **bold** | *italic* | | `code` | [link](https://example.com) |"
|
|
547
|
+
nodes = md_to_adf(md)
|
|
548
|
+
t = nodes[0]
|
|
549
|
+
cells = t['content'][1]['content']
|
|
550
|
+
c0 = cells[0]['content'][0]['content'][0]
|
|
551
|
+
c1 = cells[1]['content'][0]['content'][0]
|
|
552
|
+
c2 = t['content'][2]['content'][0]['content'][0]['content'][0]
|
|
553
|
+
c3 = t['content'][2]['content'][1]['content'][0]['content'][0]
|
|
554
|
+
assert c0['marks'][0]['type'] == 'strong'
|
|
555
|
+
assert c1['marks'][0]['type'] == 'em'
|
|
556
|
+
assert c2['marks'][0]['type'] == 'code'
|
|
557
|
+
assert c3['marks'][0]['type'] == 'link'
|
|
558
|
+
|
|
559
|
+
def test_md_table_alignment_separator(self):
|
|
560
|
+
"""Alignment markers in separator (:--, :-:, --:) are recognised and dropped."""
|
|
561
|
+
md = "\n".join([
|
|
562
|
+
"| L | C | R |",
|
|
563
|
+
"| :-- | :-: | --: |",
|
|
564
|
+
"| a | b | c |",
|
|
565
|
+
])
|
|
566
|
+
nodes = md_to_adf(md)
|
|
567
|
+
t = nodes[0]
|
|
568
|
+
assert len(t['content']) == 2 # header + 1 data, separator dropped
|
|
569
|
+
|
|
570
|
+
def test_md_table_ragged_rows(self):
|
|
571
|
+
"""Short rows pad with empty cells; long rows truncate."""
|
|
572
|
+
md = "\n".join([
|
|
573
|
+
"| A | B | C |",
|
|
574
|
+
"| --- | --- | --- |",
|
|
575
|
+
"| 1 | 2 |", # short
|
|
576
|
+
"| 4 | 5 | 6 | 7 |", # long
|
|
577
|
+
])
|
|
578
|
+
nodes = md_to_adf(md)
|
|
579
|
+
t = nodes[0]
|
|
580
|
+
r1 = t['content'][1]['content'] # short row
|
|
581
|
+
r2 = t['content'][2]['content'] # long row
|
|
582
|
+
assert len(r1) == 3
|
|
583
|
+
assert len(r2) == 3
|
|
584
|
+
|
|
585
|
+
def test_md_table_in_mixed_document(self):
|
|
586
|
+
"""Table sits between heading and bullet list, all separate nodes."""
|
|
587
|
+
md = "\n".join([
|
|
588
|
+
"## Before",
|
|
589
|
+
"",
|
|
590
|
+
"| A | B |",
|
|
591
|
+
"| --- | --- |",
|
|
592
|
+
"| 1 | 2 |",
|
|
593
|
+
"",
|
|
594
|
+
"- item one",
|
|
595
|
+
"- item two",
|
|
596
|
+
])
|
|
597
|
+
nodes = md_to_adf(md)
|
|
598
|
+
types = [n['type'] for n in nodes]
|
|
599
|
+
assert types == ['heading', 'table', 'bulletList']
|
|
600
|
+
|
|
601
|
+
def test_md_table_localids_present(self):
|
|
602
|
+
"""Every table/row/header/cell has a non-empty localId."""
|
|
603
|
+
md = "\n".join([
|
|
604
|
+
"| A | B |",
|
|
605
|
+
"| --- | --- |",
|
|
606
|
+
"| 1 | 2 |",
|
|
607
|
+
])
|
|
608
|
+
nodes = md_to_adf(md)
|
|
609
|
+
t = nodes[0]
|
|
610
|
+
assert t['attrs']['localId'] != ''
|
|
611
|
+
for row in t['content']:
|
|
612
|
+
assert row['localId'] != ''
|
|
613
|
+
for cell in row['content']:
|
|
614
|
+
assert cell['localId'] != ''
|
|
615
|
+
assert 'colwidth' in cell['attrs']
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atlassian_cli-0.10.2 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|