%% %% This is file `lua-typo.sty' v0.40, %% 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}[2020/01/01] \ProvidesPackage{lua-typo} [2021/04/18 v.0.40 Daniel Flipo] \ifdefined\directlua \RequirePackage{luatexbase,luacode,luacolor} \RequirePackage{kvoptions,atveryend} \else \PackageError{This package is meant for LuaTeX only! Aborting} {No more information available, sorry!} \fi \newdimen\luatypoLLminWD \newdimen\luatypoBackPI \newdimen\luatypoBackFuzz \newcount\luatypoStretchMax \newcount\luatypoHyphMax \newcount\luatypoPageMin \newcount\luatypoMinFull \newcount\luatypoMinPart \newcount\luatypo@LANGno \newcount\luatypo@options \newtoks\luatypo@single \newtoks\luatypo@double \begin{luacode} luatypo = { } \end{luacode} \SetupKeyvalOptions{ family=luatypo, prefix=LT@, } \DeclareBoolOption[false]{ShowOptions} \DeclareBoolOption[false]{None} \DeclareBoolOption[false]{All} \DeclareBoolOption[false]{BackParindent} \DeclareBoolOption[false]{ShortLines} \DeclareBoolOption[false]{ShortPages} \DeclareBoolOption[false]{OverfullLines} \DeclareBoolOption[false]{UnderfullLines} \DeclareBoolOption[false]{Widows} \DeclareBoolOption[false]{Orphans} \DeclareBoolOption[false]{EOPHyphens} \DeclareBoolOption[false]{RepeatedHyphens} \DeclareBoolOption[false]{ParLastHyphen} \DeclareBoolOption[false]{EOLShortWords} \DeclareBoolOption[false]{FirstWordMatch} \DeclareBoolOption[false]{LastWordMatch} \AddToKeyvalOption{luatypo}{All}{% \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 } \ProcessKeyvalOptions{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@ShowOptions \GenericWarning{* }{% *** List of possible options for lua-typo ***\MessageBreak [Default values between brackets]% \MessageBreak ShowOptions [false]\MessageBreak None [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 \MessageBreak *********************************************% \MessageBreak Lua-typo [ShowOptions] }% \fi \AtBeginDocument{% \directlua{ luatypo.HYPHmax = tex.count.luatypoHyphMax luatypo.PAGEmin = tex.count.luatypoPageMin luatypo.Stretch = tex.count.luatypoStretchMax luatypo.MinFull = tex.count.luatypoMinFull luatypo.MinPart = tex.count.luatypoMinPart luatypo.LLminWD = tex.dimen.luatypoLLminWD luatypo.BackPI = tex.dimen.luatypoBackPI luatypo.BackFuzz = tex.dimen.luatypoBackFuzz }% } \AtVeryEndDocument{% \ifnum\luatypo@options = 0 \LT@Nonetrue \fi \ifLT@None \directlua{ texio.write_nl(' ') texio.write_nl('***********************************') texio.write_nl('*** lua-typo loaded 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 texio.write_nl('*** lua-typo: No Typo Flaws found.') 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(' ') }% \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 = string.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 = string.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.single = { } luatypo.double = { } luatypo.colortbl = { } luatypo.pagelist = "" 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 split_lig = { } split_lig[0xFB00] = "ff" split_lig[0xFB01] = "fi" split_lig[0xFB02] = "fl" split_lig[0xFB03] = "ffi" split_lig[0xFB04] = "ffl" split_lig[0xFB05] = "st" split_lig[0xFB06] = "st" local DISC = node.id("disc") local GLYPH = node.id("glyph") local GLUE = node.id("glue") local KERN = node.id("kern") local HLIST = node.id("hlist") local LPAR = node.id("local_par") local MKERN = node.id("margin_kern") local PENALTY = node.id("penalty") 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 EQN = 6 local USER = 0 local HYPH = 0x2D local LIGA = 0x102 local parline = 0 local effective_glue = node.effective_glue local set_attribute = node.set_attribute local slide = node.slide local traverse = node.traverse local traverse_id = node.traverse_id local has_field = node.has_field local uses_font = node.uses_font local is_glyph = node.is_glyph 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_hlist = function (head, color) local first = head.head for n in traverse(first) do color_node(n, color) end end local color_line = function (head, color) local first = head.head for n in traverse(first) do if n and n.id == HLIST then color_hlist(n, color) else color_node(n, color) end end end local signature = function (node, string, swap) local n = node local str = string if n and n.id == GLYPH then local b, id = is_glyph(n) if b and not char_to_discard[b] then if b == 0x2019 then b = 0x27 end if b < 0x100 then str = str .. string.char(b) elseif split_lig[b] then local c = split_lig[b] if swap then c = string.reverse(c) end str = str .. c elseif n.subtype == LIGA and b > 0xE000 then local c = string.sub(b,-2) if swap then c = string.reverse(c) end str = str .. c 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 and pre.char ~= HYPH and pre.char < 0x100 then c1 = string.char(pre.char) end if post and post.char then if post.char < 0x100 then c2 = string.char(post.char) elseif split_lig[post.char] then c2 = split_lig[post.char] if swap then c2 = string.reverse(c2) end end end if swap then str = str .. c2 .. c1 else str = str .. c1 .. c2 end end local len = string.len(str) if string.find(str, "_") then len = len - 1 end return len, str end local check_last_word = function (old, node, flag) local COLOR = luatypo.colortbl[11] local match = false local new = "" local maxlen = 0 if flag and node then local swap = true local lastn = node while lastn and lastn.id ~= GLYPH and lastn.id ~= DISC do lastn = lastn.prev end local n = lastn while n and n.id ~= GLUE do maxlen, new = signature (n, new, swap) n = n.prev end if n and n.id == GLUE then new = new .. "_" repeat n = n.prev maxlen, new = signature (n, new, swap) until not n or n.id == GLUE end new = string.reverse(new) local MinFull = luatypo.MinFull local MinPart = luatypo.MinPart MinFull = math.min(MinPart,MinFull) local k = MinPart local oldlast = string.gsub (old, '.*_', '') local newlast = string.gsub (new, '.*_', '') local i = string.find(new, "_") if i and i > maxlen - MinPart + 1 then k = MinPart + 1 end local oldsub = string.sub(old,-k) local newsub = string.sub(new,-k) local l = string.len(new) if oldsub == newsub and l >= k then match = true elseif oldlast == newlast and string.len(newlast) >= MinFull then match = true oldsub = oldlast newsub = newlast k = string.len(newlast) end if match then local osub = oldsub local nsub = newsub while osub == nsub and k <= maxlen do k = k +1 osub = string.sub(old,-k) nsub = string.sub(new,-k) if osub == nsub then newsub = nsub end end pageflag = true newsub = string.gsub(newsub, '^_', '') oldsub = string.reverse(newsub) local newsub = "" local n = lastn repeat if n and n.id ~= GLUE then color_node(n, COLOR) l, newsub = signature(n, newsub, swap) elseif n then newsub = newsub .. "_" end n = n.prev until not n or newsub == oldsub or l >= k end end return new end local check_first_word = function (old, node, flag) local COLOR = luatypo.colortbl[10] local match = false local swap = false local new = "" local maxlen = 0 local start = node local n = start while n and n.id ~= GLYPH and n.id ~= DISC do n = n.next end while n and n.id ~= GLUE do maxlen, new = signature (n, new, swap) n = n.next end if n and n.id == GLUE then new = new .. "_" repeat n = n.next maxlen, new = signature (n, new, swap) until not n or n.id == GLUE end if flag then local MinFull = luatypo.MinFull local MinPart = luatypo.MinPart MinFull = math.min(MinPart,MinFull) local k = MinPart local oldsub = "" local newsub = "" local oldfirst = string.gsub (old, '_.*', '') local newfirst = string.gsub (new, '_.*', '') local i = string.find(new, "_") if i and i <= MinPart then k = MinPart + 1 end local oldsub = string.sub(old,1,k) local newsub = string.sub(new,1,k) local l = string.len(newsub) if oldsub == newsub and l >= k then match = true elseif oldfirst == newfirst and string.len(newfirst) >= MinFull then match = true oldsub = oldfirst newsub = newfirst k = string.len(newfirst) end if match then local osub = oldsub local nsub = newsub while osub == nsub and k <= maxlen do k = k + 1 osub = string.sub(old,1,k) nsub = string.sub(new,1,k) if osub == nsub then newsub = nsub end end pageflag = true newsub = string.gsub(newsub, '_$', '') --$ oldsub = newsub local newsub = "" local k = string.len(oldsub) local n = start repeat if n and n.id ~= GLUE then color_node(n, COLOR) l, newsub = signature(n, newsub, swap) elseif n then newsub = newsub .. "_" end n = n.next until not n or newsub == oldsub or l >= k end end return new end local check_regexpr = function (glyph) local COLOR = luatypo.colortbl[3] local lang = glyph.lang local match = false local lchar, id = is_glyph(glyph) local previous = glyph.prev if lang and luatypo.single[lang] then if lchar and lchar < 0x100 and previous and previous.id == GLUE then match = string.find(luatypo.single[lang], string.char(lchar)) if match then pageflag = true 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 pchar < 0x100 and pprev and pprev.id == GLUE then local pattern = string.char(pchar) .. string.char(lchar) match = string.find(luatypo.double[lang], pattern) if match then pageflag = true 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 pchar < 0x100 and ppprev and ppprev.id == GLUE then local pattern = string.char(pchar) .. string.char(lchar) match = string.find(luatypo.double[lang], pattern) if match then pageflag = true color_node(pprev,COLOR) color_node(glyph,COLOR) end end end end end 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 luatypo.check_page = function (head) 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 pageno = tex.getcount("c@page") local Stretch = math.max(luatypo.Stretch/100,1) local blskip = tex.getglue("baselineskip") local textht = tex.getdimen("textheight") local vpos_min = (PAGEmin+1) * blskip local vpos = 0 local pageflag = false local orphanflag = false local widowflag = false local lwhyphflag = false local pageshort = false local firstwd = "" local lastwd = "" local hyphcount = 0 local pageline = 0 while head do local nextnode = head.next local prevnode = head.prev local pprevnode = nil if prevnode then pprevnode = prevnode.prev end if head.id == HLIST and head.subtype == LINE then vpos = vpos + head.height + head.depth local first = head.head while first.id == MKERN or (first.id == GLUE and first.subtype == LFTSKIP) do first = first.next end local ListItem = false pageline = pageline + 1 if head.glue_set == 1 and head.glue_sign == 2 and head.glue_order == 0 and OverfullLines then pageflag = true local COLOR = luatypo.colortbl[7] color_line (head, COLOR) elseif head.glue_set >= Stretch and head.glue_sign == 1 and head.glue_order == 0 and UnderfullLines then local COLOR = luatypo.colortbl[8] pageflag = true color_line (head, COLOR) end if first.id == LPAR then hyphcount = 0 parline = 1 if not nextnode then orphanflag = true end local nn = first.next if nn and nn.id == HLIST and nn.subtype == BOX then ListItem = true end else parline = parline + 1 end if FirstWordMatch then local flag = not ListItem firstwd = check_first_word(firstwd, first, flag) end local ln = slide(first) local pn = ln.prev if pn and pn.id == GLUE and pn.subtype == PARFILL then hyphcount = 0 orphanflag = false if pageline == 1 and parline > 1 then widowflag = true end local PFskip = effective_glue(pn,head) if ShortLines then local llwd = tex.hsize - PFskip if llwd < LLminWD then pageflag = true local COLOR = luatypo.colortbl[6] local attr = oberdiek.luacolor.getattribute() color_line (head, COLOR) end end if BackParindent and PFskip < BackPI and PFskip > BackFuzz then pageflag = true local COLOR = luatypo.colortbl[12] local attr = oberdiek.luacolor.getattribute() color_line (head, COLOR) end if LastWordMatch then local flag = textline if PFskip > BackPI then flag = false end lastwd = check_last_word(lastwd, pn, flag) end elseif pn and pn.id == DISC then hyphcount = hyphcount + 1 if LastWordMatch then lastwd = check_last_word(lastwd, ln, true) end if hyphcount > HYPHmax and RepeatedHyphens then local COLOR = luatypo.colortbl[2] local pg = show_pre_disc (pn,COLOR) pageflag = true end if not nextnode and EOPHyphens then lwhyphflag = true end if nextnode and ParLastHyphen then local nnnode = nextnode.next local nnnnode = nil if nnnode and nnnode.next then nnnnode = nnnode.next if nnnnode and nnnnode.id == HLIST and nnnnode.subtype == 1 and nnnnode.glue_order == 2 then local COLOR = luatypo.colortbl[0] local pg = show_pre_disc (pn,COLOR) pageflag = true end end end else hyphcount = 0 if LastWordMatch and pn then lastwd = check_last_word(lastwd, pn, true) 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 check_regexpr(pn,line) end end end if widowflag and Widows then pageflag = true widowflag = false local COLOR = luatypo.colortbl[4] color_line (head, COLOR) end if orphanflag and Orphans then pageflag = true local COLOR = luatypo.colortbl[5] color_line (head, COLOR) end if lwhyphflag and EOPHyphens then pageflag = true local COLOR = luatypo.colortbl[1] local pg = show_pre_disc (pn,COLOR) end elseif head.id == GLUE and head.subtype == USRSKIP then vpos = vpos + head.width if not nextnode and ShortPages and pageline > 1 and pageline < PAGEmin then pageshort = true end if pageshort and vpos < vpos_min then pageflag = true local COLOR = luatypo.colortbl[9] local n = head repeat n = n.prev until n.id == HLIST and (n.subtype == LINE or n.subtype == EQN) if n then color_line(n, COLOR) end end elseif head.id == GLUE then vpos = vpos + head.width end head = nextnode end if pageflag then local pl = luatypo.pagelist local p = tonumber(string.match(pl, "%s(%d+),%s$")) if not p or pageno > p then luatypo.pagelist = luatypo.pagelist .. tostring(pageno) .. ", " end end return true end return luatypo.check_page \end{luacode} \AtEndOfPackage{% \directlua{ if not luatypo.None then luatexbase.add_to_callback ("pre_output_filter",luatypo.check_page,"check_page") 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{mygrey}{gray}{0.6}% \definecolor{myred}{rgb}{1,0.55,0} \luatypoSetColor0{red}% Paragraph last full line hyphenated \luatypoSetColor1{red}% Page last word hyphenated \luatypoSetColor2{red}% Hyphens ending two many consecutive lines \luatypoSetColor3{red}% Short word at end of line \luatypoSetColor4{cyan}% Widow \luatypoSetColor5{cyan}% Orphan \luatypoSetColor6{cyan}% Paragraph ending on a short line \luatypoSetColor7{blue}% Overfull lines \luatypoSetColor8{blue}% Underfull lines \luatypoSetColor9{red}% Nearly empty page \luatypoSetColor{10}{myred}% First word matches \luatypoSetColor{11}{myred}% Last word matches \luatypoSetColor{12}{mygrey}% Paragraph ending on a nearly full line \luatypoBackPI=1em\relax \luatypoBackFuzz=2pt\relax \luatypoLLminWD=2\parindent\relax \luatypoStretchMax=200\relax \luatypoHyphMax=2\relax \luatypoPageMin=5\relax \luatypoMinFull=4\relax \luatypoMinPART=4\relax }% %% %% %% End of file `lua-typo.sty'.