13
14

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.

Riot+ElectronでMarkdownエディタを作る

Last updated at Posted at 2017-12-11

riotが最近自分の中でキてるのでなんか作りたいなと思って、markdownエディタを作ってみました。
riot中心なのでelectronの説明はしません(そもそも起動でしか使ってない)

自分の環境

  • Windows 10 home
  • node.js v8.9.1
  • npm 5.5.1

どうやる

  • markdown-it >> markdownパーサ
  • highlight.js >> コードをハイライト表示してくれる
  • ace >> web上に埋め込めるエディター

リアルタイムにプレビューさせたいので,
aceエディタの中身が変更されたら、エディタの値をmarkdown-itでパースして表示させます。そして表示されたやつにコードがあったらhighlight.jsでハイライトを付けます。

その他ファイル読み込みと上書き保存機能も付けます

やっていきましょう

ディレクトリ構成

-public
   -index.html
   -main.js
-src
   -app
     -editor
        -editor.tag
        -editor.css
     -header
        -header.tag
        -header.css
     -preview
        -preview.tag
        -preview.css
     -app.tag
     -app.css
   -main.js

パッケージと設定ファイル

package.json
{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "electron": "^1.7.9"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015-riot": "^1.1.0",
    "bootstrap": "^3.3.7",
    "brace": "^0.11.0",
    "css-loader": "^0.28.7",
    "github-markdown-css": "^2.9.0",
    "highlight.js": "^9.12.0",
    "jquery": "^3.2.1",
    "markdown-it": "^8.4.0",
    "riot": "^3.7.4",
    "riot-tag-loader": "^1.0.0",
    "style-loader": "^0.19.0",
    "webpack": "^3.10.0"
  },
  "scripts": {
    "start": "electron ./public/main.js",
    "watch": "webpack --watch --progress --color",
    "build": "webpack --progress --color"
  }
}

webpack.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'public/javascripts'),
  },
  module: {
    rules: [{
        enforce: 'pre',
        test: /\.tag$/,
        exclude: /node_modules/,
        loader: 'riot-tag-loader',
        query: {
          type: 'es6',
          debug: true
        }
      },
      {
        test: /\.css/,
        loaders: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              url: false
            }
          }
        ]
      },
      {
        test: /\.(tag|js)$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: ['es2015-riot'],
        },
      },
    ],
  },
  devtool: 'source-map',
  resolve: {
    extensions: ['*', '.js'],
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
    }),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.optimize.AggressiveMergingPlugin(),
    new webpack.ProvidePlugin({
      riot: 'riot',
      jQuery: 'jquery',
      $: 'jquery'
    }),
  ]
}

コード

まずはhtmlと起動スクリプトを書きます。

index.html
<html>
  <head>
    <meta charset="UTF-8">
    <title>Markdown Editor</title>
  </head>
  <body>
    <app-root></app-root>
    <script>
        //webpackでうまく`fs`が使えなかったのでグローバルにしました。
        window.fs = require('fs') 
    </script>
    <script src="javascripts/bundle.js"></script> <!--webpackで出力されたファイル-->
  </body>
</html>
public/main.js
// Electron起動スクリプト。
const { app, BrowserWindow } = require('electron')

const path = require('path')
const url = require('url')

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({ width: 1200, height: 1000 })
  mainWindow.loadURL(url.format({
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file:',
    slashes: true
  }))
  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (win === null) {
    createWindow()
  }
})

npm startで動作確認
アプリが立ち上がれば大丈夫です:smile:

riot側のコード

コンポーネント指向なので

  • ヘッダーコンポーネント (app-header)
  • エディタコンポーネント (app-editor)
  • プレビューコンポーネント (app-preview)

の三つのコンポーネントに分けます。
後は、ヘッダー用とエディター用のオブザーブルを作って、コンポーネント間の値渡しやイベント検知などもしやすくします。

src/main.js
//bootstrapとmarkdownのcss等。デザイン部分なのであまり重要ではない
import '../node_modules/bootstrap/dist/css/bootstrap.min.css'
import '../node_modules/github-markdown-css/github-markdown.css'
import '../node_modules/highlight.js/styles/github.css'
import '../node_modules/bootstrap'

//app-rootタグを読み込む
import './app/app.css'
import './app/app.tag'

riot.mount('app-root')

app.tag

