PDFLang est conçu pour être lu par des gens qui n’écrivent pas de programmes.
Pas de classes, pas d’héritage, pas de déclarations de types, pas de
points-virgules. Un script est un ensemble de vérifications écrites presque en
langue naturelle.
Par défaut un check en échec est une erreur et l’exécution sort avec 2. Un
check peut se déclarer consultatif :
check"Résolution des images"severity:warning {
require!visual::detect_low_resolution(300)
}
error (par défaut), warning et info. Un avertissement ou une information
ne font pas échouer l’exécution — ils sortent avec 1 et 0 — sauf avec
--fail-on warning, par lequel la CI choisit sa sévérité sans toucher au script.
tags: et severity: peuvent venir dans n’importe quel ordre.
Une erreur d’exécution dans le check — une variable mal orthographiée, un
fichier absent — reste une erreur quoi qu’ait déclaré le check. Un script
cassé n’est pas consultatif.
Toute validation s’écrit avec require ou assert. La seule différence est le
message qui apparaît dans le rapport en cas d’échec.
check"Comparing both forms" {
// require : le message est fabriqué à partir de l'expression elle-même.
// En cas d'échec, le rapport affiche :
// "requirement not met: doc.page_count > 0"
requiredoc.page_count >0
// assert : c'est vous qui écrivez le message que lira le destinataire.
// En cas d'échec, il apparaît tel quel :
// "PDF has no title in its metadata"
assertdoc.title !="", "PDF has no title in its metadata"
}
Règle pratique :require quand l’expression se lit toute seule ; assert
quand la personne qui lira le rapport doit comprendre le problème sans connaître
le script.
assertdoc.page_count >100, "too few pages"// échoue
assertdoc.title !="", "no title"// s'exécute quand même
assertdoc.author !="", "no author"// celle-ci aussi
}
Le rapport liste tous les problèmes d’un coup. C’est volontaire : la
personne qui reçoit le fichier veut la liste complète des corrections, pas une
correction à la fois.
Il en va de même entre les checks — si un check rencontre une erreur d’exécution
(une variable inconnue, par exemple), elle devient un diagnostic et les autres
checks continuent.
// Les unités de longueur sont converties en points (1 pt = 1/72 pouce) :
a=3mm// 8,5039... pt
b=2.5cm // 70,866... pt
c=1in// 72 pt
d=10pt// 10 pt
// Le pourcentage garde la valeur telle quelle :
limite=300%// 300
requirea<b// tout est en points, la comparaison est directe
requirec==72.0
requirelimite==300
}
Pouvoir écrire 3mm au lieu de 8.504 est précisément l’intérêt : cela se lit
naturellement pour qui pense en millimètres, et la conversion ne se trompe pas.
check"Ne vérifier la couverture que s'il y en a une" {
ifdoc.page_count >1 {
requiredoc.pages.first().width >0
}
}
else if s’enchaîne sans accolades supplémentaires :
taille=ifdoc.page_count >500 { "grand" }
elseifdoc.page_count >50 { "moyen" }
else { "petit" }
Trois choses à savoir :
Une branche qui ne s’exécute pas rend null. Un if sans else dont la
condition est fausse vaut null — qui est faux, donc testable directement.
Chaque branche a sa propre portée, comme un bloc ou une fonction. Une
variable créée dans une branche n’existe plus après. Affecter une variable qui
existe déjà à l’extérieur met toujours celle-là à jour.
L’accolade après la condition ouvre le corps. Si la condition se termine
elle-même par un bloc, mettez-la entre parenthèses, sinon l’accolade du corps
serait lue comme celle de ce bloc :
// faux : le { est pris pour le corps du if
// if doc.pages.all { |p| p.width > 0 } { ... }
// juste
if (doc.pages.all { |p|p.width >0 }) {
requiredoc.page_count >0
}
if sert à choisir une valeur ou à protéger une instruction — pas à remplacer
une vérification qui échoue par une vérification muette. Une validation qui doit
échouer échoue toujours ; voir 1.2.
"page #{page.number} has no page number in the footer"
}
Point de syntaxe : si l’expression après on se termine par une propriété
(comme on doc.pages), mettez-la entre parenthèses ; sinon l’accolade du corps
serait prise pour un bloc de cet appel :
interne=page.width // visible seulement dans ce bloc
requireinterne>0
}
requirelocale==42// toujours visible
requireGLOBAL==100// toujours visible
}
L’usage veut des majuscules pour les constantes et des minuscules pour les
variables. Le langage ne l’impose pas, mais les exemples et les profils fournis
suivent cette convention.
--var nom=valeur sur pdfl run, pdfl test et pdfl watch parvient au script
sous la forme vars.nom, toujours en texte. test et watch transmettent la
même valeur à chaque cas ou fichier : un nom de client pour toute l’exécution,
pas un par fichier. C’est ce qui évite qu’un profil devienne cinq copies presque
identiques :
check"Le travail correspond à la commande" {
assertdoc.title.contains(vars.commande),
"le fichier dit \"#{doc.title}\", la commande est #{vars.commande}"
Un nom non transmis est une erreur qui nomme l’option censée le fournir, et
non une chaîne vide : un check comparant à rien passerait, et annoncerait un
fichier que personne n’a validé.