PHPの脆弱性への対応が完了しました。

業務効率化

個人用に痒いところに手が届く情報整理用Wiki

はじめに

社内の作業メモや手順書が、Excel・テキスト・チャットの過去ログにバラバラに散っていませんか。 Wikiツールを入れようとすると、サーバーの用意やクラウドサービスの利用申請が必要になり、情シスの審査で止まりがちです。

そこで HTMLファイル1つをダブルクリックするだけで動く メモWikiを作りました。この記事は、作った側が情シス(社内の情報システム担当)に説明するときの資料を兼ねています。そのため前半はセキュリティと運用、後半が機能と仕組みです。

  • サーバー不要・インストール不要(実行ファイルではなく、ただのHTMLファイル)
  • 外部ライブラリなし・外部通信なし(ブラウザの CSP で外への通信そのものを禁止)
  • データは「そのPCのブラウザ内」か「社内の共有フォルダ」にしか置かない

対象ブラウザは Chrome / Edge です。
チーム共有で使う File System Access API がこの2つでしか使えないためです。

情シス向けの要点(先に結論)

確認したいこと答え根拠・詳しくは
外部と通信するかしないCDN・外部ライブラリなし。CSP で外への通信をブラウザ側で禁止。通信の監視テストで外部へのリクエスト0件
インストールが要るか要らないブラウザで開くHTMLファイル。管理者権限・レジストリ・常駐プロセスなし
データはどこに置かれるか2か所だけそのPCのブラウザ内(IndexedDB / localStorage)か、利用者が選んだ共有フォルダ
誰が読み書きできるか共有フォルダの権限どおりツール独自の権限管理はない。フォルダのアクセス権(NTFS等)がそのまま効く
勝手に別のフォルダを触るか触らない利用者が選んで「許可」したフォルダだけ。ブラウザが毎回許可を確認する
メモに危ないコードを書かれたら実行されない本文は先にすべてエスケープしてから決まった書式だけをタグにする
バックアップはファイルサーバー側が本命共有フォルダは普通のファイル(.md・画像・JSON)。ツールからも1ファイルに書き出し・戻しができる
中身を確認できるかできる1ファイルなので、テキストエディタでコード全体を読める
規模は数人〜10人程度同時編集は警告のみ・編集履歴なし。部署全体なら専用Wikiを検討

セキュリティの詳細

外部通信をしない根拠

「外部通信なし」を、作りと仕組みの両方で担保しています。

1. 作り:外のものを何も読み込まない

  • CSS・JavaScript・アイコン・フォントはすべてHTMLの中に書いている(CDN・Webフォント・外部ライブラリなし)
  • 最初から入っている例の画像も、図形で描いたSVGをHTMLの中に埋め込んだもの(外部の画像やAIで生成した画像ではない)
  • コードの中で通信に使えるAPIは fetch が2か所だけ。どちらも、バックアップから戻すときに data: URL(ファイルの中身そのもの)を画像データに変えるためで、ネットワークには出ない
  • XMLHttpRequest・WebSocket・sendBeacon・eval・new Function・Service Worker・iframe・Cookie は使っていない

2. 仕組み:ブラウザ側で外への通信を禁止する(CSP)

HTMLの先頭に Content Security Policy(CSP)を書いています。これで、仮にメモの中に外部のURLが書かれても、ブラウザが通信そのものを止めます。

📄 memo-wiki.html の先頭

<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src data: blob:; base-uri 'none'; form-action 'none'">
指定意味
default-src 'none'書いていないものは全部禁止(外部のスクリプト・CSS・フォント・iframe など)
script-src 'unsafe-inline'HTMLの中に書いたスクリプトだけ動かす(外のスクリプトは読み込めない)
img-src data: blob:画像はファイルの中身(data:)とブラウザ内のデータ(blob:)だけ。外部の画像は表示しない
connect-src data: blob:fetch などの通信は外に出られない
form-action 'none'フォームの送信先を作れない

script-src 'unsafe-inline' は、1ファイルで完結させるために必要な指定です。メモの本文はエスケープしてから表示しているので、本文からスクリプトを差し込むことはできません(後述)。

エクスポートで作る「閲覧用HTML」にも、スクリプトを一切許可しない CSP(default-src 'none'; style-src 'unsafe-inline'; img-src data:)を入れています。

