From 9ad2ca0d3cb94988dcbffae4b4a5cfe90af392e0 Mon Sep 17 00:00:00 2001 From: Guz013 Date: Thu, 30 Nov 2023 17:53:51 -0300 Subject: [PATCH] =?UTF-8?q?feat(banners-lib):=20=E2=9C=A8=20basic=20custom?= =?UTF-8?q?=20icon=20fetching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/banners/src/index.js | 44 +++++++++++++++++++++++++++++++++-- packages/banners/src/utils.js | 39 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 packages/banners/src/utils.js diff --git a/packages/banners/src/index.js b/packages/banners/src/index.js index 5d59d48..e9ebce4 100644 --- a/packages/banners/src/index.js +++ b/packages/banners/src/index.js @@ -2,6 +2,7 @@ * @typedef {import('./index.js').BannerObject} BannerObject */ import getLocalLayout from './layouts.js'; +import { isValidIcon } from './utils.js'; /** * @param {Readonly} string - The string to be converted. @@ -29,13 +30,26 @@ function htmlToString(element, document) { /** * @typedef {{ - * modify(query: string, callback: (el: Element | null) => T): T + * modify(query: string, callback: (el: Element | null) => T): T, + * asyncModify(query: string, callback: (el: Element | null) => Promise): Promise, * }} DOMHelper * @param {Element} element - The element to be manipulated. * @returns {DOMHelper} */ function domHelper(element) { return { + /** + * @template T + * @param {string} query - The query selector to find the element. + * @param {(el: Element | null) => Promise} callback - Callback to modify the element. + * @returns {Promise} - The return value of the callback. + * @throws {Error} - Throws if the element is not found. + */ + async asyncModify(query, callback) { + const el = element.querySelector(query); + + return callback(el); + }, /** * @template T * @param {string} query - The query selector to find the element. @@ -115,12 +129,36 @@ async function banner(object) { /** @type {Document} */ // @ts-expect-error because Document is not compatible with Readonly const doc = object.lib?.document ?? globalThis.document; + /** @type {(info: URL | RequestInfo, init?: RequestInit) => Promise} */ + // @ts-expect-error because fetch is Readonly in Banner object; + const lFetch = object.lib?.fetch ?? globalThis.fetch; /** @type {Readonly} */ - const svg = await getLocalLayout('horizontal', true); + const svg = await getLocalLayout('horizontal'); const dom = stringToHtml(svg, doc); const helper = domHelper(dom); + await helper.asyncModify('[data-banner-class="icon"]', async (el) => { + if (!el || !object.icon || !isValidIcon(object.icon)) return; + + const [ iconSet, iconName ] = object.icon.split(':'); + + const res = await lFetch(`https://api.iconify.design/${iconSet}/${iconName}.svg`); + + const resSvg = stringToHtml(await res.text(), doc); + + resSvg.setAttribute('x', '22'); + resSvg.setAttribute('y', '33'); + resSvg.setAttribute('width', '13'); + resSvg.setAttribute('height', '13'); + + if (resSvg.children[0].getAttribute('fill') === 'currentColor') + resSvg.children[0].setAttribute('fill', '#000000'); + + // eslint-disable-next-line require-atomic-updates + el.innerHTML = htmlToString(resSvg, doc); + }); + helper.modify('[data-banner-class="title"] > tspan', (el) => { if (!el) return; @@ -153,11 +191,13 @@ async function banner(object) { */ async function test() { const testBanner = await banner({ + icon: 'solar:4k-bold', lib: { // @ts-expect-error because Document is not DeepReadonly document: new Document(), fetch, }, + subtitle: 'this is a test with icon', title: 'Hello, world', }); diff --git a/packages/banners/src/utils.js b/packages/banners/src/utils.js new file mode 100644 index 0000000..7aff9c2 --- /dev/null +++ b/packages/banners/src/utils.js @@ -0,0 +1,39 @@ + +/** + * Checks if a given string is a URL. + * + * @param {Readonly} string - The string to be checked. + * @returns {boolean} + */ +function isURL(string) { + try { + const url = new URL(string); + + return url.protocol === 'http' || url.protocol === 'https'; + } + catch { + return false; + } +} + +/** + * Checks if a given string is a valid Iconify's icon name. + * + * @param {string} string - The string to be checked. + * @returns {boolean} + */ +function isValidIcon(string) { + if (string.includes('--')) return false; + + // eslint-disable-next-line no-secrets/no-secrets + const VALID_CHARS = 'abcdefghijklmnopqrstuvwxyz1234567890-:'; + if ([...string].some(l => !VALID_CHARS.includes(l))) + return false; + + if (!string.includes(':')) return false; + + return true; +} + +export { isURL, isValidIcon }; +