%%
%% This is file `lua-typo.sty',
%% generated with the docstrip utility.
%%
%% The original source files were:
%%
%% lua-typo.dtx  (with options: `sty')
%% 
%% IMPORTANT NOTICE:
%% For the copyright see the source file `lua-typo.dtx’.
%%
\NeedsTeXFormat{LaTeX2e}[2021/06/01]
\ProvidesPackage{lua-typo}
                [2024-04-18 v.0.87 Daniel Flipo]
\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}
\ifdefined\directlua
  \RequirePackage{luatexbase,luacode,luacolor,atveryend}
\else
  \PackageError{This package is meant for LuaTeX only! Aborting}
               {No more information available, sorry!}
\fi
\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
\begin{luacode}
luatypo = { }
\end{luacode}
\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    ,
   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]
\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@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
\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
    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
    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
   }%
}
\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}
\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
     }%
  \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
    }%
  \else
    \PackageWarning{luatypo}{Unknown language "\luatypo@LANG",
       \MessageBreak \protect\luatypoTwoChars\space command ignored}%
  \fi}
\newcommand*{\luatypoSetColor}[2]{%
  \begingroup
    \color{#2}%
    \directlua{luatypo.colortbl[#1]=\the\LuaCol@Attribute}%
  \endgroup
}
\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")
local USRSKIP  = 0
local PARSKIP  = 3
local LFTSKIP  = 8
local RGTSKIP  = 9
local TOPSKIP = 10
local PARFILL = 15
local LINE    = 1
local BOX     = 2
local INDENT  = 3
local ALIGN   = 4
local EQN     = 6
local USER = 0
local HYPH = 0x2D
local LIGA = 0x102
local parline = 0
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
local utf8_find = unicode.utf8.find
local utf8_gsub = unicode.utf8.gsub
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
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
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
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
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
local signature = function (node, string, swap)
  local n = node
  local str = string
  if n and n.id == GLYPH then
     local b = n.char
     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
    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
  local s = utf8_gsub(str, "_", "")
  local len = utf8_len(s)
  return len, str
end
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
     local lastn = node
     while lastn and lastn.id ~= GLYPH and lastn.id ~= DISC and
           lastn.id ~= HLIST do
       lastn = lastn.prev
     end
     local n = lastn
     local words = 0
     while n and (words <= 2 or maxlen < MinPart) do
       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))
     if flag and old ~= "" then
        local oldlast = utf8_gsub (old, ".*_", "")
        local newlast = utf8_gsub (new, ".*_", "")
        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
           match = true
        end
        if oldlast == newlast and utf8_len(newlast) >= MinFull then
           if utf8_len(newlast) > MinPart or not match then
              oldsub = oldlast
              newsub = newlast
           end
           match = true
        end
        if match then
           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, "^_+", "")
           local msg = "E.O.L. MATCH=" .. newsub
           log_flaw(msg, line, colno, footnote)
           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
             elseif n then
                color_node(n, COLOR)
                li, newsub = signature(n, newsub, swap)
                l = l + li - lo
                lo = li
             end
             n = n.prev
           end
        end
     end
  end
  return new, match
end
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))
  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
        match = true
     end
     if oldfirst == newfirst  and utf8_len(newfirst) >= MinFull then
        if utf8_len(newfirst) > MinPart or not match then
           oldsub = oldfirst
           newsub = newfirst
        end
        match = true
     end
     if match then
        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, "_+$", "")   --$
        local msg = "B.O.L. MATCH=" .. newsub
        log_flaw(msg, line, colno, footnote)
        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
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)
  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
     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
  if match then
     local msg = "ShortFinalWord=" .. new
     log_flaw(msg, 1, colno, footnote)
     local n = start
     repeat
       color_node(n, COLOR)
       n = n.next
     until eow_char[n.char]
     color_node(n, COLOR)
  end
  return match
end
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
  if lang and luatypo.single[lang] then
     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
  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
        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
     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
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
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
     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
        totalht = totalht + ruleht
        n = n.next
        if n and n.id == KERN and n.subtype == 1 then
           totalht = totalht + n.kern
           if totalht == 0 or totalht == ruleht then
              flag = true
           else
           end
         end
     end
  end
  return flag
end
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
  end
  return page_bot, body_bot
end
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 pflag  = false
  local ofirst = true
  local ufirst = true
  local n = head.head
  local bottom = vpos
  if vpos <= bpmn then
     bottom = bpmn + marginpp
  end
  repeat
    if n and (n.id == GLUE or n.id == PENALTY) then
       n = n.next
    elseif n and n.id == VLIST then
       n = n.head
    end
  until not n or (n.id == HLIST and n.subtype == LINE)
  local head = n
  if head then
  else
  end
  local last = head
  while head do
    local next = head.next
    if head.id == HLIST and head.subtype == LINE then
       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
          pflag = 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
          pflag = 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
  if bottom > textht + margintol and MarginparPos then
     pflag = 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, pflag
end
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
  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
              body = n
              break
           else
              first = n.list
           end
        else
        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
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 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 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")
  while head do
    local nextnode = head.next
    if head.id == HLIST and head.subtype == LINE and
          (head.height > 0 or head.depth > 0) then
       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
       end
       page_bottom, body_bottom = check_EOP(nextnode)
       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
       if footnote and page_bottom then
          ftnsplit = true
       end
       while first.id == MKERN or
             (first.id == GLUE and first.subtype == LFTSKIP) do
          first = first.next
       end
       if first.id == LPAR then
          hyphcount = 0
          firstwd = ""
          lastwd = ""
          if not footnote then
             parline = 1
             if body_bottom then
                orphanflag = true
             end
          end
          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
       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
       if ShortFinalWord and pageline == 1 and parline > 1 and
          check_page_first_word(first, colno, footnote) then
          pageflag = true
       end
       local ln = slide(first)
       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
          hyphcount = 0
          ftnsplit = false
          orphanflag = false
          if pageline == 1 and parline > 1 then
             widowflag = true
          end
          local PFskip = effective_glue(pn,head)
          if ShortLines then
             local llwd = linewd - PFskip
             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
          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
          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
          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
             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
          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
             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
       else
          hyphcount = 0
          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
       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
            bpmn, pflag = check_marginnote(head, line, colno, vpos, bpmn)
            if pflag then pageflag = true end
            page_bottom, body_bottom = check_EOP(nextnode)
         else
            local hf = head.list
            if hf and hf.id == VLIST and hf.subtype == 0 then
               break
            end
         end
      end
      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
       vpos = vpos + head.height + head.depth
       if footnote then
          ftnline = ftnline + 1
          line = ftnline
       else
          pageline = pageline + 1
          line = pageline
       end
       page_bottom, body_bottom = check_EOP (nextnode)
       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
       local prev = head.prev
       if body_bottom or footnoterule_ahead (prev) then
          footnote = true
          ftnline = 0
          body_bottom = false
          orphanflag = false
          hyphcount = 0
          firstwd = ""
          lastwd = ""
       end
    elseif body_bottom and head.id == GLUE and head.subtype == 0 then
       if first_bot then
          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
       vpos = vpos + effective_glue(head,top)
    elseif head.id == KERN and head.subtype == 1 then
       vpos = vpos + head.kern
    elseif head.id == VLIST then
       vpos = vpos + head.height + head.depth
    end
  head = nextnode
  end
  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
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")
  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
     top = body.list
     if first.id == VLIST then
        boxed = body
     end
  end
  while top do
    first = top.list
    next = top.next
    if top and top.id == VLIST and top.subtype == 0 and
       top.width > textwd/2                             then
       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
       colno = 0
       for col in traverse_id(VLIST, first) do
           colno = colno + 1
       local n, ok = check_vtop(col,colno,vpos)
       if ok then checked = true end
       end
       colno = nil
       top = top.next
    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
       colno = 0
       for n in traverse_id(HLIST, first) do
           colno = colno + 1
           local col = n.list
           if col and col.list then
              local n, ok = check_vtop(col,colno,vpos)
              if ok then checked = true end
           end
       end
       colno = nil
    end
    if boxed and not next then
       next = boxed
       boxed = nil
    end
    top = next
  end
  if not checked then
     luatypo.failedlist = luatypo.failedlist .. tostring(pageno) .. ", "
  end
  return true
end
return luatypo.check_page
\end{luacode}
\AtBeginDocument{%
  \directlua{
    if not luatypo.None then
       luatexbase.add_to_callback
           ("pre_shipout_filter",luatypo.check_page,"check_page",1)
    end
  }%
}
\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 of file `lua-typo.sty'.
