I'm struggling to understand the semantics of Chained Contexts Substitutions.
In a feature file of an OpenType font, I have (simplified):
@MarkSet0=[\A \D];
@MarkSet1=[\A \B];
lookup Lookup0 {
lookupflag UseMarkFilteringSet @MarkSet0;
sub \A \D by \C;
} Lookup0;
lookup Lookup1 {
lookupflag 0;
sub \B \B by \E;
} Lookup1;
lookup ligaLookup2 {
lookupflag UseMarkFilteringSet @MarkSet1;
sub [\A]'lookup Lookup0 [\B]'lookup Lookup1;
} ligaLookup2;
(Only ligaLookup2 belongs to a feature.)
I would expect application of both substitution rules in Lookup0 and Lookup1, to get:
ABBD -> CE
But the behaviour I see in OpenType tools (e.g. Crowbar) and in browsers is:
ABBD -> CBB
ABBG -> AEG
Somehow the substitution in Lookup0 that reaches over the BB prevents Lookup1 from changing those BB.
Can this be explained by general rules defining the semantics of Chained Context Substitutions?
Behaviour could be impacted by what data is compiled by the font tool you've used, or how particular layout engines implement an interpretation of the OpenType specification. I can comment on the semantics intended by the OpenType spec.
Based on the feature file syntax in your example, I'm assuming you're using Adobe's Font Development Kit for OpenType. I checked with an Adobe contact for that tool who confirmed the GSUB table data your example would generate, and it matched what I would have expected.
But the display results you mentioned,
ABBD -> CBB, as well as what you expected,ABBD -> CE, are both different from what I would have expected based on the OT spec, and are different from what my Adobe contact observed after trying to reproduce the result building a font with the feature file rules you gave. Rather, what I expected, and what he observed, isABBD -> AED.Let me explain why I expect
ABBD -> AED.You have a parent lookup, ligaLookup2, that specifies a mark filtering set, MarkSet1 (= {A, B}); then two nested lookups: Lookup0 which specifies a different mark filtering set (MarkSet0 = {A, D}), and Lookup1 which doesn't specify any mark filtering. The mark filtering applied to the parent lookup, ligaLookup2, would be used to determine if the glyph sequence in the text data matches the input sequence specified for the lookup, and what the input sequence is that gets processed by the nested lookups. Since MarkSet1 = {A,B}, the matched input sequence is
ABB. When the sequence of nested actions are processed, I would expect them to be operating on that sequence,ABB.The first nested action is Lookup0. It specifies mark filtering using MarkSet0, {A, D}, so given the sequence
ABBit would filter out theBs, leaving a sequence of justA. Lookup0 wants to act on a sequenceAD, but that doesn't match. So the first nested action becomes a no-op.The second nested action is Lookup1. After the first nested action has been evaluated, the interim glyph sequence is (still)
ABB. The data compiled into the font by AFDKO specifies that the action should apply starting at sequence index 1 (base 0); so Lookup1 will be operating on the sub-sequenceBB. That matches, and so the substitutionBB -> Ewould occur. The result after the section nested action is thatABBhas been transformed intoAE.Putting that back into the fuller context,
ABBDwould be transformed intoAED.Again, it's unclear why you might have observed
ABBD -> CBB. To explore further, it would be necessary to examine actual font data that reproduces that behaviour.