% \iffalse meta-comment
%
% This program can be distributed and/or modified under the terms
% of the LaTeX Project Public License either version 1.3c of this
% license or (at your option) any later version.
% The latest version of this license is in
%    http://www.latex-project.org/lppl.txt
% and version 1.3c or later is part of all distributions of LaTeX
% version 2005/12/01 or later.
%
% This file has the LPPL maintenance status "maintained".
%
%    Lua-Typo package for LaTeX version 2e
%
%    Copyright © 2020-2026 by Daniel Flipo
%
%    Please report errors to: daniel (dot) flipo (at) free (dot) fr
%
%<*batch>
%<*gobble>
\ifx\jobname\relax\let\documentclass\undefined\fi
\ifx\documentclass\undefined
\csname fi\endcsname
%</gobble>
\input docstrip.tex
\keepsilent
\let\MetaPrefix\relax
\preamble
\endpreamble
\postamble
\endpostamble
\let\MetaPrefix\DoubleperCent
\askforoverwritefalse
\generate{%
     \file{lua-typo.sty}{\from{lua-typo.dtx}{sty}}%
     \nopreamble
     \file{lua-typo.cfg}{\from{lua-typo.dtx}{cfg}}%
     \file{lua-typo-fr.ltx}{\from{lua-typo.dtx}{driver,docfr}}%
     \file{lua-typo.ltx}{\from{lua-typo.dtx}{driver,doc}}%
    }
\iffalse
   \generate{%
     \file{lua-typo.sty}{\from{lua-typo.dtx}{sty,dbg}}%
     \file{scan-page.sty}{\from{lua-typo.dtx}{scan}}%
    }
\fi
\endbatchfile
%</batch>
%<*gobble>
\fi
\expandafter\ifx\csname @currname\endcsname\empty
\csname fi\endcsname
%</gobble>
%<*driver>
%<+doc>\DocumentMetadata{pdfstandard={UA-2,A-4f}, lang=en-GB}
%<+doc>\documentclass[a4paper,british]{ltxdoc}
%<+docfr>\DocumentMetadata{pdfstandard={UA-2,A-4f}, lang=fr-FR}
%<+docfr>\documentclass[a4paper,french]{ltxdoc}
\usepackage{fontspec}
\setmainfont{erewhon}
\setsansfont{Cabin}[Scale=MatchLowercase]
\setmonofont{MonaspaceNeon}[%
    Scale = MatchLowercase,   FakeStretch = .81,
    HyphenChar=None,          Color=5D1D00,
    Extension = .otf,
    UprightFont = *-Regular,
    ItalicFont = *-Italic,
    BoldFont = *-ExtraBold,
    BoldFontItalic = *-ExtraBoldItalic,
  ]
\usepackage[expansion=true, protrusion=true]{microtype}
\usepackage{babel,varioref}
\usepackage[ShortPages, OverfullLines, UnderfullLines,
            EOPHyphens, RepeatedHyphens, % Widows, Orphans,
           ]{lua-typo}
