Inline images (#14)

* Add inline image elements

- Remove 'img' tag from the list of block tags.
- Use a Wrap widget to layout RichText and Image widgets.
- Convert _inlineElements list to accept Objects so both TextSpan and Image widgets can be added.

* Update tests

* Fix Image as child of text node

* Modify _InlineElement to inherit styling and merge spans

* Update tests

* Update tests

* Add test for nested inline images

* Refactor common _addParentInlineIfNeeded code

* Document _InlineElement class

* Document _InlineElement TextStyle merging

* Use dummy image url in tests
diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart
index c998e08..3d314f4 100644
--- a/packages/flutter_markdown/lib/src/builder.dart
+++ b/packages/flutter_markdown/lib/src/builder.dart
@@ -18,7 +18,6 @@
   'h6',
   'li',
   'blockquote',
-  'img',
   'pre',
   'ol',
   'ul',
@@ -38,8 +37,27 @@
   int nextListIndex = 0;
 }
 
+/// A collection of widgets that should be placed adjacent to (inline with)
+/// other inline elements in the same parent block.
+/// 
+/// Inline elements can be textual (a/em/strong) represented by [RichText] 
+/// widgets or images (img) represented by [Image.network] widgets.
+/// 
+/// Inline elements can be nested within other inline elements, inheriting their
+/// parent's style along with the style of the block they are in.
+/// 
+/// When laying out inline widgets, first, any adjacent RichText widgets are 
+/// merged, then, all inline widgets are enclosed in a parent [Wrap] widget.
 class _InlineElement {
-  final List<TextSpan> children = <TextSpan>[];
+  _InlineElement(this.tag, {this.style});
+ 
+  final String tag;
+
+  /// Created by merging the style defined for this element's [tag] in the
+  /// delegate's [MarkdownStyleSheet] with the style of its parent.
+  final TextStyle style;
+
+  final List<Widget> children = <Widget>[];
 }
 
 /// A delegate used by [MarkdownBuilder] to control the widgets it creates.
@@ -86,14 +104,13 @@
     _linkHandlers.clear();
 
     _blocks.add(new _BlockElement(null));
-    _inlines.add(new _InlineElement());
 
     for (md.Node node in nodes) {
       assert(_blocks.length == 1);
       node.accept(this);
     }
 
-    assert(_inlines.single.children.isEmpty);
+    assert(_inlines.isEmpty);
     return _blocks.single.children;
   }
 
@@ -101,13 +118,18 @@
   void visitText(md.Text text) {
     if (_blocks.last.tag == null) // Don't allow text directly under the root.
       return;
+
+    _addParentInlineIfNeeded(_blocks.last.tag);
+
     final TextSpan span = _blocks.last.tag == 'pre'
       ? delegate.formatText(styleSheet, text.text)
       : new TextSpan(
+          style: _inlines.last.style,
           text: text.text,
           recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null,
         );
-    _inlines.last.children.add(span);
+
+    _inlines.last.children.add(new RichText(text: span));
   }
 
   @override
