Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UWP #3

Open
deakjahn opened this issue Jun 14, 2018 · 2 comments
Open

UWP #3

deakjahn opened this issue Jun 14, 2018 · 2 comments

Comments

@deakjahn
Copy link

It really depends on how ambitious you want to be. If a very simple solution is needed for basic texts (p, b, i, br and similar), then using the idea at http://www.visuallylocated.com/post/2015/01/16/Displaying-HTML-content-in-a-TextBlock.aspx is enough. Basically:

using System.ComponentModel;
using System.Xml.Linq;
using XXX.Controls;
using XXX.UWP.Controls;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Documents;
using Xamarin.Forms;
using Xamarin.Forms.Internals;
using Xamarin.Forms.Platform.UWP;
using Span = Windows.UI.Xaml.Documents.Span;

[assembly: ExportRenderer(typeof(HtmlLabel), typeof(HtmlLabelRenderer))]
namespace XXX.UWP.Controls {

  [Preserve(AllMembers = true)]
  public class HtmlLabelRenderer : LabelRenderer {

    protected override void OnElementChanged(ElementChangedEventArgs<Label> e) {
      base.OnElementChanged(e);

      if (Control != null && Element != null && !string.IsNullOrWhiteSpace(Element.Text))
        ConvertHtml(Element.Text ?? string.Empty, Control);
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
      base.OnElementPropertyChanged(sender, e);

      if (e.PropertyName == Label.TextProperty.PropertyName && Control != null && Element != null && !string.IsNullOrWhiteSpace(Element.Text))
        ConvertHtml(Element.Text ?? string.Empty, Control);
    }

    private void ConvertHtml(string html, TextBlock control) {
      control.Inlines.Clear();
      try {
        ParseText(XElement.Parse($"<div>{html}</div>"), control.Inlines);
      }
      catch {
        Control.Text = html;
      }
    }

    private bool AddLineBreakIfNeeded(InlineCollection inlines) {
      if (inlines.Count > 0) {
        var lastInline = inlines[inlines.Count - 1];
        while ((lastInline is Span)) {
          var span = (Span)lastInline;
          if (span.Inlines.Count > 0)
            lastInline = span.Inlines[span.Inlines.Count - 1];
        }
        if (!(lastInline is LineBreak)) {
          inlines.Add(new LineBreak());
          return true;
        }
      }
      return false;
    }

    private void ParseText(XElement element, InlineCollection inlines) {
      if (element != null) {
        var currentInlines = inlines;
        string elementName = element.Name.ToString().ToLowerInvariant();
        switch (elementName) {
          case "a":
            var link = new Hyperlink();
            inlines.Add(link);
            currentInlines = link.Inlines;
            break;
          case "b":
          case "strong":
            var bold = new Bold();
            inlines.Add(bold);
            currentInlines = bold.Inlines;
            break;
          case "i":
          case "em":
            var italic = new Italic();
            inlines.Add(italic);
            currentInlines = italic.Inlines;
            break;
          case "u":
            var underline = new Underline();
            inlines.Add(underline);
            currentInlines = underline.Inlines;
            break;
          case "br":
            inlines.Add(new LineBreak());
            break;
          case "p":
            // Add two line breaks, one for the current text and the second for the gap.
            if (AddLineBreakIfNeeded(inlines))
              inlines.Add(new LineBreak());
            var paragraphSpan = new Span();
            inlines.Add(paragraphSpan);
            currentInlines = paragraphSpan.Inlines;
            break;
          case "li":
            inlines.Add(new LineBreak());
            inlines.Add(new Run { Text = " • " });
            break;
          case "ul":
          case "div":
            AddLineBreakIfNeeded(inlines);
            var divSpan = new Span();
            inlines.Add(divSpan);
            currentInlines = divSpan.Inlines;
            break;
        }

        foreach (var node in element.Nodes()) {
          if (node is XText textElement)
            currentInlines.Add(new Run { Text = textElement.Value });
          else
            ParseText(node as XElement, currentInlines);
        }

        if (elementName == "p")
          currentInlines.Add(new LineBreak());
      }
    }

  }
}

@deakjahn
Copy link
Author

deakjahn commented Jun 14, 2018

But if you want the full treatment, then the widely used MS code of https://github.com/Microsoft/WPF-Samples/blob/master/Sample%20Applications/HtmlToXamlDemo/HtmlToXamlConverter.cs can be used. The conversion is basically similar:

string xaml;
InlineCollection xamLines;
try {
  xaml = HtmlToXamlConverter.ConvertHtmlToXaml(html, false);
  xamLines = ((XamlReader.Parse(xaml) as Section).Blocks.FirstBlock as Paragraph).Inlines;
}
catch {
  return;
}

var clone = new Inline[xamLines.Count];
xamLines.CopyTo(clone, 0);

control.Inlines.Clear();
foreach (var line in clone)
  control.Inlines.Add(line);

@deakjahn
Copy link
Author

deakjahn commented Jun 15, 2018

A modification to handle HTML entities:

  public class HtmlLabelRenderer : LabelRenderer {
    private static readonly Dictionary<string, string> HtmlEntities = new Dictionary<string, string> {
      { "nbsp", "&#160;" },
      { "lt", "&#60;" },
      { "gt", "&#62;" },
      { "amp", "&#38;" },
      { "quot", "&#34;" },
      { "apos", "&#39;" },
      { "cent", "&#162;" },
      { "pound", "&#163;" },
      { "yen", "&#165;" },
      { "euro", "&#8364;" },
      { "copy", "&#169;" },
      { "reg", "&#174;" },
    };
    private static readonly string HtmlEntitiesDtd;

    static HtmlLabelRenderer() {
      string list = string.Join("", HtmlEntities.Select(ent => $"<!ENTITY {ent.Key} \"{ent.Value}\">"));
      HtmlEntitiesDtd = $"<!DOCTYPE html [{list}]>";
    }

And process the HTML like this:

      try {
        var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse };
        using (var xml = new StringReader(HtmlEntitiesDtd + html))
        using (var reader = XmlReader.Create(xml, settings)) {
          var doc = XDocument.Load(reader);
          ParseText(doc.Root, control.Inlines);
        }
      }
      catch {
        Control.Text = html;
      }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant