diff --git a/CHANGELOG.md b/CHANGELOG.md index 1145ddc..6338cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## MarkX 0.1.1: + +* MarkX Help (#26) +* MarkX now accepts commands / help (#27) +* MarkX.Headings gets heading elements (#29) +* MarkX.Lexicon gets lexicons in Markdown (#25) +* MarkX.YamlHeader support (#32, #33) +* Allowing piped markdown file input (#34) +* MarkX.Code gets code within markdown (#30) +* MarkX.CodeBlock gets code blocks within markdown (#31) +* MarkX.ToString can now stringify any property (#35) +* MarkX formatting (#36) + +--- + ## MarkX 0.1: * `Get-MarkX` ( #1, #4 ) diff --git a/Commands/Get-MarkX.ps1 b/Commands/Get-MarkX.ps1 index ad0973d..5f64855 100644 --- a/Commands/Get-MarkX.ps1 +++ b/Commands/Get-MarkX.ps1 @@ -1,14 +1,57 @@ function Get-MarkX { - [Alias('MarkX','Markdown','Get-Markdown')] + <# + .SYNOPSIS + Gets MarkX + .DESCRIPTION + Gets MarkX - Markdown as XML + + This allows us to query, extract, and customize markdown. + .EXAMPLE + # 'Hello World' In Markdown / MarkX + '# Hello World' | MarkX + .EXAMPLE + # MarkX is aliased to Markdown + # 'Hello World' as Markdown as XML + '# Hello World' | Markdown | Select -Expand XML + .EXAMPLE + # We can generate tables by piping in objects + @{n1=1;n2=2}, @{n1=2;n3=3} | MarkX + .EXAMPLE + # Make a TimesTable in MarkX + @( + "#### TimesTable" + foreach ($rowN in 1..9) { + $row = [Ordered]@{} + foreach ($colN in 1..9) { + $row["$colN"] = $colN * $rowN + } + $row + } + ) | Get-MarkX + .EXAMPLE + # We can pipe a command into MarkX + # This will get the command help as Markdown + Get-Command Get-MarkX | MarkX + .EXAMPLE + # We can pipe help into MarkX + Get-Help Get-MarkX | MarkX + .EXAMPLE + # We can get code from markdown + Get-Help Get-MarkX | + MarkX | + Select-Object -ExpandProperty Code + #> + [Alias('MarkX','Markdown','Get-Markdown')] param() $allInput = @($input) + $(if ($args) { $args - }) + }) - [PSCustomObject]@{ - PSTypeName = 'MarkX' - Markdown = $allInput - YamlHeader = $yamlheader - } + + $markx = New-Object PSObject -Property @{ + PSTypeName = 'MarkX' + } + $markx.Input = $allInput + $markx } \ No newline at end of file diff --git a/MarkX.format.ps1xml b/MarkX.format.ps1xml new file mode 100644 index 0000000..39e06d3 --- /dev/null +++ b/MarkX.format.ps1xml @@ -0,0 +1,27 @@ + + + + + MarkX + + MarkX + + + + + + + + + + + + Markdown + + + + + + + + \ No newline at end of file diff --git a/MarkX.ps1 b/MarkX.ps1 index b85ac9e..b52b2cd 100644 --- a/MarkX.ps1 +++ b/MarkX.ps1 @@ -6,6 +6,11 @@ "@ | MarkX +@" + +[![MarkX PowerShell Gallery](https://img.shields.io/powershellgallery/dt/MarkX)](https://www.powershellgallery.com/packages/MarkX/) + +"@ | MarkX @' @@ -226,3 +231,91 @@ $(& $GetMarkdownTableData) "@ | MarkX + +$lexiconMarkdown = @' + + +### Markdown Lexicons + +Since we can extra tables and data Markdown, we can also get any data of a particular known shape. + +The first special shape MarkX supports is an [at protocol lexicon](https://atproto.com/guides/lexicon) + +MarkX current supports lexicon type definitions. It will support query and procedure definitions in the future. + +A type definition consists of a namespace identifier, a description, and a series of properties. + +#### com.example.happy.birthday +> An example lexicon to record birthday messages + +|Property|Type|Description| +|-|-|-| +|`$type` | `[string]` | The type of the object. Must be `com.example.happy.birthday` | +|**`message`**| `[string]` | A birthday message | +|`forUri` | `[uri]` | A link | +|`birthday` | `[datetime]` | The birthday | +|`createdAt` | `[datetime]` | The time the record was created | + +'@ | MarkX + +$lexiconMarkdown + + +$lexiconJson = $lexiconMarkdown.Lexicon | ConvertTo-Json -Depth 5 + +$lexiconMarkdownExample = @' + + +To extract out a lexicon from the text above, we can: + +~~~PowerShell +$lexiconMarkdown.Lexicon | ConvertTo-Json -Depth 5 +~~~ + +Which gives us: + +'@ + @" + +~~~json +$lexiconJson +~~~ + +As you can see, we can take rich data within Markdown and process it into lexicons (or anything else we might want) +"@ + +$lexiconMarkdownExample | MarkX + + +$selfHelp = {Get-Help Get-MarkX | MarkX} + +$markdownHelp = @" + +### Markdown Help + +PowerShell commands generally contain help. + +We can pipe Get-Help into MarkX to get help as markdown + +~~~PowerShell +$selfHelp +~~~ + +When we run this, we get: + +"@ + +. $selfHelp + + +$InSummary = @" + +## In Summary + +MarkX is a simple and powerful tool. +It allows us to turn many objects into Markdown, and turn Markdown into many objects. + +Please pay around and see what you can do. + +"@ + +$InSummary | MarkX diff --git a/MarkX.psd1 b/MarkX.psd1 index f610d78..cc90e7c 100644 --- a/MarkX.psd1 +++ b/MarkX.psd1 @@ -12,7 +12,7 @@ RootModule = 'MarkX.psm1' # Version number of this module. -ModuleVersion = '0.1' +ModuleVersion = '0.1.1' # Supported PSEditions # CompatiblePSEditions = @() @@ -36,7 +36,7 @@ Description = 'MarkX - Markdown, XML, and PowerShell' TypesToProcess = @('MarkX.types.ps1xml') # Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() +FormatsToProcess = @('MarkX.format.ps1xml') # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = 'Get-MarkX' @@ -63,31 +63,22 @@ PrivateData = @{ # ReleaseNotes of this module ReleaseNotes = @' -## MarkX 0.1: - -* `Get-MarkX` ( #1, #4 ) -* Docs - * Sponsorship ( #20 ) - * Security ( #19 ) - * Contributing ( #18 ) - * Code of conduct ( #17 ) - * README (generated with MarkX) (#16) -* Extended Types - * `MarkX.get_HTML` ( #14 ) - * `MarkX.ToString()` ( #12 ) - * `MarkX.get_DataSet` ( #13 ) - * `MarkX.get_Images` (#11) - * `MarkX.get_Links` (#10) - * `MarkX.get_Table` ( #9 ) - * `MarkX.get_InnerText` ( #8 ) - * `MarkX.get_XML` ( #7 ) - * `MarkX.get/set_Markdown` ( #6 ) - * `MarkX.Sync` ( #5 ) - * `MarkX.ToTable` ( #22 ) -* MarkX build ( #2, #3 ) -* MarkX Action ( #21 ) -* MarkX Tests ( #15 ) - +## MarkX 0.1.1: + +* MarkX Help (#26) +* MarkX now accepts commands / help (#27) +* MarkX.Headings gets heading elements (#29) +* MarkX.Lexicon gets lexicons in Markdown (#25) +* MarkX.YamlHeader support (#32, #33) +* Allowing piped markdown file input (#34) +* MarkX.Code gets code within markdown (#30) +* MarkX.CodeBlock gets code blocks within markdown (#31) +* MarkX.ToString can now stringify any property (#35) +* MarkX formatting (#36) + +--- + +Additional release notes in [CHANGELOG](https://github.com/StartAutomating/MarkX/blob/main/CHANGELOG.md) '@ # Prerelease string of this module diff --git a/MarkX.types.ps1xml b/MarkX.types.ps1xml index 1615723..d8170ec 100644 --- a/MarkX.types.ps1xml +++ b/MarkX.types.ps1xml @@ -19,6 +19,10 @@ DB DataSet + + Heading + Headings + Link Links @@ -30,7 +34,160 @@ Sync @@ -230,12 +402,112 @@ foreach ($in in $Rows) { $markdownLines + + Code + + <# +.SYNOPSIS + Gets code +.DESCRIPTION + Gets code within Markdown / MarkX, grouped by language. +.EXAMPLE + " + # Hello World in PowerShell + ~~~PowerShell + 'Hello World' + ~~~ + " | markx | Select-Object -ExpandProperty Code +#> +$codeByLanguage = [Ordered]@{} + +foreach ($element in $this.XML | Select-Xml -XPath //code) { + $language = 'unknown' + if ($element.node.class -match 'language-(?<language>\S+)?') { + $language = $matches.language + } + if (-not $codeByLanguage[$language]) { + $codeByLanguage[$language] = @() + } + $codeByLanguage[$language] += $element.node.InnerText +} + +return $codeByLanguage + + + + CodeBlock + + <# +.SYNOPSIS + Gets code blocks +.DESCRIPTION + Gets code blocks within Markdown / MarkX +.EXAMPLE + " + # Hello World in PowerShell + ~~~PowerShell + 'Hello World' + ~~~ + " | markx | Select-Object -ExpandProperty CodeBlock +#> +$codeByLanguage = [Ordered]@{} + +foreach ($element in $this.XML | Select-Xml -XPath //code) { + $language = 'unknown' + if ($element.node.class -match 'language-(?<language>\S+)?') { + $language = $matches.language + } + if (-not $codeByLanguage[$language]) { + $codeByLanguage[$language] = @() + } + $codeByLanguage[$language] += $element.node +} + +return $codeByLanguage + + DataSet return $this.'#DataSet' + + Header + + if (-not $this.'#YamlHeader') { return } + +$convertFromYaml = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Alias,Cmdlet,Function') +if (-not $convertFromYaml) { + Write-Warning "Cannot get header without ConvertFrom-Yaml" + return +} + +return ($this.'#YamlHeader' | & $convertFromYaml) + + + param($header) + +$this.YamlHeader = $header + + + + Headings + + <# +.SYNOPSIS + Gets Markdown headings +.DESCRIPTION + Gets any heading elements in the markdown +#> +$this.XML | + Select-Xml -XPath //* | + Where-Object { + $_.Node.LocalName -match 'h[1-6]' + } | + Select-Object -ExpandProperty Node + + HTML @@ -259,6 +531,113 @@ return ("$($this.XML.XHTML.InnerXML)" + [Environment]::NewLine) $this.XML.XHTML.InnerText + + Input + + return $this.'#input' + + + param( +[PSObject[]]$InputObject +) + +$this | Add-Member NoteProperty '#Input' $InputObject -Force + +$this.Sync() + + + + Lexicon + + <# +.SYNOPSIS + Gets Lexicons from Markdown +.DESCRIPTION + Gets At Protocol Lexicons defined in Markdown. + + A lexicon table must have at least three columns: + + * Property + * Type + * Description + + It must also contain a row containing the property `$type`. + + This will be considered the lexicon's type. + + Any bolded or italic fields will be considered required. + + Lexicon tables may also be preceeded by an element containing the description. +#> + +$markdownData = $this.DataSet +:nextTable foreach ($table in $markdownData.Tables) { + $isLexiconTable = $table.Columns['Property'] -and $table.Columns['Type'] -and $table.Columns['Description'] + + if (-not $isLexiconTable) { continue nextTable } + + $hasType = $table.Select("Property='`$type'") + if (-not $hasType) { + Write-Warning "Missing `$type" + continue nextTable + } + + $lexiconType = if ($hasType.Description -match '(?:[^\.\s]+\.){3}[^\.\s]+') { + $matches.0 + } + + if (-not $lexiconType) { + continue nextTable + } + + $lexiconObject = [Ordered]@{ + lexicon = 1 + id = $lexiconType + defs = @{ + main = [Ordered]@{ + type = 'record' + description = + if ($table.ExtendedProperties.Description) { + $table.ExtendedProperties.Description + } else { + $lexiconType + } + required = @() + properties = [Ordered]@{} + } + } + } + + foreach ($row in $table) { + + if ($row.Property -eq '$type') { continue } + $lexProp = [Ordered]@{} + $lexProp.type = + switch -regex ($row.type) { + '\[\]' { 'array' } + 'object' { 'object' } + 'string' { 'string' } + 'bool|switch' { 'boolean' } + 'int|number|float|double' { 'number' } + 'date' { 'datetime' } + 'ur[il]' { 'uri'} + } + + $lexProp.description = $row.Description + if ($row.Property -match '\*') { + $lexiconObject.defs.main.required += $row.Property -replace '\*' + } + elseif ($row.tr.td.outerxml -match '<(?>b|i|strong)>') { + $lexiconObject.defs.main.required += $row.Property -replace '\*' + } + + $lexiconObject.defs.main.properties[$row.Property -replace '\*'] = $lexProp + } + + $lexiconObject +} + + Links @@ -279,53 +658,8 @@ return ("$($this.XML.XHTML.InnerXML)" + [Environment]::NewLine) [PSObject[]]$Markdown ) -$currentRows = @() -$allMarkdown = @(foreach ($md in $Markdown) { - if ($md -isnot [string]) { - if ($md -is [ScriptBlock]) { - $md = "<pre><code class='language-powershell'>$( - [Web.HttpUtility]::HtmlEncode( - "$md" - ) - )</code><pre>" - } - if ($md -is [Collections.IDictionary] -or - ($md.GetType -and -not $md.GetType().IsPrimitive)) { - $currentRows += $md - continue - } - } - - if ($currentRows) { - $this.ToTable($currentRows) - $currentRows = @() - } - - if ($md -match '(?>\.md|markdown)$' -and - (Test-Path $md -ErrorAction Ignore) - ) { - $md = Get-Content -Raw $md - } - - $yamlheader = '' - if ($md -match '^---') { - $null, $yamlheader, $md = $in -split '---', 3 - } - - $md -}) +$this.Input = $Markdown -if ($currentRows) { - $allMarkdown += $this.ToTable($currentRows) - $currentRows = @() -} - -$markdown = $allMarkdown -join [Environment]::NewLine - -$this | - Add-Member NoteProperty '#Markdown' $Markdown -Force - -$this.Sync() @@ -340,6 +674,38 @@ $this.Sync() return $this.'#XMl' + + YamlHeader + + return $this.'#YamlHeader' + + + param($header) + +if ($header -is [string]) { + $this | Add-Member NoteProperty '#YamlHeader' $header -Force + return +} + +$convertToYaml = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertTo-Yaml', 'Alias,Cmdlet,Function') +if (-not $convertToYaml) { + Write-Warning "Cannot set yaml header without converter" + return +} + +$convertParameters = @{} +if ($convertToYaml.Parameters['Depth']) { + $convertParameters['Depth'] = $FormatEnumerationLimit +} +$toYaml = $header | & $convertToYaml @convertParameters +if ($toYaml -is [string]) { + $this | Add-Member NoteProperty '#YamlHeader' $toYaml -Force +} + + + + + DefaultDisplay Markdown diff --git a/README.md b/README.md index 81233a7..f32269d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ ## Greetings from MarkX! +[![MarkX PowerShell Gallery](https://img.shields.io/powershellgallery/dt/MarkX)](https://www.powershellgallery.com/packages/MarkX/) + + ### What Is MarkX? MarkX is a useful little tool built around a useful little trick. @@ -181,3 +184,126 @@ When we run this example, we get:

