Skip to content

Commit 3ce614a

Browse files
committed
Add children macro and TOC maxLevel round-trip support
[CHILDREN] maps to ac:name="children" (self-closing); [TOC maxLevel=N] preserves the maxLevel parameter through pull and push. Nine new tests cover both macros.
1 parent 9d7e441 commit 3ce614a

3 files changed

Lines changed: 83 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# CLAUDE.md
23

34
This file provides context for Claude Code when working in this project.
@@ -74,7 +75,8 @@ open/close pairs and cannot consume content across tag boundaries.
7475
|---|---|---|---|
7576
| `code` | ` ```lang ``` ` fenced block | `ac:structured-macro ac:name="code"` | Full round-trip |
7677
| `noformat` | ` ```noformat ``` ` fenced block | `ac:structured-macro ac:name="noformat"` | Full round-trip |
77-
| `toc` | `[TOC]` placeholder | `ac:structured-macro ac:name="toc"` | Full round-trip |
78+
| `toc` | `[TOC]` or `[TOC maxLevel=N]` placeholder | `ac:structured-macro ac:name="toc"` | Full round-trip; `maxLevel` param preserved |
79+
| `children` | `[CHILDREN]` placeholder | `ac:structured-macro ac:name="children"` | Full round-trip |
7880
| `expand` | `> [!EXPAND] Title` blockquote | `ac:structured-macro ac:name="expand"` | Full round-trip; code inside expand becomes fenced on pull |
7981
| `note`/`info`/`warning`/`tip` | `> [!NOTE]` etc. GFM alerts | `ac:structured-macro ac:name="note\|info\|..."` | Full round-trip |
8082
| `jira` | `[KEY-123](jira_url/browse/KEY-123)` | `ac:structured-macro ac:name="jira"` | Full round-trip |

