Merge pull request #5 from LoredDev/dev@package-banners
Feature(package-banners): Move the banner creation logic to it's own package.
@@ -188,7 +188,7 @@ module.exports = {
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
'max-depth': ['error', 3],
|
||||
'max-nested-callbacks': ['error', 3],
|
||||
'complexity': ['error', 4],
|
||||
'complexity': ['error', 8],
|
||||
'no-tabs': ['error', { allowIndentationTabs: true }],
|
||||
'spellcheck/spell-checker': ['error', {
|
||||
skipWords: [
|
||||
@@ -226,6 +226,13 @@ module.exports = {
|
||||
'duotone',
|
||||
'tsconfig',
|
||||
'workspace',
|
||||
'woff',
|
||||
'marknow',
|
||||
'lored',
|
||||
'guz013',
|
||||
'xml',
|
||||
'jsconfig',
|
||||
'vitest',
|
||||
],
|
||||
minLength: 4,
|
||||
}],
|
||||
|
||||
4
.vscode/project.code-workspace
vendored
@@ -4,6 +4,10 @@
|
||||
"path": "../apps/www",
|
||||
"name": "apps/www"
|
||||
},
|
||||
{
|
||||
"path": "../packages/banners",
|
||||
"name": "@marknow/banners"
|
||||
},
|
||||
{
|
||||
"path": "../",
|
||||
"name": "ROOT"
|
||||
|
||||
3
.vscode/settings.json
vendored
@@ -4,5 +4,6 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"unocss.root": "apps/www"
|
||||
"unocss.root": "apps/www",
|
||||
"vitest.enable": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/src/lib/components.d.ts
|
||||
/src/lib/imports.d.ts
|
||||
/.eslint-auto-import.json
|
||||
*.woff
|
||||
*.woff2
|
||||
/static
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@iconify-json/svg-spinners": "^1.1.1",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"@iconify/utils": "^2.1.6",
|
||||
"@marknow/banners": "workspace:*",
|
||||
"@poppanator/sveltekit-svg": "^3.0.1",
|
||||
"@sveltejs/adapter-vercel": "^3.0.1",
|
||||
"@sveltejs/kit": "^1.20.2",
|
||||
@@ -30,9 +31,8 @@
|
||||
"@unocss/extractor-svelte": "^0.52.7",
|
||||
"mdsvex": "^0.10.6",
|
||||
"rehype-external-links": "^2.1.0",
|
||||
"sass": "^1.63.4",
|
||||
"sass": "^1.63.6",
|
||||
"satori": "^0.10.1",
|
||||
"satori-html": "^0.3.2",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte-check": "^3.4.3",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<main un-grid="~ rows-4">
|
||||
<div un-h="sm" un-flex="~ justify-center items-center" un-bg="#33578a">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html data.banner}
|
||||
<img src="/api.svg" alt="">
|
||||
</div>
|
||||
<div un-h="sm" un-flex="~ justify-center items-center" un-bg="#0d1117">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html data.banner}
|
||||
<img src="/api.svg" alt="">
|
||||
</div>
|
||||
<div un-h="sm" un-flex="~ justify-center items-center" un-bg="#ffffff">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html data.banner}
|
||||
<img src="/api.svg" alt="">
|
||||
</div>
|
||||
<div un-h="sm" un-flex="~ justify-center items-center" un-bg="#0a0c10">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html data.banner}
|
||||
<img src="/api.svg" alt="">
|
||||
</div>
|
||||
<div un-h="sm" un-flex="~ justify-center items-center" un-bg="#22272e">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html data.banner}
|
||||
<img src="/api.svg" alt="">
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ fetch }): Promise<{ banner: string }> => {
|
||||
const banner = await (await fetch('/api.svg')).text();
|
||||
return { banner };
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,33 +1,35 @@
|
||||
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<Response> => {
|
||||
const html = satoriHtml(Banner);
|
||||
const banner = await newBanner({
|
||||
title: 'Hello world',
|
||||
subtitle: 'This is a test!',
|
||||
icon: 'solar:hand-shake-bold-duotone',
|
||||
colors: {
|
||||
background: '#000000',
|
||||
foreground: '#ffffff',
|
||||
},
|
||||
fonts: {
|
||||
title: {
|
||||
data: await (await fetch('/Mona-Sans-SemiBold.woff')).arrayBuffer(),
|
||||
name: 'Mona Sans',
|
||||
weight: 600,
|
||||
style: 'normal',
|
||||
},
|
||||
subtitle: {
|
||||
data: await (await fetch('/Mona-Sans-Regular.woff')).arrayBuffer(),
|
||||
name: 'Mona Sans',
|
||||
weight: 400,
|
||||
style: 'normal',
|
||||
},
|
||||
},
|
||||
libConfig: {
|
||||
fetcher: fetch,
|
||||
},
|
||||
});
|
||||
|
||||
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',
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
width: 1000px;
|
||||
height: 180px;
|
||||
">
|
||||
<div style="
|
||||
box-shadow: 0 5px 12px #00000040;
|
||||
position: relative;
|
||||
font-family: 'Mona Sans';
|
||||
background-color: white;
|
||||
margin: auto;
|
||||
border-radius: 1em;
|
||||
padding: 1.2em 2.5em;
|
||||
display: flex;
|
||||
min-width: 98%;
|
||||
min-height: 20%;
|
||||
gap: 1em;
|
||||
">
|
||||
<div style="
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 1.5em 0;
|
||||
">
|
||||
<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>
|
||||
</div>
|
||||
<div style="
|
||||
align-items: center;
|
||||
display: flex;
|
||||
">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<h1 style="margin: 0; font-weight: 600;">Marknow</h1>
|
||||
<sub style="font-size: medium; font-weight: 400;">
|
||||
Create beautiful markdown for your projects with ease
|
||||
</sub>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,6 +8,8 @@
|
||||
"keywords": [],
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "turbo run test",
|
||||
"test:watch": "vitest watch",
|
||||
"prepare": "husky install",
|
||||
"lint": "eslint . && turbo run lint",
|
||||
"lint:fix": "eslint . --fix && turbo run lint -- --fix",
|
||||
@@ -24,6 +26,7 @@
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"husky": "^8.0.3",
|
||||
"turbo": "^1.10.3",
|
||||
"vercel": "^30.2.2"
|
||||
"vercel": "^30.2.2",
|
||||
"vitest": "^0.32.2"
|
||||
}
|
||||
}
|
||||
|
||||
3
packages/banners/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
/dist
|
||||
*.woff
|
||||
*.woff2
|
||||
5
packages/banners/.eslintrc.cjs
Normal file
@@ -0,0 +1,5 @@
|
||||
process.env.ESLINT_TSCONFIG = 'jsconfig.json';
|
||||
|
||||
module.exports = {
|
||||
root: false,
|
||||
};
|
||||
1
packages/banners/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/dist
|
||||
4
packages/banners/.lintstagedrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"gitDir": "../../.git",
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
17
packages/banners/jsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"module": "ES2022",
|
||||
"target": "ES2022",
|
||||
"alwaysStrict": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["./node_modules/**", "./dist/**"]
|
||||
}
|
||||
35
packages/banners/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@marknow/banners",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"source": "./src/index.js",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"keywords": [],
|
||||
"exports": {
|
||||
"types": "./src/index.d.ts",
|
||||
"require": "./dist/index.cjs",
|
||||
"import": "./src/index.js",
|
||||
"default": "./src/index.js"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./src/index.js",
|
||||
"types": "./src/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"build:cjs": "microbundle --compress false --pkg-main false --strict --tsconfig ./jsconfig.json --generateTypes false -f cjs --target node",
|
||||
"build": "pnpm run build:cjs",
|
||||
"dev": "pnpm run build -w",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"satori": "^0.10.1",
|
||||
"satori-html": "^0.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.3.1",
|
||||
"microbundle": "^0.15.1"
|
||||
}
|
||||
}
|
||||
BIN
packages/banners/src/assets/Mona-Sans-Regular.woff
Normal file
BIN
packages/banners/src/assets/Mona-Sans-SemiBold.woff
Normal file
47
packages/banners/src/fonts.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @file
|
||||
* Utility functions used to retrieve and manipulate fonts' objects.
|
||||
*
|
||||
* @author
|
||||
* Guz013 (under the Lored organization) <https://github.com/LoredDev>
|
||||
*
|
||||
* @copyright
|
||||
* Gustavo "Guz013" L. de Mello
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
*/
|
||||
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/**
|
||||
* @param {import('./types').Reader | undefined} [reader=import('node:fs').readFile]
|
||||
* The function to be used as reader of the local files.
|
||||
*
|
||||
* @typedef {import('satori').SatoriOptions['fonts'][0]} Font
|
||||
* @returns {Promise<{subtitle: Font, title: Font}>}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access protected
|
||||
*/
|
||||
export async function getMonaSansFonts(reader) {
|
||||
reader ||= (await import('node:fs/promises')).readFile;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
return {
|
||||
subtitle: {
|
||||
name: 'Mona Sans',
|
||||
weight: 400,
|
||||
style: 'normal',
|
||||
data: await reader(join(__dirname, './assets/Mona-Sans-Regular.woff')),
|
||||
},
|
||||
title: {
|
||||
name: 'Mona Sans',
|
||||
weight: 600,
|
||||
style: 'normal',
|
||||
data: await reader(join(__dirname, './assets/Mona-Sans-SemiBold.woff')),
|
||||
},
|
||||
};
|
||||
}
|
||||
117
packages/banners/src/html.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @author
|
||||
* Guz013 (under the Lored organization) <https://github.com/LoredDev>
|
||||
*
|
||||
* @copyright
|
||||
* Gustavo "Guz013" L. de Mello
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
/* eslint-disable @typescript-eslint/indent */
|
||||
/**
|
||||
* Constructor of the HTML banner converted to VNodes to be used in satori.
|
||||
* Use the properties to customize and complete it.
|
||||
*
|
||||
* @typedef {import('satori').SatoriOptions['fonts'][0]} Font
|
||||
* @typedef {{
|
||||
* dimensions: { width: number, height: number },
|
||||
* fonts: { title: Font, subtitle: Font },
|
||||
* layout: 'vertical' | 'horizontal',
|
||||
* colors: import('./types').Colors,
|
||||
* rtl: boolean,
|
||||
* }} Props
|
||||
* @param {Props} properties
|
||||
* Properties to be applied on the html.
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access protected
|
||||
*/
|
||||
export default function html({
|
||||
dimensions,
|
||||
layout,
|
||||
colors,
|
||||
fonts,
|
||||
rtl,
|
||||
}) {
|
||||
const horizontal = layout === 'horizontal';
|
||||
|
||||
return `
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
width: ${dimensions.width}px;
|
||||
height: ${dimensions.height}px;
|
||||
">
|
||||
<div style="
|
||||
box-shadow: 0 5px 12px #00000040;
|
||||
position: relative;
|
||||
color: ${colors.foreground};
|
||||
background-color: ${colors.background};
|
||||
margin: auto;
|
||||
border-radius: 1em;
|
||||
padding: ${horizontal ? '1.2' : '2.5'}em 2.5em;
|
||||
display: flex;
|
||||
${horizontal
|
||||
? rtl
|
||||
? 'flex-direction: row-reverse;'
|
||||
: 'flex-direction: row;'
|
||||
: 'flex-direction: column;'
|
||||
}
|
||||
align-items: center;
|
||||
${horizontal
|
||||
? 'justify-content: flex-start;'
|
||||
: 'justify-content: center;'
|
||||
}
|
||||
min-width: 98%;
|
||||
min-height: 20%;
|
||||
gap: 1em;
|
||||
">
|
||||
<div style="
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: ${horizontal ? '1.5' : '0'}em 0;
|
||||
width: 3.5em;
|
||||
height: 3.5em;
|
||||
">
|
||||
%%MARKNOW-PLACEHOLDER-ICON%%
|
||||
</div>
|
||||
<div style="
|
||||
align-items: center;
|
||||
display: flex;
|
||||
">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<h1 style="
|
||||
margin: ${horizontal ? '0' : '0 auto 1em auto'};
|
||||
font-weight: ${fonts.title.weight};
|
||||
font-family: ${fonts.title.name};
|
||||
text-overflow: ellipsis;
|
||||
max-width: 50em;
|
||||
display: flex;
|
||||
flex-direction: ${rtl ? 'row-reverse' : 'row'};
|
||||
">
|
||||
%%MARKNOW-PLACEHOLDER-TITLE%%
|
||||
</h1>
|
||||
<sub style="
|
||||
font-size: medium;
|
||||
font-weight: ${fonts.subtitle.weight};
|
||||
font-family: ${fonts.subtitle.name};
|
||||
text-overflow: ellipsis;
|
||||
max-width: 50em;
|
||||
display: flex;
|
||||
flex-direction: ${rtl ? 'row-reverse' : 'row'};
|
||||
${horizontal
|
||||
? 'margin: 0;'
|
||||
: 'margin: 0 auto;'
|
||||
}
|
||||
">
|
||||
%%MARKNOW-PLACEHOLDER-SUBTILE%%
|
||||
</sub>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
109
packages/banners/src/icons.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @file
|
||||
* Utility functions used to retrieve and manipulate icons SVG strings.
|
||||
*
|
||||
* @author
|
||||
* Guz013 (under the Lored organization) <https://github.com/LoredDev>
|
||||
*
|
||||
* @copyright
|
||||
* Gustavo "Guz013" L. de Mello
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utility function used to get a SVG icon from Iconify OR from an url passed as the icon.
|
||||
*
|
||||
* If the `icon` parameter is not a valid icon name or url,
|
||||
* it returns the `icon` itself.
|
||||
*
|
||||
* @param {string} icon
|
||||
* The icon's name or url endpoint.
|
||||
*
|
||||
* @param {import('./types').Fetcher | undefined} [fetcher=globalThis.fetch]
|
||||
* Fetch function to be used.
|
||||
*
|
||||
* @returns {Promise<string>}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access protected
|
||||
*/
|
||||
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 svg;
|
||||
}
|
||||
|
||||
const [collection, iconName] = icon.split(':');
|
||||
const svg = (await fetcher(`https://api.iconify.design/${collection}/${iconName}.svg`)).text();
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function used to set the icons SVG width and height to the specified dimensions.
|
||||
*
|
||||
* @param {string} svg
|
||||
* The svg string.
|
||||
*
|
||||
* @param {{width?: string | number, height?: string | number}} dimensions
|
||||
* The dimensions values, if type number it is converted to pixels.
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access protected
|
||||
*/
|
||||
export function setIconDimensions(svg, { width, height }) {
|
||||
width = typeof width === 'number'
|
||||
? `width="${width}px"`
|
||||
: !width
|
||||
? ''
|
||||
: `width="${width}"`;
|
||||
|
||||
height = typeof height === 'number'
|
||||
? `height="${height}px"`
|
||||
: !height
|
||||
? ''
|
||||
: `height="${height}"`;
|
||||
|
||||
return svg
|
||||
.replace(/width="([^"]*)"/, width)
|
||||
.replace(/height="([^"]*)"/, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given string is a valid
|
||||
* [Iconify](https://iconify.design/)/[Icônes](https://icones.js.org/)-like icon name.
|
||||
*
|
||||
* @param {string} string The string to be checked.
|
||||
* @returns {boolean}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access package
|
||||
*/
|
||||
function isIconName(string) {
|
||||
return /^[a-z0-9-]+:[a-z0-9-]+(\[\])?$/.test(string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string is a valid URL.
|
||||
*
|
||||
* @param {string} string The string to be checked.
|
||||
* @returns {boolean}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access package
|
||||
*/
|
||||
function isValidUrl(string) {
|
||||
try {
|
||||
const url = new URL(string);
|
||||
return url.protocol === 'https:' || url.protocol === 'http:';
|
||||
}
|
||||
catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
66
packages/banners/src/index.d.ts
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @file
|
||||
* This file contains all public functions and types declaration of the
|
||||
* `@marknow/banners` package. Anything declared here can be accessed directly
|
||||
* by the library consumers and has an @access public access level.
|
||||
*
|
||||
* The JSDocs of the functions are duplicated from the source files, so that
|
||||
* it is more compatible with Typescript syntax and code documentation.
|
||||
*
|
||||
* @author
|
||||
* Guz013 (under the Lored organization) <https://github.com/LoredDev>
|
||||
*
|
||||
* @copyright
|
||||
* Gustavo "Guz013" L. de Mello
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
*/
|
||||
|
||||
/**
|
||||
* The banner constructor function. Use the options to customize the
|
||||
* appearance of the resulting banner.
|
||||
*
|
||||
* @param {import('./types').BannerOptions} options
|
||||
* Options object for customizing the banner appearance.
|
||||
*
|
||||
* @returns {Promise<import('./types').Banner>}
|
||||
*
|
||||
* @example
|
||||
* import newBanner from '@marknow/banners';
|
||||
*
|
||||
* export async function GET({ fetch }) {
|
||||
* const banner = await newBanner({
|
||||
* title: 'Hello world',
|
||||
* subtitle: 'This is a example api endpoint.'
|
||||
* icon: 'material-symbols:api'
|
||||
* fonts: {
|
||||
* title: {
|
||||
* data: await (await fetch('/Mona-Sans-SemiBold.woff')).arrayBuffer(),
|
||||
* name: 'Mona Sans',
|
||||
* weight: 600,
|
||||
* },
|
||||
* subtitle: {
|
||||
* data: await (await fetch('/Mona-Sans-Regular.woff')).arrayBuffer(),
|
||||
* name: 'Mona Sans',
|
||||
* weight: 400,
|
||||
* },
|
||||
* },
|
||||
* libConfig: {
|
||||
* fetcher: fetch,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* return new Response(banner.toString(), {
|
||||
* status: 200,
|
||||
* headers: {
|
||||
* 'Content-type': 'image/svg+xml',
|
||||
* },
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access public
|
||||
*/
|
||||
export default function banner({ title, subtitle, icon, layout, config, }: import('./types').BannerOptions): Promise<import('./types').Banner>;
|
||||
|
||||
export type { BannerOptions, Banner } from './types'
|
||||
132
packages/banners/src/index.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @file
|
||||
* Main entry point of the `@marknow/banners` package. This file contains
|
||||
* all ESModules exports of the public APIs and functions of the library.
|
||||
* Anything exported here should have an @access public access level and
|
||||
* declared on {@link ./index.d.ts types declaration} file.
|
||||
*
|
||||
* @author
|
||||
* Guz013 (under the Lored organization) <https://github.com/LoredDev>
|
||||
*
|
||||
* @copyright
|
||||
* Gustavo "Guz013" L. de Mello
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
*/
|
||||
import { html as htmlToVNodes } from 'satori-html';
|
||||
import satori from 'satori';
|
||||
import bannerHtml from './html';
|
||||
import { getMonaSansFonts } from './fonts';
|
||||
import { getIcon, setIconDimensions } from './icons';
|
||||
|
||||
/**
|
||||
* The banner constructor function. Use the options to customize the
|
||||
* appearance of the resulting banner.
|
||||
*
|
||||
* @param {import('./types').BannerOptions} options
|
||||
* Options object for customizing the banner appearance.
|
||||
*
|
||||
* @returns {Promise<import('./types').Banner>}
|
||||
*
|
||||
* @example
|
||||
* import newBanner from '@marknow/banners';
|
||||
*
|
||||
* export async function GET({ fetch }) {
|
||||
* const banner = await newBanner({
|
||||
* title: 'Hello world',
|
||||
* subtitle: 'This is a example api endpoint.'
|
||||
* icon: 'material-symbols:api'
|
||||
* fonts: {
|
||||
* title: {
|
||||
* data: await (await fetch('/Mona-Sans-SemiBold.woff')).arrayBuffer(),
|
||||
* name: 'Mona Sans',
|
||||
* weight: 600,
|
||||
* },
|
||||
* subtitle: {
|
||||
* data: await (await fetch('/Mona-Sans-Regular.woff')).arrayBuffer(),
|
||||
* name: 'Mona Sans',
|
||||
* weight: 400,
|
||||
* },
|
||||
* },
|
||||
* libConfig: {
|
||||
* fetcher: fetch,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* return new Response(banner.toString(), {
|
||||
* status: 200,
|
||||
* headers: {
|
||||
* 'Content-type': 'image/svg+xml',
|
||||
* },
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access public
|
||||
*/
|
||||
export default async function banner({
|
||||
title,
|
||||
fonts,
|
||||
icon = '',
|
||||
rtl = false,
|
||||
subtitle = '',
|
||||
layout = 'horizontal',
|
||||
libConfig: config = {},
|
||||
colors = {
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
},
|
||||
}) {
|
||||
fonts ||= await getMonaSansFonts(config?.reader);
|
||||
config.iconHandler ||= icon => getIcon(icon, config?.fetcher);
|
||||
|
||||
title = truncateText(title, layout === 'horizontal' ? 45 : 90);
|
||||
subtitle = truncateText(subtitle, layout === 'horizontal' ? 100 : 200);
|
||||
|
||||
const dimensions = {
|
||||
width: 1000,
|
||||
height: layout === 'horizontal' ? 180 : 280,
|
||||
};
|
||||
|
||||
const htmlTemplate = bannerHtml({ layout, dimensions, fonts, colors, rtl });
|
||||
|
||||
const iconSvg = setIconDimensions(await config.iconHandler(icon), { width: '3.5em' });
|
||||
|
||||
const html = htmlTemplate
|
||||
.replace('%%MARKNOW-PLACEHOLDER-TITLE%%', title)
|
||||
.replace('%%MARKNOW-PLACEHOLDER-SUBTILE%%', subtitle)
|
||||
.replace('%%MARKNOW-PLACEHOLDER-ICON%%', iconSvg);
|
||||
|
||||
const vNodes = htmlToVNodes(html);
|
||||
|
||||
const svg = await satori(vNodes, {
|
||||
...dimensions,
|
||||
fonts: [
|
||||
fonts.title,
|
||||
fonts.subtitle,
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
toString() { return svg; },
|
||||
icon: iconSvg,
|
||||
vNodes,
|
||||
html,
|
||||
svg,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Small utility function used to truncate long texts on the banner
|
||||
*
|
||||
* @param {string} string - Text string to be truncated.
|
||||
* @param {number} maxChar - Maximum number of characters.
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access package
|
||||
*/
|
||||
function truncateText(string, maxChar) {
|
||||
return string.length > maxChar ? `${string.slice(0, maxChar)}...` : string;
|
||||
}
|
||||
234
packages/banners/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* @file
|
||||
* Definitions of more complex or shared Typescript types used in the package.
|
||||
*
|
||||
* @author
|
||||
* Guz013 (under the Lored organization) <https://github.com/LoredDev>
|
||||
*
|
||||
* @copyright
|
||||
* Gustavo "Guz013" L. de Mello
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
*/
|
||||
|
||||
import type { OpenMode, PathLike } from "node:fs";
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import type { SatoriOptions } from "satori/wasm";
|
||||
|
||||
/**
|
||||
* Options object for customizing the banner appearance.
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access public
|
||||
*/
|
||||
export interface BannerOptions {
|
||||
/**
|
||||
* **(REQUIRED)** Title to be displayed in the banner.
|
||||
*/
|
||||
title: string,
|
||||
|
||||
/**
|
||||
* (Optional) Set the text direction right-to-left.
|
||||
*/
|
||||
rtl?: boolean = false,
|
||||
|
||||
/**
|
||||
* (Optional) {@link https://iconify.design/ Iconify}/{@link https://icones.js.org/ Icônes}-like
|
||||
* icon name OR url for svg file of the icon to be used in the banner.
|
||||
*/
|
||||
icon?: string = '',
|
||||
|
||||
/**
|
||||
* (Optional) Subtitle/legend to be displayed in the banner
|
||||
* bellow the title.
|
||||
*/
|
||||
subtitle?: string = '',
|
||||
|
||||
/**
|
||||
* (Optional) Customize the colors of the banner.
|
||||
*
|
||||
* Customize the background color and foreground (title, subtitle
|
||||
* and icon) colors.
|
||||
*/
|
||||
colors?: Colors = { background: '#ffffff', foreground: '#000000' },
|
||||
|
||||
/**
|
||||
* (Optional) Customize the layout of the banner and position of elements.
|
||||
*/
|
||||
layout?: 'horizontal' | 'vertical' = 'horizontal',
|
||||
|
||||
/**
|
||||
* (Optional) Customize the fonts used on the banner.
|
||||
*
|
||||
* Changes the default font file used for the title and subtitle.
|
||||
* Defaults is {@link https://github.com/github/mona-sans Github's Mona Sans}
|
||||
* semi-bold and regular respectively.
|
||||
*
|
||||
* @see {@link https://github.com/vercel/satori#fonts}
|
||||
*/
|
||||
fonts?: {
|
||||
title: Font,
|
||||
subtitle: Font,
|
||||
}
|
||||
|
||||
/**
|
||||
* (Optional) Customize the behavior of the package.
|
||||
*
|
||||
* Provide functions or polyfills to be used by the package
|
||||
* for better compatibility or customization of the banner
|
||||
* creation.
|
||||
*/
|
||||
libConfig?: {
|
||||
/**
|
||||
* (Optional) Fetch function used by the package to retrieve
|
||||
* icons from {@link https://iconify.design/ Iconify} and custom
|
||||
* icons provided as url.
|
||||
*
|
||||
* Default function used is the
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch globalThis.fetch}
|
||||
* function. Compatible with modern browsers, Node.js (version 18 and greater),
|
||||
* Deno and Bun.
|
||||
*
|
||||
* @see {@link BannerOptions.icon}
|
||||
*
|
||||
* @param {RequestInfo} input - The request url/info.
|
||||
* @param {RequestInit} - Request options.
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
fetcher?: Fetcher = globalThis.fetch,
|
||||
|
||||
/**
|
||||
* (Optional) The function used to read the font files and return a Buffer or
|
||||
* ArrayBuffer from them.
|
||||
*
|
||||
* Default function used is the {@link https://nodejs.org/api/fs.html#fsreadfilepath-options-callback fs.readFile}
|
||||
* from the Node file system promises api ({@link https://nodejs.org/api/fs.html#file-system node:fs/promises}).
|
||||
* Compatible with Node.js (version 10 and greater), Deno and Bun.
|
||||
*
|
||||
* @param {PathLike | FileHandle} path - The path to the font files.
|
||||
* @returns {Promise<Buffer | ArrayBuffer>}
|
||||
*/
|
||||
reader?: Reader = import ('node:fs/promises').readFile,
|
||||
|
||||
/**
|
||||
* (Optional) The function used to get the icon svg file from {@link https://iconify.design/ Iconify}
|
||||
* or URL endpoint passed.
|
||||
*
|
||||
* @see {@link BannerOptions.icon}
|
||||
*
|
||||
* @param {string} icon - Icon name or URL.
|
||||
* @returns {string | Promise<string>}
|
||||
*/
|
||||
iconHandler?: (icon: string) => string | Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The resulting banner object.
|
||||
*
|
||||
* Has a `toString()` function to be used in string literals
|
||||
* that returns the svg string of the banner.
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access public
|
||||
*/
|
||||
export interface Banner {
|
||||
/**
|
||||
* The resulting svg of the banner.
|
||||
* @readonly
|
||||
*/
|
||||
svg: string,
|
||||
/**
|
||||
* The raw html used to create the banner.
|
||||
* @readonly
|
||||
*/
|
||||
html: string,
|
||||
/**
|
||||
* The used icon's svg.
|
||||
* @readonly
|
||||
*/
|
||||
icon: string,
|
||||
/**
|
||||
* React-element-like objects / VDOM used to create the banner.
|
||||
* @readonly
|
||||
*/
|
||||
vNodes: VNode,
|
||||
/**
|
||||
* Returns the {@link Banner.svg svg string} of the banner.
|
||||
* Useful when using the banner object directly on a string.
|
||||
*
|
||||
* @example
|
||||
* import newBanner from '@marknow/banners';
|
||||
*
|
||||
* const banner = await newBanner({ ... });
|
||||
*
|
||||
* // Prints the resulting svg instead of the banner object itself.
|
||||
* console.log(`Banner svg:\n${banner}`)
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
toString(): string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Font object for the banner passed to the `satori` package.
|
||||
*
|
||||
* @see {@link BannerOptions.fonts}
|
||||
* @see {@link https://github.com/vercel/satori#fonts}
|
||||
*
|
||||
* @module \@marknow/banners > satori
|
||||
* @access protected
|
||||
*/
|
||||
export type Font = SatoriOptions['fonts'][0];
|
||||
|
||||
export type Colors = {
|
||||
foreground: string;
|
||||
background: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Global Fetch"-like function used by the package to retrieve
|
||||
* icons from [Iconify](https://iconify.design/) and custom
|
||||
* icons provided as url.
|
||||
*
|
||||
* @param {RequestInfo} input - The request url/info.
|
||||
* @param {RequestInit} - Request options.
|
||||
* @returns {Promise<Response>}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access protected
|
||||
*/
|
||||
export type Fetcher = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit
|
||||
) => Promise<Response>;
|
||||
|
||||
/**
|
||||
* "Node.js' `fs.readFile`"-like function used to read the font files
|
||||
* and return a Buffer or ArrayBuffer from them.
|
||||
*
|
||||
* @param {RequestInfo} input - The request url/info.
|
||||
* @param {RequestInit} - Request options.
|
||||
* @returns {Promise<Response>}
|
||||
*
|
||||
* @module \@marknow/banners
|
||||
* @access protected
|
||||
*/
|
||||
export type Reader = (
|
||||
path: PathLike | FileHandle,
|
||||
) => Promise<Buffer | ArrayBuffer>
|
||||
|
||||
/**
|
||||
* React-element-like objects / VDOM object used in satori.
|
||||
*
|
||||
* @module \@marknow/banners > satori-html
|
||||
* @access protected
|
||||
*/
|
||||
export interface VNode {
|
||||
type: string;
|
||||
props: {
|
||||
style?: Record<string, any>;
|
||||
children?: string | VNode | VNode[];
|
||||
[prop: string]: any;
|
||||
};
|
||||
}
|
||||
42
packages/banners/tests/PREVIEWS.md
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
### [`banner.test.ts`](./banner.test.ts)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### [`results.test.js`](./results.test.js);
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### [`vertical-results.test.js`](./vertical-results.test.js);
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### [`rtl-results.test.js`](./rtl-results.test.js);
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### [`icons.test.js`](./icons.test.js)
|
||||
|
||||

|
||||
|
||||
|
After Width: | Height: | Size: 69 KiB |
1
packages/banners/tests/__snapshots__/banner-rtl.svg
Normal file
|
After Width: | Height: | Size: 69 KiB |
1
packages/banners/tests/__snapshots__/banner-vertical.svg
Normal file
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
1
packages/banners/tests/__snapshots__/rtl-withIcon.svg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
1
packages/banners/tests/__snapshots__/rtl-withTitle.svg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
1
packages/banners/tests/__snapshots__/withComplete.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
packages/banners/tests/__snapshots__/withIcon.svg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
1
packages/banners/tests/__snapshots__/withSubtitle.svg
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
1
packages/banners/tests/__snapshots__/withTitle.svg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
57
packages/banners/tests/banner.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { BannerOptions } from '../src/index';
|
||||
import banner from '../src/index';
|
||||
|
||||
describe('Complex banner', async () => {
|
||||
const options: BannerOptions = {
|
||||
title: '@marknow/banners',
|
||||
subtitle: 'This is a more complex banner that uses most of the package settings',
|
||||
icon: 'solar:inbox-unread-bold-duotone',
|
||||
colors: {
|
||||
foreground: '#feefec',
|
||||
background: '#1d1412',
|
||||
},
|
||||
fonts: {
|
||||
title: {
|
||||
// eslint-disable-next-line spellcheck/spell-checker
|
||||
name: 'Quattrocento',
|
||||
data: await (await fetch('https://fonts.bunny.net/quattrocento/files/quattrocento-latin-700-normal.woff')).arrayBuffer(),
|
||||
weight: 700,
|
||||
style: 'normal',
|
||||
},
|
||||
subtitle: {
|
||||
// eslint-disable-next-line spellcheck/spell-checker
|
||||
name: 'Quattrocento Sans',
|
||||
data: await (await fetch('https://fonts.bunny.net/quattrocento-sans/files/quattrocento-sans-latin-400-normal.woff')).arrayBuffer(),
|
||||
weight: 400,
|
||||
style: 'normal',
|
||||
},
|
||||
},
|
||||
libConfig: {
|
||||
fetcher: globalThis.fetch,
|
||||
},
|
||||
};
|
||||
|
||||
it('Horizontal', async () => {
|
||||
const result = await banner({
|
||||
...options,
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/banner-horizontal.svg');
|
||||
});
|
||||
|
||||
it('Horizontal RTL', async () => {
|
||||
const result = await banner({
|
||||
...options,
|
||||
rtl: true,
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/banner-rtl.svg');
|
||||
});
|
||||
|
||||
it('Vertical', async () => {
|
||||
const result = await banner({
|
||||
...options,
|
||||
layout: 'vertical',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/banner-vertical.svg');
|
||||
});
|
||||
});
|
||||
44
packages/banners/tests/icons.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import banner from '../src/index';
|
||||
|
||||
describe('Iconify icons', async () => {
|
||||
it('API\'s Iconify icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
// eslint-disable-next-line spellcheck/spell-checker
|
||||
icon: 'solar:test-tube-minimalistic-bold-duotone',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/icons-withIconifyIcon.svg');
|
||||
});
|
||||
|
||||
it('Local Iconify icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
icon: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<g fill="currentColor">
|
||||
<path
|
||||
d="M3.187 15.049a4.085 4.085 0 0 0 0 5.758a4.042 4.042 0 0 0 5.734 0l3.746-3.762l-1.772-.736a2.356 2.356 0 0 1-1.408-1.906a2.352 2.352 0 0 0-2.074-2.082h-1.51l-2.716 2.728Z" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M13.363 2.233a.8.8 0 0 1 1.13.003l7.274 7.305a.8.8 0 0 1-1.134 1.129L13.36 3.364a.8.8 0 0 1 .003-1.13Z"
|
||||
clip-rule="evenodd" />
|
||||
<path d="M14.09 4.098L3.187 15.048a4.085 4.085 0 0 0 0 5.76a4.042 4.042 0 0 0 5.734 0L19.824 9.856l-5.734-5.76Z"
|
||||
opacity=".5" />
|
||||
</g>
|
||||
</svg>
|
||||
`.toString(),
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/icons-withIconifyIcon.svg');
|
||||
});
|
||||
|
||||
it('URL Iconify icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
icon: 'https://api.iconify.design/solar:test-tube-minimalistic-bold-duotone.svg',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/icons-withIconifyIcon.svg');
|
||||
});
|
||||
});
|
||||
36
packages/banners/tests/results.test.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import banner from '../src/index';
|
||||
|
||||
describe('Horizontal Layout', async () => {
|
||||
it('With Title', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/withTitle.svg');
|
||||
});
|
||||
|
||||
it('With Subtitle', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/withSubtitle.svg');
|
||||
});
|
||||
|
||||
it('With Icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
icon: 'solar:test-tube-bold-duotone',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/withIcon.svg');
|
||||
});
|
||||
|
||||
it('With Subtitle and Icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
icon: 'solar:test-tube-bold-duotone',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/withComplete.svg');
|
||||
});
|
||||
});
|
||||
40
packages/banners/tests/rtl-results.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import banner from '../src/index';
|
||||
|
||||
describe('Horizontal Layout', async () => {
|
||||
it('With Title', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
rtl: true,
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/rtl-withTitle.svg');
|
||||
});
|
||||
|
||||
it('With Subtitle', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
rtl: true,
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/rtl-withSubtitle.svg');
|
||||
});
|
||||
|
||||
it('With Icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
icon: 'solar:test-tube-bold-duotone',
|
||||
rtl: true,
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/rtl-withIcon.svg');
|
||||
});
|
||||
|
||||
it('With Subtitle and Icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
icon: 'solar:test-tube-bold-duotone',
|
||||
rtl: true,
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/rtl-withComplete.svg');
|
||||
});
|
||||
});
|
||||
40
packages/banners/tests/vertical-result.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import banner from '../src/index';
|
||||
|
||||
describe('Vertical Layout', async () => {
|
||||
it('With Title', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
layout: 'vertical',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/vertical-withTitle.svg');
|
||||
});
|
||||
|
||||
it('With Subtitle', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
layout: 'vertical',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/vertical-withSubtitle.svg');
|
||||
});
|
||||
|
||||
it('With Icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
icon: 'solar:test-tube-bold-duotone',
|
||||
layout: 'vertical',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/vertical-withIcon.svg');
|
||||
});
|
||||
|
||||
it('With Subtitle and Icon', async () => {
|
||||
const result = await banner({
|
||||
title: 'Hello World',
|
||||
subtitle: 'This is a test',
|
||||
icon: 'solar:test-tube-bold-duotone',
|
||||
layout: 'vertical',
|
||||
});
|
||||
expect(result.toString()).toMatchFileSnapshot('./__snapshots__/vertical-withComplete.svg');
|
||||
});
|
||||
});
|
||||
10
packages/banners/vitest.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
environmentMatchGlobs: [
|
||||
['**\/*{,.node}.test.{js,ts}', 'node'],
|
||||
],
|
||||
include: ['./tests/**/*.test.{js,ts}'],
|
||||
},
|
||||
});
|
||||
1877
pnpm-lock.yaml
generated
29
turbo.json
@@ -2,16 +2,39 @@
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"outputs": ["dist/**"]
|
||||
"dependsOn": [
|
||||
"^test",
|
||||
"test"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**"
|
||||
]
|
||||
},
|
||||
"lint": {},
|
||||
"test": {
|
||||
"dependsOn": [
|
||||
"^lint",
|
||||
"lint"
|
||||
]
|
||||
},
|
||||
"test:watch": {
|
||||
"dependsOn": [
|
||||
"^lint",
|
||||
"lint"
|
||||
],
|
||||
"persistent": true
|
||||
},
|
||||
"dev": {
|
||||
"dependsOn": ["^build"],
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"preview": {
|
||||
"dependsOn": ["^build"],
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
|
||||
4
vitest.workspace.json
Normal file
@@ -0,0 +1,4 @@
|
||||
[
|
||||
"packages/*/vitest.config.{js,ts}",
|
||||
"apps/*/vitest.config.{js,ts}"
|
||||
]
|
||||