/*
    Title:  Myanmar.GDL
    Author: M. Hosken, K.R. Stribley
    Description:    Unicode Myanmar Graphite description
    License:        Open Font License 1.1

0.01    MJPH    22-JUN-2001     Original
0.90    MJPH    10-JUN-2003     Add documentation, first candidate release
0.92    MJPH     4-AUG-2003     Improve line breaking algorithm
1.00    MJPH     7-AUG-2003     Release - no changes
1.01    MJPH    16-SEP-2003     Latest ZWJ and ZWNJ usages,
                                Start to finesse: centre clusters in wraps, narrow cons
                                with wide diacritics take a wide wrap.
1.02    MJPH     8-OCT-2003     Add u1004.med_u102F
1.03    MJPH    28-OCT-2003     Add support for contractions (reduplication) killer order
1.04	KRS		31-MAY-2004		Fixed some ligature issues with kinzi
1.05	KRS		09-JUN-2004		Added breakweights for section markers
								Fixed tall yecha for 1012 + wasway
								Swapped order of U+100E/U+100D stacked ligature
1.06    KRS     14-JUL-2004		Made yecha tall with U+1001 etc and Kinzi
                                Moved lower dot to right of U+101B when hatto is present
                                U+102D is now always above the consonant in a wrap
                                even if there is a u/uu vowel present
                                U+101B is short when there is a Yapin present
1.07	KRS		31-AUG-2004		Yecha is tall when U+1012 has a stacked consonant
1.08	KRS		04-SEP-2004		Numbers can now take upper diacritics for abbreviations
                                U+1004 U+1039 is now always Kinzi as per myanmar_uni.pdf
                                U+1004 U+200D U+1039 U+101d etc is used to prevent kinzi
1.09    KRS     06-DEC-2004     Yecha is tall when U+1002 has a stacked consonant
                                NLP propose using U+200D differently to UTN 11
                                Comment out the g101a, g101b, g101d, g101f, line in the
                                tKinzi class to give this behaviour
1.10    MJPH    13-MAY-2005     no break between sign and section, 
                                tidy up font, remove hack rules, add docs for APs
                                add necessary associations to insertion rules
1.11	MJPH	 2-AUG-2005	    Fix kern on killed tall 102C preceding 1031 glyph
1.12    MJPH    27-OCT-2005     Apply KRS changes to handle 'I', section breaks.
                                Use new breakweights. Change syllable break from 20 to 15
2.00    MJPH    15-APR-2006     Unicode 5.1 - remove tall-a logic, change to disunified medials, etc.
2.01    MJPH    24-APR-2006     Attach y-medials
2.02    MJPH    16-MAY-2006     udia over y-medial loses advance to stop -u from spacing
2.03    MJPH    14-JUN-2006     U+101E U+1039 U+101E was ligating as U+103F, now it stacks
2.04    MJPH    16-JUN-2006     no U103C_102F if following a stack, U+100A U+102D U+102F handled
2.05    MJPH    10-JUL-2006     Add left ldot class for Sgaw, 1018 added to cConsWide
2.1     MJPH     2-FEB-2007     Add PDAM4 stuff
2.1.1   MJPH    20-FEB-2007     Create cPreVowel to add U+1080
2.2     MJPH    31-OCT-2007     Add chars for v2.2
2.2.1   MJPH    20-NOV-2007     U+1029 can have U+1031
                                */

Myanmar GDL Code

This file contains the font independent GDL for rendering Myanmar from Unicode. It is designed to be used in conjunction with and included by the font specific GDL code that is created automatically from the .ttf font and .xml attachment point database.

This font specific code creates glyph definitions for each glyph, including all the attachment points, order attribute and possibly some kerning values. The order and kerning values are entered as part of the font design itself and propagate through the AP database to the GDL code.

In addition the font specific GDL code contains various automatically generated classes dependent populated by glyphs dependent upon which attachment points they have. For more details on this process see the documentation for make_gdl.pl

Definitions

Since the GDL is passed through a C preprocessor, we can define various shortcuts to make life easier in the rest of the description.

The first set of definitions provide a standard way of specifying optional slot sequences. Usually the sequence consists of only one element (usually a glyph class). But it can be a sequence if necessary. We build up to allow for 0 to 4 occurrences of the sequence.

#define opt(x)      [x]?
#define opt2(x)     [opt(x) x]?
#define opt3(x)     [opt2(x) x]?
#define opt4(x)     [opt3(x) x]?

