From 2519035402ae73d064fc0e4281af01b3329e132b Mon Sep 17 00:00:00 2001 From: Gustavo Martin Date: Mon, 22 Dec 2025 08:27:42 +0100 Subject: [PATCH 1/9] added test tag and description --- ReportAlert2Html.tcl | 12 ++++++++++++ ReportBuildDict2Html.tcl | 10 +++++++++- ReportSimulate2Html.tcl | 28 +++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/ReportAlert2Html.tcl b/ReportAlert2Html.tcl index 308b751..64862f7 100644 --- a/ReportAlert2Html.tcl +++ b/ReportAlert2Html.tcl @@ -66,6 +66,18 @@ proc LocalAlert2Html {TestCaseName TestSuiteName AlertYamlFile} { AlertSettings $Alert2HtmlDict + # Extract and store Description and Tags for use in other reports + if {[dict exists $Alert2HtmlDict Description]} { + set ::osvvm::Report2TestDescription [dict get $Alert2HtmlDict Description] + } else { + set ::osvvm::Report2TestDescription "" + } + if {[dict exists $Alert2HtmlDict Tags]} { + set ::osvvm::Report2TestTags [dict get $Alert2HtmlDict Tags] + } else { + set ::osvvm::Report2TestTags "" + } + CreateAlertResultsHeader $TestCaseName AlertWrite $Alert2HtmlDict diff --git a/ReportBuildDict2Html.tcl b/ReportBuildDict2Html.tcl index 11ede1f..ee1f9ef 100644 --- a/ReportBuildDict2Html.tcl +++ b/ReportBuildDict2Html.tcl @@ -302,6 +302,7 @@ proc CreateTestCaseSummaries {TestDict} { puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " + puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " @@ -323,6 +324,12 @@ proc CreateTestCaseSummaries {TestDict} { foreach TestCase [dict get $TestSuite TestCases] { set TestName [dict get $TestCase TestCaseName] + # Get test description if available + if { [dict exists $TestCase Description] } { + set TestDescription [dict get $TestCase Description] + } else { + set TestDescription "" + } if { [dict exists $TestCase Status] } { set TestStatus [dict get $TestCase Status] set TestResults [dict get $TestCase Results] @@ -394,6 +401,7 @@ proc CreateTestCaseSummaries {TestDict} { } puts $ResultsFile " " puts $ResultsFile " " + puts $ResultsFile " " puts $ResultsFile " " if { $TestReport eq "REPORT" } { puts $ResultsFile " " @@ -426,7 +434,7 @@ proc CreateTestCaseSummaries {TestDict} { } puts $ResultsFile " " } else { - puts $ResultsFile " " + puts $ResultsFile " " } puts $ResultsFile " " } diff --git a/ReportSimulate2Html.tcl b/ReportSimulate2Html.tcl index 07036f1..44418a5 100644 --- a/ReportSimulate2Html.tcl +++ b/ReportSimulate2Html.tcl @@ -69,7 +69,20 @@ proc Simulate2Html {SettingsFileWithPath} { set BuildName $::osvvm::Report2BuildName set GenericDict $::osvvm::Report2GenericDict - + # Initialize description and tags to empty + set ::osvvm::Report2TestDescription "" + set ::osvvm::Report2TestTags "" + + # Read Description and Tags from Alert YAML file before creating summary table + if {[file exists ${Report2AlertYamlFile}]} { + set AlertDict [::yaml::yaml2dict -file ${Report2AlertYamlFile}] + if {[dict exists $AlertDict Description]} { + set ::osvvm::Report2TestDescription [dict get $AlertDict Description] + } + if {[dict exists $AlertDict Tags]} { + set ::osvvm::Report2TestTags [dict get $AlertDict Tags] + } + } CreateTestCaseSummaryTable ${TestCaseName} ${TestSuiteName} ${BuildName} ${GenericDict} @@ -143,6 +156,19 @@ proc LocalCreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName Gener puts $ResultsFile " " puts $ResultsFile " " + # Print test description if available + if {[info exists ::osvvm::Report2TestDescription] && $::osvvm::Report2TestDescription ne ""} { + puts $ResultsFile " " + } + + # Print test tags if available + if {[info exists ::osvvm::Report2TestTags] && $::osvvm::Report2TestTags ne ""} { + puts $ResultsFile " " + foreach {TagName TagValue} $::osvvm::Report2TestTags { + puts $ResultsFile " " + } + } + # Print the Generics if {${GenericDict} ne ""} { foreach {GenericName GenericValue} $GenericDict { From 070c949c40aedfa40a4a5535214d901661cdce59 Mon Sep 17 00:00:00 2001 From: Gustavo Martin Date: Wed, 24 Dec 2025 15:55:47 +0100 Subject: [PATCH 2/9] Add support for suite-level descriptions and briefs in YAML reports - Introduced SetTestSuiteDescription and SetTestSuiteBrief procedures for setting descriptions and briefs for test suites. - Updated YAML report generation to include brief and description fields. - Enhanced HTML report generation to display brief information for test suites and test cases. - Added utility functions for formatting YAML scalars and escaping HTML. --- CssOsvvmStyle.css | 4 +- OsvvmScriptsCore.tcl | 19 ++++ OsvvmScriptsCreateYamlReports.tcl | 58 +++++++++++- ReportBuildDict2Html.tcl | 95 ++++++++++++++++--- ReportBuildYaml2Dict.tcl | 15 +++ ReportSimulate2Html.tcl | 67 +++++++++---- ReportSupport.tcl | 150 ++++++++++++++++++++++++++++++ 7 files changed, 374 insertions(+), 34 deletions(-) diff --git a/CssOsvvmStyle.css b/CssOsvvmStyle.css index 581910b..8e647ff 100644 --- a/CssOsvvmStyle.css +++ b/CssOsvvmStyle.css @@ -46,9 +46,9 @@ body { header { } -main { +/* main { max-width: 1200px; -} +} */ footer { } diff --git a/OsvvmScriptsCore.tcl b/OsvvmScriptsCore.tcl index aa16924..e0e4391 100644 --- a/OsvvmScriptsCore.tcl +++ b/OsvvmScriptsCore.tcl @@ -1414,6 +1414,25 @@ proc TestSuite {SuiteName} { # CreateDirectory [file join ${::osvvm::CurrentSimulationDirectory} ${::osvvm::ResultsDirectory} ${TestSuiteName}] } +# ------------------------------------------------- +# SetTestSuiteDescription +# Sets a suite-level description which is written into the build YAML +# and displayed in the HTML "Test Suite Summary" Description column. +# +# Call this after TestSuite and before the suite finishes. +proc SetTestSuiteDescription {Description} { + set ::osvvm::TestSuiteDescription $Description +} + +# ------------------------------------------------- +# SetTestSuiteBrief +# Sets a suite-level brief (plain text) for summary tables. +# +# Call this after TestSuite and before the suite finishes. +proc SetTestSuiteBrief {Brief} { + set ::osvvm::TestSuiteBrief $Brief +} + # ------------------------------------------------- proc TestName {Name} { variable TestCaseName diff --git a/OsvvmScriptsCreateYamlReports.tcl b/OsvvmScriptsCreateYamlReports.tcl index b13cae5..80cb43a 100644 --- a/OsvvmScriptsCreateYamlReports.tcl +++ b/OsvvmScriptsCreateYamlReports.tcl @@ -48,6 +48,52 @@ namespace eval ::osvvm { package require fileutil +# ------------------------------------------------- +# FormatYamlScalar +# Return a YAML scalar string that uses native types when safe: +# - empty -> null (empty scalar) +# - true/false (case-insensitive, also accepts TRUE/FALSE) +# - integer / float +# Otherwise returns a double-quoted string with minimal escaping. +# +proc FormatYamlScalar {Value} { + # Treat unset/missing as empty + if {$Value eq ""} { + return "null" + } + + set Trimmed [string trim $Value] + if {$Trimmed eq ""} { + return "\"$Value\"" + } + + # null + if {[string equal -nocase $Trimmed "null"]} { + return "null" + } + + # boolean + if {[string equal -nocase $Trimmed "true"] || [string equal -nocase $Trimmed "false"]} { + return [string tolower $Trimmed] + } + + # integer + if {[regexp {^[-+]?\d+$} $Trimmed]} { + return $Trimmed + } + + # float (simple forms, incl exponent) + if {[regexp {^[-+]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][-+]?\d+)?$} $Trimmed] || [regexp {^[-+]?\d+(?:[eE][-+]?\d+)$} $Trimmed]} { + return $Trimmed + } + + # default: quoted string + # Use [list] to avoid Tcl list parsing edge cases + set Escaped [string map [list "\\" "\\\\" "\"" "\\\""] $Value] + return "\"$Escaped\"" +} + + variable TclZone [clock format [clock seconds] -format %z] variable IsoZone [format "%s:%s" [string range $TclZone 0 2] [string range $TclZone 3 4]] # variable TimeZoneName [clock format [clock seconds] -format %Z] @@ -198,7 +244,7 @@ proc WriteDictOfDict2Yaml {YamlFile DictName {DictValues ""} {Prefix ""} } { } else { puts $YamlFile "${Prefix}${DictName}:" foreach {Name Value} $DictValues { - puts $YamlFile "${Prefix} ${Name}: \"$Value\"" + puts $YamlFile "${Prefix} ${Name}: [FormatYamlScalar $Value]" } } } @@ -292,6 +338,10 @@ proc WriteTestCaseSettingsYaml {FileName} { # ------------------------------------------------- proc StartTestSuiteBuildYaml {SuiteName FirstRun} { variable TestSuiteStartTimeMs + # Suite-level description is set by user scripts (ex: .pro files) + # using ::osvvm::TestSuiteDescription. + set ::osvvm::TestSuiteDescription "" + set ::osvvm::TestSuiteBrief "" set RunFile [open ${::osvvm::OsvvmTempYamlFile} a] @@ -313,6 +363,12 @@ proc FinishTestSuiteBuildYaml {} { variable TestSuiteStartTimeMs set RunFile [open ${::osvvm::OsvvmTempYamlFile} a] + if {[info exists ::osvvm::TestSuiteBrief] && $::osvvm::TestSuiteBrief ne ""} { + WriteDictOfString2Yaml $RunFile Brief $::osvvm::TestSuiteBrief " " + } + if {[info exists ::osvvm::TestSuiteDescription] && $::osvvm::TestSuiteDescription ne ""} { + WriteDictOfString2Yaml $RunFile Description $::osvvm::TestSuiteDescription " " + } puts $RunFile " ElapsedTime: [ElapsedTimeMs $TestSuiteStartTimeMs]" close $RunFile } diff --git a/ReportBuildDict2Html.tcl b/ReportBuildDict2Html.tcl index ee1f9ef..3fbcaab 100644 --- a/ReportBuildDict2Html.tcl +++ b/ReportBuildDict2Html.tcl @@ -233,6 +233,7 @@ proc CreateTestSuiteSummary {} { puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " + puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " @@ -245,6 +246,14 @@ proc CreateTestSuiteSummary {} { foreach TestSuite $TestSuiteSummaryArrayOfDictionaries { set SuiteName [dict get $TestSuite Name] set SuiteStatus [dict get $TestSuite Status] + if { [dict exists $TestSuite Brief] } { + set SuiteBrief [dict get $TestSuite Brief] + } elseif { [dict exists $TestSuite Description] } { + # Backward compatibility: use first line of Description + set SuiteBrief [lindex [split [dict get $TestSuite Description] "\n"] 0] + } else { + set SuiteBrief "" + } set PassedClass "" set FailedClass "" @@ -279,6 +288,11 @@ proc CreateTestSuiteSummary {} { } puts $ResultsFile " " puts $ResultsFile " " + puts $ResultsFile " " puts $ResultsFile " " } puts $ResultsFile " " @@ -297,20 +311,46 @@ proc CreateTestCaseSummaries {TestDict} { if { [dict exists $TestDict TestSuites] } { foreach TestSuite [dict get $TestDict TestSuites] { set SuiteName [dict get $TestSuite Name] + + # Collect a stable list of generic names used by any test case in this suite. + # These become the subcolumns under the "Generics" column group. + set SuiteGenericNames {} + foreach TcForGenerics [dict get $TestSuite TestCases] { + if { [dict exists $TcForGenerics Generics] } { + set GenDict [dict get $TcForGenerics Generics] + if {![catch {dict size $GenDict}]} { + foreach GenName [dict keys $GenDict] { + if {[lsearch -exact $SuiteGenericNames $GenName] < 0} { + lappend SuiteGenericNames $GenName + } + } + } + } + } + set SuiteGenericCount [llength $SuiteGenericNames] + puts $ResultsFile "
" puts $ResultsFile "
$SuiteName Test Case Summary" puts $ResultsFile "
Test CaseDescriptionStatusChecksRequirements
${TestCaseName}${TestDescription}$TestStatus[dict get $TestResults AffirmCount]$TestCaseElapsedTime   $Reason   $Reason
Description: $::osvvm::Report2TestDescription
Test Configuration:
 $TagName: $TagValue
Requirements
passed / goal
Disabled
Alerts
Elapsed
Time
Brief
PASSED [dict get $TestSuite DisabledAlerts][dict get $TestSuite ElapsedTime]" + if {${SuiteBrief} ne ""} { + puts $ResultsFile " [EscapeHtml $SuiteBrief]" + } + puts $ResultsFile "
" puts $ResultsFile " " puts $ResultsFile " " - puts $ResultsFile " " + if { $SuiteGenericCount > 0 } { + puts $ResultsFile " " + } puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " + puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " + if { $SuiteGenericCount > 0 } { + foreach GenName $SuiteGenericNames { + puts $ResultsFile " " + } + } puts $ResultsFile " " puts $ResultsFile " " puts $ResultsFile " " @@ -324,11 +364,14 @@ proc CreateTestCaseSummaries {TestDict} { foreach TestCase [dict get $TestSuite TestCases] { set TestName [dict get $TestCase TestCaseName] - # Get test description if available - if { [dict exists $TestCase Description] } { - set TestDescription [dict get $TestCase Description] + # Get test brief if available + if { [dict exists $TestCase Brief] } { + set TestBrief [dict get $TestCase Brief] + } elseif { [dict exists $TestCase Description] } { + # Backward compatibility: use first line of Description + set TestBrief [lindex [split [dict get $TestCase Description] "\n"] 0] } else { - set TestDescription "" + set TestBrief "" } if { [dict exists $TestCase Status] } { set TestStatus [dict get $TestCase Status] @@ -382,13 +425,14 @@ proc CreateTestCaseSummaries {TestDict} { } set TestCaseHtmlFile [file join ${TestSuiteReportsDirectory} ${TestFileName}.html] set TestCaseName $TestName - if { [dict exists $TestCase Generics] } { + # Backward compatibility: if there are no generic columns, append generic values to the Test Case name. + if { ($SuiteGenericCount == 0) && [dict exists $TestCase Generics] } { set TestCaseGenerics [dict get $TestCase Generics] - if {${TestCaseGenerics} ne ""} { - set GenericValueList [dict values $TestCaseGenerics] + if {![catch {dict size $TestCaseGenerics}] && ([dict size $TestCaseGenerics] > 0)} { + set GenericValueList [dict values $TestCaseGenerics] set i 0 set ListLen [llength ${GenericValueList}] - append TestCaseName " (" + append TestCaseName " (" foreach GenericValue $GenericValueList { incr i if {$i != $ListLen} { @@ -401,7 +445,29 @@ proc CreateTestCaseSummaries {TestDict} { } puts $ResultsFile " " puts $ResultsFile " " - puts $ResultsFile " " + + # Generics are the second column group (after Test Case name). + if { $SuiteGenericCount > 0 } { + if { [dict exists $TestCase Generics] } { + set TestCaseGenerics [dict get $TestCase Generics] + } else { + set TestCaseGenerics "" + } + set HasGenerics 0 + if {![catch {dict size $TestCaseGenerics}] && ([dict size $TestCaseGenerics] > 0)} { + set HasGenerics 1 + } + foreach GenName $SuiteGenericNames { + if { $HasGenerics && [dict exists $TestCaseGenerics $GenName] } { + set GenValue [dict get $TestCaseGenerics $GenName] + } else { + set GenValue "⸻" + } + set GenDisplayValue [FormatGenericValueForHtml $GenName $GenValue $TestFileName] + puts $ResultsFile " " + } + } + puts $ResultsFile " " if { $TestReport eq "REPORT" } { puts $ResultsFile " " @@ -433,8 +499,15 @@ proc CreateTestCaseSummaries {TestDict} { set TestCaseElapsedTime missing } puts $ResultsFile " " + puts $ResultsFile " " } else { - puts $ResultsFile " " + # Remaining columns after Test Case + (optional Generics) + Status + set RemainingColumns 9 + puts $ResultsFile " " } puts $ResultsFile " " } diff --git a/ReportBuildYaml2Dict.tcl b/ReportBuildYaml2Dict.tcl index fb220a3..8f63085 100644 --- a/ReportBuildYaml2Dict.tcl +++ b/ReportBuildYaml2Dict.tcl @@ -211,6 +211,19 @@ proc ElaborateTestSuites {TestDict} { set SuiteStatus "FAILED" set BuildStatus "FAILED" } + if {[dict exists $TestSuite Description]} { + set SuiteDescription [dict get $TestSuite Description] + } else { + set SuiteDescription "" + } + if {[dict exists $TestSuite Brief]} { + set SuiteBrief [dict get $TestSuite Brief] + } elseif {$SuiteDescription ne ""} { + # Backward compatibility: use first line of Description + set SuiteBrief [lindex [split $SuiteDescription "\n"] 0] + } else { + set SuiteBrief "" + } set SuiteDict [dict create Name $SuiteName] dict append SuiteDict Status $SuiteStatus dict append SuiteDict PASSED $SuitePassed @@ -220,6 +233,8 @@ proc ElaborateTestSuites {TestDict} { dict append SuiteDict ReqGoal $SuiteReqGoal dict append SuiteDict DisabledAlerts $SuiteDisabledAlerts dict append SuiteDict ElapsedTime $SuiteElapsedTime + dict set SuiteDict Brief $SuiteBrief + dict set SuiteDict Description $SuiteDescription lappend TestSuiteSummaryArrayOfDictionaries $SuiteDict } } diff --git a/ReportSimulate2Html.tcl b/ReportSimulate2Html.tcl index 44418a5..88c49ad 100644 --- a/ReportSimulate2Html.tcl +++ b/ReportSimulate2Html.tcl @@ -62,6 +62,11 @@ proc Simulate2Html {SettingsFileWithPath} { GetTestCaseSettings $SettingsFileWithPath + + # Align local script variables with ::osvvm settings parsed from YAML + # (avoids stale values between testcases) + set Report2AlertYamlFile $::osvvm::Report2AlertYamlFile + set Report2CovYamlFile $::osvvm::Report2CovYamlFile set TestCaseFileName $::osvvm::Report2TestCaseFileName set TestCaseName $::osvvm::Report2TestCaseName @@ -156,26 +161,6 @@ proc LocalCreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName Gener puts $ResultsFile " " puts $ResultsFile " " - # Print test description if available - if {[info exists ::osvvm::Report2TestDescription] && $::osvvm::Report2TestDescription ne ""} { - puts $ResultsFile " " - } - - # Print test tags if available - if {[info exists ::osvvm::Report2TestTags] && $::osvvm::Report2TestTags ne ""} { - puts $ResultsFile " " - foreach {TagName TagValue} $::osvvm::Report2TestTags { - puts $ResultsFile " " - } - } - - # Print the Generics - if {${GenericDict} ne ""} { - foreach {GenericName GenericValue} $GenericDict { - puts $ResultsFile " " - } - } - if {[file exists ${::osvvm::Report2AlertYamlFile}]} { puts $ResultsFile " " } @@ -223,6 +208,48 @@ proc LocalCreateTestCaseSummaryTable {TestCaseName TestSuiteName BuildName Gener LinkLogoFile $ResultsFile $ReportsPrefix puts $ResultsFile " " + + # Render Description / Tags / Generics as independent sections + # (user-requested: Description not in a table; Tags + Generics in tables) + if {[info exists ::osvvm::Report2TestDescription] && $::osvvm::Report2TestDescription ne ""} { + puts $ResultsFile "
" + puts $ResultsFile "
$TestCaseName Description" + WriteMarkdownSubsetAsHtml $ResultsFile $::osvvm::Report2TestDescription " " + puts $ResultsFile "
" + puts $ResultsFile "
" + } + + if {[info exists ::osvvm::Report2TestTags] && $::osvvm::Report2TestTags ne ""} { + puts $ResultsFile "
" + puts $ResultsFile "
$TestCaseName Tags" + puts $ResultsFile "
Test CaseDescriptionGenericsStatusChecksRequirementsFunctional
Coverage
Disabled
Alerts
Elapsed
Time
Brief
$GenNameTotalPassedFailed
${TestCaseName}${TestDescription}$GenDisplayValue$TestStatus[dict get $TestResults AffirmCount]$TestCaseElapsedTime" + if {${TestBrief} ne ""} { + puts $ResultsFile " [EscapeHtml $TestBrief]" + } + puts $ResultsFile "   $Reason   $Reason
Description: $::osvvm::Report2TestDescription
Test Configuration:
 $TagName: $TagValue
Generic: $GenericName = $GenericValue
Alert Report
" + puts $ResultsFile " " + puts $ResultsFile " " + foreach {TagName TagValue} $::osvvm::Report2TestTags { + set TagDisplayValue [FormatScalarForHtml $TagValue] + puts $ResultsFile " " + } + puts $ResultsFile " " + puts $ResultsFile "
NameValue
$TagName$TagDisplayValue
" + puts $ResultsFile " " + puts $ResultsFile " " + } + + if {${GenericDict} ne ""} { + puts $ResultsFile "
" + puts $ResultsFile "
$TestCaseName Generics" + puts $ResultsFile " " + puts $ResultsFile " " + puts $ResultsFile " " + foreach {GenericName GenericValue} $GenericDict { + set GenericDisplayValue [FormatGenericValueForHtml $GenericName $GenericValue $::osvvm::Report2GenericNames] + puts $ResultsFile " " + } + puts $ResultsFile " " + puts $ResultsFile "
NameValue
$GenericName$GenericDisplayValue
" + puts $ResultsFile "
" + puts $ResultsFile "
" + } } proc FinalizeSimulationReportFile {} { diff --git a/ReportSupport.tcl b/ReportSupport.tcl index ee90704..aedd348 100644 --- a/ReportSupport.tcl +++ b/ReportSupport.tcl @@ -41,6 +41,156 @@ package require yaml +# ------------------------------------------------- +# FormatGenericValueForHtml +# +# YAML boolean scalars may load into Tcl as 0/1. For generics, prefer showing +# True/False in HTML (matching VHDL/OSVVM conventions) when we can infer the +# original boolean value from the encoded GenericNames/TestCaseFileName string. +# +proc FormatGenericValueForHtml {GenericName GenericValue {GenericNames ""}} { + set EncodedGenericName $GenericName + if {[string match "G_*" $EncodedGenericName]} { + set EncodedGenericName [string range $EncodedGenericName 2 end] + } + + if {$GenericNames ne ""} { + if {[string first "_G_${EncodedGenericName}_TRUE" $GenericNames] >= 0} { return "True" } + if {[string first "_G_${EncodedGenericName}_FALSE" $GenericNames] >= 0} { return "False" } + } + + if {[string equal -nocase $GenericValue "true"]} { return "True" } + if {[string equal -nocase $GenericValue "false"]} { return "False" } + + return $GenericValue +} + +# ------------------------------------------------- +# FormatScalarForHtml +# +# YAML booleans commonly load into Tcl as 0/1. For HTML reports, render +# booleans as True/False. +# +proc FormatScalarForHtml {Value} { + if {[string equal -nocase $Value "true"]} { return "True" } + if {[string equal -nocase $Value "false"]} { return "False" } + + # Heuristic: yaml::yaml2dict represents YAML booleans as Tcl 0/1 + if {$Value eq 1 || $Value eq "1"} { return "True" } + if {$Value eq 0 || $Value eq "0"} { return "False" } + + return $Value +} + +# ------------------------------------------------- +# EscapeHtml +# +proc EscapeHtml {Text} { + set Escaped $Text + set Escaped [string map [list "&" "&" "<" "<" ">" ">" "\"" """ "'" "'"] $Escaped] + return $Escaped +} + +# ------------------------------------------------- +# FormatInlineMarkdownSubset +# +# Supports **bold** and *italic*. +# Input must already be HTML-escaped. + +proc FormatInlineMarkdownSubset {EscapedText} { + set S $EscapedText + regsub -all {\*\*([^*]+)\*\*} $S {\1} S + regsub -all {\*([^*]+)\*} $S {\1} S + return $S +} + +# ------------------------------------------------- +# WriteMarkdownSubsetAsHtml +# +# Minimal Markdown subset: +# - Paragraphs separated by blank lines +# - Headings: ## and ### +# - Bullet list items: - +# - Inline: **bold**, *italic* +# +proc WriteMarkdownSubsetAsHtml {ResultsFile Text {Indent ""}} { + # Normalize newlines + set Normalized [string map {"\r\n" "\n" "\r" "\n"} $Text] + set Lines [split $Normalized "\n"] + + set InList 0 + set ParaLines {} + + proc _FlushParagraph {ResultsFile Indent ParaLinesVar} { + upvar 1 $ParaLinesVar ParaLines + if {[llength $ParaLines] == 0} { + return + } + set Raw [join $ParaLines " "] + set Escaped [EscapeHtml $Raw] + set Html [FormatInlineMarkdownSubset $Escaped] + puts $ResultsFile "${Indent}

${Html}

" + set ParaLines {} + } + + proc _CloseListIfOpen {ResultsFile Indent InListVar} { + upvar 1 $InListVar InList + if {$InList} { + puts $ResultsFile "${Indent}" + set InList 0 + } + } + + foreach Line $Lines { + set Line [string trimright $Line] + set Trimmed [string trim $Line] + + if {$Trimmed eq ""} { + _FlushParagraph $ResultsFile $Indent ParaLines + _CloseListIfOpen $ResultsFile $Indent InList + continue + } + + if {[string match "## *" $Trimmed] || [string match "### *" $Trimmed]} { + _FlushParagraph $ResultsFile $Indent ParaLines + _CloseListIfOpen $ResultsFile $Indent InList + + if {[string match "### *" $Trimmed]} { + set Title [string range $Trimmed 4 end] + set Tag "h4" + } else { + set Title [string range $Trimmed 3 end] + set Tag "h3" + } + set Escaped [EscapeHtml $Title] + set Html [FormatInlineMarkdownSubset $Escaped] + puts $ResultsFile "${Indent}<${Tag} class=\"subtitle\">${Html}" + continue + } + + if {[string match "- *" $Trimmed]} { + _FlushParagraph $ResultsFile $Indent ParaLines + if {!$InList} { + puts $ResultsFile "${Indent}" - set InList 0 + proc _CloseListIfOpen {ResultsFile Indent ListKindVar} { + upvar 1 $ListKindVar ListKind + if {$ListKind ne ""} { + puts $ResultsFile "${Indent}" + set ListKind "" } } @@ -147,13 +149,13 @@ proc WriteMarkdownSubsetAsHtml {ResultsFile Text {Indent ""}} { if {$Trimmed eq ""} { _FlushParagraph $ResultsFile $Indent ParaLines - _CloseListIfOpen $ResultsFile $Indent InList + _CloseListIfOpen $ResultsFile $Indent ListKind continue } if {[string match "## *" $Trimmed] || [string match "### *" $Trimmed]} { _FlushParagraph $ResultsFile $Indent ParaLines - _CloseListIfOpen $ResultsFile $Indent InList + _CloseListIfOpen $ResultsFile $Indent ListKind if {[string match "### *" $Trimmed]} { set Title [string range $Trimmed 4 end] @@ -170,9 +172,10 @@ proc WriteMarkdownSubsetAsHtml {ResultsFile Text {Indent ""}} { if {[string match "- *" $Trimmed]} { _FlushParagraph $ResultsFile $Indent ParaLines - if {!$InList} { + if {$ListKind ne "ul"} { + _CloseListIfOpen $ResultsFile $Indent ListKind puts $ResultsFile "${Indent}