@@ -253,15 +253,18 @@ def add_tag_newline_handling(base_wrapper: LineWrapper) -> LineWrapper:
253253 """
254254
255255 def enhanced_wrapper (text : str , initial_indent : str , subsequent_indent : str ) -> str :
256- # If no newlines, nothing to preserve
256+ # If no newlines in input, just wrap and apply post-processing fixes.
257+ # The base_wrapper may produce multi-line output that needs fixing.
257258 if "\n " not in text :
258- return base_wrapper (text , initial_indent , subsequent_indent )
259+ result = base_wrapper (text , initial_indent , subsequent_indent )
260+ return _fix_multiline_opening_tag_with_closing (result )
259261
260262 lines = text .split ("\n " )
261263
262- # If only one line after split, nothing to preserve
264+ # If only one line after split, same as above
263265 if len (lines ) <= 1 :
264- return base_wrapper (text , initial_indent , subsequent_indent )
266+ result = base_wrapper (text , initial_indent , subsequent_indent )
267+ return _fix_multiline_opening_tag_with_closing (result )
265268
266269 # Check if there are any tags in the text - only apply block content
267270 # heuristics when tags are present to avoid changing normal markdown behavior
@@ -299,7 +302,8 @@ def enhanced_wrapper(text: str, initial_indent: str, subsequent_indent: str) ->
299302
300303 # If we only have one segment, no tag boundaries were found
301304 if len (segments ) == 1 :
302- return base_wrapper (text , initial_indent , subsequent_indent )
305+ result = base_wrapper (text , initial_indent , subsequent_indent )
306+ return _fix_multiline_opening_tag_with_closing (result )
303307
304308 # Wrap each segment separately
305309 wrapped_segments : list [str ] = []
@@ -345,6 +349,10 @@ def enhanced_wrapper(text: str, initial_indent: str, subsequent_indent: str) ->
345349 # The Markdown parser may indent closing tags due to lazy continuation.
346350 result = _fix_closing_tag_spacing (result )
347351
352+ # Fix multi-line opening tags that have closing tags on the same line.
353+ # This works around a Markdoc parser bug (see GitHub issue #17).
354+ result = _fix_multiline_opening_tag_with_closing (result )
355+
348356 return result
349357
350358 return enhanced_wrapper
@@ -394,3 +402,79 @@ def _fix_closing_tag_spacing(text: str) -> str:
394402 fixed_lines .append (line )
395403
396404 return "\n " .join (fixed_lines )
405+
406+
407+ # Pattern to detect closing delimiter of opening tag followed by a closing tag.
408+ # This handles cases like: %}{% /tag %} or --><!-- /tag -->
409+ # where a multi-line opening tag ends and a closing tag follows on the same line.
410+ # Uses named group "closing_tag" to capture the start of the closing tag.
411+ _multiline_closing_pattern : re .Pattern [str ] = re .compile (
412+ rf"{ JINJA_TAG_CLOSE_RE } \s*(?P<closing_tag>{ JINJA_TAG_OPEN_RE } \s*/)|" # %}{% /
413+ rf"{ JINJA_COMMENT_CLOSE_RE } \s*(?P<closing_comment>{ JINJA_COMMENT_OPEN_RE } \s*/)|" # #}{# /
414+ rf"{ JINJA_VAR_CLOSE_RE } \s*(?P<closing_var>{ JINJA_VAR_OPEN_RE } \s*/)|" # }}{{ /
415+ rf"{ HTML_COMMENT_CLOSE_RE } \s*(?P<closing_html>{ HTML_COMMENT_OPEN_RE } \s*/)" # --><!-- /
416+ )
417+
418+
419+ def _fix_multiline_opening_tag_with_closing (text : str ) -> str :
420+ """
421+ Ensure closing tags are on their own line when the opening tag spans multiple lines.
422+
423+ This works around a Markdoc parser bug where multi-line opening tags with
424+ closing tags on the same line cause incorrect AST parsing.
425+
426+ Problem pattern (triggers Markdoc bug):
427+ {% tag attr1=value1
428+ attr2=value2 %}{% /tag %}
429+
430+ Fixed pattern:
431+ {% tag attr1=value1
432+ attr2=value2 %}
433+ {% /tag %}
434+
435+ Single-line paired tags like `{% field %}{% /field %}` are NOT affected.
436+ Tags in the middle of prose like `Before {% field %}{% /field %} after` are
437+ also NOT affected because the line contains content before the tag.
438+ """
439+ # Only apply fix if there are multiple lines - single line input means
440+ # no multi-line tags to fix
441+ if "\n " not in text :
442+ return text
443+
444+ lines = text .split ("\n " )
445+ result_lines : list [str ] = []
446+
447+ for i , line in enumerate (lines ):
448+ # Skip the first line - it can't be a continuation of a multi-line tag
449+ if i == 0 :
450+ result_lines .append (line )
451+ continue
452+
453+ stripped = line .lstrip ()
454+
455+ # Only process lines that are continuations (don't start with a tag opener).
456+ # If a line starts with a tag opener, the tag began on that line, not a continuation.
457+ is_tag_start = (
458+ stripped .startswith (JINJA_TAG_OPEN )
459+ or stripped .startswith (JINJA_COMMENT_OPEN )
460+ or stripped .startswith (JINJA_VAR_OPEN )
461+ or stripped .startswith (HTML_COMMENT_OPEN )
462+ )
463+
464+ if not is_tag_start :
465+ match = _multiline_closing_pattern .search (line )
466+ if match :
467+ # Find which named group matched and split at the closing tag
468+ for group_name in ["closing_tag" , "closing_comment" , "closing_var" , "closing_html" ]:
469+ if match .group (group_name ) is not None :
470+ split_pos = match .start (group_name )
471+ before = line [:split_pos ].rstrip ()
472+ closing = line [split_pos :].lstrip ()
473+ result_lines .append (before )
474+ result_lines .append (closing )
475+ break
476+ continue
477+
478+ result_lines .append (line )
479+
480+ return "\n " .join (result_lines )
0 commit comments