Ejemplo n.º 1
0
    def doit(args):
        logger = args.logger

        # Read input csv
        builder = FB.FTMLBuilder(logger, incsv = args.input, font = args.ifont)

        # Initialize FTML document:
        test = "ftml_builder test"
        ftml = FB.FTML(test, logger)

        # all chars that should be in the font:
        ftml.startTestGroup('Encoded characters')
        for uid in sorted(builder.uids()):
            if uid < 32: continue
            c = builder.char(uid)
            # iterate over all permutations of feature settings that might affect this character:
            for featlist in builder.permuteFeatures(uids = (uid,)):
                ftml.setFeatures(featlist)
                builder.render((uid,), ftml)
                # Test one character with RTL enabled:
                if uid == 67:
                    builder.render((uid,), ftml, rtl = True)
                # Don't close test -- collect consecutive encoded chars in a single row
            ftml.clearFeatures()
            for langID in sorted(c.langs):
                ftml.setLang(langID)
                builder.render((uid,), ftml)
            ftml.clearLang()

        # Write the output ftml file
        ftml.writeFile(args.output)
Ejemplo n.º 2
0
def doit(args):
    logger = args.logger

    # Read input csv
    builder = FB.FTMLBuilder(logger,
                             incsv=args.input,
                             fontcode=args.fontcode,
                             font=args.ifont,
                             ap=args.ap,
                             rtlenable=True)

    # Override default base (25CC) for displaying combining marks
    builder.diacBase = 0x0628  # beh

    # Initialize FTML document:
    test = args.test or "AllChars (NG)"  # Default to AllChars
    ftml = FB.FTML(test,
                   logger,
                   rendercheck=True,
                   fontscale=args.scale,
                   xslfn=args.xsl,
                   fontsrc=args.fontsrc,
                   defaultrtl=args.rtl)

    if test.lower().startswith("allchars"):
        # all chars that should be in the font:
        ftml.startTestGroup('Encoded characters')
        for uid in sorted(builder.uids()):
            if uid < 32: continue
            c = builder.char(uid)
            for featlist in builder.permuteFeatures(uids=(uid, )):
                ftml.setFeatures(featlist)
                builder.render((uid, ), ftml)
            ftml.clearFeatures()
            for langID in sorted(c.langs):
                ftml.setLang(langID)
                builder.render((uid, ), ftml)
            ftml.clearLang()

        # Add specials and ligatures that were in the glyph_data:
        ftml.startTestGroup('Specials & ligatures from glyph_data')
        for basename in sorted(builder.specials()):
            special = builder.special(basename)
            for featlist in builder.permuteFeatures(uids=special.uids):
                ftml.setFeatures(featlist)
                builder.render(special.uids, ftml)
                ftml.closeTest()
            ftml.clearFeatures()
            if len(special.langs):
                for langID in sorted(special.langs):
                    ftml.setLang(langID)
                    builder.render(special.uids, ftml)
                    ftml.closeTest()
                ftml.clearLang()

        # Add Lam-Alef data manually
        ftml.startTestGroup('Lam-Alef')
        lamlist = filter(
            lambda x: x in builder.uids(),
            (0x0644, 0x06B5, 0x06B6, 0x06B7, 0x06B8, 0x076A, 0x08A6))
        aleflist = filter(lambda x: x in builder.uids(),
                          (0x0627, 0x0622, 0x0623, 0x0625, 0x0671, 0x0672,
                           0x0673, 0x0675, 0x0773, 0x0774))
        for lam in lamlist:
            for alef in aleflist:
                for featlist in builder.permuteFeatures(uids=(lam, alef)):
                    ftml.setFeatures(featlist)
                    builder.render((lam, alef), ftml)
                    ftml.closeTest()
                ftml.clearFeatures()

    if test.lower().startswith("diac"):
        # Diac attachment:

        # Representative base and diac chars:
        repDiac = filter(
            lambda x: x in builder.uids(),
            (0x064E, 0x0650, 0x065E, 0x0670, 0x0616, 0x06E3, 0x08F0, 0x08F2))
        repBase = filter(
            lambda x: x in builder.uids(),
            (0x0627, 0x0628, 0x062B, 0x0647, 0x064A, 0x77F, 0x08AC))
        lamlist = filter(
            lambda x: x in builder.uids(),
            (0x0644, 0x06B5, 0x06B6, 0x06B7, 0x06B8, 0x076A, 0x08A6))
        aleflist = filter(lambda x: x in builder.uids(),
                          (0x0627, 0x0622, 0x0623, 0x0625, 0x0671, 0x0672,
                           0x0673, 0x0675, 0x0773, 0x0774))

        ftml.startTestGroup(
            'Representative diacritics on all bases that take diacritics')
        for uid in sorted(builder.uids()):
            if uid < 32 or uid in (0xAA, 0xBA): continue
            c = builder.char(uid)
            # Always process Lo, but others only if that take marks:
            if c.general == GC.OTHER_LETTER or c.isBase:
                for diac in repDiac:
                    for featlist in builder.permuteFeatures(uids=(uid, diac)):
                        ftml.setFeatures(featlist)
                        builder.render((uid, diac), ftml, addBreaks=False)
                        if diac != 0x0651:  # If not shadda
                            # include shadda, in either order:
                            builder.render((uid, diac, 0x0651),
                                           ftml,
                                           addBreaks=False)
                            builder.render((uid, 0x0651, diac),
                                           ftml,
                                           addBreaks=False)
                        if diac != 0x0654:  # If not hamza above
                            # include hamza above, in either order:
                            builder.render((uid, diac, 0x0654),
                                           ftml,
                                           addBreaks=False)
                            builder.render((uid, 0x0654, diac),
                                           ftml,
                                           addBreaks=False)
                    ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('All diacritics on representative bases')
        for uid in sorted(builder.uids()):
            # ignore non-ABS marks
            if uid < 0x600 or uid in range(0xFE00, 0xFE10): continue
            c = builder.char(uid)
            if c.general == GC.NON_SPACING_MARK:
                for base in repBase:
                    for featlist in builder.permuteFeatures(uids=(uid, base)):
                        ftml.setFeatures(featlist)
                        builder.render((base, uid),
                                       ftml,
                                       keyUID=uid,
                                       addBreaks=False)
                        if uid != 0x0651:  # if not shadda
                            # include shadda, in either order:
                            builder.render((base, uid, 0x0651),
                                           ftml,
                                           keyUID=uid,
                                           addBreaks=False)
                            builder.render((base, 0x0651, uid),
                                           ftml,
                                           keyUID=uid,
                                           addBreaks=False)
                        if diac != 0x0670:  # If not superscript alef
                            # include superscript alef, in either order:
                            builder.render((uid, diac, 0x0670),
                                           ftml,
                                           addBreaks=False)
                            builder.render((uid, 0x0670, diac),
                                           ftml,
                                           addBreaks=False)
                    ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('Special cases')
        builder.render((0x064A, 0x065E), ftml)  # Yeh + Fatha should keep dots
        builder.render((0x064A, 0x0654), ftml)  # Yeh + Hamza should loose dots
        ftml.closeTest()

        ftml.startTestGroup('LamAlef ligatures')
        diaB = 0x064D
        diaA = 0x064B
        for lam in lamlist:
            for alef in aleflist:
                for featlist in builder.permuteFeatures(uids=(lam, alef)):
                    ftml.setFeatures(featlist)
                    builder.render((lam, alef), ftml, addBreaks=False)
                    builder.render((lam, diaA, alef, diaA),
                                   ftml,
                                   addBreaks=False)
                    builder.render((lam, diaB, alef), ftml, addBreaks=False)
                    builder.render((lam, alef, diaB), ftml, addBreaks=False)
                    builder.render((lam, diaB, alef, diaB),
                                   ftml,
                                   addBreaks=False)
                    ftml.clearFeatures()
                ftml.closeTest()

    if test.lower().startswith("subtending"):
        # Generates sample data for all subtending marks. Data includes sequences of 0 to n+1
        # digits, where n is the maximum expected to be supported on the mark. Latin, Arbic-Indic,
        # and Extended Arabic-Indic digits are included.
        for digitSample in filter(lambda x: x in builder.uids(),
                                  (0x0032, 0x0668, 0x06F8)):
            digitOne = (digitSample & 0xFFF0) + 1
            for uid, lgt in filter(lambda x: x[0] in builder.uids(),
                                   ([0x600, 3], [0x0601, 4], [0x0602, 2], [
                                       0x0603, 4
                                   ], [0x0604, 4], [0x0605, 4], [0x06DD, 3])):
                c = unichr(uid)
                label = "U+{0:04X}".format(uid)
                comment = builder.char(uid).basename
                for featlist in builder.permuteFeatures(uids=(uid, )):
                    ftml.setFeatures(featlist)
                    ftml.addToTest(uid, "\u0628" + c + "\u0645", label,
                                   comment)
                    for ln in range(1, lgt + 1):
                        ftml.addToTest(uid, c + unichr(digitSample) * ln)
                    ftml.addToTest(uid,
                                   c + unichr(digitOne) + unichr(digitOne + 1))
                ftml.clearFeatures()
                ftml.closeTest()

                if uid == 0x06DD and digitOne == 0x06F1:
                    # Extra items for Eastern digits
                    for featlist in builder.permuteFeatures(uids=(uid,
                                                                  0x06F7)):
                        ftml.setFeatures(featlist)
                        ftml.addToTest(uid, c + "\u06F4\u06F6\u06F7", label,
                                       "4 6 7")
                    ftml.clearFeatures()
                    for langID in sorted(builder.char(0x06F7).langs):
                        ftml.setLang(langID)
                        ftml.addToTest(uid, c + "\u06F4\u06F6\u06F7", label,
                                       "4 6 7")
                    ftml.clearLang()
                    ftml.closeTest()

    ftml.writeFile(args.output)
