11
2

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 1 year has passed since last update.

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

Last updated at Posted at 2022-10-05

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

概要

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

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

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

機能紹介

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

Markdownを一瞬でレンダーする!

筆者が作ったlit-markdownを使って素早くMarkdownをレンダーします!というより、Markedのお兄さんたちのおかげです:sweat_smile:

ezgif.com-gif-maker (27).gif

画像を簡単に追加できる!

筆者のlit-markdown-editorを使って快適に編集できます!この記事の自画自賛のパターンは見えてきたのでしょうか?

ezgif.com-gif-maker (28).gif

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

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

ezgif.com-gif-maker (28).gif

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

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

ezgif.com-gif-maker (29).gif

ElectronでTypeScriptを使う

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

フロントエンドにはesbuildを使いました。そして、Node向けのコードは普通のtscでコンパイルしました。

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

package.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

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に公開しているので、以下のレポをご確認ください。

まとめ

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

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

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

更新備忘録

2022年10月14日

最近、Angularの勉強でRxJSの存在を知ってとてもいいなと思いましたのでこのMarkdownワープロでもパフォーマンスを改善するために導入してみました。

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

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

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を使って、200m秒に一度最新のイベントだけを配信するようにしました。

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

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

2022年10月18日

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

...

<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です。

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

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で使う場合は以下のパッケージをインストールする必要があります。

11
2
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
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?