вторник, 11 сентября 2012 г.

Blogger: replacing "Older" and "Newer" links with pages titles.

Original: this is a translation of one of the posts from this blog.

I've recently faced with a problem of replacing Bloggers "Older" and "Newer" links with their titles. Quick research showed that this is a common problem and that everybody seem to be doing it in a wrong way! Not fair. Below is the only right solution for that. Actually, this will work with any kind of links, not just Blogger, but the initial task was to make it for Blogger, so all example below are for this platform. There are several possible solutions you can think of:


Based on information from feed

Locate links, take information from the feed. To make it simple we can use jQuery or similar library (I prefer mootools). Somebody already did this: http://www.tipsntech.net/2012/06/replace-older-and-newer-post-link-with.html. I haven't tried, but I don't see a reason why it can't be working. My other widgets are using something similar and working fine, after all (related posts widget and top posts widget). Advantages: it's simple and obvious (of course if you don't have to trudge through the objects returned by the feeds). Disadvantages: you need feed (there is for Blogger, no issues). No feed no data. What does it mean? You, probably, couldn't get titles of the external pages that easily.


Loading "in place"

"Older" and "Newer" pages belong to the same blog, so the content of that pages can be easily loaded without facing same-origin policy restrictions (it will not work with external links). Examples of the solution? Lots of them. Usually using jQuery. Advantages: even more simple to implement. Disadvantages: as mentioned, it will not work with external links; it also forces browser to load 3 pages instead of 1. And what if there's more links you want to load? Users won't like it. Neither of the solutions is good enough, so I decided to write my own and do it right at this time.


Doing it right

For the cases when on a website A you need to get information from any other sites (when you need to "break" the same-origin restrictions) people invented JSONP. Long story short, you make a request to external service and that service calls your JS function on your page (with necessary parameters when applicable) and that functions does the stuff. For our task I've written a script and deployed it to several free webhostings. What it does? It gets URL, name of a callback function and id of a DOM element (you'll need that ID in many cases, trust me); then it calls that callback function passing page data into it (example). It caches results, so there are no calls to your pages every time you make a request. Callback function example you can find in graddit-extras.js. How it works:
Function gradditReplacePreviousNextBloggerLinks() takes names of the classes of the links separated by comma; these links will be replaced with corresponding pages titles. If nothing is passed then the default Blogger classes will be used ("blog-pager-newer-link" and "blog-pager-newer-link"). All the links that have given classes are passed to the script that calls our callback function gradditReplaceLinkTitleCallback for each of them.
function gradditReplacePreviousNextBloggerLinks() {
    var gradditGetElementsByClassName = function(className) {
        if (typeof document.getElementsByClassName == 'function') {
            return document.getElementsByClassName(className);
        } else {
            var allElements = document.getElementsByTagName('*');
            var foundElements = new Array();
            className = className.toLowerCase();
            for (i=0; i < allElements.length; i++) {
                var elementClasses = new Array();
                if (allElements[i].getAttribute('class')) {
                    elementClasses = allElements[i].getAttribute('class').split(' ');
                } else if (allElements[i].className) {
                    elementClasses = allElements[i].className.split(' ');
                }
                for (j=0; j < elementClasses.length; j++) {
                    if (elementClasses[j].toLowerCase() == className) {
                        foundElements.push(allElements[i]);
                    }
                }
            }
            return foundElements;
        }
    }
    var links = new Array();
    var gradditAggregateLinks = function(elements) {
        for (var i = 0; i < elements.length; i++) {
            if (elements[i].href != null) {
                var link = "";
                if (elements[i].id != null) {
                    link = [elements[i].id, elements[i].href];
                } else if (elements[i].name != null) {
                    link = [elements[i].name, elements[i].href];
                }
                if (link != "") {
                    links.push(link);
                }
            }
        }
    }
    if (arguments.length == 0) {
        gradditAggregateLinks(gradditGetElementsByClassName('blog-pager-newer-link'));
        gradditAggregateLinks(gradditGetElementsByClassName('blog-pager-older-link'));
    } else {
        for (var i = 0; i < arguments.length; i++) {
            gradditAggregateLinks(gradditGetElementsByClassName(arguments[i]));
        }
    }
    var servers = gradditGetInfoServersList();
    for (var i = 0; i < links.length; i++) {
        var server = servers[Math.floor(Math.random() * servers.length)];
        var d = document;
        var s = d.createElement('script');
        s.setAttribute('type','text/javascript');
        var src = server + "?url=" + links[i][1] + "&callback=gradditReplaceLinkTitleCallback&id=" + links[i][0];
        s.setAttribute('src', src);
        var h = d.getElementsByTagName('head').item(0);
        h.insertBefore(s, h.firstChild);
    }
}

