/*
*  ChkTeX, error searching & report routines.
*  Copyright (C) 1995-96 Jens T. Berger Thielemann
*
*  This program is free software; you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation; either version 2 of the License, or
*  (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with this program; if not, write to the Free Software
*  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*  Contact the author at:
*              Jens Berger
*              Spektrumvn. 4
*              N-0666 Oslo
*              Norway
*              E-mail: <[email protected]>
*
*
*/


#include "ChkTeX.h"
#include "FindErrs.h"
#include "OpSys.h"
#include "Utility.h"
#include "Resource.h"

#if HAVE_PCRE || HAVE_POSIX_ERE

#if HAVE_PCRE
#include <pcreposix.h>
#else
#include <regex.h>
#endif

#define REGEX_FLAGS REG_EXTENDED
#define NUM_MATCHES 10
#define ERROR_STRING_SIZE 100

regex_t* RegexArray = NULL;
regex_t* SilentRegex = NULL;
int NumRegexes = 0;

#endif

int FoundErr = EXIT_SUCCESS;
int LastWasComment = FALSE;
int SeenSpace = FALSE;
int FrenchSpacing = FALSE;

/***************************** ERROR MESSAGES ***************************/

#undef MSG
#define MSG(num, type, inuse, ctxt, text) {num, type, inuse, ctxt, text},

struct ErrMsg LaTeXMsgs[emMaxFault + 1] = {
   ERRMSGS {emMaxFault, etErr, iuOK, 0, INTERNFAULT}
};

#define istex(c)        (isalpha((unsigned char)c) || (AtLetter && (c == '@')))
#define CTYPE(func) \
static int my_##func(int c) \
{ \
  return(func(c)); \
}

#define SUPPRESSED_ON_LINE(c)  (LineSuppressions & ((uint64_t)1<<c))

#define INUSE(c) \
   ((LaTeXMsgs[(enum ErrNum) c].InUse == iuOK) && !SUPPRESSED_ON_LINE(c))

#define PSERR2(pos,len,err,a,b) \
   PrintError(err, CurStkName(&InputStack), RealBuf, pos, len, Line, a, b)

#define PSERRA(pos,len,err,a) \
   PrintError(err, CurStkName(&InputStack), RealBuf, pos, len, Line, a)

#define HEREA(len, err, a)     PSERRA(BufPtr - Buf - 1, len, err, a)
#define PSERR(pos,len,err)     PSERRA(pos,len,err,"")

#define HERE(len, err)         HEREA(len, err, "")

#define SKIP_BACK(ptr, c, check) \
   while((c = *ptr--)) \
   { \
       if (!(check))  break; \
   } \
   ptr++;

#define SKIP_AHEAD(ptr, c, check) \
   while((c = *ptr++)) \
   { \
       if (!(check)) \
           break; \
   } \
   ptr--;


/*  -=><=- -=><=- -=><=- -=><=- -=><=- -=><=- -=><=- -=><=- -=><=-  */

/*
* A list of characters LaTeX considers as an end-of-sentence characters, which
* should be detected when whether sentence spacing is correct.
*
*/
static const char LTX_EosPunc[] = { '.', ':', '?', '!', 0 };

/*
* General punctuation characters used on your system.
*/
static const char LTX_GenPunc[] = { ',', ';', 0 };

/*
* A list of characters LaTeX considers as an small punctuation characters,
* which should not be preceded by a \/.
*/
static const char LTX_SmallPunc[] = { '.', ',', 0 };

/*
* A list of characters that could be considered to start a new
* sentence, or not.
*
* This allows "Mr. ``X'' " to warn about the same as "Mr. X".
*
*/
static const char LTX_BosPunc[] = {'`', '(', '[', 0};

/*
* String used to delimit a line suppression.  This string must be
* followed immediately by the number of the warning to be suppressed.
* If more than one warning is to be suppressed, then multiple copies
* of LineSuppDelim+number must be used.
*/
const char LineSuppDelim[] = "chktex ";

/*
* String used to delimit a file suppression.  This string must be
* followed immediately by the number of the warning to be suppressed.
* If more than one warning is to be suppressed, then multiple copies
* of FileSuppDelim+number must be used.
*/
const char FileSuppDelim[] = "chktex-file ";

/*
* A bit field used to hold the suppressions for the current line.
*/
static uint64_t LineSuppressions;
/*
* A bit field used to hold the suppressions of numbered user warnings
* for the current line.
*/
static uint64_t UserLineSuppressions;

static unsigned long Line;

static const char *RealBuf;
static char *BufPtr;

static int ItFlag = efNone;
static int MathFlag = efNone;

NEWBUF(Buf, BUFFER_SIZE);
NEWBUF(CmdBuffer, BUFFER_SIZE);
NEWBUF(ArgBuffer, BUFFER_SIZE);

static enum ErrNum PerformCommand(const char *Cmd, char *Arg);

#ifdef isdigit
CTYPE(isdigit)
#else
#  define my_isdigit isdigit
#endif

#ifdef isalpha
CTYPE(isalpha)
#else
#  define my_isalpha isalpha
#endif

/*
* Reads in a TeX token from Src and puts it in Dest.
*
*/


static char *GetLTXToken(char *Src, char *Dest)
{
   int Char;

   if (Src && *Src)
   {
       if (*Src == '\\')
       {
           *Dest++ = *Src++;
           Char = *Dest++ = *Src++;

           if (istex(Char))
           {
               while (istex(Char))
                   Char = *Dest++ = *Src++;

               Src--;
               Dest--;
           }

       }
       else
           *Dest++ = *Src++;
       *Dest = 0;
   }
   else
       Src = NULL;

   return (Src);
}


/*
* Scans the `SrcBuf' for a LaTeX arg, and puts that arg into `Dest'.
* `Until' specifies what we'll copy. Assume the text is
* "{foo}bar! qux} baz".
*  GET_TOKEN       => "{foo}"
*  GET_STRIP_TOKEN => "foo"
*  '!'             => "{foo}bar!" (i.e. till the first "!")
* Returns NULL if we can't find the argument, ptr to the first character
* after the argument in other cases.
*
* If one of the tokens found is in the wl wordlist, and we're in the
* outer most paren, and Until isn't a single character, we'll stop.
* You may pass NULL as wl.
*
* We assume that you've previously skipped over leading spaces.
*
*/

#define GET_TOKEN       256
#define GET_STRIP_TOKEN 257

static char *GetLTXArg(char *SrcBuf, char *OrigDest, const int Until,
       struct WordList *wl)
{
   char *Retval;
   char *TmpPtr;
   char *Dest = OrigDest;
   unsigned long DeliCnt = 0;

   *Dest = 0;
   TmpPtr = SrcBuf;

   switch (Until)
   {
   case GET_STRIP_TOKEN:
   case GET_TOKEN:
       while ((Retval = GetLTXToken(TmpPtr, Dest)))
       {
           switch (*Dest)
           {
           case '{':
               DeliCnt++;
               break;
           case '}':
               DeliCnt--;
           }
           Dest += Retval - TmpPtr;
           TmpPtr = Retval;

           if (!DeliCnt || ((DeliCnt == 1) && wl && HasWord(Dest, wl)))
               break;
       }

       if (Retval && (*OrigDest == '{') && (Until == GET_STRIP_TOKEN))
       {
           int len = strlen(OrigDest+1);
           memmove(OrigDest, OrigDest + 1, len  + 1);
           /* Strip the last '}' off */
           OrigDest[len-1] = 0;
       }
       break;
   default:
       DeliCnt = TRUE;
       while ((Retval = GetLTXArg(TmpPtr, Dest, GET_TOKEN, NULL)))
       {
           if (*Dest == Until)
               DeliCnt = FALSE;

           Dest += Retval - TmpPtr;
           TmpPtr = Retval;

           if (!DeliCnt)
               break;
       }
       break;
   }
   *Dest = 0;

   return (Retval);
}


