Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů

19. 7. 2018
Doba čtení: 25 minut

Sdílet

Dnes se seznámíme se základními vlastnostmi knihovny Pygments, kterou je možné použít pro obarvení zdrojových kódů, konfiguračních souborů atd. Tato knihovna obsahuje podporu pro přibližně 300 jazyků a desítku výstupních formátů.

Obsah

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

2. Princip činnosti knihovny Pygments

3. Lexémy a tokeny (tokenizace)

4. Příklad tokenizace jednoduchého kódu napsaného v Pythonu

5. Základní příklad použití: převod zdrojového textu do HTML

6. Převod zdrojového textu do HTML a LaTeXu s volitelnou tvorbou uceleného dokumentu

7. Výstup na terminál (16 barev, 256 barev, true color)

8. Export obarveného kódu do grafických formátů: rastrové obrázky a vektorové kresby

9. Výpis jednotlivých lexikálních tokenů analyzovaného zdrojového kódu

10. Praktické použití: obarvení zdrojových kódů na terminálu, výstup do SVG atd.

11. Specifikace vlastního stylu (barev) při obarvování zdrojových kódů

12. Nastavení filtru pro zpracování výstupu z lexeru

13. Dostupné standardní filtry

14. Obarvení vybraných slov

15. Zvýraznění bílých znaků

16. Změna velikosti písmen v klíčových slovech

17. Jednoduchý lexer založený na rozpoznávání klíčových slov a regulárních výrazů

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

19. Odkazy na Internetu

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

V dnešním článku se seznámíme se základními způsoby použití knihovny Pygments. Tato knihovna je určena pro zvýraznění syntaxe prakticky libovolného programovacího či značkovacího jazyka. Současná verze knihovny Pygments podporuje přibližně 300 jazyků, popř. různých dialektů. Výstupní formáty jsou volitelné a již při základní instalaci dostane programátor k dispozici například výstup do HTML, LaTeXu, RTF, různých rastrových formátů, vektorového formátu SVG a v neposlední řadě taktéž výstup na terminál/konzoli podporující minimálně 16barev (volitelně i 256 barev či true color výstup). Ovšem největší síla této knihovny spočívá v tom, že je poměrně snadné do ní přidat podporu pro další jazyk, vytvořit si vlastní výstupní formát a dokonce naprogramovat takzvané filtry, které dokážou ovlivňovat výsledek práce „obarvovače“.

Dnešní článek bude rozdělen do tří částí. V první části začínající pátou kapitolou si ukážeme použití různých výstupních formátů, do kterých je možné vyexportovat zdrojový kód se zvýrazněním syntaxe. Jedná se například o klasické HTML, LaTeX, výstup na terminál/konzoli s použitím escape znaků, export do rastrového obrázku, vektorového výkresu ve formátu SVG apod. Část druhá bude prozatím pouze informativní a řekneme si v ní základní informace o filtrech sloužících pro modifikaci takzvaných tokenů (ty představují meziformát mezi vstupním textem a formátovaným výstupem). V závěrečné části si na prozatím velmi jednoduchém příkladu ukážeme způsob vytvoření vlastního lexeru, ať již se bude jednat o relativně složitý lexer určený pro nový programovací jazyk či o jednoduchý lexer sloužící pouze pro rozpoznávání klíčových slov v konfiguračních souborech.

2. Princip činnosti knihovny Pygments