GDL allows for 16 user slot attributes that we can use for just about anything. Here we give the ones we intend to use sensible names for use later on. Notice that these differ from the user glyph attributes which do not have to have a special name.

#define attached    user1
#define hasadv      user2

We assign names to the various types of breakweight.

#define BW_WORD     10
#define BW_SYLL     15
#define BW_CHAR     30
#define BW_CLIP     40
#define BW_NEVER    50

For clarity we also give each of the substitution passes a name. This was originally done because I didn't know if I was going to have to insert a pass into the sequence and wanted to be able to re-allocate pass numbers quickly and easily

#define pass_medial 1
#define pass_insert 2
#define pass_front 3
#define pass_tidy 4

There are a few glyph names that I prefer to rename. I do that here just by defining my name to be the correct name

#define g25cc g_circledash
#define gKill g1039
#define g200C g200c
#define zwsp  g200b
#define wj    g2060
#define gSpace g_space

ANY is a special name in GDL and includes no glyph. I want to define ANY to require a glyph so I redifine the name (well mask it via #define) and then create a class later of all glyphs.

#define ANY ANYGlyph

Setup various global constants in GDL. This is left to right only script. Also let's have a little extra space above, just to prove it can be done!

Bidi = 0;
//ExtraAscent = 100m;
//ExtraDescent = 500m;
AutoPseudo = 1;

Glyph Table

Most of the glyph table is defined in the font specific file which is autogenerated from the font and attachment point database. Here we list all the behaviour specific classes that we hand craft as part of this description.

table(glyph);

MAXGLYPH comes from the font specific file and tells us how many glyphs there are in the font. We use this to make a class of all glyphs. And while we are about it, set all glyphs to have a default line-breaking behaviour of not wanting a break before the glyph. Our line-breaking model is based on what happens before a glyph rather than after it.

ANY = (glyphid(0 .. MAXGLYPH)){breakweight = -BW_CHAR};         // what about pseudo glyphs?

Set various default line-breaking weights for particular glyphs. We like breaking after whitespace and we never break before a killer (U+1039)

zwsp {breakweight = BW_WORD};        // assume zero width
gSpace {breakweight = BW_WORD};
cSection = (g104a, g104b) {breakweight = BW_WORD};
gKill {breakweight = -BW_NEVER};
//g200d {breakweight = -5};
wj {breakweight = -BW_CHAR};
// encourage breaking after signs
cSigns = (g104c, g104d, g104f) {breakweight = BW_WORD};
cNum = (g1040, g1041, g1042, g1043, g1044, g1045, g1046, g1047, g1048, g1049) {breakweight = -BW_CHAR};
// discourage breaking inside quotes
cLQuote = (g_parenleft, g_quotedblleft, g_quoteleft) {breakweight = -BW_SYLL};
cRQuote = (g_parenright, g_quotedblright, g_quoteright) {breakweight = BW_WORD};

List all consonants. We include U+1021 as a consonant since it behaves like one for all intents and purposes. We allow line-breaking before a consonant since the default is that it starts a new syllable. We have rules for the cases when it does not. We also list consonants by shape: narrow or wide (for the wrap) 25cc added to allow display teaching documents as narrow consonant

cCons = (g1000, g1001, g1002, g1003, g1004, g1005, g1006, g1007,
         g1008, g1009, g100a, g100b, g100c, g100d, g100e, g100f,
         g1010, g1011, g1012, g1013, g1014, g1015, g1016, g1017,
         g1018, g1019, g101a, g101b, g101c, g101d, g101e, g101f, 
         g1020, g1021, g1022, g1025, g1027, g1029, g25cc, g103f, g1028, g104e,
         g105a, g105b, g105c, g105d, g1061, g1065, g1066, g106e,
         g106f, g1070, g1075, g1076, g1077, g1078, g1079, g107a,
         g107b, g107c, g107d, g107e, g107f, g1080, g1081, g108e){breakweight = -BW_SYLL};
cConsNar = (g1001, g1002, g1004, g1005, g1007, g100b, g100c, g100d, g100e,
          g1012, g1013, g1014, g1015, g1016, g1017, g1019, g101b, g101d, 
          g25cc, g1014_alt, g1027, g1028, g104e, g105a, g105b, g105c,
          g105d, g1061, g1065, g1066, g1075, g1076, g1077, g107f, g108e);
cConsWide = (g1000, g1003, g1006, g1008, g1009, g100a, g100f, g1010, g1011,
             g1018, g101a, g101c, g101e, g101f, g1020, g1021, g1022, g1029, g103f, g106e,
             g106f, g1070, g1078, g1079, g107a, g107b, g107c, g107d, g107e,
             g1080);

This class lists all the consonants that take the short form of -u and -uu (U+102F, U+1030)

cConsSVowel = (g1000, g1001, g1002, g1003, g1004, g1005, g1006, g1007, g100e, g100f,
               g1010, g1011, g1012, g1013, g1014, g1015, g1016, g1017, g1018, g1019,
               g101a, g101b, g101c, g101d, g101e, g101f, g1021, g1022, g1027, g25cc, g100a_alt,
               g101b_alt, g1014_alt, g103f, g105c, g1065, g106e, g1075, g1076, g1077,
               g1078, g1079, g107b, g107c, g107d, g107f, g1080, g108e);

Lists all the consonants that require a medial h to slant

cConsSlantH = (g1009, g100a, g105a, g105b, g105d);

Here we list all the medial forms of glyphs that don't have special behaviour when medialised. We list the medial forms, those that are single width and the base forms that lead to the medial forms we have listed. This is the list of all stacked consonants as opposed to true medials. Yes, the naming could do with some revision! 25cc added as base to allow display of stacked glyphs in teaching documents

cMed = (g1000_med, g1001_med, g1002_med, g1003_med, g1005_med, g1006_med, g1007_med,
        g1008_med, g100a_med, g100c_med, g100d_med, g100f_med, 
        g1010_med, g1011_med, g1012_med, g1013_med, g1014_med, g1015_med, g1016_med, g1017_med,
        g1018_med, g1019_med, g101b_med, g101c_med, g101e_med, g1021_med, g105a_med, g105b_med, g105c_med);
cMedNar = (g1001_med, g1002_med, g1005_med, g1007_med, g100c_med, g100d_med,
           g1012_med, g1013_med, g1014_med, g1015_med, g1016_med, g1017_med,
           g1019_med, g101b_med, g105a_med, g105c_med);
cMedBase = (g1000, g1001, g1002, g1003, g1005, g1006, g1007,
            g1008, g100a, g100c, g100d, g100f,
            g1010, g1011, g1012, g1013, g1014, g1015, g1016, g1017,
            g1018, g1019, g101b, g101c, g101e, g1021, g105a, g105b, g105c);

A list of all the forms that U+101B can take when not medialised

c101b = (g101b, g101b_alt, g101b_long);

Cluster characters are what I call those characters that are included as part of a syllable when medialised. I.e. are true medials. Whereas the other characters indicate syllable chaining when medialised.

cClusMed = (g103b, g103c, g103d, g103e, g105e, g105f, g1060);
cClusDia = (g103b, g103b_103d, g103b_103d_103e, g103b_103e, g103d, g103d_103e, g103e, 
            g103e_102f, g103e_1030, g105e, g105f, g1060);

y-medials are diacritics, but they don't have attachment points to attach to the base character with. So we need to write a special rule to attach them so that inter character spacing and cursors cannot occur between the base and the diacritic.

cYMed = (g103b, g103b_103d, g103b_103d_103e, g103b_103e);

There are two prevowels that reorder before their base consonant cluster

cPreVowel = (g1031, g1084);

The lower vowels must occur in a particular place in the order. (See the section on ensuring correct ordering). They may be rendered as full height, or short form (overloading medial again!). Then there is the combined medial form with -h. Finally we create a class that includes all of them in whatever form.

cLVowel = (g102f, g1030);
cLVowelM = (g102f_med, g1030_med);
cLVowelh = (g103e_102f, g103e_1030);
cLVowelAll = (cLVowel, cLVowelh, cLVowelM);

We must never break a line before an upper vowel. Upper vowels are made more complicated by the existence of kinzi ligatures. We include upper dot as an upper vowel. This breaks the strict interpretation of the character ordering in a cluster, but we get away with it since we are being looser than the standard and this would be the rendering behaviour we would want if the sequence: cons U+1036 U+102C were to occur.

cUVowel = (g103a, g1004_med, g1004_med_102d, g1004_med_102e, g1004_med_1036, g102d,
            g102e, g1032, g1032_102d, g1033, g1034, g1071, g1072, g1073, g1074,
            g1081, g1082){breakweight = -BW_NEVER};
cUSpace = (g103a, g102d, g102e, g1004_med_102e, g1004_med_102d, g1004_med,
            g1004_med_1036, g1033, g1071, g1072, g1073, g1074, g1081);
cUVowelNga = (g102e, g102d, g1033, g1036);
cNgaUVowel = (g1004_med_102e, g1004_med_102d, g1004_med_1033, g1004_med_1036);
cUTakesMa = (g102d, g1004_med);
cUWithMa = (g102d_1036, g1004_med_1036);
c1036 = (g1036, g1004_med_1036, g102d_1036);

Create a general below diacritic class consisting of single and double width below diacritics.

cBDia = (cBSDia, cBDDia);

Create classes for base characters that change shape when they have a below diacritic

cLDiaMod = (g100a, g1014, g101b);
cLDiaModed = (g100a_alt, g1014_alt, g101b_alt);

For each medialised cluster glyph we list all the forms it may take, including when it ligates with another medial.

c103b = (g103b, g103b_103e, g103b_103d);
c103d = (g103d, g103d_103e, g103b_103d);
c103e = (g103e, g103e_102f, g103e_1030, g103d_103e, g103b_103e);

U+1039 U+101B (wrap) takes numerous forms depending on context. We list various sets of these forms here. First we separate the non-ligating forms and the ligating forms. Then we list by width: narrow and wide. And then we also list be normal and alternate forms. Alternate forms are those with a gap at the top for a spacing upper vowel.

The last set is of the various forms of the medialised U+101F in its non-ligating form.

c103c_only = (g103c, g103c_wide, g103c_alt_narr, g103c_alt_wide);
c103c_mix = (g103c_103d_narr, g103c_103d_wide, g103c_102f_narr, 
        g103c_102f_wide, g103c_103d_alt_narr, g103c_102f_alt_narr, 
        g103c_103d_alt_wide, g103c_102f_alt_wide);
c103c = (c103c_only, c103c_mix);
c103c_nar = (g103c, g103c_103d_narr, g103c_102f_narr);
c103c_naralt = (g103c_alt_narr, g103c_103d_alt_narr, g103c_102f_alt_narr);
c103c_wide = (g103c_wide, g103c_103d_wide, g103c_102f_wide);
c103c_widalt = (g103c_alt_wide, g103c_103d_alt_wide, g103c_102f_alt_wide);
c103e_dia = (g103e, g103e_alt);

Then we list yet more combinations of the wrap as needed by various rules. And we also create a list of everything that can go underneath something, calling it a lower diacritic. We also list all the forms of the tall form of U+102C.

c103c_compl = (g103c_102f_narr, g103c_102f_wide);
c103c_compr = (g103c_103d_narr, g103c_103d_wide);
c103c_medn = (g103c, g103c_wide, g103c_alt_narr, g103c_alt_wide);
c103c_in = (g103e_alt, g103d, g103d_103e, cMed);
cLowDia = (g103d, g103d_103e, g103e, g103e_102f, g103e_1030, cMed, cLVowelAll);
c102b = (g102b, g102b_103a);

We separate out the rules for each base character that changes shape when it has other diacritics to interact with it (usually underneath). These classes list the various diacritics that cause the particular base character to change shape.

t1014 = (cMed, c103d, c103e, cLVowelAll, c103b);
t100a = (t1014);

A list of glyphs that move the ldot to the left, particular for Sgaw

cLeftLDot = (g103b, g1061);

Finally we have a list of glyphs that require special kerning when interacting with U+101B as a base character

cHasRkern = (g103e_102f, g103e_1030, g1030_med, g102f_med);

endtable; // glyph table

Linebreaking

The basic linebreaking algorithm states that a linebreak may occur if the glyph before has a breakweight of 0 or a +ve breakweight <= to the breakweight we are testing for, or if the following glyph has a breakweight of 0 or a -ve breakweight with absolute value <= to the breakweight we are testing for. Put in negative terms, to stop a linebreak happening, both the previous glyph must either have a -ve breakweight or a breakweight > the test breakweight, and the following glyph must either have a +ve breakweight or a breakweight with absolute value > the test breakweight. Either way it's mind boggling!

For all this, we can actually do syllable based line-breaking in Myanmar very simply. The main rule is: If a consonant has a killer (but not just a medial), then it is part of the previous syllable and you can't line-break before it (unless you are desparate). There is another rule in this table that ensure that the default linebreak that can occur before a consonant is disabled when that consonant is medialised (i.e. follows a U+1039).

table(linebreak)

// encourage breaks between section and consonant
cSection {breakweight = BW_WORD} / _ ^ cCons;
gSpace {breakweight = BW_WORD} / _ ^ cCons;

// no line breaks before visibly killed char
cTakesUDia {breakweight = -BW_CHAR} g103a {breakweight = -BW_NEVER} / _ ^ [gKill cMed]? opt4(cnTakesUDia) _ ;

// g1004 {breakweight = -5} gKill {breakweight = -5} /   g200d  _ ^; 

// no line breaks before a syllable chained consonant
cCons {breakweight = -BW_CHAR} cMedBase {breakweight = -BW_NEVER} / _ gKill _ ;
cCons {breakweight = -BW_CHAR} cClusMed {breakweight = -BW_NEVER};

// no line breaks before virama
cCons {breakweight = -BW_NEVER} / gKill _ ;

// no line break before WJ
cCons {breakweight = -BW_CHAR} / wj _ ;

// discourage breaks after aa before another cons
cCons {breakweight = -BW_CHAR} / g1021 _;

// discourage breaks around quotes
ANY {breakweight = +BW_CLIP} / cLQuote ^ _;
ANY {breakweight = -BW_CLIP}  / _ ^ cRQuote;

// discourage breaks within numbers
cSection {breakweight = +BW_CHAR} / _ cNum;  
// encourage breaks between consonants and numbers
cNum {breakweight = -BW_SYLL} / cCons _;
cSigns {breakweight = -BW_SYLL} / cSection _;

// discourage break between sign and a section
cSigns {breakweight = +BW_CLIP} / _ cSection; 


endtable; // linebreak table

Substitution

The substitution table is where most of the work is done. We break the process into 3 passes. The purpose of the first pass is to get everything down to single glyphs wherever possible. This means we are trying to remove any gKills from the slot stream. We also try to do as much as we can here, in terms of dealing with kinzi and simple ligatures. We don't do any re-ordering (apart from kinzi which is so wierd that it's best if we get it out of the way as soon as we can). By the end of this pass, there should be no gKills left in the slot stream, likewise g200C.

