「クリップボード履歴」メニューのマクロ化

提供: MeryWiki
2019年11月5日 (火) 22:54時点におけるSukemaru (トーク | 投稿記録)による版 (第4版(修正・更新版))
ナビゲーションに移動 検索に移動

概要


公式フォーラム[1] にて「クリップボード履歴」メニューの将来的に廃止されることが 予告?検討? されていますが、後継機能としては 「クリップボード履歴」プラグイン の登場により今後も安泰のようですね。

ということで、試験運用中だった自家用マクロを再度リサイクルして「クリップボード履歴スニペット」の統合版ポップアップメニューにしてみました。
一見して 誰得? なマクロですが、「クリップボード履歴」を「スニペット」機能とまとめてツールバーアイコン化できるという素敵なメリットがあります。

「クリップボード履歴」機能は Mery 2.8.1 以降の『延命措置』の新パラメータ[2] を利用したもの。
「スニペット」機能は スニペットプラグイン の設定ファイル snippets.txt を読み書きするというかたちにしています。


  • このマクロは Mery 2.8.1 以降でしか利用できません。
  • あらかじめ「includeライブラリ」の導入が必要です。
snippets.txt 内のタブインデントによる階層構造をある程度ポップアップメニューに反映させました。(2019/11/05)
ref. 「スニペットプラグイン」のページの Snippets.txt の書き方 を参照のこと。
  • 動作確認は Windows XP sp3 (32bit) × Mery ベータ版 2.8.6 (ポータブル) でしかしていません。
なにかしらの支障を来たす不具合が見つかった場合は、このマクロを削除して使用を中止するか、または フォーラム にてご報告ください。


cf. マクロライブラリには「定型文を挿入」機能に特化したマクロも別途あります。



ツールメニューの「クリップボード履歴」と異なる部分

(2019/11/05)

  • 「クリップボードのすべての履歴を削除」 「履歴からアイテムをひとつ削除」 「貼り付けしてからアイテムを削除」 「履歴からアイテムをスニペットに登録」 の機能を追加してあります。
ポップアップメニュー内のアイテムの Ctrl+クリック で「貼り付けしてからアイテムを削除」する機能は、「GetKeyState.exe 」を導入している場合にかぎり利用できます。
  • 「クリップボードのすべての履歴を削除」 したあとでも、Windows OS のクリップボードの最新の1件がテキストデータである場合は、「クリップボード履歴」にアイテムが表示されます。
Mery 本来の「クリップボード履歴」機能でも「履歴を消去」したあとにも Ctrl+V によるペーストができることがあるので、これを可視化してあります。



「スニペットプラグイン」と異なる部分

(2019/11/05)

  • このマクロからスニペットに登録したアイテムは、snippets.txt の末尾に追加されます。
  • snippets.txt 内の「 & 」記号によるアクセラレータをすべて無視します。
→ 有効文字列のある行を上から順に連番化し、番号をアクセラレータにします。
  • snippets.txt 内の「空行・空白行」の扱い方などでスニペットプラグインと異なる解釈をしている部分があります。
このマクロでは、snippets.txt 内の 空行とタブ文字だけの空白行を完全に無視するので、「空文字のサブメニュー見出し」を作りません。
→ ポップアップメニューでの表示状態がスニペットプラグインと同じ階層構造にならないことがあります。
  • snippets.txt 内の空行やタブインデントでのクループ化が適切でない部分は、ポップアップメニューに正しく反映されません (その行をスキップし、ポップアップメニューに表示しません)。
タブインデントの階層を深くするさいは、かならず一段ずつ下げてください(二段以上の差があるとエラーの元)。
階層を浅くするさいは、二段以上の差があっても構いません。


ソースコード

ダウンロード: 「ファイル:クリップボード履歴.zip」(アイコン入り)
  • 2019/11/05: 第4版(修正・更新版)


#title = "クリップボード履歴..."
#tooltip = "クリップボード履歴 と スニペット"
#icon = "clipboard_history[1].ico"
#include "include/IO.js"

/**
 * --------------------------------------------------
 * 「クリップボード履歴 と スニペット」マクロ
 *  sukemaru, 2019/08/01 - 2019/11/05
 * ※ まだまだバグが残っているかもしれません。
 * --------------------------------------------------
 * 「クリップボード履歴」メニューと「スニペット」プラグインと同等の機能を
 *   ひとつのポップアップメニューに統合します。
 * 
 * ※ ソースコードの末尾に【能書き】
 */