Při zpracování a obarvování zdrojových kódů používá knihovna Pygments několik typů tříd, jejichž instance postupně provádí jednotlivé dílčí kroky. Díky rozdělení celého zpracování do několika konfigurovatelných kroků je zajištěna velká flexibilita knihovny a možnost jejího snadného rozšiřování o další podporované jazyky, výstupní formáty, speciální filtry atd. Celý průběh zpracování vypadá následovně:

  1. Na začátku zpracování se nachází takzvaný lexer, který postupně načítá jednotlivé znaky ze vstupního řetězce (resp. souboru) a vytváří z nich lexikální 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í.
  2. Výstup z lexeru může procházet libovolným počtem filtrů sloužících pro odstranění nebo (č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 (TODO:, FIX:) apod.
  3. Za filtry 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 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 se seznámíme a opět platí – v případě potřeby je možné si naprogramovat formátovač vlastní.
Poznámka: díky tomu, že se prakticky veškeré zpracování zdrojových textů odehrává na úrovni tokenů, není nutné, aby byl celý zpracovávaný zdrojový kód (nebo jeho tokenizovaná podoba) uložen v operační paměti. Je tedy možné zpracovávat i velmi rozsáhlé dokumenty, a to bez větších nároků na operační paměť.

3. Lexémy a tokeny (tokenizace)

První část zpracování zdrojových textů je nejzajímavější a implementačně i nejsložitější. Lexer totiž musí v sekvenci znaků tvořících zdrojový text najít takzvané lexémy, tj. skupiny (sousedních) znaků odpovídajících nějakému vzorku (použít lze gramatiku, regulární výraz či ad-hoc testy). Z lexémů se posléze tvoří již zmíněné lexikální tokeny, což je – poněkud zjednodušeně řečeno – dvojice obsahující typ tokenu (někdy se namísto „typ“ používá označení „jméno“) a řetězec ze vstupního zdrojového souboru. Převodu zdrojového textu na sekvenci tokenů se někdy říká tokenizace. Účelem tokenizace může být:

  • Transformace zdrojového textu do podoby, která může být dále zpracovávána dalším modulem překladače (syntaktická analýza). V takovém případě se však některé tokeny mohou zahazovat; příkladem mohou být komentáře, tokeny představující bílé znaky apod. Spojením lexeru a modulu pro syntaktickou analýzu vznikne parser (jeho typickým výsledkem je AST).
  • Transformace zdrojového kódu pro účely zvýraznění syntaxe v editorech či prohlížečích. V tomto případě se žádné tokeny nezahazují, což je případ knihovny Pygments.
Poznámka: výše zmíněná tokenizace se používala například již v interpretrech programovacího jazyka BASIC na mnoha osmibitových domácích počítačích. Ovšem v tomto případě měly tokeny poněkud odlišnou strukturu, protože všechny příkazy a funkce byly většinou reprezentovány jednoznačným osmibitovým celým číslem, které tak současně představovalo jak typ tokenu, tak i jeho hodnotu. Důvod byl jednoduchý – v operační paměti byl uložen tokenizovaný kód a nikoli kód zapsaný uživatelem. Tento kód byl již mnohem jednodušeji zpracovatelný interpretrem než původní zdrojový kód (odpadlo neustálé volání lexeru). Navíc se každý programový řádek ihned po svém zápisu automaticky normalizoval (odstranily se bílé znaky, zkratky příkazů se expandovaly atd.). Ostatně množina příkazů a funkcí byla předem známá a nebyla rozšiřitelná (až na uživatelské funkce dostupné jen v některých BASICech). Příkladem tokenizace tohoto typu mohou být tokeny použité v interpretru programovacího jazyka Atari BASIC, které skutečně přímo odpovídají příkazům, funkcím a operátorům tohoto jazyka. Pro zajímavost:
Příkaz Kód tokenu Příkaz Kód tokenu Příkaz Kód tokenu Příkaz Kód tokenu
REM 00 NEXT 09 CLR 18 NOTE 27
DATA 01 GOTO 10 DEG 19 POINT 28
INPUT 02 GO TO 11 DIM 20 XIO 29
COLOR 03 GOSUB 12 END 21 ON 30
LIST 04 TRAP 13 NEW 22 POKE 31
ENTER 05 BYE 14 OPEN 23 PRINT 32
LET 06 CONT 15 LOAD 24 RAD 33
IF 07 COM 16 SAVE 25 READ 34
FOR 08 CLOSE 17 STATUS 26 RESTORE 35

…atd…

4. Příklad tokenizace jednoduchého kódu napsaného v Pythonu

Podívejme se nyní na příklad tokenizace velmi jednoduchého a krátkého kódu, který je naprogramován v Pythonu:

for i in range(1, 11):
    print("Hello world!")

Výsledkem tokenizace je následující sekvence tokenů, tj. dvojic typ+hodnota (řetězec):

Typ tokenu Řetězec
Token.Keyword ‚for‘
Token.Text ' '
Token.Name ‚i‘
Token.Text ' '
Token.Operator.Word ‚in‘
Token.Text ' '
Token.Name.Builtin ‚range‘
Token.Punctuation ‚(‘
Token.Literal.Number.Integer ‚1‘
Token.Punctuation ‚,‘
Token.Text ' '
Token.Literal.Number.Integer ‚11‘
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‘
Poznámka: lexer se v žádném případě nesnaží o nalezení syntaktických (a už vůbec ne sémantických) chyb v programu! Pouze se snaží rozeznat známé vzorky. To například znamená, že tokenizace proběhne i pro tento zdrojový kód, který je sémanticky naprosto chybný:
range(1, "FDA") for while with i
except for for i else
    print("Hello world!")

Výsledek tokenizace:

Typ tokenu Řetězec
Token.Name.Builtin ‚range‘
Token.Punctuation ‚(‘
Token.Literal.Number.Integer ‚1‘
Token.Punctuation ‚,‘
Token.Text ' '
Token.Literal.String.Double ‚"‘
Token.Literal.String.Double ‚FDA‘
Token.Literal.String.Double ‚"‘
Token.Punctuation ‚)‘
Token.Text ' '
Token.Keyword ‚for‘
Token.Text ' '
Token.Keyword ‚while‘
Token.Text ' '
Token.Keyword ‚with‘
Token.Text ' '
Token.Name ‚i‘
Token.Text ‚\n‘
Token.Keyword ‚except‘
Token.Text ' '
Token.Keyword ‚for‘
Token.Text ' '
Token.Keyword ‚for‘
Token.Text ' '
Token.Name ‚i‘
Token.Text ' '
Token.Keyword ‚else‘
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‘

5. Základní příklad použití: převod zdrojového textu do HTML a LaTeXu s obarvením syntaxe

Konkrétním způsobem implementace vlastního lexeru, filtrů a formátovačů se budeme zabývat v dalším textu, ovšem na začátek si ukažme základní příklad použití knihovny Pygments. První příklad slouží k převodu zdrojového kódu z Pythonu do HTML, samozřejmě s obarvením syntaxe. Převáděný zdrojový kód jsme si již ukazovali výše:

for i in range(1, 11):
    print("Hello world!")

Při převodu je nutné použít dvě třídy – lexer pro Python (třída PythonLexer) a formátovač pro HTML výstup (knihovna HtmlFormatter). Prozatím nepoužijeme žádné filtry, takže převod bude triviální a provedeme ho funkcí highlight, které se předá zdrojový kód, použitý lexer a použitý formátovač:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), HtmlFormatter()))

