// -*- coding: utf-8 -*-

//////////////////////////////////////////////////
// Outils divers et méthodes supplémentaires pour les classes de base

// Array
Array.prototype.indexOfItem = function(item) {
  for (var i = 0; i < this.length; i++) {
    if (this[i] == item) return i;
  }
  return -1;
}

Array.prototype.containsItem = function(item) {
  return this.indexOfItem(item) != -1;
}

Array.prototype.sortBy = function(field, reverse) {
    if (field == undefined) field = "title";
    this.sort(function(a, b) {
        if (a[field] == b[field]) {
            return 0;
        } else {
            var result = a[field] < b[field]? -1 : +1;
            if (reverse) result = -result;
            return result;
        }
     });
}

// String
String.prototype.addText = function(item) {
  var str = this;
  if (str != "") str += " ";
  str += item;
  return str;
}

String.prototype.expandVars = function(vars, recursive) {
    var text = this;

    // remplacer les éléments de date
    var now = vars && vars["now"]? Dat.valueOf(vars["now"]): new Date();
    text = now.expandVars(text);
    
    if (vars) {
        // remplacer la valeurs des variables
        var pos = 0;
        var re_var = new RegExp("\\$(?:([a-zA-Z0-9_]+)|{([^}]+)})", "mg");
        do {
            re_var.lastIndex = pos;
            var mo = re_var.exec(text);
            if (mo) {
                if (mo[1]) {
                    var name = mo[1];
                    var value = vars[name]? vars[name]: mo[0];
                } else {
                    var name = mo[2];
                    try {
                        var value = vars.eval(name);
                    } catch(e) {
                        var value = e.toString();
                    }
                }
                text = text.substr(0, mo.index) + value + text.substr(mo.index + mo[0].length);
                pos = mo.index;
                if (!recursive) pos += new String(value).length;
            }
        } while(mo);
    }
    
    return text;
}

function expandVars(template, vars, recursive) {
    return Str.valueOf(template).expandVars(vars, recursive);
}

String.prototype.firstLine = function() {
  // obtenir la première ligne non vide d'une chaine
  var text = this, line = "";
  var start = 0, pos;
  do {
    pos = text.indexOf("\n", start);
    if (pos == -1) {
      line = text.substr(start);
      break;
    } else {
      line = text.substring(start, pos);
      if (new RegExp("\\S").test(line)) {
        break;
      }
    }
    start = pos + 1;
  } while (pos != -1);

  return line;
}

// Date
Date.parseFr = function(text) {
    // créer une date à partir d'une chaine de la forme dd/mm/yyyy
    var re_date = new RegExp("([0-9]+)/([0-9]+)/([0-9]+)");
    var mo = re_date.exec(text);
    if (mo) {
        return new Date(parseInt(mo[3], 10), parseInt(mo[2], 10) - 1, parseInt(mo[1], 10));
    } else {
        return new Date(text);
    }
}

var MMM = ["Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Jui", "Aoû", "Sep", "Oct", "Nov", "Déc"];
var MMMM = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"];
var D = ["L", "M", "M", "J", "V", "S", "D"];
var DD = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];
var DDD = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"];
Date.prototype.expandVars = function(text) {
    text = text.replace(/%Y/g, this.getFullYear());
    text = text.replace(/%4m/g, MMMM[this.getMonth()]);
    text = text.replace(/%3m/g, MMM[this.getMonth()]);
    text = text.replace(/%0m/g, String.zeroPad(this.getMonth() + 1, 2));
    text = text.replace(/%m/g, this.getMonth());
    text = text.replace(/%3d/g, DDD[this.getDow()]);
    text = text.replace(/%2d/g, DD[this.getDow()]);
    text = text.replace(/%1d/g, D[this.getDow()]);
    text = text.replace(/%0d/g, String.zeroPad(this.getDate(), 2));
    text = text.replace(/%d/g, this.getDate());
    text = text.replace(/%H/g, this.getHours());
    text = text.replace(/%M/g, this.getMinutes());
    text = text.replace(/%S/g, this.getSeconds());
    return text;
}

Date.prototype.formatFr = function() {
    return this.expandVars("%0d/%0m/%Y");
}

