markedjs/markedでmarkdownにclassをつけてcssを当てる

4 min read読了の目安(約4400字

前提

markdownからhtmlに変換するライブラリはmarkedを使います。

https://github.com/markedjs/marked
editorはreact-simplemde-editorを使います。
https://github.com/RIP21/react-simplemde-editor
React component wrapper for EasyMDE (the most fresh SimpleMDE fork).

環境

node 14.10.1
react 17.0.1
marked 1.2.7
react-simplemde-editor 4.1.3

方針

生成するhtmlの理想は以下です。

<div class="red line">
  赤くして囲む
</div>

独自の記法を定義できたら一番良いですがソースコード見たところできませんでした。
TokenizerやRendererは定義できるのですが、肝心のTokenizerを呼び出すLexerに処理を差し込むことができないです。
markedはRendererをオーバーライドすることで生成するhtmlに手を入れることができるのでこれを利用します。
headingの場合

// Override function
const renderer = {
  heading(text, level) {
    const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');

    return `
            <h${level}>
              <a name="${escapedText}" class="anchor" href="#${escapedText}">
                <span class="header-link"></span>
              </a>
              ${text}
            </h${level}>`;
  }
};

オーバーライドできるメソッドは以下です。

  • code(string code, string infostring, boolean escaped)
  • blockquote(string quote)
  • html(string html)
  • heading(string text, number level, string raw, Slugger slugger)
  • hr()
  • list(string body, boolean ordered, number start)
  • listitem(string text, boolean task, boolean checked)
  • checkbox(boolean checked)
  • paragraph(string text)
  • table(string header, string body)
  • tablerow(string content)
  • tablecell(string content, object flags)

オーバーライドすることで自由にclassを入れ込められる形をしているのはcodeなのでこれをオーバーライドしていきます。
code(string code, string infostring, boolean escaped)
infostringに```goのgoが入るのでここに自分たちが決めた記法を入れて挙動を変えれそうです。

やり方

一旦、環境を整えます。
$ npx create-react-app test-editor
$ cd test-editor
$ npm i marked react-simplemde-editor

オーバーライドの仕方は下記にあります。

https://marked.js.org/using_pro#renderer
単に新たなRendererを定義してuseメソッドに渡せばいいです。
const renderer = {}
marked.use({ renderer });

下記にRendererが書かれてあります。ここからcode()をコピーしてきて修正します。

https://github.com/markedjs/marked/blob/master/src/Renderer.js
code(code, infostring, escaped) {
 // ここに追記

  const lang = (infostring || '').match(/\S*/)[0];
  if (this.options.highlight) {
    const out = this.options.highlight(code, lang);
    if (out != null && out !== code) {
      escaped = true;
      code = out;
    }
  }

  if (!lang) {
    return '<pre><code>'
      + (escaped ? code : escape(code, true))
      + '</code></pre>\n';
  }

  return '<pre><code class="'
    + this.options.langPrefix
    + escape(lang, true)
    + '">'
    + (escaped ? code : escape(code, true))
    + '</code></pre>\n';
}

記法はこんな感じにします。
```@red,line
赤くして囲む
```

```の右の文字列の先頭が@の場合はdivタグをレンダリングするようにします。
@の右にclassを,区切りで書くことにします。
コードは以下です。

if (infostring.charAt(0) === '@') {
  const classes = infostring.slice(1).replace(/ /g, '').split(',').filter(w => w.length > 0).join(' ')
  return `<div class="${classes}">${code}</div>\n`;
}

実際のコード

エディタのコンポーネント

Editor.js
import React from "react";
import SimpleMDE from "react-simplemde-editor";
import "easymde/dist/easymde.min.css";
import marked from "marked";
import './Editor.css';

const Editor = () => {
  const renderer = {
    code(code, infostring, escaped) {
      if (infostring.charAt(0) === '@') {
        const classes = infostring.slice(1).replace(/ /g, '').split(',').filter(w => w.length > 0).join(' ')
        return `<div class="${classes}">${code}</div>\n`;
      }

      const lang = (infostring || '').match(/\S*/)[0];
      if (this.options.highlight) {
        const out = this.options.highlight(code, lang);
        if (out != null && out !== code) {
          escaped = true;
          code = out;
        }
      }

      if (!lang) {
        return '<pre><code>'
          + (escaped ? code : escape(code, true))
          + '</code></pre>\n';
      }

      return '<pre><code class="'
        + this.options.langPrefix
        + escape(lang, true)
        + '">'
        + (escaped ? code : escape(code, true))
        + '</code></pre>\n';
    }
  };
  marked.use({ renderer });
  return (
    <SimpleMDE
      options={{
        previewRender(text) {
          return marked(text)
        }
      }}
    />
  );
}

export default Editor;
Editor.css
.red {
  color: red;
}

.line {
  border: solid;
}

実際の画面


さいごに

普段はreact書いてないので変な書き方をしているかもれしれないです。ご容赦ください。