Ejemplo n.º 3
0
def doit(args):
    logger = args.logger

    # Read input csv
    builder = FB.FTMLBuilder(logger, incsv=args.input, fontcode=args.fontcode, font=args.ifont, ap=args.ap,
                             rtlenable=args.rtl, langs=args.langs)

    # Override default base (25CC) for displaying combining marks:
    builder.diacBase = 0x0B95   # ka

    # Specify blocks of primary and secondary scripts
    comb = range(0x0300, 0x036F+1)
    taml = range(0x0B80, 0x0BFF+1)
    deva = range(0x0900, 0x0097F+1)
    vedic = range(0x1CD0, 0x1CFF+1)
    gran = range(0x11300, 0x1137F+1)
    block = list(comb) + list(taml) + list(deva) + list(vedic) + list(gran)

    # Useful ranges of codepoints
    uids = sorted(builder.uids())
    vowels = [uid for uid in uids if get_ucd(uid, 'InSC') == 'Vowel_Independent']
    consonants = [uid for uid in uids if get_ucd(uid, 'InSC') == 'Consonant']
    matras = [uid for uid in uids if 'VOWEL SIGN' in get_ucd(uid, 'na')]
    digits = [uid for uid in uids if builder.char(uid).general == 'Nd' and uid in block]

    # Initialize FTML document:
    # Default name for test: AllChars or something based on the csvdata file:
    test = args.test or 'AllChars (NG)'
    widths = None
    if args.width:
        try:
            width, units = re.match(r'(\d+)(.*)$', args.width).groups()
            if len(args.fontsrc):
                width = int(round(int(width)/len(args.fontsrc)))
            widths = {'string': f'{width}{units}'}
            logger.log(f'width: {args.width} --> {widths["string"]}', 'I')
        except:
            logger.log(f'Unable to parse width argument "{args.width}"', 'W')
    # split labels from fontsource parameter
    fontsrc = []
    labels = []
    for sl in args.fontsrc:
        try:
            s, l = sl.split('=',1)
            fontsrc.append(s)
            labels.append(l)
        except ValueError:
            fontsrc.append(sl)
            labels.append(None)
    ftml = FB.FTML(test, logger, rendercheck=not args.norendercheck, fontscale=args.scale,
                   widths=widths, xslfn=args.xsl, fontsrc=fontsrc, fontlabel=labels, defaultrtl=args.rtl)

    if test.lower().startswith("allchars"):
        # all chars that should be in the font:
        ftml.startTestGroup('Encoded characters')
        for uid in uids:
            if uid < 32: continue
            c = builder.char(uid)
            # iterate over all permutations of feature settings that might affect this character:
            for featlist in builder.permuteFeatures(uids = (uid,)):
                ftml.setFeatures(featlist)
                builder.render((uid,), ftml)
                # Don't close test -- collect consecutive encoded chars in a single row
            ftml.clearFeatures()
            if len(c.langs):
                for langID in builder.allLangs:
                    ftml.setLang(langID)
                    builder.render((uid,), ftml)
                ftml.clearLang()

        # Add unencoded specials and ligatures -- i.e., things with a sequence of USVs in the glyph_data:
        ftml.startTestGroup('Specials & ligatures from glyph_data')
        for basename in builder.specials():
            special = builder.special(basename)
            # iterate over all permutations of feature settings that might affect this special
            for featlist in builder.permuteFeatures(uids = special.uids):
                ftml.setFeatures(featlist)
                builder.render(special.uids, ftml)
                # close test so each special is on its own row:
                ftml.closeTest()
            ftml.clearFeatures()
            if len(special.langs):
                for langID in builder.allLangs:
                    ftml.setLang(langID)
                    builder.render(special.uids, ftml)
                    ftml.closeTest()
                ftml.clearLang()

        # Characters used to create SILE test data
        ftml.startTestGroup('Proof')
        for uid in vowels:
            builder.render((uid,), ftml)
        ftml.closeTest()
        for uid in matras:
            builder.render((uid,), ftml)
        ftml.closeTest()
        for uid in consonants:
            builder.render((uid,), ftml)
        ftml.closeTest()
        for uid in digits:
            builder.render((uid,), ftml)
        ftml.closeTest()

    below_marks = (0x0323, 0x1133B, 0x1133C)  # 0x1CDC, 0x1CDD, 0x1CDE, 0x1CDF
    above_marks = (0x0307, 0x0B82, 0x0BCD)  # 0x1CDA
    marks = below_marks + above_marks

    if test.lower().startswith("diac"):
        # Diac attachment:

        # Representative base and diac chars:
        repDiac = list(filter(lambda x: x in builder.uids(), marks))
        repBase = list(filter(lambda x: x in builder.uids(), (0x0B95, 0x0B85)))

        ftml.startTestGroup('Representative diacritics on all bases that take diacritics')
        for uid in uids:
            # ignore bases outside of the primary script:
            if uid not in block: continue
            c = builder.char(uid)
            # Always process Lo, but others only if that take marks:
            if c.general == 'Lo' or c.isBase:
                for diac in repDiac:
                    for featlist in builder.permuteFeatures(uids = (uid,diac)):
                        ftml.setFeatures(featlist)
                        # Don't automatically separate connecting or mirrored forms into separate lines:
                        builder.render((uid,diac), ftml, addBreaks = False)
                    ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('All diacritics on representative bases')
        for uid in uids:
            # ignore bases outside of the primary and Latin scripts:
            if uid < 0x0300 or uid in range(0xFE00, 0xFE10): continue
            c = builder.char(uid)
            if c.general == 'Mn':
                for base in repBase:
                    for featlist in builder.permuteFeatures(uids = (uid,base)):
                        ftml.setFeatures(featlist)
                        builder.render((base,uid), ftml, keyUID = uid, addBreaks = False)
                    ftml.clearFeatures()
                ftml.closeTest()

    if test.lower().startswith("matras"):
        # Combinations with matras:

        ftml.startTestGroup('Consonants with matras')
        for c in consonants:
            for m in matras:
                builder.render((c,m), ftml, label=f'{c:04X}', comment=builder.char(c).basename)
            ftml.closeTest()

    if test.lower().startswith("nuktas"):
        # Nuktas:
        ftml.startTestGroup('Nuktas')
        test_name = test.lower().split()[0]
        with open(f'tests/{test_name}.template') as nuktas:
            line_number = 0
            for line in nuktas:
                line = line.strip()
                line_number += 1
                if line == '':
                    continue
                for n in below_marks:
                    for v in above_marks:
                        text = line.replace('N', chr(n))
                        text = text.replace('V', chr(v))
                        ftml.addToTest(None, text, label=f'line {line_number}', comment=f'n={n:04X} v={v:04X}')
                        ftml.closeTest()

    # Write the output ftml file
    ftml.writeFile(args.output)
