2.5.3 Définition d’une nouvelle commande de markup
Nous allons étudier dans ce qui suit la manière de définir une nouvelle commande de markup.
Syntaxe d’une commande markup | ||
Attribution de propriétés | ||
Exemple commenté | ||
Adaptation d’une commande incorporée | ||
Conversion de markups en chaînes |
Syntaxe d’une commande markup
Une commande de markup personnalisée se définit à l’aide de la
macro Scheme define-markup-command
, placée en tête de fichier.
(define-markup-command (nom-commande layout props arg1 arg2…) (arg1-type? arg2-type?…) [ #:properties ((propriété1 valeur-par-défaut1) …) ] [ #:as-string expression ] …corps de la commande…) |
Quelques commentaires sur les arguments :
-
nom-commande
le nom que vous attribuez à votre commande de markup.
-
layout
la définition du « layout » – son formatage.
-
props
une liste de listes associatives, comprenant toutes les propriétés actives.
-
argi
le ième argument de la commande.
-
argi-type?
un type de prédicat pour le ième argument.
Si la commande utilise des propriétés à partir des arguments
props
, le mot-clé #:properties
permet de spécifier ces
différentes propriétés ainsi que leur valeur par défaut.
Les arguments se distinguent selon leur type :
- un markup, correspondant au type de prédicat
markup?
; - une liste de markups, correspondant au type de prédicat
markup-list?
; - tout autre objet Scheme, correspondant au types de prédicat tels
que
list?
,number?
,boolean?
, etc.
Il n’existe aucune restriction quant à l’ordre des arguments fournis à
la suite des arguments layout
et props
. Néanmoins, les
fonctions markup qui ont en dernier argument un markup ont
ceci de particulier qu’elles peuvent s’appliquer à des listes de
markups ; ceci résultera en une liste de markups où
tous les éléments de la liste originelle se verront appliquer cette
fonction markup avec ses arguments de tête.
La réplication des arguments de tête dans le but d’appliquer une fonction markup à une liste de markups est économique, principalement lorsqu’il s’agit d’arguments Scheme. Vous éviterez ainsi d’éventuelles pertes de performance en utilisant des arguments Scheme en tant qu’arguments principaux d’une fonction markup dont le dernier argument est un markup.
Les commandes de markup ont un cycle de vie relativement
complexe. Le corps de la définition d’une commande de markup est
chargé de convertir les arguments de la commande en expression stencil
qui sera alors renvoyée. Bien souvent, ceci s’accomplit par un appel à
la fonction interpret-markup
, en lui passant les arguments
layout et props. Ces arguments ne seront en principe connus
que bien plus tardivement dans le processus typographique. Lors de
l’expansion d’une expression LilyPond \markup
ou d’une macro
Scheme markup
, les expressions markup auront déjà vu leurs
composants assemblés en expressions markup. L’évaluation et le
contrôle du type des arguments à une commande de markup
n’interviennent qu’au moment de l’interprétation de \markup
ou
markup
.
Seule l’application de interpret-markup
sur une expression
markup réalisera effectivement la conversion des expressions
markup en stencil, au travers de l’exécution du corps des
fonctions markup.
Attribution de propriétés
Les arguments layout
et props
d’une commande de
markup fournissent un contexte à l’interprétation du
markup : taille de fonte, longueur de ligne, etc.
L’argument layout
permet d’accéder aux propriétés définies dans
les blocs \paper
, grâce à la fonction
ly:output-def-lookup
. Par exemple, la longueur de ligne,
identique à celle de la partition, est lue au travers de
(ly:output-def-lookup layout 'line-width)
L’argument props
rend certaines propriétés accessibles aux
commandes de markup. Il en va ainsi lors de l’interprétation
d’un markup de titre d’ouvrage : toutes les variables
définies dans le bloc \header
sont automatiquement ajoutées aux
props
, de telle sorte que le markup de titrage de
l’ouvrage pourra accéder aux différents champs titre, compositeur, etc.
Ceci permet aussi de configurer le comportement d’une commande de
markup : la taille des fontes, par exemple, est lue à
partir de props
plutôt que grâce à un argument font-size
.
La fonction appelant une commande de markup peut altérer la
valeur de la propriété taille des fontes et donc en modifier le
comportement. L’utilisation du mot-clé #:properties
, attaché à
define-markup-command
, permet de spécifier les propriétés devant
être lues parmi les arguments props
.
L’exemple proposé à la rubrique suivante illustre comment, au sein d’une commande de markup, accéder aux différentes propriétés et les modifier.
Exemple commenté
Nous allons, dans cet exemple, nous attacher à encadrer du texte avec un double liseré.
Commençons par construire quelque chose d’approximatif à l’aide d’un
simple markup. La lecture de
Commandes pour markup nous
indique la commande \box
, qui semble ici appropriée.
\markup \box \box HELLO
Dans un souci d’esthétique, nous aimerions que le texte et les
encadrements ne soient pas autant accolés. Selon la documentation de
\box
, cette commande utilise la propriété box-padding
,
fixée par défaut à 0,2. Cette même documentation nous indique
aussi comment la modifier :
\markup \box \override #'(box-padding . 0.6) \box A
L’espacement des deux liserés est cependant toujours trop réduit ; modifions le à son tour :
\markup \override #'(box-padding . 0.4) \box \override #'(box-padding . 0.6) \box A
Vous conviendrez que recopier une telle définition de markup
deviendra vite fastidieux. C’est pourquoi nous écrivons la commande de
markup double-box
qui prendra un seul argument – le
texte. Cette commande se chargera de dessiner les encadrements, en
tenant compte des espacements.
#(define-markup-command (double-box layout props text) (markup?) "Dessine un double encadrement autour du texte." (interpret-markup layout props #{\markup \override #'(box-padding . 0.4) \box \override #'(box-padding . 0.6) \box { #text }#})) |
ou bien son équivalent
#(define-markup-command (double-box layout props text) (markup?) "Dessine un double encadrement autour du texte." (interpret-markup layout props (markup #:override '(box-padding . 0.4) #:box #:override '(box-padding . 0.6) #:box text))) |
text
est le nom de l’argument de notre commande, et
markup?
son type – l’argument sera identifié comme étant un
markup. La fonction interpret-markup
, utilisée dans la
plupart des commandes de markup, construira un stencil à partir
de layout
, props
et un markup. Dans la seconde
variante, ce markup sera construit à l’aide de la macro Scheme
markup
– voir Construction d’un markup en Scheme. La
transformation d’une expression \markup
en expression Scheme est
des plus triviales.
Notre commande personnalisée s’utilise ainsi :
\markup \double-box A
Il serait intéressant de rendre cette commande double-box
plus
souple : les valeurs de box-padding
sont figées et ne
peuvent être modifiées à l’envie. Pareillement, il serait bien de
distinguer l’espacement entre les encadrements de l’espacement entre le
texte et ses encadrements. Nous allons donc introduire une propriété
supplémentaire, que nous appellerons inter-box-padding
, chargée
de gérer l’espacement des encadrements ; box-padding
ne
servira alors que pour l’espacement intérieur. Voici le code adapté à
ces évolutions :
#(define-markup-command (double-box layout props text) (markup?) #:properties ((inter-box-padding 0.4) (box-padding 0.6)) "Dessine un double encadrement autour du texte." (interpret-markup layout props #{\markup \override #`(box-padding . ,inter-box-padding) \box \override #`(box-padding . ,box-padding) \box { #text } #})) |
Ainsi que son équivalent à partir de la macro markup :
#(define-markup-command (double-box layout props text) (markup?) #:properties ((inter-box-padding 0.4) (box-padding 0.6)) "Dessine un double encadrement autour du texte." (interpret-markup layout props (markup #:override `(box-padding . ,inter-box-padding) #:box #:override `(box-padding . ,box-padding) #:box text))) |
C’est ici le mot-clé #:properties
qui permet de lire les
propriétés inter-box-padding
et box-padding
à partir de
l’argumenet props
; on leur a d’ailleurs fourni des valeurs
par défaut au cas où elles ne seraient pas définies.
Ces valeurs permettront alors d’adapter les propriétés de
box-padding
utilisées par les deux commandes \box
. Vous
aurez remarqué, dans l’argument \override
, la présence de
l’apostrophe inversée (`
) et de la virgule ; elles vous
permettent d’insérer une valeur variable au sein d’une expression
littérale.
Notre commande est maintenant prête à servir dans un markup, et les encadrements sont repositionnables.
#(define-markup-command (double-box layout props text) (markup?) #:properties ((inter-box-padding 0.4) (box-padding 0.6)) "Draw a double box around text." (interpret-markup layout props #{\markup \override #`(box-padding . ,inter-box-padding) \box \override #`(box-padding . ,box-padding) \box { #text } #})) \markup \double-box A \markup \override #'(inter-box-padding . 0.8) \double-box A \markup \override #'(box-padding . 1.0) \double-box A
Adaptation d’une commande incorporée
Le meilleur moyen de construire ses propres commandes de markup consiste à prendre exemple sur les commandes déjà incorporées. La plupart des commandes de markup fournies avec LilyPond sont répertoriées dans le fichier ‘scm/define-markup-commands.scm’.
Nous pourrions, par exemple, envisager d’adapter la commande
\draw-line
pour dessiner plutôt une ligne double. Voici comment
est définie la commande \draw-line
, expurgée de sa
documentation :
(define-markup-command (draw-line layout props dest) (number-pair?) #:category graphic #:properties ((thickness 1)) "…documentation…" (let ((th (* (ly:output-def-lookup layout 'line-thickness) thickness)) (x (car dest)) (y (cdr dest))) (make-line-stencil th 0 0 x y))) |
Avant de définir notre propre commande basée sur l’une de celles
fournies par LilyPond, commençons par en recopier la définition, puis
attribuons lui un autre nom. Le mot-clé #:category
peut être
supprimé sans risque ; il ne sert que lors de la génération de la
documentation et n’est d’aucune utilité pour une commande personnalisée.
(define-markup-command (draw-double-line layout props dest) (number-pair?) #:properties ((thickness 1)) "…documentation…" (let ((th (* (ly:output-def-lookup layout 'line-thickness) thickness)) (x (car dest)) (y (cdr dest))) (make-line-stencil th 0 0 x y))) |
Nous ajoutons ensuite une propriété pour gérer l’écart entre les deux
lignes, que nous appelons line-gap
, et lui attribuons une valeur
par défaut de 6 dixièmes :
(define-markup-command (draw-double-line layout props dest) (number-pair?) #:properties ((thickness 1) (line-gap 0.6)) "…documentation…" … |
Nous ajoutons enfin le code qui dessinera nos deux lignes. Deux appels
à make-line-stencil
permettrons de dessiner les lignes dont nous
regrouperons les stencils à l’aide de ly:stencil-add
:
#(define-markup-command (my-draw-line layout props dest) (number-pair?) #:properties ((thickness 1) (line-gap 0.6)) "..documentation.." (let* ((th (* (ly:output-def-lookup layout 'line-thickness) thickness)) (dx (car dest)) (dy (cdr dest)) (w (/ line-gap 2.0)) (x (cond ((= dx 0) w) ((= dy 0) 0) (else (/ w (sqrt (+ 1 (* (/ dx dy) (/ dx dy)))))))) (y (* (if (< (* dx dy) 0) 1 -1) (cond ((= dy 0) w) ((= dx 0) 0) (else (/ w (sqrt (+ 1 (* (/ dy dx) (/ dy dx)))))))))) (ly:stencil-add (make-line-stencil th x y (+ dx x) (+ dy y)) (make-line-stencil th (- x) (- y) (- dx x) (- dy y))))) \markup \my-draw-line #'(4 . 3) \markup \override #'(line-gap . 1.2) \my-draw-line #'(4 . 3)
Conversion de markups en chaînes
Les markups sont parfois convertis en chaînes littérales, ce qui
est le cas pour générer une métadonnée PDF basée sur le champ d’entête
title
ou pour convertir des paroles en MIDI. Cette conversion
n’est pas parfaite, mais elle est aussi fidèle que possible. La fonction
dévolue à cette opération est markup->string
.
composerName = \markup \box "Arnold Schönberg" \markup \composerName \markup \typewriter #(markup->string composerName)
Dans le cas de commandes de markup personnalisées, le comportement par défaut consiste à convertir dans un premier temps tous les markups ou les arguments de la liste de markups, puis à joindre les résultats par des espaces.
#(define-markup-command (authors-and layout props authorA authorB) (markup? markup?) (interpret-markup layout props #{ \markup \fill-line { \box #authorA and \box #authorB } #})) defaultBehavior = \markup \authors-and "Bertolt Brecht" "Kurt Weill" \markup \defaultBehavior \markup \typewriter #(markup->string defaultBehavior)
markup->string
peut aussi recevoir les argument nommés
#:layout layout
et #:props props
, avec la même
signification que lorsqu’ils apparaissent dans la définition d’une
commande de markup. Ils sont toutefois optionnels dans la mesure
où ils peuvent ne pas toujours être pourvus – cas typique de l’argument
layout
pour convertir en MIDI.
Afin de définir une conversion différente du comportement par
défaut dans une commande pour markup personnalisée, on peut
fournir le paramètre #:as-string
à
define-markup-command
. Il attend une expression, évaluée
par markup->string
, afin de produire la chaîne.
#(define-markup-command (authors-and layout props authorA authorB) (markup? markup?) #:as-string (format #f "~a and ~a" (markup->string authorA #:layout layout #:props props) (markup->string authorB #:layout layout #:props props)) (interpret-markup layout props #{ \markup \fill-line { \box #authorA and \box #authorB } #})) customized = \markup \authors-and "Bertolt Brecht" "Kurt Weill" \markup \customized \markup \typewriter #(markup->string customized)
Au sein de l’expression sont disponibles les mêmes variables que dans le
corps de la commande de markup, à savoir les argument de la
commande, ainsi que layout
et props
, et les éventuelles
propriétés.
#(define-markup-command (authors-and layout props authorA authorB) (markup? markup?) #:properties ((author-separator " and ")) #:as-string (format #f "~a~a~a" (markup->string authorA #:layout layout #:props props) (markup->string author-separator #:layout layout #:props props) (markup->string authorB #:layout layout #:props props)) (interpret-markup layout props #{ \markup { \box #authorA #author-separator \box #authorB } #})) customized = \markup \override #'(author-separator . ", ") \authors-and "Bertolt Brecht" "Kurt Weill" \markup \customized \markup \typewriter #(markup->string customized)
La plupart du temps, il suffira au gestionnaire personnalisé d’un appel
récursif à markup->string
sur certains arguments, comme illustré
ci-dessus. Néanmoins, on peut toujours recourir directement à
layout
et props
comme si l’on était dans le corps
principal. Une attention particulière doit être portée au fait que, dès
lors que #:layout
n’est pas passé à markup->string
,
l’argument layout
égale #f
. L’argument props
est
par défaut une liste vide.