├── .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 |
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 |
--------------------------------------------------------------------------------