Notice the explicit declaration of underlying to surface associations.

table(substitution);

pass(pass_medial);

// Note: if 200D is present this rule will not match in accordance with UTN 11
g1004 g103a gKill _ > _ _ _ g1004_med:(1 2 3) / 
    _ _ _ ^ (cCons, cNum, g104e) [gKill cMedBase]? opt4(cClusMed) cPreVowel? _;      // need opt4() here for opt()
g1004 g103a gKill cUVowelNga > _ _ _ cNgaUVowel:(1 2 3 4) /
    _ _ _ ^ (cCons, cNum, g104e) [gKill cMedBase]? opt4(cClusMed) cPreVowel? _;
gKill cMedBase > _ cMed;


// lots of ligatures, as many as we can do adjacently
gKill g1010 g103d > g1010_103d_med:(1 2 3) _ _;
g100a gKill g100a > g100a_100a:(1 2 3) _ _;
g100b gKill g100b > g100b_100b:(1 2 3) _ _;
g100b gKill g100c > g100b_100c:(1 2 3) _ _;
g100d gKill g100d > g100d_100d:(1 2 3) _ _;
g100e gKill g100d > g100d_100e:(1 2 3) _ _; // name wrong way around
g100f gKill g100b > g100f_100b:(1 2 3) _ _;
g100f gKill g100d > g100f_100d:(1 2 3) _ _;
g1014 gKill g1010 g103c > g1014_1010_103c:(1 2 3 4) _ _ _;
g1014 gKill g1010 g103c > g1014_1010_103c:(1 2 3 4) _ _ _;
g101e gKill g1010 g103c > g101e_1010_103c:(1 2 3 4) _ _ _;
g1032 g102d > g1032_102d:(1 2) _ ;
g103b g103d g103e > g103b_103d_103e:(1 2 3) _ _;
g103b g103e > g103b_103e:(1 2) _;
g103c g103d g103e > @1 g103d_103e _;
g103c g103d > g103c_103d_narr:(1 2) _;
g103d g103e > g103d_103e_small:(2 3) _ / g103c _ _ ;
g103d g103e > g103d_103e:(1 2) _;
g103b g103d > g103b_103d:(1 2) _;
gKill cMedBase g103c g102f > _ cMed @r @u / _ _ _=r cPreVowel? cUVowel? _=u;
g103c g103e g102f > @r @h @u / _=r _=h cPreVowel? cUVowel? _=u;
g103c g103e g1030 > @r @h @u / _=r _=h cPreVowel? cUVowel? _=u;
g103c g102f > g103c_102f_narr:(1 4) _ / _ cPreVowel? cUVowel? _;
g103e g102f > g103e_102f:(1 4) _ / _ cPreVowel? cUVowel? _;
g103e g1030 > g103e_1030:(1 3) _ / _ cPreVowel? _;


