if not modules then modules = { } end modules ['font-ots'] = { -- sequences
    version   = 1.001,
    comment   = "companion to font-ini.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files",
}

--[[ldx--
<p>This module is a bit more split up that I'd like but since we also want to test
with plain <l n='tex'/> it has to be so. This module is part of <l n='context'/>
and discussion about improvements and functionality mostly happens on the
<l n='context'/> mailing list.</p>

<p>The specification of OpenType is (or at least a decade ago was) kind of vague.
Apart from a lack of a proper free specifications there's also the problem that
Microsoft and Adobe may have their own interpretation of how and in what order to
apply features. In general the Microsoft website has more detailed specifications
and is a better reference. There is also some information in the FontForge help
files. In the end we rely most on the Microsoft specification.</p>

<p>Because there is so much possible, fonts might contain bugs and/or be made to
work with certain rederers. These may evolve over time which may have the side
effect that suddenly fonts behave differently. We don't want to catch all font
issues.</p>

<p>After a lot of experiments (mostly by Taco, me and Idris) the first implementation
becaus quite useful. When it did most of what we wanted, a more optimized version
evolved. Of course all errors are mine and of course the code can be improved. There
are quite some optimizations going on here and processing speed is currently quite
acceptable and has been improved over time. Many complex scripts are not yet supported
yet, but I will look into them as soon as <l n='context'/> users ask for it.</p>

<p>The specification leaves room for interpretation. In case of doubt the Microsoft
implementation is the reference as it is the most complete one. As they deal with
lots of scripts and fonts, Kai and Ivo did a lot of testing of the generic code and
their suggestions help improve the code. I'm aware that not all border cases can be
taken care of, unless we accept excessive runtime, and even then the interference
with other mechanisms (like hyphenation) are not trivial.</p>

<p>Especially discretionary handling has been improved much by Kai Eigner who uses complex
(latin) fonts. The current implementation is a compromis between his patches and my code
and in the meantime performance is quite ok. We cannot check all border cases without
compromising speed but so far we're okay. Given good test cases we can probably improve
it here and there. Especially chain lookups are non trivial with discretionaries but
things got much better over time thanks to Kai.</p>

<p>Glyphs are indexed not by unicode but in their own way. This is because there is no
relationship with unicode at all, apart from the fact that a font might cover certain
ranges of characters. One character can have multiple shapes. However, at the
<l n='tex'/> end we use unicode so and all extra glyphs are mapped into a private
space. This is needed because we need to access them and <l n='tex'/> has to include
then in the output eventually.</p>

<p>The initial data table is rather close to the open type specification and also not
that different from the one produced by <l n='fontforge'/> but we uses hashes instead.
In <l n='context'/> that table is packed (similar tables are shared) and cached on disk
so that successive runs can use the optimized table (after loading the table is
unpacked).</p>

<p>This module is sparsely documented because it is has been a moving target. The
table format of the reader changed a bit over time and we experiment a lot with
different methods for supporting features. By now the structures are quite stable</p>

<p>Incrementing the version number will force a re-cache. We jump the number by one
when there's a fix in the reader or processing code that can result in different
results.</p>

<p>This code is also used outside context but in context it has to work with other
mechanisms. Both put some constraints on the code here.</p>

--ldx]]--

-- Remark: We assume that cursives don't cross discretionaries which is okay because it
-- is only used in semitic scripts.
--
-- Remark: We assume that marks precede base characters.
--
-- Remark: When complex ligatures extend into discs nodes we can get side effects. Normally
-- this doesn't happen; ff\d{l}{l}{l} in lm works but ff\d{f}{f}{f}.
--
-- Todo: check if we copy attributes to disc nodes if needed.
--
-- Todo: it would be nice if we could get rid of components. In other places we can use
-- the unicode properties.
--
-- Remark: We do some disc juggling where we need to keep in mind that the pre, post and
-- replace fields can have prev pointers to a nesting node ... I wonder if that is still
-- needed.
--
-- Remark: This is not possible:
--
-- \discretionary {alpha-} {betagammadelta}
--   {\discretionary {alphabeta-} {gammadelta}
--      {\discretionary {alphabetagamma-} {delta}
--         {alphabetagammadelta}}}
--
-- Remark: Something is messed up: we have two mark / ligature indices, one at the
-- injection end and one here ... this is based on KE's patches but there is something
-- fishy there as I'm pretty sure that for husayni we need some connection (as it's much
-- more complex than an average font) but I need proper examples of all cases, not of
-- only some.
--
-- Remark: I wonder if indexed would be faster than unicoded. It would be a major
-- rewrite to have char being unicode + an index field in glyph nodes. Also more
-- assignments have to be made in order to keep things in sync. So, it's a no-go.
--
-- Remark: We can provide a fast loop when there are no disc nodes (tests show a 1%
-- gain). Smaller functions might perform better cache-wise. But ... memory becomes
-- faster anyway, so ...

local type, next, tonumber = type, next, tonumber
local random = math.random
local formatters = string.formatters
local insert = table.insert

local registertracker    = trackers.register

local logs               = logs
local trackers           = trackers
local nodes              = nodes
local attributes         = attributes
local fonts              = fonts

local otf                = fonts.handlers.otf
local tracers            = nodes.tracers

local trace_singles      = false  registertracker("otf.singles",      function(v) trace_singles      = v end)
local trace_multiples    = false  registertracker("otf.multiples",    function(v) trace_multiples    = v end)
local trace_alternatives = false  registertracker("otf.alternatives", function(v) trace_alternatives = v end)
local trace_ligatures    = false  registertracker("otf.ligatures",    function(v) trace_ligatures    = v end)
local trace_contexts     = false  registertracker("otf.contexts",     function(v) trace_contexts     = v end)
local trace_marks        = false  registertracker("otf.marks",        function(v) trace_marks        = v end)
local trace_kerns        = false  registertracker("otf.kerns",        function(v) trace_kerns        = v end)
local trace_cursive      = false  registertracker("otf.cursive",      function(v) trace_cursive      = v end)
local trace_preparing    = false  registertracker("otf.preparing",    function(v) trace_preparing    = v end)
local trace_bugs         = false  registertracker("otf.bugs",         function(v) trace_bugs         = v end)
local trace_details      = false  registertracker("otf.details",      function(v) trace_details      = v end)
local trace_steps        = false  registertracker("otf.steps",        function(v) trace_steps        = v end)
local trace_skips        = false  registertracker("otf.skips",        function(v) trace_skips        = v end)
local trace_directions   = false  registertracker("otf.directions",   function(v) trace_directions   = v end)
local trace_plugins      = false  registertracker("otf.plugins",      function(v) trace_plugins      = v end)

local trace_kernruns     = false  registertracker("otf.kernruns",     function(v) trace_kernruns     = v end)
local trace_discruns     = false  registertracker("otf.discruns",     function(v) trace_discruns     = v end)
local trace_compruns     = false  registertracker("otf.compruns",     function(v) trace_compruns     = v end)
local trace_testruns     = false  registertracker("otf.testruns",     function(v) trace_testruns     = v end)

local optimizekerns      = true

local report_direct      = logs.reporter("fonts","otf direct")
local report_subchain    = logs.reporter("fonts","otf subchain")
local report_chain       = logs.reporter("fonts","otf chain")
local report_process     = logs.reporter("fonts","otf process")
local report_warning     = logs.reporter("fonts","otf warning")
local report_run         = logs.reporter("fonts","otf run")

registertracker("otf.replacements", "otf.singles,otf.multiples,otf.alternatives,otf.ligatures")
registertracker("otf.positions","otf.marks,otf.kerns,otf.cursive")
registertracker("otf.actions","otf.replacements,otf.positions")
registertracker("otf.injections","nodes.injections")
registertracker("otf.sample","otf.steps,otf.actions,otf.analyzing")

local nuts               = nodes.nuts
local tonode             = nuts.tonode
local tonut              = nuts.tonut

local getfield           = nuts.getfield
local setfield           = nuts.setfield
local getnext            = nuts.getnext
local setnext            = nuts.setnext
local getprev            = nuts.getprev
local setprev            = nuts.setprev
local getboth            = nuts.getboth
local setboth            = nuts.setboth
local getid              = nuts.getid
local getattr            = nuts.getattr
local setattr            = nuts.setattr
local getprop            = nuts.getprop
local setprop            = nuts.setprop
local getfont            = nuts.getfont
local getsubtype         = nuts.getsubtype
local setsubtype         = nuts.setsubtype
local getchar            = nuts.getchar
local setchar            = nuts.setchar
local getdisc            = nuts.getdisc
local setdisc            = nuts.setdisc
local setlink            = nuts.setlink
local getcomponents      = nuts.getcomponents -- the original one, not yet node-aux
local setcomponents      = nuts.setcomponents -- the original one, not yet node-aux
local getdir             = nuts.getdir
local getwidth           = nuts.getwidth

local ischar             = nuts.is_char

local insert_node_after  = nuts.insert_after
local copy_node          = nuts.copy
local copy_node_list     = nuts.copy_list
local find_node_tail     = nuts.tail
local flush_node_list    = nuts.flush_list
local flush_node         = nuts.flush_node
local end_of_math        = nuts.end_of_math
local traverse_nodes     = nuts.traverse
local traverse_id        = nuts.traverse_id
local set_components     = nuts.set_components
local take_components    = nuts.take_components
local count_components   = nuts.count_components
local copy_no_components = nuts.copy_no_components
local copy_only_glyphs   = nuts.copy_only_glyphs

local setmetatableindex  = table.setmetatableindex

----- zwnj               = 0x200C
----- zwj                = 0x200D

local nodecodes          = nodes.nodecodes
local glyphcodes         = nodes.glyphcodes
local disccodes          = nodes.disccodes

local glyph_code         = nodecodes.glyph
local glue_code          = nodecodes.glue
local disc_code          = nodecodes.disc
local math_code          = nodecodes.math
local dir_code           = nodecodes.dir
local localpar_code      = nodecodes.localpar

local discretionary_code = disccodes.discretionary
local ligature_code      = glyphcodes.ligature

local a_state            = attributes.private('state')
local a_noligature       = attributes.private("noligature")

local injections         = nodes.injections
local setmark            = injections.setmark
local setcursive         = injections.setcursive
local setkern            = injections.setkern
local setpair            = injections.setpair
local resetinjection     = injections.reset
local copyinjection      = injections.copy
local setligaindex       = injections.setligaindex
local getligaindex       = injections.getligaindex

local fontdata           = fonts.hashes.identifiers
local fontfeatures       = fonts.hashes.features

local otffeatures        = fonts.constructors.features.otf
local registerotffeature = otffeatures.register

local onetimemessage     = fonts.loggers.onetimemessage or function() end

local getrandom          = utilities and utilities.randomizer and utilities.randomizer.get

otf.defaultnodealternate = "none" -- first last

-- We use a few semi-global variables. The handler can be called nested but this assumes
-- that the same font is used.

local tfmdata            = false
local characters         = false
local descriptions       = false
local marks              = false
local classes            = false
local currentfont        = false
local factor             = 0
local threshold          = 0
local checkmarks         = false

local sweepnode          = nil
local sweepprev          = nil
local sweepnext          = nil
local sweephead          = { }

local notmatchpre        = { }
local notmatchpost       = { }
local notmatchreplace    = { }

local handlers           = { }

local isspace            = injections.isspace
local getthreshold       = injections.getthreshold

local checkstep          = (tracers and tracers.steppers.check)    or function() end
local registerstep       = (tracers and tracers.steppers.register) or function() end
local registermessage    = (tracers and tracers.steppers.message)  or function() end

local function checkdisccontent(d)
    local pre, post, replace = getdisc(d)
    if pre     then for n in traverse_id(glue_code,pre)     do print("pre",nodes.idstostring(pre))     break end end
    if post    then for n in traverse_id(glue_code,post)    do print("pos",nodes.idstostring(post))    break end end
    if replace then for n in traverse_id(glue_code,replace) do print("rep",nodes.idstostring(replace)) break end end
end

local function logprocess(...)
    if trace_steps then
        registermessage(...)
    end
    report_direct(...)
end

local function logwarning(...)
    report_direct(...)
end

local f_unicode = formatters["%U"]
local f_uniname = formatters["%U (%s)"]
local f_unilist = formatters["% t (% t)"]

local function gref(n) -- currently the same as in font-otb
    if type(n) == "number" then
        local description = descriptions[n]
        local name = description and description.name
        if name then
            return f_uniname(n,name)
        else
            return f_unicode(n)
        end
    elseif n then
        local num, nam = { }, { }
        for i=1,#n do
            local ni = n[i]
            if tonumber(ni) then -- later we will start at 2
                local di = descriptions[ni]
                num[i] = f_unicode(ni)
                nam[i] = di and di.name or "-"
            end
        end
        return f_unilist(num,nam)
    else
        return "<error in node mode tracing>"
    end
end

local function cref(dataset,sequence,index)
    if not dataset then
        return "no valid dataset"
    elseif index then
        return formatters["feature %a, type %a, chain lookup %a, index %a"](dataset[4],sequence.type,sequence.name,index)
    else
        return formatters["feature %a, type %a, chain lookup %a"](dataset[4],sequence.type,sequence.name)
    end
end

local function pref(dataset,sequence)
    return formatters["feature %a, type %a, lookup %a"](dataset[4],sequence.type,sequence.name)
end

local function mref(rlmode)
    if not rlmode or rlmode == 0 then
        return "---"
    elseif rlmode == -1 or rlmode == "+TRT" then
        return "r2l"
    else
        return "l2r"
    end
end

-- The next code is somewhat complicated by the fact that some fonts can have ligatures made
-- from ligatures that themselves have marks. This was identified by Kai in for instance
-- arabtype:  KAF LAM SHADDA ALEF FATHA (0x0643 0x0644 0x0651 0x0627 0x064E). This becomes
-- KAF LAM-ALEF with a SHADDA on the first and a FATHA op de second component. In a next
-- iteration this becomes a KAF-LAM-ALEF with a SHADDA on the second and a FATHA on the
-- third component.

-- We can assume that languages that use marks are not hyphenated. We can also assume
-- that at most one discretionary is present.

-- We do need components in funny kerning mode but maybe I can better reconstruct then
-- as we do have the font components info available; removing components makes the
-- previous code much simpler. Also, later on copying and freeing becomes easier.
-- However, for arabic we need to keep them around for the sake of mark placement
-- and indices.

local function flattendisk(head,disc)
    local pre, post, replace, pretail, posttail, replacetail = getdisc(disc,true)
    local prev, next = getboth(disc)
    local ishead = head == disc
    setdisc(disc)
    flush_node(disc)
    if pre then
        flush_node_list(pre)
    end
    if post then
        flush_node_list(post)
    end
    if ishead then
        if replace then
            if next then
                setlink(replacetail,next)
            end
            return replace, replace
        elseif next then
            return next, next
        else
            return -- maybe warning
        end
    else
        if replace then
            if next then
                setlink(replacetail,next)
            end
            setlink(prev,replace)
            return head, replace
        else
            setlink(prev,next) -- checks for next anyway
            return head, next
        end
    end
end

local function appenddisc(disc,list)
    local pre, post, replace, pretail, posttail, replacetail = getdisc(disc,true)
    local posthead    = list
    local replacehead = copy_node_list(list)
    if post then
        setlink(posttail,posthead)
    else
        post = posthead
    end
    if replace then
        setlink(replacetail,replacehead)
    else
        replace = replacehead
    end
    setdisc(disc,pre,post,replace)
end

-- start is a mark and we need to keep that one

