Как разобрать небольшое подмножество Markdown в компоненты React?

9

У меня есть очень небольшое подмножество Markdown вместе с некоторыми пользовательскими html, которые я хотел бы проанализировать в компонентах React. Например, я хотел бы включить эту строку:

hello *asdf* *how* _are_ you !doing! today

В следующий массив:

[ "hello ", <strong>asdf</strong>, " ", <strong>how</strong>, " ", <em>are</em>, " you ", <MyComponent onClick={this.action}>doing</MyComponent>, " today" ]

и затем верните его из функции рендеринга React (React правильно отобразит массив как отформатированный HTML)

По сути, я хочу дать пользователям возможность использовать очень ограниченный набор Markdown, чтобы превратить их текст в стилизованные компоненты (и в некоторых случаях мои собственные компоненты!)

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

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

function matchStrong(result, i) {
  let match = result[i].match(/(^|[^\\])\*(.*)\*/);
  if (match) { result[i] = <strong key={"ms" + i}>{match[2]}</strong>; }
  return match;
}

function matchItalics(result, i) {
  let match = result[i].match(/(^|[^\\])_(.*)_/); // Ignores \_asdf_ but not _asdf_
  if (match) { result[i] = <em key={"mi" + i}>{match[2]}</em>; }
  return match;
}

function matchCode(result, i) {
  let match = result[i].match(/(^|[^\\])```\n?([\s\S]+)\n?```/);
  if (match) { result[i] = <code key={"mc" + i}>{match[2]}</code>; }
  return match;
}