// there should be no gKill left now
// gKill > _;
g200C > _;
g200d > _;
wj > _;

endpass;

Ensuring Correct Ordering

The following pass contains a standard idiom, that can be used for any script where glyph ordering is considered important. Each glyph contains a glyph attribute called order. The order value of a glyph specifies its required order in relation to those around it. The purpose of this pass is to highlight where glyphs are ordered incorrectly in the slot stream (and so, in the underlying text) by inserting a dotted circle before the offending diacritic.

A glyph with order == 0 is considered not to be playing. I.e. it isn't a base character and it isn't a diacritic. Such glyphs may not take diacritics and if a diacritic occurs following one of these glyphs, a dotted circle is inserted.

A glyph with order == 1 is considered to be a base character and may take diacritics.

Any glyph with order > 1 is considered to be a diacritic and must not occur following a glyph with an order greater than the order of the glyph we are interested in. For example we can't have glyphs in the order 1, 7, 5. but 1, 5, 7 is OK.

All this in two rules! Mind you the two rules do check every glyph.

#if (1)

pass(pass_insert);

A special rule to handle contractions (reduplications). It allows a killer directly after a consonant and for any diacritic to follow that killer. Notice that the rule needs to be length 3, longer than the other rules to take priority.

ANY > @3 / cCons g103a ^ _{order > 1};

