Add option to convert from markdown and to convert to DSL#14
Add option to convert from markdown and to convert to DSL#14AutoSysOps (Leo Visser) (autosysops) wants to merge 4 commits intoPSModule:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Markdown ↔ DSL conversion capabilities to the module by introducing a Markdown-to-object parser and an object-to-Markdown-DSL generator, along with documentation and Pester coverage.
Changes:
- Add
ConvertFrom-MarkdownMarkdownto parse Markdown files into a hierarchical object structure. - Add
ConvertTo-MarkdownDSLto generate an executable DSL scriptblock (or DSL string) from the parsed object structure. - Add extensive Pester tests and README documentation for the new conversion workflows.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 14 comments.
| File | Description |
|---|---|
| tests/Markdown.Tests.ps1 | Adds Pester coverage for parsing Markdown into objects, generating DSL, and executing the DSL. |
| src/functions/public/ConvertFrom-MarkdownMarkdown.ps1 | New parser converting Markdown text into a nested object tree (headers/details/paragraphs/codeblocks/tables). |
| src/functions/public/ConvertTo-MarkdownDSL.ps1 | New generator that emits DSL text/scriptblocks from the parsed object structure. |
| README.md | Documents the new conversion functions and a round-trip workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
AutoSysOps (Leo Visser) (autosysops)
left a comment
There was a problem hiding this comment.
fixed copilot review
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $markDownString += "[PSCustomObject]@{" | ||
| foreach ($property in $item.PSObject.Properties) { | ||
| $markDownString += "'$($property.Name)' = `'$($property.Value)`'; " | ||
| } | ||
| $markDownString += "}$newline" |
There was a problem hiding this comment.
Table row serialization writes '$property.Value' into PowerShell source without escaping embedded single quotes/newlines. A value like O'Reilly will generate invalid DSL, and crafted values could break out of the literal context. Escape table values for single-quoted PowerShell strings (double any ') before writing them into $markDownString.
| [Switch] $DontQuoteString | ||
| ) | ||
|
|
||
| # First the file is converted to a string containing the syntaxt for the markdown file then it will be invoked to be converted by the other functions. |
There was a problem hiding this comment.
Typo in comment: “syntaxt” should be “syntax”.
| # Loop through all childeren of the input object | ||
| foreach ($child in $InputObject) { |
There was a problem hiding this comment.
Typo in comment: “childeren” should be “children”.
| # Check if the word starts with ``` | ||
| if ($word -match '^```') { | ||
| if ($inCodeBlock) { | ||
| Write-Debug "[CodeBlock] End found, returning to parent" | ||
| # Go to the parent | ||
| $currentObject = [ref]$currentObject.Value.Parent | ||
|
|
||
| $inCodeBlock = $false |
There was a problem hiding this comment.
When $inCodeBlock is true, the parser still evaluates table/header/HTML logic for each line before it reaches this fence handler. As a result, code lines beginning with | or # can be mis-parsed as tables/headers instead of being kept as code content. Consider adding an early check near the start of the loop: if $inCodeBlock and the line is not a closing ``` fence, append the raw line to the current CodeBlock content and continue.
| $markDownString += "$($child.Type) " | ||
| if ($child.Level) { $markDownString += "$($child.Level) " } | ||
| if ($child.Title) { $markDownString += "`"$($child.Title)`" " } | ||
| if ($child.Language) { $markDownString += "`"$($child.Language)`" " } | ||
| $markDownString += "{$newline" |
There was a problem hiding this comment.
Title/Language are injected into the generated PowerShell source using double-quoted string literals without escaping. This breaks for values containing "/backticks/newlines and also enables PowerShell expansion ($var, $()) when the generated DSL is executed, which can change content or be abused if the Markdown is untrusted. Use a safe quoting/escaping strategy for PowerShell literals (typically single-quoted strings with ' doubled, or a dedicated escaping helper) before concatenating into $markDownString.
| [OutputType([scriptblock], [string])] | ||
| [CmdletBinding()] | ||
| param( | ||
| [Parameter(Mandatory = $true, Position = 0)] | ||
| [Object[]] $InputObject, | ||
|
|
||
| [Parameter(Mandatory = $false, Position = 1)] | ||
| [Switch] $AsString, | ||
|
|
||
| [Parameter(Mandatory = $false, Position = 2)] | ||
| [Switch] $DontQuoteString |
There was a problem hiding this comment.
[OutputType([scriptblock])] is inaccurate here since -AsString returns a [string]. This can mislead tooling and documentation generation. Consider declaring both output types (string + scriptblock) or adjusting the attribute to reflect the conditional output.
| $testFile = Join-Path ([System.IO.Path]::GetTempPath()) "PesterMarkdownTest_$([guid]::NewGuid().ToString()).md" | ||
| @' |
There was a problem hiding this comment.
These tests rely on $env:TEMP being set. On some PowerShell environments (especially non-Windows), TEMP may be unset or point somewhere unexpected, which can make the tests flaky. Consider using [System.IO.Path]::GetTempPath(), New-TemporaryFile, or Pester’s temp location patterns for portable test files.
| # Create a variable to hold the current working object in the converstion and helper variables | ||
| $currentObject = [ref]$returnObject |
There was a problem hiding this comment.
Typo in comment: “converstion” should be “conversion”.
| # return the right value | ||
| if ($AsString) { | ||
| Write-Debug "[Output] Returning as string" | ||
| return $markDownString | ||
| } | ||
| else { | ||
| Write-Debug "[Output] Returning as scriptblock" | ||
| return [scriptblock]::Create($markDownString) | ||
| } |
There was a problem hiding this comment.
ConvertTo-MarkdownDSL builds a PowerShell DSL script string from InputObject fields (e.g., Title, free‑text content, table cell values) and then passes it directly to [scriptblock]::Create, without escaping quotes or preventing PowerShell interpolation. When InputObject originates from ConvertFrom-MarkdownMarkdown reading a Markdown file, an attacker can craft headings, paragraphs, or table cells containing PowerShell syntax (e.g., $() subexpressions or embedded quotes/semicolons) so that arbitrary commands execute whenever the generated scriptblock is invoked. To mitigate this, treat Markdown‑derived data as pure data by rigorously escaping/encoding all interpolated values (including quotes and $), disabling DontQuoteString for untrusted input, or avoiding scriptblock creation entirely in favor of interpreting the object tree directly.
Description
In response to discussion #8 here is a PR which can read markdown files and generate a object structure out of it which is similar to the ast structure of PowerShell. There is also a function which converts this object structure to the DSL which can than be execute.
Type of change
Checklist