如何将一小部分Markdown解析为React组件?

时间:2019-12-05 21:03:30

标签: javascript arrays regex reactjs markdown

我有Markdown的很小一部分,还有一些我想解析为React组件的自定义html。例如,我想将以下字符串转换为

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;
}

Here is my previous question促成了这一点。

5 个答案:

答案 0 :(得分:4)

您似乎正在寻找一个非常小的基本解决方案。不是像react-markdown-it这样的“超级怪物”:)

我想向您推荐https://github.com/developit/snarkdown,它看上去很轻巧而且不错!仅1kb,非常简单,如果需要其他语法功能,则可以使用并扩展它。

受支持的标签列表https://github.com/developit/snarkdown/blob/master/src/index.js#L1

更新

只是注意到了关于React组件的信息,所以一开始就忽略了它。因此,这对您来说很棒,我相信以该库为例,并实现您自定义的必需组件即可完成它,而不会危险地设置HTML。该库非常小而清晰。玩得开心! :)

答案 1 :(得分: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);

结果: Running result

Regexp test result

说明:

/(?<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, function),它可以将捕获的组作为参数,我们使用这些捕获的项目查找表并生成替换字符串。

[更新]
我已经更新了代码,保留了第一个,以防其他人不需要React组件,并且您会发现它们之间几乎没有区别。 React Components

答案 2 :(得分:1)

它如何工作?

它的工作原理是逐块读取一个字符串,这可能不是 真正长字符串的最佳解决方案。

只要解析器检测到关键块正在读取,即'*'或 任何其他markdown标记,它将开始解析该元素的块,直到 解析器找到其结束标记。

它适用于多行字符串,请参见示例代码。

注意事项

您尚未指定,或者如果有,我可能会误解您的需求 解析粗体和斜体 标签的必要性 解决方案在这种情况下可能不起作用。

但是,如果需要使用上述条件,请在此处评论 然后我将调整代码。

第一次更新:调整如何处理降价标签

标记不再是硬编码的,它们是可以轻松扩展的地图 满足您的需求。

修复了您在评论中提到的错误,感谢您指出此问题= p

第二次更新:多长减价标签

实现此目的的最简单方法:用很少使用的unicode替换多长度字符

尽管方法parseMarkdown尚不支持多长度标签, 我们可以轻松地用一个简单的string.replace替换那些多长度标签 发送我们的rawMarkdown道具时。

要在实践中查看此示例,请查看位于ReactDOM.render 在代码末尾。

即使您的应用程序支持,也存在无效语言 JavaScript仍然可以检测到的unicode字符,例如:"\uFFFF"是无效的 unicode,如果我没记错的话,但是JS仍然可以进行比较("\uFFFF" === "\uFFFF" = true

乍一看似乎很hack,但是根据您的用例,我看不到 使用此路线可以解决任何主要问题。

实现这一目标的另一种方法

好吧,我们可以轻松地跟踪最后一个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

链接到代码(香草/ babel)https://codepen.io/ludanin/pen/eYmBvXw

答案 3 :(得分: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};
  }

答案 4 :(得分:0)

A working solution purely using Javascript and ReactJs without dangerouslySetInnerHTML.

方法

按字符搜索降价元素。碰到一个,立即搜索结束标记,然后将其转换为html。

代码段中支持的标签

  • 粗体
  • 斜体
  • em
  • pre

代码段的输入和输出:

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>,则内部反应会转换为 text 并推入数组。现在数组是[“怎么样”,]。摆脱内循环
  • 现在,我们从那里开始外循环,没有找到标签,因此将剩余的内容推入数组中。数组变成:[“怎么样”,,“正在做”]。
  • 在用户界面How are <b>you</b> doing?上渲染
    Note: <b>you</b> is html and not text

注意:也可以嵌套。我们需要递归调用上述逻辑

要添加新标签支持

  • 如果它们是*或!之类的字符,则将它们添加到map对象中,并以key作为字符,并将value作为对应的标签
  • 如果它们是多个字符(例如```),请使用一些不常用的char创建一对一的映射,然后插入(原因:当前,基于字符的搜索方法将导致不止一个char中断。但是,也可以通过改进逻辑来解决此问题)

它是否支持嵌套?没有
它是否支持OP提到的所有用例?是的

希望有帮助。