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
5 changes: 5 additions & 0 deletions .github/actions/setup-build-test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ runs:
# Only truthy when licensed; empty string keeps unlicensed builds unscoped.
IG_LICENSED_BUILD: ${{ inputs.licensed == 'true' && 'true' || '' }}

# Needs the generated wrappers.
- name: Type-check tests
shell: bash
run: npm run typecheck:tests

- name: Validate dist packages
shell: bash
run: npm run validate:dist ${{ inputs.verbose == 'true' && '--loglevel verbose' || '' }}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"generate:grids": "tsx ./scripts/generate.ts grids",
"generate:grid-lite": "tsx ./scripts/generate.ts grid-lite",
"typecheck:scripts": "tsc --project ./scripts/tsconfig.json",
"typecheck:tests": "tsc --project ./tests/tsconfig.json",
"check:manifests": "tsx ./scripts/check-manifests.ts",
"validate:dist": "tsx ./scripts/validate-dist.ts",
"test:browser": "vitest",
Expand Down
7 changes: 7 additions & 0 deletions src/backfills.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isObject } from './is-object.js';

const DATA_CONTEXT_PROP = 'dataContext';
let once = false;

Expand All @@ -6,6 +8,11 @@ let once = false;
* the `dataContext` prop returning the original data
*/
export function withDataContext(data: unknown) {
// Proxy throws on primitives.
if (!isObject(data)) {
return data;
}

return new Proxy(data as any, {
get(target, prop, receiver) {
if (prop === DATA_CONTEXT_PROP) {
Expand Down
162 changes: 93 additions & 69 deletions src/equal.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,115 @@
export function equal<T>(a: unknown, b: T, visited = new WeakSet()): boolean {
import { isObject } from './is-object.js';

type Visited = WeakMap<object, WeakSet<object>>;

export function equal<T>(a: unknown, b: T, visited?: Visited): boolean {
// Early return
if (Object.is(a, b)) return true;
if (Object.is(a, b)) {
return true;
}

if (isObject(a) && isObject(b)) {
if (a.constructor !== b.constructor) return false;
if (!isObject(a) || !isObject(b)) {
return false;
}

// Circular references
if (visited.has(a) && visited.has(b)) {
return true;
}
if (a.constructor !== b.constructor) {
return false;
}

visited.add(a);
visited.add(b);

// RegExp
if (isRegExp(a) && isRegExp(b)) return a.source === b.source && a.flags === b.flags;

// Maps
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) return false;
for (const [keyA, valueA] of a.entries()) {
let found = false;
for (const [keyB, valueB] of b.entries()) {
if (equal(keyA, keyB, visited) && equal(valueA, valueB, visited)) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
// Track pairs, not objects: failed Set/Map probes must not count as visited.
const pairs: Visited = visited ?? new WeakMap();
const pending = pairs.get(a) ?? new WeakSet<object>();

// Sets
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) return false;
for (const valueA of a) {
let found = false;
for (const valueB of b) {
if (equal(valueA, valueB, visited)) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
if (pending.has(b)) {
return true;
}

pairs.set(a, pending.add(b));

try {
return compare(a, b, pairs);
} finally {
pending.delete(b);
}
}

function compare(a: object, b: object, visited: Visited): boolean {
// RegExp
if (isRegExp(a) && isRegExp(b)) {
return a.source === b.source && a.flags === b.flags;
}

// Map entries iterate as [key, value] arrays.
if ((a instanceof Map && b instanceof Map) || (a instanceof Set && b instanceof Set)) {
return a.size === b.size && matchOneToOne(a, b, visited);
}

// Arrays
if (Array.isArray(a) && Array.isArray(b)) {
const length = a.length;

if (length !== b.length) {
return false;
}

// Arrays
if (Array.isArray(a) && Array.isArray(b)) {
const length = a.length;
if (length !== b.length) return false;
for (let i = 0; i < length; i++) {
if (!equal(a[i], b[i], visited)) return false;
for (let i = 0; i < length; i++) {
if (!equal(a[i], b[i], visited)) {
return false;
}
return true;
}
return true;
}

// toPrimitive
if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
// Strings based
if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
// toPrimitive; absent on null-prototype objects.
if (typeof a.valueOf === 'function' && a.valueOf !== Object.prototype.valueOf) {
return a.valueOf() === b.valueOf();
}

const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
// Strings based
if (typeof a.toString === 'function' && a.toString !== Object.prototype.toString) {
return a.toString() === b.toString();
}

for (const key of aKeys) {
if (!Object.hasOwn(b, key)) return false;
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);

for (const key of aKeys) {
if (!equal(a[key as keyof typeof a], b[key as keyof typeof b], visited)) return false;
}
if (aKeys.length !== bKeys.length) {
return false;
}

visited.delete(a);
visited.delete(b);
for (const key of aKeys) {
if (!Object.hasOwn(b, key)) {
return false;
}
}

return true;
for (const key of aKeys) {
if (!equal(a[key as keyof typeof a], b[key as keyof typeof b], visited)) {
return false;
}
}

return false;
return true;
}

function isObject(value: unknown): value is object {
return value != null && typeof value === 'object';
/** Greedy suffices: `equal` is transitive. */
function matchOneToOne(
left: Iterable<unknown>,
right: Iterable<unknown>,
visited: Visited,
): boolean {
const pool = [...right];

for (const a of left) {
const index = pool.findIndex((b) => equal(a, b, visited));

if (index < 0) {
return false;
}
pool.splice(index, 1);
}

return true;
}

function isRegExp(value: unknown): value is RegExp {
Expand Down
5 changes: 3 additions & 2 deletions src/extras/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type { IgcChatMessage } from 'igniteui-webcomponents';
import type { MarkdownRendererOptions } from 'igniteui-webcomponents/extras';
import { setupMarkdownRenderer } from 'igniteui-webcomponents/extras';
import type { ReactNode } from 'react';

export type IgrChatMarkdownRendererOptions = MarkdownRendererOptions;

export async function createChatMarkdownRenderer(
options: IgrChatMarkdownRendererOptions,
): Promise<(message: IgcChatMessage) => Promise<unknown>> {
): Promise<(message: IgcChatMessage) => Promise<ReactNode>> {
const renderer = await setupMarkdownRenderer(options);

return async (message: IgcChatMessage): Promise<unknown> => {
return async (message: IgcChatMessage): Promise<ReactNode> => {
return (
<div
style={{ display: 'contents' }}
Expand Down
3 changes: 3 additions & 0 deletions src/is-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object';
}
Loading
Loading