🐈

Railsでも使いやすいReactベースのMarkdown対応WYSIWYGエディタ「wysimark-lite」

2025/02/24に公開

背景

生成AIの回答でレポートを作成したいという要望があり、生成AIの回答はMarkdown形式で出力できるため、それをまとめれば簡単にレポートが作成できると考えました。
しかし、Markdown形式のテキストを編集するにはMarkdownの書式を理解する必要があるほか、実際の見た目も変換しないとわかりづらいという課題があります。そのため、Markdown形式のテキストを直感的に編集できるWYSIWYGエディタを探しましたが、ぴったりのものが見つかりませんでした。

そこで、MITライセンスで公開されているwysimarkというWYSIWYGエディタを改変し、wysimark-liteというnpmパッケージを作成しました。
開発中のReactプロジェクトだけでなく、Railsプロジェクトでも使う必要があったため、Railsでimportmapを使って簡単に導入できるように対応しています。

wysimark-liteの改変点

  • メニューの日本語対応
    ブラウザの言語設定が日本語の場合、メニューを日本語で表示するようにしました。(本家は英語のみ)
  • ボタンをトグル式に変更
    現在のカーソル位置にある装飾のON/OFF状態がボタンの見た目でわかるようにしました。(本家はボタンがアクティブにならない)
  • リストの階層対応
    リストの階層を変更できるボタンを追加しました。(本家はリストの階層表示は対応しているものの、ボタンはなし)
  • 次に入力する装飾状態の選択
    行末であらかじめ次に入力する装飾状態を選択できるようにしました。(本家では前の文字の状態がそのまま引き継がれるか、文字を選択してボタンで切り替える必要がある)
  • 空行で段落を分ける
    空行を挿入することで段落を分けられるようにしました。(本家は改行すると即座に新しい段落になる)
  • 通常のESMファイルと、依存関係をすべて含んだESMファイル(Rails用)を提供
  • 画像やファイルの添付機能を削除
    生成AIで作成するレポートには画像が不要であるうえ、依存するnpmパッケージが多かったため削除しました。

storybookを使ったDEMO

下記URLで試すことができます。
https://takeshy.github.io/wysimark-lite

使い方

Reactプロジェクトで使う場合

npm install wysimark-lite
import { Editable, useEditor } from "wysimark-lite";
import React from "react";

const Editor: React.FC = () => {
  const [value, setValue] = React.useState("");
  const editor = useEditor({});

  return (
    <div style={{ width: "800px" }}>
      <Editable editor={editor} value={value} onChange={setValue} />
    </div>
  );
};

Railsプロジェクトで使う場合

config/importmap.rbに以下を追記します。(誤字に注意してください)

pin "wysimark-lite", to: "https://cdn.jsdelivr.net/npm/wysimark-lite@latest/dist/index.js"

@latest は自分でバージョンを指定する(現在の最新は @0.14.2)方が、今後の予期せぬ変更を回避できて望ましい場合があります。

<div data-controller="wysimark">
  <%= hidden_field_tag :result, @research.result, data: { wysimark_target: "input" } %>
  <div data-wysimark-target="editor"></div>
</div>
// app/javascript/controllers/wysimark_controller.js
import { Controller } from "@hotwired/stimulus"
import { createWysimark } from "wysimark-lite"

export default class extends Controller {
  static targets = ["editor", "input"]

  connect() {
    const initialMarkdown = this.inputTarget.value
    this.wysimark = createWysimark(this.editorTarget, {
      initialMarkdown: initialMarkdown,
      onChange: (markdown) => {
        this.inputTarget.value = markdown
      }
    })
  }

  disconnect() {
    if (this.wysimark) {
      // Clean up if needed
      this.wysimark = null
    }
  }
}

Storybook

git clone https://github.com/takeshy/wysimark-lite.git
npm install
npm run storybook

備考

Railsでは bin/importmap pin パッケージ名 を実行すると、依存関係もあわせてvendor/javascript配下にファイルをダウンロードしてくれます。しかし、依存が多い場合には大量のファイルがpinされて混乱したり、失敗するケースがあります。そこでnpmパッケージ側で依存を含んだESMファイルを提供すれば、より手軽に使えることに気付きました。

この実装は、複数のビルドを行える tsup というツールを使い、tsup.config.ts のあるビルド設定に

noExternal: [/.*/],

を追加するだけで実現可能です。
また、Markdownを単純にHTMLで表示するだけのmarkdown-react-viewerも同様に作成しました。
Railsでnpmのライブラリをimportmapで使う場合に、使いたいライブラリが依存が多かった場合は大変で失敗も多いので、使いたいパッケージを呼び出すためだけのnpmプロジェクトを作成して、noExternal: [/.*/]でbuildしてできあがったjsをapp/javascriptの配下において、pin 'パッケージ名', to: 'buildしたjs'を指定するのがいいと思います。

オリジナルのwysimarkはプラグインベースで非常に改変しやすいコード構成になっており、MITライセンスで公開してくださっているため、VS CodeでCline(生成AIを使ったコード生成ツール)を使って簡単にカスタマイズできました。大変感謝しています。

Discussion