gradditReplaceLinkTitleCallback searches links and replaces their innerHTML with the titles.
function gradditReplaceLinkTitleCallback(data, id) {
    var e = document.getElementById(id);
    if (e == null) {
        e = document.getElementsByName(id);
        if (e.length > 0) {
            e = e[0];
        } else {
            e = null;
        }
    }
    if (e != null) {
        if (data["title"] != undefined) {
            data["title"] = data["title"].replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\\\"/g, "\"");
        } else {
            data["title"] = "";
        }
        if (!data["title"].match(/^error 400 \(bad request\)/) && !data["title"].match(/^service unavailable/) && data["title"] != "") {
            var title = data["title"];
            e.innerHTML = title;
        }
    }
}
There's one more function called gradditGetInfoServersList. It returns list of servers where the script is available. To avoid overloads server is picked randomly every time. Nothing really interesting. You'd need to call gradditReplacePreviousNextBloggerLinks at the very end of your page right before </body> tag, like this:
<b:if cond="data:blog.pageType == &quot;item&quot;">
  
</b:if>
or like this (with your own links classes):
<b:if cond="data:blog.pageType == &quot;item&quot">
  
</b:if>

Each link should have an id or name attribute (Blogger links have id, no worries; if that's not Blogger you're using you shouldn't forget about it). Disadvantages: you rely on the external scripts deployed to free hostings. I'll be adding new servers (and some of them will be my own) eventually, so it wouldn't be that much of headache. Advantages: it's flexible, you can work with any kind of links: internal and external; no feeds required; none of the heavy frameworks, like jQuery, is necessary. Of course, you can combine it with jQuery to initiate the process on page load like this:
$(document).ready(gradditReplacePreviousNextBloggerLinks);

Now lets make result look nicer: add arrows and limit the width to not allow links to take too much space. I've found corresponding styles (you might need to tick "Expand Widget Templates" checkbox) and replaced them with the following:
.blog-pager-older-link, .home-link,
.blog-pager-newer-link {
  background-color: $(content.background.color);
  padding: 5px;
  max-width: 200px;
  overflow: hidden;
  display: inline-block;
  text-align: left;
}

.blog-pager-newer-link:before {
  content: "\2190\0020\0020\0020";
}

.blog-pager-older-link:after {
  content: "\0020\0020\0020\2192";
}

Result you can see here: http://fruitfulbookmarks-en.blogspot.com/2011/11/rating-widgets-for-websites-blogs-and.html (scroll to the bottom of the page). That's it! If you have questions, write a comment; found mistakes - use errors report widget - just select wrong text, press Ctrl + Enter and give proper one.

суббота, 1 сентября 2012 г.

Blogger: заменяем ссылки "Следующие" и "Предыдущие" на названия статей

Оригинал: это оригинал.

Вообщем-то, я собираюсь рассмотреть более общий случай подмены произвольной ссылки на заголовок страницы, но изначальная задача состояла именно в замене ссылок на предыдущую и следующую страницы в Blogger-е, поэтому в заголовок вынесена именно она. Существует несколько способов подменить ссылки на страницы заголовками этих самых страниц. Несколько навскидку:


На основе фида

Ищем нужные ссылки, выбираем заголовки из фида. Для упрощения решения задачи можно использовать библиотеку jQuery или подобную (я люблю mootools). Пример можно посмотреть здесь: http://www.tipsntech.net/2012/06/replace-older-and-newer-post-link-with.html. Я сам конкретно этот пример реализовать не пробовал, но причин ему не работать не вижу. В конце концов, работает же извлечение информации из фидов в моих виджетах похожих страниц и лучших статей. Достоинства: всё просто и достаточно очевидно, если, конечно, дело не доходит до необходимости пробираться через дебри объектов, возвращаемых фидом. Недостатки: должен быть фид (для Blogger-а, разумеется, есть). Нет фида, нет данных. То есть, например, получить заголовки внешних ссылок может быть уже не так просто.


Загрузка "на месте"

Ссылки "Следующие" и "Предыдущие" ведут на ваш же блог, так что содержимое страниц по этим ссылкам можно без проблем загрузить и прочитать прям тут же, не упираясь в ограничения безопасности (с внешними ссылками так не выйдет). Решение предложено тут: http://bloggerforum.ru/menyaem-ss-lki-pred-dushtie-sleduyushtie-na-nazvanie-statey-t708.html с использованием всё того же jQuery. Достоинства: ещё проще, чем метод #1. Недостатки: не будет работать с внешними ссылками, вместо одной страницы каждый раз будет загружаться три, при чём целиком. А если ссылок больше двух? Это ведёт к увеличению времени загрузки страницы для пользователя и увеличению нагрузки на сервера (да кому до этих серверов дело?). Именно эти обстоятельства побудили меня попробовать приспособить одну свою наработку для решения поставленной задачи.


Делаем это правильно

Специально для таких случаев, когда на сайте A нужно получить информацию с любого сайта (хоть с А, хоть с B), т.е. когда нужно обойти систему безопасности браузера, люди придумали JSONP. Не вдаваясь в подробности: вы делаете вызов внешнему сервису, он в ответ вызывает вашу javascript функцию, передавая в неё все нужные параметры, а вы затем обрабатываете их так, как нужно. Для нашей задачи получения заголовка страниц у меня уже давно есть скрипты, разбросанные по нескольким бесплатным хостингам. Что они делают? Они на вход получают URL страницы, название callback функции и id элемента DOM, с которым идёт работа, а в ответ вызывает callback функцию, передавая в неё данные о странице и id (пример). В скриптах реальизвано кэширование результатов, так что они не запрашивают страницы каждый раз, а берут данные из кэша. Пример callback функции есть в файле graddit-extras.js. Вот как всё работает:
Функция gradditReplacePreviousNextBloggerLinks() принимает в качестве параметров набор названий классов ссылок (через запятую), которые нужно заменить на заголовки. Если классы не заданы, то по умолчанию будут заменены ссылки с классами "blog-pager-newer-link" и "blog-pager-newer-link". Все найденные ссылки по одной передаются сервису, который для каждой из них вызовет callback функцию gradditReplaceLinkTitleCallback (присутствует в этом же файле).
function gradditReplacePreviousNextBloggerLinks() {
    var gradditGetElementsByClassName = function(className) {
        if (typeof document.getElementsByClassName == 'function') {
            return document.getElementsByClassName(className);
        } else {
            var allElements = document.getElementsByTagName('*');
            var foundElements = new Array();
            className = className.toLowerCase();
            for (i=0; i < allElements.length; i++) {
                var elementClasses = new Array();
                if (allElements[i].getAttribute('class')) {
                    elementClasses = allElements[i].getAttribute('class').split(' ');
                } else if (allElements[i].className) {
                    elementClasses = allElements[i].className.split(' ');
                }
                for (j=0; j < elementClasses.length; j++) {
                    if (elementClasses[j].toLowerCase() == className) {
                        foundElements.push(allElements[i]);
                    }
                }
            }
            return foundElements;
        }
    }
    var links = new Array();
    var gradditAggregateLinks = function(elements) {
        for (var i = 0; i < elements.length; i++) {
            if (elements[i].href != null) {
                var link = "";
                if (elements[i].id != null) {
                    link = [elements[i].id, elements[i].href];
                } else if (elements[i].name != null) {
                    link = [elements[i].name, elements[i].href];
                }
                if (link != "") {
                    links.push(link);
                }
            }
        }
    }
    if (arguments.length == 0) {
        gradditAggregateLinks(gradditGetElementsByClassName('blog-pager-newer-link'));
        gradditAggregateLinks(gradditGetElementsByClassName('blog-pager-older-link'));
    } else {
        for (var i = 0; i < arguments.length; i++) {
            gradditAggregateLinks(gradditGetElementsByClassName(arguments[i]));
        }
    }
    var servers = gradditGetInfoServersList();
    for (var i = 0; i < links.length; i++) {
        var server = servers[Math.floor(Math.random() * servers.length)];
        var d = document;
        var s = d.createElement('script');
        s.setAttribute('type','text/javascript');
        var src = server + "?url=" + links[i][1] + "&callback=gradditReplaceLinkTitleCallback&id=" + links[i][0];
        s.setAttribute('src', src);
        var h = d.getElementsByTagName('head').item(0);
        h.insertBefore(s, h.firstChild);
    }
}

Функция gradditReplaceLinkTitleCallback занимается исключительно тем, что ищет указанные ссылки и заменяет их innerHTML (какой бы он ни был) на заголовки страниц.
function gradditReplaceLinkTitleCallback(data, id) {
    var e = document.getElementById(id);
    if (e == null) {
        e = document.getElementsByName(id);
        if (e.length > 0) {
            e = e[0];
        } else {
            e = null;
        }
    }
    if (e != null) {
        if (data["title"] != undefined) {
            data["title"] = data["title"].replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\\\"/g, "\"");
        } else {
            data["title"] = "";
        }
        if (!data["title"].match(/^error 400 \(bad request\)/) && !data["title"].match(/^service unavailable/) && data["title"] != "") {
            var title = data["title"];
            e.innerHTML = title;
        }
    }
}
Есть ещё одна вспомогательная функция gradditGetInfoServersList, которая возвращает список адресов, где размещены скрипты. Чтобы распределить нагрузку, при каждом запросе сервер выбирается произвольно. Ничего особенно интересного. Вызов функции gradditReplacePreviousNextBloggerLinks желательно делать в самом конце страницы, например, перед </body> вот так:

  

