1use super::aat_layout::*;
2use super::aat_map::{hb_aat_map_builder_t, hb_aat_map_t, range_flags_t};
3use super::buffer::{hb_buffer_t, UnicodeProps};
4use super::{hb_font_t, hb_glyph_info_t};
5use crate::hb::aat_layout_common::hb_aat_apply_context_t;
6use crate::hb::ot_layout::MAX_CONTEXT_LENGTH;
7use alloc::vec;
8use ttf_parser::{apple_layout, morx, FromData, GlyphId, LazyArray32};
9
10pub fn compile_flags(
14    face: &hb_font_t,
15    builder: &hb_aat_map_builder_t,
16    map: &mut hb_aat_map_t,
17) -> Option<()> {
18    let has_feature = |kind: u16, setting: u16| {
19        builder
20            .current_features
21            .binary_search_by(|probe| {
22                if probe.kind != kind {
23                    probe.kind.cmp(&kind)
24                } else {
25                    probe.setting.cmp(&setting)
26                }
27            })
28            .is_ok()
29    };
30
31    let chains = face.tables().morx.as_ref()?.chains;
32    let chain_len = chains.into_iter().count();
33    map.chain_flags.resize(chain_len, vec![]);
34
35    for (chain, chain_flags) in chains.into_iter().zip(map.chain_flags.iter_mut()) {
36        let mut flags = chain.default_flags;
37        for feature in chain.features {
38            if has_feature(feature.kind, feature.setting) {
42                flags &= feature.disable_flags;
43                flags |= feature.enable_flags;
44            } else if feature.kind == HB_AAT_LAYOUT_FEATURE_TYPE_LETTER_CASE as u16
45                && feature.setting == u16::from(HB_AAT_LAYOUT_FEATURE_SELECTOR_SMALL_CAPS)
46            {
47                let ok = has_feature(
49                    HB_AAT_LAYOUT_FEATURE_TYPE_LOWER_CASE as u16,
50                    u16::from(HB_AAT_LAYOUT_FEATURE_SELECTOR_LOWER_CASE_SMALL_CAPS),
51                );
52                if ok {
53                    flags &= feature.disable_flags;
54                    flags |= feature.enable_flags;
55                }
56            }
57            }
59
60        chain_flags.push(range_flags_t {
61            flags,
62            cluster_first: builder.range_first as u32,
63            cluster_last: builder.range_last as u32,
64        });
65    }
66
67    Some(())
68}
69
70pub fn apply<'a>(c: &mut hb_aat_apply_context_t<'a>, map: &'a mut hb_aat_map_t) -> Option<()> {
72    c.buffer.unsafe_to_concat(None, None);
73
74    let chains = c.face.tables().morx.as_ref()?.chains;
75    let chain_len = chains.into_iter().count();
76    map.chain_flags.resize(chain_len, vec![]);
77
78    for (chain, chain_flags) in chains.into_iter().zip(map.chain_flags.iter_mut()) {
79        c.range_flags = Some(chain_flags.as_mut_slice());
80        for subtable in chain.subtables {
81            if let Some(range_flags) = c.range_flags.as_ref() {
82                if range_flags.len() == 1 && (subtable.feature_flags & range_flags[0].flags == 0) {
83                    continue;
84                }
85            }
86
87            c.subtable_flags = subtable.feature_flags;
88
89            if !subtable.coverage.is_all_directions()
90                && c.buffer.direction.is_vertical() != subtable.coverage.is_vertical()
91            {
92                continue;
93            }
94
95            let reverse = if subtable.coverage.is_logical() {
123                subtable.coverage.is_backwards()
124            } else {
125                subtable.coverage.is_backwards() != c.buffer.direction.is_backward()
126            };
127
128            if reverse {
129                c.buffer.reverse();
130            }
131
132            apply_subtable(&subtable.kind, c);
133
134            if reverse {
135                c.buffer.reverse();
136            }
137        }
138    }
139
140    Some(())
141}
142
143trait driver_context_t<T: FromData> {
144    fn in_place(&self) -> bool;
145    fn can_advance(&self, entry: &apple_layout::GenericStateEntry<T>) -> bool;
146    fn is_actionable(
147        &self,
148        entry: &apple_layout::GenericStateEntry<T>,
149        buffer: &hb_buffer_t,
150    ) -> bool;
151    fn transition(
152        &mut self,
153        entry: &apple_layout::GenericStateEntry<T>,
154        buffer: &mut hb_buffer_t,
155    ) -> Option<()>;
156}
157
158const START_OF_TEXT: u16 = 0;
159
160fn drive<T: FromData>(
161    machine: &apple_layout::ExtendedStateTable<T>,
162    c: &mut dyn driver_context_t<T>,
163    ac: &mut hb_aat_apply_context_t,
164) {
165    if !c.in_place() {
166        ac.buffer.clear_output();
167    }
168
169    let mut state = START_OF_TEXT;
170    let mut last_range = ac.range_flags.as_ref().and_then(|rf| {
171        if rf.len() > 1 {
172            rf.first().map(|_| 0usize)
173        } else {
174            None
176        }
177    });
178    ac.buffer.idx = 0;
179    loop {
180        if let Some(range_flags) = ac.range_flags.as_ref() {
182            if let Some(last_range) = last_range.as_mut() {
183                let mut range = *last_range;
184                if ac.buffer.idx < ac.buffer.len {
185                    let cluster = ac.buffer.cur(0).cluster;
186                    while cluster < range_flags[range].cluster_first {
187                        range -= 1;
188                    }
189
190                    while cluster > range_flags[range].cluster_last {
191                        range += 1;
192                    }
193
194                    *last_range = range;
195                }
196
197                if range_flags[range].flags & ac.subtable_flags == 0 {
198                    if ac.buffer.idx == ac.buffer.len || !ac.buffer.successful {
199                        break;
200                    }
201
202                    state = START_OF_TEXT;
203
204                    ac.buffer.next_glyph();
205                    continue;
206                }
207            }
208        }
209
210        let class = if ac.buffer.idx < ac.buffer.len {
211            machine.class(ac.buffer.cur(0).as_glyph()).unwrap_or(1)
212        } else {
213            u16::from(apple_layout::class::END_OF_TEXT)
214        };
215
216        let entry: apple_layout::GenericStateEntry<T> = match machine.entry(state, class) {
217            Some(v) => v,
218            None => break,
219        };
220
221        let next_state = entry.new_state;
222
223        let is_safe_to_break_extra = || {
252            let wouldbe_entry = match machine.entry(START_OF_TEXT, class) {
254                Some(v) => v,
255                None => return false,
256            };
257
258            if c.is_actionable(&wouldbe_entry, ac.buffer) {
260                return false;
261            }
262
263            next_state == wouldbe_entry.new_state
265                && c.can_advance(&entry) == c.can_advance(&wouldbe_entry)
266        };
267
268        let is_safe_to_break = || {
269            if c.is_actionable(&entry, ac.buffer) {
271                return false;
272            }
273
274            let ok = state == START_OF_TEXT
276                || (!c.can_advance(&entry) && next_state == START_OF_TEXT)
277                || is_safe_to_break_extra();
278            if !ok {
279                return false;
280            }
281
282            let end_entry = match machine.entry(state, u16::from(apple_layout::class::END_OF_TEXT))
284            {
285                Some(v) => v,
286                None => return false,
287            };
288            !c.is_actionable(&end_entry, ac.buffer)
289        };
290
291        if !is_safe_to_break() && ac.buffer.backtrack_len() > 0 && ac.buffer.idx < ac.buffer.len {
292            ac.buffer.unsafe_to_break_from_outbuffer(
293                Some(ac.buffer.backtrack_len() - 1),
294                Some(ac.buffer.idx + 1),
295            );
296        }
297
298        c.transition(&entry, ac.buffer);
299
300        state = next_state;
301
302        if ac.buffer.idx >= ac.buffer.len || !ac.buffer.successful {
303            break;
304        }
305
306        if c.can_advance(&entry) {
307            ac.buffer.next_glyph();
308        } else {
309            if ac.buffer.max_ops <= 0 {
310                ac.buffer.next_glyph();
311            }
312            ac.buffer.max_ops -= 1;
313        }
314    }
315
316    if !c.in_place() {
317        ac.buffer.sync();
318    }
319}
320
321fn apply_subtable(kind: &morx::SubtableKind, ac: &mut hb_aat_apply_context_t) {
322    match kind {
323        morx::SubtableKind::Rearrangement(ref table) => {
324            let mut c = RearrangementCtx { start: 0, end: 0 };
325
326            drive::<()>(table, &mut c, ac);
327        }
328        morx::SubtableKind::Contextual(ref table) => {
329            let mut c = ContextualCtx {
330                mark_set: false,
331                face_if_has_glyph_classes:
332                    matches!(ac.face.tables().gdef, Some(gdef) if gdef.has_glyph_classes())
333                        .then_some(ac.face),
334                mark: 0,
335                table,
336            };
337
338            drive::<morx::ContextualEntryData>(&table.state, &mut c, ac);
339        }
340        morx::SubtableKind::Ligature(ref table) => {
341            let mut c = LigatureCtx {
342                table,
343                match_length: 0,
344                match_positions: [0; LIGATURE_MAX_MATCHES],
345            };
346
347            drive::<u16>(&table.state, &mut c, ac);
348        }
349        morx::SubtableKind::NonContextual(ref lookup) => {
350            let face_if_has_glyph_classes =
351                matches!(ac.face.tables().gdef, Some(gdef) if gdef.has_glyph_classes())
352                    .then_some(ac.face);
353
354            let mut last_range = ac.range_flags.as_ref().and_then(|rf| {
355                if rf.len() > 1 {
356                    rf.first().map(|_| 0usize)
357                } else {
358                    None
360                }
361            });
362
363            for info in 0..ac.buffer.len {
364                if let Some(range_flags) = ac.range_flags.as_ref() {
366                    if let Some(last_range) = last_range.as_mut() {
367                        let mut range = *last_range;
368                        if ac.buffer.idx < ac.buffer.len {
369                            let cluster = ac.buffer.cur(0).cluster;
371                            while cluster < range_flags[range].cluster_first {
372                                range -= 1;
373                            }
374
375                            while cluster > range_flags[range].cluster_last {
376                                range += 1;
377                            }
378
379                            *last_range = range;
380                        }
381
382                        if range_flags[range].flags & ac.subtable_flags == 0 {
383                            continue;
384                        }
385                    }
386                }
387
388                let info = &mut ac.buffer.info[info];
389                if let Some(replacement) = lookup.value(info.as_glyph()) {
390                    info.glyph_id = u32::from(replacement);
391                    if let Some(face) = face_if_has_glyph_classes {
392                        info.set_glyph_props(face.glyph_props(GlyphId(replacement)));
393                    }
394                }
395            }
396        }
397        morx::SubtableKind::Insertion(ref table) => {
398            let mut c = InsertionCtx {
399                mark: 0,
400                glyphs: table.glyphs,
401            };
402
403            drive::<morx::InsertionEntryData>(&table.state, &mut c, ac);
404        }
405    }
406}
407
408struct RearrangementCtx {
409    start: usize,
410    end: usize,
411}
412
413impl RearrangementCtx {
414    const MARK_FIRST: u16 = 0x8000;
415    const DONT_ADVANCE: u16 = 0x4000;
416    const MARK_LAST: u16 = 0x2000;
417    const VERB: u16 = 0x000F;
418}
419
420impl driver_context_t<()> for RearrangementCtx {
421    fn in_place(&self) -> bool {
422        true
423    }
424
425    fn can_advance(&self, entry: &apple_layout::GenericStateEntry<()>) -> bool {
426        entry.flags & Self::DONT_ADVANCE == 0
427    }
428
429    fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<()>, _: &hb_buffer_t) -> bool {
430        entry.flags & Self::VERB != 0 && self.start < self.end
431    }
432
433    fn transition(
434        &mut self,
435        entry: &apple_layout::GenericStateEntry<()>,
436        buffer: &mut hb_buffer_t,
437    ) -> Option<()> {
438        let flags = entry.flags;
439
440        if flags & Self::MARK_FIRST != 0 {
441            self.start = buffer.idx;
442        }
443
444        if flags & Self::MARK_LAST != 0 {
445            self.end = (buffer.idx + 1).min(buffer.len);
446        }
447
448        if flags & Self::VERB != 0 && self.start < self.end {
449            const MAP: [u8; 16] = [
454                0x00, 0x10, 0x01, 0x11, 0x20, 0x30, 0x02, 0x03, 0x12, 0x13, 0x21, 0x31, 0x22, 0x32, 0x23, 0x33, ];
471
472            let m = MAP[usize::from(flags & Self::VERB)];
473            let l = 2.min(m >> 4) as usize;
474            let r = 2.min(m & 0x0F) as usize;
475            let reverse_l = 3 == (m >> 4);
476            let reverse_r = 3 == (m & 0x0F);
477
478            if (self.end - self.start >= l + r) && (self.end - self.start <= MAX_CONTEXT_LENGTH) {
479                buffer.merge_clusters(self.start, (buffer.idx + 1).min(buffer.len));
480                buffer.merge_clusters(self.start, self.end);
481
482                let mut buf = [hb_glyph_info_t::default(); 4];
483
484                for (i, glyph_info) in buf[..l].iter_mut().enumerate() {
485                    *glyph_info = buffer.info[self.start + i];
486                }
487
488                for i in 0..r {
489                    buf[i + 2] = buffer.info[self.end - r + i];
490                }
491
492                if l > r {
493                    for i in 0..(self.end - self.start - l - r) {
494                        buffer.info[self.start + r + i] = buffer.info[self.start + l + i];
495                    }
496                } else if l < r {
497                    for i in (0..(self.end - self.start - l - r)).rev() {
498                        buffer.info[self.start + r + i] = buffer.info[self.start + l + i];
499                    }
500                }
501
502                for i in 0..r {
503                    buffer.info[self.start + i] = buf[2 + i];
504                }
505
506                for i in 0..l {
507                    buffer.info[self.end - l + i] = buf[i];
508                }
509
510                if reverse_l {
511                    buffer.info.swap(self.end - 1, self.end - 2);
512                }
513
514                if reverse_r {
515                    buffer.info.swap(self.start, self.start + 1);
516                }
517            }
518        }
519
520        Some(())
521    }
522}
523
524struct ContextualCtx<'a> {
525    mark_set: bool,
526    face_if_has_glyph_classes: Option<&'a hb_font_t<'a>>,
527    mark: usize,
528    table: &'a morx::ContextualSubtable<'a>,
529}
530
531impl ContextualCtx<'_> {
532    const SET_MARK: u16 = 0x8000;
533    const DONT_ADVANCE: u16 = 0x4000;
534}
535
536impl driver_context_t<morx::ContextualEntryData> for ContextualCtx<'_> {
537    fn in_place(&self) -> bool {
538        true
539    }
540
541    fn can_advance(
542        &self,
543        entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
544    ) -> bool {
545        entry.flags & Self::DONT_ADVANCE == 0
546    }
547
548    fn is_actionable(
549        &self,
550        entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
551        buffer: &hb_buffer_t,
552    ) -> bool {
553        if buffer.idx == buffer.len && !self.mark_set {
554            return false;
555        }
556
557        entry.extra.mark_index != 0xFFFF || entry.extra.current_index != 0xFFFF
558    }
559
560    fn transition(
561        &mut self,
562        entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
563        buffer: &mut hb_buffer_t,
564    ) -> Option<()> {
565        if buffer.idx == buffer.len && !self.mark_set {
568            return Some(());
569        }
570
571        let mut replacement = None;
572
573        if entry.extra.mark_index != 0xFFFF {
574            let lookup = self.table.lookup(u32::from(entry.extra.mark_index))?;
575            replacement = lookup.value(buffer.info[self.mark].as_glyph());
576        }
577
578        if let Some(replacement) = replacement {
579            buffer.unsafe_to_break(Some(self.mark), Some((buffer.idx + 1).min(buffer.len)));
580            buffer.info[self.mark].glyph_id = u32::from(replacement);
581
582            if let Some(face) = self.face_if_has_glyph_classes {
583                buffer.info[self.mark].set_glyph_props(face.glyph_props(GlyphId(replacement)));
584            }
585        }
586
587        replacement = None;
588        let idx = buffer.idx.min(buffer.len - 1);
589        if entry.extra.current_index != 0xFFFF {
590            let lookup = self.table.lookup(u32::from(entry.extra.current_index))?;
591            replacement = lookup.value(buffer.info[idx].as_glyph());
592        }
593
594        if let Some(replacement) = replacement {
595            buffer.info[idx].glyph_id = u32::from(replacement);
596
597            if let Some(face) = self.face_if_has_glyph_classes {
598                buffer.info[self.mark].set_glyph_props(face.glyph_props(GlyphId(replacement)));
599            }
600        }
601
602        if entry.flags & Self::SET_MARK != 0 {
603            self.mark_set = true;
604            self.mark = buffer.idx;
605        }
606
607        Some(())
608    }
609}
610
611struct InsertionCtx<'a> {
612    mark: u32,
613    glyphs: LazyArray32<'a, GlyphId>,
614}
615
616impl InsertionCtx<'_> {
617    const SET_MARK: u16 = 0x8000;
618    const DONT_ADVANCE: u16 = 0x4000;
619    const CURRENT_INSERT_BEFORE: u16 = 0x0800;
620    const MARKED_INSERT_BEFORE: u16 = 0x0400;
621    const CURRENT_INSERT_COUNT: u16 = 0x03E0;
622    const MARKED_INSERT_COUNT: u16 = 0x001F;
623}
624
625impl driver_context_t<morx::InsertionEntryData> for InsertionCtx<'_> {
626    fn in_place(&self) -> bool {
627        false
628    }
629
630    fn can_advance(
631        &self,
632        entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
633    ) -> bool {
634        entry.flags & Self::DONT_ADVANCE == 0
635    }
636
637    fn is_actionable(
638        &self,
639        entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
640        _: &hb_buffer_t,
641    ) -> bool {
642        (entry.flags & (Self::CURRENT_INSERT_COUNT | Self::MARKED_INSERT_COUNT) != 0)
643            && (entry.extra.current_insert_index != 0xFFFF
644                || entry.extra.marked_insert_index != 0xFFFF)
645    }
646
647    fn transition(
648        &mut self,
649        entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
650        buffer: &mut hb_buffer_t,
651    ) -> Option<()> {
652        let flags = entry.flags;
653        let mark_loc = buffer.out_len;
654
655        if entry.extra.marked_insert_index != 0xFFFF {
656            let count = flags & Self::MARKED_INSERT_COUNT;
657            buffer.max_ops -= i32::from(count);
658            if buffer.max_ops <= 0 {
659                return Some(());
660            }
661
662            let start = entry.extra.marked_insert_index;
663            let before = flags & Self::MARKED_INSERT_BEFORE != 0;
664
665            let end = buffer.out_len;
666            buffer.move_to(self.mark as usize);
667
668            if buffer.idx < buffer.len && !before {
669                buffer.copy_glyph();
670            }
671
672            for i in 0..count {
674                let i = u32::from(start + i);
675                buffer.output_glyph(u32::from(self.glyphs.get(i)?.0));
676            }
677
678            if buffer.idx < buffer.len && !before {
679                buffer.skip_glyph();
680            }
681
682            buffer.move_to(end + usize::from(count));
683
684            buffer.unsafe_to_break_from_outbuffer(
685                Some(self.mark as usize),
686                Some((buffer.idx + 1).min(buffer.len)),
687            );
688        }
689
690        if flags & Self::SET_MARK != 0 {
691            self.mark = mark_loc as u32;
692        }
693
694        if entry.extra.current_insert_index != 0xFFFF {
695            let count = (flags & Self::CURRENT_INSERT_COUNT) >> 5;
696            buffer.max_ops -= i32::from(count);
697            if buffer.max_ops < 0 {
698                return Some(());
699            }
700
701            let start = entry.extra.current_insert_index;
702            let before = flags & Self::CURRENT_INSERT_BEFORE != 0;
703            let end = buffer.out_len;
704
705            if buffer.idx < buffer.len && !before {
706                buffer.copy_glyph();
707            }
708
709            for i in 0..count {
711                let i = u32::from(start + i);
712                buffer.output_glyph(u32::from(self.glyphs.get(i)?.0));
713            }
714
715            if buffer.idx < buffer.len && !before {
716                buffer.skip_glyph();
717            }
718
719            buffer.move_to(if flags & Self::DONT_ADVANCE != 0 {
734                end
735            } else {
736                end + usize::from(count)
737            });
738        }
739
740        Some(())
741    }
742}
743
744const LIGATURE_MAX_MATCHES: usize = 64;
745
746struct LigatureCtx<'a> {
747    table: &'a morx::LigatureSubtable<'a>,
748    match_length: usize,
749    match_positions: [usize; LIGATURE_MAX_MATCHES],
750}
751
752impl LigatureCtx<'_> {
753    const SET_COMPONENT: u16 = 0x8000;
754    const DONT_ADVANCE: u16 = 0x4000;
755    const PERFORM_ACTION: u16 = 0x2000;
756
757    const LIG_ACTION_LAST: u32 = 0x80000000;
758    const LIG_ACTION_STORE: u32 = 0x40000000;
759    const LIG_ACTION_OFFSET: u32 = 0x3FFFFFFF;
760}
761
762impl driver_context_t<u16> for LigatureCtx<'_> {
763    fn in_place(&self) -> bool {
764        false
765    }
766
767    fn can_advance(&self, entry: &apple_layout::GenericStateEntry<u16>) -> bool {
768        entry.flags & Self::DONT_ADVANCE == 0
769    }
770
771    fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<u16>, _: &hb_buffer_t) -> bool {
772        entry.flags & Self::PERFORM_ACTION != 0
773    }
774
775    fn transition(
776        &mut self,
777        entry: &apple_layout::GenericStateEntry<u16>,
778        buffer: &mut hb_buffer_t,
779    ) -> Option<()> {
780        if entry.flags & Self::SET_COMPONENT != 0 {
781            if self.match_length != 0
783                && self.match_positions[(self.match_length - 1) % LIGATURE_MAX_MATCHES]
784                    == buffer.out_len
785            {
786                self.match_length -= 1;
787            }
788
789            self.match_positions[self.match_length % LIGATURE_MAX_MATCHES] = buffer.out_len;
790            self.match_length += 1;
791        }
792
793        if entry.flags & Self::PERFORM_ACTION != 0 {
794            let end = buffer.out_len;
795
796            if self.match_length == 0 {
797                return Some(());
798            }
799
800            if buffer.idx >= buffer.len {
801                return Some(()); }
803
804            let mut cursor = self.match_length;
805
806            let mut ligature_actions_index = entry.extra;
807            let mut ligature_idx = 0;
808            loop {
809                if cursor == 0 {
810                    self.match_length = 0;
812                    break;
813                }
814
815                cursor -= 1;
816                buffer.move_to(self.match_positions[cursor % LIGATURE_MAX_MATCHES]);
817
818                let action = match self
821                    .table
822                    .ligature_actions
823                    .get(u32::from(ligature_actions_index))
824                {
825                    Some(v) => v,
826                    None => break,
827                };
828
829                let mut uoffset = action & Self::LIG_ACTION_OFFSET;
830                if uoffset & 0x20000000 != 0 {
831                    uoffset |= 0xC0000000; }
833
834                let offset = uoffset as i32;
835                let component_idx = (buffer.cur(0).glyph_id as i32 + offset) as u32;
836                ligature_idx += match self.table.components.get(component_idx) {
837                    Some(v) => v,
838                    None => break,
839                };
840
841                if (action & (Self::LIG_ACTION_STORE | Self::LIG_ACTION_LAST)) != 0 {
842                    let lig = match self.table.ligatures.get(u32::from(ligature_idx)) {
843                        Some(v) => v,
844                        None => break,
845                    };
846
847                    buffer.replace_glyph(u32::from(lig.0));
848
849                    let lig_end =
850                        self.match_positions[(self.match_length - 1) % LIGATURE_MAX_MATCHES] + 1;
851                    while self.match_length - 1 > cursor {
853                        self.match_length -= 1;
854                        buffer.move_to(
855                            self.match_positions[self.match_length % LIGATURE_MAX_MATCHES],
856                        );
857                        let cur_unicode = buffer.cur(0).unicode_props();
858                        buffer
859                            .cur_mut(0)
860                            .set_unicode_props(cur_unicode | UnicodeProps::IGNORABLE.bits());
861                        buffer.replace_glyph(0xFFFF);
862                    }
863
864                    buffer.move_to(lig_end);
865                    buffer.merge_out_clusters(
866                        self.match_positions[cursor % LIGATURE_MAX_MATCHES],
867                        buffer.out_len,
868                    );
869                }
870
871                ligature_actions_index += 1;
872
873                if action & Self::LIG_ACTION_LAST != 0 {
874                    break;
875                }
876            }
877
878            buffer.move_to(end);
879        }
880
881        Some(())
882    }
883}