var start = new Date();

// ---------- ▼ 設定項目 ▼ ---------- //

// ■ クリップボード履歴内の重複アイテムを削除する
var delDupEnable = true;	// true: 削除する / false: 削除しない

// ■ ポップアップメニューを表示する位置
var menuPosMouse = true;	// true: マウス位置 / false: キャレット位置

// ■ ポップアップメニューに表示する文字数
var menuWidth = 60;

// ■ GetKeyState.exe のフルパスを指定する場合(「\」記号はふたつがさね「\\」で)
// 未指定 "" なら、Mery インストールフォルダの Macros\GetKeyState.exe
var getKeyStatePath = "";	// GetKeyState.exe なしのときも ""

// ---------- ▲ 設定項目 ▲ ---------- //


var Fso = new ActiveXObject( "Scripting.FileSystemObject" );
var WshShell = new ActiveXObject( "WScript.Shell" );

// ClipboardData.Get/ClearData() に引数を指定できるのは ver 2.8.1 以降
if ( ! VersionCheck( "2.8.1" ) ) {
  Status = " 動作要件: \"Mery ver 2.8.1\" 以上";
}

else {
  var meryPath	 = editor.FullName;
  var meryDir	 = meryPath.replace( /[^\\]+$/, "" );
  var isPortable = Fso.FileExists( meryPath.replace( /\.exe$/i, ".ini" ) );
  var profileDir = ( isPortable )
                 ? meryDir
                 : WshShell.SpecialFolders( "APPDATA" ) + "\\Mery\\";
  var snPath	 = profileDir + "Plugins\\Snippets\\Snippets.txt";
  var snIsExist	 = Fso.FileExists( snPath );

  var gksPath	 = getKeyStatePath || meryDir + "Macros\\GetKeyState.exe";
  var gksIsExist = Fso.FileExists( gksPath );
  var $ctrl		 = 0;

  var d	 = editor.ActiveDocument;
  var s	 = d.selection;
  var st = s.Text;
  var $status = Status;

  // ピン止めアイテム (スニペット)の準備
  var snText = "",  snArray = [],  snCount = 0;
  if ( snIsExist ) {
    // snippet.txt を読みこむ
    snText	 = IO.LoadFromFile( snPath, "utf-8" ) || "";
    // 空白行を除去した配列にする
    snArray	 = snText.split( "\n" );
    for ( var i = 0, len = snArray.length; i < len; i ++ ) {
      if ( /^\t*$/.test( snArray[i] ) ) {
        snArray.splice( i --, 1 );
      }
    }
    snCount	 = snArray.length;
    // snText	 = snArray.join( "\n" );
  }

  // クリップボード履歴の準備
  var cb		 = ClipboardData;
  var cbData	 = cb.GetData();
  var cbArray	 = [];
  var cbData0	 = cb.GetData( 0 );
  if ( cbData0 ) {
    for ( var i = 0, cbItem;  ; i ++ ) {
      cbItem = cb.GetData( i ) || "";
      if ( ! cbItem ) { break; }
      // 重複するアイテムを履歴から削除する
      if ( delDupEnable ) {
        for ( var j = i + 1, cbNextItem;  ; j ++ ) {
          cbNextItem = cb.GetData( j ) || "";
          if ( ! cbNextItem ) { break; }
          if ( cbItem == cbNextItem ) {
            cb.ClearData( j -- );
          }
        }
      }
      // 履歴アイテムを配列に収納する
      cbArray.push( cbItem );
    }
  }
  var cbCount	 = cbArray.length;
  var width		 = String( Math.max( snCount, cbCount ) ).length;


  // ポップアップメニューの準備
  var menu = CreatePopupMenu();
  var menuFlags = d.ReadOnly ? meMenuGrayed : 0;

  // 「ピン止めアイテム/スニペット」サブメニュー
  var sm0 = CreatePopupMenu();
  var t = ( snCount ) ? "▼ " : "";
  menu.AddPopup( t + "ピン止めアイテム/スニペット (&S) " + t, sm0 );
  if ( st ) {
    sm0.Add( "選択範囲を登録 (&P)", 300 );
  }
  if ( snIsExist ) {
    sm0.Add( "スニペットを編集 (&E) ...", 400 );
  }
  if ( st || snIsExist ) {
    sm0.Add( "----", 0, meMenuSeparator );
    sm0.Add( "キャンセル & ", 0 );
  }
  if ( snCount ) {
    sm0.Add( "----", 0, meMenuSeparator );
  }

  // スニペットのアイテムをメニューに
  if ( snText ) {
    var label1,  label2,  tab1,  tab2,  label,  lines;
    var subArray = [];	// SubMenu Array
    for ( var i = 0; i < 10; i ++ ) { subArray.push( [] ); }
    var subId,  subId1,  subId2;
    for ( var i = 0, j = 1, tab0 = 0, id; i < snCount; i ++, j ++ ) {
      try {
        label1 = snArray[i];
        label2 = snArray[j] || "";
        tab1 = ( label1.charAt( 0 ) == "\t" ) ? label1.search( /[^\t]/ ) : 0;
        tab2 = ( label2.charAt( 0 ) == "\t" ) ? label2.search( /[^\t]/ ) : 0;
        label = MenuKey( label1.replace( /^\t+|\t+[^\t]*$/g, "" )
                       , j, width, menuWidth, true );
        id = j + 400;
        // メインメニューにサブメニュー項目を追加
        if ( tab1 == 0 && tab1 < tab2 ) {
          subId = subArray[0].length;
          subArray[0].push( CreatePopupMenu() );
          if ( menuFlags ) {
            menu.Add( label + "\t▶", id, menuFlags );
          }
          else {
            menu.AddPopup( label, subArray[0][ subId ] );
          }
          sm0.Add( "----", 0, meMenuSeparator );
        }
        // メインメニューにアイテムを追加
        else if ( tab1 == 0 && tab1 == tab2 ) {
          menu.Add( label, id, menuFlags );
          if ( tab1 < tab0 ) {
            sm0.Add( "----", 0, meMenuSeparator );
          }
          sm0 .Add( label, id, menuFlags );
        }
        // サブメニュー内にサブメニュー項目を追加
        else if ( tab1 > 0 && tab1 < tab2 ) {	// 
          subId1 = subArray[ tab1 ].length;
          subId2 = subArray[ tab1 -1 ].length - 1;
          subArray[ tab1 ].push( CreatePopupMenu() );
          subArray[ tab1 -1 ][ subId2 ].AddPopup( label, subArray[ tab1 ][ subId1 ] );
          sm0.Add( "----", 0, meMenuSeparator );
        }
        // サブメニューにアイテムを追加
        else if ( tab1 > 0 && tab1 >= tab2 ) {
          subId = subArray[ tab1 -1 ].length - 1;
          subArray[ tab1 -1 ][ subId ].Add( label, id, menuFlags );
          if ( tab1 < tab0 ) {
            sm0.Add( "----", 0, meMenuSeparator );
          }
          sm0.Add( label, id, menuFlags );
        }
        tab0 = tab1;
      }
      catch( e ) {
        OutputBar.Clear();
        OutputBar.Writeln( e.name + ":  " + ScriptFullName + "\n  \""
                         + snPath + "\" の\n  " + j
                         + " 件目のアイテム(空行は除外)を処理できませんでした。\n\""
                         + label1.replace( /\t/g, " › " ) + "\"\n" );	// 
        // OutputBar.Visible = true;
        // break;		// ← ループ処理を中断する場合
        continue;		// ← ループ処理を継続する場合
      }
    }
  }
  menu.Add( "----", 0, meMenuSeparator );

  // クリップボード履歴のアイテムをメニューに
  if ( cbCount ) {
    var sm1  = CreatePopupMenu();
    var sm2  = CreatePopupMenu();
    var sm3  = CreatePopupMenu();
    var sm4  = CreatePopupMenu();
    menu.AddPopup( "▼ クリップボード履歴の一覧 (&C) ▼", sm1 );
    for ( var i = 0, id, label, lines; i < cbCount; i ++ ) {
      id = i + 1;
      label = MenuKey( cbArray[i], id, width, menuWidth );
      menu.Add( label, id, menuFlags );
      sm2. Add( label, id + 100, menuFlags );
      sm3. Add( label, id + 200 );
      sm4. Add( label, id + 300 );
    }
    menu.Add( "----", 0, meMenuSeparator );
    menu.Add( "クリップボードのすべての履歴を削除する (&E)", 99 );
    sm1.AddPopup( "履歴からアイテムをスニペットに登録 (&P)", sm4 );
    sm1.AddPopup( "貼り付けしてからアイテムを削除 (&M)", sm2 );
    sm1.AddPopup( "履歴からアイテムを削除 (&D)", sm3 );
    sm1.Add( "----", 0, meMenuSeparator );
    sm1.Add( "すべての履歴を削除 (&E)", 99 );
    sm1.Add( "----", 0, meMenuSeparator );
    sm1.Add( "キャンセル & ", 0 );
  }
  else if ( cbData.length ) {
    menu.Add( "▼ クリップボード ▼", 0, meMenuGrayed );
    label = MenuKey( cbData, 1, width, menuWidth );
    menu.Add( label, 17, menuFlags );
  }
  else {
    menu.Add( "※ クリップボードにテキストデータはありません ※"
            , 0, meMenuGrayed );
  }

  menu.Add( "----", 0, meMenuSeparator );
  menu.Add( "キャンセル & ", 0 );


  // ステータスバーの表示
  if ( menuFlags ) {
    Status = " ドキュメントは書き換え禁止です。";
  }
  else if ( gksIsExist && cbCount ) {
    Status = " 履歴アイテムの Ctrl+クリック で 「貼り付けしてからアイテムを削除」";
  }
  else {
    Status = " 「クリップボード履歴 と スニペット」マクロ";
  }
  Status += " [ "
         + ( ( new Date() - start ) / 1000 ).toFixed( 3 ).replace( /\./, ". " )
         + " 秒 ]";


  // ポップアップメニューを表示
  var r = menu.Track( + menuPosMouse );
  var confirmStr = "クリップボードのすべての履歴を削除しますか? ";

  if ( r == 0 ) {
    Status = $status;
  }

  // 1 ~ 17: クリップボード履歴のアイテムを貼り付け
  else if ( r <= 16 ) {
    Status = "";
    d.Write( cbArray[ r -1 ] );
    // Ctrl キーを押しながらのときは、貼り付けしてからアイテムを削除
    if ( gksIsExist ) {
      $ctrl = WshShell.Run( "\"" + gksPath + "\" c", 0, true );
      if ( $ctrl == 1 ) {
        cb.ClearData( r -1 );
        Status = " クリップボード履歴からアイテムを削除しました。";
      }
    }
  }
  else if ( r == 17 ) {
    d.Write( cbData );
    Status = "";
  }

  // 99: クリップボード履歴をすべて削除する
  else if ( r == 99 && Confirm( confirmStr ) ) {
    for ( var i = 0; i < cbCount; i ++ ) {
      cb.ClearData( i );
      Status = " クリップボード履歴からすべてのアイテムを削除しました。";
    }
  }

  // 101 ~ 116: 貼り付けしてからアイテムを削除
  else if ( r > 100 && r < 200 ) {
    d.Write( cbArray[ r -101 ] );
    cb.ClearData( r -101 );
    Status = " 履歴からアイテムを削除しました。";
  }

  // 201 ~ 216: クリップボード履歴からアイテムを削除
  else if ( r > 200 && r < 300 ) {
    cb.ClearData( r -201 );
    Status = " 履歴からアイテムを削除しました。";
  }

  // 300 ~ 316: スニペットに登録(ピン止め)
  else if ( r >= 300 && r < 400 ) {
    var snippetsDir = Fso.GetParentFolderName( snPath );
    var pluginsDir = Fso.GetParentFolderName( snippetsDir );
    if ( ! Fso.FolderExists( pluginsDir ) ) {
      Fso.CreateFolder( pluginsDir );
    }
    if ( ! Fso.FolderExists( snippetsDir ) ) {
      Fso.CreateFolder( snippetsDir );
    }
    // 選択範囲 または クリップボード履歴のアイテム
    var str = ( r == 300 )
            ? st
            : ( cb.GetData( r -301 ) || cb.GetData() );
    str = snText
        + ( snText ? "\n" : "" )
        + str.replace( /\\/g, "\\\\" )
             .replace( /\t/g, "\\t" )
             .replace( /\n/g, "\\n" )
             .replace( /\r/g, "\\r" )
    IO.SaveToFile( snPath, str, "utf-8", true );
    var copyFrom = ( r == 300 ) ? "範囲" : "したアイテム";
    Status = " 選択" + copyFrom + "をスニペットに登録しました。";
  }

  // 400: スニペットを編集(snippets.txt を開く)
  else if ( r == 400 ) {
    WshShell.Run( "\"" + editor.FullName + "\" \"" + snPath + "\"" );
    Status = " " + snPath;
  }

  // 401 ~ : ピン止めアイテムを貼り付け
  else if ( r > 400 ) {
    var strArr = snArray[ r -401 ].replace( /^(?:\t)*[^\t]*\t/, "" )
                                   .split( "\\\\" );
    for ( var i = 0; i < strArr.length; i ++ ) {
      strArr[i] = strArr[i].replace( /\\t/g, "\t" )
                           .replace( /\\r/g, "\r" )
                           .replace( /\\n/g, "\n" )
    }
    var str = strArr.join( "\\" );
    d.Write( str );
    Status = "";
  }

}


