NetscootShared/Common/Json.ps1

# JSONC (JSON-with-comments) tolerant reader. VS Code settings.json - which Test-EditorSolutionGuard
# inspects - is JSONC: it permits // line comments, /* */ block comments, and trailing commas, none
# of which ConvertFrom-Json accepts (on either PowerShell edition; Windows PowerShell 5.1 has no
# tolerant mode at all). Rather than fail to read a perfectly valid settings file, strip the comments
# and trailing commas first, respecting string literals, then hand clean JSON to ConvertFrom-Json.

function ConvertFrom-Jsonc {
    # Parse a JSONC string (JSON + // and /* */ comments + trailing commas) into an object. Comments
    # and a single trailing comma before } or ] are removed by a character scanner that does NOT
    # treat // or /* inside a string literal as a comment, and respects backslash escapes. Returns
    # $null for empty/whitespace input. Throws (like ConvertFrom-Json) if the cleaned text is still
    # not valid JSON.
    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Text)

    if ([string]::IsNullOrWhiteSpace($Text)) { return $null }

    $sb = [System.Text.StringBuilder]::new($Text.Length)
    $i = 0
    $len = $Text.Length
    $inString = $false

    # Drop trailing whitespace and at most one trailing comma already written to the builder. Called
    # in non-string state just before a } or ] is appended, so a trailing comma is removed safely
    # (a comma inside a string is never seen here because we only call this outside strings).
    $trimTrailingComma = {
        $k = $sb.Length - 1
        while ($k -ge 0 -and [char]::IsWhiteSpace($sb[$k])) { $k-- }
        if ($k -ge 0 -and $sb[$k] -eq ',') {
            $sb.Length = $k          # drop the comma; following whitespace is re-emitted by the char itself
        }
    }

    while ($i -lt $len) {
        $c = $Text[$i]

        if ($inString) {
            [void]$sb.Append($c)
            if ($c -eq '\') {
                # Emit the escaped character verbatim (covers \" so it does not end the string).
                if ($i + 1 -lt $len) { [void]$sb.Append($Text[$i + 1]); $i += 2; continue }
            } elseif ($c -eq '"') {
                $inString = $false
            }
            $i++
            continue
        }

        # Not in a string.
        if ($c -eq '"') {
            $inString = $true
            [void]$sb.Append($c)
            $i++
            continue
        }
        if ($c -eq '/' -and $i + 1 -lt $len) {
            $next = $Text[$i + 1]
            if ($next -eq '/') {
                # Line comment: skip to end of line (the newline itself is preserved).
                $i += 2
                while ($i -lt $len -and $Text[$i] -ne "`n") { $i++ }
                continue
            }
            if ($next -eq '*') {
                # Block comment: skip to the closing */.
                $i += 2
                while ($i + 1 -lt $len -and -not ($Text[$i] -eq '*' -and $Text[$i + 1] -eq '/')) { $i++ }
                $i += 2
                continue
            }
        }
        if ($c -eq '}' -or $c -eq ']') {
            & $trimTrailingComma
        }
        [void]$sb.Append($c)
        $i++
    }

    return ($sb.ToString() | ConvertFrom-Json)
}