# HTML output provider. # # Consumes the per-object documentation result (see DocumentationOutputJson.ps1 # header for the field list). Renders a single HTML document (or one file per # object) with CSS from Internal/Documentation/Assets/DefaultHTMLStyle.css. # # Differences vs old DocumentationHTML.psm1: # - Drops V1 NewObjectGroup/NewObjectType hooks (V2 string-arg is registered) # - Drops extended/custom property selectors that read $global:txt*/$global:cb* # UI controls. Always uses DefaultDocumentationProperties or ('Name','Value'). # Phase 2 [DocumentationContext] will reintroduce these via $ctx.Options. # - Uses the typed-object API: $PolicyObject.Name + $PolicyObject.PolicyType.Title # - Invoke-HTMLProcessAllObjects (ScopeTags consolidated table) is stubbed # because Get-TableObjects doesn't exist yet — wire up in phase 2. function Invoke-InitializeHTMLOutput { Add-DocumentationOutputProvider ([PSCustomObject]@{ Name = "HTML" Value = "html" # Path metadata for the bulk-doc UI: which option key stores the # primary output path, and whether that path is a folder or a file. # File-picker button wiring for the UI is declared separately in # UI//ClassExtensions/DocumentationOutputHTMLUIExtension.ps1 # to keep XAML control names + toolkit-specific filter strings out # of this pure-logic file. PrimaryPathOption = "HTMLDocumentName" PathIsFolder = $false PreProcess = { Invoke-HTMLPreProcessItems @args } NewObjectGroup = { Invoke-HTMLNewObjectGroup @args } NewObjectType = { Invoke-HTMLNewObjectType @args } Process = { Invoke-HTMLProcessItem @args } PostProcess = { Invoke-HTMLPostProcessItems @args } ProcessAllObjects = { Invoke-HTMLProcessAllObjects @args } }) } function Invoke-HTMLPreProcessItems { $script:sectionAnchors = @() $script:totAnchors = @() $script:htmlStrings = $null $script:currentItemFileName = $null $defaultCSSFile = [IO.Path]::Combine($script:AppRootFolder, "Internal", "Documentation", "Assets", "DefaultHTMLStyle.css") $htmlCssFile = Get-DocumentationOutputOption html "HTMLCSSFile" $defaultCSSFile if (-not $htmlCssFile) { Write-Log "CSS file not specified. Using default" 2 $htmlCssFile = $defaultCSSFile } elseif (-not [IO.File]::Exists($htmlCssFile)) { Write-Log "CSS file $htmlCssFile not found. Using default" 2 $htmlCssFile = $defaultCSSFile } if ([IO.File]::Exists($htmlCssFile)) { Write-Log "Using CSS file $htmlCssFile" $script:cssStyle = ([IO.File]::ReadAllText($htmlCssFile)) + [Environment]::NewLine } else { Write-Log "CSS file $htmlCssFile not found. No styles applied" 2 $script:cssStyle = "" } $fileName = Get-DocumentationOutputOption html "HTMLDocumentName" "" if (-not $fileName) { $fileName = "%MyDocuments%\%Organization%-%Date%.html" } $fileName = Expand-FileName $fileName $script:outFile = $fileName $script:documentPath = [IO.Path]::GetDirectoryName($fileName) $script:outputType = Get-DocumentationOutputOption html "HTMLDocumentFileType" "Full" if ($script:outputType -eq "Object") { Write-Log "Document one file for each object + index file" } else { Write-Log "Document one single file for all objects" $script:outputType = "Full" $script:htmlStrings = [System.Text.StringBuilder]::new() } } function Invoke-HTMLPostProcessItems { $userName = $null $mail = "" $me = Get-CurrentUser if ($me) { if ($me.givenName -and $me.surname) { $userName = "$($me.givenName) $($me.surname)" } else { $userName = $me.displayName } if ($me.mail) { $mail = " ($($me.mail))" } } $orgName = Get-CurrentOrganizationName $title = Get-DocumentationOutputOption html "HTMLTitleProperty" "Intune documentation" if (-not $title) { $title = "Intune documentation" } $htmlContent = [System.Text.StringBuilder]::new() [void]$htmlContent.AppendLine("") [void]$htmlContent.AppendLine($script:cssStyle) [void]$htmlContent.AppendLine("

