├── .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 |
22 | Easy like Sunday morning & fox
23 |
24 | Ea sy like Sunda y
25 | m orn ing &
26 | f ox
27 |
28 |
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 |
39 | Illegal
40 |
41 | Ill egal
42 |
43 |
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 |
24 | Lost lesson
25 |
26 | Lost less on
27 |
28 |
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 |
26 |
27 | Mme 3 He $ 2
28 |
29 |
30 | Mme 3 He
31 | $ 2
32 |
33 |
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 |
47 | H2 O x0
48 |
49 | H2 O x0
50 |
51 |
52 |
53 | H2 O
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 |
62 |
63 | H2 O YCb Cr νμ
64 |
65 |
66 | H2 O YCb Cr
70 | νμ
71 |
72 |
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 |
22 | Jesuit
23 |
24 | J es uit
25 |
26 |
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 |
22 | Not all caps are Small Caps.
23 |
24 | Not all caps are Small
25 | Caps .
26 |
27 |
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 |
38 | All caps are Small Caps.
39 |
40 | All caps are Small Caps .
41 |
42 |
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 |
55 | Quick, Brown, Lazy, Grumpy
56 |
57 | Qu ick, Br own, Lazy ,
58 | Gru mpy
59 |
60 |
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 |
24 |
25 | fi ff fl ffi Th
26 |
27 |
28 | fi
29 | ff
30 | fl
31 | ffi
32 | Th
33 |
34 |
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 |
45 |
46 | ct sp st
47 |
48 |
49 | ct
50 | sp
51 | st
52 |
53 |
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 |
64 |
65 | The bloom has gone off the rose
66 |
67 |
68 | The bloom has gone off the
69 | rose
70 |
71 |
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 |
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 |
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 |
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 |
20 | You Will Try
21 |
22 | Yo u Wi ll Try
23 |
24 |
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 |
--------------------------------------------------------------------------------