Skip to content

Commit

Permalink
Add feature generator
Browse files Browse the repository at this point in the history
  • Loading branch information
mishamyrt committed Nov 24, 2020
1 parent 8a9be31 commit 648c176
Show file tree
Hide file tree
Showing 19 changed files with 566 additions and 200 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
build/
instance_ufo/
master_ufo/
Lilex (Autosaved).glyphs
Lilex (Autosaved).glyphs
generator/__pycache__
4 changes: 4 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[MASTER]
disable=
C0115, # missing-class-docstring
C0116, # missing-function-docstring
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.autoComplete.extraPaths": ["./builder"]
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.

## [1.200] — Unreleased

Added feature generator.

## [1.100] — November 21, 2020

Added `#{` `#[` `#(` `__(` `!!`
Expand Down Expand Up @@ -51,3 +55,5 @@ IBM Plex Mono version: 3.000
[1.000]: https://github.com/mishamyrt/Lilex/releases/tag/1.000

[1.100]: https://github.com/mishamyrt/Lilex/releases/tag/1.100

[1.200]: https://github.com/mishamyrt/Lilex/compare/1.100...master
396 changes: 197 additions & 199 deletions Lilex.glyphs

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.PHONY: regenerate

BUILD_DIRECTORY := "build"
GLYPHS_FILE := "Lilex.glyphs"
VF_FILE := "$(BUILD_DIRECTORY)/variable_ttf/Lilex-VF.ttf"
Expand All @@ -8,6 +10,9 @@ else
OS := "$(shell lsb_release -si)"
endif

regenerate:
python3 generator/main.py

all: ttf otf variable_ttf

ttf: raw_ttf
Expand Down
1 change: 1 addition & 0 deletions classes/numbers_hex.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@numbers_dflt a b c d e f A B C D E F
3 changes: 3 additions & 0 deletions features/calt/colon.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Digit color align
# Example: 10:20
sub @numbers_dflt colon' @numbers_dflt by colon.valign;
4 changes: 4 additions & 0 deletions features/calt/multiply.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Multiply align
# Examples: 0xDEADBEEF, 10x2
sub zero x' @numbers_hex by multiply;
sub @numbers_dflt x' @numbers_dflt by multiply;
3 changes: 3 additions & 0 deletions features/ss01.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# simple lowercase a
sub @lca_dflt by @lca_alt1;
sub @lca_cyrl_dflt by @lca_cyrl_alt1;
2 changes: 2 additions & 0 deletions features/ss02.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# simple lowercase g
sub @lcg_dflt by @lcg_alt1;
2 changes: 2 additions & 0 deletions features/ss03.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# slashed number zero
sub zero by zero.alt01;
2 changes: 2 additions & 0 deletions features/ss04.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# plain number zero
sub zero by zero.alt02;
2 changes: 2 additions & 0 deletions features/ss05.fea
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# alternate lowercase eszett
sub germandbls by germandbls.alt01;
21 changes: 21 additions & 0 deletions generator/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

'''
Class helpers
'''

from typing import List

denied_symbols = [
'.notdef',
'NULL',
'CR'
]

def is_space (symbol: str) -> bool:
return \
'.liga' in symbol or \
symbol in denied_symbols or \
'space' in symbol

def get_not_space_glyphs (symbols: List[str]) -> List[str]:
return list(filter(lambda x: not is_space(x), symbols))
13 changes: 13 additions & 0 deletions generator/fea.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'''
fea files helpers
'''

def read_file (path: str) -> str:
with open(path) as file:
return file.read()

def read_feature (name: str) -> str:
return read_file(f'./features/{name}.fea')

