mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
498 lines
20 KiB
PowerShell
498 lines
20 KiB
PowerShell
# Markdown output provider.
|
|
#
|
|
# Consumes the per-object documentation result (see DocumentationOutputJson.ps1
|
|
# header for the field list). Renders HTML-tabled markdown with CSS styling
|
|
# from Internal/Documentation/Assets/DefaultMDStyle.css (or a user-supplied .css file).
|
|
#
|
|
# Differences vs old DocumentationMD.psm1:
|
|
# - Drops V1 NewObjectGroup/NewObjectType hooks; V2 (string-arg) is registered
|
|
# - Drops the extended/custom property selectors that read $global:cb*/$global:txt*
|
|
# UI controls. Always uses DefaultDocumentationProperties or ('Name','Value').
|
|
# Phase 2 [DocumentationContext] will reintroduce these via $ctx.Options.
|
|
# - Drops the unused commented-out block at end of Invoke-MDPostProcessItems
|
|
# - Uses the typed-object API: $PolicyObject.Name + $PolicyObject.PolicyType.Title
|
|
# instead of Get-GraphObjectName $obj $objectType + $objectType.Title
|
|
|
|
function Invoke-InitializeMDOutput {
|
|
Add-DocumentationOutputProvider ([PSCustomObject]@{
|
|
Name = "Markdown"
|
|
Value = "md"
|
|
# Path metadata (see DocumentationOutputHTML.ps1 header comment).
|
|
# UI browse-button wiring lives in
|
|
# UI/<backend>/ClassExtensions/DocumentationOutputMDUIExtension.ps1.
|
|
PrimaryPathOption = "MDDocumentName"
|
|
PathIsFolder = $false
|
|
PreProcess = { Invoke-MDPreProcessItems @args }
|
|
NewObjectGroup = { Invoke-MDNewObjectGroup @args }
|
|
NewObjectType = { Invoke-MDNewObjectType @args }
|
|
Process = { Invoke-MDProcessItem @args }
|
|
PostProcess = { Invoke-MDPostProcessItems @args }
|
|
ProcessAllObjects = { Invoke-MDProcessAllObjects @args }
|
|
})
|
|
}
|
|
|
|
function Invoke-MDProcessAllObjects {
|
|
param($allObjectTypeObjects, $objectType)
|
|
# Reserved for cross-object aggregation (e.g. consolidated ScopeTags table)
|
|
}
|
|
|
|
function Invoke-MDPreProcessItems {
|
|
$script:sectionAnchors = @()
|
|
$script:totAnchors = @()
|
|
$script:mdStrings = $null
|
|
$script:currentItemFileName = $null
|
|
|
|
$defaultCSSFile = [IO.Path]::Combine($script:AppRootFolder, "Internal", "Documentation", "Assets", "DefaultMDStyle.css")
|
|
$mdCssFile = Get-DocumentationOutputOption md "MDCSSFile" $defaultCSSFile
|
|
$includeCss = (Get-DocumentationOutputOption md "MDIncludeCSS" $true) -eq $true
|
|
|
|
if (-not $mdCssFile) {
|
|
Write-Log "CSS file not specified. Using default" 2
|
|
$mdCssFile = $defaultCSSFile
|
|
}
|
|
elseif (-not [IO.File]::Exists($mdCssFile)) {
|
|
Write-Log "CSS file $mdCssFile not found. Using default" 2
|
|
$mdCssFile = $defaultCSSFile
|
|
}
|
|
|
|
if ($includeCss -and [IO.File]::Exists($mdCssFile)) {
|
|
Write-Log "Using CSS file $mdCssFile"
|
|
$script:cssStyle = ([IO.File]::ReadAllText($mdCssFile)) + [Environment]::NewLine
|
|
}
|
|
else {
|
|
Write-Log "CSS file $mdCssFile not found. No styles applied" 2
|
|
$script:cssStyle = ""
|
|
}
|
|
|
|
$fileName = Get-DocumentationOutputOption md "MDDocumentName" ""
|
|
if (-not $fileName) { $fileName = "%MyDocuments%\%Organization%-%Date%.md" }
|
|
$fileName = Expand-FileName $fileName
|
|
|
|
$script:outFile = $fileName
|
|
$script:documentPath = [IO.Path]::GetDirectoryName($fileName)
|
|
$script:outputType = Get-DocumentationOutputOption md "MDDocumentFileType" "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:mdStrings = [System.Text.StringBuilder]::new()
|
|
}
|
|
}
|
|
|
|
function Invoke-MDPostProcessItems {
|
|
$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 md "MDTitleProperty" "Intune documentation"
|
|
if (-not $title) { $title = "Intune documentation" }
|
|
|
|
$mdContent = [System.Text.StringBuilder]::new()
|
|
[void]$mdContent.AppendLine("# $title")
|
|
[void]$mdContent.AppendLine("")
|
|
[void]$mdContent.AppendLine("")
|
|
|
|
if (-not ((Get-DocumentationOption "SkipDocumentInfo" $false) -eq $true)) {
|
|
if ($orgName) { [void]$mdContent.AppendLine("*Organization:* $orgName`n") }
|
|
if ($userName) { [void]$mdContent.AppendLine("*Generated by:* $userName$mail`n") }
|
|
|
|
$skipDate = (Get-DocumentationOutputOption md "MDDocumentSkipDate" $false) -eq $true
|
|
if (-not $skipDate) {
|
|
[void]$mdContent.AppendLine("*Generated:* $((Get-Date).ToShortDateString()) $((Get-Date).ToLongTimeString())`n")
|
|
}
|
|
}
|
|
|
|
if ($script:sectionAnchors.Count -gt 0) {
|
|
[void]$mdContent.AppendLine("")
|
|
[void]$mdContent.AppendLine("## Table of Contents")
|
|
}
|
|
|
|
foreach ($header in $script:sectionAnchors) {
|
|
$indent = [string]::new(" ", (($header.Level - 1) * 2))
|
|
[void]$mdContent.AppendLine("$indent- [$($header.Name)]($($header.FileName)#$($header.Anchor))`n")
|
|
}
|
|
[void]$mdContent.AppendLine("")
|
|
|
|
$mdText = $script:cssStyle + $mdContent.ToString()
|
|
if ($script:outputType -eq "Full" -and $script:mdStrings) {
|
|
$mdText += $script:mdStrings.ToString()
|
|
}
|
|
|
|
Save-DocumentationFile $mdText $script:outFile -OpenFile:((Get-DocumentationOutputOption md "MDOpenFile" $true) -eq $true)
|
|
}
|
|
|
|
function Invoke-MDNewObjectGroup {
|
|
param($groupId)
|
|
Add-MDHeader (Get-DocObjectTypeString $groupId) -Level 1 -UseHTML
|
|
}
|
|
|
|
function Invoke-MDNewObjectType {
|
|
param($objectTypeName)
|
|
Add-MDHeader $objectTypeName -Level 2 -UseHTML
|
|
}
|
|
|
|
# Per-object file name for Object mode. Identity, not title: the policy's own
|
|
# display name plus its type and id, the shape Get-HTMLObjectFileName uses.
|
|
#
|
|
# The display name alone was never unique. Five enrollment defaults - device
|
|
# limit, platform restrictions, enrollment status page, Windows Hello for
|
|
# Business, Windows Restore - are all called "All users and all devices", so
|
|
# they all wrote All_users_and_all_devices.md and the last one won. Deriving the
|
|
# name from the heading instead would not do either: the heading may be a
|
|
# DocumentName override, and a file name has to identify the object, not
|
|
# describe it. The type and id settle it. Spaces become underscores and the
|
|
# suffix carries no brackets, because this name lands inside Markdown link
|
|
# destinations.
|
|
function Get-MDObjectFileName {
|
|
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 { '' }
|
|
|
|
# A policy name can run past 200 characters and the suffix adds up to
|
|
# around 120 more - past what a path may hold, where the name-only file
|
|
# still wrote. The suffix is the identity, so it is the name that gives way.
|
|
$maxNameLength = 80
|
|
if ($objName.Length -gt $maxNameLength) { $objName = $objName.Substring(0, $maxNameLength).TrimEnd() }
|
|
|
|
return (Remove-InvalidFileNameChars "$objName$suffix.md").Replace(' ', '_')
|
|
}
|
|
|
|
function Invoke-MDProcessItem {
|
|
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:mdStrings = [System.Text.StringBuilder]::new()
|
|
$script:currentItemFileName = "./$(Get-MDObjectFileName $PolicyObject)"
|
|
}
|
|
|
|
Add-MDHeader $objName -Level 3 -UseHTML
|
|
[void]$script:mdStrings.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-MDTableItems $PolicyObject $typeTitle $customTable.Values $customTable.Columns $customTable.LanguageId -AddCategories -AddSubcategories
|
|
}
|
|
}
|
|
|
|
if (($documentedObj.$tableType | Measure-Object).Count -gt 0) {
|
|
Add-MDTableItems $PolicyObject $typeTitle $documentedObj.$tableType $properties $lngId -AddCategories -AddSubcategories
|
|
}
|
|
}
|
|
|
|
if (($documentedObj.ComplianceActions | Measure-Object).Count -gt 0) {
|
|
Add-MDTableItems $PolicyObject $typeTitle $documentedObj.ComplianceActions @("Action","Schedule","MessageTemplate","EmailCC") "Category.complianceActionsLabel"
|
|
}
|
|
|
|
if (($documentedObj.ApplicabilityRules | Measure-Object).Count -gt 0) {
|
|
Add-MDTableItems $PolicyObject $typeTitle $documentedObj.ApplicabilityRules @("Rule","Property","Value") "SettingDetails.applicabilityRules"
|
|
}
|
|
|
|
Add-MDObjectScripts $documentedObj
|
|
|
|
# Negative Order already rendered above the settings table.
|
|
foreach ($customTable in ($documentedObj.CustomTables | Where-Object { $_.Order -ge 0 } | Sort-Object -Property Order)) {
|
|
Add-MDTableItems $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-MDTableItems $PolicyObject $typeTitle $documentedObj.Assignments $properties "TableHeaders.assignments" -AddCategories
|
|
}
|
|
}
|
|
catch {
|
|
Write-LogError "Failed to process object $objName" $_.Exception
|
|
}
|
|
|
|
if ($script:outputType -eq "Object") {
|
|
$perObjectText = $script:cssStyle + $script:mdStrings.ToString()
|
|
$fileName = Join-Path $script:documentPath $script:currentItemFileName
|
|
Save-DocumentationFile $perObjectText $fileName
|
|
$script:mdStrings = $null
|
|
}
|
|
}
|
|
|
|
function Add-MDTableItems {
|
|
param(
|
|
$PolicyObject,
|
|
[string]$TypeTitle,
|
|
$Items,
|
|
[string[]]$Properties,
|
|
[string]$LngId,
|
|
[switch]$AddCategories,
|
|
[switch]$AddSubcategories,
|
|
$CaptionOverride
|
|
)
|
|
|
|
$objName = Get-DocCaptionName $PolicyObject
|
|
if ($CaptionOverride) {
|
|
$caption = $CaptionOverride
|
|
}
|
|
elseif ($LngId) {
|
|
$caption = "$((Get-LanguageString $LngId)) - $objName"
|
|
}
|
|
else {
|
|
$caption = "$objName ($TypeTitle)"
|
|
}
|
|
|
|
$tableText = [System.Text.StringBuilder]::new()
|
|
[void]$tableText.AppendLine("<table class='table-settings'>")
|
|
[void]$tableText.AppendLine("<tr class='table-header1'>")
|
|
|
|
$columnCount = 0
|
|
foreach ($prop in $Properties) {
|
|
[void]$tableText.AppendLine("<td>$((Invoke-DocTranslateColumnHeader $prop.Split('.')[-1]))</td>")
|
|
$columnCount++
|
|
}
|
|
[void]$tableText.AppendLine("</tr>")
|
|
|
|
$curCategory = ""
|
|
$curSubCategory = ""
|
|
|
|
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'>$((Set-MDText $itemObj.Category))</td></tr>")
|
|
$curCategory = $itemObj.Category
|
|
$curSubCategory = ""
|
|
}
|
|
|
|
if ($itemObj.SubCategory -and $curSubCategory -ne $itemObj.SubCategory -and $AddSubcategories) {
|
|
[void]$tableText.AppendLine("<tr><td colspan=`"$columnCount`" class='category-level2'>$((Set-MDText $itemObj.SubCategory))</td></tr>")
|
|
$curSubCategory = $itemObj.SubCategory
|
|
}
|
|
|
|
if ($itemObj.PropertyIndex -is [int] -and $itemObj.PropertyIndex -eq 1) {
|
|
$additionalRowClass = "row-new-property"
|
|
}
|
|
|
|
try {
|
|
[void]$tableText.AppendLine("<tr class='$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("<td class='table-header1'>$($colProp.Name)</td>")
|
|
}
|
|
[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 !important;'"
|
|
} catch {}
|
|
}
|
|
[void]$tableText.AppendLine("<td class='property-column$curCol'$style>$((Set-MDText $tmpObj.$propName -CodeBlock))</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>")
|
|
Add-MDText $tableText.ToString()
|
|
Add-MDHeader $caption -Level 6 -ToT -AddParagraph
|
|
}
|
|
|
|
function Add-MDText {
|
|
param([string]$Text, [switch]$AddParagraph)
|
|
[void]$script:mdStrings.AppendLine($Text)
|
|
if ($AddParagraph) { [void]$script:mdStrings.AppendLine("") }
|
|
}
|
|
|
|
function Set-MDText {
|
|
param([string]$Text, [switch]$CodeBlock)
|
|
|
|
if ($null -eq $Text) { return }
|
|
|
|
$txtSummary = ""
|
|
$textOut = ""
|
|
|
|
if ($Text -and $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)
|
|
}
|
|
|
|
if ($CodeBlock) {
|
|
$trim = $Text.Trim()
|
|
if ($trim.StartsWith("<?xml") -or $trim.StartsWith("<xml") -or ($trim.StartsWith("<") -and $trim.EndsWith(">"))) {
|
|
$nl = [Environment]::NewLine
|
|
$textOut = "$nl$nl``````xml$nl$Text$nl```````$nl$nl"
|
|
}
|
|
}
|
|
|
|
if (-not $CodeBlock -or -not $textOut) {
|
|
$t = $Text.Replace("|", '`|')
|
|
$t = $t.Replace("*", '`*')
|
|
$t = $t.Replace("`$", '`$')
|
|
$t = $t.Replace("`r`n", "<br />")
|
|
$textOut = $t.Replace("`n", "<br />")
|
|
}
|
|
|
|
if ($txtSummary) {
|
|
"<details class='description'><summary data-open='Minimize' data-close='$txtSummary...expand'></summary>$textOut</details>"
|
|
}
|
|
else {
|
|
$textOut
|
|
}
|
|
}
|
|
|
|
function Add-MDHeader {
|
|
param(
|
|
[string]$Text,
|
|
[int]$Level = 1,
|
|
[switch]$AddParagraph,
|
|
[switch]$UseHTML,
|
|
[switch]$ToT,
|
|
[switch]$SkipTOC
|
|
)
|
|
|
|
if ($script:mdStrings) {
|
|
$prefix = ""
|
|
if ($ToT) { $prefix = "Table $($script:totAnchors.Count + 1). " }
|
|
|
|
if ($UseHTML) {
|
|
if ($ToT) { $sectionAnchor = "table-$($script:totAnchors.Count + 1)" }
|
|
else { $sectionAnchor = "section-$($script:sectionAnchors.Count + 1)" }
|
|
|
|
[void]$script:mdStrings.AppendLine("<h$Level id=`"$prefix$sectionAnchor`">$Text</h$Level>")
|
|
}
|
|
else {
|
|
$Text = "$prefix$Text"
|
|
$sectionAnchor = $Text.ToLower().Replace(" ", "-").Replace("[","").Replace("]","")
|
|
$mdHeader = [string]::new('#', $Level)
|
|
[void]$script:mdStrings.AppendLine("$mdHeader $Text")
|
|
}
|
|
$fileName = $script:currentItemFileName
|
|
}
|
|
else {
|
|
$sectionAnchor = $null
|
|
$fileName = $null
|
|
}
|
|
|
|
if ($ToT) {
|
|
$script:totAnchors += [PSCustomObject]@{
|
|
Name = $Text; Anchor = $sectionAnchor; FileName = $fileName; Level = $Level
|
|
}
|
|
}
|
|
elseif (-not $SkipTOC) {
|
|
$script:sectionAnchors += [PSCustomObject]@{
|
|
Name = $Text; Anchor = $sectionAnchor; FileName = $fileName; Level = $Level
|
|
}
|
|
}
|
|
|
|
if ($AddParagraph) { [void]$script:mdStrings.AppendLine("`n") }
|
|
}
|
|
|
|
function Add-MDObjectScripts {
|
|
param($documentedObj)
|
|
|
|
foreach ($scriptItem in $documentedObj.Scripts) {
|
|
if (-not $scriptItem.ScriptContent -or -not $scriptItem.Caption) { continue }
|
|
[void]$script:mdStrings.AppendLine("~~~powershell")
|
|
[void]$script:mdStrings.AppendLine($scriptItem.ScriptContent)
|
|
[void]$script:mdStrings.AppendLine("~~~")
|
|
Add-MDHeader $scriptItem.Caption -Level 6 -SkipTOC -AddParagraph
|
|
}
|
|
}
|
|
|
|
Invoke-InitializeMDOutput
|