MetaNullWiki.psm1

if (-not ("System.Xml.Linq.XDocument" -as [Type])) {
    Add-Type -Assembly System.Xml.Linq
}

if (-not ("System.Web.HttpUtility" -as [Type])) {
    Add-Type -Assembly System.Web
}

if (-not ("System.Management.Automation.PSCredential" -as [Type])) {
    Add-Type -Assembly System.Management.Automation
}

#Requires -Module ConfluencePS
#Requires -Module MetaNullUtils
# Module Constants

Set-Variable CWikiNamespaces -option Constant -value @('ac','ri')
Function Clear-Placeholder {
<#
    .Synopsis
        Remove any Wiki placeholders (template values, that are not displayed in view mode) from a piece of html
    .PARAMETER InputHtml
        The HTML input
    .EXAMPLE
        $Body | Select-Placeholder
#>

[CmdletBinding(SupportsShouldProcess=$false)]
[OutputType([string])]
param (
    [Parameter(Mandatory, Position=0, ValueFromPipeline)]
    [Alias('Html','String')]
    [string]$InputHtml
)
Process {
    $InputHtml -replace '<(ac:placeholder\b).*?>.*?</\1>' | Write-Output
}
}
Function ConvertFrom-WikiArtifact {
<#
    .SYNOPSIS
        Convert a WikiArtifact object into the best matching type of artifact
    .Description
        Convert a WikiArtifact object into the best matching type of artifact. The function checks the presence of specific Page Properties OR specific Labels
    .PARAMETER InputArtifact
        The input object.
#>

[CmdletBinding()]
[OutputType(
    [WikiArtifact],
    [WikiDomain],
    [WikiProduct],
    [WikiContainer],
    [WikiTechnology],
    [WikiKeyDataEntity]
    )
]
param (
    [Parameter(ValueFromPipeline, Position=0)]
    [AllowEmptyString()]
    [Alias('Artifact','Product','Container','Technology','KeyDataEntity','Object')]
    [WikiArtifact]$InputArtifact
)
Process {
    if($InputArtifact.Properties.Id -contains 'info' -or $InputArtifact.Labels -contains 'product') {
        [WikiProduct]::new($InputArtifact) | Write-Output
    } elseif($InputArtifact.Properties.Id -contains 'container' -or $InputArtifact.Labels -contains 'container') {
        [WikiContainer]::new($InputArtifact) | Write-Output
    } elseif($InputArtifact.Properties.Id -contains 'technology' -or $InputArtifact.Labels -contains 'technology') {
        [WikiTechnology]::new($InputArtifact) | Write-Output
    } elseif($InputArtifact.Properties.Id -contains 'domain' -or $InputArtifact.Labels -contains 'domain') {
        [WikiDomain]::new($InputArtifact) | Write-Output
    } elseif($InputArtifact.Properties.Id -contains [string]::Empty -or $InputArtifact.Labels -contains 'key-data-entity') {
        [WikiKeyDataEntity]::new($InputArtifact) | Write-Output
    } elseif($InputArtifact.Properties.Length -gt 0) {
        $InputArtifact | Write-Output
    } else {
        # Nothing to return, Input Object was probably not a valid artifact
    }
}
}
Function ConvertTo-XDocument {
<#
    .SYNOPSIS
        Convert a piece of html into a XDocument
    .Description
        Convert a piece of (well formed) html into a XDocument
    .PARAMETER InputString
        The input string. It must contain a valid piece of XHTML
#>

[CmdletBinding()]
[OutputType([System.Xml.Linq.XDocument])]
param (
    [Parameter(ValueFromPipeline, Position=0)]
    [AllowEmptyString()]
    [Alias('String','Html')]
    [String]$InputString
)
Process {
    ConvertTo-UtilsXDocument -InputString $InputString -Namespaces $CWikiNamespaces | Write-Output
}
}
Function Select-HyperLink {
<#
    .Synopsis
        Select the title and uri of html hyperlinks
    .PARAMETER InputHtml
        The HTML input
    .EXAMPLE
        # Get all hyperlinks in a html page
        $Body | Select-Hyperlink
#>

[CmdletBinding()]
[OutputType([string[]])]
param (
    [Parameter(Mandatory, Position=0, ValueFromPipeline)]
    [Alias('Html','String')]
    [string]$InputHtml
)
Process {
    $InputHtml | ConvertTo-XDocument | Select-Xpath -XPath '//a[@href]' | Foreach-Object {
        $Href = $_ | Select-XPath -XPath './@href' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
        $Node = $_ | Select-XPath -XPath '.' | Select-Object -ExpandProperty Node
        $Body = $Node | Select-Object -ExpandProperty InnerXml
        $Text = $Node | Select-Object -ExpandProperty InnerText
        [PSCustomObject]@{
            Href = $Href
            Text = $Text
            Body = $Body
        } | Write-Output
    }
}
}
Function Select-LinkCard {
<#
    .Synopsis
        Select the "titles" of all wiki link cards in a blob of (html) text
    .PARAMETER InputHtml
        The HTML input
    .EXAMPLE
        # Get the titles from all link cards
        $Body | Select-LinkCard
#>

[CmdletBinding()]
[OutputType([string[]])]
param (
    [Parameter(Mandatory, Position=0, ValueFromPipeline)]
    [Alias('Html','String')]
    [string]$InputHtml
)
Begin {
    $xpathSelectors = @(
        "//ac:link/ri:page/@ri:content-title"
    )
    $xpathExpression = "($($xpathSelectors -join ' | '))"
}
Process {
    $InputHtml | ConvertTo-XDocument | Select-Xpath -XPath $xpathExpression | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value | Write-Output
}
}
Function Select-Macro {
<#
    .Synopsis
        Select the content of all excerpt macros in a wiki page (html)
 
    .EXAMPLE
        # Get the body from all exerpt macros
        $Body | Select-Excerpt
#>

[CmdletBinding()]
[OutputType([MetaNullWiki.Macro[]])]
param (
    # Hashtable to represent
    [Parameter(Mandatory, Position=0, ValueFromPipeline)]
    [Alias('Html','String')]
    [string]$InputHtml
)
Process {
    $Macros = $InputHtml | ConvertTo-XDocument | Select-Xpath -XPath '//ac:structured-macro'
    $Macros | ForEach-Object {
        $Object = [MetaNullWiki.Macro]::new()

        $Macro = $_
        $Object.Name = $Macro | Select-Xpath -XPath '@ac:name[position()=1]' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
        $Object.Parameters = @{}
        $Macro | Select-Xpath -XPath 'ac:parameter' | Select-Object -ExpandProperty Node | Select-Object name,InnerText | Foreach-Object {
            $Object.Parameters.($_.name) = $_.InnerText
        }
        $Object.Body = $Macro | Select-Xpath -XPath 'ac:rich-text-body' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerXml
        $Object.Attributes = @{}
        $Macro | Select-Xpath -XPath '@*' | Select-Object -ExpandProperty Node | Select-Object Name,Value | Foreach-Object {
            $Object.Attributes.($_.Name) = $_.Value
        }
        $Object | Write-Output
    }
}
}
Function Select-Placeholder {
<#
    .Synopsis
        Select the content of all Wiki placeholders (template values, that are not displayed in view mode)
    .PARAMETER InputHtml
        The HTML input
    .EXAMPLE
        $Body | Select-Placeholder
#>

[CmdletBinding()]
[OutputType([string[]])]
param (
    [Parameter(Mandatory, Position=0, ValueFromPipeline)]
    [Alias('Html','String')]
    [string]$InputHtml
)
Begin {
    $xpathExpression = "//ac:placeholder"
}
Process {
    $InputHtml | ConvertTo-XDocument | Select-Xpath -XPath $xpathExpression | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerXml | Write-Output
}
}
Function Select-Text {
<#
    .Synopsis
        Select the content of all text nodes in a blob of (html) text
    .PARAMETER InputHtml
        The HTML input
    .EXAMPLE
        # Extract the piexes of text from (x|ht)ml
        '<strong>Meta<u>Null</u></strong><p>was here</p>' | Select-Text
#>

[CmdletBinding()]
[OutputType([string[]])]
param (
    [Parameter(Mandatory, Position=0, ValueFromPipeline)]
    [Alias('Html','String')]
    [string]$InputHtml
)
Begin {
    $xpathSelectors = @(
        "/xml//*[local-name()=name()]"
        "//ac:link/ac:link-body"
        "//ac:structured-macro[@ac:name='status']/ac:parameter[@ac:name='title']"
    )
    $xpathExpression = "($($xpathSelectors -join ' | '))/text()[normalize-space()]"
}
Process {
    $InputHtml | ConvertTo-XDocument | Select-Xpath -XPath $xpathExpression | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerText | Write-Output
}
}
Function Select-VersionLinkCard {
<#
    .Synopsis
        Select "Versions", representred by a wiki link card and a free text version number in (html) text
    .PARAMETER InputHtml
        The HTML input
    .EXAMPLE
        # Get the titles from all link cards
        $Body | Select-VersionLinkCard
#>

[CmdletBinding()]
[OutputType([WikiVersionLink[]])]
param (
    [Parameter(Mandatory, Position=0, ValueFromPipeline)]
    [Alias('Html','String')]
    [string]$InputHtml
)
Process {
    [WikiVersionLink]::CreateFromHtml($InputHtml)
}
}
Function Select-Xpath {
<#
    .SYNOPSIS
        Use XPath on a blob of (xhtml) text
    .Description
        Use XPath on a blob of (xhtml) text.
    .PARAMETER InputDocument
        The input XML onwhich to run xpath
    .PARAMETER Xpath
        The xpath query string
    .PARAMETER Namespaces
        A list of additional NS to declare
    .EXAMPLE
        # Get URL from all hyperlinks in a page
        $Html | ConvertTo-XDocument | Select-Xpath -XPath '//a/@href/text()'
#>

[CmdletBinding(DefaultParameterSetName)]
[OutputType([object[]])]
param (
    [Parameter(Mandatory, Position=0)]
    [string]$XPath,

    [Parameter(ValueFromPipeline,Mandatory,Position=1)]
    [Alias('Document','Xml','Node')]
    [object]$InputDocument
)
Process {
    $InputDocument | Select-UtilsXpath -Namespaces $CWikiNamespaces -XPath $Xpath | Write-Output
}
}
class WikiArtifact {
    [object]$Page
    [WikiPageProperties[]]$Properties
    [string[]]$Labels
    [string[]]$LinkCards

