Ikiwiki reparation🔗

Posted by Médéric Ribreux 🗓 In blog/ Sysadmin/

Introduction

I have used ikiwiki for four years as my blog engine. I like it very much and really enjoy the fact that everything is stored in a git repository. There are lots of plugins to deal with different sorts of directives to publish content more easily. With Ikiwiki I've been more focused on content than on the publishing processus.

But there is one point I needed to change and here it is how I achieve it !

What's the problem?

I really wanted to have a sidebar as a navigation tool in the blog. For me, a true sidebar needs a calendar, or as I said: a way to navigate in the blog using time.

But this has the great drawback to launch a rebuild of the whole site… I took time to understand what was the problem. The symptom was: whatever was published or modified in the blog part of the site, the blog was always completely rebuilt.

I modified the sidebar content to follow advices founded on Ikiwiki web site. But it changed nothing. Then I started to understand: whenever an article is published, the calendar needs to be rebuilded. As there is a calendar directive in sidebar.mdwn, the sidebar is rebuilt, then eveything in the blog is rebuilt as everything in the blog depends on sidebar !

Until now, it was not really a problem because all of my articles were very light with very few pictures. Since I post the Ireland article, the site took about 45 minutes to rebuild completely on my Sheevaplug. It was time to find a solution.

How I solved it

I am not a Perl developer anymore. I used to learn Perl more than ten years ago but now, I have learned Python and I don't want to rediscover the crap of Perl semantic and grammar. My workaround could not have been a Perl code. So what about something in HTML+Javascript? After all, ikiwiki is an html compiler!

So I just built a Javascript dynamic calendar. The code is executed everytime you load a page on the blog. It uses the CSS class of the original ikiwiki calendar plugin so there will be few changes on the rendering of the calendar. There is a main object called ikiwiki_cal which just build itself in the render method. It is initialized with the date of the current day or the month of the archive page if you are in a archive page (URL likes ../archives/2014/08). When you click on the next or prev month, calendar content is dynamically changed without reloading the page where you are.

On loading, an XMLHTTPRequest is made on a file named calendar.json. This file stores the list of all the articles published on the blog in a JSON format. With articles come date of publication (like printed in meta directives) and URL of the article. This JSON is parsed when you load the page and its content is used to build the calendar. When an article has been published during the current calendar month, the day by which it has been published becomes a link to the article.

It was my first Javascript/AJAX/JSON code. I just can say that some things were really fast to code and others (AJAX) was a real pain in the ass. To be short, I hate the grammar to declare a class (anonymous functions) and I took some time to really understand callback function mechanism…

Here is the code:

