独自のレクサーの作成

お気に入りの言語のレクサーがPygmentsパッケージにない場合は、独自に作成してPygmentsを拡張することができます。

必要なものはすべて、pygments.lexer モジュールの中にあります。APIドキュメントで説明されているように、レクサーは、いくつかのキーワード引数(レクサーオプション)で初期化され、レックスするデータを含む文字列またはUnicodeオブジェクトを受け取るget_tokens_unprocessed()メソッドを提供するクラスです。

get_tokens_unprocessed()メソッドは、(index, token, value)形式のタプルを含むイテレータまたはイテラブルを返す必要があります。ほとんどの作業を行うベースレクサーがあり、それをサブクラス化できるので、通常はこれを行う必要はありません。

レクサーの追加方法

レクサーを追加するには、次の手順を実行します。

  • pygments/lexersの下で一致するモジュールを選択するか、レクサークラス用の新しいモジュールを作成します。

    注記

    非常に小さな既存のレクサーの派生物でない限り、レクサークラスを独自のモジュールに入れることをお勧めします。

  • 次に、モジュール外部からレクサーが認識されるようにします。pygments.lexersパッケージ内のすべてのモジュールは__all__を指定しています。たとえば、automation.pyは次のように設定されています。

    __all__ = ['AutohotkeyLexer', 'AutoItLexer']
    

    このリストにレクサークラスの名前を追加します(レクサーがモジュール内の唯一のクラスの場合は、リストを作成します)。

  • 最後に、レクサーマッピングを再構築することで、レクサーを公開できます。

    $ tox -e mapfiles
    

レクサーのテスト方法

新しいレクサーテストを追加するには、コードスニペットのみを含むファイルをtests/snippets/<lexer_alias>/下に作成します。次に、tox -- --update-goldens <filename.txt>を実行して、現在期待されるトークンを自動的に生成します。それらが適切であることを確認し、ファイルをコミットします。

レクサーテストは、他のすべてのテストと同様にtoxで実行されます。レクサーを開発している間は、tox -- tests/snippets/language-name/および/またはtox -- tests/examplefiles/language-name/を使用して、そのレクサーのテストのみを実行することもできます。

toxでテストスイートを実行すると、テスト入力でレクサーが実行され、出力が期待されるトークンと一致するかどうかがチェックされます。レクサーを改善している場合、トークン出力が変更されるのは普通のことです。テストの期待されるトークン出力を更新するには、もう一度tox -- --update-goldens <filename.txt>を使用します。変更を確認し、意図したとおりであることを確認してから、提案されたコード変更とともにコミットします。

大規模なテストファイルはtests/examplefilesに配置する必要があります。これはsnippetsと同様ですが、トークン出力は別のファイルに保存されます。--update-goldensを使用して出力を再生成することもできます。

注記

新しいレクサーをコントリビュートする際は、必ず例ファイルまたはテストスニペットを提供する必要があります。テストできないレクサーは受け入れられません。

RegexLexer

Pygmentsのほとんどのレクサーで使用されているレクサーベースクラスは、RegexLexerです。このクラスを使用すると、さまざまな状態に対する正規表現という観点からレキシングルールを定義できます。

状態とは、入力文字列の現在の位置に対して照合される正規表現のグループです。これらの表現の1つが一致した場合、対応するアクションが実行され(特定のタイプを持つトークンの生成や状態の変更など)、現在の位置は最後の照合が終了した位置に設定され、現在の状態の_最初の_正規表現で照合プロセスが続行されます。

注記

つまり、常に最初のエントリに戻ることになります。つまり、特定の順序で状態を照合することはできません。たとえば、次のルールを持つ状態は、意図したとおりには機能しません。

'state': [
    (r'\w+', Name,),
    (r'\s+', Whitespace,),
    (r'\w+', Keyword,)
]

上記の例では、Keywordは決して一致しません。特定のトークンタイプを順番に照合するには、下のbygroupsヘルパーを参照してください。

