[flutter_markdown] Padding builder (#486)

diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md
index 6a6a568..175d7fa 100644
--- a/packages/flutter_markdown/CHANGELOG.md
+++ b/packages/flutter_markdown/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.6.8
+
+  * Added option paddingBuilders
+
 ## 0.6.7
 
  * Fix `unnecessary_import` lint errors.
diff --git a/packages/flutter_markdown/example/lib/demos/wrap_alignment_demo.dart b/packages/flutter_markdown/example/lib/demos/wrap_alignment_demo.dart
index 74faa29..c3ca366 100644
--- a/packages/flutter_markdown/example/lib/demos/wrap_alignment_demo.dart
+++ b/packages/flutter_markdown/example/lib/demos/wrap_alignment_demo.dart
@@ -5,6 +5,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:markdown/markdown.dart' as md;
 import '../shared/dropdown_menu.dart';
 import '../shared/markdown_demo_widget.dart';
 import '../shared/markdown_extensions.dart';
@@ -127,6 +128,9 @@
                     blockquoteAlign: _wrapAlignment,
                     codeblockAlign: _wrapAlignment,
                   ),
+                  paddingBuilders: <String, MarkdownPaddingBuilder>{
+                    'p': CustomPaddingBuilder()
+                  },
                 ),
               ),
             ],
@@ -138,3 +142,28 @@
     );
   }
 }
+
+class CustomPaddingBuilder extends MarkdownPaddingBuilder {
+  final EdgeInsets _padding = const EdgeInsets.only(left: 10.0);
+  bool paddingUse = true;
+
+  @override
+  void visitElementBefore(md.Element element) {
+    if (element.children!.length == 1 && element.children![0] is md.Element) {
+      final md.Element child = element.children![0] as md.Element;
+
+      paddingUse = child.tag != 'img';
+    } else {
+      paddingUse = true;
+    }
+  }
+
+  @override
+  EdgeInsets getPadding() {
+    if (paddingUse) {
+      return _padding;
+    } else {
+      return EdgeInsets.zero;
+    }
+  }
+}
diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart
index a9be31f..c94123f 100644
--- a/packages/flutter_markdown/lib/src/builder.dart
+++ b/packages/flutter_markdown/lib/src/builder.dart
@@ -101,6 +101,7 @@
     required this.checkboxBuilder,
     required this.bulletBuilder,
     required this.builders,
+    required this.paddingBuilders,
     required this.listItemCrossAxisAlignment,
     this.fitContent = false,
     this.onTapText,
@@ -133,6 +134,9 @@
   /// Call when build a custom widget.
   final Map<String, MarkdownElementBuilder> builders;
 
+  /// Call when build a padding for widget.
+  final Map<String, MarkdownPaddingBuilder> paddingBuilders;
+
   /// Whether to allow the widget to fit the child content.
   final bool fitContent;
 
@@ -195,6 +199,10 @@
       builders[tag]!.visitElementBefore(element);
     }
 
