¿Cómo analizar un pequeño subconjunto de Markdown en componentes React?

9

Tengo un subconjunto muy pequeño de Markdown junto con algunos html personalizados que me gustaría analizar en los componentes React. Por ejemplo, me gustaría convertir esta siguiente cadena:

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

En la siguiente matriz:

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

y luego lo devuelve desde una función de representación React (React representará la matriz correctamente como HTML formateado)

Básicamente, quiero darles a los usuarios la opción de usar un conjunto muy limitado de Markdown para convertir su texto en componentes con estilo (¡y en algunos casos mis propios componentes!)

Es imprudente peligrosamente SetInnerHTML, y no quiero traer una dependencia externa, porque todos son muy pesados ​​y solo necesito una funcionalidad muy básica.

Actualmente estoy haciendo algo como esto, pero es muy frágil y no funciona en todos los casos. Me preguntaba si había una mejor manera:

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

Aquí está mi pregunta anterior que condujo a esta.

Ryan Peschel
fuente
1
¿Qué pasa si la entrada tiene elementos anidados, como font _italic *and bold* then only italic_ and normal? ¿Cuál sería el resultado esperado? ¿O nunca estará anidado?
trincot
1
No hay que preocuparse por anidar. Es un descuento muy básico para que lo usen los usuarios. Lo que sea más fácil de implementar está bien para mí. En su ejemplo, estaría completamente bien si la moldura interna no funcionara. Pero si es más fácil implementar el anidamiento que no tenerlo, entonces está bien también.
Ryan Peschel
1
Probablemente sea más fácil usar una solución estándar como npmjs.com/package/react-markdown-it
mb21
1
Sin embargo, no estoy usando Markdown. Es solo un subconjunto muy similar / pequeño (que admite un par de componentes personalizados, junto con negrita, cursiva, código, subrayado no anidados). Los fragmentos que publiqué de alguna manera funcionan, pero no parecen muy ideales, y fallan en algunos casos triviales (como si no pudieras escribir un solo asterisco como este: asdf*sin que desaparezca)
Ryan Peschel
1
bueno ... analizar Markdown o algo así como Markdown no es exactamente una tarea fácil ... las expresiones regulares no son suficientes ... para una pregunta similar con respecto a html, consulte stackoverflow.com/questions/1732348/…
mb21

Respuestas:

1

¿Cómo funciona?

Funciona leyendo un trozo de trozo por trozo, que podría no ser la mejor solución para cadenas realmente largas.

Cada vez que el analizador detecta que se está leyendo un fragmento crítico, es decir '*' o cualquier otra etiqueta de reducción, comienza a analizar fragmentos de este elemento hasta que el analizador encuentra su etiqueta de cierre.

Funciona en cadenas de varias líneas, consulte el código, por ejemplo.

Advertencias

No ha especificado, o podría haber entendido mal sus necesidades, si existe la necesidad de analizar etiquetas que están en negrita y cursiva , mi solución actual podría no funcionar en este caso.

Sin embargo, si necesita trabajar con las condiciones anteriores, simplemente comente aquí y modificaré el código.

Primera actualización: modifica cómo se tratan las etiquetas de descuento

Las etiquetas ya no están codificadas, sino que son un mapa donde se puede extender fácilmente para satisfacer sus necesidades.

Se corrigieron los errores que mencionaste en los comentarios, gracias por señalar estos problemas = p

Segunda actualización: etiquetas de descuento de longitud múltiple

La forma más fácil de lograr esto: reemplazando caracteres de varias longitudes con un Unicode raramente usado

Aunque el método parseMarkdownaún no admite etiquetas de varias longitudes, podemos reemplazar fácilmente esas etiquetas de múltiples longitudes con un simple string.replace al enviar nuestrorawMarkdown accesorio.

Para ver un ejemplo de esto en la práctica, mire el ReactDOM.render , ubicado al final del código.

Incluso si su aplicación hace soportar múltiples idiomas, hay caracteres Unicode no válidos que JavaScript sigue detectando, ej .: "\uFFFF"no es un Unicode válida, si no recuerdo mal, pero JS todavía será capaz de compararlo ("\uFFFF" === "\uFFFF" = true )

Puede parecer hack-y al principio, pero, dependiendo de su caso de uso, no veo ningún problema importante al usar esta ruta.

Otra forma de lograr esto

Bueno, podríamos rastrear fácilmente el último N(dondeN fragmentos corresponde a la longitud de la etiqueta de longitud múltiple más larga).

Habría que hacer algunos ajustes en la forma en que se parseMarkdowncomporta el método del bucle interno , es decir, verificar si el fragmento actual es parte de una etiqueta de varias longitudes, si se usa como etiqueta; de lo contrario, en casos como ``k, tendríamos que marcarlo comonotMultiLength o algo similar e impulsar ese fragmento como contenido.

Código

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

Enlace al código (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv

Enlace al código (vainilla / babel) https://codepen.io/ludanin/pen/eYmBvXw

Lukas Danin
fuente
Siento que esta solución está en el camino correcto, pero parece tener problemas para poner otros caracteres de rebaja dentro de otros. Por ejemplo, intente reemplazar This must be *bold*con This must be *bo_ld*. Hace que el HTML resultante tenga un formato incorrecto
Ryan Peschel
La falta de pruebas adecuadas produjo esto = p, mi mal. Ya lo estoy arreglando y publicaré el resultado aquí, parece un problema simple de solucionar.
Lukas Danin
Si gracias. Aunque realmente me gusta esta solución. Parece muy robusto y limpio. Sin embargo, creo que se puede refactorizar un poco para obtener aún más elegancia. Podría intentar jugar un poco con eso.
Ryan Peschel
Hecho, por cierto, modifiqué el código para admitir una forma mucho más flexible de definir etiquetas de descuento y sus respectivos valores JSX.
Lukas Danin
Hola, gracias, esto se ve genial. Solo una última cosa y creo que será perfecto. En mi publicación original, también tengo una función para los fragmentos de código (que implican triple retroceso). ¿Sería posible tener apoyo para eso también? ¿Para que las etiquetas puedan ser opcionalmente múltiples caracteres? Otra respuesta agregó soporte al reemplazar instancias de `` '' con un carácter raramente usado. Esa sería una manera fácil de hacerlo, pero no estoy seguro si eso es ideal.
Ryan Peschel
4

Parece que estás buscando una pequeña solución muy básica. No "super-monstruos" como react-markdown-it:)

¡Me gustaría recomendarle https://github.com/developit/snarkdown que se ve muy ligero y agradable! Con solo 1 kb y extremadamente simple, puede usarlo y extenderlo si necesita otras características de sintaxis.

Lista de etiquetas admitidas https://github.com/developit/snarkdown/blob/master/src/index.js#L1

Actualizar

Acabo de notar los componentes de reacción, lo perdí al principio. Así que eso es genial para usted. Creo que tomar la biblioteca como ejemplo e implementar sus componentes requeridos personalizados para hacerlo sin configurar HTML peligrosamente. La biblioteca es bastante pequeña y clara. ¡Diviértete con eso! :)

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

El resultado: Resultado corriente

Resultado de la prueba Regexp

Explicación:

/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
  • Puede definir sus etiquetas en esta sección: [*|!|_] una vez que una de ellas coincida, se capturará como un grupo y se denominará "tag_begin".

  • Y entonces (?<content>\w+) captura el contenido envuelto por la etiqueta.

  • La etiqueta final debe ser la misma que la coincidente anteriormente, por lo que aquí se usa \k<tag_begin>, y si pasó la prueba, captúrela como un grupo y asígnele un nombre "tag_end", eso es lo que (?<tag_end>\k<tag_begin>))está diciendo.

En el JS has configurado una tabla como esta:

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

  };

Use esta tabla para reemplazar las etiquetas coincidentes.

Sting.replace tiene una sobrecarga String.replace (regexp, function) que puede tomar grupos capturados como parámetros, usamos estos elementos capturados para buscar la tabla y generar la cadena de reemplazo.

[Actualización]
He actualizado el código, guardé el primero en caso de que alguien más no necesite componentes de reacción, y puede ver que hay poca diferencia entre ellos. Reaccionar componentes

Simón
fuente
Lamentablemente, no estoy seguro de si esto funciona. Porque necesito los componentes y elementos React reales en sí, no cadenas de ellos. Si miras en mi publicación original, verás que estoy agregando los elementos reales a una matriz, no cadenas de ellos. Y usar dangerouslySetInnerHTML es peligroso ya que el usuario podría ingresar cadenas maliciosas.
Ryan Peschel
Afortunadamente, es muy simple convertir el reemplazo de cadena a componentes React, he actualizado el código.
Simon
Hm? Debo estar perdiendo algo, porque todavía son cuerdas de mi parte. Incluso hice un violín con tu código. Si lee la console.logsalida, verá que la matriz está llena de cadenas, no componentes reales de React: jsfiddle.net/xftswh41
Ryan Peschel
Honestamente, no sé React, por lo que no puedo hacer todo perfectamente seguido de sus necesidades, pero creo que la información sobre cómo resolver su pregunta es suficiente, debe ponerlos en su máquina React y simplemente puede desaparecer.
Simon
La razón por la que existe este hilo es porque parece ser significativamente más difícil analizarlos en componentes React (de ahí que el título del hilo especifique esa necesidad exacta). Analizarlos en cadenas es bastante trivial y solo puede usar la función de reemplazo de cadenas. Las cadenas no son una solución ideal porque son lentas y susceptibles a XSS debido a que tienen que llamar peligrosamente
SetInnerHTML
0

puedes hacerlo así:

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

A working solution purely using Javascript and ReactJs without dangerouslySetInnerHTML.

Acercarse

Búsqueda de personaje por personaje para los elementos de rebajas. Tan pronto como se encuentre uno, busque la etiqueta final para el mismo y luego conviértalo en html.

Etiquetas admitidas en el fragmento

  • negrita
  • cursiva
  • em
  • pre

Entrada y salida del fragmento:

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

Código:

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>

Explicación detallada (con ejemplo):

Supongamos que si la cadena es How are *you* doing? Mantener una asignación de símbolos a etiquetas

map = {
 "*": "b"
}
  • Haga un bucle hasta que encuentre primero *, el texto anterior es una cadena normal
  • Empuje esa matriz interior. La matriz se convierte ["How are "]y comienza el bucle interno hasta que encuentres el siguiente *.
  • Now next between * and * needs to be bold, los convertimos en elementos html por texto y los insertamos directamente en una matriz donde Tag = b del mapa. Si lo hace <Tag>text</Tag>, reaccione internamente se convierte en texto y presione en matriz. Ahora la matriz es ["cómo están", usted ]. Romper del bucle interno
  • Ahora comenzamos el bucle externo desde allí y no se encuentran etiquetas, así que presione el resto en la matriz. La matriz se convierte en: ["cómo estás", , "haciendo"].
  • Renderizar en la interfaz de usuario How are <b>you</b> doing?
    Note: <b>you</b> is html and not text

Nota : Anidar también es posible. Necesitamos llamar a la lógica anterior en recursión

Para agregar nuevas etiquetas de apoyo

  • Si son un carácter como * o!, Agréguelos en el mapobjeto con la clave como carácter y el valor como etiqueta correspondiente
  • Si tienen más de un carácter, como `` '', cree un mapa uno a uno con algunos caracteres menos utilizados y luego insértelo (Motivo: actualmente, enfoque basado en la búsqueda de carácter por carácter y, por lo tanto, se romperá más de un carácter. , eso también se puede solucionar mejorando la lógica)

¿Es compatible con la anidación? No
¿Es compatible con todos los casos de uso mencionados por OP? si

Espero eso ayude.

Sunil Chaudhary
fuente
Hola, mirando esto ahora. ¿Es esto posible usar con soporte triple de backtick también? ¿Entonces `` asdf '' funcionaría tan bien para los bloques de código?
Ryan Peschel
Lo hará, pero algunas modificaciones pueden ser necesarias. Actualmente, solo la coincidencia de un solo carácter está disponible para * o!. Eso necesita ser modificado un poco. Los bloques de código básicamente significan asdfque se representarán <pre>asdf</pre>con fondo oscuro, ¿verdad? Déjame saber esto y lo veré. Incluso puedes intentarlo ahora. Un enfoque simple es: en la solución anterior, reemplace el `` '' en el texto con un carácter especial como ^ o ~ y mapeelo en la etiqueta previa. Entonces funcionará bien. Otro enfoque necesita más trabajo
Sunil Chaudhary
Sí, exactamente, reemplazando `` asdf``` con <pre>asdf</pre>. ¡Gracias!
Ryan Peschel
@RyanPeschel Hola! preTambién he agregado el soporte de etiquetas. Avíseme si funciona
Sunil Chaudhary
Solución interesante (usando el carácter raro). Sin embargo, un problema que todavía veo es la falta de soporte para escapar (de modo que \ * asdf * no está en negrita), que incluí soporte en el código en mi publicación original (también lo mencioné en mi elaboración vinculada al final de la publicación enviar). ¿Sería muy difícil de agregar?
Ryan Peschel