static char *PreProcess(void)
{
   char *TmpPtr;

   /* Reset any line suppressions  */
   LineSuppressions = *(uint64_t *)StkTop(&FileSuppStack);
   UserLineSuppressions = *(uint64_t *)StkTop(&UserFileSuppStack);

   /* Kill comments. */
   strcpy(Buf, RealBuf);

   TmpPtr = Buf;

   LastWasComment = FALSE;
   while ((TmpPtr = strchr(TmpPtr, '%')))
   {
       char *EscapePtr = TmpPtr;
       int NumBackSlashes = 0;
       while (EscapePtr != Buf && EscapePtr[-1] == '\\')
       {
           ++NumBackSlashes;
           --EscapePtr;
       }

       /* If there is an even number of backslashes, then it's a comment. */
       if ((NumBackSlashes % 2) == 0)
       {
           LastWasComment = TRUE;
           PSERR(TmpPtr - Buf, 1, emComment);
           *TmpPtr = 0;
           /* Check for line suppressions */
           if (!NoLineSupp)
           {
               int error;
               const int MaxSuppressionBits = 63;

               /* Convert to lowercase to compare with LineSuppDelim */
               EscapePtr = ++TmpPtr; /* move past NUL terminator */
               while ( *EscapePtr )
               {
                   *EscapePtr = tolower((unsigned char)*EscapePtr);
                   ++EscapePtr;
               }

               EscapePtr = TmpPtr; /* Save it for later */
               while ((TmpPtr = strstr(TmpPtr, FileSuppDelim))) {
                   TmpPtr += STRLEN(FileSuppDelim);
                   error = atoi(TmpPtr);

                   if (abs(error) > MaxSuppressionBits)
                   {
                       PrintPrgErr(pmSuppTooHigh, error, MaxSuppressionBits);
                   }
                   uint64_t errbit = ((uint64_t)1 << abs(error));
                   if (error > 0)
                   {
                       *(uint64_t *)StkTop(&FileSuppStack) |= errbit;
                       LineSuppressions |= errbit;
                   }
                   else
                   {
                       *(uint64_t *)StkTop(&UserFileSuppStack) |= errbit;
                       UserLineSuppressions |= errbit;
                   }
               }
               TmpPtr = EscapePtr;

               while ((TmpPtr = strstr(TmpPtr, LineSuppDelim))) {

                   TmpPtr += STRLEN(LineSuppDelim);
                   error = atoi(TmpPtr);

                   if (abs(error) > MaxSuppressionBits)
                   {
                       PrintPrgErr(pmSuppTooHigh, error, MaxSuppressionBits);
                   }

                   if (error > 0)
                   {
                       LineSuppressions |= ((uint64_t)1 << error);
                   }
                   else
                   {
                       UserLineSuppressions |= ((uint64_t)1 << (-error));
                   }
               }
           }
           break;
       }
       TmpPtr++;
   }
   return (Buf);
}

/*
* Interpret environments
*/

static void PerformEnv(char *Env, int Begin)
{
   static char VBStr[BUFFER_SIZE] = "";

   if (HasWord(Env, &MathEnvir))
   {
       if (Begin)
           PushMode(TRUE, &MathModeStack);
       else
       {
           if (!CurStkMode(&MathModeStack))
               PSERRA(BufPtr - Buf - 4, 1, emMathModeConfusion, "on");
           StkPop(&MathModeStack);
       }
   }

   if (HasWord(Env, &TextEnvir))
   {
       if (Begin)
           PushMode(FALSE, &MathModeStack);
       else
       {
           if (CurStkMode(&MathModeStack))
               PSERRA(BufPtr - Buf - 4, 1, emMathModeConfusion, "off");
           StkPop(&MathModeStack);
       }
   }

   if (Begin && HasWord(Env, &VerbEnvir))
   {
       VerbMode = TRUE;
       strcpy(VBStr, "\\end{");
       strcat(VBStr, Env);
       strcat(VBStr, "}");
       VerbStr = VBStr;
   }
}

static char *SkipVerb(void)
{
   char *TmpPtr = BufPtr;
   int TmpC;

   if (VerbMode && BufPtr)
   {
       if (!(TmpPtr = strstr(BufPtr, VerbStr)))
           BufPtr = &BufPtr[strlen(BufPtr)];
       else
       {
           VerbMode = FALSE;
           BufPtr = &TmpPtr[strlen(VerbStr)];
           SKIP_AHEAD(BufPtr, TmpC, LATEX_SPACE(TmpC));
           if (*BufPtr)
               PSERR(BufPtr - Buf, strlen(BufPtr) - 2, emIgnoreText);
       }
   }
   return (TmpPtr);
}

#define CHECKDOTS(wordlist, dtval) \
for(i = 0; (i < wordlist.Stack.Used) && !(Back && Front);  i++) \
{ if(!strafter(PstPtr, wordlist.Stack.Data[i])) \
        Back = dtval; \
  if(!strinfront(PrePtr, wordlist.Stack.Data[i])) \
        Front = dtval; }



/*
* Checks that the dots are correct
*/

static enum DotLevel CheckDots(char *PrePtr, char *PstPtr)
{
   unsigned long i;
   int TmpC;
   enum DotLevel Front = dtUnknown, Back = dtUnknown;

   if (CurStkMode(&MathModeStack))
   {
       PrePtr--;
#define SKIP_EMPTIES(macro, ptr) macro(ptr, TmpC, \
(LATEX_SPACE(TmpC) || (TmpC == '{') || (TmpC == '}')))

       SKIP_EMPTIES(SKIP_BACK, PrePtr);
       SKIP_EMPTIES(SKIP_AHEAD, PstPtr);

       CHECKDOTS(CenterDots, dtCDots);

       if (!(Front && Back))
       {
           CHECKDOTS(LowDots, dtLDots);
       }
       return (Front & Back);
   }
   else
       return (dtLDots);

}

static const char *Dot2Str(enum DotLevel dl)
{
   const char *Retval = INTERNFAULT;
   switch (dl)
   {
   case dtUnknown:
       Retval = "\\cdots or \\ldots";
       break;
   case dtDots:
       Retval = "\\dots";
       break;
   case dtCDots:
       Retval = "\\cdots";
       break;
   case dtLDots:
       Retval = "\\ldots";
       break;
   }
   return Retval;
}

/*
* Wipes a command, according to the definition in WIPEARG
*/

static void WipeArgument(const char *Cmd, char *CmdPtr)
{
   unsigned long CmdLen = strlen(Cmd);
   const char *Format;
   char *TmpPtr;
   int c, TmpC;

   if (Cmd && *Cmd)
   {
       TmpPtr = &CmdPtr[CmdLen];
       Format = &Cmd[CmdLen + 1];

       while (TmpPtr && *TmpPtr && *Format)
       {
           switch (c = *Format++)
           {
           case '*':
               SKIP_AHEAD(TmpPtr, TmpC, LATEX_SPACE(TmpC));
               if (*TmpPtr == '*')
                   TmpPtr++;
               break;
           case '[':
               SKIP_AHEAD(TmpPtr, TmpC, LATEX_SPACE(TmpC));
               if (*TmpPtr == '[')
                   TmpPtr = GetLTXArg(TmpPtr, ArgBuffer, ']', NULL);
               break;
           case '(':
               SKIP_AHEAD(TmpPtr, TmpC, LATEX_SPACE(TmpC));
               if (*TmpPtr == '(')
                   TmpPtr = GetLTXArg(TmpPtr, ArgBuffer, ')', NULL);
               break;
           case '{':
               SKIP_AHEAD(TmpPtr, TmpC, LATEX_SPACE(TmpC));
               TmpPtr = GetLTXArg(TmpPtr, ArgBuffer, GET_TOKEN, NULL);
           case '}':
           case ']':
           case ')':
               break;
           default:
               PrintPrgErr(pmWrongWipeTemp, &Cmd[strlen(Cmd) + 1]);
               break;
           }
       }

       if (TmpPtr)
           strwrite(CmdPtr+CmdLen, VerbClear, TmpPtr - CmdPtr - CmdLen);
       else
           strxrep(CmdPtr+CmdLen, "()[]{}", *VerbClear);
   }
}

/*
* Checks italic.
*
*/

static void CheckItal(const char *Cmd)
{
   int TmpC;
   char *TmpPtr;
   if (HasWord(Cmd, &NonItalic))
       ItState = itOff;
   else if (HasWord(Cmd, &Italic))
       ItState = itOn;
   else if (HasWord(Cmd, &ItalCmd))
   {
       TmpPtr = BufPtr;
       SKIP_AHEAD(TmpPtr, TmpC, LATEX_SPACE(TmpC));
       if (*TmpPtr == '{')
       {
           ItFlag = ItState ? efItal : efNoItal;
           ItState = itOn;
       }
   }
}

