Avanturist.org.PATCH

By York Last update 2 hours ago — Installed 3,746 times. Daily Installs: 1, 6, 2, 3, 0, 0, 2, 2, 0, 3, 3, 10, 4, 4, 2, 3, 4, 1, 3, 3, 1, 4, 3, 2, 7, 1, 5, 3, 19, 19, 12, 28

There are 26 previous versions of this script.

the source is over 100KB, syntax highlighting in the browser is too slow

// ==UserScript==
// @name           Avanturist.org.PATCH
// @description    Avanturist.org forum PATCH
// @version        0.11.5
// @include        http://avanturist.org*
// @include        http://www.avanturist.org*
// @include        https://avanturist.org*
// @include        https://www.avanturist.org*
// ==/UserScript==


var scriptVersion = "0.11.5";
var scriptDate = "27.11.2009";
var app;
var wnd = typeof unsafeWindow != "undefined" ? unsafeWindow : window;
var userAgent = navigator.userAgent.toLowerCase();
var isOpera = /opera/.test(userAgent);
var url = location.href;
var clearUrl = url.replace(/#.*/, "");
// XPath до тела сообщения. При этом в качестве начала пути надо использовать document
// TODO Перенести в какой-нибудь класс, когда закончу рефакторинг
var xpathMsgTableBody = "//div[@class='divMain']/div[@class='divContainer']/" +
    "div[@class='content']/div[@class='text']/div[not(@class)]/" +
    "table[@class='table']/tbody[1]/tr/td/table[@class='table']/tbody[1]";
var xpathPostBody = xpathMsgTableBody + "/tr[2]/td[2]/div[@class='post']";
var debug = /^.*#enable_log$/.test(url);
var start;

var log;
if (debug) {
    if (isOpera) {
        log = function(msg) {
            opera.postError("AVANTURIST.ORG: " + msg);
        }
    } else {
        log = function(msg) {
            GM_log("AVANTURIST.ORG: " + msg);
        }
    }
} else {
    log = function(msg) {
    }
}

var logError;
if (isOpera) {
    logError = function(msg) {
        opera.postError("AVANTURIST.ORG ERROR: " + msg);
    }
} else {
    logError = function(msg) {
        GM_log("AVANTURIST.ORG ERROR: " + msg);
    }
}

log("User-Agent: " + userAgent);
log("Is Opera? " + isOpera);
log("url = " + url);


// ****************************************************************************
// ********************************* HTML *************************************
// ****************************************************************************


// TODO: Здесь одна проблема, форма вставляется на страницу, и в одном случае оказывается вложенной.
// По-моему, вложенные формы запрещены, но в данный момент работает, благодаря div
var formHtml =
    "<div id='york_div%idx%' style='visibility: hidden; z-index: 10; position: absolute; background-color: rgb(245, 245, 250); border-style: solid; border-width: 1px; border-color: rgb(60, 97, 164); padding: 3px'>" +
    "  <form id='york_form%idx%' method='get' action='/forum/index.php'>" +
    "    <input type='hidden' id='york_topic%idx%' name='topic' value=''/>" +
    "    <input type='hidden' id='york_start%idx%' name='start' value=''/>" +
    //   Номер страницы:
    "    \u041d\u043e\u043c\u0435\u0440 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b:" +
    "    <input type='text' id='york_page%idx%' value='' maxlength='4' size='4'/>" +
    //   Перейти
    "    <input type='submit' value='\u041f\u0435\u0440\u0435\u0439\u0442\u0438'/>" +
    "  </form>" +
    "</div>";


// ****************************************************************************
// ******************************** HELPERS ***********************************
// ****************************************************************************


function $id(id) {
    return document.getElementById(id);
}


function $x(xpath, context) {
    context = context || document.body;
    return document.evaluate(xpath, context, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
}


function $x1(xpath, context) {
    var snapshot = $x(xpath, context);
    if (snapshot.snapshotLength > 0) {
        return snapshot.snapshotItem(0);
    } else {
        return null;
    }
}


// TODO переделать, надо бы topic_id где-нибудь хранить, а не вычислять каждый раз
function getTopicId() {
    var elements = document.getElementsByName("topic_id");
    if (0 < elements.length) {
        return elements[0].value;
    } else {
        return null;
    }
}


function showHideForm(e) {
    log("showHideForm()");

    e = e || window.event;
    var src = e.srcelement ? e.srcelement : e.target;

    var divId = src.getAttribute("div_id");
    var div = $id(divId);
    if (div.style.visibility == "visible") {
        div.style.visibility = "hidden";
        log("Form " + divId + " was hidden");
    } else {
        div.style.visibility = "visible";
        var inputId = src.getAttribute("input_id");
        if (inputId != null) {
            var input = $id(inputId);
            input.focus();
        }
        log("Form " + divId + " was showed");
    }

    e.returnValue = false;
    if (typeof e.preventDefault != "undefined") {
        e.preventDefault();
    }
    return false;
}


// ****************************************************************************
// ***************************** CTopicWrapper ********************************
// ****************************************************************************


var topicWrapper;


function CTopicWrapper() {
    log("CTopicWrapper()");

    this.boards = new Array();
    // TODO дублирование кода
    var xpathDivMain = "div[@class='divMain']";
    var xpathDivContainer = xpathDivMain + "/div[@class='divContainer']";
    var xpathDivContent = xpathDivContainer + "/div[@class='content']";
    var xpathDivText = xpathDivContent + "/div[@class='text']";
    // TODO собрать работу с xpath в одном месте
    var xpath = xpathDivText + "/div[not(@class)]/table[contains(@class, boardTable)]/tbody/tr[th[@colspan='7']/table[@class='boardTable']]";
    var snapshot = $x(xpath);
    log("Number of found boards: " + snapshot.snapshotLength);
    for (var i = 0; i < snapshot.snapshotLength; i++) {
        var boardTr = snapshot.snapshotItem(i);
        var boardIdx = this.boards.length;
        var board = new CBoard(boardTr, boardIdx);
        this.boards[boardIdx] = board;
    }
};


CTopicWrapper.onClick = function(e) {
    log("CTopicWrapper.onClick()");

    e = e || window.event;
    var src = e.srcelement ? e.srcelement : e.target;

    var boardIdx = src.getAttribute("board_idx");
    board = topicWrapper.boards[boardIdx];
    board.onClick();

    e.returnValue = false;
    if (typeof e.preventDefault != "undefined") {
        e.preventDefault();
    }
    return false;
};


CTopicWrapper.prototype = {
    collapse: function() {
        log("CTopicWrapper.collapse()");

        for (var i = 0; i < this.boards.length; i++) {
            this.boards[i].collapse();
        }
    },

    expand: function() {
        log("CTopicWrapper.expand()");

        for (var i = 0; i < this.boards.length; i++) {
            this.boards[i].expand();
        }
    },

    boards: []
};


// ****************************************************************************
// ******************************** CBoard ************************************
// ****************************************************************************


function CBoard(boardTr, boardIdx) {
    log("CBoard(): boardIdx = " + boardIdx + ", row index = " + boardTr.rowIndex);

    this.board = boardTr;
    this.isCollapsed = false;
    this.href = null;
    this.hiddenTopics = [];

    // TODO переписать
    // TODO добавить настройку
    // Всегда сворачиваю раздел "Модерация"
    var collapseAll = 7 == boardIdx;

    var table = boardTr.parentNode;
    var rows = table.rows;
    var hasHidden = false;
    for (var i = boardTr.rowIndex + 1; i < rows.length; i++) {
        var topicTr = rows[i];
        if (topicTr.cells.length != 7) {
            break;
        }

        var topic = new CTopic(this, topicTr);
        if (topic.isRead || collapseAll) {
            this.hiddenTopics.push(topic);
        }
    }

    var xpath = "th/table[@class='boardTable']/tbody/tr[1]";
    var tr = $x1(xpath, boardTr);
    if (tr) {
        this.href = document.createElement("A");
        this.href.href = "#";
        this.href.innerHTML = this.collapseText;
        this.href.setAttribute("board_idx", boardIdx);
        this.href.addEventListener("click", CTopicWrapper.onClick, false);
        if (this.hiddenTopics.length == 0) {
            this.href.style.visibility = "hidden";
        }

        var th = document.createElement("TH");
        th.width = "20%";
        th.style.textAlign = "center";
        th.appendChild(this.href);
        tr.insertBefore(th, tr.cells[1]);
        tr.cells[0].width = "40%";
        tr.cells[2].width = "40%";
        tr.cells[2].style.textAlign = "right";

        // Выравниваю название раздела влево
        tr.cells[0].align = "left";

        // TODO переписать
        // Если есть список модераторов, то помещаю его в новую строку таблицы
        var small = $x1("th[1]/small", tr);
        if (null != small) {
            var tr2 = document.createElement("TR");
            th = document.createElement("TH");
            th.colSpan = 3;
            th.align = "left";
            th.appendChild(small);
            tr2.appendChild(th);
            var table = tr.parentNode;
            table.appendChild(tr2);
        }
    } else {
        logError("TR wasn't found");
    }
};


CBoard.prototype = {
    collapse: function() {
        log("CBoard.collapse(): row index = " + this.board.rowIndex);

        for (var i = 0; i < this.hiddenTopics.length; i++) {
            this.hiddenTopics[i].collapse();
        }
        if (this.href != null) {
            this.href.innerHTML = this.expandText;
        } else {
            logError("this.href == null. It isn't expected");
        }
        this.isCollapsed = true;
    },

    expand: function() {
        log("CBoard.expand()");

        for (var i = 0; i < this.hiddenTopics.length; i++) {
            this.hiddenTopics[i].expand();
        }
        this.href.innerHTML = this.collapseText;
        this.isCollapsed = false;
    },

    onClick: function() {
        log("CBoard.onClick()");

        if (this.isCollapsed) {
            this.expand();
        } else {
            this.collapse();
        }
    },

    // "СВЕРНУТЬ"
    collapseText: "\u0421\u0412\u0415\u0420\u041d\u0423\u0422\u042c",

    // "РАЗВЕРНУТЬ"
    expandText: "\u0420\u0410\u0417\u0412\u0415\u0420\u041d\u0423\u0422\u042c"
};


// ****************************************************************************
// ******************************** CTopic ************************************
// ****************************************************************************


function CTopic(board, topicTr) {
    log("CTopic(): row index = " + topicTr.rowIndex);

    CTopic.nextId++;
    topicTr.setAttribute("topicId", CTopic.nextId);
    CTopic.topics[CTopic.nextId] = this;

    this.board = board;
    this.topic = topicTr;
    this.isRead = topicTr.cells[2].innerHTML.indexOf("new_post.gif") == -1;

    var xpath = "td[3]/a[1]";
    var topicHref = $x1(xpath, topicTr);
    if (topicHref != null) {
        var regexp = /^.*[\/?&]topic[=,](\d+).*$/;
        var matches = regexp.exec(topicHref.href);
        if (matches != null && matches.length == 2) {
            this.topicId = matches[1];
        } else {
            logError("Topic ID wasn't found. URL: " + topicHref.href + ", matches = " + matches);
        }
    } else {
        logError("Topic HREF wasn't found");
    }
};


CTopic.find = function(topicTr) {
    var topicId = topicTr.getAttribute("topicId");
    if (topicId == null) {
        return null;
    }
    return CTopic.topics[topicId];
}


CTopic.topics = new Array();
CTopic.nextId = 0;


CTopic.prototype = {
    collapse: function() {
        log("CTopic.collapse()");
        this.topic.style.display = "none";
    },

    expand: function() {
        log("CTopic.expand()");
        this.topic.style.display = "";
    }
};


// ****************************************************************************
// ********************************* OTHER ************************************
// ****************************************************************************


function createGotoPageForms(topicId) {
    log("createGotoPageForms(" + topicId + ")");

    var tables = document.body.getElementsByTagName("TABLE");
    var found = 0;
    for (var i = 0; i < tables.length && found < 2; i++) {
        var table = tables[i];
        var innerTables = table.getElementsByTagName("TABLE");
        if (innerTables != null && innerTables.length > 0) {
            continue;
        }

        // "Страниц:"
        var pages = "\u0421\u0442\u0440\u0430\u043d\u0438\u0446:";
        if (table.innerHTML.indexOf(pages) != -1) {
            log("Page navigator #" + found + " is found");
            // Нашли таблицу, включающую список страниц
            // Ищем элемент с текстом "Страниц:"
            var cell = table.rows[0].cells[0];
            // Это нужный элемент!
            var text = cell.childNodes[0];
            // Убеждаемся, что это он
            if (text.nodeType == 3 && text.nodeValue.indexOf(pages) == 0) {
                var href = document.createElement("A");
                href.appendChild(text);
                href.href = "#";
                href.setAttribute("div_id", "york_div" + found);
                href.setAttribute("input_id", "york_page" + found);
                href.addEventListener("click", showHideForm, false);
                href.style.fontWeight = "bold";
                cell.insertBefore(href, cell.childNodes[0]);

                var span = document.createElement("SPAN");
                span.id = "york_span" + found;
                span.innerHTML = formHtml.replace(/%idx%/g, found);
                cell.insertBefore(span, cell.childNodes[1]);

                var form = $id("york_form" + found);
                form.setAttribute("idx", found);
                form.addEventListener("submit", submitGotoPageForm, false);

                var topic = $id("york_topic" + found);
                topic.value = topicId;

                found++;
            }
        }
    }
}


function submitGotoPageForm(e) {
    log("submitGotoPageForm()");

    e = e || window.event;
    var src = e.srcelement ? e.srcelement : e.target;

    var idx = src.getAttribute("idx");
    var inputStart = $id("york_start" + idx);
    var inputPage = $id("york_page" + idx);
    inputStart.value = (inputPage.value - 1) * 20;
}


// Почему-то не работают всякие хитрые jQuery запросы, поэтому просо перебираю элементы
function getArchiveSourceId(href) {
    var childrens = $(href).parent().children();
    for (var i = 0; i < childrens.length; i++) {
        var node = childrens.get(i);
        if (node.name == "source_id") {
            return node.value;
        }
    }
    log("source_id wasn't found! target = " + href);
    return null;
}


function archiveSeen() {
    log("archiveSeen()");

    var source_id = getArchiveSourceId(this);
    if (!source_id || source_id == 'undefined') {
        // "Ошибка подтверждения нового поступления в ваш личный архив!"
        alert("\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f \u0432 \u0432\u0430\u0448 \u043b\u0438\u0447\u043d\u044b\u0439 \u0430\u0440\u0445\u0438\u0432!");
        logError("Can't approve the archive card: source_id is undefined");
        return;
    }

    // отправляем запрос
    $.getJSON("/myarchive/seen/" + source_id, {source_id: source_id}, function(data) {
        if (data.result == 1) {
            $('#ga_myarchive_card_' + source_id).slideUp("slow");
        } else {
            // "Ошибка: "
            alert("\u041e\u0448\u0438\u0431\u043a\u0430: " + data.result_status);
            logError("The archive card with source_id=" + source_id + " wasn't confirmed. " +
                "The server returns an error. data.result_status = " + data.result_status);
        }
    });

    return false;
}


function journalSeen() {
    log("journalSeen()");

    var source_id = getArchiveSourceId(this);
    if (!source_id || source_id == 'undefined') {
        // "Ошибка подтверждения нового поступления в ваш личный журнал!"
        alert("\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f \u0432 \u0432\u0430\u0448 \u043b\u0438\u0447\u043d\u044b\u0439 \u0436\u0443\u0440\u043d\u0430\u043b!");
        logError("Can't approve the journal card: source_id is undefined");
        return;
    }

    // отправляем запрос
    $.getJSON(
        "/myjournal/seen/" + source_id,
        {source_id: source_id},
        function(data) {
            if (data.result == 1) {
                $('#ga_myjournal_card_' + source_id).slideUp("slow");
            } else {
                // "Ошибка: "
                alert("\u041e\u0448\u0438\u0431\u043a\u0430: " + data.result_status);
                logError("The jorunal card with source_id=" + source_id + " wasn't confirmed. " +
                    "The server returns an error. data.result_status = " + data.result_status);
            }
        }
    );

    return false;
}


function archiveDelete() {
    log("archiveDelete()");

    // "Вы действительно хотите удалить данный материал?"
    if (!confirm("\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b?")) {
        return false;
    }

    var source_id = getArchiveSourceId(this);
    if (!source_id || source_id == 'undefined') {
        // "Ошибка удаления нового поступления в ваш личный архив!"
        alert("\u041e\u0448\u0438\u0431\u043a\u0430 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f \u0432 \u0432\u0430\u0448 \u043b\u0438\u0447\u043d\u044b\u0439 \u0430\u0440\u0445\u0438\u0432!");
        logError("Can't delete the archive card: source_id is undefined");
        return;
    }

    // отправляем запрос
    $.getJSON("/myarchive/delete/" + source_id, {source_id: source_id}, function(data) {
        if (data.result == 1) {
            $('#ga_myarchive_card_' + source_id).slideUp("slow");
        } else {
            // "Ошибка: "
            alert("\u041e\u0448\u0438\u0431\u043a\u0430: " + data.result_status);
            logError("The archive card with source_id=" + source_id + " wasn't deleted. " +
                "The server returns an error. data.result_status = " + data.result_status);
        }
    });

    return false;
}


function journalDelete() {
    log("journalDelete()");

    // "Вы действительно хотите удалить данный материал?"
    if (!confirm("\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b?")) {
        return false;
    }

    var source_id = getArchiveSourceId(this);
    if (!source_id || source_id == 'undefined') {
        // "Ошибка удаления нового поступления в ваш личный журнал!"
        alert("\u041e\u0448\u0438\u0431\u043a\u0430 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f \u0432 \u0432\u0430\u0448 \u043b\u0438\u0447\u043d\u044b\u0439 \u0436\u0443\u0440\u043d\u0430\u043b!");
        logError("Can't delete the archive card: source_id is undefined");
        return;
    }

    // отправляем запрос
    $.getJSON(
        "/myjournal/delete/" + source_id,
        {source_id: source_id},
        function(data) {
            if (data.result == 1) {
                $('#ga_myjournal_card_' + source_id).slideUp("slow");
            } else {
                // "Ошибка: "
                alert("\u041e\u0448\u0438\u0431\u043a\u0430: " + data.result_status);
                logError("The journal card with source_id=" + source_id + " wasn't deleted. " +
                    "The server returns an error. data.result_status = " + data.result_status);
            }
        }
    );

    return false;
}


/*
 * Если ссылка длинная, то обрезает её, чтобы она не портила форматирование
 */
function cutLongLink(link, redirectUrl) {
    log("cutLongLink()");

    var href = link.href;
    var html = link.innerHTML;
    if (href.indexOf(redirectUrl) == 0) {
        href = href.substr(redirectUrl.length);
    }
    if (href.indexOf("http://") == 0 && html.indexOf("http://") == 0 && html.length > 80) {
        var found = href == html;
        if (!found) {
            var htmlAmp = html.replace(/&amp;/g, "&");
            found = href == htmlAmp;
            if (!found) {
                var decodedHref;
                try {
                    decodedHref = decodeURI(href);
                } catch (err) {
                    decodedHref = href;
                    logError("Can't decode href: " + href);
                }
                var decodedHtml;
                try {
                    decodedHtml = decodeURI(htmlAmp);
                } catch (err) {
                    decodedHtml = htmlAmp;
                    logError("Can't decode htmlAmp: " + htmlAmp);
                }
                found = decodedHref == decodedHtml;
            } else {
                log("href == htmlAmp");
            }
        } else {
            log("href == html");
        }

        if (found) {
            log("Found long link: " + href);
            link.innerHTML = html.substr(0, 77) + "...";
            link.style.fontStyle = "italic";
        }
    }
}


function processMessageLinks() {
    log("processMessageLinks()");

    var redirectUrl = "http://www.avanturist.org/redirect.php?url=";
    var xpath = xpathPostBody + "//a";
    var snapshot = $x(xpath);
    log("processMessageLinks(): number of found links: " + snapshot.snapshotLength);
    for (var i = 0; i < snapshot.snapshotLength; i++) {
        var link = snapshot.snapshotItem(i);

        cutLongLink(link, redirectUrl);
        if (link.href.indexOf(redirectUrl) == 0) {
            // Во всех ссылках надо заменить & на его URL encode представление
            link.href = link.href.replace(/&/g, "%26");
        }
    }
}


// ****************************************************************************
// ************************** Обработчики страниц *****************************
// ****************************************************************************


function handleArchivePage() {
    log("handleArchivePage()");

    var seenHrefs = $(".ga_myarchive_seen");
    if (seenHrefs != null && seenHrefs.length > 0) {
        seenHrefs.unbind("click");
        seenHrefs.attr("class", "");
        seenHrefs.click(archiveSeen);
    } else {
        // "\"Подтвердить\" links weren't found!"
        log("\"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\" links weren't found!");
    }

    var deleteHrefs = $(".ga_myarchive_delete");
    if (deleteHrefs != null && deleteHrefs.length > 0) {
        deleteHrefs.unbind("click");
        deleteHrefs.attr("class", "");
        deleteHrefs.click(archiveDelete);
    } else {
        // "\"Удалить\" links weren't found!"
        log("\"\u0423\u0434\u0430\u043b\u0438\u0442\u044c\" links weren't found!");
    }
}


function handleJournalPage() {
    log("handleJournalPage()");

    // TODO: дублирование кода с handleArchivePage(), и не в рамках MVC
    var seenHrefs = $(".ga_myjournal_seen");
    if (seenHrefs != null && seenHrefs.length > 0) {
        seenHrefs.unbind("click");
        seenHrefs.attr("class", "");
        seenHrefs.click(journalSeen);
    } else {
        // "\"Подтвердить\" links weren't found!"
        log("\"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\" links weren't found!");
    }

    var deleteHrefs = $(".ga_myjournal_delete");
    if (deleteHrefs != null && deleteHrefs.length > 0) {
        deleteHrefs.unbind("click");
        deleteHrefs.attr("class", "");
        deleteHrefs.click(journalDelete);
    } else {
        // "\"Удалить\" links weren't found!"
        log("\"\u0423\u0434\u0430\u043b\u0438\u0442\u044c\" links weren't found!");
    }
}


function handleTopicPage() {
    log("handleTopicPage()");

    var topicId = getTopicId();
    log("topic_id = " + topicId);
    if (topicId != null) {
        createGotoPageForms(topicId);
    }

    processMessageLinks();
}


function handleBoardPage() {
    log("handleBoardPage()");

}


function handleForumMainPage() {
    log("handleForumMainPage()");

    // TODO workaround
    if (app.controllers[0].settings.sett_collapseTopics) {
        topicWrapper = new CTopicWrapper();
        topicWrapper.collapse();
    } else {
        log("Don't collapse the read topics");
    }
}


function handleOtherForumPages() {
    log("handleOtherForumPages()");

    // TODO: IMPLEMENT
}


function handleOtherSitePages() {
    log("handleOtherSitePages()");

    // TODO: IMPLEMENT
}


// ****************************************************************************
// ****************************** Точка входа *********************************
// ****************************************************************************


function onLoad(e) {
    log("onLoad()");

    start = new Date();
    log("START: " + start);

    if (!document.body) {
        logError("document.body hasn't been loaded");
        return;
    }

    if (typeof $ == "undefined") {
        $ = wnd.jQuery;
    }

    main();
}


function main(e) {
    var jQueryInstalled = true;
    if (typeof $ == "undefined") {
        logError("jQuery hasn't installed. URL: " + url);
        $ = function() {};
        jQueryInstalled = false;
    }


    // ************************************************************************
    // **************************** Settings Model ****************************
    // ************************************************************************

    // Вид и контроллер находятся ниже, т.к. может оказаться, что нет необходимости
    // создавать

    function CSettingsModel() {
        log("CSettingsModel()");

        this.isVisible = false;

        this.sett_collapseTopics = false;
        this.label_sett_collapseTopics =
            // Сворачивать
            "\u0421\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0442\u044c " +
            // прочитанные
            "\u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0435 " +
            // темы после загрузки
            "\u0442\u0435\u043c\u044b \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 " +
            // главной страницы
            "\u0433\u043b\u0430\u0432\u043d\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b";

        this.sett_searchPosts = false;
        this.label_sett_searchPosts =
            // Автоматически
            "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 " +
            // искать
            "\u0438\u0441\u043a\u0430\u0442\u044c " +
            // сообщения
            "\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f";

        this.sett_confirmSearchPosts = false;
        this.label_sett_confirmSearchPosts =
            // Выдавать запрос
            "\u0412\u044b\u0434\u0430\u0432\u0430\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441 " +
            // перед началом
            "\u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0447\u0430\u043b\u043e\u043c " +
            // поиска сообщения
            "\u043f\u043e\u0438\u0441\u043a\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f";

        this.sett_borderTables = false;
        this.label_sett_borderTables =
            // Рисовать
            "\u0420\u0438\u0441\u043e\u0432\u0430\u0442\u044c " +
            // границы у
            "\u0433\u0440\u0430\u043d\u0438\u0446\u044b \u0443 " +
            // таблиц в
            "\u0442\u0430\u0431\u043b\u0438\u0446 \u0432 " +
            // сообщениях
            "\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u0445";

        this.sett_addImageLinks = false;
        this.label_sett_addImageLinks =
            // Добавлять
            "\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c " +
            // ссылки к
            "\u0441\u0441\u044b\u043b\u043a\u0438 \u043a " +
            // уменьшенным
            "\u0443\u043c\u0435\u043d\u044c\u0448\u0435\u043d\u043d\u044b\u043c " +
            // изображениям
            "\u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u043c";

        this.userIgnoreType = CSettingsModel.BLACK_LIST;
        this.userIgnoreListBlack = new Array();
        this.userIgnoreListWhite = new Array();

        this.load();
    }


    CSettingsModel.BLACK_LIST = 0;
    CSettingsModel.WHITE_LIST = 1;


    CSettingsModel.prototype = {
        store: function(name, value) {
            log("CSettingsModel.store()");

            if (isOpera) {
                var yearInSec = 365 * 24 * 60 * 60;
                var now = (new Date()).getTime();
                var fiveYearsLater = new Date(now + 5 * 1000 * yearInSec);
                document.cookie = escape(name) + "=" + escape(value) +
                    ";expires=" + fiveYearsLater.toGMTString() +
                    ";path=/";
            } else {
                GM_setValue(name, value);
            }
        },

        restore: function(name) {
            log("CSettingsModel.restore()");

            if (isOpera) {
                var value = null;
                var cookies = document.cookie.split("; ");
                for (var i = 0; i < cookies.length; i++) {
                        var cookie = cookies[i].split("=");
                        if (cookie[0] == escape(name)) {
                        try {
                            value = unescape(cookie[1]);
                        } catch(e) {
                            logError("Can't unescape the value of the cookie: '" + cookie[0] + "=" + cookie[1] + "'");
                        }
                    break;
                }
            }
                return value;
            } else {
                return GM_getValue(name);
            }
        },

        save: function() {
            log("CSettingsModel.save()");

            var value = "";
            for (var key in this) {
                if (key.indexOf("sett_") == 0) {
                    if (value.length > 0) {
                        value += ",";
                    }
                    value += key + '=' + this[key];
                }
            }

            if (value.length > 0) {
                value += ",";
            }
            value += "userIgnoreType=";
            value += this.userIgnoreType;

            var listBlack = this.userIgnoreListToString(this.userIgnoreListBlack);
            value += ",userIgnoreListBlack=";
            value += listBlack;

            var listWhite = this.userIgnoreListToString(this.userIgnoreListWhite);
            value += ",userIgnoreListWhite=";
            value += listWhite;

            this.store("york_settings", value);
        },

        load: function() {
            log("CSettingsModel.load()");

            var value = this.restore("york_settings");
            if (value == null) {
                logError("Stored settings weren't found");
                return;
            }

            var values = value.split(",");
            for (var i = 0; i < values.length; i++) {
                var setting = values[i].split("=");
                if (setting[0].indexOf("sett_") == 0) {
                    this[setting[0]] = eval(setting[1]);
                } else if (setting[0] == "userIgnoreType") {
                    this.userIgnoreType = parseInt(setting[1]);
                } else if (setting[0] == "userIgnoreList") {
                    // Обрабатываю эту настройку для совместимостью с версиями до 0.08.x включительно
                    if (this.userIgnoreType == CSettingsModel.BLACK_LIST) {
                        this.userIgnoreListFromString(this.userIgnoreListBlack, setting[1]);
                    } else {
                        this.userIgnoreListFromString(this.userIgnoreListWhite, setting[1]);
                    }
                } else if (setting[0] == "userIgnoreListBlack") {
                        this.userIgnoreListFromString(this.userIgnoreListBlack, setting[1]);
                } else if (setting[0] == "userIgnoreListWhite") {
                        this.userIgnoreListFromString(this.userIgnoreListWhite, setting[1]);
                } else {
                    logError("CSettingsModel.load(): Unknown setting: " + values[i]);
                }
            }
        },

        userIgnoreListFromString: function(list, str) {
            var idNamePairs = str.split("|");
            var len = idNamePairs.length;
            for (var i = 0; i < len; i++) {
                if (idNamePairs[i] != null && idNamePairs[i].length > 0) {
                    var pair = idNamePairs[i].split(":");
                    var id = NaN;
                    try {
                        id = parseInt(pair[0]);
                    } catch (e) {
                        id = NaN;
                    }
                    if (!isNaN(id)) {
                        list[pair[0]] = pair[1] == null ? "" : pair[1];
                    }
                }
            }
        },

        userIgnoreListToString: function(list) {
            var users = "";
            if (list.length > 0) {
                var regexp = /[,:|]/g;
                for (var id in list) {
                    if (users.length > 0) {
                        users += "|";
                    }
                    var name = list[id];
                    users += id;
                    users += ":";
                    if (!isOpera) {
                        users += name.replace(regexp, "_");
                    }
                }
            }
            return users;
        }
    };

    // Конец CSettingsModel
    // ************************************************************************


    var settingsModel = new CSettingsModel();

    if (settingsModel.sett_searchPosts) {
        // В целях оптимизации объявляю класс CPostSearcher только если задана
        // соответсвующая настройка. После объявления класса идёт использующий
        // его код

        // ********************************************************************
        // ************************** CPostSearcher ***************************
        // ********************************************************************

        function CPostSearcher(msdId) {
            log("CPostSearcher(" + msgId + ")");

            this.msgId = msgId;

            this.topicId = getTopicId();
            if (null == this.topicId) {
                throw "CPostSearcher(): topic_id wasn't found";
            }

            // TODO дублирование кода
            // Нахожу навигатор по страницам и определяю текущую страницу (находится в теге <b>)
            // Ищем тэг B, после которого идёт текст, содержащий "]"
            var xpath = "//body/div[@class='divMain']/div[@class='divContainer']/" +
                "div[@class='content']/div[@class='text']/div[not(@class)]/" +
                "table[not(@id)]/tbody[1]/tr[1]/td[@class='middletext']/" +
                "b[following-sibling::node()[position()=1 and contains(self::text(), ']')]]";
            // TODO $id("content")
            var b = $x1(xpath, $id("content"));
            if (b == null) {
                throw "CPostSearcher(): Navigator wasn't found";
            }
            this.currentPage = parseInt(b.innerHTML);
            log("currentPage = " + this.currentPage);

            // TODO дублирование кода
            // Определяю номер последней страницы темы (последняя ссылка в навигаторе)
            xpath = "//body/div[@class='divMain']/div[@class='divContainer']/" +
                "div[@class='content']/div[@class='text']/div[not(@class)]/" +
                "table[not(@id)]/tbody[1]/tr[1]/td[@class='middletext']/" +
                "a[@class='navPages' and position()=last()]";
//            xpath = "tbody[1]/tr[1]/td[1]/table[not(@id)]/tbody[1]/tr[1]/td[@class='middletext']/a[@class='navPages' and position()=last()]";
            // TODO $id("content")
//            var href = $x1(xpath, $id("content"));
            var href = $x1(xpath);
            this.lastPage = href != null ? parseInt(href.innerHTML) : 1;
            this.lastPage = Math.max(this.lastPage, this.currentPage);
            log("lastPage = " + this.lastPage);

            var params = CPostSearcher.parseUrlParameters(url);
            this.searchFrom = parseInt(params["search_from"]);
            this.searchTo = parseInt(params["search_to"]);
            this.searchMid = parseInt(params["search_mid"]);

            // Ниже mode может быть изменён!
            this.mode = 1;
            if (!this.initPageMessageIds()) {
                throw "CPostSearcher(): Can't initialize page message ids";
            }

            if (this.firstPageMsgId > this.lastPageMsgId) {
                // У пользователя сообщения в обратном порядке! Поэтому все идентификаторы
                // сообщений буду брать со знаком минус, в этом случае алгоритм почти
                // не надо менять!
                this.mode = -1;
                this.msgId *= -1;
                this.firstPageMsgId *= -1;
                this.lastPageMsgId *= -1;
            }
        };

        CPostSearcher.prototype = {
            searchMessage: function() {
                log("CPostSearcher.searchMessage()");

                if (isNaN(this.searchFrom) || isNaN(this.searchTo)) {
                    // Этап I. Первая загрузка страницы
                    return this.firstPageLoad();
                } else if (isNaN(this.searchMid)) {
                    // Этап II. Поиск диапазона страниц, содержащих сообщение
                    return this.searchPageInterval();
                } else {
                    // Этап III. Поиск сообщения где-то между двумя страницами
                    return this.binaryPageSearch();
                }
            },

            // Этап I. Первая загрузка страницы
            // Проверяем, есть ли искомое сообщение на странице, и если нет,
            // то определяем в какую сторону двигаться чтобы его найти
            firstPageLoad: function() {
                log("CPostSearcher.firstPageLoad()");

                var msg =
                    // "Вы пытаетесь
                    "\u0412\u044b \u043f\u044b\u0442\u0430\u0435\u0442\u0435\u0441\u044c " +
                    // перейти к
                    "\u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043a " +
                    // сообщению № "
                    "\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044e \u2116 " + this.msgId +
                    // .\nЭто сообщение
                    ".\n\u042d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 " +
                    // не найдено на
                    "\u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043d\u0430 " +
                    // текущей
                    "\u0442\u0435\u043a\u0443\u0449\u0435\u0439 " +
                    // странице №
                    "\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u2116 " + this.currentPage +
                    // .\nНачать поиск
                    ".\n\u041d\u0430\u0447\u0430\u0442\u044c \u043f\u043e\u0438\u0441\u043a " +
                    // сообщения?\n
                    "\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f?\n" +
                    // (В процессе
                    "(\u0412 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0435 " +
                    // поиска
                    "\u043f\u043e\u0438\u0441\u043a\u0430 " +
                    // страница будет
                    "\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0431\u0443\u0434\u0435\u0442 " +
                    // неоднократно
                    "\u043d\u0435\u043e\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u043e " +
                    // перегружена.
                    "\u043f\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043d\u0430.)";
                var step = 5;
                this.searchFrom = this.currentPage;
                if (this.msgId < this.firstPageMsgId) {
                    if (this.currentPage == 1) {
                        // Встаём на первое сообщение на странице
                        return this.jumpToMessage(1, this.firstPageMsgId);
                    } else {
                        step = Math.floor(this.currentPage / step);
                        step = Math.max(1, step);
                        this.searchTo = this.searchFrom - step;
                        this.searchTo = Math.max(1, this.searchTo);
                        if (!settingsModel.sett_confirmSearchPosts || confirm(msg)) {
                            return this.jumpToSearchToPage();
                        } else {
                            return false;
                        }
                    }
                } else if (this.lastPageMsgId < this.msgId) {
                    if (this.currentPage == this.lastPage) {
                        // Встаём на последнее сообщение на странице
                        return this.jumpToMessage(this.lastPage, this.lastPageMsgId);
                    } else {
                        step = Math.floor((this.lastPage - this.currentPage) / step);
                        step = Math.max(1, step);
                        this.searchTo = this.searchFrom + step;
                        this.searchTo = Math.min(this.lastPage, this.searchTo);
                        if (!settingsModel.sett_confirmSearchPosts || confirm(msg)) {
                            return this.jumpToSearchToPage();
                        } else {
                            return false;
                        }
                    }
                } else {
                    log("Message " + this.msgId + " is on the current page");
                    // Ищем сообщение на странице, если не находим, то ищем сообщение,
                    // которое было непосредственно перед ним
                    var targetMsgId = this.msgId;
                    for (var i = 0; i < this.pageMessagesSnapshot.snapshotLength; i++) {
                        var id = this.extractMessageId(i);
                        if (id < this.msgId) {
                            targetMsgId = id;
                        } else if (id == this.msgId) {
                            targetMsgId = id;
                            break;
                        } else {
                            break;
                        }
                    }
                    if (targetMsgId != this.msgId) {
                        return this.jumpToMessage(this.currentPage, targetMsgId);
                    }
                }

                return false;
            },

            // Этап II. Поиск диапазона страниц, содержащих сообщение
            // При этом если searchFrom < searchTo, то движемся справа налево, иначе слева направо
            searchPageInterval: function() {
                log("CPostSearcher.searchPageInterval()");

                var step = Math.abs(this.searchTo - this.searchFrom);
                if (this.msgId < this.firstPageMsgId) {
                    if (this.searchFrom < this.searchTo) {
                        // Диапазон найден! Переходим к этапу III
                        if (step == 1) {
                            // Сообщения не существует ни на одной из соседних
                            // страниц. Значит надо загрузить страницу с минимальным
                            // номером и перейти на ней к последнему сообщению
                            // В данном случае надо загрузить страницу searchFrom.
                            // Для этого делаем переход к ней, она при загрузке обнаружит,
                            // что сообщение на ней отсутствует и перейдёт к последнему
                            // сообщению на странице
                            this.swapFromAndTo();
                            return this.jumpToSearchToPage();
                        } else {
                            step = Math.floor(step / 2);
                            this.searchMid = this.searchFrom + step;
                            return this.jumpToSearchMidPage();
                        }
                    } else if (this.searchTo < this.searchFrom) {
                        // Недолёт. Надо дальше двигаться в сторону уменьшения номеров страниц
                        if (this.searchTo == 1) {
                            // Встаём на первое сообщение на странице
                            return this.jumpToMessage(1, this.firstPageMsgId);
                        } else {
                            this.searchFrom = this.searchTo;
                            this.searchTo = this.searchFrom - step;
                            this.searchTo = Math.max(1, this.searchTo);
                            return this.jumpToSearchToPage();
                        }
                    } else {
                        // Такого быть не может
                        logError("CPostSearcher.searchPageInterval(): Unexpected error");
                    }
                } else if (this.lastPageMsgId < this.msgId) {
                    if (this.searchFrom < this.searchTo) {
                        // Недолёт. Надо дальше двигаться в сторону увеличения номеров страниц
                        if (this.searchTo == this.lastPage) {
                            // Встаём на последнее сообщение на странице
                            return this.jumpToMessage(this.lastPage, this.lastPageMsgId);
                        } else {
                            this.searchFrom = this.searchTo;
                            this.searchTo = this.searchFrom + step;
                            this.searchTo = Math.min(this.lastPage, this.searchTo);
                            return this.jumpToSearchToPage();
                        }
                    } else if (this.searchTo < this.searchFrom) {
                        // Диапазон найден! Ищем сообщение в этом диапазоне
                        if (step == 1) {
                            // Сообщения не существует ни на одной из соседних
                            // страниц. Значит надо загрузить страницу с минимальным
                            // номером и перейти на ней к последнему сообщению
                            // В данном случае надо перейти к последнему сообщению на текущей странице
                            return this.jumpToMessage(this.currentPage, this.lastPageMsgId);
                        } else {
                            // Теперь можно сделать так, чтобы searchFrom всегда было меньше searchTo
                            this.swapFromAndTo();
                            step = Math.floor(step / 2);
                            this.searchMid = this.searchFrom + step;
                            return this.jumpToSearchMidPage();
                        }
                    } else {
                        // Такого быть не может
                        logError("CPostSearcher.searchPageInterval(): Unexpected error");
                    }
                } else {
                    log("Message " + this.msgId + " is found");
                    return this.jumpToMessage(this.currentPage, this.msgId);
                }

                return false;
            },

            // Этап III. Поиск сообщения где-то между двумя страницами
            // Всегда выполняется условие: searchFrom < searchMid < searchTo
            binaryPageSearch: function() {
                log("CPostSearcher.binaryPageSearch()");

                if (this.msgId < this.firstPageMsgId) {
                    // Сообщение находится между searchFrom и searchMid
                    this.searchTo = this.searchMid;
                    var step = this.searchTo - this.searchFrom;
                    if (step == 1) {
                        // Сообщения не существует ни на одной из соседних
                        // страниц. Значит надо загрузить страницу с минимальным
                        // номером и перейти на ней к последнему сообщению
                        // В данном случае надо загрузить страницу searchFrom
                        this.swapFromAndTo();
                        return this.jumpToSearchToPage();
                    } else {
                        step = Math.floor(step / 2);
                        this.searchMid = this.searchFrom + step;
                        return this.jumpToSearchMidPage();
                    }
                } else if (this.lastPageMsgId < this.msgId) {
                    // Сообщение находится между searchMid и searchTo
                    this.searchFrom = this.searchMid;
                    var step = this.searchTo - this.searchFrom;
                    if (step == 1) {
                        // Сообщения не существует ни на одной из соседних
                        // страниц. Значит надо загрузить страницу с минимальным
                        // номером и перейти на ней к последнему сообщению
                        // В данном случае надо перейти к последнему сообщению на текущей странице
                        return this.jumpToMessage(this.searchFrom, this.lastPageMsgId);
                    } else {
                        step = Math.floor(step / 2);
                        this.searchMid = this.searchFrom + step;
                        return this.jumpToSearchMidPage();
                    }
                } else {
                    log("Message " + msgId + " is found");
                    return this.jumpToMessage(this.searchMid, this.msgId);
                }

                return false;
            },

            initPageMessageIds: function() {
                log("CPostSearcher.getPageMessageIds()");

                // TODO Можно оптимизировать, где-нибудь сохранив ссылку на таблицу сообщений
//                var snapshot = $x(xpathMsgTableBody);
                var snapshot = $x("/html/body/div[@class='divMain']/div[@class='divContainer']/div[@class='content']/div[@class='text']/div[not(@class)]/table[@class='table']/tbody[1]/tr/td/table[@class='table']/tbody[1]");
                if (snapshot.snapshotLength > 0) {
                    this.pageMessagesSnapshot = snapshot;
                    this.firstPageMsgId = this.extractMessageId(0);
                    this.lastPageMsgId = this.extractMessageId(snapshot.snapshotLength - 1);
                    log("firstPageMsgId = " + this.firstPageMsgId);
                    log("lastPageMsgId = " + this.lastPageMsgId);
                    return !isNaN(this.firstPageMsgId) && !isNaN(this.lastPageMsgId);
                } else {
                    logError("CPostSearcher.initPageMessageIds(). The message table wasn't found");
                    return false;
                }
            },

            extractMessageId: function(idx) {
                var msgTableBody = this.pageMessagesSnapshot.snapshotItem(idx);
                var id = parseInt(msgTableBody.rows[1].cells[1].id.substr(13));
                if (!isNaN(id)) {
                    // Зачем это нужно см. в конструкторе
                    id *= this.mode;
                }
                return id;
            },

            swapFromAndTo: function() {
                var temp = this.searchFrom;
                this.searchFrom = this.searchTo;
                this.searchTo = temp;
            },

            jumpToMessage: function(page, msg) {
                var start = 20 * (page - 1);
                // Про необходимость Math.abs см. в конструкторе
                wnd.location.replace(wnd.location.protocol + "//www.avanturist.org/forum/index.php/" +
                    "topic," + this.topicId +
                    "." + start +
                    ".html#msg" + Math.abs(msg));
            },

            jumpToSearchToPage: function() {
                var start = 20 * (this.searchTo - 1);
                // Про необходимость Math.abs см. в конструкторе
                wnd.location.replace(wnd.location.protocol + "//www.avanturist.org/forum/index.php" +
                    "?topic=" + this.topicId +
                    "&start=" + start +
                    "&search_from=" + this.searchFrom +
                    "&search_to=" + this.searchTo +
                    "#msg" + Math.abs(this.msgId));
                return true;
            },

            jumpToSearchMidPage: function() {
                var start = 20 * (this.searchMid - 1);
                // Про необходимость Math.abs см. в конструкторе
                wnd.location.replace(wnd.location.protocol + "//www.avanturist.org/forum/index.php" +
                    "?topic=" + this.topicId +
                    "&start=" + start +
                    "&search_from=" + this.searchFrom +
                    "&search_to=" + this.searchTo +
                    "&search_mid=" + this.searchMid +
                    "#msg" + Math.abs(this.msgId));
                return true;
            }
        };


        CPostSearcher.parseUrlParameters = function(url) {
            log("CPostSearcher.parseUrlParameters(" + url + ")");

            var res = new Array();
            var idxQuery = url.indexOf("?") + 1;
            if (idxQuery == 0) { // Выше была прибавлена 1, поэтому сравнение не с -1
                log("Question mark wasn't found");
                return res;
            }

            var idxHash = url.indexOf("#");
            idxHash = idxHash == -1 ? url.length() : idxHash;
            if (idxHash <= idxQuery) {
                logError("invalid URL");
                return res;
            }

            var paramStr = url.substr(idxQuery, idxHash - idxQuery);
            log("paramStr = " + paramStr);
            var pairs = paramStr.split("&");
            for (var i = 0; i < pairs.length; i++) {
                var pair = pairs[i].split("=");
                res[pair[0]] = pair[1];
            }

            return res;
        }

        // Конец CPostSearcher
        // ************************************************************************

        // Выполняем поиск нужного сообщения
        var regexp = /^.*#msg(\d+)$/;
        var matches = regexp.exec(url);
        if (matches != null && matches.length == 2) {
            // URL страницы создан для перехода к конкректному сообщению
            var msgId = parseInt(matches[1]);
            log("msgId = " + msgId);

            try {
                var postSearcher = new CPostSearcher(msgId);
                if (postSearcher.searchMessage()) {
                    // Необходимо перегрузить страницу, поэтому дальше выполнять скрипт не надо
                    return;
                }
            } catch (e) {
                logError("onLoad(): error in CPostSearcher initialization. " + e);
            }
        } else {
            log("There is not a search message URL");
        }
    } // if (settingsModel.sett_searchPosts)


    // NOTE: Изменение свойства visibility работает быстрее, чем изменение display!!!
    var DIV_STYLE =
        "visibility: hidden;" +
        "z-index: 2;" +
        "position: absolute;" +
        "background-color: rgb(245, 245, 250);" +
        "border: 1px solid rgb(60, 97, 164);" +
        "padding: 3px;" +
        "color: black;";


    // ****************************************************************************
    // ****************************** User ignore *********************************
    // ****************************************************************************


    function CDisplayedMessageModel(userIgnoreModel, originalMsg, userId, userName) {
        log("CDisplayedMessageModel()");

        this.userIgnoreModel    = userIgnoreModel;
        this.originalMsg        = originalMsg;
        this.userId             = userId;
        this.userName           = userName;
    }


    CDisplayedMessageModel.prototype = {
        removeFromList: function() {
            this.userIgnoreModel.removeFromList(this.userId);
        },


        addToList: function() {
            this.userIgnoreModel.addToList(this.userId, this.userName);
        }
    }


    function CDisplayedMessageController(userIgnoreController, model, view) {
        log("CDisplayedMessageController()");

        app.addController(this);

        this.userIgnoreController = userIgnoreController;
        this.model = model;
        this.view = view;
    }


    CDisplayedMessageController.prototype = {
        handle: function(src, e) {
            log("CDisplayedMessageController.handle(" + src + ", " + e.type + ")");

            var view = this.view;
            var model = this.model;
            if (src == view.listCtrlHref) {
                if (model.userIgnoreModel.isBlackList) {
                    var msg =
                        // Вы действительно
                        "\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e " +
                        // хотите поместить \"
                        "\u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u043e\u043c\u0435\u0441\u0442\u0438\u0442\u044c \"" +
                        this.model.userName +
                        // \" в \"чёрный\"
                        "\" \u0432 \"\u0447\u0451\u0440\u043d\u044b\u0439\" " +
                        // список?
                        "\u0441\u043f\u0438\u0441\u043e\u043a?";
                    if (!confirm(msg)) {
                        return false;
                    }

                    this.model.addToList();
                } else {
                    var msg =
                        // Вы действительно
                        "\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e " +
                        // хотите удалить \"
                        "\u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \"" +
                        this.model.userName +
                        // \" из \"белого\"
                        "\" \u0438\u0437 \"\u0431\u0435\u043b\u043e\u0433\u043e\" " +
                        // списка?
                        "\u0441\u043f\u0438\u0441\u043a\u0430?";
                    if (!confirm(msg)) {
                        return false;
                    }

                    this.model.removeFromList();
                }
                this.userIgnoreController.updateSettings();
                wnd.location.reload();
                return false;
            } else {
                logError("CDisplayedMessageController.handle(): An unexpected event. src.id = " + src.id + ", e.type = " + e.type);
                return true;
            }
        }
    }


    function CDisplayedMessageView(model, userIgnoreController) {
        log("CDisplayedMessageView()");

        this.model = model;
        this.controller = new CDisplayedMessageController(userIgnoreController, model, this);

        xpath = "tbody/tr[1]/td[1]/b";
        this.profileHrefNode = $x1(xpath, this.model.originalMsg);
        if (this.profileHrefNode == null) {
            logError("CDisplayedMessageView(): Profile URL wasn't found");
            return;
        }
        var cellUser = this.profileHrefNode.parentNode;

        this.createUserCellContentNode();;
        cellUser.appendChild(this.userCellContentNode);
    }


    CDisplayedMessageView.prototype = {
        createListCtrlHref: function() {
            log("CDisplayedMessageView.createListCtrlHref()");

            var href = document.createElement("A");
            href.href = "#";
            href.innerHTML = "x";
            href.setAttribute("style", "font-weight: bold; font-size: 16px; color: red;");
            if (this.model.userIgnoreModel.isBlackList) {
                href.title =
                    // Поместить
                    "\u041f\u043e\u043c\u0435\u0441\u0442\u0438\u0442\u044c " +
                    // пользователя в
                    "\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 " +
                    // \"чёрный\" список
                    "\"\u0447\u0451\u0440\u043d\u044b\u0439\" \u0441\u043f\u0438\u0441\u043e\u043a";
            } else {
                href.title =
                    // Удалить
                    "\u0423\u0434\u0430\u043b\u0438\u0442\u044c " +
                    // пользователя из
                    "\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u0437 " +
                    // \"белого\" списка
                    "\"\u0431\u0435\u043b\u043e\u0433\u043e\" \u0441\u043f\u0438\u0441\u043a\u0430";
            }
            app.addEventListener(href, "click", this.controller);

            this.listCtrlHref = href;
        },


        createUserCellContentNode: function() {
            log("C...MessageView.createCollapsedUserCell()");

            this.createListCtrlHref();

            var table = document.createElement("TABLE");
            table.width = "100%";
            table.cellPadding = "0";
            table.cellSpacing = "0";
            var row = table.insertRow(-1);

            var userCell = row.insertCell(-1);
            userCell.width = "95%";
            userCell.appendChild(this.profileHrefNode);

            var hrefCell = row.insertCell(-1);
            hrefCell.width = "5%";
            hrefCell.align = "right";
            hrefCell.appendChild(this.listCtrlHref);

            this.userCellContentNode = table;
        }
    }


    function CHiddenMessageModel(userIgnoreModel, originalMsg, userId, userName) {
        log("CHiddenMessageModel()");

        this.userIgnoreModel    = userIgnoreModel;
        this.originalMsg        = originalMsg;
        this.userId             = userId;
        this.userName           = userName;
        this.isVisible          = true;
    }


    CHiddenMessageModel.prototype = {
        removeFromList: function() {
            this.userIgnoreModel.removeFromList(this.userId);
        },


        addToList: function() {
            this.userIgnoreModel.addToList(this.userId, this.userName);
        }
    }


    function CHiddenMessageController(userIgnoreController, model, view) {
        log("CHiddenMessageController()");

        app.addController(this);

        this.userIgnoreController = userIgnoreController;
        this.model = model;
        this.view = view;
    }


    CHiddenMessageController.prototype = {
        handle: function(src, e) {
            log("CHiddenMessageController.handle(" + src + ", " + e.type + ")");

            if (src == this.view.expandHref) {
                this.view.expand();
                return false;
            } else if (src == this.view.collapseHref) {
                this.view.collapse();
                return false;
            } else if (src == this.view.listCtrlHref) {
                if (this.model.userIgnoreModel.isBlackList) {
                    var msg =
                        // Вы действительно
                        "\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e " +
                        // хотите удалить \"
                        "\u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \"" +
                        this.model.userName +
                        // \" из \"чёрного\"
                        "\" \u0438\u0437 \"\u0447\u0451\u0440\u043d\u043e\u0433\u043e\" " +
                        // списка?
                        "\u0441\u043f\u0438\u0441\u043a\u0430?";
                    if (!confirm(msg)) {
                        return false;
                    }

                    this.model.removeFromList();
                } else {
                    var msg =
                        // Вы действительно
                        "\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e " +
                        // хотите добавить \"
                        "\u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \"" +
                        this.model.userName +
                        // \" в \"белый\"
                        "\" \u0432 \"\u0431\u0435\u043b\u044b\u0439\" " +
                        // список?
                        "\u0441\u043f\u0438\u0441\u043e\u043a?";
                    if (!confirm(msg)) {
                        return false;
                    }

                    this.model.addToList();
                }
                this.userIgnoreController.updateSettings();
                wnd.location.reload();
                return false;
            } else {
                logError("An unexpected event. src.id = " + src.id + ", e.type = " + e.type);
                return true;
            }
        }
    }


    function CHiddenMessageView(model, userIgnoreController) {
        log("CHiddenMessageView()");

        this.model = model;
        this.controller = new CHiddenMessageController(userIgnoreController, model, this);
        this.expanded = model.originalMsg;

        this.parseExpandedView();
        this.createUserCellContentNode();
        this.createCollapsedView();

        this.expanded.parentNode.insertBefore(this.collapsed, this.expanded);
    }


    CHiddenMessageView.prototype = {
        createListCtrlHref: function() {
            log("CHiddenMessageView.createListCtrlHref()");

            var href = document.createElement("A");
            href.href = "#";
            href.innerHTML = "+";
            href.setAttribute("style", "font-weight: bold; font-size: 16px; color: blue;");
            if (this.model.userIgnoreModel.isBlackList) {
                href.title =
                    // Удалить
                    "\u0423\u0434\u0430\u043b\u0438\u0442\u044c " +
                    // пользователя из
                    "\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u0437 " +
                    // \"чёрного\" списка
                    "\"\u0447\u0451\u0440\u043d\u043e\u0433\u043e\" \u0441\u043f\u0438\u0441\u043a\u0430";
            } else {
                href.title =
                    // Добавить
                    "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c " +
                    // пользователя в
                    "\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 " +
                    // \"белый\" список
                    "\"\u0431\u0435\u043b\u044b\u0439\" \u0441\u043f\u0438\u0441\u043e\u043a";
            }
            app.addEventListener(href, "click", this.controller);

            this.listCtrlHref = href;
        },


        createUserCellContentNode: CDisplayedMessageView.prototype.createUserCellContentNode,


        createCollapsedView: function() {
            log("CHiddenMessageView.createCollapsedView()");

            var msgTable = document.createElement("TABLE");
            // Стиль message_hidden_york описан ниже в CPageView.addCustomCSS()
            msgTable.className      = "message_hidden_york";
            msgTable.cellSpacing    = "1";
            msgTable.cellPadding    = "5";
            msgTable.width          = "100%";
            msgTable.style.display  = "none";
            var row = msgTable.insertRow(-1);

            var cellUser = row.insertCell(-1);
            cellUser.width = 200;

            var cellInfo = row.insertCell(-1);

            var cellInfoTable = document.createElement("TABLE");
            cellInfoTable.width  = "100%";
            row = cellInfoTable.insertRow(-1);

            var cellInfoCell1 = row.insertCell(-1);
            cellInfoCell1.width = "40%";

            var cellInfoCell2 = row.insertCell(-1);
            cellInfoCell2.width = "10%";
            cellInfoCell2.align = "center";

            var cellInfoCell3 = row.insertCell(-1);
            cellInfoCell3.width = "40%";
            cellInfoCell3.align = "right";
            cellInfoCell3.id = "XXX";

            var href = document.createElement("A");
            href.href = "#";
            href.style.fontWeight = "bold";
            // "РАЗВЕРНУТЬ"
            href.innerHTML = "\u0420\u0410\u0417\u0412\u0415\u0420\u041d\u0423\u0422\u042c";
            app.addEventListener(href, "click", this.controller);
            cellInfoCell2.appendChild(href);

            cellInfo.appendChild(cellInfoTable);

            this.collapsed = msgTable;
            this.collapsedCellUser   = cellUser;
            this.collapsedCellInfo   = cellInfoCell1;
            this.collapsedCellCtrl   = cellInfoCell2;
            this.collapsedCellRating = cellInfoCell3;
            this.expandHref = href;
        },


        parseExpandedView: function() {
            log("CHiddenMessageView.parseExpandedView()");

            xpath = "tbody/tr[1]/td[1]/b";
            this.profileHrefNode = $x1(xpath, this.expanded);
            if (this.profileHrefNode == null) {
                logError("CHiddenMessageView.parseExpandedView(): Profile URL wasn't found");
                return;
            }
            this.expandedCellUser = this.profileHrefNode.parentNode;

            // Ссылки из первой ячейка заголовка сообщения: якорь и дата
            xpath = "tbody/tr[1]/td[2]/table/tbody/tr[1]/td[1]/a";
            var snapshot = $x(xpath, this.expanded);
            if (snapshot.snapshotLength == 0) {
                logError("CHiddenMessageView.parseExpandedView(): Anchor and date weren't found");
                return;
            }
            // Добавляю все ссылки из 1-й ячейки заголовка сообщения в массив
            this.cellAnchorContent = new Array();
            for (var i = 0; i < snapshot.snapshotLength; i++) {
                var children = snapshot.snapshotItem(i);
                this.cellAnchorContent[i] = children;
            }
            this.expandedCellAnchor = this.cellAnchorContent[0].parentNode;

            // Создаю ссылку "СВЕРНУТЬ"
            var href = document.createElement("A");
            href.href = "#";
            href.style.fontWeight = "bold";
            // "СВЕРНУТЬ"
            href.innerHTML = "\u0421\u0412\u0415\u0420\u041d\u0423\u0422\u042c";
            app.addEventListener(href, "click", this.controller);
            // Строка - заголовок сообщения
            var tr = this.expandedCellAnchor.parentNode;
            // Добавляю ячейку в середину, было 2 ячейки стало 3
            var td = tr.insertCell(1);
            td.align = "center";
            td.width = "10%";
            td.appendChild(href);
            tr.cells[0].width = "40%";
            tr.cells[2].width = "40%";
            this.collapseHref = href;

            // Ссылка на div с рейтингом
            xpath = "tbody/tr[3]/td[2]/table/tbody/tr[1]/td[1]/div";
            this.ratingDiv = $x1(xpath, this.expanded);
            if (this.ratingDiv == null) {
                logError("CHiddenMessageView.parseExpandedView(): Message rating wasn't found");
                return;
            }
            this.expandedCellRating = this.ratingDiv.parentNode;
        },


        createCollapsedUserCell: function() {
            log("CHiddenMessageView.createCollapsedUserCell()");

            var table = document.createElement("TABLE");
            table.width = "100%";
            var row = userTable.insertRow(-1);
            var userCell = userTableRow.insertCell(-1);
            var hrefCell = userTableRow.insertCell(-1);
            hrefCell.align = "right";

            this.collapsedCellUser      = cellUser;
            this.collapsedCellListCtrl  = hrefCell;
        },


        collapse: function() {
            log("CHiddenMessageView.collapse()");

            if (!this.model.isVisible) {
                return;
            }

            this.expanded.style.display = "none";

            this.collapsedCellUser.appendChild(this.userCellContentNode);
            var len = this.cellAnchorContent.length;
            for (var i = 0; i < len; i++) {
                this.collapsedCellInfo.appendChild(this.cellAnchorContent[i]);
            }
            this.collapsedCellRating.appendChild(this.ratingDiv);

            this.collapsed.style.display = "table";

            this.model.isVisible = false;
        },


        expand: function() {
            log("CHiddenMessageView.expand()");

            if (this.model.isVisible) {
                return;
            }

            this.collapsed.style.display = "none";

            this.expandedCellUser.appendChild(this.userCellContentNode);
            var len = this.cellAnchorContent.length;
            for (var i = 0; i < len; i++) {
                this.expandedCellAnchor.appendChild(this.cellAnchorContent[i]);
            }
            this.expandedCellRating.insertBefore(this.ratingDiv, this.expandedCellRating.childNodes[0]);

            this.expanded.style.display = "table";

            this.model.isVisible = true;
        }
    }


    function CUserIgnoreModel(isBlackList, users, scriptUserId) {
        log("CUserIgnoreModel()");

        this.isBlackList = isBlackList;
        this.users = users;
        this.hiddenMessages = new Array();
        this.displayedMessages = new Array();

        var xpath = xpathMsgTableBody + "/tr[1]/td[1]/b/a[contains(@href,'profile')]";
        var snapshot = $x(xpath, $id("topic_posts"));
        //       b  td tr
        xpath = "../../../td[2]/table/tbody/tr[1]/td[2]/a[@class='all_save_to_journal']";
        for (var i = 0; i < snapshot.snapshotLength; i++) {
            var profileHref = snapshot.snapshotItem(i);
            var saveToJournal = $x1(xpath, profileHref);
            if (saveToJournal != null) {
                log("There is script user's message");
                continue;
            }

            var href = profileHref.href;
            var idx = href.indexOf(";u=");
            if (idx == -1) {
                logError("CUserIgnoreModel(). Can't find user ID. href = " + href);
                continue;
            }
            var userId = href.substr(idx + 3);
            var userName = profileHref.innerHTML;
            //             a           b          td         tr         tbody      table
            var msgTable = profileHref.parentNode.parentNode.parentNode.parentNode.parentNode;
            var contains = users[userId] != null;
            if (contains && isBlackList || !contains && !isBlackList) {
                log("Found hidden message: idx = " + i);
                var len = this.hiddenMessages.length;
                this.hiddenMessages[len] = new CHiddenMessageModel(this, msgTable, userId, userName);
            } else {
                var len = this.displayedMessages.length;
                this.displayedMessages[len] = new CDisplayedMessageModel(this, msgTable, userId, userName);
            }
        }
    }


    CUserIgnoreModel.prototype = {
        removeFromList: function(userId) {
            delete this.users[userId];
        },


        addToList: function(userId, userName) {
            this.users[userId] = userName;
        }
    }


    function CUserIgnoreController(settings) {
        log("CUserIgnoreController()");

        app.addController(this);

        this.settings = settings;
        if (settings.userIgnoreType == CSettingsModel.BLACK_LIST) {
            this.model = new CUserIgnoreModel(true,  settings.userIgnoreListBlack);
        } else {
            this.model = new CUserIgnoreModel(false, settings.userIgnoreListWhite);
        }
        this.view = new CUserIgnoreView(this.model, this);

        var len = this.model.hiddenMessages.length;
        for (var i = 0; i < len; i++) {
            var msgModel = this.model.hiddenMessages[i];
            this.view.addHiddenMsgView(new CHiddenMessageView(msgModel, this));
        }

        len = this.model.displayedMessages.length;
        for (var i = 0; i < len; i++) {
            var msgModel = this.model.displayedMessages[i];
            this.view.addDisplayedMsgView(new CDisplayedMessageView(msgModel, this));
        }

        // alt + ctrl + ↑
        app.addHotKey("alt+ctrl+38", this);
        // ctrl + shift + ↑
        app.addHotKey("ctrl+shift+38", this);
        // alt + ctrl + ↓
        app.addHotKey("alt+ctrl+40", this);
        // ctrl + shift + ↓
        app.addHotKey("ctrl+shift+40", this);
    }


    CUserIgnoreController.prototype = {
        handleHotKey: function(hotKey) {
            log("CUserIgnoreController.handleHotKey(" + hotKey + ")");

            if ("alt+ctrl+38" == hotKey || "ctrl+shift+38" == hotKey) {
                // alt + ctrl + ↑ ИЛИ ctrl + shift + ↑
                this.collapse();
            } else if ("alt+ctrl+40" == hotKey || "ctrl+shift+40" == hotKey) {
                // alt + ctrl + ↓ ИЛИ alt + shift + ↓
                this.expand();
            }
        },


        collapse: function() {
            this.view.collapse();
        },


        expand: function() {
            this.view.expand();
        },


        updateSettings: function() {
            if (this.model.isBlackList) {
                this.settings.userIgnoreType = CSettingsModel.BLACK_LIST;
                this.settings.userIgnoreListBlack = this.model.users;
            } else {
                this.settings.userIgnoreType = CSettingsModel.WHITE_LIST;
                this.settings.userIgnoreListWhite = this.model.users;
            }
            this.settings.save();
        }
    }


    function CUserIgnoreView(model, controller) {
        log("CUserIgnoreView()");

        this.model = model;
        this.controller = controller;
        this.hiddenMsgViews = new Array();
        this.displayedMsgViews = new Array();
    }


    CUserIgnoreView.prototype = {
        addHiddenMsgView: function(msgView) {
            this.hiddenMsgViews[this.hiddenMsgViews.length] = msgView;
        },


        addDisplayedMsgView: function(msgView) {
            this.displayedMsgViews[this.displayedMsgViews.length] = msgView;
        },


        collapse: function() {
            log("CUserIgnoreView.collapse()");

            var views = this.hiddenMsgViews;
            var len = views.length;
            for (var i = 0; i < len; i++) {
                views[i].collapse();
            }
        },


        expand: function() {
            log("CUserIgnoreView.expand()");

            var views = this.hiddenMsgViews;
            var len = views.length;
            for (var i = 0; i < len; i++) {
                views[i].expand();
            }
        }
    }


    // ****************************************************************************
    // ************************ Settings View & Controller ************************
    // ****************************************************************************

    // Здесь находится всё, кроме модели. Модель создаётся в самом начале метода
    // onLoad, т.к. она используется ещё до создания других объектов


    function CSettingsController(model, menuCtrl) {
        log("CSettingsController()");

        app.addController(this);

        this.model = model;
        this.view = new CSettingsView(this, menuCtrl);
    }


    CSettingsController.prototype = {
        handle: function(src, e) {
            log("CSettingsController.handle(" + src + ", " + e.type + ")");

            var view = this.view;
            if (src == view.btnSave) {
                this.showHideForm();
                this.synchronizeModel();
                this.model.save();
                wnd.location.reload();
                return false;
            } else if (src == view.href || src == view.btnCancel) {
                this.showHideForm();
                return false;
            } else if (src == view.userIgnoreRadioBlack || src == view.userIgnoreRadioWhite) {
                this.synchronizeSelect();
                return true;
            } else if (src == view.userIgnoreBtnDelete) {
                var select =
                    view.userIgnoreRadioBlack.checked ?
                    view.userIgnoreSelectBlack :
                    view.userIgnoreSelectWhite;
                var options = select.options;
                var len = options.length;
                var idx = -1;
                for (var i = 0; i < len; i++) {
                    if (options[i].selected) {
                        idx = i;
                        break;
                    }
                }
                if (idx != -1) {
                    var option = options[idx];
                    select.removeChild(option);
                    len--;
                    if (idx < len) {
                        options[idx].selected = true;
                    } else if (len > 0) {
                        options[len - 1].selected = true;
                    }
                } else {
                    logError("CSettingsController.handle(): selectedIndex = " + idx);
                }
                return false;
            } else if (src == view.userIgnoreBtnImport) {
                var useBlackList = view.userIgnoreRadioBlack.checked;
                var importStr = prompt(
                    // Импорт \"
                    "\u0418\u043c\u043f\u043e\u0440\u0442 \"" +
                    // БЕЛОГО : ЧЁРНОГО
                    (useBlackList ? "\u0427\u0401\u0420\u041d\u041e\u0413\u041e" : "\u0411\u0415\u041b\u041e\u0413\u041e") +
                    // \" списка.\n"
                    "\" \u0441\u043f\u0438\u0441\u043a\u0430.\n" +
                    // Вставьте
                    "\u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 " +
                    // строку,
                    "\u0441\u0442\u0440\u043e\u043a\u0443, " +
                    // полученную
                    "\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u0443\u044e " +
                    // при экспорте
                    "\u043f\u0440\u0438 \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0435 " +
                    // списка
                    "\u0441\u043f\u0438\u0441\u043a\u0430");
                if (importStr != null && importStr.length > 0) {
                    var arr = new Array();
                    this.model.userIgnoreListFromString(arr, importStr);
                    this.createUserIgnoreListOptionsFromArray(useBlackList, arr);
                }
                return false;
            } else if (src == view.userIgnoreBtnExport) {
                var useBlackList = view.userIgnoreRadioBlack.checked;
                var arr = this.userIgnoreListOptionsToArray(useBlackList);
                var exportStr = this.model.userIgnoreListToString(arr);
                prompt(
                    // Экспорт \"
                    "\u042d\u043a\u0441\u043f\u043e\u0440\u0442 \"" +
                    // БЕЛОГО : ЧЁРНОГО
                    (useBlackList ? "\u0427\u0401\u0420\u041d\u041e\u0413\u041e" : "\u0411\u0415\u041b\u041e\u0413\u041e") +
                    // \" списка.\n"
                    "\" \u0441\u043f\u0438\u0441\u043a\u0430.\n" +
                    // Сохраните
                    "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u0435 " +
                    // куда-нибудь
                    "\u043a\u0443\u0434\u0430-\u043d\u0438\u0431\u0443\u0434\u044c " +
                    // эту строку.\n
                    "\u044d\u0442\u0443 \u0441\u0442\u0440\u043e\u043a\u0443.\n" +
                    // В любой момент
                    "\u0412 \u043b\u044e\u0431\u043e\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 " +
                    // Вы можете
                    "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 " +
                    // восстановить
                    "\u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c " +
                    // сохранённый\n
                    "\u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d\u043d\u044b\u0439\n" +
                    // список
                    "\u0441\u043f\u0438\u0441\u043e\u043a " +
                    // воспользовавшись
                    "\u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0432\u0448\u0438\u0441\u044c " +
                    // кнопкой
                    "\u043a\u043d\u043e\u043f\u043a\u043e\u0439 " +
                    // \"Импорт\"
                    "\"\u0418\u043c\u043f\u043e\u0440\u0442\"",
                    exportStr);
                return false;
            } else {
                logError("CSettingsController.handle(): An unexpected event. src.id = " + src.id + ", e.type = " + e.type);
                return true;
            }
        },

        synchronizeForm: function() {
            log("CSettingsController.synchronizeForm()");

            var model = this.model;
            var view = this.view;
            for (var key in model) {
                if (key.indexOf("sett_") == 0) {
                    var checkbox = view[key];
                    if (checkbox != null) {
                        checkbox.checked = model[key];
                    }
                }
            }

            if (model.userIgnoreType == CSettingsModel.BLACK_LIST) {
                view.userIgnoreRadioBlack.checked = true;
                view.userIgnoreRadioWhite.checked = false;
            } else {
                view.userIgnoreRadioBlack.checked = false;
                view.userIgnoreRadioWhite.checked = true;
            }

            this.createUserIgnoreListOptionsFromArray(true,  model.userIgnoreListBlack);
            this.createUserIgnoreListOptionsFromArray(false, model.userIgnoreListWhite);

            this.synchronizeSelect();
        },


        synchronizeModel: function() {
            log("CSettingsController.synchronizeModel()");

            var model = this.model;
            var view = this.view;
            for (var key in view) {
                if (key.indexOf("sett_") == 0) {
                    var checkbox = view[key];
                    if (checkbox != null) {
                        model[key] = checkbox.checked;
                    }
                }
            }

            model.userIgnoreType =
                view.userIgnoreRadioBlack.checked ?
                CSettingsModel.BLACK_LIST :
                CSettingsModel.WHITE_LIST;

            model.userIgnoreListBlack = this.userIgnoreListOptionsToArray(true);
            model.userIgnoreListWhite = this.userIgnoreListOptionsToArray(false);
        },

        createUserIgnoreListOptionsFromArray: function(useBlackList, userIgnoreList) {
            // Вывожу элементы в порядке их идентификаторов
            var sortedIds = new Array();
            for (var id in userIgnoreList) {
                sortedIds[sortedIds.length] = parseInt(id);
            }
            // Почему-то sort() без параметра не работала, пришлось написать функцию
            sortedIds.sort(function(a, b) {
                if (a < b) {
                    return -1;
                } else if (a > b) {
                    return 1;
                } else {
                    return 0;
                }
            });

            var select =
                useBlackList ?
                this.view.userIgnoreSelectBlack :
                this.view.userIgnoreSelectWhite;
            // Удаляю все существующие элементы
            var len = select.options.length;
            for (i = len - 1; i >= 0; i--) {
                select.removeChild(select.options[i]);
            }

            len = sortedIds.length;
            for (var i = 0; i < len; i++) {
                var option = document.createElement("OPTION");
                var id = sortedIds[i];
                var name = userIgnoreList[new String(id)];
                option.value = id;
                option.setAttribute("userName", name);
                if (name.length > 0) {
                    option.innerHTML = id + " (" + name + ")";
                } else {
                    option.innerHTML = id;
                }
                select.options[select.options.length] = option;
            }
            if (select.options.length > 0) {
                select.options[0].selected = true;
            }
        },

        userIgnoreListOptionsToArray: function(useBlackList) {
            var select =
                useBlackList ?
                this.view.userIgnoreSelectBlack :
                this.view.userIgnoreSelectWhite;
            var options = select.options;
            var arr = new Array();
            var len = options.length;
            for (var i = 0; i < len; i++) {
                var userId = options[i].value;
                var userName = options[i].getAttribute("userName");
                arr[userId] = userName;
            }
            return arr;
        },


        showHideForm: function() {
            if (this.model.isVisible) {
                this.view.div.style.visibility = "hidden";
                this.model.isVisible = false;
            } else {
                this.synchronizeForm();
                this.view.div.style.visibility = "visible";
                this.model.isVisible = true;
            }
        },

        synchronizeSelect: function() {
            log("CSettingsController.synchronizeSelect()");

            var view = this.view;
            if (view.userIgnoreRadioBlack.checked) {
                view.userIgnoreSelectBlack.style.visibility = "inherit";
                view.userIgnoreSelectBlack.style.zOrder     = "20";
                view.userIgnoreSelectWhite.style.visibility = "hidden";
                view.userIgnoreSelectWhite.style.zOrder     = "10";
            } else {
                view.userIgnoreSelectWhite.style.visibility = "inherit";
                view.userIgnoreSelectWhite.style.zOrder     = "20";
                view.userIgnoreSelectBlack.style.visibility = "hidden";
                view.userIgnoreSelectBlack.style.zOrder     = "10";
            }
        }
    }


    function CSettingsView(controller, menuCtrl) {
        this.model = controller.model;
        this.controller = controller;

        this.href = document.createElement("A");
        this.href.href = "#";
        // Настройки
        this.href.innerHTML = "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438";
        app.addEventListener(this.href, "click", this.controller);

        this.form = document.createElement("FORM");
        var form = this.form;
        var model = controller.model;
        for (var key in model) {
            if (key.indexOf("sett_") == 0) {
                var input = document.createElement("INPUT");
                input.type = "checkbox";
                this[key] = input;

                var label = document.createElement("LABEL");
                label.appendChild(input);
                label.appendChild(document.createTextNode(model["label_" + key]));

                form.appendChild(label);
                form.appendChild(document.createElement("BR"));
            }
        }

        this.createUserIgnoreSettingsView();

        var button = document.createElement("INPUT");
        button.type = "button";
        // Сохранить
        button.value = "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c";
        app.addEventListener(button, "click", this.controller);
        this.btnSave = button;
        form.appendChild(button);
        form.appendChild(document.createTextNode(" "));

        button = document.createElement("INPUT");
        button.type = "button";
        // Отменить
        button.value = "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c";
        app.addEventListener(button, "click", this.controller);
        this.btnCancel = button;
        form.appendChild(button);

        this.div = document.createElement("DIV");
        this.div.setAttribute("style", DIV_STYLE);
        this.div.appendChild(form);

        // Добавляю пункт в меню
        var span = document.createElement("SPAN");
        span.style.display = "inline-block";
        span.appendChild(this.href);
        span.appendChild(this.div);

        var item = menuCtrl.createItem("");
        item.appendChild(span);
        menuCtrl.insertScriptMenuItem(item);

        // Продолжаем обработку настроек игнорирования пользователей
        this.userIgnoreRadioBlack   = $id("york_user_ignore_type_black");
        this.userIgnoreRadioWhite   = $id("york_user_ignore_type_white");
        this.userIgnoreSelectBlack  = $id("york_user_ignore_select_black");
        this.userIgnoreSelectWhite  = $id("york_user_ignore_select_white");
        this.userIgnoreBtnDelete    = $id("york_user_ignore_btn_delete");
        this.userIgnoreBtnImport    = $id("york_user_ignore_btn_import");
        this.userIgnoreBtnExport    = $id("york_user_ignore_btn_export");

        app.addEventListener(this.userIgnoreRadioBlack, "click", this.controller);
        app.addEventListener(this.userIgnoreRadioWhite, "click", this.controller);
        app.addEventListener(this.userIgnoreBtnDelete,  "click", this.controller);
        app.addEventListener(this.userIgnoreBtnImport,  "click", this.controller);
        app.addEventListener(this.userIgnoreBtnExport,  "click", this.controller);
    }


    CSettingsView.prototype = {
        createUserIgnoreSettingsView: function() {
            var div = document.createElement("DIV");
            div.setAttribute("style", "border: 1px solid black; margin-top: 10px; margin-bottom: 10px; padding: 5px; width: 300px;");
            div.innerHTML =
                // Автоскрытие сообщений
                "<b>\u0410\u0432\u0442\u043e\u0441\u043a\u0440\u044b\u0442\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439</b>" +
                "<br/>" +
                "<br/>" +
                // Тип списка:
                "\u0422\u0438\u043f \u0441\u043f\u0438\u0441\u043a\u0430:&nbsp;&nbsp;&nbsp;&nbsp;" +
                "<label>" +
                    "<input type='radio' name='york_user_ignore_type' id='york_user_ignore_type_black'/> " +
                    // "Чёрный"
                    "\"\u0427\u0451\u0440\u043d\u044b\u0439\"" +
                "</label>" +
                "&nbsp;&nbsp;&nbsp;&nbsp;" +
                "<label>" +
                    "<input type='radio' name='york_user_ignore_type' id='york_user_ignore_type_white'/> " +
                    // "Белый"
                    "\"\u0411\u0435\u043b\u044b\u0439\"" +
                "</label>" +
                "<br/>" +
                "<br/>" +
                // Список пользователей:
                "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439:" +
                "<table width='100%' cellpadding='0' cellspacint='0'>" +
                    "<tr>" +
                        "<td width='90%' rowspan='2'>" +
                            "<div style='position: relative; top: 0px; left: 0px;'>" +
                              "<select id='york_user_ignore_select_black' size='10' style='z-index: 10; width: 100%; visibility: inherit;'>" +
                              "</select>" +
                              "<select id='york_user_ignore_select_white' size='10' style='z-index: 20; position: absolute; top: 0px; left: 0px; width: 100%; visibility: hidden;'>" +
                              "</select>" +
                            "</div>" +
                        "</td>" +
                        "<td width='10%' valign='top'>" +
                            // Удалить
                            "<input type='button' id='york_user_ignore_btn_delete' value='\u0423\u0434\u0430\u043b\u0438\u0442\u044c'/> " +
                        "</td>" +
                    "</tr>" +
                    "<tr>" +
                        "<td width='10%' valign='bottom'>" +
                            // Импорт
                            "<input type='button' id='york_user_ignore_btn_import' value='\u0418\u043c\u043f\u043e\u0440\u0442'/> " +
                            // Экспорт
                            "<input type='button' id='york_user_ignore_btn_export' value='\u042d\u043a\u0441\u043f\u043e\u0440\u0442'/> " +
                        "</td>" +
                    "</tr>" +
                "</table>" +
                "";

            this.form.appendChild(div);
        }
    }


    // ****************************************************************************
    // ******************************** Search ************************************
    // ****************************************************************************


    function CSearchModel(topicId) {
        log("CSearchModel(" + topicId + ")");

        this.isVisible = false;
        this.isTopic = topicId != null;
        if (this.isTopic) {
            this.topicId = topicId;
            this.topicTitle = document.title;
        }
    }


    function CSearchController(pageCtrl, menuCtrl) {
        log("CSearchController()");

        app.addController(this);

        this.pageCtrl = pageCtrl;
        this.model = new CSearchModel(pageCtrl.model.topicId);
        this.view = new CSearchView(this.model, this, menuCtrl);
    }


    CSearchController.prototype = {
        handle: function(src, e) {
            log("CSearchController.handle(" + src + ", " + e.type + ")");

            if (src == this.view.form) {
                if (this.model.isTopic) {
                    var view = this.view;
                    var form = view.form;
                    if (view.getCheckbox().checked) {
                        form.appendChild(view.getInputTopicId());
                        form.appendChild(view.getInputTopicTitle());
                    } else {
                        form.removeChild(view.getInputTopicId());
                        form.removeChild(view.getInputTopicTitle());
                    }
                }

                this.showHideForm();

                return true;
            } else if (src == this.view.href) {
                this.showHideForm();
                return false;
            } else {
                logError("An unexpected event. src.id = " + src.id + ", e.type = " + e.type);
                return true;
            }
        },

        showHideForm: function() {
            if (this.model.isVisible) {
                this.view.div.style.visibility = "hidden";
                this.model.isVisible = false;
            } else {
                this.view.div.style.visibility = "visible";
                this.view.getInputText().focus();
                this.model.isVisible = true;
            }
        }
    };


    function CSearchView(model, controller, menuCtrl) {
        log("CSearchView()");

        this.model = model;
        this.controller = controller;

        this.href = document.createElement("A");
        this.href.href = "#";
        this.href.innerHTML =
            "<img src='http://www.google.com/favicon.ico' alt='g' " +
            "style='vertical-align: middle; height: 15px;'/>&nbsp;" +
            // Поиск с Google
            "\u041f\u043e\u0438\u0441\u043a \u0441 Google";
        app.addEventListener(this.href, "click", this.controller);

        this.form = document.createElement("FORM");
        var form = this.form;
        form.method = "GET";
        form.action = "http://www.google.com/search";
        form.target = "_blank";
        form.innerHTML =
            "<input type='text' id='york_search_text' name='q' value='' size='30'/>" +
            // Поиск
            "<input type='submit' value='\u041f\u043e\u0438\u0441\u043a'/>";
        if (model.isTopic) {
            form.innerHTML +=
                "<br/>" +
                "<input type='checkbox' id='york_checkbox_in_topic' checked/>" +
                "<label for='york_checkbox_in_topic'>" +
                    // Только в
                    "\u0422\u043e\u043b\u044c\u043a\u043e \u0432 " +
                    // текущей теме
                    "\u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0442\u0435\u043c\u0435" +
                "</label>" +
                "<input type='hidden' id='york_search_topic_id' name='q' value='inurl:" + model.topicId + "'/>" +
                "<input type='hidden' id='york_search_topic_title' name='q' value='intitle:\"" + model.topicTitle + "\"'/>";
        }
        form.innerHTML +=
            "<input type='hidden' name='q' value='site:avanturist.org'/>" +
            "<input type='hidden' name='q' value='inurl:forum'/>" +
            "<input type='hidden' name='q' value='inurl:topic'/>";

        app.addEventListener(this.form, "submit", this.controller);

        this.div = document.createElement("DIV");
        this.div.align = "left";
        this.div.setAttribute("style", DIV_STYLE);
        this.div.appendChild(this.form);

        var span = document.createElement("SPAN");
        span.style.display = "inline-block";
        span.appendChild(this.href);
        span.appendChild(this.div);

        var item = menuCtrl.createItem("");
        item.appendChild(span);
        menuCtrl.insertTopUserItem(item, 0);
    }


    CSearchView.prototype = {
        getInputText: function() {
            if (this.inputText == null) {
                this.inputText = $id("york_search_text");
            }
            return this.inputText;
        },

        getInputTopicId: function() {
            if (this.inputTopicId == null) {
                this.inputTopicId = $id("york_search_topic_id");
            }
            return this.inputTopicId;
        },

        getInputTopicTitle: function() {
            if (this.inputTopicTitle == null) {
                this.inputTopicTitle = $id("york_search_topic_title");
            }
            return this.inputTopicTitle;
        },

        getCheckbox: function() {
            if (this.checkbox == null) {
                this.checkbox = $id("york_checkbox_in_topic");
            }
            return this.checkbox;
        }
    }


    // ****************************************************************************
    // ********************************* Menu *************************************
    // ****************************************************************************


    // TODO !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    function throwError(msg) {
        logError(msg);
        throw msg;
    }


    function CMenuControl() {
        log("CMenuControl()");

        var xpathDivMain = "div[@class='divMain']";
        var xpathDivTopMenu = xpathDivMain + "/div[@class='topMenu']";
        var xpathDivContainer = xpathDivMain + "/div[@class='divContainer']";
        var xpathDivContent = xpathDivContainer + "/div[@class='content']";
        var xpathDivText = xpathDivContent + "/div[@class='text']";

        var topMenu = $x1(xpathDivTopMenu);
        if (null == topMenu) {
            throwError("CMenuControl(): Top menu wasn't found");
        }

        // Раньше, это меню было в виде списка (ul), теперь это простые div,
        // поэтому для единообразия помещаю все эти div в ещё один div
        var topMenuItems = $x(xpathDivTopMenu + "/div[@class='link']");
        if (null == topMenuItems || 0 == topMenuItems.snapshotLength) {
            throwError("CMenuControl(): Top menu items weren't found");
        }
        this.topMenuLinks = document.createElement("DIV");
        var firstMenuItem = topMenuItems.snapshotItem(0);
        firstMenuItem.parentNode.insertBefore(this.topMenuLinks, firstMenuItem);
        for (var i = 0; i < topMenuItems.snapshotLength; i++) {
            var item = topMenuItems.snapshotItem(i);
            this.topMenuLinks.appendChild(item);
        }

        this.topMenuUser = $x1(xpathDivTopMenu + "/ul[@class='userLinks']");
        if (null == this.topMenuUser) {
            throwError("CMenuControl(): Top user menu wasn't found");
        }

        // Создаю div для меню скрипта
        var divScriptMenu = document.createElement("DIV");
        divScriptMenu.className = "topMenu";
        divScriptMenu.style.paddingTop = "4px";

        this.scriptMenu = document.createElement("UL");
        this.scriptMenu.className = "userLinks";
        this.scriptMenu.style.width = "600px";
        this.scriptMenu.style.cssFloat = "left";

        this.scriptLinks = document.createElement("UL");
        this.scriptLinks.className = "userLinks";

        // Добавляю меню скрипта на страницу
        divScriptMenu.appendChild(this.scriptMenu);
        divScriptMenu.appendChild(this.scriptLinks);
        topMenu.parentNode.insertBefore(divScriptMenu, topMenu.nextSibling);

        // Удаляю два пустых div'а
        var xpathVSpacer = xpathDivText + "/div[position()=1 and count(child::*)=0]";
        for (var i = 0; i < 2; ++i) {
            var divRemovedSpacer = $x1(xpathVSpacer);
            if (null == divRemovedSpacer) {
                // Если не удалось удалить элемент, то продолжаем работу
                logError("CMenuControl(): Vertical spacer wasn't found");
                break;
            }
            divRemovedSpacer.parentNode.removeChild(divRemovedSpacer);
        }

        // Уменьшаю высоту div'а clear
        var divClear = $x1(xpathDivText + "/div[@class='clear']");
        if (null == divClear) {
            // Если не удалось удалить элемент, то продолжаем работу
            logError("CMenuControl(): Vertical spacer wasn't found");
        } else {
            divClear.style.height = "8px";
        }

        // Добавляю текст "Скрипт:"
        // <b>Скрипт:</b>
        var item = this.createItem("<b>\u0421\u043a\u0440\u0438\u043f\u0442:</b>");
        this.insertScriptMenuItem(item);

        // Делаю отступ перед именем пользователя
        item = this.createItem("&nbsp;&nbsp;&nbsp;");
        this.insertTopUserItem(item, 0);
    }


    CMenuControl.prototype = {
        insertTopLinksItem: function(item, idx) {
            this.insertItem(this.topMenuLinks, item, idx);
        },

        insertTopUserItem: function(item, idx) {
            this.insertItem(this.topMenuUser, item, idx);
        },

        insertScriptMenuItem: function(item, idx) {
            this.insertItem(this.scriptMenu, item, idx);
        },

        insertScriptLinksItem: function(item, idx) {
            this.insertItem(this.scriptLinks, item, idx);
        },

        insertItem: function(menu, item, idx) {
            if (null == idx) {
                menu.appendChild(item);
            } else {
                menu.insertBefore(item, menu.childNodes[idx]);
            }
        },

        createItem: function(text, url) {
            var textNode;
            if (null != url) {
                textNode = document.createElement("A");
                textNode.href = url;
            } else {
                textNode = document.createElement("SPAN");
            }
            textNode.innerHTML = text;

            var item = document.createElement("LI");
            item.appendChild(textNode);

            return item;
        }
    }


    // ****************************************************************************
    // ********************************* Page *************************************
    // ****************************************************************************


    function CPageModel() {
        log("CPageModel()");

        if (jQueryInstalled && /myarchive/.test(url)) {
            this.pageType = CPageModel.PAGE_TYPE_ARCHIVE;
        } else if (jQueryInstalled && /myjournal/.test(url)) {
            this.pageType = CPageModel.PAGE_TYPE_JOURNAL;
        } else if (jQueryInstalled && /index\.php.+action=post/.test(url)) {
            this.pageType = CPageModel.PAGE_TYPE_MESSAGE;
        } else if (jQueryInstalled && /index\.php.+topic[=,]/.test(url)) {
            this.pageType = CPageModel.PAGE_TYPE_TOPIC;
        } else if (jQueryInstalled && /index\.php.+board[=,]/.test(url)) {
            this.pageType = CPageModel.PAGE_TYPE_BOARD;
        } else if (jQueryInstalled && /\/forum\//.test(url)) {
            // TODO дублирование кода, где-то уже это есть
            // TODO здесь было $id("category")
            if (null != $x1("div[@class='divMain']/div[@class='divContainer']/div[@class='content']/div[@class='text']/div[not(@class)]/table[contains(@class, 'boardTable')]")) {
                this.pageType = CPageModel.PAGE_TYPE_MAIN_FORUM_PAGE;
            } else {
                this.pageType = CPageModel.PAGE_TYPE_OTHER_FORUM_PAGE;
            }
        } else {
            this.pageType = CPageModel.PAGE_TYPE_OTHER_SITE_PAGE;
        }
        log("Page type = " + this.pageType);

        if (CPageModel.PAGE_TYPE_TOPIC == this.pageType) {
            // TODO дублирование кода, где-то уже это есть
            this.topicId = getTopicId();
            log("topic_id = " + this.topicId);
//            if (null != this.topicId) {
//                this.topicId = input.value;
//            }

            // TODO: Дублирование кода, см. CPostSearcher
            // Нахожу навигатор по страницам и определяю текущую страницу (находится в теге <b>)
            // Ищем тэг B, после которого идёт текст, содержащий "]"
//            var xpath = "tbody[1]/tr[1]/td[1]/table[not(@id)]/tbody[1]/tr[1]/td[@class='middletext']/b[following-sibling::node()[position()=1 and contains(self::text(), ']')]]";
            var xpath = "//body/div[@class='divMain']/div[@class='divContainer']/" +
                "div[@class='content']/div[@class='text']/div[not(@class)]/" +
                "table[not(@id)]/tbody[1]/tr[1]/td[@class='middletext']/" +
                "b[following-sibling::node()[position()=1 and contains(self::text(), ']')]]";
            // TODO $id("content")
            var b = $x1(xpath, $id("content"));
            if (b != null) {
                this.currentPage = parseInt(b.innerHTML);
                log("currentPage = " + this.currentPage);
            } else {
                thtowError("CPostSearcher(): Navigator wasn't found");
            }

            // TODO: Дублирование кода, см. CPostSearcher
            // Определяю номер последней страницы темы (последняя ссылка в навигаторе)
//            xpath = "tbody[1]/tr[1]/td[1]/table[not(@id)]/tbody[1]/tr[1]/td[@class='middletext']/a[@class='navPages' and position()=last()]";
            xpath = "//body/div[@class='divMain']/div[@class='divContainer']/" +
                "div[@class='content']/div[@class='text']/div[not(@class)]/" +
                "table[not(@id)]/tbody[1]/tr[1]/td[@class='middletext']/" +
                "a[@class='navPages' and position()=last()]";
            // TODO $id("content")
            var href = $x1(xpath, $id("content"));
            this.lastPage = href != null ? parseInt(href.innerHTML) : 1;
            this.lastPage = Math.max(this.lastPage, this.currentPage);
            log("lastPage = " + this.lastPage);

            if (this.currentPage != 1) {
                this.firstPageUrl = this.buildPageUrl(1);
                this.prevPageUrl  = this.buildPageUrl(this.currentPage - 1);
            }

            if (this.currentPage != this.lastPage) {
                this.nextPageUrl = this.buildPageUrl(this.currentPage + 1);
                this.lastPageUrl = this.buildPageUrl(this.lastPage);
            }

            this.addBottomLinkTree();
        }
    }


    CPageModel.PAGE_TYPE_ARCHIVE            = 0x0201;
    CPageModel.PAGE_TYPE_JOURNAL            = 0x0202;
    CPageModel.PAGE_TYPE_MAIN_FORUM_PAGE    = 0x0104;
    CPageModel.PAGE_TYPE_BOARD              = 0x0108;
    CPageModel.PAGE_TYPE_TOPIC              = 0x0510;
    CPageModel.PAGE_TYPE_MESSAGE            = 0x0420;   // Форма отправки/редактирования сообщения
    CPageModel.PAGE_TYPE_OTHER_FORUM_PAGE   = 0x0140;
    CPageModel.PAGE_TYPE_OTHER_SITE_PAGE    = 0x0280;

    // Страницы форума, на которых надо показывать меню, поиск и настройки
    CPageModel.PAGE_TYPE_FORUM_PAGE_GROUP   = 0x0100;
    // Прочие страницы сайта
    CPageModel.PAGE_TYPE_SITE_GROUP         = 0x0200;
    // Страницы форума отображающие содержиое темы: просмотр темы и отправка сообщения
    CPageModel.PAGE_TYPE_FORUM_TOPIC_GROUP  = 0x0400;


    CPageModel.prototype = {
        isClosedTopic: function() {
            if (this.pageType != CPageModel.PAGE_TYPE_TOPIC)
                return false;

            if (typeof this.isClosed == "undefined") {
                // TODO дублирование кода
                var xpathDivMain = "div[@class='divMain']";
                var xpathDivContainer = xpathDivMain + "/div[@class='divContainer']";
                var xpathDivContent = xpathDivContainer + "/div[@class='content']";
                var xpathDivText = xpathDivContent + "/div[@class='text']";
                var xpathDivTopMenu = xpathDivMain + "/div[@class='topMenu']";
                var xpathULUserLinks = xpathDivTopMenu + "/ul[@class='userLinks']";
                // Проверяю, вошёл ли пользователь
                var xpath = xpathULUserLinks + "/li/a[contains(@href, '/user/signup')]";
                var userSignedup = null == $x1(xpath);

                if (userSignedup) {
                    // Проверяю, есть ли кнопка "Ответить"
                    // TODO как-нибудь надо упростить обращение с xpath
                    var xpath = xpathDivText + "/div[not(@class)]/table[not(@class)]/tbody[1]/tr[1]/td[2]/a[contains(@href, 'action=post')]";
                    this.isClosed = null == $x1(xpath);
                } else {
                    this.isClosed = false;
                }
            }

            return this.isClosed;
        },

        buildPageUrl: function(page) {
            var start = (page - 1) * 20;
            return "/forum/index.php/topic," + this.topicId + "." + start + ".html";
        },

        addBottomLinkTree: function() {
            log("CPageModel.addBottomLinkTree()");

            // TODO дублирование кода
            var xpathDivMain = "div[@class='divMain']";
            var xpathDivContainer = xpathDivMain + "/div[@class='divContainer']";
            var xpathDivContent = xpathDivContainer + "/div[@class='content']";
            var xpathDivText = xpathDivContent + "/div[@class='text']";
            // TODO собрать работу с xpath в одном месте
            xpath = xpathDivText + "//table[0 < count(tbody/tr/td/a[@class='nav'])]";
            var linkTrees = $x(xpath);
            if (linkTrees.snapshotLength < 2) {
                log("The bottom link tree is absent");
                var tableLinkTree = linkTrees.snapshotItem(0);
                var divLinkTree = tableLinkTree.parentNode;
                var clone = divLinkTree.cloneNode(true);
                // Навигатор вставляю в конец, и это нормально
                divLinkTree.parentNode.appendChild(clone);
            } else {
                log("The bottom link tree is present");
            }
        }
    };


    function CPageController(isForumPage, isTopic) {
        log("CPageController()");

        app.addController(this);

        this.settings = new CSettingsModel();
        this.model = new CPageModel();
        this.view = new CPageView(this, this.model);

        if ((this.model.pageType & CPageModel.PAGE_TYPE_FORUM_PAGE_GROUP) != 0) {
            this.searchCtrl = new CSearchController(this, this.view.menuCtrl);

            // Настройка меню скрипта
            this.settingsCtrl = new CSettingsController(this.settings, this.view.menuCtrl);
            this.view.createUserMenuItems();
        }
        if (CPageModel.PAGE_TYPE_TOPIC == this.model.pageType) {
            this.userIgnoreController = new CUserIgnoreController(this.settings);
            this.userIgnoreController.collapse();
            // Горячие клавиши для навигации по теме
            // ctrl + ←
            app.addHotKey("ctrl+37", this);
            // ctrl + →
            app.addHotKey("ctrl+39", this);
            // ctrl + Home
            app.addHotKey("ctrl+36", this);
            // ctrl + End
            app.addHotKey("ctrl+35", this);
        }
        if (CPageModel.PAGE_TYPE_MAIN_FORUM_PAGE == this.model.pageType) {
            // Горячие клавиши для сворачивания/разворачивания тем на первой странице
            // alt + ctrl + ↑
            app.addHotKey("alt+ctrl+38", this);
            // ctrl + shift + ↑
            app.addHotKey("ctrl+shift+38", this);
            // alt + ctrl + ↓
            app.addHotKey("alt+ctrl+40", this);
            // ctrl + shift + ↓
            app.addHotKey("ctrl+shift+40", this);
        }
    }


    CPageController.prototype = {
        handle: function(src, e) {
            log("CPageController.handle(" + src + ", " + e.type + ")");

            if (e.type == "click") {
                if (src == this.view.collapseAllBoards) {
                    // TODO: переделать
                    topicWrapper.collapse();
                    return false;

                } else if (src == this.view.expandAllBoards) {
                    // TODO: переделать
                    topicWrapper.expand();
                    return false;

                } else if (src == this.view.collapseIgnoredMsgs) {
                    this.userIgnoreController.collapse();
                    return false;

                } else if (src == this.view.expandIgnoredMsgs) {
                    this.userIgnoreController.expand();
                    return false;

                } else if (src == this.view.btnCode || src.parentNode == this.view.btnCode) {
                    wnd.surroundText("[code]", "[/code]", this.view.textArea);
                    return false;

                } else if (src == this.view.btnUrl || src.parentNode == this.view.btnUrl) {
                    // Введите URL. Например, http://yandex.ru
                    var href = wnd.prompt("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 URL. " +
                        "\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, http://yandex.ru", "");
                    wnd.surroundText("[url=" + href + "]", "[/url]", this.view.textArea);
                    return false;

                } else if (src == this.view.btnQuote || src.parentNode == this.view.btnQuote) {
                    // Укажите автора цитаты или её источник. Например, avanturist.
                    // Можете ничего не указывать.
                    // Кнопка "Отменить" отменяет создание цитаты.
                    var source = wnd.prompt("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 " +
                        "\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a " +
                        "\u0446\u0438\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f. " +
                        "\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, avanturist.\n" +
                        "\u041c\u043e\u0436\u0435\u0442\u0435 " +
                        "\u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 " +
                        "\u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c.\n" +
                        "\u041a\u043d\u043e\u043f\u043a\u0430 " +
                        "\"\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c\" " +
                        "\u043e\u0442\u043c\u0435\u043d\u044f\u0435\u0442 " +
                        "\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 " +
                        "\u0446\u0438\u0442\u0430\u0442\u044b.", "");
                    if (source != null) {
                        var openQuote = "[quote" + (source != "" ? "=" + source : "") + "]";
                        wnd.surroundText(openQuote, "[/quote]", this.view.textArea);
                    }
                    return false;

                } else if (src == this.view.btnFormatTable || src.parentNode == this.view.btnFormatTable) {
                    var textArea = this.view.textArea;
                    // Can a text range be created?
                    if (typeof(textArea.caretPos) != "undefined" && textArea.createTextRange) {
                        var caretPos = textArea.caretPos;
                        var len = caretPos.text.length;

                        var formatted = this.formatTable(caretPos.text);
                        if (formatted.length > 0) {
                            caretPos.text = formatted;
                            textArea.focus(caretPos);
                        }
                    } else if (typeof(textArea.selectionStart) != "undefined") {
                        // Mozilla text range wrap
                        var begin = textArea.value.substr(0, textArea.selectionStart);
                        var selection = textArea.value.substr(textArea.selectionStart,
                            textArea.selectionEnd - textArea.selectionStart);
                        var end = textArea.value.substr(textArea.selectionEnd);
                        var newCursorPos = textArea.selectionStart;
                        var scrollPos = textArea.scrollTop;

                        var formatted = this.formatTable(selection);
                        if (formatted.length > 0) {
                            textArea.value = begin + formatted + end;

                            if (textArea.setSelectionRange) {
                                textArea.setSelectionRange(newCursorPos, newCursorPos + formatted.length);
                                textArea.focus();
                            }
                            textArea.scrollTop = scrollPos;
                        }
                    } else {
                        // Извините, функция не поддерживается вашим браузером
                        alert("\u0418\u0437\u0432\u0438\u043d\u0438\u0442\u0435, " +
                            "\u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043d\u0435 " +
                            "\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f " +
                            "\u0432\u0430\u0448\u0438\u043c " +
                            "\u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043e\u043c");
                    }
                    return false;

                } else {
                    logError("An unexpected event. src.id = " + src.id + ", e.type = " + e.type);
                    return true;
                }

            } else if (e.type == "mouseover") {
                if (wnd.bbc_highlight) {
                    wnd.bbc_highlight(src, true);
                }
                return false;

            } else if (e.type == "mouseout") {
                if (wnd.bbc_highlight) {
                    wnd.bbc_highlight(src, false);
                }
                return false;

            } else {
                logError("An unexpected event. src.id = " + src.id + ", e.type = " + e.type);
                return true;
            }
        },


        handleHotKey: function(hotKey) {
            log("CPageController.handleHotKey(" + hotKey + ")");

            if ("alt+ctrl+38" == hotKey || "ctrl+shift+38" == hotKey) {
                // alt + ctrl + ↑ ИЛИ ctrl + shift + ↑
                // TODO: переделать
                topicWrapper.collapse();

            } else if ("alt+ctrl+40" == hotKey || "ctrl+shift+40" == hotKey) {
                // alt + ctrl + ↓ ИЛИ alt + shift + ↓
                // TODO: переделать
                topicWrapper.expand();

            } else if ("ctrl+37" == hotKey) {
                // ctrl + ←
                if (null != this.model.prevPageUrl) {
                    window.location.replace(this.model.prevPageUrl);
                };

            } else if ("ctrl+39" == hotKey) {
                // ctrl + →
                if (null != this.model.nextPageUrl) {
                    window.location.replace(this.model.nextPageUrl);
                };

            } else if ("ctrl+36" == hotKey) {
                // ctrl + Home
                if (null != this.model.firstPageUrl) {
                    window.location.replace(this.model.firstPageUrl);
                };

            } else if ("ctrl+35" == hotKey) {
                // ctrl + End
                if (null != this.model.lastPageUrl) {
                    window.location.replace(this.model.lastPageUrl);
                };
            }
        },


        handleSaveToArchive: function(src, e) {
            log("CPageController.handleSaveToArchive(" + src + ", " + e.type + ")");

            var topic_id   = $(src).parent().parent().find("input[name='topic_id']").val();
            var message_id = $(src).parent().parent().find("input[name='message_id']").val();

            // Проверяем входные параметры
            if (topic_id == 'undefined' || message_id == 'undefined') {
                // Ошибка сохранения материала в ваш личный архив!
                alert("\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u0430 \u0432 \u0432\u0430\u0448 \u043b\u0438\u0447\u043d\u044b\u0439 \u0430\u0440\u0445\u0438\u0432!");
                return;
            }

            // отправляем запрос
            $.getJSON("/forum/index.php?action=all_ajax_save_to_archive",
                {topic_id: topic_id, message_id: message_id},
                function(data) {
                    if (data.result == 1) {
                        $(src).hide();
                    } else {
                        alert(data.result_status);
                    }
                }
            );

            return false;
        },

        formatTable: function(text) {
            // Сначала удаляем все лишние символы сначала и конца строки
            var start = text.length;
            for (var i = 0; i < text.length; i++) {
                var ch = text[i];
                if (ch != " " && ch != "\r" && ch != "\n") {
                    start = i;
                    break;
                }
            }
            if (start == text.length) {
                logError("CPageController.formatTable(): the text is empty");
                return "";
            }

            var end = 0;
            for (var i = text.length - 1; i > start; i--) {
                var ch = text[i];
                if (ch != " " && ch != "\r" && ch != "\n") {
                    end = i;
                    break;
                }
            }

            var res = "[table]\n[tr][td]";
            var len = text.length;
            for (var i = start; i <= end; i++) {
                var ch = text[i];
                if (ch == "\t") {
                    res += "[/td][td]";
                } else if (ch == "\n") {
                    res += "[/td][/tr]\n[tr][td]";
                } else if (ch == "\r") {
                    // Ignore
                } else {
                    res += ch;
                }
            }
            res += "[/td][/tr]\n[/table]\n";

            return res;
        }
    };


    function CPageView(controller, model) {
        log("CPageView()");

        this.controller = controller;
        this.model = model;

        if ((model.pageType & CPageModel.PAGE_TYPE_FORUM_PAGE_GROUP) != 0) {
            this.menuCtrl = new CMenuControl();

            var item = document.createElement("DIV");
            item.className = "link";
            item.style.width = "80px";
            item.innerHTML = "<a href=\"/forum/index.php?action=stats\">" +
                // Статистика
                "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430" +
                "</a>";
            this.menuCtrl.insertTopLinksItem(item, 3);

            item = this.menuCtrl.createItem(
                "<img src='/forum/Themes/default/images/ga_ico_search.gif' " +
                    "style='vertical-align:middle; margin-bottom:1px;'/>" +
                // &nbsp;Поиск
                "&nbsp;\u041f\u043e\u0438\u0441\u043a",
                "/forum/index.php?action=search");
            this.menuCtrl.insertTopUserItem(item, 0);

            item = this.menuCtrl.createItem(
                // Непрочитанные
                "\u041d\u0435\u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0435 " +
                // темы
                "\u0442\u0435\u043c\u044b",
                "/forum/index.php?action=unread");
            this.menuCtrl.insertScriptLinksItem(item);

            item = this.menuCtrl.createItem(
                // Непрочитанные
                "\u041d\u0435\u043f\u0440\u043e\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0435 " +
                // ответы
                "\u043e\u0442\u0432\u0435\u0442\u044b",
                "/forum/index.php?action=unreadreplies");
            this.menuCtrl.insertScriptLinksItem(item);

            item = this.menuCtrl.createItem(
                // Последние
                "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 " +
                // сообщения
                "\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f",
                "/forum/index.php?action=recent");
            this.menuCtrl.insertScriptLinksItem(item);
        }

        if (model.pageType == CPageModel.PAGE_TYPE_MESSAGE) {
            var xpath = "table/tbody[1]/tr[2]/td[1]/table[1]/tbody[1]/tr/td[2]/textarea[@name='message']";
            this.textArea  = $x1(xpath, $id("postmodify"));
            if (this.textArea == null) {
                logError("CPageView(): the text area wasn't found");
                return;
            }

            xpath = "table/tbody[1]/tr[2]/td[1]/table[1]/tbody[1]/tr/td[2]/br[preceding-sibling::img[position()=1 and @alt='|']]";
            var br = $x1(xpath, $id("postmodify"));
            if (br == null) {
                logError("CPageView(): <br> in the buttons cell wasn't found");
                return;
            }

            var td = br.parentNode;
            td.removeChild(br);

            // Ищем картинку разделитель, если находим, то добавляем разделитель
            for (var i = 0; i < td.childNodes.length; i++) {
                var child = td.childNodes[i];
                if (child.tagName != null && child.tagName == "IMG") {
                    td.appendChild(child.cloneNode(true));
                    td.appendChild(document.createTextNode(" "));
                    break;
                }
            }

            // Форматировать таблицу
            this.btnFormatTable = this.createButton("/forum/Themes/default/images/bbc/flash.gif",
                "\u0424\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c " +
                "\u0442\u0430\u0431\u043b\u0438\u0446\u0443");
            td.appendChild(this.btnFormatTable);

            // Код
            this.btnCode = this.createButton("/forum/Themes/default/images/bbc/code.gif",
                "\u041a\u043e\u0434");
            td.appendChild(this.btnCode);

            // Вставить URL
            this.btnUrl = this.createButton("/forum/Themes/default/images/bbc/url.gif",
                "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c URL");
            td.appendChild(this.btnUrl);

            // Цитата
            this.btnQuote = this.createButton("/forum/Themes/default/images/bbc/quote.gif",
                "\u0426\u0438\u0442\u0430\u0442\u0430");
            td.appendChild(this.btnQuote);
        }

        if (model.pageType == CPageModel.PAGE_TYPE_TOPIC) {
            if (model.isClosedTopic()) {
                this.addSaveToArchiveButtons();
            }

            if (controller.settings.sett_addImageLinks) {
                this.addImageLinks();
            }

            // TODO: добавить настройку
            this.updateHeadLinks();
        }

        if ((model.pageType & CPageModel.PAGE_TYPE_FORUM_TOPIC_GROUP) != 0) {
            if (controller.settings.sett_borderTables) {
                this.borderTables();
            }
        }

        if ((model.pageType & CPageModel.PAGE_TYPE_FORUM_PAGE_GROUP) != 0 ||
            (model.pageType & CPageModel.PAGE_TYPE_FORUM_TOPIC_GROUP) != 0) {
            this.addScriptVersion();
        }

        this.addImageAlt();
        this.addFavicon();
        this.addCustomCSS();
    }


    CPageView.prototype = {
        createUserMenuItems: function(parentNode) {
            if (CPageModel.PAGE_TYPE_MAIN_FORUM_PAGE == this.model.pageType) {
                // Свернуть разделы
                this.collapseAllBoards = this.appendScriptMenuItem(
                    "\u0421\u0432\u0435\u0440\u043d\u0443\u0442\u044c " +
                    "\u0440\u0430\u0437\u0434\u0435\u043b\u044b");
                // Развернуть разделы
                this.expandAllBoards = this.appendScriptMenuItem(
                    "\u0420\u0430\u0437\u0432\u0435\u0440\u043d\u0443\u0442\u044c " +
                    "\u0440\u0430\u0437\u0434\u0435\u043b\u044b");
            } else if (CPageModel.PAGE_TYPE_TOPIC == this.model.pageType) {
                // Свернуть игнорируемые сообщения
                this.collapseIgnoredMsgs = this.appendScriptMenuItem(
                    "\u0421\u0432\u0435\u0440\u043d\u0443\u0442\u044c " +
                    "\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u0435\u043c\u044b\u0435 " +
                    "\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f");
                // Развернуть игнорируемые сообщения
                this.expandIgnoredMsgs = this.appendScriptMenuItem(
                    "\u0420\u0430\u0437\u0432\u0435\u0440\u043d\u0443\u0442\u044c " +
                    "\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u0435\u043c\u044b\u0435 " +
                    "\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f");
            }
        },

        appendScriptMenuItem: function(title) {
            var href = document.createElement("A");
            href.href = "#";
            href.innerHTML = title;
            app.addEventListener(href, "click", this.controller);

            var item = this.menuCtrl.createItem("");
            item.appendChild(href);
            this.menuCtrl.insertScriptMenuItem(item);

            return href;
        },

        createButton: function(imgUrl, title) {
            var img = document.createElement("IMG");
            img.src = imgUrl;
            img.title = title;
            img.alt = title;
            img.height = 22;
            img.width = 23;
            img.align = "bottom";
            img.style.margin = "0px 2px 0px 1px";
            img.style.backgroundImage = "url(/forum/Themes/default/images/bbc/bbc_bg.gif)";
            img.style.verticalAlign = "middle";
            app.addEventListener(img, "mouseout", this.controller);
            app.addEventListener(img, "mouseover", this.controller);

            var href = document.createElement("A");
            href.href = "#";
            app.addEventListener(href, "click", this.controller);
            href.appendChild(img);

            return href;
        },

        borderTables: function() {
            var cssCode =
                ".text .table .table .post table, #preview_section .post table {" +
                    "border: 2px solid rgb(213, 213, 234);" +
                    "border-spacing: 0px;" +
                    "border-collapse: collapse;" +
                    "margin-top: 5px; } " +
                ".text .table .table .post tr:first-child, #preview_section .post tr:first-child {" +
                    "font-weight: bold; }" +
                ".text .table .table .post td, #preview_section .post td {" +
                    "border: 1px solid rgb(213, 213, 234);" +
                    "padding: 2px;" +
                "}";
            var styleElement = document.createElement("style");
            styleElement.type = "text/css";
            if (styleElement.styleSheet) {
                styleElement.styleSheet.cssText = cssCode;
            } else {
                styleElement.appendChild(document.createTextNode(cssCode));
            }
            document.getElementsByTagName("head")[0].appendChild(styleElement);
        },

        // Окружаю рисунки без ссылок на источник, такими ссылками
        addImageLinks: function() {
            log("CPageView.addImageLinks()");

            // Выбираю все рисунки из текстов сообщений, которые:
            // 1. не содержат в пути "avanturist.org/",
            // 2. путь к нем начинается с "http://",
            // 3. не окружены ссылками
            // 4. имеют теги height и width
            var xpath = xpathPostBody + "//img[" +
                "not(contains(@src, 'avanturist.org/')) and " +
                "starts-with(@src, 'http://') and " +
                "string-length(@width) > 0 and string-length(@height) > 0 and " +
                "count(ancestor::a)=0]";
            var snapshot = $x(xpath, $id("topic_posts"));
            log("Found images: " + snapshot.snapshotLength);
            for (var i = 0; i < snapshot.snapshotLength; i++) {
                var img = snapshot.snapshotItem(i);
                var href = document.createElement("A");
                href.href = img.src;
                href.target = "_blank";
                img.parentNode.insertBefore(href, img);
                href.appendChild(img);
            }
        },

        // Добавляю атрибут alt ко всем рисункам без этого атрибута
        addImageAlt: function() {
            log("CPageView.addImageAlt()");

            // Выбираю все рисунки без атрибута alt
            var xpath = "//img[@alt='']";
            var snapshot = $x(xpath);
            log("Found images without alt: " + snapshot.snapshotLength);
            for (var i = 0; i < snapshot.snapshotLength; i++) {
                var img = snapshot.snapshotItem(i);
                img.alt = "IMG";
            }
        },

        addSaveToArchiveButtons: function() {
            log("CPageView.addSaveToArchiveButtons()");

            // Получаю последние ячейки в таблице, которая находится в правой верхней ячейки таблицы сообщения
            var xpath = xpathMsgTableBody + "/tr[1]/td[2]/table/tbody/tr[1]/td[count(../td)]";
            var snapshot = $x(xpath, $id("topic_posts"));
            for (var i = 0; i < snapshot.snapshotLength; i++) {
                var td = snapshot.snapshotItem(i);

                var img = document.createElement("IMG");
                img.src = "/forum/Themes/default/images/btn_save_to_archive.gif";
                img.style.verticalAlign = "middle";
                img.style.marginTop = "-3px";
                img.style.marginBottom = "-4px";
                // Сохранить в архив
                img.alt = "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432 \u0430\u0440\u0445\u0438\u0432";
                img.title = img.alt;

                var href = document.createElement("A");
                href.href = "#";
                href.appendChild(img);
                app.addEventListener(href, "click", this.controller, "handleSaveToArchive");

                td.appendChild(href);
            }
        },
        
        updateHeadLinks: function() {
            log("CPageView.updateHeadLinks()");

            var head = document.getElementsByTagName("HEAD")[0];

            // Только (?) для Opera
            var link = document.createElement("LINK");
            link.href = "#";
            link.rel = "up";
            head.appendChild(link);

            link = document.createElement("LINK");
            link.href = "/forum/index.php?action=search";
            link.rel = "search";
            head.appendChild(link);

            link = document.createElement("LINK");
            link.href = "/forum/index.php";
            link.rel = "Index";
            head.appendChild(link);

            link = document.createElement("LINK");
            link.href = "/forum/index.php";
            link.rel = "Contents";
            head.appendChild(link);

            link = document.createElement("LINK");
            link.href = "/forum/index.php?action=help";
            link.rel = "Help";
            head.appendChild(link);

            if (null != this.model.firstPageUrl) {
                link = link.cloneNode(true);
                link.href = this.model.firstPageUrl;
                link.rel = "start";
                head.appendChild(link);

                // Только (?) для Opera
                link = link.cloneNode(true);
                link.rel = "first";
                head.appendChild(link);
            }

            if (null != this.model.lastPageUrl) {
                link = link.cloneNode(true);
                link.href = this.model.lastPageUrl;
                link.rel = "last";
                head.appendChild(link);
            }

            var xpath = "/html/head/link[@rel='prev']";
            link = $x1(xpath);
            if (null != this.model.prevPageUrl) {
                if (null == link) {
                    log("CPageView.updateHeadLinks(): <link rel='prev'> wasn't found");
                    link = document.createElement("LINK");
                    link.rel = "prev";
                    head.appendChild(link);
                }
                link.href = this.model.prevPageUrl;
            } else if (null != link) {
                link.parentNode.removeChild(link);
            }

            xpath = "/html/head/link[@rel='next']";
            link = $x1(xpath);
            if (null != this.model.nextPageUrl) {
                if (null == link) {
                    log("CPageView.updateHeadLinks(): <link rel='next'> wasn't found");
                    link = document.createElement("LINK");
                    link.rel = "next";
                    head.appendChild(link);
                }
                link.href = this.model.nextPageUrl;
            } else if (null != link) {
                link.parentNode.removeChild(link);
            }
        },

        addFavicon: function() {
            var head = document.getElementsByTagName("HEAD")[0];
            var link = document.createElement("LINK");
            link.href = "data:image/x-icon;base64," +
                "AAABAAIAEBAAAAEACABoBQAAJgAAABAQAAABACAAaAQAAI4FAAAoAAAAEAAA" +
                "ACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAABAAAAAAAAAAAAAP///wBENwQA" +
                "nq+kAKOwpACcppcAoK+jAJekmQCerp4AmaGOAJOdgQCNmHgAmKiOAJiihgCb" +
                "n38AkZiAAJGcfQCWnIIAl56CAJ2nlACgqZwApK6fAKe0qgCxwLEAcndBAEtM" +
                "EwA8KAAAKRUAACELAACanZsAoaqXAI2ZfQCMl3YAjpRyAI+cfgCVpIsAlaGL" +
                "AJ+rmwCgq5YAhY9wAEdFEwA0GwwAJQQAACYAAAAkAAAANB0jAKawkQCPm4AA" +
                "k5+AAJWbfwCao4sAl6GIAJKZegCcoYYAnaWKAGJfQQAjAAAALgIIAMnNwACf" +
                "qJQAo6qgAKKvmACepIkAmaGDAJadfQCSmoEAqK2OAGBZQQBHoT8AOkkQAFA8" +
                "NwBARB0ANZI6ACeJIgC4zMIAt8OwAKiyqQCpuasAprKkAI6TbwCTmXYAn6J+" +
                "AFRGMgB2bU8AanVLADtwIgBgiFsASoRSAFOYVABWOzwAIAUAAHFtbACkr54A" +
                "lqaRAKezqQCWnHIAmp15AJedfgCosIgAWGKHAGCKzwAtdhEAUIJ3AEZdPwBN" +
                "fyYAMFxKAKizuACbp4EAlZ16AJejgwCmrpUAmJt4AJCZbgCSmXQAmJ1qAE9s" +
                "wwBxqYgAMYw1ADmMRABQpzIARViTAC8xlQB1enIAn6N2AJ6fdQCboYAAoauR" +
                "AKGkjwCcoH0AkZd2AJ2feABkfrEAbaulAFrAqwAio1QAKqNCAEaBHQAgK6YA" +
                "tbuSAJyeewCgpogAn6SCAKWvlQClr54AsbmuALG/sgC6xbgAeo+vAHuprQAe" +
                "nSUAK58zACyXFQBFei4AOD6YAMPJrQCutqIArbWkAKatmQCot6UArrWfAK20" +
                "rQCxu7EArL62ALK/qwBfh9oAVIZbADp5AAA6eBAAQ1umAHlxkwC7xakAt8Cy" +
                "ALO6pwCxvrAAtryqAJWhhwCmr5wAs7qqAKu0pACjr6MAXXKaAFB51ACBqPAA" +
                "QWXQAB44jQCCfIAAsrifAKitlgCjsJcAqK+ZAKu0mwCeqI8AnKmTAKOtoQCq" +
                "tacAiJaAAIGBbQCAlckALk6iABcddQCJg6IAW19AAIuLdQClrJQAkpl5AKGn" +
                "iwChpYYAusKvALLArQDAzsYAm6iZAEtICQBkYlcA6P/vALS8zQDR7e8AxdnU" +
                "AExLLQAnEAAAdHNlALW/sACus5gAsLaZAKy7sQCDgWkAUU8iADkqAgBRURoA" +
                "QTQbANv38wBudb0Ag5CzAMvm8AAqHgAARjcTADUjAAA/NQ8Af3xrALvGtQBJ" +
                "RR0AWlgmAEU4FQBQTCEAPCwAALzP0wCBiscAkqS/ALPNyQAvFgAASUYVAEEs" +
                "EwBSSxoAUk0TADo1AgBwjIQAEEREAALv8PHy8/T19vf4+fr7/P3f4OHi4+Tl" +
                "5ufo6err7O3uz9DR0tPU1dbX2Nna29zd3r/AwcLDxMXGx8jJysvMzc6vsLGy" +
                "s7S1tre4ubq7vL2+n6ChoqOkpaanqKmqq6ytro+QkZKTlJWWl5iZmpucnZ5/" +
                "gIGCg4SFhoeIiYqLjI2Ob3BxcnN0dXZ3eHl6e3x9fl9gYWJjZGVmZ2hpamts" +
                "bW5PUFFSU1RVVldYWVpbXF1eP0BBQkNERUZHSElKS0xNTjM0NTY3OCssLCw5" +
                "Ojs8PT4jJCUmJygpKissLS4vMDEyExQVFhcYGRobHB0eHyAhIgMEBQYHCAkK" +
                "CwwNDg8QERIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
                "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAABAAAAAgAAAAAQAgAAAA" +
                "AABABAAAAAAAAAAAAAAAAAAAAAAAAEQ3BP9JRR3/Wlgm/0U4Ff9QTCH/PCwA" +
                "/7zP0/+Bisf/kqS//7PNyf8vFgD/SUYV/0EsE/9SSxr/Uk0T/zo1Av+su7H/" +
                "g4Fp/1FPIv85KgL/UVEa/0E0G//b9/P/bnW9/4OQs//L5vD/Kh4A/0Y3E/81" +
                "IwD/PzUP/398a/+7xrX/usKv/7LArf/Azsb/m6iZ/0tICf9kYlf/6P/v/7S8" +
                "zf/R7e//xdnU/0xLLf8nEAD/dHNl/7W/sP+us5j/sLaZ/56oj/+cqZP/o62h" +
                "/6q1p/+IloD/gYFt/4CVyf8uTqL/Fx11/4mDov9bX0D/i4t1/6WslP+SmXn/" +
                "oaeL/6Glhv+VoYf/pq+c/7O6qv+rtKT/o6+j/11ymv9QedT/gajw/0Fl0P8e" +
                "OI3/gnyA/7K4n/+orZb/o7CX/6ivmf+rtJv/rrWf/620rf+xu7H/rL62/7K/" +
                "q/9fh9r/VIZb/zp5AP86eBD/Q1um/3lxk/+7xan/t8Cy/7O6p/+xvrD/tryq" +
                "/6Wvnv+xua7/sb+y/7rFuP96j6//e6mt/x6dJf8rnzP/LJcV/0V6Lv84Ppj/" +
                "w8mt/662ov+ttaT/pq2Z/6i3pf+hpI//nKB9/5GXdv+dn3j/ZH6x/22rpf9a" +
                "wKv/IqNU/yqjQv9GgR3/ICum/7W7kv+cnnv/oKaI/5+kgv+lr5X/mJt4/5CZ" +
                "bv+SmXT/mJ1q/09sw/9xqYj/MYw1/zmMRP9QpzL/RViT/y8xlf91enL/n6N2" +
                "/56fdf+boYD/oauR/5accv+anXn/l51+/6iwiP9YYof/YIrP/y12Ef9Qgnf/" +
                "Rl0//01/Jv8wXEr/qLO4/5ungf+VnXr/l6OD/6aulf+Ok2//k5l2/5+ifv9U" +
                "RjL/dm1P/2p1S/87cCL/YIhb/0qEUv9TmFT/Vjs8/yAFAP9xbWz/pK+e/5am" +
                "kf+ns6n/maGD/5adff+SmoH/qK2O/2BZQf9HoT//OkkQ/1A8N/9ARB3/NZI6" +
                "/yeJIv+4zML/t8Ow/6iyqf+puav/prKk/5ehiP+SmXr/nKGG/52liv9iX0H/" +
                "IwAA/yYAAP8kAAD/JAAA/yQAAP8uAgj/yc3A/5+olP+jqqD/oq+Y/56kif+V" +
                "pIv/laGL/5+rm/+gq5b/hY9w/0dFE/80Gwz/JQQA/yYAAP8kAAD/NB0j/6aw" +
                "kf+Pm4D/k5+A/5Wbf/+ao4v/naeU/6CpnP+krp//p7Sq/7HAsf9yd0H/S0wT" +
                "/zwoAP8pFQD/IQsA/5qdm/+hqpf/jZl9/4yXdv+OlHL/j5x+/56vpP+jsKT/" +
                "nKaX/6Cvo/+XpJn/nq6e/5mhjv+TnYH/jZh4/5iojv+Yoob/m59//5GYgP+R" +
                "nH3/lpyC/5eegv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
                "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
            link.rel = "shortcut icon";
            link.type = "image/x-icon";
            head.appendChild(link);
        },

        addCustomCSS: function() {
            log("CPageView.addCustomCSS()");

            var cssCode =
                // Подчёрквает синим пунктиром ссылки в цитатах
                ".signature a {" +
                    "text-decoration: none !important;" +
                    "border-bottom: 1px dashed blue;}" +
                // Отменяет подчёркивание и пр. в ссылках в цитатах
                ".signature a * {" +
                    "text-decoration: none !important;}" +
                // Подчёрквает ссылки в цитатах
                ".quote a {" +
                    "text-decoration: underline !important;" +
                    "color: blue !important;}" +
                // Запрет подчёркивания ссылок в заголовках вложенных цитат
                ".quoteheader a {" +
                    "text-decoration: none !important;" +
                    "color: black !important;}" +
                // Подчёркивание ссылок в заголовках цитат при наведении на них курсора мыши
                ".quoteheader a:hover {" +
                    "text-decoration: underline !important;" +
                    "color: blue !important;}" +
                // Набор стилей для свёрнутых сообщений
                ".message_hidden_york {" +
                    "border: 1px solid #3C61A4;" +
                    "background-color: #d5d5ea; }" +
                ".message_hidden_york td {" +
                    "background-color: #e8e8ff; }" +
                ".message_hidden_york .ga_message_expert_vote_rate {" +
                    "float: right; }" +
                "";
            var styleElement = document.createElement("style");
            styleElement.type = "text/css";
            if (styleElement.styleSheet) {
                styleElement.styleSheet.cssText = cssCode;
            } else {
                styleElement.appendChild(document.createTextNode(cssCode));
            }
            document.getElementsByTagName("head")[0].appendChild(styleElement);
        },

        addScriptVersion: function() {
            log("CPageView.addScriptVersion()");

            // Нахожу BR после строки "Все права защищены..."
            // TODO дублирование кода!!!
            var xpathDivMain = "div[@class='divMain']";
            var xpathDivBottom = xpathDivMain + "/div[@class='bottom']";
            var xpath = xpathDivBottom + "/br[2]";
            var br = $x1(xpath);
            if (null == br) {
                logError("CPageView.addScriptVersion(): BR wasn't found");
                return;
            }

            var span = document.createElement("SPAN");
            var href = document.createElement("A");
            href.href = "/forum/index.php/topic,196.html";
            href.innerHTML =
                // Патч к
                "\u041f\u0430\u0442\u0447 \u043a " +
                // форуму
                "\u0444\u043e\u0440\u0443\u043c\u0443 v" + scriptVersion +
                // от
                " \u043e\u0442 " + scriptDate;
            // Отменяю все выделения для ссылок внизу страницы, так они будут
            // выглядеть как обычный текст
            href.style.fontWeight = "normal";
            href.style.color = "#666666";
            // Чтобы всё же хоть как-то выделить ссылку делаю её подчёркнутой
            href.style.textDecoration = "underline";

            span.appendChild(document.createTextNode(" "));
            span.appendChild(href);
            br.parentNode.insertBefore(span, br);
        }
    }



    // ****************************************************************************
    // ********************************** App *************************************
    // ****************************************************************************


    app = {
        controllers: new Array(),

        run: function() {
            log("app.run()");

            this.hotKeys = new Array();
            document.addEventListener("keydown", app.keyPressed, false);

            this.pageController = new CPageController();

            var isForumPage = false;
            var isTopic = false;
            if (jQueryInstalled && /myarchive/.test(url)) {
                handleArchivePage();
            } else if (jQueryInstalled && /myjournal/.test(url)) {
                handleJournalPage();
            } else if (jQueryInstalled && /index\.php.+topic[=,]/.test(url)) {
                isForumPage = true;
                isTopic = true;
                handleTopicPage();
            } else if (jQueryInstalled && /index\.php.+board[=,]/.test(url)) {
                isForumPage = true;
                handleBoardPage();
            } else if (jQueryInstalled && /\/forum\//.test(url)) {
                // TODO здесь было $id("category")
                if (null != $x1("div[@class='divMain']/div[@class='divContainer']/div[@class='content']/div[@class='text']/div[not(@class)]/table[contains(@class, 'boardTable')]")) {
                    isForumPage = true;
                    handleForumMainPage();
                } else {
                    isForumPage = true;
                    handleOtherForumPages();
                }
            } else {
                handleOtherSitePages();
            }
        },

        addController: function(controller) {
            controller.idx = app.controllers.length;
            app.controllers[controller.idx] = controller;
        },

        actionHandler: function(e) {
            log("actionHandler()");

            e = e || window.event;
            var src = e.srcelement ? e.srcelement : e.target;

            var controllerIdx = src.getAttribute("ctrl_idx");
            while (controllerIdx == null) {
                src = src.parentNode;
                if (src != null && typeof src.getAttribute == "function") {
                    controllerIdx = src.getAttribute("ctrl_idx");
                } else {
                    break;
                }
            }
            if (controllerIdx == null) {
                src = e.srcelement ? e.srcelement : e.target;
                logError("Controller index wasn't found. Event: type = " + e.type + ", src = " + src);
                return true;
            }

            var controller = app.controllers[parseInt(controllerIdx)];
            if (controller != null) {
                var res;
                var handler = src.getAttribute("ctrl_handler");
                if (handler) {
                    res = controller[handler](src, e);
                } else {
                    res = controller.handle(src, e);
                }
                if (!res) {
                    e.returnValue = false;
                    if (typeof e.preventDefault != "undefined") {
                        e.preventDefault();
                    }
                }
                return res;
            } else {
                logError("Controller wasn't found. Event: type = " + e.type + ", src = " + src + ", controllerIdx = " + controllerIdx);
                return true;
            }
        },

        addEventListener: function(element, type, controller, handler) {
            element.setAttribute("ctrl_idx", controller.idx);
            if (handler) {
                element.setAttribute("ctrl_handler", handler);
            }
            element.addEventListener(type, app.actionHandler, false);
        },

        keyPressed: function(e) {
            log("keyPressed()");

            e = e || window.event;
            var keyCode = e.which || e.keyCode;
            var hotKey = "";
            hotKey += e.altKey   ? "alt+"   : "";
            hotKey += e.ctrlKey  ? "ctrl+"  : "";
            hotKey += e.shiftKey ? "shift+" : "";
            hotKey += keyCode;
            log("Hot key: " + hotKey);

            var ctrls = app.hotKeys[hotKey];
            if (null != ctrls) {
                log("Controllers count: " + ctrls.length);
                for (var i = 0; i < ctrls.length; i++) {
                    ctrls[i].handleHotKey(hotKey);
                }
                return false;
            }

            return true;
        },

        addHotKey: function(hotKey, controller) {
            var hotKeyLower = hotKey.toLowerCase();
            var ctrls = this.hotKeys[hotKeyLower];
            if (ctrls == null) {
                ctrls = new Array();
                this.hotKeys[hotKeyLower] = ctrls;
            }
            ctrls.push(controller);
        }
    };


    // ****************************************************************************
    // ********************************* START ************************************
    // ****************************************************************************


    app.run();

    var end = new Date();
    log("END: " + end);
    log("SCRIPT TOOK " + (end.getTime() - start.getTime()) + " ms");
}



if (/^http:\/\/[^\/]*avanturist.org(\/|\?|$)/.test(url)) {
    if (isOpera) {
        // TODO оптимизация загрузки страниц архива, улучшить код, написать такую же для журнала
        if (/myarchive/.test(url)) {
            window.opera.addEventListener(
                "BeforeExternalScript",
                function (e) {
                    var src = e.element.getAttribute('src');
                    if (src.match(/ga_mycolumns\.js$|ga_myjournal\.js$/)) {
                        e.preventDefault();
                    }
                },
                false
            );
        }

        if(typeof(opera.version) == "function" && opera.version() >= 9) {
            log("Opera 9.x. Subscribing on the DOMContentLoaded event");
            document.addEventListener("DOMContentLoaded", onLoad, false);
        } else {
            log("Opera older then 9.0. Subscribing on the load event");
            document.addEventListener('load', onLoad, false);
        }
    } else {
        log("Firefox. Calling onLoad()");
        onLoad();
    }
} else {
    logError(url + " - isn't avanturist.org. Do nothing.");
}