Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214const URL_FUNC_RE = /\b(?:url|image-set|-webkit-image-set|image|cross-fade|element)\s*\(/i;
export function assertSafeCssValue(property, value) { if (URL_FUNC_RE.test(value)) { throw new Error(`disallowed url() in ${property}`); }}
// Rejects all url()/image-set()/etc, matching the stylesheet ruleexport function assertSafeSvgValue(attribute, value) { if (typeof value !== "string") return; if (URL_FUNC_RE.test(value)) { throw new Error(`disallowed url() in ${attribute}`); } if (value.includes("\\")) { throw new Error(`disallowed escape in ${attribute}`); }}
export function assertSafeInlineStyleValue(property, value) { // Reject any backslash so a CSS-escape-encoded identifier (e.g. `\75rl(...)`) // can't slip past URL_FUNC_RE if (value.includes("\\")) { throw new Error(`disallowed escape in ${property}`); } assertSafeCssValue(property, value);}
export function validatePluginCss(text) { const sheet = new CSSStyleSheet(); sheet.replaceSync(text); walk(sheet.cssRules); return sheet;}
function walk(rules) { for (const rule of rules) { if (rule instanceof CSSImportRule) throw new Error("@import not allowed"); if (rule instanceof CSSFontFaceRule) throw new Error("@font-face not allowed"); if (rule instanceof CSSNamespaceRule) throw new Error("@namespace not allowed");
if (rule.style) { for (const prop of rule.style) { assertSafeCssValue(prop, rule.style.getPropertyValue(prop)); } }
if (rule.cssRules) walk(rule.cssRules); }}
function cssStringLiteral(value) { return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;}
const FONT_STYLES = new Set(["normal", "italic", "oblique"]);const FONT_WEIGHT_KEYWORDS = new Set(["normal", "bold"]);
function isValidFontWeightNumber(value) { if (!/^\d{1,4}$/.test(value)) return false; const num = Number(value); return num >= 1 && num <= 1000;}
function isValidFontWeight(value) { if (typeof value !== "string") return false; if (FONT_WEIGHT_KEYWORDS.has(value)) return true; const parts = value.trim().split(/\s+/); if (parts.length === 1) return isValidFontWeightNumber(parts[0]); if (parts.length === 2) { return ( isValidFontWeightNumber(parts[0]) && isValidFontWeightNumber(parts[1]) && Number(parts[0]) <= Number(parts[1]) ); } return false;}
function buildFontFaceRule({ family, weight, style, url, file }) { const format = /\.woff2$/i.test(file) ? "woff2" : "woff"; const lines = [ `font-family: ${cssStringLiteral(family)};`, `src: url(${cssStringLiteral(url)}) format(${cssStringLiteral(format)});`, `font-weight: ${weight};`, `font-style: ${style};`, ]; return `@font-face { ${lines.join(" ")} }`;}
export class PluginStylesLoader { constructor() { this._manifestSheets = new Map(); this._snippetSheets = new Map(); this._fontSheets = new Map(); this._fontUrls = new Map(); }
mountFonts(pluginId, descriptors) { if (!descriptors || descriptors.length === 0) { this._unmountFonts(pluginId); return; } const normalized = descriptors.map((desc, i) => { const weight = desc.weight ?? "400"; const style = desc.style ?? "normal"; if (!isValidFontWeight(weight)) { throw new Error(`fonts[${i}] invalid weight "${weight}"`); } if (!FONT_STYLES.has(style)) { throw new Error(`fonts[${i}] invalid style "${style}"`); } return { ...desc, weight, style }; }); this._unmountFonts(pluginId); const urls = []; const ruleTexts = []; for (const desc of normalized) { const url = URL.createObjectURL(desc.blob); urls.push(url); ruleTexts.push( buildFontFaceRule({ family: desc.family, weight: desc.weight, style: desc.style, url, file: desc.file, }), ); } const sheet = new CSSStyleSheet(); sheet.replaceSync(ruleTexts.join("\n")); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; this._fontSheets.set(pluginId, sheet); this._fontUrls.set(pluginId, urls); }
_unmountFonts(pluginId) { const sheet = this._fontSheets.get(pluginId); if (sheet) { document.adoptedStyleSheets = document.adoptedStyleSheets.filter( (entry) => entry !== sheet, ); this._fontSheets.delete(pluginId); } const urls = this._fontUrls.get(pluginId); if (urls) { for (const url of urls) URL.revokeObjectURL(url); this._fontUrls.delete(pluginId); } }
mount(pluginId, cssText) { if (this._manifestSheets.has(pluginId)) this._unmountManifest(pluginId); const sheet = validatePluginCss(cssText); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; this._manifestSheets.set(pluginId, sheet); }
unmount(pluginId) { this._unmountManifest(pluginId); const snippets = this._snippetSheets.get(pluginId); if (snippets && snippets.size) { const toRemove = new Set(snippets.values()); document.adoptedStyleSheets = document.adoptedStyleSheets.filter( (entry) => !toRemove.has(entry), ); } this._snippetSheets.delete(pluginId); this._unmountFonts(pluginId); }
mountSnippet(pluginId, snippetId, cssText) { const sheet = validatePluginCss(cssText); let snippets = this._snippetSheets.get(pluginId); if (!snippets) { snippets = new Map(); this._snippetSheets.set(pluginId, snippets); } const existing = snippets.get(snippetId); if (existing) { document.adoptedStyleSheets = document.adoptedStyleSheets.filter( (entry) => entry !== existing, ); } document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; snippets.set(snippetId, sheet); }
unmountSnippet(pluginId, snippetId) { const snippets = this._snippetSheets.get(pluginId); if (!snippets) return; const sheet = snippets.get(snippetId); if (!sheet) return; document.adoptedStyleSheets = document.adoptedStyleSheets.filter( (entry) => entry !== sheet, ); snippets.delete(snippetId); if (snippets.size === 0) this._snippetSheets.delete(pluginId); }
_unmountManifest(pluginId) { const sheet = this._manifestSheets.get(pluginId); if (!sheet) return; document.adoptedStyleSheets = document.adoptedStyleSheets.filter( (entry) => entry !== sheet, ); this._manifestSheets.delete(pluginId); }}