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",
}

-- to be checked: discrun doesn't seem to do something useful now (except run the
-- check again) so if we need it again we'll do a zwnjrun or so

-- components will go away and be replaced by a property table which simplifies
-- code (also more efficient)

-- beware, on my development machine we test a slightly a more optimized version

-- assumptions:
--
-- cursives don't cross discretionaries
-- marks precede bases
--
-- pitfalls:
--
-- when we append to a dics field we need to set the field in order to update tail
--
-- This is a version of font-otn.lua adapted to the new font loader code. It
-- is a context version which can contain experimental code, but when we
-- have serious patches we will backport to the font-otn files. The plain
-- loader that ships with context also uses this now.
--
-- todo: looks like we have a leak somewhere (probably in ligatures)
-- todo: copy attributes to disc
-- todo: get rid of components, better use the tounicode entry if needed (at all)
--
-- 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
--
-- not possible:
--
-- \discretionary {alpha-} {betagammadelta}
--   {\discretionary {alphabeta-} {gammadelta}
--      {\discretionary {alphabetagamma-} {delta}
--         {alphabetagammadelta}}}

--[[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 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.</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.</p>

<p>After a lot of experiments (mostly by Taco, me and Idris) we're now at yet another
implementation. 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 acceptable. Not all functions are implemented yet, often because I
lack the fonts for testing. Many scripts are not yet supported either, 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>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). The flattening code used later is a prelude to an even more compact table
format (and as such it keeps evolving).</p>

<p>This module is sparsely documented because it is a moving target. The table format
of the reader changes and we experiment a lot with different methods for supporting
features.</p>

<p>As with the <l n='afm'/> code, we may decide to store more information in the
<l n='otf'/> table.</p>

<p>Incrementing the version number will force a re-cache. We jump the number by one
when there's a fix in the <l n='fontforge'/> library or <l n='lua'/> code that
results in different tables.</p>
--ldx]]--

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

local logs, trackers, nodes, attributes = logs, trackers, nodes, attributes

local registertracker   = trackers.register
local registerdirective = directives.register

local fonts = fonts
local otf   = fonts.handlers.otf

local trace_lookups      = false  registertracker("otf.lookups",      function(v) trace_lookups      = v end)
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)
----- trace_applied      = false  registertracker("otf.applied",      function(v) trace_applied      = 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)

----- zwnjruns           = true   registerdirective("otf.zwnjruns",     function(v) zwnjruns = v end)
local optimizekerns      = true
local alwaysdisc         = true   registerdirective("otf.alwaysdisc", function(v) alwaysdisc = v end)

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 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 remove_node        = nuts.remove

local setmetatableindex  = table.setmetatableindex

local zwnj               = 0x200C
local zwj                = 0x200D
local wildcard           = "*"
local default            = "dflt"

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 privateattribute   = attributes.private

-- 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.

local a_state            = privateattribute('state')

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 cursonce           = true

local fonthashes         = fonts.hashes
local fontdata           = fonthashes.identifiers
local fontfeatures       = fonthashes.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 global variables. The handler can be called nested but this assumes that the
-- same font is used. Nested calls are normally not needed (only for devanagari).

local tfmdata         = false
local characters      = false
local descriptions    = false
local marks           = 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

-- we use this for special testing and documentation

