changeset 2572:ce0787373150

introduce ID cache in request, make everybody generate valid IDs even inside includes fixes the TOC macro, top/bottom etc. links inside includes, also [#anchorlinks] etc.
author Johannes Berg <johannes AT sipsolutions DOT net>
date Thu, 26 Jul 2007 16:29:13 +0200
parents a7dc3cc36362
children fd896a933d58
files MoinMoin/formatter/__init__.py MoinMoin/formatter/text_html.py MoinMoin/formatter/text_python.py MoinMoin/macro/Include.py MoinMoin/macro/RandomQuote.py MoinMoin/macro/TableOfContents.py MoinMoin/parser/_tests/test_text_moin_wiki.py MoinMoin/request/__init__.py
diffstat 8 files changed, 277 insertions(+), 101 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/formatter/__init__.py	Thu Jul 26 16:26:44 2007 +0200
+++ b/MoinMoin/formatter/__init__.py	Thu Jul 26 16:29:13 2007 +0200
@@ -69,9 +69,13 @@
         return ""
 
     def startContent(self, content_id="content", **kw):
+        if self.page:
+            self.request.begin_include(self.page.page_name)
         return ""
 
     def endContent(self):
+        if self.page:
+            self.request.end_include()
         return ""
 
     # Links ##############################################################
@@ -309,6 +313,7 @@
         """ parser_name MUST be valid!
             writes out the result instead of returning it!
         """
+        # attention: this is copied into text_python!
         parser = wikiutil.searchAndImportPlugin(self.request.cfg, "parser", parser_name)
 
         args = self._get_bang_args(lines[0])
@@ -359,3 +364,9 @@
     def comment(self, text, **kw):
         return ""
 
+    def make_id_unique(self, id):
+        id = self.request.make_unique_id(id, self.request.include_id)
+        if self.request.include_id:
+            id = '%s.%s' % (
+                wikiutil.anchor_name_from_text(self.request.include_id), id)
+        return id
--- a/MoinMoin/formatter/text_html.py	Thu Jul 26 16:26:44 2007 +0200
+++ b/MoinMoin/formatter/text_html.py	Thu Jul 26 16:29:13 2007 +0200
@@ -17,6 +17,10 @@
 from MoinMoin.Page import Page
 from MoinMoin.action import AttachFile
 
+# insert IDs into output wherever they occur
+# warning: breaks toggle line numbers javascript
+_id_debug = False
+
 line_anchors = True
 prettyprint = False
 
@@ -184,11 +188,9 @@
         self._in_code = 0 # used by text_gedit
         self._in_code_area = 0
         self._in_code_line = 0
-        self._code_area_num = 0
         self._code_area_js = 0
         self._code_area_state = ['', 0, -1, -1, 0]
         self._show_section_numbers = None
-        self._content_ids = []
         self.pagelink_preclosed = False
         self._is_included = kw.get('is_included', False)
         self.request = request
@@ -316,13 +318,15 @@
             return ' '.join(all)
         return ''
 
-    def _open(self, tag, newline=False, attr=None, allowed_attrs=None, **kw):
+    def _open(self, tag, newline=False, attr=None, allowed_attrs=None,
+              is_unique=False, **kw):
         """ Open a tag with optional attributes (INTERNAL USE BY HTML FORMATTER ONLY!)
 
         @param tag: html tag, string
         @param newline: render tag so following data is on a separate line
         @param attr: dict with tag attributes
         @param allowed_attrs: list of allowed attributes for this element
+        @param is_unique: ID is already unique
         @param kw: arbitrary attributes and values
         @rtype: string ?
         @return: open tag with attributes as a string
@@ -330,6 +334,21 @@
         # If it is self-closing, then don't expect a closing tag later on.
         is_self_closing = (tag in _self_closing_tags) and ' /' or ''
 
+        # make ID unique
+        id = None
+        if not is_unique:
+            if attr and 'id' in attr:
+                attr['id'] = self.make_id_unique(attr['id'])
+                id = attr['id']
+            if 'id' in kw:
+                kw['id'] = self.make_id_unique(kw['id'])
+                id = kw['id']
+        else:
+            if attr and 'id' in attr:
+                id = attr['id']
+            if 'id' in kw:
+                id = kw['id']
+
         if tag in _blocks:
             # Block elements
             result = []
@@ -344,6 +363,8 @@
             result.append('<%s%s%s>' % (tag, attributes, is_self_closing))
             if newline:
                 result.append(self._newline())
+            if _id_debug and id:
+                result.append('(%s) ' % id)
             tagstr = ''.join(result)
         else:
             # Inline elements
@@ -384,23 +405,19 @@
         """ Start page content div.
 
         A link anchor is provided at the beginning of the div, with
-        an id of 'top' or 'top_xxxx' if the content_id argument is
-        set to 'xxxx'.
+        an id of 'top' (modified by the request ID cache).
         """
 
-        # Setup id
-        if content_id != 'content':
-            aid = 'top_%s' % (content_id, )
-        else:
-            aid = 'top'
-        self._content_ids.append(content_id)
+        if hasattr(self, 'page'):
+            self.request.begin_include(self.page.page_name)
+
         result = []
         # Use the content language
         attr = self._langAttr(self.request.content_lang)
         attr['id'] = content_id
         result.append(self._open('div', newline=False, attr=attr,
                                  allowed_attrs=['align'], **kw))
-        result.append(self.anchordef(aid))
+        result.append(self.anchordef('top'))
         if newline:
             result.append('\n')
         return ''.join(result)
@@ -409,23 +426,14 @@
         """ Close page content div.
 
         A link anchor is provided at the end of the div, with
-        an id of 'bottom' or 'bottom_xxxx' if the content_id argument
-        to the previus startContent() call was set to 'xxxx'.
+        an id of 'bottom' (modified by the request ID cache).
         """
 
-        # Setup id
-        try:
-            cid = self._content_ids.pop()
-        except:
-            cid = 'content'
-        if cid != 'content':
-            aid = 'bottom_%s' % (cid, )
-        else:
-            aid = 'bottom'
-
         result = []
-        result.append(self.anchordef(aid))
+        result.append(self.anchordef('bottom'))
         result.append(self._close('div', newline=newline))
+        if hasattr(self, 'page'):
+            self.request.end_include()
         return ''.join(result)
 
     def lang(self, on, lang_name):
@@ -552,6 +560,7 @@
         # Don't add newlines, \n, as it will break pre and
         # line-numbered code sections (from line_achordef() method).
         #return '<a id="%s"></a>' % (id, ) # do not use - this breaks PRE sections for IE
+        id = self.make_id_unique(id)
         return '<span class="anchor" id="%s"></span>' % wikiutil.escape(id, 1)
 
     def line_anchordef(self, lineno):
@@ -574,10 +583,13 @@
         The id argument, if provided, is instead the id of this link
         itself and not of the target element the link references.
         """
-
         attrs = self._langAttr()
         if name:
-            attrs['href'] = '#%s' % name
+            if self.request.include_id:
+                attrs['href'] = '#%s.%s' % (
+                    wikiutil.anchor_name_from_text(self.request.include_id), name)
+            else:
+                attrs['href'] = '#%s' % name
         if 'href' in kw:
             del kw['href']
         if on:
@@ -800,7 +812,6 @@
         non-break spaces.
         """
         tag = 'tt'
-        # Maybe we don't need this, because we have tt will be in inlineStack.
         self._in_code = on
         if on:
             return self._open(tag, allowed_attrs=[], **kw)
@@ -904,7 +915,7 @@
         must be unique within the document.  The show, start, and step are
         used for line numbering.
 
-        Note this is not like most formatter methods, it can not take any
+B        Note this is not like most formatter methods, it can not take any
         extra keyword arguments.
 
         Call once with on=1 to start the region, and a second time
@@ -912,12 +923,13 @@
         """
         _ = self.request.getText
         res = []
-        ci = self.request.make_unique_id('CA-%s_%03d' % (code_id, self._code_area_num))
         if on:
+            ci = self.make_id_unique('CA-%s' % code_id)
             # Open a code area
             self._in_code_area = 1
             self._in_code_line = 0
-            self._code_area_state = [ci, show, start, step, start]
+            # id in here no longer used
+            self._code_area_state = [None, show, start, step, start]
 
             # Open the code div - using left to right always!
             attr = {'class': 'codearea', 'lang': 'en', 'dir': 'ltr'}
@@ -935,13 +947,13 @@
 document.write('<a href="#" onclick="return togglenumber(\'%s\', %d, %d);" \
                 class="codenumbers">%s<\/a>');
 </script>
-''' % (self._code_area_state[0], self._code_area_state[2], self._code_area_state[3],
+''' % (ci, self._code_area_state[2], self._code_area_state[3],
        _("Toggle line numbers"))
                 res.append(toggleLineNumbersLink)
 
             # Open pre - using left to right always!
-            attr = {'id': self._code_area_state[0], 'lang': 'en', 'dir': 'ltr'}
-            res.append(self._open('pre', newline=True, attr=attr))
+            attr = {'id': ci, 'lang': 'en', 'dir': 'ltr'}
+            res.append(self._open('pre', newline=True, attr=attr, is_unique=True))
         else:
             # Close code area
             res = []
@@ -952,7 +964,6 @@
 
             # Update state
             self._in_code_area = 0
-            self._code_area_num += 1
 
         return ''.join(res)
 
@@ -1172,17 +1183,13 @@
             number = '.'.join([str(x) for x in self.request._fmt_hd_counters[self._show_section_numbers-1:]])
             if number: number += ". "
 
-        # make ID unique
-        if 'id' in kw:
-            kw['id'] = self.request.make_unique_id(kw['id'])
-
         # Add space before heading, easier to check source code
         result = '\n' + self._open('h%d' % heading_depth, **kw)
 
         if self.request.user.show_topbottom:
             result += "%s%s%s%s%s%s" % (
-                       self.url(1, "#bottom", do_escape=0), self.icon('bottom'), self.url(0),
-                       self.url(1, "#top", do_escape=0), self.icon('top'), self.url(0))
+                       self.anchorlink(1, "bottom"), self.icon('bottom'), self.anchorlink(0),
+                       self.anchorlink(1, "top"), self.icon('top'), self.anchorlink(0))
 
         return "%s%s" % (result, number)
 
