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/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();
}
///
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