Ejemplo n.º 4
0
def doit(args):
    logger = args.logger

    # Read input csv
    builder = FB.FTMLBuilder(logger, incsv = args.input, fontcode = args.fontcode, font = args.ifont, ap = args.ap, rtlenable = True)

    # Override default base (25CC) for displaying combining marks:
    builder.diacBase = 0x0628   # beh

    # Initialize FTML document:
    test = args.test or "AllChars (NG)"  # Default to "AllChars (NG)"
    ftml = FB.FTML(test, logger, rendercheck = True, fontscale = args.scale, xslfn = args.xsl, fontsrc = args.fontsrc)

    if test.lower().startswith("allchars"):
        # all chars that should be in the font:
        ftml.startTestGroup('Encoded characters')
        for uid in sorted(builder.uids()):
            if uid < 32: continue
            c = builder.char(uid)
            # iterate over all permutations of feature settings that might affect this character:
            for featlist in builder.permuteFeatures(uids = (uid,)):
                ftml.setFeatures(featlist)
                builder.render((uid,), ftml)
                # Don't close test -- collect consecutive encoded chars in a single row
            ftml.clearFeatures()
            for langID in sorted(c.langs):
                ftml.setLang(langID)
                builder.render((uid,), ftml)
            ftml.clearLang()

        # Add unencoded specials and ligatures -- i.e., things with a sequence of USVs in the glyph_data:
        ftml.startTestGroup('Specials & ligatures from glyph_data')
        for basename in sorted(builder.specials()):
            special = builder.special(basename)
            # iterate over all permutations of feature settings that might affect this special
            for featlist in builder.permuteFeatures(uids = special.uids):
                ftml.setFeatures(featlist)
                builder.render(special.uids, ftml)
                # close test so each special is on its own row:
                ftml.closeTest()
            ftml.clearFeatures()
            if len(special.langs):
                for langID in sorted(special.langs):
                    ftml.setLang(langID)
                    builder.render(special.uids, ftml)
                    ftml.closeTest()
                ftml.clearLang()

        # Add Lam-Alef data manually
        ftml.startTestGroup('Lam-Alef')
        # generate list of lam and alef characters that should be in the font:
        lamlist = filter(lambda x: x in builder.uids(), (0x0644, 0x06B5, 0x06B6, 0x06B7, 0x06B8, 0x076A, 0x08A6))
        aleflist = filter(lambda x: x in builder.uids(), (0x0627, 0x0622, 0x0623, 0x0625, 0x0671, 0x0672, 0x0673, 0x0675, 0x0773, 0x0774))
        # iterate over all combinations:
        for lam in lamlist:
            for alef in aleflist:
                for featlist in builder.permuteFeatures(uids = (lam, alef)):
                    ftml.setFeatures(featlist)
                    builder.render((lam,alef), ftml)
                    # close test so each combination is on its own row:
                    ftml.closeTest()
                ftml.clearFeatures()

    if test.lower().startswith("diac"):
        # Diac attachment:

        # Representative base and diac chars:
        repDiac = filter(lambda x: x in builder.uids(), (0x064E, 0x0650, 0x065E, 0x0670, 0x0616, 0x06E3, 0x08F0, 0x08F2))
        repBase = filter(lambda x: x in builder.uids(), (0x0627, 0x0628, 0x062B, 0x0647, 0x064A, 0x77F, 0x08AC))

        ftml.startTestGroup('Representative diacritics on all bases that take diacritics')
        for uid in sorted(builder.uids()):
            # ignore some I don't care about:
            if uid < 32 or uid in (0xAA, 0xBA): continue
            c = builder.char(uid)
            # Always process Lo, but others only if that take marks:
            if c.general == 'Lo' or c.isBase:
                for diac in repDiac:
                    for featlist in builder.permuteFeatures(uids = (uid,diac)):
                        ftml.setFeatures(featlist)
                        # Don't automatically separate connecting or mirrored forms into separate lines:
                        builder.render((uid,diac), ftml, addBreaks = False)
                    ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('All diacritics on representative bases')
        for uid in sorted(builder.uids()):
            # ignore non-ABS marks
            if uid < 0x600 or uid in range(0xFE00, 0xFE10): continue
            c = builder.char(uid)
            if c.general == 'Mn':
                for base in repBase:
                    for featlist in builder.permuteFeatures(uids = (uid,base)):
                        ftml.setFeatures(featlist)
                        builder.render((base,uid), ftml, keyUID = uid, addBreaks = False)
                    ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('Special cases')
        builder.render((0x064A, 0x065E), ftml)   # Yeh + Fatha should keep dots
        builder.render((0x064A, 0x0654), ftml)   # Yeh + Hamza should loose dots
        ftml.closeTest()

    # Write the output ftml file
    ftml.writeFile(args.output)
