/ 最近 .rdf 追記 編集 設定 本棚

脳log[20071230] SHJS - Syntax Highlighting in JavaScriptでシンタックスハイライト



2007年12月30日 (日)

最終更新: 2010-01-06T04:21+0900

[tDiary][SHJS] SHJS - Syntax Highlighting in JavaScriptでシンタックスハイライト

こちらを参考にしました。http://www.revulo.com/blog/?date=20070817#p01

 追加や変更が必要なファイルとディレクトリの一覧。

shjs/sh_main.min.js
SHJSのメインスクリプト。
shjs/sh_style.css
デフォルトのハイライトテーマ。
shjs/lang/*.min.js
各種言語用の色分け定義ファイル。
shjs/css/*.css
切り替え可能なテーマ集。
misc/lib/hikidoc.rb
(tDiary-2.2.0からこの位置に存在する) Wikiスタイルが利用するライブラリ。
misc/plugin/shjs.rb
これから書く tDiaryプラグイン

 shjsディレクトリについて

http://shjs.sourceforge.net/doc/download.html の download a binary distribution をたどってダウンロードした ZIPファイルを tDiaryのインストールディレクトリの下に展開する。

  • shjs、shjs/lang、shjs/cssディレクトリのパーミッションには x が必要。(ブラウザの要求に応じて HTTPサーバーが中の個別のファイルにアクセスできなければいけないから)
  • cssファイルと jsファイルのパーミッションには r が必要。(ブラウザの要求に応じて HTTPサーバーがファイルの内容を読めないといけないから)
  • なお、shjsディレクトリの位置はパーミッションを満たしてさえいれば index.rbと同じディレクトリに限らずどこでも良い。あとで tDiaryの設定画面から、設定をデフォルトから変更する必要が生じるが。

 misc/lib/hikidoc.rbについて

今日まで複数行PRE記法の存在すら知らなかったわけだけど、hikidoc.rbには複数行PREにシンタックスハイライト機能を簡単に追加するためのコードが既に存在していた。(参照:http://kazuhiko.tdiary.net/20060915.html#p01)

けれど、もう SHJSを使うことに決めているので、その部分の二行をコメントアウトしてその下に一行付け加えた。

  def parse_pre( text )
    ret = text
    ret.gsub!( /^#{MULTI_PRE_OPEN_RE}[ \t]*(\w*)$(.*?)^#{MULTI_PRE_CLOSE_RE}$/m ) do |str|
      begin
        raise if $1.empty?
#        convertor = Syntax::Convertors::HTML.for_syntax($1.downcase)
#        "\n" + store_block( convertor.convert( unescape_html( restore_pre( $2 ) ) ) ) + "\n\n"
         "\n" + store_block( %Q[<pre class="sh_#{$1.downcase}">%s%s</pre>] % [parse_plugin( %Q{{{ shjs('#{$1.downcase}') }}} ), restore_pre( $2 )] ) + "\n"
      rescue

これにより

<<<ruby
ruby script here
>>>

<pre class="sh_ruby">
ruby script here
</pre>

へと変換される。あとはブラウザが javascriptに従って構文を色分けしてくれるというわけだ。

 misc/plugin/shjs.rbについて

SHJSのスタイルシートとスクリプトを日記に埋め込むためのプラグイン。SHJSに同梱されているたくさんの CSSファイルのプレビュー機能が欲しくて設定画面も作った。

shjs.rb 設定画面

日記の中で明示的に呼び出して使うプラグインではないので、shjs.rbを有効にして一度 設定を済ませてしまえば、あとは複数行PRE記法で言語名を指定したときに勝手に構文がハイライトされる。(ではどこで呼び出されるのかといえば、前項の misc/lib/hikidoc.rbに忍ばせてあったのだ)

動作テストもかねて shjs.rbの全文を貼り付けてみる。(後半は Rubyスクリプトというより HTMLなんだけど、なんで HTMLタグがうまく色づけされてるんだ?)

def shjs_init
	@shjs_required_langs = [];
	'';
end

def shjs(lang, code=nil)
	@shjs_required_langs.push(lang) if(@shjs_required_langs and not @shjs_required_langs.include?(lang));
	return code.nil? ? '' : %Q[</p>\n<pre class="sh_#{h lang}">#{h code}</pre>\n<p>];
end

def shjs_footer
	return (@shjs_required_langs && !@shjs_required_langs.empty?) ? <<"HTML" : '';
<link rel="stylesheet" type="text/css" href="#{h shjs_style_url}">
<script type="text/javascript" src="#{h shjs_js_url}"></script>
#{@shjs_required_langs.sort.map{|lang|
%Q[<script type="text/javascript" src="#{h shjs_js_url(lang)}"></script>]
}.join("\n")}
<script type="text/javascript">
	sh_highlightDocument();
	sh_languages = null;
</script>
HTML
end

def shjs_style_url(css=@options['shjs_style'])
	url = '';
	url << (@options['shjs_url'] || 'shjs');
	url << (css ? "/css/#{u css}.css" : "/sh_style.css");
	return url;
end

def shjs_js_url(lang=nil)
	url = '';
	url << (@options['shjs_url'] || 'shjs');
	url << (lang ? "/lang/sh_#{u lang}.min.js" : '/sh_main.min.js');
	return url;
end

add_header_proc{
	shjs_init;
	'';
}

add_footer_proc{
	shjs_footer;
}

if(@mode.index('conf'))

	def shjs_csslist
		unless(@shjs_csslist)
			@shjs_csslist = [];
			Dir.chdir("#{@options['shjs_dir'] || 'shjs'}/css"){
				Dir.glob('*.css').sort.each{|css|
					@shjs_csslist.push(css.chomp('.css'));
				}
			}
		end
		return @shjs_csslist;
	rescue Exception
		@shjs_csslist_errmsg = $!.to_s;
		return [];
	end

	def shjs_saveconf
		@conf['shjs_style'] = @cgi.params['shjs_style'][0].to_s;
		@conf['shjs_url'] = @cgi.params['shjs_url'][0].to_s.chomp('/');
		@conf['shjs_dir'] = @cgi.params['shjs_dir'][0].to_s.chomp('/');
		%w(shjs_style shjs_url shjs_dir).each{|key|
			@conf.delete(key) if(@conf[key].empty?);
		}
	end

	add_conf_proc( 'shjs', 'SHJS シンタックスハイライト', 'theme' ){
		shjs_saveconf if(@mode == 'saveconf');

		shjs_init; shjs('ruby');
		<<-"CONFFORM".sub('RUBYSCRIPT', h(<<-'RUBYSCRIPT'.gsub(/^\t+/, '')))
		<h2 class="subtitle">SHJS - Syntax Highlighting in JavaScript</h2>
			<p>http://shjs.sourceforge.net</p>

		<h3>配色設定 (shjs_style)</h3>
			<p><select name="shjs_style"><option value="">sh_style</option>#{
					shjs_csslist.map{|style| %Q[<option#{' selected="selected"'if(style==@conf['shjs_style'])}>#{h style}</option>] }.join('')
				}</select></p>
			<p>サンプル Rubyスクリプト</p>
				<pre class="sh_ruby">RUBYSCRIPT</pre>
			<p>デフォルトは <nobr>#{h @conf.base_url}shjs/sh_style.css</nobr></p>
			<p>すこし上にスタイルシートのリストが表示されていないときは shjs_dirを先に設定してください。</p>
			<p><strong>#{@shjs_csslist_errmsg}</strong></p>
		<h3>SHJSをインストールしたフォルダ (shjs_dir)</h3>
			<p>sh_main.min.jsファイルと sh_style.cssファイル、cssフォルダと langフォルダが入ったフォルダです。</p>
			<p>デフォルトは <nobr>#{h Dir.pwd}/shjs</nobr></p>
			<p><input name="shjs_dir" type="text" value="#{h @conf['shjs_dir']}" style="width:90%"></p>
		<h3>shjs_dirにブラウザでアクセスするときの URL (shjs_url)</h3>
			<p>デフォルトは <nobr>#{h @conf.base_url}shjs</nobr></p>
			<p><input name="shjs_url" type="text" value="#{h @conf['shjs_url']}" style="width:90%"></p>
		CONFFORM
			$KCODE = 's'
			require 'shjs'
			def say_hello
			   puts :hello
			end
			10000.times{ say_hello }
			%w[this is not string literal but array.]
			if @mode =~ /\A(save)?conf\z/i
			   # show config form.
			end
			exit unless defined? Const;
		RUBYSCRIPT
	}
end

 追記@2007-12-31:hikidoc.rbをいじらない方法

プラグインメソッド shjs()に二番目のパラメータ(code)を追加。これで、SHJSをサーバーへコピーして、shjs.rbを有効化&設定するだけでシンタックスハイライトを使うことができる。hikidoc.rbの変更は不要。

{{shjs 'ruby', <<RUBY
print "hello"
print "hello"
print "hello"
RUBY}}

と書くと

print "hello"
print "hello"
print "hello"

こうなる。複数行PRE記法よりタイプ数は多くなるけど、プラグインの枠内におさまってるので tDiary本体をいじるハードルはなくなった。この前書いたようにスタイル関連のスクリプトで迂闊に NameErrorを発生させるとその日の日記がなかったことになるので、避けられる危険は避けるに越したことはない。

 追記@2008-01-02

shjs.rbで埋め込む SCRIPTタグに defer="defer"を付けようかと思っていたら逆に window.onloadで実行していた sh_highlightDocument() をその場で実行することに。

window.onloadって画像も含めた全ての外部ファイル読み込みが終わってから呼ばれるんだよね。でもページが読める状態になっても読み込みが完了していないことってザラにあるわけで、それじゃ遅い。色のついてないコードを見せてしまうことになる。

今度 sh_highlightDocument()を呼ぶ場所(タイミング)は HTMLのほとんど末尾なので操作対象の DOMは既にアクセス可能になっている。問題なし。ではスクリプトの方は?

sh_highlightDocument()を定義する sh_main.js(sh_main.min.js)ファイルの評価が終わる前に sh_highlightDocument()を呼ぶことはできない。sh_main.min.jsを読み込む SCRIPTタグに試しに defer="defer"を付けたら Firefoxでは問題はなかったが IE7は期待通りにエラーを出してくれた。defer="defer"を付けるわけにはいかない。(付けなければ大丈夫なのかは別の問題だけど、今のところエラーは出ていない)

そんなわけで HTMLのレイアウトを優先するつもりがスクリプトの実行を優先する結果になってしまった。

 追記@2008-01-10:<link>要素の位置が文法違反

<link>は <head>内に置かなければいけない。

ハイライト機能を使ったときだけ SHJS関連の CSSや JSファイルを参照したいから footer_procで <link>や <script>を出力しているが、これは本文が評価される前の header_procの時点*では SHJSが使われているか否か判断できないからこうなっている。

Firefox2も IE7もよきに計らってくれるので実害はない。

 追記@2008-01-12: hikidoc.rb (VERSION 0.0.2)を変更する場合は……

--- hikidoc.rb.002	Sun Jan 13 01:19:03 2008
+++ hikidoc.rb	Sun Jan 13 04:02:25 2008
@@ -668,6 +669,12 @@ class HikiDoc
       syntax = info ? info.downcase : nil
       if syntax
         begin
+          # Use "Syntax Highlighting in JavaScript"
+          #  instead of Syntax::Convertors::HTML.
+          @f.print %Q(<pre class="sh_#{escape_html_param syntax}">), text(str), "</pre>\n"
+          @f.puts inline_plugin(%Q(shjs #{syntax.dump}))
+          return  
+
           convertor = Syntax::Convertors::HTML.for_syntax(syntax)
           @f.puts convertor.convert(str)
           return

* Hikiでは header_procの呼び出しより本文の評価の方が早い気がする。header_procで初期化とかしてるとはまる。