В Twake мы предоставляем приложение для обмена мгновенными сообщениями с множеством других приложений, таких как хранилище, диспетчер задач, календарь, видеоконференцсвязь… Twake - это платформа для совместной работы, разработанная для современных команд. Наше приложение для обмена сообщениями имеет базовые функции форматирования. Но некоторые проблемы существовали в более ранних версиях Twake: нам нужно было декодировать клиентскую сторону сообщения, чтобы сгенерировать соответствующие узлы HTML, конечно, это не было возможностью отправлять и получать напрямую HTML. Наконец, не было возможности сгенерировать серверную часть HTML, потому что JavaScript лучше подходит для управления виртуальной DOM, а наша структура React на самом деле не предназначена для оценки строк HTML, поступающих с сервера. Вот и решили сделать сами! 🤓
Когда мы разрабатывали первую версию Twake, мы реализовали библиотеку разметки JS, которая имела возможность анализировать множество вещей, таких как таблицы, заголовки, списки маркеров, ссылки, изображения и т. Д. Эта библиотека принимает простую строку разметки и возвращает HTML-код. нить. Первая проблема заключалась в том, что анализ разметки выполняется очень медленно из-за большого количества доступных функций. В этой библиотеке было сложно отключить бесполезные функции, такие как размеры заголовков или таблиц. Вторая основная проблема заключалась в выводе HTML. React плохо работает с обычным HTML, потому что он не извлекает выгоду из виртуальной модели DOM React и для нас не позволяет использовать компонент React для анализируемых элементов. Вот почему мы решили частично проанализировать сообщение, прежде чем отправлять его на сервер, это в некотором роде похоже на байт-код в JAVA.
Чтобы реализовать эту функцию, мы решили отправлять на сервер строки JSON вместо ввода данных пользователем. Это означает, что вместо отправки и сохранения чего-то вроде *Twake* is the _best_ :sunglasses:
мы будем хранить что-то вроде [{type: “bold", content: ["Twake"]}, “ is the ”, {type: "underline", content: ["best"]}, “ “, {type: "emoji", content: "sunglasses"}]
. Этот JSON будет использоваться для создания HTML с помощью React. Мы также должны иметь возможность вернуться к исходному вводу пользователя из этого объекта для возможностей редактирования. Почему JSON, а не пользовательский ввод? Поскольку JSON будет обрабатываться намного быстрее, чем вводимые пользователем данные, поскольку анализ JSON выполняется собственными методами браузера.
Почему именно JSON, а не HTML? Поскольку хранить HTML в базе данных и передавать его другим пользователям опасно, кому-то легко изменить сгенерированный HTML и, например, выполнить XSS-атаки. И если мы напрямую загружаем HTML в React, мы не можем использовать виртуальные компоненты React DOM и React.
Готов идти ? В Twake 1.2 есть функции форматирования:
- * полужирный *
- ° курсив °
- ~ зачеркивание ~
- _underline_
- `встроенный код`
- `` многострочный код ``
-: grin: 😀
- @romaricmourgues (quote user)
- ›однострочная цитата
- ››› многострочная цитата
Этот код является упрощением Markdown и очень близок к коду здесь, в документации Slack.
Подготовка класса компиляции
Как вы, возможно, знаете, мы используем React Framework, но следующий код можно легко преобразовать в обычный код. У нашего класса будет три метода:
- Пользовательский ввод ›JSON
- JSON› Пользовательский ввод
- JSON ›Дерево узлов React
Мы также определяем наш код псевдо-уценки в конструкторе, каждый элемент объекта будет содержать:
- начальное значение любого формата (как ключ объекта),
- конечное значение в регулярном выражении,
- символы, разрешенные как дочерний формат,
- и генератор узлов как методы, принимающие дочерний элемент в качестве аргумента.
import Emojione from 'components/Emojione/Emojione.js' import User from "components/ui/User.js"; class PseudoMarkdownCompiler { constructor(){ this.pseudo_markdown = { "```": { end: "```", allowed_chars: "(.|\n)" object: (child) => <div className='multiline-code'>{child}</div> }, "`": { end: "`", allowed_chars: "." object: (child) => <div className='inline-code'>{child}</div> }, "_": { end: "_", allowed_chars: "." object: (child) => <div className='underline'>{child}</div> }, "~": { end: "~", allowed_chars: "." object: (child) => <div className='strikethrough'>{child}</div> }, "*": { end: "\\*", allowed_chars: "." object: (child) => <div className='bold'>{child}</div> }, "°": { end: "°", allowed_chars: "." object: (child) => <div className='italic'>{child}</div> }, ">>>": { end: false, allowed_chars: "(.|\n)" object: (child) => <div className='italic'>{child}</div> }, ">": { end: "$", object: (child) => <div className='italic'>{child}</div> }, ":": { allowed_chars: "[a-z_]", end: ":", object: (child) => <Emojione type={child[0]} /> }, "@": { allowed_chars: "[a-z_.\-A-Z0-9]", end: " ", object: (child) => <User data={child} /> } } } compileToJSON(str){ //TODO } compileToText(json){ //TODO } compileToHTML(json){ //TODO } } const service = new PseudoMarkdownCompiler(); export default service;
Это класс, который мы используем в нашем приложении React, поскольку вы можете видеть, что класс будет использоваться как синглтон с использованием значения экспорта по умолчанию для экземпляра нашей службы. В этом нет необходимости, и вы можете работать со статическими методами класса. Вы уже можете видеть преимущество создания дерева JSON для React: мы сможем рекурсивно вызывать методы «объекта» для генерации нашего сообщения, используя простые ‹div› или сложные компоненты, такие как ‹User /›.
Создать JSON
Генерация объекта JSON очень проста, мы комбинируем рекурсивные вызовы и регулярные выражения.
compileToJSON(str){ var result = []; var min_index_of = -1; var min_index_of_key = null; Object.keys(this.pseudo_markdown).forEach((starting_value)=>{ var io = str.indexOf(starting_value); if(io >= 0 && (min_index_of < 0 || io < min_index_of)){ min_index_of = io; min_index_of_key = starting_value; } }); if(min_index_of_key){ var str_left = str.substr(0, min_index_of); var char = min_index_of_key; var str_right = str.substr(min_index_of+char.length); //Seach end of element in str_right var match = str_right.match( new RegExp( "^(" +(this.pseudo_markdown[char].allowed_chars || ".") +"*" +(this.pseudo_markdown[char].end?"?":"") +")" +(this.pseudo_markdown[char].end ?"("+this.pseudo_markdown[char].end+")" :"") , "m" ) ); if(!match){ str_left = str_left+char; result.push(str_left); }else{ if(str_left){ result.push(str_left); } //Generate object var object = { start: char, content: this.compileToJSON(match[1]), end: match[2] } result.push(object); str_right = str_right.substr(match[0].length); } result = result.concat( this.compileToJSON(str_right) ); return result; }else{ if(str){ return [str]; }else{ return []; } } }
Эти методы генерируют дерево JSON со всем, что нам нужно для создания узлов HTML, и достаточно безопасным, чтобы его можно было получать и использовать как его с сервера. Объект JSON выглядит так:
//compileToJSON(">>> Hello\n > _Underline *sub-bold-quote*_ \n end of main quote") [ {"start": ">>>", "content": [ " Hello\n ", {"start":">", "content":[ " ", {"start":"_", "content":[ "Underline ", {"start":"*", "content":[ "sub-quote-citation" ], "end":"*" } ], "end":"_" }, " " ], "end":"" }, "\n end of main quote" ], "end":"" } ]
Вы можете улучшить код, чтобы отключить рекурсию в форматах «встроенный код» и «многострочный код», распознать URL-адреса или отключить распознавание некоторых символов, которым предшествует косая черта (это может быть сложно, вам нужно игнорировать символы с косой чертой с улучшенными регулярными выражениями или используя циклы while, мы использовали последний в нашей финальной версии).
Сгенерируйте HTML и получите данные, введенные пользователем, из JSON
Чтобы вернуть пользовательский ввод, вам просто нужно рекурсивно объединить элементы start, content и end. Это должно дать вам именно то, что ввел пользователь.
Для создания HTML вы можете использовать «объектные» части this.pseudo_markdown
. Поскольку React работает с деревом узлов и списками узлов, вам просто нужно использовать функцию JavaScript [].map((item)=>{})
.
compileToHTML(json){ var result = []; json.forEach((item)=>{ if(typeof item == "string"){ result.push(item); } else { if(this.pseudo_markdown[item.start]){ result.push( this.pseudo_markdown[item.start].object( this.compileToHTML(item.content) ) ); } } }); return result; }
Теперь вы можете создать свой стиль и добавить компоненты, которые хотите использовать, в финальной версии мы используем компонент для формата многострочного кода, чтобы обеспечить раскрашивание с помощью highlight.js (будьте осторожны! Используйте асинхронную загрузку для языков если вы не хотите удваивать размер приложения и время загрузки)
Бонус: маркированные списки 😀
Если вы хотите интегрировать маркированные списки в свои функции, похоже, что это не вопрос стиля, а ваш вводимый текст. Вам не нужны жирные, специальные цвета, курсив или что-то в этом роде в конечном результате: вы хотите помочь пользователю автоматически добавлять новые маркеры при написании сообщения (не поверите мне? Попробуйте Slack!). Для этого вам нужно будет отредактировать свой textarea.value
в событии keydown
, а затем переместить курсор в правильное положение. Вы можете найти getCaretPosition
функцию здесь и setCaretPosition
функцию здесь.
<textarea onkeydown={(evt) => autoCompleteBulletList(evt)} /> autoCompleteBulletList(evt) { var input = evt.target; if(evt.key === "Enter"){ var cursor_position = getCaretPosition(input); var value = input.value; var str_before = value.substr(0, cursor_position); var str_after = value.substr(cursor_position); //Get previous line var src_line_before = str_before.split("\n").pop(); var addon = ""; var match = src_line_before.match(new RegExp("^• ", "")); if(match){ addon = "• "; }; input.value = str_before + "\n" + addon + str_after; setCaretPosition(input, cursor_position+addon.length+1); //Do not add the line break, we did it ourselves evt.stopPropagation(); evt.preventDefault(); } }
После того, как вы успешно реализовали этот код, вы можете добавить больше типов маркеров, таких как «-» или более интересных, вы можете реализовать «a. »И автоматически сгенерировать следующую букву (или цифру для« 1 »). Тогда не забудьте сделать его удобным для пользователя: когда вы вводите ввод, должна появиться новая марка, но если вы вводите ввод второй раз, новая марка должна исчезнуть, как если бы она была отменена. Попробуйте каждый конкретный случай в Slack, это хороший способ убедиться, что он хорошо работает на вашей стороне.
Заключение
Эта функция позволяет снизить затраты на ваш браузер, пытающийся анализировать вашу псевдо-уценку из исходной исходной строки каждый раз, когда вы хотите показать сообщение в своем приложении для обмена сообщениями.
Не стесняйтесь спрашивать, есть ли у вас какие-либо вопросы или комментарии по поводу этой статьи, а пока желаю хорошей недели! ⛱