├── .editorconfig ├── .gitignore ├── .jscs.json ├── .jshintrc ├── .npmignore ├── .travis.yml ├── Contributing.md ├── Gruntfile.js ├── License.md ├── Pull_Request_Template.md ├── Readme.md ├── package.json ├── tasks ├── bin │ └── eotlitetool.py ├── engines │ ├── fontforge.js │ ├── fontforge │ │ └── generate.py │ └── node.js ├── templates │ ├── bem.css │ ├── bem.json │ ├── bootstrap.css │ ├── bootstrap.json │ └── demo.html ├── util │ └── util.js └── webfont.js ├── test ├── camel │ ├── MailRu.svg │ └── PlusOne.svg ├── src │ ├── mailru.svg │ ├── odnoklassniki.svg │ ├── pinterest.svg │ ├── plusone.svg │ └── single.svg ├── src_duplicate_names │ ├── one │ │ ├── mailru.svg │ │ ├── odnoklassniki.svg │ │ └── pinterest.svg │ └── two │ │ ├── mailru.svg │ │ └── odnoklassniki.svg ├── src_filename_length │ └── length.svg ├── src_folders │ ├── icons │ │ ├── facebook.svg │ │ ├── github.svg │ │ ├── twitter.svg │ │ └── vkontakte.svg │ ├── more │ │ ├── mailru.svg │ │ └── odnoklassniki.svg │ ├── paths.json │ ├── pinterest.svg │ ├── plusone.svg │ └── single.svg ├── src_ligatures │ ├── git-hub.svg │ ├── mailru.svg │ └── odnoklassniki.svg ├── src_one │ └── home.svg ├── src_space │ └── ma il ru.svg ├── templates │ ├── context-test.html │ ├── custom.js │ ├── custom.json │ ├── template.css │ ├── template.html │ ├── template.json │ ├── template.sass │ └── template.scss └── webfont_test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,.travis.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/tmp 3 | .cache 4 | Changelog.md 5 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": ["for", "while", "do"], 3 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return"], 4 | "requireParenthesesAroundIIFE": true, 5 | "requireSpacesInFunctionExpression": { "beforeOpeningCurlyBrace": true }, 6 | "disallowSpacesInFunctionExpression": { "beforeOpeningRoundBrace": true }, 7 | "disallowMultipleVarDecl": true, 8 | "disallowSpacesInsideObjectBrackets": true, 9 | "disallowSpacesInsideArrayBrackets": true, 10 | "disallowSpacesInsideParentheses": true, 11 | "disallowSpaceAfterObjectKeys": true, 12 | "requireCommaBeforeLineBreak": true, 13 | "requireOperatorBeforeLineBreak": ["+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 14 | "disallowSpaceAfterBinaryOperators": ["!"], 15 | "disallowSpaceBeforeBinaryOperators": [","], 16 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 17 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 18 | "requireSpaceBeforeBinaryOperators": ["?", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 19 | "requireSpaceAfterBinaryOperators": ["?", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 20 | "requireCamelCaseOrUpperCaseIdentifiers": true, 21 | "disallowKeywords": ["with"], 22 | "disallowMultipleLineStrings": true, 23 | "validateLineBreaks": "LF", 24 | "validateIndentation": "\t", 25 | "disallowMixedSpacesAndTabs": "smart", 26 | "disallowTrailingWhitespace": true, 27 | "requireKeywordsOnNewLine": ["else"], 28 | "requireLineFeedAtFileEnd": true, 29 | "maximumLineLength": 140, 30 | "safeContextKeyword": "that", 31 | "requireDotNotation": true, 32 | "validateJSDoc": { 33 | "checkParamNames": true, 34 | "checkRedundantParams": true, 35 | "requireParamTypes": true 36 | }, 37 | "excludeFiles": ["node_modules/**"] 38 | } 39 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "white": false, 4 | "smarttabs": true, 5 | "eqeqeq": true, 6 | "immed": true, 7 | "latedef": false, 8 | "newcap": true, 9 | "undef": true, 10 | "laxbreak": true 11 | } 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Formula 3 | test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "0.12" 5 | - "4" 6 | - "5" 7 | - "6" 8 | addons: 9 | apt: 10 | packages: 11 | - fontforge 12 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | I love pull requests. And following this simple guidelines will make your pull request easier to merge. 4 | 5 | 6 | ## Submitting pull requests 7 | 8 | 1. Create a new branch, please don’t work in master directly. 9 | 2. Add failing tests (if there’re any tests in project) for the change you want to make. Run tests (usually `grunt` or `npm test`) to see the tests fail. 10 | 3. Hack on. 11 | 4. Run tests to see if the tests pass. Repeat steps 2–4 until done. 12 | 5. Update the documentation to reflect any changes. 13 | 6. Push to your fork and submit a pull request. 14 | 15 | 16 | ## JavaScript code style 17 | 18 | - Tab indentation. 19 | - Single-quotes. 20 | - Semicolon. 21 | - Strict mode. 22 | - No trailing whitespace. 23 | - Variables where needed. 24 | - Multiple variable statements. 25 | - Space after keywords and between arguments and operators. 26 | - Use === and !== over == and !=. 27 | - Return early. 28 | - Limit line lengths to 120 chars. 29 | - Prefer readability over religion. 30 | 31 | Example: 32 | 33 | ```js 34 | 'use strict'; 35 | 36 | function foo(bar, fum) { 37 | if (!bar) return; 38 | 39 | var hello = 'Hello'; 40 | var ret = 0; 41 | for (var barIdx = 0; barIdx < bar.length; barIdx++) { 42 | if (bar[barIdx] === hello) { 43 | ret += fum(bar[barIdx]); 44 | } 45 | } 46 | 47 | return ret; 48 | } 49 | ``` 50 | 51 | 52 | ## Other notes 53 | 54 | - If you have commit access to repo and want to make big change or not sure about something, make a new branch and open pull request. 55 | - Don’t commit generated files: compiled from Stylus CSS, minified JavaScript, etc. 56 | - Don’t change version number and changelog. 57 | - Install [EditorConfig](http://editorconfig.org/) plugin for your code editor. 58 | - If code you change uses different style (probably it’s an old code) use file’s style instead of style described on this page. 59 | - Feel free to [ask me](http://sapegin.me/contacts) anything you need. 60 | 61 | 62 | ## How to run tests 63 | 64 | Install dependencies: 65 | 66 | ```bash 67 | npm install grunt-cli -g 68 | npm install 69 | ``` 70 | 71 | Run: 72 | 73 | ```bash 74 | grunt 75 | ``` 76 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | var path = require('path'); 4 | 5 | module.exports = function(grunt) { 6 | 'use strict'; 7 | 8 | require('load-grunt-tasks')(grunt); 9 | 10 | grunt.initConfig({ 11 | webfont: { 12 | test1: { 13 | src: 'test/src/*.svg', 14 | dest: 'test/tmp/test1', 15 | options: { 16 | hashes: false 17 | } 18 | }, 19 | test2: { 20 | src: 'test/src/*.svg', 21 | dest: 'test/tmp/test2/fonts', 22 | destCss: 'test/tmp/test2', 23 | options: { 24 | font: 'myfont', 25 | types: 'woff,svg', 26 | syntax: 'bootstrap' 27 | } 28 | }, 29 | embed: { 30 | src: 'test/src/*.svg', 31 | dest: 'test/tmp/embed', 32 | options: { 33 | hashes: false, 34 | embed: true 35 | } 36 | }, 37 | embed_woff: { 38 | src: 'test/src/*.svg', 39 | dest: 'test/tmp/embed_woff', 40 | options: { 41 | types: 'woff', 42 | hashes: false, 43 | embed: true 44 | } 45 | }, 46 | embed_ttf: { 47 | src: 'test/src/*.svg', 48 | dest: 'test/tmp/embed_ttf', 49 | options: { 50 | types: 'ttf', 51 | hashes: false, 52 | embed: 'ttf' 53 | } 54 | }, 55 | embed_ttf_woff: { 56 | src: 'test/src/*.svg', 57 | dest: 'test/tmp/embed_ttf_woff', 58 | options: { 59 | types: 'ttf,woff', 60 | hashes: false, 61 | embed: 'ttf,woff' 62 | } 63 | }, 64 | one: { 65 | src: 'test/src_one/*.svg', 66 | dest: 'test/tmp/one', 67 | options: { 68 | hashes: false 69 | } 70 | }, 71 | template: { 72 | src: 'test/src/*.svg', 73 | dest: 'test/tmp/template', 74 | options: { 75 | template: 'test/templates/template.css' 76 | } 77 | }, 78 | template_scss: { 79 | src: 'test/src/*.svg', 80 | dest: 'test/tmp/template_scss', 81 | options: { 82 | stylesheet: 'scss', 83 | template: 'test/templates/template.scss' 84 | } 85 | }, 86 | template_sass: { 87 | src: 'test/src/*.svg', 88 | dest: 'test/tmp/template_sass', 89 | options: { 90 | template: 'test/templates/template.sass' 91 | } 92 | }, 93 | html_template: { 94 | src: 'test/src/*.svg', 95 | dest: 'test/tmp/html_template', 96 | options: { 97 | htmlDemoTemplate: 'test/templates/template.html' 98 | } 99 | }, 100 | html_filename: { 101 | src: 'test/src/*.svg', 102 | dest: 'test/tmp/html_filename', 103 | options: { 104 | htmlDemoFilename: 'index' 105 | } 106 | }, 107 | relative_path: { 108 | src: 'test/src/*.svg', 109 | dest: 'test/tmp/relative_path', 110 | options: { 111 | relativeFontPath: '../iamrelative', 112 | hashes: false 113 | } 114 | }, 115 | sass: { 116 | src: 'test/src/*.svg', 117 | dest: 'test/tmp/sass', 118 | options: { 119 | stylesheet: 'sass' 120 | } 121 | }, 122 | less: { 123 | src: 'test/src/*.svg', 124 | dest: 'test/tmp/less', 125 | options: { 126 | stylesheet: 'less' 127 | } 128 | }, 129 | css_plus_scss: { 130 | src: 'test/src/*.svg', 131 | dest: 'test/tmp/sass', 132 | destCss: 'test/tmp/css', 133 | destScss: 'test/tmp/scss', 134 | options: { 135 | stylesheets: ['css', 'scss'] 136 | } 137 | }, 138 | stylus_bem: { 139 | src: 'test/src/*.svg', 140 | dest: 'test/tmp/stylus_bem', 141 | options: { 142 | stylesheet: 'styl' 143 | } 144 | }, 145 | stylus_bootstrap: { 146 | src: 'test/src/*.svg', 147 | dest: 'test/tmp/stylus_bootstrap', 148 | options: { 149 | stylesheet: 'styl', 150 | syntax: 'bootstrap' 151 | } 152 | }, 153 | spaces: { 154 | src: 'test/src_space/*.svg', 155 | dest: 'test/tmp/spaces' 156 | }, 157 | disable_demo: { 158 | src: 'test/src_one/*.svg', 159 | dest: 'test/tmp/disable_demo', 160 | options: { 161 | htmlDemo: false 162 | } 163 | }, 164 | non_css_demo: { 165 | src: 'test/src/*.svg', 166 | dest: 'test/tmp/non_css_demo', 167 | options: { 168 | stylesheet: 'less', 169 | relativeFontPath: '../iamrelative', 170 | htmlDemo: true 171 | } 172 | }, 173 | parent_source: { 174 | src: '../grunt-webfont/test/src/*.svg', 175 | dest: 'test/tmp/parent_source', 176 | options: { 177 | hashes: false 178 | } 179 | }, 180 | // #167: Ligatures with hypen don’t work 181 | ligatures: { 182 | src: 'test/src_ligatures/*.svg', 183 | dest: 'test/tmp/ligatures', 184 | options: { 185 | hashes: false, 186 | ligatures: true 187 | } 188 | }, 189 | duplicate_names: { 190 | src: '../grunt-webfont/test/src_duplicate_names/**/*.svg', 191 | dest: 'test/tmp/duplicate_names', 192 | options: { 193 | hashes: false, 194 | rename: function(name) { 195 | return [path.basename(path.dirname(name)), path.basename(name)].join('-'); 196 | } 197 | } 198 | }, 199 | order: { 200 | src: 'test/src/*.svg', 201 | dest: 'test/tmp/order', 202 | options: { 203 | types: 'woff,svg', 204 | order: 'svg,woff', 205 | hashes: false 206 | } 207 | }, 208 | template_options: { 209 | src: 'test/src/*.svg', 210 | dest: 'test/tmp/template_options', 211 | options: { 212 | hashes: false, 213 | syntax: 'bem', 214 | stylesheet: 'less', 215 | templateOptions: { 216 | baseClass: 'glyph-icon', 217 | classPrefix: 'glyph_' 218 | } 219 | } 220 | }, 221 | node: { 222 | src: 'test/src/*.svg', 223 | dest: 'test/tmp/node', 224 | options: { 225 | hashes: false, 226 | engine: 'node' 227 | } 228 | }, 229 | ie7: { 230 | src: 'test/src/*.svg', 231 | dest: 'test/tmp/ie7', 232 | options: { 233 | hashes: false, 234 | ie7: true, 235 | syntax: 'bem' 236 | } 237 | }, 238 | ie7_bootstrap: { 239 | src: 'test/src/*.svg', 240 | dest: 'test/tmp/ie7_bootstrap', 241 | options: { 242 | hashes: false, 243 | ie7: true, 244 | syntax: 'bootstrap' 245 | } 246 | }, 247 | optimize_enabled: { 248 | src: 'test/src/*.svg', 249 | dest: 'test/tmp/optimize_enabled', 250 | options: { 251 | engine: 'node', 252 | types: 'svg', 253 | autoHint: false, 254 | optimize: true 255 | } 256 | }, 257 | optimize_disabled: { 258 | src: 'test/src/*.svg', 259 | dest: 'test/tmp/optimize_disabled', 260 | options: { 261 | engine: 'node', 262 | types: 'svg', 263 | autoHint: false, 264 | optimize: false 265 | } 266 | }, 267 | codepoints: { 268 | src: 'test/src/*.svg', 269 | dest: 'test/tmp/codepoints', 270 | options: { 271 | hashes: false, 272 | startCodepoint: 0x41, 273 | codepoints: { 274 | single: 0x43 275 | } 276 | } 277 | }, 278 | camel: { 279 | src: 'test/camel/*.svg', 280 | dest: 'test/tmp/camel', 281 | options: { 282 | hashes: false 283 | } 284 | }, 285 | folders: { 286 | src: 'test/src_folders/**/*.svg', 287 | dest: 'test/tmp/folders', 288 | options: { 289 | hashes: false 290 | } 291 | }, 292 | woff2: { 293 | src: 'test/src/*.svg', 294 | dest: 'test/tmp/woff2', 295 | options: { 296 | types: 'woff2,woff' 297 | } 298 | }, 299 | woff2_node: { 300 | src: 'test/src/*.svg', 301 | dest: 'test/tmp/woff2_node', 302 | options: { 303 | types: 'woff2,woff', 304 | engine: 'node' 305 | } 306 | }, 307 | target_overrides: { 308 | src: 'test/src/*.svg', 309 | options: { 310 | dest: 'test/tmp/target_overrides_icons', 311 | destCss: 'test/tmp/target_overrides_css', 312 | } 313 | }, 314 | font_family_name: { 315 | src: 'test/src/*.svg', 316 | dest: 'test/tmp/font_family_name', 317 | options: { 318 | fontFamilyName: 'customName', 319 | types: 'ttf', 320 | } 321 | }, 322 | custom_output: { 323 | src: 'test/src/*.svg', 324 | options: { 325 | dest: 'test/tmp/custom_output_icons', 326 | destCss: 'test/tmp/custom_output_css', 327 | customOutputs: [{ 328 | template: 'test/templates/custom.js', 329 | dest: 'test/tmp/custom_output/test-icon-config.js' 330 | }, { 331 | template: 'test/templates/custom.json', 332 | dest: 'test/tmp/custom_output' 333 | }, { 334 | template: 'test/templates/context-test.html', 335 | dest: 'test/tmp/custom_output', 336 | context: { 337 | testHeading: 'Hello, world!' 338 | } 339 | }] 340 | } 341 | }, 342 | enabled_template_variables: { 343 | src: 'test/src/*.svg', 344 | dest: 'test/tmp/enabled_template_variables', 345 | options: { 346 | relativeFontPath: '../iamrelative', 347 | fontPathVariables: true, 348 | stylesheets: ['css', 'scss', 'less'] 349 | } 350 | }, 351 | filename_length: { 352 | src: 'test/src_filename_length/*.svg', 353 | dest: 'test/tmp/filename_length', 354 | options: { 355 | autoHint: false, 356 | engine: 'node', 357 | hashes: false, 358 | types: 'woff' 359 | } 360 | }, 361 | }, 362 | nodeunit: { 363 | all: ['test/webfont_test.js'] 364 | }, 365 | jshint: { 366 | all: ['Gruntfile.js', 'tasks/*.js', 'test/*.js'], 367 | options: { 368 | jshintrc: true 369 | } 370 | }, 371 | watch: { 372 | scripts: { 373 | files: '<%= jshint.all %>', 374 | tasks: ['jshint', 'jscs'], 375 | options: { 376 | debounceDelay: 100, 377 | nospawn: true 378 | } 379 | }, 380 | }, 381 | jscs: { 382 | options: { 383 | config: ".jscs.json", 384 | }, 385 | all: ['tasks/*.js'] 386 | }, 387 | clean: ['test/tmp'] 388 | }); 389 | 390 | grunt.loadTasks('tasks'); 391 | 392 | grunt.registerTask('test', ['nodeunit']); 393 | grunt.registerTask('default', ['jshint', 'jscs', 'clean', 'webfont', 'test', 'clean']); 394 | 395 | }; 396 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright © 2014 Artem Sapegin, http://sapegin.me 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Pull_Request_Template.md: -------------------------------------------------------------------------------- 1 | If you want your pull request to be merged, please: 2 | 3 | 1. Explain the use case or bug you’re solving. 4 | 2. Add tests. 5 | 3. Add docs. 6 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # SVG to webfont converter for Grunt 2 | 3 | [![Powered by You](http://sapegin.github.io/powered-by-you/badge.svg)](http://sapegin.github.io/powered-by-you/) 4 | [![Build Status](https://travis-ci.org/sapegin/grunt-webfont.svg)](https://travis-ci.org/sapegin/grunt-webfont) 5 | [![Downloads on npm](http://img.shields.io/npm/dm/grunt-webfont.svg?style=flat)](https://www.npmjs.com/package/grunt-webfont) 6 | 7 | Generate custom icon webfonts from SVG files via Grunt. Inspired by [Font Custom](https://github.com/FontCustom/fontcustom). 8 | 9 | This task will make all you need to use font-face icon on your website: font in all needed formats, CSS/Sass/Less/Stylus and HTML demo page. 10 | 11 | ## Features 12 | 13 | * Works on Mac, Windows and Linux. 14 | * Very flexible. 15 | * Supports all web font formats: WOFF, WOFF2, EOT, TTF and SVG. 16 | * Semantic: uses [Unicode private use area](http://bit.ly/ZnkwaT). 17 | * [Cross-browser](http://www.fontspring.com/blog/further-hardening-of-the-bulletproof-syntax/): IE8+. 18 | * BEM or Bootstrap output CSS style. 19 | * CSS preprocessors support. 20 | * Data:uri embedding. 21 | * Ligatures. 22 | * HTML preview. 23 | * Custom templates. 24 | 25 | 26 | ## Installation 27 | 28 | This plugin requires Grunt 0.4. Note that `ttfautohint` is optional, but your generated font will not be properly hinted if it’s not installed. And make sure you don’t use `ttfautohint` 0.97 because that version won’t work. 29 | 30 | ### OS X 31 | 32 | ``` 33 | brew install ttfautohint fontforge --with-python 34 | npm install grunt-webfont --save-dev 35 | ``` 36 | 37 | *You may need to use `sudo` for `brew`, depending on your setup.* 38 | 39 | *`fontforge` isn’t required for `node` engine (see below).* 40 | 41 | ### Linux 42 | 43 | ``` 44 | sudo apt-get install fontforge ttfautohint 45 | npm install grunt-webfont --save-dev 46 | ``` 47 | 48 | *`fontforge` isn’t required for the `node` engine (see [below](#available-engines)).* 49 | 50 | ### Windows 51 | 52 | ``` 53 | npm install grunt-webfont --save-dev 54 | ``` 55 | 56 | Then [install `ttfautohint`](http://www.freetype.org/ttfautohint/#download) (optional). 57 | 58 | Then install `fontforge`. 59 | * Download and install [fontforge](http://fontforge.github.io/en-US/downloads/windows/). 60 | * Add `C:\Program Files (x86)\FontForgeBuilds\bin` to your `PATH` environment variable. 61 | 62 | *`fontforge` isn’t required for the `node` engine (see [below](#available-engines)).* 63 | 64 | ## Available Engines 65 | 66 | There are two font rendering engines available. See also `engine` option below. 67 | 68 | ### fontforge 69 | 70 | #### Pros 71 | 72 | * All features supported. 73 | * The best results. 74 | 75 | #### Cons 76 | 77 | * You have to install `fontforge`. 78 | * Really weird bugs sometimes. 79 | 80 | ### node 81 | 82 | #### Pros 83 | 84 | * No external dependencies (except optional `ttfautohint`). 85 | * Works on all platforms. 86 | 87 | #### Cons 88 | 89 | * Doesn’t work [with some SVG files](https://github.com/fontello/svg2ttf/issues/25). 90 | * Ligatures aren’t supported. 91 | 92 | 93 | ## Configuration 94 | 95 | Add somewhere in your `Gruntfile.js`: 96 | 97 | ```javascript 98 | grunt.loadNpmTasks('grunt-webfont'); 99 | ``` 100 | 101 | Inside your `Gruntfile.js` file add a section named `webfont`. See Parameters section below for details. 102 | 103 | 104 | ### Parameters 105 | 106 | #### src 107 | 108 | Type: `string|array` 109 | 110 | Glyphs list: SVG. String or array. Wildcards are supported. 111 | 112 | #### dest 113 | 114 | Type: `string` 115 | 116 | Directory for resulting files. 117 | 118 | #### destCss 119 | 120 | Type: `string` Default: _`dest` value_ 121 | 122 | Directory for resulting CSS files (if different than font directory). You can also define `destScss`, `destSass`, `destLess` and `destStyl` to specify a directory per stylesheet type. 123 | 124 | #### Options 125 | 126 | All options should be inside `options` object: 127 | 128 | ``` javascript 129 | webfont: { 130 | icons: { 131 | src: 'icons/*.svg', 132 | dest: 'build/fonts', 133 | options: { 134 | ... 135 | } 136 | } 137 | } 138 | ``` 139 | 140 | #### font 141 | 142 | Type: `string` Default: `icons` 143 | 144 | Name of font and base name of font files. 145 | 146 | #### fontFilename 147 | 148 | Type: `string` Default: Same as `font` option 149 | 150 | Filename for generated font files, you can add placeholders for the same data that gets passed to the [template](#template). 151 | 152 | For example, to get the hash to be part of the filenames: 153 | 154 | ```js 155 | options: { 156 | fontFilename: 'icons-{hash}' 157 | } 158 | ``` 159 | 160 | #### hashes 161 | 162 | Type: `boolean` Default: `true` 163 | 164 | Append font file names with unique string to flush browser cache when you update your icons. 165 | 166 | #### styles 167 | 168 | Type: `string|array` Default: `'font,icon'` 169 | 170 | List of styles to be added to CSS files: `font` (`font-face` declaration), `icon` (base `.icon` class), `extra` (extra stuff for Bootstrap (only for `syntax` = `'bootstrap'`). 171 | 172 | #### types 173 | 174 | Type: `string|array` Default: `'eot,woff,ttf'`, available: `'eot,woff2,woff,ttf,svg'` 175 | 176 | Font files types to generate. 177 | 178 | #### order 179 | 180 | Type: `string|array` Default: `'eot,woff,ttf,svg'` 181 | 182 | Order of `@font-face`’s `src` values in CSS file. (Only file types defined in `types` option will be generated.) 183 | 184 | #### syntax 185 | 186 | Type: `string` Default: `bem` 187 | 188 | Icon classes syntax. `bem` for double class names: `icon icon_awesome` or `bootstrap` for single class names: `icon-awesome`. 189 | 190 | #### template 191 | 192 | Type: `string` Default: `` 193 | 194 | Custom CSS template path (see `tasks/templates` for some examples). Should be used instead of `syntax`. (You probably need to define `htmlDemoTemplate` option too.) 195 | 196 | Template is a pair of CSS and JSON (optional) files with the same name. 197 | 198 | For example, your Gruntfile: 199 | 200 | ```js 201 | options: { 202 | template: 'my_templates/tmpl.css' 203 | } 204 | ``` 205 | 206 | `my_templates/tmpl.css`: 207 | 208 | ```css 209 | @font-face { 210 | font-family:"<%= fontBaseName %>"; 211 | ... 212 | } 213 | ... 214 | ``` 215 | 216 | `my_templates/tmpl.json`: 217 | 218 | ```json 219 | { 220 | "baseClass": "icon", 221 | "classPrefix": "icon_" 222 | } 223 | ``` 224 | 225 | Some extra data is available for you in templates: 226 | 227 | * `hash`: a unique string to flush browser cache. Available even if `hashes` option is `false`. 228 | 229 | * `fontRawSrcs`: array of font-face’s src values not merged to a single line: 230 | 231 | ``` 232 | [ 233 | [ 234 | 'url("icons.eot")' 235 | ], 236 | [ 237 | 'url("icons.eot?#iefix") format("embedded-opentype")', 238 | 'url("icons.woff") format("woff")', 239 | 'url("icons.ttf") format("truetype")' 240 | ] 241 | ] 242 | ``` 243 | 244 | 245 | #### templateOptions 246 | 247 | Type: `object` Default: `{}` 248 | 249 | Extends/overrides CSS template or syntax’s JSON file. Allows custom class names in default css templates. 250 | 251 | ``` javascript 252 | options: { 253 | templateOptions: { 254 | baseClass: 'glyph-icon', 255 | classPrefix: 'glyph_' 256 | } 257 | } 258 | ``` 259 | 260 | #### stylesheets 261 | 262 | Type: `array` Default: `['css']` or extension of `template` 263 | 264 | Stylesheet type. Can be `css`, `sass`, `scss` or `less`. If `sass` or `scss` is used, `_` will prefix the file (so it can be a used as a partial). You can define just `stylesheet` if you are generating just one type. 265 | 266 | #### relativeFontPath 267 | 268 | Type: `string` Default: `null` 269 | 270 | Custom font path. Will be used instead of `destCss` *in* CSS file. Useful with CSS preprocessors. 271 | 272 | #### fontPathVariables 273 | 274 | Type: `boolean` Default: `false` 275 | 276 | Create font-path variables for `less`, `scss` and `sass` files. Can be used to override the `relativeFontPath` 277 | in custom preprocessor tasks or configs. 278 | 279 | The variable name is a combination of the `font` name appended with `-font-path`. 280 | 281 | 282 | #### version 283 | 284 | Type: `string` Default: `false` 285 | 286 | Version number added to `.ttf` version of the font (FontForge Engine only). Also used in the heading of the default demo.html template. Useful to align with the version of other assets that are part of a larger system. 287 | 288 | #### htmlDemo 289 | 290 | Type: `boolean` Default: `true` 291 | 292 | If `true`, an HTML file will be available (by default, in `destCSS` folder) to test the render. 293 | 294 | #### htmlDemoTemplate 295 | 296 | Type: `string` Default: `null` 297 | 298 | Custom demo HTML template path (see `tasks/templates/demo.html` for an example) (requires `htmlDemo` option to be true). 299 | 300 | #### htmlDemoFilename 301 | 302 | Type: `string` Default: _`fontBaseName` value_ 303 | 304 | Custom name for the demo HTML file (requires `htmlDemo` option to be true). Useful if you want to name the output something like `index.html` instead of the font name. 305 | 306 | #### destHtml 307 | 308 | Type: `string` Default: _`destCss` value_ 309 | 310 | Custom demo HTML demo path (requires `htmlDemo` option to be true). 311 | 312 | #### embed 313 | 314 | Type: `string|array` Default: `false` 315 | 316 | If `true` embeds WOFF (*only WOFF*) file as data:uri. 317 | 318 | IF `ttf` or `woff` or `ttf,woff` embeds TTF or/and WOFF file. 319 | 320 | If there are more file types in `types` option they will be included as usual `url(font.type)` CSS links. 321 | 322 | #### ligatures 323 | 324 | Type: `boolean` Default: `false` 325 | 326 | If `true` the generated font files and stylesheets will be generated with opentype ligature features. The character sequences to be replaced by the ligatures are determined by the file name (without extension) of the original SVG. 327 | 328 | For example, you have a heart icon in `love.svg` file. The HTML `

I love you!

` will be rendered as `I ♥ you!`. 329 | 330 | #### rename 331 | 332 | Type: `function` Default: `path.basename` 333 | 334 | You can use this function to change how file names translates to class names (the part after `icon_` or `icon-`). By default it’s a name of a file. 335 | 336 | For example you can group your icons into several folders and add folder name to class name: 337 | 338 | ```js 339 | options: { 340 | rename: function(name) { 341 | // .icon_entypo-add, .icon_fontawesome-add, etc. 342 | return [path.basename(path.dirname(name)), path.basename(name)].join('-'); 343 | } 344 | } 345 | ``` 346 | 347 | #### skip 348 | 349 | Type: `boolean` Default: `false` 350 | 351 | If `true` task will not be ran. In example, you can skip task on Windows (becase of difficult installation): 352 | 353 | ```javascript 354 | options: { 355 | skip: require('os').platform() === 'win32' 356 | } 357 | ``` 358 | 359 | #### engine 360 | 361 | Type: `string` Default: `fontforge` 362 | 363 | Font rendering engine: `fontforge` or `node`. See comparison in [Available Engines](#available-engines) section above. 364 | 365 | #### ie7 366 | 367 | Type: `boolean` Default: `false` 368 | 369 | Adds IE7 support using a `*zoom: expression()` hack. 370 | 371 | #### optimize 372 | 373 | Type: `boolean` Default: `true` 374 | 375 | If `false` the SVGO optimization will not be used. This is useful in cases where the optimizer will produce faulty web fonts by removing relevant SVG paths or attributes. 376 | 377 | #### normalize 378 | 379 | Type: `boolean` Default: `false` 380 | 381 | When using the fontforge engine, if false, glyphs will be generated with a fixed width equal to fontHeight. In most cases, this will produce an extra blank space for each glyph. If set to true, no extra space will be generated. Each glyph will have a width that matches its boundaries. 382 | 383 | #### startCodepoint 384 | 385 | Type: `integer` Default: `0xF101` 386 | 387 | Starting codepoint used for the generated glyphs. Defaults to the start of the Unicode private use area. 388 | 389 | #### codepoints 390 | 391 | Type: `object` Default: `null` 392 | 393 | Specific codepoints to use for certain glyphs. Any glyphs not specified in the codepoints block will be given incremented as usual from the `startCodepoint`, skipping duplicates. 394 | 395 | ```javascript 396 | options: { 397 | codepoints: { 398 | single: 0xE001 399 | } 400 | } 401 | ``` 402 | 403 | #### codepointsFile 404 | Type: `string` Default: `null` 405 | 406 | Uses and Saves the codepoint mapping by name to this file. 407 | 408 | NOTE: will overwrite the set codepoints option. 409 | 410 | #### autoHint 411 | 412 | Type: `boolean` Default: `true` 413 | 414 | Enables font auto hinting using `ttfautohint`. 415 | 416 | #### round 417 | 418 | Type: `number` Default: `10e12` 419 | 420 | Setup SVG path rounding. 421 | 422 | #### fontHeight 423 | 424 | Type: `number` Default: `512` 425 | 426 | The output font height. 427 | 428 | #### fontFamilyName 429 | 430 | Type: `string` Default: _`font` value_ 431 | 432 | If you’d like your generated fonts to have a name that’s different than the `font` value, you can specify this as a string. This will allow a unique display name within design authoring tools when installing fonts locally. For example, your font’s name could be `GitHub Octicons` with a filename of `octicons.ttf`. 433 | 434 | ```javascript 435 | options: { 436 | fontFamilyName: 'GitHub Octicons', 437 | } 438 | ``` 439 | 440 | #### descent 441 | 442 | Type: `number` Default: `64` 443 | 444 | The font descent. The descent should be a positive value. The ascent formula is: `ascent = fontHeight - descent`. 445 | 446 | #### callback 447 | 448 | Type: `function` Default: `null` 449 | 450 | Allows for a callback to be called when the task has completed and passes in the filename of the generated font, an array of the various font types created, an array of all the glyphs created and the hash used to flush browser cache. 451 | 452 | ```javascript 453 | options: { 454 | callback: function(filename, types, glyphs, hash) { 455 | // ... 456 | } 457 | } 458 | ``` 459 | 460 | #### customOutputs 461 | 462 | Type: `array` Default: `undefined` 463 | 464 | Allows for custom content to be generated and output in the same way as `htmlDemo`. 465 | 466 | Each entry in `customOutputs` should be an object with the following parameters: 467 | 468 | * `template` - (`string`) the path to the underscore-template you wish to use. 469 | * `dest` - (`string`) the path to the destination where you want the resulting file to live. 470 | * `context` \[optional\] - (`object`) a hash of values to pass into the context of the template 471 | 472 | At compile-time each template will have access to the same context as the compile-time environment of `htmlDemoTemplate` (as extended by the `context` object, if provided. See config-example below. 473 | 474 | #### execMaxBuffer 475 | If you get stderr maxBuffer exceeded warning message, engine probably logged a lot of warning messages. To see this warnings run grunt in verbose mode `grunt --verbose`. To go over this warning you can try to increase buffer size by this option. Default value is `1024 * 200` 476 | 477 | ### Config Examples 478 | 479 | #### Simple font generation 480 | 481 | ```javascript 482 | webfont: { 483 | icons: { 484 | src: 'icons/*.svg', 485 | dest: 'build/fonts' 486 | } 487 | } 488 | ``` 489 | 490 | #### Custom font name, fonts and CSS in different folders 491 | 492 | ```javascript 493 | webfont: { 494 | icons: { 495 | src: 'icons/*.svg', 496 | dest: 'build/fonts', 497 | destCss: 'build/fonts/css', 498 | options: { 499 | font: 'ponies' 500 | } 501 | } 502 | } 503 | ``` 504 | 505 | #### Custom CSS classes 506 | 507 | ```js 508 | webfont: { 509 | icons: { 510 | src: 'icons/*.svg', 511 | dest: 'build/fonts', 512 | options: { 513 | syntax: 'bem', 514 | templateOptions: { 515 | baseClass: 'glyph-icon', 516 | classPrefix: 'glyph_' 517 | } 518 | } 519 | } 520 | } 521 | ``` 522 | 523 | #### To use with CSS preprocessor 524 | 525 | ```javascript 526 | webfont: { 527 | icons: { 528 | src: 'icons/*.svg', 529 | dest: 'build/fonts', 530 | destCss: 'build/styles', 531 | options: { 532 | stylesheet: 'styl', 533 | relativeFontPath: '/build/fonts' 534 | } 535 | } 536 | } 537 | ``` 538 | 539 | #### Embedded font file 540 | 541 | ```javascript 542 | webfont: { 543 | icons: { 544 | src: 'icons/*.svg', 545 | dest: 'build/fonts', 546 | options: { 547 | types: 'woff', 548 | embed: true 549 | } 550 | } 551 | } 552 | ``` 553 | 554 | #### Custom Outputs 555 | 556 | ```javascript 557 | webfont: { 558 | icons: { 559 | src: 'icons/*.svg', 560 | dest: 'build/fonts', 561 | options: { 562 | customOutputs: [{ 563 | template: 'templates/icon-glyph-list-boilerplate.js', 564 | dest: 'build/js/icon-glyph-list.js' 565 | }, { 566 | template: 'templates/icon-glyph-config-boilerplate.json', 567 | dest: 'build/js/icon-glyphs.json' 568 | }, { 569 | template: 'templates/icon-web-home.html', 570 | dest: 'build/', 571 | context: { 572 | homeHeading: 'Your Icon Font', 573 | homeMessage: 'The following glyphs are available in this font:' 574 | } 575 | }] 576 | } 577 | } 578 | } 579 | ``` 580 | 581 | We might then include the following corresponding templates. 582 | 583 | The first, for `icon-glyph-list-boilerplate.js`, a file that outputs a list of icon-glyph slugs. 584 | 585 | ``` 586 | // file: icon-glyph-list-boilerplate.js 587 | 588 | (function(window) { 589 | 'use strict'; 590 | 591 | var iconList = <%= JSON.stringify(glyphs) %>; 592 | window.iconList = iconList; 593 | }(this)); 594 | ``` 595 | 596 | The second, for `icon-glyph-config-boilerplate.json`, a file that dumps all JSON data in the current template context. 597 | 598 | ``` 599 | // file: icon-glyph-config-boilerplate.json 600 | 601 | <%= JSON.stringify(arguments[0], null, '\t') %> 602 | ``` 603 | 604 | And finally, the third, for `icon-web-home.html`, a file that has access to the values provided in the `context` object supplied. 605 | 606 | ``` 607 | // file: icon-web-home.html 608 | 609 | 610 | 611 | 612 | 613 | Context Test 614 | 615 | 616 |

<%= homeHeading %>

617 |

<%= homeMessage %>

618 | 623 | 624 | 625 | ``` 626 | 627 | ## CSS Preprocessors Caveats 628 | 629 | You can change CSS file syntax using `stylesheet` option (see above). It change file extension (so you can specify any) with some tweaks. Replace all comments with single line comments (which will be removed after compilation). 630 | 631 | ### Dynamic font-path 632 | You can enable the `fontPathVariables` in combination with `relativeFontPath` to create a overridable font-path. 633 | 634 | For example scss: 635 | ```scss 636 | $icons-font-path : "/relativeFontPath/" !default; 637 | @font-face { 638 | font-family:"icons"; 639 | src:url($icons-font-path + "icons.eot"); 640 | src:url($icons-font-path + "icons.eot?#iefix") format("embedded-opentype"), 641 | url($icons-font-path + "icons.woff") format("woff"), 642 | url($icons-font-path + "icons.ttf") format("truetype"); 643 | font-weight:normal; 644 | font-style:normal; 645 | } 646 | ``` 647 | 648 | ### Sass 649 | 650 | If `stylesheet` option is `sass` or `scss`, `_` will prefix the file (so it can be a used as a partial). 651 | 652 | ### Less 653 | 654 | If `stylesheet` option is `less`, regular CSS icon classes will be expanded with corresponding Less mixins. 655 | 656 | The Less mixins then may be used like so: 657 | 658 | ```css 659 | .profile-button { 660 | .icon-profile; 661 | } 662 | ``` 663 | 664 | ## Troubleshooting 665 | 666 | ### I have problems displaying the font in Firefox 667 | 668 | Firefox doesn’t allow cross-domain fonts: [Specifications](http://www.w3.org/TR/css3-fonts/#font-fetching-requirements), [Bugzilla Ticket](https://bugzilla.mozilla.org/show_bug.cgi?id=604421), [How to fix it](https://coderwall.com/p/v4uwyq). 669 | 670 | ### My images are getting corrupted 671 | 672 | #### Using the node engine 673 | 674 | * Certain SVG's are not supported. See the [svg2ttf](https://github.com/fontello/svg2ttf) project which is used to convert from SVG to TTF (which is then converted forward to WOFF and WOFF2). 675 | * `autoHint` also adjusts the font file and can cause your font to look different to the SVG, so you could try switching it off (though it may make windows view of the font worse). 676 | 677 | #### Using fontforge 678 | 679 | Check the following... 680 | 681 | * Your paths are clockwise. Anti-clockwise paths may cause fills to occur differently. 682 | * Your paths are not overlapping. Overlapping paths will cause one of the areas to be inverted rather than combined. Use an editor to union your two paths together. 683 | * `autoHint` also adjusts the font file and can cause your font to look different to the SVG, so you could try switching it off (though it may make windows view of the font worse). 684 | * If you get stderr maxBuffer exceeded warning message, fontforge probably logged a lot of warning messages. To see this warnings run grunt in verbose mode `grunt --verbose`. To go over this warning you can try to increase buffer size by [execMaxBuffer](#execMaxBuffer). 685 | 686 | ## Changelog 687 | 688 | The changelog can be found on the [Releases page](https://github.com/sapegin/grunt-webfont/releases). 689 | 690 | ## License 691 | 692 | The MIT License, see the included [License.md](License.md) file. 693 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-webfont", 3 | "description": "Ultimate SVG to webfont converter for Grunt.", 4 | "version": "1.7.2", 5 | "homepage": "https://github.com/sapegin/grunt-webfont", 6 | "author": { 7 | "name": "Artem Sapegin", 8 | "url": "http://sapegin.me/" 9 | }, 10 | "contributors": [ 11 | "Maxime Thirouin (http://moox.io/)", 12 | "Aaron Lampros (https://github.com/alampros)", 13 | "Cyrille Meichel (https://github.com/landru29)" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/sapegin/grunt-webfont.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/sapegin/grunt-webfont/issues" 21 | }, 22 | "license": "MIT", 23 | "main": "tasks/webfont.js", 24 | "scripts": { 25 | "test": "grunt --stack" 26 | }, 27 | "engines": { 28 | "node": ">=0.12.0" 29 | }, 30 | "dependencies": { 31 | "async": "~1.5.2", 32 | "chalk": "~1.1.1", 33 | "glob": "~7.0.0", 34 | "lodash": "~4.17.10", 35 | "memorystream": "~0.3.1", 36 | "mkdirp": "~0.5.1", 37 | "svg2ttf": "~2.1.1", 38 | "svgicons2svgfont": "~1.1.0", 39 | "svgo": "~0.6.1", 40 | "temp": "~0.8.3", 41 | "ttf2eot": "~1.3.0", 42 | "ttf2woff": "~1.3.0", 43 | "ttf2woff2": "~2.0.3", 44 | "underscore.string": "~3.2.3", 45 | "winston": "~2.1.1" 46 | }, 47 | "devDependencies": { 48 | "grunt": "~0.4.5", 49 | "grunt-cli": "~0.1.13", 50 | "grunt-contrib-clean": "~1.0.0", 51 | "grunt-contrib-jshint": "~0.11.3", 52 | "grunt-contrib-nodeunit": "~0.4.1", 53 | "grunt-contrib-watch": "~0.6.1", 54 | "grunt-jscs": "~1.0.0", 55 | "load-grunt-tasks": "~3.4.0", 56 | "stylus": "~0.53.0", 57 | "xml2js": "~0.4.16" 58 | }, 59 | "peerDependencies": { 60 | "grunt": ">=0.4.0" 61 | }, 62 | "keywords": [ 63 | "gruntplugin", 64 | "font", 65 | "webfont", 66 | "fontforge", 67 | "font-face", 68 | "woff", 69 | "woff2", 70 | "ttf", 71 | "svg", 72 | "eot", 73 | "truetype", 74 | "css", 75 | "icon" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /tasks/bin/eotlitetool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # This is special grunt-webfont verion of eotlitetool.py. 5 | # https://github.com/sapegin/grunt-webfont 6 | # 7 | # Changes: 8 | # * Output option now works. 9 | # * Compatible with Python 3. 10 | # 11 | 12 | # ***** BEGIN LICENSE BLOCK ***** 13 | # Version: MPL 1.1/GPL 2.0/LGPL 2.1 14 | # 15 | # The contents of this file are subject to the Mozilla Public License Version 16 | # 1.1 (the "License"); you may not use this file except in compliance with 17 | # the License. You may obtain a copy of the License at 18 | # http://www.mozilla.org/MPL/ 19 | # 20 | # Software distributed under the License is distributed on an "AS IS" basis, 21 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 22 | # for the specific language governing rights and limitations under the 23 | # License. 24 | # 25 | # The Original Code is font utility code. 26 | # 27 | # The Initial Developer of the Original Code is Mozilla Corporation. 28 | # Portions created by the Initial Developer are Copyright (C) 2009 29 | # the Initial Developer. All Rights Reserved. 30 | # 31 | # Contributor(s): 32 | # John Daggett 33 | # 34 | # Alternatively, the contents of this file may be used under the terms of 35 | # either the GNU General Public License Version 2 or later (the "GPL"), or 36 | # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 37 | # in which case the provisions of the GPL or the LGPL are applicable instead 38 | # of those above. If you wish to allow use of your version of this file only 39 | # under the terms of either the GPL or the LGPL, and not to allow others to 40 | # use your version of this file under the terms of the MPL, indicate your 41 | # decision by deleting the provisions above and replace them with the notice 42 | # and other provisions required by the GPL or the LGPL. If you do not delete 43 | # the provisions above, a recipient may use your version of this file under 44 | # the terms of any one of the MPL, the GPL or the LGPL. 45 | # 46 | # ***** END LICENSE BLOCK ***** */ 47 | 48 | # eotlitetool.py - create EOT version of OpenType font for use with IE 49 | # 50 | # Usage: eotlitetool.py [-o output-filename] font1 [font2 ...] 51 | # 52 | 53 | # OpenType file structure 54 | # http://www.microsoft.com/typography/otspec/otff.htm 55 | # 56 | # Types: 57 | # 58 | # BYTE 8-bit unsigned integer. 59 | # CHAR 8-bit signed integer. 60 | # USHORT 16-bit unsigned integer. 61 | # SHORT 16-bit signed integer. 62 | # ULONG 32-bit unsigned integer. 63 | # Fixed 32-bit signed fixed-point number (16.16) 64 | # LONGDATETIME Date represented in number of seconds since 12:00 midnight, January 1, 1904. The value is represented as a signed 64-bit integer. 65 | # 66 | # SFNT Header 67 | # 68 | # Fixed sfnt version // 0x00010000 for version 1.0. 69 | # USHORT numTables // Number of tables. 70 | # USHORT searchRange // (Maximum power of 2 <= numTables) x 16. 71 | # USHORT entrySelector // Log2(maximum power of 2 <= numTables). 72 | # USHORT rangeShift // NumTables x 16-searchRange. 73 | # 74 | # Table Directory 75 | # 76 | # ULONG tag // 4-byte identifier. 77 | # ULONG checkSum // CheckSum for this table. 78 | # ULONG offset // Offset from beginning of TrueType font file. 79 | # ULONG length // Length of this table. 80 | # 81 | # OS/2 Table (Version 4) 82 | # 83 | # USHORT version // 0x0004 84 | # SHORT xAvgCharWidth 85 | # USHORT usWeightClass 86 | # USHORT usWidthClass 87 | # USHORT fsType 88 | # SHORT ySubscriptXSize 89 | # SHORT ySubscriptYSize 90 | # SHORT ySubscriptXOffset 91 | # SHORT ySubscriptYOffset 92 | # SHORT ySuperscriptXSize 93 | # SHORT ySuperscriptYSize 94 | # SHORT ySuperscriptXOffset 95 | # SHORT ySuperscriptYOffset 96 | # SHORT yStrikeoutSize 97 | # SHORT yStrikeoutPosition 98 | # SHORT sFamilyClass 99 | # BYTE panose[10] 100 | # ULONG ulUnicodeRange1 // Bits 0-31 101 | # ULONG ulUnicodeRange2 // Bits 32-63 102 | # ULONG ulUnicodeRange3 // Bits 64-95 103 | # ULONG ulUnicodeRange4 // Bits 96-127 104 | # CHAR achVendID[4] 105 | # USHORT fsSelection 106 | # USHORT usFirstCharIndex 107 | # USHORT usLastCharIndex 108 | # SHORT sTypoAscender 109 | # SHORT sTypoDescender 110 | # SHORT sTypoLineGap 111 | # USHORT usWinAscent 112 | # USHORT usWinDescent 113 | # ULONG ulCodePageRange1 // Bits 0-31 114 | # ULONG ulCodePageRange2 // Bits 32-63 115 | # SHORT sxHeight 116 | # SHORT sCapHeight 117 | # USHORT usDefaultChar 118 | # USHORT usBreakChar 119 | # USHORT usMaxContext 120 | # 121 | # 122 | # The Naming Table is organized as follows: 123 | # 124 | # [name table header] 125 | # [name records] 126 | # [string data] 127 | # 128 | # Name Table Header 129 | # 130 | # USHORT format // Format selector (=0). 131 | # USHORT count // Number of name records. 132 | # USHORT stringOffset // Offset to start of string storage (from start of table). 133 | # 134 | # Name Record 135 | # 136 | # USHORT platformID // Platform ID. 137 | # USHORT encodingID // Platform-specific encoding ID. 138 | # USHORT languageID // Language ID. 139 | # USHORT nameID // Name ID. 140 | # USHORT length // String length (in bytes). 141 | # USHORT offset // String offset from start of storage area (in bytes). 142 | # 143 | # head Table 144 | # 145 | # Fixed tableVersion // Table version number 0x00010000 for version 1.0. 146 | # Fixed fontRevision // Set by font manufacturer. 147 | # ULONG checkSumAdjustment // To compute: set it to 0, sum the entire font as ULONG, then store 0xB1B0AFBA - sum. 148 | # ULONG magicNumber // Set to 0x5F0F3CF5. 149 | # USHORT flags 150 | # USHORT unitsPerEm // Valid range is from 16 to 16384. This value should be a power of 2 for fonts that have TrueType outlines. 151 | # LONGDATETIME created // Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer 152 | # LONGDATETIME modified // Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer 153 | # SHORT xMin // For all glyph bounding boxes. 154 | # SHORT yMin 155 | # SHORT xMax 156 | # SHORT yMax 157 | # USHORT macStyle 158 | # USHORT lowestRecPPEM // Smallest readable size in pixels. 159 | # SHORT fontDirectionHint 160 | # SHORT indexToLocFormat // 0 for short offsets, 1 for long. 161 | # SHORT glyphDataFormat // 0 for current format. 162 | # 163 | # 164 | # 165 | # Embedded OpenType (EOT) file format 166 | # http://www.w3.org/Submission/EOT/ 167 | # 168 | # EOT version 0x00020001 169 | # 170 | # An EOT font consists of a header with the original OpenType font 171 | # appended at the end. Most of the data in the EOT header is simply a 172 | # copy of data from specific tables within the font data. The exceptions 173 | # are the 'Flags' field and the root string name field. The root string 174 | # is a set of names indicating domains for which the font data can be 175 | # used. A null root string implies the font data can be used anywhere. 176 | # The EOT header is in little-endian byte order but the font data remains 177 | # in big-endian order as specified by the OpenType spec. 178 | # 179 | # Overall structure: 180 | # 181 | # [EOT header] 182 | # [EOT name records] 183 | # [font data] 184 | # 185 | # EOT header 186 | # 187 | # ULONG eotSize // Total structure length in bytes (including string and font data) 188 | # ULONG fontDataSize // Length of the OpenType font (FontData) in bytes 189 | # ULONG version // Version number of this format - 0x00020001 190 | # ULONG flags // Processing Flags (0 == no special processing) 191 | # BYTE fontPANOSE[10] // OS/2 Table panose 192 | # BYTE charset // DEFAULT_CHARSET (0x01) 193 | # BYTE italic // 0x01 if ITALIC in OS/2 Table fsSelection is set, 0 otherwise 194 | # ULONG weight // OS/2 Table usWeightClass 195 | # USHORT fsType // OS/2 Table fsType (specifies embedding permission flags) 196 | # USHORT magicNumber // Magic number for EOT file - 0x504C. 197 | # ULONG unicodeRange1 // OS/2 Table ulUnicodeRange1 198 | # ULONG unicodeRange2 // OS/2 Table ulUnicodeRange2 199 | # ULONG unicodeRange3 // OS/2 Table ulUnicodeRange3 200 | # ULONG unicodeRange4 // OS/2 Table ulUnicodeRange4 201 | # ULONG codePageRange1 // OS/2 Table ulCodePageRange1 202 | # ULONG codePageRange2 // OS/2 Table ulCodePageRange2 203 | # ULONG checkSumAdjustment // head Table CheckSumAdjustment 204 | # ULONG reserved[4] // Reserved - must be 0 205 | # USHORT padding1 // Padding - must be 0 206 | # 207 | # EOT name records 208 | # 209 | # USHORT FamilyNameSize // Font family name size in bytes 210 | # BYTE FamilyName[FamilyNameSize] // Font family name (name ID = 1), little-endian UTF-16 211 | # USHORT Padding2 // Padding - must be 0 212 | # 213 | # USHORT StyleNameSize // Style name size in bytes 214 | # BYTE StyleName[StyleNameSize] // Style name (name ID = 2), little-endian UTF-16 215 | # USHORT Padding3 // Padding - must be 0 216 | # 217 | # USHORT VersionNameSize // Version name size in bytes 218 | # bytes VersionName[VersionNameSize] // Version name (name ID = 5), little-endian UTF-16 219 | # USHORT Padding4 // Padding - must be 0 220 | # 221 | # USHORT FullNameSize // Full name size in bytes 222 | # BYTE FullName[FullNameSize] // Full name (name ID = 4), little-endian UTF-16 223 | # USHORT Padding5 // Padding - must be 0 224 | # 225 | # USHORT RootStringSize // Root string size in bytes 226 | # BYTE RootString[RootStringSize] // Root string, little-endian UTF-16 227 | 228 | 229 | 230 | import optparse 231 | import struct 232 | 233 | class FontError(Exception): 234 | """Error related to font handling""" 235 | pass 236 | 237 | def multichar(str): 238 | vals = struct.unpack('4B', (str[:4]).encode()) 239 | return (vals[0] << 24) + (vals[1] << 16) + (vals[2] << 8) + vals[3] 240 | 241 | def multicharval(v): 242 | return struct.pack('4B', (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF) 243 | 244 | class EOT: 245 | EOT_VERSION = 0x00020001 246 | EOT_MAGIC_NUMBER = 0x504c 247 | EOT_DEFAULT_CHARSET = 0x01 248 | EOT_FAMILY_NAME_INDEX = 0 # order of names in variable portion of EOT header 249 | EOT_STYLE_NAME_INDEX = 1 250 | EOT_VERSION_NAME_INDEX = 2 251 | EOT_FULL_NAME_INDEX = 3 252 | EOT_NUM_NAMES = 4 253 | 254 | EOT_HEADER_PACK = '<4L10B2BL2H7L18x' 255 | 256 | class OpenType: 257 | SFNT_CFF = multichar('OTTO') # Postscript CFF SFNT version 258 | SFNT_TRUE = 0x10000 # Standard TrueType version 259 | SFNT_APPLE = multichar('true') # Apple TrueType version 260 | 261 | SFNT_UNPACK = '>I4H' 262 | TABLE_DIR_UNPACK = '>4I' 263 | 264 | TABLE_HEAD = multichar('head') # TrueType table tags 265 | TABLE_NAME = multichar('name') 266 | TABLE_OS2 = multichar('OS/2') 267 | TABLE_GLYF = multichar('glyf') 268 | TABLE_CFF = multichar('CFF ') 269 | 270 | OS2_FSSELECTION_ITALIC = 0x1 271 | OS2_UNPACK = '>4xH2xH22x10B4L4xH14x2L' 272 | 273 | HEAD_UNPACK = '>8xL' 274 | 275 | NAME_RECORD_UNPACK = '>6H' 276 | NAME_ID_FAMILY = 1 277 | NAME_ID_STYLE = 2 278 | NAME_ID_UNIQUE = 3 279 | NAME_ID_FULL = 4 280 | NAME_ID_VERSION = 5 281 | NAME_ID_POSTSCRIPT = 6 282 | PLATFORM_ID_UNICODE = 0 # Mac OS uses this typically 283 | PLATFORM_ID_MICROSOFT = 3 284 | ENCODING_ID_MICROSOFT_UNICODEBMP = 1 # with Microsoft platformID BMP-only Unicode encoding 285 | LANG_ID_MICROSOFT_EN_US = 0x0409 # with Microsoft platformID EN US lang code 286 | 287 | def eotname(ttf): 288 | i = ttf.rfind('.') 289 | if i != -1: 290 | ttf = ttf[:i] 291 | return ttf + '.eotlite' 292 | 293 | def readfont(f): 294 | data = open(f, 'rb').read() 295 | return data 296 | 297 | def get_table_directory(data): 298 | """read the SFNT header and table directory""" 299 | datalen = len(data) 300 | sfntsize = struct.calcsize(OpenType.SFNT_UNPACK) 301 | if sfntsize > datalen: 302 | raise FontError('truncated font data') 303 | sfntvers, numTables = struct.unpack(OpenType.SFNT_UNPACK, data[:sfntsize])[:2] 304 | if sfntvers != OpenType.SFNT_CFF and sfntvers != OpenType.SFNT_TRUE: 305 | raise FontError('invalid font type') 306 | 307 | font = {} 308 | font['version'] = sfntvers 309 | font['numTables'] = numTables 310 | 311 | # create set of offsets, lengths for tables 312 | table_dir_size = struct.calcsize(OpenType.TABLE_DIR_UNPACK) 313 | if sfntsize + table_dir_size * numTables > datalen: 314 | raise FontError('truncated font data, table directory extends past end of data') 315 | table_dir = {} 316 | for i in range(0, numTables): 317 | start = sfntsize + i * table_dir_size 318 | end = start + table_dir_size 319 | tag, check, bongo, dirlen = struct.unpack(OpenType.TABLE_DIR_UNPACK, data[start:end]) 320 | table_dir[tag] = {'offset': bongo, 'length': dirlen, 'checksum': check} 321 | 322 | font['tableDir'] = table_dir 323 | 324 | return font 325 | 326 | def get_name_records(nametable): 327 | """reads through the name records within name table""" 328 | name = {} 329 | # read the header 330 | headersize = 6 331 | count, strOffset = struct.unpack('>2H', nametable[2:6]) 332 | namerecsize = struct.calcsize(OpenType.NAME_RECORD_UNPACK) 333 | if count * namerecsize + headersize > len(nametable): 334 | raise FontError('names exceed size of name table') 335 | name['count'] = count 336 | name['strOffset'] = strOffset 337 | 338 | # read through the name records 339 | namerecs = {} 340 | for i in range(0, count): 341 | start = headersize + i * namerecsize 342 | end = start + namerecsize 343 | platformID, encodingID, languageID, nameID, namelen, offset = struct.unpack(OpenType.NAME_RECORD_UNPACK, nametable[start:end]) 344 | if platformID != OpenType.PLATFORM_ID_MICROSOFT or \ 345 | encodingID != OpenType.ENCODING_ID_MICROSOFT_UNICODEBMP or \ 346 | languageID != OpenType.LANG_ID_MICROSOFT_EN_US: 347 | continue 348 | namerecs[nameID] = {'offset': offset, 'length': namelen} 349 | 350 | name['namerecords'] = namerecs 351 | return name 352 | 353 | def make_eot_name_headers(fontdata, nameTableDir): 354 | """extracts names from the name table and generates the names header portion of the EOT header""" 355 | nameoffset = nameTableDir['offset'] 356 | namelen = nameTableDir['length'] 357 | name = get_name_records(fontdata[nameoffset : nameoffset + namelen]) 358 | namestroffset = name['strOffset'] 359 | namerecs = name['namerecords'] 360 | 361 | eotnames = (OpenType.NAME_ID_FAMILY, OpenType.NAME_ID_STYLE, OpenType.NAME_ID_VERSION, OpenType.NAME_ID_FULL) 362 | nameheaders = [] 363 | for nameid in eotnames: 364 | if nameid in namerecs: 365 | namerecord = namerecs[nameid] 366 | noffset = namerecord['offset'] 367 | nlen = namerecord['length'] 368 | nformat = '%dH' % (nlen / 2) # length is in number of bytes 369 | start = nameoffset + namestroffset + noffset 370 | end = start + nlen 371 | nstr = struct.unpack('>' + nformat, fontdata[start:end]) 372 | nameheaders.append(struct.pack(' os2Dir['length']: 411 | raise FontError('OS/2 table invalid length') 412 | 413 | os2fields = struct.unpack(OpenType.OS2_UNPACK, fontdata[os2offset : os2offset + os2size]) 414 | 415 | panose = [] 416 | urange = [] 417 | codepage = [] 418 | 419 | weight, fsType = os2fields[:2] 420 | panose[:10] = os2fields[2:12] 421 | urange[:4] = os2fields[12:16] 422 | fsSelection = os2fields[16] 423 | codepage[:2] = os2fields[17:19] 424 | 425 | italic = fsSelection & OpenType.OS2_FSSELECTION_ITALIC 426 | 427 | # read in values from head table 428 | headDir = tableDir[OpenType.TABLE_HEAD] 429 | headoffset = headDir['offset'] 430 | headsize = struct.calcsize(OpenType.HEAD_UNPACK) 431 | 432 | if headsize > headDir['length']: 433 | raise FontError('head table invalid length') 434 | 435 | headfields = struct.unpack(OpenType.HEAD_UNPACK, fontdata[headoffset : headoffset + headsize]) 436 | checkSumAdjustment = headfields[0] 437 | 438 | # make name headers 439 | nameheaders = make_eot_name_headers(fontdata, tableDir[OpenType.TABLE_NAME]) 440 | rootstring = make_root_string() 441 | 442 | # calculate the total eot size 443 | eotSize = struct.calcsize(EOT.EOT_HEADER_PACK) + len(nameheaders) + len(rootstring) + fontDataSize 444 | fixed = struct.pack(EOT.EOT_HEADER_PACK, 445 | *([eotSize, fontDataSize, version, flags] + panose + [charset, italic] + 446 | [weight, fsType, magicNumber] + urange + codepage + [checkSumAdjustment])) 447 | 448 | return ''.join((fixed, nameheaders, rootstring)) 449 | 450 | 451 | def write_eot_font(eot, header, data): 452 | open(eot,'wb').write(''.join((header, data))) 453 | return 454 | 455 | def main(): 456 | 457 | # deal with options 458 | p = optparse.OptionParser() 459 | p.add_option('--output', '-o') 460 | options, args = p.parse_args() 461 | 462 | # iterate over font files 463 | for f in args: 464 | data = readfont(f) 465 | if len(data) == 0: 466 | print('Error reading %s' % f) 467 | else: 468 | eot = options.output or eotname(f) 469 | header = make_eot_header(data) 470 | write_eot_font(eot, header, data) 471 | 472 | 473 | if __name__ == '__main__': 474 | main() 475 | -------------------------------------------------------------------------------- /tasks/engines/fontforge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt-webfont: fontforge engine 3 | * 4 | * @requires fontforge, ttfautohint 1.00+ (optional), eotlitetool.py 5 | * @author Artem Sapegin (http://sapegin.me) 6 | */ 7 | 8 | module.exports = function(o, allDone) { 9 | 'use strict'; 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | var temp = require('temp'); 14 | var async = require('async'); 15 | var exec = require('child_process').exec; 16 | var chalk = require('chalk'); 17 | var _ = require('lodash'); 18 | var logger = o.logger || require('winston'); 19 | var wf = require('../util/util'); 20 | 21 | // Copy source files to temporary directory 22 | var tempDir = temp.mkdirSync(); 23 | o.files.forEach(function(file) { 24 | fs.writeFileSync(path.join(tempDir, o.rename(file)), fs.readFileSync(file)); 25 | }); 26 | 27 | // Run Fontforge 28 | var args = [ 29 | 'fontforge', 30 | '-script', 31 | '"' + path.join(__dirname, 'fontforge/generate.py') + '"' 32 | ].join(' '); 33 | 34 | var proc = exec(args, {maxBuffer: o.execMaxBuffer}, function(err, out, code) { 35 | if (err instanceof Error && err.code === 127) { 36 | return fontforgeNotFound(); 37 | } 38 | else if (err) { 39 | if (err instanceof Error) { 40 | return error(err.message); 41 | } 42 | 43 | // Skip some fontforge output such as copyrights. Show warnings only when no font files was created 44 | // or in verbose mode. 45 | var success = !!wf.generatedFontFiles(o); 46 | var notError = /(Copyright|License |with many parts BSD |Executable based on sources from|Library based on sources from|Based on source from git)/; 47 | var version = /(Executable based on sources from|Library based on sources from)/; 48 | var lines = err.split('\n'); 49 | 50 | var warn = []; 51 | lines.forEach(function(line) { 52 | if (!line.match(notError) && !success) { 53 | warn.push(line); 54 | } 55 | else { 56 | logger.verbose(chalk.grey('fontforge: ') + line); 57 | } 58 | }); 59 | 60 | if (warn.length) { 61 | return error(warn.join('\n')); 62 | } 63 | } 64 | 65 | // Trim fontforge result 66 | var json = out.replace(/^[^{]+/, '').replace(/[^}]+$/, ''); 67 | 68 | // Parse json 69 | var result; 70 | try { 71 | result = JSON.parse(json); 72 | } 73 | catch (e) { 74 | logger.verbose('Webfont did not receive a proper JSON result from Python script: ' + e); 75 | return error( 76 | 'Something went wrong when running fontforge. Probably fontforge wasn’t installed correctly or one of your SVGs is too complicated for fontforge.\n\n' + 77 | '1. Try to run Grunt in verbose mode: ' + chalk.bold('grunt --verbose webfont') + ' and see what fontforge says. Then search GitHub issues for the solution: ' + chalk.underline('https://github.com/sapegin/grunt-webfont/issues') + '.\n\n' + 78 | '2. Try to use “node” engine instead of “fontforge”: ' + chalk.underline('https://github.com/sapegin/grunt-webfont#engine') + '\n\n' + 79 | '3. To find “bad” icon try to remove SVGs one by one until error disappears. Then try to simplify this SVG in Sketch, Illustrator, etc.\n\n' 80 | ); 81 | } 82 | 83 | allDone({ 84 | fontName: path.basename(result.file) 85 | }); 86 | }); 87 | 88 | // Send JSON with params 89 | if (!proc) return; 90 | proc.stdin.on('error', function(err) { 91 | if (err.code === 'EPIPE') { 92 | fontforgeNotFound(); 93 | } 94 | }); 95 | 96 | proc.stderr.on('data', function (data) { 97 | logger.verbose(data); 98 | }); 99 | proc.stdout.on('data', function (data) { 100 | logger.verbose(data); 101 | }); 102 | proc.on('exit', function (code, signal) { 103 | if (code !== 0) { 104 | logger.log( // cannot use error() because it will stop execution of callback of exec (which shows error message) 105 | "fontforge process has unexpectedly closed.\n" + 106 | "1. Try to run grunt in verbose mode to see fontforge output: " + chalk.bold('grunt --verbose webfont') + ".\n" + 107 | "2. If stderr maxBuffer exceeded try to increase " + chalk.bold('execMaxBuffer') + ", see " + 108 | chalk.underline('https://github.com/sapegin/grunt-webfont#execMaxBuffer') + ". " 109 | ); 110 | } 111 | return true; 112 | }); 113 | 114 | var params = _.extend(o, { 115 | inputDir: tempDir 116 | }); 117 | proc.stdin.write(JSON.stringify(params)); 118 | proc.stdin.end(); 119 | 120 | function error() { 121 | logger.error.apply(null, arguments); 122 | allDone(false); 123 | return false; 124 | } 125 | 126 | function fontforgeNotFound() { 127 | error('fontforge not found. Please install fontforge and all other requirements: ' + chalk.underline('https://github.com/sapegin/grunt-webfont#installation')); 128 | } 129 | 130 | }; 131 | -------------------------------------------------------------------------------- /tasks/engines/fontforge/generate.py: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/FontCustom/fontcustom/blob/master/lib/fontcustom/scripts/generate.py 2 | 3 | import fontforge 4 | import os 5 | import sys 6 | import json 7 | import re 8 | from subprocess import call 9 | from distutils.spawn import find_executable 10 | 11 | args = json.load(sys.stdin) 12 | 13 | f = fontforge.font() 14 | f.encoding = 'UnicodeFull' 15 | f.copyright = '' 16 | f.design_size = 16 17 | f.em = args['fontHeight'] 18 | f.descent = args['descent'] 19 | f.ascent = args['fontHeight'] - args['descent'] 20 | if args['version']: 21 | f.version = args['version'] 22 | if args['normalize']: 23 | f.autoWidth(0, 0, args['fontHeight']) 24 | 25 | KERNING = 15 26 | 27 | 28 | def create_empty_char(f, c): 29 | pen = f.createChar(ord(c), c).glyphPen() 30 | pen.moveTo((0, 0)) 31 | pen = None 32 | 33 | 34 | if args['addLigatures']: 35 | f.addLookup('liga', 'gsub_ligature', (), (('liga', (('latn', ('dflt')), )), )) 36 | f.addLookupSubtable('liga', 'liga') 37 | 38 | for dirname, dirnames, filenames in os.walk(args['inputDir']): 39 | for filename in sorted(filenames): 40 | name, ext = os.path.splitext(filename) 41 | filePath = os.path.join(dirname, filename) 42 | size = os.path.getsize(filePath) 43 | 44 | if ext in ['.svg']: 45 | # HACK: Remove tags 46 | svgfile = open(filePath, 'r+') 47 | svgtext = svgfile.read() 48 | svgfile.seek(0) 49 | 50 | # Replace the tags with nothing 51 | svgtext = svgtext.replace('', '') 52 | svgtext = svgtext.replace('', '') 53 | 54 | if args['normalize']: 55 | # Replace the width and the height 56 | svgtext = re.sub(r'(]*)width="[^"]*"([^>]*>)', r'\1\2', svgtext) 57 | svgtext = re.sub(r'(]*)height="[^"]*"([^>]*>)', r'\1\2', svgtext) 58 | 59 | # Remove all contents of file so that we can write out the new contents 60 | svgfile.truncate() 61 | svgfile.write(svgtext) 62 | svgfile.close() 63 | 64 | cp = args['codepoints'][name] 65 | 66 | if args['addLigatures']: 67 | name = str(name) # Convert Unicode to a regular string because addPosSub doesn't work with Unicode 68 | for char in name: 69 | create_empty_char(f, char) 70 | glyph = f.createChar(cp, name) 71 | glyph.addPosSub('liga', tuple(name)) 72 | else: 73 | glyph = f.createChar(cp, str(name)) 74 | glyph.importOutlines(filePath) 75 | 76 | if args['normalize']: 77 | glyph.left_side_bearing = glyph.right_side_bearing = 0 78 | else: 79 | glyph.width = args['fontHeight'] 80 | 81 | if args['round']: 82 | glyph.round(int(args['round'])) 83 | 84 | fontfile = args['dest'] + os.path.sep + args['fontFilename'] 85 | 86 | f.fontname = args['fontFilename'] 87 | f.familyname = args['fontFamilyName'] 88 | f.fullname = args['fontFamilyName'] 89 | 90 | if args['addLigatures']: 91 | def generate(filename): 92 | f.generate(filename, flags=('opentype')) 93 | else: 94 | def generate(filename): 95 | f.generate(filename) 96 | 97 | 98 | # TTF 99 | generate(fontfile + '.ttf') 100 | 101 | # Hint the TTF file 102 | # ttfautohint is optional 103 | if (find_executable('ttfautohint') and args['autoHint']): 104 | call('ttfautohint --symbol --fallback-script=latn --no-info "%(font)s.ttf" "%(font)s-hinted.ttf" && mv "%(font)s-hinted.ttf" "%(font)s.ttf"' % {'font': fontfile}, shell=True) 105 | f = fontforge.open(fontfile + '.ttf') 106 | 107 | # SVG 108 | if 'svg' in args['types']: 109 | generate(fontfile + '.svg') 110 | 111 | # Fix SVG header for webkit (from: https://github.com/fontello/font-builder/blob/master/bin/fontconvert.py) 112 | svgfile = open(fontfile + '.svg', 'r+') 113 | svgtext = svgfile.read() 114 | svgfile.seek(0) 115 | svgfile.write(svgtext.replace('', '')) 116 | svgfile.close() 117 | 118 | scriptPath = os.path.dirname(os.path.realpath(__file__)) 119 | 120 | # WOFF 121 | if 'woff' in args['types']: 122 | generate(fontfile + '.woff') 123 | 124 | # EOT 125 | if 'eot' in args['types']: 126 | # eotlitetool.py script to generate IE7-compatible .eot fonts 127 | call('python "%(path)s/../../bin/eotlitetool.py" "%(font)s.ttf" --output "%(font)s.eot"' % {'path': scriptPath, 'font': fontfile}, shell=True) 128 | 129 | # Delete TTF if not needed 130 | if (not 'ttf' in args['types']) and (not 'woff2' in args['types']): 131 | os.remove(fontfile + '.ttf') 132 | 133 | print(json.dumps({'file': fontfile})) 134 | -------------------------------------------------------------------------------- /tasks/engines/node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt-webfont: Node.js engine 3 | * 4 | * @requires ttfautohint 1.00+ (optional) 5 | * @author Artem Sapegin (http://sapegin.me) 6 | */ 7 | 8 | module.exports = function(o, allDone) { 9 | 'use strict'; 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | var async = require('async'); 14 | var temp = require('temp'); 15 | var exec = require('child_process').exec; 16 | var _ = require('lodash'); 17 | var StringDecoder = require('string_decoder').StringDecoder; 18 | var svgicons2svgfont = require('svgicons2svgfont'); 19 | var svg2ttf = require('svg2ttf'); 20 | var ttf2woff = require('ttf2woff'); 21 | var ttf2eot = require('ttf2eot'); 22 | var SVGO = require('svgo'); 23 | var MemoryStream = require('memorystream'); 24 | var logger = o.logger || require('winston'); 25 | var wf = require('../util/util'); 26 | 27 | // @todo Ligatures 28 | 29 | var fonts = {}; 30 | 31 | var generators = { 32 | svg: function(done) { 33 | var font = ''; 34 | var decoder = new StringDecoder('utf8'); 35 | svgFilesToStreams(o.files, function(streams) { 36 | var stream = svgicons2svgfont(streams, { 37 | fontName: o.fontFamilyName, 38 | fontHeight: o.fontHeight, 39 | descent: o.descent, 40 | normalize: o.normalize, 41 | round: o.round, 42 | log: logger.verbose.bind(logger), 43 | error: logger.error.bind(logger) 44 | }); 45 | stream.on('data', function(chunk) { 46 | font += decoder.write(chunk); 47 | }); 48 | stream.on('end', function() { 49 | fonts.svg = font; 50 | done(font); 51 | }); 52 | }); 53 | }, 54 | 55 | ttf: function(done) { 56 | getFont('svg', function(svgFont) { 57 | var font = svg2ttf(svgFont, {}); 58 | font = new Buffer(font.buffer); 59 | autohintTtfFont(font, function(hintedFont) { 60 | // ttfautohint is optional 61 | if (hintedFont) { 62 | font = hintedFont; 63 | } 64 | fonts.ttf = font; 65 | done(font); 66 | }); 67 | }); 68 | }, 69 | 70 | woff: function(done) { 71 | getFont('ttf', function(ttfFont) { 72 | var font = ttf2woff(new Uint8Array(ttfFont), {}); 73 | font = new Buffer(font.buffer); 74 | fonts.woff = font; 75 | done(font); 76 | }); 77 | }, 78 | 79 | woff2: function(done) { 80 | // Will be converted from TTF later 81 | done(); 82 | }, 83 | 84 | eot: function(done) { 85 | getFont('ttf', function(ttfFont) { 86 | var font = ttf2eot(new Uint8Array(ttfFont)); 87 | font = new Buffer(font.buffer); 88 | fonts.eot = font; 89 | done(font); 90 | }); 91 | } 92 | }; 93 | 94 | var steps = []; 95 | 96 | // Font types 97 | var typesToGenerate = o.types.slice(); 98 | if (o.types.indexOf('woff2') !== -1 && o.types.indexOf('ttf' === -1)) typesToGenerate.push('ttf'); 99 | typesToGenerate.forEach(function(type) { 100 | steps.push(createFontWriter(type)); 101 | }); 102 | 103 | // Run! 104 | async.waterfall(steps, allDone); 105 | 106 | function getFont(type, done) { 107 | if (fonts[type]) { 108 | done(fonts[type]); 109 | } 110 | else { 111 | generators[type](done); 112 | } 113 | } 114 | 115 | function createFontWriter(type) { 116 | return function(done) { 117 | getFont(type, function(font) { 118 | fs.writeFileSync(wf.getFontPath(o, type), font); 119 | done(); 120 | }); 121 | }; 122 | } 123 | 124 | function svgFilesToStreams(files, done) { 125 | 126 | async.map(files, function(file, fileDone) { 127 | 128 | function fileStreamed(name, stream) { 129 | fileDone(null, { 130 | codepoint: o.codepoints[name], 131 | name: name, 132 | stream: stream 133 | }); 134 | } 135 | 136 | function streamSVG(name, file) { 137 | var stream = fs.createReadStream(file); 138 | fileStreamed(name, stream); 139 | } 140 | 141 | function streamSVGO(name, file) { 142 | var svg = fs.readFileSync(file, 'utf8'); 143 | var svgo = new SVGO(); 144 | try { 145 | svgo.optimize(svg, function(res) { 146 | var stream = new MemoryStream(res.data, { 147 | writable: false 148 | }); 149 | fileStreamed(name, stream); 150 | }); 151 | } catch(err) { 152 | logger.error('Can’t simplify SVG file with SVGO.\n\n' + err); 153 | fileDone(err); 154 | } 155 | } 156 | 157 | var idx = files.indexOf(file); 158 | var name = o.glyphs[idx]; 159 | 160 | if(o.optimize === true) { 161 | streamSVGO(name, file); 162 | } else { 163 | streamSVG(name, file); 164 | } 165 | }, function(err, streams) { 166 | if (err) { 167 | logger.error('Can’t stream SVG file.\n\n' + err); 168 | allDone(false); 169 | } 170 | else { 171 | done(streams); 172 | } 173 | }); 174 | } 175 | 176 | function autohintTtfFont(font, done) { 177 | var tempDir = temp.mkdirSync(); 178 | var originalFilepath = path.join(tempDir, 'font.ttf'); 179 | var hintedFilepath = path.join(tempDir, 'hinted.ttf'); 180 | 181 | if (!o.autoHint){ 182 | done(false); 183 | return; 184 | } 185 | // Save original font to temporary directory 186 | fs.writeFileSync(originalFilepath, font); 187 | 188 | // Run ttfautohint 189 | var args = [ 190 | 'ttfautohint', 191 | '--symbol', 192 | '--fallback-script=latn', 193 | '--windows-compatibility', 194 | '--no-info', 195 | originalFilepath, 196 | hintedFilepath 197 | ].join(' '); 198 | 199 | exec(args, {maxBuffer: o.execMaxBuffer}, function(err, out, code) { 200 | if (err) { 201 | if (err.code === 127) { 202 | logger.verbose('Hinting skipped, ttfautohint not found.'); 203 | done(false); 204 | return; 205 | } 206 | logger.error('Can’t run ttfautohint.\n\n' + err.message); 207 | done(false); 208 | return; 209 | } 210 | 211 | // Read hinted font back 212 | var hintedFont = fs.readFileSync(hintedFilepath); 213 | done(hintedFont); 214 | }); 215 | } 216 | 217 | }; 218 | -------------------------------------------------------------------------------- /tasks/templates/bem.css: -------------------------------------------------------------------------------- 1 | /* Generated by grunt-webfont */ 2 | 3 | <% if (fontfaceStyles) { %> 4 | <% if (fontPathVariables && stylesheet !== 'css') { %> 5 | <%= fontPathVariable %> 6 | <% } %> 7 | <% if (fontSrc1 && embed.length) { %> 8 | @font-face { 9 | font-family:"<%= fontFamilyName %>"; 10 | src:<%= fontSrc1 %>; 11 | font-weight:normal; 12 | font-style:normal; 13 | } 14 | <% } %>@font-face { 15 | font-family:"<%= fontFamilyName %>";<% if (fontSrc1) { %> 16 | src:<%= fontSrc1 %>;<% }%> 17 | src:<%= fontSrc2 %>; 18 | font-weight:normal; 19 | font-style:normal; 20 | } 21 | <% } %> 22 | <% if (baseStyles) { %>.<%= baseClass %><% if (addLigatures) { %>, 23 | .ligature-icons<% } %> { 24 | <% if (stylesheet === 'less') { %>&:before {<% } %> 25 | font-family:"<%= fontFamilyName %>"; 26 | <% if (stylesheet === 'less') { %>}<% } %> 27 | display:inline-block; 28 | line-height:1; 29 | font-weight:normal; 30 | font-style:normal; 31 | speak:none; 32 | text-decoration:inherit; 33 | text-transform:none; 34 | text-rendering:auto; 35 | -webkit-font-smoothing:antialiased; 36 | -moz-osx-font-smoothing:grayscale; 37 | } 38 | <% } %> 39 | 40 | <% if (iconsStyles) { %>/* Icons */<% for (var glyphIdx = 0; glyphIdx < glyphs.length; glyphIdx++) { %> 41 | <% if (stylesheet === 'less') { %> 42 | .<%= classPrefix %><%= glyphs[glyphIdx] %> { 43 | &:before { 44 | content:"<% if (addLigatures) { %><%= glyphs[glyphIdx] %><% } else { %>\<%= codepoints[glyphIdx] %><% } %>"; 45 | }<% if (ie7) {%> 46 | *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#x<%= codepoints[glyphIdx] %>;'); 47 | <% } %> 48 | } 49 | <% } else { %> 50 | <% if (ie7) {%>.<%= classPrefix %><%= glyphs[glyphIdx] %> { 51 | *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#x<%= codepoints[glyphIdx] %>;'); 52 | } 53 | <% } %> 54 | .<%= classPrefix %><%= glyphs[glyphIdx] %>:before { 55 | content:"<% if (addLigatures) { %><%= glyphs[glyphIdx] %><% } else { %>\<%= codepoints[glyphIdx] %><% } %>"; 56 | }<% } } } %> 57 | -------------------------------------------------------------------------------- /tasks/templates/bem.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseClass": "icon", 3 | "classPrefix": "icon_" 4 | } 5 | -------------------------------------------------------------------------------- /tasks/templates/bootstrap.css: -------------------------------------------------------------------------------- 1 | /* Generated by grunt-webfont */ 2 | /* Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css */ 3 | 4 | <% if (fontfaceStyles) { %> 5 | <% if (fontPathVariables && stylesheet !== 'css') { %> 6 | <%= fontPathVariable %> 7 | <% } %> 8 | <% if (fontSrc1 && embed.length) { %> 9 | @font-face { 10 | font-family:"<%= fontFamilyName %>"; 11 | src:<%= fontSrc1 %>; 12 | font-weight:normal; 13 | font-style:normal; 14 | } 15 | <% } %>@font-face { 16 | font-family:"<%= fontFamilyName %>";<% if (fontSrc1) { %> 17 | src:<%= fontSrc1 %>;<% }%> 18 | src:<%= fontSrc2 %>; 19 | font-weight:normal; 20 | font-style:normal; 21 | } 22 | <% } %> 23 | <% if (baseStyles) { %> 24 | /* Bootstrap Overrides */ 25 | [class^="<%= classPrefix %>"]:before, 26 | [class*=" <%= classPrefix %>"]:before<% if (ie7) {%>, 27 | [class^="<%= classPrefix %>"], 28 | [class*=" <%= classPrefix %>"]<% } %><% if (addLigatures) { %>, 29 | .ligature-icons<% } %> { 30 | font-family:"<%= fontFamilyName %>"; 31 | display:inline-block; 32 | line-height:1; 33 | font-weight:normal; 34 | font-style:normal; 35 | speak:none; 36 | text-decoration:inherit; 37 | text-transform:none; 38 | text-rendering:auto; 39 | -webkit-font-smoothing:antialiased; 40 | -moz-osx-font-smoothing:grayscale; 41 | }<% } %> 42 | <% if (iconsStyles && stylesheet === 'less') { %> 43 | /* Mixins */ 44 | <% for (var glyphIdx = 0; glyphIdx < glyphs.length; glyphIdx++) { %> 45 | .<%= classPrefix %><%= glyphs[glyphIdx] %><% if(glyphIdx === glyphs.length-1) { %> { <% } else { %>, <% } } %> 46 | &:before { 47 | font-family:"<%= fontFamilyName %>"; 48 | display:inline-block; 49 | font-weight:normal; 50 | font-style:normal; 51 | text-decoration:inherit; 52 | } 53 | }<% } %> 54 | <% if (extraStyles) { %> 55 | a [class^="<%= classPrefix %>"], 56 | a [class*=" <%= classPrefix %>"] { 57 | display:inline-block; 58 | text-decoration:inherit; 59 | } 60 | /* Makes the font 33% larger relative to the icon container */ 61 | .<%= classPrefix %>large:before { 62 | vertical-align:top; 63 | font-size:1.333em; 64 | } 65 | /* Keeps button heights with and without icons the same */ 66 | .btn [class^="<%= classPrefix %>"], 67 | .btn [class*=" <%= classPrefix %>"] { 68 | line-height:0.9em; 69 | } 70 | li [class^="<%= classPrefix %>"], 71 | li [class*=" <%= classPrefix %>"] { 72 | display:inline-block; 73 | width:1.25em; 74 | text-align:center; 75 | } 76 | /* 1.5 increased font size for <%= classPrefix %>large * 1.25 width */ 77 | li .<%= classPrefix %>large[class^="<%= classPrefix %>"], 78 | li .<%= classPrefix %>large[class*=" <%= classPrefix %>"] { 79 | width:1.875em; 80 | } 81 | li[class^="<%= classPrefix %>"], 82 | li[class*=" <%= classPrefix %>"] { 83 | margin-left:0; 84 | list-style-type:none; 85 | } 86 | li[class^="<%= classPrefix %>"]:before, 87 | li[class*=" <%= classPrefix %>"]:before { 88 | text-indent:-2em; 89 | text-align:center; 90 | } 91 | li[class^="<%= classPrefix %>"].<%= classPrefix %>large:before, 92 | li[class*=" <%= classPrefix %>"].<%= classPrefix %>large:before { 93 | text-indent:-1.333em; 94 | } 95 | <% } %> 96 | 97 | <% if (iconsStyles) { %>/* Icons */<% for (var glyphIdx = 0; glyphIdx < glyphs.length; glyphIdx++) { %> 98 | <% if (stylesheet === 'less') { %> 99 | .<%= classPrefix %><%= glyphs[glyphIdx] %> { 100 | &:before { 101 | content:"<% if (addLigatures) { %><%= glyphs[glyphIdx] %><% } else { %>\<%= codepoints[glyphIdx] %><% } %>"; 102 | } 103 | <% if (ie7) {%> 104 | *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#x<%= codepoints[glyphIdx] %>;'); 105 | <% } %> 106 | }<% } else { %> 107 | <% if (ie7) {%>.<%= classPrefix %><%= glyphs[glyphIdx] %> { 108 | *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#x<%= codepoints[glyphIdx] %>;'); 109 | } 110 | <% } %> 111 | .<%= classPrefix %><%= glyphs[glyphIdx] %>:before { 112 | content:"<% if (addLigatures) { %><%= glyphs[glyphIdx] %><% } else { %>\<%= codepoints[glyphIdx] %><% } %>"; 113 | }<% } %> 114 | <% } } %> 115 | -------------------------------------------------------------------------------- /tasks/templates/bootstrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseClass": "", 3 | "classPrefix": "icon-" 4 | } 5 | -------------------------------------------------------------------------------- /tasks/templates/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= fontFamilyName %> 6 | 60 | 61 | 62 |

<%= fontFamilyName %><% if (version) { %>version <%= version %><% } %>

63 | 64 |
65 | <% for (var glyphIdx = 0; glyphIdx < glyphs.length; glyphIdx++) { var glyph = glyphs[glyphIdx] %> 66 |
<%= classPrefix %><%= glyph %>
67 | <% } %> 68 |
69 | 70 | <% if (addLigatures) { %> 71 | 76 | <% } %> 77 | 78 |

Usage

79 |
<i class="<%= baseClass ? baseClass + ' ' : '' %><%= classPrefix %>name"></i>
80 | <% if (addLigatures) { %> 81 |
<i class="ligature-icons">name</i>
82 | <% } %> 83 | 84 | 85 | 86 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /tasks/util/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt-webfont: common stuff 3 | * 4 | * @author Artem Sapegin (http://sapegin.me) 5 | */ 6 | 7 | var path = require('path'); 8 | var glob = require('glob'); 9 | 10 | var exports = {}; 11 | 12 | /** 13 | * Unicode Private Use Area start. 14 | * http://en.wikipedia.org/wiki/Private_Use_(Unicode) 15 | * @type {Number} 16 | */ 17 | exports.UNICODE_PUA_START = 0xF101; 18 | 19 | /** 20 | * @font-face’s src values generation rules. 21 | * @type {Object} 22 | */ 23 | exports.fontsSrcsMap = { 24 | eot: [ 25 | { 26 | ext: '.eot' 27 | }, 28 | { 29 | ext: '.eot?#iefix', 30 | format: 'embedded-opentype' 31 | } 32 | ], 33 | woff: [ 34 | false, 35 | { 36 | ext: '.woff', 37 | format: 'woff', 38 | embeddable: true 39 | }, 40 | ], 41 | woff2: [ 42 | false, 43 | { 44 | ext: '.woff2', 45 | format: 'woff2', 46 | embeddable: true 47 | }, 48 | ], 49 | ttf: [ 50 | false, 51 | { 52 | ext: '.ttf', 53 | format: 'truetype', 54 | embeddable: true 55 | }, 56 | ], 57 | svg: [ 58 | false, 59 | { 60 | ext: '.svg#{fontBaseName}', 61 | format: 'svg' 62 | }, 63 | ] 64 | }; 65 | 66 | /** 67 | * CSS fileaname prefixes: _icons.scss. 68 | * @type {Object} 69 | */ 70 | exports.cssFilePrefixes = { 71 | _default: '', 72 | sass: '_', 73 | scss: '_' 74 | }; 75 | 76 | /** 77 | * @font-face’s src parts seperators. 78 | * @type {Object} 79 | */ 80 | exports.fontSrcSeparators = { 81 | _default: ',\n\t\t', 82 | styl: ', ' 83 | }; 84 | 85 | /** 86 | * List of available font formats. 87 | * @type {String} 88 | */ 89 | exports.fontFormats = 'eot,woff2,woff,ttf,svg'; 90 | 91 | /** 92 | * Returns list of all generated font files. 93 | * 94 | * @param {Object} o Options. 95 | * @return {Array} 96 | */ 97 | exports.generatedFontFiles = function(o) { 98 | var mask = '*.{' + o.types + '}'; 99 | return glob.sync(path.join(o.dest, o.fontFilename + mask)); 100 | }; 101 | 102 | /** 103 | * Returns path to font of specified format. 104 | * 105 | * @param {Object} o Options. 106 | * @param {String} type Font type (see `wf.fontFormats`). 107 | * @return {String} 108 | */ 109 | exports.getFontPath = function(o, type) { 110 | return path.join(o.dest, o.fontFilename + '.' + type); 111 | }; 112 | 113 | // Expose 114 | module.exports = exports; 115 | -------------------------------------------------------------------------------- /tasks/webfont.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SVG to webfont converter for Grunt 3 | * 4 | * @requires ttfautohint 5 | * @author Artem Sapegin (http://sapegin.me) 6 | */ 7 | 8 | module.exports = function(grunt) { 9 | 'use strict'; 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | var async = require('async'); 14 | var glob = require('glob'); 15 | var chalk = require('chalk'); 16 | var mkdirp = require('mkdirp'); 17 | var crypto = require('crypto'); 18 | var ttf2woff2 = require('ttf2woff2'); 19 | var _ = require('lodash'); 20 | var _s = require('underscore.string'); 21 | var wf = require('./util/util'); 22 | 23 | grunt.registerMultiTask('webfont', 'Compile separate SVG files to webfont', function() { 24 | 25 | /** 26 | * Winston to Grunt logger adapter. 27 | */ 28 | var logger = { 29 | warn: function() { 30 | grunt.log.warn.apply(null, arguments); 31 | }, 32 | error: function() { 33 | grunt.warn.apply(null, arguments); 34 | }, 35 | log: function() { 36 | grunt.log.writeln.apply(null, arguments); 37 | }, 38 | verbose: function() { 39 | grunt.verbose.writeln.apply(null, arguments); 40 | } 41 | }; 42 | 43 | var allDone = this.async(); 44 | var params = this.data; 45 | var options = this.options(); 46 | var md5 = crypto.createHash('md5'); 47 | 48 | /* 49 | * Check for `src` param on target config 50 | */ 51 | this.requiresConfig([this.name, this.target, 'src'].join('.')); 52 | 53 | /* 54 | * Check for `dest` param on either target config or global options object 55 | */ 56 | if (_.isUndefined(params.dest) && _.isUndefined(options.dest)) { 57 | logger.warn('Required property ' + [this.name, this.target, 'dest'].join('.') 58 | + ' or ' + [this.name, this.target, 'options.dest'].join('.') + ' missing.'); 59 | } 60 | 61 | if (options.skip) { 62 | completeTask(); 63 | return; 64 | } 65 | 66 | // Source files 67 | var files = _.filter(this.filesSrc, isSvgFile); 68 | if (!files.length) { 69 | logger.warn('Specified empty list of source SVG files.'); 70 | completeTask(); 71 | return; 72 | } 73 | 74 | // path must be a string, see https://nodejs.org/api/path.html#path_path_extname_path 75 | if (typeof options.template !== 'string') { 76 | options.template = ''; 77 | } 78 | 79 | // Options 80 | var o = { 81 | logger: logger, 82 | fontBaseName: options.font || 'icons', 83 | destCss: options.destCss || params.destCss || params.dest, 84 | destScss: options.destScss || params.destScss || params.destCss || params.dest, 85 | destSass: options.destSass || params.destSass || params.destCss || params.dest, 86 | destLess: options.destLess || params.destLess || params.destCss || params.dest, 87 | destStyl: options.destStyl || params.destStyl || params.destCss || params.dest, 88 | dest: options.dest || params.dest, 89 | relativeFontPath: options.relativeFontPath, 90 | fontPathVariables: options.fontPathVariables || false, 91 | addHashes: options.hashes !== false, 92 | addLigatures: options.ligatures === true, 93 | template: options.template, 94 | syntax: options.syntax || 'bem', 95 | templateOptions: options.templateOptions || {}, 96 | stylesheets: options.stylesheets || [options.stylesheet || path.extname(options.template).replace(/^\./, '') || 'css'], 97 | htmlDemo: options.htmlDemo !== false, 98 | htmlDemoTemplate: options.htmlDemoTemplate, 99 | htmlDemoFilename: options.htmlDemoFilename, 100 | styles: optionToArray(options.styles, 'font,icon'), 101 | types: optionToArray(options.types, 'eot,woff,ttf'), 102 | order: optionToArray(options.order, wf.fontFormats), 103 | embed: options.embed === true ? ['woff'] : optionToArray(options.embed, false), 104 | rename: options.rename || path.basename, 105 | engine: options.engine || 'fontforge', 106 | autoHint: options.autoHint !== false, 107 | codepoints: options.codepoints, 108 | codepointsFile: options.codepointsFile, 109 | startCodepoint: options.startCodepoint || wf.UNICODE_PUA_START, 110 | ie7: options.ie7 === true, 111 | normalize: options.normalize === true, 112 | optimize: options.optimize === false ? false : true, 113 | round: options.round !== undefined ? options.round : 10e12, 114 | fontHeight: options.fontHeight !== undefined ? options.fontHeight : 512, 115 | descent: options.descent !== undefined ? options.descent : 64, 116 | version: options.version !== undefined ? options.version : false, 117 | cache: options.cache || path.join(__dirname, '..', '.cache'), 118 | callback: options.callback, 119 | customOutputs: options.customOutputs, 120 | execMaxBuffer: options.execMaxBuffer || 1024 * 200 121 | }; 122 | 123 | o = _.extend(o, { 124 | fontName: o.fontBaseName, 125 | destCssPaths: { 126 | css: o.destCss, 127 | scss: o.destScss, 128 | sass: o.destSass, 129 | less: o.destLess, 130 | styl: o.destStyl 131 | }, 132 | relativeFontPath: o.relativeFontPath || path.relative(o.destCss, o.dest), 133 | destHtml: options.destHtml || o.destCss, 134 | fontfaceStyles: has(o.styles, 'font'), 135 | baseStyles: has(o.styles, 'icon'), 136 | extraStyles: has(o.styles, 'extra'), 137 | files: files, 138 | glyphs: [] 139 | }); 140 | 141 | o.hash = getHash(); 142 | o.fontFilename = template(options.fontFilename || o.fontBaseName, o); 143 | o.fontFamilyName = template(options.fontFamilyName || o.fontBaseName, o); 144 | 145 | // “Rename” files 146 | o.glyphs = o.files.map(function(file) { 147 | return o.rename(file).replace(path.extname(file), ''); 148 | }); 149 | 150 | // Check or generate codepoints 151 | // @todo Codepoint can be a Unicode code or character. 152 | var currentCodepoint = o.startCodepoint; 153 | if (!o.codepoints) o.codepoints = {}; 154 | if (o.codepointsFile) o.codepoints = readCodepointsFromFile(); 155 | o.glyphs.forEach(function(name) { 156 | if (!o.codepoints[name]) { 157 | o.codepoints[name] = getNextCodepoint(); 158 | } 159 | }); 160 | if (o.codepointsFile) saveCodepointsToFile(); 161 | 162 | // Check if we need to generate font 163 | var previousHash = readHash(this.name, this.target); 164 | logger.verbose('New hash:', o.hash, '- previous hash:', previousHash); 165 | if (o.hash === previousHash) { 166 | logger.verbose('Config and source files weren’t changed since last run, checking resulting files...'); 167 | var regenerationNeeded = false; 168 | 169 | var generatedFiles = wf.generatedFontFiles(o); 170 | if (!generatedFiles.length){ 171 | regenerationNeeded = true; 172 | } 173 | else { 174 | generatedFiles.push(getDemoFilePath()); 175 | o.stylesheets.forEach(function(stylesheet) { 176 | generatedFiles.push(getCssFilePath(stylesheet)); 177 | }); 178 | 179 | regenerationNeeded = _.some(generatedFiles, function(filename) { 180 | if (!filename) return false; 181 | if (!fs.existsSync(filename)) { 182 | logger.verbose('File', filename, ' is missed.'); 183 | return true; 184 | } 185 | return false; 186 | }); 187 | } 188 | if (!regenerationNeeded) { 189 | logger.log('Font ' + chalk.cyan(o.fontName) + ' wasn’t changed since last run.'); 190 | completeTask(); 191 | return; 192 | } 193 | } 194 | 195 | // Save new hash and run 196 | saveHash(this.name, this.target, o.hash); 197 | async.waterfall([ 198 | createOutputDirs, 199 | cleanOutputDir, 200 | generateFont, 201 | generateWoff2Font, 202 | generateStylesheets, 203 | generateDemoHtml, 204 | generateCustomOutputs, 205 | printDone 206 | ], completeTask); 207 | 208 | /** 209 | * Call callback function if it was specified in the options. 210 | */ 211 | function completeTask() { 212 | if (o && _.isFunction(o.callback)) { 213 | o.callback(o.fontName, o.types, o.glyphs, o.hash); 214 | } 215 | allDone(); 216 | } 217 | 218 | /** 219 | * Calculate hash to flush browser cache. 220 | * Hash is based on source SVG files contents, task options and grunt-webfont version. 221 | * 222 | * @return {String} 223 | */ 224 | function getHash() { 225 | // Source SVG files contents 226 | o.files.forEach(function(file) { 227 | md5.update(fs.readFileSync(file, 'utf8')); 228 | }); 229 | 230 | // Options 231 | md5.update(JSON.stringify(o)); 232 | 233 | // grunt-webfont version 234 | var packageJson = require('../package.json'); 235 | md5.update(packageJson.version); 236 | 237 | // Templates 238 | if (o.template) { 239 | md5.update(fs.readFileSync(o.template, 'utf8')); 240 | } 241 | if (o.htmlDemoTemplate) { 242 | md5.update(fs.readFileSync(o.htmlDemoTemplate, 'utf8')); 243 | } 244 | 245 | return md5.digest('hex'); 246 | } 247 | 248 | /** 249 | * Create output directory 250 | * 251 | * @param {Function} done 252 | */ 253 | function createOutputDirs(done) { 254 | o.stylesheets.forEach(function(stylesheet) { 255 | mkdirp.sync(option(o.destCssPaths, stylesheet)); 256 | }); 257 | mkdirp.sync(o.dest); 258 | done(); 259 | } 260 | 261 | /** 262 | * Clean output directory 263 | * 264 | * @param {Function} done 265 | */ 266 | function cleanOutputDir(done) { 267 | var htmlDemoFileMask = path.join(o.destCss, o.fontBaseName + '*.{css,html}'); 268 | var files = glob.sync(htmlDemoFileMask).concat(wf.generatedFontFiles(o)); 269 | async.forEach(files, function(file, next) { 270 | fs.unlink(file, next); 271 | }, done); 272 | } 273 | 274 | /** 275 | * Generate font using selected engine 276 | * 277 | * @param {Function} done 278 | */ 279 | function generateFont(done) { 280 | var engine = require('./engines/' + o.engine); 281 | engine(o, function(result) { 282 | if (result === false) { 283 | // Font was not created, exit 284 | completeTask(); 285 | return; 286 | } 287 | 288 | if (result) { 289 | o = _.extend(o, result); 290 | } 291 | 292 | done(); 293 | }); 294 | } 295 | 296 | /** 297 | * Converts TTF font to WOFF2. 298 | * 299 | * @param {Function} done 300 | */ 301 | function generateWoff2Font(done) { 302 | if (!has(o.types, 'woff2')) { 303 | done(); 304 | return; 305 | } 306 | 307 | // Read TTF font 308 | var ttfFontPath = wf.getFontPath(o, 'ttf'); 309 | var ttfFont = fs.readFileSync(ttfFontPath); 310 | 311 | // Remove TTF font if not needed 312 | if (!has(o.types, 'ttf')) { 313 | fs.unlinkSync(ttfFontPath); 314 | } 315 | 316 | // Convert to WOFF2 317 | var woffFont = ttf2woff2(ttfFont); 318 | 319 | // Save 320 | var woff2FontPath = wf.getFontPath(o, 'woff2'); 321 | fs.writeFile(woff2FontPath, woffFont, function() {done();}); 322 | } 323 | 324 | /** 325 | * Generate CSS 326 | * 327 | * @param {Function} done 328 | */ 329 | function generateStylesheets(done) { 330 | // Convert codepoints to array of strings 331 | var codepoints = []; 332 | _.each(o.glyphs, function(name) { 333 | codepoints.push(o.codepoints[name].toString(16)); 334 | }); 335 | o.codepoints = codepoints; 336 | 337 | // Prepage glyph names to use as CSS classes 338 | o.glyphs = _.map(o.glyphs, classnameize); 339 | 340 | o.stylesheets.sort(function(a, b) { 341 | return a === 'css' ? 1 : -1; 342 | }).forEach(generateStylesheet); 343 | 344 | done(); 345 | } 346 | 347 | /** 348 | * Generate CSS 349 | * 350 | * @param {String} stylesheet type: css, scss, ... 351 | */ 352 | function generateStylesheet(stylesheet) { 353 | o.relativeFontPath = normalizePath(o.relativeFontPath); 354 | 355 | // Generate font URLs to use in @font-face 356 | var fontSrcs = [[], []]; 357 | o.order.forEach(function(type) { 358 | if (!has(o.types, type)) return; 359 | wf.fontsSrcsMap[type].forEach(function(font, idx) { 360 | if (font) { 361 | fontSrcs[idx].push(generateFontSrc(type, font, stylesheet)); 362 | } 363 | }); 364 | }); 365 | 366 | // Convert urls to strings that could be used in CSS 367 | var fontSrcSeparator = option(wf.fontSrcSeparators, stylesheet); 368 | fontSrcs.forEach(function(font, idx) { 369 | // o.fontSrc1, o.fontSrc2 370 | o['fontSrc'+(idx+1)] = font.join(fontSrcSeparator); 371 | }); 372 | o.fontRawSrcs = fontSrcs; 373 | 374 | // Read JSON file corresponding to CSS template 375 | var templateJson = readTemplate(o.template, o.syntax, '.json', true); 376 | if (templateJson) o = _.extend(o, JSON.parse(templateJson.template)); 377 | 378 | // Now override values with templateOptions 379 | if (o.templateOptions) o = _.extend(o, o.templateOptions); 380 | 381 | // Generate CSS 382 | var ext = path.extname(o.template) || '.css'; // Use extension of o.template file if given, or default to .css 383 | o.cssTemplate = readTemplate(o.template, o.syntax, ext); 384 | var cssContext = _.extend(o, { 385 | iconsStyles: true, 386 | stylesheet: stylesheet 387 | }); 388 | 389 | var css = renderTemplate(o.cssTemplate, cssContext); 390 | 391 | // Fix CSS preprocessors comments: single line comments will be removed after compilation 392 | if (has(['sass', 'scss', 'less', 'styl'], stylesheet)) { 393 | css = css.replace(/\/\* *(.*?) *\*\//g, '// $1'); 394 | } 395 | 396 | // Save file 397 | fs.writeFileSync(getCssFilePath(stylesheet), css); 398 | } 399 | 400 | /** 401 | * Gets the codepoints from the set filepath in o.codepointsFile 402 | */ 403 | function readCodepointsFromFile(){ 404 | if (!o.codepointsFile) return {}; 405 | if (!fs.existsSync(o.codepointsFile)){ 406 | logger.verbose('Codepoints file not found'); 407 | return {}; 408 | } 409 | 410 | var buffer = fs.readFileSync(o.codepointsFile); 411 | return JSON.parse(buffer.toString()); 412 | } 413 | 414 | /** 415 | * Saves the codespoints to the set file 416 | */ 417 | function saveCodepointsToFile(){ 418 | if (!o.codepointsFile) return; 419 | var codepointsToString = JSON.stringify(o.codepoints, null, 4); 420 | try { 421 | fs.writeFileSync(o.codepointsFile, codepointsToString); 422 | logger.verbose('Codepoints saved to file "' + o.codepointsFile + '".'); 423 | } catch (err) { 424 | logger.error(err.message); 425 | } 426 | } 427 | 428 | /* 429 | * Prepares base context for templates 430 | */ 431 | function prepareBaseTemplateContext() { 432 | var context = _.extend({}, o); 433 | return context; 434 | } 435 | 436 | /* 437 | * Makes custom extends necessary for use with preparing the template context 438 | * object for the HTML demo. 439 | */ 440 | function prepareHtmlTemplateContext() { 441 | 442 | var context = prepareBaseTemplateContext(); 443 | 444 | var htmlStyles; 445 | 446 | // Prepare relative font paths for injection into @font-face refs in HTML 447 | var relativeRe = new RegExp(_s.escapeRegExp(o.relativeFontPath), 'g'); 448 | var htmlRelativeFontPath = normalizePath(path.relative(o.destHtml, o.dest)); 449 | var _fontSrc1 = o.fontSrc1.replace(relativeRe, htmlRelativeFontPath); 450 | var _fontSrc2 = o.fontSrc2.replace(relativeRe, htmlRelativeFontPath); 451 | 452 | _.extend(context, { 453 | fontSrc1: _fontSrc1, 454 | fontSrc2: _fontSrc2, 455 | fontfaceStyles: true, 456 | baseStyles: true, 457 | extraStyles: false, 458 | iconsStyles: true, 459 | stylesheet: 'css' 460 | }); 461 | 462 | // Prepares CSS for injection into 53 | 54 | 55 |

<%= fontBaseName %>

56 | 57 |
58 | <% for (var glyphIdx = 0; glyphIdx < glyphs.length; glyphIdx++) { var glyph = glyphs[glyphIdx] %> 59 |
<%= classPrefix %><%= glyph %>
60 | <% } %> 61 |
62 | 63 |

Usage

64 |
<i class="<%= baseClass ? baseClass + ' ' : '' %><%= classPrefix %>name"></i>
65 | 66 | 67 | 68 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /test/templates/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseClass": "icon", 3 | "classPrefix": "icon_" 4 | } 5 | -------------------------------------------------------------------------------- /test/templates/template.sass: -------------------------------------------------------------------------------- 1 | /* Custom template */ 2 | 3 | <% if (fontfaceStyles) { %>@font-face 4 | font-family:"<%= fontBaseName %>"<% if (fontSrc1) { %> 5 | src: <%= fontSrc1 %><% }%> 6 | src: <%= fontSrc2 %> 7 | font-weight:normal 8 | font-style:normal 9 | <% } %> 10 | <% if (baseStyles) { %>.icon 11 | font-family:"<%= fontBaseName %>" 12 | display:inline-block 13 | font-style:normal 14 | speak:none 15 | -webkit-font-smoothing:antialiased 16 | <% } %> 17 | <% if (iconsStyles) { %>/* Icons */<% for (var glyphIdx = 0; glyphIdx < glyphs.length; glyphIdx++) { %> 18 | .icon_<%= glyphs[glyphIdx] %>:before 19 | content:"\<%= codepoints[glyphIdx] %>"<% } %><% } %> 20 | -------------------------------------------------------------------------------- /test/templates/template.scss: -------------------------------------------------------------------------------- 1 | /* Custom template */ 2 | 3 | <% if (fontfaceStyles) { %>@font-face { 4 | font-family:"<%= fontBaseName %>";<% if (fontSrc1) { %> 5 | src: <%= fontSrc1 %>;<% }%> 6 | src: <%= fontSrc2 %>; 7 | font-weight:normal; 8 | font-style:normal; 9 | } 10 | <% } %> 11 | <% if (baseStyles) { %>.icon { 12 | font-family:"<%= fontBaseName %>"; 13 | display:inline-block; 14 | font-style:normal; 15 | speak:none; 16 | -webkit-font-smoothing:antialiased; 17 | } 18 | <% } %> 19 | <% if (iconsStyles) { %>/* Icons */<% for (var glyphIdx = 0; glyphIdx < glyphs.length; glyphIdx++) { %> 20 | .icon_<%= glyphs[glyphIdx] %>:before { content:"\<%= codepoints[glyphIdx] %>"; }<% } %><% } %> 21 | -------------------------------------------------------------------------------- /test/webfont_test.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var grunt = require('grunt'); 7 | var parseXMLString = require('xml2js').parseString; 8 | var wf = require('../tasks/util/util'); 9 | 10 | function find(haystack, needle) { 11 | return haystack.indexOf(needle) !== -1; 12 | } 13 | 14 | function findDuplicates(haystack, needles) { 15 | var sorted_arr = haystack.sort(); 16 | 17 | var results = []; 18 | for (var i = 0; i < haystack.length - 1; i++) { 19 | if (sorted_arr[i + 1] === sorted_arr[i]) { 20 | results.push(sorted_arr[i]); 21 | } 22 | } 23 | 24 | return results; 25 | } 26 | 27 | exports.webfont = { 28 | test1: function(test) { 29 | // All out files should be created and should not be empty 30 | 'woff,ttf,eot'.split(',').forEach(function(type) { 31 | var name = type.toUpperCase(); 32 | test.ok(fs.existsSync('test/tmp/test1/icons.' + type), name + ' file created.'); 33 | test.ok(grunt.file.read('test/tmp/test1/icons.' + type).length, name + ' file not empty.'); 34 | }); 35 | 36 | 'css,html'.split(',').forEach(function(type) { 37 | var name = type.toUpperCase(); 38 | test.ok(fs.existsSync('test/tmp/test1/icons.' + type), name + ' file created.'); 39 | test.ok(grunt.file.read('test/tmp/test1/icons.' + type).length, name + ' file not empty.'); 40 | }); 41 | 42 | var svgs = grunt.file.expand('test/src/**.*'); 43 | var css = grunt.file.read('test/tmp/test1/icons.css'); 44 | var html = grunt.file.read('test/tmp/test1/icons.html'); 45 | 46 | // CSS links to font files are correct 47 | 'woff,ttf,eot'.split(',').forEach(function(type) { 48 | test.ok( 49 | find(css, 'url("icons.' + type), 50 | 'File path ' + type + ' should be in CSS file.' 51 | ); 52 | }); 53 | 54 | // Double EOT (for IE9 compat mode) 55 | test.ok( 56 | find(css, 'src:url("icons.eot");'), 57 | 'First EOT declaration.' 58 | ); 59 | test.ok( 60 | find(css, 'src:url("icons.eot?#iefix") format("embedded-opentype"),'), 61 | 'Second EOT declaration.' 62 | ); 63 | 64 | // Every SVG file should have corresponding entry in CSS and HTML files 65 | svgs.forEach(function(file, index) { 66 | var id = path.basename(file, '.svg'); 67 | test.ok( 68 | find(css, '.icon_' + id + ':before'), 69 | 'Icon ' + id + ' should be in CSS file.' 70 | ); 71 | test.ok( 72 | find(css, 'content:"\\' + (wf.UNICODE_PUA_START + index).toString(16) + '"'), 73 | 'Character at index ' + index + ' has its codepoint in the CSS' 74 | ); 75 | test.ok( 76 | find(html, '
icon_' + id + '
'), 77 | 'Icon ' + id + ' should be in HTML file.' 78 | ); 79 | }); 80 | 81 | test.done(); 82 | }, 83 | 84 | test2: function(test) { 85 | var css = grunt.file.read('test/tmp/test2/myfont.css'); 86 | 87 | // Read hash 88 | var hash = css.match(/url\("fonts\/myfont\.woff\?([0-9a-f]{32})"\)/); 89 | hash = hash && hash[1]; 90 | test.ok(hash, 'Hash calculated.'); 91 | 92 | // All out files should be created and should not be empty 93 | 'woff,svg'.split(',').forEach(function(type) { 94 | var name = type.toUpperCase(), 95 | prefix = 'test/tmp/test2/fonts/myfont.'; 96 | test.ok(fs.existsSync(prefix + type), name + ' file created.'); 97 | test.ok(grunt.file.read(prefix + type).length, name + ' file not empty.'); 98 | }); 99 | 'css,html'.split(',').forEach(function(type) { 100 | var name = type.toUpperCase(); 101 | test.ok(fs.existsSync('test/tmp/test2/myfont.' + type), name + ' file created.'); 102 | test.ok(grunt.file.read('test/tmp/test2/myfont.' + type).length, name + ' file not empty.'); 103 | }); 104 | 105 | // Excluded file types should not be created 106 | 'eot,ttf'.split(',').forEach(function(type) { 107 | var name = type.toUpperCase(), 108 | prefix = 'test/tmp/test2/fonts/myfont.'; 109 | test.ok(!fs.existsSync(prefix + type), name + ' file NOT created.'); 110 | }); 111 | 112 | var svgs = grunt.file.expand('test/src/**.*'); 113 | var html = grunt.file.read('test/tmp/test2/myfont.html'); 114 | 115 | // CSS links to font files are correct 116 | 'woff,svg'.split(',').forEach(function(type) { 117 | test.ok( 118 | find(css, 'url("fonts/myfont.' + type + '?' + hash), 119 | 'File path ' + type + ' should be in CSS file.' 120 | ); 121 | }); 122 | 123 | // CSS links to excluded formats should not be included 124 | 'ttf,eot'.split(',').forEach(function(type) { 125 | test.ok( 126 | !find(css, 'fonts/myfont.' + type), 127 | 'File path ' + type + ' should be in CSS file.' 128 | ); 129 | }); 130 | 131 | 132 | // Every SVG file should have corresponding entry in CSS and HTML files 133 | svgs.forEach(function(file) { 134 | var id = path.basename(file, '.svg'); 135 | test.ok( 136 | find(css, '.icon-' + id + ':before'), 137 | 'Icon ' + id + ' should be in CSS file.' 138 | ); 139 | test.ok( 140 | find(html, '
icon-' + id + '
'), 141 | 'Icon ' + id + ' should be in HTML file.' 142 | ); 143 | }); 144 | 145 | test.done(); 146 | }, 147 | 148 | embed: function(test) { 149 | // All out files should be created and should not be empty 150 | 'ttf,eot'.split(',').forEach(function(type) { 151 | var name = type.toUpperCase(), 152 | prefix = 'test/tmp/embed/icons.'; 153 | test.ok(fs.existsSync(prefix + type), name + ' file created.'); 154 | test.ok(grunt.file.read(prefix + type).length, name + ' file not empty.'); 155 | }); 156 | 157 | // WOFF should be deleted 158 | 'woff'.split(',').forEach(function(type) { 159 | var name = type.toUpperCase(), 160 | prefix = 'test/tmp/embed/icons.'; 161 | test.ok(!fs.existsSync(prefix + type), name + ' file NOT created.'); 162 | }); 163 | 164 | var css = grunt.file.read('test/tmp/embed/icons.css'); 165 | 166 | // Data:uri 167 | var m = css.match(/data:application\/x-font-woff;charset=utf-8;base64,.*?format\("woff"\)/g); 168 | test.equal(m && m.length, 1, 'WOFF (default) data:uri'); 169 | 170 | test.done(); 171 | }, 172 | 173 | embed_woff: function(test) { 174 | // Excluded file types should not be created + WOFF should be deleted 175 | 'woff,ttf,eot'.split(',').forEach(function(type) { 176 | var name = type.toUpperCase(), 177 | prefix = 'test/tmp/embed_woff/icons.'; 178 | test.ok(!fs.existsSync(prefix + type), name + ' file NOT created.'); 179 | }); 180 | 181 | var css = grunt.file.read('test/tmp/embed_woff/icons.css'); 182 | var m; 183 | 184 | // Data:uri 185 | m = css.match(/data:application\/x-font-woff;charset=utf-8;base64,.*?format\("woff"\)/g); 186 | test.equal(m && m.length, 1, 'Data:uri'); 187 | 188 | test.done(); 189 | }, 190 | 191 | embed_ttf: function(test) { 192 | // Excluded file types should not be created + TTF should be deleted 193 | 'woff,ttf,eot'.split(',').forEach(function(type) { 194 | var name = type.toUpperCase(), 195 | prefix = 'test/tmp/embed_ttf/icons.'; 196 | test.ok(!fs.existsSync(prefix + type), name + ' file NOT created.'); 197 | }); 198 | 199 | var css = grunt.file.read('test/tmp/embed_ttf/icons.css'); 200 | var m; 201 | 202 | // Data:uri 203 | m = css.match(/data:application\/x-font-ttf;charset=utf-8;base64,.*?format\("truetype"\)/g); 204 | test.equal(m && m.length, 1, 'TrueType data:uri'); 205 | 206 | test.done(); 207 | }, 208 | 209 | embed_ttf_woff: function(test) { 210 | // Excluded file types should not be created + TTF should be deleted 211 | 'woff,ttf,eot'.split(',').forEach(function(type) { 212 | var name = type.toUpperCase(), 213 | prefix = 'test/tmp/embed_ttf_woff/icons.'; 214 | test.ok(!fs.existsSync(prefix + type), name + ' file NOT created.'); 215 | }); 216 | 217 | var css = grunt.file.read('test/tmp/embed_ttf_woff/icons.css'); 218 | var m; 219 | 220 | // Data:uri 221 | m = css.match(/data:application\/x-font-ttf;charset=utf-8;base64,.*?format\("truetype"\)/g); 222 | test.equal(m && m.length, 1, 'TrueType data:uri'); 223 | m = css.match(/data:application\/x-font-woff;charset=utf-8;base64,.*?format\("woff"\)/g); 224 | test.equal(m && m.length, 1, 'WOFF data:uri'); 225 | 226 | test.done(); 227 | }, 228 | 229 | one: function(test) { 230 | // All out files should be created and should not be empty 231 | 'woff,ttf,eot'.split(',').forEach(function(type) { 232 | var name = type.toUpperCase(); 233 | test.ok(fs.existsSync('test/tmp/one/icons.' + type), name + ' file created.'); 234 | test.ok(grunt.file.read('test/tmp/one/icons.' + type).length, name + ' file not empty.'); 235 | }); 236 | 237 | 'css,html'.split(',').forEach(function(type) { 238 | var name = type.toUpperCase(); 239 | test.ok(fs.existsSync('test/tmp/one/icons.' + type), name + ' file created.'); 240 | test.ok(grunt.file.read('test/tmp/one/icons.' + type).length, name + ' file not empty.'); 241 | }); 242 | 243 | var svgs = grunt.file.expand('test/src_one/**.*'), 244 | css = grunt.file.read('test/tmp/one/icons.css'); 245 | 246 | // CSS links to font files are correct 247 | 'woff,ttf,eot'.split(',').forEach(function(type) { 248 | test.ok( 249 | find(css, 'icons.' + type), 250 | 'File path ' + type + ' should be in CSS file.' 251 | ); 252 | }); 253 | 254 | // Every SVG file should have corresponding entry in CSS file 255 | svgs.forEach(function(file) { 256 | var id = path.basename(file, '.svg'); 257 | test.ok( 258 | find(css, '.icon_' + id + ':before'), 259 | 'Icon ' + id + ' should be in CSS file.' 260 | ); 261 | }); 262 | 263 | test.done(); 264 | }, 265 | 266 | template: function(test) { 267 | var css = grunt.file.read('test/tmp/template/icons.css'); 268 | 269 | // There should be comment from custom template 270 | test.ok( 271 | find(css, 'Custom template'), 272 | 'Comment from custom template.' 273 | ); 274 | 275 | test.done(); 276 | }, 277 | 278 | template_scss: function(test) { 279 | var cssFilename = 'test/tmp/template_scss/_icons.scss'; 280 | 281 | test.ok(fs.existsSync(cssFilename), 'SCSS template: .scss file created.'); 282 | 283 | var css = grunt.file.read(cssFilename); 284 | 285 | // There should be comment from custom template 286 | test.ok( 287 | find(css, 'Custom template'), 288 | 'SCSS template: comment from custom template.' 289 | ); 290 | 291 | test.done(); 292 | }, 293 | 294 | template_sass: function(test) { 295 | var cssFilename = 'test/tmp/template_sass/_icons.sass'; 296 | 297 | test.ok(fs.existsSync(cssFilename), 'SASS template: .sass file created (stylesheet extension derived from template name).'); 298 | 299 | var css = grunt.file.read(cssFilename); 300 | 301 | // There should be comment from custom template 302 | test.ok( 303 | find(css, 'Custom template'), 304 | 'SASS template: comment from custom template.' 305 | ); 306 | 307 | test.done(); 308 | }, 309 | 310 | enabled_template_variables: function(test) { 311 | var scssFilename = 'test/tmp/enabled_template_variables/_icons.scss'; 312 | var lessFilename = 'test/tmp/enabled_template_variables/icons.less'; 313 | var htmlFilename = 'test/tmp/enabled_template_variables/icons.html'; 314 | 315 | var scss = grunt.file.read(scssFilename); 316 | var less = grunt.file.read(lessFilename); 317 | var html = grunt.file.read(htmlFilename); 318 | 319 | // There should be a variable declaration for scss preprocessor 320 | test.ok( 321 | find(scss, '$icons-font-path : "../iamrelative/" !default;'), 322 | 'SCSS enable template variables: variable exists.' 323 | ); 324 | 325 | // There should be a variable declaration for scss preprocessor 326 | test.ok( 327 | find(scss, '$icons-font-path : "../iamrelative/" !default;'), 328 | 'SCSS enable template variables: variable exists.' 329 | ); 330 | 331 | // There should be a variable declaration for less preprocessor 332 | test.ok( 333 | find(less, '@icons-font-path : "../iamrelative/";'), 334 | 'LESS enable template variables: variable exists.' 335 | ); 336 | 337 | // The variable should be used in the less file 338 | test.ok( 339 | find(less, 'url("@{icons-font-path}icons'), 340 | 'LESS enable template variables: variable used.' 341 | ); 342 | 343 | // The LESS variable should not be included in the html demo source 344 | test.ok( 345 | !find(html, 'url("@{icons-font-path}icons'), 346 | 'Path variables were found in the HTML demo.' 347 | ); 348 | 349 | test.done(); 350 | }, 351 | 352 | html_template: function(test) { 353 | var demo = grunt.file.read('test/tmp/html_template/icons.html'); 354 | 355 | // There should be comment from custom template 356 | test.ok( 357 | find(demo, 'Custom template'), 358 | 'Comment from custom template.' 359 | ); 360 | 361 | test.done(); 362 | }, 363 | 364 | html_filename: function(test) { 365 | var htmlfile = 'test/tmp/html_filename/index.html'; 366 | 367 | // There should be comment from custom template 368 | test.ok(fs.existsSync(htmlfile), 'HTML demo file custom name created.'); 369 | 370 | test.done(); 371 | }, 372 | 373 | relative_path: function(test) { 374 | var css = grunt.file.read('test/tmp/relative_path/icons.css'); 375 | 376 | // CSS links to font files are correct 377 | 'woff,ttf,eot'.split(',').forEach(function(type) { 378 | test.ok( 379 | find(css, 'url("../iamrelative/icons.' + type), 380 | 'File path ' + type + ' should be in CSS file.' 381 | ); 382 | }); 383 | 384 | test.done(); 385 | }, 386 | 387 | sass: function(test) { 388 | test.ok(fs.existsSync('test/tmp/sass/_icons.sass'), 'SASS file with underscore created.'); 389 | test.ok(!fs.existsSync('test/tmp/sass/icons.sass'), 'SASS file without underscore not created.'); 390 | test.ok(!fs.existsSync('test/tmp/sass/icons.css'), 'CSS file not created.'); 391 | 392 | var svgs = grunt.file.expand('test/src/**.*'); 393 | var sass = grunt.file.read('test/tmp/sass/_icons.sass'); 394 | 395 | // There should be comment from custom template 396 | var m = sass.match(/\/\* *(.*?) *\*\//g); 397 | test.ok(!m, 'No regular CSS comments.'); 398 | 399 | // There should be comment from custom template 400 | m = sass.match(/^\/\//gm); 401 | test.equal(m && m.length, 2, 'Single line comments.'); 402 | 403 | test.done(); 404 | }, 405 | 406 | less: function(test) { 407 | test.ok(fs.existsSync('test/tmp/less/icons.less'), 'LESS file created.'); 408 | test.ok(!fs.existsSync('test/tmp/less/icons.css'), 'CSS file not created.'); 409 | 410 | var svgs = grunt.file.expand('test/src/**.*'); 411 | var less = grunt.file.read('test/tmp/less/icons.less'); 412 | 413 | // There should be comment from custom template 414 | var m = less.match(/\/\* *(.*?) *\*\//g); 415 | test.ok(!m, 'No regular CSS comments.'); 416 | 417 | // There should be comment from custom template 418 | m = less.match(/^\/\//gm); 419 | test.equal(m && m.length, 2, 'Single line comments.'); 420 | 421 | // Every SVG file should have two corresponding entries in CSS file 422 | svgs.forEach(function(file) { 423 | var id = path.basename(file, '.svg'); 424 | test.ok( 425 | find(less, '.icon_' + id + ' {\n\t&:before'), 426 | 'LESS Mixin ' + id + ' should be in CSS file.' 427 | ); 428 | }); 429 | 430 | test.done(); 431 | }, 432 | 433 | css_plus_scss: function(test) { 434 | test.ok(fs.existsSync('test/tmp/scss/_icons.scss'), 'SCSS file with underscore created.'); 435 | test.ok(!fs.existsSync('test/tmp/scss/icons.scss'), 'SCSS file without underscore not created.'); 436 | test.ok(fs.existsSync('test/tmp/css/icons.css'), 'CSS file is created.'); 437 | 438 | test.done(); 439 | }, 440 | 441 | stylus_bem: function(test) { 442 | test.ok(fs.existsSync('test/tmp/stylus_bem/icons.styl'), 'Stylus file created.'); 443 | test.ok(!fs.existsSync('test/tmp/stylus_bem/icons.css'), 'CSS file not created.'); 444 | 445 | var styl = grunt.file.read('test/tmp/stylus_bem/icons.styl'); 446 | 447 | // There should be comment from custom template 448 | var m = styl.match(/\/\* *(.*?) *\*\//g); 449 | test.ok(!m, 'No regular CSS comments.'); 450 | 451 | // There should be comment from custom template 452 | m = styl.match(/^\/\//gm); 453 | test.equal(m && m.length, 2, 'Single line comments.'); 454 | 455 | var stylus = require('stylus'); 456 | var s = stylus(styl); 457 | 458 | s.render(function(err, css) { 459 | if (err) { 460 | console.log('Stylus compile error:'); 461 | console.log(err); 462 | } 463 | test.ok(!err, 'Stylus file compiled.'); 464 | test.done(); 465 | }); 466 | }, 467 | 468 | stylus_bootstrap: function(test) { 469 | test.ok(fs.existsSync('test/tmp/stylus_bootstrap/icons.styl'), 'Stylus file created.'); 470 | test.ok(!fs.existsSync('test/tmp/stylus_bootstrap/icons.css'), 'CSS file not created.'); 471 | 472 | var styl = grunt.file.read('test/tmp/stylus_bootstrap/icons.styl'); 473 | 474 | var stylus = require('stylus'); 475 | var s = stylus(styl); 476 | 477 | s.render(function(err, css) { 478 | if (err) { 479 | console.log('Stylus compile error:'); 480 | console.log(err); 481 | } 482 | test.ok(!err, 'Stylus file compiled.'); 483 | test.done(); 484 | }); 485 | }, 486 | 487 | spaces: function(test) { 488 | var css = grunt.file.read('test/tmp/spaces/icons.css'); 489 | 490 | test.ok( 491 | find(css, '.icon_ma-il-ru:before'), 492 | 'Spaces in class name should be replaced by hyphens.' 493 | ); 494 | test.ok( 495 | find(css, 'content:"\\' + wf.UNICODE_PUA_START.toString(16) + '";'), 496 | 'Right codepoint should exists.' 497 | ); 498 | 499 | test.done(); 500 | }, 501 | 502 | disable_demo: function(test) { 503 | test.ok(fs.existsSync('test/tmp/disable_demo/icons.css'), 'CSS file created.'); 504 | test.ok(!fs.existsSync('test/tmp/disable_demo/icons.html'), 'HTML file not created.'); 505 | 506 | test.done(); 507 | }, 508 | 509 | non_css_demo: function(test) { 510 | test.ok(!fs.existsSync('test/tmp/non_css_demo/icons.css'), 'CSS file not created.'); 511 | test.ok(fs.existsSync('test/tmp/non_css_demo/icons.html'), 'HTML file created.'); 512 | 513 | var html = grunt.file.read('test/tmp/non_css_demo/icons.html'); 514 | 515 | test.ok( 516 | find(html, '@font-face {'), 517 | 'Font-face declaration exists in HTML.' 518 | ); 519 | 520 | test.ok( 521 | find(html, '.icon {'), 522 | 'Base icon exists in HTML.' 523 | ); 524 | 525 | test.ok( 526 | !find(html, 'url("../iamrelative/icons-'), 527 | 'Relative paths should not be in HTML.' 528 | ); 529 | 530 | test.ok( 531 | !find(html, '&:before'), 532 | 'LESS mixins should not be in HTML.' 533 | ); 534 | 535 | // Every SVG file should have corresponding entry in