jsiedit: идея и создание удобного подключаемого WYSIWYM редактора с примером для Хабрахабра

Введение


В статье описываю подход к созданию удобного инструмента на Javascript для онлайн-редактирования текстов. В качестве примера создал прототип для редактирования статей на Хабре (описан ниже). С его помощью сейчас и вношу изменения в данную статью.

Передо мной встала задача выбора онлайн-редактора для текстов на сайте. Самым очевидным решением оказался бы один из WYSIWYG редакторов. Но этот вариант мне не понравился по нескольким причинам. Во-первых, многие уязвимости популярных CMS систем связаны именно с WYSIWYG редакторами. Во-вторых, после публикации текст часто будет отличаться от того, что было в редакторе. В-третьих, подобные редакторы сложно расширить для поддержки новых тэгов и элементов. Поэтому остановился на WYSIWYM редакторе.



Одновременно с выбором WYSIWYM редактора встал вопрос с выбором языка разметки. Стоит ли использовать Wiki или Markdown синтаксис, может быть TeX-подобный язык или даже непосредственно HTML, а для некоторых задач, возможно, будет достаточно и bbCode? После некоторых размышлений пришёл к выводу, что хранится данные могут в любом формате, но с обязательным чётким разделением содержимого и атрибутов. Это будет гарантировать, что даже изменение алгоритмов отображения не исказит информацию. Что же касается редактирования, то пользователю можно дать возможность изменения данных удобным ему способом.

Проблема


К существующим реализациям онлайн-редакторов у меня есть огромная претензия. Они неудобны, поскольку представление отделено от кода. Конечно, здесь стоит возразить, что это и есть основа WYSIWYM. Всё верно, но давайте рассмотрим конкретную ситуацию.

Предположим, что пишите статью на Хабре или ответ на форуме. Синтаксис соответствующих тегов знаком, поэтому проблем с вводом текста не возникает. Если нужно вставить изображение или иным образом выделить элемент, то можно выбрать подходящий тэг на панели инструментов или ввести вручную. Первый вариант текста готов, но для проверки форматирования перед отправкой потребуется нажать кнопку «Предпросмотр». И вот только здесь появляется не только исходный текст со всеми тэгами, но и его конкретное представление.

В ходе просмотра уже отформатированного текста находите опечатку или что-то хотите исправить и дополнить. Тут возникает проблема. Ошибочное место уже найдено в области предварительного просмотра, но для исправления необходимо вернуться в редактор, где среди множества тэгов найти тот фрагмент, который предстоит исправить.

Эта проблема хорошо заметна в wiki или со страницами CMS, когда при попытке немного скорректировать какое-нибудь предложение приходится редактировать весь документ. И чем больше документ, тем сложнее пользователю. Он уже нашёл место, которое следует исправить в наглядном представлении, но ему надо повторно пройти весь этот путь поиска, но уже в исходном коде.

Решение


Естественной выглядит возможность динамического подключения редактора для выделенного пользователем фрагмента. Нашёл и посмотрел на следующие реализации: Jeditable, jquery-in-place-editor, jQuery Plugin: In-Line Text Edit, Ajax.InPlaceEditor, EditableGrid, InlineEdit 3. Недостаток у всех один и тот же: они позволяют загрузить редактор лишь для отдельного элемента, поскольку не поддерживают форматы. Поэтому решено было сделать редактор самому и «изобрести велосипед».

Так появился jsiedit.

В ходе написания статьи наткнулся на Redactor, который позволяет включить редактор для всего текста, но там опять блок задаётся заранее, а ещё это WYSIWYG редактор. Наиболее близок к моей идее оказывается Redactor Air mode, но это лишь форматирование.

Цели и задачи jsiedit

На начальном этапе реализации были определены следующие ключевые требования:
  • Пользователь должен иметь возможность редактировать отформатированный текст;
  • Объём редактируемого блока должен определяться самим пользователем.


Реализация

Итак, необходимо было решить две задачи. Начнём с возможности редактирования форматированного текста. Возьмём любой набор тэгов, например, bbCode. Предположим, что изначально документ создавался именно в этом формате и на сервере хранится текст именно с bbCode тэгами. При отображении страницы пользователю на сервере происходит преобразование bbCode тэгов в соответствующие конструкции HTML.
Теперь пользователь хочет отредактировать часть уже отформатированного текста. Получается, что нам надо получить исходные bbCode тэги для выбранного фрагмента. Тут возможно два подхода. Во-первых, можно динамически (на клиенте) преобразовать HTML код в соответствующий bbCode текст. Во-вторых, можно заранее сохранить информацию о bbCode тэгах в атрибутах HTML тэгов:
<p>Обычный текст, но с <b data-bbCode="b">выделением полужирным</b> шрифтом.</p>

Ещё можно рассмотреть вариант, когда у сервера динамически запрашиваются bbCode для конкретных элементов, но это потребует уже более серьёзной доработки на стороне сервера.
При проектировании я не стал выбирать один из этих трёх вариантов, а решил воспользоваться callback функцией, чтобы разработчик сам решил, каким образом HTML представление должно быть преобразовано в необходимый формат. В примерах использовал динамическое преобразование HTML.

