HTML渲染为UWP的原生控件

  1. 1. 没啥用的前言
  2. 2. 尝试搜索
  3. 3. 结构化HTML
  4. 4. 遍历DOM树
  5. 5. RichTextBlock

没啥用的前言

说着再做UWP就剁手,我还是开了一个新坑🤣
这次是B岛UWP端
论坛客户端的一个老大难问题是内容的呈现。论坛一般以网页端为主,网页做好,论坛活跃起来后之后才会开发客户端/有开发者愿意做第三方的客户端。因此,API绝大多数情况是为网页端为一等公民的。此外,各个UI框架展示内容的格式也各有不同。以上两个原因导致HTML被选做富文本展示的通用语言。

对于客户端来说,HTML的呈现就成了问题。可以嵌入浏览器来渲染HTML,但存在两个难以解决的问题

  • 与应用原生部分交互困难,
  • 可能有性能问题。

因此,客户端的做法一般是绕过WebView,将HTML直接渲染为原生控件。那么问题来了,怎么做呢?Android的TextView可以渲染部分HTML,但UWP里就没有相应的API了。//@微软,出来挨打

先来看看手上有什么工具。首先是原生的XAML控件。Windows SDK 1903与.Net starndard 2.0兼容。又有一大堆.NET Standard 2.0的类库可以用了。

API返回的结果是

1
报个BUG。主岛的API p模式的data2,文档说小于1按1处理,实际上取0时返回的是'[',<br /><br />curl http://bog.ac/api/p/0/0<br />]

尝试搜索

搜索一下,找到了WinRT-RichTextBlock.Html2Xaml,它能把HTML渲染到RichTextBlock上,但是很遗憾,它不支持UWP。继续搜索,找到HTML2XAML的一个支持UWP的fork,使用后发现有不支持的标签。查看他的代码,似乎用到了xslt。面对HTML已经够头疼了,还是别引入另一个标记语言了。出师不利。

继续搜索,找到了一个MarkdownTextBlock的库。我以前做另一个论坛的客户端时用过它。当时是先想办法把HTML转成Markdown,再用它来渲染。但是在处理多级嵌套引用(<quote>)的时候会出错。况且时隔这么久我已经看不懂当年写的代码了🤣

通过这两次搜索我们得到了以下信息:

  1. RichTextBlock很可能能够作为我们渲染的容器
  2. HTML标签和使用的原生控件有关
  3. 结构化的输出处理起来更方便,如果能把HTML转化为DOM树,靠dfs就可以实现转换。

第一步,先要把HTML结构化。

结构化HTML

要把某种语言结构化,Parser是不二选择。而HTML的Parser因为经常面对残缺的HTML,通常支持将残缺的片段补齐。AngleSharp就是一个基于.NET Standard的HTML parser。
第一步搞定。有了结构,接下来就顺手多了。

遍历DOM树

上面把HTML转成Markdown的源函数,里面的一大堆分支看的云里雾里。加上奇奇怪怪的边界情况后更是让人头疼。有没有一种代码的组织方法能让我针对一个标签写一个函数?答案是:Visitor pattern. 百科上写的详细的多,我就只举一个例子。假设有<p>,<img>,<a>标签需要解析。定义INode作为所有DOM元素的接口,IVisitor是要对元素访问的接口。INode的实现者通过visitor.Visit(this)把控制流返还给Visitor。只需在visitor上实现对各个类的Visit方法,就达成目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
interface INode
{
void Accept(IVisitor visitor);
}
interface IVisitor
{
void Visit(ANode node);
void Visit(PNode node);
void Visit(ImgNode node);
}
class PNode:INode
{
public IReadonlyList<INode> Children{get;}
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
class ANode:INode
{
public string Href{get;}
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
class ImgNode:INode
{
public string src{get;}
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
public class DOMVisitor:IVisitor
{
public void Visit(PNode node)
{
foreach(var item in node.Children)
item.Accept(this);
}
public void Visit(ANode node)
{
//new Hyper link
}
public void Visit(ImgNode node)
{
//new Image
}
}

RichTextBlock

接下来考虑如何呈现。根据文档RichTextBlock可以包含多个Block,一个Block又可以包含若干Inline。与HTML标签刚好对应!InlineUIContainer自己是Inline,但Child属性可以塞下任何UIElement。如果塞进去另一个RichTextBlock就实现了对引用<quote>呈现。这样可以实现任意级引用<quote>的呈现。具体实现上,提供一个Stack<Block>供使用。转化为Inline的元素每次添加到栈顶的Block中。遇到转化为Block的元素则压栈。最后把Block按照先后顺序加入RichTextBlock即可。

下面是效果,具体代码请参考HTML ParserRichTextBlockRenderer

picture

搞定了一个困扰多年的难题,可喜可贺(^o^)ノ