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.

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.

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