@@ -119,7 +141,13 @@
         _listIndents.add(tag);
       _blocks.add(new _BlockElement(tag));
     } else {
-      _inlines.add(new _InlineElement());
+      _addParentInlineIfNeeded(_blocks.last.tag);
+
+      TextStyle parentStyle = _inlines.last.style;
+      _inlines.add(new _InlineElement(
+        tag,
+        style: parentStyle.merge(styleSheet.styles[tag]),
+      ));
     }
 
     if (tag == 'a') {
@@ -138,51 +166,48 @@
 
       final _BlockElement current = _blocks.removeLast();
       Widget child;
-      if (tag == 'img') {
-        child = _buildImage(element.attributes['src']);
-      } else {
-        if (current.children.isNotEmpty) {
-          child = new Column(
-            crossAxisAlignment: CrossAxisAlignment.stretch,
-            children: current.children,
-          );
-        } else {
-          child = const SizedBox();
-        }
 
-        if (_isListTag(tag)) {
-          assert(_listIndents.isNotEmpty);
-          _listIndents.removeLast();
-        } else if (tag == 'li') {
-          if (_listIndents.isNotEmpty) {
-            child = new Row(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: <Widget>[
-                new SizedBox(
-                  width: styleSheet.listIndent,
-                  child: _buildBullet(_listIndents.last),
-                ),
-                new Expanded(child: child)
-              ],
-            );
-          }
-        } else if (tag == 'blockquote') {
-          child = new DecoratedBox(
-            decoration: styleSheet.blockquoteDecoration,
-            child: new Padding(
-              padding: new EdgeInsets.all(styleSheet.blockquotePadding),
-              child: child,
-            ),
-          );
-        } else if (tag == 'pre') {
-          child = new DecoratedBox(
-            decoration: styleSheet.codeblockDecoration,
-            child: new Padding(
-              padding: new EdgeInsets.all(styleSheet.codeblockPadding),
-              child: child,
-            ),
+      if (current.children.isNotEmpty) {
+        child = new Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: current.children,
+        );
+      } else {
+        child = const SizedBox();
+      }
+
+      if (_isListTag(tag)) {
+        assert(_listIndents.isNotEmpty);
+        _listIndents.removeLast();
+      } else if (tag == 'li') {
+        if (_listIndents.isNotEmpty) {
+          child = new Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: <Widget>[
+              new SizedBox(
+                width: styleSheet.listIndent,
+                child: _buildBullet(_listIndents.last),
+              ),
+              new Expanded(child: child)
+            ],
           );
         }
+      } else if (tag == 'blockquote') {
+        child = new DecoratedBox(
+          decoration: styleSheet.blockquoteDecoration,
+          child: new Padding(
+            padding: new EdgeInsets.all(styleSheet.blockquotePadding),
+            child: child,
+          ),
+        );
+      } else if (tag == 'pre') {
+        child = new DecoratedBox(
+          decoration: styleSheet.codeblockDecoration,
+          child: new Padding(
+            padding: new EdgeInsets.all(styleSheet.codeblockPadding),
+            child: child,
+          ),
+        );
       }
 
       _addBlockChild(child);
@@ -190,15 +215,15 @@
       final _InlineElement current = _inlines.removeLast();
       final _InlineElement parent = _inlines.last;
 
-      if (current.children.isNotEmpty) {
-        parent.children.add(new TextSpan(
-          style: styleSheet.styles[tag],
-          children: current.children,
-        ));
+      if (tag == 'img') {
+        // create an image widget for this image
+        current.children.add(_buildImage(element.attributes['src']));
+      } else if (tag == 'a') {
+        _linkHandlers.removeLast();
+      }
 
-        if (tag == 'a') {
-          _linkHandlers.removeLast();
-        }
+      if (current.children.isNotEmpty) {
+        parent.children.addAll(current.children);
       }
     }
   }
@@ -233,6 +258,15 @@
     );
   }
 
+  void _addParentInlineIfNeeded(String tag) {
+    if (_inlines.isEmpty) {
+      _inlines.add(new _InlineElement(
+        tag,
+        style: styleSheet.styles[tag],
+      ));
+    }
+  }
+
   void _addBlockChild(Widget child) {
     final _BlockElement parent = _blocks.last;
     if (parent.children.isNotEmpty)
@@ -242,12 +276,35 @@
   }
 
   void _addAnonymousBlockIfNeeded(TextStyle style) {
+    if (_inlines.isEmpty) {
+      return;
+    }
+
     final _InlineElement inline = _inlines.single;
     if (inline.children.isNotEmpty) {
-      final TextSpan span = new TextSpan(style: style, children: inline.children);
-      _addBlockChild(new RichText(text: span));
+      List<Widget> mergedInlines = _mergeInlineChildren(inline);
+      final Wrap wrap = new Wrap(children: mergedInlines);
+      _addBlockChild(wrap);
       _inlines.clear();
-      _inlines.add(new _InlineElement());
     }
   }
+
+  /// Merges adjacent [TextSpan] children of the given [_InlineElement]
+  List<Widget> _mergeInlineChildren(_InlineElement inline) {
+    List<Widget> mergedTexts = <Widget>[];
+    for (Widget child in inline.children) {
+      if (mergedTexts.isNotEmpty && mergedTexts.last is RichText && child is RichText) {
+        RichText previous = mergedTexts.removeLast();
+        List<TextSpan> children = previous.text.children != null
+          ? new List.from(previous.text.children)
+          : [previous.text];
+        children.add(child.text);
+        TextSpan mergedSpan = new TextSpan(children: children);
+        mergedTexts.add(new RichText(text: mergedSpan));
+      } else {
+        mergedTexts.add(child);
+      }
+    }
+    return mergedTexts;
+  }
 }