レクサー状態はスタックに保持されます。新しい状態が入力されるたびに、新しい状態がスタックにプッシュされます。最も基本的なレクサー(DiffLexerなど)は、1つの状態のみが必要です。

各状態は、(regexactionnew_state)形式のタプルのリストとして定義され、最後の項目は省略可能です。最も基本的な形式では、actionはトークンタイプ(Name.Builtinなど)です。つまり、regexが一致した場合、一致したテキストとタイプtokentypeを持つトークンを生成し、new_stateを状態スタックにプッシュします。新しい状態が'#pop'の場合、代わりに最上位の状態がスタックからポップされます。複数の状態をポップするには、'#pop:2'などを使用します。'#push'は、現在の状態をスタックの一番上に2回プッシュすることと同義です。

次の例は、ビルトインレクサーからのDiffLexerを示しています。ビルトインレクサーのルックアップ関数で使用されるnamealiasesfilenamesという追加属性が含まれていることに注意してください。レクサーには必須ではありません。

from pygments.lexer import RegexLexer
from pygments.token import *

class DiffLexer(RegexLexer):
    name = 'Diff'
    aliases = ['diff']
    filenames = ['*.diff']

    tokens = {
        'root': [
            (r' .*\n', Text),
            (r'\+.*\n', Generic.Inserted),
            (r'-.*\n', Generic.Deleted),
            (r'@.*\n', Generic.Subheading),
            (r'Index.*\n', Generic.Heading),
            (r'=.*\n', Generic.Heading),
            (r'.*\n', Text),
        ]
    }

このレクサーは1つの状態しか使用していないことがわかります。レクサーがテキストのスキャンを開始すると、最初に現在の文字がスペースかどうかを確認します。これがtrueの場合、改行まですべてをスキャンし、データをTextトークン(「特別な強調表示なし」トークン)として返します。

このルールが一致しない場合、現在の文字がプラス記号かどうかを確認します。そして同様です。

現在の位置でルールが一致しない場合、現在の文字はレキシングエラーを示すErrorトークンとして出力され、位置が1つ増加します。

レクサーの使用

新しいレクサーを使用する最も簡単な方法は、現在のディレクトリを基準としたファイルからレクサーをロードするためのPygmentsのサポートを使用することです。

まず、レクサークラスの名前をCustomLexerに変更します。

from pygments.lexer import RegexLexer
from pygments.token import *

class CustomLexer(RegexLexer):
    """All your lexer code goes here!"""

次に、追加フラグ-xを使用して、コマンドラインからレクサーをロードしてテストできます。

$ python -m pygments -x -l your_lexer_file.py <inputfile>

CustomLexer以外のクラス名を指定するには、コロンを付けて追加します。

$ python -m pygments -x -l your_lexer.py:SomeLexer <inputfile>

または、Python APIを使用します。

# For a lexer named CustomLexer
your_lexer = load_lexer_from_file(filename, **options)

# For a lexer named MyNewLexer
your_named_lexer = load_lexer_from_file(filename, "MyNewLexer", **options)

カスタムレクサーとフォーマッターをロードする際は、信頼できるファイルのみを使用することに細心の注意を払ってください。Pygmentsは、それらに対してevalと同等の処理を実行します。

Pygments APIでのみレクサーを使用する場合は、レクサーを自分でインポートしてインスタンス化し、pygments.highlight()に渡すことができます。

-fフラグを使用して、ターミナルエスケープシーケンス以外の出力形式を選択します。HtmlFormatterは、レクサーのデバッグに役立ちます。debug_token_typesオプションを使用して、入力ファイルの各部分に割り当てられたトークンタイプを表示できます。

$ python -m pygments -x -f html -Ofull,debug_token_types -l your_lexer.py:SomeLexer <inputfile>

各トークンにカーソルを合わせると、ツールチップとしてトークンタイプが表示されます。

レクサーが他の人にも役立つ場合は、Pygmentsにコントリビュートしていただけると幸いです。Pygmentsへのコントリビューションでアドバイスをご覧ください。

正規表現フラグ

