Skip to content

Commit d272bc7

Browse files
committed
Add another HTML panel for the HtmlKit output
1 parent 0a72ee9 commit d272bc7

3 files changed

Lines changed: 249 additions & 12 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// "Therefore those skilled at the unorthodox
2+
// are infinite as heaven and earth,
3+
// inexhaustible as the great rivers.
4+
// When they come to an end,
5+
// they begin again,
6+
// like the days and months;
7+
// they die and are reborn,
8+
// like the four seasons."
9+
//
10+
// - Sun Tsu,
11+
// "The Art of War"
12+
13+
using System;
14+
using System.Collections.Generic;
15+
using System.IO;
16+
using System.Text;
17+
using HtmlKit;
18+
19+
namespace TheArtOfDev.HtmlRenderer.Demo.Common
20+
{
21+
/// <summary>
22+
/// Utility to format HTML into a readable representation while preserving semantic structure.
23+
/// </summary>
24+
public static class HtmlPrettyPrinter
25+
{
26+
/// <summary>
27+
/// html tags that should be written with indentation on separate lines
28+
/// </summary>
29+
private static readonly HashSet<string> _blockTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
30+
{
31+
"article", "aside", "blockquote", "body", "caption", "colgroup", "dd", "div", "dl", "dt", "fieldset", "figcaption",
32+
"figure", "footer", "form", "head", "header", "html", "li", "main", "nav", "ol", "p", "section", "table", "tbody",
33+
"td", "tfoot", "th", "thead", "tr", "ul", "title"
34+
};
35+
36+
/// <summary>
37+
/// html tags that don't have a closing tag and should not change indentation depth
38+
/// </summary>
39+
private static readonly HashSet<string> _voidTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
40+
{
41+
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
42+
};
43+
44+
/// <summary>
45+
/// Format the given html into a readable left-to-right comparable layout.
46+
/// </summary>
47+
public static string Format(string html)
48+
{
49+
if (string.IsNullOrWhiteSpace(html))
50+
return string.Empty;
51+
52+
var builder = new StringBuilder(html.Length * 2);
53+
using (var reader = new StringReader(html))
54+
using (var writer = new StringWriter(builder))
55+
{
56+
var tokenizer = new HtmlTokenizer(reader);
57+
HtmlToken token;
58+
int indent = 0;
59+
60+
while (tokenizer.ReadNextToken(out token))
61+
{
62+
switch (token.Kind)
63+
{
64+
case HtmlTokenKind.DocType:
65+
case HtmlTokenKind.Comment:
66+
AppendTokenLine(builder, writer, token, indent);
67+
break;
68+
69+
case HtmlTokenKind.Tag:
70+
var tag = (HtmlTagToken)token;
71+
var isBlockTag = _blockTags.Contains(tag.Name);
72+
var isVoidTag = tag.IsEmptyElement || _voidTags.Contains(tag.Name);
73+
74+
if (tag.IsEndTag)
75+
{
76+
if (isBlockTag)
77+
{
78+
indent = Math.Max(0, indent - 1);
79+
AppendTokenLine(builder, writer, token, indent);
80+
}
81+
else
82+
{
83+
token.WriteTo(writer);
84+
}
85+
}
86+
else if (isBlockTag)
87+
{
88+
AppendTokenLine(builder, writer, token, indent);
89+
if (!isVoidTag)
90+
indent++;
91+
}
92+
else
93+
{
94+
token.WriteTo(writer);
95+
}
96+
break;
97+
98+
case HtmlTokenKind.CData:
99+
case HtmlTokenKind.Data:
100+
case HtmlTokenKind.ScriptData:
101+
var data = ((HtmlDataToken)token).Data;
102+
if (!string.IsNullOrWhiteSpace(data))
103+
{
104+
if (IsAtLineStart(builder))
105+
AppendIndent(builder, indent);
106+
builder.Append(data);
107+
}
108+
break;
109+
}
110+
}
111+
}
112+
113+
return builder.ToString().Trim();
114+
}
115+
116+
/// <summary>
117+
/// Append the given html token on its own line using the given indentation level.
118+
/// </summary>
119+
private static void AppendTokenLine(StringBuilder builder, StringWriter writer, HtmlToken token, int indent)
120+
{
121+
if (!IsAtLineStart(builder))
122+
builder.AppendLine();
123+
124+
AppendIndent(builder, indent);
125+
token.WriteTo(writer);
126+
builder.AppendLine();
127+
}
128+
129+
/// <summary>
130+
/// Append indentation spaces to the string builder.
131+
/// </summary>
132+
private static void AppendIndent(StringBuilder builder, int indent)
133+
{
134+
builder.Append(' ', indent * 2);
135+
}
136+
137+
/// <summary>
138+
/// Return if the builder currently points at the start of a new line.
139+
/// </summary>
140+
private static bool IsAtLineStart(StringBuilder builder)
141+
{
142+
return builder.Length == 0 || builder[builder.Length - 1] == '\n';
143+
}
144+
}
145+
}

