From c3754a6f2707dfcf13bb3c7a7123249f8b33fae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Tue, 28 Jul 2026 07:25:24 +0200 Subject: [PATCH 1/2] Add another HTML panel for the HtmlKit output --- Source/Demo/Common/HtmlPrettyPrinter.cs | 145 +++++++++++++++++++ Source/Demo/WinForms/MainControl.Designer.cs | 86 ++++++++++- Source/Demo/WinForms/MainControl.cs | 30 +++- 3 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 Source/Demo/Common/HtmlPrettyPrinter.cs diff --git a/Source/Demo/Common/HtmlPrettyPrinter.cs b/Source/Demo/Common/HtmlPrettyPrinter.cs new file mode 100644 index 000000000..09d0e207d --- /dev/null +++ b/Source/Demo/Common/HtmlPrettyPrinter.cs @@ -0,0 +1,145 @@ +// "Therefore those skilled at the unorthodox +// are infinite as heaven and earth, +// inexhaustible as the great rivers. +// When they come to an end, +// they begin again, +// like the days and months; +// they die and are reborn, +// like the four seasons." +// +// - Sun Tsu, +// "The Art of War" + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using HtmlKit; + +namespace TheArtOfDev.HtmlRenderer.Demo.Common +{ + /// + /// Utility to format HTML into a readable representation while preserving semantic structure. + /// + public static class HtmlPrettyPrinter + { + /// + /// html tags that should be written with indentation on separate lines + /// + private static readonly HashSet _blockTags = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "article", "aside", "blockquote", "body", "caption", "colgroup", "dd", "div", "dl", "dt", "fieldset", "figcaption", + "figure", "footer", "form", "head", "header", "html", "li", "main", "nav", "ol", "p", "section", "table", "tbody", + "td", "tfoot", "th", "thead", "tr", "ul", "title" + }; + + /// + /// html tags that don't have a closing tag and should not change indentation depth + /// + private static readonly HashSet _voidTags = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" + }; + + /// + /// Format the given html into a readable left-to-right comparable layout. + /// + public static string Format(string html) + { + if (string.IsNullOrWhiteSpace(html)) + return string.Empty; + + var builder = new StringBuilder(html.Length * 2); + using (var reader = new StringReader(html)) + using (var writer = new StringWriter(builder)) + { + var tokenizer = new HtmlTokenizer(reader); + HtmlToken token; + int indent = 0; + + while (tokenizer.ReadNextToken(out token)) + { + switch (token.Kind) + { + case HtmlTokenKind.DocType: + case HtmlTokenKind.Comment: + AppendTokenLine(builder, writer, token, indent); + break; + + case HtmlTokenKind.Tag: + var tag = (HtmlTagToken)token; + var isBlockTag = _blockTags.Contains(tag.Name); + var isVoidTag = tag.IsEmptyElement || _voidTags.Contains(tag.Name); + + if (tag.IsEndTag) + { + if (isBlockTag) + { + indent = Math.Max(0, indent - 1); + AppendTokenLine(builder, writer, token, indent); + } + else + { + token.WriteTo(writer); + } + } + else if (isBlockTag) + { + AppendTokenLine(builder, writer, token, indent); + if (!isVoidTag) + indent++; + } + else + { + token.WriteTo(writer); + } + break; + + case HtmlTokenKind.CData: + case HtmlTokenKind.Data: + case HtmlTokenKind.ScriptData: + var data = ((HtmlDataToken)token).Data; + if (!string.IsNullOrWhiteSpace(data)) + { + if (IsAtLineStart(builder)) + AppendIndent(builder, indent); + builder.Append(data); + } + break; + } + } + } + + return builder.ToString().Trim(); + } + + /// + /// Append the given html token on its own line using the given indentation level. + /// + private static void AppendTokenLine(StringBuilder builder, StringWriter writer, HtmlToken token, int indent) + { + if (!IsAtLineStart(builder)) + builder.AppendLine(); + + AppendIndent(builder, indent); + token.WriteTo(writer); + builder.AppendLine(); + } + + /// + /// Append indentation spaces to the string builder. + /// + private static void AppendIndent(StringBuilder builder, int indent) + { + builder.Append(' ', indent * 2); + } + + /// + /// Return if the builder currently points at the start of a new line. + /// + private static bool IsAtLineStart(StringBuilder builder) + { + return builder.Length == 0 || builder[builder.Length - 1] == '\n'; + } + } +} diff --git a/Source/Demo/WinForms/MainControl.Designer.cs b/Source/Demo/WinForms/MainControl.Designer.cs index 9a4dff315..424a9a52f 100644 --- a/Source/Demo/WinForms/MainControl.Designer.cs +++ b/Source/Demo/WinForms/MainControl.Designer.cs @@ -31,11 +31,15 @@ private void InitializeComponent() this._splitContainer1 = new System.Windows.Forms.SplitContainer(); this._samplesTreeView = new System.Windows.Forms.TreeView(); this._splitContainer2 = new System.Windows.Forms.SplitContainer(); + this._splitContainer3 = new System.Windows.Forms.SplitContainer(); this._htmlPanel = new HtmlRenderer.WinForms.HtmlPanel(); this._splitter = new System.Windows.Forms.Splitter(); this._webBrowser = new System.Windows.Forms.WebBrowser(); + this._inputHtmlLabel = new System.Windows.Forms.Label(); this._reloadColorsLink = new System.Windows.Forms.LinkLabel(); this._htmlEditor = new System.Windows.Forms.RichTextBox(); + this._fixedHtmlLabel = new System.Windows.Forms.Label(); + this._fixedHtmlEditor = new System.Windows.Forms.RichTextBox(); this._htmlToolTip = new HtmlRenderer.WinForms.HtmlToolTip(); this._splitContainer1.Panel1.SuspendLayout(); this._splitContainer1.Panel2.SuspendLayout(); @@ -43,6 +47,9 @@ private void InitializeComponent() this._splitContainer2.Panel1.SuspendLayout(); this._splitContainer2.Panel2.SuspendLayout(); this._splitContainer2.SuspendLayout(); + this._splitContainer3.Panel1.SuspendLayout(); + this._splitContainer3.Panel2.SuspendLayout(); + this._splitContainer3.SuspendLayout(); this.SuspendLayout(); // // _splitContainer1 @@ -89,13 +96,35 @@ private void InitializeComponent() // // _splitContainer2.Panel2 // - this._splitContainer2.Panel2.Controls.Add(this._reloadColorsLink); - this._splitContainer2.Panel2.Controls.Add(this._htmlEditor); + this._splitContainer2.Panel2.Controls.Add(this._splitContainer3); this._splitContainer2.Size = new System.Drawing.Size(729, 593); this._splitContainer2.SplitterDistance = 476; this._splitContainer2.TabIndex = 13; this._splitContainer2.TabStop = false; // + // _splitContainer3 + // + this._splitContainer3.Dock = System.Windows.Forms.DockStyle.Fill; + this._splitContainer3.Location = new System.Drawing.Point(0, 0); + this._splitContainer3.Name = "_splitContainer3"; + // + // _splitContainer3.Panel1 + // + this._splitContainer3.Panel1.Controls.Add(this._htmlEditor); + this._splitContainer3.Panel1.Controls.Add(this._reloadColorsLink); + this._splitContainer3.Panel1.Controls.Add(this._inputHtmlLabel); + // + // _splitContainer3.Panel2 + // + this._splitContainer3.Panel2.Controls.Add(this._fixedHtmlEditor); + this._splitContainer3.Panel2.Controls.Add(this._fixedHtmlLabel); + this._splitContainer3.Size = new System.Drawing.Size(729, 113); + this._splitContainer3.Panel1MinSize = 150; + this._splitContainer3.Panel2MinSize = 150; + this._splitContainer3.SplitterDistance = 364; + this._splitContainer3.TabIndex = 9; + this._splitContainer3.TabStop = false; + // // _htmlPanel // this._htmlPanel.AutoScroll = true; @@ -130,12 +159,22 @@ private void InitializeComponent() this._webBrowser.TabIndex = 7; this._webBrowser.Visible = false; // + // _inputHtmlLabel + // + this._inputHtmlLabel.Dock = System.Windows.Forms.DockStyle.Top; + this._inputHtmlLabel.Location = new System.Drawing.Point(0, 0); + this._inputHtmlLabel.Name = "_inputHtmlLabel"; + this._inputHtmlLabel.Size = new System.Drawing.Size(364, 24); + this._inputHtmlLabel.TabIndex = 9; + this._inputHtmlLabel.Text = "Input HTML"; + this._inputHtmlLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // // _reloadColorsLink // - this._reloadColorsLink.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this._reloadColorsLink.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this._reloadColorsLink.AutoSize = true; this._reloadColorsLink.BackColor = System.Drawing.Color.White; - this._reloadColorsLink.Location = new System.Drawing.Point(666, 97); + this._reloadColorsLink.Location = new System.Drawing.Point(277, 6); this._reloadColorsLink.Name = "_reloadColorsLink"; this._reloadColorsLink.Size = new System.Drawing.Size(44, 13); this._reloadColorsLink.TabIndex = 8; @@ -146,14 +185,37 @@ private void InitializeComponent() // _htmlEditor // this._htmlEditor.Dock = System.Windows.Forms.DockStyle.Fill; - this._htmlEditor.Location = new System.Drawing.Point(0, 0); + this._htmlEditor.Location = new System.Drawing.Point(0, 24); this._htmlEditor.Name = "_htmlEditor"; - this._htmlEditor.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.ForcedVertical; - this._htmlEditor.Size = new System.Drawing.Size(729, 113); + this._htmlEditor.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Both; + this._htmlEditor.Size = new System.Drawing.Size(364, 89); this._htmlEditor.TabIndex = 7; this._htmlEditor.Text = ""; + this._htmlEditor.WordWrap = false; this._htmlEditor.TextChanged += new System.EventHandler(this.OnHtmlEditorTextChanged); // + // _fixedHtmlLabel + // + this._fixedHtmlLabel.Dock = System.Windows.Forms.DockStyle.Top; + this._fixedHtmlLabel.Location = new System.Drawing.Point(0, 0); + this._fixedHtmlLabel.Name = "_fixedHtmlLabel"; + this._fixedHtmlLabel.Size = new System.Drawing.Size(361, 24); + this._fixedHtmlLabel.TabIndex = 10; + this._fixedHtmlLabel.Text = "Fixed HTML"; + this._fixedHtmlLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // _fixedHtmlEditor + // + this._fixedHtmlEditor.Dock = System.Windows.Forms.DockStyle.Fill; + this._fixedHtmlEditor.Location = new System.Drawing.Point(0, 24); + this._fixedHtmlEditor.Name = "_fixedHtmlEditor"; + this._fixedHtmlEditor.ReadOnly = true; + this._fixedHtmlEditor.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Both; + this._fixedHtmlEditor.Size = new System.Drawing.Size(361, 89); + this._fixedHtmlEditor.TabIndex = 11; + this._fixedHtmlEditor.Text = ""; + this._fixedHtmlEditor.WordWrap = false; + // // _htmlToolTip // this._htmlToolTip.AutoPopDelay = 15000; @@ -176,8 +238,12 @@ private void InitializeComponent() this._splitContainer1.ResumeLayout(false); this._splitContainer2.Panel1.ResumeLayout(false); this._splitContainer2.Panel2.ResumeLayout(false); - this._splitContainer2.Panel2.PerformLayout(); this._splitContainer2.ResumeLayout(false); + this._splitContainer3.Panel1.ResumeLayout(false); + this._splitContainer3.Panel1.PerformLayout(); + this._splitContainer3.Panel2.ResumeLayout(false); + this._splitContainer3.Panel2.PerformLayout(); + this._splitContainer3.ResumeLayout(false); this.ResumeLayout(false); } @@ -187,11 +253,15 @@ private void InitializeComponent() private System.Windows.Forms.SplitContainer _splitContainer1; private System.Windows.Forms.TreeView _samplesTreeView; private System.Windows.Forms.SplitContainer _splitContainer2; + private System.Windows.Forms.SplitContainer _splitContainer3; private HtmlRenderer.WinForms.HtmlPanel _htmlPanel; private System.Windows.Forms.Splitter _splitter; private System.Windows.Forms.WebBrowser _webBrowser; + private System.Windows.Forms.Label _inputHtmlLabel; private System.Windows.Forms.LinkLabel _reloadColorsLink; private System.Windows.Forms.RichTextBox _htmlEditor; + private System.Windows.Forms.Label _fixedHtmlLabel; + private System.Windows.Forms.RichTextBox _fixedHtmlEditor; private HtmlRenderer.WinForms.HtmlToolTip _htmlToolTip; } } diff --git a/Source/Demo/WinForms/MainControl.cs b/Source/Demo/WinForms/MainControl.cs index c683e99e2..5ad5e5525 100644 --- a/Source/Demo/WinForms/MainControl.cs +++ b/Source/Demo/WinForms/MainControl.cs @@ -64,6 +64,7 @@ public MainControl() _htmlToolTip.SetToolTip(_htmlPanel, Resources.Tooltip); _htmlEditor.Font = new Font(FontFamily.GenericMonospace, 10); + _fixedHtmlEditor.Font = _htmlEditor.Font; LoadSamples(); @@ -126,6 +127,7 @@ public string GetHtml() public void SetHtml(string html) { _htmlPanel.Text = html; + UpdateFixedHtmlView(); } @@ -203,9 +205,11 @@ private void OnSamplesTreeViewAfterSelect(object sender, TreeViewEventArgs e) _htmlPanel.AvoidImagesLateLoading = !sample.FullName.Contains("Many images"); _htmlPanel.Text = sample.Html; + UpdateFixedHtmlView(); } catch (Exception ex) { + SetColoredText(_fixedHtmlEditor, string.Empty); MessageBox.Show(ex.ToString(), "Failed to render HTML"); } @@ -239,9 +243,11 @@ private void OnUpdateHtmlTimerTick(object state) try { _htmlPanel.Text = _htmlEditor.Text; + UpdateFixedHtmlView(); } catch (Exception ex) { + SetColoredText(_fixedHtmlEditor, string.Empty); MessageBox.Show(ex.ToString(), "Failed to render HTML"); } @@ -288,6 +294,14 @@ private string GetFixedHtml() return html; } + /// + /// Update the fixed html shown in the read-only editor. + /// + private void UpdateFixedHtmlView() + { + SetColoredText(_fixedHtmlEditor, HtmlPrettyPrinter.Format(_htmlPanel.GetHtml() ?? string.Empty)); + } + /// /// Reload the html shown in the html editor by running coloring again. /// @@ -329,10 +343,18 @@ private static void OnLinkClicked(object sender, HtmlLinkClickedEventArgs e) /// private void SetColoredText(string text) { - var selectionStart = _htmlEditor.SelectionStart; - _htmlEditor.Clear(); - _htmlEditor.Rtf = HtmlSyntaxHighlighter.Process(text); - _htmlEditor.SelectionStart = selectionStart; + SetColoredText(_htmlEditor, text); + } + + /// + /// Set html syntax color text on the given RTF html editor. + /// + private static void SetColoredText(RichTextBox richTextBox, string text) + { + var selectionStart = richTextBox.SelectionStart; + richTextBox.Clear(); + richTextBox.Rtf = HtmlSyntaxHighlighter.Process(text ?? string.Empty); + richTextBox.SelectionStart = Math.Min(selectionStart, richTextBox.TextLength); } #endregion From 8a3508ff2df3c15123d98a9fa7f992358f82548e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Tue, 28 Jul 2026 07:30:55 +0200 Subject: [PATCH 2/2] Add another HTML panel for the HtmlKit output in WPF --- Source/Demo/WPF/MainControl.xaml | 52 +++++++++++++++++++++++------ Source/Demo/WPF/MainControl.xaml.cs | 26 ++++++++++++++- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/Source/Demo/WPF/MainControl.xaml b/Source/Demo/WPF/MainControl.xaml index 714c2dbf1..9f4fd98a6 100644 --- a/Source/Demo/WPF/MainControl.xaml +++ b/Source/Demo/WPF/MainControl.xaml @@ -37,16 +37,48 @@ - + + + + + + + + + + + + + + + + + + + + + Colored + + + + + + + + + + + + + + - - - Colored - diff --git a/Source/Demo/WPF/MainControl.xaml.cs b/Source/Demo/WPF/MainControl.xaml.cs index 00fd7b5c5..bce20029e 100644 --- a/Source/Demo/WPF/MainControl.xaml.cs +++ b/Source/Demo/WPF/MainControl.xaml.cs @@ -128,6 +128,7 @@ public string GetHtml() public void SetHtml(string html) { _htmlPanel.Text = html; + UpdateFixedHtmlView(); if (string.IsNullOrWhiteSpace(html)) { _htmlPanel.InvalidateMeasure(); @@ -219,9 +220,11 @@ private void OnTreeView_SelectedItemChanged(object sender, RoutedPropertyChanged { _htmlPanel.AvoidImagesLateLoading = !sample.FullName.Contains("Many images"); _htmlPanel.Text = sample.Html; + UpdateFixedHtmlView(); } catch (Exception ex) { + SetFixedHtmlText(string.Empty); MessageBox.Show(ex.ToString(), "Failed to render HTML"); } @@ -255,9 +258,11 @@ private void OnUpdateHtmlTimerTick(object state) try { _htmlPanel.Text = GetHtmlEditorText(); + UpdateFixedHtmlView(); } catch (Exception ex) { + SetFixedHtmlText(string.Empty); MessageBox.Show(ex.ToString(), "Failed to render HTML"); } @@ -307,6 +312,23 @@ private string GetFixedHtml() return html; } + /// + /// Update the fixed html shown in the read-only editor. + /// + private void UpdateFixedHtmlView() + { + SetFixedHtmlText(HtmlPrettyPrinter.Format(_htmlPanel.GetHtml() ?? string.Empty), _coloredCheckBox.IsChecked.GetValueOrDefault(false)); + } + + /// + /// Set formatted HTML text in the fixed HTML viewer. + /// + private void SetFixedHtmlText(string text, bool color = true) + { + text = text ?? string.Empty; + _fixedHtmlEditor.Text = color ? HtmlSyntaxHighlighter.Process(text) : text.Replace("\n", "\\par "); + } + /// /// Reload the html shown in the html editor by running coloring again. /// @@ -320,7 +342,9 @@ private void OnRefreshLink_MouseLeftButtonUp(object sender, MouseButtonEventArgs /// private void OnColoredCheckbox_click(object sender, RoutedEventArgs e) { - SetColoredText(GetHtmlEditorText(), _coloredCheckBox.IsChecked.GetValueOrDefault(false)); + var color = _coloredCheckBox.IsChecked.GetValueOrDefault(false); + SetColoredText(GetHtmlEditorText(), color); + UpdateFixedHtmlView(); } ///