4949 ExpectedPathArg ,
5050 FetchError ,
5151 ImageSuggested ,
52+ InvalidDirectiveStructure ,
5253 InvalidField ,
5354 InvalidLiteralInclude ,
5455 InvalidTableStructure ,
5556 InvalidURL ,
5657 MalformedGlossary ,
58+ MissingChild ,
5759 RemovedLiteralBlockSyntax ,
5860 TabMustBeDirective ,
5961 TodoInfo ,
@@ -454,6 +456,9 @@ def dispatch_departure(self, node: docutils.nodes.Node) -> None:
454456 elif isinstance (popped , n .Directive ) and popped .name == "tabs" :
455457 self .validate_tabs_children (popped )
456458
459+ elif isinstance (popped , n .Directive ) and popped .name == "io-code-block" :
460+ self .diagnostics .extend (_validate_io_code_block_children (popped ))
461+
457462 elif (
458463 isinstance (popped , n .Directive )
459464 and f"{ popped .domain } :{ popped .name } " == ":glossary"
@@ -700,9 +705,12 @@ def create_page() -> Tuple[Page, EmbeddedRstParser]:
700705 )
701706 return doc
702707
703- elif name == "literalinclude" :
708+ elif name == "literalinclude" or name == "input" or name == "output" :
709+ if name == "literalinclude" :
710+ if argument_text is None :
711+ self .diagnostics .append (ExpectedPathArg (name , line ))
712+ return doc
704713 if argument_text is None :
705- self .diagnostics .append (ExpectedPathArg (name , line ))
706714 return doc
707715
708716 _ , filepath = util .reroot_path (
@@ -722,80 +730,88 @@ def create_page() -> Tuple[Page, EmbeddedRstParser]:
722730 return doc
723731
724732 lines = text .split ("\n " )
733+ # Capture the original file-length before splicing it
734+ len_file = len (lines )
725735
726- def _locate_text (text : str ) -> int :
727- """
728- Searches the literally-included file ('lines') for the specified text. If no such text is found,
729- add an InvalidLiteralInclude diagnostic.
730- """
731- assert isinstance (text , str )
732- loc = next ((idx for idx , line in enumerate (lines ) if text in line ), - 1 )
733- if loc < 0 :
734- self .diagnostics .append (
735- InvalidLiteralInclude (f'"{ text } " not found in { filepath } ' , line )
736- )
737- return loc
738-
739- # Locate the start_after query
740- start_after = 0
741- if "start-after" in options :
742- start_after_text = options ["start-after" ]
743- start_after = _locate_text (start_after_text )
744- # Only increment start_after if text is specified, to avoid capturing the start_after_text
745- start_after += 1
746-
747- # ...now locate the end_before query
748- end_before = len (lines )
749- if "end-before" in options :
750- end_before_text = options ["end-before" ]
751- end_before = _locate_text (end_before_text )
752-
753- # Check that start_after_text precedes end_before_text (and end_before exists)
754- if start_after >= end_before >= 0 :
755- self .diagnostics .append (
756- InvalidLiteralInclude (
757- f'"{ end_before_text } " precedes "{ start_after_text } " in { filepath } ' ,
758- line ,
759- )
760- )
736+ if name == "literalinclude" :
761737
762- # If we failed to locate end_before text, default to the end-of-file
763- if end_before == - 1 :
738+ def _locate_text (text : str ) -> int :
739+ """
740+ Searches the literally-included file ('lines') for the specified text. If no such text is found,
741+ add an InvalidLiteralInclude diagnostic.
742+ """
743+ assert isinstance (text , str )
744+ loc = next (
745+ (idx for idx , line in enumerate (lines ) if text in line ), - 1
746+ )
747+ if loc < 0 :
748+ self .diagnostics .append (
749+ InvalidLiteralInclude (
750+ f'"{ text } " not found in { filepath } ' , line
751+ )
752+ )
753+ return loc
754+
755+ # Locate the start_after query
756+ start_after = 0
757+ if "start-after" in options :
758+ start_after_text = options ["start-after" ]
759+ # start_after = self._locate_text(start_after_text, lines, line, text)
760+ start_after = _locate_text (start_after_text )
761+ # Only increment start_after if text is specified, to avoid capturing the start_after_text
762+ start_after += 1
763+
764+ # ...now locate the end_before query
764765 end_before = len (lines )
766+ if "end-before" in options :
767+ end_before_text = options ["end-before" ]
768+ # end_before = self._locate_text(end_before_text, lines, line, text)
769+ end_before = _locate_text (end_before_text )
765770
766- # Capture the original file-length before splicing it
767- len_file = len (lines )
768- lines = lines [start_after :end_before ]
769-
770- dedent = 0
771- if "dedent" in options :
772- # Dedent is specified as a flag
773- if isinstance (options ["dedent" ], bool ):
774- # Deduce a reasonable dedent
775- try :
776- dedent = min (
777- len (line ) - len (line .lstrip ())
778- for line in lines
779- if len (line .lstrip ()) > 0
780- )
781- except ValueError :
782- # Handle the (unlikely) case where there are no non-empty lines
783- dedent = 0
784- # Dedent is specified as a nonnegative integer (number of characters):
785- # Note: since boolean is a subtype of int, this conditonal must follow the
786- # above bool-type conditional.
787- elif isinstance (options ["dedent" ], int ):
788- dedent = options ["dedent" ]
789- else :
771+ # Check that start_after_text precedes end_before_text (and end_before exists)
772+ if start_after >= end_before >= 0 :
790773 self .diagnostics .append (
791774 InvalidLiteralInclude (
792- f'Dedent " { dedent } " of type { type ( dedent ) } ; expected nonnegative integer or flag ' ,
775+ f'" { end_before_text } " precedes " { start_after_text } " in { filepath } ' ,
793776 line ,
794777 )
795778 )
796- return doc
797779
798- lines = [line [dedent :] for line in lines ]
780+ # If we failed to locate end_before text, default to the end-of-file
781+ if end_before == - 1 :
782+ end_before = len (lines )
783+
784+ lines = lines [start_after :end_before ]
785+
786+ dedent = 0
787+ if "dedent" in options :
788+ # Dedent is specified as a flag
789+ if isinstance (options ["dedent" ], bool ):
790+ # Deduce a reasonable dedent
791+ try :
792+ dedent = min (
793+ len (line ) - len (line .lstrip ())
794+ for line in lines
795+ if len (line .lstrip ()) > 0
796+ )
797+ except ValueError :
798+ # Handle the (unlikely) case where there are no non-empty lines
799+ dedent = 0
800+ # Dedent is specified as a nonnegative integer (number of characters):
801+ # Note: since boolean is a subtype of int, this conditonal must follow the
802+ # above bool-type conditional.
803+ elif isinstance (options ["dedent" ], int ):
804+ dedent = options ["dedent" ]
805+ else :
806+ self .diagnostics .append (
807+ InvalidLiteralInclude (
808+ f'Dedent "{ dedent } " of type { type (dedent )} ; expected nonnegative integer or flag' ,
809+ line ,
810+ )
811+ )
812+ return doc
813+
814+ lines = [line [dedent :] for line in lines ]
799815
800816 emphasize_lines = None
801817 if "emphasize-lines" in options :
@@ -806,7 +822,8 @@ def _locate_text(text: str) -> int:
806822 except ValueError as err :
807823 self .diagnostics .append (
808824 InvalidLiteralInclude (
809- f"Invalid emphasize-lines specification caused: { err } " , line
825+ f"Invalid emphasize-lines specification caused: { err } " ,
826+ line ,
810827 )
811828 )
812829
@@ -833,6 +850,16 @@ def _locate_text(text: str) -> int:
833850
834851 doc .children .append (code )
835852
853+ elif name == "io-code-block" :
854+ if argument_text is not None :
855+ self .diagnostics .append (
856+ InvalidDirectiveStructure (
857+ "did not expect an argument, language should be passed as an option to input/output directives" ,
858+ line ,
859+ )
860+ )
861+ return doc
862+
836863 elif name == "include" :
837864 if argument_text is None :
838865 self .diagnostics .append (ExpectedPathArg (name , util .get_line (node )))
@@ -984,6 +1011,75 @@ def __make_child_visitor(self) -> "JSONVisitor":
9841011 return visitor
9851012
9861013
1014+ def _validate_io_code_block_children (node : n .Directive ) -> List [Diagnostic ]:
1015+ """Validates that a given io-code-block directive has 1 input and 1 output
1016+ child nodes, and copies the io-code-block's options into the options of the
1017+ underlying code nodes."""
1018+ # new_children should contain input and output directives
1019+ new_children : List [n .Node ] = []
1020+ line = node .start [0 ]
1021+ expected_children = {"input" , "output" }
1022+ diagnostics : List [Diagnostic ] = []
1023+
1024+ for child in node .children :
1025+ if not isinstance (child , n .Directive ):
1026+ diagnostics .append (
1027+ InvalidDirectiveStructure (
1028+ f"expected input/output child directives, saw { child .type } " ,
1029+ line ,
1030+ )
1031+ )
1032+ continue
1033+
1034+ if child .name in expected_children :
1035+ new_grandchildren : List [n .Node ] = []
1036+
1037+ # Input or output should have 1 child Code node
1038+ if len (child .children ) == 1 :
1039+ expected_children .remove (child .name )
1040+
1041+ # child nodes for input/output will inherit parent options
1042+ grandchild = child .children [0 ]
1043+ if isinstance (grandchild , n .Code ):
1044+ grandchild .lang = (
1045+ child .options ["language" ]
1046+ if "language" in child .options
1047+ else None
1048+ )
1049+ grandchild .caption = (
1050+ node .options ["caption" ] if "caption" in node .options else None
1051+ )
1052+ grandchild .copyable = (
1053+ True
1054+ if "copyable" in node .options and node .options ["copyable" ]
1055+ else False
1056+ )
1057+ new_grandchildren .append (grandchild )
1058+ child .children = new_grandchildren
1059+ new_children .append (child )
1060+ else :
1061+ # either duplicate input/output or invalid child is provided
1062+ msg = f"already contains an { child .name } directive"
1063+ if not (child .name == "input" or child .name == "output" ):
1064+ msg = f"does not accept child { child .name } "
1065+ diagnostics .append (
1066+ InvalidDirectiveStructure (
1067+ msg ,
1068+ line ,
1069+ )
1070+ )
1071+
1072+ # handle missing nested input and/or output directives
1073+ if len (expected_children ) != 0 or len (new_children ) != 2 :
1074+ for expected_child in expected_children :
1075+ diagnostics .append (MissingChild ("io-code-block" , expected_child , line ))
1076+ if expected_child == "input" :
1077+ new_children = []
1078+
1079+ node .children = new_children
1080+ return diagnostics
1081+
1082+
9871083class InlineJSONVisitor (JSONVisitor ):
9881084 """A JSONVisitor subclass which does not emit block nodes."""
9891085
0 commit comments