local take_components = getcomponents -- we overload here (for now)
local set_components  = setcomponents -- we overload here (for now)
----- get_components  = getcomponents -- we overload here (for now)

local function count_components(start,marks)
    if getid(start) ~= glyph_code then
        return 0
    elseif getsubtype(start) == ligature_code then
        local i = 0
        local components = getcomponents(start)
        while components do
            i = i + count_components(components,marks)
            components = getnext(components)
        end
        return i
    elseif not marks[getchar(start)] then
        return 1
    else
        return 0
    end
end

local function markstoligature(head,start,stop,char)
    if start == stop and getchar(start) == char then
        return head, start
    else
        local prev = getprev(start)
        local next = getnext(stop)
        setprev(start)
        setnext(stop)
        local base = copy_no_components(start,copyinjection)
        if head == start then
            head = base
        end
        resetinjection(base)
        setchar(base,char)
        setsubtype(base,ligature_code)
        set_components(base,start)
        setlink(prev,base,next)
        return head, base
    end
end

-- local function toligature(head,start,stop,char,dataset,sequence,markflag,discfound) -- brr head
--     if getattr(start,a_noligature) == 1 then
--         -- so we can do: e\noligature{ff}e e\noligature{f}fie (we only look at the first)
--         return head, start
--     end
--     if start == stop and getchar(start) == char then
--         resetinjection(start)
--         setchar(start,char)
--         return head, start
--     end
--     local prev = getprev(start)
--     local next = getnext(stop)
--     local comp = start
--     setprev(start)
--     setnext(stop)
--     local base = copy_no_components(start,copyinjection)
--     if start == head then
--         head = base
--     end
--     resetinjection(base)
--     setchar(base,char)
--     setsubtype(base,ligature_code)
--     set_components(base,comp)
--     setlink(prev,base,next)
--     if not discfound then
--         local deletemarks = markflag ~= "mark"
--         local components = start
--         local baseindex = 0
--         local componentindex = 0
--         local head = base
--         local current = base
--         -- first we loop over the glyphs in start .. stop
--         while start do
--             local char = getchar(start)
--             if not marks[char] then
--                 baseindex = baseindex + componentindex
--                 componentindex = getcomponentindex(start,marks)
--             elseif not deletemarks then -- quite fishy
--                 setligaindex(start,baseindex + getligaindex(start,componentindex))
--                 if trace_marks then
--                     logwarning("%s: keep mark %s, gets index %s",pref(dataset,sequence),gref(char),getligaindex(start))
--                 end
--                 local n = copy_node(start)
--                 copyinjection(n,start)
--                 head, current = insert_node_after(head,current,n) -- unlikely that mark has components
--             elseif trace_marks then
--                 logwarning("%s: delete mark %s",pref(dataset,sequence),gref(char))
--             end
--             start = getnext(start)
--         end
--         -- we can have one accent as part of a lookup and another following
--      -- local start = components -- was wrong (component scanning was introduced when more complex ligs in devanagari was added)
--         local start = getnext(current)
--         while start do
--             local char = ischar(start)
--             if char then
--                 if marks[char] then
--                     setligaindex(start,baseindex + getligaindex(start,componentindex))
--                     if trace_marks then
--                         logwarning("%s: set mark %s, gets index %s",pref(dataset,sequence),gref(char),getligaindex(start))
--                     end
--                     start = getnext(start)
--                 else
--                     break
--                 end
--             else
--                 break
--             end
--         end
--     else
--         -- discfound ... forget about marks .. probably no scripts that hyphenate and have marks
--         local discprev, discnext = getboth(discfound)
--         if discprev and discnext then
--             -- we assume normalization in context, and don't care about generic ... especially
--             -- \- can give problems as there we can have a negative char but that won't match
--             -- anyway
--             local pre, post, replace, pretail, posttail, replacetail = getdisc(discfound,true)
--             if not replace then -- todo: signal simple hyphen
--                 local prev     = getprev(base)
--                 local current  = comp
--                 local previous = nil
--                 local copied   = nil
--                 while current do
--                     if getid(current) == glyph_code then
--                         local n = copy_node(current)
--                         if copied then
--                             setlink(previous,n)
--                         else
--                             copied = n
--                         end
--                         previous = n
--                     end
--                     current = getnext(current)
--                 end
--                 setprev(discnext) -- also blocks funny assignments
--                 setnext(discprev) -- also blocks funny assignments
--                 if pre then
--                     setlink(discprev,pre)
--                 end
--                 pre = comp
--                 if post then
--                     setlink(posttail,discnext)
--                     setprev(post)
--                 else
--                     post = discnext
--                 end
--                 setlink(prev,discfound,next)
--                 setboth(base)
--                 set_components(base,copied)
--                 setdisc(discfound,pre,post,base) -- was discretionary_code
--                 base = prev -- restart
--             end
--         end
--     end
--     return head, base
-- end

local function toligature(head,start,stop,char,dataset,sequence,markflag,discfound) -- brr head
    if getattr(start,a_noligature) == 1 then
        -- so we can do: e\noligature{ff}e e\noligature{f}fie (we only look at the first)
        return head, start
    end
    if start == stop and getchar(start) == char then
        resetinjection(start)
        setchar(start,char)
        return head, start
    end
    local prev = getprev(start)
    local next = getnext(stop)
    local comp = start
    setprev(start)
    setnext(stop)
    local base = copy_no_components(start,copyinjection)
    if start == head then
        head = base
    end
    resetinjection(base)
    setchar(base,char)
    setsubtype(base,ligature_code)
    set_components(base,comp)
    setlink(prev,base,next)
    if not discfound then
        local deletemarks = markflag ~= "mark"
        local components = start
        local baseindex = 0
        local componentindex = 0
        local head = base
        local current = base
        -- first we loop over the glyphs in start .. stop
        while start do
            local char = getchar(start)
            if not marks[char] then
                baseindex = baseindex + componentindex
                componentindex = count_components(start,marks)
            elseif not deletemarks then -- quite fishy
                setligaindex(start,baseindex + getligaindex(start,componentindex))
                if trace_marks then
                    logwarning("%s: keep mark %s, gets index %s",pref(dataset,sequence),gref(char),getligaindex(start))
                end
                local n = copy_node(start)
                copyinjection(n,start)
                head, current = insert_node_after(head,current,n) -- unlikely that mark has components
            elseif trace_marks then
                logwarning("%s: delete mark %s",pref(dataset,sequence),gref(char))
            end
            start = getnext(start)
        end
        -- we can have one accent as part of a lookup and another following
        local start = getnext(current)
        while start do
            local char = ischar(start)
            if char then
                if marks[char] then
                    setligaindex(start,baseindex + getligaindex(start,componentindex))
                    if trace_marks then
                        logwarning("%s: set mark %s, gets index %s",pref(dataset,sequence),gref(char),getligaindex(start))
                    end
                    start = getnext(start)
                else
                    break
                end
            else
                break
            end
        end
    else
        -- discfound ... forget about marks .. probably no scripts that hyphenate and have marks
        local discprev, discnext = getboth(discfound)
        if discprev and discnext then
            -- we assume normalization in context, and don't care about generic ... especially
            -- \- can give problems as there we can have a negative char but that won't match
            -- anyway
            local pre, post, replace, pretail, posttail, replacetail = getdisc(discfound,true)
            if not replace then
                local prev = getprev(base)
                local comp = take_components(base)
                local copied = copy_only_glyphs(comp)
                if pre then
                    setlink(discprev,pre)
                else
                    setnext(discprev) -- also blocks funny assignments
                end
                pre = comp
                if post then
                    setlink(posttail,discnext)
                    setprev(post)
                else
                    post = discnext
                    setprev(discnext) -- also blocks funny assignments
                end
                setlink(prev,discfound,next)
                setboth(base)
                -- here components have a pointer so we can't free it!
                set_components(base,copied)
                replace = base
                setdisc(discfound,pre,post,replace) -- was discretionary_code
                base = prev
            end
        end
    end
    return head, base
end

local function multiple_glyphs(head,start,multiple,ignoremarks,what)
    local nofmultiples = #multiple
    if nofmultiples > 0 then
        resetinjection(start)
        setchar(start,multiple[1])
        if nofmultiples > 1 then
            local sn = getnext(start)
            for k=2,nofmultiples do
             -- untested:
             --
             -- while ignoremarks and marks[getchar(sn)] then
             --     local sn = getnext(sn)
             -- end
                local n = copy_node(start) -- ignore components
                resetinjection(n)
                setchar(n,multiple[k])
                insert_node_after(head,start,n)
                start = n
            end
            if what == true then
                -- we're ok
            elseif what > 1 then
                local m = multiple[nofmultiples]
                for i=2,what do
                    local n = copy_node(start) -- ignore components
                    resetinjection(n)
                    setchar(n,m)
                    insert_node_after(head,start,n)
                    start = n
                end
            end
        end
        return head, start, true
    else
        if trace_multiples then
            logprocess("no multiple for %s",gref(getchar(start)))
        end
        return head, start, false
    end
end

local function get_alternative_glyph(start,alternatives,value)
    local n = #alternatives
    if n == 1 then
        -- we could actually change that into a gsub and save some memory in the
        -- font loader but it makes tracing more messy
        return alternatives[1], trace_alternatives and "1 (only one present)"
    elseif value == "random" then
        local r = getrandom and getrandom("glyph",1,n) or random(1,n)
        return alternatives[r], trace_alternatives and formatters["value %a, taking %a"](value,r)
    elseif value == "first" then
        return alternatives[1], trace_alternatives and formatters["value %a, taking %a"](value,1)
    elseif value == "last" then
        return alternatives[n], trace_alternatives and formatters["value %a, taking %a"](value,n)
    end
    value = value == true and 1 or tonumber(value)
    if type(value) ~= "number" then
        return alternatives[1], trace_alternatives and formatters["invalid value %s, taking %a"](value,1)
    end
 -- local a = alternatives[value]
 -- if a then
 --     -- some kind of hash
 --     return a, trace_alternatives and formatters["value %a, taking %a"](value,a)
 -- end
    if value > n then
        local defaultalt = otf.defaultnodealternate
        if defaultalt == "first" then
            return alternatives[n], trace_alternatives and formatters["invalid value %s, taking %a"](value,1)
        elseif defaultalt == "last" then
            return alternatives[1], trace_alternatives and formatters["invalid value %s, taking %a"](value,n)
        else
            return false, trace_alternatives and formatters["invalid value %a, %s"](value,"out of range")
        end
    elseif value == 0 then
        return getchar(start), trace_alternatives and formatters["invalid value %a, %s"](value,"no change")
    elseif value < 1 then
        return alternatives[1], trace_alternatives and formatters["invalid value %a, taking %a"](value,1)
    else
        return alternatives[value], trace_alternatives and formatters["value %a, taking %a"](value,value)
    end
end

-- handlers

function handlers.gsub_single(head,start,dataset,sequence,replacement)
    if trace_singles then
        logprocess("%s: replacing %s by single %s",pref(dataset,sequence),gref(getchar(start)),gref(replacement))
    end
    resetinjection(start)
    setchar(start,replacement)
    return head, start, true
end

function handlers.gsub_alternate(head,start,dataset,sequence,alternative)
    local kind  = dataset[4]
    local what  = dataset[1]
    local value = what == true and tfmdata.shared.features[kind] or what
    local choice, comment = get_alternative_glyph(start,alternative,value)
    if choice then
        if trace_alternatives then
            logprocess("%s: replacing %s by alternative %a to %s, %s",pref(dataset,sequence),gref(getchar(start)),gref(choice),comment)
        end
        resetinjection(start)
        setchar(start,choice)
    else
        if trace_alternatives then
            logwarning("%s: no variant %a for %s, %s",pref(dataset,sequence),value,gref(getchar(start)),comment)
        end
    end
    return head, start, true
end

function handlers.gsub_multiple(head,start,dataset,sequence,multiple)
    if trace_multiples then
        logprocess("%s: replacing %s by multiple %s",pref(dataset,sequence),gref(getchar(start)),gref(multiple))
    end
    return multiple_glyphs(head,start,multiple,sequence.flags[1],dataset[1])
end

function handlers.gsub_ligature(head,start,dataset,sequence,ligature)
    local current   = getnext(start)
    if not current then
        return head, start, false, nil
    end
    local stop      = nil
    local startchar = getchar(start)
    if marks[startchar] then
        while current do
            local char = ischar(current,currentfont)
            if char then
                local lg = ligature[char]
                if lg then
                    stop     = current
                    ligature = lg
                    current  = getnext(current)
                else
                    break
                end
            else
                break
            end
        end
        if stop then
            local lig = ligature.ligature
            if lig then
                if trace_ligatures then
                    local stopchar = getchar(stop)
                    head, start = markstoligature(head,start,stop,lig)
                    logprocess("%s: replacing %s upto %s by ligature %s case 1",pref(dataset,sequence),gref(startchar),gref(stopchar),gref(getchar(start)))
                else
                    head, start = markstoligature(head,start,stop,lig)
                end
                return head, start, true, false
            else
                -- ok, goto next lookup
            end
        end
    else
        local skipmark  = sequence.flags[1]
        local discfound = false
        local lastdisc  = nil
        while current do
            local char, id = ischar(current,currentfont)
            if char then
                if skipmark and marks[char] then
                    current = getnext(current)
                else -- ligature is a tree
                    local lg = ligature[char] -- can there be multiple in a row? maybe in a bad font
                    if lg then
                        if not discfound and lastdisc then
                            discfound = lastdisc
                            lastdisc  = nil
                        end
                        stop     = current -- needed for fake so outside then
                        ligature = lg
                        current  = getnext(current)
                    else
                        break
                    end
                end
            elseif char == false then
                -- kind of weird
                break
            elseif id == disc_code then
                -- tricky .. we also need to do pre here
                local replace = getfield(current,"replace")
                if replace then
                    -- of{f-}{}{f}e  o{f-}{}{f}fe  o{-}{}{ff}e (oe and ff ligature)
                    -- we can end up here when we have a start run .. testruns start at a disc but
                    -- so here we have the other case: char + disc
                    while replace do
                        local char, id = ischar(replace,currentfont)
                        if char then
                            local lg = ligature[char] -- can there be multiple in a row? maybe in a bad font
                            if lg then
                                ligature = lg
                                replace  = getnext(replace)
                            else
                                return head, start, false, false
                            end
                        else
                            return head, start, false, false
                        end
                    end
                    stop = current
                end
                lastdisc = current
                current  = getnext(current)
            else
                break
            end
        end
        local lig = ligature.ligature
        if lig then
            if stop then
                if trace_ligatures then
                    local stopchar = getchar(stop)
                    head, start = toligature(head,start,stop,lig,dataset,sequence,skipmark,discfound)
                    logprocess("%s: replacing %s upto %s by ligature %s case 2",pref(dataset,sequence),gref(startchar),gref(stopchar),gref(lig))
                else
                    head, start = toligature(head,start,stop,lig,dataset,sequence,skipmark,discfound)
                end
            else
                -- weird but happens (in some arabic font)
                resetinjection(start)
                setchar(start,lig)
                if trace_ligatures then
                    logprocess("%s: replacing %s by (no real) ligature %s case 3",pref(dataset,sequence),gref(startchar),gref(lig))
                end
            end
            return head, start, true, discfound
        else
            -- weird but happens, pseudo ligatures ... just the components
        end
    end
    return head, start, false, discfound
end