или так (с вашими собственными классами, перечисленными через запятую):

  


У каждой ссылки должен быть id или name (в шаблонах Blogger уже есть id, делать ничего не нужно, но если это какие-то ваши собственные ссылки, то имейте в виду). Недостатки такого решения: вы зависите от внешних скриптов, размещенных на бесплатных хостингах. Постепенно я буду размещать скрипты на большем количестве хостингов, некоторые из них будут моими собственными, что должно увеличить надёжность решения. Достоинства подхода в том, что он универсален: вы можете заменить какие угодно ссылки, ведущие на какие угодно сайты, вы не зависите от фидов, а сами скрипты js не зависят ни от какого фрэймворка, вроде jQuery. Разумеется, вы можете ипользвать jQuery совместно с представленным методом, например, для вызова gradditReplacePreviousNextBloggerLinks после загрузки страницы:
$(document).ready(gradditReplacePreviousNextBloggerLinks);

Теперь немного украсим результат: дополним ссылки стрелками и жёстко зададим ширину, чтобы при слишком длинных заголовках ссылки не "уплывали". Я нашёл соответствующие стили в шаблоне (не забудьте включить галочку "Расширить шаблоны виджета") и заменил их на это:
.blog-pager-older-link, .home-link,
.blog-pager-newer-link {
  background-color: $(content.background.color);
  padding: 5px;
  max-width: 200px;
  overflow: hidden;
  display: inline-block;
  text-align: left;
}

