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

9

J'ai un très petit sous-ensemble de Markdown ainsi que du HTML personnalisé que j'aimerais analyser dans les composants React. 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 retourner à partir d'une fonction de rendu React (React rendra le tableau correctement au format HTML formaté)

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.

Ryan Peschel
la source
1
Et si l'entrée contient des éléments imbriqués, comme font _italic *and bold* then only italic_ and normal? Quel serait le résultat attendu? Ou ne sera-t-il jamais imbriqué?
trincot
1
Pas besoin de s'inquiéter de l'imbrication. C'est juste un démarque très basique pour les utilisateurs. Ce qui est le plus facile à mettre en œuvre me convient. Dans votre exemple, ce serait tout à fait correct si le pliage intérieur ne fonctionnait pas. Mais s'il est plus facile d'implémenter l'imbrication que de ne pas l'avoir, c'est bien aussi.
Ryan Peschel
1
Il est probablement plus simple d'utiliser simplement une solution standard
mb21
1
Je n'utilise pas de démarque cependant. C'est juste un sous-ensemble très similaire / petit (qui prend en charge quelques composants personnalisés, ainsi que les caractères gras, italiques, code et souligné non imbriqués). Les extraits que j'ai postés fonctionnent quelque peu, mais ne semblent pas très idéaux, et échouent dans certains cas triviaux (comme vous ne pouvez pas taper un seul astérisque comme celui-ci: asdf*sans qu'il disparaisse)
Ryan Peschel
1
eh bien ... analyser le markdown ou quelque chose comme le markdown n'est pas exactement une tâche facile ... les expressions régulières ne le coupent pas ... pour une question similaire concernant le HTML, voir stackoverflow.com/questions/1732348/…
mb21

Réponses:

1

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émarque, il 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 est nécessaire d'analyser les balises à la fois en gras et en italique , ma solution actuelle pourrait ne pas fonctionner dans ce cas.

Si vous avez cependant besoin de 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 que 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 parseMarkdownne 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 rawMarkdownaccessoire.

Pour voir un exemple de cela 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 sera toujours en mesure de 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 N(où Ncorrespond à la longueur de la balise multi-longueur la plus longue).

Il y aurait quelques ajustements à apporter au comportement de la méthode de boucle à l'intérieur 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 aurions besoin de le marquer comme notMultiLengthou quelque chose de similaire et de 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

Lukas Danin
la source
J'ai l'impression que cette solution est sur la bonne voie, mais elle semble avoir des problèmes avec la mise en place d'autres caractères de démarque dans d'autres. Par exemple, essayez de remplacer This must be *bold*par This must be *bo_ld*. Cela entraîne une mauvaise formation du code HTML résultant
Ryan Peschel
Le manque de tests appropriés a produit ceci = p, mon mauvais. Je le corrige déjà et je vais publier le résultat ici, semble être un problème simple à résoudre.
Lukas Danin
Oui merci. J'aime vraiment cette solution. Il semble très robuste et propre. Je pense que cela peut être un peu refactorisé pour encore plus d'élégance. Je pourrais essayer de jouer un peu avec.
Ryan Peschel
Terminé, en passant, j'ai peaufiné le code pour prendre en charge une manière beaucoup plus flexible de définir les balises de démarque et leurs valeurs JSX respectives.
Lukas Danin
Hé merci, ça a l'air super. Juste une dernière chose et je pense que ce sera parfait. Dans mon article d'origine, j'ai également une fonction pour les extraits de code (qui impliquent des triples backticks). Serait-il possible d'avoir un soutien pour cela également? Pour que les balises puissent éventuellement contenir plusieurs caractères? Une autre réponse a ajouté le support en remplaçant les instances de `` '' par un caractère rarement utilisé. Ce serait un moyen facile de le faire, mais je ne sais pas si c'est l'idéal.
Ryan Peschel
4

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

Mise à jour

Je viens de remarquer les composants React, je l'ai manqué au début. Donc, c'est génial pour vous, je crois que nous prenons la bibliothèque comme exemple et implémentons vos composants requis personnalisés pour le faire sans définir le HTML dangereusement. La bibliothèque est assez petite et claire. Aie du plaisir avec ça! :)

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

Le résultat: Résultat courant

Résultat du test d'expression régulière

Explication:

/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
  • Vous pouvez définir vos balises dans cette section: [*|!|_]une fois que l'une d'elles est mise en correspondance, elle sera capturée en tant que groupe et nommée "tag_begin".

  • Et (?<content>\w+)capture ensuite le contenu enveloppé par la balise.

  • La balise de fin doit être la même que la balise précédente, donc ici utilise \k<tag_begin>, et si elle a réussi le test, capturez-la en groupe et donnez-lui un nom "tag_end", c'est ce qui (?<tag_end>\k<tag_begin>))dit.

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 les 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. Composants React

Simon
la source
Malheureusement, je ne sais pas si cela fonctionne. Parce que j'ai besoin des composants et des éléments React eux-mêmes, pas de chaînes. Si vous regardez dans mon article d'origine, vous verrez que j'ajoute les éléments réels eux-mêmes à un tableau, pas des chaînes d'entre eux. Et utiliser dangerouslySetInnerHTML est dangereux car l'utilisateur peut entrer des chaînes malveillantes.
Ryan Peschel
Heureusement, il est très simple de convertir le remplacement de chaîne en composants React, j'ai mis à jour le code.
Simon
Hm? Je dois manquer quelque chose, car ce sont toujours des cordes de ma part. J'ai même fait un violon avec votre code. Si vous lisez la console.logsortie, vous verrez que le tableau est plein de chaînes, pas de véritables composants React: jsfiddle.net/xftswh41
Ryan Peschel
Honnêtement, je ne connais pas React, donc je ne peux pas tout faire parfaitement suivi par vos besoins, mais je pense que les informations sur la façon de résoudre votre question sont suffisantes, vous devez les mettre sur votre machine React et cela peut tout simplement aller.
Simon
La raison pour laquelle ce thread existe est parce qu'il semble être beaucoup plus difficile de les analyser en composants React (d'où le titre du thread spécifiant ce besoin exact). Les analyser en chaînes est assez trivial et vous pouvez simplement utiliser la fonction de remplacement de chaîne. Les chaînes ne sont pas une solution idéale car elles sont lentes et sensibles à XSS en raison de devoir appeler dangereusement
SetInnerHTML
0

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

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 si la chaîne est 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 démarre 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 dans le tableau où Tag = b de la carte. Si vous le faites <Tag>text</Tag>, réagissez en interne convertit en texte et poussez dans le tableau. Maintenant, le tableau est ["comment allez - vous ", vous ]. Rupture de la boucle intérieure
  • Maintenant, nous commençons la boucle externe à partir de là et aucune balise n'est trouvée, donc appuyez sur restant dans le tableau. Le tableau devient: ["comment ça va", toi , "fais"].
  • 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 la prise en charge de nouvelles balises

  • S'il s'agit d'un caractère comme * ou!, Ajoutez-les dans l' mapobjet avec la clé comme caractère et la valeur comme balise correspondante
  • S'ils sont composés de plusieurs caractères tels que `` '', créez une carte un à un avec des caractères moins fréquemment utilisés, puis insérez , 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 cela aide.

Sunil Chaudhary
la source
Salut, je regarde ça maintenant. Est-il possible d'utiliser également le support triple backtick? Donc, `` asdf '' fonctionnerait aussi bien pour les blocs de code?
Ryan Peschel
Ce sera le cas, mais certaines modifications pourraient être nécessaires. Actuellement, seule la correspondance d'un seul caractère est disponible pour * ou!. Cela doit être modifié un peu. Les blocs de code signifient essentiellement que asdfle rendu sera <pre>asdf</pre>sur fond sombre, non? Faites le moi savoir et je verrai. Même vous pouvez essayer maintenant. Une approche simple est la suivante: dans la solution ci-dessus, remplacez le `` `dans le texte par un caractère spécial tel que ^ ou ~ et mappez-le à la balise pre. Ensuite, cela fonctionnera bien. Une autre approche nécessite plus de travail
Sunil Chaudhary
Oui, exactement, en remplaçant `` asdf '' par <pre>asdf</pre>. Merci!
Ryan Peschel
@RyanPeschel Salut! J'ai également ajouté la preprise en charge des balises. Faites-moi savoir si cela fonctionne
Sunil Chaudhary
Solution intéressante (en utilisant le caractère rare). Un problème que je vois toujours est le manque de support pour l'échappement (tel que \ * asdf * n'est pas en gras), que j'ai inclus le support pour dans le code dans mon article d'origine (également mentionné dans mon élaboration liée à la fin de la Publier). Serait-ce très difficile à ajouter?
Ryan Peschel