diff --git a/.github/workflows/pester-tests.yml b/.github/workflows/pester-tests.yml index dfdc459..e6f3949 100644 --- a/.github/workflows/pester-tests.yml +++ b/.github/workflows/pester-tests.yml @@ -20,6 +20,11 @@ jobs: Set-PSRepository PSGallery -InstallationPolicy Trusted Install-Module -Name Pester -Force -SkipPublisherCheck Install-Module -Name PowerShell-Yaml -Force -SkipPublisherCheck + + - name: Install pandoc + shell: pwsh + run: | + choco install pandoc -y - name: Run Pester Tests shell: pwsh diff --git a/README.md b/README.md index 4504467..a037056 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,35 @@ A PowerShell library for publishing markdown files authored in markdown to Blogg Get-BloggerPosts ``` +1. Fetch an individual post from your blog + + ``` + Get-BloggerPost -PostId + ``` + + You can also persist the post to disk as HTML or markdown in the current directory. + + When using `HTML` format, files are saved as `.html` + + When using `Markdown` format, files are saved as `.md` + + ``` + Get-BloggerPost -PostId <postid> -Format HTML + Get-BloggerPost -PostId <postid> -Format Markdown + ``` + + You can specify an output directory where the file will be saved. + + ``` + Get-BloggerPost -PostId <postid> -OutDirectory "C:\MyPosts" -Format HTML + ``` + + You can also include a `FolderDateFormat` that uses the `published` property of the blog post to construct a subfolder. + + ``` + Get-BloggerPost -PostId <postId> -OutDirectory ".\Blog" -FolderDateFormat "YYYY\\MM" -Format Markdown + ``` + 1. Publish a markdown file to your blog as draft ``` diff --git a/src/public/ConvertTo-HtmlFromMarkdown.ps1 b/src/public/ConvertTo-HtmlFromMarkdown.ps1 index 3ca0081..46da4fe 100644 --- a/src/public/ConvertTo-HtmlFromMarkdown.ps1 +++ b/src/public/ConvertTo-HtmlFromMarkdown.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Convert a file to Markdown using Pandoc + Convert a Markdown file to HTML using Pandoc .PARAMETER File The file path of the markdown file diff --git a/src/public/ConvertTo-MarkdownFromHtml.ps1 b/src/public/ConvertTo-MarkdownFromHtml.ps1 new file mode 100644 index 0000000..a350b44 --- /dev/null +++ b/src/public/ConvertTo-MarkdownFromHtml.ps1 @@ -0,0 +1,73 @@ +<# +.SYNOPSIS + Convert HTML content or a HTML file to Markdown using Pandoc + +.PARAMETER File + The file path of the html file. Required when Content is not specified. + +.PARAMETER Content + The HTML content to convert to Markdown. Required when File is not specified. + +.PARAMETER OutFile + The resulting markdown file, if specified. + +#> +function ConvertTo-MarkdownFromHtml { + param( + [Parameter(Mandatory, ParameterSetName = "FromFile")] + [ValidateScript({ Test-Path $_ -PathType Leaf })] + [string]$File, + + [Parameter(Mandatory, ParameterSetName = "FromContent")] + [string]$Content, + + [Parameter(ParameterSetName = "FromFile")] + [Parameter(Mandatory=$false, ParameterSetName = "FromContent")] + [string]$OutFile + ) + + # when FromContent is specified, write the content to a temporary file + if ($PSCmdlet.ParameterSetName -eq "FromContent") { + # If content is provided, create a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + ".html" + Set-Content -Path $tempFile -Value $Content + $File = $tempFile + } + + # ensure that the file is an absolute path because pandoc.exe doesn't like powershell relative paths + $File = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($File) + + # Use pandoc to convert the markdown to Html + $pandocArgs = "`"{0}`" " -f $File + $pandocArgs += "-f {0} " -f $BloggerSession.PandocHtmlFormat + $pandocArgs += "-t {0} " -f $BloggerSession.PandocMarkdownFormat + + # add additional command-line arguments + if ($BloggerSession.PandocAdditionalArgs) { + Write-Verbose "Using additional args" + $pandocArgs += "{0} " -f $BloggerSession.PandocAdditionalArgs + } + + if (!($OutFile)) { + $OutFile = Join-Path (Split-Path $File -Parent) ((Split-Path $File -LeafBase) + ".md") + Write-Verbose "Using OutFile: $OutFile" + } + # ensure that the file is an absolute path because pandoc.exe doesn't like powershell relative paths + $OutFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutFile) + + $pandocArgs += "-o `"{0}`" " -f $OutFile + + Write-Verbose ">> pandoc $($pandocArgs)" + Start-Process pandoc -ArgumentList $pandocArgs -NoNewWindow -Wait + + $content = Get-Content $OutFile -Raw + + # remove temporary files + if ($PSCmdlet.ParameterSetName -eq "FromContent") { + Remove-Item $File + } elseif (!$PSBoundParameters.ContainsKey("OutFile")) { + Remove-Item $OutFile + } + + return $content +} \ No newline at end of file diff --git a/src/public/Get-BloggerPost.ps1 b/src/public/Get-BloggerPost.ps1 new file mode 100644 index 0000000..a7758c7 --- /dev/null +++ b/src/public/Get-BloggerPost.ps1 @@ -0,0 +1,145 @@ +<# +.DESCRIPTION + Retrieves an individual post from a specified Blogger blog and optionally saves the content to a file as HTML or Markdown. + +.PARAMETER BlogId + The ID of the blog to retrieve the post from. If not specified, uses the BlogId in the user preferences. + +.PARAMETER PostId + The ID of the post to retrieve. This parameter is required. + +.PARAMETER Format + The format of the post content to retrieve. Use either Markdown or HTML. + +.PARAMETER FolderDateFormat + The folder name as expressed in a DateTime format string. For example, "YYYY/MM" which will save files + in a folder structure like "2023/10" based on the date of the post. + +.PARAMETER OutDirectory + The directory where the HTML file will be saved. If not specified, uses the current directory. + +.EXAMPLE + Get-BloggerPost -PostId "1234567890123456789" + +.EXAMPLE + Get-BloggerPost -BlogId "9876543210987654321" -PostId "1234567890123456789" -Format HTML -OutDirectory "C:\temp" + +.EXAMPLE + Get-BloggerPost -BlogId "9876543210987654321" -PostId "1234567890123456789" -Format Markdown -DateFormat "YYYY\\MM" -OutDirectory "C:\blogposts" +#> +Function Get-BloggerPost { + [CmdletBinding()] + param( + [Parameter(ParameterSetName = "Default")] + [Parameter(ParameterSetName = "Persist")] + [string]$BlogId, + + [Parameter(Mandatory,ParameterSetName = "Default")] + [Parameter(Mandatory,ParameterSetName = "Persist")] + [string]$PostId, + + [Parameter(Mandatory, ParameterSetName = "Persist")] + [ValidateSet("HTML", "Markdown")] + [string]$Format, + + [Parameter(ParameterSetName ="Persist")] + [string]$FolderDateFormat, + + [Parameter(ParameterSetName = "Persist")] + [string]$OutDirectory = (Get-Location).Path + ) + + if (!$PSBoundParameters.ContainsKey("BlogId")) { + $BlogId = $BloggerSession.BlogId + if ([string]::IsNullOrEmpty($BlogId) -or $BlogId -eq 0) { + throw "BlogId not specified and no default BlogId found in settings." + } + } + + if ([string]::IsNullOrEmpty($PostId)) { + throw "PostId is required." + } + + try { + $uri = "https://www.googleapis.com/blogger/v3/blogs/$BlogId/posts/$PostId" + + $result = Invoke-GApi -uri $uri + + if ($null -eq $result) { + throw "No post found with PostId '$PostId' in blog '$BlogId'." + } + + # Construct a subfolder based on the published date + if ($FolderDateFormat -and $result.published) { + $date = [datetime]::Parse($result.published) + $formattedDate = $date.ToString($FolderDateFormat) + $OutDirectory = Join-Path -Path $OutDirectory -ChildPath $formattedDate + } + + # Ensure the output directory exists + if (!(Test-Path -Path $OutDirectory)) { + try { + New-Item -ItemType Directory -Path $OutDirectory -Force | Out-Null + } + catch { + throw "Failed to create output directory '$OutDirectory': $($_.Exception.Message)" + } + } + + # Extract the HTML content + $htmlContent = $result.content + + if ([string]::IsNullOrEmpty($htmlContent)) { + Write-Warning "Post '$PostId' has no content." + $htmlContent = "" + } + + # Create the output file path + try { + + switch ($Format) { + + # Save the HTML content to a file + "HTML" { + + $fileName = "$PostId.html" + $filePath = Join-Path -Path $OutDirectory -ChildPath $fileName + $htmlContent | Out-File -FilePath $filePath -Encoding UTF8 + Write-Verbose "Post content saved to: $filePath" + } + + # Save the Post to a Markdown file + "Markdown" { + + $title = $result.title + $frontMatter = [ordered]@{ + postId = $result.id + } + $file = "$title.md" + $filePath = Join-Path -Path $OutDirectory -ChildPath $file + ConvertTo-MarkdownFromHtml -Content $result.content -OutFile $filePath > $null + Set-MarkdownFrontMatter -File $filePath -Replace $frontMatter + Write-Verbose "Post content saved to: $filePath" + } + } + + # Return the post object for further processing if needed + return $result + } + catch { + throw "Failed to save post content to file '$filePath': $($_.Exception.Message)" + } + } + catch { + # Handle specific HTTP errors + if ($_.Exception -like "*404*" -or $_.Exception -like "*Not Found*") { + throw "Post with PostId '$PostId' not found in blog '$BlogId'. Please verify the PostId and BlogId are correct." + } + elseif ($_.Exception -like "*403*" -or $_.Exception -like "*Forbidden*") { + throw "Access denied to blog '$BlogId' or post '$PostId'. Please verify your permissions." + } + else { + Write-Error $_.ToString() -ErrorAction Stop + } + } +} diff --git a/src/tests/ConvertTo-HtmlFromMarkdown.Tests.ps1 b/src/tests/ConvertTo-HtmlFromMarkdown.Tests.ps1 deleted file mode 100644 index c1fbcc0..0000000 --- a/src/tests/ConvertTo-HtmlFromMarkdown.Tests.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -# Describe "ConvertTo Markdown" { -# BeforeAll { -# Import-Module $PSScriptRoot\_TestHelpers.ps1 -Force -# } - -# BeforeEach { -# Import-Module $PSScriptRoot\..\PSBlogger.psm1 -Force -# } - - -# } \ No newline at end of file diff --git a/src/tests/ConvertTo-MarkdownFromHtml.Tests.ps1 b/src/tests/ConvertTo-MarkdownFromHtml.Tests.ps1 new file mode 100644 index 0000000..3dcc672 --- /dev/null +++ b/src/tests/ConvertTo-MarkdownFromHtml.Tests.ps1 @@ -0,0 +1,79 @@ +Describe "ConvertTo-MarkdownFromHtml" { + BeforeAll { + Import-Module $PSScriptRoot\_TestHelpers.ps1 -Force + + } + + BeforeEach { + Import-Module $PSScriptRoot\..\PSBlogger.psm1 -Force + } + + Context "Using Content" { + + BeforeEach { + $outFile = "TestDrive:\123.md" + $htmlContent = "<h1>Hello World</h1>" + } + + AfterEach { + if (Test-Path $outFile) { + Remove-Item $outFile -Force + } + } + + It "Should save HTML content to a markdown file" { + # act + ConvertTo-MarkdownFromHtml -Content $htmlContent -OutFile $outFile + + # assert + Test-Path $outFile | Should -BeTrue + } + + It "Should convert HTML content to Markdown file" { + # act + $content = ConvertTo-MarkdownFromHtml -Content $htmlContent -OutFile $outFile + + # assert + $content = (Get-Content -Path $outFile -Raw).Split("`r") + $content[0] | Should -Be "# Hello World" + } + + It "Should not persist to disk if OutFile is not specified" { + # act + $content = ConvertTo-MarkdownFromHtml -Content $htmlContent + + # assert + $content | Should -Not -BeNullOrEmpty + Test-Path $outFile | Should -BeFalse + } + } + + Context "Using File" { + + BeforeEach { + $htmlContent = "<h1>Hello World</h1>" + $htmlFile = "TestDrive:\123.html" + $markdownFile = "TestDrive:\123.md" + Set-Content -Path $htmlFile -Value $htmlContent + } + + It "Should convert HTML file to Markdown" { + # act + $content = ConvertTo-MarkdownFromHtml -File $htmlFile -OutFile $markdownFile + + # assert + Test-Path $markdownFile | Should -BeTrue + $content = (Get-Content -Path $markdownFile -Raw).Split("`r") + $content[0] | Should -Be "# Hello World" + } + + It "Should delete temporary file" { + # act + $content = ConvertTo-MarkdownFromHtml -File $htmlFile + + # assert + $content | Should -Not -BeNullOrEmpty + Test-Path $markdownFile | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/src/tests/Get-BloggerPost.Tests.ps1 b/src/tests/Get-BloggerPost.Tests.ps1 new file mode 100644 index 0000000..b1855f3 --- /dev/null +++ b/src/tests/Get-BloggerPost.Tests.ps1 @@ -0,0 +1,285 @@ +Describe "Get-BloggerPost" { + BeforeEach { + Import-Module $PSScriptRoot\..\PSBlogger.psm1 -Force + Import-Module $PSScriptRoot\_TestHelpers.ps1 -Force + + $OutDirectory = Resolve-Path TestDrive: + } + + AfterEach { + # remove temporarily created files + $ExpectedPath = Join-Path (Get-Location).Path -ChildPath "123.html" + if (Test-Path $ExpectedPath) { + Remove-Item $ExpectedPath -Force + } + } + + Context "Parameter validation" { + It "Should require PostId parameter" { + # act / assert + { Get-BloggerPost -Format Markdown } | Should -Throw "*PostId*" + } + + It "Should throw error when BlogId is not provided and not in session" { + # arrange + InModuleScope PSBlogger { + $BloggerSession.BlogId = $null + } + + # act / assert + { + Get-BloggerPost -PostId "123" + } | Should -Throw "*BlogId not specified*" + } + + It "Should use session BlogId when not provided" { + # arrange + InModuleScope PSBlogger { + # setup blog id + $BloggerSession.BlogId = "test-blog-id" + + # setup blog post retrieval + Mock Invoke-GAPi { + return @{ content = "<html>Test content</html>" } + } + } + + # act + $result = Get-BloggerPost -PostId "123" -Format HTML -OutDirectory $OutDirectory + + # assert + $result | Should -Not -BeNullOrEmpty + Test-Path -Path (Join-Path -Path $OutDirectory -ChildPath "123.html") | Should -BeTrue + } + } + + Context "API interaction" { + BeforeEach { + InModuleScope PSBlogger { + # Mock the session to return a test blog ID + $BloggerSession.BlogId = "test-blog-id" + } + } + + It "Should call correct API endpoint" { + InModuleScope PSBlogger { + Mock Invoke-GApi { + return @{ content = "<html>Test content</html>" } + } -ParameterFilter { + $uri -eq "https://www.googleapis.com/blogger/v3/blogs/test-blog-id/posts/123" + } -Verifiable + } + + # act + $result = Get-BloggerPost -PostId "123" + + # assert + $result | Should -Not -BeNullOrEmpty + Should -InvokeVerifiable + } + + It "Should handle 404 errors with meaningful message" { + # arrange + Mock -ModuleName PSBlogger Invoke-GApi { + throw [System.Exception]::new("404 Not Found") + } + + # act / assert + { + Get-BloggerPost -PostId "nonexistent" + } | Should -Throw "*Post with PostId 'nonexistent' not found*" + } + + It "Should handle 403 errors with meaningful message" { + # arrange + Mock -ModuleName PSBlogger Invoke-GApi { + throw [System.Exception]::new("403 Forbidden") + } + + # act / assert + { + Get-BloggerPost -PostId "123" + } | Should -Throw "*Access denied*" + } + + It "Should handle empty content gracefully" { + # arrange + InModuleScope PSBlogger { + Mock Invoke-GApi { + return @{ content = "" } + } + + Mock Write-Warning { } + } + + + # act + Get-BloggerPost -PostId "123" + + # assert: verify warning is issued + Should -Invoke -ModuleName PSBlogger Write-Warning -Exactly 1 -ParameterFilter { + $Message -like "*has no content*" + } + } + } + + Context "As HTML" { + BeforeEach { + InModuleScope PSBlogger { + # Mock the session to return a test blog ID + $BloggerSession.BlogId = "test-blog-id" + + # mock post retrieval + Mock Invoke-GApi { + return @{ content = "<html>Test content</html>" } + } + } + } + + It "Should create output directory if it doesn't exist" { + # arrange + $OutDirectory = "TestDrive:\nonexistent" + + # act + Get-BloggerPost -PostId "123" -OutDirectory $OutDirectory -Format HTML + + # assert + Test-Path -Path $OutDirectory | Should -BeTrue + } + + It "Should save content to correct filename" { + + # arrange + Get-BloggerPost -PostId "123" -OutDirectory (Resolve-Path "TestDrive:") -Format HTML + + # assert + Test-Path -Path "TestDrive:\123.html" | Should -BeTrue + } + + It "Should use current directory when OutDirectory not specified" { + # arrange + $ExpectedPath = Join-Path (Get-Location).Path -ChildPath "123.html" + + # act + Get-BloggerPost -PostId "123" -Format HTML + + # assert + Test-Path -Path $ExpectedPath | Should -BeTrue + } + } + + Context "As Markdown" { + + BeforeEach { + InModuleScope PSBlogger { + # Mock the session to return a test blog ID + $BloggerSession.BlogId = "test-blog-id" + + $postId = "123" + + # mock post retrieval + Mock Invoke-GApi { + return @{ + id = $postId + title = "Test Post" + published = "2023-10-01T12:00:00Z" + content = "<h1>Hello World</h1><p>This is a post.</p>" + } + } + } + + $postId = "123" + $title = "Test Post" + $outFile = "TestDrive:\$title.md" + + } + + AfterEach { + if (Test-Path $outFile) { + Remove-Item $outFile -Force + } + } + + It "Should write post details to frontmatter" { + + # act + Get-BloggerPost -PostId $postId -Format Markdown -OutDirectory "TestDrive:\" + + # assert + $frontMatter = Get-MarkdownFrontMatter -File $outFile + $frontMatter.postId | Should -Be "123" + } + } + + Context "Using FolderDateFormat" { + + BeforeEach { + InModuleScope PSBlogger { + # Mock the session to return a test blog ID + $BloggerSession.BlogId = "test-blog-id" + + $postId = "123" + + # mock post retrieval + Mock Invoke-GApi { + return @{ + id = $postId + title = "Test Post" + published = "2023-10-01T12:00:00Z" + content = "<h1>Hello World</h1><p>This is a post.</p>" + } + } + } + } + + It "Should write file to specified formatted directory - <dateformat> - <format>" -TestCases @( + @{ DateFormat = "yyyy\\MM"; ExpectedPath = "TestDrive:\2023\10\Test Post.md"; Format = "Markdown" } + @{ DateFormat = "yyyy\\MM\\dd"; ExpectedPath = "TestDrive:\2023\10\01\123.html"; Format = "HTML" } + ) { + # arrange + $invokeArgs = @{ + PostId = 123 + Format = $Format + OutDirectory = "TestDrive:\" + FolderDateFormat = $DateFormat + } + + # act + Get-BloggerPost @invokeArgs + + # assert + Test-Path $ExpectedPath | Should -BeTrue + + } + + It "Should ignore folderdateformat when not specified" { + + # act + Get-BloggerPost -PostId 123 -Format HTML -OutDirectory "TestDrive:\" + + # assert + Test-Path "TestDrive:\123.html" | Should -BeTrue + + } + + It "Should ignore folderdateformat when post has not been published" { + # arrange + InModuleScope PSBlogger { + Mock Invoke-GApi { + return @{ + id = 123 + title = "Test Post" + published = $null + content = "<h1>Hello World</h1><p>This is a post.</p>" + } + } + } + + # act + Get-BloggerPost -PostId 123 -Format HTML -OutDirectory "TestDrive:\" -FolderDateFormat "YYYY\\MM" + + # assert + Test-Path "TestDrive:\123.html" | Should -BeTrue + } + } +}