-- cmgraded-hyphenation.lua
--
-- Restore OT1+pdflatex hyphenation for Computer Modern Graded text under
-- lualatex. In OT1 an accented letter is built with TeX's \accent primitive,
-- which makes it a boundary for the hyphenation ALGORITHM: only the run before
-- a word's first accent is hyphenated automatically; from the accent onward the
-- word is not (e.g. "Metalloberflaeche" may break in "Metallober-", but not
-- around or after the accent). A literal '-' or an explicit \- is unaffected --
-- those are not the algorithm's doing and OT1 breaks there regardless (marking
-- the rest of an accented word by hand with \- is exactly the OT1 idiom). This
-- package's OpenType text uses precomposed Unicode accented glyphs, which
-- lualatex would hyphenate in full the modern way; this file reproduces the
-- classic OT1 behaviour regarding accents instead.
--
-- Self-contained: the accent set A below and the callback that follows are all
-- it needs, so this generated file can be copied and used on its own (the set A
-- is exactly the accented code points cmgraded composes, i.e. the OT1 \accent
-- letters). Loaded by cmgraded.sty only when the 'exacthyphenation' key is on.
--
local A = {
  [0x00C0]=true, [0x00C1]=true, [0x00C2]=true, [0x00C3]=true, [0x00C4]=true, [0x00C5]=true, [0x00C7]=true, [0x00C8]=true, [0x00C9]=true, [0x00CA]=true, 
  [0x00CB]=true, [0x00CC]=true, [0x00CD]=true, [0x00CE]=true, [0x00CF]=true, [0x00D1]=true, [0x00D2]=true, [0x00D3]=true, [0x00D4]=true, [0x00D5]=true, 
  [0x00D6]=true, [0x00D9]=true, [0x00DA]=true, [0x00DB]=true, [0x00DC]=true, [0x00DD]=true, [0x00E0]=true, [0x00E1]=true, [0x00E2]=true, [0x00E3]=true, 
  [0x00E4]=true, [0x00E5]=true, [0x00E7]=true, [0x00E8]=true, [0x00E9]=true, [0x00EA]=true, [0x00EB]=true, [0x00EC]=true, [0x00ED]=true, [0x00EE]=true, 
  [0x00EF]=true, [0x00F1]=true, [0x00F2]=true, [0x00F3]=true, [0x00F4]=true, [0x00F5]=true, [0x00F6]=true, [0x00F9]=true, [0x00FA]=true, [0x00FB]=true, 
  [0x00FC]=true, [0x00FD]=true, [0x00FF]=true, [0x0100]=true, [0x0101]=true, [0x0102]=true, [0x0103]=true, [0x0106]=true, [0x0107]=true, [0x0108]=true, 
  [0x0109]=true, [0x010A]=true, [0x010B]=true, [0x010C]=true, [0x010D]=true, [0x010E]=true, [0x010F]=true, [0x0112]=true, [0x0113]=true, [0x0114]=true, 
  [0x0115]=true, [0x0116]=true, [0x0117]=true, [0x011A]=true, [0x011B]=true, [0x011C]=true, [0x011D]=true, [0x011E]=true, [0x011F]=true, [0x0120]=true, 
  [0x0121]=true, [0x0124]=true, [0x0125]=true, [0x0128]=true, [0x0129]=true, [0x012A]=true, [0x012B]=true, [0x012C]=true, [0x012D]=true, [0x0130]=true, 
  [0x0134]=true, [0x0135]=true, [0x0139]=true, [0x013A]=true, [0x013D]=true, [0x013E]=true, [0x0143]=true, [0x0144]=true, [0x0147]=true, [0x0148]=true, 
  [0x014C]=true, [0x014D]=true, [0x014E]=true, [0x014F]=true, [0x0150]=true, [0x0151]=true, [0x0154]=true, [0x0155]=true, [0x0158]=true, [0x0159]=true, 
  [0x015A]=true, [0x015B]=true, [0x015C]=true, [0x015D]=true, [0x015E]=true, [0x015F]=true, [0x0160]=true, [0x0161]=true, [0x0162]=true, [0x0163]=true, 
  [0x0164]=true, [0x0165]=true, [0x0168]=true, [0x0169]=true, [0x016A]=true, [0x016B]=true, [0x016C]=true, [0x016D]=true, [0x016E]=true, [0x016F]=true, 
  [0x0170]=true, [0x0171]=true, [0x0174]=true, [0x0175]=true, [0x0176]=true, [0x0177]=true, [0x0178]=true, [0x0179]=true, [0x017A]=true, [0x017B]=true, 
  [0x017C]=true, [0x017D]=true, [0x017E]=true, [0x01CD]=true, [0x01CE]=true, [0x01CF]=true, [0x01D0]=true, [0x01D1]=true, [0x01D2]=true, [0x01D3]=true, 
  [0x01D4]=true, [0x01E6]=true, [0x01E7]=true, [0x01F0]=true, [0x01F4]=true, [0x01F5]=true, [0x1E26]=true, [0x1E27]=true, [0x1E44]=true, [0x1E45]=true, 
  [0x1E58]=true, [0x1E59]=true, [0x1E80]=true, [0x1E81]=true, [0x1E82]=true, [0x1E83]=true, [0x1E84]=true, [0x1E85]=true, [0x1E97]=true, [0x1EBC]=true, 
  [0x1EBD]=true, [0x1EF2]=true, [0x1EF3]=true, [0x1EF8]=true, [0x1EF9]=true, 
}