abc

abc
123
456
789

def

def
123
246
369
4812
12 10 + + +### Markdown Lexicons + +Since we can extra tables and data Markdown, we can also get any data of a particular known shape. + +The first special shape MarkX supports is an [at protocol lexicon](https://atproto.com/guides/lexicon) + +MarkX current supports lexicon type definitions. It will support query and procedure definitions in the future. + +A type definition consists of a namespace identifier, a description, and a series of properties. + +#### com.example.happy.birthday +> An example lexicon to record birthday messages + +|Property|Type|Description| +|-|-|-| +|`$type` | `[string]` | The type of the object. Must be `com.example.happy.birthday` | +|**`message`**| `[string]` | A birthday message | +|`forUri` | `[uri]` | A link | +|`birthday` | `[datetime]` | The birthday | +|`createdAt` | `[datetime]` | The time the record was created | + + + +To extract out a lexicon from the text above, we can: + +~~~PowerShell +$lexiconMarkdown.Lexicon | ConvertTo-Json -Depth 5 +~~~ + +Which gives us: + +~~~json +{ + "lexicon": 1, + "id": "com.example.happy.birthday", + "defs": { + "main": { + "type": "record", + "description": "com.example.happy.birthday", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string", + "description": "A birthday message" + }, + "forUri": { + "type": "uri", + "description": "A link" + }, + "birthday": { + "type": "datetime", + "description": "The birthday" + }, + "createdAt": { + "type": "datetime", + "description": "The time the record was created" + } + } + } + } +} +~~~ + +As you can see, we can take rich data within Markdown and process it into lexicons (or anything else we might want) +# Get-MarkX +## Gets MarkX +### Gets MarkX - Markdown as XML + +This allows us to query, extract, and customize markdown. + +'Hello World' In Markdown / MarkX +~~~PowerShell +'# Hello World' | MarkX +~~~ +MarkX is aliased to Markdown +'Hello World' as Markdown as XML +~~~PowerShell +'# Hello World' | Markdown | Select -Expand XML +~~~ +We can generate tables by piping in objects +~~~PowerShell +@{n1=1;n2=2}, @{n1=2;n3=3} | MarkX +~~~ +Make a TimesTable in MarkX +~~~PowerShell +@( + "#### TimesTable" + foreach ($rowN in 1..9) { + $row = [Ordered]@{} + foreach ($colN in 1..9) { + $row["$colN"] = $colN * $rowN + } + $row + } +) | Get-MarkX +~~~ +We can pipe a command into MarkX +This will get the command help as Markdown +~~~PowerShell +Get-Command Get-MarkX | MarkX +~~~ +We can pipe help into MarkX +~~~PowerShell +Get-Help Get-MarkX | MarkX +~~~ +We can get code from markdown +~~~PowerShell +Get-Help Get-MarkX | + MarkX | + Select-Object -ExpandProperty Code +~~~ + +## In Summary + +MarkX is a simple and powerful tool. +It allows us to turn many objects into Markdown, and turn Markdown into many objects. + +Please pay around and see what you can do. + diff --git a/Types/MarkX/Alias.psd1 b/Types/MarkX/Alias.psd1 index 3d378e6..e454738 100644 --- a/Types/MarkX/Alias.psd1 +++ b/Types/MarkX/Alias.psd1 @@ -2,4 +2,5 @@ DB = 'DataSet' Tables = 'Table' Link = 'Links' + Heading = 'Headings' } \ No newline at end of file diff --git a/Types/MarkX/MarkX.format.ps1 b/Types/MarkX/MarkX.format.ps1 new file mode 100644 index 0000000..7dde8b0 --- /dev/null +++ b/Types/MarkX/MarkX.format.ps1 @@ -0,0 +1 @@ +Write-FormatView -TypeName MarkX -Property Markdown -Wrap diff --git a/Types/MarkX/Sync.ps1 b/Types/MarkX/Sync.ps1 index 82fa5ee..affd14d 100644 --- a/Types/MarkX/Sync.ps1 +++ b/Types/MarkX/Sync.ps1 @@ -1,3 +1,156 @@ +$currentRows = @() +$allMarkdown = @(:nextInput foreach ($md in $this.Input) { + if ($md -isnot [string]) { + # If the markdown was a file + if ($md -is [IO.FileInfo] -and + # and it had the extension .md or markdown + $md.Extension -in '.md', '.markdown') { + $md = Get-Content -LiteralPath $md.Fullname -Raw + $md + continue + } + + if ($md -is [Management.Automation.CommandInfo]) { + $cmdHelp = if ( + $md -is [Management.Automation.FunctionInfo] -or + $md -is [Management.Automation.AliasInfo] + ) { + Get-Help -Name $md.Name + } elseif ($md -is [Management.Automation.ExternalScriptInfo]) { + Get-Help -Name $md.Source + } else { + continue nextInput + } + if ($cmdHelp) { + $md = $cmdHelp + } + } + + if ($md.pstypenames -match 'HelpInfo') { + @( + if ($md.Name -match '[\\/]') { + "# $(@($md.Name -split '[\\/]')[-1] -replace '\.ps1')" + } else { + "# $($md.Name)" + } + + if ($md.Synopsis) { + "## $($md.Synopsis)" + } + $description = $md.Description.text -join [Environment]::NewLine + if ($description) { + "### $($description)" + } + + $md.alertset.alert.text -join [Environment]::NewLine + + foreach ($example in $md.examples.example) { + $exampleNumber++ + + # Combine the code and remarks + $exampleLines = + @( + $example.Code + foreach ($remark in $example.Remarks.text) { + if (-not $remark) { continue } + $remark + } + ) -join ([Environment]::NewLine) -split '(?>\r\n|\n)' # and split into lines + + # Anything until the first non-comment line is a markdown predicate to the example + $nonCommentLine = $false + $markdownLines = @() + + # Go thru each line in the example as part of a loop + $codeBlock = @(foreach ($exampleLine in $exampleLines) { + # Any comments until the first uncommentedLine are markdown + if ($exampleLine -match '^\#' -and -not $nonCommentLine) { + $markdownLines += $exampleLine -replace '^\#' -replace '^\s+' + } else { + $nonCommentLine = $true + $exampleLine + } + }) -join [Environment]::NewLine + + # Join all of our markdown lines together + $markdownLines -join [Environment]::NewLine + "~~~PowerShell" + $codeBlock + "~~~" + } + ) -join [Environment]::NewLine + continue nextInput + } + if ($md -is [ScriptBlock]) { + "
$(
+                [Web.HttpUtility]::HtmlEncode(
+                    "$md"
+                )
+            )
"
+            continue nextInput
+        } 
+        if ($md -is [Collections.IDictionary] -or 
+            ($md.GetType -and 
+                (-not $md.GetType().IsPrimitive)
+            )  
+        ) {            
+            $currentRows += $md            
+            continue
+        }        
+    }
+    
+    if ($currentRows) {    
+        $this.ToTable($currentRows)        
+        $currentRows = @()
+    }
+
+    if ($md -match '(?>\.md|markdown)$' -and
+        (Test-Path $md -ErrorAction Ignore)
+    ) {
+        $md = Get-Content -Raw $md
+    }
+
+    $yamlheader = ''
+    if ($md -match '^---') {
+        $null, $yamlheader, $md = $in -split '---', 3
+        if ($yamlheader) {
+            $this | Add-Member NoteProperty '#YamlHeader' $yamlheader -Force
+        }
+    }
+
+    $md
+})
+
+if ($currentRows) {    
+    $allMarkdown += $this.ToTable($currentRows)
+    $currentRows = @()
+}
+
+$yamlHeaders = @()
+$allMarkdown = @(foreach ($md in $allMarkdown) {
+    if ($md -match '^---') {
+        $null, $yamlheader, $restOfMakdown = $md -split '---', 3
+        if ($yamlheader) {
+            $yamlHeaders+= $yamlheader            
+        }
+        $restOfMakdown
+    } else {
+        $md
+    }
+})
+
+if ($yamlHeaders) {
+    $yamlHeader = $yamlHeaders -join (
+        [Environment]::NewLine + '---' + [Environment]::NewLine
+    )
+    $this | Add-Member NoteProperty '#YamlHeader' $yamlHeader -Force
+}
+
+$markdown = $allMarkdown -join [Environment]::NewLine
+
+$this | 
+    Add-Member NoteProperty '#Markdown' $Markdown -Force
+
 $Markdown = $this.'#Markdown'
 
 if (-not $Markdown) { return }
