From 7e143fa2133be4a9089a529a52264fa00a18bff1 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 13 Jun 2023 16:26:45 +0100 Subject: [PATCH 01/12] Add the contextual-aware feature writer --- Lib/glyphsLib/featureWriters/__init__.py | 0 .../featureWriters/markFeatureWriter.py | 266 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 Lib/glyphsLib/featureWriters/__init__.py create mode 100644 Lib/glyphsLib/featureWriters/markFeatureWriter.py diff --git a/Lib/glyphsLib/featureWriters/__init__.py b/Lib/glyphsLib/featureWriters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Lib/glyphsLib/featureWriters/markFeatureWriter.py b/Lib/glyphsLib/featureWriters/markFeatureWriter.py new file mode 100644 index 000000000..5f2ad9773 --- /dev/null +++ b/Lib/glyphsLib/featureWriters/markFeatureWriter.py @@ -0,0 +1,266 @@ +from collections import OrderedDict, defaultdict +import re + +from glyphsLib.builder.constants import OBJECT_LIBS_KEY +from ufo2ft.featureWriters import ast +from ufo2ft.featureWriters.markFeatureWriter import ( + MARK_PREFIX, + LIGA_SEPARATOR, + LIGA_NUM_RE, + MarkToBasePos, + NamedAnchor, + MarkFeatureWriter, + quantize, +) + + +class ContextuallyAwareNamedAnchor(NamedAnchor): + __slots__ = ( + "name", + "x", + "y", + "isMark", + "key", + "number", + "markClass", + "isContextual", + "libData", + ) + + @classmethod + def parseAnchorName( + cls, + anchorName, + markPrefix=MARK_PREFIX, + ligaSeparator=LIGA_SEPARATOR, + ligaNumRE=LIGA_NUM_RE, + ignoreRE=None, + ): + """Parse anchor name and return a tuple that specifies: + 1) whether the anchor is a "mark" anchor (bool); + 2) the "key" name of the anchor, i.e. the name after stripping all the + prefixes and suffixes, which identifies the class it belongs to (str); + 3) An optional number (int), starting from 1, which identifies that index + of the ligature component the anchor refers to. + + The 'ignoreRE' argument is an optional regex pattern (str) identifying + sub-strings in the anchor name that should be ignored when parsing the + three elements above. + """ + number = None + isContextual = False + if ignoreRE is not None: + anchorName = re.sub(ignoreRE, "", anchorName) + + if anchorName[0] == "*": + isContextual = True + anchorName = anchorName[1:] + anchorName = re.sub(r"\..*", "", anchorName) + + m = ligaNumRE.match(anchorName) + if not m: + key = anchorName + else: + number = m.group(1) + key = anchorName.rstrip(number) + separator = ligaSeparator + if key.endswith(separator): + assert separator + key = key[: -len(separator)] + number = int(number) + else: + # not a valid ligature anchor name + key = anchorName + number = None + + if anchorName.startswith(markPrefix) and key: + if number is not None: + raise ValueError("mark anchor cannot be numbered: %r" % anchorName) + isMark = True + key = key[len(markPrefix) :] + if not key: + raise ValueError("mark anchor key is nil: %r" % anchorName) + else: + isMark = False + + return isMark, key, number, isContextual + + def __init__(self, name, x, y, markClass=None, libData=None): + self.name = name + self.x = x + self.y = y + isMark, key, number, isContextual = self.parseAnchorName( + name, + markPrefix=self.markPrefix, + ligaSeparator=self.ligaSeparator, + ligaNumRE=self.ligaNumRE, + ignoreRE=self.ignoreRE, + ) + if number is not None: + if number < 1: + raise ValueError("ligature component indexes must start from 1") + else: + assert key, name + self.isMark = isMark + self.key = key + self.number = number + self.markClass = markClass + self.isContextual = isContextual + self.libData = libData + + +class ContextualMarkFeatureWriter(MarkFeatureWriter): + NamedAnchor = ContextuallyAwareNamedAnchor + + def _getAnchorLists(self): + gdefClasses = self.context.gdefClasses + if gdefClasses.base is not None: + # only include the glyphs listed in the GDEF.GlyphClassDef groups + include = gdefClasses.base | gdefClasses.ligature | gdefClasses.mark + else: + # no GDEF table defined in feature file, include all glyphs + include = None + result = OrderedDict() + for glyphName, glyph in self.getOrderedGlyphSet().items(): + if include is not None and glyphName not in include: + continue + anchorDict = OrderedDict() + for anchor in glyph.anchors: + anchorName = anchor.name + if not anchorName: + self.log.warning( + "unnamed anchor discarded in glyph '%s'", glyphName + ) + continue + if anchorName in anchorDict: + self.log.warning( + "duplicate anchor '%s' in glyph '%s'", anchorName, glyphName + ) + x = quantize(anchor.x, self.options.quantization) + y = quantize(anchor.y, self.options.quantization) + libData = None + if anchor.identifier: + libData = glyph.lib[OBJECT_LIBS_KEY].get(anchor.identifier) + a = self.NamedAnchor(name=anchorName, x=x, y=y, libData=libData) + if a.isContextual and not libData: + continue + anchorDict[anchorName] = a + if anchorDict: + result[glyphName] = list(anchorDict.values()) + return result + + def _makeMarkToBaseAttachments(self, contextual=False): + markGlyphNames = self.context.markGlyphNames + baseClass = self.context.gdefClasses.base + result = [] + for glyphName, anchors in self.context.anchorLists.items(): + # exclude mark glyphs, or glyphs not listed in GDEF Base + if glyphName in markGlyphNames or ( + baseClass is not None and glyphName not in baseClass + ): + continue + baseMarks = [] + for anchor in anchors: + if not contextual and anchor.isContextual: + continue + if contextual: + # Check if anchor context fits + pass + if anchor.markClass is None or anchor.number is not None: + # skip anchors for which no mark class is defined; also + # skip '_1', '_2', etc. suffixed anchors for this lookup + # type; these will be are added in the mark2liga lookup + continue + assert not anchor.isMark + baseMarks.append(anchor) + if not baseMarks: + continue + result.append(MarkToBasePos(glyphName, baseMarks)) + return result + + def _makeFeatures(self): + features = super()._makeFeatures() + # Now do the contextual ones + + # Arrange by context + by_context = defaultdict(list) + markGlyphNames = self.context.markGlyphNames + + for glyphName, anchors in self.context.anchorLists.items(): + if glyphName in markGlyphNames: + continue + for anchor in anchors: + if not anchor.isContextual: + continue + anchor_context = anchor.libData["GPOS_Context"] + by_context[anchor_context].append((glyphName, anchor)) + if not by_context: + return features, [] + + # Pull the lookups from the feature and replace them with lookup references, + # to ensure the order is correct + lookups = features["mark"].statements + features["mark"].statements = [ + ast.LookupReferenceStatement(lu) for lu in lookups + ] + + dispatch_lookups = {} + for ix, (fullcontext, glyph_anchor_pair) in enumerate(by_context.items()): + # Make the contextual lookup + lookupname = "ContextualMark_%i" % ix + if ";" in fullcontext: + before, after = fullcontext.split(";") + # I know it's not really a comment but this is the easiest way + # to get the lookup flag in there without reparsing it. + else: + after = fullcontext + before = "" + if before not in dispatch_lookups: + dispatch_lookups[before] = ast.LookupBlock( + "ContextualMarkDispatch_%i" % len(dispatch_lookups.keys()) + ) + dispatch_lookups[before].statements.append(ast.Comment(before + ";\n")) + features["mark"].statements.append( + ast.LookupReferenceStatement(dispatch_lookups[before]) + ) + lkp = dispatch_lookups[before] + lookup = ast.LookupBlock(lookupname) + for glyph, anchor in glyph_anchor_pair: + lookup.statements.append(MarkToBasePos(glyph, [anchor]).asAST()) + lookups.append(lookup) + + for glyph, anchor in glyph_anchor_pair: + marks = ast.GlyphClass( + self.context.markClasses[anchor.key].glyphs.keys() + ).asFea() + if "&" not in after: + after = after.replace("*", "* &") + # Replace & with mark name if present + contextual = after.replace("*", f"{glyph}") + contextual = contextual.replace("&", f"{marks}' lookup {lookupname}") + lkp.statements.append(ast.Comment(f"pos {contextual};")) + + lookups.extend(dispatch_lookups.values()) + + return features, lookups + + def _write(self): + self._pruneUnusedAnchors() + + newClassDefs = self._makeMarkClassDefinitions() + self._setBaseAnchorMarkClasses() + + features, lookups = self._makeFeatures() + if not features: + return False + + feaFile = self.context.feaFile + + self._insert( + feaFile=feaFile, + markClassDefs=newClassDefs, + features=[features[tag] for tag in sorted(features.keys())], + lookups=lookups, + ) + + return True From a1bc184c39c714955b40f28c9e3f6f5565dabe7a Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 13 Jun 2023 16:26:53 +0100 Subject: [PATCH 02/12] Plug in the contextual-aware feature writer --- Lib/glyphsLib/builder/constants.py | 7 +------ Lib/glyphsLib/builder/font.py | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/glyphsLib/builder/constants.py b/Lib/glyphsLib/builder/constants.py index 4182bdd7c..b09792e00 100644 --- a/Lib/glyphsLib/builder/constants.py +++ b/Lib/glyphsLib/builder/constants.py @@ -210,14 +210,9 @@ # mode for the ufo2ft KernFeatureWriter whenever the GSFont contain a non-automatic # 'kern' feature. # See https://glyphsapp.com/tutorials/contextual-kerning -# NOTE: Even though we use the default "skip" mode for the MarkFeatureWriter, -# we still must include it this custom featureWriters list, as this is used -# instead of the default ufo2ft list of feature writers. -# This also means that if ufo2ft adds new writers to that default list, we -# would need to update this accordingly... :-/ DEFAULT_FEATURE_WRITERS = [ {"class": "KernFeatureWriter", "options": {"mode": "append"}}, - {"class": "MarkFeatureWriter", "options": {"mode": "skip"}}, + {"module": "glyphsLib.featureWriters.markFeatureWriter", "class": "ContextualMarkFeatureWriter", "options": {"mode": "skip"}}, ] DEFAULT_LAYER_NAME = PUBLIC_PREFIX + "default" diff --git a/Lib/glyphsLib/builder/font.py b/Lib/glyphsLib/builder/font.py index 6618a575c..d2fcd1fba 100644 --- a/Lib/glyphsLib/builder/font.py +++ b/Lib/glyphsLib/builder/font.py @@ -15,6 +15,8 @@ from .common import to_ufo_time, from_ufo_time from .constants import ( + DEFAULT_FEATURE_WRITERS, + UFO2FT_FEATURE_WRITERS_KEY, UFO2FT_FILTERS_KEY, APP_VERSION_LIB_KEY, KEYBOARD_INCREMENT_KEY, @@ -52,6 +54,7 @@ def to_ufo_font_attributes(self, family_name): ufo.lib.setdefault(UFO2FT_FILTERS_KEY, []).append( {"namespace": "glyphsLib.filters", "name": "eraseOpenCorners", "pre": True} ) + ufo.lib[UFO2FT_FEATURE_WRITERS_KEY] = DEFAULT_FEATURE_WRITERS self.to_ufo_custom_params(ufo, font) # .custom_params self.to_ufo_master_attributes(ufo, master) # .masters From f9358596162cda71c29bb5c5d91ed217c13ca01e Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 13 Jun 2023 16:49:49 +0100 Subject: [PATCH 03/12] Add CursFeatureWriter to default feature writers --- Lib/glyphsLib/builder/constants.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/glyphsLib/builder/constants.py b/Lib/glyphsLib/builder/constants.py index b09792e00..16640b3f6 100644 --- a/Lib/glyphsLib/builder/constants.py +++ b/Lib/glyphsLib/builder/constants.py @@ -212,7 +212,12 @@ # See https://glyphsapp.com/tutorials/contextual-kerning DEFAULT_FEATURE_WRITERS = [ {"class": "KernFeatureWriter", "options": {"mode": "append"}}, - {"module": "glyphsLib.featureWriters.markFeatureWriter", "class": "ContextualMarkFeatureWriter", "options": {"mode": "skip"}}, + { + "module": "glyphsLib.featureWriters.markFeatureWriter", + "class": "ContextualMarkFeatureWriter", + "options": {"mode": "skip"}, + }, + {"class": "CursFeatureWriter"}, ] DEFAULT_LAYER_NAME = PUBLIC_PREFIX + "default" From 0f7f054451aec5c89470b820ed7843368695c514 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 14 Jun 2023 11:44:03 +0100 Subject: [PATCH 04/12] We don't need to subclass this --- .../featureWriters/markFeatureWriter.py | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/Lib/glyphsLib/featureWriters/markFeatureWriter.py b/Lib/glyphsLib/featureWriters/markFeatureWriter.py index 5f2ad9773..5cba73e90 100644 --- a/Lib/glyphsLib/featureWriters/markFeatureWriter.py +++ b/Lib/glyphsLib/featureWriters/markFeatureWriter.py @@ -149,35 +149,6 @@ def _getAnchorLists(self): result[glyphName] = list(anchorDict.values()) return result - def _makeMarkToBaseAttachments(self, contextual=False): - markGlyphNames = self.context.markGlyphNames - baseClass = self.context.gdefClasses.base - result = [] - for glyphName, anchors in self.context.anchorLists.items(): - # exclude mark glyphs, or glyphs not listed in GDEF Base - if glyphName in markGlyphNames or ( - baseClass is not None and glyphName not in baseClass - ): - continue - baseMarks = [] - for anchor in anchors: - if not contextual and anchor.isContextual: - continue - if contextual: - # Check if anchor context fits - pass - if anchor.markClass is None or anchor.number is not None: - # skip anchors for which no mark class is defined; also - # skip '_1', '_2', etc. suffixed anchors for this lookup - # type; these will be are added in the mark2liga lookup - continue - assert not anchor.isMark - baseMarks.append(anchor) - if not baseMarks: - continue - result.append(MarkToBasePos(glyphName, baseMarks)) - return result - def _makeFeatures(self): features = super()._makeFeatures() # Now do the contextual ones From ecf016d7217a64fc2db6dcaebc7a3dc58398109a Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 14 Jun 2023 11:44:50 +0100 Subject: [PATCH 05/12] Add comments to aid debugging, this stuff is complex --- Lib/glyphsLib/featureWriters/markFeatureWriter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/glyphsLib/featureWriters/markFeatureWriter.py b/Lib/glyphsLib/featureWriters/markFeatureWriter.py index 5cba73e90..549ec1f49 100644 --- a/Lib/glyphsLib/featureWriters/markFeatureWriter.py +++ b/Lib/glyphsLib/featureWriters/markFeatureWriter.py @@ -157,7 +157,7 @@ def _makeFeatures(self): by_context = defaultdict(list) markGlyphNames = self.context.markGlyphNames - for glyphName, anchors in self.context.anchorLists.items(): + for glyphName, anchors in sorted(self.context.anchorLists.items()): if glyphName in markGlyphNames: continue for anchor in anchors: @@ -186,6 +186,7 @@ def _makeFeatures(self): else: after = fullcontext before = "" + after = after.strip() if before not in dispatch_lookups: dispatch_lookups[before] = ast.LookupBlock( "ContextualMarkDispatch_%i" % len(dispatch_lookups.keys()) @@ -195,6 +196,7 @@ def _makeFeatures(self): ast.LookupReferenceStatement(dispatch_lookups[before]) ) lkp = dispatch_lookups[before] + lkp.statements.append(ast.Comment("# " + after)) lookup = ast.LookupBlock(lookupname) for glyph, anchor in glyph_anchor_pair: lookup.statements.append(MarkToBasePos(glyph, [anchor]).asAST()) @@ -209,7 +211,9 @@ def _makeFeatures(self): # Replace & with mark name if present contextual = after.replace("*", f"{glyph}") contextual = contextual.replace("&", f"{marks}' lookup {lookupname}") - lkp.statements.append(ast.Comment(f"pos {contextual};")) + lkp.statements.append( + ast.Comment(f"pos {contextual}; # {glyph}/{anchor.name}") + ) lookups.extend(dispatch_lookups.values()) From 59baee24ef1ead9a99c0b503364dddc072c7ada1 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 14 Jun 2023 11:45:32 +0100 Subject: [PATCH 06/12] Help to minimize differences between masters --- Lib/glyphsLib/featureWriters/markFeatureWriter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/glyphsLib/featureWriters/markFeatureWriter.py b/Lib/glyphsLib/featureWriters/markFeatureWriter.py index 549ec1f49..b31d4c8d7 100644 --- a/Lib/glyphsLib/featureWriters/markFeatureWriter.py +++ b/Lib/glyphsLib/featureWriters/markFeatureWriter.py @@ -163,7 +163,7 @@ def _makeFeatures(self): for anchor in anchors: if not anchor.isContextual: continue - anchor_context = anchor.libData["GPOS_Context"] + anchor_context = anchor.libData["GPOS_Context"].strip() by_context[anchor_context].append((glyphName, anchor)) if not by_context: return features, [] From 2e9f4e1d116096220e89e9a447728067b091a910 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 14 Jun 2023 11:47:13 +0100 Subject: [PATCH 07/12] Try to get more specific rules in first --- Lib/glyphsLib/featureWriters/markFeatureWriter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/glyphsLib/featureWriters/markFeatureWriter.py b/Lib/glyphsLib/featureWriters/markFeatureWriter.py index b31d4c8d7..a37a53bf2 100644 --- a/Lib/glyphsLib/featureWriters/markFeatureWriter.py +++ b/Lib/glyphsLib/featureWriters/markFeatureWriter.py @@ -176,7 +176,12 @@ def _makeFeatures(self): ] dispatch_lookups = {} - for ix, (fullcontext, glyph_anchor_pair) in enumerate(by_context.items()): + # We sort the full context by longest first. This isn't perfect + # but it gives us the best chance that more specific contexts + # (typically longer) will take precedence over more general ones. + for ix, (fullcontext, glyph_anchor_pair) in enumerate( + sorted(by_context.items(), key=lambda x: -len(x[0])) + ): # Make the contextual lookup lookupname = "ContextualMark_%i" % ix if ";" in fullcontext: From 8efd7683f5bfb78ad91ebf381e1b2d1242a766c3 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 31 Jul 2023 22:14:03 +0300 Subject: [PATCH 08/12] Add a test for ContextualMarkFeatureWriter --- tests/data/ContextualAnchors.glyphs | 1019 +++++++++++++++++++++++++++ tests/feature_writers_test.py | 32 + 2 files changed, 1051 insertions(+) create mode 100644 tests/data/ContextualAnchors.glyphs create mode 100644 tests/feature_writers_test.py diff --git a/tests/data/ContextualAnchors.glyphs b/tests/data/ContextualAnchors.glyphs new file mode 100644 index 000000000..b2b3c8150 --- /dev/null +++ b/tests/data/ContextualAnchors.glyphs @@ -0,0 +1,1019 @@ +{ +.appVersion = "3208"; +.formatVersion = 3; +axes = ( +{ +name = Weight; +tag = wght; +} +); +customParameters = ( +{ +name = "Write lastChange"; +value = 0; +}, +{ +name = "Write DisplayStrings"; +value = 0; +}, +{ +name = "Propagate Anchors"; +value = 0; +} +); +date = "2023-07-31 15:25:34 +0000"; +familyName = "Contextual Anchors Test"; +featurePrefixes = ( +{ +automatic = 1; +code = "languagesystem DFLT dflt; + +languagesystem arab dflt; +"; +name = Languagesystems; +} +); +features = ( +{ +automatic = 1; +code = "feature init; +feature medi; +feature fina; +"; +tag = aalt; +}, +{ +code = "sub beh-ar by behDotless-ar dotbelow-ar; +"; +tag = ccmp; +}, +{ +automatic = 1; +code = "sub behDotless-ar by behDotless-ar.init; +"; +tag = init; +}, +{ +automatic = 1; +code = "sub behDotless-ar by behDotless-ar.medi; +"; +tag = medi; +}, +{ +automatic = 1; +code = "sub behDotless-ar by behDotless-ar.fina; +"; +tag = fina; +} +); +fontMaster = ( +{ +axesValues = ( +17 +); +customParameters = ( +{ +name = TTFStems; +value = ( +{ +horizontal = 1; +name = Thin; +width = 16; +}, +{ +horizontal = 1; +name = Lowercase; +width = 16; +}, +{ +horizontal = 1; +name = Uppercase; +width = 18; +} +); +} +); +iconName = Light; +id = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +metricValues = ( +{ +over = 10; +pos = 800; +}, +{ +over = 10; +pos = 700; +}, +{ +over = 10; +pos = 470; +}, +{ +over = -10; +}, +{ +over = -10; +pos = -200; +} +); +name = Light; +stemValues = ( +16, +16, +18, +17, +19 +); +userData = { +GSOffsetHorizontal = -30; +GSOffsetProportional = 1; +GSOffsetVertical = -10; +GSRoughenHorizontal = 15; +GSRoughenSegmentLength = 15; +GSRoughenVertical = 10; +com.schriftgestaltung.Glyphs.ufoData = { +customBinaryData = <746865206279746573>; +}; +noodleExtremesAndInflections = 1; +noodleRemoveOverlap = 0; +noodleThickness = "106.0"; +}; +visible = 1; +}, +{ +axesValues = ( +90 +); +customParameters = ( +{ +name = TTFStems; +value = ( +{ +horizontal = 1; +name = Thin; +width = 80; +}, +{ +horizontal = 1; +name = Lowercase; +width = 88; +}, +{ +horizontal = 1; +name = Uppercase; +width = 91; +} +); +} +); +guides = ( +{ +locked = 1; +pos = (-126,90); +} +); +id = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +metricValues = ( +{ +over = 12; +pos = 800; +}, +{ +over = 12; +pos = 700; +}, +{ +over = 12; +pos = 480; +}, +{ +over = -12; +}, +{ +over = -12; +pos = -200; +} +); +name = Regular; +stemValues = ( +80, +88, +91, +90, +93 +); +userData = { +GSOffsetHorizontal = 45; +GSOffsetMakeStroke = 1; +GSOffsetVertical = 44; +GSRoughenHorizontal = 15; +GSRoughenSegmentLength = 15; +GSRoughenVertical = 10; +}; +visible = 1; +}, +{ +axesValues = ( +220 +); +customParameters = ( +{ +name = TTFStems; +value = ( +{ +horizontal = 1; +name = Thin; +width = 108; +}, +{ +horizontal = 1; +name = Lowercase; +width = 210; +}, +{ +horizontal = 1; +name = Uppercase; +width = 215; +} +); +} +); +iconName = Bold; +id = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +metricValues = ( +{ +over = 15; +pos = 800; +}, +{ +over = 15; +pos = 700; +}, +{ +over = 15; +pos = 490; +}, +{ +over = -15; +}, +{ +over = -15; +pos = -200; +} +); +name = Bold; +stemValues = ( +108, +210, +215, +220, +225 +); +userData = { +GSOffsetCapStyle = 3; +GSOffsetHorizontal = 50; +GSOffsetProportional = 1; +GSOffsetVertical = 10; +}; +visible = 1; +} +); +glyphs = ( +{ +glyphname = "behDotless-ar"; +layers = ( +{ +anchors = ( +{ +name = bottom; +pos = (200,-25); +} +); +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +metricRight = "=20"; +shapes = ( +{ +closed = 1; +nodes = ( +(359,0,l), +(359,148,l), +(319,108,l), +(319,40,l), +(102,40,ls), +(80,40,o), +(29,43,o), +(0,82,c), +(15,13,o), +(67,0,o), +(101,0,cs) +); +} +); +width = 379; +}, +{ +anchors = ( +{ +name = bottom; +pos = (290,-25); +} +); +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +metricRight = "=50"; +shapes = ( +{ +closed = 1; +nodes = ( +(500,0,l), +(500,300,l), +(400,200,l), +(400,100,l), +(153,100,ls), +(115,100,o), +(22,110,o), +(0,200,c), +(0,36,o), +(92,0,o), +(152,0,cs) +); +} +); +width = 550; +}, +{ +anchors = ( +{ +name = bottom; +pos = (380,-25); +} +); +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +metricRight = "=100"; +shapes = ( +{ +closed = 1; +nodes = ( +(661,0,l), +(661,521,l), +(461,321,l), +(461,200,l), +(203,200,ls), +(62,200,o), +(23,223,o), +(0,300,c), +(0,100,o), +(63,0,o), +(202,0,cs) +); +} +); +width = 761; +} +); +metricLeft = "=0"; +metricRight = "=100"; +unicode = 1646; +}, +{ +glyphname = "behDotless-ar.fina"; +layers = ( +{ +anchors = ( +{ +name = bottom; +pos = (200,-25); +} +); +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +shapes = ( +{ +closed = 1; +nodes = ( +(379,0,l), +(379,40,l), +(359,40,l), +(359,148,l), +(319,108,l), +(319,40,l), +(102,40,ls), +(80,40,o), +(29,43,o), +(0,82,c), +(15,13,o), +(67,0,o), +(101,0,cs) +); +} +); +width = 379; +}, +{ +anchors = ( +{ +name = bottom; +pos = (290,-25); +} +); +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +shapes = ( +{ +closed = 1; +nodes = ( +(550,0,l), +(550,100,l), +(500,100,l), +(500,300,l), +(400,200,l), +(400,100,l), +(153,100,ls), +(115,100,o), +(22,110,o), +(0,200,c), +(0,36,o), +(92,0,o), +(152,0,cs) +); +} +); +width = 550; +}, +{ +anchors = ( +{ +name = bottom; +pos = (380,-25); +} +); +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +shapes = ( +{ +closed = 1; +nodes = ( +(761,0,l), +(761,200,l), +(661,200,l), +(661,521,l), +(461,321,l), +(461,200,l), +(203,200,ls), +(62,200,o), +(23,223,o), +(0,300,c), +(0,100,o), +(63,0,o), +(202,0,cs) +); +} +); +width = 761; +} +); +metricLeft = "=0"; +metricRight = "=0"; +}, +{ +glyphname = "behDotless-ar.medi"; +layers = ( +{ +anchors = ( +{ +name = bottom; +pos = (25,-85); +} +); +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +shapes = ( +{ +closed = 1; +nodes = ( +(60,0,l), +(60,40,l), +(40,40,l), +(40,148,l), +(0,108,l), +(0,40,l), +(-32,40,l), +(-32,0,l) +); +} +); +width = 60; +}, +{ +anchors = ( +{ +name = bottom; +pos = (55,-100); +} +); +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +shapes = ( +{ +closed = 1; +nodes = ( +(151,0,l), +(151,100,l), +(101,100,l), +(101,300,l), +(1,200,l), +(1,100,l), +(-49,100,l), +(-49,0,l) +); +} +); +width = 151; +}, +{ +anchors = ( +{ +name = bottom; +pos = (105,-100); +} +); +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +shapes = ( +{ +closed = 1; +nodes = ( +(300,0,l), +(300,200,l), +(200,200,l), +(200,521,l), +(0,321,l), +(0,200,l), +(-40,200,l), +(-40,0,l) +); +} +); +width = 300; +} +); +metricRight = "=0"; +}, +{ +glyphname = "behDotless-ar.init"; +layers = ( +{ +anchors = ( +{ +name = "*bottom"; +pos = (25,-50); +userData = { +GPOS_Context = "reh-ar *"; +}; +}, +{ +name = bottom; +pos = (25,-85); +} +); +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +metricRight = "=20"; +shapes = ( +{ +closed = 1; +nodes = ( +(42,0,l), +(42,148,l), +(2,108,l), +(2,40,l), +(-30,40,l), +(-30,0,l) +); +} +); +width = 62; +}, +{ +anchors = ( +{ +name = "*bottom"; +pos = (55,-50); +userData = { +GPOS_Context = "reh-ar *"; +}; +}, +{ +name = bottom; +pos = (55,-100); +} +); +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +metricRight = "=50"; +shapes = ( +{ +closed = 1; +nodes = ( +(100,0,l), +(100,300,l), +(0,200,l), +(0,100,l), +(-90,100,l), +(-90,0,l) +); +} +); +width = 150; +}, +{ +anchors = ( +{ +name = "*bottom"; +pos = (105,-50); +userData = { +GPOS_Context = "reh-ar *"; +}; +}, +{ +name = bottom; +pos = (105,-100); +} +); +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +metricRight = "=100"; +shapes = ( +{ +closed = 1; +nodes = ( +(200,0,l), +(200,521,l), +(0,321,l), +(0,200,l), +(-40,200,l), +(-40,0,l) +); +} +); +width = 300; +} +); +metricRight = "=0"; +}, +{ +glyphname = "beh-ar"; +layers = ( +{ +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +shapes = ( +{ +ref = "behDotless-ar"; +}, +{ +pos = (85,-40); +ref = "dotbelow-ar"; +} +); +width = 379; +}, +{ +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +shapes = ( +{ +ref = "behDotless-ar"; +}, +{ +pos = (140,-75); +ref = "dotbelow-ar"; +} +); +width = 550; +}, +{ +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +shapes = ( +{ +ref = "behDotless-ar"; +}, +{ +pos = (180,-125); +ref = "dotbelow-ar"; +} +); +width = 761; +} +); +unicode = 1576; +}, +{ +glyphname = "reh-ar"; +layers = ( +{ +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +metricLeft = "=-20"; +shapes = ( +{ +closed = 1; +nodes = ( +(49,-85,o), +(62,-32,o), +(62,2,cs), +(62,228,l), +(22,188,l), +(22,3,ls), +(22,-19,o), +(19,-71,o), +(-20,-100,c) +); +} +); +width = 82; +}, +{ +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +metricLeft = "=-50"; +metricRight = "=50"; +shapes = ( +{ +closed = 1; +nodes = ( +(114,-150,o), +(150,-58,o), +(150,2,cs), +(150,300,l), +(50,200,l), +(50,3,ls), +(50,-35,o), +(40,-128,o), +(-50,-150,c) +); +} +); +width = 200; +}, +{ +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +metricLeft = "=-100"; +metricRight = "=100"; +shapes = ( +{ +closed = 1; +nodes = ( +(100,-200,o), +(200,-137,o), +(200,2,cs), +(200,421,l), +(0,221,l), +(0,3,ls), +(0,-138,o), +(-23,-177,o), +(-100,-200,c) +); +} +); +width = 300; +} +); +metricRight = "=20"; +unicode = 1585; +}, +{ +glyphname = "dotbelow-ar"; +layers = ( +{ +anchors = ( +{ +name = _bottom; +pos = (115,15); +} +); +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +shapes = ( +{ +closed = 1; +nodes = ( +(123,-15,o), +(130,-8,o), +(130,0,cs), +(130,8,o), +(123,15,o), +(115,15,cs), +(107,15,o), +(100,8,o), +(100,0,cs), +(100,-8,o), +(107,-15,o), +(115,-15,cs) +); +} +); +width = 230; +}, +{ +anchors = ( +{ +name = _bottom; +pos = (150,50); +} +); +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +shapes = ( +{ +closed = 1; +nodes = ( +(178,-50,o), +(200,-28,o), +(200,0,cs), +(200,28,o), +(178,50,o), +(150,50,cs), +(122,50,o), +(100,28,o), +(100,0,cs), +(100,-28,o), +(122,-50,o), +(150,-50,cs) +); +} +); +width = 300; +}, +{ +anchors = ( +{ +name = _bottom; +pos = (200,100); +} +); +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +shapes = ( +{ +closed = 1; +nodes = ( +(255,-100,o), +(300,-55,o), +(300,0,cs), +(300,55,o), +(255,100,o), +(200,100,cs), +(145,100,o), +(100,55,o), +(100,0,cs), +(100,-55,o), +(145,-100,o), +(200,-100,cs) +); +} +); +width = 400; +} +); +} +); +instances = ( +{ +axesValues = ( +17 +); +instanceInterpolations = { +"C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = 1; +}; +manualInterpolation = 1; +name = Thin; +weightClass = 100; +}, +{ +axesValues = ( +30 +); +instanceInterpolations = { +"3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.17808; +"C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = 0.82192; +}; +name = "Extra Light"; +weightClass = 200; +}, +{ +axesValues = ( +55 +); +instanceInterpolations = { +"3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.52055; +"C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = 0.47945; +}; +name = Light; +weightClass = 300; +}, +{ +axesValues = ( +90 +); +instanceInterpolations = { +"3E7589AA-8194-470F-8E2F-13C1C581BE24" = 1; +}; +name = Regular; +}, +{ +axesValues = ( +133 +); +instanceInterpolations = { +"3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.66923; +"BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = 0.33077; +}; +name = Medium; +weightClass = 500; +}, +{ +axesValues = ( +179 +); +instanceInterpolations = { +"3E7589AA-8194-470F-8E2F-13C1C581BE24" = 0.31538; +"BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = 0.68462; +}; +name = Bold; +weightClass = 700; +}, +{ +axesValues = ( +220 +); +instanceInterpolations = { +"BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = 1; +}; +name = Black; +weightClass = 900; +} +); +metrics = ( +{ +type = ascender; +}, +{ +type = "cap height"; +}, +{ +filter = "category == \"letter\""; +type = "x-height"; +}, +{ +type = baseline; +}, +{ +type = descender; +} +); +note = "Bla bla"; +stems = ( +{ +horizontal = 1; +name = hStem0; +}, +{ +horizontal = 1; +name = hStem1; +}, +{ +horizontal = 1; +name = hStem2; +}, +{ +name = vStem0; +}, +{ +name = vStem1; +} +); +unitsPerEm = 1000; +userData = { +AsteriskParameters = { +"253E7231-480D-4F8E-8754-50FC8575C08E" = ( +"754", +"30", +7, +51, +"80", +"50" +); +}; +GSDimensionPlugin.Dimensions = { +"3E7589AA-8194-470F-8E2F-13C1C581BE24" = { +HH = 91; +HV = 93; +OH = 91; +OV = 93; +arAlef = 86; +arBar = 92; +nV = 90; +oH = 88; +}; +"BFFFD157-90D3-4B85-B99D-9A2F366F03CA" = { +HH = 215; +HV = 225; +nV = 220; +oH = 210; +}; +"C4872ECA-A3A9-40AB-960A-1DB2202F16DE" = { +HH = 18; +HV = 19; +nV = 17; +oH = 16; +}; +}; +uniTestValue = def; +}; +versionMajor = 1; +versionMinor = 0; +} diff --git a/tests/feature_writers_test.py b/tests/feature_writers_test.py new file mode 100644 index 000000000..bb3eff3da --- /dev/null +++ b/tests/feature_writers_test.py @@ -0,0 +1,32 @@ +from glyphsLib import load_to_ufos +from glyphsLib.featureWriters.markFeatureWriter import ContextualMarkFeatureWriter +from ufo2ft.featureWriters import ast + + +def test_contextual_anchors(datadir): + ufos = load_to_ufos(datadir.join("ContextualAnchors.glyphs")) + + for ufo in ufos: + writer = ContextualMarkFeatureWriter() + feaFile = ast.FeatureFile() + assert str(feaFile) == "" + assert writer.write(ufo, feaFile) + + assert len(feaFile.markClasses) == 1 + assert "MC_bottom" in feaFile.markClasses + + feature = feaFile.statements[-1] + assert feature.name == "mark" + assert len(feature.statements) == 2 + + lookup = feature.statements[1].lookup + + assert str(lookup) == ( + "lookup ContextualMarkDispatch_0 {\n" + " ;\n" + "\n" + " # reh-ar *\n" + " pos reh-ar behDotless-ar.init [dotbelow-ar]' " + "lookup ContextualMark_0; # behDotless-ar.init/*bottom\n" + "} ContextualMarkDispatch_0;\n" + ) From 4b7c17d126f0ba98ad61ecea78a0d2288aec50e3 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 1 Aug 2023 01:42:45 +0300 Subject: [PATCH 09/12] Add another contextual anchors test Also improve how the code is written by skipping lone semicolon when there is no before part. --- .../featureWriters/markFeatureWriter.py | 7 +- tests/data/ContextualAnchors.glyphs | 286 ++++++++++++++++++ tests/feature_writers_test.py | 23 +- 3 files changed, 306 insertions(+), 10 deletions(-) diff --git a/Lib/glyphsLib/featureWriters/markFeatureWriter.py b/Lib/glyphsLib/featureWriters/markFeatureWriter.py index a37a53bf2..43916278c 100644 --- a/Lib/glyphsLib/featureWriters/markFeatureWriter.py +++ b/Lib/glyphsLib/featureWriters/markFeatureWriter.py @@ -196,12 +196,15 @@ def _makeFeatures(self): dispatch_lookups[before] = ast.LookupBlock( "ContextualMarkDispatch_%i" % len(dispatch_lookups.keys()) ) - dispatch_lookups[before].statements.append(ast.Comment(before + ";\n")) + if before: + dispatch_lookups[before].statements.append( + ast.Comment(f"{before};") + ) features["mark"].statements.append( ast.LookupReferenceStatement(dispatch_lookups[before]) ) lkp = dispatch_lookups[before] - lkp.statements.append(ast.Comment("# " + after)) + lkp.statements.append(ast.Comment(f"# {after}")) lookup = ast.LookupBlock(lookupname) for glyph, anchor in glyph_anchor_pair: lookup.statements.append(MarkToBasePos(glyph, [anchor]).asAST()) diff --git a/tests/data/ContextualAnchors.glyphs b/tests/data/ContextualAnchors.glyphs index b2b3c8150..c10ea1a9c 100644 --- a/tests/data/ContextualAnchors.glyphs +++ b/tests/data/ContextualAnchors.glyphs @@ -287,6 +287,10 @@ anchors = ( { name = bottom; pos = (200,-25); +}, +{ +name = top; +pos = (192,103); } ); layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; @@ -316,6 +320,10 @@ anchors = ( { name = bottom; pos = (290,-25); +}, +{ +name = top; +pos = (243,182); } ); layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; @@ -345,6 +353,10 @@ anchors = ( { name = bottom; pos = (380,-25); +}, +{ +name = top; +pos = (265,322); } ); layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; @@ -565,6 +577,13 @@ GPOS_Context = "reh-ar *"; }; }, { +name = "*bottom.twodots"; +pos = (25,-126); +userData = { +GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar *"; +}; +}, +{ name = bottom; pos = (25,-85); } @@ -596,6 +615,13 @@ GPOS_Context = "reh-ar *"; }; }, { +name = "*bottom.twodots"; +pos = (55,-200); +userData = { +GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar *"; +}; +}, +{ name = bottom; pos = (55,-100); } @@ -627,6 +653,13 @@ GPOS_Context = "reh-ar *"; }; }, { +name = "*bottom.twodots"; +pos = (105,-226); +userData = { +GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar *"; +}; +}, +{ name = bottom; pos = (105,-100); } @@ -697,6 +730,63 @@ width = 761; unicode = 1576; }, { +glyphname = "behTwodotsbelowDotabove-ar"; +layers = ( +{ +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +shapes = ( +{ +ref = "behDotless-ar"; +}, +{ +pos = (55,-40); +ref = "twodotshorizontalbelow-ar"; +}, +{ +pos = (77,117); +ref = "dotabove-ar"; +} +); +width = 379; +}, +{ +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +shapes = ( +{ +ref = "behDotless-ar"; +}, +{ +pos = (70,-75); +ref = "twodotshorizontalbelow-ar"; +}, +{ +pos = (93,231); +ref = "dotabove-ar"; +} +); +width = 550; +}, +{ +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +shapes = ( +{ +ref = "behDotless-ar"; +}, +{ +pos = (55,-125); +ref = "twodotshorizontalbelow-ar"; +}, +{ +pos = (65,421); +ref = "dotabove-ar"; +} +); +width = 761; +} +); +unicode = 1876; +}, +{ glyphname = "reh-ar"; layers = ( { @@ -769,6 +859,59 @@ metricRight = "=20"; unicode = 1585; }, { +glyphname = "dotabove-ar"; +layers = ( +{ +anchors = ( +{ +name = _top; +pos = (115,-14); +} +); +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +shapes = ( +{ +alignment = 1; +ref = "dotbelow-ar"; +} +); +width = 230; +}, +{ +anchors = ( +{ +name = _top; +pos = (150,-49); +} +); +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +shapes = ( +{ +alignment = 1; +ref = "dotbelow-ar"; +} +); +width = 300; +}, +{ +anchors = ( +{ +name = _top; +pos = (200,-99); +} +); +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +shapes = ( +{ +alignment = 1; +ref = "dotbelow-ar"; +} +); +width = 400; +} +); +}, +{ glyphname = "dotbelow-ar"; layers = ( { @@ -859,6 +1002,149 @@ nodes = ( width = 400; } ); +}, +{ +glyphname = "twodotshorizontalbelow-ar"; +layers = ( +{ +anchors = ( +{ +name = _bottom; +pos = (145,15); +} +); +layerId = "C4872ECA-A3A9-40AB-960A-1DB2202F16DE"; +shapes = ( +{ +closed = 1; +nodes = ( +(123,-15,o), +(130,-8,o), +(130,0,cs), +(130,8,o), +(123,15,o), +(115,15,cs), +(107,15,o), +(100,8,o), +(100,0,cs), +(100,-8,o), +(107,-15,o), +(115,-15,cs) +); +}, +{ +closed = 1; +nodes = ( +(183,-15,o), +(190,-8,o), +(190,0,cs), +(190,8,o), +(183,15,o), +(175,15,cs), +(167,15,o), +(160,8,o), +(160,0,cs), +(160,-8,o), +(167,-15,o), +(175,-15,cs) +); +} +); +width = 290; +}, +{ +anchors = ( +{ +name = _bottom; +pos = (220,50); +} +); +layerId = "3E7589AA-8194-470F-8E2F-13C1C581BE24"; +shapes = ( +{ +closed = 1; +nodes = ( +(178,-50,o), +(200,-28,o), +(200,0,cs), +(200,28,o), +(178,50,o), +(150,50,cs), +(122,50,o), +(100,28,o), +(100,0,cs), +(100,-28,o), +(122,-50,o), +(150,-50,cs) +); +}, +{ +closed = 1; +nodes = ( +(318,-50,o), +(340,-28,o), +(340,0,cs), +(340,28,o), +(318,50,o), +(290,50,cs), +(262,50,o), +(240,28,o), +(240,0,cs), +(240,-28,o), +(262,-50,o), +(290,-50,cs) +); +} +); +width = 440; +}, +{ +anchors = ( +{ +name = _bottom; +pos = (325,100); +} +); +layerId = "BFFFD157-90D3-4B85-B99D-9A2F366F03CA"; +shapes = ( +{ +closed = 1; +nodes = ( +(255,-100,o), +(300,-55,o), +(300,0,cs), +(300,55,o), +(255,100,o), +(200,100,cs), +(145,100,o), +(100,55,o), +(100,0,cs), +(100,-55,o), +(145,-100,o), +(200,-100,cs) +); +}, +{ +closed = 1; +nodes = ( +(505,-100,o), +(550,-55,o), +(550,0,cs), +(550,55,o), +(505,100,o), +(450,100,cs), +(395,100,o), +(350,55,o), +(350,0,cs), +(350,-55,o), +(395,-100,o), +(450,-100,cs) +); +} +); +width = 650; +} +); } ); instances = ( diff --git a/tests/feature_writers_test.py b/tests/feature_writers_test.py index bb3eff3da..7917a7dd2 100644 --- a/tests/feature_writers_test.py +++ b/tests/feature_writers_test.py @@ -12,21 +12,28 @@ def test_contextual_anchors(datadir): assert str(feaFile) == "" assert writer.write(ufo, feaFile) - assert len(feaFile.markClasses) == 1 + assert len(feaFile.markClasses) == 2 assert "MC_bottom" in feaFile.markClasses feature = feaFile.statements[-1] assert feature.name == "mark" - assert len(feature.statements) == 2 - - lookup = feature.statements[1].lookup + assert len(feature.statements) == 3 + lookup = feature.statements[-2].lookup assert str(lookup) == ( "lookup ContextualMarkDispatch_0 {\n" - " ;\n" - "\n" + " lookupflag UseMarrkFilteringSet [twodotshorizontalbelow];\n" " # reh-ar *\n" - " pos reh-ar behDotless-ar.init [dotbelow-ar]' " - "lookup ContextualMark_0; # behDotless-ar.init/*bottom\n" + " pos reh-ar behDotless-ar.init [dotbelow-ar twodotshorizontalbelow-ar]'" + " lookup ContextualMark_0; # behDotless-ar.init/*bottom.twodots\n" "} ContextualMarkDispatch_0;\n" ) + + lookup = feature.statements[-1].lookup + assert str(lookup) == ( + "lookup ContextualMarkDispatch_1 {\n" + " # reh-ar *\n" + " pos reh-ar behDotless-ar.init [dotbelow-ar twodotshorizontalbelow-ar]'" + " lookup ContextualMark_1; # behDotless-ar.init/*bottom\n" + "} ContextualMarkDispatch_1;\n" + ) From 5d3cdd8ca8b687ac82af266497c840227a312659 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 1 Aug 2023 01:48:34 +0300 Subject: [PATCH 10/12] Test also use of & in contextual anchor context --- tests/data/ContextualAnchors.glyphs | 6 +++--- tests/feature_writers_test.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/data/ContextualAnchors.glyphs b/tests/data/ContextualAnchors.glyphs index c10ea1a9c..045c48332 100644 --- a/tests/data/ContextualAnchors.glyphs +++ b/tests/data/ContextualAnchors.glyphs @@ -580,7 +580,7 @@ GPOS_Context = "reh-ar *"; name = "*bottom.twodots"; pos = (25,-126); userData = { -GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar *"; +GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar * behDotess-ar.medi &"; }; }, { @@ -618,7 +618,7 @@ GPOS_Context = "reh-ar *"; name = "*bottom.twodots"; pos = (55,-200); userData = { -GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar *"; +GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar * behDotess-ar.medi &"; }; }, { @@ -656,7 +656,7 @@ GPOS_Context = "reh-ar *"; name = "*bottom.twodots"; pos = (105,-226); userData = { -GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar *"; +GPOS_Context = "lookupflag UseMarrkFilteringSet [twodotshorizontalbelow]; reh-ar * behDotess-ar.medi &"; }; }, { diff --git a/tests/feature_writers_test.py b/tests/feature_writers_test.py index 7917a7dd2..a2bef515d 100644 --- a/tests/feature_writers_test.py +++ b/tests/feature_writers_test.py @@ -23,8 +23,9 @@ def test_contextual_anchors(datadir): assert str(lookup) == ( "lookup ContextualMarkDispatch_0 {\n" " lookupflag UseMarrkFilteringSet [twodotshorizontalbelow];\n" - " # reh-ar *\n" - " pos reh-ar behDotless-ar.init [dotbelow-ar twodotshorizontalbelow-ar]'" + " # reh-ar * behDotess-ar.medi &\n" + " pos reh-ar behDotless-ar.init behDotess-ar.medi" + " [dotbelow-ar twodotshorizontalbelow-ar]'" " lookup ContextualMark_0; # behDotless-ar.init/*bottom.twodots\n" "} ContextualMarkDispatch_0;\n" ) From 66509dcf0e0aab5735bbf10d1679d6cdf6e6ff6f Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 1 Aug 2023 14:12:30 +0300 Subject: [PATCH 11/12] Fix feature writers list Add missing GdefFeatureWriter, and remove unneeded mode option from KernFeatureWriter since we now always add insertion marker comment to always generate kern feature even if a manual feature exists. --- Lib/glyphsLib/builder/constants.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Lib/glyphsLib/builder/constants.py b/Lib/glyphsLib/builder/constants.py index 16640b3f6..f41d5b1de 100644 --- a/Lib/glyphsLib/builder/constants.py +++ b/Lib/glyphsLib/builder/constants.py @@ -204,19 +204,15 @@ UFO2FT_COLOR_LAYERS_KEY = "com.github.googlei18n.ufo2ft.colorLayers" UFO2FT_META_TABLE_KEY = PUBLIC_PREFIX + "openTypeMeta" -# ufo2ft KernFeatureWriter default to "skip" mode (i.e. do not write features -# if they are already present), while Glyphs.app always adds its automatic -# kerning to any user written kern lookups. So we need to pass custom "append" -# mode for the ufo2ft KernFeatureWriter whenever the GSFont contain a non-automatic -# 'kern' feature. -# See https://glyphsapp.com/tutorials/contextual-kerning + DEFAULT_FEATURE_WRITERS = [ - {"class": "KernFeatureWriter", "options": {"mode": "append"}}, + {"class": "KernFeatureWriter"}, { "module": "glyphsLib.featureWriters.markFeatureWriter", "class": "ContextualMarkFeatureWriter", "options": {"mode": "skip"}, }, + {"class": "GdefFeatureWriter"}, {"class": "CursFeatureWriter"}, ] From 840f4916a2e4f9186a0fece99b493dfd4b2eade8 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 1 Aug 2023 14:24:19 +0300 Subject: [PATCH 12/12] Remove default option from MarkFeatureWriter too --- Lib/glyphsLib/builder/constants.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/glyphsLib/builder/constants.py b/Lib/glyphsLib/builder/constants.py index f41d5b1de..ee41cef03 100644 --- a/Lib/glyphsLib/builder/constants.py +++ b/Lib/glyphsLib/builder/constants.py @@ -210,7 +210,6 @@ { "module": "glyphsLib.featureWriters.markFeatureWriter", "class": "ContextualMarkFeatureWriter", - "options": {"mode": "skip"}, }, {"class": "GdefFeatureWriter"}, {"class": "CursFeatureWriter"},