Flutter Markdown Widget Class 源碼解析
Flutter 中的 MarkdownWidget
是提供即時預覽的 Widget,該套件使用 markdown 解析產生出 HTML 標籤,然後再依據 HTML 標籤產生出對應的 Widget
,本文解釋了大部分的邏輯。由於我感興趣的部分是學習渲染部分並來實作 org-mode 的即時預覽,所以 Toc 跟滾動的細節本文略過。
MarkdownWidget
MarkdownWidget
最後的產出是一個 ListView
,而 MarkdownGenerator
負責解析 markdown 字串,建立成對應的 Widget
, 儲存 SplayTreeSet
中,最後透過 ListView
組合。 SplayTreeSet
是一個可以相對排序的集合,基於平衡樹(Self-balancing binary search tree),允許大多數操作在對數時間完成。
整理相關類圖如下:
classDiagram class MarkdownWidget { } class _MarkdownWidgetState { } class SelectionArea { } class AutoScroll { } class VisibilityDetector { } class MarkdownGenerator { } class SplayTreeSet { } class ListView { } MarkdownWidget --> _MarkdownWidgetState _MarkdownWidgetState --> MarkdownGenerator MarkdownGenerator --> "產生" SplayTreeSet _MarkdownWidgetState --> "產生" SelectionArea SelectionArea *-- AutoScroll AutoScroll *-- VisibilityDetector VisibilityDetector *-- ListView ListView --> "使用" SplayTreeSet
核心邏輯在 BuildMarkdownWidget
。
Widget buildMarkdownWidget() {
final markdownWidget = NotificationListener<UserScrollNotification>(
onNotification: (notification) {
final ScrollDirection direction = notification.direction;
isForward = direction == ScrollDirection.forward;
return true;
},
child: ListView.builder(
shrinkWrap: widget.shrinkWrap,
physics: widget.physics,
controller: controller,
itemBuilder: (ctx, index) => wrapByAutoScroll(index,
wrapByVisibilityDetector(index, _widgets[index]), controller),
itemCount: _widgets.length,
padding: widget.padding,
),
);
return widget.selectable
? SelectionArea(child: markdownWidget)
: markdownWidget;
}
MarkdownConfig
MarkdownConfig
用來拿 WidgetConfig
。 WidgetConfig
用來設定 MarkdownTag
產生的 Widget
對應的 style config。
MarkdownGenerator
classDiagram %% Package: markdown_widget class MarkdownGenerator { } class MarkdownGeneratorConfig { } class WidgetVisitor { } class WidgetConfig { } class SpanNode { } class MarkdownTag { } %% Package: markdown class Document { } %% Relationships MarkdownGenerator --> MarkdownGeneratorConfig : "" MarkdownGenerator --> WidgetVisitor : "" WidgetVisitor --> Document : "" WidgetVisitor --> SpanNode : "" WidgetVisitor --> WidgetConfig : "" WidgetVisitor --> MarkdownTag : ""
首先用 Document.parseLine
解析出 Nodes,透過 WidgetVisitor
再根據 MarkdownTag
去呼叫對應的 NodeGenerator
,產生對應的 SpanNode
,最後再組合起來。WidgetVisitor
是採用 Visitor Pattern 方式實作。
///convert [data] to widgets
///[onTocList] can provider [Toc] list
List<Widget> buildWidgets(String data,
{ValueCallback<List<Toc>>? onTocList}) {
final m.Document document = m.Document(
extensionSet: m.ExtensionSet.gitHubFlavored,
encodeHtml: false,
inlineSyntaxes: inlineSyntaxes,
blockSyntaxes: blockSyntaxes,
);
final List<String> lines = data.split(RegExp(r'(\r?\n)|(\r?\t)|(\r)'));
final List<m.Node> nodes = document.parseLines(lines);
final List<Toc> tocList = [];
final visitor = WidgetVisitor(
config: config,
generators: generators,
textGenerator: textGenerator,
onNodeAccepted: (node, index) {
onNodeAccepted?.call(node, index);
if (node is HeadingNode) {
final listLength = tocList.length;
tocList.add(
Toc(node: node, widgetIndex: index, selfIndex: listLength));
}
});
final spans = visitor.visit(nodes);
onTocList?.call(tocList);
final List<Widget> widgets = [];
spans.forEach((span) {
widgets.add(Padding(
padding: linesMargin,
child: Text.rich(span.build()),
));
});
return widgets;
}
WidgetConfig
WidgetConfig
主要的子類是 BlockConfig
,InlineConfig
跟 LeafConfig
。 這三者都是抽象類別。下面列出 code tag 的 config 實現。
///config class for code, tag: code
class CodeConfig implements InlineConfig {
final TextStyle style;
const CodeConfig(
{this.style = const TextStyle(backgroundColor: Color(0xffeff1f3))});
static CodeConfig get darkConfig =>
CodeConfig(style: const TextStyle(backgroundColor: Color(0xff555555)));
@nonVirtual
@override
String get tag => MarkdownTag.code.name;
}
SpanNodGeneratorWithTag
指定 Generator 要跟哪個 tag 綁一起。
ElementNode
ElementNode
是 SpanNode
的子類,允許接收其他 SpanNode
當成 children。在處理 ordered list
這種 block,或是處理文字裡面的連結時會用到 SpanNode
。
整理類圖如下:
classDiagram %% Package: markdown_widget/lib/widgets/span_node.dart class SpanNode { <<abstract<< } class ElementNode { <<abstract<< } SpanNode <|-- ElementNode %% Package: markdown_widget/lib/widgets/blocks/all.dart class TableNode { } class ListNode { } class BlackquoteNode { } class CodeBlockNode { } class HeadingNode { } class HrNode { } class LinkNode { } class ParagraphNode { } class CodeNode { } class ImgNode { } class InputNode { } ElementNode <|-- TableNode ElementNode <|-- ListNode ElementNode <|-- BlackquoteNode ElementNode <|-- CodeBlockNode ElementNode <|-- HeadingNode ElementNode <|-- HrNode ElementNode <|-- LinkNode ElementNode <|-- ParagraphNode ElementNode <|-- CodeNode ElementNode <|-- ImgNode ElementNode <|-- InputNode
舉例來說,下面這個包含連結的字串
aaaa link
就會變成一個 Node,裡面包函 aaaa
、' '、 link
三個 Node。
ConcreteElementNode
這是一個特殊的 ElementNode
,用來表示 Empty tag ,同時也用來表示 plain-text object。
///the default concrete node for ElementNode
class ConcreteElementNode extends ElementNode {
final String tag;
final TextStyle style;
ConcreteElementNode({this.tag = '', TextStyle? style})
: this.style = style ?? const TextStyle();
@override
InlineSpan build() => childrenSpan;
}
ImgNode
ImgNode
是另一個特殊的 Node,使用 WidgetSpan
組合文字跟圖片。可參閱 Flutter RichText 使用案例解析 Flutter WidgetSpan 设置图片显示一文,有比較完整的介紹。
MarkdownTag
結論
要實現 org-mode 即時預覽, 我們需要另外改寫
- markdown.MarkdownWidget
- markdown.Node
- markdown_widget.MarkdownTag
- markdown_widget.WidgetVisitor
SpanNodeGenerator
則可以重複使用。至於 org-mode 語法 parser 的部分,可以透過實作自己的 BlockSyntax
跟 Inline subclasses
,然後使用 Document
跟 NodeVisitor
的完整邏輯。