--- a/MoinMoin/formatter/text_python.py	Thu Jul 26 16:26:44 2007 +0200
+++ b/MoinMoin/formatter/text_python.py	Thu Jul 26 16:29:13 2007 +0200
@@ -11,6 +11,7 @@
 import time
 from MoinMoin import wikiutil
 
+
 class Formatter:
     """
         Inserts '<<<>>>' into the page and adds python code to
@@ -76,10 +77,23 @@
                                  # this object reusable
         return "".join(source)
 
+    def __cache_if_no_id(self, name, *args, **kw):
+        if not 'id' in kw:
+            return getattr(self.formatter, name)(*args, **kw)
+        else:
+            return __insert_code('request.write(%s.%s(*%r, **%r))' %
+                (self.__formatter, name, args, kw))
+
     def __getattr__(self, name):
-        """ For every thing we have no method/attribute use the formatter
         """
-        return getattr(self.formatter, name)
+        For every thing we have no method/attribute use the formatter
+        unless there's an ID in the keywords.
+        """
+        attr = getattr(self.formatter, name)
+        if callable(attr):
+            return lambda *args, **kw: self.__cache_if_no_id(name, *args, **kw)
+        else:
+            return attr
 
     def __insert_code(self, call):
         """ returns the python code
@@ -180,6 +194,8 @@
 
     def macro(self, macro_obj, name, args):
         if self.__is_static(macro_obj.get_dependencies(name)):
+            # XXX: why is this necessary??
+            macro_obj.formatter = self
             return macro_obj.execute(name, args)
         else:
             return self.__insert_code(
@@ -197,10 +213,63 @@
             Dependencies = self.defaultDependencies
 
         if self.__is_static(Dependencies):
-            return self.formatter.parser(parser_name, lines)
+            # XXX: copied from FormatterBase because we can't inherit from it
+            parser = wikiutil.searchAndImportPlugin(self.request.cfg, "parser", parser_name)
+
+            args = self._get_bang_args(lines[0])
+            if args is not None:
+                lines = lines[1:]
+            p = parser('\n'.join(lines), self.request, format_args=args)
+            p.format(self)
+            del p
+            return ''
         else:
             return self.__insert_code('%s%s.parser(%r, %r)' %
                                       (self.__adjust_formatter_state(),
                                        self.__formatter,
                                        parser_name, lines))
 
+    def startContent(self, content_id='content', newline=True, **kw):
+        # we need to tell the request about the start of the content
+        return self.__insert_code('request.write(%s.startContent(content_id=%r, newline=%r, **%r))' %
+            (self.__formatter, content_id, newline, kw))
+
+    def endContent(self, newline=True):
+        # we need to tell the request about the end of the content
+        return self.__insert_code('request.write(%s.endContent(newline=%r))' %
+            (self.__formatter, newline))
+
+    def anchorlink(self, on, name='', **kw):
+        # anchorlink depends on state now, namely the include ID in the request.
+        if on:
+            return self.__insert_code('request.write(%s.anchorlink(on=%r, name=%r, **%r))' %
+                (self.__formatter, on, name, kw))
+        else:
+            return self.formatter.anchorlink(on, name=name, **kw)
+
+    def line_anchorlink(self, on, lineno=0):
+        # anchorlink depends on state now, namely the include ID in the request.
+        if on:
+            return self.__insert_code('request.write(%s.anchorlink(on=%r, %r))' %
+                (self.__formatter, on, lineno))
+        else:
+            return self.formatter.line_anchorlink(on, lineno)
+
+    def code_area(self, on, code_id, code_type='code', show=0, start=-1, step=-1):
+        if on:
+            # update state of the HTML formatter
+            self.formatter._in_code_area = 1
+            self.formatter._in_code_line = 0
+            self.formatter._code_area_state = [None, show, start, step, start]
+            return self.__insert_code('request.write(%s.code_area(True, %r, %r, %r, %r, %r))' %
+                (self.__formatter, code_id, code_type, show, start, step))
+        else:
+            return self.formatter.code_area(False, code_id, code_type, show, start, step)
+
+    def line_anchordef(self, lineno):
+        return self.__insert_code('request.write(%s.line_anchordef(%r))' %
+            (self.__formatter, lineno))
+
+    def anchordef(self, id):
+        return self.__insert_code('request.write(%s.anchordef(%r))' %
+            (self.__formatter, id))
--- a/MoinMoin/macro/Include.py	Thu Jul 26 16:26:44 2007 +0200
+++ b/MoinMoin/macro/Include.py	Thu Jul 26 16:29:13 2007 +0200
@@ -52,7 +52,7 @@
         titles.append((title_text, level))
     return titles
 
-def execute(macro, text, args_re=re.compile(_args_re_pattern), title_re=re.compile(_title_re, re.M), called_by_toc=0):
+def execute(macro, text, args_re=re.compile(_args_re_pattern), title_re=re.compile(_title_re, re.M)):
     request = macro.request
     _ = request.getText
 
@@ -172,10 +172,6 @@
         ##result.append("*** f=%s t=%s ***" % (from_re, to_re))
         ##result.append("*** f=%d t=%d ***" % (from_pos, to_pos))
 
-        if called_by_toc:
-            result.append(inc_page.get_raw_body())
-            continue
-
         if not hasattr(request, "_Include_backto"):
             request._Include_backto = this_page.page_name
 
@@ -207,9 +203,9 @@
         strfile = StringIO.StringIO()
         request.redirect(strfile)
         try:
-            cid = request.make_unique_id("Include_%s" % wikiutil.quoteWikinameURL(inc_page.page_name))
-            inc_page.send_page(content_only=1, content_id=cid,
-                               omit_footnotes=True)
+            inc_page.send_page(content_only=True,
+                               omit_footnotes=True,
+                               count_hit=False)
             result.append(strfile.getvalue())
         finally:
             request.redirect()
--- a/MoinMoin/macro/RandomQuote.py	Thu Jul 26 16:26:44 2007 +0200
+++ b/MoinMoin/macro/RandomQuote.py	Thu Jul 26 16:29:13 2007 +0200
@@ -49,7 +49,7 @@
     quote = random.choice(quotes)
     page.set_raw_body(quote, 1)
     quote = macro.request.redirectedOutput(page.send_page,
-        content_only=1, content_id="RandomQuote_%s" % wikiutil.quoteWikinameFS(page.page_name) )
+        content_only=1, content_id="RandomQuote")
 
     return quote
 
--- a/MoinMoin/macro/TableOfContents.py	Thu Jul 26 16:26:44 2007 +0200
+++ b/MoinMoin/macro/TableOfContents.py	Thu Jul 26 16:29:13 2007 +0200
@@ -2,6 +2,25 @@
 """
     MoinMoin - TableOfContents Macro
 