Date.prototype.formatFrLong = function() {
    return this.expandVars("%3d %0d/%0m/%Y");
}

Date.prototype.getWom = function() {
    // obtenir le numéro de la semaine du mois: 0 - 4
    var lundi = this.getMonday();
    var day = lundi.getDate();
    return Math.floor(day/7);
}

Date.prototype.getDow = function() {
    // obtenir le jour de la semaine: 0(lundi) - 6(dimanche)
    var dow = this.getDay();
    dow = (dow + 6) % 7;
    return dow;
}

Date.prototype.getMidnight = function() {
    // obtenir la même date à minuit
    return new Date(this.getFullYear(), this.getMonth(), this.getDate());
}

Date.prototype.addMonths = function(nb) {
    // ajouter nb mois à la date, et retourner la date résultante à minuit
    var y = this.getFullYear(), m = this.getMonth(), d = this.getDate();
    m += nb;
    while (m < 0) {
        m += 12;
        y--;
    }
    while (m > 11) {
        m -= 12;
        y++;
    }
    return new Date(y, m, d);
}

Date.prototype.addDays = function(nb) {
    // ajouter nb jours à la date, et retourner la date résultante à minuit
    var y = this.getFullYear(), m = this.getMonth(), d = this.getDate();
    return new Date(y, m, d + nb);
}

Date.prototype.diffDays = function(date) {
    return (date.getMidnight() - this.getMidnight()) / 86400000;
}

Date.prototype.getMonday = function() {
    // obtenir le lundi de la semaine en cours
    return this.addDays(-this.getDow());
}

Date.prototype.getFirstDay = function() {
    // obtenir le premier jour du mois
    return new Date(this.getFullYear(), this.getMonth(), 1);
}

var NUM_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
Date.prototype.getNumDays = function() {
    // obtenir le nombre de jours de ce mois
    var m = this.getMonth();
    if (m == 1) {
        var y = this.getFullYear();
        var isleap = (y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0);
        return NUM_DAYS[m] + (isleap? 1: 0);
    } else {
        return NUM_DAYS[m];
    }
}

Date.prototype.getLastDay = function() {
    // obtenir le dernier jour du mois
    return new Date(this.getFullYear(), this.getMonth(), this.getNumDays());
}

// ClickHandler
var ClickHandler = {};
ClickHandler.getEvent = function(e) {
    if (!e) e = window.event;
    return e;
}
ClickHandler.end = function(e) {
    e.cancelBubble = true;
    if (e.stopPropagation) e.stopPropagation();
    return false;
}

// Str
var Str = {};
Str.valueOf = function(s) {
    if (typeof(s) == "string") s = new String(s);
    if (!(s instanceof String)) {
        if (s == null || s == undefined) s = "";
        s = new String(s);
    }
    return s;
}

// Dat
var Dat = {};
Dat.valueOf = function(d) {
    if (d instanceof String || typeof(d) == "string") {
        return Date.parseFr(d);
    } else if (d instanceof Date) {
        return d;
    } else {
        return new Date(d);
    }
}

// Bool
var Bool = {};
Bool.valueOf = function(b) {
    if (b instanceof String || typeof(b) == "string") {
        var b = Str.valueOf(b).toLowerCase();
        return ["oui", "o", "yes", "y", "vrai", "v", "true", "t", "1"].containsItem(b);
    }
    return Boolean(b).valueOf();
}

//////////////////////////////////////////////////
// Fonctions pour gérer les tags

Array.prototype.removeTag = function(tag) {
  var pos = this.indexOfItem(tag);
  if (pos != -1) {
    this.splice(pos, 1);
  }
  return this;
}

Array.prototype.addTag = function(tag) {
  this.pushUnique(tag);
  return this;
}

Array.prototype.addTags = function(tags) {
  if (tags instanceof String || typeof(tags) == "string") {
    tags = new String(tags).splitTags();
  }
  for (var i = 0; i < tags.length; i++) {
    this.addTag(tags[i]);
  }
}

Array.prototype.containsTag = function(tag) {
  return this.containsItem(tag);
}

Array.prototype.joinTags = function() {
  var result = [];
  for (var i = 0; i < this.length; i++) {
    result.push(String.encodeTiddlyLink(this[i]));
  }
  return result.join(" ");
}

