1use ttf_parser::{apple_layout, morx, FromData, GlyphId, LazyArray32};
2
3use crate::aat::feature_selector;
4use crate::aat::{FeatureType, Map, MapBuilder};
5use crate::buffer::Buffer;
6use crate::plan::ShapePlan;
7use crate::{Face, GlyphInfo};
8
9pub fn compile_flags(face: &Face, builder: &MapBuilder) -> Option<Map> {
10 let mut map = Map::default();
11
12 for chain in face.tables().morx.as_ref()?.chains {
13 let mut flags = chain.default_flags;
14 for feature in chain.features {
15 // Check whether this type/setting pair was requested in the map,
16 // and if so, apply its flags.
17
18 if builder.has_feature(feature.kind, feature.setting) {
19 flags &= feature.disable_flags;
20 flags |= feature.enable_flags;
21 } else if feature.kind == FeatureType::LetterCase as u16
22 && feature.setting == u16::from(feature_selector::SMALL_CAPS)
23 {
24 // Deprecated. https://github.com/harfbuzz/harfbuzz/issues/1342
25 let ok = builder.has_feature(
26 FeatureType::LowerCase as u16,
27 u16::from(feature_selector::LOWER_CASE_SMALL_CAPS),
28 );
29 if ok {
30 flags &= feature.disable_flags;
31 flags |= feature.enable_flags;
32 }
33 }
34 }
35
36 map.chain_flags.push(flags);
37 }
38
39 Some(map)
40}
41
42pub fn apply(plan: &ShapePlan, face: &Face, buffer: &mut Buffer) -> Option<()> {
43 for (chain_idx, chain) in face.tables().morx.as_ref()?.chains.into_iter().enumerate() {
44 let flags = plan.aat_map.chain_flags[chain_idx];
45 for subtable in chain.subtables {
46 if subtable.feature_flags & flags == 0 {
47 continue;
48 }
49
50 if !subtable.coverage.is_all_directions()
51 && buffer.direction.is_vertical() != subtable.coverage.is_vertical()
52 {
53 continue;
54 }
55
56 // Buffer contents is always in logical direction. Determine if
57 // we need to reverse before applying this subtable. We reverse
58 // back after if we did reverse indeed.
59 //
60 // Quoting the spec:
61 // """
62 // Bits 28 and 30 of the coverage field control the order in which
63 // glyphs are processed when the subtable is run by the layout engine.
64 // Bit 28 is used to indicate if the glyph processing direction is
65 // the same as logical order or layout order. Bit 30 is used to
66 // indicate whether glyphs are processed forwards or backwards within
67 // that order.
68 //
69 // Bit 30 Bit 28 Interpretation for Horizontal Text
70 // 0 0 The subtable is processed in layout order
71 // (the same order as the glyphs, which is
72 // always left-to-right).
73 // 1 0 The subtable is processed in reverse layout order
74 // (the order opposite that of the glyphs, which is
75 // always right-to-left).
76 // 0 1 The subtable is processed in logical order
77 // (the same order as the characters, which may be
78 // left-to-right or right-to-left).
79 // 1 1 The subtable is processed in reverse logical order
80 // (the order opposite that of the characters, which
81 // may be right-to-left or left-to-right).
82
83 let reverse = if subtable.coverage.is_logical() {
84 subtable.coverage.is_backwards()
85 } else {
86 subtable.coverage.is_backwards() != buffer.direction.is_backward()
87 };
88
89 if reverse {
90 buffer.reverse();
91 }
92
93 apply_subtable(&subtable.kind, buffer, face);
94
95 if reverse {
96 buffer.reverse();
97 }
98 }
99 }
100
101 Some(())
102}
103
104trait Driver<T: FromData> {
105 fn in_place(&self) -> bool;
106 fn can_advance(&self, entry: &apple_layout::GenericStateEntry<T>) -> bool;
107 fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<T>, buffer: &Buffer) -> bool;
108 fn transition(
109 &mut self,
110 entry: &apple_layout::GenericStateEntry<T>,
111 buffer: &mut Buffer,
112 ) -> Option<()>;
113}
114
115const START_OF_TEXT: u16 = 0;
116
117fn drive<T: FromData>(
118 machine: &apple_layout::ExtendedStateTable<T>,
119 c: &mut dyn Driver<T>,
120 buffer: &mut Buffer,
121) {
122 if !c.in_place() {
123 buffer.clear_output();
124 }
125
126 let mut state = START_OF_TEXT;
127 buffer.idx = 0;
128 loop {
129 let class = if buffer.idx < buffer.len {
130 machine
131 .class(buffer.info[buffer.idx].as_glyph())
132 .unwrap_or(1)
133 } else {
134 u16::from(apple_layout::class::END_OF_TEXT)
135 };
136
137 let entry: apple_layout::GenericStateEntry<T> = match machine.entry(state, class) {
138 Some(v) => v,
139 None => break,
140 };
141
142 let next_state = entry.new_state;
143
144 // Conditions under which it's guaranteed safe-to-break before current glyph:
145 //
146 // 1. There was no action in this transition; and
147 //
148 // 2. If we break before current glyph, the results will be the same. That
149 // is guaranteed if:
150 //
151 // 2a. We were already in start-of-text state; or
152 //
153 // 2b. We are epsilon-transitioning to start-of-text state; or
154 //
155 // 2c. Starting from start-of-text state seeing current glyph:
156 //
157 // 2c'. There won't be any actions; and
158 //
159 // 2c". We would end up in the same state that we were going to end up
160 // in now, including whether epsilon-transitioning.
161 //
162 // and
163 //
164 // 3. If we break before current glyph, there won't be any end-of-text action
165 // after previous glyph.
166 //
167 // This triples the transitions we need to look up, but is worth returning
168 // granular unsafe-to-break results. See eg.:
169 //
170 // https://github.com/harfbuzz/harfbuzz/issues/2860
171
172 let is_safe_to_break_extra = || {
173 // 2c
174 let wouldbe_entry = match machine.entry(START_OF_TEXT, class) {
175 Some(v) => v,
176 None => return false,
177 };
178
179 // 2c'
180 if c.is_actionable(&wouldbe_entry, &buffer) {
181 return false;
182 }
183
184 // 2c"
185 return next_state == wouldbe_entry.new_state
186 && c.can_advance(&entry) == c.can_advance(&wouldbe_entry);
187 };
188
189 let is_safe_to_break = || {
190 // 1
191 if c.is_actionable(&entry, &buffer) {
192 return false;
193 }
194
195 // 2
196 let ok = state == START_OF_TEXT
197 || (!c.can_advance(&entry) && next_state == START_OF_TEXT)
198 || is_safe_to_break_extra();
199 if !ok {
200 return false;
201 }
202
203 // 3
204 let end_entry = match machine.entry(state, u16::from(apple_layout::class::END_OF_TEXT))
205 {
206 Some(v) => v,
207 None => return false,
208 };
209 return !c.is_actionable(&end_entry, &buffer);
210 };
211
212 if !is_safe_to_break() && buffer.backtrack_len() > 0 && buffer.idx < buffer.len {
213 buffer.unsafe_to_break_from_outbuffer(buffer.backtrack_len() - 1, buffer.idx + 1);
214 }
215
216 c.transition(&entry, buffer);
217
218 state = next_state;
219
220 if buffer.idx >= buffer.len || !buffer.successful {
221 break;
222 }
223
224 if c.can_advance(&entry) {
225 buffer.next_glyph();
226 } else {
227 if buffer.max_ops <= 0 {
228 buffer.next_glyph();
229 }
230 buffer.max_ops -= 1;
231 }
232 }
233
234 if !c.in_place() {
235 buffer.swap_buffers();
236 }
237}
238
239fn apply_subtable(kind: &morx::SubtableKind, buffer: &mut Buffer, face: &Face) {
240 match kind {
241 morx::SubtableKind::Rearrangement(ref table) => {
242 let mut c = RearrangementCtx { start: 0, end: 0 };
243
244 drive::<()>(table, &mut c, buffer);
245 }
246 morx::SubtableKind::Contextual(ref table) => {
247 let mut c = ContextualCtx {
248 mark_set: false,
249 face_if_has_glyph_classes:
250 matches!(face.tables().gdef, Some(gdef) if gdef.has_glyph_classes())
251 .then_some(face),
252 mark: 0,
253 table,
254 };
255
256 drive::<morx::ContextualEntryData>(&table.state, &mut c, buffer);
257 }
258 morx::SubtableKind::Ligature(ref table) => {
259 let mut c = LigatureCtx {
260 table,
261 match_length: 0,
262 match_positions: [0; LIGATURE_MAX_MATCHES],
263 };
264
265 drive::<u16>(&table.state, &mut c, buffer);
266 }
267 morx::SubtableKind::NonContextual(ref lookup) => {
268 let face_if_has_glyph_classes =
269 matches!(face.tables().gdef, Some(gdef) if gdef.has_glyph_classes())
270 .then_some(face);
271 for info in &mut buffer.info {
272 if let Some(replacement) = lookup.value(info.as_glyph()) {
273 info.glyph_id = u32::from(replacement);
274 if let Some(face) = face_if_has_glyph_classes {
275 info.set_glyph_props(face.glyph_props(GlyphId(replacement)));
276 }
277 }
278 }
279 }
280 morx::SubtableKind::Insertion(ref table) => {
281 let mut c = InsertionCtx {
282 mark: 0,
283 glyphs: table.glyphs,
284 };
285
286 drive::<morx::InsertionEntryData>(&table.state, &mut c, buffer);
287 }
288 }
289}
290
291struct RearrangementCtx {
292 start: usize,
293 end: usize,
294}
295
296impl RearrangementCtx {
297 const MARK_FIRST: u16 = 0x8000;
298 const DONT_ADVANCE: u16 = 0x4000;
299 const MARK_LAST: u16 = 0x2000;
300 const VERB: u16 = 0x000F;
301}
302
303impl Driver<()> for RearrangementCtx {
304 fn in_place(&self) -> bool {
305 true
306 }
307
308 fn can_advance(&self, entry: &apple_layout::GenericStateEntry<()>) -> bool {
309 entry.flags & Self::DONT_ADVANCE == 0
310 }
311
312 fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<()>, _: &Buffer) -> bool {
313 entry.flags & Self::VERB != 0 && self.start < self.end
314 }
315
316 fn transition(
317 &mut self,
318 entry: &apple_layout::GenericStateEntry<()>,
319 buffer: &mut Buffer,
320 ) -> Option<()> {
321 let flags = entry.flags;
322
323 if flags & Self::MARK_FIRST != 0 {
324 self.start = buffer.idx;
325 }
326
327 if flags & Self::MARK_LAST != 0 {
328 self.end = (buffer.idx + 1).min(buffer.len);
329 }
330
331 if flags & Self::VERB != 0 && self.start < self.end {
332 // The following map has two nibbles, for start-side
333 // and end-side. Values of 0,1,2 mean move that many
334 // to the other side. Value of 3 means move 2 and
335 // flip them.
336 const MAP: [u8; 16] = [
337 0x00, // 0 no change
338 0x10, // 1 Ax => xA
339 0x01, // 2 xD => Dx
340 0x11, // 3 AxD => DxA
341 0x20, // 4 ABx => xAB
342 0x30, // 5 ABx => xBA
343 0x02, // 6 xCD => CDx
344 0x03, // 7 xCD => DCx
345 0x12, // 8 AxCD => CDxA
346 0x13, // 9 AxCD => DCxA
347 0x21, // 10 ABxD => DxAB
348 0x31, // 11 ABxD => DxBA
349 0x22, // 12 ABxCD => CDxAB
350 0x32, // 13 ABxCD => CDxBA
351 0x23, // 14 ABxCD => DCxAB
352 0x33, // 15 ABxCD => DCxBA
353 ];
354
355 let m = MAP[usize::from(flags & Self::VERB)];
356 let l = 2.min(m >> 4) as usize;
357 let r = 2.min(m & 0x0F) as usize;
358 let reverse_l = 3 == (m >> 4);
359 let reverse_r = 3 == (m & 0x0F);
360
361 if self.end - self.start >= l + r {
362 buffer.merge_clusters(self.start, (buffer.idx + 1).min(buffer.len));
363 buffer.merge_clusters(self.start, self.end);
364
365 let mut buf = [GlyphInfo::default(); 4];
366
367 for (i, glyph_info) in buf[..l].iter_mut().enumerate() {
368 *glyph_info = buffer.info[self.start + i];
369 }
370
371 for i in 0..r {
372 buf[i + 2] = buffer.info[self.end - r + i];
373 }
374
375 if l > r {
376 for i in 0..(self.end - self.start - l - r) {
377 buffer.info[self.start + r + i] = buffer.info[self.start + l + i];
378 }
379 } else if l < r {
380 for i in (0..(self.end - self.start - l - r)).rev() {
381 buffer.info[self.start + r + i] = buffer.info[self.start + l + i];
382 }
383 }
384
385 for i in 0..r {
386 buffer.info[self.start + i] = buf[2 + i];
387 }
388
389 for i in 0..l {
390 buffer.info[self.end - l + i] = buf[i];
391 }
392
393 if reverse_l {
394 buffer.info.swap(self.end - 1, self.end - 2);
395 }
396
397 if reverse_r {
398 buffer.info.swap(self.start, self.start + 1);
399 }
400 }
401 }
402
403 Some(())
404 }
405}
406
407struct ContextualCtx<'a> {
408 mark_set: bool,
409 face_if_has_glyph_classes: Option<&'a Face<'a>>,
410 mark: usize,
411 table: &'a morx::ContextualSubtable<'a>,
412}
413
414impl ContextualCtx<'_> {
415 const SET_MARK: u16 = 0x8000;
416 const DONT_ADVANCE: u16 = 0x4000;
417}
418
419impl Driver<morx::ContextualEntryData> for ContextualCtx<'_> {
420 fn in_place(&self) -> bool {
421 true
422 }
423
424 fn can_advance(
425 &self,
426 entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
427 ) -> bool {
428 entry.flags & Self::DONT_ADVANCE == 0
429 }
430
431 fn is_actionable(
432 &self,
433 entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
434 buffer: &Buffer,
435 ) -> bool {
436 if buffer.idx == buffer.len && !self.mark_set {
437 return false;
438 }
439
440 entry.extra.mark_index != 0xFFFF || entry.extra.current_index != 0xFFFF
441 }
442
443 fn transition(
444 &mut self,
445 entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
446 buffer: &mut Buffer,
447 ) -> Option<()> {
448 // Looks like CoreText applies neither mark nor current substitution for
449 // end-of-text if mark was not explicitly set.
450 if buffer.idx == buffer.len && !self.mark_set {
451 return Some(());
452 }
453
454 let mut replacement = None;
455
456 if entry.extra.mark_index != 0xFFFF {
457 let lookup = self.table.lookup(u32::from(entry.extra.mark_index))?;
458 replacement = lookup.value(buffer.info[self.mark].as_glyph());
459 }
460
461 if let Some(replacement) = replacement {
462 buffer.unsafe_to_break(self.mark, (buffer.idx + 1).min(buffer.len));
463 buffer.info[self.mark].glyph_id = u32::from(replacement);
464
465 if let Some(face) = self.face_if_has_glyph_classes {
466 buffer.info[self.mark].set_glyph_props(face.glyph_props(GlyphId(replacement)));
467 }
468 }
469
470 replacement = None;
471 let idx = buffer.idx.min(buffer.len - 1);
472 if entry.extra.current_index != 0xFFFF {
473 let lookup = self.table.lookup(u32::from(entry.extra.current_index))?;
474 replacement = lookup.value(buffer.info[idx].as_glyph());
475 }
476
477 if let Some(replacement) = replacement {
478 buffer.info[idx].glyph_id = u32::from(replacement);
479
480 if let Some(face) = self.face_if_has_glyph_classes {
481 buffer.info[self.mark].set_glyph_props(face.glyph_props(GlyphId(replacement)));
482 }
483 }
484
485 if entry.flags & Self::SET_MARK != 0 {
486 self.mark_set = true;
487 self.mark = buffer.idx;
488 }
489
490 Some(())
491 }
492}
493
494struct InsertionCtx<'a> {
495 mark: u32,
496 glyphs: LazyArray32<'a, GlyphId>,
497}
498
499impl InsertionCtx<'_> {
500 const SET_MARK: u16 = 0x8000;
501 const DONT_ADVANCE: u16 = 0x4000;
502 const CURRENT_INSERT_BEFORE: u16 = 0x0800;
503 const MARKED_INSERT_BEFORE: u16 = 0x0400;
504 const CURRENT_INSERT_COUNT: u16 = 0x03E0;
505 const MARKED_INSERT_COUNT: u16 = 0x001F;
506}
507
508impl Driver<morx::InsertionEntryData> for InsertionCtx<'_> {
509 fn in_place(&self) -> bool {
510 false
511 }
512
513 fn can_advance(
514 &self,
515 entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
516 ) -> bool {
517 entry.flags & Self::DONT_ADVANCE == 0
518 }
519
520 fn is_actionable(
521 &self,
522 entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
523 _: &Buffer,
524 ) -> bool {
525 (entry.flags & (Self::CURRENT_INSERT_COUNT | Self::MARKED_INSERT_COUNT) != 0)
526 && (entry.extra.current_insert_index != 0xFFFF
527 || entry.extra.marked_insert_index != 0xFFFF)
528 }
529
530 fn transition(
531 &mut self,
532 entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
533 buffer: &mut Buffer,
534 ) -> Option<()> {
535 let flags = entry.flags;
536 let mark_loc = buffer.out_len;
537
538 if entry.extra.marked_insert_index != 0xFFFF {
539 let count = flags & Self::MARKED_INSERT_COUNT;
540 buffer.max_ops -= i32::from(count);
541 if buffer.max_ops <= 0 {
542 return Some(());
543 }
544
545 let start = entry.extra.marked_insert_index;
546 let before = flags & Self::MARKED_INSERT_BEFORE != 0;
547
548 let end = buffer.out_len;
549 buffer.move_to(self.mark as usize);
550
551 if buffer.idx < buffer.len && !before {
552 buffer.copy_glyph();
553 }
554
555 // TODO We ignore KashidaLike setting.
556 for i in 0..count {
557 let i = u32::from(start + i);
558 buffer.output_glyph(u32::from(self.glyphs.get(i)?.0));
559 }
560
561 if buffer.idx < buffer.len && !before {
562 buffer.skip_glyph();
563 }
564
565 buffer.move_to(end + usize::from(count));
566
567 buffer.unsafe_to_break_from_outbuffer(
568 self.mark as usize,
569 (buffer.idx + 1).min(buffer.len),
570 );
571 }
572
573 if flags & Self::SET_MARK != 0 {
574 self.mark = mark_loc as u32;
575 }
576
577 if entry.extra.current_insert_index != 0xFFFF {
578 let count = (flags & Self::CURRENT_INSERT_COUNT) >> 5;
579 buffer.max_ops -= i32::from(count);
580 if buffer.max_ops < 0 {
581 return Some(());
582 }
583
584 let start = entry.extra.current_insert_index;
585 let before = flags & Self::CURRENT_INSERT_BEFORE != 0;
586 let end = buffer.out_len;
587
588 if buffer.idx < buffer.len && !before {
589 buffer.copy_glyph();
590 }
591
592 // TODO We ignore KashidaLike setting.
593 for i in 0..count {
594 let i = u32::from(start + i);
595 buffer.output_glyph(u32::from(self.glyphs.get(i)?.0));
596 }
597
598 if buffer.idx < buffer.len && !before {
599 buffer.skip_glyph();
600 }
601
602 // Humm. Not sure where to move to. There's this wording under
603 // DontAdvance flag:
604 //
605 // "If set, don't update the glyph index before going to the new state.
606 // This does not mean that the glyph pointed to is the same one as
607 // before. If you've made insertions immediately downstream of the
608 // current glyph, the next glyph processed would in fact be the first
609 // one inserted."
610 //
611 // This suggests that if DontAdvance is NOT set, we should move to
612 // end+count. If it *was*, then move to end, such that newly inserted
613 // glyphs are now visible.
614 //
615 // https://github.com/harfbuzz/harfbuzz/issues/1224#issuecomment-427691417
616 buffer.move_to(if flags & Self::DONT_ADVANCE != 0 {
617 end
618 } else {
619 end + usize::from(count)
620 });
621 }
622
623 Some(())
624 }
625}
626
627const LIGATURE_MAX_MATCHES: usize = 64;
628
629struct LigatureCtx<'a> {
630 table: &'a morx::LigatureSubtable<'a>,
631 match_length: usize,
632 match_positions: [usize; LIGATURE_MAX_MATCHES],
633}
634
635impl LigatureCtx<'_> {
636 const SET_COMPONENT: u16 = 0x8000;
637 const DONT_ADVANCE: u16 = 0x4000;
638 const PERFORM_ACTION: u16 = 0x2000;
639
640 const LIG_ACTION_LAST: u32 = 0x80000000;
641 const LIG_ACTION_STORE: u32 = 0x40000000;
642 const LIG_ACTION_OFFSET: u32 = 0x3FFFFFFF;
643}
644
645impl Driver<u16> for LigatureCtx<'_> {
646 fn in_place(&self) -> bool {
647 false
648 }
649
650 fn can_advance(&self, entry: &apple_layout::GenericStateEntry<u16>) -> bool {
651 entry.flags & Self::DONT_ADVANCE == 0
652 }
653
654 fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<u16>, _: &Buffer) -> bool {
655 entry.flags & Self::PERFORM_ACTION != 0
656 }
657
658 fn transition(
659 &mut self,
660 entry: &apple_layout::GenericStateEntry<u16>,
661 buffer: &mut Buffer,
662 ) -> Option<()> {
663 if entry.flags & Self::SET_COMPONENT != 0 {
664 // Never mark same index twice, in case DONT_ADVANCE was used...
665 if self.match_length != 0
666 && self.match_positions[(self.match_length - 1) % LIGATURE_MAX_MATCHES]
667 == buffer.out_len
668 {
669 self.match_length -= 1;
670 }
671
672 self.match_positions[self.match_length % LIGATURE_MAX_MATCHES] = buffer.out_len;
673 self.match_length += 1;
674 }
675
676 if entry.flags & Self::PERFORM_ACTION != 0 {
677 let end = buffer.out_len;
678
679 if self.match_length == 0 {
680 return Some(());
681 }
682
683 if buffer.idx >= buffer.len {
684 return Some(()); // TODO: Work on previous instead?
685 }
686
687 let mut cursor = self.match_length;
688
689 let mut ligature_actions_index = entry.extra;
690 let mut ligature_idx = 0;
691 loop {
692 if cursor == 0 {
693 // Stack underflow. Clear the stack.
694 self.match_length = 0;
695 break;
696 }
697
698 cursor -= 1;
699 buffer.move_to(self.match_positions[cursor % LIGATURE_MAX_MATCHES]);
700
701 // We cannot use ? in this loop, because we must call
702 // buffer.move_to(end) in the end.
703 let action = match self
704 .table
705 .ligature_actions
706 .get(u32::from(ligature_actions_index))
707 {
708 Some(v) => v,
709 None => break,
710 };
711
712 let mut uoffset = action & Self::LIG_ACTION_OFFSET;
713 if uoffset & 0x20000000 != 0 {
714 uoffset |= 0xC0000000; // Sign-extend.
715 }
716
717 let offset = uoffset as i32;
718 let component_idx = (buffer.cur(0).glyph_id as i32 + offset) as u32;
719 ligature_idx += match self.table.components.get(component_idx) {
720 Some(v) => v,
721 None => break,
722 };
723
724 if (action & (Self::LIG_ACTION_STORE | Self::LIG_ACTION_LAST)) != 0 {
725 let lig = match self.table.ligatures.get(u32::from(ligature_idx)) {
726 Some(v) => v,
727 None => break,
728 };
729
730 buffer.replace_glyph(u32::from(lig.0));
731
732 let lig_end =
733 self.match_positions[(self.match_length - 1) % LIGATURE_MAX_MATCHES] + 1;
734 // Now go and delete all subsequent components.
735 while self.match_length - 1 > cursor {
736 self.match_length -= 1;
737 buffer.move_to(
738 self.match_positions[self.match_length % LIGATURE_MAX_MATCHES],
739 );
740 buffer.replace_glyph(0xFFFF);
741 }
742
743 buffer.move_to(lig_end);
744 buffer.merge_out_clusters(
745 self.match_positions[cursor % LIGATURE_MAX_MATCHES],
746 buffer.out_len,
747 );
748 }
749
750 ligature_actions_index += 1;
751
752 if action & Self::LIG_ACTION_LAST != 0 {
753 break;
754 }
755 }
756
757 buffer.move_to(end);
758 }
759
760 Some(())
761 }
762}
763