ディレクトリ構造をサクッとツリー図にする「Directory tree generator」を作りました

ディレクトリ構造をサクッとツリー図にする「Directory tree generator」を作りました

READMEやドキュメントにディレクトリ構造を載せたいとき、毎回 ├──└── を手打ちしていませんか?
地味に面倒で、しかも深い階層になるとズレが気になって直したり、深さが変わるたびに縦線の調整が必要になったり。
そこで、インデントを入れたテキストをそのままツリー図に変換するツールを作りました。

まずは完成品

https://mifa.tokyo/tools/directory-tree-generator

主な機能:

  • インデントされたテキストをリアルタイムでツリー図に変換
  • スペース・タブ混在も自動判定
  • # 行をコメントとしてツリーに挿入
  • テキストのコピー / .txt ダウンロード / PNG画像として保存
  • 全角スペースの自動補正

使い方

左の入力欄に、インデントでディレクトリ構造を書くだけです。

src
  pages
    index.astro
  components
    Header.astro
  styles
    global.css
public
  favicon.ico

右にすぐ変換されます。インデントがスペースでもタブでも自動で判定します。

技術的な実装

1. インデントの自動検出

ユーザーにインデント種別を選ばせるUIは省いて、入力テキストから自動で判定する方式にしました。
やっていることはシンプルで、コメント行と空行を除いた最初の有効行を見て、インデントに使われている文字を確認するだけです。

JavaScript

function detectIndent(lines: string[]): IndentInfo {
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const m = line.match(/^(\s+)/);
    if (!m) continue;
    const ws = m[1];
    if (ws.includes('\t')) {
      const tabs = (ws.match(/\t/g) ?? []).length;
      return { type: 'tab', size: tabs, label: `タブ×${tabs}` };
    }
    return { type: 'space', size: ws.length, label: `スペース×${ws.length}` };
  }
  return { type: 'space', size: 2, label: 'デフォルト (スペース×2)' };
}

タブが含まれていればタブ、そうでなければスペースとして扱い、最初のインデント量をそのまま1単位として使います。スペース2つで書いても4つで書いても、それぞれ正しく深さに変換されます。

2. コメント行の枝判定除外

# で始まる行をコメントとしてツリーに挿入できます。枝記号(├─ └─)を付けずに親の縦線だけ維持して表示するイメージです。

.
├─ src
│  # コンポーネント類
│  ├─ components
│  └─ styles
└─ public

ここで気をつけないといけないのが、コメント行が 隣接する実ノードの枝判定に影響しないようにすることです。
通常のノードは「同じ深さに後続ノードがあれば ├─、なければ └─」で枝を決めます。コメント行をそのまま混ぜると、コメントを見て ├─ にすべき実ノードが └─ になってしまうことがあります。

JavaScript

// isLast: コメント行を除外して判定
let isLast = true;
for (let j = i + 1; j < nodes.length; j++) {
  if (nodes[j].depth < d) break;
  if (nodes[j].depth === d && !nodes[j].isComment) {
    isLast = false;
    break;
  }
}

同様に、コメントが最後の行のとき(後続に実ノードがない)は縦線も不要なので、isLast をそのままコメント行の表示にも流用しています。

JavaScript

function commentStr(isLast: boolean, depth: number): string {
  const pipe  = depth > 1 ? ' │  ' : '│  ';
  const blank = depth > 1 ? '    ' : '   ';
  return isLast ? blank : pipe;
}

3. 全角スペースの自動補正

日本語環境だと、IMEの影響でインデントに全角スペースが混入することがあります。ツリー変換時にインデントとして認識されないため、入力のたびに検出して半角に置換するようにしました。
単純に textarea.value を書き換えるだけだと、カーソル位置が先頭にリセットされてしまいます。selectionStart / selectionEnd で書き換え前の位置を保存して、書き換え後に復元することで自然な入力感を保てます。

JavaScript

function normalizeInput(el: HTMLTextAreaElement): void {
  const normalized = el.value.replace(/ /g, ' ');
  if (normalized === el.value) return; // 全角がなければ何もしない
  const start = el.selectionStart;
  const end = el.selectionEnd;
  el.value = normalized;
  el.setSelectionRange(start, end); // カーソル位置を復元
}

全角スペースがない場合は早期リターンするので、通常の入力時は value の比較だけで終わります。

4. PNG生成をCanvas APIで自前実装

PNG出力はよく html2canvas が使われますが、今回はテキストを描画するだけなのでCanvas APIを直接使って実装しました。外部ライブラリへの依存をなくしつつ、Retina対応や文字色の切り替えも細かく制御できます。

JavaScript

const dpr = window.devicePixelRatio || 2;
cvs.width = w * dpr;
cvs.height = h * dpr;
ctx.scale(dpr, dpr);

devicePixelRatio を掛けてキャンバスの実解像度を上げ、scale() で描画座標を元に戻すことで高DPIディスプレイでも滲まない画像になります。
ライトモード / ダークモードの判定も matchMedia で拾えるので、保存した画像がユーザーの環境に合った配色になります。

JavaScript

const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const bg = isDark ? '#1a1a1a' : '#ffffff';
const fg = isDark ? '#e8e8e8' : '#1a1a1a';

コメント行はグレーで描画して、テキスト出力との見た目の一貫性を持たせています。

まとめ

ツール自体の実装はシンプルですが、「インデント自動判定」「コメント行の枝への影響を排除」あたりは地味に考える箇所がありました。
ドキュメントを書く機会があれば使ってみてください。