.blog-pager-newer-link:before {
  content: "\2190\0020\0020\0020";
}

.blog-pager-older-link:after {
  content: "\0020\0020\0020\2192";
}

Пример конечного результата: http://fruitfulbookmarks-ru.blogspot.com/2011/11/fruitful-bookmarks.html (листайте в самый низ страницы). Вот и всё. Есть вопросы - пишите в комментариях; нашли ошибки - воспользуйтесь менеджером ошибок: просто выделите неверный текст, нажмите Ctrl + Enter и предложите верный вариант.

среда, 2 ноября 2011 г.

Эмуляция nextval для получения последовательности в MySQL

Оригинал: http://www.microshell.com/database/mysql/emulating-nextval-function-to-get-sequence-in-mysql/, автор Maresa.

        Бывают случаи, когда обычного автоинкремента становится недостаточно, и то, что вам нужно — это последовательности. К сожалению, в MySQL поддержки последовательностей нет. Кто работал с СУБД PostgreSQL, знает, что функция nextval() очень полезна. В этой статье я опишу, как средствами MySQL можно эмулировать nextval из PostgreSQL.


Чего мы хотим достичь

        В PostgreSQL вам сперва нужно создать последовательность, чтобы затем можно было обращаться к ней при помощи nextval(). Я собираюсь средствами MySQL создать nextval() с функционалом как можно более приближенным к nextval() из PostgreSQL. Итак, nextval() в PostgreSQL используется следующим образом:
-- Выполните этот код в PostgreSQL,
-- заменив sequence_name на имя вашей последовательности
SELECT nextval('sequence_name');
Если вы попытаетесь исполнить приведённый выше код в MySQL, то получите ошибку 1305, функция nextval не существует. Мы хотим заставить этот код работать в MySQL точно таким же образом, как он работает в PostgreSQL.


Структура базы данных

        Так выглядит моя база данных. Может быть хорошей идеей — разместить весь код в отдельной базе. Так лучше и сделать, если сервер целиком в вашем распоряжении, в противном же случае просто создайте таблицу и функцию внутри вашей базы.
CREATE DATABASE `sequence`;
CREATE TABLE `sequence`.`sequence_data` (
    `sequence_name` varchar(100) NOT NULL,
    `sequence_increment` int(11) unsigned NOT NULL DEFAULT 1,
    `sequence_min_value` int(11) unsigned NOT NULL DEFAULT 1,
    `sequence_max_value` bigint(20) unsigned NOT NULL DEFAULT 18446744073709551615,
    `sequence_cur_value` bigint(20) unsigned DEFAULT 1,
    `sequence_cycle` boolean NOT NULL DEFAULT FALSE,
    PRIMARY KEY (`sequence_name`)
) ENGINE=MyISAM;
        Небольшие разъяснения
        Я определяю последовательность как положительное число, способное только увеличиваться. Поэтому все колонки созданы как unsigned. Если вам нужна последовательность, которая может принимать отрицательные значения, уберите свойство unsigned.

        Я также хочу, чтобы шаг последовательности мог быть любым, не только 1. Например, для того, чтобы получать только чётные или только нечётные числа, достаточно будет установить соответствующее начальное значение и sequence_increment установить равным 2.

        Последняя колонка (sequence_cycle) — это флаг, определяющий, может ли последовательность сбрасываться в начальное значение при достижении своего максимума. Кроме этого, sequence_cur_value может принимать значение NULL, я воспользуюсь этим при переполнении, если sequence_cycle равен false (для сигнализации об ошибке).


Создаём последовательность

        Создание последовательности — это обычное добавление записи в таблицу
-- Создаём последовательность со значениями полей по умолчанию
INSERT INTO sequence.sequence_data
    (sequence_name)
VALUE
    ('sq_my_sequence')
;
-- Можно установить собственный значения
INSERT INTO sequence.sequence_data
    (sequence_name, sequence_increment, sequence_max_value)