function handlers.gpos_single(head,start,dataset,sequence,kerns,rlmode,step,i,injection)
    local startchar = getchar(start)
    if step.format == "pair" then
        local dx, dy, w, h = setpair(start,factor,rlmode,sequence.flags[4],kerns,injection)
        if trace_kerns then
            logprocess("%s: shifting single %s by (%p,%p) and correction (%p,%p)",pref(dataset,sequence),gref(startchar),dx,dy,w,h)
        end
    else
        -- needs checking .. maybe no kerns format for single
        local k = setkern(start,factor,rlmode,kerns,injection)
        if trace_kerns then
            logprocess("%s: shifting single %s by %p",pref(dataset,sequence),gref(startchar),k)
        end
    end
    return head, start, false
end

function handlers.gpos_pair(head,start,dataset,sequence,kerns,rlmode,step,i,injection)
    local snext = getnext(start)
    if not snext then
        return head, start, false
    else
        local prev = start
        while snext do
            local nextchar = ischar(snext,currentfont)
            if nextchar then
                local krn = kerns[nextchar]
                if not krn and marks[nextchar] then
                    prev = snext
                    snext = getnext(snext)
                elseif not krn then
                    break
                elseif step.format == "pair" then
                    local a, b = krn[1], krn[2]
                    if optimizekerns then
                        -- this permits a mixed table, but we could also decide to optimize this
                        -- in the loader and use format 'kern'
                        if not b and a[1] == 0 and a[2] == 0 and a[4] == 0 then
                            local k = setkern(snext,factor,rlmode,a[3],injection)
                            if trace_kerns then
                                logprocess("%s: shifting single %s by %p",pref(dataset,sequence),gref(nextchar),k)
                            end
                            return head, start, true
                        end
                    end
                    if a and #a > 0 then
                        local x, y, w, h = setpair(start,factor,rlmode,sequence.flags[4],a,injection)
                        if trace_kerns then
                            local startchar = getchar(start)
                            logprocess("%s: shifting first of pair %s and %s by (%p,%p) and correction (%p,%p) as %s",pref(dataset,sequence),gref(startchar),gref(nextchar),x,y,w,h,injection or "injections")
                        end
                    end
                    if b and #b > 0 then
                        local x, y, w, h = setpair(snext,factor,rlmode,sequence.flags[4],b,injection)
                        if trace_kerns then
                            local startchar = getchar(snext)
                            logprocess("%s: shifting second of pair %s and %s by (%p,%p) and correction (%p,%p) as %s",pref(dataset,sequence),gref(startchar),gref(nextchar),x,y,w,h,injection or "injections")
                        end
                    end
                    return head, start, true
                elseif krn ~= 0 then
                    local k = setkern(snext,factor,rlmode,krn,injection)
                    if trace_kerns then
                        logprocess("%s: inserting kern %p between %s and %s as %s",pref(dataset,sequence),k,gref(getchar(prev)),gref(nextchar),injection or "injections")
                    end
                    return head, start, true
                else -- can't happen
                    break
                end
            else
                break
            end
        end
        return head, start, false
    end
end

--[[ldx--
<p>We get hits on a mark, but we're not sure if the it has to be applied so
we need to explicitly test for basechar, baselig and basemark entries.</p>
--ldx]]--

function handlers.gpos_mark2base(head,start,dataset,sequence,markanchors,rlmode)
    local markchar = getchar(start)
    if marks[markchar] then
        local base = getprev(start) -- [glyph] [start=mark]
        if base then
            local basechar = ischar(base,currentfont)
            if basechar then
                if marks[basechar] then
                    while base do
                        base = getprev(base)
                        if base then
                            basechar = ischar(base,currentfont)
                            if basechar then
                                if not marks[basechar] then
                                    break
                                end
                            else
                                if trace_bugs then
                                    logwarning("%s: no base for mark %s, case %i",pref(dataset,sequence),gref(markchar),1)
                                end
                                return head, start, false
                            end
                        else
                            if trace_bugs then
                                logwarning("%s: no base for mark %s, case %i",pref(dataset,sequence),gref(markchar),2)
                            end
                            return head, start, false
                        end
                    end
                end
                local ba = markanchors[1][basechar]
                if ba then
                    local ma = markanchors[2]
                    local dx, dy, bound = setmark(start,base,factor,rlmode,ba,ma,characters[basechar],false,checkmarks)
                    if trace_marks then
                        logprocess("%s, anchor %s, bound %s: anchoring mark %s to basechar %s => (%p,%p)",
                            pref(dataset,sequence),anchor,bound,gref(markchar),gref(basechar),dx,dy)
                    end
                    return head, start, true
                end
            elseif trace_bugs then
                logwarning("%s: nothing preceding, case %i",pref(dataset,sequence),1)
            end
        elseif trace_bugs then
            logwarning("%s: nothing preceding, case %i",pref(dataset,sequence),2)
        end
    elseif trace_bugs then
        logwarning("%s: mark %s is no mark",pref(dataset,sequence),gref(markchar))
    end
    return head, start, false
end

function handlers.gpos_mark2ligature(head,start,dataset,sequence,markanchors,rlmode)
    local markchar = getchar(start)
    if marks[markchar] then
        local base = getprev(start) -- [glyph] [optional marks] [start=mark]
        if base then
            local basechar = ischar(base,currentfont)
            if basechar then
                if marks[basechar] then
                    while base do
                        base = getprev(base)
                        if base then
                            basechar = ischar(base,currentfont)
                            if basechar then
                                if not marks[basechar] then
                                    break
                                end
                            else
                                if trace_bugs then
                                    logwarning("%s: no base for mark %s, case %i",pref(dataset,sequence),gref(markchar),1)
                                end
                                return head, start, false
                            end
                        else
                            if trace_bugs then
                                logwarning("%s: no base for mark %s, case %i",pref(dataset,sequence),gref(markchar),2)
                            end
                            return head, start, false
                        end
                    end
                end
                local ba = markanchors[1][basechar]
                if ba then
                    local ma = markanchors[2]
                    if ma then
                        local index = getligaindex(start)
                        ba = ba[index]
                        if ba then
                            local dx, dy, bound = setmark(start,base,factor,rlmode,ba,ma,characters[basechar],false,checkmarks)
                            if trace_marks then
                                logprocess("%s, anchor %s, index %s, bound %s: anchoring mark %s to baselig %s at index %s => (%p,%p)",
                                    pref(dataset,sequence),anchor,index,bound,gref(markchar),gref(basechar),index,dx,dy)
                            end
                            return head, start, true
                        else
                            if trace_bugs then
                                logwarning("%s: no matching anchors for mark %s and baselig %s with index %a",pref(dataset,sequence),gref(markchar),gref(basechar),index)
                            end
                        end
                    end
                elseif trace_bugs then
                --  logwarning("%s: char %s is missing in font",pref(dataset,sequence),gref(basechar))
                    onetimemessage(currentfont,basechar,"no base anchors",report_fonts)
                end
            elseif trace_bugs then
                logwarning("%s: prev node is no char, case %i",pref(dataset,sequence),1)
            end
        elseif trace_bugs then
            logwarning("%s: prev node is no char, case %i",pref(dataset,sequence),2)
        end
    elseif trace_bugs then
        logwarning("%s: mark %s is no mark",pref(dataset,sequence),gref(markchar))
    end
    return head, start, false
end

function handlers.gpos_mark2mark(head,start,dataset,sequence,markanchors,rlmode)
    local markchar = getchar(start)
    if marks[markchar] then
        local base = getprev(start) -- [glyph] [basemark] [start=mark]
        local slc = getligaindex(start)
        if slc then -- a rather messy loop ... needs checking with husayni
            while base do
                local blc = getligaindex(base)
                if blc and blc ~= slc then
                    base = getprev(base)
                else
                    break
                end
            end
        end
        if base then
            local basechar = ischar(base,currentfont)
            if basechar then -- subtype test can go
                local ba = markanchors[1][basechar] -- slot 1 has been made copy of the class hash
                if ba then
                    local ma = markanchors[2]
                    local dx, dy, bound = setmark(start,base,factor,rlmode,ba,ma,characters[basechar],true,checkmarks)
                    if trace_marks then
                        logprocess("%s, anchor %s, bound %s: anchoring mark %s to basemark %s => (%p,%p)",
                            pref(dataset,sequence),anchor,bound,gref(markchar),gref(basechar),dx,dy)
                    end
                    return head, start, true
                end
            end
        end
    elseif trace_bugs then
        logwarning("%s: mark %s is no mark",pref(dataset,sequence),gref(markchar))
    end
    return head, start, false
end

function handlers.gpos_cursive(head,start,dataset,sequence,exitanchors,rlmode,step,i) -- to be checked
    local startchar = getchar(start)
    if marks[startchar] then
        if trace_cursive then
            logprocess("%s: ignoring cursive for mark %s",pref(dataset,sequence),gref(startchar))
        end
    else
        local nxt = getnext(start)
        while nxt do
            local nextchar = ischar(nxt,currentfont)
            if not nextchar then
                break
            elseif marks[nextchar] then
                -- should not happen (maybe warning)
                nxt = getnext(nxt)
            else
                local exit = exitanchors[3]
                if exit then
                    local entry = exitanchors[1][nextchar]
                    if entry then
                        entry = entry[2]
                        if entry then
                            local dx, dy, bound = setcursive(start,nxt,factor,rlmode,exit,entry,characters[startchar],characters[nextchar])
                            if trace_cursive then
                                logprocess("%s: moving %s to %s cursive (%p,%p) using anchor %s and bound %s in %s mode",pref(dataset,sequence),gref(startchar),gref(nextchar),dx,dy,anchor,bound,mref(rlmode))
                            end
                            return head, start, true
                        end
                    end
                end
                break
            end
        end
    end
    return head, start, false
end

--[[ldx--
<p>I will implement multiple chain replacements once I run into a font that uses
it. It's not that complex to handle.</p>
--ldx]]--

local chainprocs = { }

local function logprocess(...)
    if trace_steps then
        registermessage(...)
    end
    report_subchain(...)
end

local logwarning = report_subchain

local function logprocess(...)
    if trace_steps then
        registermessage(...)
    end
    report_chain(...)
end

local logwarning = report_chain

-- We could share functions but that would lead to extra function calls with many
-- arguments, redundant tests and confusing messages.

-- The reversesub is a special case, which is why we need to store the replacements
-- in a bit weird way. There is no lookup and the replacement comes from the lookup
-- itself. It is meant mostly for dealing with Urdu.

local function reversesub(head,start,stop,dataset,sequence,replacements,rlmode)
    local char        = getchar(start)
    local replacement = replacements[char]
    if replacement then
        if trace_singles then
            logprocess("%s: single reverse replacement of %s by %s",cref(dataset,sequence),gref(char),gref(replacement))
        end
        resetinjection(start)
        setchar(start,replacement)
        return head, start, true
    else
        return head, start, false
    end
end


chainprocs.reversesub = reversesub

--[[ldx--
<p>This chain stuff is somewhat tricky since we can have a sequence of actions to be
applied: single, alternate, multiple or ligature where ligature can be an invalid
one in the sense that it will replace multiple by one but not neccessary one that
looks like the combination (i.e. it is the counterpart of multiple then). For
example, the following is valid:</p>

<typing>
<line>xxxabcdexxx [single a->A][multiple b->BCD][ligature cde->E] xxxABCDExxx</line>
</typing>

<p>Therefore we we don't really do the replacement here already unless we have the
single lookup case. The efficiency of the replacements can be improved by deleting
as less as needed but that would also make the code even more messy.</p>
--ldx]]--

--[[ldx--
<p>Here we replace start by a single variant.</p>
--ldx]]--

-- To be done (example needed): what if > 1 steps

-- this is messy: do we need this disc checking also in alternaties?

local function reportzerosteps(dataset,sequence)
    logwarning("%s: no steps",cref(dataset,sequence))
end

local function reportmoresteps(dataset,sequence)
    logwarning("%s: more than 1 step",cref(dataset,sequence))
end

-- local function reportbadsteps(dataset,sequence)
--     logwarning("%s: bad step, no proper return values",cref(dataset,sequence))
-- end

function chainprocs.gsub_single(head,start,stop,dataset,sequence,currentlookup,chainindex)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local current = start
        local mapping = steps[1].coverage
        while current do
            local currentchar = ischar(current)
            if currentchar then
                local replacement = mapping[currentchar]
                if not replacement or replacement == "" then
                    if trace_bugs then
                        logwarning("%s: no single for %s",cref(dataset,sequence,chainindex),gref(currentchar))
                    end
                else
                    if trace_singles then
                        logprocess("%s: replacing single %s by %s",cref(dataset,sequence,chainindex),gref(currentchar),gref(replacement))
                    end
                    resetinjection(current)
                    setchar(current,replacement)
                end
                return head, start, true
            elseif currentchar == false then
                -- can't happen
                break
            elseif current == stop then
                break
            else
                current = getnext(current)
            end
        end
    end
    return head, start, false
end

--[[ldx--
<p>Here we replace start by a sequence of new glyphs.</p>
--ldx]]--

function chainprocs.gsub_multiple(head,start,stop,dataset,sequence,currentlookup)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local startchar   = getchar(start)
        local replacement = steps[1].coverage[startchar]
        if not replacement or replacement == "" then
            if trace_bugs then
                logwarning("%s: no multiple for %s",cref(dataset,sequence),gref(startchar))
            end
        else
            if trace_multiples then
                logprocess("%s: replacing %s by multiple characters %s",cref(dataset,sequence),gref(startchar),gref(replacement))
            end
            return multiple_glyphs(head,start,replacement,sequence.flags[1],dataset[1])
        end
    end
    return head, start, false
end

--[[ldx--
<p>Here we replace start by new glyph. First we delete the rest of the match.</p>
--ldx]]--

-- char_1 mark_1 -> char_x mark_1 (ignore marks)
-- char_1 mark_1 -> char_x

-- to be checked: do we always have just one glyph?
-- we can also have alternates for marks
-- marks come last anyway
-- are there cases where we need to delete the mark

function chainprocs.gsub_alternate(head,start,stop,dataset,sequence,currentlookup)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local kind    = dataset[4]
        local what    = dataset[1]
        local value   = what == true and tfmdata.shared.features[kind] or what -- todo: optimize in ctx
        local current = start
        local mapping = steps[1].coverage
        while current do
            local currentchar = ischar(current)
            if currentchar then
                local alternatives = mapping[currentchar]
                if alternatives then
                    local choice, comment = get_alternative_glyph(current,alternatives,value)
                    if choice then
                        if trace_alternatives then
                            logprocess("%s: replacing %s by alternative %a to %s, %s",cref(dataset,sequence),gref(char),choice,gref(choice),comment)
                        end
                        resetinjection(start)
                        setchar(start,choice)
                    else
                        if trace_alternatives then
                            logwarning("%s: no variant %a for %s, %s",cref(dataset,sequence),value,gref(char),comment)
                        end
                    end
                end
                return head, start, true
            elseif currentchar == false then
                -- can't happen
                break
            elseif current == stop then
                break
            else
                current = getnext(current)
            end
        end
    end
    return head, start, false
end

--[[ldx--
<p>When we replace ligatures we use a helper that handles the marks. I might change
this function (move code inline and handle the marks by a separate function). We
assume rather stupid ligatures (no complex disc nodes).</p>
--ldx]]--

