# subflow: Test (global) # Excel to HTML - the quick try, AUTONOMOUS: any workbook range becomes an HTML table that keeps the look of Excel # (fills, fonts, borders, merged cells, column widths, number formats, conditional formatting, hyperlinks). The table opens in the # browser; with bool_SendToOutlook = True it also lands in an Outlook draft (classic Outlook only). # NO input / output variable: the parameters are plain SET lines in the first region, so this subflow can be pasted alone # into any empty Main. Or run it here with 'Run from here' on its first action. # The reusable building block is the local function Convert_Excel_To_Html (same script), called by Main. # PAD has no action that reads the formatting of a cell: ONE 'Run PowerShell script' action asks Excel itself # (COM, DisplayFormat) and returns a table with inline CSS, the only styling that every Outlook client keeps. # Everything else is native: guards, HTML file, browser or Outlook, final message. Regions: cyan values, red guards, purple processing, gold output. @@regionColor: '#57FFE1' **REGION 1. Variables: parameters, the only lines to change # The workbook to send: any .xlsx / .xlsm, opened read-only, never modified SET txt_WorkbookPath TO $'''C:\\Reports\\Sales\\Sales_Report_2026-Q3.xlsx''' # Worksheet name; empty = the sheet that was active when the workbook was saved SET txt_SheetName TO $'''''' # Range to export in Excel notation (A1:H18); empty = the used range of the sheet SET txt_RangeAddress TO $'''''' # Folder that receives the HTML file (it opens in your browser at the end) SET txt_OutputFolder TO $'''C:\\Reports\\Sales\\Email''' # False = only the HTML file, opened in the browser (needs Excel only); True = also an email through the classic Outlook SET bool_SendToOutlook TO False # Used when bool_SendToOutlook is True: Outlook account that sends (as listed in Outlook), recipients, subject, text above the table (HTML allowed) SET txt_OutlookAccount TO $'''your.name@example.com''' SET txt_EmailTo TO $'''sales-managers@example.com''' SET txt_EmailSubject TO $'''Sales report Q3 2026 - straight from Excel''' SET txt_EmailIntro TO $'''Hello,

