Come analizzare un piccolo sottoinsieme di Markdown nei componenti di React?


9

Ho un piccolo sottoinsieme di Markdown insieme ad alcuni HTML personalizzati che vorrei analizzare nei componenti di React. Ad esempio, vorrei trasformare questa stringa seguente:

hello *asdf* *how* _are_ you !doing! today

Nel seguente array:

[ "hello ", <strong>asdf</strong>, " ", <strong>how</strong>, " ", <em>are</em>, " you ", <MyComponent onClick={this.action}>doing</MyComponent>, " today" ]

e quindi restituirlo da una funzione di rendering di React (React renderà l'array correttamente come HTML formattato)

Fondamentalmente, voglio dare agli utenti la possibilità di utilizzare un set molto limitato di Markdown per trasformare il loro testo in componenti con stile (e in alcuni casi i miei componenti!)

Non è saggio mettere pericolosamente SetInnerHTML e non voglio introdurre una dipendenza esterna, perché sono tutti molto pesanti e ho solo bisogno di funzionalità molto basilari.

Attualmente sto facendo qualcosa del genere, ma è molto fragile e non funziona in tutti i casi. Mi chiedevo se ci fosse un modo migliore:

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

Ecco la mia domanda precedente che ha portato a questo.


1
Cosa succede se l'input ha elementi nidificati, come font _italic *and bold* then only italic_ and normal? Quale sarebbe il risultato atteso? O non sarà mai nidificato?
trincot

1
Non devi preoccuparti di nidificare. È solo un markdown di base che gli utenti possono usare. Qualunque cosa sia più facile da implementare, va bene per me. Nel tuo esempio, sarebbe del tutto perfetto se l'audacia interna non funzionasse. Ma se è più semplice implementare l'annidamento che non averlo, va bene lo stesso.
Ryan Peschel,

1
Probabilmente è più semplice utilizzare una soluzione standard
mb21

1
Non sto usando markdown però. È solo un sottoinsieme molto simile / piccolo (che supporta un paio di componenti personalizzati, insieme a grassetto, corsivo, codice, sottolineatura non nidificati). Gli snippet che ho pubblicato funzionano in qualche modo, ma non sembrano molto ideali, e falliscono in alcuni casi banali, (come se non potessi digitare un singolo asterick come questo: asdf*senza che scompaia)
Ryan Peschel,

1
beh ... parsing Markdown o qualcosa di simile Markdown non è esattamente un compito facile ... espressioni regolari non tagliare ... per una domanda simile per quanto riguarda html, vedere stackoverflow.com/questions/1732348/...
MB21

Risposte:


1

Come funziona?

Funziona leggendo un pezzo di stringa per pezzo, che potrebbe non essere la soluzione migliore per stringhe molto lunghe.

Ogni volta che il parser rileva che viene letto un pezzo critico, ovvero '*'qualsiasi altro tag markdown, inizia ad analizzare blocchi di questo elemento fino a quando il parser trova il suo tag di chiusura.

Funziona su stringhe multilinea, ad esempio il codice.

Avvertenze

Non hai specificato, o avrei potuto fraintendere le tue esigenze, se c'è la necessità di analizzare tag sia in grassetto che in corsivo , la mia soluzione attuale potrebbe non funzionare in questo caso.

Se hai bisogno, tuttavia, di lavorare con le condizioni di cui sopra, commenta qui e ottimizzerò il codice.

Primo aggiornamento: modifica il modo in cui vengono trattati i tag markdown

I tag non sono più codificati, ma sono una mappa in cui è possibile estenderli facilmente per adattarli alle proprie esigenze.

Risolti i bug che hai citato nei commenti, grazie per aver segnalato questo problema = p

Secondo aggiornamento: tag di markdown multi-lunghezza

Il modo più semplice per raggiungere questo obiettivo: sostituire i caratteri multi-lunghezza con un unicode usato raramente

Sebbene il metodo parseMarkdownnon supporti ancora i tag multi-lunghezza, possiamo facilmente sostituire quei tag multi-lunghezza con un semplice string.replace quando inviamo il nostro rawMarkdownprop.

Per vedere un esempio di questo in pratica, guarda ReactDOM.render, situato alla fine del codice.

Anche se l'applicazione non supportare più lingue, ci sono caratteri Unicode validi che JavaScript rileva ancora, es .: "\uFFFF"non è un unicode valida, se ricordo bene, ma JS saranno ancora in grado di confrontarlo ( "\uFFFF" === "\uFFFF" = true)

All'inizio potrebbe sembrare hack-y ma, a seconda del tuo caso d'uso, non vedo alcun problema importante usando questo percorso.

Un altro modo per raggiungere questo obiettivo

Bene, potremmo facilmente rintracciare gli ultimi pezzi N(dove Ncorrisponde alla lunghezza del tag multi-lunghezza più lungo).

Ci sarebbero alcune modifiche da apportare al modo in cui parseMarkdownsi comporta il metodo del ciclo interno , vale a dire verificare se il blocco corrente fa parte di un tag multi-lunghezza, se lo si utilizza come tag; altrimenti, in casi del genere ``k, avremmo bisogno di contrassegnarlo come notMultiLengtho qualcosa di simile e spingere quel pezzo come contenuto.

Codice

// 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'));

Link al codice (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv

Link al codice (vaniglia / babele) https://codepen.io/ludanin/pen/eYmBvXw


Sento che questa soluzione è sulla buona strada, ma sembra che abbia problemi con l'inserimento di altri personaggi markdown all'interno di altri. Ad esempio, prova a sostituire This must be *bold*con This must be *bo_ld*. Fa sì che l'HTML risultante sia malformato
Ryan Peschel il

La mancanza di test adeguati ha prodotto questo = p, mio ​​male. Lo sto già risolvendo e pubblicherò il risultato qui, sembra un semplice problema da risolvere.
Lukas Danin,

Si, grazie. Mi piace davvero questa soluzione però. Sembra molto robusto e pulito. Penso che possa essere riformulato un po 'anche se per ancora più eleganza. Potrei provare a scherzarci un po '.
Ryan Peschel,

A proposito, ho modificato il codice per supportare un modo molto più flessibile di definire i tag markdown e i loro rispettivi valori JSX.
Lukas Danin,

Ehi grazie, sembra fantastico. Solo un'ultima cosa e penso che sarà perfetto. Nel mio post originale ho una funzione anche per i frammenti di codice (che coinvolgono triple backtick). Sarebbe possibile avere supporto anche per quello? In modo che i tag possano facoltativamente essere più caratteri? Un'altra risposta ha aggiunto il supporto sostituendo le istanze di `` '' con un carattere usato raramente. Sarebbe un modo semplice per farlo, ma non sono sicuro che sia l'ideale.
Ryan Peschel,

4

Sembra che tu stia cercando una soluzione molto semplice. Non "super-mostri" come react-markdown-it:)

Vorrei raccomandarti https://github.com/developit/snarkdown che sembra piuttosto leggero e carino! Solo 1kb ed estremamente semplice, puoi usarlo ed estenderlo se hai bisogno di altre funzionalità di sintassi.

Elenco di tag supportati https://github.com/developit/snarkdown/blob/master/src/index.js#L1

Aggiornare

Ho appena notato i componenti di reazione, all'inizio l'ho perso. Quindi è fantastico per te credo di prendere la libreria come esempio e implementare i componenti personalizzati richiesti per farlo senza impostare pericolosamente HTML. La biblioteca è piuttosto piccola e chiara. Divertiti con esso! :)


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);

