31
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Electron + React.js でちょっとした Markdown Viewer を作成して少し知見が溜まったので宣伝とハマりどころなどまとめた (Markdown 表示 編)

Posted at

ネタが複数あるので分けて作成してきたのですが、今回が最後になります。
前回までに gulpfile でのポイントおよび Electron でのポイントを以下で紹介しています。

こちらも見ていただけると幸いです。再び宣伝するのは、くどい気もしたのですが 前回の記事を見ていない人のほうが圧倒的に多いと思いますし、最後なので少し宣伝。

作成したもの

markcat
Electron製の Markdown Viewer

機能的な特徴

  • Intellij IDEA の Darcula 風の表示テーマも用意
  • 表示テーマの変更も可能
  • Github Flavored Markdown
  • コードハイライト表示
  • 編集時の自動更新
  • ドラッグ&ドロップからの Markdown 表示
  • (Windows) SendTo に配置することにより、エクスプローラーの「送る」からの Markdown 表示
  • (Windows)ファイル関連付けを行うと、ダブルクリックからの Markdown 表示
  • (Mac)このアプリケーションで開く からの起動。
  • (Mac)ファイル拡張子で関連付けを行うと、ダブルクリックからの Markdown 表示

開発としての特徴

  • Typescript + gulp + react + electron と今風の技術を利用しています。
  • 実装はそれなりにシンプルなので学習にも最適
  • とはいえ Markdown としての基本機能に追加して上記のこだわり機能を実装しています。

開発動機

Markdown は普段は Atom などのエディタを利用して作成しています。
Markdown を見る時も同じく Atom を利用したり、Chrome の拡張機能を利用して表示したりしていたのですが、md ファイルをダブルクリックしたり、エクスプローラーのコンテキストメニューからサクッと見ることができないかと考えていました。
そんな時 WEB+DB Press の React の記事の中で marked という markdown parser が紹介されていたのをきっかけに、また rhysd/Shiba を拝見して、自分用の Markdown Viewer を作ってみたいと思いました。

ソースファイル

以下で公開しています。
https://github.com/ma-tu/markcat

利用方法

GitHub の Release にコンパイル済みのファイルを配置しているので、環境に合わせた zip ファイルをダウンロードして、適当なフォルダに解凍後 markcat を実行するだけです。
詳細は こちらの README を参照ください。

知見

Markdown Parser と コードハイライトの対応

Markdown Viewer の一番のコアである .md の Markdown File を HTML 形式に変更する部分は以下のライブラリを利用させていただきました。

  • HTML に変更する部分については marked
  • Markdown のコード部分のシンタックスハイライト表示については highlight.js
  • GitHub風に表示するために github-markdown-css

Darcula 風テーマは上記の highlight.js の css および github-markdown-css の色について調整して作成しました。

以下が Markdown File を HTML 形式に変換している箇所になります。
少し補足説明します。

  • convertMarkedHtml() 関数の引数 content には、Markdown File の 内容を文字列で渡しています。
  • marked の setOptions で highlight を指定することで、コードブロックのハイライト処理を制御することができます。
  • 今回はコードブロックのハイライト処理に、 highlight.js を利用していますが、他のマークアップ言語を利用することもできます。
  • lang には コードブロックに [ js ] と指定した場合の [ js ] という文字列が渡ってきます。
  • 基本的にはこの lang を渡すだけでもよいのですが、Qiita の Markdown File では コードブロックで [ js:src/renderer/marked.ts ] と記述できるように拡張されています。このケースに対応するために [ : ] で区切って処理するようにしています。
src/renderer/marked.ts
import * as marked from 'marked'
import {highlight} from 'highlight.js';

export function convertMarkedHtml(content: string): string {
  marked.setOptions({
    highlight: function (code: string, lang: string): string {
      if (lang === undefined) {
          return code
      }
      
      const langSplit = lang.split(':')
      try {
          return highlight(langSplit[0], code).value
      } catch (e) {
          console.log(e.message)
          return code
      }
    }
  })

  return marked(content)
}

上記の convertMarkedHtml() 関数で変換した HTML 形式の文字列を React の dangerouslySetInnerHTML というプロパティに渡すことにより表示を行います。このプロパティは HTML をエスケープせずに表示するためのプロパティなのでクロスサイトスクリプティングの危険性があるため、このような名前になっているようです。

少し古い Version ですが、以下の説明がわかりやすいと思います。

