From 39f68dde7ed936c53d90e512dc0a7b2314fbdbcf Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 9 Dec 2023 22:29:51 +0000 Subject: [PATCH 1/3] feat: add text wrapping option --- package-lock.json | 7 -- package.json | 4 +- public/index.html | 3 + src/index.test.ts | 75 +++++++++++-- src/index.ts | 9 +- src/sanitizers.test.ts | 190 +++++++++++++++++++------------- src/sanitizers.ts | 18 ++- src/simple-svg-placeholder.d.ts | 17 --- src/simple-svg-placeholder.ts | 81 ++++++++++++++ src/utils.ts | 17 ++- 10 files changed, 301 insertions(+), 120 deletions(-) delete mode 100644 src/simple-svg-placeholder.d.ts create mode 100644 src/simple-svg-placeholder.ts 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..90fe32f 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; +} From 9ab1d21ed91979418c1ca156a1a570e4194f6a56 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 9 Dec 2023 22:35:09 +0000 Subject: [PATCH 2/3] docs: update index --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 90fe32f..d2eb1aa 100644 --- a/public/index.html +++ b/public/index.html @@ -228,7 +228,7 @@

Available API Options

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

From f550cde35f35883c35d597963f72c1f1edfa90b3 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 9 Dec 2023 22:38:29 +0000 Subject: [PATCH 3/3] chore: bump codeql version --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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