The paper describes an approach to the creation of a convenient tool for Javascript for online word processing. As an example, created a prototype for editing articles Habré ( described below ). With it now and am making changes to the article.
I faced the problem of choosing an online editor for the text on the site. The most obvious solution would have been one of the WYSIWYG editors. However, this option has not liked me for several reasons. First, many vulnerabilities in popular CMS systems are connected with WYSIWYG editors. Second, after the publication of the text will often be different from what it was in the editor. Third, these editors is difficult to extend to support new tags and elements. Therefore focused on the WYSIWYM editor.
Along with the choice WYSIWYM editor was a question with a choice of markup language. Should I use Wiki or Markdown syntax can be TeX-like language, or even just HTML, but for some tasks, it may be sufficient and bbCode? After some deliberation, came to the conclusion that the data can be stored in any format, but provided a clear separation of content and attributes. This will ensure that even a change of algorithms display does not distort the information. As for editing, the user can be given the ability to change data it convenient way.
Existing implementations online editor I have a huge complaint. They are uncomfortable, because the view is separated from the code. Of course, there is argued that this is the basis WYSIWYM. All true, but let's look at the specific situation.
Suppose that writing an article on the Habré or answer yet. The syntax tag sign, so the problems with text input does not arise. If you need to insert an image, or otherwise highlight, you can select the appropriate tag in the toolbar or entered manually. The first version of the text is ready, but to check the formatting before submitting need to click "Preview." And only here there is not only the source of all tags, but its concrete representation.
After reviewing already formatted text is a typo or something you want to correct and complete. There is a problem. Erroneous position has already been found in the preview, but for repair, you must go back to the editor, where, among many tags to find the one piece that will fix it.
This problem is clearly visible in the wiki or from the CMS, when you try to correct any offer little need to edit the entire document. The more paper, the harder the user. He had already found a place that should be corrected in a visual representation, but he needs to re-take the whole path, but in the source code.
Looks natural ability to dynamically connect editor for the selected user segment. Found and looked at the following realization: Jeditable , jquery-in-Place-Editor , jQuery Plugin: In-Line Text Edit , Ajax.InPlaceEditor , EditableGrid , InlineEdit 3 . Lack all the same: they allow you to download only editor for an individual item, as not a supported format. Therefore it was decided to make the editor himself and "reinvent the wheel."
So there jsiedit .
In the course of writing this article came across Redactor , which allows the editor of the text, but there again, the block is set in advance, and still is a WYSIWYG editor. The closest to my idea is Redactor Air mode , but this is just the formatting.
At the initial stage of the identified key requirements are:
So, we had to solve two problems. Let's start with the rich text editing. Take any set of tags, for example, bbCode. Suppose that the original document was created in this format, and the text is stored on the server is a bbCode tags. When a page to the user on the server is converted to the corresponding tags bbCode design HTML.
Now the user wants to edit a part of the already formatted text. It turns out that we need to get the source bbCode tags for the selected track. There are two possible approaches. First, you can dynamically (on the client) to convert the HTML code into the appropriate text bbCode. Second, you can save the information in advance of bbCode tags in HTML tag attributes:
Can still be considered an option when the server dynamically bbCode requested for specific items, but it will take more than a serious improvement on the server side.
In the design, I did not choose one of these three options, and decided to use the callback function to the developer himself decided how the HTML representation must be converted to the required format. The example uses a dynamic transformation of HTML.
After completing the entry must be converted back and save the changes to the server. In this case, again, it was decided to use the callback function and allow the developer to decide yourself what to do.
Selecting text
Now consider the second problem - enabling the user to choose which piece of text he wants to edit. Over the allocation of the mouse has a good article Range, TextRange and Selection , so the objects themselves and function of Javascript will not describe.
It remains a matter of convenience to the user. Let's say I drop a couple of isolated words in a sentence and start the editor. What exactly do I want to edit: just two words, all offering or an entire paragraph? And if I do not select the words completely, but only a few letters? In this case, I think that should be given the opportunity to edit the entire paragraph, that is comprehensive tag. But the closest the ambient tag may not <p>, and another, for example, <b>:
If we select "(!)", The editor should be displayed only for "internal (!) Release," or for the whole paragraph? I think that here the user should be allowed to edit the entire paragraph, but for flexibility, it was decided to include callback function, which for each DOM element will tell whether it is possible activation of the editor. Such a function can be implemented like this:
This results in the following algorithm to determine the selected range:
All right, but here we can wait a trap. Consider the following example:
If we just take commonAncestorContainer, we get <div> and give the user to edit the entire text. On the other hand, the user is likely to want to edit only the selected two sections. In this case, we have to expand each dedicated to full coverage <p> tags and stop.
The Range object is suitable properties: startContainer and endContainer. But here it is necessary to align the containers to the same level that they were brothers. The result was the following code:
This code should also add the previous code verification editing specific unit. Use methods and setStartBefore setEndAfter to create the range:
On this problem of definition of the selection is complete.
The mapping editor
The next step was the mapping editor. It can be displayed in a separate window, to be recorded on the original page, but I was interested in an option, the editor appears in place of the edited text. At first, the task seemed quite simple and was written the following code:
When this code executes the reality was very different from those predicted. The reason was that the attribute was startContainer "ancestor" to my selection. I can not explain it by the fact that the new range starts before the selection. As a result, decided to use previously computed variables sc and ec.
The next surprise was associated with an attempt to add an item to the range. In practice, it turned out that the object being added is in the range and destroyed the next line. To avoid this area for editing after the band began to create. Further decided to approximately determine the height for the editor. The result was the following code:
To be able to save your changes and to cancel the edit textarea was added a couple of buttons.
The last issue was the challenge editor. The editor can be activated, for example, by pressing a button on the page, from the context menu automatically when you select text with pressed Alt, etc. Decided to add the simplest method - this map button near where evolution stopped.
It required to add an event handler mouseup. Hatching position buttons defined by attributes and pageX pageY. It turned out like this:
This code may not work correctly, as it creates a new button on the «Edit» whenever the button is released. To correct enough in a global variable to store current state and change it depending on your actions.
In this first version jsiedit was ready.
To demonstrate the capabilities it was decided to create a prototype for Habr. The need for such an experienced editor to write the first article is, as it gets more and without a preview look for errors proved difficult and uncomfortable. This is exacerbated by the ban on the entry form resizing. As a result, the initial version of a text written in a notebook. Here you can read about the launch of examples to try it yourself.
Created by the editor should have let you edit any text in the preview, but after saving the edited text change as well as its source code in the box. As the launch was supposed to use the bookmarklet. Actually, that's what it is all already up and running.
Consider the challenges faced during the creation of the editor.
The lack of paragraphs
The first difficulty was the fact that the text Habra no paragraphs. Instead, they are used when saving just breaks <br />. As a result of the edited block should be limited to certain tags, borders. Let's look at the next-generated HTML code:
The entire text is in one DIV, so the previous algorithm will return to edit the entire text. In this case, we need to "cut" block between the two nearest BR tags. To do this, we can move the properties and previousSibling nextSibling.
The most difficult at this stage was the approach to the definition of the functions of verification nodes. In the end, it was decided that the function will produce an array of logical properties for this node:
Received the following test function:
Given the specificity of the preview Habré, we obtain the following function:
With a new feature has been rewritten block delineation selected data:
It is possible that it would be better to carry out checks on the type of tag, but I have arranged for a prototype and test it.
Converting text
To the editor took two conversion functions. The first is to the selected page to form a code fragment in a markup language Habra. This is a fairly easy part, because you can bypass the DOM and convert tags separately.
The inverse transformation can be performed using the existing mechanism for Habrahabr preview - send the corrected text to the server and get back the already HTML code. But I decided to do this conversion on the client. Initially tried to find a ready HTML parser in Javascript. Unfortunately, I have not found the implementation staged. Then realized that the need to write a parser from scratch and reinvent the wheel once. Because the case is not fast, it was decided to postpone the parser for individual items, but for the prototype to find at least a temporary solution. As a very simple approach, it was decided to simply add a conversion for non-standard conditions of tags. The result was the following function:
But with this function, then I had to understand in detail. There was a problem with the program code - tag <source>. All included tags should not be interpreted, but they must be interpreted outside of these blocks. Because of this format was the "break."
One of the working solution was automatically replace all the characters "<" to "<" when receiving the code. While it worked, but the resulting code in the editor was too ugly. As a result, began to refine the function. Algorithm selected the following: find all the pieces of code that includes the code, and then replace them in all the critical characters. Received the following conversion:
Вероятно, что данный код можно сделать намного симпатичнее, но сейчас редактор для Хабра создавался лишь как некоторый прототип — обоснование идеи. По мере необходимости функциональность можно улучшать и добавлять дополнительные тэги. Думаю, что регулярных выражений вполне хватит. У идеи реализации парсера приоритет понизил.
Подводя итог, хочу подчеркнуть, что целью данной статьи было описание идеи по созданию 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: Запустить проще всего как букмарклет ( что это такое ). При создании новой темы надо сделать предварительный просмотр, после чего в области предварительного просмотра будет активироваться редактор при выделении мышью. При сохранении все данные из области предварительного просмотра будут переноситься в окно ввода.
Здесь изображение с подробной инструкцией.
Это ссылка на букмарклет (её надо добавлять в закладки). Вот сам отформатированный текст букмарклета:
Предупреждение: сейчас поддерживаются лишь следующие тэги: 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>, то угловые скобки "<" и ">" окажутся заменёнными на "<" и ">". Но после их исправления у тэгов <source> и </source> всё должно стать опять корректно.
6. При редактировании статьи пропадает текст у тэга <habracut>, поскольку его нет в предварительном просмотре.
Дополнение 1: Добавлена возможность редактирования существующих статей, а не только создание новых.
Также исправлена ошибка, из-за которых скрипт не работал в браузерах на основе WebKit. Спасибо Leksat и tkf . Причина была в том, что для меня естественным было задать для параметра функции значение по умолчанию. Firefox всё принял, а вот chrome следующую конструкцию не понимает:
I faced the problem of choosing an online editor for the text on the site. The most obvious solution would have been one of the WYSIWYG editors. However, this option has not liked me for several reasons. First, many vulnerabilities in popular CMS systems are connected with WYSIWYG editors. Second, after the publication of the text will often be different from what it was in the editor. Third, these editors is difficult to extend to support new tags and elements. Therefore focused on the WYSIWYM editor.
Along with the choice WYSIWYM editor was a question with a choice of markup language. Should I use Wiki or Markdown syntax can be TeX-like language, or even just HTML, but for some tasks, it may be sufficient and bbCode? After some deliberation, came to the conclusion that the data can be stored in any format, but provided a clear separation of content and attributes. This will ensure that even a change of algorithms display does not distort the information. As for editing, the user can be given the ability to change data it convenient way.
Problem
Existing implementations online editor I have a huge complaint. They are uncomfortable, because the view is separated from the code. Of course, there is argued that this is the basis WYSIWYM. All true, but let's look at the specific situation.
Suppose that writing an article on the Habré or answer yet. The syntax tag sign, so the problems with text input does not arise. If you need to insert an image, or otherwise highlight, you can select the appropriate tag in the toolbar or entered manually. The first version of the text is ready, but to check the formatting before submitting need to click "Preview." And only here there is not only the source of all tags, but its concrete representation.
After reviewing already formatted text is a typo or something you want to correct and complete. There is a problem. Erroneous position has already been found in the preview, but for repair, you must go back to the editor, where, among many tags to find the one piece that will fix it.
This problem is clearly visible in the wiki or from the CMS, when you try to correct any offer little need to edit the entire document. The more paper, the harder the user. He had already found a place that should be corrected in a visual representation, but he needs to re-take the whole path, but in the source code.
Decision
Looks natural ability to dynamically connect editor for the selected user segment. Found and looked at the following realization: Jeditable , jquery-in-Place-Editor , jQuery Plugin: In-Line Text Edit , Ajax.InPlaceEditor , EditableGrid , InlineEdit 3 . Lack all the same: they allow you to download only editor for an individual item, as not a supported format. Therefore it was decided to make the editor himself and "reinvent the wheel."
So there jsiedit .
In the course of writing this article came across Redactor , which allows the editor of the text, but there again, the block is set in advance, and still is a WYSIWYG editor. The closest to my idea is Redactor Air mode , but this is just the formatting.
Goals and objectives jsiedit
At the initial stage of the identified key requirements are:
- The user should be able to edit formatted text;
- Edited volume unit should be determined by the user.
Implementation
So, we had to solve two problems. Let's start with the rich text editing. Take any set of tags, for example, bbCode. Suppose that the original document was created in this format, and the text is stored on the server is a bbCode tags. When a page to the user on the server is converted to the corresponding tags bbCode design HTML.
Now the user wants to edit a part of the already formatted text. It turns out that we need to get the source bbCode tags for the selected track. There are two possible approaches. First, you can dynamically (on the client) to convert the HTML code into the appropriate text bbCode. Second, you can save the information in advance of bbCode tags in HTML tag attributes:
<p>Обычный текст, но с <b data-bbCode="b">выделением полужирным</b> шрифтом.</p>
Can still be considered an option when the server dynamically bbCode requested for specific items, but it will take more than a serious improvement on the server side.
In the design, I did not choose one of these three options, and decided to use the callback function to the developer himself decided how the HTML representation must be converted to the required format. The example uses a dynamic transformation of HTML.
After completing the entry must be converted back and save the changes to the server. In this case, again, it was decided to use the callback function and allow the developer to decide yourself what to do.
Selecting text
Now consider the second problem - enabling the user to choose which piece of text he wants to edit. Over the allocation of the mouse has a good article Range, TextRange and Selection , so the objects themselves and function of Javascript will not describe.
It remains a matter of convenience to the user. Let's say I drop a couple of isolated words in a sentence and start the editor. What exactly do I want to edit: just two words, all offering or an entire paragraph? And if I do not select the words completely, but only a few letters? In this case, I think that should be given the opportunity to edit the entire paragraph, that is comprehensive tag. But the closest the ambient tag may not <p>, and another, for example, <b>:
<p>Немного текста с <b>внутренним (!) выделением</b>.</p>
If we select "(!)", The editor should be displayed only for "internal (!) Release," or for the whole paragraph? I think that here the user should be allowed to edit the entire paragraph, but for flexibility, it was decided to include callback function, which for each DOM element will tell whether it is possible activation of the editor. Such a function can be implemented like this:
function jsiedit_fn_sample_tag_check(elem) { switch (elem.tagName) { case 'P': case 'DIV': case 'SPAN': return true; } return false; }
This results in the following algorithm to determine the selected range:
elem = range.commonAncestorContainer; // получим предка для выделения while (elem && !fn_is_valid_for_edit(elem)) // используем callback функцию { elem = elem.parentNode; // перейдём к предку }
All right, but here we can wait a trap. Consider the following example:
<div>... здесь идёт много текста ... <p>Это первый параграф и пользователь начинает выделение, например, отсюда. Параграф заканчивается,</p> <p>но выделение продолжается и <b>завершается лишь здесь.</b> В результате у нас выделено несколько абзацев.</p> ... а тут опять идёт много текста ...</div>
If we just take commonAncestorContainer, we get <div> and give the user to edit the entire text. On the other hand, the user is likely to want to edit only the selected two sections. In this case, we have to expand each dedicated to full coverage <p> tags and stop.
The Range object is suitable properties: startContainer and endContainer. But here it is necessary to align the containers to the same level that they were brothers. The result was the following code:
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; } }
This code should also add the previous code verification editing specific unit. Use methods and setStartBefore setEndAfter to create the range:
var rng_new = document.createRange(); rng_new.setStartBefore(sc); rng_new.setEndAfter(ec);
On this problem of definition of the selection is complete.
The mapping editor
The next step was the mapping editor. It can be displayed in a separate window, to be recorded on the original page, but I was interested in an option, the editor appears in place of the edited text. At first, the task seemed quite simple and was written the following code:
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(); // Удалим редактируемый текст из представления
When this code executes the reality was very different from those predicted. The reason was that the attribute was startContainer "ancestor" to my selection. I can not explain it by the fact that the new range starts before the selection. As a result, decided to use previously computed variables sc and ec.
The next surprise was associated with an attempt to add an item to the range. In practice, it turned out that the object being added is in the range and destroyed the next line. To avoid this area for editing after the band began to create. Further decided to approximately determine the height for the editor. The result was the following code:
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(); // Удалим редактируемый текст из представления
To be able to save your changes and to cancel the edit textarea was added a couple of buttons.
The last issue was the challenge editor. The editor can be activated, for example, by pressing a button on the page, from the context menu automatically when you select text with pressed Alt, etc. Decided to add the simplest method - this map button near where evolution stopped.
It required to add an event handler mouseup. Hatching position buttons defined by attributes and pageX pageY. It turned out like this:
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);
This code may not work correctly, as it creates a new button on the «Edit» whenever the button is released. To correct enough in a global variable to store current state and change it depending on your actions.
In this first version jsiedit was ready.
Editor Habrahabr
To demonstrate the capabilities it was decided to create a prototype for Habr. The need for such an experienced editor to write the first article is, as it gets more and without a preview look for errors proved difficult and uncomfortable. This is exacerbated by the ban on the entry form resizing. As a result, the initial version of a text written in a notebook. Here you can read about the launch of examples to try it yourself.
Created by the editor should have let you edit any text in the preview, but after saving the edited text change as well as its source code in the box. As the launch was supposed to use the bookmarklet. Actually, that's what it is all already up and running.
Consider the challenges faced during the creation of the editor.
The lack of paragraphs
The first difficulty was the fact that the text Habra no paragraphs. Instead, they are used when saving just breaks <br />. As a result of the edited block should be limited to certain tags, borders. Let's look at the next-generated HTML code:
Начало статьи <br> <h4>Заголовок</h4><br> Один из параграфов, но он не выделен в блок "P".<br> <br> Другой параграф, внутри которого есть <b>дополнительные <i>вложенные</i> тэги</b>. Завершается опять через тэг "BR"<br> потом начинается следующий абзац.<br> <br> Продолжение статьи
The entire text is in one DIV, so the previous algorithm will return to edit the entire text. In this case, we need to "cut" block between the two nearest BR tags. To do this, we can move the properties and previousSibling nextSibling.
The most difficult at this stage was the approach to the definition of the functions of verification nodes. In the end, it was decided that the function will produce an array of logical properties for this node:
- This node can be independently selected.
- The node should be included in the sample.
- This site may be the common ancestor to select.
- This node can be a limiting unit when selecting brothers.
Received the following test function:
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; }
Given the specificity of the preview Habré, we obtain the following function:
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; }
With a new feature has been rewritten block delineation selected data:
function jsiedit_get_bounds
During the test the first version jsiedit sometimes instead of the selection in the editor provides the whole text. The reason for this was that at the beginning of the selection system on the void as a return startContainer ancestor, but remained startOffset child, with which there is a selection. The situation is similar to the end. So I had to use the following code: 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); }
It is possible that it would be better to carry out checks on the type of tag, but I have arranged for a prototype and test it.
Converting text
To the editor took two conversion functions. The first is to the selected page to form a code fragment in a markup language Habra. This is a fairly easy part, because you can bypass the DOM and convert tags separately.
The inverse transformation can be performed using the existing mechanism for Habrahabr preview - send the corrected text to the server and get back the already HTML code. But I decided to do this conversion on the client. Initially tried to find a ready HTML parser in Javascript. Unfortunately, I have not found the implementation staged. Then realized that the need to write a parser from scratch and reinvent the wheel once. Because the case is not fast, it was decided to postpone the parser for individual items, but for the prototype to find at least a temporary solution. As a very simple approach, it was decided to simply add a conversion for non-standard conditions of tags. The result was the following function:
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; };
But with this function, then I had to understand in detail. There was a problem with the program code - tag <source>. All included tags should not be interpreted, but they must be interpreted outside of these blocks. Because of this format was the "break."
One of the working solution was automatically replace all the characters "<" to "<" when receiving the code. While it worked, but the resulting code in the editor was too ugly. As a result, began to refine the function. Algorithm selected the following: find all the pieces of code that includes the code, and then replace them in all the critical characters. Received the following conversion:
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; };
Вероятно, что данный код можно сделать намного симпатичнее, но сейчас редактор для Хабра создавался лишь как некоторый прототип — обоснование идеи. По мере необходимости функциональность можно улучшать и добавлять дополнительные тэги. Думаю, что регулярных выражений вполне хватит. У идеи реализации парсера приоритет понизил.
Conclusion
Подводя итог, хочу подчеркнуть, что целью данной статьи было описание идеи по созданию javascript in-place WYSIWYM редактора и конкретного подхода к реализации. Буду рад, если подобный редактор заинтересует ещё кого-нибудь. Напишите, пожалуйста, ваше мнение. Может быть уже существуют более интересные решения?
Questions and Answers
В1: Почему не используется библиотека xxxxxxx? Почему обрабатываются не все ошибки? Почему код не работает в браузере yyyyyyy?
О1: Я постарался изложить идею in-place WYSIWYM редактора. Прототип — это proof of concept, который работает в FF18, где сейчас и пишу данные строки. Пока я был единственным заинтересованным потребителем. Если вам интересно развитие данной библиотеки, то напишите. Приведённой информации достаточно, чтобы обеспечить работу в требуемых браузерах, подключить нужную библиотеку или фреймворк.
Дополнение: Скрипт скорректирован для работы в браузерах на основе WebKit.
В2: Почему букмарклет для Хабра не выделяет синтаксис при сохранении? Почему поддерживаются не все тэги?
О2: Если будет интерес, то это всё можно реализовать. Из оставшихся тэгов в первую очередь реализовал бы <video>.
В3: Какие ближайшие планы?
О3: Очень сильно будут зависеть от вашей реакции.
В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>, то угловые скобки "<" и ">" окажутся заменёнными на "<" и ">". Но после их исправления у тэгов <source> и </source> всё должно стать опять корректно.
6. При редактировании статьи пропадает текст у тэга <habracut>, поскольку его нет в предварительном просмотре.
Дополнение 1: Добавлена возможность редактирования существующих статей, а не только создание новых.
Также исправлена ошибка, из-за которых скрипт не работал в браузерах на основе WebKit. Спасибо Leksat и tkf . Причина была в том, что для меня естественным было задать для параметра функции значение по умолчанию. Firefox всё принял, а вот chrome следующую конструкцию не понимает:
0 commentaires:
Enregistrer un commentaire