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
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
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
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:
- 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.
- 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.VisibleWhitespaceFilter, pygments.filters.CodeTagFilter a pygments.filters.KeywordCaseFilter.
- 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.Terminal256Formatter a pygments.formatters.TerminalTrueColorFormatter), pygments.formatters.HtmlFormatter, pygments.formatters.LaTexFormatter, pygments.formatters.SvgFormatter a pro ladění užitečný pygments.formatters.RawTokenFormatter. 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
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.
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'
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ě:
- Ze skupiny libovolných alfanumerických znaků je vytvořen token s typem Name.Attribute
- Případné mezery jsou seskupeny do tokenu s typem Text
- Samotný znak „=“ bude samostatným tokenem Operator
- Za ním následující případné mezery jsou seskupeny do tokenu s typem Text
- 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/presentations. 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ář:
Další soubory, s nimiž jsme se dnes setkali:
# | Soubor | Popis | Odkaz |
---|---|---|---|
1 | pygments.mm | schéma zpracování | https://github.com/tisnik/presentations/blob/master/pygments/pygments.mm |
20. Odkazy na Internetu
- Pygments – Python syntax highlighter
http://pygments.org/ - Pygments (dokumentace)
http://pygments.org/docs/ - Write your own filter
http://pygments.org/docs/filterdevelopment/ - Write your own lexer
http://pygments.org/docs/lexerdevelopment/ - Write your own formatter
http://pygments.org/docs/formatterdevelopment/ - Jazyky podporované knihovnou Pygments
http://pygments.org/languages/ - Pygments FAQ
http://pygments.org/faq/ - Pygments 2.2.0 (na PyPi)
https://pypi.org/project/Pygments/ - Syntax highlighting
https://en.wikipedia.org/wiki/Syntax_highlighting - Lexical analysis
https://en.wikipedia.org/wiki/Lexical_analysis - Lexical grammar
https://en.wikipedia.org/wiki/Lexical_grammar - Compiler Construction/Lexical analysis
https://en.wikibooks.org/wiki/Compiler_Construction/Lexical_analysis - Compiler Design – Lexical Analysis
https://www.tutorialspoint.com/compiler_design/compiler_design_lexical_analysis.htm - Lexical Analysis – An Intro
https://www.scribd.com/document/383765692/Lexical-Analysis - prompt_toolkit 2.0.3 na PyPi
https://pypi.org/project/prompt_toolkit/ - python-prompt-toolkit na GitHubu
https://github.com/jonathanslenders/python-prompt-toolkit - Comparing Python Command-Line Parsing Libraries – Argparse, Docopt, and Click
https://realpython.com/comparing-python-command-line-parsing-libraries-argparse-docopt-click/ - Rosetta Code
http://rosettacode.org/wiki/Rosetta_Code - Mandelbrot set: Sinclair ZX81 BASIC
http://rosettacode.org/wiki/Mandelbrot_set#Sinclair_ZX81_BASIC - Lexikální analýza (Wikipedia)
https://cs.wikipedia.org/wiki/Lexik%C3%A1ln%C3%AD_anal%C3%BDza - Quex, a lexical analyzer generator
http://quex.sourceforge.net/ - ATARI BASIC – tokenizace
https://www.atariarchives.org/dere/chapt10.php - BASIC token
https://www.c64-wiki.com/wiki/BASIC_token - CamelCase (Wikipedia)
https://en.wikipedia.org/wiki/Camel_case - Snake case
https://en.wikipedia.org/wiki/Snake_case - Kebab-case
https://en.wikipedia.org/wiki/Letter_case#Special_case_styles