def read_class (name: str) -> str:
return read_file(f'./classes/{name}.fea')
91 changes: 91 additions & 0 deletions generator/glyphs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Glyphs.app format processor
"""

import re
from typing import Union, List, Dict, Tuple

convert_nl = lambda x: x.replace('\n', "\\012")

# Determine minimum indentation (first line doesn't count):
class GlyphsFile:
glyphs: List[str] = []
__raw: str = ''
__path: str
__features: Dict[str, str]
__classes: Dict[str, str]

def __init__ (self, path: str):
with open(path) as file:
for line in file:
self.__raw += f'{line}'
name = self.__find_glyph_name(line)
if name is not None:
self.glyphs.append(name)
self.__path = path
self.__features = self.__read_list('features')
self.__classes = self.__read_list('classes')

def flush (self):
file = open(self.__path, 'w')
file.write(self.__raw)

def set_feature (self, name, value):
self.__features[name] = f'"{convert_nl(value)}"'
self.__write_list('features', self.__features)

def set_class (self, name, value):
self.__classes[name] = f'"{convert_nl(value)}"'
self.__write_list('classes', self.__classes)

@property
def ligatures(self):
return list(
map(lambda x: x.replace('.liga', ''),
filter(lambda x: x.endswith('.liga'), self.glyphs)))

def __find_glyph_name(self, line: str) -> Union[str, None]:
result = re.match(r'glyphname = (.*);', line)
if result is None:
return None
return result.group(1)

def __read_fields (self, field: str, text_slice: str) -> List[str]:
pattern = re.compile(rf'{field} = (.*)', re.M)
values: List[str] = []
for value_match in pattern.finditer(text_slice):
values.append(value_match.group(1)[:-1])
return values

def __find_definition_index (self, field: str, prefix = '') -> Tuple:
start_index = self.__raw.index(f"{field} = {prefix}")
end_index = self.__raw.index(');', start_index)
return (start_index, end_index + 2)

def __write_list (self, field: str, value: Dict[str, str]):
start, end = self.__find_definition_index(field)
list_str = ''
for name, code in value.items():
list_str += (
'{\n'
f'code = {code};\n'
f'name = {name};\n'
'},\n'
)
self.__raw = ''.join((
self.__raw[:start],
f'{field} = (\n',
f'{list_str[:-2]}\n'
');',
self.__raw[end:]
))

def __read_list (self, field: str):
start, end = self.__find_definition_index(field, prefix='(')
text_slice = self.__raw[start:end + 2]
keys = self.__read_fields('name', text_slice)
values = self.__read_fields('code', text_slice)

if len(keys) != len(values):
raise Exception('Keys and values count differs')
return dict(zip(keys, values))
164 changes: 164 additions & 0 deletions generator/ligatures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
'''
Ligatures feature generator
'''

from typing import List

ignore_exceptions = {
'slash_asterisk': [
"slash' asterisk slash",
"asterisk slash' asterisk"
],
'asterisk_slash': [
"slash asterisk' slash",
"asterisk' slash asterisk"
],
'asterisk_asterisk': [
"slash asterisk' asterisk",
"asterisk' asterisk slash"
],
"asterisk_asterisk_asterisk": [
"slash asterisk' asterisk asterisk",
"asterisk' asterisk asterisk slash",
],
"colon_colon": [
"colon' colon [less greater]",
"[less greater] colon' colon"
],
"less_less": ["less' less [asterisk plus dollar]"],
"equal_equal": [
"bracketleft equal' equal",
"equal' equal bracketright",
"equal [colon exclam] equal' equal",
"[less greater bar slash] equal' equal",
"equal' equal [less greater bar slash]",
"equal' equal [colon exclam] equal"
],
"equal_equal_equal": [
"equal [colon exclam] equal' equal equal",
"[less greater bar slash] equal' equal equal",
"equal' equal equal [less greater bar slash]",
"equal' equal equal [colon exclam] equal",
"bracketleft equal' equal equal",
"equal' equal equal bracketright"
],
"colon_equal": ["equal colon' equal"],
"exclam_equal": ["equal exclam' equal"],
"exclam_equal_equal": ["equal exclam' equal equal"],
"less_equal": [
"equal less' equal",
"less' equal [less greater bar colon exclam slash]"
],
"greater_equal": [
"equal greater' equal",
"greater' equal [less greater bar colon exclam slash]"
],
"greater_greater": [
"[hyphen equal] greater' greater",
"greater' greater hyphen",
"[asterisk plus dollar] greater' greater",
"greater' greater equal [equal less greater bar colon exclam slash]"
],
"bar_bar": [
"[hyphen equal] bar' bar",
"bar' bar hyphen",
"bar' bar equal [equal less greater bar colon exclam slash]"
],
"slash_slash": [
"equal slash' slash",
"slash' slash equal"
],
"hyphen_hyphen": [
"[less greater bar] hyphen' hyphen",
"hyphen' hyphen [less greater bar]"
],
}

ignore_prefixes = {
'parenleft question': [
'colon',
'equal'
'exclaim'
],
'less question': ['equal'],
'parenleft question less': [
'equal',
'exclaim'
],
}

# Replacemnt ignore templates map
# ignore sub
ignore_templates = {
2: [
"1 1' 2",
"1' 2 2"
],
3: [
"1 1' 2 3",
"1' 2 3 3"
],
4: [
"1 1' 2 3 4",
"1' 2 3 4 4"
]
}

# Replacemnt templates map
# sub
replace_templates = {
2: [
"LIG 2' by 1_2.liga",
"1' 2 by LIG"
],
3: [
"LIG LIG 3' by 1_2_3.liga",
"LIG 2' 3 by LIG",
"1' 2 3 by LIG"
],
4: [
"LIG LIG LIG 4' by 1_2_3_4.liga",
"LIG LIG 3' 4 by LIG",
"LIG 2' 3 4 by LIG",
"1' 2 3 4 by LIG"
]
}

def render_statements (statements: List[str], prefix: str) -> str:
return '\n'.join(map(lambda x: f' {prefix} {x};', statements))

def render_template (template: str, glyphs: List[str]) -> str:
for i, _ in enumerate(glyphs):
template = template.replace(str(i + 1), glyphs[i])
return template

def render_lookup (replace: List[str], ignore: List[str], glyphs: List[str]) -> str:
name = '_'.join(glyphs)
template = (
f"lookup {name}" + " { \n"
f"{render_statements(ignore, 'ignore sub')}"
f"{render_statements(replace, 'sub')}"
"\n} " + f"{name};"
)
return render_template(template, glyphs)

def get_ignore_prefixes(name: str, length: int) -> List[str]:
ignores: List[str] = []
tail = ''
for i in range(length - 1):
tail += f' {i + 1}'
for statement, starts in ignore_prefixes.items():
for start in starts:
if name.startswith(start):
ignores.append(f"{statement} 1' {tail}")
return ignores

def render_ligature_lookups (ligatures: List[str]) -> str:
result = ""
for name in ligatures:
glyphs = name.split('_')
lenght = len(glyphs)
ignores = ignore_templates[lenght] + get_ignore_prefixes(name, lenght)
replaces = replace_templates[lenght]
result += render_lookup(replaces, ignores, glyphs) + "\n"
return result
Loading

0 comments on commit 648c176

Please sign in to comment.