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_graphemes();
91 }
92
93 apply_subtable(&subtable.kind, buffer, face);
94
95 if reverse {
96 buffer.reverse_graphemes();
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(
214 Some(buffer.backtrack_len() - 1),
215 Some(buffer.idx + 1),
216 );
217 }
218
219 c.transition(&entry, buffer);
220
221 state = next_state;
222
223 if buffer.idx >= buffer.len || !buffer.successful {
224 break;
225 }
226
227 if c.can_advance(&entry) {
228 buffer.next_glyph();
229 } else {
230 if buffer.max_ops <= 0 {
231 buffer.next_glyph();
232 }
233 buffer.max_ops -= 1;
234 }
235 }
236
237 if !c.in_place() {
238 buffer.sync();
239 }
240}
241
242fn apply_subtable(kind: &morx::SubtableKind, buffer: &mut Buffer, face: &Face) {
243 match kind {
244 morx::SubtableKind::Rearrangement(ref table) => {
245 let mut c = RearrangementCtx { start: 0, end: 0 };
246
247 drive::<()>(table, &mut c, buffer);
248 }
249 morx::SubtableKind::Contextual(ref table) => {
250 let mut c = ContextualCtx {
251 mark_set: false,
252 face_if_has_glyph_classes:
253 matches!(face.tables().gdef, Some(gdef) if gdef.has_glyph_classes())
254 .then_some(face),
255 mark: 0,
256 table,
257 };
258
259 drive::<morx::ContextualEntryData>(&table.state, &mut c, buffer);
260 }
261 morx::SubtableKind::Ligature(ref table) => {
262 let mut c = LigatureCtx {
263 table,
264 match_length: 0,
265 match_positions: [0; LIGATURE_MAX_MATCHES],
266 };
267
268 drive::<u16>(&table.state, &mut c, buffer);
269 }
270 morx::SubtableKind::NonContextual(ref lookup) => {
271 let face_if_has_glyph_classes =
272 matches!(face.tables().gdef, Some(gdef) if gdef.has_glyph_classes())
273 .then_some(face);
274 for info in &mut buffer.info {
275 if let Some(replacement) = lookup.value(info.as_glyph()) {
276 info.glyph_id = u32::from(replacement);
277 if let Some(face) = face_if_has_glyph_classes {
278 info.set_glyph_props(face.glyph_props(GlyphId(replacement)));
279 }
280 }
281 }
282 }
283 morx::SubtableKind::Insertion(ref table) => {
284 let mut c = InsertionCtx {
285 mark: 0,
286 glyphs: table.glyphs,
287 };
288
289 drive::<morx::InsertionEntryData>(&table.state, &mut c, buffer);
290 }
291 }
292}
293
294struct RearrangementCtx {
295 start: usize,
296 end: usize,
297}
298
299impl RearrangementCtx {
300 const MARK_FIRST: u16 = 0x8000;
301 const DONT_ADVANCE: u16 = 0x4000;
302 const MARK_LAST: u16 = 0x2000;
303 const VERB: u16 = 0x000F;
304}
305
306impl Driver<()> for RearrangementCtx {
307 fn in_place(&self) -> bool {
308 true
309 }
310
311 fn can_advance(&self, entry: &apple_layout::GenericStateEntry<()>) -> bool {
312 entry.flags & Self::DONT_ADVANCE == 0
313 }
314
315 fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<()>, _: &Buffer) -> bool {
316 entry.flags & Self::VERB != 0 && self.start < self.end
317 }
318
319 fn transition(
320 &mut self,
321 entry: &apple_layout::GenericStateEntry<()>,
322 buffer: &mut Buffer,
323 ) -> Option<()> {
324 let flags = entry.flags;
325
326 if flags & Self::MARK_FIRST != 0 {
327 self.start = buffer.idx;
328 }
329
330 if flags & Self::MARK_LAST != 0 {
331 self.end = (buffer.idx + 1).min(buffer.len);
332 }
333
334 if flags & Self::VERB != 0 && self.start < self.end {
335 // The following map has two nibbles, for start-side
336 // and end-side. Values of 0,1,2 mean move that many
337 // to the other side. Value of 3 means move 2 and
338 // flip them.
339 const MAP: [u8; 16] = [
340 0x00, // 0 no change
341 0x10, // 1 Ax => xA
342 0x01, // 2 xD => Dx
343 0x11, // 3 AxD => DxA
344 0x20, // 4 ABx => xAB
345 0x30, // 5 ABx => xBA
346 0x02, // 6 xCD => CDx
347 0x03, // 7 xCD => DCx
348 0x12, // 8 AxCD => CDxA
349 0x13, // 9 AxCD => DCxA
350 0x21, // 10 ABxD => DxAB
351 0x31, // 11 ABxD => DxBA
352 0x22, // 12 ABxCD => CDxAB
353 0x32, // 13 ABxCD => CDxBA
354 0x23, // 14 ABxCD => DCxAB
355 0x33, // 15 ABxCD => DCxBA
356 ];
357
358 let m = MAP[usize::from(flags & Self::VERB)];
359 let l = 2.min(m >> 4) as usize;
360 let r = 2.min(m & 0x0F) as usize;
361 let reverse_l = 3 == (m >> 4);
362 let reverse_r = 3 == (m & 0x0F);
363
364 if self.end - self.start >= l + r {
365 buffer.merge_clusters(self.start, (buffer.idx + 1).min(buffer.len));
366 buffer.merge_clusters(self.start, self.end);
367
368 let mut buf = [GlyphInfo::default(); 4];
369
370 for (i, glyph_info) in buf[..l].iter_mut().enumerate() {
371 *glyph_info = buffer.info[self.start + i];
372 }
373
374 for i in 0..r {
375 buf[i + 2] = buffer.info[self.end - r + i];
376 }
377
378 if l > r {
379 for i in 0..(self.end - self.start - l - r) {
380 buffer.info[self.start + r + i] = buffer.info[self.start + l + i];
381 }
382 } else if l < r {
383 for i in (0..(self.end - self.start - l - r)).rev() {
384 buffer.info[self.start + r + i] = buffer.info[self.start + l + i];
385 }
386 }
387
388 for i in 0..r {
389 buffer.info[self.start + i] = buf[2 + i];
390 }
391
392 for i in 0..l {
393 buffer.info[self.end - l + i] = buf[i];
394 }
395
396 if reverse_l {
397 buffer.info.swap(self.end - 1, self.end - 2);
398 }
399
400 if reverse_r {
401 buffer.info.swap(self.start, self.start + 1);
402 }
403 }
404 }
405
406 Some(())
407 }
408}
409
410struct ContextualCtx<'a> {
411 mark_set: bool,
412 face_if_has_glyph_classes: Option<&'a Face<'a>>,
413 mark: usize,
414 table: &'a morx::ContextualSubtable<'a>,
415}
416
417impl ContextualCtx<'_> {
418 const SET_MARK: u16 = 0x8000;
419 const DONT_ADVANCE: u16 = 0x4000;
420}
421
422impl Driver<morx::ContextualEntryData> for ContextualCtx<'_> {
423 fn in_place(&self) -> bool {
424 true
425 }
426
427 fn can_advance(
428 &self,
429 entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
430 ) -> bool {
431 entry.flags & Self::DONT_ADVANCE == 0
432 }
433
434 fn is_actionable(
435 &self,
436 entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
437 buffer: &Buffer,
438 ) -> bool {
439 if buffer.idx == buffer.len && !self.mark_set {
440 return false;
441 }
442
443 entry.extra.mark_index != 0xFFFF || entry.extra.current_index != 0xFFFF
444 }
445
446 fn transition(
447 &mut self,
448 entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
449 buffer: &mut Buffer,
450 ) -> Option<()> {
451 // Looks like CoreText applies neither mark nor current substitution for
452 // end-of-text if mark was not explicitly set.
453 if buffer.idx == buffer.len && !self.mark_set {
454 return Some(());
455 }
456
457 let mut replacement = None;
458
459 if entry.extra.mark_index != 0xFFFF {
460 let lookup = self.table.lookup(u32::from(entry.extra.mark_index))?;
461 replacement = lookup.value(buffer.info[self.mark].as_glyph());
462 }
463
464 if let Some(replacement) = replacement {
465 buffer.unsafe_to_break(Some(self.mark), Some((buffer.idx + 1).min(buffer.len)));
466 buffer.info[self.mark].glyph_id = u32::from(replacement);
467
468 if let Some(face) = self.face_if_has_glyph_classes {
469 buffer.info[self.mark].set_glyph_props(face.glyph_props(GlyphId(replacement)));
470 }
471 }
472
473 replacement = None;
474 let idx = buffer.idx.min(buffer.len - 1);
475 if entry.extra.current_index != 0xFFFF {
476 let lookup = self.table.lookup(u32::from(entry.extra.current_index))?;
477 replacement = lookup.value(buffer.info[idx].as_glyph());
478 }
479
480 if let Some(replacement) = replacement {
481 buffer.info[idx].glyph_id = u32::from(replacement);
482
483 if let Some(face) = self.face_if_has_glyph_classes {
484 buffer.info[self.mark].set_glyph_props(face.glyph_props(GlyphId(replacement)));
485 }
486 }
487
488 if entry.flags & Self::SET_MARK != 0 {
489 self.mark_set = true;
490 self.mark = buffer.idx;
491 }
492
493 Some(())
494 }
495}
496
497struct InsertionCtx<'a> {
498 mark: u32,
499 glyphs: LazyArray32<'a, GlyphId>,
500}
501
502impl InsertionCtx<'_> {
503 const SET_MARK: u16 = 0x8000;
504 const DONT_ADVANCE: u16 = 0x4000;
505 const CURRENT_INSERT_BEFORE: u16 = 0x0800;
506 const MARKED_INSERT_BEFORE: u16 = 0x0400;
507 const CURRENT_INSERT_COUNT: u16 = 0x03E0;
508 const MARKED_INSERT_COUNT: u16 = 0x001F;
509}
510
511impl Driver<morx::InsertionEntryData> for InsertionCtx<'_> {
512 fn in_place(&self) -> bool {
513 false
514 }
515
516 fn can_advance(
517 &self,
518 entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
519 ) -> bool {
520 entry.flags & Self::DONT_ADVANCE == 0
521 }
522
523 fn is_actionable(
524 &self,
525 entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
526 _: &Buffer,
527 ) -> bool {
528 (entry.flags & (Self::CURRENT_INSERT_COUNT | Self::MARKED_INSERT_COUNT) != 0)
529 && (entry.extra.current_insert_index != 0xFFFF
530 || entry.extra.marked_insert_index != 0xFFFF)
531 }
532
533 fn transition(
534 &mut self,
535 entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
536 buffer: &mut Buffer,
537 ) -> Option<()> {
538 let flags = entry.flags;
539 let mark_loc = buffer.out_len;
540
541 if entry.extra.marked_insert_index != 0xFFFF {
542 let count = flags & Self::MARKED_INSERT_COUNT;
543 buffer.max_ops -= i32::from(count);
544 if buffer.max_ops <= 0 {
545 return Some(());
546 }
547
548 let start = entry.extra.marked_insert_index;
549 let before = flags & Self::MARKED_INSERT_BEFORE != 0;
550
551 let end = buffer.out_len;
552 buffer.move_to(self.mark as usize);
553
554 if buffer.idx < buffer.len && !before {
555 buffer.copy_glyph();
556 }
557
558 // TODO We ignore KashidaLike setting.
559 for i in 0..count {
560 let i = u32::from(start + i);
561 buffer.output_glyph(u32::from(self.glyphs.get(i)?.0));
562 }
563
564 if buffer.idx < buffer.len && !before {
565 buffer.skip_glyph();
566 }
567
568 buffer.move_to(end + usize::from(count));
569
570 buffer.unsafe_to_break_from_outbuffer(
571 Some(self.mark as usize),
572 Some((buffer.idx + 1).min(buffer.len)),
573 );
574 }
575
576 if flags & Self::SET_MARK != 0 {
577 self.mark = mark_loc as u32;
578 }
579
580 if entry.extra.current_insert_index != 0xFFFF {
581 let count = (flags & Self::CURRENT_INSERT_COUNT) >> 5;
582 buffer.max_ops -= i32::from(count);
583 if buffer.max_ops < 0 {
584 return Some(());
585 }
586
587 let start = entry.extra.current_insert_index;
588 let before = flags & Self::CURRENT_INSERT_BEFORE != 0;
589 let end = buffer.out_len;
590
591 if buffer.idx < buffer.len && !before {
592 buffer.copy_glyph();
593 }
594
595 // TODO We ignore KashidaLike setting.
596 for i in 0..count {
597 let i = u32::from(start + i);
598 buffer.output_glyph(u32::from(self.glyphs.get(i)?.0));
599 }
600
601 if buffer.idx < buffer.len && !before {
602 buffer.skip_glyph();
603 }
604
605 // Humm. Not sure where to move to. There's this wording under
606 // DontAdvance flag:
607 //
608 // "If set, don't update the glyph index before going to the new state.
609 // This does not mean that the glyph pointed to is the same one as
610 // before. If you've made insertions immediately downstream of the
611 // current glyph, the next glyph processed would in fact be the first
612 // one inserted."
613 //
614 // This suggests that if DontAdvance is NOT set, we should move to
615 // end+count. If it *was*, then move to end, such that newly inserted
616 // glyphs are now visible.
617 //
618 // https://github.com/harfbuzz/harfbuzz/issues/1224#issuecomment-427691417
619 buffer.move_to(if flags & Self::DONT_ADVANCE != 0 {
620 end
621 } else {
622 end + usize::from(count)
623 });
624 }
625
626 Some(())
627 }
628}
629
630const LIGATURE_MAX_MATCHES: usize = 64;
631
632struct LigatureCtx<'a> {
633 table: &'a morx::LigatureSubtable<'a>,
634 match_length: usize,
635 match_positions: [usize; LIGATURE_MAX_MATCHES],
636}
637
638impl LigatureCtx<'_> {
639 const SET_COMPONENT: u16 = 0x8000;
640 const DONT_ADVANCE: u16 = 0x4000;
641 const PERFORM_ACTION: u16 = 0x2000;
642
643 const LIG_ACTION_LAST: u32 = 0x80000000;
644 const LIG_ACTION_STORE: u32 = 0x40000000;
645 const LIG_ACTION_OFFSET: u32 = 0x3FFFFFFF;
646}
647
648impl Driver<u16> for LigatureCtx<'_> {
649 fn in_place(&self) -> bool {
650 false
651 }
652
653 fn can_advance(&self, entry: &apple_layout::GenericStateEntry<u16>) -> bool {
654 entry.flags & Self::DONT_ADVANCE == 0
655 }
656
657 fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<u16>, _: &Buffer) -> bool {
658 entry.flags & Self::PERFORM_ACTION != 0
659 }
660
661 fn transition(
662 &mut self,
663 entry: &apple_layout::GenericStateEntry<u16>,
664 buffer: &mut Buffer,
665 ) -> Option<()> {
666 if entry.flags & Self::SET_COMPONENT != 0 {
667 // Never mark same index twice, in case DONT_ADVANCE was used...
668 if self.match_length != 0
669 && self.match_positions[(self.match_length - 1) % LIGATURE_MAX_MATCHES]
670 == buffer.out_len
671 {
672 self.match_length -= 1;
673 }
674
675 self.match_positions[self.match_length % LIGATURE_MAX_MATCHES] = buffer.out_len;
676 self.match_length += 1;
677 }
678
679 if entry.flags & Self::PERFORM_ACTION != 0 {
680 let end = buffer.out_len;
681
682 if self.match_length == 0 {
683 return Some(());
684 }
685
686 if buffer.idx >= buffer.len {
687 return Some(()); // TODO: Work on previous instead?
688 }
689
690 let mut cursor = self.match_length;
691
692 let mut ligature_actions_index = entry.extra;
693 let mut ligature_idx = 0;
694 loop {
695 if cursor == 0 {
696 // Stack underflow. Clear the stack.
697 self.match_length = 0;
698 break;
699 }
700
701 cursor -= 1;
702 buffer.move_to(self.match_positions[cursor % LIGATURE_MAX_MATCHES]);
703
704 // We cannot use ? in this loop, because we must call
705 // buffer.move_to(end) in the end.
706 let action = match self
707 .table
708 .ligature_actions
709 .get(u32::from(ligature_actions_index))
710 {
711 Some(v) => v,
712 None => break,
713 };
714
715 let mut uoffset = action & Self::LIG_ACTION_OFFSET;
716 if uoffset & 0x20000000 != 0 {
717 uoffset |= 0xC0000000; // Sign-extend.
718 }
719
720 let offset = uoffset as i32;
721 let component_idx = (buffer.cur(0).glyph_id as i32 + offset) as u32;
722 ligature_idx += match self.table.components.get(component_idx) {
723 Some(v) => v,
724 None => break,
725 };
726
727 if (action & (Self::LIG_ACTION_STORE | Self::LIG_ACTION_LAST)) != 0 {
728 let lig = match self.table.ligatures.get(u32::from(ligature_idx)) {
729 Some(v) => v,
730 None => break,
731 };
732
733 buffer.replace_glyph(u32::from(lig.0));
734
735 let lig_end =
736 self.match_positions[(self.match_length - 1) % LIGATURE_MAX_MATCHES] + 1;
737 // Now go and delete all subsequent components.
738 while self.match_length - 1 > cursor {
739 self.match_length -= 1;
740 buffer.move_to(
741 self.match_positions[self.match_length % LIGATURE_MAX_MATCHES],
742 );
743 buffer.replace_glyph(0xFFFF);
744 }
745
746 buffer.move_to(lig_end);
747 buffer.merge_out_clusters(
748 self.match_positions[cursor % LIGATURE_MAX_MATCHES],
749 buffer.out_len,
750 );
751 }
752
753 ligature_actions_index += 1;
754
755 if action & Self::LIG_ACTION_LAST != 0 {
756 break;
757 }
758 }
759
760 buffer.move_to(end);
761 }
762
763 Some(())
764 }
765}
766