Private/ConvertTo-XKCDPlainText.ps1

Function ConvertTo-XKCDPlainText {
    <#
    .SYNOPSIS
        Converts explainxkcd wikitext markup into plain, readable text for display in the console.
    #>

    [cmdletbinding()]
    Param(
        # The raw MediaWiki wikitext to convert.
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowEmptyString()]
        [string]
        $WikiText
    )

    process {
        $Text = $WikiText

        # Builds a url-safe page-title segment: spaces become underscores (MediaWiki/Wikipedia's own convention,
        # kept for readability), and anything else that isn't url-safe (e.g. "?", seen in "what if? (blog)") is
        # percent-encoded so the link actually works, rather than being cut short or rejected by the server.
        # Parentheses are left literal, since e.g. "Installer_(OS_X)" is the real, working Wikipedia url.
        Function Format-XKCDUrlSegment([string]$Value) {
            $Underscored = $Value -replace ' ', '_'
            ([uri]::EscapeDataString($Underscored)) -replace '%28', '(' -replace '%29', ')'
        }

        # Strip HTML comments and the section heading itself (its name is already known to the caller)
        $Text = $Text -replace '(?s)<!--.*?-->', ''
        $Text = $Text -replace '(?m)^==+\s*.*?\s*==+\s*$', ''

        # <nowiki>x</nowiki> just protects x from being interpreted as markup, so unwrap it as-is
        $Text = $Text -replace '(?s)<nowiki>(.*?)</nowiki>', '$1'

        # Inline/preformatted code, e.g. <code>print()</code> or <pre>...</pre>, is wrapped in backticks
        # (Markdown-style) so it stays recognisable as code in plain text and can be styled distinctly when displayed.
        $Text = $Text -replace '(?s)<code>(.*?)</code>', '`$1`'
        $Text = $Text -replace '(?s)<pre>(.*?)</pre>', '`$1`'

        # MediaWiki also renders any line starting with a space as preformatted code (a separate convention from
        # <code>/<pre>, e.g. a shell command example) -- wrap those in backticks too.
        $Text = $Text -replace '(?m)^ +(.+)$', '`$1`'

        # External links: [http://url display text] -> "[display text](http://url)" (Markdown-style, so the
        # words themselves can become the working hyperlink without printing the url separately);
        # [http://url] -> "http://url" since there's no other text to show.
        $Text = $Text -replace '\[(https?://\S+)\s+([^\]]+)\]', '[$2]($1)'
        $Text = $Text -replace '\[(https?://\S+)\]', '$1'

        # Internal wiki links/categories: [[Page|Display]] -> "[Display](url)", [[Page]] -> "[Page](url)", so
        # these become working hyperlinks too, the same way external links do. A leading ":" (used to link to a
        # category/file page rather than apply it) isn't part of the real title, so it's dropped from the url.
        # Anchors to a section of the current page (e.g. [[#Trivia]]) are left as plain text, since there's no
        # page url available here to link them against.
        $InternalLinkEvaluator = [System.Text.RegularExpressions.MatchEvaluator] {
            param($Match)
            $Parts = $Match.Groups[1].Value -split '\|'
            $Target = $Parts[0].TrimStart(':')
            $Display = $Parts[-1]

            if ($Target.StartsWith('#')) {
                $Display
            }
            else {
                $PageAndAnchor = $Target -split '#', 2
                $Page = Format-XKCDUrlSegment $PageAndAnchor[0]
                $UrlTarget = if ($PageAndAnchor.Count -gt 1) { "$Page#$($PageAndAnchor[1])" } else { $Page }
                "[$Display](https://www.explainxkcd.com/wiki/index.php/$UrlTarget)"
            }
        }
        $Text = [regex]::Replace($Text, '\[\[([^\]]+)\]\]', $InternalLinkEvaluator)

        # Templates. {{w|Page}}/{{w|Page|Display}} and {{what if|N}}/{{what if|N|Display}} are links to Wikipedia
        # and the What If blog respectively, so become working Markdown-style hyperlinks the same way other
        # links do, e.g. {{w|Retina Display}} -> "[Retina Display](https://en.wikipedia.org/wiki/Retina_Display)".
        # Other templates aren't links, e.g. {{Citation needed}} -> "[citation needed]". Resolved innermost-first
        # so nested templates (rare, but possible) collapse correctly.
        $TemplateEvaluator = [System.Text.RegularExpressions.MatchEvaluator] {
            param($Match)
            $Parts = $Match.Groups[1].Value -split '\|'
            $Name = $Parts[0].Trim()

            if ($Name -ieq 'w' -and $Parts.Count -gt 1) {
                $PageAndAnchor = $Parts[1] -split '#', 2
                $Page = Format-XKCDUrlSegment $PageAndAnchor[0]
                $UrlTarget = if ($PageAndAnchor.Count -gt 1) { "$Page#$($PageAndAnchor[1])" } else { $Page }
                "[$($Parts[-1])](https://en.wikipedia.org/wiki/$UrlTarget)"
            }
            elseif ($Name -ieq 'what if' -and $Parts.Count -gt 1) {
                $Display = if ($Parts.Count -gt 2) { $Parts[-1] } else { "what if #$($Parts[1])" }
                "[$Display](https://what-if.xkcd.com/$($Parts[1])/)"
            }
            elseif ($Parts.Count -gt 1) {
                $Parts[-1]
            }
            else {
                "[$($Parts[0].ToLower())]"
            }
        }
        while ($Text -match '\{\{([^{}]+)\}\}') {
            $Text = [regex]::Replace($Text, '\{\{([^{}]+)\}\}', $TemplateEvaluator)
        }

        # Bold/italic markup -> Markdown-style ("'''''" -> "***", "'''" -> "**", "''" -> "*") so it can be styled
        # distinctly when displayed, instead of just being discarded. Order matters: the longest marker must be
        # replaced first, since e.g. a run of 5 apostrophes is one bold-italic marker, not a 3- and a 2-marker.
        $Text = $Text -replace "'''''", '***' -replace "'''", '**' -replace "''", '*'

        # Definition lists, e.g. "; Term" -> "Term:". Description/reply lines are indented by nesting depth,
        # e.g. ": Description" -> " Description", "::: Nested reply" -> " Nested reply".
        $Text = $Text -replace '(?m)^;\s*(.+)$', '$1:'
        $IndentEvaluator = [System.Text.RegularExpressions.MatchEvaluator] {
            param($Match)
            ' ' * $Match.Groups[1].Value.Length
        }
        $Text = [regex]::Replace($Text, '(?m)^(:+)\s*', $IndentEvaluator)

        # Collapse the resulting excess blank lines and surrounding whitespace
        $Text = $Text -replace '(?m)[ \t]+$', ''
        $Text = $Text -replace '(?s)\n{3,}', "`n`n"

        $Text.Trim()
    }
}