@@ -501,6 +501,59 @@ def build_docs(files_docs: list[FileDoc], source_root: str, output_root: str) ->
501501 print (f"Documentation generation complete. Output written to { out_root } " )
502502
503503
504+ def _escape_curly_braces (text : str ) -> str :
505+ """Escape { and } outside fenced and inline code blocks so MDX doesn't treat them as JSX."""
506+ triple_segments = re .split (r"(```.*?```)" , text , flags = re .DOTALL )
507+ for i , triple_seg in enumerate (triple_segments ):
508+ if triple_seg .startswith ("```" ):
509+ continue
510+ single_segments = re .split (r"(`[^`]*`)" , triple_seg )
511+ for j , single_seg in enumerate (single_segments ):
512+ if not single_seg .startswith ("`" ):
513+ single_seg = single_seg .replace ("{" , r"\{" ).replace ("}" , r"\}" )
514+ single_segments [j ] = single_seg
515+ triple_segments [i ] = "" .join (single_segments )
516+ return "" .join (triple_segments )
517+
518+
519+ def _convert_rst_code_blocks (text : str ) -> str :
520+ """Convert RST :: indented code blocks to fenced ```python blocks."""
521+ lines = text .split ("\n " )
522+ out : list [str ] = []
523+ i = 0
524+ while i < len (lines ):
525+ line = lines [i ]
526+ if line .rstrip ().endswith ("::" ):
527+ # Emit the line with :: stripped (or removed if standalone)
528+ stripped = line .rstrip ()[:- 2 ].rstrip ()
529+ out .append (stripped )
530+ i += 1
531+ # Collect the indented block that follows
532+ block : list [str ] = []
533+ while i < len (lines ) and (lines [i ].strip () == "" or lines [i ].startswith (" " )):
534+ block .append (lines [i ])
535+ i += 1
536+ # Strip trailing blank lines from block
537+ while block and block [- 1 ].strip () == "" :
538+ block .pop ()
539+ if block :
540+ out .append ("```python" )
541+ for bl in block :
542+ out .append (bl [4 :] if bl .startswith (" " ) else bl )
543+ out .append ("```" )
544+ else :
545+ out .append (line )
546+ i += 1
547+ return "\n " .join (out )
548+
549+
550+ def _sanitize_description (text : str ) -> str :
551+ """Convert RST code blocks and escape MDX-unsafe curly braces in description text."""
552+ text = _convert_rst_code_blocks (text )
553+ text = _escape_curly_braces (text )
554+ return text
555+
556+
504557def write_function (parts : list [str ], fn : Any , heading_level : int = 3 ) -> None :
505558 """
506559 Append formatted documentation for a function to the provided parts list.
@@ -537,34 +590,15 @@ def write_function(parts: list[str], fn: Any, heading_level: int = 3) -> None:
537590 fdoc = fn .doc
538591 if isinstance (fdoc , dict ):
539592 if fdoc .get ("description" ):
540- parts .append (f"{ fdoc .get ('description' )} \n " )
593+ parts .append (f"{ _sanitize_description ( fdoc .get ('description' ) )} \n " )
541594 if fdoc .get ("params" ):
542595 parts .append ("**Arguments**\n " )
543596 for p in fdoc .get ("params" , []):
544597 name = p .get ("name" ) or "<arg>"
545598 t = p .get ("type" )
546599 desc = p .get ("description" ) or ""
547600 desc = desc .replace ("\n " , "\n " )
548-
549- # Escape curly braces outside code blocks (triple or single backticks)
550- def escape_curly_braces (text : str ) -> str :
551- # Split by triple backtick code blocks first
552- triple_segments = re .split (r"(```.*?```)" , text , flags = re .DOTALL )
553- for i , triple_seg in enumerate (triple_segments ):
554- if triple_seg .startswith ("```" ):
555- # Inside triple backtick code block, leave as is
556- continue
557- # Now split by single backtick code blocks
558- single_segments = re .split (r"(`[^`]*`)" , triple_seg )
559- for j , single_seg in enumerate (single_segments ):
560- if not single_seg .startswith ("`" ):
561- single_seg = single_seg .replace ("{" , r"\{" ).replace ("}" , r"\}" )
562- single_segments [j ] = single_seg
563- triple_segments [i ] = "" .join (single_segments )
564- return "" .join (triple_segments )
565-
566- desc = escape_curly_braces (desc )
567-
601+ desc = _escape_curly_braces (desc )
568602 if t :
569603 parts .append (f"- `{ name } ` (`{ t } `): { desc } " )
570604 else :
@@ -574,37 +608,36 @@ def escape_curly_braces(text: str) -> str:
574608 parts .append ("**Raises**\n " )
575609 for r in fdoc .get ("raises" , []):
576610 typ = r .get ("type" ) or "Exception"
577- desc = r .get ("description" ) or ""
611+ desc = _escape_curly_braces ( r .get ("description" ) or "" )
578612 parts .append (f"- `{ typ } `: { desc } " )
579613 parts .append ("" )
580614 if fdoc .get ("returns" ):
581615 parts .append ("**Returns**\n " )
582616 ret = fdoc .get ("returns" )
583617 if ret :
584618 rtyp = ret .get ("type" ) or ""
585- rdesc = ret .get ("description" ) or ""
619+ rdesc = _escape_curly_braces ( ret .get ("description" ) or "" )
586620 if rtyp :
587621 parts .append (f"- `{ rtyp } `: { rdesc } \n " )
588622 else :
589623 parts .append (f"- { rdesc } \n " )
590624 if fdoc .get ("examples" ):
591625 parts .append ("**Examples**\n " )
592- is_in_code_block = False
593626 for ex in fdoc .get ("examples" , []):
594627 code = ex .get ("code" )
595- if code :
596- if not is_in_code_block and code . startswith ( ">>>" ):
597- is_in_code_block = True
598- parts . append ( "```python" )
599- if is_in_code_block and not code . startswith ( ">>>" ) and not code . startswith ( "..." ):
600- is_in_code_block = False
601- parts .append ("```\n " )
602- if not code . startswith ( ">>>" ) and not code . startswith ( "..." ):
603- parts .append (code )
604- else :
605- parts .append (code [ 4 :] )
606- if is_in_code_block :
607- parts .append ("```\n " )
628+ if not code :
629+ continue
630+ if "```" in code :
631+ # Already contains fenced blocks — output as-is (may include prose + code )
632+ parts . append ( code )
633+ elif code . startswith ( ">>>" ) or code . startswith ( "..." ):
634+ parts .append ("```python " )
635+ parts . append ( code [ 4 :])
636+ parts .append ("```" )
637+ else :
638+ parts .append ("```python" )
639+ parts . append ( code )
640+ parts .append ("```" )
608641 parts .append ("" )
609642 if fdoc .get ("notes" ):
610643 parts .append ("**Notes**\n " )
@@ -651,44 +684,43 @@ def write_class(parts: list[str], cls: Any) -> None:
651684 cdoc = cls .doc
652685 if isinstance (cdoc , dict ):
653686 if cdoc .get ("description" ):
654- parts .append (f"{ cdoc .get ('description' )} \n " )
687+ parts .append (f"{ _sanitize_description ( cdoc .get ('description' ) )} \n " )
655688 if cdoc .get ("params" ):
656689 parts .append ("**Arguments**\n " )
657690 for p in cdoc .get ("params" , []):
658691 name = p .get ("name" ) or "<arg>"
659692 t = p .get ("type" )
660693 desc = p .get ("description" ) or ""
661694 desc = desc .replace ("\n " , "\n " )
695+ desc = _escape_curly_braces (desc )
662696 if t :
663697 parts .append (f"- `{ name } ` (`{ t } `): { desc } \n " )
664698 else :
665699 parts .append (f"- `{ name } `: { desc } \n " )
666700
667701 if cdoc .get ("examples" ):
668702 parts .append ("**Examples**\n " )
669- is_in_code_block = False
670703 for ex in cdoc .get ("examples" , []):
671704 code = ex .get ("code" )
672- if code :
673- if not is_in_code_block and code .startswith (">>>" ):
674- is_in_code_block = True
675- parts .append ("```python" )
676- if is_in_code_block and not code .startswith (">>>" ) and not code .startswith ("..." ):
677- is_in_code_block = False
678- parts .append ("```\n " )
679- if not code .startswith (">>>" ) and not code .startswith ("..." ):
680- parts .append (code )
681- else :
682- parts .append (code [4 :])
683- if is_in_code_block :
684- parts .append ("```\n " )
705+ if not code :
706+ continue
707+ if "```" in code :
708+ parts .append (code )
709+ elif code .startswith (">>>" ) or code .startswith ("..." ):
710+ parts .append ("```python" )
711+ parts .append (code [4 :])
712+ parts .append ("```" )
713+ else :
714+ parts .append ("```python" )
715+ parts .append (code )
716+ parts .append ("```" )
685717 parts .append ("" )
686718 if cdoc .get ("returns" ):
687719 parts .append ("**Returns**\n " )
688720 ret = cdoc .get ("returns" )
689721 if ret :
690722 rtyp = ret .get ("type" ) or ""
691- rdesc = ret .get ("description" ) or ""
723+ rdesc = _escape_curly_braces ( ret .get ("description" ) or "" )
692724 if rtyp :
693725 parts .append (f"- `{ rtyp } `: { rdesc } \n " )
694726 else :
@@ -736,17 +768,15 @@ def write_module(fd: FileDoc, parts: list[str]) -> None:
736768 md = fd .module .doc
737769 if isinstance (md , dict ):
738770 if md .get ("description" ):
739- parts .append (f"{ md .get ('description' )} \n " )
771+ parts .append (f"{ _sanitize_description ( md .get ('description' ) )} \n " )
740772 if md .get ("params" ):
741773 parts .append ("**Arguments**\n " )
742774 for p in md .get ("params" , []):
743775 name = p .get ("name" ) or "<arg>"
744776 t = p .get ("type" )
745777 desc = p .get ("description" ) or ""
746-
747- # If description is multi-line, indent subsequent lines for better Markdown rendering
748778 desc = desc .replace ("\n " , "\n " )
749-
779+ desc = _escape_curly_braces ( desc )
750780 if t :
751781 parts .append (f"- `{ name } ` (`{ t } `): { desc } \n " )
752782 else :
@@ -755,14 +785,14 @@ def write_module(fd: FileDoc, parts: list[str]) -> None:
755785 parts .append ("**Raises**\n " )
756786 for r in md .get ("raises" , []):
757787 typ = r .get ("type" ) or "Exception"
758- desc = r .get ("description" ) or ""
788+ desc = _escape_curly_braces ( r .get ("description" ) or "" )
759789 parts .append (f"- `{ typ } `: { desc } \n " )
760790 if md .get ("returns" ):
761791 parts .append ("**Returns**\n " )
762792 ret = md .get ("returns" )
763793 if ret :
764794 rtyp = ret .get ("type" ) or ""
765- rdesc = ret .get ("description" ) or ""
795+ rdesc = _escape_curly_braces ( ret .get ("description" ) or "" )
766796 if rtyp :
767797 parts .append (f"- `{ rtyp } `: { rdesc } \n " )
768798 else :
0 commit comments