Commits

Roger Haase committed bc07c47

fix 275 Cannot insert images into paragraphs

  • Participants
  • Parent commits 8e26395

Comments (0)

Files changed (3)

MoinMoin/converter/_tests/test_include.py

         assert e.data == 'a(b)'
 
     def test_IncludeHandlesCircularRecursion(self):
-        # issue #80
-        # we use text/x.moin.wiki markup to make tests simple
+        # detect circular recursion and create error message
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page2}}')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page3}}')
         update_item(u'page3', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page4}}')
         update_item(u'page4', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page2}}')
-
         page1 = Item.create(u'page1')
         rendered = page1.content._render_data()
         # an error message will follow strong tag
         assert '<strong class="moin-error">' in rendered
 
     def test_ExternalInclude(self):
+        # external include
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{http://moinmo.in}}')
         rendered = Item.create(u'page1').content._render_data()
         assert '<object class="moin-http moin-transclusion" data="http://moinmo.in" data-href="http://moinmo.in">http://moinmo.in</object>' in rendered
+        # external include embedded within text (object is an inline tag)
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'before {{http://moinmo.in}} after')
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>before <object class="moin-http moin-transclusion" data="http://moinmo.in" data-href="http://moinmo.in">http://moinmo.in</object> after</p>' in rendered
+        # external include embedded within text italic and bold markup (object is an inline tag)
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"before ''italic '''bold {{http://moinmo.in}} bold''' italic'' normal")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>before <em>italic <strong>bold <object class="moin-http moin-transclusion" data="http://moinmo.in" data-href="http://moinmo.in">http://moinmo.in</object> bold</strong> italic</em> normal</p>' in rendered
 
     def test_InlineInclude(self):
-        # issue #28
-        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}".')
 
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'before {{page2}} after')
+        # transclude single paragraph as inline
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Single line')
         rendered = Item.create(u'page1').content._render_data()
-        assert '<p>Content of page2 is "<span class="moin-transclusion" data-href="/page2">Single line</span>".</p>' in rendered
-
+        assert '<p>before <span class="moin-transclusion" data-href="/page2">Single line</span> after</p>' in rendered
+        # transclude multiple paragraphs as block
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Two\n\nParagraphs')
         rendered = Item.create(u'page1').content._render_data()
-        assert '<p>Content of page2 is "</p><div class="moin-transclusion" data-href="/page2"><p>Two</p><p>Paragraphs</p></div><p>".</p></div>' in rendered
-
+        assert '<p>before </p><div class="moin-transclusion" data-href="/page2"><p>Two</p><p>Paragraphs</p></div><p> after</p></div>' in rendered
+        # transclude single paragraph with internal markup as inline
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"this text contains ''italic'' string")
         rendered = Item.create(u'page1').content._render_data()
-        assert 'Content of page2 is "<span class="moin-transclusion" data-href="/page2">this text contains <em>italic</em>' in rendered
-
+        assert 'before <span class="moin-transclusion" data-href="/page2">this text contains <em>italic</em>' in rendered
+        # transclude single paragraph as only content within a paragraph
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is\n\n{{page2}}')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Single Line")
         rendered = Item.create(u'page1').content._render_data()
         assert '<p>Content of page2 is</p><p><span class="moin-transclusion" data-href="/page2">Single Line</span></p>' in rendered
-
-        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}"')
+        # transclude single row table within a paragraph, block element forces paragraph to be split into 2 parts
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'before {{page2}} after')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"|| table || cell ||")
         rendered = Item.create(u'page1').content._render_data()
-        assert 'Content of page2 is "</p>' in rendered
-        assert '<table>' in rendered
+        assert '<p>before </p><div class="moin-transclusion" data-href="/page2"><table' in rendered
+        assert '</table></div><p> after</p>' in rendered
         assert rendered.count('<table>') == 1
-
-        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}"')
+        # transclude two row table within a paragraph, block element forces paragraph to be split into 2 parts
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'before {{page2}} after')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"|| this || has ||\n|| two || rows ||")
         rendered = Item.create(u'page1').content._render_data()
