1# AutoDoc: Generate documentation from GAP source code
2#
3# Copyright of AutoDoc belongs to its developers.
4# Please refer to the COPYRIGHT file for details.
5#
6# SPDX-License-Identifier: GPL-2.0-or-later
7
8##
9InstallGlobalFunction( Scan_for_AutoDoc_Part,
10  function( line, plain_text_mode )
11    local position, whitespace_position, command, argument;
12    #! @DONT_SCAN_NEXT_LINE
13    position := PositionSublist( line, "#!" );
14    if position = fail and plain_text_mode = false then
15        return [ false, line ];
16    fi;
17    if plain_text_mode <> true then
18        line := StripBeginEnd( line{[ position + 2 .. Length( line ) ]}, " " );
19    fi;
20    ## Scan for a command
21    position := PositionSublist( line, "@" );
22    if position = fail then
23        return [ "STRING", line ];
24    fi;
25    whitespace_position := PositionSublist( line, " " );
26    if whitespace_position = fail then
27        command := line{[ position .. Length( line ) ]};
28        argument := "";
29    else
30        command := line{[ position .. whitespace_position - 1 ]};
31        argument := line{[ whitespace_position + 1 .. Length( line ) ]};
32    fi;
33    return [ command, argument ];
34end );
35
36## Scans a string for <element> after <element_not_before_element> appeared.
37## This is necessary to scan the filter list for method declarations
38## that contain \[\].
39BindGlobal( "AUTODOC_PositionElementIfNotAfter",
40  function( list, element, element_not_before_element )
41    local current_pos;
42    if not IsList( list ) then
43        Error( "<list> must be a list" );
44    fi;
45    if Length( list ) > 0 and list[ 1 ] = element then
46        return 1;
47    fi;
48
49    for current_pos in [ 2 .. Length( list ) ] do
50        if list[ current_pos ] = element and list[ current_pos - 1 ] <> element_not_before_element then
51            return current_pos;
52        fi;
53    od;
54    return fail;
55end );
56
57
58BindGlobal( "AutoDoc_PrintWarningForConstructor",
59            AutoDoc_CreatePrintOnceFunction( "Installed GAPDoc version does not support constructors" ) );
60
61##
62InstallGlobalFunction( AutoDoc_Type_Of_Item,
63  function( current_item, type, default_chapter_data )
64    local item_rec, entries, has_filters, ret_val;
65    item_rec := current_item;
66    if PositionSublist( type, "DeclareCategoryCollections") <> fail then
67        entries := [ "Filt", "categories" ];
68        ret_val := "<C>true</C> or <C>false</C>";
69        has_filters := "No";
70        if not IsBound( item_rec!.arguments ) then
71            item_rec!.arguments := "obj";
72        fi;
73        item_rec!.coll_suffix := true;
74    elif PositionSublist( type, "DeclareCategory" ) <> fail then
75        entries := [ "Filt", "categories" ];
76        ret_val := "<C>true</C> or <C>false</C>";
77        has_filters := 1;
78    elif PositionSublist( type, "DeclareRepresentation" ) <> fail then
79        entries := [ "Filt", "categories" ];
80        ret_val := "<C>true</C> or <C>false</C>";
81        has_filters := 1;
82    elif PositionSublist( type, "DeclareAttribute" ) <> fail then
83        entries := [ "Attr", "attributes" ];
84        has_filters := 1;
85    elif PositionSublist( type, "DeclareProperty" ) <> fail then
86        entries := [ "Prop", "properties" ];
87        ret_val := "<C>true</C> or <C>false</C>";
88        has_filters := 1;
89    elif PositionSublist( type, "DeclareOperation" ) <> fail then
90        entries := [ "Oper", "methods" ];
91        has_filters := "List";
92    elif PositionSublist( type, "DeclareConstructor" ) <> fail then
93        if IsPackageMarkedForLoading( "GAPDoc", ">=1.6.1" ) then
94            entries := [ "Constr", "methods" ];
95        else
96            AutoDoc_PrintWarningForConstructor();
97            entries := [ "Oper", "methods" ];
98        fi;
99        has_filters := "List";
100    elif PositionSublist( type, "DeclareGlobalFunction" ) <> fail then
101        entries := [ "Func", "global_functions" ];
102        has_filters := "No";
103        if not IsBound( item_rec!.arguments ) then
104            item_rec!.arguments := "arg";
105        fi;
106    elif PositionSublist( type, "DeclareGlobalVariable" ) <> fail then
107        entries := [ "Var", "global_variables" ];
108        has_filters := "No";
109        item_rec!.arguments := fail;
110        item_rec!.return_value := false;
111    elif PositionSublist( type, "DeclareFilter" ) <> fail then
112        entries := [ "Filt", "properties" ];
113        has_filters := "No";
114        item_rec!.arguments := fail;
115        item_rec!.return_value := false;
116    elif PositionSublist( type, "DeclareInfoClass" ) <> fail then
117        entries := [ "InfoClass", "info_classes" ];
118        has_filters := "No";
119        item_rec!.arguments := fail;
120        item_rec!.return_value := false;
121    elif PositionSublist( type, "KeyDependentOperation" ) <> fail then
122        entries := [ "Oper", "methods" ];
123        has_filters := 2;
124    else
125        return fail;
126    fi;
127    item_rec!.item_type := entries[ 1 ];
128    if not IsBound( item_rec!.chapter_info ) or item_rec!.chapter_info = [ ] then
129        item_rec!.chapter_info := default_chapter_data.( entries[ 2 ] );
130    fi;
131    if IsBound( ret_val ) and ( item_rec!.return_value = [ ] or item_rec!.return_value = false ) then
132        item_rec!.return_value := [ ret_val ];
133    fi;
134    return has_filters;
135end );
136
137##
138InstallGlobalFunction( AutoDoc_Parser_ReadFiles,
139  function( filename_list, tree, default_chapter_data )
140    local current_item, flush_and_recover, chapter_info, current_string_list,
141          Scan_for_Declaration_part, flush_and_prepare_for_item, current_line, filestream,
142          level_scope, scope_group, read_example, command_function_record, autodoc_read_line,
143          current_command, was_declaration, filename, system_scope, groupnumber, rest_of_file_skipped,
144          context_stack, new_man_item, add_man_item, Reset, read_code, title_item, title_item_list, plain_text_mode,
145          current_line_unedited, deprecated,
146          ReadLineWithLineCount, Normalized_ReadLine, line_number, ErrorWithPos, create_title_item_function,
147          current_line_positition_for_filter, read_session_example;
148    groupnumber := 0;
149    level_scope := 0;
150    autodoc_read_line := false;
151    context_stack := [ ];
152    chapter_info := [ ];
153    line_number := 0;
154
155    ReadLineWithLineCount := function( stream )
156        line_number := line_number + 1;
157        return ReadLine( stream );
158    end;
159    Normalized_ReadLine := function( stream )
160        local string;
161        string := ReadLineWithLineCount( stream );
162        if string = fail then
163            return fail;
164        fi;
165        NormalizeWhitespace( string );
166        return string;
167    end;
168    ErrorWithPos := function(arg)
169        local list;
170        list := Concatenation(arg, [ ",\n", "at ", filename, ":", line_number]);
171        CallFuncList(Error, list);
172    end;
173    new_man_item := function( )
174        local man_item;
175        if IsBound( current_item ) and IsTreeForDocumentationNodeForManItemRep( current_item ) then
176            return current_item;
177        fi;
178
179        # implicitly end any subsection
180        if IsBound( chapter_info[ 3 ] ) then
181            Unbind( chapter_info[ 3 ] );
182            current_item := SectionInTree( tree, chapter_info[ 1 ], chapter_info[ 2 ] );
183        fi;
184
185        if IsBound( current_item ) then
186            Add( context_stack, current_item );
187        fi;
188
189        man_item := DocumentationManItem( tree );
190        if IsBound( scope_group ) then
191            SetGroupName( man_item, scope_group );
192        fi;
193        man_item!.chapter_info := ShallowCopy( chapter_info );
194        man_item!.tester_names := fail;
195        return man_item;
196    end;
197    add_man_item := function( )
198        local man_item;
199        man_item := current_item;
200        if context_stack <> [ ] then
201            current_item := Remove( context_stack );
202        else
203            Unbind( current_item );
204        fi;
205        if IsBound( man_item!.chapter_info ) then
206            SetChapterInfo( man_item, man_item!.chapter_info );
207        fi;
208        if Length( ChapterInfo( man_item ) ) <> 2 then
209            ErrorWithPos( "declarations must be documented within a section" );
210        fi;
211        Add( tree, man_item );
212    end;
213    Reset := function( )
214        chapter_info := [ ];
215        context_stack := [ ];
216        Unbind( current_item );
217        plain_text_mode := false;
218    end;
219    Scan_for_Declaration_part := function()
220        local declare_position, current_type, filter_string, has_filters,
221              position_parenthesis, nr_of_attr_loops, i;
222
223        ## fail is bigger than every integer
224        declare_position := Minimum( [ PositionSublist( current_line, "Declare" ), PositionSublist( current_line, "KeyDependentOperation" ) ] );
225        if declare_position <> fail then
226            current_item := new_man_item();
227            current_line := current_line{[ declare_position .. Length( current_line ) ]};
228            position_parenthesis := PositionSublist( current_line, "(" );
229            if position_parenthesis = fail then
230                ErrorWithPos( "Something went wrong" );
231            fi;
232            current_type := current_line{ [ 1 .. position_parenthesis - 1 ] };
233            has_filters := AutoDoc_Type_Of_Item( current_item, current_type, default_chapter_data );
234            if has_filters = fail then
235                ErrorWithPos( "Unrecognized scan type" );
236                return false;
237            fi;
238            current_line := current_line{ [ position_parenthesis + 1 .. Length( current_line ) ] };
239            ## Now the funny part begins:
240            ## try fetching the name:
241            ## Assuming the name is in the same line as its
242            while PositionSublist( current_line, "," ) = fail and PositionSublist( current_line, ");" ) = fail do
243                current_line := Normalized_ReadLine( filestream );
244            od;
245            current_line := StripBeginEnd( current_line, " " );
246            current_item!.name := current_line{ [ 1 .. Minimum( [ PositionSublist( current_line, "," ), PositionSublist( current_line, ");" ) ] ) - 1 ] };
247            current_item!.name := StripBeginEnd( ReplacedString( current_item!.name, "\"", "" ), " " );
248
249            # Deal with DeclareCategoryCollections: this has some special
250            # rules on how the name of a new category is derived from the
251            # string given to it. Since the code for that is not available in
252            # a separate GAP function, we have to replicate this logic here.
253            # To understand what's going on, please refer to the
254            # DeclareCategoryCollections documentation and implementation.
255            if IsBound(current_item!.coll_suffix) then
256                if EndsWith(current_item!.name, "Collection") then
257                    current_item!.name :=
258                    current_item!.name{[1..Length(current_item!.name)-6]};
259                fi;
260                if EndsWith(current_item!.name, "Coll") then
261                    current_item!.coll_suffix := "Coll";
262                else
263                    current_item!.coll_suffix := "Collection";
264                fi;
265                current_item!.name := Concatenation(current_item!.name,
266                                                    current_item!.coll_suffix);
267            fi;
268
269            current_line := current_line{ [ Minimum( [ PositionSublist( current_line, "," ), PositionSublist( current_line, ");" ) ] ) + 1 .. Length( current_line ) ] };
270            filter_string := "for ";
271            ## FIXME: The next two if's can be merged at some point
272            if IsInt( has_filters ) then
273                for i in [ 1 .. has_filters ] do
274                    ## We now search for the filters. A filter is either followed by a ',', if there is more than one,
275                    ## or by ');' if it is the only or last one. So we search for the next delimiter.
276                    while PositionSublist( current_line, "," ) = fail and PositionSublist( current_line, ");" ) = fail do
277                        Append( filter_string, StripBeginEnd( current_line, " " ) );
278                        current_line := ReadLineWithLineCount( filestream );
279                        NormalizeWhitespace( current_line );
280                    od;
281                    current_line_positition_for_filter := Minimum( [ PositionSublist( current_line, "," ), PositionSublist( current_line, ");" ) ] ) - 1;
282                    Append( filter_string, StripBeginEnd( current_line{ [ 1 .. current_line_positition_for_filter ] }, " " ) );
283                    current_line := current_line{[ current_line_positition_for_filter + 1 .. Length( current_line ) ]};
284                    if current_line[ 1 ] = ',' then
285                        current_line := current_line{[ 2 .. Length( current_line ) ]};
286                    elif current_line[ 1 ] = ')' then
287                        current_line := current_line{[ 3 .. Length( current_line ) ]};
288                    fi;
289                    ## FIXME: Refactor this whole if IsInt( has_filters ) case!
290                    if has_filters - i > 0 then
291                        Append( filter_string, ", " );
292                    fi;
293                od;
294            elif has_filters = "List" then
295                while AUTODOC_PositionElementIfNotAfter( current_line, '[', '\\' ) = fail do
296                    current_line := ReadLineWithLineCount( filestream );
297                    NormalizeWhitespace( current_line );
298                od;
299                current_line := current_line{ [ AUTODOC_PositionElementIfNotAfter( current_line, '[', '\\' ) + 1 .. Length( current_line ) ] };
300                while AUTODOC_PositionElementIfNotAfter( current_line, ']', '\\' ) = fail do
301                    Append( filter_string, StripBeginEnd( current_line, " " ) );
302                    current_line := ReadLineWithLineCount( filestream );
303                    NormalizeWhitespace( current_line );
304                od;
305                Append( filter_string, StripBeginEnd( current_line{[ 1 .. AUTODOC_PositionElementIfNotAfter( current_line, ']', '\\' ) - 1 ]}, " " ) );
306            else
307                filter_string := false;
308            fi;
309            if IsString( filter_string ) then
310                filter_string := ReplacedString( filter_string, "\"", "" );
311            fi;
312            if filter_string <> false then
313                if current_item!.tester_names = fail and StripBeginEnd( filter_string, " " ) <> "for" then
314                    current_item!.tester_names := filter_string;
315                fi;
316                if StripBeginEnd( filter_string, " " ) = "for" then
317                    has_filters := "empty_argument_list";
318                fi;
319                ##Adjust arguments
320                if not IsBound( current_item!.arguments ) then
321                    if IsInt( has_filters ) then
322                        if has_filters = 1 then
323                            current_item!.arguments := "arg";
324                        else
325                            current_item!.arguments := JoinStringsWithSeparator( List( [ 1 .. has_filters ], i -> Concatenation( "arg", String( i ) ) ), "," );
326                        fi;
327                    elif has_filters = "List" then
328                        current_item!.arguments := List( [ 1 .. Length( SplitString( filter_string, "," ) ) ], i -> Concatenation( "arg", String( i ) ) );
329                        if Length( current_item!.arguments ) = 1 then
330                            current_item!.arguments := "arg";
331                        else
332                            current_item!.arguments := JoinStringsWithSeparator( current_item!.arguments, "," );
333                        fi;
334                    elif has_filters = "empty_argument_list" then
335                        current_item!.arguments := "";
336                    fi;
337                fi;
338            fi;
339            add_man_item();
340            return true;
341        fi;
342        declare_position := Minimum( [ PositionSublist( current_line, "InstallMethod" ), PositionSublist( current_line, "InstallOtherMethod" ) ] );
343                            ## Fail is larger than every integer.
344        if declare_position <> fail then
345            current_item := new_man_item();
346            current_item!.item_type := "Oper";
347            ##Find name
348            position_parenthesis := PositionSublist( current_line, "(" );
349            current_line := current_line{ [ position_parenthesis + 1 .. Length( current_line ) ] };
350            ## find next colon
351            current_item!.name := "";
352            while PositionSublist( current_line, "," ) = fail do
353                Append( current_item!.name, current_line );
354                current_line := Normalized_ReadLine( filestream );
355            od;
356            position_parenthesis := PositionSublist( current_line, "," );
357            Append( current_item!.name, current_line{[ 1 .. position_parenthesis - 1 ]} );
358            NormalizeWhitespace( current_item!.name );
359            current_item!.name := StripBeginEnd( current_item!.name, " " );
360            while AUTODOC_PositionElementIfNotAfter( current_line, '[', '\\' ) = fail do
361                current_line := Normalized_ReadLine( filestream );
362            od;
363            position_parenthesis := AUTODOC_PositionElementIfNotAfter( current_line, '[', '\\' );
364            current_line := current_line{[ position_parenthesis + 1 .. Length( current_line ) ]};
365            filter_string := "for ";
366            while PositionSublist( current_line, "]" ) = fail do
367                Append( filter_string, current_line );
368            od;
369            position_parenthesis := AUTODOC_PositionElementIfNotAfter( current_line, ']', '\\' );
370            Append( filter_string, current_line{[ 1 .. position_parenthesis - 1 ]} );
371            current_line := current_line{[ position_parenthesis + 1 .. Length( current_line )]};
372            NormalizeWhitespace( filter_string );
373            if IsString( filter_string ) then
374                filter_string := ReplacedString( filter_string, "\"", "" );
375            fi;
376            if current_item!.tester_names = fail then
377                current_item!.tester_names := filter_string;
378            fi;
379            ##Maybe find some argument names
380            if not IsBound( current_item!.arguments ) then
381                while PositionSublist( current_line, "function(" ) = fail and PositionSublist( current_line, ");" ) = fail do
382                    current_line := Normalized_ReadLine( filestream );
383                od;
384                position_parenthesis := PositionSublist( current_line, "function(" );
385                if position_parenthesis <> fail then
386                    current_line := current_line{[ position_parenthesis + 9 .. Length( current_line ) ]};
387                    filter_string := "";
388                    while PositionSublist( current_line, ")" ) = fail do;
389                        current_line := StripBeginEnd( current_line, " " );
390                        Append( filter_string, current_line );
391                        current_line := Normalized_ReadLine( current_line );
392                    od;
393                    position_parenthesis := PositionSublist( current_line, ")" );
394                    Append( filter_string, current_line{[ 1 .. position_parenthesis - 1 ]} );
395                    NormalizeWhitespace( filter_string );
396                    filter_string := StripBeginEnd( filter_string, " " );
397                    current_item!.arguments := filter_string;
398                fi;
399            fi;
400            if not IsBound( current_item!.arguments ) then
401                current_item!.arguments := Length( SplitString( current_item!.tester_names, "," ) );
402                current_item!.arguments := JoinStringsWithSeparator( List( [ 1 .. current_item!.arguments ], i -> Concatenation( "arg", String( i ) ) ), "," );
403            fi;
404            add_man_item();
405            return true;
406        fi;
407        return false;
408    end;
409    read_code := function( )
410        local code, temp_curr_line, comment_pos, before_comment;
411        code := [ "<Listing Type=\"Code\"><![CDATA[\n" ];
412        while true do
413            temp_curr_line := ReadLineWithLineCount( filestream );
414            if temp_curr_line[ Length( temp_curr_line )] = '\n' then
415                temp_curr_line := temp_curr_line{[ 1 .. Length( temp_curr_line ) - 1 ]};
416            fi;
417            if plain_text_mode = false then
418                comment_pos := PositionSublist( temp_curr_line, "#!" );
419                if comment_pos <> fail then
420                    before_comment := NormalizedWhitespace( temp_curr_line{ [ 1 .. comment_pos - 1 ] } );
421                    if before_comment = "" then
422                        temp_curr_line := temp_curr_line{[ comment_pos + 2 .. Length( temp_curr_line ) ]};
423                    fi;
424                fi;
425            fi;
426            if filestream = fail or PositionSublist( temp_curr_line, "@EndCode" ) <> fail then
427                break;
428            fi;
429            Add( code, temp_curr_line );
430        od;
431        Add( code, "]]></Listing>\n" );
432        return code;
433    end;
434    read_example := function( is_tested_example )
435        local temp_string_list, temp_curr_line, temp_pos_comment, is_following_line, item_temp, example_node;
436        example_node := DocumentationExample( tree );
437        example_node!.is_tested_example := is_tested_example;
438        temp_string_list := example_node!.content;
439        is_following_line := false;
440        while true do
441            temp_curr_line := ReadLineWithLineCount( filestream );
442            if temp_curr_line[ Length( temp_curr_line )] = '\n' then
443                temp_curr_line := temp_curr_line{[ 1 .. Length( temp_curr_line ) - 1 ]};
444            fi;
445            if filestream = fail or PositionSublist( temp_curr_line, "@EndExample" ) <> fail
446                                 or PositionSublist( temp_curr_line, "@EndLog" ) <> fail then
447                break;
448            fi;
449            ##if is comment, simply remove comments.
450            #! @DONT_SCAN_NEXT_LINE
451            temp_pos_comment := PositionSublist( temp_curr_line, "#!" );
452            if temp_pos_comment <> fail then
453                temp_curr_line := temp_curr_line{[ temp_pos_comment + 3 .. Length( temp_curr_line ) ]};
454                Add( temp_string_list, temp_curr_line );
455                is_following_line := false;
456                continue;
457            else
458                if is_following_line then
459                    temp_curr_line := Concatenation( "> ", temp_curr_line );
460                    if PositionSublist( temp_curr_line, ";" ) <> fail then
461                        is_following_line := false;
462                    fi;
463                else
464                    if temp_curr_line = "" then
465                        continue;
466                    fi;
467                    temp_curr_line := Concatenation( "gap> ", temp_curr_line );
468                    is_following_line := PositionSublist( temp_curr_line, ";" ) = fail;
469                fi;
470                Add( temp_string_list, temp_curr_line );
471                continue;
472            fi;
473        od;
474        return example_node;
475    end;
476    read_session_example := function( is_tested_example, plain_text_mode )
477        local temp_string_list, temp_curr_line, temp_pos_comment,
478              is_following_line, item_temp, example_node,
479              incorporate_this_line;
480        example_node := DocumentationExample( tree );
481        example_node!.is_tested_example := is_tested_example;
482        temp_string_list := example_node!.content;
483        while true do
484            temp_curr_line := ReadLineWithLineCount( filestream );
485            if temp_curr_line[ Length( temp_curr_line )] = '\n' then
486                temp_curr_line := temp_curr_line{[ 1 .. Length( temp_curr_line ) - 1 ]};
487            fi;
488            if filestream = fail or PositionSublist( temp_curr_line, "@EndExampleSession" ) <> fail
489                                 or PositionSublist( temp_curr_line, "@EndLogSession" ) <> fail then
490                break;
491            fi;
492            incorporate_this_line := plain_text_mode;
493            if not plain_text_mode then
494                #! @DONT_SCAN_NEXT_LINE
495                temp_pos_comment := PositionSublist( temp_curr_line, "#!" );
496                if temp_pos_comment <> fail then
497                    incorporate_this_line := true;
498                    temp_curr_line := temp_curr_line{[ temp_pos_comment + 2 .. Length( temp_curr_line ) ]};
499                    if Length( temp_curr_line ) >= 1 and temp_curr_line[ 1 ] = ' ' then
500                        Remove( temp_curr_line, 1 );
501                    fi;
502                fi;
503            fi;
504            if incorporate_this_line then
505                Add( temp_string_list, temp_curr_line );
506            fi;
507        od;
508        return example_node;
509    end;
510    deprecated := function(name, f)
511        return function(args...)
512            Info(InfoWarning, 1, TextAttr.1, "WARNING: ----------------------------------------------------------------------------", TextAttr.reset);
513            Info(InfoWarning, 1, TextAttr.1, "WARNING: ", name, " is deprecated; please refer to the AutoDoc manual for details", TextAttr.reset);
514            Info(InfoWarning, 1, TextAttr.1, "WARNING: ----------------------------------------------------------------------------", TextAttr.reset);
515            f();
516        end;
517    end;
518    command_function_record := rec(
519        ## HACK: Needed for AutoDoc parser to be scanned savely.
520        ##       The lines where the AutoDoc comments are
521        ##       searched cause problems otherwise.
522        @DONT_SCAN_NEXT_LINE := function()
523            ReadLineWithLineCount( filestream );
524        end,
525        @DoNotReadRestOfFile := function()
526            Reset();
527            rest_of_file_skipped := true;
528        end,
529        @BeginAutoDoc := deprecated("@BeginAutoDoc", function()
530            autodoc_read_line := fail;
531        end),
532        @AutoDoc := ~.@BeginAutoDoc,
533        @EndAutoDoc := deprecated("@EndAutoDoc", function()
534            autodoc_read_line := false;
535        end),
536
537        @Chapter := function()
538            local scope_chapter;
539            scope_chapter := ReplacedString( current_command[ 2 ], " ", "_" );
540            current_item := ChapterInTree( tree, scope_chapter );
541            chapter_info[ 1 ] := scope_chapter;
542        end,
543        @ChapterLabel := function()
544            local scope_chapter, label_name;
545            if not IsBound( chapter_info[ 1 ] ) then
546                ErrorWithPos( "found @ChapterLabel with no active chapter" );
547            fi;
548            label_name := ReplacedString( current_command[ 2 ], " ", "_" );
549            scope_chapter := ChapterInTree( tree, chapter_info[ 1 ] );
550            scope_chapter!.additional_label := Concatenation( "Chapter_", label_name );
551        end,
552        @ChapterTitle := function()
553            local scope_chapter;
554            if not IsBound( chapter_info[ 1 ] ) then
555                ErrorWithPos( "found @ChapterTitle with no active chapter" );
556            fi;
557            scope_chapter := ChapterInTree( tree, chapter_info[ 1 ] );
558            scope_chapter!.title_string := current_command[ 2 ];
559        end,
560
561        @Section := function()
562            local scope_section;
563            if not IsBound( chapter_info[ 1 ] ) then
564                ErrorWithPos( "found @Section with no active chapter" );
565            fi;
566            scope_section := ReplacedString( current_command[ 2 ], " ", "_" );
567            current_item := SectionInTree( tree, chapter_info[ 1 ], scope_section );
568            Unbind( chapter_info[ 3 ] );
569            chapter_info[ 2 ] := scope_section;
570        end,
571        @SectionLabel := function()
572            local scope_section, label_name;
573            if not IsBound( chapter_info[ 2 ] ) then
574                ErrorWithPos( "found @SectionLabel with no active section" );
575            fi;
576            label_name := ReplacedString( current_command[ 2 ], " ", "_" );
577            scope_section := SectionInTree( tree, chapter_info[ 1 ], chapter_info[ 2 ] );
578            scope_section!.additional_label := Concatenation( "Section_", label_name );
579        end,
580        @SectionTitle := function()
581            local scope_section;
582            if not IsBound( chapter_info[ 2 ] ) then
583                ErrorWithPos( "found @SectionTitle with no active section" );
584            fi;
585            scope_section := SectionInTree( tree, chapter_info[ 1 ], chapter_info[ 2 ] );
586            scope_section!.title_string := current_command[ 2 ];
587        end,
588        @EndSection := deprecated("@EndSection", function()
589            if not IsBound( chapter_info[ 2 ] ) then
590                ErrorWithPos( "found @EndSection with no active section" );
591            fi;
592            Unbind( chapter_info[ 2 ] );
593            Unbind( chapter_info[ 3 ] );
594            current_item := ChapterInTree( tree, chapter_info[ 1 ] );
595        end),
596
597        @Subsection := function()
598            local scope_subsection;
599            if not IsBound( chapter_info[ 1 ] ) or not IsBound( chapter_info[ 2 ] ) then
600                ErrorWithPos( "found @Subsection with no active section" );
601            fi;
602            scope_subsection := ReplacedString( current_command[ 2 ], " ", "_" );
603            current_item := SubsectionInTree( tree, chapter_info[ 1 ], chapter_info[ 2 ], scope_subsection );
604            chapter_info[ 3 ] := scope_subsection;
605        end,
606        @SubsectionLabel := function()
607            local scope_subsection, label_name;
608            if not IsBound( chapter_info[ 3 ] ) then
609                ErrorWithPos( "found @SubsectionLabel with no active subsection" );
610            fi;
611            label_name := ReplacedString( current_command[ 2 ], " ", "_" );
612            scope_subsection := SubsectionInTree( tree, chapter_info[ 1 ], chapter_info[ 2 ], chapter_info[ 3 ] );
613            scope_subsection!.additional_label := Concatenation( "Subsection_", label_name );
614        end,
615        @SubsectionTitle := function()
616            local scope_subsection;
617            if not IsBound( chapter_info[ 3 ] ) then
618                ErrorWithPos( "found @SubsectionTitle with no active subsection" );
619            fi;
620            scope_subsection := SubsectionInTree( tree, chapter_info[ 1 ], chapter_info[ 2 ], chapter_info[ 3 ] );
621            scope_subsection!.title_string := current_command[ 2 ];
622        end,
623        @EndSubsection := deprecated("@EndSubsection", function()
624            if not IsBound( chapter_info[ 3 ] ) then
625                ErrorWithPos( "found @EndSubsection with no active subsection" );
626            fi;
627            Unbind( chapter_info[ 3 ] );
628            current_item := SectionInTree( tree, chapter_info[ 1 ], chapter_info[ 2 ] );
629        end),
630
631        @BeginGroup := function()
632            local grp;
633            if current_command[ 2 ] = "" then
634                groupnumber := groupnumber + 1;
635                current_command[ 2 ] := Concatenation( "AutoDoc_generated_group", String( groupnumber ) );
636            fi;
637            scope_group := ReplacedString( current_command[ 2 ], " ", "_" );
638        end,
639        @EndGroup := function()
640            Unbind( scope_group );
641        end,
642        @Description := function()
643            current_item := new_man_item();
644            SetManItemToDescription( current_item );
645            NormalizeWhitespace( current_command[ 2 ] );
646            if current_command[ 2 ] <> "" then
647                Add( current_item, current_command[ 2 ] );
648            fi;
649        end,
650        @Returns := function()
651            current_item := new_man_item();
652            SetManItemToReturnValue( current_item );
653            if current_command[ 2 ] <> "" then
654                Add( current_item, current_command[ 2 ] );
655            fi;
656        end,
657        @Arguments := function()
658            current_item := new_man_item();
659            current_item!.arguments := current_command[ 2 ];
660        end,
661        @Label := function()
662            current_item := new_man_item();
663            current_item!.tester_names := current_command[ 2 ];
664        end,
665        @Group := function()
666            local group_name;
667            current_item := new_man_item();
668            group_name := ReplacedString( current_command[ 2 ], " ", "_" );
669            SetGroupName( current_item, group_name );
670        end,
671        @GroupTitle := function()
672            local group_name, chap_info, group_obj;
673            current_item := new_man_item();
674            if not HasGroupName( current_item ) then
675                ErrorWithPos( "found @GroupTitle with no Group set" );
676            fi;
677            group_name := GroupName( current_item );
678            chap_info := fail;
679            if HasChapterInfo( current_item ) then
680                chap_info := ChapterInfo( current_item );
681            elif IsBound( current_item!.chapter_info ) then
682                chap_info := current_item!.chapter_info;
683            fi;
684            if chap_info = fail or Length( chap_info ) = 0 then
685                chap_info := chapter_info;
686            fi;
687            if Length( chap_info ) <> 2 then
688                ErrorWithPos( "can only set @GroupTitle within a Chapter and Section.");
689            fi;
690            group_obj := DocumentationGroup( tree, group_name, chap_info );
691            group_obj!.title_string := current_command[ 2 ];
692        end,
693        @ChapterInfo := function()
694            local current_chapter_info;
695            current_item := new_man_item();
696            current_chapter_info := SplitString( current_command[ 2 ], "," );
697            current_chapter_info := List( current_chapter_info, i -> ReplacedString( StripBeginEnd( i, " " ), " ", "_" ) );
698            SetChapterInfo( current_item, current_chapter_info );
699        end,
700        @BREAK := function()
701            ErrorWithPos( current_command[ 2 ] );
702        end,
703        @SetLevel := function()
704            level_scope := Int( current_command[ 2 ] );
705        end,
706        @ResetLevel := function()
707            level_scope := 0;
708        end,
709        @Level := function()
710            current_item!.level := Int( current_command[ 2 ] );
711        end,
712
713        @InsertChunk := function()
714            local label_name;
715            label_name := ReplacedString( current_command[ 2 ], " ", "_" );
716            Add( current_item, DocumentationChunk( tree, label_name ) );
717        end,
718        @BeginChunk := function()
719            local label_name;
720            if IsBound( current_item ) then
721                Add( context_stack, current_item );
722            fi;
723            label_name := ReplacedString( current_command[ 2 ], " ", "_" );
724            current_item := DocumentationChunk( tree, label_name );
725        end,
726        @Chunk := ~.@BeginChunk,
727        @EndChunk := function()
728            if autodoc_read_line = true then
729                autodoc_read_line := false;
730            fi;
731            if context_stack <> [ ] then
732                current_item := Remove( context_stack );
733            else
734                Unbind( current_item );
735            fi;
736        end,
737
738        @InsertSystem := deprecated("@InsertSystem", ~.@InsertChunk),
739        @System := deprecated("@System", ~.@BeginChunk),
740        @BeginSystem := ~.@System,
741        @EndSystem := deprecated("@EndSystem", ~.@EndChunk),
742
743        @BeginCode := function()
744            local label_name, tmp_system;
745            label_name := ReplacedString( current_command[ 2 ], " ", "_" );
746            tmp_system := DocumentationChunk( tree, label_name );
747            Append( tmp_system!.content, read_code() );
748        end,
749        @Code := ~.@BeginCode,
750        @InsertCode := ~.@InsertChunk,
751
752        @BeginExample := function()
753            local example_node;
754            example_node := read_example( true );
755            Add( current_item, example_node );
756        end,
757
758        @Example := ~.@BeginExample,
759        @BeginLog := function()
760            local example_node;
761            example_node := read_example( false );
762            Add( current_item, example_node );
763        end,
764        @Log := ~.@BeginLog,
765
766        STRING := function()
767            local comment_pos;
768            if not IsBound( current_item ) then
769                return;
770            fi;
771            comment_pos := PositionSublist( current_line_unedited, "#!" );
772            if comment_pos <> fail then
773                current_line_unedited := current_line_unedited{[ comment_pos + 2 .. Length( current_line_unedited ) ]};
774            fi;
775            Add( current_item, current_line_unedited );
776        end,
777        @BeginLatexOnly := function()
778            Add( current_item, "<Alt Only=\"LaTeX\"><![CDATA[" );
779            if current_command[ 2 ] <> "" then
780                Add( current_item, current_command[ 2 ] );
781            fi;
782        end,
783        @EndLatexOnly := function()
784            if autodoc_read_line = true then
785                autodoc_read_line := false;
786            fi;
787            Add( current_item, "]]></Alt>" );
788        end,
789        @LatexOnly := function()
790            Add( current_item, "<Alt Only=\"LaTeX\"><![CDATA[" );
791            Add( current_item, current_command[ 2 ] );
792            Add( current_item, "]]></Alt>" );
793        end,
794        @BeginNotLatex := function()
795            Add( current_item, "<Alt Not=\"LaTeX\"><![CDATA[" );
796            if current_command[ 2 ] <> "" then
797                Add( current_item, current_command[ 2 ] );
798            fi;
799        end,
800        @EndNotLatex := function()
801            if autodoc_read_line = true then
802                autodoc_read_line := false;
803            fi;
804            Add( current_item, "]]></Alt>" );
805        end,
806        @NotLatex := function()
807            Add( current_item, "<Alt Not=\"LaTeX\"><![CDATA[" );
808            Add( current_item, current_command[ 2 ] );
809            Add( current_item, "]]></Alt>" );
810        end,
811        @Dependency := function()
812            if not IsBound( tree!.worksheet_dependencies ) then
813                tree!.worksheet_dependencies := [ ];
814            fi;
815            NormalizeWhitespace( current_command[ 2 ] );
816            Add( tree!.worksheet_dependencies, SplitString( current_command[ 2 ], " " ) );
817        end,
818        @BeginAutoDocPlainText := deprecated("@BeginAutoDocPlainText", function()
819            plain_text_mode := true;
820        end),
821        @AutoDocPlainText := ~.@BeginAutoDocPlainText,
822        @EndAutoDocPlainText := deprecated("@EndAutoDocPlainText", function()
823            plain_text_mode := false;
824        end),
825        @ExampleSession := function()
826            local example_node;
827            example_node := read_session_example( true, plain_text_mode );
828            Add( current_item, example_node );
829        end,
830        @BeginExampleSession := ~.@ExampleSession,
831        @LogSession := function()
832            local example_node;
833            example_node := read_session_example( false, plain_text_mode );
834            Add( current_item, example_node );
835        end,
836        @BeginLogSession := ~.@LogSession
837    );
838
839    ## The following commands are specific for worksheets. They do not have a packageinfo,
840    ## and no place to extract those infos. So these commands are needed to make insert the
841    ## information directly into the document.
842    title_item_list := [ "Title", "Subtitle", "Version", "TitleComment", "Author",
843                         "Date", "Address", "Abstract", "Copyright", "Acknowledgements", "Colophon" ];
844
845    create_title_item_function := function( name )
846        return function()
847            if not IsBound( tree!.TitlePage.( name ) ) then
848                tree!.TitlePage.( name ) := [ ];
849            fi;
850            current_item := tree!.TitlePage.( name );
851            Add( current_item, current_command[ 2 ] );
852        end;
853    end;
854
855    ## Note that we need to create these functions in the helper function
856    ## create_title_item_function to ensure that the <name> variable is bound properly.
857    ## Without this intermediate helper, the wrong closure is taken,
858    ## and later, when the function is executed, the value for <name> will be the last
859    ## value <title_item> had, i.e., the last entry of <title_item_list>.
860    for title_item in title_item_list do
861        command_function_record.( Concatenation( "@", title_item ) ) := create_title_item_function( title_item );
862    od;
863
864    rest_of_file_skipped := false;
865    ##Now read the files.
866    for filename in filename_list do
867        Reset();
868        ## FIXME: Is this dangerous?
869        if PositionSublist( filename, ".autodoc" ) <> fail then
870            plain_text_mode := true;
871        fi;
872        filestream := InputTextFile( filename );
873        if filestream = fail then
874            Error( "could not open ", filename );
875        fi;
876        line_number := 0;
877        while true do
878            if rest_of_file_skipped = true then
879                rest_of_file_skipped := false;
880                break;
881            fi;
882            current_line := ReadLineWithLineCount( filestream );
883            if current_line = fail then
884                break;
885            fi;
886            current_line_unedited := ShallowCopy( current_line );
887            NormalizeWhitespace( current_line );
888            current_command := Scan_for_AutoDoc_Part( current_line, plain_text_mode );
889            if current_command[ 1 ] <> false then
890                if autodoc_read_line <> fail then
891                    autodoc_read_line := true;
892                fi;
893                if not IsBound( command_function_record.(current_command[ 1 ]) ) then
894                    ErrorWithPos("unknown AutoDoc command ", current_command[ 1 ]);
895                fi;
896                command_function_record.(current_command[ 1 ])();
897                continue;
898            fi;
899            current_line := current_command[ 2 ];
900            if autodoc_read_line = true or autodoc_read_line = fail then
901                was_declaration := Scan_for_Declaration_part( );
902                if not was_declaration and autodoc_read_line <> fail then
903                    autodoc_read_line := false;
904                fi;
905            fi;
906        od;
907    od;
908end );
909