Inline syntax

Render standard Markdown links correctly, then layer your product's own tokens—mentions, channels, emoji, issue references, or template tags—into the same flowing text.

Autolinks

Bare URLs, www. hosts, email addresses, and angle-bracket autolinks become links without pre-processing. The parser follows GFM-style boundary rules, so surrounding punctuation and balanced parentheses stay out of the destination.

reply.dart
1GptMarkdown( 2 'Ship it: https://pub.dev or mail ada@example.com', 3 onLinkTap: (url, title) => launchUrlString(url), 4)
InputResult
see https://x.com.The period stays outside the link.
(https://x.com)The unbalanced closing parenthesis stays outside.
https://en.wikipedia.org/wiki/Foo_(bar)Balanced parentheses stay inside.
www.example.comLinks as http://www.example.com.
ada@example.comLinks as mailto:ada@example.com.
**https://x.com**Renders as a bold link; the bold markers never enter the destination.
`https://x.com`Remains code, not a link.

Angle autolinks such as <https://x.com>, <mailto:a@b.com>, and <a@b.com> follow CommonMark's deliberate-author syntax and accept any scheme. Bare links remain limited to the allowlist shown above.

links.dart
1// Bare http, https, mailto, and xmpp links work automatically. 2// Add app-specific URL schemes only when your product expects them: 3GptMarkdown( 4 text, 5 autolinkSchemes: const {'myapp', 'slack'}, 6) 7 8// Or leave bare URLs as plain text. Explicit [label](url) links still work. 9GptMarkdown(text, autolink: false)
Avoid a URL pre-processor.

Rewriting bare URLs into Markdown links before rendering can capture punctuation or formatting markers. Let the inline parser see the surrounding syntax first, or turn autolink off if a legacy pre-processor must remain.

App-specific tokens with InlinePattern

InlinePattern is the recommended extension point for inline product syntax. Patterns are matched before built-in inline components, so a matching pattern deliberately takes precedence.

issue_pattern.dart
1import 'package:flutter/gestures.dart'; 2import 'package:flutter/material.dart'; 3import 'package:gpt_markdown/gpt_markdown.dart'; 4 5GptMarkdown( 6 text, 7 inlinePatterns: [ 8 InlinePattern( 9 pattern: RegExp(r'(?<![\w-])GH-(\d+)\b'), 10 builder: (context, match, style) => TextSpan( 11 text: match.group(0), 12 style: style.copyWith( 13 color: Theme.of(context).colorScheme.primary, 14 fontWeight: FontWeight.w600, 15 ), 16 recognizer: TapGestureRecognizer() 17 ..onTap = () => openIssue(match.group(1)!), 18 ), 19 // TextSpan is safe to opt into every Markdown scope. 20 scopes: MarkdownComponent.allScopes, 21 ), 22 ], 23)

Use lookarounds and word boundaries to describe a token's actual boundaries. Patterns are matched against the whole document, so ^ and $ are rarely the right anchors.

Mentions and channels

Use InlinePattern.prefixed for @name and #channel. It knows not to claim an @ inside an email address or a # inside a URL fragment, and longer known names win over shorter ones.

channels.dart
1// Recommended: only known names match. 2InlinePattern.prefixed( 3 prefix: '#', 4 knownNames: myChannelNames, // ['general', 'design-review', ...] 5 builder: (context, match, style) => WidgetSpan( 6 alignment: PlaceholderAlignment.baseline, 7 baseline: TextBaseline.alphabetic, 8 child: ChannelChip(name: match.group(0)!.substring(1)), 9 ), 10) 11 12// This avoids treating #2959, a URL fragment, or a hex colour as a channel. 13// 14// Opt in only when every token is meaningful in your product: 15InlinePattern.prefixed( 16 prefix: '#', 17 knownNames: myChannelNames, 18 genericTokenPattern: r'[A-Za-z0-9_][A-Za-z0-9_-]*', 19 builder: (context, match, style) => TextSpan( 20 text: match.group(0), 21 style: style, 22 ), 23)
Use known names by default. A generic # fallback can turn issue numbers, mid-sentence headings, and colors into chips. Matching is case-insensitive.

Emoji and delimited syntax

InlinePattern.delimited handles closing delimiters that a prefixed token cannot express. It keeps ordinary times such as 10:30:45 and URLs with ports from becoming shortcodes.

emoji.dart
1const emoji = {'tada': '🎉', 'rocket': '🚀', 'fire': '🔥'}; 2 3InlinePattern.delimited( 4 open: ':', 5 knownNames: emoji.keys, 6 builder: (context, match, style) { 7 final name = match.namedGroup('name'); 8 final glyph = name == null ? null : emoji[name]; 9 return TextSpan(text: glyph ?? match.group(0), style: style); 10 }, 11) 12 13// Delimited patterns can also be asymmetric or multi-character: 14InlinePattern.delimited( 15 open: '{{', 16 close: '}}', 17 knownNames: templateNames, 18 builder: buildTemplateSpan, 19)

The token is available as the named name group (and group 1), even when a generic token pattern includes its own groups. Boundaries prevent :tada:xyz from matching while adjacent :fire::fire: tokens still match twice. Return the raw match for an unknown name so author text never becomes an empty gap.

TextSpan, WidgetSpan, and scopes

Prefer a TextSpan whenever your token can be text. It wraps, participates in selection, and aligns to the surrounding baseline. A WidgetSpan is for a real chip, icon, or image.

inline_ui.dart
1// A TextSpan wraps, remains selectable, and stays on the text baseline. 2builder: (context, match, style) => TextSpan( 3 text: match.group(0), 4 style: style, 5) 6 7// Use WidgetSpan only for actual UI. The package compensates for text scaling 8// when InlinePattern returns one: 9builder: (context, match, style) => WidgetSpan( 10 alignment: PlaceholderAlignment.middle, 11 child: Icon(Icons.tag, size: (style.fontSize ?? 14) * 1.15), 12)
Scope safety matters.

A widget inside a Markdown link label becomes a nested placeholder and can disappear on iOS. Patterns default to MarkdownComponent.allScopesExceptLinkLabel; keep that default for widget-based UI. Without it, [#design](https://example.com) can become blank on iOS with no visible error. Opt into MarkdownComponent.allScopes only when returning a safe TextSpan.

Common mistakes

  • Returning an empty span for an unknown token. Return the original match so the author's text never vanishes.
  • Building the pattern list on every frame. Cache it in a field or make it const; list identity participates in rendering work.
  • Using a custom component for a simple token. Start with InlinePattern; use a component only for genuinely new Markdown syntax.