mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-02 03:25:53 +01:00
feat: multiple fonts fallbacks (#8286)
This commit is contained in:
parent
d0a380758e
commit
230d0edc44
@ -95,6 +95,11 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Warmup the connection for Google fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
||||||
<script>
|
<script>
|
||||||
@ -114,52 +119,16 @@
|
|||||||
) {
|
) {
|
||||||
window.location.href = "https://app.excalidraw.com";
|
window.location.href = "https://app.excalidraw.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
// point into our CDN in prod
|
|
||||||
window.EXCALIDRAW_ASSET_PATH =
|
|
||||||
"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Following placeholder is replaced during the build step -->
|
||||||
|
<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->
|
||||||
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<script>
|
<script>
|
||||||
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||||
</script>
|
</script>
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
||||||
|
|
||||||
<!-- Excalidraw version -->
|
|
||||||
<meta name="version" content="{version}" />
|
|
||||||
|
|
||||||
<!-- Warmup the connection for Google fonts -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
|
|
||||||
<!-- Preload all default fonts and Virgil for backwards compatibility to avoid swap on init -->
|
|
||||||
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/Excalifont-Regular-C9eKQy_N.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/Virgil-Regular-hO16qHwV.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/ComicShanns-Regular-D0c8wzsC.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<% } else { %>
|
|
||||||
<!-- in DEV we need to preload from the local server and without the hash -->
|
<!-- in DEV we need to preload from the local server and without the hash -->
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
@ -184,7 +153,7 @@
|
|||||||
/>
|
/>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- For Nunito only preload the latin range, which should be enough for now -->
|
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
|
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
|
||||||
@ -200,6 +169,13 @@
|
|||||||
type="text/css"
|
type="text/css"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
|
||||||
|
<!-- Excalidraw version -->
|
||||||
|
<meta name="version" content="{version}" />
|
||||||
|
|
||||||
<% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
|
<% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
|
||||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
|
||||||
<script>
|
<script>
|
||||||
|
@ -63,15 +63,15 @@ export const FontPickerList = React.memo(
|
|||||||
() =>
|
() =>
|
||||||
Array.from(Fonts.registered.entries())
|
Array.from(Fonts.registered.entries())
|
||||||
.filter(([_, { metadata }]) => !metadata.serverSide)
|
.filter(([_, { metadata }]) => !metadata.serverSide)
|
||||||
.map(([familyId, { metadata, fontFaces }]) => {
|
.map(([familyId, { metadata, fonts }]) => {
|
||||||
const font = {
|
const fontDescriptor = {
|
||||||
value: familyId,
|
value: familyId,
|
||||||
icon: metadata.icon,
|
icon: metadata.icon,
|
||||||
text: fontFaces[0].fontFace.family,
|
text: fonts[0].fontFace.family,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (metadata.deprecated) {
|
if (metadata.deprecated) {
|
||||||
Object.assign(font, {
|
Object.assign(fontDescriptor, {
|
||||||
deprecated: metadata.deprecated,
|
deprecated: metadata.deprecated,
|
||||||
badge: {
|
badge: {
|
||||||
type: DropDownMenuItemBadgeType.RED,
|
type: DropDownMenuItemBadgeType.RED,
|
||||||
@ -80,7 +80,7 @@ export const FontPickerList = React.memo(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return font as FontDescriptor;
|
return fontDescriptor as FontDescriptor;
|
||||||
})
|
})
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1,
|
a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1,
|
||||||
|
@ -1,41 +1,29 @@
|
|||||||
import { stringToBase64, toByteString } from "../data/encode";
|
import { stringToBase64, toByteString } from "../data/encode";
|
||||||
|
import { LOCAL_FONT_PROTOCOL } from "./metadata";
|
||||||
|
|
||||||
export interface Font {
|
export interface Font {
|
||||||
url: URL;
|
urls: URL[];
|
||||||
fontFace: FontFace;
|
fontFace: FontFace;
|
||||||
getContent(): Promise<string>;
|
getContent(): Promise<string>;
|
||||||
}
|
}
|
||||||
export const UNPKG_PROD_URL = `https://unpkg.com/${
|
export const UNPKG_PROD_URL = `https://unpkg.com/${
|
||||||
import.meta.env.VITE_PKG_NAME
|
import.meta.env.VITE_PKG_NAME
|
||||||
}@${import.meta.env.PKG_VERSION}/dist/prod/`;
|
? `${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 class ExcalidrawFont implements Font {
|
export class ExcalidrawFont implements Font {
|
||||||
public readonly url: URL;
|
public readonly urls: URL[];
|
||||||
public readonly fontFace: FontFace;
|
public readonly fontFace: FontFace;
|
||||||
|
|
||||||
constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
|
constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
|
||||||
// absolute assets paths, which are found in tests and excalidraw-app build, won't work with base url, so we are stripping initial slash away
|
this.urls = ExcalidrawFont.createUrls(uri);
|
||||||
const assetUrl: string = uri.replace(/^\/+/, "");
|
|
||||||
let baseUrl: string | undefined = undefined;
|
|
||||||
|
|
||||||
// fallback to unpkg to form a valid URL in case of a passed relative assetUrl
|
const sources = this.urls
|
||||||
let baseUrlBuilder = window.EXCALIDRAW_ASSET_PATH || UNPKG_PROD_URL;
|
.map((url) => `url(${url}) ${ExcalidrawFont.getFormat(url)}`)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
// in case user passed a root-relative url (~absolute path),
|
this.fontFace = new FontFace(family, sources, {
|
||||||
// like "/" or "/some/path", or relative (starts with "./"),
|
|
||||||
// prepend it with `location.origin`
|
|
||||||
if (/^\.?\//.test(baseUrlBuilder)) {
|
|
||||||
baseUrlBuilder = new URL(
|
|
||||||
baseUrlBuilder.replace(/^\.?\/+/, ""),
|
|
||||||
window?.location?.origin,
|
|
||||||
).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure there is a trailing slash, otherwise url won't be correctly concatenated
|
|
||||||
baseUrl = `${baseUrlBuilder.replace(/\/+$/, "")}/`;
|
|
||||||
|
|
||||||
this.url = new URL(assetUrl, baseUrl);
|
|
||||||
this.fontFace = new FontFace(family, `url(${this.url})`, {
|
|
||||||
display: "swap",
|
display: "swap",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
weight: "400",
|
weight: "400",
|
||||||
@ -44,35 +32,128 @@ export class ExcalidrawFont implements Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches woff2 content based on the registered url (browser).
|
* Tries to fetch woff2 content, based on the registered urls.
|
||||||
|
* Returns last defined url in case of errors.
|
||||||
*
|
*
|
||||||
* Use dataurl outside the browser environment.
|
* Note: uses browser APIs for base64 encoding - use dataurl outside the browser environment.
|
||||||
*/
|
*/
|
||||||
public async getContent(): Promise<string> {
|
public async getContent(): Promise<string> {
|
||||||
if (this.url.protocol === "data:") {
|
let i = 0;
|
||||||
// it's dataurl, the font is inlined as base64, no need to fetch
|
const errorMessages = [];
|
||||||
return this.url.toString();
|
|
||||||
|
while (i < this.urls.length) {
|
||||||
|
const url = this.urls[i];
|
||||||
|
|
||||||
|
if (url.protocol === "data:") {
|
||||||
|
// it's dataurl, the font is inlined as base64, no need to fetch
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: "font/woff2",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const mimeType = await response.headers.get("Content-Type");
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
return `data:${mimeType};base64,${await stringToBase64(
|
||||||
|
await toByteString(buffer),
|
||||||
|
true,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// response not ok, try to continue
|
||||||
|
errorMessages.push(
|
||||||
|
`"${url.toString()}" returned status "${response.status}"`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorMessages.push(`"${url.toString()}" returned error "${e}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(this.url, {
|
console.error(
|
||||||
headers: {
|
`Failed to fetch font "${
|
||||||
Accept: "font/woff2",
|
this.fontFace.family
|
||||||
},
|
}" from urls "${this.urls.toString()}`,
|
||||||
});
|
JSON.stringify(errorMessages, undefined, 2),
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
// in case of issues, at least return the last url as a content
|
||||||
console.error(
|
// defaults to unpkg for bundled fonts (so that we don't have to host them forever) and http url for others
|
||||||
`Couldn't fetch font-family "${this.fontFace.family}" from url "${this.url}"`,
|
return this.urls.length ? this.urls[this.urls.length - 1].toString() : "";
|
||||||
response,
|
}
|
||||||
|
|
||||||
|
private static createUrls(uri: string): URL[] {
|
||||||
|
if (uri.startsWith(LOCAL_FONT_PROTOCOL)) {
|
||||||
|
// no url for local fonts
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.startsWith("http") || uri.startsWith("data")) {
|
||||||
|
// one url for http imports or data url
|
||||||
|
return [new URL(uri)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// absolute assets paths, which are found in tests and excalidraw-app build, won't work with base url, so we are stripping initial slash away
|
||||||
|
const assetUrl: string = uri.replace(/^\/+/, "");
|
||||||
|
const urls: URL[] = [];
|
||||||
|
|
||||||
|
if (typeof window.EXCALIDRAW_ASSET_PATH === "string") {
|
||||||
|
const normalizedBaseUrl = this.normalizeBaseUrl(
|
||||||
|
window.EXCALIDRAW_ASSET_PATH,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
urls.push(new URL(assetUrl, normalizedBaseUrl));
|
||||||
|
} else if (Array.isArray(window.EXCALIDRAW_ASSET_PATH)) {
|
||||||
|
window.EXCALIDRAW_ASSET_PATH.forEach((path) => {
|
||||||
|
const normalizedBaseUrl = this.normalizeBaseUrl(path);
|
||||||
|
urls.push(new URL(assetUrl, normalizedBaseUrl));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const mimeType = await response.headers.get("Content-Type");
|
// fallback url for bundled fonts
|
||||||
const buffer = await response.arrayBuffer();
|
urls.push(new URL(assetUrl, UNPKG_PROD_URL));
|
||||||
|
|
||||||
return `data:${mimeType};base64,${await stringToBase64(
|
return urls;
|
||||||
await toByteString(buffer),
|
}
|
||||||
true,
|
|
||||||
)}`;
|
private static getFormat(url: URL) {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(url).pathname;
|
||||||
|
const parts = pathname.split(".");
|
||||||
|
|
||||||
|
if (parts.length === 1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `format('${parts.pop()}')`;
|
||||||
|
} catch (error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static normalizeBaseUrl(baseUrl: string) {
|
||||||
|
let result = baseUrl;
|
||||||
|
|
||||||
|
// in case user passed a root-relative url (~absolute path),
|
||||||
|
// like "/" or "/some/path", or relative (starts with "./"),
|
||||||
|
// prepend it with `location.origin`
|
||||||
|
if (/^\.?\//.test(result)) {
|
||||||
|
result = new URL(
|
||||||
|
result.replace(/^\.?\/+/, ""),
|
||||||
|
window?.location?.origin,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure there is a trailing slash, otherwise url won't be correctly concatenated
|
||||||
|
result = `${result.replace(/\/+$/, "")}/`;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/* Only UI fonts here, which are needed before the editor initializes. */
|
/* Only UI fonts here, which are needed before the editor initializes. */
|
||||||
/* These also cannot be preprended with `EXCALIDRAW_ASSET_PATH`. */
|
/* These cannot be dynamically prepended with `EXCALIDRAW_ASSET_PATH`. */
|
||||||
|
/* WARN: The following content is replaced during excalidraw-app build */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Assistant";
|
font-family: "Assistant";
|
||||||
|
@ -39,7 +39,7 @@ export class Fonts {
|
|||||||
number,
|
number,
|
||||||
{
|
{
|
||||||
metadata: FontMetadata;
|
metadata: FontMetadata;
|
||||||
fontFaces: Font[];
|
fonts: Font[];
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
| undefined;
|
| undefined;
|
||||||
@ -121,12 +121,9 @@ export class Fonts {
|
|||||||
|
|
||||||
public load = async () => {
|
public load = async () => {
|
||||||
// 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 } of Fonts.registered.values()) {
|
for (const { fonts } of Fonts.registered.values()) {
|
||||||
for (const { fontFace, url } of fontFaces) {
|
for (const { fontFace } of fonts) {
|
||||||
if (
|
if (!window.document.fonts.has(fontFace)) {
|
||||||
url.protocol !== LOCAL_FONT_PROTOCOL &&
|
|
||||||
!window.document.fonts.has(fontFace)
|
|
||||||
) {
|
|
||||||
window.document.fonts.add(fontFace);
|
window.document.fonts.add(fontFace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,8 +145,10 @@ export class Fonts {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't let it all fail if just one font fails to load
|
// don't let it all fail if just one font fails to load
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to load font: "${fontString}" with error "${e}", given the following registered font:`,
|
`Failed to load font "${fontString}" from urls "${Fonts.registered
|
||||||
JSON.stringify(Fonts.registered.get(fontFamily), undefined, 2),
|
.get(fontFamily)
|
||||||
|
?.fonts.map((x) => x.urls)}"`,
|
||||||
|
e,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,7 +167,7 @@ export class Fonts {
|
|||||||
const fonts = {
|
const fonts = {
|
||||||
registered: new Map<
|
registered: new Map<
|
||||||
ValueOf<typeof FONT_FAMILY>,
|
ValueOf<typeof FONT_FAMILY>,
|
||||||
{ metadata: FontMetadata; fontFaces: Font[] }
|
{ metadata: FontMetadata; fonts: Font[] }
|
||||||
>(),
|
>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -253,7 +252,7 @@ function register(
|
|||||||
| {
|
| {
|
||||||
registered: Map<
|
registered: Map<
|
||||||
ValueOf<typeof FONT_FAMILY>,
|
ValueOf<typeof FONT_FAMILY>,
|
||||||
{ metadata: FontMetadata; fontFaces: Font[] }
|
{ metadata: FontMetadata; fonts: Font[] }
|
||||||
>;
|
>;
|
||||||
},
|
},
|
||||||
family: string,
|
family: string,
|
||||||
@ -267,7 +266,7 @@ function register(
|
|||||||
if (!registeredFamily) {
|
if (!registeredFamily) {
|
||||||
this.registered.set(familyId, {
|
this.registered.set(familyId, {
|
||||||
metadata,
|
metadata,
|
||||||
fontFaces: params.map(
|
fonts: params.map(
|
||||||
({ uri, descriptors }) => new ExcalidrawFont(family, uri, descriptors),
|
({ uri, descriptors }) => new ExcalidrawFont(family, uri, descriptors),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -27,6 +27,8 @@ export interface FontMetadata {
|
|||||||
deprecated?: true;
|
deprecated?: true;
|
||||||
/** flag to indicate a server-side only font */
|
/** flag to indicate a server-side only font */
|
||||||
serverSide?: true;
|
serverSide?: true;
|
||||||
|
/** flag to indiccate a local-only font */
|
||||||
|
local?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FONT_METADATA: Record<number, FontMetadata> = {
|
export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
@ -85,6 +87,7 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
},
|
},
|
||||||
icon: FontFamilyNormalIcon,
|
icon: FontFamilyNormalIcon,
|
||||||
deprecated: true,
|
deprecated: true,
|
||||||
|
local: true,
|
||||||
},
|
},
|
||||||
[FONT_FAMILY.Cascadia]: {
|
[FONT_FAMILY.Cascadia]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
|
2
packages/excalidraw/global.d.ts
vendored
2
packages/excalidraw/global.d.ts
vendored
@ -1,7 +1,7 @@
|
|||||||
interface Window {
|
interface Window {
|
||||||
ClipboardItem: any;
|
ClipboardItem: any;
|
||||||
__EXCALIDRAW_SHA__: string | undefined;
|
__EXCALIDRAW_SHA__: string | undefined;
|
||||||
EXCALIDRAW_ASSET_PATH: string | undefined;
|
EXCALIDRAW_ASSET_PATH: string | string[] | undefined;
|
||||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||||
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
||||||
DEBUG_FRACTIONAL_INDICES: boolean | undefined;
|
DEBUG_FRACTIONAL_INDICES: boolean | undefined;
|
||||||
|
@ -43,7 +43,6 @@ import type { RenderableElementsMap } from "./types";
|
|||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import { LOCAL_FONT_PROTOCOL } from "../fonts/metadata";
|
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@ -375,35 +374,28 @@ export const exportToSvg = async (
|
|||||||
? []
|
? []
|
||||||
: await Promise.all(
|
: await Promise.all(
|
||||||
Array.from(fontFamilies).map(async (x) => {
|
Array.from(fontFamilies).map(async (x) => {
|
||||||
const { fontFaces } = Fonts.registered.get(x) ?? {};
|
const { fonts, metadata } = Fonts.registered.get(x) ?? {};
|
||||||
|
|
||||||
if (!Array.isArray(fontFaces)) {
|
if (!Array.isArray(fonts)) {
|
||||||
console.error(
|
console.error(
|
||||||
`Couldn't find registered font-faces for font-family "${x}"`,
|
`Couldn't find registered fonts for font-family "${x}"`,
|
||||||
Fonts.registered,
|
Fonts.registered,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(
|
if (metadata?.local) {
|
||||||
fontFaces
|
// don't inline local fonts
|
||||||
.filter((font) => font.url.protocol !== LOCAL_FONT_PROTOCOL)
|
return;
|
||||||
.map(async (font) => {
|
}
|
||||||
try {
|
|
||||||
const content = await font.getContent();
|
|
||||||
|
|
||||||
return `@font-face {
|
return Promise.all(
|
||||||
|
fonts.map(
|
||||||
|
async (font) => `@font-face {
|
||||||
font-family: ${font.fontFace.family};
|
font-family: ${font.fontFace.family};
|
||||||
src: url(${content});
|
src: url(${await font.getContent()});
|
||||||
}`;
|
}`,
|
||||||
} catch (e) {
|
),
|
||||||
console.error(
|
|
||||||
`Skipped inlining font with URL "${font.url.toString()}"`,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
const OSS_FONTS_CDN =
|
||||||
|
"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom vite plugin to convert url woff2 imports into a text.
|
* Custom vite plugin to convert url woff2 imports into a text.
|
||||||
* Other woff2 imports are automatically served and resolved as a file uri.
|
* Other woff2 imports are automatically served and resolved as a file uri.
|
||||||
@ -41,6 +44,89 @@ module.exports.woff2BrowserPlugin = () => {
|
|||||||
`const $1 = $2`,
|
`const $1 = $2`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use CDN for Assistant
|
||||||
|
if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) {
|
||||||
|
return `/* WARN: The following content is generated during excalidraw-app build */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Assistant";
|
||||||
|
src: url(${OSS_FONTS_CDN}Assistant-Regular-DVxZuzxb.woff2)
|
||||||
|
format("woff2"),
|
||||||
|
url(./Assistant-Regular.woff2) format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
style: normal;
|
||||||
|
display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Assistant";
|
||||||
|
src: url(${OSS_FONTS_CDN}Assistant-Medium-DrcxCXg3.woff2)
|
||||||
|
format("woff2"),
|
||||||
|
url(./Assistant-Medium.woff2) format("woff2");
|
||||||
|
font-weight: 500;
|
||||||
|
style: normal;
|
||||||
|
display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Assistant";
|
||||||
|
src: url(${OSS_FONTS_CDN}Assistant-SemiBold-SCI4bEL9.woff2)
|
||||||
|
format("woff2"),
|
||||||
|
url(./Assistant-SemiBold.woff2) format("woff2");
|
||||||
|
font-weight: 600;
|
||||||
|
style: normal;
|
||||||
|
display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Assistant";
|
||||||
|
src: url(${OSS_FONTS_CDN}Assistant-Bold-gm-uSS1B.woff2)
|
||||||
|
format("woff2"),
|
||||||
|
url(./Assistant-Bold.woff2) format("woff2");
|
||||||
|
font-weight: 700;
|
||||||
|
style: normal;
|
||||||
|
display: swap;
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// using EXCALIDRAW_ASSET_PATH as a SSOT
|
||||||
|
if (!isDev && id.endsWith("excalidraw-app/index.html")) {
|
||||||
|
return code.replace(
|
||||||
|
"<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->",
|
||||||
|
`<script>
|
||||||
|
// point into our CDN in prod, fallback to root (excalidraw.com) domain in case of issues
|
||||||
|
window.EXCALIDRAW_ASSET_PATH = [
|
||||||
|
"${OSS_FONTS_CDN}",
|
||||||
|
"/",
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Preload all default fonts and Virgil for backwards compatibility to avoid swap on init -->
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="${OSS_FONTS_CDN}Excalifont-Regular-C9eKQy_N.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="${OSS_FONTS_CDN}ComicShanns-Regular-D0c8wzsC.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -72,12 +72,14 @@ vi.mock(
|
|||||||
...mod,
|
...mod,
|
||||||
ExcalidrawFont: class extends ExcalidrawFontImpl {
|
ExcalidrawFont: class extends ExcalidrawFontImpl {
|
||||||
public async getContent(): Promise<string> {
|
public async getContent(): Promise<string> {
|
||||||
if (this.url.protocol !== "file:") {
|
const url = this.urls[0];
|
||||||
|
|
||||||
|
if (url.protocol !== "file:") {
|
||||||
return super.getContent();
|
return super.getContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// read local assets directly, without running a server
|
// read local assets directly, without running a server
|
||||||
const content = await fs.promises.readFile(this.url);
|
const content = await fs.promises.readFile(url);
|
||||||
return `data:font/woff2;base64,${content.toString("base64")}`;
|
return `data:font/woff2;base64,${content.toString("base64")}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
28
vercel.json
28
vercel.json
@ -6,7 +6,7 @@
|
|||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"key": "Access-Control-Allow-Origin",
|
"key": "Access-Control-Allow-Origin",
|
||||||
"value": "*"
|
"value": "https://excalidraw.com"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "X-Content-Type-Options",
|
"key": "X-Content-Type-Options",
|
||||||
@ -21,6 +21,32 @@
|
|||||||
"value": "origin"
|
"value": "origin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/:file*.woff2",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "public, max-age=31536000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Access-Control-Allow-Origin",
|
||||||
|
"value": "https://excalidraw.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/(Virgil|Cascadia|Assistant-Regular).woff2",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "public, max-age=31536000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Access-Control-Allow-Origin",
|
||||||
|
"value": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"redirects": [
|
"redirects": [
|
||||||
|
Loading…
Reference in New Issue
Block a user