// Very brittle and inefficient
export function convertMarkdownToComponents(message) {
  let result = message.match(/(\\?([!*_`+-]{1,3})([\s\S]+?)\2)|\s|([^\\!*_`+-]+)/g);

  if (result == null) { return message; }

  for (let i = 0; i < result.length; i++) {
    if (matchCode(result, i)) { continue; }
    if (matchStrong(result, i)) { continue; }
    if (matchItalics(result, i)) { continue; }
  }

  return result;
}

Вот мой предыдущий вопрос, который привел к этому.

Райан Пешел
источник
1
Что делать, если вход имеет вложенные элементы, например font _italic *and bold* then only italic_ and normal? Каков будет ожидаемый результат? Или это никогда не будет вложенным?
Trincot
1
Не нужно беспокоиться о вложенности. Это просто базовая скидка для пользователей. То, что легче всего реализовать, хорошо для меня. В вашем примере было бы совершенно нормально, если бы внутренний шрифт не работал. Но если проще реализовать вложение, чем не иметь его, то это тоже хорошо.
Райан
1
Вероятно, проще всего использовать готовое решение, например, npmjs.com/package/react-markdown-it
mb21,
1
Я не использую уценку все же. Это просто очень похожее / небольшое подмножество (которое поддерживает несколько пользовательских компонентов, наряду с не вложенным жирным шрифтом, курсивом, кодом, подчеркиванием). Фрагменты, которые я опубликовал, несколько работают, но не кажутся очень идеальными и терпят неудачу в некоторых тривиальных случаях (например, вы не можете напечатать одну звездочку, как это: asdf*без ее исчезновения)
Райан
1
ну ... разбор уценки или что-то вроде уценки не совсем легкая задача ... регулярные выражения не обрезают ее ... для аналогичного вопроса относительно html, см. stackoverflow.com/questions/1732348/…
mb21

Ответы:

1

Как это работает?

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

Всякий раз, когда анализатор обнаруживает, что читается критический фрагмент, т. '*'Е. Или любой другой тег уценки, он начинает анализировать фрагменты этого элемента, пока анализатор не найдет свой закрывающий тег.

Он работает на многострочных строках, см. Код, например.

Предостережения

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

Однако, если вам нужно работать с вышеуказанными условиями, просто прокомментируйте здесь, и я настрою код.

Первое обновление: настройка тегов уценки

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

Исправлены ошибки, о которых вы упоминали в комментариях, спасибо за указание на эти проблемы = p

Второе обновление: многократные теги уценки

Самый простой способ добиться этого: замена многомерных символов на редко используемый Юникод

Хотя метод parseMarkdownеще не поддерживает теги с несколькими длинами, мы можем легко заменить эти теги с несколькими длинами на простые string.replace при отправке нашего rawMarkdownреквизита.

Чтобы увидеть пример этого на практике, посмотрите на ReactDOM.render, расположенный в конце кода.

Даже если ваше приложение делает поддержку нескольких языков, есть недопустимые символы Юникода , что JavaScript все еще обнаруживает, напр .: "\uFFFF"не является допустимым юникода, если я правильно помню, но JS все равно будет иметь возможность сравнить его ( "\uFFFF" === "\uFFFF" = true)

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

Еще один способ достижения этого

Ну, мы могли бы легко отследить последние N(где Nсоответствует длине самого длинного тега с длинной длиной) фрагменты.

Было бы необходимо внести некоторые изменения в parseMarkdownповедение метода внутри цикла , т. Е. Проверить, является ли текущий фрагмент частью тега с длинной длиной, если он используется в качестве тега; в противном случае, в таких случаях ``k, нам нужно пометить его как notMultiLengthили что-то похожее и вставить этот кусок как контент.

Код

// Instead of creating hardcoded variables, we can make the code more extendable
// by storing all the possible tags we'll work with in a Map. Thus, creating
// more tags will not require additional logic in our code.
const tags = new Map(Object.entries({
  "*": "strong", // bold
  "!": "button", // action
  "_": "em", // emphasis
  "\uFFFF": "pre", // Just use a very unlikely to happen unicode character,
                   // We'll replace our multi-length symbols with that one.
}));
// Might be useful if we need to discover the symbol of a tag
const tagSymbols = new Map();
tags.forEach((v, k) => { tagSymbols.set(v, k ); })

const rawMarkdown = `
  This must be *bold*,

  This also must be *bo_ld*,

  this _entire block must be
  emphasized even if it's comprised of multiple lines_,

  This is an !action! it should be a button,

  \`\`\`
beep, boop, this is code
  \`\`\`

  This is an asterisk\\*
`;

class App extends React.Component {
  parseMarkdown(source) {
    let currentTag = "";
    let currentContent = "";

    const parsedMarkdown = [];

    // We create this variable to track possible escape characters, eg. "\"
    let before = "";

    const pushContent = (
      content,
      tagValue,
      props,
    ) => {
      let children = undefined;

      // There's the need to parse for empty lines
      if (content.indexOf("\n\n") >= 0) {
        let before = "";
        const contentJSX = [];

        let chunk = "";
        for (let i = 0; i < content.length; i++) {
          if (i !== 0) before = content[i - 1];

          chunk += content[i];

          if (before === "\n" && content[i] === "\n") {
            contentJSX.push(chunk);
            contentJSX.push(<br />);
            chunk = "";
          }

          if (chunk !== "" && i === content.length - 1) {
            contentJSX.push(chunk);
          }
        }

        children = contentJSX;
      } else {
        children = [content];
      }
      parsedMarkdown.push(React.createElement(tagValue, props, children))
    };

    for (let i = 0; i < source.length; i++) {
      const chunk = source[i];
      if (i !== 0) {
        before = source[i - 1];
      }

      // Does our current chunk needs to be treated as a escaped char?
      const escaped = before === "\\";

      // Detect if we need to start/finish parsing our tags

      // We are not parsing anything, however, that could change at current
      // chunk
      if (currentTag === "" && escaped === false) {
        // If our tags array has the chunk, this means a markdown tag has
        // just been found. We'll change our current state to reflect this.
        if (tags.has(chunk)) {
          currentTag = tags.get(chunk);

          // We have simple content to push
          if (currentContent !== "") {
            pushContent(currentContent, "span");
          }

          currentContent = "";
        }
      } else if (currentTag !== "" && escaped === false) {
        // We'll look if we can finish parsing our tag
        if (tags.has(chunk)) {
          const symbolValue = tags.get(chunk);

          // Just because the current chunk is a symbol it doesn't mean we
          // can already finish our currentTag.
          //
          // We'll need to see if the symbol's value corresponds to the
          // value of our currentTag. In case it does, we'll finish parsing it.
          if (symbolValue === currentTag) {
            pushContent(
              currentContent,
              currentTag,
              undefined, // you could pass props here
            );

            currentTag = "";
            currentContent = "";
          }
        }
      }

      // Increment our currentContent
      //
      // Ideally, we don't want our rendered markdown to contain any '\'
      // or undesired '*' or '_' or '!'.
      //
      // Users can still escape '*', '_', '!' by prefixing them with '\'
      if (tags.has(chunk) === false || escaped) {
        if (chunk !== "\\" || escaped) {
          currentContent += chunk;
        }
      }

      // In case an erroneous, i.e. unfinished tag, is present and the we've
      // reached the end of our source (rawMarkdown), we want to make sure
      // all our currentContent is pushed as a simple string
      if (currentContent !== "" && i === source.length - 1) {
        pushContent(
          currentContent,
          "span",
          undefined,
        );
      }
    }

    return parsedMarkdown;
  }

  render() {
    return (
      <div className="App">
        <div>{this.parseMarkdown(this.props.rawMarkdown)}</div>
      </div>
    );
  }
}

ReactDOM.render(<App rawMarkdown={rawMarkdown.replace(/```/g, "\uFFFF")} />, document.getElementById('app'));

Ссылка на код (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv

Ссылка на код (vanilla / babel) https://codepen.io/ludanin/pen/eYmBvXw

Лукас Данин
источник
Я чувствую, что это решение на правильном пути, но, похоже, возникают проблемы с размещением других символов уценки внутри других. Например, попробуйте заменить This must be *bold*на This must be *bo_ld*. Это приводит к тому, что полученный HTML будет искажен
Райан
Отсутствие надлежащего тестирования произвело это = p, мой плохой. Я уже исправляю это и собираюсь опубликовать результат здесь, кажется простой проблемой, которую нужно исправить.
Лукас Данин
Да спасибо. Мне действительно нравится это решение, хотя. Кажется, очень прочный и чистый. Я думаю, что это может быть немного изменено, хотя для еще большей элегантности. Я мог бы попытаться возиться с этим немного.
Райан
Готово, между прочим, я настроил код для поддержки гораздо более гибкого способа определения тегов уценки и их соответствующих значений JSX.
Лукас Данин
Эй, спасибо, это выглядит великолепно. Еще одна вещь, и я думаю, что она будет идеальной. В моем исходном посте у меня также есть функция для фрагментов кода (которые включают тройные обратные тики). Будет ли возможно иметь поддержку для этого? Чтобы теги могли по выбору состоять из нескольких символов? Другой ответ добавил поддержку, заменив экземпляры `` `на редко используемый символ. Это был бы простой способ сделать это, но не уверен, что это идеально.
Райан
4

Похоже, вы ищете небольшое очень простое решение. Не "супер-монстры", как react-markdown-it:)

Я бы хотел порекомендовать вам https://github.com/developit/snarkdown, который выглядит довольно легким и красивым! Просто 1 КБ и очень просто, вы можете использовать его и расширить его, если вам нужны другие синтаксические функции.

Список поддерживаемых тегов https://github.com/developit/snarkdown/blob/master/src/index.js#L1

Обновить

Просто заметил про реагирующие компоненты, пропустил это в начале. Так что это здорово для вас, я полагаю, взять библиотеку в качестве примера и реализовать ваши собственные необходимые компоненты, чтобы сделать это без опасной настройки HTML. Библиотека довольно маленькая и понятная. Веселитесь с этим! :)

Александр Шурыгин
источник
3
var table = {
  "*":{
    "begin":"<strong>",
    "end":"</strong>"
    },
  "_":{
    "begin":"<em>",
    "end":"</em>"
    },
  "!":{
    "begin":"<MyComponent onClick={this.action}>",
    "end":"</MyComponent>"
    },

  };

var myMarkdown = "hello *asdf* *how* _are_ you !doing! today";
var tagFinder = /(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/gm;

//Use case 1: direct string replacement
var replaced = myMarkdown.replace(tagFinder, replacer);
function replacer(match, whole, tag_begin, content, tag_end, offset, string) {
  return table[tag_begin]["begin"] + content + table[tag_begin]["end"];
}
alert(replaced);

//Use case 2: React components
var pieces = [];
var lastMatchedPosition = 0;
myMarkdown.replace(tagFinder, breaker);
function breaker(match, whole, tag_begin, content, tag_end, offset, string) {
  var piece;
  if (lastMatchedPosition < offset)
  {
    piece = string.substring(lastMatchedPosition, offset);
    pieces.push("\"" + piece + "\"");
  }
  piece = table[tag_begin]["begin"] + content + table[tag_begin]["end"];
  pieces.push(piece);
  lastMatchedPosition = offset + match.length;

}
alert(pieces);

Результат: Результат выполнения

Результат теста на регулярное выражение

Объяснение:

/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
  • Вы можете определить свои теги в этом разделе: как [*|!|_]только один из них будет сопоставлен, он будет записан как группа и назван как «tag_begin».

  • И затем (?<content>\w+)захватывает контент, завернутый в тег.

  • Конечный тег должен быть таким же, как и ранее подобранный, поэтому здесь используется \k<tag_begin>, и если он прошел тест, то захватывает его как группу и присваивает ему имя «tag_end», вот что (?<tag_end>\k<tag_begin>))говорит.

В JS вы создали таблицу следующим образом:

var table = {
  "*":{
    "begin":"<strong>",
    "end":"</strong>"
    },
  "_":{
    "begin":"<em>",
    "end":"</em>"
    },
  "!":{
    "begin":"<MyComponent onClick={this.action}>",
    "end":"</MyComponent>"
    },

  };

Используйте эту таблицу, чтобы заменить соответствующие теги.

Sting.replace имеет перегрузку String.replace (regexp, функция), которая может принимать захваченные группы в качестве параметров, мы используем эти захваченные элементы для просмотра таблицы и генерирования замещающей строки.

[Обновление]
Я обновил код, я сохранил первый на тот случай, если кому-то еще не нужны реагирующие компоненты, и вы можете видеть, что между ними мало различий. Реагировать Компоненты

Саймон
источник
К сожалению, я не уверен, что это работает. Потому что мне нужны сами компоненты и элементы React, а не их строки. Если вы посмотрите на мой оригинальный пост, то увидите, что я добавляю в массив сами фактические элементы, а не их строки. И использование опасно SetInnerHTML опасно, поскольку пользователь может вводить вредоносные строки.
Райан
К счастью, преобразовать замену строки в компоненты React очень просто, я обновил код.
Симон
Хм? Я должно быть что-то упустил, потому что они все еще струны на моем конце. Я даже поиграл с твоим кодом. Если вы прочитаете console.logвывод, то увидите, что массив заполнен строками, а не фактическими компонентами React: jsfiddle.net/xftswh41
Райан
Честно говоря, я не знаю React, поэтому я не могу сделать так, чтобы все идеально соответствовало вашим потребностям, но я думаю, что информации о том, как решить ваш вопрос, достаточно, вы должны поместить их на свой React-компьютер, и он просто сможет работать.
Саймон
Причина, по которой этот поток существует, заключается в том, что, кажется, значительно сложнее разобрать их в компоненты React (отсюда и заголовок потока, в котором указывается именно эта необходимость). Разобрать их в строки довольно тривиально, и вы можете просто использовать функцию замены строк. Строки не являются идеальным решением, потому что они медленны и чувствительны к XSS из-за необходимости опасного вызова
Райан
0

Вы можете сделать это так:

//inside your compoenet

   mapData(myMarkdown){
    return myMarkdown.split(' ').map((w)=>{

        if(w.startsWith('*') && w.endsWith('*') && w.length>=3){
           w=w.substr(1,w.length-2);
           w=<strong>{w}</strong>;
         }else{
             if(w.startsWith('_') && w.endsWith('_') && w.length>=3){
                w=w.substr(1,w.length-2);
                w=<em>{w}</em>;
              }else{
                if(w.startsWith('!') && w.endsWith('!') && w.length>=3){
                w=w.substr(1,w.length-2);
                w=<YourComponent onClick={this.action}>{w}</YourComponent>;
                }
            }
         }
       return w;
    })

}


 render(){
   let content=this.mapData('hello *asdf* *how* _are_ you !doing! today');
    return {content};
  }
Джатин Пармар
источник
0

A working solution purely using Javascript and ReactJs without dangerouslySetInnerHTML.

Подходить

Посимвольный поиск элементов уценки. Как только он встречается, найдите конечный тег для того же самого и затем конвертируйте его в HTML.

Поддерживаемые теги во фрагменте

  • смелый
  • курсив
  • Эм
  • до

Ввод и вывод из фрагмента:

JsFiddle: https://jsfiddle.net/sunil12738/wg7emcz1/58/

Код:

const preTag = "đ"
const map = {
      "*": "b",
      "!": "i",
      "_": "em",
      [preTag]: "pre"
    }

class App extends React.Component {
    constructor(){
      super()
      this.getData = this.getData.bind(this)
    }

    state = {
      data: []
    }
    getData() {
      let str = document.getElementById("ta1").value
      //If any tag contains more than one char, replace it with some char which is less frequently used and use it
      str = str.replace(/```/gi, preTag)
      const tempArr = []
      const tagsArr = Object.keys(map)
      let strIndexOf = 0;
      for (let i = 0; i < str.length; ++i) {
        strIndexOf = tagsArr.indexOf(str[i])
        if (strIndexOf >= 0 && str[i-1] !== "\\") {
          tempArr.push(str.substring(0, i).split("\\").join("").split(preTag).join(""))
          str = str.substr(i + 1);
          i = 0;
          for (let j = 0; j < str.length; ++j) {
            strIndexOf = tagsArr.indexOf(str[j])
            if (strIndexOf >= 0 && str[j-1] !== "\\") {
              const Tag = map[str[j]];
              tempArr.push(<Tag>{str.substring(0, j).split("\\").join("")}</Tag>)
              str = str.substr(j + 1);
              i = 0;
              break
             }
          }
        }
      }
      tempArr.push(str.split("\\").join(""))
      this.setState({
        data: tempArr,
      })
    }
    render() {
      return (
        <div>
          <textarea rows = "10"
            cols = "40"
           id = "ta1"
          /><br/>
          <button onClick={this.getData}>Render it</button><br/> 
          {this.state.data.map(x => x)} 
        </div>
      )
    }
  }

ReactDOM.render(
  <App/>,
  document.getElementById('root')
);
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
  <div id="root"></div>
</body>

Подробное объяснение (с примером):

Предположим, если строка - это « How are *you* doing? Сохранить отображение символов на теги».

map = {
 "*": "b"
}
  • Цикл, пока вы не найдете первый *, текст до этого является нормальной строкой
  • Нажмите это внутри массива. Массив стал["How are "] и запускать внутренний цикл, пока не найдете следующий *.
  • Now next between * and * needs to be bold, мы конвертируем их в html-элемент по тексту и напрямую помещаем в массив где Tag = b с карты. Если вы это сделаете <Tag>text</Tag>, внутренняя реакция преобразуется в текст и толкает в массив. Теперь массив [«как дела», вы ]. Вырваться из внутреннего цикла
  • Теперь мы запускаем внешний цикл, и теги не найдены, поэтому нажмите оставшиеся в массиве. Массив становится: [«как дела», вы «делаете»].
  • Рендер на пользовательском интерфейсе How are <b>you</b> doing?
    Note: <b>you</b> is html and not text

Примечание : вложение также возможно. Нам нужно вызвать вышеуказанную логику в рекурсии

Добавить поддержку новых тегов

  • Если это один символ, такой как * или!, Добавьте их в mapобъект с ключом в качестве символа и значением в качестве соответствующего тега
  • Если они содержат более одного символа, такого как `` `, создайте карту« один к одному »с некоторым менее часто используемым символом, а затем вставьте (Причина: в настоящее время подход основан на поиске по символам и, следовательно, более одного символа сломается. Однако Об этом тоже можно позаботиться, улучшив логику)

Это поддерживает вложение? Нет
Поддерживает ли он все случаи использования, упомянутые OP? да

Надеюсь, поможет.

Сунил Чаудхари
источник
Привет, просматривая это сейчас. Возможно ли это использовать с поддержкой тройного обратного удара? Так `` `asdf``` будет работать так же хорошо для блоков кода?
Райан
Это будет, но некоторые изменения могут быть необходимы. В настоящее время существует только одно совпадение символов для * или! Это нужно немного изменить. Блоки кода в основном означают, asdfчто будут отображаться <pre>asdf</pre>с темным фоном, верно? Дайте мне знать это, и я увижу. Даже вы можете попробовать сейчас. Простой подход заключается в следующем: в приведенном выше решении замените `` `в тексте специальным символом, например ^ или ~, и сопоставьте его с предварительным тегом. Тогда все будет работать нормально. Другой подход требует дополнительной работы
Сунил Чаудхари
Да, точно, заменив `` `asdf``` на <pre>asdf</pre>. Спасибо!
Райан
@RyanPeschel Привет! Также добавили preподдержку тегов. Дайте мне знать, если это работает
Сунил Чаудхари
Интересное решение (используется редкий персонаж). Однако я по-прежнему вижу одну проблему - отсутствие поддержки для экранирования (так что \ * asdf * не выделен жирным шрифтом), о которой я включил поддержку в код в моем исходном посте (также упоминал об этом в своей связанной разработке в конце после). Это было бы очень трудно добавить?
Райан