├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintrc.cjs ├── .github ├── renovate.json └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── README.md ├── docs ├── eleventy.config.js ├── package.json ├── remark │ ├── directives.js │ ├── headings.js │ ├── prose.js │ └── sample.js ├── src │ ├── assets │ │ ├── fonts │ │ │ ├── Inter-italic.var.woff2 │ │ │ ├── Inter-roman.var.woff2 │ │ │ ├── allan-regular.woff2 │ │ │ ├── caflisch-script-pro-regular.woff2 │ │ │ ├── ebgaramond-semibold.woff2 │ │ │ ├── exo-medium.woff2 │ │ │ ├── hypatia-sans-pro-bold.woff2 │ │ │ ├── sorts-mill-goudy-regular.woff2 │ │ │ └── warnock-pro-bold.woff2 │ │ ├── images │ │ │ └── og-image.png │ │ ├── main.css │ │ └── styles.11ty.js │ ├── data │ │ └── classes │ │ │ ├── alternates.js │ │ │ ├── caps.js │ │ │ ├── kerning.js │ │ │ ├── ligatures.js │ │ │ ├── ot-alternates.js │ │ │ ├── ot-ligatures.js │ │ │ └── position.js │ ├── feature-alternates.md │ ├── feature-ligatures.md │ ├── feature-position.md │ ├── font-variant-alternates.md │ ├── font-variant-caps.md │ ├── font-variant-east-asian.md │ ├── font-variant-ligatures.md │ ├── includes │ │ ├── class-table.njk │ │ ├── logo.njk │ │ ├── menu-button.njk │ │ ├── page-header.njk │ │ └── site-head.njk │ ├── index.md │ ├── layouts │ │ ├── base.njk │ │ ├── home.njk │ │ └── page.njk │ ├── public │ │ ├── android-chrome-192.png │ │ ├── android-chrome-512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-dark.png │ │ ├── favicon-dark.svg │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── favicon.svg │ │ ├── manifest.json │ │ └── pinned-tab.svg │ └── typography-kerning.md └── tailwind.config.js ├── package.json ├── plugin ├── CHANGELOG.md ├── LICENSE.txt ├── __tests__ │ ├── helpers.ts │ ├── plugin.test.ts │ ├── run.ts │ └── setup.ts ├── package.json ├── src │ ├── index.ts │ └── plugin.ts ├── tsconfig.build.json ├── types │ └── vitest.d.ts └── vite.config.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@latest/schema.json", 3 | "access": "public", 4 | "baseBranch": "main", 5 | "changelog": [ 6 | "@zazen/changesets-changelog", 7 | { "repo": "stormwarning/tailwindcss-opentype" } 8 | ], 9 | "commit": false, 10 | "ignore": [], 11 | "linked": [], 12 | "updateInternalDependencies": "patch" 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | const config = { 3 | extends: [ 4 | '@zazen', 5 | '@zazen/eslint-config/node', 6 | '@zazen/eslint-config/typescript', 7 | ], 8 | env: { 9 | node: true, 10 | }, 11 | ignorePatterns: ['dist', '*.njk'], 12 | rules: { 13 | '@typescript-eslint/ban-types': 'off', 14 | '@typescript-eslint/lines-between-class-members': 'off', 15 | '@typescript-eslint/padding-line-between-statements': 'off', 16 | 17 | /** 18 | * Currently conflicting with 'yoda' and 'unicorn/explicit-length-check'. 19 | */ 20 | 'etc/prefer-less-than': 'off', 21 | 22 | 'n/file-extension-in-import': ['error', 'always'], 23 | 24 | 'no-multi-assign': 'off', 25 | }, 26 | overrides: [ 27 | { 28 | // Jest config 29 | files: ['**/__tests__/**/*.{js,ts,tsx}', '**/*.@(spec|test).{js,ts,tsx}'], 30 | env: { 31 | jest: true, 32 | }, 33 | rules: { 34 | 'import/no-extraneous-dependencies': 'off', 35 | }, 36 | }, 37 | { 38 | files: ['**/*.d.ts'], 39 | rules: { 40 | // Prevent conflicts with `import/no-mutable-exports`. 41 | 'prefer-let/prefer-let': 'off', 42 | }, 43 | }, 44 | { 45 | files: ['docs/**/*.js', '*.config.js'], 46 | rules: { 47 | 'import/no-extraneous-dependencies': 'off', 48 | }, 49 | }, 50 | ], 51 | } 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 54 | module.exports = config 55 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>tidaltheory/renovate-config"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | # https://github.com/actions/checkout 11 | - name: Checkout repo 12 | uses: actions/checkout@v4 13 | 14 | # https://github.com/pnpm/action-setup 15 | - uses: pnpm/action-setup@v2 16 | with: 17 | version: 10 18 | 19 | - name: Setup Node.js 18.x 20 | uses: actions/setup-node@v3.1.1 21 | with: 22 | node-version: 18.x 23 | cache: pnpm 24 | 25 | - name: Install dependencies 26 | run: pnpm install --frozen-lockfile 27 | 28 | - name: Build 29 | run: npm run build 30 | 31 | - name: Test 32 | run: npm test 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | # https://github.com/actions/checkout 14 | - name: Checkout repo 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | # https://github.com/pnpm/action-setup 20 | - uses: pnpm/action-setup@v2 21 | with: 22 | version: 10 23 | 24 | - name: Setup Node.js 18.x 25 | uses: actions/setup-node@v3.1.1 26 | with: 27 | node-version: 18.x 28 | cache: pnpm 29 | 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | # https://github.com/changesets/action 34 | - name: Create release PR or publish to npm 35 | id: changesets 36 | uses: changesets/action@v1 37 | with: 38 | # This expects you to have a script called release which does a build 39 | # for your packages and calls changeset publish 40 | publish: npm run release 41 | title: 'Publish release' 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | docs/src/assets/styles.css 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Disable hooks in CI. 4 | [ -n "$CI" ] && exit 0 5 | 6 | npx lint-staged 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tailwindcss-opentype 2 | 3 | [![npm version][npm-img]][npm-url] 4 | [![npm downloads][npm-dls]][npm-url] 5 | 6 | > Tailwind CSS utility classes for advanced typographic features. 7 | 8 | ## Usage 9 | 10 | ``` 11 | npm install tailwindcss-opentype 12 | ``` 13 | 14 | **📚 Read the [full documentation](https://tailwindcss-opentype.netlify.app).** 15 | 16 | ## Thanks 17 | 18 | - [Utility OpenType](https://github.com/kennethormandy/utility-opentype) by [@kennethormandy](https://github.com/kennethormandy) 19 | - [OpenType Features](https://sparanoid.com/lab/opentype-features/) by [@sparanoid](https://github.com/sparanoid) 20 | 21 | ## Related 22 | 23 | [✂️ tailwindcss-capsize](https://github.com/stormwarning/tailwindcss-capsize) — Utility classes for trimming leading whitespace. 24 | 25 | 26 | [npm-url]: https://www.npmjs.com/package/tailwindcss-opentype 27 | [npm-img]: https://img.shields.io/npm/v/tailwindcss-opentype.svg?style=flat-square 28 | [npm-dls]: https://img.shields.io/npm/dt/tailwindcss-opentype.svg?style=flat-square 29 | -------------------------------------------------------------------------------- /docs/eleventy.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 2 | 3 | import eleventyNavigationPlugin from '@11ty/eleventy-navigation' 4 | import eleventyRemark from '@fec/eleventy-plugin-remark' 5 | import dedent from 'dedent' 6 | import { rehype } from 'rehype' 7 | import rehypeAutolinkHeadings from 'rehype-autolink-headings' 8 | import rehypeSlug from 'rehype-slug' 9 | import { remark } from 'remark' 10 | import remarkDirective from 'remark-directive' 11 | 12 | import { remarkDirectives } from './remark/directives.js' 13 | import { remarkHeadings } from './remark/headings.js' 14 | import { remarkSample } from './remark/sample.js' 15 | 16 | /** @param {import('@11ty/eleventy').UserConfig} eleventyConfig */ 17 | export default async function config(eleventyConfig) { 18 | eleventyConfig.addPassthroughCopy({ 19 | 'src/assets/fonts': './assets/fonts', 20 | }) 21 | eleventyConfig.addPassthroughCopy({ 22 | 'src/assets/images': './assets/images', 23 | }) 24 | eleventyConfig.addPassthroughCopy({ 'src/public': '.' }) 25 | 26 | eleventyConfig.addPairedShortcode( 27 | 'navitem', 28 | (content, url, isSelected, isInactive) => { 29 | let tag = '' 30 | 31 | if (isInactive) { 32 | tag = `${content}` 33 | } else { 34 | let linkClass = [ 35 | 'px-3 py-2 transition-colors duration-200 relative block', 36 | isSelected && 'text-sky-700', 37 | !isSelected && 'hover:text-grey-900 text-grey-500', 38 | ].join(' ') 39 | 40 | tag = dedent` 41 | 44 | ${content} 45 | ` 46 | } 47 | 48 | return `
  • ${tag}
  • ` 49 | }, 50 | ) 51 | 52 | eleventyConfig.addPlugin(eleventyNavigationPlugin) 53 | eleventyConfig.addPlugin(eleventyRemark, { 54 | plugins: [ 55 | remarkHeadings, 56 | remarkDirective, 57 | remarkDirectives, 58 | remarkSample, 59 | // Require('./remark/prose'), 60 | ], 61 | }) 62 | 63 | eleventyConfig.addTransform( 64 | 'rehype', 65 | /** @param {string} content */ async (content, outputPath) => { 66 | let newContent = content 67 | 68 | if (outputPath?.endsWith('.html')) { 69 | let result = await rehype() 70 | .use(rehypeSlug) 71 | .use(rehypeAutolinkHeadings, { 72 | test: (element, index, parent) => parent.tagName !== 'nav', 73 | properties: { 74 | class: 75 | 'absolute ml-[-0.75em] md:ml-[-1em] pr-[0.5em] !no-underline !text-grey-400 opacity-0 group-hover:opacity-100', 76 | }, 77 | content: { 78 | type: 'text', 79 | value: '¶', 80 | }, 81 | }) 82 | .process(content) 83 | 84 | newContent = result.toString() 85 | } 86 | 87 | return newContent 88 | }, 89 | ) 90 | 91 | return { 92 | dir: { 93 | input: 'src', 94 | data: 'data', 95 | includes: 'includes', 96 | layouts: 'layouts', 97 | output: 'dist', 98 | }, 99 | // PathPrefix: 100 | // process.env.NODE_ENV === 'production' 101 | // ? '/tailwindcss-opentype/' 102 | // : '', 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "eleventy --config=eleventy.config.js", 8 | "dev": "TAILWIND_MODE=watch eleventy --config=eleventy.config.js --serve" 9 | }, 10 | "devDependencies": { 11 | "@11ty/eleventy": "3.0.0", 12 | "@11ty/eleventy-navigation": "0.3.5", 13 | "@11ty/eleventy-plugin-syntaxhighlight": "5.0.0", 14 | "@fec/eleventy-plugin-remark": "4.0.0", 15 | "@tailwindcss/typography": "0.5.2", 16 | "@types/hast": "3.0.4", 17 | "@types/mdast": "4.0.4", 18 | "autoprefixer": "10.3.4", 19 | "dedent": "1.5.3", 20 | "hastscript": "9.0.1", 21 | "mdast-util-to-hast": "13.2.0", 22 | "postcss": "8.4.23", 23 | "postcss-cli": "8.3.1", 24 | "prismjs": "1.30.0", 25 | "rehype": "13.0.2", 26 | "rehype-autolink-headings": "7.1.0", 27 | "rehype-parse": "9.0.1", 28 | "rehype-slug": "6.0.0", 29 | "remark": "15.0.1", 30 | "remark-directive": "4.0.0", 31 | "tailwindcss": "3.3.2", 32 | "tailwindcss-opentype": "workspace:*", 33 | "unified": "11.0.5", 34 | "unist-util-visit": "5.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/remark/directives.js: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent' 2 | import { h } from 'hastscript' 3 | import { toHast } from 'mdast-util-to-hast' 4 | import parse from 'rehype-parse' 5 | import { unified } from 'unified' 6 | import { visit } from 'unist-util-visit' 7 | 8 | export function remarkDirectives() { 9 | /** @param {import('mdast').Root} tree */ 10 | return (tree) => { 11 | visit( 12 | tree, 13 | ['textDirective', 'leafDirective', 'containerDirective'], 14 | (node) => { 15 | if ( 16 | node.type === 'containerDirective' || 17 | node.type === 'leafDirective' || 18 | node.type === 'textDirective' 19 | ) { 20 | let data = (node.data ??= {}) 21 | /** @type {import('hast').Element} */ 22 | let hast = h(node.name, node.attributes) 23 | let children = [] 24 | 25 | switch (node.name) { 26 | case 'feat': { 27 | let tags = 28 | node.children.length > 0 && node.children[0].type === 'text' 29 | ? node.children[0].value.split(',') 30 | : undefined 31 | let features = dedent` 32 | 33 | ${tags.map((tag) => markupTagText(tag))} 34 | ` 35 | .replaceAll('\n', '') 36 | .replaceAll('\t', '') 37 | .replace(',', ' ') 38 | 39 | let parsed = unified().use(parse).parse(features) 40 | let html = parsed.children[0] 41 | /** @todo Fix type information here. */ 42 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 43 | children = html.children[1].children 44 | 45 | hast = h(node.name, node.attributes, [children]) 46 | data.hChildren = hast.children 47 | break 48 | } 49 | 50 | case 'reminder': { 51 | let icon = dedent` 52 | 53 | 54 | ` 55 | let existing = toHast(node.children[0]) 56 | /** @todo Fix type information here. */ 57 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 58 | let child = unified().use(parse).parse(icon).children[0] 59 | .children[1].children 60 | 61 | existing.properties.class = '!m-0' 62 | console.log(existing) 63 | hast = h( 64 | 'div', 65 | { class: 'flex gap-4 p-4 bg-grey-100 rounded-lg' }, 66 | [child, existing], 67 | ) 68 | data.hName = hast.tagName 69 | data.hProperties = hast.properties 70 | data.hChildren = hast.children 71 | break 72 | } 73 | 74 | default: 75 | data.hName = hast.tagName 76 | data.hProperties = hast.properties 77 | } 78 | } 79 | }, 80 | ) 81 | } 82 | } 83 | 84 | function markupTagText(tag) { 85 | return dedent` 86 | ${tag} 87 | ` 88 | } 89 | -------------------------------------------------------------------------------- /docs/remark/headings.js: -------------------------------------------------------------------------------- 1 | import { visit } from 'unist-util-visit' 2 | 3 | export function remarkHeadings() { 4 | /** @param {import('@types/mdast').Root} tree */ 5 | return (tree) => { 6 | visit( 7 | tree, 8 | 'heading', 9 | /** @param {import('@types/mdast').Heading} node */ (node) => { 10 | if (node.depth === 1) return 11 | 12 | let data = (node.data ??= {}) 13 | let properties = (data.hProperties ??= {}) 14 | /** @type {string[]} */ 15 | let classes = (properties.class ??= []) 16 | 17 | classes.push('group flex whitespace-pre-wrap') 18 | }, 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/remark/prose.js: -------------------------------------------------------------------------------- 1 | function prose() { 2 | return (tree) => { 3 | let insideProse = false 4 | 5 | tree.children = tree.children.flatMap((node, i) => { 6 | console.log('inside:', insideProse) 7 | console.log('node:', node) 8 | // if (insideProse && isJsNode(node)) { 9 | // insideProse = false 10 | // return [{ type: 'jsx', value: '' }, node] 11 | // } 12 | if (!insideProse) { 13 | insideProse = true 14 | return [ 15 | { 16 | type: 'html', 17 | value: '
    ', 18 | // tagName: 'div', 19 | // properties: { class: 'prose' }, 20 | }, 21 | node, 22 | ...(i === tree.children.length - 1 23 | ? [{ type: 'html', value: '
    ' }] 24 | : []), 25 | ] 26 | } 27 | 28 | if (i === tree.children.length - 1 && insideProse) { 29 | return [node, { type: 'html', value: '' }] 30 | } 31 | 32 | return [node] 33 | }) 34 | } 35 | } 36 | 37 | module.exports = prose 38 | -------------------------------------------------------------------------------- /docs/remark/sample.js: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent' 2 | import Prism from 'prismjs' 3 | import loadLanguages from 'prismjs/components/index.js' 4 | import parse from 'rehype-parse' 5 | import { unified } from 'unified' 6 | import { visit } from 'unist-util-visit' 7 | 8 | loadLanguages() 9 | 10 | const previewBackground = { 11 | amber: 'bg-gradient-to-r from-amber-50 to-amber-100 accent-amber', 12 | orange: 'bg-gradient-to-r from-orange-50 to-orange-100 accent-orange', 13 | rose: 'bg-gradient-to-r from-rose-50 to-rose-100 accent-rose', 14 | fuchsia: 'bg-gradient-to-r from-fuchsia-50 to-fuchsia-100 accent-fuchsia', 15 | indigo: 'bg-gradient-to-r from-indigo-50 to-indigo-100 accent-indigo', 16 | emerald: 'bg-gradient-to-r from-emerald-50 to-teal-100 accent-emerald', 17 | } 18 | 19 | /** 20 | * @param {string} code 21 | * @param {string} prismLanguage 22 | * @returns {string} 23 | */ 24 | function highlightCode(code, prismLanguage) { 25 | let isDiff = prismLanguage.startsWith('diff-') 26 | let language = isDiff ? prismLanguage.slice(5) : prismLanguage 27 | /** @type {import('prismjs').Grammar} */ 28 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 29 | let grammar = Prism.languages[isDiff ? 'diff' : language] 30 | 31 | if (!grammar) { 32 | // eslint-disable-next-line no-console 33 | console.warn(`Unrecognised language: ${prismLanguage}`) 34 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 35 | return Prism.util.encode(code) 36 | } 37 | 38 | let highlighted = Prism.highlight(code, grammar, prismLanguage) 39 | 40 | return language === 'html' 41 | ? highlighted.replaceAll( 42 | /\*\*(.*?)\*\*/g, 43 | (_, text) => 44 | `${text}`, 45 | ) 46 | : highlighted 47 | } 48 | 49 | export function remarkSample() { 50 | /** @param {import('@types/mdast').Root} tree */ 51 | return (tree) => { 52 | visit(tree, 'code', (node) => { 53 | if (node.lang !== 'html') return 54 | 55 | let hasPreview = false 56 | // 57 | // let previewClassName 58 | let previewCode 59 | let snippetCode = node.value 60 | .replace( 61 | /(.*?)<\/template>/is, 62 | /** @param {string} content */ 63 | (m, class1, class2, content) => { 64 | hasPreview = true 65 | // 66 | // previewClassName = class1 || class2 67 | previewCode = content 68 | return '' 69 | }, 70 | ) 71 | .trim() 72 | 73 | if (!hasPreview) return 74 | snippetCode ||= previewCode 75 | 76 | snippetCode = highlightCode(dedent(snippetCode).trim(), 'html') 77 | let snippetHast = unified().use(parse).parse(snippetCode) 78 | 79 | let meta = node.meta ? node.meta.trim().split(/\s+/) : [] 80 | let color = meta.find((x) => !/^resizable(:|$)/.test(x)) 81 | 82 | let previewHast = unified().use(parse).parse(previewCode) 83 | let preview = { 84 | type: 'element', 85 | tagName: 'div', 86 | properties: { 87 | class: [ 88 | 'rounded-t-xl overflow-hidden code-sample', 89 | previewBackground[color], 90 | ], 91 | }, 92 | children: [ 93 | { 94 | type: 'element', 95 | tagName: 'div', 96 | properties: { 97 | class: 'flex overflow-x-auto', 98 | }, 99 | children: [ 100 | { 101 | type: 'element', 102 | tagName: 'div', 103 | properties: { 104 | class: 105 | 'p-10 text-grey-600 mix-blend-multiply whitespace-nowrap', 106 | }, 107 | /** @todo Fix type information here. */ 108 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 109 | children: previewHast.children[0].children[1].children, 110 | }, 111 | ], 112 | }, 113 | ], 114 | } 115 | 116 | let snippet = { 117 | type: 'element', 118 | tagName: 'div', 119 | properties: { class: 'overflow-hidden rounded-b-xl' }, 120 | children: [ 121 | { 122 | type: 'element', 123 | tagName: 'pre', 124 | properties: { 125 | class: `scrollbar-none overflow-x-auto !m-0 !p-6 text-sm leading-snug !rounded-none language-${node.lang} text-white`, 126 | }, 127 | children: [ 128 | { 129 | type: 'element', 130 | tagName: 'code', 131 | properties: { class: 'language-html' }, 132 | children: node.data?.hChildren ?? [ 133 | snippetHast.children[0].children[1], 134 | ], 135 | }, 136 | ], 137 | }, 138 | ], 139 | } 140 | 141 | let n = node 142 | 143 | n.type = 'code-sample' 144 | n.data ??= {} 145 | 146 | n.data.hName = 'div' 147 | n.data.hProperties = { 148 | className: ['relative overflow-hidden mb-8'], 149 | } 150 | n.data.hChildren = [preview, snippet] 151 | }) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /docs/src/assets/fonts/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /docs/src/assets/fonts/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /docs/src/assets/fonts/allan-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/allan-regular.woff2 -------------------------------------------------------------------------------- /docs/src/assets/fonts/caflisch-script-pro-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/caflisch-script-pro-regular.woff2 -------------------------------------------------------------------------------- /docs/src/assets/fonts/ebgaramond-semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/ebgaramond-semibold.woff2 -------------------------------------------------------------------------------- /docs/src/assets/fonts/exo-medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/exo-medium.woff2 -------------------------------------------------------------------------------- /docs/src/assets/fonts/hypatia-sans-pro-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/hypatia-sans-pro-bold.woff2 -------------------------------------------------------------------------------- /docs/src/assets/fonts/sorts-mill-goudy-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/sorts-mill-goudy-regular.woff2 -------------------------------------------------------------------------------- /docs/src/assets/fonts/warnock-pro-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/fonts/warnock-pro-bold.woff2 -------------------------------------------------------------------------------- /docs/src/assets/images/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/assets/images/og-image.png -------------------------------------------------------------------------------- /docs/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | /*! purgecss start ignore */ 4 | .token.tag, 5 | .token.class-name, 6 | .token.selector, 7 | .token.selector .class, 8 | .token.function { 9 | @apply text-fuchsia-400; 10 | } 11 | 12 | .token.attr-name, 13 | .token.keyword, 14 | .token.rule, 15 | .token.operator, 16 | .token.pseudo-class, 17 | .token.important { 18 | @apply text-cyan-400; 19 | } 20 | 21 | .token.attr-value, 22 | .token.class, 23 | .token.string, 24 | .token.number, 25 | .token.unit, 26 | .token.color { 27 | @apply text-lime-300; 28 | } 29 | 30 | .token.punctuation, 31 | .token.module, 32 | .token.property { 33 | @apply text-sky-200; 34 | } 35 | 36 | .token.atapply .token:not(.rule):not(.important) { 37 | color: inherit; 38 | } 39 | 40 | .language-shell .token:not(.comment) { 41 | color: inherit; 42 | } 43 | 44 | .language-css .token.function { 45 | color: inherit; 46 | } 47 | 48 | .token.comment { 49 | @apply text-gray-400; 50 | } 51 | 52 | .token.deleted:not(.prefix) { 53 | @apply relative block -mx-4 px-4; 54 | } 55 | 56 | .token.deleted:not(.prefix)::after { 57 | content: ""; 58 | @apply pointer-events-none absolute inset-0 block bg-rose-400 bg-opacity-25; 59 | } 60 | 61 | .token.deleted.prefix { 62 | @apply text-gray-400 select-none; 63 | } 64 | 65 | .token.inserted:not(.prefix) { 66 | @apply block bg-emerald-700 bg-opacity-50 -mx-4 px-4; 67 | } 68 | 69 | .token.inserted.prefix { 70 | @apply text-emerald-200 text-opacity-75 select-none; 71 | } 72 | /*! purgecss end ignore */ 73 | 74 | @tailwind components; 75 | @tailwind utilities; 76 | 77 | @layer base { 78 | @font-face { 79 | font-family: "Inter var"; 80 | font-weight: 100 900; 81 | font-style: normal; 82 | font-named-instance: "Regular"; 83 | src: url(/assets/fonts/Inter-roman.var.woff2) format("woff2"); 84 | font-display: swap; 85 | } 86 | @font-face { 87 | font-family: "Inter var"; 88 | font-weight: 100 900; 89 | font-style: italic; 90 | font-named-instance: "Italic"; 91 | src: url(/assets/fonts/Inter-italic.var.woff2) format("woff2"); 92 | font-display: swap; 93 | } 94 | @font-face { 95 | font-family: "Allan"; 96 | font-weight: 400; 97 | src: url(/assets/fonts/allan-regular.woff2) format("woff2"); 98 | font-display: swap; 99 | } 100 | @font-face { 101 | font-family: "Caflisch Script"; 102 | font-weight: 400; 103 | src: url(/assets/fonts/caflisch-script-pro-regular.woff2) format("woff2"); 104 | font-display: swap; 105 | } 106 | @font-face { 107 | font-family: "EB Garamond"; 108 | font-weight: 400; 109 | src: url(/assets/fonts/ebgaramond-semibold.woff2) format("woff2"); 110 | font-display: swap; 111 | } 112 | @font-face { 113 | font-family: "Exo"; 114 | font-weight: 400; 115 | src: url(/assets/fonts/exo-medium.woff2) format("woff2"); 116 | font-display: swap; 117 | } 118 | @font-face { 119 | font-family: "Hypatia Sans Pro"; 120 | font-weight: 400; 121 | src: url(/assets/fonts/hypatia-sans-pro-bold.woff2) format("woff2"); 122 | font-display: swap; 123 | } 124 | @font-face { 125 | font-family: "Sorts Mill Goudy"; 126 | font-weight: 400; 127 | src: url(/assets/fonts/sorts-mill-goudy-regular.woff2) format("woff2"); 128 | font-display: swap; 129 | } 130 | @font-face { 131 | font-family: "Warnock Pro"; 132 | font-weight: 400; 133 | src: url(/assets/fonts/warnock-pro-bold.woff2) format("woff2"); 134 | font-display: swap; 135 | } 136 | } 137 | 138 | @layer utilities { 139 | .accent-amber { 140 | --accent-color: theme("colors.amber.600"); 141 | } 142 | .accent-orange { 143 | --accent-color: theme("colors.orange.600"); 144 | } 145 | .accent-rose { 146 | --accent-color: theme("colors.rose.600"); 147 | } 148 | .accent-fuchsia { 149 | --accent-color: theme("colors.fuchsia.600"); 150 | } 151 | .accent-indigo { 152 | --accent-color: theme("colors.indigo.600"); 153 | } 154 | .accent-emerald { 155 | --accent-color: theme("colors.emerald.600"); 156 | } 157 | 158 | .code-highlight { 159 | border-radius: 0.1875rem; 160 | padding: 0.0625rem 0.1875rem; 161 | margin: 0 -0.1875rem; 162 | } 163 | .bg-code-highlight { 164 | background-color: rgba(134, 239, 172, 0.25); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /docs/src/assets/styles.11ty.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | 3 | import autoprefixer from 'autoprefixer' 4 | import postcss from 'postcss' 5 | import tailwindcss from 'tailwindcss' 6 | 7 | export default class Styles { 8 | async data() { 9 | return { 10 | permalink: 'assets/styles.css', 11 | } 12 | } 13 | 14 | async render() { 15 | let processed = await postcss([ 16 | tailwindcss('./tailwind.config.js'), 17 | autoprefixer, 18 | ]).process(await fs.readFile('./src/assets/main.css'), { 19 | from: undefined, 20 | }) 21 | 22 | return processed.css 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/data/classes/alternates.js: -------------------------------------------------------------------------------- 1 | const alternates = { 2 | 'historical-forms': 'font-variant-alternates: historical-forms;', 3 | } 4 | 5 | export default alternates 6 | -------------------------------------------------------------------------------- /docs/src/data/classes/caps.js: -------------------------------------------------------------------------------- 1 | const caps = { 2 | 'small-caps': 'font-variant-caps: small-caps;', 3 | 'all-small-caps': 'font-variant-caps: all-small-caps;', 4 | 'titling-caps': 'font-variant-caps: titling-caps;', 5 | } 6 | 7 | export default caps 8 | -------------------------------------------------------------------------------- /docs/src/data/classes/kerning.js: -------------------------------------------------------------------------------- 1 | const kerning = { 2 | kerning: 'font-kerning: auto;', 3 | 'kerning-normal': 'font-kerning: normal;', 4 | 'kerning-none': 'font-kerning: none;', 5 | } 6 | 7 | export default kerning 8 | -------------------------------------------------------------------------------- /docs/src/data/classes/ligatures.js: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent' 2 | 3 | const ligatures = { 4 | 'common-ligatures': dedent`--ot-liga: common-ligatures; 5 | font-variant-ligatures: var(--ot-liga) var(--ot-dlig) var(--ot-calt);`, 6 | 'no-common-ligatures': dedent`--ot-liga: no-common-ligatures; 7 | font-variant-ligatures: var(--ot-liga) var(--ot-dlig) var(--ot-calt);`, 8 | 'discretionary-ligatures': dedent`--ot-dlig: discretionary-ligatures; 9 | font-variant-ligatures: var(--ot-liga) var(--ot-dlig) var(--ot-calt);`, 10 | 'no-discretionary-ligatures': dedent`--ot-dlig: no-discretionary-ligatures; 11 | font-variant-ligatures: var(--ot-liga) var(--ot-dlig) var(--ot-calt);`, 12 | contextual: dedent`--ot-calt: contextual; 13 | font-variant-ligatures: var(--ot-liga) var(--ot-dlig) var(--ot-calt);`, 14 | 'no-contextual': dedent`--ot-calt: no-contextual; 15 | font-variant-ligatures: var(--ot-liga) var(--ot-dlig) var(--ot-calt);`, 16 | } 17 | 18 | export default ligatures 19 | -------------------------------------------------------------------------------- /docs/src/data/classes/ot-alternates.js: -------------------------------------------------------------------------------- 1 | const otAlternates = { 2 | salt: '--ot-salt: "salt" 1', 3 | 'ss-01': '--ot-ss01: "ss01" 1', 4 | 'ss-02': '--ot-ss02: "ss02" 1', 5 | 'ss-03': '--ot-ss03: "ss03" 1', 6 | 'ss-04': '--ot-ss04: "ss04" 1', 7 | } 8 | 9 | export default otAlternates 10 | -------------------------------------------------------------------------------- /docs/src/data/classes/ot-ligatures.js: -------------------------------------------------------------------------------- 1 | const otLigatures = { 2 | hlig: '--ot-hlig: "hlig" 1', 3 | } 4 | 5 | export default otLigatures 6 | -------------------------------------------------------------------------------- /docs/src/data/classes/position.js: -------------------------------------------------------------------------------- 1 | const position = { 2 | // 'super-position': '--ot-sups: "sups"', 3 | // 'sub-position': '--ot-subs: "subs"', 4 | // 'inferior-position': '--ot-sinf: "sinf"', 5 | sups: '--ot-sups: "sups" 1', 6 | subs: '--ot-subs: "subs" 1', 7 | sinf: '--ot-sinf: "sinf" 1', 8 | } 9 | 10 | export default position 11 | -------------------------------------------------------------------------------- /docs/src/feature-alternates.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Alternates 4 | description: Utilities for controlling the usage of alternate glyphs. 5 | tags: ot-feature 6 | eleventyNavigation: 7 | key: Alternates 8 | order: 3 9 | classData: ot-alternates 10 | --- 11 | 12 | ## Usage 13 | 14 | These utilities provide access to OpenType alternate glyph features not currently available via the higher-level CSS properties. For other alternate glyph features, use the [Font Variant Alternates](/font-variant-alternates) utilities. 15 | 16 | ### Stylistic alternates :feat[salt] 17 | 18 | Sometimes a significant portion of a typeface’s unique character comes from a few specific glyphs. Stylistic Alternates offer an opportunity to change these, and change the tone of the typeface. 19 | 20 | ```html amber 21 | 29 | 30 |

    Easy like Sunday morning & fox

    31 | ``` 32 | 33 | ### Stylistic sets :feat[ss01–ss20] 34 | 35 | This feature replaces sets of default character glyphs with stylistic variants. Glyphs in stylistic sets may be designed to harmonise visually, interact in particular ways, or otherwise work together. 36 | 37 | ```html emerald 38 | 44 | 45 |

    Illegal

    46 | ``` 47 | 48 | Note that fonts may employ stylistic sets in completely arbitrary and individual ways. In this example, Inter uses `ss02` to change a series of glyphs into less ambiguous forms, but the same stylistic set in another font could produce completely different changes. 49 | 50 | The OpenType spec allows for as many as 20 different sets to be defined in a font; by default this plugin includes utilities for `ss01` through `ss04`. To add additional sets or to change the label of the utility class for specific sets, use the `stylisticSets` key in your `theme` or `extends` config. 51 | 52 | ```js 53 | // tailwind.config.js 54 | module.exports = { 55 | theme: { 56 | stylisticSets: { 57 | 'open-digits': 'ss01', 58 | disambiguate: 'ss02', 59 | 'curved-r': 'ss03', 60 | }, 61 | extend: { 62 | stylisticSets: { 63 | '04': 'ss04', 64 | }, 65 | }, 66 | }, 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/src/feature-ligatures.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Ligatures 4 | description: Utilities for controlling ligatures and contextual forms. 5 | tags: ot-feature 6 | eleventyNavigation: 7 | key: Ligatures 8 | order: 1 9 | classData: ot-ligatures 10 | --- 11 | 12 | ## Usage 13 | 14 | These utilities provide access to OpenType ligature features not currently available via the higher-level CSS properties. For other ligature features, use the [Font Variant Ligatures](/font-variant-ligatures) utilities. 15 | 16 | ### Historical ligatures :feat[hlig] 17 | 18 | Some ligatures were in common use in the past, but appear anachronistic today. Some fonts include the historical forms as alternates, so they can be used for a “period” effect. The most common example is the long s paired with most ascenders, while a tz ligature is also found in German blackletter type. 19 | 20 | Depending on the font, historical ligatures may need to have [historical forms](/font-variant-alternates/#historical-forms-hist) enabled as well. Alternatively, using the actual historical glyph (`ſ` for the long `s` in this example) should apply the ligature without having to apply the historical forms for the entire run of text. 21 | 22 | ```html emerald 23 | 29 | 30 |

    Lost lesson

    31 | ``` 32 | 33 | **Pro tip:** To _prevent_ a ligature from being rendered where it is not appropriate, insert a zero-width non-joiner — `‌` — between the glyphs. Conversely, a zero-width joiner — `‍` — should render the ligature form, without any CSS needed! 34 | -------------------------------------------------------------------------------- /docs/src/feature-position.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Position 4 | description: Utilities for controlling alternate, smaller glyphs that are positioned as superscript or subscript. 5 | tags: ot-feature 6 | eleventyNavigation: 7 | key: Position 8 | order: 5 9 | classData: position 10 | --- 11 | 12 | ## Usage 13 | 14 | While it is possible to use the `font-variant-position` utilities at the "block" level, depending on the typeface this may result in other characters being substituted for the repositioned glyphs. To avoid this, wrap the appropriate characters in an inline element, such as `` or ``. 15 | 16 | Using `` or `` elements has its own pitfalls, however. Common "reset" styles and even browser default styles often try to approximate superscript or subscript glyphs, which should be disabled if you are using a font designed with these features. These resets and defaults vary, so these utilities don't attempt to disable any default styles for these elements. Either account for this in your own baseline styles, or use a more neutral wrapper, like ``. 17 | 18 | The examples below use `` and `` with the default Tailwind CSS reset styles for the initial result, and `` elements with the utility classes added for the final result, in order to show the difference between the "synthesized" characters and the specifically designed forms. 19 | 20 | ### Superscript :feat[sups] 21 | 22 | This feature replaces lining or oldstyle figures with superscript figures, often used for footnote indication, and replaces lowercase letters with superscript letters. 23 | 24 | ```html emerald 25 | 34 | 35 |

    Mme

    36 | ``` 37 | 38 | This illustrates a case where blanket application of the feature wouldn't work: 39 | in **:span[3]{.sups}He** we want the :kbd[3] superscripted, but not the lowercase :kbd[e]. 40 | 41 | ### Subscript :feat[subs] 42 | 43 | Perhaps the most familiar example of subscripts is in chemical formulas. 44 | 45 | ```html orange 46 | 52 | 53 |

    H2O

    54 | ``` 55 | 56 | ### Scientific inferior :feat[sinf] 57 | 58 | Scientific inferior are for chemical and mathematical typesetting, and include optically corrected letters and numbers. This feature is often conflated with subscripts and may not be fully supported for every scientific notation form. For optimal results, something like [LaTeX](https://katex.org/) may be a better option. 59 | 60 | ```html rose 61 | 73 | 74 |

    H2O YCbCr νμ

    75 | ``` 76 | -------------------------------------------------------------------------------- /docs/src/font-variant-alternates.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Font Variant Alternates 4 | description: Utilities for controlling the usage of alternate glyphs. 5 | tags: font-variant 6 | eleventyNavigation: 7 | key: Font Variant Alternates 8 | order: 3 9 | classData: alternates 10 | --- 11 | 12 | ## Usage 13 | 14 | Use the `font-variant-alternates` utilities to access alternative styles for different characters. These can be applied to blocks of text, in the case of historical forms or stylesets, or to individual characters, as with swash glyphs or character variants. 15 | 16 | ### Historical forms :feat[hist] 17 | 18 | Historical glyph variants may not be useful in everyday typesetting situations, but can prove useful when referencing the past. 19 | 20 | ```html fuchsia 21 | 27 | 28 |

    Jesuit

    29 | ``` 30 | -------------------------------------------------------------------------------- /docs/src/font-variant-caps.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Font Variant Caps 4 | description: Utilities for controlling alternate glyphs for capital letters. 5 | tags: font-variant 6 | eleventyNavigation: 7 | key: Font Variant Caps 8 | order: 2 9 | classData: caps 10 | --- 11 | 12 | ## Usage 13 | 14 | Use the `font-variant-caps` utilities to transform letters into optimised capital forms. Small Caps are less distracting than all capitals for longer form text settings. They also provide an additional way to apply emphasis within text. 15 | 16 | ### Small caps :feat[cmcp] 17 | 18 | This feature turns lowercase characters into small capitals. 19 | 20 | ```html amber 21 | 28 | 29 |

    Not all caps are Small Caps.

    30 | ``` 31 | 32 | ### All small caps :feat[smcp,c2sc] 33 | 34 | Like `small-caps` but transforms uppercase characters into small capitals as well. 35 | 36 | ```html orange 37 | 43 | 44 |

    All caps are Small Caps.

    45 | ``` 46 | 47 | ### Titling caps :feat[titl] 48 | 49 | Uppercase letter glyphs are often designed for use with lowercase letters. When used in all uppercase titling sequences they can appear too strong. Titling capitals are designed specifically for this situation. 50 | 51 | Note: This feature is not _exclusively_ for capital letters, but for any forms better suited for large type, as in titles. It is included with these utilities due to how it is applied in the W3C spec. 52 | 53 | ```html emerald 54 | 61 | 62 |

    Quick Brown Lazy Grumpy

    63 | ``` 64 | -------------------------------------------------------------------------------- /docs/src/font-variant-east-asian.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Font Variant East Asian 4 | description: Utilities for controlling alternate glyphs for East Asian scripts, like Japanese and Chinese. 5 | tags: font-variant 6 | eleventyNavigation: 7 | key: Font Variant East Asian 8 | order: 4 9 | inactive: true 10 | --- 11 | -------------------------------------------------------------------------------- /docs/src/font-variant-ligatures.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Font Variant Ligatures 4 | description: Utilities for controlling ligatures and contextual forms. 5 | tags: font-variant 6 | eleventyNavigation: 7 | key: Font Variant Ligatures 8 | order: 1 9 | classData: ligatures 10 | --- 11 | 12 | ## Usage 13 | 14 | Use the `font-variant-ligatures` utilities to enable ligatures and contextual forms in textual content. Each setting can be disabled by prefixing the class with `no-`. 15 | 16 | These utilities are composable so you can enable multiple `font-variant-ligatures` features by combining multiple classes in your HTML: 17 | 18 | ### Common ligatures :feat[liga] 19 | 20 | Most common ligatures mitigate spacing issues between specific combinations of letters within a typeface, often by connecting glyphs that might otherwise collide. Common ligatures are usually enabled by default in fonts that support them, and can be disabled if needed. 21 | 22 | ```html orange 23 | 35 | 36 |

    fi ff fl ffi Th

    37 | ``` 38 | 39 | ### Discretionary ligatures :feat[dlig] 40 | 41 | Discretionary ligatures’ defining characteristic is that they are available to enable at your discretion: they are disabled by default. Often, these are additional ligatures that might be considered too attention-grabbing or unconventional to be enabled in many situations. 42 | 43 | ```html rose 44 | 54 | 55 |

    ct sp st

    56 | ``` 57 | 58 | ### Contextual alternates :feat[calt] 59 | 60 | Like ligatures (though not strictly a ligature feature), contextual alternates are commonly used to harmonize the shapes of glyphs with the surrounding context. This feature is also enabled by default, except in Chrome, and cannot be disabled in Safari. 61 | 62 | ```html indigo 63 | 72 | 73 |

    The bloom has gone off the rose

    74 | ``` 75 | -------------------------------------------------------------------------------- /docs/src/includes/class-table.njk: -------------------------------------------------------------------------------- 1 | {% macro table(classes) %} 2 |
    3 | Default class reference 4 |
    7 | 8 | 9 | 10 | 13 | 18 | 19 | 20 | 21 | {% for class, property in classes %} 22 | 23 | 28 | 31 | 32 | {% endfor %} 33 | 34 |
    11 |
    Class
    12 |
    14 |
    15 | Properties 16 |
    17 |
    26 | {{- class -}} 27 | 29 | {{- property -}} 30 |
    35 |
    36 |
    37 | {% endmacro %} 38 | -------------------------------------------------------------------------------- /docs/src/includes/logo.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/src/includes/menu-button.njk: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /docs/src/includes/page-header.njk: -------------------------------------------------------------------------------- 1 | {% macro header(title='', description, border) %} 2 |
    3 |
    4 |

    {{ title }}

    5 | {# {badge.key && badge.value && ( 6 |
    7 |
    {badge.key}
    8 |
    {badge.value}
    9 |
    10 | )} #} 11 |
    12 | {% if description -%} 13 |

    {{ description }}

    14 | {%- endif %} 15 |
    16 | {% endmacro %} 17 | -------------------------------------------------------------------------------- /docs/src/includes/site-head.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% if isHome %}{{ title }}{% else %}{{ title }} — TailwindCSS OpenType{% endif %} 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 38 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: TailwindCSS OpenType — utility classes for advanced typographic features. 4 | --- 5 | 6 | :::figure 7 | 8 | > OpenType features are like secret compartments in fonts. Unlock them and you'll find ways to make fonts look and behave differently in subtle and dramatic ways. Not all OpenType features are appropriate to use all of the time, but some features are critical for great typography. 9 | 10 | ::figcaption[— Tim Brown, Head of Typography at Adobe] 11 | 12 | ::: 13 | 14 | OpenType fonts include widely expanded character sets and layout features, which provide richer linguistic support and advanced typographic control. 15 | 16 | ## Getting started 17 | 18 | After installing the plugin, include it your Tailwind config and the utility classes will be available. 19 | 20 | ```js 21 | // tailwind.config.js 22 | module.exports = { 23 | ... 24 | plugins: [ 25 | require('tailwindcss-opentype'), 26 | ], 27 | } 28 | ``` 29 | 30 | Don’t forget to use PurgeCSS or Tailwind’s JIT mode to ensure only the classes you need are included in your production code. 31 | 32 | The styles applied by these classes don’t enable these features in any and every font. There may be some cases where the browser will try to synthesize some features like small caps, but for best (or indeed, any) results, check which feature your chosen font supports. Each OpenType feature has a corresponding four-letter code — check for these in your typeface documentation or with your font provider to see which ones are available. 33 | 34 | ## Compatibility 35 | 36 | In addition to the feature availability mentioned before, there’s also the issue of browser compatibility. Happily, browser support for OpenType features is quite good. Applying them isn’t always easy, however (something this plugin aims to address). This is primarily due to how the widely-supported `font-feature-settings` property works. 37 | 38 | ## Variants 39 | 40 | Includes no variants by default since it's unlikely you'd need to change these settings in different contexts, but hey I'm not the cops. Variants can be set for each variant group individually. 41 | -------------------------------------------------------------------------------- /docs/src/layouts/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include 'site-head.njk' %} 5 | 6 | 7 | 18 | {% include 'menu-button.njk' %} 19 |
    20 |
    21 | 80 |
    81 |
    82 |
    83 | {{ content | safe }} 84 |
    85 |
    86 |
    87 |
    88 |
    89 | 90 | 91 | -------------------------------------------------------------------------------- /docs/src/layouts/home.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | isHome: true 4 | --- 5 |

    6 | TailwindCSS × OpenType 7 |

    8 |

    9 | Tailwind CSS utility classes for advanced typographic features. 10 |

    11 |

    12 | These utilities help you make the most of the font that you are using & 13 | make your web typography truly sing. 14 |

    15 |
    16 | {{ content | safe }} 17 |
    18 | -------------------------------------------------------------------------------- /docs/src/layouts/page.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | {% from 'page-header.njk' import header with context %} 5 | {% from 'class-table.njk' import table with context %} 6 | 7 | {{ header(title=title, description=description) }} 8 | 9 | {% if classData %} 10 | {{ table(classes[classData]) }} 11 | {% endif %} 12 | 13 |
    14 | {{ content | safe }} 15 |
    16 | -------------------------------------------------------------------------------- /docs/src/public/android-chrome-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/public/android-chrome-192.png -------------------------------------------------------------------------------- /docs/src/public/android-chrome-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/public/android-chrome-512.png -------------------------------------------------------------------------------- /docs/src/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/src/public/favicon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/public/favicon-dark.png -------------------------------------------------------------------------------- /docs/src/public/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/public/favicon.ico -------------------------------------------------------------------------------- /docs/src/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/tailwindcss-opentype/326c62b18d32196222066e3af846c20021ed59be/docs/src/public/favicon.png -------------------------------------------------------------------------------- /docs/src/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/src/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenType.tw", 3 | "icons": [ 4 | { 5 | "src": "/android-chrome-192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/android-chrome-512.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "background_color": "#ffffff", 17 | "display": "standalone" 18 | } 19 | -------------------------------------------------------------------------------- /docs/src/public/pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/src/typography-kerning.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Font Kerning 4 | description: Utilities to set the use of the kerning information stored in a font. 5 | tags: typography 6 | eleventyNavigation: 7 | key: Font Kerning 8 | order: 1 9 | classData: kerning 10 | --- 11 | 12 | ## Usage 13 | 14 | Although a well-designed typeface has consistent inter-glyph spacing overall, some glyph combinations require adjustment for improved legibility. If the `letter-spacing` property is defined, kerning adjustments are considered part of the default spacing and letter spacing adjustments are made _after_ kerning has been applied. 15 | 16 | ### Kerning :feat[kern] 17 | 18 | ```html emerald 19 | 25 | 26 |

    You Will Try

    27 | ``` 28 | 29 | Kerning often defaults to `auto` in the browser, which may disable kerning at smaller font sizes. It can be disabled manually if needed using `.kerning-none`, or force-enabled using `.kerning-normal`. 30 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ 2 | import typographyPlugin from '@tailwindcss/typography' 3 | import opentypePlugin from 'tailwindcss-opentype' 4 | import colors from 'tailwindcss/colors' 5 | import defaultTheme from 'tailwindcss/defaultTheme' 6 | 7 | /** @type {import('tailwindcss').Config} */ 8 | const config = { 9 | content: [ 10 | './src/**/*.{html,md,njk}', 11 | './eleventy.config.js', 12 | './remark/*.js', 13 | ], 14 | theme: { 15 | extend: { 16 | colors: { 17 | grey: colors.gray, 18 | amber: colors.amber, 19 | orange: colors.orange, 20 | rose: colors.rose, 21 | fuchsia: colors.fuchsia, 22 | indigo: colors.indigo, 23 | lime: colors.lime, 24 | emerald: colors.emerald, 25 | teal: colors.teal, 26 | cyan: colors.cyan, 27 | sky: colors.sky, 28 | violet: colors.violet, 29 | }, 30 | 31 | typography: (theme) => ({ 32 | DEFAULT: { 33 | css: { 34 | // MaxWidth: 'none', 35 | color: theme('colors.grey.500'), 36 | '> :first-child': { marginTop: '0' }, 37 | '> :last-child': { marginBottom: '0' }, 38 | '&:first-child > :first-child': { marginTop: '0' }, 39 | '&:last-child > :last-child': { marginBottom: '0' }, 40 | 'h1, h2': { 41 | letterSpacing: '-0.025em', 42 | }, 43 | 'h2, h3': { 44 | 'scroll-margin-top': `${(70 + 40) / 16}rem`, 45 | }, 46 | code: { 47 | fontWeight: '400', 48 | color: theme('colors.violet.600'), 49 | }, 50 | 'h3 code': { 51 | fontWeight: 'inherit', 52 | color: 'inherit', 53 | }, 54 | 'h3 code::before': { content: 'none' }, 55 | 'h3 code::after': { content: 'none' }, 56 | '.code-sample p': { margin: 0 }, 57 | '.code-sample mark': { 58 | color: 'var(--accent-color)', 59 | background: 'none', 60 | }, 61 | table: { 62 | fontSize: theme('fontSize.sm')[0], 63 | lineHeight: theme('fontSize.sm')[1].lineHeight, 64 | }, 65 | thead: { 66 | color: theme('colors.grey.600'), 67 | borderBottomColor: theme('colors.grey.200'), 68 | }, 69 | 'thead th': { 70 | paddingTop: 0, 71 | fontWeight: theme('fontWeight.semibold'), 72 | }, 73 | 'tbody tr': { 74 | borderBottomColor: theme('colors.grey.200'), 75 | }, 76 | 'tbody tr:last-child': { 77 | borderBottomWidth: '1px', 78 | }, 79 | 'tbody code': { 80 | fontSize: theme('fontSize.xs')[0], 81 | }, 82 | }, 83 | }, 84 | }), 85 | 86 | spacing: { 87 | 18: '4.5rem', 88 | 88: '22rem', 89 | '15px': '0.9375rem', 90 | '23px': '1.4375rem', 91 | full: '100%', 92 | }, 93 | 94 | fontFamily: { 95 | sans: ['Inter var', ...defaultTheme.fontFamily.sans], 96 | allan: 'Allan', 97 | caflisch: 'Caflisch Script', 98 | exo: 'Exo', 99 | garamond: 'EB Garamond, serif', 100 | hypatia: 'Hypatia Sans Pro, sans-serif', 101 | goudy: 'Sorts Mill Goudy, serif', 102 | warnock: 'Warnock Pro, serif', 103 | }, 104 | 105 | width: { 106 | xl: '36rem', 107 | }, 108 | 109 | maxWidth: { 110 | '4.5xl': '60rem', 111 | '8xl': '90rem', 112 | }, 113 | 114 | maxHeight: (theme) => ({ 115 | sm: '30rem', 116 | '(screen-18)': `calc(100vh - ${theme('spacing.18')})`, 117 | }), 118 | 119 | scale: { 120 | 80: '0.8', 121 | }, 122 | }, 123 | }, 124 | variants: {}, 125 | plugins: [typographyPlugin, opentypePlugin], 126 | } 127 | 128 | export default config 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-opentype-monorepo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "homepage": "https://tailwindcss-opentype.netlify.app", 6 | "repository": "stormwarning/tailwindcss-opentype", 7 | "license": "ISC", 8 | "author": "Jeff Nelson (https://tidaltheory.io)", 9 | "type": "module", 10 | "scripts": { 11 | "build": "pnpm run -F './plugin' build", 12 | "build:docs": "pnpm run -F './docs' build", 13 | "changeset": "changeset add", 14 | "dev": "pnpm run -F './docs' dev", 15 | "prepare": "husky", 16 | "release": "npm run build && changeset publish", 17 | "test": "pnpm run -F './plugin' test" 18 | }, 19 | "lint-staged": { 20 | "*.{js,cjs,ts}": [ 21 | "eslint --fix", 22 | "prettier --write" 23 | ], 24 | "package.json": "prettier --write" 25 | }, 26 | "devDependencies": { 27 | "@changesets/cli": "2.17.0", 28 | "@types/node": "22.13.10", 29 | "@zazen/changesets-changelog": "2.0.3", 30 | "@zazen/eslint-config": "6.10.0", 31 | "@zazen/prettier-config": "1.1.1", 32 | "eslint": "8.57.1", 33 | "eslint-import-resolver-node": "0.3.9", 34 | "husky": "9.1.7", 35 | "lint-staged": "15.5.0", 36 | "prettier": "3.2.5", 37 | "prettier-plugin-jinja-template": "2.0.0", 38 | "typescript": "5.8.2", 39 | "vitest": "3.0.8" 40 | }, 41 | "packageManager": "pnpm@10.6.3", 42 | "engines": { 43 | "node": ">=16" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # tailwindcss-opentype 2 | 3 | ## 1.1.0 — 2022-08-12 4 | 5 | #### 🎁 Added 6 | 7 | - Add stylistic alternates — `salt` — utility [#90](https://github.com/stormwarning/tailwindcss-opentype/pull/90) 8 | 9 | ## 1.0.0 — 2022-03-25 10 | 11 | #### 💣 Breaking changes 12 | 13 | - Drop support for legacy AOT mode in order to support v3 [#87](https://github.com/stormwarning/tailwindcss-opentype/pull/87) 14 | 15 | Plugin no longer supports v1, may still work in v2 as long as JIT mode is enabled. 16 | 17 | ## 0.5.0 — 2022-03-24 18 | 19 | #### 🎁 Added 20 | 21 | - Add stylistic sets utilities [#84](https://github.com/stormwarning/tailwindcss-opentype/pull/84) 22 | 23 | ## 0.4.0 — 2021-09-21 24 | 25 | #### 🎁 Added 26 | 27 | - Add historical ligatures — `hlig` — utility [#69](https://github.com/stormwarning/tailwindcss-opentype/pull/69) 28 | - Add `font-kerning` utilities [#67](https://github.com/stormwarning/tailwindcss-opentype/pull/67) 29 | 30 | ## 0.3.0 — 2021-09-08 31 | 32 | #### 🎁 Added 33 | 34 | - Simplify `font-feature-settings` use in JIT-mode [#57](https://github.com/stormwarning/tailwindcss-opentype/pull/57) 35 | Allows use of low-level font feature utilities without requiring the `.font-features` class to activate. 36 | 37 | ## 0.2.0 — 2021-08-20 38 | 39 | #### 🎁 Added 40 | 41 | - Add `position` utility classes [#10](https://github.com/stormwarning/tailwindcss-opentype/pull/10) 42 | Uses the low-level `font-feature-settings` property under the hood. 43 | 44 | ## 0.1.0 — 2021-04-19 45 | 46 | #### ♻️ Changed 47 | 48 | - Update `font-variant-ligature` utilities [#6](https://github.com/stormwarning/tailwindcss-opentype/pull/6) 49 | Adds negation utilities and makes classes composable. 50 | 51 | #### 🎁 Added 52 | 53 | - Add documentation microsite [#8](https://github.com/stormwarning/tailwindcss-opentype/pull/8) 54 | 55 | ## 0.0.2 — 2021-04-07 56 | 57 | #### 🎉 Initial release! 58 | 59 | - Add initial utility classes [#4](https://github.com/stormwarning/tailwindcss-opentype/pull/4) 60 | A handful of the more common OpenType variant settings, no fallbacks yet. 61 | -------------------------------------------------------------------------------- /plugin/LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright Jeff Nelson 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /plugin/__tests__/helpers.ts: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import tailwindcss, { type Config } from 'tailwindcss' 3 | 4 | const css = String.raw 5 | 6 | export async function generateCss(testConfig: Omit) { 7 | let config: Config = { 8 | ...testConfig, 9 | content: ['./**/*.test.ts'], 10 | corePlugins: { preflight: false }, 11 | } 12 | let input = css` 13 | @tailwind utilities; 14 | ` 15 | 16 | return postcss(tailwindcss(config)).process(input, { from: undefined }) 17 | } 18 | -------------------------------------------------------------------------------- /plugin/__tests__/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | import { describe, expect, it } from 'vitest' 3 | 4 | import opentypePlugin from '../src/plugin.js' 5 | import { css, run } from './run.js' 6 | 7 | const CSS_INPUT = css` 8 | @tailwind utilities; 9 | ` 10 | 11 | describe('Plugin', () => { 12 | it('generates utility classes', async () => { 13 | let config: Config = { 14 | content: ['./**/*.test.ts'], 15 | theme: { 16 | stylisticSets: { 17 | '01': 'ss01', 18 | named: 'ss02', 19 | '03': 'ss03', 20 | }, 21 | }, 22 | plugins: [opentypePlugin], 23 | corePlugins: [], 24 | } 25 | let { css } = await run(CSS_INPUT, config) 26 | 27 | expect(css).toMatchFormattedCss(` 28 | .kerning { 29 | font-kerning: auto 30 | } 31 | 32 | .kerning-normal { 33 | font-kerning: normal 34 | } 35 | 36 | .kerning-none { 37 | font-kerning: none 38 | } 39 | 40 | .common-ligatures, .no-common-ligatures, .discretionary-ligatures, .no-discretionary-ligatures, .contextual, .no-contextual { 41 | --ot-liga: var(--tw-empty, /*!*/); 42 | --ot-dlig: var(--tw-empty, /*!*/); 43 | --ot-calt: var(--tw-empty, /*!*/); 44 | font-variant-ligatures: var(--ot-liga) var(--ot-dlig) var(--ot-calt) 45 | } 46 | 47 | .common-ligatures { 48 | --ot-liga: common-ligatures 49 | } 50 | 51 | .no-common-ligatures { 52 | --ot-liga: no-common-ligatures 53 | } 54 | 55 | .discretionary-ligatures { 56 | --ot-dlig: discretionary-ligatures 57 | } 58 | 59 | .no-discretionary-ligatures { 60 | --ot-dlig: no-discretionary-ligatures 61 | } 62 | 63 | .contextual { 64 | --ot-calt: contextual 65 | } 66 | 67 | .no-contextual { 68 | --ot-calt: no-contextual 69 | } 70 | 71 | .small-caps { 72 | font-variant-caps: small-caps 73 | } 74 | 75 | .all-small-caps { 76 | font-variant-caps: all-small-caps 77 | } 78 | 79 | .titling-caps { 80 | font-variant-caps: titling-caps 81 | } 82 | 83 | .historical-forms { 84 | font-variant-alternates: historical-forms 85 | } 86 | 87 | .sups { 88 | --ot-sups: "sups" 1; 89 | font-feature-settings: var(--ot-features); 90 | } 91 | 92 | .subs { 93 | --ot-subs: "subs" 1; 94 | font-feature-settings: var(--ot-features); 95 | } 96 | 97 | .sinf { 98 | --ot-sinf: "sinf" 1; 99 | font-feature-settings: var(--ot-features); 100 | } 101 | 102 | .hlig { 103 | --ot-hlig: "hlig" 1; 104 | font-feature-settings: var(--ot-features); 105 | } 106 | 107 | .ss-01 { 108 | --ot-ss01: "ss01" 1; 109 | font-feature-settings: var(--ot-features); 110 | } 111 | 112 | .ss-03 { 113 | --ot-ss03: "ss03" 1; 114 | font-feature-settings: var(--ot-features); 115 | } 116 | 117 | .ss-named { 118 | --ot-ss02: "ss02" 1; 119 | font-feature-settings: var(--ot-features); 120 | } 121 | `) 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /plugin/__tests__/run.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import postcss from 'postcss' 4 | import tailwind, { type Config } from 'tailwindcss' 5 | import { expect } from 'vitest' 6 | 7 | export const css = (strings: string[] | ArrayLike) => String.raw({ raw: strings }) 8 | export const html = (strings: string[] | ArrayLike) => String.raw({ raw: strings }) 9 | 10 | export function run(input: string, config: Config) { 11 | let { currentTestName } = expect.getState() 12 | 13 | return postcss(tailwind(config)).process(input, { 14 | // eslint-disable-next-line unicorn/prefer-module 15 | from: `${path.resolve(__filename)}?test=${currentTestName}`, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /plugin/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cribbed with ❤️ from tailwind-container-queries. 3 | * @see https://github.com/tailwindlabs/tailwindcss-container-queries/blob/main/jest/custom-matchers.js 4 | */ 5 | 6 | import { diff } from 'jest-diff' 7 | // eslint-disable-next-line import/default 8 | import prettier from 'prettier' 9 | import { expect } from 'vitest' 10 | 11 | expect.extend({ 12 | async toMatchFormattedCss(received: string, argument: string) { 13 | async function format(input: string) { 14 | return prettier.format(input.replaceAll('\n', ''), { 15 | parser: 'css', 16 | printWidth: 100, 17 | }) 18 | } 19 | 20 | function stripped(value: string) { 21 | return value 22 | .replace(/\/\* ! tailwindcss .* \*\//, '') 23 | .replaceAll(/\s/g, '') 24 | .replaceAll(';', '') 25 | } 26 | 27 | let options = { 28 | comment: 'stripped(received) === stripped(argument)', 29 | isNot: this.isNot, 30 | promise: this.promise, 31 | } 32 | 33 | let formattedReceived = await format(received) 34 | let formattedArgument = await format(argument) 35 | 36 | let didPass = stripped(formattedReceived) === stripped(formattedArgument) 37 | 38 | let message = didPass 39 | ? () => 40 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 41 | '\n\n' + 42 | `Expected: not ${this.utils.printExpected(formattedReceived)}\n` + 43 | `Received: ${this.utils.printReceived(formattedArgument)}` 44 | : () => { 45 | let actual = formattedReceived 46 | let expected = formattedArgument 47 | 48 | let diffString = diff(expected, actual, { 49 | expand: this.expand, 50 | }) 51 | 52 | return ( 53 | this.utils.matcherHint('toMatchCss', undefined, undefined, options) + 54 | '\n\n' + 55 | (diffString?.includes('- Expect') 56 | ? `Difference:\n\n${diffString}` 57 | : `Expected: ${this.utils.printExpected(expected)}\n` + 58 | `Received: ${this.utils.printReceived(actual)}`) 59 | ) 60 | } 61 | 62 | return { actual: received, message, pass: didPass } 63 | }, 64 | }) 65 | -------------------------------------------------------------------------------- /plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-opentype", 3 | "version": "1.1.0", 4 | "description": "Tailwind CSS utility classes for advanced typographic features.", 5 | "keywords": [ 6 | "tailwindcss", 7 | "tailwindcss-plugin", 8 | "opentype", 9 | "font features", 10 | "typography" 11 | ], 12 | "homepage": "https://tailwindcss-opentype.netlify.app", 13 | "repository": "stormwarning/tailwindcss-opentype", 14 | "license": "ISC", 15 | "author": "Jeff (https://tidaltheory.io)", 16 | "type": "commonjs", 17 | "main": "dist/index.js", 18 | "types": "dist/index.d.ts", 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "build": "tsc -b tsconfig.build.json", 24 | "prepare": "npm run build", 25 | "release": "npm run build && changeset publish", 26 | "test": "vitest run" 27 | }, 28 | "devDependencies": { 29 | "jest-diff": "29.7.0", 30 | "postcss": "8.4.23", 31 | "postcss-cli": "8.3.1", 32 | "tailwindcss": "3.3.2", 33 | "typescript": "5.8.2", 34 | "vitest": "3.0.8" 35 | }, 36 | "peerDependencies": { 37 | "tailwindcss": ">=3" 38 | }, 39 | "engines": { 40 | "node": ">=16" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import plugin from './plugin.js' 2 | 3 | export = plugin 4 | -------------------------------------------------------------------------------- /plugin/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import plugin from 'tailwindcss/plugin.js' 2 | 3 | const JIT_FONT_FEATURE_DEFAULTS = { 4 | '@defaults font-feature-settings': {}, 5 | 'font-feature-settings': 'var(--ot-features)', 6 | } 7 | 8 | const opentypePlugin = plugin.withOptions( 9 | () => 10 | function ({ addBase, addUtilities, matchUtilities, theme }) { 11 | addUtilities({ 12 | '.kerning': { 'font-kerning': 'auto' }, 13 | '.kerning-normal': { 'font-kerning': 'normal' }, 14 | '.kerning-none': { 'font-kerning': 'none' }, 15 | }) 16 | 17 | addUtilities({ 18 | '.common-ligatures, .no-common-ligatures, .discretionary-ligatures, .no-discretionary-ligatures, .contextual, .no-contextual': 19 | { 20 | '--ot-liga': 'var(--tw-empty, /*!*/)', 21 | '--ot-dlig': 'var(--tw-empty, /*!*/)', 22 | '--ot-calt': 'var(--tw-empty, /*!*/)', 23 | 'font-variant-ligatures': 'var(--ot-liga) var(--ot-dlig) var(--ot-calt)', 24 | }, 25 | '.common-ligatures': { '--ot-liga': 'common-ligatures' }, 26 | '.no-common-ligatures': { '--ot-liga': 'no-common-ligatures' }, 27 | '.discretionary-ligatures': { 28 | '--ot-dlig': 'discretionary-ligatures', 29 | }, 30 | '.no-discretionary-ligatures': { 31 | '--ot-dlig': 'no-discretionary-ligatures', 32 | }, 33 | '.contextual': { '--ot-calt': 'contextual' }, 34 | '.no-contextual': { '--ot-calt': 'no-contextual' }, 35 | }) 36 | 37 | addUtilities({ 38 | '.small-caps': { 39 | 'font-variant-caps': 'small-caps', 40 | }, 41 | '.all-small-caps': { 42 | 'font-variant-caps': 'all-small-caps', 43 | }, 44 | '.titling-caps': { 45 | 'font-variant-caps': 'titling-caps', 46 | }, 47 | }) 48 | 49 | addUtilities({ 50 | '.historical-forms': { 51 | 'font-variant-alternates': 'historical-forms', 52 | }, 53 | }) 54 | 55 | let stylisticSetsValues = 56 | theme('stylisticSets', { 57 | '01': 'ss01', 58 | '02': 'ss02', 59 | '03': 'ss03', 60 | '04': 'ss04', 61 | }) ?? {} 62 | let stylisticSetsProperties = Object.values(stylisticSetsValues).map( 63 | (tag: string) => `var(--ot-${tag})`, 64 | ) 65 | let stylisticSetsDefaults: Record = {} 66 | for (let tag of Object.values(stylisticSetsValues)) { 67 | stylisticSetsDefaults[`--ot-${tag}`] = `"${tag}" 0` 68 | } 69 | 70 | addBase({ 71 | '@defaults font-feature-settings': { 72 | '--ot-sups': '"sups" 0', 73 | '--ot-subs': '"subs" 0', 74 | '--ot-sinf': '"sinf" 0', 75 | '--ot-hlig': '"hlig" 0', 76 | '--ot-salt': '"salt" 0', 77 | ...stylisticSetsDefaults, 78 | '--ot-features': [ 79 | 'var(--ot-sups)', 80 | 'var(--ot-subs)', 81 | 'var(--ot-sinf)', 82 | 'var(--ot-hlig)', 83 | 'var(--ot-salt)', 84 | ...stylisticSetsProperties, 85 | ].join(', '), 86 | }, 87 | }) 88 | 89 | addUtilities({ 90 | '.sups': { 91 | '--ot-sups': '"sups" 1', 92 | ...JIT_FONT_FEATURE_DEFAULTS, 93 | }, 94 | '.subs': { 95 | '--ot-subs': '"subs" 1', 96 | ...JIT_FONT_FEATURE_DEFAULTS, 97 | }, 98 | '.sinf': { 99 | '--ot-sinf': '"sinf" 1', 100 | ...JIT_FONT_FEATURE_DEFAULTS, 101 | }, 102 | '.hlig': { 103 | '--ot-hlig': '"hlig" 1', 104 | ...JIT_FONT_FEATURE_DEFAULTS, 105 | }, 106 | '.salt': { 107 | '--ot-salt': '"salt" 1', 108 | ...JIT_FONT_FEATURE_DEFAULTS, 109 | }, 110 | }) 111 | 112 | matchUtilities( 113 | { 114 | ss: (value: string) => ({ 115 | [`--ot-${value}`]: `"${value}" 1`, 116 | ...JIT_FONT_FEATURE_DEFAULTS, 117 | }), 118 | }, 119 | { 120 | values: stylisticSetsValues, 121 | }, 122 | ) 123 | }, 124 | ) 125 | 126 | export default opentypePlugin 127 | -------------------------------------------------------------------------------- /plugin/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | // "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | // Modules 5 | "module": "node16", 6 | "moduleResolution": "node16", 7 | 8 | // Emit 9 | "outDir": "dist", 10 | "declaration": true, 11 | "declarationMap": false, 12 | "maxNodeModuleJsDepth": 0 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /plugin/types/vitest.d.ts: -------------------------------------------------------------------------------- 1 | import type { Assertion, AsymmetricMatchersContaining } from 'vitest' 2 | 3 | interface CustomMatchers { 4 | toMatchFormattedCss(recieved: string, argument?: string): R 5 | } 6 | 7 | declare module 'vitest' { 8 | interface Assertion extends CustomMatchers {} 9 | interface AsymmetricMatchersContaining extends CustomMatchers {} 10 | } 11 | -------------------------------------------------------------------------------- /plugin/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | setupFiles: ['./__tests__/setup.ts'], 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'plugin' 3 | - 'docs' 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from '@zazen/prettier-config' 2 | 3 | /** @type {import('prettier').Config} */ 4 | const config = { 5 | ...baseConfig, 6 | plugins: [...baseConfig.plugins, 'prettier-plugin-jinja-template'], 7 | overrides: [ 8 | ...baseConfig.overrides, 9 | { 10 | files: ['*.njk'], 11 | options: { 12 | parser: 'jinja-template', 13 | }, 14 | }, 15 | ], 16 | } 17 | 18 | export default config 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | // Type Checking 5 | "strictNullChecks": true, 6 | 7 | // Modules 8 | "module": "nodenext", 9 | "moduleResolution": "nodenext", 10 | "resolveJsonModule": true, 11 | 12 | // Emit 13 | "outDir": "dist", 14 | 15 | // JavaScript Support 16 | "allowJs": true, 17 | "checkJs": true, 18 | // Fix 11ty JSDoc types. 19 | "maxNodeModuleJsDepth": 2, 20 | 21 | // Interop Constraints 22 | "allowSyntheticDefaultImports": true, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true, 26 | 27 | // Language and Environment 28 | "target": "esnext", 29 | 30 | // Projects 31 | "incremental": true, 32 | 33 | // Completeness 34 | "skipLibCheck": true 35 | }, 36 | "include": ["docs", "plugin", ".eslintrc.cjs", "*.config.js"], 37 | "exclude": ["node_modules", "dist"] 38 | } 39 | --------------------------------------------------------------------------------