function chainprocs.gsub_ligature(head,start,stop,dataset,sequence,currentlookup,chainindex)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local startchar = getchar(start)
        local ligatures = steps[1].coverage[startchar]
        if not ligatures then
            if trace_bugs then
                logwarning("%s: no ligatures starting with %s",cref(dataset,sequence,chainindex),gref(startchar))
            end
        else
            local current         = getnext(start)
            local discfound       = false
            local last            = stop
            local nofreplacements = 1
            local skipmark        = currentlookup.flags[1] -- sequence.flags?
            while current do
                -- todo: ischar ... can there really be disc nodes here?
                local id = getid(current)
                if id == disc_code then
                    if not discfound then
                        discfound = current
                    end
                    if current == stop then
                        break -- okay? or before the disc
                    else
                        current = getnext(current)
                    end
                else
                    local schar = getchar(current)
                    if skipmark and marks[schar] then -- marks
                        -- if current == stop then -- maybe add this
                        --     break
                        -- else
                            current = getnext(current)
                        -- end
                    else
                        local lg = ligatures[schar]
                        if lg then
                            ligatures       = lg
                            last            = current
                            nofreplacements = nofreplacements + 1
                            if current == stop then
                                break
                            else
                                current = getnext(current)
                            end
                        else
                            break
                        end
                    end
                end
            end
            local ligature = ligatures.ligature
            if ligature then
                if chainindex then
                    stop = last
                end
                if trace_ligatures then
                    if start == stop then
                        logprocess("%s: replacing character %s by ligature %s case 3",cref(dataset,sequence,chainindex),gref(startchar),gref(ligature))
                    else
                        logprocess("%s: replacing character %s upto %s by ligature %s case 4",cref(dataset,sequence,chainindex),gref(startchar),gref(getchar(stop)),gref(ligature))
                    end
                end
                head, start = toligature(head,start,stop,ligature,dataset,sequence,skipmark,discfound)
                return head, start, true, nofreplacements, discfound
            elseif trace_bugs then
                if start == stop then
                    logwarning("%s: replacing character %s by ligature fails",cref(dataset,sequence,chainindex),gref(startchar))
                else
                    logwarning("%s: replacing character %s upto %s by ligature fails",cref(dataset,sequence,chainindex),gref(startchar),gref(getchar(stop)))
                end
            end
        end
    end
    return head, start, false, 0, false
end

function chainprocs.gpos_single(head,start,stop,dataset,sequence,currentlookup,rlmode,chainindex)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local startchar = getchar(start)
        local step      = steps[1]
        local kerns     = step.coverage[startchar]
        if not kerns then
            -- skip
        elseif step.format == "pair" then
            local dx, dy, w, h = setpair(start,factor,rlmode,sequence.flags[4],kerns) -- currentlookup.flags ?
            if trace_kerns then
                logprocess("%s: shifting single %s by (%p,%p) and correction (%p,%p)",cref(dataset,sequence),gref(startchar),dx,dy,w,h)
            end
        else -- needs checking .. maybe no kerns format for single
            local k = setkern(start,factor,rlmode,kerns,injection)
            if trace_kerns then
                logprocess("%s: shifting single %s by %p",cref(dataset,sequence),gref(startchar),k)
            end
        end
    end
    return head, start, false
end

function chainprocs.gpos_pair(head,start,stop,dataset,sequence,currentlookup,rlmode,chainindex) -- todo: injections ?
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local snext = getnext(start)
        if snext then
            local startchar = getchar(start)
            local step      = steps[1]
            local kerns     = step.coverage[startchar] -- always 1 step
            if kerns then
                local prev = start
                while snext do
                    local nextchar = ischar(snext,currentfont)
                    if not nextchar then
                        break
                    end
                    local krn = kerns[nextchar]
                    if not krn and marks[nextchar] then
                        prev = snext
                        snext = getnext(snext)
                    elseif not krn then
                        break
                    elseif step.format == "pair" then
                        local a, b = krn[1], krn[2]
                        if optimizekerns then
                            -- this permits a mixed table, but we could also decide to optimize this
                            -- in the loader and use format 'kern'
                            if not b and a[1] == 0 and a[2] == 0 and a[4] == 0 then
                                local k = setkern(snext,factor,rlmode,a[3],"injections")
                                if trace_kerns then
                                    logprocess("%s: shifting single %s by %p",cref(dataset,sequence),gref(startchar),k)
                                end
                                return head, start, true
                            end
                        end
                        if a and #a > 0 then
                            local startchar = getchar(start)
                            local x, y, w, h = setpair(start,factor,rlmode,sequence.flags[4],a,"injections") -- currentlookups flags?
                            if trace_kerns then
                                logprocess("%s: shifting first of pair %s and %s by (%p,%p) and correction (%p,%p)",cref(dataset,sequence),gref(startchar),gref(nextchar),x,y,w,h)
                            end
                        end
                        if b and #b > 0 then
                            local startchar = getchar(start)
                            local x, y, w, h = setpair(snext,factor,rlmode,sequence.flags[4],b,"injections")
                            if trace_kerns then
                                logprocess("%s: shifting second of pair %s and %s by (%p,%p) and correction (%p,%p)",cref(dataset,sequence),gref(startchar),gref(nextchar),x,y,w,h)
                            end
                        end
                        return head, start, true
                    elseif krn ~= 0 then
                        local k = setkern(snext,factor,rlmode,krn)
                        if trace_kerns then
                            logprocess("%s: inserting kern %s between %s and %s",cref(dataset,sequence),k,gref(getchar(prev)),gref(nextchar))
                        end
                        return head, start, true
                    else
                        break
                    end
                end
            end
        end
    end
    return head, start, false
end

function chainprocs.gpos_mark2base(head,start,stop,dataset,sequence,currentlookup,rlmode)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local markchar = getchar(start)
        if marks[markchar] then
            local markanchors = steps[1].coverage[markchar] -- always 1 step
            if markanchors then
                local base = getprev(start) -- [glyph] [start=mark]
                if base then
                    local basechar = ischar(base,currentfont)
                    if basechar then
                        if marks[basechar] then
                            while base do
                                base = getprev(base)
                                if base then
                                    local basechar = ischar(base,currentfont)
                                    if basechar then
                                        if not marks[basechar] then
                                            break
                                        end
                                    else
                                        if trace_bugs then
                                            logwarning("%s: no base for mark %s, case %i",pref(dataset,sequence),gref(markchar),1)
                                        end
                                        return head, start, false
                                    end
                                else
                                    if trace_bugs then
                                        logwarning("%s: no base for mark %s, case %i",pref(dataset,sequence),gref(markchar),2)
                                    end
                                    return head, start, false
                                end
                            end
                        end
                        local ba = markanchors[1][basechar]
                        if ba then
                            local ma = markanchors[2]
                            if ma then
                                local dx, dy, bound = setmark(start,base,factor,rlmode,ba,ma,characters[basechar],false,checkmarks)
                                if trace_marks then
                                    logprocess("%s, anchor %s, bound %s: anchoring mark %s to basechar %s => (%p,%p)",
                                        cref(dataset,sequence),anchor,bound,gref(markchar),gref(basechar),dx,dy)
                                end
                                return head, start, true
                            end
                        end
                    elseif trace_bugs then
                        logwarning("%s: prev node is no char, case %i",cref(dataset,sequence),1)
                    end
                elseif trace_bugs then
                    logwarning("%s: prev node is no char, case %i",cref(dataset,sequence),2)
                end
            elseif trace_bugs then
                logwarning("%s: mark %s has no anchors",cref(dataset,sequence),gref(markchar))
            end
        elseif trace_bugs then
            logwarning("%s: mark %s is no mark",cref(dataset,sequence),gref(markchar))
        end
    end
    return head, start, false
end

function chainprocs.gpos_mark2ligature(head,start,stop,dataset,sequence,currentlookup,rlmode)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local markchar = getchar(start)
        if marks[markchar] then
            local markanchors = steps[1].coverage[markchar] -- always 1 step
            if markanchors then
                local base = getprev(start) -- [glyph] [optional marks] [start=mark]
                if base then
                    local basechar = ischar(base,currentfont)
                    if basechar then
                        if marks[basechar] then
                            while base do
                                base = getprev(base)
                                if base then
                                    local basechar = ischar(base,currentfont)
                                    if basechar then
                                        if not marks[basechar] then
                                            break
                                        end
                                    else
                                        if trace_bugs then
                                            logwarning("%s: no base for mark %s, case %i",cref(dataset,sequence),markchar,1)
                                        end
                                        return head, start, false
                                    end
                                else
                                    if trace_bugs then
                                        logwarning("%s: no base for mark %s, case %i",cref(dataset,sequence),markchar,2)
                                    end
                                    return head, start, false
                                end
                            end
                        end
                        local ba = markanchors[1][basechar]
                        if ba then
                            local ma = markanchors[2]
                            if ma then
                                local index = getligaindex(start)
                                ba = ba[index]
                                if ba then
                                    local dx, dy, bound = setmark(start,base,factor,rlmode,ba,ma,characters[basechar],false,checkmarks)
                                    if trace_marks then
                                        logprocess("%s, anchor %s, bound %s: anchoring mark %s to baselig %s at index %s => (%p,%p)",
                                            cref(dataset,sequence),anchor,a or bound,gref(markchar),gref(basechar),index,dx,dy)
                                    end
                                    return head, start, true
                                end
                            end
                        end
                    elseif trace_bugs then
                        logwarning("%s, prev node is no char, case %i",cref(dataset,sequence),1)
                    end
                elseif trace_bugs then
                    logwarning("%s, prev node is no char, case %i",cref(dataset,sequence),2)
                end
            elseif trace_bugs then
                logwarning("%s, mark %s has no anchors",cref(dataset,sequence),gref(markchar))
            end
        elseif trace_bugs then
            logwarning("%s, mark %s is no mark",cref(dataset,sequence),gref(markchar))
        end
    end
    return head, start, false
end

function chainprocs.gpos_mark2mark(head,start,stop,dataset,sequence,currentlookup,rlmode)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local markchar = getchar(start)
        if marks[markchar] then
            local markanchors = steps[1].coverage[markchar] -- always 1 step
            if markanchors then
                local base = getprev(start) -- [glyph] [basemark] [start=mark]
                local slc = getligaindex(start)
                if slc then -- a rather messy loop ... needs checking with husayni
                    while base do
                        local blc = getligaindex(base)
                        if blc and blc ~= slc then
                            base = getprev(base)
                        else
                            break
                        end
                    end
                end
                if base then -- subtype test can go
                    local basechar = ischar(base,currentfont)
                    if basechar then
                        local ba = markanchors[1][basechar]
                        if ba then
                            local ma = markanchors[2]
                            if ma then
                                local dx, dy, bound = setmark(start,base,factor,rlmode,ba,ma,characters[basechar],true,checkmarks)
                                if trace_marks then
                                    logprocess("%s, anchor %s, bound %s: anchoring mark %s to basemark %s => (%p,%p)",
                                        cref(dataset,sequence),anchor,bound,gref(markchar),gref(basechar),dx,dy)
                                end
                                return head, start, true
                            end
                        end
                    elseif trace_bugs then
                        logwarning("%s: prev node is no mark, case %i",cref(dataset,sequence),1)
                    end
                elseif trace_bugs then
                    logwarning("%s: prev node is no mark, case %i",cref(dataset,sequence),2)
                end
            elseif trace_bugs then
                logwarning("%s: mark %s has no anchors",cref(dataset,sequence),gref(markchar))
            end
        elseif trace_bugs then
            logwarning("%s: mark %s is no mark",cref(dataset,sequence),gref(markchar))
        end
    end
    return head, start, false
end

function chainprocs.gpos_cursive(head,start,stop,dataset,sequence,currentlookup,rlmode)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    if nofsteps == 0 then
        reportzerosteps(dataset,sequence)
    else
        local startchar   = getchar(start)
        local exitanchors = steps[1].coverage[startchar] -- always 1 step
        if exitanchors then
            if marks[startchar] then
                if trace_cursive then
                    logprocess("%s: ignoring cursive for mark %s",pref(dataset,sequence),gref(startchar))
                end
            else
                local nxt = getnext(start)
                while nxt do
                    local nextchar = ischar(nxt,currentfont)
                    if not nextchar then
                        break
                    elseif marks[nextchar] then
                        -- should not happen (maybe warning)
                        nxt = getnext(nxt)
                    else
                        local exit = exitanchors[3]
                        if exit then
                            local entry = exitanchors[1][nextchar]
                            if entry then
                                entry = entry[2]
                                if entry then
                                    local dx, dy, bound = setcursive(start,nxt,factor,rlmode,exit,entry,characters[startchar],characters[nextchar])
                                    if trace_cursive then
                                        logprocess("%s: moving %s to %s cursive (%p,%p) using anchor %s and bound %s in %s mode",pref(dataset,sequence),gref(startchar),gref(nextchar),dx,dy,anchor,bound,mref(rlmode))
                                    end
                                    return head, start, true
                                end
                            end
                        elseif trace_bugs then
                            onetimemessage(currentfont,startchar,"no entry anchors",report_fonts)
                        end
                        break
                    end
                end
            end
        elseif trace_cursive and trace_details then
            logprocess("%s, cursive %s is already done",pref(dataset,sequence),gref(getchar(start)),alreadydone)
        end
    end
    return head, start, false
end

-- what pointer to return, spec says stop
-- to be discussed ... is bidi changer a space?
-- elseif char == zwnj and sequence[n][32] then -- brrr

local function show_skip(dataset,sequence,char,ck,class)
    logwarning("%s: skipping char %s, class %a, rule %a, lookuptype %a",cref(dataset,sequence),gref(char),class,ck[1],ck[8] or ck[2])
end

-- A previous version had disc collapsing code in the (single sub) handler plus some
-- checking in the main loop, but that left the pre/post sequences undone. The best
-- solution is to add some checking there and backtrack when a replace/post matches
-- but it takes a bit of work to figure out an efficient way (this is what the sweep*
-- names refer to). I might look into that variant one day again as it can replace
-- some other code too. In that approach we can have a special version for gub and pos
-- which gains some speed. This method does the test and passes info to the handlers
-- (sweepnode, sweepmode, sweepprev, sweepnext, etc). Here collapsing is handled in the
-- main loop which also makes code elsewhere simpler (i.e. no need for the other special
-- runners and disc code in ligature building). I also experimented with pushing preceding
-- glyphs sequences in the replace/pre fields beforehand which saves checking afterwards
-- but at the cost of duplicate glyphs (memory) but it's too much overhead (runtime).
--
-- In the meantime Kai had moved the code from the single chain into a more general handler
-- and this one (renamed to chaindisk) is used now. I optimized the code a bit and brought
-- it in sycn with the other code. Hopefully I didn't introduce errors. Note: this somewhat
-- complex approach is meant for fonts that implement (for instance) ligatures by character
-- replacement which to some extend is not that suitable for hyphenation. I also use some
-- helpers. This method passes some states but reparses the list. There is room for a bit of
-- speed up but that will be done in the context version. (In fact a partial rewrite of all
-- code can bring some more efficientry.)
--
-- I didn't test it with extremes but successive disc nodes still can give issues but in
-- order to handle that we need more complex code which also slows down even more. The main
-- loop variant could deal with that: test, collapse, backtrack.

local new_kern = nuts.pool.kern

local function checked(head)
    local current = head
    while current do
        if getid(current) == glue_code then
            local kern = new_kern(getwidth(current))
            if head == current then
                local next = getnext(current)
                if next then
                    setlink(kern,next)
                end
                flush_node(current)
                head    = kern
                current = next
            else
                local prev, next = getboth(current)
                setlink(prev,kern,next)
                flush_node(current)
                current = next
            end
        else
            current = getnext(current)
        end
    end
    return head
end

