├── .gitignore ├── CHANGELOG.md ├── DEFINITIONS.md ├── FilterListManager.glyphsPlugin └── Contents │ ├── Info.plist │ ├── MacOS │ └── plugin │ └── Resources │ ├── CustomFilter.plist │ └── plugin.py ├── LICENSE ├── README.md ├── TOOLS.md └── tools ├── exportfilters.py ├── glformatter.py └── glinter.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # PyCharm files 107 | .idea 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### 0.6.0 4 | 5 | - updated for new Glyphs application plugin format 6 | - Python 3 support 7 | 8 | ### 0.5.0 9 | 10 | - German menu item localization (thanks JM!) 11 | 12 | ### 0.4.1 13 | 14 | - removed undefined German string localizations for Edit menu items 15 | 16 | ### 0.4.0 17 | 18 | - initial release 19 | - added automated backup of user's CustomFilters.plist file at the time of plugin install to prevent loss of custom filters 20 | - added exception handling for local/remote definition file parsing 21 | - refactored directory and file paths to module level constants 22 | - eliminated unnecessary variable definitions 23 | - added url decoding for filter list name parsing from remote file names 24 | - added documentation 25 | 26 | See repository releases for pre-release changelog documentation prior to the v0.4.0 release 27 | -------------------------------------------------------------------------------- /DEFINITIONS.md: -------------------------------------------------------------------------------- 1 | ## Filter List Definition File Spec 2 | 3 | The filter list definition file is a text file that is formatted with a newline-delimited list of glyph names to include in the new Glyphs filter list. Each definition file defines a single filter list. You can use one or more filter list definition files simultaneously during updates with the FLM plugin and these settings files can be stored on your local macOS system or on a remote web server that is accessible through HTTP GET requests. 4 | 5 | ### Filter list name 6 | 7 | Define the filter list name in the base name of the definition file. You may include spaces, hyphens, and parentheses in these names. 8 | 9 | The following are examples of how filenames are mapped to filter list names in the Glyphs application user interface: 10 | 11 | - `Esperanto.txt` = `Esperanto` 12 | - `Basic Latin.txt` = `Basic Latin` 13 | - `MES-1.txt` = `MES-1` 14 | - `Serbian (Latin).txt` = `Serbian (Latin)` 15 | 16 | The plugin provides support for empty lines in the body of the text file to improve readability and organization of the document. Use as many empty lines as you need in the definition file to format it however it is most helpful for sharing and maintenance of the definitions. 17 | 18 | ### Glyph names 19 | 20 | Format your filter list definition file with one glyph name per line. This is called newline-delimited format. Use either Unicode hexadecimal code point style names (e.g, `uni00F6`) or AGL-style nice names (e.g., `odieresis`) as supported by the Glyphs application. 21 | 22 | ### Comment line support 23 | 24 | Comment lines (i.e., lines in the definition file that include text that is not intended for use as part of the glyph name list) are indicated with either `#` or `//` at the beginning of the line. Include one of these two comment indicators at the beginning of each line that is intended to be a comment line in your definition file document. It is not mandatory to use comment indicators on empty lines that are used to format your document (e.g., empty lines between glyph name definitions, see example below). 25 | 26 | Note: Comment indicators can be added to the start of one or more lines that includes glyph names in order to exclude these lines from the list of glyphs used in a filter. This can be helpful if you need to make periodic modifications to sets of glyph names in a given list. Simply place a `#` or `//` at the start of the glyph name line to exclude it from the filter list definition. 27 | 28 | ### Example 29 | 30 | The following is an example of a valid filter list definition file that includes comment and empty line formatting: 31 | 32 | ##### Filename: `Esperanto.txt` 33 | 34 | ``` 35 | // ------------------------------------------------- 36 | // Esperanto Glyphs filter list definition file 37 | // Copyright 2018, Some Person 38 | // MIT License 39 | // ------------------------------------------------- 40 | 41 | # U+0108 42 | Ccircumflex 43 | 44 | # U+0109 45 | ccircumflex 46 | 47 | # U+011C 48 | Gcircumflex 49 | 50 | # U+011D 51 | gcircumflex 52 | 53 | # U+0124 54 | Hcircumflex 55 | 56 | # U+0125 57 | hcircumflex 58 | 59 | # U+0134 60 | Jcircumflex 61 | 62 | # U+0135 63 | jcircumflex 64 | 65 | # U+015C 66 | Scircumflex 67 | 68 | # U+015D 69 | scircumflex 70 | 71 | # U+016C 72 | Ubreve 73 | 74 | # U+016D 75 | ubreve 76 | 77 | ``` 78 | 79 | The same definition file could be written in a much more concise format like this: 80 | 81 | ##### Filename: `Esperanto.txt` 82 | 83 | ``` 84 | Ccircumflex 85 | ccircumflex 86 | Gcircumflex 87 | gcircumflex 88 | Hcircumflex 89 | hcircumflex 90 | Jcircumflex 91 | jcircumflex 92 | Scircumflex 93 | scircumflex 94 | Ubreve 95 | ubreve 96 | ``` 97 | 98 | The filter list that is created in the Glyphs application does not differ between the two definition file examples above. Use the approach that works best for you. 99 | -------------------------------------------------------------------------------- /FilterListManager.glyphsPlugin/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | org.sourcefoundry.FilterListManager 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | FilterListManager 15 | CFBundleShortVersionString 16 | 0.5.1 17 | CFBundleVersion 18 | 51 19 | UpdateFeedURL 20 | https://raw.githubusercontent.com/source-foundry/FilterListManager/master/FilterListManager.glyphsPlugin/Contents/Info.plist 21 | productPageURL 22 | https://github.com/source-foundry/FilterListManager 23 | productReleaseNotes 24 | Changelog: https://github.com/source-foundry/FilterListManager/blob/master/CHANGELOG.md 25 | NSHumanReadableCopyright 26 | Copyright 2018 Christopher Simpkins, Apache License 2.0 27 | NSPrincipalClass 28 | FilterListManager 29 | 30 | 31 | -------------------------------------------------------------------------------- /FilterListManager.glyphsPlugin/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/FilterListManager/c3c50fd2c0da2b51140804bf3c5f523465049c26/FilterListManager.glyphsPlugin/Contents/MacOS/plugin -------------------------------------------------------------------------------- /FilterListManager.glyphsPlugin/Contents/Resources/CustomFilter.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | name 7 | Incompatible Master 8 | predicate 9 | mastersCompatible == 0 10 | 11 | 12 | list 13 | 14 | Eth 15 | eth 16 | Lslash 17 | lslash 18 | Scaron 19 | scaron 20 | Yacute 21 | yacute 22 | Thorn 23 | thorn 24 | Zcaron 25 | zcaron 26 | onehalf 27 | onequarter 28 | onesuperior 29 | threequarters 30 | threesuperior 31 | twosuperior 32 | brokenbar 33 | minus 34 | multiply 35 | space 36 | exclam 37 | quotedbl 38 | numbersign 39 | dollar 40 | percent 41 | ampersand 42 | quotesingle 43 | parenleft 44 | parenright 45 | asterisk 46 | plus 47 | comma 48 | hyphen 49 | period 50 | slash 51 | zero 52 | one 53 | two 54 | three 55 | four 56 | five 57 | six 58 | seven 59 | eight 60 | nine 61 | colon 62 | semicolon 63 | less 64 | equal 65 | greater 66 | question 67 | at 68 | A 69 | B 70 | C 71 | D 72 | E 73 | F 74 | G 75 | H 76 | I 77 | J 78 | K 79 | L 80 | M 81 | N 82 | O 83 | P 84 | Q 85 | R 86 | S 87 | T 88 | U 89 | V 90 | W 91 | X 92 | Y 93 | Z 94 | bracketleft 95 | backslash 96 | bracketright 97 | asciicircum 98 | underscore 99 | grave 100 | a 101 | b 102 | c 103 | d 104 | e 105 | f 106 | g 107 | h 108 | i 109 | j 110 | k 111 | l 112 | m 113 | n 114 | o 115 | p 116 | q 117 | r 118 | s 119 | t 120 | u 121 | v 122 | w 123 | x 124 | y 125 | z 126 | braceleft 127 | bar 128 | braceright 129 | asciitilde 130 | Adieresis 131 | Aring 132 | Ccedilla 133 | Eacute 134 | Ntilde 135 | Odieresis 136 | Udieresis 137 | aacute 138 | agrave 139 | acircumflex 140 | adieresis 141 | atilde 142 | aring 143 | ccedilla 144 | eacute 145 | egrave 146 | ecircumflex 147 | edieresis 148 | iacute 149 | igrave 150 | icircumflex 151 | idieresis 152 | ntilde 153 | oacute 154 | ograve 155 | ocircumflex 156 | odieresis 157 | otilde 158 | uacute 159 | ugrave 160 | ucircumflex 161 | udieresis 162 | dagger 163 | degree 164 | cent 165 | sterling 166 | section 167 | bullet 168 | paragraph 169 | germandbls 170 | registered 171 | copyright 172 | trademark 173 | acute 174 | dieresis 175 | notequal 176 | AE 177 | Oslash 178 | infinity 179 | plusminus 180 | lessequal 181 | greaterequal 182 | yen 183 | micro 184 | partialdiff 185 | summation 186 | product 187 | pi 188 | integral 189 | ordfeminine 190 | ordmasculine 191 | Omega 192 | ae 193 | oslash 194 | questiondown 195 | exclamdown 196 | logicalnot 197 | radical 198 | florin 199 | approxequal 200 | Delta 201 | guillemetleft 202 | guillemetright 203 | ellipsis 204 | Agrave 205 | Atilde 206 | Otilde 207 | OE 208 | oe 209 | endash 210 | emdash 211 | quotedblleft 212 | quotedblright 213 | quoteleft 214 | quoteright 215 | divide 216 | lozenge 217 | ydieresis 218 | Ydieresis 219 | fraction 220 | euro 221 | guilsinglleft 222 | guilsinglright 223 | fi 224 | fl 225 | daggerdbl 226 | periodcentered 227 | quotesinglbase 228 | quotedblbase 229 | perthousand 230 | Acircumflex 231 | Ecircumflex 232 | Aacute 233 | Edieresis 234 | Egrave 235 | Iacute 236 | Icircumflex 237 | Idieresis 238 | Igrave 239 | Oacute 240 | Ocircumflex 241 | Ograve 242 | Uacute 243 | Ucircumflex 244 | Ugrave 245 | idotless 246 | circumflex 247 | tilde 248 | macron 249 | breve 250 | dotaccent 251 | ring 252 | cedilla 253 | hungarumlaut 254 | ogonek 255 | caron 256 | 257 | name 258 | Mac Roman 259 | 260 | 261 | list 262 | 263 | space 264 | exclam 265 | quotedbl 266 | numbersign 267 | dollar 268 | percent 269 | ampersand 270 | quotesingle 271 | parenleft 272 | parenright 273 | asterisk 274 | plus 275 | comma 276 | hyphen 277 | period 278 | slash 279 | zero 280 | one 281 | two 282 | three 283 | four 284 | five 285 | six 286 | seven 287 | eight 288 | nine 289 | colon 290 | semicolon 291 | less 292 | equal 293 | greater 294 | question 295 | at 296 | A 297 | B 298 | C 299 | D 300 | E 301 | F 302 | G 303 | H 304 | I 305 | J 306 | K 307 | L 308 | M 309 | N 310 | O 311 | P 312 | Q 313 | R 314 | S 315 | T 316 | U 317 | V 318 | W 319 | X 320 | Y 321 | Z 322 | bracketleft 323 | backslash 324 | bracketright 325 | asciicircum 326 | underscore 327 | grave 328 | a 329 | b 330 | c 331 | d 332 | e 333 | f 334 | g 335 | h 336 | i 337 | j 338 | k 339 | l 340 | m 341 | n 342 | o 343 | p 344 | q 345 | r 346 | s 347 | t 348 | u 349 | v 350 | w 351 | x 352 | y 353 | z 354 | braceleft 355 | bar 356 | braceright 357 | asciitilde 358 | euro 359 | quotesinglbase 360 | florin 361 | quotedblbase 362 | ellipsis 363 | dagger 364 | daggerdbl 365 | circumflex 366 | perthousand 367 | Scaron 368 | guilsinglleft 369 | OE 370 | Zcaron 371 | quoteleft 372 | quoteright 373 | quotedblleft 374 | quotedblright 375 | bullet 376 | endash 377 | emdash 378 | tilde 379 | trademark 380 | scaron 381 | guilsinglright 382 | oe 383 | zcaron 384 | Ydieresis 385 | exclamdown 386 | cent 387 | sterling 388 | currency 389 | yen 390 | brokenbar 391 | section 392 | dieresis 393 | copyright 394 | ordfeminine 395 | guillemetleft 396 | logicalnot 397 | softhyphen 398 | registered 399 | macron 400 | degree 401 | plusminus 402 | twosuperior 403 | threesuperior 404 | acute 405 | mu 406 | paragraph 407 | periodcentered 408 | cedilla 409 | onesuperior 410 | ordmasculine 411 | guillemetright 412 | onequarter 413 | onehalf 414 | threequarters 415 | questiondown 416 | Agrave 417 | Aacute 418 | Acircumflex 419 | Atilde 420 | Adieresis 421 | Aring 422 | AE 423 | Ccedilla 424 | Egrave 425 | Eacute 426 | Ecircumflex 427 | Edieresis 428 | Igrave 429 | Iacute 430 | Icircumflex 431 | Idieresis 432 | Eth 433 | Ntilde 434 | Ograve 435 | Oacute 436 | Ocircumflex 437 | Otilde 438 | Odieresis 439 | multiply 440 | Oslash 441 | Ugrave 442 | Uacute 443 | Ucircumflex 444 | Udieresis 445 | Yacute 446 | Thorn 447 | germandbls 448 | agrave 449 | aacute 450 | acircumflex 451 | atilde 452 | adieresis 453 | aring 454 | ae 455 | ccedilla 456 | egrave 457 | eacute 458 | ecircumflex 459 | edieresis 460 | igrave 461 | iacute 462 | icircumflex 463 | idieresis 464 | eth 465 | ntilde 466 | ograve 467 | oacute 468 | ocircumflex 469 | otilde 470 | odieresis 471 | divide 472 | oslash 473 | ugrave 474 | uacute 475 | ucircumflex 476 | udieresis 477 | yacute 478 | thorn 479 | ydieresis 480 | 481 | name 482 | Windows 1252 483 | 484 | 485 | list 486 | 487 | .notdef 488 | NULL 489 | CR 490 | space 491 | exclam 492 | quotedbl 493 | numbersign 494 | dollar 495 | percent 496 | ampersand 497 | quotesingle 498 | parenleft 499 | parenright 500 | asterisk 501 | plus 502 | comma 503 | hyphen 504 | period 505 | slash 506 | zero 507 | one 508 | two 509 | three 510 | four 511 | five 512 | six 513 | seven 514 | eight 515 | nine 516 | colon 517 | semicolon 518 | less 519 | equal 520 | greater 521 | question 522 | at 523 | A 524 | B 525 | C 526 | D 527 | E 528 | F 529 | G 530 | H 531 | I 532 | J 533 | K 534 | L 535 | M 536 | N 537 | O 538 | P 539 | Q 540 | R 541 | S 542 | T 543 | U 544 | V 545 | W 546 | X 547 | Y 548 | Z 549 | bracketleft 550 | backslash 551 | bracketright 552 | asciicircum 553 | underscore 554 | grave 555 | a 556 | b 557 | c 558 | d 559 | e 560 | f 561 | g 562 | h 563 | i 564 | j 565 | k 566 | l 567 | m 568 | n 569 | o 570 | p 571 | q 572 | r 573 | s 574 | t 575 | u 576 | v 577 | w 578 | x 579 | y 580 | z 581 | braceleft 582 | bar 583 | braceright 584 | asciitilde 585 | 586 | name 587 | ASCII 588 | 589 | 590 | 591 | -------------------------------------------------------------------------------- /FilterListManager.glyphsPlugin/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import division, print_function, unicode_literals 3 | 4 | ########################################################################################################### 5 | # 6 | # 7 | # Filter List Manager 8 | # A plugin for the Glyphs font editor 9 | # Copyright 2018 Christopher Simpkins 10 | # Apache License 2.0 11 | # 12 | # 13 | ########################################################################################################### 14 | 15 | import logging 16 | import objc 17 | import os 18 | import plistlib 19 | import shutil 20 | import subprocess 21 | try: 22 | import urllib2 as urllibrequest 23 | import urlparse as urllibparse 24 | except: 25 | import urllib.request as urllibrequest 26 | import urllib.parse as urllibparse 27 | 28 | from GlyphsApp import * 29 | from GlyphsApp.plugins import * 30 | 31 | # ----------------- 32 | # Path definitions 33 | # ----------------- 34 | # Glyphs application paths 35 | GLYPHS_PLIST_FILE = os.path.join( 36 | os.path.expanduser("~"), 37 | "Library", 38 | "Application Support", 39 | "Glyphs", 40 | "CustomFilter.plist", 41 | ) 42 | 43 | 44 | # FLM plugin paths 45 | FLM_GLYPHSFILTERS_DIR = os.path.join(os.path.expanduser("~"), "GlyphsFilters") 46 | FLM_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "GlyphsFilters", "backup") 47 | FLM_BACKUP_ORIGINAL_FILE = os.path.join(FLM_BACKUP_DIR, "CustomFilters.plist.original") 48 | FLM_BACKUP_PREVIOUS_FILE = os.path.join(FLM_BACKUP_DIR, "CustomFilters.plist") 49 | FLM_DEFAULT_PLIST = os.path.join( 50 | os.path.expanduser("~"), 51 | "Library", 52 | "Application Support", 53 | "Glyphs", 54 | "Plugins", 55 | "FilterListManager.glyphsPlugin", 56 | "Contents", 57 | "Resources", 58 | "CustomFilter.plist", 59 | ) 60 | FLM_LOG_DIR = os.path.join(os.path.expanduser("~"), "GlyphsFilters", "logs") 61 | FLM_LOG_FILE = os.path.join(FLM_LOG_DIR, "flm.log") 62 | FLM_REMOTE_DEF_FILE = os.path.join( 63 | os.path.expanduser("~"), "GlyphsFilters", "remote", "defs.txt" 64 | ) 65 | 66 | # --------------- 67 | # Logging setup 68 | # --------------- 69 | if not os.path.isdir(FLM_LOG_DIR): 70 | os.makedirs(FLM_LOG_DIR) 71 | logging.basicConfig( 72 | filename=FLM_LOG_FILE, 73 | filemode="w", 74 | format="%(asctime)s %(levelname)s %(message)s", 75 | datefmt="%m/%d/%Y %I:%M:%S %p", 76 | level=logging.DEBUG, 77 | ) 78 | 79 | # ---------------------------- 80 | # CustomFilters.plist backup 81 | # ---------------------------- 82 | # Backup CustomFilters.plist file present at the time of 83 | # plugin installation (to avoid permanent elimination of 84 | # custom filters that user created before plugin use) 85 | try: 86 | if not os.path.isfile(FLM_BACKUP_ORIGINAL_FILE) and os.path.isfile(GLYPHS_PLIST_FILE): 87 | if not os.path.isdir(FLM_BACKUP_DIR): 88 | os.makedirs(FLM_BACKUP_DIR) 89 | shutil.copy(GLYPHS_PLIST_FILE, FLM_BACKUP_ORIGINAL_FILE) 90 | except: 91 | pass 92 | 93 | class FilterListManager(GeneralPlugin): 94 | @objc.python_method 95 | def settings(self): 96 | self.update_name = Glyphs.localize( 97 | { 98 | "en": "Update Filter Lists", 99 | "de": "Filterlisten aktualisieren" 100 | } 101 | ) 102 | self.restoredefault_name = Glyphs.localize( 103 | { 104 | "en": "Restore Default Filter Lists", 105 | "de": "Standard-Filterlisten wiederherstellen", 106 | } 107 | ) 108 | self.opendir_name = Glyphs.localize( 109 | { 110 | "en": "Open GlyphsFilters Directory", 111 | "de": "Verzeichnis GlyphsFilters öffnen", 112 | } 113 | ) 114 | 115 | @objc.python_method 116 | def start(self): 117 | try: 118 | # new API in Glyphs 2.3.1-910 119 | new_update_menu_item = NSMenuItem(self.update_name, self.updateFilters_) 120 | new_restore_menu_item = NSMenuItem(self.restoredefault_name, self.restoreFilters_) 121 | new_opendir_menu_item = NSMenuItem(self.opendir_name, self.openGlyphsfiltersDirectory_) 122 | Glyphs.menu[EDIT_MENU].append(new_update_menu_item) 123 | Glyphs.menu[EDIT_MENU].append(new_restore_menu_item) 124 | Glyphs.menu[EDIT_MENU].append(new_opendir_menu_item) 125 | except Exception: 126 | main_menu = Glyphs.mainMenu() 127 | update_selector = objc.selector(self.updateFilters_, signature="v@:@") 128 | restore_selector = objc.selector(self.restoreFilters_, signature="v@:@") 129 | open_selector = objc.selector( 130 | self.openGlyphsfiltersDirectory_, signature="v@:@" 131 | ) 132 | new_update_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( 133 | self.update_name, update_selector, "" 134 | ) 135 | new_restore_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( 136 | self.restore_name, restore_selector, "" 137 | ) 138 | new_open_menu_item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( 139 | self.opendir_name, open_selector, "" 140 | ) 141 | new_update_menu_item.setTarget_(self) 142 | main_menu.itemWithTag_(5).submenu().addItem_(new_update_menu_item) 143 | new_restore_menu_item.setTarget_(self) 144 | main_menu.itemWithTag_(5).submenu().addItem_(new_restore_menu_item) 145 | new_open_menu_item.setTarget_(self) 146 | main_menu.itemWithTag_(5).submenu().addItem_(new_open_menu_item) 147 | 148 | def updateFilters_(self, sender): 149 | """Perform the list filter update""" 150 | # Expected filter definitions directory test 151 | if not self.filter_directory_is_present(): 152 | Glyphs.showNotification( 153 | "Filter List Manager", 154 | "ERROR: Unable to locate ~/GlyphsFilters directory! See log.", 155 | ) 156 | logging.error( 157 | "Unable to locate the ~/GlyphsFilters directory. This directory is mandatory for execution. Please create it!" 158 | ) 159 | return 0 160 | 161 | # -------------------------------------------- 162 | # 163 | # Backup original CustomFilters.plist file 164 | # 165 | # -------------------------------------------- 166 | 167 | # read previous CustomFilters.plist definition file 168 | # - used to prepare backup 169 | # - used to define new definition file 170 | try: 171 | previous_plist_data = plistlib.readPlist(GLYPHS_PLIST_FILE) 172 | except Exception as e: 173 | Glyphs.showNotification( 174 | "Filter List Manager", 175 | "ERROR: Unable to read CustomFilters.plist file. See log.", 176 | ) 177 | logging.error( 178 | "Unable to read the original CustomFilters.plist file in order to create a backup. Error: " 179 | + str(e) 180 | ) 181 | return 1 182 | 183 | new_plist_list = [] # storage data structure for new plist file definitions 184 | 185 | # backup the existing CustomFilters.plist file in the 186 | # ~/GlyphsFilters/backup directory 187 | try: 188 | if os.path.exists(FLM_BACKUP_DIR): 189 | shutil.move(GLYPHS_PLIST_FILE, FLM_BACKUP_PREVIOUS_FILE) 190 | else: 191 | os.makedirs(FLM_BACKUP_DIR) 192 | shutil.move(GLYPHS_PLIST_FILE, FLM_BACKUP_PREVIOUS_FILE) 193 | except Exception as e: 194 | Glyphs.showNotification( 195 | "Filter List Manager", 196 | "ERROR: Unable to backup your CustomFilters.plist file. See log.", 197 | ) 198 | logging.error( 199 | "Unable to backup your CustomFilters.plist file. Error: " + str(e) 200 | ) 201 | return 1 202 | 203 | # ----------------------------------------------------- 204 | # 205 | # Update CustomFilters.plist file with new definitions 206 | # 207 | # ----------------------------------------------------- 208 | 209 | # parse local filter list definition files 210 | try: 211 | local_filter_definitions_list = self.get_local_filter_definitions_list() 212 | except Exception as e: 213 | Glyphs.showNotification( 214 | "Filter List Manager", 215 | "Failed to parse local definition files. See log.", 216 | ) 217 | logging.error("Failed to parse local definitions files. Error: " + str(e)) 218 | return 1 219 | 220 | # parse remote filter list definition files 221 | try: 222 | remote_filter_definitions_list = self.get_remote_filter_definitions_list() 223 | except Exception as e: 224 | Glyphs.showNotification( 225 | "Filter List Manager", 226 | "Failed to parse remote definition files. See log.", 227 | ) 228 | logging.error("Failed to parse remote definitions files. Error: " + str(e)) 229 | return 1 230 | 231 | plist_filter_definitions_list = [] 232 | 233 | # create list with data structure that is formatted 234 | # so that it can be translated to a plist file on write 235 | for local_filter in local_filter_definitions_list: 236 | filter_dict = {} 237 | filter_dict["name"] = local_filter.name 238 | filter_dict["list"] = local_filter.list 239 | plist_filter_definitions_list.append(filter_dict) 240 | logging.info( 241 | "Found local definition file for the filter '" + local_filter.name + "'" 242 | ) 243 | 244 | for remote_filter in remote_filter_definitions_list: 245 | filter_dict = {} 246 | filter_dict["name"] = remote_filter.name 247 | filter_dict["list"] = remote_filter.list 248 | plist_filter_definitions_list.append(filter_dict) 249 | logging.info( 250 | "Found remote definition file for the filter '" 251 | + remote_filter.name 252 | + "'" 253 | ) 254 | 255 | # confirm that at least one definition was parsed from the definition files 256 | # if not, abort 257 | if ( 258 | len(local_filter_definitions_list) == 0 259 | and len(remote_filter_definitions_list) == 0 260 | ): 261 | Glyphs.showNotification( 262 | "Filter List Manager", 263 | "Unable to identify new filter list definition files. No changes were made.", 264 | ) 265 | logging.info( 266 | "Unable to identify new filter list definition files. There were no changes made to the filter list definitions." 267 | ) 268 | return 0 269 | 270 | # filter out previous filter list contents in the CustomFilter.plist 271 | # file and keep previous non-filter list contents 272 | for previous_definition in previous_plist_data: 273 | if "list" in previous_definition: 274 | if "name" in previous_definition: 275 | logging.info( 276 | "Removing previously defined filter list '" 277 | + previous_definition["name"] 278 | + "'" 279 | ) 280 | else: 281 | new_plist_list.append(previous_definition) 282 | if "name" in previous_definition: 283 | logging.info( 284 | "Saving previously defined CustomFilters.plist data with name '" 285 | + previous_definition["name"] 286 | + "'" 287 | ) 288 | 289 | # add new data that was read from local and remote 290 | # definition files to a data format that translates 291 | # to a plist file 292 | for new_definition in plist_filter_definitions_list: 293 | new_plist_list.append(new_definition) 294 | if "name" in new_definition: 295 | logging.info("Adding new filter list '" + new_definition["name"] + "'") 296 | 297 | # write new CustomFilters.plist definition file to disk 298 | try: 299 | plistlib.writePlist(new_plist_list, GLYPHS_PLIST_FILE) 300 | logging.info("The new CustomFilter.plist file write was successful.") 301 | except Exception as e: 302 | Glyphs.showNotification( 303 | "Filter List Manager", 304 | "ERROR: Unable to write CustomFilters.plist file. See log.", 305 | ) 306 | logging.error( 307 | "Unable to write new CustomFilters.plist file to disk. Error: " + str(e) 308 | ) 309 | return 1 310 | 311 | Glyphs.showNotification( 312 | "Filter List Manager", 313 | "The filter list updates were successful. Please quit and restart Glyphs.", 314 | ) 315 | logging.info( 316 | "The filter list updates were successful. Please quit and restart the Glyphs application to view the new filter lists." 317 | ) 318 | 319 | def restoreFilters_(self, sender): 320 | """Perform restore of default list filters""" 321 | # copy the default definitions to the Glyphs application 322 | try: 323 | default_filters = plistlib.readPlist(FLM_DEFAULT_PLIST) 324 | plistlib.writePlist(default_filters, GLYPHS_PLIST_FILE) 325 | except Exception as e: 326 | Glyphs.showNotification( 327 | "Filter List Manager", 328 | "ERROR: Unable to restore the default filter list definitions. See log.", 329 | ) 330 | logging.error( 331 | "Unable to restore default filter list definitions. Error: " + str(e) 332 | ) 333 | return 1 334 | 335 | Glyphs.showNotification( 336 | "Filter List Manager", 337 | "The default filter list restoration was successful. Please quit and restart Glyphs.", 338 | ) 339 | logging.info( 340 | "The default filter list restoration was successful. Please quit and restart the Glyphs application to view the filter lists." 341 | ) 342 | 343 | def openGlyphsfiltersDirectory_(self, sender): 344 | """Called from a plugin Edit menu item and opens the ~/GlyphsFilters directory in the macOS Finder""" 345 | if not os.path.isdir(FLM_GLYPHSFILTERS_DIR): 346 | Glyphs.showNotification( 347 | "Filter List Manager", 348 | "Unable to find ~/GlyphsFilters directory. Please create this path.", 349 | ) 350 | logging.error( 351 | "Unable to find the ~/GlyphsFilters directory. Please create this directory path." 352 | ) 353 | else: 354 | subprocess.call(["open", FLM_GLYPHSFILTERS_DIR]) 355 | logging.info( 356 | "The ~/GlyphsFilters directory was opened with the Edit menu item." 357 | ) 358 | 359 | @objc.python_method 360 | def filter_directory_is_present(self): 361 | """Tests for presence of the ~/GlyphsFilters directory""" 362 | if not os.path.isdir(FLM_GLYPHSFILTERS_DIR): 363 | return False 364 | else: 365 | return True 366 | 367 | @objc.python_method 368 | def get_local_filter_definitions_list(self): 369 | """Reads and launches parsing of local definition files, returns a Python list of 370 | Filter objects that are created from the parse""" 371 | local_definitions_list = [] 372 | 373 | if not os.path.isdir(FLM_GLYPHSFILTERS_DIR): 374 | # return an empty list if the directory is not found 375 | return [] 376 | 377 | raw_definitions_file_list = [ 378 | f 379 | for f in os.listdir(FLM_GLYPHSFILTERS_DIR) 380 | if os.path.isfile(os.path.join(FLM_GLYPHSFILTERS_DIR, f)) 381 | ] 382 | definitions_file_list = [] 383 | # filter list for dotfiles. This eliminates macOS .DS_Store files that lead to errors during processing 384 | for definition_file in raw_definitions_file_list: 385 | if definition_file[0] == ".": 386 | pass 387 | else: 388 | definitions_file_list.append(definition_file) 389 | for definition_file in definitions_file_list: 390 | definition_path_list = definition_file.split(".") 391 | # define the filter list name as the file name 392 | new_filter = Filter(definition_path_list[0]) 393 | with open(os.path.join(FLM_GLYPHSFILTERS_DIR, definition_file)) as f: 394 | # define the filter object with the definitions in the text data 395 | text = f.read() 396 | new_filter.define_list_with_newline_delimited_text(text) 397 | 398 | local_definitions_list.append(new_filter) 399 | 400 | return local_definitions_list 401 | 402 | @objc.python_method 403 | def get_remote_filter_definitions_list(self): 404 | """Pulls, reads, and launches parsing of remote definition files, returns a Python list of 405 | Filter objects that are created from the parse""" 406 | remote_definitions_list = [] 407 | 408 | if not os.path.isfile(FLM_REMOTE_DEF_FILE): 409 | return [] 410 | 411 | with open(FLM_REMOTE_DEF_FILE) as f: 412 | for line in f: 413 | url = line.strip() 414 | if len(url) == 0: 415 | pass 416 | elif url[0] in ("#", "/"): 417 | pass 418 | else: 419 | # unquote URL defined in list to define file name 420 | # (in case the user pasted a urlencoded string) 421 | decoded_url = urllibparse.unquote(url) 422 | parsed_url = urllibparse.urlparse(decoded_url).path 423 | parsed_path = os.path.split(parsed_url) 424 | filter_defintion_filename = parsed_path[1] 425 | new_filter = Filter(filter_defintion_filename) 426 | 427 | # quote URL defined in list to make HTTP GET request 428 | response = urllibrequest.urlopen(url) 429 | text = response.read() 430 | new_filter.define_list_with_newline_delimited_text(text) 431 | remote_definitions_list.append(new_filter) 432 | 433 | return remote_definitions_list 434 | 435 | @objc.python_method 436 | def __file__(self): 437 | """Glyphs plugin API specific method. Please leave this method unchanged""" 438 | return __file__ 439 | 440 | 441 | class Filter(object): 442 | """Filter is an object that maintains data elements for Glyphs application filter lists. 443 | It is instantiated with a new filter list name and list elements are defined with 444 | a class method""" 445 | 446 | def __init__(self, name): 447 | self.comment_delimiters = ("#", "/") 448 | self.name = name 449 | self.list = [] 450 | 451 | def define_list_with_newline_delimited_text(self, text): 452 | """Filter class method that defines a Filter object with parsed data from newline- 453 | delimited text files of list elements""" 454 | raw_code_point_list = text.splitlines() 455 | filtered_code_point_list = [] 456 | for item in raw_code_point_list: 457 | test_item = item.strip() 458 | if len(test_item) == 0: # discard blank lines in definition file 459 | pass 460 | elif ( 461 | test_item[0] in self.comment_delimiters 462 | ): # discard comment lines in definition file 463 | pass 464 | else: 465 | filtered_code_point_list.append(test_item) 466 | self.list = filtered_code_point_list 467 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filter List Manager (FLM) 2 | 3 | ## About 4 | 5 | Filter List Manager (FLM) is a free, open source plugin for the [Glyphs font editor](https://glyphsapp.com) that automates the creation of glyph filter lists with simple newline-delimited glyph definition text files. Definition files can be stored locally on your computer or remotely (e.g., in a Github repository) and used with the plugin. 6 | 7 | ## Quickstart Guide 8 | 9 | - Download the FLM plugin from the repository releases and install in the Glyphs application 10 | - Export your previously defined filter list definitions to FLM formatted definition files with [the instructions below](#please-read-before-use) 11 | - Create newline-delimited local filter list definition files in `~/GlyphsFilters` using the base filename of the definition file to name your filter list 12 | - (*Optional*) Push one or more new filter list definition files to a remote server (or locate definition files that someone else has pushed to a remote server) and create a `~/GlyphsFilters/remote/defs.txt` settings file to [define the URL to the remote filter list definition files](https://github.com/source-foundry/FilterListManager/tree/dev#remote-definition-files) 13 | - Select the Edit > Update Filter Lists menu item in the Glyphs editor 14 | - Close and restart Glyphs to view your new filter lists! 15 | 16 | ## Table of Contents 17 | 18 | - [Please Read Before Use](#please-read-before-use) (Really, please read it!) 19 | - [Plugin Installation, Upgrades, Removal](#plugin-installation) 20 | - [How to Make Filter List Definition Files](DEFINITIONS.md) 21 | - [Filter List Definition File Storage Locations](#filter-list-definition-file-storage-location) 22 | - [Plugin Usage](#plugin-usage) 23 | - [Tools for Filter List Management](TOOLS.md) 24 | - [Contributing](#contributing) 25 | - [License](LICENSE) 26 | 27 | ## Please Read Before Use! 28 | 29 | The first run of the plugin replaces all existing Glyphs font editor filter lists that you have previously defined with the filter lists that are defined using the definition files specified for the FLM plugin. A handy Python 3 script is available to export your existing filter list definitions to the FLM definition file format and should be used before you run a FLM update. Download the `exportfilters.py` script located in the `tools` directory of this repository and run the script with a Python 3 interpreter using the following command from any directory on your system: 30 | 31 | ``` 32 | python3 exportfilters.py 33 | ``` 34 | 35 | Confirm that all of your existing filters are defined in new filter list definition files that are exported to the directory `~/GlyphsFilters`, then proceed with use of the plugin to define additional filter lists and manage your previous filter lists. 36 | 37 | For those who don't read documentation, there is a backup of the original `CustomFilter.plist` file that was present on FLM install. This file contains all of the filter list definitions that were present before you ran the FLM plugin. You can find this backup on the path `~/GlyphsFilters/backup/CustomFilter.plist.original`. Rename the file to `CustomFilter.plist` and move it to the path `~/Library/Application Support/Glyphs/CustomFilter.plist` to restore your original filter list state, then use the documentation above to export the filters for use with this plugin. 38 | 39 | ## Plugin Installation 40 | 41 | 1. Download the [latest release archive](https://github.com/source-foundry/FilterListManager/releases) from the FLM repository Releases in either a *.zip or *.tar.gz compressed format 42 | 2. Unpack the downloaded archive file 43 | 3. Open the unpacked directory 44 | 4. Double-click the plugin `FilterListManager.glyphsPlugin` in the top level of the directory to open it with the Glyphs application 45 | 5. Acknowledge that you want to install FLM in the Glyphs Install Plugin dialog that appears 46 | 47 | ### Upgrade Plugin 48 | 49 | The `Preferences > Addons > Plugins` window in Glyphs will indicate when an update is available for the FLM plugin. To upgrade your installed plugin, locate the [latest release of the plugin](https://github.com/source-foundry/FilterListManager/releases/latest) and follow the same instructions that you followed for the initial installation above. 50 | 51 | Changes that were included in releases since your last upgrade are indicated in the repository [CHANGELOG.md](CHANGELOG.md). 52 | 53 | ### Uninstall Plugin 54 | 55 | Uninstall the FLM plugin by deleting the directory on the path `~/Library/Application Support/Glyphs/Plugins/FilterListManager.glyphsPlugin`. Next, delete the `~/GlyphsFilters` directory with the command: 56 | 57 | ``` 58 | $ rm -rf ~/GlyphsFilters 59 | ``` 60 | 61 | All filter lists that were previously defined with FLM will remain in your editor. If you wish to remove any previously defined filter lists, use the builtin functionality for filter list management in the Glyphs GUI. Removal of the FLM plugin does not limit use of previously defined filter lists. You can remove all components of the plugin and continue to use filter lists normally in the absence of the plugin and all settings files. There is no need to modify the builtin functionality for filter list management in Glyphs following an uninstall of the FLM plugin. 62 | 63 | ## How to Make a Filter List Definition File 64 | 65 | The filter list definition file is a newline-delimited text file that lists your desired glyph names. See [DEFINITIONS.md](DEFINITIONS.md) for detailed documentation of how you format the file and define both glyph list names and the glyph names that are included in the filter lists. 66 | 67 | ## Filter List Definition File Storage Location 68 | 69 | The filter list definition files that you use to define new filter lists can be stored on your local macOS system or on a remote server that is accessible through HTTP GET requests. One or more local and remote filter list definition files can be used simultaneously to define all desired Glyphs font editor filter lists with the FLM plugin. 70 | 71 | ### Local definition files 72 | 73 | Place the filter list definition files that you make in the top level of the directory `~/GlyphsFilters` on your macOS system. If this directory does not exist after you install the FLM plugin, open your terminal and enter the following command: 74 | 75 | ``` 76 | $ mkdir ~/GlyphsFilters 77 | ``` 78 | 79 | The GlyphsFilters directory can be opened by selecting the Edit > Open GlyphsFilters Directory menu item in the Glyphs application after you install the FLM plugin. 80 | 81 | Note: FLM does not search sub-directories of `~/GlyphsFilters` for local definition files so you can create one or more sub-directories to store definition files that are not in active use. Sub-directory names `logs`, `backup`, and `remote` are reserved for the plugin. Please do not overwrite these directory paths. 82 | 83 | ### Remote definition files 84 | 85 | You can upload filter list definition files to any publicly accessible web server or use files that have been shared by others with this approach. You must create one additional settings file in the `~/GlyphsFilters` directory to define one or more URL where the FLM plugin can locate the remotely stored definition files. 86 | 87 | Save a text file on the path `~/GlyphsFilters/remote/defs.txt` with a newline-delimited list of URL that point to one or more filter list definition files. Each URL represents a new filter list definition for the Glyphs application. You may use the comment line indicators `#` and `//` in the `~/GlyphsFilters/remote/defs.txt` file. Empty lines are also permitted (and ignored). 88 | 89 | An example of a valid `~/GlyphsFilters/remote/defs.txt` file that points to two filter list definition files located on a remote Github server follows: 90 | 91 | ``` 92 | // Remote filter list definitions 93 | 94 | # Mac Roman filter list definitions 95 | https://raw.githubusercontent.com/source-foundry/charset-filters/master/Mac-Roman.txt 96 | 97 | # MES-1 filter list definitions 98 | https://raw.githubusercontent.com/source-foundry/charset-filters/master/MES-1.txt 99 | ``` 100 | 101 | Please note in the above example that you must use the URL for the "Raw" text file if you push your definition files to Github. This is formatted as `https://raw.githubusercontent.com/[account]/[project name]/[branch]/[filename]`. When you enter this URL in your browser you should see only the text file with no Github website UI around it. If you see the Github UI in the browser window, the URL that you are viewing points to HTML text and this will lead to errors during the FLM filter list update attempt. 102 | 103 | ## Plugin Usage 104 | 105 | Following installation, you will find three new menu items under the Glyphs application Edit menu: 106 | 107 | - Update Filter Lists 108 | - Restore Default Filter Lists 109 | - Open GlyphsFilters Directory 110 | 111 | ### Update Filter Lists Menu Item 112 | 113 | Select the Update Filter Lists menu item to perform an update of your Glyphs filter list definitions using all local and remote filter list definition files that you define in your `~/GlyphsFilters` directory. 114 | 115 | 116 | ### Restore Default Filter Lists Menu Item 117 | 118 | Select the Restore Default Filter Lists menu item to restore a default set of filter lists that include the ASCII, Mac Roman, and Windows 1252 filters. 119 | 120 | ### Open GlyphsFilters Directory Menu Item 121 | 122 | Select the Open GlyphsFilters Directory menu item to open the `~/GlyphsFilters` directory in the macOS Finder. The `~/GlyphsFilters` directory is the location where you store local filter list definition files and remote definition files. This directory also includes application logs that can be used to explore what happened during processing or evaluate errors. Lastly, and importantly, the directory includes backups of the `CustomFilters.plist` definition file at the time of plugin install and just prior to the last FLM plugin filter list update run so that you can recover any lost data. 123 | 124 | 125 | ## Tools for Filter List Management 126 | 127 | The `tools` directory in the FLM source repository contains Python 3 scripts that assist with the creation and management of filter list definition files. Please see the [TOOLS.md](TOOLS.md) documentation for details. 128 | 129 | ## Contributing 130 | 131 | Contributions to the FLM plugin project are warmly welcomed. Fork the repository and submit a Github pull request to suggest modifications to the source. Source must be contributed under the Apache License 2.0. File new issue reports for problems that you identify during use or for plugin enhancement suggestions. 132 | 133 | ## License 134 | 135 | [Apache License 2.0](LICENSE) -------------------------------------------------------------------------------- /TOOLS.md: -------------------------------------------------------------------------------- 1 | ## Filter List Management Tools 2 | 3 | The filter list management tools include Python 3 scripts that are located in the [`tools` directory](https://github.com/source-foundry/FilterListManager/tree/master/tools) of the FLM repository. 4 | 5 | If a Python 3 interpreter is not available on your system, please see the Python documentation at https://www.python.org/downloads/ for installation instructions. 6 | 7 | The tools include: 8 | 9 | ### `exportfilters.py` 10 | 11 | The `exportfilters.py` script exports existing Glyphs application filter lists to the FLM definition file format and saves these definition files in the GlyphsFilters directory. 12 | 13 | #### Usage 14 | 15 | Download the script file from the FLM repository and execute it from any directory on your macOS system with the following command in your terminal: 16 | 17 | ``` 18 | $ python3 exportfilters.py 19 | ``` 20 | 21 | Confirm that the files were exported to your GlyphsFilters directory and that the files include the expected glyph names. 22 | 23 | 24 | ### `glformatter.py` 25 | 26 | The `glformatter.py` script formats lists of glyph names with additional Unicode data in a comment line above the glyph name definition. 27 | 28 | The data that are added depend upon how you specify your glyph name in the definition file. The possible data include: 29 | 30 | - Unicode code point in hexadecimal format 31 | - AGL-like nice name (if production name is specified in the file) 32 | - Unicode code point hexadecimal format production name (if nice name is specified in the file) 33 | - Unicode description of the code point 34 | 35 | #### Usage 36 | 37 | Download the script file from the FLM repository and execute it from any directory on your macOS system. Include one or more file paths to local definition files as arguments to the script: 38 | 39 | ``` 40 | $ python3 glformatter.py [filepath 1] [filepath ...] 41 | ``` 42 | 43 | The script reformats the file in place and saves a backup of the in-file on the path `[filepath].pre`. 44 | 45 | 46 | ### `glinter.py` 47 | 48 | The `glinter.py` script tests each glyph name that is specified in one or more filter list definition files against those that are defined in the Glyphs application GlyphData.xml file and the guidelines provided in the [Adobe OpenType Feature File Specification](https://github.com/adobe-type-tools/afdko/blob/develop/docs/OpenTypeFeatureFileSpecification.html) for *development* glyph names. 49 | 50 | The Glyphs application specific GlyphData.xml tests include any modifications that you've made to the GlyphData.xml data to define new glyph names. The intent is to permit you to confirm that the glyph names that you specified in your definition list will be properly recognized and imported into the Glyphs application for use. 51 | 52 | The GlyphData.xml tests that are performed include: 53 | 54 | 1) specified glyph name is a Glyphs application nice name 55 | 2) specified glyph name is a Glyphs application production name 56 | 3) specified glyph name is a Glyphs application alternate name 57 | 58 | The glyph name passes if criteria for (1) or (2) are met. The script suggests conversion to the nice name or production name (with those values!) if criteria for (3) are met. If there is no match for (1), (2), and (3), the script raises an error and indicates the glyph name and file path to the definition file. 59 | 60 | The Adobe OpenType Feature File Specification tests that are performed include: 61 | 62 | 1) glyph name is 63 characters or less in length 63 | 2) glyph name does not start with a period (except ".null" and ".notdef") 64 | 3) glyph name only includes characters in the set: 65 | - `A-Z` 66 | - `a-z` 67 | - `0-9` 68 | - `.` (period) 69 | - `_` (underscore) 70 | - `*` (asterisk) 71 | - `+` (plus sign) 72 | - `-` (hyphen) 73 | - `:` (colon) 74 | - `^` (circumflex accent) 75 | - `|` (vertical bar) 76 | - `~` (tilde) 77 | 78 | #### Usage 79 | 80 | Download the script file from the FLM repository and execute it from any directory on your macOS system. Include one or more file paths to local definition files as arguments to the script: 81 | 82 | ``` 83 | $ python3 glinter.py [filepath 1] [filepath ...] 84 | ``` 85 | 86 | Please note that the script does not modify the filter list definition file during execution. It indicates potential errors for your review. You must edit the file to address valid errors. -------------------------------------------------------------------------------- /tools/exportfilters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # =========================================================== 5 | # 6 | # 7 | # exportfilters.py 8 | # A Glyphs font editor filter list to FLM plugin 9 | # definition file exporter script 10 | # Copyright 2018 Christopher Simpkins 11 | # Apache License 2.0 12 | # 13 | # Version 0.1.1 14 | # 15 | # =========================================================== 16 | 17 | # USAGE 18 | # 19 | # This script is dependent upon the Python 3 interpreter. If a Python 3 interpreter 20 | # is not available on your system, please see the Python documentation at 21 | # https://www.python.org/downloads/ for instructions on how to install it. 22 | # 23 | # 24 | # Execute the script from any directory on your system with the following command 25 | # in your terminal window: 26 | # 27 | # python3 exportfilters.py 28 | # 29 | # 30 | # Following execution your existing Glyphs application filter lists will be 31 | # exported as newline delimited definition files that are formatted so that 32 | # they work with the Filter List Manager plugin. The filter list name is 33 | # mapped to the base file name of the definition file and the glyph definitions 34 | # are listed with newline separators. You can find all definition files on 35 | # the path ~/GlyphsFilters. 36 | 37 | import os 38 | import plistlib 39 | import sys 40 | 41 | 42 | def main(): 43 | try: 44 | # default paths 45 | glyphs_plist_path = os.path.join( 46 | os.path.expanduser("~"), 47 | "Library", 48 | "Application Support", 49 | "Glyphs", 50 | "CustomFilter.plist", 51 | ) 52 | 53 | flm_definitions_directory_path = os.path.join( 54 | os.path.expanduser("~"), "GlyphsFilters" 55 | ) 56 | 57 | if not os.path.isdir(flm_definitions_directory_path): 58 | os.makedirs(flm_definitions_directory_path) 59 | 60 | with open(glyphs_plist_path, "rb") as f: 61 | filters_data = plistlib.load(f) 62 | for filter_dict in filters_data: 63 | if "list" in filter_dict: 64 | # define filter file name and filter list contents 65 | filter_name = filter_dict["name"] 66 | filter_list = filter_dict["list"] 67 | 68 | base_filepath = filter_name + ".txt" 69 | outfile_path = os.path.join( 70 | flm_definitions_directory_path, base_filepath 71 | ) 72 | 73 | # create newline-delimited file format 74 | outfile_text = ( 75 | "// " + filter_name + " filter" + os.linesep + os.linesep 76 | ) 77 | for glyph_name in filter_list: 78 | outfile_text += glyph_name + os.linesep 79 | 80 | with open(outfile_path, "w") as w: 81 | w.write(outfile_text) 82 | print(filter_name + " filter list exported...") 83 | 84 | print("---") 85 | print("Export complete!") 86 | print("You can find the definition files in the directory ~/GlyphsFilters") 87 | except Exception as e: 88 | sys.stderr.write( 89 | "[ERROR] There was an error during the export attempt. " 90 | + str(e) 91 | + os.linesep 92 | ) 93 | sys.exit(1) 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | -------------------------------------------------------------------------------- /tools/glformatter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # ============================================================== 5 | # 6 | # 7 | # glformatter.py 8 | # Glyph name filter list definition file formatter 9 | # Copyright 2018 Christopher Simpkins 10 | # Apache License 2.0 11 | # 12 | # Version 0.1.1 13 | # 14 | # ============================================================== 15 | 16 | # USAGE 17 | # 18 | # This script is dependent upon the Python 3 interpreter. If a Python 3 interpreter 19 | # is not available on your system, please see the Python documentation at 20 | # https://www.python.org/downloads/ for instructions on how to install it. 21 | # 22 | # 23 | # Execute the script from any directory on your system by passing one or more 24 | # filepath arguments to local filter list definition files on your system: 25 | # 26 | # $ python3 glformatter.py [filepath 1] [filepath ...] 27 | # 28 | # The script reformats the text in the filter list definition file with the 29 | # following: 30 | # 31 | # - adds a comment line above each glyph name definition in the file 32 | # that includes: 33 | # - Unicode code point (in hexadecimal name format) 34 | # - AGL-style nice name (if not used for glyph name definition) 35 | # - production name (if not used for glyph name definition) 36 | # - Unicode description 37 | # 38 | # The file write takes place on the original file path. The original file 39 | # is backed up on the path: 40 | # 41 | # [original file path].pre 42 | 43 | 44 | import os 45 | import shutil 46 | import sys 47 | import xml.etree.ElementTree as ET 48 | 49 | COMMENT_DELIMITERS = ("#", "/") 50 | 51 | 52 | class Glyph(object): 53 | def __init__(self): 54 | self.unicode = "" 55 | self.name = "" 56 | self.description = "" 57 | self.production = "" 58 | self.alt_names = [] 59 | 60 | 61 | def main(argv): 62 | glyphlist = [] 63 | unicode_set = set() 64 | name_set = set() 65 | production_set = set() 66 | altname_set = set() 67 | 68 | tree = ET.parse( 69 | "/Applications/Glyphs.app/Contents/Frameworks/GlyphsCore.framework/Versions/A/Resources/GlyphData.xml" 70 | ) 71 | root = tree.getroot() 72 | for child in root: 73 | glyph = Glyph() 74 | if "unicode" in child.attrib: 75 | glyph.unicode = child.attrib["unicode"] 76 | unicode_set.add(glyph.unicode) 77 | if "name" in child.attrib: 78 | glyph.name = child.attrib["name"] 79 | name_set.add(glyph.name) 80 | if "description" in child.attrib: 81 | glyph.description = child.attrib["description"] 82 | if "production" in child.attrib: 83 | glyph.production = child.attrib["production"] 84 | production_set.add(glyph.production) 85 | if "altNames" in child.attrib: 86 | namestring = child.attrib["altNames"] 87 | if len(namestring) > 0: 88 | name_list = namestring.split(",") 89 | for altname in name_list: 90 | save_name = altname.strip() 91 | glyph.alt_names.append(save_name) 92 | altname_set.add(save_name) 93 | 94 | glyphlist.append(glyph) 95 | 96 | for filepath in argv: 97 | if not os.path.isfile(filepath): 98 | sys.stderr.write( 99 | "[ERROR] " + filepath + " does not appear to be a valid filepath!" 100 | ) 101 | sys.exit(1) 102 | 103 | outfile_list = [] 104 | 105 | with open(filepath, "r") as f: 106 | text = f.read() 107 | text_line_list = text.splitlines() 108 | for line in text_line_list: 109 | if line[0] in COMMENT_DELIMITERS: 110 | outfile_list.append(line) 111 | elif len(line) == 0: 112 | outfile_list.append("") 113 | elif line is None: 114 | outfile_list.append("") 115 | else: 116 | glyph_name = line 117 | if glyph_name in name_set: 118 | outfile_list.append( 119 | get_comment_string(glyphlist, name=glyph_name) 120 | ) 121 | outfile_list.append(glyph_name + os.linesep) 122 | elif glyph_name in production_set: 123 | outfile_list.append( 124 | get_comment_string(glyphlist, production=glyph_name) 125 | ) 126 | outfile_list.append(glyph_name + os.linesep) 127 | else: 128 | outfile_list.append(glyph_name + os.linesep) 129 | 130 | # backup file to originalpath with ".pre" suffix 131 | backup_filepath = filepath + ".pre" 132 | shutil.copyfile(filepath, backup_filepath) 133 | 134 | # write out formatted file 135 | out_text = "\n".join(outfile_list) 136 | with open(filepath, "w") as f: 137 | f.write(out_text) 138 | 139 | 140 | def get_comment_string(glyphlist, name=None, production=None): 141 | if name is not None: 142 | for glyph in glyphlist: 143 | if glyph.name == name: 144 | comment_string = "# " 145 | if len(glyph.unicode) > 0: 146 | comment_string += "U+" + glyph.unicode 147 | if len(glyph.production) > 0: 148 | comment_string += " | " + glyph.production 149 | if len(glyph.description) > 0: 150 | comment_string += " | " + glyph.description 151 | return comment_string 152 | elif production is not None: 153 | for glyph in glyphlist: 154 | if glyph.production == production: 155 | comment_string = "# " 156 | if len(glyph.unicode) > 0: 157 | comment_string += "U+" + glyph.unicode 158 | if len(glyph.name) > 0: 159 | comment_string += " | " + glyph.name 160 | if len(glyph.description) > 0: 161 | comment_string += " | " + glyph.description 162 | return comment_string 163 | else: 164 | return "" 165 | 166 | 167 | if __name__ == "__main__": 168 | main(sys.argv[1:]) 169 | -------------------------------------------------------------------------------- /tools/glinter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # ============================================================== 5 | # 6 | # 7 | # glinter.py 8 | # Glyph name validity testing script 9 | # Copyright 2018 Christopher Simpkins 10 | # Apache License 2.0 11 | # 12 | # Version 0.3.1 13 | # 14 | # ============================================================== 15 | 16 | # USAGE 17 | # 18 | # This script is dependent upon the Python 3 interpreter. If a Python 3 interpreter 19 | # is not available on your system, please see the Python documentation at 20 | # https://www.python.org/downloads/ for instructions on how to install it. 21 | # 22 | # 23 | # Execute the script from any directory on your system by passing one or more 24 | # filepath arguments to local filter list definition files on your system: 25 | # 26 | # $ python3 glinter.py [filepath 1] [filepath ...] 27 | # 28 | # The script tests each glyph name listed in the definition file vs. 29 | # the Glyphs application GlyphData.xml file and Adobe OpenType 30 | # Feature File specification for development glyph names 31 | # 32 | # Glyph names are tested against the Glyphs GlyphData.xml file to determine 33 | # if they are: 34 | # - Glyphs-defined nice names (AGL style names) 35 | # - Glyphs-defined Unicode hexadecimal style names (e.g, uniXXXX, uXXXXX) 36 | # - common alternate names -- if found, the above two glyph names are suggested 37 | # 38 | # If the glyph name is not found in the above set of lists, the script will raise 39 | # an error and identify the definition file path where the error was found. 40 | # 41 | # For glyph names that pass the above tests, the following additional tests 42 | # that are based upon the Adobe OT Feature File spec are performed: 43 | # - glyph name < 64 characters in length 44 | # - glyph name does not contain a leading period (except .notdef and .null) 45 | # - glyph name contains valid characters for development glyph names 46 | # 47 | # Adobe reference: 48 | # https://github.com/adobe-type-tools/afdko/blob/develop/docs/OpenTypeFeatureFileSpecification.html 49 | 50 | import os 51 | import re 52 | import sys 53 | import xml.etree.ElementTree as ET 54 | 55 | 56 | class Glyph(object): 57 | def __init__(self): 58 | self.unicode = "" 59 | self.name = "" 60 | self.description = "" 61 | self.production = "" 62 | self.alt_names = [] 63 | 64 | 65 | class Filter(object): 66 | """Filter is an object that maintains data elements for Glyphs application filter lists. 67 | It is instantiated with a new filter list name and list elements are defined with 68 | a class method""" 69 | 70 | def __init__(self, name): 71 | self.comment_delimiters = ("#", "/") 72 | self.name = name 73 | self.list = [] 74 | 75 | def define_list_with_newline_delimited_text(self, text): 76 | """Filter class method that defines a Filter object with parsed data from newline- 77 | delimited text files of list elements""" 78 | raw_code_point_list = text.splitlines() 79 | filtered_code_point_list = [] 80 | for item in raw_code_point_list: 81 | test_item = item.strip() 82 | if len(test_item) == 0: # discard blank lines in definition file 83 | pass 84 | elif ( 85 | test_item[0] in self.comment_delimiters 86 | ): # discard comment lines in definition file 87 | pass 88 | else: 89 | filtered_code_point_list.append(test_item) 90 | self.list = filtered_code_point_list 91 | 92 | 93 | def main(argv): 94 | # parse GlyphData.xml file for Glyphs application supported glyph name values 95 | glyphlist = [] 96 | unicode_set = set() 97 | name_set = set() 98 | production_set = set() 99 | altname_set = set() 100 | 101 | tree = ET.parse( 102 | "/Applications/Glyphs.app/Contents/Frameworks/GlyphsCore.framework/Versions/A/Resources/GlyphData.xml" 103 | ) 104 | root = tree.getroot() 105 | for child in root: 106 | glyph = Glyph() 107 | if "unicode" in child.attrib: 108 | glyph.unicode = child.attrib["unicode"] 109 | unicode_set.add(glyph.unicode) 110 | if "name" in child.attrib: 111 | glyph.name = child.attrib["name"] 112 | name_set.add(glyph.name) 113 | if "description" in child.attrib: 114 | glyph.description = child.attrib["description"] 115 | if "production" in child.attrib: 116 | glyph.production = child.attrib["production"] 117 | production_set.add(glyph.production) 118 | if "altNames" in child.attrib: 119 | namestring = child.attrib["altNames"] 120 | if len(namestring) > 0: 121 | name_list = namestring.split(",") 122 | for altname in name_list: 123 | save_name = altname.strip() 124 | glyph.alt_names.append(save_name) 125 | altname_set.add(save_name) 126 | 127 | glyphlist.append(glyph) 128 | 129 | # parse filter list definition files that were passed as command line args 130 | filter_list = [] 131 | 132 | for filepath in argv: 133 | if os.path.isfile(filepath): 134 | new_filter = Filter(filepath) 135 | with open(filepath) as f: 136 | text = f.read() 137 | new_filter.define_list_with_newline_delimited_text(text) 138 | filter_list.append(new_filter) 139 | else: 140 | sys.stderr.write( 141 | "[ERROR]: Unable to identify a file on the path '" 142 | + filepath 143 | + "'" 144 | + os.linesep 145 | ) 146 | 147 | if len(filter_list) == 0: 148 | sys.stderr.write( 149 | "[ERROR]: Unable to identify any definitions in the requested files!" 150 | + os.linesep 151 | ) 152 | sys.exit(1) 153 | 154 | # BEGIN TESTS 155 | ANY_ERROR_DETECTED = False 156 | for a_filter in filter_list: 157 | FILTER_ERROR_DETECTED = False 158 | for glyph_name in a_filter.list: 159 | if glyph_name in name_set: 160 | pass 161 | elif glyph_name in production_set: 162 | pass 163 | elif glyph_name in altname_set: 164 | ANY_ERROR_DETECTED = True 165 | FILTER_ERROR_DETECTED = True 166 | sys.stderr.write( 167 | "'" 168 | + glyph_name 169 | + "' in definition file '" 170 | + a_filter.name 171 | + "' appears to be an alternate name. Consider replacement with one of the following names:" 172 | + os.linesep 173 | ) 174 | for a_glyph in glyphlist: 175 | if glyph_name in a_glyph.alt_names: 176 | if len(a_glyph.name) > 0: 177 | sys.stderr.write(" --> " + a_glyph.name + os.linesep) 178 | if len(a_glyph.production) > 0: 179 | sys.stderr.write(" --> " + a_glyph.production + os.linesep) 180 | if len(a_glyph.unicode) == 4: 181 | uni_name = "uni" + a_glyph.unicode 182 | if not uni_name == a_glyph.production: 183 | sys.stderr.write(" --> " + uni_name + os.linesep) 184 | elif len(a_glyph.unicode) == 5: 185 | uni_name = "u" + a_glyph.unicode 186 | if not uni_name == a_glyph.production: 187 | sys.stderr.write(" --> " + uni_name + os.linesep) 188 | else: 189 | sys.stderr.write( 190 | "'" 191 | + glyph_name 192 | + "' does not appear to be a valid glyph name!" 193 | + os.linesep 194 | ) 195 | ANY_ERROR_DETECTED = True 196 | FILTER_ERROR_DETECTED = True 197 | 198 | # BEGIN non-GlyphData.xml tests 199 | if FILTER_ERROR_DETECTED is False: 200 | # https://github.com/adobe-type-tools/afdko/blob/develop/docs/OpenTypeFeatureFileSpecification.html 201 | # glyph name length 202 | if len(glyph_name) > 63: 203 | sys.stderr.write( 204 | "'" + glyph_name + "' is too long! (> 63 characters)" 205 | ) 206 | FILTER_ERROR_DETECTED = True 207 | ANY_ERROR_DETECTED = True 208 | # glyph name starts with period outside of defined positions (including .null defined in GlyphData.xml) 209 | if glyph_name[0] == "." and (glyph_name not in (".notdef", ".null")): 210 | sys.stderr.write( 211 | "'" + glyph_name + "' includes an invalid leading period!" 212 | ) 213 | FILTER_ERROR_DETECTED = True 214 | ANY_ERROR_DETECTED = True 215 | 216 | # glyph name contains valid characters 217 | if not has_valid_characters(glyph_name): 218 | sys.stderr.write( 219 | "'" + glyph_name + "' contains invalid characters!" + os.linesep 220 | ) 221 | FILTER_ERROR_DETECTED = True 222 | ANY_ERROR_DETECTED = True 223 | 224 | if FILTER_ERROR_DETECTED is True: 225 | sys.stderr.write("[ERROR] --> " + a_filter.name + os.linesep) 226 | else: 227 | print("[OK] " + a_filter.name) 228 | 229 | if ANY_ERROR_DETECTED is True: 230 | sys.exit(1) 231 | else: 232 | print("All tests passed!") 233 | sys.exit(0) 234 | 235 | 236 | def has_valid_characters(glyph_name): 237 | """Tests for presence of valid characters in a glyph name as specified by the Adobe 238 | OpenType Feature File specification. The test here includes characters 239 | specified for 'development' names, a broader set than the production name 240 | definition 241 | 242 | https://github.com/adobe-type-tools/afdko/blob/develop/docs/OpenTypeFeatureFileSpecification.html""" 243 | valid_characters = r"^[A-Za-z0-9\._\*\+\-\:\^\|\~]{1,63}$" 244 | regex = re.compile(valid_characters) 245 | return re.match(regex, glyph_name) 246 | 247 | 248 | if __name__ == "__main__": 249 | main(sys.argv[1:]) 250 | --------------------------------------------------------------------------------