Il risultato: Risultato corrente

Risultati del test Regexp

Spiegazione:

/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
  • Puoi definire i tuoi tag in questa sezione [*|!|_]:, una volta che uno di essi è abbinato, verrà catturato come gruppo e chiamato come "tag_begin".

  • E quindi (?<content>\w+)acquisisce il contenuto racchiuso dal tag.

  • Il tag finale deve essere uguale a quello precedentemente abbinato, quindi utilizza qui \k<tag_begin>, e se ha superato il test, acquisiscilo come gruppo e assegnagli un nome "tag_end", questo è ciò che (?<tag_end>\k<tag_begin>))sta dicendo.

In JS hai creato una tabella come questa:

var table = {
  "*":{
    "begin":"<strong>",
    "end":"</strong>"
    },
  "_":{
    "begin":"<em>",
    "end":"</em>"
    },
  "!":{
    "begin":"<MyComponent onClick={this.action}>",
    "end":"</MyComponent>"
    },

  };

Utilizzare questa tabella per sostituire i tag corrispondenti.

Sting.replace ha un sovraccarico String.replace (regexp, funzione) che può prendere gruppi acquisiti come parametri, usiamo questi elementi catturati per cercare la tabella e generare la stringa di sostituzione.

[Aggiornamento]
Ho aggiornato il codice, ho conservato il primo nel caso in cui qualcun altro non abbia bisogno di reagire ai componenti, e puoi vedere che c'è poca differenza tra loro. Componenti di React


Sfortunatamente non sono sicuro che funzioni. Perché ho bisogno degli stessi componenti ed elementi React stessi, non di loro stringhe. Se guardi nel mio post originale vedrai che sto aggiungendo gli elementi reali a un array, non stringhe di essi. L'uso di dangerouslySetInnerHTML è pericoloso in quanto l'utente potrebbe inserire stringhe dannose.
Ryan Peschel,

