Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ This file describes changes in the AutoDoc package.
defined (via `@BeginChunk`/`@BeginCode`)
- Allow using Markdown-style headings `#`/`##`/`###` as aliases for
`@Chapter`/`@Section`/`@Subsection` in `.autodoc` files and doc comments
- Support fenced code blocks using triple backticks or tildes in
Markdown-like text; `@listing`, `@example`, and `@log` info strings
select the corresponding GAPDoc element

2025.12.19
- Don't replace empty lines in `@BeginCode` blocks by `<P/>`
Expand Down
41 changes: 41 additions & 0 deletions doc/Comments.xml
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,47 @@ can be important.</E>
This produces the following output:<Br/>
Call function <C>foobar()</C> at the start.

</Subsection>

<Subsection Label="MarkdownExtensionFencedCode">
<Heading>Fenced code blocks</Heading>

One can insert verbatim code blocks by placing the code between lines
containing at least three backticks or at least three tildes. The opening
fence may optionally be followed by an info string. The values
<C>@listing</C>, <C>@example</C>, and <C>@log</C> select the corresponding
GAPDoc element; any other value is currently ignored. If nothing is specified
the default is to generate <C>&lt;Listing&gt;</C>. Example:

<Listing><![CDATA[
#! ```@listing
#! if x = 2 then
#! Print("1 + 1 = 2 holds, all is good\n");
#! fi;
#! ```
#! ~~~@example
#! gap> [ 1 .. 3 ] ^ 2;
#! [ 1, 4, 9 ]
#! ~~~
#! ```@log
#! #I some log message
#! ```
]]></Listing>

This produces the following output:<Br/>
<Listing><![CDATA[
if x = 2 then
Print("1 + 1 = 2 holds, all is good\n");
fi;
]]></Listing>
<Example><![CDATA[
gap> [ 1 .. 3 ] ^ 2;
[ 1, 4, 9 ]
]]></Example>
<Log><![CDATA[
#I some log message
]]></Log>

</Subsection>