+    The macro works as follows: First, it renders the page using
+    the TOCFormatter (below) to get access to the outline of the
+    page. During the page rendering, only macros whose
+    'generates_headings' property is set and True are rendered,
+    most macros don't generate any headings and thus need not be
+    executed speeding up the process considerably.
+
+    The generated outline is then written to the output.
+
+    However, this is not all. Consider included pages that include
+    a TOC themselves! First of all, TOCs don't generate headings
+    so we avoid recursion during the collection process. Secondly,
+    we always keep track of which content we are in and the
+    formatter's heading method is responsible for making all
+    IDs they generate unique. We use the same algorithm to make
+    the IDs unique during the TOCFormatter rendering step so that
+    in the end we can output the same IDs and the TOC is linked
+    correctly, even in the case of multiple nested inclusions.
+
     @copyright: 2007 MoinMoin:JohannesBerg
     @license: GNU GPL, see COPYING for details.
 """
@@ -12,7 +31,8 @@
 from MoinMoin import wikiutil
 
 
-Dependencies = ['page']
+# cannot be cached because of TOCs in included pages
+Dependencies = ['time']
 
 class TOCFormatter(FormatterBase):
     def __init__(self, request, **kw):
@@ -25,10 +45,20 @@
             self.collected_headings[-1][2] += text
         return text
 
+    def startContent(self, *args, **kw):
+        res = FormatterBase.startContent(self, *args, **kw)
+        self.collected_headings.append([1, self.request.include_id, None])
+        return res
+
+    def endContent(self):
+        res = FormatterBase.endContent(self)
+        self.collected_headings.append([0, self.request.include_id, None])
+        return res
+
     def heading(self, on, depth, **kw):
         id = kw.get('id', None)
         if not id is None:
-            id = self.request.make_unique_id(kw['id'])
+            id = self.request.make_unique_id(id, self.request.include_id)
         self.in_heading = on
         if on:
             self.collected_headings.append([depth, id, u''])
@@ -53,8 +83,6 @@
     sysmsg = _anything_return_empty
     startDocument = _anything_return_empty
     endDocument = _anything_return_empty
-    startContent = _anything_return_empty
-    endContent = _anything_return_empty
     pagelink = _anything_return_empty
     interwikilink = _anything_return_empty
     url = _anything_return_empty
@@ -114,7 +142,9 @@
 
     pname = macro.formatter.page.page_name
 
+    macro.request.push_unique_ids()
     macro.request._tocfm_collected_headings = []
+
     tocfm = TOCFormatter(macro.request)
     p = Page(macro.request, pname, formatter=tocfm, rev=macro.request.rev)
     output = macro.request.redirectedOutput(p.send_page,
@@ -122,7 +152,7 @@
                                             count_hit=False,
                                             omit_footnotes=True)
 
-    macro.request.reset_unique_ids()
+    macro.request.pop_unique_ids()
 
     _ = macro.request.getText
 
@@ -134,23 +164,48 @@
     ]
 
     lastlvl = 0
+    old_incl_id = macro.request.include_id
+    macro.request.include_id = None
 
     for lvl, id, txt in macro.request._tocfm_collected_headings:
+        if txt is None:
+            incl_id = id
+            continue
         if lvl > maxdepth or not id:
             continue
+        if incl_id:
+            id = '%s.%s' % (wikiutil.anchor_name_from_text(incl_id), id)
+        need_li = lastlvl >= lvl
         while lastlvl > lvl:
-            result.append(macro.formatter.number_list(0))
+            result.extend([
+                macro.formatter.listitem(0),
+                macro.formatter.number_list(0),
+            ])
             lastlvl -= 1
         while lastlvl < lvl:
-            result.append(macro.formatter.number_list(1))
+            result.extend([
+                macro.formatter.number_list(1),
+                macro.formatter.listitem(1),
+            ])
             lastlvl += 1
+        if need_li:
+            result.extend([
+                macro.formatter.listitem(0),
+                macro.formatter.listitem(1),
+            ])
         result.extend([
-            macro.formatter.listitem(1),
+            '\n',
             macro.formatter.anchorlink(1, id),
             macro.formatter.text(txt),
             macro.formatter.anchorlink(0),
-            macro.formatter.listitem(0),
         ])
 
+    while lastlvl > 0:
+        macro.formatter.listitem(0),
+        result.append(macro.formatter.number_list(0))
+        lastlvl -= 1
+
+    macro.request.include_id = old_incl_id
+
     result.append(macro.formatter.div(0))
     return ''.join(result)
--- a/MoinMoin/parser/_tests/test_text_moin_wiki.py	Thu Jul 26 16:26:44 2007 +0200
+++ b/MoinMoin/parser/_tests/test_text_moin_wiki.py	Thu Jul 26 16:29:13 2007 +0200
@@ -40,8 +40,9 @@
         output = StringIO()
         saved_write = self.request.write
         self.request.write = output.write
+        self.request.page = page
         try:
-            Parser(body, self.request).format(page.formatter)
+            page.send_page(content_only=True, do_cache=False)
         finally:
             self.request.write = saved_write
         return output.getvalue()
@@ -472,15 +473,17 @@
 
     def testUrlAfterBlock(self):
         """ parser.wiki: tests url after block element """
-        cases = ('some text {{{some block text\n}}} and a URL http://moinmo.in/',
-                 'some text {{{some block text\n}}} and a WikiName')
+        case = 'some text {{{some block text\n}}} and a URL http://moinmo.in/'
 
-        for case in cases:
-            result = self.parse(case).strip()
-            match = result.endswith('</a>')
-            expected = True
-            self.assert_(match is True,
-                         'Expected "%(expected)s" but got "%(result)s"' % locals())
+        result = self.parse(case)
+        assert result.find('and a URL <a ') > -1
+
+    def testWikiNameAfterBlock(self):
+        """ parser.wiki: tests url after block element """
+        case = 'some text {{{some block text\n}}} and a WikiName'
+
+        result = self.parse(case)
+        assert result.find('and a <a ') > -1
 
     def testColorizedPythonParserAndNestingPreBrackets(self):
         """ tests nested {{{ }}} for the python colorized parser
