Hatena::ブログ(Diary)

chalcedonyの外部記憶装置・出張版 RSSフィード

2013-07-03

選択した文字列を索引項目に登録するJavaScript(読み仮名自動入力、CS3〜)

はいこんばんは。

InDesignで索引項目を追加するときはソートのために読み仮名を入力しなくてはならないのですが、手打ちするのがあまりにもめんどくさいので自動的に取得するスクリプトを作りました。

読み仮名の取得にはみんな大好きYahoo!のテキスト解析WebAPIを利用しています。

これを

f:id:chalcedony_htn:20130703213940p:image

こうします

f:id:chalcedony_htn:20130703213941p:image


あくまで自分用に作ったものなので、

  • 参照形式はデフォルトの「現在のページ」のみ
  • 項目のレベルは設定できない(すべてレベル1になる)

という仕様になっています。

あと、APIアプリケーションIDは消してありますので、もし使用する場合は自分のアプリケーションID(ランダムな英数字になってます)を取得して、「◆◆◆ココにアプリケーションIDを書く◆◆◆」のところに入れてください(1箇所だけです)。YahooのIDを作れば誰でも取得できます。

ちょっと試してみたいという人にはだいぶ不親切ですがご了承ください><


このスクリプトを書くにあたって、2つの記事を大いに参考にさせていただいています。


動作確認は、WinXP + CS4、WinXP + CS5.5、Win7 + CS5.5、MacOSX 10.7.5 + CS3、MacOSX 10.5.8 + CS3、MacOSX 10.5.8 + CS4で行いました。Macでの動作確認にご協力いただいたお二方、ありがとうございました! 遅れてすいません!


■使用前の確認事項


■使用中の注意事項

  • APIが期待通りの読み仮名を返してくるとは限らないので、必ず1件ずつ確認しながら登録してください。
  • 登録直後は索引項目のページ数が表示されません。索引パネルのメニューから[プレビューを更新]を実行すると直ります。
  • 索引登録時、ドキュメントの最初のページ付近にダミーのテキストフレームが作られます。通常は自動的に削除されますが、スクリプトが途中で止まった場合などに残る可能性があります。ひととおり登録したら、フレームが残っていないか確認してください。あったら消してね。
  • なにが起きても泣かないようにデータのバックアップを取りながら使用してください(念押し)。

// 索引登録支援ツール(InDesign CS3〜)
// Yahoo!のテキスト解析APIを利用して索引の読み仮名入力を自動化します。
// http://developer.yahoo.co.jp/webapi/jlp/
//
// 以下の記事をパク^H^H大いに参考にさせていただいています。
// kmutoさん
// via http://d.kmuto.jp/20120912.html
// CLさん
// via http://d.hatena.ne.jp/C_L/20081012/indesign_socket_http
//
// v0.9 2013/04/03
// v1.0 2013/06/03  デフォルトスタイルの初期化処理追加、自分で使用開始
// v1.1 2013/06/17  初期化処理をやめてダミー文字のサイズだけ指定する形に変更
// 
// NYSL http://www.kmonos.net/nysl/
// ==============================================================================


main();


// メインの処理
function main(){

  //Yahoo!APIのアプリケーションID
  var myAppID = "◆◆◆ココにアプリケーションIDを書く◆◆◆";

  // 選択状態チェック(テキストオブジェクトを選択してる状態のみ動作)
  if(app.selection.length == 0 || !app.selection[0].constructor.name.match(/^(Text|Word|Character|Paragraph|Line|TextColumn|TextStyleRange)$/)){
    alert("索引登録可能なテキストを選択してください。");
    return false;
  }

  var actDoc = app.activeDocument;
  var idx = (actDoc.indexes.length > 0) ? actDoc.indexes[0] : actDoc.indexes.add();

  var targ = app.selection[0]; // 選択中のテキスト
  var title = targ.contents;   // 索引項目になる文言

  // APIリクエスト
  var yapiObj = new YAPIReading();
  yapiObj.appid = myAppID;
  var reading = yapiObj.getReading(title); // 読みがなになる文言

  // 表示ダイアログ準備
  // ダイアログで読みがなを修正可能
  var dlg = createDialog();
  dlg.tf.text = title;   // 索引項目欄に入力
  dlg.rf.text = reading; // 読みがな欄に入力
  // 読みがなが空文字(APIがエラー返してきてる)だったらメッセージを上書きする
  if(reading == ""){ dlg.info.text = "読みがなの自動取得に失敗しました。直接入力してください。"; }

  // ダイアログ表示から登録実行
  // 登録ボタンを押すと、その時点の読みがな欄のテキストを読みがなとして登録
  if(dlg.show() == 1){
    title = dlg.tf.text;
    reading = dlg.rf.text;

    try{
      embedIndex(idx, targ, title, reading); // 登録実行
    } catch(e) {
      alert("登録に失敗しました。\n索引項目または読みがなに使えない文字がないか確認してください。");
      arguments.callee();
    }

    return true;
  }

  return false; // 登録しなかったらfalse返すことにしておく(なんとなく)
}





// ***************************************************************************
//
// 以下、関数・オブジェクト定義など
//
// ***************************************************************************


