root/lang/javascript/vimperator-plugins/branches/1.2/migemized_find.js

Revision 22751, 13.1 kB (checked in by retlet, 22 months ago)

手元の環境で動いているものをtags/2.0pre_1016からコピー

Line 
1// ==VimperatorPlugin==
2// @name           Migemized Find
3// @description-ja デフォルトのドキュメント内検索をミゲマイズする。
4// @license        Creative Commons 2.1 (Attribution + Share Alike)
5// @version        2.5
6// ==/VimperatorPlugin==
7//
8// Usage:
9//    検索ワードの一文字目が
10//      '/'  => 正規表現検索
11//      '?'  => Migemo検索
12//      以外 => Migemo検索
13//
14//    :ml <検索ワード> [-c <色>]
15//    :migelight <検索ワード> [-c <色>]
16//      検索ワードを指定色で強調表示する。
17//
18//    :ml! <色1> <色2> ... <色N>
19//    :migelight!  <色1> <色2> ... <色N>
20//      指定の色の強調表示を消す
21//
22//    :ml! all
23//    :migelight! all
24//      全ての強調表示を消す。
25//
26//    let g:migemized_find_language = "cat";
27//      ミ言語設定
28//
29// Author:
30//    anekos
31//
32// Link:
33//    http://d.hatena.ne.jp/nokturnalmortum/20080805#1217941126
34
35(function () { try {
36
37  let XMigemoCore = Components.classes['@piro.sakura.ne.jp/xmigemo/factory;1']
38                     .getService(Components.interfaces.pIXMigemoFactory)
39                     .getService(liberator.globalVariables.migemized_find_language || 'ja');
40
41  function getPosition (elem) {
42    if (!elem)
43      return {x: 0, y: 0};
44    let parent = getPosition(elem.offsetParent);
45    return { x: (elem.offsetLeft || 0) + parent.x,
46             y: (elem.offsetTop  || 0) + parent.y  }
47  }
48
49  function slashArray (ary, center) {
50    let head = [], tail = [];
51    let current = head;
52    for (let i = 0; i < ary.length; i++) {
53      let it = ary[i];
54      if (it == center)
55        current = tail;
56      else
57        current.push(it);
58    }
59    return [head, tail];
60  }
61
62  let MF = {
63    // 定数
64    MODE_NORMAL: 0,
65    MODE_REGEXP: 1,
66    MODE_MIGEMO: 2,
67
68    // 全体で共有する変数
69    lastSearchText: null,
70    lastSearchExpr: null,
71    lastDirection: null,
72    lastColor: null,
73    currentSearchText: null,
74    currentSearchExpr: null,
75    currentColor: null,
76
77    // submit の為に使う
78    firstResult: null,
79
80    // --color-- の部分は置換される。
81    style: 'background-color: --color--; color: black; border: dotted 3px blue;',
82    findColor: 'lightblue',
83    highlightColor: 'orange',
84
85    // 手抜き用プロパティ
86    get document function () content.document,
87
88    // タブ毎に状態を保存するために、変数を用意
89    // 初回アクセス時に初期化を行う
90    get storage function () (
91      gBrowser.mCurrentTab.__migemized_find_storage ||
92      (gBrowser.mCurrentTab.__migemized_find_storage = {
93        highlightRemovers: {},
94      })
95    ),
96
97    // 現在のタブのフレームリスト
98    get currentFrames function () {
99      let result = [];
100      (function (frame) {
101        // ボディがない物は検索対象外なので外す
102        if (frame.document.body.localName.toLowerCase() == 'body')
103          result.push(frame);
104        for (let i = 0; i < frame.frames.length; i++)
105          arguments.callee(frame.frames[i]);
106      })(content);
107      return result;
108    },
109
110    // ボディを範囲とした Range を作る
111    makeBodyRange: function (frame) {
112      let range = frame.document.createRange();
113      range.selectNodeContents(frame.document.body);
114      return range;
115    },
116
117    // this.style に色を適用した物を返す
118    coloredStyle: function (color) {
119      return this.style.replace(/--color--/, color);
120    },
121
122    // 検索文字列から検索モードと検索文字列を得る。
123    searchTextToRegExpString: function (str) {
124      let [head, tail] = [str[0], str.slice(1)];
125      switch (head) {
126        case '/':
127          return tail;
128        case '?':
129          return XMigemoCore.getRegExp(tail);
130      }
131      return XMigemoCore.getRegExp(str);
132    },
133
134    // 指定色のハイライト削除
135    removeHighlight: function (color) {
136      (this.storage.highlightRemovers[color] || function () void(0))();
137      delete this.storage.highlightRemovers[color];
138    },
139
140    focusLink: function (range) {
141      let node = range.commonAncestorContainer;
142      while (node && node.parentNode) {
143        if (node.localName.toString().toLowerCase() == 'a')
144          return void(Components.lookupMethod(node, 'focus').call(node));
145        node = node.parentNode;
146      }
147    },
148
149    highlight: function (target, color, doScroll, setRemover) {
150      let span = this.document.createElement('span');
151
152      span.setAttribute('style', this.coloredStyle(color));
153      target.range.surroundContents(span);
154     
155      if (doScroll) {
156        let scroll = function () {
157          let pos = getPosition(span);
158          target.frame.scroll(pos.x - (target.frame.innerWidth / 2),
159                              pos.y - (target.frame.innerHeight / 2));
160          let sel = target.frame.getSelection();
161          let r = target.range.cloneRange();
162          r.collapse(true);
163          sel.removeAllRanges();
164          sel.addRange(r);
165        };
166        setTimeout(scroll, 0);
167      }
168
169      let remover = function () {
170        let range = this.document.createRange();
171        range.selectNodeContents(span);
172        let content = range.extractContents();
173        range.setStartBefore(span);
174        range.insertNode(content);
175        range.selectNode(span);
176        range.deleteContents();
177      };
178
179      if (setRemover)
180        this.storage.highlightRemovers[color] = remover;
181
182      return remover;
183    },
184
185    find: function (str, backwards, range, start, end) {
186      if (!range)
187        range = this.makeBodyRange(this.currentFrames[0]);
188
189      if (!start) {
190        start = range.startContainer.ownerDocument.createRange();
191        start.setStartBefore(range.startContainer);
192      }
193      if (!end) {
194        end = range.endContainer.ownerDocument.createRange();
195        end.setEndAfter(range.endContainer);
196      }
197
198      // 検索方向に合わせて、開始終了位置を交換
199      if (backwards)
200        [start, end] = [end, start];
201
202      try {
203        return XMigemoCore.regExpFind(str, 'i', range, start, end, backwards);
204      } catch (e) {
205        return false;
206      }
207    },
208
209    findFirst: function (str, backwards, color) {
210      if (!color)
211        color = this.findColor;
212
213      this.lastDirection = backwards;
214      let expr = this.searchTextToRegExpString(str);
215      this.currentSearchText = str;
216      this.currentSearchExpr = expr;
217      this.currentColor = color;
218
219      let result, frames = this.currentFrames;
220      if (backwards)
221        frames = frames.reverse();
222
223      for each (let frame in frames) {
224        let ret = this.find(expr, backwards, this.makeBodyRange(frame));
225        if (ret) {
226          result = this.storage.lastResult = {
227            frame: frame,
228            range: ret,
229          };
230          break;
231        }
232      }
233
234      this.removeHighlight(color);
235
236      if (result)
237        this.highlight(result, color, true, true);
238
239      this.firstResult = result;
240
241      return result;
242    },
243
244    findSubmit: function (str, backwards, color) {
245      this.findFirst(str, backwards, color);
246      return this.submit();
247    },
248
249    findAgain: function (reverse) {
250      let backwards = !!(!this.lastDirection ^ !reverse);
251      let last = this.storage.lastResult;
252      let frames = this.currentFrames;
253
254      // 前回の結果がない場合、(初め|最後)のフレームを対象にする
255      // findFirst と"似た"挙動になる
256      if (last) {
257        if (backwards) {
258          end = last.range.cloneRange();
259          end.setEnd(last.range.startContainer, last.range.startOffset);
260        } else {
261          start = last.range.cloneRange();
262          start.setStart(last.range.endContainer, last.range.endOffset);
263        }
264      } else {
265        let idx = backwards ? frames.length - 1
266                            : 0;
267        last = {frame: frames[0], range: this.makeBodyRange(frames[0])};
268      }
269
270      this.removeHighlight(this.lastColor);
271
272      let str = this.lastSearchExpr;
273      let start, end;
274
275      let result;
276      let ret = this.find(str, backwards, this.makeBodyRange(last.frame), start, end);
277
278      if (ret) {
279        result = {frame: last.frame, range: ret};
280      } else {
281        // 見つからなかったので、ほかのフレームから検索
282        let [head, tail] = slashArray(frames, last.frame);
283        let next = backwards ? head.reverse().concat(tail.reverse())
284                             : tail.concat(head);
285        for each (let frame in next) {
286          let r = this.find(str, backwards, this.makeBodyRange(frame));
287          if (r) {
288            result = {frame: frame, range: r};
289            break;
290          }
291        }
292      }
293
294      this.storage.lastResult = result;
295
296      if (result) {
297        this.highlight(result, this.lastColor, true, true);
298        this.focusLink(result);
299      }
300
301      return result;
302    },
303
304    submit: function () {
305      this.lastSearchText = this.currentSearchText;
306      this.lastSearchExpr = this.currentSearchExpr;
307      this.lastColor = this.currentColor;
308      if (this.firstResult)
309        this.focusLink(this.firstResult.range);
310      return this.firstResult;
311    },
312
313    cancel: function () {
314    },
315
316    highlightAll: function (str, color) {
317      let expr = this.searchTextToRegExpString(str);
318      this.lastSearchText = str;
319      this.lastSearchExpr = expr;
320
321      if (!color)
322        color = this.highlightColor;
323
324      this.removeHighlight(color);
325
326      let frames = this.currentFrames;
327      let removers = [];
328
329      for each (let frame in frames) {
330        let frameRange = this.makeBodyRange(frame);
331        let ret, start = frameRange;
332        while (ret = this.find(expr, false, frameRange, start)) {
333          removers.push(this.highlight({frame: frame, range: ret}, color, false, false));
334          start = ret.cloneRange();
335          start.setStart(ret.endContainer, ret.endOffset);
336        }
337      }
338
339      this.storage.highlightRemovers[color] = function () { removers.forEach(function (it) it.call()); };
340
341      return removers;
342    },
343  };
344
345
346  // 前のタイマーを削除するために保存しておく
347  let delayCallTimer = null;
348  let delayedFunc = null;
349
350  // Vimp の仕様変更に対応
351  let _backwards;
352  let _findFirst = function (str, backwards) {
353      // 短時間に何回も検索をしないように遅延させる
354      delayedFunc = function () MF.findFirst(str, backwards);
355      if (delayCallTimer) {
356        delayCallTimer = null;
357        clearTimeout(delayCallTimer);
358      }
359      delayCallTimer = setTimeout(function () delayedFunc(), 500);
360  };
361
362  // ミゲモ化セット
363  let migemized = {
364    find: function find (str, backwards) {
365      _backwards = backwards;
366      if (str)
367        _findFirst(str, backwards);
368    },
369
370    findAgain: function findAgain (reverse) {
371      if (!MF.findAgain(reverse))
372        liberator.echoerr('not found: ' + MF.lastSearchText);
373    },
374
375    searchSubmitted: function searchSubmitted (command, forcedBackward) {
376      if (delayCallTimer) {
377        delayCallTimer = null;
378        clearTimeout(delayCallTimer);
379        delayedFunc();
380      }
381      if (!MF.submit())
382        liberator.echoerr('not found: ' + MF.currentSearchText);
383    },
384
385    searchCanceled: function searchCanceled () {
386      MF.cancel();
387    },
388
389    searchKeyPressed: function (str) {
390      _findFirst(str, _backwards);
391    },
392  };
393
394
395  // オリジナルの状態に戻せるように保存しておく
396  let (original = {}) {
397    for (let name in migemized)
398      original[name] = search[name];
399
400    function set (funcs) {
401      for (let name in funcs)
402        search[name] = funcs[name];
403    }
404
405    set(migemized);
406
407    MF.install = function () set(migemized);
408    MF.uninstall = function () set(original);
409  }
410
411
412  // highlight コマンド
413  commands.addUserCommand(
414    ['ml', 'migelight'],
415    'Migelight matched words',
416    function (opts, bang) {
417      if (bang) {
418        let colors = opts.arguments.join(' ') + ' ' + (opts['-color'] || '');
419        liberator.execute('removemigelight ' + colors);
420      } else {
421        let r = MF.highlightAll(opts.arguments.join(' '), opts['-color']);
422        liberator.echo(r ? r.length + ' words migelighted.'
423               : 'word not found.');
424      }
425    },
426    {
427      bang: true,
428      options: [
429        [['-color', '-c'], commands.OPTION_STRING],
430      ]
431    }
432  );
433
434  // remove highlight コマンド
435  commands.addUserCommand(
436    ['rml', 'removemigelight'],
437    'Remove migelight',
438    function (args) {
439      // HEAD (2)
440      if (args != undefined)
441        args = args.string;
442      if (!args)
443        return MF.removeHighlight(MF.highlightColor);
444      if (args == 'all')
445        return [f() for each (f in MF.storage.highlightRemovers)];
446      for each (let color in args.split(/\s+/))
447        MF.removeHighlight(color);
448    }
449  );
450 
451  // find コマンド
452  commands.addUserCommand(
453    ['mf[ind]'],
454    'Migemized find',
455    function (opts) {
456      if (!MF.findSubmit(opts.arguments.join(' '), opts['-backward'], opts['-color']))
457        liberator.echoerr('not found: ' + MF.currentSearchText);
458    },
459    {
460      options: [
461        [['-backward', '-b'], commands.OPTION_NOARG],
462        [['-color', '-c'], commands.OPTION_STRING],
463      ]
464    }
465  );
466
467  // 外から使えるように
468  liberator.plugins.migemizedFind = MF;
469
470}catch(e){liberator.log(e);}})();
Note: See TracBrowser for help on using the browser.