/*
* Interpret isolated commands.
*
*/

static void PerformBigCmd(char *CmdPtr)
{
   char *TmpPtr;
   const char *ArgEndPtr;
   unsigned long CmdLen = strlen(CmdBuffer);
   int TmpC;
   enum ErrNum ErrNum;
   struct ErrInfo *ei;

   enum DotLevel dotlev, realdl = dtUnknown;

   TmpPtr = BufPtr;
   SKIP_AHEAD(TmpPtr, TmpC, LATEX_SPACE(TmpC));

   ArgEndPtr = GetLTXArg(TmpPtr, ArgBuffer, GET_STRIP_TOKEN, NULL);

   /* Kill `\verb' commands */

   if (WipeVerb)
   {
       if (!strcmp(CmdBuffer, "\\verb"))
       {
           if (*BufPtr && (*BufPtr != '*' || BufPtr[1]))
           {
               if (*BufPtr == '*')
                   TmpPtr = strchr(&BufPtr[2], BufPtr[1]);
               else
                   TmpPtr = strchr(&BufPtr[1], *BufPtr);
               if (TmpPtr)
                   strwrite(CmdPtr, VerbClear, (TmpPtr - CmdPtr) + 1);
               else
                   PSERR(CmdPtr - Buf, 5, emNoArgFound);
           }
       }
   }

   if (HasWord(CmdBuffer, &IJAccent))
   {
       if (ArgEndPtr)
       {
           TmpPtr = ArgBuffer;
           SKIP_AHEAD(TmpPtr, TmpC, TmpC == '{');      /* } */

           if ((*TmpPtr == 'i') || (*TmpPtr == 'j'))
               PrintError(emAccent, CurStkName(&InputStack), RealBuf,
                          CmdPtr - Buf, (long)strlen(CmdBuffer), Line,
                          CmdBuffer, *TmpPtr,
                          CurStkMode(&MathModeStack) ? "math" : "");
       }
       else
           PSERR(CmdPtr - Buf, CmdLen, emNoArgFound);
   }

   if (HasWord(CmdBuffer, &NotPreSpaced) && SeenSpace)
       PSERRA(CmdPtr - Buf - 1, 1, emRemPSSpace, CmdBuffer);

   if ((TmpPtr = HasWord(CmdBuffer, &NoCharNext)))
   {
       char *BPtr = BufPtr;

       TmpPtr += strlen(TmpPtr) + 1;
       SKIP_AHEAD(BPtr, TmpC, LATEX_SPACE(TmpC));

       if (strchr(TmpPtr, *BPtr))
       {
           PSERR2(CmdPtr - Buf, CmdLen, emNoCharMean, CmdBuffer, *BPtr);
       }
   }

   /* LaTeX environment tracking */
   if (!strcmp(CmdBuffer, "\\begin") || !strcmp(CmdBuffer, "\\end"))
   {
       if (ArgEndPtr)
       {
           if (!strcmp(ArgBuffer, "document"))
               InHeader = FALSE;

           if (CmdBuffer[1] == 'b')
           {
               if (!(PushErr(ArgBuffer, Line, CmdPtr - Buf,
                             CmdLen, RealBuf, &EnvStack)))
                   PrintPrgErr(pmNoStackMem);
           }
           else
           {
               if ((ei = PopErr(&EnvStack)))
               {
                   if (strcmp(ei->Data, ArgBuffer))
                       PrintError(emExpectC, CurStkName(&InputStack), RealBuf,
                                  CmdPtr - Buf,
                                  (long) strlen(CmdBuffer),
                                  Line, ei->Data, ArgBuffer);

                   FreeErrInfo(ei);
               }
               else
                   PrintError(emSoloC, CurStkName(&InputStack), RealBuf,
                              CmdPtr - Buf,
                              (long) strlen(CmdBuffer),
                              Line, ArgBuffer);
           }

           PerformEnv(ArgBuffer, (int) CmdBuffer[1] == 'b');
       }
       else
           PSERR(CmdPtr - Buf, CmdLen, emNoArgFound);
   }

   /* ConTeXt \start \stop tracking */
   if (!strncmp(CmdBuffer, "\\start", 6) || !strncmp(CmdBuffer, "\\stop", 5))
   {
       if (CmdBuffer[3] == 'a') /* start */
       {
           TmpPtr = CmdBuffer + 6;
           if (!(PushErr(TmpPtr, Line, CmdPtr - Buf + 6,
                         CmdLen - 6, RealBuf, &ConTeXtStack)))
               PrintPrgErr(pmNoStackMem);
       }
       else
       {
           TmpPtr = CmdBuffer + 5;
           if ((ei = PopErr(&ConTeXtStack)))
           {
               if (strcmp(ei->Data, TmpPtr))
                   PrintError(emExpectConTeXt, CurStkName(&InputStack), RealBuf,
                              CmdPtr - Buf + 5,
                              (long) strlen(TmpPtr),
                              Line, ei->Data, TmpPtr);

               FreeErrInfo(ei);
           }
           else
           {
               PrintError(emSoloC, CurStkName(&InputStack), RealBuf,
                          CmdPtr - Buf,
                          (long) strlen(CmdBuffer),
                          Line, TmpPtr);
           }
       }
       /* TODO: Do I need to call PerformEnv? */
       /* It handles math and verbatim environments */
   }

   CheckItal(CmdBuffer);

   if ((ErrNum = PerformCommand(CmdBuffer, BufPtr)))
       PSERR(CmdPtr - Buf, CmdLen, ErrNum);

   if (!strcmp(CmdBuffer, "\\cdots"))
       realdl = dtCDots;

   if (!strcmp(CmdBuffer, "\\ldots"))
       realdl = dtLDots;

   if (!strcmp(CmdBuffer, "\\dots"))
       realdl = dtLDots;

   if (realdl != dtUnknown)
   {
       dotlev = CheckDots(CmdPtr, BufPtr);
       if (dotlev && (dotlev != realdl))
       {
           const char *cTmpPtr = Dot2Str(dotlev);
           PSERRA(CmdPtr - Buf, CmdLen, emEllipsis, cTmpPtr);
       }
   }

   if ((TmpPtr = HasWord(CmdBuffer, &WipeArg)))
       WipeArgument(TmpPtr, CmdPtr);
}

/*
* Check user abbreviations. Pass a pointer to the `.';
* also ensure that it's followed by spaces, etc.
*
* Note: We assume that all abbrevs have been transferred from
* AbbrevCase into Abbrev.
*/

static void CheckAbbrevs(const char *Buffer)
{
   long i;
   char *TmpPtr;
   const char *AbbPtr;

   if (INUSE(emInterWord))
   {
       TmpPtr = TmpBuffer + Abbrev.MaxLen + 2;
       *TmpPtr = 0;
       AbbPtr = Buffer;

       for (i = Abbrev.MaxLen; i >= 0; i--)
       {
           *--TmpPtr = *AbbPtr--;
           if (!isalpha((unsigned char)*AbbPtr) &&
               /* Ignore spacing problems after commands if desired */
               (*AbbPtr != '\\' || (CmdSpace & csInterWord)) &&
               HasWord(TmpPtr, &Abbrev))
           {
               PSERR(Buffer - Buf + 1, 1, emInterWord);
           }
           if (!*AbbPtr)
               break;
       }
   }
}


/*
* Check misc. things which can't be included in the main loop.
*
*/