// 索引を追加する関数 ********************************************************
// kmutoさんのアイディア(マーカーのコピペ)を拝借
// via http://d.kmuto.jp/20120912.html
// ***************************************************************************
function embedIndex(index, target, title, reading) {

  // ダミーのテキストフレームを作って★マークとか入れておく
  var dummyFrame = index.parent.pages[0].textFrames.add();
  dummyFrame.geometricBounds = [0, 0, 50, 50]; // サイズは適当
  dummyFrame.insertionPoints[0].pointSize = 1; // 文字あふれ対策にサイズを小さくしておく
  dummyFrame.contents = "★";

  // 項目追加
  // ダミーの★マークのところに索引マーカーを入れる
  index.topics.add(title, reading).pageReferences.add(dummyFrame.characters[0]);

  // マーカー文字をカット&ペーストして正しい位置に移動
  dummyFrame.characters[0].select();
  app.cut();
  target.insertionPoints[0].select();
  app.pasteWithoutFormatting(); // フォーマットなしでペースト

  // ダミーのフレームを始末
  dummyFrame.remove();
}



// ダイアログオブジェクトを作って返す関数 ************************************
// あとで部品にアクセスしやすいようにショートカット作ってある
// dlg.tf   : 項目入力欄
// dlg.rf   : 読みがな入力欄
// dlg.info : 情報欄
// ***************************************************************************
function createDialog(){
  var dlg = new Window("dialog", "索引登録");
  dlg.orientation = "row";
  dlg.alignChildren = "top";

  var inputG = dlg.add("group");
  inputG.orientation = "column";
  inputG.alignChildren = "left";
  var infoLabel = inputG.add("statictext", undefined, "読みがなを修正してください。キャンセルすると登録を中止します。");

  var inputTitle = inputG.add("group");
  inputTitle.orientation = "row";
  var titleLabel = inputTitle.add("statictext", undefined, "索引項目:");
  var titleField = inputTitle.add ("statictext", undefined, undefined);
  titleLabel.characters = 9;
  titleField.characters = 35;

  var inputReading = inputG.add("group");
  inputReading.orientation = "row";
  var readingLabel = inputReading.add("statictext", undefined, "読みがな:");
  var readingField = inputReading.add ("edittext", undefined, undefined);
  readingLabel.characters = 9;
  readingField.characters = 35;

  var buttonG = dlg.add("group");
  buttonG.orientation = "column";
  buttonG.add("button", undefined, "登録", {name: "ok"});
  buttonG.add("button", undefined, "キャンセル", {name: "cancel"});

  // ショートカット定義
  dlg.tf = titleField;
  dlg.rf = readingField;
  dlg.info = infoLabel;

  return dlg;
}



// 読みがな取得用クラス定義 **************************************************
// アプリケーションIDはインスタンス側で設定する
//
// var hoge = new YAPIReading();      // インスタンス作成
// hoge.appid = "★★アプリケーションID★★"; // 自分のアプリケーションIDを指定する
// var title = "僕の妹は漢字が読める";     // 読みたい文言
// var reading = hoge.getReading(title);     // 解析結果を取得
//
// ってする
// 記号類はfilterで除去。これも変更可能
// 読みがな部分の抽出はテキストそのまま正規表現でぶっこぬき。XML解析なにそれおいしいの
// ***************************************************************************
function YAPIReading(){
  this.appid    = "";
  this.filter   = "1|2|3|4|5|6|7|8|9|10|11|12";
  this.response = "reading";
  this.results  = "ma";

  this.rex = /<reading>(.*?)<\/reading>/g; // <reading>要素を見つける正規表現(gオプション付き)

  this.getReading = function(sentence){
    var reading = "";

    var requestURI = 'http://jlp.yahooapis.jp/MAService/V1/parse?'
                   + 'appid=' + this.appid
                   + '&ma_filter=' + this.filter
                   + '&response='  + this.response
                   + '&results='   + this.results
                   + '&sentence='  + encodeURI(sentence);

    var lwp = new Lwp();
    var result = lwp.get(requestURI);

    // gオプション付きRegExpオブジェクトのexecループ
    // <reading>要素を見つけるたびに中身のテキストを足していく
    var m;
    while (m = this.rex.exec(result)) {
      reading += m[1];
    }
    return reading; // リクエストに失敗した場合はreadingタグがないので空文字になってるはず
  }
  return this;
}


// HTTPアクセス(GET)用クラス定義 *******************************************
// CLさんのモジュールを微改造
// via http://d.hatena.ne.jp/C_L/20081012/indesign_socket_http
// prototypeをやめてUser-Agentをそれっぽくしただけ
// ***************************************************************************
function Lwp() {
  this.userAgent = "InDesign/" + app.version + " (InDesign " + app.version + "; " + $.os + "; ja)";
  this.uri = function(uri) {
    var rex = new RegExp('http://([^:/]+)(?::(\d+))?(.+)');
    // via http://pc11.2ch.net/test/read.cgi/php/1015692614/57
    var urlObj =[];
    if ( uri.match(rex) ) {
      urlObj.host = RegExp.$1;
      urlObj.port = RegExp.$2 ?RegExp.$2 :80;
      urlObj.path = RegExp.$3;
    }
    return urlObj;
  }
  this.get = function (uri) {
    var conn = new Socket;
    var urlObj = this.uri(uri);
    if ( conn.open(urlObj.host + ':' + urlObj.port, 'UTF-8') ) {
      conn.write ("GET " + urlObj.path + " HTTP/1.0\n" + "Host: " + urlObj.host + "\n" + "User-Agent: " + this.userAgent + "\n\n");
        var reply = conn.read(999999);
        conn.close();
      return reply.substring(reply.indexOf("\n\n") + 2);
    }
  }
  return this;
};