После завершения ввода необходимо выполнить обратное преобразование и сохранить изменения на сервере. В данном случае опять было решено воспользоваться callback функцией и дать возможность разработчику самому решить, что следует делать.

Выделение текста

Теперь рассмотрим вторую задачу — предоставление возможности пользователю самому выбрать, какой фрагмент текста он хочет отредактировать. По поводу выделения мышью есть хорошая статья Range, TextRange и Selection, поэтому сами объекты и функции Javascript описывать не буду.

Остаётся вопрос удобства со стороны пользователя. Представим, что я мышью выделил пару слов в предложении и запустил редактор. Что именно я хотел отредактировать: только эти два слова, всё предложение или весь параграф? А если я выделил слова не полностью, а лишь по несколько букв? В данном случае считаю, что следует давать возможность отредактировать весь параграф, то есть объемлющий тэг. Но ближайшим объемлющим тэгом может оказаться не <p>, а другой, например, <b>:
<p>Немного текста с <b>внутренним (!) выделением</b>.</p>

Если мы выделим "(!)", то следует отобразить редактор лишь для «внутренним (!) выделением» или для всего параграфа? Считаю, что здесь пользователю необходимо дать возможность отредактировать весь абзац, но для гибкости было решено включить callback функцию, которая для каждого DOM элемента будет сообщать, возможна ли активация редактора. Подобная функция может быть реализована примерно так:
function jsiedit_fn_sample_tag_check(elem)
{
    switch (elem.tagName)
    {
        case 'P':
        case 'DIV':
        case 'SPAN':
            return true;
    }
    return false;
}


В результате получается следующий алгоритм определения выделенного диапазона:
elem = range.commonAncestorContainer; // получим предка для выделения

while (elem && !fn_is_valid_for_edit(elem)) // используем callback функцию
{
  elem = elem.parentNode; // перейдём к предку
}


Всё правильно, но тут нас может поджидать ловушка. Давайте рассмотрим следующий пример:
<div>... здесь идёт много текста ...

<p>Это первый параграф и пользователь начинает выделение, например, отсюда. Параграф заканчивается,</p>

<p>но выделение продолжается и <b>завершается лишь здесь.</b> В результате у нас выделено несколько абзацев.</p>

... а тут опять идёт много текста ...</div>

Если мы просто возьмём commonAncestorContainer, то получим <div> и отдадим пользователю на редактирование весь текст. С другой стороны, пользователь, скорее всего, хочет отредактировать лишь выделенные два параграфа. В этом случае нам надо расширить каждое выделение до полного охвата тэгов <p> и остановиться.
У объекта Range есть подходящие свойства: startContainer и endContainer. Но тут надо выровнять контейнеры до одного уровня, чтобы они оказались братьями. Получился следующий код:

var prnt = rng.commonAncestorContainer; // Это ближайший общий предок
var sc = rng.startContainer; // Это "начало" выделения
var ec = rng.endContainer; // Это "конец" выделения

if ((sc == prnt) || (ec == prnt)) // Один из граничных контейнеров является общим для всего выделения
{
    sc = prnt;
    ec = prnt;
}
else // Будем искать пока не станут братьями
{
  while (sc.parentNode != prnt)
  {
    sc = sc.parentNode;
  }
  
  while (ec.parentNode != prnt)
  {
    ec = ec.parentNode;
  }
}

К этому коду надо добавить ещё предыдущий код по проверке возможности редактирования конкретного блока. Воспользуемся методами setStartBefore и setEndAfter для создания диапазона:
    var rng_new = document.createRange();
    rng_new.setStartBefore(sc);
    rng_new.setEndAfter(ec);


На этом задача определения выделенного блока завершается.

Отображение редактора

Следующим шагом стало отображение редактора. Он может отображаться в отдельном окне, быть зафиксирован на исходной странице, но меня интересовал вариант, когда редактор появляется на месте самого редактируемого текста. Вначале задача показалась совсем простой и был написан следующий код:
    var tarea = document.createElement("textarea"); // Создаём область для редактирования
    tarea.value = fn_get_text_for_range(rng_new); // Получим текст для редактирования
    tarea.style.width = rng_new.startContainer.clientWidth; // Установим ширину редактора
    rng_new.startContainer.parentNode.insertBefore(tarea, rng_new.startContainer); // Добавляем текстовый редактор перед выделенной областью
    rng_new.deleteContents(); // Удалим редактируемый текст из представления

При выполнении этого кода реальность сильно отличалась от прогнозируемого результата. Причиной стало то, что атрибут startContainer оказался «предком» для моего выделения. Могу объяснить это тем, что новый диапазон начинается до выделенного блока. В результате решил воспользоваться вычисленными ранее переменными sc и ec.
Следующая неожиданность была связана с попыткой добавить объект до диапазона. На практике получалось, что добавляемый объект попадал в диапазон и уничтожался следующей строчкой. Чтобы избежать этого область для редактирования стал создавать после диапазона. Дополнительно решил приблизительно определять высоту для редактора. Получился следующий код:
    var tarea = document.createElement("textarea");  // Создаём область для редактирования
    tarea.value = fn_get_text_for_range(rng_new); // Получим текст для редактирования
    tarea.style.width = ec.clientWidth + 'px'; // Установим ширину редактора
    if (sc == ec)
    {
        tarea.style.height = Math.min(document.body.clientHeight / 2, sc.clientHeight) + 'px'; // Зададим высоту редактора
    }
    else
    {
        tarea.style.height = Math.min(document.body.clientHeight / 2, sc.clientHeight + ec.clientHeight) + 'px'; // Зададим высоту редактора
    }

    ec.parentNode.insertBefore(tarea, ec.nextSibling); // Добавим редактор после области
    rng_new.deleteContents(); // Удалим редактируемый текст из представления

