1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは、小さな町工場でデータサイエンティストをしているココアです。

みなさんはobsidianを用いてプログラムのコードを管理されたり、整理された経験はありますか?
実際にobsidianでは複数端末で同期できるので、趣味で作成したコードの共有としてobsidianはかなり重宝しています。
しかし、エディタを用いて作成したコードとobsidianには、ちょっと困ったポイントがあります。

ObsidianでVSCodeなどのテキストエディタからコードをコピー&ペーストすると、余計な改行が入ってしまう経験はないでしょうか。それによってペーストしたコードを手動で囲むのも面倒です。

この問題を解決するため、VSCodeなどのエディタからコードをペーストする際に、余計な改行を自動削除し、さらに適切な言語のコードブロックで自動的に囲むObsidianプラグイン「Code Paste Cleaner」を開発しました。

本記事では、この問題の原因、プラグインの機能、そして技術的な実装について解説します。

問題の原因

なぜVSCodeから貼り付けると改行が増えるのか

VSCodeからObsidianにコードを貼り付けると、各行の間に余計な空行が挿入される現象が発生します。これは以下の理由によるものです。

  1. クリップボードのデータ形式
    VSCodeはコピー時に、クリップボードに複数の形式でデータを保存します。
  • text/plain:プレーンテキスト
  • text/html:HTML形式(スタイル情報を含む)
  1. Obsidianの解釈
    Obsidianがペーストを処理する際、HTML形式を優先的に解釈します。VSCodeのHTML形式では、各行が<div><p>タグで囲まれており、Obsidianがこれを解釈する過程で余計な改行が挿入されます。
<!-- VSCodeがクリップボードに保存するHTML形式のイメージ -->
<div>function example() {</div>
<div>  return true;</div>
<div>}</div>

この構造がObsidianで解釈されると、各<div>の間に改行が追加されてしまうのです。

プラグインの機能

基本機能

「Code Paste Cleaner」プラグインは、以下の基本機能を提供します。

  1. 余計な改行の自動削除:HTML形式によって挿入される余計な改行を自動的に削除
  2. 自動コードブロック化:ペーストされたコードを自動的に```で囲む
  3. 言語の自動検出:コードの内容から言語を自動判定してシンタックスハイライトを適用
  4. 最小行数の設定:コードブロック化する最小行数を指定可能

特殊コンテンツの自動検出

プラグインは、コード以外の特殊なコンテンツも自動的に検出し、適切な形式で貼り付けます。

Markdown表の検出

Markdown形式の表を検出し、コードブロックで囲まずにそのまま貼り付けます。
検出条件:

  • パイプ(|)で区切られた列がある
  • セパレーター行(例:|---|---|)が含まれる
| Name | Age | City |
|------|-----|------|
| Alice | 25 | Tokyo |
| Bob | 30 | Osaka |

Mermaid記法の検出

Mermaidダイアグラムを検出し、自動的に```mermaidブロックで囲みます。
検出されるMermaid記法:

  • graph, flowchart (TD/TB/BT/RL/LR)
  • sequenceDiagram
  • classDiagram
  • stateDiagram
  • erDiagram
  • gantt
  • pie
  • journey
  • gitGraph
  • C4Context
  • mindmap
  • timeline

Gitとかmarkdownユーザーは重宝していることでしょう。

SVGの自動検出

SVGコードを検出し、コードブロックで囲まずにそのままレンダリング可能な形式で貼り付けます。
検出条件:

  • <svg>タグで始まる
  • </svg>タグで終わる

ターミナル/シェルコードの検出

ターミナルやシェルのコマンド、エラーメッセージを検出し、```bashブロックで囲みます。
検出されるパターン:
プロンプト記号:

$ ls -la
# apt update
% echo "hello"
> dir
user@host$ git status
~/project$ npm install
C:\Users\name> python script.py

エラーメッセージ:

Error: Cannot find module 'express'
fatal: not a git repository
warning: LF will be replaced by CRLF
bash: command not found
permission denied

一般的なコマンド:

ls, cd, pwd, mkdir, rm, cp, mv
cat, grep, find, chmod, chown, sudo
git, npm, yarn, pip, python, node
docker, kubectl, ssh, scp

言語の自動検出

プラグインは、以下の言語を自動検出します。

  • TypeScript
  • JavaScript
  • Python
  • Java
  • C++
  • C
  • C#
  • Go
  • Rust
  • HTML
  • CSS
  • JSON
  • SQL
  • Bash/Shell
  • Ruby
  • PHP
    言語検出は、コード内の特徴的なキーワードや構文パターンを使用して行われます。

設定項目

プラグインの設定画面(Settings → Code Paste Cleaner)では、以下の項目をカスタマイズできます。

Code Block Settings

Auto code block(自動コードブロック化)

  • 有効:貼り付けられたコードを自動的に```で囲む
  • 無効:コードブロックを作成せず、プレーンテキストとして貼り付け

