読者です 読者をやめる 読者になる 読者になる

SilverCityで構文のカラーリングを行う(下)

Python

 SilverCityの字句解析器(LexerModule)を使えば、標準HTMLジェネレーターが対応していない言語の解析を行ったり、独自のHTML出力を行うことができます。

字句解析器を生成する

 find_lexer_module_by_name、またはfind_lexer_module_by_idを使ってLexerを生成します。
 find_lexer_module_by_idに与えるint型のidは、SilverCity.ScintillaConstantsに定義されていますが、Pythonから使う場合は、find_lexer_module_by_nameを使って、文字列で字句解析器を指定した方が楽でしょう。例えば、次のように使います。

>>> from SilverCity import find_lexer_module_by_name
>>> find_lexer_module_by_name('python')
<LexerModule object for "python" at 0x50d3e0>


 言語名は小文字で指定しなくてはならないので、注意が必要です。バージョン0.96で有効な言語名を列挙しておきます。

ada, asm, asp, batch, bullant, clw, clwnocase, conf,
cpp, cppnocase, css, eiffel, eiffelkw, escript, f77,
forth, fortran, html, lisp, lout, lua, matlab, metapost,
mmixal, nncrontab, nsis,pascal, perl, php, pov,
powerbasic, ps, python, ruby, sql, tcl, tex, vb,
vbscript, xml, yaml

PropertySetを設定する

 字句解析器を生成したならば、tokenize_by_styleメソッドを使って、対象の文書をトークンに分割します。

tokens = lexer.tokenize_by_style(text, wordlist, propertyset)

 上の例の中の、第二引数のwordlistが、後述するWordListオブジェクトのシーケンス、第三引数がpropertysetがPropertySetオブジェクトです。
 PropertySetを使うと、字句解析のカスタマイズができるようですが、今回は詳しく調査することはしませんでした。
 PropertySetの設定を省略する場合は、コンストラクタ引数を空にして、PropertySetを生成し、tokenize_by_styleメソッドに渡します。

tokens = lexer.tokenize_by_style(text, wordlist, PropertySet())

WordListを設定する

 tokenize_by_styleの第二引数に与えるシーケンスは、中身がWordListのインスタンス、長さがLexerModuleのget_number_of_wordlistsメソッドが返す数と同じでなくてはなりません。

>>> lexer = find_lexer_module_by_name('python')
>>> lexer.get_number_of_wordlists()
2


 [WordList(), WordList()]のように、空のWordListを使っても、字句解析を行うこともできますが、この場合、予約語と非予約語を区別できません。
 これを避けるためには、WordListのコンストラクタに、予約語・キーワードをスペースで区切った文字列を渡します。
 例えば、CSSの字句解析を行う例は以下のようになります。

# -*- coding: utf-8 -*-
from cStringIO import StringIO
from SilverCity import Keywords, PropertySet, WordList
from SilverCity import find_lexer_module_by_name as find_lexer

css1 = Keywords.css_keywords
pseudo_classes = Keywords.css_keywords_2

# TODO CSS2プロパティを入れる
css2 = ""

# TODO CSS3プロパティを入れる
css3 = ""

wordlists = (
    WordList(css1),
    WordList(pseudo_classes),
    WordList(css2 + css3)
)

lexer = find_lexer('css')
tokens = lexer.tokenize_by_style(
            file('style.css').read(),
            wordlists,
            PropertySet()
         )

 WordListに与える予約語・キーワードは、字句解析の結果に影響を与えます。上の例では、結果のトークンにおいて、CSS1のプロパティとCSS2のプロパティは区別されることになります。
 逆に、CSS1, CSS2を区別したくない場合は、wordlistsシーケンスを次のように書き換えます。

wordlists = (
    WordList(css1 + css2 + css3),
    WordList(pseudo_classes),
    WordList()
)


 それぞれのLexerModuleが期待している予約語の一覧は、LexerModuleのget_wordlist_descriptionsメソッドを使って確認できます。

>>> lexer = find_lexer_module_by_name("css")
>>> lexer.get_wordlist_descriptions()
('CSS1 Keywords', 'Pseudo classes', 'CSS2 Keywords')


 SilverCity.Keywordsに、いくつかの言語のキーワードが定義されているので、これを使えば自分で予約語・キーワードを書き出す必要はありません。

cpp_keywords, css_keywords, doxygen_keywords
hypertext_keywords, perl_keywords, php_keywords
python_keywords, ruby_keywords, sgml_keywords
sql_keywords, vxml_keywords, xslt_keywords, yaml_keywords

HTMLに変換する

 これで字句解析器を作り、トークン分割まで行うことができました。これでようやく、構文カラーリングを施したHTMLへ変換することができます。
 変換の処理自体は難しくありませんが、変換にあたって、SilverCity.ScintillaConstantsに定義されている
各言語のスタイルIDを、CSSクラス名に対応付けるための変換テーブルが必要になります。これを準備するのが非常に手間がかかります。

from xml.sax.saxutils import escape

# 変換テーブル(省略)
STYLE_TABLE = {
    'python' : {},
    'php'    : {}
}

def generate_html(tokens, lang):
    style = STYLE_TABLE.get(lang, {})
    buf = []
    for t in tokens:
        text = escape(t['text'])
        s = t['style']
        if s in style:
            buf.extend(('<span class="%s">' % style[s],
                        text,
                        '</span>')
                      )
        else:
            buf.append(text)
    return ''.join(buf)

まとめ

 「SilverCity標準のHTMLジェネレーターを使わなくても、ソースコードをHTMLに変換するのは簡単」と書きましたが、こうやって一通り手続きを追っていくと、最初に思ったより手間がかかりますね。
 今回のエントリは、SilverCityによる自作HTML変換プログラムでカラーリングを行ってみました。SilverCityを調べ始めたのも、これが目的。
 本当は、はてなダイアリーでも、はてなグループスーパーpre記法 シンタックス・ハイライトが使えればいいのですが・・・