個人用に痒いところに手が届く情報整理用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. メモの中の外部画像・リンクの扱い
のように外部の画像を書いても読み込まず、「🖼 図(開く)」というリンクとして表示する- リンクは、利用者がクリックしたときだけ新しいタブで開く(
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は、メモを読むためだけのファイルです(スクリプトなし・戻すことはできない)
- 共有フォルダのメモは普通のファイルなので、ファイルサーバーの通常のバックアップ対象に入れておくのがいちばん確実です
記録として残るもの
- 各メモの先頭に、最後に更新した日時と人の名前(自己申告)
- 作業チェックの各行に、チェックした時刻と人の名前
- 誰がいつファイルを書き換えたかを正確に追う必要がある場合は、ファイルサーバーの監査ログを使ってください
導入チェックリスト(情シスとの確認用)
導入前に、次を決めておくとスムーズです。
| # | 確認すること | 推奨 |
|---|---|---|
| 1 | HTMLファイルの置き場所 | 利用者は読み取り専用の場所に置く(勝手に書き換えられないように)。更新は管理者が差し替える |
| 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で禁止している」「データは共有フォルダの普通のファイルで、権限もバックアップも今のファイルサーバーの仕組みに乗る」と言えるのが大きかったです。
同じような悩みを持っている方の参考になれば幸いです。
