J'ai un très petit sous-ensemble de Markdown ainsi que du HTML personnalisé que je voudrais analyser en React composants. Par exemple, je voudrais transformer cette chaîne suivante:
hello *asdf* *how* _are_ you !doing! today
Dans le tableau suivant:
[ "hello ", <strong>asdf</strong>, " ", <strong>how</strong>, " ", <em>are</em>, " you ", <MyComponent onClick={this.action}>doing</MyComponent>, " today" ]
puis le renvoyer à partir d'une fonction de rendu React (React rendra le tableau correctement au format HTML)
Fondamentalement, je veux donner aux utilisateurs la possibilité d'utiliser un ensemble très limité de Markdown pour transformer leur texte en composants stylisés (et dans certains cas mes propres composants!)
Il est imprudent de mettre dangereusement SetInnerHTML, et je ne veux pas apporter de dépendance externe, car ils sont tous très lourds, et je n'ai besoin que de fonctionnalités très basiques.
Je fais actuellement quelque chose comme ça, mais c'est très fragile et ne fonctionne pas dans tous les cas. Je me demandais s'il y avait une meilleure façon:
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;
}
Voici ma question précédente qui a conduit à celle-ci.
Cela fonctionne en lisant un morceau de chaîne par morceau, ce qui n'est peut-être pas la meilleure solution pour les très longues chaînes.
Chaque fois que l'analyseur détecte qu'un morceau critique est en cours de lecture, c'est-à-dire '*'
ou toute autre balise de démarquage, elle commence à analyser des morceaux de cet élément jusqu'à ce que l'analyseur trouve sa balise de fermeture.
Il fonctionne sur des chaînes multi-lignes, voir le code par exemple.
Vous n'avez pas précisé, ou j'aurais pu mal comprendre vos besoins, s'il y a la nécessité d'analyser des balises à la fois en gras et italique, ma solution actuelle pourrait ne pas fonctionner dans ce cas.
Si vous devez cependant travailler avec les conditions ci-dessus, commentez ici et je modifierai le code.
Les balises ne sont plus codées en dur, mais plutôt une carte où vous pouvez facilement étendre pour répondre à vos besoins.
Correction des bugs que vous avez mentionnés dans les commentaires, merci d'avoir signalé ce problème = p
Bien que la méthode parseMarkdown
ne prenne pas encore en charge les balises multi-longueur, nous pouvons facilement remplacer ces balises multi-longueur par un simple string.replace
lors de l'envoi de notre rawMarkdown
prop.
Pour voir un exemple de ceci dans la pratique, regardez le ReactDOM.render
, situé à la fin du code.
Même si votre application prend en charge plusieurs langues, il existe toujours des caractères Unicode non valides que JavaScript détecte, par exemple: "\uFFFF"
n'est pas un unicode valide, si je me souviens bien, mais JS pourra toujours le comparer ("\uFFFF" === "\uFFFF" = true
)
Cela peut sembler piraté au début, mais, selon votre cas d'utilisation, je ne vois aucun problème majeur en utilisant cette route.
Eh bien, nous pourrions facilement suivre les derniers morceaux de N
(où N
correspond à la longueur de la balise multi-longueur la plus longue).
Il y aurait quelques ajustements à apporter au comportement de la boucle à l'intérieur de la méthode parseMarkdown
, c'est-à-dire vérifier si le morceau actuel fait partie d'une balise de plusieurs longueurs, s'il l'utilise comme balise; sinon, dans des cas comme ``k
, nous devons le marquer comme notMultiLength
ou quelque chose de similaire et pousser ce morceau comme contenu.
// 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'));
Lien vers le code (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv
Lien vers le code (Vanilla/babel) https://codepen.io/ludanin/pen/eYmBvXw
Il semble que vous recherchiez une petite solution très basique. Pas des "super-monstres" comme react-markdown-it
:)
Je voudrais vous recommander https://github.com/developit/snarkdown qui a l'air assez léger et agréable! Juste 1 Ko et extrêmement simple, vous pouvez l'utiliser et l'étendre si vous avez besoin d'autres fonctionnalités de syntaxe.
Liste des balises prises en charge https://github.com/developit/snarkdown/blob/master/src/index.js#L1
Je viens de remarquer les composants React, je l'ai manqué au début. Donc, c'est génial pour vous, je crois prendre l'exemple de la bibliothèque et implémenter vos composants personnalisés requis pour le faire sans paramétrer HTML dangereusement. La bibliothèque est assez petite et claire. Aie du plaisir avec ça! :)
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);
Explication:
/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
Vous pouvez définir vos tags dans cette section: [*|!|_]
, une fois que l'un d'eux correspond, il sera capturé en tant que groupe et nommé "tag_begin".
Puis (?<content>\w+)
capture le contenu enveloppé par la balise.
La balise de fin doit être identique à la balise précédente, donc ici utilise \k<tag_begin>
, et s'il a réussi le test, capturez-le en groupe et donnez-lui un nom "tag_end", c'est ce que (?<tag_end>\k<tag_begin>))
Est en train de dire.
Dans le JS, vous avez configuré une table comme celle-ci:
var table = {
"*":{
"begin":"<strong>",
"end":"</strong>"
},
"_":{
"begin":"<em>",
"end":"</em>"
},
"!":{
"begin":"<MyComponent onClick={this.action}>",
"end":"</MyComponent>"
},
};
Utilisez ce tableau pour remplacer les balises correspondantes.
Sting.replace a une surcharge String.replace (regexp, fonction) qui peut prendre des groupes capturés comme paramètres, nous utilisons ces éléments capturés pour rechercher la table et générer la chaîne de remplacement.
[Mise à jour]
J'ai mis à jour le code, j'ai gardé le premier au cas où quelqu'un d'autre n'aurait pas besoin de composants réactifs, et vous pouvez voir qu'il y a peu de différence entre eux.
A working solution purely using Javascript and ReactJs without dangerouslySetInnerHTML.
Recherche caractère par caractère des éléments de démarque. Dès que l'on en rencontre, recherchez la balise de fin pour la même, puis convertissez-la en html.
JsFiddle: https://jsfiddle.net/sunil12738/wg7emcz1/58/
Code:
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>
Supposons que la chaîne soit How are *you* doing?
Conserver un mappage des symboles aux balises
map = {
"*": "b"
}
["How are "]
et lancez la boucle intérieure jusqu'à ce que vous trouviez la prochaine *.Now next between * and * needs to be bold
, nous les convertissons en élément html par texte et poussons directement le tableau où Tag = b de la carte. Si tu fais <Tag>text</Tag>
, réagit en interne se transforme en texte et Poussez dans le tableau. Maintenant, le tableau est ["comment sont", tu]. Rupture de la boucle intérieureHow are <b>you</b> doing?
Note: <b>you</b> is html and not text
Remarque : l'imbrication est également possible. Nous devons appeler la logique ci-dessus en récursivité
map
avec la clé comme caractère et la valeur comme balise correspondante Prend-il en charge l'imbrication? Non
Prend-il en charge tous les cas d'utilisation mentionnés par OP? Oui
J'espère que ça aide.
vous pouvez le faire comme ceci:
//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};
}