jquery-ptmplを拡張して、HTMLタグのclass属性を読み書きしやすく指定する

jquery-ptmplでは、内部のほとんどの関数を簡単に置き換えられるようになっています。今日はその一例を紹介します。

<script src="jquery.js"></script>
<script src="jquery.ptmpl.js"></script>

<script>

(function (jQuery, undefined) {

var oldPtmplTranslateHtmlToLiteral = jQuery.ptmplTranslateHtmlToLiteral;
jQuery.ptmplTranslateHtmlToLiteral = newPtmplTranslateHtmlToLiteral;

function newPtmplTranslateHtmlToLiteral(str) {
	str = str
		.replace(/<(\w+)\.([\w-.]+)/g, function (all, tag, klass) {
			return '<'+tag+' class="'+klass.replace(/\./g, ' ')+'"';
		});
	return oldPtmplTranslateHtmlToLiteral(str);
}

})(jQuery);

</script>

jQuery.ptmplTranslateHtmlToLiteral関数は、テンプレートのコンパイル時にのみ呼び出され、テンプレート中のテンプレートタグ以外の部分(=普通にHTMLが書いてある部分)を文字列リテラルに変換する関数です。

上記のコードでは、例えば

に置き換えるような処理を、jQuery.ptmplTranslateHtmlToLiteral関数に追加しています。

この拡張によって、

<script id="template" type="text/x-jquery-tmpl">
  <div class="hoge">
    <div class="fuga">
      ...
    </div>
  </div>
</script>

と書く代わりに、

<script id="template" type="text/x-jquery-tmpl">
  <div.hoge>
    <div.fuga>
      ...
    </div>
  </div>
</script>

と書くだけで済むようになります。

jQuery Pluggable Templates Plugin
https://github.com/atsumu/jquery-ptmpl

jquery-tmplとやや互換性のある、高速で柔軟なテンプレートエンジン jquery-ptmpl を作りました

jquery-tmplに対する不満を解消するため、テンプレートエンジンを自作しました。
ついでに、jquery-tmplより2倍から10倍以上高速に動作するようです。 (リポジトリベンチマーク参照)

ソースコードgithubで公開しています。
200行程度しかないので読みやすいはず。

jQuery Pluggable Templates Plugin
https://github.com/atsumu/jquery-ptmpl

サンプルコード

<div id="place"></div>

<script id="tmpl-sample" type="text/x-jquery-tmpl">

  <h2>this is sample.</h2>

  {{! you can use template local variable. }}
  {{$ var foo = '<span style="color:red">output raw html</span>'; }}
  {{html foo}}

  {{each(k, v) a}}
    {{if v % 2 == 0}}
      {{continue}}
    {{else v % 3 == 0}}
      {{break}}
    {{else}}
      <div>k={{= k}}, v={{= v}}</div>
    {{/if}}
  {{/each}}

  {{tmpl({ b:3 }) "#tmpl-other"}}

</script>

<script id="tmpl-other" type="text/x-jquery-tmpl">
  here is other template. {{= b}}
</script>

<scirpt>
  jQuery(function ($) {
    $("#tmpl-sample").ptmpl({ a:[1,2,3,4] }).appendTo("#place");
  });
</script>

jquery-ptmplの特徴

ローカル変数が使える
<script id="tmpl-sample" type="text/x-jquery-tmpl">
 {{$ var foo = "bar"; }}
 {{= foo}}
</script>

{{$ ...}}の中に書いたコードは、コンパイル後のコードの中にそのまま埋め込まれます。
1テンプレート=1関数としてコンパイルされるので、定義した変数は同じテンプレート内でのみ有効です。

タグを定義できる
<script>
jQuery.ptmplDefineTag({
  '=': function (code, str) {
    code.push('_PTMPL_HTML.push(jQuery.ptmplEscapeHtml((', str, ')));');
  }});
</script>

のように、jQuery.ptmplDefineTag関数を使ってテンプレートタグを定義できます。

pixivのマンガの各ページをウィンドウサイズに合わせて縮小するgreasemonkey

説明

ウィンドウからはみ出るページを、自動的にウィンドウサイズに合わせて縮小するgreasemonkeyです。
スクロールしなくてもページ全体が見えるので、サクサク読み進められるかも。

特徴は以下の通りです。

・ボタンの位置を変えるので、縦幅をギリギリまで使える
・ウィンドウサイズを変えたとき、画像サイズを自動的に合わせる
・ウィンドウサイズを変えたとき、見てたページまで自動的にスクロールする
・ウィンドウサイズより小さい画像はそのまま
・この機能を無効化するボタンを追加する