Fortunatamente è molto semplice convertire la sostituzione di stringa in componenti React, ho aggiornato il codice.
Simon,

Hm? Devo mancare qualcosa, perché sono ancora stringhe sulla mia parte. Ho persino fatto un violino con il tuo codice. Se leggi l' console.logoutput vedrai che l'array è pieno di stringhe, non di effettivi componenti React: jsfiddle.net/xftswh41
Ryan Peschel,

Onestamente non conosco React, quindi non posso rendere tutto perfettamente seguito dalle tue esigenze, ma penso che le informazioni su come risolvere la tua domanda siano sufficienti, devi metterle sulla tua macchina React e può andare.
Simon,

Il motivo per cui esiste questo thread è perché sembra essere molto più difficile analizzarli nei componenti di React (da qui il titolo del thread che specifica quell'esatto bisogno). Analizzarli in stringhe è abbastanza banale e puoi semplicemente usare la funzione di sostituzione delle stringhe. Le stringhe non sono la soluzione ideale perché sono lente e suscettibili all'XSS a causa della necessità di chiamare pericolosamente
SetInnerHTML

0

puoi farlo in questo modo:

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

Approccio

Ricerca carattere per carattere per gli elementi di marcatura. Non appena ne incontriamo uno, cerca il tag finale per lo stesso e poi convertilo in HTML.

Tag supportati nello snippet

  • grassetto
  • corsivo
  • em
  • pre

Input e output dallo snippet:

JsFiddle: https://jsfiddle.net/sunil12738/wg7emcz1/58/

Codice:

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>

Spiegazione dettagliata (con esempio):

Supponiamo che stringa sia How are *you* doing? Mantieni un mapping per i simboli ai tag

map = {
 "*": "b"
}
  • Ripeti fino a trovare prima *, il testo prima di quello è una stringa normale
  • Spingilo all'interno dell'array. La matrice diventa ["How are "]e avvia il ciclo interno fino a trovare il prossimo *.
  • Now next between * and * needs to be bold, li convertiamo in elemento html per testo e inseriamo direttamente l'array dove Tag = b dalla mappa. In tal caso <Tag>text</Tag>, reagisci convertendo internamente in testo e spingi in array. Ora array è ["come stai", tu ]. Interruzione dal circuito interno
  • Ora iniziamo il ciclo esterno da lì e non vengono trovati tag, quindi spingi rimanendo nell'array. La matrice diventa: ["come stai", tu , "facendo"].
  • Rendering sull'interfaccia utente How are <b>you</b> doing?
    Note: <b>you</b> is html and not text

Nota : è possibile anche l'annidamento. Dobbiamo chiamare la logica di cui sopra in ricorsione

Per aggiungere nuovi tag supporto

  • Se sono un carattere come * o!, Aggiungili mapnell'oggetto con chiave come carattere e valore come tag corrispondente
  • Se sono più di un carattere come `` '', crea una mappa uno a uno con alcuni caratteri usati meno frequentemente e poi inserisci (Motivo: attualmente, l'approccio basato sul carattere per ricerca di caratteri e quindi più di un carattere si romperà. Tuttavia , che può anche essere curato migliorando la logica)

Supporta la nidificazione? No
Supporta tutti i casi d'uso menzionati da OP? sì

Spero che sia d'aiuto.


Ciao, guarda adesso. È possibile utilizzare anche questo con supporto backtick triplo? Quindi `` asdf`` funzionerebbe anche per i blocchi di codice?
Ryan Peschel,

Sarà ma potrebbero essere necessarie alcune modifiche. Attualmente, esiste solo la corrispondenza di un singolo carattere per * o!. Questo deve essere leggermente modificato. In pratica i blocchi di codice significano asdfche saranno resi <pre>asdf</pre>con uno sfondo scuro, giusto? Fammi sapere e vedrò. Anche tu puoi provare ora. Un approccio semplice è: nella soluzione precedente, sostituisci `` `nel testo con un carattere speciale come ^ o ~ e mappalo al pre tag. Quindi funzionerà bene. Un altro approccio ha bisogno di altro lavoro
Sunil Chaudhary,

Sì, esattamente, sostituendo `` asdf``` con <pre>asdf</pre>. Grazie!
Ryan Peschel,

@RyanPeschel Hi! Ho aggiunto anche il presupporto per i tag. Fammi sapere se funziona
Sunil Chaudhary,

Soluzione interessante (usando il carattere raro). Un problema che ancora vedo è la mancanza di supporto per la fuga (tale che \ * asdf * non è in grassetto), che ho incluso il supporto nel codice nel mio post originale (anche menzionato nella mia elaborazione collegata alla fine del inviare). Sarebbe molto difficile aggiungere?
Ryan Peschel,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.