// these rules have sort length 3 which is awkward for any ligature processing

_ > g25cc:1 / ANY _ ^ ANY{order > 1 && order <= @1.order};
_ > g25cc:1 / ANY{order == 0} _ ^ ANY{order > 1};

endpass;

#endif

Main Pass

Now we have single glyphs all in the right order (at least in storage terms, excepting kinzi). We now need to re-order everything and get the right contextual form and generally sort everything out. Notice that we don't advance the cursor beyond a base char, we leave that to the fallback rule. This ensures that we have the maximum opportunity to re-order things before the base character.

pass(pass_front);

U+1014 takes the short tail form when wrapped

g1014 > g1014_alt / (c103c_nar, c103c_naralt) _ ;

Moves the -e vowel to the front. Leave cursor where it was (the rule will not rematch, because we have removed the -e).

_ cPreVowel > @e:e _ / _ ^ c103c? cCons cMed? opt4(cClusDia) _=e;

Here we get the right wrap into the right place. We start with a narrow form. Usually the glyph is g103c, but ligatures can occur in the first pass to create the other forms in that class. Each rule, therefore, maps from c101b_nar. The different rules match according to which corresponding class the wrap glyphs should be mapped to. We also move the wrap to the front (after -e if it is there) and rematch the consonant.

// suboptimal matches all around here
_ c103c_nar > c103c_wide:r$r _ / ^ _ cConsWide cMed? c103b? _=r;
_ c103c_nar > c103c_naralt:r$r _ / ^ _ cConsNar cMedNar? _=r c103d? c103e? cUSpace;
_ c103c_nar > c103c_widalt:r$r _ / ^ _ cConsWide cMed? _=r c103d? c103e? cUSpace;
_ c103c_nar > c103c_naralt:r$r _ / ^ _ cConsNar g103a cMedNar? _=r;
_ c103c_nar > c103c_widalt:r$r _ / ^ _ cConsWide g103a cMed? _=r;
_ c103c_nar > @r:r _ / ^ _ cConsNar cMedNar? c103b? _=r;
_ c103c_nar > c103c_wide:r$r _ / ^ _ cConsNar cMed? c103b? _=r;