3. メモの中の外部画像・リンクの扱い

  • ![図](https://...) のように外部の画像を書いても読み込まず、「🖼 図(開く)」というリンクとして表示する
  • リンクは、利用者がクリックしたときだけ新しいタブで開く(rel="noopener" 付き)。開いた先との通信は、普段のブラウザ利用と同じ扱い

4. テストで確認したこと

Playwright(ブラウザを自動で操作するツール)で次を確かめています。

  • 起動して例のメモをすべて表示しても、外部へのリクエストは0件
  • ページの中から fetch('https://example.com/') を実行すると、CSP で止まる
  • 外部の画像を読み込もうとすると、ネットワークに出る前に CSP で止まる(失敗理由が csp)

社内のプロキシやファイアウォールで通信ログを確認できるなら、試用時に「このツールを使っている間に外部への通信がないこと」を見ておくと、より確実です。

データの保存場所

保存先中身場所消えるとき
ブラウザ内(IndexedDB memo-wiki)「💾 自分だけ」のメモ・画像・書き込み・作業チェック、共有フォルダのハンドル(場所の情報)そのPC・そのブラウザのプロファイルブラウザの「閲覧データの削除」でサイトデータを消したとき
ブラウザ内(localStorage)表示の設定(テーマ・一覧の形・開いていたメモ)、自分の名前、書きかけの下書き同上同上
共有フォルダ「👥 チーム共有」のメモ(.md)・画像・書き込み(.json)・作業チェック(.json)利用者が選んだフォルダファイルサーバー側で消したとき
  • 共有フォルダの中身は、エクスプローラーで見てもわかる普通のファイルです(構成は後述)。ツールがなくても、Markdown なのでテキストエディタで読めます
  • 最初から入っている「例」フォルダのメモ(使い方・例文・テンプレート)はHTMLの中にだけあり、共有フォルダには書き出しません
  • ブラウザ内の保存は、ブラウザによってはHTMLの置き場所(パス)ごとに別の保存領域として扱われます。HTMLの置き場所を途中で変えないようにしてください

共有フォルダへのアクセス

チーム共有には、ブラウザ標準の File System Access API を使っています。

  • 読み書きできるのは、利用者がフォルダ選択画面で選び、ブラウザの確認で「許可」したフォルダとその中だけ
  • 次に開いたときも、ブラウザが改めて許可を確認する(許可が切れていれば「前回のフォルダを開く」ボタンを出し、押したときにだけ許可を求める)
  • ツールが書き込み・削除するのは、メモのフォルダの中の .md・images/・drawings/・作業チェック.json と、空になったフォルダだけ。メモと関係ないファイルは書き換えも削除もしない
  • 共有フォルダのアクセス権(読み取り専用・変更可など)はそのまま効く。読み取り専用の人は、見ることはできても保存できない

ツールにはログインや権限管理の仕組みはありません。🔒 ロックは「うっかり編集の防止」で、誰でも解除できます。更新者の名前も利用者が自分で入力するもので、本人確認ではありません。閲覧・編集できる人の管理は、共有フォルダのアクセス権で行ってください。

メモに書かれた内容が実行されないこと

Markdown を画面に出すときは、最初に escapeHtml で本文全体をエスケープしてから、決まった書き方(見出し・太字・表・色の指定など)だけをタグに置き換えています。メモに <script> や onerror= などを書いても、ただの文字として表示されます。

📄 変換の最初に全体をエスケープする

let html = escapeHtml(text);

画像は data: か、メモのフォルダの中の images/〜 だけを表示します。リンクとして扱うのは https: http: mailto: file: と相対パスだけで、javascript: などはリンクになりません。

バックアップと復旧

困ったこと戻し方
メモを消した🗑 ゴミ箱から戻せる(30日で自動削除)
内容を書き換えてしまったツールに編集履歴はない。ファイルサーバーのバックアップ(Windows の「以前のバージョン」など)から .md を戻す
ブラウザのデータを消した(自分だけモード)エクスポートしたJSONをインポートして戻す
別のPC・別の共有フォルダへ移したいエクスポート(JSON)→ インポート
  • エクスポート(JSON)には、メモ・画像・書き込み・作業チェックの状態がすべて入り、そのままインポートで元に戻せます
  • 閲覧用HTMLは、メモを読むためだけのファイルです(スクリプトなし・戻すことはできない)
  • 共有フォルダのメモは普通のファイルなので、ファイルサーバーの通常のバックアップ対象に入れておくのがいちばん確実です

記録として残るもの

  • 各メモの先頭に、最後に更新した日時と人の名前(自己申告)
  • 作業チェックの各行に、チェックした時刻と人の名前
  • 誰がいつファイルを書き換えたかを正確に追う必要がある場合は、ファイルサーバーの監査ログを使ってください

導入チェックリスト(情シスとの確認用)

導入前に、次を決めておくとスムーズです。

#確認すること推奨
1HTMLファイルの置き場所利用者は読み取り専用の場所に置く(勝手に書き換えられないように)。更新は管理者が差し替える
2配布したHTMLが改ざんされていないか配布時にハッシュ値を記録しておき、certutil -hashfile memo-wiki.html SHA256 で照合
3共有フォルダの場所と権限チーム用のフォルダを1つ用意し、編集する人は変更可、見るだけの人は読み取り専用
4バックアップ共有フォルダをファイルサーバーのバックアップ対象に入れる。「以前のバージョン」が使えると復旧が楽
5ブラウザChrome または Edge の最新版。社内ポリシーで File System Access API(フォルダ選択)を止めていないか確認
6書いてはいけない情報パスワード・個人情報などを書かないルールを決める(ツール側で暗号化はしない)
7利用規模数人〜10人程度。超えるなら専用のWikiツールを検討
8責任者HTMLの更新・共有フォルダの管理をする人を決める

1ファイルなので、情シスでコードを読んで確認してもらうこともできます。
目安は約4,100行(JavaScript 約3,300行、使い方などの最初のメモを含む)です。

運用ルールの例

  • パスワード・APIキー・個人情報はメモに書かない(書くのは「どこに保管しているか」まで)
  • 本番作業の手順書は 🔒 ロックしておき、作業は作業チェックで記録する
  • 同じメモを複数人で同時に直さない(直すときはチャットで一声かける)
  • 月に1回、エクスポート(JSON)を取って決まった場所に保存する
  • HTMLを新しくするときは、管理者が差し替えて全員に連絡する

リスクと制約

リスク・制約影響対策
編集履歴がない書き換えた内容を前に戻せないファイルサーバーのバックアップ・「以前のバージョン」。定期エクスポート
同時編集は警告のみ2人が同じメモを直すと、どちらかを選ぶ必要がある保存時に競合を検知して選ばせる。運用で同時編集を避ける
権限管理・ログインがない共有フォルダに書ける人は誰でも編集・削除できる共有フォルダのアクセス権で管理。大事なメモはロック
暗号化しないフォルダを読める人はメモを読める機密情報を書かないルール。フォルダの権限を絞る
ブラウザ内保存は消えることがある「自分だけ」モードのメモがブラウザのデータ削除で消えるチームの情報は共有フォルダへ。定期エクスポート
Chrome / Edge のみほかのブラウザでは共有できない対象ブラウザを決めて案内する
規模人数・メモ数が多いと検索や競合が増える数人〜10人程度で使う。超えたら専用Wikiへ(.md なので移しやすい)

ここからは、利用者向けの機能と仕組みの紹介です。

全体像

項目内容
目的作業メモ・手順書・本番作業手順を一か所で書き、探し、共有する
本体memo-wiki.html の1ファイル(HTML / CSS / JavaScript をすべて内包)
外部依存なし(CDN・外部ライブラリ・外部通信を使わない。CSP で外への通信を禁止)
対応ブラウザChrome / Edge(推奨)
想定規模数人〜10人程度のチーム

画面は左に検索・フォルダ・タグ・メモ一覧、右にメモ本文という2カラム構成です。

保存先は2種類

項目💾 自分だけ👥 チーム共有
保存場所そのパソコンのブラウザ内(IndexedDB)社内の共有フォルダ(File System Access API)
見える人自分のみ同じフォルダを選んだ人全員
始め方設定不要保存先ボタン → フォルダを選ぶ → 許可
更新者の記録なし初回保存時に名前を聞いて記録
注意ブラウザのデータ削除で消える同時編集は警告のみ(自動統合なし)

保存先を切り替えても、元の場所のメモは消えません。共有フォルダが空のときは、ブラウザ内のメモをコピーするか確認します。

共有フォルダを選ぶ処理

ブラウザ標準の showDirectoryPicker でフォルダを選んでもらい、そのハンドルを IndexedDB に保存しておきます。次回からは同じフォルダを開き直せます(ブラウザの許可は求められます)。

📄 フォルダを選ぶ

if (!window.showDirectoryPicker) {
  return alert('このブラウザはチーム共有に対応していません。Chrome か Edge で開いてください。');
}
const handle = await showDirectoryPicker({ id: 'memo-wiki', mode: 'readwrite' });

同時編集への備え

複数人で使うと「同じメモを同時に保存した」問題が起きます。そこで次のようにしています。

  • 保存はメモ1件ずつ:全メモをまとめて書き戻さないので、別の人が別のメモを保存しても消えない
  • 競合チェック:開いたときのメモの更新時刻(共有フォルダではファイルの最終更新日時)を覚えておき、保存時に変わっていたら「上書き/相手の内容を読み込む」を選んでもらう
  • 下書きの退避:入力が止まって約1秒後に下書きを localStorage に退避。保存せずに閉じても、次回起動時に復元できる

自動マージはしません。同じメモを2人が同時に直した場合は、どちらを残すかを人が選びます。

共有フォルダのファイル構成

共有フォルダには、エクスプローラーで見ても分かる形で保存します。メモ1件につきフォルダを1つ作り、本文の Markdown・画像・書き込み・作業チェックをその中にまとめます。

📄 共有フォルダの中身

共有フォルダ/
├── 手順書/
│   ├── 本番リリース手順/
│   │   ├── 本番リリース手順.md   … メモ本文
│   │   ├── images/               … このメモで使っている画像だけ
│   │   ├── drawings/             … 画像への書き込み情報(画像と同じ名前+.json)
│   │   └── 作業チェック.json      … 作業チェックの状態
│   └── サーバー再起動手順/
│       └── サーバー再起動手順.md
├── 議事録/
│   └── 2026-10-14 定例/
│       └── 2026-10-14 定例.md
├── 未分類/          … フォルダなしのメモ
└── _ゴミ箱/         … ゴミ箱のメモ
  • ツール上のフォルダ名 親/子 は、そのまま入れ子のフォルダになります
  • 同じタイトルが重なったら (2) を付けます
  • ファイル名に使えない文字は _ に置き換えます
  • タイトルやフォルダを変えると、メモのフォルダが画像・書き込み・作業チェックごと移動・改名します
  • 本文から消した画像や、書き込みで置き換わった画像は、保存したときに自動で削除します
  • 空になったフォルダは自動で削除します
  • エクスプローラーで作ったフォルダ(空のフォルダも)や、メモのフォルダを動かした結果は、そのままツールのフォルダ構成として読み込みます

各メモの先頭には、管理用の情報(Front Matter)を書いています。

📄 手順書/本番リリース手順/本番リリース手順.md

---
id: m8x2k4p1a
title: 本番リリース手順
category: 手順書
tags: [本番, リリース]
updated: 2026-10-14T10:30:00+09:00
author: 山田
color: 赤
locked: true
---
## 事前確認
1. [ ] 作業連絡をチャットに投稿
1. [ ] バックアップ取得を確認

エクスプローラーやエディタで .md を直接書き換えても、ツールで再読み込みすれば反映されます。メモのフォルダごとエクスプローラーで動かした場合も、次に開いたときにその場所で読み込みます。

機能一覧

機能仕様
作成空のメモ、または「テンプレート」フォルダのメモから作成。テンプレートの {{今日}} {{時刻}} {{日時}} は作成時の日付・時刻になる
例使い方・仕様書・例文・テンプレートは「例」フォルダに最初から入っている。HTMLの中にだけあり、保存先には書き出さない。チーム共有ではテンプレートだけを一覧に出す
日付・時刻🕒 日時ボタン、または Ctrl+;(今日)・Ctrl+:(今の時刻)。表記は 2026/10/19(月) 14:30
名前・用語の候補@ で人の名前、Ctrl+Space で用語の候補。候補は左下の「📒 マスタ台帳」(専用の編集画面)から作る
検索タイトル・本文・タグ・フォルダ・更新者を空白区切りのAND検索。一致箇所を強調表示
フォルダ1メモに1つ。親/子 で入れ子。開け閉めできるツリー表示と一覧表示を切り替え。ドラッグで移動、フォルダ名の変更でまとめて移動。開閉状態は次回も保持
タグいくつでも。使用数順に15個表示し、残りは「ほか○件」に折りたたみ。複数選択はAND。入力時に候補表示
色ラベル12色から1つ。一覧の左端に表示し、色で絞り込み
ゴミ箱削除はゴミ箱へ。元に戻す・完全に削除・空にする。30日で自動削除
ロックロックしたメモは編集・削除できない
保存状態保存済み/編集中/未保存/保存中/保存できていない を文字で表示
テーマ自動・ライト・ダークを切り替え
入出力エクスポート(JSON)はメモ・画像・書き込み・作業チェックを1ファイルにまとめ、そのままインポートで戻せる。閲覧用HTMLは見るためだけのもの

ロック(うっかり編集の防止)

使い方や本番手順など、気軽に書き換えてほしくないメモは 🔒 ロックできます。

  • ロック中は 編集・削除ができない(E キーでの編集も無効)
  • 作業チェック・複製・フォルダの移動はできる(複製したメモはロックなし)
  • 解除は誰でもできる(確認ダイアログあり)

これは「うっかり防止」であって権限管理ではありません。
閲覧・編集の権限は共有フォルダのアクセス権で管理してください。

作業チェック(本番作業向け)

1. [ ] 手順 と書いた行は、表示画面でチェックできるチェックリストになります。

  • チェックした行は緑になり、時刻と名前を記録
  • 次にやる行を黄色で表示し、進捗バーで「済み / 全体」を表示
  • 記録をコピー(作業報告にそのまま貼れる)、リセットができる
  • チェック状態は手順本体とは別に保存するので、手順書そのものは汚れない
  • チーム共有では、チェック状態も全員に共有される

画像と書き込み

  • Ctrl+V やドラッグで画像を貼り付け(横幅1600pxを超える画像は自動で縮小)
  • プレビューの画像をクリックすると、四角・丸・矢印・線・ペン・文字・塗りで書き込みができる(色も変更可)
  • 書き込みは元画像とは別に保存するので、あとから直せる
  • 本文から消した画像や、書き込みで置き換わった画像は、保存したときに自動で削除

名前・用語の候補(マスタ台帳)

議事録やトラブル記録で何度も書く人の名前や用語は、左下の「📒 マスタ台帳」ボタンで開く専用画面に登録しておくと候補に出ます。普通のメモとは別枠で、一覧や検索には出ません。区分で絞り込み・検索・行の追加と削除ができ、区分は自由に増やせます。チーム共有なら全員で同じ台帳を使えます。

保存先には「マスタ台帳」というメモとして、次のような表で保存します。

📄 マスタ台帳(保存される形)

| 区分 | 名前 | よみ | 説明 |
|---|---|---|---|
| 人 | 山田 | やまだ | インフラ担当 |
| 用語 | 本番環境 | ほんばんかんきょう | お客様が使っている環境 |
| システム | 勤怠システム | きんたい | 毎月25日締め |
  • 区分が「人」の行は、本文で @ を打つと候補に出る(@山田 は表示画面で色付き)
  • それ以外の行は Ctrl+Space で候補に出る
  • 「よみ」を書いておけば、ひらがなでも探せる

表のマス目エディタ

「▦ 表」ボタンで、Excelのようなマス目で表を編集できます。セルの結合・分割、列幅、セル内改行に対応しています。

書き方(Markdown+独自拡張)

基本は Markdown です。ツールバーの「機能一覧」からボタンで挿入もできるので、記法を覚えなくても書けます。

書式書き方補足
見出し## 見出し 
太字・斜体・取り消し**太字** *斜体* ~~取消~~Ctrl+B / Ctrl+I
箇条書き・番号- 項目 1. 手順 
チェック1. [ ] 手順表示画面でチェックできる
文字色{赤:文字}11色
マーカー{マーカー黄:文字}7色
付箋:::赤 注意 〜 :::9色
コード前後を ``` で囲むコピーボタン付き

独自拡張の例

文字色・マーカー・付箋は、日本語の色名で書けるようにしました。

📄 文字色とマーカー

作業前に {赤:必ず} バックアップを取る。
対象は {マーカー黄:本番DBのみ}。

📄 付箋

:::赤 注意
メンテナンス中はアラートが鳴ります。監視チームに事前連絡すること。
:::

表は、セル結合(<< で左と結合、^^ で上と結合)と列幅の指定ができます。

📄 セル結合と列幅

{列幅: 120, 自動}
| 時刻 | 内容 |
|---|---|
| 10:00 | 障害発生を検知 |
| 10:30 | 遅延が解消したことを確認<br>(監視画面で確認) |
| ^^ | 関係者へ連絡 |

文字色・付箋・表の結合・列幅はこのツール独自の書き方です。
ほかの Markdown ツール(Qiita を含む)では、文字のまま表示されます。

コードの要点

本体は約4,100行(JavaScript 約3,300行)あるので、全文は記事に載せず、仕組みの要になる部分だけ紹介します。全文は末尾のリンクからどうぞ。

📄 コードの構成(memo-wiki.html の中)

<style>   画面のデザイン(ライト/ダーク)
<body>    画面の部品
<script type="text/plain" class="starter-note">  最初から入れておくメモ(「例」フォルダ:使い方・例文・テンプレート)
<script>
  共通の道具 / 色 / アプリの状態 / 保存状態の表示
  保存先1:このブラウザ(IndexedDB)
  保存先2:共有フォルダ(File System Access API)
  Markdown を画面用のHTMLにする
  左の一覧 / メモの表示 / ゴミ箱 / 作業チェック / 編集画面 / 下書きの退避
  書式ツールバー / 画像の追加 / 表エディタ / 画像への書き込み
  読み込みと保存先の切り替え / バックアップ・書き出し / 表示テーマ / 画面の操作 / 起動
</script>

保存先を同じ形にそろえる

ブラウザ内と共有フォルダのどちらでも、画面側は store.save(note) を呼ぶだけで済むように、2つの保存先に同じ名前の関数(load / save / remove / putImage / getRun / putRun …)を持たせています。

さらに、保存系の関数を包んで「保存中 → 保存済み/保存できていない」の表示を自動で切り替えています。

📄 保存状態の表示を自動で切り替える

function withSaveStatus(storage) {
  for (const name of ['save', 'saveAll', 'remove', 'putImage', 'putRun', 'deleteRun']) {
    const original = storage[name];
    storage[name] = async (...args) => {
      setSaveStatus('saving');
      try {
        const result = await original(...args);
        if (result !== 'conflict') lastSavedAt = Date.now();
        setSaveStatus(dirty || result === 'conflict' ? 'dirty' : editing ? 'editing' : 'saved');
        return result;
      } catch (error) {
        setSaveStatus('error');
        throw error;
      }
    };
  }
  return storage;
}

ブラウザ内:1件ずつ・取りこぼさずに保存

IndexedDB の1つのトランザクションの中で「読む → 書き換える → 書く」を行うので、別のタブと同時に保存しても他のメモを消しません。開いたときの更新時刻(_base)と保存されている更新時刻が違えば、競合として返します。

📄 IndexedDB への保存(抜粋)

// 読む→書き換える→書く を1回で行う(別のタブと同時に保存しても取りこぼさない)
update: (key, change) => run('readwrite', (kv, done) => {
  const r = kv.get(key);
  r.onsuccess = () => {
    const { value, result } = change(r.result || []);
    kv.put(value, key);
    done(result);
  };
}),

📄 競合チェック付きの保存

async save(note, force) {
  const result = await idb.update('notes', (list) => {
    const index = list.findIndex((x) => x.id === note.id);
    if (!force && index >= 0 && list[index].updated !== note._base) return { value: list, result: 'conflict' };
    if (index >= 0) list[index] = makeNote(note);
    else list.push(makeNote(note));
    return { value: list };
  });
  if (result !== 'conflict') note._base = note.updated;
  return result;
},

共有フォルダ:ファイルの更新日時で競合を見分ける

共有フォルダでは、読み込んだときのファイルの最終更新日時(lastModified)を覚えておき、保存前に変わっていないかを確かめます。タイトルやフォルダが変わったら、新しい場所にメモのフォルダを作り、画像・書き込み・作業チェックを移してから古いフォルダを片付けます。最後に、本文で使っている画像だけがフォルダに残るようにそろえます(syncImages)。

📄 共有フォルダへの保存

async function save(note, force) {
  let now = locations.get(note.id);
  if (!force && note._mtime) {
    const file = await stillThere(now, note.id);
    if (!file || file.lastModified !== note._mtime) return 'conflict';
  }
  if (now && !(await stillThere(now, note.id))) {
    now = await locate(note.id);
    if (now) locations.set(note.id, now);
    else locations.delete(note.id);
  }
  const place = await placeFor(note);
  const file = place.dir + '.md';
  const dir = await dirAt(pathOf(place), true);
  const moved = now && pathOf(now) !== pathOf(place);
  const handle = await writeFile(dir, file, noteToFile(note));
  if (now && !moved && now.file !== file) await dir.removeEntry(now.file).catch(() => {});
  note._mtime = (await handle.getFile()).lastModified;
  locations.set(note.id, { ...place, file });
  if (moved) await moveNoteDir(pathOf(now), now.file, dir);
  await syncImages(note, dir);
}

📄 ファイル名に使えない文字を置き換える

const safeFileName = (name, fallback) =>
  String(name || '').replace(/[\\/:*?"<>|\x00-\x1f]/g, '_').replace(/[. ]+$/, '').trim().slice(0, 60) || fallback;

独自記法:日本語の色名を正規表現にする

{赤:文字} や {マーカー黄:文字} は、色の一覧から正規表現を組み立てて変換しています。「マーカー黄」が「黄」より先に当たるよう、長い名前から並べるのがポイントです。

📄 色の書き方を見分ける正規表現

const INLINE_COLOR_CLASS = {
  ...Object.fromEntries(Object.keys(TEXT_COLORS).map((name) => [name, 'c-' + name])),
  ...Object.fromEntries(Object.keys(MARKER_COLORS).map((name) => ['マーカー' + name, 'm-' + name])),
};
const INLINE_COLOR_RE = new RegExp(`\\{(${Object.keys(INLINE_COLOR_CLASS).sort((a, b) => b.length - a.length).join('|')}):([^{}]+)\\}`, 'g');

Markdown の変換は、最初に escapeHtml で全体をエスケープしてから、決まった書き方だけをタグに置き換えています。メモに <script> などが書かれても実行されません。

作業チェック:手順の文字で行を見分ける

チェック状態は手順本体とは別に保存しています。行番号で覚えると手順を1行足しただけでずれるので、「手順の文字+同じ文字の何個目か」をキーにしています。

📄 チェック行のキー

// チェックボックスは「文字+同じ文字の何個目か」で見分ける(チェック状態の保存に使う)
function taskItemHtml(text, alreadyDone) {
  const count = (taskCounter.get(text) || 0) + 1;
  taskCounter.set(text, count);
  return `<li class="task${alreadyDone ? ' done' : ''}"><label>`
    + `<input type="checkbox" data-task="${escapeHtml(text + '#' + count)}" ...>`
    + /* 省略 */ '';
}

外部の画像はリンクにする

CSP で止めるだけだと「画像が壊れて見える」ので、表示の段階で外部の画像をリンクに変えています。

📄 画像の書き方を変換する(抜粋)

.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (all, alt, url) => {
  if (/^data:image\//.test(url)) return `<img alt="${alt}" src="${url}">`;
  // 外の画像は読み込まない(外部通信をしないため)。リンクとして表示する
  if (/^https?:/.test(url)) return `<a href="${url}" target="_blank" rel="noopener">🖼 ${alt || '外部の画像'}(開く)</a>`;
  if (/^images\/[\w.-]+$/.test(url)) return `<img alt="${alt}" data-src="${url}">`;
  return all;
})

ソースコード全文

ソースコード全文はこちら:https://github.com/sora2424/memo-wiki

おわりに

「サーバーを立てずに、まずはチームで手順を共有したい」という場面では、HTMLファイル1つでも十分実用になりました。 情シスに説明するときも、「外部通信はCSPで禁止している」「データは共有フォルダの普通のファイルで、権限もバックアップも今のファイルサーバーの仕組みに乗る」と言えるのが大きかったです。

同じような悩みを持っている方の参考になれば幸いです。

そら

千葉県在住 エンジニア6年目
最近はApexばっかりやってます・・・
(写真はイメージです。こんなイケメンではない)

コメントを書く