1 module tocbuilder;
2 
3 import std.algorithm;
4 import std.array: back, empty;
5 import std.stdio;
6 import std.string: format;
7 import std.array;
8 
9 struct TocItem
10 {
11     private string name;
12     private string url;
13     private TocItem[] items;
14 
15     /// Computed by preCache() below ///
16 
17     /// Item name split by '.' This is an optimization (redundant with e.g. name.splitter)
18     private string[] nameParts;
19     /// Is this a package item?
20     private bool isPackage;
21     /// JS for opening/closing packages.
22     private string spanJS;
23 
24     /// HTML content of the list item (can be wrapped in any <li> or <span>).
25     private string listItem;
26 
27     /// Precompute any values that will be frequently reused.
28     private void preCache()
29     {
30         import std.string: split;
31         nameParts = name.split(".");
32         isPackage = items.length != 0;
33         if(url is null)
34         {
35             listItem = name;
36         }
37         else 
38         {
39             if(nameParts.length > 1)
40             {
41                 listItem ~= nameParts[0 .. $ - 1].join(".") ~ ".";
42             }
43             listItem ~= `<a href="%s">%s</a>`.format(url, nameParts.back);
44         }
45     }
46 
47     /** Write the TOC item.
48      *
49      * Params:
50      *
51      * dst        = Range to write to.
52      * moduleName = Name of the module/package in the documentation page of which
53      *              we're writing this TOC, if we're writing module/package documentation.
54      */
55     public void write(R)(ref R dst, string moduleName = "")
56     {
57         // Is this TOC item the module/package the current documentation page
58         // documents?
59         const isSelected = name == moduleName;
60 
61         dst.put(`<li>`);
62         const css = isPackage || isSelected;
63         
64         if(!css)
65         {
66             dst.put(listItem);
67         }
68         else
69         {
70             dst.put(`<span class="`);
71             if(isPackage)  { dst.put("package"); }
72             if(isSelected) { dst.put(" selected"); }
73             dst.put(`"`);
74             if(isPackage)  { dst.put(spanJS); }
75             dst.put(`>`);
76             dst.put(listItem);
77             dst.put("</span>\n"); 
78         }
79 
80         if (isPackage)
81         {
82             auto moduleParts = moduleName.splitter(".");
83             const block = moduleParts.startsWith(nameParts);
84             dst.put(`<ul id="`);
85             dst.put(name);
86             dst.put(`"`);
87             if (moduleParts.startsWith(nameParts))
88             {
89                 dst.put(` style='display:block; margin-top: 0.5em;'`);
90             }
91             dst.put(">\n");
92 
93             foreach (item; items)
94                 item.write(dst, moduleName);
95             // End a package's list of members
96             dst.put("</ul>\n");
97         }
98         dst.put("</li>\n");
99     }
100 }
101 
102 TocItem[] buildTree(string[] strings, string[string] links, const size_t offset = 0)
103 {
104     TocItem[] items;
105     size_t i = 0;
106     strings.sort();
107     while (i < strings.length)
108     {
109         size_t j = i + 1;
110         auto s = strings[i][offset .. $].findSplit(".");
111         const string prefix = s[0];
112         string suffix = s[2];
113         TocItem item;
114         if (prefix.length != 0 && suffix.length != 0)
115         {
116             while (j < strings.length && strings[j][offset .. $].startsWith(prefix ~ s[1]))
117                 j++;
118             if (i < j)
119             {
120                 size_t o = offset + prefix.length + 1;
121                 item.items = buildTree(strings[i .. j], links, o);
122             }
123         }
124         else
125             item.url = links[strings[i]];
126 
127         item.name = strings[i][0 .. item.items.empty ? $ : offset + prefix.length];
128 
129         if(items.length > 0 && items.back.name == item.name)
130         {
131             items.back.items = item.items;
132         }
133         else
134         {
135             items ~= item;
136         }
137 
138         i = j;
139     }
140     foreach(ref item; items)
141     {
142         item.preCache();
143     }
144     return items;
145 }