Rules to handle U+101B in its base form (not as a wrap). Whether we need a short leg form or a full length leg with no foot.

g101b > g101b_alt / ^ _ c103e? cUVowel? cLVowelAll;
g101b > g101b_alt / ^ _ c103d;
g101b > g101b_alt / ^ _ c103b;
g101b > g101b_long / ^ _ g103e;
g101b > g101b_long / ^ _ cMed cUVowel? cLVowel;

Map full height lower vowels into short form, if appropriate. Also handle special glyphs when inside a wrap.

// krs the next rule is to prevent medial being substitued with wrap
// (wrap is by now in front of consonant)
g1030 > @u / c103c_only cConsSVowel cUVowel? _=u ^;
cLVowel > cLVowelM / ^ cConsSVowel cUVowel? _;
g1014 cLVowel > g1014_alt:(1) cLVowelM:(3) / ^ _ cUVowel? _;
g100a cLVowel > g100a_alt:(1) cLVowelM:(3) / ^ _ cUVowel? _;
g103d_103e > g103d_103e_small / c103c cCons _;
cUTakesMa g1036 > cUWithMa:(1 3) _ / _ cLVowel? _;

Glyphs that take an alternate form with certain diacritics.

g1014 > g1014_alt / ^ _ t1014;
g100a > g100a_alt / ^ _ t100a;
g1009 > g1025 / ^ _ g103a;          // is this the rule?
g1009 > g1025 / ^ _ c103e? cPreVowel? cUVowel? cCons;

