// Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0s import { getWebServerEndpoint } from "@/util/endpoints"; import { boundNumber, isBlank } from "@/util/util"; import { generate as generateCSS, parse as parseCSS, walk as walkCSS } from "css-tree"; function encodeFileURL(file: string) { const webEndpoint = getWebServerEndpoint(); const fileUri = formatRemoteUri(file, "local"); const rtn = webEndpoint + `/wave/stream-file?path=${encodeURIComponent(fileUri)}&no404=1`; return rtn; } export function processBackgroundUrls(cssText: string): string { if (isBlank(cssText)) { return null; } cssText = cssText.trim(); if (cssText.endsWith(";")) { cssText = cssText.slice(0, -1); } const attrRe = /^background(-image)?\s*:\s*/i; cssText = cssText.replace(attrRe, ""); const ast = parseCSS("background: " + cssText, { context: "declaration", }); let hasUnsafeUrl = false; walkCSS(ast, { visit: "Url", enter(node) { const originalUrl = node.value.trim(); if ( originalUrl.startsWith("http:") || originalUrl.startsWith("https:") || originalUrl.startsWith("data:") ) { return; } // allow file:/// urls (if they are absolute) if (originalUrl.startsWith("file://")) { const path = originalUrl.slice(7); if (!path.startsWith("/")) { console.log(`Invalid background, contains a non-absolute file URL: ${originalUrl}`); hasUnsafeUrl = true; return; } const newUrl = encodeFileURL(path); node.value = newUrl; return; } // allow absolute paths if (originalUrl.startsWith("/") || originalUrl.startsWith("~/") || /^[a-zA-Z]:(\/|\\)/.test(originalUrl)) { const newUrl = encodeFileURL(originalUrl); node.value = newUrl; return; } hasUnsafeUrl = true; console.log(`Invalid background, contains an unsafe URL scheme: ${originalUrl}`); }, }); if (hasUnsafeUrl) { return null; } const rtnStyle = generateCSS(ast); if (rtnStyle == null) { return null; } return rtnStyle.replace(/^background:\s*/, ""); } export function computeBgStyleFromMeta(meta: MetaType, defaultOpacity: number = null): React.CSSProperties { const bgAttr = meta?.["bg"]; if (isBlank(bgAttr)) { return null; } try { const processedBg = processBackgroundUrls(bgAttr); const rtn: React.CSSProperties = {}; rtn.background = processedBg; rtn.opacity = boundNumber(meta["bg:opacity"], 0, 1) ?? defaultOpacity; if (!isBlank(meta?.["bg:blendmode"])) { rtn.backgroundBlendMode = meta["bg:blendmode"]; } return rtn; } catch (e) { console.error("error processing background", e); return null; } } export function formatRemoteUri(path: string, connection: string): string { connection = connection ?? "local"; // TODO: We need a better way to handle s3 paths let retVal: string; if (connection.startsWith("aws:")) { retVal = `${connection}:s3://${path ?? ""}`; } else { retVal = `wsh://${connection}/${path}`; } return retVal; }