    WikiArtifact() {
        $this.InitFromHashtable(@{})
    }
    WikiArtifact([object]$Page) {
        $this.InitFromHashtable(@{Page = $Page})
    }
    WikiArtifact([object]$Page,[string[]]$Labels) {
        $this.InitFromHashtable(@{Page = $Page; Labels = $Labels})
    }
    WikiArtifact([object]$Page,[string[]]$Labels,[WikiPageProperties[]]$Properties) {
        $this.InitFromHashtable(@{Page = $Page; Labels = $Labels; Properties = $Properties})
    }
    WikiArtifact([WikiArtifact]$Artifact) {
        $this.InitFromHashtable(@{Page = $Artifact.Page; Labels = $Artifact.Labels; Properties = $Artifact.Properties})
    }

    [void] SetPage([object]$Page) {
        if($null -ne $Page) {
            $this.Page = $Page.PSObject.Copy()
        }
    }
    [void] SetProperties([WikiPageProperties[]]$Properties) {
        if($null -ne $Properties) {
            $this.Properties = $Properties.Clone()
        } else {
            $this.Properties = @()
        }
    }
    [void] SetLabels([string[]]$Labels) {
        if($null -ne $Labels) {
            $this.Labels = $Labels.Clone()
        } else {
            $this.Labels = @()
        }
    }
    [void] SetLinkCards([string[]]$LinkCards) {
        if($null -ne $LinkCards) {
            $this.LinkCards = $LinkCards.Clone()
        } else {
            $this.LinkCards = @()
        }
    }