When does the -h slant? Inside a wrap and after a consonant that forces it to slant.

g103e > g103e_alt / ^ c103c cConsSVowel _;
g103e > g103e_alt / cConsSlantH _;

Tall -a vowel ligature

g102b g103a > g102b_103a:(1 2) _;
g1062 g103a > g1062_103a:(1 2) _;

endpass;

endtable; // substitution table

Positioning

The positioning process is done in two passes. The first passes attaches diacritics to their base character. This is complicated by the lower dot (U+1037) which is attached contextually. Each rule in the first pass follows a standard idiom:

base dia {attach {to = @1; at = APS; with = APM}; attached = 1} / ^ _ opt3(nBase) _{attached == 0};

base is the class of things that dia can attach to. The aim of the rule is to match a dia with the most immediate glyph it can attach to. Thus base should include everything that dia can attach to. In some cases the base class includes the contents of the dia class, when diacritics can stack.

When the rule matches, we attach the dia to the base using the attachment point APS on the base and the APM attachment point on the dia. Notice it is the dia that moves to attach to the base and not the other way around.

Because we need to match the same base multiple times for the multiple dias that can attach to it, we need to keep the cursor in front of the base until everything is attached and we can move on. So that we do not keep rematching and reattaching the same dia we mark each glyph we attach as being attached, using the attached slot attribute (which resolves to user1). Thus we only want to match dias with attached of zero. Thus the environment is the base followed by everything that cannot be a base or dia followed by a dia with attached == 0.

By using the attached slot attribute, we can have multiple types of diacritics that attach to a base character at and with different attachment points. Thus multiple of the above rules can be used together and interact correctly.

The rendering approach here is not necessarily the best that can be used, but it does show that different approaches to solving the same problem can be used very easily with Graphite.

The attachment points have the following names:

BSS
attach single width lower diacritic to here
BDS
attach double width lower diacritic to here
BDM
attach lower double width diacritic using this AP
BSM
attach lower single width diacritic using this AP
LS
attach lower dot to here
LLS
attach left moving lower dot to here
LM
use this AP when attaching lower dot
RM
use when attaching base to wrap
RL
attach base to wrap here
US
attach upper diacritic to here
UM
use this AP when attaching upper diacritic
table(positioning);

pass(1);

The normal advance width for a wrap is just beyond the left hand stem. Since we actually attach the base character to the wrap, we need to change the advance of the wrap to be the full width of the wrap. We do this here.

c103c {advance.x = advx; hasadv = 1} / ^ _{hasadv == 0};

First we address the lower dot (U+1037) and decide which glyph is its base character. In different contexts it takes a different base character. We describe them all in the environments. The final fallback is to treat g1037 as a simple cBSDia in a later rule, so we only need the specials here.

// This lot is a bit messy. We could probably simplify this, but at least it works!

cConsSVowel g1037 {attach {to = @2; at = LS; with = LM}; attached = 1} \
                                        / ^ c103c_only? _ cUDia? _{attached == 0};
c103c g1037 {attach {to = @1; at = LS; with = LM}; attached = 1} \
                                        / ^ _ cConsSVowel cMed c103e_dia? cUDia? _{attached == 0};
c103c g1037 {attach {to = @1; at = LS; with = LM}; attached = 1} \
                                        / ^ _ cConsSVowel cMed? c103e_dia cUDia? _{attached == 0};
c103c_mix g1037 {attach {to = @1; at = LS; with = LM}; attached = 1} \
                                        / ^ _ cConsSVowel cUDia? _{attached == 0};
c101b g1037 {attach {to = @1; at = LS; with = LM}; attached = 1} \
                                        / ^ _ (cMed, cClusDia) cUVowel? cLVowelM? c1036? _{attached == 0};
c101b g1037 {attach {to = @1; at = LS; with = LM}; attached = 1} \
                                        / ^ _ cUVowel? cLVowelM c1036? _{attached == 0};
c101b g1037 {attach {to = @1; at = LLS; with = LM}; attached = 1} \
                                        / ^ _ cUDia? _{attached == 0};
