/*
////////////////////////////////////////////////////////////////////////
// WikiEdit                                                           //
// v. 3.03                                                            //
// supported: MZ1.4+, MSIE5+, Opera 8+                                //
//                                                                    //
// (c) Roman "Kukutz" Ivanov <thingol@mail.ru>, 2003-2005             //
//   based on AutoIndent for textarea                                 //
//   (c) Roman "Kukutz" Ivanov, Evgeny Nedelko, 2003                  //
// Many thanks to Alexander Babaev, Sergey Kruglov, Evgeny Nedelko    //
//             and Nikolay Jaremko                                    //
// http://wackowiki.com/WikiEdit                                      //
//                                                                    //
////////////////////////////////////////////////////////////////////////

For license see LICENSE.TXT
*/

function WikiEdit () {
 this.mark = "##inspoint##";
 this.begin = "##startpoint##";
 this.rbegin = new RegExp(this.begin);
 this.end = "##endpoint##";
 this.rend = new RegExp(this.end);
 this.rendb = new RegExp("^" + this.end);
 this.tab = false;
 this.enterpressed = false;
 this.undostack = new Array();
 this.buttons = new Array();
}

WikiEdit.prototype = new ProtoEdit();

// STATIC!
// init all textarea which have given class
WikiEdit.initByClass = function( textarea_class, name, nameClass, imgPath ) {
  var id_count = 0;
  var id_prefix = "wikiedit_auto_id_";
  var ta_class_re = new RegExp( "(\\s|^)"+textarea_class+"(\\s|$)" );
  var tas = document.getElementsByTagName( "textarea" );
  for( var i in tas ) {
    if (ta_class_re.test(tas[i].className)) {
      var ta = tas[i];
      if (ta.id == "") 
        ta.id = id_prefix + (id_count++);
      var wE = new WikiEdit();
      wE.init( ta.id, name, nameClass, imgPath );
    }
  }
}

// initialisation
WikiEdit.prototype.init = function(id, name, nameClass, imgPath) {
    // Проверим, можем ли мыработать в этом браузере
    if (!(isMZ || isIE || isO8))
        return;

    this.mzBugFixed=true;
    if (isMZ && navigator.userAgent.substr(navigator.userAgent.indexOf("Gecko/")+6,4)=="2003" ) {
        this.mzBugFixed=(navigator.userAgent.substr(navigator.userAgent.indexOf("Gecko/")+6,8)>20030510);
        mzOld=(navigator.userAgent.substr(navigator.userAgent.indexOf("Gecko/")+6,8)<20030110);
        this.MZ = mzOld ? false : true;
    }
    if (isMZ && navigator.userAgent.substr(navigator.userAgent.indexOf("Gecko/")+6,4)=="2002" )
        this.MZ=false;
    if (!(this.MZ || isIE || isO8))
        return;

    this._init(id);

    // инициализируемся на базе переданных параметров: 
    this.imagesPath = (imgPath?imgPath:"images/");  //  - путь к изображениям 
    this.editorName = name;                         //  - Заголовок редактора
    this.editorNameClass = nameClass;               //  - класс, которым он будет показываться

    this.actionName = "document.getElementById('" + this.id + "')._owner.insTag";  // !! 

    if (isMZ || isO8) {
        try {
            this.undotext = this.area.value;
            this.undosels = this.area.selectionStart;
            this.undosele = this.area.selectionEnd;
        } catch(e){};
    }
    if (isIE)
        this.area.addBehavior(this.imagesPath+"sel.htc");


// this.addButton("h1","h1","'==','==',0,1");
    this.addButton("h2","Heading 2","'===','===',0,1");
    this.addButton("h3","Heading 3","'====','====',0,1");
    this.addButton(" ");
    this.addButton("bold","Bold","'**','**'");
    this.addButton("italic","Italic","'//','//'");
    this.addButton("underline","Underline","'__','__'");
    this.addButton("strike","Strikethrough","'--','--'");
    this.addButton(" ");
    this.addButton("ul","List","'  * ','',0,1,1");
    this.addButton("ol","Numbered list","'  1. ','',0,1,1");
    this.addButton(" ");
    this.addButton("outdent","Outdent","","document.getElementById('" + this.id + "')._owner.unindent");
    this.addButton("indent","Indent","'  ','',0,1");
    this.addButton(" ");
// this.addButton("quote","quote","'\\n<[',']>\\n',2");
    this.addButton("hr","Line","'','\\n-----------\\n',2");
    this.addButton("textred","Marked text","'!!','!!',2");
    this.addButton("createlink","Hyperlink","","document.getElementById('" + this.id + "')._owner.createLink");
    this.addButton("createtable","Insert Table","'','\\n#|\\n|| | ||\\n|| | ||\\n|#\\n',2");
    this.addButton(" ");
    this.addButton("help","Help & About","","document.getElementById('" + this.id + "')._owner.help");
    this.addButton("customhtml",'<td><div style="font:12px Arial;text-decoration:underline; padding:4px;" id="hilfe_' + this.id + '" onmouseover=\'this.className="btn-hover";\' '
                   + 'onmouseout=\'this.className="btn-";\' class="btn-" '
                   + 'onclick="this.className=\'btn-pressed\';window.open(\'http://wackowiki.com/WackoDocumentation/WackoFormatting\');" '
                   + ' title="Help on Wiki-formatting">Help'
                   + '</div></td>');
    
    // Создаем тулбар (вставляем DIV с предварительно сформированным контентом перед TEXTAREA)
    try {
        var toolbar = document.createElement("div");
        toolbar.id = "tb_"+this.id;
        this.area.parentNode.insertBefore(toolbar, this.area);
        toolbar = document.getElementById("tb_"+this.id);
        toolbar.innerHTML = this.createToolbar(1);
    } catch(e){};
}