$title

") if (-not ((Get-DocumentationOption "SkipDocumentInfo" $false) -eq $true)) { if ($orgName) { [void]$htmlContent.AppendLine("Organization: $orgName
") } if ($userName) { [void]$htmlContent.AppendLine("Generated by: $userName$mail
") } [void]$htmlContent.AppendLine("Generated: $((Get-Date).ToShortDateString()) $((Get-Date).ToLongTimeString())
") } if ($script:sectionAnchors.Count -gt 0) { [void]$htmlContent.AppendLine("
") [void]$htmlContent.AppendLine("

Table of Contents

") } $tocMaxLevel = 4 foreach ($header in $script:sectionAnchors) { if ($tocMaxLevel -gt 0 -and $header.Level -gt $tocMaxLevel) { continue } [void]$htmlContent.AppendLine("$($header.Name)
") } if ($script:sectionAnchors.Count -gt 0) { [void]$htmlContent.AppendLine("
") } $htmlText = $htmlContent.ToString() if ($script:outputType -eq "Full" -and $script:htmlStrings) { $htmlText += $script:htmlStrings.ToString() } $htmlText += "" Save-DocumentationFile $htmlText $script:outFile -OpenFile:((Get-DocumentationOutputOption html "HTMLOpenFile" $true) -eq $true) } function Invoke-HTMLNewObjectGroup { param($groupId) $script:objectHeaderLevel = 2 Add-HTMLHeader (Get-DocObjectTypeString $groupId) } function Invoke-HTMLNewObjectType { param($objectTypeName) $script:objectHeaderLevel = 3 Add-HTMLHeader $objectTypeName $script:objectHeaderLevel = 4 } function Invoke-HTMLProcessAllObjects { param($documentationInfo) # ScopeTags consolidated table is deferred — Get-TableObjects helper lands # in phase 2 alongside the engine. For now this is a no-op. } function Invoke-HTMLProcessItem { param($PolicyObject, $documentedObj) if (-not $documentedObj -or -not $PolicyObject) { return } # A documented object may ask to be titled by something other than its display # name (see Get-DocumentationDisplayName). Headings and captions follow it; the # file name below deliberately does not. $objName = Get-DocumentationDisplayName $PolicyObject $documentedObj $script:docDisplayName = $objName $typeTitle = $PolicyObject.PolicyType.Title if ($script:outputType -eq "Object") { $script:totAnchors = @() $script:htmlStrings = [System.Text.StringBuilder]::new() $script:currentItemFileName = Get-HTMLObjectFileName $PolicyObject } Add-HTMLHeader $objName [void]$script:htmlStrings.AppendLine("
") try { foreach ($tableType in @("BasicInfo","FilteredSettings")) { if ($tableType -eq "BasicInfo") { $properties = @("Name","Value") $lngId = "SettingDetails.basics" } else { if ($documentedObj.DefaultDocumentationProperties) { $properties = $documentedObj.DefaultDocumentationProperties } else { $properties = @("Name","Value") } $lngId = "TableHeaders.settings" } # Custom tables with a negative Order belong ABOVE the settings # table: the portal shows a MAM app config's "Settings catalog" # blade above its "Settings" blade. if ($tableType -eq "FilteredSettings") { foreach ($customTable in ($documentedObj.CustomTables | Where-Object { $_.Order -lt 0 } | Sort-Object -Property Order)) { Add-HTMLTableItems $PolicyObject $typeTitle $customTable.Values $customTable.Columns $customTable.LanguageId -AddCategories -AddSubcategories } } if (($documentedObj.$tableType | Measure-Object).Count -gt 0) { Add-HTMLTableItems $PolicyObject $typeTitle $documentedObj.$tableType $properties $lngId -AddCategories -AddSubcategories } } if (($documentedObj.ComplianceActions | Measure-Object).Count -gt 0) { Add-HTMLTableItems $PolicyObject $typeTitle $documentedObj.ComplianceActions @("Action","Schedule","MessageTemplate","EmailCC") "Category.complianceActionsLabel" } if (($documentedObj.ApplicabilityRules | Measure-Object).Count -gt 0) { Add-HTMLTableItems $PolicyObject $typeTitle $documentedObj.ApplicabilityRules @("Rule","Property","Value") "SettingDetails.applicabilityRules" } Add-HTMLObjectScripts $documentedObj # Negative Order already rendered above the settings table. foreach ($customTable in ($documentedObj.CustomTables | Where-Object { $_.Order -ge 0 } | Sort-Object -Property Order)) { Add-HTMLTableItems $PolicyObject $typeTitle $customTable.Values $customTable.Columns $customTable.LanguageId -AddCategories -AddSubcategories } if (($documentedObj.Assignments | Measure-Object).Count -gt 0) { if ($documentedObj.Assignments[0].RawIntent) { $properties = @("GroupMode","Group","Filter","FilterMode") $settingsObj = $documentedObj.Assignments | Where-Object { $_.Settings -ne $null } | Select-Object -First 1 if ($settingsObj) { foreach ($objProp in $settingsObj.Settings.Keys) { if ($objProp -in $properties) { continue } if ($objProp -in @("Category","RawIntent")) { continue } $properties += "Settings.$objProp" } } } else { $hasFilter = $false foreach ($a in $documentedObj.Assignments) { if ($a.PSObject.Properties.Name -contains "FilterMode") { $hasFilter = $true; break } } $properties = @("Group") if ($hasFilter) { $properties += @("Filter","FilterMode") } } Add-HTMLTableItems $PolicyObject $typeTitle $documentedObj.Assignments $properties "TableHeaders.assignments" -AddCategories } } catch { Write-LogError "Failed to process object $objName" $_.Exception } if ($script:outputType -eq "Object") { $perObject = [System.Text.StringBuilder]::new() [void]$perObject.AppendLine("") [void]$perObject.AppendLine($script:cssStyle) $htmlText = $perObject.ToString() + $script:htmlStrings.ToString() + "" $fileName = Join-Path $script:documentPath $script:currentItemFileName Save-DocumentationFile $htmlText $fileName $script:htmlStrings = $null } } function Get-HTMLObjectFileName { param($PolicyObject) $objName = if ($PolicyObject.Name) { [string]$PolicyObject.Name } else { 'Unnamed policy' } $id = if ($PolicyObject.Id) { [string]$PolicyObject.Id } else { $null } $typeId = if ($PolicyObject.PolicyType -and $PolicyObject.PolicyType.Id) { [string]$PolicyObject.PolicyType.Id } else { $null } $suffix = if ($typeId -and $id) { " [$typeId-$id]" } elseif ($id) { " [$id]" } else { '' } return Remove-InvalidFileNameChars "$objName$suffix.html" } function Add-HTMLHeader { param( [string]$HeaderText, [int]$Level = $script:objectHeaderLevel, [switch]$ToT, [switch]$SkipTOC ) if ($script:htmlStrings) { $prefix = "" if ($ToT) { $prefix = "Table $($script:totAnchors.Count + 1). " $sectionAnchor = "table-$($script:totAnchors.Count + 1)" } else { $sectionAnchor = "section-$($script:sectionAnchors.Count + 1)" } [void]$script:htmlStrings.AppendLine("$HeaderText") $fileName = $script:currentItemFileName } else { $sectionAnchor = $null $fileName = $null } if ($ToT) { $script:totAnchors += [PSCustomObject]@{ Name = $HeaderText; Anchor = $sectionAnchor; Level = $Level; FileName = $fileName } } elseif (-not $SkipTOC) { $script:sectionAnchors += [PSCustomObject]@{ Name = $HeaderText; Anchor = $sectionAnchor; Level = $Level; FileName = $fileName } } } function Add-HTMLTableItems { param( $PolicyObject, [string]$TypeTitle, $Items, [string[]]$Properties, [string]$LngId, [switch]$AddCategories, [switch]$AddSubcategories, $CaptionOverride ) if ($CaptionOverride) { $caption = $CaptionOverride } elseif ($LngId -and $PolicyObject) { $caption = "$((Get-LanguageString $LngId)) - $(Get-DocCaptionName $PolicyObject)" } elseif ($PolicyObject) { $caption = "$(Get-DocCaptionName $PolicyObject) ($TypeTitle)" } else { $caption = $TypeTitle } $tableText = [System.Text.StringBuilder]::new() [void]$tableText.AppendLine("") [void]$tableText.AppendLine("") $columnCount = 0 foreach ($prop in $Properties) { [void]$tableText.AppendLine("") $columnCount++ } [void]$tableText.AppendLine("") $curCategory = "" $curSubCategory = "" $row = 1 foreach ($itemObj in $Items) { $additionalRowClass = "" if ($itemObj.Category -and $curCategory -ne $itemObj.Category -and $AddCategories) { [void]$tableText.AppendLine("") $curCategory = $itemObj.Category $curSubCategory = "" $row = 1 } if ($itemObj.SubCategory -and $curSubCategory -ne $itemObj.SubCategory -and $AddSubcategories) { [void]$tableText.AppendLine("") $curSubCategory = $itemObj.SubCategory $row = 1 } if ($itemObj.PropertyIndex -is [int] -and $itemObj.PropertyIndex -eq 1) { $additionalRowClass = "row-new-property" } try { $rowClass = if (($row % 2) -eq 1) { "row-odd" } else { "row-even" } $row++ [void]$tableText.AppendLine("") $curCol = 1 foreach ($prop in $Properties) { try { $propArr = $prop.Split('.') $tmpObj = $itemObj $propName = $propArr[-1] for ($x = 0; $x -lt ($propArr.Count - 1); $x++) { $tmpObj = $tmpObj."$($propArr[$x])" } if ($propName -eq "Value" -and ($itemObj.FullValueTable | Measure-Object).Count -gt 0) { [void]$tableText.AppendLine("") } else { $style = "" if ($curCol -eq 1 -and $itemObj.Level) { try { $level = [int]$itemObj.Level $style = " style='padding-left:$((5 + ($level * 5)))px;'" } catch {} } [void]$tableText.AppendLine("") } } catch { Write-LogError "Failed to add property value for $prop" $_.Exception } $curCol++ } } catch { Write-Log "Failed to process property" 2 } finally { [void]$tableText.AppendLine("") } } [void]$tableText.AppendLine("
$((Invoke-DocTranslateColumnHeader $prop.Split('.')[-1]))
$($itemObj.Category)
$($itemObj.SubCategory)
") foreach ($colProp in $itemObj.FullValueTable[0].PSObject.Properties) { [void]$tableText.AppendLine("") } [void]$tableText.AppendLine("") foreach ($rowVal in $itemObj.FullValueTable) { [void]$tableText.AppendLine("") foreach ($colProp in $itemObj.FullValueTable[0].PSObject.Properties) { [void]$tableText.AppendLine("") } [void]$tableText.AppendLine("") } [void]$tableText.AppendLine("
$($colProp.Name)
$($rowVal."$($colProp.Name)")
$((Set-HTMLText $tmpObj.$propName))
") [void]$script:htmlStrings.Append($tableText.ToString()) Add-HTMLHeader $caption -Level 6 } function Set-HTMLText { param([string]$Text, [switch]$NoCodeBlock) if (-not $Text) { return } $txtSummary = "" if ($Text.Length -gt 250) { $summaryMax = 40 $idx = $Text.IndexOfAny(@("`r","`n")) if ($idx -gt 10 -and $idx -lt 50) { $summaryMax = $idx } $txtSummary = $Text.Substring(0, $summaryMax) } $code = $false if (-not $NoCodeBlock) { $trim = $Text.Trim() if ($trim.StartsWith("<") -and $trim.EndsWith(">")) { $code = $true $Text = "
$($Text.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"'))
" if ($txtSummary) { $txtSummary = $txtSummary.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') } } } if (-not $code) { $Text = $Text.Replace("`r`n", "
").Replace("`n", "
").Replace('&', '&') } if ($txtSummary) { "
$Text
" } else { $Text } } function Add-HTMLObjectScripts { param($documentedObj) foreach ($scriptItem in $documentedObj.Scripts) { if (-not $scriptItem.ScriptContent -or -not $scriptItem.Caption) { continue } [void]$script:htmlStrings.AppendLine("
")
        [void]$script:htmlStrings.AppendLine($scriptItem.ScriptContent)
        [void]$script:htmlStrings.AppendLine("
") Add-HTMLHeader $scriptItem.Caption -Level 6 -SkipTOC } } Invoke-InitializeHTMLOutput