%\renewcommand*\descriptionlabel[1]{%
%   \hspace{\labelsep}\texttt{#1}}
\usepackage{array,url,verbatim}
\usepackage[numbered]{hypdoc}
\hypersetup{colorlinks,urlcolor=blue,unicode}
%
%<+docfr>\OnlyDescription
%<+docfr>\let\FrenchDoc\begingroup\let\endFrenchDoc\endgroup
%<+doc|docfr>\let\Debugging\comment\let\endDebugging\endcomment
%<*doc>
\let\FrenchDoc\comment\let\endFrenchDoc\endcomment
\RecordChanges
\AtEndDocument{%
  \clearpage
  \section{Change History}%
   \GlossaryPrologue{}%
   Changes are listed in reverse order (latest first)
   from version~0.30.
   \PrintChanges
}
%</doc>
\newcommand*\texdir[1]{\textsc{#1}}
\newcommand*\file[1]{\texttt{#1}}
\newcommand*\pkg[1]{\texttt{#1}}
\newcommand*\opt[1]{\texttt{#1}}
\renewcommand\meta[1]{\texttt{\textsl{<#1>}}}
\newcommand*\node[1]{\textsc{#1}}
%
\setlength{\parindent}{0pt}
\setlength{\parskip}{.3\baselineskip plus 0.3pt minus 0.3pt}
\begin{document}
\GetFileInfo{lua-typo.sty}
\DocInput{lua-typo.dtx}
\end{document}

%%% Local Variables:
%%% coding: utf-8
%%% TeX-engine: luatex
%%% End:
%</driver>
%<*gobble>
\fi
%</gobble>
% \fi
%
% \begin{FrenchDoc}
% \begin{center}
%   \textbf{\Large Recherche d’imperfections typographiques\\[3pt]
%           avec LuaLaTeX}
%   \\[.5\baselineskip]^^A\]
%   {\large Daniel Flipo}\\
%   \texttt{daniel.flipo@free.fr}
% \end{center}
%
% \section{De quoi s’agit-il ?}
%
%    L’extension \pkg{lua-typo} décrite ci-dessous%
%    \footnote{Version \fileversion, mise à jour le \filedate.}
%    permet de mettre en lumière par un changement de couleur, les
%    lignes potentiellement imparfaites d’un fichier PDF produit par
%    LuaLaTeX ; comme son nom l’indique elle ne fonctionne qu’avec
%    LuaLaTeX. Une liste des pages concernées est affichée à la fin
%    du fichier \file{.log}, permettant un accès rapide aux pages
%    incriminées. \pkg{lua-typo} crée également un fichier de suffixe
%    \file{.typo} regroupant les informations (type, page, ligne) sur
%    les imperfections relevées.
%
%    Normalement, c’est-à-dire lorsque la justification n’est pas trop
%    étroite, (Lua)TeX fait du bon travail, surtout si \pkg{microtype}
%    est utilisé mais il peut rester des points à vérifier, notamment
%    des lignes trop pleines ou lavées (\emph{Overfull, Underfull box}),
%    des veuves et des orphelines, des mots coupés en fin de page ou
%    d’alinéa ou sur plusieurs lignes consécutives, des dernières lignes
%    d’alinéa trop courtes ou presque pleines, des pages quasi vides.
%    La répétition d’un même mot ou partie de mot au début ou à la fin
%    de deux lignes consécutives est aussi détectée.
%    La présence en fin de ligne de certains mots très courts (une ou
%    deux lettres, liste dépendant de la langue) peut également être
%    recherchée.
%
%    \textbf{Important :} a) les lignes totalement ou partiellement
%    coloriées par \pkg{lua-typo} le sont uniquement pour
%    \emph{attirer l’attention} du correcteur à qui il appartient
%    de décider si ces lignes doivent être remaniées ou non.
%    Certains « défauts » peuvent être acceptables dans certaines
%    conditions (multicolonnage, documents techniques) et pas dans
%    d’autres, œuvres litéraires par exemple.
%    Seul un humain entrainé peut décider si une ligne légèrement lavée
%    est acceptable ou non, ou si la suppression d’une coupure malvenue
%    ne va pas provoquer d’autres désordres bien plus graves.\\
%    b) Inversement, il n’est pas exclu que \pkg{lua-typo} comporte
%    des bogues l’empêchant de détecter des lignes potentiellement
%    imparfaites. Depuis la version~0.85, le fichier \file{.typo}
%    fournit, s’il y a lieu, une liste de pages « suspectes », celles où
%    aucune ligne de texte n’a été trouvée.  L’avertissement peut être
%    anodin (page contenant uniquement des illustrations par exemple) ou
%    révéler un défaut de détection.\\
%    c) Les vérifications de \pkg{lua-typo} sur les boîtes de type
%    minipages ou parbox ou marginpar ne portent que sur les
%    « Overfull/Underfull boxes ».
%
%    \pkg{lua-typo} est largement configurable et devrait pouvoir
%    s’adapter aux exigences variables des auteurs ou correcteurs :
%    voir ci-dessous la liste des options et le fichier de configuration
%    \file{lua-typo.cfg}.
%
%    \pkg{lua-typo} ne corrigeant aucun des « défauts » relevés, quels
%    sont les moyens à notre disposition pour le faire manuellement ?
%    Reformuler une phrase est souvent efficace, un auteur peut se le
%    permettre, un correcteur ne le peut pas. Il est possible de jouer
%    sur l’espace inter-mots grâce à la commande TeX \cs{spaceskip},
%    ou sur l’interlettrage grâce à la commande \cs{textls} de
%    \pkg{microtype} mais dans les deux cas, le faire
%    \emph{avec parcimonie}\footnote{%
%    Le but ultime est de parvenir à un « gris typographique »,
%    aussi parfait que possible ; l’argument optionnel de \cs{textls} ne
%    devrait guère sortir de l’intervalle $[-5,+15]$ (unité
%    1/1000\up{e}\,\texttt{em}) et \cs{spaceskip} devrait rester très
%    proche de l’espace-mot standard déterminée par les \cs{fontdimen}.}
%    pour que le remède ne soit pas pire que le mal.
%    Un \emph{léger} accroissement de l’espace inter-mots ou de
%    l’interlettrage dans un groupe de mots peut rendre acceptable la
%    dernière ligne initialement trop courte d’un paragraphe ou, si elle
%    était presque pleine, ajouter une ligne, ce qui peut permettre
%    de supprimer une orpheline.
%    De même une légère contraction peut supprimer la dernière ligne
%    (courte) d’un paragraphe et éviter une veuve en début de la page
%    suivante.
%
%    Je conseille de n’appliquer \pkg{lua-typo} que sur des textes
%    « presque au point », d’améliorer ce qui peut l’être puis de
%    \emph{supprimer} l’appel à \pkg{lua-typo} afin de ne pas risquer
%    de mettre en lumière les imperfections que l’on aura renoncé
%    à corriger. Pour appliquer toutes les vérifications proposées par
%    \pkg{lua-typo}, il suffit d’ajouter dans le préambule la ligne %\\
%    |\usepackage[All]{lua-typo}|
%
%    La version courante (\fileversion) nécessite un noyau LaTeX récent,
%    2022/06/01 ou ultérieur. Ceux qui ne disposent que d’un noyau
%    antérieur à 2021/06/01 reçoivent un message d’erreur
%    «\texttt{Unable to register callback}» ; une version «rollback »
%    est prévue à leur intention, elle se charge par la commande
%    |\usepackage[All]{lua-typo}[=v0.4]|. Une autre version
%    intermédiaire est présente, elle se charge avec l’option |[=v0.65]|.
%
%    Les fichiers \file{demo.tex} et \file{demo.pdf} fournissent un
%    exemple du traitement opéré par \pkg{lua-typo}.
%
%    Un grand merci à Jacques André et Thomas Savary pour avoir accepté
%    de tester les pré-versions et pour leurs retours riches et toujours
%    pertinents ; leurs suggestions et leurs encouragements ont
%    grandement contribué à améliorer la première version mise en ligne.
%    Merci également à Michel Bovani pour ses remarques et suggestions
%    qui ont conduit à la version~0.61.
%
%  \section{Utilisation}
%
%    Comme indiqué plus haut la vérification la plus complète s’obtient
%    par :\\
%    |\usepackage[All]{lua-typo}|
%
%    Il est possible de choisir les tests à activer de deux
%    manières, soit « tout sauf … » soit « seulement ceci et cela ».
%    Pour tout activer sauf les options \meta{OptX} et \meta{OptY} :\\
%    |\usepackage[All, |\meta{OptX}|=false, |\meta{OptY}|=false]{lua-typo}|\\
%    ou pour se limiter aux tests \meta{OptX} et \meta{OptY} :\\
%    |\usepackage[|\meta{OptX}|, |\meta{OptY}|]{lua-typo}|
%
%    La liste des options et le type des vérifications proposées sont
%    présentés dans le tableau \vpageref{options-fr}.
%    Par exemple, pour limiter les vérifications aux lignes trop pleines
%    ou creuses, il suffit de coder :\\
%    |\usepackage[OverfullLines, UnderfullLines]{lua-typo}|\\
%    Pour tout vérifier sauf les coupures répétées en fin de ligne on
%    codera :\\
%    |\usepackage[All, RepeatedHyphens=false]{lua-typo}|\\
%    Notez que l’option that \opt{All} doit être la première de la
%    liste, les suivantes étant rétirées de la liste complète définie
%    par~\opt{All}.
%
%    \begin{table}[ht]
%      \centering\label{options-fr}
%    \begin{tabular}{>{\ttfamily}ll}
%      \multicolumn{1}{l}{Nom} & Imperfection à signaler\\ \hline
%      All             & Active toutes les options ci-dessous\\
%      ShortLines      & Dernière ligne d’alinéa trop courte ?\\
%      BackParindent   & Dernière ligne d’alinéa \emph{presque} pleine ?\\
%      ShortPages      & Page quasi vide (quelques lines) ?\\
%      OverfullLines   & Ligne trop pleine ?\\
%      UnderfullLines  & Ligne lavée ? \\
%      Widows          & Veuve (haut de page) ?\\
%      Orphans         & Orpheline (bas de page) ?\\
%      EOPHyphens      & Mot coupé en bas de page ?\\
%      RepeatedHyphens & Coupures sur trop de lignes consécutives ?\\
%      ParLastHyphen   & Coupure à l’avant-dernière ligne d’un alinéa ?\\
%      EOLShortWords   & Mots courts (1 or 2 lettres) en fin de ligne ?\\
%      FirstWordMatch  & Même (partie de) mot en début de lignes
%                        consécutives ?\\
%      LastWordMatch   & Même (partie de) mot en fin de lignes
%                        consécutives ?\\
%      FootnoteSplit   & Fin de note de bas de page sur page suivante?\\
%      ShortFinalWord  & Mot de fin de phrase court en haut de page\\
%      MarginparPos    & Note marginale se terminant trop bas\\
%      \hline
%    \end{tabular}
%    \end{table}
%
%    Le nom des différentes options n’étant pas facile à mémoriser, il
%    est possible de les retrouver sans devoir consulter la
%    documentation ; l’option \opt{ShowOptions} affiche la liste
%    complète dans le fichier \file{.log} :
%    |\usepackage[ShowOptions]{lua-typo}|
%
%    L’option \opt{None}, empêche toute vérification :
%    |\usepackage[None]{lua-typo}|
%    a pour effet de supprimer complètement tout ajout de code LuaTeX
%    (aucune fonction n’est ajoutée aux \emph{callbacks} de LuaTeX).
%    Cette option peut-être utile lors de la toute dernière compilation,
%    elle n’est pas tout-à-fait équivalente à la mise en commentaire de la
%    ligne car les variables utilisées par \pkg{luatypo} restent
%    définies ; si certaines ont été modifiées dans le préambule aucun
%    message d’erreur du type ``\emph{Undefined Control Sequence}’’ ne
%    sera émis à leur sujet.
%
%    Terminons par quelques précisions sur ces options.
%    \begin{description}
%    \item[\opt{FirstWordMatch :}] les répétitions en début de ligne dans les
%      listes ne sont pas signalées. Ceci est voulu car elles résultent
%      d’un choix délibéré de l’auteur.
%    \item[\opt{ShortPages :}] lorsque le nombre de lignes d’une page est jugé
%      insuffisant (voir ci-dessous), seule la dernière ligne de celle-ci
%      est mise en couleur.
%    \item[\opt{RepeatedHyphens :}] de même, lorsque le nombre de lignes
%      consécutives affectées par des coupures dépasse le seuil fixé
%      (voir ci-dessous), ne sont coloriées que les coupures en excès.
%    \item[\opt{ShortFinalWord :}] lorsque le premier mot de la première ligne
%      d’une page termine une phrase et qu’il est court (au plus
%      |\luatypoMinLen=4| lettres), on le signale.
%    \end{description}
%
%    \section{Incompatibilités connues}
%
%    La version actuelle de \pkg{lua-typo} est incompatible avec
%    l’extension \pkg{reledmac}. Si celle-ci est chargée, aucune
%    vérification n’est effectuée par \pkg{lua-typo}, l’utilisateur
%    en est informé par un message dans le fichier \file{.log}.
%
%    \section{Paramétrage personnalisé}
%
%    Pour certaines vérifications faites par \pkg{lua-typo} un
%    paramétrage est nécessaire : à partir de quelle limite une dernière
%    ligne d’alinéa est-elle considérée comme trop courte ?
%    Combien de coupures consécutives en bout de ligne sont-elles
%    acceptables ? Ces réglages dépendent évidemment du contexte, un
%    correcteur de romans aura des exigences plus strictes qu’un auteur
%    de documentation technique par exemple…
%
%    \pkg{lua-typo} permet de modifier le réglage des curseurs soit dans
%    le fichier \file{lua-typo.cfg} soit dans le préambule après l’appel
%    de \pkg{lua-typo} ; les réglages placés dans le préambule prévalent
%    sur ceux du fichier \file{lua-typo.cfg} qui eux-mêmes prévalent sur
%    les réglages internes de l’extension.
%
%    Le fichier \file{lua-typo.cfg} fourni avec la distribution reprend
%    exactement les réglages internes, il se trouve normalement dans le
%    répertoire \texdir{texmfdist} des distributions TeXLive, MikTeX, etc.
%    L’utilisateur a la possibilité de recopier ce fichier soit dans son
%    répertoire de travail, soit dans son répertoire \texdir{texmfhome}
%    ou \texdir{texmflocal} et de le personnaliser comme il l’entend.
%
%    Voici la liste complète des paramètres personnalisables avec leur
%    valeur par défaut, leurs noms sont systématiquement préfixés par
%    |luatypo| afin d’éviter de possibles conflits avec d’autres
%    extensions.
%    \begin{description}
%      \item[\opt{BackParindent :}]  la dernière ligne d’un alinéa
%        devrait, soit se terminer à plus de |\luatypoBackPI=1em|
%        de la marge droite, soit être (approximativement) pleine
%        (tolérance |\luatypoBackFuzz=2pt|)%
%        \footnote{Certains auteurs n’acceptent pas les lignes pleines
%          en fin de paragraphe, ceux-là pourront faire
%          \cs{luatypoBackFuzz=0pt} pour qu’elles soient détectées comme
%          fautives.}.
%
%      \item[\opt{ShortLines :}]  |\luatypoLLminWD=2\parindent|%
%        \footnote{Ou \texttt{20pt} si \cs{parindent=0pt}.}
%        fixe la longueur minimale acceptable pour la dernière ligne
%        d’un alinéa.
%
%      \item[\opt{ShortPages :}] |\luatypoPageMin=5| fixe le nombre
%        minimal de lignes d’une page pour que celle-ci ne soit pas
%        déclarée trop  courte. En fait, la position de la dernière
%        ligne est prise en compte afin que les pages de titre ou celles
%        contenant une image ne soient pas signalées comme fautives.
%
%      \item[\opt{RepeatedHyphens :}] |\luatypoHyphMax=2| fixe le nombre maximal
%        acceptable de lignes consécutives terminées par un mot coupé.
%
%      \item[\opt{UnderfullLines :}] |\luatypoStretchMax=200| fixe le
%        pourcentage maximal acceptable pour l’étirement des
%        espaces-mots, au-delà la ligne est déclarée lavée.
%        La valeur donnée doit être un entier supérieur ou égal à~100,
%        cette valeur 100 correspond à l’étirement maximal prévu par la
%        fonte (|\fontdimen3|) ; avec ce réglage attendez-vous à trouver
%        une kyrielle de lignes creuses ! En fait la valeur par défaut
%        (200) correspond approximativement à ce que TeX, avec les
%        réglages par défaut (|\tolerance=200|, |\hbadness=1000|),
%        considère comme \emph{Underfull hbox}.
%
%      \item[\opt{First/LastWordMatch :}] |\luatypoMinFull=3| et
%        |\luatypoMinPart=4| désignent les nombres minimaux
%        de lettres identiques (resp. pour un mot complet ou pour une
%        partie de mot) au début ou à la fin de deux lignes consécutives
%        déclenchant l’avertissement.
%        Avec ce réglage (3 et 4), seront détectées deux lignes se
%        terminant par « cible » et « irrésistible » ou
%        « irrésistible-(ment) » (quatre lettres en commun), ainsi que la
%        présence de « mon » en début ou fin de deux lignes consécutives
%        (trois lettres en commun), mais « mon » et « mont » en début de
%        ligne échappent à la détection.
%
%      \item[\opt{EOLShortWords :}] cette option signale la présence en fin de
%        ligne de mots très courts (une ou deux lettres)
%        qui sont répertoriés dans une des listes
%        suivantes (elles dépendent de la langue courante) :\\
%        |\luatypoOneChar{|\meta{langue}|}{'|\meta{liste de mots}|'}|\\
%        |\luatypoTwoChars{|\meta{langue}|}{'|\meta{liste de mots}|'}|
%
%        Lorsque les listes correspondant à la langue du document sont
%        vides, aucune vérification n’est effectuée. Pour l’instant,
%        il y a deux lignes (non actives) prévues pour le français :\\
%        |\luatypoOneChar{french}{'À Ô Y'}|\\
%        |\luatypoTwoChars{french}{'Je Tu Il On Au De'}|
%
%        Deux contraintes sont à respecter lorsqu’on veut
%        personnaliser ces listes :\\
%        a) le premier argument (langue) \emph{doit être connu de}
%        \pkg{babel}, aussi les commandes |\luatypoOneChar| et
%        |\luatypoTwoChars|, si elles sont utilisées, doivent l’être
%        \emph{après} le chargement de \pkg{babel}, une bonne habitude
%        à prendre est donc de toujours charger \pkg{lua-typo}
%        \emph{après} \pkg{babel} ;
%        b) le second argument \emph{doit être une chaîne de
%        caractères}, donc entourée de simples ou doubles
%        \emph{quotes} \textsc{ascii} et composées de mots séparés
%        par des espaces comme dans les exemples ci-dessus.
%
%      \item[\opt{MarginparPos :}] \cs{luatypoMarginparTol}est une
%        \emph{dimension} qui vaut |\baselineskip| par défaut;
%        les notes marginales qui se terminent à plus de
%        |\luatypoMarginparTol| en dessous de la dernière ligne
%        de la page sont déclarées fautives.
%
%    \end{description}
%
%    À chacune des vérifications faites par \pkg{lua-typo} peut être
%    attachée une couleur spécifique pour mettre en évidence les
%    imperfections détectées.
%    Actuellement, seulement six couleurs sont utilisées par défaut,
%    voici leur définition dans \file{lua-typo.cfg} :
%    \begin{verbatim}
% \definecolor{LTgrey}{gray}{0.6}
% \definecolor{LTred}{rgb}{1,0.55,0}
% \definecolor{LTline}{rgb}{0.7,0,0.3}
% \luatypoSetColor1{red}      % Coupure à l’avant-dernière ligne
% \luatypoSetColor2{red}      % Coupure en bas de page
% \luatypoSetColor3{red}      % Coupures consécutives
% \luatypoSetColor4{red}      % Mot court en fin de ligne
% \luatypoSetColor5{cyan}     % Veuve
% \luatypoSetColor6{cyan}     % Orpheline
% \luatypoSetColor7{cyan}     % Dernière ligne d’alinéa trop courte
% \luatypoSetColor8{blue}     % Ligne trop pleine
% \luatypoSetColor9{blue}     % Ligne creuse
% \luatypoSetColor{10}{red}   % Page presque vide (qq. lignes)
% \luatypoSetColor{11}{LTred} % Répétitions en début de ligne
% \luatypoSetColor{12}{LTred} % Répétitions en fin de ligne
% \luatypoSetColor{13}{LTgrey}% Dernière ligne alinéa presque pleine
% \luatypoSetColor{14}{cyan}  % Note de bas de page éclatée
% \luatypoSetColor{15}{red}   % Mot de fin de phrase en haut de page
% \luatypoSetColor{16}{LTline}% Ligne présentant plusieurs "défauts"
% \luatypoSetColor{17}{red}   % Note marginale se terminant trop bas
%    \end{verbatim}
%    \pkg{lua-typo} charge les extensions \pkg{luacolor} et donc
%    \pkg{color}. Seules les couleurs portant un nom (\emph{named
%    colors}) peuvent être utilisées dans la commande
%    |\luatypoSetColor| ; pour en définir de nouvelles il faut donc,
%    soit utiliser la commande |\definecolor| de l’extension
%    \pkg{color} (comme ci-dessus pour |LTgrey| ou |LTred|), soit
%    charger l’extension \pkg{xcolor} qui donne accès à une
%    kyrielle de noms de couleurs.
% \end{FrenchDoc}
%
% \StopEventually{}
%
% \begin{center}
%   \textbf{\Large Highlighting Typographical Flaws with LuaLaTeX}
%   \\[.5\baselineskip]^^A\]
%   {\large Daniel Flipo}\\
%   \texttt{daniel.flipo@free.fr}
% \end{center}
%
% \section{What is it about?}
%
%    The file \file{\filename}\footnote{The file described in this
%    section has version number \fileversion\ and was last revised on
%    \filedate.}, is meant for careful writers and proofreaders who do
%    not feel totally satisfied with LaTeX output, the most frequent
%    issues being overfull or underfull lines, widows and orphans,
%    hyphenated words split across two pages, two many consecutive lines
%    ending with hyphens, paragraphs ending on too short or nearly full
%    lines, homeoarchy, etc.
%
%    This package, which works with LuaLaTeX only,
%    \emph{does not try to correct anything} but just highlights
%    potential issues (the offending lines or end of lines are printed
%    in colour) and provides at the end of the \file{.log} file a summary
%    of pages to be checked and manually improved if possible.
%    \pkg{lua-typo} also creates a \file{<jobname>.typo} file which
%    summarises the informations (type, page, line number) about the
%    detected issues.
%
%    \textbf{Important notice:} a) the highlighted lines are only meant
%    to \emph{draw the proofreader’s attention} on possible issues, it
%    is up to him/her to decide whether an improvement is desirable or
%    not; they should \emph{not} be regarded as blamable! some issues
%    may be acceptable in some conditions (multi-columns, technical
%    papers) and unbearable in others (literary works f.i.).
%    Moreover, correcting a potential issue somewhere may result in
%    other much more serious flaws somewhere else …\\
%    b) Conversely, possible bugs in \pkg{lua-typo} might hide issues
%    that should normally be highlighted. Starting with version~0.85,
%    the \file{<jobname>.typo} file lists, if any, the pages on which
%    no text line could be found.  The warning may be irrelevant (page
%    only composed of figures) or point out a possible bug.\\
%    c) Regarding boxed materials, i.e.\ lines of text included in
%    minipages, parboxes or marginotes, \pkg{lua-typo} \emph{only checks}
%    Overfull/Undefull boxes.
%
%    \pkg{lua-typo} is highly configurable in order to meet the
%    variable expectations of authors and correctors: see the options’
%    list and the \file{lua-typo.cfg} configuration file below.
%
%    When \pkg{lua-typo} shows possible flaws in the page layout,
%    how can we fix them? The simpliest way is to rephrase some bits of
%    text… this is an option for an author, not for a proofreader.
%    When the text can not be altered, it is possible to \emph{slightly}
%    adjust the inter-word spacing (via the TeX commands |\spaceskip| and
%    |\xspaceskip|) and/or the letter spacing (via \pkg{microtype}’s
%    |\textls| command): slightly enlarging either of them or both may
%    be sufficient to make a paragraph’s last line acceptable when it
%    was originally too short or add a line to a paragraph when its last
%    line was nearly full, thus possibly removing an orphan.
%    Conversely, slightly reducing them may remove a paragraph’s last line
%    (when it was short) and get rid of a widow on top of next page.
%
%    I suggest to add a call |\usepackage[All]{lua-typo}| to the
%    preamble of a document which is ``nearly finished’’
%    \emph{and to remove it} once all possible corrections have been
%    made:  if some flaws remain, getting them printed in colour in
%    the final document would be a shame!
%
%    Starting with version 0.50 a recent LaTeX kernel (dated 2021/06/01)
%    is required.  Users running an older kernel will get a warning
%    and an error message ``\texttt{Unable to register callback}’’;
%    for them, a ``rollback’’ version of \pkg{lua-typo} is provided,
%    it can be loaded this way: |\usepackage[All]{lua-typo}[=v0.4]|.
%
%    \enlargethispage*{\baselineskip}
%    The current version (\fileversion) requires a LaTeX kernel dated
%    2022/06/01 or later.  Another ``rollback’’ version |[=v0.65]| has
%    been added for those who run an older kernel.
%
%    See files \file{demo.tex} and \file{demo.pdf} for a short example
%    (in French).
%
%    I am very grateful to Jacques André and Thomas Savary, who kindly
%    tested my beta versions, providing much valuable feedback and
%    suggesting many improvements for the first released version.
%    Special thanks to both of them and to Michel Bovani whose
%    contributions led to version~0.61!
%
%  \section{Usage}
%
%    The easiest way to trigger all checks perfomed by \pkg{lua-typo}
%    is:\\
%    |\usepackage[All]{lua-typo}|
%
%    It is possible to enable or disable some checks through boolean
%    options passed to \pkg{lua-typo};
%    you may want to perform all checks except a few, then
%    \pkg{lua-typo} should be loaded this way:\\
%    |\usepackage[All, |\meta{OptX}|=false, |\meta{OptY}|=false]{lua-typo}|\\
%    or to enable just a few checks, then do it this way:\\
%    |\usepackage[|\meta{OptX}|, |\meta{OptY}|, |\meta{OptZ}|]{lua-typo}|
%
%    Here is the full list of possible checks (name and purpose):\\[12pt]
%    \begin{tabular}{>{\ttfamily}ll}
%      \multicolumn{1}{l}{Name}  & Glitch to highlight\\ \hline
%      All             & Turns all options to \opt{true}\\
%      BackParindent   & paragraph’s last line \emph{nearly} full?\\
%      ShortLines      & paragraph’s last line too short?\\
%      ShortPages      & nearly empty page (just a few lines)?\\
%      OverfullLines   & overfull lines?\\
%      UnderfullLines  & underfull lines?\\
%      Widows          & widows (top of page)?\\
%      Orphans         & orphans (bottom of page)?\\
%      EOPHyphens      & hyphenated word split across two pages?\\
%      RepeatedHyphens & too many consecutive hyphens?\\
%      ParLastHyphen   & paragraph’s last full line hyphenated?\\
%      EOLShortWords   & short words (1 or 2 chars) at end of line?\\
%      FirstWordMatch  & same (part of) word starting two consecutive lines?\\
%      LastWordMatch   & same (part of) word ending two consecutive lines?\\
%      FootnoteSplit   & footnotes spread over two pages or more?\\
%      ShortFinalWord  & Short word ending a sentence on the next page\\
%      MarginparPos    & Margin note ending too low on the page\\
%       \hline
%    \end{tabular}\\[12pt]
%    For example, if you want \pkg{lua-typo} to only warn about overfull
%    and underfull lines, you can load \pkg{lua-typo} like this:\\
%    |\usepackage[OverfullLines, UnderfullLines]{lua-typo}|\\
%    If you want everything to be checked except paragraphs ending on
%    a short line try:\\
%    |\usepackage[All, ShortLines=false]{lua-typo}|\\
%    please note that \opt{All} has to be the first one, as options are
%    taken into account as they are read \emph{i.e.} from left to right.
%
%    \newpage
%    The list of all available options is printed to the \file{.log}
%    file when option \opt{ShowOptions} is passed to \pkg{lua-typo},
%    this option provides an easy way to get their names without having
%    to look into the documentation.
%
%    With option \opt{None}, \pkg{lua-typo} \emph{does absolutely
%    nothing}, all checks are disabled as the main function is not added
%    to any LuaTeX callback.  It not quite equivalent to commenting out
%    the |\usepackage{lua-typo}| line though, as user defined commands
%    related to \pkg{lua-typo} are still defined and will not print
%    any error message.
%
%    Please be aware of the following features:
%    \begin{description}
%      \item[\opt{FirstWordMatch:}] the first word of consecutive list items
%        is not highlighted, as these repetitions result of the author’s
%        choice.
%      \item[\opt{ShortPages:}] if a page is considered too short, its last
%        line only is highlighted, not the whole page.
%      \item[\opt{RepeatedHyphens:}] ditto, when the number of consecutives
%        hyphenated lines is too high, only the hyphenated words in
%        excess (the last ones) are hightlighted.
%      \item[\opt{ShortFinalWord :}] the first word on a page is highlighted
%        if it ends a sentence and is short (up to |\luatypoMinLen=4|
%        letters).
%
%      \end{description}
%
%    \section{Known issues}
%
%    \pkg{lua-typo} is currently incompatible with the \pkg{reledmac}
%    package.  When the latter is loaded, no check is performed by
%    \pkg{lua-typo}, a warning is issued in the \file{.log} file.
%
%    \section{Customisation}
%
%    Some of the checks mentionned above require tuning, for
%    instance, when is a last paragraph’s length called too short?
%    how many hyphens ending consecutive lines are acceptable?
%    \pkg{lua-typo} provides user customisable parameters to set
%    what is regarded as acceptable or not.
%
%    A default configuration file \file{lua-typo.cfg} is provided
%    with all parameters set to their defaults; it is located under
%    the \texdir{texmfdist} directory.  It is up to the users to copy
%    this file into their working directory (or \texdir{texmfhome} or
%    \texdir{texmflocal}) and tune the defaults according to their own
%    taste.
%
%    It is also possible to provide defaults directly in the
%    document’s preamble (this overwrites the corresponding settings
%    done in the configuration file found on TeX’s search path: current
%    directory, then \texdir{texmfhome}, \texdir{texmflocal} and
%    finally \texdir{texmfdist}.
%
%    Here are the parameters names (all prefixed by |luatypo| in order
%    to avoid conflicts with other packages) and their default values:
%    \begin{description}
%      \item[\opt{BackParindent:}] paragraphs’ last line should either
%        end at at sufficient distance of the right margin
%        (|\luatypoBackPI|, default |1em|) or (approximately) touch the
%        right margin ---the tolerance is
%        |\luatypoBackFuzz| (default |2pt|)%
%        \footnote{Some authors do not accept full lines at end of
%          paragraphs, they can just set \cs{luatypoBackFuzz=0pt}
%          to make them pointed out as faulty.}.
%
%      \item[\opt{ShortLines:}] |\luatypoLLminWD=2\parindent|%
%        \footnote{Or \texttt{20pt} if \cs{parindent=0pt}.}
%        sets the minimum acceptable length for paragraphs’ last lines.
%
%      \item[\opt{ShortPages:}] |\luatypoPageMin=5| sets the minimum
%        acceptable number of lines on a page (chapters’ last page
%        for instance).  Actually, the last line’s vertical position on
%        the page is taken into account so that f.i.\ title pages or
%        pages ending on a picture are not pointed out.
%
%      \item[\opt{RepeatedHyphens:}] |\luatypoHyphMax=2| sets the maximum
%        acceptable number of consecutive hyphenated lines.
%
%      \item[\opt{UnderfullLines:}] |\luatypoStretchMax=200| sets the maximum
%        acceptable percentage of stretch acceptable before a line is
%        tagged by \pkg{lua-typo} as underfull; it must be an integer
%        over 100, 100 means that the slightest stretch exceeding the
%        font tolerance (|\fontdimen3|) will be warned about (be
%        prepared for a lot of ``underfull lines’’ with this setting),
%        the default value 200 is just below what triggers TeX’s
%        ``Underfull hbox’’ message (when |\tolerance=200| and
%        |\hbadness=1000|).
%
%      \item[\opt{First/LastWordMatch:}] |\luatypoMinFull=3|,
%        |\luatypoMinPart=4| set the minimum number of characters
%        required for a match to be pointed out.  With this setting (3
%        and 4), two occurrences of the word `out’ at the beginning or
%        end of two consecutive lines will be highlighted (three chars,
%        `in’ wouldn’t match), whereas a line ending with ``full’’ or
%        ``overfull’’ followed by one ending with ``underfull’’ will
%        match (four chars): the second occurence of ``full’’ or
%        ``erfull’’ will be highlighted.
%
%      \item[\opt{EOLShortWords:}] this check deals with lines ending with
%        very short words (one or two characters), not all of them but
%        a user selected list depending on the current language.\\
%        |\luatypoOneChar{|\meta{language}|}{'|\meta{list of words}|'}|\\
%        |\luatypoTwoChars{|\meta{language}|}{'|\meta{list of words}|'}|
%
%        Currently, defaults (commented out) are suggested for the French
%        language only:\\
%        |\luatypoOneChar{french}{'À Ô Y'}|\\
%        |\luatypoTwoChars{french}{'Je Tu Il On Au De'}|
%
%        Feel free to customise these lists for French or to add your
%        own shorts words for other languages but remember that
%        a) the first argument (language name) \emph{must be known by}
%        \pkg{babel}, so if you add |\luatypoOneChar| or
%        |\luatypoTwoChars| commands, please make sure that
%        \pkg{lua-typo} is loaded \emph{after} \pkg{babel};
%        b) the second argument \emph{must be a string}
%        (\emph{i.e.} surrounded by single or double \textsc{ascii}
%        quotes) made of your words separated by spaces.
%
%      \item[\opt{MarginparPos:}] |\luatypoMarginparTol}| is
%        a \emph{dimension} which defaults to |\baselineskip|;
%        marginal notes trigger a flaw if they end lower than
%        |\luatypoMarginparTol| under the page’s last line.
%
%      \end{description}
%
%    It is possible to define a specific colour for each
%    typographic flaws that \pkg{lua-typo} deals with.
%    Currently, only six colours are used in \file{lua-typo.cfg}:
%    \begin{verbatim}
% \definecolor{LTgrey}{gray}{0.6}
% \definecolor{LTred}{rgb}{1,0.55,0}
% \definecolor{LTline}{rgb}{0.7,0,0.3}
% \luatypoSetColor1{red}      % Paragraph last full line hyphenated
% \luatypoSetColor2{red}      % Page last word hyphenated
% \luatypoSetColor3{red}      % Hyphens on consecutive lines
% \luatypoSetColor4{red}      % Short word at end of line
% \luatypoSetColor5{cyan}     % Widow
% \luatypoSetColor6{cyan}     % Orphan
% \luatypoSetColor7{cyan}     % Paragraph ending on a short line
% \luatypoSetColor8{blue}     % Overfull lines
% \luatypoSetColor9{blue}     % Underfull lines
% \luatypoSetColor{10}{red}   % Nearly empty page (a few lines)
% \luatypoSetColor{11}{LTred} % First word matches
% \luatypoSetColor{12}{LTred} % Last word matches
% \luatypoSetColor{13}{LTgrey}% Paragraph’s last line nearly full
% \luatypoSetColor{14}{cyan}  % Footnotes spread over two pages
% \luatypoSetColor{15}{red}   % Short final word on top of the page
% \luatypoSetColor{16}{LTline}% Line color for multiple flaws
% \luatypoSetColor{17}{red}   % Margin note ending too low\end{verbatim}
%    \pkg{lua-typo} loads the \pkg{luacolor} package which loads the
%    \pkg{color} package from the LaTeX graphic bundle.
%    |\luatypoSetColor| requires named colours, so you can either use
%    the |\definecolor| from \pkg{color} package to define yours
%    (as done in the config file for `LTgrey’ and `LTred’) or load the
%    \pkg{xcolor} package which provides a bunch of named colours.
%
%    \section{\TeX{}nical details}
%
% \iffalse
%<*sty|scan>
%% IMPORTANT NOTICE:
%% For the copyright see the source file `lua-typo.dtx’.
%%
\NeedsTeXFormat{LaTeX2e}[2021/06/01]
%</sty|scan>
%<+sty>\ProvidesPackage{lua-typo}
%<+scan>\ProvidesPackage{scan-page}
%<+dtx>\ProvidesFile{lua-typo.dtx}
%<*dtx|sty|scan>
                [2026-01-07 v.0.88 Daniel Flipo]
%</dtx|sty|scan>
%<*sty>
% \fi
%
% \changes{v0.50}{2021/05/05}{Rollback mechanism used for recovering
%    older versions.}
%
%    Starting with version~0.50, this package uses the rollback
%    mechanism to provide easier backward compatibility.
%    Rollback version~0.40 is provided for users who would have
%    a LaTeX kernel older than 2021/06/01.
%    Rollback version~0.65 is provided for users who would have
%    a LaTeX kernel older than 2022/06/01.
%
%    \begin{macrocode}
\DeclareRelease{v0.4}{2021-01-01}{lua-typo-2021-04-18.sty}
\DeclareRelease{v0.65}{2023-03-08}{lua-typo-2023-03-08.sty}
\DeclareCurrentRelease{}{2023-09-13}
%    \end{macrocode}
%
%    This package only runs with LuaLaTeX and requires packages
%    \pkg{luatexbase}, \pkg{luacode}, \pkg{luacolor} and
%    \pkg{atveryend}.
%
%    \begin{macrocode}
\ifdefined\directlua
  \RequirePackage{luatexbase,luacode,luacolor,atveryend}
\else
  \PackageError{This package is meant for LuaTeX only! Aborting}
               {No more information available, sorry!}
\fi
%    \end{macrocode}
%
%    Let’s define the necessary internal counters, dimens, token
%    registers and commands…
%
%    \begin{macrocode}
\newdimen\luatypoLLminWD
\newdimen\luatypoBackPI
\newdimen\luatypoBackFuzz
\newdimen\luatypoMarginparTol
\newcount\luatypoStretchMax
\newcount\luatypoHyphMax
\newcount\luatypoPageMin
\newcount\luatypoMinFull
\newcount\luatypoMinPart
\newcount\luatypoMinLen
\newcount\luatypo@LANGno
\newcount\luatypo@options
\newtoks\luatypo@single
\newtoks\luatypo@double
%    \end{macrocode}
%    … and define a global table for this package.
%    \begin{macrocode}
\begin{luacode}
luatypo = { }
\end{luacode}
%    \end{macrocode}
%
%    Set up \pkg{ltkeys} initializations.
%    Option \opt{All} resets all booleans relative to specific
%    typographic checks to \opt{true}.
%
% \changes{v0.70}{2023/04/08}{Package options no longer require
%    `kvoptions’, they rely on LaTeX `ltkeys’ package.}
%
%    \begin{macrocode}
\DeclareKeys[luatypo]
  {
   ShowOptions.if     = LT@ShowOptions     ,
   None.if            = LT@None            ,
   BackParindent.if   = LT@BackParindent   ,
   ShortLines.if      = LT@ShortLines      ,
   ShortPages.if      = LT@ShortPages      ,
   OverfullLines.if   = LT@OverfullLines   ,
   UnderfullLines.if  = LT@UnderfullLines  ,
   Widows.if          = LT@Widows          ,
   Orphans.if         = LT@Orphans         ,
   EOPHyphens.if      = LT@EOPHyphens      ,
   RepeatedHyphens.if = LT@RepeatedHyphens ,
   ParLastHyphen.if   = LT@ParLastHyphen   ,
   EOLShortWords.if   = LT@EOLShortWords   ,
   FirstWordMatch.if  = LT@FirstWordMatch  ,
   LastWordMatch.if   = LT@LastWordMatch   ,
   FootnoteSplit.if   = LT@FootnoteSplit   ,
   ShortFinalWord.if  = LT@ShortFinalWord  ,
   MarginparPos.if    = LT@MarginparPos    ,
   PageFirstLine.if   = LT@PageFirstLine   ,
   All.if             = LT@All             ,
   All.code           = \LT@ShortLinestrue     \LT@ShortPagestrue
                        \LT@OverfullLinestrue  \LT@UnderfullLinestrue
                        \LT@Widowstrue         \LT@Orphanstrue
                        \LT@EOPHyphenstrue     \LT@RepeatedHyphenstrue
                        \LT@ParLastHyphentrue  \LT@EOLShortWordstrue
                        \LT@FirstWordMatchtrue \LT@LastWordMatchtrue
                        \LT@BackParindenttrue  \LT@FootnoteSplittrue
                        \LT@ShortFinalWordtrue \LT@MarginparPostrue
  }
\ProcessKeyOptions[luatypo]
%    \end{macrocode}
%
%     Forward these options to the |luatypo| global table.
%     Wait until the config file \file{lua-typo.cfg} has been read
%     in order to give it a chance of overruling the boolean options.
%     This enables the user to permanently change the defaults.
%
%    \begin{macrocode}
\AtEndOfPackage{%
  \ifLT@None
    \directlua{ luatypo.None = true }%
  \else
    \directlua{ luatypo.None = false }%
  \fi
  \ifLT@BackParindent
    \advance\luatypo@options by 1
    \directlua{ luatypo.BackParindent = true }%
  \else
    \directlua{ luatypo.BackParindent = false }%
  \fi
  \ifLT@ShortLines
    \advance\luatypo@options by 1
    \directlua{ luatypo.ShortLines = true }%
  \else
    \directlua{ luatypo.ShortLines = false }%
  \fi
  \ifLT@ShortPages
    \advance\luatypo@options by 1
    \directlua{ luatypo.ShortPages = true }%
  \else
    \directlua{ luatypo.ShortPages = false }%
  \fi
  \ifLT@OverfullLines
    \advance\luatypo@options by 1
    \directlua{ luatypo.OverfullLines = true }%
  \else
    \directlua{ luatypo.OverfullLines = false }%
  \fi
  \ifLT@UnderfullLines
    \advance\luatypo@options by 1
    \directlua{ luatypo.UnderfullLines = true }%
  \else
    \directlua{ luatypo.UnderfullLines = false }%
  \fi
  \ifLT@Widows
    \advance\luatypo@options by 1
    \directlua{ luatypo.Widows = true }%
  \else
    \directlua{ luatypo.Widows = false }%
  \fi
  \ifLT@Orphans
    \advance\luatypo@options by 1
    \directlua{ luatypo.Orphans = true }%
  \else
    \directlua{ luatypo.Orphans = false }%
  \fi
  \ifLT@EOPHyphens
    \advance\luatypo@options by 1
    \directlua{ luatypo.EOPHyphens = true }%
  \else
    \directlua{ luatypo.EOPHyphens = false }%
  \fi
  \ifLT@RepeatedHyphens
    \advance\luatypo@options by 1
    \directlua{ luatypo.RepeatedHyphens = true }%
  \else
    \directlua{ luatypo.RepeatedHyphens = false }%
  \fi
  \ifLT@ParLastHyphen
    \advance\luatypo@options by 1
    \directlua{ luatypo.ParLastHyphen = true }%
  \else
    \directlua{ luatypo.ParLastHyphen = false }%
  \fi
  \ifLT@EOLShortWords
    \advance\luatypo@options by 1
    \directlua{ luatypo.EOLShortWords = true }%
  \else
    \directlua{ luatypo.EOLShortWords = false }%
  \fi
  \ifLT@FirstWordMatch
    \advance\luatypo@options by 1
    \directlua{ luatypo.FirstWordMatch = true }%
  \else
    \directlua{ luatypo.FirstWordMatch = false }%
  \fi
  \ifLT@LastWordMatch
    \advance\luatypo@options by 1
    \directlua{ luatypo.LastWordMatch = true }%
  \else
    \directlua{ luatypo.LastWordMatch = false }%
  \fi
  \ifLT@FootnoteSplit
    \advance\luatypo@options by 1
    \directlua{ luatypo.FootnoteSplit = true }%
  \else
    \directlua{ luatypo.FootnoteSplit = false }%
  \fi
  \ifLT@ShortFinalWord
    \advance\luatypo@options by 1
    \directlua{ luatypo.ShortFinalWord = true }%
  \else
    \directlua{ luatypo.ShortFinalWord = false }%
  \fi
  \ifLT@MarginparPos
    \advance\luatypo@options by 1
    \directlua{ luatypo.MarginparPos = true }%
  \else
    \directlua{ luatypo.MarginparPos = false }%
  \fi
  \ifLT@PageFirstLine
    \advance\luatypo@options by 1
    \directlua{ luatypo.PageFirstLine = true }%
  \else
    \directlua{ luatypo.PageFirstLine = false }%
  \fi
}
%    \end{macrocode}
%
%    |ShowOptions| is specific:
%
%    \begin{macrocode}
\ifLT@ShowOptions
  \GenericWarning{* }{%
     *** List of possible options for lua-typo ***\MessageBreak
     [Default values between brackets]%
     \MessageBreak
     ShowOptions     [false]\MessageBreak
     None            [false]\MessageBreak
     All             [false]\MessageBreak
     BackParindent   [false]\MessageBreak
     ShortLines      [false]\MessageBreak
     ShortPages      [false]\MessageBreak
     OverfullLines   [false]\MessageBreak
     UnderfullLines  [false]\MessageBreak
     Widows          [false]\MessageBreak
     Orphans         [false]\MessageBreak
     EOPHyphens      [false]\MessageBreak
     RepeatedHyphens [false]\MessageBreak
     ParLastHyphen   [false]\MessageBreak
     EOLShortWords   [false]\MessageBreak
     FirstWordMatch  [false]\MessageBreak
     LastWordMatch   [false]\MessageBreak
     FootnoteSplit   [false]\MessageBreak
     ShortFinalWord  [false]\MessageBreak
     MarginparPos    [false]\MessageBreak
     \MessageBreak
     *********************************************%
     \MessageBreak Lua-typo [ShowOptions]
   }%
\fi
%    \end{macrocode}
%
%   Some defaut values which can be customised in the preamble
%   are forwarded to Lua AtBeginDocument.
%
% \changes{v0.87}{2024/04/18}{Add warning: lua-typo incompatible
%    with the `reledmac’ package.}
%
%    \begin{macrocode}
\AtBeginDocument{%
  \@ifpackageloaded{reledmac}%
    {\PackageWarning{lua-typo}{%
        'lua-typo' is incompatible with\MessageBreak
        the 'reledmac' package.\MessageBreak
        'lua-typo' checking disabled.\MessageBreak
        Reported}%
     \LT@Nonetrue
     \directlua{ luatypo.None = true }%
    }{}%
  \directlua{
    luatypo.HYPHmax = tex.count.luatypoHyphMax
    luatypo.PAGEmin = tex.count.luatypoPageMin
    luatypo.Stretch = tex.count.luatypoStretchMax
    luatypo.MinFull = tex.count.luatypoMinFull
    luatypo.MinPart = tex.count.luatypoMinPart
%    \end{macrocode}
%    Ensure |MinFull|$\leq$|MinPart|.
%    \begin{macrocode}
    luatypo.MinFull  = math.min(luatypo.MinPart,luatypo.MinFull)
    luatypo.MinLen   = tex.count.luatypoMinLen
    luatypo.LLminWD  = tex.dimen.luatypoLLminWD
    luatypo.BackPI   = tex.dimen.luatypoBackPI
    luatypo.BackFuzz = tex.dimen.luatypoBackFuzz
    luatypo.MParTol  = tex.dimen.luatypoMarginparTol
%    \end{macrocode}
% \changes{v0.80}{2023/04/28}{New table `luatypo.map’ for colours.}
%
%    Build a compact table holding all colours defined by
%    \file{lua-typo} (no duplicates).
%    \begin{macrocode}
    local tbl = luatypo.colortbl
    local map = { }
    for i,v in ipairs (luatypo.colortbl) do
      if i == 1 or v > tbl[i-1] then
         table.insert(map, v)
      end
    end
    luatypo.map = map
   }%
}
%    \end{macrocode}
%
%    Print the summary of offending pages ---if any--- at the
%    (very) end of document and write the report file on disc,
%    unless option |None| has been selected.
%
%    On every page, at least one line of text should be found.
%    Otherwise, \pkg{lua-typo} presumes something went wrong and
%    writes the page number to a |failedlist| list.
%    In case |pagelist| is empty \emph{and} |failedlist| \emph{is not},
%    a warning is issued instead of the \texttt{No Typo Flaws found.}
%    message (new to version~0.85).
%
% \changes{v0.85}{2023/09/07}{Warn in case some pages failed to
%    be checked properly.}
%
%    \begin{macrocode}
\AtVeryEndDocument{%
\ifnum\luatypo@options = 0 \LT@Nonetrue \fi
\ifLT@None
  \directlua{
    texio.write_nl(' ')
    texio.write_nl('************************************')
    texio.write_nl('*** lua-typo running with NO option:')
    texio.write_nl('*** NO CHECK PERFORMED! ***')
    texio.write_nl('************************************')
    texio.write_nl(' ')
   }%
\else
  \directlua{
    texio.write_nl(' ')
    texio.write_nl('*************************************')
    if luatypo.pagelist == " " then
       if luatypo.failedlist == " " then
          texio.write_nl('*** lua-typo: No Typo Flaws found.')
       else
          texio.write_nl('*** WARNING: ')
          texio.write('lua-typo failed to scan these pages:')
          texio.write_nl('***' .. luatypo.failedlist)
          texio.write_nl('*** Please report to the maintainer.')
       end
    else
       texio.write_nl('*** lua-typo: WARNING *************')
       texio.write_nl('The following pages need attention:')
       texio.write(luatypo.pagelist)
    end
    texio.write_nl('*************************************')
    texio.write_nl(' ')
    if luatypo.failedlist == " " then
    else
       local prt = "WARNING: lua-typo failed to scan pages " ..
                   luatypo.failedlist .. "\string\n\string\n"
       luatypo.buffer = prt .. luatypo.buffer
    end
    local fileout= tex.jobname .. ".typo"
    local out=io.open(fileout,"w+")
    out:write(luatypo.buffer)
    io.close(out)
   }%
\fi}
%    \end{macrocode}
%
% \begin{macro}{\luatypoOneChar}
% \begin{macro}{\luatypoTwoChars}
%    These commands set which short words should be avoided at end of
%    lines.  The first argument is a language name, say \opt{french},
%    which is turned into a command |\l@french| expanding to a number
%    known by luatex, otherwise an error message occurs.
%    The utf-8 string entered as second argument has to be
%    converted into the font internal coding.
%    \begin{macrocode}
\newcommand*{\luatypoOneChar}[2]{%
  \def\luatypo@LANG{#1}\luatypo@single={#2}%
  \ifcsname l@\luatypo@LANG\endcsname
    \luatypo@LANGno=\the\csname l@\luatypo@LANG\endcsname \relax
    \directlua{
      local langno = \the\luatypo@LANGno
      local string = \the\luatypo@single
      luatypo.single[langno] = " "
      for p, c in utf8.codes(string) do
        local s = utf8.char(c)
        luatypo.single[langno] = luatypo.single[langno] .. s
      end
%<dbg>      texio.write_nl('SINGLE=' .. luatypo.single[langno])
%<dbg>      texio.write_nl(' ')
     }%
  \else
    \PackageWarning{luatypo}{Unknown language "\luatypo@LANG",
       \MessageBreak \protect\luatypoOneChar\space command ignored}%
  \fi}
\newcommand*{\luatypoTwoChars}[2]{%
  \def\luatypo@LANG{#1}\luatypo@double={#2}%
  \ifcsname l@\luatypo@LANG\endcsname
    \luatypo@LANGno=\the\csname l@\luatypo@LANG\endcsname \relax
    \directlua{
      local langno = \the\luatypo@LANGno
      local string = \the\luatypo@double
      luatypo.double[langno] = " "
      for p, c in utf8.codes(string) do
        local s = utf8.char(c)
        luatypo.double[langno] = luatypo.double[langno] .. s
      end
%<dbg>      texio.write_nl('DOUBLE=' .. luatypo.double[langno])
%<dbg>      texio.write_nl(' ')
    }%
  \else
    \PackageWarning{luatypo}{Unknown language "\luatypo@LANG",
       \MessageBreak \protect\luatypoTwoChars\space command ignored}%
  \fi}
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \vspace*{\baselineskip}
% \begin{macro}{\luatypoSetColor}
%    This is a user-level command to customise the colours highlighting
%    the sixteen types of possible typographic flaws.
%    The first argument is a number (flaw type: 1-16), the second the
%    named colour associated to it.
%    The colour support is based on the \pkg{luacolor} package (colour
%    attributes).
%    \begin{macrocode}
\newcommand*{\luatypoSetColor}[2]{%
  \begingroup
    \color{#2}%
    \directlua{luatypo.colortbl[#1]=\the\LuaCol@Attribute}%
  \endgroup
}
%\luatypoSetColor{0}{black}
%    \end{macrocode}
% \end{macro}
%
%    The Lua code now, initialisations.
%
%    \begin{macrocode}
\begin{luacode}
luatypo.colortbl   = { }
luatypo.map        = { }
luatypo.single     = { }
luatypo.double     = { }
luatypo.pagelist   = " "
luatypo.failedlist = " "
luatypo.buffer     = "List of typographic flaws found for "
                      .. tex.jobname .. ".pdf:\string\n\string\n"

local char_to_discard = { }
char_to_discard[string.byte(",")] = true
char_to_discard[string.byte(".")] = true
char_to_discard[string.byte("!")] = true
char_to_discard[string.byte("?")] = true
char_to_discard[string.byte(":")] = true
char_to_discard[string.byte(";")] = true
char_to_discard[string.byte("-")] = true

local eow_char = { }
eow_char[string.byte(".")] = true
eow_char[string.byte("!")] = true
eow_char[string.byte("?")] = true
eow_char[utf8.codepoint("…")] = true

local DISC  = node.id("disc")
local GLYPH = node.id("glyph")
local GLUE  = node.id("glue")
local KERN  = node.id("kern")
local RULE  = node.id("rule")
local HLIST = node.id("hlist")
local VLIST = node.id("vlist")
local LPAR  = node.id("local_par")
local MKERN = node.id("margin_kern")
local PENALTY = node.id("penalty")
local WHATSIT = node.id("whatsit")
%    \end{macrocode}
%    Glue subtypes:
%    \begin{macrocode}
local USRSKIP  = 0
local PARSKIP  = 3
local LFTSKIP  = 8
local RGTSKIP  = 9
local TOPSKIP = 10
local PARFILL = 15
%    \end{macrocode}
%    Hlist subtypes:
%    \begin{macrocode}
local LINE    = 1
local BOX     = 2
local INDENT  = 3
local ALIGN   = 4
local EQN     = 6
%    \end{macrocode}
%    Penalty subtypes:
%    \begin{macrocode}
local USER = 0
local HYPH = 0x2D
%    \end{macrocode}
%    Glyph subtypes:
%    \begin{macrocode}
local LIGA = 0x102
%    \end{macrocode}
%    Counter |parline| (current paragraph) \emph{must not be reset}
%    on every new page!
%    \begin{macrocode}
local parline = 0
%    \end{macrocode}
%    Local definitions for the `node’ library:
%    \begin{macrocode}
local dimensions = node.dimensions
local rangedimensions = node.rangedimensions
local effective_glue = node.effective_glue
local set_attribute = node.set_attribute
local get_attribute = node.get_attribute
local slide = node.slide
local traverse = node.traverse
local traverse_id = node.traverse_id
local has_field = node.has_field
local uses_font = node.uses_font
local is_glyph  = node.is_glyph
local utf8_len  = utf8.len
%    \end{macrocode}
%    Local definitions from the `unicode.utf8’ library: replacements are
%    needed for functions |string.gsub()|, |string.sub()|, |string.find()|
%    and |string.reverse()| which are meant for one-byte characters only.
%
% \changes{v0.65}{2023/03/02}{Three new functions for utf-8 strings’
%    manipulations.}
%
%    |utf8_find| requires an utf-8 string and a `pattern’ (also utf-8),
%    it returns |nil| if pattern is not found, or the \emph{byte}
%    position of the first match otherwise [not an issue as we only
%    care for true/false].
%    \begin{macrocode}
local utf8_find = unicode.utf8.find
%    \end{macrocode}
%    |utf8_gsub| mimics |string.gsub| for utf-8 strings.
%    \begin{macrocode}
local utf8_gsub = unicode.utf8.gsub
%    \end{macrocode}
%    |utf8_reverse| returns the reversed string (utf-8 chars read  from
%    end to beginning) [same as |string.reverse| but for utf-8 strings].
%    \begin{macrocode}
local utf8_reverse = function (s)
  if utf8_len(s) > 1 then
     local so = ""
     for p, c in utf8.codes(s) do
         so = utf8.char(c) .. so
     end
     s = so
  end
  return s
end
%    \end{macrocode}
%    |utf8_sub| returns the substring of s that starts at i and
%    continues until j (j-i-1 utf8 chars.). \emph{Warning: it requires
%    $i\ge1$ and $j\ge i$}.
%    \begin{macrocode}
local utf8_sub = function (s,i,j)
    i=utf8.offset(s,i)
    j=utf8.offset(s,j+1)-1
    return string.sub(s,i,j)
end
%    \end{macrocode}
%
%\changes{v0.32}{2021/03/14}{Better protection against unexpected
%    nil nodes.}
%
%    The next function colours glyphs and discretionaries.
%    It requires two arguments: a node and a (named) colour.
%
%    \begin{macrocode}
local color_node = function (node, color)
  local attr = oberdiek.luacolor.getattribute()
  if node and node.id == DISC then
     local pre = node.pre
     local post = node.post
     local repl = node.replace
     if pre then
        set_attribute(pre,attr,color)
     end
     if post then
        set_attribute(post,attr,color)
     end
     if repl then
        set_attribute(repl,attr,color)
     end
  elseif node then
     set_attribute(node,attr,color)
  end
end
%    \end{macrocode}
%
%    The next function colours a whole line without overriding
%    previously set colours by f.i.\ homeoarchy, repeated hyphens etc.
%    It requires two arguments: a line’s node and a (named) colour.
%
%    Digging into nested hlists and vlists is needed f.i.\ to colour
%    aligned equations.
%
% \changes{v0.50}{2021/05/05}{Go down deeper into hlists and vlists to
%    colour nodes.}
%
% \changes{v0.80}{2023/04/18}{`color\_line’ no longer overwrites colors
%    set previously.}
%
%    \begin{macrocode}
local color_line = function (head, color)
  local first = head.head
  local map = luatypo.map
  local color_node_if = function (node, color)
    local c = oberdiek.luacolor.getattribute()
    local att = get_attribute(node,c)
    local uncolored = true
    for i,v in ipairs (map) do
      if att == v then
         uncolored = false
         break
      end
    end
    if uncolored then
       color_node (node, color)
    end
  end
  for n in traverse(first) do
      if n.id == HLIST or n.id == VLIST then
         local ff = n.head
         for nn in traverse(ff) do
           if nn.id == HLIST or nn.id == VLIST then
              local f3 = nn.head
              for n3 in traverse(f3) do
                if n3.id == HLIST or n3.id == VLIST then
                   local f4 = n3.head
                   for n4 in traverse(f4) do
                     if n4.id == HLIST or n4.id == VLIST then
                        local f5 = n4.head
                        for n5 in traverse(f5) do
                          if n5.id == HLIST or n5.id == VLIST then
                             local f6 = n5.head
                             for n6 in traverse(f6) do
                               color_node_if(n6, color)
                             end
                          else
                             color_node_if(n5, color)
                          end
                        end
                     else
                        color_node_if(n4, color)
                     end
                   end
                else
                   color_node_if(n3, color)
                end
              end
           else
              color_node_if(nn, color)
           end
         end
      else
         color_node_if(n, color)
      end
  end
end
%    \end{macrocode}
%
%    The next function takes four arguments: a string, two numbers
%    (which can be \node{nil}) and a flag.  It appends a line to
%    a buffer which will be written to file `\cs{jobname.typo}’.
%
% \changes{v0.50}{2021/05/13}{Summary of flaws written to file
%    `\cs{jobname.typo}’.}
%
%    \begin{macrocode}
log_flaw= function (msg, line, colno, footnote)
  local pageno = tex.getcount("c@page")
  local prt ="p. " .. pageno
  if colno then
     prt = prt .. ", col." .. colno
  end
  if line then
     local l = string.format("%2d, ", line)
     if footnote then
        prt = prt .. ", (ftn.) line " .. l
     else
        prt = prt .. ", line " .. l
     end
  end
  prt =  prt .. msg
  luatypo.buffer = luatypo.buffer .. prt .. "\string\n"
end
%    \end{macrocode}
%
%    The next three functions deal with ``homeoarchy’’, \emph{i.e.}
%    lines beginning or ending with the same (part of) word.
%    While comparing two words, the only significant nodes are glyphs
%    and ligatures, dicretionnaries other than ligatures, kerns
%    (letterspacing) should be discarded.
%    For each word to be compared we build a ``signature’’ made of
%    glyphs, split ligatures and underscores (representing glues).
%
% \changes{v0.65}{2023/03/02}{All ligatures are now split using the
%    node’s `components’ field rather than a table.}
%
% \changes{v0.86}{2024/01/10}{Typo corrected in the signature function.}
%
%    The first function adds a (non-nil) node to a signature of type
%    string, nil nodes are ignored.
%    It returns the augmented string and its length
%    (underscores are omitted in the length computation).
%    The last argument is a boolean needed when building a signature
%    backwards (see |check_line_last_word|).
%    \begin{macrocode}
local signature = function (node, string, swap)
  local n = node
  local str = string
  if n and n.id == GLYPH then
     local b = n.char
%    \end{macrocode}
%    Punctuation has to be discarded; other glyphs may be ligatures,
%    then they have a |components| field which holds the list of glyphs
%    which compose the ligature.
%    \begin{macrocode}
     if b and not char_to_discard[b] then
        if n.components then
           local c = ""
           for nn in traverse_id(GLYPH, n.components) do
             c = c .. utf8.char(nn.char)
           end
           if swap then
              str = str .. utf8_reverse(c)
           else
              str = str .. c
           end
        else
           str = str .. utf8.char(b)
        end
     end
  elseif n and n.id == DISC then
%    \end{macrocode}
%    Discretionaries are split into |pre| and |post| and both parts
%    are stored.  They might be ligatures (\emph{ffl, ffi})…
%    \begin{macrocode}
    local pre = n.pre
    local post = n.post
    local c1 = ""
    local c2 = ""
    if pre and pre.char then
       if pre.components then
          for nn in traverse_id(GLYPH, pre.components) do
            c1 = c1 .. utf8.char(nn.char)
          end
       else
          c1 = utf8.char(pre.char)
       end
       c1 = utf8_gsub(c1, "-", "")
    end
    if post and post.char then
       if post.components then
          for nn in traverse_id(GLYPH, post.components) do
            c2 = c2 .. utf8.char(nn.char)
          end
       else
          c2 = utf8.char(post.char)
       end
    end
    if swap then
       str = str .. utf8_reverse(c2) .. c1
    else
       str = str .. c1 .. c2
    end
  elseif n and n.id == GLUE then
       str = str .. "_"
  end
%    \end{macrocode}
%    The returned length is the number of \emph{letters}.
%    \begin{macrocode}
  local s = utf8_gsub(str, "_", "")
  local len = utf8_len(s)
  return len, str
end
%    \end{macrocode}
%
%    The next function looks for consecutive lines ending with
%    the same letters.\par
%    It requires five arguments: a string (previous line’s signature),
%    a node (the last one on the current line), a line number, a column
%    number (possibly |nil|) and a boolean to cancel checking in some
%    cases (end of paragraphs).
%    It prints the matching part at end of linewith with the supplied
%    colour and returns the current line’s last word and a boolean (match).
%
% \changes{v0.32}{2021/03/14}{Functions `check\_line\_first\_word’ and
%    `check\_line\_last\_word’ rewritten.}
%
% \changes{v0.50}{2021/05/13}{Homeoarchy detection added for lines
%    starting or ending on \cs{mbox}.}
%
% \changes{v0.61}{2023/02/06}{`check\_line\_last\_word’ returns a flag
%    to set pageflag.}
%
% \changes{v0.70}{2023/04/08}{`check\_line\_first\_word’ and
%    `check\_line\_last\_word’: length of matches corrected.}
%
% \changes{v0.80}{2023/04/18}{`check\_line\_first\_word’ and
%    `check\_line\_last\_word’: argument footnote added.}
%
%    \begin{macrocode}
local check_line_last_word =
            function (old, node, line, colno, flag, footnote)
  local COLOR = luatypo.colortbl[12]
  local match = false
  local new = ""
  local maxlen = 0
  local MinFull = luatypo.MinFull
  local MinPart = luatypo.MinPart
  if node then
     local swap = true
     local box, go
%    \end{macrocode}
%    Step back to the last glyph or discretionary or hbox.
%    \begin{macrocode}
     local lastn = node
     while lastn and lastn.id ~= GLYPH and lastn.id ~= DISC and
           lastn.id ~= HLIST do
       lastn = lastn.prev
     end
%    \end{macrocode}
%    A signature is built from the last two (or more) words
%    on the current line.
%    \begin{macrocode}
     local n = lastn
     local words = 0
     while n and (words <= 2 or maxlen < MinPart) do
%    \end{macrocode}
%    Go down inside  boxes, read their content from end to beginning,
%    then step out.
%    \begin{macrocode}
       if n and n.id == HLIST then
          box = n
          local first = n.head
          local lastn = slide(first)
          n = lastn
          while n do
            maxlen, new = signature (n, new, swap)
            n = n.prev
          end
          n = box.prev
          local w = utf8_gsub(new, "_", "")
          words = words + utf8_len(new) - utf8_len(w) + 1
       else
          repeat
            maxlen, new = signature (n, new, swap)
            n = n.prev
          until not n or n.id == GLUE or n.id == HLIST
          if n and n.id == GLUE then
             maxlen, new = signature (n, new, swap)
             words = words + 1
             n = n.prev
          end
       end
     end
     new = utf8_reverse(new)
     new = utf8_gsub(new, "_+$", "")  -- $
     new = utf8_gsub(new, "^_+", "")
     maxlen = math.min(utf8_len(old), utf8_len(new))
%<dbg>     texio.write_nl('EOLsigold=' .. old)
%<dbg>     texio.write('   EOLsig=' .. new)
%    \end{macrocode}
%     When called with flag |false|, |check_line_last_word| doesn’t
%     compare it with the previous line’s, but just returns the
%     last word’s signature.
%    \begin{macrocode}
     if flag and old ~= "" then
%    \end{macrocode}
%    |oldlast| and |newlast| hold the last (full) words to be compared
%    later:
%    \begin{macrocode}
        local oldlast = utf8_gsub (old, ".*_", "")
        local newlast = utf8_gsub (new, ".*_", "")
%    \end{macrocode}
%    Let’s look for a partial match: build |oldsub| and |newsub|,
%    reading (backwards) the last |MinPart| \emph{non-space} characters
%    of both lines.
%    \begin{macrocode}
        local oldsub = ""
        local newsub = ""
        local dlo = utf8_reverse(old)
        local wen = utf8_reverse(new)
        for p, c in utf8.codes(dlo) do
          local s = utf8_gsub(oldsub, "_", "")
          if utf8_len(s) < MinPart then
             oldsub = utf8.char(c) .. oldsub
          end
        end
        for p, c in utf8.codes(wen) do
          local s = utf8_gsub(newsub, "_", "")
          if utf8_len(s) < MinPart then
             newsub = utf8.char(c) .. newsub
          end
        end
        if oldsub == newsub then
%<dbg>           texio.write_nl('EOLnewsub=' .. newsub)
           match = true
        end
        if oldlast == newlast and utf8_len(newlast) >= MinFull then
%<dbg>           texio.write_nl('EOLnewlast=' .. newlast)
           if utf8_len(newlast) > MinPart or not match then
              oldsub = oldlast
              newsub = newlast
           end
           match = true
        end
        if match then
%    \end{macrocode}
%    Minimal full or partial match |newsub| of length |k|;
%    any more glyphs matching?
%    \begin{macrocode}
           local k = utf8_len(newsub)
           local osub = utf8_reverse(oldsub)
           local nsub = utf8_reverse(newsub)
           while osub == nsub and k < maxlen do
             k = k + 1
             osub = utf8_sub(dlo,1,k)
             nsub = utf8_sub(wen,1,k)
             if osub == nsub then
                newsub = utf8_reverse(nsub)
             end
           end
           newsub = utf8_gsub(newsub, "^_+", "")
%<dbg>           texio.write_nl("EOLfullmatch=" .. newsub)
           local msg = "E.O.L. MATCH=" .. newsub
           log_flaw(msg, line, colno, footnote)
%    \end{macrocode}
%    Lest’s colour the matching string.
%    \begin{macrocode}
           local ns = utf8_gsub(newsub, "_", "")
           k = utf8_len(ns)
           oldsub = utf8_reverse(newsub)
           local newsub = ""
           local n = lastn
           local l = 0
           local lo = 0
           local li = 0
           while n and newsub ~= oldsub and l < k do
             if n and n.id == HLIST then
                local first = n.head
                for nn in traverse_id(GLYPH, first) do
                  color_node(nn, COLOR)
                  local c = nn.char
                  if not char_to_discard[c] then l = l + 1 end
                end
%<dbg>           texio.write_nl('l (box)=' .. l)
             elseif n then
                color_node(n, COLOR)
                li, newsub = signature(n, newsub, swap)
                l = l + li - lo
                lo = li
%<dbg>           texio.write_nl('l=' .. l)
             end
             n = n.prev
           end
        end
     end
  end
  return new, match
end
%    \end{macrocode}
%
%    Same thing for beginning of lines: check the first two words
%    and compare their signature with the previous line’s.
%
% \changes{v0.61}{2023/02/06}{`check\_line\_first\_word’ returns a flag
%    to set pageflag.}
%
%    \begin{macrocode}
local check_line_first_word =
            function (old, node, line, colno, flag, footnote)
  local COLOR = luatypo.colortbl[11]
  local match = false
  local swap = false
  local new = ""
  local maxlen = 0
  local MinFull = luatypo.MinFull
  local MinPart = luatypo.MinPart
  local n = node
  local box, go
  while n and n.id ~= GLYPH and n.id ~= DISC and
        (n.id ~= HLIST or n.subtype == INDENT) do
     n = n.next
  end
  start = n
  local words = 0
  while n and (words <= 2 or maxlen < MinPart) do
    if n and n.id == HLIST then
       box = n
       n = n.head
       while n do
         maxlen, new = signature (n, new, swap)
         n = n.next
       end
       n = box.next
       local w = utf8_gsub(new, "_", "")
       words = words + utf8_len(new) - utf8_len(w) + 1
    else
       repeat
         maxlen, new = signature (n, new, swap)
         n = n.next
       until not n or n.id == GLUE or n.id == HLIST
       if n and n.id == GLUE then
          maxlen, new = signature (n, new, swap)
          words = words + 1
          n = n.next
       end
    end
  end
  new = utf8_gsub(new, "_+$", "") -- $
  new = utf8_gsub(new, "^_+", "")
  maxlen = math.min(utf8_len(old), utf8_len(new))
%<dbg>  texio.write_nl('BOLsigold=' .. old)
%<dbg>  texio.write('   BOLsig=' .. new)
%    \end{macrocode}
%     When called with flag |false|, |check_line_first_word| doesn’t
%     compare it with the previous line’s, but returns
%     the first word’s signature.
%    \begin{macrocode}
  if flag and old ~= "" then
     local oldfirst = utf8_gsub (old, "_.*", "")
     local newfirst = utf8_gsub (new, "_.*", "")
     local oldsub = ""
     local newsub = ""
     for p, c in utf8.codes(old) do
       local s = utf8_gsub(oldsub, "_", "")
       if utf8_len(s) < MinPart then
          oldsub = oldsub .. utf8.char(c)
       end
     end
     for p, c in utf8.codes(new) do
       local s = utf8_gsub(newsub, "_", "")
       if utf8_len(s) < MinPart then
          newsub = newsub .. utf8.char(c)
       end
     end
     if oldsub == newsub then
%<dbg>        texio.write_nl('BOLnewsub=' .. newsub)
        match = true
     end
     if oldfirst == newfirst  and utf8_len(newfirst) >= MinFull then
%<dbg>        texio.write_nl('BOLnewfirst=' .. newfirst)
        if utf8_len(newfirst) > MinPart or not match then
           oldsub = oldfirst
           newsub = newfirst
        end
        match = true
     end
     if match then
%    \end{macrocode}
%    Minimal full or partial match |newsub| of length |k|;
%    any more glyphs matching?
%    \begin{macrocode}
        local k = utf8_len(newsub)
        local osub = oldsub
        local nsub = newsub
        while osub == nsub and k < maxlen do
          k = k + 1
          osub = utf8_sub(old,1,k)
          nsub = utf8_sub(new,1,k)
          if osub == nsub then
             newsub = nsub
          end
        end
        newsub = utf8_gsub(newsub, "_+$", "")   --$
%<dbg>        texio.write_nl('BOLfullmatch=' .. newsub)
        local msg = "B.O.L. MATCH=" .. newsub
        log_flaw(msg, line, colno, footnote)
%    \end{macrocode}
%    Lest’s colour the matching string.
%    \begin{macrocode}
        local ns = utf8_gsub(newsub, "_", "")
        k = utf8_len(ns)
        oldsub = newsub
        local newsub = ""
        local n = start
        local l = 0
        local lo = 0
        local li = 0
        while n and newsub ~= oldsub and l < k do
          if n and n.id == HLIST then
             local nn = n.head
             for nnn in traverse(nn) do
               color_node(nnn, COLOR)
               local c = nn.char
               if not char_to_discard[c] then l = l + 1 end
             end
          elseif n then
             color_node(n, COLOR)
             li, newsub = signature(n, newsub, swap)
             l = l + li - lo
             lo = li
          end
          n = n.next
        end
     end
  end
  return new, match
end
%    \end{macrocode}
%
% \changes{v0.65}{2023/03/02}{New `check\_page\_first\_word’ function.}
%
%    The next function is meant to be called on the first line of a new
%    page.  It checks the first word: if it ends a sentence and is short
%    (up to |\luatypoMinLen| characters), the function returns |true|
%    and colours the offending word.  Otherwise it just returns |false|.
%    The function requires two arguments: the line’s first node and
%    a column number (possibly |nil|).
%
%    \begin{macrocode}
local check_page_first_word = function (node, colno, footnote)
  local COLOR = luatypo.colortbl[15]
  local match = false
  local swap = false
  local new = ""
  local minlen = luatypo.MinLen
  local len = 0
  local n = node
  local pn
  while n and n.id ~= GLYPH and n.id ~= DISC and
        (n.id ~= HLIST or n.subtype == INDENT) do
     n = n.next
  end
  local start = n
  if n and n.id == HLIST then
     start = n.head
     n = n.head
  end
  repeat
    len, new = signature (n, new, swap)
    n = n.next
  until len > minlen or (n and n.id == GLYPH and eow_char[n.char]) or
        (n and n.id == GLUE) or
        (n and n.id == KERN and n.subtype == 1)
%    \end{macrocode}
%    In French `?’ and `!’ are preceded by a glue (babel) or a kern
%    (polyglossia).
%    \begin{macrocode}
  if n and (n.id == GLUE or n.id == KERN) then
     pn = n
     n = n.next
  end
  if len <= minlen and n and n.id == GLYPH and eow_char[n.char] then
%    \end{macrocode}
%    If the line does not ends here, set |match| to |true| (otherwise
%    this line is just a short line):
%    \begin{macrocode}
     repeat
       n = n.next
     until not n or n.id == GLYPH or
           (n.id == GLUE and n.subtype == PARFILL)
     if n and n.id == GLYPH then
        match = true
     end
  end
%<dbg>  texio.write_nl('FinalWord=' .. new)
  if match then
     local msg = "ShortFinalWord=" .. new
     log_flaw(msg, 1, colno, footnote)
%    \end{macrocode}
%    Lest’s colour the final word and punctuation sign.
%    \begin{macrocode}
     local n = start
     repeat
       color_node(n, COLOR)
       n = n.next
     until eow_char[n.char]
     color_node(n, COLOR)
  end
  return match
end
%    \end{macrocode}
%
%    The next function looks for a short word (one or two
%    chars) at end of lines, compares it to a given list and colours it
%    if matches.  The first argument must be a node of type |GLYPH|,
%    usually the last line’s node, the next two are the line and
%    column number.
%
% \changes{v0.61}{2023/02/06}{`check\_regexpr’ returns a flag
%    to set pageflag in `check\_vtop’.}
%
%    \begin{macrocode}
local check_regexpr = function (glyph, line, colno, footnote)
  local COLOR = luatypo.colortbl[4]
  local lang = glyph.lang
  local match = false
  local retflag = false
  local lchar, id = is_glyph(glyph)
  local previous = glyph.prev
%    \end{macrocode}
%    First look for single chars unless the list of words is empty.
%    \begin{macrocode}
  if lang and luatypo.single[lang] then
%    \end{macrocode}
%    For single char words, the previous node is a glue.
%    \begin{macrocode}
     if lchar and previous and previous.id == GLUE then
        match = utf8_find(luatypo.single[lang], utf8.char(lchar))
        if match then
           retflag = true
           local msg = "RGX MATCH=" .. utf8.char(lchar)
           log_flaw(msg, line, colno, footnote)
           color_node(glyph,COLOR)
        end
     end
  end
%    \end{macrocode}
%    Look for two chars words unless the list of words is empty.
%    \begin{macrocode}
  if lang and luatypo.double[lang] then
     if lchar and previous and previous.id == GLYPH then
        local pchar, id = is_glyph(previous)
        local pprev = previous.prev
%    \end{macrocode}
%    For two chars words, the previous node is a glue…
%    \begin{macrocode}
        if pchar and pprev and pprev.id == GLUE then
           local pattern = utf8.char(pchar) .. utf8.char(lchar)
           match = utf8_find(luatypo.double[lang], pattern)
           if match then
              retflag = true
              local msg = "RGX MATCH=" .. pattern
              log_flaw(msg, line, colno, footnote)
              color_node(previous,COLOR)
              color_node(glyph,COLOR)
           end
        end
%    \end{macrocode}
%    …unless a kern is found between the two chars.
%    \begin{macrocode}
     elseif lchar and previous and previous.id == KERN then
        local pprev = previous.prev
        if pprev and pprev.id == GLYPH then
           local pchar, id = is_glyph(pprev)
           local ppprev = pprev.prev
           if pchar and ppprev and ppprev.id == GLUE then
              local pattern = utf8.char(pchar) .. utf8.char(lchar)
              match = utf8_find(luatypo.double[lang], pattern)
              if match then
                 retflag = true
                 local msg = "REGEXP MATCH=" .. pattern
                 log_flaw(msg, line, colno, footnote)
                 color_node(pprev,COLOR)
                 color_node(glyph,COLOR)
              end
           end
        end
     end
  end
return retflag
end
%    \end{macrocode}
%
%    The next function prints the first part of an hyphenated
%    word up to the discretionary, with a supplied colour.
%    It requires two arguments: a \node{disc} node and a (named) colour.
%
%    \begin{macrocode}
local show_pre_disc = function (disc, color)
  local n = disc
  while n and n.id ~= GLUE do
    color_node(n, color)
    n = n.prev
  end
  return n
  end
%    \end{macrocode}
%
% \begin{macro}{footnoterule-ahead}
%    The next function scans the current \node{vlist} in search
%    of a |\footnoterule|; it returns |true| if found, false otherwise.
%    The \node{rule} node above footnotes is normaly surrounded by
%    two (vertical) \node{kern} nodes, the total height is either
%    0 (standard and koma classes) or equals the rule’s height
%    (memoir class).
%
% \changes{v0.50}{2021/05/02}{New function `footnoterule\_ahead’.}
%
%    \begin{macrocode}
local footnoterule_ahead = function (head)
  local n = head
  local flag = false
  local totalht, ruleht, ht1, ht2, ht3
  if n and n.id == KERN and n.subtype == 1 then
     totalht = n.kern
     n = n.next
%<dbg>     ht1 = string.format("%.2fpt", totalht/65536)
%    \end{macrocode}
% \changes{v0.51}{2023/01/17}{In some cases glue nodes might preceed
%    the footnote rule; next line added}
%    \begin{macrocode}
     while n and n.id == GLUE do n = n.next end
     if n and n.id == RULE and n.subtype == 0 then
        ruleht = n.height
%<dbg>  ht2 = string.format("%.2fpt", ruleht/65536)
        totalht = totalht + ruleht
        n = n.next
        if n and n.id == KERN and n.subtype == 1 then
%<dbg>     ht3 = string.format("%.2fpt", n.kern/65536)
           totalht = totalht + n.kern
           if totalht == 0 or totalht == ruleht then
              flag = true
           else
%<dbg>             texio.write_nl(' ')
%<dbg>             texio.write_nl('Not a footnoterule:')
%<dbg>             texio.write('  KERN height=' .. ht1)
%<dbg>             texio.write('  RULE height=' .. ht2)
%<dbg>             texio.write('  KERN height=' .. ht3)
           end
         end
     end
  end
  return flag
end
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{check-EOP}
%    This function looks ahead of |node| in search of a page end
%    or a footnote rule and returns the flags |page_bottom| and
%    |body_bottom| [used in text and display math lines].
%    \begin{macrocode}
local check_EOP = function (node)
  local n = node
  local page_bot = false
  local body_bot = false
  while n and (n.id == GLUE    or n.id == PENALTY or
               n.id == WHATSIT )    do
    n = n.next
  end
  if not n then
     page_bot = true
     body_bot = true
  elseif footnoterule_ahead(n) then
     body_bot = true
%<dbg>     texio.write_nl('=> FOOTNOTE RULE ahead')
%<dbg>     texio.write_nl('check_vtop: last line before footnotes')
%<dbg>     texio.write_nl(' ')
  end
  return page_bot, body_bot
end
%    \end{macrocode}
% \end{macro}
%
% \changes{v0.85}{2023/09/07}{New function `check\_marginnote’.}
%
% \begin{macro}{check-marginnote}
%    This function checks margin notes for overfull/underfull lines;
%    It also warns if a margin note ends too low under the last line
%    of text.
%    \begin{macrocode}
local check_marginnote = function (head, line, colno, vpos, bpmn)
  local OverfullLines   = luatypo.OverfullLines
  local UnderfullLines  = luatypo.UnderfullLines
  local MarginparPos    = luatypo.MarginparPos
  local margintol       = luatypo.MParTol
  local marginpp        = tex.getdimen("marginparpush")
  local textht          = tex.getdimen("textheight")
  local mnflag  = false
  local ofirst = true
  local ufirst = true
  local n = head.head
  local bottom = vpos
  if vpos <= bpmn then
     bottom = bpmn + marginpp
  end
%<dbg>  texio.write_nl('*** Margin note? ***')
  repeat
    if n and (n.id == GLUE or n.id == PENALTY) then
%<dbg>     texio.write_nl('    Found GLUE or PENALTY')
       n = n.next
    elseif n and n.id == VLIST then
%<dbg>     texio.write_nl('    Found VLIST')
       n = n.head
    end
  until not n or (n.id == HLIST and n.subtype == LINE)
  local head = n
  if head then
%<dbg>     texio.write_nl('    Found HLIST')
  else
%<dbg>     texio.write_nl('    No text line found.')
  end
%<dbg>  local l = 0
  local last = head
  while head do
    local next = head.next
    if head.id == HLIST and head.subtype == LINE then
%<dbg>       l = l + 1
%<dbg>       texio.write_nl('    Checking line ' .. l)
       bottom = bottom + head.height + head.depth
       local first = head.head
       local linewd = head.width
       local hmax = linewd + tex.hfuzz
       local w,h,d = dimensions(1,2,0, first)
       local Stretch = math.max(luatypo.Stretch/100,1)
       if w > hmax and OverfullLines then
%<dbg>          texio.write(': Overfull!')
          mnflag = true
          local COLOR = luatypo.colortbl[8]
          color_line (head, COLOR)
          if ofirst then
             local msg = "OVERFULL line(s) in margin note"
             log_flaw(msg, line, colno, false)
             ofirst = false
          end
       elseif head.glue_set > Stretch and head.glue_sign == 1 and
              head.glue_order == 0 and UnderfullLines then
%<dbg>          texio.write(': Underfull!')
          mnflag = true
          local COLOR = luatypo.colortbl[9]
          color_line (head, COLOR)
          if ufirst then
             local msg = "UNDERFULL line(s) in margin note"
             log_flaw(msg, line, colno, false)
             ufirst = false
          end
       end
    end
    last = head
    head = next
  end
%<dbg>  local tht  = string.format("%.1fpt", textht/65536)
%<dbg>  local bott = string.format("%.1fpt", bottom/65536)
%<dbg>  texio.write_nl('    Bottom=' .. bott)
%<dbg>  texio.write('  TextBottom=' ..tht)
  if bottom > textht + margintol and MarginparPos then
     mnflag = true
     local COLOR = luatypo.colortbl[17]
     color_line (last, COLOR)
     local msg = "Margin note too low"
     log_flaw(msg, line, colno, false)
  end
  return bottom, mnflag
end
%    \end{macrocode}
% \end{macro}
%
% \changes{v0.88}{2026/01/06}{New function `check\_vbox’.}
%
% \begin{macro}{check-vbox}
%    This function checks vboxes for overfull/underfull lines.
%    \begin{macrocode}
local check_vbox = function (head, line, colno, vpos, lmax)
  local OverfullLines   = luatypo.OverfullLines
  local UnderfullLines  = luatypo.UnderfullLines
  local vbflag  = false
  local l = 0
  local ll = line
  local n = head.head
%<dbg>  texio.write_nl('*** vbox found ***')
  while n do
    if n.id == HLIST and n.subtype == LINE then
       l = l + 1
       if l > 1 then ll = ll + 1 end
%<dbg>       texio.write_nl('l = ' .. l)
       local first = n.head
       local linewd = n.width
       local hmax = linewd + tex.hfuzz
       local w,h,d = dimensions(1,2,0, first)
       local Stretch = math.max(luatypo.Stretch/100,1)
       if w > hmax and OverfullLines then
%<dbg>          texio.write(': Overfull!')
          vbflag = true
          local COLOR = luatypo.colortbl[8]
          color_line (n, COLOR)
          local wpt = string.format("%.2fpt", (w-n.width)/65536)
          local msg = "OVERFULL line in vbox, " .. wpt
          log_flaw(msg, ll, colno, false)
       elseif n.glue_set > Stretch and n.glue_sign == 1 and
              n.glue_order == 0 and UnderfullLines then
%<dbg>          texio.write(': Underfull!')
          vbflag = true
          local COLOR = luatypo.colortbl[9]
          color_line (n, COLOR)
          local s = string.format("%.0f%s", 100*n.glue_set, "%")
          local msg = "UNDERFULL line in vbox, stretch=" .. s
          log_flaw(msg, ll, colno, false)
       end
    end
    n = n.next
  end
  lmax = math.max(l, lmax)
%<dbg>  texio.write_nl('lmax = ' .. lmax)
  return lmax, vbflag
end
% \end{macro}
%
% \begin{macro}{get-pagebody}
%    The next function scans the \node{vlist}s on the current
%    page in search of the page body.
%    It returns the corresponding node or nil in case of failure.
%
% \changes{v0.50}{2021/05/02}{New function `get\_pagebody’ required for
%    callback `pre\_shipout\_filter’.}
%
% \changes{v0.86}{2024/01/10}{Package `stfloats’ adds 1sp to the
%    external \cs{vbox}. Be less picky regarding height test.}
%
% \changes{v0.87}{2024/04/18}{\cs{get\_pagebody} improved: it
%    failed for crop + hyperref.}
%
%    \begin{macrocode}
local get_pagebody = function (head)
  local textht = tex.getdimen("textheight")
  local fn = head.list
  local body
  repeat
    fn = fn.next
  until fn.id == VLIST and fn.height > 0
%<dbg>  texio.write_nl(' ')
%<dbg>  local ht = string.format("%.1fpt", fn.height/65536)
%<dbg>  local dp = string.format("%.1fpt", fn.depth/65536)
%<dbg>  texio.write_nl('get_pagebody: TOP VLIST')
%<dbg>  texio.write(' ht=' .. ht .. '  dp=' .. dp)
%    \end{macrocode}
%    Enter the first \node{vlist} found, recursively scan its
%    internal \node{vlist}s high enough to include the 'body'
%    the height of which is known ('textht')…
%    \begin{macrocode}
  first = fn.list
  repeat
    for n in traverse_id(VLIST,first) do
%    \end{macrocode}
%    Package `stfloats’ seems to add 1sp to the external \cs{vbox}
%    for each float found on the page.
%    Add $\pm8$sp tolerance when comparing |n.height| to
%    |\textheight|.
%    \begin{macrocode}
        if n.subtype == 0 and n.height >= textht-1 then
           if n.height <= textht+8  then
%<dbg>         local ht = string.format("%.1fpt",  n.height/65536)
%<dbg>         texio.write_nl('BODY found: ht=' .. ht)
%<dbg>         texio.write('=' .. n.height .. 'sp')
%<dbg>         texio.write_nl(' ')
              body = n
              break
           else
              first = n.list
           end
        else
%<dbg>         texio.write_nl('Skip short VLIST:')
%<dbg>         local ht = string.format("%.1fpt",  n.height/65536)
%<dbg>         local dp = string.format("%.1fpt",  n.depth/65536)
%<dbg>         texio.write('ht=' .. ht .. '=' .. n.height .. 'sp')
%<dbg>         texio.write(';  dp=' .. dp)
        end
    end
  until body or not first
  if not body then
     texio.write_nl('***lua-typo ERROR: PAGE BODY *NOT* FOUND!***')
  end
  return body
end
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{check-vtop}
%    The next function is called repeatedly by |check_page| (see below);
%    it scans the boxes found in the page body (f.i.\  columns)
%    in search of typographical flaws and logs.
%
% \changes{v0.40}{2021/04/18}{Title pages, pages with figures and/or
%    tables may not be empty pages: check `vpos’ last line’s position.}
%
% \changes{v0.60}{2023/01/28}{Loop redesigned.}
%
% \changes{v0.60}{2023/01/29}{Typographical flaws are recorded here
%    (formerly in check\_page).}
%
% \changes{v0.60}{2023/01/29}{Break `check\_vtop’ loop
%     if a two columns box starts.}
%
%    \begin{macrocode}
check_vtop = function (top, colno, vpos)
  local head = top.list
  local PAGEmin   = luatypo.PAGEmin
  local HYPHmax   = luatypo.HYPHmax
  local LLminWD   = luatypo.LLminWD
  local BackPI    = luatypo.BackPI
  local BackFuzz  = luatypo.BackFuzz
  local BackParindent   = luatypo.BackParindent
  local ShortLines      = luatypo.ShortLines
  local ShortPages      = luatypo.ShortPages
  local OverfullLines   = luatypo.OverfullLines
  local UnderfullLines  = luatypo.UnderfullLines
  local Widows          = luatypo.Widows
  local Orphans         = luatypo.Orphans
  local EOPHyphens      = luatypo.EOPHyphens
  local RepeatedHyphens = luatypo.RepeatedHyphens
  local FirstWordMatch  = luatypo.FirstWordMatch
  local ParLastHyphen   = luatypo.ParLastHyphen
  local EOLShortWords   = luatypo.EOLShortWords
  local LastWordMatch   = luatypo.LastWordMatch
  local FootnoteSplit   = luatypo.FootnoteSplit
  local ShortFinalWord  = luatypo.ShortFinalWord
  local PageFirstLine   = luatypo.PageFirstLine
  local Stretch  = math.max(luatypo.Stretch/100,1)
  local blskip   = tex.getglue("baselineskip")
  local vpos_min = PAGEmin * blskip
  vpos_min = vpos_min * 1.5
  local linewd = tex.getdimen("textwidth")
  local first_bot  = true
  local done       = false
  local footnote   = false
  local ftnsplit   = false
  local orphanflag = false
  local widowflag  = false
  local pageshort  = false
  local overfull   = false
  local underfull  = false
  local shortline  = false
  local backpar    = false
  local pflflag    = false
  local firstwd = ""
  local lastwd  = ""
  local hyphcount = 0
  local pageline = 0
  local ftnline = 0
  local line = 0
  local bpmn = 0
  local body_bottom = false
  local page_bottom = false
  local pageflag = false
  local pageno = tex.getcount("c@page")
%    \end{macrocode}
%    The main loop scans the content of the |\vtop| holding the page
%    (or column) body, footnotes included.
%    \begin{macrocode}
  while head do
    local nextnode = head.next
%    \end{macrocode}
%    Let’s scan the top nodes of this vbox: expected are \node{hlist}
%    (text lines or vboxes), \node{rule}, \node{kern}, \node{glue}…
%    \begin{macrocode}
    if head.id == HLIST and head.subtype == LINE and
          (head.height > 0 or head.depth > 0) then
%    \end{macrocode}
%    This is a text line, store its width, increment counters
%    |pageline| or |ftnline| and |line| (for |log_flaw|).
%    Let’s update |vpos| (vertical position in `sp’ units) and
%    set flag |done| to |true|.
%    \begin{macrocode}
       vpos = vpos + head.height + head.depth
       done = true
       local linewd = head.width
       local first = head.head
       local ListItem = false
       if footnote then
          ftnline = ftnline + 1
          line = ftnline
       else
          pageline = pageline + 1
          line = pageline
          pflflag = false
       end
%    \end{macrocode}
%    Is this line the last one on the page or before footnotes?
%    This has to be known early in order to set the flags |orphanflag|
%    and |ftnsplit|.
%    \begin{macrocode}
       page_bottom, body_bottom = check_EOP(nextnode)
%    \end{macrocode}
%    Is the current line overfull or underfull?
% \changes{v0.50}{2021/05/13}{Detection of overfull boxes fixed: the
%    former code didn’t work for typewriter fonts.}
% \changes{v0.80}{2023/04/18}{Colouring lines deferred until the full
%    line is scanned.}
%    \begin{macrocode}
       local hmax = linewd + tex.hfuzz
       local w,h,d = dimensions(1,2,0, first)
       if w > hmax and OverfullLines then
          pageflag = true
          overfull = true
          local wpt = string.format("%.2fpt", (w-head.width)/65536)
          local msg = "OVERFULL line " .. wpt
          log_flaw(msg, line, colno, footnote)
       elseif head.glue_set > Stretch and head.glue_sign == 1 and
              head.glue_order == 0 and UnderfullLines then
          pageflag = true
          underfull = true
          local s = string.format("%.0f%s", 100*head.glue_set, "%")
          local msg = "UNDERFULL line stretch=" .. s
          log_flaw(msg, line, colno, footnote)
       end
%    \end{macrocode}
%    In footnotes, set flag |ftnsplit| to |true| on page’s last line.
%    This flag will be reset to false if the current line ends a
%    paragraph.
%    \begin{macrocode}
       if footnote and page_bottom then
          ftnsplit = true
       end
%    \end{macrocode}
%    The current node being a line, |first| is its first node.
%    Skip margin kern and/or leftskip if any.
%
% \changes{v0.40}{2021/04/18}{Both MKERN and LFTSKIP may occur on
%    the same line.}
%
% \changes{v0.40}{2021/04/18}{All hlists of subtype LINE now
%    count as a pageline.}
%
%    \begin{macrocode}
       while first.id == MKERN or
             (first.id == GLUE and first.subtype == LFTSKIP) do
          first = first.next
       end
%    \end{macrocode}
%    Now let’s analyse the beginning of the current line.
%    \begin{macrocode}
       if first.id == LPAR then
%    \end{macrocode}
%    It starts a paragraph… Reset |parline| except in footnotes
%    (|parline| and |pageline| counts are for ``body’’ \emph{only},
%    they are frozen in footnotes).
%    \begin{macrocode}
          hyphcount = 0
          firstwd = ""
          lastwd = ""
          if not footnote then
             parline = 1
             if pageline == 1 and PageFirstLine then
%    \end{macrocode}
%    This paragraph starts on top of this page or column, is it indented?
%    \begin{macrocode}
                local nn = first.next
                if nn.id == HLIST and nn.subtype == INDENT and
                   nn.width > 0 then
                   pageflag = true
                   pflflag = true
                end
             end
             if body_bottom then
%    \end{macrocode}
%    We are at the page bottom (footnotes excluded), this ligne is
%    an orphan (unless it is the unique line of the paragraph, this
%    will be checked later when scanning the end of line).
%    \begin{macrocode}
                orphanflag = true
             end
          end
%    \end{macrocode}
%    List items begin with |LPAR| followed by an hbox.
%    \begin{macrocode}
          local nn = first.next
          if nn and nn.id == HLIST and nn.subtype == BOX then
             ListItem = true
          end
       elseif not footnote then
          parline = parline + 1
       end
%    \end{macrocode}
%    Does the first word and the one on the previous line match
%    (except lists)?
%    \begin{macrocode}
       if FirstWordMatch then
          local flag = not ListItem and (line > 1)
          firstwd, flag =
             check_line_first_word(firstwd, first, line, colno,
                                   flag, footnote)
          if flag then
             pageflag = true
          end
       end
%    \end{macrocode}
%    Check the page’s first word (end of sentence?).
%    \begin{macrocode}
       if ShortFinalWord and pageline == 1 and parline > 1 and
          check_page_first_word(first, colno, footnote) then
          pageflag = true
       end
%    \end{macrocode}
%    Check for possible vboxes (minipages, parboxes) inside this line.
%    \begin{macrocode}
       local cn = first
       local lmax = 1
       repeat
         if cn.id == VLIST and cn.subtype == 0 then
            lmax, vbflag = check_vbox (cn, line, colno, vpos, lmax)
         end
         cn = cn.next
       until not cn
       if not footnote then
          line = line + lmax - 1
          parline = parline + lmax - 1
          pageline = pageline + lmax - 1
       end
%    \end{macrocode}
%    Let’s now check the end of line: |ln| (usually a rightskip)
%    and |pn| are the last two nodes.
%    \begin{macrocode}
       local ln = slide(first)
%    \end{macrocode}
%    Skip a possible RULE pointing an overfull line.
%    \begin{macrocode}
       if ln.id == RULE and ln.subtype == 0 then
          ln = ln.prev
       end
       local pn = ln.prev
       if pn and pn.id == GLUE and pn.subtype == PARFILL then
%    \end{macrocode}
%    CASE 1: this line ends the paragraph, reset |ftnsplit| and |orphan|
%    flags to false…
%    \begin{macrocode}
%<dbg>    texio.write_nl('EOL CASE 1: end of paragraph')
          hyphcount = 0
          ftnsplit = false
          orphanflag = false
%    \end{macrocode}
%    it is a widow if it is the page’s first line and it does’nt
%    start a new paragraph. If so, we flag this line as `widow’;
%    colouring full lines will take place later.
%    \begin{macrocode}
          if pageline == 1 and parline > 1 then
             widowflag = true
          end
%    \end{macrocode}
%    |PFskip| is the rubber length (in sp) added to complete the line.
%    \begin{macrocode}
          local PFskip = effective_glue(pn,head)
          if ShortLines then
             local llwd = linewd - PFskip
%<dbg>             local PFskip_pt = string.format("%.1fpt", PFskip/65536)
%<dbg>             local llwd_pt = string.format("%.1fpt", llwd/65536)
%<dbg>             texio.write_nl('PFskip= ' .. PFskip_pt)
%<dbg>             texio.write('  llwd= ' .. llwd_pt)
%    \end{macrocode}
%    |llwd| is the line’s length. Is it too short?
%    \begin{macrocode}
             if llwd < LLminWD then
                pageflag = true
                shortline = true
                local msg = "SHORT LINE: length=" ..
                            string.format("%.0fpt", llwd/65536)
                log_flaw(msg, line, colno, footnote)
             end
          end
%    \end{macrocode}
%    Does this (end of paragraph) line ends too close to the right
%    margin?
%    \begin{macrocode}
          if BackParindent and PFskip < BackPI and
             PFskip >= BackFuzz and parline > 1 then
             pageflag = true
             backpar = true
             local msg = "NEARLY FULL line: backskip=" ..
                         string.format("%.1fpt", PFskip/65536)
             log_flaw(msg, line, colno, footnote)
          end
%    \end{macrocode}
%    Does the last word and the one on the previous line match?
%    \begin{macrocode}
          if LastWordMatch then
             local flag = true
             if PFskip > BackPI or line == 1 then
                flag = false
             end
             local pnp = pn.prev
             lastwd, flag =
                check_line_last_word(lastwd, pnp, line, colno,
                                     flag, footnote)
             if flag then
                pageflag = true
             end
          end
       elseif pn and pn.id == DISC then
%    \end{macrocode}
%    CASE 2: the current line ends with an hyphen.
%    \begin{macrocode}
%<dbg>    texio.write_nl('EOL CASE 2: hyphen')
          hyphcount = hyphcount + 1
          if hyphcount > HYPHmax and RepeatedHyphens then
             local COLOR = luatypo.colortbl[3]
             local pg = show_pre_disc (pn,COLOR)
             pageflag = true
             local msg = "REPEATED HYPHENS: more than " .. HYPHmax
             log_flaw(msg, line, colno, footnote)
          end
          if (page_bottom or body_bottom) and EOPHyphens then
%    \end{macrocode}
%    This hyphen occurs on the page’s last line (body or footnote),
%    colour (differently) the last word.
%    \begin{macrocode}
             pageflag = true
             local msg = "LAST WORD SPLIT"
             log_flaw(msg, line, colno, footnote)
             local COLOR = luatypo.colortbl[2]
             local pg = show_pre_disc (pn,COLOR)
          end
%    \end{macrocode}
%    Track matching words at end of line.
%    \begin{macrocode}
          if LastWordMatch then
             local flag = true
             lastwd, flag =
                check_line_last_word(lastwd, pn, line, colno,
                                     flag, footnote)
             if flag then
                pageflag = true
             end
          end
          if nextnode and ParLastHyphen then
%    \end{macrocode}
%    Does the next line end the current paragraph? If so, |nextnode| is
%    a `linebreak penalty’, the next one is a `baseline skip’ and the
%    node after is a \node{hlist-1} with |glue_order=2|.
%    \begin{macrocode}
             local nn = nextnode.next
             local nnn = nil
             if nn and nn.next then
                nnn = nn.next
                if nnn.id == HLIST and nnn.subtype == LINE and
                   nnn.glue_order == 2 then
                   pageflag = true
                   local msg = "HYPHEN on next to last line"
                   log_flaw(msg, line, colno, footnote)
                   local COLOR = luatypo.colortbl[1]
                   local pg = show_pre_disc (pn,COLOR)
                end
             end
          end
%    \end{macrocode}
%    CASE 3: the current line ends with anything else (\node{glyph},
%    \node{mkern}, \node{hlist}, etc.), then reset |hyphcount| and
%    check for `LastWordMatch’ and `EOLShortWords’.
%    \begin{macrocode}
       else
%<dbg>    texio.write_nl('EOL CASE 3')
          hyphcount = 0
%    \end{macrocode}
%    Track matching words at end of line and short words.
%    \begin{macrocode}
          if LastWordMatch and pn then
             local flag = true
             lastwd, flag =
                check_line_last_word(lastwd, pn, line, colno,
                                     flag, footnote)
             if flag then
                pageflag = true
             end
          end
          if EOLShortWords then
             while pn and pn.id ~= GLYPH and pn.id ~= HLIST do
               pn = pn.prev
             end
             if pn and pn.id == GLYPH then
                if check_regexpr(pn, line, colno, footnote) then
                   pageflag = true
                end
             end
          end
       end
%    \end{macrocode}
%    End of scanning for the main type of node (text lines).
%    Let’s colour the whole line if necessary.  If more than one kind
%    of flaw \emph{affecting the whole line} has been detected,
%    a special colour is used [homearchy, repeated hyphens, etc.
%    will still be coloured properly:  |color_line| doesn’t override
%    previously set colours].
%    \begin{macrocode}
       if pflflag then
          local COLOR = luatypo.colortbl[5]
          color_line (head, COLOR)
          local msg = "INDENTED first line (paragraph start)."
          log_flaw(msg, line, colno, false)
       end
       if widowflag and Widows then
          pageflag = true
          local msg = "WIDOW"
          log_flaw(msg, line, colno, footnote)
          local COLOR = luatypo.colortbl[5]
          if backpar or shortline or overfull or underfull then
             COLOR = luatypo.colortbl[16]
             if backpar then backpar = false end
             if shortline then shortline = false end
             if overfull then overfull = false end
             if underfull then underfull = false end
          end
          color_line (head, COLOR)
          widowflag = false
       elseif orphanflag and Orphans then
          pageflag = true
          local msg = "ORPHAN"
          log_flaw(msg, line, colno, footnote)
          local COLOR = luatypo.colortbl[6]
          if overfull or underfull then
             COLOR = luatypo.colortbl[16]
          end
          color_line (head, COLOR)
       elseif ftnsplit and FootnoteSplit then
          pageflag = true
          local msg = "FOOTNOTE SPLIT"
          log_flaw(msg, line, colno, footnote)
          local COLOR = luatypo.colortbl[14]
          if overfull or underfull then
             COLOR = luatypo.colortbl[16]
          end
          color_line (head, COLOR)
       elseif shortline then
          local COLOR = luatypo.colortbl[7]
          color_line (head, COLOR)
          shortline = false
       elseif overfull then
          local COLOR = luatypo.colortbl[8]
          color_line (head, COLOR)
          overfull = false
       elseif underfull then
          local COLOR = luatypo.colortbl[9]
          color_line (head, COLOR)
          underfull = false
       elseif backpar then
          local COLOR = luatypo.colortbl[13]
          color_line (head, COLOR)
          backpar = false
       end
    elseif head and head.id == HLIST and head.subtype == BOX then
      if head.width > 0 then
         if head.height == 0 then
%    \end{macrocode}
%    This is a possible margin note.
%    \begin{macrocode}
            bpmn, mnflag = check_marginnote(head, line, colno, vpos, bpmn)
            if mnflag then pageflag = true end
            page_bottom, body_bottom = check_EOP(nextnode)
         else
%    \end{macrocode}
%    Leave |check_vtop| if a two columns box starts.
%    \begin{macrocode}
            local hf = head.list
            if hf and hf.id == VLIST and hf.subtype == 0 then
%<dbg>               texio.write_nl('check_vtop: BREAK => multicol')
%<dbg>               texio.write_nl(' ')
               break
            else
               line = line + 1
               pageline = pageline + 1
            end
         end
      end
%    \end{macrocode}
% \changes{v0.80}{2023/04/18}{hlist-2: added detection of page bottom
%    and increment vpos.}
%    This is an |\hbox| (f.i.\ centred), let’s update |vpos| and
%    check for page bottom. Counter pageline is \emph{not} incremented.
%    \begin{macrocode}
      vpos = vpos + head.height + head.depth
      page_bottom, body_bottom = check_EOP (nextnode)
    elseif head.id == HLIST and
          (head.subtype == EQN or head.subtype == ALIGN) and
          (head.height > 0 or head.depth > 0) then
%    \end{macrocode}
%    This line is a displayed or aligned equation.
%    Let’s update |vpos| and the line number.
%
% \changes{v0.50}{2021/05/05}{Consider displayed and aligned equations
%    too for overfull boxes.}
%
%    \begin{macrocode}
       vpos = vpos + head.height + head.depth
       if footnote then
          ftnline = ftnline + 1
          line = ftnline
       else
          pageline = pageline + 1
          line = pageline
       end
%    \end{macrocode}
%    Is this line the last one on the page or before footnotes?
%    (information needed to set the |pageshort| flag).
%    \begin{macrocode}
       page_bottom, body_bottom = check_EOP (nextnode)
%    \end{macrocode}
%    Let’s check for an `Overfull box’.  For a displayed equation
%    it is straightforward.  A set of aligned equations all have the
%    same (maximal) width; in order to avoid highlighting the whole
%    set, we have to look for glues at the end of embedded
%    \node{hlist}s.
%    \begin{macrocode}
       local fl = true
       local wd = 0
       local hmax = 0
       if head.subtype == EQN then
          local f = head.list
          wd = rangedimensions(head,f)
          hmax = head.width + tex.hfuzz
       else
          wd = head.width
          hmax = tex.getdimen("linewidth") + tex.hfuzz
       end
       if wd > hmax and OverfullLines then
          if head.subtype == ALIGN then
             local first = head.list
             for n in traverse_id(HLIST, first) do
                 local last = slide(n.list)
                 if last.id == GLUE and last.subtype == USER then
                    wd = wd - effective_glue(last,n)
                    if wd <= hmax then fl = false end
                 end
             end
          end
          if fl then
             pageflag = true
             local w = wd - hmax + tex.hfuzz
             local wpt = string.format("%.2fpt", w/65536)
             local msg = "OVERFULL equation " .. wpt
             log_flaw(msg, line, colno, footnote)
             local COLOR = luatypo.colortbl[8]
             color_line (head, COLOR)
          end
       end
    elseif head and head.id == RULE and head.subtype == 0  then
       vpos = vpos + head.height + head.depth
%    \end{macrocode}
%    This is a \node{rule}, possibly a footnote rule. It has most likely
%    been detected on the previous line (then |body_bottom=true|) but
%    might have no text before (footnote-only page!).
%    \begin{macrocode}
       local prev = head.prev
       if body_bottom or footnoterule_ahead (prev) then
%    \end{macrocode}
%    If it is, set the |footnote| flag and reset some counters
%    and flags for the coming footnote lines.
%    \begin{macrocode}
%<dbg>       texio.write_nl('check_vtop: footnotes start')
%<dbg>       texio.write_nl(' ')
          footnote = true
          ftnline = 0
          body_bottom = false
          orphanflag = false
          hyphcount = 0
          firstwd = ""
          lastwd = ""
       end
%    \end{macrocode}
%    Track short pages: check the number of lines at end of page,
%    in case this number is low, \emph{and} |vpos| is less than
%    |vpos_min|, fetch the last line and colour it.
%    \begin{macrocode}
    elseif body_bottom and head.id == GLUE and head.subtype == 0 then
       if first_bot then
%<dbg>          local vpos_pt = string.format("%.1fpt", vpos/65536)
%<dbg>          local vmin_pt = string.format("%.1fpt", vpos_min/65536)
%<dbg>          texio.write_nl('pageline=' .. pageline)
%<dbg>          texio.write_nl('vpos=' .. vpos_pt)
%<dbg>          texio.write('   vpos_min=' .. vmin_pt)
%<dbg>          if page_bottom then
%<dbg>             local tht    = tex.getdimen("textheight")
%<dbg>             local tht_pt = string.format("%.1fpt", tht/65536)
%<dbg>             texio.write('   textheight=' .. tht_pt)
%<dbg>          end
%<dbg>          texio.write_nl(' ')
          if pageline > 1 and pageline < PAGEmin
             and vpos < vpos_min  and ShortPages  then
             pageshort = true
             pageflag = true
             local msg = "SHORT PAGE: only " .. pageline .. " lines"
             log_flaw(msg, line, colno, footnote)
             local COLOR = luatypo.colortbl[10]
             local n = head
             repeat
               n = n.prev
             until n.id == HLIST and n.subtype == LINE
             color_line (n, COLOR)
          end
          first_bot = false
       end
    elseif head.id == GLUE then
%    \end{macrocode}
%    Increment |vpos| on other vertical glues.
%    \begin{macrocode}
       vpos = vpos + effective_glue(head,top)
    elseif head.id == KERN and head.subtype == 1 then
%    \end{macrocode}
%    This is a vertical kern, let’s update |vpos|.
%    \begin{macrocode}
       vpos = vpos + head.kern
    elseif head.id == VLIST then
%    \end{macrocode}
%    This is a |\vbox|, let’s update |vpos|.
%    \begin{macrocode}
       vpos = vpos + head.height + head.depth
%<dbg>    local tht = head.height + head.depth
%<dbg>    local tht_pt = string.format("%.1fpt", tht/65536)
%<dbg>    texio.write(' vbox: height=' .. tht_pt)
    end
  head = nextnode
  end
%<dbg>  if nextnode then
%<dbg>     texio.write('Exit check_vtop,  next=')
%<dbg>     texio.write(tostring(node.type(nextnode.id)))
%<dbg>     texio.write('-'.. nextnode.subtype)
%<dbg>  else
%<dbg>     texio.write_nl('Exit check_vtop,  next=nil')
%<dbg>  end
%<dbg>  texio.write_nl('')
%    \end{macrocode}
%    Update the list of flagged pages avoiding duplicates:
%    \begin{macrocode}
  if pageflag then
     local plist = luatypo.pagelist
     local lastp = tonumber(string.match(plist, "%s(%d+),%s$"))
     if not lastp or pageno > lastp then
        luatypo.pagelist = luatypo.pagelist .. tostring(pageno) .. ", "
     end
  end
  return head, done
%    \end{macrocode}
%    |head| is nil unless |check_vtop| exited on a two column start.
%    |done| is true unless |check_vtop| found no text line.
%    \begin{macrocode}
end
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{check-page}
%    This is the main function which will be added to the
%    |pre_shipout_filter| callback unless option |None| is selected.
%    It executes |get_pagebody| which returns a node of type
%    \node{vlist}-0, then scans this \node{vlist}: expected are
%    \node{vlist}-0 (full width block) or \node{hlist}-2 (multi
%    column block).
%    The vertical position of the current node is stored in the |vpos|
%    dimension (integer in `sp’ units, 1\,pt~=~65536\,sp). It is used to
%    detect short pages.
%
% \changes{v0.60}{2023/01/29}{Loop redesigned to properly handle
%    two colums.}
%
%    \begin{macrocode}
luatypo.check_page = function (head)
  local pageno = tex.getcount("c@page")
  local body = get_pagebody(head)
  local textwd, textht, checked, boxed
  local top, first, next
  local n2, n3, col, colno
  local vpos = 0
  local footnote = false
  local count = 0
  if body then
     top = body
     first = body.list
     textwd = tex.getdimen("textwidth")
     textht = tex.getdimen("textheight")
%<dbg>     texio.write_nl('Body=' .. tostring(node.type(top.id)))
%<dbg>     texio.write('-' .. tostring(top.subtype))
%<dbg>     texio.write(';  First=' .. tostring(node.type(first.id)))
%<dbg>     texio.write('-' .. tostring(first.subtype))
%<dbg>     texio.write_nl(' ')
  end
  if ((first and first.id == HLIST and first.subtype == BOX) or
      (first and first.id == VLIST and first.subtype == 0))      and
     (first.width == textwd and first.height > 0 and not boxed)  then
%    \end{macrocode}
%    Some classes (\cls{memoir}, \cls{tugboat} …) use one more level
%    of bowing for two columns, let’s step down one level.
%    \begin{macrocode}
%<dbg>     local boxwd = string.format("%.1fpt", first.width/65536)
%<dbg>     texio.write_nl('One step down: boxwd=' .. boxwd)
%<dbg>     texio.write_nl('Glue order=' .. tostring(first.glue_order))
%<dbg>     texio.write_nl(' ')
     top = body.list
%    \end{macrocode}
%    A float on top of a page is a VLIST-0 included in a VLIST-0 (body),
%    it should not trigger this step down. Workaround: the body will be
%    scanned again.
%    \begin{macrocode}
     if first.id == VLIST then
        boxed = body
     end
  end
%    \end{macrocode}
%    Main loop:
%    \begin{macrocode}
  while top do
    first = top.list
    next = top.next
%<dbg>    count = count + 1
%<dbg>    texio.write_nl('Page loop' .. count)
%<dbg>    texio.write(': top=' .. tostring(node.type(top.id)))
%<dbg>    texio.write('-' .. tostring(top.subtype))
%<dbg>    if first then
%<dbg>      texio.write('  first=' .. tostring(node.type(first.id)))
%<dbg>      texio.write('-' .. tostring(first.subtype))
%<dbg>    end
    if top and top.id == VLIST and top.subtype == 0 and
       top.width > textwd/2                             then
%    \end{macrocode}
%    Single column, run |check_vtop| on the top vlist.
%    \begin{macrocode}
%<dbg>       local boxht = string.format("%.1fpt", top.height/65536)
%<dbg>       local boxwd = string.format("%.1fpt", top.width/65536)
%<dbg>       texio.write_nl('**VLIST: ')
%<dbg>       texio.write(tostring(node.type(top.id)))
%<dbg>       texio.write('-' .. tostring(top.subtype))
%<dbg>       texio.write('  wd=' .. boxwd .. '  ht=' .. boxht)
%<dbg>       texio.write_nl(' ')
       local n, ok = check_vtop(top,colno,vpos)
       if ok then checked = true end
       if n then
          next = n
       end
    elseif (top and top.id == HLIST and top.subtype == BOX) and
           (first and first.id == VLIST and first.subtype == 0) and
           (first.height > 0 and first.width > 0) then
%    \end{macrocode}
%    Two or more columns, each one is boxed in a vlist.\par
%    Run |check_vtop| on every column.
%    \begin{macrocode}
%<dbg>           texio.write_nl('**MULTICOL type1:')
%<dbg>           texio.write_nl(' ')
       colno = 0
       for col in traverse_id(VLIST, first) do
           colno = colno + 1
%<dbg>           texio.write_nl('Start of col.' .. colno)
%<dbg>           texio.write_nl(' ')
       local n, ok = check_vtop(col,colno,vpos)
       if ok then checked = true end
%<dbg>           texio.write_nl('End of col.' .. colno)
%<dbg>           texio.write_nl(' ')
       end
       colno = nil
       top = top.next
%<dbg>       texio.write_nl('MULTICOL type1 END: next=')
%<dbg>       texio.write(tostring(node.type(top.id)))
%<dbg>       texio.write('-' .. tostring(top.subtype))
%<dbg>       texio.write_nl(' ')
    elseif (top and top.id == HLIST and top.subtype == BOX) and
           (first and first.id == HLIST and first.subtype == BOX) and
           (first.height > 0 and first.width > 0) then
%    \end{macrocode}
%    Two or more columns, each one is boxed in an hlist which
%    holds a vlist.\par
%    Run |check_vtop| on every column.
%    \begin{macrocode}
%<dbg>     texio.write_nl('**MULTICOL type2:')
%<dbg>     texio.write_nl(' ')
       colno = 0
       for n in traverse_id(HLIST, first) do
           colno = colno + 1
           local col = n.list
           if col and col.list then
%<dbg>              texio.write_nl('Start of col.' .. colno)
%<dbg>              texio.write_nl(' ')
              local n, ok = check_vtop(col,colno,vpos)
              if ok then checked = true end
%<dbg>              texio.write_nl('End of col.' .. colno)
%<dbg>              texio.write_nl(' ')
           end
       end
       colno = nil
    end
%    \end{macrocode}
%    Workaround for top floats: check the whole body again.
%    \begin{macrocode}
    if boxed and not next then
       next = boxed
       boxed = nil
    end
    top = next
  end
  if not checked then
     luatypo.failedlist = luatypo.failedlist .. tostring(pageno) .. ", "
%<dbg>     texio.write_nl(' ')
%<dbg>     texio.write_nl('WARNING: no text line found on page ')
%<dbg>     texio.write(tostring(pageno))
%<dbg>     texio.write_nl(' ')
  end
  return true
end
return luatypo.check_page
\end{luacode}
%    \end{macrocode}
% \end{macro}
%
% \changes{v0.50}{2021/05/02}{Callback `pre\_output\_filter’ replaced
%    by `pre\_shipout\_filter’, in the former the material is not boxed
%    yet and footnotes are not visible.}
%
%    NOTE: |effective_glue| requires a `parent’ node, as pointed out by
%    Marcel Krüger on S.E., this implies using |pre_shipout_filter|
%    instead of |pre_output_filter|.
%
%    Add the |luatypo.check_page| function to the |pre_shipout_filter|
%    callback (with priority 1 for colour attributes to be effective),
%    unless option |None| is selected.
%
%    \begin{macrocode}
\AtBeginDocument{%
  \directlua{
    if not luatypo.None then
       luatexbase.add_to_callback
           ("pre_shipout_filter",luatypo.check_page,"check_page",1)
    end
  }%
}
%    \end{macrocode}
%
%    Load a config file if present in LaTeX’s search path or
%    set reasonnable defaults.
%
% \changes{v0.61}{2023/02/07}{Colours mygrey, myred renamed as
%    LTgrey, LTred.}
%
%    \begin{macrocode}
\InputIfFileExists{lua-typo.cfg}%
   {\PackageInfo{lua-typo.sty}{"lua-typo.cfg" file loaded}}%
   {\PackageInfo{lua-typo.sty}{"lua-typo.cfg" file not found.
                               \MessageBreak Providing default values.}%
    \definecolor{LTgrey}{gray}{0.6}%
    \definecolor{LTred}{rgb}{1,0.55,0}
    \definecolor{LTline}{rgb}{0.7,0,0.3}
    \luatypoSetColor1{red}%       Paragraph last full line hyphenated
    \luatypoSetColor2{red}%       Page last word hyphenated
    \luatypoSetColor3{red}%       Hyphens on to many consecutive lines
    \luatypoSetColor4{red}%       Short word at end of line
    \luatypoSetColor5{cyan}%      Widow
    \luatypoSetColor6{cyan}%      Orphan
    \luatypoSetColor7{cyan}%      Paragraph ending on a short line
    \luatypoSetColor8{blue}%      Overfull lines
    \luatypoSetColor9{blue}%      Underfull lines
    \luatypoSetColor{10}{red}%    Nearly empty page
    \luatypoSetColor{11}{LTred}%  First word matches
    \luatypoSetColor{12}{LTred}%  Last word matches
    \luatypoSetColor{13}{LTgrey}% Paragraph ending on a nearly full line
    \luatypoSetColor{14}{cyan}%   Footnote split
    \luatypoSetColor{15}{red}%    Too short first (final) word on the page
    \luatypoSetColor{16}{LTline}% Line color for multiple flaws
    \luatypoSetColor{17}{red}%    Margin note ending too low
    \luatypoBackPI=1em\relax
    \luatypoBackFuzz=2pt\relax
    \ifdim\parindent=0pt \luatypoLLminWD=20pt\relax
    \else\luatypoLLminWD=2\parindent\relax\fi
    \luatypoStretchMax=200\relax
    \luatypoHyphMax=2\relax
    \luatypoPageMin=5\relax
    \luatypoMinFull=3\relax
    \luatypoMinPart=4\relax
    \luatypoMinLen=4\relax
    \luatypoMarginparTol=\baselineskip
   }%
%    \end{macrocode}
% \iffalse
%</sty>
% \fi
%
%  \section{Configuration file}
%
% \iffalse
%<*cfg>
% \fi
%
% \let\theCodelineNo\relax
%    \begin{macrocode}
%%% Configuration file for lua-typo.sty
%%% These settings can also be overruled in the preamble.

%% Minimum gap between end of paragraphs’ last lines and the right margin
\luatypoBackPI=1em\relax
\luatypoBackFuzz=2pt\relax

%% Minimum length of paragraphs’ last lines
\ifdim\parindent=0pt \luatypoLLminWD=20pt\relax
\else \luatypoLLminWD=2\parindent\relax
\fi

%% Maximum number of consecutive hyphenated lines
\luatypoHyphMax=2\relax

%% Nearly empty pages: minimum number of lines
\luatypoPageMin=5\relax

%% Maximum acceptable stretch before a line is tagged as Underfull
\luatypoStretchMax=200\relax

%% Minimum number of matching characters for words at begin/end of line
\luatypoMinFull=3\relax
\luatypoMinPart=4\relax

%% Minimum number of characters for the first word on a page if it ends
%% a sentence (version >= 0.65).
\ifdefined\luatypoMinLen \luatypoMinLen=4\relax\fi

%% Acceptable marginpars must end at |\luatypoMarginparTol| under
%% the page’s last line or above (version >= 0.85).
\ifdefined\luatypoMarginparTol \luatypoMarginparTol=\baselineskip \fi

%% Default colours = red, cyan, blue, LTgrey, LTred, LTline.
\definecolor{LTgrey}{gray}{0.6}
\definecolor{LTred}{rgb}{1,0.55,0}
\definecolor{LTline}{rgb}{0.7,0,0.3}
\luatypoSetColor1{red}%       Paragraph last full line hyphenated
\luatypoSetColor2{red}%       Page last word hyphenated
\luatypoSetColor3{red}%       Hyphens on to many consecutive lines
\luatypoSetColor4{red}%       Short word at end of line
\luatypoSetColor5{cyan}%      Widow
\luatypoSetColor6{cyan}%      Orphan
\luatypoSetColor7{cyan}%      Paragraph ending on a short line
\luatypoSetColor8{blue}%      Overfull lines
\luatypoSetColor9{blue}%      Underfull lines
\luatypoSetColor{10}{red}%    Nearly empty page
\luatypoSetColor{11}{LTred}%  First word matches
\luatypoSetColor{12}{LTred}%  Last word matches
\luatypoSetColor{13}{LTgrey}% Paragraph ending on a nearly full line
\luatypoSetColor{14}{cyan}%   Footnote split
\luatypoSetColor{15}{red}%    Too short first (final) word on the page
\luatypoSetColor{16}{LTline}% Line color for multiple flaws
\luatypoSetColor{17}{red}%    Margin note ending too low

%% Language specific settings (example for French):
%% short words (two letters max) to be avoided at end of lines.
%%\luatypoOneChar{french}{"A À Ô Y"}
%%\luatypoTwoChars{french}{"Ah Au Ça Çà Ce De Il Je La Là Le Ma Me Ne Ni
%%                          Oh On Or Ou Où Sa Se Si Ta Tu Va Vu"}
%    \end{macrocode}
%
% \iffalse
%</cfg>
% \fi
%
% \changes{v0.60}{2023/02/02}{Debugging stuff added.}
%
%  \section{Debugging lua-typo}
%
%    Personal stuff useful \emph{only} for maintaining the
%    \pkg{lua-typo} package has been added at the end of
%    \file{lua-typo.dtx} in version~0.60.
%    It is not extracted unless a) both `|\iffalse|’ and `|\fi|’ on
%    lines 41 and 46 at the beginning of \file{lua-typo.dtx} are
%    commented out and b) all files are generated again by a
%    |luatex lua-typo.dtx| command; then a (very) verbose version of
%    \file{lua-typo.sty} is generated together with
%    a \file{scan-page.sty} file which can be used instead of
%    \file{lua-typo.sty} to show the structured list of nodes
%    found in a document.
%
% \iffalse
%<*scan>
% \fi
%
% \begin{Debugging}
%    \begin{macrocode}
\ifdefined\directlua
  \RequirePackage{luatexbase,luacode,luacolor}
  \RequirePackage{kvoptions,atveryend}
\else
  \PackageError{This package is meant for LuaTeX only! Aborting}
               {No more information available, sorry!}
\fi

\newdimen\luatypoLLminWD
\newdimen\luatypoBackPI
\newdimen\luatypoBackFuzz
\newcount\luatypoStretchMax
\newcount\luatypoHyphMax
\newcount\luatypoPageMin
\newcount\luatypoMinFull
\newcount\luatypoMinPart
\newcount\luatypo@LANGno
\newcount\luatypo@options
\newtoks\luatypo@single
\newtoks\luatypo@double

\AtBeginDocument{%
  \directlua{
    luatypo.HYPHmax = tex.count.luatypoHyphMax
    luatypo.PAGEmin = tex.count.luatypoPageMin
    luatypo.Stretch = tex.count.luatypoStretchMax
    luatypo.MinFull = tex.count.luatypoMinFull
    luatypo.MinPart = tex.count.luatypoMinPart
    luatypo.LLminWD = tex.dimen.luatypoLLminWD
    luatypo.BackPI  = tex.dimen.luatypoBackPI
    luatypo.BackFuzz  = tex.dimen.luatypoBackFuzz
   }%
}

\AtVeryEndDocument{%
  \directlua{
    texio.write_nl(' ')
    texio.write_nl('***************************')
    texio.write_nl('*** PAGE SCANNING ONLY: ***')
    texio.write_nl('*** NO CHECK PERFORMED! ***')
    texio.write_nl('***************************')
    texio.write_nl(' ')
   }%
}

\newcommand*{\luatypoOneChar}[2]{}
\newcommand*{\luatypoTwoChars}[2]{}
\newcommand*{\luatypoSetColor}[2]{}

\begin{luacode}
luatypo = { }
luatypo.single = { }
luatypo.double = { }
luatypo.colortbl  = { }
luatypo.pagelist  = " "
luatypo.failedlist  = " "

local hyphcount = 0
local pageno = 0
local prevno = 0
local pageflag = false

local DISC  = node.id("disc")
local GLYPH = node.id("glyph")
local GLUE  = node.id("glue")
local KERN  = node.id("kern")
local HLIST = node.id("hlist")
local VLIST = node.id("vlist")
local LPAR  = node.id("local_par")
local MKERN = node.id("margin_kern")
local PENALTY = node.id("penalty")
local USRSKIP  = 0
local PARSKIP  = 3
local LEFTSKIP = 8
local TOPSKIP = 10
local PARFILL = 15
local LINE    = 1
local BOX     = 2
local USER = 0

local dimensions = node.dimensions
local rangedimensions = node.rangedimensions
local effective_glue = node.effective_glue
local set_attribute = node.set_attribute
local slide = node.slide
local traverse = node.traverse
local traverse_id = node.traverse_id
local has_field = node.has_field
local uses_font = node.uses_font
local is_glyph  = node.is_glyph

%    \end{macrocode}
%    Some helpers functions.
%    \begin{macrocode}
local get_pagebody = function (head)
  local textht = tex.getdimen("textheight")
  local fn = head.list
  local body
  repeat
    fn = fn.next
  until fn.id == VLIST and fn.height > 0
  local ht = string.format("%.1fpt", fn.height/65536)
  local dp = string.format("%.1fpt", fn.depth/65536)
  texio.write_nl("SKIP vlist: ht=" .. ht .. " dp=" .. dp)
  first = fn.list
  repeat
    for n in traverse_id(VLIST,first) do
        if n.subtype == 0 and n.height >= textht-1 then
           if n.height <= textht+8  then
              local ht = string.format("%.1fpt",  n.height/65536)
              texio.write_nl('BODY found: ht=' .. ht)
              texio.write('=' .. n.height .. 'sp')
              texio.write_nl(' ')
              body = n
              break
           else
              first = n.list
           end
        else
              texio.write_nl('Skip short VLIST:')
              local ht = string.format("%.1fpt",  n.height/65536)
              local dp = string.format("%.1fpt",  n.depth/65536)
              texio.write('ht=' .. ht .. '=' .. n.height .. 'sp')
              texio.write(';  dp=' .. dp)
        end
    end
  until body or not first
  if not body then
     texio.write_nl('***lua-typo ERROR: PAGE BODY *NOT* FOUND!***')
  end
  return body
end

local print_subtype = function (node,parent)
  local n = node
  if n.id == GLYPH then
     texio.write(utf8.char(n.char))
  elseif n.id == GLUE then
     texio.write(tostring(n.subtype))
     real_pt = string.format("%.1fpt", effective_glue(n,parent)/65536)
     texio.write(' realwd=' .. real_pt)
  elseif n.id == DISC then
     local c = ""
     if n.replace then
        for nn in traverse_id(GLYPH, n.replace) do
            c = c .. utf8.char(nn.char)
        end
        texio.write(c)
     end
     if n.pre then
        c = ""
        for nn in traverse_id(GLYPH, n.pre) do
            c = c .. utf8.char(nn.char)
        end
        texio.write(' pre=' .. c)
     end
     if n.post then
        c = ""
        for nn in traverse_id(GLYPH, n.post) do
            c = c .. utf8.char(nn.char)
        end
     texio.write(' post=' .. c)
     end
  elseif n.subtype and n.subtype < 256  then
     texio.write(tostring(n.subtype))
     if n.height and n.depth then
        ht_pt = string.format("%.1fpt", n.height/65536)
        dp_pt = string.format("%.1fpt", n.depth/65536)
        texio.write(' ht=' .. ht_pt .. ' dp=' .. dp_pt)
        if n.width then
           wd_pt = string.format("%.1fpt", n.width/65536)
           texio.write(' wd=' .. wd_pt)
        end
     end
  end
end

%    \end{macrocode}
%    Main function.  It prints a structured list of nodes found on the
%    page body (header and footer excluded, footnotes included).
%    \begin{macrocode}
local scanvlist = function (head)
  local textht = tex.getdimen("textheight")
  local textwd = tex.getdimen("textwidth")
  local pageno = tex.getcount("c@page")
  texio.write_nl('PAGE ' .. pageno)
  texio.write('  textheight='.. string.format("%.1fpt", textht/65536))
  texio.write('  textwidth=' .. string.format("%.1fpt", textwd/65536))
  local linecount = 0
  local body, parent, ht_pt, dp_pt, real_pt
  body = get_pagebody(head)
  if body then
     parent = body
     head = body.head
     texio.write_nl('Enter outer vbox (body) of type ')
     texio.write(tostring(node.type(body.id)))
     texio.write('-')
     texio.write(tostring(body.subtype))
  else
     texio.write_nl('Function get_pagebody returned NIL!')
     texio.write_nl('=> Scanning whole page: node type ')
     texio.write(tostring(node.type(head.id)))
     texio.write('-')
     texio.write(tostring(head.subtype))
  end
  while head do
    texio.write_nl(tostring(node.type(head.id)))
    texio.write('-')
    print_subtype(head,body)
    if head.kern then
       ht_pt = string.format("%.1fpt", head.kern/65536)
       texio.write(' KERN ht/wd=' .. ht_pt)
    end
    local next = head.next
    local first = head.head
    for n in traverse(first) do
        parent = head
        texio.write_nl('  ' .. tostring(node.type(n.id)))
        texio.write('-')
        print_subtype(n,head)
        if n.id == VLIST or n.id == HLIST then
          local ff = n.head
          for nn in traverse(ff) do
             parent = n
             texio.write_nl('    ' .. tostring(node.type(nn.id)))
             texio.write('-')
             print_subtype(nn,n)
             if nn.id == VLIST or nn.id == HLIST then
                local f3 = nn.head
                for n3 in traverse(f3) do
                  parent = nn
                  texio.write_nl('      ' .. tostring(node.type(n3.id)))
                  texio.write('-')
                  print_subtype(n3,nn)
                  if n3.id == VLIST or n3.id == HLIST then
                     local f4 = n3.head
                     for n4 in traverse(f4) do
                       parent = n3
                       texio.write_nl('        ' .. tostring(node.type(n4.id)))
                       texio.write('-')
                       print_subtype(n4,n3)
                       if n4.id == VLIST or n4.id == HLIST then
                          local f5 = n4.head
                          for n5 in traverse(f5) do
                            parent = n4
                            texio.write_nl('          ' .. tostring(node.type(n5.id)))
                            texio.write('-')
                            print_subtype(n5,n4)
                          end
                       end
                     end
                  end
                end
              end
          end
        end
    end
    head = next
  end
  return true
end

%    \end{macrocode}
%    Add the main function to callback |pre_shipout_filter|.
%    \begin{macrocode}
luatexbase.add_to_callback("pre_shipout_filter",scanvlist,"check_vlists")
%    \end{macrocode}
\end{luacode}
% \end{Debugging}
%
% \iffalse
%</scan>
% \fi
%
% \Finale
\endinput

%%% Local Variables:
%%% fill-column: 72
%%% coding: utf-8
%%% TeX-engine: luatex
%%% End:
