Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions Source/Demo/Common/HtmlPrettyPrinter.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Utility to format HTML into a readable representation while preserving semantic structure.
/// </summary>
public static class HtmlPrettyPrinter
{
/// <summary>
/// html tags that should be written with indentation on separate lines
/// </summary>
private static readonly HashSet<string> _blockTags = new HashSet<string>(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"
};

/// <summary>
/// html tags that don't have a closing tag and should not change indentation depth
/// </summary>
private static readonly HashSet<string> _voidTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
};

/// <summary>
/// Format the given html into a readable left-to-right comparable layout.
/// </summary>
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();
}

/// <summary>
/// Append the given html token on its own line using the given indentation level.
/// </summary>
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();
}

/// <summary>
/// Append indentation spaces to the string builder.
/// </summary>
private static void AppendIndent(StringBuilder builder, int indent)
{
builder.Append(' ', indent * 2);
}

/// <summary>
/// Return if the builder currently points at the start of a new line.
/// </summary>
private static bool IsAtLineStart(StringBuilder builder)
{
return builder.Length == 0 || builder[builder.Length - 1] == '\n';
}
}
}
52 changes: 42 additions & 10 deletions Source/Demo/WPF/MainControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,48 @@
<WebBrowser x:Name="_webBrowser" Grid.Column="2" Visibility="Collapsed"/>
</Grid>
<GridSplitter Grid.Row="1" HorizontalAlignment="Stretch" Height="4" Background="#BFDBFF" />
<xctk:RichTextBox x:Name="_htmlEditor" Grid.Row="2" VerticalScrollBarVisibility="Visible" BorderThickness="0"
TextChanged="OnHtmlEditor_TextChanged" />
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="150" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" MinWidth="150" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" VerticalAlignment="Center">Input HTML</Label>
<Label Grid.Column="1" Margin="0,0,10,0" VerticalAlignment="Center" FontSize="11" Foreground="Blue" Cursor="Hand"
MouseLeftButtonUp="OnRefreshLink_MouseLeftButtonUp">
<Underline>Refresh</Underline>
</Label>
<CheckBox x:Name="_coloredCheckBox" Grid.Column="2" VerticalAlignment="Center" FontSize="11" Foreground="Blue" Cursor="Hand"
Click="OnColoredCheckbox_click">
<Underline>Colored</Underline>
</CheckBox>
</Grid>
<xctk:RichTextBox x:Name="_htmlEditor" Grid.Row="1" BorderThickness="0"
VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Visible"
TextChanged="OnHtmlEditor_TextChanged" />
</Grid>
<GridSplitter Grid.Column="1" Width="4" VerticalAlignment="Stretch" ResizeBehavior="PreviousAndNext" Background="#BFDBFF" />
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Grid.Row="0" VerticalAlignment="Center">Fixed HTML</Label>
<xctk:RichTextBox x:Name="_fixedHtmlEditor" Grid.Row="1" BorderThickness="0" IsReadOnly="True"
VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Visible" />
</Grid>
</Grid>
</Grid>
<Label HorizontalAlignment="Right" Margin="0,0,16,14" VerticalAlignment="Bottom" Grid.Column="1" FontSize="11" Foreground="Blue" Cursor="Hand"
MouseLeftButtonUp="OnRefreshLink_MouseLeftButtonUp">
<Underline>Refresh</Underline>
</Label>
<CheckBox x:Name="_coloredCheckBox" HorizontalAlignment="Right" Margin="0,0,22,2" VerticalAlignment="Bottom" Grid.Column="1" FontSize="11" Foreground="Blue" Cursor="Hand"
Click="OnColoredCheckbox_click">
<Underline>Colored</Underline>
</CheckBox>
</Grid>
</UserControl>
26 changes: 25 additions & 1 deletion Source/Demo/WPF/MainControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public string GetHtml()
public void SetHtml(string html)
{
_htmlPanel.Text = html;
UpdateFixedHtmlView();
if (string.IsNullOrWhiteSpace(html))
{
_htmlPanel.InvalidateMeasure();
Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -307,6 +312,23 @@ private string GetFixedHtml()
return html;
}

/// <summary>
/// Update the fixed html shown in the read-only editor.
/// </summary>
private void UpdateFixedHtmlView()
{
SetFixedHtmlText(HtmlPrettyPrinter.Format(_htmlPanel.GetHtml() ?? string.Empty), _coloredCheckBox.IsChecked.GetValueOrDefault(false));
}

/// <summary>
/// Set formatted HTML text in the fixed HTML viewer.
/// </summary>
private void SetFixedHtmlText(string text, bool color = true)
{
text = text ?? string.Empty;
_fixedHtmlEditor.Text = color ? HtmlSyntaxHighlighter.Process(text) : text.Replace("\n", "\\par ");
}

/// <summary>
/// Reload the html shown in the html editor by running coloring again.
/// </summary>
Expand All @@ -320,7 +342,9 @@ private void OnRefreshLink_MouseLeftButtonUp(object sender, MouseButtonEventArgs
/// </summary>
private void OnColoredCheckbox_click(object sender, RoutedEventArgs e)
{
SetColoredText(GetHtmlEditorText(), _coloredCheckBox.IsChecked.GetValueOrDefault(false));
var color = _coloredCheckBox.IsChecked.GetValueOrDefault(false);
SetColoredText(GetHtmlEditorText(), color);
UpdateFixedHtmlView();
}

/// <summary>
Expand Down
Loading
Loading