| 
							
								 | 
							
							"""\
 | 
						
						
						
						
							 | 
							
								 | 
							
							MS VOLT ``.vtp`` to AFDKO ``.fea`` OpenType Layout converter.
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							Usage
 | 
						
						
						
						
							 | 
							
								 | 
							
							-----
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							To convert a VTP project file:
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							.. code-block:: sh
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    $ fonttools voltLib.voltToFea input.vtp output.fea
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							It is also possible convert font files with `TSIV` table (as saved from Volt),
 | 
						
						
						
						
							 | 
							
								 | 
							
							in this case the glyph names used in the Volt project will be mapped to the
 | 
						
						
						
						
							 | 
							
								 | 
							
							actual glyph names in the font files when written to the feature file:
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							.. code-block:: sh
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    $ fonttools voltLib.voltToFea input.ttf output.fea
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							The ``--quiet`` option can be used to suppress warnings.
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							The ``--traceback`` can be used to get Python traceback in case of exceptions,
 | 
						
						
						
						
							 | 
							
								 | 
							
							instead of suppressing the traceback.
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							Limitations
 | 
						
						
						
						
							 | 
							
								 | 
							
							-----------
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							* Not all VOLT features are supported, the script will error if it it
 | 
						
						
						
						
							 | 
							
								 | 
							
							  encounters something it does not understand. Please report an issue if this
 | 
						
						
						
						
							 | 
							
								 | 
							
							  happens.
 | 
						
						
						
						
							 | 
							
								 | 
							
							* AFDKO feature file syntax for mark positioning is awkward and does not allow
 | 
						
						
						
						
							 | 
							
								 | 
							
							  setting the mark coverage. It also defines mark anchors globally, as a result
 | 
						
						
						
						
							 | 
							
								 | 
							
							  some mark positioning lookups might cover many marks than what was in the VOLT
 | 
						
						
						
						
							 | 
							
								 | 
							
							  file. This should not be an issue in practice, but if it is then the only way
 | 
						
						
						
						
							 | 
							
								 | 
							
							  is to modify the VOLT file or the generated feature file manually to use unique
 | 
						
						
						
						
							 | 
							
								 | 
							
							  mark anchors for each lookup.
 | 
						
						
						
						
							 | 
							
								 | 
							
							* VOLT allows subtable breaks in any lookup type, but AFDKO feature file
 | 
						
						
						
						
							 | 
							
								 | 
							
							  implementations vary in their support; currently AFDKO’s makeOTF supports
 | 
						
						
						
						
							 | 
							
								 | 
							
							  subtable breaks in pair positioning lookups only, while FontTools’ feaLib
 | 
						
						
						
						
							 | 
							
								 | 
							
							  support it for most substitution lookups and only some positioning lookups.
 | 
						
						
						
						
							 | 
							
								 | 
							
							"""
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							import logging
 | 
						
						
						
						
							 | 
							
								 | 
							
							import re
 | 
						
						
						
						
							 | 
							
								 | 
							
							from io import StringIO
 | 
						
						
						
						
							 | 
							
								 | 
							
							from graphlib import TopologicalSorter
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							from fontTools.feaLib import ast
 | 
						
						
						
						
							 | 
							
								 | 
							
							from fontTools.ttLib import TTFont, TTLibError
 | 
						
						
						
						
							 | 
							
								 | 
							
							from fontTools.voltLib import ast as VAst
 | 
						
						
						
						
							 | 
							
								 | 
							
							from fontTools.voltLib.parser import Parser as VoltParser
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							log = logging.getLogger("fontTools.voltLib.voltToFea")
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							TABLES = ["GDEF", "GSUB", "GPOS"]
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							def _flatten_group(group):
 | 
						
						
						
						
							 | 
							
								 | 
							
							    ret = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							    if isinstance(group, (tuple, list)):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for item in group:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            ret.extend(_flatten_group(item))
 | 
						
						
						
						
							 | 
							
								 | 
							
							    elif hasattr(group, "enum"):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        ret.extend(_flatten_group(group.enum))
 | 
						
						
						
						
							 | 
							
								 | 
							
							    else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							        ret.append(group)
 | 
						
						
						
						
							 | 
							
								 | 
							
							    return ret
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							# Topologically sort of group definitions to ensure that all groups are defined
 | 
						
						
						
						
							 | 
							
								 | 
							
							# before they are referenced. This is necessary because FEA requires it but
 | 
						
						
						
						
							 | 
							
								 | 
							
							# VOLT does not, see below.
 | 
						
						
						
						
							 | 
							
								 | 
							
							def sort_groups(groups):
 | 
						
						
						
						
							 | 
							
								 | 
							
							    group_map = {group.name.lower(): group for group in groups}
 | 
						
						
						
						
							 | 
							
								 | 
							
							    graph = {
 | 
						
						
						
						
							 | 
							
								 | 
							
							        group.name.lower(): [
 | 
						
						
						
						
							 | 
							
								 | 
							
							            x.group.lower()
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for x in _flatten_group(group)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if isinstance(x, VAst.GroupName)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        ]
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for group in groups
 | 
						
						
						
						
							 | 
							
								 | 
							
							    }
 | 
						
						
						
						
							 | 
							
								 | 
							
							    sorter = TopologicalSorter(graph)
 | 
						
						
						
						
							 | 
							
								 | 
							
							    return [group_map[name] for name in sorter.static_order()]
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							class Lookup(ast.LookupBlock):
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def __init__(self, name, use_extension=False, location=None):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        super().__init__(name, use_extension, location)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self.chained = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							class VoltToFea:
 | 
						
						
						
						
							 | 
							
								 | 
							
							    _NOT_LOOKUP_NAME_RE = re.compile(r"[^A-Za-z_0-9.]")
 | 
						
						
						
						
							 | 
							
								 | 
							
							    _NOT_CLASS_NAME_RE = re.compile(r"[^A-Za-z_0-9.\-]")
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def __init__(self, file_or_path, font=None):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if isinstance(file_or_path, VAst.VoltFile):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._doc, self._file_or_path = file_or_path, None
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._doc, self._file_or_path = None, file_or_path
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._font = font
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._glyph_map = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._glyph_order = None
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._gdef = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._glyphclasses = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._features = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._lookups = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._marks = set()
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._ligatures = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._markclasses = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._anchors = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._settings = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._lookup_names = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._class_names = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _lookupName(self, name):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if name not in self._lookup_names:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            res = self._NOT_LOOKUP_NAME_RE.sub("_", name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            while res in self._lookup_names.values():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                res += "_"
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._lookup_names[name] = res
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return self._lookup_names[name]
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _className(self, name):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if name not in self._class_names:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            res = self._NOT_CLASS_NAME_RE.sub("_", name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            while res in self._class_names.values():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                res += "_"
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._class_names[name] = res
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return self._class_names[name]
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _collectStatements(self, doc, tables, ignore_unsupported_settings=False):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # Collect glyph difinitions first, as we need them to map VOLT glyph names to font glyph name.
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for statement in doc.statements:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if isinstance(statement, VAst.GlyphDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._glyphDefinition(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # Collect and sort group definitions first, to make sure a group
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # definition that references other groups comes after them since VOLT
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # does not enforce such ordering, and feature file require it.
 | 
						
						
						
						
							 | 
							
								 | 
							
							        groups = [s for s in doc.statements if isinstance(s, VAst.GroupDefinition)]
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for group in sort_groups(groups):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._groupDefinition(group)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for statement in doc.statements:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if isinstance(statement, VAst.AnchorDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if "GPOS" in tables:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    self._anchorDefinition(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(statement, VAst.SettingDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._settingDefinition(statement, ignore_unsupported_settings)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(statement, (VAst.GlyphDefinition, VAst.GroupDefinition)):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                pass  # Handled above
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(statement, VAst.ScriptDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._scriptDefinition(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif not isinstance(statement, VAst.LookupDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                raise NotImplementedError(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # Lookup definitions need to be handled last as they reference glyph
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # and mark classes that might be defined after them.
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for statement in doc.statements:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if isinstance(statement, VAst.LookupDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if statement.pos and "GPOS" not in tables:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    continue
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if statement.sub and "GSUB" not in tables:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    continue
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._lookupDefinition(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _buildFeatureFile(self, tables):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        doc = ast.FeatureFile()
 | 
						
						
						
						
							 | 
							
								 | 
							
							        statements = doc.statements
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if self._glyphclasses:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(ast.Comment("# Glyph classes"))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.extend(self._glyphclasses.values())
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if self._markclasses:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(ast.Comment("\n# Mark classes"))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.extend(c[1] for c in sorted(self._markclasses.items()))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if self._lookups:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(ast.Comment("\n# Lookups"))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for lookup in self._lookups.values():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.extend(lookup.chained)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(lookup)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # Prune features
 | 
						
						
						
						
							 | 
							
								 | 
							
							        features = self._features.copy()
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for feature_tag in features:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            scripts = features[feature_tag]
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for script_tag in scripts:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                langs = scripts[script_tag]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for language_tag in langs:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    langs[language_tag] = [
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        l for l in langs[language_tag] if l.lower() in self._lookups
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    ]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                scripts[script_tag] = {t: l for t, l in langs.items() if l}
 | 
						
						
						
						
							 | 
							
								 | 
							
							            features[feature_tag] = {t: s for t, s in scripts.items() if s}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        features = {t: f for t, f in features.items() if f}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if features:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(ast.Comment("# Features"))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for feature_tag, scripts in features.items():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                feature = ast.FeatureBlock(feature_tag)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                script_tags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if feature_tag == "aalt" and len(script_tags) > 1:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    log.warning(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        "FEA syntax does not allow script statements in 'aalt' feature, "
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        "so only lookups from the first script will be included."
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    script_tags = script_tags[:1]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for script_tag in script_tags:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    if feature_tag != "aalt":
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        feature.statements.append(ast.ScriptStatement(script_tag))
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    language_tags = sorted(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        scripts[script_tag],
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        key=lambda k: 0 if k == "dflt" else 1,
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    if feature_tag == "aalt" and len(language_tags) > 1:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        log.warning(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            "FEA syntax does not allow language statements in 'aalt' feature, "
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            "so only lookups from the first language will be included."
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        language_tags = language_tags[:1]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    for language_tag in language_tags:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        if feature_tag != "aalt":
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            include_default = True if language_tag == "dflt" else False
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            feature.statements.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                                ast.LanguageStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                                    language_tag.ljust(4),
 | 
						
						
						
						
							 | 
							
								 | 
							
							                                    include_default=include_default,
 | 
						
						
						
						
							 | 
							
								 | 
							
							                                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        for name in scripts[script_tag][language_tag]:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            lookup = self._lookups[name.lower()]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            lookupref = ast.LookupReferenceStatement(lookup)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            feature.statements.append(lookupref)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(feature)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if self._gdef and "GDEF" in tables:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            classes = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for name in ("BASE", "MARK", "LIGATURE", "COMPONENT"):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if name in self._gdef:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    classname = "GDEF_" + name.lower()
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    glyphclass = ast.GlyphClassDefinition(classname, self._gdef[name])
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    statements.append(glyphclass)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    classes.append(ast.GlyphClassName(glyphclass))
 | 
						
						
						
						
							 | 
							
								 | 
							
							                else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    classes.append(None)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							            gdef = ast.TableBlock("GDEF")
 | 
						
						
						
						
							 | 
							
								 | 
							
							            gdef.statements.append(ast.GlyphClassDefStatement(*classes))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(gdef)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return doc
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def convert(self, tables=None, ignore_unsupported_settings=False):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if self._doc is None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._doc = VoltParser(self._file_or_path).parse()
 | 
						
						
						
						
							 | 
							
								 | 
							
							        doc = self._doc
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if tables is None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            tables = TABLES
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if self._font is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._glyph_order = self._font.getGlyphOrder()
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._collectStatements(doc, tables, ignore_unsupported_settings)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        fea = self._buildFeatureFile(tables)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return fea.asFea()
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _glyphName(self, glyph):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        try:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            name = glyph.glyph
 | 
						
						
						
						
							 | 
							
								 | 
							
							        except AttributeError:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            name = glyph
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return ast.GlyphName(self._glyph_map.get(name, name))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _groupName(self, group):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        try:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            name = group.group
 | 
						
						
						
						
							 | 
							
								 | 
							
							        except AttributeError:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            name = group
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return ast.GlyphClassName(self._glyphclasses[name.lower()])
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _glyphSet(self, item):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return [
 | 
						
						
						
						
							 | 
							
								 | 
							
							            (self._glyphName(x) if isinstance(x, (str, VAst.GlyphName)) else x)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for x in item.glyphSet()
 | 
						
						
						
						
							 | 
							
								 | 
							
							        ]
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _coverage(self, coverage, flatten=False):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        items = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for item in coverage:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if isinstance(item, VAst.GlyphName):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                items.append(self._glyphName(item))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(item, VAst.GroupName):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                items.append(self._groupName(item))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(item, VAst.Enum):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                item = self._coverage(item.enum, flatten=True)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if flatten:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    items.extend(item)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    items.append(ast.GlyphClass(item))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(item, VAst.Range):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                item = self._glyphSet(item)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if flatten:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    items.extend(item)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    items.append(ast.GlyphClass(item))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                raise NotImplementedError(item)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return items
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _context(self, context):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        out = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for item in context:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            coverage = self._coverage(item, flatten=True)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if len(coverage) > 1:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                coverage = ast.GlyphClass(coverage)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                coverage = coverage[0]
 | 
						
						
						
						
							 | 
							
								 | 
							
							            out.append(coverage)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return out
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _groupDefinition(self, group):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        name = self._className(group.name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        glyphs = self._coverage(group.enum.enum, flatten=True)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        glyphclass = ast.GlyphClass(glyphs)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        classdef = ast.GlyphClassDefinition(name, glyphclass)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._glyphclasses[group.name.lower()] = classdef
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _glyphDefinition(self, glyph):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        try:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._glyph_map[glyph.name] = self._glyph_order[glyph.id]
 | 
						
						
						
						
							 | 
							
								 | 
							
							        except TypeError:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            pass
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if glyph.type in ("BASE", "MARK", "LIGATURE", "COMPONENT"):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if glyph.type not in self._gdef:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._gdef[glyph.type] = ast.GlyphClass()
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._gdef[glyph.type].glyphs.append(self._glyphName(glyph.name))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if glyph.type == "MARK":
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._marks.add(glyph.name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif glyph.type == "LIGATURE":
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._ligatures[glyph.name] = glyph.components
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _scriptDefinition(self, script):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        stag = script.tag
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for lang in script.langs:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            ltag = lang.tag
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for feature in lang.features:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                lookups = {l.split("\\")[0]: True for l in feature.lookups}
 | 
						
						
						
						
							 | 
							
								 | 
							
							                ftag = feature.tag
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if ftag not in self._features:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    self._features[ftag] = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if stag not in self._features[ftag]:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    self._features[ftag][stag] = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert ltag not in self._features[ftag][stag]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._features[ftag][stag][ltag] = lookups.keys()
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _settingDefinition(self, setting, ignore_unsupported=False):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if setting.name.startswith("COMPILER_"):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._settings[setting.name] = setting.value
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif not ignore_unsupported:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            log.warning(f"Unsupported setting ignored: {setting.name}")
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _adjustment(self, adjustment):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        adv, dx, dy, adv_adjust_by, dx_adjust_by, dy_adjust_by = adjustment
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        adv_device = adv_adjust_by and adv_adjust_by.items() or None
 | 
						
						
						
						
							 | 
							
								 | 
							
							        dx_device = dx_adjust_by and dx_adjust_by.items() or None
 | 
						
						
						
						
							 | 
							
								 | 
							
							        dy_device = dy_adjust_by and dy_adjust_by.items() or None
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return ast.ValueRecord(
 | 
						
						
						
						
							 | 
							
								 | 
							
							            xPlacement=dx,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            yPlacement=dy,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            xAdvance=adv,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            xPlaDevice=dx_device,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            yPlaDevice=dy_device,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            xAdvDevice=adv_device,
 | 
						
						
						
						
							 | 
							
								 | 
							
							        )
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _anchor(self, adjustment):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        adv, dx, dy, adv_adjust_by, dx_adjust_by, dy_adjust_by = adjustment
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        assert not adv_adjust_by
 | 
						
						
						
						
							 | 
							
								 | 
							
							        dx_device = dx_adjust_by and dx_adjust_by.items() or None
 | 
						
						
						
						
							 | 
							
								 | 
							
							        dy_device = dy_adjust_by and dy_adjust_by.items() or None
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return ast.Anchor(
 | 
						
						
						
						
							 | 
							
								 | 
							
							            dx or 0,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            dy or 0,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            xDeviceTable=dx_device or None,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            yDeviceTable=dy_device or None,
 | 
						
						
						
						
							 | 
							
								 | 
							
							        )
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _anchorDefinition(self, anchordef):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        anchorname = anchordef.name
 | 
						
						
						
						
							 | 
							
								 | 
							
							        glyphname = anchordef.glyph_name
 | 
						
						
						
						
							 | 
							
								 | 
							
							        anchor = self._anchor(anchordef.pos)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if glyphname not in self._anchors:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._anchors[glyphname] = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if anchorname.startswith("MARK_"):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            anchorname = anchorname[:5] + anchorname[5:].lower()
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            anchorname = anchorname.lower()
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if anchorname not in self._anchors[glyphname]:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._anchors[glyphname][anchorname] = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							        self._anchors[glyphname][anchorname][anchordef.component] = anchor
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _gposLookup(self, lookup, fealookup):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        statements = fealookup.statements
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        pos = lookup.pos
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if isinstance(pos, VAst.PositionAdjustPairDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                coverage_1 = pos.coverages_1[idx1 - 1]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                coverage_2 = pos.coverages_2[idx2 - 1]
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # If not both are groups, use “enum pos” otherwise makeotf will
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # fail.
 | 
						
						
						
						
							 | 
							
								 | 
							
							                enumerated = False
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for item in coverage_1 + coverage_2:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    if not isinstance(item, VAst.GroupName):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        enumerated = True
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs1 = self._coverage(coverage_1)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs2 = self._coverage(coverage_2)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                record1 = self._adjustment(pos1)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                record2 = self._adjustment(pos2)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(glyphs1) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(glyphs2) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    ast.PairPosStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        glyphs1[0], record1, glyphs2[0], record2, enumerated=enumerated
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif isinstance(pos, VAst.PositionAdjustSingleDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for a, b in pos.adjust_single:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs = self._coverage(a)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                record = self._adjustment(b)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(glyphs) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    ast.SinglePosStatement([(glyphs[0], record)], [], [], False)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif isinstance(pos, VAst.PositionAttachDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            anchors = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							            allmarks = set()
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for coverage, anchorname in pos.coverage_to:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # In feature files mark classes are global, but in VOLT they
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # are defined per-lookup. If we output mark class definitions
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # for all marks that use a given anchor, we might end up with a
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # mark used in two different classes in the same lookup, which
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # is causes feature file compilation error.
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # At the expense of uglier feature code, we make the mark class
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # name by appending the current lookup name not the anchor
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # name, and output mark class definitions only for marks used
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # in this lookup.
 | 
						
						
						
						
							 | 
							
								 | 
							
							                classname = self._className(f"{anchorname}.{lookup.name}")
 | 
						
						
						
						
							 | 
							
								 | 
							
							                markclass = ast.MarkClass(classname)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # Anchor names are case-insensitive in VOLT
 | 
						
						
						
						
							 | 
							
								 | 
							
							                anchorname = anchorname.lower()
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # We might still end in marks used in two different anchor
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # classes, so we filter out already used marks.
 | 
						
						
						
						
							 | 
							
								 | 
							
							                marks = set()
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for mark in coverage:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    marks.update(mark.glyphSet())
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if not marks.isdisjoint(allmarks):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    marks.difference_update(allmarks)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    if not marks:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        continue
 | 
						
						
						
						
							 | 
							
								 | 
							
							                allmarks.update(marks)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for glyphname in marks:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    glyph = self._glyphName(glyphname)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    anchor = self._anchors[glyphname][f"MARK_{anchorname}"][1]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    markdef = ast.MarkClassDefinition(markclass, anchor, glyph)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    self._markclasses[(glyphname, classname)] = markdef
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for base in pos.coverage:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    for name in base.glyphSet():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        if name not in anchors:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            anchors[name] = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        if (anchorname, classname) not in anchors[name]:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            anchors[name].append((anchorname, classname))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							            is_ligature = all(n in self._ligatures for n in anchors)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            is_mark = all(n in self._marks for n in anchors)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for name in anchors:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                components = 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if is_ligature:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    components = self._ligatures[name]
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                marks = [[] for _ in range(components)]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for mark, classname in anchors[name]:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    markclass = ast.MarkClass(classname)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    for component in range(1, components + 1):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        if component in self._anchors[name][mark]:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            anchor = self._anchors[name][mark][component]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            marks[component - 1].append((anchor, markclass))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                base = self._glyphName(name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if is_mark:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    mark = ast.MarkMarkPosStatement(base, marks[0])
 | 
						
						
						
						
							 | 
							
								 | 
							
							                elif is_ligature:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    mark = ast.MarkLigPosStatement(base, marks)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    mark = ast.MarkBasePosStatement(base, marks[0])
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(mark)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif isinstance(pos, VAst.PositionAttachCursiveDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # Collect enter and exit glyphs
 | 
						
						
						
						
							 | 
							
								 | 
							
							            enter_coverage = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for coverage in pos.coverages_enter:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for base in coverage:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    for name in base.glyphSet():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        enter_coverage.append(name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            exit_coverage = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for coverage in pos.coverages_exit:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for base in coverage:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    for name in base.glyphSet():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        exit_coverage.append(name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # Write enter anchors, also check if the glyph has exit anchor and
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # write it, too.
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for name in enter_coverage:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyph = self._glyphName(name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                entry = self._anchors[name]["entry"][1]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                exit = None
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if name in exit_coverage:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    exit = self._anchors[name]["exit"][1]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    exit_coverage.pop(exit_coverage.index(name))
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(ast.CursivePosStatement(glyph, entry, exit))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # Write any remaining exit anchors.
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for name in exit_coverage:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyph = self._glyphName(name)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                exit = self._anchors[name]["exit"][1]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(ast.CursivePosStatement(glyph, None, exit))
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            raise NotImplementedError(pos)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _gposContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        statements = fealookup.statements
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        pos = lookup.pos
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if isinstance(pos, VAst.PositionAdjustPairDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs1 = self._coverage(pos.coverages_1[idx1 - 1])
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs2 = self._coverage(pos.coverages_2[idx2 - 1])
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(glyphs1) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(glyphs2) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs = (glyphs1[0], glyphs2[0])
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if ignore:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
 | 
						
						
						
						
							 | 
							
								 | 
							
							                else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    statement = ast.ChainContextPosStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        prefix, glyphs, suffix, [chained, chained]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif isinstance(pos, VAst.PositionAdjustSingleDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            glyphs = [ast.GlyphClass()]
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for a, _ in pos.adjust_single:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs[0].extend(self._coverage(a, flatten=True))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if ignore:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
 | 
						
						
						
						
							 | 
							
								 | 
							
							            else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statement = ast.ChainContextPosStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    prefix, glyphs, suffix, [chained]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif isinstance(pos, VAst.PositionAttachDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            glyphs = [ast.GlyphClass()]
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for coverage, _ in pos.coverage_to:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs[0].extend(self._coverage(coverage, flatten=True))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if ignore:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
 | 
						
						
						
						
							 | 
							
								 | 
							
							            else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statement = ast.ChainContextPosStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    prefix, glyphs, suffix, [chained]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            raise NotImplementedError(pos)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _gsubLookup(self, lookup, fealookup):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        statements = fealookup.statements
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        sub = lookup.sub
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # Alternate substitutions are represented by adding multiple
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # substitutions for the same glyph, so we need to collect them into one
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # to many mapping.
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if isinstance(sub, VAst.SubstitutionAlternateDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            alternates = {}
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for key, val in sub.mapping.items():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if not key or not val:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    path, line, column = sub.location
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    continue
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs = self._coverage(key)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                replacements = self._coverage(val)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(glyphs) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                for src_glyph, repl_glyph in zip(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    glyphs[0].glyphSet(), replacements[0].glyphSet()
 | 
						
						
						
						
							 | 
							
								 | 
							
							                ):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    alternates.setdefault(str(self._glyphName(src_glyph)), []).append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        str(self._glyphName(repl_glyph))
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for glyph, replacements in alternates.items():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statement = ast.AlternateSubstStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    [], glyph, [], ast.GlyphClass(replacements)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            return
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for key, val in sub.mapping.items():
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if not key or not val:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                path, line, column = sub.location
 | 
						
						
						
						
							 | 
							
								 | 
							
							                log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
 | 
						
						
						
						
							 | 
							
								 | 
							
							                continue
 | 
						
						
						
						
							 | 
							
								 | 
							
							            glyphs = self._coverage(key)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            replacements = self._coverage(val)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if isinstance(sub, VAst.SubstitutionSingleDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(glyphs) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(replacements) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    ast.SingleSubstStatement(glyphs, replacements, [], [], False)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # This is handled in gsubContextLookup()
 | 
						
						
						
						
							 | 
							
								 | 
							
							                pass
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(sub, VAst.SubstitutionMultipleDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(glyphs) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    ast.MultipleSubstStatement([], glyphs[0], [], replacements)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif isinstance(sub, VAst.SubstitutionLigatureDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                assert len(replacements) == 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statement = ast.LigatureSubstStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    [], glyphs, [], replacements[0], False
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # If any of the input glyphs is a group, we need to
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # explode the substitution into multiple ligature substitutions
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # since feature file syntax does not support classes in
 | 
						
						
						
						
							 | 
							
								 | 
							
							                # ligature substitutions.
 | 
						
						
						
						
							 | 
							
								 | 
							
							                n = max(len(x.glyphSet()) for x in glyphs)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if n > 1:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    # All input should either be groups of the same length or single glyphs
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    assert all(len(x.glyphSet()) in (n, 1) for x in glyphs)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    glyphs = [x.glyphSet() for x in glyphs]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    glyphs = [([x[0]] * n if len(x) == 1 else x) for x in glyphs]
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    # In this case ligature replacements must be a group of the same length
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    # as the input groups, or a single glyph. VOLT
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    # allows the replacement glyphs to be longer and truncates them.
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    # So well allow that and zip() below will do the truncation
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    # for us.
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    replacement = replacements[0].glyphSet()
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    if len(replacement) == 1:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        replacement = [replacement[0]] * n
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    assert len(replacement) >= n
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    # Add the unexploded statement commented out for reference.
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    statements.append(ast.Comment(f"# {statement}"))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    for zipped in zip(*glyphs, replacement):
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        zipped = [self._glyphName(x) for x in zipped]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        statements.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            ast.LigatureSubstStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                                [], zipped[:-1], [], zipped[-1], False
 | 
						
						
						
						
							 | 
							
								 | 
							
							                            )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    statements.append(statement)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                raise NotImplementedError(sub)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _gsubContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        statements = fealookup.statements
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        sub = lookup.sub
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # Reverse substitutions is a special case, it can’t use chained lookups.
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for key, val in sub.mapping.items():
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if not key or not val:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    path, line, column = sub.location
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    continue
 | 
						
						
						
						
							 | 
							
								 | 
							
							                glyphs = self._coverage(key)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                replacements = self._coverage(val)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                statements.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    ast.ReverseChainSingleSubstStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        prefix, suffix, glyphs, replacements
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							            fealookup.chained = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							            return
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if not isinstance(
 | 
						
						
						
						
							 | 
							
								 | 
							
							            sub,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            (
 | 
						
						
						
						
							 | 
							
								 | 
							
							                VAst.SubstitutionSingleDefinition,
 | 
						
						
						
						
							 | 
							
								 | 
							
							                VAst.SubstitutionMultipleDefinition,
 | 
						
						
						
						
							 | 
							
								 | 
							
							                VAst.SubstitutionLigatureDefinition,
 | 
						
						
						
						
							 | 
							
								 | 
							
							                VAst.SubstitutionAlternateDefinition,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            ),
 | 
						
						
						
						
							 | 
							
								 | 
							
							        ):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            raise NotImplementedError(type(sub))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        glyphs = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for key, val in sub.mapping.items():
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if not key or not val:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                path, line, column = sub.location
 | 
						
						
						
						
							 | 
							
								 | 
							
							                log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
 | 
						
						
						
						
							 | 
							
								 | 
							
							                continue
 | 
						
						
						
						
							 | 
							
								 | 
							
							            glyphs.extend(self._coverage(key, flatten=True))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if len(glyphs) > 1:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            glyphs = [ast.GlyphClass(glyphs)]
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if ignore:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(ast.IgnoreSubstStatement([(prefix, glyphs, suffix)]))
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            statements.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                ast.ChainContextSubstStatement(prefix, glyphs, suffix, [chained])
 | 
						
						
						
						
							 | 
							
								 | 
							
							            )
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    def _lookupDefinition(self, lookup):
 | 
						
						
						
						
							 | 
							
								 | 
							
							        mark_attachement = None
 | 
						
						
						
						
							 | 
							
								 | 
							
							        mark_filtering = None
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        flags = 0
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if lookup.direction == "RTL":
 | 
						
						
						
						
							 | 
							
								 | 
							
							            flags |= 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if not lookup.process_base:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            flags |= 2
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # FIXME: Does VOLT support this?
 | 
						
						
						
						
							 | 
							
								 | 
							
							        # if not lookup.process_ligatures:
 | 
						
						
						
						
							 | 
							
								 | 
							
							        #     flags |= 4
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if not lookup.process_marks:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            flags |= 8
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif isinstance(lookup.process_marks, str):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            mark_attachement = self._groupName(lookup.process_marks)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        elif lookup.mark_glyph_set is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            mark_filtering = self._groupName(lookup.mark_glyph_set)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        lookupflags = None
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if flags or mark_attachement is not None or mark_filtering is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            lookupflags = ast.LookupFlagStatement(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                flags, mark_attachement, mark_filtering
 | 
						
						
						
						
							 | 
							
								 | 
							
							            )
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        use_extension = False
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"):
 | 
						
						
						
						
							 | 
							
								 | 
							
							            use_extension = True
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if "\\" in lookup.name:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # Merge sub lookups as subtables (lookups named “base\sub”),
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # makeotf/feaLib will issue a warning and ignore the subtable
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # statement if it is not a pairpos lookup, though.
 | 
						
						
						
						
							 | 
							
								 | 
							
							            name = lookup.name.split("\\")[0]
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if name.lower() not in self._lookups:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                fealookup = Lookup(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    self._lookupName(name),
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    use_extension=use_extension,
 | 
						
						
						
						
							 | 
							
								 | 
							
							                )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if lookupflags is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    fealookup.statements.append(lookupflags)
 | 
						
						
						
						
							 | 
							
								 | 
							
							                fealookup.statements.append(ast.Comment("# " + lookup.name))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                fealookup = self._lookups[name.lower()]
 | 
						
						
						
						
							 | 
							
								 | 
							
							                fealookup.statements.append(ast.SubtableStatement())
 | 
						
						
						
						
							 | 
							
								 | 
							
							                fealookup.statements.append(ast.Comment("# " + lookup.name))
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._lookups[name.lower()] = fealookup
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            fealookup = Lookup(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._lookupName(lookup.name),
 | 
						
						
						
						
							 | 
							
								 | 
							
							                use_extension=use_extension,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            )
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if lookupflags is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                fealookup.statements.append(lookupflags)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            self._lookups[lookup.name.lower()] = fealookup
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if lookup.comments is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            fealookup.statements.append(ast.Comment("# " + lookup.comments))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        contexts = []
 | 
						
						
						
						
							 | 
							
								 | 
							
							        for context in lookup.context:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            prefix = self._context(context.left)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            suffix = self._context(context.right)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            ignore = context.ex_or_in == "EXCEPT_CONTEXT"
 | 
						
						
						
						
							 | 
							
								 | 
							
							            contexts.append([prefix, suffix, ignore])
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # It seems that VOLT will create contextual substitution using
 | 
						
						
						
						
							 | 
							
								 | 
							
							            # only the input if there is no other contexts in this lookup.
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if ignore and len(lookup.context) == 1:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                contexts.append([[], [], False])
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if contexts:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            chained = ast.LookupBlock(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._lookupName(lookup.name + " chained"),
 | 
						
						
						
						
							 | 
							
								 | 
							
							                use_extension=use_extension,
 | 
						
						
						
						
							 | 
							
								 | 
							
							            )
 | 
						
						
						
						
							 | 
							
								 | 
							
							            fealookup.chained.append(chained)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if lookup.sub is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._gsubLookup(lookup, chained)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif lookup.pos is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._gposLookup(lookup, chained)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            for prefix, suffix, ignore in contexts:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                if lookup.sub is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    self._gsubContextLookup(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        lookup, prefix, suffix, ignore, fealookup, chained
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							                elif lookup.pos is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    self._gposContextLookup(
 | 
						
						
						
						
							 | 
							
								 | 
							
							                        lookup, prefix, suffix, ignore, fealookup, chained
 | 
						
						
						
						
							 | 
							
								 | 
							
							                    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            if lookup.sub is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._gsubLookup(lookup, fealookup)
 | 
						
						
						
						
							 | 
							
								 | 
							
							            elif lookup.pos is not None:
 | 
						
						
						
						
							 | 
							
								 | 
							
							                self._gposLookup(lookup, fealookup)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							def main(args=None):
 | 
						
						
						
						
							 | 
							
								 | 
							
							    """Convert MS VOLT to AFDKO feature files."""
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    import argparse
 | 
						
						
						
						
							 | 
							
								 | 
							
							    from pathlib import Path
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    from fontTools import configLogger
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    parser = argparse.ArgumentParser(
 | 
						
						
						
						
							 | 
							
								 | 
							
							        "fonttools voltLib.voltToFea", description=main.__doc__
 | 
						
						
						
						
							 | 
							
								 | 
							
							    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							    parser.add_argument(
 | 
						
						
						
						
							 | 
							
								 | 
							
							        "input", metavar="INPUT", type=Path, help="input font/VTP file to process"
 | 
						
						
						
						
							 | 
							
								 | 
							
							    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							    parser.add_argument(
 | 
						
						
						
						
							 | 
							
								 | 
							
							        "featurefile", metavar="OUTPUT", type=Path, help="output feature file"
 | 
						
						
						
						
							 | 
							
								 | 
							
							    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							    parser.add_argument(
 | 
						
						
						
						
							 | 
							
								 | 
							
							        "-t",
 | 
						
						
						
						
							 | 
							
								 | 
							
							        "--table",
 | 
						
						
						
						
							 | 
							
								 | 
							
							        action="append",
 | 
						
						
						
						
							 | 
							
								 | 
							
							        choices=TABLES,
 | 
						
						
						
						
							 | 
							
								 | 
							
							        dest="tables",
 | 
						
						
						
						
							 | 
							
								 | 
							
							        help="List of tables to write, by default all tables are written",
 | 
						
						
						
						
							 | 
							
								 | 
							
							    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							    parser.add_argument(
 | 
						
						
						
						
							 | 
							
								 | 
							
							        "-q", "--quiet", action="store_true", help="Suppress non-error messages"
 | 
						
						
						
						
							 | 
							
								 | 
							
							    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							    parser.add_argument(
 | 
						
						
						
						
							 | 
							
								 | 
							
							        "--traceback", action="store_true", help="Don’t catch exceptions"
 | 
						
						
						
						
							 | 
							
								 | 
							
							    )
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    options = parser.parse_args(args)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    configLogger(level=("ERROR" if options.quiet else "INFO"))
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    file_or_path = options.input
 | 
						
						
						
						
							 | 
							
								 | 
							
							    font = None
 | 
						
						
						
						
							 | 
							
								 | 
							
							    try:
 | 
						
						
						
						
							 | 
							
								 | 
							
							        font = TTFont(file_or_path)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if "TSIV" in font:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            file_or_path = StringIO(font["TSIV"].data.decode("utf-8"))
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            log.error('"TSIV" table is missing, font was not saved from VOLT?')
 | 
						
						
						
						
							 | 
							
								 | 
							
							            return 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							    except TTLibError:
 | 
						
						
						
						
							 | 
							
								 | 
							
							        pass
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    converter = VoltToFea(file_or_path, font)
 | 
						
						
						
						
							 | 
							
								 | 
							
							    try:
 | 
						
						
						
						
							 | 
							
								 | 
							
							        fea = converter.convert(options.tables)
 | 
						
						
						
						
							 | 
							
								 | 
							
							    except NotImplementedError as e:
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if options.traceback:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            raise
 | 
						
						
						
						
							 | 
							
								 | 
							
							        location = getattr(e.args[0], "location", None)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        message = f'"{e}" is not supported'
 | 
						
						
						
						
							 | 
							
								 | 
							
							        if location:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            path, line, column = location
 | 
						
						
						
						
							 | 
							
								 | 
							
							            log.error(f"{path}:{line}:{column}: {message}")
 | 
						
						
						
						
							 | 
							
								 | 
							
							        else:
 | 
						
						
						
						
							 | 
							
								 | 
							
							            log.error(message)
 | 
						
						
						
						
							 | 
							
								 | 
							
							        return 1
 | 
						
						
						
						
							 | 
							
								 | 
							
							    with open(options.featurefile, "w") as feafile:
 | 
						
						
						
						
							 | 
							
								 | 
							
							        feafile.write(fea)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							if __name__ == "__main__":
 | 
						
						
						
						
							 | 
							
								 | 
							
							    import sys
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							    sys.exit(main())
 |