diff --git a/packages/flutter_markdown/test/flutter_markdown_test.dart b/packages/flutter_markdown/test/flutter_markdown_test.dart
index 268d140..3900041 100644
--- a/packages/flutter_markdown/test/flutter_markdown_test.dart
+++ b/packages/flutter_markdown/test/flutter_markdown_test.dart
@@ -21,7 +21,7 @@
 
     final Iterable<Widget> widgets = tester.allWidgets;
     _expectWidgetTypes(
-        widgets, <Type>[Directionality, MarkdownBody, Column, RichText]);
+        widgets, <Type>[Directionality, MarkdownBody, Column, Wrap, RichText]);
     _expectTextStrings(widgets, <String>['Hello']);
   });
 
@@ -30,7 +30,7 @@
 
     final Iterable<Widget> widgets = tester.allWidgets;
     _expectWidgetTypes(
-        widgets, <Type>[Directionality, MarkdownBody, Column, RichText]);
+        widgets, <Type>[Directionality, MarkdownBody, Column, Wrap, RichText]);
     _expectTextStrings(widgets, <String>['Header']);
   });
 
@@ -100,12 +100,10 @@
           tester.allWidgets.firstWhere((Widget widget) => widget is RichText);
       final TextSpan span = textWidget.text;
 
-      (span.children[0].children[0].recognizer as TapGestureRecognizer).onTap();
+      (span.recognizer as TapGestureRecognizer).onTap();
 
-      expect(span.children.length, 1);
-      expect(span.children[0].children.length, 1);
-      expect(span.children[0].children[0].recognizer.runtimeType,
-          equals(TapGestureRecognizer));
+      expect(span.children, null);
+      expect(span.recognizer.runtimeType, equals(TapGestureRecognizer));
       expect(tapResult, 'href');
     });
 
@@ -128,8 +126,8 @@
         return true;
       });
 
-      expect(span.children.length, 1);
-      expect(span.children[0].children.length, 3);
+      expect(span.children.length, 3);
+      expect(gestureRecognizerTypes.length, 3);
       expect(gestureRecognizerTypes, everyElement(TapGestureRecognizer));
       expect(tapResults.length, 3);
       expect(tapResults, everyElement('href'));
@@ -155,12 +153,7 @@
         return true;
       });
 
-
       expect(span.children.length, 3);
-      expect(span.children[0].children.length, 1);
-      expect(span.children[1].children, null);
-      expect(span.children[2].children.length, 1);
-
       expect(gestureRecognizerTypes,
           orderedEquals([TapGestureRecognizer, Null, TapGestureRecognizer]));
       expect(tapResults, orderedEquals(['firstHref', 'secondHref']));
@@ -172,6 +165,26 @@
       createHttpClient = createMockImageHttpClient;
     });
 
+    testWidgets('should not interupt styling', (WidgetTester tester) async {
+      await tester.pumpWidget(_boilerplate(const Markdown(
+        data:'_textbefore ![alt](img) textafter_',
+      )));
+
+      final RichText firstTextWidget =
+          tester.allWidgets.firstWhere((Widget widget) => widget is RichText);
+      final Image image =
+          tester.allWidgets.firstWhere((Widget widget) => widget is Image);
+      final NetworkImage networkImage = image.image;
+      final RichText secondTextWidget =
+          tester.allWidgets.lastWhere((Widget widget) => widget is RichText);
+
+      expect(firstTextWidget.text.text, 'textbefore ');
+      expect(firstTextWidget.text.style.fontStyle, FontStyle.italic);
+      expect(networkImage.url,'img');
+      expect(secondTextWidget.text.text, ' textafter');
+      expect(secondTextWidget.text.style.fontStyle, FontStyle.italic);
+    });
+
     testWidgets('should work with a link', (WidgetTester tester) async {
       await tester
           .pumpWidget(_boilerplate(const Markdown(data: '![alt](img#50x50)')));
@@ -191,7 +204,7 @@
       final RichText richText =
         tester.allWidgets.firstWhere((Widget widget) => widget is RichText);
       TextSpan textSpan = richText.text;
-      expect(textSpan.children[0].text, 'Hello ');
+      expect(textSpan.text, 'Hello ');
       expect(textSpan.style, isNotNull);
     });
   });