Výsledkem bude fragment HTML stránky:

Obrázek 1: Výsledný fragment HTML stránky vytvořený předchozím příkladem.

6. Převod zdrojového textu do HTML a LaTeXu s volitelnou tvorbou uceleného dokumentu

Získat je možné i úplnou HTML stránku, tj. ucelený dokument s hlavičkou, patičkou atd. K tomuto účelu se používá volitelný parametr full nastavený na hodnotu True a předaný formátovači:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), HtmlFormatter(full=True)))

Obrázek 2: Úplná podoba vytvořené HTML stránky zobrazené v prohlížeči.

Podobným způsobem můžeme převést zdrojový kód do LaTeXu, a to implicitně ve zkrácené podobě (bez nových příkazů a maker) určené pro vložení do většího dokumentu:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import LatexFormatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), LatexFormatter()))

Obrázek 3: Výsledek konverze zdrojového kódu do LaTeXu.

Alternativně můžeme vytvořit i samostatný (standalone) dokument, který by měl být bez dalších změn zpracovatelný LaTeXem popř. samozřejmě i pdfLaTeXem:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import LatexFormatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), LatexFormatter(full=True)))

Obrázek 4: Výsledek konverze zdrojového kódu do plnohodnotného dokumentu zpracovatelného LaTeXem (zde je konkrétně ukázána hlavička s několika TeXovskými makry).

7. Výstup na terminál (16 barev, 256 barev, true color)

Užitečný může být i přímý výstup obarveného zdrojového kódu na terminál/konzoli. V tomto případě příslušný formátovač vygeneruje escape sekvence (řídicí znaky), které instruují terminál, jakým způsobem má změnit styl písma. Na začátku každého řádku je obarvení obnoveno, což je důležité ve chvíli, kdy je zapotřebí výstup rozdělit na jednotlivé stránky. V současné verzi knihovny Pygments existují tři formátovače určené pro vytváření escape sekvencí pro terminál:

Formátovač Význam
TerminalFormatter určený pro výstup na běžné emulátory s 16 barvami
Terminal256Formatter určené pro terminály umožňující zobrazení 256 barev (Xterm apod.)
TerminalTrueColorFormatter určené pro terminály umožňující výběr z 16 milionů barev (true color)

