diff --git a/PSModule/M365Documentation/Functions/Write-M365DocMD.ps1 b/PSModule/M365Documentation/Functions/Write-M365DocMD.ps1 new file mode 100644 index 0000000..b0996a2 --- /dev/null +++ b/PSModule/M365Documentation/Functions/Write-M365DocMD.ps1 @@ -0,0 +1,75 @@ +Function Write-M365DocMD(){ + <# + .SYNOPSIS + Outputs the documentation as Markdown file. + .DESCRIPTION + This function takes the passed data and is outputing it to the Markdown file. + + .PARAMETER FullDocumentationPath + Path including filename where the documentation should be created. The filename has to end with .md. + + Note: + If there is already a file present, the documentation will be added at the end of the existing document. + + .PARAMETER Data + M365 documentation object which shoult be written to MD. + + .EXAMPLE + Write-M365DocMD -FullDocumentationPath $FullDocumentationPath -Data $Data + + .NOTES + NAME: Thomas Kurth / 21.7.2023 + #> + param( + [ValidateScript({ + if($_ -notmatch "(\.md)"){ + throw "The file specified in the path argument must be of type md." + } + return $true + })] + [System.IO.FileInfo]$FullDocumentationPath = ".\$($Data.CreationDate.ToString("yyyyMMddHHmm"))-WPNinjas-Doc.md", + [Parameter(ValueFromPipeline,Mandatory)] + [Doc]$Data + ) + Begin { + + } + Process { + #region CopyTemplate + Write-Progress -Id 10 -Activity "Create Markdown File" -Status "Prepare Markdown template" -PercentComplete 0 + + "# M365 Documentation" | Out-File -LiteralPath $FullDocumentationPath -Append + "" | Out-File -LiteralPath $FullDocumentationPath -Append + "Date: $(Get-Date -Format "HH:mm dd.MM.yyyy")" | Out-File -LiteralPath $FullDocumentationPath -Append + "Components: $($Data.Components -join ", ")" | Out-File -LiteralPath $FullDocumentationPath -Append + "Tenant: $($Data.Organization)" | Out-File -LiteralPath $FullDocumentationPath -Append + "" | Out-File -LiteralPath $FullDocumentationPath -Append + "## Contents" | Out-File -LiteralPath $FullDocumentationPath -Append + "" | Out-File -LiteralPath $FullDocumentationPath -Append + "_TOC_" | Out-File -LiteralPath $FullDocumentationPath -Append + + Write-Progress -Id 10 -Activity "Create Markdown File" -Status "Prepared Markdown template" -PercentComplete 10 + #endregion + + # Prepare TOC + $script:toc = "" + + $progress = 0 + foreach($Section in $Data.SubSections){ + $progress++ + Write-Progress -Id 10 -Activity "Create Markdown File" -Status "Write Section" -CurrentOperation $Section.Title -PercentComplete (($progress / $Data.SubSections.count) * 100) + Write-DocumentationMDSection -FullDocumentationPath $FullDocumentationPath -Data $Section -Level 1 + } + + # Write TOC + Write-Progress -Id 10 -Activity "Create Markdown File" -Status "Write TOC" -PercentComplete 99 + $content = Get-Content -LiteralPath $FullDocumentationPath -Raw + $content = $content.Replace("_TOC_",$script:toc) + $content | Out-File -LiteralPath $FullDocumentationPath -Force + + Write-Progress -Id 10 -Activity "Create Markdown File" -Status "Finished creation" -Completed + } + End { + + } +} \ No newline at end of file diff --git a/PSModule/M365Documentation/Internal/Helper/Format-MarkdownTableListStyle.ps1 b/PSModule/M365Documentation/Internal/Helper/Format-MarkdownTableListStyle.ps1 new file mode 100644 index 0000000..a1f04fb --- /dev/null +++ b/PSModule/M365Documentation/Internal/Helper/Format-MarkdownTableListStyle.ps1 @@ -0,0 +1,156 @@ +<# +.SYNOPSIS +Formats the output as a Format-List style markdown table. + +.DESCRIPTION +The Format-MarkdownTableListStyle cmdlet formats the output of a command as a Format-List style markdown table which each property is displayed on a separate col. + +Markdown text will be copied to the clipboard. + +.PARAMETER InputObject +Specifies the objects to be formatted. Enter a variable that contains the objects or type a command or expression that gets the objects. + +.PARAMETER HideStandardOutput +Indicates that the cmdlet hides the standard Format-List style output. + +.PARAMETER ShowMarkdown +Indicates that the cmdlet outputs the markdown text to the console. + +.PARAMETER DoNotCopyToClipboard +Indicates the the cmdlet does not copy the markdown text to the clipboard. + +.PARAMETER Property +Specifies the object properties that appear in the display and the order in which they appear. Wildcards are permitted. + +If you omit this parameter, the properties that appear in the display depend on the object being displayed. The parameter name "Property" is optional. + +.EXAMPLE +Get-Process notepad | Format-MarkdownTableListStyle + +.EXAMPLE +Get-Process notepad | fml Name,Path + +.NOTES +You can also refer to Format-MarkdownTableListStyle by its built-in alias, FML. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# https://github.com/microsoft/FormatPowerShellToMarkdownTable + +#> +function Format-MarkdownTableListStyle { + [CmdletBinding()] + [Alias("fml")] + + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object] + $InputObject, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [switch] + $HideStandardOutput, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [switch] + $ShowMarkdown, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [switch] + $DoNotCopyToClipboard, + + [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $false)] + [string[]] + $Property = @() + ) + + Begin { + if ($null -ne $InputObject -and $InputObject.GetType().BaseType -eq [System.Array]) { + Write-Error "InputObject must not be System.Array. Don't use InputObject, but use the pipeline to pass the array object." + $NeedToReturn = $true + return + } + + $LastCommandLine = (Get-PSCallStack)[1].Position.Text + + $Result = "" + + $TempOutputList = New-Object System.Collections.Generic.List[object] + } + + Process { + if ($NeedToReturn) { return } + + $CurrentObject = $null + + if (($Property.Length -eq 0) -or ($Property.Length -eq 1 -and $Property[0] -eq "")) { + $Property = @("*") + } + + if ($_ -eq $null) { + $CurrentObject = $InputObject + } + else { + $CurrentObject = $_ + } + + if ($CurrentObject.GetType().Name.ToLower() -eq "string") { + # CurrentObject is a simple String object + # Display like a FT style + + $Output = "" + + if ($Result -eq "") { + $Output += "||`r`n" + $Output += "|:--|`r`n" + } + + $Output += "|$(Invoke-EscapeMarkdown($CurrentObject))|`r`n" + + $Result += $Output + + $TempOutputList.Add($CurrentObject) + } + else { + $CurrentObject = $CurrentObject | Select-Object -Property $Property -ErrorAction SilentlyContinue + $Props = $CurrentObject | Get-Member -Name $Property -MemberType Property, NoteProperty + + $Output = "|Property|Value|`r`n" + $Output += "|:--|:--|`r`n" + + $TempOutput = New-Object PSCustomObject + + foreach ($Prop in $Props) { + $EscapedPropName = Invoke-EscapeMarkdown($Prop.Name) + $EscapedPropValue = Invoke-EscapeMarkdown($CurrentObject.($($Prop.Name))) + $Output += "|$EscapedPropName|$EscapedPropValue`r`n" + $TempOutput | Add-Member -MemberType NoteProperty $Prop.Name -Value $CurrentObject.($($Prop.Name)) -Force + } + + $Output += "`r`n" + + $Result += $Output + + $TempOutputList.Add($TempOutput) + } + } + + End { + if ($NeedToReturn) { return } + + $ResultForConsole = $Result + $Result = "**" + (Invoke-EscapeMarkdown($LastCommandLine)) + "**`r`n`r`n" + $Result + + if ($HideStandardOutput.IsPresent -eq $false) { + $TempOutputList | Format-List * + } + + if ($ShowMarkdown.IsPresent) { + Write-Output $ResultForConsole + } + + if ($DoNotCopyToClipboard.IsPresent -eq $false) { + Set-Clipboard $Result + Write-Warning "Markdown text has been copied to the clipboard." + } + } +} \ No newline at end of file diff --git a/PSModule/M365Documentation/Internal/Helper/Format-MarkdownTableTableStyle.ps1 b/PSModule/M365Documentation/Internal/Helper/Format-MarkdownTableTableStyle.ps1 new file mode 100644 index 0000000..09a80db --- /dev/null +++ b/PSModule/M365Documentation/Internal/Helper/Format-MarkdownTableTableStyle.ps1 @@ -0,0 +1,268 @@ +<# +.SYNOPSIS +Formats the output as a Format-Table style markdown table. + +.DESCRIPTION +The Format-MarkdownTableTableStyle cmdlet formats the output of a command as a Format-Table style markdown table which each property is displayed on a separate row. + +Markdown text will be copied to the clipboard. + +.PARAMETER InputObject +Specifies the objects to be formatted. Enter a variable that contains the objects or type a command or expression that gets the objects. + +.PARAMETER HideStandardOutput +Indicates that the cmdlet hides the standard Format-Table style output. + +.PARAMETER ShowMarkdown +Indicates that the cmdlet outputs the markdown text to the console. + +.PARAMETER DoNotCopyToClipboard +Indicates the the cmdlet does not copy the markdown text to the clipboard. + +.PARAMETER Property +Specifies the object properties that appear in the display and the order in which they appear. Wildcards are permitted. + +If you omit this parameter, the properties that appear in the display depend on the object being displayed. The parameter name "Property" is optional. + +.EXAMPLE +Get-Process notepad | Format-MarkdownTableTableStyle + +.EXAMPLE +Get-Process notepad | fmt Name,Path + +.NOTES +You can also refer to Format-MarkdownTableTableStyle by its built-in alias, FMT. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# https://github.com/microsoft/FormatPowerShellToMarkdownTable + +#> +function Format-MarkdownTableTableStyle { + [CmdletBinding()] + [Alias("fmt")] + + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object] + $InputObject, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [switch] + $HideStandardOutput, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [switch] + $ShowMarkdown, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [switch] + $DoNotCopyToClipboard, + + [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $false)] + [string[]] + $Property = @() + ) + + Begin { + ## Internal Function + + function UseAllProperty([object]$InputObject) { + try { + if ($null -eq $InputObject) { + return $true + } + + $DataType = ($InputObject | Get-Member)[0].TypeName + + if ($DataType.StartsWith("Selected.")) { + return $true + } + elseif ($DataType.StartsWith("Deserialized.")) { + $DataType = $DataType.Remove(0, 13) + } + + $FormatData = Get-FormatData -TypeName $DataType -ErrorAction SilentlyContinue + + if ($null -eq $FormatData) { + return $true + } + + return $false + } + catch { + return $true + } + } + + if ($null -ne $InputObject -and $InputObject.GetType().BaseType -eq [System.Array]) { + Write-Error "InputObject must not be System.Array. Don't use InputObject, but use the pipeline to pass the array object." + $NeedToReturn = $true + return + } + + $LastCommandLine = (Get-PSCallStack)[1].Position.Text + + $Result = "" + + $HeadersForFormatTableStyle = New-Object System.Collections.Generic.List[string] + $ContentsForFormatTableStyle = New-Object System.Collections.Generic.List[object] + + $TempOutputList = New-Object System.Collections.Generic.List[object] + } + + Process { + if ($NeedToReturn) { return } + + $CurrentObject = $null + + if ($_ -eq $null) { + $CurrentObject = $InputObject + } + else { + $CurrentObject = $_ + } + + if ($CurrentObject.GetType().Name.ToLower() -eq "string") { + # CurrentObject is a simple String object + $Props = @("") + } + elseif (($Property.Length -eq 0) -or ($Property.Length -eq 1 -and $Property[0] -eq "")) { + if (UseAllProperty($CurrentObject)) { + $Property = @("*") + $CurrentObject = $CurrentObject | Select-Object -Property $Property + $Props = $CurrentObject | Get-Member -Name $Property -MemberType Property, NoteProperty + } + else { + $DataType = ($CurrentObject | Get-Member)[0].TypeName + + if ($DataType.StartsWith("Deserialized.")) { + $DataType = $DataType.Remove(0, 13) + } + + $FormatData = Get-FormatData -TypeName $DataType -ErrorAction SilentlyContinue + + $TempPSObject = New-Object PSCustomObject + + $TempHeaderList = New-Object System.Collections.Generic.List[string] + + if ($FormatData.FormatViewDefinition.Control.Headers) + { + for ($i = 0; $i -lt $FormatData.FormatViewDefinition.Control.Headers.Count; $i++) { + $HeaderName = $FormatData.FormatViewDefinition.Control.Headers[$i].Label + + if ($null -eq $HeaderName -or $HeaderName -eq "") { + $HeaderName = $FormatData.FormatViewDefinition.Control.Rows.Columns[$i].DisplayEntry.Value + } + + $TempSelectedObject = $null + + if ($FormatData.FormatViewDefinition.Control.Rows.Columns[$i].DisplayEntry.ValueType -eq "ScriptBlock") { + $TempSelectedObject = $CurrentObject | Select-Object @{ + n = $HeaderName; + e = ([scriptblock]::Create($FormatData.FormatViewDefinition.Control.Rows.Columns[$i].DisplayEntry.Value)) + } + } + else { + $PropertyName = $FormatData.FormatViewDefinition.Control.Rows.Columns[$i].DisplayEntry.Value + + $TempSelectedObject = $CurrentObject | Select-Object @{ + n = $HeaderName; + e = {$_.$($PropertyName)} + } + } + + $Value = $TempSelectedObject.$($HeaderName) + $TempPSObject | Add-Member -MemberType NoteProperty $HeaderName -Value $Value + $TempHeaderList.Add($HeaderName) + } + } + else { + for ($i = 0; $i -lt $FormatData.FormatViewDefinition.Control.Entries.Items.Count; $i++) { + $HeaderName = $FormatData.FormatViewDefinition.Control.Entries.Items[$i].DisplayEntry.Value + + $TempSelectedObject = $null + + $TempSelectedObject = $CurrentObject | Select-Object @{ + n = $HeaderName; + e = {$_.$($HeaderName)} + } + + $Value = $TempSelectedObject.$($HeaderName) + $TempPSObject | Add-Member -MemberType NoteProperty $HeaderName -Value $Value + $TempHeaderList.Add($HeaderName) + } + } + + $CurrentObject = $TempPSObject | Select-Object -Property $TempHeaderList + $Props = $CurrentObject | Get-Member -Name $TempHeaderList -MemberType Property, NoteProperty + } + } + else { + $CurrentObject = $CurrentObject | Select-Object -Property $Property -ErrorAction SilentlyContinue + $Props = $CurrentObject | Get-Member -Name $Property -MemberType Property, NoteProperty + } + + foreach ($Prop in $Props) { + if ($HeadersForFormatTableStyle.Contains($Prop.Name) -eq $false) { + $HeadersForFormatTableStyle.Add($Prop.Name) + } + } + + $ContentsForFormatTableStyle.Add($CurrentObject) + } + + End { + if ($NeedToReturn) { return } + + $HeaderRow = "|" + $SeparatorRow = "|" + $ContentRow = "" + + foreach ($Prop in $HeadersForFormatTableStyle) { + $HeaderRow += "$(Invoke-EscapeMarkdown($Prop))|" + $SeparatorRow += ":--|" + + } + + foreach ($Content in $ContentsForFormatTableStyle) { + $TempOutput = New-Object PSCustomObject + $ContentRow += "|" + + if ($HeadersForFormatTableStyle.Count -eq "1" -and $HeadersForFormatTableStyle[0] -eq "") { + # Content is an array of simple data type, like String. + $ContentRow += "$(Invoke-EscapeMarkdown($Content))|" + $TempOutput = $null + $TempOutput = $Content + } + else { + foreach ($Prop in $HeadersForFormatTableStyle) { + $ContentRow += "$(Invoke-EscapeMarkdown($Content.($($Prop))))|" + + $TempOutput | Add-Member -MemberType NoteProperty $Prop -Value $Content.($($Prop)) + } + } + + $ContentRow += "`r`n" + + $TempOutputList.Add($TempOutput) + } + + $Result = $HeaderRow + "`r`n" + $SeparatorRow + "`r`n" + $ContentRow + + $ResultForConsole = $Result + $Result = "**" + (Invoke-EscapeMarkdown($LastCommandLine)) + "**`r`n`r`n" + $Result + + if ($HideStandardOutput.IsPresent -eq $false) { + $TempOutputList | Format-Table * -AutoSize + } + + if ($ShowMarkdown.IsPresent) { + Write-Output $ResultForConsole + } + + if ($DoNotCopyToClipboard.IsPresent -eq $false) { + Set-Clipboard $Result + Write-Warning "Markdown text has been copied to the clipboard." + } + } +} \ No newline at end of file diff --git a/PSModule/M365Documentation/Internal/Helper/Invoke-EscapeMarkdown.ps1 b/PSModule/M365Documentation/Internal/Helper/Invoke-EscapeMarkdown.ps1 new file mode 100644 index 0000000..904709b --- /dev/null +++ b/PSModule/M365Documentation/Internal/Helper/Invoke-EscapeMarkdown.ps1 @@ -0,0 +1,21 @@ +function Invoke-EscapeMarkdown([object]$InputObject) { + $Temp = "" + + if ($null -eq $InputObject) { + return "" + } + elseif ($InputObject.GetType().BaseType -eq [System.Array]) { + $Temp = "{" + [System.String]::Join(", ", $InputObject) + "}" + } + elseif ($InputObject.GetType() -eq [System.Collections.ArrayList] -or $InputObject.GetType().ToString().StartsWith("System.Collections.Generic.List")) { + $Temp = "{" + [System.String]::Join(", ", $InputObject.ToArray()) + "}" + } + elseif (Get-Member -InputObject $InputObject -Name ToString -MemberType Method) { + $Temp = $InputObject.ToString() + } + else { + $Temp = "" + } + + return $Temp.Replace("\", "\\").Replace("*", "\*").Replace("_", "\_").Replace("``", "\``").Replace("$", "\$").Replace("|", "\|").Replace("<", "\<").Replace(">", "\>").Replace([System.Environment]::NewLine, "
") +} \ No newline at end of file diff --git a/PSModule/M365Documentation/Internal/Output/Write-DocumentationMDSection.ps1 b/PSModule/M365Documentation/Internal/Output/Write-DocumentationMDSection.ps1 new file mode 100644 index 0000000..5fddc0c --- /dev/null +++ b/PSModule/M365Documentation/Internal/Output/Write-DocumentationMDSection.ps1 @@ -0,0 +1,68 @@ +Function Write-DocumentationMDSection(){ + <# + .SYNOPSIS + Outputs a section of the documentation to Markdown + .DESCRIPTION + This function takes the passed data and is outputing it to the Markdown file. + .EXAMPLE + Write-DocumentationWordSection -FullDocumentationPath $FullDocumentationPath -Data $Data -Level 1 + + .NOTES + NAME: Thomas Kurth / 21.7.2023 + #> + param( + [string]$FullDocumentationPath, + [DocSection]$Data, + [int]$Level = 1 + ) + + if($Data.Objects -or $Data.SubSections){ + if(-not [String]::IsNullOrEmpty($Data.Title)){ + $Heading = "" + $tocspace = "" + $x = 0 + while($x -le $Level){ + $Heading += "#" + $tocspace += " " + $x = $x + 1 + } + $Heading += " $($Data.Title)" + $Heading | Out-File -LiteralPath $FullDocumentationPath -Append + "" | Out-File -LiteralPath $FullDocumentationPath -Append + + #Fix indentation + $tocspace = $tocspace.Substring(4) + $TitleClean = $Data.Title.ToLower().Replace(" ","-") -replace '[^a-zA-Z0-9/_/-]', '' + $script:toc += "$tocspace- [$($Data.Title)](#$($TitleClean))" + [System.Environment]::NewLine + } + if($Data.Text){ + $Data.Text | Out-File -LiteralPath $FullDocumentationPath -Append + "" | Out-File -LiteralPath $FullDocumentationPath -Append + } + if($Data.Objects){ + if($Data.Transpose){ + foreach($singleObj in $Data.Objects){ + if($singleObj.displayName -ne $Data.Title -and $Data.Title -ne $singleObj.'Display Name' -and -not [String]::IsNullOrEmpty($singleObj.displayName)){ + $Heading = "" + $x = 0 + while($x -le ($Level + 1)){ + $Heading += "#" + $x = $x + 1 + } + $Heading += " $($singleObj.displayName)" + $Heading | Out-File -LiteralPath $FullDocumentationPath -Append + "" | Out-File -LiteralPath $FullDocumentationPath -Append + } + $singleObj | Format-MarkdownTableListStyle -HideStandardOutput -DoNotCopyToClipboard -ShowMarkdown | Out-File -LiteralPath $FullDocumentationPath -Append + } + + } else { + $Data.Objects | Format-MarkdownTableTableStyle -HideStandardOutput -DoNotCopyToClipboard -ShowMarkdown | Out-File -LiteralPath $FullDocumentationPath -Append + } + + } + foreach($Section in $Data.SubSections){ + Write-DocumentationMDSection -FullDocumentationPath $FullDocumentationPath -Data $Section -Level ($Level + 1) + } + } +} \ No newline at end of file diff --git a/PSModule/M365Documentation/M365Documentation.psd1 b/PSModule/M365Documentation/M365Documentation.psd1 index aaef5b1..867b85e 100644 --- a/PSModule/M365Documentation/M365Documentation.psd1 +++ b/PSModule/M365Documentation/M365Documentation.psd1 @@ -72,7 +72,7 @@ RequiredModules = @('MSAL.PS', # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = 'Connect-M365Doc', 'Get-M365Doc', 'Invoke-M365DocTranslationUI', 'New-M365DocAppRegistration', 'Optimize-M365Doc', 'Write-M365DocCsv', - 'Write-M365DocJson', 'Write-M365DocWord' + 'Write-M365DocJson', 'Write-M365DocWord', 'Write-M365DocMD' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @()