flutter_markdownでカスタムのブロックタグを実装する方法
はじめに
flutter_markdown | Flutter package
flutter_markdownとは、FlutterでMarkdown形式のテキストを表示するためのライブラリです。Markdown記法で記述されたテキストを簡単に変換・表示できるのが特徴です。
ただし、Markdown記法に含まれない表現を実現したい場合には、追加の方法が必要です。
Markdown記法には、ブロックタグとインラインタグの2種類があります。
本記事では、カスタムのブロックタグについて取り上げます。インラインタグについては、別の機会に詳しく説明します。
今回作るもの
<embed_link title="..." description="..." image_url="..." favicon_url="..." url="..." />
今回は上記の文字列を下の画像のような埋め込みリンクになるように作っていきます。

それぞれの要素名を下記として扱っていきます。

実装方法
MarkdownBodyの説明
FlutterでMarkdownを表示するためには、 MarkdownBody を使用します。このWidgetを利用することで、Markdown形式のテキストを簡単にレンダリングできます。
また、ブロックタグをカスタマイズして表示するためには、以下の2つのプロパティを設定する必要があります:
- blockSyntaxes
- builders
以下はその基本的なコード例です:
return fmd.MarkdownBody(
// 表示するMarkdownの文字列
data: content,
blockSyntaxes: <BlockSyntax>[
// ...
],
builders: <String, MarkdownElementBuilder>{
// ...
},
);
blockSyntaxesの説明
blockSyntaxes は、以下のように定義されています:
/// Collection of custom block syntax types to be used parsing the Markdown data.
final List<md.BlockSyntax>? blockSyntaxes;
このプロパティは、Markdownデータをパースする際に使用するカスタムブロックタグのコレクションを指定するためのものです。
簡単に言えば、data に渡した文字列を解析し、特定のブロックタグとして解釈するために必要な設定を行う役割を担います。
buildersの説明
builders は、以下のように定義されています:
/// Render certain tags, usually used with [extensionSet]
///
/// For example, we will add support for `sub` tag:
///
/// ```dart
/// builders: {
/// 'sub': SubscriptBuilder(),
/// }
/// ```
///
/// The `SubscriptBuilder` is a subclass of [MarkdownElementBuilder].
final Map<String, MarkdownElementBuilder> builders;
このプロパティは、特定のタグをレンダリングする際に必要な設定を行うためのものです。
例えば、sub タグをサポートするには、次のように SubscriptBuilder を指定します:
builders: {
'sub': SubscriptBuilder(),
}
なお、SubscriptBuilder は MarkdownElementBuilder を継承したクラスです。
※ builders は、blockSyntaxes で設定したタグをレンダリングする場合に使用されますが、blockSyntaxes を使用しないタグのカスタマイズにも活用できます。
blockSyntaxesの実装
以下は、 blockSyntaxes にカスタムのブロックタグを追加する例です。
import 'package:flutter_markdown/flutter_markdown.dart' as fmd;
import 'package:markdown/markdown.dart' as md;
class EmbedLinkSyntax extends md.BlockSyntax {
const EmbedLinkSyntax();
@override
RegExp get pattern => RegExp(
r'<embed_link\s+(?=.*title="([^"]+)")(?=.*description="([^"]+)")(?=.*image_url="([^"]+)")(?=.*favicon_url="([^"]+)")(?=.*url="([^"]+)").*?\/?>',
);
@override
md.Node? parse(md.BlockParser parser) {
final match = pattern.firstMatch(parser.current.content);
if (match != null) {
final title = match.group(1) ?? '';
final description = match.group(2) ?? '';
final imageUrl = match.group(3) ?? '';
final faviconUrl = match.group(4) ?? '';
final url = match.group(5) ?? '';
// カスタムノードを作成
final element = md.Element('embed_link', []);
element.attributes.addAll({
'title': title,
'description': description,
'image_url': imageUrl,
'favicon_url': faviconUrl,
'url': url,
});
// パーサーを次の行に進める
parser.advance();
return element;
}
return null;
}
}
1. BlockSyntaxを継承したクラスの作成
カスタムのタグを追加するために、BlockSyntax を継承したクラスを作成します。
2. patternの定義
パース時にカスタムタグ embed_link を識別するための正規表現を定義します:
@override
RegExp get pattern => RegExp(
r'<embed_link\s+(?=.*title="([^"]+)")(?=.*description="([^"]+)")(?=.*image_url="([^"]+)")(?=.*favicon_url="([^"]+)")(?=.*url="([^"]+)").*?\/?>',
);
正規表現の説明
- <embed_link\s+
- タグ名が embed_link で始まり、続いて1つ以上の空白文字があることを確認します。
- (?=.*title="([^"]+)")
- title 属性が存在し、その値をキャプチャします。?= はポジティブ・ルックアヘッドで条件の存在を確認します。
- 同様に、description、image_url、favicon_url、url 属性の存在と値をキャプチャします。
- .*?/?>
- 任意の文字列が続き、自己終了タグ(/>)または終了タグ(>)で終わることを確認します。
※ title、description、image_url、favicon_url、url は順不同で記述可能です。
3. parseメソッドの実装
pattern に一致するタグから値を取得し、カスタムノード(Element)を作成します:
@override
md.Node? parse(md.BlockParser parser) {
final match = pattern.firstMatch(parser.current.content);
if (match != null) {
final title = match.group(1) ?? '';
final description = match.group(2) ?? '';
final imageUrl = match.group(3) ?? '';
final faviconUrl = match.group(4) ?? '';
final url = match.group(5) ?? '';
// カスタムノードを作成
final element = md.Element('embed_link', []);
element.attributes.addAll({
'title': title,
'description': description,
'image_url': imageUrl,
'favicon_url': faviconUrl,
'url': url,
});
// パーサーを次の行に進める
parser.advance();
return element;
}
return null;
}
- 正規表現で一致した値を取得し、
Elementを生成します。 Elementのタグ名はembed_linkに設定します(後でMarkdownBodyのbuildersにセットするため)。- 必要な値を
attributesに追加し、次の行へ進めます。
これで、blockSyntaxes 側の実装は完了です。
MarkdownElementBuilderの実装
次に、 MarkdownBody の builders に設定する MarkdownElementBuilder の実装について解説します。
class EmbedLinkBuilder extends fmd.MarkdownElementBuilder {
@override
bool isBlockElement() => true;
@override
Widget? visitElementAfterWithContext(
BuildContext context,
md.Element element,
TextStyle? preferredStyle,
TextStyle? parentStyle,
) {
final title = element.attributes['title'];
final description = element.attributes['description'];
final imageUrl = element.attributes['image_url'];
final faviconUrl = element.attributes['favicon_url'];
final url = element.attributes['url'];
// 表示するWidgetを作成
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ...
);
}
}
1. MarkdownElementBuilderを継承したクラスを作成
カスタム要素をレンダリングするために、 MarkdownElementBuilder を継承したクラスを作成します。
2. isBlockElementのオーバーライド
ブロック要素であることを指定するために、 isBlockElement メソッドを true に設定します:
@override
bool isBlockElement() => true;
3. visitElementAfterWithContextの実装
カスタムタグがレンダリングされる際に表示するWidgetを定義します:
@override
Widget? visitElementAfterWithContext(
BuildContext context,
md.Element element,
TextStyle? preferredStyle,
TextStyle? parentStyle,
) {
final title = element.attributes['title'];
final description = element.attributes['description'];
final imageUrl = element.attributes['image_url'];
final faviconUrl = element.attributes['favicon_url'];
final url = element.attributes['url'];
// 表示するWidgetを作成
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ...
);
}
表示内容の作成手順
EmbedLinkSyntaxのparseメソッドで設定したElementのattributesから必要なデータを取得します- 必要なデータを基に表示用のWidgetを作成します
- (例:title, description, imageUrl, faviconUrl, url など)
これで MarkdownElementBuilder の実装は完了です。
MarkdownBodyの実装
これまでに作成した EmbedLinkSyntax と EmbedLinkBuilder を MarkdownBody にセットします。
return fmd.MarkdownBody(
blockSyntaxes: const [
EmbedLinkSyntax(),
],
builders: {
'embed_link': EmbedLinkBuilder(),
},
);
設定内容
blockSyntaxesEmbedLinkSyntaxを追加し、カスタムのブロックタグを認識できるようにします。
builders- タグ名 'embed_link' に対応する
EmbedLinkBuilderを設定し、レンダリング時に適切なWidgetを生成します。
- タグ名 'embed_link' に対応する
これでカスタムブロックタグとそのレンダリング処理を含む MarkdownBody の実装が完成です!
補足
補足1: embed_linkの内容について
今回、embed_link には title、description、image_url、favicon_url、url など、表示に必要な値を直接設定しています。
しかし、より適切な方法として、embed_link には url のみを設定し、EmbedLinkBuilder 側で url を基にスクレイピングを行い、表示に必要なデータを取得する方法があります。この方法のほうが柔軟性や拡張性に優れています。
今回その方法を採用しなかった理由
Flutter Web では CORS(Cross-Origin Resource Sharing) の問題があるため、スクレイピングによるデータ取得を実現するのは困難でした。
以下のリンク先には解決方法が記載されていますが、SDKの直接変更が必要だったため、今回は断念しました。
How to solve flutter web api cors error only with dart code? - Stack Overflow
補足2: embed_link に必要な値の取得について
補足1で述べた通り、 embed_link には表示に必要なすべての値を直接指定していますが、毎回手動で値を取得するのは手間です。そのため、コマンドラインツールを作成して効率化しました。このツールでは、 html と http パッケージを利用して必要なデータを自動取得します。
コマンドツールコード
// ignore_for_file: avoid_print
import 'package:html/parser.dart' show parse;
import 'package:http/http.dart' as http;
/// dart scraping_site.dart <url>
Future<void> main(List<String> args) async {
if (args.isEmpty) {
print('Usage: dart scraper.dart <url>');
return;
}
final url = args[0];
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final document = parse(response.body);
// Title
final title = document.querySelector('title')?.text ?? '';
// Description
final description = document.querySelector('meta[name="description"]')?.attributes['content'] ?? '';
// Favicon
final favicon = document.querySelector('link[rel~="icon"]')?.attributes['href'] ?? '';
// OGP Image
final ogImage = document.querySelector('meta[property="og:image"]')?.attributes['content'] ?? '';
print(
'<embed_link title="$title" description="$description" image_url="$ogImage" favicon_url="$favicon" url="$url" />',
);
} else {
print('Failed: Status code: ${response.statusCode}');
}
} on Exception catch (e) {
print('Error: $e');
}
}
コードの説明
- URLの指定
- コマンドライン引数で対象のURLを指定します(例:dart scraping_site.dart :url)。
- スクレイピングの実行
- 指定されたURLのHTMLデータを取得し、title、description、favicon_url、image_url を解析します。
- 結果の出力
- 必要なデータを埋め込んだ
embed_linkタグ形式で標準出力に表示します。
- 必要なデータを埋め込んだ
これにより、対象サイトの情報を簡単に取得し、embed_link タグを生成することが可能になります。
最後に
カスタムのブロックタグの実装方法について解説しました。これでMarkdownの表示に自由度が増したと思います。
別の機会にカスタムのインラインタグの実装方法について書きたいと思います。