React : Rendering a syntax highlighted code block

2019-07-07 14:01发布

问题:

I'm working on an article system using React and JSX. My articles sometimes have code examples within their content. I have implemented highlight.js to add style to these blocks. My main problem is that my articles use HTML tags, I therefore use React's dangerouslySetInnerHTML method. This works alright but of course any HTML code in my code blocks get interpreted as HTML. I was wondering if any of you had any insight on how I should implement this. Should I remove all HTML from my content and parse it (using markdown) before safely rendering it as text ?

Thanks for reading this.

回答1:

I use markdown with highlight and then innerHtml. This is from https://github.com/calitek/ProjectNotes.

'use strict';

var fs = require('fs');
var Remarkable = require('remarkable');
var hljs       = require('highlight.js');
let lodash = require('lodash');

var md = new Remarkable({
  highlight: function (str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(lang, str).value;
      } catch (err) {}
    }

    try {
      return hljs.highlightAuto(str).value;
    } catch (err) {}

    return '';
  }
});

var rootDataPath = './data';

var getFile = function(clientData, doneCallBack) {
  let getNote = function(fileData) {
    var filePath = rootDataPath + '/filenotes.json';
    var notesReadCallBack = function(err, data){
      if (err) doneCallBack({fileData: fileData, noteData: {note: 'error'}});
      else {
        let noteData;
        var jsonData = JSON.parse(data.toString());
        let noteRecord = lodash.findWhere(jsonData, {nodeid: clientData.nodeid});
        if (noteRecord) {
          let noteIndex = lodash.indexOf(jsonData, noteRecord);
          noteData = jsonData[noteIndex];
        } else {
          noteData = {nodeid: clientData.nodeid, note: ""};
        }
        let returnObject = {
          noteData: noteData,
          fileData: fileData
        }
        return doneCallBack(returnObject);
      }
    };
    fs.readFile(filePath, notesReadCallBack);
  }
  var fileReadCallBack = function(err, data){
    if (err) doneCallBack({note: 'error'});
    else {
      let inData = data.toString();
      let inFile = clientData.filePath;
      if (inFile.endsWith('.js') || inFile.endsWith('.json') || inFile.endsWith('.css')) {
        inData = '``` javascript\n' + inData + '```';
      }

      let outData = md.render(inData);
      getNote(outData);
    }
  };
  fs.readFile(clientData.filePath, fileReadCallBack);
};

I am doing the markdown rendering on the server. Then sending that to the component.

import React from 'react';

let FileCtrlSty = {
  height: '60%',
  marginBottom: '20px',
  width: '100%'
};

export default class FileCtrl extends React.Component {
  render() {
    let htmlDivSty = {height: '100%', width: '100%'}
    if (this.props.fileData.startsWith('<pre>')) htmlDivSty.overflow = 'hidden';
    else htmlDivSty.overflow = 'auto';
    let fileHtml = {__html: this.props.fileData};
    return (
      <div id='FileCtrlSty' style={FileCtrlSty}>
        <div dangerouslySetInnerHTML={fileHtml} style={htmlDivSty}></div>
      </div>
    );
  }
}



回答2:

My solution was to implement a simple "markdown" like parser within my article view. The idea is to parse the markdown using regex and return JSX elements from those results. I'm not so good at regular expressions (this could probably be improved) but here goes :

module.exports = React.createClass({
//this regex matchs \n\n, all wrapping ** **, wrapping ''' '''
mainRegex: /(\n\n|\*\*(?:[\s\S]*?)\*\*|'''(?:[\s\S]*?)''')/g,
titleRegex : /(?:\*\*([\s\S]*?)\*\*)/,
codeRegex : /(?:'''([\s\S]*?)''')/,

getInitialState: function() {
    return {
        content: []
    };
},

setContent: function() {
    if (this.props.htmlContent && !this.state.content.length) this.setState( {content: this.formatText(this.props.htmlContent) });
},

formatText: function(_text) {

    var _this = this;
    var _content = [];

    _text = _text.split(this.mainRegex);

    _text.forEach(function(_frag) {

        if (!/\n\n/g.test(_frag) ) {

            if (_this.titleRegex.test(_frag))  _content.push( {type: "h2", value: _frag.match(_this.titleRegex)[1] } );
            else if (_frag!=="") _content.push({type: "p", value: _frag});

        } else if (_this.codeRegex.test(_frag))  {

            _content.push( {type: "code", value: _frag.match(_this.codeRegex)[1] } );
        }

    });

    return _content;
},
render: function() {

    return <article>
        {this.state.content.map(this.renderText)}
    </article>;

},

renderText:function(_tag, i) {

    switch (_tag.type) {

        case "p":
            return <p key={i}>{_tag.value}</p>;
            break;

        case "h2":
            return <h2 key={i}>{_tag.value}</h2>;
            break;

        case "code":
            return <pre key={i}><code clasName="js">{_tag.value}</code></pre>;
            break;

    }

},

componentDidUpdate: function() {

    this.setContent();
    this.highlightCode();

},

highlightCode: function() {

    var _codeBlocks = document.getElementsByTagName('code');
    for (var i = 0, j = _codeBlocks.length; i<j; ++i) {
        hljs.highlightBlock(_codeBlocks[i]);
    }

}
});