PoshCode Logo PowerShell Code Repository

Xml Module 4 (modification of post by Joel Bennett view diff)
View followups from Joel Bennett | diff | embed code: <script type="text/javascript" src="http://PoshCode.org/embed/1677"></script>download | new post

Never content to leave well-enough alone, I’ve rewritten my XML DSL with 100% less cruft. New-XDocument no longer requires the “xe” command as long as the name of your XML Element doesn’t coincide with that of a PowerShell command (if it does, you need the “xe” on the front), and namespaces can be referred to by short name like dc:creator to keep things simple. Please review the examples on New-XDocument as this is a breaking change.

The other functions round out the set of XML functionality (especially if you don’t have PSCX). In particular, my Select-XML improves over the built-in Select-XML by leveraging Remove-XmlNamespace to provide a -RemoveNamespace parameter — if it’s supplied, all of the namespace declarations and prefixes are removed from all XML nodes (by an XSL transform) before searching (so you can actually find things, even with namespace-qualified xml).

It is, however, important to note that this means that the returned results will not have namespaces in them, even if the input XML did, and only raw XmlNodes are returned from this function, so the output isn’t compatible with the built in Select-Xml — instead, it’s equivalent to using it the way I usually do: Select-Xml ... | Select-Object -Expand Node

  1. #requires -version 2.0
  2.  
  3. # Improves over the built-in Select-XML by leveraging Remove-XmlNamespace http://poshcode.org/1492
  4. # to provide a -RemoveNamespace parameter -- if it's supplied, all of the namespace declarations
  5. # and prefixes are removed from all XML nodes (by an XSL transform) before searching.
  6. # IMPORTANT: returned results *will not* have namespaces in them, even if the input XML did.
  7.  
  8. # Also, only raw XmlNodes are returned from this function, so the output isn't completely compatible
  9. # with the built in Select-Xml. It's equivalent to using Select-Xml ... | Select-Object -Expand Node
  10.  
  11. # Version History:
  12. # Select-Xml 2.0 This was the first script version I wrote.
  13. #                it didn't function identically to the built-in Select-Xml with regards to parameter parsing
  14. # Select-Xml 2.1 Matched the built-in Select-Xml parameter sets, it's now a drop-in replacement
  15. #                BUT only if you were using the original with: Select-Xml ... | Select-Object -Expand Node
  16. # Select-Xml 2.2 Fixes a bug in the -Content parameterset where -RemoveNamespace was *presumed*
  17. # Version    3.0 Added New-XDocument and associated generation functions for my XML DSL
  18. # Version    3.1 Fixed a really ugly bug in New-XDocument in 3.0 which I should not have released
  19. # Version    4.0 Never content to leave well enough alone, I've completely reworked New-XDocument
  20.  
  21. $xlr8r = [type]::gettype("System.Management.Automation.TypeAccelerators")
  22. $xlinq = [Reflection.Assembly]::Load("System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
  23. $xlinq.GetTypes() | ? { $_.IsPublic -and !$_.IsSerializable -and $_.Name -ne "Extensions" -and !$xlr8r::Get[$_.Name] } | % {
  24.   $xlr8r::Add( $_.Name, $_.FullName )
  25. }
  26. if(!$xlr8r::Get["Stack"]) {
  27.    $xlr8r::Add( "Stack", "System.Collections.Generic.Stack``1, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" )
  28. }
  29. if(!$xlr8r::Get["Dictionary"]) {
  30.    $xlr8r::Add( "Dictionary", "System.Collections.Generic.Dictionary``2, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" )
  31. }
  32. if(!$xlr8r::Get["PSParser"]) {
  33.    $xlr8r::Add( "PSParser", "System.Management.Automation.PSParser, System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" )
  34. }
  35.  
  36.  
  37.  
  38. function Format-XML {
  39. #.Synopsis
  40. #   Pretty-print formatted XML source
  41. #.Description
  42. #       Runs an XmlDocument through an auto-indenting XmlWriter
  43. #.Parameter Xml
  44. #       The Xml Document
  45. #.Parameter Indent
  46. #       The indent level (defaults to 2 spaces)
  47. Param(
  48.         [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
  49.         [xml]$xml
  50. ,
  51.         [Parameter(Mandatory=$false)]
  52.         $indent=2
  53. )
  54.     $StringWriter = New-Object System.IO.StringWriter
  55.     $XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter
  56.     $xmlWriter.Formatting = "indented"
  57.     $xmlWriter.Indentation = $Indent
  58.     $xml.WriteContentTo($XmlWriter)
  59.     $XmlWriter.Flush()
  60.     $StringWriter.Flush()
  61.     Write-Output $StringWriter.ToString()
  62. }
  63. Set-Alias fxml Format-Xml
  64.  
  65. function Select-Xml {
  66. #.Synopsis
  67. #  The Select-XML cmdlet lets you use XPath queries to search for text in XML strings and documents. Enter an XPath query, and use the Content, Path, or Xml parameter to specify the XML to be searched.
  68. #.Description
  69. #  Improves over the built-in Select-XML by leveraging Remove-XmlNamespace to provide a -RemoveNamespace parameter -- if it's supplied, all of the namespace declarations and prefixes are removed from all XML nodes (by an XSL transform) before searching.  
  70. #  
  71. #  However, only raw XmlNodes are returned from this function, so the output isn't currently compatible with the built in Select-Xml, but is equivalent to using Select-Xml ... | Select-Object -Expand Node
  72. #
  73. #  Also note that if the -RemoveNamespace switch is supplied the returned results *will not* have namespaces in them, even if the input XML did, and entities get expanded automatically.
  74. #.Parameter Content
  75. #  Specifies a string that contains the XML to search. You can also pipe strings to Select-XML.
  76. #.Parameter Namespace
  77. #   Specifies a hash table of the namespaces used in the XML. Use the format @{<namespaceName> = <namespaceUri>}.
  78. #.Parameter Path
  79. #   Specifies the path and file names of the XML files to search.  Wildcards are permitted.
  80. #.Parameter Xml
  81. #  Specifies one or more XML nodes to search.
  82. #.Parameter XPath
  83. #  Specifies an XPath search query. The query language is case-sensitive. This parameter is required.
  84. #.Parameter RemoveNamespace
  85. #  Allows the execution of XPath queries without namespace qualifiers.
  86. #  
  87. #  If you specify the -RemoveNamespace switch, all namespace declarations and prefixes are actually removed from the Xml before the XPath search query is evaluated, and your XPath query should therefore NOT contain any namespace prefixes.
  88. #
  89. #  Note that this means that the returned results *will not* have namespaces in them, even if the input XML did, and entities get expanded automatically.
  90. [CmdletBinding(DefaultParameterSetName="Xml")]
  91. PARAM(
  92.    [Parameter(Position=1,ParameterSetName="Path",Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
  93.    [ValidateNotNullOrEmpty()]
  94.    [Alias("PSPath")]
  95.    [String[]]$Path
  96. ,
  97.    [Parameter(Position=1,ParameterSetName="Xml",Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
  98.    [ValidateNotNullOrEmpty()]
  99.    [Alias("Node")]
  100.    [System.Xml.XmlNode[]]$Xml
  101. ,
  102.    [Parameter(ParameterSetName="Content",Mandatory=$true,ValueFromPipeline=$true)]
  103.    [ValidateNotNullOrEmpty()]
  104.    [String[]]$Content
  105. ,
  106.    [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
  107.    [ValidateNotNullOrEmpty()]
  108.    [Alias("Query")]
  109.    [String[]]$XPath
  110. ,
  111.    [Parameter(Mandatory=$false)]
  112.    [ValidateNotNullOrEmpty()]
  113.    [Hashtable]$Namespace
  114. ,
  115.    [Switch]$RemoveNamespace
  116. )
  117. BEGIN {
  118.    function Select-Node {
  119.    PARAM([Xml.XmlNode]$Xml, [String[]]$XPath, $NamespaceManager)
  120.    BEGIN {
  121.       foreach($node in $xml) {
  122.          if($NamespaceManager -is [Hashtable]) {
  123.             $nsManager = new-object System.Xml.XmlNamespaceManager $node.NameTable
  124.             foreach($ns in $Namespace.GetEnumerator()) {
  125.                $nsManager.AddNamespace( $ns.Key, $ns.Value )
  126.             }
  127.          }
  128.          
  129.          foreach($path in $xpath) {
  130.             $node.SelectNodes($path, $NamespaceManager)
  131.    }  }  }  }
  132.  
  133.    [Text.StringBuilder]$XmlContent = [String]::Empty
  134. }
  135.  
  136. PROCESS {
  137.    $NSM = $Null; if($PSBoundParameters.ContainsKey("Namespace")) { $NSM = $Namespace }
  138.  
  139.    switch($PSCmdlet.ParameterSetName) {
  140.       "Content" {
  141.          $null = $XmlContent.AppendLine( $Content -Join "`n" )
  142.       }
  143.       "Path" {
  144.          foreach($file in Get-ChildItem $Path) {
  145.             [Xml]$Xml = Get-Content $file
  146.             if($RemoveNamespace) {
  147.                $Xml = Remove-XmlNamespace $Xml
  148.             }
  149.             Select-Node $Xml $XPath  $NSM
  150.          }
  151.       }
  152.       "Xml" {
  153.          foreach($node in $Xml) {
  154.             if($RemoveNamespace) {
  155.                $node = Remove-XmlNamespace $node
  156.             }
  157.             Select-Node $node $XPath $NSM
  158.          }
  159.       }
  160.    }
  161. }
  162. END {
  163.    if($PSCmdlet.ParameterSetName -eq "Content") {
  164.       [Xml]$Xml = $XmlContent.ToString()
  165.       if($RemoveNamespace) {
  166.          $Xml = Remove-XmlNamespace $Xml
  167.       }
  168.       Select-Node $Xml $XPath  $NSM
  169.    }
  170. }
  171.  
  172. }
  173. Set-Alias slxml Select-Xml
  174.  
  175. function Convert-Node {
  176. #.Synopsis
  177. # Convert a single XML Node via XSL stylesheets
  178. param(
  179. [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
  180. [System.Xml.XmlReader]$XmlReader,
  181. [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
  182. [System.Xml.Xsl.XslCompiledTransform]$StyleSheet
  183. )
  184. PROCESS {
  185.    $output = New-Object IO.StringWriter
  186.    $StyleSheet.Transform( $XmlReader, $null, $output )
  187.    Write-Output $output.ToString()
  188. }
  189. }
  190.    
  191. function Convert-Xml {
  192. #.Synopsis
  193. #  The Convert-XML function lets you use Xslt to transform XML strings and documents.
  194. #.Description
  195. #.Parameter Content
  196. #  Specifies a string that contains the XML to search. You can also pipe strings to Select-XML.
  197. #.Parameter Namespace
  198. #   Specifies a hash table of the namespaces used in the XML. Use the format @{<namespaceName> = <namespaceUri>}.
  199. #.Parameter Path
  200. #   Specifies the path and file names of the XML files to search.  Wildcards are permitted.
  201. #.Parameter Xml
  202. #  Specifies one or more XML nodes to search.
  203. #.Parameter Xsl
  204. #  Specifies an Xml StyleSheet to transform with...
  205. [CmdletBinding(DefaultParameterSetName="Xml")]
  206. PARAM(
  207.    [Parameter(Position=1,ParameterSetName="Path",Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
  208.    [ValidateNotNullOrEmpty()]
  209.    [Alias("PSPath")]
  210.    [String[]]$Path
  211. ,
  212.    [Parameter(Position=1,ParameterSetName="Xml",Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
  213.    [ValidateNotNullOrEmpty()]
  214.    [Alias("Node")]
  215.    [System.Xml.XmlNode[]]$Xml
  216. ,
  217.    [Parameter(ParameterSetName="Content",Mandatory=$true,ValueFromPipeline=$true)]
  218.    [ValidateNotNullOrEmpty()]
  219.    [String[]]$Content
  220. ,
  221.    [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
  222.    [ValidateNotNullOrEmpty()]
  223.    [Alias("StyleSheet")]
  224.    [String[]]$Xslt
  225. )
  226. BEGIN {
  227.    $StyleSheet = New-Object System.Xml.Xsl.XslCompiledTransform
  228.    if(Test-Path @($Xslt)[0] -EA 0) {
  229.       Write-Verbose "Loading Stylesheet from $(Resolve-Path @($Xslt)[0])"
  230.       $StyleSheet.Load( (Resolve-Path @($Xslt)[0]) )
  231.    } else {
  232.       Write-Verbose "$Xslt"
  233.       $StyleSheet.Load(([System.Xml.XmlReader]::Create((New-Object System.IO.StringReader ($Xslt -join "`n")))))
  234.    }
  235.    [Text.StringBuilder]$XmlContent = [String]::Empty
  236. }
  237. PROCESS {
  238.    switch($PSCmdlet.ParameterSetName) {
  239.       "Content" {
  240.          $null = $XmlContent.AppendLine( $Content -Join "`n" )
  241.       }
  242.       "Path" {
  243.          foreach($file in Get-ChildItem $Path) {
  244.             Convert-Node -Xml ([System.Xml.XmlReader]::Create((Resolve-Path $file))) $StyleSheet
  245.          }
  246.       }
  247.       "Xml" {
  248.          foreach($node in $Xml) {
  249.             Convert-Node -Xml (New-Object Xml.XmlNodeReader $node) $StyleSheet
  250.          }
  251.       }
  252.    }
  253. }
  254. END {
  255.    if($PSCmdlet.ParameterSetName -eq "Content") {
  256.       [Xml]$Xml = $XmlContent.ToString()
  257.       Convert-Node -Xml $Xml $StyleSheet
  258.    }
  259. }
  260. }
  261. Set-Alias cvxml Convert-Xml
  262.  
  263. function Remove-XmlNamespace {
  264. #.Synopsis
  265. #  Removes namespace definitions and prefixes from xml documents
  266. #.Description
  267. #  Runs an xml document through an XSL Transformation to remove namespaces from it if they exist.
  268. #  Entities are also naturally expanded
  269. #.Parameter Content
  270. #  Specifies a string that contains the XML to transform.
  271. #.Parameter Path
  272. #  Specifies the path and file names of the XML files to transform. Wildcards are permitted.
  273. #
  274. #  There will bne one output document for each matching input file.
  275. #.Parameter Xml
  276. #  Specifies one or more XML documents to transform
  277. [CmdletBinding(DefaultParameterSetName="Xml")]
  278. PARAM(
  279.    [Parameter(Position=1,ParameterSetName="Path",Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
  280.    [ValidateNotNullOrEmpty()]
  281.    [Alias("PSPath")]
  282.    [String[]]$Path
  283. ,
  284.    [Parameter(Position=1,ParameterSetName="Xml",Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
  285.    [ValidateNotNullOrEmpty()]
  286.    [Alias("Node")]
  287.    [System.Xml.XmlNode[]]$Xml
  288. ,
  289.    [Parameter(ParameterSetName="Content",Mandatory=$true,ValueFromPipeline=$true)]
  290.    [ValidateNotNullOrEmpty()]
  291.    [String[]]$Content
  292. ,
  293.    [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
  294.    [ValidateNotNullOrEmpty()]
  295.    [Alias("StyleSheet")]
  296.    [String[]]$Xslt
  297. )
  298. BEGIN {
  299.    $StyleSheet = New-Object System.Xml.Xsl.XslCompiledTransform
  300.    $StyleSheet.Load(([System.Xml.XmlReader]::Create((New-Object System.IO.StringReader @"
  301. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  302.   <xsl:output method="xml" indent="yes"/>
  303.   <xsl:template match="/|comment()|processing-instruction()">
  304.      <xsl:copy>
  305.         <xsl:apply-templates/>
  306.      </xsl:copy>
  307.   </xsl:template>
  308.  
  309.   <xsl:template match="*">
  310.      <xsl:element name="{local-name()}">
  311.         <xsl:apply-templates select="@*|node()"/>
  312.      </xsl:element>
  313.   </xsl:template>
  314.  
  315.   <xsl:template match="@*">
  316.      <xsl:attribute name="{local-name()}">
  317.         <xsl:value-of select="."/>
  318.      </xsl:attribute>
  319.   </xsl:template>
  320. </xsl:stylesheet>
  321. "@))))
  322.    [Text.StringBuilder]$XmlContent = [String]::Empty
  323. }
  324. PROCESS {
  325.    switch($PSCmdlet.ParameterSetName) {
  326.       "Content" {
  327.          $null = $XmlContent.AppendLine( $Content -Join "`n" )
  328.       }
  329.       "Path" {
  330.          foreach($file in Get-ChildItem $Path) {
  331.             [Xml]$Xml = Get-Content $file
  332.             Convert-Node -Xml $Xml $StyleSheet
  333.          }
  334.       }
  335.       "Xml" {
  336.          $Xml | Convert-Node $StyleSheet
  337.       }
  338.    }
  339. }
  340. END {
  341.    if($PSCmdlet.ParameterSetName -eq "Content") {
  342.       [Xml]$Xml = $XmlContent.ToString()
  343.       Convert-Node -Xml $Xml $StyleSheet
  344.    }
  345. }
  346. }
  347. Set-Alias rmns Remove-XmlNamespace
  348.  
  349.  
  350.  
  351. function New-XDocument {
  352. #.Synopsis
  353. #       Creates a new XDocument (the new xml document type)
  354. #.Description
  355. #  This is the root for a new XML mini-dsl, akin to New-BootsWindow for XAML
  356. #  It creates a new XDocument, and takes scritpblock(s) to define it's contents
  357. #.Parameter root
  358. #       The root node name
  359. #.Parameter version
  360. #       Optional: the XML version. Defaults to 1.0
  361. #.Parameter encoding
  362. #       Optional: the Encoding. Defaults to UTF-8
  363. #.Parameter standalone
  364. #  Optional: whether to specify standalone in the xml declaration. Defaults to "yes"
  365. #.Parameter args
  366. #       this is where all the dsl magic happens. Please see the Examples. :)
  367. #
  368. #.Example
  369. # [XNamespace]$dc = "http`://purl.org/dc/elements/1.1"
  370. # [string]$xml = New-XDocument rss -dc $dc -version "2.0" {
  371. #    channel {
  372. #       title {"Test RSS Feed"}
  373. #       link {"http`://HuddledMasses.org"}
  374. #       description {"An RSS Feed generated simply to demonstrate my XML DSL"}
  375. #       dc:language {"en"}
  376. #       dc:creator {"Jaykul@HuddledMasses.org"}
  377. #       dc:rights {"Copyright 2009, CC-BY"}
  378. #       dc:date {(Get-Date -f u) -replace " ","T"}
  379. #
  380. #       item {
  381. #          title {"The First Item"}
  382. #          link {"http`://huddledmasses.org/new-site-new-layout-lost-posts/"}
  383. #          guid -isPermaLink true {"http`://huddledmasses.org/new-site-new-layout-lost-posts/"}
  384. #          description {"Ema Lazarus' Poem"}
  385. #          pubDate {(Get-Date 10/31/2003 -f u) -replace " ","T"}
  386. #       }
  387. #    }
  388. # }
  389. #
  390. # $xml.Declaration.ToString()  ## I can't find a way to have this included in the $xml.ToString()
  391. # $xml.ToString()
  392. #
  393. #
  394. # OUTPUT: (NOTE: I added the space in the http: to paste it on PoshCode -- those aren't in the output)
  395. #
  396. #
  397. # <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  398. # <rss xmlns:dc="http ://purl.org/dc/elements/1.1" version="2.0">
  399. #   <channel>
  400. #     <title>Test RSS Feed</title>
  401. #     <link>http ://HuddledMasses.org</link>
  402. #     <description>An RSS Feed generated simply to demonstrate my XML DSL</description>
  403. #     <dc:language>en</dc:language>
  404. #     <dc:creator>Jaykul@HuddledMasses.org</dc:creator>
  405. #     <dc:rights>Copyright 2009, CC-BY</dc:rights>
  406. #     <dc:date>2009-07-26T00:50:08Z</dc:date>
  407. #     <item>
  408. #       <title>The First Item</title>
  409. #       <link>http ://huddledmasses.org/new-site-new-layout-lost-posts/</link>
  410. #       <guid isPermaLink="true">http ://huddledmasses.org/new-site-new-layout-lost-posts/</guid>
  411. #       <description>Ema Lazarus' Poem</description>
  412. #       <pubDate>2003-10-31T00:00:00Z</pubDate>
  413. #     </item>
  414. #   </channel>
  415. # </rss>
  416. #
  417. #.Example
  418. #  This time with a default namespace
  419. ## IMPORTANT! ## NOTE that I use the "xe" shortcut which is redefined when you specify a namespace
  420. ##            ## for the root element, so that all child elements (by default) inherit that.
  421. ##            ## You can still control the prefixes by passing the namespace as a parameter
  422. ##            ## e.g.: -atom $atom
  423. ## The `: in the http`: is still only there for PoshCode, you can just use http: ...
  424. #
  425. # [XNamespace]$atom="http`://www.w3.org/2005/Atom"
  426. # [XNamespace]$dc = "http`://purl.org/dc/elements/1.1"
  427. #
  428. # New-XDocument ($atom + "feed") -Encoding "UTF-16" -$([XNamespace]::Xml +'lang') "en-US" -dc $dc {
  429. #       title {"Test First Entry"}
  430. #       link {"http`://HuddledMasses.org"}
  431. #       updated {(Get-Date -f u) -replace " ","T"}
  432. #       author {
  433. #               name {"Joel Bennett"}
  434. #               uri {"http`://HuddledMasses.org"}
  435. #       }
  436. #       id {"http`://huddledmasses.org/" }
  437. #
  438. #       entry {
  439. #       title {"Test First Entry"}
  440. #               link {"http`://HuddledMasses.org/new-site-new-layout-lost-posts/" }
  441. #               id {"http`://huddledmasses.org/new-site-new-layout-lost-posts/" }
  442. #               updated {(Get-Date 10/31/2003 -f u) -replace " ","T"}
  443. #               summary {"Ema Lazarus' Poem"}
  444. #               link -rel license -href "http://creativecommons.org/licenses/by/3.0/" -title "CC By-Attribution"
  445. #               dc:rights { "Copyright 2009, Some rights reserved (licensed under the Creative Commons Attribution 3.0 Unported license)" }
  446. #       category -scheme "http://huddledmasses.org/tag/" -term "huddled-masses"
  447. #       }
  448. # } | % { $_.Declaration.ToString(); $_.ToString() }
  449. #
  450. #
  451. #  OUTPUT: (NOTE: I added the spaces again to the http: to paste it on PoshCode)
  452. #
  453. #
  454. # <?xml version="1.0" encoding="UTF-16" standalone="yes"?>
  455. # <feed xml:lang="en-US" xmlns="http ://www.w3.org/2005/Atom">
  456. #   <title>Test First Entry</title>
  457. #   <link>http ://HuddledMasses.org</link>
  458. #   <updated>2009-07-29T17:25:49Z</updated>
  459. #   <author>
  460. #      <name>Joel Bennett</name>
  461. #      <uri>http ://HuddledMasses.org</uri>
  462. #   </author>
  463. #   <id>http ://huddledmasses.org/</id>
  464. #   <entry>
  465. #     <title>Test First Entry</title>
  466. #     <link>http ://HuddledMasses.org/new-site-new-layout-lost-posts/</link>
  467. #     <id>http ://huddledmasses.org/new-site-new-layout-lost-posts/</id>
  468. #     <updated>2003-10-31T00:00:00Z</updated>
  469. #     <summary>Ema Lazarus' Poem</summary>
  470. #     <link rel="license" href="http ://creativecommons.org/licenses/by/3.0/" title="CC By-Attribution" />
  471. #     <dc:rights>Copyright 2009, Some rights reserved (licensed under the Creative Commons Attribution 3.0 Unported license)</dc:rights>
  472. #     <category scheme="http ://huddledmasses.org/tag/" term="huddled-masses" />
  473. #   </entry>
  474. # </feed>
  475. #
  476. #
  477. Param(
  478.    [Parameter(Mandatory = $true, Position = 0)]
  479.    [System.Xml.Linq.XName]$root
  480. ,
  481.    [Parameter(Mandatory = $false)]
  482.    [string]$Version = "1.0"
  483. ,
  484.    [Parameter(Mandatory = $false)]
  485.    [string]$Encoding = "UTF-8"
  486. ,
  487.    [Parameter(Mandatory = $false)]
  488.    [string]$Standalone = "yes"
  489. ,
  490.    [Parameter(Position=99, Mandatory = $false, ValueFromRemainingArguments=$true)]
  491.    [PSObject[]]$args
  492. )
  493. BEGIN {
  494.    $script:NameSpaceHash = New-Object 'Dictionary[String,XNamespace]'
  495.    $script:NameSpaceStack = New-Object 'Stack[XNamespace]'
  496.    if(![string]::IsNullOrEmpty( $root.NamespaceName )) {
  497.                 $script:NameSpaceStack.Push( $root.Namespace )
  498.    } elseif( $script:NameSpaceStack.Count -gt 0 ) {
  499.       $script:NameSpaceStack.Push( $script:NameSpaceStack.Peek() )
  500.    } else {
  501.       $script:NameSpaceStack.Push( $null )
  502.         }
  503. }
  504. PROCESS {
  505.    #New-Object XDocument (New-Object XDeclaration "1.0", "UTF-8", "yes"),(
  506.    New-Object XDocument (New-Object XDeclaration $Version, $Encoding, $standalone),(
  507.       New-Object XElement $(
  508.          $root
  509.          while($args) {
  510.             $attrib, $value, $args = $args
  511.             if($attrib -is [ScriptBlock]) {
  512.                Write-Verbose "Preparsed DSL: $attrib"
  513.                $attrib = DSL $attrib
  514.                Write-Verbose "Reparsed DSL: $attrib"
  515.                &$attrib
  516.             } elseif ( $value -is [ScriptBlock] -and "-Content".StartsWith($attrib)) {
  517.                $value = DSL $value
  518.                &$value
  519.             } elseif ( $value -is [XNamespace]) {
  520.                New-Object XAttribute ([XNamespace]::Xmlns + $attrib.TrimStart("-")), $value
  521.                $script:NameSpaceHash.Add($attrib.TrimStart("-"), $value)
  522.             } else {
  523.                New-Object XAttribute $attrib.TrimStart("-"), $value
  524.             }
  525.          }
  526.       ))
  527. }
  528. END {
  529.    $null = $script:NameSpaceStack.Pop()
  530. }
  531. }
  532.  
  533. Set-Alias xml New-XDocument
  534. Set-Alias New-Xml New-XDocument
  535.  
  536. function New-XAttribute {
  537. #.Synopsys
  538. #       Creates a new XAttribute (an xml attribute on an XElement for XDocument)
  539. #.Description
  540. #  This is the work-horse for the XML mini-dsl
  541. #.Parameter name
  542. #       The attribute name
  543. #.Parameter value
  544. #  The attribute value
  545. Param([Parameter(Mandatory=$true)]$name,[Parameter(Mandatory=$true)]$value)
  546.    New-Object XAttribute $name, $value
  547. }
  548. Set-Alias xa New-XAttribute
  549. Set-Alias New-XmlAttribute New-XAttribute
  550.  
  551.  
  552. function New-XElement {
  553. #.Synopsys
  554. #       Creates a new XElement (an xml tag for XDocument)
  555. #.Description
  556. #  This is the work-horse for the XML mini-dsl
  557. #.Parameter tag
  558. #       The name of the xml tag
  559. #.Parameter args
  560. #       this is where all the dsl magic happens. Please see the Examples. :)
  561. Param(
  562.    [Parameter(Mandatory = $true, Position = 0)]
  563.    [System.Xml.Linq.XName]$tag
  564. ,
  565.    [Parameter(Position=99, Mandatory = $false, ValueFromRemainingArguments=$true)]
  566.    [PSObject[]]$args
  567. )
  568. BEGIN {
  569.         if([string]::IsNullOrEmpty( $tag.NamespaceName )) {
  570.                 $tag = $($script:NameSpaceStack.Peek()) + $tag
  571.       if( $script:NameSpaceStack.Count -gt 0 ) {
  572.          $script:NameSpaceStack.Push( $script:NameSpaceStack.Peek() )
  573.       } else {
  574.          $script:NameSpaceStack.Push( $null )
  575.       }      
  576.         } else {
  577.       $script:NameSpaceStack.Push( $tag.Namespace )
  578.         }
  579. }
  580. PROCESS {
  581.   New-Object XElement $(
  582.      $tag
  583.      while($args) {
  584.         $attrib, $value, $args = $args
  585.         if($attrib -is [ScriptBlock]) { # then it's content
  586.            &$attrib
  587.         } elseif ( $value -is [ScriptBlock] -and "-Content".StartsWith($attrib)) { # then it's content
  588.            &$value
  589.         } elseif ( $value -is [XNamespace]) {
  590.            New-Object XAttribute ([XNamespace]::Xmlns + $attrib.TrimStart("-")), $value
  591.            $script:NameSpaceHash.Add($attrib.TrimStart("-"), $value)
  592.         } else {
  593.            New-Object XAttribute $attrib.TrimStart("-"), $value
  594.         }        
  595.      }
  596.    )
  597. }
  598. END {
  599.    $null = $script:NameSpaceStack.Pop()
  600. }
  601. }
  602. Set-Alias xe New-XElement
  603. Set-Alias New-XmlElement New-XElement
  604.  
  605.  
  606. function DSL {
  607. Param([ScriptBlock]$script)
  608.    $parserrors = $null
  609.    $global:tokens = [PSParser]::Tokenize( $script, [ref]$parserrors )
  610.    $duds = $global:tokens | Where-Object { $_.Type -eq "Command" -and ($(Get-Command $_.Content -EA 0) -eq $Null) }
  611.    [Array]::Reverse( $duds )
  612.    
  613.    [string[]]$ScriptText = "$script" -split "`n"
  614.  
  615.    ForEach($token in $duds ) {
  616.       # replace : notation with namespace notation
  617.       if( $token.Content.Contains(":") ) {
  618.          $key, $localname = $token.Content -split ":"
  619.          $ScriptText[($token.StartLine - 1)] = $ScriptText[($token.StartLine - 1)].Remove( $token.StartColumn -1, $token.Length ).Insert( $token.StartColumn -1, "'" + $($script:NameSpaceHash[$key] + $localname) + "'" )
  620.       }
  621.       # insert 'xe' before everything (unless it's a valid command)
  622.       $ScriptText[($token.StartLine - 1)] = $ScriptText[($token.StartLine - 1)].Insert( $token.StartColumn -1, "xe " )
  623.    }
  624.    Write-Output ([ScriptBlock]::Create( ($ScriptText -join "`n") ))
  625. }
  626.    
  627.  
  628. Export-ModuleMember -alias * -function New-XDocument, New-XAttribute, New-XElement, Remove-XmlNamespace, Convert-Xml, Select-Xml, Format-Xml

Submit a correction or amendment below (
click here to make a fresh posting)
After submitting an amendment, you'll be able to view the differences between the old and new posts easily.

Syntax highlighting:


Remember me