Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v0.2.33] - 2026-01-12

### Added
- Add registry for `Blob`s as `blob:` URIs
- Add `useScopedStyle()`
- Add `stringify` inside `useScopedStyle()` and convert `Blob`s to `blob:` using `registerBlob()`

### Deprecated
- Deprecated `createStyleScope()`

## [v0.2.32] - 2026-01-09

### Changed
Expand Down
53 changes: 53 additions & 0 deletions blob-registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const SOURCE_REGISTRY = new WeakMap();

/**
* Create and register a `blob:` URI for a source.
*
* @param {Blob} source The `Blob` to regsiter.
* @returns {string} A `blob:` URI for the Object Source.
* @throws {TypeError} If the `source` is an invalid type.
*/
export function registerBlob(source) {
if (! (source instanceof Blob)) {
throw new TypeError('Expected a `Blob`');
} else if (SOURCE_REGISTRY.has(source)) {
return SOURCE_REGISTRY.get(source);
} else {
const uri = URL.createObjectURL(source);
SOURCE_REGISTRY.set(source, uri);
return uri;
}
}

/**
* Revoke an Object URL and remove it from the registry.
*
* @param {any} source The Object registered for a `blob:` URI.
* @returns {boolean} Whether or not the URI was revoked and unregistered.
*/
export function unregisterBlob(source) {
if (SOURCE_REGISTRY.has(source)) {
const uri = SOURCE_REGISTRY.get(source);
URL.revokeObjectURL(uri);
SOURCE_REGISTRY.delete(source);
return true;
} else {
return false;
}
}

/**
* Get a registered `blob:` URI for a Source Object.
*
* @param {Blob} source The `Blob` the URI is registered to.
* @returns {string|undefined} The corresponding `blob:` URI.
*/
export const getBlobURL = source => SOURCE_REGISTRY.get(source);

/**
* Check if an Object Source has a registered `blob:` URI.
*
* @param {Blob} source The `Blob` to check for.
* @returns {boolean} Whether or not the source has a registered `blob:` URI.
*/
export const hasBlobURL = source => SOURCE_REGISTRY.has(source);
4 changes: 3 additions & 1 deletion core.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export {
} from '@aegisjsproject/callback-registry/callbackRegistry.js';

