Private/XmlHelpers.ps1

# Loads an XLIFF file into an XmlDocument with whitespace preserved for round-tripping.
function Read-XliffXmlDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    $resolvedPath = Resolve-XliffPath -Path $Path
    $document = [System.Xml.XmlDocument]::new()
    $document.PreserveWhitespace = $true

    $settings = [System.Xml.XmlReaderSettings]::new()
    $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
    $settings.IgnoreWhitespace = $false
    $settings.XmlResolver = $null

    $stream = [System.IO.File]::OpenRead($resolvedPath)
    try {
        $reader = [System.Xml.XmlReader]::Create($stream, $settings)
        try {
            $document.Load($reader)
        } finally {
            $reader.Close()
        }
    } finally {
        $stream.Dispose()
    }

    Assert-XliffDocument -Document $document -Path $resolvedPath
    return $document
}

# Saves an XmlDocument as UTF-8 without BOM and without altering line endings.
function Save-XliffXmlDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument]$Document,

        [Parameter(Mandatory)]
        [string]$Path
    )

    $fullPath = Assert-XliffOutputDirectory -Path $Path
    $settings = [System.Xml.XmlWriterSettings]::new()
    $settings.Encoding = [System.Text.UTF8Encoding]::new($false)
    $settings.Indent = $false
    $settings.NewLineHandling = [System.Xml.NewLineHandling]::None

    $writer = [System.Xml.XmlWriter]::Create($fullPath, $settings)
    try {
        $Document.Save($writer)
    } finally {
        $writer.Close()
    }

    return $fullPath
}

# Verifies that the document root is <xliff version="1.2">.
function Assert-XliffDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument]$Document,

        [string]$Path = '<memory>'
    )

    if (-not $Document.DocumentElement -or $Document.DocumentElement.LocalName -ne 'xliff') {
        throw "'$Path' is not an XLIFF document."
    }

    $version = Get-XliffAttributeValue -Node $Document.DocumentElement -Name 'version'
    if ($version -ne '1.2') {
        throw "'$Path' uses XLIFF version '$version'. XliffParser currently supports XLIFF 1.2."
    }
}

function Select-XliffNodes {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlNode]$Node,

        [Parameter(Mandatory)]
        [string]$XPath,

        [Parameter(Mandatory)]
        [System.Xml.XmlNamespaceManager]$NamespaceManager
    )

    return $Node.SelectNodes($XPath, $NamespaceManager)
}

function Select-XliffSingleNode {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlNode]$Node,

        [Parameter(Mandatory)]
        [string]$XPath,

        [Parameter(Mandatory)]
        [System.Xml.XmlNamespaceManager]$NamespaceManager
    )

    return $Node.SelectSingleNode($XPath, $NamespaceManager)
}

function Get-XliffFileNode {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlNode]$Node
    )

    $current = $Node
    while ($current) {
        if ($current.LocalName -eq 'file') {
            return $current
        }

        $current = $current.ParentNode
    }

    return $null
}

function Get-XliffTranslationUnitNodes {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument]$Document
    )

    $namespaceManager = New-XliffNamespaceManager -Document $Document
    return Select-XliffNodes -Node $Document -XPath '//xlf:trans-unit' -NamespaceManager $namespaceManager
}

function Get-XliffChildElement {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlNode]$Node,

        [Parameter(Mandatory)]
        [string]$LocalName
    )

    foreach ($child in $Node.ChildNodes) {
        if ($child.NodeType -eq [System.Xml.XmlNodeType]::Element -and $child.LocalName -eq $LocalName) {
            return $child
        }
    }

    return $null
}

function Get-XliffElementText {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [System.Xml.XmlNode]$Node
    )

    if (-not $Node) {
        return $null
    }

    return $Node.InnerText
}

function Ensure-XliffChildElement {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement]$Parent,

        [Parameter(Mandatory)]
        [string]$LocalName
    )

    $existing = Get-XliffChildElement -Node $Parent -LocalName $LocalName
    if ($existing) {
        return [System.Xml.XmlElement]$existing
    }

    $namespaceUri = $Parent.NamespaceURI
    $element = $Parent.OwnerDocument.CreateElement($Parent.Prefix, $LocalName, $namespaceUri)

    if ($LocalName -eq 'target') {
        $source = Get-XliffChildElement -Node $Parent -LocalName 'source'
        if ($source -and $source.NextSibling) {
            [void]$Parent.InsertAfter($element, $source)
            return $element
        }

        if ($source) {
            [void]$Parent.AppendChild($element)
            return $element
        }
    }

    [void]$Parent.AppendChild($element)
    return $element
}

function Set-XliffUnitTarget {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement]$UnitNode,

        [AllowNull()]
        [string]$Target
    )

    $targetNode = Ensure-XliffChildElement -Parent $UnitNode -LocalName 'target'
    $targetNode.InnerText = if ($null -eq $Target) { '' } else { $Target }
    return $targetNode
}

function Set-XliffUnitState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement]$UnitNode,

        [AllowNull()]
        [string]$State
    )

    $targetNode = Ensure-XliffChildElement -Parent $UnitNode -LocalName 'target'
    if ([string]::IsNullOrWhiteSpace($State)) {
        $targetNode.RemoveAttribute('state')
    } else {
        $targetNode.SetAttribute('state', $State)
    }
}