@@ -492,10 +495,7 @@
 pattern = re.compile(r'{{{This is some nested text}}}')}}}"""
         output = self.parse(raw)
         output = ''.join(output)
-        result = "r'{{{This is some nested text}}}'" in output
-        expected = True
-
-        assert expected == result
+        assert "r'{{{This is some nested text}}}'" in output
 
     def testColorizedPythonParserAndNestingPreBracketsWithLinebreak(self):
         """ tests nested {{{ }}} for the python colorized parser
@@ -508,11 +508,7 @@
 }}}"""
         output = self.parse(raw)
         output = ''.join(output)
-        print output
-        result = "r'{{{This is some nested text}}}'" in output
-        expected = True
-
-        assert expected == result
+        assert "r'{{{This is some nested text}}}'" in output
 
     def testNestingPreBrackets(self):
         """ tests nested {{{ }}} for the wiki parser
@@ -523,10 +519,7 @@
 You can use {{{brackets}}}}}}"""
         output = self.parse(raw)
         output = ''.join(output)
-        result = 'You can use {{{brackets}}}' in output
-        expected = True
-
-        assert expected == result
+        assert 'You can use {{{brackets}}}' in output
 
     def testNestingPreBracketsWithLinebreak(self):
         """ tests nested {{{ }}} for the wiki parser
