Private/Hyde.Validation.ps1

# Initialize a validation report structure for doctor checks.
function newHydeValidationReport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [HydeBuildContext]$Context
    )

    # Doctor collects issues into a single report so callers can inspect the full set at once.
    return [pscustomobject]@{
        Context = $Context
        Healthy = $true
        Issues  = New-Object System.Collections.ArrayList
    }
}

# Add a validation issue to the report with context.
function addHydeValidationIssue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        $Report,

        [Parameter(Mandatory = $true)]
        [string]$Code,

        [Parameter(Mandatory = $true)]
        [string]$Message,

        [string]$Path,

        [ValidateSet('Warning', 'Error')]
        [string]$Severity = 'Error'
    )

    # Every issue carries a stable code so tests and future callers can reason about the result.
    [void]$Report.Issues.Add([pscustomobject]@{
        Severity = $Severity
        Code     = $Code
        Path     = $Path
        Message  = $Message
    })

    $Report.Healthy = $false
}

# Check a layout for missing parents or parse errors.
function testHydeLayoutForIssues {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [HydeDocument]$Document,

        [Parameter(Mandatory = $true)]
        [HydeBuildContext]$Context,

        [Parameter(Mandatory = $true)]
        $Report
    )

    if (-not $Document.FrontMatter.ContainsKey('layout')) {
        return
    }

    $layoutName = [string]$Document.FrontMatter.layout
    if ([string]::IsNullOrWhiteSpace($layoutName) -or $layoutName -in @('none', 'null')) {
        return
    }

    try {
        [void](getHydeLayoutChain -LayoutName $layoutName -Context $Context)
    } catch {
        addHydeValidationIssue -Report $Report -Code 'MissingLayout' -Path $Document.RelativePath -Message $_.Exception.Message
        return
    }
}

# Validate a document for front matter and rendering issues.
function testHydeDocumentForIssues {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [HydeDocument]$Document,

        [Parameter(Mandatory = $true)]
        [HydeBuildContext]$Context,

        [Parameter(Mandatory = $true)]
        $Report
    )

    try {
        initializeHydeDocument -Document $Document -Context $Context
    } catch {
        addHydeValidationIssue -Report $Report -Code 'InvalidFrontMatter' -Path $Document.RelativePath -Message $_.Exception.Message
        return
    }

    testHydeLayoutForIssues -Document $Document -Context $Context -Report $Report
}

# Detect duplicate output paths among documents.
function testHydeOutputConflicts {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [HydeBuildContext]$Context,

        [Parameter(Mandatory = $true)]
        $Report
    )

    # Track generated output paths so doctor can report collisions before build time.
    $seenOutputs = @{}

    foreach ($document in $Context.Documents) {
        if (-not $document.Published -or -not $document.WriteOutput) {
            continue
        }

        $outputPath = $document.OutputRelativePath.Replace('\', '/')
        if ($seenOutputs.ContainsKey($outputPath)) {
            addHydeValidationIssue -Report $Report -Code 'DuplicateOutputPath' -Path $document.RelativePath -Message "Output path '$outputPath' conflicts with '$($seenOutputs[$outputPath])'."
            continue
        }

        $seenOutputs[$outputPath] = $document.RelativePath
    }

    foreach ($staticFile in $Context.StaticFiles) {
        $outputPath = $staticFile.OutputRelativePath.Replace('\', '/')
        if ($seenOutputs.ContainsKey($outputPath)) {
            addHydeValidationIssue -Report $Report -Code 'DuplicateOutputPath' -Path $staticFile.RelativePath -Message "Output path '$outputPath' conflicts with '$($seenOutputs[$outputPath])'."
            continue
        }

        $seenOutputs[$outputPath] = $staticFile.RelativePath
    }
}

# Validate the configured theme directory before content-level checks run.
function testHydeThemeConfiguration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [HydeBuildContext]$Context,

        [Parameter(Mandatory = $true)]
        $Report
    )

    if ([string]::IsNullOrWhiteSpace($Context.ThemePath)) {
        return
    }

    if (-not (Test-Path -LiteralPath $Context.ThemePath -PathType Container)) {
        addHydeValidationIssue -Report $Report -Code 'MissingThemeDirectory' -Path $Context.ThemePath -Message "Configured theme directory '$($Context.ThemePath)' does not exist."
        return
    }

    $layoutsDirectoryName = if ($Context.Settings.ContainsKey('layouts_dir') -and -not [string]::IsNullOrWhiteSpace([string]$Context.Settings.layouts_dir)) {
        [string]$Context.Settings.layouts_dir
    } else {
        '_layouts'
    }

    $includesDirectoryName = if ($Context.Settings.ContainsKey('includes_dir') -and -not [string]::IsNullOrWhiteSpace([string]$Context.Settings.includes_dir)) {
        [string]$Context.Settings.includes_dir
    } else {
        '_includes'
    }

    $themeSupportPaths = @(
        [System.IO.Path]::Combine($Context.ThemePath, $layoutsDirectoryName),
        [System.IO.Path]::Combine($Context.ThemePath, $includesDirectoryName),
        [System.IO.Path]::Combine($Context.ThemePath, 'assets')
    )

    if (-not ($themeSupportPaths | Where-Object { Test-Path -LiteralPath $_ })) {
        addHydeValidationIssue -Report $Report -Code 'InvalidThemeDirectory' -Path $Context.ThemePath -Message "Configured theme directory '$($Context.ThemePath)' does not contain layouts, includes, or assets."
    }

    $themeLayoutsPath = [System.IO.Path]::Combine($Context.ThemePath, $layoutsDirectoryName)
    if (-not (Test-Path -LiteralPath $themeLayoutsPath -PathType Container)) {
        addHydeValidationIssue -Report $Report -Code 'ThemeMissingLayouts' -Path $Context.ThemePath -Severity 'Warning' -Message "Theme directory '$($Context.ThemePath)' does not contain a layouts directory."
    }
}

# Run all validation checks across the site content.
function testHydeSiteContent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [HydeBuildContext]$Context
    )

    $report = newHydeValidationReport -Context $Context
    testHydeThemeConfiguration -Context $Context -Report $report

    foreach ($document in $Context.Documents) {
        Write-Verbose "Validating document '$($document.RelativePath)'."
        testHydeDocumentForIssues -Document $document -Context $Context -Report $report
    }

    testHydeOutputConflicts -Context $Context -Report $report
    return $report
}