    [void] InitFromHashtable([hashtable]$properties) {
        foreach ($Property in $Properties.Keys) {
            switch($Property) {
                'Page' {
                    $this.SetPage($Properties.Page)
                    if($properties.Keys -notcontains 'Properties') {
                        # Load 'properties' from the page, unless if they are explicitly provided
                        $this.LoadPageProperties()
                        $this.LoadLinkCards()
                    }
                }
                'Properties' {
                    $this.SetProperties($Properties.Properties)
                }
                'Labels' {
                    $this.SetLabels($Properties.Labels)
                }
                'LinkCards' {
                    $this.SetLinkCards($Properties.LinkCards)
                }
                default {
                    [System.ArgumentException]::New("No such property: $($Property).")
                }
            }
        }
    }

    [WikiArtifact] Clone() {
        return $this.PSObject.Copy()
    }

    [void] LoadPageProperties() {
        if($this.Page) {
            $this.Properties = [WikiPageProperties]::CreateFromMacros([WikiMacro]::CreateFromHtml($this.Page.Body))
        } else {
            $this.Properties = [WikiPageProperties[]]@()
        }
    }

    [void] LoadLinkCards() {
        if($this.Page) {
            $this.LinkCards = $this.Page.Body | Select-LinkCard | Select-Object -Unique | Sort-Object
        } else {
            $this.LinkCards = [string[]]@()
        }
    }

