feat(jira): convert {info}/{note}/{warning}/{tip}/{expand} macros to Markdown (read-only) - #1502
Conversation
8f6450d to
80af347
Compare
There was a problem hiding this comment.
Thanks for the contribution @AnubhavSolanki! The macro-specific label mapping and parameter coverage are a useful base.
- The production read path strips the generated expand structure before users receive it, and the round-trip tests bypass that path.
See inline comments for details.
There was a problem hiding this comment.
The updated production-path coverage now catches the original <details> loss, @AnubhavSolanki, and that part is fixed.
- Multi-paragraph admonitions still break during the write-back pass.
See inline comments for details.
The admonition reverse ended the body at the first blank line, so
{info:title=Context}first para\n\nsecond para{info} round-tripped to
{info:...}first para{info}\n\nsecond para — later paragraphs silently
moved outside the callout on edit.
Render admonitions with the same <details><summary> structure {expand}
already uses (protected through _convert_html_to_markdown by
clean_jira_text), with the macro-type label in the summary. A single
_revert_details router restores {info}/{note}/{warning}/{tip} when the
summary carries an admonition label and {expand} otherwise. The explicit
</details> close is the body boundary, so paragraphs, lists and fenced
code all stay inside the restored macro.
Adds clean_jira_text -> markdown_to_jira regressions for multi-paragraph
info and expand bodies.
Github-Issue: sooperset#1502
There was a problem hiding this comment.
Thanks for the follow-up, @AnubhavSolanki. The production-path preservation and explicit boundaries now cover the multi-paragraph, list, and fenced-code cases from the last round.
- Nested admonition and expand blocks still break during write-back.
See inline comments for details.
The <details> reverse used a single non-nesting regex, so a nested block
like {expand:More}{info:title=Context}inside{info}{expand} closed the
outer macro at the first inner </details> and leaked literal
[details]/[summary] markup.
Match only an innermost <details> (summary and body forbidden from
crossing a nested <details>/</details> or </summary>) and loop the
substitution until none remain, so nesting resolves from the inside out
for both {expand}-wrapping-admonition and admonition-wrapping-{expand}.
Adds clean_jira_text -> markdown_to_jira regressions for both nesting
directions.
Github-Issue: sooperset#1502
There was a problem hiding this comment.
Thanks for fixing the nested write-back path, @AnubhavSolanki. The stack-based <details> restoration now handles both mixed nesting directions from the last round, and the focused regressions pass.
- The read-side Jira macro parsing still leaks raw tags for same-type nesting.
See inline comments for details.
jira_to_markdown used non-nesting regexes, so same-type nested macros
like {expand:Outer}{expand:Inner}inside{expand}after{expand} closed the
outer block at the first inner tag and leaked a raw {expand:Inner} /
trailing {expand} into the markdown.
Convert admonition and expand blocks with a stack-based inside-out scan
(_convert_macro_blocks), mirroring the existing _restore_details_blocks
on the write side. Nested same-type and 3-level mixed blocks now close at
the correct boundary, matching Atlassian's nested-macro semantics.
Adds clean_jira_text -> markdown_to_jira regressions for same-type expand,
same-type info, and 3-level mixed nesting (asserting no raw tags leak and
the nesting structure survives).
Github-Issue: sooperset#1502
There was a problem hiding this comment.
Thanks for fixing the same-type read parser, @AnubhavSolanki. The stack pass now handles the nested cases from the last round, and the focused regressions pass.
- The visible summary is still used as the macro-type discriminator, so valid expand titles can change the Jira macro on write-back.
- Option-only expand parameters are exposed as the visible title.
See inline comments for details.
The visible <details> summary doubled as the macro-type discriminator on
write-back, so an expand whose title looked like an admonition label was
silently converted to that admonition: {expand:⚠️ Warning: Deployment
notes}body{expand} round-tripped to {warning:title=Deployment notes}...
A normal edit could replace a collapsible with a warning callout.
Carry the source macro in a data-jira-macro="<kind>" attribute on the
<details> tag (invisible to users, protected through the HTML->markdown
pass) and route markdown_to_jira off that attribute instead of parsing
the summary. Title text can no longer change the macro.
Also fix option-only expand params: {expand:macro-id=123} now shows
'Details' as the summary and drops the internal option, instead of
exposing 'macro-id=123' as the user-visible title.
Adds regressions for each admonition-label-shaped expand title and the
option-only expand form, retaining the nested cases.
Github-Issue: sooperset#1502
There was a problem hiding this comment.
Thanks for fixing the marker-title collision and option-only expand handling, @AnubhavSolanki. The exact-title regressions cover the last review well.
- The new
<details>protection also preserves arbitrary HTML attributes from Jira input, which bypasses the existing HTML conversion.
See inline comments for details.
… HTML The <details> tag-protection in clean_jira_text matched any <details ...> with any attributes, so attacker-supplied markup from Jira input bypassed _convert_html_to_markdown: <details onclick="alert(1)"> survived verbatim where upstream main strips the tag and attributes — an injection vector. Protect only the tags of a block opened by a canonical <details data-jira-macro="<kind>"> marker (_protect_macro_tags): a stack pairs open/close tags, and only the marker block's own <details>, </details>, and inner <summary>/</summary> are shielded. Any other <details> — including onclick/style-bearing input — is left unprotected and stripped as raw HTML. Adds a regression asserting arbitrary onclick/style attributes (and the <details> tag itself) do not survive clean_jira_text, while a generated macro marker still converts and round-trips. Github-Issue: sooperset#1502
There was a problem hiding this comment.
Thanks for tightening the marker protection, @AnubhavSolanki. The unknown-marker and extra-attribute regressions close the HTML bypass from the last round, and the focused macro suite passes.
- Angle-bracket text in macro titles is reinterpreted as HTML and changes during write-back.
- An explicit expand title of
Detailsis dropped during write-back.
See inline comments for details.
|
@sooperset — a human steer would help here before I keep iterating with the review bot. State: all six review rounds are addressed, every inline thread resolved, and CI is green on Python 3.10–3.13 (lint + full test suite). The feature adds bidirectional conversion for Two questions where a maintainer's judgment matters more than another bot round:
No rush — I'll keep refining the review feedback in the meantime. Thanks for the thorough automated review; it caught several real bugs (including an HTML-attribute sanitization gap). |
There was a problem hiding this comment.
Thanks for the follow-up, @AnubhavSolanki. The angle-bracket and explicit Details regressions from the last round are fixed, and the focused macro suite now passes.
- The final entity-decoding pass also rewrites user-authored raw macro parameters and can corrupt their delimiters.
See inline comments for details.
The final entity-decoding pass in markdown_to_jira rewrote every
{info}/{expand} it matched, including user-authored raw macro syntax.
An entity in a raw parameter was decoded — e.g.
markdown_to_jira('{info:title=x}y}body{info}') returned
'{info:title=x}y}body{info}', where } decoded to } and closed the
opening tag early, corrupting the macro.
Mark titles this preprocessor generates (the revert helpers) with a
private NUL-delimited sentinel, and decode entities only for params
carrying it, stripping the sentinel. User-authored raw macros lack the
sentinel and pass through unchanged, so literal entities like } or
& are preserved.
Adds regressions for entity-containing raw macros and a bare body entity.
Github-Issue: sooperset#1502
|
Thanks @AnubhavSolanki — and I appreciate you laying the tradeoff out plainly rather than just landing the bigger version. Made this an easy call. Let's go read-only (Jira→Markdown), same as {panel} works today. Why:
If lossless write-back turns out to be something people actually hit, we can add it later as its own PR. So for this one: strip the write-back path, keep the Jira→Markdown conversion, and squash the history when you finalize. Appreciate the thorough work on this. |
Convert Jira admonition and expand macros to labelled Markdown while reading. Remove the reverse macro conversion and generated HTML markers so Markdown edits cannot mutate user-authored Jira parameters.\n\nGithub-Issue: sooperset#1502
There was a problem hiding this comment.
Thanks for narrowing this to the read-only path, @AnubhavSolanki. Removing write-back cut the risky surface substantially, and the focused macro matrix is a solid base.
- Preserve Markdown block boundaries around nested converted macros. The current stripping joins surrounding prose and corrupts content.
See inline comments for details.
02d0fbb to
595ae5e
Compare
|
Done — went read-only as you decided.
PR title/description updated to reflect the read-only scope. Write-back can be a separate PR if it's ever needed. Thanks for the clear call — agree it's the better shape. |
Github-Issue: sooperset#1502
There was a problem hiding this comment.
Thanks for pushing the nested boundary regressions, @AnubhavSolanki. The exact clean_jira_text() assertions now cover the previous joining bug well.
- HTML-looking title text is still corrupted in the production read path.
- The read-only PR still changes fenced-code write-back without a regression.
See inline comments for details.
47e6b16 to
e8bdf3f
Compare
|
The exact production-path title assertions now cover the previous corruption, @AnubhavSolanki.
See inline comments for details.
|
There was a problem hiding this comment.
The exact production-path title assertions now cover the previous corruption, @AnubhavSolanki.
- Scope the heading protection to headings generated from Jira macros. The current regex also exempts ordinary bold Jira text from HTML conversion.
See inline comments for details.
…Markdown
jira_to_markdown only handled {panel}; the admonition macros
({info}, {note}, {warning}, {tip}) and the {expand} collapsible block
leaked through as raw {macro}...{macro} wiki markup.
Convert them read-only (Jira -> Markdown), consistent with {panel}:
- Admonitions render as a bold labelled heading keeping the macro type,
e.g. **ℹ️ Info: <title>** (or **ℹ️ Info** with no title), then the body.
- {expand} renders as **▸ Expand: <title>** (or **▸ Expand**); the
{expand:Foo} shorthand and {expand:title=Foo} both yield the title,
while option-only params such as {expand:macro-id=123} render as plain
**▸ Expand** without exposing the internal option.
- Titles are HTML-escaped so angle brackets / HTML-looking text survive
the HTML->markdown pass literally instead of being reparsed as tags.
- Generated headings are wrapped in a private sentinel so clean_jira_text
protects only those headings from the HTML pass; user-authored bold text
that merely starts with a macro label is still sanitized normally.
- Nested and same-type-nested macros are handled by a stack-based
inside-out scan; inner code blocks are preserved via placeholder
protection.
Conversion is one-directional by design; write-back (Markdown -> Jira)
is out of scope and can be a follow-up PR.
Github-Issue: sooperset#1503
1b39f1e to
74f8fab
Compare
There was a problem hiding this comment.
The 6 user-authored heading cases now cover the <b>, <a>, and <script> lookalikes from the last round, @AnubhavSolanki.
- User content can still forge the fixed protection marker and bypass HTML conversion.
- Multiline macro titles leak the private marker into the returned text.
See inline comments for details.
| # (see _protect_macro_headings) without matching user-authored bold text that | ||
| # merely happens to start with the same label. NUL bytes cannot occur in | ||
| # normal document text; the sentinel is stripped after the HTML pass. | ||
| _MACRO_HEADING_MARK = "\x00JMH\x00" |
There was a problem hiding this comment.
BLOCKER: This fixed marker isn't proof that the heading came from _convert_macro_blocks. clean_jira_text("\x00JMH\x00**▸ Expand: <script>alert(1)</script>**\x00JMH\x00") treats user-authored text as generated and returns the raw <script> tag. Required for merge: use a protection token or storage path that can't collide with the original input, then add an exact regression for forged marker input.
| mark = re.escape(_MACRO_HEADING_MARK) | ||
| text = _extract_blocks( | ||
| text, | ||
| rf"{mark}(\*\*[^\n]*?\*\*){mark}", |
There was a problem hiding this comment.
IMPORTANT: This extractor only matches one-line headings, while the title parser accepts newlines. clean_jira_text("{info:title=line1\nline2}<p>body</p>{info}") returns raw \x00JMH\x00 markers and escaped bold syntax. Normalize title line breaks or make the protected representation handle them, then assert that the public result contains no sentinel.
|
@sooperset — a scope/direction check before I iterate further, because the review bot and I are converging on diminishing returns. Since you approved read-only, the remaining rounds have all been about one thing: protecting the generated bold heading (e.g.
The root issue is that the generated heading and user content share one text stream, so any in-band signal I use to tell them apart can be spoofed. I can keep hardening it, but I think it's worth your call on scope first: Option A — merge read-only as-is (matching Option B — keep hardening the protection. I fix the forgeable-marker and multiline cases (strip any pre-existing marker from input, normalize title newlines). Doable, but it makes these macros meaningfully more complex than My recommendation is A — match (State: read-only conversion works and is well-tested; CI green on 3.10–3.13; all prior threads resolved.) |
Reported-by: AnubhavSolanki
There was a problem hiding this comment.
The per-input UUID and DOTALL extraction close both cases from the last round, @AnubhavSolanki. The focused forged-marker and multiline-title regressions pass.
- The storage placeholder can still collide with user content and silently duplicate generated headings.
See inline comments for details.
| rf"{mark}(.*?){mark}", | ||
| lambda match: match.group(1), | ||
| macro_headings, | ||
| "JIRAMACRO", |
There was a problem hiding this comment.
BLOCKER: The UUID marker itself can't collide with the source, but _extract_blocks(..., "JIRAMACRO") replaces each protected heading with the fixed \x00JIRAMACRO{index}\x00 placeholder. If the original body already contains \x00JIRAMACRO0\x00, _restore_blocks replaces both occurrences. For example, before\x00JIRAMACRO0\x00{info:title=Safe}body{info}after duplicates the Info heading and deletes the literal user text. This reintroduces the same in-band collision class the latest fix is meant to close. Required for merge: make the extraction placeholder collision-free for the current input, and add an exact clean_jira_text regression that preserves a literal \x00JIRAMACRO0\x00 next to a genuine macro.
Description
jira_to_markdownonly converted the{panel}macro. The admonition macros —{info},{note},{warning},{tip}— and the{expand}collapsible blockleaked through into the output as raw
{macro}...{macro}wiki markup.This adds read-only conversion (Jira → Markdown) for those five macros,
consistent with how
{panel}is handled today. Write-back (Markdown → Jira) isintentionally out of scope per maintainer decision and can be a follow-up PR.
Fixes: #1503
Changes
e.g.
**ℹ️ Info: <title>**(or**ℹ️ Info**with no title), then the body.{expand}renders as**▸ Expand: <title>**(or**▸ Expand**). The{expand:Foo}shorthand and{expand:title=Foo}both yield the title;option-only params such as
{expand:macro-id=123}render as plain**▸ Expand**without exposing the internal option.inside-out scan so they close at the correct boundary; inner code blocks are
preserved via the existing placeholder protection.
<details>, no markers, no HTML-pass protection —so nothing to sanitize and no write-back attack surface.
Testing
converted {info}/{expand} (incl. nested and code-bearing) via clean_jira_text; confirmed no raw macro tags leakTestAdmonitionAndExpandBlockscovers all four admonition types (titled/untitled),multiline bodies, adjacent-block isolation,
{expand}title/option forms,same-type and 3-level mixed nesting (asserting no raw tags leak), inner code
preservation, and survival through the full
clean_jira_textfetch path.Checklist