Ejemplo n.º 5
0
def doit(args):
    logger = args.logger

    builder = FTMLBuilder_LCG(logger,
                              incsv=args.input,
                              fontcode=args.fontcode,
                              font=args.ifont,
                              ap=args.ap)

    # Initialize FTML document:
    test = args.test or "AllChars"  # Default to "AllChars"

    # split labels from fontsource parameter
    fontsrc_lst, fontlabel_lst = [], []
    for sl in args.fontsrc:
        try:
            s, l = sl.split('=', 1)
            fontsrc_lst.append(s)
            fontlabel_lst.append(l)
        except ValueError:
            fontsrc_lst.append(sl)
            fontlabel_lst.append(None)

    ftml = FB.FTML(test,
                   logger,
                   rendercheck=False,
                   fontscale=args.scale,
                   xslfn=args.xsl,
                   fontsrc=fontsrc_lst,
                   fontlabel=fontlabel_lst)

    # Char to use in allframed test to surround other chars for checking spacing
    frame_uid = 0x006F

    # Representative base and diac chars:
    #  cedilla (H), vertical line below (L), ogonek (O), comma abov right (R), vertical line above (U)
    repDiac = [
        x for x in [0x0327, 0x0329, 0x0328, 0x0315, 0x030D]
        if x in builder.uids()
    ]
    ap_type_uid = {}
    for diac_uid in repDiac:
        c = builder.char(diac_uid)
        for ap in c.aps:
            if ap.startswith("_"):
                ap_type_uid[ap[1:]] = diac_uid

    if test.lower().startswith("allchars") or test.lower().startswith(
            "allframed"):
        # all chars that should be in the font:
        framed = test.lower().startswith("allframed")
        uids = special_uids = None

        ftml.startTestGroup('Encoded characters')
        for uid in sorted(builder.uids()):
            if uid < 32: continue
            c = builder.char(uid)
            if not framed:
                builder.render((uid, ), ftml)
            else:  # TODO: is there a cleaner way to create uids? (also see special_uids creation below)
                uids = [frame_uid]
                uids.extend(
                    (uid, )
                )  # used extend() instead of append() to be parallel to special_uids
                uids.append(frame_uid)
                builder.render(uids, ftml, keyUID=uid, descUIDs=(uid, ))
            ftml.closeTest()
            for langID in sorted(c.langs):
                ftml.setLang(langID)
                if not framed:
                    builder.render((uid, ), ftml)
                else:
                    builder.render(uids, ftml, keyUID=uid, descUIDs=(uid, ))
            ftml.clearLang()

        # Add unencoded specials and ligatures -- i.e., things with a sequence of USVs in the glyph_data:
        ftml.startTestGroup('Specials & ligatures from glyph_data')
        for gname in sorted(builder.specials()):
            special = builder.special(gname)
            if not framed:
                builder.render(special.uids, ftml)
            else:
                special_uids = [frame_uid]
                special_uids.extend(special.uids)
                special_uids.append(frame_uid)
                builder.render(special_uids,
                               ftml,
                               keyUID=special.uids[0],
                               descUIDs=(special.uids))
            ftml.closeTest()
            if len(special.langs):
                for langID in sorted(special.langs):
                    ftml.setLang(langID)
                    if not framed:
                        builder.render(special.uids, ftml)
                    else:
                        builder.render(special_uids,
                                       ftml,
                                       keyUID=special.uids[0],
                                       descUIDs=(special.uids))
                ftml.clearLang()

    if test.lower().startswith("features"):
        # support for adding a diac after each char that is being tested
        # tests include: feature, feature_U, feature_L, feature_O, feature_H, feature_R
        ap_type, c_mark = None, None
        ix = test.find("_")
        if ix != -1:
            ap_type = test[ix + 1:]
        if ap_type:
            try:
                ap_uid = ap_type_uid[ap_type]
            except KeyError:
                logger.log("Invalid AP type: %s" % ap_type, "S")
            c_mark = builder.char(ap_uid)

        ftml.startTestGroup('Features from glyph_data')

        # build map from features (and all combinations) to the uids that are affected by a feature
        feats_to_uid = {}
        char_special_iter = chain(
            [builder.char(uid) for uid in builder.uids()],
            [builder.special(gname) for gname in builder.specials()])
        for cs in char_special_iter:
            feat_combs = chain.from_iterable(
                combinations(cs.feats, r) for r in range(len(cs.feats) + 1))
            for feats in feat_combs:
                if not feats: continue
                feats = sorted(feats)
                feat_set = " ".join(feats)
                if type(cs) is FB.FChar:
                    feats_to_uid.setdefault(feat_set, []).append(cs.uid)
                elif type(cs) is FB.FSpecial:
                    feats_to_uid.setdefault(feat_set, []).append(cs.uids)
                else:
                    pass

        # create tests for all uids affected by a feature, organized by features
        feats_sort = {}  # sort based on number of features in combo
        for feat_set in feats_to_uid.keys():
            cnt = len(feat_set.split(" "))
            try:
                feats_sort[cnt].append(feat_set)
            except:
                feats_sort[cnt] = [feat_set]

        for i in sorted(feats_sort.keys()):
            feat_set_lst = sorted(
                feats_sort[i]
            )  # list of feat combos where number in combo is i
            for feat_set in feat_set_lst:  # one feat combo
                if feat_set == "smcp":
                    continue  # skip smcp test (but not interaction of smcp with other features); see "smcp" test
                ftml.startTestGroup(f'{feat_set}')

                feats = feat_set.split(" ")  # separate feat combo into feats
                tvlist_lst = []
                for feat in feats:
                    tvlist = builder.features[feat].tvlist[
                        1:]  # all values of feats except default
                    tvlist_lst.append(
                        tvlist
                    )  # build list of list of all value for each feat
                p_lst = list(
                    product(*tvlist_lst)
                )  # find all combo of all values, MUST flatten the list of lists

                # list of uids to test against feat combo
                uid_lst = sorted(feats_to_uid[feat_set],
                                 key=lambda x: x[0] if type(x) is list else x)
                uid_char_lst = [u for u in uid_lst if type(u) is int]
                uid_lig_lst = [u for u in uid_lst if type(u) is list]

                # break uids into groups
                uidlst_ct = 16 if not ap_type else 8

                # generate ftml for all chars associated with the features
                uidlst_lst = [
                    uid_char_lst[i:i + uidlst_ct]
                    for i in range(0, len(uid_char_lst), uidlst_ct)
                ]
                for uidlst in uidlst_lst:  #uidlst contains uids to test
                    base_diac_lst, base_lst = [], []
                    if not ap_type:
                        # features test (no '_<AP>')
                        base_diac_lst, base_lst = uidlst, uidlst
                    else:
                        # features_U, features_L, etc. tests
                        for uid in uidlst:
                            c_base = builder.char(uid)
                            if builder.matchMarkBase(c_mark, c_base):
                                base_lst.append(uid)
                                base_diac_lst.extend((uid, ap_uid))
                    ftml.clearFeatures()
                    builder.render(base_diac_lst, ftml, descUIDs=base_lst
                                   )  # render all uids without feat setting
                    for tv_lst in p_lst:  # for one list of values out of all lists of values
                        ftml.setFeatures(tv_lst)
                        builder.render(base_diac_lst, ftml, descUIDs=base_lst)

                # generate ftml for all ligatures associated with the features
                #   the ligature sequence is followed by a space
                liglst_lst = [
                    uid_lig_lst[i:i + uidlst_ct]
                    for i in range(0, len(uid_lig_lst), uidlst_ct)
                ]
                for liglst in liglst_lst:  # liglst contains lists of uids & name to tests
                    lig_lst, lig_diac_lst = [], []
                    for lig in liglst:  # lig is alist of uids & name
                        if not ap_type:
                            # 'features test (no '_<AP>')
                            lig_lst.extend(lig)
                            lig_lst.append(ord(' '))
                            lig_diac_lst.extend(lig)
                            lig_diac_lst.append(ord(' '))
                        else:
                            # features_U, features_L, etc. tests
                            c_base = builder.special(lig)
                            if builder.matchMarkBase(c_mark, c_base):
                                lig_lst.extend(lig)
                                lig_lst.append(ord(' '))
                                lig_diac_lst.extend(lig)
                                lig_diac_lst.append(
                                    ap_uid
                                )  # add AP based on type of features_<AP> test
                                lig_diac_lst.append(ord(' '))
                    lig_lst, lig_diac_lst = lig_lst[0:-1], lig_diac_lst[
                        0:-1]  # trim trailing spaces
                    ftml.clearFeatures()
                    builder.render(lig_diac_lst, ftml, descUIDs=lig_lst
                                   )  # render all uids without feat setting
                    for tv_lst in p_lst:  # for one list of values out of all lists of values
                        ftml.setFeatures(tv_lst)
                        builder.render(lig_diac_lst, ftml, descUIDs=lig_lst)

    if test.lower().startswith("smcp"):
        # TODO: improve test for "c2sc" ?
        # Example of what report needs to show: LtnSmEgAlef LtnSmEgAlef.sc LtnCapEgAlef
        #  could add "LtnCapEgAlef <with 'c2sc' feature applied>" but commented out below

        # support adding a diac after each char that is being tested
        # tests include: smcp, smcp_U, etc

        ap_type = None
        ix = test.find("_")
        if ix != -1:
            ap_type = test[ix + 1:]
        if ap_type:
            try:
                ap_uid = ap_type_uid[ap_type]
            except KeyError:
                logger.log("Invalid AP type: %s" % ap_type, "S")
            c_mark = builder.char(ap_uid)

        ftml.startTestGroup('Small caps from glyph_data')

        char_special_iter = chain(
            [builder.char(uid) for uid in builder.uids()],
            [builder.special(gname) for gname in builder.specials()])
        smcp_uid_lst = []
        for cs in char_special_iter:
            if 'smcp' in cs.feats:
                if type(cs) is FB.FChar: smcp_uid_lst.append(cs.uid)
                if type(cs) is FB.FSpecial: smcp_uid_lst.append(cs.uids)

        smcp_uid_lst.sort(key=lambda x: x[0] if type(x) is list else x)
        smcp_char_lst = [u for u in smcp_uid_lst if type(u) is int]
        smcp_lig_lst = [u for u in smcp_uid_lst if type(u) is list]

        uidlst_ct = 16 if not ap_type else 8

        uidlst_lst = [
            smcp_char_lst[i:i + uidlst_ct]
            for i in range(0, len(smcp_char_lst), uidlst_ct)
        ]
        for uidlst in uidlst_lst:
            base_diac_lst, base_lst = [], []
            if not ap_type:
                base_diac_lst, base_lst = uidlst, uidlst
            else:
                for uid in uidlst:
                    c_base = builder.char(uid)
                    if builder.matchMarkBase(c_mark, c_base):
                        base_lst.append(uid)
                        base_diac_lst.extend((uid, ap_uid))
            ftml.clearFeatures()
            builder.render(
                base_diac_lst, ftml,
                descUIDs=base_lst)  # render all uids without feat setting
            ftml.setFeatures(builder.features['smcp'].tvlist[1:])
            builder.render(base_diac_lst, ftml, descUIDs=base_lst)
            ftml.clearFeatures()

            # add uppercase uids to test
            # TODO: may need a better way to convert lower to upper
            upper_base_diac_lst, upper_base_lst = [], []
            for lower_uid in base_diac_lst:
                try:
                    upper_base_diac_lst.append(ord(chr(lower_uid).upper()))
                except:
                    upper_base_diac_lst.append(ord('X'))
            for lower_uid in base_lst:
                try:
                    upper_base_lst.append(ord(chr(lower_uid).upper()))
                except:
                    upper_base_lst.append(ord('X'))
            builder.render(upper_base_diac_lst, ftml, descUIDs=upper_base_lst)
            ftml.setFeatures([("c2sc", "1")
                              ])  # TODO: kludgy way to add a 'c2sc' test
            builder.render(upper_base_diac_lst, ftml, descUIDs=upper_base_lst)

        liglst_lst = [
            smcp_lig_lst[i:i + uidlst_ct]
            for i in range(0, len(smcp_lig_lst), uidlst_ct)
        ]
        for liglst in liglst_lst:  # liglst contains lists of uids & name to tests
            lig_lst, lig_diac_lst = [], []
            for lig in liglst:  # lig is alist of uids & name
                if not ap_type:
                    # 'smcp test (no '_<AP>')
                    lig_lst.extend(lig)
                    lig_lst.append(ord(' '))
                    lig_diac_lst.extend(lig)
                    lig_diac_lst.append(ord(' '))
                else:
                    # scmp_U, smcp_L, etc. tests
                    c_base = builder.special(lig)
                    if builder.matchMarkBase(c_mark, c_base):
                        lig_lst.extend(lig)
                        lig_lst.append(ord(' '))
                        lig_diac_lst.extend(lig)
                        lig_diac_lst.append(
                            ap_uid
                        )  # add AP based on type of features_<AP> test
                        lig_diac_lst.append(ord(' '))
            lig_lst, lig_diac_lst = lig_lst[0:-1], lig_diac_lst[
                0:-1]  # trim trailing spaces
            ftml.clearFeatures()
            builder.render(
                lig_diac_lst, ftml,
                descUIDs=lig_lst)  # render all uids without feat setting
            ftml.setFeatures(builder.features['smcp'].tvlist[1:])
            builder.render(lig_diac_lst, ftml, descUIDs=lig_lst)

    if test.lower().startswith("diac"):
        # A E H O a e i o modifier-small-letter-o
        repBase = [
            x for x in [
                0x0041, 0x0045, 0x0048, 0x004F, 0x0061, 0x0065, 0x0069, 0x006F,
                0x1D52
            ] if x in builder.uids()
        ]

        # Diac attachment:
        ftml.startTestGroup(
            'Representative diacritics on all bases that take diacritics')
        for uid in sorted(builder.uids()):
            # adjust for Latin
            # ignore some I don't care about:
            # if uid < 32 or uid in (0xAA, 0xBA): continue
            if uid < 32: continue
            c = builder.char(uid)
            # Always process Lo, but others only if that take marks:
            if c.general == 'Lo' or c.isBase:
                for diac in repDiac:
                    c_mark = builder.char(diac)
                    if builder.matchMarkBase(c_mark, c):
                        builder.render((uid, diac),
                                       ftml,
                                       keyUID=uid,
                                       descUIDs=[uid])
                    else:
                        # TODO: possibly output 'X' to mark place where mismatches occur
                        pass
                    # for featlist in builder.permuteFeatures(uids = (uid,diac)):
                    #     ftml.setFeatures(featlist)
                    #     # Don't automatically separate connecting or mirrored forms into separate lines:
                    #     builder.render((uid,diac), ftml, addBreaks = False)
                    # ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('All diacritics on representative bases')
        for uid in sorted(builder.uids()):
            c = builder.char(uid)
            if c.general == 'Mn':
                for base in repBase:
                    c_base = builder.char(base)
                    if builder.matchMarkBase(c, c_base):
                        builder.render((base, uid),
                                       ftml,
                                       keyUID=uid,
                                       descUIDs=[uid])
                    else:  # TODO: possibly output 'X' to mark place where mismatches occur
                        pass
                    # for featlist in builder.permuteFeatures(uids = (uid,base)):
                    #     ftml.setFeatures(featlist)
                    #     builder.render((base,uid), ftml, keyUID = uid, addBreaks = False)
                    # ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('Special case - cv79 (NFD)')
        # cv79 - Kayan grave_acute
        kayan_diac_lst = [0x0300, 0x0301]  # comb_grave, comb_acute
        kayan_base_char_lst = [
            'a', 'e', 'i', 'o', 'n', 'u', 'w', 'y', 'A', 'E', 'I', 'O', 'N',
            'U', 'W', 'Y'
        ]
        kayan_base_lst = [ord(x) for x in kayan_base_char_lst]
        builder.render_lists(kayan_base_lst,
                             kayan_diac_lst,
                             ftml, [('cv79', '1')],
                             keyUID=kayan_diac_lst[0])
        ftml.closeTestGroup()

        ftml.startTestGroup('Special case - cv79 (NFC)')
        # cv79 - Kayan grave_acute
        kayan_diac_lst = [0x0301]  # comb_acute
        kayan_base_name_lst = [
            'LtnSmAGrave', 'LtnSmAGrave.SngStory', 'LtnSmEGrave',
            'LtnSmIGrave', 'LtnSmOGrave', 'LtnSmNGrave', 'LtnSmUGrave',
            'LtnSmWGrave', 'LtnSmYGrave', 'LtnCapAGrave', 'LtnCapEGrave',
            'LtnCapIGrave', 'LtnCapOGrave', 'LtnCapNGrave', 'LtnCapUGrave',
            'LtnCapWGrave', 'LtnCapYGrave'
        ]
        # try: kayan_base_lst = [builder.char(x).uid for x in kayan_base_name_lst]
        # except KeyError as key_exc: logger.log('glyph missing: {}'.format(key_exc), 'W')
        kayan_base_lst = []
        for base in kayan_base_name_lst:
            # TODO: fix this kludgy way of handling Andika's encoding of literacy glyphs
            # LtnSmAGrave.SngStory will raise an exception in non-Andika fonts
            #   LtnSmAGrave will do the same in Andika
            try:
                kayan_base_lst.append(builder.char(base).uid)
            except KeyError as key_exc:
                logger.log('glyph missing: {}'.format(key_exc), 'W')
        builder.render_lists(kayan_base_lst,
                             kayan_diac_lst,
                             ftml, [('cv79', '1')],
                             keyUID=kayan_diac_lst[0])
        ftml.closeTestGroup()

        ftml.startTestGroup('Dot removal')
        diac_lst = [0x301]  #comb_acute
        # the below glyph list was copied from classes.xml and massaged into Python format
        base_name_lst = [
            'CySmByelorusUkrainI', 'CySmJe', 'LtnSmI', 'LtnSmI.SItal',
            'LtnSmI.sc', 'LtnSmIOgonek', 'LtnSmIRetrHook', 'LtnSmIStrk',
            'LtnSmJ', 'LtnSmJCrossedTail', 'LtnSmJStrk', 'LtnSubSmI',
            'LtnSubSmJ', 'LtnSupSmI', 'LtnSupSmIStrk', 'ModSmJ',
            'ModSmJCrossedTail'
        ]
        base_lst = []
        for x in base_name_lst:
            try:
                base_lst.append(builder.char(x).uid)
            except:
                pass
        builder.render_lists(base_lst, diac_lst, ftml, keyUID=diac_lst[0])
        builder.render_lists(base_lst,
                             diac_lst,
                             ftml,
                             feature_lst=builder.features['smcp'].tvlist[1:],
                             keyUID=diac_lst[0])
        builder.render_lists(base_lst,
                             diac_lst,
                             ftml,
                             feature_lst=builder.features['ss05'].tvlist[1:],
                             keyUID=diac_lst[0])
        ftml.closeTestGroup()

        ftml.startTestGroup('Superscript diacritics')
        diac_lst = [0x308]  #CombDiaer.Sup
        # the below glyph list was copied from classes.xml and massaged into Python format
        base_name_lst = [
            'GrSubSmBeta', 'GrSubSmChi', 'GrSubSmGamma', 'GrSubSmPhi',
            'GrSubSmRho', 'LtnSubSmA', 'LtnSubSmA.SngStory', 'LtnSubSmE',
            'LtnSubSmH', 'LtnSubSmI', 'LtnSubSmI.Dotless', 'LtnSubSmJ',
            'LtnSubSmJ.Dotless', 'LtnSubSmK', 'LtnSubSmL', 'LtnSubSmL.SItal',
            'LtnSubSmM', 'LtnSubSmN', 'LtnSubSmO', 'LtnSubSmP', 'LtnSubSmR',
            'LtnSubSmS', 'LtnSubSmSchwa', 'LtnSubSmT', 'LtnSubSmU',
            'LtnSubSmV', 'LtnSubSmX', 'LtnSupSmA', 'LtnSupSmA.SngStory',
            'LtnSupSmAe', 'LtnSupSmAlpha', 'LtnSupSmB', 'LtnSupSmBarredO',
            'LtnSupSmBarredODep', 'LtnSupSmCCurl', 'LtnSupSmCCurlDep',
            'LtnSupSmCapI', 'LtnSupSmCapIDep', 'LtnSupSmCapOe', 'LtnSupSmCapY',
            'LtnSupSmClosedRevOpnE', 'LtnSupSmD', 'LtnSupSmDotlessJStrk',
            'LtnSupSmDotlessJStrkDep', 'LtnSupSmE', 'LtnSupSmEng',
            'LtnSupSmEngDep', 'LtnSupSmEsh', 'LtnSupSmEshDep', 'LtnSupSmEzh',
            'LtnSupSmEzhDep', 'LtnSupSmF', 'LtnSupSmFDep', 'LtnSupSmG',
            'LtnSupSmG.SngBowl', 'LtnSupSmI', 'LtnSupSmI.Dotless',
            'LtnSupSmIStrk', 'LtnSupSmIStrk.Dotless', 'LtnSupSmIStrkDep',
            'LtnSupSmK', 'LtnSupSmLRetrHook', 'LtnSupSmLRetrHookDep',
            'LtnSupSmM', 'LtnSupSmMDep', 'LtnSupSmN', 'LtnSupSmNLftHook',
            'LtnSupSmNLftHookDep', 'LtnSupSmO', 'LtnSupSmOStrk', 'LtnSupSmOe',
            'LtnSupSmOeDep', 'LtnSupSmOpnE', 'LtnSupSmOpnO',
            'LtnSupSmOpnO.TopSerif', 'LtnSupSmP', 'LtnSupSmRamsHorn',
            'LtnSupSmRevE', 'LtnSupSmRevOpnE', 'LtnSupSmRevOpnEDep',
            'LtnSupSmSchwa', 'LtnSupSmScriptG', 'LtnSupSmScriptGDep',
            'LtnSupSmT', 'LtnSupSmTurnedA', 'LtnSupSmTurnedAlpha',
            'LtnSupSmTurnedAlphaDep', 'LtnSupSmTurnedM',
            'LtnSupSmTurnedMLngLeg', 'LtnSupSmTurnedMLngLegDep',
            'LtnSupSmTurnedV', 'LtnSupSmTurnedVDep', 'LtnSupSmU',
            'LtnSupSmUBar', 'LtnSupSmUBarDep', 'LtnSupSmUpsilon',
            'LtnSupSmUpsilonDep', 'LtnSupSmV', 'LtnSupSmZ', 'LtnSupSmZCurl',
            'LtnSupSmZCurlDep', 'LtnSupSmZDep', 'ModCapA', 'ModCapAe',
            'ModCapB', 'ModCapBarredB', 'ModCapD', 'ModCapE', 'ModCapG',
            'ModCapH', 'ModCapHStrk', 'ModCapI', 'ModCapJ', 'ModCapK',
            'ModCapL', 'ModCapM', 'ModCapN', 'ModCapO', 'ModCapOu',
            'ModCapOu.OpenTop', 'ModCapP', 'ModCapR', 'ModCapRevE',
            'ModCapRevN', 'ModCapT', 'ModCapU', 'ModCapV', 'ModCapW',
            'ModSmAin', 'ModSmBeta', 'ModSmBottomHalfO', 'ModSmC', 'ModSmCDep',
            'ModSmCapIStrk', 'ModSmCapIStrkDep', 'ModSmCapInvR', 'ModSmCapL',
            'ModSmCapLDep', 'ModSmCapN', 'ModSmCapNDep', 'ModSmCapU',
            'ModSmCapUBar', 'ModSmCapUDep', 'ModSmChi', 'ModSmDelta',
            'ModSmEth', 'ModSmEthDep', 'ModSmGamma', 'ModSmGrGamma',
            'ModSmGrPhi', 'ModSmH', 'ModSmHHook', 'ModSmHStrk', 'ModSmHeng',
            'ModSmIota', 'ModSmIotaDep', 'ModSmJ', 'ModSmJ.Dotless',
            'ModSmJCrossedTail', 'ModSmJCrossedTail.Dotless',
            'ModSmJCrossedTailDep', 'ModSmL', 'ModSmLMiddleTilde',
            'ModSmLPalHook', 'ModSmLPalHookDep', 'ModSmMHook', 'ModSmMHookDep',
            'ModSmNRetrHook', 'ModSmNRetrHookDep', 'ModSmPhi', 'ModSmPhiDep',
            'ModSmR', 'ModSmRevGlottalStop', 'ModSmS', 'ModSmSHook',
            'ModSmSHookDep', 'ModSmSdwysU', 'ModSmTPalHook',
            'ModSmTPalHookDep', 'ModSmTheta', 'ModSmThetaDep', 'ModSmTopHalfO',
            'ModSmTrndAe', 'ModSmTrndI', 'ModSmTrndOpnE', 'ModSmTrndR',
            'ModSmTrndRHook', 'ModSmTurnedH', 'ModSmTurnedHDep',
            'ModSmTurnedY', 'ModSmVHook', 'ModSmVHook.StraightLft',
            'ModSmVHook.StraightLftHighHook', 'ModSmVHookDep', 'ModSmW',
            'ModSmX', 'ModSmY', 'ModSmZRetrHook', 'ModSmZRetrHookDep',
            'ModGlottalStop', 'ModRevGlottalStop'
        ]
        base_lst = []
        for x in base_name_lst:
            try:
                base_lst.append(builder.char(x).uid)
            except:
                pass
        builder.render_lists(base_lst, diac_lst, ftml, keyUID=diac_lst[0])
        ftml.closeTestGroup()

        # ftml.startTestGroup('Special case - cv75')
        # ftml.clearFeatures()
        # # comb_circumflex, comb_acute, space, a, comb_circumflex, comb_acute
        # builder.render((0x0302, 0x0301, 0x0020, 0x0061, 0x0302, 0x0301), ftml, descUIDs=(0x0302, 0x0301))
        # # o, comb_circumflex, comb_acute
        # builder.render((0x006F, 0x0302, 0x0301), ftml, keyUID=0x0302)
        # ftml.setFeatures([["cv75", "1"]])
        # builder.render((0x0302, 0x0301, 0x0020, 0x0061, 0x0302, 0x0301), ftml, descUIDs=(0x0302, 0x0301))
        # builder.render((0x006F, 0x0302, 0x0301), ftml, keyUID=0x0302)
        # ftml.closeTestGroup()

    # Write the output ftml file
    ftml.writeFile(args.output)
Ejemplo n.º 6
0
def doit(args):
    logger = args.logger

    # Read input csv
    builder = FB.FTMLBuilder(logger,
                             incsv=args.input,
                             fontcode=args.fontcode,
                             font=args.ifont,
                             ap=args.ap,
                             rtlenable=True,
                             langs=args.langs)

    # Override default base (25CC) for displaying combining marks
    builder.diacBase = 0x0628  # beh

    def basenameSortKey(uid: int):
        return builder.char(uid).basename.lower()

    # Initialize FTML document:
    test = args.test or "AllChars (NG)"  # Default to AllChars
    ftml = FB.FTML(test,
                   logger,
                   rendercheck=not args.norendercheck,
                   fontscale=args.scale,
                   xslfn=args.xsl,
                   fontsrc=args.fontsrc,
                   defaultrtl=args.rtl)

    if test.lower().startswith("allchars"):
        # all chars that should be in the font:
        ftml.startTestGroup('Encoded characters')
        for uid in sorted(builder.uids()):
            if uid < 32: continue
            c = builder.char(uid)
            for featlist in builder.permuteFeatures(uids=(uid, )):
                ftml.setFeatures(featlist)
                builder.render((uid, ), ftml)
            ftml.clearFeatures()
            if len(c.langs):
                for langID in builder.allLangs:
                    ftml.setLang(langID)
                    builder.render((uid, ), ftml)
                ftml.clearLang()

        # Add specials and ligatures that were in the glyph_data:
        ftml.startTestGroup('Specials & ligatures from glyph_data')
        for basename in sorted(builder.specials()):
            special = builder.special(basename)
            for featlist in builder.permuteFeatures(uids=special.uids,
                                                    feats=special.feats):
                ftml.setFeatures(featlist)
                builder.render(special.uids, ftml)
                ftml.closeTest()
            ftml.clearFeatures()
            if len(special.langs):
                for langID in builder.allLangs:
                    ftml.setLang(langID)
                    builder.render(special.uids, ftml)
                    ftml.closeTest()
                ftml.clearLang()

        # Add Lam-Alef data manually
        ftml.startTestGroup('Lam-Alef')
        lamlist = list(
            filter(lambda x: x in builder.uids(),
                   (0x0644, 0x06B5, 0x06B6, 0x06B7, 0x06B8, 0x076A, 0x08A6)))
        aleflist = list(
            filter(lambda x: x in builder.uids(),
                   (0x0627, 0x0622, 0x0623, 0x0625, 0x0671, 0x0672, 0x0673,
                    0x0675, 0x0773, 0x0774)))
        for lam in lamlist:
            for alef in aleflist:
                for featlist in builder.permuteFeatures(uids=(lam, alef)):
                    ftml.setFeatures(featlist)
                    builder.render((lam, alef), ftml)
                    ftml.closeTest()
                ftml.clearFeatures()
                if lam == 0x0644 and 'cv02' in builder.features:
                    # Also test lam with hamza above for warsh variants
                    for featlist in builder.permuteFeatures(uids=(lam, 0x0654,
                                                                  alef),
                                                            feats=('cv02', )):
                        ftml.setFeatures(featlist)
                        builder.render((lam, 0x0654, alef), ftml)
                        ftml.closeTest()
                    ftml.clearFeatures()

        # Add Allah data manually
        ftml.startTestGroup('Allah ligatures')
        ftml.addToTest(0xFDF2, r"\uFDF2", comment="Rule 1")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0641\u0644\u0644\u0647",
                       label="f-l-l-h",
                       comment="shouldn't match")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u0651\u0670\u0647",
                       label="a-l-l-s-da-hf",
                       comment="Rule 2 (daggeralef)")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u0670\u0651\u0647",
                       label="a-l-l-da-s-hf")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u0651\u0670\u06C1",
                       label="a-l-l-s-da-hgf")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u0670\u0651\u06C1",
                       label="a-l-l-da-s-hgf")
        ftml.closeTest()

        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u0651\u064E\u0647",
                       label="a-l-l-s-f-hf",
                       comment="Rule 2 (fatha)")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u064E\u0651\u0647",
                       label="a-l-l-f-s-hf")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u0651\u064E\u06C1",
                       label="a-l-l-s-f-hgf")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u064E\u0651\u06C1",
                       label="a-l-l-f-s-hgf")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u06EB\u0644\u064E\u0651\u06C1",
                       label="a-l-M-l-s-da-hgf",
                       comment="Rule 2c: shouldn't match")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0641\u0644\u0644\u064E\u0651\u06C1",
                       label="f-l-l-s-da-hgf",
                       comment="Rule 2d: non-alef")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0641\u0627\u0644\u0644\u064E\u0651\u06C1",
                       label="f-a-l-l-s-da-hgf",
                       comment="Rule 2d: not isolate alef")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u06EB\u0644\u0644\u064E\u0651\u06C1",
                       label="a-M-l-l-s-da-hgf",
                       comment="Rule 2d: Mark")
        ftml.closeTest()
        ftml.addToTest(None,
                       r" \u0644\u0644\u0651\u064E\u0647",
                       label="space-l-l-s-da-hf",
                       comment="Rule 2d: shouldn't match")
        ftml.closeTest()

        ftml.addToTest(None,
                       r"\u0627\u0644\u0644\u0647",
                       label="a-l-l-h",
                       comment="Rule 3")
        ftml.closeTest()
        ftml.addToTest(None, r"\u0622\u0644\u0644\u0647", label="aM-l-l-h")
        ftml.closeTest()
        ftml.addToTest(None, r"\u0623\u0644\u0644\u0647", label="aH-l-l-h")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0671\u0644\u0644\u0647",
                       label="aW-l-l-h",
                       comment="won't work")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u06EB\u0644\u0644\u0647",
                       label="a-M-l-l-h")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0641\u0627\u0644\u0644\u0647",
                       label="f-a-l-l-h",
                       comment="Rule 3a: shouldn't match")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u06EB\u0644\u0647",
                       label="a-l-M-l-h",
                       comment="Rule 3d: shouldn't match")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u200D\u0644\u0647",
                       label="a-l-zwj-l-h",
                       comment="Rule 4a: shouldn't match")
        ftml.closeTest()
        ftml.addToTest(None,
                       r"\u0627\u0644\u200D\u0644\u0651\u0670\u0647",
                       label="a-l-zwj-l-s-da-h",
                       comment="Rule 4a: shouldn't match")
        ftml.closeTest()

    if test.lower().startswith("al sorted"):
        # all AL chars, sorted by shape:
        ftml.startTestGroup('Arabic Letters')
        for uid in sorted(filter(lambda u: get_ucd(u, 'bc') == 'AL',
                                 builder.uids()),
                          key=joinGoupSortKey):
            c = builder.char(uid)
            for featlist in builder.permuteFeatures(uids=(uid, )):
                ftml.setFeatures(featlist)
                builder.render((uid, ), ftml)
            ftml.clearFeatures()
            if len(c.langs):
                for langID in builder.allLangs:
                    ftml.setLang(langID)
                    builder.render((uid, ), ftml)
                ftml.clearLang()

    if test.lower().startswith("diac"):
        # Diac attachment:

        doLongTest = 'short' not in test.lower()

        # Representative base and diac chars:
        if doLongTest:
            repDiac = list(
                filter(lambda x: x in builder.uids(),
                       (0x064E, 0x0650, 0x065E, 0x0670, 0x0616, 0x06E3, 0x08F0,
                        0x08F2)))
            repBase = list(
                filter(
                    lambda x: x in builder.uids(),
                    (0x0627, 0x0628, 0x062B, 0x0647, 0x064A, 0x77F, 0x08AC)))
            lamlist = list(
                filter(
                    lambda x: x in builder.uids(),
                    (0x0644, 0x06B5, 0x06B6, 0x06B7, 0x06B8, 0x076A, 0x08A6)))
            aleflist = list(
                filter(lambda x: x in builder.uids(),
                       (0x0627, 0x0622, 0x0623, 0x0625, 0x0671, 0x0672, 0x0673,
                        0x0675, 0x0773, 0x0774)))
        else:
            repDiac = list(
                filter(lambda x: x in builder.uids(),
                       (0x064E, 0x0650, 0x0670)))
            repBase = list(
                filter(lambda x: x in builder.uids(), (0x0627, 0x0628)))
            lamlist = list(
                filter(
                    lambda x: x in builder.uids(),
                    (0x0644, 0x06B5, 0x06B6, 0x06B7, 0x06B8, 0x076A, 0x08A6)))
            aleflist = list(
                filter(lambda x: x in builder.uids(),
                       (0x0627, 0x0622, 0x0623, 0x0625, 0x0671, 0x0672, 0x0673,
                        0x0675, 0x0773, 0x0774)))

        ftml.startTestGroup(
            'Representative diacritics on all bases that take diacritics')
        for uid in sorted(builder.uids()):
            if uid < 32 or uid in (0xAA, 0xBA): continue
            c = builder.char(uid)
            # Always process Lo, but others only if that take marks:
            if c.general == 'Lo' or c.isBase:
                for diac in repDiac:
                    for featlist in builder.permuteFeatures(uids=(uid, diac)):
                        ftml.setFeatures(featlist)
                        builder.render((uid, diac),
                                       ftml,
                                       addBreaks=False,
                                       dualJoinMode=2)
                        if doLongTest:
                            if diac != 0x0651:  # If not shadda
                                # include shadda, in either order:
                                builder.render((uid, diac, 0x0651),
                                               ftml,
                                               addBreaks=False,
                                               dualJoinMode=2)
                                builder.render((uid, 0x0651, diac),
                                               ftml,
                                               addBreaks=False,
                                               dualJoinMode=2)
                            if diac != 0x0654:  # If not hamza above
                                # include hamza above, in either order:
                                builder.render((uid, diac, 0x0654),
                                               ftml,
                                               addBreaks=False,
                                               dualJoinMode=2)
                                builder.render((uid, 0x0654, diac),
                                               ftml,
                                               addBreaks=False,
                                               dualJoinMode=2)
                    ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('All Arabic diacritics on representative bases')
        for uid in sorted(builder.uids()):
            # ignore non-ABS marks
            if uid < 0x600 or uid in range(0xFE00, 0xFE10): continue
            c = builder.char(uid)
            if c.general == 'Mn':
                for base in repBase:
                    for featlist in builder.permuteFeatures(uids=(uid, base)):
                        ftml.setFeatures(featlist)
                        builder.render((base, uid),
                                       ftml,
                                       keyUID=uid,
                                       addBreaks=False,
                                       dualJoinMode=2)
                        if doLongTest:
                            if uid != 0x0651:  # if not shadda
                                # include shadda, in either order:
                                builder.render((base, uid, 0x0651),
                                               ftml,
                                               keyUID=uid,
                                               addBreaks=False,
                                               dualJoinMode=2)
                                builder.render((base, 0x0651, uid),
                                               ftml,
                                               keyUID=uid,
                                               addBreaks=False,
                                               dualJoinMode=2)
                            if diac != 0x0670:  # If not superscript alef
                                # include superscript alef, in either order:
                                builder.render((uid, diac, 0x0670),
                                               ftml,
                                               addBreaks=False,
                                               dualJoinMode=2)
                                builder.render((uid, 0x0670, diac),
                                               ftml,
                                               addBreaks=False,
                                               dualJoinMode=2)
                    ftml.clearFeatures()
                ftml.closeTest()

        ftml.startTestGroup('Special cases')
        builder.render((0x064A, 0x064E), ftml)  # Yeh + Fatha should keep dots
        builder.render((0x064A, 0x0654), ftml)  # Yeh + Hamza should loose dots
        ftml.closeTest()

        ftml.startTestGroup('LamAlef ligatures')
        diaB = 0x064D
        diaA = 0x064B
        for lam in lamlist:
            for alef in aleflist:
                for featlist in builder.permuteFeatures(uids=(lam, alef)):
                    ftml.setFeatures(featlist)
                    builder.render((lam, alef), ftml, addBreaks=False)
                    builder.render((lam, diaA, alef, diaA),
                                   ftml,
                                   addBreaks=False)
                    builder.render((lam, diaB, alef), ftml, addBreaks=False)
                    builder.render((lam, alef, diaB), ftml, addBreaks=False)
                    builder.render((lam, diaB, alef, diaB),
                                   ftml,
                                   addBreaks=False)
                    ftml.clearFeatures()
                ftml.closeTest()

    if test.lower().startswith("subtending"):
        # Generates sample data for all subtending marks. Data includes sequences of 0 to n+1
        # digits, where n is the maximum expected to be supported on the mark. Latin, Arbic-Indic,
        # and Extended Arabic-Indic digits are included.
        for digitSample in filter(lambda x: x in builder.uids(),
                                  (0x0032, 0x0668, 0x06F8)):
            digitOne = (digitSample & 0xFFF0) + 1
            for uid, lgt in filter(lambda x: x[0] in builder.uids(),
                                   ([0x600, 3], [0x0601, 4], [0x0602, 2], [
                                       0x0603, 4
                                   ], [0x0604, 4], [0x0605, 4], [0x06DD, 3])):
                c = chr(uid)
                label = "U+{0:04X} {1}".format(
                    uid, 'latn' if digitOne == 0x0031 else
                    'arab' if digitOne == 0x0661 else 'urdu')
                comment = builder.char(uid).basename
                for featlist in builder.permuteFeatures(uids=(uid, )):
                    ftml.setFeatures(featlist)
                    ftml.addToTest(uid, "\u0628" + c + "\u0645", label,
                                   comment)
                    for ln in range(1, lgt + 1):
                        ftml.addToTest(uid, c + chr(digitSample) * ln)
                    ftml.addToTest(uid, c + chr(digitOne) + chr(digitOne + 1))
                ftml.clearFeatures()
                ftml.closeTest()

                if uid == 0x06DD and digitOne == 0x06F1:
                    # Extra items for Eastern digits
                    for featlist in builder.permuteFeatures(uids=(uid,
                                                                  0x06F7)):
                        ftml.setFeatures(featlist)
                        ftml.addToTest(uid, c + "\u06F4\u06F6\u06F7", label,
                                       "4 6 7")
                    ftml.clearFeatures()
                    for langID in builder.allLangs:
                        ftml.setLang(langID)
                        for featlist in ((None, ), (['cv80',
                                                     '1'], ), (['cv80',
                                                                '2'], )):
                            ftml.setFeatures(featlist)
                            ftml.addToTest(uid, c + "\u06F4\u06F6\u06F7",
                                           label, "4 6 7")
                        ftml.clearFeatures()
                    ftml.clearLang()
                    ftml.closeTest()

    if test.lower().startswith("showinv"):
        # Sample data for chars that have a "show invisible" feature
        # The 'r', 'a', 'ra' indicates whether this is standard in Roman fonts, Arabic fonts, or both.
        invlist = [(0x034F, 'r'), (0x061C, 'a'), (0x200B, 'r'), (0x200C, 'ra'),
                   (0x200D, 'ra'), (0x200E, 'ra'), (0x200F, 'ra'),
                   (0x202A, 'ra'), (0x202B, 'ra'), (0x202C, 'ra'),
                   (0x202D, 'ra'), (0x202E, 'ra'), (0x202E, 'r'),
                   (0x2060, 'r'), (0x2061, 'r'), (0x2062, 'r'), (0x2063, 'r'),
                   (0x2066, 'a'), (0x2067, 'a'), (0x2068, 'a'), (0x2069, 'a'),
                   (0xFE00, 'ra'), (0xFE01, 'ra'), (0xFE02, 'ra'),
                   (0xFE03, 'ra'), (0xFE04, 'ra'), (0xFE05, 'ra'),
                   (0xFE06, 'ra'), (0xFE07, 'ra'), (0xFE08, 'ra'),
                   (0xFE09, 'ra'), (0xFE0A, 'ra'), (0xFE0B, 'ra'),
                   (0xFE0C, 'ra'), (0xFE0D, 'ra'), (0xFE0E, 'ra'),
                   (0xFE0F, 'ra')]
        featlist = (('invs', '1'), ('ss06', '1'))
        ftml.setFeatures(featlist)
        for inv in invlist:
            uid = inv[0]
            c = chr(uid)
            label = 'U+{0:04X} ({1})'.format(uid, inv[1])
            comment = builder.char(
                uid).basename if uid in builder.uids() else ""
            ftml.addToTest(uid, " " + c + " ", label, comment)
            ftml.closeTest()
        ftml.clearFeatures()

    if test.lower().startswith('daggeralef'):
        for uid in sorted(builder.uids(), key=joinGoupSortKey):
            if get_ucd(uid, 'jg') not in ('Sad', 'Seen', 'Yeh'):
                # If not Yeh, Sad or seen joining group we're not interested
                continue
            for featlist in builder.permuteFeatures(uids=(uid, 0x0670)):
                ftml.setFeatures(featlist)
                builder.render((uid, 0x0670), ftml)
            ftml.clearFeatures()
            ftml.closeTest()

    if test.lower().startswith('kern'):
        rehs = sorted(
            filter(lambda uid: get_ucd(uid, 'jg') == 'Reh', builder.uids()))
        waws = sorted(
            filter(lambda uid: get_ucd(uid, 'jg') == 'Waw', builder.uids()))
        uids = sorted(filter(
            lambda uid: get_ucd(uid, 'jt') in ('D', 'R') or uid == 0xFD3E,
            builder.uids()),
                      key=joinGoupSortKey)
        # NB: I wondered about including punctuation, i.e.,  get_ucd(uid, 'gc').startswith('P'), but the default
        #     spacing is pretty good and graphite collision avoidance makes it worse, so the only one we need is FDFE

        dbehf = chr(0x066E) + chr(0x200D)  # dotless beh final
        alef = chr(0x0627)  # alef
        zwj = chr(0x200D)  # Zero width joiner
        ma = 0x064B  # Mark above (fathatan)
        mb = 0x064D  # chr(0x064D)     # Mark below (kasratan)

        if "data" not in test.lower():
            ftml.startTestGroup('All the rehs')
            for uid in rehs:
                c = chr(uid)
                label = 'U+{0:04X}'.format(uid)
                comment = builder.char(uid).basename
                for featlist in builder.permuteFeatures(uids=(uid, )):
                    ftml.setFeatures(featlist)
                    ftml.addToTest(uid, c + dbehf + ' ' + zwj + c + dbehf,
                                   label, comment)
                ftml.clearFeatures()
                ftml.closeTest()

            ftml.startTestGroup('All the waws')
            for uid in waws:
                c = chr(uid)
                label = 'U+{0:04X}'.format(uid)
                comment = builder.char(uid).basename
                for featlist in builder.permuteFeatures(uids=(uid, )):
                    ftml.setFeatures(featlist)
                    ftml.addToTest(uid, c + dbehf + ' ' + zwj + c + dbehf,
                                   label, comment)
                ftml.clearFeatures()
                ftml.closeTest()

            # reh or waw plus the others
            for uid1 in (0x631, 0x648):  # (reh, waw)
                ftml.startTestGroup('{} + all the others'.format(
                    get_ucd(uid1, 'jg')))
                c1 = chr(uid1)
                for uid2 in uids:
                    c2 = chr(uid2)
                    comment = builder.char(uid2).basename
                    label = 'U+{:04X}'.format(uid2)
                    for featlist in builder.permuteFeatures(uids=(uid1, uid2)):
                        ftml.setFeatures(featlist)
                        if get_ucd(uid2, 'jt') == 'D':
                            ftml.addToTest(uid2, zwj + c1 + c2 + zwj, label,
                                           comment)
                            ftml.addToTest(uid2, c1 + c2 + zwj)
                        ftml.addToTest(uid2, zwj + c1 + c2, label, comment)
                        ftml.addToTest(uid2, c1 + c2)
                    ftml.clearFeatures()
                    ftml.closeTest()

        else:
            # exhaustive test for kerning data extraction
            ftml.defaultRTL = True
            addMarks = "with marks" in test.lower()
            for uid1 in rehs:  # (rehs[0],)
                for uid2 in uids:
                    for featlist in builder.permuteFeatures(uids=(uid1, uid2)):
                        ftml.setFeatures(featlist)
                        builder.render([uid1, uid2],
                                       ftml,
                                       addBreaks=False,
                                       rtl=True,
                                       dualJoinMode=1)
                        if addMarks:
                            builder.render([uid1, uid2, mb],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, uid2, ma],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, uid2, mb, ma],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, uid2, ma, mb],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, ma, uid2],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, ma, uid2, mb],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, ma, uid2, ma],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, ma, uid2, mb, ma],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, ma, uid2, ma, mb],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, mb, uid2],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, mb, uid2, mb],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, mb, uid2, ma],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, mb, uid2, mb, ma],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                            builder.render([uid1, mb, uid2, ma, mb],
                                           ftml,
                                           addBreaks=False,
                                           rtl=True,
                                           dualJoinMode=1)
                    ftml.clearFeatures()
                    ftml.closeTest()

    if test.lower().startswith('chadian'):
        rehs = '[' + ''.join(
            map(
                chr,
                filter(lambda uid: get_ucd(uid, 'jg') == 'Reh',
                       builder.uids()))) + ']'
        uids = '[' + ''.join(
            map(
                chr,
                filter(
                    lambda uid: get_ucd(uid, 'jt') in
                    ('D', 'R') or uid == 0xFD3E, builder.uids()))) + ']'
        marks = '[' + ''.join(
            map(
                chr,
                filter(lambda uid: get_ucd(uid, 'gc').startswith('M'),
                       builder.uids()))) + ']'
        rehwordsRE = re.compile(f'({rehs}{marks}{uids}{marks}*)')
        with open('/SRC/ABS Text Samples/Chad/Chadian Arabic AS word list.txt',
                  encoding="utf8") as f:
            for line_no, line in enumerate(f):
                res = ''
                matches = ''
                lastEnd = 0
                for m in rehwordsRE.finditer(line):
                    if m.start() > 0:
                        res += line[lastEnd:m.start()]
                    # I wish I could output <em> around the kerned pair, something like:
                    #     res += f'<em>{m.group()}</em>'
                    # but apparently ftml.py doesn't support this :-(
                    # So just append
                    res += m.group()
                    # Keep track af all matched strings for feature permutations
                    matches += m.group()
                    lastEnd = m.end()
                if len(res) > 0:
                    # Add tail to result
                    res += line[lastEnd:]
                    # figure features based only on what matched
                    matchedUids = map(ord, list(matches))
                    for featlist in builder.permuteFeatures(uids=matchedUids):
                        ftml.setFeatures(featlist)
                        # Add to test:
                        ftml.addToTest(None, res, f'line {line_no}')
                        ftml.clearFeatures()
                        ftml.closeTest()

    if test.lower().startswith('yehbar'):
        # Yehbarree tail interacting with diacs below previous char
        uids = sorted(filter(lambda uid: get_ucd(uid, 'jt') in ('D', ),
                             builder.uids()),
                      key=basenameSortKey)
        markbelow = r'\u064D'  # kasratan
        markabove = r'\u06EC'  # dotStopabove-ar
        zwj = r'\u200D'  # Zero width joiner

        ftml.startTestGroup('U+06D2 yehbarree')
        yehbarree = r'\u06D2'
        for uid in uids:
            if uid < 32: continue
            c = r'\u{:04X}'.format(uid)
            label = 'U+{:04X}'.format(uid)
            comment = builder.char(uid).basename
            for featlist in builder.permuteFeatures(uids=(uid, )):
                ftml.setFeatures(featlist)
                ftml.addToTest(
                    uid,
                    f"{c}{markabove}{yehbarree} {zwj}{c}{markabove}{yehbarree} {c}{markbelow}{markabove}{yehbarree} {zwj}{c}{markbelow}{markabove}{yehbarree}",
                    label, comment)
                ftml.closeTest()
            ftml.clearFeatures()

        # Also test other forms of yehbarree (yehbarreeHamzaabove-ar, yehbarreeTwoabove, yehbarreeThreeabove-ar)
        ftml.startTestGroup('yehbarree-like')
        for yehbarree in filter(lambda x: x in builder.uids(),
                                (0x06D3, 0x077A, 0x077B)):
            for uid in filter(lambda x: x in builder.uids(), (0x06A0, 0x08B3)):
                c = r'\u{:04X}'.format(uid)
                yb = r'\u{:04X}'.format(yehbarree)
                label = 'U+{:04X} U+{:04X}'.format(uid, yehbarree)
                comment = builder.char(uid).basename + ' ' + builder.char(
                    yehbarree).basename
                for featlist in builder.permuteFeatures(uids=(uid, )):
                    ftml.setFeatures(featlist)
                    ftml.addToTest(
                        uid,
                        f"{c}{markabove}{yb} {zwj}{c}{markabove}{yb} {c}{markbelow}{markabove}{yb} {zwj}{c}{markbelow}{markabove}{yb}",
                        label, comment)
                    ftml.closeTest()
                ftml.clearFeatures()

    if test.lower().startswith('classes'):
        zwj = chr(0x200D)
        lsb = ''  # chr(0xF130)
        rsb = ''  # chr(0xF131)

        glyphsSeen = set()

        uids = sorted(
            filter(lambda uid: builder.char(uid).general == 'Lo' and uid > 255,
                   builder.uids()))
        uids = sorted(uids, key=joinGoupSortKey)
        for uid in uids:
            c = chr(uid)
            thischar = builder.char(uid)
            label = 'U+{:04X}'.format(uid)
            for featlist in builder.permuteFeatures(uids=(uid, )):
                gname = thischar.basename
                if len(featlist) == 1 and featlist[0] is not None:
                    # See if we can find an alternate glyph name:
                    feat = '{}={}'.format(featlist[0][0], featlist[0][1])
                    gname = thischar.altnames.get(feat, gname)
                if gname not in glyphsSeen:
                    glyphsSeen.add(gname)
                    comment = gname
                    ftml.setFeatures(featlist)
                    ftml.addToTest(uid, lsb + c + rsb, label,
                                   comment)  #isolate
                    if get_ucd(uid, 'jt') == 'D':
                        ftml.addToTest(uid, lsb + c + zwj + rsb)  # initial
                        ftml.addToTest(uid,
                                       lsb + zwj + c + zwj + rsb)  # medial
                    if get_ucd(uid, 'jt') in ('R', 'D'):
                        ftml.addToTest(uid, lsb + zwj + c + rsb)  # final
            ftml.clearFeatures()
            ftml.closeTest()

    ftml.writeFile(args.output)