8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
15
.changeset/config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [],
|
||||
"privatePackages": {
|
||||
"version": true,
|
||||
"tag": true
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
'no-undef': ['warn'],
|
||||
'import/no-mutable-exports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -187,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: [
|
||||
@@ -225,6 +226,13 @@ module.exports = {
|
||||
'duotone',
|
||||
'tsconfig',
|
||||
'workspace',
|
||||
'woff',
|
||||
'marknow',
|
||||
'lored',
|
||||
'guz013',
|
||||
'xml',
|
||||
'jsconfig',
|
||||
'vitest',
|
||||
],
|
||||
minLength: 4,
|
||||
}],
|
||||
|
||||
49
.github/actions/pnpm-setup/action.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: PNPM Setup
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
required: false
|
||||
type: number
|
||||
default: 18
|
||||
pnpm-version:
|
||||
required: false
|
||||
type: number
|
||||
default: 8
|
||||
install-deps:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
run_install: false
|
||||
|
||||
- name: Get store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
if: inputs.install-deps
|
||||
run: pnpm install
|
||||
shell: bash
|
||||
43
.github/workflows/checks.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: 🧪 Code checking
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
linting:
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/pnpm-setup
|
||||
|
||||
- name: Run ESLint
|
||||
run: pnpm run lint
|
||||
vitest:
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/pnpm-setup
|
||||
|
||||
- name: Run Vitests
|
||||
run: pnpm run test
|
||||
4
.lintstagedrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"gitDir": "./.git",
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
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"
|
||||
|
||||
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"unocss.root": "apps/www",
|
||||
"vitest.enable": true
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/src/lib/components.d.ts
|
||||
/src/lib/imports.d.ts
|
||||
/.eslint-auto-import.json
|
||||
/static
|
||||
|
||||
68
apps/www/.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"globals": {
|
||||
"afterUpdate": true,
|
||||
"backIn": true,
|
||||
"backInOut": true,
|
||||
"backOut": true,
|
||||
"beforeUpdate": true,
|
||||
"blur": true,
|
||||
"bounceIn": true,
|
||||
"bounceInOut": true,
|
||||
"bounceOut": true,
|
||||
"circIn": true,
|
||||
"circInOut": true,
|
||||
"circOut": true,
|
||||
"createEventDispatcher": true,
|
||||
"crossfade": true,
|
||||
"cubicIn": true,
|
||||
"cubicInOut": true,
|
||||
"cubicOut": true,
|
||||
"derived": true,
|
||||
"draw": true,
|
||||
"elasticIn": true,
|
||||
"elasticInOut": true,
|
||||
"elasticOut": true,
|
||||
"expoIn": true,
|
||||
"expoInOut": true,
|
||||
"expoOut": true,
|
||||
"fade": true,
|
||||
"flip": true,
|
||||
"fly": true,
|
||||
"get": true,
|
||||
"getAllContexts": true,
|
||||
"getContext": true,
|
||||
"hasContext": true,
|
||||
"linear": true,
|
||||
"onDestroy": true,
|
||||
"onMount": true,
|
||||
"quadIn": true,
|
||||
"quadInOut": true,
|
||||
"quadOut": true,
|
||||
"quartIn": true,
|
||||
"quartInOut": true,
|
||||
"quartOut": true,
|
||||
"quintIn": true,
|
||||
"quintInOut": true,
|
||||
"quintOut": true,
|
||||
"readable": true,
|
||||
"scale": true,
|
||||
"setContext": true,
|
||||
"sineIn": true,
|
||||
"sineInOut": true,
|
||||
"sineOut": true,
|
||||
"slide": true,
|
||||
"spring": true,
|
||||
"tick": true,
|
||||
"tweened": true,
|
||||
"writable": true,
|
||||
"csr": true,
|
||||
"ssr": true,
|
||||
"render": true,
|
||||
"banner": true,
|
||||
"Banner": true,
|
||||
"load": true,
|
||||
"data": true,
|
||||
"PageData": true,
|
||||
"GET": true
|
||||
}
|
||||
}
|
||||
3
apps/www/.gitignore
vendored
@@ -8,6 +8,3 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
/src/lib/imports.d.ts
|
||||
/src/lib/components.d.ts
|
||||
/.eslintrc-auto-import.json
|
||||
|
||||
4
apps/www/.lintstagedrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"gitDir": "../../.git",
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
@@ -18,23 +18,24 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource-variable/fira-code": "^5.0.3",
|
||||
"@fontsource-variable/inter": "^5.0.3",
|
||||
"@iconify-json/solar": "^1.1.1",
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.11",
|
||||
"@typescript-eslint/parser": "^5.59.11",
|
||||
"@unocss/extractor-svelte": "^0.52.7",
|
||||
"cal-sans": "^1.0.1",
|
||||
"mdsvex": "^0.10.6",
|
||||
"rehype-external-links": "^2.1.0",
|
||||
"sass": "^1.63.4",
|
||||
"sass": "^1.63.6",
|
||||
"satori": "^0.10.1",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte-check": "^3.4.3",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^5.1.3",
|
||||
"unocss": "^0.52.7",
|
||||
|
||||
11
apps/www/src/app.css
Normal file
@@ -0,0 +1,11 @@
|
||||
@font-face {
|
||||
font-family: "Mona Sans";
|
||||
src: url("$lib/assets/Mona-Sans.woff2") format("woff2 supports variations"),
|
||||
url("$lib/assets/Mona-Sans.woff2") format("woff2-variations");
|
||||
font-weight: 200 900;
|
||||
font-stretch: 75% 125%;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Mona Sans";
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<body data-sveltekit-preload-data="hover" un-bg="#0d1117">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
apps/www/src/lib/assets/Mona-Sans.woff2
Normal file
7
apps/www/src/lib/components.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// generated by unplugin-svelte-components
|
||||
// We suggest you to commit this file into source control
|
||||
declare global {
|
||||
const Test: typeof import("./Test.svelte")["default"]
|
||||
}
|
||||
|
||||
export {}
|
||||
69
apps/www/src/lib/imports.d.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const Banner: typeof import('./Banner')['default']
|
||||
const GET: typeof import('../routes/api.svg/+server')['GET']
|
||||
const afterUpdate: typeof import('svelte')['afterUpdate']
|
||||
const backIn: typeof import('svelte/easing')['backIn']
|
||||
const backInOut: typeof import('svelte/easing')['backInOut']
|
||||
const backOut: typeof import('svelte/easing')['backOut']
|
||||
const beforeUpdate: typeof import('svelte')['beforeUpdate']
|
||||
const blur: typeof import('svelte/transition')['blur']
|
||||
const bounceIn: typeof import('svelte/easing')['bounceIn']
|
||||
const bounceInOut: typeof import('svelte/easing')['bounceInOut']
|
||||
const bounceOut: typeof import('svelte/easing')['bounceOut']
|
||||
const circIn: typeof import('svelte/easing')['circIn']
|
||||
const circInOut: typeof import('svelte/easing')['circInOut']
|
||||
const circOut: typeof import('svelte/easing')['circOut']
|
||||
const createEventDispatcher: typeof import('svelte')['createEventDispatcher']
|
||||
const crossfade: typeof import('svelte/transition')['crossfade']
|
||||
const csr: typeof import('../routes/+page')['csr']
|
||||
const cubicIn: typeof import('svelte/easing')['cubicIn']
|
||||
const cubicInOut: typeof import('svelte/easing')['cubicInOut']
|
||||
const cubicOut: typeof import('svelte/easing')['cubicOut']
|
||||
const data: typeof import('../routes/+page.svelte')['data']
|
||||
const derived: typeof import('svelte/store')['derived']
|
||||
const draw: typeof import('svelte/transition')['draw']
|
||||
const elasticIn: typeof import('svelte/easing')['elasticIn']
|
||||
const elasticInOut: typeof import('svelte/easing')['elasticInOut']
|
||||
const elasticOut: typeof import('svelte/easing')['elasticOut']
|
||||
const expoIn: typeof import('svelte/easing')['expoIn']
|
||||
const expoInOut: typeof import('svelte/easing')['expoInOut']
|
||||
const expoOut: typeof import('svelte/easing')['expoOut']
|
||||
const fade: typeof import('svelte/transition')['fade']
|
||||
const flip: typeof import('svelte/animate')['flip']
|
||||
const fly: typeof import('svelte/transition')['fly']
|
||||
const get: typeof import('svelte/store')['get']
|
||||
const getAllContexts: typeof import('svelte')['getAllContexts']
|
||||
const getContext: typeof import('svelte')['getContext']
|
||||
const hasContext: typeof import('svelte')['hasContext']
|
||||
const linear: typeof import('svelte/easing')['linear']
|
||||
const load: typeof import('../routes/+page')['load']
|
||||
const onDestroy: typeof import('svelte')['onDestroy']
|
||||
const onMount: typeof import('svelte')['onMount']
|
||||
const quadIn: typeof import('svelte/easing')['quadIn']
|
||||
const quadInOut: typeof import('svelte/easing')['quadInOut']
|
||||
const quadOut: typeof import('svelte/easing')['quadOut']
|
||||
const quartIn: typeof import('svelte/easing')['quartIn']
|
||||
const quartInOut: typeof import('svelte/easing')['quartInOut']
|
||||
const quartOut: typeof import('svelte/easing')['quartOut']
|
||||
const quintIn: typeof import('svelte/easing')['quintIn']
|
||||
const quintInOut: typeof import('svelte/easing')['quintInOut']
|
||||
const quintOut: typeof import('svelte/easing')['quintOut']
|
||||
const readable: typeof import('svelte/store')['readable']
|
||||
const render: typeof import('./render')['default']
|
||||
const scale: typeof import('svelte/transition')['scale']
|
||||
const setContext: typeof import('svelte')['setContext']
|
||||
const sineIn: typeof import('svelte/easing')['sineIn']
|
||||
const sineInOut: typeof import('svelte/easing')['sineInOut']
|
||||
const sineOut: typeof import('svelte/easing')['sineOut']
|
||||
const slide: typeof import('svelte/transition')['slide']
|
||||
const spring: typeof import('svelte/motion')['spring']
|
||||
const ssr: typeof import('../routes/+page')['ssr']
|
||||
const tick: typeof import('svelte')['tick']
|
||||
const tweened: typeof import('svelte/motion')['tweened']
|
||||
const writable: typeof import('svelte/store')['writable']
|
||||
}
|
||||
9
apps/www/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import 'virtual:uno.css';
|
||||
import '@fontsource-variable/fira-code';
|
||||
import '../app.css';
|
||||
</script>
|
||||
|
||||
<div un-bg="#0d1117">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,6 +1,22 @@
|
||||
<script>
|
||||
// your script goes here
|
||||
</script>
|
||||
|
||||
<h1 un-text="red">Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<img src="/api.svg" alt="">
|
||||
</div>
|
||||
</main>
|
||||
|
||||
38
apps/www/src/routes/api.svg/+server.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import newBanner from '@marknow/banners';
|
||||
|
||||
export const GET = (async ({ fetch }): Promise<Response> => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(`${banner.toString()}`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-type': 'image/svg+xml',
|
||||
},
|
||||
});
|
||||
}) satisfies RequestHandler;
|
||||
BIN
apps/www/static/Mona-Sans-Regular.woff
Normal file
BIN
apps/www/static/Mona-Sans-SemiBold.woff
Normal file
@@ -1,19 +1,19 @@
|
||||
import adapter from '@sveltejs/adapter-vercel';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
import { mdsvex } from 'mdsvex';
|
||||
import rhExternalLinks from 'rehype-external-links';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte', '.svx', '.md'],
|
||||
preprocess: [vitePreprocess(), mdsvex({
|
||||
preprocess: [sveltePreprocess(), mdsvex({
|
||||
rehypePlugins: [
|
||||
[rhExternalLinks, {
|
||||
target: '_blank',
|
||||
rel: ['nofollow', 'noopener', 'noreferrer'],
|
||||
}],
|
||||
],
|
||||
extensions: ['.svelte', '.svx', '.md'],
|
||||
extensions: ['.svx', '.md'],
|
||||
})],
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
|
||||
@@ -36,17 +36,13 @@ export default defineConfig({
|
||||
presetWebFonts({
|
||||
fonts: {
|
||||
sans: {
|
||||
name: 'Inter',
|
||||
name: 'Mona Sans',
|
||||
provider: 'none',
|
||||
},
|
||||
code: {
|
||||
name: 'Fira Code',
|
||||
provider: 'none',
|
||||
},
|
||||
cal: {
|
||||
name: 'Cal Sans',
|
||||
provider: 'none',
|
||||
},
|
||||
},
|
||||
}),
|
||||
// @ts-expect-error It seems that this preset
|
||||
|
||||
@@ -14,8 +14,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
SvelteImport({
|
||||
include: [
|
||||
/\.svelte/,
|
||||
/\.svelte\?svelte/,
|
||||
/\.svelte$/,
|
||||
],
|
||||
external: [
|
||||
...findPathsByExtension(path.join(__dirname, 'src'), '.svx').map((filePath) => {
|
||||
@@ -47,21 +46,18 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
dirs: [
|
||||
'./src/**/*',
|
||||
'./src/lib',
|
||||
],
|
||||
importPathTransform: (importPath) => {
|
||||
if (path.extname(importPath) === '.svg')
|
||||
return `${importPath}?component`;
|
||||
if (getFileName(importPath).startsWith('+'))
|
||||
return '';
|
||||
else return importPath;
|
||||
},
|
||||
dts: './src/lib/components.d.ts',
|
||||
}),
|
||||
AutoImport({
|
||||
include: [
|
||||
/\.svelte/,
|
||||
/\.svelte\?svelte/,
|
||||
/\.svelte$/,
|
||||
/.[tj]sx?$/,
|
||||
],
|
||||
imports: [
|
||||
|
||||
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "marknow",
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.0",
|
||||
"packageManager": "pnpm@8.0.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
@@ -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",
|
||||
@@ -17,6 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.39.5",
|
||||
"@changesets/cli": "^2.26.2",
|
||||
"@commitlint/config-conventional": "^17.6.5",
|
||||
"@commitlint/types": "^17.4.4",
|
||||
"eslint": "^8.42.0",
|
||||
@@ -24,9 +27,7 @@
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"husky": "^8.0.3",
|
||||
"turbo": "^1.10.3",
|
||||
"vercel": "^30.2.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
"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}'],
|
||||
},
|
||||
});
|
||||
2681
pnpm-lock.yaml
generated
25
turbo.json
@@ -2,16 +2,35 @@
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"outputs": ["dist/**"]
|
||||
"dependsOn": [
|
||||
"^test",
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**"
|
||||
]
|
||||
},
|
||||
"lint": {},
|
||||
"test": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
"test:watch": {
|
||||
"persistent": true
|
||||
},
|
||||
"dev": {
|
||||
"dependsOn": ["^build"],
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"preview": {
|
||||
"dependsOn": ["^build"],
|
||||
"dependsOn": [
|
||||
"^build",
|
||||
"build"
|
||||
],
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
|
||||
4
vitest.workspace.json
Normal file
@@ -0,0 +1,4 @@
|
||||
[
|
||||
"packages/*/vitest.config.{js,ts}",
|
||||
"apps/*/vitest.config.{js,ts}"
|
||||
]
|
||||