最終更新: 2011-10-28T14:10+0900
^http://www\.google\.(?:[^/]+)/url\?.*?[&;]cd=(1\d)\b.*?[&;]url=[^&;]*?(\d{6}[\dpct]*).*?[&;]q=([^&;]+).*$ google検索 \1th of「\3」→ \2 ^http://www\.google\.(?:[^/]+)/url\?.*?[&;]cd=(\d*1)\b.*?[&;]url=[^&;]*?(\d{6}[\dpct]*).*?[&;]q=([^&;]+).*$ google検索 \1st of「\3」→ \2 ^http://www\.google\.(?:[^/]+)/url\?.*?[&;]cd=(\d*2)\b.*?[&;]url=[^&;]*?(\d{6}[\dpct]*).*?[&;]q=([^&;]+).*$ google検索 \1nd of「\3」→ \2 ^http://www\.google\.(?:[^/]+)/url\?.*?[&;]cd=(\d*3)\b.*?[&;]url=[^&;]*?(\d{6}[\dpct]*).*?[&;]q=([^&;]+).*$ google検索 \1rd of「\3」→ \2 ^http://www\.google\.(?:[^/]+)/url\?.*?[&;]cd=(\d+).*?[&;]url=[^&;]*?(\d{6}[\dpct]*).*?[&;]q=([^&;]+).*$ google検索 \1th of「\3」→ \2
パラメータの順番を仮定しないために、結局こうなった。名前付きキャプチャは関係なくて、繰り返し付きキャプチャに最終的に入るものを把握することが大事。
^http://www\.google\.(?:[^/]+)/url(?:[?&;](?:url=[^&;]*?(\d{6}[\dpct]*)[^&;]*|q=([^&;]+)|cd=(1\d)|(?!cd=)[^&;]*))*$ google検索 \3th of「\2」→ \1 ^http://www\.google\.(?:[^/]+)/url(?:[?&;](?:url=[^&;]*?(\d{6}[\dpct]*)[^&;]*|q=([^&;]+)|cd=(\d*1)|(?!cd=)[^&;]*))*$ google検索 \3st of「\2」→ \1 ^http://www\.google\.(?:[^/]+)/url(?:[?&;](?:url=[^&;]*?(\d{6}[\dpct]*)[^&;]*|q=([^&;]+)|cd=(\d*2)|(?!cd=)[^&;]*))*$ google検索 \3nd of「\2」→ \1 ^http://www\.google\.(?:[^/]+)/url(?:[?&;](?:url=[^&;]*?(\d{6}[\dpct]*)[^&;]*|q=([^&;]+)|cd=(\d*3)|(?!cd=)[^&;]*))*$ google検索 \3rd of「\2」→ \1 ^http://www\.google\.(?:[^/]+)/url(?:[?&;](?:url=[^&;]*?(\d{6}[\dpct]*)[^&;]*|q=([^&;]+)|cd=(\d+)|(?!cd=)[^&;]*))*$ google検索 \3th of「\2」→ \1
リファラ周りを覗いたついでに、セルフリンクの記録を解禁した。my-sequel.rbプラグインの機能を代替できるかな、と。
tdiary.confのベースとした tdiary.conf.sampleに最初からセルフリンクよけがあるのでこれを削除。dayモードからのリンクでないものを除外するように(=dayモードからのリンクだけを記録するように)ブラウザで設定した。(index.rbの有無とか日付を ?date=形式で渡してるかどうかとかサイトに合わせた調整が必要)
リンク元記録除外リスト ^http://vvvvvv\.sakura\.ne\.jp(?!/ds14050/diary/\d{8})
最新の日記にだけ表示される「以前の日記へのリンク元」からのリンクは自己言及とは言えずノイズでしかないので 05referer.rbの referer_save_currentに次のコードを挿入した。
# reject self-reference from (maybe) volatile list return if referer.start_with?(@conf.base_url) and (date = referer.scan(/\b(\d{4})(\d\d)(\d\d)\b/).last) and latest_day?(Struct.new(:date).new(Time.local(*date)))
期待通り動いてるか動いてないかはまだ観察できてない。
「以前の日記へのリンク元」へセルフリンクを記録しない方法をとらなかったのは、そこで過去の日記の動きを観察したかったから。でもリストに記録された日からどこへ移動したかはわからないね。リファラを集約するときに「どこへ(のリンクか)」という情報を蔑ろにしたせいだ。余計なお世話だと思ってた googleの新しい形式(といっても2009年)のリファラがその情報を与えてくれてたりする。
tdiary.conf.beginnerには FeedReaderからのリンクを記録しない設定が予め用意されている。俺も除外したいと思った。でも tdiary.conf.sampleには用意されていない。beginnerの定義、.beginnerと .sampleを分ける法が知りたい。
どうやら volatile referer listからのリンクの除外に失敗してる。原理的にリファラを表示したときとリンクを踏んだときの間に日記の投稿があった場合には除外できないんだけど、それにしては数が多い。volatile referer listは特別に内部リンクを表示しないことにする。
@referer_volatile.each_referer( limit ) do |count,ref| next if ref.start_with?(@conf.base_url) # この一行。 result << %Q[<li>#{count} <a rel="nofollow" href="#{h ref}">#{h disp_referer( @referer_table, ref )}</a></li>\n] end
最終更新: 2011-08-04T03:29+0900
日と日の境が行き過ぎてしまわないようにスクロール量を制限する。
うっとうしいかな? スムーズスクロールも効かないしな。でも AutoPagerizeの機能の一部としてあってもいいような気もする。
動作確認済み> Firefox 5.0, Internet Explorer 9, Safari 5.0.2, Opera 11.50
* この日記(tDiary-2.3.3.20091124)にはまだ js_urlメソッドとか enable_jsメソッドとか無いのです。
最終更新: 2017-09-15T10:23+0900
なんというか……ばっちい(だから非公開(書き直すつもりがないので観念した))。だらだら書き下すとこうなるのね、っていう。申し訳のように抜き出したカスタマイズポイントもうまく分離できなかった記憶がある。たんに切り取って別の場所に貼り付けただけだろう、と。svn logを見てみたら 2009年3月に書いたものらしい。最初はセクションごとに表示する仕様で、それだと日記を分断して邪魔なので一日の最後に表示するオプションを付けたとか。……logによれば。とりあえずこの日記(tDiary-2.3.3.20091124, Ruby-1.8.7-p248)では動いてるみたい。
「カテゴリ[……]の他の日記」リンクの URLはこの日記に特有の、最新表示とカテゴリ表示をくっつけたもののだから他の日記では使えないね(それも非公開の理由)。
2007年や 2009年の日記のタイトルが 2011年のこの日記から参照できてるのは「プラグインが自由に日記データを取得できる手段を提供した」恩恵を my-exプラグインが受けているからじゃないかと推測してる。
最終更新: 2012-12-12T02:17+0900
適当にカスタマイズして設置した。
XHTML化キットの存在は投稿されたアナウンスを読んで知ってたんだけど、XHTML好きにもかかわらず今まで導入してなかったのは、日記に数式を書く機会がなかったのと、出力段階での文字列置換による XHTML化が乱暴に思えたから。結局、他に方法がないので目をつぶったが。
閲覧不可能になった日記がないか月別表示で全日記を表示してみたら 10か所くらい見つかった。パターンは以下の通り。
<script>タグの中であっても & は & と書かなければいけない。不等号も。でも if(a && b) {}
とか置換してしまったらアホだ。CDATAセクションを使う。
scriptタグが解釈できないブラウザなんて PCを手に入れた当時から持ってなかったので
<script><!-- --></script>
なんて最初から書かなかったし、XHTMLを知ってからは下のように書くことにしてる。
<script>/*<![CDATA[*/ /*]]>*/</script>
コメントで囲うのは JavaScriptとして正しくあるため。より字数の少ない行コメントにしないのは、なにかでエラーになったから。MSがリリースしてた JavaScriptを暗号化(難読化)するツールに <script>タグを含んだ HTMLファイルを通したときだったか……。
但し書きを付けても無駄。HTMLと日本語は混ぜるな危険。中途半端な HTMLの隠蔽は悪。tDiaryスタイルと etDiaryスタイルより Wikiスタイル。
自業自得。さすがに、タグに挟まれた部分に & や <, > を放置してたわけではない。
<a href="ttp://hoge/script.cgi?a=b&c=d"> <img src="ttp://hoge/script.cgi?a=b&c=d">
すべてこの形の & がエラーになっていた。HTMLを出力するスクリプトでは、属性値は機械的に必ず HTMLエスケープするようにしてるんだけど、手書きだと上のような (X)HTMLは正しく見えてしまうのか、忘れられてた。こんなことがあるとやっぱりセミコロンで区切りたいねえ。
それにしても、ちらちら目に入る古い日記が恥ずかしくて死ねる。読む人間も、そもそも読める内容もないのであえて気づかぬふりで放置するけども。
最終更新: 2010-05-01T17:13+0900
リストをネストしたときだけ生のテキストに<p>タグが付くのがオリジナルのはてなと違うんだそう。ついでに空の<p>タグが生成されるのも気になるけどそれは別。
最初に。<li>はブロック要素もインライン要素も包含可能らしいけど両方を並べて含んでいいのかはわからなかった。読めない(block or inlineの 0以上の繰り返し、だという気はする)。Another HTML-lintが文句を言わなかったのでいいものとしよう。
次に、CodeReposの hatena_style.rbの中を見てみる。Hatena::Blockがなにやら特別な位置を占めてるけど、一応 Hatena::Inlineと同じように振る舞うみたい。ところで、Hatena::Blockはブロック要素の連なり、Hatena::Inlineはインライン要素の連なりを表してるけど、その両方の連なり(<li>の中身とか)を表すクラスがない。
てっとりばやく済ませるために、Hatena::BlockAndorInlineというクラスを用意して Hatena::Blockが行っていた仕事をごっそり移動するそれだけでなく。Hatena::BlockAndorInlineにはインライン要素を含んでいいのかを示すフラグを initializeを通して与え、それにより何が何でも<p>タグで囲むのかそのままにするのかという動作を変えることにした。現状、求められてるのがその違いだけだから。
Hatenaスタイルは使ってないからあからさまなバグがあっても気付かない可能性が大きいです。
suppress_ptag_in_nested_list.patch (7.1KiB, 2010-04-16, r37217との差分)古くなりました。
順序付きリストや定義リストの対応を忘れてる。
<dt>の中はインライン要素の 0以上の繰り返し、<dd>の中はインライン要素とブロック要素の 0以上の繰り返しっぽい。Hatenaスタイルでは定義リストにはインライン要素しか認めていないので対応は必要なし。順序付きリストだけ。
suppress_ptag_in_nested_list.rev2.patch (8.2KiB, 2010-04-16)
svn diffで -x -p (-x <ARG>: diffに渡すオプション, -p: Cの関数名を表示)を初めて付けてみたけど、Rubyスクリプトであってもクラス名は拾ってくれるみたい。
次々空いてくる穴をふさぐのはどちらかといえば好きだけど……。
それはそれとして、
Hatena::Section#bodyは期待通りうごかないかも。(目的がデバッグ出力だったなら OKだけど)
def body @tree.body.to_s end
@treeは以前は Hatena::Blockか Hatena::Inlineだった、俺の変更で Hatena::BlockAndorInlineか Hatena::Inlineになった。カスタマイズされた to_sメソッドを持っているのは Hatena::Blockだけ。
肝心の深すぎる再帰は再現できない。自分自身を子要素に持ってるなんてことはないはずだから convertメソッドが自分自身を呼び出すしかない。その可能性があったのは Hatena::Block#convertのみ。そのときは if @body == selfでチェックして @body(Hatena::Block).convertを回避していたし、今は @bodyに Hatena::Inlineか Hatena::BlockAndorInlineしか入っていないから自分自身を呼ぶことは考えられない。わからん。キャッシュのせいだったら拍子抜けだなあ。<<<以前は @bodyに Hatena::Blockが入っていたんだから、古いデータと新しいコードの組み合わせで無限再帰は十分ありうる話。.parserキャッシュにはクラス名やインスタンス変数も入ってるし。
fix_c37239.patch (623B, c37239の修正)
サブタイトルの有無の判断が間違って反対になっていた。
サブタイトルのみのときにエラーになっていた。
fix_c37240.patch (955B, c37240の修正)
順序付き、順序なしリストの階層が勝手に深くなっていた。
セクションの分割に失敗していた。
たぶん HTMLソースレベルの空行なんだと思う。Hatena::BlockAndorInline#convertでインライン要素の後だけは改行を付加しないようにすればなくせるが、<p>の有無と違って(white-space:preとかしない限り)見た目に関与しないのでこだわらない。(<p>の後ろと</p>の前にも改行が挿入される、だとか考え始めるときりがない)
スレ主は hatena_style.rbを使うことにこだわってはいないという。こちらは楽しんでやってるんだけど、いつまでもデバッグに付き合ってもらうわけにもいかないだろう。はてなはウクレレ記法ができてこそのはてなだと思ってるので、それができない hatena_style.rbから乗り換えるのも悪くないと思う。
「ウクレレ記法」で検索したら主犯が見つかった。最初は Hikiに実装したって書いてるよ。(始めて一か月ほどだったとも書かれていて)つくづくプログラミングは何に使うかが大事だと思う。(英語なんかと一緒でツール)
http://coderepos.org/share/ に書かれている通りにメールを送ってみたが音沙汰がない。スパム判定されたか。comitterってそのページに書かれてるので全てだよね。行動力のある人間なら(というか人並みの人でも) Twitterなり IRCなり blogなりでアクションを起こすんだろうが……。(だから手続きが自動化されてなさそうなのが嫌だったんだ。漏れるし遅れるし。それにサービス自体個人の奉仕に依存してる)
気になる。自分のバグも他人のバグも気になる。
Hatena::Section#initializeの部分の修正は、既に行頭の *で分割された文字列が与えられるから問題ないとして先のファイル(fix_c37240.patch)に含めてなかったけど、まあ subが gsubになったって減るもんじゃなし。
それだけでなく 実は一行だけ buffer = '' を buffer = lines.shiftに書き換えた。これがないと無限ループに陥るケース(「>>>」という本文。3文字目は他の文字でもいい)があるみたいなので。
最終更新: 2010-05-07T12:01+0900
設定画面に jQueryが仕込まれたのと、JavaScriptの置き場所が用意されたので、ファイルのアップロードを非同期に行う準備が整ったのではないか。独自のサブミットボタンを編集画面に貼り付けるプラグインが全部非同期になれば、プレビュー画面にアップロードフォームを表示しても(編集中の本文が消えたと)怒られないだろう。
ジャンクだけど Firefox3.6と Internet Explorer8と Google Chrome4.1で動くものをこの日記(最新じゃないので jQuery対応ではない)に仕込んでみた。
これで本文を書きながらプレビューもできるしファイルのアップロードもできる。
クリップボードアクセスのために Flashを使うよりはましだけど、<iframe>に頼らなければいけないというのもわりと屈辱的。<frameset><frame>以上に、これまで存在を認めてこなかったのに。
何となくできないものと思っていたけど <iframe>の中から外のドキュメントにアクセスすることができるみたい。それができたら編集画面が無制限に入れ子になる(編集画面の<iframe>の中の編集画面の<iframe>の中の……)ことを今より簡単に防止できて、スクリプトももうちょっと使いやすく書き換えられる。
「Karetta|[JavaScript] Firefoxでtextareaのカーソル位置に文字列を挿入した後にスクロールが先頭に戻ってしまう問題(karetta.jp)」で紹介されている通りで直った。といってもできるだけ余計なことをしないように条件を付けたが。
var scrollTop = textarea.scrollTop; /* ここで、テキストエリアに文字列を挿入する。 */ if(0 == textarea.scrollTop && 0 < scrollTop) { // スクロール位置がリセットされていたので復元する。 textarea.scrollTop = scrollTop; }
ところで、Internet Explorer 8だけはテキストエリアのキャレット位置を保存しようなんてことを考えなかったみたいで、いったんテキストエリアがフォーカスを失うとキャレットが必ず末尾に移動する。スクロール位置がどうこうどころではないわな。
「何となくできないものと思っていたけど <iframe>の中から外のドキュメントにアクセスすること」 < HTML5 sanbbox属性
最終更新: 2014-12-05T17:23+0900
送信元が限られてるのかワンパターンだったので、これまでは </a> を NGワードにしてほとんどの spamを防いでいたんだけど、今日は手ひどくやられた。
フィルタはインストール方法がよくわからない。Comment-key Filterを tdiary/filter/ に設置したがエラーが出るので、とりあえず最新の 2.3.3.20091124にアップデートした。そうすると plugin/60sf.rbが有効になっているはずなので misc/filter/ にフィルタスクリプトを、misc/filter/plugin/ にフィルタに関連する設定などを表示するプラグインスクリプトを、misc/filter/plugin/ja/ などにプラグインの言語リソースをインストールし、設定画面でフィルタを有効にするといずれのスクリプトも読み込まれるようになる。
コメントフォームにキー文字列を埋め込み、コメントが HTMLフォームを通して投稿されたことを確認する。キーは日記設置者の決めた文字列と日記の日付によって一意に決められるので、ある日のキー文字列を一度取得すればその日には何件でも機械的に投稿することができる。GETして HTMLを切り刻む手間をかければ最初から機械的に投稿することもできる。そんなのは防げない。
tDiary-2.3.3.20091124にインストールするのなら key.rbを misc/filter/key.rbへ、comment_key.rbを misc/filter/plugin/key.rbへ、ja/comment_key.rbを misc/filter/plugin/ja/key.rbへコピーすれば良い。key.rbというファイル名と KeyFilterというクラス名は対応しているので、key.rbから comment_key.rbへの改名はやめたほうがいい。コピー後にフィルタを有効にし、キー文字列を設定する。
連投対策。同じ IPアドレスからの連続する投稿をはじく。30分につき 3件までしか許可しない、など。
# coding: utf-8 # # limit_freq.rb: # # a spam-filtering plugin of tDiary # rejects frequent comments posted from an IP address. module TDiary::Filter class LimitFreqFilter < Filter def comment_filter( diary, comment ) now = Time.now.to_i comment_i = Comment_t.new( now, @cgi.remote_addr ) # 数値形式の日時と文字列形式の IPアドレスの二要素 # 配列の配列を、日時をもとに昇順にソートしたもの。 # log = [[123456, "1.2.3.4"], [234567, "5.6.7.8"],...] log = [] require 'pstore' ps = PStore.new( cache_path ) ps.transaction( false ) { |db| log = db.fetch( DBRoot, log ) # 新しいコメントを logに追加する。 log.insert( ArrayExtension.lower_bound(log){|pair| pair.first - comment_i.time }, [comment_i.time, comment_i.ipaddr] ) # 古い logを捨てる。 oldest = now - time_span log.slice!( 0 ... ArrayExtension.lower_bound(log){|pair| pair.first - oldest } ) db[DBRoot] = log db.commit } # 残った logに投稿者の IPアドレスがいくつ含まれるか数え、 # その数が閾値を超えていないか確かめる。 count = 0 if log.all?{|pair| count += 1 if pair.last == comment_i.ipaddr count < threshold } then return this_is_not_a_spam( comment ) else debug("limit_freq.rb: spam: the # of comments posted from #{comment_i.ipaddr} exceeds threshold:#{threshold} within the last #{time_span.to_i}seconds.") return this_is_a_spam( comment ) end rescue debug("limit_freq.rb: BUG: #{$!}") return this_is_not_a_spam( comment ) end private DBRoot = 'LimitFreqLog' Comment_t = Struct.new(:time, :ipaddr) def cache_path return File.join( (@conf.cache_path || "#{@conf.data_path}cache"), 'limit_freq.data' ) end # どれだけの期間(秒単位)、コメントの投稿頻度の判定に # 利用するログ(日時とIPアドレスのペア)を保存するか。 def time_span 30 * 60 end # time_span当たり、何件目の投稿からを拒否するか。 def threshold 4 end module ArrayExtension # ソート済みだという前提を活かしていない! def lower_bound( arr, &block ) index = 0 arr.length.times{ return index if 0 <= yield( arr[index] ) index += 1 } return index end module_function :lower_bound end end end
やめればいいのに本体もちょろちょろと変更。
Index: core/tdiary.rb =================================================================== --- core/tdiary.rb (リビジョン 44436) +++ core/tdiary.rb (作業コピー) @@ -342,6 +342,22 @@ @logger.info("#{@cgi.remote_addr}->#{(@cgi.params['date'][0] || 'no date').dump}: #{msg}") end + + private + # config: hide or drop spam comment + def hide_spam? + return @conf.options.include?('spamfilter.filter_mode') && @conf.options['spamfilter.filter_mode'] + end + + def this_is_a_spam( comment ) + comment.show = false + return hide_spam? + rescue + return false # comment could be a String(@cgi.referer). + end + def this_is_not_a_spam( comment ) + return true + end end end @@ -1303,7 +1319,7 @@ filter_path = @conf.filter_path || "#{PATH}/tdiary/filter" Dir::glob( "#{filter_path}/*.rb" ).sort.each do |file| require file.untaint - @filters << TDiary::Filter::const_get( "#{File::basename( file, '.rb' ).capitalize}Filter" )::new( @cgi, @conf, @logger ) + @filters << TDiary::Filter::const_get( "#{File::basename( file, '.rb' ).split(/[-_]+/).map{|x|x.capitalize}.join('')}Filter" )::new( @cgi, @conf, @logger ) end end Index: core/plugin/60sf.rb =================================================================== --- core/plugin/60sf.rb (リビジョン 44436) +++ core/plugin/60sf.rb (作業コピー) @@ -4,10 +4,9 @@ # Modified by KURODA Hiraku. SF_PREFIX = 'sf' -@sf_path = ( @conf["#{SF_PREFIX}.path"] || "#{::TDiary::PATH}/misc/filter" ).to_a -@sf_path = @sf_path.collect do |path| - /\/$/ =~ path ? path.chop : path -end +@sf_path = ( @conf["#{SF_PREFIX}.path"] || "#{::TDiary::PATH}/misc/filter" ).to_a.map{|path| + path.chomp('/') +} # get plugin option def sf_option( key ) @@ -128,7 +127,7 @@ if File.readable?( path ) then begin require path - @sf_filters << TDiary::Filter::const_get("#{File::basename(filename, ".rb").capitalize}Filter")::new(@cgi, @conf, @logger) + @sf_filters << ::TDiary::Filter::const_get("#{File::basename(filename, ".rb").split(/[-_]+/).map{|x|x.capitalize}.join('')}Filter")::new(@cgi, @conf, @logger) plugin_path = "#{dir}/plugin/#{filename}" load_plugin(plugin_path) if File.readable?(plugin_path) rescue Exception
一件二件すり抜けるぐらいはいいかと思っていたがここ数日は毎日なのでいいかげん腹が立つ。なによりアクセスログを見たらヒット数、転送量トップが spammerだというのが決定的に許せない。節度を知れ。iptablesはいじれないので .htaccessで Apacheに拒否してもらう。
spamコメントがくる月っていうのは、いつもの決まった 2、3か国からのアクセスではなく 10近い国から少数ずつアクセスがあるもんだけど、日単位では固定の IPアドレスから数十件の spamがくるのが常だった。でも今日は一件一件みごとに IPアドレスが異なっている。IPアドレスブロックで次のステージに進んでしまったのか。
spamコメントの目的が特定の URLへの誘導であるかぎりは、コメントに含まれる URLのドメインが白か黒かを判定する方法が有効でしょうね。外部に問い合わせるのは避けたかったんだけど、spamコメントの内容が URLも含めてワンパターンだから実際の問い合わせはごくごく限られた回数にできそうだし、悪くないかな。
ここで上記の comment-keyフィルタも含めてスパムフィルタの最新版が管理されていた。 >http://coderepos.org/share/browser/platform/tdiary/filter
plugin/00default.rbに含まれるメソッド navi_itemを自分でも使っていたのだけど、tDiaryをアップデートしたら三番目の引数が真偽値からリンクの rel属性文字列へと変更されているせいで rel="true" なるリンクができていた。こうする。
def navi_item( link, label, rel = nil ) rel = "nofollow" if rel == true # backward compatibility
スパムコメント一掃のために YYYYMM.tdcを削除したけど日記の編集画面からコメントが消えない。YYYYMM.parserを開くとコメントが含まれていたし、これを削除すると編集画面からもコメントが消えた。原因はデータファイルを直接削除するというイレギュラーな操作だけど、日記の編集はマスターデータに対して行いたい気もする。
最終更新: 2013-05-21T00:39+0900
<h4>と <h5>にもアンカー(<a name="..."></a>)を与える。href属性や中のテキスト( )は URLをコピーしやすくするためのおまけ。そもそも <h4>や <h5>に id属性を付加するのでなく <a>を利用しているところからが利便性目的以外のなにものでもない。<h4>と <h5>の中に <a>を置くことには、highlight.rbを修正しなくてもハイライトが機能するというメリットもある。デメリットは見出し語の先頭に余分な空白が挿入されている、ということ。セクションタイトル右端の編集用リンク「✍」は、<h3>に含まれないように気をつかっているのですよ。
def do_html4( date, idx, opt ) strdump = lambda{|s| s.dump.gsub( /%/, '\\\\045' ) } r = @html.lstrip r.sub!( %r!<h3>(.+?)</h3>!m ) do "<h3><%= subtitle_proc( Time::at( #{date.to_i} ), #{strdump.call $1} ) %></h3>" end or r.sub!( %r!^<p>(.+?)</p>$!m ) do "<p><%= subtitle_proc( Time::at( #{date.to_i} ), #{strdump.call $1} ) %></p>" end serial = [nil, nil, nil, '%02d'%idx, '00', '00'] dumped_param = strdump.call "#{date.strftime( '%Y%m%d' )}#p#{'%02d' % idx}" header_t = Struct.new(:opentag, :level) bqlevel = 0 r.gsub!( %r!(<blockquote\b)|(</blockquote>)|<h(4|5)[^>]*>!i ) do bqlevel += 1 if $1 bqlevel -= 1 if $2 next $& if bqlevel != 0 or $3.nil? h = header_t.new($&, $3.to_i) serial[h.level, serial.length-h.level] = Array.new(serial.length-h.level){|i| i==0 ? serial[h.level].succ : '00' } frag = 'p' + serial[3 .. h.level].join('.') %!#{h.tag}<a name="#{frag}" href="<%=anchor(#{dumped_param}).gsub(/#p#{'%02d'%idx}/, #{strdump.call('#'+frag)}) %>"> </a>! end r.gsub( /<(\/)?tdiary-section>/, '<\\1p>' ) end
2段階に評価されるせいで、すんごく読みにくい。
ハッシュテーブルのキーがシンボルか文字列か気にしたくないのと、括弧や引用符やコロンがじゃまくさいので、. ひとつで済む Structに登場してもらった。
lambdaの .call() も嫌いだなあ。[] で () を代用するおぞましさよりはマシだが。
試してないけど、「end or r.sub!(...」を複数の行に分けるとシンタックスエラーになりそうなのも嫌。
破壊的メソッドが返す nilを利用するコードを初めて書いた!
H4のシリアルナンバーが増加したときに H5のシリアルをリセットするように変更した。以前はこういう連番だった。
最後が p01.02.01になるように。
URLフォーマットが変わると影響範囲が広くて困る。自分の日記へのリンクを検出する正規表現をあちこちで書き直した。
Index: core/tdiary/wiki_style.rb =================================================================== --- core/tdiary/wiki_style.rb (リビジョン 43193) +++ core/tdiary/wiki_style.rb (作業コピー) @@ -149,7 +149,7 @@ hikihtml = HikiDoc::HTMLOutput.new html.gsub!( %r!<a href="(.*?)">(.*?)</a>! ) do k, u = hikihtml.unescape_html($2), hikihtml.unescape_html($1) - if /^(\d{4}|\d{6}|\d{8}|\d{8}-\d+)[^\d]*?#?([pct]\d+)?$/ =~ u then + if /^(\d{4}|\d{6}|\d{8}|\d{8}-\d+)\D*([pct]\d+(?:\.\d+)*)?$/ =~ u then %Q[<%=my '#{$1}#{$2}', '#{escape_quote CGI.escapeHTML k}' %>] elsif /:/ =~ u scheme, path = u.split( /:/, 2 ) Index: core/plugin/00default.rb =================================================================== --- core/plugin/00default.rb (リビジョン 43193) +++ core/plugin/00default.rb (作業コピー) @@ -480,7 +480,7 @@ # make anchor string # def anchor( s ) - if /^([\-\d]+)#?([pct]\d*)?$/ =~ s then + if /^([\-\d]+)#?([pct][\d\.])?$/ =~ s then if $2 then "?date=#$1##$2" else @@ -540,7 +540,7 @@ # make anchor tag in my diary # def my( a, str, title = nil ) - date, frag = a.scan( /^(\d{8}-\d+|\d{8}|\d{6}|\d{4}|)\D*([pct]\d+)?$/ )[0] + date, frag = a.scan( /^(\d{8}-\d+|\d{8}|\d{6}|\d{4})\D*([pct]\d+(?:\.\d+)*)?$/ )[0] anc = frag ? "#{date}#{frag}" : date index = /^https?:/ =~ @index ? '' : @conf.base_url index += @index.sub(%r|^\./|, '') Index: plugin/html_anchor.rb =================================================================== --- plugin/html_anchor.rb (リビジョン 43193) +++ plugin/html_anchor.rb (作業コピー) @@ -14,7 +14,7 @@ if @conf.index.empty? or /\/$/ =~ @conf.index def anchor( s ) - if /^([\-\d]+)#?([pct]\d*)?$/ =~ s then + if /^([\-\d]+)#?([pct][\d\.]*)?$/ =~ s then if $2 then "#$1.html##$2" else Index: plugin/my-sequel.rb =================================================================== --- plugin/my-sequel.rb (リビジョン 43193) +++ plugin/my-sequel.rb (作業コピー) @@ -487,7 +487,7 @@ alias :my_sequel_orig_my :my unless defined?(my_sequel_orig_my) def my(*args) if @my_sequel_active and @my_sequel_date and @my_sequel_anchor and @mode != 'preview' then - dst_date, frag = args[0].scan(/^(\d{8})\D*(?:p(\d+))?$/)[0] + dst_date, frag = args[0].scan(/^(\d{8})\D*(?:p(\d+)(?:\.\d+)*)?$/)[0] if dst_date and dst_date < @my_sequel_date then dst_anchor = "#{dst_date}#{frag ? "#p%02d" % frag.to_i : ''}" @my_sequel.add(@my_sequel_anchor, dst_anchor) Index: plugin/my-ex.rb =================================================================== --- plugin/my-ex.rb (リビジョン 43193) +++ plugin/my-ex.rb (作業コピー) @@ -13,9 +13,10 @@ unless @conf.mobile_agent? +alias :my_overwritten_by_my_ex :my def my( a, str, title = nil ) m = /^(\d{8})\D*(?:([pct])(\d+))?$/.match( a ) - return '' unless m + return my_overwritten_by_my_ex( a, str, title ) unless m _, date, place, frag = *m if title.nil? and date and frag and @diaries[date] then
misc/plugin/html_anchor.rbと plugin/00default.rbプラグインを再び修正。
元々 | if /^([\-\d]+)#?([pct]\d*)?$/ =~ s then |
修正ミス | if /^([\-\d]+)#?([pct]\d+(?:\.\d+)*)?$/ =~ s then |
OK | if /^([\-\d]+)#?([pct][\d\.]*)?$/ =~ s then |
「ツッコミを入れる」リンクは YYYYMMDD#c へのリンクなので、cの後ろに数字が続いていない。これをうっかり除外してしまっていた。
最終更新: 2009-11-05T05:03+0900
いやらしいプラグインだなあ(笑)
しかしこの変哲のないただリンクに見えるもののどこに着目して、googleは小見出しを作成してるんだ。
最終更新: 2013-09-14T00:17+0900
使えないのを知っていて以前から HikiDocフォーマットでタイトルを書いていた。>>20090823 >>20090403
これを HTML化するのは意外と簡単。プラグインでできる。
add_title_proc {|date, title| if title.index('<') title.sub(/<span class="title">([^<>]+)<\/span>/){ %/<div class="title">#{WikiSection.new(CGI.unescapeHTML $1).body_to_html}<\/div>/ } else WikiSection.new(CGI.unescapeHTML title).body_to_html end }
今日のタイトルに含まれる「==日記==ブログ」という部分があまりにわかりにくかったので、HTML化してみた次第。URL自動リンクも有効になって、うまうま。
勘違い発覚。Headingがブロック要素を包含できる気がしていたが、Heading自身がブロック要素だということの記憶違い。<div>を含めちゃだめだ。
修正。
add_title_proc {|date, title| inline_or_nil = lambda{|src| lines = src.split(/\r?\n/) return nil if 1 < lines.length html = WikiSection.new(lines.first).body_to_html return nil if html[0,3] != '<p>' or html[-4,4] != '</p>' return html } if title.index('<') title.sub(/<span class="title">([^<>]+)<\/span>/){|_0| html = inline_or_nil.call(CGI.unescapeHTML $1) html ? %/<span class="title">#{html}<\/span>/ : _0 } else inline_or_nil.call(CGI.unescapeHTML title) or title end rescue title }
それなりにチェックはしてるけど、ブロック要素を返すプラグインを呼んだりしたら(HTMLの文法的に)即アウト。
最終更新: 2009-08-28T23:41+0900
表示方法はこう。
# coding: utf-8 require 'date' add_section_leave_proc{|date, index| diary = @diaries[date.strftime('%Y%m%d')] next unless diary # in case @mode == 'preview' section, sidx = nil, 0 diary.each_section{|sec| sidx+=1 if sidx == index section = sec break end } lm = section.last_modified rescue next next unless lm lm = DateTime.new(*(lm.utc.to_a.values_at(5,4,3,2,1,0))).new_offset(Rational(135,360)) # 日本時間 lm.strftime %<<p class="lastmodified">最終更新: %Y-%m-%dT%H:%M%Z</p>> # 色分けテストとして、あえてタグと同じアングルブラケットで囲ってみた。 }
DateTimeのオフセットの単位がわからんかった。ブラウザのブックマークが「Rubyリファレンスマニュアル - 20051129」だからなあ。るりまには載ってるかも。とりあえず「fraction of a day」(date.rb documented by William Webber)とのこと。慣れない Rationalを使ったもんだから Rational(135,360)と書くべき所に Rational(135/360)と書いてしまい、オフセット 0の結果にしばし首をひねった。Rational()の呼び出しより引数の評価の方が先だからやむをえないことだけど、分数を表現するのにはやっぱり / を使いたい(使ってしまった)。Fixnumと Bignumのシームレスな移行のように、 Rationalへも融通無碍に切り替わって欲しい。利用者には Numericだけを利用しているように思わせる、ということ。必要なときに整数化(小数化)メソッドを呼ぶし、変数に整数(や分母を 1に約分できる分数)が入ってることを利用者が知っていれば、そのまま整数を前提としたメソッドを呼んだりできるといい。変わるのは、整数型の演算結果が整数型であることを前提にした(旧来の言語の呪縛に過ぎない)切り捨て除算がなくなる以外にあるだろうか。それも Pythonみたいに // を割り当てれば、無駄な有理数化、再整数化を避けられる。実感に基づいて、既に通った(んじゃなかったっけな?)議論を蒸し返してみました。俺は整数と小数の垣根を取っ払った JavaScriptを、最初は驚いたけど、評価している。JavaScriptのシンプルさが好きだ。セミコロンインサーションも、Cより怠けることを許していながら、Rubyの改行をターミネータにするやり方よりフォーマットが自由で、最高にバランスがいい(はまるのは returnだけだ)。require 'rational'; 10.to_r/2 とか不格好すぎるでしょ。ハードウェアと型から離れて本質に戻りましょう。算数で割り算と分数は同じものだったはずだ。<追記@2009-08-22>require 'mathn' がつまり Fixnumと Bignumと Rational(と Complex)をより親密にするおまじないでした(いまさら何を)。mathnって、読めない(マスエヌ?)のと名前から中身が想像できないのとで意識せず無視してたけど、Rationalとセットで見かけることが多かった気がする。それはそうと、Rationalや Complexの細々した議論に埋もれて全体の方向性が見えない。Ruby 2.0あたりでは require 'mathn' が不要になっているんだろうか……。</追記>
るりまにはスタンドアロンサーバー版と chm版より、スタティック HTML版を用意して欲しいなあ。chmだと閲覧が IEベースになってしまって、文字の大きさやスクロール量、進む戻るが自由にならなくて使いにくい。マニュアルサーバーを起動しておくのは嫌ですよ。view.cgiで CGIしようとしたらリンクがルートからの絶対アドレスなせいで Not Found。Apacheは既に動いてるから、名前ベースのバーチャルホストや待ち受けポートの追加で、るりまに一つのホスト(or ポート)を与えることはできると思うけど……大仰なのでやらない。base_urlのオプションが用意されてるから view.cgiの設定を間違えてるだけの気もするけど……わからない。
組み込みの Timeが UTCと localtimeしか扱わないのがもったいない。任意のオフセットに基づいた日時を出力したいだけだから、DateTimeは牛刀な印象がある。<追記@2009-08-21>よーくかんがえよー(命令形)、・・・・・・・・・ー。なんてことはない。オフセット分だけ未来(過去)の UTC時刻が即ちローカルタイムだよ。当たり前すぎて俺が何を言いたいのかわからないでしょう。先のスクリプト片の最後は DateTimeを使わずにこう書ける、ということです。
lm = section.last_modified rescue next next unless lm offset = 9 * 60 * 60 # 秒 lm_local = (lm + offset).utc # UTCと見せかけて lmの地方時。 %<<p class="lastmodified">最終更新: %d-%02d-%02dT%02d:%02d%s%+03d%02d</p>> % [lm_local.year, lm_local.month, lm_local.day, lm_local.hour, lm_local.min, offset/60/60, offset/60%60] }
……てなことを、makerss.rbの中の TDiary::RDFSection#time_stringが
g = @time.dup.gmtime l = Time::local( g.year, g.month, g.day, g.hour, g.min, g.sec )
gmtimeに基づく年月日時分秒からローカルタイムを作っている部分を見ていて(遅まきながら)気付いた。gmtimeも localtimeも皮をむけば UNIX epochからの経過秒に過ぎないんだから、どういう意味を持たせるかはこちらの自由だった。まあ、比較はできなくなりますが……(lm.to_i ≠ lm_local.to_i はその意味(同じ瞬間の別表現であること)を考えると望ましくない結果)。</追記>
脱線終了。表示するまでの仕込みがこんな感じ。
主に plugin/makerss.rbからの要請で更新日時を記録したいので、ちょっとした修正では最終更新日時は更新されない。とかいいながら、この日記には変更のあったセクションを見つける別の方法が入っている(20090705p01)ので makerss.rbが最終更新日時を利用するようにはしていない。
最初にポストされた時刻も有用だろうか? 日記だから最初にポストされた日はほとんど確定してるし、時刻まで知りたいとも思わないけど。
WikiSectionに last_modifiedプロパティをくっつけたけど、WikiSection自体はこれを管理してなくて、外部から操作されるだけだってのがいけてない。やっぱり to_src()と initialize()が DUMP & LOADを担う(last_modifiedや authorその他の情報をフォーマットに含める)か、メソッド群を整備してこちらの望む操作を WikiDiary、WikiSection自身にやってもらう(自分でやるから付帯情報のコピー漏れも発生しない道理)のがいい。
「編集」でセクションを追加したとき、更新日時のコピー処理でのぬるぽを修正。(if old_section and...を追加した)
速くなると聞いては捨ておけぬ。
category_anchorでの nil.yearエラーは、起こったのがカテゴリモードだったら既知だけど最新N日表示だから違うし、TDiaryBase@dateは読み出し専用プロパティだから(navi_user.rbのような荒技を使ったりしない)プラグインには変更できないし……。(結局わかりません)
plugin/makerss.rbが直前に変更のあったセクションの他に、過去にちょっとした修正のあったセクションを *.rdfのエントリに加えてしまうのを防げるので賛成。一度考えた回避策はちょっとした修正が *.rdfに反映されてしまうので影響が大きくて断念したし。
という手順を考えた。
セクションインデックスがずれるような変更だった場合は影響範囲が無駄に大きくなるけど、セクションインデックスはセクションIDを兼ねているのでやむなし(URLだって変わっちゃってますから)。
新規作成や修正されたセクションに適用される最終更新日時を、一回の「編集」や「追記」につき一つに限らないと、update_procの中で、変更のあったセクションを見つけるときにアバウトな処理をするはめになりそう。(心配しなくてもそうはならんでしょうが)
Before...
♭ ななしデバッグモードでコンパイルすると、4883行で、 error C2440: 'int' から 'CLayoutInt..
♭ ななしrev8です。 あいうえお 1かきくけこ 0123456789 「い」の右側から、3行全部を0幅選択してスペース..
♭ ななしrev8.1でOKな気がします。 いい感じ (^^)
♭ anonymouse「1文字のTAB/SPACE入力が必ずインデント扱いになってしまっていて変」 (2009年11月10日 (火) 10..
♭ ななし念のため。 上記の、 ・タブキーとスペースキーに割り付けられた機能を削除すると、 複数行選択したときのタブやスペ..
♭ ds14050ええと、具体的にどういう動作が不満で、どういう結果になるのがよいのでしょうか。rev.9版のバイナリは http:/..
♭ ななし行末を超えた位置にも通常文字は入る。 SPACEキーにインデントを割り付けて*いない*ときは、 行末を超えた位置には..
♭ ds14050この部分ですね。 ---- if( nDataLen == 1 && IsIndentChar( pData[0] ..
♭ ななしnDataLen == 1 && IsIndentChar( pData[0] ) なんていう条件だと、 クリッ..
♭ ななし> 選択範囲が一行だけだったとき、Command_INDENT_TAB()は Command_INDENTの代わりに..
♭ ds14050>1字のSPACE/TABという条件は、コードをはじめて見た >瞬間におかしいと思ったし、誰でも同じことが想像できる..
♭ ds14050どうしようもないな。「元の挙動を残し」た。<<<大嘘
♭ ds14050なんかもうぐだぐだだけど気づいてしまってので訂正。 >知らないのは事実です。最初のコメントに書いたとおり。 最初のコ..