diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 204d07c..daf05fd 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -226,6 +226,7 @@ module.exports = { 'duotone', 'tsconfig', 'workspace', + 'woff', ], minLength: 4, }], diff --git a/apps/www/.eslintignore b/apps/www/.eslintignore index c191179..7c1958c 100644 --- a/apps/www/.eslintignore +++ b/apps/www/.eslintignore @@ -1,5 +1,3 @@ /src/lib/components.d.ts /src/lib/imports.d.ts /.eslint-auto-import.json -*.woff -*.woff2 diff --git a/apps/www/src/routes/api.svg/+server.ts b/apps/www/src/routes/api.svg/+server.ts index da95891..4457ad2 100644 --- a/apps/www/src/routes/api.svg/+server.ts +++ b/apps/www/src/routes/api.svg/+server.ts @@ -1,33 +1,12 @@ import type { RequestHandler } from '@sveltejs/kit'; -import satori from 'satori'; -import { html as satoriHtml } from 'satori-html'; -import Banner from './Banner.html?raw'; -import font400 from '$lib/assets/Mona-Sans-Regular.woff?url'; -import font600 from '$lib/assets/Mona-Sans-SemiBold.woff?url'; +import newBanner from '@marknow/banners'; -export const GET = (async ({ fetch }): Promise => { - const html = satoriHtml(Banner); +export const GET = (async (): Promise => { + const banner = await newBanner({ + title: 'Hello world', + }); - const banner = await satori(html, - { - width: 1000, - height: 180, - fonts: [ - { - name: 'Mona Sans', - weight: 400, - style: 'normal', - data: await (await fetch(font400)).arrayBuffer(), - }, - { - name: 'Mona Sans', - weight: 600, - style: 'normal', - data: await (await fetch(font600)).arrayBuffer(), - }], - }); - - return new Response(banner, { + return new Response(`${banner.toString()}`, { status: 200, headers: { 'Content-type': 'image/svg+xml', diff --git a/apps/www/src/routes/api.svg/Banner.html b/apps/www/src/routes/api.svg/Banner.html deleted file mode 100644 index a0066b4..0000000 --- a/apps/www/src/routes/api.svg/Banner.html +++ /dev/null @@ -1,48 +0,0 @@ -
-
-
- - - - - - -
-
-
-

Marknow

- - Create beautiful markdown for your projects with ease - -
-
-
-
diff --git a/packages/banners/.eslintignore b/packages/banners/.eslintignore index 9b1c8b1..090e4b1 100644 --- a/packages/banners/.eslintignore +++ b/packages/banners/.eslintignore @@ -1 +1,3 @@ /dist +*.woff +*.woff2 diff --git a/apps/www/src/lib/assets/Mona-Sans-Regular.woff b/packages/banners/src/assets/Mona-Sans-Regular.woff similarity index 100% rename from apps/www/src/lib/assets/Mona-Sans-Regular.woff rename to packages/banners/src/assets/Mona-Sans-Regular.woff diff --git a/apps/www/src/lib/assets/Mona-Sans-SemiBold.woff b/packages/banners/src/assets/Mona-Sans-SemiBold.woff similarity index 100% rename from apps/www/src/lib/assets/Mona-Sans-SemiBold.woff rename to packages/banners/src/assets/Mona-Sans-SemiBold.woff diff --git a/packages/banners/src/fonts.js b/packages/banners/src/fonts.js new file mode 100644 index 0000000..5604b0a --- /dev/null +++ b/packages/banners/src/fonts.js @@ -0,0 +1,29 @@ +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * @param {import('./types').Reader | undefined} reader + * @typedef {import('satori').SatoriOptions['fonts'][0]} Font + * @returns {Promise<{regular: Font, bold: Font}>} + */ +export async function getMonaSansFonts(reader) { + reader ||= (await import('node:fs/promises')).readFile; + + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + + return { + regular: { + name: 'Mona Sans', + weight: 400, + style: 'normal', + data: await reader(join(__dirname, './assets/Mona-Sans-Regular.woff')), + }, + bold: { + name: 'Mona Sans', + weight: 600, + style: 'normal', + data: await reader(join(__dirname, './assets/Mona-Sans-SemiBold.woff')), + }, + }; +} diff --git a/packages/banners/src/html.js b/packages/banners/src/html.js new file mode 100644 index 0000000..eb40e1d --- /dev/null +++ b/packages/banners/src/html.js @@ -0,0 +1,85 @@ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/indent */ +/** + * Returns the html string of the banner to be used by satori. + * Use the params to customize and complete it. + * + * @param {'vertical' | 'horizontal'} layout + * @param {{width: number, height: number}} dimensions + * + * @return {string} + */ +export function generateBannerHtml(layout, dimensions) { + /** @type {boolean} */ + const horizontal = layout === 'horizontal'; + + return ` +
+
+
+ + + + + + +
+
+
+

+ %%MARKNOW-PLACEHOLDER-TITLE%% +

+ + %%MARKNOW-PLACEHOLDER-SUBTILE%% + +
+
+
+
+ `; +} diff --git a/packages/banners/src/index.d.ts b/packages/banners/src/index.d.ts new file mode 100644 index 0000000..3f1888b --- /dev/null +++ b/packages/banners/src/index.d.ts @@ -0,0 +1,3 @@ +import type { BannerOptions, Banner } from "./types"; + +export default async function banner(options: BannerOptions): Promise; diff --git a/packages/banners/src/index.js b/packages/banners/src/index.js new file mode 100644 index 0000000..50705af --- /dev/null +++ b/packages/banners/src/index.js @@ -0,0 +1,43 @@ +import { html as htmlToVNodes } from 'satori-html'; +import satori from 'satori'; +import { generateBannerHtml } from './html'; +import { getMonaSansFonts } from './fonts'; + +/** + * @param {import('./types').BannerOptions} options + * @returns {Promise} + */ +export default async function banner({ + title, + subtitle = '', + layout = 'horizontal', + config, +}) { + const dimensions = { + width: 1000, + height: layout === 'horizontal' ? 180 : 680, + }; + + const bannerFonts = await getMonaSansFonts(config?.reader); + + const html = generateBannerHtml(layout, dimensions) + .replace('%%MARKNOW-PLACEHOLDER-TITLE%%', title) + .replace('%%MARKNOW-PLACEHOLDER-SUBTILE%%', subtitle); + + const vNodes = htmlToVNodes(html); + + const svg = await satori(vNodes, { + ...dimensions, + fonts: [ + bannerFonts.bold, + bannerFonts.regular, + ], + }); + + return { + html, + vNodes, + svg, + toString() { return svg; }, + }; +} diff --git a/packages/banners/src/types.d.ts b/packages/banners/src/types.d.ts new file mode 100644 index 0000000..e632df3 --- /dev/null +++ b/packages/banners/src/types.d.ts @@ -0,0 +1,46 @@ +import type { Abortable } from "node:events"; +import type { OpenMode, PathLike } from "node:fs"; +import type { FileHandle } from "node:fs/promises"; + +export type Reader = ( + path: PathLike | FileHandle, +) => Promise + +/** + * Options object for creating a banner. + * + * @package `@marknow/banners` + */ +export interface BannerOptions { + title: string, + subtitle?: string, + layout?: 'horizontal' | 'vertical' = 'horizontal', + config?: { + reader?: Reader, + } +} + +/** + * + */ +export interface Banner { + toString(): string, + html: string, + svg: string, + vNodes: VNode, +} + +/** + * **Copied from the satori-html package,** + * React-element-like objects / VDOM object used in satori. + * + * @package `satori-html` + */ +export interface VNode { + type: string; + props: { + style?: Record; + children?: string | VNode | VNode[]; + [prop: string]: any; + }; +}