-        assert 'Content of page2 is "</p>' in rendered
-        assert '<table>' in rendered
+        # inclusion of block item within a paragraph results in a before and after p
+        assert '<p>before </p><div class="moin-transclusion" data-href="/page2"><table' in rendered
+        assert '</table></div><p> after</p>' in rendered
         assert rendered.count('<table>') == 1
+        # transclude nonexistent item
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'before {{nonexistent}} after')
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>before <span class="moin-transclusion" data-href="/nonexistent"><a href="/+modify/nonexistent">' in rendered
+        assert '</a></span> after</p>' in rendered
+        # transclude empty item
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'text {{page2}} text')
+        update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <span class="moin-transclusion" data-href="/page2"></span> text</p>' in rendered
+    def test_InlineIncludeCreole(self):
+        # transclude single paragraph as inline using creole parser
+        update_item(u'creole', {CONTENTTYPE: u'text/x.moin.creole;charset=utf-8'}, u'creole item')
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.creole;charset=utf-8'}, u'before {{creole}} after')
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>before <span class="moin-transclusion" data-href="/creole">creole item</span> after</p>' in rendered
+    def test_InlineIncludeWithinMarkup(self):
+        # transclude single line item within italic and bold markup
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Normal ''italic '''bold {{page2}} bold''' italic'' normal")
+        update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Single Line")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>Normal <em>italic <strong>bold <span class="moin-transclusion" data-href="/page2">Single Line</span> bold</strong> italic</em> normal</p>' in rendered
+        # transclude double line item within italic and bold markup
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Normal ''italic '''bold {{page2}} bold''' italic'' normal")
+        update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Double\n\nLine")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>Normal <em>italic <strong>bold </strong></em></p><div class="moin-transclusion" data-href="/page2"><p>Double</p><p>Line</p></div><p><em><strong> bold</strong> italic</em> normal</p>' in rendered
+        # transclude single line item within comment
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"comment /* before {{page2}} after */")
+        update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Single Line")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>comment <span class="comment">before <span class="moin-transclusion" data-href="/page2">Single Line</span> after</span></p>' in rendered
+        # transclude double line item within comment
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"comment /* before {{page2}} after */")
+        update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Double\n\nLine")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>comment <span class="comment">before </span></p><div class="comment moin-transclusion" data-href="/page2"><p>Double</p><p>Line</p></div><p><span class="comment"> after</span></p>' in rendered
 
-    def test_InlineIncludeLogo(self):
+    def test_InlineIncludeImage(self):
         # the 3rd parameter, u'',  should be a binary string defining a png image, but it is not needed for this simple test
-        update_item(u'logo', {CONTENTTYPE: u'image/png'}, u'')
-
-        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{logo}}')
+        update_item(u'logo.png', {CONTENTTYPE: u'image/png'}, u'')
+        # simple transclusion
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{logo.png}}')
         rendered = Item.create(u'page1').content._render_data()
-        assert '<div class="moin-transclusion" data-href="/logo"><img alt="logo"' in rendered
-
-        # <p /> is not valid html5; should be <p></p>. to be valid.  Even better, there should be no empty p's.
-        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{logo}}{{logo}}')
+        assert '<p><span class="moin-transclusion" data-href="/logo.png"><img alt="logo.png" src=' in rendered
+        assert '/logo.png" /></span></p>' in rendered
+        # within paragraph
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'text {{logo.png}} text')
         rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <span class="moin-transclusion" data-href="/logo.png"><img alt="logo.png" src=' in rendered
+        assert '/logo.png" /></span> text</p>' in rendered
+        # within markup
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Normal ''italic '''bold {{logo.png}} bold''' italic'' normal")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>Normal <em>italic <strong>bold <span class="moin-transclusion" data-href="/logo.png"><img alt="logo.png" src=' in rendered
+        assert '/logo.png" /></span> bold</strong> italic</em> normal</p>' in rendered
+        # multiple transclusions
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{logo.png}}{{logo.png}}')
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p><span class="moin-transclusion" data-href="/logo.png"><img alt="logo.png" src=' in rendered
+        assert '/logo.png" /></span><span class="moin-transclusion" data-href="/logo.png"><img alt="logo.png" src=' in rendered
+        # check for old bug
         assert '<p />' not in rendered
         assert '<p></p>' not in rendered