+    if (paddingBuilders.containsKey(tag)) {
+      paddingBuilders[tag]!.visitElementBefore(element);
+    }
+
     int? start;
     if (_isBlockTag(tag)) {
       _addAnonymousBlockIfNeeded();
@@ -424,6 +432,11 @@
     } else {
       final _InlineElement current = _inlines.removeLast();
       final _InlineElement parent = _inlines.last;
+      EdgeInsets padding = EdgeInsets.zero;
+
+      if (paddingBuilders.containsKey(tag)) {
+        padding = paddingBuilders[tag]!.getPadding();
+      }
 
       if (builders.containsKey(tag)) {
         final Widget? child =
@@ -433,10 +446,13 @@
         }
       } else if (tag == 'img') {
         // create an image widget for this image
-        current.children.add(_buildImage(
-          element.attributes['src']!,
-          element.attributes['title'],
-          element.attributes['alt'],
+        current.children.add(_buildPadding(
+          padding,
+          _buildImage(
+            element.attributes['src']!,
+            element.attributes['title'],
+            element.attributes['alt'],
+          ),
         ));
       } else if (tag == 'br') {
         current.children.add(_buildRichText(const TextSpan(text: '\n')));
@@ -573,6 +589,14 @@
     );
   }
 
+  Widget _buildPadding(EdgeInsets padding, Widget child) {
+    if (padding == EdgeInsets.zero) {
+      return child;
+    }
+
+    return Padding(padding: padding, child: child);
+  }
+
   void _addParentInlineIfNeeded(String? tag) {
     if (_inlines.isEmpty) {
       _inlines.add(_InlineElement(
@@ -603,6 +627,10 @@
       blockAlignment = _wrapAlignmentForBlockTag(_currentBlockTag);
       textAlign = _textAlignForBlockTag(_currentBlockTag);
       textPadding = _textPaddingForBlockTag(_currentBlockTag);
+
+      if (paddingBuilders.containsKey(_currentBlockTag)) {
+        textPadding = paddingBuilders[_currentBlockTag]!.getPadding();
+      }
     }
 
     final _InlineElement inline = _inlines.single;
diff --git a/packages/flutter_markdown/lib/src/widget.dart b/packages/flutter_markdown/lib/src/widget.dart
index 447adfd..0e4ca00 100644
--- a/packages/flutter_markdown/lib/src/widget.dart
+++ b/packages/flutter_markdown/lib/src/widget.dart
@@ -163,6 +163,7 @@
     this.checkboxBuilder,
     this.bulletBuilder,
     this.builders = const <String, MarkdownElementBuilder>{},
+    this.paddingBuilders = const <String, MarkdownPaddingBuilder>{},
     this.fitContent = false,
     this.listItemCrossAxisAlignment =
         MarkdownListItemCrossAxisAlignment.baseline,
@@ -234,6 +235,19 @@
   /// The `SubscriptBuilder` is a subclass of [MarkdownElementBuilder].
   final Map<String, MarkdownElementBuilder> builders;
 
+  /// Add padding for different tags (use only for block elements and img)
+  ///
+  /// For example, we will add padding for `img` tag:
+  ///
+  /// ```dart
+  /// paddingBuilders: {
+  ///   'img': ImgPaddingBuilder(),
+  /// }
+  /// ```
+  ///
+  /// The `ImgPaddingBuilder` is a subclass of [MarkdownPaddingBuilder].
+  final Map<String, MarkdownPaddingBuilder> paddingBuilders;
+
   /// Whether to allow the widget to fit the child content.
   final bool fitContent;
 
@@ -317,6 +331,7 @@
       checkboxBuilder: widget.checkboxBuilder,
       bulletBuilder: widget.bulletBuilder,
       builders: widget.builders,
+      paddingBuilders: widget.paddingBuilders,
       fitContent: widget.fitContent,
       listItemCrossAxisAlignment: widget.listItemCrossAxisAlignment,
       onTapText: widget.onTapText,
@@ -391,6 +406,8 @@
     MarkdownBulletBuilder? bulletBuilder,
     Map<String, MarkdownElementBuilder> builders =
         const <String, MarkdownElementBuilder>{},
+    Map<String, MarkdownPaddingBuilder> paddingBuilders =
+        const <String, MarkdownPaddingBuilder>{},
     MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment =
         MarkdownListItemCrossAxisAlignment.baseline,
     this.shrinkWrap = true,
@@ -412,6 +429,7 @@
           imageBuilder: imageBuilder,
           checkboxBuilder: checkboxBuilder,
           builders: builders,
+          paddingBuilders: paddingBuilders,
           listItemCrossAxisAlignment: listItemCrossAxisAlignment,
           bulletBuilder: bulletBuilder,
           fitContent: fitContent,
@@ -464,6 +482,8 @@
     MarkdownBulletBuilder? bulletBuilder,
     Map<String, MarkdownElementBuilder> builders =
         const <String, MarkdownElementBuilder>{},
+    Map<String, MarkdownPaddingBuilder> paddingBuilders =
+        const <String, MarkdownPaddingBuilder>{},
     MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment =
         MarkdownListItemCrossAxisAlignment.baseline,
     this.padding = const EdgeInsets.all(16.0),
@@ -487,6 +507,7 @@
           imageBuilder: imageBuilder,
           checkboxBuilder: checkboxBuilder,
           builders: builders,
+          paddingBuilders: paddingBuilders,
           listItemCrossAxisAlignment: listItemCrossAxisAlignment,
           bulletBuilder: bulletBuilder,
           softLineBreak: softLineBreak,
@@ -541,3 +562,13 @@
     return true;
   }
 }
+
+/// An interface for an padding builder for element.
+abstract class MarkdownPaddingBuilder {
+  /// Called when an Element has been reached, before its children have been
+  /// visited.
+  void visitElementBefore(md.Element element) {}
+
+  /// Called when a widget node has been rendering and need tag padding.
+  EdgeInsets getPadding() => EdgeInsets.zero;
+}
diff --git a/packages/flutter_markdown/pubspec.yaml b/packages/flutter_markdown/pubspec.yaml
index 0ce150c..1dbb23b 100644
--- a/packages/flutter_markdown/pubspec.yaml
+++ b/packages/flutter_markdown/pubspec.yaml
@@ -4,7 +4,7 @@
   formatted with simple Markdown tags.
 repository: https://github.com/flutter/packages/tree/master/packages/flutter_markdown
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_markdown%22
-version: 0.6.7
+version: 0.6.8
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/flutter_markdown/test/padding_test.dart b/packages/flutter_markdown/test/padding_test.dart
new file mode 100644
index 0000000..6f27ca9
--- /dev/null
+++ b/packages/flutter_markdown/test/padding_test.dart
@@ -0,0 +1,67 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'utils.dart';
+
+void main() => defineTests();
+
+void defineTests() {
+  group('Padding builders', () {
+    testWidgets(
+      'use paddingBuilders for p',
+      (WidgetTester tester) async {
+        const double paddingX = 10.0;
+
+        await tester.pumpWidget(
+          boilerplate(
+            Markdown(
+                data: '**line 1**\n\n# H1\n![alt](/assets/images/logo.png)',
+                paddingBuilders: <String, MarkdownPaddingBuilder>{
+                  'p': CustomPaddingBuilder(paddingX * 1),
+                  'strong': CustomPaddingBuilder(paddingX * 2),
+                  'h1': CustomPaddingBuilder(paddingX * 3),
+                  'img': CustomPaddingBuilder(paddingX * 4),
+                }),
+          ),
+        );
+
+        final List<Padding> paddings =
+            tester.widgetList<Padding>(find.byType(Padding)).toList();
+
+        expect(paddings.length, 4);
+        expect(
+          paddings[0].padding.along(Axis.horizontal) == paddingX * 1 * 2,
+          true,
+        );
+        expect(
+          paddings[1].padding.along(Axis.horizontal) == paddingX * 3 * 2,
+          true,
+        );
+        expect(
+          paddings[2].padding.along(Axis.horizontal) == paddingX * 1 * 2,
+          true,
+        );
+        expect(
+          paddings[3].padding.along(Axis.horizontal) == paddingX * 4 * 2,
+          true,
+        );
+      },
+    );
+  });
+}
+
+class CustomPaddingBuilder extends MarkdownPaddingBuilder {
+  CustomPaddingBuilder(this.paddingX);
+
+  double paddingX;
+
+  @override
+  EdgeInsets getPadding() {
+    return EdgeInsets.symmetric(horizontal: paddingX);
+  }
+}