// ---------- ▼ 関数 ▼ ---------- //

/**
 * 関数 VersionCheck( versionStr )
 * Mery 本体が引数で指定したバージョン以上かチェックする( e.g. "2.6.9" )
 * 戻り値は、真偽値 true/false
 */
function VersionCheck( versionStr ) {
  var Pad2 = function( str ) {
    return str.replace( /[0-9]+/g , function( digit ) {
      return digit.length < 2 ? "0" + digit : digit
    } )
  };
  var editorVer = + ( Pad2( editor.Version ).replace( /\./g, "" )
                                            .slice( 0, 6 ) );
  var requirement = + ( Pad2( versionStr ).replace( /\./g, "" )
                                          .slice( 0, 6 ) );
  return ( editorVer >= requirement );
}

/**
 * 関数 MenuKey( str, num, numWidth, menuWidth, boolean )
 * ポップアップメニューに表示するラベルを生成する
 * ・行頭空白を除去、空白文字を圧縮:		→「›」(U+203A)
 * ・改行記号を可視化:						→「↲」(U+21B2) または「⏎」(U+23CE)
 * ・削られてしまう「&」を補完
 * ・「¥」(U+005C) を「∖」に置換:			→「∖」(U+2216)
 * ・ゼロ幅や特殊な空白文字を豆腐に置換:	→「⊠」(U+22A0) または「▯」(U+25AF)
 * ・判別しづらいメタ文字を全角に置換:		!"%'(),.:;@[]`{|}
 * ・「a-z」を全角に置換
 * ・行番号を空白でケタ埋め:				EN SPACE「 」(U+2002)
 * ・文字数を切り詰め
 */