Для возможности сохранения результатов и отмены редактирования к textarea была добавлена пара кнопок.

Последним вопросом стал вызов редактора. Редактор может активироваться, например, нажатием на определённую кнопку на странице, из контекстного меню, автоматически при выделении текста с нажатым Alt и т.п. Решил добавить наиболее простой метод — это отображение кнопки рядом с местом прекращения выделения.

Для этого потребовалось добавить обработчик события mouseup. Положение для вывода кнопки определял по атрибутам pageX и pageY. Получилось примерно следующее:
function jsiedit_mouseup(event)
{
    var btn = document.createElement("input");
    btn.type = "button";
    btn.value = "Edit";
    btn.style.position = 'absolute';
    btn.style.top      = event.pageY + 'px';
    btn.style.left     = event.pageX + 'px';
    btn.onclick = fn_start_editor;
    document.body.appendChild(btn);
}

function jsiedit_onload()
{
  document.body.addEventListener("mouseup", jsiedit_mouseup, false);
}

document.addEventListener("DOMContentLoaded", jsiedit_onload, false);

Данный код работает недостаточно корректно, поскольку создаёт по новой кнопке «Edit» при каждом отпускании кнопки мыши. Для корректировки достаточно в глобальной переменной сохранить текущее состояние и менять его в зависимости от действий пользователя.

На этом первая версия jsiedit была готова.

Редактор для Хабрахабра

В качестве демонстрации возможностей решено было создать прототип для Хабра. Необходимость в подобном редакторе ощутил при написании уже первой статьи, поскольку она получалась большой, а без предварительного просмотра искать ошибки оказалось сложно и неудобно. Ситуацию усугублял ещё запрет на изменение размеров формы ввода. В результате первоначальный вариант текста написал в блокноте. Тут можно прочитать о запуске примера, чтобы попробовать самому.

Создаваемый редактор должен был позволять редактировать любой текст в области предварительного просмотра, а после сохранения изменять как отредактированный текст, так и его исходник в поле ввода. В качестве запуска предполагалось использовать букмарклет. Собственно, именно так это всё уже и работает.

Рассмотрим проблемы, с которыми столкнулся в ходе создания редактора.

Отсутствие параграфов

Первой сложностью оказался тот факт, что в текстах Хабра отсутствуют параграфы. Вместо них при сохранении используются просто разрывы строк <br />. В результате редактируемый блок должен ограничиваться некоторыми тэгами-границами. Давайте посмотрим на следующий сгенерированный HTML код:
Начало статьи
<br>
<h4>Заголовок</h4><br>
Один из параграфов, но он не выделен в блок "P".<br>
<br>
Другой параграф, внутри которого есть <b>дополнительные <i>вложенные</i> тэги</b>. Завершается опять через тэг "BR"<br>
потом начинается следующий абзац.<br>
<br>
Продолжение статьи

Весь текст находится в одном DIV, поэтому предыдущий алгоритм вернёт на редактирование весь текст. В данном случае нам необходимо «вырезать» блок между двумя ближайшими BR тэгами. Для этого можем двигаться по свойствам previousSibling и nextSibling.

Самым сложным на данном этапе оказался выбор подхода к определению функций по проверке узлов. В итоге было принято решение, что функция будет выдавать массив логических свойств для данного узла:
  1. Данный узел может быть самостоятельно выбран.
  2. Сам узел должен быть включён в выборку.
  3. Данный узел может быть общим предком для выбора.
  4. Данный узел может быть ограничивающим узлом при выборе братьев.

Получилась следующая функция проверки:
function jsiedit_fn_sample_check_node(node)
{
    switch (node.tagName)
    {
        case 'BR':
            return [false, false, false, true];
        case 'P':
            return [true, true, false, true];
        case 'DIV':
        case 'SPAN':
            return [true, false, true, true];
    }
    return false;
}


Если учитывать специфику предварительного просмотра на Хабре, то получим такую функцию:
function jsiedit_fn_sample_habr_check_node(node)
{
    switch (node.tagName)
    {
        case 'BR':
            return [false, false, false, true];
        case 'DIV':
            if (node.className == 'content html_format')
                return [true, false, true, true];
    }
    return false;
}