local checkstep       = (nodes and nodes.tracers and nodes.tracers.steppers.check)    or function() end
local registerstep    = (nodes and nodes.tracers and nodes.tracers.steppers.register) or function() end
local registermessage = (nodes and nodes.tracers and nodes.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

-- 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 copy_glyph(g) -- next and prev are untouched !
    local components = getfield(g,"components")
    if components then
        setfield(g,"components")
        local n = copy_node(g)
        copyinjection(n,g) -- we need to preserve the lig indices
        setfield(g,"components",components)
        return n
    else
        local n = copy_node(g)
        copyinjection(n,g) -- we need to preserve the lig indices
        return n
    end
end

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 = phead
    end
    if replace then
        setlink(replacetail,replacehead)
    else
        replace = rhead
    end
    setdisc(disc,pre,post,replace)
end

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

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_glyph(start)
        if head == start then
            head = base
        end
        resetinjection(base)
        setchar(base,char)
        setsubtype(base,ligature_code)
        setfield(base,"components",start)
        setlink(prev,base)
        setlink(base,next)
        return head, base
    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.

local function getcomponentindex(start) -- we could store this offset in the glyph (nofcomponents)
    if getid(start) ~= glyph_code then  -- and then get rid of all components
        return 0
    elseif getsubtype(start) == ligature_code then
        local i = 0
        local components = getfield(start,"components")
        while components do
            i = i + getcomponentindex(components)
            components = getnext(components)
        end
        return i
    elseif not marks[getchar(start)] then
        return 1
    else
        return 0
    end
end

local a_noligature = attributes.private("noligature")

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
    -- needs testing (side effects):
    local components = getfield(start,"components")
    if components then
     -- we get a double free .. needs checking
     -- flush_node_list(components)
    end
    --
    local prev = getprev(start)
    local next = getnext(stop)
    local comp = start
    setprev(start)
    setnext(stop)
    local base = copy_glyph(start)
    if start == head then
        head = base
    end
    resetinjection(base)
    setchar(base,char)
    setsubtype(base,ligature_code)
    setfield(base,"components",comp) -- start can have components ... do we need to flush?
    if prev then
        setnext(prev,base)
    end
    if next then
        setprev(next,base)
    end
    setboth(base,prev,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)
            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)
                setlink(discfound,next)
                setboth(base)
                setfield(base,"components",copied)
                setdisc(discfound,pre,post,base) -- was discretionary_code
                base = prev -- restart
            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 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
        local done = false
        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
                            done = true
                            break
                        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
                    done = true
                    break
                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
                    done = true
                    break
                else -- can't happen
                    break
                end
            else
                break
            end
        end
        return head, start, done
    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 done = false
    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 not done and 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
                            done = true
                        end
                    end
                end
                break
            end
        end
    end
    return head, start, done
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 reportmoresteps(dataset,sequence)
    logwarning("%s: more than 1 step",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
    local current = start
    while current do
        local currentchar = ischar(current)
        if currentchar then
            local replacement = steps[1].coverage[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
    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
    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
    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
    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
    while current do
        local currentchar = ischar(current)
        if currentchar then
            local alternatives = steps[1].coverage[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
    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
    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
    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
    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
    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
    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
            local done = false
            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
                            done = true
                            break
                        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
                    done = true
                    break
                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
                    done = true
                    break
                else
                    break
                end
            end
            return head, start, done
        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
    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
    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
    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
    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
    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
    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
    local startchar   = getchar(start)
    local exitanchors = steps[1].coverage[startchar] -- always 1 step
    if exitanchors then
        local done = false
        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 not done and 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
                                done = true
                                break
                            end
                        end
                    elseif trace_bugs then
                        onetimemessage(currentfont,startchar,"no entry anchors",report_fonts)
                    end
                    break
                end
            end
        end
        return head, start, done
    else
        if trace_cursive and trace_details then
            logprocess("%s, cursive %s is already done",pref(dataset,sequence),gref(getchar(start)),alreadydone)
        end
        return head, start, false
    end
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(getfield(current,"width"))
            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)
                setlink(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 function chaindisk(head,start,last,dataset,sequence,chainlookup,rlmode,k,ck,chainproc)

    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 checkdisc     = getprev(head) -- hm bad name head
    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
                if notmatchpre[current] ~= notmatchreplace[current] then
                    lookaheaddisc = current
                end
                local replace = getfield(current,"replace")
                while replace and i <= l do
                    if getid(replace) == glyph_code then
                        i = i + 1
                    end
                    replace = getnext(replace)
                end
                last    = current
                current = getnext(c)
            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
                    local replace = getfield(c,"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
                    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 ok = 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

        setprev(lookaheaddisc,cprev)
        if cprev then
            setnext(cprev,lookaheaddisc)
        end
        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
        for i=1,insertedmarks do
            cnew = getnext(cnew)
        end
        local clast = cnew
        for i=f,l do
            clast = getnext(clast)
        end
        if not notmatchpre[lookaheaddisc] then
            cf, start, ok = chainproc(cf,start,last,dataset,sequence,chainlookup,rlmode,k)
        end
        if not notmatchreplace[lookaheaddisc] then
            new, cnew, ok = chainproc(new,cnew,clast,dataset,sequence,chainlookup,rlmode,k)
        end
        if pre then
            setlink(cl,pre)
        end
        if replace then
            local tail = find_node_tail(new)
            setlink(tail,replace)
        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(last)

    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
            cf, start, ok = chainproc(cf,start,last,dataset,sequence,chainlookup,rlmode,k)
        end
        if not notmatchreplace[backtrackdisc] then
            new, cnew, ok = chainproc(new,cnew,clast,dataset,sequence,chainlookup,rlmode,k)
        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

        head, start, ok = chainproc(head,start,last,dataset,sequence,chainlookup,rlmode,k)

    end

    return head, start, ok
end

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

local function handle_contextchain(head,start,dataset,sequence,contexts,rlmode)
    local sweepnode    = sweepnode
    local sweeptype    = sweeptype
    local currentfont  = currentfont
    local diskseen     = false
    local checkdisc    = 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
    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
                match = seq[1][char]
            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)
                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 ccd = descriptions[char]
                            if ccd then
                                local class = ccd.class or "base"
                                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
                                        match = not notmatchpre[discfound]
                                    else
                                        match = false
                                    end
                                    break
                                end
                            else
                                if discfound then
                                    notmatchreplace[discfound] = true
                                    match = not notmatchpre[discfound]
                                else
                                    match = false
                                end
                                break
                            end
                        elseif char == false then
                            if discfound then
                                notmatchreplace[discfound] = true
                                match = not notmatchpre[discfound]
                            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
                                        match = not notmatchpre[last]
                                        break
                                    end
                                end
                                match = not notmatchpre[last]
                            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 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 ccd = descriptions[char]
                                    if ccd then
                                        local class = ccd.class
                                        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) -- moved here
                                        elseif seq[n][char] then
                                            if n > 1 then -- new test
                                                prev = getprev(prev) -- moved here
                                            end
                                            n = n - 1
                                        else
                                            if discfound then
                                                notmatchreplace[discfound] = true
                                                match = not notmatchpost[discfound]
                                            else
                                                match = false
                                            end
                                            break
                                        end
                                    else
                                        if discfound then
                                            notmatchreplace[discfound] = true
                                            match = not notmatchpost[discfound]
                                        else
                                            match = false
                                        end
                                        break
                                    end
                                 -- prev = getprev(prev) -- moved up
                                elseif char == false then
                                    if discfound then
                                        notmatchreplace[discfound] = true
                                        match = not notmatchpost[discfound]
                                    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
                                                    match = not notmatchpost[prev]
                                                    break
                                                end
                                            end
                                            if not match then
                                                break
                                            end
                                        end
                                    end
                                    -- maybe only if match
                                    prev = getprev(prev)
                                elseif seq[n][32] and isspace(prev,threshold) 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 then
                    if sweeptype == "post" or sweeptype == "replace" then
                        current   = getnext(sweepnode)
                     -- sweeptype = nil
                    end
                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 ccd = descriptions[char]
                                if ccd then
                                    local class = ccd.class
                                    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
                                            match = not notmatchpre[discfound]
                                        else
                                            match = false
                                        end
                                        break
                                    end
                                else
                                    if discfound then
                                        notmatchreplace[discfound] = true
                                        match = not notmatchpre[discfound]
                                    else
                                        match = false
                                    end
                                    break
                                end
                            elseif char == false then
                                if discfound then
                                    notmatchreplace[discfound] = true
                                    match = not notmatchpre[discfound]
                                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
                                            match = notmatchpre[current]
                                            break
                                        end
                                    end
                                    if not match then
                                        break
                                    end
                                else
                                    -- skip 'm
                                end
                                -- maybe only if match
                                current = getnext(current)
                            elseif seq[n][32] and isspace(current,threshold) 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
            -- Can lookups be of a different type?
            local diskchain = diskseen or sweepnode
            if trace_contexts then
                local rule       = ck[1]
                local lookuptype = ck[8] or ck[2]
                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,s-last,lookuptype)
            end
            local chainlookups = ck[6]
            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]
                    local chainkind   = chainlookup.type
                    local chainproc   = chainprocs[chainkind]
                    if chainproc then
                        local ok
                        if diskchain then
                            head, start, ok = chaindisk(head,start,last,dataset,sequence,chainlookup,rlmode,1,ck,chainproc)
                        else
                            head, start, ok = chainproc(head,start,last,dataset,sequence,chainlookup,rlmode,1)
                        end
                        if ok then
                            done = true
                        end
                    else
                        logprocess("%s: %s is not yet supported (1)",cref(dataset,sequence),chainkind)
                    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
                    --
                    local i = 1
                    while start do
                        if skipped then
                            while start do -- todo: use properties
                                local char = getchar(start)
                                local ccd = descriptions[char]
                                if ccd then
                                    local class = ccd.class or "base"
                                    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
                            local chainkind = chainlookup.type
                            local chainproc = chainprocs[chainkind]
                            if chainproc then
                                local ok, n
                                if diskchain then
                                    head, start, ok    = chaindisk(head,start,last,dataset,sequence,chainlookup,rlmode,i,ck,chainproc)
                                else
                                    head, start, ok, n = chainproc(head,start,last,dataset,sequence,chainlookup,rlmode,i)
                                end
                                -- 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
                        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
            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

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

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

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

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)
        setlink(disc,next)
    end
    return nextstart, done