function MenuKey( str, num, numWidth, menuWidth, conv ) {
  if ( conv ) {
    var strArr = str.replace( /^[^\t]*\t/, "" ).split( "\\\\" );
    for ( var i = 0; i < strArr.length; i ++ ) {
      strArr[i] = strArr[i].replace( /\\t/g, " › " )
                           .replace( /\\r|(\\r)?\\n/g, " ↲ " );
    }
    str = strArr.join( "\\" );
  }
  var reg = /[\u00A0\u1680\u180e\u2000-\u200A\u2028\u2029\u202F\u205F\uFEFF]/g;
  var menuKey = str.replace( /(?:\t|[ ]{3,}|[ ]{2,})+/g, " › " )
                   .slice( 0, menuWidth + 1 )
                   .replace( /(?:\r?\n|\r)/g, " ↲ " )
                   .replace( /[&]/g, "&&" )
                   .replace( /[\\]/g, "∖" )
                   .replace( reg, "▯" )
                   .replace( /[!"%'(),.:;@\[\]`a-z{|}]/g,
                     function( tmp ) {
                       return String.fromCharCode( tmp.charCodeAt( 0 ) + 0xFEE0 )
                     } );
  num = num ? ( "   " + num ).slice( - numWidth ).replace( /\d$/, "&$&: " ) : "";
  menuKey = ( menuKey.length > menuWidth )
          ? menuKey.slice( 0, menuWidth ) + " ..."
          : menuKey;
  return num + menuKey;
}


// ---------- ▼ 能書き ▼ ---------- //

/**
 * 【このマクロの仕様・制限事項】
 * ・ver 2.8.1 以前の Mery では使用できません。
 * 
 * ・「クリップボード履歴」は Mery 本体のメモリストアのものを使用します。
 * ※「クリップボードのすべての履歴を削除」したあとでも、
 *    Windows OS のクリップボード(最新1件)にテキストデータが残っているときは
 *    クリップボード履歴メニューに1件だけ表示します。
 * 
 * ・「スニペット」プラグインの設定ファイル snippets.txt の読み書きをします。
 * ・
 * ※「include ライブラリ」が必要です(snippets.txt ファイルの読み書きに使用)。
 * ※「スニペット」プラグインを導入していない場合でも、
 *   「ピン止めアイテム/スニペット」機能を使用できます。
 *     snippets.txt ファイルの保存場所は Mery\Plugins\Snippets フォルダです
 *   (インストーラ版では %AppData%\Mery\Plugins\Snippets フォルダ)。
 * 
 * ・「GetKeyState.exe(キー状態取得実行ファイル) 」で機能を拡張できます。
 *      → クリップボード履歴のアイテムを Ctrl+クリックした場合、
 *         アイテムを貼り付けし、貼り終えたアイテムを削除します。
 * 
 * ※「GetKeyState.exe」は Mery\Macros フォルダに配置してください
 *   (Macros フォルダ以外の場所にあるなら設定項目でパスを指定する)。
 * ※「GetKeyState.exe」がなくても、クリップボード履歴の
 *    サブメニュー項目から「貼り付けしてからアイテムを削除」できます。
 * 
 * 【スニペットプラグインと異なる部分】 (2019/11/05)
 * ・ このマクロからスニペットに登録したアイテムは
 *   snippets.txt の末尾に追加されます。
 * ・ snippets.txt 内の「&」記号によるアクセラレータはすべて無視します
 *  (有効文字列の行の順に連番化して)。
 * ・ snippets.txt 内の 空行・空白行 の扱い方などで
 *   スニペットプラグインと異なる解釈をしている部分があり、
 *   snippets.txt 内の空行やタブインデントでのクループ化が適切でないと
 *   ポップアップメニューでの表示状態が
 *   スニペットプラグインと同じ階層構造になりません。
 * ※ このマクロでは、snippets.txt 内の 空行とタブ文字だけの空白行を
 *    完全に無視するので、「空文字のサブメニュー見出し」を作りません。
 * ※ タブインデントの階層を深くするさいは、
 *    かならず一段ずつ下げてください(二段以上の差があるとエラーの元)。
 *    階層を浅くするさいは、二段以上の差があっても構いません。
 * ※ タブインデントによる階層構造に不適切な部分があったときは、
 *    その行をスキップし、ポップアップメニューに表示しません。
 */


更新履歴

• 2019/08/01: 初版
• 2019/08/06:
 ・「ピン止めアイテムを貼り付け」コードの変数ミスを修正
 ・クリップボード履歴内の重複アイテムを削除する設定を追加。
 ・ピン止めアイテムが少ないときはメインメニューにアイテムを表示。
 ・snippets.txt 内の空行を無視。
 ・クリップボード履歴に長大なデータがあるときの動作速度を少しだけ改善。
• 2019/11/01:
 ・ステータス表示を追加
 ・クリップボードにテキストデータがないときのメニュー構成を変更
 ・アイテムの行数(改行数+1)をメニュー内に追加表示
 ・メニュー内に表示する改行コードの矢印を「↲」(U+21B2) に変更
 ・スニペット機能での「ピン止め/貼り付け」のさいの、改行コードやタブコードではない文字列「\n」や「\t」の扱いを修正
 ・snippets.txt がないときのエラーを修正
• 2019/11/05
 ・各アイテムの行数表示を廃止
 ・クリップボード履歴からスニペットにアイテムを登録するコマンドを追加
 ・メインメニューに表示するピン止めアイテム数の上限設定を廃止
 ・スニペット機能での「ピン止め/貼り付け」のさいの、改行コードやタブコードではない文字列「\n」や「\t」の扱いを再修正
  ( … したつもりだが、まだ不完全かも)
 ・スニペットプラグインのサブメニュー化階層構造をある程度再現した
  ( … つもりだが、アレンジしたので同じ表示状態にならないことがある)
スポンサーリンク