С учётом новой функции был переписан блок определения границ выбранных данных:
function jsiedit_get_bounds
function jsiedit_get_bounds(fn_check_node)
{
    var sel = window.getSelection();	// Получим выделенный блок
    
    if (!(typeof sel === 'undefined'))	// Проверим, что что-то было выделено
    {
        if (sel.rangeCount == 1)	// Нас не интересуют множественные выделения
        {
            var rng = sel.getRangeAt(0);	// Получим range выделенного блока
            var prnt = rng.commonAncestorContainer; // Это ближайший общий предок
            var sc = rng.startContainer; // Это "начало" выделения
            var ec = rng.endContainer; // Это "конец" выделения

            if ((prnt.tagName == 'DIV') || (prnt.tagName == 'SPAN'))
            {
                if (prnt == sc)
                    sc = prnt.childNodes.item(rng.startOffset);
                if (prnt == ec)
                    ec = prnt.childNodes.item(rng.endOffset);
            }			
            
            var chk = fn_check_node(prnt);
            var include_bounds = [true, true];	// Следует ли включить границы
            
            if (chk && chk[2] && (sc != prnt) && (ec != prnt))	//	Надо поднять границы до уровня предка, будем искать пока не станут братьями
            {
                while (sc.parentNode != prnt)
                {
                    sc = sc.parentNode;
                }

                while (ec.parentNode != prnt)
                {
                    ec = ec.parentNode;
                }
            }
            else if (chk && chk[0])	// Сам предок может быть включен, добавляем
            {
                return [prnt, chk[1]];
            }
            else	// Необходимо найти предка, которого получится включить
            {
                while (prnt.parentNode)
                {
                    chk = fn_check_node(prnt.parentNode);
                    if (chk && chk[2])
                    {
                        sc = prnt;
                        ec = prnt;
                        prnt = prnt.parentNode;
                        break;
                    }
                    else if (chk && chk[0])
                    {
                        return [prnt.parentNode, chk[1]];
                    }
                    prnt = prnt.parentNode;
                    if (!prnt.parentNode)
                        return false;
                }				
            }
            
            chk = fn_check_node(sc);
            if (chk && chk[0])	// Узел может быть выбран самостоятельно
            {
            }
            else
            {
                while (sc.previousSibling)	//	Есть "младший" брат
                {
                    sc = sc.previousSibling;
                    chk = fn_check_node(sc);
                    
                    if (chk && chk[3])
                    {
                        include_bounds[0] = chk[1];	// Надо ли включать сам объект
                        break;
                    }
                }
            }
            
            chk = fn_check_node(ec);
            if (chk && chk[0])	// Узел может быть выбран самостоятельно
            {
            }
            else
            {
                while (ec.nextSibling)	//	Есть "младший" брат
                {
                    ec = ec.nextSibling;
                    chk = fn_check_node(ec);
                    
                    if (chk && chk[3])
                    {
                        include_bounds[1] = chk[1];	// Надо ли включать сам объект
                        break;
                    }
                }
            }
            
            return [sc, ec, include_bounds[0], include_bounds[1]];
        }
    }
    return false;
}

В ходе проверки первой версии jsiedit иногда вместо выделенного фрагмента в редакторе оказывался весь текст. Причина этого оказалась в том, что при начале выделения на пустоте система в качестве startContainer возвращала предка, а в startOffset сохранялся дочерний элемент, с которого идёт выделение. Аналогичная ситуация и с окончанием. Поэтому пришлось воспользоваться следующим кодом:
if ((prnt.tagName == 'DIV') || (prnt.tagName == 'SPAN'))
{
    if (prnt == sc)
        sc = prnt.childNodes.item(rng.startOffset);
    if (prnt == ec)
        ec = prnt.childNodes.item(rng.endOffset);
}

Возможно, что правильнее было бы выполнять проверку по типу тэга, но для прототипа меня устроила и эта проверка.

Преобразование текста

Для работы редактора потребовались две функции преобразования. Первая должна по выделенному на странице фрагменту сформировать код на языке разметки Хабра. Это довольно простая часть, поскольку можно выполнить обход DOM и преобразовывать тэги отдельно.

Обратное преобразование можно выполнить с помощью уже существующего на Хабрахабре механизма предварительного просмотра — отправить скорректированный текст на сервер и получить обратно уже HTML код. Но я решил выполнить это преобразование на стороне клиента. Первоначально попытался найти готовый HTML парсер на Javascript. К сожалению, найденные реализации меня не устроили. Тогда понял, что потребуется писать парсер с нуля и изобретать очередной велосипед. Поскольку дело это не быстрое, то решено было отложить парсер для отдельных статей, а для прототипа найти хотя бы временное решение. В качестве самого простого подхода решено было просто добавить преобразование для отличных от стандарных тэгов. Получилась следующая функция:
jsiedit_fn_sample_habr_produce = function(src)
{
    var rep = [
        [/<source\s+lang=/gi, '<pre><code class='],
        [/<\/source>/gi, '</code></pre>'],
        [/\n/g, '<br>'],
        [/<hh\s+user=['"]([^'"]+)['"]\s*\/>/gi, '<a href="http://habrahabr.ru/users/$1/" class="user_link">$1</a>'],
        [/<spoiler\s+title=['"]([^'"]+)['"]>/gi, '<div class="spoiler"><b class="spoiler_title">$1<\/b><div class="spoiler_text">'],
        [/<\/spoiler>/gi, '</div></div>']
    ];

    var str = src;
    var i;
    for (i = 0; i < rep.length; i++ )
    {
        str = str.replace(rep[i][0], rep[i][1]);
    }
    return str;
};

