/// <summary> /// The bound HtmlDocument changed. Map the HTML nodes into XAML nodes and build the corresponding /// RichTextBox for display. /// </summary> private static void FormattedTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { // TODO, Split up long boxes so that they are under height 2000px RichTextBox textBlock = sender as RichTextBox; textBlock.Blocks.Clear(); HtmlDocument doc = e.NewValue as HtmlDocument; if (doc == null) { return; } Paragraph para = new Paragraph(); foreach (var node in doc.DocumentNode.ChildNodes) { HtmlNode _node = node; // Hack to allow nested font tag if (_node.Name == "font" && _node.FirstChild != null) { _node = node.FirstChild; } // Hack to allow strange quotes (nested in a span class quote tag) else if (_node.Name == "span" && _node.Attributes.Contains("class") && _node.Attributes["class"].Value == "quote" && _node.HasChildNodes && _node.FirstChild.Name == "a" && _node.FirstChild.Attributes.Contains("class") && _node.FirstChild.Attributes["class"].Value == "quotelink" && _node.FirstChild.Attributes.Contains("href")) { _node = _node.FirstChild; } if (_node.Name == "br") { para.Inlines.Add(new LineBreak()); } // Regular quote else if (_node.Name == "a" && _node.Attributes.Contains("class") && _node.Attributes["class"].Value == "quotelink" && _node.Attributes.Contains("href")) { Match m = r.Match(_node.Attributes["href"].Value); // Thread quote if (m.Success) { Hyperlink h = new Hyperlink(); h.Click += (hsender, he) => { if (textBlock.DataContext is PostViewModel) { // Because this click is *not* a routed event, we can't cancel it or // mark it as handled and it will actually bubble down to the child post // grid and hit that tap event too. To work around this we set the rootframe // hit test property as a sentinel value to signal that tap handler to ignore // the invocation. App.IsPostTapAllowed = false; PostViewModel pvm = (PostViewModel)textBlock.DataContext; pvm.QuoteLinkTapped(m.Groups[2].Value, m.Groups[3].Value, ulong.Parse(m.Groups[4].Value)); // We clear this sentinel property on the dispatcher, since the routed tap events actually get queued on // the dispatcher and don't happen until this current function returns. Deployment.Current.Dispatcher.BeginInvoke(() => App.IsPostTapAllowed = true); } }; h.Inlines.Add(new Run() { Foreground = App.Current.Resources["LinkBrush"] as SolidColorBrush, Text = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",") }); para.Inlines.Add(h); } else { Match m3 = r3.Match(_node.Attributes["href"].Value); if (m3.Success) { Hyperlink h = new Hyperlink(); h.Click += (hsender, he) => { if (textBlock.DataContext is PostViewModel) { App.IsPostTapAllowed = false; (textBlock.DataContext as PostViewModel).BoardLinkTapped(m3.Groups[1].Value + m3.Groups[2].Value); Deployment.Current.Dispatcher.BeginInvoke(() => App.IsPostTapAllowed = true); } }; h.Inlines.Add(new Run() { Foreground = App.Current.Resources["LinkBrush"] as SolidColorBrush, Text = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",") }); para.Inlines.Add(h); } else { Match m2 = r2.Match(_node.Attributes["href"].Value); // Text board quote? if (m2.Success) { para.Inlines.Add(WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",")); } else { //Debug.WriteLine(_node.OuterHtml); para.Inlines.Add(WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",")); } } } } // Dead Quote else if (_node.Name == "span" && _node.Attributes.Contains("class") && _node.Attributes["class"].Value == "quote deadlink") { para.Inlines.Add(new Run() { Foreground = App.Current.Resources["GreentextBrush"] as SolidColorBrush, Text = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",") }); } // Greentext else if (_node.Name == "span" && _node.Attributes.Contains("class") && _node.Attributes["class"].Value == "quote") { para.Inlines.Add(new Run() { Foreground = App.Current.Resources["GreentextBrush"] as SolidColorBrush, Text = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",") }); } // Dead Quote 2# else if (_node.Name == "span" && _node.Attributes.Contains("class") && _node.Attributes["class"].Value == "quote deadlink") { para.Inlines.Add(new Run() { Foreground = App.Current.Resources["GreentextBrush"] as SolidColorBrush, Text = WebUtility.HtmlDecode(_node.ChildNodes[0].InnerText).Replace("'", "'").Replace(",", ",") }); } // Banned text else if ((_node.Name == "b" || _node.Name == "strong") && _node.Attributes.Contains("style") && _node.Attributes["style"].Value.Replace(" ", "") == "color:red;") { para.Inlines.Add(new Run() { Foreground = App.Current.Resources["BannedBrush"] as SolidColorBrush, Text = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",") }); } // Spoiler else if (_node.Name == "s") { Hyperlink h = new Hyperlink() { TextDecorations = null }; int len = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",").Length; // To simulate spoiler display, we just put the unicode wide block symbol over and over in a spoiler color, // since we can't actually highlight a region of the text or obscure it normally. Run r = new Run() { Foreground = App.Current.Resources["SpoilerBrush"] as SolidColorBrush, Text = new String('\u2588', len), FontSize = CriticalSettingsManager.Current.TextSize }; bool isClicked = false; h.Click += (hsender, he) => { if (isClicked) { return; } isClicked = true; App.IsPostTapAllowed = false; r.Text = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ","); r.FontSize = CriticalSettingsManager.Current.TextSize; h.TextDecorations = null; Deployment.Current.Dispatcher.BeginInvoke(() => App.IsPostTapAllowed = true); }; h.Inlines.Add(r); para.Inlines.Add(h); } //Hyperlink else if (_node.Name == "a" && _node.Attributes.Contains("href")) { Hyperlink h = new Hyperlink() { NavigateUri = new Uri(_node.Attributes["href"].Value), TargetName = "_blank" }; h.Click += (hsender, he) => { App.IsPostTapAllowed = false; Deployment.Current.Dispatcher.BeginInvoke(() => App.IsPostTapAllowed = true); }; h.Inlines.Add(new Run() { Foreground = App.Current.Resources["LinkBrush"] as SolidColorBrush, Text = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",") }); para.Inlines.Add(h); } // Bold else if (_node.Name == "b" || _node.Name == "strong") { Bold b = new Bold(); b.Inlines.Add(WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",")); para.Inlines.Add(b); } // Underline else if (_node.Name == "u") { Underline u = new Underline(); u.Inlines.Add(WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",")); para.Inlines.Add(u); } else if (_node.Name == "small") { para.Inlines.Add(new Run() { Text = WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ","), FontSize = CriticalSettingsManager.Current.TextSize - 3 }); } // Regular text else if (_node.Name == "#text") { // para.Inlines.Add(WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",")); List <Inline> inlines = MarkupLinks(WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",")); foreach (Inline inline in inlines) { para.Inlines.Add(inline); } } else { //Debug.WriteLine(_node.OuterHtml); para.Inlines.Add(WebUtility.HtmlDecode(_node.InnerText).Replace("'", "'").Replace(",", ",")); } } textBlock.Blocks.Add(para); }