+
+namespace {
+std::string generateCssClassAtDepth(unsigned depth) {
+ std::string css_class = "entry";
+
+ while (depth > 0) {
+ depth -= 1;
+ css_class.insert(0, "sub");
+ }
+
+ return css_class;
+}
+
+struct IndexNode {
+ std::vector<IndexEntry> entries;
+ std::vector<IndexNode*> children;
+};
+
+docstring termAtLevel(const IndexNode* node, unsigned depth)
+{
+ // The typical entry has a depth of 1 to 3: the call stack would then be at most 4 (due to the root node). This
+ // function could be made constant time by copying the term in each node, but that would make data duplication that
+ // may fall out of sync; the performance benefit would probably be negligible.
+ if (!node->entries.empty()) {
+ LASSERT(node->entries.begin()->terms().size() >= depth + 1, return from_ascii(""));
+ return node->entries.begin()->terms()[depth];
+ }
+
+ if (!node->children.empty()) {
+ return termAtLevel(*node->children.begin(), depth);
+ }
+
+ LASSERT(false, return from_ascii(""));
+}
+
+void insertIntoNode(const IndexEntry& entry, IndexNode* node, unsigned depth = 0)
+{
+ // depth == 0 is for the root, not yet the index, hence the increase when going to vector size.
+ for (IndexNode* child : node->children) {
+ if (entry.terms()[depth] == termAtLevel(child, depth)) {
+ if (depth + 1 == entry.terms().size()) { // == child.entries.begin()->terms().size()
+ // All term entries match: it's an entry.
+ child->entries.emplace_back(entry);
+ return;
+ } else {
+ insertIntoNode(entry, child, depth + 1);
+ return;
+ }
+ }
+ }
+
+ // Out of the loop: no matching child found, create a new (possibly nested) child for this entry. Due to the
+ // possibility of nestedness, only insert the current entry when the right level is reached. This is needed if the
+ // first entry for a word has several levels that never appeared.
+ // In particular, this case is called for the first entry.
+ IndexNode* new_node = node;
+ do {
+ new_node->children.emplace_back(new IndexNode{{}, {}});
+ new_node = new_node->children.back();
+ depth += 1;
+ } while (depth + 1 <= entry.terms().size()); // depth == 0: root node, no text associated.
+ new_node->entries.emplace_back(entry);
+}
+
+IndexNode* buildIndexTree(vector<IndexEntry>& entries)
+{
+ // Sort the entries, first on the main entry, then the subentry, then the subsubentry,
+ // thanks to the implementation of operator<.
+ // If this operation is not performed, the algorithm below is no more correct (and ensuring that it works with
+ // unsorted entries would make its complexity blow up).
+ stable_sort(entries.begin(), entries.end());
+
+ // Cook the index into a nice tree data structure: entries at a given level in the index as a node, with subentries
+ // as children.
+ auto* index_root = new IndexNode{{}, {}};
+ for (const IndexEntry& entry : entries) {
+ insertIntoNode(entry, index_root);
+ }
+
+ return index_root;
+}
+
+void outputIndexPage(XMLStream & xs, const IndexNode* root_node, unsigned depth = 0)
+{
+ LASSERT(root_node->entries.size() + root_node->children.size() > 0, return);
+
+ xs << xml::StartTag("li", "class='" + generateCssClassAtDepth(depth) + "'");
+ xs << xml::CR();
+ xs << XMLStream::ESCAPE_NONE << termAtLevel(root_node, depth);
+ // By tree assumption, all the entries at this node have the same set of terms.
+
+ if (!root_node->entries.empty()) {
+ xs << XMLStream::ESCAPE_NONE << " — ";
+ unsigned entry_number = 1;
+
+ for (unsigned i = 0; i < root_node->entries.size(); ++i) {
+ const IndexEntry &entry = root_node->entries[i];
+
+ std::string const link_attr = "href='#" + entry.inset()->paragraphs()[0].magicLabel() + "'";
+ xs << xml::StartTag("a", link_attr);
+ xs << from_ascii(std::to_string(entry_number));
+ xs << xml::EndTag("a");
+
+ if (i < root_node->entries.size() - 1) {
+ xs << ", ";
+ }
+ entry_number += 1;
+ }
+ }
+
+ if (!root_node->entries.empty() && !root_node->children.empty()) {
+ xs << xml::CR();
+ }
+
+ if (!root_node->children.empty()) {
+ xs << xml::StartTag("ul", "class='" + generateCssClassAtDepth(depth) + "'");
+ xs << xml::CR();
+
+ for (const IndexNode* child : root_node->children) {
+ outputIndexPage(xs, child, depth + 1);
+ }
+
+ xs << xml::EndTag("ul");
+ xs << xml::CR();
+ }
+
+ xs << xml::EndTag("li");
+ xs << xml::CR();
+}
+
+// Only useful for debugging.
+void printTree(const IndexNode* root_node, unsigned depth = 0)
+{
+ static const std::string pattern = " ";
+ std::string prefix;
+ for (unsigned i = 0; i < depth; ++i) {
+ prefix += pattern;
+ }
+ const std::string prefix_long = prefix + pattern + pattern;
+
+ docstring term_at_level;
+ if (depth == 0) {
+ // The root has no term.
+ std::cout << "<ROOT>" << std::endl;
+ } else {
+ LASSERT(depth - 1 <= 10, return); // Check for overflows.
+ term_at_level = termAtLevel(root_node, depth - 1);
+ std::cout << prefix << to_utf8(term_at_level) << " (x " << std::to_string(root_node->entries.size()) << ")"
+ << std::endl;
+ }
+
+ for (const IndexEntry& entry : root_node->entries) {
+ if (entry.terms().size() != depth) {
+ std::cout << prefix_long << "ERROR: an entry doesn't have the same number of terms" << std::endl;
+ }
+ if (depth > 0 && entry.terms()[depth - 1] != term_at_level) {
+ std::cout << prefix_long << "ERROR: an entry doesn't have the right term at depth " << std::to_string(depth)
+ << std::endl;
+ }
+ }
+
+ for (const IndexNode* node : root_node->children) {
+ printTree(node, depth + 1);
+ }
+}
+}