diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index c1267b2..96e1295 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -23,10 +23,10 @@ jobs:
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
-
+
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
@@ -34,7 +34,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v1
+ uses: github/codeql-action/autobuild@v2
# âšī¸ Command-line programs to run using the OS shell.
# đ https://git.io/JvXDl
@@ -48,4 +48,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/package-lock.json b/package-lock.json
index dd252a3..dcde69c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,6 @@
"@adaptivelink/pops": "0.5.7",
"@cloudflare/kv-asset-handler": "0.3.0",
"@cloudflare/workers-types": "4.20231121.0",
- "@cloudfour/simple-svg-placeholder": "1.1.0",
"@nodecraft/eslint-config": "31.0.1",
"@types/sanitize-html": "2.9.5",
"@typescript-eslint/eslint-plugin": "6.13.2",
@@ -318,12 +317,6 @@
"integrity": "sha512-+kWfpCkqiepwAKXyHoE0gnkPgkLhz0/9HOBIGhHRsUvUKvhUtm3mbqqoGRWgF1qcjzrDUBbrrOq4MYHfFtc2RA==",
"dev": true
},
- "node_modules/@cloudfour/simple-svg-placeholder": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@cloudfour/simple-svg-placeholder/-/simple-svg-placeholder-1.1.0.tgz",
- "integrity": "sha512-023tyGesX8pSTm6J7wex1o/5mPo4HSSzd6hjb1ijpx3k/qCScSyWJRKXXVdK1srngAQcNe4HLEdR9HQnPxysKg==",
- "dev": true
- },
"node_modules/@esbuild-plugins/node-globals-polyfill": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz",
diff --git a/package.json b/package.json
index a61bf87..7da09c2 100644
--- a/package.json
+++ b/package.json
@@ -26,13 +26,13 @@
"publish:prod": "wrangler deploy --env production --verbose",
"publish:staging": "wrangler deploy --env staging --verbose",
"start:dev": "wrangler dev",
- "test": "vitest"
+ "test": "vitest run",
+ "test:watch": "vitest watch"
},
"devDependencies": {
"@adaptivelink/pops": "0.5.7",
"@cloudflare/kv-asset-handler": "0.3.0",
"@cloudflare/workers-types": "4.20231121.0",
- "@cloudfour/simple-svg-placeholder": "1.1.0",
"@nodecraft/eslint-config": "31.0.1",
"@types/sanitize-html": "2.9.5",
"@typescript-eslint/eslint-plugin": "6.13.2",
diff --git a/public/index.html b/public/index.html
index 6d143f9..d2eb1aa 100644
--- a/public/index.html
+++ b/public/index.html
@@ -226,6 +226,9 @@
Available API Options
textColor
Color of the text. For transparency, use an rgba
or hsla
value. Defaults to rgba(0,0,0,0.5)
+
+ textwrap
+ Wrap text to fit within the image (to best ability). Will not alter font size, so especially long string may still appear outside of the image. Defaults to false
Example URL
diff --git a/src/index.test.ts b/src/index.test.ts
index 167f18e..0363032 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -8,6 +8,8 @@ import {
} from 'vitest';
import { unstable_dev } from 'wrangler';
+import { getKeys } from './utils';
+
import type { UnstableDevWorker } from 'wrangler';
describe('Worker', () => {
@@ -43,32 +45,81 @@ describe('Worker', () => {
expect(resp.status).toBe(200);
const text = await resp.text();
- expect(text).toBe(' Hello World ');
+ expect(text).toBe('Hello World ');
});
test.each([
+ // basic tests
+ [
+ {
+ width: 350,
+ height: 100,
+ },
+ '350Ã100 ',
+ ],
+ [
+ {
+ width: 200,
+ height: 100,
+ bgColor: '#000',
+ textColor: 'rgba(255,255,255,0.5)',
+ },
+ '200Ã100 ',
+ ],
[
- '?width=350&height=100',
- ' 350Ã100 ',
+ {
+ width: 140,
+ height: 100,
+ bgColor: '#313131',
+ textColor: '#dfdfde',
+ },
+ '140Ã100 ',
],
[
- '?width=200&height=100&bgColor=%23000&textColor=rgba(255,255,255,0.5)',
- ' 200Ã100 ',
+ {
+ width: 350,
+ height: 100,
+ text: 'placeholders.dev',
+ },
+ 'placeholders.dev ',
],
[
- '?width=140&height=100&bgColor=%23313131&textColor=%23dfdfde',
- ' 140Ã100 ',
+ {
+ width: 1055,
+ height: 100,
+ text: 'Hello World',
+ bgColor: '#434343',
+ textColor: '#dfdfde',
+ },
+ 'Hello World ',
],
+ // text wrapping
[
- '?width=350&height=100&text=placeholders.dev',
- ' placeholders.dev ',
+ {
+ width: 250,
+ height: 200,
+ text: 'This text is too long',
+ bgColor: '#f7f6f6',
+ textWrap: false,
+ },
+ 'This text is too long ',
],
[
- '?width=1055&height=100&text=Hello%20World&bgColor=%23434343&textColor=%23dfdfde',
- ' Hello World ',
+ {
+ width: 250,
+ height: 200,
+ text: 'This text is too long',
+ bgColor: '#f7f6f6',
+ textWrap: true,
+ },
+ 'This text is too long
',
],
])('should return accurate svg image with query params %s', async (query, expected) => {
- const req = new Request(`https://example.com/api/${query}`, { method: 'GET' });
+ const searchParams = new URLSearchParams();
+ for (const key of getKeys(query)) {
+ searchParams.set(key, String(query[key]));
+ }
+ const req = new Request(`https://example.com/api/?${searchParams.toString()}`, { method: 'GET' });
const resp = await worker.fetch(req.url);
expect(resp.status).toBe(200);
diff --git a/src/index.ts b/src/index.ts
index b83bb06..dd1e29e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,8 @@
import { getAssetFromKV } from '@cloudflare/kv-asset-handler';
-import generateSVG, { type Options } from '@cloudfour/simple-svg-placeholder';
import manifestJSON from '__STATIC_CONTENT_MANIFEST';
import { sanitizers } from './sanitizers';
+import { type Options, simpleSvgPlaceholder } from './simple-svg-placeholder';
import { type Env } from './types';
import { addHeaders } from './utils';
import {
@@ -25,7 +25,10 @@ async function handleEvent(request: Request, env: Env, ctx: ExecutionContext) {
// when in dev, we serve from `/api`
if (url.host === 'images.placeholders.dev' || url.pathname.startsWith('/api')) {
// do our API work
- let response = await cache.match(url, { ignoreMethod: true }); // try to find match for this request in the edge cache
+ let response;
+ if (url.host === 'images.placeholders.dev') {
+ response = await cache.match(url, { ignoreMethod: true }); // try to find match for this request in the edge cache
+ }
if (response) {
// use cache found on Cloudflare edge. Set X-Worker-Cache header for helpful debug
const newHdrs = new Headers(response.headers);
@@ -63,7 +66,7 @@ async function handleEvent(request: Request, env: Env, ctx: ExecutionContext) {
}
}
}
- response = new Response(generateSVG(imageOptions), {
+ response = new Response(simpleSvgPlaceholder(imageOptions), {
headers: {
'content-type': 'image/svg+xml; charset=utf-8',
'access-control-allow-origin': '*',
diff --git a/src/sanitizers.test.ts b/src/sanitizers.test.ts
index 72cad14..a0224f3 100644
--- a/src/sanitizers.test.ts
+++ b/src/sanitizers.test.ts
@@ -1,96 +1,136 @@
import { describe, expect, it } from 'vitest';
-import { sanitizers } from './sanitizers';
+import {
+ sanitizeBoolean,
+ sanitizeColor,
+ sanitizeNumber,
+ sanitizeString,
+} from './sanitizers';
describe('Sanitizers', () => {
it('number', () => {
- expect(sanitizers.width(350)).toBe(350);
- expect(sanitizers.width('350')).toBe(350);
- expect(sanitizers.width(' 350')).toBe(350);
- expect(sanitizers.width(' 350 ')).toBe(350);
- expect(sanitizers.width(350.5)).toBe(350.5);
- expect(sanitizers.width('350.5')).toBe(350.5);
- expect(sanitizers.width('350.50')).toBe(350.5);
- expect(sanitizers.width('0350.50')).toBe(350.5);
- expect(sanitizers.width('')).toBe(null);
- expect(sanitizers.width('abc')).toBe(null);
- expect(sanitizers.width('350abc')).toBe(null);
- expect(sanitizers.width('350 abc')).toBe(null);
- expect(sanitizers.width('abc350')).toBe(null);
- expect(sanitizers.width('abc 350')).toBe(null);
- expect(sanitizers.width('350.5')).toBe(350.5);
- expect(sanitizers.width('350.5abc')).toBe(null);
- expect(sanitizers.width('350.5 abc')).toBe(null);
- expect(sanitizers.width('abc350.5')).toBe(null);
- expect(sanitizers.width('abc 350.5')).toBe(null);
- expect(sanitizers.width('350.5.5')).toBe(null);
- expect(sanitizers.width('350.5.5abc')).toBe(null);
- expect(sanitizers.width('350.5.5 abc')).toBe(null);
- expect(sanitizers.width('abc350.5.5')).toBe(null);
- expect(sanitizers.width('abc 350.5.5')).toBe(null);
- expect(sanitizers.width('350.5.5.5')).toBe(null);
- expect(sanitizers.width('350.5.5.5abc')).toBe(null);
- expect(sanitizers.width('350.5.5.5 abc')).toBe(null);
- expect(sanitizers.width('abc350.5.5.5')).toBe(null);
- expect(sanitizers.width('abc 350.5.5.5')).toBe(null);
- expect(sanitizers.width('350 350')).toBe(null);
- expect(sanitizers.width('350 350.5')).toBe(null);
- expect(sanitizers.width('350.5 350')).toBe(null);
- expect(sanitizers.width('350.5 350.5')).toBe(null);
- expect(sanitizers.width('350 350 350')).toBe(null);
- expect(sanitizers.width('350 350 350 350')).toBe(null);
+ expect(sanitizeNumber(350)).toBe(350);
+ expect(sanitizeNumber('350')).toBe(350);
+ expect(sanitizeNumber(' 350')).toBe(350);
+ expect(sanitizeNumber(' 350 ')).toBe(350);
+ expect(sanitizeNumber(350.5)).toBe(350.5);
+ expect(sanitizeNumber('350.5')).toBe(350.5);
+ expect(sanitizeNumber('350.50')).toBe(350.5);
+ expect(sanitizeNumber('0350.50')).toBe(350.5);
+ expect(sanitizeNumber('')).toBe(null);
+ expect(sanitizeNumber('abc')).toBe(null);
+ expect(sanitizeNumber('350abc')).toBe(null);
+ expect(sanitizeNumber('350 abc')).toBe(null);
+ expect(sanitizeNumber('abc350')).toBe(null);
+ expect(sanitizeNumber('abc 350')).toBe(null);
+ expect(sanitizeNumber('350.5')).toBe(350.5);
+ expect(sanitizeNumber('350.5abc')).toBe(null);
+ expect(sanitizeNumber('350.5 abc')).toBe(null);
+ expect(sanitizeNumber('abc350.5')).toBe(null);
+ expect(sanitizeNumber('abc 350.5')).toBe(null);
+ expect(sanitizeNumber('350.5.5')).toBe(null);
+ expect(sanitizeNumber('350.5.5abc')).toBe(null);
+ expect(sanitizeNumber('350.5.5 abc')).toBe(null);
+ expect(sanitizeNumber('abc350.5.5')).toBe(null);
+ expect(sanitizeNumber('abc 350.5.5')).toBe(null);
+ expect(sanitizeNumber('350.5.5.5')).toBe(null);
+ expect(sanitizeNumber('350.5.5.5abc')).toBe(null);
+ expect(sanitizeNumber('350.5.5.5 abc')).toBe(null);
+ expect(sanitizeNumber('abc350.5.5.5')).toBe(null);
+ expect(sanitizeNumber('abc 350.5.5.5')).toBe(null);
+ expect(sanitizeNumber('350 350')).toBe(null);
+ expect(sanitizeNumber('350 350.5')).toBe(null);
+ expect(sanitizeNumber('350.5 350')).toBe(null);
+ expect(sanitizeNumber('350.5 350.5')).toBe(null);
+ expect(sanitizeNumber('350 350 350')).toBe(null);
+ expect(sanitizeNumber('350 350 350 350')).toBe(null);
});
it('string', () => {
- expect(sanitizers.text('Hello World')).toBe('Hello World');
- expect(sanitizers.text('')).toBe('');
- expect(sanitizers.text(' ')).toBe(' ');
- expect(sanitizers.text('')).toBe('');
- expect(sanitizers.text('abc')).toBe('abc');
- expect(sanitizers.text('abc')).toBe('abc');
- expect(sanitizers.text('abcabc')).toBe('abcabc');
- expect(sanitizers.text(' abc')).toBe(' abc');
- expect(sanitizers.text('abc ')).toBe('abc ');
- expect(sanitizers.text('abc abc')).toBe('abc abc');
- expect(sanitizers.text(' abc ')).toBe(' abc ');
- expect(sanitizers.text('barfoo ')).toBe('barfoo');
- expect(sanitizers.text('barfoo ')).toBe('barfoo');
+ expect(sanitizeString('Hello World')).toBe('Hello World');
+ expect(sanitizeString('')).toBe('');
+ expect(sanitizeString(' ')).toBe(' ');
+ expect(sanitizeString('')).toBe('');
+ expect(sanitizeString('abc')).toBe('abc');
+ expect(sanitizeString('abc')).toBe('abc');
+ expect(sanitizeString('abcabc')).toBe('abcabc');
+ expect(sanitizeString(' abc')).toBe(' abc');
+ expect(sanitizeString('abc ')).toBe('abc ');
+ expect(sanitizeString('abc abc')).toBe('abc abc');
+ expect(sanitizeString(' abc ')).toBe(' abc ');
+ expect(sanitizeString('barfoo ')).toBe('barfoo');
+ expect(sanitizeString('barfoo ')).toBe('barfoo');
// @ts-expect-error intentional bad data
- expect(sanitizers.text(123)).toBe('123');
+ expect(sanitizeString(123)).toBe('123');
});
it('color', () => {
// double check string sanitization
- expect(sanitizers.bgColor('Hello World')).toBe(null);
- expect(sanitizers.bgColor('')).toBe(null);
- expect(sanitizers.bgColor(' ')).toBe(null);
+ expect(sanitizeColor('Hello World')).toBe(null);
+ expect(sanitizeColor('')).toBe(null);
+ expect(sanitizeColor(' ')).toBe(null);
// hex
- expect(sanitizers.bgColor('#000')).toBe('#000');
- expect(sanitizers.bgColor('#000000')).toBe('#000000');
- expect(sanitizers.bgColor('#00000000')).toBe('#00000000');
- expect(sanitizers.bgColor('#000000000')).toBe(null);
- expect(sanitizers.bgColor('#yyy')).toBe(null);
- expect(sanitizers.bgColor('#yyyyyy')).toBe(null);
+ expect(sanitizeColor('#000')).toBe('#000');
+ expect(sanitizeColor('#000000')).toBe('#000000');
+ expect(sanitizeColor('#00000000')).toBe('#00000000');
+ expect(sanitizeColor('#000000000')).toBe(null);
+ expect(sanitizeColor('#yyy')).toBe(null);
+ expect(sanitizeColor('#yyyyyy')).toBe(null);
// rgb/rgba
- expect(sanitizers.bgColor('rgb(0,0,0)')).toBe('rgb(0,0,0)');
- expect(sanitizers.bgColor('rgb(0, 0, 0)')).toBe('rgb(0, 0, 0)');
- expect(sanitizers.bgColor('rgb(0,0,0,0)')).toBe('rgb(0,0,0,0)');
- expect(sanitizers.bgColor('rgb(a,b,c)')).toBe(null);
- expect(sanitizers.bgColor('rgb(...)')).toBe(null);
- expect(sanitizers.bgColor('rgb(1)')).toBe(null);
+ expect(sanitizeColor('rgb(0,0,0)')).toBe('rgb(0,0,0)');
+ expect(sanitizeColor('rgb(0, 0, 0)')).toBe('rgb(0, 0, 0)');
+ expect(sanitizeColor('rgb(0,0,0,0)')).toBe('rgb(0,0,0,0)');
+ expect(sanitizeColor('rgb(a,b,c)')).toBe(null);
+ expect(sanitizeColor('rgb(...)')).toBe(null);
+ expect(sanitizeColor('rgb(1)')).toBe(null);
- expect(sanitizers.bgColor('rgba(0,0,0,0)')).toBe('rgba(0,0,0,0)');
- expect(sanitizers.bgColor('rgba(0, 0, 0, 0)')).toBe('rgba(0, 0, 0, 0)');
- expect(sanitizers.bgColor('rgba(0,0,0,0.5)')).toBe('rgba(0,0,0,0.5)');
- expect(sanitizers.bgColor('rgba(a,b,c)')).toBe(null);
- expect(sanitizers.bgColor('rgba(...)')).toBe(null);
- expect(sanitizers.bgColor('rgba(1)')).toBe(null);
+ expect(sanitizeColor('rgba(0,0,0,0)')).toBe('rgba(0,0,0,0)');
+ expect(sanitizeColor('rgba(0, 0, 0, 0)')).toBe('rgba(0, 0, 0, 0)');
+ expect(sanitizeColor('rgba(0,0,0,0.5)')).toBe('rgba(0,0,0,0.5)');
+ expect(sanitizeColor('rgba(a,b,c)')).toBe(null);
+ expect(sanitizeColor('rgba(...)')).toBe(null);
+ expect(sanitizeColor('rgba(1)')).toBe(null);
- expect(sanitizers.bgColor('black')).toBe('black');
- expect(sanitizers.bgColor('white')).toBe('white');
- expect(sanitizers.bgColor('blue')).toBe('blue');
- expect(sanitizers.bgColor('blueyyyy')).toBe(null);
+ expect(sanitizeColor('black')).toBe('black');
+ expect(sanitizeColor('white')).toBe('white');
+ expect(sanitizeColor('blue')).toBe('blue');
+ expect(sanitizeColor('blueyyyy')).toBe(null);
+ });
+
+ it('boolean', () => {
+ expect(sanitizeBoolean('true')).toBe(true);
+ expect(sanitizeBoolean('false')).toBe(false);
+ expect(sanitizeBoolean('1')).toBe(true);
+ expect(sanitizeBoolean('0')).toBe(false);
+ expect(sanitizeBoolean('')).toBe(false);
+ expect(sanitizeBoolean('abc')).toBe(false);
+ expect(sanitizeBoolean('trueabc')).toBe(false);
+ expect(sanitizeBoolean('true abc')).toBe(false);
+ expect(sanitizeBoolean('abctrue')).toBe(false);
+ expect(sanitizeBoolean('abc true')).toBe(false);
+ expect(sanitizeBoolean('true.5')).toBe(false);
+ expect(sanitizeBoolean('true.5abc')).toBe(false);
+ expect(sanitizeBoolean('true.5 abc')).toBe(false);
+ expect(sanitizeBoolean('abctrue.5')).toBe(false);
+ expect(sanitizeBoolean('abc true.5')).toBe(false);
+ expect(sanitizeBoolean('true.5.5')).toBe(false);
+ expect(sanitizeBoolean('true.5.5abc')).toBe(false);
+ expect(sanitizeBoolean('true.5.5 abc')).toBe(false);
+ expect(sanitizeBoolean('abctrue.5.5')).toBe(false);
+ expect(sanitizeBoolean('abc true.5.5')).toBe(false);
+ expect(sanitizeBoolean('true.5.5.5')).toBe(false);
+ expect(sanitizeBoolean('true.5.5.5abc')).toBe(false);
+ expect(sanitizeBoolean('true.5.5.5 abc')).toBe(false);
+ expect(sanitizeBoolean('abctrue.5.5.5')).toBe(false);
+ expect(sanitizeBoolean('abc true.5.5.5')).toBe(false);
+ expect(sanitizeBoolean('true true')).toBe(false);
+ expect(sanitizeBoolean('true true.5')).toBe(false);
+ expect(sanitizeBoolean('true.5 true')).toBe(false);
+ expect(sanitizeBoolean('true.5 true.5')).toBe(false);
+
+ // @ts-expect-error intentional bad data
+ expect(sanitizeBoolean(1)).toBe(true);
});
});
diff --git a/src/sanitizers.ts b/src/sanitizers.ts
index f4595c9..3fc2f55 100644
--- a/src/sanitizers.ts
+++ b/src/sanitizers.ts
@@ -1,7 +1,7 @@
import sanitizeHtml from 'sanitize-html';
import validateColor from 'validate-color';
-function sanitizeNumber(input: string | number) {
+export function sanitizeNumber(input: string | number) {
const isValid = !/^\s*$/.test(String(input).trim()) && !Number.isNaN(Number(input));
if (isValid) {
return Number(input);
@@ -9,13 +9,13 @@ function sanitizeNumber(input: string | number) {
return null;
}
-function sanitizeString(input: string) {
+export function sanitizeString(input: string) {
let value = sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
value = value.replace(/["<>]+/g, '');
return value;
}
-function sanitizeColor(input: string) {
+export function sanitizeColor(input: string) {
const value = sanitizeString(input); // first remove any HTML
const isValidColor = validateColor(value);
if (isValidColor) {
@@ -23,6 +23,17 @@ function sanitizeColor(input: string) {
}
return null;
}
+
+export function sanitizeBoolean(input: string) {
+ if (String(input).toLowerCase() === 'true') {
+ return true;
+ }
+ if (String(input) === '1') {
+ return true;
+ }
+ return false;
+}
+
export const sanitizers = {
width: sanitizeNumber,
height: sanitizeNumber,
@@ -33,4 +44,5 @@ export const sanitizers = {
fontSize: sanitizeNumber,
bgColor: sanitizeColor,
textColor: sanitizeColor,
+ textWrap: sanitizeBoolean,
};
diff --git a/src/simple-svg-placeholder.d.ts b/src/simple-svg-placeholder.d.ts
deleted file mode 100644
index b8b853c..0000000
--- a/src/simple-svg-placeholder.d.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-declare module '@cloudfour/simple-svg-placeholder' {
- type Options = {
- width?: number;
- height?: number;
- text?: string;
- dy?: string;
- fontFamily?: string;
- fontWeight?: string | number;
- fontSize?: number;
- bgColor?: string;
- textColor?: string;
- dataUri?: boolean;
- charset?: string;
- };
- export default function generateSVG(options: Options): string;
- export type { Options };
-}
diff --git a/src/simple-svg-placeholder.ts b/src/simple-svg-placeholder.ts
new file mode 100644
index 0000000..27d28d4
--- /dev/null
+++ b/src/simple-svg-placeholder.ts
@@ -0,0 +1,81 @@
+// Heavily based on https://github.com/cloudfour/simple-svg-placeholder/tree/main
+// License: MIT
+
+export type Options = {
+ width?: number;
+ height?: number;
+ text?: string;
+ dy?: number;
+ fontFamily?: string;
+ fontWeight?: string | number;
+ fontSize?: number;
+ lineHeight?: number;
+ bgColor?: string;
+ textColor?: string;
+ dataUri?: boolean;
+ charset?: string;
+ textWrap?: boolean;
+ padding?: string;
+};
+
+
+export function simpleSvgPlaceholder({
+ width = 300,
+ height = 150,
+ text = `${width}Ã${height}`,
+ fontFamily = 'sans-serif',
+ fontWeight = 'bold',
+ fontSize = Math.floor(Math.min(width, height) * 0.2),
+ lineHeight = 1.2,
+ dy = fontSize * 0.35,
+ bgColor = '#ddd',
+ textColor = 'rgba(0,0,0,0.5)',
+ dataUri = true,
+ charset = 'utf8',
+ textWrap = false,
+ padding = '0.5em',
+}: Options = {}) {
+ let content = '';
+ if (textWrap) {
+ content = `
+ ${text}
+ `;
+ } else {
+ content = `${text} `;
+ }
+
+ const str = `
+
+ ${content}
+ `;
+
+ // Thanks to: filamentgroup/directory-encoder
+ const cleaned = str
+ .replace(/[\t\n\r]/gim, '') // Strip newlines and tabs
+ .replace(/\s\s+/g, ' ') // Condense multiple spaces
+ .replace(/'/gim, '\\i'); // Normalize quotes
+
+ if (dataUri) {
+ const encoded = encodeURIComponent(cleaned)
+ .replace(/\(/g, '%28') // Encode brackets
+ .replace(/\)/g, '%29');
+
+ return `data:image/svg+xml;charset=${charset},${encoded}`;
+ }
+
+ return cleaned;
+}
diff --git a/src/utils.ts b/src/utils.ts
index aea20a1..53c6d21 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -54,4 +54,19 @@ export class PoPsRewriter implements HTMLRewriterElementContentHandlers {
}
}
-export const availableImageOptions = ['width', 'height', 'text', 'dy', 'fontFamily', 'fontWeight', 'fontSize', 'bgColor', 'textColor'] as const;
+export const availableImageOptions = [
+ 'width',
+ 'height',
+ 'text',
+ 'dy',
+ 'fontFamily',
+ 'fontWeight',
+ 'fontSize',
+ 'bgColor',
+ 'textColor',
+ 'textWrap',
+] as const;
+
+export function getKeys(obj: T) {
+ return Object.keys(obj) as Array;
+}