-- Register once. Reproduce OT1's hyphenation of accented words: TeX's \accent
-- turns the accented letter into a boundary, so only the run BEFORE the first
-- accent of a word is hyphenated by the algorithm -- nothing from an accent
-- onward is (its run starts after the accent box, not after a space). We
-- therefore neutralise every ALGORITHMIC break (disc subtype 'regular') that
-- sits at or after an accented glyph in its word, or within righthyphenmin
-- characters before one. A literal '-' ('automatic') and an explicit \-
-- ('explicit') are left untouched: OT1 breaks there regardless of accents (and
-- explicit \- is exactly how one hand-marks the parts OT1 won't do itself).
-- Runs on pre_linebreak_filter (a list callback), so it composes with
-- babel/polyglossia language switches and microtype.
if not cmgraded_exacthyphenation then
  cmgraded_exacthyphenation = true
  local traverse = node.traverse
  local DISC  = node.id("disc")
  local GLYPH = node.id("glyph")
  local KERN  = node.id("kern")
  local GLUE  = node.id("glue")
  local REGULAR
  for k, v in pairs(node.subtypes("disc")) do
    if v == "regular" then REGULAR = k break end
  end
  -- The f-ligatures stand for several characters, so righthyphenmin is counted
  -- in characters (as TeX does), not glyphs -- otherwise a break like
  -- "Metallober-<fl>ache" would see the accent one glyph too close.
  local LIG = { [0xFB00]=2, [0xFB01]=2, [0xFB02]=2, [0xFB03]=3, [0xFB04]=3 }
  local function suppress(head)
    local rh = tex.righthyphenmin
    if rh < 1 then rh = 2 end
    local sawaccent = false          -- an accent earlier in the current word?
    for n in traverse(head) do
      local id = n.id
      if id == GLYPH then
        if A[n.char] then sawaccent = true end
      elseif id == DISC and n.subtype == REGULAR then
        local kill = sawaccent       -- (a) break lies at/after an accent
        if not kill then             -- (b) an accent within righthyphenmin chars after
          local chars, q = 0, n.next
          while q and chars < rh do
            local qid = q.id
            if qid == GLYPH then
              if A[q.char] then kill = true break end
              chars = chars + (LIG[q.char] or 1)
            elseif qid == KERN then
              -- interior kern: skip
            else
              break                  -- word boundary
            end
            q = q.next
          end
        end
        if kill then n.penalty = 10000 end
      elseif id == KERN then
        -- interior kern: keep the word going
      elseif id == GLUE then
        sawaccent = false            -- inter-word space: reset
      end
    end
    return head
  end
  luatexbase.add_to_callback("pre_linebreak_filter", suppress, "cmgraded.exacthyphenation")
end