    [WikiPageProperties[]] GetPageProperties([string]$Id) {
        return $this.Properties | Where-Object { $_.Id -eq $Id }
    }

    [string] ToString() {
        return "[$($this.Page.ID)] [$($this.GetType().Name -replace '^Wiki')] $($this.Page.Title)"
    }
}
class WikiContainer : WikiArtifact {

    WikiContainer() : base() {}
    WikiContainer([object]$Page) : base($Page) {}
    WikiContainer([object]$Page,[string[]]$Labels) : base($Page,$Labels) {}
    WikiContainer([object]$Page,[string[]]$Labels,[WikiPageProperties[]]$Properties) : base($Page,$Labels,$Properties) {}
    WikiContainer([WikiArtifact]$Artifact) : base($Artifact) {}
    WikiContainer([WikiContainer]$Container) : base($Container.Page,$Container.Labels,$Container.Properties) {}
}
class WikiDomain : WikiArtifact {

    WikiDomain() : base() {}
    WikiDomain([object]$Page) : base($Page) {}
    WikiDomain([object]$Page,[string[]]$Labels) : base($Page,$Labels) {}
    WikiDomain([object]$Page,[string[]]$Labels,[WikiPageProperties[]]$Properties) : base($Page,$Labels,$Properties) {}
    WikiDomain([WikiArtifact]$Artifact) : base($Artifact) {}
    WikiDomain([WikiDomain]$Container) : base($Container.Page,$Container.Labels,$Container.Properties) {}
}
class WikiKeyDataEntity : WikiArtifact {

    WikiKeyDataEntity() : base() {}
    WikiKeyDataEntity([object]$Page) : base($Page) {}
    WikiKeyDataEntity([object]$Page,[string[]]$Labels) : base($Page,$Labels) {}
    WikiKeyDataEntity([object]$Page,[string[]]$Labels,[WikiPageProperties[]]$Properties) : base($Page,$Labels,$Properties) {}
    WikiKeyDataEntity([WikiArtifact]$Artifact) : base($Artifact) {}
    WikiKeyDataEntity([WikiKeyDataEntity]$Container) : base($Container.Page,$Container.Labels,$Container.Properties) {}
}
class WikiMacro {
    [string]$Name
    [hashtable]$Parameters
    [hashtable]$Attributes
    [string]$Body

