mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
477 lines
19 KiB
PowerShell
477 lines
19 KiB
PowerShell
# 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/<backend>/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("<HTML>")
|
|
[void]$htmlContent.AppendLine($script:cssStyle)
|
|
[void]$htmlContent.AppendLine("<H1 class='header-level1'>$title</H1>")
|
|
|
|
if (-not ((Get-DocumentationOption "SkipDocumentInfo" $false) -eq $true)) {
|
|
if ($orgName) { [void]$htmlContent.AppendLine("Organization: $orgName<br />") }
|
|
if ($userName) { [void]$htmlContent.AppendLine("Generated by: $userName$mail<br />") }
|
|
[void]$htmlContent.AppendLine("Generated: $((Get-Date).ToShortDateString()) $((Get-Date).ToLongTimeString())<br />")
|
|
}
|
|
|
|
if ($script:sectionAnchors.Count -gt 0) {
|
|
[void]$htmlContent.AppendLine("<br />")
|
|
[void]$htmlContent.AppendLine("<H2 class='header-level2'>Table of Contents</H2>")
|
|
}
|
|
|
|
$tocMaxLevel = 4
|
|
foreach ($header in $script:sectionAnchors) {
|
|
if ($tocMaxLevel -gt 0 -and $header.Level -gt $tocMaxLevel) { continue }
|
|
[void]$htmlContent.AppendLine("<a href='$($header.FileName)#$($header.Anchor)' class='anchor-style anchor-level$($header.Level)'>$($header.Name)</a><br />")
|
|
}
|
|
if ($script:sectionAnchors.Count -gt 0) { [void]$htmlContent.AppendLine("<br />") }
|
|
|
|
$htmlText = $htmlContent.ToString()
|
|
if ($script:outputType -eq "Full" -and $script:htmlStrings) {
|
|
$htmlText += $script:htmlStrings.ToString()
|
|
}
|
|
$htmlText += "</HTML>"
|
|
|
|
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("<br />")
|
|
|
|
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("<HTML>")
|
|
[void]$perObject.AppendLine($script:cssStyle)
|
|
$htmlText = $perObject.ToString() + $script:htmlStrings.ToString() + "</HTML>"
|
|
$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("<H$Level id=`"$prefix$sectionAnchor`" class='header-level$Level'>$HeaderText</H$Level>")
|
|
$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("<table class='table-settings'>")
|
|
[void]$tableText.AppendLine("<tr>")
|
|
|
|
$columnCount = 0
|
|
foreach ($prop in $Properties) {
|
|
[void]$tableText.AppendLine("<th>$((Invoke-DocTranslateColumnHeader $prop.Split('.')[-1]))</th>")
|
|
$columnCount++
|
|
}
|
|
[void]$tableText.AppendLine("</tr>")
|
|
|
|
$curCategory = ""
|
|
$curSubCategory = ""
|
|
$row = 1
|
|
|
|
foreach ($itemObj in $Items) {
|
|
$additionalRowClass = ""
|
|
|
|
if ($itemObj.Category -and $curCategory -ne $itemObj.Category -and $AddCategories) {
|
|
[void]$tableText.AppendLine("<tr><td colspan=`"$columnCount`" class='category-level1'>$($itemObj.Category)</td></tr>")
|
|
$curCategory = $itemObj.Category
|
|
$curSubCategory = ""
|
|
$row = 1
|
|
}
|
|
|
|
if ($itemObj.SubCategory -and $curSubCategory -ne $itemObj.SubCategory -and $AddSubcategories) {
|
|
[void]$tableText.AppendLine("<tr><td colspan=`"$columnCount`" class='category-level2'>$($itemObj.SubCategory)</td></tr>")
|
|
$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("<tr class='$rowClass $additionalRowClass'>")
|
|
|
|
$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("<td><table class='table-value'><tr>")
|
|
foreach ($colProp in $itemObj.FullValueTable[0].PSObject.Properties) {
|
|
[void]$tableText.AppendLine("<th>$($colProp.Name)</th>")
|
|
}
|
|
[void]$tableText.AppendLine("</tr>")
|
|
foreach ($rowVal in $itemObj.FullValueTable) {
|
|
[void]$tableText.AppendLine("<tr>")
|
|
foreach ($colProp in $itemObj.FullValueTable[0].PSObject.Properties) {
|
|
[void]$tableText.AppendLine("<td>$($rowVal."$($colProp.Name)")</td>")
|
|
}
|
|
[void]$tableText.AppendLine("</tr>")
|
|
}
|
|
[void]$tableText.AppendLine("</table></td>")
|
|
}
|
|
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("<td class='property-column$curCol'$style>$((Set-HTMLText $tmpObj.$propName))</td>")
|
|
}
|
|
}
|
|
catch {
|
|
Write-LogError "Failed to add property value for $prop" $_.Exception
|
|
}
|
|
$curCol++
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log "Failed to process property" 2
|
|
}
|
|
finally {
|
|
[void]$tableText.AppendLine("</tr>")
|
|
}
|
|
}
|
|
|
|
[void]$tableText.AppendLine("</table>")
|
|
[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 = "<pre class='code'>$($Text.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"'))</pre>"
|
|
if ($txtSummary) {
|
|
$txtSummary = $txtSummary.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $code) {
|
|
$Text = $Text.Replace("`r`n", "<br />").Replace("`n", "<br />").Replace('&', '&')
|
|
}
|
|
|
|
if ($txtSummary) {
|
|
"<details class='description'><summary data-open='Minimize' data-close='$txtSummary...expand'></summary>$Text</details>"
|
|
}
|
|
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("<pre class='code'>")
|
|
[void]$script:htmlStrings.AppendLine($scriptItem.ScriptContent)
|
|
[void]$script:htmlStrings.AppendLine("</pre>")
|
|
Add-HTMLHeader $scriptItem.Caption -Level 6 -SkipTOC
|
|
}
|
|
}
|
|
|
|
Invoke-InitializeHTMLOutput
|