local function setdiscchecked(d,pre,post,replace)
    if pre     then pre     = checked(pre)     end
    if post    then post    = checked(post)    end
    if replace then replace = checked(replace) end
    setdisc(d,pre,post,replace)
end

local noflags = { false, false, false, false }

local function chainrun(head,start,last,dataset,sequence,rlmode,ck,skipped)

    local size         = ck[5] - ck[4] + 1
    local flags        = sequence.flags or noflags
    local done         = false
    local skipmark     = flags[1]
    local chainlookups = ck[6]

    -- current match
    if chainlookups then
        local nofchainlookups = #chainlookups
        -- Lookups can be like { 1, false, 3 } or { false, 2 } or basically anything and
        -- #lookups can be less than #current
        if size == 1 then
         -- if nofchainlookups > size then
         --     -- bad rules
         -- end
            local chainlookup = chainlookups[1]
            for j=1,#chainlookup do
                local chainstep = chainlookup[j]
                local chainkind = chainstep.type
                local chainproc = chainprocs[chainkind]
                if chainproc then
                    local ok
                    head, start, ok = chainproc(head,start,last,dataset,sequence,chainstep,rlmode,1)
                    if ok then
                        done = true
                    end
                else
                    logprocess("%s: %s is not yet supported (1)",cref(dataset,sequence),chainkind)
                end
            end
         else
            -- See LookupType 5: Contextual Substitution Subtable. Now it becomes messy. The
            -- easiest case is where #current maps on #lookups i.e. one-to-one. But what if
            -- we have a ligature. Cf the spec we then need to advance one character but we
            -- really need to test it as there are fonts out there that are fuzzy and have
            -- too many lookups:
            --
            -- U+1105 U+119E U+1105 U+119E : sourcehansansklight: script=hang ccmp=yes
            --
            -- Even worse are these family emoji shapes as they can have multiple lookups
            -- per slot (probably only for gpos).
            local i = 1
            while start do
                if skipped then
                    while start do
                        local char  = getchar(start)
                        local class = classes[char]
                        if class then
                            if class == skipmark or class == skipligature or class == skipbase or (markclass and class == "mark" and not markclass[char]) then
                                start = getnext(start)
                            else
                                break
                            end
                        else
                            break
                        end
                    end
                end
                local chainlookup = chainlookups[i]
                if chainlookup then
                    for j=1,#chainlookup do
                        local chainstep = chainlookup[j]
                        local chainkind = chainstep.type
                        local chainproc = chainprocs[chainkind]
                        if chainproc then
                            local ok, n
                            head, start, ok, n = chainproc(head,start,last,dataset,sequence,chainstep,rlmode,i)
                            -- messy since last can be changed !
                            if ok then
                                done = true
                                if n and n > 1 and i + n > nofchainlookups then
                                    -- this is a safeguard, we just ignore the rest of the lookups
                                    break
                                end
                            end
                        else
                            -- actually an error
                            logprocess("%s: %s is not yet supported (2)",cref(dataset,sequence),chainkind)
                        end
                    end
                end
                i = i + 1
                if i > size or not start then
                    break
                elseif start then
                    start = getnext(start)
                end
            end
        end
    else
        -- todo: needs checking for holes in the replacements
        local replacements = ck[7]
        if replacements then
            head, start, done = reversesub(head,start,last,dataset,sequence,replacements,rlmode)
        else
            done = true
            if trace_contexts then
                logprocess("%s: skipping match",cref(dataset,sequence))
            end
        end
    end
    return head, start, done
end

local function chaindisk(head,start,dataset,sequence,rlmode,ck,skipped)

    if not start then
        return head, start, false
    end

    local startishead   = start == head
    local seq           = ck[3]
    local f             = ck[4]
    local l             = ck[5]
    local s             = #seq
    local done          = false
    local sweepnode     = sweepnode
    local sweeptype     = sweeptype
    local sweepoverflow = false
    local keepdisc      = not sweepnode
    local lookaheaddisc = nil
    local backtrackdisc = nil
    local current       = start
    local last          = start
    local prev          = getprev(start)
    local hasglue       = false

    -- fishy: so we can overflow and then go on in the sweep?
    -- todo : id can also be glue_code as we checked spaces

    local i = f
    while i <= l do
        local id = getid(current)
        if id == glyph_code then
            i       = i + 1
            last    = current
            current = getnext(current)
        elseif id == glue_code then
            i       = i + 1
            last    = current
            current = getnext(current)
            hasglue = true
        elseif id == disc_code then
            if keepdisc then
                keepdisc      = false
                lookaheaddisc = current
                local replace = getfield(current,"replace")
                if not replace then
                    sweepoverflow = true
                    sweepnode     = current
                    current       = getnext(current)
                else
                    while replace and i <= l do
                        if getid(replace) == glyph_code then
                            i = i + 1
                        end
                        replace = getnext(replace)
                    end
                    current = getnext(replace)
                end
                last = current
            else
                head, current = flattendisk(head,current)
            end
        else
            last    = current
            current = getnext(current)
        end
        if current then
            -- go on
        elseif sweepoverflow then
            -- we already are folling up on sweepnode
            break
        elseif sweeptype == "post" or sweeptype == "replace" then
            current = getnext(sweepnode)
            if current then
                sweeptype     = nil
                sweepoverflow = true
            else
                break
            end
        else
            break -- added
        end
    end

    if sweepoverflow then
        local prev = current and getprev(current)
        if not current or prev ~= sweepnode then
            local head = getnext(sweepnode)
            local tail = nil
            if prev then
                tail = prev
                setprev(current,sweepnode)
            else
                tail = find_node_tail(head)
            end
            setnext(sweepnode,current)
            setprev(head)
            setnext(tail)
            appenddisc(sweepnode,head)
        end
    end

    if l < s then
        local i = l
        local t = sweeptype == "post" or sweeptype == "replace"
        while current and i < s do
            local id = getid(current)
            if id == glyph_code then
                i       = i + 1
                current = getnext(current)
            elseif id == glue_code then
                i       = i + 1
                current = getnext(current)
                hasglue = true
            elseif id == disc_code then
                if keepdisc then
                    keepdisc = false
                    if notmatchpre[current] ~= notmatchreplace[current] then
                        lookaheaddisc = current
                    end
                    -- we assume a simple text only replace (we could use nuts.count)
                    local replace = getfield(current,"replace")
                    while replace and i < s do
                        if getid(replace) == glyph_code then
                            i = i + 1
                        end
                        replace = getnext(replace)
                    end
                    current = getnext(current)
                elseif notmatchpre[current] ~= notmatchreplace[current] then
                    head, current = flattendisk(head,current)
                else
                    current = getnext(current) -- HH
                end
            else
                current = getnext(current)
            end
            if not current and t then
                current = getnext(sweepnode)
                if current then
                    sweeptype = nil
                end
            end
        end
    end

    if f > 1 then
        local current = prev
        local i       = f
        local t       = sweeptype == "pre" or sweeptype == "replace"
        if not current and t and current == checkdisk then
            current = getprev(sweepnode)
        end
        while current and i > 1 do -- missing getprev added / moved outside
            local id = getid(current)
            if id == glyph_code then
                i = i - 1
            elseif id == glue_code then
                i       = i - 1
                hasglue = true
            elseif id == disc_code then
                if keepdisc then
                    keepdisc = false
                    if notmatchpost[current] ~= notmatchreplace[current] then
                        backtrackdisc = current
                    end
                    -- we assume a simple text only replace (we could use nuts.count)
                    local replace = getfield(current,"replace")
                    while replace and i > 1 do
                        if getid(replace) == glyph_code then
                            i = i - 1
                        end
                        replace = getnext(replace)
                    end
                elseif notmatchpost[current] ~= notmatchreplace[current] then
                    head, current = flattendisk(head,current)
                end
            end
            current = getprev(current)
            if t and current == checkdisk then
                current = getprev(sweepnode)
            end
        end
    end

    local done = false

    if lookaheaddisc then

        local cf            = start
        local cl            = getprev(lookaheaddisc)
        local cprev         = getprev(start)
        local insertedmarks = 0

        while cprev do
            local char = ischar(cf,currentfont)
            if char and marks[char] then
                insertedmarks = insertedmarks + 1
                cf            = cprev
                startishead   = cf == head
                cprev         = getprev(cprev)
            else
                break
            end
        end
        setlink(cprev,lookaheaddisc)
        setprev(cf)
        setnext(cl)
        if startishead then
            head = lookaheaddisc
        end
        local pre, post, replace = getdisc(lookaheaddisc)
        local new  = copy_node_list(cf)
        local cnew = new
        if pre then
            setlink(find_node_tail(cf),pre)
        end
        if replace then
            local tail = find_node_tail(new)
            setlink(tail,replace)
        end
        for i=1,insertedmarks do
            cnew = getnext(cnew)
        end
        cl = start
        local clast = cnew
        for i=f,l do
            cl    = getnext(cl)
            clast = getnext(clast)
        end
        if not notmatchpre[lookaheaddisc] then
            local ok = false
            cf, start, ok = chainrun(cf,start,cl,dataset,sequence,rlmode,ck,skipped)
            if ok then
                done = true
            end
        end
        if not notmatchreplace[lookaheaddisc] then
            local ok = false
            new, cnew, ok = chainrun(new,cnew,clast,dataset,sequence,rlmode,ck,skipped)
            if ok then
                done = true
            end
        end
        if hasglue then
            setdiscchecked(lookaheaddisc,cf,post,new)
        else
            setdisc(lookaheaddisc,cf,post,new)
        end
        start          = getprev(lookaheaddisc)
        sweephead[cf]  = getnext(clast)
        sweephead[new] = getnext(cl)

    elseif backtrackdisc then

        local cf            = getnext(backtrackdisc)
        local cl            = start
        local cnext         = getnext(start)
        local insertedmarks = 0

        while cnext do
            local char = ischar(cnext,currentfont)
            if char and marks[char] then
                insertedmarks = insertedmarks + 1
                cl            = cnext
                cnext         = getnext(cnext)
            else
                break
            end
        end
        if cnext then
            setprev(cnext,backtrackdisc)
        end
        setnext(backtrackdisc,cnext)
        setprev(cf)
        setnext(cl)
        local pre, post, replace, pretail, posttail, replacetail = getdisc(backtrackdisc,true)
        local new  = copy_node_list(cf)
        local cnew = find_node_tail(new)
        for i=1,insertedmarks do
            cnew = getprev(cnew)
        end
        local clast = cnew
        for i=f,l do
            clast = getnext(clast)
        end
        if not notmatchpost[backtrackdisc] then
            local ok = false
            cf, start, ok = chainrun(cf,start,last,dataset,sequence,rlmode,ck,skipped)
            if ok then
                done = true
            end
        end
        if not notmatchreplace[backtrackdisc] then
            local ok = false
            new, cnew, ok = chainrun(new,cnew,clast,dataset,sequence,rlmode,ck,skipped)
            if ok then
                done = true
            end
        end
        if post then
            setlink(posttail,cf)
        else
            post = cf
        end
        if replace then
            setlink(replacetail,new)
        else
            replace = new
        end
        if hasglue then
            setdiscchecked(backtrackdisc,pre,post,replace)
        else
            setdisc(backtrackdisc,pre,post,replace)
        end
        start              = getprev(backtrackdisc)
        sweephead[post]    = getnext(clast)
        sweephead[replace] = getnext(last)

    else

        local ok = false
        head, start, ok = chainrun(head,start,last,dataset,sequence,rlmode,ck,skipped)
        if ok then
            done = true
        end

    end

    return head, start, done
end

local function chaintrac(head,start,dataset,sequence,rlmode,ck,skipped)
    local rule       = ck[1]
    local lookuptype = ck[8] or ck[2]
    local nofseq     = #ck[3]
    local first      = ck[4]
    local last       = ck[5]
    local char       = getchar(start)
    logwarning("%s: rule %s matches at char %s for (%s,%s,%s) chars, lookuptype %a",
        cref(dataset,sequence),rule,gref(char),first-1,last-first+1,nofseq-last,lookuptype)
end