Вот с этой функцией потом пришлось подробно разбираться. Появилась проблема, связанная с кодами программ — тэгом <source>. Все входящие в него тэги не должны интерпретироваться, но они должны интерпретироваться вне этих блоков. Из-за этого форматирование стало «ломаться».
Одним из работающих решений оказалась автоматическая замена всех символов "<" на "&lt;" при получении кода. Хотя это и сработало, но получающийся в редакторе код оказался слишком некрасивым. В результате приступил к доработке функции. Алгоритм выбрал следующий: найти все куски кода, которые включают код, после чего заменить внутри них все критические символы. Получилось следующее преобразование:
jsiedit_fn_sample_habr_produce = function(src)
{
    var fn_source = function(s)
    {
        var res = s.match(/^<source\s+lang=([^>]*)>([\s\S]*)<\/source>$/i);
        if (res)
            return '<pre><code class=' + res[1] + '>' + res[2].replace(/</g,'<').replace(/>/g,'>') + '</code></pre>';
        res = s.match(/^<source>([\s\S]*)<\/source>$/i);
        if (res)
            return '<pre><code>' + res[1].replace(/</g,'<').replace(/>/g,'>') + '</code></pre>';
        
        res = s.match(/^<pre>([\s\S]*)<\/pre>$/i);
        if (res)
            return '<pre>' + res[1].replace(/</g,'<').replace(/>/g,'>') + '</pre>';
        return s.replace(/</g,'<').replace(/>/g,'>');
    };

    var rep = [
        [/(<source\s([\s\S])*?<\/source>)|(<source>([\s\S])*?<\/source>)|(<pre>([\s\S])*?<\/pre>)/gi, fn_source],
        [/<anchor>([^<]*)<\/anchor>/gi, '<a name="$1"></a>'],
        [/\n/g, '<br>'],
        [/<hh\s+user=['"]([^'"]+)['"]\s*\/>/gi, '<a href="http://habrahabr.ru/users/$1/" class="user_link">$1</a>'],
        [/<spoiler\s+title=['"]([^'"]+)['"]>/gi, '<div class="spoiler"><b class="spoiler_title">$1<\/b><div class="spoiler_text">'],
        [/<\/spoiler>/gi, '</div></div>']
    ];

    var str = src;
    var i;
    for (i = 0; i < rep.length; i++ )
    {
        str = str.replace(rep[i][0], rep[i][1]);
    }
    return str;
};

Вероятно, что данный код можно сделать намного симпатичнее, но сейчас редактор для Хабра создавался лишь как некоторый прототип — обоснование идеи. По мере необходимости функциональность можно улучшать и добавлять дополнительные тэги. Думаю, что регулярных выражений вполне хватит. У идеи реализации парсера приоритет понизил.

Заключение


Подводя итог, хочу подчеркнуть, что целью данной статьи было описание идеи по созданию javascript in-place WYSIWYM редактора и конкретного подхода к реализации. Буду рад, если подобный редактор заинтересует ещё кого-нибудь. Напишите, пожалуйста, ваше мнение. Может быть уже существуют более интересные решения?

Вопросы и ответы

В1: Почему не используется библиотека xxxxxxx? Почему обрабатываются не все ошибки? Почему код не работает в браузере yyyyyyy?
О1: Я постарался изложить идею in-place WYSIWYM редактора. Прототип — это proof of concept, который работает в FF18, где сейчас и пишу данные строки. Пока я был единственным заинтересованным потребителем. Если вам интересно развитие данной библиотеки, то напишите. Приведённой информации достаточно, чтобы обеспечить работу в требуемых браузерах, подключить нужную библиотеку или фреймворк.
Дополнение: Скрипт скорректирован для работы в браузерах на основе WebKit.

В2: Почему букмарклет для Хабра не выделяет синтаксис при сохранении? Почему поддерживаются не все тэги?
О2: Если будет интерес, то это всё можно реализовать. Из оставшихся тэгов в первую очередь реализовал бы <video>.

В3: Какие ближайшие планы?
О3: Очень сильно будут зависеть от вашей реакции. Во-первых, надо добавить поддержку редактирования статей (сейчас не могу проверить), а не только создание новых. (уже добавлено) Во-вторых, устранение известных ошибок и расширение списка поддерживаемых тэгов. Потом, вероятнее всего, начну с Add-On для FF, чтобы избавиться от букмарклета.

В4: Где можно найти исходные тексты?
О4: Всё находится на странице проекта на github: http://praestans.github.com/jsiedit/.

В5: Как использовать jsiedit для Хабрахабра?
О5: Запустить проще всего как букмарклет (что это такое). При создании новой темы надо сделать предварительный просмотр, после чего в области предварительного просмотра будет активироваться редактор при выделении мышью. При сохранении все данные из области предварительного просмотра будут переноситься в окно ввода.

Здесь изображение с подробной инструкцией.

Это ссылка на букмарклет (её надо добавлять в закладки). Вот сам отформатированный текст букмарклета:
javascript: (function ()
{
 var a = document.createElement('script');
 a.type = 'text/javascript';
 a.src = 'http://praestans.github.com/jsiedit/lib/habr_bmk.js';
 document.getElementsByTagName('head')[0].appendChild(a); 
})(); 


