SDL_ttf: wikiheaders: Updated script from latest in SDL3.

From 5af466a319fec351004279fe64a77bc8fe51b3f5 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Tue, 14 May 2024 11:45:39 -0400
Subject: [PATCH] wikiheaders: Updated script from latest in SDL3.

---
 build-scripts/wikiheaders.pl | 1344 ++++++++++++++++++++++++++++------
 1 file changed, 1128 insertions(+), 216 deletions(-)

diff --git a/build-scripts/wikiheaders.pl b/build-scripts/wikiheaders.pl
index c2bb8edb..185701b4 100755
--- a/build-scripts/wikiheaders.pl
+++ b/build-scripts/wikiheaders.pl
@@ -29,6 +29,9 @@
 my $copy_direction = 0;
 my $optionsfname = undef;
 my $wikipreamble = undef;
+my $wikiheaderfiletext = 'Defined in %fname%';
+my $manpageheaderfiletext = 'Defined in %fname%';
+my $headercategoryeval = undef;
 my $changeformat = undef;
 my $manpath = undef;
 my $gitrev = undef;
@@ -39,6 +42,8 @@
     $copy_direction = 1, next if $_ eq '--copy-to-header';
     $copy_direction = -1, next if $_ eq '--copy-to-wiki';
     $copy_direction = -2, next if $_ eq '--copy-to-manpages';
