我有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促成了这一点。
答案 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);
说明:
/(?<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),它可以将捕获的组作为参数,我们使用这些捕获的项目查找表并生成替换字符串。
答案 2 :(得分:1)
它的工作原理是逐块读取一个字符串,这可能不是 真正长字符串的最佳解决方案。
只要解析器检测到关键块正在读取,即'*'
或
任何其他markdown标记,它将开始解析该元素的块,直到
解析器找到其结束标记。
它适用于多行字符串,请参见示例代码。
您尚未指定,或者如果有,我可能会误解您的需求 解析粗体和斜体 标签的必要性 解决方案在这种情况下可能不起作用。
但是,如果需要使用上述条件,请在此处评论 然后我将调整代码。
标记不再是硬编码的,它们是可以轻松扩展的地图 满足您的需求。
修复了您在评论中提到的错误,感谢您指出此问题= p
尽管方法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。
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作为对应的标签 它是否支持嵌套?没有
它是否支持OP提到的所有用例?是的
希望有帮助。