@@ -17,7 +170,6 @@ $this |
 if (-not $this.'#XML') { return }
 
 $tables = $this.'#XML' | Select-Xml //table
-if (-not $tables) { return }
 filter innerText {
     $in = $_
     if ($in -is [string]) { "$in" }
diff --git a/Types/MarkX/ToString.ps1 b/Types/MarkX/ToString.ps1
index 12b6c8c..035c9d2 100644
--- a/Types/MarkX/ToString.ps1
+++ b/Types/MarkX/ToString.ps1
@@ -1,2 +1,18 @@
+if ($args) {
+    $anyOutput = foreach ($arg in $args) {
+        $thisArg = $this.$arg
+        if ($thisArg) {
+            if ($thisArg.XHTML.InnerXML) {
+                "$($thisArg.XHTML.InnerXML)" + [Environment]::NewLine
+            } else {
+                "$thisArg"
+            }
+        }
+    }
+    if ($anyOutput) {
+        return $anyOutput -join [Environment]::NewLine
+    }    
+}
+
 if (-not $this.XML.XHTML) { return '' }
 return ("$($this.XML.XHTML.InnerXML)" + [Environment]::NewLine)
\ No newline at end of file
diff --git a/Types/MarkX/get_Code.ps1 b/Types/MarkX/get_Code.ps1
new file mode 100644
index 0000000..4f53108
--- /dev/null
+++ b/Types/MarkX/get_Code.ps1
@@ -0,0 +1,27 @@
+<#
+.SYNOPSIS
+    Gets code
+.DESCRIPTION
+    Gets code within Markdown / MarkX, grouped by language.
+.EXAMPLE
+    "
+    # Hello World in PowerShell
+    ~~~PowerShell
+    'Hello World'
+    ~~~
+    " | markx | Select-Object -ExpandProperty Code 
+#>
+$codeByLanguage = [Ordered]@{}
+
+foreach ($element in $this.XML | Select-Xml -XPath //code) {
+    $language = 'unknown'
+    if ($element.node.class -match 'language-(?\S+)?') {
+        $language = $matches.language        
+    }
+    if (-not $codeByLanguage[$language]) {
+        $codeByLanguage[$language] = @()
+    }
+    $codeByLanguage[$language] += $element.node.InnerText
+}
+
+return $codeByLanguage
\ No newline at end of file
diff --git a/Types/MarkX/get_CodeBlock.ps1 b/Types/MarkX/get_CodeBlock.ps1
new file mode 100644
index 0000000..1a6f934
--- /dev/null
+++ b/Types/MarkX/get_CodeBlock.ps1
@@ -0,0 +1,27 @@
+<#
+.SYNOPSIS
+    Gets code blocks
+.DESCRIPTION
+    Gets code blocks within Markdown / MarkX
+.EXAMPLE
+    "
+    # Hello World in PowerShell
+    ~~~PowerShell
+    'Hello World'
+    ~~~
+    " | markx | Select-Object -ExpandProperty CodeBlock 
+#>
+$codeByLanguage = [Ordered]@{}
+
+foreach ($element in $this.XML | Select-Xml -XPath //code) {
+    $language = 'unknown'
+    if ($element.node.class -match 'language-(?\S+)?') {
+        $language = $matches.language        
+    }
+    if (-not $codeByLanguage[$language]) {
+        $codeByLanguage[$language] = @()
+    }
+    $codeByLanguage[$language] += $element.node
+}
+
+return $codeByLanguage
\ No newline at end of file
diff --git a/Types/MarkX/get_Header.ps1 b/Types/MarkX/get_Header.ps1
new file mode 100644
index 0000000..782f37f
--- /dev/null
+++ b/Types/MarkX/get_Header.ps1
@@ -0,0 +1,9 @@
+if (-not $this.'#YamlHeader') { return }
+
+$convertFromYaml = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertFrom-Yaml', 'Alias,Cmdlet,Function')
+if (-not $convertFromYaml) {
+    Write-Warning "Cannot get header without ConvertFrom-Yaml"
+    return
+}
+
+return ($this.'#YamlHeader' | & $convertFromYaml)
\ No newline at end of file
diff --git a/Types/MarkX/get_Headings.ps1 b/Types/MarkX/get_Headings.ps1
new file mode 100644
index 0000000..d31ed45
--- /dev/null
+++ b/Types/MarkX/get_Headings.ps1
@@ -0,0 +1,12 @@
+<#
+.SYNOPSIS
+    Gets Markdown headings
+.DESCRIPTION
+    Gets any heading elements in the markdown
+#>
+$this.XML | 
+    Select-Xml -XPath //* |
+    Where-Object {
+        $_.Node.LocalName -match 'h[1-6]'
+    } |
+    Select-Object -ExpandProperty Node
\ No newline at end of file
diff --git a/Types/MarkX/get_Input.ps1 b/Types/MarkX/get_Input.ps1
new file mode 100644
index 0000000..890e9e0
--- /dev/null
+++ b/Types/MarkX/get_Input.ps1
@@ -0,0 +1 @@
+return $this.'#input'
\ No newline at end of file
diff --git a/Types/MarkX/get_Lexicon.ps1 b/Types/MarkX/get_Lexicon.ps1
new file mode 100644
index 0000000..4b9fb65
--- /dev/null
+++ b/Types/MarkX/get_Lexicon.ps1
@@ -0,0 +1,87 @@
+<#
+.SYNOPSIS
+    Gets Lexicons from Markdown 
+.DESCRIPTION
+    Gets At Protocol Lexicons defined in Markdown.
+
+    A lexicon table must have at least three columns:
+    
+    * Property
+    * Type
+    * Description
+
+    It must also contain a row containing the property `$type`.
+
+    This will be considered the lexicon's type.
+
+    Any bolded or italic fields will be considered required.
+
+    Lexicon tables may also be preceeded by an element containing the description.
+#>
+
+$markdownData = $this.DataSet
+:nextTable foreach ($table in $markdownData.Tables) {
+    $isLexiconTable = $table.Columns['Property'] -and $table.Columns['Type'] -and $table.Columns['Description']
+
+    if (-not $isLexiconTable) { continue nextTable }
+
+    $hasType = $table.Select("Property='`$type'")
+    if (-not $hasType) {
+        Write-Warning "Missing `$type"
+        continue nextTable
+    }
+
+    $lexiconType = if ($hasType.Description -match '(?:[^\.\s]+\.){3}[^\.\s]+') {
+        $matches.0
+    }
+
+    if (-not $lexiconType) {
+        continue nextTable
+    }
+
+    $lexiconObject = [Ordered]@{
+        lexicon = 1
+        id = $lexiconType
+        defs = @{
+            main = [Ordered]@{
+                type = 'record'
+                description = 
+                    if ($table.ExtendedProperties.Description) {
+                        $table.ExtendedProperties.Description
+                    } else {
+                        $lexiconType
+                    }
+                required = @()
+                properties = [Ordered]@{}
+            }
+        }
+    }
+
+    foreach ($row in $table) {
+        
+        if ($row.Property -eq '$type') { continue }
+        $lexProp = [Ordered]@{}            
+        $lexProp.type =                 
+            switch -regex ($row.type) {
+                '\[\]' { 'array' }
+                'object' { 'object' }
+                'string' { 'string' }
+                'bool|switch' { 'boolean' }
+                'int|number|float|double' { 'number' }
+                'date' { 'datetime' }
+                'ur[il]' { 'uri'}
+            }
+            
+        $lexProp.description = $row.Description
+        if ($row.Property -match '\*') {
+            $lexiconObject.defs.main.required += $row.Property -replace '\*'
+        } 
+        elseif ($row.tr.td.outerxml -match '<(?>b|i|strong)>') {
+            $lexiconObject.defs.main.required += $row.Property -replace '\*'
+        }
+        
+        $lexiconObject.defs.main.properties[$row.Property -replace '\*'] = $lexProp
+    }
+
+    $lexiconObject        
+}
\ No newline at end of file
diff --git a/Types/MarkX/get_YamlHeader.ps1 b/Types/MarkX/get_YamlHeader.ps1
new file mode 100644
index 0000000..bbca11b
--- /dev/null
+++ b/Types/MarkX/get_YamlHeader.ps1
@@ -0,0 +1 @@
+return $this.'#YamlHeader'
\ No newline at end of file
diff --git a/Types/MarkX/set_Header.ps1 b/Types/MarkX/set_Header.ps1
new file mode 100644
index 0000000..cea3547
--- /dev/null
+++ b/Types/MarkX/set_Header.ps1
@@ -0,0 +1,3 @@
+param($header)
+
+$this.YamlHeader = $header
\ No newline at end of file
diff --git a/Types/MarkX/set_Input.ps1 b/Types/MarkX/set_Input.ps1
new file mode 100644
index 0000000..33478d2
--- /dev/null
+++ b/Types/MarkX/set_Input.ps1
@@ -0,0 +1,7 @@
+param(
+[PSObject[]]$InputObject
+)
+
+$this | Add-Member NoteProperty '#Input' $InputObject -Force
+
+$this.Sync()
\ No newline at end of file
diff --git a/Types/MarkX/set_Markdown.ps1 b/Types/MarkX/set_Markdown.ps1
index f164646..b7d51e7 100644
--- a/Types/MarkX/set_Markdown.ps1
+++ b/Types/MarkX/set_Markdown.ps1
@@ -2,50 +2,4 @@ param(
 [PSObject[]]$Markdown
 )
 
-$currentRows = @()
-$allMarkdown = @(foreach ($md in $Markdown) {    
-    if ($md -isnot [string]) {        
-        if ($md -is [ScriptBlock]) {
-            $md = "
$(
-                [Web.HttpUtility]::HtmlEncode(
-                    "$md"
-                )
-            )
"
-        } 
-        if ($md -is [Collections.IDictionary] -or 
-            ($md.GetType -and -not $md.GetType().IsPrimitive))  {            
-            $currentRows += $md            
-            continue
-        }
-    }
-    
-    if ($currentRows) {    
-        $this.ToTable($currentRows)        
-        $currentRows = @()
-    }
-
-    if ($md -match '(?>\.md|markdown)$' -and
-        (Test-Path $md -ErrorAction Ignore)
-    ) {
-        $md = Get-Content -Raw $md
-    }
-
-    $yamlheader = ''
-    if ($md -match '^---') {
-        $null, $yamlheader, $md = $in -split '---', 3
-    }
-
-    $md
-})
-
-if ($currentRows) {    
-    $allMarkdown += $this.ToTable($currentRows)
-    $currentRows = @()
-}
-
-$markdown = $allMarkdown -join [Environment]::NewLine
-
-$this | 
-    Add-Member NoteProperty '#Markdown' $Markdown -Force
-
-$this.Sync()
\ No newline at end of file
+$this.Input = $Markdown
diff --git a/Types/MarkX/set_YamlHeader.ps1 b/Types/MarkX/set_YamlHeader.ps1
new file mode 100644
index 0000000..ab46613
--- /dev/null
+++ b/Types/MarkX/set_YamlHeader.ps1
@@ -0,0 +1,23 @@
+param($header)
+
+if ($header -is [string]) {
+    $this | Add-Member NoteProperty '#YamlHeader' $header -Force
+    return
+}
+
+$convertToYaml = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertTo-Yaml', 'Alias,Cmdlet,Function')
+if (-not $convertToYaml) {
+    Write-Warning "Cannot set yaml header without converter"
+    return
+}
+
+$convertParameters = @{}
+if ($convertToYaml.Parameters['Depth']) {
+    $convertParameters['Depth'] = $FormatEnumerationLimit
+}
+$toYaml = $header | & $convertToYaml @convertParameters
+if ($toYaml -is [string]) {
+    $this | Add-Member NoteProperty '#YamlHeader' $toYaml -Force
+}
+
+