Clone in a terminal:
GlyphSequence.vala
/*
	Copyright (C) 2013 2015 2017 Johan Mattsson

	This library is free software; you can redistribute it and/or modify 
	it under the terms of the GNU Lesser General Public License as 
	published by the Free Software Foundation; either version 3 of the 
	License, or (at your option) any later version.

	This library is distributed in the hope that it will be useful, but 
	WITHOUT ANY WARRANTY; without even the implied warranty of 
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
	Lesser General Public License for more details.
*/
namespace BirdFont {

public class GlyphSequence : GLib.Object {
	
	/** A list of all glyphs */
	public Gee.ArrayList<Glyph?> glyph;

	/** A list of corresponding glyph ranges if applicable. */ 
	public Gee.ArrayList<GlyphRange?> ranges;

	OtfTags otf_tags;

	public GlyphSequence () {
		glyph = new Gee.ArrayList<Glyph?> ();
		ranges = new Gee.ArrayList<GlyphRange?> ();
		otf_tags = new OtfTags ();
	}
	
	public void set_otf_tags (OtfTags tags) {
		this.otf_tags = tags;
	}
	
	public int length () {
		return glyph.size;
	}
	
	public void add (Glyph? g) {
		glyph.add (g);
		ranges.add (null);
	}
	
	public void append (GlyphSequence c) {
		foreach (Glyph? g in c.glyph) {
			glyph.add (g);
		}

		foreach (GlyphRange? r in c.ranges) {
			ranges.add (r);
		}
	}
	
	/** Perform glyph substitution.
	 * @param tags enable otf features
	 * @return a new sequence with ligatures
	 */
	public GlyphSequence process_ligatures (Font font) {
		// FIXME add range to ligature
		GlyphSequence ligature_sequence = new GlyphSequence ();
		bool has_range = false;
		Ligatures ligatures;
		
		foreach (Glyph? g in glyph) {
			ligature_sequence.glyph.add (g);
		}
				
		foreach (GlyphRange? r in ranges) { 
			ligature_sequence.ranges.add (r);
			if (r != null) {
				has_range = true;
			}
		}
		
		// skip ligature substitution if this sequence contains ranges
		if (has_range) {
			return ligature_sequence;
		}

		ligatures = font.get_ligatures ();

		for (int index = 0; index < glyph.size; index++) {
			bool found = false;
			foreach (ContextualLigature c in  ligatures.contextual_ligatures) {
				if (c.is_valid ()) {
					found = ligature_sequence.replace_contextual (c.get_backtrack (),
						c.get_input (),
						c.get_lookahead (),
						c.get_ligature_sequence (),
						ref index);

					if (found) {
						index--;
						break;
					}
				}
			}
		}

		ligatures.get_single_substitution_ligatures ((substitute, ligature) => {
			ligature_sequence.replace (substitute, ligature);
		});
		
		// salt and similar tags
		foreach (string tag in otf_tags.elements) {
			Gee.ArrayList<Alternate> alternates;
			alternates = font.alternates.get_alt (tag);

			foreach (Alternate a in alternates) {
				GlyphSequence old = new GlyphSequence ();
				Glyph? g = font.get_glyph_by_name (a.glyph_name);

				if (g != null) {
					old.add (g);
						
					if (a.alternates.size > 0) {
						// FIXME: pick one of several alternates
						string alt_name = a.alternates.get (0);
						Glyph? alt = font.get_glyph_by_name (alt_name);
						
						if (alt != null) {
							GlyphSequence replacement = new GlyphSequence ();
							replacement.add (alt);
							ligature_sequence.replace (old, replacement);
						} else {
							warning (@"Alternate does not exist: $(alt_name)");
						}
					}
				} else {
					warning (@"Alternative for a missing glyph: $(a.glyph_name)");
				}
			}
		}
		
		ligature_sequence.ranges.clear ();
		for (int i = 0; i < ligature_sequence.glyph.size; i++) {
			ligature_sequence.ranges.add (null);
		}
		
		return ligature_sequence;
	}
	
	void replace (GlyphSequence old, GlyphSequence replacement) {
		int i = 0;
		while (i < glyph.size) {
			if (starts_with (old, i)) {
				glyph = substitute (i, old.glyph.size, replacement);
				i += replacement.length ();
			} else {
				i++;
			}
		}
	}

	bool replace_contextual (GlyphSequence backtrack, GlyphSequence input, GlyphSequence lookahead, 
		GlyphSequence replacement, ref int index) {
			
		bool start, middle, end;
		int advance = 0;

		start = starts_with (backtrack, index);
		middle = starts_with (input, index + backtrack.length ());
		end = starts_with (lookahead, index + backtrack.length () + input.length ());

		if (start && middle && end) {
			glyph = substitute (index + backtrack.length (), input.length (), replacement);
			advance = backtrack.length () + replacement.length ();
			index += advance;

			if (advance <= 0) {
				warning ("No advancement.");
				return false;
			}

			return true;
		}

		return false;
	}
		
	bool starts_with (GlyphSequence old, uint index) {
		Glyph? gl;

		foreach (Glyph? g in old.glyph) {
			if (index >= glyph.size) {
				return false;
			}
			
			gl = glyph.get ((int) index);
		
			if (g != gl) {
				return false;
			}
			
			index++;
		}
		
		return true;
	}
	
	Gee.ArrayList<Glyph?> substitute (uint index, uint length, GlyphSequence substitute) {
		Gee.ArrayList<Glyph?> new_list = new Gee.ArrayList<Glyph?> ();
		int i = 0;
		
		foreach (Glyph? g in glyph) {
			if (i == index) {
				foreach (Glyph? gn in substitute.glyph) {
					new_list.add (gn);
				}
			}

			if (!(i >= index && i < index + length)) {
				new_list.add (g);
			}

			i++;
		}
		
		return new_list;
	}

	public string to_string () {
		StringBuilder s = new StringBuilder();
		foreach (Glyph? g in glyph) {
			
			if (g != null) {
				Glyph gl = (!) g;
 
				if (gl.unichar_code == '\0') {
					s.append(@"[$(gl.name)]");
				} else {
					string unichar_code = (!) gl.unichar_code.to_string ();
					s.append(unichar_code);
				}
			} else {
				s.append(@"[null]");
			}
		}

		return s.str;
	}
}

}

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112