String.prototype.splitTags = function() {
  return this.readBracketedList(true);
}

String.prototype.addTag = function(tag) {
  var thistags = this.splitTags();
  thistags.addTag(tag);
  return thistags.joinTags();
}

String.prototype.addTags = function(tags) {
  var thistags = this.splitTags();
  thistags.addTags(tags);
  return thistags.joinTags();
}

//////////////////////////////////////////////////
// Gestion des tiddlers et patches divers

// Tiddlers
var Tiddlers = {};
Tiddlers.valueOf = function(tiddler) {
    if (!(tiddler instanceof Tiddler)) {
        tiddler = store.tiddlers[tiddler];
    }
    return tiddler;
}

Tiddlers.getTags = function(tiddler) {
    // obtenir la valeur de tags pour un tiddler
    var tiddler = this.valueOf(tiddler);
    if (tiddler) return tiddler.getTags();
    return "";
}

Tiddlers.getSubtitle = function(tiddler) {
    // obtenir la valeur de subtitle pour un title
    var tiddler = this.valueOf(tiddler);
    if (tiddler) return tiddler.getSubtitle();
    return config.messages.subtitleUnknown;
}

Tiddlers.getFirstLine = function(tiddler) {
    var tiddler = this.valueOf(tiddler);
    if (tiddler) return tiddler.text.firstLine();
    return "";
}

// patches divers
var createTiddlyButton_patchedByBase = window.createTiddlyButton;
window.createTiddlyButton = function(parent, text, tooltip, action, klass, id, accessKey) {
    // afficher la touche de raccourci dans le title du bouton
    if (accessKey) {
        if (!tooltip) tooltip = "";
        tooltip = Str.valueOf(tooltip).addText("(Alt + " + accessKey + ")");
    }
    return createTiddlyButton_patchedByBase(parent, text, tooltip, action, klass, id, accessKey);
}

closeTiddler_patchedByBase = window.closeTiddler;
window.closeTiddler = function(title, slowly) {
    // patcher close pour désactiver l'animation sur la fermeture
    var saveAnimate = config.options.chkAnimate;
    config.options.chkAnimate = false;
    closeTiddler_patchedByBase(title, slowly);
    config.options.chkAnimate = saveAnimate;
}

displayTiddlers_patchedByBase = window.displayTiddlers;
window.displayTiddlers = function(src, titles, state, highlightText, highlightCaseSensitive, animate, slowly) {
    // patcher displayTiddlers pour permettre l'affichage d'une liste de tiddlers générés par une macro.
    // La macro doit implémenter textHandler pour être reconnue 
    if (titles.indexOf("<<") != -1) {
        var pos = 0;
        var re_macro = new RegExp("<<([^>\\s]+)(?:\\s*)([^>]*)>>", "mg");
        do {
            re_macro.lastIndex = pos;
            var mo = re_macro.exec(titles);
            if (mo) {
                var macro = config.macros[mo[1]];
                if (macro && macro.textHandler) {
                    var params = mo[2].readMacroParams();
                    var text = macro.textHandler(params);
                    titles = titles.substr(0, mo.index) + text + titles.substr(mo.index + mo[0].length);
                    pos = mo.index + new String(text).length;
                } else {
                    pos = mo.index + mo[0].length;
                }
            }
        } while (mo);
        titles = titles.replace(/<<([^>\s]+)(?:\s*)([^>]*)>>/g, "");
    }
    displayTiddlers_patchedByBase(src, titles, state, highlightText,highlightCaseSensitive,animate,slowly);
}

// patcher createTiddlerToolbar pour que la liste des boutons à afficher soit dans un tableau modifiable
config.views.wikified.toolbarClose.onClick = onClickToolbarClose;
config.views.wikified.toolbarEdit.onClick = onClickToolbarEdit;
config.views.wikified.toolbarEdit.notReadOnly = true;
config.views.wikified.toolbarPermalink.onClick = onClickToolbarPermaLink;
config.views.wikified.toolbarReferences.onClick = onClickToolbarReferences;
config.views.wikified.toolbarButtons = [
    config.views.wikified.toolbarClose,
    config.views.wikified.toolbarEdit,
    config.views.wikified.toolbarPermalink,
    config.views.wikified.toolbarReferences];

config.views.editor.toolbarDone.onClick = onClickToolbarSave;
config.views.editor.toolbarDone.notReadOnly = true;
config.views.editor.toolbarCancel.onClick = onClickToolbarUndo;
config.views.editor.toolbarDelete.onClick = onClickToolbarDelete;
config.views.editor.toolbarDelete.notReadOnly = true;
config.views.editor.toolbarButtons = [
    config.views.editor.toolbarDone,
    config.views.editor.toolbarCancel,
    config.views.editor.toolbarDelete];

// Create a tiddler toolbar according to whether it's an editor or not
createTiddlerToolbar = function(title, isEditor) {
    var theToolbar = document.getElementById("toolbar" + title);
    if(theToolbar) {
        var lingo = config.views;
        if (isEditor) lingo = lingo.editor;
        else lingo = lingo.wikified;
        
        removeChildren(theToolbar);
        insertSpacer(theToolbar);
        var toolbarButtons = lingo.toolbarButtons;
        for (var i = 0; i < toolbarButtons.length; i++) {
            var toolbarButton = toolbarButtons[i];
            
            if (i > 0) insertSpacer(theToolbar);
            if (!readOnly || !toolbarButton.notReadOnly) { 
                createTiddlyButton(theToolbar, toolbarButton.text, toolbarButton.tooltip, toolbarButton.onClick);
            }
        }
    }
}

function installToolbarButton(toolbarButton, editor, before) {
    // installer un bouton dans la vue wiki (par défaut) ou dans la vue éditeur (si editor==true), avant le bouton before
    var lingo = config.views;
    if (editor) lingo = lingo.editor;
    else lingo = lingo.wikified;
    
    var pos = -1;
    if (before) {
        pos = lingo.toolbarButtons.indexOfItem(before);
    }
    if (pos == -1) {
        lingo.toolbarButtons.push(toolbarButton);
    } else {
        lingo.toolbarButtons.splice(pos, 0, toolbarButton);
    }
}

function removeToolbarButton(toolbarButton, editor) {
    var lingo = config.views;
    if (editor) lingo = lingo.editor;
    else lingo = lingo.wikified;
    
    var pos = lingo.toolbarButtons.indexOfItem(toolbarButton);
    if (pos != -1) {
        lingo.toolbarButtons.splice(pos, 1);
    }
}

//////////////////////////////////////////////////
// Gestion des paramètres

// Process a string list of macro parameters into an array. Parameters can be quoted
// with "", '', [[]] or left unquoted (and therefore space-separated)
// On reconnait aussi les paramètres de la forme name="value", name='value' ou name=value
String.prototype.readMacroParams = function() {
    var regexpMacroParam = new RegExp("(?:\\s*)(?:" +
            "(?:\"([^\"]*)\")|" +
            "(?:'([^']*)')|" +
            "(?:\\[\\[([^\\]]*)\\]\\])|" +
            "([^\"'\\s]\\S*=(?:[^\"'\\s]\\S*|\"[^\"]*\"|'[^']*'))|" +
            "([^\"'\\s]\\S*)" +
        ")","mg");
    var params = [];
    do {
        var match = regexpMacroParam.exec(this);
        if(match) {
            if(match[1]) params.push(match[1]); // Double quoted
            else if(match[2]) params.push(match[2]); // Single quoted
            else if(match[3]) params.push(match[3]); // Double-square-bracket quoted
            else if(match[4]) params.push(match[4]); // name=value, name="value" ou name='value'
            else if(match[5]) params.push(match[5]); // Unquoted
        }
    } while(match);
    return params;
}

function Options(params) {
    // Dans un tableau de paramètre construit par String.readMacroParams, reconnaitre les options de la forme name=value
    // L'objet retourné contient le tableau arg qui est une copie de params sans les options, et une propriété pour chaque option.

    this.args = [];
    var re_option = new RegExp("([^\"'\\s]\\S*)=(?:([^\"'\\s]\\S*)|\"([^\"]*)\"|'([^']*)')");
    for (var i = 0; i < params.length; i++) {
        var mo = re_option.exec(params[i]);
        if (mo) {
            var name = mo[1];
            var value = mo[2]? mo[2]: mo[3]? mo[3]: mo[4]? mo[4]: "";
            this[name] = value;
        } else {
            this.args.push(params[i]);
        }
    }
}
Options.prototype.get = function(name, def) {
    // obtenir la valeur d'une option, avec une valeur par défaut
    if (this[name]) return this[name];
    return def;
}    
Options.prototype.getArg = function(index, def) {
    // obtenir la valeur d'un argument, avec une valeur par défaut
    if (this.args[index]) return this.args[index];
    return def;
}
Options.prototype.toParams = function() {
    var params = "";
    for (var i = 0; i < this.args.length; i++) {
        var arg = this.args[i];
        if (new RegExp("\\s").test(arg)) {
            params = params + " \"" + arg + "\"";
        } else {
            params = params + " " + arg;
        }
    }
    for (var prop in this) {
        if (prop != "args" && prop != "get" && prop != "getArg" && prop != "toParams") {
            params = params + " " + prop + "=";

            var arg = new String(this[prop]);
            if (new RegExp("\\s").test(arg)) {
                params = params + "\"" + arg + "\"";
            } else {
                params = params + arg;
            }
        }
    }
    return params;
}

//////////////////////////////////////////////////
// La macro newTiddler
// arguments: title text= tooltip= tags=

config.macros.newTiddler.handler = function(place, macroName, params) {
    // créer un nouveau tiddler avec le nom params[0] et les tags params[1..n]
    if(!readOnly) {
        var options = new Options(params);
        var legacy = options.args[0]? false: true;
        var tags = expandVars(options.get("tags", ""));
        if (legacy) {
            var title = config.macros.newTiddler.title;
            var tooltip = options.get("tooltip", config.macros.newTiddler.prompt);
            var text = options.get("text", config.macros.newTiddler.label);
        } else {
            var title = expandVars(options.args[0]);
            var tooltip = options.get("tooltip", "Create a new tiddler named " + title);
            var text = options.get("text", "new " + (tags? "tagged ": "") + "tiddler");
        }
        
        var onClick = function() {
            displayTiddler(null, title, 2, null, null, false, false);
            var tagsBox = document.getElementById("editorTags" + title);
            if(tagsBox) tagsBox.value = tagsBox.value.addTags(tags);
            if (legacy) {
                var e = document.getElementById("editorTitle" + title);
                e.focus();
                e.select();
            }
            return false;
        }
        createTiddlyButton(place, text, tooltip, onClick, undefined, undefined, legacy? this.accessKey: undefined);
    }
}

//////////////////////////////////////////////////
// Les macros de gestion d'événements

// La macro event
// arguments: date title nbDays= format=
config.macros.event = {
    TODAY: "aujourd'hui",
    TOMORROW: "demain",
    YESTERDAY: "hier",
    FUTURE_EVENT: "dans $remaining jours",
    PAST_EVENT: "il y a ${-remaining} jours",
    FORMAT: "$remainingDays: $title le $date"
};
config.macros.event.getRemainingDays = function(remaining) {
    if (remaining == 0) remainingDays = this.TODAY;
    else if (remaining == 1) remainingDays = this.TOMORROW;
    else if (remaining == -1) remainingDays = this.YESTERDAY;
    else if (remaining < 0) remainingDays = this.PAST_EVENT;
    else remainingDays = this.FUTURE_EVENT;
    
    return remainingDays;
}
config.macros.event.textHandler = function(params) {
    var options = new Options(params);
    var date = options.args[0]? Date.parseFr(options.args[0]): new Date();
    var title = options.args[1]? options.args[1]: "Unnamed event";
    var nbDays = parseInt(options.get("nbDays", "15"), 10);
    var format = options.get("format", this.FORMAT);

    var remaining = new Date().diffDays(date);
    var remainingDays = this.getRemainingDays(remaining);

    var vars = {remaining: remaining, remainingDays: remainingDays, title: title, date: date.formatFr()};
    if (Math.abs(remaining) <= nbDays) return expandVars(format, vars, true);
}
config.macros.event.handler = function(place, macroName, params) {
    var text = this.textHandler(params);
    if (text && text != "") wikify(text, place);
}

// La macro showEvents
// arguments: nbDays= format= headerFormat= noEventsFormat=
config.macros.showEvents = {
    NO_EVENTS_FORMAT: "Il n'y a pas d'événements en cours",
    HEADER_FORMAT: "|Date|Evénement|",
    FORMAT: "|$date|$title|"
};
config.macros.showEvents.textHandler = function(params) {
    var options = new Options(params);
    var noEventsFormat = options.get("noEventsFormat", this.NO_EVENTS_FORMAT);
    var headerFormat = options.get("headerFormat", this.HEADER_FORMAT);
    var format = options.get("format", this.FORMAT);
    
    var vars = {}
    var tiddlers = store.reverseLookup("tags", "event", true, "title");
    
    if (tiddlers && tiddlers.length) {
        var text = expandVars(headerFormat, vars) + "\n";
    
        for (var i = 0; i < tiddlers.length; i++) {
            var tags = tiddlers[i].getTags();
            var re_date = new RegExp("\\s?@([0-9]+/[0-9]+/[0-9]+)\\s?", "g");
            var mo = re_date.exec(tags);
            if (mo) {
                var date = Date.parseFr(mo[1]);
                var title = tiddlers[i].title;
                var remaining = new Date().diffDays(date);
                var remainingDays = config.macros.event.getRemainingDays(remaining);
        
                var vars = {remaining: remaining, remainingDays: remainingDays, title: title, date: date.formatFr()};
                text += expandVars(format, vars, true) + "\n";
            }
        }
    } else {
        var text = expandVars(noEventsFormat, vars);
    }
    
    return text;
}
config.macros.showEvents.handler = function(place, macroName, params) {
    var text = this.textHandler(params);
    if (text && text != "") wikify(text, place);
}

// La macro calendar
// arguments:
var currentFirstDay;

config.macros.calendar = {};
config.macros.calendar.refresh = function(e) {
    var target = resolveTarget(ClickHandler.getEvent(e));
    var tiddler = findContainingTiddler(target);
    if (tiddler) {
        refreshTiddler(tiddler.id.substr(7));
    } else {
        while(target && target.id != "mainMenu") {
            target = target.parentNode;
        }
        if (target) {
            refreshMenu();
        }
    }
}
config.macros.calendar.handler = function(place, macroName, params) {
    var now = new Date().getMidnight();
    if (currentFirstDay) {
        var firstDay = currentFirstDay;
    } else {
        var firstDay = now.getFirstDay();
    }
    var firstCalDay = firstDay.getMonday();

    var table, tr, td;
    
    buildModifiedTiddlers();

    table = createTiddlyElement(place, "table");
    table.setAttribute("class", "calendar");
    
    var onClickPreviousMonth = function(e) {
        currentFirstDay = currentFirstDay.addMonths(-1);
        config.macros.calendar.refresh(e);
        return false;
    }

    var onClickNextMonth = function(e) {
        currentFirstDay = currentFirstDay.addMonths(1);
        config.macros.calendar.refresh(e);
        return false;
    }

    tr = createTiddlyElement(table, "tr");
    tr.setAttribute("class", "navRow");

    td = createTiddlyElement(tr, "td");
    createTiddlyButton(td, "<", "mois précédent", onClickPreviousMonth);
    
    td = createTiddlyElement(tr, "td");
    td.colSpan = 5;
    createTiddlyText(td, MMMM[firstDay.getMonth()] + " " + firstDay.getFullYear());
    
    td = createTiddlyElement(tr, "td");
    createTiddlyButton(td, ">", "mois suivant", onClickNextMonth);

    tr = createTiddlyElement(table, "tr");
    tr.setAttribute("class", "hdrRow");
    
    for (var i = 0; i < D.length; i++) {
        td = createTiddlyElement(tr, "td");
        createTiddlyText(td, D[i]);
    }

    createCal(table, now, firstCalDay, firstDay);
    currentFirstDay = firstDay;
}

