diff --git a/components/Card.tsx b/components/Card.tsx index 6eff7b4..b662bca 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -1,9 +1,9 @@ import { isYoutubeLink } from "@lib/string.ts"; import { IconBrandYoutube } from "@components/icons.tsx"; -import { GenericResource } from "@lib/types.ts"; import { SmallRating } from "@components/Rating.tsx"; import { Link } from "@islands/Link.tsx"; import { parseRating } from "@lib/helpers.ts"; +import { GenericResource, getNameOfResource } from "@lib/marka/schema.ts"; export function Card( { @@ -101,14 +101,16 @@ export function ResourceCard( ? `/api/images?image=${img}&width=200&height=200` : "/placeholder.svg"; + const rating = res.content.reviewRating?.ratingValue + ? parseRating(res.content.reviewRating.ratingValue) + : undefined; + return ( diff --git a/components/MetaTags.tsx b/components/MetaTags.tsx index 1ec5601..b811f16 100644 --- a/components/MetaTags.tsx +++ b/components/MetaTags.tsx @@ -1,5 +1,6 @@ -import { GenericResource } from "@lib/types.ts"; import { Head } from "$fresh/runtime.ts"; +import { GenericResource, getNameOfResource } from "@lib/marka/schema.ts"; +import { formatDate } from "@lib/string.ts"; function generateJsonLd(resource: GenericResource): string { const imageUrl = resource.content?.image @@ -8,34 +9,34 @@ function generateJsonLd(resource: GenericResource): string { const baseSchema: Record = { "@context": "https://schema.org", - "@type": resource.content?._type, // Converts type to PascalCase + "@type": resource.content?._type, name: resource.name, - description: resource.content || resource.meta?.average || "", - keywords: resource.tags?.join(", ") || "", + description: resource.content || "", + keywords: resource.content.keywords?.join(", ") || "", image: imageUrl, }; - if (resource.meta?.author) { + if (resource.content?.author) { baseSchema.author = { "@type": "Person", - name: resource.meta.author, + name: resource.content.author, }; } - if (resource.meta?.date) { + if (resource.content?.datePublished) { try { - baseSchema.datePublished = new Date(resource.meta.date).toISOString(); + baseSchema.datePublished = formatDate( + new Date(resource.content.datePublished), + ); } catch (_) { // Ignore invalid date } } - if (resource.meta?.rating) { - baseSchema.aggregateRating = { - "@type": "AggregateRating", - ratingValue: resource.meta.rating, - ratingCount: 1, - bestRating: 5, // Assuming a scale of 1 to 10 + if (resource.content?.reviewRating) { + baseSchema.reviewRating = { + "@type": "Rating", + ...resource.content.reviewRating, }; } @@ -51,7 +52,7 @@ export function MetaTags({ resource }: { resource: GenericResource }) { return ( <> - + s && (typeof s === "string" ? s?.length > 1 : true) ).map((s) => { + if (!s) return; if (typeof s === "string") { - return {s}; + return {s}; } else { - return {s.title}; + return {s.title}; } })} diff --git a/components/layouts/main.tsx b/components/layouts/main.tsx index 6f655db..f7f4884 100644 --- a/components/layouts/main.tsx +++ b/components/layouts/main.tsx @@ -1,6 +1,6 @@ import { ComponentChildren } from "preact"; import Search from "@islands/Search.tsx"; -import { GenericResource } from "@lib/types.ts"; +import { GenericResource } from "@lib/marka/schema.ts"; export type Props = { children: ComponentChildren; @@ -21,7 +21,7 @@ export const MainLayout = ( if (hasSearch) { return ( diff --git a/deno.json b/deno.json index 85a66fb..697a747 100644 --- a/deno.json +++ b/deno.json @@ -22,6 +22,7 @@ "@lib": "./lib", "@lib/": "./lib/", "@libsql/client": "npm:@libsql/client@^0.14.0", + "@openai/openai": "jsr:@openai/openai@^6.7.0", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1", "@std/http": "jsr:@std/http@^1.0.12", diff --git a/islands/IngredientsList.tsx b/islands/IngredientsList.tsx index c7513f1..d267f49 100644 --- a/islands/IngredientsList.tsx +++ b/islands/IngredientsList.tsx @@ -9,15 +9,12 @@ function formatAmount(num: number) { } function formatUnit(unit: string, amount: number) { - const unitKey = unit.toLowerCase() as keyof typeof unitsOfMeasure; + if (!unit) return ""; + const unitKey = unit.toLowerCase() as (keyof typeof unitsOfMeasure); if (unitKey in unitsOfMeasure) { if (amount > 1 && unitsOfMeasure[unitKey].plural !== undefined) { return unitsOfMeasure[unitKey].plural; } - if (unitKey !== "cup") { - return unitsOfMeasure[unitKey].short; - } - return unitKey.toString(); } else { return unit; @@ -66,9 +63,22 @@ export const IngredientsList: FunctionalComponent< {ingredients.map((item) => { - return ( - - ); + if ("items" in item) { + return item.items.map((ing, i) => { + return ( + + ); + }); + } else { + return ( + + ); + } })}
diff --git a/islands/KMenu.tsx b/islands/KMenu.tsx index e3ee48b..a55ca5c 100644 --- a/islands/KMenu.tsx +++ b/islands/KMenu.tsx @@ -103,8 +103,9 @@ export const KMenu = ( } useEventListener("keydown", (ev: KeyboardEvent) => { + const target = ev.target as HTMLElement; if (ev.key === "k") { - if (ev?.target?.nodeName == "INPUT") { + if (target.nodeName == "INPUT") { return; } diff --git a/islands/Link.tsx b/islands/Link.tsx index 014e184..e34f8c5 100644 --- a/islands/Link.tsx +++ b/islands/Link.tsx @@ -1,7 +1,6 @@ import { useEffect } from "preact/hooks"; declare global { - // deno-lint-ignore no-var var loadingTimeout: ReturnType | undefined; } diff --git a/islands/Recommendations.tsx b/islands/Recommendations.tsx index 7cac972..19b428b 100644 --- a/islands/Recommendations.tsx +++ b/islands/Recommendations.tsx @@ -1,5 +1,6 @@ import { useCallback, useState } from "preact/hooks"; import { IconWand } from "@components/icons.tsx"; +import { RecommendationResource } from "@lib/recommendation.ts"; type RecommendationState = "disabled" | "loading"; @@ -11,7 +12,7 @@ export function Recommendations( }, ) { const [state, setState] = useState("disabled"); - const [results, setResults] = useState(); + const [results, setResults] = useState(); const startFetch = useCallback( async () => { @@ -44,9 +45,9 @@ export function Recommendations(
-

{res.title}

+

{res.id}

); })} @@ -66,6 +67,7 @@ export function Recommendations( {!results && state === "disabled" && ( - * - * // Ensure `batchLog` is invoked once after 1 second of debounced calls. - * const debounced = useDebouncedCallback(batchLog, 250, { 'maxWait': 1000 }) - * const source = new EventSource('/stream') - * source.addEventListener('message', debounced) - * - * // Cancel the trailing debounced invocation. - * window.addEventListener('popstate', debounced.cancel) - * - * // Check for pending invocations. - * const status = debounced.pending() ? "Pending..." : "Ready" - */ export default function useDebouncedCallback< - T extends (...args: any) => ReturnType, + T extends (...args: unknown[]) => unknown, >( - func: T, - wait?: number, - options?: Options, -): DebouncedState { - const lastCallTime = useRef(null); - const lastInvokeTime = useRef(0); - const timerId = useRef(null); - const lastArgs = useRef([]); - const lastThis = useRef(); - const result = useRef>(); - const funcRef = useRef(func); - const mounted = useRef(true); + callback: T, + delay: number, + options?: { + /** Call on the leading edge. Default: false */ + leading?: boolean; + /** Call on the trailing edge. Default: true */ + trailing?: boolean; + }, +): Debounced { + const callbackRef = useRef(callback); + const timerRef = useRef(null); + const argsRef = useRef | null>(null); + // Always use the latest callback without re-creating the debounced fn useEffect(() => { - funcRef.current = func; - }, [func]); + callbackRef.current = callback; + }, [callback]); - // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. - const useRAF = !wait && wait !== 0 && typeof window !== "undefined"; + const leading = !!options?.leading; + const trailing = options?.trailing !== false; // default true - if (typeof func !== "function") { - throw new TypeError("Expected a function"); - } - - wait = +wait || 0; - options = options || {}; - - const leading = !!options.leading; - const trailing = "trailing" in options ? !!options.trailing : true; // `true` by default - const maxing = "maxWait" in options; - const maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : null; - - useEffect(() => { - mounted.current = true; - return () => { - mounted.current = false; - }; - }, []); - - // You may have a question, why we have so many code under the useMemo definition. - // - // This was made as we want to escape from useCallback hell and - // not to initialize a number of functions each time useDebouncedCallback is called. - // - // It means that we have less garbage for our GC calls which improves performance. - // Also, it makes this library smaller. - // - // And the last reason, that the code without lots of useCallback with deps is easier to read. - // You have only one place for that. - const debounced = useMemo(() => { - const invokeFunc = (time: number) => { - const args = lastArgs.current; - const thisArg = lastThis.current; - - lastArgs.current = lastThis.current = null; - lastInvokeTime.current = time; - return (result.current = funcRef.current.apply(thisArg, args)); - }; - - const startTimer = (pendingFunc: () => void, wait: number) => { - if (useRAF) cancelAnimationFrame(timerId.current); - timerId.current = useRAF - ? requestAnimationFrame(pendingFunc) - : setTimeout(pendingFunc, wait); - }; - - const shouldInvoke = (time: number) => { - if (!mounted.current) return false; - - const timeSinceLastCall = time - lastCallTime.current; - const timeSinceLastInvoke = time - lastInvokeTime.current; - - // Either this is the first call, activity has stopped and we're at the - // trailing edge, the system time has gone backwards and we're treating - // it as the trailing edge, or we've hit the `maxWait` limit. - return ( - !lastCallTime.current || - timeSinceLastCall >= wait || - timeSinceLastCall < 0 || - (maxing && timeSinceLastInvoke >= maxWait) - ); - }; - - const trailingEdge = (time: number) => { - timerId.current = null; - - // Only invoke if we have `lastArgs` which means `func` has been - // debounced at least once. - if (trailing && lastArgs.current) { - return invokeFunc(time); + const debounced = useMemo>(() => { + const clear = () => { + if (timerRef.current != null) { + clearTimeout(timerRef.current); + timerRef.current = null; } - lastArgs.current = lastThis.current = null; - return result.current; }; - const timerExpired = () => { - const time = Date.now(); - if (shouldInvoke(time)) { - return trailingEdge(time); + const invoke = () => { + const a = argsRef.current; + argsRef.current = null; + if (a) { + callbackRef.current(...a); } - // https://github.com/xnimorz/use-debounce/issues/97 - if (!mounted.current) { - return; + }; + + const fn = ((...args: Parameters) => { + const shouldCallLeading = leading && timerRef.current == null; + + argsRef.current = args; + + if (timerRef.current != null) clearTimeout(timerRef.current); + + timerRef.current = globalThis.setTimeout(() => { + timerRef.current = null; + if (trailing) invoke(); + }, delay); + + if (shouldCallLeading) { + // Leading edge call happens immediately + invoke(); } - // Remaining wait calculation - const timeSinceLastCall = time - lastCallTime.current; - const timeSinceLastInvoke = time - lastInvokeTime.current; - const timeWaiting = wait - timeSinceLastCall; - const remainingWait = maxing - ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) - : timeWaiting; + }) as Debounced; - // Restart the timer - startTimer(timerExpired, remainingWait); + fn.cancel = () => { + argsRef.current = null; + clear(); }; - const func: DebouncedState = (...args: Parameters): ReturnType => { - const time = Date.now(); - const isInvoking = shouldInvoke(time); - - lastArgs.current = args; - lastThis.current = this; - lastCallTime.current = time; - - if (isInvoking) { - if (!timerId.current && mounted.current) { - // Reset any `maxWait` timer. - lastInvokeTime.current = lastCallTime.current; - // Start the timer for the trailing edge. - startTimer(timerExpired, wait); - // Invoke the leading edge. - return leading ? invokeFunc(lastCallTime.current) : result.current; - } - if (maxing) { - // Handle invocations in a tight loop. - startTimer(timerExpired, wait); - return invokeFunc(lastCallTime.current); - } + fn.flush = () => { + if (timerRef.current != null) { + clear(); + invoke(); } - if (!timerId.current) { - startTimer(timerExpired, wait); - } - return result.current; }; - func.cancel = () => { - if (timerId.current) { - useRAF - ? cancelAnimationFrame(timerId.current) - : clearTimeout(timerId.current); - } - lastInvokeTime.current = 0; - lastArgs.current = - lastCallTime.current = - lastThis.current = - timerId.current = - null; - }; + fn.pending = () => timerRef.current != null; - func.isPending = () => { - return !!timerId.current; - }; + return fn; + // Recreate only if timing/edge behavior changes + }, [delay, leading, trailing]); - func.flush = () => { - return !timerId.current ? result.current : trailingEdge(Date.now()); - }; - - return func; - }, [leading, maxing, wait, maxWait, trailing, useRAF]); + // Cancel on unmount + useEffect(() => () => debounced.cancel(), [debounced]); return debounced; } diff --git a/lib/hooks/useEventListener.ts b/lib/hooks/useEventListener.ts index 16d37bd..d7470d4 100644 --- a/lib/hooks/useEventListener.ts +++ b/lib/hooks/useEventListener.ts @@ -6,7 +6,7 @@ export function useEventListener( element: typeof globalThis | HTMLElement = globalThis, ) { // Create a ref that stores handler - const savedHandler = useRef<(event: Event) => void>(); + const savedHandler = useRef<(event: T) => void>(); // Update ref.current value if handler changes. // This allows our effect below to always get latest handler ... @@ -27,11 +27,11 @@ export function useEventListener( const eventListener = (event: T) => savedHandler?.current?.(event); // Add event listener - element.addEventListener(eventName, eventListener); + element.addEventListener(eventName, (ev) => eventListener(ev as T)); // Remove event listener on cleanup return () => { - element.removeEventListener(eventName, eventListener); + element.removeEventListener(eventName, (ev) => eventListener(ev as T)); }; }, [eventName, element], // Re-run if eventName or element changes diff --git a/lib/hooks/useThrottledCallback.ts b/lib/hooks/useThrottledCallback.ts index 64b99f0..4fe9dd6 100644 --- a/lib/hooks/useThrottledCallback.ts +++ b/lib/hooks/useThrottledCallback.ts @@ -6,7 +6,7 @@ type ThrottleOptions = { }; const useThrottledCallback = ( - callback: (...args: any[]) => void, + callback: (...args: unknown[]) => void, delay: number, options: ThrottleOptions = {}, ) => { @@ -24,7 +24,7 @@ const useThrottledCallback = ( }; }, [timer]); - const throttledCallback = (...args: any[]) => { + const throttledCallback = (...args: unknown[]) => { const now = Date.now(); if (leading && !isLeading) { diff --git a/lib/hooks/useTraceProps.ts b/lib/hooks/useTraceProps.ts index e76f4e2..b5a2c9f 100644 --- a/lib/hooks/useTraceProps.ts +++ b/lib/hooks/useTraceProps.ts @@ -1,14 +1,17 @@ import { useEffect, useRef } from "preact/hooks"; -export function useTraceUpdate(props) { +export function useTraceUpdate(props: Record) { const prev = useRef(props); useEffect(() => { - const changedProps = Object.entries(props).reduce((ps, [k, v]) => { - if (prev.current[k] !== v) { - ps[k] = [prev.current[k], v]; - } - return ps; - }, {}); + const changedProps = Object.entries(props).reduce( + (ps: Record, [k, v]) => { + if (prev.current[k] !== v) { + ps[k] = [prev.current[k], v]; + } + return ps; + }, + {}, + ); if (Object.keys(changedProps).length > 0) { console.log("Changed props:", changedProps); } diff --git a/lib/image.ts b/lib/image.ts index 5fc0046..ef02e41 100644 --- a/lib/image.ts +++ b/lib/image.ts @@ -134,7 +134,7 @@ async function getLocalImage( */ async function storeLocalImage( url: string, - content: ArrayBuffer, + content: Uint8Array | ArrayBuffer, { width, height }: { width?: number; height?: number } = {}, ) { const isValid = await verifyImage(new Uint8Array(content)); @@ -249,7 +249,7 @@ async function verifyImage(imageBuffer: Uint8Array): Promise { export async function getImageContent( url: string, { width, height }: { width?: number; height?: number } = {}, -): Promise<{ content: ArrayBuffer; mimeType: string }> { +): Promise<{ content: Uint8Array; mimeType: string }> { log.debug("Getting image content", { url, width, height }); // Check if we have the image metadata in database @@ -267,8 +267,8 @@ export async function getImageContent( // Fetch and cache original if needed if (!originalImage) { const fetchedImage = await getRemoteImage(url); - await storeLocalImage(url, fetchedImage.buffer); originalImage = new Uint8Array(fetchedImage.buffer); + await storeLocalImage(url, originalImage); } // Resize image diff --git a/lib/marka/schema.ts b/lib/marka/schema.ts index a704b56..ce7d8d8 100644 --- a/lib/marka/schema.ts +++ b/lib/marka/schema.ts @@ -68,6 +68,7 @@ export const ReviewContentSchema = makeContentSchema("Review", { export const RecipeContentSchema = makeContentSchema("Recipe", { description: z.string().optional(), name: z.string().optional(), + reviewRating: ReviewRatingSchema.optional(), recipeIngredient: z.array(z.string()).optional(), recipeInstructions: z.array(z.string()).optional(), totalTime: z.string().optional(), @@ -124,3 +125,16 @@ export type RecipeResource = z.infer & { export type GenericResource = z.infer & { image?: typeof imageTable.$inferSelect; }; + +export function getNameOfResource(res: GenericResource): string { + if (res.content?._type === "Article" && res.content.headline) { + return res.content.headline; + } + if (res.content?._type === "Review" && res.content.itemReviewed?.name) { + return res.content.itemReviewed.name; + } + if (res.content?._type === "Recipe" && res.content.name) { + return res.content.name; + } + return "Unnamed Resource"; +} diff --git a/lib/openai.ts b/lib/openai.ts index ac1e4a0..969db6b 100644 --- a/lib/openai.ts +++ b/lib/openai.ts @@ -1,5 +1,5 @@ -import OpenAI from "https://deno.land/x/openai@v4.69.0/mod.ts"; -import { zodResponseFormat } from "https://deno.land/x/openai@v4.69.0/helpers/zod.ts"; +import OpenAI, { toFile } from "@openai/openai"; +import { zodResponseFormat } from "@openai/openai/helpers/zod"; import { OPENAI_API_KEY } from "@lib/env.ts"; import { hashString } from "@lib/helpers.ts"; import { createCache } from "@lib/cache.ts"; @@ -216,7 +216,7 @@ export async function createTags(content: string) { export async function extractRecipe(content: string) { if (!openAI) return; - const completion = await openAI.beta.chat.completions.parse({ + const completion = await openAI.chat.completions.parse({ model: model, temperature: 0.1, messages: [ @@ -234,7 +234,7 @@ export async function extractRecipe(content: string) { export async function extractArticleMetadata(content: string) { if (!openAI) return; - const completion = await openAI.beta.chat.completions.parse({ + const completion = await openAI.chat.completions.parse({ model: model, temperature: 0.1, messages: [ @@ -259,7 +259,7 @@ export async function transcribe( ): Promise { if (!openAI) return; - const file = new File([mp3Data], "audio.mp3", { + const file = await toFile(mp3Data, "audio.mp3", { type: "audio/mpeg", }); diff --git a/lib/parseIngredient.ts b/lib/parseIngredient.ts index 669e2b1..e46156e 100644 --- a/lib/parseIngredient.ts +++ b/lib/parseIngredient.ts @@ -1,7 +1,7 @@ import { parseIngredient, unitsOfMeasure as _unitsOfMeasure, -} from "npm:parse-ingredient"; +} from "parse-ingredient"; import { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts"; import { removeMarkdownFormatting } from "@lib/string.ts"; @@ -77,7 +77,7 @@ export function parseIngredients( }; const unit = ingredient.unit.toLowerCase() as keyof typeof unitsOfMeasure; - if (unit in unitsOfMeasure && unit !== "cup") { + if (unit in unitsOfMeasure) { ingredient.unit = unitsOfMeasure[unit].short; } diff --git a/lib/recommendation.ts b/lib/recommendation.ts index 38cde98..cc1987b 100644 --- a/lib/recommendation.ts +++ b/lib/recommendation.ts @@ -2,9 +2,9 @@ import * as openai from "@lib/openai.ts"; import * as tmdb from "@lib/tmdb.ts"; import { parseRating } from "@lib/helpers.ts"; import { createCache } from "@lib/cache.ts"; -import { GenericResource, ReviewResource } from "./marka/schema.ts"; +import { ReviewResource } from "./marka/schema.ts"; -type RecommendationResource = { +export type RecommendationResource = { id: string; type: string; rating: number; @@ -96,5 +96,5 @@ export async function getAllRecommendations(): Promise< > { const keys = cache.keys(); const res = await Promise.all(keys.map((k) => cache.get(k))); - return res.map((r) => JSON.parse(r)); + return res.filter((s) => !!s).map((r) => JSON.parse(r)); } diff --git a/lib/search.ts b/lib/search.ts index 084448f..339a6e6 100644 --- a/lib/search.ts +++ b/lib/search.ts @@ -1,8 +1,9 @@ import { resources } from "@lib/resources.ts"; -import fuzzysort from "npm:fuzzysort"; +import fuzzysort from "fuzzysort"; import { extractHashTags } from "@lib/string.ts"; import { listResources } from "./marka/index.ts"; import { GenericResource } from "./marka/schema.ts"; +import { parseRating } from "./helpers.ts"; type ResourceType = keyof typeof resources; @@ -72,8 +73,8 @@ export async function searchResource( if ( !(resource.name in results) && - rating && resource?.content?.reviewRating && - resource.content?.reviewRating?.ratingValue >= rating + rating && resource.content.reviewRating?.ratingValue && + parseRating(resource.content.reviewRating.ratingValue) >= rating ) { results[resource.name] = resource; } diff --git a/lib/string.ts b/lib/string.ts index 0e63caa..e277f0f 100644 --- a/lib/string.ts +++ b/lib/string.ts @@ -1,5 +1,3 @@ -import { resources } from "@lib/resources.ts"; - export function formatDate(date: Date): string { const options = { year: "numeric", month: "long", day: "numeric" } as const; return new Intl.DateTimeFormat("en-US", options).format(date); diff --git a/lib/types.ts b/lib/types.ts index 4597993..b5bcf91 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,4 +1,4 @@ -import { GenericResource, GenericResourceSchema } from "./marka/schema.ts"; +import { GenericResource } from "./marka/schema.ts"; export interface TMDBMovie { adult: boolean; @@ -39,7 +39,7 @@ export interface GiteaOauthUser { preferred_username: string; email: string; picture: string; - groups: any; + groups: unknown; } export type SearchResult = { diff --git a/lib/webScraper.ts b/lib/webScraper.ts index 1ed6e45..5fd85a6 100644 --- a/lib/webScraper.ts +++ b/lib/webScraper.ts @@ -11,12 +11,14 @@ export function absolutizeDomUrls(dom: JSDOM, domain: string): void { const base = toBase(domain); const rewrite = (selector: string, attr: string) => { - document.querySelectorAll(selector).forEach((el) => { - const v = el.getAttribute(attr); - if (!v) return; - const abs = toAbsolute(v, base); - if (abs !== v) el.setAttribute(attr, abs); - }); + document.querySelectorAll(selector).forEach( + (el: HTMLElement) => { + const v = el.getAttribute(attr); + if (!v) return; + const abs = toAbsolute(v, base); + if (abs !== v) el.setAttribute(attr, abs); + }, + ); }; // Common URL attributes @@ -41,35 +43,35 @@ export function absolutizeDomUrls(dom: JSDOM, domain: string): void { rewrite("form[action]", "action"); rewrite("video[poster]", "poster"); - // srcset (img, source) document - .querySelectorAll("img[srcset], source[srcset]") - .forEach((el) => { + .querySelectorAll("img[srcset], source[srcset]") + .forEach((el: HTMLImageElement) => { const v = el.getAttribute("srcset"); if (!v) return; const abs = absolutizeSrcset(v, base); if (abs !== v) el.setAttribute("srcset", abs); }); - // Inline CSS in style attributes: url(...) - document.querySelectorAll("[style]").forEach((el) => { - const v = el.getAttribute("style"); - if (!v) return; - const abs = absolutizeCssUrls(v, base); - if (abs !== v) el.setAttribute("style", abs); - }); + document.querySelectorAll("[style]").forEach( + (el: HTMLElement) => { + const v = el.getAttribute("style"); + if (!v) return; + const abs = absolutizeCssUrls(v, base); + if (abs !== v) el.setAttribute("style", abs); + }, + ); - //