Processing fallback fonts in the correct order

This commit is contained in:
Marcel Mraz 2024-08-27 17:49:50 +02:00
parent f40a8d0cab
commit 8ad59687bb
19 changed files with 280 additions and 122 deletions

View File

@ -66,11 +66,11 @@ export const FontPickerList = React.memo(
.filter(
([_, { metadata }]) => !metadata.serverSide && !metadata.fallback,
)
.map(([familyId, { metadata, fonts }]) => {
.map(([familyId, { metadata, fontFaces }]) => {
const fontDescriptor = {
value: familyId,
icon: metadata.icon ?? FontFamilyNormalIcon,
text: fonts[0].fontFace.family,
text: fontFaces[0]?.fontFace?.family ?? "Unknown",
};
if (metadata.deprecated) {

View File

@ -114,6 +114,9 @@ export const CLASSES = {
SHAPE_ACTIONS_MENU: "App-menu__left",
};
export const CHINESE_HANDWRITTEN_FALLBACK_FONT = "Xiaolai";
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
/**
* // TODO: shouldn't be really `const`, likely neither have integers as values, due to value for the custom fonts, which should likely be some hash.
*
@ -133,7 +136,17 @@ export const FONT_FAMILY = {
"Comic Shanns": 8,
"Liberation Sans": 9,
// from here on fallback fonts only
Xiaolai: 1000,
[CHINESE_HANDWRITTEN_FALLBACK_FONT]: 1000,
[WINDOWS_EMOJI_FALLBACK_FONT]: 9999,
};
// TODO: perahaps could be specific per-family in the future
export const FONT_FAMILY_FALLBACKS = {
string: `${CHINESE_HANDWRITTEN_FALLBACK_FONT}, ${WINDOWS_EMOJI_FALLBACK_FONT}`,
ordered: [
FONT_FAMILY[CHINESE_HANDWRITTEN_FALLBACK_FONT],
FONT_FAMILY[WINDOWS_EMOJI_FALLBACK_FONT],
],
};
export const THEME = {
@ -157,10 +170,6 @@ export const FRAME_STYLE = {
nameLineHeight: 1.25,
};
// TODO: consider adding a built-in fallback font for emojis
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
export const CHINESE_HANDWRITTEN_FALLBACK_FONT = "Xiaolai";
export const MIN_FONT_SIZE = 1;
export const DEFAULT_FONT_SIZE = 20;
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Excalifont;

View File

@ -3,31 +3,30 @@ import { LOCAL_FONT_PROTOCOL } from "./metadata";
import loadWoff2 from "./wasm/woff2.loader";
import loadHbSubset from "./wasm/hb-subset.loader";
export interface Font {
export interface IExcalidrawFontFace {
urls: URL[];
fontFace: FontFace;
getContent(codePoints: ReadonlySet<number>): Promise<string>;
toCSS(
characters: string,
codePoints: Set<number>,
): Promise<string> | undefined;
}
export const UNPKG_FALLBACK_URL = `https://unpkg.com/${
import.meta.env.VITE_PKG_NAME
? `${import.meta.env.VITE_PKG_NAME}@${import.meta.env.PKG_VERSION}` // should be provided by vite during package build
: "@excalidraw/excalidraw" // fallback to latest package version (i.e. for app)
}/dist/prod/`;
export interface ExcalidrawFontFace {
uri: string;
descriptors?: FontFaceDescriptors;
};
export class ExcalidrawFont implements Font {
export class ExcalidrawFontFace implements IExcalidrawFontFace {
public readonly urls: URL[];
public readonly fontFace: FontFace;
private static readonly UNPKG_FALLBACK_URL = `https://unpkg.com/${
import.meta.env.VITE_PKG_NAME
? `${import.meta.env.VITE_PKG_NAME}@${import.meta.env.PKG_VERSION}` // should be provided by vite during package build
: "@excalidraw/excalidraw" // fallback to latest package version (i.e. for app)
}/dist/prod/`;
constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
this.urls = ExcalidrawFont.createUrls(uri);
this.urls = ExcalidrawFontFace.createUrls(uri);
const sources = this.urls
.map((url) => `url(${url}) ${ExcalidrawFont.getFormat(url)}`)
.map((url) => `url(${url}) ${ExcalidrawFontFace.getFormat(url)}`)
.join(", ");
this.fontFace = new FontFace(family, sources, {
@ -38,6 +37,41 @@ export class ExcalidrawFont implements Font {
});
}
/**
* Generates CSS `@font-face` definition with the (subsetted) font source as a data url for the characters within the unicode range.
*
* Retrieves `undefined` otherwise.
*/
public toCSS(
characters: string,
codePoints: Set<number>,
): Promise<string> | undefined {
const unicodeRangeRegex = this.fontFace.unicodeRange
.split(", ")
.map((range) => {
const [start, end] = range.replace("U+", "").split("-");
if (end) {
return `\\u${start}-\\u${end}`;
}
return `\\u${start}`;
})
.join("");
if (!new RegExp(`[${unicodeRangeRegex}]`).exec(characters)) {
// quick exit, so that this does not count as a pending promsie
return;
}
return new Promise<string>((resolve) => {
this.getContent(codePoints).then((content) => {
resolve(`@font-face {
font-family: ${this.fontFace.family};
src: url(${content});
}`);
});
});
}
/**
* Tries to fetch woff2 content, based on the registered urls (from first to last, treated as fallbacks).
*
@ -45,29 +79,29 @@ export class ExcalidrawFont implements Font {
*
* @returns base64 with subsetted glyphs based on the passed codepoint, last defined url otherwise
*/
public async getContent(codePoints: ReadonlySet<number>): Promise<string> {
private async getContent(codePoints: ReadonlySet<number>): Promise<string> {
let i = 0;
const errorMessages = [];
while (i < this.urls.length) {
const url = this.urls[i];
// it's dataurl (server), the font is inlined as base64, no need to fetch
if (url.protocol === "data:") {
const arrayBuffer = Buffer.from(
url.toString().split(",")[1],
"base64",
).buffer;
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
arrayBuffer,
codePoints,
);
return base64;
}
try {
// it's dataurl (server), the font is inlined as base64, no need to fetch
if (url.protocol === "data:") {
const arrayBuffer = Buffer.from(
url.toString().split(",")[1],
"base64",
).buffer;
const base64 = await ExcalidrawFontFace.subsetGlyphsByCodePoints(
arrayBuffer,
codePoints,
);
return base64;
}
const response = await fetch(url, {
headers: {
Accept: "font/woff2",
@ -76,7 +110,7 @@ export class ExcalidrawFont implements Font {
if (response.ok) {
const arrayBuffer = await response.arrayBuffer();
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
const base64 = await ExcalidrawFontFace.subsetGlyphsByCodePoints(
arrayBuffer,
codePoints,
);
@ -128,11 +162,11 @@ export class ExcalidrawFont implements Font {
const subsetSnft = subset(decompressedBinary, codePoints);
const compressedBinary = compress(subsetSnft.buffer);
return ExcalidrawFont.toBase64(compressedBinary.buffer);
return ExcalidrawFontFace.toBase64(compressedBinary.buffer);
} catch (e) {
console.error("Skipped glyph subsetting", e);
// Fallback to encoding whole font in case of errors
return ExcalidrawFont.toBase64(arrayBuffer);
return ExcalidrawFontFace.toBase64(arrayBuffer);
}
}
@ -178,7 +212,7 @@ export class ExcalidrawFont implements Font {
}
// fallback url for bundled fonts
urls.push(new URL(assetUrl, UNPKG_FALLBACK_URL));
urls.push(new URL(assetUrl, ExcalidrawFontFace.UNPKG_FALLBACK_URL));
return urls;
}

View File

@ -10,12 +10,11 @@ import { isTextElement } from "../element";
import { getFontString } from "../utils";
import { FONT_FAMILY } from "../constants";
import { FONT_METADATA, type FontMetadata } from "./metadata";
import {
ExcalidrawFont,
type ExcalidrawFontFace,
type Font,
} from "./ExcalidrawFont";
import { getContainerElement } from "../element/textElement";
import {
ExcalidrawFontFace,
type IExcalidrawFontFace,
} from "./ExcalidrawFontFace";
import { CascadiaFontFaces } from "./woff2/Cascadia";
import { ComicFontFaces } from "./woff2/Comic";
import { ExcalifontFontFaces } from "./woff2/Excalifont";
@ -25,6 +24,7 @@ import { LilitaFontFaces } from "./woff2/Lilita";
import { NunitoFontFaces } from "./woff2/Nunito";
import { VirgilFontFaces } from "./woff2/Virgil";
import { XiaolaiFontFaces } from "./woff2/Xiaolai";
import { EmojiFontFaces } from "./woff2/Emoji";
export class Fonts {
// it's ok to track fonts across multiple instances only once, so let's use
@ -36,7 +36,7 @@ export class Fonts {
number,
{
metadata: FontMetadata;
fonts: Font[];
fontFaces: IExcalidrawFontFace[];
}
>
| undefined;
@ -148,13 +148,13 @@ export class Fonts {
fontFamilies: Array<ExcalidrawTextElement["fontFamily"]>,
) {
// add all registered font faces into the `document.fonts` (if not added already)
for (const { fonts, metadata } of Fonts.registered.values()) {
for (const { fontFaces, metadata } of Fonts.registered.values()) {
// skip registering font faces for local fonts (i.e. Helvetica)
if (metadata.local) {
continue;
}
for (const { fontFace } of fonts) {
for (const { fontFace } of fontFaces) {
if (!window.document.fonts.has(fontFace)) {
window.document.fonts.add(fontFace);
}
@ -179,7 +179,7 @@ export class Fonts {
console.error(
`Failed to load font "${fontString}" from urls "${Fonts.registered
.get(fontFamily)
?.fonts.map((x) => x.urls)}"`,
?.fontFaces.map((x) => x.urls)}"`,
e,
);
}
@ -199,17 +199,17 @@ export class Fonts {
const fonts = {
registered: new Map<
ValueOf<typeof FONT_FAMILY>,
{ metadata: FontMetadata; fonts: Font[] }
{ metadata: FontMetadata; fontFaces: IExcalidrawFontFace[] }
>(),
};
const init = (
family: keyof typeof FONT_FAMILY,
...fontFaces: ExcalidrawFontFace[]
...fontFacesDescriptors: ExcalidrawFontFaceDescriptor[]
) => {
const metadata = FONT_METADATA[FONT_FAMILY[family]];
register.call(fonts, family, metadata, ...fontFaces);
register.call(fonts, family, metadata, ...fontFacesDescriptors);
};
init("Cascadia", ...CascadiaFontFaces);
@ -222,7 +222,10 @@ export class Fonts {
init("Lilita One", ...LilitaFontFaces);
init("Nunito", ...NunitoFontFaces);
init("Virgil", ...VirgilFontFaces);
// fallback font faces
init("Xiaolai", ...XiaolaiFontFaces);
init("Segoe UI Emoji", ...EmojiFontFaces);
Fonts._initialized = true;
@ -248,7 +251,7 @@ export class Fonts {
*
* @param family font family
* @param metadata font metadata
* @param faces font faces
* @param fontFacesDecriptors font faces descriptors
*/
function register(
this:
@ -256,12 +259,12 @@ function register(
| {
registered: Map<
ValueOf<typeof FONT_FAMILY>,
{ metadata: FontMetadata; fonts: Font[] }
{ metadata: FontMetadata; fontFaces: IExcalidrawFontFace[] }
>;
},
family: string,
metadata: FontMetadata,
...faces: ExcalidrawFontFace[]
...fontFacesDecriptors: ExcalidrawFontFaceDescriptor[]
) {
// TODO: likely we will need to abandon number "id" in order to support custom fonts
const familyId = FONT_FAMILY[family as keyof typeof FONT_FAMILY];
@ -270,8 +273,9 @@ function register(
if (!registeredFamily) {
this.registered.set(familyId, {
metadata,
fonts: faces.map(
({ uri, descriptors }) => new ExcalidrawFont(family, uri, descriptors),
fontFaces: fontFacesDecriptors.map(
({ uri, descriptors }) =>
new ExcalidrawFontFace(family, uri, descriptors),
),
});
}
@ -309,3 +313,8 @@ export const getLineHeight = (fontFamily: FontFamilyValues) => {
return lineHeight as ExcalidrawTextElement["lineHeight"];
};
export interface ExcalidrawFontFaceDescriptor {
uri: string;
descriptors?: FontFaceDescriptors;
}

View File

@ -119,6 +119,17 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
},
fallback: true,
},
[FONT_FAMILY["Segoe UI Emoji"]]: {
metrics: {
// reusing Excalifont metrics
unitsPerEm: 1000,
ascender: 886,
descender: -374,
lineHeight: 1.25,
},
local: true,
fallback: true,
},
};
/** Unicode ranges defined by google fonts */

View File

@ -1,7 +1,7 @@
import CascadiaCodeRegular from "./CascadiaCode-Regular.woff2";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const CascadiaFontFaces: ExcalidrawFontFace[] = [
export const CascadiaFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: CascadiaCodeRegular,
},

View File

@ -1,7 +1,7 @@
import ComicShannsRegular from "./ComicShanns-Regular.woff2";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const ComicFontFaces: ExcalidrawFontFace[] = [
export const ComicFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: ComicShannsRegular,
},

View File

@ -0,0 +1,8 @@
import { LOCAL_FONT_PROTOCOL } from "../../metadata";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const EmojiFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: LOCAL_FONT_PROTOCOL,
},
];

View File

@ -1,7 +1,7 @@
import Excalifont from "./Excalifont-Regular.woff2";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const ExcalifontFontFaces: ExcalidrawFontFace[] = [
export const ExcalifontFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: Excalifont,
},

View File

@ -1,7 +1,7 @@
import { LOCAL_FONT_PROTOCOL } from "../../metadata";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const HelveticaFontFaces: ExcalidrawFontFace[] = [
export const HelveticaFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: LOCAL_FONT_PROTOCOL,
},

View File

@ -1,7 +1,7 @@
import LiberationSansRegular from "./LiberationSans-Regular.woff2";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const LiberationFontFaces: ExcalidrawFontFace[] = [
export const LiberationFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: LiberationSansRegular,
},

View File

@ -2,9 +2,9 @@ import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WB
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
import { GOOGLE_FONTS_RANGES } from "../../metadata";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const LilitaFontFaces: ExcalidrawFontFace[] = [
export const LilitaFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: LilitaLatinExt,
descriptors: { unicodeRange: GOOGLE_FONTS_RANGES.LATIN_EXT },

View File

@ -5,9 +5,9 @@ import CyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiO
import Vietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
import { GOOGLE_FONTS_RANGES } from "../../metadata";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const NunitoFontFaces: ExcalidrawFontFace[] = [
export const NunitoFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: CyrilicExt,
descriptors: {

View File

@ -1,7 +1,7 @@
import Virgil from "./Virgil-Regular.woff2";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
export const VirgilFontFaces: ExcalidrawFontFace[] = [
export const VirgilFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: Virgil,
},

View File

@ -218,7 +218,7 @@ import _213 from "./31dc7ab58ea136b7dca23037305e023b.woff2";
import _214 from "./d53f5bd45db0e96874d1174341701b59.woff2";
import _215 from "./6fe856ceea3ef870142b02ddecc1b006.woff2";
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
import { type ExcalidrawFontFaceDescriptor } from "../..";
/* Generated By cn-font-split@5.2.1 https://www.npmjs.com/package/cn-font-split
CreateTime: Thu, 22 Aug 2024 10:58:35 GMT;
@ -240,7 +240,7 @@ licenseURL: http://scripts.sil.org/OFL
*/
// TODO_CHINESE: consider having import for each (to automate build step)
export const XiaolaiFontFaces: ExcalidrawFontFace[] = [
export const XiaolaiFontFaces: ExcalidrawFontFaceDescriptor[] = [
{
uri: _0,
descriptors: {

View File

@ -67,6 +67,7 @@
"canvas-roundrect-polyfill": "0.0.1",
"clsx": "1.1.1",
"cross-env": "7.0.3",
"es6-promise-pool": "2.5.0",
"fractional-indexing": "3.2.0",
"fuzzy": "0.1.3",
"image-blob-reduce": "3.0.1",

View File

@ -9,7 +9,13 @@ import type {
import type { Bounds } from "../element/bounds";
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
import { renderSceneToSvg } from "../renderer/staticSvgScene";
import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
import {
arrayToMap,
distance,
getFontString,
PromisePool,
toBrandedType,
} from "../utils";
import type { AppState, BinaryFiles } from "../types";
import {
DEFAULT_EXPORT_PADDING,
@ -18,6 +24,7 @@ import {
SVG_NS,
THEME,
THEME_FILTER,
FONT_FAMILY_FALLBACKS,
} from "../constants";
import { getDefaultAppState } from "../appState";
import { serializeAsJSON } from "../data/json";
@ -39,7 +46,6 @@ import type { RenderableElementsMap } from "./types";
import { syncInvalidIndices } from "../fractionalIndex";
import { renderStaticScene } from "../renderer/staticScene";
import { Fonts } from "../fonts";
import type { Font } from "../fonts/ExcalidrawFont";
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
@ -439,6 +445,7 @@ const getFontFaces = async (
elements: readonly ExcalidrawElement[],
): Promise<string[]> => {
const fontFamilies = new Set<number>();
const characters = new Set<string>();
const codePoints = new Set<number>();
for (const element of elements) {
@ -449,52 +456,85 @@ const getFontFaces = async (
fontFamilies.add(element.fontFamily);
// gather unique codepoints only when inlining fonts
for (const codePoint of Array.from(element.originalText, (u) =>
u.codePointAt(0),
)) {
if (codePoint) {
codePoints.add(codePoint);
for (const char of element.originalText) {
if (!characters.has(char)) {
characters.add(char);
codePoints.add(char.codePointAt(0)!);
}
}
}
const getSource = (font: Font) => {
try {
// retrieve font source as dataurl based on the used codepoints
return font.getContent(codePoints);
} catch {
// fallback to font source as a url
return font.urls[0].toString();
}
};
const uniqueChars = Array.from(characters).join("");
const fontFaces = await Promise.all(
Array.from(fontFamilies).map(async (x) => {
const { fonts, metadata } = Fonts.registered.get(x) ?? {};
// quick check for Han (might match a bit more, but that's fine)
if (uniqueChars.match(/\p{Script=Han}/u)) {
fontFamilies.add(FONT_FAMILY.Xiaolai);
}
if (!Array.isArray(fonts)) {
console.error(
`Couldn't find registered fonts for font-family "${x}"`,
Fonts.registered,
);
return [];
}
const iterator = fontFacesIterator(fontFamilies, uniqueChars, codePoints);
if (metadata?.local) {
// don't inline local fonts
return [];
}
// don't trigger hundreds/thousands of concurrent requests, instead go 3 requests at a time, in a controlled manner
// in other words, does not block the main thread and avoids related issues, including potential rate limits
const concurrency = 3;
const fontFaces = await new PromisePool(iterator, concurrency).all();
return Promise.all(
fonts.map(
async (font) => `@font-face {
font-family: ${font.fontFace.family};
src: url(${await getSource(font)});
}`,
),
);
}),
);
return fontFaces.flat();
// dedup just in case (i.e. could be the same font faces with 0 glyphs)
return Array.from(new Set(fontFaces));
};
function* fontFacesIterator(
families: Set<number>,
characters: string,
codePoints: Set<number>,
): Generator<Promise<[number, string]>> {
// the order between the families is important, as fallbacks need to be defined first and in the reversed order
// so that they get overriden with the later defined font faces, i.e. in case they share some codepoints
const reversedFallbacks = Array.from(FONT_FAMILY_FALLBACKS.ordered).reverse();
for (const [familyIndex, family] of Array.from(families).entries()) {
const fallbackIndex = reversedFallbacks.findIndex(
(fallback) => fallback === family,
);
let order: number;
if (fallbackIndex !== -1) {
// making sure the fallback fonts are defined first
order = fallbackIndex;
} else {
// making sure the built-in font faces are always defined after fallback fonts
order = FONT_FAMILY_FALLBACKS.ordered.length + familyIndex;
}
// making a safe buffer in between the families, assuming there won't be more than 10k font faces per family
order *= 10_000;
const { fontFaces, metadata } = Fonts.registered.get(family) ?? {};
if (!Array.isArray(fontFaces)) {
console.error(
`Couldn't find registered fonts for font-family "${family}"`,
Fonts.registered,
);
continue;
}
if (metadata?.local) {
// don't inline local fonts
continue;
}
for (const [fontFaceIndex, fontFace] of fontFaces.entries()) {
const pendingFontFace = fontFace.toCSS(characters, codePoints);
// so that we don't add undefined to the pool
if (pendingFontFace) {
yield new Promise(async (resolve) => {
const promiseIndex = order + fontFaceIndex;
const fontFaceCSS = await pendingFontFace;
resolve([promiseIndex, fontFaceCSS]);
});
}
}
}
}

View File

@ -1,9 +1,10 @@
import Pool from "es6-promise-pool";
import { COLOR_PALETTE } from "./colors";
import type { EVENT } from "./constants";
import {
CHINESE_HANDWRITTEN_FALLBACK_FONT,
DEFAULT_VERSION,
FONT_FAMILY,
FONT_FAMILY_FALLBACKS,
isDarwin,
WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants";
@ -89,8 +90,8 @@ export const getFontFamilyString = ({
}) => {
for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
if (id === fontFamily) {
// TODO: we should fallback first to generic family names first, rather than directly to the emoji font
return `${fontFamilyString}, ${CHINESE_HANDWRITTEN_FALLBACK_FONT}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
// TODO: we should fallback first to generic family names first
return `${fontFamilyString}, ${FONT_FAMILY_FALLBACKS.string}`;
}
}
return WINDOWS_EMOJI_FALLBACK_FONT;
@ -1167,3 +1168,43 @@ export const promiseTry = async <TValue, TArgs extends unknown[]>(
export const isAnyTrue = (...args: boolean[]): boolean =>
Math.max(...args.map((arg) => (arg ? 1 : 0))) > 0;
// extending the missing types
// relying on the [Index, T] to keep a correct order
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
addEventListener: (
type: "fulfilled",
listener: (event: { data: { result: [Index, T] } }) => void,
) => (event: { data: { result: [Index, T] } }) => void;
removeEventListener: (
type: "fulfilled",
listener: (event: { data: { result: [Index, T] } }) => void,
) => void;
};
export class PromisePool<T> {
private readonly pool: TPromisePool<T>;
private readonly entries: Record<number, T> = {};
constructor(
source: IterableIterator<Promise<[number, T]>>,
concurrency: number,
) {
this.pool = new Pool(
source as unknown as () => void | PromiseLike<[number, T][]>,
concurrency,
) as TPromisePool<T>;
}
public all() {
const listener = this.pool.addEventListener("fulfilled", (event) => {
const [index, value] = event.data.result;
this.entries[index] = value;
});
return this.pool.start().then(() => {
setTimeout(() => this.pool.removeEventListener("fulfilled", listener));
return Object.values(this.entries);
});
}
}

View File

@ -5521,6 +5521,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es6-promise-pool@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/es6-promise-pool/-/es6-promise-pool-2.5.0.tgz#147c612b36b47f105027f9d2bf54a598a99d9ccb"
integrity sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==
esbuild-plugin-external-global@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/esbuild-plugin-external-global/-/esbuild-plugin-external-global-1.0.1.tgz#e3bba0e3a561f61b395bec0984a90ed0de06c4ce"