Merge pull request #12 from LoredDev/main@pre-publish

Main@pre publish
This commit is contained in:
Guz
2023-06-27 17:09:35 -03:00
committed by GitHub
68 changed files with 4073 additions and 115 deletions

8
.changeset/README.md Normal file
View 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
View 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
}
}

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,4 @@
{
"gitDir": "./.git",
"*": "eslint --fix"
}

View File

@@ -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
View 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
README.md Normal file
View File

@@ -0,0 +1 @@
![](https://mdnow.vercel.app/api.svg)

View File

@@ -1,3 +1,4 @@
/src/lib/components.d.ts
/src/lib/imports.d.ts
/.eslint-auto-import.json
/static

View 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
View File

@@ -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
View File

@@ -0,0 +1,4 @@
{
"gitDir": "../../.git",
"*": "eslint --fix"
}

View File

@@ -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
View 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";
}

View File

@@ -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>

Binary file not shown.

7
apps/www/src/lib/components.d.ts vendored Normal file
View 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
View 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']
}

View 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>

View File

@@ -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>

View 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;

Binary file not shown.

Binary file not shown.

View 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(),

View File

@@ -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

View File

@@ -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: [

View File

@@ -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"
}
}

View File

@@ -0,0 +1,3 @@
/dist
*.woff
*.woff2

View File

@@ -0,0 +1,5 @@
process.env.ESLINT_TSCONFIG = 'jsconfig.json';
module.exports = {
root: false,
};

1
packages/banners/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/dist

View File

@@ -0,0 +1,4 @@
{
"gitDir": "../../.git",
"*": "eslint --fix"
}

View 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/**"]
}

View 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"
}
}

Binary file not shown.

Binary file not shown.

View 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')),
},
};
}

View 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>
`;
}

View 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
View 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'

View 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
View 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;
};
}

View File

@@ -0,0 +1,42 @@
### [`banner.test.ts`](./banner.test.ts)
![horizontal](__snapshots__/banner-horizontal.svg)
![vertical](__snapshots__/banner-vertical.svg)
![rtl](__snapshots__/banner-rtl.svg)
### [`results.test.js`](./results.test.js);
![withTitle](__snapshots__/withTitle.svg)
![withSubtitle](__snapshots__/withSubtitle.svg)
![withIcon](__snapshots__/withIcon.svg)
![withComplete](__snapshots__/withComplete.svg)
### [`vertical-results.test.js`](./vertical-results.test.js);
![withTitle](__snapshots__/vertical-withTitle.svg)
![withSubtitle](__snapshots__/vertical-withSubtitle.svg)
![withIcon](__snapshots__/vertical-withIcon.svg)
![withComplete](__snapshots__/vertical-withComplete.svg)
### [`rtl-results.test.js`](./rtl-results.test.js);
![withTitle](__snapshots__/rtl-withTitle.svg)
![withSubtitle](__snapshots__/rtl-withSubtitle.svg)
![withIcon](__snapshots__/rtl-withIcon.svg)
![withComplete](__snapshots__/rtl-withComplete.svg)
### [`icons.test.js`](./icons.test.js)
![icons](__snapshots__/icons-withIconifyIcon.svg)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 69 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 69 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

View 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');
});
});

View 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');
});
});

View 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');
});
});

View 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');
});
});

View 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');
});
});

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View File

@@ -0,0 +1,4 @@
[
"packages/*/vitest.config.{js,ts}",
"apps/*/vitest.config.{js,ts}"
]