static void CheckRest(void)
{
   unsigned long Count;
   long CmdLen;
   char *UsrPtr;

   /* Search for user-specified warnings */

#if ! (HAVE_PCRE || HAVE_POSIX_ERE)

   if (INUSE(emUserWarnRegex) && UserWarnRegex.Stack.Used > 0)
   {
       PrintPrgErr(pmNoRegExp);
       ClearWord( &UserWarnRegex );
   }
   else if (INUSE(emUserWarn))
   {
       strcpy(TmpBuffer, Buf);
   }

#else

   if (INUSE(emUserWarnRegex) && UserWarnRegex.Stack.Used > 0)
   {
       static char error[ERROR_STRING_SIZE];
       static regmatch_t MatchVector[NUM_MATCHES];
       int rc;
       int len = strlen(TmpBuffer);
       strcpy(TmpBuffer, Buf);

       /* Compile all regular expressions if not already compiled. */
       if ( !RegexArray && UserWarnRegex.Stack.Used > 0 )
       {
           RegexArray = (regex_t*)malloc( sizeof(regex_t) * UserWarnRegex.Stack.Used );
           if (!RegexArray)
           {
               /* Allocation failed. */
               PrintPrgErr(pmNoRegexMem);
               ClearWord(&UserWarnRegex);
               NumRegexes = 0;
           }
           else
           {
               NumRegexes = 0;
               FORWL(Count, UserWarnRegex)
               {
                   char *pattern = UserWarnRegex.Stack.Data[Count];
                   char *CommentEnd = NULL;

                   /* See if it's got a special name that it goes by.
                      Only use the comment if it's at the very beginning. */
                   if ( strncmp(pattern,"(?#",3) == 0 )
                   {
                       CommentEnd = strchr(pattern, ')');
                       /* TODO: check for PCRE/POSIX only regexes */
                       if ( CommentEnd != NULL )
                       {
                           *CommentEnd = '\0';
                           /* We're leaking a little here, but this was never freed until exit anyway... */
                           UserWarnRegex.Stack.Data[NumRegexes] = pattern+3;

                           /* Compile past the end of the comment so that it works with POSIX too. */
                           pattern = CommentEnd + 1;
                       }
                   }

                   /* Ignore PCRE and POSIX specific regexes.
                    * This is mostly to make testing easier. */
                   if ( strncmp(pattern,"PCRE:",5) == 0 )
                   {
                       #if HAVE_PCRE
                       pattern += 5;
                       #else
                       continue;
                       #endif
                   }
                   if ( strncmp(pattern,"POSIX:",6) == 0 )
                   {
                       #if HAVE_POSIX_ERE
                       pattern += 6;
                       #else
                       continue;
                       #endif
                   }

                   rc = regcomp((regex_t*)(&RegexArray[NumRegexes]),
                                pattern, REGEX_FLAGS);

                   /* Compilation failed: print the error message */
                   if (rc != 0)
                   {
                       /* TODO: decide whether a non-compiling regex should completely stop, or just be ignored */
                       regerror(rc,(regex_t*)(&RegexArray[NumRegexes]),
                                error, ERROR_STRING_SIZE);
                       PrintPrgErr(pmRegexCompileFailed, pattern, error);
                   }
                   else
                   {
                       if ( !CommentEnd )
                       {
                           ((char*)UserWarnRegex.Stack.Data[NumRegexes])[0] = '\0';
                       }
                       ++NumRegexes;
                   }
               }
           }
       }

       for (Count = 0; Count < NumRegexes; ++Count)
       {
           int offset = 0;
           char *ErrMessage = UserWarnRegex.Stack.Data[Count];
           const int NamedWarning = strlen(ErrMessage) > 0;

           while (offset < len)
           {
               /* Check if this warning should be suppressed. */
               if (UserLineSuppressions && NamedWarning)
               {
                   /* The warning can be named with positive or negative numbers. */
                   int UserWarningNumber = abs(atoi(ErrMessage));
                   if (UserLineSuppressions & ((uint64_t)1 << UserWarningNumber))
                   {
                       break;
                   }
               }

               rc = regexec( (regex_t*)(&RegexArray[Count]), TmpBuffer+offset,
                             NUM_MATCHES, MatchVector, 0);
               /* Matching failed: handle error cases */
               if (rc != 0)
               {
                   switch(rc)
                   {
                       case REG_NOMATCH:
                           /* no match, no problem */
                           break;
                       default:
                           regerror(rc, (regex_t*)(&RegexArray[Count]),
                                    error, ERROR_STRING_SIZE);
                           PrintPrgErr(pmRegexMatchingError, error);
                           break;
                   }

                   offset = len; /* break out of loop */
               }
               else
               {
#define MATCH (MatchVector[0])
                   if ( NamedWarning )
                   {
                       /* User specified error message */
                       PSERR2(offset + MATCH.rm_so, MATCH.rm_eo - MATCH.rm_so,
                              emUserWarnRegex,
                              strlen(ErrMessage), ErrMessage);
                   }
                   else
                   {
                       /* Default -- show the match */
                       PSERR2(offset + MATCH.rm_so, MATCH.rm_eo - MATCH.rm_so,
                              emUserWarnRegex,
                              /* The format specifier expects an int */
                              (int)(MATCH.rm_eo - MATCH.rm_so),
                              TmpBuffer + offset + MATCH.rm_so);
                   }
                   if ( MATCH.rm_eo == 0 )
                   {
                       /* Break out of loop if the match was empty.
                        * This avoids an infinite loop when the match
                        * is empty, e.g $ */
                       offset = len;
                   }
                   else
                   {
                       offset += MATCH.rm_eo;
                   }
#undef MATCH
               }
           }
       }
   }
   else if (INUSE(emUserWarn))
   {
       strcpy(TmpBuffer, Buf);
   }

#endif


   if (INUSE(emUserWarn))
   {
       FORWL(Count, UserWarn)
       {
           for (UsrPtr = TmpBuffer;
                (UsrPtr = strstr(UsrPtr, UserWarn.Stack.Data[Count]));
                UsrPtr++)
           {
               CmdLen = strlen(UserWarn.Stack.Data[Count]);
               PSERRA(UsrPtr - TmpBuffer, CmdLen, emUserWarn, UserWarn.Stack.Data[Count]);
           }
       }

       strlwr(TmpBuffer);

       FORWL(Count, UserWarnCase)
       {
           for (UsrPtr = TmpBuffer;
                (UsrPtr = strstr(UsrPtr, UserWarnCase.Stack.Data[Count]));
                UsrPtr++)
           {
               CmdLen = strlen(UserWarnCase.Stack.Data[Count]);
               PSERRA(UsrPtr - TmpBuffer, CmdLen, emUserWarn, UserWarnCase.Stack.Data[Count]);
           }
       }
   }
}


/*
* Checks that the dash-len is correct.
*/

static void CheckDash(void)
{
   char *TmpPtr;
   int TmpC;
   long TmpCount, Len;
   struct WordList *wl = NULL;
   unsigned long i;
   int Errored;
   char *PrePtr = &BufPtr[-2];

   TmpPtr = BufPtr;
   SKIP_AHEAD(TmpPtr, TmpC, TmpC == '-');
   TmpCount = TmpPtr - BufPtr + 1;

   if (CurStkMode(&MathModeStack))
   {
       if (TmpCount > 1)
           HERE(TmpCount, emWrongDash);
   }
   else
   {
       if (LATEX_SPACE(*PrePtr) && LATEX_SPACE(*TmpPtr))
           wl = &WordDash;
       if (isdigit((unsigned char)*PrePtr) && isdigit((unsigned char)*TmpPtr))
           wl = &NumDash;
       if (isalpha((unsigned char)*PrePtr) && isalpha((unsigned char)*TmpPtr))
           wl = &HyphDash;

       if (wl)
       {
           Errored = TRUE;
           FORWL(i, *wl)
           {
               Len = strtol(wl->Stack.Data[i], NULL, 0);
               if (TmpCount == Len)
               {
                   Errored = FALSE;
                   break;
               }
           }

           if (Errored)
           {
               struct WordList *el = &DashExcpt;

               FORWL(i, *el)
               {
                   char *exception = el->Stack.Data[i];

                   char *e = exception;
                   while ( *e )
                   {
                       if ( *e == '-' && 0 == strncmp( BufPtr, e, strlen(e) ) )
                       {
                           char *f = e;
                           TmpPtr = BufPtr;
                           while ( f > exception && *(--f) == *(--TmpPtr) )
                           {
                               /* Nothing */
                           }

                           if ( f <= exception && *f == *TmpPtr )
                           {
                               Errored = FALSE;
                               break;
                           }
                       }

                       ++e;
                   }

                   if ( !Errored )
                       break;
               }
           }

           /* Check DashExcpt looking for phrase with hyphenation that doesn't
            * match what's in DashExcpt.  This really only makes sense for
            * HyphDash, but it should be cheap in the other cases. */
           if (!Errored)
           {
               TmpPtr = BufPtr-1;

               SKIP_BACK(TmpPtr, TmpC, (TmpC == '-'));
               SKIP_BACK(TmpPtr, TmpC, isalpha(TmpC));
               /* If we found a dash going backwards, the we already checked
                * this on the first dash */
               if (*TmpPtr != '-')
               {
                   /* PrePtr now points to the beginning of the hyphenated phrase */
                   PrePtr = ++TmpPtr;

                   struct WordList *el = &DashExcpt;

                   FORWL(i, *el)
                   {
                       char *e = el->Stack.Data[i];
                       TmpPtr = PrePtr;

                       /* Walk through the strings until we find a
                        * mismatch.  */
                       int FoundHyphenDiff = FALSE;
                       while (*e && *TmpPtr && *e == *TmpPtr)
                       {
                           /* Skip past characters that are the same */
                           while (*e && *TmpPtr && *e == *TmpPtr)
                           {
                               ++e;
                               ++TmpPtr;
                           }
                           /* Skip past differences in hyphens */
                           while (*e == '-' && *TmpPtr != '-')
                           {
                               ++e;
                               FoundHyphenDiff = TRUE;
                           }
                           while (*TmpPtr == '-' && *e != '-')
                           {
                               ++TmpPtr;
                               FoundHyphenDiff = TRUE;
                           }
                       }

                       /* If there was no mismatch all the way to the end of e,
                        * and TmpPtr is not in the middle of a word, then they
                        * matched ignoring hyphens, so we have found the one
                        * DashExcpt element we care about and don't have to
                        * check any others.  Moreover, if we found a difference
                        * in hyphenation, then we must warn because it matches
                        * something in DashExcpt but with improper hyphenation.
                        * It's possible they could put the same phrase in twice
                        * with different hyphenations, but that seems pretty
                        * pathological. */
                       if (*e == '\0' && !isalpha((unsigned char)*TmpPtr))
                       {
                           if (FoundHyphenDiff)
                               Errored = TRUE;
                           break;
                       }
                   }
               }
           }

           if (Errored)
               HERE(TmpCount, emWrongDash);
       }
   }
}