Please find below the sales report of the quarter, exactly as it looks in the workbook.''' # True = the email is saved in the Drafts folder and opened on screen (demo: nothing leaves the mailbox); False = it is sent SET bool_DraftOnly TO True **ENDREGION @@regionColor: '#F4B6B6' **REGION 2. Preconditions: a missing workbook is said clearly, a missing output folder is created IF (File.IfFile.DoesNotExist File: txt_WorkbookPath) THEN Display.ShowMessageDialog.ShowMessage Title: $'''Excel to Outlook''' Message: $'''Workbook not found: %txt_WorkbookPath%. Fix txt_WorkbookPath in the first region of this subflow, then run again.''' Icon: Display.Icon.ErrorIcon Buttons: Display.Buttons.OK DefaultButton: Display.DefaultButton.Button1 IsTopMost: True ButtonPressed=> _txt_ButtonPressed EXIT Code: 0 END IF (Folder.IfFolderExists.DoesNotExist Path: txt_OutputFolder) THEN # Create folder needs an existing parent: split the path into parent + name first File.GetPathPart File: txt_OutputFolder Directory=> fold_OutputParent FileName=> txt_OutputFolderName Folder.Create FolderPath: fold_OutputParent FolderName: txt_OutputFolderName Folder=> _fold_Output END **ENDREGION @@regionColor: '#D9C3E9' **REGION 3. Convert: Excel renders the range, the script writes it as an HTML table with inline styles # The same script as in Convert_Excel_To_Html, without the details line; its parameters come from the variables above Scripting.RunPowershellScript.RunScript Script: $'''# Excel to HTML - turns an Excel range into an HTML table that keeps the look of Excel and survives Outlook. # How: Excel itself publishes the range as HTML (PublishObjects, its \'Save as web page\' engine: one call, a few # hundred milliseconds whatever the size), then this script rewrites that HTML for email: the CSS classes of the # \') if ($mStyle.Success) { $styleBlock = $mStyle.Groups[1].Value.Replace(\'\', \' \') foreach ($m in [regex]::Matches($styleBlock, \'(?s)([^{}]+)\\{([^{}]*)\\}\')) { $decl = Split-Declarations $m.Groups[2].Value foreach ($sel in ($m.Groups[1].Value -split \',\')) { $s = $sel.Trim() if ($s -eq \'\') { continue } if (-not $rules.ContainsKey($s)) { $rules[$s] = New-Object System.Collections.Specialized.OrderedDictionary } Merge-Into $rules[$s] $decl } } } # 3. The table alone, without the Office conditional comments $mTable = [regex]::Match($html, \'(?is)]*>.*?\') if (-not $mTable.Success) { throw \'Excel produced no table for this range\' } $table = $mTable.Value $table = [regex]::Replace($table, \'(?is).*?\', \'\') $table = [regex]::Replace($table, \'(?s)\', \'\') $table = [regex]::Replace($table, \'(?s)\', \'\') # 4. Columns: width and hidden state (Excel writes a hidden column as width=0 / display:none) $colHidden = New-Object System.Collections.ArrayList $colWidth = New-Object System.Collections.ArrayList foreach ($m in [regex]::Matches($table, \'(?i)]*)>\')) { $a = Parse-Attributes $m.Groups[1].Value $span = 1 if ($a.ContainsKey(\'span\')) { $span = [int]$a[\'span\'] } $w = 0 if ($a.ContainsKey(\'width\')) { $w = [int]$a[\'width\'] } $hidden = ($w -le 0) -or ($a.ContainsKey(\'style\') -and $a[\'style\'] -match \'display\\s*:\\s*none\') for ($i = 0; $i -lt $span; $i++) { [void]$colHidden.Add($hidden); [void]$colWidth.Add($w) } } $totalWidth = 0 $colVisible = 0 for ($i = 0; $i -lt $colWidth.Count; $i++) { if (-not $colHidden[$i]) { $totalWidth += $colWidth[$i]; $colVisible++ } } $rowCount = 0 # 5. Rows and cells: classes inlined, hidden rows and columns skipped, spans kept $out = New-Object System.Text.StringBuilder [void]$out.Append(\'\') for ($i = 0; $i -lt $colWidth.Count; $i++) { if (-not $colHidden[$i]) { [void]$out.Append(\'\') } } [void]$out.AppendLine() $rowspanLeft = New-Object int[] 1024 foreach ($mr in [regex]::Matches($table, \'(?is)]*)>(.*?)\')) { $ra = Parse-Attributes $mr.Groups[1].Value $rowHidden = ($ra.ContainsKey(\'style\') -and $ra[\'style\'] -match \'display\\s*:\\s*none\') $rowStyle = \'\' if ($ra.ContainsKey(\'style\')) { $rd = Split-Declarations $ra[\'style\'] if ($rd.Contains(\'height\')) { $rowStyle = \'height:\' + $rd[\'height\'] + \';\' } } $rowAttrs = \'\' if ($ra.ContainsKey(\'height\')) { $rowAttrs = \' height=\"\' + $ra[\'height\'] + \'\"\' } $pos = 0 $cells = New-Object System.Text.StringBuilder foreach ($mc in [regex]::Matches($mr.Groups[2].Value, \'(?is)]*)>(.*?)\')) { while ($pos -lt $rowspanLeft.Length -and $rowspanLeft[$pos] -gt 0) { $pos++ } $ca = Parse-Attributes $mc.Groups[1].Value $colspan = 1 if ($ca.ContainsKey(\'colspan\')) { $colspan = [int]$ca[\'colspan\'] } $rowspan = 1 if ($ca.ContainsKey(\'rowspan\')) { $rowspan = [int]$ca[\'rowspan\'] } $visible = 0 $wsum = 0 for ($i = $pos; $i -lt ($pos + $colspan); $i++) { if ($i -lt $colHidden.Count) { if (-not $colHidden[$i]) { $visible++; $wsum += $colWidth[$i] } } else { $visible++ } } if ($rowspan -gt 1) { for ($i = $pos; $i -lt ($pos + $colspan) -and $i -lt $rowspanLeft.Length; $i++) { $rowspanLeft[$i] = $rowspan } } $pos += $colspan if ($visible -eq 0 -or $rowHidden) { continue } $attrText = \'\' if ($visible -gt 1) { $attrText += \' colspan=\"\' + $visible + \'\"\' } if ($rowspan -gt 1) { $attrText += \' rowspan=\"\' + $rowspan + \'\"\' } if ($wsum -gt 0) { $attrText += \' width=\"\' + $wsum + \'\"\' } if ($ca.ContainsKey(\'height\')) { $attrText += \' height=\"\' + $ca[\'height\'] + \'\"\' } if ($ca.ContainsKey(\'align\')) { $attrText += \' align=\"\' + $ca[\'align\'] + \'\"\' } if ($ca.ContainsKey(\'valign\')) { $attrText += \' valign=\"\' + $ca[\'valign\'] + \'\"\' } $style = Inline-Style $rules $ca \'td\' $content = Inline-Content $rules $mc.Groups[2].Value if ($content -eq \'\') { $content = \' \' } [void]$cells.Append(\'\' + $content + \'\') } for ($i = 0; $i -lt $rowspanLeft.Length; $i++) { if ($rowspanLeft[$i] -gt 0) { $rowspanLeft[$i]-- } } if ($rowHidden) { continue } [void]$out.Append(\'\' + $cells.ToString() + \'\') [void]$out.AppendLine() $rowCount++ } [void]$out.Append(\'
\') $result = Encode-NonAscii $out.ToString() if ($OutputPath -ne \'\') { [System.IO.File]::WriteAllText($OutputPath, $result, [System.Text.Encoding]::ASCII) } if ($DetailsLine -ne \'\') { $tab = [string][char]9 Write-Output (\'excel-to-html\' + $tab + $sheetUsed + $tab + $address + $tab + $rowCount + $tab + $colVisible + $tab + $clock.ElapsedMilliseconds) } Write-Output $result } catch { [Console]::Error.WriteLine(\'excel-to-html : \' + $_.Exception.Message) } finally { if ($wb -ne $null) { $wb.Close($false) } if ($excel -ne $null) { $excel.Quit() [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) } [GC]::Collect() [GC]::WaitForPendingFinalizers() if ($tmp -ne \'\' -and (Test-Path -LiteralPath $tmp)) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } if ($tmp -ne \'\') { $stem = [System.IO.Path]::GetFileNameWithoutExtension($tmp) Get-ChildItem -LiteralPath $env:TEMP -Directory -Filter ($stem + \'_*\') -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } }''' ScriptOutput=> txt_HtmlTable ScriptError=> txt_ScriptError # The output ends with a line break that would land in the email Text.Trim Text: txt_HtmlTable TrimOption: Text.TrimOption.Both TrimmedText=> txt_HtmlTable **ENDREGION @@regionColor: '#F4B6B6' **REGION 4. Guard: the action never fails by itself, a script error only fills ScriptError IF txt_ScriptError <> $'''''' THEN Display.ShowMessageDialog.ShowMessage Title: $'''Excel to Outlook''' Message: $'''The conversion failed: %txt_ScriptError%''' Icon: Display.Icon.ErrorIcon Buttons: Display.Buttons.OK DefaultButton: Display.DefaultButton.Button1 IsTopMost: True ButtonPressed=> _txt_ButtonPressed EXIT Code: 1 END **ENDREGION @@regionColor: '#FFD966' **REGION 5. Output: the HTML file, and the email when Outlook is asked File.GetPathPart File: txt_WorkbookPath FileName=> txt_WorkbookName FileNameWithoutExtension=> txt_WorkbookBaseName SET txt_HtmlFile TO $'''%txt_OutputFolder%\\%txt_WorkbookBaseName%.html''' # Inline styles only: Outlook desktop, Outlook on the web and the mobile apps drop most