</Section>
Expand Down
62 changes: 61 additions & 1 deletion gap/Markdown.gi
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,67 @@ InstallGlobalFunction( CONVERT_LIST_OF_STRINGS_IN_MARKDOWN_TO_GAPDOC_XML,
local i, current_list, current_string, max_line_length,
current_position, already_in_list, command_list_with_translation, beginning,
commands, position_of_command, insert, beginning_whitespaces, temp, string_list_temp, skipped,
already_inserted_paragraph, in_list, in_item;
already_inserted_paragraph, in_list, in_item, converted_string_list,
fence_char, fence_length, trimmed_line, code_block, info_string,
fence_element;

converted_string_list := [ ];
i := 1;
while i <= Length( string_list ) do
trimmed_line := StripBeginEnd( string_list[ i ], " \t\r\n" );
if Length( trimmed_line ) >= 3 and
( ForAll( trimmed_line{ [ 1 .. 3 ] }, c -> c = '`' ) or
ForAll( trimmed_line{ [ 1 .. 3 ] }, c -> c = '~' ) ) then
fence_char := trimmed_line[ 1 ];
fence_length := 1;
while fence_length < Length( trimmed_line ) and
trimmed_line[ fence_length + 1 ] = fence_char do
fence_length := fence_length + 1;
od;
if fence_length >= 3 then
info_string := NormalizedWhitespace(
trimmed_line{ [ fence_length + 1 .. Length( trimmed_line ) ] }
);
fence_element := "Listing";
if info_string = "@example" then
fence_element := "Example";
elif info_string = "@log" then
fence_element := "Log";
elif info_string = "@listing" then
fence_element := "Listing";
fi;
Add( converted_string_list,
Concatenation( "<", fence_element, "><![CDATA[" ) );
i := i + 1;
code_block := false;
while i <= Length( string_list ) do
trimmed_line := StripBeginEnd( string_list[ i ], " \t\r\n" );
if Length( trimmed_line ) >= fence_length and
ForAll( trimmed_line{ [ 1 .. fence_length ] }, c -> c = fence_char ) and
ForAll( trimmed_line{ [ fence_length + 1 .. Length( trimmed_line ) ] },
c -> c in " \t\r\n" ) then
code_block := true;
break;
fi;
Add( converted_string_list,
ReplacedString( string_list[ i ], "]]>", "]]]]><![CDATA[>" ) );
i := i + 1;
od;
if code_block = true then
Add( converted_string_list,
Concatenation( "]]></", fence_element, ">" ) );
i := i + 1;
continue;
fi;
Add( converted_string_list,
Concatenation( "]]></", fence_element, ">" ) );
break;
fi;
fi;
Add( converted_string_list, string_list[ i ] );
i := i + 1;
od;
string_list := converted_string_list;

## Check for paragraphs by turning an empty string into <P/>

Expand Down
72 changes: 70 additions & 2 deletions gap/Parser.gi
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ InstallGlobalFunction( AutoDoc_Parser_ReadFiles,
current_line_unedited,
ReadLineWithLineCount, Normalized_ReadLine, line_number, ErrorWithPos, create_title_item_function,
current_line_positition_for_filter, read_session_example, DeclarationDelimiterPosition,
ReadInstallMethodFilterString, ReadInstallMethodArguments;
ReadInstallMethodFilterString, ReadInstallMethodArguments,
markdown_fence, preserve_shebang_in_fenced_code,
AUTODOC_MarkdownFenceFromLine, AUTODOC_IsMatchingMarkdownFence,
current_line_fence, current_line_is_fence_delimiter;
groupnumber := 0;
level_scope := 0;
autodoc_read_line := false;
Expand All @@ -240,6 +243,44 @@ InstallGlobalFunction( AutoDoc_Parser_ReadFiles,
list := Concatenation(arg, [ ",\n", "at ", filename, ":", line_number]);
CallFuncList(Error, list);
end;
AUTODOC_MarkdownFenceFromLine := function( line )
local comment_pos, trimmed_line, fence_char, fence_length;
if plain_text_mode then
trimmed_line := StripBeginEnd( Chomp( line ), " \t\r\n" );
else
comment_pos := AUTODOC_PositionPrefixShebang( line );
if comment_pos = fail then
return fail;
fi;
trimmed_line := StripBeginEnd(
Chomp( line{ [ comment_pos + 2 .. Length( line ) ] } ),
" \t\r\n"
);
fi;
if Length( trimmed_line ) < 3 or
not ( trimmed_line[ 1 ] in "`~" ) or
not ForAll( trimmed_line{ [ 1 .. 3 ] }, c -> c = trimmed_line[ 1 ] ) then
return fail;
fi;
fence_char := trimmed_line[ 1 ];
fence_length := 1;
while fence_length < Length( trimmed_line ) and
trimmed_line[ fence_length + 1 ] = fence_char do
fence_length := fence_length + 1;
od;
return rec(
char := fence_char,
length := fence_length,
remainder := trimmed_line{ [ fence_length + 1 .. Length( trimmed_line ) ] }
);
end;
AUTODOC_IsMatchingMarkdownFence := function( fence, current_line_fence )
return current_line_fence <> fail and
fence <> fail and
current_line_fence.char = fence.char and
current_line_fence.length >= fence.length and
ForAll( current_line_fence.remainder, c -> c in " \t\r\n" );
end;
DeclarationDelimiterPosition := function( line )
return Minimum( [ PositionSublist( line, "," ), PositionSublist( line, ");" ) ] );
end;
Expand Down Expand Up @@ -339,6 +380,7 @@ InstallGlobalFunction( AutoDoc_Parser_ReadFiles,
context_stack := [ ];
Unbind( current_item );
plain_text_mode := false;
markdown_fence := fail;
end;
Scan_for_Declaration_part := function()
local declare_position, current_type, filter_string, has_filters,
Expand Down Expand Up @@ -831,7 +873,7 @@ InstallGlobalFunction( AutoDoc_Parser_ReadFiles,
return;
fi;
comment_pos := AUTODOC_PositionPrefixShebang( current_line_unedited );
if comment_pos <> fail then
if comment_pos <> fail and not preserve_shebang_in_fenced_code then
current_line_unedited := current_line_unedited{[ comment_pos + 2 .. Length( current_line_unedited ) ]};
fi;
Add( current_item, current_line_unedited );
Expand Down Expand Up @@ -938,13 +980,39 @@ InstallGlobalFunction( AutoDoc_Parser_ReadFiles,
fi;
current_line_unedited := ShallowCopy( current_line );
NormalizeWhitespace( current_line );
current_line_fence := AUTODOC_MarkdownFenceFromLine( current_line_unedited );
current_line_is_fence_delimiter := false;
if current_line_fence <> fail then
if markdown_fence = fail then
current_line_is_fence_delimiter := true;
else
current_line_is_fence_delimiter :=
AUTODOC_IsMatchingMarkdownFence( markdown_fence, current_line_fence );
fi;
fi;
preserve_shebang_in_fenced_code :=
markdown_fence <> fail and
AUTODOC_PositionPrefixShebang( current_line_unedited ) <> fail and
not current_line_is_fence_delimiter;
current_command := Scan_for_AutoDoc_Part( current_line, plain_text_mode );
if current_line_is_fence_delimiter then
current_command[ 1 ] := "STRING";
elif markdown_fence <> fail and current_command[ 1 ] <> false then
current_command[ 1 ] := "STRING";
fi;
if current_command[ 1 ] <> false then
autodoc_read_line := true;
if not IsBound( command_function_record.(current_command[ 1 ]) ) then
ErrorWithPos("unknown AutoDoc command ", current_command[ 1 ]);
fi;
command_function_record.(current_command[ 1 ])();
if current_line_is_fence_delimiter then
if markdown_fence = fail then
markdown_fence := current_line_fence;
else
markdown_fence := fail;
fi;
fi;
continue;
fi;
current_line := current_command[ 2 ];
Expand Down
82 changes: 82 additions & 0 deletions tst/misc.tst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,88 @@ gap> item!.tester_names;
gap> item!.arguments;
"x,y"

#
# fenced code blocks in Markdown-like text
#
gap> CONVERT_LIST_OF_STRINGS_IN_MARKDOWN_TO_GAPDOC_XML([
> "Before",
> "```gap",
> "if x = 2 then",
> " Print(\"ok\\n\");",
> "fi;",
> "```",
> "After"
> ]) = [
> "Before",
> "<Listing><![CDATA[",
> "if x = 2 then",
> " Print(\"ok\\n\");",
> "fi;",
> "]]></Listing>",
> "After"
> ];
true
gap> CONVERT_LIST_OF_STRINGS_IN_MARKDOWN_TO_GAPDOC_XML([
> "~~~",
> "gap> [[2]]>[[1]];",
> "~~~"
> ]) = [
> "<Listing><![CDATA[",
> "gap> [[2]]]]><![CDATA[>[[1]];",
> "]]></Listing>"
> ];
true
gap> CONVERT_LIST_OF_STRINGS_IN_MARKDOWN_TO_GAPDOC_XML([
> "```@example",
> "gap> 1 + 1;",
> "2",
> "```"
> ]) = [
> "<Example><![CDATA[",
> "gap> 1 + 1;",
> "2",
> "]]></Example>"
> ];
true
gap> CONVERT_LIST_OF_STRINGS_IN_MARKDOWN_TO_GAPDOC_XML([
> "```@log",
> "#I some log message",
> "```"
> ]) = [
> "<Log><![CDATA[",
> "#I some log message",
> "]]></Log>"
> ];
true

#
# fenced code blocks preserve literal #! lines
#
gap> tmpdir := Filename(DirectoryTemporary(), "autodoc-fenced-comments-test");;
gap> if IsDirectoryPath(tmpdir) then RemoveDirectoryRecursively(tmpdir); fi;
gap> AUTODOC_CreateDirIfMissing(tmpdir);
true
gap> tmpdir_obj := Directory(tmpdir);;
gap> file1 := Filename(tmpdir_obj, "fenced-comments.gd");;
gap> stream := OutputTextFile(file1, false);;
gap> AppendTo(stream, "#! @Chapter FenceChapter\n");;
gap> AppendTo(stream, "#! @Section FenceSection\n");;
gap> AppendTo(stream, "#! ```@listing\n");;
gap> AppendTo(stream, "#! @Section ThisMustStayLiteral\n");;
gap> AppendTo(stream, "#! #! AlsoLiteral\n");;
gap> AppendTo(stream, "#! ```\n");;
gap> CloseStream(stream);
gap> tree5 := DocumentationTree();;
gap> AutoDoc_Parser_ReadFiles([file1], tree5, rec());
gap> WriteDocumentation(tree5, tmpdir_obj, 0);
gap> xml := StringFile(Filename(tmpdir_obj, "_Chapter_FenceChapter.xml"));;
gap> PositionSublist(xml, "<Listing><![CDATA[\n#! @Section ThisMustStayLiteral\n#! #! AlsoLiteral\n]]></Listing>") <> fail;
true
gap> PositionSublist(xml, "Section_ThisMustStayLiteral") = fail;
true
gap> RemoveDirectoryRecursively(tmpdir);
true

#
# warn about defined-but-never-inserted chunks
#
Expand Down