new linkify handling

Should fix mentions in non-HTML messages
This commit is contained in:
Henry Hiles 2026-11-01 16:42:23 -05:00
commit 40f5d0f459
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
2 changed files with 123 additions and 42 deletions

View file

@ -0,0 +1,45 @@
import "package:html/dom.dart";
import "package:html/parser.dart";
import "package:linkify/linkify.dart";
import "package:nexus/helpers/matrix_linkifier.dart";
extension LinkifyHtml on String {
/// Wraps bare links in this HTML's text in `<a>` tags.
///
/// Only text nodes are touched, so attribute values are left alone, and
/// text already inside a link or code is skipped.
String get linkifiedHtml {
final fragment = parseFragment(this);
_linkifyNode(fragment);
return fragment.outerHtml;
}
}
void _linkifyNode(Node node) {
if (node is Element && const {"a", "code", "pre"}.contains(node.localName)) {
return;
}
for (final child in node.nodes.toList()) {
if (child is! Text) {
_linkifyNode(child);
continue;
}
final elements = linkifyText(child.data);
if (!elements.any((element) => element is LinkableElement)) continue;
elements
.map(
(element) => switch (element) {
LinkableElement(:final text, :final url) =>
Element.tag("a")
..attributes["href"] = url
..append(Text(text)),
_ => Text(element.text),
},
)
.forEach((replacement) => node.insertBefore(replacement, child));
child.remove();
}
}

View file

@ -0,0 +1,38 @@
import "package:linkify/linkify.dart";
/// Splits [text] into plain text and URL, `matrix:` and email links.
List<LinkifyElement> linkifyText(String text) => linkify(
text,
options: const LinkifyOptions(humanize: false),
linkifiers: const [UrlLinkifier(), MatrixLinkifier(), EmailLinkifier()],
);
/// Linkifies `matrix:` URIs, which [UrlLinkifier] only handles for http(s).
final class const MatrixLinkifier() extends Linkifier {
static final _regex = RegExp(
r"\bmatrix:[^\s<>\x22\x27]*[^\s<>\x22\x27.,;:!?)\]}]",
caseSensitive: false,
);
@override
List<LinkifyElement> parse(
List<LinkifyElement> elements,
LinkifyOptions options,
) => elements
.expand(
(element) => element is TextElement ? _split(element.text) : [element],
)
.toList();
Iterable<LinkifyElement> _split(String text) sync* {
var start = 0;
for (final match in _regex.allMatches(text)) {
if (match.start > start) {
yield TextElement(text.substring(start, match.start));
}
yield UrlElement(match[0]!);
start = match.end;
}
if (start < text.length) yield TextElement(text.substring(start));
}
}