Compare commits

...

7 Commits

Author SHA1 Message Date
are b51f6d178c fix: checkbox position misaligned caused by margin 2025-01-31 00:38:46 +01:00
Shalini 84bab403ff Fix: issue #8818 Xiaolai font has been set as a fallback for Excalifont (#9055)
Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com>
2025-01-30 13:41:41 +00:00
Are 61e0bb83d0 feat: improve library sidebar performance (#9060)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-01-30 14:41:08 +01:00
Saikat Das bd1590fc74 feat: implement custom Range component for opacity control (#9009)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-01-29 21:46:40 +00:00
Marcel Mraz d29c3db7f6 fix: fonts not loading on export (again) (#9064) 2025-01-29 22:24:26 +01:00
Marcel Mraz a58822c1c1 fix: merge server-side fonts with liberation sans (#9052) 2025-01-29 22:04:49 +01:00
David Luzar a3e1619635 fix: hyperlinks html entities (#9063) 2025-01-29 19:02:54 +01:00
19 changed files with 237 additions and 140 deletions
@@ -121,6 +121,7 @@ import {
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { pointFrom } from "../../math"; import { pointFrom } from "../../math";
import { Range } from "../components/Range";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@@ -630,25 +631,12 @@ export const actionChangeOpacity = register({
}; };
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<label className="control-label"> <Range
{t("labels.opacity")} updateData={updateData}
<input elements={elements}
type="range" appState={appState}
min="0" testId="opacity"
max="100" />
step="10"
onChange={(event) => updateData(+event.target.value)}
value={
getFormValue(
elements,
appState,
(element) => element.opacity,
true,
appState.currentItemOpacity,
) ?? undefined
}
/>
</label>
), ),
}); });
+44 -20
View File
@@ -1,4 +1,10 @@
import React, { useState, useCallback, useMemo, useRef } from "react"; import React, {
useState,
useCallback,
useMemo,
useRef,
useEffect,
} from "react";
import type Library from "../data/library"; import type Library from "../data/library";
import { import {
distributeLibraryItemsOnSquareGrid, distributeLibraryItemsOnSquareGrid,
@@ -150,27 +156,40 @@ const usePendingElementsMemo = (
appState: UIAppState, appState: UIAppState,
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
) => { ) => {
const create = () => const create = useCallback(
getSelectedElements(elements, appState, { (appState: UIAppState, elements: readonly NonDeletedExcalidrawElement[]) =>
includeBoundTextElement: true, getSelectedElements(elements, appState, {
includeElementsInFrames: true, includeBoundTextElement: true,
}); includeElementsInFrames: true,
const val = useRef(create()); }),
[],
);
const val = useRef(create(appState, elements));
const prevAppState = useRef<UIAppState>(appState); const prevAppState = useRef<UIAppState>(appState);
const prevElements = useRef(elements); const prevElements = useRef(elements);
if ( const update = useCallback(() => {
!isShallowEqual( if (
appState.selectedElementIds, !isShallowEqual(
prevAppState.current.selectedElementIds, appState.selectedElementIds,
) || prevAppState.current.selectedElementIds,
!isShallowEqual(elements, prevElements.current) ) ||
) { !isShallowEqual(elements, prevElements.current)
val.current = create(); ) {
prevAppState.current = appState; val.current = create(appState, elements);
prevElements.current = elements; prevAppState.current = appState;
} prevElements.current = elements;
return val.current; }
}, [create, appState, elements]);
return useMemo(
() => ({
update,
value: val.current,
}),
[update, val],
);
}; };
/** /**
@@ -181,6 +200,7 @@ export const LibraryMenu = () => {
const { library, id, onInsertElements } = useApp(); const { library, id, onInsertElements } = useApp();
const appProps = useAppProps(); const appProps = useAppProps();
const appState = useUIAppState(); const appState = useUIAppState();
const app = useApp();
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const elements = useExcalidrawElements(); const elements = useExcalidrawElements();
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]); const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
@@ -203,9 +223,13 @@ export const LibraryMenu = () => {
}); });
}, [setAppState]); }, [setAppState]);
useEffect(() => {
return app.onPointerUpEmitter.on(() => pendingElements.update());
}, [app, pendingElements]);
return ( return (
<LibraryMenuContent <LibraryMenuContent
pendingElements={pendingElements} pendingElements={pendingElements.value}
onInsertLibraryItems={onInsertLibraryItems} onInsertLibraryItems={onInsertLibraryItems}
onAddToLibrary={deselectItems} onAddToLibrary={deselectItems}
setAppState={setAppState} setAppState={setAppState}
@@ -63,27 +63,6 @@
max-width: 100%; max-width: 100%;
} }
.library-unit__checkbox-container,
.library-unit__checkbox-container:hover,
.library-unit__checkbox-container:active {
align-items: center;
background: none;
border: none;
color: var(--icon-fill-color);
display: flex;
justify-content: center;
margin: 0;
padding: 0.5rem;
position: absolute;
left: 2rem;
bottom: 2rem;
cursor: pointer;
input {
cursor: pointer;
}
}
.library-unit__checkbox { .library-unit__checkbox {
position: absolute; position: absolute;
top: 0.125rem; top: 0.125rem;
@@ -91,7 +70,7 @@
margin: 0; margin: 0;
.Checkbox-box { .Checkbox-box {
margin: 0; margin: 0 !important;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
border-radius: 4px; border-radius: 4px;
+59
View File
@@ -0,0 +1,59 @@
@import "../css/variables.module.scss";
.excalidraw {
--Range-track-background: var(--button-bg);
--Range-track-background-active: var(--color-primary);
--Range-thumb-background: var(--color-on-surface);
--Range-legend-color: var(--text-primary-color);
.range-wrapper {
position: relative;
padding-top: 10px;
padding-bottom: 30px;
}
.range-input {
width: 100%;
height: 4px;
-webkit-appearance: none;
background: var(--Range-track-background);
border-radius: 2px;
outline: none;
}
.range-input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: var(--Range-thumb-background);
border-radius: 50%;
cursor: pointer;
border: none;
}
.range-input::-moz-range-thumb {
width: 20px;
height: 20px;
background: var(--Range-thumb-background);
border-radius: 50%;
cursor: pointer;
border: none;
}
.value-bubble {
position: absolute;
bottom: 0;
transform: translateX(-50%);
font-size: 12px;
color: var(--Range-legend-color);
}
.zero-label {
position: absolute;
bottom: 0;
left: 4px;
font-size: 12px;
color: var(--Range-legend-color);
}
}
+65
View File
@@ -0,0 +1,65 @@
import React, { useEffect } from "react";
import { getFormValue } from "../actions/actionProperties";
import { t } from "../i18n";
import "./Range.scss";
export type RangeProps = {
updateData: (value: number) => void;
appState: any;
elements: any;
testId?: string;
};
export const Range = ({
updateData,
appState,
elements,
testId,
}: RangeProps) => {
const rangeRef = React.useRef<HTMLInputElement>(null);
const valueRef = React.useRef<HTMLDivElement>(null);
const value = getFormValue(
elements,
appState,
(element) => element.opacity,
true,
appState.currentItemOpacity,
);
useEffect(() => {
if (rangeRef.current && valueRef.current) {
const rangeElement = rangeRef.current;
const valueElement = valueRef.current;
const inputWidth = rangeElement.offsetWidth;
const thumbWidth = 15; // 15 is the width of the thumb
const position =
(value / 100) * (inputWidth - thumbWidth) + thumbWidth / 2;
valueElement.style.left = `${position}px`;
rangeElement.style.background = `linear-gradient(to right, var(--color-primary) 0%, var(--color-primary) ${value}%, var(--button-bg) ${value}%, var(--button-bg) 100%)`;
}
}, [value]);
return (
<label className="control-label">
{t("labels.opacity")}
<div className="range-wrapper">
<input
ref={rangeRef}
type="range"
min="0"
max="100"
step="10"
onChange={(event) => {
updateData(+event.target.value);
}}
value={value}
className="range-input"
data-testid={testId}
/>
<div className="value-bubble" ref={valueRef}>
{value !== 0 ? value : null}
</div>
<div className="zero-label">0</div>
</div>
</label>
);
};
@@ -1,6 +1,6 @@
.excalidraw { .excalidraw {
.excalifont { .excalifont {
font-family: "Excalifont"; font-family: "Excalifont", "Xiaolai";
} }
// WelcomeSreen common // WelcomeSreen common
+3
View File
@@ -32,6 +32,7 @@
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08), 0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08),
0px 7px 14px 0px rgba(0, 0, 0, 0.05); 0px 7px 14px 0px rgba(0, 0, 0, 0.05);
--button-bg: var(--color-surface-mid);
--button-hover-bg: var(--color-surface-high); --button-hover-bg: var(--color-surface-high);
--button-active-bg: var(--color-surface-high); --button-active-bg: var(--color-surface-high);
--button-active-border: var(--color-brand-active); --button-active-border: var(--color-brand-active);
@@ -171,6 +172,8 @@
--button-destructive-bg-color: #5a0000; --button-destructive-bg-color: #5a0000;
--button-destructive-color: #{$oc-red-3}; --button-destructive-color: #{$oc-red-3};
--button-bg: var(--color-surface-high);
--button-gray-1: #363636; --button-gray-1: #363636;
--button-gray-2: #272727; --button-gray-2: #272727;
--button-gray-3: #222; --button-gray-3: #222;
+2 -2
View File
@@ -25,7 +25,7 @@ describe("normalizeLink", () => {
expect(normalizeLink("file://")).toBe("file://"); expect(normalizeLink("file://")).toBe("file://");
expect(normalizeLink("[test](https://test)")).toBe("[test](https://test)"); expect(normalizeLink("[test](https://test)")).toBe("[test](https://test)");
expect(normalizeLink("[[test]]")).toBe("[[test]]"); expect(normalizeLink("[[test]]")).toBe("[[test]]");
expect(normalizeLink("<test>")).toBe("&lt;test&gt;"); expect(normalizeLink("<test>")).toBe("<test>");
expect(normalizeLink("test&")).toBe("test&amp;"); expect(normalizeLink("test&")).toBe("test&");
}); });
}); });
+2 -2
View File
@@ -1,12 +1,12 @@
import { sanitizeUrl } from "@braintree/sanitize-url"; import { sanitizeUrl } from "@braintree/sanitize-url";
import { sanitizeHTMLAttribute } from "../utils"; import { escapeDoubleQuotes } from "../utils";
export const normalizeLink = (link: string) => { export const normalizeLink = (link: string) => {
link = link.trim(); link = link.trim();
if (!link) { if (!link) {
return link; return link;
} }
return sanitizeUrl(sanitizeHTMLAttribute(link)); return sanitizeUrl(escapeDoubleQuotes(link));
}; };
export const isLocalLink = (link: string | null) => { export const isLocalLink = (link: string | null) => {
+4 -8
View File
@@ -1,11 +1,7 @@
import { register } from "../actions/register"; import { register } from "../actions/register";
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants"; import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
import type { ExcalidrawProps } from "../types"; import type { ExcalidrawProps } from "../types";
import { import { escapeDoubleQuotes, getFontString, updateActiveTool } from "../utils";
getFontString,
sanitizeHTMLAttribute,
updateActiveTool,
} from "../utils";
import { setCursorForShape } from "../cursor"; import { setCursorForShape } from "../cursor";
import { newTextElement } from "./newElement"; import { newTextElement } from "./newElement";
import { wrapText } from "./textWrapping"; import { wrapText } from "./textWrapping";
@@ -212,7 +208,7 @@ export const getEmbedLink = (
// Note that we don't attempt to parse the username as it can consist of // Note that we don't attempt to parse the username as it can consist of
// non-latin1 characters, and the username in the url can be set to anything // non-latin1 characters, and the username in the url can be set to anything
// without affecting the embed. // without affecting the embed.
const safeURL = sanitizeHTMLAttribute( const safeURL = escapeDoubleQuotes(
`https://twitter.com/x/status/${postId}`, `https://twitter.com/x/status/${postId}`,
); );
@@ -231,7 +227,7 @@ export const getEmbedLink = (
if (RE_REDDIT.test(link)) { if (RE_REDDIT.test(link)) {
const [, page, postId, title] = link.match(RE_REDDIT)!; const [, page, postId, title] = link.match(RE_REDDIT)!;
const safeURL = sanitizeHTMLAttribute( const safeURL = escapeDoubleQuotes(
`https://reddit.com/r/${page}/comments/${postId}/${title}`, `https://reddit.com/r/${page}/comments/${postId}/${title}`,
); );
const ret: IframeDataWithSandbox = { const ret: IframeDataWithSandbox = {
@@ -249,7 +245,7 @@ export const getEmbedLink = (
if (RE_GH_GIST.test(link)) { if (RE_GH_GIST.test(link)) {
const [, user, gistId] = link.match(RE_GH_GIST)!; const [, user, gistId] = link.match(RE_GH_GIST)!;
const safeURL = sanitizeHTMLAttribute( const safeURL = escapeDoubleQuotes(
`https://gist.github.com/${user}/${gistId}`, `https://gist.github.com/${user}/${gistId}`,
); );
const ret: IframeDataWithSandbox = { const ret: IframeDataWithSandbox = {
+9 -35
View File
@@ -3,7 +3,6 @@ import {
FONT_FAMILY_FALLBACKS, FONT_FAMILY_FALLBACKS,
CJK_HAND_DRAWN_FALLBACK_FONT, CJK_HAND_DRAWN_FALLBACK_FONT,
WINDOWS_EMOJI_FALLBACK_FONT, WINDOWS_EMOJI_FALLBACK_FONT,
isSafari,
getFontFamilyFallbacks, getFontFamilyFallbacks,
} from "../constants"; } from "../constants";
import { isTextElement } from "../element"; import { isTextElement } from "../element";
@@ -137,50 +136,28 @@ export class Fonts {
/** /**
* Load font faces for a given scene and trigger scene update. * Load font faces for a given scene and trigger scene update.
*
* FontFaceSet loadingdone event we listen on may not always
* fire (looking at you Safari), so on init we manually load all
* fonts and rerender scene text elements once done.
*
* For Safari we make sure to check against each loaded font face
* with the unique characters per family in the scene,
* otherwise fonts might remain unloaded.
*/ */
public loadSceneFonts = async (): Promise<FontFace[]> => { public loadSceneFonts = async (): Promise<FontFace[]> => {
const sceneFamilies = this.getSceneFamilies(); const sceneFamilies = this.getSceneFamilies();
const charsPerFamily = isSafari const charsPerFamily = Fonts.getCharsPerFamily(
? Fonts.getCharsPerFamily(this.scene.getNonDeletedElements()) this.scene.getNonDeletedElements(),
: undefined; );
return Fonts.loadFontFaces(sceneFamilies, charsPerFamily); return Fonts.loadFontFaces(sceneFamilies, charsPerFamily);
}; };
/** /**
* Load font faces for passed elements - use when the scene is unavailable (i.e. export). * Load font faces for passed elements - use when the scene is unavailable (i.e. export).
*
* For Safari we make sure to check against each loaded font face,
* with the unique characters per family in the elements
* otherwise fonts might remain unloaded.
*/ */
public static loadElementsFonts = async ( public static loadElementsFonts = async (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
): Promise<FontFace[]> => { ): Promise<FontFace[]> => {
const fontFamilies = Fonts.getUniqueFamilies(elements); const fontFamilies = Fonts.getUniqueFamilies(elements);
const charsPerFamily = isSafari const charsPerFamily = Fonts.getCharsPerFamily(elements);
? Fonts.getCharsPerFamily(elements)
: undefined;
return Fonts.loadFontFaces(fontFamilies, charsPerFamily); return Fonts.loadFontFaces(fontFamilies, charsPerFamily);
}; };
/**
* Load all registered font faces.
*/
public static loadAllFonts = async (): Promise<FontFace[]> => {
const allFamilies = Fonts.getAllFamilies();
return Fonts.loadFontFaces(allFamilies);
};
/** /**
* Generate CSS @font-face declarations for the given elements. * Generate CSS @font-face declarations for the given elements.
*/ */
@@ -223,7 +200,7 @@ export class Fonts {
private static async loadFontFaces( private static async loadFontFaces(
fontFamilies: Array<ExcalidrawTextElement["fontFamily"]>, fontFamilies: Array<ExcalidrawTextElement["fontFamily"]>,
charsPerFamily?: Record<number, Set<string>>, charsPerFamily: Record<number, Set<string>>,
) { ) {
// add all registered font faces into the `document.fonts` (if not added already) // add all registered font faces into the `document.fonts` (if not added already)
for (const { fontFaces, metadata } of Fonts.registered.values()) { for (const { fontFaces, metadata } of Fonts.registered.values()) {
@@ -248,7 +225,7 @@ export class Fonts {
private static *fontFacesLoader( private static *fontFacesLoader(
fontFamilies: Array<ExcalidrawTextElement["fontFamily"]>, fontFamilies: Array<ExcalidrawTextElement["fontFamily"]>,
charsPerFamily?: Record<number, Set<string>>, charsPerFamily: Record<number, Set<string>>,
): Generator<Promise<void | readonly [number, FontFace[]]>> { ): Generator<Promise<void | readonly [number, FontFace[]]>> {
for (const [index, fontFamily] of fontFamilies.entries()) { for (const [index, fontFamily] of fontFamilies.entries()) {
const font = getFontString({ const font = getFontString({
@@ -256,12 +233,9 @@ export class Fonts {
fontSize: 16, fontSize: 16,
}); });
// WARN: without "text" param it does not have to mean that all font faces are loaded, instead it could be just one! // WARN: without "text" param it does not have to mean that all font faces are loaded as it could be just one irrelevant font face!
// for Safari on init, we rather check with the "text" param, even though it's less efficient, as otherwise fonts might remain unloaded // instead, we are always checking chars used in the family, so that no required font faces remain unloaded
const text = const text = Fonts.getCharacters(charsPerFamily, fontFamily);
isSafari && charsPerFamily
? Fonts.getCharacters(charsPerFamily, fontFamily)
: "";
if (!window.document.fonts.check(font, text)) { if (!window.document.fonts.check(font, text)) {
yield promiseTry(async () => { yield promiseTry(async () => {
@@ -50,7 +50,7 @@ describe("actionStyles", () => {
// Roughness // Roughness
fireEvent.click(screen.getByTitle("Cartoonist")); fireEvent.click(screen.getByTitle("Cartoonist"));
// Opacity // Opacity
fireEvent.change(screen.getByLabelText("Opacity"), { fireEvent.change(screen.getByTestId("opacity"), {
target: { value: "60" }, target: { value: "60" },
}); });
@@ -338,7 +338,7 @@ describe("contextMenu element", () => {
// Roughness // Roughness
fireEvent.click(screen.getByTitle("Cartoonist")); fireEvent.click(screen.getByTitle("Cartoonist"));
// Opacity // Opacity
fireEvent.change(screen.getByLabelText("Opacity"), { fireEvent.change(screen.getByTestId("opacity"), {
target: { value: "60" }, target: { value: "60" },
}); });
+1 -7
View File
@@ -1,4 +1,4 @@
import { isTransparent, sanitizeHTMLAttribute } from "../utils"; import { isTransparent } from "../utils";
describe("Test isTransparent", () => { describe("Test isTransparent", () => {
it("should return true when color is rgb transparent", () => { it("should return true when color is rgb transparent", () => {
@@ -11,9 +11,3 @@ describe("Test isTransparent", () => {
expect(isTransparent("#ced4da")).toEqual(false); expect(isTransparent("#ced4da")).toEqual(false);
}); });
}); });
describe("sanitizeHTMLAttribute()", () => {
it("should escape HTML attribute special characters & not double escape", () => {
expect(sanitizeHTMLAttribute(`&"'><`)).toBe("&amp;&quot;&#39;&gt;&lt;");
});
});
+2
View File
@@ -681,6 +681,8 @@ export type AppClassProperties = {
getEditorUIOffsets: App["getEditorUIOffsets"]; getEditorUIOffsets: App["getEditorUIOffsets"];
visibleElements: App["visibleElements"]; visibleElements: App["visibleElements"];
excalidrawContainerValue: App["excalidrawContainerValue"]; excalidrawContainerValue: App["excalidrawContainerValue"];
onPointerUpEmitter: App["onPointerUpEmitter"];
}; };
export type PointerDownState = Readonly<{ export type PointerDownState = Readonly<{
+6 -11
View File
@@ -1226,15 +1226,10 @@ export class PromisePool<T> {
} }
} }
export const sanitizeHTMLAttribute = (html: string) => { /**
return ( * use when you need to render unsafe string as HTML attribute, but MAKE SURE
html * the attribute is double-quoted when constructing the HTML string
// note, if we're not doing stupid things, escaping " is enough, */
// but we might end up doing stupid things export const escapeDoubleQuotes = (str: string) => {
.replace(/&/g, "&amp;") return str.replace(/"/g, "&quot;");
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
.replace(/>/g, "&gt;")
.replace(/</g, "&lt;")
);
}; };
Binary file not shown.
Binary file not shown.
+29 -11
View File
@@ -123,6 +123,17 @@ module.exports.woff2ServerPlugin = (options = {}) => {
"./assets/NotoEmoji-Regular-2048.ttf", "./assets/NotoEmoji-Regular-2048.ttf",
); );
const liberationPath = path.resolve(
__dirname,
"./assets/LiberationSans-Regular.ttf",
);
// need to use the same em size as built-in fonts, otherwise pyftmerge throws (modified manually with font forge)
const liberationPath_2048 = path.resolve(
__dirname,
"./assets/LiberationSans-Regular-2048.ttf",
);
const xiaolaiFont = Font.create(fs.readFileSync(xiaolaiPath), { const xiaolaiFont = Font.create(fs.readFileSync(xiaolaiPath), {
type: "ttf", type: "ttf",
}); });
@@ -130,6 +141,10 @@ module.exports.woff2ServerPlugin = (options = {}) => {
type: "ttf", type: "ttf",
}); });
const liberationFont = Font.create(fs.readFileSync(liberationPath), {
type: "ttf",
});
const sortedFonts = Array.from(fonts.entries()).sort( const sortedFonts = Array.from(fonts.entries()).sort(
([family1], [family2]) => (family1 > family2 ? 1 : -1), ([family1], [family2]) => (family1 > family2 ? 1 : -1),
); );
@@ -141,13 +156,6 @@ module.exports.woff2ServerPlugin = (options = {}) => {
continue; continue;
} }
const fallbackFontsPaths = [];
const shouldIncludeXiaolaiFallback = family.includes("Excalifont");
if (shouldIncludeXiaolaiFallback) {
fallbackFontsPaths.push(xiaolaiPath);
}
const baseFont = Regular[0]; const baseFont = Regular[0];
const tempPaths = Regular.map((_, index) => const tempPaths = Regular.map((_, index) =>
path.resolve(outputDir, `temp_${family}_${index}.ttf`), path.resolve(outputDir, `temp_${family}_${index}.ttf`),
@@ -165,10 +173,18 @@ module.exports.woff2ServerPlugin = (options = {}) => {
const mergedFontPath = path.resolve(outputDir, `${family}.ttf`); const mergedFontPath = path.resolve(outputDir, `${family}.ttf`);
const fallbackFontsPaths = [];
const shouldIncludeXiaolaiFallback = family.includes("Excalifont");
if (shouldIncludeXiaolaiFallback) {
fallbackFontsPaths.push(xiaolaiPath);
}
// add liberation as fallback to all fonts, so that unknown characters are rendered similarly to how browser renders them (Helvetica, Arial, etc.)
if (baseFont.data.head.unitsPerEm === 2048) { if (baseFont.data.head.unitsPerEm === 2048) {
fallbackFontsPaths.push(emojiPath_2048); fallbackFontsPaths.push(emojiPath_2048, liberationPath_2048);
} else { } else {
fallbackFontsPaths.push(emojiPath); fallbackFontsPaths.push(emojiPath, liberationPath);
} }
// drop Vertical related metrics, otherwise it does not allow us to merge the fonts // drop Vertical related metrics, otherwise it does not allow us to merge the fonts
@@ -196,10 +212,12 @@ module.exports.woff2ServerPlugin = (options = {}) => {
const base = baseFont.data.name[field]; const base = baseFont.data.name[field];
const xiaolai = xiaolaiFont.data.name[field]; const xiaolai = xiaolaiFont.data.name[field];
const emoji = emojiFont.data.name[field]; const emoji = emojiFont.data.name[field];
const liberation = liberationFont.data.name[field];
// liberation font
return shouldIncludeXiaolaiFallback return shouldIncludeXiaolaiFallback
? `${base} & ${xiaolai} & ${emoji}` ? `${base} & ${xiaolai} & ${emoji} & ${liberation}`
: `${base} & ${emoji}`; : `${base} & ${emoji} & ${liberation}`;
}; };
mergedFont.set({ mergedFont.set({