VALUE
    ('sq_sequence_2', 10, 100)
;


Определяем nextval() в MySQL

        Теперь, когда у нас есть структура данных и создано несколько последовательностей, давайте посмотрим на определение функции nextval() в MySQL:
CREATE FUNCTION `nextval` (`seq_name` varchar(100))
    RETURNS bigint(20) NOT DETERMINISTIC
BEGIN
    DECLARE cur_val bigint(20);
    SELECT
        sequence_cur_value INTO cur_val
    FROM
        sequence.sequence_data
    WHERE
        sequence_name = seq_name
    ;
    IF cur_val IS NOT NULL THEN
        UPDATE
            sequence.sequence_data
        SET
            sequence_cur_value =
                IF (
                    (sequence_cur_value + sequence_increment) > sequence_max_value,
                    IF (
                        sequence_cycle = TRUE,
                        sequence_min_value,
                        NULL
                    ),
                    sequence_cur_value + sequence_increment
                )
        WHERE
            sequence_name = seq_name
        ;
    END IF;
    RETURN cur_val;
END;
        Если вы используете SQLyog, то для создания функции щёлкните правой кнопкой мыши на "Functions", а затем на "create function", либо так: Objects -> Functions -> Create Function…, а затем скопируйте приведённый код в открывшееся окно.


Разбор функции nextval()

        Давайте взглянем на функцию. Я определяю функцию как NOT DETERMINISTIC, потому что вызов функции с неизменными аргументами не гарантирует одинаковое возвращаемое значение. Вообще-то, она никогда не должна возвращать один и тот же результат, за исключением случаев переполнения с выставленным флагом sequence_cycle.

        После определения переменных мы получаем текущее значение последовательности из таблицы, записывая результат в переменную cur_val (строки 5 – 11). Затем, если cur_val не равен NULL (помните, колонка sequence_cur_value определена таким образом, что может принимать значение NULL), мы хотим увеличить cur_val, чтобы в следующий раз при вызове функция была готова вернуть следующее значение последовательности.

        Обновить значения sequence_cur_val не так просто. Мы должны принять во внимание следующее:
  1. Если следующее значение укладывается в пределы последовательности, мы просто увеличиваем sequence_cur_val на sequence_increment.
  2. Если же оно за разрешёнными пределами последовательности, то нужно проверить
    • Если sequence_cycle установлено (равно true), сбросить sequence_cur_val в начальное состояние sequence_min_val.
    • Если sequence_cycle равно false, установить sequence_cur_val в NULL, тем самым обозначив состояние переполнения / ошибки.
И наконец возвращаем cur_val. Заметьте, что select в строках 5 – 11 вернёт NULL, если последовательность с указанным именем не существует. Таким образом, nextval() возвращает NULL в 2 случаях (и оба сигнализируют об ошибке):
  1. Если последовательность не существует
  2. Если значение последовательности вышло за установленные пределы


Как вызывать функцию nextval() в MySQL

        Теперь, когда структуру данных определена, и сама функция готова, мы можем просто вызвать nextval(). Измените запрос соответственно вашим условиям, которые обсуждались ранее: работаете ли вы с отдельной базой или нет.
SELECT nextval('sq_my_sequence') as next_sequence;
        Вот и всё. Есть одна ловушка с этой функцией, о которой написано в следующей моей статье. Помните о репликации, когда пишете SQL. Надеюсь, статья поможет вам. Как обычно, я жду комментарии, вопросы, критику, которые помогут мне и остальным лучше разобраться в материале.


        Дополнение от переводчика
        Эта же проблема более подробно разобрана в статьях "MySQL. Генерация числовых последовательностей. Эмуляция SEQUENCE.", см. части 1 и 2. Авторы обеих статей (смотрите комментарии к оригиналу) приходят к выводу, что в реальных условиях использовать такой метод нельзя из за проблем с неатомарностью операций либо блокированием транзакций.