Hlavní navigace

Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů: vlastní filtry a lexery

26. 7. 2018
Doba čtení: 35 minut

Sdílet

Ve druhé části článku o knihovně Pygments si řekneme, jak navrhnout vlastní filtry určené pro zpracování proudu tokenů. Dále si ukážeme vytvoření nových lexerů, popř. úprav lexerů, které jsou již v Pygments implementovány.

Obsah

1. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů: vlastní filtry a lexery

2. Proces zpracování zdrojového kódu třídami z knihovny Pygments

3. Konstrukce jednoduchého filtru pro zpracování funkcí používajících CamelCase

4. První varianta filtru pro zpracování funkcí používajících CamelCase

5. Zařazení filtru do „kolony“ zpracování tokenů

6. Použití dekorátoru @simplefilter

7. Nepatrné vylepšení předchozího příkladu

8. Korektnější varianta filtru – test typu a hodnoty následujícího tokenu

9. Upravený kód filtru a jeho výsledky

10. Zpracování posledního tokenu v proudu

11. Konfigurovatelný filtr

12. Třída s implementací vlastního lexeru

13. Lexer se dvěma stavy – rozpoznání definice funkce

14. Výsledek zvýraznění syntaxe a úplný kód demonstračního příkladu s lexerem se dvěma stavy

15. Lexer se třemi stavy

16. Výsledek zvýraznění syntaxe a úplný kód demonstračního příkladu s lexerem se třemi stavy

17. Použití klauzule bygroups aneb lexer pro zpracování INI souborů

18. Příklad pro třetí část článku – lexer pro klasický jazyk BASIC

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů: vlastní filtry a lexery

V prvním článku věnovaném knihovně Pygments jsme se seznámili s tím, k čemu vlastně tato knihovna slouží a jak ji lze využít v praxi (obarvování zdrojových kódů, úpravy textu na úrovni tokenů atd.). Také jsme si ve stručnosti popsali celý způsob zpracování – od vstupních zdrojových textů přes proud tokenů až po výsledný dokument, popř. obarvený text zobrazený na ploše terminálu. V závěru článku byly popsány i některé standardní filtry, které jsou již v této knihovně implementovány a je možné je použít – NameHighlightFilter, VisibleWhitespaceFilter a KeywordCaseFilter.

Obrázek 1: Obarvení zdrojového kódu na terminálu rozpoznávajícího minimálně 256 barev. Barva pozadí je nastavena na černou.

Dnes se již budeme věnovat poněkud složitější problematice. Nejdříve si totiž ukážeme, jak je možné navrhnout vlastní filtry, které budou sloužit pro zpracování proudu tokenů, tj. k modifikaci tokenů, jejich mazání či naopak přidávání dalších vlastních tokenů do proudu. Posléze se seznámíme se způsobem vytvoření nových lexerů a samozřejmě také s možností upravit si stávající lexery (ty dnes podporují cca 300 programovacích jazyků, značkovacích jazyků, konfiguračních souborů aj.). Samozřejmě nesmíme zapomenout ani na poslední část celého řetězce zpracování, takže se ve třetím článku zmíníme o možnostech při psaní formátovačů. Díky tomu, jak knihovna Pygments pracuje, je vytvoření nového formátovače relativně snadné a přímočaré (navíc se nový formátovač automaticky stane dostupným pro všech již zmíněných cca 300 podporovaných jazyků).

Obrázek 2: Změna stylu zobrazení při použití terminálu s možností práce s 256 barvami.

2. Proces zpracování zdrojového kódu třídami z knihovny Pygments

Před popisem dalších možností nabízených knihovnou Pygments si jen ve stručnosti připomeňme, jakým způsobem vlastně probíhá celý proces zpracování od vstupního zdrojového kódu po výsledný dokument nebo text zobrazený na terminálu:

  1. Na začátku zpracování se nachází takzvaný lexer, který postupně načítá jednotlivé znaky ze vstupního řetězce (resp. ze souboru) a vytváří z nich lexikální tokeny (zkráceně jen tokeny). Pro každý podporovaný jazyk se používá jiný lexer a samozřejmě je možné v případě potřeby si napsat lexer vlastní. Již minule jsme se seznámili se dvěma standardními lexery – pygments.lexers.PythonLexer a pygments.lexers.pascal.DelphiLexer. První z nich je určený pro vytvoření tokenizovaného kódu Pythonu, druhý se používá pro Delphi, ObjectPascal i „obyčejný“ Pascal.
  2. Výstup produkovaný lexerem může procházet libovolným počtem filtrů sloužících pro odstranění nebo (mnohem častěji) modifikaci jednotlivých tokenů; ať již jejich typů či přímo textu, který tvoří hodnotu tokenu. Díky existenci filtrů je například možné nechat si zvýraznit vybrané bílé znaky, slova se speciálním významem v komentářích (typicky „TODO:“, „FIX:“) apod. Některé standardní filtry dodávané společně s knihovnou Pygments již známe: pygments.filters.NameHighlightFilter, pygments.filters.VisibleW­hitespaceFilter, pygments.filters.CodeTagFilter a pygments.filters.KeywordCaseFilter.
  3. Za filtry (pokud jsou samozřejmě použity) se nachází formátovač (formatter), který postupně načítá jednotlivé tokeny a převádí je do výstupního formátu. K dispozici je několik standardních formátovačů zajišťujících například tisk na terminál, výstup do HTML, LaTeXu, RTF, SVG apod. S některými formátovači jsme se opět seznámili minule. Patří mezi ně pygments.formatters.TerminalFormatter (s variantami pygments.formatters.Termi­nal256Formatter a pygments.formatters.Termi­nalTrueColorFormatter), pygments.formatters.HtmlFormatter, pygments.formatters.LaTexFormatter, pygments.formatters.SvgFormatter a pro ladění užitečný pygments.formatters.RawTo­kenFormatter. Další formátovače závisí na externích knihovnách; typicky na knihovně PIL/Pillow.

Obrázek 3: Příklad konfigurace celé „kolony“ určené pro zpracování zdrojových kódů ve čtyřech programovacích jazycích.

3. Konstrukce jednoduchého filtru pro zpracování funkcí používajících CamelCase

Podívejme se nyní na konstrukci jednoduchého filtru, který bude určen pro zpracování jmen funkcí používajících CamelCase, tj. způsob pojmenování, v němž jsou jednotlivá slova oddělena pouze velikostí znaků (verzálky/mínusky). Filtr bude takové názvy přejmenovávat do formy známé pod označením snake_case, v níž jsou jednotlivá slova od sebe oddělena podtržítkem. Toto přejmenování je možné pro jakýkoli řetězec implementovat pomocí dvou regulárních výrazů doplněných o metodu lower().

První regulární výraz zajistí oddělení poslední verzálky ze sekvence verzálek:

thisIsFOOBARFunction → this_IsFOOBAR_Function