Предупреждение: сейчас поддерживаются лишь следующие тэги: A, ANCHOR, B, BLOCKQUOTE, BR, EM, H1, H2, H3, H4, H5, H6, HABRACUT, HH, HR, I, IMG, LI, OL, SOURCE, STRIKE, STRONG, SUB, SUP, TABLE, TD, TH, TR, U, UL. При использовании очередного (нового для себя) тэга с помощью предварительного просмотра проверяйте, что данный тэг корректно интерпретируется. Для некоторых вариантов использования может быть получено некорректное форматирование, а текст соответствующего элемента пропадёт.

В6: Что ещё необходимо знать?
О6: В настоящий момент есть несколько известных мне ошибок и особенностей работы:
1. При сохранении списков между происходит добавление <br> между <li> блоками, поэтому список начинает «разъезжаться». Если новый тэг <li> будет на строке окончания предыдущего, то лишних пропусков не будет.
2. После сохранения последнего параграфа, иногда, он меняется местами с предпоследним. Предполагаю, здесь необходимо разбираться с функциями работы с диапазонами и вставки в DOM.
3. Если выделить большой блок текста, то этот блок будет скрыт, а редактор окажется в самом начале блока и необходимо будет выполнить прокрутку вверх. Необходимо добавить функцию, которая обеспечивала бы видимость редактора на экране при начале редактирования.
4. Одной из особенностей реализации является то, что программа сама генерирует исходный текст разметки на основе HTML текста предварительного просмотра. Поскольку исходный авторский код недоступен, то все тэги оказываются представлены однообразно, в текущей версии — прописными буквами.
5. Как следствие того, что программа выполняет преобразование введённого текста в html, а потом обратно в код хабрахабра, при ошибках преобразования невозможно вернуть текст обратно для исправления даже минимальных ошибок. Если в «испорченный» код попали блоки <source>, то угловые скобки "<" и ">" окажутся заменёнными на "&lt;" и "&gt;". Но после их исправления у тэгов <source> и </source> всё должно стать опять корректно.
6. При редактировании статьи пропадает текст у тэга <habracut>, поскольку его нет в предварительном просмотре.

Дополнение 1: Добавлена возможность редактирования существующих статей, а не только создание новых.
Также исправлена ошибка, из-за которых скрипт не работал в браузерах на основе WebKit. Спасибо Leksat и tkf. Причина была в том, что для меня естественным было задать для параметра функции значение по умолчанию. Firefox всё принял, а вот chrome следующую конструкцию не понимает:
function(param1, param2=default_value)
Поделиться публикацией

Похожие публикации