Remove extra line breaks(余計な改行の削除)

  • 有効:VSCodeなどから発生する余計な改行を自動削除
  • 無効:改行の削除を行わず、プレーンテキストをそのまま貼り付け

Default language(デフォルト言語)

  • 言語の自動検出に失敗した場合に使用する言語を指定
  • 空欄の場合は自動検出のみ

Minimum lines for code block(コードブロック化の最小行数)

  • 自動コードブロック化を行う最小行数
  • デフォルト:2行
  • 1行のコードスニペットはコードブロック化しない設定が可能

Special Content Detection

Enable Markdown table detection(Markdown表の検出)

  • 有効:Markdown表を自動検出してコードブロックで囲まず、そのまま貼り付け
  • 無効:表も通常のテキストとして処理

Enable Mermaid detection(Mermaid記法の検出)

  • 有効:Mermaid記法を自動検出して```mermaidブロックで囲む
  • 無効:Mermaidも通常のテキストとして処理

Enable SVG detection(SVGの検出)

  • 有効:SVGコードを自動検出してコードブロックで囲まず、レンダリング可能な形式で貼り付け
  • 無効:SVGも通常のテキストとして処理

Enable Terminal/Shell detection(ターミナル/シェルの検出)

  • 有効:ターミナルコマンドやエラーメッセージを自動検出して```bashブロックで囲む
  • 無効:ターミナル出力も通常のテキストとして処理

技術的な実装

開発環境

プラグインは以下の技術スタックで開発しました。

  • 言語:TypeScript
  • ビルドツール:esbuild
  • ターゲット環境:Obsidian Plugin API

プロジェクト構成

obsidian-code-paste/
├── main.ts              # メインのプラグインロジック
├── manifest.json        # プラグインのメタデータ
├── package.json         # npm設定
├── tsconfig.json        # TypeScript設定
├── esbuild.config.mjs   # esbuild設定
└── styles.css           # スタイルシート

ペーストイベントのインターセプト

Obsidianのエディタペーストイベントをインターセプトし、プレーンテキストのみを取得します。

async onload() {
  await this.loadSettings();
  // ペーストイベントをインターセプト
  this.registerEvent(
    this.app.workspace.on('editor-paste', (evt: ClipboardEvent, editor: Editor) => {
      this.handlePaste(evt, editor);
    })
  );
  this.addSettingTab(new CodePasteSettingTab(this.app, this));
}

ペースト処理のコアロジック

ペースト処理では、以下の順序で処理を行います。

handlePaste(evt: ClipboardEvent, editor: Editor) {
  if (!evt.clipboardData) {
    return;
  }
  // プレーンテキストを取得(HTML形式を無視)
  const plainText = evt.clipboardData.getData('text/plain');
  if (!plainText) {
    return;
  }
  // 特殊コンテンツタイプを検出
  const isMarkdownTable = this.settings.enableTableDetection && 
                          this.detectIfMarkdownTable(plainText);
  const isMermaid = this.settings.enableMermaidDetection && 
                    this.detectIfMermaid(plainText);
  const isSVG = this.settings.enableSVGDetection && 
                this.detectIfSVG(plainText);
  const isTerminal = this.settings.enableTerminalDetection && 
                     this.detectIfTerminal(plainText);
  // 各タイプに応じた処理
  if (isMarkdownTable) {
    evt.preventDefault();
    let processedText = plainText;
    if (this.settings.removeExtraLineBreaks) {
      processedText = this.removeExtraLineBreaksForTable(processedText);
    }
    editor.replaceSelection(processedText);
    return;
  }
  // ... その他の特殊コンテンツ処理
  // コードっぽいかどうかを判定
  const looksLikeCode = this.detectIfCode(plainText);
  if (!looksLikeCode) {
    // コードでなければデフォルトの動作
    return;
  }
  // デフォルトのペースト動作をキャンセル
  evt.preventDefault();
  // テキストを処理
  let processedText = plainText;
  // 余計な改行を削除
  if (this.settings.removeExtraLineBreaks) {
    processedText = this.removeExtraLineBreaks(processedText);
  }
  // 自動コードブロック化
  if (this.settings.autoCodeBlock) {
    const lineCount = processedText.split('\n').length;
    if (lineCount >= this.settings.minLinesForCodeBlock) {
      const language = this.detectLanguage(processedText) || 
                       this.settings.defaultLanguage;
      processedText = '```' + language + '\n' + processedText + '\n```';
    }
  }
  // エディタに挿入
  editor.replaceSelection(processedText);
}

