An FAQ section on a site often contains a lot of pages pertaining to various topics. Manually organizing these page is a lot of effort and the built-in Wiki.Tree and Wiki.Directory functions are not discriminating enough to be useful in this case.
Instead, I wanted to rely on tags to organize the content for me by topics. This way, each page could belong to more than one topic if needed. Also, I wanted the presentation to be split up over two columns. For the latter, I had two options: either rely on CSS to float the sections or implement a balancing algorithm that would always guarantee to the optimal balancing of both columns. Not being one to chose the easy route, I opted for the latter!
This code can be seen in its fully glory on the DekiScript Samples/FAQ page. The scripts on this page were tested on 8.08.2.
The first step of our code is to collect all pages and organize them by their tags. If a page has no tag, we'll put it into the "(unclassified)" list. Once this code has run, the variable tagmap will be a map of pages by tag. The order of pages is not really relevant at this point.
/* I use the 'base' variable to hold the starting point
* by default, it uses the current page. Replace it with
* wiki.getpage to use another page as the starting point
* for the enumeration.
*/
var base = page;
var tagmap = { };
foreach(var p in base.subpages) {
var tags = p.tags;
if(#tags) {
/* let's loop over each tag and collect a list of pages for each tag */
foreach(var t in tags) {
let tagmap ..= { (t.name) : tagmap[t.name] .. [ p ] };
}
} else {
/* this is a page without tags; I'm capturing it into an '(unclassified')
* section, but if you'd rather not see untagged pages, just remove this line.
*/
let tagmap ..= { '(unclassified)' : tagmap['(unclassified)'] .. [ p ] };
}
}
While we're far away from having our final result yet. It's not a bad idea to quickly check if we have what we think we have! To do so, we'll need to insert a bit of HTML code with some DekiScript.
The following code is displaying a sorted list of each tag using a <h3> section with a bulleted list of the linked page titles in alphabetical order. Note the use of String.ToCamelCase to automatically convert to uppercase the first letter of each tag.
<div init="(code from above)" foreach="var tagname in list.sort(map.keys(tagmap))">
<h3>{{ string.tocamelcase(tagname) }}</h3>
<ul>
<li foreach="var p in list.sort(tagmap[tagname], 'title')">{{ web.link(p.uri, p.title) }}</li>
</ul>
</div>
This step is a bit technical, but for those who care, here is how we're going to split up the collected pages into two evenly balanced columns.
In order to achieve the best balanced layout, we need to find the optimal distribution of content. Fortunately, there is a greedy (i.e. fast) algorithm that does exactly that!
This algorithm will ensure that we end up with two columns that have an optimal balance of items. The definition of optimal here is that no other partition of tags would have lead to a smaller discrepancy between the lengths of the left and right columns.
Ok, enough algorithmic mumbo-jumbo. Time to code this up! The following code immediately follows the code from Step 1.
/* First we need to create a sortable data-structure. To this end, we create tag_count
* as a list of key value pairs, where the key is the name of the tag and the value
* is the number of pages associated with the tag
*/
var tag_count = [ ];
foreach(var tag in map.keys(tagmap)) {
let tag_count ..= [ { tag: tag, count: #tagmap[tag] } ];
}
let tag_count = list.sort(tag_count, 'count', true);
/* Now we need to partition the tagmap across the two columns. Let's define two variables:
* a list and a counter for each column.
*/
var left_tags = [];
var left_tags_sum = 0;
var right_tags = [];
var right_tags_sum = 0;
foreach(var t in tag_count) {
/* check if the right columm has more items than the left column */
if(right_tags_sum > left_tags_sum) {
/* add tag to the left column */
let left_tags_sum += t.count;
let left_tags ..= [ t.tag ];
} else {
/* add tag to the right column */
let right_tags_sum += t.count;
let right_tags ..= [ t.tag ];
}
}
To display our results into two columns, I'm simply using a <table> element. CSS-purists may not like it, so if you know better. Don't hesitate to contribute! :)
But before we show anything, we first need to check if tagmap contains any results. If it doesn't, our fancy table display is going to be empty without any additional information. Instead, we should show some text that no pages were found.
Next, I found that while sorting tags alphabeticaly made sense, sorting the pages for each tag by their title wasn't that useful. Instead, let's sort the pages in decreasing order of their view count. After all, it makes sense that the most useful FAQ pages are the ones which have been looked up the most often. To this end, I also changed how each list entry is shown by adding the view count for each. Note the use of Num.Format to make sure that numbers are properly formatted.
<div init="(code from step 1 and step 2)">
<div if="#tagmap">
<table>
<tr valign="top">
<td style="padding-right: 20px;">
<!-- enumerate all tags in the left column in alphabetical order -->
<div foreach="var tag in list.sort(left_tags)">
<h5>{{ string.tocamelcase(tag) }}</h5>
<!-- enumerate all pages by decreasing view-count -->
<ul init="var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left')">
<li foreach="var p in pages">
<span style="font-size: small;">{{ web.link(p.uri, p.title) }} </span><span style="color: rgb(128, 128, 128);font-size: small;">({{ num.format(p.viewcount, '#,##0') }} views)</span>
</li>
</ul>
</div>
</td>
<td style="padding-right: 20px;">
<!-- enumerate all tags in the right column in alphabetical order -->
<div foreach="var tag in list.sort(right_tags)">
<h5>{{ string.tocamelcase(tag) }}</h5>
<!-- enumerate all pages by decreasing view-count -->
<ul init="var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left')">
<li foreach="var p in pages">
<span style="font-size: small;">{{ web.link(p.uri, p.title) }} </span><span style="color: rgb(128, 128, 128);font-size: small;">({{ num.format(p.viewcount, '#,##0') }} views)</span>
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>
<div if="!#tagmap">No pages found</div>
</div>
To save you the time and effort from piecing together the code from steps 1 through 3, I've assembled them into a complete working example below. Enjoy!
<div init="
/* I use the 'base' variable to hold the starting point
* by default, it uses the current page. Replace it with
* wiki.getpage to use another page as the starting point
* for the enumeration.
*/
var base = page;
var tagmap = { };
foreach(var p in base.subpages) {
var tags = p.tags;
if(#tags) {
/* let's loop over each tag and collect a list of pages for each tag */
foreach(var t in tags) {
let tagmap ..= { (t.name) : tagmap[t.name] .. [ p ] };
}
} else {
/* this is a page without tags; I'm capturing it into an '(unclassified')
* section, but if you'd rather not see untagged pages, just remove this line.
*/
let tagmap ..= { '(unclassified)' : tagmap['(unclassified)'] .. [ p ] };
}
}
/* First we need to create a sortable data-structure. To this end, we create tag_count
* as a list of key value pairs, where the key is the name of the tag and the value
* is the number of pages associated with the tag
*/
var tag_count = [ ];
foreach(var tag in map.keys(tagmap)) {
let tag_count ..= [ { tag: tag, count: #tagmap[tag] } ];
}
let tag_count = list.sort(tag_count, 'count', true);
/* Now we need to partition the tagmap across the two columns. Let's define two variables:
* a list and a counter for each column.
*/
var left_tags = [];
var left_tags_sum = 0;
var right_tags = [];
var right_tags_sum = 0;
foreach(var t in tag_count) {
/* check if the right columm has more items than the left column */
if(right_tags_sum > left_tags_sum) {
/* add tag to the left column */
let left_tags_sum += t.count;
let left_tags ..= [ t.tag ];
} else {
/* add tag to the right column */
let right_tags_sum += t.count;
let right_tags ..= [ t.tag ];
}
}">
<div if="#tagmap">
<table>
<tr valign="top">
<td style="padding-right: 20px;">
<!-- enumerate all tags in the left column in alphabetical order -->
<div foreach="var tag in list.sort(left_tags)">
<h5>{{ string.tocamelcase(tag) }}</h5>
<!-- enumerate all pages by decreasing view-count -->
<ul init="var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left')">
<li foreach="var p in pages">
<span style="font-size: small;">{{ web.link(p.uri, p.title) }} </span><span style="color: rgb(128, 128, 128);font-size: small;">({{ num.format(p.viewcount, '#,##0') }} views)</span>
</li>
</ul>
</div>
</td>
<td style="padding-right: 20px;">
<!-- enumerate all tags in the right column in alphabetical order -->
<div foreach="var tag in list.sort(right_tags)">
<h5>{{ string.tocamelcase(tag) }}</h5>
<!-- enumerate all pages by decreasing view-count -->
<ul init="var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left')">
<li foreach="var p in pages">
<span style="font-size: small;">{{ web.link(p.uri, p.title) }} </span><span style="color: rgb(128, 128, 128);font-size: small;">({{ num.format(p.viewcount, '#,##0') }} views)</span>
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>
<div if="!#tagmap">No pages found</div>
</div>
/block, line 40, column 18: ")" expected
If I then edit the page all of the code starting with "left_tags_sum" to the table is visible in the WYSIWYG editor. It is not there when I first switch from source to WYSIWYG, only after I save and edit.
Are there any good references for how to troubleshoot Deki Script? The syntax looks valid and I cannot find any unmatched elements which has led me to a dead end.
Thanks for any help.
More as I figure it out...