Commits

Anonymous committed 1fac6fb

Merge fix for #8925 from 0.12-stable

Comments (0)

Files changed (2)

trac/wiki/macros.py

     The `include` and `exclude` lists accept shell-style patterns.
     """)
 
-    SPLIT_RE = re.compile(r"([/ 0-9.]+)")
+    SPLIT_RE = re.compile(r"(/| )")
+    NUM_SPLIT_RE = re.compile(r"([0-9.]+)")
 
     def expand_macro(self, formatter, name, content):
         args, kw = parse_args(content)
         prefix = args[0].strip() if args else None
         hideprefix = args and len(args) > 1 and args[1].strip() == 'hideprefix'
-        minsize = max(int(kw.get('min', 2)), 2)
+        minsize = max(int(kw.get('min', 1)), 1)
+        minsize_group = max(minsize, 2)
         depth = int(kw.get('depth', -1))
         start = prefix.count('/') if prefix else 0
         format = kw.get('format', '')
             """
             page_paths = []
             for page in pages:
-                path = [elt.rstrip('/').strip() for elt in self.SPLIT_RE.split(
-                        wiki.format_page_name(omitprefix(page), split=True))]
+                path = [elt.strip() for elt in self.SPLIT_RE.split(
+                        self.NUM_SPLIT_RE.sub(r" \1 ", 
+                        wiki.format_page_name(omitprefix(page), split=True)))]
                 page_paths.append(([elt for elt in path if elt], page))
             return page_paths
 
             return [(wiki.format_page_name(omitprefix(page)).split("/"), page)
                     for page in pages]
 
-        # create the group hierarchy (same for group and hierarchy formats)
-        def split_in_groups(entries):
+        # the different tree structures, each corresponding to its rendering
+        def tree_group(entries):
             """Transform a flat list of entries into a tree structure.
             
             `entries` is a list of `(path_elements, page_name)` pairs
                 # grouping
                 grouped_entries = [(path_elements[1:], page_name)
                                    for path_elements, page_name in grouper]
-                if key and len(grouped_entries) >= minsize:
-                    subnodes = split_in_groups(sorted(grouped_entries))
+
+                if key and len(grouped_entries) >= minsize_group:
+                    subnodes = tree_group(sorted(grouped_entries))
                     if len(subnodes) == 1:
                         subkey, subnodes = subnodes[0]
-                        node = (key + subkey, subnodes) # FIXME
+                        node = (key + subkey, subnodes)
+                        groups.append(node)
+                    elif self.SPLIT_RE.match(key):
+                        for elt in subnodes:
+                            if isinstance(elt, tuple):
+                                subkey, subnodes = elt
+                                elt = (key + subkey, subnodes)
+                            groups.append(elt)
                     else:
                         node = (key, subnodes)
-                    groups.append(node)
+                        groups.append(node)                    
                 else:
                     for path_elements, page_name in grouped_entries:
                         groups.append(page_name)
             return groups
 
+        def tree_hierarchy(entries):
+            """Transform a flat list of entries into a tree structure.
+            
+            `entries` is a list of `(path_elements, page_name)` pairs
+            
+            Return a list organized in a tree structure, in which:
+              - a leaf is a `(rest, page)` pair, where:
+                - `rest` is the rest of the path to be shown
+                - `page` is a page name
+              - a node is a `(key, nodes, page)` pair, where:
+                - `key` is the leftmost of the path elements, common to the
+                  grouped (path element, page_name) entries
+                - `page` is a page name (if one exists for that node)
+                - `nodes` is a list of nodes or leaves
+            """
+            groups = []
+
+            for key, grouper in groupby(entries, lambda (elts, name):
+                                                    elts[0] if elts else ''):
+                grouped_entries  = [e for e in grouper]
+                sub_entries  = [e for e in grouped_entries if len(e[0]) > 1]
+                key_entries  = [e for e in grouped_entries if len(e[0]) == 1]
+                key_entry = key_entries[0] if key_entries else None
+                key_page = key_entry[1] if key_entries else None
+
+                if key and len(sub_entries) >= minsize:
+                    # remove key from path_elements in grouped entries for
+                    # further grouping
+                    sub_entries = [(path_elements[1:], page)
+                                   for path_elements, page in sub_entries]
+
+                    subnodes = tree_hierarchy(sorted(sub_entries))
+                    node = (key, key_page, subnodes)
+                    groups.append(node)
+                else:
+                    if key_entry:
+                        groups.append(key_entry)
+                    groups.extend(sub_entries)
+            return groups
+            
         # the different rendering formats
         def render_group(group):
             return tag.ul(
-                tag.li(isinstance(elt, tuple) and 
-                       tag(tag.strong(elt[0]), render_group(elt[1])) or
-                       tag.a(wiki.format_page_name(elt),
+                tag.li(tag(tag.strong(elt[0].strip('/')), render_group(elt[1]))
+                       if isinstance(elt, tuple) else
+                       tag.a(wiki.format_page_name(omitprefix(elt)),
                              href=formatter.href.wiki(elt)))
                 for elt in group)
 
         def render_hierarchy(group):
             return tag.ul(
-                tag.li(isinstance(elt, tuple) and 
-                       tag(tag.a(elt[0], href=formatter.href.wiki(elt[0])),
-                           render_hierarchy(elt[1][0:])) or
-                       tag.a(elt.rpartition('/')[2],
-                             href=formatter.href.wiki(elt)))
+                tag.li(tag(tag.a(elt[0], href=formatter.href.wiki(elt[1]))
+                           if elt[1] else tag(elt[0]),
+                           render_hierarchy(elt[2]))
+                       if len(elt) == 3 else
+                       tag.a('/'.join(elt[0]),
+                             href=formatter.href.wiki(elt[1])))
                 for elt in group)
         
-        splitter, renderer = {
-            'group':     (split_pages_group,     render_group),
-            'hierarchy': (split_pages_hierarchy, render_hierarchy),
-            }.get(format, (None, None))
+        transform = {
+            'group': lambda p: render_group(tree_group(split_pages_group(p))),
+            'hierarchy': lambda p: render_hierarchy(
+                                    tree_hierarchy(split_pages_hierarchy(p))),
+            }.get(format, None)
 
-        if splitter and renderer:
-            titleindex = renderer(split_in_groups(splitter(pages)))
+        if transform:
+            titleindex = transform(pages)
         else:
             titleindex = tag.ul(
                 tag.li(tag.a(wiki.format_page_name(omitprefix(page)),

trac/wiki/tests/macros.py

 [[TitleIndex(WikiStart/, format=hierarchy)]]
 ------------------------------
 <p>
-</p><div class="titleindex"><ul><li><a href="/wiki/WikiStart">WikiStart</a><ul><li><a href="/wiki/WikiStart/First">First</a></li><li><a href="/wiki/WikiStart/Second">Second</a></li><li><a href="/wiki/WikiStart/Third">Third</a></li></ul></li></ul></div><p>
+</p><div class="titleindex"><ul><li>WikiStart<ul><li><a href="/wiki/WikiStart/First">First</a></li><li><a href="/wiki/WikiStart/Second">Second</a></li><li><a href="/wiki/WikiStart/Third">Third</a></li></ul></li></ul></div><p>
 </p>
 ------------------------------
 ============================== TitleIndex, group format, prefix hidden
 [[TitleIndex(Wiki,hideprefix,format=group)]]
 ------------------------------
 <p>
-</p><div class="titleindex"><ul><li><strong>End</strong><ul><li><a href="/wiki/WikiEnd/First">WikiEnd/First</a></li><li><a href="/wiki/WikiEnd/Second">WikiEnd/Second</a></li></ul></li><li><strong>Start</strong><ul><li><a href="/wiki/WikiStart">WikiStart</a></li><li><a href="/wiki/WikiStart/First">WikiStart/First</a></li><li><a href="/wiki/WikiStart/Second">WikiStart/Second</a></li><li><a href="/wiki/WikiStart/Third">WikiStart/Third</a></li></ul></li></ul></div><p>
+</p><div class="titleindex"><ul><li><strong>End</strong><ul><li><a href="/wiki/WikiEnd/First">End/First</a></li><li><a href="/wiki/WikiEnd/Second">End/Second</a></li></ul></li><li><strong>Start</strong><ul><li><a href="/wiki/WikiStart">Start</a></li><li><a href="/wiki/WikiStart/First">Start/First</a></li><li><a href="/wiki/WikiStart/Second">Start/Second</a></li><li><a href="/wiki/WikiStart/Third">Start/Third</a></li></ul></li></ul></div><p>
 </p>
 ------------------------------
 ============================== TitleIndex, hierarchy format, prefix hidden
 [[TitleIndex(format=group)]]
 ------------------------------
 <p>
-</p><div class="titleindex"><ul><li><strong>0.11</strong><ul><li><strong>Group</strong><ul><li><a href="/wiki/0.11/GroupOne">0.11/GroupOne</a></li><li><a href="/wiki/0.11/GroupTwo">0.11/GroupTwo</a></li></ul></li><li><a href="/wiki/0.11/Test">0.11/Test</a></li></ul></li><li><strong>Test</strong><ul><li><strong>0.11Abc</strong><ul><li><a href="/wiki/Test0.11/Abc">Test0.11/Abc</a></li><li><a href="/wiki/Test0.11Abc">Test0.11Abc</a></li></ul></li><li><strong>0.12</strong><ul><li><a href="/wiki/Test0.12Def">Test0.12Def</a></li><li><a href="/wiki/Test0.12Ijk">Test0.12Ijk</a></li></ul></li><li><strong>0.13</strong><ul><li><a href="/wiki/Test0.13alpha">Test0.13alpha</a></li><li><a href="/wiki/Test0.13beta">Test0.13beta</a></li></ul></li><li><a href="/wiki/Test0.131">Test0.131</a></li><li><a href="/wiki/Test2">Test2</a></li><li><a href="/wiki/TestTest">TestTest</a></li><li><a href="/wiki/TestThing">TestThing</a></li></ul></li><li><a href="/wiki/WikiStart">WikiStart</a></li></ul></div><p>
+</p><div class="titleindex"><ul><li><strong>0.11</strong><ul><li><strong>Group</strong><ul><li><a href="/wiki/0.11/GroupOne">0.11/GroupOne</a></li><li><a href="/wiki/0.11/GroupTwo">0.11/GroupTwo</a></li></ul></li><li><a href="/wiki/0.11/Test">0.11/Test</a></li></ul></li><li><strong>Test</strong><ul><li><strong>0.11</strong><ul><li><a href="/wiki/Test0.11/Abc">Test0.11/Abc</a></li><li><a href="/wiki/Test0.11Abc">Test0.11Abc</a></li></ul></li><li><strong>0.12</strong><ul><li><a href="/wiki/Test0.12Def">Test0.12Def</a></li><li><a href="/wiki/Test0.12Ijk">Test0.12Ijk</a></li></ul></li><li><strong>0.13</strong><ul><li><a href="/wiki/Test0.13alpha">Test0.13alpha</a></li><li><a href="/wiki/Test0.13beta">Test0.13beta</a></li></ul></li><li><a href="/wiki/Test0.131">Test0.131</a></li><li><a href="/wiki/Test2">Test2</a></li><li><a href="/wiki/TestTest">TestTest</a></li><li><a href="/wiki/TestThing">TestThing</a></li></ul></li><li><a href="/wiki/WikiStart">WikiStart</a></li></ul></div><p>
 </p>
 ------------------------------
 ============================== TitleIndex, compact format with prefix hidden, including Test0.13*
         ])
 
 
+TITLEINDEX5_MACRO_TEST_CASES = u"""
+============================== TitleIndex, hierarchy format with complex hierarchy
+[[TitleIndex(format=hierarchy)]]
+------------------------------
+<p>
+</p><div class="titleindex"><ul><li><a href="/wiki/TracDev">TracDev</a><ul><li><a href="/wiki/TracDev/ApiChanges">ApiChanges</a><ul><li><a href="/wiki/TracDev/ApiChanges/0.10">0.10</a></li><li><a href="/wiki/TracDev/ApiChanges/0.11">0.11</a></li><li><a href="/wiki/TracDev/ApiChanges/0.12">0.12</a><ul><li>Missing<ul><li><a href="/wiki/TracDev/ApiChanges/0.12/Missing/Exists">Exists</a></li></ul></li></ul></li></ul></li></ul></li><li><a href="/wiki/WikiStart">WikiStart</a></li></ul></div><p>
+</p>
+------------------------------
+============================== TitleIndex, hierarchy format with complex hierarchy (and min=5)
+[[TitleIndex(format=hierarchy,min=5)]]
+------------------------------
+<p>
+</p><div class="titleindex"><ul><li><a href="/wiki/TracDev">TracDev</a><ul><li><a href="/wiki/TracDev/ApiChanges">ApiChanges</a></li><li><a href="/wiki/TracDev/ApiChanges/0.10">ApiChanges/0.10</a></li><li><a href="/wiki/TracDev/ApiChanges/0.11">ApiChanges/0.11</a></li><li><a href="/wiki/TracDev/ApiChanges/0.12">ApiChanges/0.12</a></li><li><a href="/wiki/TracDev/ApiChanges/0.12/Missing/Exists">ApiChanges/0.12/Missing/Exists</a></li></ul></li><li><a href="/wiki/WikiStart">WikiStart</a></li></ul></div><p>
+</p>
+------------------------------
+============================== TitleIndex, group format with complex hierarchy
+[[TitleIndex(format=group)]]
+------------------------------
+<p>
+</p><div class="titleindex"><ul><li><strong>TracDev</strong><ul><li><a href="/wiki/TracDev">TracDev</a></li><li><strong>ApiChanges</strong><ul><li><a href="/wiki/TracDev/ApiChanges">TracDev/ApiChanges</a></li><li><a href="/wiki/TracDev/ApiChanges/0.10">TracDev/ApiChanges/0.10</a></li><li><a href="/wiki/TracDev/ApiChanges/0.11">TracDev/ApiChanges/0.11</a></li><li><strong>0.12</strong><ul><li><a href="/wiki/TracDev/ApiChanges/0.12">TracDev/ApiChanges/0.12</a></li><li><a href="/wiki/TracDev/ApiChanges/0.12/Missing/Exists">TracDev/ApiChanges/0.12/Missing/Exists</a></li></ul></li></ul></li></ul></li><li><a href="/wiki/WikiStart">WikiStart</a></li></ul></div><p>
+</p>
+------------------------------
+"""
+
+def titleindex5_setup(tc):
+    add_pages(tc, [
+        'TracDev',
+        'TracDev/ApiChanges',
+        'TracDev/ApiChanges/0.10',
+        'TracDev/ApiChanges/0.11',
+        'TracDev/ApiChanges/0.12',
+        'TracDev/ApiChanges/0.12/Missing/Exists',
+        ])
+
+
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(formatter.suite(IMAGE_MACRO_TEST_CASES, file=__file__))
     suite.addTest(formatter.suite(TITLEINDEX4_MACRO_TEST_CASES, file=__file__,
                                   setup=titleindex4_setup,
                                   teardown=titleindex_teardown))
+    suite.addTest(formatter.suite(TITLEINDEX5_MACRO_TEST_CASES, file=__file__,
+                                  setup=titleindex5_setup,
+                                  teardown=titleindex_teardown))
     return suite