Druhý regulární výraz vloží mezi jednotlivá slova podtržítka:

this_IsFOOBAR_Function → this_Is_FOOBAR_Function

Poslední řádek již jen zajistí převod celého řetězce na mínusky:

this_Is_FOOBAR_Function → this_is_foobar_function

Implementovaná konverzní funkce:

def convert(self, name):
    results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', results)
    return results.lower()

Vlastní filtr je tvořen třídou odvozenou od třídy Filter. Nutné je reimplementovat metodu filter, které je (po jejím automatickém zavolání) předán objekt představující lexer a generátor představovaný proudem tokenů. Metoda filter bude taktéž implementována formou generátoru, který bude postupně vracet dvojice typ-tokenu + hodnota-tokenu. Filtr, který bude všechny tokeny pouze předávat pro další zpracování, tedy může vypadat následovně::

class NopFilter(Filter):
 
    def __init__(self, **options):
        Filter.__init__(self, **options)
 
    def filter(self, lexer, stream):
        for ttype, value in stream:
            yield ttype, value

Náš filtr bude muset být implementován nepatrně složitěji – pro tokeny typu Name.Function a Name (to není přesné, ale prozatím si s touto implementací vystačíme) se změní jejich hodnota (řetězec) s využitím výše popsané metody convert:

def filter(self, lexer, stream):
    for ttype, value in stream:
        if ttype is Name.Function or ttype is Name:
            value = self.convert(value)
        yield ttype, value

4. První varianta filtru pro zpracování funkcí používajících CamelCase

První – doposud značně neúplná – varianta našeho filtru by mohla být implementována následujícím způsobem:

class CamelCaseFilter(Filter):
 
    def __init__(self, **options):
        Filter.__init__(self, **options)
 
    def convert(self, name):
        results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
        results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', results)
        return results.lower()
 
    def filter(self, lexer, stream):
        for ttype, value in stream:
            if ttype is Name.Function or ttype is Name:
                value = self.convert(value)
            yield ttype, value
Poznámka: ve skutečnosti bude filtr konvertovat hodnoty všech tokenů typu Name, což ovšem zahrnuje například i názvy proměnných. Později si ukážeme jedno z možných řešení tohoto problému.

5. Zařazení filtru do „kolony“ zpracování tokenů

Náš nově vytvořený filtr se nyní pokusíme přidat do „kolony“ určené pro zpracování tokenů. Klasický způsob zpracování (bez filtru) vypadá takto:

print(highlight(code, PythonLexer(), TerminalFormatter()))

Zařazení filtru je jen nepatrně delší:

lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(CamelCaseFilter())
 
print(highlight(code, lexer, TerminalFormatter()))

Filtr si otestujeme na následujícím fragmentu kódu naprogramovaného v Pythonu:

code = """
for i in range(1, 11):
    print("Hello world!")
 
if x and y:
    print("yes")
 
if x or y:
    print("dunno")
 
globalVariable = 42
 
def helloWorld():
    print("Hello world!")
 
helloWorld()
 
print(globalVariable)
"""

Obrázek 4: Barevné zvýraznění syntaxe provedené před aplikací filtru.

Výsledek aplikace filtru by měl vypadat následovně:

Obrázek 5: Barevné zvýraznění syntaxe provedené společně s filtrem CamelCaseFilter.

Poznámka: povšimněte si, že se změnilo i jméno globální proměnné. Tento zásadní nedostatek filtru postupně odstraníme.

Následuje výpis úplného zdrojového kódu tohoto demonstračního příkladu:

import re
 
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from pygments.filter import Filter
from pygments.token import Name
from pygments.util import get_bool_opt
 
 
class CamelCaseFilter(Filter):
 
    def __init__(self, **options):
        Filter.__init__(self, **options)
 
    def convert(self, name):
        results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
        results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', results)
        return results.lower()
 
    def filter(self, lexer, stream):
        for ttype, value in stream:
            if ttype is Name.Function or ttype is Name:
                value = self.convert(value)
            yield ttype, value
 
 
code = """
for i in range(1, 11):
    print("Hello world!")
 
if x and y:
    print("yes")
 
if x or y:
    print("dunno")
 
globalVariable = 42
 
def helloWorld():
    print("Hello world!")
 
helloWorld()
 
print(globalVariable)
"""
 
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(CamelCaseFilter())
 
print(highlight(code, lexer, TerminalFormatter()))

6. Použití dekorátoru @simplefilter

Ve skutečnosti existuje ještě jeden alternativní a pro zápis kratší způsob vytvoření filtru. Tento způsob je založen na dekorátoru nazvaném @simplefilter, který se zapisuje před funkci s implementací filtru. Tato funkce musí akceptovat čtyři parametry – self (ve skutečnosti totiž dekorátor vytvoří třídu s metodou), lexer s referencí na použitý lexer, stream s referencí na generátor tokenů a nakonec parametr options s případnými konfiguračními volbami:

@simplefilter
def to_snake_case(self, lexer, stream, options):
    for ttype, value in stream:
        if ttype is Name.Function or ttype is Name:
            results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', value)
            results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', value)
            value = results.lower()
        yield ttype, value

Registrace filtru s jeho přidáním do „kolony“:

lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(to_snake_case())
 
print(highlight(code, lexer, TerminalFormatter()))

Opět se podívejme na úplný zdrojový kód upraveného demonstračního příkladu:

import re
 
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from pygments.filter import simplefilter
from pygments.token import Name
from pygments.util import get_bool_opt
 
 
@simplefilter
def to_snake_case(self, lexer, stream, options):
    for ttype, value in stream:
        if ttype is Name.Function or ttype is Name:
            results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', value)
            results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', value)
            value = results.lower()
        yield ttype, value
 
 
code = """
for i in range(1, 11):
    print("Hello world!")
 
if x and y:
    print("yes")
 
if x or y:
    print("dunno")
 
globalVariable = 42
 
def helloWorld():
    print("Hello world!")
 
helloWorld()
 
print(globalVariable)
"""
 
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(to_snake_case())
 
print(highlight(code, lexer, TerminalFormatter()))

Pro zajímavost se podívejme, jakým způsobem je vlastně dekorátor @simplefilter implementován. Základem je třída nazvaná FunctionFilter s implementací obecného filtru – generátoru, který je založen na použití druhého programátorem definovaného generátoru představovaného metodou FunctionFilter.function(). Při konstrukci objektu se kontroluje, jestli metoda function() existuje:

class FunctionFilter(Filter):
    function = None
 
    def __init__(self, **options):
        if not hasattr(self, 'function'):
            raise TypeError('%r used without bound function' %
                            self.__class__.__name__)
        Filter.__init__(self, **options)
 
    def filter(self, lexer, stream):
        for ttype, value in self.function(lexer, stream, self.options):
            yield ttype, value