<!--app.tag-->
<app-root>
  <!--ヘッダー部分-->
  <app-header header="{ header }"></app-header>

  <div class="container-fluid">

    <!--エディタ部分-->
    <div class="col-sm-6">
      <app-editor editor="{ editor }" header="{ header }"></app-editor>
    </div>

    <!--プレビュー部分-->
    <div class="col-sm-6">
      <app-preview editor="{ editor }"></app-preview>
    </div>

  </div>

  <script>
    import './editor/editor.tag'
    import './editor/editor.css'

    import './preview/preview.tag'
    import './preview/preview.css'

    import './header/header.tag'
    import './header/header.css'

    //エディタ用のオブザーブル
    this.editor = riot.observable()
    //ヘッダー用のオブザーブル
    this.header = riot.observable()

  </script>
</app-root>

header.tag

<!--header.tag-->
<app-header>
  <div class="group-button">

    <label>
      <span class="btn btn-default">load
        <input type="file" id="file" onchange="{ load }">
      </span>
    </label>

    <button class="btn btn-default" onclick="{ save }">save</button>

  </div>

  <script>

    this.load = () => {
      const path = $('#file')[0].files[0].path
      this.opts.header.trigger('load', path)  //loadイベント発火
    }

    this.save = () => {
      this.opts.header.trigger('save')  //saveイベント発火
    }

  </script>
</app-header>

editor.tag

<!--editor.tag-->
<app-editor>
  <div id="editor"></div>
  <script>
    import ace from 'brace'
    import 'brace/mode/markdown'  //markdownのハイライト
    import 'brace/theme/monokai'  //エディタのテーマ(sublimeっぽくした)

    $(document).ready(() => {
   //エディタ設定
      this.editor = ace.edit('editor')
      this.editor.$blockScrolling = Infinity
      this.editor.setTheme('ace/theme/monokai')
      this.editor.getSession().setMode('ace/mode/markdown')
      this.editor.setFontSize(14)
      this.editor.getSession().setUseWrapMode(true)
      this.editor.getSession().setTabSize(2)

      // エディタに変更を検知したとき、editorオブザーブルにchangeイベントを発火
      // エディタの値も渡すよ
      this.editor.getSession().on('change', () => {
        this.opts.editor.trigger('change', this.editor.getValue())
      })

    })

    //headerオブザーブルのloadイベントを検知したら、ファイルの読み込み処理
    this.opts.header.on('load', path => {
      this.path = path
      fs.readFile(this.path, 'utf8', (err, data) => {
        this.editor.setValue(data)
      })
    })

    //headerオブザーブルのsaveイベントを検知したら、上書き保存処理
    this.opts.header.on('save', () => {
      fs.writeFile(this.path, this.editor.getValue(), err => {
        if(!err)
          alert('せいこう!')
      });
    })
  </script>
</app-editor>

preview.tag

<!--preview.tag-->
<app-preview>
  <article class="markdown-body"></article>
  <script>
    import MarkDownIt from 'markdown-it'
    import hljs from 'highlight.js'
    hljs.initHighlightingOnLoad()

    //editorオブザーブルのchangeイベントを検知したら
    this.opts.editor.on('change', data => {
      const md = new MarkDownIt()
      const result = md.render(data) //エディタの値をパース

      $('article').html(result)
      $('pre code').each((i, block) => {
        hljs.highlightBlock(block)
      })
    })
  </script>
</app-preview>

cssコード

適当です。

app.css
.container-fluid {
  padding: 0px;
  width: 100%;
  height: 100%;
}

.col-sm-6 {
  padding: 0px;
}
preview.css
app-preview article {
  padding: 5px;
  margin: 10px;
  height: 100%;
  overflow: auto;
}
header.css
app-header #file {
  display: none;
}

editor.css
app-editor #editor {
  position: relative;
  height: 100%;
}

動作確認

npm run buildしてからnpm startを実行してください。

結果
image.png

:thumbsup::thumbsup::thumbsup::thumbsup::thumbsup::thumbsup::thumbsup::thumbsup::thumbsup::thumbsup::thumbsup:

最後に

  • riotのobservable機能がとても便利だということが分かった。
  • webpackでなぜかfsがうまく使えなかったし、electronをimportしようとするとエラーが出ちゃう。。。
13
14
2

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
13
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?