# Converts a <trans-unit> XML node into an XliffTranslationUnit with hidden XML references.
function ConvertTo-XliffTranslationUnit {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement]$UnitNode,

        [Parameter(Mandatory)]
        [System.Xml.XmlDocument]$Document,

        [string]$Path
    )

    $fileNode = Get-XliffFileNode -Node $UnitNode
    $sourceNode = Get-XliffChildElement -Node $UnitNode -LocalName 'source'
    $targetNode = Get-XliffChildElement -Node $UnitNode -LocalName 'target'
    $noteNodes = @($UnitNode.ChildNodes | Where-Object {
            $_.NodeType -eq [System.Xml.XmlNodeType]::Element -and $_.LocalName -eq 'note'
        })

    $developerNote = $noteNodes | Where-Object {
        (Get-XliffAttributeValue -Node $_ -Name 'from') -eq 'Developer'
    } | Select-Object -First 1

    $note = if ($developerNote) {
        $developerNote.InnerText
    } elseif ($noteNodes.Count -gt 0) {
        ($noteNodes | ForEach-Object { $_.InnerText }) -join '; '
    } else {
        $null
    }

    $unit = [XliffTranslationUnit]::new(
        (Get-XliffAttributeValue -Node $UnitNode -Name 'id'),
        (Get-XliffElementText -Node $sourceNode),
        (Get-XliffElementText -Node $targetNode),
        (Get-XliffAttributeValue -Node $targetNode -Name 'state'),
        $note,
        (Get-XliffAttributeValue -Node $fileNode -Name 'source-language'),
        (Get-XliffAttributeValue -Node $fileNode -Name 'target-language')
    )

    $unit.Path = $Path
    $unit.XmlNode = $UnitNode
    $unit.XmlDocument = $Document

    return $unit
}

function Get-XliffTranslationUnitsFromDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument]$Document,

        [string]$Path
    )

    foreach ($node in Get-XliffTranslationUnitNodes -Document $Document) {
        ConvertTo-XliffTranslationUnit -UnitNode ([System.Xml.XmlElement]$node) -Document $Document -Path $Path
    }
}

function Get-XliffStreamingTranslationUnits {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    $resolvedPath = Resolve-XliffPath -Path $Path
    $settings = [System.Xml.XmlReaderSettings]::new()
    $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
    $settings.IgnoreWhitespace = $false
    $settings.XmlResolver = $null

    $sourceLanguage = $null
    $targetLanguage = $null
    $stream = [System.IO.File]::OpenRead($resolvedPath)
    try {
        $reader = [System.Xml.XmlReader]::Create($stream, $settings)
        try {
            while ($reader.Read()) {
                if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.LocalName -eq 'file') {
                    $sourceLanguage = $reader.GetAttribute('source-language')
                    $targetLanguage = $reader.GetAttribute('target-language')
                }

                if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.LocalName -eq 'trans-unit') {
                    $unitDocument = [System.Xml.XmlDocument]::new()
                    $unitDocument.PreserveWhitespace = $true
                    $unitNode = [System.Xml.XmlElement]$unitDocument.ReadNode($reader)
                    if ($unitNode) {
                        $sourceNode = Get-XliffChildElement -Node $unitNode -LocalName 'source'
                        $targetNode = Get-XliffChildElement -Node $unitNode -LocalName 'target'
                        $noteNode = Get-XliffChildElement -Node $unitNode -LocalName 'note'
                        [XliffTranslationUnit]::new(
                            (Get-XliffAttributeValue -Node $unitNode -Name 'id'),
                            (Get-XliffElementText -Node $sourceNode),
                            (Get-XliffElementText -Node $targetNode),
                            (Get-XliffAttributeValue -Node $targetNode -Name 'state'),
                            (Get-XliffElementText -Node $noteNode),
                            $sourceLanguage,
                            $targetLanguage
                        )
                    }
                }
            }
        } finally {
            $reader.Close()
        }
    } finally {
        $stream.Dispose()
    }
}

# Builds a hashtable keyed by translation unit Id for fast sync/compare lookups.
function Get-XliffUnitMap {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [XliffTranslationUnit[]]$InputObject
    )

    begin {
        $map = @{}
    }

    process {
        foreach ($unit in $InputObject) {
            if ($unit.Id -and -not $map.ContainsKey($unit.Id)) {
                $map[$unit.Id] = $unit
            }
        }
    }

    end {
        return $map
    }
}

function Get-XliffInsertionParent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument]$Document
    )

    $namespaceManager = New-XliffNamespaceManager -Document $Document
    $group = Select-XliffSingleNode -Node $Document -XPath '//xlf:file/xlf:body//xlf:group[1]' -NamespaceManager $namespaceManager
    if ($group) {
        return $group
    }

    $body = Select-XliffSingleNode -Node $Document -XPath '//xlf:file/xlf:body' -NamespaceManager $namespaceManager
    if ($body) {
        return $body
    }

    throw 'No XLIFF body element was found.'
}

# Clones a source <trans-unit> into a target document during Sync-XliffFile.
function Import-XliffUnitNode {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement]$SourceUnitNode,

        [Parameter(Mandatory)]
        [System.Xml.XmlDocument]$TargetDocument,

        [string]$InitialTarget = '',

        [string]$InitialState = 'needs-translation'
    )

    $imported = [System.Xml.XmlElement]$TargetDocument.ImportNode($SourceUnitNode, $true)
    [void](Set-XliffUnitTarget -UnitNode $imported -Target $InitialTarget)
    Set-XliffUnitState -UnitNode $imported -State $InitialState

    $parent = Get-XliffInsertionParent -Document $TargetDocument
    [void]$parent.AppendChild($imported)

    return $imported
}

function Remove-XliffUnitNode {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlNode]$UnitNode
    )

    if ($UnitNode.ParentNode) {
        [void]$UnitNode.ParentNode.RemoveChild($UnitNode)
    }
}