Skip to main content

Overview

The Markdown plugin provides bidirectional conversion between Markdown text and AppFlowy Editor documents. It supports GitHub Flavored Markdown and custom syntax extensions.

Installation

The Markdown plugin is included with AppFlowy Editor:
import 'package:appflowy_editor/appflowy_editor.dart';

Importing Markdown

Basic Usage

Convert Markdown text to an AppFlowy document:
const markdown = r'''# Hello AppFlowy!''';
final editorState = EditorState(
  document: markdownToDocument(markdown),
);

With Custom Parsers

Extend the parser with custom markdown elements:
final document = markdownToDocument(
  markdown,
  markdownParsers: [
    const MyCustomMarkdownParser(),
  ],
  inlineSyntaxes: [
    MyCustomInlineSyntax(),
  ],
);

Supported Markdown Elements

The decoder supports these Markdown elements out of the box:
  • Headings: # H1 through ###### H6
  • Lists: Bulleted (-, *), numbered (1.), and todo lists (- [ ], - [x])
  • Formatting: Bold (**text**), italic (*text*), strikethrough (~~text~~)
  • Links: [text](url)
  • Images: ![alt](url)
  • Code: Inline code and code blocks
  • Quotes: Block quotes (>)
  • Tables: GitHub Flavored Markdown tables
  • Dividers: Horizontal rules (---)

Exporting to Markdown

Basic Usage

Convert an AppFlowy document to Markdown:
final document = editorState.document;
final markdown = documentToMarkdown(document);

With Line Breaks

Add custom line breaks between blocks:
final markdown = documentToMarkdown(
  document,
  lineBreak: '\n',
);

With Custom Parsers

Handle custom node types during export:
final markdown = documentToMarkdown(
  document,
  customParsers: [
    const MyCustomNodeParser(),
  ],
);

Implementation Details

Markdown Codec

The plugin uses a Codec<Document, String> implementation:
class AppFlowyEditorMarkdownCodec extends Codec<Document, String> {
  const AppFlowyEditorMarkdownCodec({
    this.markdownInlineSyntaxes = const [],
    this.markdownParsers = const [],
    this.encodeParsers = const [],
    this.lineBreak = '',
  });

  @override
  Converter<String, Document> get decoder => DocumentMarkdownDecoder(
    markdownElementParsers: markdownParsers,
    inlineSyntaxes: markdownInlineSyntaxes,
  );

  @override
  Converter<Document, String> get encoder => DocumentMarkdownEncoder(
    parsers: encodeParsers,
    lineBreak: lineBreak,
  );
}

Built-in Parsers

Decoder Parsers

The decoder uses these parsers (from /lib/src/plugins/markdown/document_markdown.dart:18-31):
  • MarkdownParagraphParserV2
  • MarkdownHeadingParserV2
  • MarkdownTodoListParserV2
  • MarkdownUnorderedListParserV2
  • MarkdownOrderedListParserV2
  • MarkdownBlockQuoteParserV2
  • MarkdownTableListParserV2
  • MarkdownDividerParserV2
  • MarkdownImageParserV2

Encoder Parsers

The encoder uses these parsers (from /lib/src/plugins/markdown/document_markdown.dart:44-56):
  • TextNodeParser
  • BulletedListNodeParser
  • NumberedListNodeParser
  • TodoListNodeParser
  • QuoteNodeParser
  • CodeBlockNodeParser
  • HeadingNodeParser
  • ImageNodeParser
  • TableNodeParser
  • DividerNodeParser

Creating Custom Parsers

Custom Node Parser (Encoder)

Implement the NodeParser interface:
class MyCustomNodeParser extends NodeParser {
  const MyCustomNodeParser();

  @override
  String get id => 'my_custom_type';

  @override
  String transform(Node node, DocumentMarkdownEncoder encoder) {
    // Convert node to markdown string
    return '**${node.delta?.toPlainText()}**';
  }
}

Custom Markdown Parser (Decoder)

Implement the CustomMarkdownParser interface:
class MyCustomMarkdownParser extends CustomMarkdownParser {
  const MyCustomMarkdownParser();

  @override
  List<Node> transform(
    md.Node element,
    List<CustomMarkdownParser> parsers,
  ) {
    // Parse markdown element and return AppFlowy nodes
    return [paragraphNode(text: element.textContent)];
  }
}

Custom Inline Syntax

Extend markdown’s inline syntax:
import 'package:markdown/markdown.dart' as md;

class MyCustomInlineSyntax extends md.InlineSyntax {
  MyCustomInlineSyntax() : super(r'\*\*(.+?)\*\*');

  @override
  bool onMatch(md.InlineParser parser, Match match) {
    // Handle the matched syntax
    return true;
  }
}

Limitations

Some advanced formatting may not be preserved during conversion:
  • Font size and font family
  • Text alignment
  • Custom colors (unless using custom parsers)
  • Nested formatting in some edge cases

Example: Complete Import Flow

Here’s a complete example from the source code:
// From documentation/importing.md:28-35
const markdown = r'''# Hello AppFlowy!''';
final editorState = EditorState(
  document: markdownToDocument(markdown),
);

See Also

Build docs developers (and LLMs) love