web-dev-qa-db-fra.com

Comment analyser un petit sous-ensemble de Markdown en composants React?

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.

9
Ryan Peschel

Comment ça fonctionne?

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.

Avertissements

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.

Première mise à jour: modifie la façon dont les balises de démarque sont traitées

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

Deuxième mise à jour: balises de démarque multi-longueur

Le moyen le plus simple d'y parvenir: remplacer les caractères multi-longueurs par un unicode rarement utilisé

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.

Une autre façon d'y parvenir

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.

Code

// 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

1
Lukas Danin

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

Mettre à jour

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! :)

4
Alexandr Shurigin
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);

Le résultat: Running result

Résultat du test Regexp

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. React Components

3
Simon

A working solution purely using Javascript and ReactJs without dangerouslySetInnerHTML.

Approche

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.

Balises prises en charge dans l'extrait de code

  • audacieux
  • italique
  • em
  • pré

Entrée et sortie de l'extrait:

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>

Explication détaillée (avec exemple):

Supposons que la chaîne soit How are *you* doing? Conserver un mappage des symboles aux balises

map = {
 "*": "b"
}
  • Boucle jusqu'à ce que vous trouviez d'abord *, le texte avant c'est une chaîne normale
  • Poussez ce tableau à l'intérieur. Le tableau devient ["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érieure
  • Maintenant, nous commençons la boucle externe à partir de là et aucune balise n'est trouvée, donc Push reste dans le tableau. Le tableau devient: ["comment sont", tu, " Faire"].
  • Rendu sur l'interface utilisateur How 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é

Pour ajouter de nouvelles balises

  • S'il s'agit d'un caractère comme * ou!, Ajoutez-les dans l'objet map avec la clé comme caractère et la valeur comme balise correspondante
  • S'il s'agit de plusieurs caractères tels que `` '', créez une carte un à un avec des caractères moins fréquemment utilisés, puis insérez (Raison: actuellement, approche basée sur la recherche caractère par caractère et donc plus d'un caractère se brisera. Cependant , cela peut aussi être pris en charge en améliorant la logique)

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.

0
Sunil Chaudhary

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};
  }
0
Jatin Parmar