-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
825 additions
and
701 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,9 @@ | ||
import antfu from '@antfu/eslint-config' | ||
|
||
export default antfu() | ||
export default antfu().append({ | ||
files: ['playground/**'], | ||
rules: { | ||
'antfu/no-top-level-await': 'off', | ||
'no-console': 'off', | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,11 @@ | ||
import assert from 'node:assert' | ||
import * as pkg from 'unifont' | ||
// @ts-check | ||
|
||
// eslint-disable-next-line no-console | ||
console.log(pkg.welcome()) | ||
import { createUnifont, providers } from 'unifont' | ||
|
||
assert.strictEqual(pkg.welcome(), 'hello world') | ||
const unifont = await createUnifont([ | ||
providers.google(), | ||
]) | ||
|
||
const fonts = await unifont.resolveFontFace('Poppins') | ||
|
||
console.log(fonts) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
type Awaitable<T> = T | Promise<T> | ||
|
||
export interface Storage { | ||
getItem: (key: string) => Awaitable<any | null> | ||
setItem: (key: string, value: unknown) => Awaitable<void> | ||
} | ||
|
||
export function memoryStorage() { | ||
const cache = new Map<string, unknown>() | ||
return { | ||
getItem(key: string) { | ||
return cache.get(key) | ||
}, | ||
setItem(key: string, value: unknown) { | ||
cache.set(key, value) | ||
}, | ||
} satisfies Storage | ||
} | ||
|
||
export function createAsyncStorage(storage: Storage) { | ||
return { | ||
async getItem<T = unknown>(key: string, init?: () => Promise<T>) { | ||
return await storage.getItem(key) ?? (init ? await init() : null) | ||
}, | ||
async setItem(key: string, value: unknown) { | ||
await storage.setItem(key, value) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import type { FontFaceData, LocalFontSource, RemoteFontSource } from '../types' | ||
|
||
import { type Declaration, findAll, parse } from 'css-tree' | ||
|
||
const extractableKeyMap: Record<string, keyof FontFaceData> = { | ||
'src': 'src', | ||
'font-display': 'display', | ||
'font-weight': 'weight', | ||
'font-style': 'style', | ||
'font-feature-settings': 'featureSettings', | ||
'font-variations-settings': 'variationSettings', | ||
'unicode-range': 'unicodeRange', | ||
} | ||
|
||
const formatMap: Record<string, string> = { | ||
woff2: 'woff2', | ||
woff: 'woff', | ||
otf: 'opentype', | ||
ttf: 'truetype', | ||
eot: 'embedded-opentype', | ||
svg: 'svg', | ||
} | ||
|
||
const formatPriorityList = Object.values(formatMap) | ||
|
||
export function extractFontFaceData(css: string, family?: string): FontFaceData[] { | ||
const fontFaces: FontFaceData[] = [] | ||
|
||
for (const node of findAll(parse(css), node => node.type === 'Atrule' && node.name === 'font-face')) { | ||
if (node.type !== 'Atrule' || node.name !== 'font-face') { | ||
continue | ||
} | ||
|
||
if (family) { | ||
const isCorrectFontFace = node.block?.children.some((child) => { | ||
if (child.type !== 'Declaration' || child.property !== 'font-family') { | ||
return false | ||
} | ||
|
||
const value = extractCSSValue(child) as string | string[] | ||
const slug = family.toLowerCase() | ||
if (typeof value === 'string' && value.toLowerCase() === slug) { | ||
return true | ||
} | ||
if (Array.isArray(value) && value.length > 0 && value.some(v => v.toLowerCase() === slug)) { | ||
return true | ||
} | ||
return false | ||
}) | ||
|
||
// Don't extract font face data from this `@font-face` rule if it doesn't match the specified family | ||
if (!isCorrectFontFace) { | ||
continue | ||
} | ||
} | ||
|
||
const data: Partial<FontFaceData> = {} | ||
for (const child of node.block?.children || []) { | ||
if (child.type === 'Declaration' && child.property in extractableKeyMap) { | ||
const value = extractCSSValue(child) as any | ||
data[extractableKeyMap[child.property]!] = child.property === 'src' && !Array.isArray(value) ? [value] : value | ||
} | ||
} | ||
fontFaces.push(data as FontFaceData) | ||
} | ||
|
||
return mergeFontSources(fontFaces) | ||
} | ||
|
||
function processRawValue(value: string) { | ||
return value.split(',').map(v => v.trim().replace(/^(?<quote>['"])(.*)\k<quote>$/, '$2')) | ||
} | ||
|
||
function extractCSSValue(node: Declaration) { | ||
if (node.value.type === 'Raw') { | ||
return processRawValue(node.value.value) | ||
} | ||
|
||
const values = [] as Array<string | number | RemoteFontSource | LocalFontSource> | ||
let buffer = '' | ||
for (const child of node.value.children) { | ||
if (child.type === 'Function') { | ||
if (child.name === 'local' && child.children.first?.type === 'String') { | ||
values.push({ name: child.children.first.value }) | ||
} | ||
if (child.name === 'format' && child.children.first?.type === 'String') { | ||
(values.at(-1) as RemoteFontSource).format = child.children.first.value | ||
} | ||
if (child.name === 'tech' && child.children.first?.type === 'String') { | ||
(values.at(-1) as RemoteFontSource).tech = child.children.first.value | ||
} | ||
} | ||
if (child.type === 'Url') { | ||
values.push({ url: child.value }) | ||
} | ||
if (child.type === 'Identifier') { | ||
buffer = buffer ? `${buffer} ${child.name}` : child.name | ||
} | ||
if (child.type === 'String') { | ||
values.push(child.value) | ||
} | ||
if (child.type === 'Operator' && child.value === ',' && buffer) { | ||
values.push(buffer) | ||
buffer = '' | ||
} | ||
if (child.type === 'UnicodeRange') { | ||
values.push(child.value) | ||
} | ||
if (child.type === 'Number') { | ||
values.push(Number(child.value)) | ||
} | ||
} | ||
|
||
if (buffer) { | ||
values.push(buffer) | ||
} | ||
|
||
if (values.length === 1) { | ||
return values[0] | ||
} | ||
|
||
return values | ||
} | ||
|
||
function mergeFontSources(data: FontFaceData[]) { | ||
const mergedData: FontFaceData[] = [] | ||
for (const face of data) { | ||
const keys = Object.keys(face).filter(k => k !== 'src') as Array<keyof typeof face> | ||
const existing = mergedData.find(f => (Object.keys(f).length === keys.length + 1) && keys.every(key => f[key]?.toString() === face[key]?.toString())) | ||
if (existing) { | ||
existing.src.push(...face.src) | ||
} | ||
else { | ||
mergedData.push(face) | ||
} | ||
} | ||
|
||
// Sort font sources by priority | ||
for (const face of mergedData) { | ||
face.src.sort((a, b) => { | ||
// Prioritise local fonts (with 'name' property) over remote fonts, and then formats by formatPriorityList | ||
const aIndex = 'format' in a ? formatPriorityList.indexOf(a.format || 'woff2') : -2 | ||
const bIndex = 'format' in b ? formatPriorityList.indexOf(b.format || 'woff2') : -2 | ||
return aIndex - bIndex | ||
}) | ||
} | ||
|
||
return mergedData | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import defu from 'defu' | ||
import { fetch } from 'node-fetch-native/proxy' | ||
import { joinURL, withQuery } from 'ufo' | ||
|
||
interface Mini$FetchOptions extends RequestInit { | ||
baseURL?: string | ||
responseType?: 'json' | 'arrayBuffer' | ||
query?: Record<string, any> | ||
} | ||
|
||
function mini$fetch<T = unknown>(url: string, options?: Mini$FetchOptions) { | ||
if (options?.baseURL) { | ||
url = joinURL(options.baseURL, url) | ||
} | ||
if (options?.query) { | ||
url = withQuery(url, options.query) | ||
} | ||
return fetch(url, options) | ||
.then(r => options?.responseType === 'json' ? r.json() : options?.responseType === 'arrayBuffer' ? r.arrayBuffer() : r.text()) as Promise<T> | ||
} | ||
|
||
export const $fetch = Object.assign(mini$fetch, { | ||
create: (defaults?: Mini$FetchOptions) => <T = unknown> (url: string, options?: Mini$FetchOptions) => mini$fetch<T>(url, defu(options, defaults)), | ||
}) |
Oops, something went wrong.