Private/Test-DhNavMatch.ps1
|
<# Navigation visibility — the single source of truth. A section is visible for a selected path IFF Test-DhNavMatch returns $true. The runtime JS (Get-DhJsNav.ps1, navMatches) mirrors this exactly; a Pester test pins the two together, and Tests/js/nav.test.js drives the JS copy through the same truth table under Node. Why a predicate and not show/hide branches: 1.x patched visibility incrementally across four functions using two competing mechanisms (an inline style.display and the panel-active class), which is what made a block declared with -NavGroup + -NavSubGroup permanently unreachable (readme-dev gotcha #26). State here is DERIVED — recomputed whole from the selected path, idempotent, and independent of the route taken to get there. Depth-agnostic on purpose: nothing below mentions a number of levels. #> function ConvertFrom-DhNavPath { <# .SYNOPSIS Split a '/'-separated nav path into its segments. .DESCRIPTION '//' is the escape for a literal '/' inside a segment. Empty and whitespace-only segments are rejected — they would produce an unaddressable node. Returns an empty array for '' or $null (a page-global item). .EXAMPLE ConvertFrom-DhNavPath 'Storage/Tagging' -> @('Storage','Tagging') ConvertFrom-DhNavPath 'Reports/A//B' -> @('Reports','A/B') #> param([string] $Path) if ([string]::IsNullOrWhiteSpace($Path)) { return @() } # Protect the '//' escape, split on the remaining single '/', then restore. $sentinel = [char]0x1F # unit separator: never legal in a nav label $guarded = $Path -replace '//', $sentinel $segments = $guarded -split '/' $out = foreach ($s in $segments) { $seg = ($s -replace $sentinel, '/').Trim() if ([string]::IsNullOrEmpty($seg)) { throw "Nav path '$Path' contains an empty segment. Use '//' for a literal '/'." } $seg } return @($out) } function Test-DhNavMatch { <# .SYNOPSIS Is a section visible for the selected nav path? .PARAMETER DeclaredPath The section's own path segments (empty array = page-global item). .PARAMETER SelectedPath The path currently selected in the menu. .PARAMETER Scope Exact — visible only on this exact view (default). Subtree — visible for this node and everything beneath it. Global — visible everywhere, regardless of path. .OUTPUTS [bool] #> param( [string[]] $DeclaredPath = @(), [string[]] $SelectedPath = @(), [ValidateSet('Exact','Subtree','Global')] [string] $Scope = 'Exact' ) if ($Scope -eq 'Global') { return $true } $declared = @($DeclaredPath) $selected = @($SelectedPath) # Declared deeper than the selection can never match. if ($declared.Count -gt $selected.Count) { return $false } # Every declared segment must prefix-match the selection. # Case-sensitive by design: nav matching has always been case-sensitive # (readme-dev gotcha #10), and -StrictNav will flag case-only collisions. for ($i = 0; $i -lt $declared.Count; $i++) { if (-not [string]::Equals($declared[$i], $selected[$i], [StringComparison]::Ordinal)) { return $false } } # Prefix matched. Exact requires the depths to agree; Subtree does not. return ($Scope -eq 'Subtree') -or ($declared.Count -eq $selected.Count) } |