/*
* Pushes and pops nesting characters.
*
*/

static void HandleBracket(char Char)
{
   unsigned long BrOffset;     /* Offset into BrOrder array */
   struct ErrInfo *ei;
   char TmpC, Match;
   char ABuf[2], BBuf[2];
   char *TmpPtr;

   AddBracket(Char);

   if ((BrOffset = BrackIndex(Char)) != ~0UL)
   {
       if (BrOffset & 1)       /* Closing bracket of some sort */
       {
           if ((ei = PopErr(&CharStack)))
           {
               Match = MatchBracket(*(ei->Data));
               /* Return italics to proper state */
               if (ei->Flags & efNoItal)
               {
                   if (ItState == itOn)
                   {
                       TmpPtr = BufPtr;
                       SKIP_AHEAD(TmpPtr, TmpC, TmpC == '}');

                       /* If the next character is a period or comma,
                        * or the last character is, then it's not an
                        * error. */
                       /* Checking 2 characters back seems dangerous,
                        * but it's already done in CheckDash. */
                       if ( !strchr(LTX_SmallPunc, *TmpPtr) &&
                            !strchr(LTX_SmallPunc, *(TmpPtr-2)) )
                           HERE(1, emNoItFound);
                   }

                   ItState = FALSE;
               }
               else if (ei->Flags & efItal)
                   ItState = TRUE;

               /* Same for math mode */
               if (ei->Flags & efMath || ei->Flags & efNoMath)
                   StkPop(&MathModeStack);

               FreeErrInfo(ei);
           }
           else
               Match = 0;

           if (Match != Char)
           {
               ABuf[0] = Match;
               BBuf[0] = Char;
               ABuf[1] = BBuf[1] = 0;
               if (Match)
                   PrintError(emExpectC, CurStkName(&InputStack), RealBuf,
                              BufPtr - Buf - 1, 1, Line,
                              ABuf, BBuf);
               else
                   HEREA(1, emSoloC, BBuf);
           }

       }
       else                    /* Opening bracket of some sort  */
       {
           if ((ei = PushChar(Char, Line, BufPtr - Buf - 1,
                              &CharStack, RealBuf)))
           {
               if (Char == '{')
               {
                   switch (ItFlag)
                   {
                   default:
                       ei->Flags |= ItFlag;
                       ItFlag = efNone;
                       break;
                   case efNone:
                       ei->Flags |= ItState ? efItal : efNoItal;
                   }

                   switch (MathFlag)
                   {
                   case efNone:
                       break;
                   case efMath:
                   case efNoMath:
                       PushMode((MathFlag == efMath), &MathModeStack);
                       /* Save for when we exit this delimiter */
                       ei->Flags |= MathFlag;
                       /* Reset flag just in case... */
                       MathFlag = efNone;
                       break;
                   }
               }
           }

           else
               PrintPrgErr(pmNoStackMem);
       }
   }
}


/*
* Checks to see if CmdBuffer matches any of the words in Silent, or
* any of the regular expressions in SilentCase.
*
*/

int CheckSilentRegex(void)
{

#if ! (HAVE_PCRE || HAVE_POSIX_ERE)

   return HasWord(CmdBuffer, &Silent) != NULL;

#else

   static char error[ERROR_STRING_SIZE];
   char *pattern;
   char *tmp;
   int i;
   int rc;
   int len = 4;                /* Enough for the (?:) */

   /* Initialize regular expression */
   if (INUSE(emSpaceTerm) && SilentCase.Stack.Used > 0)
   {
       /* Find the total length we need */
       /* There is 1 for | and the final for null terminator */
       FORWL(i, SilentCase)
       {
           len += strlen( SilentCase.Stack.Data[i] ) + 1;
       }

       /* (A|B|...) */
       tmp = (pattern = (char*)malloc( sizeof(char) * len ));

       #if HAVE_PCRE
       tmp = stpcpy(tmp,"(?:");
       #else
       tmp = stpcpy(tmp,"(");
       #endif

       FORWL(i, SilentCase)
       {
           tmp = stpcpy(tmp, SilentCase.Stack.Data[i]);
           *tmp++ = '|';
       }
       tmp = stpcpy(tmp - 1, ")");

       SilentRegex = malloc( sizeof(regex_t) );
       rc = regcomp(SilentRegex, pattern, REGEX_FLAGS);

       /* Compilation failed: print the error message */
       if (rc != 0)
       {
           regerror(rc, SilentRegex, error, ERROR_STRING_SIZE);
           PrintPrgErr(pmRegexCompileFailed, pattern, error);
           SilentRegex = NULL;
       }
       /* Ensure we won't initialize it again */
       SilentCase.Stack.Used = 0;
       free(pattern);
   }

   /* Check against the normal */
   if ( HasWord(CmdBuffer, &Silent) )
       return 1;
   if (!SilentRegex)
       return 0;

   /* Check against the regexes */
   rc = regexec(SilentRegex, CmdBuffer, 0, NULL, 0);
   if (rc == 0)
       return 1;

   /* Matching failed: handle error cases */
   switch(rc)
   {
       case REG_NOMATCH:
           return 0;
           break;
       default:
           regerror(rc, SilentRegex, error, ERROR_STRING_SIZE);
           PrintPrgErr(pmRegexMatchingError, error);
           break;
   }
   return 0;

#endif
}

/*
* Searches the `Buf' for possible errors, and prints the errors. `Line'
* is supplied for error printing.
*/

