diff --git a/src/components/icon/library.default.ts b/src/components/icon/library.default.ts
index 81d20b9bc..aac2eb1c7 100644
--- a/src/components/icon/library.default.ts
+++ b/src/components/icon/library.default.ts
@@ -39,28 +39,30 @@ function getIconUrl(name: string, family: string, variant: string) {
}
// Icons used for internal components, prefetched for speed
-export const fetched: IconLibraryCache<1, 3> = {
- solid: {
- check: ``,
- 'chevron-down': ``,
- 'chevron-left': ``,
- 'chevron-right': ``,
- circle: ``,
- 'eye-dropper': ``,
- 'grip-vertical': ``,
- indeterminate: ``,
- minus: ``,
- pause: ``,
- play: ``,
- star: ``,
- user: ``,
- xmark: ``,
- },
- regular: {
- 'circle-xmark': ``,
- copy: ``,
- eye: ``,
- 'eye-slash': ``,
+export const fetched: IconLibraryCache<2> = {
+ classic: {
+ solid: {
+ check: ``,
+ 'chevron-down': ``,
+ 'chevron-left': ``,
+ 'chevron-right': ``,
+ circle: ``,
+ 'eye-dropper': ``,
+ 'grip-vertical': ``,
+ indeterminate: ``,
+ minus: ``,
+ pause: ``,
+ play: ``,
+ star: ``,
+ user: ``,
+ xmark: ``,
+ },
+ regular: {
+ 'circle-xmark': ``,
+ copy: ``,
+ eye: ``,
+ 'eye-slash': ``,
+ },
},
};
diff --git a/src/components/icon/library.ts b/src/components/icon/library.ts
index 8a74ec508..fbdcc7665 100644
--- a/src/components/icon/library.ts
+++ b/src/components/icon/library.ts
@@ -1,9 +1,15 @@
-import { deepEntries } from '../../utilities/deep.js';
+import { flatten } from '../../utilities/deep.js';
import type WaIcon from '../icon/icon.js';
import defaultLibrary from './library.default.js';
-import type { IconLibrary, IconLibraryCache, IconLibraryResolver, UnregisteredIconLibrary } from './types.d.ts';
+import type {
+ IconLibrary,
+ IconLibraryCache,
+ IconLibraryFetched,
+ IconLibraryResolver,
+ UnregisteredIconLibrary,
+} from './types.d.ts';
-export type { IconLibrary, IconLibraryCache, UnregisteredIconLibrary } from './types.d.ts';
+export type { IconLibrary, IconLibraryCache, IconLibraryFetched, UnregisteredIconLibrary } from './types.d.ts';
let registry: IconLibrary[] = [];
let watchedIcons: WaIcon[] = [];
@@ -34,10 +40,10 @@ export function registerIconLibrary(name: string, options: UnregisteredIconLibra
resolver: options.resolver,
mutator: options.mutator,
spriteSheet: options.spriteSheet,
- fetched: flattenIconLibraryCache(options.resolver, options.fetched),
- addFetched(cache: IconLibraryCache<1, 3>) {
+ fetched: options.fetched ? flattenIconLibraryCache(options.fetched, options.resolver) : {},
+ addFetched(cache: IconLibraryFetched) {
// Convert flat URL → markup cache to deep family → variant → icon name → markup cache
- let flatCache = flattenIconLibraryCache(this.resolver, cache);
+ let flatCache = flattenIconLibraryCache(cache, this.resolver);
this.fetched ??= {};
Object.assign(this.fetched, flatCache);
},
@@ -51,21 +57,19 @@ export function registerIconLibrary(name: string, options: UnregisteredIconLibra
});
}
-/** Convert deep family → variant → icon name → markup cache to flat URL → markup cache */
-function flattenIconLibraryCache(resolver: IconLibraryResolver, cache?: IconLibraryCache<1, 3>): IconLibraryCache<1> {
- if (!cache) {
- return {};
- }
-
- return Object.fromEntries(
- deepEntries(cache, {
- transformPath: (path: string[]) => {
- let name = path.pop()!;
- let [family, variant] = path;
- return [resolver(name, family, variant)];
- },
- }),
- );
+/**
+ * Convert the deep family → variant → icon name → markup cache that is more convenient to provide
+ * to the flat URL → markup cache that icon libraries use internally
+ **/
+function flattenIconLibraryCache(cache: IconLibraryFetched, resolver: IconLibraryResolver): IconLibraryCache<0> {
+ return flatten(cache, {
+ getKey(path: string[]) {
+ // name is always the last value no matter the depth
+ let name = path.pop()!;
+ let [family, variant] = path;
+ return resolver(name, family, variant);
+ },
+ }) as IconLibraryCache<0>;
}
/** Removes an icon library from the registry. */
diff --git a/src/components/icon/types.d.ts b/src/components/icon/types.d.ts
index f0c9621ad..e250880e2 100644
--- a/src/components/icon/types.d.ts
+++ b/src/components/icon/types.d.ts
@@ -5,10 +5,19 @@ export type IconLibraryMutator = (svg: SVGElement) => void;
// e.g., Decrement[3] yields 2
type Decrement = [never, 0, 1, 2];
-// Record of string → string or nested Record
-export type IconLibraryCache = Min extends 0
- ? string | (Max extends 0 ? never : Record>)
- : Record>;
+/**
+ * Record of string → string or nested string → Record
+ * The number indicates how many params we have; none (0), just family (1), or family and style (2).
+ * IconLibraryCache<0> is Record (name → markup)
+ * IconLibraryCache<1> is Record> (family → name → markup)
+ * IconLibraryCache<2> is Record>> (family → variant → name → markup)
+ */
+export type IconLibraryCache = Record<
+ string,
+ N extends 0 ? string : IconLibraryCache
+>;
+
+export type IconLibraryFetched = IconLibraryCache<0> | IconLibraryCache<1> | IconLibraryCache<2>;
export interface UnregisteredIconLibrary {
resolver: IconLibraryResolver;
@@ -17,7 +26,7 @@ export interface UnregisteredIconLibrary {
// Max depth: family → variant → icon name → markup
// but may be shallower for libraries that don't use variants or families
- fetched?: IconLibraryCache<1, 3>;
+ fetched?: IconLibraryFetched;
}
// Registered icon library
@@ -28,6 +37,6 @@ export interface IconLibrary {
spriteSheet?: boolean;
// One level only: URL → markup
- fetched?: IconLibraryCache<1>;
- addFetched: (cache: IconLibraryCache<1, 3>) => void;
+ fetched?: IconLibraryCache<0>;
+ addFetched: (cache: IconLibraryFetched) => void;
}
diff --git a/src/utilities/deep.ts b/src/utilities/deep.ts
index cfc9b3b06..e0320dbb4 100644
--- a/src/utilities/deep.ts
+++ b/src/utilities/deep.ts
@@ -1,6 +1,3 @@
-type Property = keyof any;
-export type EachCallback = (value: any, key: Property, parent: object, path: Property[]) => any;
-
export function isPlainObject(obj: any) {
return isObject(obj, 'Object');
}
@@ -14,70 +11,149 @@ export function isObject(obj: any, type: string) {
return proto.constructor?.name === type;
}
-/**
- * Iterate over a deep array, recursively for plain objects
- * @param obj The object to iterate over. Can be an array or a plain object, or even a primitive value.
- * @param callback. value is === parent[key]
- * @param parentObj The parent object of the current value Mainly used internally to facilitate recursion.
- * @param key The key of the current value. Mainly used internally to facilitate recursion.
- * @param path Any existing path (not including the key). Mainly used internally to facilitate recursion.
- */
-export function deepEach(obj: any, callback: EachCallback, parentObj?: object, key?: Property, path: Property[] = []) {
- if (key !== undefined) {
- let ret = callback(obj, key, parentObj!, path);
+type Property = keyof any;
+export type EachCallback = (value: any, path: Property[], parent?: object) => T;
+export type EachOptions = {
+ filter?: EachCallback;
+ descend?: EachCallback;
+};
- if (ret !== undefined) {
- if (ret === false) {
- // Do not descend further
- return;
+/**
+ * Iterate over a deep object or array recursively
+ *
+ * @param obj The object to iterate over. Can be a plain object,or array, or even a primitive value.
+ * @param callback. The callback to execute for each value. Will not be called on circular references.
+ * Returning `false` will stop the iteration and a non-undefined value will overwrite the value.
+ */
+export function deepEach(obj: any, callback: EachCallback, options: EachOptions = {}): any {
+ // Used to track circular references
+ // WeakSet is used to avoid memory leaks
+ let visited = new WeakSet();
+
+ return _deepEach(
+ obj,
+ (value, path, parent) => {
+ if (value && typeof value === 'object') {
+ if (visited.has(value)) {
+ // Abort mission
+ return false;
+ }
+
+ visited.add(value);
}
- // Overwrite value
- // @ts-expect-error
- parentObj[key] = ret;
- obj = ret;
+ return callback(value, path, parent);
+ },
+ options,
+ );
+}
+
+/**
+ * Private recursive function to support `deepEach()`.
+ * @param value Same as {@link deepEach}
+ * @param callback Same as {@link deepEach}
+ * @param path The path to `value` from the root object as an array of keys.
+ * @param parent The parent object of the current value. `value === parent[path.at(-1)]`
+ */
+function _deepEach(value: any, callback: EachCallback, options: EachOptions, path: Property[] = [], parent?: object) {
+ if (path.length > 0) {
+ let included = options.filter?.(value, path, parent) ?? true;
+
+ if (included) {
+ let ret = callback(value, path, parent!);
+
+ if (ret !== undefined) {
+ // Overwrite value
+ let key = path.at(-1);
+ // @ts-expect-error TS doesn't know that if path.length > 0, parent is an object
+ value = parent[key] = ret;
+ }
}
}
- let newPath = key !== undefined ? [...path, key] : path;
+ let isArray = Array.isArray(value);
+ let isObject = !isArray && isPlainObject(value);
+ let isContainer = isArray || isObject;
- if (Array.isArray(obj)) {
- for (let i = 0; i < obj.length; i++) {
- deepEach(obj[i], callback, obj, i, newPath);
+ if (isContainer) {
+ let descend = options.descend?.(value, path, parent) ?? true;
+
+ if (!descend) {
+ // Do not descend further
+ return;
}
- } else if (isPlainObject(obj)) {
- for (let key in obj) {
- deepEach(obj[key], callback, obj, key, newPath);
+
+ if (isArray) {
+ for (let i = 0; i < value.length; i++) {
+ _deepEach(value[i], callback, options, [...path, i], value);
+ }
+ } else if (isObject) {
+ for (let key in value) {
+ _deepEach(value[key], callback, options, [...path, key], value);
+ }
}
}
}
export type DeepEntriesOptions = {
filter?: EachCallback;
- transformPath?: (path: Property[]) => Property[];
};
/**
* Like Object.entries, but for deeply nested objects.
* For shallow objects the output is the same as Object.entries.
+ * Circular references will not be included.
* @param obj The object to iterate over.
* @param options.filter - If this returns false, the entry is not added to the result.
- * @param options.transformPath - Transform the path, e.g. to serialize it to a single key.
- * @returns Array of arrays. In each array the last value is the value, and all values before that are the keys.
+ * @param options.transformPath - Transform the path, e.g. to serialize it to a single key or reduce levels.
+ * @returns Array of arrays. In each item, the last value is the value, and all values before that are the keys.
+ * So for an object with N levels, the result will be an array of arrays with N+1 items each.
*/
export function deepEntries(obj: any, options: DeepEntriesOptions = {}): any[][] {
- let { filter, transformPath } = options;
+ let { filter } = options;
let entries: any[][] = [];
- deepEach(obj, (value, key, parent, path) => {
- let included = filter?.(value, key, parent, path) ?? true;
+ deepEach(obj, (value, path, parent) => {
+ let included = filter?.(value, path, parent) ?? true;
if (included) {
- let fullPath = [...path, key];
- path = transformPath?.(fullPath) ?? fullPath;
- entries.push([...fullPath, value]);
+ entries.push([...path, value]);
}
});
return entries;
}
+
+export type FlattenOptions = {
+ getKey?: (path: Property[], value?: any) => Property;
+};
+
+/**
+ * Convert a potentially deeply nested object to a flat object.
+ * Circular references will not be included.
+ * @param obj
+ * @param options.getKey - A function to transform the path to a key. The default is to join the path with '.'.
+ */
+export function flatten(
+ obj: Record,
+ options: FlattenOptions = {},
+): Record {
+ let { getKey = path => path.join('.') } = options;
+
+ if (!obj || typeof obj !== 'object') {
+ return {} as Record;
+ }
+
+ let entries = deepEntries(obj).map(pathAndValue => {
+ if (pathAndValue.length < 2) {
+ return null;
+ }
+
+ let value = pathAndValue.pop() as T;
+ let path = pathAndValue as Property[];
+ let key = getKey(path, value);
+ return [key, value];
+ }) as [string, T][];
+
+ return Object.fromEntries(entries.filter(Boolean)) as Record;
+}