    WikiMacro() {
        $this.InitFromHashtable(@{})
    }
    WikiMacro([string]$Name,[hashtable]$Parameters,[hashtable]$Attributes,[string]$Body) {
        $this.InitFromHashtable(@{Name = $Name; Parameters = $Parameters; Attributes = $Attributes; Body = $Body })
    }
    WikiMacro([string]$Name,[hashtable]$Parameters,[hashtable]$Attributes) {
        $this.InitFromHashtable(@{Name = $Name; Parameters = $Parameters; Attributes = $Attributes })
    }
    WikiMacro([string]$Name,[hashtable]$Parameters,[string]$Body) {
        $this.InitFromHashtable(@{Name = $Name; Parameters = $Parameters; Body = $Body })
    }
    WikiMacro([string]$Name, [string]$Body) {
        $this.InitFromHashtable(@{Name = $Name; Body = $Body })
    }
    WikiMacro([string]$Name,[hashtable]$Parameters) {
        $this.InitFromHashtable(@{Name = $Name; Parameters = $Parameters})
    }

    [void] SetName([string]$Name) {
        $this.Name = $Name
    }
    [void] SetParameters([hashtable]$Parameters) {
        $this.Parameters = $Parameters.Clone()
    }
    [void] SetAttributes([hashtable]$Attributes) {
        $this.Attributes = $Attributes.Clone()
    }
    [void] SetBody([string]$Body) {
        $this.Body = $Body
    }
    
    [void] InitFromHashtable([hashtable]$properties) {
        foreach ($Property in $Properties.Keys) {
            if($Properties.$Property -is [hashtable]) {
                $this.$Property = $Properties.$Property.Clone()
            } else {
                $this.$Property = $Properties.$Property
            }
        }
    }

    [WikiMacro] Clone() {
        return $this.PSObject.Copy()
    }


    static [WikiMacro[]] CreateFromHtml([string]$Html) {
        return [WikiMacro]::CreateFromXDocument(($Html | ConvertTo-XDocument))
    }

    static [WikiMacro[]] CreateFromXDocument([object]$Document) {
        $Output = @()

        $Document | Select-Xpath -XPath '//ac:structured-macro' | Foreach-Object {
            $Name = $_ | Select-Xpath -XPath './@ac:name' | Select-Object -ExpandProperty Node -First 1 | Select-Object -ExpandProperty '#text'
            $Attributes = @{}
            $_ | Select-Xpath -XPath './@*' | Foreach-Object {
                $Attributes += @{  $_.Node.Name = $_.Node.Value }
            }
            $Parameters = @{}
            $_ | Select-Xpath -XPath './ac:parameter' | Foreach-Object {
                $Parameters += @{  $_.Node.Name = $_.Node.InnerXml }
            }
            $Body = $_ | Select-Xpath -XPath './ac:rich-text-body' | Select-Object -ExpandProperty Node -First 1 | Select-Object -ExpandProperty InnerXml
            $Output += ([WikiMacro]::new($Name,$Parameters,$Attributes,$Body))
        }
        return [WikiMacro[]]($Output)
    }

    [string] ToString() {
        return "[Macro] Name:$($this.Name), Parameters:{$(($this.Parameters.Keys -join ', '))}, Body:$($this.Body)"
    }

}
class WikiPageProperties {
    [string]$Id
    [hashtable]$Properties
    [WikiMacro]$Macro

    WikiPageProperties() {
        $this.InitFromHashtable(@{})
    }
    WikiPageProperties([hashtable]$Properties) {
        $this.InitFromHashtable(@{Properties = $Properties })
    }
    WikiPageProperties([hashtable]$Properties, [string] $Id) {
        $this.InitFromHashtable(@{Properties = $Properties; Id = $Id })
    }
    WikiPageProperties([string]$Id, [hashtable]$Properties, [WikiMacro]$Macro) {
        $this.InitFromHashtable(@{Properties = $Properties; Id = $Id; Macro = $Macro })
    }


