Types/MarkX/get_DataSet.ps1

<#
.SYNOPSIS
    Gets Markdown DataSets
.DESCRIPTION
    Gets Markdown tables as a dataset
#>

param()

# If we already have a data set, return it.
if ($this.'#DataSet') {return $this.'#DataSet' }

# If we could not coerce the markdown into XML,
# there is no dataset.
if (-not $this.'#XML') { return  }

# Query all table elements within the markdown
$tables = $this.'#XML' | Select-Xml //table

# Make a quick little filter to get at the inner text
filter innerText {
    $in = $_
    # PowerShell presents inner text in three ways:
    # 1. As a string property.
    if ($in -is [string]) { "$in" }
    # 2. As an element with an innerText property.
    elseif ($in.innerText) { "$($in.innerText)" }
    # 3. As a property bag with a `#text` property.
    elseif ($in.'#text') { "$($in.'#text')" }
}

# Create another filter to try to upcast things
function bestType {
    # We will do this by taking all of the input as an array
    # and casting to various strongly typed arrays.
    $allIn = @($input) + @(if ($args) { $args})    
    # In order of preference, that is:
    switch ($true) {
        # * `[float]`
        { $allIn -as [float[]] } {            
            [float]; break
        }
        # * `[double]`
        { $allIn -as [double[]] } {
            [double]; break
        }
        # * `[decimal]`
        { $allIn -as [decimal[]] } {
            [decimal]; break
        }
        # * `[long]`
        { $allIn -as [long[]] } {
            [long]; break
        }
        # * `[ulong]`
        { $allIn -as [ulong[]] } {
            [uint32]; break
        }        
        # * `[timespan]`
        { $allIn -as [timespan[]] } {
            [timespan]; break
        }
        # * `[DateTime]`
        { $allIn -as [DateTime[]] } {
            [DateTime]; break
        }        
        default {
            # * `[string]`
            [string]
        }
    }            
}

# Create a new dataset
$markdownData = [Data.DataSet]::new('MarkX')
# Keep track of our table number, since tables may not have names.
$tableNumber = 0
foreach ($table in $tables) {
    $tableNumber++
    $markdownDataTable  = $markdownData.Tables.Add("MarkdownTable$tableNumber")
    
    # Get our table headers' inner text.
    # These are our property names.
    [string[]]$PropertyNames = @( $table.Node.thead.tr.th | innerText )

    # We want to upcast our datatable as much as possible
    # so we need to collect the rows first

    # We will put them in a dictionary
    $TableDictionary = [Ordered]@{}
    $propertyIndex = 0
    # Walk over each property name
    foreach ($property in $propertyNames) {
        # get all values in this column as inner Text
        $TableDictionary[$property] = @(
            foreach ($row in $table.Node.tbody.tr) {
                @($row.td)[$propertyIndex] | innerText
            }
        )
        # and move onto the next property.
        $propertyIndex++
    }    

    # Now that we have all of the data collected,
    $markdownDataTable.Columns.AddRange(@(
        # we can create the data columns
        foreach ($property in $propertyNames) {            
            $propertyIndex = 0
            $bestType = $TableDictionary[$property] | bestType
            [Data.DataColumn]::new($property, $bestType, '', 'Attribute')
        }
        # We will always add one more column that contains the table row
        # Because this cannot always be serialized, we will hide it from the schema.
        [Data.DataColumn]::new('tr', [xml.xmlelement], '', 'Hidden')
    ))
        
    # Now we need to walk over each row.
    foreach ($row in $table.Node.tbody.tr) {
        # and get the values as inner text
        $propertyValues = @(            
            $row.td | innerText
            $row
        )
        # and add them.
        # PowerShell will cast the values to the appropriate types for the table.
        $null = $markdownDataTable.Rows.Add($propertyValues)
    }

    # Now, for bonus points, check to see if we have a description.
    # We can get a description from the previous sibling
    $previous = $table.Node.PreviousSibling
    # If it was a blockquote, that counts.
    if ($previous.LocalName -eq 'blockquote') {
        $markdownDataTable.ExtendedProperties.Add("description", $previous.InnerText)
        $previous = $previous.PreviousSibling
    }
    # Last but not least, we can derive a table name from a previous header.
    if ($previous.LocalName -match 'h[1-6]') {
        $markdownDataTable.TableName = $previous.InnerText
    }
}

# Cache our dataset to the object so we do not have to recompute it each tiem.
$this | Add-Member NoteProperty '#DataSet' $markdownData -Force

# And return the cached dataset.
return $this.'#DataSet'