U formátovače TerminalFormatter lze volit barvu pozadí (světlá/tmavá) a popř. i zapnout číslování řádků. Obě možnosti jsou ukázány v dalším demonstračním příkladu:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
print("-----------------------")
 
print(highlight(code, PythonLexer(), TerminalFormatter(linenos=True)))
 
print("-----------------------")
 
print(highlight(code, PythonLexer(), TerminalFormatter(bg="light")))
 
print("-----------------------")
 
print(highlight(code, PythonLexer(), TerminalFormatter(bg="dark")))

Obrázek 5: Výsledek obarvení zdrojového kódu na terminálu s 16 barvami. Barva pozadí je nastavena na černou.

Další příklad ukazuje použití terminálu, který rozpoznává 256 barev:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import Terminal256Formatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), Terminal256Formatter()))

Obrázek 6: Výsledek obarvení zdrojového kódu na terminálu rozpoznávajícího minimálně 256 barev. Barva pozadí je opět nastavena na černou.

Třetí příklad je podobný příkladu předchozímu, ovšem předpokládá terminál s pravými barvami:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalTrueColorFormatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), TerminalTrueColorFormatter()))

Obrázek 7: Výsledek obarvení zdrojového kódu na terminálu pracujícího s pravými barvami (true color).

Poznámka: pro otestování schopností vašeho terminálu můžete použít skript https://gist.github.com/hSA­TAC/1095100, který by u terminálu podporujícího 256 barev měl zobrazit tento vzorek:

Obrázek 8: Test, zda terminál podporuje 256 barev.

8. Export obarveného kódu do grafických formátů: rastrové obrázky a vektorové kresby

V knihovně Pygments nalezneme i formátovače, jejichž výstupem jsou rastrové obrázky popř. vektorové kresby. Jedná se o následující formátovače:

Formátovač Význam
ImageFormatter výstup do zvoleného rastrového formátu (ve výchozím nastavení PNG)
JpgImageFormatter výstup do rastrového formátu JPEG
BmpImageFormatter výstup do rastrového formátu BMP
GifImageFormatter výstup do rastrového formátu GIF
SvgFormatter výstup do vektorového formátu SVG

Ukažme si výstup do SVG s využitím formátovače nazvaného příznačně SvgFormatter:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import SvgFormatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), SvgFormatter()))

Výsledný SVG soubor: 1.svg.

Většinou je nutné přidat do vektorového výkresu světlé pozadí, což je ukázáno na ručně upraveném souboru 2.svg.

Obrázek 9: Zobrazení SVG v prohlížečce vektorových výkresů.

Obrázek 10: Díky lineárním transformacím je možné text rotovat beze ztráty kvality.

9. Výpis jednotlivých lexikálních tokenů analyzovaného zdrojového kódu

Zajímavý je formátovač nazvaný RawTokenFormatter. Ten slouží pro výpis typů a současně i hodnot tokenů. Formát výstupu je následující:

"Typtokenu\t'hodnota tokenu'\n
 Typtokenu\t'hodnota tokenu'\n
 Typtokenu\t'hodnota tokenu'\n"

V dalším příkladu výsledek vytvořený tímto formátovačem zpracujeme zpět na dvojice typ_tokenu+hodnota_tokenu, které ve zformátované podobě vytiskneme na standardní výstup. Pro rozdělení původního řetězce dobře poslouží metoda String.split():

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import RawTokenFormatter
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
tokens = highlight(code, PythonLexer(), 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]))

Výsledek pro náš jednoduchý vstupní zdrojový kód by měl vypadat následovně:

Token.Keyword                     'for'
Token.Text                        ' '
Token.Name                        'i'
Token.Text                        ' '
Token.Operator.Word               'in'
Token.Text                        ' '
Token.Name.Builtin                'range'
Token.Punctuation                 '('
Token.Literal.Number.Integer      '1'
Token.Punctuation                 ','
Token.Text                        ' '
Token.Literal.Number.Integer      '11'
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'

Jak jsme si již řekli v úvodních kapitolách, neprovádí lexer žádnou kontrolu na syntaxi ani na sémantiku zápisu, takže můžeme tokenizovat i po všech stránkách neplatný kód:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import RawTokenFormatter
 
code = """
range(1, "FDA") for while with i
except for for i else
    print("Hello world!")
"""
 
tokens = highlight(code, PythonLexer(), 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]))

S výsledkem:

Token.Name.Builtin                'range'
Token.Punctuation                 '('
Token.Literal.Number.Integer      '1'
Token.Punctuation                 ','
Token.Text                        ' '
Token.Literal.String.Double       '"'
Token.Literal.String.Double       'FDA'
Token.Literal.String.Double       '"'
Token.Punctuation                 ')'
Token.Text                        ' '
Token.Keyword                     'for'
Token.Text                        ' '
Token.Keyword                     'while'
Token.Text                        ' '
Token.Keyword                     'with'
Token.Text                        ' '
Token.Name                        'i'
Token.Text                        '\n'
Token.Keyword                     'except'
Token.Text                        ' '
Token.Keyword                     'for'
Token.Text                        ' '
Token.Keyword                     'for'
Token.Text                        ' '
Token.Name                        'i'
Token.Text                        ' '
Token.Keyword                     'else'
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'

10. Praktické použití: obarvení zdrojových kódů na terminálu, výstup do SVG atd.

V této kapitole si pro zajímavost uvedeme zdrojové kódy dvou skriptů, které je možné použít v praxi. Prvnímu skriptu je nutné na příkazové řádce předat jméno vstupního souboru (psaného v Pythonu). Výsledkem bude obarvený zdrojový kód vypsaný přímo na terminál. Skript bude možné použít na jakémkoli terminálu s podporou minimálně šestnácti barev:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from sys import argv, exit
 
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
 
if len(argv) <= 1:
    print("Usage: pygments11_highlight_source.py FILENAME")
    exit(1)
 
with open(argv[1], 'r') as fin:
    code = fin.read()
    print(highlight(code, PythonLexer(), TerminalFormatter()))

Druhý skript slouží k převodu zdrojového kódu do formátu SVG, který lze využít například pro tvorbu badgí apod.:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from sys import argv, exit
 
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import SvgFormatter
 
if len(argv) <= 1:
    print("Usage: pygments12_source2svg.py FILENAME > DRAWING.svg")
    exit(1)
 
with open(argv[1], 'r') as fin:
    code = fin.read()
    print(highlight(code, PythonLexer(), SvgFormatter()))

11. Specifikace vlastního stylu (barev) při obarvování zdrojových kódů

Zejména při tisku obarveného zdrojového kódu na terminál může být užitečné změnit barvu jednotlivých typů tokenů. I to je samozřejmě možné v knihovně Pygments zařídit a to relativně jednoduše. K tomuto účelu se používá třída Style, on níž je možné si odvodit novou třídu s uživatelsky definovaným stylem. V případě použití terminálu, který podporuje 256 barev, lze do stylů zapsat barvy ve formátu #rgb, podobně, jako je tomu v HTML a CSS. Kromě toho je možné specifikovat i styl zobrazení textu (tučný, podtržený, kurzíva). Styly se nastavují pro jednotlivé typy tokenů (je tedy nutné importovat příslušné třídy):

class NewStyle(Style):
    default_style = ""
    styles = {
        Comment:                'italic #888',
        Keyword:                'underline #f00',
        Name.Builtin:           'bold #ff0',
        String:                 '#0f0 bg:#232'
    }

Nová třída se styly se použije snadno – styl se předá formátovači v nepovinném parametru style:

print(highlight(code, PythonLexer(), Terminal256Formatter(style=NewStyle)))

Podívejme se nyní na úplný zdrojový kód tohoto příkladu:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import Terminal256Formatter
from pygments.style import Style
from pygments.token import Keyword, Name, Comment, String, Error, \
     Number, Operator, Generic
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), Terminal256Formatter()))
 
print("-----------------------")
 
 
class NewStyle(Style):
    default_style = ""
    styles = {
        Comment:                'italic #888',
        Keyword:                'underline #f00',
        Name.Builtin:           'bold #ff0',
        String:                 '#0f0 bg:#232'
    }
 
 
print(highlight(code, PythonLexer(), Terminal256Formatter(style=NewStyle)))

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

Druhý příklad je velmi podobný, ovšem při specifikaci stylů jsou použity názvy standardních barev platné i pro původní 16barevné terminály. Povšimněte si, že každá barva opět začíná znakem #:

class NewStyle(Style):
    default_style = ""
    styles = {
        Comment:                'italic #ansidarkgray',
        Keyword:                'underline #ansired',
        Name.Builtin:           'bold #ansiyellow',
        String:                 '#ansilightgray'
    }