int FindErr(const char *_RealBuf, const unsigned long _Line)
{
   char *CmdPtr;               /* We'll have to copy each command out. */
   char *PrePtr;               /* Ptr to char in front of command, NULL if
                                * the cmd appears as the first character  */
   char *TmpPtr;               /* Temporary pointer */
   char *ErrPtr;               /* Ptr to where an error started */

   char TmpC,                  /* Just a temp var used throughout the proc. */
     MatchC, Char;             /* Char. currently processed */
   unsigned long CmdLen;       /* Length of misc. things */
   int MixingQuotes;

   int (*pstcb) (int c);

   enum DotLevel dotlev;

   FoundErr = EXIT_SUCCESS;

   if (_RealBuf)
   {
       RealBuf = _RealBuf;
       Line = _Line;

       if (!LastWasComment)
       {
           SeenSpace = TRUE;
       }
       BufPtr = PreProcess();

       BufPtr = SkipVerb();

       /* Skip past leading whitespace which is insignificant in TeX to avoid
        * spurious warnings (Delete this space to maintain correct
        * pagereferences).  If we have seen a space we don't _need_ to skip
        * past, and doing so misses Message 30 (Multiple spaces detected).  We
        * can miss some of Message 30 in the "not SeenSpace" case too, but I
        * think it's less important, since Message 30 is for newbies.
        */
       if (!SeenSpace && BufPtr)
       {
           SKIP_AHEAD(BufPtr, TmpC, LATEX_SPACE(TmpC));
       }

       while (BufPtr && *BufPtr)
       {
           PrePtr = BufPtr - 1;
           Char = *BufPtr++;
           if (isspace((unsigned char)Char))
               Char = ' ';

           switch (Char)
           {
           case '~':
               TmpPtr = NULL;
               if (isspace((unsigned char)*PrePtr))
                   TmpPtr = PrePtr;
               else if (isspace((unsigned char)*BufPtr))
                   TmpPtr = BufPtr;

               if (TmpPtr)
                   PSERR(TmpPtr - Buf, 1, emDblSpace);
               break;

           case 'X':
           case 'x':
               TmpPtr = PrePtr;

               SKIP_BACK(TmpPtr, TmpC,
                         (LATEX_SPACE(TmpC) || strchr("$", TmpC)));

               if (isdigit((unsigned char)*TmpPtr))
               {
                   TmpPtr = BufPtr;

                   SKIP_AHEAD(TmpPtr, TmpC,
                              (LATEX_SPACE(TmpC) || strchr("$", TmpC)));

                   if (isdigit((unsigned char)*TmpPtr))
                       HERE(1, emUseTimes);
               }
               /* FALLTHRU */
               /* CTYPE: isalpha() */
           case 'a':
           case 'b':
           case 'c':
           case 'd':
           case 'e':
           case 'f':
           case 'g':
           case 'h':
           case 'i':
           case 'j':
           case 'k':
           case 'l':
           case 'm':
           case 'n':
           case 'o':
           case 'p':
           case 'q':
           case 'r':
           case 's':
           case 't':
           case 'u':
           case 'v':
           case 'w':          /* case 'x': */
           case 'y':
           case 'z':

           case 'A':
           case 'B':
           case 'C':
           case 'D':
           case 'E':
           case 'F':
           case 'G':
           case 'H':
           case 'I':
           case 'J':
           case 'K':
           case 'L':
           case 'M':
           case 'N':
           case 'O':
           case 'P':
           case 'Q':
           case 'R':
           case 'S':
           case 'T':
           case 'U':
           case 'V':
           case 'W':          /* case 'X': */
           case 'Y':
           case 'Z':
               if (!isalpha((unsigned char)*PrePtr) && (*PrePtr != '\\') &&
                   CurStkMode(&MathModeStack))
               {
                   TmpPtr = BufPtr;
                   CmdPtr = CmdBuffer;
                   do
                   {
                       *CmdPtr++ = Char;
                       Char = *TmpPtr++;
                   }
                   while (isalpha((unsigned char)Char));

                   *CmdPtr = 0;

                   if (HasWord(CmdBuffer, &MathRoman))
                       HEREA(strlen(CmdBuffer), emWordCommand, CmdBuffer);
               }

               break;
           case ' ':
               TmpPtr = BufPtr;
               SKIP_AHEAD(TmpPtr, TmpC, LATEX_SPACE(TmpC));

               if (*TmpPtr && *PrePtr)
               {
                   if ((TmpPtr - BufPtr) > 0)
                   {
                       HERE(TmpPtr - BufPtr + 1, emMultiSpace);
                       strwrite(BufPtr, VerbClear, TmpPtr - BufPtr - 1);
                   }
               }
               break;

           case '.':
               if ((Char == *BufPtr) && (Char == BufPtr[1]))
               {
                   const char *cTmpPtr;
                   dotlev = CheckDots(&PrePtr[1], &BufPtr[2]);
                   cTmpPtr = Dot2Str(dotlev);
                   HEREA(3, emEllipsis, cTmpPtr);
               }

               /* Regexp: "([^A-Z@.])\.[.!?:]*\s[ \`([]*[a-z]" */

               TmpPtr = BufPtr;
               SKIP_AHEAD(TmpPtr, TmpC, strchr(LTX_EosPunc, TmpC));
               if (LATEX_SPACE(*TmpPtr))
               {
                   if (!isupper((unsigned char)*PrePtr) && (*PrePtr != '@') &&
                       (*PrePtr != '.'))
                   {
                       SKIP_AHEAD(TmpPtr, TmpC,
                                  (LATEX_SPACE(TmpC) || TmpC == '\\' ||
                                   strchr(LTX_BosPunc, TmpC)));
                       if (islower((unsigned char)*TmpPtr))
                       {
                           /* If it's a silent macro, count it as space. */
                           int IsSilent = FALSE;
                           if (*(TmpPtr - 1) == '\\')
                           {
                               GetLTXToken(TmpPtr - 1, CmdBuffer);
                               IsSilent = CheckSilentRegex();
                           }

                           /* Ignore spacing problems after commands if desired */
                           TmpPtr = PrePtr;
                           SKIP_BACK(TmpPtr, TmpC, istex(TmpC));
                           if (!IsSilent &&
                               (*TmpPtr != '\\' || (CmdSpace & csInterWord)))
                           {
                               PSERR(BufPtr - Buf, 1, emInterWord);
                           }
                       }
                       else
                           CheckAbbrevs(&BufPtr[-1]);
                   }
               }

               /* FALLTHRU */
           case ':':
           case '?':
           case '!':
           case ';':
               /* Regexp: "[A-Z][A-Z][.!?:;]\s+" */

               if (isspace((unsigned char)*BufPtr) &&
                   isupper((unsigned char)*PrePtr) &&
                   (isupper((unsigned char)PrePtr[-1]) || (Char != '.')) &&
                   !FrenchSpacing)
               {
                   /* Ignore spacing problems after commands if desired */
                   TmpPtr = PrePtr;
                   SKIP_BACK(TmpPtr, TmpC, istex(TmpC));
                   if (*TmpPtr != '\\' || (CmdSpace & csInterSentence))
                       HERE(1, emInterSent);
               }

               /* FALLTHRU */
           case ',':
               if (isspace((unsigned char)*PrePtr) &&
                   !(isdigit((unsigned char)*BufPtr) &&
                     ((BufPtr[-1] == '.') || (BufPtr[-1] == ','))))
                   PSERR(PrePtr - Buf, 1, emSpacePunct);

               if (CurStkMode(&MathModeStack) &&
                   (((*BufPtr == '$') && (BufPtr[1] != '$')) ||
                    (!strafter(BufPtr, "\\)"))))
                   HEREA(1, emPunctMath, "outside inner");

               if (!CurStkMode(&MathModeStack) &&
                   (((*PrePtr == '$') && (PrePtr[-1] == '$')) ||
                    (!strinfront(PrePtr, "\\]"))))
                   HEREA(1, emPunctMath, "inside display");

               break;
           case '\'':
           case '`':
               if ((Char == *BufPtr) && (Char == BufPtr[1]))
               {
                   PrintError(emThreeQuotes, CurStkName(&InputStack), RealBuf,
                              BufPtr - Buf - 1, 3, Line,
                              Char, Char, Char, Char, Char, Char);
               }

               if (Char == '\'')
                   MatchC = '`';
               else
                   MatchC = '\'';

               TmpPtr = BufPtr;
               SKIP_AHEAD(TmpPtr, TmpC, TmpC == Char);

               MixingQuotes = FALSE;

               if ((*TmpPtr == MatchC) || (*TmpPtr == '\"') ||
                   (*TmpPtr == '\xB4')) /* xB4 = latin1 acute accent */
                   MixingQuotes = TRUE;

               SKIP_AHEAD(TmpPtr, TmpC, strchr("`\'\"\xB4", TmpC)); /* xB4 = latin1 acute accent */

               if (MixingQuotes)
                   HERE(TmpPtr - BufPtr + 1, emQuoteMix);

               switch (Char)
               {
               case '\'':
                   if (isalpha((unsigned char)*TmpPtr) &&
                       (strchr(LTX_GenPunc, *PrePtr) || isspace((unsigned char)*PrePtr)))
                       HERE(TmpPtr - BufPtr + 1, emBeginQ);

                   /* Now check quote style */
#define ISPUNCT(ptr) ((strchr(LTX_EosPunc, *ptr) || strchr(LTX_GenPunc, *ptr)) && (ptr[-1] != '\\'))

                   /* We ignore all single words/abbreviations in quotes */

                   {
                       char *WordPtr = PrePtr;
                       SKIP_BACK(WordPtr, TmpC, (isalnum((unsigned char)TmpC) ||
                                                 strchr(LTX_GenPunc, TmpC)));

                       if (*WordPtr != '`')
                       {
                           if (*PrePtr && (Quote != quTrad)
                               && ISPUNCT(PrePtr))
                               PSERRA(PrePtr - Buf, 1,
                                      emQuoteStyle, "in front of");

                           if (*TmpPtr && (Quote != quLogic)
                               && ISPUNCT(TmpPtr))
                               PSERRA(TmpPtr - Buf, 1,
                                      emQuoteStyle, "after");
                       }
                   }

                   break;
               case '`':
                   if (isalpha((unsigned char)*PrePtr) &&
                       (strchr(LTX_GenPunc, *TmpPtr) || isspace((unsigned char)*TmpPtr)))
                       HERE(TmpPtr - BufPtr + 1, emEndQ);
                   break;
               }
               BufPtr = TmpPtr;
               break;
           case '"':
               HERE(1, emUseQuoteLiga);
               break;

           case '\264':             /* � (in Latin-1) */
               HERE(1, emUseOtherQuote);
               break;

           case '_':
           case '^':
               if (*PrePtr != '\\')
               {
                   TmpPtr = PrePtr;
                   SKIP_BACK(TmpPtr, TmpC, LATEX_SPACE(TmpC));

                   CmdLen = 1;

                   switch (*TmpPtr)
                   {
                       /*{ */
                   case '}':
                       if (PrePtr[-1] != '\\')
                           break;

                       CmdLen++;
                       PrePtr--;
                       /* FALLTHRU */
                       /*[( */
                   case ')':
                   case ']':
                       PSERR(PrePtr - Buf, CmdLen, emEnclosePar);
                   }

                   TmpPtr = BufPtr;
                   SKIP_AHEAD(TmpPtr, TmpC, LATEX_SPACE(TmpC));

                   ErrPtr = TmpPtr;

                   if (isalpha((unsigned char)*TmpPtr))
                       pstcb = &my_isalpha;
                   else if (isdigit((unsigned char)*TmpPtr))
                       pstcb = &my_isdigit;
                   else
                       break;

                   while ((*pstcb) (*TmpPtr++))
                       ;
                   TmpPtr--;

                   if ((TmpPtr - ErrPtr) > 1)
                       PSERR(ErrPtr - Buf, TmpPtr - ErrPtr, emEmbrace);
               }
               break;
           case '-':
               CheckDash();
               break;
           case '\\':         /* Command encountered  */
               BufPtr = GetLTXToken(--BufPtr, CmdBuffer);

               if (SeenSpace)
               {
                   /* We must be careful to not point to the "previous space"
                    * when it was actually on the previous line.  This could
                    * cause us to write into someone else's memory (inside of
                    * PrintError). */
                   if (HasWord(CmdBuffer, &Linker))
                       PSERR( (PrePtr > Buf) ? (PrePtr - Buf) : 0,
                              1, emNBSpace);
                   if (HasWord(CmdBuffer, &PostLink))
                       PSERR( (PrePtr > Buf) ? (PrePtr - Buf) : 0,
                              1, emFalsePage);
               }

               if (LATEX_SPACE(*BufPtr) && !CurStkMode(&MathModeStack) &&
                   !CheckSilentRegex() && (strlen(CmdBuffer) != 2))
               {
                   PSERR(BufPtr - Buf, 1, emSpaceTerm);
               }
               else if ((*BufPtr == '\\') && (!isalpha((unsigned char)BufPtr[1])) &&
                        (!LATEX_SPACE(BufPtr[1])))
                   PSERR(BufPtr - Buf, 2, emNotIntended);

               PerformBigCmd(PrePtr + 1);
               BufPtr = SkipVerb();

               break;

           case '(':
               if (*PrePtr && !LATEX_SPACE(*PrePtr) && !isdigit((unsigned char)*PrePtr)
                   && !strchr("([{`~", *PrePtr))
               {
                   if (PrePtr[-1] != '\\')     /* Short cmds */
                   {
                       TmpPtr = PrePtr;
                       SKIP_BACK(TmpPtr, TmpC, istex(TmpC));
                       if (*TmpPtr != '\\')    /* Long cmds */
                           PSERRA(BufPtr - Buf - 1, 1, emSpaceParen,
                                  "in front of");
                   }
               }
               if (isspace((unsigned char)*BufPtr))
                   PSERRA(BufPtr - Buf, 1, emNoSpaceParen, "after");
               HandleBracket(Char);
               break;

           case ')':
               if (SeenSpace)
                   PSERRA(BufPtr - Buf - 1, 1, emNoSpaceParen,
                          "in front of");
               if (isalpha((unsigned char)*BufPtr))
                   PSERRA(BufPtr - Buf, 1, emSpaceParen, "after");
               HandleBracket(Char);
               break;

           case '}':
           case '{':
           case '[':
           case ']':
               HandleBracket(Char);
               break;
           case '$':
               if (*PrePtr != '\\')
               {
                   if (*BufPtr == '$')
                   {
                       BufPtr++;
                       TmpPtr = BufPtr;
                       SKIP_AHEAD(TmpPtr, TmpC, (TmpC != '$' && TmpC != '\0'));
                       PSERR(BufPtr - Buf - 2, TmpPtr-BufPtr+4, emDisplayMath);
                   }
                   else
                   {
                       TmpPtr = BufPtr;
                       SKIP_AHEAD(TmpPtr, TmpC, (TmpC != '$' && TmpC != '\0'));
                       PSERR(BufPtr - Buf - 1, TmpPtr-BufPtr+2, emInlineMath);
                   }
                   if (CurStkMode(&MathModeStack))
                   {
                       StkPop(&MathModeStack);
                   }
                   else
                   {
                       PushMode(TRUE, &MathModeStack);
                   }
               }

               break;
           }

           SeenSpace = LATEX_SPACE(Char);
       }

       if (!VerbMode)
       {
           CheckRest();
       }

   }

   return FoundErr;
}