対応ブラウザ

firefox 3.6とchrome 10で動作確認しました。

HaH_modoki (Hit-a-Hint風greasemonkey)

atsumu-t2009-04-10

追記 2009-04-21 01:51

window.openをGM_openInTabに変えたので、ポップアップブロックに引っかからなくなったか引っかかりにくくなったはずです。

追記

忘れるのでメモ。
偶数文字目と奇数文字目に使う文字を別々に指定できるようにすると、子音字が連続することを防げるので、ローマ字入力の人にとって入力しやすくなる。

概要

キーボード操作のみでリンクを開くためのgreasemonkeyです。
http://d.hatena.ne.jp/javascripter/20080531/1212190340 を元にしました。

document.link.hasOwnPropertyが呼び出せなくてイヤになったので、書きかけですが公開します。
firefox3以降で動きます。
たまにしかJavaScriptを書かないので、変なところがあったらコメントお願いします。

特徴

  • フォームにフォーカスを移せる。
  • フォームにフォーカスがあるときは動かない。
  • フォームからフォーカスをはずすキーを割り当てれる。
  • リンクの選択に使うキー・使わないキーを自由に選べる。
  • 新しいウィンドウで開くかどうかを入力中にトグルするキーを割り当てれる。
  • 最後にEnterを押さなくていい。

実はHit-a-Hintとか使ったことがないので差がわかりません。

マニュアル

先頭付近にある変数oのプロパティがオプションです。

プロパティ 説明
start_keys リンクの選択を開始するキー
cancel_keys リンクの選択をキャンセルして終了するキー
input_keys リンクの選択に使用するキー
shift_is_new_window shift状態のときに新しいウィンドウで開くかどうか
shift_on_keys shift状態にするキー
shift_off_keys shift状態をやめるキー
shift_toggle_keys shift状態をトグルするキー
blur_keys フォーカスをはずすキー
tip_offset_h tipの表示位置のオフセット(縦)
tip_offset_v tipの表示位置のオフセット(横)
tip_class tipに割り当てるクラス名。他のグリモンとかぶってたら変える

どの機能にも複数のキーを指定できます。

液晶が届いたので、残りは後で書きます。

ソースコード

// ==UserScript==
// @name           HaH_modoki
// @namespace      http://d.hatena.ne.jp/atsumu-t/
// @include        *
// ==/UserScript==

var ESC = String.fromCharCode (27);
var o = {
  start_keys: 'o',
  cancel_keys: 'o' + ESC,
  input_keys: 'abcdefghjklmnprstuvwxy',
  shift_is_new_window: false,
  shift_on_keys: '',
  shift_off_keys: '',
  shift_toggle_keys: 'i',
  blur_keys: '' + ESC,
  
  tip_offset_h: 0,
  tip_offset_v: 0,
  
  tip_class: '_HaHm_tip',
};

function makeTipTemplate () {
  var t = document.createElement ('span');
  t.className = o.tip_class;
  with (t.style) {
    color = 'white';
    backgroundColor = 'rgba(0, 0, 0, 0.5)';
    paddingLeft = '0.2em';
    paddingRight = '0.2em';
    borderColor = 'white';
    borderStyle = 'solid';
    borderWidth = '0.1em';
    
    zIndex = '999999';
    position = 'absolute';
  }
  return t;
}

var g = {
  objs: new Array (),
  tip_digit: new Number (),
  enable: false,
  s: new State (),
};

function State () {
  this.shift = false;
  this.input = new String ();
}

function open (href, shift) {
  if ((o.shift_is_new_window && shift)
      || (!o.shift_is_new_window && !shift))
    //window.open (href, '_blank', '');
    GM_openInTab (href);
  else
    location.href = href;
}

function log (x, base) {
  return Math.log (x) / Math.log (base);
}

function key_match (e, s) {
  return (s.indexOf (String.fromCharCode (e.which)) != -1
          || s.indexOf (String.fromCharCode (e.keyCode)) != -1);
}

function keypress (e) {
  {
    var elem = e.target;
    var elem_name = elem.nodeName.toLowerCase ();
    
    if (key_match (e, o.blur_keys)) {
      elem.blur ();
      if (g.enable) {
        removeTip ();
        g.enable = false;
      }
    }
    if (elem_name == 'textarea'
        || elem_name == 'select'
        || (elem_name == 'input'
            && (elem.type == 'text' || elem.type == 'password'))
        || e.ctrlKey || e.altKey || e.button)
      return true;
  }

  var c = String.fromCharCode (e.which);
  if (!g.enable) {
    if (key_match (e, o.start_keys)) {
      if (!showTip ())
        return true;
      g.enable = true;
      g.s = new State ();
    }
  } else {
    if (key_match (e, o.cancel_keys)) {
      removeTip ();
      g.enable = false;
    } else if (key_match (e, o.shift_on_keys)) {
      g.s.shift = true;
    } else if (key_match (e, o.shift_off_keys)) {
      g.s.shift = false;
    } else if (key_match (e, o.shift_toggle_keys)) {
      g.s.shift = !g.s.shift;
    } else if (key_match (e, o.input_keys)) {
      g.s.input += c;
      var tips = document.getElementsByClassName ('_HaHm_tip');
      
      var removes = [];
      for (var i = 0; i < tips.length; ++i) {
        if (tips.item(i).textContent.indexOf (c) == 0)
          tips.item(i).style.color = 'yellow';
        else
          removes.push (i);
      }
      if (removes.length == tips.length
          && g.s.input.length != g.tip_digit) {
        removeTip ();
        g.enable = false;
        return false;
      }
      for (var i = 0; i < removes.length; ++i) {
        var item = tips.item(removes[i] - i);
        item.parentNode.removeChild (item);
      }
    }

    if (g.s.input.length == g.tip_digit) {
      removeTip ();
      g.enable = false;
      var n = 0;
      for (var i = 0; i < g.tip_digit; ++i) {
        n = n * o.input_keys.length + o.input_keys.indexOf (g.s.input[i]);
      }
      if (n < g.objs.length)
        {
          if (g.objs[n].proc == 'open')
            open (g.objs[n].elem.href, g.s.shift);
          else if (g.objs[n].proc == 'focus')
            g.objs[n].elem.focus ();
        }
    }
  }
  return false;
}

function showTip () {
  var template = makeTipTemplate ();

  var w_left = window.scrollX;
  var w_right = window.scrollX + window.innerWidth;
  var w_top = window.scrollY;
  var w_bottom = window.scrollY + window.innerHeight;
  var w_width = w_right - w_left;
  var w_height = w_bottom - w_top;
  
  g.objs = new Array ();
  Array.filter (
    document.links,
    function (l) {
      var pos = l.getBoundingClientRect ();
      if (0 <= pos.left && pos.right <= w_width
          && 0 <= pos.top && pos.bottom <= w_height)
        g.objs.push ({elem: l, proc: 'open'});
      return true;
    });

  {
    var inputs = document.getElementsByTagName ("input");
    var textareas = document.getElementsByTagName ("textarea");
    var selects = document.getElementsByTagName ("select");
    var as = new Array (inputs.length + textareas.length + selects.length);
    for (var i = 0; i < inputs.length; ++i)
      as[i] = inputs[i];
    for (var i = 0; i < textareas.length; ++i)
      as[inputs.length + i] = textareas[i];
    for (var i = 0; i < selects.length; ++i)
      as[inputs.length + textareas.length + i] = selects[i];

    Array.filter (
      as,
      function (a) {
        var pos = a.getBoundingClientRect ();
        if (0 <= pos.left && pos.right <= w_width
            && 0 <= pos.top && pos.bottom <= w_height)
          g.objs.push ({elem: a, proc: 'focus'});
        return true;
      });
  }

  if (g.objs.length == 0)
    return false;

  g.tip_digit = (g.objs.length == 0 ? 0
                 : Math.floor (log (g.objs.length, o.input_keys.length)) + 1);

  g.objs.forEach (
    function (link, i) {
      var l = link.elem;
      var pos = l.getBoundingClientRect ();
      var tip = template.cloneNode (true);
      var str = "";
      var rest = i;
      for (var d = g.tip_digit; d > 0; --d) {
        str = o.input_keys[rest % o.input_keys.length] + str;
        rest = Math.floor (rest / o.input_keys.length);
      }
      with (tip) {
        textContent = str;
        style.left = pos.left + w_left + o.tip_offset_h * g.tip_digit + 'px';
        style.top = pos.top + w_top + o.tip_offset_v + 'px';
      }
      document.body.appendChild (tip);
    });

  return true;
}

function removeTip () {
  var tips = document.getElementsByClassName (o.tip_class);
  while (tips.length)
    tips[0].parentNode.removeChild (tips[0]);
}

document.addEventListener ('keypress', keypress, false);