local function handle_contextchain(head,start,dataset,sequence,contexts,rlmode)
    local sweepnode    = sweepnode
    local sweeptype    = sweeptype
    local currentfont  = currentfont
    local diskseen     = false
    local checkdisc    = sweeptype and getprev(head)
    local flags        = sequence.flags or noflags
    local done         = false
    local skipmark     = flags[1]
    local skipligature = flags[2]
    local skipbase     = flags[3]
    local markclass    = sequence.markclass
    local skipped      = false
    local startprev,
          startnext    = getboth(start)
    for k=1,#contexts do -- i've only seen ccmp having > 1 (e.g. dejavu)
        local match   = true
        local current = start
        local last    = start
        local ck      = contexts[k]
        local seq     = ck[3]
        local s       = #seq
        local size    = 1
        -- f..l = mid string
        if s == 1 then
            -- this seldom happens as it makes no sense (bril, ebgaramond, husayni, minion)
            local char = ischar(current,currentfont)
            if char then
                if not seq[1][char] then
                    match = false
                end
            end
        else
            -- maybe we need a better space check (maybe check for glue or category or combination)
            -- we cannot optimize for n=2 because there can be disc nodes
            local f = ck[4]
            local l = ck[5]
            -- current match
            size = l - f + 1
            if size > 1 then
                -- before/current/after | before/current | current/after
                local discfound -- = nil
                local n = f + 1
             -- last = getnext(last) -- the second in current (first already matched)
                last = startnext -- the second in current (first already matched)
                while n <= l do
                    if not last and (sweeptype == "post" or sweeptype == "replace") then
                        last      = getnext(sweepnode)
                        sweeptype = nil
                    end
                    if last then
                        local char, id = ischar(last,currentfont)
                        if char then
                            local class = classes[char]
                            if class then
                                if class == skipmark or class == skipligature or class == skipbase or (markclass and class == "mark" and not markclass[char]) then
                                    skipped = true
                                    if trace_skips then
                                        show_skip(dataset,sequence,char,ck,class)
                                    end
                                    last = getnext(last)
                                elseif seq[n][char] then
                                    if n < l then
                                        last = getnext(last)
                                    end
                                    n = n + 1
                                else
                                    if discfound then
                                        notmatchreplace[discfound] = true
                                        if notmatchpre[discfound] then
                                            match = false
                                        end
                                    else
                                        match = false
                                    end
                                    break
                                end
                            else
                                if discfound then
                                    notmatchreplace[discfound] = true
                                    if notmatchpre[discfound] then
                                        match = false
                                    end
                                else
                                    match = false
                                end
                                break
                            end
                        elseif char == false then
                            if discfound then
                                notmatchreplace[discfound] = true
                                if notmatchpre[discfound] then
                                    match = false
                                end
                            else
                                match = false
                            end
                            break
                        elseif id == disc_code then
                            diskseen              = true
                            discfound             = last
                            notmatchpre[last]     = nil
                            notmatchpost[last]    = true
                            notmatchreplace[last] = nil
                            local pre, post, replace = getdisc(last)
                            if pre then
                                local n = n
                                while pre do
                                    if seq[n][getchar(pre)] then
                                        n = n + 1
                                        pre = getnext(pre)
                                        if n > l then
                                            break
                                        end
                                    else
                                        notmatchpre[last] = true
                                        break
                                    end
                                end
                                if n <= l then
                                    notmatchpre[last] = true
                                end
                            else
                                notmatchpre[last] = true
                            end
                            if replace then
                                -- so far we never entered this branch
                                while replace do
                                    if seq[n][getchar(replace)] then
                                        n = n + 1
                                        replace = getnext(replace)
                                        if n > l then
                                            break
                                        end
                                    else
                                        notmatchreplace[last] = true
                                        if notmatchpre[last] then
                                            match = false
                                        end
                                        break
                                    end
                                end
                                -- why here again
                                if notmatchpre[last] then
                                    match = false
                                end
                            end
                            -- maybe only if match
                            last = getnext(last)
                        else
                            match = false
                            break
                        end
                    else
                        match = false
                        break
                    end
                end
            end
            -- before
            if match and f > 1 then
             -- local prev = getprev(start)
             -- if prev then
                if startprev then
                    local prev = startprev
                    if prev == checkdisc and (sweeptype == "pre" or sweeptype == "replace") then
                        prev      = getprev(sweepnode)
                     -- sweeptype = nil
                    end
                    if prev then
                        local discfound -- = nil
                        local n = f - 1
                        while n >= 1 do
                            if prev then
                                local char, id = ischar(prev,currentfont)
                                if char then
                                    local class = classes[char]
                                    if class then
                                        if class == skipmark or class == skipligature or class == skipbase or (markclass and class == "mark" and not markclass[char]) then
                                            skipped = true
                                            if trace_skips then
                                                show_skip(dataset,sequence,char,ck,class)
                                            end
                                            prev = getprev(prev)
                                        elseif seq[n][char] then
                                            if n > 1 then
                                                prev = getprev(prev)
                                            end
                                            n = n - 1
                                        else
                                            if discfound then
                                                notmatchreplace[discfound] = true
                                                if notmatchpost[discfound] then
                                                    match = false
                                                end
                                            else
                                                match = false
                                            end
                                            break
                                        end
                                    else
                                        if discfound then
                                            notmatchreplace[discfound] = true
                                            if notmatchpost[discfound] then
                                                match = false
                                            end
                                        else
                                            match = false
                                        end
                                        break
                                    end
                                elseif char == false then
                                    if discfound then
                                        notmatchreplace[discfound] = true
                                        if notmatchpost[discfound] then
                                            match = false
                                        end
                                    else
                                        match = false
                                    end
                                    break
                                elseif id == disc_code then
                                    -- the special case: f i where i becomes dottless i ..
                                    diskseen              = true
                                    discfound             = prev
                                    notmatchpre[prev]     = true
                                    notmatchpost[prev]    = nil
                                    notmatchreplace[prev] = nil
                                    local pre, post, replace, pretail, posttail, replacetail = getdisc(prev,true)
                                    if pre ~= start and post ~= start and replace ~= start then
                                        if post then
                                            local n = n
                                            while posttail do
                                                if seq[n][getchar(posttail)] then
                                                    n = n - 1
                                                    if posttail == post then
                                                        break
                                                    else
                                                        posttail = getprev(posttail)
                                                        if n < 1 then
                                                            break
                                                        end
                                                    end
                                                else
                                                    notmatchpost[prev] = true
                                                    break
                                                end
                                            end
                                            if n >= 1 then
                                                notmatchpost[prev] = true
                                            end
                                        else
                                            notmatchpost[prev] = true
                                        end
                                        if replace then
                                            -- we seldom enter this branch (e.g. on brill efficient)
                                            while replacetail do
                                                if seq[n][getchar(replacetail)] then
                                                    n = n - 1
                                                    if replacetail == replace then
                                                        break
                                                    else
                                                        replacetail = getprev(replacetail)
                                                        if n < 1 then
                                                            break
                                                        end
                                                    end
                                                else
                                                    notmatchreplace[prev] = true
                                                    if notmatchpost[prev] then
                                                        match = false
                                                    end
                                                    break
                                                end
                                            end
                                            if not match then
                                                break
                                            end
                                        end
                                    end
                                    -- maybe only if match
                                    prev = getprev(prev)
                                elseif id == glue_code and seq[n][32] and isspace(prev,threshold,id) then
                                    n = n - 1
                                    prev = getprev(prev)
                                else
                                    match = false
                                    break
                                end
                            else
                                match = false
                                break
                            end
                        end
                    else
                        match = false
                    end
                else
                    match = false
                end
            end
            -- after
            if match and s > l then
                local current = last and getnext(last)
                if not current and (sweeptype == "post" or sweeptype == "replace") then
                    current   = getnext(sweepnode)
                 -- sweeptype = nil
                end
                if current then
                    local discfound -- = nil
                    -- removed optimization for s-l == 1, we have to deal with marks anyway
                    local n = l + 1
                    while n <= s do
                        if current then
                            local char, id = ischar(current,currentfont)
                            if char then
                                local class = classes[char]
                                if class then
                                    if class == skipmark or class == skipligature or class == skipbase or (markclass and class == "mark" and not markclass[char]) then
                                        skipped = true
                                        if trace_skips then
                                            show_skip(dataset,sequence,char,ck,class)
                                        end
                                        current = getnext(current) -- was absent
                                    elseif seq[n][char] then
                                        if n < s then -- new test
                                            current = getnext(current) -- was absent
                                        end
                                        n = n + 1
                                    else
                                        if discfound then
                                            notmatchreplace[discfound] = true
                                            if notmatchpre[discfound] then
                                                match = false
                                            end
                                        else
                                            match = false
                                        end
                                        break
                                    end
                                else
                                    if discfound then
                                        notmatchreplace[discfound] = true
                                        if notmatchpre[discfound] then
                                            match = false
                                        end
                                    else
                                        match = false
                                    end
                                    break
                                end
                            elseif char == false then
                                if discfound then
                                    notmatchreplace[discfound] = true
                                    if notmatchpre[discfound] then
                                        match = false
                                    end
                                else
                                    match = false
                                end
                                break
                            elseif id == disc_code then
                                diskseen                 = true
                                discfound                = current
                                notmatchpre[current]     = nil
                                notmatchpost[current]    = true
                                notmatchreplace[current] = nil
                                local pre, post, replace = getdisc(current)
                                if pre then
                                    local n = n
                                    while pre do
                                        if seq[n][getchar(pre)] then
                                            n = n + 1
                                            pre = getnext(pre)
                                            if n > s then
                                                break
                                            end
                                        else
                                            notmatchpre[current] = true
                                            break
                                        end
                                    end
                                    if n <= s then
                                        notmatchpre[current] = true
                                    end
                                else
                                    notmatchpre[current] = true
                                end
                                if replace then
                                    -- so far we never entered this branch
                                    while replace do
                                        if seq[n][getchar(replace)] then
                                            n = n + 1
                                            replace = getnext(replace)
                                            if n > s then
                                                break
                                            end
                                        else
                                            notmatchreplace[current] = true
                                            -- different than others, needs checking if "not" is okay
                                            if not notmatchpre[current] then
                                                match = false
                                            end
                                            break
                                        end
                                    end
                                    if not match then
                                        break
                                    end
                                else
                                    -- skip 'm
                                end
                                -- maybe only if match
                                current = getnext(current)
                            elseif id == glue_code and seq[n][32] and isspace(current,threshold,id) then
                                n = n + 1
                                current = getnext(current)
                            else
                                match = false
                                break
                            end
                        else
                            match = false
                            break
                        end
                    end
                else
                    match = false
                end
            end
        end
        if match then
            if trace_contexts then
                chaintrac(head,start,dataset,sequence,rlmode,ck,skipped)
            end
            if diskseen or sweepnode then
                head, start, done = chaindisk(head,start,dataset,sequence,rlmode,ck,skipped)
            else
                head, start, done = chainrun(head,start,last,dataset,sequence,rlmode,ck,skipped)
            end
            if done then
                break -- out of contexts (new, needs checking)
            end
        end
    end
    if diskseen then
        notmatchpre     = { }
        notmatchpost    = { }
        notmatchreplace = { }
    end
    return head, start, done
end

handlers.gsub_context             = handle_contextchain
handlers.gsub_contextchain        = handle_contextchain
handlers.gsub_reversecontextchain = handle_contextchain
handlers.gpos_contextchain        = handle_contextchain
handlers.gpos_context             = handle_contextchain

-- this needs testing

local function chained_contextchain(head,start,stop,dataset,sequence,currentlookup,rlmode)
    local steps    = currentlookup.steps
    local nofsteps = currentlookup.nofsteps
    if nofsteps > 1 then
        reportmoresteps(dataset,sequence)
    end
    return handle_contextchain(head,start,dataset,sequence,currentlookup,rlmode)
end

chainprocs.gsub_context             = chained_contextchain
chainprocs.gsub_contextchain        = chained_contextchain
chainprocs.gsub_reversecontextchain = chained_contextchain
chainprocs.gpos_contextchain        = chained_contextchain
chainprocs.gpos_context             = chained_contextchain

-- experiment (needs no handler in font-otc so not now):
--
-- function otf.registerchainproc(name,f)
--  -- chainprocs[name] = f
--     chainprocs[name] = function(head,start,stop,dataset,sequence,currentlookup,rlmode)
--         local done = currentlookup.nofsteps > 0
--         if not done then
--             reportzerosteps(dataset,sequence)
--         else
--             head, start, done = f(head,start,stop,dataset,sequence,currentlookup,rlmode)
--             if not head or not start then
--                 reportbadsteps(dataset,sequence)
--             end
--         end
--         return head, start, done
--     end
-- end

local missing = setmetatableindex("table")

local function logprocess(...)
    if trace_steps then
        registermessage(...)
    end
    report_process(...)
end

local logwarning = report_process

local function report_missing_coverage(dataset,sequence)
    local t = missing[currentfont]
    if not t[sequence] then
        t[sequence] = true
        logwarning("missing coverage for feature %a, lookup %a, type %a, font %a, name %a",
            dataset[4],sequence.name,sequence.type,currentfont,tfmdata.properties.fullname)
    end
end

local resolved = { } -- we only resolve a font,script,language pair once

-- todo: pass all these 'locals' in a table

local sequencelists = setmetatableindex(function(t,font)
    local sequences = fontdata[font].resources.sequences
    if not sequences or not next(sequences) then
        sequences = false
    end
    t[font] = sequences
    return sequences
end)

-- fonts.hashes.sequences = sequencelists

do -- overcome local limit

    local autofeatures    = fonts.analyzers.features
    local featuretypes    = otf.tables.featuretypes
    local defaultscript   = otf.features.checkeddefaultscript
    local defaultlanguage = otf.features.checkeddefaultlanguage

    local wildcard        = "*"
    local default         = "dflt"

    local function initialize(sequence,script,language,enabled,autoscript,autolanguage)
        local features = sequence.features
        if features then
            local order = sequence.order
            if order then
                local featuretype = featuretypes[sequence.type or "unknown"]
                for i=1,#order do
                    local kind  = order[i]
                    local valid = enabled[kind]
                    if valid then
                        local scripts   = features[kind]
                        local languages = scripts and (
                            scripts[script] or
                            scripts[wildcard] or
                            (autoscript and defaultscript(featuretype,autoscript,scripts))
                        )
                        local enabled = languages and (
                            languages[language] or
                            languages[wildcard] or
                            (autolanguage and defaultlanguage(featuretype,autolanguage,languages))
                        )
                        if enabled then
                            return { valid, autofeatures[kind] or false, sequence, kind }
                        end
                    end
                end
            else
                -- can't happen
            end
        end
        return false
    end

    function otf.dataset(tfmdata,font) -- generic variant, overloaded in context
        local shared       = tfmdata.shared
        local properties   = tfmdata.properties
        local language     = properties.language or "dflt"
        local script       = properties.script   or "dflt"
        local enabled      = shared.features
        local autoscript   = enabled and enabled.autoscript
        local autolanguage = enabled and enabled.autolanguage
        local res = resolved[font]
        if not res then
            res = { }
            resolved[font] = res
        end
        local rs = res[script]
        if not rs then
            rs = { }
            res[script] = rs
        end
        local rl = rs[language]
        if not rl then
            rl = {
                -- indexed but we can also add specific data by key
            }
            rs[language] = rl
            local sequences = tfmdata.resources.sequences
            if sequences then
                for s=1,#sequences do
                    local v = enabled and initialize(sequences[s],script,language,enabled,autoscript,autolanguage)
                    if v then
                        rl[#rl+1] = v
                    end
                end
            end
        end
        return rl
    end

end

-- Functions like kernrun, comprun etc evolved over time and in the end look rather
-- complex. It's a bit of a compromis between extensive copying and creating subruns.
-- The logic has been improved a lot by Kai and Ivo who use complex fonts which
-- really helped to identify border cases on the one hand and get insight in the diverse
-- ways fonts implement features (not always that consistent and efficient). At the same
-- time I tried to keep the code relatively efficient so that the overhead in runtime
-- stays acceptable.

local function report_disc(what,n)
    report_run("%s: %s > %s",what,n,languages.serializediscretionary(n))
end

local function kernrun(disc,k_run,font,attr,...)
    --
    -- we catch <font 1><disc font 2>
    --
    if trace_kernruns then
        report_disc("kern",disc)
    end
    --
    local prev, next = getboth(disc)
    --
    local nextstart = next
    local done      = false
    --
    local pre, post, replace, pretail, posttail, replacetail = getdisc(disc,true)
    --
    local prevmarks = prev
    --
    -- can be optional, because why on earth do we get a disc after a mark (okay, maybe when a ccmp
    -- has happened but then it should be in the disc so basically this test indicates an error)
    --
    while prevmarks do
        local char = ischar(prevmarks,font)
        if char and marks[char] then
            prevmarks = getprev(prevmarks)
        else
            break
        end
    end
    --
    if prev and not ischar(prev,font) then  -- and (pre or replace)
        prev = false
    end
    if next and not ischar(next,font) then  -- and (post or replace)
        next = false
    end
    --
    -- we need to get rid of this nest mess some day .. has to be done otherwise
    --
    if pre then
        if k_run(pre,"injections",nil,font,attr,...) then
            done = true
        end
        if prev then
            local nest = getprev(pre)
            setlink(prev,pre)
            if k_run(prevmarks,"preinjections",pre,font,attr,...) then -- or prev?
                done = true
            end
            setprev(pre,nest)
-- setprev(pre)
            setnext(prev,disc)
        end
    end
    --
    if post then
        if k_run(post,"injections",nil,font,attr,...) then
            done = true
        end
        if next then
            setlink(posttail,next)
            if k_run(posttail,"postinjections",next,font,attr,...) then
                done = true
            end
            setnext(posttail)
            setprev(next,disc)
        end
    end
    --
    if replace then
        if k_run(replace,"injections",nil,font,attr,...) then
            done = true
        end
        if prev then
            local nest = getprev(replace)
            setlink(prev,replace)
            if k_run(prevmarks,"replaceinjections",replace,font,attr,...) then -- getnext(replace))
                done = true
            end
            setprev(replace,nest)
         -- setprev(replace)
            setnext(prev,disc)
        end
        if next then
            setlink(replacetail,next)
            if k_run(replacetail,"replaceinjections",next,font,attr,...) then
                done = true
            end
            setnext(replacetail)
            setprev(next,disc)
        end
    elseif prev and next then
        setlink(prev,next)
        if k_run(prevmarks,"emptyinjections",next,font,attr,...) then
            done = true
        end
        setlink(prev,disc,next)
    end
    return nextstart, done
end

-- fonts like ebgaramond do ligatures this way (less efficient than e.g. dejavu which
-- will do the testrun variant)