// switch TAB key interception on and off
WikiEdit.prototype.switchTab = function() {
    this.tab = !this.tab;
}

// internal functions ----------------------------------------------------
// Добавление тэга к строке слева. (если установлен skip - пропускается разметка для списка)
WikiEdit.prototype._LSum = function (Tag, Text, Skip) {
  if (Skip) {
    q = Text.match(/^(\s*)(\*{2})(.*)$/); 
    if (q!=null) 
      return q[1]+Tag+q[2]+q[3];

    q = Text.match(/^(\s*)(([*]|([[(]([.-=+xX ]|(%((100)|([0-9]{1,2}))%))[\])])|([1-9][0-9]*|[a-zA-Z])([.)]))( |))(.*)$/); 
    if (q!=null) 
      return q[1]+q[2]+Tag+q[13];
  }
  q = Text.match(/^(\s*)(.*)$/);
  return q[1]+Tag+q[2];
}

// добавление тэга к строке справа (завершающие пробелы не включаются в зону тэга)
WikiEdit.prototype._RSum = function (Text, Tag) {
  q = Text.match(/^(.*)(\s*)$/);
  return q[1]+Tag+q[2];
}

WikiEdit.prototype._TSum = function (Text, Tag, Tag2, Skip) {
    var bb = new RegExp("^(\\s*)"+this.begin+"(\\s*)(\\*{2})(.*)$");
    q = Text.match(bb);
    if (q!=null)
        Text = q[1]+this.begin+q[2]+Tag+q[3]+q[4];
    else {
        var w = new RegExp("^(\\s*)"+this.begin+"(\\s*)(([*]|([[(]([.-=+xX ]|(%((100)|([0-9]{1,2}))%))[\\])])|([1-9][0-9]*|[a-zA-Z])([.)]))( |))(.*)$");
        q = Text.match(w);
        if (Skip && q!=null)
            Text = q[1]+this.begin+q[2]+q[3]+Tag+q[14];
        else {
            var w = new RegExp("^(.*)"+this.begin+"(\\s*)(.*)$");
            var q = Text.match(w);
            if (q!=null)
                Text = q[1]+this.begin+q[2]+Tag+q[3];
        }
    }
    var w = new RegExp("(\\s*)"+this.end+"(.*)$");
    var q = Text.match(w);
    if (q!=null) {
        // TODO: поправить regex, выкинуть цикл
        var w = new RegExp("^(.*)"+this.end);
        var q1 = Text.match(w);
        if (q1!=null) {
            var s = q1[1];
            ch = s.substring(s.length-1, s.length);
            while (ch == " ") {
                s = s.substring(0, s.length-1);
                ch = s.substring(s.length-1, s.length);
            }
            Text = s+Tag2+q[1]+this.end+q[2];
        }
    }
    return Text;
}

