« eoblogをMarkdown記法で書く | メイン | eoblogをMarkdown記法で書く3(プレビュー) »

2011年12月17日 (土)

eoblogをMarkdown記法で書く2(SyntaxHighlighter導入)

昨日の記事(eoblogをMarkdown記法で書く)の続き。

見た目的な問題からソースコードを構文強調表示してくれるスクリプトの導入を行います。またkramdownと組み合わせたときの問題点を修正してます。

構文強調表示JavaScript

ソースコードを載せるとき、普通に<pre>タグだけではこんな感じになってしまい、味気ないです(カスタムCSS使用して枠は出してるけど)。

OptionParser.new{|opt|
  opt.on('-f','--full') {|v| @option_hash[:full] = v}        # オプション引数 なし
  opt.parse!(ARGV)
}
p @option_hash

調べてみると、構文強調表示するJavaScriptがいくつかあるみたい。

  1. Markdown記法(kramdown)で作成するので、<pre>または<code>タグで装飾されてる
  2. Ruby、JavaScript、C/C++、できればHTMLの構文強調ができる
  3. 行番号が表示できる。開始の行番号が指定できるのが望ましい
  4. <ol><li>で行番号実現してるのは嫌(HTMLソースが汚い。コピペしにくい)
  5. ソースコードの折り畳み(出来れば任意位置)ができるのがいい