Комментарии 19

    –3
    Что-то ваш скрипт в Хроме не работает.
    Ubuntu 12.04, Chrome 24.0.1312.52
      +2
      Ответ был приведён в подразделе «Вопросы и ответы» в первом же вопросе.
        +2
        Когда ж я уже научусь сначала читать
          +5
          Дабы окончательно искупить свою невнимательность, вот вам совет как заставить скрипт работать в хроме.
          Уберите дефолтное значение аргумента в первой строке. В JS они не предусмотрены.

          Было:
          jsiedit_fn_sample_habr_parse = function(elem, keep_format = false)
          

          Стало:
          jsiedit_fn_sample_habr_parse = function(elem, keep_format)
          
            +3
            Я добавлю, чтобы в js присваивать какие-то значения по-умолчанию для функций, достачтоно написать:
            paramN = paramN || "default value";
            

            первыми строчками в функции. Не бойтесь, JS не будет ругаться, если передается недостаточно параметров. ваш код превратиться в такой:
            jsiedit_fn_sample_habr_parse = function(elem, keep_format)
            {
            	keep_format = keep_format || false;
            	…
            }
            
        0
        > Ситуацию усугублял ещё запрет на изменение размеров формы ввода.
        Это — потому что в коде сайта в all.css, строка 62, стоит:
        textarea {
            resize: none;
        }
        Это легко исправляется 1 строчкой юзерстилей,
        textarea {
            resize: vertical !important;
        }

        Такое давно работает, например, в ZenComment, потому что неудобства ощущались всегда и давно, а на сайте не спешат исправлять. В результате, с юзерстилем любые поля ввода могут растягиваться по вертикали.

        А в скрипте HabrAjax в июле 2012 немного похимичено, и добавлен авторост полей ввода (статья на Хабре), после чего забыта даже необходимость растягивания любого из полей ввода на Хабре.

        А по теме, получившийся редактор нельзя называть WYSIWYM — это просто редактор фрагментов с предпросмотром. Что, конечно, не умаляет его достижения. Как и редактор Хабра с предпросмотром — не WYSIWYG, хотя предпросмотр — в HTML. Смотрим определение — ru.wikipedia.org/wiki/WYSIWYM «WYSIWYM — сокращение от англоязычного What You See Is What You Mean (То, что ты видишь, есть то, что ты имеешь в виду)». От WYSIWYG отличается тем, что «G» ориентирован на одно представление текста, конкретную ширину, шрифт, а «M» кодируется более абстрактно, через теги. Но просмотр может вполне быть и в формате WYSIWYG, как это видно на демо одного распространённого редактора.

        Почему-то ещё был упомянут Markdown, но не реализован. Идея ввода текстов на Хабр через вики-разметку высказывалась в статье о последнем обновлении Хабра. Ей дали 9 плюсов — habrahabr.ru/company/tm/blog/116840/#comment_5641111 (а латексу — 13). Поэтому, поддержка найдётся. Другое дело, что она может оказаться пассивной. Вот, для себя не вижу необходимости в разметке, мне полностью хватает кнопок над полем ввода, которые модифицируются скриптом HabrAjax (есть спойлер, Font, иной ввод Script и т.д.). И всё, для Хабра это покрывает 100% потребностей. Для других мест — скорее, может пригодиться; говорят, что на Западе Markdown более распространён.

        Далее, что сказать по реализации? Мы получили редактор фрагментов, в котором приходится вручную вводить теги.

        *) было бы гораздо удобнее увидеть над полем ввода те же самые кнопочки, к которым привыкли в полях ввода комментариев или статей;
        *) потом создаётся запрос на обновление, всё передёргивается, прокручивается наверх — часть удобств теряется, к которым стремились. Хорошо бы отключить встроенную на сайте прокрутку;
        *) даже без прокрутки получили сильно завязанный на коды сайта редактор. Без этого никуда, но для поддержки скрипта это тоже неудобство;
        *) использование собственного предпросмотра — выдаёт не совсем аутентичный вид фрагмента, потому что не пропускается через сервер. Альтернатива — только задействовать предпросмотр сайта, потом поймать коллбек этого предпросмотра — ещё большее залезание в код. Тогда без проблем увидим не только фичи, но и баги генератора страниц (например, <img src="..." align="center/>).

        Всё это, конечно, неизбежно — или залезать в код сайта, или пользоваться чем-то попроще. Например, при правке статей я приспособился использовать Ctrl-C — Ctrl-F — Ctrl-V — Enter — Enter, чтобы попасть на участок поля ввода с тем же фрагментом. Если учесть что скриптом поле ввода у меня разворачивается на 80% высоты окна, оказывается достаточно удобно и без головной боли поддержки и правки чужого кода.

        Однако, в HabrAjax есть многое для того, чтобы собрать редактор фрагментов с кнопками, с контекстной кнопкой по выделению, которая там уже есть и заготовлена для отметки ошибок, и выполняет ещё несколько ролей. Более того, там даже поле ввода для редактирования выделенного фрагмента уже есть (см. эту страницу, там иллюстрации: spmbt.kodingen.com/habrahabr/habrAjax/habraQuotes-support.htm ). Если бы я делал его, то на основе HabrAjax. Если будет нужно, могу помочь прикрутить его в виде плагина, который вызывается контекстной кнопкой из установленного HabrAjax.
        • НЛО прилетело и опубликовало эту надпись здесь
            +2
            Вы преувеличиваете. Я упоминаю свои скрипты только по делу. И сожалею, что они у Вас вызывают отрицательные эмоции.

            Кстати, запиливаю сейчас отличную фичу — просмотр картинок.
            0
            Что касается понятий WYSIWYM и WYSIWYG, то моё мнение следующее:
            а) Если говорить про внутреннее представление, то у WYSIWYM оно непосредственно используется (или может использоваться) для редактирования. У WYSIWYG оно носит технический характер.
            б) Если говорить про редакторы, то WYSIWYG позволяет редактировать в представлении максимально близком к итовому. У WYSIWYM обычно текстовый вид.

            Вы можете использовать WYSIWYG редактор для создания HTML страниц, но при необходимости можно открыть и plaintext редактор для изменения HTML уже как WYSIWYM. Хотя может показаться, что в HTML всё жёстко задано, но возможность перекрыть любые стили своими (например, через внешний CSS файл) приводит к тому, что даже WYSIWYG редактор не обеспечивает точности представления.

            Почему-то ещё был упомянут Markdown, но не реализован.
            Далее, что сказать по реализации? Мы получили редактор фрагментов, в котором приходится вручную вводить теги.
            Реализация — это лишь proof-of-concept идеи. Суть идеи в возможности видеть итоговое представление и точечно изменять отдельные фрагменты с помощью подходящего языка разметки.

            Подскажите, если знаете готовый WYSIWYM редактор фрагментов, который позволяет пользователю самому выбирать фрагменты для редактирования. Мне найти не удалось.
              0
              > У WYSIWYM обычно текстовый вид.

              По демо Wymeditor (1) я этого не заметил. Может быть, мало искал, но сложилось впечатление, что стараются в таких редакторах делать WYSIWYG-оболочку.
              (Может быть, сделать 2 таба (HTML — VIEW) и кнопку Save рядом?)
              > Подскажите, если знаете готовый WYSIWYM… выбирать фрагменты для редактирования

              Не знаю, и не искал таких. Ведь редактировани фрагмента — это чисто ручная работа над конкретной реализацией движка. Как раз то, что Вы сделали. Любой редактор для этого подойдёт, если прикрутить превью фрагмента HTML.
                0
                Wymeditor — это исключение. Посмотрите, например, на обычные textarea в phpBB и Wikipedia (MediaWiki).

                Что касается редактирования, то попробуйте отредактировать большую страницу Wikipedia, наполненную различными ссылками и сносками. Только возможность редактирования отдельных разделов немного спасает от переизбытка тэгов. Может быть, конечно, всем нравится преодолевать трудности, но мне более симпатична идея редактирования выделенного пользователем фрагмента.
            +1
            Лучше бы в Хабре сделали поддержку markdown.
              0
              Пусть это — не совсем тема топика, но хотелось бы узнать мнения людей по этому вопросу — нужен ли Markdown.
              0
              Вы не пробовали JSHint'ом, там, проверить свой код, тесты написать?
                0
                В ходе тестирования искал ошибки и следил за чистотой консоли. Пока это был лишь пример.
                Сейчас прогнал через JSHint. Основные предупреждения связаны с «Expected '===' and instead saw '=='.».

                Жаль, конечно, что никто не прокомментировал саму идею, а всё сводилось лишь к скрипту. Но кто-то заинтересовался, поэтому движение в этом направлении продолжу и буду писать по мере продвижения.
                  0
                  Мне просто не понравилось решение. С т. з. юзабилити, довольно неудобно выделять код, нажимать редактировать, потом сохранять. Во-первых, это слишком много действий, во-вторых это создает лишний режим.
                  Сама предпосылка вида
                  представление отделено от кода

                  вызывает вопрос. Зачем вам код? Вы заметку пишете или код?

                  Прежде чем бросаться с места в обрыв, я бы сидел и думал над сценариями и вариантами реализации, чтобы быть совершенно уверенным в решении.

                  Поэтому я просто поделился с вами абсолютно верными методиками гарантии качества продукта.

                  И кстати, вы очень многословны для такой мелочи – ощущение, что текста написали больше, чем кода. Надо понимать, что будет людям интересно читать.
                    0
                    Мне просто не понравилось решение. С т. з. юзабилити, довольно неудобно выделять код, нажимать редактировать, потом сохранять. Во-первых, это слишком много действий, во-вторых это создает лишний режим.
                    Не вполне понял по поводу выделения кода.
                    У нас есть текстовый редактор (без представления), который поддерживает тэги. Можно редактировать в нём. Но когда текста много, то делать это неудобно, поэтому предлагается перейти в режим просмотра и там редактировать уже выборочно.

                    WYSIWYG редакторы подходят для небольших документов, особенно, когда текст только набирается или форматируется. Сложность редактирования структуры и текста в больших документах заметно возрастают и не всегда очевидно, каким образом можно что-то исправить. Например, часто для исправления текста в Word приходится копировать его в Notepad++, после чего вставлять обратно и там форматировать.
                    В качестве примера приведу математические формулы. Сравните их ввод и подготовку к печати в Word по сравнению с LaTeX.

                    Прежде чем бросаться с места в обрыв, я бы сидел и думал над сценариями и вариантами реализации, чтобы быть совершенно уверенным в решении.
                    Можете что-нибудь предложить в качестве решения задачи онлайн редактирования большого (страниц 10-15) отформатированного текста на основе тэгов? WYSIWYG аналоги Google Docs не подходят, поскольку не расширяемы и отсутствует возможность редактирования внутреннего представления.

                    Поэтому я просто поделился с вами абсолютно верными методиками гарантии качества продукта.
                    Не вполне понял, где приведены сами методики, хотя сочетание «абсолютно верными» меня уже немного испугало. За JSHint спасибо.
                      0
                      Не вполне понял, где приведены сами методики

                      Методика простая – используйте grunt, там есть все необходимые инструменты.

                      Можете что-нибудь предложить в качестве решения задачи онлайн редактирования большого (страниц 10-15) отформатированного текста на основе тэгов?

                      Конкретных идей у меня нет (не размышлял на эту тему), но предложенный вариант и вообще ход мыслей, основанный на том, что обязательно должны быть режимы представления и кода – не нравится. Если честно, не до конца понял, чем вам ворд для больших документов не подошел. Я в режиме структуры без проблем делал 150-страничный дисер. Поясните пожалуйста подробнее, в чем у вас возникла сложность с вордом?

                      Есть соображения, что должен быть хороший инструмент распознавания ввода, возможно общепринятая простая нотация для этого ввода. В случае, если режимы кода/отображения исключительно необходимы, то надо делать что-то безрежимное, как пример latex, или тот-же annotated source в backbone/underscore, где видно сразу обе части и происходит синхронное выделение фрагментов.
                        0
                        Grunt посмотрел, надо будет настроить, спасибо.

                        В Word возникают проблемы с форматированием, когда не ясно, откуда оно берётся. Чаще всего проблема с удалением пустых строк. Когда при удалении меняется формат предыдущего параграфа. Не всегда очевидно, каким образом его сохранить. Другая проблема с работой с распозанным текстом, когда задаётся различное число колонок, позиционирование и т.п. Если документ изначально создаю сам, то тут всё намного проще.

                        Идея с параллельным представлением кода и результата нравится, но как опция.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое