Como analisar um pequeno subconjunto do Markdown nos componentes React?

9

Eu tenho um subconjunto muito pequeno do Markdown, juntamente com alguns html personalizados que gostaria de analisar nos componentes do React. Por exemplo, eu gostaria de transformar esta seguinte string:

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

Na seguinte matriz:

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

e, em seguida, retorne-o da função React render (React renderizará a matriz corretamente como HTML formatado)

Basicamente, quero dar aos usuários a opção de usar um conjunto muito limitado de Markdown para transformar seu texto em componentes estilizados (e, em alguns casos, meus próprios componentes!)

Não é prudente definir perigosamente o SetInnerHTML, e não quero trazer uma dependência externa, porque todas elas são muito pesadas e preciso apenas de uma funcionalidade básica.

Atualmente, estou fazendo algo assim, mas é muito quebradiço e não funciona em todos os casos. Fiquei me perguntando se havia uma maneira melhor:

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

Aqui está a minha pergunta anterior que levou a esta.

Ryan Peschel
fonte
11
E se a entrada tiver itens aninhados, como font _italic *and bold* then only italic_ and normal? Qual seria o resultado esperado? Ou nunca será aninhado?
trincot
11
Não precisa se preocupar com o aninhamento. É apenas uma remarcação muito básica para os usuários usarem. Tudo o que é mais fácil de implementar está bem comigo. No seu exemplo, seria ótimo se o negrito interno não funcionasse. Mas se é mais fácil implementar o aninhamento do que não tê-lo, tudo bem também.
Ryan Peschel
11
Provavelmente, é mais fácil usar apenas uma solução pronta para
uso
11
Eu não estou usando remarcação embora. É apenas um subconjunto muito semelhante / pequeno (que suporta alguns componentes personalizados, juntamente com negrito, itálico, código e sublinhado não aninhados). Os trechos que eu postei um pouco de trabalho, mas não parece muito ideal, e falhar em alguns casos triviais, (como você não pode digitar uma única astericks como este: asdf*sem ele desaparecer)
Ryan Peschel
11
bem ... analisar markdown ou algo parecido com markdown não é exatamente uma tarefa fácil ... as expressões regulares não o cortam ... para uma pergunta semelhante sobre html, consulte stackoverflow.com/questions/1732348/…
mb21

Respostas:

1

Como funciona?

Ele funciona lendo uma sequência de partes por parte, o que pode não ser a melhor solução para seqüências realmente longas.

Sempre que o analisador detectar que uma parte crítica está sendo lida, '*'ou seja, qualquer outra tag de redução, ele começa a analisar partes desse elemento até que o analisador encontre sua marca de fechamento.

Funciona em cadeias de linhas múltiplas, veja o código por exemplo.

Ressalvas

Você não especificou, ou eu poderia ter interpretado mal suas necessidades, se houver a necessidade de analisar tags em negrito e itálico , minha solução atual pode não funcionar nesse caso.

Se você precisar, no entanto, trabalhar com as condições acima, basta comentar aqui e eu ajustarei o código.

Primeira atualização: aprimora o tratamento das tags de remarcação

As tags não são mais codificadas, mas são um mapa onde você pode estender facilmente para atender às suas necessidades.

Corrigidos os erros que você mencionou nos comentários, obrigado por apontar esses problemas = p

Segunda atualização: tags de remarcação de vários comprimentos

A maneira mais fácil de conseguir isso: substituindo caracteres de vários comprimentos por um unicode raramente usado

Embora o método parseMarkdownainda não suporte tags de vários comprimentos, podemos facilmente substituí-las por uma simples string.replace ao enviar nossarawMarkdown suporte.

Para ver um exemplo disso na prática, veja o ReactDOM.render , localizado no final do código.

Mesmo que a sua aplicação não suporte a vários idiomas, há caracteres Unicode inválidos se o JavaScript ainda detecta, ex .: "\uFFFF"não é um unicode válido, se bem me lembro, mas JS ainda será capaz de compará-lo ("\uFFFF" === "\uFFFF" = true )

Pode parecer hackeado no começo, mas, dependendo do seu caso de uso, não vejo grandes problemas ao usar esta rota.

Outra maneira de conseguir isso

Bem, poderíamos rastrear facilmente o último N(ondeN pedaços corresponde ao comprimento da tag mais longa).

