# local subflow: Convert_Excel_To_Html
# inputs: in_loc_txt_WorkbookPath, in_loc_txt_SheetName, in_loc_txt_RangeAddress
# outputs: out_loc_txt_Html, out_loc_bool_Ok, out_loc_txt_Error, out_loc_txt_SheetName, out_loc_txt_RangeAddress, out_loc_num_RowCount, out_loc_num_ColumnCount, out_loc_num_ElapsedMs
# Turns one Excel range into an HTML table that keeps the look of Excel, with inline CSS only (Outlook desktop, web, mobile).
# in_loc_txt_SheetName empty = the sheet active when the workbook was saved; in_loc_txt_RangeAddress empty = the used range.
# Never throws: out_loc_bool_Ok False and out_loc_txt_Error say what went wrong (workbook missing, sheet or range unknown, Excel absent).
# Excel publishes the range with its own HTML exporter; the script copies the CSS classes inline, drops the mso-* rules, the hidden
# rows and columns, encodes every non-ASCII character as an entity, and reports the details on the first line of its output.
@@regionColor: '#57FFE1'
**REGION 1. Outputs initialised: every path leaves them defined
SET out_loc_txt_Html TO $''''''
SET out_loc_bool_Ok TO False
SET out_loc_txt_Error TO $''''''
SET out_loc_txt_SheetName TO in_loc_txt_SheetName
SET out_loc_txt_RangeAddress TO in_loc_txt_RangeAddress
SET out_loc_num_RowCount TO 0
SET out_loc_num_ColumnCount TO 0
SET out_loc_num_ElapsedMs TO 0
**ENDREGION
@@regionColor: '#F4B6B6'
**REGION 2. Precondition: a missing workbook is reported, never thrown
IF (File.IfFile.DoesNotExist File: in_loc_txt_WorkbookPath) THEN
SET out_loc_txt_Error TO $'''Workbook not found: %in_loc_txt_WorkbookPath%'''
EXIT FUNCTION
END
**ENDREGION
@@regionColor: '#D9C3E9'
**REGION 3. Convert: Excel publishes the range, the script inlines the CSS; the first line of the output carries the details
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_ScriptOutput ScriptError=> txt_ScriptError
# The action never fails by itself: a script error only fills ScriptError
IF txt_ScriptError <> $'''''' THEN
Text.Trim Text: txt_ScriptError TrimOption: Text.TrimOption.Both TrimmedText=> out_loc_txt_Error
EXIT FUNCTION
END
**ENDREGION
@@regionColor: '#FFD966'
**REGION 4. Outputs: the details line (tab-separated: name, sheet, range, rows, columns, milliseconds), then the HTML table
Text.ParseText.RegexParseForFirstOccurrence Text: txt_ScriptOutput TextToFind: $'''^[^\\r\\n]*''' StartingPosition: 0 IgnoreCase: False Match=> txt_DetailsLine OccurrencePosition=> _num_DetailsPosition
Text.SplitText.Split Text: txt_DetailsLine StandardDelimiter: Text.StandardDelimiter.Tab DelimiterTimes: 1 Result=> lst_Details
Text.Replace.ReplaceTextWithRegex Text: txt_ScriptOutput TextToFind: $'''^[^\\r\\n]*\\r?\\n''' ReplaceWith: $'''''' IgnoreCase: False ActivateEscapeSequences: False Result=> txt_Html
Text.Trim Text: txt_Html TrimOption: Text.TrimOption.Both TrimmedText=> out_loc_txt_Html
SET out_loc_txt_SheetName TO lst_Details[1]
SET out_loc_txt_RangeAddress TO lst_Details[2]
Text.ToNumber Text: lst_Details[3] Number=> out_loc_num_RowCount
Text.ToNumber Text: lst_Details[4] Number=> out_loc_num_ColumnCount
Text.ToNumber Text: lst_Details[5] Number=> out_loc_num_ElapsedMs
SET out_loc_bool_Ok TO True
**ENDREGION