# 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//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("") [void]$tableText.AppendLine("") $columnCount = 0 foreach ($prop in $Properties) { [void]$tableText.AppendLine("") $columnCount++ } [void]$tableText.AppendLine("") $curCategory = "" $curSubCategory = "" foreach ($itemObj in $Items) { $additionalRowClass = "" if ($itemObj.Category -and $curCategory -ne $itemObj.Category -and $AddCategories) { [void]$tableText.AppendLine("") $curCategory = $itemObj.Category $curSubCategory = "" } if ($itemObj.SubCategory -and $curSubCategory -ne $itemObj.SubCategory -and $AddSubcategories) { [void]$tableText.AppendLine("") $curSubCategory = $itemObj.SubCategory } if ($itemObj.PropertyIndex -is [int] -and $itemObj.PropertyIndex -eq 1) { $additionalRowClass = "row-new-property" } try { [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 !important;'" } 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]))
$((Set-MDText $itemObj.Category))
$((Set-MDText $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-MDText $tmpObj.$propName -CodeBlock))
") 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(""))) { $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", "
") $textOut = $t.Replace("`n", "
") } if ($txtSummary) { "
$textOut
" } 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("$Text") } 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