    [void] SetId([string]$Id) {
        $this.Id = $Id
    }
    [void] SetProperties([hashtable]$Properties) {
        $this.Properties = $Properties.Clone()
    }
    [void] SetMacro([string]$Macro) {
        $this.Macro = $Macro.Clone()
    }


    [void] InitFromHashtable([hashtable]$properties) {
        foreach ($Property in $Properties.Keys) {
            switch($Property) {
                'Macro' {
                    $this.$Property = $Properties.$Property.Clone()
                }
                'Properties' {
                    $this.$Property = $Properties.$Property.Clone()    
                }
                'Id' {
                    $this.$Property = $Properties.$Property    
                }
                default {
                    [System.ArgumentException]::New("No such property: $($Property).")
                }
            }
        }
    }

    [WikiPageProperties] Clone() {
        return $this.PSObject.Copy()
    }


    static [WikiPageProperties[]] CreateFromHtml([string]$Html) {
        return [WikiPageProperties]::CreateFromXDocument(($Html | ConvertTo-XDocument))
    }
    static [WikiPageProperties[]] CreateFromXDocument([object]$Document) {
        return [WikiPageProperties]::CreateFromMacros([WikiMacro]::CreateFromXDocument($Document))
    }
    static [WikiPageProperties[]] CreateFromMacros([WikiMacro[]]$Macros) {
        $Output = @()

        $Macros | Where-Object {
            $_.Name -eq 'details'
        } | Foreach-Object {
            $Macro = $_.Clone()
            $Id = $_.Parameters.id
            $Properties = @{}
            $_.Body | ConvertTo-XDocument | Select-Xpath '//tr' | Foreach-Object {
                $Key = ($_ | Select-Xpath '(./td|./th)[position()=1]').Node.InnerText
                # $Value = ($_ | Select-Xpath '(./td|./th)[position()=2]/p').Node.InnerXml
                $Value = ($_ | Select-Xpath '(./td|./th)[position()=2]/p' | Remove-Placeholder | Where-Object {$_})
                $Properties += @{ $Key = $Value }
            }
            $Output += [WikiPageProperties]::new($Id,$Properties,$Macro)
        }
        return [WikiPageProperties[]]($Output)
    }

    [string] ToString() {
        return "[PageProperties] Id:$($this.Id), Properties:{$(($this.Properties.Keys -join ', '))}"
    }

}
class WikiProduct : WikiArtifact {

    WikiProduct() : base() {}
    WikiProduct([object]$Page) : base($Page) {}
    WikiProduct([object]$Page,[string[]]$Labels) : base($Page,$Labels) {}
    WikiProduct([object]$Page,[string[]]$Labels,[WikiPageProperties[]]$Properties) : base($Page,$Labels,$Properties) {}
    WikiProduct([WikiArtifact]$Artifact) : base($Artifact) {}
    WikiProduct([WikiProduct]$Container) : base($Container.Page,$Container.Labels,$Container.Properties) {}
}
class WikiTechnology : WikiArtifact {

    WikiTechnology() : base() {}
    WikiTechnology([object]$Page) : base($Page) {}
    WikiTechnology([object]$Page,[string[]]$Labels) : base($Page,$Labels) {}
    WikiTechnology([object]$Page,[string[]]$Labels,[WikiPageProperties[]]$Properties) : base($Page,$Labels,$Properties) {}
    WikiTechnology([WikiArtifact]$Artifact) : base($Artifact) {}
    WikiTechnology([WikiTechnology]$Technology) : base($Technology.Page,$Technology.Labels,$Technology.Properties) {}

