diff --git a/Lib/glyphsLib/builder/constants.py b/Lib/glyphsLib/builder/constants.py index 49a90c406..b2b300ac2 100644 --- a/Lib/glyphsLib/builder/constants.py +++ b/Lib/glyphsLib/builder/constants.py @@ -208,10 +208,7 @@ DEFAULT_FEATURE_WRITERS = [ {"class": "CursFeatureWriter"}, {"class": "KernFeatureWriter"}, - { - "module": "glyphsLib.featureWriters.markFeatureWriter", - "class": "ContextualMarkFeatureWriter", - }, + {"class": "MarkFeatureWriter"}, {"class": "GdefFeatureWriter"}, ] diff --git a/Lib/glyphsLib/featureWriters/markFeatureWriter.py b/Lib/glyphsLib/featureWriters/markFeatureWriter.py index 60a0d56a1..45d506c8e 100644 --- a/Lib/glyphsLib/featureWriters/markFeatureWriter.py +++ b/Lib/glyphsLib/featureWriters/markFeatureWriter.py @@ -1,278 +1,5 @@ -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, - MarkFeatureWriter, - MarkToBasePos, - NamedAnchor, -) -from ufo2ft.util import quantize - - -class ContextuallyAwareNamedAnchor(NamedAnchor): - __slots__ = ( - "name", - "x", - "y", - "isMark", - "key", - "number", - "markClass", - "isContextual", - "isIgnorable", - "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 - - isIgnorable = not key[0].isalpha() - - return isMark, key, number, isContextual, isIgnorable - - def __init__(self, name, x, y, markClass=None, libData=None): - self.name = name - self.x = x - self.y = y - isMark, key, number, isContextual, isIgnorable = 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.isIgnorable = isIgnorable - self.libData = libData +from ufo2ft.featureWriters.markFeatureWriter import MarkFeatureWriter class ContextualMarkFeatureWriter(MarkFeatureWriter): - NamedAnchor = ContextuallyAwareNamedAnchor - - def _getAnchor(self, glyphName, anchorName, anchor=None): - # the variable FEA aware method is defined with ufo2ft v3; make sure we don't - # fail but continue to work unchanged with older ufo2ft MarkFeatureWriter API. - try: - getter = super()._getAnchor - except AttributeError: - x = anchor.x - y = anchor.y - if hasattr(self.options, "quantization"): - x = quantize(x, self.options.quantization) - y = quantize(y, self.options.quantization) - return x, y - else: - return getter(glyphName, anchorName, anchor=anchor) - - 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, y = self._getAnchor(glyphName, anchorName, anchor=anchor) - 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 - if a.isIgnorable: - continue - anchorDict[anchorName] = a - if anchorDict: - result[glyphName] = list(anchorDict.values()) - 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 sorted(self.context.anchorLists.items()): - if glyphName in markGlyphNames: - continue - for anchor in anchors: - if not anchor.isContextual: - continue - anchor_context = anchor.libData["GPOS_Context"].strip() - 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 = {} - # 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: - 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 = "" - after = after.strip() - if before not in dispatch_lookups: - dispatch_lookups[before] = ast.LookupBlock( - "ContextualMarkDispatch_%i" % len(dispatch_lookups.keys()) - ) - 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(f"# {after}")) - lookup = ast.LookupBlock(lookupname) - for glyph, anchor in glyph_anchor_pair: - lookup.statements.append(MarkToBasePos(glyph, [anchor]).asAST()) - lookups.append(lookup) - - # Insert mark glyph names after base glyph names if not specified otherwise. - if "&" not in after: - after = after.replace("*", "* &") - - # Group base glyphs by anchor - glyphs = {} - for glyph, anchor in glyph_anchor_pair: - glyphs.setdefault(anchor.key, [anchor, []])[1].append(glyph) - - for anchor, bases in glyphs.values(): - bases = " ".join(bases) - marks = ast.GlyphClass( - self.context.markClasses[anchor.key].glyphs.keys() - ).asFea() - - # Replace * with base glyph names - contextual = after.replace("*", f"[{bases}]") - - # Replace & with mark glyph names - contextual = contextual.replace("&", f"{marks}' lookup {lookupname}") - lkp.statements.append(ast.Comment(f"pos {contextual}; # {anchor.name}")) - - 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 + pass diff --git a/requirements-dev.in b/requirements-dev.in index e1be9f06a..a00e40e68 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -10,5 +10,5 @@ fonttools[ufo,unicode] >= 4.38.0 # extras ufoNormalizer>=0.3.2 defcon>=0.6.0 -ufo2ft>=3.0.0b1 +ufo2ft>=3.3.0 skia-pathops diff --git a/requirements-dev.txt b/requirements-dev.txt index 8a171c0d5..3a00a9143 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,9 +6,9 @@ # appdirs==1.4.4 # via fs -attrs==23.2.0 +attrs==24.2.0 # via flake8-bugbear -black==24.3.0 +black==24.8.0 # via -r requirements-dev.in booleanoperations==0.9.0 # via ufo2ft @@ -16,71 +16,74 @@ cffsubr==0.3.0 # via ufo2ft click==8.1.7 # via black -coverage==7.4.0 +coverage==7.6.1 # via -r requirements-dev.in defcon==0.10.3 # via -r requirements-dev.in -execnet==2.0.2 +execnet==2.1.1 # via pytest-xdist -flake8==7.0.0 +flake8==7.1.1 # via # -r requirements-dev.in # flake8-bugbear -flake8-bugbear==24.1.17 +flake8-bugbear==24.8.19 # via -r requirements-dev.in -fonttools[ufo,unicode]==4.47.2 +fontmath==0.9.4 + # via ufo2ft +fonttools[ufo,unicode]==4.53.1 # via # -r requirements-dev.in # booleanoperations # cffsubr # defcon + # fontmath # ufo2ft fs==2.4.16 # via fonttools iniconfig==2.0.0 # via pytest -lxml==5.1.0 +lxml==5.3.0 # via xmldiff mccabe==0.7.0 # via flake8 mypy-extensions==1.0.0 # via black -packaging==23.2 +packaging==24.1 # via # black # pytest pathspec==0.12.1 # via black -platformdirs==4.1.0 +platformdirs==4.3.6 # via black -pluggy==1.4.0 +pluggy==1.5.0 # via pytest pyclipper==1.3.0.post5 # via booleanoperations -pycodestyle==2.11.1 +pycodestyle==2.12.1 # via flake8 pyflakes==3.2.0 # via flake8 -pytest==7.4.4 +pytest==8.3.3 # via # -r requirements-dev.in # pytest-randomly # pytest-xdist pytest-randomly==3.15.0 # via -r requirements-dev.in -pytest-xdist==3.5.0 +pytest-xdist==3.6.1 # via -r requirements-dev.in six==1.16.0 # via fs skia-pathops==0.8.0.post1 # via -r requirements-dev.in -ufo2ft==3.0.0b1 +ufo2ft==3.3.0 # via -r requirements-dev.in -ufonormalizer==0.6.1 +ufonormalizer==0.6.2 # via -r requirements-dev.in unicodedata2==15.1.0 # via fonttools -xmldiff==2.6.3 +xmldiff==2.7.0 # via -r requirements-dev.in # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements.txt b/requirements.txt index 6ac16c9e0..b83fe4038 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,9 +6,9 @@ # appdirs==1.4.4 # via fs -attrs==23.2.0 +attrs==24.2.0 # via ufolib2 -fonttools[ufo,unicode]==4.47.2 +fonttools[ufo,unicode]==4.53.1 # via # glyphsLib (setup.cfg) # ufolib2 diff --git a/tests/feature_writers_test.py b/tests/feature_writers_test.py index d7cd2b3a1..94ee8d525 100644 --- a/tests/feature_writers_test.py +++ b/tests/feature_writers_test.py @@ -37,9 +37,7 @@ def test_contextual_anchors(datadir): "lookup ContextualMarkDispatch_0 {\n" " lookupflag UseMarrkFilteringSet [twodotshorizontalbelow];\n" " # reh-ar * behDotess-ar.medi &\n" - " pos reh-ar [behDotless-ar.init] behDotess-ar.medi" - " [dotbelow-ar twodotsverticalbelow-ar twodotshorizontalbelow-ar]'" - " lookup ContextualMark_0; # *bottom.twodots\n" + " pos reh-ar [behDotless-ar.init] behDotess-ar.medi @MC_bottom' lookup ContextualMark_0;\n" "} ContextualMarkDispatch_0;\n" ) @@ -48,9 +46,7 @@ def test_contextual_anchors(datadir): "lookup ContextualMarkDispatch_1 {\n" " lookupflag UseMarrkFilteringSet [twodotsverticalbelow];\n" " # reh-ar *\n" - " pos reh-ar [behDotless-ar.init behDotless-ar.init.alt]" - " [dotbelow-ar twodotsverticalbelow-ar twodotshorizontalbelow-ar]'" - " lookup ContextualMark_1; # *bottom.vtwodots\n" + " pos reh-ar [behDotless-ar.init behDotless-ar.init.alt] @MC_bottom' lookup ContextualMark_1;\n" "} ContextualMarkDispatch_1;\n" ) @@ -58,9 +54,7 @@ def test_contextual_anchors(datadir): assert str(lookup) == ( "lookup ContextualMarkDispatch_2 {\n" " # reh-ar *\n" - " pos reh-ar [behDotless-ar.init] " - "[dotbelow-ar twodotsverticalbelow-ar twodotshorizontalbelow-ar]'" - " lookup ContextualMark_2; # *bottom\n" + " pos reh-ar [behDotless-ar.init] @MC_bottom' lookup ContextualMark_2;\n" "} ContextualMarkDispatch_2;\n" )