local function comprun(disc,c_run,...) -- vararg faster than the whole list
    if trace_compruns then
        report_disc("comp",disc)
    end
    --
    local pre, post, replace = getdisc(disc)
    local renewed = false
    --
    if pre then
        sweepnode = disc
        sweeptype = "pre" -- in alternative code preinjections is uc_c_sed (also used then for properties, saves a variable)
        local new, done = c_run(pre,...)
        if done then
            pre     = new
            renewed = true
        end
    end
    --
    if post then
        sweepnode = disc
        sweeptype = "post"
        local new, done = c_run(post,...)
        if done then
            post    = new
            renewed = true
        end
    end
    --
    if replace then
        sweepnode = disc
        sweeptype = "replace"
        local new, done = c_run(replace,...)
        if done then
            replace = new
            renewed = true
        end
    end
    --
    sweepnode = nil
    sweeptype = nil
    if renewed then
        setdisc(disc,pre,post,replace)
    end
    --
    return getnext(disc), renewed
end

-- local function testrun(disc,t_run,c_run,...)
--     if trace_testruns then
--         report_disc("test",disc)
--     end
--     local prev, next = getboth(disc)
--     if not next then
--         -- weird discretionary
--         return
--     end
--     local pre, post, replace, pretail, posttail, replacetail = getdisc(disc,true)
--     local done = false
--     if replace and prev then
--         -- this is a bit strange as we only do replace here and not post
--         -- anyway, we only look ahead ... the idea is that we discard a
--         -- disc when there is a ligature crossing the replace boundary
--         setlink(replacetail,next)
--         local ok, overflow = t_run(replace,next,...)
--         if ok and overflow then
--             -- so, we can have crossed the boundary
--             setfield(disc,"replace")
--             setlink(prev,replace)
--          -- setlink(replacetail,next)
--             setboth(disc)
--             flush_node_list(disc)
--             return replace, true -- restart .. tricky !
--         else
--             -- we stay inside the disc
--             setnext(replacetail)
--             setprev(next,disc)
--         end
--      -- pre, post, replace, pretail, posttail, replacetail = getdisc(disc,true)
--     end
--     --
--     -- like comprun
--     --
--     local renewed = false
--     --
--     if pre then
--         sweepnode = disc
--         sweeptype = "pre"
--         local new, ok = c_run(pre,...)
--         if ok then
--             pre     = new
--             renewed = true
--         end
--     end
--     --
--     if post then
--         sweepnode = disc
--         sweeptype = "post"
--         local new, ok = c_run(post,...)
--         if ok then
--             post    = new
--             renewed = true
--         end
--     end
--     --
--     if replace then
--         sweepnode = disc
--         sweeptype = "replace"
--         local new, ok = c_run(replace,...)
--         if ok then
--             replace = new
--             renewed = true
--         end
--     end
--     --
--     sweepnode = nil
--     sweeptype = nil
--     if renewed then
--         setdisc(disc,pre,post,replace)
--         return next, true
--     else
--         return next, done
--     end
-- end

local function testrun(disc,t_run,c_run,...)
    if trace_testruns then
        report_disc("test",disc)
    end
    local prev, next = getboth(disc)
    if not next then
        -- weird discretionary
        return
    end
    local pre, post, replace, pretail, posttail, replacetail = getdisc(disc,true)
    local done = false
    if (post or replace) and prev then
        if post then
            setlink(posttail,next)
        else
            post = next
        end
        if replace then
            setlink(replacetail,next)
        else
            replace = next
        end
        local d_post    = t_run(post,next,...)
        local d_replace = t_run(replace,next,...)
        if (d_post and d_post > 0) or (d_replace and d_replace > 0) then
            local d = d_replace or d_post
            if d_post and d < d_post then
                d = d_post
            end
            local head, tail = getnext(disc), disc
            for i=1,d do
                tail = getnext(tail)
                if getid(tail) == disc_code then
                    head, tail = flattendisk(head,tail)
                end
            end
            local next = getnext(tail)
            setnext(tail)
            setprev(head)
            local new  = copy_node_list(head)
            if posttail then
                setlink(posttail,head)
            else
                post = head
            end
            if replacetail then
                setlink(replacetail,new)
            else
                replace = new
            end
            setlink(disc,next)
        else
            -- we stay inside the disc
            if posttail then
                setnext(posttail)
            else
                post = nil
            end
            setnext(replacetail)
            if replacetail then
                setnext(replacetail)
            else
                replace = nil
            end
            setprev(next,disc)
        end
     -- pre, post, replace, pretail, posttail, replacetail = getdisc(disc,true)
    end
    --
    -- like comprun
    --
    local renewed = false
    --
    if pre then
        sweepnode = disc
        sweeptype = "pre"
        local new, ok = c_run(pre,...)
        if ok then
            pre     = new
            renewed = true
        end
    end
    --
    if post then
        sweepnode = disc
        sweeptype = "post"
        local new, ok = c_run(post,...)
        if ok then
            post    = new
            renewed = true
        end
    end
    --
    if replace then
        sweepnode = disc
        sweeptype = "replace"
        local new, ok = c_run(replace,...)
        if ok then
            replace = new
            renewed = true
        end
    end
    --
    sweepnode = nil
    sweeptype = nil
    if renewed then
        setdisc(disc,pre,post,replace)
        return next, true
    else
        return next, done
    end
end

-- We can make some assumptions with respect to discretionaries. First of all it is very
-- unlikely that some of the analysis related attributes applies. Then we can also assume
-- that the ConTeXt specific dynamic attribute is different, although we do use explicit
-- discretionaries (maybe we need to tag those some day). So, at least for now, we don't
-- have the following test in the sub runs:
--
-- -- local a = getattr(start,0)
-- -- if a then
-- --     a = (a == attr) and (not attribute or getprop(start,a_state) == attribute)
-- -- else
-- --     a = not attribute or getprop(start,a_state) == attribute
-- -- end
-- -- if a then
--
-- but use this instead:
--
-- -- local a = getattr(start,0)
-- -- if not a or (a == attr) then
--
-- and even that one is probably not needed. However, we can handle interesting
-- cases now:
--
--  1{2{\oldstyle\discretionary{3}{4}{5}}6}7\par
--  1{2\discretionary{3{\oldstyle3}}{{\oldstyle4}4}{5{\oldstyle5}5}6}7\par


local nesting = 0

local function c_run_single(head,font,attr,lookupcache,step,dataset,sequence,rlmode,handler)
    local done  = false
    local sweep = sweephead[head]
    if sweep then
        start = sweep
        sweephead[head] = nil
    else
        start = head
    end
    while start do
        local char = ischar(start,font)
        if char then
            local a -- happens often so no assignment is faster
            if attr then
                a = getattr(start,0)
            end
            if not a or (a == attr) then
                local lookupmatch = lookupcache[char]
                if lookupmatch then
                    local ok
                    head, start, ok = handler(head,start,dataset,sequence,lookupmatch,rlmode,step,1)
                    if ok then
                        done = true
                    end
                end
                if start then
                    start = getnext(start)
                end
            else
                -- go on can be a mixed one
                start = getnext(start)
            end
        elseif char == false then
            return head, done
        elseif sweep then
            -- else we loose the rest
            return head, done
        else
            -- in disc component
            start = getnext(start)
        end
    end
    return head, done
end

-- local function t_run_single(start,stop,font,attr,lookupcache)
--     while start ~= stop do
--         local char = ischar(start,font)
--         if char then
--             local a -- happens often so no assignment is faster
--             if attr then
--                 a = getattr(start,0)
--             end
--             local startnext = getnext(start)
--             if not a or (a == attr) then
--                 local lookupmatch = lookupcache[char]
--                 if lookupmatch then -- hm, hyphens can match (tlig) so we need to really check
--                     -- if we need more than ligatures we can outline the code and use functions
--                     local s = startnext
--                     local l = nil
--                     local d = 0
--                     while s do
--                         if s == stop then
--                             d = 1
--                         elseif d > 0 then
--                             d = d + 1
--                         end
--                         local lg = lookupmatch[getchar(s)]
--                         if lg then
--                             l = lg
--                             s = getnext(s)
--                         else
--                             break
--                         end
--                     end
--                     if l and l.ligature then
--                         return true, d > 1
--                     end
--                 end
--             else
--                 -- go on can be a mixed one
--             end
--             start = starttnext
--         else
--             break
--         end
--     end
-- end

local function t_run_single(start,stop,font,attr,lookupcache)
    local lastd = nil
    while start ~= stop do
        local char = ischar(start,font)
        if char then
            local a -- happens often so no assignment is faster
            if attr then
                a = getattr(start,0)
            end
            local startnext = getnext(start)
            if not a or (a == attr) then
                local lookupmatch = lookupcache[char]
                if lookupmatch then -- hm, hyphens can match (tlig) so we need to really check
                    -- if we need more than ligatures we can outline the code and use functions
                    local s     = startnext
                    local ss    = nil
                    local sstop = s == stop
                    if not s then
                        s = ss
                        ss = nil
                    end
                    while getid(s) == disc_code do
                        ss = getnext(s)
                        s  = getfield(s,"replace")
                        if not s then
                            s = ss
                            ss = nil
                        end
                    end
                    local l = nil
                    local d = 0
                    while s do
                        local lg = lookupmatch[getchar(s)]
                        if lg then
                            if sstop then
                                d = 1
                            elseif d > 0 then
                                d = d + 1
                            end
                            l = lg
                            s = getnext(s)
                            sstop = s == stop
                            if not s then
                                s  = ss
                                ss = nil
                            end
                            while getid(s) == disc_code do
                                ss = getnext(s)
                                s  = getfield(s,"replace")
                                if not s then
                                    s  = ss
                                    ss = nil
                                end
                            end
                        else
                            break
                        end
                    end
                    if l and l.ligature then
                        lastd = d
                    end
                end
            else
                -- go on can be a mixed one
            end
            if lastd then
                return lastd
            end
            start = startnext
        else
            break
        end
    end
end

local function k_run_single(sub,injection,last,font,attr,lookupcache,step,dataset,sequence,rlmode,handler)
    local a -- happens often so no assignment is faster
    if attr then
        a = getattr(sub,0)
    end
    if not a or (a == attr) then
        for n in traverse_nodes(sub) do -- only gpos
            if n == last then
                break
            end
            local char = ischar(n)
            if char then
                local lookupmatch = lookupcache[char]
                if lookupmatch then
                    local h, d, ok = handler(sub,n,dataset,sequence,lookupmatch,rlmode,step,1,injection)
                    if ok then
                        return true
                    end
                end
            end
        end
    end
end

local function c_run_multiple(head,font,attr,steps,nofsteps,dataset,sequence,rlmode,handler)
    local done  = false
    local sweep = sweephead[head]
    if sweep then
        start = sweep
        sweephead[head] = nil
    else
        start = head
    end
    while start do
        local char = ischar(start,font)
        if char then
            local a -- happens often so no assignment is faster
            if attr then
                a = getattr(start,0)
            end
            if not a or (a == attr) then
                for i=1,nofsteps do
                    local step        = steps[i]
                    local lookupcache = step.coverage
                    if lookupcache then
                        local lookupmatch = lookupcache[char]
                        if lookupmatch then
                            -- we could move all code inline but that makes things even more unreadable
                            local ok
                            head, start, ok = handler(head,start,dataset,sequence,lookupmatch,rlmode,step,i)
                            if ok then
                                done = true
                                break
                            elseif not start then
                                -- don't ask why ... shouldn't happen
                                break
                            end
                        end
                    else
                        report_missing_coverage(dataset,sequence)
                    end
                end
                if start then
                    start = getnext(start)
                end
            else
                -- go on can be a mixed one
                start = getnext(start)
            end
        elseif char == false then
            -- whatever glyph
            return head, done
        elseif sweep then
            -- else we loose the rest
            return head, done
        else
            -- in disc component
            start = getnext(start)
        end
    end
    return head, done
end

-- local function t_run_multiple(start,stop,font,attr,steps,nofsteps)
--     while start ~= stop do
--         local char = ischar(start,font)
--         if char then
--             local a -- happens often so no assignment is faster
--             if attr then
--                 a = getattr(start,0)
--             end
--             local startnext = getnext(start)
--             if not a or (a == attr) then
--                 for i=1,nofsteps do
--                     local step = steps[i]
--                     local lookupcache = step.coverage
--                     if lookupcache then
--                         local lookupmatch = lookupcache[char]
--                         if lookupmatch then
--                             -- if we need more than ligatures we can outline the code and use functions
--                             local s = startnext
--                             local l = nil
--                             local d = 0
--                             while s do
--                                 if s == stop then
--                                     d = 1
--                                 elseif d > 0 then
--                                     d = d + 1
--                                 end
--                                 local lg = lookupmatch[getchar(s)]
--                                 if lg then
--                                     l = lg
--                                     s = getnext(s)
--                                 else
--                                     break
--                                 end
--                             end
--                             if l and l.ligature then
--                                 return true, d > 1
--                             end
--                         end
--                     else
--                         report_missing_coverage(dataset,sequence)
--                     end
--                 end
--             else
--                 -- go on can be a mixed one
--             end
--             start = startnext
--         else
--             break
--         end
--     end
-- end

local function t_run_multiple(start,stop,font,attr,steps,nofsteps)
    local lastd = nil
    while start ~= stop do
        local char = ischar(start,font)
        if char then
            local a -- happens often so no assignment is faster
            if attr then
                a = getattr(start,0)
            end
            local startnext = getnext(start)
            if not a or (a == attr) then
                for i=1,nofsteps do
                    local step = steps[i]
                    local lookupcache = step.coverage
                    if lookupcache then
                        local lookupmatch = lookupcache[char]
                        if lookupmatch then
                            -- if we need more than ligatures we can outline the code and use functions
                            local s     = startnext
                            local ss    = nil
                            local sstop = s == stop
                            if not s then
                                s  = ss
                                ss = nil
                            end
                            while getid(s) == disc_code do
                                ss = getnext(s)
                                s  = getfield(s,"replace")
                                if not s then
                                    s  = ss
                                    ss = nil
                                end
                            end
                            local l = nil
                            local d = 0
                            while s do
                                local lg = lookupmatch[getchar(s)]
                                if lg then
                                    if sstop then
                                        d = 1
                                    elseif d > 0 then
                                        d = d + 1
                                    end
                                    l = lg
                                    s = getnext(s)
                                    sstop = s == stop
                                    if not s then
                                        s  = ss
                                        ss = nil
                                    end
                                    while getid(s) == disc_code do
                                        ss = getnext(s)
                                        s  = getfield(s,"replace")
                                        if not s then
                                            s  = ss
                                            ss = nil
                                        end
                                    end
                                else
                                    break
                                end
                            end
                            if l and l.ligature then
                                lastd = d
                            end
                        end
                    else
                        report_missing_coverage(dataset,sequence)
                    end
                end
            else
                -- go on can be a mixed one
            end
            if lastd then
                return lastd
            end
            start = startnext
        else
            break
        end
    end
end

local function k_run_multiple(sub,injection,last,font,attr,steps,nofsteps,dataset,sequence,rlmode,handler)
    local a -- happens often so no assignment is faster
    if attr then
        a = getattr(sub,0)
    end
    if not a or (a == attr) then
        for n in traverse_nodes(sub) do -- only gpos
            if n == last then
                break
            end
            local char = ischar(n)
            if char then
                for i=1,nofsteps do
                    local step        = steps[i]
                    local lookupcache = step.coverage
                    if lookupcache then
                        local lookupmatch = lookupcache[char]
                        if lookupmatch then
                            local h, d, ok = handler(head,n,dataset,sequence,lookupmatch,step,rlmode,i,injection)
                            if ok then
                                return true
                            end
                        end
                    else
                        report_missing_coverage(dataset,sequence)
                    end
                end
            end
        end
    end
