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


9

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

这是我之前导致这个问题的问题


1
如果输入中包含嵌套项,该font _italic *and bold* then only italic_ and normal怎么办?预期的结果是什么?还是永远不会嵌套?
特里科特

1
无需担心嵌套。这只是供用户使用的最基本的降价促销。对我来说,最容易实现的方法都可以。在您的示例中,如果内部加粗不起作用,那将是完全可以的。但是,如果实现嵌套比没有嵌套更容易,那也没关系。
Ryan Peschel

1
这可能比较容易,只需使用一个现成的,现成的解决方案一样npmjs.com/package/react-markdown-it
MB21

1
我没有使用markdown。它只是其中的一个非常相似/很小的子集(它支持几个自定义组件,以及非嵌套的粗体,斜体,代码和下划线)。我发布的代码片段有些奏效,但看起来并不理想,并且在某些琐碎的情况下失败了(例如,您不能像这样键入单个星号:asdf*它不会消失)
Ryan Peschel

1
好...解析降价或类似的降价不正是一件容易的事......正则表达式不剪...了解有关HTML类似的问题,请参见stackoverflow.com/questions/1732348/...
MB21

Answers:


1

怎么运行的?

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

每当解析器检测到正在读取关键块(即'*'任何其他markdown标记)时,它将开始解析该元素的块,直到解析器找到其结束标记为止。

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

注意事项

您尚未指定,否则我可能会误解您的需求,如果有必要解析粗体和斜体的标记,则当前的解决方案在这种情况下可能无法正常工作。

但是,如果需要使用上述条件,请在此处注释,然后我将对代码进行调整。

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

标签不再是硬编码的,它们是一张地图,您可以在其中轻松扩展以满足您的需求。

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

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

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

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

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

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

乍一看似乎很麻烦,但是根据您的用例,使用此路由我看不到任何重大问题。

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

好吧,我们可以轻松地跟踪最后一个NN对应于最长的多长度标签的长度)块。

将对循环内部方法的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


我觉得这个解决方案是在正确的轨道上,但是将其他markdown字符放在其他字符中似乎有问题。例如,尝试更换This must be *bold*This must be *bo_ld*。它导致生成的HTML格式不正确
Ryan Peschel

缺乏适当的测试导致了= p,这很糟糕。我已经在解决它,并将结果发布在这里,似乎是一个简单的问题。
卢卡斯·丹宁

是的,谢谢。我确实很喜欢这种解决方案。看起来非常坚固和干净。我认为可以对其进行一些重构,以获得更多的优雅。我可能会尝试弄乱它。
Ryan Peschel

顺便说一句,我已经完成了代码的调整,以支持一种更加灵活的方式来定义markdown标签及其各自的JSX值。
卢卡斯·丹宁

嘿,谢谢,这看起来很棒。最后一件事,我认为这将是完美的。在我的原始帖子中,我也具有用于代码段的功能(涉及到三个反引号)。是否也可以为此提供支持?这样标签可以选择是多个字符?另一个答复通过用很少使用的字符替换```实例来增加了支持。那将是一个简单的方法,但是不确定是否理想。
Ryan Peschel

4

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

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

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

更新资料

刚注意到React组件,一开始就没注意到它。因此,这对您很有用,我相信以该库为例,并实现您自定义的必需组件即可完成它,而不会危险地设置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,function),它可以将捕获的组作为参数,我们使用这些捕获的项来查找表并生成替换字符串。

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


不幸的是,我不确定这是否可行。因为我需要实际的React组件和元素本身,而不是它们的字符串。如果您查看我的原始文章,您会发现我正在将实际元素本身添加到数组中,而不是它们的字符串。而且危险地使用SetInnerHTML十分危险,因为用户可能输入恶意字符串。
Ryan Peschel

幸运的是,将字符串替换转换为React组件非常简单,我已经更新了代码。
西蒙(Simon)

嗯?我一定想念一些东西,因为它们仍然对我不利。我什至摆弄了您的代码。如果您阅读console.log输出,您会看到数组中充满了字符串,而不是实际的React组件:jsfiddle.net/xftswh41
Ryan Peschel

老实说,我不了解React,所以我不能使所有事情都完全符合您的需求,但是我认为有关如何解决问题的信息就足够了,您需要将它们放到React机器上,这样就可以了。
西蒙

之所以存在该线程,是因为将其解析为React组件似乎要困难得多(因此线程标题指定了确切的需求)。将它们解析为字符串非常简单,您可以只使用字符串替换功能。字符串不是理想的解决方案,因为它们很慢并且由于必须危险地调用而很容易受到XSS的影响
SetInnerHTML

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.

方法

逐字符搜索markdown元素。一旦遇到一个,搜索相同的结束标记,然后将其转换为html。

片段中支持的标签

  • 胆大
  • 斜体字
  • EM
  • 预先

代码段的输入和输出:

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

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

添加新标签支持

  • 如果它们是*或!这样的一个字符,则将它们添加到map对象中,并以key作为字符,并将value作为对应的标签
  • 如果它们是多个字符,例如```,请使用一些不经常使用的char创建一对一的映射,然后插入(原因:目前,基于字符的搜索方法会破坏一个以上char。 ,也可以通过改进逻辑来加以注意)

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

希望能帮助到你。


嗨,现在看这个。是否可以与三重反勾支持一起使用?那么```asdf``对于代码块同样适用吗?
Ryan Peschel

它将,但是可能需要进行一些修改。当前,*或!仅用于单个字符匹配。这需要一点点修改。代码块基本上意味着asdf将以<pre>asdf</pre>深色背景呈现,对吗?让我知道这一点,我将会看到。即使您现在也可以尝试。一种简单的方法是:在上述解决方案中,用特殊字符(例如^或〜)替换文本中的```并将其映射到pre标签。然后它将正常工作。另一种方法需要一些更多的工作
苏尼尔·乔杜里

是的,正好用替换```asdf``` <pre>asdf</pre>。谢谢!
Ryan Peschel

@RyanPeschel嗨!还添加了pre标签支持。让我知道它是否有效
Sunil Chaudhary

有趣的解决方案(使用稀有字符)。我仍然看到的一个问题是缺乏对转义的支持(例如\ * asdf *不加粗),这在我的原始帖子中也包含了对代码的支持(在末尾的链接阐述中也提到了这一点)。发布)。很难添加吗?
Ryan Peschel
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.