/*
* Tries to create plural forms for words. Put a '%s' where a
* suffix should be put, e.g. "warning%s". Watch your %'s!
*/

static void Transit(FILE * fh, unsigned long Cnt, const char *Str)
{
   switch (Cnt)
   {
   case 0:
       fputs("No ", fh);
       fprintf(fh, Str, "s");
       break;
   case 1:
       fputs("One ", fh);
       fprintf(fh, Str, "");
       break;
   default:
       fprintf(fh, "%ld ", Cnt);
       fprintf(fh, Str, "s");
       break;
   }
}

/*
* Prints the status/conclusion after doing all the testing, including
* bracket stack status, math mode, etc.
*/

void PrintStatus(unsigned long Lines)
{
   unsigned long Cnt;
   struct ErrInfo *ei;


   while ((ei = PopErr(&CharStack)))
   {
       PrintError(emNoMatchC, ei->File, ei->LineBuf, ei->Column,
                  ei->ErrLen, ei->Line, (char *) ei->Data);
       FreeErrInfo(ei);
   }

   while ((ei = PopErr(&EnvStack)))
   {
       PrintError(emNoMatchC, ei->File, ei->LineBuf, ei->Column,
                  ei->ErrLen, ei->Line, (char *) ei->Data);
       FreeErrInfo(ei);
   }

   while ((ei = PopErr(&ConTeXtStack)))
   {
       PrintError(emNoMatchConTeXt, ei->File, ei->LineBuf, ei->Column,
                  ei->ErrLen, ei->Line, (char *) ei->Data);
       FreeErrInfo(ei);
   }

   if (CurStkMode(&MathModeStack))
   {
       PrintError(emMathStillOn, CurStkName(&InputStack), "", 0L, 0L, Lines);
   }

   for (Cnt = 0L; Cnt < (NUMBRACKETS >> 1); Cnt++)
   {
       if (Brackets[Cnt << 1] != Brackets[(Cnt << 1) + 1])
       {
           PrintError(emNoMatchCC, CurStkName(&InputStack), "", 0L, 0L, Lines,
                      BrOrder[Cnt << 1], BrOrder[(Cnt << 1) + 1]);
       }
   }

   if (!Quiet)
   {
       Transit(stderr, ErrPrint, "error%s printed; ");
       Transit(stderr, WarnPrint, "warning%s printed; ");
       Transit(stderr, UserSupp, "user suppressed warning%s; ");
       Transit(stderr, LineSupp, "line suppressed warning%s.\n");

       /* Print how to suppress warnings. */
       if ( ErrPrint + WarnPrint > 0 ) {
           fprintf(
               stderr,
               "See the manual for how to suppress some or all of these warnings/errors.\n"
               "The manual is available "
#ifdef TEX_LIVE
               "by running `texdoc chktex` or "
#endif
               "at https://www.nongnu.org/chktex/ChkTeX.pdf\n");
       }
   }
}



