| 1 |
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$Path,
[switch]$Diagnostic,
[switch]$InProcess
)
$ErrorActionPreference = 'Stop'
$script:Diagnostic = [bool]$Diagnostic
$script:LogPath = $null
$script:LogFailures = 0
$script:ErrorStats = @{}
$script:SourceStats = @{}
$script:FloodLimit = 20
$script:FloodWindowSec = 5
$script:LastConsoleText = $null
$script:LastConsoleUtc = [DateTime]::MinValue
$script:LastRepeatUtc = [DateTime]::MinValue
$script:ForceClose = $false
$script:SafeMode = $false
$script:InUnhandled = $false
$script:UnhandledHandler = $null
$script:TickBusy = $false
$script:SelectionTickBusy = $false
$script:TickCount = 0
$script:SelectionTickCount = 0
$script:LastHealthUtc = [DateTime]::MinValue
$script:DeferredQueue = New-Object System.Collections.Generic.List[scriptblock]
$script:RunMarkerName = 'HtmlEditor.RunCount'
$script:ScriptVersion = '2026-0924-08'
function Get-EditorDataFolder {
[OutputType([string])]
param()
$root = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
if ([string]::IsNullOrEmpty($root)) {
$root = [System.IO.Path]::GetTempPath()
}
return [System.IO.Path]::Combine($root, 'HtmlEditor')
}
function Initialize-EditorLog {
try {
$folder = Get-EditorDataFolder
[void][System.IO.Directory]::CreateDirectory($folder)
$file = [System.IO.Path]::Combine($folder, 'HtmlEditor.log')
if ([System.IO.File]::Exists($file) -and (New-Object System.IO.FileInfo $file).Length -gt 5MB) {
$old = [System.IO.Path]::Combine($folder, 'HtmlEditor.old.log')
if ([System.IO.File]::Exists($old)) {
[System.IO.File]::Delete($old)
}
[System.IO.File]::Move($file, $old)
}
$script:LogPath = $file
}
catch {
$script:LogPath = $null
Write-Warning ('HtmlEditor could not create its log file: {0}' -f $_.Exception.Message)
}
}
function Write-EditorLog {
param(
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Message,
[ValidateSet('Info', 'Warning', 'Error', 'Trace')][string]$Level = 'Info'
)
if ($Level -eq 'Trace' -and -not $script:Diagnostic) {
return
}
if ([string]::IsNullOrEmpty($script:LogPath) -or $script:LogFailures -ge 3) {
return
}
try {
$stamp = [DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss.fff')
$body = ($Message -replace '\r?\n', ([Environment]::NewLine + ' '))
$line = '{0} [{1}] {2,-7} {3}{4}' -f $stamp, $PID, $Level.ToUpperInvariant(), $body, [Environment]::NewLine
[System.IO.File]::AppendAllText($script:LogPath, $line, [System.Text.Encoding]::UTF8)
}
catch {
$script:LogFailures++
if ($script:LogFailures -eq 3) {
Write-Warning ('HtmlEditor stopped writing its log after 3 failures ({0}): {1}' -f $script:LogPath, $_.Exception.Message)
}
}
}
function Write-EditorTrace {
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Message)
if ($script:Diagnostic) {
Write-EditorLog -Message $Message -Level Trace
}
}
function Get-ErrorDetail {
[OutputType([pscustomobject])]
param([Parameter(Mandatory = $true)][AllowNull()][object]$ErrorObject)
$record = $null
$exception = $null
if ($ErrorObject -is [System.Management.Automation.ErrorRecord]) {
$record = $ErrorObject
$exception = $ErrorObject.Exception
}
elseif ($ErrorObject -is [System.Exception]) {
$exception = $ErrorObject
}
else {
$exception = New-Object System.Exception ([string]$ErrorObject)
}
if ($null -eq $record -and $exception -is [System.Management.Automation.IContainsErrorRecord]) {
$record = $exception.ErrorRecord
}
$wrappers = New-Object System.Collections.Generic.List[string]
while ($null -ne $exception.InnerException -and (
$exception -is [System.Reflection.TargetInvocationException] -or
$exception -is [System.Management.Automation.MethodInvocationException] -or
$exception.GetType() -eq [System.Management.Automation.RuntimeException])) {
$wrappers.Add($exception.GetType().Name)
$exception = $exception.InnerException
}
if ($null -eq $record -and $exception -is [System.Management.Automation.IContainsErrorRecord]) {
$record = $exception.ErrorRecord
}
$line = 0
$function = ''
$scriptStack = ''
if ($null -ne $record) {
if ($null -ne $record.InvocationInfo) {
$line = [int]$record.InvocationInfo.ScriptLineNumber
}
$scriptStack = [string]$record.ScriptStackTrace
}
if ($scriptStack.Length -gt 0) {
$first = ($scriptStack -split '\r?\n')[0]
$match = [regex]::Match($first, '^at (?<name>[^,]+),')
if ($match.Success) {
$function = $match.Groups['name'].Value.Trim()
}
}
if ($function.Length -eq 0) {
$function = '(unknown)'
}
$native = ''
if ($exception -is [System.ComponentModel.Win32Exception]) {
$native = [string]$exception.NativeErrorCode
}
$inner = New-Object System.Collections.Generic.List[string]
$walk = $exception.InnerException
while ($null -ne $walk -and $inner.Count -lt 5) {
$inner.Add(('{0} (0x{1:X8}): {2}' -f $walk.GetType().FullName, $walk.HResult, $walk.Message))
$walk = $walk.InnerException
}
$type = $exception.GetType().FullName
$hresult = '0x{0:X8}' -f $exception.HResult
$message = [string]$exception.Message
$summary = '{0} ({1}{2}) in {3} line {4}: {5}' -f $type, $hresult, $(if ($native) { ', Win32 ' + $native } else { '' }), $function, $line, $message
$full = New-Object System.Text.StringBuilder
[void]$full.AppendLine($summary)
if ($wrappers.Count -gt 0) {
[void]$full.AppendLine('Wrapped by: ' + ($wrappers -join ' > '))
}
if ($scriptStack.Length -gt 0) {
[void]$full.AppendLine('ScriptStackTrace:')
[void]$full.AppendLine($scriptStack)
}
if (-not [string]::IsNullOrEmpty($exception.StackTrace)) {
[void]$full.AppendLine('.NET stack:')
[void]$full.AppendLine($exception.StackTrace)
}
foreach ($item in $inner) {
[void]$full.AppendLine('Inner: ' + $item)
}
return [pscustomobject]@{
Type = $type
HResult = $hresult
NativeError = $native
Message = $message
Line = $line
Function = $function
ScriptStackTrace = $scriptStack
Summary = $summary
Full = $full.ToString().TrimEnd()
Key = ('{0}|{1}|{2}|{3}|{4}' -f $type, $hresult, $function, $line, $message)
}
}
function Update-FloodWindow {
[OutputType([int])]
param(
[Parameter(Mandatory = $true)][hashtable]$Entry,
[Parameter(Mandatory = $true)][DateTime]$NowUtc
)
if ($null -eq $Entry.WindowStart -or ($NowUtc - $Entry.WindowStart).TotalSeconds -gt $script:FloodWindowSec) {
$Entry.WindowStart = $NowUtc
$Entry.WindowCount = 0
}
$Entry.WindowCount++
return [int]$Entry.WindowCount
}
function Register-EditorError {
[OutputType([string])]
param(
[Parameter(Mandatory = $true)][string]$Source,
[Parameter(Mandatory = $true)][AllowNull()][object]$ErrorObject,
[scriptblock]$StopAction,
[DateTime]$NowUtc = [DateTime]::UtcNow
)
$detail = Get-ErrorDetail -ErrorObject $ErrorObject
$key = $Source + '|' + $detail.Key
if (-not $script:SourceStats.ContainsKey($Source)) {
$script:SourceStats[$Source] = @{ Count = 0; WindowStart = $null; WindowCount = 0; Tripped = $false }
}
$sourceEntry = $script:SourceStats[$Source]
$sourceEntry.Count++
$sourceInWindow = Update-FloodWindow -Entry $sourceEntry -NowUtc $NowUtc
$entry = $script:ErrorStats[$key]
$state = 'Repeat'
if ($null -eq $entry) {
$entry = @{ Count = 0; WindowStart = $null; WindowCount = 0; Tripped = $false; Summary = $detail.Summary; Source = $Source }
$script:ErrorStats[$key] = $entry
$state = 'New'
}
$entry.Count++
$inWindow = Update-FloodWindow -Entry $entry -NowUtc $NowUtc
$flooding = ($inWindow -gt $script:FloodLimit) -or ($sourceInWindow -gt (5 * $script:FloodLimit))
if ($flooding -and -not $sourceEntry.Tripped) {
$sourceEntry.Tripped = $true
$entry.Tripped = $true
$stopNote = 'has nothing to stop'
if ($null -ne $StopAction) {
try {
& $StopAction
$stopNote = 'was stopped'
}
catch {
$stopNote = 'could not be stopped: ' + $_.Exception.Message
}
}
$text = 'Error flood: {0} reported {1} errors within {2} s ({3} identical); {0} {4}. Last: {5}. Details: {6}' -f $Source, $sourceInWindow, $script:FloodWindowSec, $inWindow, $stopNote, $detail.Summary, $script:LogPath
Write-EditorLog -Message $text -Level Error
Write-StatusMessage -Message $text -Level Error
return 'Tripped'
}
if ($state -eq 'New') {
Write-EditorLog -Message ('[{0}] {1}' -f $Source, $detail.Full) -Level Error
Write-StatusMessage -Message ('{0}: {1} (details: {2})' -f $Source, $detail.Summary, $script:LogPath) -Level Error
return 'New'
}
$count = [int]$entry.Count
if ($count -in @(2, 5, 10, 20, 50, 100) -or ($count % 500) -eq 0) {
Write-EditorLog -Message ('[{0}] repeated {1} times: {2}' -f $Source, $count, $detail.Summary) -Level Warning
}
if ($null -ne $script:MessageText -and ($NowUtc - $script:LastRepeatUtc).TotalMilliseconds -ge 1000) {
$script:LastRepeatUtc = $NowUtc
try {
Set-StatusText -Message ('{0}: {1} (repeated {2} times)' -f $Source, $detail.Summary, $count) -Level Error
}
catch {
Write-EditorLog -Message ('Status bar update failed: {0}' -f $_.Exception.Message) -Level Warning
}
}
return 'Repeat'
}
function Write-ErrorSummary {
foreach ($key in @($script:ErrorStats.Keys)) {
$entry = $script:ErrorStats[$key]
if ($entry.Count -gt 1) {
Write-EditorLog -Message ('Error total: {0} x [{1}] {2}{3}' -f $entry.Count, $entry.Source, $entry.Summary, $(if ($entry.Tripped) { ' (breaker tripped)' } else { '' })) -Level Warning
}
}
}
trap {
if ($null -ne (Get-Command -Name Write-EditorLog -CommandType Function -ErrorAction SilentlyContinue)) {
Write-EditorLog -Message ('Fatal: ' + (Get-ErrorDetail -ErrorObject $_).Full) -Level Error
if (-not [string]::IsNullOrEmpty($script:LogPath)) {
Write-Warning ('HtmlEditor stopped. Details: {0}' -f $script:LogPath)
}
}
break
}
Initialize-EditorLog
if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) {
Write-Error 'HtmlEditor requires Windows (WPF and the MSHTML browser control).'
exit 1
}
if (-not [string]::IsNullOrWhiteSpace($Path)) {
$Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
}
function Get-RelaunchReason {
[OutputType([string])]
param(
[switch]$InProcess,
[System.Threading.ApartmentState]$Apartment = [System.Threading.Thread]::CurrentThread.ApartmentState,
[AllowNull()][object]$RunCount = [AppDomain]::CurrentDomain.GetData($script:RunMarkerName),
[scriptblock]$DispatcherTest = { Test-ThreadDispatcher }
)
if ($Apartment -ne [System.Threading.ApartmentState]::STA) {
return 'the current thread is not STA'
}
if ($InProcess) {
return ''
}
if ($null -ne $RunCount) {
return ('HtmlEditor already ran {0} time(s) in this PowerShell session' -f $RunCount)
}
if (& $DispatcherTest) {
return 'WPF was already used on this PowerShell thread (for example an earlier HtmlEditor window)'
}
return ''
}
function Test-ThreadDispatcher {
[OutputType([bool])]
param()
try {
Add-Type -AssemblyName WindowsBase
return ($null -ne [System.Windows.Threading.Dispatcher]::FromThread([System.Threading.Thread]::CurrentThread))
}
catch {
Write-EditorLog -Message ('Could not check for an existing dispatcher: {0}' -f $_.Exception.Message) -Level Warning
return $false
}
}
function Get-RelaunchHost {
[OutputType([string])]
param()
$name = 'powershell.exe'
if ($PSVersionTable.PSEdition -eq 'Core') {
$name = 'pwsh.exe'
}
$candidate = [System.IO.Path]::Combine($PSHOME, $name)
if ([System.IO.File]::Exists($candidate)) {
return $candidate
}
return $name
}
$script:HadDispatcher = Test-ThreadDispatcher
Write-EditorLog -Message ('HtmlEditor {0} starting. PID {1}, PowerShell {2} {3}, host {4}, thread {5} {6}, 64-bit {7}, OS {8}. Args: Path={9} Diagnostic={10} InProcess={11}. Dispatcher already on thread: {12}. Earlier runs in session: {13}.' -f $script:ScriptVersion, $PID, $PSVersionTable.PSVersion, $PSVersionTable.PSEdition, [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName, [System.Threading.Thread]::CurrentThread.ManagedThreadId, [System.Threading.Thread]::CurrentThread.ApartmentState, [Environment]::Is64BitProcess, [Environment]::OSVersion.VersionString, $Path, [bool]$Diagnostic, [bool]$InProcess, $script:HadDispatcher, [int][AppDomain]::CurrentDomain.GetData($script:RunMarkerName))
$relaunchReason = Get-RelaunchReason -InProcess:$InProcess
if ($relaunchReason.Length -gt 0) {
if ([string]::IsNullOrEmpty($PSCommandPath)) {
Write-Error ('HtmlEditor cannot run here ({0}). Save it as a .ps1 file and run it with -STA.' -f $relaunchReason)
exit 1
}
$hostExe = Get-RelaunchHost
$argumentList = @('-STA', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"{0}"' -f $PSCommandPath), '-InProcess')
if (-not [string]::IsNullOrWhiteSpace($Path)) {
$argumentList += @('-Path', ('"{0}"' -f $Path))
}
if ($Diagnostic) {
$argumentList += '-Diagnostic'
}
Write-EditorLog -Message ('Starting a new process because {0}: {1} {2}' -f $relaunchReason, $hostExe, ($argumentList -join ' '))
$child = Start-Process -FilePath $hostExe -ArgumentList $argumentList -WindowStyle Minimized -PassThru
Write-Host ('HtmlEditor started in a new PowerShell process (PID {0}) because {1}. Log: {2}' -f $child.Id, $relaunchReason, $script:LogPath)
exit
}
if ($script:HadDispatcher) {
Write-EditorLog -Message '-InProcess on a thread that already hosted WPF: pending messages are drained before the window is created.' -Level Warning
}
try {
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Windows.Forms, System.Drawing, WindowsFormsIntegration, System.Xml
}
catch {
Write-Error ('Failed to load WPF / WinForms assemblies: {0}' -f $_.Exception.Message)
exit 1
}
[System.Windows.Forms.Application]::EnableVisualStyles()
if ($script:HadDispatcher) {
try {
[System.Windows.Forms.Application]::DoEvents()
}
catch {
Write-EditorLog -Message ('Draining the message queue failed: {0}' -f $_.Exception.Message) -Level Warning
}
}
Write-EditorTrace -Message 'Assemblies loaded.'
$script:StartPath = $Path
$script:ClearStyleName = '(clear style)'
$script:CurrentPath = $null
$script:OriginalText = ''
$script:SavedText = ''
$script:BaselineBody = ''
$script:HeadDirty = $false
$script:DocReady = $false
$script:PendingLoad = $false
$script:AllowNavigation = $false
$script:ExpectedTempFile = $null
$script:InjectedBaseUri = $null
$script:ScriptReady = $false
$script:SourceMode = $false
$script:SourceTextAtEntry = $null
$script:SourceCleanText = $null
$script:ForceReloadOnDesign = $false
$script:SuppressStyleEvent = $false
$script:SuppressToggle = $false
$script:Started = $false
$script:StyleUndo = New-Object System.Collections.Generic.List[object]
$script:TempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), ('HtmlEditor_' + $PID))
$script:TempCounter = 0
$script:LoadStatus = $null
$script:LastTickError = $null
$script:LastShortcutName = $null
$script:LastShortcutTime = [DateTime]::MinValue
$script:StartupMessages = New-Object System.Collections.Generic.List[object]
$script:Window = $null
$script:MessageText = $null
$script:Browser = $null
$script:FormsHost = $null
$script:SelectionMarkerPattern = '<!--HTESEL[SE][0-9a-f]{32}-->'
$script:PendingSelectionId = ''
$script:ClearSizeName = '(clear size)'
$script:SuppressColorEvent = $false
$script:SuppressSizeEvent = $false
$script:PendingColorChoice = ''
$script:LastCustomColor = $null
$script:ColorCombo = $null
$script:SizeCombo = $null
$script:FormatButton = $null
$script:ColorSwatches = $null
$script:ColorCustomItem = $null
$script:ColorShownPanel = $null
$script:ColorShownSwatch = $null
$script:ColorShownText = $null
$script:ColorShownHex = ''
$script:ColorComboWidth = 150
$script:SuppressComboEvents = $false
$script:SelectionInfoKey = $null
$script:ToolbarRefreshPending = $false
$script:SelectionTimer = $null
$script:LastSelectionError = $null
$script:SizeShownText = ''
$script:SizeUserPicked = $false
$script:SettingsKeyPath = 'Software\HtmlEditor'
$script:AutoSave = $false
$script:AutoSaveMenu = $null
$script:AutoSaveSignature = $null
$script:AutoSaveDueUtc = $null
$script:AutoSaveNote = ''
$script:ModalDepth = 0
$script:DefaultHighlight = '#ffff00'
$script:HighlightColor = '#ffff00'
$script:HighlightButton = $null
$script:HighlightMenuButton = $null
$script:OutdentButton = $null
$script:IndentButton = $null
$script:HighlightBar = $null
$script:HighlightPopup = $null
$script:HighlightGrid = $null
$script:HighlightPopupClosedUtc = [DateTime]::MinValue
$script:PageKeyQueue = New-Object System.Collections.Generic.List[string]
$script:HighlightPalette = @(
'#FFFF00', '#00FF00', '#00FFFF', '#FF00FF', '#0000FF',
'#FF0000', '#000080', '#008080', '#008000', '#800080',
'#800000', '#808000', '#808080', '#C0C0C0', '#000000',
'#FFFF99', '#CCFFCC', '#CCFFFF', '#FF99CC', '#99CCFF',
'#FFC000', '#92D050', '#33CCCC', '#CC99FF', '#00B0F0',
'#DBE5F1', '#F2DCDB', '#EBF1DD', '#E5E0EC', '#FDEADA',
'#B8CCE4', '#E5B9B7', '#D7E3BC', '#CCC1D9', '#FBD5B5',
'#95B3D7', '#D99694', '#C3D69B', '#B2A2C7', '#FAC08F'
)
function Set-StatusText {
param(
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Message,
[ValidateSet('Info', 'Warning', 'Error')][string]$Level = 'Info'
)
$script:MessageText.Text = ('[{0}] {1}' -f [DateTime]::Now.ToString('HH:mm:ss'), $Message)
$script:MessageText.ToolTip = $Message
switch ($Level) {
'Error' { $script:MessageText.Foreground = [System.Windows.Media.Brushes]::Firebrick }
'Warning' { $script:MessageText.Foreground = [System.Windows.Media.Brushes]::DarkOrange }
default { $script:MessageText.Foreground = [System.Windows.SystemColors]::ControlTextBrush }
}
}
function Write-StatusMessage {
param(
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Message,
[ValidateSet('Info', 'Warning', 'Error')][string]$Level = 'Info'
)
if ($Level -ne 'Info') {
Write-EditorLog -Message $Message -Level $Level
$now = [DateTime]::UtcNow
if ($Message -cne $script:LastConsoleText -or ($now - $script:LastConsoleUtc).TotalSeconds -ge 5) {
Write-Warning $Message
}
$script:LastConsoleText = $Message
$script:LastConsoleUtc = $now
}
else {
Write-EditorTrace -Message ('Status: ' + $Message)
}
if ($null -eq $script:MessageText) {
if ($script:StartupMessages.Count -lt 200) {
$script:StartupMessages.Add([pscustomobject]@{ Message = $Message; Level = $Level })
}
return
}
Set-StatusText -Message $Message -Level $Level
}
function Show-ScriptResult {
param([AllowEmptyString()][string]$Result)
if ([string]::IsNullOrEmpty($Result)) {
Write-StatusMessage -Message 'The editor script returned no result.' -Level Warning
return
}
$colon = $Result.IndexOf(':')
$code = ''
$text = $Result
if ($colon -gt 0) {
$code = $Result.Substring(0, $colon)
$text = $Result.Substring($colon + 1)
}
switch ($code) {
'OK' { Write-StatusMessage -Message $text }
'INFO' { Write-StatusMessage -Message $text }
'ERR' { Write-StatusMessage -Message $text -Level Warning }
default { Write-StatusMessage -Message $Result }
}
}
function Set-BrowserEmulation {
$key = 'HKCU:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION'
try {
if (-not (Test-Path -LiteralPath $key)) {
[void](New-Item -Path $key -Force)
}
$names = @('powershell.exe', 'pwsh.exe', ([System.Diagnostics.Process]::GetCurrentProcess().ProcessName + '.exe')) |
Select-Object -Unique
foreach ($name in $names) {
[void](New-ItemProperty -LiteralPath $key -Name $name -Value 11001 -PropertyType DWord -Force)
}
}
catch {
Write-StatusMessage -Message ('Could not set FEATURE_BROWSER_EMULATION (the editor may run in a legacy IE mode): {0}' -f $_.Exception.Message) -Level Warning
}
}
function Get-CssStyleName {
[OutputType([string[]])]
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$CssText)
$blockElements = @('p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'address', 'div')
$classElements = $blockElements + @('span')
$css = [regex]::Replace($CssText, '(?s)/\*.*?\*/', ' ')
$css = $css.Replace('<!--', ' ').Replace('-->', ' ')
$found = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
$depth = 0
$preludeStart = 0
$tokens = [regex]::Matches($css, '"(?:\\.|[^"\\])*"|''(?:\\.|[^''\\])*''|[{};]')
foreach ($token in $tokens) {
$value = $token.Value
if ($value -eq '{') {
if ($depth -eq 0) {
$prelude = $css.Substring($preludeStart, $token.Index - $preludeStart).Trim()
if ($prelude.Length -gt 0 -and $prelude[0] -ne [char]64) {
foreach ($selector in $prelude.Split(',')) {
$candidate = $selector.Trim()
if ($candidate -match '^([A-Za-z][A-Za-z0-9]*)$') {
$tag = $Matches[1].ToLowerInvariant()
if ($blockElements -contains $tag) {
[void]$found.Add($tag)
}
}
elseif ($candidate -match '^([A-Za-z][A-Za-z0-9]*)?\.(-?[_A-Za-z][_A-Za-z0-9-]*)$') {
$className = $Matches[2]
if ([string]::IsNullOrEmpty($Matches[1])) {
[void]$found.Add('.' + $className)
}
else {
$tag = $Matches[1].ToLowerInvariant()
if ($classElements -contains $tag) {
[void]$found.Add($tag + '.' + $className)
}
}
}
}
}
}
$depth++
}
elseif ($value -eq '}') {
if ($depth -gt 0) {
$depth--
}
if ($depth -eq 0) {
$preludeStart = $token.Index + 1
}
}
elseif ($value -eq ';') {
if ($depth -eq 0) {
$preludeStart = $token.Index + 1
}
}
}
$elements = New-Object System.Collections.Generic.List[string]
$classes = New-Object System.Collections.Generic.List[string]
foreach ($name in $found) {
if ($name.Contains('.')) {
$classes.Add($name)
}
else {
$elements.Add($name)
}
}
$elements.Sort([System.StringComparer]::OrdinalIgnoreCase)
$classes.Sort([System.StringComparer]::OrdinalIgnoreCase)
return ($elements.ToArray() + $classes.ToArray())
}
function Get-HtmlAttributeValue {
param(
[Parameter(Mandatory = $true)][string]$Tag,
[Parameter(Mandatory = $true)][string]$Name
)
$pattern = '(?is)\s' + [regex]::Escape($Name) + '\s*=\s*(?:"([^"]*)"|''([^'']*)''|([^\s"''>]+))'
$match = [regex]::Match($Tag, $pattern)
if (-not $match.Success) {
return $null
}
foreach ($group in 1..3) {
if ($match.Groups[$group].Success) {
return [System.Net.WebUtility]::HtmlDecode($match.Groups[$group].Value)
}
}
return $null
}
function Get-StylesheetHref {
[OutputType([string[]])]
param([AllowEmptyString()][string]$Html)
$scan = [regex]::Replace($Html, '(?s)<!--.*?-->', '')
$result = New-Object System.Collections.Generic.List[string]
foreach ($match in [regex]::Matches($scan, '(?is)<link\b[^>]*>')) {
$rel = Get-HtmlAttributeValue -Tag $match.Value -Name 'rel'
if ($null -eq $rel -or $rel -notmatch '(?i)(^|\s)stylesheet(\s|$)') {
continue
}
$href = Get-HtmlAttributeValue -Tag $match.Value -Name 'href'
if (-not [string]::IsNullOrWhiteSpace($href)) {
$result.Add($href.Trim())
}
}
return $result.ToArray()
}
function Resolve-StylesheetPath {
[OutputType([string])]
param(
[Parameter(Mandatory = $true)][string]$Href,
[AllowEmptyString()][string]$BaseDirectory
)
$value = $Href.Trim()
$cut = $value.IndexOfAny([char[]]@('?', '#'))
if ($cut -ge 0) {
$value = $value.Substring(0, $cut)
}
if ($value -match '^(?i)file:') {
return (New-Object System.Uri($value)).LocalPath
}
$value = [System.Uri]::UnescapeDataString($value).Replace('/', '\')
if ($value -match '^[A-Za-z]:\\' -or $value.StartsWith('\\')) {
return [System.IO.Path]::GetFullPath($value)
}
if ([string]::IsNullOrEmpty($BaseDirectory)) {
throw 'relative path cannot be resolved until the document has been saved'
}
if ($value.StartsWith('\')) {
$root = [System.IO.Path]::GetPathRoot($BaseDirectory)
return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($root, $value.TrimStart('\')))
}
return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($BaseDirectory, $value))
}
function Set-StyleItem {
param([string[]]$Names)
$script:SuppressStyleEvent = $true
try {
$script:StyleCombo.Items.Clear()
[void]$script:StyleCombo.Items.Add($script:ClearStyleName)
foreach ($name in $Names) {
[void]$script:StyleCombo.Items.Add($name)
}
$script:StyleCombo.SelectedIndex = -1
}
finally {
$script:SuppressStyleEvent = $false
}
}
function Update-StyleList {
[OutputType([pscustomobject])]
param(
[AllowEmptyString()][string]$Html,
[AllowEmptyString()][string]$BaseDirectory
)
$cssParts = New-Object System.Collections.Generic.List[string]
$notes = New-Object System.Collections.Generic.List[string]
$level = 'Info'
$styleBlocks = [regex]::Matches($Html, '(?is)<style\b[^>]*>(.*?)</style\s*>')
foreach ($block in $styleBlocks) {
$cssParts.Add($block.Groups[1].Value)
}
$linked = 0
$remote = 0
foreach ($href in @(Get-StylesheetHref -Html $Html)) {
if ($href -match '^(?i)(https?:)?//') {
$remote++
continue
}
if ($href -match '^(?i)[a-z][a-z0-9+.\-]+:' -and $href -notmatch '^(?i)file:') {
$remote++
continue
}
try {
$cssPath = Resolve-StylesheetPath -Href $href -BaseDirectory $BaseDirectory
if ([System.IO.File]::Exists($cssPath)) {
$cssParts.Add([System.IO.File]::ReadAllText($cssPath, [System.Text.Encoding]::UTF8))
$linked++
}
else {
$notes.Add(('Missing stylesheet: {0} ({1})' -f $href, $cssPath))
$level = 'Warning'
}
}
catch {
$notes.Add(('Stylesheet {0} skipped: {1}' -f $href, $_.Exception.Message))
$level = 'Warning'
}
}
$names = @(Get-CssStyleName -CssText ($cssParts -join "`n"))
Set-StyleItem -Names $names
$message = '{0} style(s) from {1} style block(s) and {2} linked stylesheet(s)' -f $names.Count, $styleBlocks.Count, $linked
if ($remote -gt 0) {
$message += ('; {0} remote stylesheet(s) skipped' -f $remote)
}
if ($notes.Count -gt 0) {
$message += ('. ' + ($notes -join ' | '))
}
return [pscustomobject]@{ Message = $message; Level = $level }
}
function Get-BlankHtml {
$lines = @(
"<!DOCTYPE html>"
"<html lang=`"en`">"
"<head>"
"<meta charset=`"utf-8`">"
"<title>Untitled</title>"
"</head>"
"<body>"
"<p>Start typing here, or use Open to load an HTML file.</p>"
"</body>"
"</html>"
""
)
return ($lines -join "`r`n")
}
function Get-DocumentDirectory {
if ([string]::IsNullOrEmpty($script:CurrentPath)) {
return $null
}
return [System.IO.Path]::GetDirectoryName($script:CurrentPath)
}
function ConvertTo-LoadableHtml {
[OutputType([string])]
param(
[AllowEmptyString()][string]$Html,
[AllowEmptyString()][string]$BaseDirectory
)
$script:InjectedBaseUri = $null
$inject = ''
if ($Html -notmatch '(?is)<meta\b[^>]*http-equiv\s*=\s*["'']?\s*X-UA-Compatible') {
$inject += "<meta http-equiv=`"X-UA-Compatible`" content=`"IE=edge`" data-hte-injected=`"1`">"
}
if (-not [string]::IsNullOrEmpty($BaseDirectory) -and $Html -notmatch '(?is)<base\b') {
$folder = $BaseDirectory.TrimEnd('\', '/') + '\'
$uri = (New-Object System.Uri($folder)).AbsoluteUri
$script:InjectedBaseUri = $uri
$inject += ("<base href=`"{0}`" data-hte-injected=`"1`">" -f [System.Net.WebUtility]::HtmlEncode($uri))
}
if ($inject.Length -eq 0) {
return $Html
}
foreach ($pattern in @('(?is)<head(?:\s[^>]*)?>', '(?is)<html(?:\s[^>]*)?>', '(?is)<!DOCTYPE[^>]*>')) {
$match = [regex]::Match($Html, $pattern)
if ($match.Success) {
$at = $match.Index + $match.Length
return ($Html.Substring(0, $at) + $inject + $Html.Substring($at))
}
}
return ($inject + $Html)
}
function Get-BodyHtml {
[OutputType([string])]
param()
if ($null -eq $script:Browser) {
return ''
}
$doc = $script:Browser.Document
if ($null -eq $doc -or $null -eq $doc.Body) {
return ''
}
$html = $doc.Body.InnerHtml
if ($null -eq $html) {
return ''
}
return [string]$html
}
function Get-DocumentHtml {
[OutputType([string])]
param([switch]$KeepSelectionMarkers)
$html = Get-SplicedDocumentHtml
if ($KeepSelectionMarkers) {
return $html
}
return [regex]::Replace($html, $script:SelectionMarkerPattern, '')
}
function Get-SplicedDocumentHtml {
[OutputType([string])]
param()
$bodyHtml = Get-BodyHtml
if (-not [string]::IsNullOrEmpty($script:InjectedBaseUri)) {
$pattern = '(?i)(\b(?:href|src)\s*=\s*["'']?)' + [regex]::Escape($script:InjectedBaseUri)
$bodyHtml = [regex]::Replace($bodyHtml, $pattern, '$1')
}
$original = [string]$script:OriginalText
$open = [regex]::Match($original, '(?is)<body(?:\s[^>]*)?>')
if ($open.Success) {
$prefixEnd = $open.Index + $open.Length
$close = $original.LastIndexOf('</body', [System.StringComparison]::OrdinalIgnoreCase)
if ($close -ge $prefixEnd) {
return ($original.Substring(0, $prefixEnd) + $bodyHtml + $original.Substring($close))
}
return ($original.Substring(0, $prefixEnd) + $bodyHtml)
}
$doc = $script:Browser.Document
$htmlElements = $doc.GetElementsByTagName('html')
if ($htmlElements.Count -lt 1) {
throw 'Unable to read the document element from the browser.'
}
$outer = [string]$htmlElements[0].OuterHtml
$outer = [regex]::Replace($outer, '(?is)<script\b[^>]*data-hte-injected[^>]*>.*?</script>\s*', '')
$outer = [regex]::Replace($outer, '(?is)<(?:meta|base)\b[^>]*data-hte-injected[^>]*>\s*', '')
$outer = [regex]::Replace($outer, '(?is)(<body\b[^>]*?)\s+contenteditable\s*=\s*(?:"true"|''true''|true)', '$1')
$doctype = [regex]::Match($original, '(?is)<!DOCTYPE[^>]*>')
if ($doctype.Success) {
return ($doctype.Value + "`r`n" + $outer)
}
return $outer
}
function Get-EditorScript {
$lines = @(
"(function () {"
" var w = window, d = document;"
" var BLOCKS = { P: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, BLOCKQUOTE: 1, PRE: 1, ADDRESS: 1, DIV: 1, LI: 1, TD: 1, TH: 1, DD: 1, DT: 1 };"
" var TO_PARAGRAPH = { H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, PRE: 1, ADDRESS: 1, BLOCKQUOTE: 1 };"
" var FIXED = { LI: 1, TD: 1, TH: 1, DD: 1, DT: 1 };"
" function msg(e) { return (e && (e.message || e.description)) ? (e.message || e.description) : String(e); }"
" function norm(s) {"
" var t = String(s === null || s === undefined ? '' : s).split(/[\s\u00a0]+/).join(' ');"
" while (t.charAt(0) === ' ') { t = t.substring(1); }"
" while (t.length > 0 && t.charAt(t.length - 1) === ' ') { t = t.substring(0, t.length - 1); }"
" return t;"
" }"
" function tagOf(n) { return (n && n.nodeName) ? String(n.nodeName).toUpperCase() : ''; }"
" function isBlock(n) { return !!(n && n.nodeType === 1 && BLOCKS[tagOf(n)]); }"
" function elemOf(n) { return (n && n.nodeType === 3) ? n.parentNode : n; }"
" function blockOf(n) { n = elemOf(n); while (n && n !== d.body) { if (isBlock(n)) { return n; } n = n.parentNode; } return null; }"
" function getRange() { if (w.getSelection) { var s = w.getSelection(); if (s && s.rangeCount > 0) { return s.getRangeAt(0); } } return null; }"
" function pointRange(c, o) { var r = d.createRange(); r.setStart(c, o); r.setEnd(c, o); return r; }"
" function cmp(c1, o1, c2, o2) { return pointRange(c1, o1).compareBoundaryPoints(0, pointRange(c2, o2)); }"
" function intersectRange(r, n) {"
" var nr = d.createRange();"
" nr.selectNodeContents(n);"
" if (cmp(r.startContainer, r.startOffset, nr.endContainer, nr.endOffset) >= 0) { return null; }"
" if (cmp(r.endContainer, r.endOffset, nr.startContainer, nr.startOffset) <= 0) { return null; }"
" if (cmp(r.startContainer, r.startOffset, nr.startContainer, nr.startOffset) > 0) { nr.setStart(r.startContainer, r.startOffset); }"
" if (cmp(r.endContainer, r.endOffset, nr.endContainer, nr.endOffset) < 0) { nr.setEnd(r.endContainer, r.endOffset); }"
" return nr;"
" }"
" function overlapText(r, n) { var nr = intersectRange(r, n); return nr ? norm(nr.toString()) : ''; }"
" function fullText(n) { var nr = d.createRange(); nr.selectNodeContents(n); return norm(nr.toString()); }"
" function blocksInRange(r) {"
" var all = d.body.getElementsByTagName('*'), found = [], leaves = [], i, j, keep;"
" for (i = 0; i < all.length; i++) { if (isBlock(all[i]) && overlapText(r, all[i]) !== '') { found.push(all[i]); } }"
" for (i = 0; i < found.length; i++) {"
" keep = true;"
" for (j = 0; j < found.length; j++) { if (i !== j && found[i].contains(found[j])) { keep = false; break; } }"
" if (keep) { leaves.push(found[i]); }"
" }"
" return leaves;"
" }"
" function targetBlocks(r) { var b = blocksInRange(r), one; if (b.length === 0) { one = blockOf(r.startContainer); if (one) { b = [one]; } } return b; }"
" function selectContents(n) { if (!w.getSelection || !d.createRange) { return; } var s = w.getSelection(), r = d.createRange(); r.selectNodeContents(n); s.removeAllRanges(); s.addRange(r); }"
" function selectBlocks(list) {"
" if (!list.length || !w.getSelection) { return; }"
" var s = w.getSelection(), r = d.createRange(), last = list[list.length - 1];"
" r.setStart(list[0], 0); r.setEnd(last, last.childNodes.length); s.removeAllRanges(); s.addRange(r);"
" }"
" function replaceBlocks(blocks, tag) {"
" var out = [], i, a, at, old, nw, up = tag.toUpperCase();"
" for (i = 0; i < blocks.length; i++) {"
" old = blocks[i];"
" if (tagOf(old) === up || FIXED[tagOf(old)]) { out.push(old); continue; }"
" nw = d.createElement(tag);"
" for (a = 0; a < old.attributes.length; a++) {"
" at = old.attributes[a];"
" if (at.specified !== false && at.name.toLowerCase() !== 'class' && at.name.toLowerCase() !== 'classname') { nw.setAttribute(at.name, at.value); }"
" }"
" while (old.firstChild) { nw.appendChild(old.firstChild); }"
" old.parentNode.replaceChild(nw, old);"
" out.push(nw);"
" }"
" return out;"
" }"
" function formatBlocks(tag) {"
" var up = tag.toUpperCase(), r = getRange(), blocks, i, result = [], mismatch = false;"
" if (!r) { return []; }"
" blocks = targetBlocks(r);"
" for (i = 0; i < blocks.length; i++) { if (tagOf(blocks[i]) !== up) { mismatch = true; } }"
" if (mismatch) {"
" mismatch = false;"
" try { d.execCommand('formatBlock', false, '<' + tag + '>'); } catch (e1) { mismatch = true; }"
" r = getRange();"
" if (r && !mismatch) { blocks = targetBlocks(r); }"
" for (i = 0; i < blocks.length; i++) { if (tagOf(blocks[i]) !== up) { mismatch = true; } }"
" if (mismatch && blocks.length) { blocks = replaceBlocks(blocks, tag); selectBlocks(blocks); }"
" }"
" for (i = 0; i < blocks.length; i++) { if (tagOf(blocks[i]) === up) { result.push(blocks[i]); } }"
" return result;"
" }"
" function legacyRange() { if (d.selection && d.selection.createRange) { var tr = d.selection.createRange(); if (tr && tr.parentElement) { return tr; } } return null; }"
" function legacyApplyClass(cls, tag) {"
" var tr = legacyRange(), block, text;"
" if (!tr) { return 'ERR:No text selection is available.'; }"
" block = blockOf(tr.parentElement());"
" if (tag !== '' && tag !== 'span') {"
" if (!block || tagOf(block) !== tag.toUpperCase()) { d.execCommand('formatBlock', false, '<' + tag + '>'); tr = legacyRange(); block = tr ? blockOf(tr.parentElement()) : null; }"
" if (block && tagOf(block) === tag.toUpperCase()) { block.className = cls; return 'OK:Applied ' + tag + '.' + cls + '.'; }"
" return 'ERR:Could not convert the selection to <' + tag + '>.';"
" }"
" text = norm(tr.text);"
" if (text === '') { return 'ERR:Select some text first.'; }"
" if (tag !== 'span' && block && norm(block.innerText) === text) { block.className = cls; return 'OK:Applied .' + cls + ' to the block.'; }"
" tr.pasteHTML('<span class=\x22' + cls + '\x22>' + tr.htmlText + '</span>');"
" return 'OK:Wrapped the selection in a span with class ' + cls + '.';"
" }"
" function legacyClearStyle() {"
" var tr = legacyRange(), p, n, b;"
" if (!tr) { return 'ERR:No text selection is available.'; }"
" p = tr.parentElement();"
" for (n = p; n && n !== d.body && !isBlock(n); n = n.parentNode) {"
" if (tagOf(n) === 'SPAN' && n.className) { n.outerHTML = n.innerHTML; return 'OK:Removed the styled span.'; }"
" }"
" b = blockOf(p);"
" if (b && b.className) { b.removeAttribute('className'); b.removeAttribute('class'); return 'OK:Removed the class from the block.'; }"
" d.execCommand('RemoveFormat', false, null);"
" return 'OK:Removed inline formatting (RemoveFormat).';"
" }"
" w.__hteApplyClass = function (cls, tag) {"
" try {"
" var r, blocks, list, text, span, i;"
" tag = String(tag || '').toLowerCase();"
" r = getRange();"
" if (!r) { return legacyApplyClass(cls, tag); }"
" if (tag !== '' && tag !== 'span') {"
" if (!targetBlocks(r).length) { return 'ERR:Place the cursor inside a paragraph or heading first.'; }"
" list = formatBlocks(tag);"
" if (!list.length) { return 'ERR:Could not convert the selection to <' + tag + '>.'; }"
" for (i = 0; i < list.length; i++) { list[i].className = cls; }"
" return 'OK:Applied ' + tag + '.' + cls + ' to ' + list.length + ' block(s).';"
" }"
" text = norm(r.toString());"
" if (r.collapsed || text === '') { return 'ERR:Select some text (or a whole paragraph) before applying ' + tag + '.' + cls + '.'; }"
" blocks = blocksInRange(r);"
" if (tag !== 'span') {"
" if (blocks.length > 1) { for (i = 0; i < blocks.length; i++) { blocks[i].className = cls; } return 'OK:Applied .' + cls + ' to ' + blocks.length + ' blocks.'; }"
" if (blocks.length === 1 && text === fullText(blocks[0])) { blocks[0].className = cls; selectContents(blocks[0]); return 'OK:Applied .' + cls + ' to the <' + tagOf(blocks[0]).toLowerCase() + '> block.'; }"
" } else if (blocks.length > 1) {"
" return 'ERR:span.' + cls + ' can only wrap text inside a single block.';"
" }"
" span = d.createElement('span');"
" span.className = cls;"
" try { r.surroundContents(span); } catch (e1) { span.appendChild(r.extractContents()); r.insertNode(span); }"
" selectContents(span);"
" return 'OK:Wrapped the selection in <span class=\x22' + cls + '\x22>.';"
" } catch (e) { return 'ERR:' + msg(e); }"
" };"
" w.__hteClearStyle = function () {"
" try {"
" var r = getRange(), anc, n, spans = [], inner, i, count = 0, blocks, cleared = 0, convert = false, s, p, list;"
" if (!r) { return legacyClearStyle(); }"
" anc = elemOf(r.commonAncestorContainer);"
" for (n = anc; n && n !== d.body && !isBlock(n); n = n.parentNode) { if (tagOf(n) === 'SPAN' && n.className) { spans.push(n); } }"
" if (!r.collapsed && anc && anc.getElementsByTagName) {"
" inner = anc.getElementsByTagName('span');"
" for (i = 0; i < inner.length; i++) { if (inner[i].className && overlapText(r, inner[i]) !== '') { spans.push(inner[i]); } }"
" }"
" for (i = 0; i < spans.length; i++) {"
" s = spans[i];"
" if (!s.parentNode) { continue; }"
" s.removeAttribute('class'); if (s.className) { s.removeAttribute('className'); }"
" if (isBareSpan(s)) { unwrap(s); }"
" count++;"
" }"
" if (count) { return 'OK:Removed the class from ' + count + ' span(s).'; }"
" blocks = targetBlocks(r);"
" for (i = 0; i < blocks.length; i++) {"
" if (blocks[i].className) { blocks[i].removeAttribute('class'); if (blocks[i].className) { blocks[i].removeAttribute('className'); } cleared++; }"
" }"
" if (cleared) { return 'OK:Removed the class from ' + cleared + ' block(s).'; }"
" for (i = 0; i < blocks.length; i++) { if (TO_PARAGRAPH[tagOf(blocks[i])]) { convert = true; } }"
" if (convert) { list = formatBlocks('p'); return 'OK:Converted ' + list.length + ' block(s) back to <p>.'; }"
" if (!r.collapsed) { d.execCommand('RemoveFormat', false, null); return 'OK:Removed inline formatting (RemoveFormat).'; }"
" return 'INFO:Nothing to clear at the cursor.';"
" } catch (e) { return 'ERR:' + msg(e); }"
" };"
" w.__hteReplaceBlock = function (tag) {"
" try {"
" var r = getRange(), blocks, list, n = 0, i, up = String(tag).toUpperCase();"
" if (!r) { return 'ERR:Block conversion needs a standards-mode document (IE9 or later).'; }"
" blocks = targetBlocks(r);"
" if (!blocks.length) { return 'ERR:Place the cursor inside a paragraph or heading first.'; }"
" for (i = 0; i < blocks.length; i++) { if (tagOf(blocks[i]) !== up && !FIXED[tagOf(blocks[i])]) { n++; } }"
" if (n === 0) { return 'INFO:The selection is already <' + tag + '> (or is a list item / table cell).'; }"
" list = replaceBlocks(blocks, tag);"
" selectBlocks(list);"
" return 'OK:Changed ' + n + ' block(s) to <' + tag + '>.';"
" } catch (e) { return 'ERR:' + msg(e); }"
" };"
" function unwrap(s) { var p = s.parentNode; if (!p) { return; } while (s.firstChild) { p.insertBefore(s.firstChild, s); } p.removeChild(s); }"
" function attrCount(el) { var i, n = 0; for (i = 0; i < el.attributes.length; i++) { if (el.attributes[i].specified !== false) { n++; } } return n; }"
" function isBareSpan(el) { return tagOf(el) === 'SPAN' && attrCount(el) === 0; }"
" function camel(p) { return String(p).replace(/-([a-z])/g, function (m, c) { return c.toUpperCase(); }); }"
" function getProp(el, p) { var st = el.style; if (!st) { return ''; } if (st.getPropertyValue) { return st.getPropertyValue(p) || ''; } return st[camel(p)] || ''; }"
" function setProp(el, p, v) { var st = el.style; if (st.setProperty) { st.setProperty(p, v, ''); } else { st[camel(p)] = v; } }"
" function removeProp(el, p) {"
" var st = el.style;"
" if (st.removeProperty) { st.removeProperty(p); } else { st[camel(p)] = ''; }"
" if (norm(st.cssText) === '') { el.removeAttribute('style'); }"
" }"
" function matchingSpan(r, text) {"
" var n, kids, i;"
" for (n = elemOf(r.commonAncestorContainer); n && n !== d.body && !isBlock(n); n = n.parentNode) {"
" if (tagOf(n) === 'SPAN' && fullText(n) === text) { return n; }"
" }"
" n = elemOf(r.commonAncestorContainer);"
" if (n && n.getElementsByTagName) {"
" kids = n.getElementsByTagName('span');"
" for (i = 0; i < kids.length; i++) { if (fullText(kids[i]) === text && overlapText(r, kids[i]) === text) { return kids[i]; } }"
" }"
" return null;"
" }"
" function spansWithProp(r, prop) {"
" var out = [], anc = elemOf(r.commonAncestorContainer), n, inner, i;"
" for (n = anc; n && n !== d.body && !isBlock(n); n = n.parentNode) { if (tagOf(n) === 'SPAN' && getProp(n, prop)) { out.push(n); } }"
" if (!r.collapsed && anc && anc.getElementsByTagName) {"
" inner = anc.getElementsByTagName('span');"
" for (i = 0; i < inner.length; i++) { if (getProp(inner[i], prop) && overlapText(r, inner[i]) !== '') { out.push(inner[i]); } }"
" }"
" return out;"
" }"
" function wrapInline(r, prop, value) {"
" var span = d.createElement('span'), inner, list = [], i;"
" setProp(span, prop, value);"
" try { r.surroundContents(span); } catch (e1) { span.appendChild(r.extractContents()); r.insertNode(span); }"
" inner = span.getElementsByTagName('span');"
" for (i = 0; i < inner.length; i++) { list.push(inner[i]); }"
" for (i = 0; i < list.length; i++) { if (getProp(list[i], prop)) { removeProp(list[i], prop); if (isBareSpan(list[i])) { unwrap(list[i]); } } }"
" return span;"
" }"
" w.__hteApplyInlineStyle = function (prop, value) {"
" try {"
" var r = getRange(), text, target, blocks, subs = [], spans = [], i, sub, sel, nr, last, list, count = 0;"
" prop = String(prop); value = String(value || '');"
" if (!r) { return 'ERR:Text color and size need a standards-mode document (IE9 or later).'; }"
" if (value === '') {"
" list = spansWithProp(r, prop);"
" for (i = 0; i < list.length; i++) { if (!list[i].parentNode) { continue; } removeProp(list[i], prop); if (isBareSpan(list[i])) { unwrap(list[i]); } count++; }"
" return count ? ('OK:Removed ' + prop + ' from ' + count + ' span(s).') : ('INFO:No ' + prop + ' was found on the selection.');"
" }"
" text = norm(r.toString());"
" if (r.collapsed || text === '') { return 'ERR:Select some text first.'; }"
" target = matchingSpan(r, text);"
" if (target) { setProp(target, prop, value); selectContents(target); return 'OK:Set ' + prop + ': ' + value + ' on the existing span.'; }"
" blocks = blocksInRange(r);"
" if (blocks.length > 1) {"
" for (i = 0; i < blocks.length; i++) { sub = intersectRange(r, blocks[i]); if (sub && norm(sub.toString()) !== '') { subs.push(sub); } }"
" for (i = 0; i < subs.length; i++) { spans.push(wrapInline(subs[i], prop, value)); }"
" if (spans.length && w.getSelection) {"
" nr = d.createRange(); last = spans[spans.length - 1];"
" nr.setStart(spans[0], 0); nr.setEnd(last, last.childNodes.length);"
" sel = w.getSelection(); sel.removeAllRanges(); sel.addRange(nr);"
" }"
" return 'OK:Applied ' + prop + ': ' + value + ' in ' + spans.length + ' blocks.';"
" }"
" selectContents(wrapInline(r, prop, value));"
" return 'OK:Applied ' + prop + ': ' + value + '.';"
" } catch (e) { return 'ERR:' + msg(e); }"
" };"
" var MARK = 'HTESEL';"
" function findMarkers(n, out) {"
" var c;"
" for (c = n.firstChild; c; c = c.nextSibling) {"
" if (c.nodeType === 8 && String(c.data).indexOf(MARK) === 0) { out.push(c); } else if (c.nodeType === 1) { findMarkers(c, out); }"
" }"
" return out;"
" }"
" function dropEmptyText(q) { if (q && q.nodeType === 3 && q.data === '' && q.parentNode) { q.parentNode.removeChild(q); } }"
" function removeNodes(list) {"
" var i, n = 0, x;"
" for (i = 0; i < list.length; i++) {"
" x = list[i];"
" if (x.parentNode) { dropEmptyText(x.previousSibling); dropEmptyText(x.nextSibling); x.parentNode.removeChild(x); n++; }"
" }"
" return n;"
" }"
" w.__hteMarkSelection = function (id) {"
" try {"
" var r = getRange(), s, e, er, sr;"
" if (!r || !d.body.contains(elemOf(r.commonAncestorContainer))) { return 'NONE'; }"
" e = d.createComment(MARK + 'E' + id); s = d.createComment(MARK + 'S' + id);"
" er = r.cloneRange(); er.collapse(false); er.insertNode(e);"
" sr = r.cloneRange(); sr.collapse(true); sr.insertNode(s);"
" return r.collapsed ? 'CARET' : 'RANGE';"
" } catch (x) { removeNodes(findMarkers(d.body, [])); return 'ERR:' + msg(x); }"
" };"
" w.__hteUnmarkSelection = function (id, scroll) {"
" try {"
" var list = findMarkers(d.body, []), s = null, e = null, i, r = null, sel, probe, n;"
" for (i = 0; i < list.length; i++) { if (list[i].data === MARK + 'S' + id) { s = list[i]; } else if (list[i].data === MARK + 'E' + id) { e = list[i]; } }"
" if (s && e && w.getSelection) { try { r = d.createRange(); r.setStartAfter(s); r.setEndBefore(e); } catch (x1) { r = null; } }"
" if (scroll && s && s.parentNode) {"
" probe = d.createElement('span'); s.parentNode.insertBefore(probe, s);"
" try { if (probe.scrollIntoView) { probe.scrollIntoView(true); } } finally { probe.parentNode.removeChild(probe); }"
" }"
" n = removeNodes(list);"
" if (r) { sel = w.getSelection(); sel.removeAllRanges(); sel.addRange(r); }"
" return 'OK:' + n;"
" } catch (x) { removeNodes(findMarkers(d.body, [])); return 'ERR:' + msg(x); }"
" };"
" var selSeq = 1, selSeen = 0;"
" function onSelChange() { selSeq++; }"
" function classText(el) {"
" var c = el.getAttribute ? el.getAttribute('class') : '';"
" if (!c && typeof el.className === 'string') { c = el.className; }"
" return norm(c);"
" }"
" w.__hteSelectionInfo = function (force) {"
" try {"
" var r, tr, n = null, el, out = [], parts, i, tag, block = '', color = '', size = '', computed = '', cs, c, hl = '';"
" if (!force && selSeq === selSeen) { return ''; }"
" selSeen = selSeq;"
" r = getRange();"
" if (r) { n = elemOf(r.commonAncestorContainer); keepRange(r); } else { tr = legacyRange(); if (tr) { n = tr.parentElement(); } }"
" if (!n || n.nodeType !== 1 || (n !== d.body && !d.body.contains(n))) { return 'NONE'; }"
" for (el = n; el && el.nodeType === 1 && el !== d.body; el = el.parentNode) {"
" tag = tagOf(el).toLowerCase();"
" c = classText(el);"
" parts = c === '' ? [] : c.split(' ');"
" for (i = 0; i < parts.length; i++) { out.push('C|' + tag + '|' + parts[i]); }"
" if (block === '' && isBlock(el)) { block = tag; }"
" if (color === '') { color = norm(getProp(el, 'color')); }"
" if (size === '') { size = norm(getProp(el, 'font-size')); }"
" if (hl === '') { hl = norm(getProp(el, 'background-color')); }"
" }"
" try {"
" if (w.getComputedStyle) { cs = w.getComputedStyle(n, null); computed = cs ? norm(cs.fontSize) : ''; }"
" else if (n.currentStyle) { computed = norm(n.currentStyle.fontSize); }"
" } catch (e2) { computed = ''; }"
" out.push('B|' + block); out.push('K|' + color); out.push('S|' + size); out.push('Z|' + computed); out.push('H|' + hl);"
" return 'OK\n' + out.join('\n');"
" } catch (e) { return 'ERR:' + msg(e); }"
" };"
" var MARGIN_OK = { P: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, DIV: 1, PRE: 1, BLOCKQUOTE: 1, ADDRESS: 1, DD: 1, DT: 1 };"
" var FLOW = { UL: 1, OL: 1, P: 1, DIV: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, BLOCKQUOTE: 1, PRE: 1, TABLE: 1, DL: 1, ADDRESS: 1, HR: 1 };"
" var savedRange = null, events = [], lastKey = {}, markSeq = 0;"
" w.__hteHighlight = '#ffff00';"
" w.__hteSetHighlight = function (c) { w.__hteHighlight = String(c); return 'OK'; };"
" function isList(n) { var t = tagOf(n); return t === 'UL' || t === 'OL'; }"
" function prevElem(n) { n = n.previousSibling; while (n && n.nodeType !== 1) { n = n.previousSibling; } return n; }"
" function lastElem(n) { n = n.lastChild; while (n && n.nodeType !== 1) { n = n.previousSibling; } return n; }"
" function hasElemAfter(n) { n = n.nextSibling; while (n) { if (n.nodeType === 1) { return true; } n = n.nextSibling; } return false; }"
" function closestLi(n) { n = elemOf(n); while (n && n !== d.body) { if (tagOf(n) === 'LI') { return n; } n = n.parentNode; } return null; }"
" function hasContent(n) { var c; for (c = n.firstChild; c; c = c.nextSibling) { if (c.nodeType !== 3 || c.data.replace(/[ \t\r\n]+/g, '') !== '') { return true; } } return false; }"
" function rangeInBody(r) { return !!(r && d.body.contains(elemOf(r.commonAncestorContainer))); }"
" function keepRange(r) { if (rangeInBody(r)) { savedRange = r.cloneRange(); } }"
" w.__hteSaveSelection = function () { try { var r = getRange(); if (rangeInBody(r)) { keepRange(r); return 'OK'; } return 'NONE'; } catch (e) { return 'ERR:' + msg(e); } };"
" w.__hteRestoreSelection = function () {"
" try {"
" var s;"
" if (rangeInBody(getRange()) || !savedRange || !w.getSelection) { return 'KEPT'; }"
" if (!rangeInBody(savedRange)) { savedRange = null; return 'STALE'; }"
" s = w.getSelection(); s.removeAllRanges(); s.addRange(savedRange);"
" return 'RESTORED';"
" } catch (e) { return 'ERR:' + msg(e); }"
" };"
" function ownOverlap(r, li) {"
" var c;"
" for (c = li.firstChild; c; c = c.nextSibling) {"
" if (c.nodeType === 1 && isList(c)) { continue; }"
" if ((c.nodeType === 1 || c.nodeType === 3) && overlapText(r, c) !== '') { return true; }"
" }"
" return false;"
" }"
" function indentTargets(r) {"
" var out = [], lis = [], all, i, j, li, b, blocks, keep;"
" if (!r.collapsed) {"
" all = d.body.getElementsByTagName('li');"
" for (i = 0; i < all.length; i++) { if (ownOverlap(r, all[i])) { lis.push(all[i]); } }"
" }"
" if (!lis.length) { li = closestLi(r.startContainer); if (li) { lis.push(li); } }"
" for (i = 0; i < lis.length; i++) {"
" keep = true;"
" for (j = 0; j < lis.length; j++) { if (i !== j && lis[j].contains(lis[i])) { keep = false; break; } }"
" if (keep) { out.push({ li: true, el: lis[i] }); }"
" }"
" blocks = targetBlocks(r);"
" for (i = 0; i < blocks.length; i++) {"
" b = blocks[i];"
" if (closestLi(b) || !MARGIN_OK[tagOf(b)]) { continue; }"
" out.push({ li: false, el: b });"
" }"
" return out;"
" }"
" function nestLi(li) {"
" var list = li.parentNode, prev, sub, tag;"
" if (!list || !isList(list)) { return false; }"
" tag = list.nodeName.toLowerCase();"
" prev = prevElem(li);"
" if (prev && tagOf(prev) === 'LI') {"
" sub = lastElem(prev);"
" if (!sub || tagOf(sub) !== tagOf(list)) { sub = d.createElement(tag); prev.appendChild(sub); }"
" sub.appendChild(li);"
" } else if (prev && tagOf(prev) === tagOf(list)) {"
" prev.appendChild(li);"
" } else {"
" sub = d.createElement(tag);"
" list.insertBefore(sub, li);"
" sub.appendChild(li);"
" }"
" return true;"
" }"
" function liftLi(li) {"
" var list = li.parentNode, outer, tail = null, p, n, parent, anchor;"
" if (!list || !isList(list)) { return false; }"
" outer = list.parentNode;"
" if (hasElemAfter(li)) { tail = d.createElement(list.nodeName.toLowerCase()); while (li.nextSibling) { tail.appendChild(li.nextSibling); } }"
" if (outer && tagOf(outer) === 'LI' && outer.parentNode) {"
" if (tail) { li.appendChild(tail); }"
" outer.parentNode.insertBefore(li, outer.nextSibling);"
" } else if (outer && isList(outer)) {"
" if (tail) { li.appendChild(tail); }"
" outer.insertBefore(li, list.nextSibling);"
" } else {"
" parent = list.parentNode; anchor = list.nextSibling;"
" p = d.createElement('p');"
" while (li.firstChild) { n = li.firstChild; if (n.nodeType === 1 && FLOW[tagOf(n)]) { break; } p.appendChild(n); }"
" if (hasContent(p)) { parent.insertBefore(p, anchor); }"
" while (li.firstChild) { parent.insertBefore(li.firstChild, anchor); }"
" if (tail) { parent.insertBefore(tail, anchor); }"
" list.removeChild(li);"
" }"
" if (!lastElem(list) && list.parentNode) { list.parentNode.removeChild(list); }"
" return true;"
" }"
" function toInches(v) {"
" var s = norm(v).toLowerCase(), n = parseFloat(s), u = s.replace(/[-+0-9.\s]/g, '');"
" if (isNaN(n)) { return 0; }"
" if (u === 'in') { return n; }"
" if (u === 'pt') { return n / 72; }"
" if (u === 'cm') { return n / 2.54; }"
" if (u === 'mm') { return n / 25.4; }"
" if (u === 'em' || u === 'rem' || u === 'pc') { return n / 6; }"
" return n / 96;"
" }"
" function stepMargin(el, dir) {"
" var cur = getProp(el, 'margin-left'), inches = toInches(cur), next;"
" next = dir > 0 ? (Math.floor(inches * 2 + 0.000001) + 1) / 2 : (Math.ceil(inches * 2 - 0.000001) - 1) / 2;"
" if (next <= 0) { if (cur !== '') { removeProp(el, 'margin-left'); return true; } return false; }"
" setProp(el, 'margin-left', next + 'in');"
" return true;"
" }"
" function indentCore(dir) {"
" var r = getRange(), targets, i, t, lis = 0, blocks = 0, hasLi = false, id, st, run;"
" if (!r || !rangeInBody(r)) { return 'ERR:Place the cursor in the document first (indent needs IE9 or later).'; }"
" targets = indentTargets(r);"
" if (!targets.length) { return 'INFO:Place the cursor in a paragraph, heading or list item to change its indent.'; }"
" for (i = 0; i < targets.length; i++) { if (targets[i].li) { hasLi = true; } }"
" run = function () {"
" for (i = 0; i < targets.length; i++) {"
" t = targets[i];"
" if (t.li) { if (dir > 0 ? nestLi(t.el) : liftLi(t.el)) { lis++; } }"
" else if (stepMargin(t.el, dir)) { blocks++; }"
" }"
" };"
" if (hasLi) {"
" id = 'ind' + (++markSeq);"
" st = w.__hteMarkSelection(id);"
" try { run(); } finally { if (st === 'RANGE' || st === 'CARET') { w.__hteUnmarkSelection(id, ''); } }"
" } else { run(); }"
" if (lis + blocks === 0) { return dir > 0 ? 'INFO:Nothing to indent.' : 'INFO:Already at the left margin.'; }"
" return 'OK:' + (dir > 0 ? 'Increased' : 'Decreased') + ' indent (' + lis + ' list item(s), ' + blocks + ' block(s)).';"
" }"
" w.__hteIndent = function (dir) { try { return indentCore(Number(dir) > 0 ? 1 : -1); } catch (e) { return 'ERR:' + msg(e); } };"
" function tabCore(shift) {"
" var r = getRange(), b, pre, t, nr, s;"
" if (!rangeInBody(r)) { return ''; }"
" if (blocksInRange(r).length > 1 || closestLi(r.startContainer)) { return indentCore(shift ? -1 : 1); }"
" b = blockOf(r.startContainer);"
" if (r.collapsed && b && MARGIN_OK[tagOf(b)]) {"
" pre = d.createRange(); pre.setStart(b, 0); pre.setEnd(r.startContainer, r.startOffset);"
" if (pre.toString() === '') { return indentCore(shift ? -1 : 1); }"
" }"
" if (shift) { return ''; }"
" t = d.createTextNode('\u00a0\u00a0\u00a0\u00a0');"
" if (!r.collapsed) { r.deleteContents(); }"
" r.insertNode(t);"
" nr = d.createRange(); nr.setStartAfter(t); nr.collapse(true);"
" s = w.getSelection(); s.removeAllRanges(); s.addRange(nr);"
" return '';"
" }"
" function pushEvent(item) { events.push(item); if (events.length > 50) { events.shift(); } }"
" w.__hteTakeEvent = function () { return events.length ? events.shift() : ''; };"
" function runPageKey(name) {"
" var before = d.body.innerHTML, res;"
" if (name === 'tab' || name === 'shift-tab') { res = tabCore(name === 'shift-tab'); }"
" else if (name === 'indent' || name === 'outdent') { res = indentCore(name === 'indent' ? 1 : -1); }"
" else if (name === 'highlight') { w.__hteRestoreSelection(); res = w.__hteApplyInlineStyle('background-color', String(w.__hteHighlight || '#ffff00')); }"
" else { return 'ERR:Unknown key action ' + name + '.'; }"
" if (d.body.innerHTML !== before) { pushEvent('U' + before + '\u0001' + d.body.innerHTML); }"
" if (res) { pushEvent('M' + res); }"
" selSeq++;"
" return res || 'OK';"
" }"
" w.__htePageKey = function (name) {"
" try {"
" var now = new Date().getTime();"
" name = String(name);"
" if (lastKey[name] && now - lastKey[name] < 300) { return 'DUP'; }"
" lastKey[name] = now;"
" return runPageKey(name);"
" } catch (e) { return 'ERR:' + msg(e); }"
" };"
" function onKey(e) {"
" var k, c, a, s, name = '';"
" e = e || w.event; k = e.keyCode; c = e.ctrlKey; a = e.altKey; s = e.shiftKey;"
" if (k === 116 || (c && !a && (k === 76 || k === 78 || k === 79 || k === 80 || k === 82 || k === 83))) {"
" if (e.preventDefault) { e.preventDefault(); }"
" e.returnValue = false;"
" try { e.keyCode = 0; } catch (x) { e.cancelBubble = true; }"
" return false;"
" }"
" if (k === 9 && !c && !a) { name = s ? 'shift-tab' : 'tab'; }"
" else if (a && s && !c && (k === 37 || k === 39)) { name = k === 39 ? 'indent' : 'outdent'; }"
" else if (c && a && !s && k === 72) { name = 'highlight'; }"
" if (name !== '') {"
" w.__htePageKey(name);"
" if (e.preventDefault) { e.preventDefault(); }"
" e.returnValue = false;"
" return false;"
" }"
" return true;"
" }"
" w.__hteInit = function () {"
" try {"
" d.body.contentEditable = 'true';"
" if (!w.__hteKeysHooked) {"
" if (d.addEventListener) { d.addEventListener('keydown', onKey, true); } else if (d.attachEvent) { d.attachEvent('onkeydown', onKey); }"
" w.__hteKeysHooked = true;"
" }"
" if (!w.__hteSelHooked) {"
" if (d.addEventListener) {"
" d.addEventListener('selectionchange', onSelChange, false);"
" d.addEventListener('mouseup', onSelChange, true);"
" d.addEventListener('keyup', onSelChange, true);"
" } else if (d.attachEvent) {"
" d.attachEvent('onselectionchange', onSelChange); d.attachEvent('onmouseup', onSelChange); d.attachEvent('onkeyup', onSelChange);"
" }"
" w.__hteSelHooked = true;"
" }"
" return 'ok';"
" } catch (e) { return 'ERR:' + msg(e); }"
" };"
" w.__hteMode = function () { return String(d.documentMode || ''); };"
"})();"
)
return ($lines -join "`n")
}
$script:EditorScript = Get-EditorScript
function Import-HtmlText {
param(
[AllowEmptyString()][string]$Html,
[AllowEmptyString()][string]$BaseDirectory,
[AllowEmptyString()][string]$LoadHtml = '',
[AllowEmptyString()][string]$SelectionMarkerId = ''
)
$script:OriginalText = $Html
$script:PendingSelectionId = $SelectionMarkerId
$script:DocReady = $false
$script:ScriptReady = $false
$script:StyleUndo.Clear()
$script:LoadStatus = Update-StyleList -Html $Html -BaseDirectory $BaseDirectory
$sourceForLoad = $Html
if (-not [string]::IsNullOrEmpty($LoadHtml)) {
$sourceForLoad = $LoadHtml
}
$loadable = ConvertTo-LoadableHtml -Html $sourceForLoad -BaseDirectory $BaseDirectory
if (-not [System.IO.Directory]::Exists($script:TempDir)) {
[void][System.IO.Directory]::CreateDirectory($script:TempDir)
}
$script:TempCounter++
$tempFile = [System.IO.Path]::Combine($script:TempDir, ('edit_{0}.htm' -f $script:TempCounter))
[System.IO.File]::WriteAllText($tempFile, $loadable, (New-Object System.Text.UTF8Encoding($true)))
$script:ExpectedTempFile = $tempFile
$script:PendingLoad = $true
$script:AllowNavigation = $true
Write-StatusMessage -Message 'Loading document...'
$script:Browser.Navigate((New-Object System.Uri($tempFile)))
}
function Install-EditorScript {
$doc = $script:Browser.Document
$script:ScriptReady = $false
$state = $null
try {
[void]$doc.InvokeScript('eval', [object[]]@($script:EditorScript))
$state = $doc.InvokeScript('__hteInit')
}
catch {
Write-StatusMessage -Message ('Script injection via eval failed: {0}' -f $_.Exception.Message) -Level Warning
}
if ([string]$state -ne 'ok') {
try {
$heads = $doc.GetElementsByTagName('head')
if ($heads.Count -gt 0) {
$scriptElement = $doc.CreateElement('script')
$scriptElement.SetAttribute('type', 'text/javascript')
$scriptElement.SetAttribute('data-hte-injected', '1')
$scriptElement.DomElement.text = $script:EditorScript
[void]$heads[0].AppendChild($scriptElement)
$state = $doc.InvokeScript('__hteInit')
}
}
catch {
Write-StatusMessage -Message ('Script injection via a script element failed: {0}' -f $_.Exception.Message) -Level Warning
}
}
$script:ScriptReady = ([string]$state -eq 'ok')
if (-not $script:ScriptReady) {
Write-StatusMessage -Message ('Editor script helper unavailable (result: {0}). Class styles and (clear style) will not work; element styles, Bold and Italic still do.' -f [string]$state) -Level Warning
}
}
function Initialize-EditableDocument {
$doc = $script:Browser.Document
if ($null -eq $doc -or $null -eq $doc.Body) {
throw 'The loaded document has no body element.'
}
$doc.Body.SetAttribute('contentEditable', 'true')
Install-EditorScript
if (-not [string]::IsNullOrEmpty($script:PendingSelectionId)) {
$markerId = $script:PendingSelectionId
$script:PendingSelectionId = ''
Restore-LoadedSelection -Id $markerId
}
$script:BaselineBody = Get-BodyHtml
$script:PendingLoad = $false
$script:AllowNavigation = $false
$script:DocReady = $true
$script:SelectionInfoKey = $null
Request-ToolbarRefresh
Reset-AutoSaveTimer
$script:AutoSaveNote = ''
if ($script:ScriptReady) {
try {
[void](Invoke-EditorScript -Name '__hteSetHighlight' -Arguments @($script:HighlightColor))
}
catch {
Write-StatusMessage -Message ('Could not pass the highlight color to the page: {0}' -f $_.Exception.Message) -Level Warning
}
}
$message = 'Ready.'
$level = 'Info'
if ($null -ne $script:LoadStatus) {
$message = $script:LoadStatus.Message
$level = $script:LoadStatus.Level
}
if ($script:ScriptReady) {
$mode = [string]$doc.InvokeScript('__hteMode')
$modeNumber = $mode -as [int]
if ($null -ne $modeNumber) {
$message += (' [IE document mode {0}]' -f $modeNumber)
if ($modeNumber -lt 9) {
$message += ' Legacy mode: check FEATURE_BROWSER_EMULATION / X-UA-Compatible.'
$level = 'Warning'
}
}
}
Write-StatusMessage -Message $message -Level $level
Update-DirtyIndicator
Set-EditorFocus
}
function Test-DocumentDirty {
[OutputType([bool])]
param()
if ($script:SourceMode) {
return ($script:SourceBox.Text -cne $script:SourceCleanText)
}
if ($script:HeadDirty) {
return $true
}
if (-not $script:DocReady) {
return $false
}
return ((Get-BodyHtml) -cne $script:BaselineBody)
}
function Update-DirtyIndicator {
$dirty = Test-DocumentDirty
if ($dirty) {
$script:DirtyText.Text = '*'
}
else {
$script:DirtyText.Text = ''
}
if ([string]::IsNullOrEmpty($script:CurrentPath)) {
$name = 'Untitled'
$pathText = '(untitled - not saved yet)'
}
else {
$name = [System.IO.Path]::GetFileName($script:CurrentPath)
$pathText = $script:CurrentPath
}
if ($script:PathText.Text -cne $pathText) {
$script:PathText.Text = $pathText
$script:PathText.ToolTip = $pathText
}
$title = $name
if ($dirty) {
$title += ' *'
}
if ($script:SourceMode) {
$title += ' [source]'
}
$title += ' - HTML Editor'
if ($script:Window.Title -cne $title) {
$script:Window.Title = $title
}
}
function Set-EditorFocus {
try {
if ($script:SourceMode) {
[void]$script:SourceBox.Focus()
}
elseif ($null -ne $script:Browser) {
[void]$script:FormsHost.Focus()
[void]$script:Browser.Focus()
}
}
catch {
Write-StatusMessage -Message ('Could not focus the editor: {0}' -f $_.Exception.Message) -Level Warning
}
}
function Remove-TempFolder {
try {
if ([System.IO.Directory]::Exists($script:TempDir)) {
[System.IO.Directory]::Delete($script:TempDir, $true)
}
}
catch {
Write-StatusMessage -Message ('Could not remove temp folder {0}: {1}' -f $script:TempDir, $_.Exception.Message) -Level Warning
}
}
function New-HtmlDocument {
Exit-SourceView
$blank = Get-BlankHtml
$script:CurrentPath = $null
$script:SavedText = $blank
$script:HeadDirty = $false
Import-HtmlText -Html $blank -BaseDirectory $null
Update-DirtyIndicator
}
function Open-HtmlDocument {
param([Parameter(Mandatory = $true)][string]$FilePath)
$full = [System.IO.Path]::GetFullPath($FilePath)
if (-not [System.IO.File]::Exists($full)) {
throw ('File not found: {0}' -f $full)
}
$text = [System.IO.File]::ReadAllText($full, [System.Text.Encoding]::UTF8)
Exit-SourceView
$script:CurrentPath = $full
$script:SavedText = $text
$script:HeadDirty = $false
Import-HtmlText -Html $text -BaseDirectory ([System.IO.Path]::GetDirectoryName($full))
Update-DirtyIndicator
}
function Save-HtmlDocument {
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)][string]$FilePath,
[switch]$Quiet
)
if ($script:SourceMode) {
$output = $script:SourceBox.Text
}
else {
if (-not $script:DocReady) {
throw 'The document is still loading; try again in a moment.'
}
$output = Get-DocumentHtml
}
$full = [System.IO.Path]::GetFullPath($FilePath)
$oldDir = Get-DocumentDirectory
[System.IO.File]::WriteAllText($full, $output, (New-Object System.Text.UTF8Encoding($false)))
$script:CurrentPath = $full
$script:SavedText = $output
$newDir = [System.IO.Path]::GetDirectoryName($full)
$dirChanged = ($oldDir -ne $newDir)
$savedMessage = 'Saved {0} ({1:N0} characters, UTF-8 without BOM).' -f $full, $output.Length
if ($script:SourceMode) {
$script:SourceCleanText = $output
if ($dirChanged) {
$script:ForceReloadOnDesign = $true
}
if (-not $Quiet) {
Write-StatusMessage -Message $savedMessage
}
}
else {
$script:OriginalText = $output
$script:HeadDirty = $false
$script:BaselineBody = Get-BodyHtml
if ($dirChanged) {
Import-HtmlText -Html $output -BaseDirectory $newDir
$script:LoadStatus.Message = $savedMessage + ' Reloaded from the new folder: ' + $script:LoadStatus.Message
}
if (-not $Quiet) {
Write-StatusMessage -Message $savedMessage
}
}
Update-DirtyIndicator
return $true
}
function Confirm-DiscardChanges {
[OutputType([bool])]
param()
$dirty = $true
try {
$dirty = Test-DocumentDirty
}
catch {
Write-StatusMessage -Message ('Could not determine whether there are unsaved changes: {0}' -f $_.Exception.Message) -Level Warning
}
if (-not $dirty) {
return $true
}
if (Save-PendingAutoSave) {
return $true
}
$name = 'Untitled'
if (-not [string]::IsNullOrEmpty($script:CurrentPath)) {
$name = [System.IO.Path]::GetFileName($script:CurrentPath)
}
$script:ModalDepth++
try {
$answer = [System.Windows.MessageBox]::Show(
$script:Window,
('Save changes to {0}?' -f $name),
'HTML Editor',
[System.Windows.MessageBoxButton]::YesNoCancel,
[System.Windows.MessageBoxImage]::Warning)
}
finally {
$script:ModalDepth--
}
switch ($answer) {
'Yes' { return [bool](Invoke-SaveCommand) }
'No' { return $true }
default { return $false }
}
}
function Set-FormattingEnabled {
param([bool]$Enabled)
foreach ($control in @($script:StyleCombo, $script:ColorCombo, $script:SizeCombo, $script:BoldButton, $script:ItalicButton,
$script:HighlightButton, $script:HighlightMenuButton, $script:OutdentButton, $script:IndentButton)) {
$control.IsEnabled = $Enabled
}
$script:FormatButton.IsEnabled = -not $Enabled
if ($null -ne $script:ColorShownPanel) {
$script:ColorShownPanel.Opacity = $(if ($Enabled) { 1.0 } else { 0.4 })
}
if (-not $Enabled) {
Close-HighlightPalette
}
}
function Show-SourceView {
if (-not $script:DocReady) {
throw 'The document is still loading; try again in a moment.'
}
$dirtyNow = Test-DocumentDirty
$snapshot = Get-SourceSnapshot
$script:ForceReloadOnDesign = $false
$script:SourceBox.Text = $snapshot.Text
$shown = $script:SourceBox.Text
$script:SourceTextAtEntry = $shown
if ($dirtyNow) {
$script:SourceCleanText = $null
}
else {
$script:SourceCleanText = $shown
}
$script:SourceMode = $true
$script:FormsHost.Visibility = [System.Windows.Visibility]::Collapsed
$script:SourceBox.Visibility = [System.Windows.Visibility]::Visible
Set-FormattingEnabled -Enabled $false
Set-EditorFocus
if ($snapshot.Start -ge 0) {
Select-SourceRange -ExpectedText $snapshot.Text -Start $snapshot.Start -Length $snapshot.Length
$viewMessage = 'HTML source view (selection kept). Toggle back to push your edits into the document.'
}
else {
$script:SourceBox.Select(0, 0)
$script:SourceBox.ScrollToHome()
$viewMessage = 'HTML source view. Toggle back to push your edits into the document.'
}
if ($script:AutoSave) {
$viewMessage += ' Auto Save is paused until you switch back.'
}
Write-StatusMessage -Message $viewMessage
Update-DirtyIndicator
}
function Show-DesignView {
$newText = $script:SourceBox.Text
$selectionStart = $script:SourceBox.SelectionStart
$selectionLength = $script:SourceBox.SelectionLength
$reload = $script:ForceReloadOnDesign -or ($newText -cne $script:SourceTextAtEntry)
$script:SourceMode = $false
$script:ForceReloadOnDesign = $false
$script:SourceBox.Visibility = [System.Windows.Visibility]::Collapsed
$script:FormsHost.Visibility = [System.Windows.Visibility]::Visible
Set-FormattingEnabled -Enabled $true
if ($reload) {
$script:HeadDirty = ($newText -cne $script:SavedText)
$markerId = [guid]::NewGuid().ToString('N')
$loadHtml = ''
try {
$loadHtml = Add-SourceSelectionMarker -Html $newText -Start $selectionStart -Length $selectionLength -Id $markerId
}
catch {
Write-StatusMessage -Message ('Could not carry the selection back to the WYSIWYG view: {0}' -f $_.Exception.Message) -Level Warning
$loadHtml = ''
}
if ([string]::IsNullOrEmpty($loadHtml)) {
$markerId = ''
}
Import-HtmlText -Html $newText -BaseDirectory (Get-DocumentDirectory) -LoadHtml $loadHtml -SelectionMarkerId $markerId
}
else {
if ($null -ne $script:SourceCleanText -and $newText -ceq $script:SourceCleanText) {
$script:OriginalText = $newText
$script:HeadDirty = $false
$script:BaselineBody = Get-BodyHtml
}
Write-StatusMessage -Message 'WYSIWYG view (source unchanged).'
Request-ToolbarRefresh
Set-EditorFocus
}
Update-DirtyIndicator
}
function Exit-SourceView {
if (-not $script:SourceMode) {
return
}
$script:SuppressToggle = $true
try {
$script:SourceToggle.IsChecked = $false
}
finally {
$script:SuppressToggle = $false
}
$script:SourceMode = $false
$script:SourceBox.Visibility = [System.Windows.Visibility]::Collapsed
$script:FormsHost.Visibility = [System.Windows.Visibility]::Visible
Set-FormattingEnabled -Enabled $true
}
function Get-SourceSnapshot {
[OutputType([pscustomobject])]
param()
$id = [guid]::NewGuid().ToString('N')
$startMarker = '<!--HTESELS' + $id + '-->'
$endMarker = '<!--HTESELE' + $id + '-->'
$bodyBefore = Get-BodyHtml
$state = 'NONE'
$marked = $null
try {
if ($script:ScriptReady) {
$state = Invoke-EditorScript -Name '__hteMarkSelection' -Arguments @($id)
if ($state -like 'ERR:*') {
Write-StatusMessage -Message ('The selection could not be marked: {0}' -f $state.Substring(4)) -Level Warning
}
}
$marked = Get-DocumentHtml -KeepSelectionMarkers
}
finally {
if ($state -eq 'RANGE' -or $state -eq 'CARET') {
try {
$unmark = Invoke-EditorScript -Name '__hteUnmarkSelection' -Arguments @($id, '')
if ($unmark -notlike 'OK:*') {
Write-StatusMessage -Message ('Removing the selection markers reported: {0}' -f $unmark) -Level Warning
}
}
catch {
Write-StatusMessage -Message ('Removing the selection markers failed: {0}' -f $_.Exception.Message) -Level Warning
}
}
try {
if ((Get-BodyHtml) -cne $bodyBefore) {
$script:Browser.Document.Body.InnerHtml = $bodyBefore
Write-StatusMessage -Message 'Selection markers changed the document, so the body was restored (the selection may be lost).' -Level Warning
}
}
catch {
Write-StatusMessage -Message ('Could not verify marker removal: {0}' -f $_.Exception.Message) -Level Warning
}
}
$start = $marked.IndexOf($startMarker, [System.StringComparison]::Ordinal)
$end = $marked.IndexOf($endMarker, [System.StringComparison]::Ordinal)
$text = $marked
$selectionStart = -1
$selectionLength = 0
if ($start -ge 0 -and $end -gt $start) {
$text = $marked.Remove($end, $endMarker.Length).Remove($start, $startMarker.Length)
$selectionStart = $start
$selectionLength = $end - $start - $startMarker.Length
}
elseif ($start -ge 0) {
$text = $marked.Remove($start, $startMarker.Length)
$selectionStart = $start
}
$text = [regex]::Replace($text, $script:SelectionMarkerPattern, '')
return [pscustomobject]@{ Text = $text; Start = $selectionStart; Length = $selectionLength }
}
function Get-MappedOffset {
[OutputType([int])]
param(
[AllowEmptyString()][string]$From,
[AllowEmptyString()][string]$To,
[int]$Offset
)
$Offset = [Math]::Max(0, [Math]::Min($Offset, $From.Length))
if ($From -ceq $To) {
return $Offset
}
$breaks = [regex]::Matches($From.Substring(0, $Offset), '\r\n|\n|\r')
$line = $breaks.Count
$column = $Offset
if ($line -gt 0) {
$last = $breaks[$line - 1]
$column = $Offset - ($last.Index + $last.Length)
}
$toBreaks = [regex]::Matches($To, '\r\n|\n|\r')
if ($line -gt $toBreaks.Count) {
return $To.Length
}
$lineStart = 0
if ($line -gt 0) {
$previous = $toBreaks[$line - 1]
$lineStart = $previous.Index + $previous.Length
}
$lineEnd = $To.Length
if ($line -lt $toBreaks.Count) {
$lineEnd = $toBreaks[$line].Index
}
return [Math]::Min($lineStart + $column, $lineEnd)
}
function Select-SourceRange {
param(
[AllowEmptyString()][string]$ExpectedText,
[int]$Start,
[int]$Length
)
$shown = $script:SourceBox.Text
$first = Get-MappedOffset -From $ExpectedText -To $shown -Offset $Start
$last = Get-MappedOffset -From $ExpectedText -To $shown -Offset ($Start + $Length)
$script:SourceBox.Select($first, [Math]::Max(0, $last - $first))
Invoke-Deferred -Action { Show-SourceSelection }
}
function Show-SourceSelection {
try {
if (-not $script:SourceMode) {
return
}
$box = $script:SourceBox
$box.UpdateLayout()
$start = $box.SelectionStart
$line = [regex]::Matches($box.Text.Substring(0, $start), '\r\n|\n|\r').Count
$lineHeight = $box.FontFamily.LineSpacing * $box.FontSize
$box.ScrollToVerticalOffset([Math]::Max(0, ($line * $lineHeight) - ($box.ViewportHeight / 3)))
$box.UpdateLayout()
$rect = $box.GetRectFromCharacterIndex($start)
if (-not $rect.IsEmpty -and ($rect.Left -lt 0 -or $rect.Left -gt ($box.ViewportWidth - 40))) {
$box.ScrollToHorizontalOffset([Math]::Max(0, $box.HorizontalOffset + $rect.Left - 40))
}
[void]$box.Focus()
}
catch {
Write-StatusMessage -Message ('Could not scroll to the selection: {0}' -f $_.Exception.Message) -Level Warning
}
}
function Test-SourceMarkerOffset {
[OutputType([bool])]
param(
[AllowEmptyString()][string]$Html,
[int]$Offset
)
$open = [regex]::Match($Html, '(?is)<body(?:\s[^>]*)?>')
if (-not $open.Success) {
return $false
}
$bodyStart = $open.Index + $open.Length
$bodyEnd = $Html.LastIndexOf('</body', [System.StringComparison]::OrdinalIgnoreCase)
if ($bodyEnd -lt $bodyStart) {
$bodyEnd = $Html.Length
}
if ($Offset -lt $bodyStart -or $Offset -gt $bodyEnd) {
return $false
}
if ($Offset -eq $bodyStart) {
return $true
}
$at = $Offset - 1
if ($Html.LastIndexOf('<', $at) -gt $Html.LastIndexOf('>', $at)) {
return $false
}
$ordinal = [System.StringComparison]::Ordinal
if ($Html.LastIndexOf('<!--', $at, $ordinal) -gt $Html.LastIndexOf('-->', $at, $ordinal)) {
return $false
}
$ignoreCase = [System.StringComparison]::OrdinalIgnoreCase
foreach ($tag in @('script', 'style', 'textarea')) {
if ($Html.LastIndexOf('<' + $tag, $at, $ignoreCase) -gt $Html.LastIndexOf('</' + $tag, $at, $ignoreCase)) {
return $false
}
}
$amp = $Html.LastIndexOf('&', $at)
if ($amp -ge 0 -and ($Offset - $amp) -le 32) {
if ($Html.Substring($amp, $Offset - $amp) -match '^&#?[A-Za-z0-9]*$') {
return $false
}
}
return $true
}
function Add-SourceSelectionMarker {
[OutputType([string])]
param(
[AllowEmptyString()][string]$Html,
[int]$Start,
[int]$Length,
[Parameter(Mandatory = $true)][string]$Id
)
$end = $Start + $Length
$startOk = Test-SourceMarkerOffset -Html $Html -Offset $Start
$endOk = Test-SourceMarkerOffset -Html $Html -Offset $end
if (-not $startOk -and -not $endOk) {
return ''
}
if (-not $startOk) {
$Start = $end
}
elseif (-not $endOk) {
$end = $Start
}
return $Html.Insert($end, '<!--HTESELE' + $Id + '-->').Insert($Start, '<!--HTESELS' + $Id + '-->')
}
function Restore-LoadedSelection {
param([Parameter(Mandatory = $true)][string]$Id)
try {
if ($script:ScriptReady) {
$result = Invoke-EditorScript -Name '__hteUnmarkSelection' -Arguments @($Id, 'scroll')
if ($result -notlike 'OK:*') {
Write-StatusMessage -Message ('Restoring the selection reported: {0}' -f $result) -Level Warning
}
}
}
catch {
Write-StatusMessage -Message ('Could not restore the selection: {0}' -f $_.Exception.Message) -Level Warning
}
finally {
try {
$body = Get-BodyHtml
if ([regex]::IsMatch($body, $script:SelectionMarkerPattern)) {
$script:Browser.Document.Body.InnerHtml = [regex]::Replace($body, $script:SelectionMarkerPattern, '')
}
}
catch {
Write-StatusMessage -Message ('Could not remove leftover selection markers: {0}' -f $_.Exception.Message) -Level Warning
}
}
}
function Invoke-Deferred {
param([Parameter(Mandatory = $true)][scriptblock]$Action)
if ($script:SafeMode) {
Write-EditorLog -Message 'Deferred action skipped (safe mode after an error flood).' -Level Warning
return
}
if ($script:DeferredQueue.Count -ge 50) {
Write-StatusMessage -Message 'Too many pending actions; this one was skipped.' -Level Warning
return
}
$script:DeferredQueue.Add($Action)
try {
[void]$script:Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [System.Action]{ Invoke-DeferredQueue })
}
catch {
$script:DeferredQueue.Remove($Action) | Out-Null
[void](Register-EditorError -Source 'Deferred' -ErrorObject $_)
}
}
function Invoke-DeferredQueue {
if ($script:DeferredQueue.Count -eq 0) {
return
}
$next = $script:DeferredQueue[0]
$script:DeferredQueue.RemoveAt(0)
try {
& $next
}
catch {
[void](Register-EditorError -Source 'Deferred' -ErrorObject $_)
}
}
function Stop-EditorTimers {
if ($null -ne $script:Timer) {
$script:Timer.Stop()
}
if ($null -ne $script:SelectionTimer) {
$script:SelectionTimer.Stop()
}
$script:DeferredQueue.Clear()
$script:PageKeyQueue.Clear()
$script:SafeMode = $true
Write-EditorLog -Message 'Timers stopped: change tracking, Auto Save and toolbar follow are paused. Save your work and restart the editor.' -Level Warning
}
function Save-RecoveryCopy {
[OutputType([string])]
param()
try {
if (-not $script:DocReady -or $script:SourceMode) {
return ''
}
$folder = [System.IO.Path]::Combine((Get-EditorDataFolder), 'Recovery')
[void][System.IO.Directory]::CreateDirectory($folder)
$name = 'untitled'
if (-not [string]::IsNullOrEmpty($script:CurrentPath)) {
$name = [System.IO.Path]::GetFileNameWithoutExtension($script:CurrentPath)
}
$file = [System.IO.Path]::Combine($folder, ('{0}-{1}.html' -f $name, [DateTime]::Now.ToString('yyyyMMdd-HHmmss')))
[System.IO.File]::WriteAllText($file, (Get-DocumentHtml), (New-Object System.Text.UTF8Encoding($false)))
Write-EditorLog -Message ('Recovery copy written: {0}' -f $file) -Level Warning
return $file
}
catch {
Write-EditorLog -Message ('Recovery copy failed: {0}' -f $_.Exception.Message) -Level Error
return ''
}
}
function Receive-UnhandledException {
param([Parameter(Mandatory = $true)]$ExceptionArgs)
$ExceptionArgs.Handled = $true
if ($script:InUnhandled) {
return
}
$script:InUnhandled = $true
try {
[void](Register-EditorError -Source 'Dispatcher' -ErrorObject $ExceptionArgs.Exception -StopAction { Stop-EditorTimers })
$stats = $script:SourceStats['Dispatcher']
if ($stats.Tripped -and $stats.WindowCount -gt (10 * $script:FloodLimit) -and -not $script:ForceClose) {
$copy = Save-RecoveryCopy
$script:ForceClose = $true
$text = 'The editor keeps failing and is closing. Recovery copy: {0}. Log: {1}' -f $(if ($copy) { $copy } else { '(none)' }), $script:LogPath
Write-EditorLog -Message $text -Level Error
Write-Warning $text
$script:Window.Close()
}
}
catch {
Write-EditorLog -Message ('Unhandled-exception handler failed: {0}' -f $_.Exception.Message) -Level Error
}
finally {
$script:InUnhandled = $false
}
}
function Write-HealthSample {
if (-not $script:Diagnostic) {
return
}
$now = [DateTime]::UtcNow
if (($now - $script:LastHealthUtc).TotalSeconds -lt 10) {
return
}
$script:LastHealthUtc = $now
$user = '?'
$gdi = '?'
try {
if (-not ('HtmlEditorNative.GuiResources' -as [type])) {
Add-Type -Namespace 'HtmlEditorNative' -Name 'GuiResources' -MemberDefinition '[System.Runtime.InteropServices.DllImport("user32.dll")] public static extern uint GetGuiResources(System.IntPtr hProcess, uint uiFlags);'
}
$handle = [System.Diagnostics.Process]::GetCurrentProcess().Handle
$gdi = [HtmlEditorNative.GuiResources]::GetGuiResources($handle, 0)
$user = [HtmlEditorNative.GuiResources]::GetGuiResources($handle, 1)
}
catch {
Write-EditorLog -Message ('GetGuiResources unavailable: {0}' -f $_.Exception.Message) -Level Trace
}
$process = [System.Diagnostics.Process]::GetCurrentProcess()
$errors = 0
foreach ($entry in $script:ErrorStats.Values) {
$errors += $entry.Count
}
Write-EditorTrace -Message ('Health: USER={0} GDI={1} handles={2} workingSet={3:N0}MB ticks={4}/{5} deferred={6} errors={7} safeMode={8}' -f $user, $gdi, $process.HandleCount, ($process.WorkingSet64 / 1MB), $script:TickCount, $script:SelectionTickCount, $script:DeferredQueue.Count, $errors, $script:SafeMode)
}
function Invoke-NewCommand {
try {
if (-not (Confirm-DiscardChanges)) {
Write-StatusMessage -Message 'New document cancelled.'
return
}
New-HtmlDocument
}
catch {
Write-StatusMessage -Message ('New document failed: {0}' -f $_.Exception.Message) -Level Error
}
}
function Invoke-OpenCommand {
try {
if (-not (Confirm-DiscardChanges)) {
Write-StatusMessage -Message 'Open cancelled.'
return
}
$dialog = New-Object Microsoft.Win32.OpenFileDialog
$dialog.Title = 'Open HTML file'
$dialog.Filter = 'HTML files (*.html;*.htm)|*.html;*.htm|All files (*.*)|*.*'
$dialog.CheckFileExists = $true
$folder = Get-DocumentDirectory
if (-not [string]::IsNullOrEmpty($folder)) {
$dialog.InitialDirectory = $folder
}
$script:ModalDepth++
try {
$picked = $dialog.ShowDialog($script:Window)
}
finally {
$script:ModalDepth--
}
if ($picked -ne $true) {
Write-StatusMessage -Message 'Open cancelled.'
return
}
Open-HtmlDocument -FilePath $dialog.FileName
}
catch {
Write-StatusMessage -Message ('Open failed: {0}' -f $_.Exception.Message) -Level Error
}
}
function Invoke-SaveAsCommand {
[OutputType([bool])]
param()
try {
$dialog = New-Object Microsoft.Win32.SaveFileDialog
$dialog.Title = 'Save HTML file as'
$dialog.Filter = 'HTML files (*.html;*.htm)|*.html;*.htm|All files (*.*)|*.*'
$dialog.DefaultExt = '.html'
$dialog.AddExtension = $true
$dialog.OverwritePrompt = $true
if ([string]::IsNullOrEmpty($script:CurrentPath)) {
$dialog.FileName = 'Untitled.html'
}
else {
$dialog.InitialDirectory = [System.IO.Path]::GetDirectoryName($script:CurrentPath)
$dialog.FileName = [System.IO.Path]::GetFileName($script:CurrentPath)
}
$script:ModalDepth++
try {
$picked = $dialog.ShowDialog($script:Window)
}
finally {
$script:ModalDepth--
}
if ($picked -ne $true) {
Write-StatusMessage -Message 'Save As cancelled.'
return $false
}
return (Save-HtmlDocument -FilePath $dialog.FileName)
}
catch {
Write-StatusMessage -Message ('Save As failed: {0}' -f $_.Exception.Message) -Level Error
return $false
}
}
function Invoke-SaveCommand {
[OutputType([bool])]
param()
try {
if ([string]::IsNullOrEmpty($script:CurrentPath)) {
return (Invoke-SaveAsCommand)
}
return (Save-HtmlDocument -FilePath $script:CurrentPath)
}
catch {
Write-StatusMessage -Message ('Save failed: {0}' -f $_.Exception.Message) -Level Error
return $false
}
}
function Invoke-EditorScript {
[OutputType([string])]
param(
[Parameter(Mandatory = $true)][string]$Name,
[object[]]$Arguments = @()
)
if (-not $script:ScriptReady) {
throw 'The editor script helper is not available in this document (scripting may be disabled).'
}
$doc = $script:Browser.Document
if ($Arguments.Count -gt 0) {
$result = $doc.InvokeScript($Name, $Arguments)
}
else {
$result = $doc.InvokeScript($Name)
}
if ($null -eq $result) {
throw ('Editor script function {0} returned nothing.' -f $Name)
}
return [string]$result
}
function Add-StyleUndoSnapshot {
param(
[AllowEmptyString()][string]$Before,
[AllowEmptyString()][string]$After
)
if ($Before -ceq $After) {
return
}
$script:StyleUndo.Add([pscustomobject]@{ Before = $Before; After = $After })
while ($script:StyleUndo.Count -gt 50) {
$script:StyleUndo.RemoveAt(0)
}
}
function Set-BlockFormat {
[OutputType([string])]
param([Parameter(Mandatory = $true)][string]$Tag)
$before = Get-BodyHtml
try {
$script:Browser.Document.ExecCommand('FormatBlock', $false, ('<{0}>' -f $Tag))
}
catch {
Write-StatusMessage -Message ('Native formatBlock {0} failed ({1}); trying the script fallback.' -f $Tag, $_.Exception.Message) -Level Warning
}
if ((Get-BodyHtml) -cne $before) {
return ('OK:Applied <{0}>.' -f $Tag)
}
if ($script:ScriptReady) {
return (Invoke-EditorScript -Name '__hteReplaceBlock' -Arguments @($Tag))
}
return 'INFO:No change. Place the cursor inside a paragraph or heading.'
}
function Invoke-StyleCommand {
param([Parameter(Mandatory = $true)][string]$StyleName)
try {
if ($script:SourceMode) {
Write-StatusMessage -Message 'Switch back to the WYSIWYG view to apply styles.' -Level Warning
return
}
if (-not $script:DocReady) {
Write-StatusMessage -Message 'The document is still loading.' -Level Warning
return
}
$before = Get-BodyHtml
if ($StyleName -eq $script:ClearStyleName) {
$result = Invoke-EditorScript -Name '__hteClearStyle'
}
elseif ($StyleName.Contains('.')) {
$dot = $StyleName.IndexOf('.')
$tag = $StyleName.Substring(0, $dot)
$className = $StyleName.Substring($dot + 1)
$result = Invoke-EditorScript -Name '__hteApplyClass' -Arguments @($className, $tag)
}
else {
$result = Set-BlockFormat -Tag $StyleName
}
Add-StyleUndoSnapshot -Before $before -After (Get-BodyHtml)
Show-ScriptResult -Result $result
Request-ToolbarRefresh
}
catch {
Write-StatusMessage -Message ('Style {0} failed: {1}' -f $StyleName, $_.Exception.Message) -Level Error
}
}
function Invoke-FormatCommand {
param([Parameter(Mandatory = $true)][ValidateSet('Bold', 'Italic')][string]$Command)
try {
if ($script:SourceMode) {
Write-StatusMessage -Message ('{0} is only available in the WYSIWYG view.' -f $Command) -Level Warning
return
}
if (-not $script:DocReady) {
Write-StatusMessage -Message 'The document is still loading.' -Level Warning
return
}
$script:Browser.Document.ExecCommand($Command, $false, $null)
Write-StatusMessage -Message ('{0} toggled.' -f $Command)
Request-ToolbarRefresh
}
catch {
Write-StatusMessage -Message ('{0} failed: {1}' -f $Command, $_.Exception.Message) -Level Error
}
Set-EditorFocus
}
function Invoke-UndoCommand {
try {
Request-ToolbarRefresh
if ($script:SourceMode) {
[void]$script:SourceBox.Undo()
return
}
if (-not $script:DocReady) {
Write-StatusMessage -Message 'The document is still loading.' -Level Warning
return
}
if ($script:StyleUndo.Count -gt 0) {
$top = $script:StyleUndo[$script:StyleUndo.Count - 1]
if ((Get-BodyHtml) -ceq $top.After) {
$script:Browser.Document.Body.InnerHtml = $top.Before
$script:StyleUndo.RemoveAt($script:StyleUndo.Count - 1)
Write-StatusMessage -Message 'Style change undone.'
Set-EditorFocus
return
}
}
$script:Browser.Document.ExecCommand('Undo', $false, $null)
Write-StatusMessage -Message 'Undo.'
}
catch {
Write-StatusMessage -Message ('Undo failed: {0}' -f $_.Exception.Message) -Level Error
}
Set-EditorFocus
}
function Invoke-InlineStyleCommand {
param(
[Parameter(Mandatory = $true)][ValidateSet('color', 'font-size', 'background-color')][string]$Property,
[AllowEmptyString()][string]$Value
)
try {
if ($script:SourceMode) {
Write-StatusMessage -Message 'Switch back to the WYSIWYG view to change text color or size.' -Level Warning
return
}
if (-not $script:DocReady) {
Write-StatusMessage -Message 'The document is still loading.' -Level Warning
return
}
[void](Invoke-EditorScript -Name '__hteRestoreSelection')
$before = Get-BodyHtml
$result = Invoke-EditorScript -Name '__hteApplyInlineStyle' -Arguments @($Property, $Value)
Add-StyleUndoSnapshot -Before $before -After (Get-BodyHtml)
Show-ScriptResult -Result $result
Request-ToolbarRefresh
}
catch {
Write-StatusMessage -Message ('Text {0} failed: {1}' -f $Property, $_.Exception.Message) -Level Error
}
Set-EditorFocus
}
function Get-ColorBytes {
param([AllowEmptyString()][string]$Hex)
$normal = ConvertTo-HexColor -Color $Hex
if ($normal.Length -ne 7) {
return $null
}
return @(
[Convert]::ToByte($normal.Substring(1, 2), 16),
[Convert]::ToByte($normal.Substring(3, 2), 16),
[Convert]::ToByte($normal.Substring(5, 2), 16)
)
}
function New-SwatchBrush {
param([AllowEmptyString()][string]$Hex)
$rgb = Get-ColorBytes -Hex $Hex
if ($null -eq $rgb) {
return [System.Windows.Media.Brushes]::Transparent
}
$color = [System.Windows.Media.Color]::FromRgb($rgb[0], $rgb[1], $rgb[2])
$brush = New-Object System.Windows.Media.SolidColorBrush($color)
$brush.Freeze()
return $brush
}
function New-ColorItem {
param(
[Parameter(Mandatory = $true)][string]$Label,
[Parameter(Mandatory = $true)][string]$Tag,
[AllowEmptyString()][string]$Hex,
[switch]$NoSwatch
)
$normal = ConvertTo-HexColor -Color $Hex
$swatch = New-Object System.Windows.Shapes.Rectangle
$swatch.Width = 14
$swatch.Height = 14
$swatch.Margin = New-Object System.Windows.Thickness(0, 0, 6, 0)
$swatch.Stroke = [System.Windows.Media.Brushes]::Gray
$swatch.StrokeThickness = 1
$swatch.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
$swatch.Fill = New-SwatchBrush -Hex $normal
if ($NoSwatch) {
$swatch.Visibility = [System.Windows.Visibility]::Hidden
}
$text = New-Object System.Windows.Controls.TextBlock
$text.Text = $Label
$text.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
$panel = New-Object System.Windows.Controls.StackPanel
$panel.Orientation = [System.Windows.Controls.Orientation]::Horizontal
[void]$panel.Children.Add($swatch)
[void]$panel.Children.Add($text)
$item = New-Object System.Windows.Controls.ComboBoxItem
$item.Content = $panel
$item.Tag = $Tag
$item.MinWidth = $script:ColorComboWidth - 4
$item.DataContext = [pscustomobject]@{ Label = $Label; Tag = $Tag; Hex = $normal }
[System.Windows.Controls.TextSearch]::SetText($item, $Label)
if ($normal.Length -gt 0) {
$item.ToolTip = '{0} ({1})' -f $Label, $normal.ToUpperInvariant()
}
return $item
}
function Add-CustomColorItem {
param([AllowEmptyString()][string]$Hex)
$normal = ConvertTo-HexColor -Color $Hex
if ($normal.Length -eq 0) {
return
}
$combo = $script:ColorCombo
if ($null -ne $script:ColorCustomItem) {
if ([string]$script:ColorCustomItem.Tag -eq $normal) {
return
}
$combo.Items.Remove($script:ColorCustomItem)
$script:ColorCustomItem = $null
}
$script:SuppressColorEvent = $true
try {
$script:ColorCustomItem = New-ColorItem -Label ('Custom ' + $normal.ToUpperInvariant()) -Tag $normal -Hex $normal
$combo.Items.Insert($combo.Items.Count - 1, $script:ColorCustomItem)
}
finally {
$script:SuppressColorEvent = $false
}
}
function Set-ColorShown {
param(
[AllowEmptyString()][string]$Label,
[AllowEmptyString()][string]$Hex
)
if ($null -eq $script:ColorShownSwatch -or $null -eq $script:ColorShownText) {
return
}
$normal = ConvertTo-HexColor -Color $Hex
$script:ColorShownSwatch.Fill = New-SwatchBrush -Hex $normal
if ($normal.Length -eq 0) {
$script:ColorShownText.Text = 'Automatic'
$script:ColorShownText.Opacity = 0.6
$script:ColorCombo.ToolTip = 'Text color: automatic (no inline color). More... opens the color dialog.'
}
else {
if ([string]::IsNullOrEmpty($Label)) {
$Label = $normal.ToUpperInvariant()
}
$script:ColorShownText.Text = $Label
$script:ColorShownText.Opacity = 1.0
$script:ColorCombo.ToolTip = 'Text color: {0} ({1}). More... opens the color dialog.' -f $Label, $normal.ToUpperInvariant()
}
$script:ColorShownHex = $normal
}
function Initialize-ColorList {
$swatches = [ordered]@{
'Black' = '#000000'
'Dark gray' = '#404040'
'Gray' = '#808080'
'Red' = '#ff0000'
'Dark red' = '#8b0000'
'Orange' = '#ff8c00'
'Gold' = '#b8860b'
'Green' = '#008000'
'Dark green' = '#006400'
'Teal' = '#008080'
'Blue' = '#0000ff'
'Navy' = '#000080'
'Purple' = '#800080'
'Magenta' = '#c71585'
}
$script:ColorSwatches = $swatches
$script:ColorCustomItem = $null
$script:SuppressColorEvent = $true
try {
$script:ColorCombo.Items.Clear()
[void]$script:ColorCombo.Items.Add((New-ColorItem -Label '(clear color)' -Tag 'clear' -Hex ''))
foreach ($entry in $swatches.GetEnumerator()) {
[void]$script:ColorCombo.Items.Add((New-ColorItem -Label $entry.Key -Tag $entry.Value -Hex $entry.Value))
}
[void]$script:ColorCombo.Items.Add((New-ColorItem -Label 'More...' -Tag 'more' -Hex '' -NoSwatch))
$script:ColorCombo.SelectedIndex = -1
}
finally {
$script:SuppressColorEvent = $false
}
Set-ColorShown -Label '' -Hex ''
}
function Invoke-ColorCommand {
param([Parameter(Mandatory = $true)][string]$Choice)
try {
if ($script:SourceMode) {
Write-StatusMessage -Message 'Switch back to the WYSIWYG view to change text color.' -Level Warning
return
}
$value = $Choice
if ($Choice -eq 'clear') {
$value = ''
}
elseif ($Choice -eq 'more') {
$dialog = New-Object System.Windows.Forms.ColorDialog
try {
$dialog.FullOpen = $true
$dialog.AnyColor = $true
if ($null -ne $script:LastCustomColor) {
$dialog.Color = $script:LastCustomColor
}
$script:ModalDepth++
try {
$dialogResult = $dialog.ShowDialog()
}
finally {
$script:ModalDepth--
}
if ($dialogResult -ne [System.Windows.Forms.DialogResult]::OK) {
Write-StatusMessage -Message 'Color selection cancelled.'
Set-EditorFocus
return
}
$picked = $dialog.Color
$script:LastCustomColor = $picked
$value = '#{0:x2}{1:x2}{2:x2}' -f $picked.R, $picked.G, $picked.B
Add-CustomColorItem -Hex $value
}
finally {
$dialog.Dispose()
}
}
Invoke-InlineStyleCommand -Property 'color' -Value $value
}
catch {
Write-StatusMessage -Message ('Text color failed: {0}' -f $_.Exception.Message) -Level Error
}
}
function Initialize-SizeList {
$script:SuppressSizeEvent = $true
try {
$script:SizeCombo.Items.Clear()
[void]$script:SizeCombo.Items.Add($script:ClearSizeName)
foreach ($size in @(8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 36, 48, 72)) {
[void]$script:SizeCombo.Items.Add([string]$size)
}
$script:SizeCombo.SelectedIndex = -1
}
finally {
$script:SuppressSizeEvent = $false
}
}
function Reset-SizeBox {
$script:SuppressSizeEvent = $true
try {
$script:SizeCombo.SelectedIndex = -1
$script:SizeCombo.Text = ''
$script:SizeShownText = ''
$script:SizeUserPicked = $false
}
finally {
$script:SuppressSizeEvent = $false
}
}
function ConvertTo-PointSize {
[OutputType([string])]
param([AllowEmptyString()][string]$Text)
$value = [regex]::Replace($Text.Trim(), '(?i)\s*pt$', '')
if ($value.Length -eq 0) {
return $null
}
$number = 0.0
$styles = [System.Globalization.NumberStyles]::Float
$parsed = [double]::TryParse($value, $styles, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$number)
if (-not $parsed) {
$parsed = [double]::TryParse($value, $styles, [System.Globalization.CultureInfo]::CurrentCulture, [ref]$number)
}
if (-not $parsed -or [double]::IsNaN($number) -or $number -lt 1 -or $number -gt 400) {
return $null
}
return ([Math]::Round($number, 2).ToString([System.Globalization.CultureInfo]::InvariantCulture) + 'pt')
}
function Invoke-SizeCommand {
param([AllowEmptyString()][string]$Text)
try {
$entry = $Text.Trim()
if ($entry.Length -eq 0) {
return
}
if ($entry -eq $script:ClearSizeName) {
Invoke-InlineStyleCommand -Property 'font-size' -Value ''
return
}
$size = ConvertTo-PointSize -Text $entry
if ($null -eq $size) {
Write-StatusMessage -Message ('Invalid text size "{0}": enter a number of points from 1 to 400.' -f $entry) -Level Warning
return
}
Invoke-InlineStyleCommand -Property 'font-size' -Value $size
}
catch {
Write-StatusMessage -Message ('Text size failed: {0}' -f $_.Exception.Message) -Level Error
}
}
function Invoke-FormatSourceCommand {
try {
if (-not $script:SourceMode) {
Write-StatusMessage -Message 'Format is only available in the HTML source view.' -Level Warning
return
}
$box = $script:SourceBox
$before = $box.Text
$formatted = Format-HtmlSource -Html $before
if ($formatted -ceq $before) {
Write-StatusMessage -Message 'The source is already formatted.'
return
}
$box.BeginChange()
try {
$box.SelectAll()
$box.SelectedText = $formatted
}
finally {
$box.EndChange()
}
$box.Select(0, 0)
$box.ScrollToHome()
[void]$box.Focus()
Update-DirtyIndicator
$lineCount = [regex]::Matches($formatted, '\n').Count
Write-StatusMessage -Message ('Source formatted ({0:N0} lines, 4-space indent). Ctrl+Z in the source box undoes it.' -f $lineCount)
}
catch {
Write-StatusMessage -Message ('Format failed: {0}' -f $_.Exception.Message) -Level Error
}
}
function Invoke-Shortcut {
[OutputType([bool])]
param(
[string]$Key,
[bool]$Shift
)
if (@('S', 'O', 'N') -notcontains $Key) {
return $false
}
$name = $Key
if ($Shift) {
$name += '+Shift'
}
$now = [DateTime]::UtcNow
if ($name -eq $script:LastShortcutName -and ($now - $script:LastShortcutTime).TotalMilliseconds -lt 400) {
return $true
}
$script:LastShortcutName = $name
$script:LastShortcutTime = $now
switch ($Key) {
'S' {
if ($Shift) {
Invoke-Deferred -Action { [void](Invoke-SaveAsCommand) }
}
else {
Invoke-Deferred -Action { [void](Invoke-SaveCommand) }
}
}
'O' { Invoke-Deferred -Action { Invoke-OpenCommand } }
'N' { Invoke-Deferred -Action { Invoke-NewCommand } }
}
return $true
}
function Get-HtmlToken {
[OutputType([object[]])]
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Html)
$pattern = '\G(?:(?<c><!--[\s\S]*?(?:-->|\z))|(?<d><![^>]*>|<\?[^>]*>)|(?<e></[A-Za-z][^>]*>)|(?<s><[A-Za-z][^\s/>]*(?:"[^"]*"|''[^'']*''|[^''">])*>)|(?<t>[^<]+|<))'
$tokenRegex = New-Object System.Text.RegularExpressions.Regex($pattern)
$nameRegex = New-Object System.Text.RegularExpressions.Regex('^</?([A-Za-z][A-Za-z0-9:_-]*)')
$rawTags = @('pre', 'script', 'style', 'textarea')
$tokens = New-Object System.Collections.Generic.List[object]
$pos = 0
$length = $Html.Length
while ($pos -lt $length) {
$match = $tokenRegex.Match($Html, $pos)
if (-not $match.Success -or $match.Length -eq 0) {
$tokens.Add([pscustomobject]@{ Kind = 'Text'; Name = ''; Raw = $Html.Substring($pos); Content = ''; EndRaw = ''; SelfClose = $false })
break
}
$raw = $match.Value
$pos += $match.Length
$kind = 'Text'
$name = ''
if ($match.Groups['c'].Success) {
$kind = 'Comment'
}
elseif ($match.Groups['d'].Success) {
$kind = 'Decl'
}
elseif ($match.Groups['e'].Success) {
$kind = 'End'
$name = $nameRegex.Match($raw).Groups[1].Value.ToLowerInvariant()
}
elseif ($match.Groups['s'].Success) {
$kind = 'Start'
$name = $nameRegex.Match($raw).Groups[1].Value.ToLowerInvariant()
}
$token = [pscustomobject]@{ Kind = $kind; Name = $name; Raw = $raw; Content = ''; EndRaw = ''; SelfClose = $false }
if ($kind -eq 'Start') {
$token.SelfClose = $raw.EndsWith('/>')
if ($rawTags -contains $name -and -not $token.SelfClose) {
$closeRegex = New-Object System.Text.RegularExpressions.Regex(('</' + [regex]::Escape($name) + '\s*>'), [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$close = $closeRegex.Match($Html, $pos)
if ($close.Success) {
$token.Content = $Html.Substring($pos, $close.Index - $pos)
$token.EndRaw = $close.Value
$pos = $close.Index + $close.Length
}
else {
$token.Content = $Html.Substring($pos)
$pos = $length
}
$token.Kind = 'Raw'
}
}
$tokens.Add($token)
}
return $tokens.ToArray()
}
function Format-HtmlSource {
[OutputType([string])]
param(
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Html,
[ValidateRange(1, 16)][int]$IndentSize = 4
)
$blockNames = @('html', 'head', 'body', 'title', 'meta', 'link', 'base', 'noscript', 'div', 'p', 'ul', 'ol', 'li',
'dl', 'dt', 'dd', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', 'caption', 'colgroup', 'col',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'header', 'footer', 'nav', 'article', 'aside', 'main',
'blockquote', 'form', 'fieldset', 'legend', 'figure', 'figcaption', 'hr', 'address', 'details', 'summary',
'menu', 'center', 'dialog', 'hgroup', 'search', 'frameset', 'frame')
$voidNames = @('area', 'base', 'br', 'col', 'embed', 'frame', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr')
$closesParagraph = @('address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'div', 'dl', 'fieldset',
'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr',
'main', 'menu', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul')
$block = [System.Collections.Generic.HashSet[string]]::new([string[]]$blockNames, [System.StringComparer]::OrdinalIgnoreCase)
$void = [System.Collections.Generic.HashSet[string]]::new([string[]]$voidNames, [System.StringComparer]::OrdinalIgnoreCase)
$newline = "`n"
if ($Html.IndexOf("`r`n", [System.StringComparison]::Ordinal) -ge 0) {
$newline = "`r`n"
}
$wsChars = [char[]]@(32, 9, 13, 10, 12)
$unit = ' ' * $IndentSize
$tokens = @(Get-HtmlToken -Html $Html)
$lines = New-Object System.Collections.Generic.List[string]
$stack = New-Object System.Collections.Generic.List[string]
$runParts = New-Object System.Collections.Generic.List[object]
$lineBreak = New-Object System.Text.RegularExpressions.Regex('(\r\n|\n|\r)[ \t]*')
$emit = {
param([string]$Text)
$lines.Add(($unit * $stack.Count) + $Text)
}
$addToRun = {
param([string]$Text, [bool]$Reindent)
$runParts.Add([pscustomobject]@{ Text = $Text; Reindent = $Reindent })
}
$runIsBlank = {
foreach ($part in $runParts) {
if ($part.Text.Trim($wsChars).Length -gt 0) {
return $false
}
}
return $true
}
$flush = {
$indent = $unit * $stack.Count
$builder = New-Object System.Text.StringBuilder
foreach ($part in $runParts) {
if ($part.Reindent) {
[void]$builder.Append($lineBreak.Replace($part.Text, ('$1' + $indent)))
}
else {
[void]$builder.Append($part.Text)
}
}
$runParts.Clear()
$text = $builder.ToString().Trim($wsChars)
if ($text.Length -gt 0) {
& $emit $text
}
}
$popTop = {
$stack.RemoveAt($stack.Count - 1)
}
$closeImplicit = {
param([string]$Name)
if ($stack.Count -eq 0) {
return
}
$top = $stack[$stack.Count - 1]
if ($top -eq 'p' -and $closesParagraph -contains $Name) {
& $popTop
}
elseif ($Name -eq 'li' -and $top -eq 'li') {
& $popTop
}
elseif (($Name -eq 'dt' -or $Name -eq 'dd') -and ($top -eq 'dt' -or $top -eq 'dd')) {
& $popTop
}
elseif (($Name -eq 'td' -or $Name -eq 'th') -and ($top -eq 'td' -or $top -eq 'th')) {
& $popTop
}
elseif (@('tr', 'thead', 'tbody', 'tfoot') -contains $Name) {
$closable = @('td', 'th', 'tr')
if ($Name -ne 'tr') {
$closable = @('td', 'th', 'tr', 'thead', 'tbody', 'tfoot')
}
while ($stack.Count -gt 0 -and $closable -contains $stack[$stack.Count - 1]) {
& $popTop
}
}
}
$isBoundary = {
param($Token)
if ($null -eq $Token) {
return $true
}
if ($Token.Kind -eq 'Start' -or $Token.Kind -eq 'End') {
return $block.Contains($Token.Name)
}
if ($Token.Kind -eq 'Raw') {
return ($Token.Name -ne 'textarea')
}
return $true
}
$nextSignificant = {
param([int]$From)
for ($k = $From; $k -lt $tokens.Count; $k++) {
$candidate = $tokens[$k]
if ($candidate.Kind -eq 'Text' -and $candidate.Raw.Trim($wsChars).Length -eq 0) {
continue
}
return $candidate
}
return $null
}
$findLeafEnd = {
param([int]$Index)
$name = $tokens[$Index].Name
for ($k = $Index + 1; $k -lt $tokens.Count; $k++) {
$candidate = $tokens[$k]
if ($candidate.Kind -eq 'End' -and $candidate.Name -eq $name) {
return $k
}
if ($candidate.Kind -eq 'Decl') {
return -1
}
if (($candidate.Kind -eq 'Start' -or $candidate.Kind -eq 'End') -and $block.Contains($candidate.Name)) {
return -1
}
if ($candidate.Kind -eq 'Raw' -and $candidate.Name -ne 'textarea') {
return -1
}
}
return -1
}
$i = 0
while ($i -lt $tokens.Count) {
$token = $tokens[$i]
$full = $token.Raw + $token.Content + $token.EndRaw
if ($token.Kind -eq 'Text') {
& $addToRun $token.Raw $true
}
elseif ($token.Kind -eq 'Decl') {
& $flush
& $emit $token.Raw
}
elseif ($token.Kind -eq 'Comment' -or ($token.Kind -eq 'Raw' -and ($token.Name -eq 'script' -or $token.Name -eq 'style'))) {
$beforeOk = (& $runIsBlank)
$afterOk = (& $isBoundary (& $nextSignificant ($i + 1)))
if (-not $afterOk -and ($i + 1) -lt $tokens.Count) {
$next = $tokens[$i + 1]
$afterOk = ($next.Kind -eq 'Text' -and $next.Raw.Length -gt 0 -and $wsChars -contains $next.Raw[0])
}
if ($beforeOk -and $afterOk) {
& $flush
if ($token.Kind -eq 'Raw' -and $token.Name -eq 'style' -and $token.EndRaw.Length -gt 0) {
$cssLines = @(Format-CssText -Css $token.Content -BaseIndent ($unit * $stack.Count) -Unit $unit)
if ($cssLines.Count -eq 0) {
& $emit ($token.Raw + $token.EndRaw)
}
else {
& $emit $token.Raw
foreach ($cssLine in $cssLines) {
$lines.Add($cssLine)
}
& $emit $token.EndRaw
}
}
else {
& $emit $full
}
}
else {
& $addToRun $full $false
}
}
elseif ($token.Kind -eq 'Raw' -and $token.Name -eq 'pre') {
& $flush
& $closeImplicit 'pre'
$preText = $null
if ($token.EndRaw.Length -gt 0) {
$preText = Format-PreText -Content $token.Content -Indent ($unit * $stack.Count) -Unit $unit
}
if ($null -eq $preText) {
& $emit $full
}
else {
& $emit ($token.Raw + $preText.First)
foreach ($preLine in $preText.Lines) {
$lines.Add($preLine)
}
& $emit $token.EndRaw
}
}
elseif ($token.Kind -eq 'Start' -and $block.Contains($token.Name)) {
& $flush
& $closeImplicit $token.Name
$isVoid = $void.Contains($token.Name) -or $token.SelfClose
$end = -1
if (-not $isVoid) {
$end = & $findLeafEnd $i
}
if ($end -gt $i) {
$inner = New-Object System.Text.StringBuilder
for ($k = $i + 1; $k -lt $end; $k++) {
$part = $tokens[$k]
[void]$inner.Append($part.Raw).Append($part.Content).Append($part.EndRaw)
}
$innerText = $inner.ToString().Trim($wsChars)
if ($innerText.IndexOfAny([char[]]@(10, 13)) -lt 0) {
& $emit ($token.Raw + $innerText + $tokens[$end].Raw)
$i = $end + 1
continue
}
}
& $emit $token.Raw
if (-not $isVoid) {
$stack.Add($token.Name)
}
}
elseif ($token.Kind -eq 'End' -and $block.Contains($token.Name)) {
& $flush
$open = $stack.LastIndexOf($token.Name)
if ($open -ge 0) {
$stack.RemoveRange($open, $stack.Count - $open)
}
& $emit $token.Raw
}
else {
& $addToRun $full $false
}
$i++
}
& $flush
if ($lines.Count -eq 0) {
return ''
}
$text = $lines -join $newline
if (-not ($text.EndsWith("`n") -or $text.EndsWith("`r"))) {
$text += $newline
}
return $text
}
function Format-PreText {
[OutputType([pscustomobject])]
param(
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content,
[AllowEmptyString()][string]$Indent = '',
[string]$Unit = ' '
)
$ws = [char[]]@(32, 9)
$all = [regex]::Split($Content, '\r\n|\n|\r')
if ($all.Count -lt 2) {
return $null
}
$count = $all.Count
if ($all[$count - 1].Trim($ws).Length -eq 0) {
$count--
}
$minimum = [int]::MaxValue
for ($n = 1; $n -lt $count; $n++) {
$line = $all[$n]
$body = $line.TrimStart($ws)
if ($body.Length -gt 0) {
$minimum = [Math]::Min($minimum, $line.Length - $body.Length)
}
}
$prefix = $Indent + $Unit
$lines = New-Object System.Collections.Generic.List[string]
for ($n = 1; $n -lt $count; $n++) {
$line = $all[$n]
if ($line.Trim($ws).Length -eq 0) {
$lines.Add('')
}
else {
$lines.Add($prefix + $line.Substring($minimum))
}
}
return [pscustomobject]@{ First = $all[0]; Lines = $lines.ToArray() }
}
function Format-CssText {
[OutputType([string[]])]
param(
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Css,
[AllowEmptyString()][string]$BaseIndent = '',
[string]$Unit = ' '
)
$ws = [char[]]@(32, 9, 12)
$all = [regex]::Split($Css, '\r\n|\n|\r')
$first = 0
$last = $all.Count - 1
while ($first -le $last -and $all[$first].Trim($ws).Length -eq 0) {
$first++
}
while ($last -ge $first -and $all[$last].Trim($ws).Length -eq 0) {
$last--
}
$result = New-Object System.Collections.Generic.List[string]
if ($first -gt $last) {
return $result.ToArray()
}
$noQuote = [char]0
$depth = 0
$inComment = $false
$quote = $noQuote
$shift = 0
$unbalanced = $false
for ($n = $first; $n -le $last; $n++) {
$line = $all[$n]
$body = $line.TrimStart($ws)
$lead = $line.Length - $body.Length
if ($quote -ne $noQuote) {
$result.Add($line)
}
elseif ($inComment) {
if ($body.Length -eq 0) {
$result.Add('')
}
elseif ($shift -ge 0) {
$result.Add((' ' * $shift) + $line)
}
else {
$result.Add($line.Substring([Math]::Min(-$shift, $lead)))
}
}
elseif ($body.Length -eq 0) {
$result.Add('')
}
else {
$closers = 0
while ($closers -lt $body.Length -and $body[$closers] -eq '}') {
$closers++
}
$level = [Math]::Max(0, $depth - $closers)
$indent = $BaseIndent + ($Unit * ($level + 1))
$shift = $indent.Length - $lead
$result.Add($indent + $body)
}
$k = 0
while ($k -lt $line.Length) {
$c = $line[$k]
$next = [char]0
if ($k + 1 -lt $line.Length) {
$next = $line[$k + 1]
}
if ($inComment) {
if ($c -eq '*' -and $next -eq '/') {
$inComment = $false
$k++
}
}
elseif ($quote -ne $noQuote) {
if ($c -eq '\') {
$k++
}
elseif ($c -eq $quote) {
$quote = $noQuote
}
}
elseif ($c -eq '/' -and $next -eq '*') {
$inComment = $true
$k++
}
elseif ($c -eq '"' -or $c -eq "'") {
$quote = $c
}
elseif ($c -eq '{') {
$depth++
}
elseif ($c -eq '}') {
if ($depth -gt 0) {
$depth--
}
else {
$unbalanced = $true
}
}
$k++
}
if ($quote -ne $noQuote -and -not $line.EndsWith('\')) {
$quote = $noQuote
}
}
if (-not $unbalanced -and $depth -eq 0 -and -not $inComment) {
return $result.ToArray()
}
$common = $null
for ($n = $first; $n -le $last; $n++) {
$line = $all[$n]
$body = $line.TrimStart($ws)
if ($body.Length -eq 0) {
continue
}
$leadText = $line.Substring(0, $line.Length - $body.Length)
if ($null -eq $common) {
$common = $leadText
}
else {
$same = 0
while ($same -lt $common.Length -and $same -lt $leadText.Length -and $common[$same] -eq $leadText[$same]) {
$same++
}
$common = $common.Substring(0, $same)
}
}
$result.Clear()
for ($n = $first; $n -le $last; $n++) {
$line = $all[$n]
if ($line.Trim($ws).Length -eq 0) {
$result.Add('')
}
else {
$result.Add($BaseIndent + $Unit + $line.Substring($common.Length))
}
}
return $result.ToArray()
}
function ConvertTo-HexColor {
[OutputType([string])]
param([AllowEmptyString()][string]$Color)
$text = $Color.Trim().ToLowerInvariant()
if ($text.Length -eq 0) {
return ''
}
$match = [regex]::Match($text, '^#([0-9a-f]{3}|[0-9a-f]{6})$')
if ($match.Success) {
$hex = $match.Groups[1].Value
if ($hex.Length -eq 3) {
$hex = '{0}{0}{1}{1}{2}{2}' -f $hex[0], $hex[1], $hex[2]
}
return ('#' + $hex)
}
$number = '(\d{1,3}(?:\.\d+)?|\.\d+)(%?)'
$match = [regex]::Match($text, ('^rgba?\(\s*' + $number + '\s*[,\s]\s*' + $number + '\s*[,\s]\s*' + $number + '\s*(?:[,/]\s*[\d.]+%?\s*)?\)$'))
if ($match.Success) {
$channels = New-Object System.Collections.Generic.List[int]
foreach ($group in @(1, 3, 5)) {
$value = [double]::Parse($match.Groups[$group].Value, [System.Globalization.CultureInfo]::InvariantCulture)
if ($match.Groups[$group + 1].Value -eq '%') {
$value = $value * 255 / 100
}
$channels.Add([int][Math]::Max(0, [Math]::Min(255, [Math]::Round($value, [System.MidpointRounding]::AwayFromZero))))
}
return ('#{0:x2}{1:x2}{2:x2}' -f $channels[0], $channels[1], $channels[2])
}
if ($text -match '^[a-z]+$') {
try {
$named = [System.Drawing.Color]::FromName($text)
if ($named.IsKnownColor -and -not $named.IsSystemColor -and $named.A -eq 255) {
return ('#{0:x2}{1:x2}{2:x2}' -f $named.R, $named.G, $named.B)
}
}
catch {
Write-StatusMessage -Message ('Could not read the color name {0}: {1}' -f $text, $_.Exception.Message) -Level Warning
}
}
return ''
}
function ConvertTo-PointText {
[OutputType([string])]
param([AllowEmptyString()][string]$CssSize)
$match = [regex]::Match($CssSize.Trim().ToLowerInvariant(), '^(\d+(?:\.\d+)?|\.\d+)(pt|px)$')
if (-not $match.Success) {
return ''
}
$value = [double]::Parse($match.Groups[1].Value, [System.Globalization.CultureInfo]::InvariantCulture)
if ($match.Groups[2].Value -eq 'px') {
$value = [Math]::Round($value * 0.75 * 2, [System.MidpointRounding]::AwayFromZero) / 2
}
else {
$value = [Math]::Round($value, 2)
}
if ($value -le 0) {
return ''
}
return $value.ToString([System.Globalization.CultureInfo]::InvariantCulture)
}
function ConvertFrom-SelectionInfo {
[OutputType([pscustomobject])]
param(
[AllowEmptyString()][string]$Info,
[AllowEmptyCollection()][string[]]$StyleNames = @(),
[AllowNull()][System.Collections.IDictionary]$Swatches = $null
)
$lines = $Info -split "`n"
if ($lines.Count -eq 0 -or $lines[0] -ne 'OK') {
return $null
}
$classes = New-Object System.Collections.Generic.List[object]
$block = ''
$colorText = ''
$inlineSize = ''
$computedSize = ''
$highlightText = ''
for ($i = 1; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
$bar = $line.IndexOf('|')
if ($bar -lt 1) {
continue
}
$key = $line.Substring(0, $bar)
$rest = $line.Substring($bar + 1)
switch ($key) {
'C' {
$split = $rest.IndexOf('|')
if ($split -gt 0) {
$classes.Add([pscustomobject]@{ Tag = $rest.Substring(0, $split); Name = $rest.Substring($split + 1) })
}
}
'B' { $block = $rest }
'K' { $colorText = $rest }
'S' { $inlineSize = $rest }
'Z' { $computedSize = $rest }
'H' { $highlightText = $rest }
}
}
$findStyle = {
param([string]$Candidate)
foreach ($styleName in $StyleNames) {
if ([string]::Equals($styleName, $Candidate, [System.StringComparison]::OrdinalIgnoreCase)) {
return $styleName
}
}
return ''
}
$style = ''
foreach ($class in $classes) {
$style = & $findStyle ($class.Tag + '.' + $class.Name)
if ($style.Length -eq 0) {
$style = & $findStyle ('.' + $class.Name)
}
if ($style.Length -gt 0) {
break
}
}
if ($style.Length -eq 0 -and $block.Length -gt 0) {
$style = & $findStyle $block
}
$hex = ConvertTo-HexColor -Color $colorText
$colorName = ''
if ($hex.Length -gt 0 -and $null -ne $Swatches) {
foreach ($entry in $Swatches.GetEnumerator()) {
if ([string]$entry.Value -eq $hex) {
$colorName = [string]$entry.Key
break
}
}
}
$size = ConvertTo-PointText -CssSize $inlineSize
$sizeSource = 'inline'
if ($size.Length -eq 0) {
$size = ConvertTo-PointText -CssSize $computedSize
$sizeSource = 'computed'
}
if ($size.Length -eq 0) {
$sizeSource = ''
}
return [pscustomobject]@{
Style = $style
Block = $block
Color = $hex
ColorName = $colorName
Size = $size
SizeSource = $sizeSource
Highlight = (ConvertTo-HexColor -Color $highlightText)
}
}
function Get-SelectionSummary {
[OutputType([string])]
param([Parameter(Mandatory = $true)][pscustomobject]$Info)
$styleText = '(none)'
if ($Info.Style.Length -gt 0) {
$styleText = $Info.Style
}
$colorText = '(none)'
if ($Info.Color.Length -gt 0) {
$colorText = $Info.Color
if ($Info.ColorName.Length -gt 0) {
$colorText = '{0} ({1})' -f $Info.Color, $Info.ColorName
}
}
$sizeText = '(none)'
if ($Info.Size.Length -gt 0) {
$sizeText = $Info.Size + 'pt'
if ($Info.SizeSource -eq 'computed') {
$sizeText += ' (computed)'
}
}
$highlightShown = '(none)'
if ($null -ne $Info.Highlight -and $Info.Highlight.Length -gt 0) {
$highlightShown = $Info.Highlight
}
return ('Style: {0} | Color: {1} | Size: {2} | Highlight: {3}' -f $styleText, $colorText, $sizeText, $highlightShown)
}
function Get-StyleName {
[OutputType([string[]])]
param()
$names = New-Object System.Collections.Generic.List[string]
foreach ($item in $script:StyleCombo.Items) {
$text = [string]$item
if ($text -ne $script:ClearStyleName) {
$names.Add($text)
}
}
return $names.ToArray()
}
function Set-ColorDisplay {
param([AllowEmptyString()][string]$Hex)
$combo = $script:ColorCombo
if ($combo.SelectedIndex -ne -1) {
$combo.SelectedIndex = -1
}
$normal = ConvertTo-HexColor -Color $Hex
if ($normal.Length -eq 0) {
Set-ColorShown -Label '' -Hex ''
return
}
$label = ''
if ($null -ne $script:ColorSwatches) {
foreach ($entry in $script:ColorSwatches.GetEnumerator()) {
if ($entry.Value -eq $normal) {
$label = $entry.Key
break
}
}
}
if ($label.Length -eq 0) {
Add-CustomColorItem -Hex $normal
$label = $normal.ToUpperInvariant()
}
Set-ColorShown -Label $label -Hex $normal
}
function Set-ToolbarSelection {
param([AllowNull()][pscustomobject]$Info)
$script:SuppressComboEvents = $true
try {
$styleIndex = -1
$hex = ''
$size = ''
$highlight = ''
if ($null -ne $Info) {
if ($Info.Style.Length -gt 0) {
$styleIndex = $script:StyleCombo.Items.IndexOf($Info.Style)
}
$hex = $Info.Color
$size = $Info.Size
$highlight = $Info.Highlight
}
Set-HighlightBar -Hex $highlight
$script:StyleCombo.SelectedIndex = $styleIndex
Set-ColorDisplay -Hex $hex
$script:SizeCombo.SelectedIndex = -1
$script:SizeCombo.Text = $size
$script:SizeShownText = $script:SizeCombo.Text
$script:SizeUserPicked = $false
}
finally {
$script:SuppressComboEvents = $false
}
}
function Request-ToolbarRefresh {
$script:ToolbarRefreshPending = $true
}
function Update-SelectionToolbar {
if ($script:SourceMode -or -not $script:DocReady -or -not $script:ScriptReady -or $script:PendingLoad -or $script:ModalDepth -gt 0) {
return
}
Receive-PageEvent
if ($script:HighlightPopup.IsOpen -or $script:StyleCombo.IsDropDownOpen -or $script:ColorCombo.IsDropDownOpen -or $script:SizeCombo.IsDropDownOpen -or $script:SizeCombo.IsKeyboardFocusWithin) {
return
}
$force = $script:ToolbarRefreshPending
$script:ToolbarRefreshPending = $false
$raw = Invoke-EditorScript -Name '__hteSelectionInfo' -Arguments @($force)
if ($raw.Length -eq 0) {
return
}
if ($raw.StartsWith('ERR:')) {
throw $raw.Substring(4)
}
if (-not $force -and $raw -ceq $script:SelectionInfoKey) {
return
}
$script:SelectionInfoKey = $raw
$info = $null
if ($raw -ne 'NONE') {
$info = ConvertFrom-SelectionInfo -Info $raw -StyleNames (Get-StyleName) -Swatches $script:ColorSwatches
}
Set-ToolbarSelection -Info $info
if ($null -ne $info -and -not $force) {
Write-StatusMessage -Message (Get-SelectionSummary -Info $info)
}
}
function Get-AutoSaveSetting {
[OutputType([bool])]
param()
$key = $null
try {
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($script:SettingsKeyPath, $false)
if ($null -eq $key) {
return $false
}
$value = $key.GetValue('AutoSave', 0)
$number = $value -as [int]
return ($null -ne $number -and $number -ne 0)
}
catch {
Write-StatusMessage -Message ('Could not read the Auto Save setting: {0}' -f $_.Exception.Message) -Level Warning
return $false
}
finally {
if ($null -ne $key) {
$key.Close()
}
}
}
function Set-AutoSaveSetting {
param([bool]$Enabled)
$key = $null
try {
$key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($script:SettingsKeyPath)
$key.SetValue('AutoSave', [int]$Enabled, [Microsoft.Win32.RegistryValueKind]::DWord)
}
catch {
Write-StatusMessage -Message ('Could not store the Auto Save setting: {0}' -f $_.Exception.Message) -Level Warning
}
finally {
if ($null -ne $key) {
$key.Close()
}
}
}
function Reset-AutoSaveTimer {
$script:AutoSaveSignature = $null
$script:AutoSaveDueUtc = $null
}
function Write-AutoSaveNote {
param([Parameter(Mandatory = $true)][string]$Note)
if ($script:AutoSaveNote -cne $Note) {
$script:AutoSaveNote = $Note
Write-StatusMessage -Message $Note
}
}
function Set-AutoSaveEnabled {
param([bool]$Enabled)
$script:AutoSave = $Enabled
$script:AutoSaveNote = ''
Reset-AutoSaveTimer
Set-AutoSaveSetting -Enabled $Enabled
if (-not $Enabled) {
Write-StatusMessage -Message 'Auto Save off.'
}
elseif ([string]::IsNullOrEmpty($script:CurrentPath)) {
Write-AutoSaveNote -Note 'Auto Save: save the file once to enable'
}
else {
Write-StatusMessage -Message 'Auto Save on: changes are saved about 2 seconds after the last edit.'
}
}
function Save-AutoSaveDocument {
[OutputType([bool])]
param()
try {
[void](Save-HtmlDocument -FilePath $script:CurrentPath -Quiet)
Reset-AutoSaveTimer
$script:AutoSaveNote = ''
Write-StatusMessage -Message ('Auto-saved {0}' -f [DateTime]::Now.ToLongTimeString())
return $true
}
catch {
$script:AutoSaveDueUtc = [DateTime]::UtcNow.AddSeconds(10)
Write-StatusMessage -Message ('Auto Save failed (still on, retrying in 10 seconds): {0}' -f $_.Exception.Message) -Level Error
return $false
}
}
function Invoke-AutoSave {
if (-not $script:AutoSave -or $script:ModalDepth -gt 0) {
return
}
if ($script:SourceMode) {
Reset-AutoSaveTimer
return
}
if (-not $script:DocReady -or $script:PendingLoad) {
return
}
if (-not (Test-DocumentDirty)) {
Reset-AutoSaveTimer
return
}
if ([string]::IsNullOrEmpty($script:CurrentPath)) {
Write-AutoSaveNote -Note 'Auto Save: save the file once to enable'
return
}
$signature = Get-BodyHtml
$now = [DateTime]::UtcNow
if ($null -eq $script:AutoSaveSignature -or $signature -cne $script:AutoSaveSignature) {
$script:AutoSaveSignature = $signature
$script:AutoSaveDueUtc = $now.AddSeconds(2)
return
}
if ($null -eq $script:AutoSaveDueUtc -or $now -lt $script:AutoSaveDueUtc) {
return
}
[void](Save-AutoSaveDocument)
}
function Save-PendingAutoSave {
[OutputType([bool])]
param()
if (-not $script:AutoSave -or $script:SourceMode -or -not $script:DocReady -or $script:PendingLoad) {
return $false
}
if ([string]::IsNullOrEmpty($script:CurrentPath)) {
return $false
}
return (Save-AutoSaveDocument)
}
function Get-HighlightSetting {
[OutputType([string])]
param()
$key = $null
try {
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($script:SettingsKeyPath, $false)
if ($null -eq $key) {
return $script:DefaultHighlight
}
return (Resolve-HighlightColor -Value $key.GetValue('HighlightColor', $null))
}
catch {
Write-StatusMessage -Message ('Could not read the highlight color setting: {0}' -f $_.Exception.Message) -Level Warning
return $script:DefaultHighlight
}
finally {
if ($null -ne $key) {
$key.Close()
}
}
}
function Resolve-HighlightColor {
[OutputType([string])]
param([AllowNull()][object]$Value)
if ($null -eq $Value) {
return $script:DefaultHighlight
}
$hex = ConvertTo-HexColor -Color ([string]$Value)
if ($hex.Length -eq 0) {
return $script:DefaultHighlight
}
return $hex
}
function Set-HighlightSetting {
param([Parameter(Mandatory = $true)][string]$Color)
$key = $null
try {
$key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($script:SettingsKeyPath)
$key.SetValue('HighlightColor', $Color, [Microsoft.Win32.RegistryValueKind]::String)
}
catch {
Write-StatusMessage -Message ('Could not store the highlight color setting: {0}' -f $_.Exception.Message) -Level Warning
}
finally {
if ($null -ne $key) {
$key.Close()
}
}
}
function New-HexBrush {
param([Parameter(Mandatory = $true)][string]$Hex)
return (New-SwatchBrush -Hex $Hex)
}
function Set-HighlightBar {
param([AllowEmptyString()][string]$Hex)
$shown = $Hex
if ([string]::IsNullOrEmpty($shown)) {
$shown = $script:HighlightColor
}
$script:HighlightBar.Fill = New-HexBrush -Hex $shown
}
function Set-HighlightColor {
param(
[Parameter(Mandatory = $true)][string]$Color,
[switch]$NoSave
)
$hex = ConvertTo-HexColor -Color $Color
if ($hex.Length -eq 0) {
throw ('Not a valid highlight color: {0}' -f $Color)
}
$script:HighlightColor = $hex
$script:HighlightButton.ToolTip = ('Highlight {0} (Ctrl+Alt+H)' -f $hex.ToUpperInvariant())
Set-HighlightBar -Hex $hex
if ($script:ScriptReady -and $script:DocReady) {
[void](Invoke-EditorScript -Name '__hteSetHighlight' -Arguments @($hex))
}
if (-not $NoSave) {
Set-HighlightSetting -Color $hex
}
}
function Initialize-HighlightPalette {
$normalBorder = New-HexBrush -Hex '#E2E4E7'
$hoverBorder = New-HexBrush -Hex '#F29436'
$script:HighlightGrid.Children.Clear()
foreach ($hex in $script:HighlightPalette) {
$swatch = New-Object System.Windows.Controls.Border
$swatch.Width = 24
$swatch.Height = 24
$swatch.Margin = New-Object System.Windows.Thickness(3)
$swatch.BorderThickness = New-Object System.Windows.Thickness(1)
$swatch.BorderBrush = $normalBorder
$swatch.Background = New-HexBrush -Hex $hex
$swatch.ToolTip = $hex.ToUpperInvariant()
$swatch.Tag = $hex.ToLowerInvariant()
$swatch.Cursor = [System.Windows.Input.Cursors]::Hand
$swatch.Focusable = $false
$swatch.Add_MouseEnter({ param($sender, $e) $sender.BorderBrush = $hoverBorder; $sender.BorderThickness = New-Object System.Windows.Thickness(2) }.GetNewClosure())
$swatch.Add_MouseLeave({ param($sender, $e) $sender.BorderBrush = $normalBorder; $sender.BorderThickness = New-Object System.Windows.Thickness(1) }.GetNewClosure())
$swatch.Add_MouseLeftButtonUp({
param($sender, $e)
$e.Handled = $true
Select-HighlightSwatch -Color ([string]$sender.Tag)
})
[void]$script:HighlightGrid.Children.Add($swatch)
}
}
function Open-HighlightPalette {
try {
if (([DateTime]::UtcNow - $script:HighlightPopupClosedUtc).TotalMilliseconds -lt 250) {
return
}
if ($script:SourceMode) {
Write-StatusMessage -Message 'Switch back to the WYSIWYG view to highlight text.' -Level Warning
return
}
if ($script:ScriptReady -and $script:DocReady) {
[void](Invoke-EditorScript -Name '__hteSaveSelection')
}
$script:HighlightPopup.IsOpen = $true
}
catch {
Write-StatusMessage -Message ('Could not open the highlight colors: {0}' -f $_.Exception.Message) -Level Error
}
}
function Close-HighlightPalette {
if ($null -ne $script:HighlightPopup -and $script:HighlightPopup.IsOpen) {
$script:HighlightPopup.IsOpen = $false
}
}
function Select-HighlightSwatch {
param([AllowEmptyString()][string]$Color)
try {
Close-HighlightPalette
if ([string]::IsNullOrEmpty($Color)) {
Invoke-InlineStyleCommand -Property 'background-color' -Value ''
return
}
Set-HighlightColor -Color $Color
Invoke-InlineStyleCommand -Property 'background-color' -Value $script:HighlightColor
}
catch {
Write-StatusMessage -Message ('Highlight failed: {0}' -f $_.Exception.Message) -Level Error
}
}
function Invoke-IndentCommand {
param([Parameter(Mandatory = $true)][ValidateSet('Increase', 'Decrease')][string]$Direction)
try {
if ($script:SourceMode) {
Write-StatusMessage -Message 'Switch back to the WYSIWYG view to change the indent.' -Level Warning
return
}
if (-not $script:DocReady) {
Write-StatusMessage -Message 'The document is still loading.' -Level Warning
return
}
[void](Invoke-EditorScript -Name '__hteRestoreSelection')
$step = 1
if ($Direction -eq 'Decrease') {
$step = -1
}
$before = Get-BodyHtml
$result = Invoke-EditorScript -Name '__hteIndent' -Arguments @($step)
Add-StyleUndoSnapshot -Before $before -After (Get-BodyHtml)
Show-ScriptResult -Result $result
Update-DirtyIndicator
Request-ToolbarRefresh
}
catch {
Write-StatusMessage -Message ('{0} indent failed: {1}' -f $Direction, $_.Exception.Message) -Level Error
}
Set-EditorFocus
}
function Receive-PageEvent {
for ($n = 0; $n -lt 50; $n++) {
$item = [string](Invoke-EditorScript -Name '__hteTakeEvent')
if ($item.Length -eq 0) {
return
}
$kind = $item.Substring(0, 1)
$rest = $item.Substring(1)
if ($kind -eq 'U') {
$split = $rest.IndexOf([char]1)
if ($split -ge 0) {
Add-StyleUndoSnapshot -Before $rest.Substring(0, $split) -After $rest.Substring($split + 1)
}
}
elseif ($kind -eq 'M') {
Show-ScriptResult -Result $rest
}
}
}
function Request-PageKey {
param([Parameter(Mandatory = $true)][ValidateSet('tab', 'shift-tab', 'indent', 'outdent', 'highlight')][string]$Name)
$script:PageKeyQueue.Add($Name)
Invoke-Deferred -Action { Invoke-PageKeyQueue }
}
function Invoke-PageKeyQueue {
while ($script:PageKeyQueue.Count -gt 0) {
$name = $script:PageKeyQueue[0]
$script:PageKeyQueue.RemoveAt(0)
Invoke-PageKey -Name $name
}
}
function Invoke-PageKey {
param([Parameter(Mandatory = $true)][string]$Name)
try {
if ($script:SourceMode) {
if ($Name -eq 'highlight') {
Write-StatusMessage -Message 'Switch back to the WYSIWYG view to highlight text.' -Level Warning
}
return
}
if (-not $script:DocReady -or -not $script:ScriptReady) {
return
}
$result = [string](Invoke-EditorScript -Name '__htePageKey' -Arguments @($Name))
if ($result.StartsWith('ERR:')) {
Show-ScriptResult -Result $result
}
Receive-PageEvent
Update-DirtyIndicator
Request-ToolbarRefresh
}
catch {
Write-StatusMessage -Message ('Key action {0} failed: {1}' -f $Name, $_.Exception.Message) -Level Error
}
}
function Get-WindowXaml {
$lines = @(
"<Window xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'"
" xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'"
" Title='HTML Editor' Width='1250' Height='800' MinWidth='640' MinHeight='400'"
" WindowStartupLocation='CenterScreen'>"
" <DockPanel LastChildFill='True'>"
" <Menu DockPanel.Dock='Top'>"
" <MenuItem Header='_File'>"
" <MenuItem x:Name='mnuNew' Header='_New' InputGestureText='Ctrl+N'/>"
" <MenuItem x:Name='mnuOpen' Header='_Open...' InputGestureText='Ctrl+O'/>"
" <MenuItem x:Name='mnuSave' Header='_Save' InputGestureText='Ctrl+S'/>"
" <MenuItem x:Name='mnuSaveAs' Header='Save _As...' InputGestureText='Ctrl+Shift+S'/>"
" <Separator/>"
" <MenuItem x:Name='mnuAutoSave' Header='A_uto Save' IsCheckable='True' ToolTip='Save automatically about 2 seconds after the last change (the file must have been saved once)'/>"
" <Separator/>"
" <MenuItem x:Name='mnuExit' Header='E_xit' InputGestureText='Alt+F4'/>"
" </MenuItem>"
" </Menu>"
" <ToolBarTray DockPanel.Dock='Top'>"
" <ToolBar ToolBarTray.IsLocked='True'>"
" <TextBlock Text='Style:' VerticalAlignment='Center' Margin='6,0,4,0'/>"
" <ComboBox x:Name='cboStyle' Width='180' Focusable='False' ToolTip='Apply a style defined in the document CSS to the selection'/>"
" <TextBlock Text='Color:' VerticalAlignment='Center' Margin='8,0,4,0'/>"
" <Grid x:Name='gridColor' Width='150' HorizontalAlignment='Left' VerticalAlignment='Center'>"
" <ComboBox x:Name='cboColor' Width='150' HorizontalAlignment='Left' Focusable='False' Style='{DynamicResource {x:Static ToolBar.ComboBoxStyleKey}}' ToolTip='Text color for the selection (More... opens the color dialog)'/>"
" <DockPanel x:Name='panColorShown' Width='126' HorizontalAlignment='Left' VerticalAlignment='Center' Margin='4,0,20,0' IsHitTestVisible='False' LastChildFill='True'>"
" <Rectangle x:Name='rectColorShown' DockPanel.Dock='Left' Width='14' Height='14' Margin='0,0,6,0' Stroke='Gray' StrokeThickness='1' Fill='Transparent'/>"
" <TextBlock x:Name='txtColorShown' Text='Automatic' Opacity='0.6' VerticalAlignment='Center' TextTrimming='CharacterEllipsis'/>"
" </DockPanel>"
" </Grid>"
" <Button x:Name='btnHighlight' Focusable='False' Padding='2,1' Margin='6,0,0,0' ToolTip='Highlight (Ctrl+Alt+H)'>"
" <Grid Width='20' Height='18'>"
" <Path Data='M 6,11 L 13,2.5 L 16.5,5.5 L 9,13.5 Z' Fill='White' Stroke='#5B5B59' StrokeThickness='1' StrokeLineJoin='Round'/>"
" <Path Data='M 6,11 L 9,13.5 L 6.5,14.5 L 4.5,13 Z' Fill='#7D7D7B'/>"
" <Rectangle x:Name='rectHighlight' Height='3.5' VerticalAlignment='Bottom' Fill='#FFFF00' Stroke='#A0A0A0' StrokeThickness='0.5'/>"
" </Grid>"
" </Button>"
" <Button x:Name='btnHighlightMenu' Focusable='False' Padding='2,1' ToolTip='Highlight colors'>"
" <Path Data='M 0,0 L 7,0 L 3.5,4 Z' Width='7' Height='4' Fill='#404040' VerticalAlignment='Center' Margin='1,0'/>"
" </Button>"
" <TextBlock Text='Size:' VerticalAlignment='Center' Margin='8,0,4,0'/>"
" <ComboBox x:Name='cboSize' Width='90' IsEditable='True' IsTextSearchEnabled='False' ToolTip='Text size in points: pick one, or type 1-400 and press Enter'/>"
" <Separator/>"
" <Button x:Name='btnOutdent' Focusable='False' Padding='3,2' ToolTip='Decrease Indent (Shift+Tab, Alt+Shift+Left)'>"
" <Canvas Width='18' Height='14'>"
" <Path Stroke='#404040' StrokeThickness='1' Data='M 1,1.5 L 17,1.5 M 8,5.5 L 17,5.5 M 8,9.5 L 17,9.5 M 1,13.5 L 17,13.5'/>"
" <Path Fill='#2B579A' Data='M 6,4 L 6,11 L 1.5,7.5 Z'/>"
" </Canvas>"
" </Button>"
" <Button x:Name='btnIndent' Focusable='False' Padding='3,2' ToolTip='Increase Indent (Tab, Alt+Shift+Right)'>"
" <Canvas Width='18' Height='14'>"
" <Path Stroke='#404040' StrokeThickness='1' Data='M 1,1.5 L 17,1.5 M 8,5.5 L 17,5.5 M 8,9.5 L 17,9.5 M 1,13.5 L 17,13.5'/>"
" <Path Fill='#2B579A' Data='M 1.5,4 L 1.5,11 L 6,7.5 Z'/>"
" </Canvas>"
" </Button>"
" <Separator/>"
" <Button x:Name='btnBold' Width='28' Focusable='False' ToolTip='Bold (Ctrl+B)'><TextBlock Text='B' FontWeight='Bold'/></Button>"
" <Button x:Name='btnItalic' Width='28' Focusable='False' ToolTip='Italic (Ctrl+I)'><TextBlock Text='I' FontStyle='Italic' FontFamily='Times New Roman' FontSize='14'/></Button>"
" <Button x:Name='btnUndo' Content='Undo' Padding='6,2' Focusable='False' ToolTip='Undo (style changes first, then native undo)'/>"
" <Separator/>"
" <ToggleButton x:Name='tglSource' Content='HTML Source' Padding='6,2' ToolTip='Toggle between the WYSIWYG view and the HTML source'/>"
" <Button x:Name='btnFormat' Content='Format' Padding='6,2' IsEnabled='False' ToolTip='Re-indent the HTML source (source view only)'/>"
" </ToolBar>"
" </ToolBarTray>"
" <StatusBar DockPanel.Dock='Bottom'>"
" <StatusBarItem><TextBlock x:Name='txtDirty' Width='12' FontWeight='Bold' Foreground='Firebrick' ToolTip='Unsaved changes'/></StatusBarItem>"
" <Separator/>"
" <StatusBarItem><TextBlock x:Name='txtPath' Text='(untitled)' MaxWidth='520' TextTrimming='CharacterEllipsis'/></StatusBarItem>"
" <Separator/>"
" <StatusBarItem><TextBlock x:Name='txtMessage' TextTrimming='CharacterEllipsis'/></StatusBarItem>"
" </StatusBar>"
" <Grid x:Name='gridEditor'>"
" <Popup x:Name='popHighlight' Placement='Bottom' StaysOpen='False' Focusable='False' PopupAnimation='None'>"
" <Border Background='White' BorderBrush='#C6C6C6' BorderThickness='1' Padding='6'>"
" <StackPanel>"
" <UniformGrid x:Name='gridHighlight' Columns='5'/>"
" <Button x:Name='btnNoHighlight' Focusable='False' Margin='0,6,0,0' Padding='4,3' Background='Transparent' BorderThickness='0' ToolTip='Remove the highlight from the selection'>"
" <TextBlock><Underline>N</Underline>o Color</TextBlock>"
" </Button>"
" </StackPanel>"
" </Border>"
" </Popup>"
" <TextBox x:Name='txtSource' Visibility='Collapsed' FontFamily='Consolas, Courier New' FontSize='13'"
" AcceptsReturn='True' AcceptsTab='True' TextWrapping='NoWrap'"
" HorizontalScrollBarVisibility='Auto' VerticalScrollBarVisibility='Auto'/>"
" </Grid>"
" </DockPanel>"
"</Window>"
)
return ($lines -join "`n")
}
try {
[xml]$xamlXml = Get-WindowXaml
$reader = New-Object System.Xml.XmlNodeReader $xamlXml
$script:Window = [Windows.Markup.XamlReader]::Load($reader)
}
catch {
Write-Error ('Failed to build the window from XAML: {0}' -f $_.Exception.Message)
exit 1
}
$script:StyleCombo = $script:Window.FindName('cboStyle')
$script:BoldButton = $script:Window.FindName('btnBold')
$script:ItalicButton = $script:Window.FindName('btnItalic')
$script:SourceToggle = $script:Window.FindName('tglSource')
$script:SourceBox = $script:Window.FindName('txtSource')
$script:DirtyText = $script:Window.FindName('txtDirty')
$script:PathText = $script:Window.FindName('txtPath')
$script:EditorGrid = $script:Window.FindName('gridEditor')
$script:MessageText = $script:Window.FindName('txtMessage')
$script:ColorCombo = $script:Window.FindName('cboColor')
$script:ColorShownPanel = $script:Window.FindName('panColorShown')
$script:ColorShownSwatch = $script:Window.FindName('rectColorShown')
$script:ColorShownText = $script:Window.FindName('txtColorShown')
$script:SizeCombo = $script:Window.FindName('cboSize')
$script:FormatButton = $script:Window.FindName('btnFormat')
$script:AutoSaveMenu = $script:Window.FindName('mnuAutoSave')
$script:HighlightButton = $script:Window.FindName('btnHighlight')
$script:HighlightMenuButton = $script:Window.FindName('btnHighlightMenu')
$script:HighlightBar = $script:Window.FindName('rectHighlight')
$script:HighlightPopup = $script:Window.FindName('popHighlight')
$script:HighlightGrid = $script:Window.FindName('gridHighlight')
$script:OutdentButton = $script:Window.FindName('btnOutdent')
$script:IndentButton = $script:Window.FindName('btnIndent')
$script:HighlightPopup.PlacementTarget = $script:HighlightButton
Initialize-HighlightPalette
try {
Set-HighlightColor -Color (Get-HighlightSetting) -NoSave
}
catch {
Write-StatusMessage -Message ('Highlight color setup failed: {0}' -f $_.Exception.Message) -Level Warning
}
Initialize-ColorList
Initialize-SizeList
$script:AutoSave = Get-AutoSaveSetting
$script:AutoSaveMenu.IsChecked = $script:AutoSave
Set-BrowserEmulation
$script:Browser = New-Object System.Windows.Forms.WebBrowser
$script:Browser.ScriptErrorsSuppressed = $true
$script:Browser.IsWebBrowserContextMenuEnabled = $true
$script:Browser.WebBrowserShortcutsEnabled = $true
$script:Browser.AllowWebBrowserDrop = $true
$script:FormsHost = New-Object System.Windows.Forms.Integration.WindowsFormsHost
$script:FormsHost.Child = $script:Browser
[void]$script:EditorGrid.Children.Add($script:FormsHost)
$script:Timer = New-Object System.Windows.Threading.DispatcherTimer
$script:Timer.Interval = [TimeSpan]::FromMilliseconds(500)
$script:SelectionTimer = New-Object System.Windows.Threading.DispatcherTimer
$script:SelectionTimer.Interval = [TimeSpan]::FromMilliseconds(300)
$script:Browser.Add_DocumentCompleted({
param($sender, $e)
try {
if (-not $script:PendingLoad) {
return
}
if ($null -eq $e.Url -or $null -eq $script:Browser.Url) {
return
}
if ($e.Url.AbsoluteUri -ne $script:Browser.Url.AbsoluteUri) {
return
}
$expectedName = [System.IO.Path]::GetFileName($script:ExpectedTempFile)
if (-not $e.Url.IsFile -or [System.IO.Path]::GetFileName($e.Url.LocalPath) -ne $expectedName) {
$script:PendingLoad = $false
$script:AllowNavigation = $false
Write-StatusMessage -Message ('The editor failed to load the document (landed on {0}).' -f $e.Url.AbsoluteUri) -Level Error
return
}
Write-EditorTrace -Message ('DocumentCompleted: {0}' -f $e.Url.AbsoluteUri)
Initialize-EditableDocument
}
catch {
$script:PendingLoad = $false
$script:AllowNavigation = $false
Write-StatusMessage -Message ('Document initialization failed: {0}' -f $_.Exception.Message) -Level Error
[void](Register-EditorError -Source 'DocumentCompleted' -ErrorObject $_)
}
})
$script:Browser.Add_Navigating({
param($sender, $e)
try {
Write-EditorTrace -Message ('Navigating: {0} (allowed {1})' -f $e.Url, $script:AllowNavigation)
if ($script:AllowNavigation) {
return
}
$e.Cancel = $true
$target = '(unknown)'
if ($null -ne $e.Url) {
$target = $e.Url.ToString()
}
Write-StatusMessage -Message ('Navigation blocked in the editor: {0}' -f $target) -Level Warning
}
catch {
[void](Register-EditorError -Source 'Navigating' -ErrorObject $_)
}
})
$script:Browser.Add_NewWindow({
param($sender, $e)
try {
$e.Cancel = $true
Write-StatusMessage -Message 'A new browser window was blocked.' -Level Warning
}
catch {
[void](Register-EditorError -Source 'NewWindow' -ErrorObject $_)
}
})
$script:Browser.Add_PreviewKeyDown({
param($sender, $e)
try {
$keys = [System.Windows.Forms.Keys]
Write-EditorTrace -Message ('Browser key: {0} ctrl={1} alt={2} shift={3}' -f $e.KeyCode, $e.Control, $e.Alt, $e.Shift)
if ($e.KeyCode -eq $keys::Escape -and $script:HighlightPopup.IsOpen) {
Close-HighlightPalette
}
elseif ($e.KeyCode -eq $keys::Tab -and -not $e.Control -and -not $e.Alt) {
$e.IsInputKey = $true
if ($e.Shift) {
Request-PageKey -Name 'shift-tab'
}
else {
Request-PageKey -Name 'tab'
}
}
elseif ($e.Alt -and $e.Shift -and -not $e.Control -and ($e.KeyCode -eq $keys::Left -or $e.KeyCode -eq $keys::Right)) {
if ($e.KeyCode -eq $keys::Right) {
Request-PageKey -Name 'indent'
}
else {
Request-PageKey -Name 'outdent'
}
}
elseif ($e.Control -and $e.Alt -and -not $e.Shift -and $e.KeyCode -eq $keys::H) {
Request-PageKey -Name 'highlight'
}
elseif ($e.Control -and -not $e.Alt) {
[void](Invoke-Shortcut -Key $e.KeyCode.ToString() -Shift $e.Shift)
}
}
catch {
[void](Register-EditorError -Source 'BrowserKey' -ErrorObject $_)
}
})
$script:Window.Add_PreviewKeyDown({
param($sender, $e)
try {
$mods = [System.Windows.Input.Keyboard]::Modifiers
$ctrl = ([int]($mods -band [System.Windows.Input.ModifierKeys]::Control)) -ne 0
$alt = ([int]($mods -band [System.Windows.Input.ModifierKeys]::Alt)) -ne 0
$shift = ([int]($mods -band [System.Windows.Input.ModifierKeys]::Shift)) -ne 0
$inputKey = [System.Windows.Input.Key]
$key = $e.Key
if ($key -eq $inputKey::System) {
$key = $e.SystemKey
}
$inPage = (-not $script:SourceMode) -and $script:FormsHost.IsKeyboardFocusWithin
if ($key -ne $inputKey::LeftCtrl -and $key -ne $inputKey::RightCtrl -and $key -ne $inputKey::LeftShift -and $key -ne $inputKey::RightShift) {
Write-EditorTrace -Message ('Window key: {0} ctrl={1} alt={2} shift={3} inPage={4}' -f $key, $ctrl, $alt, $shift, $inPage)
}
if ($key -eq $inputKey::Escape -and $script:HighlightPopup.IsOpen) {
Close-HighlightPalette
$e.Handled = $true
}
elseif ($key -eq $inputKey::Tab -and -not $ctrl -and -not $alt -and $inPage) {
if ($shift) {
Request-PageKey -Name 'shift-tab'
}
else {
Request-PageKey -Name 'tab'
}
$e.Handled = $true
}
elseif ($alt -and $shift -and -not $ctrl -and -not $script:SourceMode -and ($key -eq $inputKey::Left -or $key -eq $inputKey::Right)) {
if ($key -eq $inputKey::Right) {
Request-PageKey -Name 'indent'
}
else {
Request-PageKey -Name 'outdent'
}
$e.Handled = $true
}
elseif ($ctrl -and $alt -and -not $shift -and $key -eq $inputKey::H) {
Request-PageKey -Name 'highlight'
$e.Handled = $true
}
elseif ($ctrl -and -not $alt) {
if (Invoke-Shortcut -Key $key.ToString() -Shift $shift) {
$e.Handled = $true
}
}
}
catch {
[void](Register-EditorError -Source 'WindowKey' -ErrorObject $_)
}
})
$script:BoldButton.Add_Click({ Invoke-FormatCommand -Command 'Bold' })
$script:ItalicButton.Add_Click({ Invoke-FormatCommand -Command 'Italic' })
$script:Window.FindName('btnUndo').Add_Click({ Invoke-UndoCommand })
$script:HighlightButton.Add_Click({
try {
Close-HighlightPalette
Invoke-InlineStyleCommand -Property 'background-color' -Value $script:HighlightColor
}
catch {
Write-StatusMessage -Message ('Highlight failed: {0}' -f $_.Exception.Message) -Level Error
}
})
$script:HighlightMenuButton.Add_Click({ Open-HighlightPalette })
$script:Window.FindName('btnNoHighlight').Add_Click({ Select-HighlightSwatch -Color '' })
$script:HighlightPopup.Add_Closed({ $script:HighlightPopupClosedUtc = [DateTime]::UtcNow })
$script:OutdentButton.Add_Click({ Invoke-IndentCommand -Direction 'Decrease' })
$script:IndentButton.Add_Click({ Invoke-IndentCommand -Direction 'Increase' })
$script:Window.Add_Deactivated({ Close-HighlightPalette })
$script:Window.FindName('mnuNew').Add_Click({ Invoke-Deferred -Action { Invoke-NewCommand } })
$script:Window.FindName('mnuOpen').Add_Click({ Invoke-Deferred -Action { Invoke-OpenCommand } })
$script:Window.FindName('mnuSave').Add_Click({ Invoke-Deferred -Action { [void](Invoke-SaveCommand) } })
$script:Window.FindName('mnuSaveAs').Add_Click({ Invoke-Deferred -Action { [void](Invoke-SaveAsCommand) } })
$script:Window.FindName('mnuExit').Add_Click({
Invoke-Deferred -Action {
try {
$script:Window.Close()
}
catch {
Write-StatusMessage -Message ('Exit failed: {0}' -f $_.Exception.Message) -Level Error
}
}
})
$script:AutoSaveMenu.Add_Click({
try {
Set-AutoSaveEnabled -Enabled ([bool]$script:AutoSaveMenu.IsChecked)
}
catch {
Write-StatusMessage -Message ('Auto Save toggle failed: {0}' -f $_.Exception.Message) -Level Error
}
})
$script:StyleCombo.Add_SelectionChanged({
param($sender, $e)
if ($script:SuppressStyleEvent -or $script:SuppressComboEvents) {
return
}
$selected = $script:StyleCombo.SelectedItem
if ($null -eq $selected) {
return
}
Invoke-StyleCommand -StyleName ([string]$selected)
Invoke-Deferred -Action {
$script:SuppressStyleEvent = $true
try {
$script:StyleCombo.SelectedIndex = -1
}
catch {
Write-StatusMessage -Message ('Could not reset the style list: {0}' -f $_.Exception.Message) -Level Warning
}
finally {
$script:SuppressStyleEvent = $false
}
Set-EditorFocus
}
})
$script:ColorCombo.Add_SelectionChanged({
param($sender, $e)
if ($script:SuppressColorEvent -or $script:SuppressComboEvents) {
return
}
$selected = $script:ColorCombo.SelectedItem
if ($null -eq $selected) {
return
}
$script:PendingColorChoice = [string]$selected.Tag
Invoke-Deferred -Action {
$script:SuppressColorEvent = $true
try {
$script:ColorCombo.SelectedIndex = -1
}
catch {
Write-StatusMessage -Message ('Could not reset the color list: {0}' -f $_.Exception.Message) -Level Warning
}
finally {
$script:SuppressColorEvent = $false
}
Invoke-ColorCommand -Choice $script:PendingColorChoice
}
})
$script:SizeCombo.Add_SelectionChanged({
param($sender, $e)
if (-not $script:SuppressSizeEvent -and -not $script:SuppressComboEvents) {
$script:SizeUserPicked = $true
}
})
$script:SizeCombo.Add_DropDownClosed({
try {
if ($script:SuppressSizeEvent -or $script:SuppressComboEvents -or -not $script:SizeUserPicked) {
return
}
$script:SizeUserPicked = $false
$selected = $script:SizeCombo.SelectedItem
if ($null -eq $selected) {
return
}
$entry = [string]$selected
Reset-SizeBox
Invoke-SizeCommand -Text $entry
}
catch {
Write-StatusMessage -Message ('Size selection failed: {0}' -f $_.Exception.Message) -Level Error
}
})
$script:SizeCombo.Add_PreviewKeyDown({
param($sender, $e)
try {
if ($e.Key -eq [System.Windows.Input.Key]::Enter) {
$e.Handled = $true
$entry = $script:SizeCombo.Text
Reset-SizeBox
if ([string]::IsNullOrWhiteSpace($entry)) {
Set-EditorFocus
}
else {
Invoke-SizeCommand -Text $entry
}
}
elseif ($e.Key -eq [System.Windows.Input.Key]::Escape) {
$e.Handled = $true
Reset-SizeBox
Set-EditorFocus
}
}
catch {
Write-StatusMessage -Message ('Size entry failed: {0}' -f $_.Exception.Message) -Level Error
}
})
$script:SizeCombo.Add_LostKeyboardFocus({
param($sender, $e)
try {
if ($script:SuppressSizeEvent -or $script:SizeCombo.IsKeyboardFocusWithin) {
return
}
$entry = $script:SizeCombo.Text
if ([string]::IsNullOrWhiteSpace($entry) -or $entry -ceq $script:SizeShownText) {
return
}
Reset-SizeBox
Invoke-SizeCommand -Text $entry
}
catch {
Write-StatusMessage -Message ('Size entry failed: {0}' -f $_.Exception.Message) -Level Error
}
})
$script:FormatButton.Add_Click({ Invoke-FormatSourceCommand })
$script:SourceToggle.Add_Checked({
if ($script:SuppressToggle) {
return
}
try {
Show-SourceView
}
catch {
Write-StatusMessage -Message ('Cannot show the HTML source: {0}' -f $_.Exception.Message) -Level Error
$script:SuppressToggle = $true
try {
$script:SourceToggle.IsChecked = $false
}
finally {
$script:SuppressToggle = $false
}
}
})
$script:SourceToggle.Add_Unchecked({
if ($script:SuppressToggle) {
return
}
try {
Show-DesignView
}
catch {
Write-StatusMessage -Message ('Could not apply the HTML source: {0}' -f $_.Exception.Message) -Level Error
}
})
$script:Timer.Add_Tick({
if ($script:TickBusy) {
return
}
$script:TickBusy = $true
try {
$script:TickCount++
if ($script:TickCount -eq 1) {
Write-EditorTrace -Message 'Change timer: first tick.'
}
Update-DirtyIndicator
Invoke-AutoSave
Write-HealthSample
}
catch {
[void](Register-EditorError -Source 'ChangeTimer' -ErrorObject $_ -StopAction { $script:Timer.Stop() })
}
finally {
$script:TickBusy = $false
}
})
$script:SelectionTimer.Add_Tick({
if ($script:SelectionTickBusy) {
return
}
$script:SelectionTickBusy = $true
try {
$script:SelectionTickCount++
if ($script:SelectionTickCount -eq 1) {
Write-EditorTrace -Message 'Selection timer: first tick.'
}
Update-SelectionToolbar
}
catch {
[void](Register-EditorError -Source 'SelectionTimer' -ErrorObject $_ -StopAction { $script:SelectionTimer.Stop() })
}
finally {
$script:SelectionTickBusy = $false
}
})
$script:UnhandledHandler = [System.Windows.Threading.DispatcherUnhandledExceptionEventHandler]{
param($sender, $e)
Receive-UnhandledException -ExceptionArgs $e
}
$script:Window.Dispatcher.add_UnhandledException($script:UnhandledHandler)
$script:Window.Add_ContentRendered({
if ($script:Started) {
return
}
$script:Started = $true
Write-EditorTrace -Message 'ContentRendered: starting timers and loading the document.'
foreach ($pending in $script:StartupMessages) {
Set-StatusText -Message $pending.Message -Level $pending.Level
}
$script:StartupMessages.Clear()
try {
$script:Timer.Start()
$script:SelectionTimer.Start()
if ([string]::IsNullOrWhiteSpace($script:StartPath)) {
New-HtmlDocument
}
else {
Open-HtmlDocument -FilePath $script:StartPath
}
}
catch {
Write-StatusMessage -Message ('Startup load failed: {0}' -f $_.Exception.Message) -Level Error
try {
New-HtmlDocument
}
catch {
Write-StatusMessage -Message ('Could not create a blank document: {0}' -f $_.Exception.Message) -Level Error
}
}
})
$script:Window.Add_Closing({
param($sender, $e)
Write-EditorTrace -Message ('Closing (forced {0}).' -f $script:ForceClose)
if ($script:ForceClose) {
return
}
try {
if (-not (Confirm-DiscardChanges)) {
$e.Cancel = $true
}
}
catch {
$closeError = $_.Exception.Message
Write-StatusMessage -Message ('Close check failed: {0}' -f $closeError) -Level Error
$script:ModalDepth++
try {
$answer = [System.Windows.MessageBox]::Show(
$script:Window,
('Could not check for unsaved changes ({0}). Close anyway?' -f $closeError),
'HTML Editor',
[System.Windows.MessageBoxButton]::YesNo,
[System.Windows.MessageBoxImage]::Error)
}
finally {
$script:ModalDepth--
}
if ($answer -ne [System.Windows.MessageBoxResult]::Yes) {
$e.Cancel = $true
}
}
})
$script:Window.Add_Closed({
Write-EditorTrace -Message 'Closed: stopping timers and disposing the browser.'
try {
$script:Timer.Stop()
$script:SelectionTimer.Stop()
$script:DeferredQueue.Clear()
$script:PageKeyQueue.Clear()
if ($null -ne $script:Browser) {
$script:Browser.Dispose()
}
}
catch {
Write-StatusMessage -Message ('Cleanup failed: {0}' -f $_.Exception.Message) -Level Warning
}
Remove-TempFolder
})
$runCount = [AppDomain]::CurrentDomain.GetData($script:RunMarkerName)
if ($null -eq $runCount) {
$runCount = 0
}
[AppDomain]::CurrentDomain.SetData($script:RunMarkerName, ([int]$runCount + 1))
Write-EditorTrace -Message 'ShowDialog.'
try {
[void]$script:Window.ShowDialog()
}
catch {
$fatal = Get-ErrorDetail -ErrorObject $_
Write-EditorLog -Message ('The editor window failed: ' + $fatal.Full) -Level Error
Write-Warning ('The editor window failed: {0} Details: {1}' -f $fatal.Summary, $script:LogPath)
}
finally {
try {
$script:Timer.Stop()
$script:SelectionTimer.Stop()
$script:Window.Dispatcher.remove_UnhandledException($script:UnhandledHandler)
}
catch {
Write-EditorLog -Message ('Final cleanup failed: {0}' -f $_.Exception.Message) -Level Warning
}
Write-ErrorSummary
Write-EditorLog -Message 'HtmlEditor closed.'
}
|