コード検出ロジック

コードかどうかを判定するため、以下のパターンを使用します。

detectIfCode(text: string): boolean {
  // 特殊コンテンツは除外
  if (this.detectIfMarkdownTable(text) || 
      this.detectIfMermaid(text) || 
      this.detectIfSVG(text) || 
      this.detectIfTerminal(text)) {
    return false;
  }
  // コードの特徴を検出
  const codePatterns = [
    /^[\s]*(?:import|export|function|class|const|let|var|def|public|private|protected)/m,
    /[\{\}\[\];]/,
    /^\s*\/\//m,
    /^\s*\/\*/m,
    /^\s*#include/m,
    /^\s*package/m,
    /=>\s*\{/,
    /\bif\s*\(|\bfor\s*\(|\bwhile\s*\(/
  ];
  // いずれかのパターンにマッチすればコードと判定
  return codePatterns.some(pattern => pattern.test(text));
}

言語検出ロジック

正規表現を使用して、コードから言語を推測します。
より効率的な言語検出ロジックがあれば、ぜひご教示ください。

detectLanguage(text: string): string {
  const languagePatterns: { [key: string]: RegExp } = {
    'typescript': /(?:import.*from|interface|type\s+\w+\s*=|export\s+(?:default\s+)?(?:class|function|interface))/,
    'javascript': /(?:import.*from|const|let|var|function|=>)/,
    'python': /(?:^|\s)(?:def|class|import|from|print)\s/m,
    'java': /(?:public|private|protected)\s+(?:class|interface|static)/,
    'cpp': /#include\s*[<"]|(?:std::|cout|cin)/,
    'c': /#include\s*[<"]|(?:printf|scanf)/,
    'csharp': /(?:using\s+System|namespace|public\s+class)/,
    'go': /(?:package\s+main|func\s+main|import\s+\()/,
    'rust': /(?:fn\s+main|let\s+mut|use\s+std)/,
    'html': /<(?:html|head|body|div|span|p|a)\b/i,
    'css': /\{[^}]*(?:color|font|margin|padding|display):/,
    'json': /^\s*[\{\[]/,
    'sql': /(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\s/i,
    'bash': /^#!\/bin\/(?:bash|sh)|(?:\$\(|\${)/m,
    'ruby': /(?:^|\s)(?:def|class|module|require|puts)\s/m,
    'php': /<\?php|(?:echo|print)\s/,
  };
  for (const [language, pattern] of Object.entries(languagePatterns)) {
    if (pattern.test(text)) {
      return language;
    }
  }
  return '';
}

Markdown表の検出

表を検出するため、パイプ記号とセパレーター行をチェックします。

detectIfMarkdownTable(text: string): boolean {
  const lines = text.trim().split('\n');
  // 最低2行必要(ヘッダー + セパレーター)
  if (lines.length < 2) {
    return false;
  }
  // パイプで区切られた行があるか
  const hasPipes = lines.some(line => line.includes('|'));
  if (!hasPipes) {
    return false;
  }
  // セパレーター行を探す(例: |---|---|)
  const hasSeparator = lines.some(line => {
    const trimmed = line.trim();
    return /^\|?[\s\-:|]+\|?$/.test(trimmed) && trimmed.includes('-');
  });
  return hasSeparator;
}

ターミナルコードの検出

ターミナル出力を検出するため、複数のパターンをチェックします。

detectIfTerminal(text: string): boolean {
  const lines = text.trim().split('\n');
  // ターミナルの特徴的なパターン
  const terminalPatterns = [
    // プロンプト記号(行頭)
    /^[\$#%>]\s+/m,
    /^[~\/].*[$#%>]\s+/m,
    /^[a-zA-Z]:\\.*>/m,
    /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+.*[$#%>]\s+/m,
    // エラーメッセージ
    /^(?:Error|error|ERROR|fatal|Fatal|FATAL|warning|Warning|WARNING):/m,
    /bash:|zsh:|sh:|command not found/i,
    /permission denied/i,
    /no such file or directory/i,
    // 一般的なターミナルコマンド
    /(?:^|\$\s+)(?:ls|cd|pwd|mkdir|rm|cp|mv|cat|grep|find|chmod|chown|sudo|git|npm|yarn|pip|python|node|docker|kubectl|ssh|scp)\s/m,
    // ファイルパス
    /^\/(?:home|usr|var|etc|opt|bin|sbin|tmp)/m,
    /^~\/[a-zA-Z0-9_\-\/]+/m,
    /^[A-Z]:\\(?:Users|Program Files|Windows)/i,
  ];
  // 短いテキスト(1-2行)の場合は、より厳格な判定
  if (lines.length <= 2) {
    const strictPatterns = [
      /^[\$#%>]\s+/m,
      /^[~\/].*[$#%>]\s+/m,
      /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+.*[$#%>]\s+/m,
      /^(?:Error|error|fatal|warning):/m,
      /command not found/i,
    ];
    return strictPatterns.some(pattern => pattern.test(text));
  }
  return terminalPatterns.some(pattern => pattern.test(text));
}

余計な改行の削除

連続する空行を1つに減らします。

removeExtraLineBreaks(text: string): string {
  // 連続する空行を1つの空行に減らす
  let cleaned = text.replace(/\n\s*\n\s*\n/g, '\n\n');
  // 行間の不要な空白を削除
  const lines = cleaned.split('\n');
  const processedLines = lines.filter((line, index, array) => {
    // 完全に空の行が連続している場合、1つだけ残す
    if (line.trim() === '') {
      if (index > 0 && array[index - 1].trim() === '') {
        return false;
      }
    }
    return true;
  });
  return processedLines.join('\n');
}

表用の改行削除

表の構造を保持しながら、空行のみを削除します。

removeExtraLineBreaksForTable(text: string): string {
  const lines = text.split('\n');
  // 空行のみを削除、表の行は保持
  const processedLines = lines.filter(line => line.trim() !== '');
  return processedLines.join('\n');
}

インストール方法

手動インストール

  1. GitHubリポジトリのリリースからmain.jsmanifest.jsonstyles.cssをダウンロード

image.png

  1. Vaultのプラグインフォルダにcode-paste-cleanerフォルダを作成

image.png

  1. ダウンロードしたファイルを配置

  2. Obsidianを再起動してプラグインを有効化

image.png

コミュニティプラグインから(いつか対応します...)

今しばらくお待ちくださいませ (+_+)

使い方

  1. プラグインを有効化すると、自動的にペーストイベントを監視
  2. VSCodeや他のエディタからコードをコピー
  3. Obsidianのノートに通常通り貼り付け(Ctrl+V / Cmd+V)
  4. コードが自動的にクリーンアップされ、適切なコードブロックで囲まれる

実際の使用例

通常のコード

VSCodeからコピー:

function greet(name: string): string {
  return `Hello, ${name}!`;
}

Obsidianに貼り付けると自動的に:

```typescript
function greet(name: string): string {
  return `Hello, ${name}!`;
}
```

ターミナルコマンド

ターミナルからコピー:

$ npm install express
$ npm run dev

Obsidianに貼り付けると自動的に:

```bash
$ npm install express
$ npm run dev
```

Markdown表

エディタからコピー:

| Name | Age |
|------|-----|
| Alice | 25 |

Obsidianに貼り付けると:

| Name | Age |
|------|-----|
| Alice | 25 |

(コードブロックで囲まれず、表として表示される)

トラブルシューティング

プラグインが動作しない場合

  1. Obsidianを再起動
  2. プラグインを無効化→有効化
  3. 設定を確認(必要な機能が有効になっているか)

コードが検出されない場合

  • 「Minimum lines for code block」の設定を確認
  • デフォルト言語を設定して、すべてのコードブロック化を有効にする

表やMermaidがコードブロックで囲まれてしまう場合

  • 該当する検出機能が有効になっているか設定を確認

まとめ

「Code Paste Cleaner」プラグインを使用することで、以下のメリットが得られます。

  1. VSCodeからのコピペが快適に:余計な改行が入らず、そのままの形で貼り付け可能
  2. 手動操作の削減:```で囲む作業が不要、言語も自動検出
  3. 多様なコンテンツに対応:コード、表、Mermaid、SVG、ターミナル出力など、各コンテンツタイプを自動判別
  4. 柔軟な設定:各機能をON/OFFでき、自分の好みに合わせてカスタマイズ可能
    特に、技術記事を書いたり、コードスニペットを多用するObsidianユーザーにとって、このプラグインは作業効率を大きく向上させるツールとなるはずです。
    GitHubでオープンソースとして公開していますので、バグ報告や機能要望はIssuesでお待ちしています。

リンク

1
3
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
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?