В 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, это хороший способ убедиться, что он хорошо работает на вашей стороне.

Заключение

Эта функция позволяет снизить затраты на ваш браузер, пытающийся анализировать вашу псевдо-уценку из исходной исходной строки каждый раз, когда вы хотите показать сообщение в своем приложении для обмена сообщениями.
Не стесняйтесь спрашивать, есть ли у вас какие-либо вопросы или комментарии по поводу этой статьи, а пока желаю хорошей недели! ⛱