// Вставка разметки в текст
// Tag: тэг, вставляемый перед заселекченным текстом
// Text: собственно текст (заселекченная область отмечена строками this.begin и this.end)
// Tag2: тэг, втавляемый после заселекченного текста
// onNewLine:
//   0 - add tags on every line inside selection
//   1 - add tags only on the first line of selection
//   2 - add tags before and after selection
//   3 - add tags only if there's one line -- not implemented
// expand:
//   0 - add tags on selection
//   1 - add tags on full line(s)
// strip: удалить разметку списков и т.д. (для конвертации списка в список)
WikiEdit.prototype.MarkUp = function (Tag, Text, Tag2, onNewLine, expand, strip) {
    var skip = expand == 0 ? 1 : 0; // 1, если нужно добавлять тэги к полной строке
    var r = '';         // возвращаемый текст
    var fIn = false;    // флаг того, что началась помеченная зона
    var fOut = false;   // флаг завершения помеченной зоны
    var add = 0;
    //var w = new RegExp("^  ( *)(([*]|([1-9][0-9]*|[a-zA-Z])([.)]))( |))"); // TODO: поправить ошибку с пробелами
    var w = new RegExp("^  ( *)(([*]|([[]([.-=+xX ]|(%((100)|([0-9]{1,2}))%))\])|([(]([.-=+xX ]|(%((100)|([0-9]{1,2}))%))[)])|([1-9][0-9]*|[a-zA-Z])([.)]))( |))"); // TODO: поправить ошибку с пробелами
    if (!isO8)
        Text = Text.replace(new RegExp("\r", "g"), "");
    var lines = Text.split(isO8 ? '\r\n' : '\n'); // TODO:
    for (var i = 0; i < lines.length; i++) {
        if (this.rbegin.test(lines[i])) // если в строке начинается пометка
            fIn = true;
        if (this.rendb.test(lines[i]))  // если это пустая строка, на котрой заканчивается пометка
            fIn = false;
        if (this.rend.test(lines[i]))   // если в этой строке пометказаканчивается
            fOut = true;
        if (this.rendb.test(lines[i+1])) {  // если следюущая строка пустая и в ней заканчивается пометка 
            fOut = true;                    // выставим флаг завершения пометки и переместим маркер в текущую строку
            lines[i+1]=lines[i+1].replace(this.rend, "");
            lines[i]=lines[i]+this.end;
        }
        if (r != '')
            r += '\n';

        // конвертация списка в список: удаляем существующую разметку списка
        if (fIn && strip==1) {  
            if (this.rbegin.test(lines[i])) {
                lines[i] = lines[i].replace(this.rbegin, "");
                lines[i] = lines[i].replace(w, "$1");   
                lines[i] = this.begin+lines[i];
            } else
                lines[i] = lines[i].replace(w, "$1");   
        }

        //добавляем первый таг, если первая либо добавляем последний, если последняя
        //иначе добавляем неизменный текст
        if (fIn && (onNewLine==0 | (onNewLine==1 && add==0) | (onNewLine==2 && (add==0 || fOut)))) {
            //добавляем таги
            if (expand==1) {     // к строчкам 
                l = lines[i];
                if (add==0) // если это первая обрабатываемая строчка
                    l = this._LSum(Tag, l, skip); // (Tag, l, 0)
                if (fOut)   // если это последняя обрабатываемая строчка
                    l = this._RSum(l, Tag2); 
            } else {            // к заселекченной области
                //  не экспанд. это значит, что
                //  если первая строка, то добавляем реплейсом первый и суммой второй
                //  если последняя, то добавляем суммой первый и реплейсом второй
                //  если первая и последняя, то оба реплейсом
                //  иначе суммой
                l = this._TSum(lines[i], Tag, Tag2, skip);
            }
            if (add!=0 && onNewLine!=2) // если это не первая строка и добавлять нужно к каждой строке
                l = this._LSum(Tag, l, skip);
            if (!fOut  && onNewLine!=2) // если добавляется к каждой строке и это не последняя строка 
                l = this._RSum(l, Tag2);
            r += l;
            add++;
        } else 
            r += lines[i]; //добавляем неизменный текст
        if (fOut)
            fIn = false;    // если заселекченная зона закончена - снимаем флаг нахождения в помеченной зоне
    } // foreach line
    return r;
}