+
+    def test_IncludeAsLinkAlternate(self):
+        # image as link alternate
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"text [[page2|{{logo.png}}]] text")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <a href="/page2"><span class="moin-transclusion" data-href="/logo.png"><img alt="logo.png" src="' in rendered
+        assert '/logo.png" /></span></a> text</p>' in rendered
+        # link alternate with image embedded in markup
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"text [[page2|plain '''bold {{logo.png}} bold''' plain]] text")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <a href="/page2">plain <strong>bold <span class="moin-transclusion" data-href="/logo.png"><img alt="logo.png" src="' in rendered
+        assert '/logo.png" /></span> bold</strong> plain</a> text</p>' in rendered
+        # nonexistent image used in link alternate
+        # XXX html validation errora: A inside A - the image alternate turns into an A-tag to create the non-existant image.  Error is easily seen.
+        # IE9, Firefox, Chrome, Safari, and Opera display this OK;  the only usable hyperlink is to create the missing image.
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"text [[page2|{{logoxxx.png}}]] text")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <a href="/page2"><span class="moin-transclusion" data-href="/logoxxx.png"><a href="/+modify/logoxxx.png">' in rendered
+        assert '</a></span></a> text</p>' in rendered
+        # image used as alternate to nonexistent page
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u"text [[page2xxx|{{logo.png}}]] text")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <a class="moin-nonexistent" href="/page2xxx"><span class="moin-transclusion" data-href="/logo.png"><img alt="logo.png" src="' in rendered
+        assert '/logo.png" /></span></a> text</p>' in rendered
+        # transclude block elem as link alternate to nonexistent page
+        # XXX html validation errors, block element inside A.
+        # IE9, Firefox, Chrome, Safari, and Opera display this OK;  the hyperlink is the entire div enclosing the block elem
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'text [[MyPage|{{page2}}]] text')
+        update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Double\n\nLine")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <a class="moin-nonexistent" href="/MyPage"><div class="moin-transclusion" data-href="/page2"><p>Double</p><p>Line</p></div></a> text</p>' in rendered
+        # transclude empty item as link alternate to nonexistent page
+        # hyperlink will be empty span and invisible
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'text [[MyPage|{{page2}}]] text')
+        update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"")
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <a class="moin-nonexistent" href="/MyPage"><span class="moin-transclusion" data-href="/page2"></span></a> text</p>' in rendered
+        # transclude external page as link alternate to nonexistent page
+        update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'text [[MyPage|{{http://moinmo.in}}]] text')
+        rendered = Item.create(u'page1').content._render_data()
+        assert '<p>text <a class="moin-nonexistent" href="/MyPage"><object class="moin-http moin-transclusion" data="http://moinmo.in" data-href="http://moinmo.in">http://moinmo.in</object></a> text</p>' in rendered

MoinMoin/converter/html_out.py

                 attribs = elem.attrib.copy()
                 if moin_page.page_href in attribs:
                     del attribs[moin_page.page_href]
+                if attribs and len(item) == 1:
+
+                    if item[0].tag.name in ('object', 'a'):
+                        # png, jpg, gif are objects here, will be changed to img when they are processed
+                        # transclusion is a single inline element "My pet {{bird.jpg}} flys." or "[[SomePage|{{Logo.png}}]]"
+                        return self.new_copy(html.span, item, attribs)
+
+                    elif item[0].tag.name == 'p':
+                        # transclusion is a single p-tag that can be coerced to inline  "Yes, we have {{no}} bananas."
+                        new_span = html.span(children=item[0][:])
+                        return self.new_copy(html.span, new_span, attribs)
+
+                # transclusion is a block element
                 return self.new_copy(html.div, item, attribs)
 
         raise RuntimeError('page:page need to contain exactly one page:body tag, got {0!r}'.format(elem[:]))