end

-- to be checked, nowadays we probably can assume properly matched directions
-- so maybe we no longer need a stack

local function txtdirstate(start,stack,top,rlparmode)
    local dir = getdir(start)
    local new = 1
    if dir == "+TRT" then
        top = top + 1
        stack[top] = dir
        new = -1
    elseif dir == "+TLT" then
        top = top + 1
        stack[top] = dir
    elseif dir == "-TRT" or dir == "-TLT" then
        top = top - 1
        if stack[top] == "+TRT" then
            new = -1
        end
    else
        new = rlparmode
    end
    if trace_directions then
        report_process("directions after txtdir %a: parmode %a, txtmode %a, level %a",dir,mref(rlparmode),mref(new),top)
    end
    return getnext(start), top, new
end

local function pardirstate(start)
    local dir = getdir(start)
    local new = 0
    if dir == "TLT" then
        new = 1
    elseif dir == "TRT" then
        new = -1
    end
    if trace_directions then
        report_process("directions after pardir %a: parmode %a",dir,mref(new))
    end
    return getnext(start), new, new
end

otf.helpers             = otf.helpers or { }
otf.helpers.txtdirstate = txtdirstate
otf.helpers.pardirstate = pardirstate

-- This is the main loop. We run over the node list dealing with a specific font. The
-- attribute is a context specific thing. We could work on sub start-stop ranges instead
-- but I wonder if there is that much speed gain (experiments showed that it made not
-- much sense) and we need to keep track of directions anyway. Also at some point I
-- want to play with font interactions and then we do need the full sweeps. Apart from
-- optimizations the principles of processing the features hasn't changed much since
-- the beginning.

local function featuresprocessor(head,font,attr,direction)

    local sequences = sequencelists[font] -- temp hack

    if not sequencelists then
        return head, false
    end

    nesting = nesting + 1

    if nesting == 1 then
        currentfont  = font
        tfmdata      = fontdata[font]
        descriptions = tfmdata.descriptions -- only needed in gref so we could pass node there instead
        characters   = tfmdata.characters   -- but this branch is not entered that often anyway
  local resources    = tfmdata.resources
        marks        = resources.marks
        classes      = resources.classes
        threshold,
        factor       = getthreshold(font)
        checkmarks   = tfmdata.properties.checkmarks

    elseif currentfont ~= font then

        report_warning("nested call with a different font, level %s, quitting",nesting)
        nesting = nesting - 1
        return head, false

    end

    -- some 10% faster when no dynamics but hardly measureable on real runs .. but: it only
    -- works when we have no other dynamics as otherwise the zero run will be applied to the
    -- whole stream for which we then need to pass another variable which we won't

    -- if attr == 0 then
    --     attr = false
    -- end

    head = tonut(head)

    if trace_steps then
        checkstep(head)
    end

    local initialrl = direction == "TRT" and -1 or 0

    local done      = false
    local datasets  = otf.dataset(tfmdata,font,attr)
    local dirstack  = { } -- could move outside function but we can have local runs
    sweephead       = { }

    -- Keeping track of the headnode is needed for devanagari. (I generalized it a bit
    -- so that multiple cases are also covered.) We could prepend a temp node.

    -- We don't goto the next node when a disc node is created so that we can then treat
    -- the pre, post and replace. It's a bit of a hack but works out ok for most cases.

    for s=1,#datasets do
        local dataset      = datasets[s]
        ----- featurevalue = dataset[1] -- todo: pass to function instead of using a global
        local attribute    = dataset[2]
        local sequence     = dataset[3] -- sequences[s] -- also dataset[5]
        local rlparmode    = initialrl
        local topstack     = 0
        local typ          = sequence.type
        local gpossing     = typ == "gpos_single" or typ == "gpos_pair" -- store in dataset
        local handler      = handlers[typ]
        local steps        = sequence.steps
        local nofsteps     = sequence.nofsteps
        if not steps then
            -- this permits injection, watch the different arguments
            local h, d, ok = handler(head,head,dataset,sequence,nil,nil,nil,0,font,attr)
            if ok then
                done = true
                if h then
                    head = h
                end
            end
        elseif typ == "gsub_reversecontextchain" then
            -- this is a limited case, no special treatments like 'init' etc
            local start  = find_node_tail(head)
            local rlmode = 0 -- how important is this .. do we need to check for dir?
            while start do
                local char = ischar(start,font)
                if char then
                    local a -- happens often so no assignment is faster
                    if attr then
                        a = getattr(start,0)
                    end
                    if not a or (a == attr) then
                        for i=1,nofsteps do
                            local step = steps[i]
                            local lookupcache = step.coverage
                            if lookupcache then
                                local lookupmatch = lookupcache[char]
                                if lookupmatch then
                                    -- todo: disc?
                                    local ok
                                    head, start, ok = handler(head,start,dataset,sequence,lookupmatch,rlmode,step,i)
                                    if ok then
                                        done = true
                                        break
                                    end
                                end
                            else
                                report_missing_coverage(dataset,sequence)
                            end
                        end
                        if start then
                            start = getprev(start)
                        end
                    else
                        start = getprev(start)
                    end
                else
                    start = getprev(start)
                end
            end
        else
            local start  = head
            local rlmode = initialrl
            if nofsteps == 1 then -- happens often
                local step = steps[1]
                local lookupcache = step.coverage
                if not lookupcache then
                    report_missing_coverage(dataset,sequence)
                else
                    while start do
                        local char, id = ischar(start,font)
                        if char then
                         -- local a = attr and getattr(start,0)
                         -- if a then
                         --     a = (a == attr) and (not attribute or getprop(start,a_state) == attribute)
                         -- else
                         --     a = not attribute or getprop(start,a_state) == attribute
                         -- end
                            local a -- happens often so no assignment is faster
                            if attr then
                                if getattr(start,0) == attr and (not attribute or getprop(start,a_state) == attribute) then
                                    a = true
                                end
                            elseif not attribute or getprop(start,a_state) == attribute then
                                a = true
                            end
                            if a then
                                local lookupmatch = lookupcache[char]
                                if lookupmatch then
                                    local ok
                                    head, start, ok = handler(head,start,dataset,sequence,lookupmatch,rlmode,step,1)
                                    if ok then
                                        done = true
                                    end
                                end
                                if start then
                                    start = getnext(start)
                                end
                            else
                               start = getnext(start)
                            end
                        elseif char == false then
                           -- whatever glyph
                           start = getnext(start)
                        elseif id == glue_code then
                            -- happens often
                           start = getnext(start)
                        elseif id == disc_code then
                            local ok
                            if gpossing then
                                start, ok = kernrun(start,k_run_single,             font,attr,lookupcache,step,dataset,sequence,rlmode,handler)
                            elseif typ == "gsub_ligature" then
                                start, ok = testrun(start,t_run_single,c_run_single,font,attr,lookupcache,step,dataset,sequence,rlmode,handler)
                            else
                                start, ok = comprun(start,c_run_single,             font,attr,lookupcache,step,dataset,sequence,rlmode,handler)
                            end
                            if ok then
                                done = true
                            end
                        elseif id == math_code then
                            start = getnext(end_of_math(start))
                        elseif id == dir_code then
                            start, topstack, rlmode = txtdirstate(start,dirstack,topstack,rlparmode)
                        elseif id == localpar_code then
                            start, rlparmode, rlmode = pardirstate(start)
                        else
                            start = getnext(start)
                        end
                    end
                end

            else
                while start do
                    local char, id = ischar(start,font)
                    if char then
                     -- local a = attr and getattr(start,0)
                     -- if a then
                     --     a = (a == attr) and (not attribute or getprop(start,a_state) == attribute)
                     -- else
                     --     a = not attribute or getprop(start,a_state) == attribute
                     -- end
                        local a -- happens often so no assignment is faster
                        if attr then
                            if getattr(start,0) == attr and (not attribute or getprop(start,a_state) == attribute) then
                                a = true
                            end
                        elseif not attribute or getprop(start,a_state) == attribute then
                            a = true
                        end
                        if a then
                            for i=1,nofsteps do
                                local step        = steps[i]
                                local lookupcache = step.coverage
                                if lookupcache then
                                    local lookupmatch = lookupcache[char]
                                    if lookupmatch then
                                        -- we could move all code inline but that makes things even more unreadable
                                        local ok
                                        head, start, ok = handler(head,start,dataset,sequence,lookupmatch,rlmode,step,i)
                                        if ok then
                                            done = true
                                            break
                                        elseif not start then
                                            -- don't ask why ... shouldn't happen
                                            break
                                        end
                                    end
                                else
                                    report_missing_coverage(dataset,sequence)
                                end
                            end
                            if start then
                                start = getnext(start)
                            end
                        else
                            start = getnext(start)
                        end
                    elseif char == false then
                       -- whatever glyph
                        start = getnext(start)
                    elseif id == glue_code then
                        -- happens often
                        start = getnext(start)
                    elseif id == disc_code then
                        local ok
                        if gpossing then
                            start, ok = kernrun(start,k_run_multiple,               font,attr,steps,nofsteps,dataset,sequence,rlmode,handler)
                        elseif typ == "gsub_ligature" then
                            start, ok = testrun(start,t_run_multiple,c_run_multiple,font,attr,steps,nofsteps,dataset,sequence,rlmode,handler)
                        else
                            start, ok = comprun(start,c_run_multiple,               font,attr,steps,nofsteps,dataset,sequence,rlmode,handler)
                        end
                        if ok then
                            done = true
                        end
                    elseif id == math_code then
                        start = getnext(end_of_math(start))
                    elseif id == dir_code then
                        start, topstack, rlmode = txtdirstate(start,dirstack,topstack,rlparmode)
                    elseif id == localpar_code then
                        start, rlparmode, rlmode = pardirstate(start)
                    else
                        start = getnext(start)
                    end
                end
            end
        end

        if trace_steps then -- ?
            registerstep(head)
        end

    end

    nesting = nesting - 1
    head    = tonode(head)

    return head, done
end

-- so far

local plugins = { }
otf.plugins   = plugins

function otf.registerplugin(name,f)
    if type(name) == "string" and type(f) == "function" then
        plugins[name] = { name, f }
    end
end

local function plugininitializer(tfmdata,value)
    if type(value) == "string" then
        tfmdata.shared.plugin = plugins[value]
    end
end

local function pluginprocessor(head,font)
    local s = fontdata[font].shared
    local p = s and s.plugin
    if p then
        if trace_plugins then
            report_process("applying plugin %a",p[1])
        end
        return p[2](head,font)
    else
        return head, false
    end
end

local function featuresinitializer(tfmdata,value)
    -- nothing done here any more
end

registerotffeature {
    name         = "features",
    description  = "features",
    default      = true,
    initializers = {
        position = 1,
        node     = featuresinitializer,
        plug     = plugininitializer,
    },
    processors   = {
        node     = featuresprocessor,
        plug     = pluginprocessor,
    }
}

otf.nodemodeinitializer = featuresinitializer
otf.featuresprocessor   = featuresprocessor

-- This can be used for extra handlers, but should be used with care!

otf.handlers = handlers -- used in devanagari

-- We implement one here:

local setspacekerns = nodes.injections.setspacekerns if not setspacekerns then os.exit() end

if fontfeatures then

    function otf.handlers.trigger_space_kerns(head,start,dataset,sequence,_,_,_,_,font,attr)
        local features = fontfeatures[font]
        local enabled  = features and features.spacekern and features.kern
        if enabled then
            setspacekerns(font,sequence)
        end
        return head, start, enabled
    end

else -- generic (no hashes)

    function otf.handlers.trigger_space_kerns(head,start,dataset,sequence,_,_,_,_,font,attr)
        local shared   = fontdata[font].shared
        local features = shared and shared.features
        local enabled  = features and features.spacekern and features.kern
        if enabled then
            setspacekerns(font,sequence)
        end
        return head, start, enabled
    end

end

local function hasspacekerns(data)
    local sequences = data.resources.sequences
    for i=1,#sequences do
        local sequence = sequences[i]
        local steps    = sequence.steps
        if steps and sequence.features.kern then
            for i=1,#steps do
                local coverage = steps[i].coverage
                if not coverage then
                    -- maybe an issue
                elseif coverage[32] then
                    return true
                else
                    for k, v in next, coverage do
                        if v[32] then
                            return true
                        end
                    end
                end
            end
        end
    end
    return false
end

otf.readers.registerextender {
    name   = "spacekerns",
    action = function(data)
        data.properties.hasspacekerns = hasspacekerns(data)
    end
}

-- we merge the lookups but we still honor the language / script

local function spaceinitializer(tfmdata,value) -- attr
    local resources  = tfmdata.resources
    local spacekerns = resources and resources.spacekerns
    local properties = tfmdata.properties
    if value and spacekerns == nil then
        if properties and properties.hasspacekerns then
            local sequences = resources.sequences
            local left  = { }
            local right = { }
            local last  = 0
            local feat  = nil
            for i=1,#sequences do
                local sequence = sequences[i]
                local steps    = sequence.steps
                if steps then
                    local kern = sequence.features.kern
                    if kern then
                        if feat then
                            for script, languages in next, kern do
                                local f = feat[script]
                                if f then
                                    for l in next, languages do
                                        f[l] = true
                                    end
                                else
                                    feat[script] = languages
                                end
                            end
                        else
                            feat = kern
                        end
                        for i=1,#steps do
                            local step     = steps[i]
                            local coverage = step.coverage
                            local rules    = step.rules
                            local format   = step.format
                            if rules then
                                -- not now: analyze (simple) rules
                            elseif coverage then
                                -- what to do if we have no [1] but only [2]
                                local single = format == gpos_single
                                local kerns  = coverage[32]
                                if kerns then
                                    for k, v in next, kerns do
                                        if type(v) ~= "table" then
                                            right[k] = v
                                        elseif single then
                                            right[k] = v[3]
                                        else
                                            local one = v[1]
                                            if one then
                                                right[k] = one[3]
                                            end
                                        end
                                    end
                                end
                                for k, v in next, coverage do
                                    local kern = v[32]
                                    if kern then
                                        if type(kern) ~= "table" then
                                            left[k] = kern
                                        elseif single then
                                            left[k] = kern[3]
                                        else
                                            local one = kern[1]
                                            if one then
                                                left[k] = one[3]
                                            end
                                        end
                                    end
                                end
                            end
                        end
                        last = i
                    end
                else
                    -- no steps ... needed for old one ... we could use the basekerns
                    -- instead
                end
            end
            left  = next(left)  and left  or false
            right = next(right) and right or false
            if left or right then
                spacekerns = {
                    left  = left,
                    right = right,
                }
                if last > 0 then
                    local triggersequence = {
                        features = { kern = feat or { dflt = { dflt = true, } } },
                        flags    = noflags,
                        name     = "trigger_space_kerns",
                        order    = { "kern" },
                        type     = "trigger_space_kerns",
                        left     = left,
                        right    = right,
                    }
                    insert(sequences,last,triggersequence)
                end
            else
                spacekerns = false
            end
        else
            spacekerns = false
        end
        resources.spacekerns = spacekerns
    end
    return spacekerns
end

registerotffeature {
    name         = "spacekern",
    description  = "space kern injection",
    default      = true,
    initializers = {
        node     = spaceinitializer,
    },
}

local function markinitializer(tfmdata,value)
    local properties = tfmdata.properties
    properties.checkmarks = value
end

registerotffeature {
    name         = "checkmarks",
    description  = "check mark widths",
    default      = true,
    initializers = {
        node     = markinitializer,
    },
}