@@ -539,10 +532,7 @@
         output = self.parse(raw)
         output = ''.join(output)
         print output
-        result = 'You can use {{{brackets}}}' in output
-        expected = True
-
-        assert expected == result
+        assert 'You can use {{{brackets}}}' in output
 
     def testTextBeforeNestingPreBrackets(self):
         """ tests text before nested {{{ }}} for the wiki parser
@@ -553,10 +543,7 @@
 You can use {{{brackets}}}}}}"""
         output = self.parse(raw)
         output = ''.join(output)
-        result = 'Example <span class="anchor" id="line-0"></span><ul><li style="list-style-type:none"><span class="anchor" id="line-0"></span><pre>You can use {{{brackets}}}</pre>' in output
-        expected = True
-
-        assert expected == result
+        assert 'Example <span class="anchor" id="line-0-1"></span><ul><li style="list-style-type:none"><span class="anchor" id="line-0-2"></span><pre>You can use {{{brackets}}}</pre>' in output
 
     def testManyNestingPreBrackets(self):
         """ tests two nestings  ({{{ }}} and {{{ }}}) in one line for the wiki parser
@@ -580,7 +567,7 @@
         raw = 'def {{{ghi}}} jkl {{{mno}}} pqr'
         output = ''.join(self.parse(raw))
         # expected output copied from 1.5
-        expected = 'def <tt>ghi</tt> jkl <tt>mno</tt><span class="anchor" id="line-0"></span>pqr'
+        expected = 'def <tt>ghi</tt> jkl <tt>mno</tt><span class="anchor" id="line-0-1"></span>pqr'
         assert expected in output
 
 class TestLinkingMarkup(ParserTestCase):
@@ -622,4 +609,3 @@
 
 
 coverage_modules = ['MoinMoin.parser.text_moin_wiki']
-
--- a/MoinMoin/request/__init__.py	Thu Jul 26 16:26:44 2007 +0200
+++ b/MoinMoin/request/__init__.py	Thu Jul 26 16:29:13 2007 +0200
@@ -692,7 +692,7 @@
         self.current_lang = self.cfg.language_default
 
         # caches unique ids
-        self._page_ids = {}
+        self.init_unique_ids()
 
         if hasattr(self, "_fmt_hd_counters"):
             del self._fmt_hd_counters
@@ -1380,7 +1380,7 @@
         from MoinMoin import failure
         failure.handle(self, err)
 
-    def make_unique_id(self, base):
+    def make_unique_id(self, base, namespace=None):
         """
         Generates a unique ID using a given base name. Appends a running count to the base.
 
