parseFluentMarkup function
Parse a resolved Fluent message string into a span tree.
resolvedText is the output of FluentBundle.formatMessage — all
placeholders interpolated, plurals selected, terms expanded, bidi-
isolation marks added. Tags inside that string (<bold>foo</bold>,
<a href="x">y</a>, <br/>) are parsed as HTML5 markup; the rest
remains plain text.
The returned list always contains at least one span — an empty
resolved string produces [FluentTextSpan('')]. This keeps
downstream renderers' empty-list handling out of the hot path.
HTML5 conformance is inherited from package:html:
- Tag names case-folded to lowercase:
<Bold>⇒tag = "bold". - Both quote styles work:
href="x",href='x',href=x. - Named character entities decoded:
&⇒&,'⇒'. - Self-closing:
<br/>,<br>,<br></br>are equivalent. - Malformed-tag recovery: unclosed
<bold>oopscloses implicitly; stray</bold>is dropped; mismatched<b><i>x</b></i>is restructured per HTML5 rules. - Comments (
<!-- … -->) are dropped. - Whitespace is preserved.
Bidi-isolation marks (FSI U+2068, PDI U+2069) are stripped from attribute values; text spans keep them.
Implementation
List<FluentSpan> parseFluentMarkup(String resolvedText) {
// Empty string short-circuit. parseFragment('') returns a fragment
// with zero child nodes, which would yield an empty list — but the
// contract is that callers always get at least one span.
if (resolvedText.isEmpty) {
return const [FluentTextSpan('')];
}
// package:html's parseFragment treats input as HTML5 markup in a
// body context. The result is a `DocumentFragment` whose `nodes`
// are the top-level elements + text nodes.
final fragment = html.parseFragment(resolvedText);
final spans = <FluentSpan>[];
for (final node in fragment.nodes) {
final converted = _nodeToSpan(node);
if (converted != null) spans.add(converted);
}
// If the resolved string had no parseable content (only comments,
// dropped junk, etc.), surface a single empty text span so callers
// don't have to special-case empty lists.
if (spans.isEmpty) {
return const [FluentTextSpan('')];
}
return spans;
}