Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
14
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

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

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しようとするとエラーが出ちゃう。。。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
14
Help us understand the problem. What are the problem?