cLeftLDot g1037 {attach {to = @1; at = LLS; with = LM}; attached = 1} \
                                        / ^ _ _ {attached == 0}; 

// rearrange 102d when there is a full height u/uu so that it goes on top of consonant
//cCons cUDia {attach {to = @a; at = US; with = UM}; attached = 1} \
//                                        / ^ _=a c103e_dia? cLVowel _{attached == 0};
// take precedence over default udia attachment rule
cTakesUDia cUDia {attach {to = @u; at = US; with = UM}; attached = 1} cLVowel \
                                          / ^ _ opt2(cnTakesUDia) _{attached == 0} _=u;

Now we do the standard attachment rules. Notice that we first try to attach a wide lower diacritic to its corresponding AP on the base character. Failing that we fall back to using the same AP as the narrow diacritics. This is controlled by the contents of the cTakes classes which are automatically generated as part of generating the font specific GDL, based on which APs a glyph has.

cTakesBSDia cBSDia {attach {to = @1; at = BSS; with = BSM}; attached = 1} \
                                        / ^ _ opt(cnTakesBSDia) _{attached == 0};
cTakesBDDia cBDDia {attach {to = @1; at = BDS; with = BDM}; attached = 1} \
                                        / ^ _ opt(cnTakesBDDia) _{attached == 0};
cTakesBSDia cBDDia {attach {to = @1; at = BSS; with = BDM}; attached = 1} \
                                        / ^ _ opt(cnTakesBSDia) _{attached == 0};
cTakesUDia cUDia {attach {to = @1; at = US; with = UM}; attached = 1} \
                                        / ^ _ opt2(cnTakesUDia) _{attached == 0};

Attaching the base character to the wrap is a radical way of ensuring that the user cannot insert a cursor between the base character and the wrap.

cTakesRDia cRDia {attach {to = @1; at = RS; with = RM}; attached = 1; insert = 1} / ^ _ _=r{attached == 0};

More lower dot (U+1037 which is the contents of cLDia) rules. Getting these things right is a major part of the testing.

// c101b_med cLDia {attach {to = @l; at = LS; with = LM}; attached = 1} \
//                                      / ^ cCons cMed? opt2(cLowDia) _=l cUDia? _{attached == 0};
// cTakesLLDia cLDia {attach {to = @1; at = LLS; with = LM}; attached = 1} \
//                                      / ^ _ opt3(cnTakesLLDia) _{attached == 0};
// According to official rules, lower dot should be to right with h medial, so disable this rule
//g101f_med cLDia {attach {to = @1; at = LLS; with = LM}; attached = 1} \
//                                        / ^ _ opt3(cnTakesLDia) _{attached == 0};
cTakesLDia cLDia {attach {to = @1; at = LS; with = LM}; attached = 1} \
                                        / ^ _ opt3(cnTakesLDia) _{attached == 0};

Attach the non-attached

cCons cYMed {attach.to = @1; attached = 1} / ^ _ cMed? g103a? _{attached == 0};

endpass;

Kerning

There is very little kerning to be done. We need to kern a diacritic in relation to U+101B if necessary. We also kern the next glyph following a killed tall -a towards the tall -a if it is not tall enough to hit the killer.

We also centre a cluster within its wrap. This is probably done more easily by using a centred attachment point in the wrap and the BDS on the consonant. But we don't have that information in the font.

pass(2);

g101b_alt {kern.x = @2.rkern + 10m} cHasRkern {shift.x = -rkern};
// c102c_tall {kern.x = -xkern} / cCons cLowDia? _;
c102b {kern.x = xkern / 2} / cUDia _;
c102b {kern.x = xkern / 2} / c103c cCons cLVowel? cLDia? _;
cConsNar / g102b_103a=a _ cLowDia? cUDia;
cCons {kern.x = -@a.xkern} / g102b_103a=a _;
cPreVowel {kern.x = -@a.xkern} / g102b_103a=a _;
cRDia {shift.x = (@r.advancewidth + @r.advance.x - advance.x) / 2 + @r.position.x - position.x} / cTakesRDia=r _;
cUVowel {advance.x = 0m} / c103b _ ;

// KRS try shifting 102D when it accompanies 1036 - it might be better to have a ligature for this
// the kern values have been set manually in the font gdl file, so will be lost when it is regenerated
//g102d {shift.x = -@1.xkern} g1036 {kern.x = @1.xkern + @2.xkern} / _ _;
//g102d g1036 {kern.x = @1.xkern + @2.xkern} / _ _;

endpass;

endtable; // positioning table