// массивы для обрабоки hotkeys
// элементом массива является объект. В нем определяются: 
//   sel - если для обработки требуется помеченный блок 
//   shift - если должен быть нажат шифт
//   tag - обработка сводится к вызову insTag (в этом случае, остальные поля соответствуют параметрам insTag) 
var altKeys = new Array ();
var ctrlKeys = new Array ();
ctrlKeys[49] = { tag: true, shift: false, tag1: "==",     tag2: "==",     expand: 1 };    // 1
ctrlKeys[50] = { tag: true, shift: false, tag1: "===",    tag2: "===",    expand: 1 };    // 2
ctrlKeys[51] = { tag: true, shift: false, tag1: "====",   tag2: "====",   expand: 1 };    // 3
ctrlKeys[52] = { tag: true, shift: false, tag1: "=====",  tag2: "=====",  expand: 1 };    // 4
ctrlKeys[61] = { tag: true, shift: false, sel: true, tag1: "++", tag2: "++" };  // =
ctrlKeys[66] = { tag: true, shift: false, sel: true, tag1: "**", tag2: "**" };  // B
ctrlKeys[72] = { tag: true, shift: false, sel: true, tag1: "??", tag2: "??" };  // H
ctrlKeys[73] = { tag: true, shift: false, sel: true, tag1: "//", tag2: "//" };  // I
ctrlKeys[74] = { tag: true, shift: false, sel: true, tag1: "!!", tag2: "!!" };  // J
ctrlKeys[78] = { tag: true, shift: true,  tag1: "  1. ",  tag2: "",       newLine: 0, expand: 1, strip: 1 }; // N
ctrlKeys[79] = ctrlKeys[78];    // O
ctrlKeys[83] = { tag: true, shift: true, sel: true, tag1: "--", tag2: "--" };  // S
ctrlKeys[85] = { tag: true, shift: false, sel: true, tag1: "__", tag2: "__" };  // U
ctrlKeys[95] = { tag: true, shift: false, tag1: "", tag2: "\n----\n", newLine: 2 }; // _

// Обработчик нажатия на клавишу
// e инициализируется всегда, даже в IE - за счет конструкции, в которой вызывается этот обработчик
WikiEdit.prototype.keyDown = function (e) {
    if (!this.enabled)
        return;

    var l, q, l1, re, tr, str, t, tr2, tr1, r1, re, q, e, ko;
    var justenter = false;
    var wasEvent = remundo = res = false;
    var noscroll = isMZ ? false : true; 

    var t = this.area;

    // Преобразуем значение, полученное из eventа
    var Key = e.keyCode || e.charCode;  
    if (isIE && e.ctrlKey) {
        if (Key == 187) Key = 61;
        if (Key == 189 && e.shiftKey) Key = 95;
    }
    // попробуем получить описатель кнопки 
    if (e.ctrlKey && ctrlKeys[Key]) 
        ko = ctrlKeys[Key];
    else if (!e.ctrlKey && e.altKey && altKeys[Key])
        ko = altKeys[Key];
// else if (e.keyCode != 13 && e.keyCode != 9) 

    if (Key==8 || Key==13 || Key==32 || (Key>45 && Key<91) || (Key>93 && Key<112) || (Key>123 && Key<144) || (Key>145 && Key<255))
        remundo = Key;
    if (e.altKey && !e.ctrlKey)
        Key=Key+4096;
    if (e.ctrlKey)
        Key=Key+2048;

    if (isMZ && e.type == "keypress" && this.checkKey(Key)) {
        e.preventDefault();
        e.stopPropagation();
        return false;
    }
    if (isMZ && e.type == "keyup" && (Key==9 || Key==13))
        return false;

    // Запоминаем предыдущее значение текста для UNDO
    // TODO: сделать, если не unlimited, то хотя бы побольше
    if (isMZ || isO8) {
        var scroll = t.scrollTop;
        undotext = t.value;
        undosels = t.selectionStart;
        undosele = t.selectionEnd;
    }

    // получаем выделенный текст
    if (isIE) {
        tr  = document.selection.createRange(); // BUG? эта конструкция не сворачивается в одну строчку
        str = tr.text;
    } else
        str = t.value.substr(t.selectionStart, t.selectionEnd - t.selectionStart);
    sel = (str.length > 0);

    if (ko) {
        if (ko.tag && (ko.shift == e.shiftKey) && (!ko.sel || (ko.sel && sel))) 
            res = this.insTag (ko.tag1, ko.tag2, ko.newLine, ko.expand, ko.striped);
    } else

    switch (Key) {
/*    case 2138: //Ctrl+Z, Undo
        if ((isMZ || isO8) && this.undotext) { // В IE все произойдет само, в остальных - откатываем сохраненный текст и выделение
            t.value = this.undotext;
            t.setSelectionRange(this.undosels, this.undosele);
            this.undotext = "";
        }
        break;*/
    case 9:    //Tab   - indent/undent
    case 4181: //Alt+U
    case 4169: //Alt+I
//  case 2132: //T -- disabled because conflict with FireFox Ctrl+T shortcut
        if (this.tab || Key!=9)
            res = (e.shiftKey || Key==4181) ? this.unindent() : this.insTag("  ", "", 0, 1);
        break;
    case 4179: // Alt+S - save
    case 2061: // Ctrl+Enter
        try {
            if (weSave) weSave();
        } catch(e){};
        break;
    case 2124: //Ctrl+L  - unordered list (+Shift)
    case 4172: //Alt+L   - link
        res = (e.shiftKey && e.ctrlKey) ? this.insTag("  * ", "", 0, 1, 1) : this.createLink(e.altKey);
        break;
    case 13:
    case 4109:
        if (e.shiftKey) { //Shift+Enter
            res = false;
            break;
        }
        var text = t.value;
        if (!isO8)
            text = text.replace(/\r/g, "");
        var sel1 = text.substr(0, t.selectionStart);
        var sel2 = text.substr(t.selectionEnd);
        //if (isO8) sel1 = sel1.replace(/\r\n$/, "");
        // completion 
        re = new RegExp("(^|\n)(((?:  )+(?=\\S))((([*]|([[]([.-=+xX ]|(%((100)|([0-9]{1,2}))%))\])|([(]([.-=+xX ]|(%((100)|([0-9]{1,2}))%))[)])|([1-9][0-9]*|[a-zA-Z])([.)]))( |))|))("+(this.enterpressed?"\\s":"[^\r\n]")+"*)"+(this.mzBugFixed?"":"\r?\n?")+"$");
        q = sel1.match(re);
        if (q!=null) {
            if (!this.enterpressed) {
                    re = new RegExp("([1-9][0-9]*)([.)])");
                    q2 = q[2].match(re);
                    if (q2)
                        q[2]=q[2].replace(re, String(Number(q2[1])+1)+q2[2]);
                    re = new RegExp ("[[(].*[)\\]]");
                    if (q[2].match (re)) 
                        q[2] = q[3]+"( ) ";
            } else {
                sel1 = sel1.replace(re, "");
                q[2] = "";
            }

            t.value=sel1+(this.mzBugFixed?"\n":"")+q[2]+sel2;
            sel = q[2].length + sel1.length + (this.mzBugFixed?1:0) + (isO8?1:0);
            t.setSelectionRange(sel, sel);

            if (isMZ) {
                if (t.childNodes[0] != null) {
                    t.childNodes[0].nodeValue=t.value;
                    var temp=document.createRange();
                    temp.setStart(t.childNodes[0],sel-2);
                    temp.setEnd(t.childNodes[0],sel);
                }
                //t.scrollIntoView(true);
                z=t.selectionStart;
                lines=t.value.substr(0,z).split('\n').length-1;
                totalLines=t.value.split('\n').length-1;
                if (scroll + t.offsetHeight + 25 > Math.floor((t.scrollHeight/(totalLines+1))*lines)) {
                    t.scrollTop = Math.floor((t.scrollHeight/(totalLines+1))*lines)  - t.offsetHeight + 20;
                    t.focus();
                    noscroll = true;
                }
            } else if (isIE) {
                var op = this.area;
                var tp = 0; 
                var lf = 0;
                do {
                    tp+=op.offsetTop;
                    lf+=op.offsetLeft;
                } while (op=op.offsetParent);
                if (tr.offsetTop >= this.area.clientHeight+tp)
                    tr.scrollIntoView(false);
            }
            res = true;
        } // /completion
        var justenter = true;
        break;
    }   // switch

    this.enterpressed = justenter;
    if (!res && remundo) 
        this.undotext = "";

    if (res) {
        this.area.focus();
        if (isMZ || isO8) {
            this.undotext=undotext;
            this.undosels=undosels;
            this.undosele=undosele;
            if (wasEvent) 
                return true;
            e.cancelBubble = true;
            e.preventDefault();
            e.stopPropagation();
        }
        if (!noscroll) 
            t.scrollTop = scroll;
        e.returnValue = false;
        return false;
    }
}