/*
* Uses OutputFormat. Be sure that `String'
* does not contain tabs, newlines, etc.
* Prints a formatted string. Formatting codes understood:
*  %b  - string to print Between fields (from -s option)
*  %c  - Column position of error
*  %d  - lenght of error (Digit)
*  %f  - current Filename
*  %i  - Turn on inverse printing mode.
*  %I  - Turn off inverse printing mode.
*  %k  - Kind of error (warning, error, message)
*  %l  - Line number of error
*  %m  - warning Message
*  %n  - warning Number
*  %u  - an Underlining line (like the one which appears when using -v1)
*  %r  - part of line in front of error ('S' - 1)
*  %s  - part of line which contains error (String)
*  %t  - part of line after error ('S' + 1)
*/


void
PrintError(const enum ErrNum Error, const char *File, const char *String,
          const long Position, const long Len, const long LineNo,  ...)
{
   static                      /* Just to reduce stack usage... */
   char PrintBuffer[BUFFER_SIZE];
   va_list MsgArgs;

   char *LastNorm = OutputFormat;
   char *of;
   int c;

   enum Context Context;

   if (betw(emMinFault, Error, emMaxFault))
   {
       switch (LaTeXMsgs[Error].InUse)
       {
       case iuOK:
           if (SUPPRESSED_ON_LINE(Error))
           {
               LineSupp++;
           }
           else
           {
               Context = LaTeXMsgs[Error].Context;

               if (!HeadErrOut)
                   Context |= ctOutHead;

#define RGTCTXT(Ctxt, Var) if((Context & Ctxt) && !(Var)) break;

               RGTCTXT(ctInMath, CurStkMode(&MathModeStack));
               RGTCTXT(ctOutMath, !CurStkMode(&MathModeStack));
               RGTCTXT(ctInHead, InHeader);
               RGTCTXT(ctOutHead, !InHeader);

               /* Count how warnings or errors we've found, and
                * update the return code with the worst. */
               switch (LaTeXMsgs[Error].Type)
               {
               case etWarn:
                   WarnPrint++;
                   FoundErr = max(FoundErr, EXIT_WARNINGS);
                   break;
               case etErr:
                   ErrPrint++;
                   FoundErr = max(FoundErr, EXIT_ERRORS);
                   break;
               case etMsg:
                   break;
               }

               while ((of = strchr(LastNorm, '%')))
               {
                   c = *of;
                   *of = 0;

                   fputs(LastNorm, OutputFile);

                   *of++ = c;

                   switch (c = *of++)
                   {
                   case 'b':
                       fputs(Delimit, OutputFile);
                       break;
                   case 'c':
                       /* TODO: need to add the offset of the column
                        * here when long lines are broken. */
                       fprintf(OutputFile, "%ld", Position + 1);
                       break;
                   case 'd':
                       fprintf(OutputFile, "%ld", Len);
                       break;
                   case 'f':
                       fputs(File, OutputFile);
                       break;
                   case 'i':
                       fputs(ReverseOn, OutputFile);
                       break;
                   case 'I':
                       fputs(ReverseOff, OutputFile);
                       break;
                   case 'k':
                       switch (LaTeXMsgs[Error].Type)
                       {
                       case etWarn:
                           fprintf(OutputFile, "Warning");
                           break;
                       case etErr:
                           fprintf(OutputFile, "Error");
                           break;
                       case etMsg:
                           fprintf(OutputFile, "Message");
                           break;
                       }
                       break;
                   case 'l':
                       fprintf(OutputFile, "%ld", LineNo);
                       break;
                   case 'm':
                       va_start(MsgArgs, LineNo);
                       vfprintf(OutputFile,
                                LaTeXMsgs[Error].Message, MsgArgs);
                       va_end(MsgArgs);
                       break;
                   case 'n':
                       fprintf(OutputFile, "%d", Error);
                       break;
                   case 'u':
                       sfmemset(PrintBuffer, ' ', (long) Position);

                       sfmemset(&PrintBuffer[Position], '^', Len);
                       PrintBuffer[Position + Len] = 0;
                       fputs(PrintBuffer, OutputFile);
                       break;
                   case 'r':
                       substring(String, PrintBuffer, 0L, Position);
                       fputs(PrintBuffer, OutputFile);
                       break;
                   case 's':
                       substring(String, PrintBuffer, Position, Len);
                       fputs(PrintBuffer, OutputFile);
                       break;
                   case 't':
                       substring(String, PrintBuffer,
                                 Position + Len, LONG_MAX);
                       fputs(PrintBuffer, OutputFile);
                       break;
                   default:
                       fputc(c, OutputFile);
                       break;
                   }
                   LastNorm = of;
               }
               fputs(LastNorm, OutputFile);
           }
           break;
       case iuNotUser:
           UserSupp++;
           break;
       case iuNotSys:
           break;
       }
   }
}

/*
* All commands isolated is routed through this command, so we can
* update global statuses like math mode and whether @ is a letter
* or not.
*/

static enum ErrNum PerformCommand(const char *Cmd, char *Arg)
{
   const char *Argument = "";
   enum ErrNum en = emMinFault;
   int TmpC;

   if (!strcmp(Cmd, "\\makeatletter"))
       AtLetter = TRUE;
   else if (!strcmp(Cmd, "\\makeatother"))
       AtLetter = FALSE;
   else if (!strcmp(Cmd, "\\frenchspacing"))
       FrenchSpacing = TRUE;
   else if (!strcmp(Cmd, "\\nonfrenchspacing"))
       FrenchSpacing = FALSE;
   else if (InputFiles && !(strcmp(Cmd, "\\input") && strcmp(Cmd, "\\include")))
   {
       SKIP_AHEAD(Arg, TmpC, LATEX_SPACE(TmpC));
       if (*Arg == '{')        /* } */
       {
           if (GetLTXArg(Arg, TmpBuffer, GET_STRIP_TOKEN, NULL))
               Argument = TmpBuffer;
       }
       else
           Argument = strip(Arg, STRP_BTH);

       if (!(Argument && PushFileName(Argument, &InputStack)))
           en = emNoCmdExec;
   }
   else if (HasWord(Cmd, &Primitives))
       en = emTeXPrim;
   else if (HasWord(Cmd, &MathCmd))
   {
       SKIP_AHEAD(Arg, TmpC, LATEX_SPACE(TmpC));
       if (*Arg == '{')
       {
           /* We will actually turn on math mode when we enter the {} */
           MathFlag = efMath;
       }
   }
   else if (HasWord(Cmd, &TextCmd))
   {
       SKIP_AHEAD(Arg, TmpC, LATEX_SPACE(TmpC));
       if (*Arg == '{')
       {
           /* We will actually turn on text mode when we enter the {} */
           MathFlag = efNoMath;
       }
   }
   else if (*Cmd == '\\')
   {
       /* Quicker check of single lettered commands. */
       switch (Cmd[1])
       {
       case '(':
       case '[':
           PushMode(TRUE, &MathModeStack);
           break;
       case ']':
       case ')':
           if (!CurStkMode(&MathModeStack))
               PSERRA(BufPtr - Buf - 2, 1, emMathModeConfusion, "on");

           StkPop(&MathModeStack);
           break;
       case '/':
           switch (ItState)
           {
           case itOn:
               ItState = itCorrected;
               Argument = Arg;

               SKIP_AHEAD(Argument, TmpC, TmpC == '{' || TmpC == '}');

               if (strchr(".,", *Argument))
                   en = emItPunct;

               break;
           case itCorrected:
               en = emItDup;
               break;
           case itOff:
               en = emItInNoIt;
           }
           break;
       }
   }

   return (en);
}