function createCal(table, now, firstCalDay, firstDay) {
    var td, tr;
    
    var lastDay = firstDay.getLastDay();
    var day = firstCalDay;
    
    do {
        tr = createTiddlyElement(table, "tr");
        tr.setAttribute("class", "calRow");
        for (var i = 0; i < 7; i++) {
            td = createTiddlyElement(tr, "td");
            createCalDay(td, day, firstDay, now, lastDay);
            day = day.addDays(1);
        }
    } while (day <= lastDay);
}

function createCalDay(td, day, firstDay, now, lastDay) {
    var modified = getModifiedTiddlers(day);
    var events = getEvents(day);
    var title = day.formatFr();

    var klass = new String();
    if (day.getTime() == now.getTime()) klass = klass.addText("calToday");
    if (day < firstDay || day > lastDay) klass = klass.addText("calOther");
    if ((modified && modified.length) ||(events && events.length)) {
        klass = klass.addText("calEvent");
        var a = [];
        if (modified && modified.length) a.push("" + modified.length + " tiddler(s)");
        if (events && events.length) a.push("" + events.length + " événements(s)");
        title += ": " + a.join(",");
    }
    td.setAttribute("class", klass);

    td.title = title;
    td.onclick = function(e) {
        e = ClickHandler.getEvent(e);
        var popup = Popup.create(resolveTarget(e));
        
        if (!readOnly) {
            var tags = "event @" + day.formatFr();
            wikify("<<newTiddler text=\"nouvel événement\" tooltip=\"nouvel événement le " + day.formatFr() + "\" tags=\"" + tags + "\">>", popup);
        }
        
        var addLinks = function(tiddlers, title, field) {
            if (tiddlers && tiddlers.length) {
                if (field) tiddlers.sortBy(field);
                var div = createTiddlyElement(popup, "div", null, null, title);
                for (var i = 0; i < tiddlers.length; i++) {
                    var div = createTiddlyElement(popup, "div");
                    var link = createTiddlyLink(div, tiddlers[i].title, false);
                    insertSpacer(link);
                    insertSpacer(link);
                    createTiddlyText(link, tiddlers[i].title);
                }
            }
        }
        
        addLinks(events, "événements:");
        addLinks(modified, "modifiés:", "title");
        
        Popup.show();
        return ClickHandler.end(e);
    }
    
    createTiddlyText(td, day.getDate());
}

var modifiedTiddlers;

function buildModifiedTiddlers() {
    modifiedTiddlers = {};
    for (var t in store.tiddlers) {
        var tiddler = store.tiddlers[t];
        var date = tiddler.modified.formatFr();
        if (!modifiedTiddlers[date]) {
            modifiedTiddlers[date] = [];
        }
        modifiedTiddlers[date].push(tiddler);
    }
}

function getModifiedTiddlers(date) {
    return modifiedTiddlers[date.formatFr()];
}

function getEvents(date) {
    return store.reverseLookup("tags", "@" + date.formatFr(), true, "modified");
}

setStylesheet("\
/** Le calendrier se divise en trois parties: nav (pour la navigation),\
hdr (pour les jours de la semaine), et cal (pour les jours du calendrier proprement dit)\
\
Chaque style est préfixé de nav, hdr ou cal.\
*/\
.calendar {\
    margin: 0em;\
    text-align: center;\
    font-size: 9pt;\
}\
\
/** la barre de navigation */\
.navRow {\
}\
\
/** la ligne d'en-tête, qui contient les jours */\
.hdrRow {\
}\
\
/** une ligne du calendrier */\
.calRow {\
}\
\
.calToday { /* cellule de calendrier: date du jour */\
    color: Blue;\
    border: 1px solid black;\
}\
\
.calOther { /* cellule de calendrier: jour d'un autre mois */\
    color: LightSteelBlue;\
    font-size: 8pt;\
}\
\
.calEvent { /* cellule de calendrier: événement disponible */\
    font-weight: bold;\
    text-decoration: underline;\
}\
", "calendar");

//////////////////////////////////////////////////
// Gestion de l'autorefresh
function refreshAllTiddlersTaggedAutoRefresh(hint) {
  // rafraichir tous les tiddlers ayant le tag "autoRefresh"
  var tagged = store.getTaggedTiddlers("autoRefresh");
  for (var i = 0; i < tagged.length; i++) {
    refreshTiddler(tagged[i].title);
  }
}
config.notifyTiddlers.push({name: null, notify: refreshAllTiddlersTaggedAutoRefresh});