正規表現フラグは、正規表現内でローカルに定義するか(r'(?x)foo bar')、レクサークラスにflags属性を追加してグローバルに定義することができます。属性が定義されていない場合、デフォルトはre.MULTILINEです。正規表現フラグの詳細については、Pythonドキュメントの正規表現に関するページを参照してください。

複数のトークンの同時スキャン

これまでは、正規表現、アクション、状態のルールタプル内のaction要素は単一のトークンタイプでした。ここでは、他のいくつかの可能な値の最初のものを見ていきます。

INIファイルを強調表示する、より複雑なレクサーを次に示します。INIファイルは、セクション、コメント、key = valueペアで構成されています。

from pygments.lexer import RegexLexer, bygroups
from pygments.token import *

class IniLexer(RegexLexer):
    name = 'INI'
    aliases = ['ini', 'cfg']
    filenames = ['*.ini', '*.cfg']

    tokens = {
        'root': [
            (r'\s+', Whitespace),
            (r';.*', Comment),
            (r'\[.*?\]$', Keyword),
            (r'(.*?)(\s*)(=)(\s*)(.*)',
             bygroups(Name.Attribute, Whitespace, Operator, Whitespace, String))
        ]
    }

レクサーは最初に、空白、コメント、セクション名を探します。次に、'='記号で区切られたキーと値のペアのように見える行と、オプションの空白を探します。

bygroupsヘルパーは、正規表現内の各キャプチャグループを異なるトークンタイプで生成します。最初にName.Attributeトークン、次にオプションの空白のTextトークン、次に等号のOperatorトークン。次に、再び空白のTextトークン。行の残りはStringとして返されます。

これが機能するためには、一致のすべての部分がキャプチャグループ((...))の中にあり、ネストされたキャプチャグループがあってはならないことに注意してください。それでもグループが必要な場合は、この構文を使用して非キャプチャグループを定義します。(?:some|words|here)(開始括弧の後の?:に注意してください)。

正規表現内で、出力には含めたくないが、後方参照のために使用するキャプチャグループが必要な場合(例:r'(<(foo|bar)>)(.*?)(</\2>)')、bygroups 関数に None を渡すと、そのグループは出力からスキップされます。

状態の変更

多くの字句解析器は、期待通りに動作するために複数の状態を必要とします。例えば、いくつかの言語では、複数行コメントをネストすることができます。これは再帰的なパターンなので、正規表現のみを使用して字句解析することは不可能です。