‎Source/Demo/WinForms/MainControl.Designer.cs‎

Lines changed: 78 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Source/Demo/WinForms/MainControl.cs‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public MainControl()
6464
_htmlToolTip.SetToolTip(_htmlPanel, Resources.Tooltip);
6565

6666
_htmlEditor.Font = new Font(FontFamily.GenericMonospace, 10);
67+
_fixedHtmlEditor.Font = _htmlEditor.Font;
6768

6869
LoadSamples();
6970

@@ -126,6 +127,7 @@ public string GetHtml()
126127
public void SetHtml(string html)
127128
{
128129
_htmlPanel.Text = html;
130+
UpdateFixedHtmlView();
129131
}
130132

131133

@@ -203,9 +205,11 @@ private void OnSamplesTreeViewAfterSelect(object sender, TreeViewEventArgs e)
203205
_htmlPanel.AvoidImagesLateLoading = !sample.FullName.Contains("Many images");
204206

205207
_htmlPanel.Text = sample.Html;
208+
UpdateFixedHtmlView();
206209
}
207210
catch (Exception ex)
208211
{
212+
SetColoredText(_fixedHtmlEditor, string.Empty);
209213
MessageBox.Show(ex.ToString(), "Failed to render HTML");
210214
}
211215

@@ -239,9 +243,11 @@ private void OnUpdateHtmlTimerTick(object state)
239243
try
240244
{
241245
_htmlPanel.Text = _htmlEditor.Text;
246+
UpdateFixedHtmlView();
242247
}
243248
catch (Exception ex)
244249
{
250+
SetColoredText(_fixedHtmlEditor, string.Empty);
245251
MessageBox.Show(ex.ToString(), "Failed to render HTML");
246252
}
247253

@@ -288,6 +294,14 @@ private string GetFixedHtml()
288294
return html;
289295
}
290296

297+
/// <summary>
298+
/// Update the fixed html shown in the read-only editor.
299+
/// </summary>
300+
private void UpdateFixedHtmlView()
301+
{
302+
SetColoredText(_fixedHtmlEditor, HtmlPrettyPrinter.Format(_htmlPanel.GetHtml() ?? string.Empty));
303+
}
304+
291305
/// <summary>
292306
/// Reload the html shown in the html editor by running coloring again.
293307
/// </summary>
@@ -329,10 +343,18 @@ private static void OnLinkClicked(object sender, HtmlLinkClickedEventArgs e)
329343
/// </summary>
330344
private void SetColoredText(string text)
331345
{
332-
var selectionStart = _htmlEditor.SelectionStart;
333-
_htmlEditor.Clear();
334-
_htmlEditor.Rtf = HtmlSyntaxHighlighter.Process(text);
335-
_htmlEditor.SelectionStart = selectionStart;
346+
SetColoredText(_htmlEditor, text);
347+
}
348+
349+
/// <summary>
350+
/// Set html syntax color text on the given RTF html editor.
351+
/// </summary>
352+
private static void SetColoredText(RichTextBox richTextBox, string text)
353+
{
354+
var selectionStart = richTextBox.SelectionStart;
355+
richTextBox.Clear();
356+
richTextBox.Rtf = HtmlSyntaxHighlighter.Process(text ?? string.Empty);
357+
richTextBox.SelectionStart = Math.Min(selectionStart, richTextBox.TextLength);
336358
}
337359

338360
#endregion

0 commit comments

Comments
 (0)