MoinMoin/converter/include.py

 
 Although this module is named include.py, many comments within and the moin docs
 use the word transclude as defined by http://www.linfo.org/transclusion.html, etc.
+
+Adjusting the DOM
+=================
+
+After expanding the include elements, in many cases it is necessary to adjust
+the DOM to prevent the generation of invalid HTML.  Using a simple example,
+"\n{{SomeItem}}\n", the starting DOM structure created by the moinwiki_in.py
+(or other parser) is:
+
+    Page > Body > P > Include
+
+After expansion of the Include, the structure will be:
+
+    Page > Body > P > Page > Body > (P | Div | Object |...)
+
+moinwiki_in.py (or other parser) does not adjust the DOM structure based upon
+whether the contents of the transcluded item are inline or block.  Sometime after
+include processing is complete, html_out.py will convert the transcluded
+Body > Page into a Div or Span wrapping the transclusion contents.
+
+This works well for things like "\n||mytable||{{BlockOrInline}}||\n" where
+almost any type of element is valid within a table cell's td.
+
+But without DOM adjustment, "\n{{Block}}\n" will generate invalid HTML
+because html_out.py will convert the DOM structure:
+
+    Page > Body > P > Page > Body > (Pre | Div | P, P... | ...)
+
+into:
+
+    ...<body><p><div>...</div></p></body>...
+
+where the </p> is invalid.
+
+In some cases it is desirable to coerce a transcluded small image or phrase into a
+inline element embedded within a paragraph. Here html_out.py will wrap the transclusion in
+a Span rather than a Div or convert a P-tag containing a phrase into a Span.
+
+    "My pet {{bird.jpg}} flys.", "[[SomePage|{{Logo.png}}]]" or "Yes, we have {{no}} bananas."
+
+In complex cases where a block level item is transcluded within the midst of
+several levels of text markup, such as:
+
+   "\nplain ''italic '''bold {{BlockItem}} bold''' italic'' plain\n"
+
+then we must avoid generating invalid html like:
+
+    <p>plain <emphasis>italic <strong>bold <div>
+    ...</div> bold</strong> italic</emphasis> plain</p>
+
+where <div...</div> contains the transcluded item, but rather:
+
+    <p>plain <emphasis>italic <strong>bold</strong></emphasis></p><div>
+    ...</div><p><emphasis><strong> bold</strong> italic</emphasis> plain</p>
+
+In these complex cases, we must build a DOM structure that will replace
+the containing element's parent, grand-parent, great-grand-parent...
+
+When a block element is embedded within a comment, it is important that the
+class="comment" is copied to the transclusion to provide the show/hide and
+highlighted styles normally applied to comments.
+
+    "\n/* normal ''italic ~-small {{detail.csv}} small-~ italic'' normal */\n".
+
+Conveniently, the class="comment" is added to the span element within the
+moinwiki_in.py parser and is available to include.py.  However, the moin-big
+and moin-small classes are applied to span elements by html_out.py so those
+classes are not available.  Italic, bold, stroke, and underline styling
+effects are implemented through specialized tags rather than CSS classes.
+In the example above, only class="comment" will be applied to detail.csv.
 """
 
 
 from __future__ import absolute_import, division
 
 from emeraldtree import ElementTree as ET
-import re, types
+import re, types, copy
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
 from MoinMoin.items import Item
 from MoinMoin.util.mime import type_moin_document
 from MoinMoin.util.iri import Iri, IriPath
-from MoinMoin.util.tree import moin_page, xinclude, xlink
+from MoinMoin.util.tree import html, moin_page, xinclude, xlink
 
 from MoinMoin.converter.html_out import mark_item_as_transclusion, Attributes
 
+# elements generated by moin wiki markup that cannot have block children
+NO_BLOCK_CHILDREN = [
+        'p',
+        'span', # /*comment*/, ~+big+~, ~-small-~ via classes comment, moin-big, moin-small
+        'emphasis', # ''italic''
+        'strong', # '''bold'''
+        'del', # --(stroke)--
+        'ins', # __underline__
+        # 'sub', # ,,subscript,, # no markup allowed within subscripts
+        # 'sup', # ^superscript^ # no markup allowed within superscripts
+        'a', # [[SomeItem|{{logo.png}}]]
+        ]
+
 
 class XPointer(list):
     """
             return cls()
 
     def recurse(self, elem, page_href):