Haveria alguns ajustes a serem feitos na maneira como o loop dentro do método parseMarkdownse comporta, ou seja, verificar se o pedaço atual faz parte de uma tag de vários comprimentos, se é usado como tag; caso contrário, em casos como ``k, precisaríamos marcá-lo comonotMultiLength ou algo semelhante e colocar esse pedaço como conteúdo.

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

Link para o código (TypeScript) https://codepen.io/ludanin/pen/GRgNWPv

Link para o código (vanilla / babel) https://codepen.io/ludanin/pen/eYmBvXw

Lukas Danin
fonte
Sinto que esta solução está no caminho certo, mas parece ter problemas ao colocar outros caracteres de remarcação dentro dos outros. Por exemplo, tente substituir This must be *bold*por This must be *bo_ld*. Isso faz com que o HTML resultante a ser mal formado
Ryan Peschel
A falta de testes adequados produziu isso = p, meu mal. Já estou corrigindo e indo postar o resultado aqui, parece um problema simples de corrigir.
Lukas Danin
Sim obrigado. Eu realmente gosto dessa solução. Parece muito robusto e limpo. Eu acho que isso pode ser refatorado um pouco, para ainda mais elegância. Eu posso tentar brincar um pouco com isso.
Ryan Peschel
Feito isso, a propósito, aprimorei o código para oferecer suporte a uma maneira muito mais flexível de definir tags de remarcação e seus respectivos valores JSX.
Lukas Danin
Ei, obrigado, isso parece ótimo. Só mais uma coisa e acho que será perfeito. No meu post original, também tenho uma função para trechos de código (que envolvem backticks triplos). Seria possível ter suporte para isso também? Para que as tags possam opcionalmente ter vários caracteres? Outra resposta adicionou suporte ao substituir instâncias de `` `por um caractere raramente usado. Essa seria uma maneira fácil de fazer isso, mas não tenho certeza se isso é ideal.
Ryan Peschel
4

Parece que você está procurando uma pequena solução muito básica. Não "super-monstros" como react-markdown-it:)

Gostaria de recomendar https://github.com/developit/snarkdown, que parece bem leve e agradável! Apenas 1kb e extremamente simples, você pode usá-lo e estendê-lo se precisar de outros recursos de sintaxe.

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

Atualizar

Só notei sobre componentes de reação, perdi no começo. Portanto, isso é ótimo para você, acredito que tome a biblioteca como exemplo e implemente os componentes necessários necessários para fazê-lo sem configurar HTML perigosamente. A biblioteca é bem pequena e clara. Divirta-se com isso! :)

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

O resultado: Resultado em execução

Resultado do teste Regexp

Explicação:

/(?<item>(?<tag_begin>[*|!|_])(?<content>\w+)(?<tag_end>\k<tag_begin>))/
  • Você pode definir suas tags nesta seção: [*|!|_]depois que uma delas corresponder, ela será capturada como um grupo e nomeada como "tag_begin".

  • E depois (?<content>\w+)captura o conteúdo envolvido pela tag.

  • A tag final deve ser a mesma que a correspondente anteriormente, então aqui é usada \k<tag_begin>e, se passou no teste, capture-a como um grupo e dê um nome "tag_end", é o que (?<tag_end>\k<tag_begin>))está dizendo.

No JS, você configurou uma tabela como esta:

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

  };

Use esta tabela para substituir as tags correspondentes.

Sting.replace tem uma sobrecarga String.replace (regexp, function) que pode receber grupos capturados como parâmetros, usamos esses itens capturados para procurar na tabela e gerar a sequência de substituição.

[Atualização] Atualizei
o código, mantive o primeiro caso alguém não precise de componentes de reação e você pode ver que há pouca diferença entre eles. Reagir componentes

Simon
fonte
Infelizmente não tenho certeza se isso funciona. Porque eu preciso dos componentes e dos elementos reais do React, e não das cadeias. Se você olhar no meu post original, verá que estou adicionando os elementos reais a uma matriz, não seqüências de caracteres. E usar perigosamente o SetInnerHTML é perigoso, pois o usuário pode inserir seqüências maliciosas.
Ryan Peschel
Felizmente, é muito simples converter a substituição de string em componentes React, atualizei o código.
Simon
Hum? Eu devo estar perdendo alguma coisa, porque eles ainda estão do meu lado. Eu até fiz um violino com o seu código. Se você ler a console.logsaída que você verá a matriz está cheio de cordas, não real Reagir componentes: jsfiddle.net/xftswh41
Ryan Peschel
Honestamente, eu não sei o React, por isso não posso fazer tudo perfeitamente seguido pelas suas necessidades, mas acho que as informações sobre como resolver sua pergunta são suficientes, você precisa colocá-las na sua máquina do React e isso pode ser feito.
Simon
A razão pela qual esse encadeamento existe é porque parece ser significativamente mais difícil analisá-los nos componentes React (daí o título do encadeamento que especifica essa necessidade exata). Analisá-los em strings é bastante trivial e você pode simplesmente usar a função de substituição de strings. As seqüências de caracteres não são a solução ideal porque são lentas e suscetíveis ao XSS devido à necessidade de chamar perigosamente
SetInnerHTML
0

você pode fazer assim:

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

A working solution purely using Javascript and ReactJs without dangerouslySetInnerHTML.

Aproximação

Pesquisa de caractere por caractere para os elementos de remarcação. Assim que for encontrada, pesquise a tag final e a converta em html.

Tags suportadas no snippet

  • negrito
  • itálico
  • em
  • pré

Entrada e saída do snippet:

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>

Explicação detalhada (com exemplo):

Suponha que se string é How are *you* doing? Manter um mapeamento para símbolos para tags

map = {
 "*": "b"
}
  • Faça um loop até encontrar primeiro *, texto antes que seja uma string normal
  • Empurre essa matriz interna. A matriz se torna ["How are "]e inicia o loop interno até encontrar o próximo *.
  • Now next between * and * needs to be bold, nós os convertemos no elemento html pelo texto e enviamos diretamente a matriz onde Tag = b do mapa. Se o fizer <Tag>text</Tag>, reagir internamente converte em texto e empurra para matriz. Agora, o array é ["como está", você ]. Quebra do loop interno
  • Agora começamos o loop externo a partir daí e nenhuma tag é encontrada, então empurre o restante da matriz. A matriz se torna: ["como está", você , "fazendo"].
  • Renderizar na interface do usuário How are <b>you</b> doing?
    Note: <b>you</b> is html and not text

Nota : O aninhamento também é possível. Precisamos chamar a lógica acima na recursão

Para adicionar suporte a novas tags

  • Se eles tiverem um caractere como * ou!, Adicione-os map objeto com a chave como caractere e o valor como tag correspondente
  • Se eles tiverem mais de um caractere, como `` ``, crie um mapa individual com alguns caracteres usados ​​com menos frequência e, em seguida, insira (Razão: atualmente, abordagem baseada no caractere pela pesquisa de caracteres e assim mais de um caractere será interrompido. , que também pode ser resolvido melhorando a lógica)

Suporta aninhamento? Não
Suporta todos os casos de uso mencionados pelo OP? sim

Espero que ajude.

Sunil Chaudhary
fonte
Oi, olhando isso agora. Isso também é possível com o suporte triplo do backtick? Então `` asdf`` funcionaria também para blocos de código?
Ryan Peschel
Será, mas algumas modificações podem ser necessárias. Atualmente, apenas a correspondência de caracteres únicos existe para * ou!. Isso precisa ser modificado um pouco. Blocos de código basicamente significa asdfque serão renderizados <pre>asdf</pre>com fundo escuro, certo? Deixe-me saber isso e eu vou ver. Até você pode tentar agora. Uma abordagem simples é: Na solução acima, substitua o `` `no texto por um caractere especial como ^ ou ~ e mapeie-o para pré-tag. Então vai funcionar bem. Outra abordagem precisa de mais trabalho
Sunil Chaudhary
Sim, exatamente, substituindo `` asdf``` por <pre>asdf</pre>. Obrigado!
Ryan Peschel
@RyanPeschel Hi! preTambém foram adicionados o suporte à tag. Deixe-me saber se ele funciona
Sunil Chaudhary
Solução interessante (usando o caractere raro). Um problema que ainda vejo é a falta de suporte para escape (como \ * asdf * não está em negrito), que incluí suporte no código da minha postagem original (também o mencionei na minha elaboração vinculada no final do postar). Isso seria muito difícil de adicionar?
Ryan Peschel