PSQuizmaster.psm1
|
#load functions #1 July 2026 Defining the PSQuizTheme as a custom object class PSQuizTheme { $Title = 'PaleTurquoise1 bold' $Question = 'SeaGreen2' $Correct = 'Chartreuse1' $Incorrect = 'DeepPink2' $BorderColor = 'PaleGreen1' $Note = 'LightGoldenrod2_2 Underline' $DefaultLogo <# I am cheating here because Write-SpectreHost doesn't write to the pipeline, but a return type of [Void] might be misleading. This is also easier and faster than creating a custom format file to convert the SpectreConsole colors into ANSI and then display them. #> [string]Show() { Write-Host "`r" $This.PSObject.Properties.Where({$_.name -notMatch "logo"}).foreach({ $setting = "{0} [{1}]{1}[/]" -f $_.name.PadRight(12), $_.Value Write-SpectreHost $setting }) Return "" } } $FunFolder = Join-Path -Path $PSScriptRoot -ChildPath functions Get-ChildItem -Path $FunFolder -Filter *.ps1 | ForEach-Object { . $_.FullName } #this is a private variable not exposed to the user $PSQuizSettingsFile = Join-Path -Path $HOME -ChildPath '.psquizsettings.json' #3 July 2026 Added support for a logo image $defaultLogo = "$PSScriptRoot\assets\MsPowerShell.jpg" if (Test-Path -Path $PSQuizSettingsFile) { #if the settings file is found, use it to define $PSQuizSettingsFile $PSQuizSettings = Get-Content -Path $PSQuizSettingsFile | ConvertFrom-Json Set-Variable -Name PSQuizPath -Value $PSQuizSettings.PSQuizPath -Scope Global if ($PSQuizSettings.psQuizTheme) { #write-host "Using saved quiz theme" Set-Variable -Name psQuizTheme -Value ([PSQuizTheme]::new()) -Scope Global $global:PSQuizTheme.Title = $PSQuizSettings.psQuizTheme.Title $global:PSQuizTheme.Question = $PSQuizSettings.psQuizTheme.Question $global:PSQuizTheme.Correct = $PSQuizSettings.psQuizTheme.Correct $global:PSQuizTheme.Incorrect = $PSQuizSettings.psQuizTheme.Incorrect $global:psQuizTheme.BorderColor = $PSQuizSettings.psQuizTheme.BorderColor $global:PSQuizTheme.Note = $PSQuizSettings.psQuizTheme.Note $global:psQuizTheme.DefaultLogo = $PSQuizSettings.psQuizTheme.DefaultLogo } else { #use default values #write-host "Creating default quiz theme" $global:PSQuizTheme = [PSQuizTheme]::new() $global:psQuizTheme.DefaultLogo = $defaultLogo } } else { #write-host "no saved quiz settings found" #otherwise set $PSQuizSettingsFile to the module's Quizzes folder Set-Variable -Name PSQuizPath -Value (Join-Path -Path $PSScriptRoot -ChildPath 'quizzes') -Scope Global #27 June 2026 Support SpectreConsole theming for the quiz #use default values $global:PSQuizTheme = [PSQuizTheme]::new() $global:psQuizTheme.DefaultLogo = $defaultLogo } #Path to the JSON schema file #this is an internal variable $PSQuizSchema = "https://raw.githubusercontent.com/jdhitsolutions/PSQuizMaster/main/psquiz.schema-v2.json" #v1 schema #'https://raw.githubusercontent.com/jdhitsolutions/PSQuizMaster/main/psquiz.schema.json' #for local testing # $PSQuizSchema = "file:///c://scripts//psquizmaster//psquiz.schema.json" Register-ArgumentCompleter -CommandName Invoke-PSQuiz -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) #PowerShell code to populate $WordToComplete Get-ChildItem -Path $PSQuizPath -Filter *quiz.json | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { # completion text,listItem text,result type,Tooltip [System.Management.Automation.CompletionResult]::new($_.fullName, $_.name, 'ParameterValue', $_.FullName) } } #add a VSCode menu item to insert the UTC date time into a quiz file if ($host.Name -eq 'Visual Studio Code Host') { $psEditor.Window.ShowInformationMessage('Loading PSQuizMaster VSCode features') $sb = { $utc = '{0:u}' -f (Get-Date).ToUniversalTime() $Context = $psEditor.GetEditorContext() $context.CurrentFile.InsertText($utc) } Register-EditorCommand -Name QuizDate -DisplayName 'Insert PSQuiz date' -ScriptBlock $sb <# In some cases, you may need to reload the window (Command Palette - Developer Reload windows). Especially if you are masking and unmasking the same long string in the same session. You will need to re-import the module after reloading the window. Note that if you remove the module, the underlying VSCode functions will be removed but the command palette menu choices will remain, but won't work. Reloading the window will reset the command palette. #> function global:MaskQuizItem { param($Context) function _hideAnswer { param([string]$Answer) #match on non-whitespace word [regex]$word = '\S+' foreach ($part in $Answer) { $word.Matches($part).Value.ForEach({ $_.toCharArray().Foreach({ '{0:d3}' -f ($_ -as [int]) }) -join '' }) -join ' ' } } if ($Context.CurrentFile.language -ne 'json') { $psEditor.Window.ShowWarningMessage('This command must be run on a PSQuiz json file') } else { # Write-Host "Masking selected quiz item" $selected = [Microsoft.PowerShell.EditorServices.Extensions.FileRange, Microsoft.PowerShell.EditorServices]::new($Context.SelectedRange.Start, $Context.SelectedRange.end) [string]$text = $Context.CurrentFile.GetText($selected) [string]$mask = _hideAnswer $text $context.CurrentFile.InsertText($mask) } } Register-EditorCommand -Name MaskQuizItem -DisplayName 'Mask Quiz Item' -Function MaskQuizItem function global:UnmaskQuizItem { param($Context) function _showAnswer { param([string]$ProtectedAnswer) [regex]$number = '\d{3}' $out = foreach ($part in $ProtectedAnswer.split()) { $number.Matches($part).Value.ForEach({ ([int]$_ -as [string][char]) }) -join '' } $out -join ' ' } if ($Context.CurrentFile.language -ne 'json') { $psEditor.Window.ShowWarningMessage('This command must be run on a PSQuiz json file') } else { #write-Host "Unmasking selected quiz item" $selected = [Microsoft.PowerShell.EditorServices.Extensions.FileRange, Microsoft.PowerShell.EditorServices]::new($Context.SelectedRange.Start, $Context.SelectedRange.end) [string]$text = $Context.CurrentFile.GetText($selected) #write-host "unmasking $text [$($text.Length)]" [string]$plain = _showAnswer $text #write-host "with $plain" if ($plain.length -gt 0) { $context.CurrentFile.InsertText($plain) } else { $psEditor.Window.ShowWarningMessage('Failed to unmask the selected item. You might need to reload the window and re-import the module into the integrated PowerShell terminal. Command Palette - Developer: Reload Window') } } } Register-EditorCommand -Name UnmaskQuizItem -DisplayName 'Unmask Quiz Item' -Function UnmaskQuizItem function global:InsertQuizItem { param($Context) #insert the JSON for a new quiz item if ($Context.CurrentFile.language -ne 'json') { $psEditor.Window.ShowWarningMessage('This command must be run on a PSQuiz json file') } else { $template = @' { "question" : "<enter your question>", "answer" : "<the correct answer>", "distractors" : [ "<distractor #1>", "<distractor #2>", "<distractor #3>", "<distractor #4>" ], "note" : "<enter an optional note or delete this line>" } '@ $context.CurrentFile.InsertText($template) } } Register-EditorCommand -Name InsertQuizItem -DisplayName 'Insert Quiz Item' -Function InsertQuizItem } #add OnRemove event to clean up global variable $onRemoveScript = { Remove-Variable -Name PSQuizPath -Scope Global -ErrorAction SilentlyContinue Remove-Variable -Name PSQuizTheme -Scope Global -ErrorAction SilentlyContinue if ($host.Name -match 'code') { #command names are case-sensitive Unregister-EditorCommand -Name UnmaskQuizItem Unregister-EditorCommand -Name MaskQuizItem Unregister-EditorCommand -Name QuizDate Unregister-EditorCommand -Name InsertQuizItem } } $ExecutionContext.SessionState.Module.OnRemove += $OnRemoveScript |