py_conf_sync.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ def update_page(self, page_id: str, title: str, storage_body: str, version: int)
203203
r'<ac:structured-macro[^>]*\bac:name="toc"(?:[^/>]|/(?!>))*(?:/>|>.*?</ac:structured-macro>)',
204204
re.DOTALL,
205205
)
206+
_CHILDREN_MACRO_RE = re.compile(
207+
r'<ac:structured-macro[^>]*\bac:name="children"(?:[^/>]|/(?!>))*(?:/>|>.*?</ac:structured-macro>)',
208+
re.DOTALL,
209+
)
206210
_PANEL_MACRO_RE = re.compile(
207211
r'<ac:structured-macro[^>]*\bac:name="(note|info|warning|tip)"[^>]*>'
208212
r'.*?<ac:rich-text-body[^>]*>(.*?)</ac:rich-text-body>'
@@ -360,7 +364,12 @@ def storage_to_markdown(storage_html: str, jira_url: str | None = None, base_url
360364
# TODO: add round-trip support for status badges
361365
# (ac:structured-macro ac:name="status") →
362366
# inline marker e.g. `[STATUS:colour:label]`, restored on push.
363-
cleaned = _TOC_MACRO_RE.sub('<p>[TOC]</p>', storage_html)
367+
def _replace_toc_macro(m):
368+
ml = re.search(r'<ac:parameter\s+ac:name="maxLevel">(\d+)</ac:parameter>', m.group(0))
369+
return f'<p>[TOC maxLevel={ml.group(1)}]</p>' if ml else '<p>[TOC]</p>'
370+
371+
cleaned = _TOC_MACRO_RE.sub(_replace_toc_macro, storage_html)
372+
cleaned = _CHILDREN_MACRO_RE.sub('<p>[CHILDREN]</p>', cleaned)
364373
cleaned = _CODE_MACRO_RE.sub(lambda m: _replace_code_macro(m.group(0)), cleaned)
365374
cleaned = _NOFORMAT_MACRO_RE.sub(lambda m: _replace_noformat_macro(m.group(0)), cleaned)
366375
cleaned = _JIRA_MACRO_RE.sub(lambda m: _replace_jira_macro(m.group(0), jira_url), cleaned)
@@ -432,9 +441,19 @@ def markdown_to_storage(markdown_text: str, base_url: str | None = None, page_id
432441

433442
html = re.sub(r"<br>", "<br />", html)
434443
html = re.sub(r"<hr>", "<hr />", html)
444+
def _toc_to_storage(m):
445+
if m.group(1):
446+
return (
447+
'<ac:structured-macro ac:name="toc" ac:schema-version="1">'
448+
f'<ac:parameter ac:name="maxLevel">{m.group(1)}</ac:parameter>'
449+
'</ac:structured-macro>'
450+
)
451+
return '<ac:structured-macro ac:name="toc" ac:schema-version="1"></ac:structured-macro>'
452+
453+
html = re.sub(r'<p>\[TOC(?:\s+maxLevel=(\d+))?\]</p>', _toc_to_storage, html)
435454
html = re.sub(
436-
r'<p>\[TOC\]</p>',
437-
'<ac:structured-macro ac:name="toc" ac:schema-version="1"></ac:structured-macro>',
455+
r'<p>\[CHILDREN\]</p>',
456+
'<ac:structured-macro ac:name="children" ac:schema-version="1"></ac:structured-macro>',
438457
html,
439458
)
440459

tests/test_conversion.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,64 @@ def test_toc_round_trip(self):
442442
result = markdown_to_storage(md)
443443
assert 'ac:name="toc"' in result
444444

445+
def test_pull_toc_with_max_level(self):
446+
storage = (
447+
'<ac:structured-macro ac:name="toc" ac:schema-version="1" ac:macro-id="abc">'
448+
'<ac:parameter ac:name="maxLevel">2</ac:parameter>'
449+
'</ac:structured-macro>'
450+
)
451+
result = storage_to_markdown(storage)
452+
assert "[TOC maxLevel=2]" in result
453+
454+
def test_push_toc_with_max_level(self):
455+
result = markdown_to_storage("[TOC maxLevel=3]")
456+
assert 'ac:name="toc"' in result
457+
assert '<ac:parameter ac:name="maxLevel">3</ac:parameter>' in result
458+
459+
def test_toc_max_level_round_trip(self):
460+
storage = (
461+
'<ac:structured-macro ac:name="toc" ac:schema-version="1">'
462+
'<ac:parameter ac:name="maxLevel">2</ac:parameter>'
463+
'</ac:structured-macro>'
464+
)
465+
md = storage_to_markdown(storage)
466+
result = markdown_to_storage(md)
467+
assert 'ac:name="toc"' in result
468+
assert '<ac:parameter ac:name="maxLevel">2</ac:parameter>' in result
469+
470+
def test_push_toc_without_max_level_has_no_param(self):
471+
result = markdown_to_storage("[TOC]")
472+
assert 'ac:name="toc"' in result
473+
assert 'maxLevel' not in result
474+
475+
476+
class TestChildren:
477+
def test_pull_children_self_closing(self):
478+
storage = '<ac:structured-macro ac:name="children" ac:schema-version="2" ac:macro-id="abc123" />'
479+
result = storage_to_markdown(storage)
480+
assert "[CHILDREN]" in result
481+
482+
def test_push_children_placeholder(self):
483+
result = markdown_to_storage("[CHILDREN]")
484+
assert 'ac:name="children"' in result
485+
486+
def test_children_round_trip(self):
487+
storage = '<ac:structured-macro ac:name="children" ac:schema-version="2" ac:macro-id="abc" />'
488+
md = storage_to_markdown(storage)
489+
result = markdown_to_storage(md)
490+
assert 'ac:name="children"' in result
491+
492+
def test_pull_children_does_not_eat_following_content(self):
493+
storage = (
494+
'<ac:structured-macro ac:name="children" ac:schema-version="2" ac:macro-id="abc" />'
495+
'<h2>Section Heading</h2>'
496+
'<p>Paragraph text.</p>'
497+
)
498+
result = storage_to_markdown(storage)
499+
assert "[CHILDREN]" in result
500+
assert "Section Heading" in result
501+
assert "Paragraph text" in result
502+
445503

446504
class TestNoformat:
447505
def test_pull_noformat_macro(self):

0 commit comments

Comments
 (0)