以上の条件で探してみた結果Syntax Highlighterが良さげなので、これを導入してみます(任意位置の折り畳みは出来ないけど)なお今回導入したのは 3.0.83 です。バージョンが違ってたりすると導入方法が微妙に異なります。また同じSyntax Highlighterという名前の別のJava Scriptなんかもあったりして、調べててかなり混乱しました(^^ゞ

Syntax Highlighter ダウンロードとサーバへのアップロード

Syntax Highlighterのダウンロードページからダウンロードして、解凍します。

解凍画面

解凍すると、上のようなフォルダやファイルができます。重要なのは

scripts
各言語の構文定義JavaScriptや本体JavaScriptが入ったフォルダ
styles
デザインを決めるCSSファイルの入ったフォルダ

の2つ。とりあえず何も考えずにこの2つのフォルダを適当な場所にアップロードします。

eoblogの場合、コントロールパネル→ファイルで操作します(下図参照)

ファイル操作

イメージを見れば分かりますが、私はホーム/default/syntaxを作成し、その下にscripts、stylesフォルダを作成してそれぞれのファイルをアップしました。

HTMLコードの記述

次にブログ上で使えるようにするためカスタムHTMLコードを記述します。
eoblogの場合、「ブログ」→「デザイン」→「レイアウトを変更」を選択し、「モジュールの追加」をクリックします。

モジュール追加

そして、HTMLコードを記述する(モジュールの名前は何でもいい)んですが、記述の方法は2通りあります。

1.直接使用する言語のJavaScriptを埋め込む(ver 2.x方式)

<script type="text/javascript" src="$YourUploadPath$/scripts/shCore.js"></script>
<!--使用する言語のjsファイルを記述 start-->
<script type="text/javascript" src="$YourUploadPath$/scripts/shBrushJScript.js"></script>
<script type="text/javascript" src="$YourUploadPath$/scripts/shBrushRuby.js"></script>
<script type="text/javascript" src="$YourUploadPath$/scripts/shBrushXml.js"></script>
<script type="text/javascript" src="$YourUploadPath$/scripts/shBrushCpp.js"></script>
<!--使用する言語のjsファイルを記述 end-->
<link type="text/css" rel="stylesheet" href="$YourUploadPath$/styles/shCore.css"/>
<!--使用するCSSファイル(stylesフォルダのshTheme~のどれか)を指定 -->
<link type="text/css" rel="stylesheet" href="$YourUploadPath$/styles/shThemeFadeToGrey.css"/>
<script type="text/javascript">
<!--SyntaxHighlighter.config.tagName = "code";     「スクリプトの作成」参照-->
SyntaxHighlighter.defaults['toolbar'] = false;     <!--お好みで-->
SyntaxHighlighter.defaults['auto-links'] = false;  <!--お好みで-->
SyntaxHighlighter.all();                           <!--必ず必要-->
</script>

言語とJavaScriptの関係についてはSyntax Highlighter(公式)のSyntaxesを参照して下さい。(ファイル名でだいたい分かりますが・・・。)

2.Autoloaderを使用する(ver 3.x方式)

<script type="text/javascript" src="$YourUploadPath$/syntax/scripts/shCore.js"></script>
<script type="text/javascript" src="$YourUploadPath$/syntax/scripts/shAutoloader.js"></script>
<link type="text/css" rel="stylesheet" href="$YourUploadPath$/syntax/styles/shCore.css"/>
<!--使用するCSSファイル(stylesフォルダのshTheme~のどれか)を指定 -->
<link type="text/css" rel="stylesheet" href="$YourUploadPath$/syntax/styles/shThemeFadeToGrey.css"/>
<script type="text/javascript">
<!--SyntaxHighlighter.config.tagName = "code";     「スクリプトの作成」参照-->
SyntaxHighlighter.defaults['toolbar'] = false;     <!--お好みで-->
SyntaxHighlighter.defaults['auto-links'] = false;  <!--お好みで-->

function path()
{
  var args = arguments,
      result = []
      ;
       
  for(var i = 0; i < args.length; i++)
      result.push(args[i].replace('@', '$YourUploadPath$/syntax/scripts/'));
       
  return result
};
SyntaxHighlighter.autoloader.apply(null, path(
  'applescript            @shBrushAppleScript.js',
  'actionscript3 as3      @shBrushAS3.js',
  'bash shell             @shBrushBash.js',
  'coldfusion cf          @shBrushColdFusion.js',
  'cpp c                  @shBrushCpp.js',
  'c# c-sharp csharp      @shBrushCSharp.js',
  'css                    @shBrushCss.js',
  'delphi pascal          @shBrushDelphi.js',
  'diff patch pas         @shBrushDiff.js',
  'erl erlang             @shBrushErlang.js',
  'groovy                 @shBrushGroovy.js',
  'java                   @shBrushJava.js',
  'jfx javafx             @shBrushJavaFX.js',
  'js jscript javascript  @shBrushJScript.js',
  'perl pl                @shBrushPerl.js',
  'php                    @shBrushPhp.js',
  'text plain             @shBrushPlain.js',
  'py python              @shBrushPython.js',
  'ruby rails ror rb      @shBrushRuby.js',
  'sass scss              @shBrushSass.js',
  'scala                  @shBrushScala.js',
  'sql                    @shBrushSql.js',
  'vb vbnet               @shBrushVb.js',
  'xml xhtml xslt html    @shBrushXml.js'
));
SyntaxHighlighter.all();                           <!--必ず必要-->
</script>

$YourUploadPath$はscriptsやstylesをアップしたURL(http://~)で置換して下さい
ブログサービスに依るかもしれませんが、相対パス表記は駄目です。

どちらを使用しても大丈夫ですが、「Autoloaderを使用する」方はページ内で使用してる言語定義JavaScriptを動的にロードするみたいなんでお奨めです(表示が速い・・・のかな?)。なお<head>部分しかいじれないブログサービスの場合はAutoloaderを使用しない従来方式の方がいいような気がします(後述しますが、プレビュー時にもSyntax Highlighter使用しようとして<head>の最後の部分に上記コードを入れてたら上手く動かなかったので)。

書き方

<pre class="brush: ruby">
OptionParser.new{|opt|
  opt.on('-f','--full') {|v| @option_hash[:full] = v}        # オプション引数 なし
  opt.parse!(ARGV)
}
p @option_hash
</pre>

のように書けば

OptionParser.new{|opt|
  opt.on('-f','--full') {|v| @option_hash[:full] = v}        # オプション引数 なし
  opt.parse!(ARGV)
}
p @option_hash

のように表示されます1

また、タイトルを付けることも可能です。

<pre class="brush: ruby" title="sample">
OptionParser.new{|opt|
  opt.on('-f','--full') {|v| @option_hash[:full] = v}        # オプション引数 なし
  opt.parse!(ARGV)
}
p @option_hash
</pre>

表示はこんな感じ。

OptionParser.new{|opt|
  opt.on('-f','--full') {|v| @option_hash[:full] = v}        # オプション引数 なし
  opt.parse!(ARGV)
}
p @option_hash

その他指定できるパラメータの詳細については Syntax Highlighter公式のconfiguration を参照して下さい。

日本語のページなら SyntaxHighlighter - ソースコードを見やすく表示させるJavaScript(3) などが詳しいです(ver 2.x系の説明ですが、この辺りは恐らく同じです)

Markdown記法の+kramdown拡張ではブロックレベル属性の場合、{: xxx=yyy}をブロックの前に記述すると属性を指定できるので

{: class="brush:ruby"}
~~~
OptionParser.new{|opt|
  opt.on('-f','--full') {|v| @option_hash[:full] = v}        # オプション引数 なし
  opt.parse!(ARGV)
}
p @option_hash
~~~

もしくは

{: class="brush:ruby"}
    OptionParser.new{|opt|
      opt.on('-f','--full') {|v| @option_hash[:full] = v}        # オプション引数 なし
      opt.parse!(ARGV)
    }
    p @option_hash

と書けばOKだと思ってたんですが、これだと<code></code>が表示されます。

<code>
OptionParser.new{|opt|
  opt.on('-f','--full') {|v| @option_hash[:full] = v}        # オプション引数 なし
  opt.parse!(ARGV)
}
p @option_hash
</code>

こんな感じ2。毎回置換するのも鬱陶しいので、スクリプトを作って対策します。

スクリプトの作成

で、対策案ですが

  1. <pre class="brush:~">という表記の時、 <code></code>を除去する

  2. <pre class="brush:~">という表記の時、属性を<code>タグの方へ移す。

のいずれかが考えられます。HTMLマークアップ的には「2.」の方が本道のような気がするので、こちらで行きます。
(Syntax Highlighterでは構文強調を動作させるタグを変更することが可能ですし)

まずSyntax Highlighterの設定を変更します。

「直接使用する言語のJavaScriptを埋め込む(ver 2.x方式)」場合は

SyntaxHighlighter.config.tagName = "code";         <!--追加-->

のように12行目のコメントを外す。「Autoloaderを使用する(ver 3.x方式)」場合は

SyntaxHighlighter.config.tagName = "code";         <!--追加-->

のように7行目のコメントを外して、サーバーに変更を保存(&反映)します。

んで、rubyスクリプトは以下のような感じ。
お手軽なんで、変換・置換に正規表現使いまくってるけどいいのかな?(遅そう)

# -*- coding: utf-8 -*-
#require 'rubygems'
require 'kramdown'
require 'optparse'

@option_hash = {
  :tabwidth => 4,
  :blogmode => true,
  }

#markdown記法をHTML(body)に変換する
#返り値:body文字列
def mkd2htmlbody(text)
  text.encode!("UTF-8")
  #text = text.lines.to_a[1..-1].join # Just ignore the first line
  body = Kramdown::Document.new(text).to_html
  if ( @option_hash[:blogmode] )
    if (false)
      # <pre class="brush~><code>~</code></pre>
      # の場合、<code>、</code>が邪魔なので取り除く
      body = body.gsub(\
          %r!<pre[\s\t]+([^<>]*?)class[\s\t]*="brush(.*?)>[\s\t\r\n]*<code>(.+?)</code>[\s\t\r\n]*</pre>!mi,\
          '<pre \1class="brush\2>\3</pre>')
    else
      # <pre class="brush~><code>~</code></pre>を
      # <pre><code class="brush~>~</code></pre>に変更
      body = body.gsub(\
          %r!<pre[\s\t]+([^<>]*?)class[\s\t]*="brush(.+?)>[\s\t\r\n]*<code>(.+?)</code>[\s\t\r\n]*</pre>!mi,\
          '<pre><code \1class="brush\2>\3</code></pre>')
    end
  end
  #<pre>~</pre>内のTABを取り除く。タブ幅はtabwidthで与える
  body = body.split(/\n/).map {|line|
    if (/<pre/ === line) .. (/<\/pre>/ === line) 
      line.gsub(/\t/, "\s"*@option_hash[:tabwidth])
    else
      line
    end
  }.join "\n"
  str_h1 = nil
  # H1→消去 H2→H4 H3→H5 H4→H6に変更する
  if ( @option_hash[:blogmode] )
    #H1消去
    body = body.gsub(%r!<h1.*?>(.+?)</h1>!i, "")
    str_h1 = $1
    #H4→H6、H3→H5、H2→H4
    for i in [4,3,2]
      body = body.gsub(/<h#{i}(.+?)<\/h#{i}>/,"<h#{i+2}\\1<\/h#{i+2}>")
    end
  end
  return body, str_h1
end

def mkd2htmlbodyfile(inputname, outputname)
  sbody = nil
  File.open(inputname, "r") { |file_in|
    sbody, = mkd2htmlbody(file_in.read)
  }
  File.open(outputname, "w") { |file_ou|
    file_ou.puts sbody
  }
end

##### main ######
OptionParser.new{|opt|
  opt.on('-w VAL', '--tabwidth VAL', /[0-9]+/, "<pre>tag expand tabwidth(default:4)"){|v| @option_hash[:tabwidth] = v}
  opt.on('--[no-]blog', "blog mode(default:true)"){|v| @option_hash[:blogmode] = v}
  opt.parse!(ARGV)
}

mkd2htmlbodyfile(ARGV[0], ARGV[1])

参考
Ruby 1.9.2 リファレンスマニュアル
逆引きRuby
ujihisa/mdv - github(kramdownを使ったrubyコード)
<code></code>を除去したい場合

Syntax HighlighterのtagNameを変更したくない場合(対策案1)はmkd2html.rbの18行目をfalse→trueとすれば動くと思います。

生成されたタグをいじるのはなんとなく後ろめたい&将来的になんか困るかもしれないので、変換する/しないを切り替えられるコマンドラインオプションをつけてあります。

<pre>タグ内のTABをスペースに

ソースコードにTABが入ってるとコピペしたときにタブ幅の設定によってはインデントがずれてしまうので、スクリプト内で変換するようにしてみました。

rubyの「条件式としての範囲式」て便利ですね。githubから取ってきたサンプルコード見てて、「何これ?」と思ってたんですが、理解できるとちょっと感動。C++とかだと

void tab2space_inpre( vector<string> &lines. int tab_width )
{
    bool is_pre_flg = false;
    for ( vector<string>::iterator p = lines.begin(); p != lines.end(); p++ )
    {
        string& line = *p;
        if ( /*lineの中に<pre がみつかった*/ )
        {
            /*lineの中のTABをSPACEに変換*/
            if ( /*lineの中に</pre>が見つからなかった*/ ) is_pre_flg = true;
        }
        else if ( is_pre_flg )
        {
            /*lineの中のTABをSPACEに変換*/
            if ( /*lineの中に</pre>が見つかった*/ ) is_pre_flg = false;
        }
    }
}

みたいにどうしても状態遷移フラグが必要なんだけど、これが必要ないし、コードもすっきりする。

あとmkd2html.rbの33行目を

if (/<pre/ =~ line) .. (/<\/pre>/ =~ line) 

としても動くのが、最初理解できなかった。’RegExp =~ String’はマッチしたらマッチしたインデックス。マッチしなかったらnilを返すんですが、「先頭で見つかったら(0が返ってきたら)動かないじゃん」と思ってたけどrubyではif(0)if(true)と評価されるんですね( ・_・;)
早めに気付いてよかった。いつかどこかで大嵌まりするところでした。

見出しレベルの変換

mkd2html.rbの42行目~50行目で見出しレベルのシフトを行ってます。

eoblogでは

H1
ブログタイトル
H2
ブログ副題と日付
H3
記事タイトル

となっていて、ブログ内で使用するのはH4~が自然な感じです。ということでローカルで編集するMarkdownファイル自体は

eoblogをMarkdown記法で書く2(SyntaxHighlighter導入)
====
本文1

スクリプトの作成
----
本文2

### 見出しレベルの変換
本文3

みたいにH1から書いて、アップする際には、H1を除去、H2→H4、H3→H5とかって変換するようにしてみました。
今回は除去したH1ですが、HTMLプレビューの際には使用します。

必ず見出しレベルの変換すると将来的に困るかもしれないんで、一応コマンドライン引数オプションで変更できるようにしてあります。(デフォルトは変換)

使い方
$ ruby mkd2html [option] input_mkd_file output_html_file

こんな感じで実行します。
入力ファイルは規定の外部エンコーディングEncoding.default_externalのファイルを指定します。Windows XPなら’Windows-31J’(cp932、Shift-JIS)で書いたファイルを渡します。出力のHTMLファイルはUTF-8エンコーディングで出力されます。

長くなってきたんで、今日はこれで終了。 続く・・・と思う。


  1. 当ページではpreタグではなく、codeタグで装飾してるので、実際のHTMLソースは説明と異なります。

  2. あくまで見た目ね。既に対策済みなので、実際のHTMLソースは説明と異なります

2011/12/18

mkd2html.rb内mkd2htmlbody関数内のバグ修正

トラックバック

このページのトラックバックURL:
http://app.blog.eonet.jp/t/trackback/558347/27616337

eoblogをMarkdown記法で書く2(SyntaxHighlighter導入)を参照しているブログ:

コメント

コメントを投稿

最近のコメント