+    $copy_direction = -3, next if $_ eq '--report-coverage-gaps';
+    $copy_direction = -4, next if $_ eq '--copy-to-latex';
     if (/\A--options=(.*)\Z/) {
         $optionsfname = $1;
         next;
@@ -66,6 +71,8 @@
 if (defined $optionsfname) {
     open OPTIONS, '<', $optionsfname or die("Failed to open options file '$optionsfname': $!\n");
     while (<OPTIONS>) {
+        next if /\A\s*\#/;  # Skip lines that start with (optional whitespace, then) '#' as comments.
+
         chomp;
         if (/\A(.*?)\=(.*)\Z/) {
             my $key = $1;
@@ -93,11 +100,20 @@
             $wikiurl = $val, next if $key eq 'wikiurl';
             $bugreporturl = $val, next if $key eq 'bugreporturl';
             $wikipreamble = $val, next if $key eq 'wikipreamble';
+            $wikiheaderfiletext = $val, next if $key eq 'wikiheaderfiletext';
+            $manpageheaderfiletext = $val, next if $key eq 'manpageheaderfiletext';
+            $headercategoryeval = $val, next if $key eq 'headercategoryeval';
         }
     }
     close(OPTIONS);
 }
 
+sub escLaTeX {
+    my $str = shift;
+    $str =~ s/([_\#\&\^])/\\$1/g;
+    return $str;
+}
+
 my $wordwrap_mode = 'mediawiki';
 sub wordwrap_atom {   # don't call this directly.
     my $str = shift;
@@ -174,6 +190,8 @@ sub wordwrap_one_paragraph {  # don't call this directly.
         if ($item ne '') {
             $retval .= wordwrap_with_bullet_indent($bullet, $item);
         }
+    } elsif ($p =~ /\A\s*\|.*\|\s*\n/) {  # Markdown table
+        $retval = "$p\n";  # don't wrap it (!!! FIXME: but maybe parse by lines until we run out of table...)
     } else {
         $retval = wordwrap_atom($p) . "\n";
     }
@@ -297,7 +315,7 @@ sub wikify_chunk {
         $str = $codedstr . $str;
 
         if (defined $code) {
-            $str .= "```$codelang$code```";
+            $str .= "```$codelang\n$code\n```\n";
         }
     }
 
@@ -313,7 +331,7 @@ sub wikify {
 
     #print("WIKIFY WHOLE:\n\n$str\n\n\n");
 
-    while ($str =~ s/\A(.*?)\`\`\`(c\+\+|c)(.*?)\`\`\`//ms) {
+    while ($str =~ s/\A(.*?)\`\`\`(.*?)\n(.*?)\n\`\`\`(\n|\Z)//ms) {
         $retval .= wikify_chunk($wikitype, $1, $2, $3);
     }
     $retval .= wikify_chunk($wikitype, $str, undef, undef);
@@ -370,7 +388,7 @@ sub dewikify_chunk {
         }
 
         if (defined $code) {
-            $str .= "```$codelang$code```";
+            $str .= "\n```$codelang\n$code\n```\n";
         }
     } elsif ($dewikify_mode eq 'manpage') {
         $str =~ s/\./\\[char46]/gms;  # make sure these can't become control codes.
@@ -420,9 +438,6 @@ sub dewikify_chunk {
 
             # bullets
             $str =~ s/^\- /\n\\\(bu /gm;
-
-        } else {
-            die("Unexpected wikitype when converting to manpages\n");   # !!! FIXME: need to handle Markdown wiki pages.
         }
 
         if (defined $code) {
@@ -435,8 +450,80 @@ sub dewikify_chunk {
             }
             $str .= ".EX\n$code\n.EE\n.PP\n";
         }
+    } elsif ($dewikify_mode eq 'LaTeX') {
+        if ($wikitype eq 'mediawiki') {
+            # Dump obvious wikilinks.
+            if (defined $apiprefixregex) {
+                $str =~ s/\s*\[\[($apiprefixregex[a-zA-Z0-9_]+)\]\]/$1/gms;
+            }
+
+            # links
+            $str =~ s/\[(https?\:\/\/.*?)\s+(.*?)\]/\\href{$1}{$2}/g;
+
+            # <code></code> is also popular.  :/
+            $str =~ s/\s*\<code>(.*?)<\/code>/ \\texttt{$1}/gms;
+
+            # bold+italic
+            $str =~ s/\s*'''''(.*?)'''''/ \\textbf{\\textit{$1}}/gms;
+
+            # bold
+            $str =~ s/\s*'''(.*?)'''/ \\textbf{$1}/gms;
+
+            # italic
+            $str =~ s/\s*''(.*?)''/ \\textit{$1}/gms;
+
+            # bullets
+            $str =~ s/^\*\s+/  \\item /gm;
+        } elsif ($wikitype eq 'md') {
+            # Dump obvious wikilinks.
+            if (defined $apiprefixregex) {
+                $str =~ s/\[(\`?$apiprefixregex[a-zA-Z0-9_]+\`?)\]\($apiprefixregex[a-zA-Z0-9_]+\)/$1/gms;
+            }
+
+            # links
+            $str =~ s/\[(.*?)]\((https?\:\/\/.*?)\)/\\href{$2}{$1}/g;
+
+            # <code></code> is also popular.  :/
+            $str =~ s/\s*\`(.*?)\`/ \\texttt{$1}/gms;
+
+            # bold+italic
+            $str =~ s/\s*\*\*\*(.*?)\*\*\*/ \\textbf{\\textit{$1}}/gms;
+
+            # bold
+            $str =~ s/\s*\*\*(.*?)\*\*/ \\textbf{$1}/gms;
+
+            # italic
+            $str =~ s/\s*\*(.*?)\*/ \\textit{$1}/gms;
+
+            # bullets
+            $str =~ s/^\-\s+/  \\item /gm;
+        }
+
+        # Wrap bullet lists in itemize blocks...
+        $str =~ s/^(\s*\\item .*?)(\n\n|\Z)/\n\\begin{itemize}\n$1$2\n\\end{itemize}\n\n/gms;
+
+        $str = escLaTeX($str);
+
+        if (defined $code) {
+            $code =~ s/\A\n+//gms;
+            $code =~ s/\n+\Z//gms;
+
+            if (($codelang eq '') || ($codelang eq 'output')) {
+                $str .= "\\begin{verbatim}\n$code\n\\end{verbatim}\n";
+            } else {
+                if ($codelang eq 'c') {
+                    $codelang = 'C';
+                } elsif ($codelang eq 'c++') {
+                    $codelang = 'C++';
+                } else {
+                    die("Unexpected codelang '$codelang'");
+                }
+                $str .= "\n\\lstset{language=$codelang}\n";
+                $str .= "\\begin{lstlisting}\n$code\n\\end{lstlisting}\n";
+            }
+        }
     } else {
-        die("Unexpected dewikify_mode\n");
+        die("Unexpected dewikify_mode");
     }
 
     #print("\n\nDEWIKIFY CHUNK DONE:\n\n$str\n\n\n");
@@ -455,8 +542,14 @@ sub dewikify {
     $str =~ s/\A[\s\n]*\=\= .*? \=\=\s*?\n+//ms;
 
     my $retval = '';
-    while ($str =~ s/\A(.*?)<syntaxhighlight lang='?(.*?)'?>(.*?)<\/syntaxhighlight\>//ms) {
-        $retval .= dewikify_chunk($wikitype, $1, $2, $3);
+    if ($wikitype eq 'mediawiki') {
+        while ($str =~ s/\A(.*?)<syntaxhighlight lang='?(.*?)'?>(.*?)<\/syntaxhighlight\>//ms) {
+            $retval .= dewikify_chunk($wikitype, $1, $2, $3);
+        }
+    } elsif ($wikitype eq 'md') {
+        while ($str =~ s/\A(.*?)\n```(.*?)\n(.*?)\n```\n//ms) {
+            $retval .= dewikify_chunk($wikitype, $1, $2, $3);
+        }
     }
     $retval .= dewikify_chunk($wikitype, $str, undef, undef);
 
@@ -498,30 +591,93 @@ sub usage {
     'Draft',
     '[Brief]',
     'Deprecated',
+    'Header File',
     'Syntax',
     'Function Parameters',
+    'Macro Parameters',
+    'Fields',
+    'Values',
     'Return Value',
     'Remarks',
     'Thread Safety',
     'Version',
     'Code Examples',
-    'Related Functions'
+    'See Also'
 );
 
 # Sections that only ever exist in the wiki and shouldn't be deleted when
 #  not found in the headers.
 my %only_wiki_sections = (  # The ones don't mean anything, I just need to check for key existence.
     'Draft', 1,
-    'Code Examples', 1
+    'Code Examples', 1,
+    'Header File', 1
 );
 
 
 my %headers = ();       # $headers{"SDL_audio.h"} -> reference to an array of all lines of text in SDL_audio.h.
-my %headerfuncs = ();   # $headerfuncs{"SDL_OpenAudio"} -> string of header documentation for SDL_OpenAudio, with comment '*' bits stripped from the start. Newlines embedded!
+my %headersyms = ();   # $headersyms{"SDL_OpenAudio"} -> string of header documentation for SDL_OpenAudio, with comment '*' bits stripped from the start. Newlines embedded!
 my %headerdecls = ();
-my %headerfuncslocation = ();   # $headerfuncslocation{"SDL_OpenAudio"} -> name of header holding SDL_OpenAudio define ("SDL_audio.h" in this case).
-my %headerfuncschunk = ();   # $headerfuncschunk{"SDL_OpenAudio"} -> offset in array in %headers that should be replaced for this function.
-my %headerfuncshasdoxygen = ();   # $headerfuncschunk{"SDL_OpenAudio"} -> 1 if there was no existing doxygen for this function.
+my %headersymslocation = ();   # $headersymslocation{"SDL_OpenAudio"} -> name of header holding SDL_OpenAudio define ("SDL_audio.h" in this case).
+my %headersymschunk = ();   # $headersymschunk{"SDL_OpenAudio"} -> offset in array in %headers that should be replaced for this symbol.
+my %headersymshasdoxygen = ();   # $headersymshasdoxygen{"SDL_OpenAudio"} -> 1 if there was no existing doxygen for this function.
+my %headersymstype = ();   # $headersymstype{"SDL_OpenAudio"} -> 1 (function), 2 (macro), 3 (struct), 4 (enum), 5 (other typedef)
+my %headersymscategory = ();   # $headersymscategory{"SDL_OpenAudio"} -> 'Audio' ... this is set with a `/* WIKI CATEGEORY: Audio */` comment in the headers that sets it on all symbols until a new comment changes it. So usually, once at the top of the header file.
+
+my %wikitypes = ();  # contains string of wiki page extension, like $wikitypes{"SDL_OpenAudio"} == 'mediawiki'
+my %wikisyms = ();  # contains references to hash of strings, each string being the full contents of a section of a wiki page, like $wikisyms{"SDL_OpenAudio"}{"Remarks"}.
+my %wikisectionorder = ();   # contains references to array, each array item being a key to a wikipage section in the correct order, like $wikisectionorder{"SDL_OpenAudio"}[2] == 'Remarks'
+
+my %referenceonly = ();  # $referenceonly{"Y"} -> symbol name that this symbol is bound to. This makes wiki pages that say "See X" where "X" is a typedef and "Y" is a define attached to it. These pages are generated in the wiki only and do not bridge to the headers or manpages.
+
+my @coverage_gap = ();  # array of strings that weren't part of documentation, or blank, or basic preprocessor logic. Lets you see what this script is missing!
+
+sub add_coverage_gap {
+    if ($copy_direction == -3) {  # --report-coverage-gaps
+        my $text = shift;
+        my $dent = shift;
+        my $lineno = shift;
+        return if $text =~ /\A\s*\Z/;  # skip blank lines
+        return if $text =~ /\A\s*\#\s*(if|el|endif|include)/; # skip preprocessor floof.
+        push @coverage_gap, "$dent:$lineno: $text";
+    }
+}
+
+sub print_undocumented_section {
+    my $fh = shift;
+    my $typestr = shift;
+    my $typeval = shift;
+
+    print $fh "## $typestr defined in the headers, but not in the wiki\n\n";
+    my $header_only_sym = 0;
+    foreach (sort keys %headersyms) {
+        my $sym = $_;
+        if ((not defined $wikisyms{$sym}) && ($headersymstype{$sym} == $typeval)) {
+            print $fh "- [$sym]($sym)\n";
+            $header_only_sym = 1;
+        }
+    }
+    if (!$header_only_sym) {
+        print $fh "(none)\n";
+    }
+    print $fh "\n";
+
+    if (0) {  # !!! FIXME: this lists things that _shouldn't_ be in the headers, like MigrationGuide, etc, but also we don't know if they're functions, macros, etc at this point (can we parse that from the wiki page, though?)
+    print $fh "## $typestr defined in the wiki, but not in the headers\n\n";
+
+    my $wiki_only_sym = 0;
+    foreach (sort keys %wikisyms) {
+        my $sym = $_;
+        if ((not defined $headersyms{$sym}) && ($headersymstype{$sym} == $typeval)) {
+            print $fh "- [$sym]($sym)\n";
+            $wiki_only_sym = 1;
+        }
+    }
+    if (!$wiki_only_sym) {
+        print $fh "(none)\n";
+    }
+    print $fh "\n";
+    }
+}
 
 my $incpath = "$srcpath";
 $incpath .= "/$incsubdir" if $incsubdir ne '';
@@ -538,32 +694,85 @@ sub usage {
     next if not $dent =~ /$selectheaderregex/;  # just selected headers.
     open(FH, '<', "$incpath/$dent") or die("Can't open '$incpath/$dent': $!\n");
 
-    my @contents = ();
+    # You can optionally set a wiki category with Perl code in .wikiheaders-options that gets eval()'d per-header,
+    # and also if you put `/* WIKI CATEGORY: blah */` on a line by itself, it'll change the category for any symbols
+    # below it in the same file. If no category is set, one won't be added for the symbol (beyond the standard CategoryFunction, etc)
+    my $current_wiki_category = undef;
+    if (defined $headercategoryeval) {
+        $_ = $dent;
+        $current_wiki_category = eval($headercategoryeval);
+        if (($current_wiki_category eq '') || ($current_wiki_category eq '-')) {
+            $current_wiki_category = undef;
+        }
+        #print("CATEGORY FOR '$dent' IS " . (defined($current_wiki_category) ? "'$current_wiki_category'" : '(undef)') . "\n");
+    }
 
+    my @contents = ();
+    my $ignoring_lines = 0;
+    my $header_comment = -1;
+    my $lineno = 0;
     while (<FH>) {
         chomp;
+        $lineno++;
+        my $symtype = 0;  # nothing, yet.
         my $decl;
         my @templines;
         my $str;
         my $has_doxygen = 1;
-        if (/\A\s*extern\s+(SDL_DEPRECATED\s+|)DECLSPEC/) {  # a function declaration without a doxygen comment?
+
+        # Since a lot of macros are just preprocessor logic spam and not all macros are worth documenting anyhow, we only pay attention to them when they have a Doxygen comment attached.
+        # Functions and other things are a different story, though!
+
+        if ($header_comment == -1) {
+            $header_comment = /\A\/\*\s*\Z/ ? 1 : 0;
+        } elsif (($header_comment == 1) && (/\A\*\/\s*\Z/)) {
+            $header_comment = 0;
+        }
+
+        if ($ignoring_lines && /\A\s*\#\s*endif\s*\Z/) {
+            $ignoring_lines = 0;
+            push @contents, $_;
+            next;
+        } elsif ($ignoring_lines) {
+            push @contents, $_;
+            next;
+        } elsif (/\A\s*\#\s*ifndef\s+SDL_WIKI_DOCUMENTATION_SECTION\s*\Z/) {
+            $ignoring_lines = 1;
+            push @contents, $_;
+            next;
+        } elsif (/\A\s*\/\*\s*WIKI CATEGORY:\s*(.*?)\s*\*\/\s*\Z/) {
+            $current_wiki_category = (($1 eq '') || ($1 eq '-')) ? undef : $1;
+            #print("CATEGORY FOR '$dent' CHANGED TO " . (defined($current_wiki_category) ? "'$current_wiki_category'" : '(undef)') . "\n");
+            push @contents, $_;
+            next;
+        } elsif (/\A\s*extern\s+(SDL_DEPRECATED\s+|)(SDLMAIN_)?DECLSPEC/) {  # a function declaration without a doxygen comment?
+            $symtype = 1;   # function declaration
+            @templines = ();
+            $decl = $_;
+            $str = '';
+            $has_doxygen = 0;
+        } elsif (/\A\s*SDL_FORCE_INLINE/) {  # a (forced-inline) function declaration without a doxygen comment?
+            $symtype = 1;   # function declaration
             @templines = ();
             $decl = $_;
             $str = '';
             $has_doxygen = 0;
         } elsif (not /\A\/\*\*\s*\Z/) {  # not doxygen comment start?
             push @contents, $_;
+            add_coverage_gap($_, $dent, $lineno) if ($header_comment == 0);
             next;
         } else {   # Start of a doxygen comment, parse it out.
             @templines = ( $_ );
             while (<FH>) {
                 chomp;
+                $lineno++;
                 push @templines, $_;
                 last if /\A\s*\*\/\Z/;
                 if (s/\A\s*\*\s*\`\`\`/```/) {  # this is a hack, but a lot of other code relies on the whitespace being trimmed, but we can't trim it in code blocks...
                     $str .= "$_\n";
                     while (<FH>) {
                         chomp;
+                        $lineno++;
                         push @templines, $_;
                         s/\A\s*\*\s?//;
                         if (s/\A\s*\`\`\`/```/) {
@@ -580,89 +789,357 @@ sub usage {
             }
 
             $decl = <FH>;
+            $lineno++ if defined $decl;
             $decl = '' if not defined $decl;
             chomp($decl);
-            if (not $decl =~ /\A\s*extern\s+(SDL_DEPRECATED\s+|)DECLSPEC/) {
+            if ($decl =~ /\A\s*extern\s+(SDL_DEPRECATED\s+|)(SDLMAIN_)?DECLSPEC/) {
+                $symtype = 1;   # function declaration
+            } elsif ($decl =~ /\A\s*SDL_FORCE_INLINE/) {
+                $symtype = 1;   # (forced-inline) function declaration
+            } elsif ($decl =~ /\A\s*\#\s*define\s+/) {
+                $symtype = 2;   # macro
+            } elsif ($decl =~ /\A\s*(typedef\s+|)(struct|union)/) {
+                $symtype = 3;   # struct or union
+            } elsif ($decl =~ /\A\s*(typedef\s+|)enum/) {
+                $symtype = 4;   # enum
+            } elsif ($decl =~ /\A\s*typedef\s+.*\Z/) {
+                $symtype = 5;   # other typedef
+            } else {
                 #print "Found doxygen but no function sig:\n$str\n\n";
                 foreach (@templines) {
                     push @contents, $_;
+                    add_coverage_gap($_, $dent, $lineno);
                 }
                 push @contents, $decl;
+                add_coverage_gap($decl, $dent, $lineno);
                 next;
             }
         }
 
         my @decllines = ( $decl );
+        my $sym = '';
+
+        if ($symtype == 1) {  # a function
+            my $is_forced_inline = ($decl =~ /\A\s*SDL_FORCE_INLINE/);
+
+            if ($is_forced_inline) {
+                if (not $decl =~ /\)\s*(\{.*|)\s*\Z/) {
+                    while (<FH>) {
+                        chomp;
+                        $lineno++;
+                        push @decllines, $_;
+                        s/\A\s+//;
+                        s/\s+\Z//;
+                        $decl .= " $_";
+                        last if /\)\s*(\{.*|)\s*\Z/;
+                    }
+                }
+                $decl =~ s/\s*\)\s*(\{.*|)\s*\Z/);/;
+            } else {
+                if (not $decl =~ /\)\s*;/) {
+                    while (<FH>) {
+                        chomp;
+                        $lineno++;
+                        push @decllines, $_;
+                        s/\A\s+//;
+                        s/\s+\Z//;
+                        $decl .= " $_";
+                        last if /\)\s*;/;
+                    }
+                }
+                $decl =~ s/\s+\);\Z/);/;
+            }
+
+            $decl =~ s/\s+\Z//;
+
+            if (!$is_forced_inline && $decl =~ /\A\s*extern\s+(SDL_DEPRECATED\s+|)(SDLMAIN_)?DECLSPEC\s+(const\s+|)(unsigned\s+|)(.*?)\s*(\*?)\s*SDLCALL\s+(.*?)\s*\((.*?)\);/) {
+                $sym = $7;
+                #$decl =~ s/\A\s*extern\s+DECLSPEC\s+(.*?)\s+SDLCALL/$1/;
+            } elsif ($is_forced_inline && $decl =~ /\A\s*SDL_FORCE_INLINE\s+(SDL_DEPRECATED\s+|)(const\s+|)(unsigned\s+|)(.*?)([\*\s]+)(.*?)\s*\((.*?)\);/) {
+                $sym = $6;
+            } else {
+                #print "Found doxygen but no function sig:\n$str\n\n";
+                foreach (@templines) {
+                    push @contents, $_;
+                }
+                foreach (@decllines) {
+                    push @contents, $_;
+                }
+                next;
+            }
 
-        if (not $decl =~ /\)\s*;/) {
+            if (!$is_forced_inline) {  # !!! FIXME: maybe we need to do this for forced-inline stuff too?
+                $decl = '';  # build this with the line breaks, since it looks better for syntax highlighting.
+                foreach (@decllines) {
+                    if ($decl eq '') {
+                        $decl = $_;
+                        $decl =~ s/\Aextern\s+(SDL_DEPRECATED\s+|)(SDLMAIN_)?DECLSPEC\s+(.*?)\s+(\*?)SDLCALL\s+/$3$4 /;
+                    } else {
+                        my $trimmed = $_;
+                        # !!! FIXME: trim space for SDL_DEPRECATED if it was used, too.
+                        $trimmed =~ s/\A\s{24}//;  # 24 for shrinking to match the removed "extern DECLSPEC SDLCALL "
+                        $decl .= $trimmed;
+                    }
+                    $decl .= "\n";
+                }
+            }
+
+            # !!! FIXME: code duplication with typedef processing, below.
+            # We assume any `#define`s directly after the function are related to it: probably bitflags for an integer typedef.
+            # We'll also allow some other basic preprocessor lines.
+            # Blank lines are allowed, anything else, even comments, are not.
+            my $blank_lines = 0;
+            my $lastpos = tell(FH);
+            my $lastlineno = $lineno;
+            my $additional_decl = '';
+            my $saw_define = 0;
             while (<FH>) {
                 chomp;
-                push @decllines, $_;
-                s/\A\s+//;
-                s/\s+\Z//;
-                $decl .= " $_";
-                last if /\)\s*;/;
+
+                $lineno++;
+
+                if (/\A\s*\Z/) {
+                    $blank_lines++;
+                } elsif (/\A\s*\#\s*(define|if|else|elif|endif)(\s+|\Z)/) {
+                    if (/\A\s*\#\s*define\s+([a-zA-Z0-9_]*)/) {
+                        $referenceonly{$1} = $sym;
+                        $saw_define = 1;
+                    } elsif (!$saw_define) {
+                        # if the first non-blank thing isn't a #define, assume we're done.
+                        seek(FH, $lastpos, 0);  # re-read eaten lines again next time.
+                        $lineno = $lastlineno;
+                        last;
+                    }
+
+                    # update strings now that we know everything pending is to be applied to this declaration. Add pending blank lines and the new text.
+
+                    # At Sam's request, don't list property defines with functions. (See #9440)
+                    my $is_property = /\A\s*\#\s*define\s+SDL_PROP_/;
+                    if (!$is_property) {
+                        if ($blank_lines > 0) {
+                            while ($blank_lines > 0) {
+                                $additional_decl .= "\n";
+                                push @decllines, '';
+                                $blank_lines--;
+                            }
+                        }
+                        $additional_decl .= "\n$_";
+                        push @decllines, $_;
+                        $lastpos = tell(FH);
+                    }
+                } else {
+                    seek(FH, $lastpos, 0);  # re-read eaten lines again next time.
+                    $lineno = $lastlineno;
+                    last;
+                }
             }
-        }
+            $decl .= $additional_decl;
 
-        $decl =~ s/\s+\);\Z/);/;
-        $decl =~ s/\s+\Z//;
-        #print("DECL: [$decl]\n");
+        } elsif ($symtype == 2) {  # a macro
+            if ($decl =~ /\A\s*\#\s*define\s+(.*?)(\(.*?\)|)\s+/) {
+                $sym = $1;
+                #$decl =~ s/\A\s*extern\s+DECLSPEC\s+(.*?)\s+SDLCALL/$1/;
+            } else {
+                #print "Found doxygen but no macro:\n$str\n\n";
+                foreach (@templines) {
+                    push @contents, $_;
+                }
+                foreach (@decllines) {
+                    push @contents, $_;
+                }
+                next;
+            }
 
-        my $fn = '';
-        if ($decl =~ /\A\s*extern\s+(SDL_DEPRECATED\s+|)DECLSPEC\s+(const\s+|)(unsigned\s+|)(.*?)\s*(\*?)\s*SDLCALL\s+(.*?)\s*\((.*?)\);/) {
-            $fn = $6;
-            #$decl =~ s/\A\s*extern\s+DECLSPEC\s+(.*?)\s+SDLCALL/$1/;
-        } else {
-            #print "Found doxygen but no function sig:\n$str\n\n";
-            foreach (@templines) {
-                push @contents, $_;
+            while ($decl =~ /\\\Z/) {
+                my $l = <FH>;
+                last if not $l;
+                $lineno++;
+                chomp($l);
+                push @decllines, $l;
+                #$l =~ s/\A\s+//;
+                $l =~ s/\s+\Z//;
+                $decl .= "\n$l";
             }
-            foreach (@decllines) {
-                push @contents, $_;
+        } elsif (($symtype == 3) || ($symtype == 4)) {  # struct or union or enum
+            my $has_definition = 0;
+            if ($decl =~ /\A\s*(typedef\s+|)(struct|union|enum)\s*(.*?)\s*(\n|\{|\;|\Z)/) {
+                my $ctype = $2;
+                my $origsym = $3;
+                my $ending = $4;
+                $sym = $origsym;
+                if ($sym =~ s/\A(.*?)(\s+)(.*?)\Z/$1/) {
+                    die("Failed to parse '$origsym' correctly!") if ($sym ne $1);  # Thought this was "typedef struct MySym MySym;" ... it was not.  :(  This is a hack!
+                }
+                if ($sym eq '') {
+                    die("\n\n$0 FAILURE!\n" .
+                        "There's a 'typedef $ctype' in $incpath/$dent without a name at the top.\n" .
+                        "Instead of `typedef $ctype {} x;`, this should be `typedef $ctype x {} x;`.\n" .
+                        "This causes problems for wikiheaders.pl and scripting language bindings.\n" .
+                        "Please fix it!\n\n");
+                }
+                $has_definition = ($ending ne ';');
+            } else {
+                #print "Found doxygen but no datatype:\n$str\n\n";
+                foreach (@templines) {
+                    push @contents, $_;
+                }
+                foreach (@decllines) {
+                    push @contents, $_;
+                }
+                next;
             }
-            next;
-        }
 
-        $decl = '';  # build this with the line breaks, since it looks better for syntax highlighting.
-        foreach (@decllines) {
-            if ($decl eq '') {
-                $decl = $_;
-                $decl =~ s/\Aextern\s+(SDL_DEPRECATED\s+|)DECLSPEC\s+(.*?)\s+(\*?)SDLCALL\s+/$2$3 /;
+            # This block attempts to find the whole struct/union/enum definition by counting matching brackets. Kind of yucky.
+            if ($has_definition) {
+                my $started = 0;
+                my $brackets = 0;
+                my $pending = $decl;
+
+                $decl = '';
+                while (!$started || ($brackets != 0)) {
+                    foreach my $seg (split(/([{}])/, $pending)) {
+                        $decl .= $seg;
+                        if ($seg eq '{') {
+                            $started = 1;
+                            $brackets++;
+                        } elsif ($seg eq '}') {
+                            die("Something is wrong with header $incpath/$dent while parsing $sym; is a bracket missing?\n") if ($brackets <= 0);
+                            $brackets--;
+                        }
+                    }
+
+                    if (!$started || ($brackets != 0)) {
+                        $pending = <FH>;
+                        die("EOF/error reading $incpath/$dent while parsing $sym\n") if not $pending;
+                        $lineno++;
+                        chomp($pending);
+                        push @decllines, $pending;
+                        $decl .= "\n";
+                    }
+                }
+                # this currently assumes the struct/union/enum ends on the line with the final bracket. I'm not writing a C parser here, fix the header!
+            }
+        } elsif ($symtype == 5) {  # other typedef
+            if ($decl =~ /\A\s*typedef\s+(.*)\Z/) {
+                my $tdstr = $1;
+
+                if (not $decl =~ /;/) {
+                    while (<FH>) {
+                        chomp;
+                        $lineno++;
+                        push @decllines, $_;
+                        s/\A\s+//;
+                        s/\s+\Z//;
+                        $decl .= " $_";
+                        last if /;/;
+                    }
+                }
+                $decl =~ s/\s+(\))?;\Z/$1;/;
+
+                $tdstr =~ s/;\s*\Z//;
+
+                #my $datatype;
+                if ($tdstr =~ /\A(.*?)\s*\((.*?)\s*\*\s*(.*?)\)\s*\((.*?)(\))?/) {  # a function pointer type
+                    $sym = $3;
+                    #$datatype = "$1 ($2 *$sym)($4)";
+                } elsif ($tdstr =~ /\A(.*[\s\*]+)(.*?)\s*\Z/) {
+                    $sym = $2;
+                    #$datatype = $1;
+                } else {
+                    die("Failed to parse typedef '$tdstr' in $incpath/$dent!\n");  # I'm hitting a C grammar nail with a regexp hammer here, y'all.
+                }
+
+                $sym =~ s/\A\s+//;
+                $sym =~ s/\s+\Z//;
+                #$datatype =~ s/\A\s+//;
+                #$datatype =~ s/\s+\Z//;
             } else {
-                my $trimmed = $_;
-                # !!! FIXME: trim space for SDL_DEPRECATED if it was used, too.
-                $trimmed =~ s/\A\s{24}//;  # 24 for shrinking to match the removed "extern DECLSPEC SDLCALL "
-                $decl .= $trimmed;
+                #print "Found doxygen but no datatype:\n$str\n\n";
+                foreach (@templines) {
+                    push @contents, $_;
+                }
+                foreach (@decllines) {
+                    push @contents, $_;
+                }
+                next;
             }
-            $decl .= "\n";
+
+            # We assume any `#define`s directly after the typedef are related to it: probably bitflags for an integer typedef.
+            # We'll also allow some other basic preprocessor lines.
+            # Blank lines are allowed, anything else, even comments, are not.
+            my $blank_lines = 0;
+            my $lastpos = tell(FH);
+            my $lastlineno = $lineno;
+            my $additional_decl = '';
+            my $saw_define = 0;
+            while (<FH>) {
+                chomp;
+
+                $lineno++;
+
+                if (/\A\s*\Z/) {
+                    $blank_lines++;
+                } elsif (/\A\s*\#\s*(define|if|else|elif|endif)(\s+|\Z)/) {
+                    if (/\A\s*\#\s*define\s+([a-zA-Z0-9_]*)/) {
+                        $referenceonly{$1} = $sym;
+                        $saw_define = 1;
+                    } elsif (!$saw_define) {
+                        # if the first non-blank thing isn't a #define, assume we're done.
+                        seek(FH, $lastpos, 0);  # re-read eaten lines again next time.
+                        $lineno = $lastlineno;
+                        last;
+                    }
+                    # update strings now that we know everything pending is to be applied to this declaration. Add pending blank lines and the new text.
+                    if ($blank_lines > 0) {
+                        while ($blank_lines > 0) {
+                            $additional_decl .= "\n";
+                            push @decllines, '';
+                            $blank_lines--;
+                        }
+                    }
+                    $additional_decl .= "\n$_";
+                    push @decllines, $_;
+                    $lastpos = tell(FH);
+                } else {
+                    seek(FH, $lastpos, 0);  # re-read eaten lin

(Patch may be truncated, please check the link at the top of this post.)