Implementace @simplefilter je následující – funkce f, která tento dekorátor používá, je využita pro vytvoření nové třídy nazvané stejně jako dekorovaná funkce. Tato nová třída je odvozena od FunctionFilter, samozřejmě již s navázanou metodou FunctionFilter.function():

def simplefilter(f):
    return type(f.__name__, (FunctionFilter,), {
                'function':     f,
                '__module__':   getattr(f, '__module__'),
                '__doc__':      f.__doc__
})

7. Nepatrné vylepšení předchozího příkladu

Předchozí příklad si ještě před dalšími úpravami nepatrně vylepšíme tak, aby byla konverzí funkce osamostatněna (tak se dá lépe testovat a upravovat). Výsledná podoba příkladu je následující:

import re
 
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from pygments.filter import simplefilter
from pygments.token import Name
from pygments.util import get_bool_opt
 
 
def name_to_snake_case(name):
    results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', results)
    return results.lower()
 
 
@simplefilter
def to_snake_case(self, lexer, stream, options):
    for ttype, value in stream:
        if ttype is Name.Function or ttype is Name:
            value = name_to_snake_case(value)
        yield ttype, value
 
 
code = """
for i in range(1, 11):
    print("Hello world!")
 
if x and y:
    print("yes")
 
if x or y:
    print("dunno")
 
globalVariable = 42
 
def helloWorld():
    print("Hello world!")
 
helloWorld()
 
print(globalVariable)
"""
 
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(to_snake_case())
 
print(highlight(code, lexer, TerminalFormatter()))

8. Korektnější varianta filtru – test typu a hodnoty následujícího tokenu

Filtr, který byl ukázán v předchozích dvou kapitolách, ve skutečnosti není příliš přesný. Na screenshotech jste si pravděpodobně všimli, že kromě názvů funkcí v jejich definicích a při jejich volání (což chceme) mění i názvy proměnných, protože i ty jsou v tokenizovaném kódu reprezentovány tokenem typu Name, který je sám o sobě neodlišitelný od tokenu použitého při volání funkce (ne při její deklaraci). Ostatně můžeme se sami podívat, jak vypadá tokenizovaný výstup tohoto příkladu psaného v Pythonu:

globalVariable = 42
def helloWorld():
    print("Hello world!")
helloWorld()
print(globalVariable)

Tokenizovaný výstup se zvýrazněním typů tokenů, které nás zajímají při konverzi:

Token.Name                        'globalVariable'
Token.Text                        ' '
Token.Operator                    '='
Token.Text                        ' '
Token.Literal.Number.Integer      '42'
Token.Text                        '\n'
Token.Keyword                     'def'
Token.Text                        ' '
Token.Name.Function               'helloWorld'
Token.Punctuation                 '('
Token.Punctuation                 ')'
Token.Punctuation                 ':'
Token.Text                        '\n'
Token.Text                        '    '
Token.Keyword                     'print'
Token.Punctuation                 '('
Token.Literal.String.Double       '"'
Token.Literal.String.Double       'Hello world!'
Token.Literal.String.Double       '"'
Token.Punctuation                 ')'
Token.Text                        '\n'
Token.Name                        'helloWorld'
Token.Punctuation                 '('
Token.Punctuation                 ')'
Token.Text                        '\n'
Token.Keyword                     'print'
Token.Punctuation                 '('
Token.Name                        'globalVariable'
Token.Punctuation                 ')'
Token.Text                        '\n'

