Lexer Pygments
Lexer Pygments pour coloration syntaxique du code Catnip.
Vue d'ensemble
Le module catnip.tools.pygments fournit un lexer Pygments qui permet la coloration syntaxique du code Catnip dans tous
les outils compatibles Pygments : générateurs de documentation (Sphinx, MkDocs), éditeurs de texte, outils de diff,
sites web, etc.
Fichier : catnip/tools/pygments.py Classe : CatnipLexer Génération : Auto-généré depuis la grammaire
Tree-sitter - NE PAS ÉDITER MANUELLEMENT
Le lexer est synchronisé automatiquement avec la grammaire pour garantir que les mots-clés, opérateurs et syntaxe restent cohérents avec le parser Tree-sitter.
Installation
Package Pygments
Le lexer est inclus dans le package Catnip et enregistré automatiquement via entry_points dans setup.py :
entry_points={
'pygments.lexers': [
'catnip = catnip.tools.pygments:CatnipLexer',
],
}
Après installation de Catnip, le lexer est disponible globalement :
pip install catnip
pygmentize -l catnip script.cat
Vérification
# Lister les lexers installés
pygmentize -L lexers | grep -i catnip
# Tester la coloration
echo 'x = (n) => { n * 2 }' | pygmentize -l catnip -f terminal
Utilisation
CLI avec pygmentize
# Coloration terminal
pygmentize -l catnip script.cat
# Export HTML
pygmentize -l catnip -f html -o script.html script.cat
# Export HTML avec CSS inline
pygmentize -l catnip -f html -O full,style=monokai script.cat > script.html
# Export LaTeX
pygmentize -l catnip -f latex script.cat > script.tex
Sphinx
Configuration conf.py :
# Pygments est utilisé automatiquement pour la coloration
pygments_style = 'monokai'
# Dans les fichiers .rst
.. code-block:: catnip
factorial = (n) => {
if n <= 1 { return 1 }
else { return n * factorial(n - 1) }
}
MkDocs
Configuration mkdocs.yml :
markdown_extensions:
- pymdownx.highlight:
use_pygments: true
- pymdownx.superfences
Fichiers Markdown :
```catnip
numbers = list(1, 2, 3, 4, 5)
doubled = numbers.[* 2]
print(doubled)
```
GitLab/GitHub
GitLab et GitHub n'utilisent pas Pygments nativement, mais on peut rendre le code lisible :
```python
# GitLab/GitHub n'ont pas de support Catnip natif
# On utilise 'python' comme fallback pour une coloration partielle
factorial = (n) => {
if n <= 1 { return 1 }
else { return n * factorial(n - 1) }
}
```
Pour une vraie coloration sur ces plateformes, il faut contribuer au projet github/linguist.
Programmatique
from pygments import highlight
from pygments.formatters import TerminalFormatter, HtmlFormatter
from catnip.tools.pygments import CatnipLexer
code = """
factorial = (n) => {
if n <= 1 { return 1 }
else { return n * factorial(n - 1) }
}
print(factorial(5))
"""
# Terminal colorisé
lexer = CatnipLexer()
formatter = TerminalFormatter()
result = highlight(code, lexer, formatter)
print(result)
# HTML
formatter = HtmlFormatter(style='monokai', full=True)
html = highlight(code, lexer, formatter)
with open('output.html', 'w') as f:
f.write(html)
Tokens supportés
Le lexer reconnaît les catégories de tokens suivantes :
Commentaires
# Ceci est un commentaire
x = 42 # commentaire inline
- Type Pygments :
Comment.Single - Pattern :
#.*?$
Mots-clés
Contrôle de flux : if, elif, else, for, while, match, return, break, continue
if x { print("yes") }
for i in range(10) { print(i) }
match x { 1 => { "one" } }
- Type :
Keyword
Opérateurs logiques : and, or, not, in
if x and y { print("both") }
if not z { print("nope") }
- Type :
Keyword
Pragmas : pragma
pragma("tco", True)
- Type :
Keyword
Constantes
True, False, None
x = True
y = None
- Type :
Keyword.Constant
Types builtin
list, dict, set, tuple
numbers = list(1, 2, 3)
mapping = dict()
- Type :
Keyword.Type
Chaînes de caractères
Strings simples et triples
text = "hello"
long_text = """
multi
line
"""
- Type :
String
F-strings
name = "Alice"
greeting = f"Hello, {name}!"
- Type :
String - Pattern :
(?:f|F)("""…""" | '''…''' | "…" | '…')
Nombres
Entiers décimaux : 42, 1000
x = 42
- Type :
Number.Integer
Binaires : 0b1010
flags = 0b11010110
- Type :
Number.Bin
Octaux : 0o755
permissions = 0o755
- Type :
Number.Oct
Hexadécimaux : 0xFF, 0xDEADBEEF
color = 0xFF5733
- Type :
Number.Hex
Flottants : 3.14, 1e-5, 2.5e10
pi = 3.14159
epsilon = 1e-10
- Type :
Number.Float
Opérateurs
Arithmétiques : +, -, *, /, //, **, %
x = 2 ** 10
y = 17 % 5
- Type :
Operator
Comparaison : ==, !=, <, >, <=, >=
if x >= 10 { print("big") }
- Type :
Operator
Bitwise : &, |, ^, ~, <<, >>
flags = 0b1010 | 0b0101
- Type :
Operator
Spéciaux : =, =>
x = 42
f = (n) => { n * 2 }
- Type :
Operator
Ponctuations
{, }, (, ), [, ], ,, :, ;, .
func(a, b, c)
list[0]
{ x = 1; y = 2 }
- Type :
Punctuation
Identifiants
Noms de variables, fonctions, paramètres
factorial = (n) => { n * factorial(n - 1) }
- Type :
Name
États du lexer
Le lexer utilise deux états pour gérer correctement le broadcasting :
État root
État par défaut pour le code normal.
x = 42
y = list(1, 2, 3)
Tous les tokens standards sont reconnus.
État broadcast
État spécial activé après .[ pour gérer les expressions de broadcasting.
numbers.[* 2]
data.[if > 10]
Le lexer entre dans cet état quand il rencontre .[ et en sort sur ]. Cela permet de parser correctement les
expressions complexes dans le broadcast, y compris :
- Nested brackets :
data.[[x, y]] - Nested braces :
data.[{ x = compute(); x * 2 }] - Keywords comme
if:data.[if condition]
Le lexer entre en mode broadcast quand il voit
.[, ce qui déclenche un changement d'état qui affecte… le lexer lui-même. C'est un lexer qui se reconfigure en temps réel en fonction de ce qu'il lit. Une forme d'auto-modification contrôlée, parfaitement prévisible et totalement délibérée.
Régénération du lexer
Le lexer est auto-généré depuis la grammaire Tree-sitter. Il ne faut jamais l'éditer manuellement.
Quand régénérer ?
Régénérer après toute modification de catnip_rs/grammar/grammar.js qui affecte :
- Les mots-clés (
if,while,match, etc.) - Les opérateurs (
+,=>,.[, etc.) - Les terminaux de base (nombres, strings, identifiants, etc.)
Commande de régénération
python -m catnip.tools.extract_grammar --update-lexer
Cela :
- Lit
catnip_rs/grammar/src/grammar.json(généré par Tree-sitter) - Extrait keywords, operators, terminals
- Génère
catnip/tools/pygments.pyavec les règles de tokenisation - Écrase le fichier existant
Workflow complet
# 1. Modifier la grammaire Tree-sitter
vim catnip_rs/grammar/grammar.js
# 2. Regénérer le parser Tree-sitter
cd catnip_rs/grammar && npx tree-sitter generate && cd ../..
# 3. Régénérer le lexer Pygments
python -m catnip.tools.extract_grammar --update-lexer
# 4. Tester
echo 'x = (n) => { n * 2 }' | pygmentize -l catnip -f terminal
# 5. Commit (si pertinent)
git add catnip/tools/pygments.py catnip_rs/grammar/
git commit -m "update grammar and regenerate lexer"
Le lexer se génère depuis la grammaire, qui elle-même décrit comment parser le code qui génère… le lexer. C'est une boucle d'auto-génération dont la sortie valide l'entrée qui a permis de la produire. Le serpent qui se mord la queue, version outillage compilateur.
Architecture interne
Le lexer utilise RegexLexer de Pygments avec des règles de correspondance par expressions régulières.
Structure de base
class CatnipLexer(RegexLexer):
name = 'Catnip'
aliases = ['catnip', 'cat']
filenames = ['*.cat', '*.catnip']
mimetypes = ['text/x-catnip']
tokens = {
'root': [
# Liste de patterns (regex, token_type)
(r'#.*?$', Comment.Single),
(r'\s+', Whitespace),
# …
],
'broadcast': [
# Patterns spécifiques au broadcast
(r'\]', Punctuation, '#pop'), # Sort de l'état
# …
],
}
Ordre des patterns
Critique : Les patterns sont évalués dans l'ordre de définition. Les plus spécifiques doivent venir avant les plus génériques.
# BON : f-strings avant strings normales
(r'[fF]"(?:[^"\\]|\\.)*"', String),
(r'"(?:[^"\\]|\\.)*"', String),
# BON : opérateurs multi-char avant single-char
(r'(>>|>=|==|<=|<<|//|\*\*|!=|…)', Operator),
(r'[+\-*/%]', Operator), # Après !
# MAUVAIS : identifiants avant keywords
(r'[a-zA-Z_]\w*', Name), # Matche "if", "while", etc.
(words(('if', 'while', …), suffix=r'\b'), Keyword) # Jamais atteint !
Le générateur gère cet ordre automatiquement.
Exemples de patterns
Keywords avec word boundary
(
words(
('if', 'while', 'for', …),
suffix=r'\b', # Boundary pour éviter "iffy" ou "whilex"
),
Keyword,
)
F-strings multi-formats
(
r'(?:f|F)("""(?:[^"\\]|\\.)*?"""' # Triple quotes
r"|'''(?:[^'\\]|\\.)*?'''" # Triple single quotes
r'|"(?:[^"\\]|\\.)*"' # Double quotes
r"|'(?:[^'\\]|\\.)*')", # Single quotes
String,
)
Nombres avec bases multiples
(r'0[bB][01]+', Number.Bin), # Binaire : 0b1010
(r'0[oO][0-7]+', Number.Oct), # Octal : 0o755
(r'0[xX][0-9a-fA-F]+', Number.Hex), # Hexa : 0xFF
(r'\d+\.\d+([eE][+-]?\d+)?', Number.Float), # Float : 3.14, 1e-5
(r'\d+[eE][+-]?\d+', Number.Float), # Scientific : 2e10
(r'\d+', Number.Integer), # Décimal : 42
Gestion du broadcasting
Le broadcast .[expression] nécessite un état séparé pour gérer correctement les nested structures.
Transition root → broadcast
# Dans 'root'
(r'\.\[', Punctuation, 'broadcast'), # Push état 'broadcast'
Sortie broadcast → root
# Dans 'broadcast'
(r'\]', Punctuation, '#pop'), # Pop état, retour à 'root'
Cela permet de parser correctement :
data.[{ if x > 10 { [x, y] } else { [0, 0] } }]
# ^ ^ état 'broadcast'
# push pop
Le lexer bascule d'état quand il rencontre un délimiteur qui change les règles de parsing pour la suite du code. C'est comme si le lexer disait : « Bon, maintenant je lis du broadcast, donc je change mes lunettes ». Sauf que les lunettes sont une table de regex et le changement est un push/pop de stack. Métaphore parfaitement technique.
Limitations connues
-
Pas de support nativement dans GitHub/GitLab : Ces plateformes utilisent leurs propres systèmes de coloration (Linguist, Rouge). Il faut contribuer à ces projets séparément.
-
Highlighting partiel dans certains éditeurs : Certains éditeurs (Sublime Text, Atom) nécessitent des plugins spécifiques qui ne se basent pas uniquement sur Pygments.
-
Pas de semantic highlighting : Le lexer fait du tokenisation purement syntaxique. Il ne distingue pas :
-
Variables vs fonctions (tous →
Name) - Fonctions builtin vs user-defined
-
Scope ou résolution de noms
-
Edge cases de broadcasting : Les nested broadcasts avec quotes complexes peuvent parfois matcher incorrectement :
```python # OK data.[if > 10]
# Edge case : nested f-string dans broadcast data.[f"{x if x > 10 else 0}"] # Peut confondre le ] ```
Ces cas sont rares en pratique et n'affectent pas la majorité du code.
Un lexer qui reconnaît tous les tokens sauf quelques edge cases rarissimes, c'est comme un système de tri postal qui fonctionne parfaitement sauf pour les colis qui contiennent d'autres colis qui contiennent des adresses. Techniquement imparfait, pratiquement suffisant, philosophiquement acceptable.
Intégration dans les outils
VSCode / TextMate
VSCode n'utilise pas Pygments mais TextMate grammars. Pour VSCode, il faut créer une extension séparée avec un fichier
.tmLanguage.json.
Voir : VSCode Language Extensions
Vim / Neovim
Vim utilise ses propres syntaxes (.vim files). Le lexer Pygments ne s'applique pas directement, mais peut servir de
référence pour créer un syntax/catnip.vim.
Exemple de conversion manuelle :
" syntax/catnip.vim
syn keyword catnipKeyword if elif else while for match return break continue
syn keyword catnipConstant True False None
syn keyword catnipOperator and or not in
syn match catnipComment "#.*$"
syn region catnipString start='"' end='"' skip='\\"'
Emacs
Emacs utilise des modes majeurs avec des règles de fontification. Le lexer Pygments peut inspirer un catnip-mode.el.
JupyterLab / Notebook
JupyterLab utilise CodeMirror pour la coloration, pas Pygments. Il faut créer un mode CodeMirror séparé.
Voir : CodeMirror Language Modes
Exemples complets
Script complet avec coloration
# Fibonacci avec memoization
fib_cache = dict()
fib = (n) => {
if n in fib_cache {
return fib_cache[n]
}
if n <= 1 {
result = n
} else {
result = fib(n - 1) + fib(n - 2)
}
fib_cache[n] = result
return result
}
# Calcul
numbers = list(0, 1, 2, 3, 4, 5, 10, 15, 20)
results = numbers.[fib]
# Affichage
for i in range(len(numbers)) {
n = numbers[i]
f = results[i]
print(f"fib({n}) = {f}")
}
Tokens reconnus :
- Comments :
# Fibonacci avec memoization,# Calcul,# Affichage - Identifiers :
fib_cache,fib,n,result,numbers,results,i,f - Keywords :
if,else,for,in,return - Types :
dict,list - Operators :
=,=>,+,- - Punctuations :
{,},(,),[,],, - Strings :
f"fib({n}) = {f}" - Numbers :
0,1,2,3,4,5,10,15,20
Export HTML styled
from pygments import highlight
from pygments.formatters import HtmlFormatter
from catnip.tools.pygments import CatnipLexer
code = open('script.cat').read()
lexer = CatnipLexer()
formatter = HtmlFormatter(
style='monokai',
full=True,
linenos='table',
title='Catnip Script'
)
html = highlight(code, lexer, formatter)
with open('output.html', 'w') as f:
f.write(html)
Résultat : Un fichier HTML autonome avec CSS inline, numéros de ligne en table, thème Monokai.
Documentation Sphinx
docs/conf.py :
extensions = ['sphinx.ext.autodoc']
pygments_style = 'monokai'
docs/examples.rst :
Exemples Catnip
===============
Factorial
---------
.. code-block:: catnip
factorial = (n) => {
if n <= 1 { return 1 }
else { return n * factorial(n - 1) }
}
print(factorial(5)) # 120
Compilation :
sphinx-build -b html docs/ docs/_build/
Le code Catnip est automatiquement colorisé via le lexer Pygments.
Documentation qui génère du HTML à partir de RST qui embed du code Catnip qui est parsé par un lexer généré depuis une grammaire qui parse le code Catnip. Le cycle complet de la documentation autoréférentielle : le code s'explique lui-même via des outils qu'il génère pour se documenter. Meta-circulaire.