You've successfully subscribed to developer.school
Great! Next, complete checkout for full access to developer.school
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.
Billing info update failed.

Custom Markdown InlineSyntax with Flutter

Paul Halliday
Paul Halliday

As you're very well aware, Markdown is a great way to dynamic styled text to your mobile and web applications. If you're looking for how to add this to your Flutter applications, you can check out my article on this topic here:

How to Display Markdown in Flutter using flutter_markdown
Have you ever been in a position where you want to display a large amount oftext on screen? Granted, you could do this with a widget tree using Text, RichText, Column and so on, but this isn’t very scalable. It also doesn’t lenditself to being adaptable/editable quickly by people outside the mob…

This article deals with how to add custom inline blocks of markdown, for example, the "NEW" button seen in this image below:

As a result of this, I'm going to make the assumption that you already have a Flutter project with markdown and flutter_markdown installed.

We get our green button by typing --NEW-- in our .md file.

Adding our InlineSyntax to the Markdown file

To get started, we'll need to update our Markdown file with the String that we want the InlineParser to match on. This can be anything you want, and for our project I've called this --NEW-- for example's sake.

## November 9, 2020

--NEW--

- There is now a CHANGELOG! Each major change to the application is documented here.

Creating a Custom InlineSyntax Parser

Now that we've added our tag to the Markdown file, we can go ahead and write a custom class named ColoredBoxInlineSyntax which extends the InlineSyntax class from the markdown package:

import 'package:markdown/markdown.dart';

class ColoredBoxInlineSyntax extends InlineSyntax {
  /// This is a primitive example pattern
  ColoredBoxInlineSyntax({
    String pattern = r'--(.*?)--',
  }) : super(pattern);

  @override
  bool onMatch(InlineParser parser, Match match) {
    /// This creates a new element with the tag name `coloredBox`
    /// The `textContent` of this new tag will be the
    /// pattern match _without_ the dashes.
    ///
    /// We can change how this looks by creating a custom
    /// [MarkdownElementBuilder] from the `flutter_markdown` package.
    final withoutDashes = match.group(0).replaceAll(RegExp(r'--'), "");

    Element el = Element.text("coloredBox", withoutDashes.titleCase());

    parser.addNode(el);
    return true;
  }
}

class StringFormatError {
  String message;

  StringFormatError(this.message);
}

extension TitleCaseExtension on String {
  String titleCase() {
    try {
      if (this.contains(" ")) {
        return this
            .trim()
            .split(' ')
            .map((String word) =>
                "${word[0].toUpperCase()}${word.substring(1).toLowerCase()}")
            .join(" ");
      } else {
        return "${this[0].toUpperCase()}${this.substring(1).toLowerCase()}";
      }
    } catch (e) {
      throw StringFormatError("Cannot format String: $this");
    }
  }
}

As you can see, we just need to pass our ColoredBoxInlineSyntax class a Regex pattern and this will go ahead and create a new Element named coloredBox with our title cased text.

Creating a Custom MarkdownElementBuilder

We aren't finished there though. We need to create a MarkdownElementBuilder so that we can tell flutter_markdown exactly how this element should be rendered on screen:

class ColoredBoxMarkdownElementBuilder extends MarkdownElementBuilder {
  /// Once again, purely for example purposes.
  Color _backgroundColorForElement(String text) {
    if (text.toLowerCase() == "new") {
      return Colors.green;
    } else if (text.toLowerCase() == "breaking") {
      return Colors.red;
    }

    return Colors.amber;
  }

  Color _textColorForBackground(Color backgroundColor) {
    if (ThemeData.estimateBrightnessForColor(backgroundColor) ==
        Brightness.dark) {
      return Colors.white;
    }

    return Colors.black;
  }

  @override
  Widget visitElementAfter(md.Element element, TextStyle preferredStyle) {
    return Container(
      margin: EdgeInsets.symmetric(horizontal: 0, vertical: 2),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(6)),
        color: _backgroundColorForElement(element.textContent),
      ),
      child: Padding(
        padding: const EdgeInsets.all(4.0),
        child: Text(
          element.textContent,
          style: TextStyle(
              color: _textColorForBackground(
                _backgroundColorForElement(
                  element.textContent,
                ),
              ),
              fontWeight: FontWeight.bold),
        ),
      ),
    );
  }
}

This is what gives our Element the styling, and you may have noticed that we can be extra specific (if we really want to) as seen in our _backgroundColorForElement method.

  • Any time our text is new we'll have a green box.
  • Any time our text is breaking we'll have a red box.
  • Otherwise, we'll have an amber box.

Registering MarkdownElementBuilders

The final stage of our process is to register both our InlineSyntax and MarkdownElementBuilder by passing them to the Markdown or MarkdownBody widget:

FutureBuilder(
  future: rootBundle.loadString(markdownFilePath),
  builder:
      (BuildContext context, AsyncSnapshot<String> snapshot) {
    if (snapshot.hasError) {
      print(snapshot.error);
      return Text(snapshot.error);
    }

    if (snapshot.hasData) {
      return Markdown(
        data: snapshot.data,
        builders: {
          "coloredBox": ColoredBoxMarkdownElementBuilder(),
        },
        inlineSyntaxes: [
          ColoredBoxInlineSyntax(),
        ],
      );
    }

    return Center(child: CircularProgressIndicator());
  },
)
  • The ColoredBoxInlineSyntax will generate the coloredBox element tags
  • The ColoredBoxMarkdownElementBuilder will be applied to each coloredBox that is found, thus, creating our colored box with text.

Here's a final example of how this looks with a couple of examples:

This was generated from the following Markdown:

November 9, 2020 --NEW--

November 9, 2020 --BREAKING--

November 9, 2020 --OTHER--

--November 9, 2020--
Flutter

Paul Halliday

👋 Want to see more content? Head over to the YouTube channel: https://youtube.com/c/paulhalliday!