/*
 * Javascript Calendar for Ikiwiki
 * You have to define a JSON file that grab all of the articles date
 * to populate the calendar.
 * The goal of this code is to make ikiwiki sidebar plugin compatible with calendar
 *
 * Copyright 2014 Médéric RIBREUX
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

    Date.prototype.isLeap = function() {
	var year = this.getFullYear();
	return year % 400 === 0 || ( year % 4 === 0 && year % 100 !== 0);
    }

    Date.prototype.getDaysInMonth = function() {
	this.daysInMonth = [31,28,31,30,31,30,31,31,30,31,30,31][this.getMonth()]
	// Feb on leap year
	if (this.isLeap() && this.getMonth() == 1)  {
		this.daysInMonth += 1;
	}
	return this.daysInMonth;
    }

    //var articles;

    var ikiwiki_cal = {
    init: function(json_data) {
	this.days = ["Monday" ,"Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
	this.months = ["January","February","March","April","May","June","July","August","September","October","November","December"];

	//get URL:
	var url = window.location;
	this.month = this.year = null;
	// get arguments of the URL
	if (url.pathname.length > 1) {
	    slashes = url.pathname.substr(1)
	    if (slashes.charAt(slashes.length-1) == '/')
		slashes = slashes.substr(0,slashes.length-1);

	    url_elem = slashes.split('/');
	    if (url_elem.length > 1) {
		if (url_elem.indexOf("archives")) {
		    month = url_elem[url_elem.indexOf("archives")+2];
		    year = url_elem[url_elem.indexOf("archives")+1];
		    if (!(isNaN(parseInt(month)) || isNaN(parseInt(year)))) {
			this.month = parseInt(month)-1;
			this.year = parseInt(year);
		    }
		}
	    }
        }

	if (this.month == null || this.year == null) {
	    var today = new Date();
	    this.month = today.getMonth();
	    this.year = today.getFullYear();
	}

	this.element = document.getElementById('calendar');

	this.articles = json_data;

	},
    change_date: function(month, year) {
	this.month = month;
	this.year = year;
	this.render();
	},
    prev_month: function() {
	if (this.month == 0) {
		this.month = 11;
		this.year--;
	} else
		this.month--;

	this.render();
	},
    next_month: function() {
	if (this.month == 11) {
		this.month = 0;
		this.year++;
	} else
		this.month++;

	this.render();
	},
    articles_from_month: function(json_articles) {
	var lst = [];
	for(var i = 0; i < json_articles.length; i++) {
	    var dt = new Date(Date.parse(json_articles[i].date))
	    if (dt.getFullYear() == this.year && dt.getMonth() == this.month) {
		lst.push(json_articles[i]);
	    }
    	}

	return lst;

	},

    render: function() {
	var html = ''
	var articles = this.articles_from_month(this.articles);

	// what is the previous month ?
	var m = (this.month+1).toString();
	if (m.length == 1)
	    m = "0"+m;
	if (this.month == 0) {
		pm = 12;
		py = this.year-1;
	} else
		pm = this.month

	// What's the next month ?
	if (this.month == 11) {
		nm = 1;
		ny = this.year+1;
	} else
		nm = this.month+2

	if (pm.toString().length == 1)
	    pm = "0"+pm.toString();

	if (nm.toString().length == 1)
	    nm = "0"+nm.toString();

	// header
	html = '<table class="month-calendar">\n'
	html += '<tbody><tr>\n'
	html += '<th class="month-calendar-arrow">'
	html += '<a href="#" onclick="ikiwiki_cal.prev_month(); return false;" title="' + this.months[parseInt(pm)-1] +'">←'
	html += '</a></th>'
	html += '<th class="month-calendar-head" colspan="5">'
	html += '<a href="/blog/archives/' + this.year + '/' + m + '/" title="' + this.months[this.month]+'">'+this.months[this.month].substring(0,3)+' '+this.year+'</a></th>'
	html += '<th class="month-calendar-arrow">'
	html += '<a href="#" onclick="ikiwiki_cal.next_month(); return false;" title="' + this.months[parseInt(nm)-1] +'">→'
	html += '</a></th>'
	html += '</tr>'

	// days header:
	html += '<tr>'
	for (var i=0; i < this.days.length; i++) {
	    html += '<th class="month-calendar-day-head '+this.days[i]+'" title="'+this.days[i]+'">'+this.days[i].substring(0,1)+'</th>\n'
	}
        html +='</tr><tr>'

	// days body:
	var first_day_date = new Date(this.year, this.month, 1, 0, 0, 0);
	var first_day_num = first_day_date.getDay() == 0 ? 6 : first_day_date.getDay()-1;
	var last_day = first_day_date.getDaysInMonth();
	var last_day_date = new Date(this.year, this.month, last_day, 0, 0, 0, 0);
	var last_day_num = last_day_date.getDay() == 0 ? 6 : last_day_date.getDay()-1;

	for (var i = 0; i < (first_day_num+last_day+(6-last_day_num)); i++) {
	    if (i > 1 && i % 7 == 0 && i < (first_day_num + last_day)) {

		html += '</tr><tr>';
	    }
	    if (i<first_day_num || i>(first_day_num + last_day -1)) {
		html += '<td class="month-calendar-day-noday '+ this.days[i%7] + '">&nbsp;</td>';
	    }
	    else {
		var article = null;
		for(var j = 0; j < articles.length; j++) {
		    var art = articles[j];
		    var dt = new Date(Date.parse(art.date))

		    if (dt.getDate() == (i-first_day_num+1)) {
			article = art;
		    }
		}
		if (article != null)
		    html += '<td class="month-calendar-day-link '+ this.days[i%7] + '"><a href="'+article.url+'" title="'+article.title+'">'+(i-first_day_num+1).toString()+'</a></td>';
		else
		    html += '<td class="month-calendar-day-nolink '+ this.days[i%7] + '">'+(i-first_day_num+1).toString()+'</td>';
		    
		
	    }

	}
	html +='</tr>';

	
	// End:

	html += '</tbody></table>\n'
	
	this.element.innerHTML = html;
	}
    }

    // JSON Loading function:
    function json_request(callback) {
    var req = new XMLHttpRequest();

    req.onreadystatechange = function() {
        if (req.readyState == 4 && (req.status == 200 || req.status == 0)) {
	    callback(JSON.parse(req.responseText));
        }
    };

    req.open("GET", "/calendar.json", true);
    req.send();
    }

    // Callback function to deal with Ajax response:
    function json_do(jsondata) {
       //articles = jsondata;
      ikiwiki_cal.init(jsondata);
      ikiwiki_cal.render();
    }

    // Function to go to a precise month
    function go_to_month(month, year) {
      ikiwiki_cal.change_date(month, year);
    }

    // Main code
    window.addEventListener('load', function() {

    // Verify if we have a calendar:
    var calendar = document.getElementById("calendar");
    if (calendar != null) {
	// Just call the JSON request and create our Calendar
	json_request(json_do);
  }
});

How to deploy it ?

First, you have a Javascript file named calendar.js. Put it at the root of your repository. Ikiwiki just compile it by doing nothing. So it will be available at the root of your website.

The second step is to change some templates. The first one is the page template. Because of HTML scrutinization from Ikiwiki, you can't add a