markdown support for talk messages

This commit is contained in:
2026-08-16 21:35:41 +02:00
parent bd83a88e70
commit 02a7b87b5b
14 changed files with 379 additions and 19 deletions
@@ -0,0 +1,76 @@
import 'package:markdown/markdown.dart' as md;
/// The Markdown feature set the app renders for Talk messages — CommonMark plus
/// the GFM extensions the official Talk clients enable.
///
/// It deliberately omits `InlineHtmlSyntax` (raw HTML stays literal), footnotes,
/// alert blocks, color swatches and `:shortcode:` emoji — matching Talk's
/// restrictions. Shared by the renderer ([MessageMarkdown]) and the plain-text
/// stripper below so both agree on what counts as Markdown.
final md.ExtensionSet talkMarkdownExtensionSet = md.ExtensionSet(
<md.BlockSyntax>[
const md.FencedCodeBlockSyntax(),
const md.TableSyntax(),
const md.UnorderedListWithCheckboxSyntax(),
const md.OrderedListWithCheckboxSyntax(),
],
<md.InlineSyntax>[
md.StrikethroughSyntax(),
md.AutolinkExtensionSyntax(),
],
);
/// Block-level tags after which running text would otherwise glue together;
/// their boundaries become a single space in the flattened preview.
const Set<String> _blockTags = {
'p',
'li',
'blockquote',
'pre',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'br',
'hr',
'tr',
'th',
'td',
};
/// Flattens a Markdown [source] into a single line of readable plain text —
/// markers (`**`, `#`, `` ` ``, list bullets, link/image syntax …) removed,
/// links reduced to their label and images to their alt text. Used for the
/// one-line previews (reply quote, conversation list) where Talk also strips
/// formatting rather than rendering it.
String markdownToPlainText(String source) {
final nodes = md.Document(
extensionSet: talkMarkdownExtensionSet,
encodeHtml: false,
).parse(source);
final buffer = StringBuffer();
_collect(nodes, buffer);
return buffer.toString().replaceAll(RegExp(r'\s+'), ' ').trim();
}
void _collect(List<md.Node> nodes, StringBuffer buffer) {
for (final node in nodes) {
if (node is md.Text) {
buffer.write(node.text);
} else if (node is md.Element) {
// Images carry their text in the `alt` attribute, not in child nodes.
if (node.tag == 'img') {
buffer.write(node.attributes['alt'] ?? '');
continue;
}
final isBlock = _blockTags.contains(node.tag);
if (isBlock) buffer.write(' ');
final children = node.children;
if (children != null) _collect(children, buffer);
if (isBlock) buffer.write(' ');
}
}
}