-        # on first call, elem.tag.name=='page'. Decendants (body, div, p, include, page, etc.) are processed by recursing through DOM
+        # on first call, elem.tag.name=='page'. Descendants (body, div, p, include, page, etc.) are processed by recursing through DOM
 
         # stack is used to detect transclusion loops
         page_href_new = elem.get(self.tag_page_href)
             while i < len(elem):
                 child = elem[i]
                 if isinstance(child, ET.Node):
-                    # almost everything in the DOM will be an ET.Node, exceptions are unicode nodes under p nodes
 
                     ret = self.recurse(child, page_href)
 
                     if ret:
-                        # "Normally" we are here because child.tag.name==include and ret is a transcluded item (ret.tag.name=page, image, or object, etc.)
-                        # that must be inserted into the DOM replacing elem[i].
-                        # This is complicated by the DOM having many inclusions, such as "\n{{SomePage}}\n" that are a child of a "p".
-                        # To prevent generation of invalid HTML5 (e.g. "<p>text<p>text</p></p>"), the DOM must be adjusted.
-                        if isinstance(ret, types.ListType):
-                            # the transclusion may be a return of the container variable from below, add to DOM replacing the current node
-                            elem[i:i+1] = ret
-                        elif elem.tag.name == 'p':
-                            # ancestor P nodes with tranclusions  have special case issues, we may need to mangle the ret
+                        # Either child or a descendant of child is a transclusion.
+                        # See top of this script for notes on why these DOM adjustmenta are required.
+                        if isinstance(ret, ET.Node) and elem.tag.name in NO_BLOCK_CHILDREN:
                             body = ret[0]
-                            # check for instance where ret is a page, ret[0] a body, ret[0][0] a P
-                            if not isinstance(body, unicode) and ret.tag.name == 'page' and body.tag.name == 'body' and \
-                                len(body) == 1 and body[0].tag.name == 'p':
-                                # special case:  "some text {{SomePage}} more text" or "\n{{SomePage}}\n" where SomePage contains a single p.
-                                # the content of the transcluded P will be inserted directly into ancestor P.
-                                p = body[0]
+                            if len(body) == 0:
+                                # the transcluded item is empty, insert an empty span into DOM
+                                attrib = Attributes(ret).convert()
+                                elem[i] = ET.Element(moin_page.span, attrib=attrib)
+                            elif isinstance(body[0], ET.Node) and (len(body) > 1 or body[0].tag.name not in ('p', 'object', 'a')):
+                                # Complex case: "some text {{BlockItem}} more text" or "\n{{BlockItem}}\n" where
+                                # the BlockItem body contains multiple p's, a table, preformatted text, etc.
+                                # These block elements cannot be made a child of the current elem, so we create
+                                # a container to replace elem.
+                                # Create nodes to hold any siblings before and after current child (elem[i])
+                                before = copy.deepcopy(elem)
+                                after = copy.deepcopy(elem)
+                                before[:] = elem[0:i]
+                                after[:] = elem[i+1:]
+                                if len(before):
+                                    # there are siblings before transclude, save them in container
+                                    container.append(before)
+                                new_trans_ptr = len(container)
                                 # get attributes from page node; we expect {class: "moin-transclusion"; data-href: "http://some.org/somepage"}
                                 attrib = Attributes(ret).convert()