src/renderer/components/comntentHolderComponent.tsx
render() {
  return (
    <div id="holder" onDragOver={this.handleDragOver.bind(this)} onDrop={this.handleDrop.bind(this)}>
      <div id="content" dangerouslySetInnerHTML={{__html: this.props.html}} />
    </div>
  )
}

Markdown のリンクをクリックした時に外部ブラウザを利用して表示する方法

以下のような記述を Markdown File で指定すると、以下のように a タグに変換されます。
このリンクをクリックすると、Electron 上でリンク先のファイルを読み込みます。結果として Markdown Viewer としてのページからリンク先のページに遷移してしまい、Markdown Viewer として利用できなくなります。そのため今回は MarkCat で a タグをクリックしたときには、外部ブラウザを利用してリンク先のファイルを表示するようにしています。

[Google](http://www.google.co.jp) 
<a href="http://www.google.co.jp">Google</a>

以下補足します。

  • setOpenLinkWithBrowser() 関数で document 内の a タグの一覧を取得し、その OnClick イベントに対して event.preventDefault() でキャンセルしたうえで、electron.shell.openExternal() 関数を利用して外部ブラウザを呼び出しています。
  • electron の shell.openExternal() については atom/electron の shell api リファレンス を参照ください。
  • React.js では DOM に対しての操作は DOMツリーに追加されたタイミング、更新されたタイミングで行う必要があります。
    componentDidMount はコンポーネントが DOMツリーに追加された時に呼ばれます。
    componentDidUpdate はコンポーネントが更新された後に呼ばれます。
src/renderer/components/markdownComponent.tsx
componentDidMount() {
  this.setOpenLinkWithBrowser()
}

componentDidUpdate() {
  this.setOpenLinkWithBrowser()
}

private setOpenLinkWithBrowser(): void {
  const anchors = document.querySelectorAll('a')
  for(let i = 0; i < anchors.length; i++) {
    const anker = anchors.item(i) as HTMLLinkElement
    anker.onclick = (e) => {
      event.preventDefault()

      const target = event.target as HTMLLinkElement
      electron.shell.openExternal(target.href)
    }
  }
}

Thema を動的に入れ替える方法

Thema を Dark <-> Normal に変更したときには、以下の changeCssThemaFile() を実行します。
document の id="css-thema" の HTMLLinkElement を取得して href を変更することにより Thema の更新を行っています。

src/renderer/services/thema.ts
function changeCssThemaFile(cssFilePath: string) {
  var linkEl: HTMLLinkElement = document.getElementById('css-thema') as HTMLLinkElement
  linkEl.href = cssFilePath;
}
src/renderer/index.html
<link rel="stylesheet" id="css-thema" href="./css/thema-normal.css">

編集時の自動更新の方法

Markdown の編集時の自動更新は chokidar を利用しています。
以下補足します。

  • chokidar.watch(<監視対象パス>) で監視対象のパスを指定して watcher オブジェクトを取得します。
  • watchker オブジェクトに対して on(event, callback) で Event の Listner を登録します。
  • on('add', callback) はファイルの追加を監視します。
  • on('change', callback) はファイルの変更を監視します。
  • ファイルの追加・変更が発生すると、対象のファイルを読込み、そのファイルのコンテンツを引数にして handleUpdate() 関数を呼び出します。
src/renderer/services/watcher.ts
import * as chokidar from 'chokidar'
import * as fs from 'fs'

export class Watcher {
  private watcher: any
  private currentPath: string
  private handleUpdate: any

  constructor(path: string, handleUpdate: any) {
    this.currentPath = path
    this.handleUpdate = handleUpdate

    this.watcher = chokidar.watch(this.currentPath)
    this.watcher.on('add', this.onUpdate.bind(this))
    this.watcher.on('change', this.onUpdate.bind(this))
  }

  public changeWatchPath(path: string): void {
    this.watcher.unwatch(this.currentPath)
    this.watcher.add(path)
    this.currentPath = path
  }

  private onUpdate(path: string, stats: any) {
    fs.readFile(path, 'utf8', (err: any, content: any) => {
      if (err) throw err
      this.handleUpdate(content)
    });
  }
}

終わりに

いかがだったでしょうか?
Markdown viewer を作成するにあたってのポイントはすべて記述したつもりです。
少しでも誰かの開発のヒントになることができれば幸いです。
またできましたら MarkCat 使ってみてください。
最後にこの記事を見た人が好きな言語を利用して、もっとすごい Markdown Viewer を作成していただけたりしたら最高です。

31
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?