Samozřejmě existuje možnost, jak tento poměrně zásadní problém vyřešit. Ta spočívá v tom, že se budeme dívat na dva po sobě jdoucí tokeny – vždy na ten token, který bude výsledkem volání generátoru (viz příkaz yield) a token následující. Pokud bude následující token typu Punctuation a současně bude mít hodnotu „(“ (levá kulatá závorka), můžeme v Pythonu předpokládat, že se jedná o volání funkce (ve skutečnosti to není zcela přesné, ale pro první přiblížení může být tento postup dostatečně dobrý). Samotná funkce zjišťující, jestli se má konverze názvu provést či nikoli, může vypadat následovně (funkci předáváme typy a hodnoty dvou po sobě jdoucích tokenů, u současného tokenu však pouze jeho typ):

def is_function_call_token(current_type, next_type, next_value):
    return current_type is Name \
           and next_type is Punctuation \
           and next_value == "("

Samozřejmě je nutné upravit i celý kód filtru takovým způsobem, aby dokázal získat současný token (což je primitivní) i token následující. Jedno z možných řešení (je jich ovšem více) spočívá ve využití funkce itertools.tee(), která dokáže z jednoho iterátoru vytvořit větší množství nezávislých iterátorů. My budeme potřebovat iterátory dva:

lookahead, tokens = itertools.tee(stream)

Jeden z těchto iterátorů hned na začátku posuneme dopředu o jeden token:

next(lookahead)

A následně již můžeme ve smyčce používat dva tokeny – současný a ten následující:

@simplefilter
def to_snake_case(self, lexer, stream, options):
    lookahead, tokens = itertools.tee(stream)
    next(lookahead)
    for current_token in tokens:
        current_type = current_token[0]
        current_value = current_token[1]
        next_token = next(lookahead)
        next_type = next_token[0]
        next_value = next_token[1]
        if current_type is Name.Function \
           or is_function_call_token(current_type, next_type, next_value):
            current_value = name_to_snake_case(current_value)
        yield current_type, current_value

V proudu tokenů se tedy budou měnit pouze ty tokeny, které jsou na následujícím výpisu zvýrazněny:

Token.Name                        'globalVariable'     ne, protože nenásleduje token s otvírací závorkou
Token.Text                        ' '
Token.Operator                    '='
Token.Text                        ' '
Token.Literal.Number.Integer      '42'
Token.Text                        '\n'
Token.Keyword                     'def'
Token.Text                        ' '
Token.Name.Function               'helloWorld'
Token.Punctuation                 '('
Token.Punctuation                 ')'
Token.Punctuation                 ':'
Token.Text                        '\n'
Token.Text                        '    '
Token.Keyword                     'print'
Token.Punctuation                 '('
Token.Literal.String.Double       '"'
Token.Literal.String.Double       'Hello world!'
Token.Literal.String.Double       '"'
Token.Punctuation                 ')'
Token.Text                        '\n'
Token.Name                        'helloWorld'         ano, protože následuje token se závorkou
Token.Punctuation                 '('                  budoucí/následující token, který byl otestován
Token.Punctuation                 ')'
Token.Text                        '\n'
Token.Keyword                     'print'
Token.Punctuation                 '('
Token.Name                        'globalVariable'     ne, protože nenásleduje token s otvírací závorkou
Token.Punctuation                 ')'
Token.Text                        '\n'
Poznámka: navržené řešení obsahuje jednu chybu. Ta je popsána v navazujícím textu, ovšem pokuste se zamyslet nad tím, proč a v jaké chvíli k chybě dojde.

9. Upravený kód filtru a jeho výsledky

Úplný zdrojový kód po všech úpravách vypadá následovně:

import re
 
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from pygments.filter import simplefilter
from pygments.token import Name, Punctuation
from pygments.util import get_bool_opt
 
import itertools
 
 
def name_to_snake_case(name):
    results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', results).lower()
 
 
def is_function_call_token(current_type, next_type, next_value):
    return current_type is Name \
           and next_type is Punctuation \
           and next_value == "("
 
 
@simplefilter
def to_snake_case(self, lexer, stream, options):
    lookahead, tokens = itertools.tee(stream)
    next(lookahead)
    for current_token in tokens:
        current_type = current_token[0]
        current_value = current_token[1]
        next_token = next(lookahead)
        next_type = next_token[0]
        next_value = next_token[1]
        if current_type is Name.Function \
           or is_function_call_token(current_type, next_type, next_value):
            current_value = name_to_snake_case(current_value)
        yield current_type, current_value
 
 
code = """
for i in range(1, 11):
    print("Hello world!")
 
if x and y:
    print("yes")
 
if x or y:
    print("dunno")
 
globalVariable = 42
 
def helloWorld():
    print("Hello world!")
 
helloWorld()
 
print(globalVariable)
"""
 
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(to_snake_case())
 
print(highlight(code, lexer, TerminalFormatter()))

Obrázek 6: Obarvený kód bez použití filtru.

Obrázek 7: Obarvený kód při použití nové varianty filtru.

10. Zpracování posledního tokenu v proudu

Předchozí demonstrační příklad s filtrem měl jednu vadu: ve chvíli, kdy se přečetl poslední budoucí/následující token, byl generátor představovaný filtrem automaticky ukončen, protože došlo k vyhození výjimky typu StopIteration. To je sice očekávané chování, ovšem znamená, že poslední token (ať již byl jakéhokoli typu) nebyl poslán na výstup filtru a tudíž se ve výsledku neobjevila příslušná část kódu. V našem konkrétním příkladu to příliš nevadilo, protože poslední token byl typu Token.Text s hodnotou „\n“ (konec řádku), ale u jiného vstupu by to mohlo mít mnohem horší následky. Jedno z možných řešení tohoto problému spočívá v explicitním otestování, zda budoucí/následující token existuje a v zákazu vyhození výjimky ve chvíli, kdy neexistuje (místo hodnoty tokenu se vrátí předdefinovaná hodnota None):

@simplefilter
def to_snake_case(self, lexer, stream, options):
    lookahead, tokens = itertools.tee(stream)
    next(lookahead)
    for current_token in tokens:
        current_type = current_token[0]
        current_value = current_token[1]
        next_token = next(lookahead, None)
        if next_token:
            next_type = next_token[0]
            next_value = next_token[1]
            if current_type is Name.Function \
               or is_function_call_token(current_type, next_type, next_value):
                current_value = name_to_snake_case(current_value)
        yield current_type, current_value

Výsledky:

Obrázek 8: Obarvený kód bez použití filtru.

Obrázek 9: Obarvený kód při použití nové (korektní) varianty filtru.

Opět následuje výpis úplného zdrojového kódu vylepšeného příkladu:

import re
 
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from pygments.filter import simplefilter
from pygments.token import Name, Punctuation
from pygments.util import get_bool_opt
 
import itertools
 
 
def name_to_snake_case(name):
    results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', results).lower()
 
 
def is_function_call_token(current_type, next_type, next_value):
    return current_type is Name \
           and next_type is Punctuation \
           and next_value == "("
 
 
@simplefilter
def to_snake_case(self, lexer, stream, options):
    lookahead, tokens = itertools.tee(stream)
    next(lookahead)
    for current_token in tokens:
        current_type = current_token[0]
        current_value = current_token[1]
        next_token = next(lookahead, None)
        if next_token:
            next_type = next_token[0]
            next_value = next_token[1]
            if current_type is Name.Function \
               or is_function_call_token(current_type, next_type, next_value):
                current_value = name_to_snake_case(current_value)
        yield current_type, current_value
 
 
code = """
for i in range(1, 11):
    print("Hello world!")
 
if x and y:
    print("yes")
 
if x or y:
    print("dunno")
 
globalVariable = 42
 
def helloWorld():
    print("Hello world!")
 
helloWorld()
 
print(globalVariable)
"""
 
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(to_snake_case())
 
print(highlight(code, lexer, TerminalFormatter()))

11. Konfigurovatelný filtr

Ukažme si ještě jednu možnou modifikaci filtru, s níž se poměrně často setkáme (ostatně ji obsahují i všechny standardní filtry). Ta bude spočívat v přidání nepovinných parametrů, které je možné filtru předat. Těchto parametrů může být libovolný počet a předávají se formou slovníku obsahujícího dvojice (jméno parametru+hodnota). U našeho konkrétního příkladu můžeme uživatelům filtru umožnit měnit nejenom jména funkcí, ale i všechna další jména (proměnných atd.), která jsou v tokenizovaném kódu reprezentována tokeny typu Token.Name. Povšimněte si, jakým způsobem se využívá pomocná funkce pro získání pravdivostní hodnoty z předaných parametrů:

@simplefilter
def to_snake_case(self, lexer, stream, options):
    convert_all_names = get_bool_opt(options, 'convert_all_names', default=False)
    if convert_all_names:
        for ttype, value in stream:
            if ttype is Name.Function or ttype is Name:
                results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', value)
                results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', results)
                value = results.lower()
            yield ttype, value
    else:
        lookahead, tokens = itertools.tee(stream)
        next(lookahead)
        for current_token in tokens:
            current_type = current_token[0]
            current_value = current_token[1]
            next_token = next(lookahead, None)
            if next_token:
                next_type = next_token[0]
                next_value = next_token[1]
                if current_type is Name.Function \
                   or is_function_call_token(current_type, next_type, next_value):
                    current_value = name_to_snake_case(current_value)
            yield current_type, current_value

Příklad použití:

# pridani filtru
lexer.add_filter(to_snake_case())
 
print(highlight(code, lexer, TerminalFormatter()))
 
input()
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(to_snake_case(convert_all_names=True))

Výsledky:

Obrázek 10: Obarvený kód bez použití filtru.

Obrázek 11: Obarvený kód při použití nové (korektní) varianty filtru.

Obrázek 12: Obarvený kód při použití nové (korektní) varianty filtru s parametrem convert_all_names=True.

Opět si ukažme zdrojový kód tohoto příkladu:

import re
 
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from pygments.filter import simplefilter
from pygments.token import Name, Punctuation
from pygments.util import get_bool_opt
 
import itertools
 
 
def name_to_snake_case(name):
    results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', results)
    return results.lower()
 
 
def is_function_call_token(current_type, next_type, next_value):
    return current_type is Name \
           and next_type is Punctuation \
           and next_value == "("
 
 
@simplefilter
def to_snake_case(self, lexer, stream, options):
    convert_all_names = get_bool_opt(options, 'convert_all_names', default=False)
    if convert_all_names:
        for ttype, value in stream:
            if ttype is Name.Function or ttype is Name:
                results = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', value)
                results = re.sub('([a-z0-9])([A-Z])', r'\1_\2', results)
                value = results.lower()
            yield ttype, value
    else:
        lookahead, tokens = itertools.tee(stream)
        next(lookahead)
        for current_token in tokens:
            current_type = current_token[0]
            current_value = current_token[1]
            next_token = next(lookahead, None)
            if next_token:
                next_type = next_token[0]
                next_value = next_token[1]
                if current_type is Name.Function \
                   or is_function_call_token(current_type, next_type, next_value):
                    current_value = name_to_snake_case(current_value)
            yield current_type, current_value
 
 
code = """
for i in range(1, 11):
    print("Hello world!")
 
if x and y:
    print("yes")
 
if x or y:
    print("dunno")
 
globalVariable = 42
 
def helloWorld():
    print("Hello world!")
 
helloWorld()
 
print(globalVariable)
"""
 
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
input()
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(to_snake_case())
 
print(highlight(code, lexer, TerminalFormatter()))
 
input()
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(to_snake_case(convert_all_names=True))
 
print(highlight(code, lexer, TerminalFormatter()))

12. Třída s implementací vlastního lexeru

Uživatelem-programátorem definované filtry sice mohou být hodně užitečné, ovšem ve chvíli, kdy budeme potřebovat zpracovat (obarvit) zdrojový kód nebo konfigurační soubor psaný v jazyce nepodporovaném knihovnou Pygments, je nutné zjistit, jakým způsobem se implementují nové lexery, popř. jak lze upravit lexery stávající. S tímto problémem jsme se již setkali v předchozím článku, v němž jsme si ukázali následující lexer založený na jednoduchých regulárních výrazech určených pro rozpoznání několika klíčových slov v textu:

class FooLangLexer(RegexLexer):
    name = 'foolang'
    aliases = ['foolang']
    filenames = ['*.foolang']
 
    tokens = {
        'root': [
            (r'\ *print', Name.Function),
            (r'for', Keyword),
            (r'while', Keyword),
            (r'goto', Generic.Error),
            (r'begin', Keyword),
            (r'end', Keyword),
            (r'.+', Generic.Normal),
        ]
    }

Povšimněte si, jakým způsobem je celá třída s novým lexerem strukturována. Obsahuje tři statické atributy s popisem jména jazyka, případných alternativních jmen (aliases) a taktéž se seznamem přípon, které mají soubory psané v daném programovacím nebo značkovacím jazyku. Ovšem nejdůležitější částí lexeru je statický atribut tokens, který je uložen ve formě slovníku. Tento slovník slouží k popisu konečného automatu (finite-state machine) se zásobníkem stavů, přičemž v každém stavu může lexer rozpoznávat libovolné množství regulárních výrazů. Pokud je regulární výraz nalezen, provede se vygenerování tokenu s příslušným typem (například Name.Function nebo Generic.Normal) a konečný automat se může přepnout do jiného stavu :-) (původní stav se uloží na zásobník, takže je možné se k němu kdykoli vrátit pomocí operace „#pop“). V našem konkrétním lexeru je však použit jen jediný stav nazvaný root a automat se tedy nepřepíná (je tedy zredukován na pouhou kombinační logiku a bylo by ho možné zjednodušit na konstrukci typu switch-case).

13. Lexer se dvěma stavy – rozpoznání definice funkce

Můžeme si samozřejmě ukázat nepatrně složitější lexer, v němž se budou rozeznávat dva stavy – běžný program a definici funkce, která bude začínat klíčovými slovy def function a končit slovy end function. Tělo definované funkce bude pro jednoduchost obarveno konstantní barvou (samozřejmě si však můžeme lexer později vylepšit). Vzhledem k tomu, že se stav ukládá na zásobník, můžeme se k předchozímu stavu dostat snadno speciálním jménem „#pop“:

class FooLangLexer(RegexLexer):
    name = 'foolang'
    aliases = ['foolang']
    filenames = ['*.foolang']
 
    tokens = {
        'root': [
            (r'\ *print', Name.Function),
            (r'for', Keyword),
            (r'while', Keyword),
            (r'goto', Generic.Error),
            (r'begin', Keyword),
            (r'end', Keyword),
            (r'def function', Name.Function, 'function'),
            (r'.+', Generic.Normal),
            (r'\n', Generic.Normal)
        ],
        'function': [
            (r'end function', Name.Function, '#pop'),
            (r'.+', Comment),
            (r'\n', Comment)
        ]
    }

Zdrojový kód jazyka „FooLang“ pro otestování lexeru:

for i in range(1, 11)
begin
    print("Hello world!")
end
 
def function Foo
    for i in range(5):
        print("hello world!")
end function
 
while i < 10
begin
    inc i
    print(i)
end
 
def function Bar
    for i in range(5):
        print("hello world!")
end function
 
goto 10

Výsledný proud tokenů generovaný naším novým lexerem se zvýrazněním těch tokenů, které byly nalezeny ve stavu „function“:

Token.Keyword                     'for'
Token.Generic.Normal              ' i in range(1, 11)'
Token.Generic.Normal              '\n'
Token.Keyword                     'begin'
Token.Generic.Normal              '\n'
Token.Name.Function               '    print'
Token.Generic.Normal              '("Hello world!")'
Token.Generic.Normal              '\n'
Token.Keyword                     'end'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '\n'
Token.Name.Function               'def function'
Token.Comment                     ' Foo'
Token.Comment                     '\n'
Token.Comment                     '    for i in range(5):'
Token.Comment                     '\n'
Token.Comment                     '        print("hello world!")'
Token.Comment                     '\n'
Token.Name.Function               'end function'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '\n'
Token.Keyword                     'while'
Token.Generic.Normal              ' i < 10'
Token.Generic.Normal              '\n'
Token.Keyword                     'begin'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '    inc i'
Token.Generic.Normal              '\n'
Token.Name.Function               '    print'
Token.Generic.Normal              '(i)'
Token.Generic.Normal              '\n'
Token.Keyword                     'end'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '\n'
Token.Name.Function               'def function'
Token.Comment                     ' Bar'
Token.Comment                     '\n'
Token.Comment                     '    for i in range(5):'
Token.Comment                     '\n'
Token.Comment                     '        print("hello world!")'
Token.Comment                     '\n'
Token.Name.Function               'end function'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '\n'
Token.Generic.Error               'goto'
Token.Generic.Normal              ' 10'
Token.Generic.Normal              '\n'

14. Výsledek zvýraznění syntaxe a úplný kód demonstračního příkladu s lexerem se dvěma stavy

Příklad výsledku vytvořeného novým lexerem se dvěma stavy. Povšimněte si, jak je obarven vnitřek funkce:

Obrázek 13: Výsledek zvýraznění syntaxe novým lexerem.

Celý kód demonstračního příkladu s novým dvoustavovým lexerem vypadá následovně:

from pygments import highlight
from pygments.lexer import RegexLexer
from pygments.token import *
from pygments.formatters import TerminalFormatter, RawTokenFormatter
from pygments.filters import NameHighlightFilter
 
 
class FooLangLexer(RegexLexer):
    name = 'foolang'
    aliases = ['foolang']
    filenames = ['*.foolang']
 
    tokens = {
        'root': [
            (r'\ *print', Name.Function),
            (r'for', Keyword),
            (r'while', Keyword),
            (r'goto', Generic.Error),
            (r'begin', Keyword),
            (r'end', Keyword),
            (r'def function', Name.Function, 'function'),
            (r'.+', Generic.Normal),
            (r'\n', Generic.Normal)
        ],
        'function': [
            (r'end function', Name.Function, '#pop'),
            (r'.+', Comment),
            (r'\n', Comment)
        ]
    }
 
 
code = """
for i in range(1, 11)
begin
    print("Hello world!")
end
 
def function Foo
    for i in range(5):
        print("hello world!")
end function
 
while i < 10
begin
    inc i
    print(i)
end
 
def function Bar
    for i in range(5):
        print("hello world!")
end function
 
goto 10
"""
 
 
print(highlight(code, FooLangLexer(), TerminalFormatter()))
input()
 
tokens = highlight(code, FooLangLexer(), RawTokenFormatter())
 
tokens = tokens.decode()
 
for token in tokens.split("\n"):
    foobar = token.split("\t")
    if len(foobar) == 2:
        print("{token:30}    {value}".format(token=foobar[0], value=foobar[1]))

15. Lexer se třemi stavy

Nepatrně složitější lexer, jehož zdrojový kód je umístěn pod tento odstavec, navíc rozpoznává jméno funkce a proto používá tři stavy: výchozí stav (root), stav po def function a stav po zadání jména funkce. Zde si povšimněte způsobu odstranění DVOU prvků ze zásobníku stavů pomocí „#pop:2“. Je tomu tak z toho důvodu, že zásobník bude postupně obsahovat stavy [„root“], [„root“, „function_name“] a [„root“, „function_name“, „function“] a ze stavu „function“ budeme chtít ihned přejít zpět do stavu „root“:

class FooLangLexer(RegexLexer):
    name = 'foolang'
    aliases = ['foolang']
    filenames = ['*.foolang']
 
    tokens = {
        'root': [
            (r'\ *print', Name.Function),
            (r'for', Keyword),
            (r'while', Keyword),
            (r'goto', Generic.Error),
            (r'begin', Keyword),
            (r'end', Keyword),
            (r'def function ', Keyword, 'function_name'),
            (r'.+', Generic.Normal),
            (r'\n', Generic.Normal)
        ],
        'function': [
            (r'end function', Keyword, '#pop:2'),
            (r'.+', Comment),
            (r'\n', Comment)
        ],
        'function_name': [
            (r'[A-Za-z]+', Name.Function, 'function'),
            (r'.+', Comment),
            (r'\n', Comment)
        ]
    }

Výsledný proud tokenů generovaný naším novým lexerem:

Token.Keyword                     'for'
Token.Generic.Normal              ' i in range(1, 11)'
Token.Generic.Normal              '\n'
Token.Keyword                     'begin'
Token.Generic.Normal              '\n'
Token.Name.Function               '    print'
Token.Generic.Normal              '("Hello world!")'
Token.Generic.Normal              '\n'
Token.Keyword                     'end'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '\n'
Token.Keyword                     'def function '
Token.Name.Function               'Foo'
Token.Comment                     '\n'
Token.Comment                     '    for i in range(5):'
Token.Comment                     '\n'
Token.Comment                     '        print("hello world!")'
Token.Comment                     '\n'
Token.Keyword                     'end function'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '\n'
Token.Keyword                     'while'
Token.Generic.Normal              ' i < 10'
Token.Generic.Normal              '\n'
Token.Keyword                     'begin'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '    inc i'
Token.Generic.Normal              '\n'
Token.Name.Function               '    print'
Token.Generic.Normal              '(i)'
Token.Generic.Normal              '\n'
Token.Keyword                     'end'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '\n'
Token.Keyword                     'def function '
Token.Name.Function               'Bar'
Token.Comment                     '\n'
Token.Comment                     '    for i in range(5):'
Token.Comment                     '\n'
Token.Comment                     '        print("hello world!")'
Token.Comment                     '\n'
Token.Keyword                     'end function'
Token.Generic.Normal              '\n'
Token.Generic.Normal              '\n'
Token.Generic.Error               'goto'
Token.Generic.Normal              ' 10'
Token.Generic.Normal              '\n'

16. Výsledek zvýraznění syntaxe a úplný kód demonstračního příkladu s lexerem se třemi stavy

Příklad výsledku vytvořeného novým lexerem se třemi stavy. Povšimněte si, jak je obarven vnitřek funkce a jakým způsobem je zvýrazněno její jméno:

Obrázek 14: Výsledek zvýraznění syntaxe novým lexerem se třemi stavy.

Celý kód demonstračního příkladu s novým třístavovým lexerem vypadá následovně:

from pygments import highlight
from pygments.lexer import RegexLexer
from pygments.token import *
from pygments.formatters import TerminalFormatter, RawTokenFormatter
from pygments.filters import NameHighlightFilter
 
 
class FooLangLexer(RegexLexer):
    name = 'foolang'
    aliases = ['foolang']
    filenames = ['*.foolang']
 
    tokens = {
        'root': [
            (r'\ *print', Name.Function),
            (r'for', Keyword),
            (r'while', Keyword),
            (r'goto', Generic.Error),
            (r'begin', Keyword),
            (r'end', Keyword),
            (r'def function ', Keyword, 'function_name'),
            (r'.+', Generic.Normal),
            (r'\n', Generic.Normal)
        ],
        'function': [
            (r'end function', Keyword, '#pop:2'),
            (r'.+', Comment),
            (r'\n', Comment)
        ],
        'function_name': [
            (r'[A-Za-z]+', Name.Function, 'function'),
            (r'.+', Comment),
            (r'\n', Comment)
        ]
    }
 
 
code = """
for i in range(1, 11)
begin
    print("Hello world!")
end
 
def function Foo
    for i in range(5):
        print("hello world!")
end function
 
while i < 10
begin
    inc i
    print(i)
end
 
def function Bar
    for i in range(5):
        print("hello world!")
end function
 
goto 10
"""
 
 
print(highlight(code, FooLangLexer(), TerminalFormatter()))
input()
 
tokens = highlight(code, FooLangLexer(), RawTokenFormatter())
 
tokens = tokens.decode()
 
for token in tokens.split("\n"):
    foobar = token.split("\t")
    if len(foobar) == 2:
        print("{token:30}    {value}".format(token=foobar[0], value=foobar[1]))

17. Použití klauzule bygroups aneb lexer pro zpracování INI souborů

Zkusme si nyní vytvořit lexer pro jednoduché INI soubory, s nimiž se pravděpodobně většina čtenářů Roota již setkala. Zvýraznění syntaxe INI souborů budeme testovat na tomto úryvku kódu:

; komentar
 
[sekce]
x=10
y=20
 
[dalsi-sekce]
foo=bar

Budeme vyžadovat zhruba následující zvýraznění syntaxe, v němž je parametr před znakem „=“ vybarven odlišně než hodnota za znakem „=“:

Obrázek 15: Zvýraznění INI souboru dále popsaným lexerem.

Toho lze dosáhnout různými způsoby, ovšem nejpřímější bude použití regulárního výrazu s několika skupinami, kde každá skupina je uzavřena mezi kulaté závorky. Každé takto definované skupině bude přiřazen jeden typ tokenu, a to s využitím klauzule bygroups:

(r'(.*?)(\s*)(=)(\s*)(.*?)$', bygroups(Name.Attribute, Text, Operator, Text, String))

Tento zápis je zpracován následovně:

  1. Ze skupiny libovolných alfanumerických znaků je vytvořen token s typem Name.Attribute
  2. Případné mezery jsou seskupeny do tokenu s typem Text
  3. Samotný znak „=“ bude samostatným tokenem Operator
  4. Za ním následující případné mezery jsou seskupeny do tokenu s typem Text
  5. Zbylé znaky jsou seskupeny do tokenu String

Samozřejmě musíme doplnit další pravidla pro jednořádkové komentáře a sekce [], takže výsledný lexer bude vypadat takto:

# viz http://pygments.org/docs/lexerdevelopment/
class IniFileLexer(RegexLexer):
    name = 'INI'
    aliases = ['ini', 'cfg']
    filenames = ['*.ini', '*.cfg']
 
    tokens = {
        'root': [
            (r'\s+', Text),
            (r';.*?$', Comment),
            (r'\[.*?\]$', Keyword),
            (r'(.*?)(\s*)(=)(\s*)(.*?)$',
             bygroups(Name.Attribute, Text, Operator, Text, String))
        ]
    }

Opět se podívejme na celý demonstrační příklad:

import re
from pygments import highlight
from pygments.lexer import *
from pygments.token import *
from pygments.style import Style
from pygments.formatters import Terminal256Formatter
from pygments.filters import NameHighlightFilter
 
from pygments.lexer import RegexLexer, bygroups
from pygments.token import *
 
 
# viz http://pygments.org/docs/lexerdevelopment/
class IniFileLexer(RegexLexer):
    name = 'INI'
    aliases = ['ini', 'cfg']
    filenames = ['*.ini', '*.cfg']
 
    tokens = {
        'root': [
            (r'\s+', Text),
            (r';.*?$', Comment),
            (r'\[.*?\]$', Keyword),
            (r'(.*?)(\s*)(=)(\s*)(.*?)$',
             bygroups(Name.Attribute, Text, Operator, Text, String))
        ]
    }
 
 
code = """
; komentar
 
[sekce]
x=10
y=20
 
[dalsi-sekce]
foo=bar
"""
 
 
class NewStyle(Style):
    default_style = ""
    styles = {
        Comment:        '#888',
        Text:           '#ansired',
        Keyword:        '#88f',
        Name.Attribute: 'nobold #ansiyellow',
    }
 
 
print(highlight(code, IniFileLexer(), Terminal256Formatter(style=NewStyle)))

18. Příklad pro třetí část článku – lexer pro klasický jazyk BASIC

Ve třetím článku si ukážeme ještě složitější lexery, například lexer určený pro obarvení klasického BASICu:

Obrázek 16: Obarvení zdrojového kódu v klasickém BASICu.

Již dopředu si pro samostudium můžeme ukázat, jak bude implementace takového lexeru vypadat:

import re
from pygments import highlight
from pygments.lexer import *
from pygments.token import *
from pygments.style import Style
from pygments.formatters import Terminal256Formatter
from pygments.filters import NameHighlightFilter
 
 
# odvozeno od tridy QBasicLexer
# http://pygments.org/docs/lexers/#lexers-for-basic-like-languages-other-than-vb-net
 
class BasicLexer(RegexLexer):
    name = 'Basic'
    aliases = ['basic']
    filenames = ['*.BAS', '*.bas']
    mimetypes = ['text/basic']
 
    declarations = ('DATA', 'LET')
 
    functions = (
        'ABS', 'ASC', 'ATN', 'COS', 'DATE$', 'EXP', 'FRE',
        'INKEY$', 'INPUT$', 'LEN', 'LOG', 'PEEK', 'SGN', 'SIN',
        'SQR', 'STICK', 'STR$', 'STRIG', 'TAN', 'TIME$', 'VAL'
    )
 
    operators = ('AND', 'OR', 'XOR', 'NOT')
 
    statements = (
        'BEEP', 'CLEAR', 'CLS', 'DATA', 'DATE$', 'DIM', 'PLOT',
        'DRAWTO', 'FOR', 'NEXT', 'GOSUB', 'GOTO', 'IF', 'THEN', 'INPUT',
        'LET', 'LINE', 'POKE', 'PRINT', 'PRINT #', 'PRINT USING', 'REM',
        'RETURN', 'RUN', 'STOP', 'STRIG', 'TIME$'
    )
 
    tokens = {
        'root': [
            (r'\n+', Text),
            (r'\s+', Text.Whitespace),
            (r'^(\s*)(\d*)(\s*)(REM .*)$',
             bygroups(Text.Whitespace, Name.Label, Text.Whitespace,
                      Comment.Single)),
            (r'^(\s*)(\d+)(\s*)',
             bygroups(Text.Whitespace, Name.Label, Text.Whitespace)),
            (r'(?=[\s]*)(\w+)(?=[\s]*=)', Name.Variable.Global),
            (r'(?=[^"]*)\'.*$', Comment.Single),
            (r'"[^\n"]*"', String.Double),
            (r'(DIM)(\s+)([^\s(]+)',
             bygroups(Keyword.Declaration, Text.Whitespace, Name.Variable.Global)),
            (r'^(\s*)([a-zA-Z_]+)(\s*)(\=)',
             bygroups(Text.Whitespace, Name.Variable.Global, Text.Whitespace,
                      Operator)),
            (r'(GOTO|GOSUB)(\s+)(\w+\:?)',
             bygroups(Keyword.Reserved, Text.Whitespace, Name.Label)),
            include('declarations'),
            include('functions'),
            include('operators'),
            include('statements'),
            (r'[a-zA-Z_]\w*[$@#&!]', Name.Variable.Global),
            (r'[a-zA-Z_]\w*\:', Name.Label),
            (r'\-?\d*\.\d+[@|#]?', Number.Float),
            (r'\-?\d+[@|#]', Number.Float),
            (r'\-?\d+#?', Number.Integer.Long),
            (r'\-?\d+#?', Number.Integer),
            (r'!=|==|:=|\.=|<<|>>|[-~+/\\*%=<>&^|?:!.]', Operator),
            (r'[\[\]{}(),;]', Punctuation),
            (r'[\w]+', Name.Variable.Global),
        ],
        'declarations': [
            (r'\b(%s)(?=\(|\b)' % '|'.join(map(re.escape, declarations)),
             Keyword.Declaration),
        ],
        'functions': [
            (r'\b(%s)(?=\(|\b)' % '|'.join(map(re.escape, functions)),
             Name.Builtin),
        ],
        'operators': [
            (r'\b(%s)(?=\(|\b)' % '|'.join(map(re.escape, operators)), Operator.Word),
        ],
        'statements': [
            (r'\b(%s)\b' % '|'.join(map(re.escape, statements)),
             Keyword.Reserved),
        ],
    }
 
 
code = """
1500 REM === DRAW a LINE. Ported from C version
1510 REM Inputs are X1, Y1, X2, Y2: Destroys value of X1, Y1
1520 DX = ABS(X2 - X1):SX = -1:IF X1 < X2 THEN SX = 1
1530 DY = ABS(Y2 - Y1):SY = -1:IF Y1 < Y2 THEN SY = 1
1540 ER = -DY
1545 IF DX > DY THEN ER = DX
1550 ER = INT(ER / 2)
1555 REM This command may differ depending ON BASIC dialect
1560 PLOT X1,Y1
1570 IF X1 = X2 AND Y1 = Y2 THEN RETURN
1580 E2 = ER
1590 IF E2 > -DX THEN ER = ER - DY:X1 = X1 + SX
1600 IF E2 < DY THEN ER = ER + DX:Y1 = Y1 + SY
1610 GOTO 1560
"""
 
 
class <strong>NewStyle</strong>(Style):
    default_style = ""
    styles = {
        Comment:                '#888',
        Keyword.Declaration:    '#ansired',
        Keyword.Reserved:       '#88f',
        Name.Builtin:           'nobold #ansiyellow',
        String:                 '#ansilightgray',
        Operator.Word:          '#f0f',
        Name.Label:             '#fff'
    }
 
 
print(highlight(code, BasicLexer(), Terminal256Formatter(style=NewStyle)))

19. Repositář s demonstračními příklady

Všechny dnes popisované demonstrační příklady byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/pre­sentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:

# Příklad Popis Odkaz
1 pygments19_custom_filter.py uživatelsky definovaný filtr https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments19_custom_fil­ter.py
2 pygments20_custom_filter_decorator.py filtr definovaný přes dekorátor https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments20_custom_fil­ter_decorator.py
3 pygments21_better_custom_filter.py vylepšení předchozích filtrů https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments21_better_cus­tom_filter.py
4 pygments22_lookahead_filter.py filtr se sledováním dalšího tokenu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments22_lookahead_fil­ter.py
5 pygments23_correct_lookahead_filter.py oprava zpracování posledního tokenu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments23_correct_lo­okahead_filter.py
6 pygments24_configurable_lo­okahead_filter.py konfigurovatelný filtr https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments24_configura­ble_lookahead_filter.py
7 pygments25_lexer_states.py lexer se dvěma stavy https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments25_lexer_sta­tes.py
8 pygments26_three_states.py lexer se třemi stavy https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments26_three_sta­tes.py
9 pygments27_bygroups.py použití klauzule bygroups https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments27_bygroups­.py
10 pygments28_basic_lexer.py lexer pro BASIC https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments28_basic_le­xer.py

Další soubory, s nimiž jsme se dnes setkali:

# Soubor Popis Odkaz
1 pygments.mm schéma zpracování https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments.mm

20. Odkazy na Internetu

  1. Pygments – Python syntax highlighter
    http://pygments.org/
  2. Pygments (dokumentace)
    http://pygments.org/docs/
  3. Write your own filter
    http://pygments.org/docs/fil­terdevelopment/
  4. Write your own lexer
    http://pygments.org/docs/le­xerdevelopment/
  5. Write your own formatter
    http://pygments.org/docs/for­matterdevelopment/
  6. Jazyky podporované knihovnou Pygments
    http://pygments.org/languages/
  7. Pygments FAQ
    http://pygments.org/faq/
  8. Pygments 2.2.0 (na PyPi)
    https://pypi.org/project/Pygments/
  9. Syntax highlighting
    https://en.wikipedia.org/wi­ki/Syntax_highlighting
  10. Lexical analysis
    https://en.wikipedia.org/wi­ki/Lexical_analysis
  11. Lexical grammar
    https://en.wikipedia.org/wi­ki/Lexical_grammar
  12. Compiler Construction/Lexical analysis
    https://en.wikibooks.org/wi­ki/Compiler_Construction/Le­xical_analysis
  13. Compiler Design – Lexical Analysis
    https://www.tutorialspoin­t.com/compiler_design/com­piler_design_lexical_analy­sis.htm
  14. Lexical Analysis – An Intro
    https://www.scribd.com/do­cument/383765692/Lexical-Analysis
  15. prompt_toolkit 2.0.3 na PyPi
    https://pypi.org/project/prom­pt_toolkit/
  16. python-prompt-toolkit na GitHubu
    https://github.com/jonathan­slenders/python-prompt-toolkit
  17. Comparing Python Command-Line Parsing Libraries – Argparse, Docopt, and Click
    https://realpython.com/comparing-python-command-line-parsing-libraries-argparse-docopt-click/
  18. Rosetta Code
    http://rosettacode.org/wi­ki/Rosetta_Code
  19. Mandelbrot set: Sinclair ZX81 BASIC
    http://rosettacode.org/wi­ki/Mandelbrot_set#Sinclair_ZX81_BA­SIC
  20. Lexikální analýza (Wikipedia)
    https://cs.wikipedia.org/wi­ki/Lexik%C3%A1ln%C3%AD_anal%C3%BDza
  21. Quex, a lexical analyzer generator
    http://quex.sourceforge.net/
  22. ATARI BASIC – tokenizace
    https://www.atariarchives­.org/dere/chapt10.php
  23. BASIC token
    https://www.c64-wiki.com/wiki/BASIC_token
  24. CamelCase (Wikipedia)
    https://en.wikipedia.org/wi­ki/Camel_case
  25. Snake case
    https://en.wikipedia.org/wi­ki/Snake_case
  26. Kebab-case
    https://en.wikipedia.org/wi­ki/Letter_case#Special_ca­se_styles

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.