├── .gitignore ├── index.js ├── test ├── sample-config │ ├── locale-to-url.json │ └── fonts.js ├── sample-font-packs │ ├── fonts-with-default │ │ ├── fonts │ │ │ ├── en │ │ │ │ ├── opensans-regular.eot │ │ │ │ ├── opensans-regular.otf │ │ │ │ ├── opensans-regular.ttf │ │ │ │ ├── opensans-regular.woff │ │ │ │ ├── opensans-regular.woff2 │ │ │ │ └── opensans-regular.svg │ │ │ ├── latin │ │ │ │ ├── opensans-regular.eot │ │ │ │ ├── opensans-regular.otf │ │ │ │ ├── opensans-regular.ttf │ │ │ │ ├── opensans-regular.woff │ │ │ │ ├── opensans-regular.woff2 │ │ │ │ └── opensans-regular.svg │ │ │ └── default │ │ │ │ ├── opensans-regular.eot │ │ │ │ ├── opensans-regular.otf │ │ │ │ ├── opensans-regular.ttf │ │ │ │ ├── opensans-regular.woff │ │ │ │ ├── opensans-regular.woff2 │ │ │ │ └── opensans-regular.svg │ │ └── index.js │ ├── fonts-without-default │ │ ├── fonts │ │ │ └── en │ │ │ │ ├── opensans-regular.eot │ │ │ │ ├── opensans-regular.ttf │ │ │ │ ├── opensans-regular.woff │ │ │ │ └── opensans-regular.svg │ │ └── index.js │ ├── fonts-missing-font │ │ ├── fonts │ │ │ └── default │ │ │ │ ├── opensans-regular.eot │ │ │ │ ├── opensans-regular.woff │ │ │ │ └── opensans-regular.svg │ │ └── index.js │ ├── shadows-into-light │ │ ├── fonts │ │ │ ├── default │ │ │ │ ├── shadows-into-light.eot │ │ │ │ ├── shadows-into-light.ttf │ │ │ │ └── shadows-into-light.woff │ │ │ └── Shadows Into Light- TOU.txt │ │ ├── package.json │ │ ├── index.js │ │ ├── LICENSE │ │ └── README.md │ └── fonts-with-default-in-locale-to-subdirs │ │ ├── fonts │ │ └── en │ │ │ ├── opensans-regular.eot │ │ │ ├── opensans-regular.ttf │ │ │ └── opensans-regular.woff │ │ └── index.js ├── mocks │ ├── req-mock.js │ └── res-mock.js ├── font-pack-configurator-test.js ├── font-responder-test.js ├── css-responder-test.js └── middleware-test.js ├── LICENSE ├── .travis.yml ├── CONTRIBUTING.md ├── .jshintrc ├── CHANGLELOG.md ├── script ├── check_font_pack.js ├── process_directory.js └── convert_zip.js ├── lib ├── respond.js ├── util.js ├── font-responder.js ├── middleware.js ├── font-pack-configurator.js └── css-responder.js ├── package.json ├── npm-shrinkwrap.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/middleware'); 2 | -------------------------------------------------------------------------------- /test/sample-config/locale-to-url.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": "latin", 3 | "en-UK": "latin", 4 | "es": "latin", 5 | "it-ch": "extended" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.eot -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.otf -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.ttf -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.woff -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.woff2 -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.eot -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.otf -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.ttf -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.woff -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-without-default/fonts/en/opensans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-without-default/fonts/en/opensans-regular.eot -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-without-default/fonts/en/opensans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-without-default/fonts/en/opensans-regular.ttf -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-without-default/fonts/en/opensans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-without-default/fonts/en/opensans-regular.woff -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-missing-font/fonts/default/opensans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-missing-font/fonts/default/opensans-regular.eot -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-missing-font/fonts/default/opensans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-missing-font/fonts/default/opensans-regular.woff -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.eot -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.otf -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.ttf -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.woff -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.woff2 -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.woff2 -------------------------------------------------------------------------------- /test/sample-font-packs/shadows-into-light/fonts/default/shadows-into-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/shadows-into-light/fonts/default/shadows-into-light.eot -------------------------------------------------------------------------------- /test/sample-font-packs/shadows-into-light/fonts/default/shadows-into-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/shadows-into-light/fonts/default/shadows-into-light.ttf -------------------------------------------------------------------------------- /test/sample-font-packs/shadows-into-light/fonts/default/shadows-into-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/shadows-into-light/fonts/default/shadows-into-light.woff -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6 5 | 6 | script: 7 | - npm run nsp 8 | - npm test 9 | 10 | notifications: 11 | email: 12 | - shane@shanetomlinson.com 13 | - stomlinson@mozilla.com 14 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default-in-locale-to-subdirs/fonts/en/opensans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default-in-locale-to-subdirs/fonts/en/opensans-regular.eot -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default-in-locale-to-subdirs/fonts/en/opensans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default-in-locale-to-subdirs/fonts/en/opensans-regular.ttf -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default-in-locale-to-subdirs/fonts/en/opensans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shane-tomlinson/connect-fonts/master/test/sample-font-packs/fonts-with-default-in-locale-to-subdirs/fonts/en/opensans-regular.woff -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | MOAR font packs! See [connect-fonts-tools](https://github.com/shane-tomlinson/connect-fonts-tools) for tools to make this easy. [connect-fonts-opensans](https://github.com/shane-tomlinson/connect-fonts-opensans) is an example of a finished font pack. 4 | 5 | Any updates to connect-fonts are appreciated. All submissions will be reviewed and considered for merge. 6 | 7 | 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "passfail": false, 3 | "maxerr": 100, 4 | "node": true, 5 | "forin": false, 6 | "boss": true, 7 | "noarg": true, 8 | "undef": true, 9 | "unused": true, 10 | "browser": true, 11 | "laxbreak": true, 12 | "laxcomma": true, 13 | "eqeqeq": true, 14 | "eqnull": true, 15 | "expr": true, 16 | "indent": 2, 17 | "white": false, 18 | "predef": [ 19 | "exports", 20 | "require", 21 | "process" 22 | ], 23 | "es5": true, 24 | "esnext": true, 25 | "shadow": false, 26 | "supernew": false, 27 | "strict": false 28 | } 29 | -------------------------------------------------------------------------------- /CHANGLELOG.md: -------------------------------------------------------------------------------- 1 | # A list of important changes, newest changes first. 2 | 3 | ## 2.1.3 4 | * Update node-font-face-generator to 0.1.8. 5 | 6 | ## 2.0.3 7 | * Add support for woff2 files. 8 | 9 | ## 2.0.2 10 | * Re-enable the tests that check the served up CSS content. 11 | * Extract common functionality between css-responder and font-responder 12 | * Use the newest node-font-face-generator which allows an SVG ID to be specified. 13 | ## 2.0.0 14 | * Eliminate middleware global variables. All access is through instance variables. 15 | 16 | ## 1.0.0 17 | * First full release! 18 | * Support Chrome Mobile. 19 | 20 | ## 0.0.12 21 | * Support for fonts hosted on a CDN. 22 | 23 | -------------------------------------------------------------------------------- /test/sample-font-packs/shadows-into-light/fonts/Shadows Into Light- TOU.txt: -------------------------------------------------------------------------------- 1 | This font was created by Kimberly Geswein. 2 | It is for personal use only. If you wish to use it commercially I ask for 3 | a one-time US $5 paypal payment to Kimberly at gesweinfamily@gmail.com 4 | 5 | Paying the commercial license fee gives you unlimited usage of this 6 | font for your t-shirts, advertisements, websites, whatever you wish! 7 | 8 | For non-profit and/or non-commercial usage-- as long as your 9 | stuff is not racist, hateful, or anti-Christian, you are free to use it as 10 | you wish! 11 | 12 | For all have sinned and come short of the glory of God. Romans 3:23 13 | For the wages of sin is death; but the free gift of God is eternal life in Christ Jesus our Lord. Romans 6:23 14 | 15 | -------------------------------------------------------------------------------- /test/sample-font-packs/shadows-into-light/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": "Shane Tomlinson (https://shanetomlinson.com)", 3 | "name": "connect-fonts-shadows-into-light", 4 | "description": "ShadowsIntoLight font set for connect-fonts", 5 | "keywords": ["font", "font-face", "CSS", "ShadowsIntoLight", "connect-fonts"], 6 | "homepage": "https://github.com/shane-tomlinson/connect-fonts-shadows-into-light", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/shane-tomlinson/connect-fonts-shadows-into-light.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/shane-tomlinson/connect-fonts-shadows-into-light/issues" 13 | }, 14 | "version": "0.0.1", 15 | "engines": { 16 | "node": ">= 0.4.7" 17 | }, 18 | "main": "index" 19 | } 20 | -------------------------------------------------------------------------------- /test/sample-font-packs/shadows-into-light/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | "root": path.join(__dirname, "fonts"), 5 | 6 | // where to find a locale's fonts in the fonts directory 7 | "locale-to-subdirs": {}, 8 | 9 | // what font types are enabled and what are the extensions of 10 | // the font files. 11 | // 12 | // valid types are embedded-opentype, woff, truetype, svg 13 | "enabled-types": [ "eot", "woff", "ttf", "svg" ], 14 | 15 | // The fonts. The name of the font must be the same as the font 16 | // in the fonts directory. 17 | "fonts": { 18 | "shadows-into-light": { 19 | "fontFamily": "Shadows Into Light", 20 | "fontStyle": "normal", 21 | "fontWeight": "400", 22 | "local": [ "Shadows Into Light", "ShadowsIntoLight" ] 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /script/check_font_pack.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | const path = require('path'), 8 | configurator = require('../lib/font-pack-configurator'); 9 | 10 | 11 | var packConfigPath = process.argv[2]; 12 | 13 | if (!packConfigPath) { 14 | console.log("usage: " + path.basename(__filename) + " "); 15 | process.exit(1); 16 | } 17 | 18 | packConfigPath = path.join(process.cwd(), packConfigPath); 19 | 20 | var packConfig = require(packConfigPath); 21 | try { 22 | configurator(packConfig); 23 | } catch(e) { 24 | console.log(String(e)); 25 | process.exit(1); 26 | } 27 | 28 | console.log("Everything looks good!"); 29 | process.exit(0); 30 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-without-default/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const path = require("path"); 6 | 7 | module.exports = { 8 | "root": path.join(__dirname, "fonts"), 9 | 10 | // where to find a locale's fonts in the fonts directory 11 | "locale-to-subdirs": { 12 | }, 13 | 14 | // enabled font types. 15 | // 16 | // valid types are eot, woff, ttf, svg 17 | "enabled-types": [ "eot", "woff", "ttf", "svg" ], 18 | 19 | // The fonts. The name of the font must be the same as the font 20 | // in the fonts directory. 21 | "fonts": { 22 | "opensans-regular": { 23 | "fontFamily": "Open Sans", 24 | "fontStyle": "normal", 25 | "fontWeight": "400", 26 | "local": [ "Open Sans", "OpenSans" ] 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lib/respond.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const fs = require('fs'); 6 | const oppressor = require('oppressor-contrib'); 7 | 8 | exports.respond = function(req, res, filename, compress) { 9 | var pipeline = req.pipe(fs.createReadStream(filename)); 10 | 11 | if (compress) { 12 | pipeline = pipeline.pipe(oppressor(req)); 13 | } 14 | 15 | pipeline.pipe(res); 16 | }; 17 | 18 | exports.setCacheControlHeaders = function setCacheControlHeaders(res, maxAge) { 19 | if (! maxAge) return; 20 | 21 | if (! res.getHeader('Date')) { 22 | res.setHeader('Date', new Date().toUTCString()); 23 | } 24 | 25 | if (! res.getHeader('Cache-Control')) { 26 | res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000)); 27 | } 28 | }; 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const path = require("path"); 6 | 7 | module.exports = { 8 | "root": path.join(__dirname, "fonts"), 9 | 10 | // where to find a locale's fonts in the fonts directory 11 | "locale-to-subdirs": { 12 | "af": "en" 13 | }, 14 | 15 | // enabled font types. 16 | // 17 | // valid types are eot, woff, otf, ttf, svg 18 | "enabled-types": [ "eot", "woff", "woff2", "otf", "ttf", "svg" ], 19 | 20 | // The fonts. The name of the font must be the same as the font 21 | // in the fonts directory. 22 | "fonts": { 23 | "opensans-regular": { 24 | "fontFamily": "Open Sans", 25 | "fontStyle": "normal", 26 | "fontWeight": "400", 27 | "local": [ "Open Sans", "OpenSans" ] 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default-in-locale-to-subdirs/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const path = require("path"); 6 | 7 | module.exports = { 8 | "root": path.join(__dirname, "fonts"), 9 | 10 | // where to find a locale's fonts in the fonts directory 11 | "locale-to-subdirs": { 12 | "default": "en" 13 | }, 14 | 15 | // enabled font types. 16 | // 17 | // valid types are eot, woff, ttf, svg 18 | "enabled-types": [ "eot", "woff", "ttf", "svg" ], 19 | 20 | // The fonts. The name of the font must be the same as the font 21 | // in the fonts directory. 22 | "fonts": { 23 | "opensans-regular": { 24 | "fontFamily": "Open Sans", 25 | "fontStyle": "normal", 26 | "fontWeight": "400", 27 | "local": [ "Open Sans", "OpenSans" ] 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-missing-font/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const path = require("path"); 6 | 7 | module.exports = { 8 | "root": path.join(__dirname, "fonts"), 9 | 10 | // where to find a locale's fonts in the fonts directory 11 | "locale-to-subdirs": { 12 | }, 13 | 14 | // enabled font types. 15 | // 16 | // valid types are eot, woff, ttf, svg 17 | // 18 | // the ttf font is missing from the directory, causing an error to be thrown. 19 | "enabled-types": [ "eot", "woff", "ttf", "svg" ], 20 | 21 | // The fonts. The name of the font must be the same as the font 22 | // in the fonts directory. 23 | "fonts": { 24 | "opensans-regular": { 25 | "fontFamily": "Open Sans", 26 | "fontStyle": "normal", 27 | "fontWeight": "400", 28 | "local": [ "Open Sans", "OpenSans" ] 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /test/sample-font-packs/shadows-into-light/LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under MPL 2.0 2 | Shadows Into Light is licenced under TOU as set by Kimbery Geswein. 3 | 4 | ============== 5 | 6 | This Source Code Form is subject to the terms of the Mozilla Public 7 | License, v. 2.0. If a copy of the MPL was not distributed with this file, 8 | You can obtain one at http://mozilla.org/MPL/2.0/. 9 | 10 | ============== 11 | 12 | This font was created by Kimberly Geswein. 13 | It is for personal use only. If you wish to use it commercially I ask for 14 | a one-time US $5 paypal payment to Kimberly at gesweinfamily@gmail.com 15 | 16 | Paying the commercial license fee gives you unlimited usage of this 17 | font for your t-shirts, advertisements, websites, whatever you wish! 18 | 19 | For non-profit and/or non-commercial usage-- as long as your 20 | stuff is not racist, hateful, or anti-Christian, you are free to use it as 21 | you wish! 22 | 23 | For all have sinned and come short of the glory of God. Romans 3:23 24 | For the wages of sin is death; but the free gift of God is eternal life in Christ Jesus our Lord. Romans 6:23 25 | 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Shane Tomlinson (https://shanetomlinson.com)", 3 | "name": "connect-fonts", 4 | "description": "Middleware to serve up web fonts.", 5 | "keywords": [ 6 | "font", 7 | "font-face", 8 | "CSS", 9 | "webfont", 10 | "typography" 11 | ], 12 | "homepage": "https://github.com/shane-tomlinson/connect-fonts", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/shane-tomlinson/connect-fonts.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/shane-tomlinson/connect-fonts/issues" 19 | }, 20 | "license": "MPL-2.0", 21 | "version": "2.1.5", 22 | "engines": { 23 | "node": ">= 6.0.0" 24 | }, 25 | "main": "index", 26 | "dependencies": { 27 | "mime": "2.0.3", 28 | "node-font-face-generator": "0.1.9", 29 | "oppressor-contrib": "1.0.1", 30 | "tmp": "0.0.33" 31 | }, 32 | "devDependencies": { 33 | "fs-extra": "4.0.2", 34 | "mkdirp": "0.5.1", 35 | "nodeunit": "0.11.1", 36 | "nsp": "2.8.1", 37 | "optimist": "0.6.1" 38 | }, 39 | "scripts": { 40 | "nsp": "nsp check", 41 | "test": "nodeunit test" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const 6 | css_generator = require("node-font-face-generator"), 7 | MissingConfigError = css_generator.MissingConfigError; 8 | 9 | exports.getRequired = function(options, name) { 10 | if (!options) { 11 | throw new MissingConfigError("options not specified"); 12 | } 13 | else if (name && !(name in options)) { 14 | throw new MissingConfigError("Missing required option: " + name); 15 | } 16 | 17 | return name ? options[name] : options; 18 | }; 19 | 20 | exports.checkRequired = function(options, name) { 21 | exports.getRequired(options, name); 22 | }; 23 | 24 | exports.asyncForEach = function(array, cb, done) { 25 | 26 | var index = 0; 27 | var arrayCopy = [].concat(array); 28 | 29 | next(null); 30 | 31 | function next(err) { 32 | if (err) return done && done(err); 33 | 34 | var item = arrayCopy.shift(); 35 | if (!item) return done && done(null); 36 | var idx = index; 37 | index++; 38 | cb(item, idx, next); 39 | } 40 | }; 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/mocks/req-mock.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const stream = require('stream'); 6 | const util = require('util'); 7 | 8 | 'use strict'; 9 | 10 | function ReqMock(options) { 11 | options = options || {}; 12 | 13 | stream.Stream.call(this); 14 | 15 | this.writable = true; 16 | this.readable = true; 17 | 18 | this.headers = { 19 | 'user-agent': options['user-agent'], 20 | 'if-none-match': options['if-none-match'] 21 | }; 22 | 23 | this.method = (options.method || 'GET').toUpperCase(); 24 | this.url = options.url || '/'; 25 | this.params = {}; 26 | } 27 | 28 | util.inherits(ReqMock, stream.Stream); 29 | 30 | ReqMock.prototype.getHeader = function(header) { 31 | return this.headers[header.toLowerCase()]; 32 | }; 33 | 34 | ReqMock.prototype.pipe = function(dest) { 35 | this.dest = dest; 36 | dest.emit('pipe', this); 37 | return dest; 38 | }; 39 | 40 | ReqMock.prototype.write = function(chunk, encoding, callback) { 41 | this.emit('data', chunk); 42 | if (callback) callback(); 43 | return true; 44 | }; 45 | 46 | ReqMock.prototype.end = function (chunk, encoding, callback) { 47 | this.emit('end', chunk); 48 | if (callback) callback(); 49 | }; 50 | 51 | 52 | module.exports = ReqMock; 53 | 54 | -------------------------------------------------------------------------------- /test/sample-config/fonts.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * This is a sample configuration in the format that is expected by 7 | * node-font-face-generator. This is not the same config format as a font pack. 8 | * To see examples of font pack configuration, see ../sample-font-packs/ 9 | */ 10 | const path = require('path'); 11 | 12 | module.exports = { 13 | "opensans-regular": { 14 | "root": path.join(__dirname, "..", "sample-data"), 15 | 16 | "fontFamily": "Open Sans", 17 | "fontStyle": "normal", 18 | "fontWeight": "400", 19 | "formats": [ 20 | { 21 | "type": "local", 22 | "url": "Open Sans" 23 | }, 24 | { 25 | "type": "local", 26 | "url": "OpenSans" 27 | }, 28 | { 29 | "type": "embedded-opentype", 30 | "url": "/fonts/en/opensans-regular.eot" 31 | }, 32 | { 33 | "type": "woff", 34 | "url": { 35 | "en": "/fonts/en/opensans-regular.woff", 36 | "default": "/fonts/default/opensans-regular.woff" 37 | } 38 | }, 39 | { 40 | "type": "truetype", 41 | "url": "/fonts/en/opensans-regular.ttf" 42 | } 43 | ], 44 | "localeToUrlKeys": { 45 | "af": "en" 46 | } 47 | } 48 | }; 49 | 50 | -------------------------------------------------------------------------------- /test/sample-font-packs/shadows-into-light/README.md: -------------------------------------------------------------------------------- 1 | # connect-fonts-shadows-into-light 2 | 3 | A [connect-fonts](https://github.com/shane-tomlinson/connect-fonts) fontpack for the shadowsintolight font. 4 | 5 | ## Usage 6 | 7 | 1. Include [connect-fonts](https://github.com/shane-tomlinson/connect-fonts) in a node module. 8 | ``` 9 | const font_middleware = require("connect-fonts"); 10 | ``` 11 | 12 | 2. Include the font packs that you want to serve. 13 | ``` 14 | const shadowsintolight = require("connect-fonts-shadows-into-light"); 15 | ``` 16 | 17 | 3. Add a middleware by calling the `setup` function. 18 | ``` 19 | app.use(font_middleware.setup({ 20 | fonts: [ shadowsintolight ], 21 | allow_origin: "https://exampledomain.com" 22 | })); 23 | ``` 24 | 25 | 4. Add a link tag to include the font CSS. 26 | ``` 27 | 28 | ``` 29 | 30 | 5. Set your CSS up to use the new font by using the "Source Sans Pro" font-family. 31 | ``` 32 | body { 33 | font-family: 'Shadows Into Light', 'sans-serif', 'serif'; 34 | } 35 | ``` 36 | 37 | 38 | ## Author 39 | * Shane Tomlinson 40 | * shane@shanetomlinson.com 41 | * stomlinson@mozilla.com 42 | * set117@yahoo.com 43 | * https://shanetomlinson.com 44 | * http://github.com/stomlinson 45 | * http://github.com/shane-tomlinson 46 | * @shane_tomlinson 47 | 48 | ## Credits 49 | 50 | Original font set downloaded from www.fontspace.com. Shadows Into Light created by [Kimberly Geswein](http://www.kimberlygeswein.com/) 51 | 52 | ## License 53 | 54 | This software is licenced under version 2.0 of the MPL 55 | 56 | https://www.mozilla.org/MPL/ 57 | 58 | Shadows Into Light is licenced under TOU as set by Kimbery Geswein. 59 | 60 | It is for personal use only. If you wish to use it commercially I ask for 61 | a one-time US $5 paypal payment to Kimberly at gesweinfamily@gmail.com 62 | 63 | Paying the commercial license fee gives you unlimited usage of this 64 | font for your t-shirts, advertisements, websites, whatever you wish! 65 | 66 | For non-profit and/or non-commercial usage-- as long as your 67 | stuff is not racist, hateful, or anti-Christian, you are free to use it as 68 | you wish! 69 | 70 | For all have sinned and come short of the glory of God. Romans 3:23 71 | For the wages of sin is death; but the free gift of God is eternal life in Christ Jesus our Lord. Romans 6:23 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /script/process_directory.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | const fs = require('fs'), 8 | path = require('path'), 9 | optimist = require('optimist') 10 | .usage('usage: ' + path.basename(__filename) + ' [-d]') 11 | .describe('d', 'Perform a dry run without changing files on disk') 12 | .default('d', false), 13 | argv = optimist.argv; 14 | 15 | const extensionsToRename = [ 16 | 'eot', 17 | 'svg', 18 | 'ttf', 19 | 'woff' 20 | ]; 21 | 22 | /** 23 | * This is a helper script that will convert the names of the font files in 24 | * a directory to make it simpler to work with connect-fonts 25 | * 26 | * All filenames will be: 27 | * => lowercased 28 | * => -webfont will be removed. 29 | * => it. will be changed to italics. 30 | */ 31 | 32 | function nonExistentDirectory(directory) { 33 | console.error(directory + " does not exist"); 34 | process.exit(1); 35 | } 36 | 37 | function invalidDirectory(directory) { 38 | console.error(directory + " is not a directory"); 39 | process.exit(1); 40 | } 41 | 42 | 43 | var subdirName = process.argv[2]; 44 | if (!subdirName) { 45 | optimist.showHelp(); 46 | process.exit(1); 47 | } 48 | 49 | var subdir = /^\//.test(subdirName) ? subdirName 50 | : path.join(process.cwd(), subdirName); 51 | 52 | if (!fs.existsSync(subdir)) { 53 | nonExistentDirectory(subdirName); 54 | } 55 | 56 | var stats = fs.statSync(subdir); 57 | if (!stats.isDirectory()) { 58 | invalidDirectory(subdirName); 59 | } 60 | 61 | var numFilesProcessed = 0; 62 | var files = fs.readdirSync(subdir); 63 | files.forEach(function(file) { 64 | if (extensionsToRename.indexOf(path.extname(file).replace('.', '')) > -1) { 65 | var newname = file.toLowerCase(); 66 | newname = newname.replace('-webfont', ''); 67 | newname = newname.replace('it.', 'italics.'); 68 | 69 | var oldpath = path.join(subdir, file); 70 | var newpath = path.join(subdir, newname); 71 | 72 | if (oldpath !== newpath) { 73 | console.log("* " + file + " => " + newname); 74 | if (!argv.d) { 75 | fs.renameSync(oldpath, newpath); 76 | } 77 | numFilesProcessed++; 78 | } 79 | } 80 | }); 81 | 82 | console.log(numFilesProcessed + " files processed"); 83 | 84 | -------------------------------------------------------------------------------- /npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-fonts", 3 | "version": "2.1.5", 4 | "dependencies": { 5 | "mime": { 6 | "version": "2.0.3", 7 | "from": "mime@2.0.3", 8 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.0.3.tgz" 9 | }, 10 | "negotiator": { 11 | "version": "0.6.1", 12 | "from": "negotiator@>=0.6.1 <0.7.0", 13 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz" 14 | }, 15 | "node-font-face-generator": { 16 | "version": "0.1.9", 17 | "from": "node-font-face-generator@0.1.9", 18 | "resolved": "https://registry.npmjs.org/node-font-face-generator/-/node-font-face-generator-0.1.9.tgz", 19 | "dependencies": { 20 | "ejs": { 21 | "version": "1.0.0", 22 | "from": "ejs@1.0.0", 23 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-1.0.0.tgz" 24 | }, 25 | "node-uap": { 26 | "version": "0.0.3", 27 | "from": "node-uap@0.0.3", 28 | "resolved": "https://registry.npmjs.org/node-uap/-/node-uap-0.0.3.tgz", 29 | "dependencies": { 30 | "uap-core": { 31 | "version": "0.5.0", 32 | "from": "git://github.com/ua-parser/uap-core.git", 33 | "resolved": "git://github.com/ua-parser/uap-core.git#5dceb896913fc953d52881bf77e6ed64fd1b4054" 34 | }, 35 | "uap-ref-impl": { 36 | "version": "0.2.0", 37 | "from": "uap-ref-impl@0.2.0", 38 | "resolved": "https://registry.npmjs.org/uap-ref-impl/-/uap-ref-impl-0.2.0.tgz" 39 | }, 40 | "yamlparser": { 41 | "version": "0.0.2", 42 | "from": "yamlparser@0.0.2", 43 | "resolved": "https://registry.npmjs.org/yamlparser/-/yamlparser-0.0.2.tgz" 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | "oppressor-contrib": { 50 | "version": "1.0.1", 51 | "from": "oppressor-contrib@1.0.1", 52 | "resolved": "https://registry.npmjs.org/oppressor-contrib/-/oppressor-contrib-1.0.1.tgz" 53 | }, 54 | "os-tmpdir": { 55 | "version": "1.0.2", 56 | "from": "os-tmpdir@>=1.0.2 <1.1.0", 57 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" 58 | }, 59 | "response-stream": { 60 | "version": "0.0.0", 61 | "from": "response-stream@>=0.0.0 <0.1.0", 62 | "resolved": "https://registry.npmjs.org/response-stream/-/response-stream-0.0.0.tgz" 63 | }, 64 | "through": { 65 | "version": "0.1.4", 66 | "from": "through@>=0.1.4 <0.2.0", 67 | "resolved": "https://registry.npmjs.org/through/-/through-0.1.4.tgz" 68 | }, 69 | "tmp": { 70 | "version": "0.0.33", 71 | "from": "tmp@0.0.33", 72 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/mocks/res-mock.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const stream = require('stream'); 6 | const util = require('util'); 7 | 8 | function RespMock(options) { 9 | options = options || {}; 10 | 11 | stream.Stream.call(this); 12 | 13 | this.writable = true; 14 | this.readable = true; 15 | 16 | this._headers = {}; 17 | this._options = options; 18 | 19 | this.on('pipe', function (src) { 20 | this.src = src; 21 | }); 22 | } 23 | 24 | util.inherits(RespMock, stream.Stream); 25 | 26 | 27 | RespMock.prototype.setHeader = function(header, value) { 28 | this._headers[header.toLowerCase()] = value; 29 | if (this._options.setHeader) this._options.setHeader.call(this, header, value); 30 | }; 31 | 32 | RespMock.prototype.getHeader = function(header) { 33 | return this._headers[header.toLowerCase()]; 34 | }; 35 | 36 | RespMock.prototype.header = function(header) { 37 | return this.getHeader(header.toLowerCase()); 38 | }; 39 | 40 | RespMock.prototype.removeHeader = function(header) { 41 | delete this._headers[header.toLowerCase()]; 42 | }; 43 | 44 | RespMock.prototype.toString = function() { 45 | return "status: " + this.statusCode + " " + 46 | JSON.stringify(this._headers, null, 2); 47 | }; 48 | 49 | RespMock.prototype.write = function(chunk, encoding, callback) { 50 | if (! this.data) { 51 | this.data = ''; 52 | } 53 | 54 | this.data += chunk.toString(encoding); 55 | if (callback) callback(); 56 | return true; 57 | }; 58 | 59 | RespMock.prototype.writeHead = function(statusCode, headers) { 60 | this.statusCode = statusCode; 61 | for (var key in headers) { 62 | this.setHeader(key, headers[key]); 63 | } 64 | }; 65 | 66 | RespMock.prototype.send = function(data, statusCode) { 67 | if (data) this.data = data; 68 | this.statusCode = statusCode || 200; 69 | 70 | if (this._options.send) this._options.send(data, statusCode); 71 | }; 72 | 73 | RespMock.prototype.getData = function() { 74 | return this.data; 75 | }; 76 | 77 | RespMock.prototype.getStatusCode = function() { 78 | return this.statusCode; 79 | }; 80 | 81 | RespMock.prototype.end = function(chunk, encoding, callback) { 82 | // this is probably wrong, but we need to emit the header event somewhere. 83 | this.emit('header'); 84 | 85 | this.encoding = encoding || 'utf8'; 86 | if (chunk) { 87 | if (! this.data) { 88 | this.data = ''; 89 | } 90 | 91 | this.data += String(chunk); 92 | } 93 | if (!this.statusCode) this.statusCode = 200; 94 | 95 | if (this._options.end) this._options.end.call(this, chunk, encoding); 96 | if (callback) callback(); 97 | }; 98 | 99 | module.exports = RespMock; 100 | -------------------------------------------------------------------------------- /lib/font-responder.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * Handle font requests. The font_responder will look for urls in the 7 | * urlToPaths map. If the url is found, the file is served. 8 | * .woff files are served with the Access-Control-Allow-Origin 9 | * header. 10 | */ 11 | 12 | const util = require("./util"), 13 | respond = require('./respond'), 14 | mime = require("mime"); 15 | 16 | /** 17 | * Certain types require Access-Control-Allow-Origin headers or 18 | * else they are not rendered. 19 | */ 20 | const TYPES_REQUIRE_ALLOW_ORIGIN_HEADERS = [ 21 | "application/x-font-woff", 22 | "application/font-woff", 23 | "application/font-woff2" 24 | ]; 25 | 26 | /** 27 | * .woff, and .woff2 are already compressed, so do not compress it. 28 | */ 29 | const TYPES_TO_COMPRESS = [ 30 | "application/vnd.ms-opentype", 31 | "application/x-font-ttf", 32 | "application/font-ttf", 33 | "application/x-font-otf", 34 | "application/font-otf", 35 | "image/svg+xml" 36 | ]; 37 | 38 | /** 39 | * setup the middleware. 40 | * @param {object} options 41 | * @param {object} options.url_to_paths - map of font urls to absolute 42 | * paths on disk 43 | * @param {string} options.allow_origin - origin to set for 44 | * Access-Control-Allow-Origin header. 45 | * @param {number} options.maxage - Provide a max-age in milliseconds for http 46 | * caching, defaults to 0. 47 | * @param {boolean} options.compress - Whether to comprss the result. 48 | */ 49 | exports.setup = function(options) { 50 | var urlToPaths = util.getRequired(options, "url_to_paths"); 51 | var allowOrigin = options.allow_origin; 52 | var maxAge = options.maxage || 0; 53 | var compress = options.compress || false; 54 | 55 | /** 56 | * The font responding middleware 57 | */ 58 | return function(req, res, next) { 59 | // get rid of any GET parameters before searching for the URL. 60 | var url = req.url.replace(/\?.*$/, ''); 61 | var fontPath = urlToPaths[url]; 62 | if (! fontPath) return next(); 63 | 64 | res.fontPath = fontPath; 65 | 66 | res.on('header', function() { 67 | setAccessControlHeaders(res, allowOrigin); 68 | respond.setCacheControlHeaders(res, maxAge); 69 | }); 70 | 71 | respond.respond(req, res, fontPath, shouldCompress(res, compress)); 72 | }; 73 | }; 74 | 75 | function setAccessControlHeaders(res, allowOrigin) { 76 | var type = getFontContentType(res); 77 | if (allowOrigin && TYPES_REQUIRE_ALLOW_ORIGIN_HEADERS.indexOf(type) > -1) { 78 | res.setHeader("Access-Control-Allow-Origin", allowOrigin); 79 | } 80 | } 81 | 82 | function shouldCompress(res, compress) { 83 | var type = getFontContentType(res); 84 | return (compress && TYPES_TO_COMPRESS.indexOf(type) > -1); 85 | } 86 | 87 | function getFontContentType(res) { 88 | var type = res.getHeader("Content-Type") || res.type || mime.getType(res.fontPath); 89 | res.type = type; 90 | return type; 91 | } 92 | 93 | 94 | -------------------------------------------------------------------------------- /test/font-pack-configurator-test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var path = require('path'), 6 | nodeunit = require('nodeunit'), 7 | configurator = require('../lib/font-pack-configurator'), 8 | pack_with_default = require('./sample-font-packs/fonts-with-default/index'), 9 | pack_with_default_in_locale_to_subdirs 10 | = require('./sample-font-packs/fonts-with-default-in-locale-to-subdirs/index'), 11 | pack_without_default = require('./sample-font-packs/fonts-without-default/index'), 12 | pack_missing_font = require('./sample-font-packs/fonts-missing-font/index'); 13 | 14 | exports.font_pack_configurator = nodeunit.testCase({ 15 | setUp: function (cb) { 16 | cb(); 17 | }, 18 | tearDown: function (cb) { 19 | cb(); 20 | }, 21 | 22 | 'get font pack configuration of valid font pack': function(test) { 23 | var config = configurator(pack_with_default); 24 | test.ok("opensans-regular" in config); 25 | 26 | var fontConfig = config["opensans-regular"]; 27 | test.equal(fontConfig.fontFamily, "Open Sans"); 28 | test.equal(fontConfig.fontStyle, "normal"); 29 | test.equal(fontConfig.fontWeight, "400"); 30 | 31 | test.equal(fontConfig.root, path.join(__dirname, "sample-font-packs", "fonts-with-default", "fonts/")); 32 | 33 | // 6 fonts in config, 2 local, 6 remote. 34 | // four remotes are svg, woff, woff2, opentype, truetype and embedded-opentype 35 | test.equal(fontConfig.formats.length, 8); 36 | 37 | // each of the remote fonts should have three locale's specified. 38 | // (locales are en, latin, default) 39 | test.equal(Object.keys(fontConfig.urlToPaths).length, 18); 40 | 41 | // check the paths to make sure they match what is expected. 42 | for (var url in fontConfig.urlToPaths) { 43 | var fontPath = fontConfig.urlToPaths[url]; 44 | test.equal(fontPath, fontConfig.root + url.replace('/fonts/', '')); 45 | } 46 | 47 | fontConfig.formats.forEach(function(format) { 48 | if (format.type === "local") { 49 | test.ok(format.url === "Open Sans" || format.url === "OpenSans"); 50 | } 51 | else { 52 | test.ok("default" in format.url); 53 | test.ok("en" in format.url); 54 | 55 | test.ok(["truetype", "opentype", "svg", "embedded-opentype", "woff", "woff2"] 56 | .indexOf(format.type) > -1); 57 | 58 | if (format.type === "svg") { 59 | test.ok(format.id); 60 | } 61 | 62 | // check that the format of the URL is good 63 | test.ok(format.url.en.indexOf(path.join( 64 | "fonts", "en", "opensans-regular")) > -1); 65 | } 66 | }); 67 | 68 | test.done(); 69 | }, 70 | 71 | 'error thrown if a font file is missing': function(test) { 72 | var err; 73 | 74 | try { 75 | var config = configurator(pack_missing_font); 76 | } catch(e) { 77 | err = e; 78 | } 79 | 80 | test.ok(err); 81 | test.done(); 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /script/convert_zip.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | const fs = require('fs-extra'), 8 | path = require('path'), 9 | tmp = require('tmp'), 10 | mkdirp = require('mkdirp'), 11 | child_process = require('child_process'), 12 | optimist = require('optimist') 13 | .usage('usage: ' + path.basename(__filename) + 14 | ' [-d]') 15 | .describe('d', 'Perform a dry run without' + 16 | ' changing files on disk') 17 | .default('d', false), 18 | argv = optimist.argv; 19 | 20 | tmp.setGracefulCleanup(); 21 | 22 | const extensionsToCopy = [ 23 | '.eot', 24 | '.svg', 25 | '.ttf', 26 | '.woff' 27 | ]; 28 | 29 | 30 | var fileName = argv._[0]; 31 | var targetName = argv._[1]; 32 | 33 | if (!(fileName && targetName)) { 34 | optimist.showHelp(); 35 | process.exit(1); 36 | } 37 | 38 | if (path.extname(fileName) !== '.zip') { 39 | notAZip(); 40 | } 41 | 42 | var filePath = /^\//.test(fileName) ? fileName 43 | : path.join(process.cwd(), fileName); 44 | if (!fs.existsSync(filePath)) { 45 | nonExistentFile(fileName); 46 | } 47 | 48 | var stats = fs.statSync(filePath); 49 | if (!stats.isFile()) { 50 | invalidFile(fileName); 51 | } 52 | 53 | tmp.dir(function(err, tmpPath) { 54 | if (err) throw err; 55 | 56 | console.log("unzipping " + path.basename(filePath)); 57 | spawn('unzip', [ filePath, '-d', tmpPath ], null, function() { 58 | console.log("processing files for connect-fonts"); 59 | 60 | processorPath = path.join(__dirname, 'process_directory.js'); 61 | 62 | spawn('node', [ processorPath, tmpPath ], null, function() { 63 | mkdirp(targetName); 64 | var files = fs.readdirSync(tmpPath); 65 | files.forEach(function(file, index) { 66 | files[index] = path.join(tmpPath, file); 67 | }); 68 | console.log(files); 69 | copyNext(files, targetName); 70 | }); 71 | }); 72 | }); 73 | 74 | 75 | function nonExistentFile(file) { 76 | console.error(file + " does not exist"); 77 | process.exit(1); 78 | } 79 | 80 | function invalidFile(file) { 81 | console.error(file + " is not a file"); 82 | process.exit(1); 83 | } 84 | 85 | function notAZip(file) { 86 | console.error(path.basename(__filename) + " can only work with zip files"); 87 | process.exit(1); 88 | } 89 | 90 | function spawn(cmd, args, opts, done) { 91 | var child = child_process.spawn(cmd, args, opts); 92 | child.stdout.pipe(process.stdout); 93 | child.stderr.pipe(process.stderr); 94 | child.on('exit', function(code) { 95 | if (code) { 96 | console.error(cmd + " exited with code " + code); 97 | process.exit(code); 98 | } 99 | done(null); 100 | }); 101 | } 102 | 103 | function copyNext(files, target) { 104 | var file = files.shift(); 105 | if (file) { 106 | if (extensionsToCopy.indexOf(path.extname(file)) > -1) { 107 | var outputPath = path.join(target, path.basename(file)); 108 | console.log("copying", path.basename(file)); 109 | fs.copy(file, outputPath, copyNext.bind(null, files, target)); 110 | } 111 | else { 112 | copyNext(files, target); 113 | } 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const util = require("./util"); 6 | const configurator = require("./font-pack-configurator"); 7 | const css_responder = require("./css-responder"); 8 | const font_responder = require("./font-responder"); 9 | const asyncForEach = require("./util").asyncForEach; 10 | 11 | /** 12 | * Does initialization 13 | * @method setup 14 | * @param {object} options 15 | * @param {array} options.fonts - list of supported font configs. 16 | * @param {string} options.allow_origin - Origin to use for fonts that 17 | * require an Access-Control-Allow-Origin header. 18 | * @param {number} options.maxage - Provide a max-age in milliseconds for http 19 | * caching, defaults to 0. 20 | * @param {boolean} options.compress - Whether to compress the output. 21 | */ 22 | exports.setup = function (options) { 23 | util.checkRequired(options); 24 | var fontPacks = util.getRequired(options, "fonts"); 25 | var allowOrigin = options.allow_origin; 26 | var maxAge = options.maxage || 0; 27 | var compress = options.compress || false; 28 | 29 | // reset these every time setup is called. 30 | var fontConfigs = exports.fontConfigs = {}; 31 | var urlToPaths = exports.urlToPaths = {}; 32 | 33 | var cssResponder = css_responder.setup({ 34 | fonts: fontConfigs, 35 | locale_to_url_keys: {}, 36 | ua: options.ua, 37 | maxage: maxAge, 38 | compress: compress 39 | }); 40 | 41 | var fontResponder = font_responder.setup({ 42 | url_to_paths: urlToPaths, 43 | allow_origin: allowOrigin, 44 | maxage: maxAge, 45 | compress: compress 46 | }); 47 | 48 | var middleware = function (req, res, next) { 49 | cssResponder(req, res, function () { 50 | fontResponder(req, res, next); 51 | }); 52 | }; 53 | 54 | middleware.urlToPaths = urlToPaths; 55 | middleware.fontConfigs = fontConfigs; 56 | middleware.generate_css = cssResponder.generate_css.bind(cssResponder); 57 | 58 | /** 59 | * Register a font pack 60 | * @method registerFontPack 61 | * @param {object} fontConfig 62 | * @param {function} done 63 | */ 64 | middleware.registerFontPack = function (fontPackConfig, done) { 65 | // font pack configuration must be changed to a form that 66 | // node-font-face-generator understands. 67 | var packsFontConfigs = configurator(fontPackConfig); 68 | var fontNames = Object.keys(packsFontConfigs); 69 | 70 | asyncForEach(fontNames, function (fontName, index, next) { 71 | var fontConfig = packsFontConfigs[fontName]; 72 | cssResponder.registerFont(fontName, fontConfig, function (err) { 73 | if (err) return done && done(err); 74 | 75 | // Registration has occurred just fine and dandy, save a reference to the 76 | // packConfig onto the fontConfig in case somebody wants to use it to 77 | // programatically display font information and update the local cache of 78 | // font configs and urlToPaths. 79 | fontConfig.packConfig = fontPackConfig; 80 | fontConfigs[fontName] = fontConfig; 81 | for (var url in fontConfig.urlToPaths) { 82 | urlToPaths[url] = fontConfig.urlToPaths[url]; 83 | } 84 | 85 | next(); 86 | }); 87 | }, function (err) { 88 | if (! done) return; 89 | if (err) return done(err); 90 | done(null, fontNames); 91 | }); 92 | }; 93 | 94 | // Now, register the font packs. 95 | fontPacks.forEach(function (fontPack) { 96 | middleware.registerFontPack(fontPack); 97 | }); 98 | 99 | 100 | return middleware; 101 | }; 102 | 103 | 104 | -------------------------------------------------------------------------------- /test/font-responder-test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const font_responder = require('../lib/font-responder'); 6 | const configurator = require('../lib/font-pack-configurator'); 7 | const pack_config = require('./sample-font-packs/fonts-with-default/index'); 8 | const nodeunit = require('nodeunit'); 9 | const ReqMock = require('./mocks/req-mock'); 10 | const ResMock = require('./mocks/res-mock'); 11 | 12 | const TEST_DOMAIN = "http://testdomain.com"; 13 | 14 | // Set a 180 day cache. 15 | const MAX_AGE = 1000 * 60 * 60 * 24 * 180; 16 | 17 | var responder; 18 | 19 | function testFontAvailable(url, contentType, test, done) { 20 | var req = new ReqMock({ 21 | url: url 22 | }); 23 | var res = new ResMock({ 24 | end: function() { 25 | if (done) return done(this); 26 | 27 | // Make sure Cache-Control headers are set. 28 | test.ok(!!this.getHeader('Cache-Control')); 29 | 30 | // contentType is set by filed. Since we are not actually streaming, we 31 | // don't know what the font type is. 32 | 33 | test.done(); 34 | } 35 | }); 36 | 37 | responder(req, res, function() { 38 | test.ok(false); 39 | }); 40 | 41 | req.end(); 42 | } 43 | 44 | exports['font-responder-test'] = nodeunit.testCase({ 45 | setUp: function (cb) { 46 | var config = configurator(pack_config); 47 | responder = font_responder.setup({ 48 | url_to_paths: config["opensans-regular"].urlToPaths, 49 | allow_origin: TEST_DOMAIN, 50 | maxage: MAX_AGE, 51 | compress: false 52 | }); 53 | cb(); 54 | }, 55 | 56 | tearDown: function (cb) { 57 | cb(); 58 | }, 59 | 60 | 'unrecognized url calls next': function(test) { 61 | var req = new ReqMock({ 62 | url: "/unrecognized/url.html" 63 | }); 64 | var res = new ResMock(); 65 | 66 | responder(req, res, function() { 67 | test.done(); 68 | }); 69 | }, 70 | 71 | 'unrecognized font calls next': function(test) { 72 | var req = new ReqMock({ 73 | url: "/fonts/en/unknown-font.woff" 74 | }); 75 | var res = new ResMock(); 76 | 77 | responder(req, res, function() { 78 | test.done(); 79 | }); 80 | }, 81 | 82 | 'recognized font with unrecognized language calls next': function(test) { 83 | var req = new ReqMock({ 84 | url: "/fonts/ru/opensans-regular.woff" 85 | }); 86 | var res = new ResMock(); 87 | 88 | responder(req, res, function() { 89 | test.done(); 90 | }); 91 | }, 92 | 93 | 'woff: recognized font, font file available - send the file': function(test) { 94 | testFontAvailable("/fonts/en/opensans-regular.woff", "application/x-font-woff", test, function(res) { 95 | test.equal(res.getHeader("Access-Control-Allow-Origin"), TEST_DOMAIN); 96 | test.done(); 97 | }); 98 | }, 99 | 100 | 'woff2: recognized font, font file available - send the file': function(test) { 101 | testFontAvailable("/fonts/en/opensans-regular.woff2", "application/x-font-woff", test, function(res) { 102 | test.equal(res.getHeader("Access-Control-Allow-Origin"), TEST_DOMAIN); 103 | test.done(); 104 | }); 105 | }, 106 | 107 | 'svg: recognized font, font file available - send the file': function(test) { 108 | testFontAvailable("/fonts/en/opensans-regular.svg", "image/svg+xml", test); 109 | }, 110 | 111 | 'eot: recognized font, font file available - send the file': function(test) { 112 | testFontAvailable("/fonts/en/opensans-regular.eot", "application/vnd.ms-fontobject", test); 113 | }, 114 | 115 | 'ttf: recognized font, font file available - send the file': function(test) { 116 | testFontAvailable("/fonts/en/opensans-regular.ttf", "application/x-font-ttf", test); 117 | }, 118 | 119 | 'otf: recognized font, font file available - send the file': function(test) { 120 | testFontAvailable("/fonts/en/opensans-regular.otf", "application/x-font-otf", test); 121 | }, 122 | 123 | 'recognized font with ? on end - send the file': function(test) { 124 | testFontAvailable("/fonts/en/opensans-regular.eot?#iefix", "application/vnd.ms-fontobject", test); 125 | } 126 | 127 | }); 128 | 129 | -------------------------------------------------------------------------------- /test/css-responder-test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const nodeunit = require('nodeunit'); 6 | const ReqMock = require('./mocks/req-mock'); 7 | const ResMock = require('./mocks/res-mock'); 8 | const font_config = require('./sample-config/fonts'); 9 | const css_responder = require('../lib/css-responder'); 10 | 11 | // Set a 180 day cache. 12 | const MAX_AGE = 1000 * 60 * 60 * 24 * 180; 13 | 14 | var middleware; 15 | 16 | exports.css_responder = nodeunit.testCase({ 17 | setUp: function (cb) { 18 | middleware = css_responder.setup({ 19 | fonts: font_config, 20 | locale_to_url_keys: {}, 21 | maxage: MAX_AGE, 22 | compress: false 23 | }); 24 | cb(); 25 | }, 26 | 27 | 'generate_css generates CSS': function(test) { 28 | middleware.generate_css("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20130125 Firefox/21.0", "en", ["opensans-regular"], function(err, cssObj) { 29 | test.equal(err, null); 30 | test.ok(cssObj.css.length); 31 | test.done(); 32 | }); 33 | }, 34 | 35 | 'get_css gets CSS': function(test) { 36 | middleware.get_css("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20130125 Firefox/21.0", "en", ["opensans-regular"], function(err, cssObj) { 37 | test.equal(err, null); 38 | test.ok(cssObj.css.length); 39 | test.done(); 40 | }); 41 | }, 42 | 43 | 'get_css gets CSS for locale specified in locale-to-subdirs': function(test) { 44 | middleware.get_css("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20130125 Firefox/21.0", "af", ["opensans-regular"], function(err, cssObj) { 45 | test.equal(err, null); 46 | test.ok(cssObj.css.indexOf("/fonts/en/opensans-regular.woff") > -1); 47 | test.done(); 48 | }); 49 | }, 50 | 51 | 'middleware responds to font.css requests that specify a locale': function(test) { 52 | var req = new ReqMock({ 53 | url: '/en/opensans-regular/fonts.css', 54 | method: 'GET', 55 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20130125 Firefox/21.0" 56 | }); 57 | 58 | var res = new ResMock({ 59 | end: function() { 60 | test.ok(this.getData().indexOf("/en/opensans-regular.woff") > -1); 61 | test.ok(this.getHeader('Cache-Control')); 62 | test.done(); 63 | } 64 | }); 65 | 66 | middleware(req, res, function() { 67 | // this should not be called. 68 | test.ok(false); 69 | test.done(); 70 | }); 71 | 72 | req.end(); 73 | }, 74 | 75 | 'middleware responds to font.css requests that do not specify a locale- default locale used': function(test) { 76 | var req = new ReqMock({ 77 | url: '/opensans-regular/fonts.css', 78 | method: 'GET', 79 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20130125 Firefox/21.0" 80 | }); 81 | 82 | var res = new ResMock({ 83 | end: function() { 84 | test.ok(this.getData().indexOf("/default/opensans-regular.woff") > -1); 85 | // Make sure Cache-Control headers are set. 86 | test.ok(this.getHeader('Cache-Control')); 87 | test.done(); 88 | } 89 | }); 90 | 91 | middleware(req, res, function() { 92 | // this should not be called. 93 | test.ok(false); 94 | test.done(); 95 | }); 96 | 97 | req.end(); 98 | }, 99 | 100 | 'registerFont registers a font after setup has been called': function(test) { 101 | middleware.registerFont("font_name", font_config, function(err) { 102 | test.ok(!err); 103 | test.done(); 104 | }); 105 | } 106 | 107 | }); 108 | 109 | exports.css_responder_with_cdn = nodeunit.testCase({ 110 | setUp: function (cb) { 111 | middleware = css_responder.setup({ 112 | fonts: font_config, 113 | host: 'https://cdn.testdomain.com', 114 | locale_to_url_keys: {} 115 | }); 116 | 117 | cb(); 118 | }, 119 | 120 | 'generate_css generates CSS': function(test) { 121 | middleware.generate_css("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20130125 Firefox/21.0", "en", ["opensans-regular"], function(err, cssObj) { 122 | test.equal(err, null); 123 | test.ok(cssObj.css.indexOf('https://cdn.testdomain.com/fonts/en/opensans-regular.woff') > -1); 124 | test.done(); 125 | }); 126 | } 127 | }); 128 | 129 | -------------------------------------------------------------------------------- /lib/font-pack-configurator.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * This module creates the required configuration for node-font-face-generator, 7 | * css-responder and font-responder. 8 | * 9 | * The input format can be found at: 10 | * 11 | * The format for node-font-face-generator can be found at: 12 | * https://github.com/shane-tomlinson/connect-fonts/blob/master/README.md 13 | */ 14 | 15 | const path = require("path"), 16 | fs = require("fs"), 17 | util = require("./util"), 18 | css_generator = require("node-font-face-generator"), 19 | InvalidConfigError = css_generator.InvalidConfigError; 20 | 21 | var existsSync = fs.existsSync || path.existsSync; 22 | 23 | function getSortedSubdirectories(root) { 24 | var subdirs = {}; 25 | 26 | // first, make a list of all directories in the root. 27 | var files = fs.readdirSync(root); 28 | files.forEach(function(file) { 29 | var filePath = path.join(root, file); 30 | var stats = fs.statSync(filePath); 31 | 32 | if (stats.isDirectory()) subdirs[file] = true; 33 | }); 34 | 35 | return Object.keys(subdirs).sort(); 36 | } 37 | 38 | function getRoot(root) { 39 | // add a trailing slash if it does not exist. 40 | if (!/\/$/.test(root)) root = root + "/"; 41 | return root; 42 | } 43 | 44 | function addLocalFormats(localFonts, formats) { 45 | if (localFonts) { 46 | localFonts.forEach(function(localName) { 47 | formats.push({ 48 | type: "local", 49 | url: localName 50 | }); 51 | }); 52 | } 53 | } 54 | 55 | function extensionToFontType(extension) { 56 | return ({ 57 | woff2: "woff2", 58 | woff: "woff", 59 | svg: "svg", 60 | eot: "embedded-opentype", 61 | ttf: "truetype", 62 | otf: "opentype" 63 | }[extension]); 64 | } 65 | 66 | function addRemoteFormats(enabledFontTypes, fontName, root, locales, 67 | formats, urlToPaths) { 68 | enabledFontTypes.forEach(function(extension) { 69 | var format = { 70 | type: extensionToFontType(extension), 71 | url: {} 72 | }; 73 | 74 | if (extension === "svg") { 75 | format.id = fontName; 76 | } 77 | 78 | // each locale is in its own subdir. Add the fonts available in each subdir 79 | // to the font list. ensure any locales specified in localeToSubdirs 80 | // are added correctly to the list. 81 | locales.forEach(function(locale) { 82 | var url = format.url[locale] = getFontUrl(locale, fontName, extension); 83 | var fontPath = urlToPaths[url] = getFontPath(root, locale, fontName, extension); 84 | 85 | // Check whether the font file exists. If not, wah wah. 86 | if (!existsSync(fontPath)) throw new InvalidConfigError("Missing font file: " + fontPath); 87 | }); 88 | 89 | formats.push(format); 90 | }); 91 | } 92 | 93 | function getFontUrl(locale, fontName, extension) { 94 | return ["", "fonts", locale, fontName + "." + extension].join("/"); 95 | } 96 | 97 | function getFontPath(root, locale, fontName, extension) { 98 | return path.join(root, locale, fontName + "." + extension); 99 | } 100 | 101 | /** 102 | * Convert from the font set format to the node-font-face-generator, 103 | * css-responder and font-responder formats. 104 | * @throws InvalidConfigError - thrown if a font file is missing 105 | */ 106 | module.exports = function(config) { 107 | var outputConfig = {}; 108 | var fonts = util.getRequired(config, "fonts"); 109 | var root = getRoot(util.getRequired(config, "root")); 110 | var localeToSubdirs = config["locale-to-subdirs"] || {}; 111 | var subdirs = getSortedSubdirectories(root); 112 | 113 | var enabledFontTypes = util.getRequired(config, "enabled-types"); 114 | 115 | for (var fontName in fonts) { 116 | var inputFontConfig = fonts[fontName]; 117 | var outputFontConfig = { 118 | fontFamily: util.getRequired(inputFontConfig, "fontFamily"), 119 | fontStyle: util.getRequired(inputFontConfig, "fontStyle"), 120 | fontWeight: util.getRequired(inputFontConfig, "fontWeight"), 121 | formats: [], 122 | // root is used to calculate the urlToPaths 123 | root: root, 124 | // A map of url to paths on disk. Used to serve font files. 125 | urlToPaths: {}, 126 | // Use the font pack specific locale-to-subdirs as the localeToUrlKeys to 127 | // pass to node-font-face-generator. node-font-face-generator will take 128 | // care of any aliasing or searching for default fonts. 129 | localeToUrlKeys: localeToSubdirs 130 | }; 131 | 132 | addLocalFormats(inputFontConfig.local, outputFontConfig.formats); 133 | addRemoteFormats(enabledFontTypes, fontName, root, subdirs, 134 | outputFontConfig.formats, outputFontConfig.urlToPaths); 135 | 136 | outputConfig[fontName] = outputFontConfig; 137 | } 138 | 139 | return outputConfig; 140 | }; 141 | 142 | 143 | -------------------------------------------------------------------------------- /test/middleware-test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var middleware = require('../lib/middleware'), 6 | nodeunit = require('nodeunit'), 7 | ReqMock = require('./mocks/req-mock'), 8 | ResMock = require('./mocks/res-mock'), 9 | opensans_config = require('./sample-font-packs/fonts-with-default/index'), 10 | shadows_into_light_config 11 | = require('./sample-font-packs/shadows-into-light/index'); 12 | 13 | // Set a 180 day cache. 14 | const MAX_AGE = 1000 * 60 * 60 * 24 * 180; 15 | 16 | var mw; 17 | 18 | function setup(config, done) { 19 | if (!done) { 20 | done = config; 21 | } 22 | 23 | config = config || {}; 24 | 25 | mw = middleware.setup({ 26 | fonts: [ opensans_config ], 27 | allow_origin: "*", 28 | ua: config.ua, 29 | maxage: MAX_AGE, 30 | compress: false 31 | }); 32 | 33 | done && done(); 34 | } 35 | 36 | 37 | function getUA(ua) { 38 | return typeof ua === "undefined" ? 39 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20130125 Firefox/21.0" : ua; 40 | } 41 | 42 | function testCSSServed(test, method, url, ua, cb) { 43 | var req = new ReqMock({ 44 | method: method, 45 | url: url, 46 | "user-agent": getUA(ua) 47 | }); 48 | 49 | var res = new ResMock({ 50 | end: function () { 51 | test.equal(this.getStatusCode(), 200, '200 success response expected'); 52 | cb(this); 53 | } 54 | }); 55 | 56 | mw(req, res, function () { 57 | test.ok(false, "next should have been called"); 58 | }); 59 | } 60 | 61 | function testCSSNotServed(test, method, url, ua) { 62 | var req = new ReqMock({ 63 | method: method, 64 | url: url, 65 | "user-agent": getUA(ua) 66 | }); 67 | 68 | var res = new ResMock({ 69 | send: function () { 70 | test.ok(false, "send should not have been called"); 71 | } 72 | }); 73 | 74 | mw(req, res, function () { 75 | test.ok(true, "non-recognized route calls next"); 76 | test.done(); 77 | }); 78 | } 79 | 80 | exports.no_ua_specified_in_config = nodeunit.testCase({ 81 | setUp: setup, 82 | 83 | 'serve fonts.css for GET /en/opensans-regular/fonts.css, no caching headers set': function(test) { 84 | testCSSServed(test, 'GET', '/en/opensans-regular/fonts.css', undefined, function(res) { 85 | test.ok(res.getData().indexOf("/fonts/en/opensans-regular.woff") > -1); 86 | test.done(); 87 | }); 88 | }, 89 | 90 | 'serve fonts.css for GET /af/opensans-regular/fonts.css - use font alias from locale-to-subdirs': function(test) { 91 | testCSSServed(test, 'GET', '/af/opensans-regular/fonts.css', undefined, function(res) { 92 | test.ok(res.getData().indexOf("/fonts/en/opensans-regular.woff") > -1); 93 | test.done(); 94 | }); 95 | }, 96 | 97 | 'serve fonts.css for GET /es/opensans-regular/fonts.css - use latin fallback font defined by node-font-face-generator': function(test) { 98 | testCSSServed(test, 'GET', '/es/opensans-regular/fonts.css', undefined, function(res) { 99 | test.ok(res.getData().indexOf("/fonts/latin/opensans-regular.woff") > -1); 100 | test.done(); 101 | }); 102 | }, 103 | 104 | 'serve fonts.css for GET /ru/opensans-regular/fonts.css - use default fallback even though font defined by node-font-face-generator - cyrillic dir does not exist': function(test) { 105 | testCSSServed(test, 'GET', '/ru/opensans-regular/fonts.css', undefined, function(res) { 106 | test.ok(res.getData().indexOf("/fonts/default/opensans-regular.woff") > -1); 107 | test.done(); 108 | }); 109 | }, 110 | 111 | 'serve fonts.css for GET /cz/opensans-regular/fonts.css - use default font': function(test) { 112 | testCSSServed(test, 'GET', '/cz/opensans-regular/fonts.css', undefined, function(res) { 113 | test.ok(res.getData().indexOf("/fonts/default/opensans-regular.woff") > -1); 114 | test.done(); 115 | }); 116 | }, 117 | 118 | 'do not serve fonts.css for POST /en/opensans-regular/fonts.css': function (test) { 119 | testCSSNotServed(test, 'POST', '/en/opensans-regular/fonts.css'); 120 | }, 121 | 122 | 'do not serve fonts.css for GET /en/Unknown/fonts.css': function (test) { 123 | testCSSNotServed(test, 'GET', '/en/Unknown/fonts.css'); 124 | }, 125 | 126 | 'do not serve fonts for GET /random/route': function (test) { 127 | testCSSNotServed(test, 'GET', '/random/route'); 128 | }, 129 | 130 | 'do not serve fonts if headers["user-agent"] is not specified': function (test) { 131 | testCSSNotServed(test, 'GET', '/en/opensans-regular/fonts.css', null); 132 | } 133 | }); 134 | 135 | exports.specify_ua_in_config = nodeunit.testCase({ 136 | setUp: setup.bind(null, { ua: 'all' }), 137 | 138 | 'serve fonts even if headers["user-agent"] is not specified in headers': function(test) { 139 | testCSSServed(test, 'GET', '/en/opensans-regular/fonts.css', undefined, function(res) { 140 | var data = res.getData(); 141 | test.ok(data.indexOf("/fonts/en/opensans-regular.woff") > -1); 142 | test.ok(data.indexOf("/fonts/en/opensans-regular.eot") > -1); 143 | test.ok(data.indexOf("/fonts/en/opensans-regular.svg#opensans-regular") > -1); 144 | test.ok(data.indexOf("/fonts/en/opensans-regular.ttf") > -1); 145 | test.ok(data.indexOf("/fonts/en/opensans-regular.otf") > -1); 146 | test.done(); 147 | }); 148 | } 149 | }); 150 | 151 | exports.register_fontpack_after_setup = nodeunit.testCase({ 152 | setUp: setup.bind(null, { ua: 'all' }), 153 | 154 | 'add a new font, see if it is served': function (test) { 155 | mw.registerFontPack(shadows_into_light_config, function (err) { 156 | test.ok(!err); 157 | 158 | testCSSServed(test, 'GET', '/en/shadows-into-light/fonts.css', undefined, function () { 159 | test.done(); 160 | }); 161 | }); 162 | } 163 | }); 164 | 165 | -------------------------------------------------------------------------------- /lib/css-responder.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 6 | /** 7 | * This file takes care of responding to font CSS requests. 8 | * 9 | * URLs of the following form are searched for: 10 | * 11 | * /:lang/:comma,separated,list,of,fonts/fonts.css 12 | * 13 | * If the URL matches the form, CSS for the font set is searched for in a local 14 | * cache. If there is a cache miss, CSS is generated using the 15 | * node-font-face-generator and saved to the cache. 16 | */ 17 | 18 | 19 | const util = require('./util'); 20 | const respond = require('./respond'); 21 | const css_generator = require('node-font-face-generator'); 22 | const InvalidFontError = css_generator.InvalidFontError; 23 | const fs = require('fs'); 24 | const path = require('path'); 25 | const tmp = require('tmp'); 26 | const crypto = require('crypto'); 27 | 28 | tmp.setGracefulCleanup(); 29 | 30 | /** 31 | * setup - must be called before generate_css or font_css_responder. Sets up 32 | * node-font-face-generator so that it can generate fonts. 33 | * @param {object} options 34 | * @param {object} fonts 35 | * @param {object} locale_to_url_keys 36 | * @param {number} options.maxage - Provide a max-age in milliseconds for http 37 | * caching, defaults to 0. 38 | * @param {boolean} options.compress - Whether to comprss the result. 39 | * @param {string} options.host - host where font files are located. 40 | */ 41 | exports.setup = function (options) { 42 | var config = options; 43 | var maxAge = options.maxage || 0; 44 | var compress = options.compress || false; 45 | var cssCache = {}; 46 | var cssTmpPath; 47 | var fonts = options.fonts; 48 | var localeToUrlKeys = options.locale_to_url_keys; 49 | 50 | util.checkRequired(options, "fonts"); 51 | util.checkRequired(options, "locale_to_url_keys"); 52 | 53 | css_generator.setup({ 54 | fonts: fonts, 55 | localeToUrlKeys: localeToUrlKeys, 56 | host: options.host 57 | }); 58 | 59 | /* 60 | * CSS responder. Looks for URLs of the form: 61 | * /:lang/:comma,separated,list,of,fonts/fonts.css 62 | * 63 | * @method font_css_responder 64 | */ 65 | var middleware = function (req, res, next) { 66 | if (req.method !== "GET") return next(); 67 | 68 | // Use a non-capturing regexp for the locale portion. locale can be left 69 | // off and the default locale will be used. 70 | var match = /(?:\/([^\/]+))?\/([^\/]+)\/fonts\.css$/.exec(req.url); 71 | if (! match) return next(); 72 | 73 | var locale = match[1] || "default"; 74 | var fonts = match[2].split(','); 75 | 76 | // the configured user agent takes precedence over the UA in the header. 77 | var ua = config.ua || req.headers['user-agent']; 78 | 79 | if (! (ua && fonts)) return next(); 80 | 81 | return middleware.get_css(ua, locale, fonts, function (err, cssObj) { 82 | // ignore InvalidFontErrors 83 | if (err instanceof InvalidFontError) return next(); 84 | if (err) return next(err); 85 | 86 | res.on('header', function() { 87 | respond.setCacheControlHeaders(res, maxAge); 88 | }); 89 | 90 | respond.respond(req, res, cssObj.cssPath, compress); 91 | }); 92 | }; 93 | 94 | 95 | /** 96 | * Register a font 97 | * @method registerFont 98 | * @param {string} fontName 99 | * @param {object} packConfig 100 | * @param {function} [done] 101 | */ 102 | middleware.registerFont = function (fontName, fontConfig, done) { 103 | fonts[fontName] = fontConfig; 104 | 105 | // reinitialize the CSS generator, there are no bad side effects. 106 | // Note, registering a new font does *NOT* reset the cssCache. 107 | css_generator.setup({ 108 | fonts: fonts, 109 | localeToUrlKeys: localeToUrlKeys 110 | }, done); 111 | }; 112 | 113 | /** 114 | * Generate CSS for a given user-agent, locale, and set of fonts. 115 | * @method generate_css 116 | * @param {string} ua - user agent string to generate fonts for. 'all' generates 117 | * CSS for all user agents. 118 | * @param {string} locale - locale to generate fonts for. 119 | * @param {Array of strings} fonts - list of fonts to get CSS for. 120 | * @param {function} done - called with two parameters when complete, err and 121 | * css. 122 | */ 123 | middleware.generate_css = function (ua, locale, fonts, done) { 124 | css_generator.get_font_css({ 125 | ua: ua, 126 | locale: locale, 127 | fonts: fonts 128 | }, function (err, cssStr) { 129 | if (err) return done(err, null); 130 | 131 | done(null, { 132 | css: cssStr 133 | }); 134 | }); 135 | }; 136 | 137 | /** 138 | * Get font css 139 | * @method get_css 140 | * @param {string} ua - user agent string to generate fonts for. 'all' generates 141 | * CSS for all user agents. 142 | * @param {string} locale - locale to generate fonts for. 143 | * @param {Array of strings} fonts - list of fonts to get CSS for. 144 | * @param {function} done - called with two parameters when complete, err and 145 | * css. 146 | */ 147 | middleware.get_css = function (ua, locale, fonts, done) { 148 | var cacheKey = getCacheKey(ua, locale, fonts); 149 | var cacheHit = cssCache[cacheKey]; 150 | 151 | if (cacheHit) return done(null, cacheHit); 152 | 153 | // no cache hit, go generate the CSS. 154 | middleware.generate_css(ua, locale, fonts, function (err, cssObj) { 155 | if (err) return done(err, null); 156 | 157 | // save CSS to disk to serve up with send 158 | prepareTmpPath(function (err, cssTmpPath) { 159 | if (err) return done(err, null); 160 | 161 | var cssPath = path.join(cssTmpPath, cacheKey + ".css"); 162 | fs.writeFile(cssPath, cssObj.css, 'utf8', function (err) { 163 | if (err) return done(err, null); 164 | 165 | // save to cache. 166 | cssObj.cssPath = cssPath; 167 | cssCache[cacheKey] = cssObj; 168 | done(null, cssObj); 169 | }); 170 | }); 171 | }); 172 | }; 173 | 174 | return middleware; 175 | 176 | function prepareTmpPath(done) { 177 | if (cssTmpPath) { 178 | return done(null, cssTmpPath); 179 | } 180 | 181 | tmp.dir(function (err, _cssTmpPath) { 182 | if (err) return done(err); 183 | 184 | cssTmpPath = _cssTmpPath; 185 | done(null, cssTmpPath); 186 | }); 187 | } 188 | 189 | }; 190 | 191 | 192 | function getCacheKey(ua, locale, fonts) { 193 | // Hash the cache key because the names can become awefully long and unruly. 194 | // So long in fact that they overrun the max filename length. 195 | var cacheKey = crypto.createHash('md5').update(ua + '-' + locale + '-' + fonts, 'utf8').digest('hex'); 196 | return cacheKey; 197 | } 198 | 199 | 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connect-fonts 2 | 3 | Connect/Express font serving middleware. Why? Because Google's font CDN is slow and slow page loads cause users to leave your site. 4 | 5 | The middleware looks for requests (expressed in Express terminology): 6 | ``` 7 | /:font-list/fonts.css 8 | ``` 9 | 10 | An example of a match is: 11 | ``` 12 | /opensans-regular,opensans-italics/fonts.css 13 | ``` 14 | 15 | When this route is matched, connect-fonts will generate a CSS response with @font-face declarations that are tailored to the user's browser. 16 | 17 | ## Usage 18 | 1. Include connect-fonts in a node module. 19 | ```js 20 | const font_middleware = require("connect-fonts"); 21 | ``` 22 | 23 | 2. Include the font packs that you want to serve. 24 | ```js 25 | const opensans = require("connect-fonts-opensans"); 26 | ``` 27 | 28 | 3. Add a middleware by calling the `setup` function. 29 | ```js 30 | app.use(font_middleware.setup({ 31 | fonts: [ opensans ], 32 | allow_origin: "https://exampledomain.com", 33 | ua: "all", 34 | maxage: 180 * 24 * 60 * 60 * 1000 // 180 days 35 | })); 36 | ``` 37 | `fonts` - array of font packs. 38 | `allow_origin` - optional - origin to set in the `Access-Control-Allow-Origin` header. Defaults to `undefined`. 39 | `ua` - optional - force a user-agent. "all" means serve up all font types to all users. If not specified, the user's user-agent header will be used to send the user only the fonts that their user-agent support. Defaults to `all`. 40 | `maxage` - optional - provide a max-age in milliseconds for http caching. Defaults to `0`. 41 | `compress` - optional - Whether to compress the CSS/font output. Defaults to `false`. 42 | 43 | 4. Add a link tag to include the font CSS. 44 | To serve a default, non-locale specific font, include a CSS link that contains the name of the font: 45 | ```html 46 | 47 | ``` 48 | 49 | 5. Set your CSS up to use the new font by using the correct font-family. 50 | ``` 51 | body { 52 | font-family: 'Open Sans', 'sans-serif', 'serif'; 53 | } 54 | ``` 55 | 56 | ## Advanced Usage 57 | 58 | ### Fonts located on another domain (CDN) 59 | It is possible to specify a host where fonts are located. This is useful if font files are located on another domain. 60 | ```js 61 | app.use(font_middleware.setup({ 62 | fonts: [ opensans ], 63 | host: "https://cdn.exampledomain.com", 64 | allow_origin: "https://exampledomain.com", 65 | ua: "all", 66 | maxage: 180 * 24 * 60 * 60 * 1000 // 180 days 67 | })); 68 | ``` 69 | 70 | 71 | 72 | ### Locale optimised fonts 73 | If a font pack contains locale optimised fonts, these can be requested by prepending 74 | the locale name before the font list in the fonts.css request. 75 | ```html 76 | 77 | ``` 78 | `scripts/subset` from [connect-fonts-tools](https://github.com/shane-tomlinson/connect-fonts-tools) can be used to create locale-optimised subsets. 79 | 80 | 81 | ### Programatically generate CSS for use in build steps 82 | One of the easiest ways to speed up your site is to minimize the number of resources that are requested. The @font-face CSS provided by fonts.css can be fetched programatically and concatinated with other site CSS during a build step. 83 | ```js 84 | var fontMiddleware = connect_fonts.setup(...); 85 | 86 | // `ua` - user agent. Use 'all' for a CSS bundle that is compatible with all browsers. 87 | // `lang` - language. generate_css can be called once for each served language, or 88 | // "default" can be specified 89 | // `fonts` - array of font names - e.g. ["opensans-regular", "opensans-italics"] 90 | fontMiddleware.generate_css(ua, lang, fonts, function(err, css) { 91 | var css_output_path = path.join(output_dir, dep); 92 | var css_output_dir = path.dirname(css_output_path); 93 | 94 | // create any missing directories. 95 | mkdirp.sync(css_output_dir); 96 | 97 | // finally, write out the file. 98 | fs.writeFileSync(css_output_path, css.css, "utf8"); 99 | }); 100 | ``` 101 | 102 | ### Direct access to font files 103 | Once connect fonts setup function is called, a map of URLs=>paths can be retreived using fontMiddleware.urlToPaths. This information can be used in a build step for tools like [connect-cachify](https://github.com/mozilla/connect-cachify/) that need access to the font file to create an caching hash. 104 | 105 | 106 | ## Create a Font Pack 107 | A font pack is an npm module like any other node library. Creating a new font pack is similar to creating any npm module. 108 | 109 | 1. Install [connect-fonts-tools](https://github.com/shane-tomlinson/connect-fonts-tools) and run its `scripts/setup` utility. 110 | ```bash 111 | npm install connect-fonts-tools 112 | cd node_modules/connect-fonts-tools 113 | ./scripts/setup 114 | ``` 115 | 116 | 2. Create a font pack target directory 117 | ```bash 118 | mkdir 119 | ``` 120 | 121 | 3. Call ``scripts/create_fontpack`` from connect-font-tools with the source directory, the target directory, and the pack name. 122 | ```bash 123 | connect-fonts-tools/scripts/create_fontpack --pn --sp --tp 124 | ``` 125 | If the font pack is for public use, specify the additional parameters to be placed inside the font pack's package.json and README.md files. 126 | ```bash 127 | connect-fonts-tools/scripts/create_fontpack --pn --ph --pr --pb --sp --tp 128 | ``` 129 | 130 | 4. Check your font pack. 131 | ``script/check_font_pack.js`` is a basic font pack linter. It will check whether pack configuration is sane and if all expected font files are available. To use it, call ``check_font_pack.js`` with the absolute path to the font pack's configuration file. 132 | ```bash 133 | script/check_font_pack.js ~/development/connect-fonts-opensans/index.js 134 | ``` 135 | 136 | 5. If the font pack is for public use, publish it to the npm repository 137 | ```bash 138 | cd 139 | npm publish 140 | ``` 141 | 142 | 6. Install the pack using npm into your project: 143 | ```bash 144 | npm install 145 | ``` 146 | If the font pack is not to be published to the npm repository, it can be installed to another local project directory: 147 | ```bash 148 | cd 149 | npm install 150 | ``` 151 | 152 | ## Author: 153 | * Shane Tomlinson 154 | * shane@shanetomlinson.com 155 | * stomlinson@mozilla.com 156 | * set117@yahoo.com 157 | * https://shanetomlinson.com 158 | * https://github.com/shane-tomlinson 159 | * @shane_tomlinson 160 | 161 | ## Get involved: 162 | MOAR font packs! See [connect-fonts-tools](https://github.com/shane-tomlinson/connect-fonts-tools) for tools to make this easy. [connect-fonts-opensans](https://github.com/shane-tomlinson/connect-fonts-opensans) is an example of a finished font pack. 163 | 164 | Any updates to connect-fonts are appreciated. All submissions will be reviewed and considered for merge. 165 | 166 | ## License: 167 | This software is available under version 2.0 of the MPL: 168 | 169 | https://www.mozilla.org/MPL/ 170 | 171 | 172 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/en/opensans-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Copyright : Digitized data copyright 20102011 Google Corporation 7 | Foundry : Ascender Corporation 8 | Foundry URL : httpwwwascendercorpcom 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-missing-font/fonts/default/opensans-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Copyright : Digitized data copyright 20102011 Google Corporation 7 | Foundry : Ascender Corporation 8 | Foundry URL : httpwwwascendercorpcom 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/default/opensans-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Copyright : Digitized data copyright 20102011 Google Corporation 7 | Foundry : Ascender Corporation 8 | Foundry URL : httpwwwascendercorpcom 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-with-default/fonts/latin/opensans-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Copyright : Digitized data copyright 20102011 Google Corporation 7 | Foundry : Ascender Corporation 8 | Foundry URL : httpwwwascendercorpcom 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /test/sample-font-packs/fonts-without-default/fonts/en/opensans-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Copyright : Digitized data copyright 20102011 Google Corporation 7 | Foundry : Ascender Corporation 8 | Foundry URL : httpwwwascendercorpcom 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | --------------------------------------------------------------------------------