Public/Convert-MAMLToHTML.ps1

Function Convert-MAMLToHTML {
    <#
      .DESCRIPTION
      Will convert MAML text or file to HTML.
 
        .EXAMPLE
        # Convert knowledge article and write to .html file
        #Example
        PS C:\> $monitor = Get-SCOMMonitor -DisplayName 'M365 Teams - Chat Synthetic Test Performance Monitor'
        PS C:\> $Article = $monitor.GetKnowledgeArticle(([System.Globalization.CultureInfo]'en-US'))
        PS C:\> Convert-MAMLToHTML -XML $Article.MamlContent | Set-Content C:\temp\KnowledgeArticle.html
 
 
      .NOTES
      Author: Tyson Paul (https://monitoringguys.com/)
      References:
      https://devio.wordpress.com/2009/09/15/command-line-xslt-processor-with-powershell/
      https://systemcenter.wiki/
 
      Version History:
      2022.06.20 - v1
    #>


    [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
                  PositionalBinding=$false,
                  HelpUri = 'https://monitoringguys.com/')]
  Param (
    # Path to XMLFile
    [Parameter(Mandatory=$true,
                ValueFromPipeline=$false,
                ValueFromPipelineByPropertyName=$false,
                ValueFromRemainingArguments=$false,
                Position=0,
                ParameterSetName='XMLFileExists')]
    [ValidateNotNullOrEmpty()]
    [string]$XMLFile,

    # XML data (raw)
    [Parameter(Mandatory=$true,
                ValueFromPipeline=$false,
                ValueFromPipelineByPropertyName=$false,
                ValueFromRemainingArguments=$false,
                Position=0,
                ParameterSetName='XMLData')]
    [ValidateNotNullOrEmpty()]
    [string]$XML,

    # Path to XSL transform file.
    [Parameter(Mandatory=$false,
                ValueFromPipeline=$false,
                ValueFromPipelineByPropertyName=$false,
                ValueFromRemainingArguments=$false,
                Position=1)]
    [string]$XSLFile
  )

  ################# FUNCTIONS #################
  Function Test-XMLFile {
    <#
        .SYNOPSIS
        Test the validity of an XML file
        .NOTES
        https://stackoverflow.com/questions/14423861/how-to-validate-xml-for-correct-syntax-format
    #>

    [CmdletBinding()]
    param (
        [parameter(mandatory=$true)][ValidateNotNullorEmpty()][string]$xmlFilePath
    )

    # Check the file exists
    if (!(Test-Path -Path $xmlFilePath)){
        throw "$xmlFilePath is not valid. Please provide a valid path to the .xml fileh"
    }
    # Check for Load or Parse errors when loading the XML file
    $xml = New-Object System.Xml.XmlDocument
    try {
        $xml.Load((Get-ChildItem -Path $xmlFilePath).FullName)
        return $true
    }
    catch [System.Xml.XmlException] {
        Write-Verbose "$xmlFilePath : $($_.toString())"
        return $false
    }
  }

  ##############################################
  Function Write-DefaultTransformFile {
      # https://systemcenter.wiki/
      $XSLData = @"
<?xml version="1.0" encoding="utf-8"?>
 
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                xmlns:maml="http://schemas.microsoft.com/maml/2004/10"
                exclude-result-prefixes="msxsl maml">
<xsl:output method="html" indent="no" encoding="utf-8" />
 
 
<xsl:template match="/">
    <xsl:apply-templates />
</xsl:template>
 
 
 
<xsl:template match="maml:section">
<xsl:apply-templates />
</xsl:template>
 
 
 
<xsl:template match="maml:lineBreak">
<br />
</xsl:template>
 
 
 
<xsl:template match="maml:navigationLink">
<a>
    <xsl:attribute name="href">
        <xsl:value-of select="maml:uri/@href"/>
    </xsl:attribute>
    <xsl:choose>
        <xsl:when test="maml:uri/@condition!=''">
            <xsl:attribute name="condition">
                <xsl:value-of select="maml:uri/@condition" />
            </xsl:attribute>
        </xsl:when>
    </xsl:choose>
    <xsl:choose>
        <xsl:when test="maml:uri/@uri!=''">
            <xsl:attribute name="uri">
                <xsl:value-of select="maml:uri/@uri" />
            </xsl:attribute>
        </xsl:when>
    </xsl:choose>
    <xsl:value-of select="maml:linkText"/>
</a>
</xsl:template>
 
 
 
<xsl:template match="maml:list">
<ul>
    <xsl:apply-templates />
</ul>
</xsl:template>
 
<xsl:template match="maml:listItem">
<li>
    <xsl:apply-templates />
</li>
</xsl:template>
 
 
 
<xsl:template match="maml:title">
<h1>
    <xsl:value-of select="."/>
</h1>
</xsl:template>
 
<xsl:template match="maml:subTitle">
<h2>
 <xsl:value-of select="." />
</h2>
</xsl:template>
 
<xsl:template match="text()">
<xsl:value-of select="."/>
</xsl:template>
 
<xsl:template match="maml:example">
    <pre>
        <xsl:apply-templates />
    </pre>
</xsl:template>
 
<xsl:template match="maml:codeInline">
    <code>
        <xsl:apply-templates />
    </code>
</xsl:template>
 
<xsl:template match="maml:computerOutputInline">
    <pre>
        <xsl:apply-templates />
    </pre>
</xsl:template>
 
<xsl:template match="maml:procedure">
    <ol>
        <xsl:apply-templates />
    </ol>
</xsl:template>
 
<xsl:template match="maml:step">
    <li>
        <xsl:apply-templates />
    </li>
</xsl:template>
 
<xsl:template match="maml:para">
    <p>
        <xsl:apply-templates />
    </p>
</xsl:template>
 
<xsl:template match="maml:ui">
    <b>
        <xsl:apply-templates />
    </b>
</xsl:template>
 
<xsl:template match="maml:entry">
    <td>
        <xsl:apply-templates />
    </td>
</xsl:template>
 
<xsl:template match="maml:headerEntry">
    <th>
        <xsl:apply-templates />
    </th>
</xsl:template>
 
<xsl:template match="maml:tableHeader">
  <thead>
     <xsl:apply-templates />
  </thead>
</xsl:template>
 
<xsl:template match="maml:row">
    <tr>
        <xsl:apply-templates />
    </tr>
</xsl:template>
 
<xsl:template match="maml:table">
    <table>
        <xsl:apply-templates />
    </table>
</xsl:template>
 
</xsl:stylesheet>
 
 
"@

    $XSLFile = Join-Path $TempDir 'maml2html.xsl'
    $XSLData | Set-Content -Path $XSLFile -Force -Encoding UTF8

  }

  ############## END FUNCTIONS #################
  ##############################################

  if ((-NOT $XMLFile) -AND (-NOT $XML))
  {
    Get-Help Convert-MAMLToHtml -Examples
    Return
  }

  # Setup temp dir for output file. Using a redirector to an output file is the only way I could find to obtain the property bag data.
  $TempDir = (Join-Path (Join-Path $env:Windir "Temp") (Join-Path 'SCOMHelper' 'Export-SCOMKnowledge') )
  New-Item -Type Directory -Path $TempDir -ErrorAction SilentlyContinue | Out-Null
  If (-NOT(Test-Path -Path $TempDir -PathType Container)) {
    Write-Error "Unable to create/access directory: [$($TempDir)]. "
    Return $false
  }

  If ($XSLFile.Length) {
    If( -NOT (Test-Path $XSLFile)) {
      Throw "XSL input file not found: $($_)"
      Exit
    }
  }
  Else {
    #dot-sourced
    . Write-DefaultTransformFile

  }
  $tmpTransformFile = New-TemporaryFile

  If ($XML) {
    $XMLFile = (New-TemporaryFile).FullName
    $XML | Set-Content -Path $XMLFile -Force -Encoding UTF8
  }

  If (-NOT (Test-XMLFile -xmlFilePath $XMLFile) ){
    $tmpXMLFile = (New-TemporaryFile).FullName
    # Make sure XML has a root
    "<Root>$(Get-Content -Path $XMLFile)</Root>" | Set-Content -Path $tmpXMLFile -Force -Encoding UTF8

    If (-NOT (Test-XMLFile -xmlFilePath $tmpXMLFile) ){
      # Make sure XML has a root
      Throw "Error loading XML data from path: $XMLFile"
      Return
    }
    # Set path to new tmp file which contains root element.
    $XMLFile = $tmpXMLFile
  }

  $xslt = New-Object System.Xml.Xsl.XslCompiledTransform;
  Try {
    $xslt.Load($XSLFile);
  } Catch {
    Write-Warning "Failed to load transform file [$($XSLFile)]. Will use default 'maml2html' transform instead."
    . Write-DefaultTransformFile
    Try {
      $xslt.Load($XSLFile);
    } Catch {
      Write-Error "Fatal Error: Failed to load default transform file [$($XSLFile)]. "
      Return
    }
  }
  $xslt.Transform($XMLFile, $tmpTransformFile.FullName);

  Return (Get-Content -Path $tmpTransformFile.FullName);
}