@@ -1388,20 +1388,72 @@
 
         @param base: the base of the id
         @type base: unicode
+        @param namespace: the namespace for the ID, used when including pages
 
-        @returns: an unique id
+        @returns: a unique (relatively to the namespace) ID
         @rtype: unicode
         """
         if not isinstance(base, unicode):
             base = unicode(str(base), 'ascii', 'ignore')
-        count = self._page_ids.get(base, -1) + 1
-        self._page_ids[base] = count
-        if count == 0:
+        if not namespace in self._page_ids:
+            self._page_ids[namespace] = {}
+        count = self._page_ids[namespace].get(base, -1) + 1
+        self._page_ids[namespace][base] = count
+        if not count:
             return base
         return u'%s-%d' % (base, count)
 
-    def reset_unique_ids(self):
+    def init_unique_ids(self):
+        '''Initialise everything needed for unique IDs'''
+        self._unique_id_stack = []
+        self._page_ids = {None: {}}
+        self.include_id = None
+        self._include_stack = []
+
+    def push_unique_ids(self):
+        '''
+        Used by the TOC macro, this ensures that the ID namespaces
+        are reset to the status when the current include started.
+        This guarantees that doing the ID enumeration twice results
+        in the same results, on any level.
+        '''
+        self._unique_id_stack.append((self._page_ids, self.include_id))
+        self.include_id, pids = self._include_stack[-1]
+        # make a copy of the containing ID namespaces, that is to say
+        # go back to the level we had at the previous include
         self._page_ids = {}
+        for namespace in pids:
+            self._page_ids[namespace] = pids[namespace].copy()
+
+    def pop_unique_ids(self):
+        '''
+        Used by the TOC macro to reset the ID namespaces after
+        having parsed the page for TOC generation and after
+        printing the TOC.
+        '''
+        self._page_ids, self.include_id = self._unique_id_stack.pop()
+
+    def begin_include(self, base):
+        '''
+        Called by the formatter when a document begins, which means
+        that include causing nested documents gives us an include
+        stack in self._include_id_stack.
+        '''
+        pids = {}
+        for namespace in self._page_ids:
+            pids[namespace] = self._page_ids[namespace].copy()
+        self._include_stack.append((self.include_id, pids))
+        self.include_id = self.make_unique_id(base)
+        if self.include_id == base:
+            self.include_id = None
+
+    def end_include(self):
+        '''
+        Called by the formatter when a document ends, restores
+        the current include ID to the previous one and discards
+        the page IDs state we kept around for push_unique_ids().
+        '''
+        self.include_id, pids = self._include_stack.pop()
 
     def httpDate(self, when=None, rfc='1123'):
         """ Returns http date string, according to rfc2068