Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
June 17, 2025

Creating a Markdown Tooling System Part 3

Welcome back. Over the last several articles, I've been sharing my scripting experiences in building a Markdown Tooling System. Last time, I shared my function and ways that I extended the object output to add value. I have some additional features I want to add. Remember, when reviewing the output of your command, consider what would make it more valuable or more straightforward to use. You don't have to include everything in the object. Some features can be added later, as I demonstrated last time. Don't forget that your custom object output needs to have a defined type name.

[PSCustomObject]@{
    PSTypeName = 'ButtondownMetadata'
    Title      = $meta.subject
    Published  = $meta.publish_date -as [datetime]
    Category   = $meta.email_type
    Link       = $web + $meta.slug
    Path       = $Path
    Status     = $meta.status
    isImported = $meta.Status -eq 'imported'
}

Let's add even more features to this.

Adding ScriptMethods

One feature that comes to mind is to add a script method to open the source Markdown file. It isn't that difficult to do this manually. Either of these expressions will open the file in Notepad:

Notepad $r[0].path
$r[0].path | Notepad

However, I want to add a script method to the object so that I can call it like this:

$r[0].Open()

> An argument could be made to use the name Edit instead of Open, but I prefer Open because it is more generic.

Creating a script method also allows me to include additional logic, such as checking if the file exists before trying to open it. Here's how I can do that:

Update-TypeData -TypeName ButtondownMetadata -MemberType ScriptMethod -MemberName Open -value {
    #it is possible the file no longer exists or has been moved
    if (Test-Path -Path $this.Path) {
        #open the file in the default editor
        Write-Host "$($PSStyle.Italic)Opening $($this.Path) in the default editor$($PSStyle.Reset)" -ForegroundColor Cyan
        Start-Process -FilePath $this.Path
    }
    else {
        Write-Warning "The file $($this.Path) cannot be found."
    }
} -force

The Write-Host line is unnecessary, but it provides a helpful indication that the file is being opened. For variety, I am using $PSStyle to format the message.

Invoke the Open() method
figure 1

For me, this action opens the file in VS Code.

I'd like to do the same thing with the Link property so that I can open the link in my default browser. Before I do that, I'm also going to define an alias property called Online, which might be more intuitive.

Update-TypeData -TypeName ButtondownMetadata -MemberType AliasProperty -MemberName Online -value Link -force

Once this is defined, I can create the script method to open the link in the default browser:

Update-TypeData -TypeName ButtondownMetadata -MemberType ScriptMethod -MemberName OpenOnline -value {
    #open the link in the default browser
    Write-Host "$($PSStyle.Italic)Opening $($this.Link) in the default browser$($PSStyle.Reset)" -ForegroundColor Cyan
    Start-Process -FilePath $this.Link
} -force

!> I could have used my alias, online, instead of Link in the script method, but I prefer to use the original property names in script methods.

Open online
figure 2

Customizing Property Values

I started this newsletter on Substack, then migrated it to Buttondown. I can see that in the Status property.

PS C:\> $r[0]

Title     : A Changelog for the Better
Published : 1/3/2023 1:05:58 PM
Category  : premium
Link      : https://buttondown.com/behind-the-powershell-pipeline/archive/a-chan...
Path      : D:\buttondown\emails\a-changelog-for-the-better.md
Status    : imported
Year      : 2023
YearMonth : 2023 January
Words     : 281
Month     : 1
MonthName : January
Online    : https://buttondown.com/behind-the-powershell-pipeline/archive/a-chan...

An imported email also implies that it was sent, and that is more important to me. I would like the Status property to reflect that. However, I shouldn't lose the original status of Imported, just in case I need it later.

What I need is a new Boolean property called isImported. This can be a static property defined at run time and not a type extension. I can also add logic in the function to set the Status property to Sent if the email was imported.

Now that I think of it, I might as well add similar boolean properties for IsScheduled and IsSent. Here's the revised function:

Function Get-ButtondownMetadata {
    [CmdletBinding()]
    [Alias('gbm')]
    [OutputType('ButtondownMetadata')]
    param (
        [Parameter(
            Mandatory,
            Position = 0,
            ValueFromPipeline,
            HelpMessage = 'Enter the path to the Markdown file containing the Buttondown newsletter metadata.'
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ }, ErrorMessage = 'Cannot find or verify the path {0}.')]
        [ValidatePattern('^.*\.md$', ErrorMessage = 'The path must point to a Markdown file.')]
        [string]$Path
    )

    Begin {
        #create a timer to measure the function runtime
        $timer = [System.Diagnostics.Stopwatch]::new()
        $timer.start()
        Write-Information "Starting $($MyInvocation.MyCommand)" -Tags runtime
        Write-Verbose "Starting $($MyInvocation.MyCommand)"
        $list = [System.Collections.Generic.List[string]]::new()
        #initialize a counter
        $k = 0
        $web = 'https://buttondown.com/behind-the-powershell-pipeline/archive/'
    } #begin
    Process {
        Write-Verbose "Processing $Path"
        Write-Information "Processing $Path" -Tags runtime
        $k++
        $content = Get-Content -Path $Path
        $list.AddRange([string[]]$content)
        $i = $list.FindIndex(0, { $args[0] -eq '---' }) + 1
        $j = $list.FindIndex($i, { $args[0] -match '---' }) - 1
        #only process if both markers found
        if ($i -AND $j) {
            Write-Debug "Processing metadata from line $i to $j"
            $lines = $list[$i..$j]
            Write-Information "Detected metadata header:`n---`n$(($lines | Out-String).Trim())`n---" -Tags data
            $lines | Where-Object { $_ -match ':' } |
            ForEach-Object -Begin {
                #initialize the hashtable
                $meta = @{}
            } -Process {
                #split into two strings
                $split = $_ -split ':', 2
                $meta.Add($split[0].Trim(), $split[1].Trim())
            }
            Write-Information $meta -Tags data
            If ($meta.subject -AND $meta.email_type) {
                [PSCustomObject]@{
                    PSTypeName  = 'ButtondownMetadata'
                    Title       = $meta.subject
                    Published   = $meta.publish_date -as [datetime]
                    Category    = $meta.email_type
                    Link        = $web + $meta.slug
                    Path        = $Path
                    Status      = $meta.status -match 'imported|sent' ? 'Sent' : $meta.status
                    isImported  = $meta.Status -eq 'imported'
                    isSent      = $meta.Status -match 'sent|imported'
                    isScheduled = $meta.Status -eq 'scheduled'
                }
                $list.Clear()
            } #if meta info found
            else {
                Write-Warning "No Buttondown metadata found in $Path"
            }
        }
        else {
            Write-Warning "No Buttondown metadata found in $Path"
        }
        #clear the list
        $list.Clear()
    } #process
    End {
        $timer.stop()
        Write-Verbose "Processed $k files in $($timer.Elapsed)"
        Write-Verbose "Ending $($MyInvocation.MyCommand)"
        Write-Information "Ending $($MyInvocation.MyCommand)" -Tags runtime
    } #end
}

I am using the PowerShell 7 ternary operator to define the Status property.

Status      = $meta.status -match 'imported|sent' ? 'Sent' : $meta.status

If the status is either imported or sent, it will be set to Sent. Otherwise, it will retain its original value.

PS C:\> $r | Group Status

Count Name                      Group
----- ----                      -----
    2 scheduled                 {@{Title=Creating a Markdown Tooling System; Published=6/10/2025 1…
  314 Sent                      {@{Title=A Changelog for the Better; Published=1/3/2023 1:05:58 PM…

PS C:\> $r | Where IsImported | Measure-Object | Select-Object Count

Count
-----
  175

PS C:\> $r | Where IsSent | Measure-Object -Property Words -Average -Sum |
Select-Object Count,Sum,Average

Count       Sum Average
-----       --- -------
  314 334753.00 1066.09
Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.