Something went wrong. Try again.
An OpenType text shaper for Zig, ported from HarfBuzz and checked against its test suite, for fonts decoded by zig-font.
Something went wrong. Try again.
32 kB · 822 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2007,2008,2009,2010 Red Hat, Inc.// SPDX-FileCopyrightText: © 2010,2012 Google, Inc.// SPDX-License-Identifier: MIT AND MIT-Modern-Variant
//! Applying `GSUB` and `GPOS` lookups: the machinery both share.//!//! The hard part of OpenType layout is not any one subtable but matching a//! sequence of glyphs against the glyph buffer, skipping the ones a lookup's//! flags say to ignore, and keeping track of which marks belong to which//! component of which ligature as glyphs come and go. This is HarfBuzz's//! `hb_ot_apply_context_t` and the functions around it in//! `hb-ot-layout-gsubgpos.hh`, kept close to the original so that the two//! give the same answers.
const std = @import("std");const Allocator = std.mem.Allocator;const font = @import("font");
const Buffer = @import("Buffer.zig");const Face = @import("Face.zig");const Map = @import("Map.zig");const gsub = @import("gsub.zig");const gpos = @import("gpos.zig");
const GlyphInfo = Buffer.GlyphInfo;const GlyphProps = Buffer.GlyphProps;const layout = font.tables.layout;const Coverage = layout.Coverage;const ClassDef = layout.ClassDef;
pub const Error = Allocator.Error;
pub const max_nesting_level = 64;pub const max_context_length = 64;
/// The lookup flag bits, as HarfBuzz keeps them in `lookup_props`: the flag/// word, with the mark filtering set's index in the high 16 bits.pub const LookupFlag = struct { pub const right_to_left: u32 = 0x0001; pub const ignore_base_glyphs: u32 = 0x0002; pub const ignore_ligatures: u32 = 0x0004; pub const ignore_marks: u32 = 0x0008; pub const ignore_flags: u32 = 0x000E; pub const use_mark_filtering_set: u32 = 0x0010; pub const mark_attachment_type: u32 = 0xFF00;};
pub fn lookupProps(flag: layout.LookupFlag, mark_filtering_set: ?u16) u32 { var props: u32 = @as(u16, @bitCast(flag)); if (flag.use_mark_filtering_set) props += @as(u32, mark_filtering_set orelse 0) << 16; return props;}
/// What a skipping iterator compares glyphs against, and how.pub const Match = union(enum) { /// Nothing: every glyph not skipped is a candidate. none, /// Glyph IDs. glyph: []const u16, /// Classes in a class definition. class: struct { def: *const ClassDef, values: []const u16 }, /// One coverage table per position. coverage: []const Coverage,};
const MayMatch = enum { no, yes, maybe };const MaySkip = enum { no, yes, maybe };const MatchResult = enum { match, not_match, skip };
/// `matcher_t` with `skipping_iterator_t`: walks the buffer from a glyph,/// forward through `info` or backward through what has been output, past/// the glyphs the lookup ignores, comparing each candidate with the next/// value of the sequence being matched.pub const SkippyIter = struct { c: *ApplyContext, idx: usize = 0, end: usize = 0, lookup_props: u32 = 0, mask: u32 = std.math.maxInt(u32), ignore_zwnj: bool = false, ignore_zwj: bool = false, ignore_hidden: bool = false, per_syllable: bool = false, syllable: u8 = 0, match: Match = .none, /// How far through the values of `match` matching has got. data_index: usize = 0,
pub fn init(it: *SkippyIter, c: *ApplyContext, context_match: bool) void { it.* = .{ .c = c, .end = c.buffer.len(), .lookup_props = c.lookup_props, // Ignore ZWNJ if matching GPOS, or matching GSUB context and asked to. .ignore_zwnj = c.table == .gpos or (context_match and c.auto_zwnj), // Ignore ZWJ if matching context, or asked to. .ignore_zwj = context_match or c.auto_zwj, // Ignore hidden glyphs (like CGJ) during GPOS. .ignore_hidden = c.table == .gpos, .mask = if (context_match) std.math.maxInt(u32) else c.lookup_mask, // Per-syllable matching is only for GSUB. .per_syllable = c.table == .gsub and c.per_syllable, }; }
pub fn setMatch(it: *SkippyIter, m: Match) void { it.match = m; it.data_index = 0; }
/// For the forward iterator of GSUB. pub fn reset(it: *SkippyIter, start: usize) void { it.idx = start; it.end = it.c.buffer.len(); it.syllable = if (it.c.buffer.idx < it.c.buffer.len()) it.c.buffer.cur(0).syllable else 0; }
/// For the backward iterator of GSUB. pub fn resetBack(it: *SkippyIter, start: usize) void { it.idx = start; it.syllable = if (it.c.buffer.idx < it.c.buffer.len()) it.c.buffer.cur(0).syllable else 0; }
/// Sets neither the end nor the syllable; GPOS does not change the /// buffer, and does not care about syllables. pub fn resetFast(it: *SkippyIter, start: usize) void { it.idx = start; }
fn mayMatch(it: *const SkippyIter, info: *const GlyphInfo) MayMatch { if (info.mask & it.mask == 0 or (it.per_syllable and it.syllable != 0 and it.syllable != info.syllable)) return .no; const gid: u16 = @truncate(info.codepoint); const ok = switch (it.match) { .none => return .maybe, .glyph => |g| it.data_index < g.len and gid == g[it.data_index], .class => |cl| it.data_index < cl.values.len and cl.def.get(gid) == cl.values[it.data_index], .coverage => |cov| it.data_index < cov.len and cov[it.data_index].get(gid) != null, }; return if (ok) .yes else .no; }
pub fn maySkip(it: *const SkippyIter, info: *const GlyphInfo) MaySkip { if (!it.c.checkGlyphProperty(info, it.lookup_props)) return .yes; if (info.isDefaultIgnorable() and (it.ignore_zwnj or !info.isZwnj()) and (it.ignore_zwj or !info.isZwj()) and (it.ignore_hidden or !info.isHidden())) return .maybe; return .no; }
pub fn matchGlyph(it: *const SkippyIter, info: *const GlyphInfo) MatchResult { const skip = it.maySkip(info); if (skip == .yes) return .skip; const m = it.mayMatch(info); if (m == .yes or (m == .maybe and skip == .no)) return .match; if (skip == .no) return .not_match; return .skip; }
/// Moves to the next glyph that matches. On failure `unsafe_to` is where /// the text stops mattering to the result. pub fn next(it: *SkippyIter, unsafe_to: ?*usize) bool { const info = it.c.buffer.info.items; const stop: isize = @as(isize, @intCast(it.end)) - 1; while (@as(isize, @intCast(it.idx)) < stop) { it.idx += 1; switch (it.matchGlyph(&info[it.idx])) { .match => { it.data_index += 1; return true; }, .not_match => { if (unsafe_to) |u| u.* = it.idx + 1; return false; }, .skip => continue, } } if (unsafe_to) |u| u.* = it.end; return false; }
/// Moves to the previous glyph that matches, among those output. pub fn prev(it: *SkippyIter, unsafe_from: ?*usize) bool { const out = it.c.buffer.outInfo(); while (it.idx > 0) { it.idx -= 1; switch (it.matchGlyph(&out[it.idx])) { .match => { it.data_index += 1; return true; }, .not_match => { if (unsafe_from) |u| u.* = @max(1, it.idx) - 1; return false; }, .skip => continue, } } if (unsafe_from) |u| u.* = 0; return false; }};
/// `hb_ot_apply_context_t`.pub const ApplyContext = struct { table: Map.TableIndex, face: *const Face, buffer: *Buffer, gdef: ?*const font.tables.gdef.Gdef, direction: Buffer.Direction, lookup_mask: u32 = 1, lookup_index: u16 = 0xFFFF, lookup_props: u32 = 0, nesting_level_left: u32 = max_nesting_level, has_glyph_classes: bool, auto_zwnj: bool = true, auto_zwj: bool = true, per_syllable: bool = false, random: bool = false, /// What syllable to give glyphs a substitution produces, if any. new_syllables: ?u8 = null, // For mark-to-base and mark-to-ligature: the base found last, and how far // the search for it went, so the search is not repeated for every mark. last_base: isize = -1, last_base_until: usize = 0, match_positions: [max_context_length]usize = undefined, iter_input: SkippyIter = undefined, iter_context: SkippyIter = undefined,
pub fn init(table: Map.TableIndex, face: *const Face, buffer: *Buffer) ApplyContext { const gdef: ?*const font.tables.gdef.Gdef = if (face.font.gdef) |*g| g else null; return .{ .table = table, .face = face, .buffer = buffer, .gdef = gdef, .direction = buffer.direction.?, .has_glyph_classes = if (gdef) |g| g.glyph_class_def != null else false, }; }
/// Must be called once the context is at its final address, since the /// iterators point back to it. pub fn initIters(c: *ApplyContext) void { c.iter_input.init(c, false); c.iter_context.init(c, true); }
pub fn setLookupMask(c: *ApplyContext, mask: u32) void { c.lookup_mask = mask; c.last_base = -1; c.last_base_until = 0; }
pub fn setLookupProps(c: *ApplyContext, props: u32) void { c.lookup_props = c.sanitizeLookupProps(props); c.initIters(); }
/// An invalid mark filtering set index unsets the flag. fn sanitizeLookupProps(c: *const ApplyContext, props: u32) u32 { if (props & LookupFlag.use_mark_filtering_set != 0) { const sets = if (c.gdef) |g| g.mark_glyph_sets.len else 0; if ((props >> 16) >= sets) return props & ~LookupFlag.use_mark_filtering_set; } return props; }
pub fn randomNumber(c: *ApplyContext) u32 { // minstd_rand, in the 32-bit arithmetic HarfBuzz does it in. c.buffer.random_state = (c.buffer.random_state *% 48271) % 2147483647; return c.buffer.random_state; }
/// `GDEF::get_glyph_props`. pub fn gdefGlyphProps(c: *const ApplyContext, glyph: u32) u16 { const g = c.gdef orelse return 0; const gid: u16 = @truncate(glyph); const cls = g.glyphClass(gid) orelse return 0; return switch (cls) { .base => GlyphProps.base_glyph, .ligature => GlyphProps.ligature, .mark => GlyphProps.mark | (g.markAttachClass(gid) << 8), else => 0, }; }
fn matchPropertiesMark(c: *const ApplyContext, info: *const GlyphInfo, glyph_props: u16, match_props: u32) bool { // With mark filtering sets, the high half of match_props is the set. if (match_props & LookupFlag.use_mark_filtering_set != 0) { const g = c.gdef orelse return false; return g.inMarkGlyphSet(@intCast(match_props >> 16), @truncate(info.codepoint)); } // The second byte of match_props means "ignore marks of attachment // type different than the attachment type specified". if (match_props & LookupFlag.mark_attachment_type != 0) return (match_props & LookupFlag.mark_attachment_type) == (glyph_props & LookupFlag.mark_attachment_type); return true; }
pub fn checkGlyphProperty(c: *const ApplyContext, info: *const GlyphInfo, match_props: u32) bool { const glyph_props = info.glyph_props; // Not covered if, for example, the glyph class is ligature and // match_props includes IgnoreLigatures. if (glyph_props & match_props & LookupFlag.ignore_flags != 0) return false; if (glyph_props & GlyphProps.mark != 0) return c.matchPropertiesMark(info, glyph_props, match_props); return true; }
fn setGlyphClass(c: *ApplyContext, glyph: u32, class_guess: u16, ligature: bool, component: bool) void { const cur = c.buffer.cur(0); if (c.new_syllables) |s| cur.syllable = s; var props = cur.glyph_props; props |= GlyphProps.substituted; if (ligature) { props |= GlyphProps.ligated; // Only the last transformation between ligation and multiple // substitution counts, as Uniscribe has it. props &= ~GlyphProps.multiplied; } if (component) props |= GlyphProps.multiplied; if (c.has_glyph_classes) { props &= GlyphProps.preserve; cur.glyph_props = props | c.gdefGlyphProps(glyph); } else if (class_guess != 0) { props &= GlyphProps.preserve; cur.glyph_props = props | class_guess; } else { cur.glyph_props = props; } }
pub fn replaceGlyph(c: *ApplyContext, glyph: u32) Error!void { c.setGlyphClass(glyph, 0, false, false); try c.buffer.replaceGlyph(glyph); }
pub fn replaceGlyphInplace(c: *ApplyContext, glyph: u32) void { c.setGlyphClass(glyph, 0, false, false); c.buffer.cur(0).codepoint = glyph; }
pub fn replaceGlyphWithLigature(c: *ApplyContext, glyph: u32, class_guess: u16) Error!void { c.setGlyphClass(glyph, class_guess, true, false); try c.buffer.replaceGlyph(glyph); }
pub fn outputGlyphForComponent(c: *ApplyContext, glyph: u32, class_guess: u16) Error!void { c.setGlyphClass(glyph, class_guess, false, true); try c.buffer.outputGlyph(glyph); }
/// Applies lookup `lookup_index` of the same table at the current glyph, /// as a context lookup's lookup records ask. pub fn recurse(c: *ApplyContext, lookup_index: u16) Error!bool { if (c.nesting_level_left == 0) { c.buffer.successful = false; return false; } c.buffer.max_ops -= 1; if (c.buffer.max_ops < 0) { c.buffer.successful = false; return false; } c.nesting_level_left -= 1; defer c.nesting_level_left += 1;
const saved_props = c.lookup_props; const saved_index = c.lookup_index; const saved_positions = c.match_positions; defer { c.lookup_index = saved_index; c.setLookupProps(saved_props); c.match_positions = saved_positions; }
return switch (c.table) { .gsub => blk: { const t = c.face.font.gsub orelse break :blk false; if (lookup_index >= t.lookups.len) break :blk false; const l = &t.lookups[lookup_index]; c.lookup_index = lookup_index; c.setLookupProps(lookupProps(l.flag, l.mark_filtering_set)); break :blk gsub.applySubtables(c, l); }, .gpos => blk: { const t = c.face.font.gpos orelse break :blk false; if (lookup_index >= t.lookups.len) break :blk false; const l = &t.lookups[lookup_index]; c.lookup_index = lookup_index; c.setLookupProps(lookupProps(l.flag, l.mark_filtering_set)); break :blk gpos.applySubtables(c, l); }, }; }
/// The item variation store device tables refer to. pub fn varStore(c: *const ApplyContext) ?*const font.tables.var_store.ItemVariationStore { const g = c.gdef orelse return null; return if (g.item_var_store) |*s| s else null; }};
/// `match_input`: matches the rest of the input sequence after the glyph at/// the cursor. On success, `match_positions[0..count]` hold where each/// position matched and the result is one past the last; on failure, where/// the text stopped mattering.pub fn matchInput( c: *ApplyContext, count: usize, m: Match, end_position: *usize, total_component_count: ?*u32,) bool { const buffer = c.buffer; if (count == 1) { end_position.* = buffer.idx + 1; c.match_positions[0] = buffer.idx; if (total_component_count) |t| t.* = buffer.cur(0).ligNumComps(); return true; } if (count > max_context_length) return false;
const it = &c.iter_input; it.reset(buffer.idx); it.setMatch(m);
// Ligatures cannot be formed across glyphs attached to different // components of previous ligatures, unless the ligature the first glyph // hangs from is itself one the lookup ignores. var total: u32 = 0; const first_lig_id = buffer.cur(0).ligId(); const first_lig_comp = buffer.cur(0).ligComp(); const LigBase = enum { not_checked, may_not_skip, may_skip }; var ligbase: LigBase = .not_checked;
for (1..count) |i| { var unsafe_to: usize = 0; if (!it.next(&unsafe_to)) { end_position.* = unsafe_to; return false; } c.match_positions[i] = it.idx;
const this_info = &buffer.info.items[it.idx]; const this_lig_id = this_info.ligId(); const this_lig_comp = this_info.ligComp();
if (first_lig_id != 0 and first_lig_comp != 0) { // If the first component was attached to a previous ligature // component, all subsequent components should be attached to the // same one, otherwise they should not ligate... if (first_lig_id != this_lig_id or first_lig_comp != this_lig_comp) { // ...unless that base ligature is ignorable. if (ligbase == .not_checked) { var found = false; const out = buffer.outInfo(); var j = buffer.outLen(); while (j > 0 and out[j - 1].ligId() == first_lig_id) { if (out[j - 1].ligComp() == 0) { j -= 1; found = true; break; } j -= 1; } ligbase = if (found and it.maySkip(&out[j]) == .yes) .may_skip else .may_not_skip; } if (ligbase == .may_not_skip) return false; } } else { // If the first component was not attached to a previous ligature // component, none of the others should be either, unless they are // attached to the first component itself. if (this_lig_id != 0 and this_lig_comp != 0 and this_lig_id != first_lig_id) return false; } total += this_info.ligNumComps(); }
end_position.* = it.idx + 1; if (total_component_count) |t| t.* = total + buffer.cur(0).ligNumComps(); c.match_positions[0] = buffer.idx; return true;}
/// `ligate_input`: replaces the matched glyphs with `lig_glyph`, and makes/// the marks among and after them point at the right component of it.pub fn ligateInput(c: *ApplyContext, count: usize, match_end: usize, lig_glyph: u32, total_component_count: u32) Error!void { const buffer = c.buffer; buffer.mergeClusters(buffer.idx, match_end);
// A base and marks ligating stay a base, so that marks after them can // still attach. All marks ligating is a mark ligature, which keeps its // ligature ID so it can still attach to a ligature in GPOS. Anything else // is a ligature, with a new ID. var is_base_ligature = buffer.info.items[c.match_positions[0]].isBaseGlyph(); var is_mark_ligature = buffer.info.items[c.match_positions[0]].isMark(); for (1..count) |i| { if (!buffer.info.items[c.match_positions[i]].isMark()) { is_base_ligature = false; is_mark_ligature = false; break; } } const is_ligature = !is_base_ligature and !is_mark_ligature;
const klass: u16 = if (is_ligature) GlyphProps.ligature else 0; const lig_id: u8 = if (is_ligature) allocateLigId(buffer) else 0; var last_lig_id = buffer.cur(0).ligId(); var last_num_components: u32 = buffer.cur(0).ligNumComps(); var components_so_far = last_num_components;
if (is_ligature) { buffer.cur(0).setLigPropsForLigature(lig_id, total_component_count); if (buffer.cur(0).generalCategory() == .non_spacing_mark) { buffer.cur(0).setGeneralCategory(.other_letter); } } try c.replaceGlyphWithLigature(lig_glyph, klass);
for (1..count) |i| { while (buffer.idx < c.match_positions[i] and buffer.successful) { if (is_ligature) { var this_comp: u32 = buffer.cur(0).ligComp(); if (this_comp == 0) this_comp = last_num_components; std.debug.assert(components_so_far >= last_num_components); const new_lig_comp = components_so_far - last_num_components + @min(this_comp, last_num_components); buffer.cur(0).setLigPropsForMark(lig_id, new_lig_comp); } try buffer.nextGlyph(); } last_lig_id = buffer.cur(0).ligId(); last_num_components = buffer.cur(0).ligNumComps(); components_so_far += last_num_components; // Skip the base glyph. buffer.idx += 1; }
if (!is_mark_ligature and last_lig_id != 0) { // Re-adjust components for any marks following. for (buffer.info.items[buffer.idx..]) |*info| { if (last_lig_id != info.ligId()) break; const this_comp = info.ligComp(); if (this_comp == 0) break; std.debug.assert(components_so_far >= last_num_components); const new_lig_comp = components_so_far - last_num_components + @min(this_comp, last_num_components); info.setLigPropsForMark(lig_id, new_lig_comp); } }}
fn allocateLigId(buffer: *Buffer) u8 { while (true) { const id = buffer.nextSerial() & 0x07; if (id != 0) return id; }}
/// `match_backtrack`: matches `count` glyphs before the output position,/// nearest first.pub fn matchBacktrack(c: *ApplyContext, count: usize, m: Match, match_start: *usize) bool { if (count == 0) { match_start.* = c.buffer.backtrackLen(); return true; } const it = &c.iter_context; it.resetBack(c.buffer.backtrackLen()); it.setMatch(m); for (0..count) |_| { var unsafe_from: usize = 0; if (!it.prev(&unsafe_from)) { match_start.* = unsafe_from; return false; } } match_start.* = it.idx; return true;}
/// `match_lookahead`: matches `count` glyphs from `start_index`.pub fn matchLookahead(c: *ApplyContext, count: usize, m: Match, start_index: usize, end_index: *usize) bool { if (count == 0) { end_index.* = start_index; return true; } const it = &c.iter_context; std.debug.assert(start_index >= 1); it.reset(start_index - 1); it.setMatch(m); for (0..count) |_| { var unsafe_to: usize = 0; if (!it.next(&unsafe_to)) { end_index.* = unsafe_to; return false; } } end_index.* = it.idx + 1; return true;}
/// `apply_lookup`: applies a matched rule's lookup records, each at its/// position in the matched input, keeping track of the positions as the/// lookups add and remove glyphs.pub fn applyLookup(c: *ApplyContext, count_in: usize, records: []const layout.SequenceLookupRecord, match_end: usize) Error!void { const buffer = c.buffer; var count = count_in;
// All positions are distances from the beginning of the *output* buffer. const bl = buffer.backtrackLen(); var end: isize = @as(isize, @intCast(bl + match_end)) - @as(isize, @intCast(buffer.idx)); { const delta: isize = @as(isize, @intCast(bl)) - @as(isize, @intCast(buffer.idx)); for (c.match_positions[0..count]) |*p| p.* = @intCast(@as(isize, @intCast(p.*)) + delta); }
for (records) |rec| { if (!buffer.successful) break; const idx = rec.sequence_index; if (idx >= count) continue;
const orig_len = buffer.backtrackLen() + buffer.lookaheadLen(); // This can happen if earlier recursed lookups deleted many entries. if (c.match_positions[idx] >= orig_len) continue;
if (!try buffer.moveTo(c.match_positions[idx])) break; if (buffer.max_ops <= 0) break;
if (!try c.recurse(rec.lookup_list_index)) continue;
const new_len = buffer.backtrackLen() + buffer.lookaheadLen(); var delta: isize = @as(isize, @intCast(new_len)) - @as(isize, @intCast(orig_len)); if (delta == 0) continue;
// The recursed lookup changed the buffer's length. If it grew by n, // assume n glyphs were added right after the current position; if it // shrank by n, assume n match positions after it were removed. end += delta; if (end < @as(isize, @intCast(c.match_positions[idx]))) { // End might end up being smaller than the current position if // the recursed lookup removed many items. Never rewind end beyond // the start of the current position. delta += @as(isize, @intCast(c.match_positions[idx])) - end; end = @intCast(c.match_positions[idx]); }
var next: usize = idx + 1; // The position after the recursed lookup.
if (delta > 0) { if (@as(usize, @intCast(delta)) + count > max_context_length) break; } else { // Delta is non-positive. delta = @max(delta, @as(isize, @intCast(next)) - @as(isize, @intCast(count))); next = @intCast(@as(isize, @intCast(next)) - delta); }
// Shift! const dst: usize = @intCast(@as(isize, @intCast(next)) + delta); if (dst != next) { const n = count - next; if (dst > next) { std.mem.copyBackwards(usize, c.match_positions[dst .. dst + n], c.match_positions[next .. next + n]); } else { std.mem.copyForwards(usize, c.match_positions[dst .. dst + n], c.match_positions[next .. next + n]); } } next = dst; count = @intCast(@as(isize, @intCast(count)) + delta);
// Fill in new entries. var j = idx + 1; while (j < next) : (j += 1) c.match_positions[j] = c.match_positions[j - 1] + 1;
// And fix up the rest. while (next < count) : (next += 1) { c.match_positions[next] = @intCast(@as(isize, @intCast(c.match_positions[next])) + delta); } }
std.debug.assert(end >= 0); _ = try buffer.moveTo(@intCast(end));}
/// `context_apply_lookup`.fn contextApplyLookup(c: *ApplyContext, input_count: usize, m: Match, records: []const layout.SequenceLookupRecord) Error!bool { if (input_count > max_context_length) return false; var match_end: usize = 0; if (matchInput(c, input_count, m, &match_end, null)) { c.buffer.unsafeToBreak(c.buffer.idx, match_end); try applyLookup(c, input_count, records, match_end); return true; } c.buffer.unsafeToConcat(c.buffer.idx, match_end); return false;}
/// `chain_context_apply_lookup`.fn chainContextApplyLookup( c: *ApplyContext, backtrack_count: usize, backtrack: Match, input_count: usize, input: Match, lookahead_count: usize, lookahead: Match, records: []const layout.SequenceLookupRecord,) Error!bool { if (input_count > max_context_length) return false; var start_index = c.buffer.outLen(); var end_index = c.buffer.idx; var match_end: usize = 0; const matched = blk: { if (!matchInput(c, input_count, input, &match_end, null)) break :blk false; end_index = match_end; break :blk matchLookahead(c, lookahead_count, lookahead, match_end, &end_index); }; if (!matched) { c.buffer.unsafeToConcat(c.buffer.idx, end_index); return false; } if (!matchBacktrack(c, backtrack_count, backtrack, &start_index)) { c.buffer.unsafeToConcatFromOutbuffer(start_index, end_index); return false; } c.buffer.unsafeToBreakFromOutbuffer(start_index, end_index); try applyLookup(c, input_count, records, match_end); return true;}
/// Sequence context, `GSUB` type 5 and `GPOS` type 7.pub fn applyContext(c: *ApplyContext, ctx: *const layout.SequenceContext) Error!bool { const gid: u16 = @truncate(c.buffer.cur(0).codepoint); switch (ctx.*) { .glyphs => |*f| { const index = f.coverage.get(gid) orelse return false; const rules = ruleSet(layout.SequenceRule, f.rule_sets, index); for (rules) |*r| { if (try contextApplyLookup(c, r.input.len + 1, .{ .glyph = r.input }, r.lookups)) return true; } return false; }, .classes => |*f| { _ = f.coverage.get(gid) orelse return false; const rules = ruleSet(layout.SequenceRule, f.rule_sets, f.class_def.get(gid)); for (rules) |*r| { if (try contextApplyLookup(c, r.input.len + 1, .{ .class = .{ .def = &f.class_def, .values = r.input } }, r.lookups)) return true; } return false; }, .coverages => |*f| { if (f.coverages.len == 0) return false; _ = f.coverages[0].get(gid) orelse return false; return contextApplyLookup(c, f.coverages.len, .{ .coverage = f.coverages[1..] }, f.lookups); }, }}
/// Chained sequence context, `GSUB` type 6 and `GPOS` type 8.pub fn applyChainContext(c: *ApplyContext, ctx: *const layout.ChainedSequenceContext) Error!bool { const gid: u16 = @truncate(c.buffer.cur(0).codepoint); switch (ctx.*) { .glyphs => |*f| { const index = f.coverage.get(gid) orelse return false; const rules = ruleSet(layout.ChainedSequenceRule, f.rule_sets, index); for (rules) |*r| { if (try chainContextApplyLookup( c, r.backtrack.len, .{ .glyph = r.backtrack }, r.input.len + 1, .{ .glyph = r.input }, r.lookahead.len, .{ .glyph = r.lookahead }, r.lookups, )) return true; } return false; }, .classes => |*f| { _ = f.coverage.get(gid) orelse return false; const rules = ruleSet(layout.ChainedSequenceRule, f.rule_sets, f.input_class_def.get(gid)); for (rules) |*r| { if (try chainContextApplyLookup( c, r.backtrack.len, .{ .class = .{ .def = &f.backtrack_class_def, .values = r.backtrack } }, r.input.len + 1, .{ .class = .{ .def = &f.input_class_def, .values = r.input } }, r.lookahead.len, .{ .class = .{ .def = &f.lookahead_class_def, .values = r.lookahead } }, r.lookups, )) return true; } return false; }, .coverages => |*f| { if (f.input.len == 0) return false; _ = f.input[0].get(gid) orelse return false; return chainContextApplyLookup( c, f.backtrack.len, .{ .coverage = f.backtrack }, f.input.len, .{ .coverage = f.input[1..] }, f.lookahead.len, .{ .coverage = f.lookahead }, f.lookups, ); }, }}
/// The rules of rule set `index`, or none for an index past the end or a/// NULL offset.fn ruleSet(comptime Rule: type, sets: []const ?[]const Rule, index: usize) []const Rule { if (index >= sets.len) return &.{}; return sets[index] orelse &.{};}