feat(banners): ✨ custom icon support
This commit is contained in:
@@ -5,6 +5,7 @@ export const GET = (async ({ fetch }): Promise<Response> => {
|
||||
const banner = await newBanner({
|
||||
title: 'Hello world',
|
||||
subtitle: 'This is a test!',
|
||||
icon: 'https://raw.githubusercontent.com/LoredDev/.github/main/assets/designs/dots-icon-dark.svg',
|
||||
colors: {
|
||||
background: '#000000',
|
||||
foreground: '#ffffff',
|
||||
@@ -23,7 +24,9 @@ export const GET = (async ({ fetch }): Promise<Response> => {
|
||||
style: 'normal',
|
||||
},
|
||||
},
|
||||
rtl: true,
|
||||
libConfig: {
|
||||
fetcher: fetch,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(`${banner.toString()}`, {
|
||||
|
||||
@@ -63,16 +63,10 @@ export function generateBannerHtml({
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: ${horizontal ? '1.5' : '0'}em 0;
|
||||
width: 3.5em;
|
||||
height: 3.5em;
|
||||
">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="3.5em" height="3.5em" viewBox="0 0 24 24">
|
||||
<g fill="currentColor">
|
||||
<path
|
||||
d="M15.75 2a.75.75 0 0 0-1.5 0v20a.75.75 0 0 0 1.5 0v-2.006c2.636-.027 4.104-.191 5.078-1.166C22 17.657 22 15.771 22 12c0-3.771 0-5.657-1.172-6.828c-.974-.975-2.442-1.139-5.078-1.166V2Z" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 20c-3.771 0-5.657 0-6.828-1.172C2 17.657 2 15.771 2 12c0-3.771 0-5.657 1.172-6.828C4.343 4 6.229 4 10 4h3v16h-3ZM6.818 7.787c.3-.037.666-.037 1.066-.037h2.232c.4 0 .766 0 1.066.037c.329.041.68.137.98.405c.052.046.1.094.146.146c.268.3.364.651.405.98c.037.3.037.666.037 1.066v.041a.75.75 0 0 1-1.5 0c0-.455-.001-.726-.026-.922c-.024-.195-.228-.227-.228-.227c-.195-.025-.466-.026-.921-.026H9.75v5.5H11a.75.75 0 0 1 0 1.5H7a.75.75 0 0 1 0-1.5h1.25v-5.5h-.325c-.455 0-.726.001-.922.026c0 0-.203.032-.227.227c-.025.196-.026.467-.026.922a.75.75 0 0 1-1.5 0v-.041c0-.4 0-.766.037-1.066c.041-.329.137-.68.405-.98c.046-.052.094-.1.146-.146c.3-.268.651-.364.98-.405Z"
|
||||
clip-rule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
%%MARKNOW-PLACEHOLDER-ICON%%
|
||||
</div>
|
||||
<div style="
|
||||
align-items: center;
|
||||
|
||||
62
packages/banners/src/icons.js
Normal file
62
packages/banners/src/icons.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @param {string} icon
|
||||
* @param {import('./types').Fetcher | undefined} fetcher
|
||||
*
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
export async function getIcon(icon, fetcher = fetch) {
|
||||
if ((!isIconName(icon) && !isValidUrl(icon))) {
|
||||
return icon;
|
||||
}
|
||||
else if (isValidUrl(icon)) {
|
||||
const svg = (await fetcher(icon)).text();
|
||||
|
||||
return setIconDimensions(await svg);
|
||||
}
|
||||
|
||||
const [collection, iconName] = icon.split(':');
|
||||
|
||||
const svg = (await fetcher(`https://api.iconify.design/${collection}/${iconName}.svg`)).text();
|
||||
|
||||
return setIconDimensions(await svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given string is a valid [Iconify](https://iconify.design/)/[Icônes](https://icones.js.org/)-like icon name.
|
||||
*
|
||||
* @param {string} iconName
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isIconName(iconName) {
|
||||
try {
|
||||
const [collection, icon] = iconName.split(':');
|
||||
return Boolean(collection) && Boolean(icon);
|
||||
}
|
||||
catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} string
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isValidUrl(string) {
|
||||
try {
|
||||
const url = new URL(string);
|
||||
return url.protocol === 'https:' || url.protocol === 'http:';
|
||||
}
|
||||
catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} svg
|
||||
* @return {string}
|
||||
*/
|
||||
function setIconDimensions(svg) {
|
||||
return svg
|
||||
.replace(/width="([^"]*)"/, 'width="3.5em"')
|
||||
.replace(/height="([^"]*)"/, '');
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { html as htmlToVNodes } from 'satori-html';
|
||||
import satori from 'satori';
|
||||
import { generateBannerHtml } from './html';
|
||||
import { getMonaSansFonts } from './fonts';
|
||||
import { getIcon } from './icons';
|
||||
|
||||
/**
|
||||
* @param {import('./types').BannerOptions} options
|
||||
@@ -10,15 +11,18 @@ import { getMonaSansFonts } from './fonts';
|
||||
export default async function banner({
|
||||
title,
|
||||
subtitle = '',
|
||||
icon = '',
|
||||
layout = 'horizontal',
|
||||
colors = {
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
},
|
||||
layout = 'horizontal',
|
||||
libConfig: config = {},
|
||||
font: customFonts,
|
||||
rtl = false,
|
||||
libConfig: config,
|
||||
}) {
|
||||
config.iconHandler ||= icon => getIcon(icon, config?.fetcher);
|
||||
|
||||
const dimensions = {
|
||||
width: 1000,
|
||||
height: 180,
|
||||
@@ -38,9 +42,12 @@ export default async function banner({
|
||||
|
||||
const htmlTemplate = generateBannerHtml({ layout, dimensions, fonts: bannerFonts, colors, rtl });
|
||||
|
||||
const iconSvg = await config.iconHandler(icon);
|
||||
|
||||
const html = htmlTemplate
|
||||
.replace('%%MARKNOW-PLACEHOLDER-TITLE%%', title)
|
||||
.replace('%%MARKNOW-PLACEHOLDER-SUBTILE%%', subtitle);
|
||||
.replace('%%MARKNOW-PLACEHOLDER-SUBTILE%%', subtitle)
|
||||
.replace('%%MARKNOW-PLACEHOLDER-ICON%%', iconSvg);
|
||||
|
||||
const vNodes = htmlToVNodes(html);
|
||||
|
||||
@@ -56,6 +63,7 @@ export default async function banner({
|
||||
html,
|
||||
vNodes,
|
||||
svg,
|
||||
icon: iconSvg,
|
||||
toString() { return svg; },
|
||||
};
|
||||
}
|
||||
|
||||
10
packages/banners/src/types.d.ts
vendored
10
packages/banners/src/types.d.ts
vendored
@@ -10,6 +10,10 @@ export type Colors = {
|
||||
background: string;
|
||||
}
|
||||
|
||||
export type Fetcher = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit
|
||||
) => Promise<Response>;
|
||||
|
||||
export type Reader = (
|
||||
path: PathLike | FileHandle,
|
||||
@@ -21,6 +25,7 @@ export type Reader = (
|
||||
* @package `@marknow/banners`
|
||||
*/
|
||||
export interface BannerOptions {
|
||||
icon?: string,
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
layout?: 'horizontal' | 'vertical',
|
||||
@@ -31,7 +36,9 @@ export interface BannerOptions {
|
||||
colors?: Colors,
|
||||
rtl?: boolean,
|
||||
libConfig?: {
|
||||
fetcher?: Fetcher = fetch,
|
||||
reader?: Reader,
|
||||
iconHandler?: (icon: string) => string | Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +49,9 @@ export interface Banner {
|
||||
toString(): string,
|
||||
html: string,
|
||||
svg: string,
|
||||
icon: string,
|
||||
vNodes: VNode,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* **Copied from the satori-html package,**
|
||||
|
||||
Reference in New Issue
Block a user