full site update
This commit is contained in:
21
node_modules/unifont/LICENCE
generated
vendored
Normal file
21
node_modules/unifont/LICENCE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Daniel Roe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
80
node_modules/unifont/README.md
generated
vendored
Normal file
80
node_modules/unifont/README.md
generated
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
# unifont
|
||||
|
||||
[![npm version][npm-version-src]][npm-version-href]
|
||||
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
||||
[![Github Actions][github-actions-src]][github-actions-href]
|
||||
[![Codecov][codecov-src]][codecov-href]
|
||||
|
||||
> Framework agnostic tools for accessing data from font CDNs and providers
|
||||
|
||||
## Usage
|
||||
|
||||
Install package:
|
||||
|
||||
```sh
|
||||
# npm
|
||||
npm install unifont
|
||||
```
|
||||
|
||||
```js
|
||||
import { createUnifont, providers } from 'unifont'
|
||||
|
||||
const unifont = await createUnifont([
|
||||
providers.google(),
|
||||
])
|
||||
|
||||
const fonts = await unifont.resolveFont('Poppins')
|
||||
|
||||
console.log(fonts)
|
||||
|
||||
const availableFonts = await unifont.listFonts()
|
||||
|
||||
console.log(availableFonts)
|
||||
```
|
||||
|
||||
In most environments, you will want to cache the results of font APIs to avoid unnecessary hits to them. By default `unifont` caches font data in memory.
|
||||
|
||||
For full control, `unifont` exposes a storage API which is compatible with `unstorage`. It simply needs to expose a `getItem` and `setItem` method.
|
||||
|
||||
```ts
|
||||
import { createUnifont, providers } from 'unifont'
|
||||
|
||||
import { createStorage } from 'unstorage'
|
||||
import fsDriver from 'unstorage/drivers/fs-lite'
|
||||
|
||||
const storage = createStorage({
|
||||
driver: fsDriver({ base: 'node_modules/.cache/unifont' }),
|
||||
})
|
||||
|
||||
const cachedUnifont = await createUnifont([providers.google()], { storage })
|
||||
|
||||
console.log(await cachedUnifont.resolveFont('Poppins'))
|
||||
|
||||
// cached data is stored in `node_modules/.cache/unifont`
|
||||
```
|
||||
|
||||
For more about the storage drivers exposed from `unstorage`, check out https://unstorage.unjs.io.
|
||||
|
||||
## 💻 Development
|
||||
|
||||
- Clone this repository
|
||||
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
|
||||
- Install dependencies using `pnpm install`
|
||||
- Run interactive tests using `pnpm dev`
|
||||
|
||||
## License
|
||||
|
||||
Made with ❤️
|
||||
|
||||
Published under [MIT License](./LICENCE).
|
||||
|
||||
<!-- Badges -->
|
||||
|
||||
[npm-version-src]: https://img.shields.io/npm/v/unifont?style=flat-square
|
||||
[npm-version-href]: https://npmjs.com/package/unifont
|
||||
[npm-downloads-src]: https://img.shields.io/npm/dm/unifont?style=flat-square
|
||||
[npm-downloads-href]: https://npm.chart.dev/unifont
|
||||
[github-actions-src]: https://img.shields.io/github/actions/workflow/status/unjs/unifont/ci.yml?branch=main&style=flat-square
|
||||
[github-actions-href]: https://github.com/unjs/unifont/actions?query=workflow%3Aci
|
||||
[codecov-src]: https://img.shields.io/codecov/c/gh/unjs/unifont/main?style=flat-square
|
||||
[codecov-href]: https://codecov.io/gh/unjs/unifont
|
163
node_modules/unifont/dist/index.d.ts
generated
vendored
Normal file
163
node_modules/unifont/dist/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
type Awaitable$1<T> = T | Promise<T>;
|
||||
type StorageValue = string | Record<string, unknown>;
|
||||
interface Storage {
|
||||
getItem: (key: string) => Awaitable$1<any | null>;
|
||||
setItem: <T extends StorageValue = StorageValue>(key: string, value: T) => Awaitable$1<void>;
|
||||
}
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
interface ProviderContext {
|
||||
storage: {
|
||||
getItem: {
|
||||
<T = unknown>(key: string): Promise<T | null>;
|
||||
<T = unknown>(key: string, init: () => Awaitable<T>): Promise<T>;
|
||||
};
|
||||
setItem: (key: string, value: unknown) => Awaitable<void>;
|
||||
};
|
||||
}
|
||||
type FontStyles = 'normal' | 'italic' | 'oblique';
|
||||
interface ResolveFontOptions {
|
||||
weights: string[];
|
||||
styles: FontStyles[];
|
||||
subsets: string[];
|
||||
fallbacks?: string[];
|
||||
}
|
||||
interface RemoteFontSource {
|
||||
url: string;
|
||||
originalURL?: string;
|
||||
format?: string;
|
||||
tech?: string;
|
||||
}
|
||||
interface LocalFontSource {
|
||||
name: string;
|
||||
}
|
||||
interface FontFaceMeta {
|
||||
/** The priority of the font face, usually used to indicate fallbacks. Smaller is more prioritized. */
|
||||
priority?: number;
|
||||
/**
|
||||
* A `RequestInit` object that should be used when fetching this font. This can be useful for
|
||||
* adding authorization headers and other metadata required for a font request.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
|
||||
*/
|
||||
init?: RequestInit;
|
||||
}
|
||||
interface FontFaceData {
|
||||
src: Array<LocalFontSource | RemoteFontSource>;
|
||||
/**
|
||||
* The font-display descriptor.
|
||||
* @default 'swap'
|
||||
*/
|
||||
display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional';
|
||||
/** A font-weight value. */
|
||||
weight?: string | number | [number, number];
|
||||
/** A font-stretch value. */
|
||||
stretch?: string;
|
||||
/** A font-style value. */
|
||||
style?: string;
|
||||
/** The range of Unicode code points to be used from the font. */
|
||||
unicodeRange?: string[];
|
||||
/** Allows control over advanced typographic features in OpenType fonts. */
|
||||
featureSettings?: string;
|
||||
/** Allows low-level control over OpenType or TrueType font variations, by specifying the four letter axis names of the features to vary, along with their variation values. */
|
||||
variationSettings?: string;
|
||||
/** Metadata for the font face used by unifont */
|
||||
meta?: FontFaceMeta;
|
||||
}
|
||||
interface ResolveFontResult {
|
||||
/**
|
||||
* Return data used to generate @font-face declarations.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face
|
||||
*/
|
||||
fonts: FontFaceData[];
|
||||
fallbacks?: string[];
|
||||
}
|
||||
interface InitializedProvider {
|
||||
resolveFont: (family: string, options: ResolveFontOptions) => Awaitable<ResolveFontResult | undefined>;
|
||||
listFonts?: (() => Awaitable<string[] | undefined>) | undefined;
|
||||
}
|
||||
interface ProviderDefinition<T = unknown> {
|
||||
(options: T, ctx: ProviderContext): Awaitable<InitializedProvider | undefined>;
|
||||
}
|
||||
interface Provider {
|
||||
_name: string;
|
||||
(ctx: ProviderContext): Awaitable<InitializedProvider | undefined>;
|
||||
}
|
||||
type ProviderFactory<T = unknown> = unknown extends T ? () => Provider : Partial<T> extends T ? (options?: T) => Provider : (options: T) => Provider;
|
||||
|
||||
interface ProviderOption$2 {
|
||||
id: string[] | string;
|
||||
}
|
||||
declare const _default$5: (options: ProviderOption$2) => Provider;
|
||||
|
||||
declare const _default$4: () => Provider;
|
||||
|
||||
declare const _default$3: () => Provider;
|
||||
|
||||
declare const _default$2: () => Provider;
|
||||
|
||||
type VariableAxis = 'opsz' | 'slnt' | 'wdth' | (string & {});
|
||||
interface ProviderOption$1 {
|
||||
experimental?: {
|
||||
/**
|
||||
* Experimental: Setting variable axis configuration on a per-font basis.
|
||||
*/
|
||||
variableAxis?: {
|
||||
[key: string]: Partial<Record<VariableAxis, ([string, string] | string)[]>>;
|
||||
};
|
||||
/**
|
||||
* Experimental: Specifying a list of glyphs to be included in the font for each font family.
|
||||
* This can reduce the size of the font file.
|
||||
*/
|
||||
glyphs?: {
|
||||
[fontFamily: string]: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
declare const _default$1: (options?: ProviderOption$1 | undefined) => Provider;
|
||||
|
||||
interface ProviderOption {
|
||||
experimental?: {
|
||||
/**
|
||||
* Experimental: Specifying a list of icons to be included in the font for each font family.
|
||||
* This can reduce the size of the font file.
|
||||
*
|
||||
* **Only available when resolving the new `Material Symbols` icons.**
|
||||
*/
|
||||
glyphs?: {
|
||||
[fontFamily: string]: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
declare const _default: (options?: ProviderOption | undefined) => Provider;
|
||||
|
||||
declare namespace providers {
|
||||
export {
|
||||
_default$5 as adobe,
|
||||
_default$4 as bunny,
|
||||
_default$3 as fontshare,
|
||||
_default$2 as fontsource,
|
||||
_default$1 as google,
|
||||
_default as googleicons,
|
||||
};
|
||||
}
|
||||
|
||||
declare function defineFontProvider<T = unknown>(name: string, provider: ProviderDefinition<T>): ProviderFactory<T>;
|
||||
|
||||
interface UnifontOptions {
|
||||
storage?: Storage;
|
||||
}
|
||||
interface Unifont {
|
||||
resolveFont: (fontFamily: string, options?: Partial<ResolveFontOptions>, providers?: string[]) => Promise<ResolveFontResult & {
|
||||
provider?: string;
|
||||
}>;
|
||||
/** @deprecated use `resolveFont` */
|
||||
resolveFontFace: (fontFamily: string, options?: Partial<ResolveFontOptions>, providers?: string[]) => Promise<ResolveFontResult & {
|
||||
provider?: string;
|
||||
}>;
|
||||
listFonts: (providers?: string[]) => Promise<string[] | undefined>;
|
||||
}
|
||||
declare const defaultResolveOptions: ResolveFontOptions;
|
||||
declare function createUnifont(providers: Provider[], options?: UnifontOptions): Promise<Unifont>;
|
||||
|
||||
export { createUnifont, defaultResolveOptions, defineFontProvider, providers };
|
||||
export type { FontFaceData, FontFaceMeta, FontStyles, LocalFontSource, Provider, ProviderContext, ProviderDefinition, ProviderFactory, RemoteFontSource, ResolveFontOptions, Unifont, UnifontOptions };
|
772
node_modules/unifont/dist/index.js
generated
vendored
Normal file
772
node_modules/unifont/dist/index.js
generated
vendored
Normal file
@@ -0,0 +1,772 @@
|
||||
import { hash } from 'ohash';
|
||||
import { findAll, parse, generate } from 'css-tree';
|
||||
import { ofetch } from 'ofetch';
|
||||
|
||||
const version = "0.5.2";
|
||||
|
||||
function memoryStorage() {
|
||||
const cache = /* @__PURE__ */ new Map();
|
||||
return {
|
||||
getItem(key) {
|
||||
return cache.get(key);
|
||||
},
|
||||
setItem(key, value) {
|
||||
cache.set(key, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
const ONE_WEEK = 1e3 * 60 * 60 * 24 * 7;
|
||||
function createAsyncStorage(storage) {
|
||||
return {
|
||||
async getItem(key, init) {
|
||||
const now = Date.now();
|
||||
const res = await storage.getItem(key);
|
||||
if (res && res.expires > now && res.version === version) {
|
||||
return res.data;
|
||||
}
|
||||
if (!init) {
|
||||
return null;
|
||||
}
|
||||
const data = await init();
|
||||
await storage.setItem(key, { expires: now + ONE_WEEK, version, data });
|
||||
return data;
|
||||
},
|
||||
async setItem(key, data) {
|
||||
await storage.setItem(key, { expires: Date.now() + ONE_WEEK, version, data });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const extractableKeyMap = {
|
||||
"src": "src",
|
||||
"font-display": "display",
|
||||
"font-weight": "weight",
|
||||
"font-style": "style",
|
||||
"font-feature-settings": "featureSettings",
|
||||
"font-variations-settings": "variationSettings",
|
||||
"unicode-range": "unicodeRange"
|
||||
};
|
||||
const formatMap = {
|
||||
woff2: "woff2",
|
||||
woff: "woff",
|
||||
otf: "opentype",
|
||||
ttf: "truetype",
|
||||
eot: "embedded-opentype",
|
||||
svg: "svg"
|
||||
};
|
||||
const formatPriorityList = Object.values(formatMap);
|
||||
function extractFontFaceData(css, family) {
|
||||
const fontFaces = [];
|
||||
for (const node of findAll(parse(css), (node2) => node2.type === "Atrule" && node2.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);
|
||||
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;
|
||||
});
|
||||
if (!isCorrectFontFace) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const data = {};
|
||||
for (const child of node.block?.children || []) {
|
||||
if (child.type === "Declaration" && child.property in extractableKeyMap) {
|
||||
const value = extractCSSValue(child);
|
||||
data[extractableKeyMap[child.property]] = ["src", "unicode-range"].includes(child.property) && !Array.isArray(value) ? [value] : value;
|
||||
}
|
||||
}
|
||||
if (!data.src) {
|
||||
continue;
|
||||
}
|
||||
fontFaces.push(data);
|
||||
}
|
||||
return mergeFontSources(fontFaces);
|
||||
}
|
||||
function processRawValue(value) {
|
||||
return value.split(",").map((v) => v.trim().replace(/^(?<quote>['"])(.*)\k<quote>$/, "$2"));
|
||||
}
|
||||
function extractCSSValue(node) {
|
||||
if (node.value.type === "Raw") {
|
||||
return processRawValue(node.value.value);
|
||||
}
|
||||
const values = [];
|
||||
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") {
|
||||
if (child.children.first?.type === "String") {
|
||||
values.at(-1).format = child.children.first.value;
|
||||
} else if (child.children.first?.type === "Identifier") {
|
||||
values.at(-1).format = child.children.first.name;
|
||||
}
|
||||
}
|
||||
if (child.name === "tech") {
|
||||
if (child.children.first?.type === "String") {
|
||||
values.at(-1).tech = child.children.first.value;
|
||||
} else if (child.children.first?.type === "Identifier") {
|
||||
values.at(-1).tech = child.children.first.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 === "Dimension") {
|
||||
const dimensionValue = child.value + child.unit;
|
||||
buffer = buffer ? `${buffer} ${dimensionValue}` : dimensionValue;
|
||||
}
|
||||
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) {
|
||||
const mergedData = [];
|
||||
for (const face of data) {
|
||||
const keys = Object.keys(face).filter((k) => k !== "src");
|
||||
const existing = mergedData.find((f) => Object.keys(f).length === keys.length + 1 && keys.every((key) => f[key]?.toString() === face[key]?.toString()));
|
||||
if (existing) {
|
||||
for (const s of face.src) {
|
||||
if (existing.src.every((src) => "url" in src ? !("url" in s) || s.url !== src.url : !("name" in s) || s.name !== src.name)) {
|
||||
existing.src.push(s);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mergedData.push(face);
|
||||
}
|
||||
}
|
||||
for (const face of mergedData) {
|
||||
face.src.sort((a, b) => {
|
||||
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;
|
||||
}
|
||||
|
||||
function mini$fetch(url, options) {
|
||||
const retries = options?.retries ?? 3;
|
||||
const retryDelay = options?.retryDelay ?? 1e3;
|
||||
return ofetch(url, {
|
||||
baseURL: options?.baseURL,
|
||||
query: options?.query,
|
||||
responseType: options?.responseType ?? "text",
|
||||
headers: options?.headers,
|
||||
retry: false
|
||||
}).catch((err) => {
|
||||
if (retries <= 0) {
|
||||
throw err;
|
||||
}
|
||||
console.warn(`Could not fetch from \`${(options?.baseURL ?? "") + url}\`. Will retry in \`${retryDelay}ms\`. \`${retries}\` retries left.`);
|
||||
return new Promise((resolve) => setTimeout(resolve, retryDelay)).then(() => mini$fetch(url, { ...options, retries: retries - 1 }));
|
||||
});
|
||||
}
|
||||
const $fetch = Object.assign(mini$fetch, {
|
||||
create: (defaults) => (url, options) => mini$fetch(url, {
|
||||
...defaults,
|
||||
...options
|
||||
})
|
||||
});
|
||||
|
||||
function defineFontProvider(name, provider) {
|
||||
return (options) => Object.assign(provider.bind(null, options || {}), { _name: name });
|
||||
}
|
||||
function prepareWeights({
|
||||
inputWeights,
|
||||
weights,
|
||||
hasVariableWeights
|
||||
}) {
|
||||
const collectedWeights = [];
|
||||
for (const weight of inputWeights) {
|
||||
if (weight.includes(" ")) {
|
||||
if (hasVariableWeights) {
|
||||
collectedWeights.push(weight);
|
||||
continue;
|
||||
}
|
||||
const [min, max] = weight.split(" ");
|
||||
collectedWeights.push(
|
||||
...weights.filter((_w) => {
|
||||
const w = Number(_w);
|
||||
return w >= Number(min) && w <= Number(max);
|
||||
}).map((w) => String(w))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (weights.includes(weight)) {
|
||||
collectedWeights.push(weight);
|
||||
}
|
||||
}
|
||||
return [...new Set(collectedWeights)].map((weight) => ({
|
||||
weight,
|
||||
variable: weight.includes(" ")
|
||||
}));
|
||||
}
|
||||
|
||||
const fontCSSAPI = $fetch.create({ baseURL: "https://use.typekit.net" });
|
||||
async function getAdobeFontMeta(id) {
|
||||
const { kit } = await $fetch(`https://typekit.com/api/v1/json/kits/${id}/published`, { responseType: "json" });
|
||||
return kit;
|
||||
}
|
||||
const KIT_REFRESH_TIMEOUT = 5 * 60 * 1e3;
|
||||
const adobe = defineFontProvider("adobe", async (options, ctx) => {
|
||||
if (!options.id) {
|
||||
return;
|
||||
}
|
||||
const familyMap = /* @__PURE__ */ new Map();
|
||||
const notFoundFamilies = /* @__PURE__ */ new Set();
|
||||
const fonts = {
|
||||
kits: []
|
||||
};
|
||||
let lastRefreshKitTime;
|
||||
const kits = typeof options.id === "string" ? [options.id] : options.id;
|
||||
await fetchKits();
|
||||
async function fetchKits(bypassCache = false) {
|
||||
familyMap.clear();
|
||||
notFoundFamilies.clear();
|
||||
fonts.kits = [];
|
||||
await Promise.all(kits.map(async (id) => {
|
||||
let meta;
|
||||
const key = `adobe:meta-${id}.json`;
|
||||
if (bypassCache) {
|
||||
meta = await getAdobeFontMeta(id);
|
||||
await ctx.storage.setItem(key, meta);
|
||||
} else {
|
||||
meta = await ctx.storage.getItem(key, () => getAdobeFontMeta(id));
|
||||
}
|
||||
if (!meta) {
|
||||
throw new TypeError("No font metadata found in adobe response.");
|
||||
}
|
||||
fonts.kits.push(meta);
|
||||
for (const family of meta.families) {
|
||||
familyMap.set(family.name, family.id);
|
||||
}
|
||||
}));
|
||||
}
|
||||
async function getFontDetails(family, options2) {
|
||||
options2.weights = options2.weights.map(String);
|
||||
for (const kit of fonts.kits) {
|
||||
const font = kit.families.find((f) => f.name === family);
|
||||
if (!font) {
|
||||
continue;
|
||||
}
|
||||
const weights = prepareWeights({
|
||||
inputWeights: options2.weights,
|
||||
hasVariableWeights: false,
|
||||
weights: font.variations.map((v) => `${v.slice(-1)}00`)
|
||||
}).map((w) => w.weight);
|
||||
const styles = [];
|
||||
for (const style of font.variations) {
|
||||
if (style.includes("i") && !options2.styles.includes("italic")) {
|
||||
continue;
|
||||
}
|
||||
if (!weights.includes(String(`${style.slice(-1)}00`))) {
|
||||
continue;
|
||||
}
|
||||
styles.push(style);
|
||||
}
|
||||
if (styles.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const css = await fontCSSAPI(`/${kit.id}.css`);
|
||||
const cssName = font.css_names[0] ?? family.toLowerCase().split(" ").join("-");
|
||||
return extractFontFaceData(css, cssName).filter((font2) => {
|
||||
const [lowerWeight, upperWeight] = Array.isArray(font2.weight) ? font2.weight : [0, 0];
|
||||
return (!options2.styles || !font2.style || options2.styles.includes(font2.style)) && (!weights || !font2.weight || Array.isArray(font2.weight) ? weights.some((weight) => Number(weight) <= upperWeight || Number(weight) >= lowerWeight) : weights.includes(String(font2.weight)));
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return {
|
||||
listFonts() {
|
||||
return [...familyMap.keys()];
|
||||
},
|
||||
async resolveFont(family, options2) {
|
||||
if (notFoundFamilies.has(family)) {
|
||||
return;
|
||||
}
|
||||
if (!familyMap.has(family)) {
|
||||
const lastRefetch = lastRefreshKitTime || 0;
|
||||
const now = Date.now();
|
||||
if (now - lastRefetch > KIT_REFRESH_TIMEOUT) {
|
||||
lastRefreshKitTime = Date.now();
|
||||
await fetchKits(true);
|
||||
}
|
||||
}
|
||||
if (!familyMap.has(family)) {
|
||||
notFoundFamilies.add(family);
|
||||
return;
|
||||
}
|
||||
const fonts2 = await ctx.storage.getItem(`adobe:${family}-${hash(options2)}-data.json`, () => getFontDetails(family, options2));
|
||||
return { fonts: fonts2 };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const fontAPI$2 = $fetch.create({ baseURL: "https://fonts.bunny.net" });
|
||||
const bunny = defineFontProvider("bunny", async (_options, ctx) => {
|
||||
const familyMap = /* @__PURE__ */ new Map();
|
||||
const fonts = await ctx.storage.getItem("bunny:meta.json", () => fontAPI$2("/list", { responseType: "json" }));
|
||||
for (const [id, family] of Object.entries(fonts)) {
|
||||
familyMap.set(family.familyName, id);
|
||||
}
|
||||
async function getFontDetails(family, options) {
|
||||
const id = familyMap.get(family);
|
||||
const font = fonts[id];
|
||||
const weights = prepareWeights({
|
||||
inputWeights: options.weights,
|
||||
hasVariableWeights: false,
|
||||
weights: font.weights.map(String)
|
||||
});
|
||||
const styleMap = {
|
||||
italic: "i",
|
||||
oblique: "i",
|
||||
normal: ""
|
||||
};
|
||||
const styles = new Set(options.styles.map((i) => styleMap[i]));
|
||||
if (weights.length === 0 || styles.size === 0)
|
||||
return [];
|
||||
const resolvedVariants = weights.flatMap((w) => [...styles].map((s) => `${w.weight}${s}`));
|
||||
const css = await fontAPI$2("/css", {
|
||||
query: {
|
||||
family: `${id}:${resolvedVariants.join(",")}`
|
||||
}
|
||||
});
|
||||
return extractFontFaceData(css);
|
||||
}
|
||||
return {
|
||||
listFonts() {
|
||||
return [...familyMap.keys()];
|
||||
},
|
||||
async resolveFont(fontFamily, defaults) {
|
||||
if (!familyMap.has(fontFamily)) {
|
||||
return;
|
||||
}
|
||||
const fonts2 = await ctx.storage.getItem(`bunny:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults));
|
||||
return { fonts: fonts2 };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const fontAPI$1 = $fetch.create({ baseURL: "https://api.fontshare.com/v2" });
|
||||
const fontshare = defineFontProvider("fontshare", async (_options, ctx) => {
|
||||
const fontshareFamilies = /* @__PURE__ */ new Set();
|
||||
const fonts = await ctx.storage.getItem("fontshare:meta.json", async () => {
|
||||
const fonts2 = [];
|
||||
let offset = 0;
|
||||
let chunk;
|
||||
do {
|
||||
chunk = await fontAPI$1("/fonts", {
|
||||
responseType: "json",
|
||||
query: {
|
||||
offset,
|
||||
limit: 100
|
||||
}
|
||||
});
|
||||
fonts2.push(...chunk.fonts);
|
||||
offset++;
|
||||
} while (chunk.has_more);
|
||||
return fonts2;
|
||||
});
|
||||
for (const font of fonts) {
|
||||
fontshareFamilies.add(font.name);
|
||||
}
|
||||
async function getFontDetails(family, options) {
|
||||
const font = fonts.find((f) => f.name === family);
|
||||
const numbers = [];
|
||||
const weights = prepareWeights({
|
||||
inputWeights: options.weights,
|
||||
hasVariableWeights: false,
|
||||
weights: font.styles.map((s) => String(s.weight.weight))
|
||||
}).map((w) => w.weight);
|
||||
for (const style of font.styles) {
|
||||
if (style.is_italic && !options.styles.includes("italic")) {
|
||||
continue;
|
||||
}
|
||||
if (!style.is_italic && !options.styles.includes("normal")) {
|
||||
continue;
|
||||
}
|
||||
if (!weights.includes(String(style.weight.weight))) {
|
||||
continue;
|
||||
}
|
||||
numbers.push(style.weight.number);
|
||||
}
|
||||
if (numbers.length === 0)
|
||||
return [];
|
||||
const css = await fontAPI$1(`/css?f[]=${`${font.slug}@${numbers.join(",")}`}`);
|
||||
return extractFontFaceData(css);
|
||||
}
|
||||
return {
|
||||
listFonts() {
|
||||
return [...fontshareFamilies];
|
||||
},
|
||||
async resolveFont(fontFamily, defaults) {
|
||||
if (!fontshareFamilies.has(fontFamily)) {
|
||||
return;
|
||||
}
|
||||
const fonts2 = await ctx.storage.getItem(`fontshare:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults));
|
||||
return { fonts: fonts2 };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const fontAPI = $fetch.create({ baseURL: "https://api.fontsource.org/v1" });
|
||||
const fontsource = defineFontProvider("fontsource", async (_options, ctx) => {
|
||||
const fonts = await ctx.storage.getItem("fontsource:meta.json", () => fontAPI("/fonts", { responseType: "json" }));
|
||||
const familyMap = /* @__PURE__ */ new Map();
|
||||
for (const meta of fonts) {
|
||||
familyMap.set(meta.family, meta);
|
||||
}
|
||||
async function getFontDetails(family, options) {
|
||||
const font = familyMap.get(family);
|
||||
const weights = prepareWeights({
|
||||
inputWeights: options.weights,
|
||||
hasVariableWeights: font.variable,
|
||||
weights: font.weights.map(String)
|
||||
});
|
||||
const styles = options.styles.filter((style) => font.styles.includes(style));
|
||||
const subsets = options.subsets ? options.subsets.filter((subset) => font.subsets.includes(subset)) : [font.defSubset];
|
||||
if (weights.length === 0 || styles.length === 0)
|
||||
return [];
|
||||
const fontDetail = await fontAPI(`/fonts/${font.id}`, { responseType: "json" });
|
||||
const fontFaceData = [];
|
||||
for (const subset of subsets) {
|
||||
for (const style of styles) {
|
||||
for (const { weight, variable } of weights) {
|
||||
if (variable) {
|
||||
try {
|
||||
const variableAxes = await ctx.storage.getItem(`fontsource:${font.family}-axes.json`, () => fontAPI(`/variable/${font.id}`, { responseType: "json" }));
|
||||
if (variableAxes && variableAxes.axes.wght) {
|
||||
fontFaceData.push({
|
||||
style,
|
||||
weight: [Number(variableAxes.axes.wght.min), Number(variableAxes.axes.wght.max)],
|
||||
src: [
|
||||
{ url: `https://cdn.jsdelivr.net/fontsource/fonts/${font.id}:vf@latest/${subset}-wght-${style}.woff2`, format: "woff2" }
|
||||
],
|
||||
unicodeRange: fontDetail.unicodeRange[subset]?.split(",")
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.error(`Could not download variable axes metadata for \`${font.family}\` from \`fontsource\`. \`unifont\` will not be able to inject variable axes for ${font.family}.`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const variantUrl = fontDetail.variants[weight][style][subset].url;
|
||||
fontFaceData.push({
|
||||
style,
|
||||
weight,
|
||||
src: Object.entries(variantUrl).map(([format, url]) => ({ url, format })),
|
||||
unicodeRange: fontDetail.unicodeRange[subset]?.split(",")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return fontFaceData;
|
||||
}
|
||||
return {
|
||||
listFonts() {
|
||||
return [...familyMap.keys()];
|
||||
},
|
||||
async resolveFont(fontFamily, options) {
|
||||
if (!familyMap.has(fontFamily)) {
|
||||
return;
|
||||
}
|
||||
const fonts2 = await ctx.storage.getItem(`fontsource:${fontFamily}-${hash(options)}-data.json`, () => getFontDetails(fontFamily, options));
|
||||
return { fonts: fonts2 };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function splitCssIntoSubsets(input) {
|
||||
const data = [];
|
||||
const comments = [];
|
||||
const nodes = findAll(
|
||||
parse(input, {
|
||||
positions: true,
|
||||
// Comments are not part of the tree. We rely on the positions to infer the subset
|
||||
onComment(value, loc) {
|
||||
comments.push({ value: value.trim(), endLine: loc.end.line });
|
||||
}
|
||||
}),
|
||||
(node) => node.type === "Atrule" && node.name === "font-face"
|
||||
);
|
||||
if (comments.length === 0) {
|
||||
return [{ subset: null, css: input }];
|
||||
}
|
||||
for (const node of nodes) {
|
||||
const comment = comments.filter((comment2) => comment2.endLine < node.loc.start.line).at(-1);
|
||||
if (!comment)
|
||||
continue;
|
||||
data.push({ subset: comment.value, css: generate(node) });
|
||||
}
|
||||
return data;
|
||||
}
|
||||
const google = defineFontProvider("google", async (_options = {}, ctx) => {
|
||||
const googleFonts = await ctx.storage.getItem("google:meta.json", () => $fetch("https://fonts.google.com/metadata/fonts", { responseType: "json" }).then((r) => r.familyMetadataList));
|
||||
const styleMap = {
|
||||
italic: "1",
|
||||
oblique: "1",
|
||||
normal: "0"
|
||||
};
|
||||
const userAgents = {
|
||||
woff2: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
ttf: "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16"
|
||||
// eot: 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)',
|
||||
// woff: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0',
|
||||
// svg: 'Mozilla/4.0 (iPad; CPU OS 4_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/4.1 Mobile/9A405 Safari/7534.48.3',
|
||||
};
|
||||
async function getFontDetails(family, options) {
|
||||
const font = googleFonts.find((font2) => font2.family === family);
|
||||
const styles = [...new Set(options.styles.map((i) => styleMap[i]))].sort();
|
||||
const glyphs = _options.experimental?.glyphs?.[family]?.join("");
|
||||
const weights = prepareWeights({
|
||||
inputWeights: options.weights,
|
||||
hasVariableWeights: font.axes.some((a) => a.tag === "wght"),
|
||||
weights: Object.keys(font.fonts)
|
||||
}).map((v) => v.variable ? {
|
||||
weight: v.weight.replace(" ", ".."),
|
||||
variable: v.variable
|
||||
} : v);
|
||||
if (weights.length === 0 || styles.length === 0)
|
||||
return [];
|
||||
const resolvedAxes = [];
|
||||
let resolvedVariants = [];
|
||||
for (const axis of ["wght", "ital", ...Object.keys(_options?.experimental?.variableAxis?.[family] ?? {})].sort(googleFlavoredSorting)) {
|
||||
const axisValue = {
|
||||
wght: weights.map((v) => v.weight),
|
||||
ital: styles
|
||||
}[axis] ?? _options.experimental.variableAxis[family][axis].map((v) => Array.isArray(v) ? `${v[0]}..${v[1]}` : v);
|
||||
if (resolvedVariants.length === 0) {
|
||||
resolvedVariants = axisValue;
|
||||
} else {
|
||||
resolvedVariants = resolvedVariants.flatMap((v) => [...axisValue].map((o) => [v, o].join(","))).sort();
|
||||
}
|
||||
resolvedAxes.push(axis);
|
||||
}
|
||||
let priority = 0;
|
||||
const resolvedFontFaceData = [];
|
||||
for (const extension in userAgents) {
|
||||
const rawCss = await $fetch("/css2", {
|
||||
baseURL: "https://fonts.googleapis.com",
|
||||
headers: {
|
||||
"user-agent": userAgents[extension]
|
||||
},
|
||||
query: {
|
||||
family: `${family}:${resolvedAxes.join(",")}@${resolvedVariants.join(
|
||||
";"
|
||||
)}`,
|
||||
...glyphs && { text: glyphs }
|
||||
}
|
||||
});
|
||||
const groups = splitCssIntoSubsets(rawCss).filter((group) => group.subset ? options.subsets.includes(group.subset) : true);
|
||||
for (const group of groups) {
|
||||
const data = extractFontFaceData(group.css);
|
||||
data.map((f) => {
|
||||
f.meta ??= {};
|
||||
f.meta.priority = priority;
|
||||
return f;
|
||||
});
|
||||
resolvedFontFaceData.push(...data);
|
||||
}
|
||||
priority++;
|
||||
}
|
||||
return resolvedFontFaceData;
|
||||
}
|
||||
return {
|
||||
listFonts() {
|
||||
return googleFonts.map((font) => font.family);
|
||||
},
|
||||
async resolveFont(fontFamily, options) {
|
||||
if (!googleFonts.some((font) => font.family === fontFamily)) {
|
||||
return;
|
||||
}
|
||||
const fonts = await ctx.storage.getItem(`google:${fontFamily}-${hash(options)}-data.json`, () => getFontDetails(fontFamily, options));
|
||||
return { fonts };
|
||||
}
|
||||
};
|
||||
});
|
||||
function googleFlavoredSorting(a, b) {
|
||||
const isALowercase = a.charAt(0) === a.charAt(0).toLowerCase();
|
||||
const isBLowercase = b.charAt(0) === b.charAt(0).toLowerCase();
|
||||
if (isALowercase !== isBLowercase) {
|
||||
return Number(isBLowercase) - Number(isALowercase);
|
||||
} else {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
}
|
||||
|
||||
const googleicons = defineFontProvider("googleicons", async (_options, ctx) => {
|
||||
const googleIcons = await ctx.storage.getItem("googleicons:meta.json", async () => {
|
||||
const response = JSON.parse((await $fetch(
|
||||
"https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true"
|
||||
)).split("\n").slice(1).join("\n"));
|
||||
return response.families;
|
||||
});
|
||||
const userAgents = {
|
||||
woff2: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
ttf: "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16"
|
||||
// eot: 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)',
|
||||
// woff: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0',
|
||||
// svg: 'Mozilla/4.0 (iPad; CPU OS 4_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/4.1 Mobile/9A405 Safari/7534.48.3',
|
||||
};
|
||||
async function getFontDetails(family) {
|
||||
const iconNames = _options.experimental?.glyphs?.[family]?.sort().join(",");
|
||||
let css = "";
|
||||
for (const extension in userAgents) {
|
||||
if (family.includes("Icons")) {
|
||||
css += await $fetch("/icon", {
|
||||
baseURL: "https://fonts.googleapis.com",
|
||||
headers: { "user-agent": userAgents[extension] },
|
||||
query: {
|
||||
family
|
||||
}
|
||||
});
|
||||
} else {
|
||||
css += await $fetch("/css2", {
|
||||
baseURL: "https://fonts.googleapis.com",
|
||||
headers: { "user-agent": userAgents[extension] },
|
||||
query: {
|
||||
family: `${family}:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200`,
|
||||
...iconNames && { icon_names: iconNames }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return extractFontFaceData(css);
|
||||
}
|
||||
return {
|
||||
listFonts() {
|
||||
return googleIcons;
|
||||
},
|
||||
async resolveFont(fontFamily, options) {
|
||||
if (!googleIcons.includes(fontFamily)) {
|
||||
return;
|
||||
}
|
||||
const fonts = await ctx.storage.getItem(`googleicons:${fontFamily}-${hash(options)}-data.json`, () => getFontDetails(fontFamily));
|
||||
return { fonts };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const providers = {
|
||||
__proto__: null,
|
||||
adobe: adobe,
|
||||
bunny: bunny,
|
||||
fontshare: fontshare,
|
||||
fontsource: fontsource,
|
||||
google: google,
|
||||
googleicons: googleicons
|
||||
};
|
||||
|
||||
const defaultResolveOptions = {
|
||||
weights: ["400"],
|
||||
styles: ["normal", "italic"],
|
||||
subsets: [
|
||||
"cyrillic-ext",
|
||||
"cyrillic",
|
||||
"greek-ext",
|
||||
"greek",
|
||||
"vietnamese",
|
||||
"latin-ext",
|
||||
"latin"
|
||||
]
|
||||
};
|
||||
async function createUnifont(providers2, options) {
|
||||
const stack = {};
|
||||
const unifontContext = {
|
||||
storage: createAsyncStorage(options?.storage ?? memoryStorage())
|
||||
};
|
||||
for (const provider of providers2) {
|
||||
stack[provider._name] = void 0;
|
||||
}
|
||||
await Promise.all(providers2.map(async (provider) => {
|
||||
try {
|
||||
const initializedProvider = await provider(unifontContext);
|
||||
if (initializedProvider)
|
||||
stack[provider._name] = initializedProvider;
|
||||
} catch (err) {
|
||||
console.error(`Could not initialize provider \`${provider._name}\`. \`unifont\` will not be able to process fonts provided by this provider.`, err);
|
||||
}
|
||||
if (!stack[provider._name]?.resolveFont) {
|
||||
delete stack[provider._name];
|
||||
}
|
||||
}));
|
||||
const allProviders = Object.keys(stack);
|
||||
async function resolveFont(fontFamily, options2, providers3 = allProviders) {
|
||||
const mergedOptions = { ...defaultResolveOptions, ...options2 };
|
||||
for (const id of providers3) {
|
||||
const provider = stack[id];
|
||||
try {
|
||||
const result = await provider?.resolveFont(fontFamily, mergedOptions);
|
||||
if (result) {
|
||||
return {
|
||||
provider: id,
|
||||
...result
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Could not resolve font face for \`${fontFamily}\` from \`${id}\` provider.`, err);
|
||||
}
|
||||
}
|
||||
return { fonts: [] };
|
||||
}
|
||||
async function listFonts(providers3 = allProviders) {
|
||||
let names;
|
||||
for (const id of providers3) {
|
||||
const provider = stack[id];
|
||||
try {
|
||||
const result = await provider?.listFonts?.();
|
||||
if (result) {
|
||||
names ??= [];
|
||||
names.push(...result);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Could not list names from \`${id}\` provider.`, err);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
return {
|
||||
resolveFont,
|
||||
// TODO: remove before v1
|
||||
resolveFontFace: resolveFont,
|
||||
listFonts
|
||||
};
|
||||
}
|
||||
|
||||
export { createUnifont, defaultResolveOptions, defineFontProvider, providers };
|
65
node_modules/unifont/package.json
generated
vendored
Normal file
65
node_modules/unifont/package.json
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "unifont",
|
||||
"type": "module",
|
||||
"version": "0.5.2",
|
||||
"packageManager": "pnpm@10.12.2",
|
||||
"description": "Framework agnostic tools for accessing data from font CDNs and providers",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/unjs/unifont.git"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "unbuild",
|
||||
"dev": "vitest dev",
|
||||
"lint": "eslint .",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepack": "pnpm build",
|
||||
"prepublishOnly": "pnpm lint && pnpm test",
|
||||
"release": "bumpp && npm publish",
|
||||
"test": "pnpm test:unit --coverage && pnpm test:types",
|
||||
"test:unit": "vitest",
|
||||
"test:types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"ohash": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "4.16.1",
|
||||
"@types/css-tree": "2.3.10",
|
||||
"@types/node": "22.15.32",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"bumpp": "10.2.0",
|
||||
"eslint": "9.29.0",
|
||||
"lint-staged": "16.1.2",
|
||||
"simple-git-hooks": "2.13.0",
|
||||
"typescript": "5.8.3",
|
||||
"unbuild": "3.5.0",
|
||||
"unstorage": "1.16.0",
|
||||
"vite": "6.3.5",
|
||||
"vitest": "3.2.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"unifont": "link:."
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npx lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,mjs,cjs,json,.*rc}": [
|
||||
"npx eslint --fix"
|
||||
]
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user