// Проинициализировать this.sel1, this.sel2, this.sel и this.str; в MZ/FF запомнить значения для undo 
WikiEdit.prototype.getDefines = function () {
  var t = this.area;

  text = t.value;
  if (!isO8) 
    text = text.replace(/\r/g, "");
  this.ss = t.selectionStart;
  this.se = t.selectionEnd;

  this.sel1 = text.substr(0, this.ss);
  this.sel2 = text.substr(this.se);
  this.sel = text.substr(this.ss, this.se - this.ss);
  this.str = this.sel1+this.begin+this.sel+this.end+this.sel2;

  if (isMZ) {
    this.scroll = t.scrollTop;
    this.undotext = t.value;
    this.undosels = t.selectionStart;
    this.undosele = t.selectionEnd;
  }
}

WikiEdit.prototype.setAreaContent = function (str) {
  var t = this.area;
  q = str.match(new RegExp("((.|\n)*)"+this.begin)); //?:
  l = q[1].length;

  if (isO8) 
    l = l + q[1].split('\n').length - 1;
    
  q = str.match(new RegExp(this.begin+"((.|\n)*)"+this.end));
  l1 = q[1].length;

  if (isO8) 
    l1 = l1 + q[1].split('\n').length - 1;  
  
  str = str.replace(this.rbegin, "");
  str = str.replace(this.rend, "");
  t.value = str;
  t.setSelectionRange(l, l + l1);
  if (isMZ) 
    t.scrollTop = this.scroll;
}

// Добавить тэги (описание параметров - см. MarkUp)
WikiEdit.prototype.insTag = function (Tag, Tag2, onNewLine, expand, strip) {
  this.area.focus ();
  this.getDefines ();
  this.setAreaContent ( this.MarkUp (Tag, this.str, Tag2, onNewLine || 0, expand || 0, strip || 0));
  return true;
}

WikiEdit.prototype.unindent = function () {
  var t = this.area;
  t.focus();
  this.getDefines();

  var r = '';
  var fIn = false;
  var rbeginb = new RegExp("^"+this.begin+"(\\s*)");
  var lines = this.str.split(isO8?'\r\n':'\n');
  for(var i = 0; i < lines.length; i++) {
    var line = lines[i];
    if (this.rbegin.test(line)) {
      fIn = true;
      line = line.replace(rbeginb, '$1'+this.begin); //catch first line
    }
    if (this.rendb.test(line))
      fIn = false;
    if (r != '') 
      r += '\n';
    r += fIn ? line.replace(/^(\s{2})|\t/, '') : line;
    if (this.rend.test(line)) 
      fIn = false;
  }
  this.setAreaContent(r);
  return true;
}

WikiEdit.prototype.createLink = function (isAlt) {
  var t = this.area;
  t.focus();
  this.getDefines();

  var n = new RegExp("\n");
  if (!n.test(this.sel)) {
    if (!isAlt) {
      lnk = prompt("Link:", this.sel) || this.sel;
      sl = prompt("Text for linking:", this.sel) || "";
      this.sel = lnk+" "+sl;
    }
    str = this.sel1+"(("+this.trim(this.sel)+"))"+this.sel2;
    t.value = str;
    t.setSelectionRange(this.sel1.length, str.length-this.sel2.length);
    return true;
  }
  return false;
}

WikiEdit.prototype.help = function () {
    s =  "         WikiEdit 3.03 \n";
    s += "  (c) Roman Ivanov, 2003-2005   \n";
    s += "  http://wackowiki.com/WikiEdit \n";
    s += "\n";
    s += "         Shortcuts:\n";
    s += " Ctrl+B - Bold\n";
    s += " Ctrl+I - Italic\n";
    s += " Ctrl+U - Underline\n";
    s += " Ctrl+Shift+S - Strikethrough\n";
    s += " Ctrl+Shift+1 .. 4 - Heading 1..4\n";
    s += " Alt+I  - Indent\n";
    s += " Alt+U  - Unindent\n";
    s += " Ctrl+J - MarkUp (!!)\n";
    s += " Ctrl+H - MarkUp (??)\n";
    s += " Alt+L - Link\n";
    s += " Ctrl+L - Link with description\n";
    s += " Ctrl+Shift+L - Unordered List\n";
    s += " Ctrl+Shift+N - Ordered List\n";
    s += " Ctrl+Shift+O - Ordered List\n";
    s += " Ctrl+= - Small text\n";
    s += " Ctrl+Shift+Minus - Horizontal line\n";
    s += " NB: all Alt-shortcuts do not work in Opera.\n";
    alert(s);
}