end

local function comprun(disc,c_run,...)
    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 proeprties, 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

-- A discrun happens when we have a zwnj. We're gpossing so it is unlikely that
-- there has been a match changing the character. Now, as we check again here
-- the question is: why do we do this ... needs checking as drun seems useless
-- ... maybe that code can go away

-- local function discrun(disc,drun,krun)
--     local prev, next = getboth(disc)
--     if trace_discruns then
--        report_disc("disc",disc)
--     end
--     if next and prev then
--         setnext(prev,next)
--      -- setprev(next,prev)
--         drun(prev)
--         setnext(prev,disc)
--      -- setprev(next,disc)
--     end
--     --
--     if krun then -- currently always false
--         local pre = getfield(disc,"pre")
--         if not pre then
--             -- go on
--         elseif prev then
--             local nest = getprev(pre)
--             setlink(prev,pre)
--             krun(prev,"preinjections")
--             setprev(pre,nest)
--             setnext(prev,disc)
--         else
--             krun(pre,"preinjections")
--         end
--     end
--     return next
-- 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 = attr and getattr(start,0)
            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 = attr and getattr(start,0)
            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 = getnext(start)
                    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 = getnext(start)
        else
            break
        end
    end
end

-- local function d_run_single(prev,font,attr,lookupcache,step,dataset,sequence,rlmode,handler)
--     local a = attr and getattr(prev,0)
--     if not a or (a == attr) then
--         local char = ischar(prev) -- can be disc
--         if char then
--             local lookupmatch = lookupcache[char]
--             if lookupmatch then
--                 local h, d, ok = handler(head,start,dataset,sequence,lookupmatch,rlmode,step,1)
--                 if ok then
--                     done = true
--                     success = true
--                 end
--             end
--         end
--     end
-- end