Mapování mezi názvem barvy a jejím konkrétním kódem si zajistí knihovna Pygments automaticky na základě tohoto slovníku:

_ansimap = {
    # dark
    '#ansiblack': '000000',
    '#ansidarkred': '7f0000',
    '#ansidarkgreen': '007f00',
    '#ansibrown': '7f7fe0',
    '#ansidarkblue': '00007f',
    '#ansipurple': '7f007f',
    '#ansiteal': '007f7f',
    '#ansilightgray': 'e5e5e5',
    # normal
    '#ansidarkgray': '555555',
    '#ansired': 'ff0000',
    '#ansigreen': '00ff00',
    '#ansiyellow': 'ffff00',
    '#ansiblue': '0000ff',
    '#ansifuchsia': 'ff00ff',
    '#ansiturquoise': '00ffff',
    '#ansiwhite': 'ffffff',
}

Podívejme se nyní na úplný zdrojový kód příkladu:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import Terminal256Formatter
from pygments.style import Style
from pygments.token import Keyword, Name, Comment, String, Error, \
     Number, Operator, Generic
 
code = """
for i in range(1, 11):
    print("Hello world!")
"""
 
print(highlight(code, PythonLexer(), Terminal256Formatter()))
 
print("-----------------------")
 
 
class NewStyle(Style):
    default_style = ""
    styles = {
        Comment:                'italic #ansidarkgray',
        Keyword:                'underline #ansired',
        Name.Builtin:           'bold #ansiyellow',
        String:                 '#ansilightgray'
    }
 
 
print(highlight(code, PythonLexer(), Terminal256Formatter(style=NewStyle)))

Obrázek 12: Změna stylu zobrazení při použití terminálu s možností práce se šestnácti barvami.

12. Nastavení filtru pro zpracování výstupu z lexeru

Při popisu činnosti knihovny Pygments, který byl uveden ve druhé kapitole jsme se krátce zmínili o takzvaných filtrech. Ty mohou být v případě potřeby zařazeny mezi lexer a formátovač. Vstupem do filtrů je sekvence tokenů, výstupem taktéž (obecně odlišná) sekvence tokenů; samotný filtr je přitom implementován formou generátoru. Ve filtrech je možné s tokeny různě manipulovat, především pak provádět následující operace:

  1. Odstraňovat tokeny ze vstupní sekvence
  2. Přidávat nové tokeny do výstupní sekvence
  3. Měnit typ tokenů, například z klíčového slova na operátor apod.
  4. Měnit samotný text (hodnotu) tokenu

Tyto operace je samozřejmě možné kombinovat, takže se například setkáme s filtry, které jeden původní token rozdělí na několik nových tokenů. Příkladem může být filtr, který v běžném komentáři nalezne zvolená slova (TODO, BUG apod.) a ty barevně zvýrazní. Technicky to znamená rozdělit původní token typu Comment na tři nové tokeny typu Comment, Keyword a znovu Comment.

13. Dostupné standardní filtry

Popišme si nyní některé standardní filtry, které jsou již implementovány a v případě potřeby použity přímo v knihovně Pygments. Jedná se o následující sadu filtrů, které je samozřejmě možné různým způsobem kombinovat:

Filtr Stručný popis
NameHighlightFilter zvýraznění vybraných slov stejným způsobem (barvou, stylem), jako jména funkcí
CodeTagFilter zvýraznění vybraných slov v komentářích (TODO atd.)
KeywordCaseFilter změna velikosti písmen klíčových slov
VisibleWhitespaceFilter zvýraznění bílých znaků nalezených ve zdrojovém kódu

V navazujících kapitolách si ukážeme, jakým způsobem se tyto filtry mohou využít.

14. Obarvení vybraných slov

První standardní filtr, s nímž se dnes seznámíme, je filtr nazvaný NameHighlightFilter. Tento filtr slouží ke zvýraznění vybraných slov ve výsledném (naformátovaném) dokumentu, přičemž styl formátování těchto slov odpovídá jménům funkcí (používá se pro ně stejný typ tokenu). Díky existenci tohoto filtru je možné rozšířit možnosti již existujícího lexeru, například přidat nové pseudoklíčové slovo do syntaxe Pythonu. Ostatně přesně tento příklad si nyní ukážeme. Použijeme v něm filtr NameHighlightFilter pro přidání dvou pseudoklíčových slov xor a goto (velmi užitečné :-) do obarvovače zdrojových kódů naprogramovaných v Pythonu:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from pygments.filters import NameHighlightFilter
 
