mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 11:03:39 +00:00
use sprites to parse symbols
This commit is contained in:
@@ -6,8 +6,11 @@ import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
|
||||
|
||||
const ICON_ASSET_VERSION = '2';
|
||||
const rawSvgPromises = new Map<string, Promise<string | null>>();
|
||||
const styleSpritePromises = new Map<string, Promise<Map<string, IconSymbolSource> | null>>();
|
||||
const svgPattern = /<svg\b([^>]*)>([\s\S]*?)<\/svg>\s*$/i;
|
||||
const symbolPattern = /<symbol\b([^>]*)>([\s\S]*?)<\/symbol>/gi;
|
||||
const viewBoxPattern = /\bviewBox="([^"]+)"/i;
|
||||
const idPattern = /\bid="([^"]+)"/i;
|
||||
const commentPattern = /<!--[\s\S]*?-->/g;
|
||||
|
||||
interface IconSymbolSource {
|
||||
@@ -44,6 +47,19 @@ function getIconAssetURL(style: string, icon: string): string {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getStyleSpriteAssetURL(style: string): string {
|
||||
const url = new URL(
|
||||
joinPathWithBaseURL(getIconAssetBaseURL(style), joinPath('sprites', `${style}.svg`))
|
||||
);
|
||||
url.searchParams.set('v', ICON_ASSET_VERSION);
|
||||
|
||||
if (style !== 'custom-icons' && GITBOOK_ICONS_TOKEN) {
|
||||
url.searchParams.set('token', GITBOOK_ICONS_TOKEN);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function parseRawSVG(document: string): IconSymbolSource | null {
|
||||
const svgMatch = document.match(svgPattern);
|
||||
if (!svgMatch) {
|
||||
@@ -68,6 +84,35 @@ function parseRawSVG(document: string): IconSymbolSource | null {
|
||||
};
|
||||
}
|
||||
|
||||
function parseStyleSprite(document: string): Map<string, IconSymbolSource> | null {
|
||||
const symbols = new Map<string, IconSymbolSource>();
|
||||
|
||||
for (const match of document.matchAll(symbolPattern)) {
|
||||
const symbolAttributes = match[1];
|
||||
const rawMarkup = match[2];
|
||||
|
||||
if (!symbolAttributes || rawMarkup === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const idMatch = symbolAttributes.match(idPattern);
|
||||
const viewBoxMatch = symbolAttributes.match(viewBoxPattern);
|
||||
const icon = idMatch?.[1];
|
||||
const viewBox = viewBoxMatch?.[1];
|
||||
|
||||
if (!icon || !viewBox) {
|
||||
continue;
|
||||
}
|
||||
|
||||
symbols.set(icon, {
|
||||
viewBox,
|
||||
markup: rawMarkup.replace(commentPattern, '').trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return symbols.size > 0 ? symbols : null;
|
||||
}
|
||||
|
||||
function buildSymbolMarkup(symbolId: string, viewBox: string, markup: string) {
|
||||
return `<symbol id="${escapeAttribute(symbolId)}" viewBox="${escapeAttribute(viewBox)}" overflow="visible">${markup}</symbol>`;
|
||||
}
|
||||
@@ -99,17 +144,45 @@ async function fetchRawSVG(style: string, icon: string): Promise<string | null>
|
||||
return request;
|
||||
}
|
||||
|
||||
async function fetchStyleSprite(style: string): Promise<Map<string, IconSymbolSource> | null> {
|
||||
const existing = styleSpritePromises.get(style);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const request = fetch(getStyleSpriteAssetURL(style), {
|
||||
cache: 'force-cache',
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseStyleSprite(await response.text());
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
styleSpritePromises.set(style, request);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one icon entry from the raw SVG source and serialize it for sprite injection or
|
||||
* same-origin lazy loading.
|
||||
*/
|
||||
export async function getIconSymbol(style: string, icon: string, symbolId: string) {
|
||||
const rawSVG = await fetchRawSVG(style, icon);
|
||||
if (!rawSVG) {
|
||||
return null;
|
||||
const spriteSymbols = await fetchStyleSprite(style);
|
||||
let source = spriteSymbols?.get(icon) ?? null;
|
||||
|
||||
if (!source) {
|
||||
const rawSVG = await fetchRawSVG(style, icon);
|
||||
if (!rawSVG) {
|
||||
return null;
|
||||
}
|
||||
|
||||
source = parseRawSVG(rawSVG);
|
||||
}
|
||||
|
||||
const source = parseRawSVG(rawSVG);
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { IconSymbolLoader } from './IconSymbolLoader';
|
||||
import { getIconAssetURL, useIcons } from './IconsProvider';
|
||||
import { getIconAssetURL, getIconSpriteAssetURL, useIcons } from './IconsProvider';
|
||||
import { getIconStyle } from './getIconStyle';
|
||||
import { getIconSymbolId, prefetchServerIconAsset, registerServerIconSymbol } from './symbols';
|
||||
import type { IconName, IconStyle } from './types';
|
||||
@@ -56,14 +56,14 @@ export const Icon = React.forwardRef(function Icon(
|
||||
const iconInstanceId = React.useId();
|
||||
const symbolId = getIconSymbolId(iconStyle, icon);
|
||||
const iconAssetURL = getIconAssetURL(context, iconStyle, icon);
|
||||
const iconSpriteAssetURL = getIconSpriteAssetURL(context, iconStyle);
|
||||
|
||||
if (context.renderMode === 'symbol') {
|
||||
prefetchServerIconAsset(iconAssetURL);
|
||||
prefetchServerIconAsset(iconSpriteAssetURL);
|
||||
registerServerIconSymbol({
|
||||
style: iconStyle,
|
||||
icon,
|
||||
symbolId,
|
||||
assetURL: iconAssetURL,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -88,3 +88,11 @@ export function getIconAssetURL(context: IconsContextType, style: string, icon:
|
||||
const iconName = typeof icon === 'string' ? icon : String(icon);
|
||||
return getAssetURL(location, `svgs/${style}/${iconName}.svg`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the sprite document of an icon style.
|
||||
*/
|
||||
export function getIconSpriteAssetURL(context: IconsContextType, style: string): string {
|
||||
const location = context.assetsByStyles?.[style] ?? context;
|
||||
return getAssetURL(location, `sprites/${style}.svg`);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ export interface RegisteredIconSymbol {
|
||||
style: string;
|
||||
icon: string;
|
||||
symbolId: string;
|
||||
assetURL?: string;
|
||||
}
|
||||
|
||||
const REGISTERED_SYMBOLS_KEY = Symbol.for('gitbook.icons.registeredSymbols');
|
||||
@@ -63,8 +62,8 @@ export function registerServerIconSymbol(symbol: RegisteredIconSymbol): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start fetching a raw SVG asset during SSR so sprite generation can reuse the in-flight or warm
|
||||
* request instead of waiting until the end of the render.
|
||||
* Start fetching a server-side icon asset during SSR so sprite generation can reuse the in-flight
|
||||
* or warm request instead of waiting until the end of the render.
|
||||
*/
|
||||
export function prefetchServerIconAsset(assetURL: string): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
Reference in New Issue
Block a user