parseFluentMarkup function

List<FluentSpan> parseFluentMarkup(
  1. String resolvedText
)

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: &amp;&, &#39;'.
  • Self-closing: <br/>, <br>, <br></br> are equivalent.
  • Malformed-tag recovery: unclosed <bold>oops closes 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;
}