XMLLab.psm1
|
function Compare-Xml { <# .SYNOPSIS Compares two XML documents and returns the differences. .INPUTS System.Xml.XmlDocument to compare to the reference XML. .OUTPUTS System.Xml.XmlDocument containing XSLT that can be applied to the reference XML to transform it to the difference XML. It contains templates for changed nodes. .FUNCTIONALITY XML .LINK Resolve-XPath .EXAMPLE Compare-Xml '<a b="z"/>' '<a b="y"/>' |Format-Xml <xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()" /> </xsl:copy> </xsl:template> <xsl:template match="/a/@b"> <xsl:attribute name="b"><![CDATA[y]]></xsl:attribute> </xsl:template> </xsl:transform> .EXAMPLE Compare-Xml '<a b="z"/>' '<a c="y"/>' |Format-Xml <xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()" /> </xsl:copy> </xsl:template> <xsl:template match="/a/@b" /> <xsl:template match="/a"> <xsl:copy> <xsl:apply-templates select="@*" /> <xsl:attribute name="c"><![CDATA[y]]></xsl:attribute> </xsl:copy> </xsl:template> </xsl:transform> .EXAMPLE Compare-Xml '<a><b/><c/><!-- d --></a>' '<a><c/><b/></a>' |Format-Xml <xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()" /> </xsl:copy> </xsl:template> <xsl:template match="/a"> <xsl:copy> <xsl:apply-templates select="c" /> <b /> </xsl:copy> </xsl:template> </xsl:transform> .EXAMPLE Compare-Xml '<a/>' '<a><!-- annotation --><new/><?node details?></a>' |Format-Xml <xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()" /> </xsl:copy> </xsl:template> <xsl:template match="/a"> <xsl:copy> <xsl:comment><![CDATA[ annotation ]]></xsl:comment> <new /> <xsl:processing-instruction name="node"><![CDATA[details]]></xsl:processing-instruction> </xsl:copy> </xsl:template> </xsl:transform> #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter','',Justification='These params are used.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','',Justification='These plurals are fine.')] [CmdletBinding()][OutputType([xml])] Param( # The original XML document to be compared. [Parameter(Position=0,Mandatory=$true)][xml] $ReferenceXml, # An XML document to compare to. [Parameter(Position=1,Mandatory=$true)][xml] $DifferenceXml ) function Test-XmlNodesMatch { Param( [Parameter(Position=0)][Xml.XmlNode] $ReferenceNode, [Parameter(Position=1)][Xml.XmlNode] $DifferenceNode ) if($null -eq $ReferenceNode) {return} elseif($null -eq $DifferenceNode) {return} elseif($ReferenceNode.NodeType -ne $DifferenceNode.NodeType) {return} elseif($ReferenceNode.NamespaceURI -cne $DifferenceNode.NamespaceURI) {return} elseif($ReferenceNode.LocalName -cne $DifferenceNode.LocalName) {return} else {return $true} } function Test-XmlAttributesEqual { Param( [Parameter(Position=0)][Xml.XmlElement] $ReferenceElement, [Parameter(Position=1)][Xml.XmlElement] $DifferenceElement ) if($ReferenceElement.Attributes.Count -ne $DifferenceElement.Attributes.Count) {return $false} foreach(${@} in $ReferenceElement.Attributes) { if(${@}.NamespaceURI) { if(!$DifferenceElement.HasAttribute(${@}.LocalName,${@}.NamespaceURI) -or $DifferenceElement.GetAttribute(${@}.LocalName,${@}.NamespaceURI) -ne ${@}.Value) {return $false} } else { if(!$DifferenceElement.HasAttribute(${@}.LocalName) -or $DifferenceElement.GetAttribute(${@}.LocalName) -ne ${@}.Value) {return $false} } } return $true } function Test-XmlNodesEqual { Param( [Parameter(Position=0)][Xml.XmlNode] $ReferenceNode, [Parameter(Position=1)][Xml.XmlNode] $DifferenceNode ) if($ReferenceNode.OuterXml -ceq $DifferenceNode.OuterXml) {return $true} elseif(!(Test-XmlNodesMatch $ReferenceNode $DifferenceNode)) {return $false} elseif($ReferenceNode.NodeType -eq 'Element' -and !(Test-XmlAttributesEqual $ReferenceNode $DifferenceNode)) {return $false} elseif($ReferenceNode.ChildNodes.Count -ne $DifferenceNode.ChildNodes.Count) {return $false} else { for($i = 0; $i -lt $ReferenceNode.ChildNodes.Count; $i++) { if(!(Test-XmlNodesEqual $ReferenceNode.ChildNodes[$i] $DifferenceNode.ChildNodes[$i])) {return $false} } return $ReferenceNode.Value -ceq $DifferenceNode.Value } } function Format-XPathMatch { Param([Parameter(Position=0,Mandatory=$true)][Xml.XmlNode] $XmlNode) $xpath = Resolve-XPath $XmlNode "match='$($xpath.XPath)' $($xpath.Namespace.GetEnumerator() |ForEach-Object {"xmlns:$($_.Name)='$($_.Value)'"})" } function ConvertTo-XmlAttributeTemplate { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlAttribute] $ReferenceAttribute, [Parameter(Position=1,Mandatory=$true)][Xml.XmlAttribute] $DifferenceAttribute ) return [xml]@" <xsl:template $(Format-XPathMatch $ReferenceAttribute) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:attribute name="$($DifferenceAttribute.Name)"><![CDATA[$($DifferenceAttribute.Value)]]></xsl:attribute> </xsl:template> "@ } function ConvertTo-XmlCDataTemplate { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlCDataSection] $ReferenceCData, [Parameter(Position=1,Mandatory=$true)][Xml.XmlCDataSection] $DifferenceCData ) return [xml]@" <xsl:template $(Format-XPathMatch $ReferenceCData) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:text><![CDATA[$($DifferenceCData.Value)]]></xsl:text> </xsl:template> "@ } function ConvertTo-XmlCommentTemplate { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlComment] $ReferenceComment, [Parameter(Position=1,Mandatory=$true)][Xml.XmlComment] $DifferenceComment ) return [xml]@" <xsl:template $(Format-XPathMatch $ReferenceComment) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:comment><![CDATA[$($DifferenceComment.Value)]]></xsl:comment> </xsl:template> "@ } function ConvertTo-XmlProcessingInstructionTemplate { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlProcessingInstruction] $ReferenceProcessingInstruction, [Parameter(Position=1,Mandatory=$true)][Xml.XmlProcessingInstruction] $DifferenceProcessingInstruction ) return [xml]@" <xsl:template $(Format-XPathMatch $ReferenceProcessingInstruction) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:processing-instruction name="$($DifferenceProcessingInstruction.Name)"> <![CDATA[$($DifferenceProcessingInstruction.Value)]]> </xsl:processing-instruction> </xsl:template> "@ } function ConvertTo-XmlSignificantWhitespaceTemplate { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlSignificantWhitespace] $ReferenceSignificantWhitespace, [Parameter(Position=1,Mandatory=$true)][Xml.XmlSignificantWhitespace] $DifferenceSignificantWhitespace ) return [xml]@" <xsl:template $(Format-XPathMatch $ReferenceSignificantWhitespace) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:text><![CDATA[$($DifferenceSignificantWhitespace.Value)]]></xsl:text> </xsl:template> "@ } function ConvertTo-XmlTextTemplate { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlText] $ReferenceText, [Parameter(Position=1,Mandatory=$true)][Xml.XmlText] $DifferenceText ) return [xml]@" <xsl:template $(Format-XPathMatch $ReferenceText) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:text><![CDATA[$($DifferenceText.Value)]]></xsl:text> </xsl:template> "@ } function ConvertTo-XmlWhitespaceTemplate { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlWhitespace] $ReferenceWhitespace, [Parameter(Position=1,Mandatory=$true)][Xml.XmlWhitespace] $DifferenceWhitespace ) if($ReferenceWhitespace.Value -ceq $DifferenceWhitespace.Value) {return} return [xml]@" <xsl:template $(Format-XPathMatch $ReferenceWhitespace) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:text>$($DifferenceWhitespace.Value)"></xsl:text> </xsl:template> "@ } function Measure-XmlNodePosition([Parameter(Position=0,Mandatory=$true)][Xml.XmlNode]$Node) { if(!($Node.PreviousSibling -or $Node.NextSibling)) {return} for($i,$n = 0,$Node; $n; $n = $n.PreviousSibling) {if(Test-XmlNodesMatch $n $Node) {$i++}} if($i -gt 1) {return "[$i]"} for($i,$n = 0,$Node.NextSibling; $n; $n = $n.NextSibling) {if(Test-XmlNodesMatch $n $Node) {$i++; break}} if($i -gt 0) {return '[1]'} else {return} } filter Format-ApplyTemplates { [CmdletBinding()][OutputType([string])] Param( [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][Xml.XmlNodeType] $NodeType, [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][ValidateNotNullOrEmpty()][string] $Name, [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][string] $LocalName, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Prefix, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $NamespaceURI, [Parameter(ValueFromPipeline=$true)][Xml.XmlNode] $Node ) $fullname = if($NamespaceURI -and !$Prefix) {$Prefix = 'ns'; "ns:$LocalName"} else {$Name} $xpath = switch($NodeType) { Attribute {"@$fullname"} CDATA {"text()$(Measure-XmlNodePosition $Node)"} Comment {"comment()$(Measure-XmlNodePosition $Node)"} Document {'/'} Element {"$fullname$(Measure-XmlNodePosition $Node)"} ProcessingInstruction {"processing-instruction('$name')$(Measure-XmlNodePosition $Node)"} SignificantWhitespace {"text()$(Measure-XmlNodePosition $Node)"} Text {"text()$(Measure-XmlNodePosition $Node)"} Whitespace {"text()$(Measure-XmlNodePosition $Node)"} default {return} } if(!$NamespaceURI) {return "<xsl:apply-templates select='$xpath'/>"} else {return "<xsl:apply-templates select='$xpath' xmlns:$Prefix='$NamespaceURI'/>"} } function ConvertTo-XmlNodeLiteral { [CmdletBinding()][OutputType([string])] Param([Parameter(Position=0,Mandatory=$true)][Xml.XmlNode] $Node) switch($Node.NodeType) { CDATA {return "<xsl:text><![CDATA[$($Node.Value)]]></xsl:text>"} Comment {return "<xsl:comment><![CDATA[$($Node.Value)]]></xsl:comment>"} Element {return $Node.OuterXml} ProcessingInstruction {return ("<{0} name='$($Node.Name)'><![CDATA[$($Node.Value)]]></{0}>" -f 'xsl:processing-instruction')} SignificantWhitespace {return "<xsl:text><![CDATA[$($Node.Value)]]></xsl:text>"} Text {return "<xsl:text><![CDATA[$($Node.Value)]]></xsl:text>"} Whitespace {return "<xsl:text><![CDATA[$($Node.Value)]]></xsl:text>"} default {return} } } function Merge-XmlNodes { Param( [Parameter(Position=0,Mandatory=$true)][AllowEmptyCollection()][Xml.XmlNode[]] $ReferenceNodes, [Parameter(Position=1,Mandatory=$true)][AllowEmptyCollection()][Xml.XmlNode[]] $DifferenceNodes ) for($d,$list = 0,@(); $d -lt $DifferenceNodes.Length; $d++) { $diff = $DifferenceNodes[$d] [int[]] $matches = if($ReferenceNodes.Length -le 0) {@()} else {0..($ReferenceNodes.Length-1) |Where-Object {Test-XmlNodesMatch $ReferenceNodes[$_] $diff}} [int] $r = if(!$matches -or $matches.Length -eq 0) {-1} elseif($matches.Length -eq 1) {$matches[0]} else { $equals = $matches |Where-Object {Test-XmlNodesEqual $ReferenceNodes[$_] $diff} |Select-Object -First 1 if($equals.Length -eq 0) {$matches[0]} else {$equals[0]} } $list += [pscustomobject]@{ ReferenceNode = if($r -eq -1) {$null} else {$ReferenceNodes[$r]} ReferenceIndex = $r DifferenceNode = $DifferenceNodes[$d] DifferenceIndex = $d Template = if($r -eq -1) {$null} else {ConvertTo-XmlNodeTemplates $ReferenceNodes[$r] $diff} } if($r -ne -1) {$ReferenceNodes[$r] = [xml]'<null/>'} } return ([pscustomobject]@{ HasDifferentOrder = !!($list |Where-Object {$_.ReferenceIndex -ne $_.DifferenceIndex}) ApplyTemplates = ($list | ForEach-Object { if($_.ReferenceNode) {$_.ReferenceNode |Format-ApplyTemplates} else {ConvertTo-XmlNodeLiteral $_.DifferenceNode} } ) -join [environment]::NewLine Templates = $list |Where-Object Template -ne $null |Select-Object -ExpandProperty Template }) } filter Add-XmlAttribute { [CmdletBinding()][OutputType([string])] Param( [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][ValidateNotNullOrEmpty()][string] $Name, [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][string] $LocalName, [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][string] $Value, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Prefix, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $NamespaceURI ) if($NamespaceURI) { if(!$Prefix) {$Prefix = 'ns'} return "<xsl:attribute name='${Prefix}:$LocalName' xmlns:$Prefix='$NamespaceURI'><![CDATA[$Value]]></xsl:attribute>" } else {return "<xsl:attribute name='$Name'><![CDATA[$Value]]></xsl:attribute>"} } function ConvertTo-XmlElementTemplates { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlElement] $ReferenceElement, [Parameter(Position=1,Mandatory=$true)][Xml.XmlElement] $DifferenceElement ) ${+} = @() foreach(${@} in $DifferenceElement.Attributes) { if(${@}.NamespaceURI) { if(!$ReferenceElement.HasAttribute(${@}.LocalName,${@}.NamespaceURI)) {${+} += ${@}} else {ConvertTo-XmlNodeTemplates $ReferenceElement.GetAttributeNode(${@}.LocalName,${@}.NamespaceURI) ${@}} } else { if(!$ReferenceElement.HasAttribute(${@}.Name)) {${+} += ${@}} else {ConvertTo-XmlNodeTemplates $ReferenceElement.GetAttributeNode(${@}.Name) ${@}} } } foreach(${@} in $ReferenceElement.Attributes) { if(!${@}.NamespaceURI) {if(!$DifferenceElement.HasAttribute(${@}.LocalName)) {ConvertTo-XmlNodeTemplates ${@} $null}} elseif(!$DifferenceElement.HasAttribute(${@}.LocalName,${@}.NamespaceURI)) {ConvertTo-XmlNodeTemplates ${@} $null} } $merge = Merge-XmlNodes $ReferenceElement.ChildNodes $DifferenceElement.ChildNodes $merge.Templates if(${+} -or $merge.HasDifferentOrder) {[xml]@" <xsl:template $(Format-XPathMatch $ReferenceElement) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:copy> $(if($ReferenceElement.HasAttributes) {'<xsl:apply-templates select="@*" />'}) $(if(${+}) {${+} |Add-XmlAttribute}) $($merge.ApplyTemplates) </xsl:copy> </xsl:template> "@} } function ConvertTo-XmlDocumentTemplates { Param( [Parameter(Position=0,Mandatory=$true)][xml] $ReferenceDocument, [Parameter(Position=1,Mandatory=$true)][xml] $DifferenceDocument ) $ns = 'xmlns:xsl="http://www.w3.org/1999/XSL/Transform"' $declaration = if($DifferenceDocument.FirstChild.NodeType -eq 'XmlDeclaration') { "omit-xml-declaration='yes'" $encoding = $DifferenceDocument.FirstChild.Encoding if($encoding) {"encoding='$encoding'"} $standalone = $DifferenceDocument.FirstChild.Standalone if($standalone) {"standalone='$standalone'"} } $doctype = $DifferenceDocument.ChildNodes |Where-Object NodeType -ceq 'DocumentType' if($doctype) { if($doctype.PublicId -like '-//W3C//DTD XHTML *') {[xml]@" <xsl:output $declaration method="xhtml" doctype-public="$($doctype.PublicId)" doctype-system="$($doctype.SystemId)" $ns /> "@} elseif($doctype.name -ceq 'html') {[xml]@" <xsl:output $declaration method="html" doctype-public="$($doctype.PublicId)" doctype-system="$($doctype.SystemId)" $ns /> "@} else {[xml]@" <xsl:output $declaration method="xml" doctype-public="$($doctype.PublicId)" doctype-system="$($doctype.SystemId)" $ns /> "@} } elseif($declaration) {[xml]@" <xsl:output $declaration method="xml" $ns /> "@} [xml]@" <xsl:template match="@*|node()" $ns><xsl:copy><xsl:apply-templates select="@*|node()" /></xsl:copy></xsl:template> "@ $reorder = $false if($ReferenceDocument.DocumentElement.PreviousSibling -or $DifferenceDocument.DocumentElement.PreviousSibling) { $refpre = foreach($node in $ReferenceDocument.ChildNodes) { if($node.NodeType -ceq 'Element') {break} elseif($node.NodeType -notin 'XmlDeclaration','DocumentType') {$node} } if($null -eq $refpre) {$refpre = @()} $diffpre = foreach($node in $DifferenceDocument.ChildNodes) { if($node.NodeType -ceq 'Element') {break} elseif($node.NodeType -notin 'XmlDeclaration','DocumentType') {$node} } if($null -eq $diffpre) {$diffpre = @()} $mergepre = Merge-XmlNodes $refpre $diffpre $reorder = $mergepre.HasDifferentOrder $mergepre.Templates } ConvertTo-XmlElementTemplates $ReferenceDocument.DocumentElement $DifferenceDocument.DocumentElement if($ReferenceDocument.DocumentElement.NextSibling -or $DifferenceDocument.DocumentElement.NextSibling) { $refpost = for($node = $ReferenceDocument.DocumentElement.NextSibling; $node; $node = $node.NextSibling) {$node} if($null -eq $refpost) {$refpost = @()} $diffpost = for($node = $DifferenceDocument.DocumentElement.NextSibling; $node; $node = $node.NextSibling) {$node} if($null -eq $diffpost) {$diffpost = @()} $mergepost = Merge-XmlNodes $refpost $diffpost if(!$reorder) {$reorder = $mergepost.HasDifferentOrder} $mergepost.Templates } if($reorder) {[xml]@" <xsl:template match="/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> $($mergepre.ApplyTemplates) <xsl:apply-templates select="*"/> $($mergepost.ApplyTemplates) </xsl:template> "@} } function ConvertTo-XmlNodeTemplates { Param( [Parameter(Position=0,Mandatory=$true)][Xml.XmlNode] $ReferenceNode, [Parameter(Position=1)][Xml.XmlNode] $DifferenceNode ) if($null -eq $DifferenceNode) {return [xml]@" <xsl:template $(Format-XPathMatch $ReferenceNode) xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/> "@} if(Test-XmlNodesEqual $ReferenceNode $DifferenceNode) {return} switch($DifferenceNode.NodeType) { Attribute {ConvertTo-XmlAttributeTemplate $ReferenceNode $DifferenceNode} CDATA {ConvertTo-XmlCDataTemplate $ReferenceNode $DifferenceNode} Comment {ConvertTo-XmlCommentTemplate $ReferenceNode $DifferenceNode} Document {ConvertTo-XmlDocumentTemplates $ReferenceNode $DifferenceNode} Element {ConvertTo-XmlElementTemplates $ReferenceNode $DifferenceNode} ProcessingInstruction {ConvertTo-XmlProcessingInstructionTemplate $ReferenceNode $DifferenceNode} SignificantWhitespace {ConvertTo-XmlSignificantWhitespaceTemplate $ReferenceNode $DifferenceNode} Text {ConvertTo-XmlTextTemplate $ReferenceNode $DifferenceNode} Whitespace {ConvertTo-XmlWhitespaceTemplate $ReferenceNode $DifferenceNode} default {return} } } function Compare-Xml { if($ReferenceXml.DocumentElement.NamespaceURI -cne $DifferenceXml.DocumentElement.NamespaceURI -or $ReferenceXml.DocumentElement.LocalName -cne $DifferenceXml.DocumentElement.LocalName) { [xml]$value = $DifferenceXml.Clone() # simplified transform: https://www.w3.org/TR/1999/REC-xslt-19991116#result-element-stylesheet [void]$value.DocumentElement.SetAttribute('version','http://www.w3.org/1999/XSL/Transform','1.0') } else { [xml]$value = '<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>' foreach($template in ConvertTo-XmlNodeTemplates $ReferenceXml $DifferenceXml) { if($null -eq $template) {continue} $template.DocumentElement.RemoveAttribute('xmlns:xsl') [void]$value.DocumentElement.AppendChild($value.ImportNode([Xml.XmlNode]$template.DocumentElement,$true)) } } return $value } Compare-Xml } function Convert-Xml { <# .SYNOPSIS Transform XML using an XSLT template. .FUNCTIONALITY XML .EXAMPLE Convert-Xml '<a xsl:version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>' '<z/>' |Format-Xml <a /> .EXAMPLE Convert-Xml -TransformFile xsd2html.xslt -Path schema.xsd -OutFile schema.html Writes schema.html by applying the xsd2html.xslt transformation to schema.xsd. #> [CmdletBinding(SupportsShouldProcess=$true)] [OutputType(ParameterSetName='Xml',[void])][OutputType(ParameterSetName='File',[void])] Param( # An XML document containing an XSLT transform. [Parameter(ParameterSetName='Xml',Position=0,Mandatory=$true)][xml] $TransformXslt, # An XML document to transform. [Parameter(ParameterSetName='Xml',Position=1,ValueFromPipeline=$true)][xml] $Xml, # The XSLT file to use to transform the XML. [Parameter(ParameterSetName='File',Mandatory=$true)][Alias('TemplateFile','XsltTemplateFile')][string] $TransformFile, # The XML file to transform. [Parameter(ParameterSetName='File',ValueFromPipelineByPropertyName=$true)][Alias('XmlFile','FullName')][string] $Path, # The file to write the transformed XML to. [Parameter()][string] $OutFile, <# When specified, indicates the XSLT is trusted, enabling the document() function and embedded script blocks. #> [switch] $TrustedXslt ) Begin { if($PSCmdlet.ParameterSetName -eq 'File') { [xml] $TransformXslt = Resolve-Path $TransformFile |Get-Content -Raw [xml] $Xml = Resolve-Path $Path |Get-Content -Raw } [version] $xsltversion = Select-Xml '/*/@version' $TransformXslt -Namespace @{ xsl='http://www.w3.org/1999/XSL/Transform'} | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value if($xsltversion -gt '1.0') { throw "XSLT version $xsltversion is not supported by the CLR." } $xslt = New-Object Xml.Xsl.XslCompiledTransform try { $xslt.Load($TransformXslt, $(if($TrustedXslt) {[Xml.Xsl.XsltSettings]::TrustedXslt}else {[Xml.Xsl.XsltSettings]::Default}), (New-Object Xml.XmlUrlResolver -Property @{Credentials = [Net.CredentialCache]::DefaultCredentials})) } catch [Management.Automation.MethodInvocationException] { for($ex = $_.Exception.InnerException; $ex; $ex = $ex.InnerException) {Write-Error $ex.Message} throw } } Process { if($PSCmdlet.ParameterSetName -eq 'File') {[xml] $Xml = Resolve-Path $Path |Get-Content -Raw} if(!$OutFile) { $ms = New-Object IO.MemoryStream $xw = [Xml.XmlWriter]::Create($ms,$xslt.OutputSettings) $xslt.Transform($Xml,$xw) $xw.Close() ; $xw.Dispose() ; $xw = $null [void]$ms.Seek(0,0) $result = New-Object xml $result.Load($ms) $ms.Close() ; $ms.Dispose() ; $ms = $null $result } else { $absOutFile = if((Split-Path $OutFile -IsAbsolute)) {$OutFile} else {Join-Path "$PWD" $OutFile} if((Test-Path $absOutFile) -and !$PSCmdlet.ShouldContinue("$(Get-Item $absOutFile |Select-Object FullName,LastWriteTime,Length)","Overwrite File?")) {Write-Warning "Skipping transform from $absPath to $OutFile"; return} $sw = New-Object IO.StreamWriter $absOutFile $xslt.Transform([Xml.XPath.IXPathNavigable]$Xml,(New-Object Xml.XmlTextWriter $sw)) $sw.Close() ; $sw.Dispose() ; $sw = $null } } } function ConvertFrom-EscapedXml { <# .SYNOPSIS Parse escaped XML into XML and serialize it. .INPUTS System.String, some escaped XML. .FUNCTIONALITY XML .OUTPUTS System.String, the XML parsed and serialized. .EXAMPLE ConvertFrom-EscapedXml '<a href="http://example.org">link</a>' <a href="http://example.org">link</a> #> [CmdletBinding()][OutputType([string])] Param( # The escaped XML text. [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)][string] $EscapedXml, # Outputs the XML without indentation. [Alias('NoIndent')][switch] $Compress ) Process { [xml] $xml = [Net.WebUtility]::HtmlDecode($EscapedXml) $sw = New-Object IO.StringWriter [Xml.XmlWriterSettings] $cfg = New-Object Xml.XmlWriterSettings -Property @{ Indent = !$Compress; OmitXmlDeclaration = $true } [Xml.XmlWriter] $xo = [Xml.XmlWriter]::Create($sw, $cfg) $xml.WriteTo($xo) $xo.Dispose() $out = $sw.ToString() $sw.Dispose() return $out } } function ConvertFrom-XmlElement { <# .SYNOPSIS Converts named nodes of an element to properties of a PSObject, recursively. .INPUTS System.Xml.XmlElement to convert into a PSObject, or System.Xml.XmlDocument to convert the document element into a PSObject, or Microsoft.PowerShell.Commands.SelectXmlInfo output from Select-Xml to convert each selected node into a PSObject. .OUTPUTS System.Management.Automation.PSCustomObject object created from selected XML. .FUNCTIONALITY XML .LINK Select-Xml .EXAMPLE Select-Xml /configuration/appSettings/add web.config |ConvertFrom-XmlElement key value --- ----- webPages:Enabled false #> [CmdletBinding()][OutputType([psobject])] Param( # The XML document to convert to a PSObject. [Parameter(ParameterSetName='Document',Position=0,Mandatory=$true,ValueFromPipeline=$true)][Xml.XmlDocument] $Document, # The XML element to convert to a PSObject. [Parameter(ParameterSetName='Element',Position=0,Mandatory=$true,ValueFromPipeline=$true)][Xml.XmlElement] $Element, # Output from the Select-Xml cmdlet. [Parameter(ParameterSetName='SelectXmlInfo',Position=0,Mandatory=$true,ValueFromPipeline=$true)] [Microsoft.PowerShell.Commands.SelectXmlInfo] $SelectXmlInfo, # Only include attributes, ignore other child nodes. [Alias('Attributes','Atts')][switch] $OnlyAttributes, # Replace any simple element that only contains one other element with its child. [Alias('CollapseSimple')][switch] $SimpleSuccession ) Process { switch($PSCmdlet.ParameterSetName) { Document {$Document.DocumentElement |ConvertFrom-XmlElement -OnlyAttributes:$OnlyAttributes} SelectXmlInfo { @($SelectXmlInfo |ForEach-Object {[Xml.XmlElement]$_.Node} | ConvertFrom-XmlElement -OnlyAttributes:$OnlyAttributes) } Element { if($OnlyAttributes) { $properties = @{} $Element.Attributes |ForEach-Object {[void]$properties.Add($_.Name,$_.Value)} return [pscustomobject]$properties } elseif($Element.HasChildNodes -and !($Element.ChildNodes.NodeType | Select-Object -Unique | Where-Object {$_ -notin [Xml.XmlNodeType]::Text,[Xml.XmlNodeType]::CDATA})) { return $Element.InnerText } elseif($SimpleSuccession -and ($Element.Attributes.Count -eq 0) -and (($Element.SelectNodes('*') |Group-Object Name |Measure-Object).Count -eq 1)) { return @($Element.SelectNodes('*') |ConvertFrom-XmlElement) } else { $properties = @{} $Element.Attributes |ForEach-Object {[void]$properties.Add($_.Name,$_.Value)} foreach($node in $Element.ChildNodes |Where-Object {$_.Name -and $_.Name -ne '#whitespace'}) { $subelements = $node.SelectNodes('*') |Group-Object Name $value = if($node.InnerText -and !$subelements) { $node.InnerText } elseif(($subelements |Measure-Object).Count -eq 1) { $subelement = $node.SelectSingleNode('*') [pscustomobject]@{$subelement.Name=@($subelement |ConvertFrom-XmlElement)} } else { ConvertFrom-XmlElement $node } if(!$properties.Contains($node.Name)) { # new property [void]$properties.Add($node.Name,$value) } else { # property name collision! if($properties[$node.Name] -isnot [Collections.Generic.List[object]]) { $properties[$node.Name] = ([Collections.Generic.List[object]]@($properties[$node.Name],$value)) } else { $properties[$node.Name].Add($value) } } } return [pscustomobject]$properties } } } } } function ConvertTo-XmlElements { <# .SYNOPSIS Serializes complex content into XML elements. .INPUTS System.Object (any object) to serialize. .OUTPUTS System.String for each XML-serialized value or property. .FUNCTIONALITY XML .EXAMPLE ConvertTo-XmlElements @{html=@{body=@{p='Some text.'}}} -SkipRoot <html> <body> <p>Some text.</p> </body> </html> .EXAMPLE [pscustomobject]@{UserName='zaphodb';Computer='eddie'} |ConvertTo-XmlElements <PSCustomObject> <UserName>zaphodb</UserName> <Computer>eddie</Computer> </PSCustomObject> .EXAMPLE '{"item": {"name": "Test", "id": 1 } }' |ConvertFrom-Json |ConvertTo-XmlElements -SkipRoot <item> <name>Test</name> <id>1</id> </item> #> [CmdletBinding()][OutputType([string])] Param( <# A hash or XML element or other object to be serialized as XML elements. Each hash value or object property value may itself be a hash or object or XML element. #> [Parameter(Position=0,ValueFromPipeline=$true)] $InputObject, <# Specifies how many levels of contained objects are included in the JSON representation. The value can be any number from 0 to 100. The default value is 2. ConvertTo-Json emits a warning if the number of levels in an input object exceeds this number. #> [ValidateRange('NonNegative')][int] $Depth = 3, # Do not wrap the input in a root element. [switch] $SkipRoot ) Begin { $Script:OFS = [Environment]::NewLine function ConvertTo-CompoundXmlElement { [CmdletBinding()] Param( [Parameter(ValueFromPipeline=$true)][string] $PropertyName, [switch] $IsFirst ) Begin {if(!$IsFirst) {Write-Output ''}} # adds OFS Process { $name = $PropertyName -replace '\[\]','Array' -replace '\A\W+','_' -replace '\W+','-' Write-Output "<$name>$(ConvertTo-XmlElement $InputObject.$PropertyName -Depth ($Depth-1))</$name>" } End {if(!$IsFirst) {Write-Output ''}} # adds OFS } function ConvertTo-SimpleXmlElement { [CmdletBinding()] Param( [Parameter(ValueFromPipeline=$true)][string] $PropertyName, [switch] $IsFirst ) Begin {if(!$IsFirst) {Write-Output ''}} # adds OFS Process { $name = $PropertyName -replace '\[\]','Array' -replace '\A\W+','_' -replace '\W+','-' Write-Output "<$name>$($InputObject.$PropertyName)</$name>" } End {if(!$IsFirst) {Write-Output ''}} # adds OFS } filter ConvertTo-XmlElement { [CmdletBinding()][OutputType([string])] Param( [Parameter(Position=0,ValueFromPipeline=$true)] $InputObject, [ValidateRange('NonNegative')][int] $Depth = 3, [switch] $IsFirst, [switch] $SkipRoot # not used here ) if($null -eq $InputObject) {return '<null />'} elseif($InputObject -is [DBNull]) {return '<DBNull />'} elseif($InputObject -is [Array]) { $InputObject |ConvertTo-XmlElement |ForEach-Object -Begin {''} -Process {"<Item>$_</Item>"} -End {''} } elseif([bool],[byte],[DateTimeOffset],[decimal],[double],[float],[guid],[int],[int16],[long],[sbyte],[timespan],[uint16],[uint32],[uint64] -contains $InputObject.GetType()) { [Xml.XmlConvert]::ToString($InputObject) } elseif($InputObject -is [datetime]) { [Xml.XmlConvert]::ToString($InputObject,'yyyy-MM-dd\THH:mm:ss') } elseif($InputObject -is [string] -or $InputObject -is [char]) { [Security.SecurityElement]::Escape($InputObject) } elseif($InputObject -is [Hashtable] -or $InputObject -is [Collections.Specialized.OrderedDictionary]) { if($Depth -gt 1) {$InputObject.Keys |ConvertTo-CompoundXmlElement -IsFirst:$IsFirst} else {$InputObject.Keys |ConvertTo-SimpleXmlElement -IsFirst:$IsFirst} } elseif($InputObject -is [PSObject]) { if(!@($InputObject.PSObject.Properties)) {return} elseif($Depth -gt 1) {$InputObject.PSObject.Properties.Name |ConvertTo-CompoundXmlElement -IsFirst:$IsFirst} else {$InputObject.PSObject.Properties.Name |ConvertTo-SimpleXmlElement -IsFirst:$IsFirst} } elseif($InputObject -is [xml]) { $InputObject.OuterXml } else { if(!@($InputObject.PSObject.Properties)) {return} elseif($Depth -gt 1) { $InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name | ConvertTo-CompoundXmlElement -IsFirst:$IsFirst } else { $InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name | ConvertTo-SimpleXmlElement -IsFirst:$IsFirst } } } } Process { if($null -eq $InputObject) {return '<null />'} elseif($InputObject -is [DBNull]) {return '<DBNull />'} elseif($SkipRoot) {return "$(ConvertTo-XmlElement @PSBoundParameters -IsFirst)"} else { $root = $InputObject.GetType().Name -replace '\[\]','Array' -replace '\A\W+','_' -replace '\W+','-' return "<$root>$(ConvertTo-XmlElement @PSBoundParameters)</$root>" } } } function Format-Xml { <# .SYNOPSIS Pretty-print XML. .INPUTS System.Xml.XmlDocument to serialize. .OUTPUTS System.String containing the serialized XML document with desired indents. .FUNCTIONALITY XML .EXAMPLE Get-PSProvider alias |ConvertTo-Xml |Format-Xml <Objects> <Object Type="System.Management.Automation.ProviderInfo"> <Property Name="ImplementingType" Type="System.RuntimeType">Microsoft.PowerShell.Commands.AliasProvider</Property> <Property Name="HelpFile" Type="System.String">System.Management.Automation.dll-Help.xml</Property> <Property Name="Name" Type="System.String">Alias</Property> <Property Name="PSSnapIn" Type="System.Management.Automation.PSSnapInInfo">Microsoft.PowerShell.Core</Property> <Property Name="ModuleName" Type="System.String">Microsoft.PowerShell.Core</Property> <Property Name="Module" Type="System.Management.Automation.PSModuleInfo" /> <Property Name="Description" Type="System.String"></Property> <Property Name="Capabilities" Type="System.Management.Automation.Provider.ProviderCapabilities">ShouldProcess</Property> <Property Name="Home" Type="System.String"></Property> <Property Name="Drives" Type="System.Collections.ObjectModel.Collection`1[System.Management.Automation.PSDriveInfo]"> <Property Type="System.Management.Automation.PSDriveInfo">Alias</Property> </Property> </Object> </Objects> .EXAMPLE Get-PSProvider alias |ConvertTo-Xml |Format-Xml -NewLineOnAttributes <Objects> <Object Type="System.Management.Automation.ProviderInfo"> <Property Name="ImplementingType" Type="System.RuntimeType">Microsoft.PowerShell.Commands.AliasProvider</Property> <Property Name="HelpFile" Type="System.String">System.Management.Automation.dll-Help.xml</Property> <Property Name="Name" Type="System.String">Alias</Property> <Property Name="PSSnapIn" Type="System.Management.Automation.PSSnapInInfo">Microsoft.PowerShell.Core</Property> <Property Name="ModuleName" Type="System.String">Microsoft.PowerShell.Core</Property> <Property Name="Module" Type="System.Management.Automation.PSModuleInfo" /> <Property Name="Description" Type="System.String"></Property> <Property Name="Capabilities" Type="System.Management.Automation.Provider.ProviderCapabilities">ShouldProcess</Property> <Property Name="Home" Type="System.String"></Property> <Property Name="Drives" Type="System.Collections.ObjectModel.Collection`1[System.Management.Automation.PSDriveInfo]"> <Property Type="System.Management.Automation.PSDriveInfo">Alias</Property> </Property> </Object> </Objects> #> [CmdletBinding()][OutputType([string])] Param( # The XML string or document to format. [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)][xml] $Xml, # A whitespace indent character to use, space by default. [ValidatePattern('\A\s\z',ErrorMessage='A whitespace character is required')] [char] $IndentChar = ' ', <# The number of IndentChars to use per level of indent, 2 by default. Set to zero for no indentation. #> [int] $Indentation = 2, # Indicates attributes should be written on a new line. [Alias('SplitAttributes','AttributesSeparated')][switch] $NewLineOnAttributes ) Process { [Xml.XmlWriterSettings] $cfg = New-Object Xml.XmlWriterSettings -Property @{ Indent = !!$Indentation IndentChars = "$IndentChar" * $Indentation OmitXmlDeclaration = $true NewLineOnAttributes = $NewLineOnAttributes } $sw = New-Object IO.StringWriter [Xml.XmlWriter] $xw = [Xml.XmlWriter]::Create($sw, $cfg) $Xml.WriteTo($xw) $xw.Dispose() $sw.ToString() $sw.Dispose() } } function Get-XmlNamespaces { <# .SYNOPSIS Gets the namespaces from a document as a dictionary. .OUTPUTS System.Collections.Generic.Dictionary[System.String,System.String] containing namespace prefixes as keys and namespace URIs as values. .FUNCTIONALITY XML .LINK https://stackoverflow.com/a/26786080/54323 .EXAMPLE Select-Xml /xsl:transform .\dataref.xslt -Namespace (Get-XmlNamespaces .\dataref.xslt) Node Path Pattern ---- ---- ------- transform C:\Users\brian\GitHub\scripts\dataref.xslt /xsl:transform .EXAMPLE Get-XmlNamespaces .\dataref.xslt Key Value --- ----- xml http://www.w3.org/XML/1998/namespace xsl http://www.w3.org/1999/XSL/Transform xs http://www.w3.org/2001/XMLSchema x urn:guid:f203a737-cebb-419d-9fbe-a684f1f13591 wsdl http://schemas.xmlsoap.org/wsdl/ http://www.w3.org/1999/xhtml #> [CmdletBinding()][OutputType([Collections.Generic.Dictionary[string,string]])] Param( # The XML file. [Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)][Alias('FullName')][string]$Path ) Process { $doc = [xml](Get-Content $Path -Raw) $nav = $doc.DocumentElement.CreateNavigator() [void]$nav.MoveToFollowing('Element') $nav.GetNamespacesInScope('All') } } function Merge-XmlSelections { <# .SYNOPSIS Builds an object using the named XPath selections as properties. .INPUTS System.Xml.XmlNode of XML or System.String of XML file names to select property values from. .OUTPUTS System.Management.Automation.PSCustomObject object with the selected properties. .FUNCTIONALITY XML .LINK https://github.com/brianary/Detextive/ .EXAMPLE Merge-XmlSelections @{Version='/*/@version';Format='/xsl:output/@method'} *.xsl* -Namespace @{xsl='http://www.w3.org/1999/XSL/Transform'} Path Version Format ---- ------- ------ Z:\Scripts\dataref.xslt 2.0 html Z:\Scripts\xhtml2fo.xsl 1.0 xml #> [CmdletBinding()][OutputType([psobject])] Param( # Any dictionary or hashtable of property name to XPath to select a value with. [Parameter(Position=0,Mandatory=$true)][Collections.IDictionary] $XPaths, # The XML to select the property values from. [Parameter(ParameterSetName='Xml',Position=1,Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] [Xml.XmlNode[]] $Xml, # XML file(s) to select the property values from. [Parameter(ParameterSetName='Path',Position=1,Mandatory=$true,ValueFromPipelineByPropertyName=$true)] [Alias('FullName')][string[]] $Path, # XML namespaces to use in the XPath expressions. [hashtable] $Namespace = $PSDefaultParameterValues['Select-Xml:Namespace'] ) Process { $ns = if($Namespace -and $Namespace.Count) {@{Namespace=$Namespace}} else {@{}} if($PSCmdlet.ParameterSetName -eq 'Xml') { foreach($x in $Xml) { $value = [ordered]@{Path='InputStream';Xml=$x.OwnerDocument} foreach($prop in $XPaths.GetEnumerator()) { $value.Add($prop.Key,($x |Select-Xml $prop.Value @ns | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value)) } [pscustomobject]$value } } else { foreach($f in $Path |Resolve-Path) { $value = [ordered]@{Path=$f} foreach($prop in $XPaths.GetEnumerator()) { $value.Add($prop.Key,(Select-Xml $prop.Value -Path $f @ns | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value)) } [pscustomobject]$value } } } } function New-NamespaceManager { <# .SYNOPSIS Creates an object to lookup XML namespace prefixes. .OUTPUTS System.Xml.XmlNamespaceManager containing the given namespaces. .FUNCTIONALITY XML .LINK https://docs.microsoft.com/dotnet/api/system.xml.xmlnamespacemanager .EXAMPLE $n = New-NamespaceManager; (Select-Xml //xhtml:td dataref.xslt).Node.SelectSingleNode('xhtml:var',$n).OuterXml <var xmlns="http://www.w3.org/1999/xhtml">ANY</var> <var xmlns="http://www.w3.org/1999/xhtml">ANY</var> #> [CmdletBinding()][OutputType([Xml.XmlNamespaceManager])] Param( <# A dictionary of prefixes and their namespace URLs. If a default Namespace value for Select-Xml exists, this will use it. #> [ValidateNotNullOrEmpty()][Collections.IDictionary] $Namespaces = $PSDefaultParameterValues['Select-Xml:Namespace'] ) $value = New-Object Xml.XmlNamespaceManager (New-Object Xml.NameTable) foreach($ns in $Namespaces.GetEnumerator()) { $value.AddNamespace($ns.Name,$ns.Value) } return,$value } function Resolve-XmlSchemaLocation { <# .SYNOPSIS Gets the namespaces and their URIs and URLs from a document. .INPUTS System.Xml.XmlDocument or System.String containing the path to an XML file. .OUTPUTS System.Management.Automation.PSCustomObject for each namespace, with Path, Node, Alias, Urn, and Url properties. .FUNCTIONALITY XML .LINK https://www.w3.org/TR/xmlschema-1/#schema-loc .LINK https://stackoverflow.com/a/26786080/54323 .EXAMPLE Resolve-XmlSchemaLocation test.xml Path : C:\test.xml Node : root Alias : xml Urn : http://www.w3.org/XML/1998/namespace Url : Path : C:\test.xml Node : root Alias : xsi Urn : http://www.w3.org/2001/XMLSchema-instance Url : #> [CmdletBinding()][OutputType([Management.Automation.PSCustomObject])] Param( # The string to check. [Parameter(ParameterSetName='Xml',Position=0,Mandatory=$true,ValueFromPipeline=$true)][xml] $Xml, # A file to check. [Parameter(ParameterSetName='Path',Position=0,Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] [Alias('FullName')][ValidateScript({Test-Path $_ -PathType Leaf})][string] $Path ) Process { $xmlsrc = if($Path) {@{Path=$Path}} else {@{Xml=$Xml}} foreach($element in (Select-Xml '//*[@xsi:schemaLocation]' @xmlsrc -Namespace @{ xsi='http://www.w3.org/2001/XMLSchema-instance'} |Select-Object -ExpandProperty Node)) { $nonsatt = $element.Attributes.GetNamedItem('noNamespaceSchemaLocation', 'http://www.w3.org/2001/XMLSchema-instance') $nons = if($nonsatt) {$nonsatt.Value} [string[]]$locations = $element.Attributes.GetNamedItem('schemaLocation', 'http://www.w3.org/2001/XMLSchema-instance').Value.Trim() -split '\s+' if($locations.Length -band 1) {Write-Warning "XML schemaLocation has $($locations.Length) entries"} $schemaLocation = @{} for($i = 1; $i -lt $locations.Length; $i += 2) { $schemaLocation[$locations[$i-1]] = $locations[$i] } $nav = $element.CreateNavigator() [void]$nav.MoveToFollowing('Element') $ns = $nav.GetNamespacesInScope('All') foreach($ns in $ns.GetEnumerator()) { [pscustomobject]@{ Path = $Path Node = $element Alias = $ns.Key Urn = $ns.Value Url = if($schemaLocation.ContainsKey($ns.Value)) {$schemaLocation[$ns.Value]} else {$nons} } } } } } function Resolve-XPath { <# .SYNOPSIS Returns the XPath of the location of an XML node. .INPUTS System.Xml.XmlNode or property of that type named XmlNode or Node. .OUTPUTS System.Management.Automation.PSCustomObject with the following properties: * XPath: The XPath that locates the node. * Namespace: The namespace table used to select the node. .FUNCTIONALITY XML .LINK https://docs.microsoft.com/dotnet/api/system.xml.xmlnode .EXAMPLE '<a><b c="value"/></a>' |Select-Xml //@c |Resolve-XPath /a/b/@c .EXAMPLE '<a>one<!-- two -->three</a>' |Select-Xml '//text()' |Resolve-XPath /a/text()[1] /a/text()[2] #> [CmdletBinding()][OutputType([string])] Param( # An XML node to retrieve the XPath for. [Alias('Node')][Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] [Xml.XmlNode] $XmlNode ) Begin { function Test-NodeMatch([Xml.XmlNode]$ReferenceNode,[Xml.XmlNode]$DifferenceNode) { return $ReferenceNode.get_NodeType() -eq $DifferenceNode.get_NodeType() -and $ReferenceNode.get_LocalName() -eq $DifferenceNode.get_LocalName() -and $ReferenceNode.get_NamespaceURI() -eq $DifferenceNode.get_NamespaceURI() } function Measure-XmlNodePosition([Parameter(Position=0,Mandatory=$true)][Xml.XmlNode]$Node) { if(!($Node.PreviousSibling -or $Node.NextSibling)) {return} for($i,$n = 0,$Node; $n; $n = $n.PreviousSibling) {if(Test-NodeMatch $n $Node) {$i++}} if($i -gt 1) {return "[$i]"} for($i,$n = 0,$Node.NextSibling; $n; $n = $n.NextSibling) {if(Test-NodeMatch $n $Node) {$i++; break}} if($i -gt 0) {return '[1]'} else {return} } function Get-NodeName([Parameter(Position=0,Mandatory=$true)][Xml.XmlNode]$Node, [Parameter(Position=1)][Collections.Hashtable]$Namespace=@{}) { if($Node.get_NamespaceURI() -and !$Node.get_Prefix() -and ($Node.get_NodeType() -ne 'Attribute' -or $Node.get_NamespaceURI() -ne $Node.OwnerElement.get_NamespaceURI())) { if($Node.get_NamespaceURI() -in $Namespace.Values) { $prefix = $Namespace.GetEnumerator() |Where-Object Value -eq $Node.get_NamespaceURI() |Select-Object -First 1 -exp Key } else { $prefix = ([uri]$Node.get_NamespaceURI()).Segments |Where-Object {$_ -match '\A[A-Za-z]\w*\z'} |Select-Object -Last 1 if(!$prefix) {$prefix = 'ns'} while($Namespace.ContainsKey($prefix)) {$prefix += Get-Random -Maximum 99} $Namespace.Add($prefix,$Node.get_NamespaceURI()) } return $prefix + ':' + $Node.get_LocalName() } else { if($Node.get_NamespaceURI() -and !$Namespace.ContainsKey($Node.get_Prefix())) {$Namespace.Add($Node.get_Prefix(),$Node.get_NamespaceURI())} return $Node.get_Name() } } function Resolve-XmlNode([Parameter(Position=0,Mandatory=$true)][Xml.XmlNode]$Node, [Parameter(Position=1)][Collections.Hashtable]$Namespace=@{}, [switch]$AsObject) { $name = Get-NodeName $Node $Namespace $xpath = switch($Node.get_NodeType()) { Attribute {"$(Resolve-XmlNode $Node.OwnerElement $Namespace)/@$name"} CDATA {"$(Resolve-XmlNode $Node.ParentNode)/text()$(Measure-XmlNodePosition $Node)"} Comment {"$(Resolve-XmlNode $Node.ParentNode)/comment()$(Measure-XmlNodePosition $Node)"} Document {if($AsObject){'/'}else{$null}} Element {"$(Resolve-XmlNode $Node.ParentNode $Namespace)/$name$(Measure-XmlNodePosition $Node)"} ProcessingInstruction { "$(Resolve-XmlNode $Node.ParentNode)/processing-instruction('$name')$(Measure-XmlNodePosition $Node)"} SignificantWhitespace {"$(Resolve-XmlNode $Node.ParentNode)/text()$(Measure-XmlNodePosition $Node)"} Text {"$(Resolve-XmlNode $Node.ParentNode)/text()$(Measure-XmlNodePosition $Node)"} Whitespace {"$(Resolve-XmlNode $Node.ParentNode)/text()$(Measure-XmlNodePosition $Node)"} default {$null} } if($AsObject) {[pscustomobject]@{XPath=$xpath;Namespace=$Namespace}} else {$xpath} } } Process { Resolve-XmlNode $XmlNode -AsObject } } function Test-Xml { <# .SYNOPSIS Try parsing text as XML, and validating it if a schema is provided. .INPUTS System.String containing a file path or potential XML data. .OUTPUTS System.Boolean indicating the XML is parseable, or System.String containing the parse error if -ErrorMessage is present and the XML isn't parseable. .FUNCTIONALITY XML .LINK https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation .LINK https://docs.microsoft.com/dotnet/api/system.xml.xmlresolver .LINK https://docs.microsoft.com/dotnet/api/system.xml.schema.validationeventhandler .LINK Resolve-XmlSchemaLocation .EXAMPLE Test-Xml -Xml '</>' False #> [CmdletBinding()][OutputType([bool])] Param( # A file to check. [Parameter(ParameterSetName='Path',Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)] [ValidateScript({Test-Path $_ -PathType Leaf})][Alias('FullName')][string] $Path, # The string to check. [Parameter(ParameterSetName='Xml',Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] [ValidateScript({!(Test-Path $_ -PathType Leaf)})][string] $Xml, # A hashtable of schema namespaces to schema locations (in addition to the xsi:schemaLocation attribute). [Alias('Schemas')][hashtable] $Schemata, # Indicates that XML Schema validation should not be performed, only XML well-formedness will be checked. [Alias('NoValidation')][switch] $SkipValidation, # Indicates that well-formedness or validation errors will result in warnings being written. [Alias('ShowWarnings')][switch] $Warnings, <# When present, returns the well-formedness or validation error messages instead of a boolean value, or nothing if successful. This effectively reverses the truthiness of the return value. #> [Alias('NotSuccessful')][switch] $ErrorMessage ) Begin { function Add-Schema([Xml.Schema.XmlSchemaSet]$set,[string]$urn,[string]$url) { Write-Verbose "Adding $url for $urn" try {[void]$set.Add($urn,$url)} catch { if(!(Test-Path $url)) {Write-Error "Error adding $url for $urn"} else { $s = [Xml.Schema.XmlSchema]::Read( [IO.File]::OpenRead($url), {throw $_.Exception} ) $s.Compile( { if($_.Severity -eq [Xml.Schema.XmlSeverityType]::Error) {Write-Error $_.Message; throw $_.Exception} else {Write-Warning $_.Message} } ) } } } } Process { if($Path){$Xml= Get-Content $Path -Raw} try{[xml]$x = $Xml} catch [Management.Automation.RuntimeException] { if($Warnings) {Write-Warning $_.Exception.Message} if(!$ErrorMessage) {return $false} else {return $_.Exception.InnerException.InnerException.Message} } if($SkipValidation) {return !$ErrorMessage} #$x.Schemas.XmlResolver = New-Object Xml.XmlUrlResolver # this should be the default, but can't set a base URL # kludgy hack to try and address XmlUrlResolver using env working dir: [Environment]::CurrentDirectory = if($Path) {Resolve-Path $Path |Split-Path} else {$PWD} $xmlsrc = if($Path) {@{Path=$Path}} else {@{Xml=$Xml}} foreach($schema in Resolve-XmlSchemaLocation @xmlsrc |Where-Object {$_.Url}) {Add-Schema $x.Schemas $schema.Urn $schema.Url} if($Schemata) {foreach($schema in $Schemata.GetEnumerator()) {Add-Schema $x.Schemas $schema.Key $schema.Value}} if($x.Schemas.Count -eq 0) {return !$ErrorMessage} $x.Schemas.Schemas().SourceUri |ForEach-Object {Write-Verbose "Added schema $_"} $Script:validationErrors = @() $Script:warn = $Warnings $x.Validate({ if($Script:warn) {Write-Warning $_.Message}; $Script:validationErrors += @($_.Message) }) if($ErrorMessage) {return $Script:validationErrors} else {return !($Script:validationErrors)} } } Export-ModuleMember -Function Compare-Xml,Convert-Xml,ConvertFrom-EscapedXml,ConvertFrom-XmlElement,ConvertTo-XmlElements,Format-Xml,Get-XmlNamespaces,Merge-XmlSelections,New-NamespaceManager,Resolve-XmlSchemaLocation,Resolve-XPath,Test-Xml |