Як розібрати невелику підмножину 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? Яким був би очікуваний результат? Або він ніколи не вкладеться?
трінкот

1
Не потрібно турбуватися про гніздування. Користувачі користуються просто базовою відміткою. Що найпростіше здійснити, зі мною добре. У вашому прикладі було б абсолютно добре, якби внутрішня шпилька не спрацювала. Але якщо легше здійснити гніздування, ніж цього не мати, то це теж добре.
Райан Пешель

1
Напевно, найпростіше просто використовувати звичайне
mb21

1
Я не використовую розмітку. Це просто дуже схожий / невеликий його підмножина (яка підтримує пару користувацьких компонентів, разом із вкладеними жирними шрифтами, курсивом, кодом, підкресленням). Фрагменти, які я розмістив дещо, але не здаються дуже ідеальними, і не спрацьовують у деяких тривіальних випадках (як, наприклад, ви не можете набрати жодної зірочки так: asdf*без неї не зникає)
Ryan Peschel

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
Ryan Peschel

Відсутність належного тестування призвело до цього = 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);

Результат: Результат бігу

Результат тесту Regexp

Пояснення:

/(?<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
Ryan Peschel

Чесно кажучи, я не знаю React, тому я не можу зробити все ідеально за вашими потребами, але я думаю, що інформації про те, як вирішити своє питання, достатньо, вам потрібно поставити їх на вашу машину React, і вона просто може піти.
Саймон

Причина, по якій існує цей потік, полягає в тому, що, здається, набагато важче розібрати їх на компоненти React (звідси назва заголовка вказує ту точну потребу). Розбиття їх на рядки досить тривіально, і ви можете просто використовувати функцію заміни рядків. Струни не є ідеальним рішенням, оскільки вони повільні та сприйнятливі до XSS через те, що потрібно небезпечно дзвонитиSetInnerHTML
Ryan Peschel

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 об'єкт з ключем як символом та значенням як відповідний тег
  • Якщо їх більше, ніж один символ, наприклад, `` `, створіть карту один на одну з менш часто використовуваним знаком та вставте (Причина: наразі підхід, заснований на пошуку символів, і так більше одного знака зламається. Однак , що також можна подбати, покращивши логіку)

Чи підтримує він гніздування? Ні
Чи підтримує він всі випадки використання, згадані ОП? Так

Сподіваюся, це допомагає.


Привіт, зараз переглядаю це. Чи можливо це використовувати і з потрійною підтримкою backtick? Так що `` asdf``` добре буде працювати для блоків коду?
Райан Пешель

Буде, але можуть знадобитися деякі зміни. Наразі для * або! Існує лише одна відповідність символів. Це потрібно трохи змінити. Кодові блоки, в основному, засоби asdfбудуть надані<pre>asdf</pre> з темним фоном, правда? Дайте мені це знати, і я побачу. Навіть ви можете спробувати зараз. Простий підхід полягає в наступному: У наведеному вище рішенні замініть текст `` `на текст спеціальним символом, таким як ^ або ~, і нанесіть його на попередній тег. Тоді це буде добре працювати. Інший підхід потребує більше роботи
Суніл Чаудхари

Так, саме, замінивши `` asdf``` на <pre>asdf</pre>. Дякую!
Райан Пешель

@RyanPeschel Привіт! Додано також preпідтримку тегів. Дайте мені знати, чи працює це
Суніл Чаудхари

Цікаве рішення (з використанням рідкісного символу). Одне питання, яке я все ще бачу, - це відсутність підтримки для втечі (такий, що \ * asdf * не виділений жирним шрифтом), про яку я включив підтримку в код у своєму початковому дописі (також згадувалося про це в моїй пов'язаній розробці наприкінці пост). Це було б дуже важко додати?
Райан Пешель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.