PsdKit.psm1
$ErrorActionPreference = 1 #.ExternalHelp PsdKit-Help.xml function Convert-PsdToXml { [OutputType([xml])] param( [Parameter(Position=0, Mandatory=1, ValueFromPipeline=1)] [string] $InputObject ) process { trap {ThrowTerminatingError($_)} New-PsdXml $InputObject } } #.ExternalHelp PsdKit-Help.xml function Convert-XmlToPsd { [OutputType([string])] param( [Parameter(Position=0, Mandatory=1)] [System.Xml.XmlNode] $Xml, [string] $Indent ) trap {ThrowTerminatingError($_)} if (!$Indent) { if (!($doc = $Xml.OwnerDocument)) {$doc = $Xml} if ($attr = $doc.DocumentElement.GetAttribute('Indent')) {$Indent = $attr} } $script:LineStarted = $false $script:Indent = Convert-Indent $Indent $script:Writer = New-Object System.IO.StringWriter try { if ($Xml.NodeType -ceq 'Document') { Write-XmlChild $Xml.DocumentElement } elseif ($Xml.Name -ceq 'Item') { Write-XmlChild $Xml } else { Write-XmlElement $Xml } $script:Writer.ToString() } finally { $script:Writer = $null } } #.ExternalHelp PsdKit-Help.xml function ConvertTo-Psd { [OutputType([String])] param( [Parameter(Position=0, ValueFromPipeline=1)] $InputObject, [string] $Indent ) begin { $objects = [System.Collections.Generic.List[object]]@() } process { $objects.Add($InputObject) } end { trap {ThrowTerminatingError($_)} $script:Indent = Convert-Indent $Indent $script:Writer = New-Object System.IO.StringWriter try { foreach($object in $objects) { Write-Psd $object } $script:Writer.ToString().TrimEnd() } finally { $script:Writer = $null } } } #.ExternalHelp PsdKit-Help.xml function Export-PsdXml { param( [Parameter(Position=0, Mandatory=1)] [string] $Path, [Parameter(Position=1, Mandatory=1)] [System.Xml.XmlNode] $Xml, [string] $Indent ) trap {ThrowTerminatingError($_)} $Path = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) [System.IO.File]::WriteAllText($Path, (Convert-XmlToPsd $Xml -Indent $Indent), ([System.Text.Encoding]::UTF8)) } #.ExternalHelp PsdKit-Help.xml function Get-PsdXml { param( [Parameter(Position=0, Mandatory=1)] [System.Xml.XmlNode] $Xml, [Parameter(Position=1)] [string] $XPath ) trap {ThrowTerminatingError($_)} if ($XPath) { $node = $xml.SelectSingleNode($XPath) if (!$node) {throw "XPath selects nothing: '$XPath'."} } else { $node = $Xml } if ($node.NodeType -ne 'Element') {throw "Unexpected node type '$($node.NodeType)'."} switch($node.Name) { Item { if ($node.ChildNodes.Count -ne 1) {throw "Element 'Item' must have one child node."} return Get-PsdXml $node.FirstChild } String { return $node.InnerText } Number { return New-Number $node.InnerText } Variable { return New-Variable $node.InnerText } Comment { return $node.InnerText } Array { return New-Array $node } Table { return New-Table $node } Cast { return New-Cast $node } default { throw "Not supported element '$_'." } } } #.ExternalHelp PsdKit-Help.xml function Import-Psd { [OutputType([Hashtable])] param( [Parameter(Position=0, Mandatory=1)] [string] $Path ) trap {ThrowTerminatingError($_)} $Path = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) Import-LocalizedData -BaseDirectory ([System.IO.Path]::GetDirectoryName($Path)) -FileName ([System.IO.Path]::GetFileName($Path)) -BindingVariable r $r } #.ExternalHelp PsdKit-Help.xml function Import-PsdXml { [OutputType([xml])] param( [Parameter(Mandatory=1)] [string] $Path ) trap {ThrowTerminatingError($_)} $script = [System.IO.File]::ReadAllText($PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path)) New-PsdXml $script } #.ExternalHelp PsdKit-Help.xml function Set-PsdXml { param( [Parameter(Position=0, Mandatory=1)] [System.Xml.XmlNode] $Xml, [Parameter(Position=1, Mandatory=1)] [AllowEmptyString()] [AllowNull()] $Value, [Parameter(Position=2)] [string] $XPath ) trap {ThrowTerminatingError($_)} if ($XPath) { $node = $xml.SelectSingleNode($XPath) if (!$node) {throw 'XPath selects nothing.'} } else { $node = $Xml } if ($node.NodeType -ne 'Element') {throw "Unexpected node type '$($node.NodeType)'."} if ($node.Name -eq 'Comment') { if ($Value -isnot [string]) {throw 'Comment must be a string.'} if ($Value.StartsWith('#')) { if ($Value -match '[\r\n]') {throw 'Line comment must be one line.'} } elseif ($Value.StartsWith('<#')) { if (!$Value.EndsWith('#>')) {throw "Block comment must end with '#>'."} } else { throw 'Comment must be line #... or block <#...#>.' } $node.InnerText = $Value return } $newXml = Convert-PsdToXml (ConvertTo-Psd $Value) $newNode = $newXml.DocumentElement if ($newNode.ChildNodes.Count -ne 1) {throw 'Not supported new value.'} $newNode = $node.OwnerDocument.ImportNode($newNode.FirstChild, $true) if ($node.Name -eq 'Item') { if ($node.ChildNodes.Count -ne 1) {throw 'Not supported old value.'} $null = $node.ReplaceChild($newNode, $node.FirstChild) } else { $null = $node.ParentNode.ReplaceChild($newNode, $node) } } function Add-XmlElement { [OutputType([System.Xml.XmlElement])] param( [Parameter(Mandatory=1)] [System.Xml.XmlElement] $Xml, [Parameter(Mandatory=1)] [string] $Name ) $Xml.AppendChild($Xml.OwnerDocument.CreateElement($Name)) } function Convert-Indent($Indent) { switch($Indent) { '' {return ' '} '1' {return "`t"} '2' {return ' '} '4' {return ' '} '0' {return ''} } $Indent } function ThrowTerminatingError($M) { $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([Exception]"$M"), $null, 0, $null)) } function ThrowUnexpectedToken($t1) { throw 'Unexpected token {0} ''{1}'' at {2}:{3}' -f $t1.Type, $t1.Content, $t1.StartLine, $t1.StartColumn } function Write-Psd($Object, $Depth=0, [switch]$NoIndent) { $indent1 = $script:Indent * $Depth if (!$NoIndent) { $script:Writer.Write($indent1) } if ($null -eq $Object) { $script:Writer.WriteLine('$null') return } $type = $Object.GetType() switch([System.Type]::GetTypeCode($type)) { Object { if ($Object -is [System.Collections.IDictionary]) { if ($Object.Count) { $script:Writer.WriteLine('@{') $indent2 = $script:Indent * ($Depth + 1) foreach($e in $Object.GetEnumerator()) { $key = $e.Key $keyType = $key.GetType() if ($keyType -eq [string]) { if ($key -match '^\w+$' -and $key -match '^\D') { $script:Writer.Write('{0}{1} = ', $indent2, $key) } else { $script:Writer.Write("{0}'{1}' = ", $indent2, $key.Replace("'", "''")) } } elseif ($keyType -eq [int]) { $script:Writer.Write('{0}{1} = ', $indent2, $key) } elseif ($keyType -eq [long]) { $script:Writer.Write('{0}{1}L = ', $indent2, $key) } else { throw "Not supported key type '$($keyType.FullName)'." } Write-Psd $e.Value ($Depth + 1) -NoIndent } $script:Writer.WriteLine("$indent1}") } else { $script:Writer.WriteLine('@{}') } return } elseif ($Object -is [System.Collections.IList]) { if ($Object.Count) { $script:Writer.WriteLine('@(') foreach($e in $Object) { Write-Psd $e ($Depth + 1) } $script:Writer.WriteLine("$indent1)" ) } else { $script:Writer.WriteLine('@()') } return } elseif ($type -eq [System.Guid] -or $type -eq [System.Version]) { $script:Writer.WriteLine("'{0}'", $Object) return } elseif ($type -eq [System.Management.Automation.SwitchParameter]) { $script:Writer.WriteLine($(if ($Object) {'$true'} else {'$false'})) return } elseif ($type -eq [System.Uri]) { $script:Writer.WriteLine("'{0}'", $Object.ToString().Replace("'", "''")) return } elseif ($Object -is [PSCustomObject]) { $script:Writer.WriteLine('@{') $indent2 = $script:Indent * ($Depth + 1) foreach($e in $Object.PSObject.Properties) { $key = $e.Name if ($key -match '^\w+$' -and $key -match '^\D') { $script:Writer.Write('{0}{1} = ', $indent2, $key) } else { $script:Writer.Write("{0}'{1}' = ", $indent2, $key.Replace("'", "''")) } Write-Psd $e.Value ($Depth + 1) -NoIndent } $script:Writer.WriteLine("$indent1}" ) return } } String { $script:Writer.WriteLine("'{0}'", $Object.Replace("'", "''")) return } Boolean { $script:Writer.WriteLine($(if ($Object) {'$true'} else {'$false'})) return } DateTime { $script:Writer.WriteLine("[DateTime] '{0}'", $Object.ToString('o')) return } Char { $script:Writer.WriteLine("'{0}'", $Object.Replace("'", "''")) return } DBNull { $script:Writer.WriteLine('$null') return } default { if ($type.IsEnum) { $script:Writer.WriteLine("'{0}'", $Object) } else { $script:Writer.WriteLine($Object) } return } } throw "Not supported type '{0}'." -f $type.FullName } function Write-XmlChild($elem, $Depth=0) { foreach($e in $elem.ChildNodes) { Write-XmlElement $e $Depth } } function Write-XmlElement($elem, $Depth=0) { switch($elem.Name) { NewLine { $script:Writer.WriteLine() $script:LineStarted = $false break } Comment { Write-Text $elem.InnerText break } Table { Write-Text '@{' Write-XmlChild $elem ($Depth + 1) Write-Text '}' break } Array { Write-Text '@(' Write-XmlChild $elem ($Depth + 1) Write-Text ')' break } Item { if ($elem.GetAttribute('Type') -eq 'String') { Write-Text ("'{0}' =" -f $elem.Key.Replace("'", "''")) } else { Write-Text ('{0} =' -f $elem.Key) } Write-XmlChild $elem $Depth break } Number { Write-Text $elem.InnerText break } String { if ($elem.GetAttribute('Type') -eq '1') { Write-Text "@'" $script:Writer.WriteLine() $script:Writer.Write($elem.InnerText) $script:Writer.WriteLine() $script:Writer.Write("'@") } else { Write-Text ("'{0}'" -f $elem.InnerText.Replace("'", "''")) } break } Variable { Write-Text ('${0}' -f $elem.InnerText) break } Cast { Write-Text ($elem.GetAttribute('Type')) Write-XmlChild $elem $Depth break } Comma { Write-Text ',' -NoSpace break } Semicolon { Write-Text ';' -NoSpace break } default { throw "Unexpected XML element '$_'." } } } function New-PsdXml($Script) { $err = $null $tokens = [System.Management.Automation.PSParser]::Tokenize($script, [ref]$err) if ($err) { $err = $err[0] $t1 = $err.Token throw 'Parser error at {0}:{1} : {2}' -f $t1.StartLine, $t1.StartColumn, $err.Message } $indent = '' $lastLine = 0 foreach($t1 in $tokens) { if ($t1.StartLine -eq $lastLine -or $t1.Type -eq 'NewLine' -or $t1.Type -eq 'Comment') {continue} if ($t1.StartColumn -eq 2) {$indent = '1'; break} if ($t1.StartColumn -eq 3) {$indent = '2'; break} if ($t1.StartColumn -gt 1) {break} $lastLine = $t1.StartLine } $xml = [xml]'<Data/>' if ($indent) { $xml.DocumentElement.SetAttribute('Indent', $indent) } $script:Queue = [System.Collections.Queue]$tokens $script:Script = $Script try { Add-Data $xml.DocumentElement $xml } finally { $script:Queue = $null $script:Script = $null } } # Add just one String, Number, Variable, Table, or Array. function Add-Value($elem, $t1) { switch($t1.Type) { String { $e = Add-XmlElement $elem String $e.InnerText = $t1.Content if ($script:Script[$t1.Start] -eq '@' -and $script:Script[$t1.Start + 1] -eq "'") { $e.SetAttribute('Type', 1) } break } Number { $e = Add-XmlElement $elem Number $e.InnerText = $t1.Content break } Variable { $e = Add-XmlElement $elem Variable $e.InnerText = $t1.Content break } GroupStart { switch($t1.Content) { '@{' { $e = Add-XmlElement $elem Table Add-Table $e } '@(' { $e = Add-XmlElement $elem Array Add-Array $e } default { ThrowUnexpectedToken $t1 } } break } Type { $e = Add-XmlElement $elem Cast $v = $t1.Content #! v2 has no [] $e.SetAttribute('Type', $(if ($v[0] -eq '[') {$v} else {"[$v]"})) $t2 = $script:Queue.Dequeue() Add-Value $e $t2 } default { ThrowUnexpectedToken $t1 } } } # Add data to the array element. function Add-Array($elem) { while($script:Queue.Count) { $t1 = $script:Queue.Dequeue() switch($t1.Type) { GroupEnd { return } NewLine { $null = Add-XmlElement $elem NewLine break } Comment { $e = Add-XmlElement $elem Comment $e.InnerText = $t1.Content break } StatementSeparator { $null = Add-XmlElement $elem Semicolon break } Operator { if ($t1.Content -eq ',') { $null = Add-XmlElement $elem Comma } else { ThrowUnexpectedToken $t1 } break } default { Add-Value $elem $t1 } } } } # Add one item to the table element. function Add-Item($elem, $t1, $Type) { $elem = Add-XmlElement $elem Item $elem.SetAttribute('Key', $t1.Content) if ($Type) { $elem.SetAttribute('Type', $Type) } $t1 = $script:Queue.Dequeue() if ($t1.Type -ne 'Operator' -or $t1.Content -ne '=') { ThrowUnexpectedToken $t1 } $valueAdded = $false while($script:Queue.Count) { $t1 = $script:Queue.Peek() switch ($t1.Type) { GroupEnd { return } StatementSeparator { return } NewLine { if ($valueAdded) {return} $null = $script:Queue.Dequeue() $null = Add-XmlElement $elem NewLine break } Comment { $null = $script:Queue.Dequeue() $e = Add-XmlElement $elem Comment $e.InnerText = $t1.Content break } Operator { if ($t1.Content -eq ',') { $null = $script:Queue.Dequeue() $null = Add-XmlElement $elem Comma } else { ThrowUnexpectedToken $t1 } break } default { $null = $script:Queue.Dequeue() $valueAdded = $true Add-Value $elem $t1 } } } } function Add-Table($elem) { while($script:Queue.Count) { $t1 = $script:Queue.Dequeue() switch($t1.Type) { GroupEnd { return } NewLine { $null = Add-XmlElement $elem NewLine break } Comment { $e = Add-XmlElement $elem Comment $e.InnerText = $t1.Content break } StatementSeparator { $null = Add-XmlElement $elem Semicolon break } Member { Add-Item $elem $t1 break } String { Add-Item $elem $t1 -Type String break } Number { Add-Item $elem $t1 -Type Number break } default { ThrowUnexpectedToken $t1 } } } } function Add-Data($elem) { while($script:Queue.Count) { $t1 = $script:Queue.Dequeue() switch($t1.Type) { NewLine { $null = Add-XmlElement $elem NewLine break } Comment { $e = Add-XmlElement $elem Comment $e.InnerText = $t1.Content break } StatementSeparator { $null = Add-XmlElement $elem Semicolon break } Operator { if ($t1.Content -eq ',') { $null = Add-XmlElement $elem Comma } else { ThrowUnexpectedToken $t1 } break } default { Add-Value $elem $t1 } } } } function Write-Text($Text, [switch]$NoSpace) { if ($script:LineStarted) { if (!$NoSpace) { $script:Writer.Write(' ') } } else { $script:LineStarted = $true $script:Writer.Write($script:Indent * $Depth) } $script:Writer.Write($Text) } function New-Number($Text) { $r = $null if ([int]::TryParse($Text, [ref]$r)) {return $r} if ([long]::TryParse($Text, [ref]$r)) {return $r} if ([double]::TryParse($Text, [ref]$r)) {return $r} throw "Not supported number '$_'." } function New-Variable($Text) { switch($Text) { false {return $false} true {return $true} null {return $null} default {throw "Not supported variable '$_'."} } } function New-Cast($node) { $typeName = $node.Type.TrimEnd(']').TrimStart('[') $type = [System.Management.Automation.LanguagePrimitives]::ConvertTo($typeName, [type]) if ([type]::GetTypeCode($type) -eq 'Object') {throw "Cast to not supported type '$typeName'."} [System.Management.Automation.LanguagePrimitives]::ConvertTo($node.InnerText, $type) } function New-Array($node) { $r = [System.Collections.Generic.List[object]]@() foreach($node in $node.ChildNodes) { switch($node.Name) { NewLine {break} String {$r.Add($node.InnerText); break} Number {$r.Add((New-Number $node.InnerText)); break} Variable {$r.Add((New-Variable $node.InnerText)); break} Table {$r.Add((New-Table $node)); break} Cast {$r.Add((New-Cast $node)); break} default {throw "Array contains not supported node '$_'."} } } , $r } function New-Table($node) { $r = [System.Collections.Specialized.OrderedDictionary]([System.StringComparer]::OrdinalIgnoreCase) foreach($node in $node.ChildNodes) { if ($node.Name -eq 'Item') { if ($node.GetAttribute('Type') -eq 'Number') { $key = New-Number $node.Key } else { $key = $node.Key } if ($node.ChildNodes.Count -ne 1) {throw "Item must have one child node."} $r.Add($key, (Get-PsdXml $node.FirstChild)) } elseif ($node.Name -ne 'NewLine') { throw "Table contains not supported node '$($node.Name)'." } } $r } Export-ModuleMember -Function @( 'Convert-PsdToXml' 'ConvertTo-Psd' 'Convert-XmlToPsd' 'Export-PsdXml' 'Get-PsdXml' 'Import-Psd' 'Import-PsdXml' 'Set-PsdXml' ) |