| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394 |
- from fontTools.feaLib.error import FeatureLibError
- from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer
- from fontTools.feaLib.variableScalar import VariableScalar
- from fontTools.misc.encodingTools import getEncoding
- from fontTools.misc.textTools import bytechr, tobytes, tostr
- import fontTools.feaLib.ast as ast
- import logging
- import os
- import re
- log = logging.getLogger(__name__)
- class Parser(object):
- """Initializes a Parser object.
- Example:
- .. code:: python
- from fontTools.feaLib.parser import Parser
- parser = Parser(file, font.getReverseGlyphMap())
- parsetree = parser.parse()
- Note: the ``glyphNames`` iterable serves a double role to help distinguish
- glyph names from ranges in the presence of hyphens and to ensure that glyph
- names referenced in a feature file are actually part of a font's glyph set.
- If the iterable is left empty, no glyph name in glyph set checking takes
- place, and all glyph tokens containing hyphens are treated as literal glyph
- names, not as ranges. (Adding a space around the hyphen can, in any case,
- help to disambiguate ranges from glyph names containing hyphens.)
- By default, the parser will follow ``include()`` statements in the feature
- file. To turn this off, pass ``followIncludes=False``. Pass a directory string as
- ``includeDir`` to explicitly declare a directory to search included feature files
- in.
- """
- extensions = {}
- ast = ast
- SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20 + 1)}
- CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99 + 1)}
- def __init__(
- self, featurefile, glyphNames=(), followIncludes=True, includeDir=None, **kwargs
- ):
- if "glyphMap" in kwargs:
- from fontTools.misc.loggingTools import deprecateArgument
- deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
- if glyphNames:
- raise TypeError(
- "'glyphNames' and (deprecated) 'glyphMap' are " "mutually exclusive"
- )
- glyphNames = kwargs.pop("glyphMap")
- if kwargs:
- raise TypeError(
- "unsupported keyword argument%s: %s"
- % ("" if len(kwargs) == 1 else "s", ", ".join(repr(k) for k in kwargs))
- )
- self.glyphNames_ = set(glyphNames)
- self.doc_ = self.ast.FeatureFile()
- self.anchors_ = SymbolTable()
- self.glyphclasses_ = SymbolTable()
- self.lookups_ = SymbolTable()
- self.valuerecords_ = SymbolTable()
- self.symbol_tables_ = {self.anchors_, self.valuerecords_}
- self.next_token_type_, self.next_token_ = (None, None)
- self.cur_comments_ = []
- self.next_token_location_ = None
- lexerClass = IncludingLexer if followIncludes else NonIncludingLexer
- self.lexer_ = lexerClass(featurefile, includeDir=includeDir)
- self.missing = {}
- self.advance_lexer_(comments=True)
- def parse(self):
- """Parse the file, and return a :class:`fontTools.feaLib.ast.FeatureFile`
- object representing the root of the abstract syntax tree containing the
- parsed contents of the file."""
- statements = self.doc_.statements
- while self.next_token_type_ is not None or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.is_cur_keyword_("include"):
- statements.append(self.parse_include_())
- elif self.cur_token_type_ is Lexer.GLYPHCLASS:
- statements.append(self.parse_glyphclass_definition_())
- elif self.is_cur_keyword_(("anon", "anonymous")):
- statements.append(self.parse_anonymous_())
- elif self.is_cur_keyword_("anchorDef"):
- statements.append(self.parse_anchordef_())
- elif self.is_cur_keyword_("languagesystem"):
- statements.append(self.parse_languagesystem_())
- elif self.is_cur_keyword_("lookup"):
- statements.append(self.parse_lookup_(vertical=False))
- elif self.is_cur_keyword_("markClass"):
- statements.append(self.parse_markClass_())
- elif self.is_cur_keyword_("feature"):
- statements.append(self.parse_feature_block_())
- elif self.is_cur_keyword_("conditionset"):
- statements.append(self.parse_conditionset_())
- elif self.is_cur_keyword_("variation"):
- statements.append(self.parse_feature_block_(variation=True))
- elif self.is_cur_keyword_("table"):
- statements.append(self.parse_table_())
- elif self.is_cur_keyword_("valueRecordDef"):
- statements.append(self.parse_valuerecord_definition_(vertical=False))
- elif (
- self.cur_token_type_ is Lexer.NAME
- and self.cur_token_ in self.extensions
- ):
- statements.append(self.extensions[self.cur_token_](self))
- elif self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError(
- "Expected feature, languagesystem, lookup, markClass, "
- 'table, or glyph class definition, got {} "{}"'.format(
- self.cur_token_type_, self.cur_token_
- ),
- self.cur_token_location_,
- )
- # Report any missing glyphs at the end of parsing
- if self.missing:
- error = [
- " %s (first found at %s)" % (name, loc)
- for name, loc in self.missing.items()
- ]
- raise FeatureLibError(
- "The following glyph names are referenced but are missing from the "
- "glyph set:\n" + ("\n".join(error)),
- None,
- )
- return self.doc_
- def parse_anchor_(self):
- # Parses an anchor in any of the four formats given in the feature
- # file specification (2.e.vii).
- self.expect_symbol_("<")
- self.expect_keyword_("anchor")
- location = self.cur_token_location_
- if self.next_token_ == "NULL": # Format D
- self.expect_keyword_("NULL")
- self.expect_symbol_(">")
- return None
- if self.next_token_type_ == Lexer.NAME: # Format E
- name = self.expect_name_()
- anchordef = self.anchors_.resolve(name)
- if anchordef is None:
- raise FeatureLibError(
- 'Unknown anchor "%s"' % name, self.cur_token_location_
- )
- self.expect_symbol_(">")
- return self.ast.Anchor(
- anchordef.x,
- anchordef.y,
- name=name,
- contourpoint=anchordef.contourpoint,
- xDeviceTable=None,
- yDeviceTable=None,
- location=location,
- )
- x, y = self.expect_number_(variable=True), self.expect_number_(variable=True)
- contourpoint = None
- if self.next_token_ == "contourpoint": # Format B
- self.expect_keyword_("contourpoint")
- contourpoint = self.expect_number_()
- if self.next_token_ == "<": # Format C
- xDeviceTable = self.parse_device_()
- yDeviceTable = self.parse_device_()
- else:
- xDeviceTable, yDeviceTable = None, None
- self.expect_symbol_(">")
- return self.ast.Anchor(
- x,
- y,
- name=None,
- contourpoint=contourpoint,
- xDeviceTable=xDeviceTable,
- yDeviceTable=yDeviceTable,
- location=location,
- )
- def parse_anchor_marks_(self):
- # Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
- anchorMarks = [] # [(self.ast.Anchor, markClassName)*]
- while self.next_token_ == "<":
- anchor = self.parse_anchor_()
- if anchor is None and self.next_token_ != "mark":
- continue # <anchor NULL> without mark, eg. in GPOS type 5
- self.expect_keyword_("mark")
- markClass = self.expect_markClass_reference_()
- anchorMarks.append((anchor, markClass))
- return anchorMarks
- def parse_anchordef_(self):
- # Parses a named anchor definition (`section 2.e.viii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.vii>`_).
- assert self.is_cur_keyword_("anchorDef")
- location = self.cur_token_location_
- x, y = self.expect_number_(), self.expect_number_()
- contourpoint = None
- if self.next_token_ == "contourpoint":
- self.expect_keyword_("contourpoint")
- contourpoint = self.expect_number_()
- name = self.expect_name_()
- self.expect_symbol_(";")
- anchordef = self.ast.AnchorDefinition(
- name, x, y, contourpoint=contourpoint, location=location
- )
- self.anchors_.define(name, anchordef)
- return anchordef
- def parse_anonymous_(self):
- # Parses an anonymous data block (`section 10 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#10>`_).
- assert self.is_cur_keyword_(("anon", "anonymous"))
- tag = self.expect_tag_()
- _, content, location = self.lexer_.scan_anonymous_block(tag)
- self.advance_lexer_()
- self.expect_symbol_("}")
- end_tag = self.expect_tag_()
- assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()"
- self.expect_symbol_(";")
- return self.ast.AnonymousBlock(tag, content, location=location)
- def parse_attach_(self):
- # Parses a GDEF Attach statement (`section 9.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.b>`_)
- assert self.is_cur_keyword_("Attach")
- location = self.cur_token_location_
- glyphs = self.parse_glyphclass_(accept_glyphname=True)
- contourPoints = {self.expect_number_()}
- while self.next_token_ != ";":
- contourPoints.add(self.expect_number_())
- self.expect_symbol_(";")
- return self.ast.AttachStatement(glyphs, contourPoints, location=location)
- def parse_enumerate_(self, vertical):
- # Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_).
- assert self.cur_token_ in {"enumerate", "enum"}
- self.advance_lexer_()
- return self.parse_position_(enumerated=True, vertical=vertical)
- def parse_GlyphClassDef_(self):
- # Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'
- assert self.is_cur_keyword_("GlyphClassDef")
- location = self.cur_token_location_
- if self.next_token_ != ",":
- baseGlyphs = self.parse_glyphclass_(accept_glyphname=False)
- else:
- baseGlyphs = None
- self.expect_symbol_(",")
- if self.next_token_ != ",":
- ligatureGlyphs = self.parse_glyphclass_(accept_glyphname=False)
- else:
- ligatureGlyphs = None
- self.expect_symbol_(",")
- if self.next_token_ != ",":
- markGlyphs = self.parse_glyphclass_(accept_glyphname=False)
- else:
- markGlyphs = None
- self.expect_symbol_(",")
- if self.next_token_ != ";":
- componentGlyphs = self.parse_glyphclass_(accept_glyphname=False)
- else:
- componentGlyphs = None
- self.expect_symbol_(";")
- return self.ast.GlyphClassDefStatement(
- baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=location
- )
- def parse_glyphclass_definition_(self):
- # Parses glyph class definitions such as '@UPPERCASE = [A-Z];'
- location, name = self.cur_token_location_, self.cur_token_
- self.expect_symbol_("=")
- glyphs = self.parse_glyphclass_(accept_glyphname=False)
- self.expect_symbol_(";")
- glyphclass = self.ast.GlyphClassDefinition(name, glyphs, location=location)
- self.glyphclasses_.define(name, glyphclass)
- return glyphclass
- def split_glyph_range_(self, name, location):
- # Since v1.20, the OpenType Feature File specification allows
- # for dashes in glyph names. A sequence like "a-b-c-d" could
- # therefore mean a single glyph whose name happens to be
- # "a-b-c-d", or it could mean a range from glyph "a" to glyph
- # "b-c-d", or a range from glyph "a-b" to glyph "c-d", or a
- # range from glyph "a-b-c" to glyph "d".Technically, this
- # example could be resolved because the (pretty complex)
- # definition of glyph ranges renders most of these splits
- # invalid. But the specification does not say that a compiler
- # should try to apply such fancy heuristics. To encourage
- # unambiguous feature files, we therefore try all possible
- # splits and reject the feature file if there are multiple
- # splits possible. It is intentional that we don't just emit a
- # warning; warnings tend to get ignored. To fix the problem,
- # font designers can trivially add spaces around the intended
- # split point, and we emit a compiler error that suggests
- # how exactly the source should be rewritten to make things
- # unambiguous.
- parts = name.split("-")
- solutions = []
- for i in range(len(parts)):
- start, limit = "-".join(parts[0:i]), "-".join(parts[i:])
- if start in self.glyphNames_ and limit in self.glyphNames_:
- solutions.append((start, limit))
- if len(solutions) == 1:
- start, limit = solutions[0]
- return start, limit
- elif len(solutions) == 0:
- raise FeatureLibError(
- '"%s" is not a glyph in the font, and it can not be split '
- "into a range of known glyphs" % name,
- location,
- )
- else:
- ranges = " or ".join(['"%s - %s"' % (s, l) for s, l in solutions])
- raise FeatureLibError(
- 'Ambiguous glyph range "%s"; '
- "please use %s to clarify what you mean" % (name, ranges),
- location,
- )
- def parse_glyphclass_(self, accept_glyphname, accept_null=False):
- # Parses a glyph class, either named or anonymous, or (if
- # ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then
- # also accept the special NULL glyph.
- if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
- if accept_null and self.next_token_ == "NULL":
- # If you want a glyph called NULL, you should escape it.
- self.advance_lexer_()
- return self.ast.NullGlyph(location=self.cur_token_location_)
- glyph = self.expect_glyph_()
- self.check_glyph_name_in_glyph_set(glyph)
- return self.ast.GlyphName(glyph, location=self.cur_token_location_)
- if self.next_token_type_ is Lexer.GLYPHCLASS:
- self.advance_lexer_()
- gc = self.glyphclasses_.resolve(self.cur_token_)
- if gc is None:
- raise FeatureLibError(
- "Unknown glyph class @%s" % self.cur_token_,
- self.cur_token_location_,
- )
- if isinstance(gc, self.ast.MarkClass):
- return self.ast.MarkClassName(gc, location=self.cur_token_location_)
- else:
- return self.ast.GlyphClassName(gc, location=self.cur_token_location_)
- self.expect_symbol_("[")
- location = self.cur_token_location_
- glyphs = self.ast.GlyphClass(location=location)
- while self.next_token_ != "]":
- if self.next_token_type_ is Lexer.NAME:
- glyph = self.expect_glyph_()
- location = self.cur_token_location_
- if "-" in glyph and self.glyphNames_ and glyph not in self.glyphNames_:
- start, limit = self.split_glyph_range_(glyph, location)
- self.check_glyph_name_in_glyph_set(start, limit)
- glyphs.add_range(
- start, limit, self.make_glyph_range_(location, start, limit)
- )
- elif self.next_token_ == "-":
- start = glyph
- self.expect_symbol_("-")
- limit = self.expect_glyph_()
- self.check_glyph_name_in_glyph_set(start, limit)
- glyphs.add_range(
- start, limit, self.make_glyph_range_(location, start, limit)
- )
- else:
- if "-" in glyph and not self.glyphNames_:
- log.warning(
- str(
- FeatureLibError(
- f"Ambiguous glyph name that looks like a range: {glyph!r}",
- location,
- )
- )
- )
- self.check_glyph_name_in_glyph_set(glyph)
- glyphs.append(glyph)
- elif self.next_token_type_ is Lexer.CID:
- glyph = self.expect_glyph_()
- if self.next_token_ == "-":
- range_location = self.cur_token_location_
- range_start = self.cur_token_
- self.expect_symbol_("-")
- range_end = self.expect_cid_()
- self.check_glyph_name_in_glyph_set(
- f"cid{range_start:05d}",
- f"cid{range_end:05d}",
- )
- glyphs.add_cid_range(
- range_start,
- range_end,
- self.make_cid_range_(range_location, range_start, range_end),
- )
- else:
- glyph_name = f"cid{self.cur_token_:05d}"
- self.check_glyph_name_in_glyph_set(glyph_name)
- glyphs.append(glyph_name)
- elif self.next_token_type_ is Lexer.GLYPHCLASS:
- self.advance_lexer_()
- gc = self.glyphclasses_.resolve(self.cur_token_)
- if gc is None:
- raise FeatureLibError(
- "Unknown glyph class @%s" % self.cur_token_,
- self.cur_token_location_,
- )
- if isinstance(gc, self.ast.MarkClass):
- gc = self.ast.MarkClassName(gc, location=self.cur_token_location_)
- else:
- gc = self.ast.GlyphClassName(gc, location=self.cur_token_location_)
- glyphs.add_class(gc)
- else:
- raise FeatureLibError(
- "Expected glyph name, glyph range, "
- f"or glyph class reference, found {self.next_token_!r}",
- self.next_token_location_,
- )
- self.expect_symbol_("]")
- return glyphs
- def parse_glyph_pattern_(self, vertical):
- # Parses a glyph pattern, including lookups and context, e.g.::
- #
- # a b
- # a b c' d e
- # a b c' lookup ChangeC d e
- prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
- hasMarks = False
- while self.next_token_ not in {"by", "from", ";", ","}:
- gc = self.parse_glyphclass_(accept_glyphname=True)
- marked = False
- if self.next_token_ == "'":
- self.expect_symbol_("'")
- hasMarks = marked = True
- if marked:
- if suffix:
- # makeotf also reports this as an error, while FontForge
- # silently inserts ' in all the intervening glyphs.
- # https://github.com/fonttools/fonttools/pull/1096
- raise FeatureLibError(
- "Unsupported contextual target sequence: at most "
- "one run of marked (') glyph/class names allowed",
- self.cur_token_location_,
- )
- glyphs.append(gc)
- elif glyphs:
- suffix.append(gc)
- else:
- prefix.append(gc)
- if self.is_next_value_():
- values.append(self.parse_valuerecord_(vertical))
- else:
- values.append(None)
- lookuplist = None
- while self.next_token_ == "lookup":
- if lookuplist is None:
- lookuplist = []
- self.expect_keyword_("lookup")
- if not marked:
- raise FeatureLibError(
- "Lookups can only follow marked glyphs",
- self.cur_token_location_,
- )
- lookup_name = self.expect_name_()
- lookup = self.lookups_.resolve(lookup_name)
- if lookup is None:
- raise FeatureLibError(
- 'Unknown lookup "%s"' % lookup_name, self.cur_token_location_
- )
- lookuplist.append(lookup)
- if marked:
- lookups.append(lookuplist)
- if not glyphs and not suffix: # eg., "sub f f i by"
- assert lookups == []
- return ([], prefix, [None] * len(prefix), values, [], hasMarks)
- else:
- if any(values[: len(prefix)]):
- raise FeatureLibError(
- "Positioning cannot be applied in the bactrack glyph sequence, "
- "before the marked glyph sequence.",
- self.cur_token_location_,
- )
- marked_values = values[len(prefix) : len(prefix) + len(glyphs)]
- if any(marked_values):
- if any(values[len(prefix) + len(glyphs) :]):
- raise FeatureLibError(
- "Positioning values are allowed only in the marked glyph "
- "sequence, or after the final glyph node when only one glyph "
- "node is marked.",
- self.cur_token_location_,
- )
- values = marked_values
- elif values and values[-1]:
- if len(glyphs) > 1 or any(values[:-1]):
- raise FeatureLibError(
- "Positioning values are allowed only in the marked glyph "
- "sequence, or after the final glyph node when only one glyph "
- "node is marked.",
- self.cur_token_location_,
- )
- values = values[-1:]
- elif any(values):
- raise FeatureLibError(
- "Positioning values are allowed only in the marked glyph "
- "sequence, or after the final glyph node when only one glyph "
- "node is marked.",
- self.cur_token_location_,
- )
- return (prefix, glyphs, lookups, values, suffix, hasMarks)
- def parse_ignore_glyph_pattern_(self, sub):
- location = self.cur_token_location_
- prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
- vertical=False
- )
- if any(lookups):
- raise FeatureLibError(
- f'No lookups can be specified for "ignore {sub}"', location
- )
- if not hasMarks:
- error = FeatureLibError(
- f'Ambiguous "ignore {sub}", there should be least one marked glyph',
- location,
- )
- log.warning(str(error))
- suffix, glyphs = glyphs[1:], glyphs[0:1]
- chainContext = (prefix, glyphs, suffix)
- return chainContext
- def parse_ignore_context_(self, sub):
- location = self.cur_token_location_
- chainContext = [self.parse_ignore_glyph_pattern_(sub)]
- while self.next_token_ == ",":
- self.expect_symbol_(",")
- chainContext.append(self.parse_ignore_glyph_pattern_(sub))
- self.expect_symbol_(";")
- return chainContext
- def parse_ignore_(self):
- # Parses an ignore sub/pos rule.
- assert self.is_cur_keyword_("ignore")
- location = self.cur_token_location_
- self.advance_lexer_()
- if self.cur_token_ in ["substitute", "sub"]:
- chainContext = self.parse_ignore_context_("sub")
- return self.ast.IgnoreSubstStatement(chainContext, location=location)
- if self.cur_token_ in ["position", "pos"]:
- chainContext = self.parse_ignore_context_("pos")
- return self.ast.IgnorePosStatement(chainContext, location=location)
- raise FeatureLibError(
- 'Expected "substitute" or "position"', self.cur_token_location_
- )
- def parse_include_(self):
- assert self.cur_token_ == "include"
- location = self.cur_token_location_
- filename = self.expect_filename_()
- # self.expect_symbol_(";")
- return ast.IncludeStatement(filename, location=location)
- def parse_language_(self):
- assert self.is_cur_keyword_("language")
- location = self.cur_token_location_
- language = self.expect_language_tag_()
- include_default, required = (True, False)
- if self.next_token_ in {"exclude_dflt", "include_dflt"}:
- include_default = self.expect_name_() == "include_dflt"
- if self.next_token_ == "required":
- self.expect_keyword_("required")
- required = True
- self.expect_symbol_(";")
- return self.ast.LanguageStatement(
- language, include_default, required, location=location
- )
- def parse_ligatureCaretByIndex_(self):
- assert self.is_cur_keyword_("LigatureCaretByIndex")
- location = self.cur_token_location_
- glyphs = self.parse_glyphclass_(accept_glyphname=True)
- carets = [self.expect_number_()]
- while self.next_token_ != ";":
- carets.append(self.expect_number_())
- self.expect_symbol_(";")
- return self.ast.LigatureCaretByIndexStatement(glyphs, carets, location=location)
- def parse_ligatureCaretByPos_(self):
- assert self.is_cur_keyword_("LigatureCaretByPos")
- location = self.cur_token_location_
- glyphs = self.parse_glyphclass_(accept_glyphname=True)
- carets = [self.expect_number_(variable=True)]
- while self.next_token_ != ";":
- carets.append(self.expect_number_(variable=True))
- self.expect_symbol_(";")
- return self.ast.LigatureCaretByPosStatement(glyphs, carets, location=location)
- def parse_lookup_(self, vertical):
- # Parses a ``lookup`` - either a lookup block, or a lookup reference
- # inside a feature.
- assert self.is_cur_keyword_("lookup")
- location, name = self.cur_token_location_, self.expect_name_()
- if self.next_token_ == ";":
- lookup = self.lookups_.resolve(name)
- if lookup is None:
- raise FeatureLibError(
- 'Unknown lookup "%s"' % name, self.cur_token_location_
- )
- self.expect_symbol_(";")
- return self.ast.LookupReferenceStatement(lookup, location=location)
- use_extension = False
- if self.next_token_ == "useExtension":
- self.expect_keyword_("useExtension")
- use_extension = True
- block = self.ast.LookupBlock(name, use_extension, location=location)
- self.parse_block_(block, vertical)
- self.lookups_.define(name, block)
- return block
- def parse_lookupflag_(self):
- # Parses a ``lookupflag`` statement, either specified by number or
- # in words.
- assert self.is_cur_keyword_("lookupflag")
- location = self.cur_token_location_
- # format B: "lookupflag 6;"
- if self.next_token_type_ == Lexer.NUMBER:
- value = self.expect_number_()
- self.expect_symbol_(";")
- return self.ast.LookupFlagStatement(value, location=location)
- # format A: "lookupflag RightToLeft MarkAttachmentType @M;"
- value_seen = False
- value, markAttachment, markFilteringSet = 0, None, None
- flags = {
- "RightToLeft": 1,
- "IgnoreBaseGlyphs": 2,
- "IgnoreLigatures": 4,
- "IgnoreMarks": 8,
- }
- seen = set()
- while self.next_token_ != ";":
- if self.next_token_ in seen:
- raise FeatureLibError(
- "%s can be specified only once" % self.next_token_,
- self.next_token_location_,
- )
- seen.add(self.next_token_)
- if self.next_token_ == "MarkAttachmentType":
- self.expect_keyword_("MarkAttachmentType")
- markAttachment = self.parse_glyphclass_(accept_glyphname=False)
- elif self.next_token_ == "UseMarkFilteringSet":
- self.expect_keyword_("UseMarkFilteringSet")
- markFilteringSet = self.parse_glyphclass_(accept_glyphname=False)
- elif self.next_token_ in flags:
- value_seen = True
- value = value | flags[self.expect_name_()]
- else:
- raise FeatureLibError(
- '"%s" is not a recognized lookupflag' % self.next_token_,
- self.next_token_location_,
- )
- self.expect_symbol_(";")
- if not any([value_seen, markAttachment, markFilteringSet]):
- raise FeatureLibError(
- "lookupflag must have a value", self.next_token_location_
- )
- return self.ast.LookupFlagStatement(
- value,
- markAttachment=markAttachment,
- markFilteringSet=markFilteringSet,
- location=location,
- )
- def parse_markClass_(self):
- assert self.is_cur_keyword_("markClass")
- location = self.cur_token_location_
- glyphs = self.parse_glyphclass_(accept_glyphname=True)
- if not glyphs.glyphSet():
- raise FeatureLibError(
- "Empty glyph class in mark class definition", location
- )
- anchor = self.parse_anchor_()
- name = self.expect_class_name_()
- self.expect_symbol_(";")
- markClass = self.doc_.markClasses.get(name)
- if markClass is None:
- markClass = self.ast.MarkClass(name)
- self.doc_.markClasses[name] = markClass
- self.glyphclasses_.define(name, markClass)
- mcdef = self.ast.MarkClassDefinition(
- markClass, anchor, glyphs, location=location
- )
- markClass.addDefinition(mcdef)
- return mcdef
- def parse_position_(self, enumerated, vertical):
- assert self.cur_token_ in {"position", "pos"}
- if self.next_token_ == "cursive": # GPOS type 3
- return self.parse_position_cursive_(enumerated, vertical)
- elif self.next_token_ == "base": # GPOS type 4
- return self.parse_position_base_(enumerated, vertical)
- elif self.next_token_ == "ligature": # GPOS type 5
- return self.parse_position_ligature_(enumerated, vertical)
- elif self.next_token_ == "mark": # GPOS type 6
- return self.parse_position_mark_(enumerated, vertical)
- location = self.cur_token_location_
- prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
- vertical
- )
- self.expect_symbol_(";")
- if any(lookups):
- # GPOS type 8: Chaining contextual positioning; explicit lookups
- if any(values):
- raise FeatureLibError(
- 'If "lookup" is present, no values must be specified', location
- )
- return self.ast.ChainContextPosStatement(
- prefix, glyphs, suffix, lookups, location=location
- )
- # Pair positioning, format A: "pos V 10 A -10;"
- # Pair positioning, format B: "pos V A -20;"
- if not prefix and not suffix and len(glyphs) == 2 and not hasMarks:
- if values[0] is None: # Format B: "pos V A -20;"
- values.reverse()
- return self.ast.PairPosStatement(
- glyphs[0],
- values[0],
- glyphs[1],
- values[1],
- enumerated=enumerated,
- location=location,
- )
- if enumerated:
- raise FeatureLibError(
- '"enumerate" is only allowed with pair positionings', location
- )
- return self.ast.SinglePosStatement(
- list(zip(glyphs, values)),
- prefix,
- suffix,
- forceChain=hasMarks,
- location=location,
- )
- def parse_position_cursive_(self, enumerated, vertical):
- location = self.cur_token_location_
- self.expect_keyword_("cursive")
- if enumerated:
- raise FeatureLibError(
- '"enumerate" is not allowed with ' "cursive attachment positioning",
- location,
- )
- glyphclass = self.parse_glyphclass_(accept_glyphname=True)
- entryAnchor = self.parse_anchor_()
- exitAnchor = self.parse_anchor_()
- self.expect_symbol_(";")
- return self.ast.CursivePosStatement(
- glyphclass, entryAnchor, exitAnchor, location=location
- )
- def parse_position_base_(self, enumerated, vertical):
- location = self.cur_token_location_
- self.expect_keyword_("base")
- if enumerated:
- raise FeatureLibError(
- '"enumerate" is not allowed with '
- "mark-to-base attachment positioning",
- location,
- )
- base = self.parse_glyphclass_(accept_glyphname=True)
- marks = self.parse_anchor_marks_()
- self.expect_symbol_(";")
- return self.ast.MarkBasePosStatement(base, marks, location=location)
- def parse_position_ligature_(self, enumerated, vertical):
- location = self.cur_token_location_
- self.expect_keyword_("ligature")
- if enumerated:
- raise FeatureLibError(
- '"enumerate" is not allowed with '
- "mark-to-ligature attachment positioning",
- location,
- )
- ligatures = self.parse_glyphclass_(accept_glyphname=True)
- marks = [self.parse_anchor_marks_()]
- while self.next_token_ == "ligComponent":
- self.expect_keyword_("ligComponent")
- marks.append(self.parse_anchor_marks_())
- self.expect_symbol_(";")
- return self.ast.MarkLigPosStatement(ligatures, marks, location=location)
- def parse_position_mark_(self, enumerated, vertical):
- location = self.cur_token_location_
- self.expect_keyword_("mark")
- if enumerated:
- raise FeatureLibError(
- '"enumerate" is not allowed with '
- "mark-to-mark attachment positioning",
- location,
- )
- baseMarks = self.parse_glyphclass_(accept_glyphname=True)
- marks = self.parse_anchor_marks_()
- self.expect_symbol_(";")
- return self.ast.MarkMarkPosStatement(baseMarks, marks, location=location)
- def parse_script_(self):
- assert self.is_cur_keyword_("script")
- location, script = self.cur_token_location_, self.expect_script_tag_()
- self.expect_symbol_(";")
- return self.ast.ScriptStatement(script, location=location)
- def parse_substitute_(self):
- assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"}
- location = self.cur_token_location_
- reverse = self.cur_token_ in {"reversesub", "rsub"}
- (
- old_prefix,
- old,
- lookups,
- values,
- old_suffix,
- hasMarks,
- ) = self.parse_glyph_pattern_(vertical=False)
- if any(values):
- raise FeatureLibError(
- "Substitution statements cannot contain values", location
- )
- new = []
- if self.next_token_ == "by":
- keyword = self.expect_keyword_("by")
- while self.next_token_ != ";":
- gc = self.parse_glyphclass_(accept_glyphname=True, accept_null=True)
- new.append(gc)
- elif self.next_token_ == "from":
- keyword = self.expect_keyword_("from")
- new = [self.parse_glyphclass_(accept_glyphname=False)]
- else:
- keyword = None
- self.expect_symbol_(";")
- if len(new) == 0 and not any(lookups):
- raise FeatureLibError(
- 'Expected "by", "from" or explicit lookup references',
- self.cur_token_location_,
- )
- # GSUB lookup type 3: Alternate substitution.
- # Format: "substitute a from [a.1 a.2 a.3];"
- if keyword == "from":
- if reverse:
- raise FeatureLibError(
- 'Reverse chaining substitutions do not support "from"', location
- )
- if len(old) != 1 or len(old[0].glyphSet()) != 1:
- raise FeatureLibError('Expected a single glyph before "from"', location)
- if len(new) != 1:
- raise FeatureLibError(
- 'Expected a single glyphclass after "from"', location
- )
- return self.ast.AlternateSubstStatement(
- old_prefix, old[0], old_suffix, new[0], location=location
- )
- num_lookups = len([l for l in lookups if l is not None])
- is_deletion = False
- if len(new) == 1 and isinstance(new[0], ast.NullGlyph):
- if reverse:
- raise FeatureLibError(
- "Reverse chaining substitutions do not support glyph deletion",
- location,
- )
- new = [] # Deletion
- is_deletion = True
- # GSUB lookup type 1: Single substitution.
- # Format A: "substitute a by a.sc;"
- # Format B: "substitute [one.fitted one.oldstyle] by one;"
- # Format C: "substitute [a-d] by [A.sc-D.sc];"
- if not reverse and len(old) == 1 and len(new) == 1 and num_lookups == 0:
- glyphs = list(old[0].glyphSet())
- replacements = list(new[0].glyphSet())
- if len(replacements) == 1:
- replacements = replacements * len(glyphs)
- if len(glyphs) != len(replacements):
- raise FeatureLibError(
- 'Expected a glyph class with %d elements after "by", '
- "but found a glyph class with %d elements"
- % (len(glyphs), len(replacements)),
- location,
- )
- return self.ast.SingleSubstStatement(
- old, new, old_prefix, old_suffix, forceChain=hasMarks, location=location
- )
- # Glyph deletion, built as GSUB lookup type 2: Multiple substitution
- # with empty replacement.
- if is_deletion and len(old) == 1 and num_lookups == 0:
- return self.ast.MultipleSubstStatement(
- old_prefix,
- old[0],
- old_suffix,
- (),
- forceChain=hasMarks,
- location=location,
- )
- # GSUB lookup type 2: Multiple substitution.
- # Format: "substitute f_f_i by f f i;"
- #
- # GlyphsApp introduces two additional formats:
- # Format 1: "substitute [f_i f_l] by [f f] [i l];"
- # Format 2: "substitute [f_i f_l] by f [i l];"
- # http://handbook.glyphsapp.com/en/layout/multiple-substitution-with-classes/
- if not reverse and len(old) == 1 and len(new) > 1 and num_lookups == 0:
- count = len(old[0].glyphSet())
- for n in new:
- if not list(n.glyphSet()):
- raise FeatureLibError("Empty class in replacement", location)
- if len(n.glyphSet()) != 1 and len(n.glyphSet()) != count:
- raise FeatureLibError(
- f'Expected a glyph class with 1 or {count} elements after "by", '
- f"but found a glyph class with {len(n.glyphSet())} elements",
- location,
- )
- return self.ast.MultipleSubstStatement(
- old_prefix,
- old[0],
- old_suffix,
- new,
- forceChain=hasMarks,
- location=location,
- )
- # GSUB lookup type 4: Ligature substitution.
- # Format: "substitute f f i by f_f_i;"
- if (
- not reverse
- and len(old) > 1
- and len(new) == 1
- and len(new[0].glyphSet()) == 1
- and num_lookups == 0
- ):
- return self.ast.LigatureSubstStatement(
- old_prefix,
- old,
- old_suffix,
- list(new[0].glyphSet())[0],
- forceChain=hasMarks,
- location=location,
- )
- # GSUB lookup type 8: Reverse chaining substitution.
- if reverse:
- if len(old) != 1:
- raise FeatureLibError(
- "In reverse chaining single substitutions, "
- "only a single glyph or glyph class can be replaced",
- location,
- )
- if len(new) != 1:
- raise FeatureLibError(
- "In reverse chaining single substitutions, "
- 'the replacement (after "by") must be a single glyph '
- "or glyph class",
- location,
- )
- if num_lookups != 0:
- raise FeatureLibError(
- "Reverse chaining substitutions cannot call named lookups", location
- )
- glyphs = sorted(list(old[0].glyphSet()))
- replacements = sorted(list(new[0].glyphSet()))
- if len(replacements) == 1:
- replacements = replacements * len(glyphs)
- if len(glyphs) != len(replacements):
- raise FeatureLibError(
- 'Expected a glyph class with %d elements after "by", '
- "but found a glyph class with %d elements"
- % (len(glyphs), len(replacements)),
- location,
- )
- return self.ast.ReverseChainSingleSubstStatement(
- old_prefix, old_suffix, old, new, location=location
- )
- if len(old) > 1 and len(new) > 1:
- raise FeatureLibError(
- "Direct substitution of multiple glyphs by multiple glyphs "
- "is not supported",
- location,
- )
- # If there are remaining glyphs to parse, this is an invalid GSUB statement
- if len(new) != 0 or is_deletion:
- raise FeatureLibError("Invalid substitution statement", location)
- # GSUB lookup type 6: Chaining contextual substitution.
- rule = self.ast.ChainContextSubstStatement(
- old_prefix, old, old_suffix, lookups, location=location
- )
- return rule
- def parse_subtable_(self):
- assert self.is_cur_keyword_("subtable")
- location = self.cur_token_location_
- self.expect_symbol_(";")
- return self.ast.SubtableStatement(location=location)
- def parse_size_parameters_(self):
- # Parses a ``parameters`` statement used in ``size`` features. See
- # `section 8.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.b>`_.
- assert self.is_cur_keyword_("parameters")
- location = self.cur_token_location_
- DesignSize = self.expect_decipoint_()
- SubfamilyID = self.expect_number_()
- RangeStart = 0.0
- RangeEnd = 0.0
- if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0:
- RangeStart = self.expect_decipoint_()
- RangeEnd = self.expect_decipoint_()
- self.expect_symbol_(";")
- return self.ast.SizeParameters(
- DesignSize, SubfamilyID, RangeStart, RangeEnd, location=location
- )
- def parse_size_menuname_(self):
- assert self.is_cur_keyword_("sizemenuname")
- location = self.cur_token_location_
- platformID, platEncID, langID, string = self.parse_name_()
- return self.ast.FeatureNameStatement(
- "size", platformID, platEncID, langID, string, location=location
- )
- def parse_table_(self):
- assert self.is_cur_keyword_("table")
- location, name = self.cur_token_location_, self.expect_tag_()
- table = self.ast.TableBlock(name, location=location)
- self.expect_symbol_("{")
- handler = {
- "GDEF": self.parse_table_GDEF_,
- "head": self.parse_table_head_,
- "hhea": self.parse_table_hhea_,
- "vhea": self.parse_table_vhea_,
- "name": self.parse_table_name_,
- "BASE": self.parse_table_BASE_,
- "OS/2": self.parse_table_OS_2_,
- "STAT": self.parse_table_STAT_,
- }.get(name)
- if handler:
- handler(table)
- else:
- raise FeatureLibError(
- '"table %s" is not supported' % name.strip(), location
- )
- self.expect_symbol_("}")
- end_tag = self.expect_tag_()
- if end_tag != name:
- raise FeatureLibError(
- 'Expected "%s"' % name.strip(), self.cur_token_location_
- )
- self.expect_symbol_(";")
- return table
- def parse_table_GDEF_(self, table):
- statements = table.statements
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.is_cur_keyword_("Attach"):
- statements.append(self.parse_attach_())
- elif self.is_cur_keyword_("GlyphClassDef"):
- statements.append(self.parse_GlyphClassDef_())
- elif self.is_cur_keyword_("LigatureCaretByIndex"):
- statements.append(self.parse_ligatureCaretByIndex_())
- elif self.is_cur_keyword_("LigatureCaretByPos"):
- statements.append(self.parse_ligatureCaretByPos_())
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError(
- "Expected Attach, LigatureCaretByIndex, " "or LigatureCaretByPos",
- self.cur_token_location_,
- )
- def parse_table_head_(self, table):
- statements = table.statements
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.is_cur_keyword_("FontRevision"):
- statements.append(self.parse_FontRevision_())
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError("Expected FontRevision", self.cur_token_location_)
- def parse_table_hhea_(self, table):
- statements = table.statements
- fields = ("CaretOffset", "Ascender", "Descender", "LineGap")
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
- key = self.cur_token_.lower()
- value = self.expect_number_()
- statements.append(
- self.ast.HheaField(key, value, location=self.cur_token_location_)
- )
- if self.next_token_ != ";":
- raise FeatureLibError(
- "Incomplete statement", self.next_token_location_
- )
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError(
- "Expected CaretOffset, Ascender, " "Descender or LineGap",
- self.cur_token_location_,
- )
- def parse_table_vhea_(self, table):
- statements = table.statements
- fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
- key = self.cur_token_.lower()
- value = self.expect_number_()
- statements.append(
- self.ast.VheaField(key, value, location=self.cur_token_location_)
- )
- if self.next_token_ != ";":
- raise FeatureLibError(
- "Incomplete statement", self.next_token_location_
- )
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError(
- "Expected VertTypoAscender, "
- "VertTypoDescender or VertTypoLineGap",
- self.cur_token_location_,
- )
- def parse_table_name_(self, table):
- statements = table.statements
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.is_cur_keyword_("nameid"):
- statement = self.parse_nameid_()
- if statement:
- statements.append(statement)
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError("Expected nameid", self.cur_token_location_)
- def parse_name_(self):
- """Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_."""
- platEncID = None
- langID = None
- if self.next_token_type_ in Lexer.NUMBERS:
- platformID = self.expect_any_number_()
- location = self.cur_token_location_
- if platformID not in (1, 3):
- raise FeatureLibError("Expected platform id 1 or 3", location)
- if self.next_token_type_ in Lexer.NUMBERS:
- platEncID = self.expect_any_number_()
- langID = self.expect_any_number_()
- else:
- platformID = 3
- location = self.cur_token_location_
- if platformID == 1: # Macintosh
- platEncID = platEncID or 0 # Roman
- langID = langID or 0 # English
- else: # 3, Windows
- platEncID = platEncID or 1 # Unicode
- langID = langID or 0x0409 # English
- string = self.expect_string_()
- self.expect_symbol_(";")
- encoding = getEncoding(platformID, platEncID, langID)
- if encoding is None:
- raise FeatureLibError("Unsupported encoding", location)
- unescaped = self.unescape_string_(string, encoding)
- return platformID, platEncID, langID, unescaped
- def parse_stat_name_(self):
- platEncID = None
- langID = None
- if self.next_token_type_ in Lexer.NUMBERS:
- platformID = self.expect_any_number_()
- location = self.cur_token_location_
- if platformID not in (1, 3):
- raise FeatureLibError("Expected platform id 1 or 3", location)
- if self.next_token_type_ in Lexer.NUMBERS:
- platEncID = self.expect_any_number_()
- langID = self.expect_any_number_()
- else:
- platformID = 3
- location = self.cur_token_location_
- if platformID == 1: # Macintosh
- platEncID = platEncID or 0 # Roman
- langID = langID or 0 # English
- else: # 3, Windows
- platEncID = platEncID or 1 # Unicode
- langID = langID or 0x0409 # English
- string = self.expect_string_()
- encoding = getEncoding(platformID, platEncID, langID)
- if encoding is None:
- raise FeatureLibError("Unsupported encoding", location)
- unescaped = self.unescape_string_(string, encoding)
- return platformID, platEncID, langID, unescaped
- def parse_nameid_(self):
- assert self.cur_token_ == "nameid", self.cur_token_
- location, nameID = self.cur_token_location_, self.expect_any_number_()
- if nameID > 32767:
- raise FeatureLibError(
- "Name id value cannot be greater than 32767", self.cur_token_location_
- )
- platformID, platEncID, langID, string = self.parse_name_()
- return self.ast.NameRecord(
- nameID, platformID, platEncID, langID, string, location=location
- )
- def unescape_string_(self, string, encoding):
- if encoding == "utf_16_be":
- s = re.sub(r"\\[0-9a-fA-F]{4}", self.unescape_unichr_, string)
- else:
- unescape = lambda m: self.unescape_byte_(m, encoding)
- s = re.sub(r"\\[0-9a-fA-F]{2}", unescape, string)
- # We now have a Unicode string, but it might contain surrogate pairs.
- # We convert surrogates to actual Unicode by round-tripping through
- # Python's UTF-16 codec in a special mode.
- utf16 = tobytes(s, "utf_16_be", "surrogatepass")
- return tostr(utf16, "utf_16_be")
- @staticmethod
- def unescape_unichr_(match):
- n = match.group(0)[1:]
- return chr(int(n, 16))
- @staticmethod
- def unescape_byte_(match, encoding):
- n = match.group(0)[1:]
- return bytechr(int(n, 16)).decode(encoding)
- def find_previous(self, statements, class_):
- for previous in reversed(statements):
- if isinstance(previous, self.ast.Comment):
- continue
- elif isinstance(previous, class_):
- return previous
- else:
- # If we find something that doesn't match what we're looking
- # for, and isn't a comment, fail
- return None
- # Out of statements to look at
- return None
- def parse_table_BASE_(self, table):
- statements = table.statements
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.is_cur_keyword_("HorizAxis.BaseTagList"):
- horiz_bases = self.parse_base_tag_list_()
- elif self.is_cur_keyword_("HorizAxis.BaseScriptList"):
- horiz_scripts = self.parse_base_script_list_(len(horiz_bases))
- statements.append(
- self.ast.BaseAxis(
- horiz_bases,
- horiz_scripts,
- False,
- location=self.cur_token_location_,
- )
- )
- elif self.is_cur_keyword_("HorizAxis.MinMax"):
- base_script_list = self.find_previous(statements, ast.BaseAxis)
- if base_script_list is None:
- raise FeatureLibError(
- "MinMax must be preceded by BaseScriptList",
- self.cur_token_location_,
- )
- if base_script_list.vertical:
- raise FeatureLibError(
- "HorizAxis.MinMax must be preceded by HorizAxis statements",
- self.cur_token_location_,
- )
- base_script_list.minmax.append(self.parse_base_minmax_())
- elif self.is_cur_keyword_("VertAxis.BaseTagList"):
- vert_bases = self.parse_base_tag_list_()
- elif self.is_cur_keyword_("VertAxis.BaseScriptList"):
- vert_scripts = self.parse_base_script_list_(len(vert_bases))
- statements.append(
- self.ast.BaseAxis(
- vert_bases,
- vert_scripts,
- True,
- location=self.cur_token_location_,
- )
- )
- elif self.is_cur_keyword_("VertAxis.MinMax"):
- base_script_list = self.find_previous(statements, ast.BaseAxis)
- if base_script_list is None:
- raise FeatureLibError(
- "MinMax must be preceded by BaseScriptList",
- self.cur_token_location_,
- )
- if not base_script_list.vertical:
- raise FeatureLibError(
- "VertAxis.MinMax must be preceded by VertAxis statements",
- self.cur_token_location_,
- )
- base_script_list.minmax.append(self.parse_base_minmax_())
- elif self.cur_token_ == ";":
- continue
- def parse_table_OS_2_(self, table):
- statements = table.statements
- numbers = (
- "FSType",
- "TypoAscender",
- "TypoDescender",
- "TypoLineGap",
- "winAscent",
- "winDescent",
- "XHeight",
- "CapHeight",
- "WeightClass",
- "WidthClass",
- "LowerOpSize",
- "UpperOpSize",
- )
- ranges = ("UnicodeRange", "CodePageRange")
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.cur_token_type_ is Lexer.NAME:
- key = self.cur_token_.lower()
- value = None
- if self.cur_token_ in numbers:
- value = self.expect_number_()
- elif self.is_cur_keyword_("Panose"):
- value = []
- for i in range(10):
- value.append(self.expect_number_())
- elif self.cur_token_ in ranges:
- value = []
- while self.next_token_ != ";":
- value.append(self.expect_number_())
- elif self.is_cur_keyword_("Vendor"):
- value = self.expect_string_()
- statements.append(
- self.ast.OS2Field(key, value, location=self.cur_token_location_)
- )
- elif self.cur_token_ == ";":
- continue
- def parse_STAT_ElidedFallbackName(self):
- assert self.is_cur_keyword_("ElidedFallbackName")
- self.expect_symbol_("{")
- names = []
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_()
- if self.is_cur_keyword_("name"):
- platformID, platEncID, langID, string = self.parse_stat_name_()
- nameRecord = self.ast.STATNameStatement(
- "stat",
- platformID,
- platEncID,
- langID,
- string,
- location=self.cur_token_location_,
- )
- names.append(nameRecord)
- else:
- if self.cur_token_ != ";":
- raise FeatureLibError(
- f"Unexpected token {self.cur_token_} " f"in ElidedFallbackName",
- self.cur_token_location_,
- )
- self.expect_symbol_("}")
- if not names:
- raise FeatureLibError('Expected "name"', self.cur_token_location_)
- return names
- def parse_STAT_design_axis(self):
- assert self.is_cur_keyword_("DesignAxis")
- names = []
- axisTag = self.expect_tag_()
- if (
- axisTag not in ("ital", "opsz", "slnt", "wdth", "wght")
- and not axisTag.isupper()
- ):
- log.warning(f"Unregistered axis tag {axisTag} should be uppercase.")
- axisOrder = self.expect_number_()
- self.expect_symbol_("{")
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.COMMENT:
- continue
- elif self.is_cur_keyword_("name"):
- location = self.cur_token_location_
- platformID, platEncID, langID, string = self.parse_stat_name_()
- name = self.ast.STATNameStatement(
- "stat", platformID, platEncID, langID, string, location=location
- )
- names.append(name)
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError(
- f'Expected "name", got {self.cur_token_}', self.cur_token_location_
- )
- self.expect_symbol_("}")
- return self.ast.STATDesignAxisStatement(
- axisTag, axisOrder, names, self.cur_token_location_
- )
- def parse_STAT_axis_value_(self):
- assert self.is_cur_keyword_("AxisValue")
- self.expect_symbol_("{")
- locations = []
- names = []
- flags = 0
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- continue
- elif self.is_cur_keyword_("name"):
- location = self.cur_token_location_
- platformID, platEncID, langID, string = self.parse_stat_name_()
- name = self.ast.STATNameStatement(
- "stat", platformID, platEncID, langID, string, location=location
- )
- names.append(name)
- elif self.is_cur_keyword_("location"):
- location = self.parse_STAT_location()
- locations.append(location)
- elif self.is_cur_keyword_("flag"):
- flags = self.expect_stat_flags()
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError(
- f"Unexpected token {self.cur_token_} " f"in AxisValue",
- self.cur_token_location_,
- )
- self.expect_symbol_("}")
- if not names:
- raise FeatureLibError('Expected "Axis Name"', self.cur_token_location_)
- if not locations:
- raise FeatureLibError('Expected "Axis location"', self.cur_token_location_)
- if len(locations) > 1:
- for location in locations:
- if len(location.values) > 1:
- raise FeatureLibError(
- "Only one value is allowed in a "
- "Format 4 Axis Value Record, but "
- f"{len(location.values)} were found.",
- self.cur_token_location_,
- )
- format4_tags = []
- for location in locations:
- tag = location.tag
- if tag in format4_tags:
- raise FeatureLibError(
- f"Axis tag {tag} already " "defined.", self.cur_token_location_
- )
- format4_tags.append(tag)
- return self.ast.STATAxisValueStatement(
- names, locations, flags, self.cur_token_location_
- )
- def parse_STAT_location(self):
- values = []
- tag = self.expect_tag_()
- if len(tag.strip()) != 4:
- raise FeatureLibError(
- f"Axis tag {self.cur_token_} must be 4 " "characters",
- self.cur_token_location_,
- )
- while self.next_token_ != ";":
- if self.next_token_type_ is Lexer.FLOAT:
- value = self.expect_float_()
- values.append(value)
- elif self.next_token_type_ is Lexer.NUMBER:
- value = self.expect_number_()
- values.append(value)
- else:
- raise FeatureLibError(
- f'Unexpected value "{self.next_token_}". '
- "Expected integer or float.",
- self.next_token_location_,
- )
- if len(values) == 3:
- nominal, min_val, max_val = values
- if nominal < min_val or nominal > max_val:
- raise FeatureLibError(
- f"Default value {nominal} is outside "
- f"of specified range "
- f"{min_val}-{max_val}.",
- self.next_token_location_,
- )
- return self.ast.AxisValueLocationStatement(tag, values)
- def parse_table_STAT_(self, table):
- statements = table.statements
- design_axes = []
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.cur_token_type_ is Lexer.NAME:
- if self.is_cur_keyword_("ElidedFallbackName"):
- names = self.parse_STAT_ElidedFallbackName()
- statements.append(self.ast.ElidedFallbackName(names))
- elif self.is_cur_keyword_("ElidedFallbackNameID"):
- value = self.expect_number_()
- statements.append(self.ast.ElidedFallbackNameID(value))
- self.expect_symbol_(";")
- elif self.is_cur_keyword_("DesignAxis"):
- designAxis = self.parse_STAT_design_axis()
- design_axes.append(designAxis.tag)
- statements.append(designAxis)
- self.expect_symbol_(";")
- elif self.is_cur_keyword_("AxisValue"):
- axisValueRecord = self.parse_STAT_axis_value_()
- for location in axisValueRecord.locations:
- if location.tag not in design_axes:
- # Tag must be defined in a DesignAxis before it
- # can be referenced
- raise FeatureLibError(
- "DesignAxis not defined for " f"{location.tag}.",
- self.cur_token_location_,
- )
- statements.append(axisValueRecord)
- self.expect_symbol_(";")
- else:
- raise FeatureLibError(
- f"Unexpected token {self.cur_token_}", self.cur_token_location_
- )
- elif self.cur_token_ == ";":
- continue
- def parse_base_tag_list_(self):
- # Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
- assert self.cur_token_ in (
- "HorizAxis.BaseTagList",
- "VertAxis.BaseTagList",
- ), self.cur_token_
- bases = []
- while self.next_token_ != ";":
- bases.append(self.expect_script_tag_())
- self.expect_symbol_(";")
- return bases
- def parse_base_script_list_(self, count):
- assert self.cur_token_ in (
- "HorizAxis.BaseScriptList",
- "VertAxis.BaseScriptList",
- ), self.cur_token_
- scripts = [self.parse_base_script_record_(count)]
- while self.next_token_ == ",":
- self.expect_symbol_(",")
- scripts.append(self.parse_base_script_record_(count))
- self.expect_symbol_(";")
- return scripts
- def parse_base_script_record_(self, count):
- script_tag = self.expect_script_tag_()
- base_tag = self.expect_script_tag_()
- coords = [self.expect_number_() for i in range(count)]
- return script_tag, base_tag, coords
- def parse_base_minmax_(self):
- script_tag = self.expect_script_tag_()
- language = self.expect_language_tag_()
- min_coord = self.expect_number_()
- self.advance_lexer_()
- if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","):
- raise FeatureLibError(
- "Expected a comma between min and max coordinates",
- self.cur_token_location_,
- )
- max_coord = self.expect_number_()
- if self.next_token_ == ",": # feature tag...
- raise FeatureLibError(
- "Feature tags are not yet supported in BASE table",
- self.cur_token_location_,
- )
- return script_tag, language, min_coord, max_coord
- def parse_device_(self):
- result = None
- self.expect_symbol_("<")
- self.expect_keyword_("device")
- if self.next_token_ == "NULL":
- self.expect_keyword_("NULL")
- else:
- result = [(self.expect_number_(), self.expect_number_())]
- while self.next_token_ == ",":
- self.expect_symbol_(",")
- result.append((self.expect_number_(), self.expect_number_()))
- result = tuple(result) # make it hashable
- self.expect_symbol_(">")
- return result
- def is_next_value_(self):
- return (
- self.next_token_type_ is Lexer.NUMBER
- or self.next_token_ == "<"
- or self.next_token_ == "("
- )
- def parse_valuerecord_(self, vertical):
- if (
- self.next_token_type_ is Lexer.SYMBOL and self.next_token_ == "("
- ) or self.next_token_type_ is Lexer.NUMBER:
- number, location = (
- self.expect_number_(variable=True),
- self.cur_token_location_,
- )
- if vertical:
- val = self.ast.ValueRecord(
- yAdvance=number, vertical=vertical, location=location
- )
- else:
- val = self.ast.ValueRecord(
- xAdvance=number, vertical=vertical, location=location
- )
- return val
- self.expect_symbol_("<")
- location = self.cur_token_location_
- if self.next_token_type_ is Lexer.NAME:
- name = self.expect_name_()
- if name == "NULL":
- self.expect_symbol_(">")
- return self.ast.ValueRecord()
- vrd = self.valuerecords_.resolve(name)
- if vrd is None:
- raise FeatureLibError(
- 'Unknown valueRecordDef "%s"' % name, self.cur_token_location_
- )
- value = vrd.value
- xPlacement, yPlacement = (value.xPlacement, value.yPlacement)
- xAdvance, yAdvance = (value.xAdvance, value.yAdvance)
- else:
- xPlacement, yPlacement, xAdvance, yAdvance = (
- self.expect_number_(variable=True),
- self.expect_number_(variable=True),
- self.expect_number_(variable=True),
- self.expect_number_(variable=True),
- )
- if self.next_token_ == "<":
- xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (
- self.parse_device_(),
- self.parse_device_(),
- self.parse_device_(),
- self.parse_device_(),
- )
- allDeltas = sorted(
- [
- delta
- for size, delta in (xPlaDevice if xPlaDevice else ())
- + (yPlaDevice if yPlaDevice else ())
- + (xAdvDevice if xAdvDevice else ())
- + (yAdvDevice if yAdvDevice else ())
- ]
- )
- if allDeltas[0] < -128 or allDeltas[-1] > 127:
- raise FeatureLibError(
- "Device value out of valid range (-128..127)",
- self.cur_token_location_,
- )
- else:
- xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (None, None, None, None)
- self.expect_symbol_(">")
- return self.ast.ValueRecord(
- xPlacement,
- yPlacement,
- xAdvance,
- yAdvance,
- xPlaDevice,
- yPlaDevice,
- xAdvDevice,
- yAdvDevice,
- vertical=vertical,
- location=location,
- )
- def parse_valuerecord_definition_(self, vertical):
- # Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_)
- assert self.is_cur_keyword_("valueRecordDef")
- location = self.cur_token_location_
- value = self.parse_valuerecord_(vertical)
- name = self.expect_name_()
- self.expect_symbol_(";")
- vrd = self.ast.ValueRecordDefinition(name, value, location=location)
- self.valuerecords_.define(name, vrd)
- return vrd
- def parse_languagesystem_(self):
- assert self.cur_token_ == "languagesystem"
- location = self.cur_token_location_
- script = self.expect_script_tag_()
- language = self.expect_language_tag_()
- self.expect_symbol_(";")
- return self.ast.LanguageSystemStatement(script, language, location=location)
- def parse_feature_block_(self, variation=False):
- if variation:
- assert self.cur_token_ == "variation"
- else:
- assert self.cur_token_ == "feature"
- location = self.cur_token_location_
- tag = self.expect_tag_()
- vertical = tag in {"vkrn", "vpal", "vhal", "valt"}
- stylisticset = None
- cv_feature = None
- size_feature = False
- if tag in self.SS_FEATURE_TAGS:
- stylisticset = tag
- elif tag in self.CV_FEATURE_TAGS:
- cv_feature = tag
- elif tag == "size":
- size_feature = True
- if variation:
- conditionset = self.expect_name_()
- use_extension = False
- if self.next_token_ == "useExtension":
- self.expect_keyword_("useExtension")
- use_extension = True
- if variation:
- block = self.ast.VariationBlock(
- tag, conditionset, use_extension=use_extension, location=location
- )
- else:
- block = self.ast.FeatureBlock(
- tag, use_extension=use_extension, location=location
- )
- self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature)
- return block
- def parse_feature_reference_(self):
- assert self.cur_token_ == "feature", self.cur_token_
- location = self.cur_token_location_
- featureName = self.expect_tag_()
- self.expect_symbol_(";")
- return self.ast.FeatureReferenceStatement(featureName, location=location)
- def parse_featureNames_(self, tag):
- """Parses a ``featureNames`` statement found in stylistic set features.
- See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_.
- """
- assert self.cur_token_ == "featureNames", self.cur_token_
- block = self.ast.NestedBlock(
- tag, self.cur_token_, location=self.cur_token_location_
- )
- self.expect_symbol_("{")
- for symtab in self.symbol_tables_:
- symtab.enter_scope()
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- block.statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.is_cur_keyword_("name"):
- location = self.cur_token_location_
- platformID, platEncID, langID, string = self.parse_name_()
- block.statements.append(
- self.ast.FeatureNameStatement(
- tag, platformID, platEncID, langID, string, location=location
- )
- )
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError('Expected "name"', self.cur_token_location_)
- self.expect_symbol_("}")
- for symtab in self.symbol_tables_:
- symtab.exit_scope()
- self.expect_symbol_(";")
- return block
- def parse_cvParameters_(self, tag):
- # Parses a ``cvParameters`` block found in Character Variant features.
- # See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_.
- assert self.cur_token_ == "cvParameters", self.cur_token_
- block = self.ast.NestedBlock(
- tag, self.cur_token_, location=self.cur_token_location_
- )
- self.expect_symbol_("{")
- for symtab in self.symbol_tables_:
- symtab.enter_scope()
- statements = block.statements
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.is_cur_keyword_(
- {
- "FeatUILabelNameID",
- "FeatUITooltipTextNameID",
- "SampleTextNameID",
- "ParamUILabelNameID",
- }
- ):
- statements.append(self.parse_cvNameIDs_(tag, self.cur_token_))
- elif self.is_cur_keyword_("Character"):
- statements.append(self.parse_cvCharacter_(tag))
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError(
- "Expected statement: got {} {}".format(
- self.cur_token_type_, self.cur_token_
- ),
- self.cur_token_location_,
- )
- self.expect_symbol_("}")
- for symtab in self.symbol_tables_:
- symtab.exit_scope()
- self.expect_symbol_(";")
- return block
- def parse_cvNameIDs_(self, tag, block_name):
- assert self.cur_token_ == block_name, self.cur_token_
- block = self.ast.NestedBlock(tag, block_name, location=self.cur_token_location_)
- self.expect_symbol_("{")
- for symtab in self.symbol_tables_:
- symtab.enter_scope()
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- block.statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.is_cur_keyword_("name"):
- location = self.cur_token_location_
- platformID, platEncID, langID, string = self.parse_name_()
- block.statements.append(
- self.ast.CVParametersNameStatement(
- tag,
- platformID,
- platEncID,
- langID,
- string,
- block_name,
- location=location,
- )
- )
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError('Expected "name"', self.cur_token_location_)
- self.expect_symbol_("}")
- for symtab in self.symbol_tables_:
- symtab.exit_scope()
- self.expect_symbol_(";")
- return block
- def parse_cvCharacter_(self, tag):
- assert self.cur_token_ == "Character", self.cur_token_
- location, character = self.cur_token_location_, self.expect_any_number_()
- self.expect_symbol_(";")
- if not (0xFFFFFF >= character >= 0):
- raise FeatureLibError(
- "Character value must be between "
- "{:#x} and {:#x}".format(0, 0xFFFFFF),
- location,
- )
- return self.ast.CharacterStatement(character, tag, location=location)
- def parse_FontRevision_(self):
- # Parses a ``FontRevision`` statement found in the head table. See
- # `section 9.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_.
- assert self.cur_token_ == "FontRevision", self.cur_token_
- location, version = self.cur_token_location_, self.expect_float_()
- self.expect_symbol_(";")
- if version <= 0:
- raise FeatureLibError("Font revision numbers must be positive", location)
- return self.ast.FontRevisionStatement(version, location=location)
- def parse_conditionset_(self):
- name = self.expect_name_()
- conditions = {}
- self.expect_symbol_("{")
- while self.next_token_ != "}":
- self.advance_lexer_()
- if self.cur_token_type_ is not Lexer.NAME:
- raise FeatureLibError("Expected an axis name", self.cur_token_location_)
- axis = self.cur_token_
- if axis in conditions:
- raise FeatureLibError(
- f"Repeated condition for axis {axis}", self.cur_token_location_
- )
- if self.next_token_type_ is Lexer.FLOAT:
- min_value = self.expect_float_()
- elif self.next_token_type_ is Lexer.NUMBER:
- min_value = self.expect_number_(variable=False)
- if self.next_token_type_ is Lexer.FLOAT:
- max_value = self.expect_float_()
- elif self.next_token_type_ is Lexer.NUMBER:
- max_value = self.expect_number_(variable=False)
- self.expect_symbol_(";")
- conditions[axis] = (min_value, max_value)
- self.expect_symbol_("}")
- finalname = self.expect_name_()
- if finalname != name:
- raise FeatureLibError('Expected "%s"' % name, self.cur_token_location_)
- return self.ast.ConditionsetStatement(name, conditions)
- def parse_block_(
- self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None
- ):
- self.expect_symbol_("{")
- for symtab in self.symbol_tables_:
- symtab.enter_scope()
- statements = block.statements
- while self.next_token_ != "}" or self.cur_comments_:
- self.advance_lexer_(comments=True)
- if self.cur_token_type_ is Lexer.COMMENT:
- statements.append(
- self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
- )
- elif self.cur_token_type_ is Lexer.GLYPHCLASS:
- statements.append(self.parse_glyphclass_definition_())
- elif self.is_cur_keyword_("anchorDef"):
- statements.append(self.parse_anchordef_())
- elif self.is_cur_keyword_({"enum", "enumerate"}):
- statements.append(self.parse_enumerate_(vertical=vertical))
- elif self.is_cur_keyword_("feature"):
- statements.append(self.parse_feature_reference_())
- elif self.is_cur_keyword_("ignore"):
- statements.append(self.parse_ignore_())
- elif self.is_cur_keyword_("language"):
- statements.append(self.parse_language_())
- elif self.is_cur_keyword_("lookup"):
- statements.append(self.parse_lookup_(vertical))
- elif self.is_cur_keyword_("lookupflag"):
- statements.append(self.parse_lookupflag_())
- elif self.is_cur_keyword_("markClass"):
- statements.append(self.parse_markClass_())
- elif self.is_cur_keyword_({"pos", "position"}):
- statements.append(
- self.parse_position_(enumerated=False, vertical=vertical)
- )
- elif self.is_cur_keyword_("script"):
- statements.append(self.parse_script_())
- elif self.is_cur_keyword_({"sub", "substitute", "rsub", "reversesub"}):
- statements.append(self.parse_substitute_())
- elif self.is_cur_keyword_("subtable"):
- statements.append(self.parse_subtable_())
- elif self.is_cur_keyword_("valueRecordDef"):
- statements.append(self.parse_valuerecord_definition_(vertical))
- elif stylisticset and self.is_cur_keyword_("featureNames"):
- statements.append(self.parse_featureNames_(stylisticset))
- elif cv_feature and self.is_cur_keyword_("cvParameters"):
- statements.append(self.parse_cvParameters_(cv_feature))
- elif size_feature and self.is_cur_keyword_("parameters"):
- statements.append(self.parse_size_parameters_())
- elif size_feature and self.is_cur_keyword_("sizemenuname"):
- statements.append(self.parse_size_menuname_())
- elif (
- self.cur_token_type_ is Lexer.NAME
- and self.cur_token_ in self.extensions
- ):
- statements.append(self.extensions[self.cur_token_](self))
- elif self.cur_token_ == ";":
- continue
- else:
- raise FeatureLibError(
- "Expected glyph class definition or statement: got {} {}".format(
- self.cur_token_type_, self.cur_token_
- ),
- self.cur_token_location_,
- )
- self.expect_symbol_("}")
- for symtab in self.symbol_tables_:
- symtab.exit_scope()
- name = self.expect_name_()
- if name != block.name.strip():
- raise FeatureLibError(
- 'Expected "%s"' % block.name.strip(), self.cur_token_location_
- )
- self.expect_symbol_(";")
- def is_cur_keyword_(self, k):
- if self.cur_token_type_ is Lexer.NAME:
- if isinstance(k, type("")): # basestring is gone in Python3
- return self.cur_token_ == k
- else:
- return self.cur_token_ in k
- return False
- def expect_class_name_(self):
- self.advance_lexer_()
- if self.cur_token_type_ is not Lexer.GLYPHCLASS:
- raise FeatureLibError("Expected @NAME", self.cur_token_location_)
- return self.cur_token_
- def expect_cid_(self):
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.CID:
- return self.cur_token_
- raise FeatureLibError("Expected a CID", self.cur_token_location_)
- def expect_filename_(self):
- self.advance_lexer_()
- if self.cur_token_type_ is not Lexer.FILENAME:
- raise FeatureLibError("Expected file name", self.cur_token_location_)
- return self.cur_token_
- def expect_glyph_(self):
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.NAME:
- return self.cur_token_.lstrip("\\")
- elif self.cur_token_type_ is Lexer.CID:
- return "cid%05d" % self.cur_token_
- raise FeatureLibError("Expected a glyph name or CID", self.cur_token_location_)
- def check_glyph_name_in_glyph_set(self, *names):
- """Adds a glyph name (just `start`) or glyph names of a
- range (`start` and `end`) which are not in the glyph set
- to the "missing list" for future error reporting.
- If no glyph set is present, does nothing.
- """
- if self.glyphNames_:
- for name in names:
- if name in self.glyphNames_:
- continue
- if name not in self.missing:
- self.missing[name] = self.cur_token_location_
- def expect_markClass_reference_(self):
- name = self.expect_class_name_()
- mc = self.glyphclasses_.resolve(name)
- if mc is None:
- raise FeatureLibError(
- "Unknown markClass @%s" % name, self.cur_token_location_
- )
- if not isinstance(mc, self.ast.MarkClass):
- raise FeatureLibError(
- "@%s is not a markClass" % name, self.cur_token_location_
- )
- return mc
- def expect_tag_(self):
- self.advance_lexer_()
- if self.cur_token_type_ is not Lexer.NAME:
- raise FeatureLibError("Expected a tag", self.cur_token_location_)
- if len(self.cur_token_) > 4:
- raise FeatureLibError(
- "Tags cannot be longer than 4 characters", self.cur_token_location_
- )
- return (self.cur_token_ + " ")[:4]
- def expect_script_tag_(self):
- tag = self.expect_tag_()
- if tag == "dflt":
- raise FeatureLibError(
- '"dflt" is not a valid script tag; use "DFLT" instead',
- self.cur_token_location_,
- )
- return tag
- def expect_language_tag_(self):
- tag = self.expect_tag_()
- if tag == "DFLT":
- raise FeatureLibError(
- '"DFLT" is not a valid language tag; use "dflt" instead',
- self.cur_token_location_,
- )
- return tag
- def expect_symbol_(self, symbol):
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == symbol:
- return symbol
- raise FeatureLibError("Expected '%s'" % symbol, self.cur_token_location_)
- def expect_keyword_(self, keyword):
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword:
- return self.cur_token_
- raise FeatureLibError('Expected "%s"' % keyword, self.cur_token_location_)
- def expect_name_(self):
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.NAME:
- return self.cur_token_
- raise FeatureLibError("Expected a name", self.cur_token_location_)
- def expect_number_(self, variable=False):
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.NUMBER:
- return self.cur_token_
- if variable and self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "(":
- return self.expect_variable_scalar_()
- raise FeatureLibError("Expected a number", self.cur_token_location_)
- def expect_variable_scalar_(self):
- self.advance_lexer_() # "("
- scalar = VariableScalar()
- while True:
- if self.cur_token_type_ == Lexer.SYMBOL and self.cur_token_ == ")":
- break
- location, value = self.expect_master_()
- scalar.add_value(location, value)
- return scalar
- def expect_master_(self):
- location = {}
- while True:
- if self.cur_token_type_ is not Lexer.NAME:
- raise FeatureLibError("Expected an axis name", self.cur_token_location_)
- axis = self.cur_token_
- self.advance_lexer_()
- if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "="):
- raise FeatureLibError(
- "Expected an equals sign", self.cur_token_location_
- )
- value = self.expect_integer_or_float_()
- location[axis] = value
- if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":":
- # Lexer has just read the value as a glyph name. We'll correct it later
- break
- self.advance_lexer_()
- if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","):
- raise FeatureLibError(
- "Expected an comma or an equals sign", self.cur_token_location_
- )
- self.advance_lexer_()
- self.advance_lexer_()
- value = int(self.cur_token_[1:])
- self.advance_lexer_()
- return location, value
- def expect_any_number_(self):
- self.advance_lexer_()
- if self.cur_token_type_ in Lexer.NUMBERS:
- return self.cur_token_
- raise FeatureLibError(
- "Expected a decimal, hexadecimal or octal number", self.cur_token_location_
- )
- def expect_float_(self):
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.FLOAT:
- return self.cur_token_
- raise FeatureLibError(
- "Expected a floating-point number", self.cur_token_location_
- )
- def expect_integer_or_float_(self):
- if self.next_token_type_ == Lexer.FLOAT:
- return self.expect_float_()
- elif self.next_token_type_ is Lexer.NUMBER:
- return self.expect_number_()
- else:
- raise FeatureLibError(
- "Expected an integer or floating-point number", self.cur_token_location_
- )
- def expect_decipoint_(self):
- if self.next_token_type_ == Lexer.FLOAT:
- return self.expect_float_()
- elif self.next_token_type_ is Lexer.NUMBER:
- return self.expect_number_() / 10
- else:
- raise FeatureLibError(
- "Expected an integer or floating-point number", self.cur_token_location_
- )
- def expect_stat_flags(self):
- value = 0
- flags = {
- "OlderSiblingFontAttribute": 1,
- "ElidableAxisValueName": 2,
- }
- while self.next_token_ != ";":
- if self.next_token_ in flags:
- name = self.expect_name_()
- value = value | flags[name]
- else:
- raise FeatureLibError(
- f"Unexpected STAT flag {self.cur_token_}", self.cur_token_location_
- )
- return value
- def expect_stat_values_(self):
- if self.next_token_type_ == Lexer.FLOAT:
- return self.expect_float_()
- elif self.next_token_type_ is Lexer.NUMBER:
- return self.expect_number_()
- else:
- raise FeatureLibError(
- "Expected an integer or floating-point number", self.cur_token_location_
- )
- def expect_string_(self):
- self.advance_lexer_()
- if self.cur_token_type_ is Lexer.STRING:
- return self.cur_token_
- raise FeatureLibError("Expected a string", self.cur_token_location_)
- def advance_lexer_(self, comments=False):
- if comments and self.cur_comments_:
- self.cur_token_type_ = Lexer.COMMENT
- self.cur_token_, self.cur_token_location_ = self.cur_comments_.pop(0)
- return
- else:
- self.cur_token_type_, self.cur_token_, self.cur_token_location_ = (
- self.next_token_type_,
- self.next_token_,
- self.next_token_location_,
- )
- while True:
- try:
- (
- self.next_token_type_,
- self.next_token_,
- self.next_token_location_,
- ) = next(self.lexer_)
- except StopIteration:
- self.next_token_type_, self.next_token_ = (None, None)
- if self.next_token_type_ != Lexer.COMMENT:
- break
- self.cur_comments_.append((self.next_token_, self.next_token_location_))
- @staticmethod
- def reverse_string_(s):
- """'abc' --> 'cba'"""
- return "".join(reversed(list(s)))
- def make_cid_range_(self, location, start, limit):
- """(location, 999, 1001) --> ["cid00999", "cid01000", "cid01001"]"""
- result = list()
- if start > limit:
- raise FeatureLibError(
- "Bad range: start should be less than limit", location
- )
- for cid in range(start, limit + 1):
- result.append("cid%05d" % cid)
- return result
- def make_glyph_range_(self, location, start, limit):
- """(location, "a.sc", "d.sc") --> ["a.sc", "b.sc", "c.sc", "d.sc"]"""
- result = list()
- if len(start) != len(limit):
- raise FeatureLibError(
- 'Bad range: "%s" and "%s" should have the same length' % (start, limit),
- location,
- )
- rev = self.reverse_string_
- prefix = os.path.commonprefix([start, limit])
- suffix = rev(os.path.commonprefix([rev(start), rev(limit)]))
- if len(suffix) > 0:
- start_range = start[len(prefix) : -len(suffix)]
- limit_range = limit[len(prefix) : -len(suffix)]
- else:
- start_range = start[len(prefix) :]
- limit_range = limit[len(prefix) :]
- if start_range >= limit_range:
- raise FeatureLibError(
- "Start of range must be smaller than its end", location
- )
- uppercase = re.compile(r"^[A-Z]$")
- if uppercase.match(start_range) and uppercase.match(limit_range):
- for c in range(ord(start_range), ord(limit_range) + 1):
- result.append("%s%c%s" % (prefix, c, suffix))
- return result
- lowercase = re.compile(r"^[a-z]$")
- if lowercase.match(start_range) and lowercase.match(limit_range):
- for c in range(ord(start_range), ord(limit_range) + 1):
- result.append("%s%c%s" % (prefix, c, suffix))
- return result
- digits = re.compile(r"^[0-9]{1,3}$")
- if digits.match(start_range) and digits.match(limit_range):
- for i in range(int(start_range, 10), int(limit_range, 10) + 1):
- number = ("000" + str(i))[-len(start_range) :]
- result.append("%s%s%s" % (prefix, number, suffix))
- return result
- raise FeatureLibError('Bad range: "%s-%s"' % (start, limit), location)
- class SymbolTable(object):
- def __init__(self):
- self.scopes_ = [{}]
- def enter_scope(self):
- self.scopes_.append({})
- def exit_scope(self):
- self.scopes_.pop()
- def define(self, name, item):
- self.scopes_[-1][name] = item
- def resolve(self, name):
- for scope in reversed(self.scopes_):
- item = scope.get(name)
- if item:
- return item
- return None
|