code = """
for i in range(1, 11):
    print("Hello world!")
if x and y:
    print("yes")
if x or y:
    print("dunno")
if x xor y:
    print("different")
goto 10
"""
 
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
print("-----------------------")
 
lexer = PythonLexer()
 
# pridani filtru
lexer.add_filter(NameHighlightFilter(
    names=['xor', 'goto']
))
 
print(highlight(code, lexer, TerminalFormatter()))

Příklad použití tohoto typu filtru:

Obrázek 13: Ukázka použití filtru, který dokáže rozpoznat nová slova.

Poznámka: samozřejmě je možné specifikovat, jakým způsobem (jakou třídou) se mají nová slova obarvit. Toto nastavení si ukážeme příště.

15. Zvýraznění bílých znaků

Druhý standardní filtr, který může být užitečný, se jmenuje VisibleWhitespaceFilter a slouží ke zvýraznění vybraných bílých znaků. Opět tento filtr použijeme pro kód naprogramovaný v Pythonu, a to z toho důvodu, že právě v tomto jazyku může dojít ke změně sémantiky algoritmu ve chvíli, kdy se mixují mezery a tabulační značky:

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
from pygments.filters import VisibleWhitespaceFilter
 
code = """
for i in range(1, 11):
\tprint("Hello world!")
if x and y:
        print("yes")
if x or y:
\tprint("dunno")
"""
 
 
print(highlight(code, PythonLexer(), TerminalFormatter()))
 
print("-----------------------")
 
lexer = PythonLexer()
lexer.add_filter(VisibleWhitespaceFilter(tabs=True))
 
print(highlight(code, lexer, TerminalFormatter()))

Příklad použití tohoto typu filtru:

Obrázek 14: Ukázka použití filtru, který dokáže rozpoznat nová bílé znaky (povšimněte si zvýraznění tabů pomocí znaků »).

Poznámka: tento filtr je opět konfigurovatelný. Jeho další možnosti si ukážeme příště.

16. Změna velikosti písmen v klíčových slovech

Třetí standardní filtr, který se jmenuje KeywordCaseFilter, se používá pro změnu velikosti písmen klíčových slov. U některých jazyků nemá větší význam, protože velikost písmen je pevně specifikovaná normou jazyka (C, Java, Python atd.), ale například u Pascalu je možné tento filtr použít pro zobrazení všech zdrojových kódů stejným stylem i ve chvíli, kdy programátoři různým způsobem míchají velká a malá písmena. V dalším příkladu je zdrojový kód napsaný právě v Pascalu vykreslen čtyřmi různými způsoby:

  1. Bez úpravy velikosti písmen
  2. Klíčová slova jsou vypsána verzálkami
  3. Klíčová slova jsou vypsána mínuskami
  4. Klíčová slova mají první písmeno verzálkou a další mínuskou
from pygments import highlight
from pygments.lexers.pascal import DelphiLexer
from pygments.formatters import TerminalFormatter
from pygments.filters import KeywordCaseFilter
 
code = """
procedure change(s:string);
begin
  s[10]:='a';
end;
 
const
  c:packed array [0..1000] of char = 'staticky retezec';
var
  s1:string[10];
  s2:string;
  i:integer;
begin
  s1:='hello';
  writeln('"', s1, '"');
  s1:=s1+' world';
  writeln('"', s1, '"');
  for i:=0 to 1000 do begin
      s2:=s2+'*';
  end;
  change(s2);
  writeln(s2);
end.
"""
 
 
print(highlight(code, DelphiLexer(), TerminalFormatter()))
 
input()
 
print("-----------------------")
 
lexer = DelphiLexer()
lexer.add_filter(KeywordCaseFilter(case='lower'))
 
print(highlight(code, lexer, TerminalFormatter()))
 
input()
 
print("-----------------------")
 
lexer = DelphiLexer()
lexer.add_filter(KeywordCaseFilter(case='upper'))
 
print(highlight(code, lexer, TerminalFormatter()))
 
input()
 
print("-----------------------")
 
lexer = DelphiLexer()
lexer.add_filter(KeywordCaseFilter(case='capitalize'))
 
print(highlight(code, lexer, TerminalFormatter()))

Příklad použití tohoto typu filtru:

<

Obrázek 15: Klíčová slova jsou vytištěna malými písmeny.

