Expand Glyphs multiple languages syntax when building UFOs - #1162
Expand Glyphs multiple languages syntax when building UFOs#1162tomekthewo wants to merge 5 commits into
Conversation
Glyphs lets a single `language` statement carry several tags and applies everything that follows to each of them, which the FEA spec does not allow, so feaLib rejects the generated features with `Expected ';'` at the second tag and the source cannot be compiled with fontmake at all. Expand the shorthand into one statement per tag on the way to the UFO, next to the variable feature conversion. The block is emitted once for the first tag and repeated for every remaining one. Named lookups cannot simply be duplicated - that would redefine them - so the repeats reference the lookup defined under the first tag, and glyph class definitions are likewise emitted only once; bare rules and lookupflags are repeated verbatim. Trailing keywords are kept on every tag. A statement that is already spec-compliant, or that cannot be parsed with confidence, is left untouched for feaLib to report. Fixes googlefonts#1109
The line scanner counted braces and matched statements on the raw text, so
anything inside a comment was taken for code. A `}` in a comment ended the
block early and bound the following rules to the last tag only -- output that
parses cleanly and ships the wrong `locl` mapping -- while a `{` swallowed the
feature's own closing brace. Statements with a trailing comment matched neither
the delimiter nor the language pattern, so they were absorbed into the previous
body and replayed, and a multi-tag statement with a trailing comment was not
expanded at all.
Blank out comments and string literals before matching, keeping column numbers
so the original lines are still what gets emitted, and carry a trailing comment
over to the first expanded statement.
Also handle three shapes that were treated as ordinary lines and so duplicated
rather than referenced or dropped: `lookup NAME useExtension {`, a lookup whose
brace is on the next line, and `markClass` definitions. Glyph class definitions
now run to their terminating semicolon instead of one line, so a multi-line
class no longer leaves orphan tokens behind.
variable_features.py already blanks comments and string literals before scanning feature code; use it instead of a second copy. Also compile the language tag pattern at module level like the others, and name the brace depth calculation the two scanners share.
| lines = stripped(fea) | ||
| assert lines.count("sub Scedilla by Scommaaccent;") == 2 | ||
| assert lines.count("language ROM;") == 1 | ||
| assert lines.count("language MOL;") == 1 |
There was a problem hiding this comment.
Test the whole output against an expectation. Counting matching lines does not guarantee the output is as expected.
There was a problem hiding this comment.
Good point, done in b354bdb — every case now compares the full expanded text, which doubles as documentation of what the expansion produces. It caught nothing new, but it is a much better regression net: the counting assertions would not have noticed the order of the emitted statements. The two round-trip tests assert the whole feature text as well.
Counting matching lines does not pin down the structure or the order of the result. Compare the full text instead, which also documents what the expansion produces.
|
So.. I’m not convinced a physical line is the right unit for this transformation. FEA defines line endings as ordinary whitespace therefore valid statements may span lines or share one. Unlike with the existing variable_features.py, expanding multiple languages cannot operate on locally delimited syntax because it needs to understand the type and scope of the following FEA statements, which means reconstructing statement boundaries, nesting, definitions and lookup references, all through a bunch of fragile regexes... E.g. this would be valid Glyphs syntax that the current code leaves unexpanded because another statement follows on the same line: language AZE CRT; sub i by idotaccent;Or take this one: language AZE CRT;
lookup idot { sub i by idotaccent; } idot;
sub Scedilla by Scommaaccent;here the scanner can't detect a lookup's opening and closing braces that occur on the same line (brace depth is 0) and treats the lookup as continuing to the end of the language section, so There are more of these all stemming from conflating line with statements. E.g. Fixing these (or even just rejecting them with a loud error to avoid silent wrong output) risks turning this into a partial second FEA parser inside glyphsLib, needing knowledge of lookup, class/markClass semantics. Besides, this operates on incomplete feature text before includes gets resolved, so there's that too. So I’m not yet convinced glyphsLib can transform the extension safely without duplicating parts of feaLib. And maybe feaLib would not necessarily be the wrong layer, despite not being official FEA... I honestly don't know, sorry for being inconclusive. |
|
(and then there is the question of also needing to implement this in fontc/fea-rs which just hit 1.0 this week.. at some point I would rather stop introducing new features in both compilers) |
|
You're right, and the line-based scanning isn't salvageable. Thanks a lot for writing the counterexamples out — all four reproduced exactly as you described, including the two that fail silently. I've rewritten the expansion on feaLib's own
The part I'd gently push back on is the "partial second FEA parser" worry — tokenizing is precisely what removes the need for one.
with the scope ending at the next
On includes: agreed, they can't be replayed, so a multi-tag scope containing one is left alone entirely. The shorthand is invalid FEA, so feaLib then reports it — the failure stays loud instead of becoming a silently partial expansion. What this does not address is your second comment about fontc parity, because it still fails unless the same expansion is implemented there too. The portability argument in my PR description ("the UFO gets plain FEA, so any compiler reads it") doesn't cover fontc, and I shouldn't have framed it that broadly. The parity cost you're describing is real and this patch doesn't remove it. One thing that might change the calculus: Glyphs 4 emits it from automatic feature generation. I took two of our Glyphs 3 sources (appVersion 3517 and 3526), opened and saved each in Glyphs 4 (appVersion 4004), and in both the automatic # before, appVersion 3517
language AZE;
lookup locl_latn_0 {
sub i by idotaccent;
} locl_latn_0;
language CRT;
lookup locl_latn_0;
language KAZ;
lookup locl_latn_0;
...
# after, appVersion 4004
language AZE CRT KAZ TAT TRK;
lookup idotaccent {
sub i by idotaccent;
} idotaccent;No edits by the designer, the feature still flagged As I read it, there are three ways this could go:
I can only guess which one you'd prefer. But if Glyphs emits the shorthand on save, everything that reads Happy to push (1) here, or cut it back to (2) — whichever you'd merge. For what it's worth on the approach itself: I ran the tokenizer over ~1100 feature code blocks from 35 production |
thanks, since you have done it already and sounds like it's better than the line-base scanner, you may as well push it
oh, great. That means we can't dismiss this and will have to support it in fontc anyway (googlefonts/fontc#2126). And maybe in feaLib as well, which would make the current PR redundant. |
|
We documented our fea additions. There is a branch for each topic: https://github.com/schriftgestalt/feature_file_workshops (This is not meant as a request to add it to the spec as is, just to have written it down properly.) |
|
Thanks Georg, I briefly checked Adobe's upstream repo the other day but couldn't find your multi-lang extension proposal. I found it now here: adobe-type-tools/feature_file_workshops#8 |
Fixes #1109.
Glyphs lets a single
languagestatement carry several tags and applies everything that follows to each of them:The FEA spec allows exactly one tag, so feaLib stops at the second one and the UFOs glyphsLib writes cannot be compiled at all —
<features>:N:14: Expected ';', column 14 being whereCRTstarts. The syntax is documented in the Glyphs Handbook.This expands the shorthand into one statement per tag in
_to_ufo_features, right next to the variable feature conversion. Doing it there rather than in feaLib means what reaches the UFO is plain spec-compliant FEA that any compiler reads — feaLib, fea-rs and makeotf alike; expanding downstream would leave the UFO itself non-portable.The block is emitted once for the first tag and repeated for every remaining one. Named lookups cannot simply be duplicated — that would redefine them — so the repeats reference the lookup defined under the first tag:
Glyph class and
markClassdefinitions are likewise emitted only once; bare rules andlookupflags are repeated verbatim, which is what the shorthand means. Trailing keywords (exclude_dfltand friends) are kept on every tag. A statement that is already spec-compliant, or that cannot be parsed with confidence, is left untouched for feaLib to report rather than guessed at.Scanning is done on a copy with comments and string literals blanked out — reusing
_blank_comments_and_stringsfromvariable_features.py— so a brace, semicolon or keyword inside a comment is never taken for code. That matters more than it sounds: a}in a comment would otherwise end the block early and silently bind the following rules to the last tag only, producing output that parses cleanly and ships the wrongloclmapping.variable_features.pywas the model for the placement, the shape of the module and the scanning approach.One-way. The expansion is not reversed on the way back, so
.glyphs→ UFO →.glyphsyields the expanded form rather than the original statement.test_expansion_does_not_round_trippins this down. UFO →.glyphs→ UFO is unaffected, since the original feature text is recovered fromORIGINAL_FEATURE_CODE_KEY. Happy to restore the shorthand into_glyphs_featuresinstead if you would rather keep it lossless.18 tests in
tests/builder/multi_language_test.py, including one throughto_ufosand one through the round trip. Full suite: 1355 passed, 64 skipped, 4 xfailed.black --checkandflake8overLib testsare clean.