├── .editorconfig ├── .github ├── bump.yml ├── labels.yml └── workflows │ └── release.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── biome.json ├── example ├── index.html ├── package.json ├── postcss.config.js ├── src │ └── style.css └── tailwind.config.js ├── inter.json ├── package.json ├── src ├── index.js └── utils.js └── update.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | 8 | [*.{html,css,js}] 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.github/bump.yml: -------------------------------------------------------------------------------- 1 | release: 2 | title-prefix: 'v' 3 | initial-version: '1.0.0' 4 | tag-prefix: 'v' 5 | body-title: "What's Changed" 6 | body-when-empty-changes: 'No changes' 7 | sort-by: 'note' 8 | sort-direction: 'ascending' 9 | commit-note-replacers: 10 | - replace-prefix: 'BREAKING CHANGE: ' 11 | new-prefix: '' 12 | - replace-prefix: 'breaking: ' 13 | new-prefix: '' 14 | - replace-prefix: 'feat: ' 15 | new-prefix: '' 16 | - replace-prefix: 'feat!: ' 17 | new-prefix: '' 18 | - replace-prefix: 'refactor: ' 19 | new-prefix: '' 20 | - replace-prefix: 'fix: ' 21 | new-prefix: '' 22 | - replace-prefix: 'docs: ' 23 | new-prefix: '' 24 | - replace-prefix: 'chore: ' 25 | new-prefix: '' 26 | - replace-prefix: 'deps: ' 27 | new-prefix: '' 28 | - replace-prefix: 'build(deps): ' 29 | new-prefix: '' 30 | - replace-prefix: 'build: ' 31 | new-prefix: '' 32 | branch: 33 | version-branch-prefix: 'v' 34 | bump-version-commit-prefix: 'v' 35 | categories: 36 | - title: 'Breaking Changes!' 37 | labels: 38 | - 'BreakingChange' 39 | commits: 40 | - 'BREAKING CHANGE:' 41 | - 'breaking:' 42 | - 'feat!:' 43 | changes-prefix: ':warning: ' 44 | - title: 'Changes' 45 | labels: 46 | - 'Feature' 47 | commits: 48 | - 'feat:' 49 | changes-prefix: ':gift: ' 50 | - title: 'Minor Changes' 51 | labels: 52 | - 'Refactor' 53 | commits: 54 | - 'refactor:' 55 | - 'chore:' 56 | - 'build:' 57 | changes-prefix: ':hammer: ' 58 | - title: 'Bug Fixes' 59 | labels: 60 | - 'Bug' 61 | commits: 62 | - 'fix:' 63 | changes-prefix: ':ambulance: ' 64 | - title: 'Changes' 65 | labels: 66 | - 'Documentation' 67 | commits: 68 | - 'docs:' 69 | changes-prefix: ':blue_book: ' 70 | - title: 'Dependency Updates' 71 | labels: 72 | - 'Dependencies' 73 | skip-label: 'Development' 74 | commits: 75 | - 'deps:' 76 | - 'build(deps):' 77 | changes-prefix: ':green_book: ' 78 | bump: 79 | default: 'patch' 80 | major: 81 | labels: 82 | - 'BreakingChange' 83 | commits: 84 | - 'BREAKING CHANGE:' 85 | - 'breaking:' 86 | - 'feat!:' 87 | minor: 88 | labels: 89 | - 'Feature' 90 | commits: 91 | - 'feat:' 92 | files: 93 | - file-path: 'package.json' 94 | line: 4 95 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: Action 2 | color: 92efb4 3 | description: '' 4 | - name: BreakingChange 5 | color: db3b4d 6 | description: '' 7 | - name: Bug 8 | color: d73a4a 9 | description: Something isn't working 10 | - name: Dependencies 11 | color: abc43e 12 | description: '' 13 | - name: Development 14 | color: fef2c0 15 | description: '' 16 | - name: Documentation 17 | color: 1d76db 18 | description: '' 19 | - name: Feature 20 | color: a2eeef 21 | description: New feature or request 22 | - name: Maintenance 23 | color: 4d6eb7 24 | description: '' 25 | - name: Question 26 | color: d876e3 27 | description: Further information is requested 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build, Release and Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | bump: 7 | description: "bump type, major or minor or patch or empty string" 8 | default: "" 9 | required: false 10 | dry_run: 11 | description: "dry run, true or false" 12 | default: "false" 13 | required: true 14 | draft: 15 | description: "draft, true or false" 16 | default: "false" 17 | required: true 18 | pre_release: 19 | description: "pre release, true or false" 20 | default: "false" 21 | required: true 22 | 23 | jobs: 24 | release: 25 | name: Release & Publish 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: Cache node_modules 33 | id: cache-modules 34 | uses: actions/cache@v4 35 | with: 36 | path: node_modules 37 | key: 18.x-${{ runner.OS }}-build-${{ hashFiles('package.json') }} 38 | 39 | - name: Build 40 | if: steps.cache-modules.outputs.cache-hit != 'true' 41 | run: npm install 42 | 43 | - uses: MeilCli/bump-release-action@v1 44 | with: 45 | config_path: ".github/bump.yml" 46 | bump: ${{ github.event.inputs.bump }} 47 | dry_run: ${{ github.event.inputs.dry_run }} 48 | draft: ${{ github.event.inputs.draft }} 49 | pre_release: ${{ github.event.inputs.pre_release }} 50 | 51 | - name: "Publish to NPM" 52 | if: github.event.inputs.dry_run != 'true' 53 | uses: JS-DevTools/npm-publish@v1 54 | with: 55 | token: ${{ secrets.NPM_AUTH_TOKEN }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .temp 3 | fonts 4 | dist 5 | node_modules 6 | package-lock.json 7 | yarn.lock 8 | pnpm-lock.yaml 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2019` `Yuri Sementsov` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailwind Inter Plugin 2 | 3 | A TailwindCSS plugin that seamlessly integrates the beautiful Inter typeface by Rasmus Andersson ([@rsms](https://twitter.com/rsms)) into your projects. This plugin provides a complete solution for using Inter font with proper metrics and advanced OpenType features. 4 | 5 | ## Features 6 | 7 | - 🎯 Adds `.font-inter` utility class for easy font family application 8 | - ⚙️ Configurable OpenType feature settings (ligatures, numerics, case features, etc.) 9 | - 🔄 Automatic `@font-face` injection from [Inter's CDN](https://rsms.me/inter/inter.css) 10 | - 🎨 Works seamlessly with Tailwind's fontSize configuration 11 | - 🚀 Zero configuration required to get started 12 | 13 | ## Installation 14 | 15 | ```sh 16 | # with npm 17 | npm install --save-dev tailwindcss-font-inter 18 | 19 | # or with yarn 20 | yarn add -D tailwindcss-font-inter 21 | ``` 22 | 23 | ## Quick Start 24 | 25 | Add the plugin to your `tailwind.config.js`: 26 | 27 | ```js 28 | // tailwind.config.js 29 | module.exports = { 30 | theme: {}, 31 | plugins: [require('tailwindcss-font-inter')] 32 | } 33 | ``` 34 | 35 | Now you can put `.font-inter` class to apply the font (by default `@font-face` definitions will be added to your CSS). 36 | 37 | ```html 38 | 39 |

Beautiful Typography

40 |

Your content with the full power of the Inter font features.

41 | 42 | ``` 43 | 44 | ## Configuration 45 | 46 | ### Plugin Options 47 | 48 | Customize the plugin behavior with these options: 49 | 50 | ```js 51 | // tailwind.config.js 52 | module.exports = { 53 | plugins: [ 54 | require('tailwindcss-font-inter')({ 55 | importFontFace: true, // Set to false if you want to import Inter from elsewhere 56 | }) 57 | ] 58 | } 59 | ``` 60 | 61 | ### Font Features 62 | 63 | Define custom sets of OpenType features: 64 | 65 | ```js 66 | // tailwind.config.js 67 | module.exports = { 68 | theme: { 69 | extend: { 70 | interFontFeatures: { 71 | numeric: ['tnum', 'salt', 'ss02'], // Tabular numbers with stylistic alternates 72 | case: ['case'], // Case-sensitive forms 73 | fractions: ['frac'], // Enable fractions 74 | 'stylistic-one': ['ss01'] // Stylistic Set 1 75 | } 76 | } 77 | }, 78 | plugins: [require('tailwindcss-font-inter')] 79 | } 80 | ``` 81 | 82 | This generates utility classes like: 83 | 84 | ```css 85 | /* Default features */ 86 | .font-feature-default { font-feature-settings: 'calt' 1, 'kern' 1; } 87 | .font-feature-normal { font-feature-settings: normal; } 88 | 89 | /* Custom features */ 90 | .font-feature-numeric { font-feature-settings: 'tnum' 1, 'salt' 1, 'ss02' 1; } 91 | .font-feature-case { font-feature-settings: 'case' 1; } 92 | .font-feature-fractions { font-feature-settings: 'frac' 1; } 93 | .font-feature-stylistic-one { font-feature-settings: 'ss01' 1; } 94 | ``` 95 | 96 | ## Manual Font Import 97 | 98 | If you set `importFontFace: false`, you'll need to import Inter yourself. You can use Google Fonts: 99 | 100 | ```html 101 | 102 | ``` 103 | 104 | Or import directly from Inter's CDN: 105 | 106 | ```css 107 | @import url('https://rsms.me/inter/inter.css'); 108 | ``` 109 | 110 | ## Browser Support 111 | 112 | Inter works in all modern browsers. The font-feature-settings are supported in: 113 | - Chrome 48+ 114 | - Firefox 34+ 115 | - Safari 9.1+ 116 | - Edge 15+ 117 | 118 | ## Credits 119 | 120 | This plugin is inspired by [tailwind-plugin-inter-font](https://github.com/imsus/tailwind-plugin-font-inter) by Imam Susanto ([@imsus](https://github.com/imsus)). 121 | 122 | ## License 123 | 124 | [MIT](LICENSE.md) 125 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Inter Font Plugin Example 7 | 8 | 9 | 10 |
11 |
12 |

Inter Font Plugin Demo

13 |

Showcasing different font features and sizes

14 |
15 | 16 |
17 |
18 |

Text Sizes

19 |
20 |

Extra Small Text (xs)

21 |

Small Text (sm)

22 |

Base Text (base)

23 |

Large Text (lg)

24 |

Extra Large Text (xl)

25 |

2XL Text (2xl)

26 |

3XL Text (3xl)

27 |

4XL Text (4xl)

28 |
29 |
30 | 31 |
32 |

Font Features

33 |
34 |

Normal Features

35 |

Default Features (calt, kern)

36 |

Numeric Features (tnum, salt, ss02)

37 |

Case Features

38 |

Fraction Features (1/2 3/4 5/6)

39 |

Stylistic Set 01

40 |
41 |
42 | 43 |
44 |

Letter Spacing

45 |
46 |

Tighter Letter Spacing

47 |

Tight Letter Spacing

48 |

Normal Letter Spacing

49 |

Wide Letter Spacing

50 |

Wider Letter Spacing

51 |

Widest Letter Spacing

52 |
53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-font-inter-example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@tailwindcss/postcss": "^4.0.2" 12 | }, 13 | "devDependencies": { 14 | "autoprefixer": "^10.4.16", 15 | "postcss": "^8.4.31", 16 | "vite": "^5.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /example/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /example/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | interFontFeatures: { 10 | numeric: ['tnum', 'salt', 'ss02'], 11 | case: ['case'], 12 | fractions: ['frac'], 13 | 'stylistic-one': ['ss01'], 14 | } 15 | }, 16 | }, 17 | plugins: [require('../src/index.js')], 18 | } 19 | -------------------------------------------------------------------------------- /inter.json: -------------------------------------------------------------------------------- 1 | {"version":"4.001;git-9221beed3","availableFeatures":["aalt","calt","case","ccmp","cpsp","cv01","cv02","cv03","cv04","cv05","cv06","cv07","cv08","cv09","cv10","cv11","cv12","cv13","cv14","dlig","dnom","frac","kern","mark","mkmk","numr","ordn","pnum","salt","sinf","ss01","ss02","ss03","ss04","ss05","ss06","ss07","ss08","subs","sups","tnum","zero"],"base":{"@font-face":[{"font-family":"InterVariable","font-style":"normal","font-weight":"100 900","font-display":"swap","src":"url('https://rsms.me/inter/font-files/InterVariable.woff2?v=4.1') format('woff2')"},{"font-family":"InterVariable","font-style":"italic","font-weight":"100 900","font-display":"swap","src":"url('https://rsms.me/inter/font-files/InterVariable-Italic.woff2?v=4.1') format('woff2')"},{"font-family":"Inter","font-style":"normal","font-weight":"100","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-Thin.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"100","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-ThinItalic.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"normal","font-weight":"200","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-ExtraLight.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"200","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-ExtraLightItalic.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"normal","font-weight":"300","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-Light.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"300","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-LightItalic.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"normal","font-weight":"400","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-Regular.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"400","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-Italic.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"normal","font-weight":"500","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-Medium.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"500","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-MediumItalic.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"normal","font-weight":"600","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"600","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-SemiBoldItalic.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"normal","font-weight":"700","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-Bold.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"700","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-BoldItalic.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"normal","font-weight":"800","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-ExtraBold.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"800","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-ExtraBoldItalic.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"normal","font-weight":"900","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-Black.woff2?v=4.1\") format(\"woff2\")"},{"font-family":"Inter","font-style":"italic","font-weight":"900","font-display":"swap","src":"url(\"https://rsms.me/inter/font-files/Inter-BlackItalic.woff2?v=4.1\") format(\"woff2\")"}]},"utilities":{".font-inter":{"font-family":"Inter, sans-serif"},"@supports(font-variation-settings: normal)":{".font-inter":{"font-family":"InterVariable, sans-serif"}}}} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-font-inter", 3 | "description": "TailwindCSS plugin for pleasant Inter Typeface integration", 4 | "version": "4.0.0", 5 | "license": "MIT", 6 | "main": "src/index.js", 7 | "author": "Yuri Sementsov ", 8 | "files": [ 9 | "src", 10 | "inter.json" 11 | ], 12 | "scripts": { 13 | "update": "node ./update.js", 14 | "lint": "biome lint .", 15 | "lint:fix": "biome check . --write" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/semencov/tailwindcss-font-inter.git" 20 | }, 21 | "keywords": [ 22 | "tailwindcss", 23 | "tailwindcss-plugin", 24 | "tailwind-plugin", 25 | "inter ui", 26 | "inter", 27 | "font", 28 | "font-feature-settings" 29 | ], 30 | "bugs": { 31 | "email": "hello@smcv.dev", 32 | "url": "https://github.com/semencov/tailwindcss-font-inter/issues" 33 | }, 34 | "homepage": "https://github.com/semencov/tailwindcss-font-inter", 35 | "peerDependencies": { 36 | "tailwindcss": "^3.0.0 || ^4.0.0" 37 | }, 38 | "devDependencies": { 39 | "@biomejs/biome": "^1.9.4", 40 | "fontkit": "^1.9.0", 41 | "node-fetch": "^2.7.0", 42 | "postcss": "^8.5.1" 43 | }, 44 | "engines": { 45 | "node": ">=18.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require("tailwindcss/plugin"); 2 | const Inter = require("../inter.json"); 3 | const { 4 | unquote, 5 | defaults, 6 | isString, 7 | isBoolean, 8 | isArrayLike, 9 | isPlainObject, 10 | mapObject, 11 | filterObject 12 | } = require("./utils"); 13 | 14 | function normalizeEntry(key, value) { 15 | let normalizedValue = isBoolean(value) ? `${1 * value}` : `${value}`; 16 | normalizedValue = 17 | normalizedValue !== "1" && normalizedValue !== "undefined" 18 | ? normalizedValue 19 | : "1"; 20 | 21 | return [unquote(key), normalizedValue]; 22 | } 23 | 24 | function generateFeatures(inputFeatures, available) { 25 | let features; 26 | 27 | if (isPlainObject(inputFeatures)) { 28 | features = mapObject(inputFeatures, (key, value = "1") => 29 | normalizeEntry(key, value), 30 | ); 31 | } else { 32 | if (isString(inputFeatures)) { 33 | features = Object.fromEntries( 34 | inputFeatures.split(",").map((f) => f.trim().split(" ")), 35 | ); 36 | } else { 37 | features = Object.fromEntries( 38 | inputFeatures.map((feature) => { 39 | let key; 40 | let value; 41 | 42 | if (isString(feature)) { 43 | [key, value = "1"] = feature.replace(/\s\s+/g, " ").split(" ", 2); 44 | } else if (isArrayLike(feature)) { 45 | [key, value = "1"] = feature; 46 | } else if (isPlainObject(feature)) { 47 | [key, value = "1"] = Object.entries(feature)[0]; 48 | } 49 | 50 | return normalizeEntry(key, value); 51 | }), 52 | ); 53 | } 54 | } 55 | 56 | features = filterObject(features, (key) => available.includes(key)); 57 | 58 | return Object.entries(features) 59 | .map(([key, value]) => `"${key}" ${value}`) 60 | .filter((val) => !!val) 61 | .sort() 62 | .join(", ") 63 | .trim(); 64 | } 65 | 66 | module.exports = plugin.withOptions((options = {}) => { 67 | const config = defaults(options, { 68 | importFontFace: true, 69 | }); 70 | 71 | return ({ addBase, addUtilities, variants, e, theme }) => { 72 | const { availableFeatures, utilities, base } = Inter; 73 | 74 | const defaultFontFeaturesTheme = { default: ["calt", "kern"] }; 75 | const defaultFontSizeVariants = ["responsive"]; 76 | 77 | const fontSizeTheme = theme("fontSize"); 78 | const fontSizeVariants = variants("fontSize", defaultFontSizeVariants); 79 | const fontFeaturesTheme = theme("interFontFeatures", defaultFontFeaturesTheme); 80 | const fontFeatures = defaults(fontFeaturesTheme, defaultFontFeaturesTheme); 81 | const fontFeaturesVariants = variants("interFontFeatures", defaultFontSizeVariants); 82 | const baseStyles = { ...(config.importFontFace ? base : {}) }; 83 | 84 | const fontSizeStyles = (fontSize) => { 85 | const [size, opts = {}] = isArrayLike(fontSize) ? fontSize : [fontSize]; 86 | 87 | return { 88 | ...opts, 89 | fontSize: size, 90 | }; 91 | }; 92 | 93 | const fontFeatureStyles = (value) => { 94 | return value.length 95 | ? { 96 | fontFeatureSettings: Array.isArray(value) 97 | ? value.join(", ") 98 | : value, 99 | } 100 | : null; 101 | }; 102 | 103 | const fontFeatureUtilities = { 104 | ...{ 105 | ".font-inter .font-feature-normal, .font-inter.font-feature-normal": { 106 | ...fontFeatureStyles("normal"), 107 | }, 108 | }, 109 | ...mapObject(fontFeatures, (modifier, value) => { 110 | const features = generateFeatures(value, availableFeatures); 111 | 112 | return [ 113 | `.font-inter .${e(`font-feature-${modifier}`)},.font-inter.${e(`font-feature-${modifier}`)}`, 114 | { 115 | ...fontFeatureStyles(features), 116 | }, 117 | ]; 118 | }), 119 | }; 120 | 121 | const fontSizeUtilities = Object.entries(fontSizeTheme).reduce( 122 | (result, [sizeModifier, sizeValue]) => { 123 | const { a, b, c } = config; 124 | 125 | const entries = [ 126 | [ 127 | `.font-inter .${e(`text-${sizeModifier}`)}, .font-inter.${e(`text-${sizeModifier}`)}`, 128 | { 129 | ...fontSizeStyles(sizeValue, { a, b, c }), 130 | }, 131 | ], 132 | ]; 133 | 134 | return Object.assign(result, Object.fromEntries(entries)); 135 | }, 136 | {}, 137 | ); 138 | 139 | // Add @font-face if importFontFace: true 140 | // see https://rsms.me/inter/inter.css 141 | addBase(baseStyles); 142 | 143 | // Add .font-inter 144 | addUtilities(utilities); 145 | 146 | // Add .font-feature-{modifier} utility classes 147 | addUtilities(fontFeatureUtilities, fontFeaturesVariants); 148 | 149 | // Add .font-inter.text-{size} utility classes 150 | addUtilities(fontSizeUtilities, fontSizeVariants); 151 | }; 152 | }); 153 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function isBoolean(val) { 2 | return typeof val === "boolean"; 3 | } 4 | 5 | function isString(val) { 6 | return typeof val === "string"; 7 | } 8 | 9 | function isNumeric(val) { 10 | return !Number.isNaN(val) && !Number.isNaN(Number.parseFloat(val)); 11 | } 12 | 13 | function isArrayLike(obj) { 14 | return obj && 15 | obj !== null && 16 | !isString(obj) && 17 | typeof obj[Symbol.iterator] === "function"; 18 | } 19 | 20 | function isPlainObject(val) { 21 | return !!val && typeof val === "object" && val.constructor === Object; 22 | } 23 | 24 | function mapObject(obj, cb) { 25 | return Object.fromEntries( 26 | (Array.isArray(obj) ? obj : Object.entries(obj)).map((val) => cb(...val)) 27 | ); 28 | } 29 | 30 | function filterObject(obj, cb) { 31 | return Object.fromEntries( 32 | (Array.isArray(obj) ? obj : Object.entries(obj)).filter((val) => cb(...val)) 33 | ); 34 | } 35 | 36 | function defaults(obj, ...defs) { 37 | return Object.assign({}, obj, ...defs.reverse(), obj); 38 | } 39 | 40 | function unquote(str) { 41 | return str.replace(/^['"]|['"]$/g, "").trim(); 42 | } 43 | 44 | function toKebabCase(str) { 45 | return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); 46 | } 47 | 48 | module.exports = { 49 | isBoolean, 50 | isString, 51 | isNumeric, 52 | isArrayLike, 53 | isPlainObject, 54 | mapObject, 55 | filterObject, 56 | defaults, 57 | unquote, 58 | toKebabCase, 59 | }; 60 | -------------------------------------------------------------------------------- /update.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require("node:path"); 4 | const { execFileSync } = require("node:child_process"); 5 | const { mkdirSync, rmSync } = require("node:fs"); 6 | const { writeFile } = require("node:fs/promises"); 7 | const fontkit = require("fontkit"); 8 | const fetch = require("node-fetch"); 9 | const postcss = require("postcss"); 10 | const { toKebabCase } = require("./src/utils.js"); 11 | 12 | const TEMP = path.resolve(".temp"); 13 | 14 | const interUrl = "https://rsms.me/inter/"; 15 | const interSource = `${interUrl}inter.css`; 16 | const interFamilies = new Set(); 17 | const interFiles = new Set(); 18 | const interFontFaces = new Set(); 19 | const inter = { 20 | version: null, 21 | availableFeatures: new Set(), 22 | base: { 23 | "@font-face": [], 24 | }, 25 | utilities: { 26 | ".font-inter": { 27 | "font-family": "Inter, sans-serif", 28 | }, 29 | "@supports(font-variation-settings: normal)": { 30 | ".font-inter": { 31 | "font-family": "InterVariable, sans-serif", 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | const extractCss = (root) => { 38 | console.info("Parsing fetched CSS..."); 39 | 40 | root.walkAtRules((rule) => { 41 | const declarations = {}; 42 | 43 | rule.walkDecls((decl) => { 44 | const name = toKebabCase(decl.prop); 45 | let value = decl.value 46 | .split(",") 47 | .map((val) => val.trim()) 48 | .join(", "); 49 | 50 | if (name === "font-family") { 51 | const fontName = value.replace(/^['"]|['"]$/g, "").trim(); 52 | if (!fontName.match(/\salt$/)) { 53 | interFamilies.add(fontName); 54 | } 55 | } 56 | 57 | if (name === "src") { 58 | const vals = value.match(/url\(['"]([^'"]*)['"]\)/gi); 59 | 60 | if (vals) { 61 | for (const val of vals) { 62 | const cleanVal = val.replace(/^url\(['"]|['"]\)$/g, ""); 63 | value = value.replace(cleanVal, interUrl + cleanVal); 64 | interFiles.add(interUrl + cleanVal); 65 | } 66 | } 67 | } 68 | 69 | declarations[name] = value; 70 | }); 71 | 72 | if (rule.name === 'font-face' && ['Inter', 'InterVariable'].includes(declarations['font-family'])) { 73 | interFontFaces.add(declarations); 74 | } 75 | }); 76 | 77 | console.log("Found font families:", [...interFamilies].join(", ")); 78 | }; 79 | 80 | const download = (fileUrl, initialFile = null) => { 81 | let file = initialFile; 82 | 83 | if (!file) { 84 | const parsed = new URL(fileUrl); 85 | const fileName = path.basename(parsed.pathname); 86 | file = path.join(TEMP, fileName); 87 | } 88 | 89 | execFileSync("curl", ["--silent", "-o", file, "-L", fileUrl], { 90 | encoding: "utf8", 91 | }); 92 | 93 | return file; 94 | }; 95 | 96 | console.info("Fetching", interSource); 97 | 98 | rmSync(TEMP, { recursive: true, force: true }); 99 | mkdirSync(TEMP, { recursive: true }); 100 | 101 | fetch(interSource) 102 | .then((res) => res.text()) 103 | .then((css) => postcss([extractCss]).process(css, { from: undefined })) 104 | .then(() => { 105 | console.info("Fetching font files..."); 106 | 107 | for (const fontFile of interFiles) { 108 | const file = download(fontFile); 109 | const font = fontkit.openSync(file); 110 | 111 | inter.version = font.version.replace(/Version\s/, ""); 112 | 113 | for (const feature of font.availableFeatures) { 114 | inter.availableFeatures.add(feature); 115 | } 116 | } 117 | 118 | inter.availableFeatures = [...inter.availableFeatures].sort(); 119 | inter.base['@font-face'] = [...interFontFaces]; 120 | 121 | console.log("Font version:", inter.version); 122 | console.log("Found font features:", inter.availableFeatures.join(", ")); 123 | }) 124 | .then(async () => { 125 | const file = path.join(process.cwd(), "inter.json"); 126 | await writeFile(file, JSON.stringify(inter)); 127 | console.info("Finished. Meta data stored in ./inter.json"); 128 | }) 129 | .catch((err) => console.error(err)); 130 | --------------------------------------------------------------------------------