export {
text, createStyleSheet, createCSSParser, css, lightCSS, darkCSS ,
text, createStyleSheet, createCSSParser, css, lightCSS, darkCSS, useScopedStyle,
styleSheetToFile, styleSheetToLink, createHTMLParser, html, doc, trustedHTML,
htmlUnsafe, docUnsafe, htmlToFile, createTrustedHTMLTemplate, xml, svg, json, math, url,
createShadowParser, shadow, styledShadow, el, createBoundParser, adoptStyles, prefixCSSRules, createStyleScope,
Expand All @@ -36,6 +36,8 @@ export {
registerComponent, getRegisteredComponentTags, getRegisteredComponents,
} from './componentRegistry.js';

export { registerBlob, unregisterBlob, getBlobURL, hasBlobURL } from './blob-registry.js';

export { createComponent, clone } from './component.js';

export * from '@aegisjsproject/state/state.js';
Expand Down
3 changes: 1 addition & 2 deletions default-policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ if (! ('trustedTypes' in globalThis && trustedTypes.createPolicy instanceof Func
...rest
} = {}) {
const el = document.createElement('div');

el.setHTML(input, { elements, attributes, comments, dataAttributes, ...rest });
el.setHTML(input, { sanitizer: { elements, attributes, comments, dataAttributes, ...rest }});

return el.innerHTML;
},
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aegisjsproject/core",
"version": "0.2.32",
"version": "0.2.33",
"description": "A fast, secure, modern, light-weight, and simple JS library for creating web components and more!",
"keywords": [
"aegis",
Expand Down
2 changes: 1 addition & 1 deletion parsers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { text } from './parsers/text.js';
export {
createStyleSheet, createCSSParser, css, lightCSS, prefixCSSRules,
createStyleSheet, createCSSParser, css, lightCSS, prefixCSSRules, useScopedStyle,
darkCSS, styleSheetToFile, styleSheetToLink, createBoundParser, adoptStyles, createStyleScope,
} from './parsers/css.js';
export {
Expand Down
77 changes: 57 additions & 20 deletions parsers/css.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
import { createStyleSheet, createCSSParser, css, setStyleSheets, addStyleSheets } from '@aegisjsproject/parsers/css.js';
import { registerBlob } from '../blob-registry.js';

function stringify(thing) {
switch(typeof thing) {
case 'string':
case 'number':
case 'bigint':
return thing;

case 'undefined':
case 'symbol':
return '';

case 'object':
if (thing instanceof Blob) {
return `url(${registerBlob(thing)})`;
} else if (thing instanceof URL) {
return `url(${thing.href})`;
} else if (thing === null) {
return '';
} else {
return thing.toString();
}
default:
return thing.toString();
}
}

const PREFIX = '_aegis_style_scope_';

/**
* Creates a scoped CSSStyleSheet attached to the provided root and returns a
* tagged template function for generating unique class names within that scope.
* Creates a scoped CSSStyleSheet and returns a tagged template function for generating
* unique class names within that scope.
*
* @param {Object} [options] Configuration options.
* @param {string} [options.baseURL] The base URL used to resolve relative URLs in the stylesheet.
* @param {string|MediaList|MediaQueryList} [options.media] The intended media for the stylesheet (e.g., "screen", "print").
* @param {boolean} [options.disabled] Whether the stylesheet is disabled by default.
* @param {string} [options.prefix] A custom prefix for generated class names.
* @returns {readonly [CSSStyleSheet, (strings: TemplateStringsArray, ...values: any[]) => string]} A tagged template function that
* inserts a new CSS rule and returns the generated class name, along with the `CSSStyleSheet` created.
*/
export function useScopedStyle({ baseURL, media, disabled, prefix = PREFIX } = {}) {
const sheet = new CSSStyleSheet({ baseURL, media: media instanceof MediaQueryList ? media.media : media, disabled });

return Object.freeze([sheet, (strings, ...values) => {
const uuid = crypto.randomUUID();
const className = `${prefix}${uuid}`;
const rule = `.${CSS.escape(prefix) + uuid} { ${String.raw(strings, ...values.map(stringify))} }`;
sheet.insertRule(rule, sheet.cssRules.length);

return className;
}]);
}

/**
* @deprecated
* @param {Document | ShadowRoot | Element} [root=document] The DOM scope to attach the stylesheet to.
* If an Element is passed, its root node (Document or ShadowRoot) is used.
*
Expand All @@ -18,23 +68,10 @@ const PREFIX = '_aegis_style_scope_';
* inserts a new CSS rule and returns the generated class name.
*/
export function createStyleScope(root = document, { baseURL, media, disabled, prefix = PREFIX } = {}) {
if (root instanceof Element) {
return createStyleScope(root.getRootNode(), { baseURL, media, disabled, prefix });
} else if (! (root instanceof Document || root instanceof ShadowRoot)) {
throw new TypeError('Root must be a Document, ShadowRoot, or Element.');
} else {
const sheet = new CSSStyleSheet({ baseURL, media: media instanceof MediaQueryList ? media.media : media, disabled });
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];

return (strings, ...values) => {
const uuid = crypto.randomUUID();
const className = `${prefix}${uuid}`;
const rule = `.${CSS.escape(prefix) + uuid} { ${String.raw(strings, ...values)} }`;
sheet.insertRule(rule, sheet.cssRules.length);

return className;
};
}
console.warn('`createStyleScope()` is deprecated. Please use `useScopedStyle()` instead.');
const result = useScopedStyle({ baseURL, media, disabled, prefix });
root.adoptedStyleSheets = [...document.adoptedStyleSheets, result[0]];
return result[1];
}

export const lightCSS = createCSSParser({ media: '(prefers-color-scheme: light)', baseURL: document.baseURI });
Expand Down Expand Up @@ -89,7 +126,7 @@ export function styleSheetToLink(styleSheet) {
link.media = styleSheet.media.mediaText;
}

link.href = URL.createObjectURL(file);
link.href = registerBlob(file);

return link;
}
Expand Down
3 changes: 2 additions & 1 deletion stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { createCallback } from '@aegisjsproject/callback-registry/callbackRegistry.js';
import { registerSignal, registerController } from '@aegisjsproject/callback-registry/callbackRegistry.js';
import { stringifyAttr, createAttribute } from './dom.js';
import { registerBlob } from './blob-registry.js';

const toData = ([name, val]) => ['data-' + name.replaceAll(/[A-Z]/g, c => `-${c.toLowerCase()}`), val];

Expand Down Expand Up @@ -116,7 +117,7 @@ export const stringify = thing => {
document.adoptedStyleSheets = [...document.adoptedStyleSheets, thing];
break;
} else if (thing instanceof Blob) {
return URL.createObjectURL(thing);
return registerBlob(thing);
} else if(thing instanceof Date) {
return formatDate(thing);
} else if (thing instanceof DOMTokenList) {
Expand Down
9 changes: 5 additions & 4 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
html, css, replaceStyles, getUniqueSelector, createComponent, closeRegistration,
data, attr, createTrustedHTMLTemplate, createStyleScope,
data, attr, createTrustedHTMLTemplate, useScopedStyle,
} from '@aegisjsproject/core';

import { sanitizer as defaultSanitizer } from '@aegisjsproject/sanitizer/config/html.js';
Expand Down Expand Up @@ -44,8 +44,10 @@ document.body.setAttribute(fooEvent, FUNCS.debug.log);
document.body.dataset[stateKey] = 'bg';
document.body.dataset[stateStyle] = 'background-color';

const [sheet, style] = useScopedStyle({ media: matchMedia('(min-width: 800px)')});

replaceStyles(document, ...document.adoptedStyleSheets, reset, baseTheme, lightTheme, darkTheme, btn, btnPrimary, btnDanger, btnWarning,
btnInfo, btnSystemAccent, btnSuccess, btnLink, btnSecondary,
btnInfo, btnSystemAccent, btnSuccess, btnLink, btnSecondary, sheet,
css`.${scope} {
color: red;
}
Expand All @@ -59,8 +61,6 @@ replaceStyles(document, ...document.adoptedStyleSheets, reset, baseTheme, lightT
}
`);

const style = createStyleScope(document.body, { media: matchMedia('(min-width: 800px)')});

const DadJoke = await customElements.whenDefined('dad-joke');
const frag = document.createDocumentFragment();
const h1 = document.createElement('h1');
Expand All @@ -83,6 +83,7 @@ try {
rgba(20, 20, 20, 0.7) 0%,
rgba(20, 20, 20, 0.4) 100%
);
background-image: ${blob};
backdrop-filter: blur(8px);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
Expand Down
Loading