-                                # make new span node and "convert" p to span by copying all of p's children
-                                span = ET.Element(moin_page.span, attrib=attrib, children=p[:])
-                                # insert the new span into the DOM replacing old include, page, body, and p elements
-                                elem[i] = span
-                            elif not isinstance(body, unicode) and ret.tag.name == 'page' and body.tag.name == 'body':
-                                # special case: "some text {{SomePage}} more text" or "\n{{SomePage}}\n" and SomePage body contains multiple p's, a table, preformatted text, etc.
-                                # note: ancestor P may have text before or after include
-                                if i > 0:
-                                    # there is text before transclude, make new p node to hold text before include and save in container
-                                    pa = ET.Element(moin_page.p)
-                                    pa[:] = elem[0:i]
-                                    container.append(pa)
-                                # get attributes from page node; we expect {class: "moin-transclusion"; data-href: "http://some.org/somepage"}
-                                attrib = Attributes(ret).convert()
-                                # make new div node, copy all of body's children, and save in container
+                                # make new div node to hold transclusion, copy children, and save in container
                                 div = ET.Element(moin_page.div, attrib=attrib, children=body[:])
-                                container.append(div)
-                                 # empty elem of siblings that were just placed in container
-                                elem[0:i+1] = []
-                                if len(elem) > 0:
-                                    # there is text after transclude, make new p node to hold text, copy siblings, save in container
-                                    pa = ET.Element(moin_page.p)
-                                    pa[:] = elem[:]
-                                    container.append(pa)
-                                    elem[:] = []
-                                # elem is now empty so while loop will terminate and container will be returned up one level in recursion
+                                container.append(div) # new_trans_ptr is index to this
+                                if len(after):
+                                    container.append(after)
+                                if elem.tag.name == 'a':
+                                    # invalid input [[MyPage|{{BlockItem}}]], best option is to retain A-tag and fail html validation
+                                    # TODO: error may not be obvious to user - add error message
+                                    elem[i] = div
+                                else:
+                                    # move up 1 level in recursion where elem becomes the child and is usually replaced by container
+                                    return [container, new_trans_ptr]
                             else:
-                                # ret may be a unicode string: take default action
+                                # default action for odd things like circular transclusion error messages
                                 elem[i] = ret
+                        elif isinstance(ret, types.ListType):
+                            # a container has been returned. Note: there are two places where a container may be returned
+                            ret_container, trans_ptr = ret
+                            # trans_ptr points to the transclusion within ret_container.
+                            # Here the transclusion will always contain a block level element
+                            if elem.tag.name in NO_BLOCK_CHILDREN:
+                                # Complex case, transclusion effects grand-parent, great-grand-parent, e.g.:
+                                # "/* comment {{BlockItem}} */" or  "text ''italic {{BlockItem}} italic'' text"
+                                # elem is an inline element, build a bigger container to replace elem's parent,
+                                before = copy.deepcopy(elem)
+                                after = copy.deepcopy(elem)
+                                before[:] = elem[0:i] + ret_container[0:trans_ptr]
+                                after[:] = ret_container[trans_ptr+1:] + elem[i+1:]
+                                if len(before):
+                                    container.append(before)
+                                new_trans_ptr = len(container)
+                                # child may have classes like "comment" that must be added to transcluded element
+                                classes = child.attrib.get(moin_page.class_, '').split()
+                                classes += ret_container[trans_ptr].attrib.get(html.class_, '').split() # this must be html, not moin_page
+                                ret_container[trans_ptr].attrib[html.class_] = ' '.join(classes) # this must be html, not moin_page
+                                container.append(ret_container[trans_ptr]) # the transclusion
+                                if len(after):
+                                    container.append(after)
+                                return [container, new_trans_ptr]
+                            else:
+                                # elem is a block element, replace child element with the container generated in lower recursion
+                                elem[i:i+1] = ret_container # elem[i] is the child
+                                # avoid duplicate recursion over nodes already processed
+                                i += len(ret_container) -1
                         else:
-                            # default action for any ret not fitting special cases above
+                            # default action for any ret not fitting special cases above, e.g. tranclusion is within a table cell
                             elem[i] = ret
+                # we are finished with this child, advance to next sibling
                 i += 1
-            if len(container) > 0:
-                return container
 
         finally:
             self.stack.pop()