local function k_run_single(sub,injection,last,font,attr,lookupcache,step,dataset,sequence,rlmode,handler)
    local a = attr and getattr(sub,0)
    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 = attr and getattr(start,0)
            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 = attr and getattr(start,0)
            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 = getnext(start)
                            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 = getnext(start)
        else
            break
        end
    end
end

-- local function d_run_multiple(prev,attr,steps,nofsteps,dataset,sequence,rlmode,handler)
--     local a = attr and getattr(prev,0)
--     if not a or (a == attr) then
--         local char = ischar(prev) -- can be disc
--         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
--                         -- we could move all code inline but that makes things even more unreadable
--                         local h, d, ok = handler(head,prev,dataset,sequence,lookupmatch,rlmode,step,i)
--                         if ok then
--                             done = true
--                             break
--                         end
--                     end
--                 else
--                     report_missing_coverage(dataset,sequence)
--                 end
--             end
--         end
--     end
-- end

local function k_run_multiple(sub,injection,last,font,attr,steps,nofsteps,dataset,sequence,rlmode,handler)
    local a = attr and getattr(sub,0)
    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 = getfield(start,"dir")
    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 = getfield(start,"dir")
    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

local function featuresprocessor(head,font,attr)

    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
        characters    = tfmdata.characters
        marks         = tfmdata.resources.marks
        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 rlmode    = 0

    local done      = false
    local datasets  = otf.dataset(tfmdata,font,attr)

    local forcedisc = alwaysdisc or not attr

    local dirstack  = { } -- could move outside function but we can have local runs

    sweephead       = { }

    -- 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.

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

    -- 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    = 0
        local topstack     = 0
        local success      = false
        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
                success = 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)
            while start do
                local char = ischar(start,font)
                if char then
                    local a = attr and getattr(start,0)
                    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
                                        success = 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 = 0 -- to be checked ?
            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
                            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
                                        success = true
                                 -- elseif gpossing and zwnjruns and char == zwnj then
                                 --     discrun(start,d_run,font,attr,lookupcache)
                                    end
                             -- elseif gpossing and zwnjruns and char == zwnj then
                             --     discrun(start,d_run,font,attr,lookupcache)
                                end
                                if start then
                                    start = getnext(start)
                                end
                            else
                               start = getnext(start)
                            end
                        elseif char == false then
                           -- whatever glyph
                           start = getnext(start)
                        elseif id == disc_code then
                            -- ctx: we could assume the same attr as the surrounding glyphs but then we loose
                            -- the option to have interesting disc nodes (we gain upto 10% on extreme tests);
                            -- if a disc would have a font field we could even be more strict (see oldstyle test
                            -- case)
                            local a = forcedisc or getsubtype(start) == discretionary_code or getattr(start,0) == attr
                            if a then
                                --     local attr = false
                                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
                                    success = true
                                end
                            else
                                start = getnext(start)
                            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
                        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
                                            success = true
                                            break
                                        elseif not start then
                                            -- don't ask why ... shouldn't happen
                                            break
                                     -- elseif gpossing and zwnjruns and char == zwnj then
                                     --     discrun(start,d_run,font,attr,steps,nofsteps)
                                        end
                                 -- elseif gpossing and zwnjruns and char == zwnj then
                                 --     discrun(start,d_run,font,attr,steps,nofsteps)
                                    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
                        start = getnext(start)
                    elseif id == disc_code then
                        -- see comment above
                        local a = forcedisc or getsubtype(start) == discretionary_code or getattr(start,0) == attr
                        if a 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
                                success = true
                            end
                        else
                            start = getnext(start)
                        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 success then
            done = true
        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] = v[3]
                                        else
                                            local one = v[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,
    },
}