Obrázek 16: Klíčová slova jsou vytištěna velkými písmeny.

Obrázek 17: Klíčová slova jsou kapitalizována.

17. Jednoduchý lexer založený na rozpoznávání klíčových slov a regulárních výrazů

Náš první lexer je velmi jednoduchý, protože bude sloužit pro rozpoznání několika klíčových slov. Bude se jednat o třídu nazvanou FooLangLexer, která bude odvozena od třídy RegexLexer, což je lexer založený na rozpoznávání textu s využitím regulárních výrazů. Náš lexer bude v první řadě rozpoznávat čtyři klíčová slova for, while, begin a end (regulární výrazy jsou tedy triviální) a pokud bude dané slovo rozpoznáno, zobrazí se stylem odpovídajícím kategorii „Keyword“ (což je standardně zelená barva). Dále bude rozpoznáno jméno funkce print a klíčové slovo goto bude považováno za chybu. Jiný text se zobrazí stylem odpovídajícím kategorii „Generic.Normal“ (což je standardně barva červená):

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),
        ]
    }

Náš lexer bude do obarvovače zařazen následovně:

print(highlight(code, FooLangLexer(), TerminalFormatter()))

S výsledkem:

Obrázek 18: Velmi jednoduchý lexer v akci.

bitcoin školení listopad 24

Poznámka: samozřejmě nám nic nebrání použít nový lexer s libovolným typem formátovače.

Pro úplnost si samozřejmě ukážeme úplný zdrojový kód tohoto příkladu:

from pygments import highlight
from pygments.lexer import RegexLexer
from pygments.token import *
from pygments.formatters import TerminalFormatter
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'.+', Generic.Normal),
        ]
    }
 
 
code = """
for i in range(1, 11)
begin
    print("Hello world!")
end
 
while i < 10
begin
    inc i
    print(i)
end
 
goto 10
"""
 
 
print(highlight(code, FooLangLexer(), TerminalFormatter()))

18. 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 pygments01_basic_usage_html_output.py výstup do HTML souboru https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments01_basic_usa­ge_html_output.py
2 pygments02_basic_usage_fu­ll_html_output.py vytvoření úplného HTML dokumentu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments02_basic_usa­ge_full_html_output.py
3 pygments03_basic_usage_latex_output.py výstup do LaTeXu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments03_basic_usa­ge_latex_output.py
4 pygments04_basic_usage_la­tex_full_output.py vytvoření úplného dokumentu pro LaTeX https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments04_basic_usa­ge_latex_full_output.py
5 pygments05_terminal_formatter.py výstup na 16barevný terminál https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments05_terminal_for­matter.py
6 pygments06_terminal256_formatter.py výstup na 256barevný terminál https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments06_terminal256_for­matter.py
7 pygments07_terminal_true_co­lor_formatter.py výstup na true color terminál https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments07_terminal_tru­e_color_formatter.py
8 pygments08_svg_formatter.py vytvoření SVG s obarveným kódem https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments08_svg_format­ter.py
9 pygments09_transform_to_tokens.py výpis tokenizované podoby programu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments09_transfor­m_to_tokens.py
10 pygments10_transform_to_to­kens_bad_syntax.py výpis tokenizované podoby programu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments10_transfor­m_to_tokens_bad_syntax.py
11 pygments11_highlight_source.py skript pro obarvení kódu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments11_highlight_sou­rce.py
12 pygments12_source2svg.py skript pro převod do SVG https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments12_source2svg­.py
13 pygments13_terminal_formatter_style.py úprava stylu výstupu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments13_terminal_for­matter_style.py
14 pygments14_ansi_terminal_for­matter_style.py úprava stylu výstupu https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments14_ansi_ter­minal_formatter_style.py
15 pygments15_name_highlight_filter.py použití filtru typu NameHighlightFilter https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments15_name_hig­hlight_filter.py
16 pygments16_visible_whitespace_filter.py použití filtru typu VisibleWhitespaceFilter https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments16_visible_whi­tespace_filter.py
17 pygments17_keyword_case_filter.py použití filtru typu KeywordCaseFilter https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments17_keyword_ca­se_filter.py
18 pygments18_custom_lexer.py vlastní velmi jednoduchý lexer https://github.com/tisnik/pre­sentations/blob/master/pyg­ments/pygments18_custom_le­xer.py

19. Odkazy na Internetu

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

Autor článku

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