diff --git a/packages/gitbook/src/app/~gitbook/icons/symbol/[style]/[icon]/route.ts b/packages/gitbook/src/app/~gitbook/icons/symbol/[style]/[icon]/route.ts
index 482832301..39c0e2088 100644
--- a/packages/gitbook/src/app/~gitbook/icons/symbol/[style]/[icon]/route.ts
+++ b/packages/gitbook/src/app/~gitbook/icons/symbol/[style]/[icon]/route.ts
@@ -23,7 +23,7 @@ export async function GET(
);
}
- return new NextResponse(symbol.symbol, {
+ return new NextResponse(symbol.document, {
headers: {
'content-type': 'image/svg+xml; charset=utf-8',
'cache-control': 'public, max-age=31536000, immutable',
diff --git a/packages/gitbook/src/lib/icons/symbols.test.tsx b/packages/gitbook/src/lib/icons/symbols.test.tsx
deleted file mode 100644
index 2129c1a27..000000000
--- a/packages/gitbook/src/lib/icons/symbols.test.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { afterEach, describe, expect, it } from 'bun:test';
-import { Icon, IconStyle, IconsProvider, clearRegisteredServerIconSymbols } from '@gitbook/icons';
-import type { NextRequest } from 'next/server';
-import { renderToStaticMarkup } from 'react-dom/server';
-
-import { GET } from '@/app/~gitbook/icons/symbol/[style]/[icon]/route';
-import { IconSpriteDefinitions } from '@/components/RootLayout/IconSpriteDefinitions';
-
-import { getIconSymbol } from './symbols';
-
-afterEach(() => {
- clearRegisteredServerIconSymbols();
-});
-
-describe('icon symbols', () => {
- it('loads symbol markup for multiple families', async () => {
- const regular = await getIconSymbol('regular', 'jar', 'gb-icon-regular-jar');
- const brand = await getIconSymbol('brands', 'github', 'gb-icon-brands-github');
- const custom = await getIconSymbol(
- 'custom-icons',
- 'gitbook',
- 'gb-icon-custom-icons-gitbook'
- );
- const sharp = await getIconSymbol(
- 'sharp-solid',
- 'download',
- 'gb-icon-sharp-solid-download'
- );
-
- expect(regular?.symbol).toContain('id="gb-icon-regular-jar"');
- expect(regular?.symbol).toContain('viewBox="0 0 320 512"');
- expect(brand?.symbol).toContain('id="gb-icon-brands-github"');
- expect(custom?.symbol).toContain('id="gb-icon-custom-icons-gitbook"');
- expect(sharp?.symbol).toContain('id="gb-icon-sharp-solid-download"');
- });
-
- it('emits only the registered subset sprite definitions', async () => {
- renderToStaticMarkup(
-
- <>
-
-
-
-
-
- >
-
- );
-
- const sprite = await IconSpriteDefinitions();
- const html = sprite ? renderToStaticMarkup(sprite) : '';
-
- expect(html).toContain('data-testid="icon-sprite-root"');
- expect(html).toContain('id="gb-icon-regular-jar"');
- expect(html).toContain('id="gb-icon-brands-github"');
- expect(html).toContain('id="gb-icon-sharp-solid-download"');
- expect(html).toContain('id="gb-icon-custom-icons-gitbook"');
- expect(html.match(/id="gb-icon-regular-jar"/g)?.length).toBe(1);
- });
-
- it('eagerly seeds search chrome icons into the sprite', async () => {
- const sprite = await IconSpriteDefinitions();
- const html = sprite ? renderToStaticMarkup(sprite) : '';
-
- expect(html).toContain('id="gb-icon-regular-search"');
- expect(html).toContain('id="gb-icon-regular-chevron-right"');
- expect(html).toContain('id="gb-icon-regular-arrow-turn-down-left"');
- expect(html).toContain('id="gb-icon-regular-xmark"');
- });
-
- it('serves symbols from the internal route', async () => {
- const response = await GET(
- new Request(
- 'http://localhost/~gitbook/icons/symbol/brands/github'
- ) as unknown as NextRequest,
- {
- params: Promise.resolve({
- style: 'brands',
- icon: 'github',
- }),
- }
- );
-
- expect(response.status).toBe(200);
- expect(response.headers.get('content-type')).toBe('image/svg+xml; charset=utf-8');
- expect(await response.text()).toContain('id="gb-icon-brands-github"');
- });
-
- it('serves alias-based symbols from the internal route', async () => {
- const response = await GET(
- new Request(
- 'http://localhost/~gitbook/icons/symbol/regular/search'
- ) as unknown as NextRequest,
- {
- params: Promise.resolve({
- style: 'regular',
- icon: 'search',
- }),
- }
- );
-
- expect(response.status).toBe(200);
- expect(await response.text()).toContain('id="gb-icon-regular-search"');
- });
-});
diff --git a/packages/gitbook/src/lib/icons/symbols.ts b/packages/gitbook/src/lib/icons/symbols.ts
index 75a567c31..92c5aaae3 100644
--- a/packages/gitbook/src/lib/icons/symbols.ts
+++ b/packages/gitbook/src/lib/icons/symbols.ts
@@ -70,12 +70,15 @@ export async function getIconSymbol(style: string, icon: string, symbolId: strin
return null;
}
+ const symbol = `${entry.markup}`;
+
return {
style,
icon,
symbolId,
viewBox: entry.viewBox,
markup: entry.markup,
- symbol: `${entry.markup}`,
+ symbol,
+ document: ``,
};
}
diff --git a/packages/icons/src/IconSymbolLoader.tsx b/packages/icons/src/IconSymbolLoader.tsx
index ebc698123..e11453ee5 100644
--- a/packages/icons/src/IconSymbolLoader.tsx
+++ b/packages/icons/src/IconSymbolLoader.tsx
@@ -38,6 +38,26 @@ function buildSymbolURL(loaderURL: string, style: string, icon: string): string
return `${normalizedLoaderURL}/${encodeURIComponent(style)}/${encodeURIComponent(icon)}`;
}
+function appendSymbolsFromDocument(markup: string): boolean {
+ const parsed = new DOMParser().parseFromString(markup, 'image/svg+xml');
+ const symbols = Array.from(parsed.querySelectorAll('symbol'));
+ if (symbols.length === 0) {
+ return false;
+ }
+
+ const spriteRoot = getSpriteRoot();
+ for (const symbol of symbols) {
+ const symbolId = symbol.getAttribute('id');
+ if (!symbolId || hasSymbol(symbolId)) {
+ continue;
+ }
+
+ spriteRoot.appendChild(document.importNode(symbol, true));
+ }
+
+ return true;
+}
+
async function loadSymbol(symbolId: string, loaderURL: string, style: string, icon: string) {
if (hasSymbol(symbolId)) {
return true;
@@ -61,7 +81,10 @@ async function loadSymbol(symbolId: string, loaderURL: string, style: string, ic
return true;
}
- getSpriteRoot().insertAdjacentHTML('beforeend', symbolMarkup);
+ if (!appendSymbolsFromDocument(symbolMarkup)) {
+ return false;
+ }
+
return hasSymbol(symbolId);
})
.catch(() => false)