diff --git a/CHANGES.md b/CHANGES.md index b880b99f..3c12285f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 `

` diff --git a/doc/Comments.xml b/doc/Comments.xml index de08fec1..9cf58bfd 100644 --- a/doc/Comments.xml +++ b/doc/Comments.xml @@ -761,6 +761,47 @@ can be important. This produces the following output:
Call function foobar() at the start. + + + + Fenced code blocks + + 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 + @listing, @example, and @log select the corresponding + GAPDoc element; any other value is currently ignored. If nothing is specified + the default is to generate <Listing>. Example: + +

[ 1 .. 3 ] ^ 2; +#! [ 1, 4, 9 ] +#! ~~~ +#! ```@log +#! #I some log message +#! ``` +]]> + + This produces the following output:
+ + [ 1 .. 3 ] ^ 2; +[ 1, 4, 9 ] +]]> + + diff --git a/gap/Markdown.gi b/gap/Markdown.gi index c274eec5..4fc57fa5 100644 --- a/gap/Markdown.gi +++ b/gap/Markdown.gi @@ -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, ">= 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 ], "]]>", "]]]]>" ) ); + i := i + 1; + od; + if code_block = true then + Add( converted_string_list, + Concatenation( "]]>" ) ); + i := i + 1; + continue; + fi; + Add( converted_string_list, + Concatenation( "]]>" ) ); + 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

diff --git a/gap/Parser.gi b/gap/Parser.gi index f22f27ec..641f3126 100644 --- a/gap/Parser.gi +++ b/gap/Parser.gi @@ -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; @@ -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; @@ -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, @@ -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 ); @@ -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 ]; diff --git a/tst/misc.tst b/tst/misc.tst index bfd3d71e..92eb0d47 100644 --- a/tst/misc.tst +++ b/tst/misc.tst @@ -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", +> "

"if x = 2 then", +> " Print(\"ok\\n\");", +> "fi;", +> "]]>", +> "After" +> ]; +true +gap> CONVERT_LIST_OF_STRINGS_IN_MARKDOWN_TO_GAPDOC_XML([ +> "~~~", +> "gap> [[2]]>[[1]];", +> "~~~" +> ]) = [ +> " "gap> [[2]]]]>[[1]];", +> "]]>" +> ]; +true +gap> CONVERT_LIST_OF_STRINGS_IN_MARKDOWN_TO_GAPDOC_XML([ +> "```@example", +> "gap> 1 + 1;", +> "2", +> "```" +> ]) = [ +> " "gap> 1 + 1;", +> "2", +> "]]>" +> ]; +true +gap> CONVERT_LIST_OF_STRINGS_IN_MARKDOWN_TO_GAPDOC_XML([ +> "```@log", +> "#I some log message", +> "```" +> ]) = [ +> " "#I some log message", +> "]]>" +> ]; +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, "") <> fail; +true +gap> PositionSublist(xml, "Section_ThisMustStayLiteral") = fail; +true +gap> RemoveDirectoryRecursively(tmpdir); +true + # # warn about defined-but-never-inserted chunks #