簡単なMarkdownワープロを作りました！

はいさい！ちゅらデータぬオースティンやいびーん！

# 概要

Electronを使って、パソコン上でMarkdownのファイルを編集できるソフトを作ってみましたので、ご紹介したいと思います。

https://tronicboy1.github.io/markdown-editor/ 

最近は、グループ会社のブログむけに記事を書くことが多くて、いつもQiitaのMarkdownワープロを拝借していました:sweat_smile:が、Qiitaさんに出さないのに申し訳ないなと思い、自分で作っておこうというのがその経緯です。

グループ会社のブログでちょこちょこ出していただいているのでご関心あれば読んでいただければと思います。

https://datumstudio.jp/blog/0630_firebase_cloud_messaging/

## 機能紹介

様々な面白いことができるので以下の動画で思う存分、自慢させていただきます。

### Markdownを一瞬でレンダーする！

筆者が作った[lit-markdown](https://www.npmjs.com/package/lit-markdown)を使って素早くMarkdownをレンダーします！というより、[Marked](https://github.com/markedjs/marked)のお兄さんたちのおかげです:sweat_smile:

![ezgif.com-gif-maker (27).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/e0f625da-9ae0-4136-bce9-084fb5f7d834.gif)

### 画像を簡単に追加できる！

筆者の[lit-markdown-editor](https://www.npmjs.com/package/lit-markdown-editor)を使って快適に編集できます！この記事の自画自賛のパターンは見えてきたのでしょうか？

![ezgif.com-gif-maker (28).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/b3a96172-7844-8768-75de-ed7f66b34207.gif)

### .zipファイルに入れた画像を全てMarkdownとともに保存できる！

JSZipを使って保存できるようにしました。

![ezgif.com-gif-maker (28).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/6b79301e-5433-dbf5-a0d2-8fbbf7bb5465.gif)

### .mdもしくは.zipファイルを開けます！

.zipで写真が入っていれば、写真を再度読み込んでくれます。

![ezgif.com-gif-maker (29).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/255f4d95-f5cc-b585-9e8e-7bf6bad66b88.gif)


# ElectronでTypeScriptを使う

ElectronでTypeScriptを使うこともとても面白くてお勧めします。ElectronのNode向けのコード(main.js）とElectronのブラウザで実行されるコード(renderer.js)をそれぞれにビルドするようにしました。

フロントエンドには[esbuild](https://esbuild.github.io/)を使いました。そして、Node向けのコードは普通の`tsc`でコンパイルしました。

以下、それぞれの設定をご参考までに共有します。

## package.jsonの設定
```json:package.json
{
  "name": "markdown-editor",
  "version": "1.0.0",
  "main": "lib/main.js",
  "license": "MIT",
  "scripts": {
    "build": "tsc && npx esbuild src/application/renderer.ts --bundle --outfile=lib/renderer.js",
    "start": "electron-forge start",
    "package": "electron-forge package",
    "make": "yarn run build && electron-forge make"
  },
  "dependencies": {
    "electron-squirrel-startup": "^1.0.0",
    "esbuild": "^0.15.9",
    "jszip": "^3.10.1",
    "lit-markdown-editor": "^2.2.0",
    "marked": "^4.1.0"
  },
  "devDependencies": {
    "@electron-forge/cli": "^6.0.0-beta.66",
    "@electron-forge/maker-deb": "^6.0.0-beta.66",
    "@electron-forge/maker-rpm": "^6.0.0-beta.66",
    "@electron-forge/maker-squirrel": "^6.0.0-beta.66",
    "@electron-forge/maker-zip": "^6.0.0-beta.66",
    "@types/marked": "^4.0.7",
    "electron": "^21.0.1"
  },
  "config": {
    "forge": {
      "packagerConfig": {},
      "makers": [
        {
          "name": "@electron-forge/maker-squirrel",
          "config": {
            "name": "markdown_editor"
          }
        },
        {
          "name": "@electron-forge/maker-zip",
          "platforms": [
            "darwin"
          ]
        },
        {
          "name": "@electron-forge/maker-deb",
          "config": {}
        },
        {
          "name": "@electron-forge/maker-rpm",
          "config": {}
        }
      ]
    }
  }
}
```

## tsconfig.json

```json-doc:tsconfig.json
{
  "compilerOptions": {
    "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
    "lib": [
      "DOM"
    ],
    "module": "Node16" /* Specify what module code is generated. */,
    "rootDir": "src/electron" /* Specify the root folder within your source files. */,
    "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
    "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */,
    "typeRoots": ["types"],                                  /* Specify multiple folders that act like './node_modules/@types'. */
    "outDir": "lib",                                   /* Specify an output folder for all emitted files. */
    "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
    "strict": true /* Enable all strict type-checking options. */,
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  },
  "files": ["src/electron/main.ts", "src/electron/preload.ts"]
}
```

詳しくみたければ、GitHubに公開しているので、以下のレポをご確認ください。

https://github.com/tronicboy1/markdown-editor

# まとめ

以上、自慢のおもちゃを紹介しました！

Electronをなかなか使ったことがなかったので、すごく楽しいなと思いました。

今後、Electronをうまいこと使うことができたらと思います。


# 更新備忘録

## 2022年10月14日
最近、Angularの勉強で[RxJS](https://rxjs.dev/)の存在を知ってとてもいいなと思いましたのでこのMarkdownワープロでもパフォーマンスを改善するために導入してみました。

例えば、以前は`input`のイベントが配信されるたびにプレビューの`<article>`要素を再レンダーしていましたが、ドキュメントが長くなるにつれ、重くなってしまう恐れがあります。

そこで、以下のようにRxJSを使って改善しました。

```typescript:renderer.ts
import { fromEvent, map, sampleTime } from "rxjs";

...

const editor = document.querySelector("lit-markdown-editor")!;
const article = document.querySelector("article")!;
const editorInput$ = fromEvent(editor, "input");
editorInput$
  .pipe(
    sampleTime(200),
    map(event => {
      const target = event.target;
      if (!(target instanceof LitMarkdownEditor)) throw TypeError();
      return target.value;
    })
  )
  .subscribe(value => {
    window.localStorage.setItem("cache", value);
    renderMarkdown(value).then(rawHTML => {
      article.innerHTML = rawHTML;
    });
  });
```

[`sampleTime`](https://rxjs.dev/api/operators/sampleTime)を使って、200m秒に一度最新のイベントだけを配信するようにしました。

そうすると、DOMを再レンダーするという重い処理が200m秒に一度しか行われなくなり、ユーザー体験は悪くならないが、パフォーマンスの問題も発生しないでしょう。
<sub>（おそらく）</sub>

@Qiita も同じような仕組みを導入したらいかがでしょうか？QiitaさんのEditorは不規則的な感じがして時々更新させるのに時間がかかります。

## 2022年10月18日

以前、`index.html`に以下のように隠れた`<input type="file">`でユーザーからのファイル入力を受け取っていました。

```html
...

<body>
  <a></a>
  <div id="root"></div>
  <input id="markdown-input" type="file" accept=".md, application/zip" hidden>

...
```

しかし、新しいブラウザAPIでこのような`<input>`要素をHTMLに埋め込む必要がなくなりました。

それは、[`File System Access API`](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)です。

このAPIを導入すれば、以下のような書き方ができます。

```typescript:src/application/scripts/open-button.ts
import loadZipFile from "../helpers/load-zip";

const editor = document.querySelector("lit-markdown-editor")!;
const openButton = document.querySelector<HTMLButtonElement>("button#open")!;

const handleOpenClick: EventListener = () => {
  const pickerOpts: OpenFilePickerOptions = {
    types: [
      {
        accept: {
          "text/plain": [".md"],
          "application/zip": [".zip"],
        },
      },
    ],
    excludeAcceptAllOption: true,
    multiple: false,
  };
  window
    .showOpenFilePicker(pickerOpts) // ここがFile System Access APIの機能です！
    .then(([fileHandler]) => fileHandler.getFile()) // ユーザーが入力したファイルを普通の<input>と同様に取得できます。
    .then(file => {
      if (!(file && file.size)) return;
      const regex = /.*\.md$/;
      if (regex.test(file.name)) {
        return file.text().then(markdown => {
          if (!markdown.length) return;
          editor.value = markdown;
        });
      }
      loadZipFile(file).then(markdown => {
        editor.value = markdown;
      });
    });
};

openButton.addEventListener("click", handleOpenClick);
```

___このAPIはまだFirefoxとSafariがサポートしていないのでご注意を！___

また、新しい機能なので、**TypeScriptのWindowインタフェースにもない**のです。

TypeScriptで使う場合は以下のパッケージをインストールする必要があります。

https://www.npmjs.com/package/@types/wicg-file-system-access