ここでは、C++スタイルのコメント(/* */ を用いた複数行コメントと、行末まで続く // を用いた単一行コメント)を認識する字句解析器を示します。

from pygments.lexer import RegexLexer
from pygments.token import *

class CppCommentLexer(RegexLexer):
    name = 'Example Lexer with states'

    tokens = {
        'root': [
            (r'[^/]+', Text),
            (r'/\*', Comment.Multiline, 'comment'),
            (r'//.*?$', Comment.Singleline),
            (r'/', Text)
        ],
        'comment': [
            (r'[^*/]+', Comment.Multiline),
            (r'/\*', Comment.Multiline, '#push'),
            (r'\*/', Comment.Multiline, '#pop'),
            (r'[*/]', Comment.Multiline)
        ]
    }

この字句解析器は、'root' 状態から字句解析を開始します。スラッシュ('/')が見つかるまで、可能な限り多くの文字列を一致させようとします。スラッシュの後の次の文字がアスタリスク('*')の場合、RegexLexer はその2文字を出力ストリームに Comment.Multiline としてマークし、'comment' 状態で定義されたルールを使用して字句解析を続行します。

スラッシュの後にアスタリスクがなかった場合、RegexLexer はそれが単一行コメント(つまり、2番目のスラッシュが続く)かどうかを確認します。これも当てはまらない場合は、単なるスラッシュであり、コメントの開始文字ではありません(単一のスラッシュに対する個別の正規表現も与えなければなりません。さもなければ、スラッシュはエラートークンとしてマークされます)。

'comment' 状態内では、同じことを繰り返します。字句解析器がアスタリスクまたはスラッシュを見つけるまでスキャンします。複数行コメントの開始であれば、'comment' 状態をスタックにプッシュし、'comment' 状態でスキャンを続行します。そうでなければ、それが複数行コメントの終わりかどうかを確認します。もしそうであれば、スタックから1つの状態をポップします。

注:空のスタックからポップしようとすると、IndexError が発生します。(これを防ぐ簡単な方法があります:'root' 状態では '#pop' しないでください)。

RegexLexer がエラートークンとしてフラグ付けされた改行文字に遭遇した場合、スタックは空になり、字句解析器は'root' 状態でスキャンを続行します。これは、誤った入力(例:単一行文字列が閉じられていない場合)に対するエラー耐性のある強調表示を作成するのに役立ちます。

高度な状態のテクニック

状態に対して行えることが他にもいくつかあります。

  • ルールのタプルの3番目の要素として単純な文字列ではなくタプルを与えることで、複数の状態をスタックにプッシュできます。例えば、ディレクティブを含むコメントを一致させたい場合、

    /* <processing directive>    rest of comment */
    

    このルールを使用できます。

    tokens = {
        'root': [
            (r'/\* <', Comment, ('comment', 'directive')),
            ...
        ],
        'directive': [
            (r'[^>]+', Comment.Directive),
            (r'>', Comment, '#pop'),
        ],
        'comment': [
            (r'[^*]+', Comment),
            (r'\*/', Comment, '#pop'),
            (r'\*', Comment),
        ]
    }
    

    これが上記のサンプルに遭遇すると、まず'comment''directive' がスタックにプッシュされ、その後、字句解析器は閉じ括弧の > が見つかるまでディレクティブ状態を続行し、次に閉じ括弧の */ が見つかるまでコメント状態を続行します。その後、両方の状態がスタックからポップされ、ルート状態での字句解析が再開されます。

    バージョン 0.9 で追加: タプルには、特別な'#push''#pop' (ただし '#pop:n' は除く)ディレクティブを含めることができます。

  • pygments.lexerinclude を使用することで、ある状態のルールを別の状態の定義に含めることができます。

    from pygments.lexer import RegexLexer, bygroups, include
    from pygments.token import *
    
    class ExampleLexer(RegexLexer):
        tokens = {
            'comments': [
                (r'(?s)/\*.*?\*/', Comment),
                (r'//.*?\n', Comment),
            ],
            'root': [
                include('comments'),
                (r'(function)( )(\w+)( )({)',
                 bygroups(Keyword, Whitespace, Name, Whitespace, Punctuation), 'function'),
                (r'.*\n', Text),
            ],
            'function': [
                (r'[^}/]+', Text),
                include('comments'),
                (r'/', Text),
                (r'\}', Punctuation, '#pop'),
            ]
        }
    

    これは、関数とコメントからなる言語の仮説的な字句解析器です。コメントはトップレベルと関数内の両方で発生する可能性があるため、両方の状態でのコメントのルールが必要です。include ヘルパーを使用すると、複数回出現するルール(この例では、'comment' 状態は、'root''function' にのみ含まれるため、字句解析器によって入力されることはありません)の繰り返しを回避できます。

  • 場合によっては、既存の状態から状態を「組み合わせる」ことが必要な場合があります。pygments.lexercombined ヘルパーを使用すると可能です。

    新しい状態の代わりに、ルールのタプルの3番目の要素として combined('state1', 'state2') を記述すると、state1とstate2から新しい匿名状態が形成され、ルールが一致した場合、字句解析器はその状態に入ります。

    これはあまり頻繁に使用されるものではありませんが、PythonLexer の文字列リテラル処理など、場合によっては役立ちます。

  • 字句解析器を異なる状態から開始させたい場合は、get_tokens_unprocessed() メソッドをオーバーライドすることでスタックを変更できます。

    from pygments.lexer import RegexLexer
    
    class ExampleLexer(RegexLexer):
        tokens = {...}
    
        def get_tokens_unprocessed(self, text, stack=('root', 'otherstate')):
            for item in RegexLexer.get_tokens_unprocessed(self, text, stack):
                yield item
    

    PhpLexer のような一部の字句解析器では、先頭の <?php プリプロセッサコメントを省略可能にするためにこれを使用します。トークンマップに存在しない値をスタックに入れると、字句解析器が簡単にクラッシュする可能性があることに注意してください。また、スタックから 'root' を削除すると、奇妙なエラーが発生する可能性があります!

  • 一部の字句解析器では、状態内のルールと一致しないものが検出されると、状態をポップする必要があります。状態リストの最後に空の正規表現を使用できますが、Pygmentsはより分かりやすい方法を提供しています:default('#pop')('', Text, '#pop') と同等です。

    バージョン 2.0 で追加。

RegexLexer から派生した字句解析器のサブクラス化

バージョン 1.6 で追加。

複数の言語が非常に似ている場合がありますが、それでも異なる字句解析器クラスで字句解析する必要があります。

RegexLexer から派生した字句解析器のサブクラスを作成する場合、親クラスと子クラスで定義された tokens ディクショナリはマージされます。例えば

from pygments.lexer import RegexLexer, inherit
from pygments.token import *

class BaseLexer(RegexLexer):
    tokens = {
        'root': [
            ('[a-z]+', Name),
            (r'/\*', Comment, 'comment'),
            ('"', String, 'string'),
            (r'\s+', Whitespace),
        ],
        'string': [
            ('[^"]+', String),
            ('"', String, '#pop'),
        ],
        'comment': [
            ...
        ],
    }

class DerivedLexer(BaseLexer):
    tokens = {
        'root': [
            ('[0-9]+', Number),
            inherit,
        ],
        'string': [
            (r'[^"\\]+', String),
            (r'\\.', String.Escape),
            ('"', String, '#pop'),
        ],
    }

BaseLexer は、名前と文字列を字句解析する2つの状態を定義します。DerivedLexer は独自のトークンディクショナリを定義し、基本字句解析器の定義を拡張します。

  • 「root」状態には追加のルールと、親クラスのトークン定義をその時点で挿入するようにPygmentsに指示する特別なオブジェクト inherit があります。

  • 「string」状態は、inherit ルールがないため、完全に置き換えられます。

  • 「comment」状態は完全に継承されます。

複数の字句解析器の使用

同じ入力に対して複数の字句解析器を使用するのは難しい場合があります。最も簡単な組み合わせ手法の1つを以下に示します。ルールのタプル内のアクションエントリを字句解析器クラスで置き換えることができます。一致したテキストは、その字句解析器で字句解析され、結果のトークンが生成されます。

例えば、この簡略化されたHTML字句解析器を見てください。

from pygments.lexer import RegexLexer, bygroups, using
from pygments.token import *
from pygments.lexers.javascript import JavascriptLexer

class HtmlLexer(RegexLexer):
    name = 'HTML'
    aliases = ['html']
    filenames = ['*.html', '*.htm']

    flags = re.IGNORECASE | re.DOTALL
    tokens = {
        'root': [
            ('[^<&]+', Text),
            ('&.*?;', Name.Entity),
            (r'<\s*script\s*', Name.Tag, ('script-content', 'tag')),
            (r'<\s*[a-zA-Z0-9:]+', Name.Tag, 'tag'),
            (r'<\s*/\s*[a-zA-Z0-9:]+\s*>', Name.Tag),
        ],
        'script-content': [
            (r'(.+?)(<\s*/\s*script\s*>)',
             bygroups(using(JavascriptLexer), Name.Tag),
             '#pop'),
        ]
    }

ここでは、<script> タグの内容は、新しく作成された JavascriptLexer のインスタンスに渡され、HtmlLexer によって処理されません。これは、他の字句解析器クラスをパラメーターとして取る using ヘルパーを使用して行われます。

bygroupsusing の組み合わせに注意してください。これにより、</script> 終了タグまでの内容は JavascriptLexer によって処理され、終了タグは Name.Tag タイプの通常のトークンとして生成されます。

また、(r'<\s*script\s*', Name.Tag, ('script-content', 'tag')) ルールにも注意してください。ここでは、'script-content''tag' の2つの状態が状態スタックにプッシュされます。つまり、まず 'tag' が処理され、属性と閉じ括弧の > が字句解析され、次に 'tag' 状態がポップされ、スタックの一番上の次の状態は 'script-content' になります。

現在定義されているクラスを参照することはできないため、現在の字句解析器クラスを参照するには、pygments.lexer からインポートした this を使用します(例:using(this))。この構成は不要に見えるかもしれませんが、これは多くの場合、深くネストされた状態を導入せずに、固定の区切り文字間の任意の構文を字句解析する最も分かりやすい方法です。

using() ヘルパーには、特別なキーワード引数 state があります。これは、指定された場合、最初に使用する字句解析器は "root" 状態ではなく、この引数で指定された状態にあることを意味します。これは、ExtendedRegexLexer (下記参照)などの高度な RegexLexer のサブクラスでは機能しません。

using() に渡されたその他のキーワード引数は、字句解析器の作成に使用されるキーワード引数に追加されます。

委任字句解析器

ネストされた字句解析器に対するもう1つのアプローチは、テンプレートエンジンの字句解析器で使用されている DelegatingLexer です。初期化時に2つの字句解析器、root_lexerlanguage_lexer を引数として取ります。

入力は次のように処理されます。まず、テキスト全体が language_lexer で字句解析されます。特別なタイプ Other で生成されたすべてのトークンは連結され、root_lexer に渡されます。次に、language_lexer の言語トークンが、適切な位置に root_lexer のトークンストリームに挿入されます。

from pygments.lexer import DelegatingLexer
from pygments.lexers.web import HtmlLexer, PhpLexer

class HtmlPhpLexer(DelegatingLexer):
    def __init__(self, **options):
        super().__init__(HtmlLexer, PhpLexer, **options)

この手順により、テンプレートタグがHTMLタグや属性内に含まれていても、テンプレートタグを含むHTMLなどが正しくハイライト表示されます。

ニードルトークンOtherを別のものに変更したい場合は、レクサーに別のトークンタイプを3番目のパラメーターとして渡すことができます。

DelegatingLexer.__init__(MyLexer, OtherLexer, Text, **options)

コールバック

言語の文法が非常に複雑な場合、レクサーは正規表現とスタックのみを使用して処理することができません。

そのため、RegexLexerでは、トークンタイプではなく、ルールタプルにコールバックを指定できます(bygroupsusingは、事前に実装されたコールバックに過ぎません)。コールバックは、2つの引数をとる関数である必要があります。

  • レクサー自体

  • 最後に一致したルールのマッチオブジェクト

コールバックは、(index, tokentype, value)タプルのイテラブルを返す(または単にyieldする)必要があります。これらはget_tokens_unprocessed()によってそのまま渡されます。indexは入力文字列におけるトークンの位置を表し、tokentypeは通常のトークンタイプ(Name.Builtinなど)を表し、valueは関連する入力文字列の部分を表します。

例はこちらをご覧ください。

from pygments.lexer import RegexLexer
from pygments.token import Generic

class HypotheticLexer(RegexLexer):

    def headline_callback(lexer, match):
        equal_signs = match.group(1)
        text = match.group(2)
        yield match.start(), Generic.Headline, equal_signs + text + equal_signs

    tokens = {
        'root': [
            (r'(=+)(.*?)(\1)', headline_callback)
        ]
    }

headline_callbackの正規表現が一致すると、マッチオブジェクトを使用して関数が呼び出されます。コールバックが完了した後、処理は通常どおり継続されることに注意してください。つまり、前のマッチの終了後です。コールバックは位置に影響を与えることはできません。

レクサーコールバックの簡単な例はほとんどありませんが、ml.pySMLLexerクラスなどで実際に動作している様子を確認できます。

ExtendedRegexLexerクラス

コールバックを使用してもRegexLexerは、Rubyなどの言語の複雑な構文規則には不十分です。

しかし、心配はいりません。それでも正規表現アプローチを放棄する必要はありません。PygmentsにはRegexLexerのサブクラスであるExtendedRegexLexerがあります。RegexLexerで知られているすべての機能もここで利用でき、トークンはまったく同じ方法で指定されます。ただし、1つの詳細を除いて。

get_tokens_unprocessed()メソッドは、内部状態データをローカル変数としてではなく、pygments.lexer.LexerContextクラスのインスタンスとして保持し、そのインスタンスは3番目の引数としてコールバックに渡されます。つまり、コールバックでレクサーの状態を変更できます。

LexerContextクラスには、次のメンバーがあります。

  • text – 入力テキスト

  • pos – 正規表現の一致に使用される現在の開始位置

  • stack – 状態スタックを含むリスト

  • end – 正規表現が一致する最大位置。これはtextの長さにデフォルト設定されます。

さらに、get_tokens_unprocessed()メソッドには文字列の代わりにLexerContextを渡すことができ、文字列引数に対して新しいコンテキストを作成する代わりに、このコンテキストを処理します。

コールバックで現在の位置を任意の位置に設定できるため、コールバックの終了後に呼び出し元によって自動的に設定されることはないことに注意してください。たとえば、上記の仮想レクサーはExtendedRegexLexerを使用して次のように記述されます。

from pygments.lexer import ExtendedRegexLexer
from pygments.token import Generic

class ExHypotheticLexer(ExtendedRegexLexer):

    def headline_callback(lexer, match, ctx):
        equal_signs = match.group(1)
        text = match.group(2)
        yield match.start(), Generic.Headline, equal_signs + text + equal_signs
        ctx.pos = match.end()

    tokens = {
        'root': [
            (r'(=+)(.*?)(\1)', headline_callback)
        ]
    }

これは混乱するかもしれませんが(実際、混乱する可能性があります)。しかし、これは必要であり、例についてはruby.pyのRubyレクサーを参照してください。

キーワードリストの処理

比較的短いリスト(数百個)の場合、words()を使用して最適化された正規表現を直接構築できます(長いリストについては、次のセクションを参照)。この関数は、メタ文字のエスケープやPythonの代替における最長一致ではなく最初の一致など、いくつかの処理を自動的に行います。リスト自体をpygments/lexers/_$lang_builtins.pyに配置し(例を参照)、可能であればコードで生成してください。

words()の使用例を以下に示します。

from pygments.lexer import RegexLexer, words, Name

class MyLexer(RegexLexer):

    tokens = {
        'root': [
            (words(('else', 'elseif'), suffix=r'\b'), Name.Builtin),
            (r'\w+', Name),
        ],
    }

ご覧のとおり、構築された正規表現にprefixsuffixの部分を追加できます。

トークンストリームの変更

一部の言語には、多くの組み込み関数が付属しています(たとえばPHP)。これらの関数の総数はシステムによって異なります。これは、誰もがすべての拡張機能をインストールしているわけではないためです。PHPの場合、3000を超える組み込み関数があります。これは非常に多くの関数であり、正規表現に含めるには多すぎます。

しかし、関数名はNameトークンのみであるため、get_tokens_unprocessed()メソッドをオーバーライドすることで解決できます。次のレクサーはPythonLexerをサブクラス化して、追加の名前を擬似キーワードとしてハイライト表示します。

from pygments.lexers.python import PythonLexer
from pygments.token import Name, Keyword

class MyPythonLexer(PythonLexer):
    EXTRA_KEYWORDS = set(('foo', 'bar', 'foobar', 'barfoo', 'spam', 'eggs'))

    def get_tokens_unprocessed(self, text):
        for index, token, value in PythonLexer.get_tokens_unprocessed(self, text):
            if token is Name and value in self.EXTRA_KEYWORDS:
                yield index, Keyword.Pseudo, value
            else:
                yield index, token, value

PhpLexerLuaLexerは、このメソッドを使用して組み込み関数を解決します。

一般的な落とし穴とベストプラクティス

正規表現はPygmentsレクサーで広く使用されています。このセクションでは、正規表現を使用する際に起こりうる一般的な間違いについて警告するために記述されています。レクサーの可読性とレビューのしやすさを向上させるためのヒントもいくつかあります。新しいレクサーに貢献したい場合は、このセクションを読むように求められますが、いずれの場合でも役立つ可能性があります。

  • ルールを作成する際には、単純なルールをマージしてみてください。たとえば、

    (r"\(", token.Punctuation),
    (r"\)", token.Punctuation),
    (r"\[", token.Punctuation),
    (r"\]", token.Punctuation),
    ("{", token.Punctuation),
    ("}", token.Punctuation),
    

    を次のように結合します。

    (r"[\(\)\[\]{}]", token.Punctuation)
    
  • .*に注意してください。これは、可能な限り貪欲に一致させます。たとえば、@.*@のようなルールは、@first@ second @third@という文字列全体に一致し、@first@@third@に一致しません。@.*?@を使用すると、早期に停止できます。?は、可能な限り少ない回数一致させようとします。

  • いわゆる「カタストロフィックバックトラッキング」に注意してください。最初の例として、正規表現(A+)*Bを考えてみましょう。これはA*Bと一致する点で同等ですが、一致には非常に時間がかかります。これは、正規表現エンジンの動作方法によるものです。50個の「A」と末尾に「C」を入力したとします。最初にA+で「A」を貪欲に一致させますが、「B」と「C」は異なるため、終了と一致できないことがわかります。次にバックトラックし、最初のA+から1つの「A」を削除し、残りを別の(A+)*と一致させようとします。これも失敗するため、入力文字列のさらに左側にバックトラックします。つまり、すべての組み合わせを試行します。

    (AAAAAAAAAAAAAAAAA)
    (AAAAAAAAAAAAAAAA)(A)
    (AAAAAAAAAAAAAAA)(AA)
    (AAAAAAAAAAAAAAA)(A)(A)
    (AAAAAAAAAAAAAA)(AAA)
    (AAAAAAAAAAAAAA)(AA)(A)
    ...
    

    したがって、一致は指数関数的な複雑さになります。レクサーでは、無効な入力を解析するときにPygmentsがハングするようになります。

    >>> import re
    >>> re.match('(A+)*B', 'A'*50 + 'C') # hangs
    

    より微妙で現実的な例として、文字列に一致させるために不適切に記述された正規表現を次に示します。

    r'"(\\?.)*?"'
    

    終了引用符がない場合、正規表現エンジンは末尾と一致できないことを検出し、*?で一致の回数を少なくしてバックトラックしようとします。バックスラッシュが見つかった場合、\\.の可能性を試したため、.を試行します(意味のない単純な文字として認識します)。これは、(無効な)入力文字列に多くのバックスラッシュがある場合、同じ指数関数的なバックトラッキングの問題につながります。これを記述する良い方法はr'"([^\\]|\\.)*?"'で、内部グループは1つの方法でしか一致できません。さらに良いのは、専用のステートを使用することで、頭痛の種なく問題を回避できるだけでなく、文字列エスケープをハイライト表示することもできます。

    'root': [
        ...,
        (r'"', String, 'string'),
        ...
    ],
    'string': [
        (r'\\.', String.Escape),
        (r'"', String, '#pop'),
        (r'[^\\"]+', String),
    ]
    
  • コメントや文字列などのパターンに対するルールを作成する際には、各トークンで可能な限り多くの文字に一致させます。これは、しない方が良い例です。

    'comment': [
        (r'\*/', Comment.Multiline, '#pop'),
        (r'.', Comment.Multiline),
    ]
    

    これは、コメント内の1文字ごとに1つのトークンを生成するため、レキシングプロセスが遅くなり、生のトークン出力(特にテスト出力)の読み取りが困難になります。代わりにこれを行ってください。

    'comment': [
        (r'\*/', Comment.Multiline, '#pop'),
        (r'[^*]+', Comment.Multiline),
        (r'\*', Comment.Multiline),
    ]