    [WikiVersionPriority] AssessVersion([version]$Version) {
        if($null -eq $version -or $version -eq '0.0.0.0') {
            return  [WikiVersionPriority]::Unknown
        }

        if($null -ne $_.VulnerableVersion -and $_.VulnerableVersion -ne '0.0.0.0' -and $Version -le $_.VulnerableVersion) {
            # Smaller than Vulnerable = Vulnerable
            return [WikiVersionPriority]::Vulnerable
        } elseif($null -ne $_.OutdatedVersion -and $_.OutdatedVersion -ne '0.0.0.0' -and $Version -le $_.OutdatedVersion) {
            # Smaller than Outdated = Outdated
            return [WikiVersionPriority]::NotSupported
        }

        if($null -ne $_.RecommendedVersion -and $_.RecommendedVersion -ne '0.0.0.0' -and $Version -ge $_.RecommendedVersion) {
            # Greater than Recommended = Recommended
            return [WikiVersionPriority]::UpToDate
        } elseif($null -ne $_.AcceptedVersion -and $_.AcceptedVersion -ne '0.0.0.0' -and $Version -ge $_.AcceptedVersion) {
            # Greater than Accepted = Accepted
            return [WikiVersionPriority]::Supported
        }

        # In any other case = Outdated
        return  [WikiVersionPriority]::NotSupported
    }
}
class WikiVersionLink {
    [string]$Title
    [version]$Version
    [string]$VersionString
    [string]$Body

    WikiVersionLink() {
        $this.InitFromHashtable(@{})
    }
    WikiVersionLink([string]$Title,[version]$Version) {
        $this.InitFromHashtable(@{Title = $Title; Version = $Version})
    }

    [void] SetTitle([string]$Title) {
        $this.Title = $Title
    }
    [void] SetVersion([version]$Version) {
        $this.Version = $Version.Clone()
    }
    
    [void] InitFromHashtable([hashtable]$properties) {
        foreach ($Property in $Properties.Keys) {
            if($Properties.$Property -is [hashtable]) {
                $this.$Property = $Properties.$Property.Clone()
            } else {
                $this.$Property = $Properties.$Property
            }
        }
    }

    [WikiVersionLink] Clone() {
        return $this.PSObject.Copy()
    }

    static [WikiVersionLink[]] CreateFromHtml([string]$Html) {
        return [WikiVersionLink]::CreateFromXDocument(($Html | ConvertTo-XDocument))
    }

    static [WikiVersionLink] CreateFromCurrentNode([object]$Node) {
        $Output = [WikiVersionLink]::empty
        $Node | Select-Xpath -XPath 'self::ac:link' | Foreach-Object {
            $Title = $_ | Select-XPath './ri:page/@ri:content-title' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
            $VersionString = $_ | Select-XPath '(./following-sibling::text()[1]|./following-sibling::code/text())' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
            if($VersionString) {
                $VersionString = $VersionString.Trim()
            }
            $Version = ConvertTo-UtilsVersion -InputString $VersionString
            <##> $Body = $_ | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty OuterXml
            $VersionLink = [WikiVersionLink]::new()
            $VersionLink.Title = $Title
            $VersionLink.Version = $Version
            <##> $VersionLink.VersionString = $VersionString
            <##> $VersionLink.Body = $Body
            $Output = $VersionLink
            # $Output += ([WikiVersionLink]::new($Title,$Version))
        }

        return [WikiVersionLink]($Output)
    }

    static [WikiVersionLink[]] CreateFromXDocument([object]$Document) {
        $Output = @()
        $Document | Select-Xpath -XPath '//ac:link' | Foreach-Object { 
            $Output += [WikiVersionLink]::CreateFromCurrentNode($_)
        }
        return $Output
    }

    [string] ToString() {
        return "[VersionLink] Link:$($this.Title), Version:$($this.Version), VersionString:$($this.VersionString)"
    }

}
enum WikiVersionPriority {
    UpToDate
    Supported
    NotSupported
    Vulnerable
    Unknown
}