//////////////////////////////////////////////////
// Le bouton keepThis

function keepTiddler(titleToKeep) {
    var display = document.getElementById("tiddlerDisplay");
    var tiddler = display.firstChild;
    while(tiddler) {
        var nextTiddler = tiddler.nextSibling;
        if(tiddler.id) {
            if(tiddler.id.substr(0,7) == "tiddler") {
              var title = tiddler.id.substr(7);
              if(titleToKeep != title && !document.getElementById("editorWrapper" + title))
                  display.removeChild(tiddler);
              }
        }
        tiddler = nextTiddler;
    }
}

var toolbarKeep = {text: "keep this", tooltip: "Close all other tiddlers"};
toolbarKeep.onClick = function(e) {
    e = ClickHandler.getEvent(e);

    if(this.parentNode.id) {
        keepTiddler(this.parentNode.id.substr(7));
    }
    
    return ClickHandler.end(e);
}

//////////////////////////////////////////////////
// Les macros closeCurrent, editCurrent, saveCurrent

config.macros.closeCurrent = {label: "close", prompt: "Close current tiddler", accessKey: "W"};
config.macros.closeCurrent.handler = function(place, macroName, params) {
  createTiddlyButton(place, this.label, this.prompt, this.onClick, null, null, this.accessKey);
}
config.macros.closeCurrent.onClick = function(e) {
    e = ClickHandler.getEvent(e);

    if (currentTiddler) {
      // ne pas fermer un tiddler s'il est en cours d'édition
      if(!document.getElementById("editorWrapper" + currentTiddler)) {
        clearMessage();
        closeTiddler(currentTiddler, e.shiftKey || e.altKey);
        currentTiddler = null;
      }
    }

    return ClickHandler.end(e);
}

config.macros.editCurrent = {label: "edit", prompt: "Edit current tiddler", accessKey: "E"};
config.macros.editCurrent.handler = function(place, macroName, params) {
    if (!readOnly) {
        createTiddlyButton(place, this.label, this.prompt, this.onClick, null, null, this.accessKey);
    }
}
config.macros.editCurrent.onClick = function(e) {
    e = ClickHandler.getEvent(e);

    if (currentTiddler) {
        clearMessage();
        displayTiddler(null, currentTiddler, 2, null, null, false, false);
    }

    return ClickHandler.end(e);
}

config.macros.saveCurrent = {label: "save", prompt: "Save current tiddler", accessKey: "S"};
config.macros.saveCurrent.handler = function(place, macroName, params) {
    if (!readOnly) {
        createTiddlyButton(place, this.label, this.prompt, this.onClick, null, null, this.accessKey);
    }
}
config.macros.saveCurrent.onClick = function(e) {
    e = ClickHandler.getEvent(e);

    if (currentTiddler) {
        clearMessage();
        saveTiddler(currentTiddler, e.shiftKey);
    }

    return ClickHandler.end(e);
}

// patcher les méthodes existantes pour supporter la notion de "current tiddler"
var currentTiddler = null;

var selectTiddler_patchedByBase = window.selectTiddler;
window.selectTiddler = function(title) {
    // un tiddler sélectionné avec la souris devient le tiddler courant
    selectTiddler_patchedByBase(title);
    currentTiddler = title;
}

var displayTiddler_patchedByBase = window.displayTiddler;
window.displayTiddler = function(src, title, state, highlightText, highlightCaseSensitive, animate, slowly) {
    displayTiddler_patchedByBase(src, title, state, highlightText, highlightCaseSensitive, animate, slowly);
    // un tiddler que l'on affiche devient le tiddler courant
    currentTiddler = title;
}

var deleteTiddler_patchedByBase = window.deleteTiddler;
window.deleteTiddler = function(title) {
    deleteTiddler_patchedByBase(title);
    if (title == currentTiddler) {
        // si on supprimer le tiddler courant, il n'y a plus de tiddler courant
        currentTiddler = null;
    }
}